☰
STM32F103C8 RS485通信测试工程:方向控制与USART中断收发实战
2026/10/28 17:21:42 网站建设 项目流程

简介:这份资源是面向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,都有 DE(Driver Enable)和 RE(Receiver 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_TC(Transmission 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 Debugger,Settings 里能识别到 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 默认挂在 PA9(TX)和 PA10(RX),复用推挽输出和浮空输入。波特率常用 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,留足余量取 4ms;9600 下 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 模式。这样出问题时能快速定位是协议解析的锅还是硬件链路的锅。另外,方向控制脚在示波器上看一眼最踏实,发送时拉高、发完立刻拉低,中间不要有毛刺,有毛刺就说明切换时序被中断打断了,得把发送函数放进临界区或者关掉相关中断。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询