简介面向STM32F103嵌入式开发者的RS485串口通信工程资源包含完整的串口驱动、RS485方向控制及收发示例代码适合工业现场远距离、抗干扰通信等应用场景的开发者参考可用于入门学习或工程复用。压缩包共213个文件以C语言源文件和头文件、启动文件、Keil工程文件为主也包含编译生成的中间文件整体约3.27兆字节目录结构清晰内含库函数、中断处理及系统配置文件可直接在Keil中加载。已有1011人学习下载。内容以MAX485、SP3485等转换芯片为例详细展示硬件接线、串口初始化含波特率、数据位、停止位配置、发送接收时的方向切换流程并讨论循环冗余校验、握手协议、主从冲突避免、电气隔离等工程要点可帮助入门者从原理到代码完整走通RS485通信链路同时也能作为调试排错的参考模板。1. STM32(F103)RS485串口通信为什么实际项目中离不开它在自动化设备现场最常看到的不是 USB 转 TTL而是两根绞在一起的 A、B 线。STM32F103C8T6 这类 Cortex-M3 主控要接入 RS485 总线不能把 USART 引脚直接怼上去因为 USART 输出的是 3.3V TTL 电平而 RS485 需要差分信号线缆动辄几十米。RS485 串口通信解决的就是远距离、多节点和抗干扰这三个问题典型应用是变频器参数读写、电表采集、Modbus 传感器接入以及带 RS485 接口的工业触摸屏。如果把这个主题做成压缩包里面通常是一个最小工程串口初始化、收发例程和参考电路。但拿到例程之前至少需要弄清自动收发电路怎么选、方向引脚为什么要在发送完成后释放、总线空闲时 A/B 电压为什么有要求。下面从硬件到代码最后落到调试技巧适合需要自己调板子的工程师也适合把 RS485 作为毕业设计基础层的同学。2. RS485 串口通信物理层与自动收发电路设计RS485 标准规定 A、B 两线之间的差分电压大于等于 1.5V 时表示逻辑 1小于等于 -1.5V 时表示逻辑 0。STM32F103 的 USART 是单端 TTL 电平必须经过 MAX485、SP3485 这类收发器才能挂到总线上。收发器内部可以简单看成两部分发送侧把 DI 的 TTL 转换成 A/B 差分接收侧把 A/B 差分转换成 RO 上的 TTL。因此硬件设计的核心就是处理好“方向使能”。2.1 TTL 转 RS485 的引脚对应与最小接线以 MAX485 为例推荐引脚对应关系如下MAX485 引脚功能说明接到 STM32F103RO接收输出A/B 差分转成 TTLUSART1_RXPA10RE接收使能低电平有效与 DE 短接后接方向控制脚DE发送使能高电平有效同上DI发送输入TTL 转差分USART1_TXPA9A差分正确总线 A 线B差分负端总线 B 线RE 和 DE 如果分开接可以做成收发独立但 RS485 是半双工总线绝大多数场景都是用同一个 GPIO 控制。也可以直接用 TXD 信号通过三极管或电容自动切换方向这就是热词里常说的 RS485 自动收发电路。自动收发的好处是软件不用管方向缺点是波特率较高、线缆较长时方向切换的毛刺可能造成第一字节丢帧。我一般只在低速和短距离场景下用自动收发距离超过 100 米就改成 GPIO 手动控制。2.2 用 GPIO 控制 STM32F103 的 RS485 方向切换初始化时把方向脚配成推挽输出默认拉低让芯片处于接收状态。发送数据时先拉高 DE再调用 USART 发送发送完成后必须等 TCTransmission Complete标志置位再拉低方向。如果只等 TXE发送数据寄存器空最后一个字节可能还没从移位寄存器发完停止位会被切断从机收到就是一帧乱码。void RS485_SetDirection(uint8_t tx) { HAL_GPIO_WritePin(RS485_DIR_GPIO_Port, RS485_DIR_Pin, tx ? GPIO_PIN_SET : GPIO_PIN_RESET); } void RS485_SendBytes(uint8_t *buf, uint16_t len) { RS485_SetDirection(1); // 进入发送模式 HAL_UART_Transmit(huart1, buf, len, 100); while (__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET) ; // 等最后一个字节彻底发完 RS485_SetDirection(0); // 恢复接收模式 }逻辑说明前半段把方向脚拉高后MAX485 的 DE 才有效DI 上的 TTL 信号才能送到 A/B 总线。HAL_UART_Transmit 在数据寄存器为空时就返回但移位寄存器可能还在发送停止位所以必须用 TC 标志等待全部物理位流发完再拉低方向回到接收态。参数说明里RS485_DIR_Pin 是 CubeMX 生成的方向引脚宏HAL_UART_Transmit 的超时写 100ms在 9600 波特率下传 64 字节大约 60ms100ms 够用。如果换成 DMA 发送不能用 while 转圈等 TC而要在 DMA 发送完成中断里拉低方向。2.3 终端电阻和偏置电阻的参数调整总线两端各并一个 120Ω 终端电阻用于匹配特征阻抗防止信号反射。短距离几米以内可以不加但一主多从或者线缆较长时缺少终端电阻会在 A/B 波形上看到明显的过冲和振铃。偏置电阻是另一个容易忽略的地方当总线空闲、没有任何节点发送时如果 A/B 之间没有压差接收器输出不确定串口会收到 0xFF 之类的乱码。常见做法是在 A 线上拉到 VCCB 线拉到 GND阻值取 620Ω 到 1kΩ具体值需要根据设备数量重新计算并联等效电阻。提示用万用表测总线空闲时的 A-B 电压应该落在 0.2V 到 5V 之间如果接近 0V就是偏置不够或没有偏置。总线节点数偏置电阻参考值说明2 ~ 161kΩ大部分 USB-RS485 模块内置17 ~ 32620Ω降低并联等效阻抗超过 32需要加中继器RS485 标准负载上限通常为 32 个偏置电阻按“上拉电阻与下拉电阻串联后再并联多个节点”来估。实际调试时只要保证 A-B 空闲电压稳定在 200mV 以上即可。后面要讲的组网和代码都是基于上述电路能正常收发的前提。3. 用 STM32F103 的 USART 实现 RS485 串口通信收发代码物理层解决后剩下的是 STM32F103 的 USART 配置。RS485 通信本身不要求特定协议串口参数由收发双方约定。最常见的是 9600bps、8 数据位、1 停止位、无校验写成串口助手里的“9600 8N1”。用 CubeMX 初始化时重点不是把图形界面点完而是理解每个参数和后续代码的互动。3.1 STM32F103 串口初始化参数波特率、停止位与校验低速率下 8N1 最通用。下面这段初始化代码省略了 RCC 时钟配置直接看关键项static void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 9600; // 与总线设备一致 huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huart1) ! HAL_OK) { Error_Handler(); } }逻辑说明USART1 时钟来自 APB2最高 72MHz。高过采样 16 倍后波特率计算是PCLK / (16 * div)所以 APB2 频率如果被改过实际波特率会偏移。参数说明WordLength 是 8 位数据无校验时支持 8 位Parity 一旦启用数据位要配成 UART_WORDLENGTH_9B因为校验位要占一位。很多“收发乱码”其实是 8E1/8O1 下配错 WordLength 造成的。3.2 基于 HAL 库的方向控制与多字节发送发送函数在第 2 章已经给出这里补上接收侧处理。接收侧用中断或者 DMA不能用阻塞轮询因为总线上什么时候来数据不可控。开启接收中断的经典写法是每次收一个字节再重新使能下一次接收uint8_t rs485_rx_buf[128]; volatile uint16_t rs485_rx_len 0; void RS485_StartReceive(void) { RS485_SetDirection(0); HAL_UART_Receive_IT(huart1, rs485_rx_buf[rs485_rx_len], 1); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rs485_rx_len; HAL_UART_Receive_IT(huart1, rs485_rx_buf[rs485_rx_len], 1); } }逻辑说明HAL_UART_Receive_IT 每次只收一个字节回调里继续使能下一次接收这样每收到一字节都会触发一次中断。如果直接把长度设成 NHAL 会在收满 N 字节后才回调无法应对变长帧。参数说明rs485_rx_len 是累计长度需要在前一帧处理完后清零。这种逐字节方式在 9600 波特率下每字节约 1.04ms中断开销可以接受但主频紧张或者帧很长时更推荐 DMA。3.3 用 DMA 空闲中断接收不定长帧的改进方案逐字节中断代码简单但更稳的是 USART 空闲中断。空闲中断指总线在线路收到至少一个字节后出现一个字节时间的空闲电平触发时机正好是 RS485 一帧数据结束后天然适应变长报文。配合 DMA 接收可以让硬件自动把数据搬进内存IDLE 到来后检查 DMA 剩余计数即可得到帧长。#define RX_BUF_SIZE 128 uint8_t dma_rx_buf[RX_BUF_SIZE]; void RS485_StartReceiveDMA(void) { HAL_UART_Receive_DMA(huart1, dma_rx_buf, RX_BUF_SIZE); } void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); HAL_UART_DMAStop(huart1); uint16_t len RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); Process_RxFrame(dma_rx_buf, len); RS485_StartReceiveDMA(); } HAL_UART_IRQHandler(huart1); }逻辑说明DMA 配置成普通模式接收缓冲区长度为 RX_BUF_SIZE每次启动后计数器从 RX_BUF_SIZE 往下减。空闲中断触发时DMA 计数器剩余值就是未接收长度用总长度减剩余值得到本次帧长度。HAL_UART_DMAStop 的作用是停止 DMA防止下一次接收覆盖当前数据。参数说明Process_RxFrame 是用户自己的协议处理函数必须在清空缓冲区之前取走数据hdma_usart1_rx 是 CubeMX 生成的 DMA 接收句柄名称要和工程一致。表格对比三种接收方式接收方式占用资源适合场景逐字节中断每字节一次中断帧短、开发快DMA 循环无中断靠超时判断连续流数据DMA IDLE一帧一次中断RS485 不定长帧、Modbus4. STM32F103 RS485 串口通信组网与一主多从实现RS485 总线上可以挂多个节点但同一时刻只能有一个节点发送否则就是总线冲突。实际工程中用得最多的是“一主多从”轮询模式只有主机在平时主动发命令从机根据地址判断是否响应。这种模式比总线竞争简单也适合 STM32F103 的资源。4.1 一主多从的连接拓扑与地址分配总线拓扑是一条主线所有设备并联在同一对 A/B 线上。每个从机分配唯一地址比如 1~247地址 0 在 Modbus RTU 中属于广播地址实际项目要慎用。轮询过程是主机先向从机地址 1 发请求等从机回复处理完后再问地址 2直到所有从机被轮询一遍。从机不能主动往总线上发数据否则会干扰正在进行的通信。从机端地址匹配可以写成#define RS485_ADDR_BROADCAST 0x00 #define RS485_ADDR_SLAVE 0x01 // 本机地址 uint8_t RS485_CheckAddress(uint8_t *buf, uint16_t len) { if (len 4) return 0; if (buf[0] ! RS485_ADDR_BROADCAST buf[0] ! RS485_ADDR_SLAVE) return 0; // 不是发给本机 return 1; }逻辑说明帧头第一个字节是地址。从机收到数据后先判断地址不匹配就直接丢弃不占用总线。参数说明广播地址一般用于同时启动、同时复位普通数据读写请求不要用否则所有从机同时回包会造成总线冲突。4.2 帧格式与 CRC16 校验Modbus RTU 是最常见的 RS485 应用协议帧格式为地址(1) 功能码(1) 数据(N) CRC16(2)CRC 低位在前。STM32F103 算 CRC16 很快可以用按位算法省掉查表空间uint16_t RS485_CRC16(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc ^ *data; for (uint8_t i 0; i 8; i) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }逻辑说明多项式 0xA001 是 Modbus RTU 的 CRC-16 反写形式逐位计算 8 个周期最后得到的 crc 需要先发低字节再发高字节。参数说明data 指向从地址开始的整帧len 不含 CRC 本身。CRC 字节序写反是从机不响应的高频原因调试时要先用串口工具对比标准计算器结果。4.3 轮询调度与超时重传多从机环境中主机不能在发送请求后无限等下去。常见做法是设定响应超时比如 100ms超时重发 2 次仍然失败就标记该节点离线继续下一台。在 STM32F103 上可以用一个 SysTick 累计毫秒变量实现volatile uint32_t tick_ms; void RS485_PollSlave(uint8_t addr) { uint32_t start tick_ms; uint8_t cmd[] { addr, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00 }; uint16_t crc RS485_CRC16(cmd, 6); cmd[6] crc 0xFF; cmd[7] crc 8; for (uint8_t retry 0; retry 3; retry) { RS485_SendBytes(cmd, 8); rs485_rx_len 0; while (tick_ms - start 100) { if (rs485_rx_len 5) { /* 地址匹配且 CRC 校验正确后处理数据 */ return; } } } /* 标记从机离线 */ }逻辑说明超时判断用tick_ms - start避免使用绝对时间导致回绕问题。rs485_rx_len 在空闲中断回调中被更新主循环或任务里轮询。参数说明100ms 超时在 9600 波特率下足够但在 4800 波特率下要加大到 200ms 以上因为每字节耗时从 1.04ms 变成 2.08ms8 字节请求加 5 字节响应需要约 27ms留出余量更安全。波特率每字节约耗时100ms 能收字节数推荐响应超时96001.04ms96100ms48002.08ms48200ms ~ 300ms1152000.087ms114920ms ~ 50ms5. 调试 RS485 串口通信时的三个测量点与低波特率坑5.1 用万用表和示波器验证自动收发电路自动收发电路好不好最直接的验证办法是让设备发送 0x55用示波器同时测量 DI 和 A-B 差分波形。0x55 会产生 01010101 的码型A-B 波形应能看到清晰的方波翻转。如果波形前几个位起点偏移说明自动收发切换太慢。没有示波器时用万用表测总线空闲时的 A-B 电压稳定在 0.2V~5V 是及格线低于 0.2V 优先补偏置电阻。5.2 排查“9600 能通4800 没数据”的问题把波特率从 9600 改到 4800 后没有数据多数不是参数写错而是系统时钟源不准。如果 STM32F103 使用 HSI 内部时钟而不是外部晶振9600 波特率误差可能刚好在容限内能通降到 4800 后分频系数非整数误差反而变大。解决办法是改用外部 8MHz 晶振并确认 APB2 时钟为 72MHz若已经用外部晶振则检查USART_BRR计算值是否接近整数。5.3 丢帧时先测停止位波形RS485 通信偶发丢帧不要急着改协议。用示波器看 A 对 B 的差分波形重点看停止位是否完整、空闲电平是否有毛刺。如果发送端用 GPIO 控制方向检查是否等 TC 后再切方向如果使用 DMA 发送必须在 DMA 传输完成中断里释放方向不能在 HAL_UART_Transmit 返回后立刻切。提示把方向释放绑定到 TC 或 DMA 完成中断排查“最后一字节丢”比全协议抓包快得多。最后一字节的停止位完整度直接决定这个 RS485 网络在真实线缆长度下是否可靠。自动收发电路、偏置电阻、方向切换时机这三件事都做对了STM32F103 的 RS485 串口通信才算真正能上线。本文还有配套的精品资源点击获取