RS485通信:從硬件電路到軟件調試全解析)
簡介面向嵌入式開發(fā)者與自動化工程師提供STM32F103實現(xiàn)RS485通信的完整工程源碼包。工程基于標準外設庫實現(xiàn)了UART初始化、波特率與數(shù)據(jù)位配置、485方向引腳DE/RE自動切換、發(fā)送完成與接收中斷處理并包含超時/溢出等錯誤檢測邏輯讀者可據(jù)此掌握RS485半雙工通信的收發(fā)時序控制方法。資源共122個文件以C源碼、H頭文件、啟動匯編、Keil工程配置為主附帶可直接燒錄的hex文件、PDF與doc說明文檔、清除編譯中間文件的bat腳本資源包大小約826KB。已有5507人學習。壓縮包內目錄結構清晰演示了結合MAX485芯片從硬件連接到軟件配置、再到串口助手調試的完整流程并提示終端電阻匹配、總線沖突避免、多節(jié)點地址識別等工程注意事項適合競賽備賽、課程設計和實際項目快速移植。 不知道你有沒有經(jīng)歷過這種情況在車間里調一臺伺服驅動器電腦和PLC都在總線上你的STM32開發(fā)板插上去數(shù)據(jù)發(fā)不出去偶爾收到一堆亂碼甚至整個總線都被“拉死”。把波特率改低、把線重新接一遍還是不行。折騰一晚上最后發(fā)現(xiàn)是方向控制引腳沒拉對或者A、B兩根線接反了。這種場景在工業(yè)現(xiàn)場太常見了而它背后的主角就是RS485。RS485這些年幾乎成了工業(yè)通信的默認選項我的STM32項目里十個有八個都要用到它從Modbus儀表、伺服驅動器、變頻器到溫濕度傳感器、電表、條碼槍只要是長距離、多節(jié)點、抗干擾要求高的場景基本都是485上場。這篇就圍繞STM32如何實現(xiàn)485通信來寫從硬件電路選型到軟件收發(fā)狀態(tài)機再到現(xiàn)場調試最容易踩的坑一次說清。1. RS485為什么這么多項目在用設計前要想清楚什么1.1 485相比232、422、I2C、SPI到底強在哪先說個最容易混淆的點RS485不是一種像SPI、I2C那樣的完整通信協(xié)議它只是物理層標準。你跑在RS485上的還是UART那套數(shù)據(jù)幀格式波特率、起始位、數(shù)據(jù)位、停止位這些和串口一模一樣。所以你在STM32上做485本質就是調一個UART外設再額外控制一下收發(fā)方向。那為什么不用RS232主要差在三點。第一是距離RS232一般15米就到頭了RS485在低速情況下能到1200米工廠車間幾十米上百米的布線非常常見。第二是多節(jié)點RS232天生只能點對點RS485通過總線掛載標準負載下能掛32個節(jié)點。第三是抗干擾RS485用差分信號傳輸兩根線A和B互為參考外界噪聲同時疊加到兩根線上接收端只判斷A-B的電壓差共模干擾被抵消掉這在變頻器、電機旁邊尤其重要。和I2C、SPI這類板級總線比485的優(yōu)勢在于它把物理層放到了外部收發(fā)器芯片上電平是差分信號不是單端電平所以能長距離傳輸。SPI的SCLK、MISO、MOSI三根線跑到半米開外就各種丟數(shù)據(jù)485上百米還能穩(wěn)定運行。說到底選485不是因為它的協(xié)議有多先進而是它在工業(yè)現(xiàn)場這種惡劣條件下用最少的線兩根和最便宜的成本實現(xiàn)了可靠的長距離多點通信。這就是它幾十年還沒被淘汰的根本原因。1.2 半雙工的方向控制是整個設計的核心矛盾485總線是半雙工工作方式同一時刻總線上只能有一個節(jié)點在發(fā)送其他節(jié)點都在接收。如果兩個節(jié)點同時拉高發(fā)送總線就會沖突數(shù)據(jù)全亂。這就是為什么每個485節(jié)點都要有一個方向控制的概念。收發(fā)器芯片內部其實有兩個放大器一個把總線上的差分信號轉換為單端信號送給STM32的RX引腳另一個把STM32的TX引腳的信號轉換為差分信號輸出到總線。這兩個放大器由芯片上的DE引腳和RE引腳控制。DE是發(fā)送使能高電平有效RE是接收使能低電平有效。很多芯片像MAX485、SP3485內部已經(jīng)把DE和RE邏輯反相處理好了所以你可以把DE和RE直接短接成一個引腳拉高就是發(fā)送模式拉低就是接收模式。這里有一個設計方案的選擇可以用GPIO軟件控制方向也可以用帶自動方向檢測功能的收發(fā)器比如MAX13487來自動切換。自動方向芯片看起來省事少占一個GPIO但在高速、長線、總線節(jié)點多的場景下方向切換時序不完全受控偶爾會出莫名其妙的丟幀。我自己的習慣是用GPIO手動控制方向代碼只多兩行但每一個時序細節(jié)都在自己手里排查問題的時候心里有底。后面所有代碼和電路都按手動GPIO控制方向的方案來講。2. 硬件電路這么接才不容易翻車2.1 收發(fā)器芯片選型與外圍電路做STM32項目時收發(fā)器芯片一般從MAX485、SP3485、MAX3485里選。STM32是3.3V供電所以最推薦SP3485或者MAX3485它們是3.3V供電版本RO和DI電平可以直接和STM32的GPIO對接不需要電平轉換電路。MAX485是5V供電的如果系統(tǒng)里本來就有5V電源也能用但RO輸出高電平是5V直接進STM32的RX引腳會有風險最好串聯(lián)電阻或者用分壓處理。節(jié)約成本沒問題但穩(wěn)定性優(yōu)先的話選3.3V版本最省心。典型的外圍電路這樣接STM32的TX引腳接收發(fā)器的DIRX引腳接收發(fā)器的RO。收發(fā)器的RE和DE短接后接一個STM32的空閑GPIO比如PB0。A和B兩根線引出到接線端子A接雙絞線的一根B接另一根。在A和B之間加一個120歐終端電阻。在A線上加一個上拉電阻到3.3VB線上加一個下拉電阻到GND一般用1k到10k。這個偏置電阻的作用是保證總線空閑時A-B的電壓差不在不確定區(qū)否則接收端會輸出隨機電平產(chǎn)生亂碼。A、B線上各串一個TVS管到GND保護收發(fā)器芯片不被靜電或浪涌打壞。終端電阻和偏置電阻的取舍值得展開說一下。120歐終端電阻是為了匹配雙絞線的特性阻抗減少信號反射。但它只應該加在總線物理線路的兩端不是每個節(jié)點都加。如果你的STM32節(jié)點正好位于總線末端那就在你的板子上加如果在中間就別加加了反而會增大負載拉低信號幅度。一個典型的Modbus總線最遠端的兩個設備各加一個120歐就對了。偏置電阻很多人會忽略但它恰恰是發(fā)數(shù)據(jù)正??臻e時亂碼這類問題的關鍵。阻值選多大取決于總線上掛了多少個節(jié)點因為每個節(jié)點的收發(fā)器都有輸入阻抗會分壓。標準做法是讓總線上所有偏置電阻并聯(lián)后在A-B之間形成的靜態(tài)壓差大于200mV。按經(jīng)驗一個節(jié)點典型偏置用10k到20k也能工作但如果總線長、節(jié)點多建議用1k到4.7k更穩(wěn)。我通常直接用2k效果穩(wěn)定。2.2 與STM32的接線及共地問題接線端子這里有個特別容易踩的坑RS485連接必須A對A、B對B不能像RS232那樣交叉。很多人第一次用485轉USB工具接STM32板子順手按232的習慣把A接B、B接A結果數(shù)據(jù)完全不通還以為是波特率問題。用萬用表量一下波形就明白了A、B接反時波形是反相的接收到的數(shù)據(jù)全是亂碼。共地問題更要重視。RS485雖然是差分傳輸?shù)珒蓚€通信節(jié)點之間仍然需要一個共同的參考地。工業(yè)現(xiàn)場經(jīng)常出現(xiàn)的情況是兩個設備分別接了不同的開關電源GND之間壓差達到幾伏甚至幾十伏這時候A、B線上的共模電壓會超過收發(fā)器的承受范圍輕則通信不正常重則直接燒掉收發(fā)器芯片。所以你的STM32板子和對端設備之間除了A、B兩根線還必須拉一根GND線把兩個系統(tǒng)的地兜在一起。記住這句話485隔離的是設備之間的信號干擾不是地電位地線該接還得接。3. 軟件部分狀態(tài)機、時序、代碼實現(xiàn)3.1 用CubeMX配置USART和方向控制引腳軟件部分用STM32的HAL庫來做。CubeMX里配置很簡單以USART1為例打開USART1模式選Asynchronous異步波特率按你的從機設備要求來一般Modbus設備默認9600或1152008位數(shù)據(jù)、無校驗、1位停止位8N1是標準配置。使能USART1全局中斷。PB0配置為GPIO_Output初始電平設為低電平。這里的關鍵是初始狀態(tài)必須是低電平也就是接收模式。如果初始化為高電平STM32一上電就會讓收發(fā)器進入發(fā)送模式把總線占住其他節(jié)點全被拉死這就是常見的一種總線異?,F(xiàn)象的根源。生成工程后在main.c里把方向控制引腳的設置寫成宏定義方便后面調用。3.2 發(fā)送邏輯先拉方向再發(fā)數(shù)據(jù)發(fā)完再拉回來發(fā)送的過程很簡單但順序絕對錯不得先拉高方向引腳、再發(fā)數(shù)據(jù)、等發(fā)送徹底完成、最后拉低方向引腳。只要順序錯一步就會出現(xiàn)最后一字節(jié)被截斷或總線一直被占用的問題。基礎的阻塞發(fā)送函數(shù)這樣寫#define RS485_DE_HIGH() HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET) #define RS485_DE_LOW() HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET) void RS485_Send_Data(uint8_t *data, uint16_t len) { RS485_DE_HIGH(); HAL_UART_Transmit(huart1, data, len, 100); HAL_Delay(1); RS485_DE_LOW(); }HAL_UART_Transmit是阻塞發(fā)送內部會等待發(fā)送完成標志TCTransmission Complete也就是最后一個字節(jié)已經(jīng)從移位寄存器里完全挪出去了。但為什么發(fā)完之后還要加個HAL_Delay(1)再拉低方向因為我實際測試下來雖然TC已經(jīng)置位但在高速主頻下UART外設和GPIO之間還有一點硬件動作余量個別芯片在發(fā)送完成后立即切換方向時最后一個字節(jié)的停止位仍然會被切掉。保險起見加一兩個字節(jié)的發(fā)送時間再切方向。波特率9600時一個字節(jié)約1ms115200時約0.1msDelay(1)完全夠用。如果對實時性要求高你完全可以用DWT計數(shù)器做一個微秒級延時去掉這個1ms但大多數(shù)場景沒必要。如果你用的是中斷發(fā)送或DMA發(fā)送那就不能在函數(shù)尾部拉低方向因為數(shù)據(jù)可能還在DMA搬運中函數(shù)已經(jīng)返回了。這時候要在HAL_UART_TxCpltCallback發(fā)送完成回調里拉低方向。這是新手最容易搞錯的地方必須單獨強調。3.3 接收邏輯中斷接收加空閑判斷湊成一幀接收部分用中斷方式循環(huán)接收再用串口空閑中斷IDLE來判定一幀數(shù)據(jù)結束。它的原理是485總線上一幀數(shù)據(jù)內部的字節(jié)間隔很短總線空閑狀態(tài)超過一個閾值時UART外設會產(chǎn)生IDLE中斷。這樣你不需要知道這一幀有多長只要總線上安靜了就認為當前這一幀已經(jīng)收完。先定義幾個緩沖區(qū)變量uint8_t rx_buffer[256]; volatile uint16_t rx_len 0; volatile uint8_t frame_ready 0; uint8_t rx_byte;接著在初始化里啟動接收HAL_UART_Receive_IT(huart1, rx_byte, 1);然后實現(xiàn)接收完成回調和中斷服務函數(shù)void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (rx_len sizeof(rx_buffer)) { rx_buffer[rx_len] rx_byte; } else { rx_len 0; } HAL_UART_Receive_IT(huart1, rx_byte, 1); } } void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); if (rx_len 0) { frame_ready 1; } rx_len 0; } }接收流程是這樣的每來一個字節(jié)中斷一次RxCpltCallback里把字節(jié)放進rx_buffer同時重新開啟單字節(jié)接收。當總線上靜默一個字節(jié)多的時間UART外設產(chǎn)生IDLE中斷此時rx_len里攢著的數(shù)據(jù)就是一整幀置frame_ready標志主循環(huán)檢測到這個標志就處理數(shù)據(jù)。這里有一個HAL庫的坑必須提__HAL_UART_CLEAR_IDLEFLAG這個宏在不同的HAL庫版本里行為不一致。保險的做法是像下面這樣手動清標志volatile uint32_t dummy; dummy huart1.Instance-SR; dummy huart1.Instance-DR;讀SR再讀DR是清UART各種標志最原始也最可靠的方式。遇到IDLE中斷一直觸發(fā)或者清了之后再也不觸發(fā)的怪問題換成這段代碼基本就解決了。如果是做Modbus RTU協(xié)議更規(guī)范的辦法是用3.5個字符時間窗口來判斷幀結束邏輯比IDLE中斷稍微復雜一點但容錯性更好??梢韵扰芡ㄖ袛嗉覫DLE的方式再按協(xié)議需求優(yōu)化。3.4 主循環(huán)里的幀處理與應答邏輯主循環(huán)的結構保持簡單只處理標志位while (1) { if (frame_ready) { frame_ready 0; // 在這里解析接收到的幀數(shù)據(jù) // 如果是Modbus請求則組幀回復 // 調用RS485_Send_Data()發(fā)送應答 } // 其他業(yè)務代碼 }如果這個STM32節(jié)點要做Modbus從站你可以在解析到正確地址和功能碼后調用前面寫的RS485_Send_Data把響應幀發(fā)出去。要注意的是回復時也要遵守先拉高方向、再發(fā)數(shù)據(jù)、發(fā)完拉低的順序和普通發(fā)送完全一樣不存在特殊處理。4. 實際調試中的問題、解決和速查4.1 三個最讓人頭大的調試現(xiàn)象及真實解決過程現(xiàn)象一STM32發(fā)送出去對端485轉USB工具收不到任何數(shù)據(jù)但直接測STM32的TX引腳有波形。這個問題我遇到不下五次萬用表量一下波形就能定位。如果STM32的TX有波形說明問題出在方向切換或A/B接線。用示波器看方向控制引腳的波形如果發(fā)送期間方向引腳始終是低電平說明軟件壓根沒進發(fā)送函數(shù)或者GPIO映射錯了。如果方向引腳在高電平期間同時看A、B兩線上有輸出那就是A/B線序錯了對調一下就通?,F(xiàn)象二接收端能收到數(shù)據(jù)但是亂碼或者第一個字節(jié)丟失、最后一字節(jié)被截斷。亂碼先查波特率Emphasis這個是最常見的。波特率對不上時收到的字節(jié)幾乎全是錯位的十六進制值。排除波特率后再查共地線。如果你的STM32板和對端設備用兩個獨立的開關電源供電先把GND連通試一下。最后一字節(jié)被截斷基本就是方向切換太早把HAL_Delay(1)加上去或者改用TC標志加延時一般就好了?,F(xiàn)象三STM32能給從機發(fā)送命令從機也執(zhí)行了動作但從不回復數(shù)據(jù)。這種情況說明發(fā)送鏈路是通的問題出在接收鏈路或者從機根本沒收到正確的幀。先用485轉USB工具掛到總線上人為給從機發(fā)一幀測試指令看從機有沒有響應。如果有響應說明從機沒問題問題回到STM32的接收側。檢查STM32有沒有進接收中斷用調試器在HAL_UART_RxCpltCallback里打斷點如果斷點不進檢查中斷優(yōu)先級和NVIC配置如果斷點進去了但frame_ready始終不置位那就是IDLE中斷沒有產(chǎn)生換成讀SR加DR的清標志方式再試。4.2 總線異常恢復與穩(wěn)定性設計485總線上有一個很揪心的問題是某個節(jié)點發(fā)完數(shù)據(jù)后忘了把方向引腳拉回低電平導致該節(jié)點一直占住總線其他所有節(jié)點全都無法通信。這種情況表現(xiàn)為整個系統(tǒng)全線癱瘓而且很難定位是哪臺設備出了問題因為總線已經(jīng)被拉死了。在STM32的代碼里我自己習慣加一道保護邏輯。每次新數(shù)據(jù)發(fā)送之前先強制把方向引腳拉低再拉高這樣即使上一次發(fā)送過程被異常中斷、方向引腳停留在高電平下一輪發(fā)送也能先從接收態(tài)重新走一遍相當于把總線狀態(tài)恢復了一遍。再配一個超時重發(fā)機制發(fā)送一幀命令后如果在規(guī)定時間內沒收到應答就重新初始化一次UART或者把GPIO重新寫一遍強制釋放總線。這種軟復位不需要重啟單片機但對現(xiàn)場穩(wěn)定性提升巨大。還有一個上電保護STM32復位期間GPIO引腳默認是浮空輸入狀態(tài)方向控制引腳如果沒有下拉電阻外部電平不確定收發(fā)器有可能在上電瞬間進入發(fā)送模式。處理辦法有兩個硬件上一是在DE/RE引腳對地加一個10k下拉電阻二是軟件上在main函數(shù)一開始就立即執(zhí)行RS485_DE_LOW()把方向鎖定在接收狀態(tài)。兩種都做最穩(wěn)。4.3 常見問題速查表我把這幾年調試485遇到的問題整理成一張速查表方便你在現(xiàn)場對照排查現(xiàn)象可能原因解決方案上電就亂碼DE/RE初始電平不確定方向引腳加下拉電阻初始化拉低最后一字節(jié)被截斷發(fā)送完立即切方向等TC標志加延時再拉低收到大量亂碼波特率不匹配核對兩端波特率A/B接反A對B、B對AA接A、B接B不要交叉通信距離一遠就丟包終端電阻缺失、線徑太細總線兩端各加120歐終端換屏蔽雙絞線某節(jié)點接入后總線癱瘓節(jié)點方向引腳卡在發(fā)送態(tài)檢查該節(jié)點軟件方向邏輯從機不回復地址錯、幀格式錯、從機本身問題用485轉USB工具單獨調試從機IDLE中斷反復觸發(fā)清標志方式不對讀SR再讀DR清除標志DMA發(fā)送不完整發(fā)送未完成就切方向在DMA發(fā)送完成回調中拉低方向供電不同地導致通信異常節(jié)點之間地電位差大節(jié)點間連接GND線最后分享一點我自己調485的習慣只要能手動控制方向就絕不依賴自動方向芯片代碼多兩行但現(xiàn)場少一堆坑。每一幀收發(fā)結束之后都讓電路回到接收態(tài)這是整個485穩(wěn)定通信的基本原則??偩€空閑的時候永遠不要讓它懸空上拉下拉電阻和終端電阻這些幾毛錢的電路該加就得加不然省下來的成本都會變成現(xiàn)場的調試時間。還有一個小技巧調試485最痛苦的是不知道總線上到底在傳什么所以我會在STM32板子上額外留一路空閑的UART接一個藍牙模塊或者直接接一根TTL轉USB線把收發(fā)數(shù)據(jù)實時打印到電腦上人就能看見總線數(shù)據(jù)流排查起來效率翻倍。這套做法在我做過的伺服控制、Modbus數(shù)據(jù)采集、多設備聯(lián)動項目里一直都很管用。本文還有配套的精品資源點擊獲取