ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TTGO T-Beam LoRa APRS固件解析与STM32移植指南

TTGO T-Beam LoRa APRS固件解析与STM32移植指南 简介一款面向TTGO T-Beam硬件、基于LoRa与APRS协议的嵌入式开源项目适合物联网开发者、业余无线电爱好者及STM32嵌入式学习者用来构建远距离位置报告与无线数据传输终端。压缩包共12个文件大小约107KB包含Arduino主程序(.ino)、C/C头文件与源文件(.h/.cpp)、PlatformIO配置(.ini)、README及Markdown说明文档以及示例图片等。其中 .ino 为主控制程序.h/.cpp 实现 LoRa 驱动与 APRS 数据封装.ini 配置编译环境readme 和 .md 提供安装及调试说明图片则可直观展示硬件节点连线和运行效果。目前已有260人学习下载适合需要参考完整代码实现的人员快速上手。通过阅读源码和文档可掌握LoRa模块驱动、APRS数据帧封装、GPS定位信息上报等关键要点还能了解在TTGO T-Beam上搭建位置追踪节点的流程并借助配置适应不同编译环境对自组无线通信终端开发颇具参考价值。1. 为什么我把这套 TTGO T-Beam LoRa APRS 固件拆了三遍户外车队进山后没有基站信号最便宜也最省电的定位上报方案不是 4G 物联网卡而是一块集成了 ESP32、LoRa 收发器和 GPS 的 TTGO T-Beam再跑一套 APRS 协议栈。这套名为 TTGO-T-Beam-LoRa-APRS-master 的固件包恰好把“读 GPS 坐标、组装 APRS 报文、通过 LoRa 发射/接收”三件事串成了完整闭环。压缩包里的 src 目录、lib/BG_RF95 和 platformio.ini 并不是复杂工程但目录结构已经把“射频驱动”和“业务逻辑”分层值得做 LoRa 通信的人逐行读。标题里的 LORASTM32 容易让人误以为主控是 STM32实际上原始工程跑在 ESP32 上但 APRS 报文的组织方式和 RFM95 的驱动逻辑并不绑定单片机型号这也是我后面单独讲 STM32 移植的原因。2. APRS over LoRa 的链路设计与源码布局2.1 项目文件结构与启动流程把压缩包解压后第一件事不是编译而是先看目录。这个工程用 PlatformIO 管理而不是传统 Arduino IDE 的单一 .ino 工程。根目录下的 platformio.ini 声明了板型、框架和串口监视器参数src 里放的是入口代码lib/BG_RF95 则是射频驱动的核心。下面这张表是文件与职责对应关系初学者照着这个顺序读源码不会迷路。路径作用src/TTGO_T-Beam_LoRa_APRS.ino入口文件负责 setup 初始化和 loop 循环调度src/TTGO_T-Beam_LoRa_APRS_config.h所有需要人工改的参数频率、呼号、发送间隔、波特率lib/BG_RF95RFM95 LoRa 芯片的驱动库封装 SPI 读写和 LoRa 调制寄存器platformio.ini工程编译配置声明 board、framework、上传速度include/README作者写的接线说明和编译注意事项INSTALL.md面向新手的安装步骤启动流程在 .ino 的 setup() 里非常清晰。Serial 先初始化用于调试LoRa 驱动通过 BG_RF95 接管 RFM95 的 SPI 引脚GPS 串口则按 config.h 里定义的波特率启动。整个启动过程里最需要注意的是 LoRa 模块的复位时序——如果 RFM95 的 RST 引脚没有按规定拉低再拉高后续所有寄存器读写都会失败现象是发送时返回错误码但 Serial 上没有任何异常。2.2 LoRa 射频参数与 APRS 帧格式传统 APRS 跑在 VHF 1200bps AFSK 上但 LoRa 是 CSS 扩频调制两者物理层不兼容。这套固件的做法是保留 APRS 的信息层格式把位置报告、状态消息这些内容直接塞进 LoRa 载荷再用自己的网关或接收端解析。带来的好处是灵敏度明显高于 1200bps AFSK在城市和山区都有可用性代价是普通 APRS 中继、igate 不能直接识别必须配合 LoRa 接收端。APRS 位置报文的标准信息字段长这样!3723.45N/12202.30W-其中!是当前位置报文标识3723.45N是北纬 37 度 23.45 分/是分隔符12202.30W是西经 122 度 02.30 分末尾的-表示符号表。这套固件在组包时通常会把报文组装成“源呼号目的路径:类型字段位置”的格式。LoRa 物理参数方面默认常用 433.775MHz带宽 125kHz扩频因子 SF7编码率 CR4/5开启 CRC。SF7 在速度和传输距离之间最均衡信标间隔短的移动追踪场景下特别合适如果做远距离无人区中继可以把 SF 提到 10但相同载荷的空中时间会增加。// 常见 BG_RF95 库初始化流程等价于工程 setup() 中的片段 void setupLoRa() { Serial.begin(115200); LoRaSPI.begin(); // 初始化 SPI 总线频率通常在 10MHz 以下 rf95.init(); // 复位并读取 RFM95 版本寄存器 rf95.setFrequency(FREQUENCY); // config.h 中定义例如 433.775F rf95.setTxPower(20, true); // 20dBm 约 100mW第二个参数必须传 true rf95.setSignalBandwidth(125e3); // 125kHz 带宽 rf95.setSpreadingFactor(7); // SF7适合移动设备短包 rf95.setCodingRate4(5); // CR4/5 rf95.setCRC(true); }这段代码里的每个参数都直接影响通信距离和兼容性。频率必须和接收端一致带宽和扩频因子必须保持一致否则射频调制解调器无法同步。TxPower 第 2 个参数是 isUsingPaBoost很多板子漏写这参数会导致输出功率异常偏低表现为近距离能收到、稍微拉开距离就丢包。2.3 main loop 的职责划分loop() 里的调度逻辑决定了整套固件能不能长期稳定运行。常见做法是每轮只做三件事从 GPS 串口读字节喂给 TinyGPS、用 millis() 判断是否到达发送间隔、检查 LoRa 接收缓存里有没有数据。void loop() { while (gpsSerial.available()) { gpsFeed(gpsSerial.read()); // 逐字节喂给 GPS 解析库 } if (gpsLocationValid millis() - lastSend BEACON_INTERVAL) { buildAndSendPacket(); // 组装 APRS 位置报文并发送 lastSend millis(); } uint8_t packet[LO_BUF_SIZE]; uint8_t len sizeof(packet); if (rf95.available()) { len rf95.receive(packet, len); // 非阻塞接收 if (len 0) { handleRxPacket(packet, len); } } powerSaveDelay(); // 极简休眠delay(10) 或进 modem idle }这个循环里最容易犯的错误是在 GPS 解析循环里加长时间 delay导致 LoRa 接收中断处理不过来。BG_RF95 这类库一般使用轮询方式读取 DIO0 引脚如果主循环被 delay(500) 阻塞数据包寄存器会被新数据覆盖。所以我一般会把 beacon 间隔和接收轮询拆开发送用BEACON_INTERVAL控制接收每轮循环都检查不引入阻塞。3. PlatformIO 构建与 config.h 参数调优3.1 platformio.ini 里的板型和依赖PlatformIO 的好处是依赖、框架、上传全在一个 ini 文件里管理。解压后的工程根目录有 platformio.ini里面至少会声明一块板子。T-Beam 早期版本是 ESP32最新有 S3 批次PlatformIO 平台espressif32对直接集成了对应 board 支持。配置如下[env:ttgo-t-beam] platform espressif32 board ttgo-t-beam framework arduino upload_speed 921600 monitor_speed 115200 monitor_filters esp32_exception_decoder lib_deps TinyGPSPlusboard ttgo-t-beam会把 T-Beam 默认的 flash 大小、PSRAM 开没开、引脚定义都带出来。如果你的 PlatformIO 版本识别不了这个板名可以换成board ttgo-t-beam或board esp32dev但后续必须手工在代码前段#defineLoRa 和 GPS 引脚。lib_deps里的 TinyGPSPlus 是 GPS 解码库网络正常情况下会自动下载BG_RF95 已经放在工程目录 lib 下面不需要额外依赖。这样做的好处是离线也能编过因为整个射频驱动都随压缩包独立存在。3.2 config.h 里的频率、呼号与信标间隔工程把用户可调参数全部集中到了TTGO_T-Beam_LoRa_APRS_config.h。把配置参数和业务代码分离是最值得借鉴的一点。打开文件后会看到类似以下内容#pragma once // 射频基础参数 #define FREQUENCY 433.775F // LoRa 载波频率单位 MHz #define TX_POWER 20 // 发射功率单位 dBm #define SIGNAL_BANDWIDTH 125E3 // 信号带宽单位 Hz #define SPREADING_FACTOR 7 // 扩频因子 SF7 #define CODING_RATE 5 // 4/5 编码率 // APRS 业务参数 #define MY_CALL BG2XYZ // 源呼号用于标识设备 #define APRS_SYMBOL // 主符号表 表示车辆 #define APRS_SYMBOL_T - // 辅助符号- 为默认符号表 #define BEACON_INTERVAL_MS 30000UL // 定位信标发送间隔毫秒下面这张表是每个参数选错的典型后果改参数前对照检查参数推荐值调整说明FREQUENCY433.775MHz 或 470MHz必须符合当地业余无线电频段规定两端不一致直接丢包TX_POWER17-20dBm过高会发热T-Beam 稳压器可能掉电压SPREADING_FACTOR7-9SF12 距离远但单包耗时几百毫秒不适合密集信标MY_CALL自己的呼号LoRa APRS 网关按呼号区分节点不能重复BEACON_INTERVAL_MS30000移动追踪建议 10-30 秒固定节点可以拉到 300 秒3.3 编译、烧录与串口监视修改完 config.h 后在工程根目录运行 PlatformIO 命令。三个命令最常用pio run -e ttgo-t-beam pio run -e ttgo-t-beam -t upload pio device monitor -e ttgo-t-beam -b 115200第一条命令只编译不烧录适合快速检查语法错误。第二条把固件通过 USB 串口写入 ESP32T-Beam 板载 USB-UART 不需要额外下载器。第三条打开串口监视器能看到 GPS NMEA 原始数据、LoRa 发送日志、以及收到的 APRS 文本帧。注意 USB 转串口芯片的驱动要提前装好否则pio device monitor会提示找不到端口。如果编译报错找不到BG_RF95.h先确认 lib 目录是否存在再检查 lib_deps 里有没有误加同名单RadioHead导致冲突。如果烧录成功但 GPS 无数据先把串口波特率改成 9600 试T-Beam 某些批次默认不供电给 GPS需要检查GPS_PWR_EN引脚是否被拉高。这些坑不算大但碰到时容易卡住新手很久。4. GPS 定位解析、LoRa 报文发送与接收排错4.1 GPS 解析从 NMEA 到 APRS 坐标TTGO T-Beam 板载 GPS 模块输出 NMEA 格式常见 GGA、RMC 语句。工程里使用 TinyGPS 做解析它在收到GPRMC或GPGGA后更新经纬度。关键代码如下void gpsFeed(uint8_t b) { if (gps.encode(b)) { if (gps.location.isValid()) { double lat gps.location.lat(); double lng gps.location.lng(); // 转成 APRS 使用的 ddmm.mm 格式 convertLatLngToAPRS(lat, lng); gpsLocationValid true; } } } void convertLatLngToAPRS(double lat, double lng) { char latStr[16], lngStr[16]; float latMin (lat - (int)lat) * 60.0; float lngMin (lng - (int)lng) * 60.0; snprintf(latStr, sizeof(latStr), %02d%05.2f%c, (int)abs(lat), latMin, lat 0 ? N : S); snprintf(lngStr, sizeof(lngStr), %03d%05.2f%c, (int)abs(lng), lngMin, lng 0 ? E : W); }这段代码里最容易出错的是 APRS 的坐标格式。NMEA 里常见的是ddmm.mmmmAPRS 位置报告也采用ddmm.mm但不带度数符号只依靠大写字母 N/S/E/W 表示象限。如果你直接把 GPS 返回的小数点格式拷贝进 APRS 报文接收端地图软件会显示错位几百米。上面代码把小数度转换为“度的小数部分乘以 60 得到分”再取两位小数就能得到3723.45N这样的标准格式。4.2 构造 APRS 位置包并发送GPS 数据有效后下一步是把经纬度放进 APRS 信息字段再交给 LoRa 驱动发送。常见的包结构是这样的void buildAndSendPacket() { char aprsPacket[160]; snprintf(aprsPacket, sizeof(aprsPacket), %sAPRS,WIDE1-1:!%s/%s%c, MY_CALL, latStr, lngStr, APRS_SYMBOL_T); uint8_t len strlen(aprsPacket); rf95.send((uint8_t*)aprsPacket, len); rf95.waitPacketSent(); Serial.printf([TX] %s\n, aprsPacket); }%sAPRS这段表示源呼号是MY_CALL目的地路径填APRSWIDE1-1是给中继转发的路径标记。车上使用建议保留WIDE1-1因为在多个 LoRa 网关构成的网络中它决定数据包能被转发几跳。若只是两台 T-Beam 点对点测试路径可以省略填TCPIP*是某些 igate 的习惯无线直连场景不推荐。发送之后一定要调用waitPacketSent()否则下一次发送会覆盖前一包。BG_RF95 在发送期间 DIO0 会拉高表示完成这个等待是必要的。如果发送回执失败优先检查 frequency 参数和硬件接线RFM95 的 CS、SCK、MOSI、MISO 是否和 ESP32 引脚对应RST 引脚有没有被其他外设占用。4.3 接收方向解析别人的 LoRa APRS 包发射只是这套链路的一半接收更多。接收侧核心代码如下uint8_t buf[255]; uint8_t len sizeof(buf); if (rf95.available()) { len rf95.receive(buf, len); if (len 0) { buf[len] \0; Serial.printf([RX] %s\n, (char*)buf); // 先做长度和格式冒烟测试 if (len 10 strchr((char*)buf, :) ! NULL) { parseAPRSPacket((char*)buf); } } }rf95.available()查询 DIO0 引脚一旦收到完整 LoRa 包就返回 true随后receive()把载荷拷贝到用户缓冲区。这里必须严格限制len避免数据包溢出缓冲区。解析时常见做法是先定位冒号冒号前是源呼号路径冒号后是 APRS 信息字段。信息字段的首字符决定报文类型!和表示位置报文:表示状态消息T表示遥测。如果接收端没有显示数据先隔离问题用第二块 T-Beam 发看接收端串口有没有[RX]前缀。没有就说明射频参数不匹配或天线没接。接上但显示乱码优先检查波特率是否一致再看 CRC 是否开启CRC 两端必须一致。LoRa 是同步扩频系统带宽、SF、CR 任何一个不一致都解不出数据这点和普通 FSK 设备差别很大。5. 把 BG_RF95 移植到 STM32 的接线与验证技巧讲完 ESP32 上的完整逻辑再说 LORASTM32 里让人惦记的 STM32。BG_RF95 库的底层就是 SPI 读写 RFM95 寄存器上层业务只和“发送一段字节”打交道。因此把它搬到 STM32 最快的方式是保留 APRS 组包逻辑只替换射频驱动层。引脚映射参考如下信号TTGO T-Beam GPIOSTM32F103 示例引脚SCKGPIO5PA5MISOGPIO19PA6MOSIGPIO27PA7CSGPIO18PA4RSTGPIO23PA3DIO0GPIO26PA2RFM95 工作电压 3.3VSTM32 也是 3.3V可以直接连接不需要电平转换。但 DIO0 必须接在一个支持外部中断的引脚上否则不能用available()轮询只能靠定时器反复读寄存器。移植时先在 STM32CubeMX 里把 SPI1 配成全双工主模式速率先设 1MHz等调通后再提高。void rf95_transfer(uint8_t *tx, uint8_t *rx, uint16_t len) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx, rx, len, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } void rf95_init(void) { uint8_t version; // 读取 RFM95 的版本寄存器地址 0x42 HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_SET); HAL_Delay(10); uint8_t cmd[2] {0x42, 0x00}; rf95_transfer(cmd, version, 1); if (version ! 0x12) { // 打印错误说明 SPI 时序或接线有问题 } }上面的0x12是 RFM95 固件版本号常见 RFM95W 上电复位后读出0x12。如果读到的不是这个值大部分情况下不是模块坏了而是 CS 时序没有严格按“拉低后写命令同时读回数据”的 SPI 规则来。注意 RFM95 的 SPI 有一个特点命令字节如果在写高位时同时有数据返回必须把整个字节交换放在同一段 CS 置位窗口内不能用读和写分成两次调用。最后分享一个验证技巧移植完成后不要急着接 GPS。先用逻辑分析仪抓 CS、SCK、MISO 三根线手动发一条rf95_init()观察 MISO 上第一个字节是否回0x12。没有逻辑分析仪就用串口把那一片 SPI 接收到的原始字节打印出来。只要版本号对上后续 APRS 组包、LoRa 发送和 ESP32 原版工程没有任何区别。这时再用串口工具的 hex 视图看 SPI 总线确认地址 0x42 写回 0x12整条链路才算真正打通。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进