news 2026/9/16 12:16:30

STM32驱动TTGO T-Beam LoRa实现APRS网关

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32驱动TTGO T-Beam LoRa实现APRS网关

简介:这份开源项目聚焦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 引脚说明
PB13SCKSPI 时钟,初始 1 MHz,不要直接上 18 MHz
PB14MISO主控读寄存器
PB15MOSI主控写寄存器
PB12NSS片选,低电平有效,不要接地
PA8RST复位,低电平复位
PA9DIO0接收完成/发送完成中断

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 LoRa0x7E直接做 CRC 校验
KISS over LoRa0xC0去掉 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)适用场景
SF7125 kHz约 50 ms近距离中继,转发频繁
SF9125 kHz约 140 ms远距离信标
SF12125 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 出来看一遍,往往比穷举参数快得多。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 12:15:39

FPGA高速数据通路实战:Spartan-6 DDR3与千兆以太网Verilog设计

简介&#xff1a;一套基于Xilinx Spartan-6 LX16 FPGA开发板的Verilog逻辑例程合集&#xff0c;面向使用ISE 14.7进行FPGA开发的工程师与学习者&#xff0c;重点覆盖DDR3、千兆以太网等高速接口的工程实现。合集收录30个例程&#xff0c;从LED、按键、PLL等入门实验&#xff0c…

作者头像 李华
网站建设 2026/9/16 12:15:34

红外遥控控制电机:从NEC协议解码到PWM调速实战

简介&#xff1a;这套实验例程围绕单片机红外遥控电机远程控制设计&#xff0c;定位于已掌握基础编程、希望深入硬件控制的入门至中级学习者。实验从红外遥控器编码解码入手&#xff0c;演示如何将按键信号解析为控制指令&#xff0c;进而驱动电机启停、调速及换向&#xff1b;…

作者头像 李华
网站建设 2026/9/16 12:14:00

Cadence SIP Layout:系统级封装物理实现核心引擎

1. 项目概述&#xff1a;Cadence SIP Layout不是“画图软件”&#xff0c;而是系统级封装的物理实现中枢Cadence SIP Layout&#xff0c;这个名称里藏着三个关键信号&#xff1a;Cadence——指代Cadence Design Systems公司旗下完整的IC与封装协同设计平台&#xff1b;SIP——S…

作者头像 李华
网站建设 2026/9/16 12:13:25

小米硬件岗笔试题背后的工程师能力图谱

1. 这不是题库搬运&#xff0c;而是硬件工程师能力图谱的现场解剖“小米2026秋招实习-硬件研发工程师-笔试题真题&#xff08;5套&#xff09;”——这个标题在应届生求职季里像一块磁铁&#xff0c;吸住所有盯着小米硬件岗的同学。但我要先泼一盆冷水&#xff1a;把这5套题当“…

作者头像 李华
网站建设 2026/9/16 12:13:05

SRGAN与图像去噪:损失函数、训练调度与工程落地全解析

简介&#xff1a;这份代码基于PyTorch实现了SRGAN图像超分辨率算法&#xff0c;并融合GaN风格的门控去噪思路&#xff0c;适合深度学习初学者、图像处理研究者以及希望复现GAN类生成模型的开发者&#xff0c;无论是课题研究还是工程实践都具备参考价值。资源配套完整的训练、测…

作者头像 李华