简介这份资源是面向STM32初学者与嵌入式开发者的RS485通信测试KEIL工程源码基于STM32F103C8单片机ARM Cortex-M3内核实现帮助读者理解GPIO方向控制、UART收发、中断处理与协议解析等核心环节。压缩包共230个文件以48个h头文件、45个c源文件为主另含32个o目标文件、32个d依赖文件、31个crf交叉引用文件及uvproj、uvopt等KEIL工程配置整体约5.94MB可直接在KEIL中打开编译与调试。源码围绕RS485半双工通信展开涉及DE/RE引脚切换、MODBUS RTU或自定义帧格式的构建与校验、接收完成与错误中断处理并附带测试程序模拟发送、应答与解析流程便于对照调试与问题定位。目前已有799人学习下载适合希望从硬件配置到软件调试系统掌握STM32通信开发的读者参考实践。1. 从一块 F103C8 最小系统板开始RS485 通信测试工程到底在测什么手头有一块 STM32F103C8 最小系统板想验证 RS485 通信能不能跑通最直接的办法不是去啃收发器手册而是先跑一个最小测试工程单片机定时往外发一串数据同时把收到的数据回显出来用串口助手一看就知道链路通不通。这个 KEIL 工程源码要解决的正是这件事——它把 STM32F103C8 的 USART、GPIO 方向控制、RS485 收发器时序这几块拼在一起形成一个可编译、可下载、可观察的闭环。它适合两类人一类是刚接触 RS485 组网、需要一份能直接编译下载的参考工程来改的嵌入式新手另一类是手头有 Modbus 或自定义协议要对接、想先确认硬件链路和方向切换时序没问题的老手。核心不在代码多复杂而在于方向控制引脚和发送完成标志这两个点有没有处理干净处理不干净就会出现发出去收不到、多设备组网时总线冲突这类玄学问题。2. RS485 半双工方向控制为什么 90% 的通信失败都出在这里RS485 是半双工总线收发共用一对差分线所以必须有一个引脚去控制收发器的方向发送时拉高或拉低取决于收发器型号接收时反过来。STM32F103C8 本身没有硬件自动方向控制得靠软件在发送前切换、发送完成后切回。这一步如果时序不对要么发出去的数据被自己吃掉要么总线一直被占用导致其他设备无法应答。2.1 收发器 DE/RE 引脚与 STM32 GPIO 的对应关系常见的 RS485 收发器比如 MAX485、SP3485、ADM2483都有 DEDriver Enable和 REReceiver Enable低有效两个控制脚。多数模块会把 DE 和 RE 短接在一起引出一个方向控制脚高电平发送低电平接收。接线时这个脚接到 STM32 的任意 GPIO 即可我一般选 PA8 或 PB12 这类不跟 USART 复用的脚避免跟串口引脚抢资源。收发器型号方向控制逻辑典型供电备注MAX485DE 高发送RE 低接收5V需外接 120Ω 终端电阻SP3485同上3.3V可直接与 F103 电平匹配ADM2483带隔离逻辑同上5V/3.3V隔离型成本高但抗干扰好选型时注意一点如果收发器是 5V 供电而 STM32 是 3.3V方向控制脚和 RO 脚最好加电平转换否则长期跑可能损伤 IO。很多翻车案例就是 5V 收发器直接怼 3.3V 单片机跑几天没事跑几个月 IO 就挂了。2.2 发送完成标志 TC 与方向切换的先后顺序这是整个工程最核心的一行逻辑。很多人用USART_SendData发完最后一个字节就立刻把方向脚拉低结果最后一个字节还没移出移位寄存器收发器就被切到接收态总线上最后一个字节直接丢掉。正确做法是等USART_FLAG_TCTransmission Complete置位后再切方向。// 发送一个字节数组发送完成后切回接收方向 void RS485_SendBytes(uint8_t *buf, uint16_t len) { uint16_t i; RS485_DIR_TX(); // 拉高方向脚进入发送态 for (i 0; i len; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, buf[i]); // 逐字节写入 DR } // 关键等 TC 置位确保最后一个字节完全移出 while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); RS485_DIR_RX(); // 切回接收态 }逻辑说明TXE只表示发送数据寄存器空可以写下一个字节但不代表最后一个字节已经出现在总线上。TC才表示移位寄存器也空了整个帧发送完毕。参数上RS485_DIR_TX()和RS485_DIR_RX()就是 GPIO 置位和复位宏具体哪个脚根据你的板子改。如果收发器方向逻辑相反把这两个宏对调即可。2.3 用 KEIL 建立 F103C8 工程的最小步骤拿到源码包后不要急着编译先确认 KEIL 里器件包和启动文件对得上。F103C8 属于中容量产品启动文件选startup_stm32f10x_md.s预定义宏写STM32F10X_MD, USE_STDPERIPH_DRIVER。如果选成hd或ld编译能过但中断向量表偏移不对跑起来会莫名其妙进 HardFault。# KEIL 工程关键配置Options for Target # Device: STM32F103C8 # Target - Xtal: 8.0 MHz # C/C - Define: STM32F10X_MD, USE_STDPERIPH_DRIVER # Debug - 选 ST-Link DebuggerSettings 里能识别到 SWD 即可步骤上先新建工程选好器件再把标准外设库的stm32f10x_gpio.c、stm32f10x_usart.c、stm32f10x_rcc.c加进来最后把测试主文件加入。编译前检查一下system_stm32f10x.c里的时钟配置默认是 72MHz如果你的晶振不是 8MHz要改HSE_VALUE和 PLL 倍频。3. USART 初始化与中断接收把收发逻辑拆成两个独立通道发送用阻塞方式最简单接收如果也用阻塞轮询主循环会被拖死稍微来点连续数据就丢包。所以工程里一般发送走阻塞、接收走中断两个通道互不干扰。中断里只做一件事把收到的字节塞进环形缓冲区主循环再去解析。3.1 USART1 参数配置与 GPIO 复用F103C8 的 USART1 默认挂在 PA9TX和 PA10RX复用推挽输出和浮空输入。波特率常用 9600 或 115200数据位 8、停止位 1、无校验。如果对接的是台达 MS300 变频器这类设备奇偶校验位要跟变频器参数对齐常见是偶校验Even设错了会一直收到乱码或帧错误。void USART1_Init(uint32_t baud) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_9; // TX gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_10; // RX gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); usart.USART_BaudRate baud; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; // 对接变频器时改成 Even usart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; usart.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART1, usart); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }参数说明USART_Parity_No改成USART_Parity_Even时USART_WordLength仍写 8b库会自动把校验位算进去实际线上是 1 起始 8 数据 1 校验 1 停止。如果对接设备要求 2 停止位改USART_StopBits_2。波特率误差方面72MHz 下 115200 的误差约 0.16%可以接受9600 误差更小。3.2 环形缓冲区与中断服务函数中断里不要做解析只做搬运。环形缓冲区大小取 128 或 256 字节够应付大多数 Modbus RTU 帧。读写指针用volatile修饰防止编译器优化掉。#define RB_SIZE 128 static volatile uint8_t rb_buf[RB_SIZE]; static volatile uint16_t rb_head 0, rb_tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART1); uint16_t next (rb_head 1) % RB_SIZE; if (next ! rb_tail) { // 缓冲区未满才写入 rb_buf[rb_head] ch; rb_head next; } // 满了就丢弃避免覆盖未读数据 } } uint16_t RB_Read(uint8_t *out, uint16_t max) { uint16_t cnt 0; while (rb_tail ! rb_head cnt max) { out[cnt] rb_buf[rb_tail]; rb_tail (rb_tail 1) % RB_SIZE; } return cnt; }逻辑说明head由中断推进tail由主循环推进两者相等表示空。判断满的条件是(head 1) % SIZE tail这样会浪费一个字节空间但换来实现简单、无需额外计数器。主循环里调RB_Read把数据取出来再按协议解析。3.3 主循环里的收发测试逻辑测试工程的主循环不需要复杂状态机先做回显收到什么原样发回去。这样用串口助手发一串数据能收到相同内容就说明收发链路和方向切换都正常。int main(void) { uint8_t buf[64]; uint16_t n; SystemInit(); RS485_GPIO_Init(); // 方向控制脚初始化 USART1_Init(115200); RS485_DIR_RX(); // 默认接收态 while (1) { n RB_Read(buf, sizeof(buf)); if (n 0) { RS485_SendBytes(buf, n); // 回显 } } }这段代码跑通后把回显改成解析 Modbus 功能码或者改成定时主动发送就能往实际项目上靠。注意RS485_SendBytes是阻塞的如果发送数据量大且波特率低主循环会被占住接收中断仍然能进但缓冲区可能溢出所以发送长度要控制。4. 避坑与排查RS485 测试工程里最容易翻车的 5 个点4.1 现象串口助手能收到数据但发下去没反应原因方向控制脚一直停在发送态收发器没切回接收总线上的数据进不来。或者方向脚接错实际接的是 RE 而 DE 悬空。解决用万用表量方向脚在空闲时的电平应该是接收态对应的电平。如果反了把RS485_DIR_TX()和RS485_DIR_RX()的置位/复位对调。再确认收发器 DE 和 RE 是否短接没短接的话两个脚都要控。4.2 现象短距离通信正常线一长就丢包或乱码原因总线两端没有接 120Ω 终端电阻信号反射导致波形畸变。或者 A/B 线接反短距离时差分阈值还能勉强识别线长了就彻底不行。解决在总线最远两端各并一个 120Ω 电阻中间设备不要接。A/B 线用双绞线别用普通平行线。如果现场干扰大换隔离型收发器或者加共模电感。4.3 现象多设备组网时一台上电其他设备就通信异常原因多个设备同时处于发送态总线被强行拉死。常见于方向控制脚上电默认电平不对或者某台设备程序跑飞后方向脚一直高。解决方向脚初始化后立刻设为接收态收发器选带失效保护fail-safe的型号总线空闲时输出确定电平。软件上加超时机制发送超过一定时间强制切回接收。4.4 现象KEIL 编译通过下载后不进 main 或进 HardFault原因启动文件选错中容量芯片用了大容量启动文件中断向量表偏移不对。或者堆栈大小设太小中断嵌套时溢出。解决确认启动文件是startup_stm32f10x_md.s预定义宏是STM32F10X_MD。在startup文件里把堆栈改大比如 Stack_Size 改成 0x400。如果用了 printf 浮点还要勾选 Use MicroLIB。4.5 现象接收中断偶尔丢字节数据帧不完整原因中断优先级被其他中断打断太久或者环形缓冲区太小主循环处理不及时导致溢出。也可能是波特率误差累积到帧尾采样点偏移。解决把 USART1 中断优先级设高一点抢占优先级给 0 或 1。缓冲区从 128 加到 256 或 512。检查SystemCoreClock是否真的是 72MHz时钟不对波特率全错。5. 从回显测试到 Modbus RTU 从机把工程改成能对接变频器的实用工具回显跑通只说明物理层和方向控制没问题真正要投入项目下一步是把它改成 Modbus RTU 从机能响应功能码 03读保持寄存器和 06写单寄存器。这样就能直接对接台达 MS300 这类变频器读频率、电流或者写启停命令。改造的核心是加一个帧超时判断。Modbus RTU 用 3.5 个字符时间作为帧间隔115200 波特率下约 300 微秒。用定时器做一个 1ms 滴答主循环里检测如果缓冲区有数据且超过 4ms 没有新字节进来就认为一帧结束开始解析。// 简化的 Modbus RTU 帧接收判断 static uint32_t last_rx_tick 0; static uint16_t frame_len 0; static uint8_t frame[256]; void Modbus_Poll(void) { uint8_t tmp[64]; uint16_t n RB_Read(tmp, sizeof(tmp)); if (n 0) { if (frame_len n sizeof(frame)) { memcpy(frame[frame_len], tmp, n); frame_len n; } last_rx_tick GetTick(); // 每收到字节刷新时间戳 } // 超过 4ms 没有新字节认为帧结束 if (frame_len 0 (GetTick() - last_rx_tick) 4) { Modbus_HandleFrame(frame, frame_len); frame_len 0; } }参数说明GetTick()返回毫秒计数由 SysTick 中断累加。4ms 是经验值115200 下 3.5 字符约 0.3ms留足余量取 4ms9600 下 3.5 字符约 4ms这时要取 5ms 或 6ms。帧长度上限 256 字节够放标准 Modbus 帧。Modbus_HandleFrame里先校验 CRC再判断地址是否匹配最后按功能码分支处理。验证方法用 Modbus Poll 或串口助手发01 03 00 00 00 01 84 0A从机应返回01 03 02 XX XX CRC。如果返回异常码 0x83说明功能码不支持或寄存器地址越界。如果完全没返回先查地址对不对、CRC 对不对、方向切换有没有问题。我自己的习惯是每改一版协议处理先用回显模式确认物理层没动过再切到 Modbus 模式。这样出问题时能快速定位是协议解析的锅还是硬件链路的锅。另外方向控制脚在示波器上看一眼最踏实发送时拉高、发完立刻拉低中间不要有毛刺有毛刺就说明切换时序被中断打断了得把发送函数放进临界区或者关掉相关中断。希望帮到你。本文还有配套的精品资源点击获取