ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103 RS485实战:半双工方向控制与一主多从通信设计

STM32F103 RS485实战:半双工方向控制与一主多从通信设计 简介面向 STM32F103 嵌入式开发者的 RS485 串口通信示例工程针对长距离、多噪声工业场景演示 UART 差分传输与一对多组网的关键实现方法。工程代码覆盖串口初始化、收发流程、RS485 转换芯片MAX485/SP3485的 DE/RE 方向切换并兼顾 CRC 校验、握手应答、冲突避免等可靠通信设计帮助开发者理解从裸机寄存器到外设库的完整配置思路。压缩包内共 213 个文件含 39 个 H 头文件与 38 个 C 源文件可组成完整驱动层另有启动汇编文件、Keil 工程配置、hex/axf 烧录与调试文件以及说明文档整体仅 3.27MB便于直接打开工程对照代码阅读。文档还提供编译、烧录与验证指引配合 .map/.lst 等构建过程文件可进一步分析编译链接细节。已有 1011 人学习适合入门到中级开发者作为 RS485 通信的项目模板快速迁移到主从轮询、传感器网络等分布式系统中。1. RS485在STM32F103上比想象中多出的那根“方向线”很多人第一次在STM32F103上做RS485串口通信都以为把TXD、RXD接到MAX485就能跑通。实际接上后发现自己发出去的帧自己收到了、从机响应永远读不到、波特率9600可以但19200乱码。问题出在RS485是半双工差分总线MAX485这类转换芯片的RO、DI本身没有方向仲裁能力必须由MCU控制DE/RE引脚。STM32F103的USART外设没有硬件RTS方向管理因此软件切方向成了工程落地时绕不开的一步。这篇文章从物理层差分信号讲清楚为什么需要DE线再给出一套基于标准外设库的发送/接收驱动写法最后用一个一主多从的轮询示例把地址、CRC和超时处理串起来。适合正在做工业采集、多机通信或课程设计的开发者尤其适合想搞懂RS485自动收发电路和GPIO控制方向之间差别的人。2. RS485物理层解析与STM32F103的外设映射2.1 为什么TTL串口不能直接拉长距离标准UART输出的TTL电平是单端信号逻辑1为3.3V或5V逻辑0为0V。STM32F103的USART引脚驱动能力强一点但抗共模干扰差线长超过几米就会因压降和电磁干扰出现误码。RS485把单端信号转换成A、B两根线之间的差分电压空闲时A比B高逻辑1A比B低为逻辑0。接收端只需比较两根线的电位差共模噪声被抵消传输距离可以到1200米还能挂多条总线。正是这种多点拓扑催生了半双工收发模式任何时刻只允许一个节点驱动总线其他节点监听。TTL转RS485看似只是加一颗转换芯片实际等于把单片机的异步串口搬到了带方向控制的差分总线上。STM32F103的USART并不感知总线方向它只负责发出字节流和接收字节流。所以RS485通信的本质是UART串口通信加一层外部方向控制而不是换了一个串口协议。理解这一点后再去分析RS485自动收发电路或者GPIO切方向方案思路会清晰很多。2.2 MAX485/SP3485与STM32F103的引脚级连接常见做法是用MAX485或SP3485做TTL转RS485。MAX485的第1脚RO接STM32的USART1_RXPA10第4脚DI接USART1_TXPA9第2、3脚RE和DE短接后由任意一个GPIO控制。RE低电平有效DE高电平有效短接后一个引脚就能控制方向输出高进入发送模式输出低进入接收模式。MAX485引脚功能STM32F103引脚方向说明1 (RO)接收数据输出PA10 (USART1_RX)芯片输出到MCU2 (RE)接收使能低有效PC1方向控制与DE短接3 (DE)驱动使能高有效PC1方向控制与RE短接4 (DI)发送数据输入PA9 (USART1_TX)MCU输出到芯片6 (A)差分正极RS485总线A总线接口7 (B)差分负极RS485总线B总线接口A、B之间要跨接120Ω终端电阻阻值等于双绞线特性阻抗用于吸收反射。距离短、波特率低时可以悬空但多节点或线长超过10米最好接上。SP3485与MAX485引脚兼容前者是3.3V供电版本适合STM32F103C8T6这类3.3V逻辑平台。MAX485如果供电5VRO输出高电平会接近5V可能超过F103引脚容忍范围需要在RO线上串联电阻或做电平转换。实际省事方案是直接用SP3485供电和信号电平都对齐。还有一种RS485自动收发电路用三极管和RC电路根据DI信号自动切换方向省掉一个GPIO。它的原理是在空闲时通过上拉电阻让DE保持低电平接收发送起始位时三极管导通把DE拉高。这个方案在9600波特率下工作稳定但在115200波特率下会由于RC充放电时间不匹配产生边沿畸变。我的习惯是优先用GPIO控制方向逻辑清晰出问题容易排查。2.3 方向切换时序是RS485通信的关键DE线从接收切到发送后不能立刻往USART数据寄存器里写数据。MAX485的驱动器使能到输出电压稳定需要几十纳秒到几微秒通常拉高DE后再延时一个位时间或者直接先写第一个字节再等待TXE靠发送过程把稳定时间覆盖掉。发送完成后必须等待USART的TC发送完成标志置位确认移位寄存器把最后一帧数据完全送出才能拉低DE。如果只等TXE标志最后一个字节可能还在移位寄存器里总线尚未释放其他从机就无法响应。这个顺序一旦反了最后一字节就会被截断整个帧结构错位。3. 基于标准外设库的RS485驱动编写3.1 使能时钟并初始化USART1与方向控制引脚使用STM32F103标准外设库时先使能GPIOA、GPIOC和USART1的时钟然后分步配置TX、RX和方向引脚。USART1的TX和RX要分别配置为推挽复用输出和浮空输入方向控制引脚配置为推挽输出。void RS485_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_USART1, ENABLE); // USART1_TX PA9 复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // USART1_RX PA10 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // PC1 控制 MAX485 的 RE/DE GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_WriteBit(GPIOC, GPIO_Pin_1, Bit_RESET); // 默认接收模式 }这段代码先把RE/DE引脚复位为低确保上电后处于接收状态避免MCU复位瞬间MAX485误驱动总线。GPIO_Speed选择50MHz是为了让方向引脚翻转足够快特别是在115200波特率下一个位时间只有8.68μs慢速输出模式会影响边沿。TX配置为复用推挽而非普通推挽是因为PA9的信号源自USART外设的发送数据线普通推挽输出会把外设信号强制钳制在高或低电平。3.2 USART通信参数与中断配置RS485的通信参数需要和总线上的其他节点一致。下面的代码配置波特率9600、8数据位、无校验、1停止位使能接收中断用于从站响应接收。void RS485_USART_Init(uint32_t baudrate) { USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }参数里没有把DE绑定到USART硬件因为F103的USART不提供RTS自动方向输出。RS485内部最基本的做法就是把USART_Mode设置为Rx和Tx同时开启方向由外部GPIO控制。中断只开了接收发送部分在下一节通过轮询方式完成这样逻辑更直观。接收中断里要根据帧长度或空闲时间判断一帧结束不能每收到一个字节就当作完整帧。3.3 DE方向切换与发送完整字节帧发送数据时先拉高DE调用USART_SendData写入第一个字节。之后不能马上写下一个字节必须等待TXE置位表示数据寄存器已空。最后一字节需要额外等待TC置位确保移位寄存器发送完毕再拉低DE。void RS485_SendBuffer(uint8_t *buf, uint16_t len) { uint16_t i; GPIO_WriteBit(GPIOC, GPIO_Pin_1, Bit_SET); // 切换到发送模式 for (i 0; i len; i) { USART_SendData(USART1, buf[i]); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); // 等数据寄存器空 } while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 等移位寄存器发完 GPIO_WriteBit(GPIOC, GPIO_Pin_1, Bit_RESET); // 释放总线切回接收 }TXE标志表示数据从软件写入USART_DR后转移到了移位寄存器但此时最后一个字节还在物理线路上没发完因此需要TC标志确认结束。这个顺序不能反过来。如果在循环里只检查TXE最后一字节的停止位还没出去就把DE拉低了总线上从设备可能只收到9个位的数据后续通信全部错位。还要注意RS485_SendBuffer执行期间最好关中断或者保证不会被高优先级中断打断超过一个位时间否则方向控制被长时间卡住总线一直被本机占用。3.4 波特率寄存器BRR的计算与误差控制USART1挂在APB2上时钟为72MHz。波特率寄存器BRR的计算公式是DIV PCLK / (16 * 波特率)。例如9600波特率DIV 72000000 / (16 * 9600) 468.75。BRR的低4位存放小数部分高12位存放整数部分。库函数会把浮点转成整数和分数写入寄存器。常用配置如下目标波特率DIV实际波特率误差9600468.7596000.00%19200234.375192000.00%11520039.06251152000.00%250000182500000.00%4608009.76564545451.36%460800在72MHz下没法整除误差达1.36%。UART接收端要求波特率误差一般在±2%以内RS485自动收发电路还会再增加抖动所以优先选能整除的波特率。如果必须用460800通常改PCLK分频系数让PCLK接近一个能被16整除的倍频值。我在STM32F103C8T6上测试过系统主频72MHz、外部晶振8MHz时9600到115200都是稳定区往上就要用逻辑分析仪数位宽确认。4. RS485一主多从轮询协议与实测排错4.1 帧格式设计地址、功能码、长度和CRCRS485只是物理层应用层需要自己约定帧格式。工业现场最常见的是Modbus RTU但自己设计协议时建议留出地址、功能码、长度和数据校验。下面这个帧结构足够覆盖多数传感器采集场景字段长度说明地址1字节从站地址1~247有效0为广播功能码1字节0x01读、0x02写等数据长度1字节后续数据字节数数据N字节寄存器值或命令参数CRC162字节低字节在前CRC16用于检测传输错误。RS485在工业现场会引入毛刺干扰没有校验的协议很容易出现主站收到错误响应但无法判断数据是否可信。CRC16多项式采用0xA001初值0xFFFF查表法或位循环法都可以。我习惯把CRC函数单独放一个文件主站和从站共用同一个实现避免两端算法不一致。4.2 主站轮询状态机的实现一主多从组网时主站按地址轮询每次只发一帧请求然后等待从站响应。用状态机管理轮询过程比死等或延时阻塞更可靠。状态定义如下typedef enum { RS485_IDLE, RS485_WAIT_SEND, RS485_WAIT_RESPONSE, RS485_SEND_NEXT } RS485_State; RS485_State rs485_state RS485_IDLE; uint8_t current_slave_id 1; uint16_t timeout_cnt 0;主循环里不断刷新状态机发送请求后等待响应。每次轮询开始时调用RS485_SendBuffer发送地址和命令切换到等待响应状态同时清零超时计数。如果收到完整帧并校验通过则处理数据并递增地址如果超时达到预设值则记录失败次数并跳到下一个从站。超时时间要大于从站处理时间加上总线空载时间一般取20~100ms。状态机的核心循环void RS485_Poll(void) { switch (rs485_state) { case RS485_IDLE: rs485_state RS485_WAIT_SEND; break; case RS485_WAIT_SEND: // 构造请求帧并发送 rs485_tx_buf[0] current_slave_id; rs485_tx_buf[1] 0x01; // 功能码 rs485_tx_buf[2] 0x02; // 数据长度 rs485_tx_buf[3] 0x00; // 寄存器地址高字节 rs485_tx_buf[4] 0x00; // 寄存器地址低字节 RS485_SendBuffer(rs485_tx_buf, 5); timeout_cnt 0; rs485_state RS485_WAIT_RESPONSE; break; case RS485_WAIT_RESPONSE: if (rs485_rx_done 1) { // CRC校验通过则记录数据 rs485_rx_done 0; current_slave_id; if (current_slave_id 247) current_slave_id 1; rs485_state RS485_IDLE; } else if (timeout_cnt 100) { timeout_cnt 0; current_slave_id; if (current_slave_id 247) current_slave_id 1; rs485_state RS485_IDLE; } break; default: break; } }这段轮询逻辑没有阻塞延时靠主循环计数做超时适合放在带RTOS的工程里也可以放在裸机while循环中。RS485_SendBuffer是同步阻塞的发送几字节的时间约几毫秒可以接受。要注意主站拉低DE后从站响应到达MCU会有总线传播延迟和MAX485的RO输出延迟所以主站切回接收后需要等待几十微秒到几毫秒才能收到第一个RXNE中断。这里的rs485_rx_done标志在USART1中断或定时器检查中置位表示收到完整一帧。4.3 从站中断接收与帧结束判断从站不能主动发数据只能响应请求。最直接的接收方式是在USART的RXNE中断里逐个字节把数据存进数组。判断一帧结束可以用两种办法一是根据帧长度字段收满N字节就算一帧二是利用串口空闲时间超过一定时间没有新字节则认为帧结束。STM32F103的USART没有硬件空闲中断但可以引入一个定时器或者直接在中断里做超时判断。下面给出一种简单可靠的做法主循环里定期检查距上次收到字节的时间超过5ms认为一帧结束。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); if (rs485_rx_index RS485_RX_BUF_SIZE) { rs485_rx_buf[rs485_rx_index] data; } last_rx_time systick_ms(); } }主循环里用systick毫秒计数当rs485_rx_index大于0时检查last_rx_time距当前时间是否超过5ms。一旦超时把rs485_rx_done置位同时将收到的地址、功能码、长度拿出来解析。如果当前帧是请求且地址等于本机地址就调用RS485_SendBuffer发送响应。要注意从站在收到完整帧之前必须先保持DE为低否则自身发送会打断接收。实际测试中发现从站在解析CRC期间就开始构造响应会因为DE引脚切换不及时导致响应帧头被截断所以要么在响应函数一开始就拉高DE要么把CRC校验和响应分两步执行。4.4 示波器与逻辑分析仪定位RS485故障RS485通信出问题时不要先改代码先看物理层。把示波器探头接A和B之间观察差分波形。空闲时A-B应该为2V~6V因为有偏置电阻发送时波形翻转差值大于0.2V就有效。常见故障现象有两种。第一种是只有发送没有接收。用示波器看MAX485的RO引脚如果主站发送时RO有翻转说明芯片方向控制没问题问题在从站的DE没有释放。常见原因是从站发送完数据后没有等待TC就拉低了DE或者从站GPIO配置成了复用输出而不是普通推挽。第二种是总线冲突。两台设备同时拉高DE总线被两个驱动源同时驱动波形会出现台阶或毛刺。这时需要逐一断开节点确认每个节点复位后是否默认处于接收状态。我的习惯是在每台设备的初始化里最先执行GPIO配置并拉低DE再配置USART。故障现象可能原因检查手段自发自收DE没有被拉高或RE/DE未短接测DE引脚电压只能发不能收发送完后未等TC就切回接收逻辑分析仪看DE与TXD时序长线乱码缺少120Ω终端电阻在总线两端并联电阻测试偶发丢帧波特率误差或从站处理太慢检查BRR误差延长超时全总线冲突多个设备上电时DE为高初始化时先拉低DE再初始化USART这五个问题覆盖了调试RS485组网时遇到的绝大多数坑。STM32F103C8T6作为从站时内部Flash和RAM比F103RCT6小协议栈不要塞太多缓冲轮询周期也要根据从站数量调整。5. 提升RS485稳定性的三个进阶技巧5.1 终端电阻和偏置电阻的搭配RS485总线两端各接一个120Ω终端电阻可以吸收反射。但只接终端电阻后总线空闲时A、B之间电压可能接近0V接收端误判。偏置电阻通常放在主站端把A上拉到VCC、B下拉到GND阻值330Ω到1kΩ。偏置电压要保证接收端差分电压大于200mV。计算方法是VCC5V上下拉电阻R1R2470Ω终端电阻RT120Ω差分电压约为5V*120/(470470120)0.56V满足要求。如果总线节点多、线缆电容大可以适当减小偏置电阻但会增大静态功耗。5.2 从站上电延时与总线空闲检测多节点上电时如果某个从站的MCU复位瞬间GPIO输出高MAX485会立刻驱动总线和其他节点的上电行为冲突。解决办法是在GPIO配置之前先通过外部硬件把RE/DE引脚拉低然后再由软件接管。另外从站收到一帧请求后不要立即回复先等待1ms到3ms目的是错开主站方向切换的拖尾时间。用示波器看主站DE下降沿到从站RO第一个电平翻转的间隔如果小于2个位时间就要延长从站的等待。5.3 用DMA加定时器空闲检测降低中断负载如果数据帧很长或通信频率高每个字节都进中断会导致CPU争用。常见优化是USART接收用DMA环形缓冲配合一个基本定时器做超时检测。初始化时把USART1的接收DMA通道指向接收缓冲区开启DMA传输完成中断。定时器每1ms检查一次DMA剩余计数如果两次检查之间DMA传输计数没有变化说明总线空闲此时解析缓冲区数据。发送仍然用轮询方式因为主站发送频率低。下面的代码片段演示了DMA接收的启动方式DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rs485_rx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RS485_RX_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE);DMA_Mode_Circular让数据源源不断写入环形缓冲区无需在中断里读USART-DR。定时器空闲检测只在字节停止到达时被调用开销远小于逐字节中断。这个方案在STM32F103上配合72MHz主频可以轻松跑满115200波特率同时留下充足时间处理协议解析和传感器读取。RS485串口通信调试时建议优先把物理层波形调干净再用逻辑分析仪记录DE和TXD的时序对齐这样剩下的代码问题都会变得很好定位。本文还有配套的精品资源点击获取
返回列表