
简介面向嵌入式开发者与物联网爱好者这是一个基于STM8S005K6微控制器与SX1276 LoRa射频芯片的UART通信工程版本V1.82可用于学习远距离无线数据传输方案的软硬件协同设计。包内共42个文件总体约110KB以C源码.c/.h、编译输出.o/.hex/.out为主同时包含IAR工程文件.ewp/.eww和调试配置文件能够直接反映完整工程的目录结构与编译产物。目前已有171人学习下载。通过阅读main.c、uart.c、sx1276-LoRa.c等关键代码可掌握STM8S005K6的UART波特率、数据位等参数配置以及SX1276频率、扩频因子、编码率等射频参数设置并理解两者之间串口通信与中断处理的配合方式。对于希望快速上手STM8与LoRa组合开发的初学者以及需要参考成熟工程结构的中级开发者该工程具有不错的实用价值。1. UART、SX1276、STM8S005K6 三种角色怎么装进同一个固件拿到这样一个压缩包文件名已经替我们划好了分工STM8S005K6 是主控SX1276 是射频前端UART 是留给外部主机的数据出口。这类固件常见于 LoRa 数传模块、工业采集终端和点对点遥控链路里。所谓“stm8加sx1276”就是让一颗 20 脚 8 位 MCU 通过 SPI 配置并驱动 SX1276同时用 UART 接收上位机下发的透传数据或 AT 指令。V1.82 是固件版本1611021040 则是 2016 年 11 月 2 日 10 点 40 分的编译时间戳。很多刚接触的人会纠结该先看射频还是先看串口我的建议是先打通 UART 这条对外链路再谈射频参数。2. STM8S005K6 的 UART 通道FIFO、帧格式、AT 指令的最小实现2.1 为什么选 STM8S005K6 这颗 20 脚芯片做 UART 网关STM8S005K6 是 ST 的 Value Line 8 位 MCU8KB Flash、640 字节 RAM、1.5KB EEPROM20 脚 TSSOP 封装。它有两个 UARTUART1 和 UART2、一个 SPI1、一个 I2C没有 USB 也没有以太网控制器。在 LoRa 模块这个场景里这些资源其实刚刚好一路 UART 接外部主机做数据透传SPI1 接 SX1276剩下的引脚再分给射频开关、状态 LED 和射频检测。选它的理由不是性能而是引脚和外设数量的匹配。STM32 当然也能做但 20 脚封装里的 STM32 同容量成本更高STM8L 系列虽然省电但这个方案的射频收发功耗本来就占大头低功耗 MCU 省下的那点电流在系统中不明显。USART、UART、I2C、SPI 这几种串行口的区别在这里也会直接影响选型SX1276 只认 SPI主机侧只认 UART而 STM8S005K6 恰好同时具备这两个控制器所以整个板子上不需要再加电平转换之外的任何协议芯片。2.2 UART 帧格式与接收 FIFO中断里只做一件事UART 侧最常见的做法是定长帧或带长度字段的变长帧。固件里通常用 7E 作为帧头后面跟命令字、数据长度、数据和 CRC8 校验帧格式大致如下字节位置内容说明00x7E帧头固定值1命令字0x01 查询版本0x10 设置频率0x20 发送数据等2数据长度从下一字节到 CRC 前的字节数3..N数据域透传数据或参数N1CRC8多项式 0x31初值 0x00CRC8 放在帧尾能让接收端边收边算收到最后一个字节时直接比对不需要把整帧缓存完再从头校验。接收侧我用一个环形缓冲区承接中断数据主循环再从 FIFO 里取字节做帧解析这样能避免在中断里做复杂的超时判断#define UART_RX_FIFO_SIZE 128 volatile uint8_t uart_rx_fifo[UART_RX_FIFO_SIZE]; volatile uint8_t rx_head 0; volatile uint8_t rx_tail 0; void UART1_RX_ISR(void) { uint8_t data; uint8_t next; data UART1-DR; /* 读数据寄存器同时清除 RXNE 标志 */ next (rx_head 1) % UART_RX_FIFO_SIZE; if (next ! rx_tail) { /* 环形缓冲留一格判满 */ uart_rx_fifo[rx_head] data; rx_head next; } } uint8_t uart_read_byte(uint8_t *out) { if (rx_head rx_tail) { return 0; /* 空 */ } *out uart_rx_fifo[rx_tail]; rx_tail (rx_tail 1) % UART_RX_FIFO_SIZE; return 1; }这段代码里 rx_head 是写入位置rx_tail 是读出位置两者相等表示 FIFO 为空next 等于 rx_tail 表示 FIFO 已满新数据直接丢弃。注意 UART1-DR 这个寄存器是读一次清一次 RXNE所以必须在中断里第一时间取走数据否则会丢字节。调试串口时的常见误区是波特率明明是 9600却把 FIFO 做得很大以为多缓存就能解决丢包。实际丢包多半是主循环里做了射频轮询或其他阻塞操作导致 FIFO 满。STM8S005K6 的 RAM 只有 640 字节UART FIFO 给 128 字节已经很奢侈剩下还要留给 SPI 缓冲和协议栈所以更合理的做法是主循环每 2 毫秒左右就要回来消费一次 FIFO。2.3 AT 指令处理怎么从 FIFO 走到参数设置很多模块固件把 UART 协议设计成 AT 指令风格主机发ATVER、ATFREQ433000000、ATTX010203这类文本指令。解析部分可以用 strncmp 逐条比对也可以用一个简单状态机。我倾向于按行处理把\r或\n当作指令结束标志同时再加一个超时保护避免主机发了一半就停住导致指令卡在缓冲区里#define CMD_MAX 32 uint8_t cmd_buf[CMD_MAX]; uint8_t cmd_len 0; uint16_t last_char_time; /* 由系统 tick 驱动 */ void poll_uart_cmd(void) { uint8_t b; while (uart_read_byte(b)) { last_char_time system_tick_ms; if (b \r || b \n) { if (cmd_len 0) { handle_at_command(cmd_buf, cmd_len); cmd_len 0; } } else if (cmd_len CMD_MAX) { cmd_buf[cmd_len] b; } } /* 半包超时超过 50ms 没收到新字节强制按当前内容解析 */ if (cmd_len 0 (system_tick_ms - last_char_time) 50) { handle_at_command(cmd_buf, cmd_len); cmd_len 0; } }超时时间 50 毫秒不是拍脑袋定的。9600 波特率下一个字节约 1.04 毫秒主机发送一条 30 字节的指令约 31 毫秒50 毫秒足够覆盖正常组包过程又不会让下一帧的头部和上一帧残余内容混在一起。handle_at_command 里根据前缀走不同分支ATVER回版本号ATFREQ转数值后调用频率配置函数ATTX把十六进制字符串转成字节数组后调用 SX1276 发送函数。2.4 UART 波特率误差要按主频算STM8S005K6 的 UART 波特率由时钟源和波特率寄存器共同决定。如果使用内部 16MHz HSI 时钟9600 波特率的分频值是 1667实际波特率 9598偏差约 0.02%但 HS 内部 RC 本身的精度大约正负 1%所以最终偏差要看芯片校准情况。如果外部接了 8MHz 晶振分频值是 833实际波特率 9603偏差 0.03%。两种方案都能满足 UART 的常规容差但要注意不能把 115200 硬套在 16MHz 上不管分频余量主频目标波特率分频值实际波特率误差16MHz HSI960016679598-0.02%16MHz HSI115200139115108-0.08%8MHz 外部晶振960083396030.03%实际项目中如果发现“发一条 AT 指令有概率回错”先不要怀疑天线先看串口工具的波特率是否和固件编译时的配置一致再看 MCU 的时钟源到底是内部 RC 还是外部晶振。像 FT231X、FT232R 这类 USB 转串口芯片驱动一般不会引入明显波特率误差反而是有些串口助手默认打开了“允许自动抑制波特率偏差”选项会把输入的 9600 自动修正到实际值掩盖了 MCU 侧的时钟偏差问题。3. SX1276 的 SPI 驱动与 LoRa 收发参数从寄存器到可跑代码3.1 STM8S005K6 的 SPI 主模式四线加复位的最小接线SX1276 的数据接口只有一组 SPI 从机接口SCK、MOSI、MISO、NSS 四根线外加 DIO0 中断输出和 RST 复位输入。STM8S005K6 的 SPI1 可以作为主模式对接时钟极性和相位都选模式 0即空闲时 SCK 为低、数据在上升沿采样。SPI 时钟频率不能直接打到 SX1276 的上限。STM8S005K6 主频 16MHzSPI1 的时钟源是主频的分频可以取 fMASTER/4 得到 4MHz这个值在 SX1276 的 SPI 时钟范围内同时给 PCB 走线和杜邦线留了余量。上电时序上MCU 复位后应先拉高 SX1276 的 RST 至少 10 毫秒再把引脚拉低然后等待至少 5 毫秒让 SX1276 内部的 32MHz 晶振起振。SPI 读写函数的实现要点是片选和字节时序协调void sx1276_spi_init(void) { /* 复用功能推挽输出时钟极性 CPOL0相位 CPHA0 */ SPI1-CR1 0x2E; /* 主模式波特率 fMASTER/4CPOL0 CPHA0 */ SPI1-CR2 0x03; /* 使能 NSS 软件管理 */ } uint8_t sx1276_write(uint8_t addr, uint8_t val) { uint8_t status; SX_NSS_LOW(); SPI1-DR (addr 0x7F) | 0x00; /* 写地址bit70 */ while (!(SPI1-SR 0x02)); /* 等待 TXE */ SPI1-DR val; while (!(SPI1-SR 0x01)); /* 等待 RXNE收到从机状态字节 */ status SPI1-DR; SX_NSS_HIGH(); return status; }写地址时 bit7 必须为 0读地址时 bit7 必须为 1这是 SX1276 SPI 协议和普通 SPI Flash 最大的不同。读寄存器时需要先发送带读标志的地址再发送一个哑字节来产生时钟从机在第二个字节期间把数据放在 MISO 上所以上面代码里读完成标志看的是 RXNE 而不是 TXE。3.2 LoRa 初始化寄存器先把 RegVersion 读到 0x12不管是新画的板子还是买来的模块上电后第一件事永远是回读 0x42 寄存器的版本号。SX1276 的 RegVersion 应该返回 0x12如果读到 0x00 或 0xFF说明 SPI 通路、电源或晶振有问题后面所有参数配置都白做。寄存器地址典型值说明RegOpMode0x010x80sleep 模式 LongRange 模式RegFrMsb/Mid/Lsb0x06/0x07/0x080x6C/0x40/0x00433MHz 中心频率RegPaConfig0x090x8FPA_BOOST 输出20dBmRegLna0x0C0x23LNA 增益最大默认RegModemConfig10x1D0x72125kHz 带宽CR4/5RegModemConfig20x1E0x74SF7CRC 校验开启RegPreambleLsb0x230x0C前导码 12 个 symbolRegPayloadLength0x24可变发送长度接收模式为 0RegPaDac0x4D0x87启用 20dBm 档位初始化函数的顺序有讲究修改频率和射频参数时必须先回到 sleep 模式否则部分寄存器写不进去void sx1276_init(void) { uint8_t ver; ver sx1276_read(0x42); if (ver ! 0x12) { error_code 0x01; /* SPI 通路异常 */ return; } sx1276_write(0x01, 0x80); /* sleep LoRa先离开 FSK 模式 */ sx1276_write(0x06, 0x6C); /* 433MHz 对应 RegFrMsb */ sx1276_write(0x07, 0x40); /* RegFrMid */ sx1276_write(0x08, 0x00); /* RegFrLsb */ sx1276_write(0x09, 0x8F); /* PA_BOOST 模式输出功率档位 */ sx1276_write(0x4D, 0x87); /* 打开 20dBm 档0x84 最大只到 17dBm */ sx1276_write(0x0C, 0x23); /* LNA 默认增益 */ sx1276_write(0x1D, 0x72); /* 125kHz 带宽编码率 4/5 */ sx1276_write(0x1E, 0x74); /* SF7开启 CRC */ sx1276_write(0x23, 0x0C); /* 前导码 12 symbol */ sx1276_write(0x01, 0x81); /* standby等待进一步指令 */ }433MHz 对应的 0x6C/0x40/0x00 不是随便给的计算方法是把频率换算成 19 位整数433000000 乘以 2 的 19 次方再除以 32MHz 晶振频率得到 0x6C4000。写这段代码时如果直接改频率不要只改高字节三个字节必须一起更新否则 SX1276 内部的分频器会先按半截值运行一段时间可能发出不在计划频点上的载波。3.3 发送与接收的切换DIO0 和 RegIrqFlags 的配合LoRa 模式下 SX1276 的发送完成标志是 RegIrqFlags 里的 TxDone 位地址 0x12位掩码 0x08接收完成是 RxDone 位掩码 0x40。DIO0 会在这两个事件发生时拉高所以可以把 DIO0 接到 STM8S005K6 的外部中断引脚也可以在主循环里轮询 RegIrqFlags代价是丢中断的概率更高。发送完成之后必须立刻回到接收状态否则模块会一直待在发送模式对端的接收窗口就错过了。我常用的发送函数会在退出时直接切回接收持续模式void sx1276_send_payload(uint8_t *buf, uint8_t len) { uint8_t i; sx1276_write(0x01, 0x80); /* 回 sleep改 FIFO 指针 */ sx1276_write(0x0D, 0x00); /* RegFifoAddrPtr 0 */ sx1276_write(0x24, len); /* RegPayloadLength len */ for (i 0; i len; i) { sx1276_write(0x00, buf[i]); /* RegFifo 逐字节写入 */ } sx1276_write(0x01, 0x83); /* TX 模式LoRa 保留位不变 */ /* 等待 TxDone读 RegIrqFlags 会同时清除标志 */ for (i 0; i 255; i) { if (sx1276_read(0x12) 0x08) { break; } } sx1276_write(0x01, 0x85); /* 回到 RXCONT 持续接收模式 */ }接收侧的轮询主循环对应读 0x12 的 RxDone 位读后立即从 FIFO 取数。这里有个容易忽略的细节RegIrqFlags 是读即清如果先读了 0x12 判断 TxDone紧接着又去读 Rssi 或 FIFO中间不会有竞态问题但如果你在中断里读了 0x12主循环再读就什么也得不到所以 Clear/Read 逻辑只能放在一处。收发切换间隙大约会有几十毫秒的盲区这个时间不能算在链路预算里。对端如果使用触发式发送建议在应用层加一个 ACK 重传机制不能只靠 SX1276 的 CRC 来判断对端是否真的收到了。4. V1.82 与 1611021040固件版本、编译时间和升级链路的工程化4.1 版本号和时间戳写在哪、谁能读固件包里 V1.82 和 1611021040 两个字段前者是给人看的版本后者是给机器做比对的时间戳。成熟一点的固件都会在编译时把这两项自动写入 Flash 固定地址而不是只放在代码注释里。这样产线测试、售后返修、现场升级都能通过 UART 指令直接读到目标板上跑的到底是哪一版。比较简单的做法是在代码里固定一个字符串数组再用编译器提供的日期宏自动生成时间戳#define FW_VERSION_STRING V1.82 const char fw_version[] FW_VERSION_STRING; const char fw_build_time[] __DATE__ __TIME__;这段代码在 STVD 和 IAR 下都能编译DATE和TIME由编译器在每次编译时自动替换。注意产线打包时不能依赖这个宏来追溯版本因为编译时间和烧录时间可能差好几天正确做法是像压缩包文件名那样用打包脚本把 git tag 或 SVN revision 和编译时间写进一个单独的 version.c烧录后再回读校验。AT 指令里对应增加一条ATVER回传固件版本、编译时间和射频参数表版本。如果射频参数表跟着固件版本一起改动那么版本号后面还可以追加一个两位的子版本例如 V1.82-RF02表示射频参数表第 2 版避免出现“固件新射频旧”的混搭状态。4.2 UART 升级时怎么保证版本不回退STM8S005K6 出厂 ROM 里带了一段 UART BootLoader通过 BOOT0 引脚在复位时进入主机侧可以用串口把固件写入 Flash。这个流程里最容易出问题的不是写入而是升级中断后的版本回退。升级命令典型功能备注0x21读版本区返回固件版本和时间戳0x22块写入按 64 字节一页写入 Flash0x23CRC 校验对整个固件区做校验0x24跳转应用区校验通过后复位运行如果 BootLoader 在块写入过程中发现 CRC 校验失败应该保持 BootLoader 状态并允许主机重新写入而不是直接跳转旧固件。反过来如果旧固件比新固件版本号更高产线脚本应该直接报错终止避免返修机上烧回一个旧版本导致现场已经部署的节点和新节点参数不一致。升级时主机侧如果是 Windows 系统使用 FT231X 或 FT232R 这类 USB 转串口芯片时注意在设备管理器的端口属性里把接收 FIFO 和发送 FIFO 的缓冲调到最小否则 9600 波特率下大块数据会被驱动攒起来BootLoader 侧的协议超时已经触发数据还没发完。4.3 产线里的版本与射频参数一致性防呆多节点 LoRa 网络对固件版本的要求比单模块高。433MHz 和 868MHz 硬件板型可能共用一个固件包如果版本管理只依靠文件名里的 V1.82 而射频参数表没有跟着编码就会出现烧录软件里选了 868 的板型却烧成 433 的固件整批节点频率不一致的批量事故。板型固件版本频率参数表版本烧录检查点433M 节点V1.82433MHzRF02上电回读 RegFr0x6C4000868M 节点V1.82868MHzRF02上电回读 RegFr0xD89000915M 节点V1.81915MHzRF01禁止升级到 V1.82产线工装可以加一步回读验证烧录完固件后通过 UART 发ATFREQ?上位机比对返回值是否和订单型号匹配。如果用的是轩微方案的 stm8 编程器注意它针对不同批次芯片的 SWIM 时序兼容性大批量烧录前先小批量抽测版本回读结果。固件一旦擦除再想从旧板子上把参数读回来就要靠备份文件所以擦除前最好先执行一次 0x21 读版本把原版本和时间戳存档既能追溯也能防呆。5. 上板调试从 UART 乱码到 SX1276 不出波的排查顺序5.1 UART 乱码先查这三处收到“串口乱码”的反馈第一反应不是改代码而是按顺序排除硬件链路。先用逻辑分析仪或示波器量 MCU 的 TX 引脚看波特率对应的位宽是否准确接着确认两个设备是不是共地TX 接 RXRX 接 TX别同向对接最后确认电平标准SX1276 模块一般是 3.3V TTL如果接到的是 RS232 电平转换板电平不匹配会表现为偶尔通、偶尔乱码。如果波形正确、接线正确、电平也对再用 FT231X 这类 USB 转串口工具抓一次完整数据看是不是只有开机后的第一帧乱码。MCU 复位期间 GPIO 状态不确定可能让 UART 发出半个字节这种情况给串口工具加一个 100 到 200 毫秒的延时再上电或者让上位机在收到连续两个帧头 0x7E 后才认为链路同步都可以解决。5.2 射频不出波先读两个寄存器SX1276 最常见的不出波原因不是天线而是配置根本没生效。先读 0x42 寄存器必须读到 0x12。如果读到 0xFF通常是 NSS 电平没有正确拉低如果读到 0x00多数是电源或者晶振问题。版本寄存器通过后再读 0x09 RegPaConfig 和 0x1D RegModemConfig1确认配置值不是被某段代码在初始化后又意外改写。LoRa 接收模式下的另一个隐藏问题是 CAD 检测或信道活动检测被误开启导致模块处于“看起来在接收实际在持续检测”的状态。检查方式是把 RegOpMode 读回来确认低三位是 0x05也就是 RXCONT而不是 0x01 或 0x03。5.3 两台设备的交叉验证顺序两台模块联调时不要直接测距离先在桌上做交叉验证模块 A 发固定长度的 0x55 数据模块 B 接收并打印 RSSI然后调转方向B 发 A 收确认双向都通后再让 A 每隔一秒发一包B 连续接收 10 分钟统计丢包率。每一步都要保留串口日志而不是靠人眼看 LED。Linux 主机侧如果使用普通 UART打开 /dev/ttyUSB0 时要注意驱动默认对 RTS/CTS 的处理模块的流量控制引脚没接但软件里却开了 CRTSCTS就会出现发一包等半天的假死现象。建议直接用stty -F /dev/ttyUSB0 9600 -crtscts关闭硬流控再用cat /dev/ttyUSB0观察接收数据。最后一板一眼地按下表顺序检查搞不定的大多数是硬件问题而不是固件问题。本文还有配套的精品资源点击获取