拓冰建站拓冰建站
首页 / 资讯中心 / 正文

AT32F403A多串口网关收发程序设计与踩坑实录

简介基于STM8S105微控制器的HW3000收发程序示例在IAR Embedded Workbench环境下开发面向需要实现、移植或调试HW3000无线通信功能的嵌入式开发者也适合正在学习STM8外设中断、UART/SPI等通信协议的学生参考。RAR压缩包内共639个文件以C语言源码和头文件为主还包含IAR工程文件ewp/eww、调试配置与映射文件、hex固件、CHM帮助文档、PDF说明以及文本说明文件整体压缩包大小16.87MB目录结构清晰便于按模块查找。代码注释详细覆盖硬件接口配置、串行通信协议、中断服务例程、数据缓冲区管理与错误检测等关键环节有助于理解HW3000与STM8S105之间的交互机制也可作为无线通信项目的基础框架进一步扩展。目前已有1104人学习下载A版本代码结构完整适合在IAR中直接打开编译、烧录验证后续可在此基础上继续完善功能。 HW3000这个项目是我去年接手的活儿。看到题目的朋友可能觉得不过是个串口收发但当你面对的是8路串口必须同时跑、每路都要收也要发、而且任何一路都不能丢数据的时候问题就从写个驱动变成了怎么设计一套不被带宽和中断拖垮的通信骨架。这篇文章我把HW3000的收发程序完整拆开讲从架构选型到代码实现再到踩坑过程适合正在做多串口网关、采集器、协议转换器这类设备的嵌入式工程师参考尤其是那些刚开始用雅特力AT32F403A的朋友。1. HW3000在做什么一台8路串口网关的硬件底子与需求拆解1.1 为什么规整到AT32F403A这颗芯片HW3000不是那种放在实验室里自娱自乐的小板子它的定位很实在工业现场数据采集网关。现场会有各种串口设备——电表、温控器、PLC、扫码枪——通过RS485或者RS232接到这台设备上网关负责把不同协议的数据汇总再通过网口或者另一路串口往上层平台送。选主控的时候我对比过几颗芯片。AT32F403A最打动我的地方是它片内集成了8个USART/UART外设。这就意味着我不需要外扩SPI转串口芯片不需要去跟并口时序搏斗8路串口的硬件资源是原生的。这颗芯片的内核是Cortex-M4F主频能拉到240MHz片上Flash有256KBSRAM有96KB对跑8路串口协议解析来说算力完全是溢出状态。而且雅特力的库函数风格比较规整从STM32迁移过来的工程师基本没有学习成本开发效率很高。1.2 8路同时收发到底难在哪先说一个很多人容易误判的点。8路串口同时收发听起来像是把8个独立的串口程序各写一遍实际上真正的难点在于并发资源的竞争。CPU只有一个中断控制器只有一个内存总线只有一条8路串口在同一时刻都有数据到达那这8个中断请求怎么排队排队的过程中数据放在哪里才不会丢更麻烦的还有时钟树。AT32F403A的8个串口分布在两条APB总线上APB2挂着USART1其他7个挂在APB1上。两条总线的外设时钟频率不同波特率发生器算出来的分频系数就不一样你在初始化的时候稍不留神某一路串口的实际波特率就会偏离预期。8路一起跑的时候波特率误差会互相叠加最终表现就是偶发乱码、校验错、粘包。此外CPU在跑应用层协议解析的时候不可能一直盯着串口寄存器。收发程序的设计目标应该是串口中断负责把数据快速搬进内存缓冲区应用层空闲了再从缓冲区里取数据做协议处理。这个搬进缓冲区的动作就是整个收发程序的灵魂。2. 收发程序的核心骨架环形队列加中断驱动别让CPU死等2.1 环形队列的设计读写指针、容量与互斥我最终选定的方案是环形队列Ring Buffer加中断驱动。为什么不直接用DMA加IDLE中断后面会细说。先讲环形队列这是多串口收发的第一块基石。环形队列本质上就是一个数组加两个指针一个写指针head一个读指针tail。数据写进来head往后挪数据读出去tail往后挪。两个指针追到数组末尾就回绕到开头像钟表指针一样转圈所以叫环形。#define UART_RX_BUF_SIZE 512 typedef struct { uint8_t buffer[UART_RX_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } ring_buf_t;读写指针为什么要加volatile因为这两个变量一个在中断里被修改写指针一个在主循环里被修改读指针编译器如果不知道它们随时可能变化就可能把读取操作优化掉导致逻辑错乱。这个坑我见过不止一次。队列容量选512字节是算过的。115200bps的波特率下1个字节的传输时间约86.8微秒8路同时跑满CPU每毫秒最多要处理大约92个字节。主频240MHz的Cortex-M4F一次中断进入退出加数据搬运大约需要2微秒左右应付这个量级绰绰有余。512字节足够缓冲在极端情况下的突发数据又不浪费内存——8个串口就是8乘512等于4KB的接收缓冲完全可接受。2.2 接收中断把每个字节在第一时间接住串口接收数据时硬件会把收到的字节放进数据寄存器同时置起接收数据满标志触发中断。中断服务函数里的任务只有一个读出数据塞进队列清标志退出。void USART1_IRQHandler(void) { if (usart_interrupt_flag_get(USART1, USART_INT_FLAG_RDBF) ! RESET) { uint8_t ch (uint8_t)usart_data_receive(USART1, USART_DATA_8BITS); ring_buf_write(uart1_rx_queue, ch); } }对应的环形队列写入函数bool ring_buf_write(ring_buf_t *rb, uint8_t data) { uint16_t next (rb-head 1) % UART_RX_BUF_SIZE; if (next rb-tail) { return false; // 队列满丢弃并返回失败 } rb-buffer[rb-head] data; rb-head next; return true; }这里有个细节判断队列满用的是head的下一个位置等于tail也就是说数组里最多存UART_RX_BUF_SIZE - 1个字节。这样做的好处是空和满两种状态不会歧义——两个指针相等时一定为空不需要额外计数器。为什么不在中断里直接做协议解析因为协议类型不同数据边界需要积累到一定长度才能判断。中断里能做的只是快进快出把宝贵的CPU时间留给主循环。我的原则是中断里只干一件事——搬数据超过三行的逻辑都是在给自己埋雷。2.3 发送路径三种方案里为什么选了TXE中断发送比接收麻烦因为发送是主动的。接收有硬件中断推着你走发送则需要应用层主动把数据交给串口。常用的发送方案有三种。第一种是阻塞式用一个for循环往数据寄存器里一个字节一个字节写每写一个要等发送完成标志置位再写下一个。这种方式代码最简单但8路中有任何一路在发长帧CPU就卡在那里干等其余7路的数据处理全部停滞这在同时收发的场景下是灾难。第二种是TXE中断发送先把要发的一整个帧放进发送队列然后使能发送寄存器空TXE中断中断每次触发就取队列里的下一个字节写入数据寄存器队列空了就关中断。这样CPU不需要等中断自己会一点点把数据送完。第三种是DMA发送一次性把帧的首地址和长度交给DMA控制器DMA搬完触发完成中断。效率最高但对DMA通道占用多8路都用DMA发加上接收也要用DMA通道资源会非常紧张。最后我给HW3000选了TXE中断发送。原因很简单波特率最高用到115200CPU有充足的时间在中断里逐个搬字节没必要为了极致效率把手里的DMA通道全押进去。留一些通道给后续扩展比如SPI Flash读写、ADC采样更从容。void uart_send_frame(uint8_t port, uint8_t *data, uint16_t len) { // 关中断保护环形队列写入避免与发送中断冲突 __disable_irq(); for (uint16_t i 0; i len; i) { ring_buf_write(tx_queue[port], data[i]); } __enable_irq(); // 使能TXE中断发送流程交给中断驱动 usart_interrupt_enable(peripheral[port], USART_INT_TXE, TRUE); }3. 关键实现细节把代码写对不如把初始化和竞争处理对3.1 初始化顺序踩过的一个大坑串口初始化看起来无非是开时钟、配引脚、设波特率、开中断但顺序不对就会出诡异问题。我最初的写法是先配置GPIO复用再配置串口参数最后初始化中断优先级。结果发现有一路串口偶发性地一上电就收到0xFF字节查了半天是GPIO配置的瞬间引脚悬空导致串口误触发接收标志。正确的顺序应该这样先打开串口外设时钟并关闭接收功能然后配置GPIO复用为串口功能再配置串口参数和使能接收最后才开中断。引脚还没复用好之前绝对不能让串口外设处于接收使能状态。用大白话说先让硬件稳定的引脚接上再让外设开始监听。AT32F403A的库函数里引脚复用配置需要用到GPIO_PinsPackConfig或者逐个设置GPIO_ModeMux不同封装、不同引脚组合对应的复用映射不一样。初始化之前一定翻开数据手册的引脚复用表把每个串口的TX、RX对应哪个引脚确认清楚再对着库函数写代码。3.2 中断优先级的分配8个中断怎么排队Cortex-M4内核用NVIC管理中断优先级AT32F403A支持4位优先级最多16个等级。硬件上8个串口的中断是可以互相抢占的如果8个串口的优先级都设成一样那同一时刻有多个中断触发就得按中断编号排队后面的串口数据只能等。等待期间数据依然到达硬件寄存器但如果前一个中断处理时间稍长新数据可能被覆盖。我的做法是把8个串口的优先级错开。设置分组为NVIC_PriorityGroup_4即只用抢占优先级不用子优先级。8个串口按端口号分配抢占优先级从3到10这样同一时刻即便多路同时来数据中断也能逐级抢占每路数据都有机会被及时搬走。应用层主循环跑在最低优先级不会被串口之外的事件干扰。优先级分配的原则是波特率高的、数据量大的串口优先级高。比如1号串口接到电表集中器通信最频繁优先级给最高4号串口接一个很少上报的温湿度探头优先级排后面完全没问题。nvic_irq_enable(USART1_IRQn, 3, 0); nvic_irq_enable(USART2_IRQn, 4, 0); nvic_irq_enable(UART3_IRQn, 5, 0); // ... 依次类推UART8给优先级103.3 粘包问题串口没有消息边界协议层必须自己划串口是字节流没有消息边界的概念。8路串口同时收数据应用层从环形队列里取出来的是一长串没有断句的字节如果不做分帧就完全无法判断一帧数据从哪开始、到哪结束。HW3000对接的现场协议五花八门但都有两个端点标记要么是固定的帧头帧尾要么是帧头加长度字段。我在收发程序里专门写了一个协议解析模块策略是从环形队列里逐字节读找到帧头就进入搜寻状态然后根据协议类型读取长度字段凑满长度后校验CRC校验通过才把整帧上抛给业务层。frame_state_t parse_fsm(ring_buf_t *rb, protocol_t *proto) { // 状态机IDLE - HEADER - LENGTH - DATA - CRC while (ring_buf_count(rb) 0) { uint8_t ch; ring_buf_read(rb, ch); switch (state) { case IDLE: if (ch proto-header) state HEADER; break; case HEADER: proto-len ch; proto-index 0; if (proto-len 0) state DATA; break; case DATA: proto-frame[proto-index] ch; if (proto-index proto-len) state CRC; break; case CRC: // 校验通过则上抛 state IDLE; break; } } }这个状态机的好处是即使某一帧数据损坏、帧头丢失下一帧正常的帧头也能把状态拉回来。8路串口各自的协议解析相互独立互不影响。4. 实测踩坑记录三个让人挠头的问题排查链路4.1 中断抢占导致的高频丢数第一次联调8路串口全部挂上模拟设备跑数据短时间看不出问题跑上十分钟后开始出现偶发丢字节。起初怀疑是环形队列太小把缓冲区加大到2048字节情况没有明显改善。后来在调试器里打断点看丢了哪个串口的数据发现丢数的串口固定是那一两个优先级较低的。排查链路先确认优先级是否真的生效把NVIC配置打出来看发现事情经过是这样的——我虽然把优先级设置了不同数值但因为代码中某次重写了优先级分组导致分组模式从第4组变成了第3组原先设置的10个抢占优先级被压缩成了抢占加子优先级混合模式部分串口的实际抢占优先级变成了相同值。同优先级的多个中断同时请求时硬件只按中断编号排队低序号的中断先执行高序号的中断只能等等待期间新数据就丢了。修复方式初始化时只调用一次nvic_priority_group_config(NVIC_PriorityGroup_4)并把这行代码放在所有nvic_irq_enable之前排查掉了一处库函数内部悄悄修改分组的地方。从此8路高负载跑满24小时也不再丢数。4.2 波特率误差叠加两条APB总线上的分频陷阱有一段时间2号串口和7号串口收发1000字节出现一两个错码频率不高但很顽固。用示波器量波形发现这两路串口的TX引脚电平宽度比标准波特率的位宽略宽一点。查时钟树AT32F403A的USART1挂在APB2最高240MHz其余串口挂在APB1最高120MHz。串口时钟源默认取自各自挂载的APB总线两路如果都设置115200算出来的分频系数一个是整数一个是小数小数分频取整后波特率产生误差。一般单个串口容许约2%的波特率误差但对面设备如果本来也有累差加上抗干扰能力一般错码就出来了。我的解决办法很简单把这两路串口的波特率调整到19200。19200在任何时钟频率下分频误差都很小再加上工业现场这个速率也足够用。收发程序里我为每一路串口单独保存波特率配置不再图省事统一用同一个值。4.3 TXE中断与发送队列的竞争关中断位置不对导致偶发粘帧早期版本发送一个帧超过64字节时偶尔出现两帧数据之间混入上一帧末尾的残留数据。排查到最后是关中断的位置问题。我在主循环里调用发送函数函数先往环形队列写数据再使能TXE中断。如果前一个发送中断已经在运行主循环这边写入队列的过程中发送中断又抢进来取走了队列里的数据而主循环还没写完一整个帧发送中断就把半帧发出去了。修复策略往发送队列写数据的全程关中断写完立刻开中断。中断被禁掉的窗口非常短只有memcpy进缓冲区的时间手动测量约几个微秒对整体实时性没有影响。这是所有共享数据操作的通用法则——改共享数据的那几行代码要么关中断要么加锁没有第三条路。5. 怎么证明这台设备真能同时收发测试方法分享5.1 自环加随机数据压力测试硬件验证阶段我写了一个自环测试固件每个串口把收到的数据原样发回去。上位机用Python加pyserial库同时打开8个串口每个串口独立线程发随机长度的随机数据。import serial import threading import random def echo_test(port, duration3600): s serial.Serial(port, 115200, timeout1) start time.time() while time.time() - start duration: payload bytes([random.randint(0, 255) for _ in range(random.randint(1, 128))]) s.write(payload) recv s.read(len(payload)) if recv ! payload: with open(error.log, a) as f: f.write(f{port} mismatch: {recv.hex()} ! {payload.hex()}\n) s.close()8个线程同时跑对比收发数据是否一致。这个方法能最快暴露丢数据、粘包、乱序三类问题。HW3000连续跑了一晚上8路总共收发约300MB数据零差错才算过关。5.2 真实负载下的长时间稳定性自环测试通过后我还没有立刻交付。把真正的协议解析挂上去接上几台真实的现场设备按照实际运行频率跑48小时。真实负载和自环最大的区别在于设备主动上报的时间点是随机的、突发性的可能几毫秒内多路同时上报也可能几分钟没有数据。这种不确定性对收发程序的缓冲能力和临界区保护是终极考验。我在这轮测试里加了一个统计模块每路串口每15分钟打印一次收发字节数、丢包数、队列最大占用深度。根据队列深度可以反过来调整缓冲大小我们的真实数据验证结果是512字节的接收队列最大占用约60%裕量充足收发程序不需要再调整。6. 后续还能怎么扩展HW3000这套收发程序做完之后其实还可以往两个方向扩展。一个是把发送路径从TXE中断切换到DMA加发送空闲中断进一步降低CPU占用率适合串口速率提升到460800甚至921600的场景另一个是把8路串口的协议解析做成插件化比如Modbus RTU、DL/T645、自定义协议都注册成独立解析器通过配置文件决定每路串口跑哪种协议。硬件资源方面AT32F403A还剩很多余量240MHz主频、几十KB空闲RAM、充足的Flash空间。做本地边缘计算都够了不只是数据透传。下一版HW3000我计划把MQTT协议栈加上去让网关直接把解析好的数据发布到云平台省掉中间层服务器转发。串口收发程序这块地基打稳了上层应用怎么盖楼都不慌。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门