ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AT32F403A多串口与HW3000 Sub-1G无线收发实战解析

AT32F403A多串口与HW3000 Sub-1G无线收发实战解析 简介围绕HW3000无线模块这是一套基于STM8S105微控制器的数据收发示例工程面向嵌入式开发者和物联网通信方向学习者用于掌握串行通信、引脚控制及中断处理等底层编码方法。代码使用IAR环境开发注释详细通俗涵盖硬件引脚配置、通信协议初始化、中断服务例程、数据缓冲区管理、校验及同步机制等完整收发流程便于对照实物或开发板进行二次移植。该工程为A版本着重展示基础收发逻辑可作为后续功能迭代与优化的起点。压缩包共639个文件以C源码127个c、头文件135个h、工程配置文件ewp/ewd/eww、文本说明及编译输出为主另含PDF、HEX、汇编等辅助文件整体约16.87MB目录结构清晰适合按模块逐项研读。已有1104人浏览学习适合希望通过真实工程案例快速理解HW3000无线模块工作机制并提升STM8嵌入式开发能力的读者。 搞嵌入式的朋友应该都遇到过这种需求现场一堆传感器、仪表的数据要统一收上来再集中送到后台。方案看起来很多用4G模组吧一年流量费比设备还贵用WiFi吧厂区里钢梁一多信号就开始飘忽。我最后选的是一套看起来很“老派”但异常稳的组合主控用雅特力的AT32F403A一口气挂8个串口无线端用华普微的HW3000做Sub-1G频段收发。整套硬件成本压到三四十块钱无遮挡实测距离能到400米穿两堵水泥墙还有信号余量比2.4G方案稳太多了。这篇文章就把这套“HW3000收发程序”的完整思路和代码细节拆开来讲重点是解决两个问题一是HW3000这种纯SPI从机芯片怎么高效驱动起来二是AT32F403A的8个串口怎么在“同时收发”的场景下不丢数据、不互相饿死。无论你是做无线采集终端、工业数据透传还是多点布线的替代方案这篇都可以直接拿来参考。1. 为什么是HW3000 AT32F403A这套组合1.1 项目需求与选型逻辑这个项目的需求其实很朴素一个采集终端接8路RS232/RS485设备每次轮询设备或者设备主动上报数据要能通过无线方式送到汇聚节点。汇聚节点再通过串口或者以太网把数据转发给上位机。整个系统要求低成本、低功耗、能穿墙、抗干扰。把需求拆开看就三条主控串口数量必须够多无线链路必须便宜且稳定软件上必须能支撑“多路同时收发”。主控选AT32F403A的理由非常直接这颗料是Cortex-M4F内核主频跑到240MHzFlash最大512KBRAM最大224KB关键是它有8个独立的USART。这个串口数量在同价位芯片里几乎没有对手。STM32F103同级别的芯片大多只有3到5个串口数量不够就得外扩串口芯片成本和布线难度都上去了。无线端没有选LoRa也没有选2.4G而是选了HW3000原因有三第一HW3000工作在470-510MHz频段这个频段绕射能力强厂房里铁皮、设备、货架对信号的衰减比2.4G小得多第二芯片价格便宜一块钱左右比动辄十几块钱的LoRa模块有优势第三HW3000自带64字节收发FIFO、硬件CRC、前导码检测把大部分链路层脏活累活都干完了主控要写的驱动比nRF24L01还简单。1.2 关键硬件资源盘点有用过AT32F403A的朋友应该知道它的8个串口虽然多但引脚复用关系比较绕。我实测的资源占用情况是这样外设数量用途USART11与HW3000调试/配置预留USART2~USART65接现场设备RS485USART71接本地按键/显示小板USART81接上位机RS232SPI11驱动HW3000DMA1/DMA2若干串口收发缓冲这里有一个关键经验8个串口如果每个都用中断一个字节一个字节收那中断频率会非常高主程序根本别想干别的。所以必须用DMA方式接收配合USART的空闲中断来判定一帧结束。这套组合拳打下来8个串口同时收数据时CPU占用率能控制在15%以内留给协议处理和时间管理的余量就很大了。2. HW3000收发机制底层怎么跑起来2.1 模式切换与SPI命令链路HW3000本质上是一个SPI从机主控通过SPI写寄存器、写FIFO、发命令来控制它。芯片内部状态机分为SLEEP、IDLE、TX、RX四种模式切换方式就是通过SPI发命令。我在驱动里封装了一套基础命令实际用到的就这几个#define HW3000_CMD_SLEEP 0x10 #define HW3000_CMD_IDLE 0x11 #define HW3000_CMD_STX 0x12 #define HW3000_CMD_SRX 0x13 #define HW3000_CMD_WRITE_FIFO 0x30 #define HW3000_CMD_READ_FIFO 0x31 #define HW3000_CMD_READ_REG 0x40 #define HW3000_CMD_WRITE_REG 0x00调用的时候要先拉低片选发命令字节然后根据命令决定是继续读写数据还是结束。需要特别留意的是HW3000对SPI时序要求比较严格SCK空闲电平、采样沿、片选释放时间都要按数据手册来错了芯片直接不响应。2.2 帧格式、FIFO与关键时序HW3000的信道配置支持FSK、GFSK、OOK几种调制方式我实测下来GFSK在2kbps到100kbps之间表现最均衡。内部自带64字节FIFO可以一次性写入几十个字节发完或者收满芯片都会通过GIO引脚拉中断通知主控。整条无线链路的数据帧格式我在项目里固定成下面这种前导码同步字长度目标地址数据区CRC自动生成0x3C3C1字节1字节N字节硬件计算前导码和同步字是硬件自动处理的主控只需要在初始化时把同步字寄存器配好。长度字段表示目标地址数据区的总字节数目标地址用来区分这个包到底应该从哪个串口发出去。CRC由HW3000硬件生成和校验能滤掉绝大部分错误帧。时序方面有组数据要记牢SLEEP到IDLE稳定时间需要5ms左右IDLE到TX或RX需要2ms左右TX过程中芯片会在数据发完后自动回到IDLE。程序里做收发切换时必须先发IDLE命令等2ms以上再切到目标模式否则芯片状态会乱表现出来就是“有时候发不出、有时候收不到”。2.3 初始化配置要点HW3000初始化的一大堆寄存器配置本质上是在做三件事定频点、定射频参数、定数据格式。void hw3000_init(void) { // 1. 先进入IDLE模式等待晶振稳定 hw3000_set_mode(HW3000_IDLE); delay_ms(10); // 2. 配置频点默认470MHz计算公式参照数据手册频率寄存器 hw3000_write_reg(REG_PLL_0, 0x01); hw3000_write_reg(REG_PLL_1, 0x23); hw3000_write_reg(REG_PLL_2, 0x45); // 3. 配置数据率与频偏这里用GFSK, 19.2kbps hw3000_write_reg(REG_BITRATE_H, 0x03); hw3000_write_reg(REG_BITRATE_L, 0xC0); hw3000_write_reg(REG_FREV_H, 0x02); hw3000_write_reg(REG_FREV_L, 0x80); // 4. 配置发射功率13dBm hw3000_write_reg(REG_TX_POWER, 0x3F); // 5. 使能CRC、自动前导码 hw3000_write_reg(REG_PKT_CTRL, 0x20); // 6. 配置GIO中断脚方便主控查询收发完成 hw3000_write_reg(REG_GIO, 0x01); }寄存器地址在不同版本的数据手册上略有差异但配置逻辑是一致的先定频率再定收发参数最后定包格式。新手最容易犯的错是只配置了频点没配置频偏结果发送端和接收端的解调带宽不一样距离稍微远一点就收不到。3. AT32F403A八个串口的资源调度设计3.1 串口-设备地址映射方案8个串口同时收发第一件要做的事是定义清楚“一个数据包从哪个串口进来该从哪个串口出去”。我在协议里加了一字节“目标地址”地址和串口之间做成固定映射表。// 串口地址映射 typedef struct { uint8_t addr; // 地址编号 USART_TypeDef *usart; // 对应的串口 } usart_map_t; const usart_map_t usart_table[] { {0x01, UART2}, {0x02, UART3}, {0x03, UART4}, {0x04, UART5}, {0x05, UART6}, {0x06, UART7}, {0x07, UART8}, };当HW3000收到一条无线数据时主控解析出目标地址字段查表找到对应的USART然后走DMA发送。如果目标地址是0x00就默认从USART1发出去这个口我预留给了调试或者转发给上位机。3.2 中断/DMA与环形缓冲8个串口同时接收每个口都用阻塞接收是不可能的。我的方案是每个串口配一条DMA接收通道数据直接进内存同时开USART的空闲中断当DMA收完一串数据并且总线上出现空闲就认为一帧结束把长度记录下来通知协议层处理。核心代码大致是这样#define USART_DMA_MAX_LEN 256 typedef struct { uint8_t data[USART_DMA_MAX_LEN]; volatile uint16_t len; volatile uint8_t locked; } uart_frame_t; uart_frame_t uart_frames[8]; void uart_dma_rx_init(void) { for (int i 0; i 8; i) { UART_DMA_Config(i, (uint32_t)uart_frames[i].data, USART_DMA_MAX_LEN); UART_EnableDMA(i); UART_EnableIDLEInterrupt(i); } }当空闲中断触发时在中断服务函数里读DMA当前剩余计数器用“最大长度-剩余计数”算出实际收到几个字节然后把locked置1禁止DMA继续写主循环处理后置0重新开始接收。这样设计的好处是8个串口互相隔离哪个口忙不影响其他口。3.3 无线链路与串口链路的速度匹配这里有个特别容易掉坑的地方串口速率往往比无线速率快得多。比如串口用115200bps而HW3000无线侧我配置的是19.2kbps无线速率那么就算串口同时涌进来15KB数据无线也得搬好一阵子。解决办法是在主循环里做“按帧调度先入先出满则丢弃”的策略。每个串口维护一个发送队列队列只缓存最近几帧超过缓存上限就丢掉最早的数据并向上层报告丢帧计数值。这比无限堆积要务实得多因为现场设备大多是周期性上报丢一帧旧数据等下一帧就行反而堆积会导致实时性下降。4. 收发程序核心实现完整代码与流程4.1 初始化流程上电后按照“先主控后外围、先本地后无线”的顺序初始化void system_init_all(void) { // 1. 系统时钟AT32F403A默认内部PLL跑240MHz system_clock_config(); // 2. 初始化8个串口的GPIO、波特率、DMA、中断 uart_all_init(); uart_dma_rx_init(); nvic_config(); // 3. 初始化SPI1速率建议先设在1MHz以下稳定后再拉高 spi1_init(); hw3000_init(); // 4. 初始化为RX模式等待接收 hw3000_set_mode(HW3000_RX); }这里有个细节SPI速率不要一开始就拉到18MHz很多板子布局不好高速SPI会心跳干扰无线接收。我一般先把SPI配到1MHz完成所有寄存器配置确认能读写之后再把速率调到4MHz收发性能完全够用因为一次最大才64字节。4.2 串口到无线的发送链路发送链路可以分成两个阶段本地串口收帧、无线帧发送。第一阶段由DMA空闲中断把完整帧收进uart_frames[n]。第二阶段是主循环里把这些帧按顺序转发到无线模块。void uart_to_wireless_task(void) { for (int i 0; i 8; i) { if (uart_frames[i].len 0 uart_frames[i].locked) { uint8_t frame_buf[64]; uint8_t target_addr 0x00; // 判断是否需要进入待发送队列 frame_buf[0] i 1; // 源地址 frame_buf[1] uart_frames[i].len; // 数据长度 memcpy(frame_buf[2], uart_frames[i].data, uart_frames[i].len); // 交给HW3000发送带发送队列防堵车 wireless_send_queue_push(frame_buf, uart_frames[i].len 2); uart_frames[i].locked 0; uart_frames[i].len 0; } } }在无线发送函数里流程是先切IDLE再把帧写入FIFO然后发STX命令最后等待GIO中断或者查询状态寄存器确认发送完成。uint8_t hw3000_send_frame(uint8_t *data, uint8_t len) { uint32_t wait 0; hw3000_set_mode(HW3000_IDLE); delay_us(100); hw3000_cmd(HW3000_CMD_WRITE_FIFO); for (uint8_t i 0; i len; i) { hw3000_spi_rw(data[i]); } hw3000_cmd(HW3000_CMD_STX); while (wait 0xFFFF) { if (hw3000_read_reg(REG_IRQ_FLAG) IRQ_TX_DONE) { break; } wait; } if (wait 0xFFFF) { return 1; // 发送超时 } return 0; }4.3 无线到串口的接收分发HW3000收到数据后GIO引脚会拉低主控先用轮询方式检测到这个信号然后进入接收流程。void wireless_to_uart_task(void) { uint8_t fifo_cnt; uint8_t rx_buf[64]; uint8_t target_addr; // 查询FIFO内已有字节数 fifo_cnt hw3000_read_reg(REG_FIFO_COUNT); if (fifo_cnt 0) { return; } // 读取全部数据 hw3000_cmd(HW3000_CMD_READ_FIFO); for (uint8_t i 0; i fifo_cnt; i) { rx_buf[i] hw3000_spi_rw(0xFF); } // 解析帧长度、目标地址、数据 if (fifo_cnt 2) return; target_addr rx_buf[0]; uint8_t payload_len fifo_cnt - 1; // 根据目标地址查表分发 for (int i 0; i 8; i) { if (usart_table[i].addr target_addr) { uart_send_dma(usart_table[i].usart, rx_buf[1], payload_len); break; } } }这里需要注意一个状态问题读取完FIFO之后要清中断标志并且要把芯片重新切回RX模式否则芯片会停留在IDLE状态后续的数据包统统收不到。4.4 主循环与调度策略主循环不写完完整版了但结构上就是三个任务轮转uart_to_wireless_task()负责把串口数据发出去wireless_to_uart_task()负责把无线数据分发给串口heartbeat_task()负责心跳和远程配置。8个串口的发送优先级我做了简单加权USART1和USART8优先级最高用于调试和上位机中间6个口按轮询处理谁也不饿死。5. 实测问题与排查技巧实录5.1 高频串口丢帧一开始我用115200波特率模拟8路设备同时上报结果丢帧非常严重。排查下来发现串口中断服务里做了太多事情关中断时间太长导致DMA接收模式下空闲中断响应不及时DMA计数器被覆盖。解决办法是中断服务函数里只置标志位和读计数器所有解析、转发全部丢到主循环做。5.2 无线误码率偏高HW3000误码率高首先要排除供电不干净。这个芯片对电源纹波很敏感我用示波器量过输入电压纹波超过50mV时接收灵敏度直接下降好几个dB。最后在模块电源脚加了10uF钽电容并联104磁片电容搭配一个磁珠误码率肉眼可见地降下来了。另外天线阻抗匹配也很重要天线走线要短、要直两侧必须有完整的地随行否则天线就白接了。5.3 多串口饿死与优先级反转当USART1口一直有大量数据进来时后面的口很容易被饿死。因为我原先的调度逻辑是“挨个检查有数据就发”结果高优先级口一旦持续有数据低优先级口永远轮不到。后来改成“基于帧数量的加权轮询”每个串口累积帧数超过阈值就必须暂停高优先级发送强制切换到低优先级问题才解决。5.4 问题排查速查表现象可能原因处理方法某个串口收不到任何数据DMA配置错误或引脚复用冲突检查GPIO复用寄存器确认DMA请求映射HW3000写寄存器读回全0xFFSPI时序不对或片选逻辑错误降低SPI速率到1MHz重新检查时钟相位无线距离只有几十米频偏没配好或天线匹配差按数据手册重新计算频偏检查天线及地平面发送和接收切换失败模式切换时间不够IDLE后加2ms以上延时再切模式上传上位机数据乱序串口发送队列没有串行化给发送DMA加互斥锁发送完成后再发下一帧两台中继互相干扰频点或地址相同配置不同频点或不同同步字在整个项目调试过程中我最大的体会是8个串口同时收发并不难难的是把无线链路这个“窄管子”管好。AT32F403A的硬件余量大HW3000的可配置参数也多但真正决定稳定性的往往是电源设计、时序细节和调度策略这些东西。曾有个客户现场怎么调信号都差折腾一下午最后发现是他们把天线接头拧得不够紧虚接着。这类问题比代码问题隐蔽多了排查起来一定要稳扎稳打一步一步过滤。本文还有配套的精品资源点击获取
返回列表