资讯动态

STM32 RS485通信实战:半双工方向控制与UART配置

发布时间:2026/9/12 20:20:17 来源:尧图企业网站定制
简介这是一份基于STM32F103的RS485通信实验资源包专门面向正在学习嵌入式串口通信、工业现场总线协议的学生与开发者。内容围绕RS485差分信号传输原理、UART2串口配置、GPIO方向控制等关键知识点详细演示如何用PG3引脚控制DE/RE以切换收发状态并附有Keil工程可直接运行的初始化、发送与接收代码。压缩包共209个文件以C源码、H头文件、Keil工程配置、编译输出及PDF说明文档为主其中.c/.h为标准外设库驱动与实验程序.uvprojx/.uvoptx用于打开或保存工程hex/axf为可烧录固件整体约4.59MB。代码结构清晰便于理解RS485收发方向切换和UART中断处理流程已有456人学习下载。整体可用作STM32串口通信实验模板帮助理解RS485总线仲裁与方向切换逻辑也可在此基础上扩展多机通信、错误重传等应用是一份值得收藏的嵌入式通信入门资料。1. RS485不是协议是一套电平规则UART加一根方向线就能跑RS485在工业现场能传1200米、挂32个节点靠的是A/B两根差分线上的电压差而不是像TTL那样用绝对电平表达0和1。很多人一听RS485通信就以为要买协议栈实际上STM32F103的USART外设已经完成了帧格式、波特率、移位输出这些工作我们只需要在发送前把收发器切到发送方向发完再切回接收方向。这个切换动作由普通GPIO完成实验里用的是PG3。本资源以STM32F103MAX485为例从硬件接线、最小系统、标准外设库初始化到中断收发和主从应答完整走一遍典型应用。适合做工业数据采集、多机通讯网关、楼宇控制的嵌入式工程师也适合正在学STM32但被半双工方向控制搞晕的新手。最后会给出CubeMX/HAL库移植思路和几个实测踩坑点。2. 硬件连接与最小系统PA2/PA3、PG3和RS485收发器的电平关系2.1 差分信号的方向控制逻辑RS485收发器MAX485、SP3485、ISL83485内部是半双工结构A、B线压差超过200mV表示逻辑1低于-200mV表示逻辑0。收发器同时有DE发送使能和RE接收使能两个引脚DE高有效RE低有效。典型电路里把DE和RE短接由MCU一个GPIO控制高电平进入发送方向低电平进入接收方向。实验文档里写“PG3低电平时允许发送数据高电平时进入接收模式”这个描述和主流收发器手册是反的如果照着接表现为主机自发自收正常但从机永远收不到。实际以MAX485手册为准发送前把PG3拉高发送完成后拉低回到接收态。这里要特别注意RS485只定义了物理层帧头、地址、校验全部要自己在应用层处理所以方向切换的时序直接影响通信可靠性。2.2 STM32F103最小系统与启动文件选择这个实验的板子先要保证最小系统能跑起来。供电3.3V、复位电路、8MHz晶振、BOOT0拉低这是STM32F103最小系统的底线。如果是自己画的板子SWD下载必须的SWDIO/PA13和SWCLK/PA14不要被其他外设占用否则DAP下载失败时往往不好查。DAP下载失败还有一个常见来源是BOOT1引脚状态。虽然STM32F103主要是BOOT0控制启动模式但BOOT1在从系统存储器启动时也会参与判断。如果下载失败并提示“Cannot access target”先量BOOT0是否为低电平再用镊子把复位引脚短接到地在Keil里勾选Reset and Run后重试。启动文件的选择同样重要它决定中断向量表和栈大小的初始布局选错会导致程序跑飞或进HardFault。可按Flash容量对照选择芯片型号Flash容量启动文件STM32F103C8T664KBstartup_stm32f10x_md.sSTM32F103RCT6256KBstartup_stm32f10x_hd.sSTM32F103ZET6512KBstartup_stm32f10x_xl.s实验工程里的FLASH大小如果只有64KB就用Medium Density启动文件如果改成256KB的芯片工程里却还放着md.s中断向量表偏移不变但部分外设中断会映射错位表现非常隐蔽。2.3 引脚分配与GPIO初始化这里使用的是USART2TX在PA2RX在PA3方向控制用PG3。PA2/PA3要配置为复用推挽输出它们内部已经连接到USART2的外设引脚不能再当作普通GPIO使用。PG3只需要推挽输出并设置默认低电平让RS485一开始处于接收方向。void RS485_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能GPIOA和GPIOG时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOG, ENABLE); // PA2/PA3复用推挽输出50MHz供USART2使用 GPIO_InitStructure.GPIO_Pin GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // PG3推挽输出控制RS485收发方向 GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; GPIO_Init(GPIOG, GPIO_InitStructure); // 默认接收态DE/RE为低 GPIO_ResetBits(GPIOG, GPIO_Pin_3); }这里把PA2和PA3放在一组初始化PG3单独初始化。注意不要照抄网上某些例程把PA9也加进去PA9是USART1的TX和USART2无关加入后会在发送时意外拉高PA9引起其他外设误动作。GPIO_Speed在方向控制引脚上设为2MHz就够了因为只是电平翻转不承载高速信号PA2/PA3保持50MHz保证串口高波特率下边沿不畸变。3. 标准外设库V3.5.0下的UART2配置时钟、波特率与收发中断3.1 为什么UART2挂在APB1上波特率分频怎么算STM32F103标准外设库初始化时需要先搞清外设时钟来自哪条总线。USART1挂在APB2上时钟72MHzUSART2和USART3挂在APB1上时钟36MHz。这个差异决定了初始化函数必须使用RCC_APB1PeriphClockCmd而不是RCC_APB2PeriphClockCmd。如果直接照抄USART1的配置函数会把USART2的时钟源当成72MHz操作寄存器没错波特率却差一倍表现出来就是接收端全是乱码。标准外设库V3.5.0里USART2的波特率分频公式是分频值 外设时钟 / (16 * 波特率)当时钟为36MHz、波特率9600时分频值 36000000 / (16 * 9600) 234.375USARTDIV寄存器中的整数部分为234小数部分为0.375即0x15A。如果换成115200分频值变成19.53小数部分取整误差约0.16%仍在允许范围内。这就是为什么115200也能用但标准建推荐用9600、19200或38400误差更小。3.2 UART2初始化代码与参数对照表这里补全一个可以直接编译的标准外设库初始化函数。代码里同时使能了接收中断后面才能做中断收发。void RS485_UART2_Init(void) { USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // USART2挂在APB1时钟域 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 9600bps, 8位数据, 1位停止位, 无校验 USART_InitStructure.USART_BaudRate 9600; 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_Tx | USART_Mode_Rx; USART_Init(USART2, USART_InitStructure); // 配置接收中断 NVIC_InitStructure.NVIC_IRQChannel USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_Cmd(USART2, ENABLE); }参数里最关键的是引脚复用和时钟源。标准库中USART2的中断服务函数名是USART2_IRQHandler不是UART2_IRQHandler命名写错会导致中断函数永远不会被调用接收数据只停在接收数据寄存器里主循环读不到。下面给出常用波特率在不同APB1时钟下的分频系数方便对照和排查乱码波特率36MHz下分频值USARTDIV近似值实际波特率9600234.3750x753960019200117.18750x39B192003840058.590x1D33839411520019.530x311115040从表中能看到38400和115200都存在微小偏差现场通信如果线缆长且干扰大尽量使用9600或19200给噪声容限留出裕量。3.3 串口1和串口3使用差异一个容易踩的时钟坑很多工程里既有USART1接调试串口又有USART2接RS485还有USART3接其他传感器。这三个外设虽然API长得一样但时钟源和引脚完全不同直接复制粘贴就会踩坑。串口时钟总线默认复用引脚中断服务函数USART1APB2 (72MHz)PA9/PA10USART1_IRQHandlerUSART2APB1 (36MHz)PA2/PA3USART2_IRQHandlerUSART3APB1 (36MHz)PB10/PB11USART3_IRQHandler实际项目中最常见的问题是只改了外设号没改RCC使能函数。比如把USART1的初始化复制到USART2只把USART_Init中的USART1改成USART2但RCC_APB2PeriphClockCmd里仍然使能的是USART1看起来初始化成功实际USART2时钟门没开写入寄存器全部无效发送超时。反过来如果在USART3上误用了APB2的72MHz去计算波特率波特率误差会达到100%对端收到的是连续帧错误标志。建议把每个串口的初始化封装成独立函数并在开头注释标明时钟总线和引脚这样后续维护时不会因为“换一个串口”引入隐蔽故障。还有一个细节USART2和USART3共用APB1总线在低功耗模式下APB1外设时钟可能会被关闭如果需要在待机唤醒后立刻通信要重新初始化USART2的波特率寄存器。4. 发送方向切换与中断接收从寄存器标志位到环形缓冲4.1 发送一帧数据的标准时序先拉高PG3再写数据等TC标志再拉低半双工RS485发送过程必须严格按时序走先把PG3拉高让收发器DE有效再写数据到发送数据寄存器。数据逐字节进入移位寄存器发出后最后一个字节还要等发送完成标志置位才能把PG3拉低否则最后一字节会在半途被切断从机收到的是缺尾帧。void RS485_SendString(char *str) { // 发送方向DE高有效 GPIO_SetBits(GPIOG, GPIO_Pin_3); // 逐字节发送判断TXE标志防止往满寄存器里继续写 while (*str) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, (uint8_t)(*str)); } // 等待最后一个字节完全移出移位寄存器 while (USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET); // 切回接收方向 GPIO_ResetBits(GPIOG, GPIO_Pin_3); }TXE标志表示发送数据寄存器已空可以写入下一个字节TC标志表示整个移位寄存器已经输出完毕。如果只用TXE判断最后把寄存器填满后立即拉低PG3物理上最后一个字节正在移位输出DE已经失效总线被释放数据帧尾部被硬生生截断。这是RS485通信里最常见也最难查的问题之一。调试时可以先用逻辑分析仪抓PG3的电平翻转时间和A/B差分输出正常时序应该是在起始位之前PG3就拉高停止位结束后至少1个bit时间再拉低。如果PG3拉低得太早大概率就是漏了TC等待。4.2 接收中断处理与环形缓冲接收端建议使用RXNE中断每收到一字节就自动存入环形缓冲。主循环只负责从缓冲区取数不会因为在解析数据时丢了后续字节。#define RS485_RX_BUF_SIZE 256 volatile uint8_t rs485_rx_buf[RS485_RX_BUF_SIZE]; volatile uint16_t rs485_rx_head 0; volatile uint16_t rs485_rx_tail 0; void USART2_IRQHandler(void) { uint8_t ch; uint16_t next; if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { ch (uint8_t)USART_ReceiveData(USART2); next (rs485_rx_head 1) % RS485_RX_BUF_SIZE; // 缓冲区满时丢弃新数据保留最旧数据 if (next ! rs485_rx_tail) { rs485_rx_buf[rs485_rx_head] ch; rs485_rx_head next; } USART_ClearITPendingBit(USART2, USART_IT_RXNE); } }这里用头尾指针实现环形缓冲初始化时头尾都指向0说明缓冲区为空。写入时头指针先加1再判断是否追上尾指针如果相等说明缓冲区满。读取时尾指针追赶头指针。这样设计和队列不同入队时不会覆盖还没读取的旧数据而是丢弃新数据适合现场噪声导致的大量垃圾帧堆积场景。需要注意环形缓冲的头尾变量在中断和主循环中都被访问如果主循环读数据时中断正好写入代码逻辑由于是单字节操作并不会出现撕裂但若要扩展成多字节判断建议在主循环里先关闭中断再取数据__disable_irq(); data rs485_rx_buf[rs485_rx_tail]; rs485_rx_tail (rs485_rx_tail 1) % RS485_RX_BUF_SIZE; __enable_irq();4.3 从机地址识别和strcmp的使用边界在多机RS485网络中主机发出的数据帧通常包含地址字节和设备命令。从机收到帧后首先检查地址是否与自己匹配匹配后再解析命令。这里使用strcmp是常见做法但UART收到的是字节流并不天然带字符串结束符。如果直接对一帧数据调用strcmp可能会越过帧尾读到环形缓冲里的旧数据形成越界读。正确的做法是维护一个接收帧数组当收到结束符比如\r\n或0x0D 0x0A时在帧尾补上\0然后再调用strcmp#define FRAME_MAX_LEN 32 char frame[FRAME_MAX_LEN]; uint8_t frame_len 0; void RS485_Parse_Frame(void) { while (rs485_rx_tail ! rs485_rx_head) { uint8_t ch rs485_rx_buf[rs485_rx_tail]; rs485_rx_tail (rs485_rx_tail 1) % RS485_RX_BUF_SIZE; if (ch \n) { frame[frame_len] \0; if (strcmp(frame, ATRST) 0) { // 处理复位命令 } frame_len 0; } else if (frame_len FRAME_MAX_LEN - 1) { frame[frame_len] ch; } else { frame_len 0; // 帧过长丢弃 } } }strcmp在STM32F103上会逐个字符比较效率不算高但用于短命令帧完全够。关键是要保证输入字符串确实以\0结尾。实验里最常见的错误是发送端发了不带结束符的固定数组接收端直接用strcmp去匹配结果在数组末尾遇到非零随机值永远匹配不上只能靠对比长度再加memcmp才能定位问题。4.4 主机应答超时重试机制半双工通信中主机发送完一帧后要切回接收方向等待从机应答。由于总线上可能有多台从机从机响应时间并不完全确定主机不能无限等下去。工程做法是在发送完成后启动一个软件超时计数器比如用SysTick提供1ms时基超过50ms没收到应答就重发。#define RESPONSE_TIMEOUT 50 uint32_t rs485_last_send_time; uint8_t rs485_waiting_response 0; void RS485_Send_With_Retry(char *cmd, uint8_t retry_times) { while (retry_times--) { RS485_SendString(cmd); rs485_waiting_response 1; rs485_last_send_time Get_Tick(); // 等待主循环或中断里收到应答并清除rs485_waiting_response while (rs485_waiting_response) { if (Get_Tick() - rs485_last_send_time RESPONSE_TIMEOUT) { break; } } if (!rs485_waiting_response) { return; } } }这里的rs485_waiting_response标志在收到合法应答帧时清零。超时不立即重发留出5ms总线空闲时间避免和上一帧尾部重叠。5. 用STM32CubeMX和HAL库移植RS485收发程序STM32CubeMX生成工程后主要工作从配置寄存器变成配置图形参数。UART2在CubeMX里默认名字是USART2波特率设为9600数据位8、停止位1、无校验。PG3在GPIO标签中设置为Output Push Pull电平拉低。这样生成的HAL库代码发送方向控制变得更直观。HAL库发送字符串时要注意HAL_UART_Transmit是阻塞函数会等待最后一个字节发送完成才返回不会像标准库那样需要手动等待TC标志。因此方向切换可以这样写void RS485_HAL_Send(uint8_t *data, uint16_t len) { HAL_GPIO_WritePin(GPIOG, GPIO_PIN_3, GPIO_PIN_SET); HAL_UART_Transmit(huart2, data, len, 100); HAL_GPIO_WritePin(GPIOG, GPIO_PIN_3, GPIO_PIN_RESET); }超时参数100表示最多等待100ms如果发送过程中总线出错不会死循环这会比标准库的while循环更安全。HAL库接收中断使用HAL_UART_Receive_IT开启后每收到一字节会进入回调但需要注意HAL库只接收单字节每接收完一字节需要重新调用一次uint8_t rx_byte; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { // 把rx_byte存入环形缓冲 HAL_UART_Receive_IT(huart2, rx_byte, 1); } }在移植过程中最容易忽略的是HAL库对GPIO方向引脚极性的设置。如果你在CubeMX里把PG3初始状态设为高在RS485收发器上就成了发送方向一上电总线就被本机占用其他设备发数据会冲突。所以初始电平必须为低。调试时建议优先用双通道逻辑分析仪抓PG3和UART2_TX。发送开始前PG3应该领先起始位至少10us拉高帧结束后PG3延迟1个bit宽度再拉低。如果看到PG3电平变化正常但远端从机收不到数据检查A/B差分线是否接反MAX485的A接对端AB接对端B。还要检查终端电阻长线传输时在总线首尾两端并联120欧姆电阻若没有终端匹配波形过冲会非常明显。另一个隐藏坑是PA11。PA11在STM32F103上同时连接到USB D-和CAN_RX如果板子里USB和CAN都有RS485调试串口又刚好复用了PA11做GPIO就会出现USB无法枚举或者CAN乱码。这个PA11 bug在F103上官方勘误里提过实验板如果USB和RS485走同一颗芯片尽量别动PA11把它保持为工具链默认的上拉状态。最后如果要用串口1和串口3同时工作调试串口建议放USART1RS485放USART2两者时钟源不同即使APB1出问题也不至于整个系统无法输出日志。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价