简介:这份开源项目聚焦TTGO T-Beam开发板上的LoRa与APRS应用,面向嵌入式开发者、物联网爱好者和业余无线电玩家,解决远距离无线数据传输与实时位置追踪需求。项目基于STM32/ESP32平台,将LoRa扩频通信与APRS自动位置报告系统结合,通过Arduino或PlatformIO环境编译运行,适合希望掌握LPWAN通信和定位上报技术的入门及进阶学习者。压缩包共12个文件,体积仅107KB,包含Arduino主程序(.ino)、配置头文件(.h)、C++源码(.cpp)、PlatformIO配置(.ini)、说明文档(readme/md)以及效果图片等,结构清晰,便于对照代码理解模块关系。已有260人学习下载。通过这份代码,可学习LoRa射频参数的配置方式、APRS信标报文的封装与解析、T-Beam外设驱动,以及跨平台开发环境的搭建思路,为自制追踪器或应急通信设备提供可复用的工程模板。
1. 用 STM32 做 TTGO T-Beam 的 LoRa APRS 网关,先想清楚协议栈选型
第一次拿到 TTGO T-Beam,很多人下意识把 SX1276 当作 ESP32 的外设直接调。换个思路,把射频前端挂在 STM32 上,APRS 网关才能省去 WiFi 蓝牙的调度干扰,时序也更可控。TTGO-T-Beam-LoRa-APRS-master_LORASTM32_ 这个名字揭示的正是这种组合:T-Beam 的 LoRa 射频前端 + STM32 主控。常见的移植理由不是性能,而是希望用标准库或寄存器直接掌控收发中断和功耗。LoRa 只是物理层,APRS 真正要处理的 AX.25 地址解析、信标发送与中继策略,必须由 STM32 端完成。适合手里同时有 T-Beam、想脱离 ESP32 做低功耗或自研板卡的嵌入式工程师。下面直接进入实现路径。
2. 从 T-Beam 接 STM32 的 SPI 线开始:SX1276 寄存器配置与 LoRa 接收
2.1 引脚映射:SX1276 不接 ESP32 的 SPI 口,改由 STM32 接管
TTGO T-Beam 板载的 LoRa 芯片通常是 SX1276 或 SX1278,接口就是标准的 SPI,控制引脚只有 NSS、RST、DIO0。很多人以为 DIO1-DIO3 都要接,实际在 APRS 这种单收单发场景里,DIO0 一个中断就够。DIO0 用于接收完成和发送完成两个事件,减少了 STM32 的引脚占用。常见接法如下表,我这里以 STM32F103 的 SPI1 为例。
| STM32 引脚 | SX1276 引脚 | 说明 |
|---|---|---|
| PB13 | SCK | SPI 时钟,初始 1 MHz,不要直接上 18 MHz |
| PB14 | MISO | 主控读寄存器 |
| PB15 | MOSI | 主控写寄存器 |
| PB12 | NSS | 片选,低电平有效,不要接地 |
| PA8 | RST | 复位,低电平复位 |
| PA9 | DIO0 | 接收完成/发送完成中断 |
T-Beam 自带的 ESP32 已经占用了这些映射方向,换成 STM32 后,接线不用改原理图,只要飞线到 STM32 开发板的 SPI 引脚。需要注意 T-Beam 的 LoRa 模块是 3.3V 电平,如果 STM32 板上有 5V 引脚,千万别把 NSS 或 MOSI 接到 5V 上,否则先烧 SX1276。
提示:DIO0 在 LoRa 模式下默认映射到 RxDone/TxDone,后续代码里不用再改 RegDioMapping1,除非你要让 DIO1 承担超时中断。
2.2 配置 SX1276 的 LoRa 模式:频率、SF、同步字按参数表写
SX1276 的底层通信极简单:一个 SPI 读函数、一个 SPI 写函数,剩下的全是寄存器操作。很多现成库的初始化很长,但 APRS 网关需要的核心寄存器并不多。先写最底层的两个函数。
static void lora_write_reg(uint8_t reg, uint8_t val) { NSS_LOW(); spi_read_write(reg | 0x80); // 寄存器最高位置 1,表示写操作 spi_read_write(val); NSS_HIGH(); } static uint8_t lora_read_reg(uint8_t reg) { uint8_t v; NSS_LOW(); spi_read_write(reg & 0x7F); // 最高位置 0,表示读操作 v = spi_read_write(0x00); NSS_HIGH(); return v; }写操作把指令字节最高位置位,读操作清零,这是 SX1276 固定规则,和普通的 SPI Flash 类似。初始化 LoRa 模式时,我一般按下面这个顺序写,先把芯片放进待机状态,再设调制参数,最后才进接收。注意,LoRa 模式和 FSK 模式在 RegOpMode 地址 0x01 的 bit7 区分,这个位必须在睡眠模式下才能修改。
lora_write_reg(0x01, 0x80); // 进入睡眠 + LoRa 模式 while ((lora_read_reg(0x01) & 0x08) == 0); // 等待 XTAL 稳定 lora_set_frequency(433900000UL); // 设为 433.9 MHz // RegModemConfig1 = 0x72: BW 125 kHz, SF7 lora_write_reg(0x1D, 0x72); // RegModemConfig2 = 0xC4: CR 4/5, 显式头,SF7 lora_write_reg(0x1E, 0xC4); // RegModemConfig3 = 0x04: 开启低数据速率优化 lora_write_reg(0x26, 0x04); // 同步字设为 0x12,LoRaWAN 是 0x34,这里必须区分 lora_write_reg(0x39, 0x12);这里面的关键参数是带宽、扩频因子、编码率和同步字。APRS over LoRa 没有统一标准,但绝大多数实现都采用 125 kHz、SF7、CR 4/5,兼顾空中时间和灵敏度。同步字 0x12 是民间 LoRa 网络的通行值,SX1276 在收到不同同步字的帧时不会产生错误中断,只会静默忽略,所以两端必须一致。
2.3 接收中断里读 FIFO,先清中断再取数据
进入连续接收模式后,LoRa 芯片会把解调出的 payload 写入内部 FIFO。收到完整帧时,DIO0 拉高,同时 RegIrqFlags 里的 RxDone 位置 1。STM32 的 EXTI 中断里不能做太久的事,一般只做两件事:读 FIFO、清中断。下面这段是我常用的处理逻辑。
void lora_handle_dio0_irq(void) { uint8_t irq = lora_read_reg(0x12); // RegIrqFlags if (irq & 0x40) { // RxDone 位 uint8_t len = lora_read_reg(0x13); // RegRxNbBytes uint8_t curr_addr = lora_read_reg(0x10); // RegFifoRxCurrentAddr lora_write_reg(0x0D, curr_addr); // FIFO 指针移到当前载荷 for (uint8_t i = 0; i < len; i++) { rx_buf[i] = lora_read_reg(0x00); // RegFifo } process_aprs_packet(rx_buf, len); lora_write_reg(0x12, 0x3F); // 清所有中断标志 } }RegIrqFlags 是只读的,必须通过写 RegIrqFlags 来清除,写 0x3F 会把 RxDone、PayloadCrcError、ValidHeader 几个常用位一起清掉。注意,如果读取 RegRxNbBytes 后不移动 FIFO 指针,FIFO 地址仍停在这次帧的起始位置,下一帧进来就会覆盖错误数据。这一步是新手最容易漏的。
3. 从 LoRa 原始包到 APRS 协议:AX.25 解帧、地址解析与 CRC 校验
3.1 分清是裸 AX.25 还是 KISS 封装再写状态机
T-Beam 上的 LoRa APRS 实现,数据链路层大多直接复用 AX.25 帧,但空中传输有两种封装。一种是直接把 AX.25 数据放进 LoRa FIFO,帧头帧尾都是 0x7E;另一种是 KISS 封装,用 0xC0 作为边界,中间包含 KISS 命令头、端口号,之后才是 AX.25 帧。STM32 端如果只做接收解析,最好两种都认,避免换固件后全部收不到。
| 封装方式 | 帧边界 | STM32 解析动作 |
|---|---|---|
| AX.25 over LoRa | 0x7E | 直接做 CRC 校验 |
| KISS over LoRa | 0xC0 | 去掉 KISS 头/尾,再做 CRC |
| Mic-E 压缩位置 | 0x7E | 需要解 Michelle 压缩算法 |
如果你的目标是参与 APRS 地面中继,建议直接使用 KISS over LoRa,因为 digipeater 转发时不需要重算压缩位置,只要原样转发。解析状态机的核心是把两个边界符之间的字节缓冲下来,而不是每收到一个字节就去切分。
3.2 AX.25 地址字段每一位都左移一位:解析源地址、目标地址和 SSID
AX.25 地址字段的规则对刚接触 APRS 的人总是很别扭:调用名每个字符的 ASCII 码左移一位,SSID 数值也左移一位放到地址字节的低位。低位还留着标志位,用于标识地址序列是否到最后一个。发送端这样移位,接收端就要右移还原。下面这段代码处理的是接收方向。
uint8_t extract_address(const uint8_t *frame, uint8_t *call, uint8_t *ssid) { uint8_t i, addr_len = 7; for (i = 0; i < 6; i++) { call[i] = frame[i] >> 1; // 还原被左移的字符 } call[6] = 0; *ssid = (frame[6] >> 1) & 0x0F; // SSID 在低位的前 4 位 return (frame[6] & 0x01) ? 1 : 0; // 地址结束标志 C 位 }ACK 的 ID 放在哪里?对于 APRS UI 帧,真正的 call 解析就这样。目的地址、源地址、digipeater 地址都用同一套移位规则。SSID 低 4 位是 0-15,30、40 这类 APRS 约定其实是路径符号,不是 SSID。STM32 上不要直接拿字符串搜索调用名,要先把地址字段按 7 字节一个单位拆开。
3.3 用 CRC-16-CCITT 挡住错误帧,别让假位置报上屏
LoRa 自身有 CRC 校验,但这层校验只保证 LoRa payload 没被物理层弄脏,不保证 payload 里的 AX.25 帧合法。尤其做 digipeater 时,收到帧的 CRC 是正确的,但转发前必须重新计算 FCS,否则下游节点会直接丢弃。这里用反射多项式 0x8408,初始值是 0xFFFF。
uint16_t ax25_crc(const uint8_t *buf, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= buf[i]; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 1) { crc = (crc >> 1) ^ 0x8408; } else { crc >>= 1; } } } return crc; }发送端把要校验的地址、控制、PID 和信息字段传进去,得到的 CRC 要先放低字节、再放高字节。接收端把整段数据(不含起始 flag 和结束 flag)一起送入 CRC 函数,算出来的值如果和帧尾 FCS 相等就是合法帧。注意,如果使用 KISS 封装,CRC 计算的起点应该跳过 KISS 头,从真正的 AX.25 目的地址开始。
一个完整的接收状态机至少要有空闲、收帧、跳过三种状态。遇到 0x7E 且当前不在收帧,就进入收帧;如果已经收满一个包,遇到 0x7E 就封包,然后交给 CRC 校验。LoRa 单包最大长度一般不超过 255 字节,所以状态机的缓冲区定到 256 足够。超过缓冲区长度要强制回到空闲态,不然下一帧会错位。
4. STM32 当作 APRS 信标和中继:定位、定时发送与 OLED 联动
4.1 构造 AX.25 UI 帧,写入 FIFO 后进入发送模式
发送一个 APRS 信标,需要先把目的地、源地址改成 AX.25 地址字段,再补上控制字段 0x03 和协议标识 0xF0,后面接 APRS 信息内容。这段构造代码可以单独拆成一个函数,方便在 GPS 数据更新时调用。注意要留出结束 flag 的位置,否则接收端状态机不会认为帧结束了。
static void build_aprs_frame(uint8_t *frame, const char *dst, const char *src, uint8_t src_ssid, const char *info) { uint16_t ptr = 0, crc; frame[ptr++] = 0x7E; // 起始 flag // 目的地址,最后一位 C 位为 0,表示后面还有源地址 put_call_address(dst, 0, frame, &ptr); // 源地址,最后一位 C 位为 1,表示这是最后一个地址 put_call_address(src, src_ssid, frame, &ptr); frame[ptr++] = 0x03; // UI 控制字段 frame[ptr++] = 0xF0; // No L3 协议标识 strcpy((char *)&frame[ptr], info); ptr += strlen(info); crc = ax25_crc(&frame[1], ptr - 1); frame[ptr++] = crc & 0xFF; frame[ptr++] = crc >> 8; frame[ptr++] = 0x7E; // 结束 flag }put_call_address 的实现就是把每个字符左移一位,最后一位字节放 SSID 和结束标志。送入 LoRa 前,要把 STM32 的 FIFO 指针归零,PayloadLength 设为整个帧长,再切到发送模式。发送完成的标志同样走 DIO0,不过是 TxDone 位。发送完成后一定要切回接收模式,否则就会一直处于 semaphore 发送状态,收不到任何中继来的包。
4.2 信标发送条件:RTC 定时加 GPS 位移双重判定
APRS 信标不是越频繁越好。公共频段 433.9 MHz 上的 LoRa 设备很多,如果每 10 秒发一次,会把自己堵死。我常用的发送策略是先看 GPS 是否固定,再看位置变化和定时器。状态机不需要复杂,一个 5 分钟定时器加上位移超 100 米判定就足够了。
| 触发条件 | 动作 | 保护限制 |
|---|---|---|
| GPS 未固定 | 只接收,不发定位信标 | 避免上报 0.0000 位置 |
| 位移超过 100 米 | 立即插入发送队列 | 两次发送间隔至少 10 秒 |
| 时间达到 5 分钟 | 即使静止也发 | 保持在线存在感 |
| 收到 digipeater 应转发的帧 | 按路径重写后转发 | 同一帧最多转发一次 |
这个策略在 T-Beam 上用 STM32 实现时,定时用的不是 HAL_Delay,而是 RTC 秒中断。发送队列用环形缓冲区,主循环每次读到 GPS 修复就更新当前位置,再检查是否需要切到发送状态。RTC 的 1Hz 中断里只置标志位,发送逻辑放在主循环,避免中断处理时间过长。
4.3 接收到的 APRS 包如何显示和记录
T-Beam 上的 OLED 是 SSD1306,用 I2C 走两线。收到合法包后,我习惯先把源调用名解析出来,再判断信息字段前两个字符是位置符号还是遥测标识。显示代码不要放在 LoRa 中断里,否则一秒钟收几十个包时会把 SPI 和 I2C 争抢问题放大。
void display_aprs_node(const char *call, const char *lat, const char *lng) { char line[32]; ssd1306_clear(); ssd1306_gotoxy(0, 0); snprintf(line, sizeof(line), "%s", call); ssd1306_puts(line); ssd1306_gotoxy(0, 2); snprintf(line, sizeof(line), "%s %s", lat, lng); ssd1306_puts(line); ssd1306_refresh(); }串口日志的作用比 OLED 更重要。调试时用 115200 波特率输出每一帧的源地址、RSSI、SNR 和 CRC 结果,能直接看出是 LoRa 没解出,还是解出了但 AX.25 校验失败。对于定位问题,OLED 只留最终结果,原始帧必须能通过串口回放。
5. 调试 LoRa APRS 的三个真坑:晶体频偏、SF 与带宽配合、静默丢包
5.1 晶体频偏会瞬间毁掉整条链路
SX1276 的频率由 RegFrMsb、RegFrMid、RegFrLsb 三个寄存器计算,公式里的参考时钟是 32 MHz 晶振。如果 T-Beam 板上的晶振实际是 30 MHz 或者其他值,写到寄存器里的频率就是错的。调整频率时不要只改代码,先读回这三个寄存器,再反算当前频点。有频谱仪或 SDR 最好,直接看载波峰是不是在配置值处。
检查方法:把 STM32 进入设置为发送模式,发一段连续 0xAA 的测试包,用 SDR 看频谱。正常情况中心频点应落在配置频率正负 1 kHz 内,如果偏移超过 10 kHz,检查晶振和时钟配置。不要靠接收端的 RSSI 判断频率是否准确,因为 LoRa 差的频率不一定表现在 RSSI 上,而是表现在丢包率上。
5.2 SF 与带宽不能单独看,它们是调制链路上的搭档
APRS 信息很短,很多人为了省电会用 SF7 125 kHz,但如果对端是 SF7 250 kHz,两边虽然都是 SF7,却无法通过前导码检测。调试时最容易犯的错就是只改一边。下表给出几组默认匹配,尽量不混用。
| 扩频因子 | 带宽 | 空中时间(20 字节 payload) | 适用场景 |
|---|---|---|---|
| SF7 | 125 kHz | 约 50 ms | 近距离中继,转发频繁 |
| SF9 | 125 kHz | 约 140 ms | 远距离信标 |
| SF12 | 125 kHz | 约 400 ms | 低速率,远程基站 |
APRS 是广播协议,接收端并不知道发送端用了什么参数,只能靠同步字和前导码强力解码。所以参数要写在配置中心,不要每台设备单独调。T-Beam 之间测试时,先发一份配置表,两边逐一核对。
5.3 静默丢包不是 LoRa 坏了,是中断和 FIFO 指针的问题
最棘手的情况是 RSSI 显示信号很强,但 STM32 从未进入接收中断。常见原因有三个:RegIrqFlags 没有清除,导致 DIO0 中断只触发一次;SPI 速度过高导致读出错误字节;读取 RegRxNbBytes 后没有把 FIFO 指针移到实际接收地址。
调试技巧是先做一个回环测试:把 STM32 发射端和接收端之间用一根杜邦线连接 SX1276 的 DIO0,再把接收端的 FIFO 内容通过串口导出。如果能收到完整帧,问题在射频天线或频率配置;如果收不到,就逐步读回寄存器,看 RegOpMode 是否回到了接收模式。每一步移动 FIFO 指针都要打印当前地址,确认与 RegFifoRxCurrentAddr 一致,这个变量是定位静默丢包的关键线索。
这几类问题都体现在 RSSI 正常、CRC 却不通过,把寄存器 dump 出来看一遍,往往比穷举参数快得多。
本文还有配套的精品资源,点击获取