TTGO T-Beam LoRa APRS固件解析与STM32移植指南
2026/9/16 12:03:17 网站建设 项目流程

简介:一款面向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,编码率 CR=4/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); // CR=4/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 = TinyGPSPlus

board = ttgo-t-beam会把 T-Beam 默认的 flash 大小、PSRAM 开没开、引脚定义都带出来。如果你的 PlatformIO 版本识别不了这个板名,可以换成board = ttgo-t-beamboard = 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 串口写入 ESP32,T-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++ 做解析,它在收到GPRMCGPGGA后更新经纬度。关键代码如下:

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.mmmm,APRS 位置报告也采用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), "%s>APRS,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); }

%s>APRS这段表示源呼号是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 示例引脚
SCKGPIO5PA5
MISOGPIO19PA6
MOSIGPIO27PA7
CSGPIO18PA4
RSTGPIO23PA3
DIO0GPIO26PA2

RFM95 工作电压 3.3V,STM32 也是 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,整条链路才算真正打通。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询