简介:基于STM32F103RCT6的RS485通讯逻辑代码工程,面向嵌入式开发者和STM32初学者,解决串口代码向新平台移植以及RS485多机通信实现的问题。压缩包共80个文件、约341KB,以C源码和H头文件为主体,另有STM32启动文件、Keil工程文件和调试配置,便于直接打开编译与跟踪;目录按硬件驱动、系统配置、标准外设库等模块划分,结构清晰。目前已有247人学习下载。工程通过江协串口代码移植完成,涵盖UART初始化、中断收发、RS485收发控制、延时与显示驱动等关键逻辑,并体现差分信号传输、多点通信、终端匹配等RS485物理层设计要点。借助该工程可深入理解寄存器配置、波特率设置、中断服务程序编写与缓冲区管理,掌握STM32平台RS485通信的完整实现方法,适用于工业控制、智能楼宇、智能家居等场景的二次开发。
1. 基于STM32实现RS485通讯:为什么这类工程包始终有市场
做嵌入式的人早晚会撞上 RS485。不是因为它的速率有多快,而是因为工业现场、分布式数据采集、PLC 联网这些场景里,RS485 是绕不开的物理层标准。基于 STM32 的 RS485 通讯工程包,本质上解决的是「MCU 的 UART 口怎么变成一条能长距离、多节点、抗干扰的总线」这件事。它把差分收发芯片、方向控制引脚、波特率配置、帧协议打包成一个能跑通的起点,拿来改一改就能接传感器、接变频器、接电表。
这个包对两类人价值最大:一类是刚接触工业通讯的开发者,想知道 USART 之外还要做什么才能让数据在线缆上稳定跑起来;另一类是项目里必须要在几个设备之间做主从通讯的工程师,需要一份能直接复现的最小系统,再去改协议、改轮询逻辑。说到底,RS485 不难,难的是方向切换时序、终端电阻匹配、总线空闲电平这些看不见摸不着的东西。这篇文章不评价某个具体压缩包的源码好坏,只讲清楚:RS485 通讯在 STM32 上到底怎么落地,参数怎么设,坑在哪。
2. RS485 物理层和 STM32 侧的硬件选型:先搞懂总线,再谈代码
2.1 RS485 为什么能传得远、带得多:差分信号与电气参数
RS485 的底层逻辑是差分信号传输。它用一对双绞线上的电压差来表示逻辑 1 和逻辑 0,A 线比 B 线高 200mV 以上是逻辑 1,反过来是逻辑 0。这种传输方式天然抗共模干扰,所以传输距离在 9600 波特率下可以到 1200 米左右,而普通 TTL 串口超过一米就容易出问题。多节点能力来自它的电气规范:标准 RS485 驱动器能带 32 个单位负载,配上高输入阻抗的收发器芯片,一个总线上挂几十个从站并非难事。
STM32 的 USART 外设输出的是 TTL 电平信号,不能直接拉到 RS485 总线上。中间必须加一个收发器芯片,常见的是某型号的国产半双工收发器,某厂商的某型号,或者直接用带自动方向控制的型号。芯片做的事情很简单:DI 接 STM32 的 TX,RO 接 STM32 的 RX,DE 和 RE 引脚控制发送/接收方向,A/B 两个引脚接总线。看到这里应该明白了,RS485 的大部分工程问题,发生在 DE/RE 方向切换和 A/B 线电平上,而不是 UART 本身。
2.2 典型接线与三个电阻的取舍:终端电阻、偏置电阻、隔离
硬件接线看起来简单,实际上有三个电阻决定这套系统能不能稳定工作。第一个是终端电阻。RS485 总线两端各并联一个 120Ω 电阻,目的是匹配线缆特性阻抗,减少信号反射。如果只有两个设备点对点通信,两端都接 120Ω;如果一台主机下面挂多个从机,只在总线物理两端接,中间的从机不接。每端 120Ω,两个端点并联后总线上约 60Ω,这是标准做法。
第二个是偏置电阻。总线空闲时,所有收发器都处于接收态,A/B 之间没有驱动,线缆上的电平可能落在 -200mV 到 +200mV 的混沌区,接收端输出不确定性。A 线通过一个电阻上拉到 VCC,B 线通过一个电阻下拉到 GND,让空闲时 A-B 为正,保证接收端稳定输出逻辑 1。这个电阻的典型值在 390Ω 到 1kΩ 之间,需要根据总线上设备数量和功耗综合折算。第三个是隔离。如果总线上设备之间地电位差过大,共模电压超出收发器承受范围,芯片会直接烧掉。常见做法是加一个隔离电源和数字隔离器,让 STM32 的地与总线地完全断开。
| 电阻类型 | 位置 | 典型值 | 缺了会怎样 |
|---|---|---|---|
| 终端电阻 | 总线物理两端 | 120Ω | 长距离反射导致误码 |
| 上拉偏置 | A 线到 VCC | 390Ω~1kΩ | 空闲时总线电平不确定 |
| 下拉偏置 | B 线到 GND | 390Ω~1kΩ | 同上 |
选型时还需要确认收发器的总线引脚是否支持热插拔、是否有短路保护。工业场景里线缆被误接是常事,选带失效保护的芯片不只多一份安全,也少一个深夜定位问题的理由。
2.3 STM32 侧引脚分配与中断资源规划
RS485 通讯占用的 STM32 资源不多:一组 USART 引脚、一个方向控制 GPIO、一个接收中断。但引脚分配有一些习惯性讲究。方向引脚最好选一个普通 GPIO,不要和 I2C、SPI 这种需要快速翻转的外设共享引脚;中断服务函数里尽量不做事,用标志位加主循环轮询的方式处理接收帧。假如项目里还要跑一个 1ms 的定时器中断,RS485 收发的优先级要给够,否则连续帧进来,RXNE 标志没来得及清,丢数据是迟早的事。
如果是某系列的低功耗型号,还要注意串口时钟来自哪个总线。某些型号的 USART1 挂在高速总线上,USART2/3 在低速总线上,不同总线时钟源影响波特率分频精度。代码里通常用逻辑分析仪去看波形验证实际波特率,而不是单纯信赖计算值。
3. 让 RS485 跑通最小收发:寄存器配置与方向切换的时序
3.1 GPIO 与 USART 初始化:不止是打开串口
STM32 的标准外设库或 HAL 库初始化串口,绝大多数项目都是照搬模板,但 RS485 场景里有几个参数必须单独确认。一是波特率。9600 是工业默认,多数仪表、变频器出厂设置就是 9600,8 数据位、无校验、1 停止位。115200 适合短距离高速率,但总线上线缆长了、分支多了,误码率会明显上升。二是指定收发器芯片的 DE 引脚初始电平:必须拉低,让收发器默认处于接收态。如果上电默认是发送态,你会看到一个诡异现象:总线一直被自己的发送占据,所有从机都没法应答。
void RS485_GPIO_Init(void) { GPIO_InitTypeDef gpio = {0}; // 使能 USART1 和方向控制引脚的 GPIO 时钟 __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // TX 引脚(PA9)复用为 USART1 TX gpio.Pin = GPIO_PIN_9; gpio.Mode = GPIO_MODE_AF_PP; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio); // RX 引脚(PA10)复用为 USART1 RX gpio.Pin = GPIO_PIN_10; gpio.Mode = GPIO_MODE_AF_INPUT; HAL_GPIO_Init(GPIOA, &gpio); // 方向控制引脚(PA8)推挽输出,初始为低 = 接收态 gpio.Pin = GPIO_PIN_8; gpio.Mode = GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOA, &gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET); }初始化里最容易漏的不是引脚模式,而是复用功能映射。某系列芯片的 USART1 可以映射到多个引脚组,如果只开了 GPIO 时钟没配置正确的复用功能号,TX 引脚上永远出不来波形。遇到这种情况首先查数据手册的 AF 映射表,确认 PA9/PA10 对应的是 USART1 而非其他外设。
3.2 发送一帧数据的时序:方向切换是 RS485 的灵魂
RS485 半双工意味着同一时刻只能有一个方向的数据。发送前要把 DE 拉高,让收发器进入发送态;发送完最后一字节的移位寄存器彻底输出完毕后,再把 DE 拉低恢复接收态。这里最大的坑在于:如果只在发送完成中断里立刻拉低 DE,最后一个字节可能还没从移位寄存器里完全送出去,总线上的数据会被截断,对端收到的是残缺帧。
void RS485_SendFrame(uint8_t *buf, uint16_t len) { // 拉高 DE,进入发送模式,等待收发器方向切换稳定 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); delay_us(20); // 等待收发器切换,典型值 10~20us // 逐字节发送,每发一字节后等待发送数据寄存器空 for (uint16_t i = 0; i < len; i++) { while (!(USART1->ISR & USART_ISR_TXE)); USART1->TDR = buf[i]; } // 关键:等待最后一字节完全移出移位寄存器,而非只等 TXE while (!(USART1->ISR & USART_ISR_TC)); // 拉低 DE,回到接收态 delay_us(10); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET); }代码里有两个延时参数值得细说。第一个延时是 DE 拉高后等待收发器进入发送态,不同芯片的切换时间不一样,快的 8us,慢的 20us,保险取 20us。第二个延时是 TC 标志置位后,再等一小段时间拉低 DE,给总线上最后一位的电平留出稳定窗口。有些工程师图省事直接用 HAL_UART_Transmit,这个函数内部确实会等 TC,但它也会把方向控制引脚的管理交给你自己,容易漏。我习惯用寄存器版写发送函数,逻辑更透明,出问题也好查。
3.3 接收侧:中断接收 + 标志位轮询,避免阻塞主循环
接收的常见做法是打开接收中断,每收到一个字节就进中断服务函数,把数据放进环形缓冲区,同时维护一个「收到完整帧」的标志位。主循环检测到标志位后去解析缓冲区内容。这个架构的优点是主循环不会被串口阻塞,即使一帧数据暂时没收完,主循环也还能处理其他任务。
#define RX_BUFFER_SIZE 128 volatile uint8_t rx_buffer[RX_BUFFER_SIZE]; volatile uint16_t rx_index = 0; volatile uint8_t rx_complete_flag = 0; void USART1_IRQHandler(void) { if (USART1->ISR & USART_ISR_RXNE) { uint8_t byte = USART1->RDR; // 简单帧格式:帧头 0xAA + 数据 + 帧尾 0x55 if (byte == 0xAA) { rx_index = 0; rx_complete_flag = 0; } rx_buffer[rx_index++] = byte; if (byte == 0x55 && rx_index >= 5) { rx_complete_flag = 1; rx_index = 0; } // 防止缓冲区溢出 if (rx_index >= RX_BUFFER_SIZE) { rx_index = 0; } } }这段代码的逻辑是:收到 0xAA 认为一帧开始,收到 0x55 且长度不低于 5 字节认为一帧结束。实际项目里帧头帧尾要设计得更严谨,比如帧头用两个字节 0xAA 0x55,然后跟长度字段,再做校验。这里只演示中断收发的骨架。参数上需要注意的是:进中断后尽早读取 RDR,否则 RXNE 标志不清除,连续收到下一字节时会产生溢出错误,ORE 标志置位后整个串口会暂时罢工。
4. 帧协议与数据可靠性:让总线上的数据稳如老狗
4.1 为什么必须设计帧格式,而不是直接丢裸数据
很多新手的第一版 RS485 代码是直接向上位机扔原始数据,接收方也按字节流处理。短距离、单个设备、线缆质量好的时候确实能跑,但一旦变成多个从机挂在一条总线上,或者线缆穿过电机、变频器这些强干扰源,裸数据流的缺陷会暴露出来:从机无法判断一帧从哪里开始、到哪里结束,更无法判断数据有没有在传输中被干扰。
帧格式的价值是让接收方「知道什么时候该听、什么时候算听完」。一个最简的工业帧至少要包含:帧头(用于同步)、地址(用于多机寻址)、功能码或命令字、数据长度、数据区、校验位。有了这个结构,接收方才能区分「收到垃圾字节」和「收到完整命令」。帧头的选择有讲究,最好选正常业务数据里几乎不会出现的字节组合,比如 0xAA 0x55,这样即使总线上有残留噪声,误判帧头的概率也低。
4.2 CRC16 校验:最常用的可靠性兜底手段
RS485 总线上的干扰会导致单个 bit 翻转。奇偶校验只能发现奇数位错误,对偶数位翻转无能为力;累加和校验实现简单但检出能力弱。工程上最常用的是 CRC16,它能检出绝大多数突发错误,而且在 STM32 上实现开销极低。
uint16_t CRC16_Modbus(uint8_t *buf, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= buf[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; }这个多项式 0xA001 是 Modbus 协议的标准 CRC16 算法,按位计算权值高但可读性强。如果项目对实时性要求高,可以预先计算 256 字节的查表版本,速度能翻几倍。实际设计帧格式时,CRC 放在数据区的最后,接收方先收完整帧再统一校验,校验不过就丢弃整帧并计数,不要发 NAK 重传,轮询式主从协议里重传机制由主机统一管理。
4.3 主从轮询模式:总线上多个从机时的调度策略
RS485 本质是总线型通讯,多机场景下必须约定「谁在什么时候可以说话」。最常见的模式是主从轮询:总线上有一个主机,所有从机不主动发数据,只在收到自己的地址时应答。主机按地址顺序挨个询问,或者按事件优先级动态调度。这样做的好处是总线上不会出现两个设备同时发送的冲突,逻辑简单,好排查。
typedef struct { uint8_t addr; uint8_t func; uint16_t len; } ModbusFrame_t; void RS485_PollSlave(uint8_t slave_addr) { uint8_t frame[8]; // 组装读保持寄存器的请求帧 frame[0] = slave_addr; frame[1] = 0x03; // 功能码:读保持寄存器 frame[2] = 0x00; // 起始地址高字节 frame[3] = 0x00; // 起始地址低字节 frame[4] = 0x00; // 寄存器数量高字节 frame[5] = 0x01; // 寄存器数量低字节 uint16_t crc = CRC16_Modbus(frame, 6); frame[6] = crc & 0xFF; frame[7] = crc >> 8; // 发送并等待应答,带超时 RS485_SendFrame(frame, sizeof(frame)); RS485_WaitResponse(100); // 超时 100ms }轮询间隔的选取直接影响总线的吞吐。以 9600 波特率为例,一字节耗时约 1.04ms,一个典型请求帧 8 字节,从机应答帧约 20 字节,加上方向切换的延时,单次问答约 30ms。挂 10 个从机,一轮完整轮询就要 300ms,主循环里其他任务的时间预算要算进去。如果觉得太慢,可以把波特率提到 38400,但前提是线缆质量和终端电阻到位。
5. RS485 调试避坑指南:现象、根因、对策
5.1 现象:能发不能收,逻辑分析仪上 A/B 线看不到波形
现象:发送端的波形正常,但接收端收不到任何数据,RXNE 从不置位。先量 DE/RE 引脚电平。收发器如果一直处于发送态,RO 引脚输出的是高阻态,STM32 的 RX 引脚上信号不会正常。查代码里初始化时方向引脚是否拉低,以及发送函数结束时有没有可靠拉低 DE。另一个高频原因:A/B 两根线接反了。这不是开玩笑,现场施工的人不一定按颜色接线,A 接 B、B 接 A,差分信号极性反转,接收端看到的数据每一位都翻转。用万用表量线缆两端,确认同色接同色。
5.2 现象:总线空闲时串口疯狂收到 0x00 或 0xFF
现象:没人发送数据,但 STM32 的接收中断不断触发,收到的全是同一字节。这是空闲电平不确定的典型表现。总线上没有设备驱动,A/B 间电压落在判定门限附近,收发器输出端随机输出高或低。没有偏置电阻的总线就像悬空的输入引脚,噪声推一下就翻转。解决:在总线两端加上拉/下拉偏置电阻,让空闲时 A 比 B 高至少 200mV。同时确认所有从机在非发送状态下是否都处于接收态,有些收发器的 RE 引脚如果悬空,芯片可能进入未知状态。
5.3 现象:近距离通信正常,线拉到 100 米以上就开始乱码
现象:一两米内设备通信没问题,换成 100 米以上的线缆后,接收数据偶尔错位或 CRC 不过。最直接的原因是终端电阻缺失或位置错误。没有终端电阻,信号在线缆末端反射,反射波叠加在原信号上,距离越长越明显。正确做法:确认总线物理两端的设备上有 120Ω 电阻。如果两个端设备没有预留电阻位置,可以单独做两个带 120Ω 电阻的转接头插在总线上。另一个原因是波特率过高。超过 50 米线缆加上高速率,信号上升沿变缓,采样位置失准,把波特率降到 9600 通常能解决大半问题。
5.4 现象:低功耗设备偶尔通信失败,唤醒后收不到第一帧
现象:设备从睡眠模式唤醒后,主机立刻发命令,从机无响应;过几百毫秒后又正常了。很多 STM32 的低功耗模式会关闭 USART 时钟和外设电源。唤醒过程涉及时钟重新稳定、外设重新初始化、收发器芯片上电稳定三个时间段。唤醒后立刻通信,很可能 STM32 的 USART 还没重新配置好,或者收发器芯片还在上电。解决:从机唤醒后先延时 50~100ms,再重新初始化 USART 和方向引脚,最后才允许接收数据。代码里可以在低功耗进入前的标志位记录状态,唤醒后按状态决定是否需要完全重新初始化。
5.5 现象:多个从机接上总线后,整个网络通信瘫痪
现象:单个从机通信正常,接上第二个从机后,主机连第一个从机都不响应了。先检查每个从机在待机时是否让收发器保持接收态。如果一个从机的 DE 引脚被意外拉高,它会持续驱动总线,其他设备发什么都被它「顶掉」。逐个断开从机排查,找到故障节点。另一个可能是某个从机的 A/B 线接反,它会往总线上灌一个反相电压,直接把整个总线的差分信号拖到无效区。排查时最有效的工具是万用表量总线静态电压:空闲时 A 相对 B 应有一百到几百毫伏正电压,如果量到负电压或接近 0,说明总线上有节点方向异常。
6. 从「能通」到「好用」:多机地址分配与自动方向切换的进阶技巧
跑通了基础收发,项目才完成一半。真正投入生产的 RS485 网络,还要解决两个问题:设备地址怎么分配才不冲突,方向切换能不能做到完全自动、少写一个延时。
设备地址分配的常见做法是硬件拨码开关加软件映射。每个从机有一个 8 位的拨码开关,上电时 STM32 读取 GPIO 电平组合,映射到 1~247 的站号。这样做的好处是现场维护时不需要拆机烧程序,拨一拨开关就能改地址。地址读取代码要注意 GPIOPull 上拉或下拉配置,拨码开关悬空时不能进入随机状态,必须有一个确定默认值。
方向控制的进阶方案是硬件自动切换。某些收发器芯片内置了自动方向检测,或者外部加一个简单的三极管电路:发送时 TXD 信号自动驱动 DE,空闲时 DE 自动恢复低电平。这样软件里完全不用写 GPIO 控制,也少了两处延时参数需要调。代价是灵活性下降,某些高低电平持续时间很短的帧边沿可能触发方向误动作,因此需要在芯片选型时确认最窄脉冲宽度指标。
另一个让项目从「能用」变「好用」的习惯是打印总线统计信息。每收到一帧 CRC 错误,计数器加一;每发一帧,记录发送耗时。通过串口调试口把这些数据输出到上位机,长时间运行后就能判断总线质量。CRC 错误率高于万分之一,说明线缆、终端电阻或偏置电阻还有优化空间。这套做法帮我解决过不少「偶尔跳一个数」的疑难杂症,比盲目改代码有效得多。
RS485 通讯这个方向投入产出比很高,它不是新技术,但工业现场永远需要有人把它用的足够稳。我自己的习惯是:每个项目都保留一套「最小验证代码」,只有 USART、方向引脚、收发函数、一个回环测试,任何新板子拿过来先跑这套代码确认硬件没问题,再往上加业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取