简介:面向具备C语言与嵌入式基础的开发者及IoT产品研发人员,SX126x系列(SX1261、SX1262、SX1268)LoRa芯片驱动源码可帮助快速搭建远距离、低功耗无线通信的驱动层,解决射频参数配置、收发控制、CAD空闲信道检测等关键技术问题。压缩包内共611个文件,以328个h头文件、261个c源文件为主体,搭配6个s汇编启动文件、3个uvprojx工程文件、2个ld链接脚本及PDF说明文档,整体约3.64MB,工程结构清晰,便于移植到基于STM32L0/L1/L4等平台的项目中。已有2374人下载学习。包内自带SX126x CAD示例工程(Cad_Example),演示了信道活动检测的实际用法,并集成了HAL库的定时、SPI、I2C、ADC等底层驱动代码;读者可对照示例理解LoRa调制解调的关键流程,获取初始化配置、发送接收函数、错误处理与状态管理等模块,进而在自有硬件上完成驱动适配与性能调试,缩短无线模块的开发周期。
1. 新一代LoRa芯片SX126x的驱动源码:为什么你的收发流程还卡在“玄学”上
SX126x 这颗新一代 LoRa 芯片,最大卖点从不是只靠“更远”打动你,而是把收发状态、中断映射、射频开关控制全部收拢到一条 SPI 总线上。很多人拿到模块后的第一反应是移植芯片厂家的驱动源码,结果还是一团黑匣子:校准没过、TX_DONE 不触发、休眠电流多出几百微安。我见过不少项目死在这些环节上,其实根子不是射频玄学,而是驱动源码里的初始化顺序和事件消费逻辑没有理顺。下面就从 SPI 最低层讲到状态机和休眠唤醒,适合正在做 LoRa 节点、网关,或者想把旧平台从上一代芯片迁移过来的人。
2. 写驱动前先定死 SPI 与引脚:SX126x 最小通信骨架
2.1 先把芯片选型搞清楚:SX1261、SX1262、SX1268 的软件边界
SX126x 不是一个单芯片,而是一个家族。我一般根据模块输出功率来选型:SX1261 最大输出在 +15dBm 附近,适合纽扣电池供电的节点;SX1262 能到 +22dBm,适合网关或需要穿透两层楼的中远距离设备;SX1268 则常见于 433MHz、470MHz 这类低频段模块。它们的硬件匹配网络和频段不同,但驱动层的命令接口基本一致。这意味着你只要维护一套驱动源码,用宏切换型号即可,不必为每颗芯片保留两套协议栈。
选型之后有一件事比调参数更优先:确认模块原理图里 DIO2 和 DIO3 是否已经被内部占用。很多模组为了省引脚,把 DIO2 接到射频收发切换芯片,把 DIO3 接给 TCXO 供电。一旦模块内部接死了这两根线,驱动代码里就不能再把 DIO2、DIO3 当普通中断映射来用,否则初始化完一进发射,GPIO 电平被射频开关乱拉,直接翻车。这一步要看你手里的模块资料,不是芯片数据手册能替你决定的。
2.2 SPI 通信模型:命令、寄存器、Buffer 三块地址空间
SX126x 的 SPI 不像普通传感器那样“写地址改位域”,而是分成三类接口。第一类是命令操作,字节流里先送命令码,再跟参数;第二类是寄存器读写,通过 ReadRegister、WriteRegister 访问配置区;第三类是 Buffer 操作,也就是芯片内部的 FIFO 数据区,发送前往里填数据,接收后从中取数据。三种操作共用同一条 SPI 总线,靠命令字区分,所以驱动源码里最核心的其实是底层收发函数,后面所有功能都建立在它上面。
SPI 时序上有两个决定成败的点。第一,片选低电平期间,每字节都是高位在前,命令码一字节,参数按数据手册要求字节对齐。第二,BUSY 引脚必须当作握手信号,芯片处理命令期间 BUSY 会拉高,驱动要等它回到低电平才能发下一条命令。很多移植失败案例都是把 BUSY 当成普通 GPIO,初始化完就放一边,命令一多必出乱子。我习惯在驱动骨架里先写一个带超时的 busy_wait,后面所有通信都从它开始。
2.3 最小驱动骨架:从 SPI 初始化到读取芯片型号
下面这段 C 代码是我常用的最底层通信骨架,它不涉及具体平台 API,只留出对接函数和宏定义。你移植时只要把platform_spi_rw、gpio_read换成实际平台函数,把操作码按芯片数据手册指令表填入即可。
/* 底层 SPI 读写:全双工,读的时候要补 0x00 */ static void spi_transfer(uint8_t *tx, uint8_t *rx, uint16_t len) { platform_spi_rw(tx, rx, len); } /* 等待 BUSY 引脚回到低电平,超时返回 -1 */ static int busy_wait(uint32_t timeout_ms) { uint32_t t0 = tick_ms(); while (gpio_read(BUSY_PIN)) { if (tick_ms() - t0 > timeout_ms) return -1; } return 0; } /* 通用命令通道:先送命令码,再送参数 */ static int sx126x_cmd(uint8_t opcode, uint8_t *params, uint8_t len) { uint8_t buf[16] = {opcode}; if (busy_wait(100) != 0) return -1; for (uint8_t i = 0; i < len; i++) buf[i + 1] = params[i]; gpio_write(NSS_PIN, 0); spi_transfer(buf, NULL, len + 1); gpio_write(NSS_PIN, 1); return busy_wait(100); } /* 读寄存器:先发地址,再读回两个字节,第二个才是寄存器值 */ static int sx126x_read_reg(uint16_t addr, uint8_t *val) { uint8_t cmd[3] = {OPCODE_READ_REG, (addr >> 8) & 0xFF, addr & 0xFF}; uint8_t out[2] = {0, 0}; if (busy_wait(100) != 0) return -1; gpio_write(NSS_PIN, 0); spi_transfer(cmd, NULL, 3); spi_transfer(NULL, out, 2); gpio_write(NSS_PIN, 1); if (busy_wait(100) != 0) return -1; *val = out[1]; return 0; }这里有几个参数值得说清楚。busy_wait的超时我设成 100ms,芯片最长的校准操作也就几十毫秒,100ms 足够区分“还在忙”和“卡死”;sx126x_cmd里的len是参数字节数,不包含命令码,所以 SPI 总线上实际传输长度是len + 1;sx126x_read_reg读回的第一个字节是芯片状态,第二个字节才是数值,很多新手拿到两字节直接取第一个,导致版本号永远读不对。spi_transfer是全双工,第二个参数传NULL表示当前方向只发不收,平台函数内部要处理空指针场景,实际工程里我会用一个全 0 的 dummy 缓冲来顶位。
2.4 复位与待机:先把芯片从“未定义”里捞出来
上电后不建议立刻发配置命令,SX126x 内部有上电复位流程,如果 RESET 引脚可控,就拉低再拉高。复位之后必须进 STANDBY,STANDBY 的配置字节可以选 RC 待机或 XOSC 待机,RC 待机功耗低,适合刚上电;XOSC 待机适合马上要发送的场景,省去内部主时钟起振等待。驱动源码里我习惯把这一步做成独立函数,初始化流程第一件事调用它,而不是一上来就设置频率。
为什么读版本寄存器能验证通信?因为版本寄存器地址固定,复位后即可读,不依赖任何调制配置。如果读出来是全 0xFF 或全 0x00,说明 SPI 时序没对上,或者 BUSY 握手失败。这个步骤就是通信探针,跑通后后面所有配置命令才有意义。我建议每个驱动都保留自检函数,产品返修时排查通信问题会快很多。
3. 调制参数与频率配置:让 SX126x 按你的速率说话
3.1 LoRa 调制四要素:扩频因子、带宽、编码率、低速率优化
LoRa 调制参数直接影响距离、速率和空中占用时间。扩频因子 SF 从 5 到 12,数值越大灵敏度越高、空中速率越慢;带宽 BW 常见 125kHz、250kHz、500kHz,带宽越宽速率越快但灵敏度变差;编码率 CR 表示校验位占比,CR 越大抗干扰越强,但有效负载占比下降。还有一个容易被忽略的低速率优化 LDRO,当 SF 较大且实际符号时间较长时,驱动里必须把 LDRO 打开,否则接收端很容易出现误码。
我调参时习惯先定带宽和 SF,再考虑编码率。比如抄表类业务常用 SF10 + 125kHz + CR4/5,单包 200 字节左右,一包在空中占几十毫秒,覆盖和速率均衡。网关上行如果用 SF7,速率高但距离近,适合室内部署。这几个参数和灵敏度、空中速率是跷跷板,驱动源码只负责把选好的参数写成命令发给芯片,而选参数是产品业务层面的事,驱动层不要写死。
3.2 初始化顺序:先校准、再设置数据包格式
把芯片从待机状态带到可用状态,我封装成下面这个函数。顺序上有个容易被忽略的点:SetPacketType必须在调制参数之前执行,因为芯片要先知道自己是 LoRa 模式还是 FSK 模式,后面的参数解释方式完全不同。
static int sx126x_lora_init(uint32_t freq_hz, uint8_t sf, uint32_t bw, uint8_t cr) { /* 0x01 表示 LoRa 包类型,0x00 才是 FSK */ sx126x_cmd(OPCODE_SET_PACKET_TYPE, (uint8_t[]){0x01}, 1); /* 频率参数按芯片内部公式拆成 4 字节 */ uint32_t rf_reg = (uint32_t)((double)freq_hz * 33554432.0 / XTAL_FREQ); uint8_t freq_buf[4] = { (rf_reg >> 24) & 0xFF, (rf_reg >> 16) & 0xFF, (rf_reg >> 8) & 0xFF, rf_reg & 0xFF }; sx126x_cmd(OPCODE_SET_RF_FREQUENCY, freq_buf, 4); /* 0x7F 表示校准全部频段,初始化时做一次即可 */ sx126x_cmd(OPCODE_CALIBRATE, (uint8_t[]){0x7F}, 1); /* 调制参数:SF/BW/CR/LDRO 的编码值见数据手册 */ uint8_t mod_buf[8] = {sf, bw_encoding(bw), cr, 0, 0, 0, 0, 0}; sx126x_cmd(OPCODE_SET_MODULATION_PARAMS, mod_buf, 8); /* 包参数:前导码、显式头、CRC 开关、负载长度都在这里 */ uint8_t pkt_buf[7] = {0x00, 0x0C, 0x01, 0x01, 0xFF, 0x00, 0x00}; sx126x_cmd(OPCODE_SET_PACKET_PARAMS, pkt_buf, 7); /* 发送功率和斜坡时间,SX1262 可到 22dBm */ sx126x_cmd(OPCODE_SET_TX_PARAMS, (uint8_t[]){0x16, 0x04}, 2); /* 中断映射:DIO1 上只使能需要的位 */ sx126x_cmd(OPCODE_SET_DIO_IRQ_PARAMS, build_irq_mask(), 8); return 0; }频率参数的计算公式是公开的:reg = freq_hz * 2^25 / XTAL_FREQ,2 的 25 次方是 33554432。XTAL_FREQ 一般取 32000000,也就是 32MHz,具体以你模块上实际晶体为准。bw_encoding是带宽到编码值的映射函数,125kHz、250kHz、500kHz 分别对应数据手册里的固定值,不同芯片版本略有差异,不要跨芯片直接复制头文件。发送功率我写的0x16是 SX1262 的 22dBm 档位,如果你用 SX1261,最大 15dBm,这个值要改,否则驱动力争求稳反而把芯片推到极限。
3.3 关键参数速查表
下面的表是我写在驱动头文件里的注释,目的是让移植的人一眼看到当前配置范围,不至于改乱。
| 参数 | 范围 | 典型值 | 说明 |
|---|---|---|---|
| SF | 5~12 | 10 | 越大灵敏度越高,速率越慢 |
| BW | 125k/250k/500k | 125k | 带宽越宽速率越快,但灵敏度下降 |
| CR | 4/5~4/8 | 4/5 | 编码率越低,冗余越多,抗干扰越强 |
| LDRO | 0/1 | SF11+125k时置1 | 低速率优化,长符号时间必开 |
| CRC | 0/1 | 1 | 关闭后接收端可能丢包,测试对比用 |
| Header | 显式/隐式 | 显式 | 显式头带长度和 CRC 信息,便于解析 |
拿到一颗新模块,我会先用最保守的一组参数点对点测通:SF10、125kHz、CR4/5、CRC 开、显式头,再把数值往激进方向调。先用通信保底,再优化速率和功耗,这条经验能省一大半调试时间。测试链路时把两端参数打成一模一样,别在接收端开自动频率校正,否则排查问题时会多一个变量。
3.4 TCXO 与 DC-DC:三个模块级小配置,少了任何一个都可能“漂移”
驱动源码里比调制参数更容易翻车的是晶体和电源模式。如果模块用了 TCXO,驱动必须把 DIO3 配置成 TCXO 供电控制,同时告诉芯片供电电压值;上电后要等 DIO3 稳定输出,TCXO 才起振。没有这一步,射频频率会随温度明显漂移,有时候隔开一两米就收不到包。这种“飘”很玄学,实际就是 DIO3 没配置成 TCXO 供电。
电源方面,SX126x 支持 LDO 和 DC-DC 两种模式。DC-DC 模式需要外围电感,很多模块已经内置;驱动在初始化时要显式指定,否则芯片默认走 LDO,接收电流能差出接近 1mA 的量级。我习惯把这两个设置写成独立配置宏:TCXO_ENABLE、DCDC_ENABLE,每个项目只改宏定义,不碰芯片层代码。还有一个模块级差异,如果模块原理图里 DIO2 已经接射频开关,SetDioIrqParams里 DIO2 对应的掩码一定不要配中断,否则会出现“一进接收 GPIO 就被乱拉”的怪毛病,这条我吃过亏,特意放在这里提醒。
4. 收发状态机:从轮询到中断,把 SX126x 驱动写成可复用代码
4.1 发送流程:写 Buffer、设基址、触发 TX
发送的物理动作比很多人想得简单:先把有效载荷写进芯片 FIFO,再发一条 SetTx 命令。难点在 FIFO 的基址规划和超时语义。SX126x 内置 FIFO 共 256 字节,SetBufferBaseAddress可以分别指定发送基址和接收基址,防止并发时互相踩踏。
int sx126x_send(uint8_t *data, uint16_t len) { /* TX 基址 0x00,RX 基址 0x00;产品里建议 RX 放到 0x80 */ sx126x_cmd(OPCODE_SET_BUFFER_BASE_ADDRESS, (uint8_t[]){0x00, 0x00}, 2); /* 先写数据,再启动发送;写数据命令不含长度,长度由包参数决定 */ sx126x_write_buffer(0x00, data, len); /* 超时两个字节填 0,表示单次发送模式,发完即停 */ sx126x_cmd(OPCODE_SET_TX, (uint8_t[]){0x00, 0x00}, 2); return 0; }代码里sx126x_write_buffer的内部实现要再走一遍 SPI:先发写 Buffer 命令和偏移地址,再连续传数据字节。SetTx 的第二个参数是超时,填 0 代表无超时,如果填了非零值,芯片会在规定时间内没完成发送就自动退回 STANDBY。产品逻辑里我会用 0,让发送完全由 TX_DONE 事件驱动。如果你在网关里做突发下行,建议从发送命令到进接收窗口之间,状态由驱动状态机统一调度,不要在应用层连续调用发送,否则第二次 SetTx 很可能因为芯片尚未退回待机而失败。
4.2 接收流程:连续接收与单次接收的取舍
接收可以分成两种:SetRx 带 0 表示连续接收模式,芯片一直监听,直到检测到合法前导码和帧;带非零超时则超时后自动切回待机,适合做网关的时隙接收。节点低功耗采集场景,我习惯用“定时唤醒 + 单次接收 + 超时回到休眠”的思路。单次接收模式下,RX_DONE 之后芯片自动回到 STANDBY,驱动状态机要立刻更新,否则下一包到来时它根本不在接收态。
int sx126x_start_rx(uint32_t timeout_symbols) { uint8_t t[2] = { (timeout_symbols >> 8) & 0xFF, timeout_symbols & 0xFF }; return sx126x_cmd(OPCODE_SET_RX, t, 2); }timeout_symbols的单位是符号数,不是毫秒。符号时间和 SF、BW 相关,例如 SF10 + 125kHz 的单符号时间约 1.024ms,所以超时 1000 代表大约 1 秒。填 0 是无限连续接收。这个函数要和状态机配合,进 RX 前把状态标记改为 RX,收到 RX_DONE 再改回 IDLE,不要靠猜。
4.3 DIO1 中断映射与回调分发:把事件从 ISR 里解耦出来
DIO1 引脚可以配置成多种中断事件的输出。SetDioIrqParams的 8 字节参数中,第 1、2 字节是全局中断使能掩码,第 3、4 字节是 DIO1 对应的掩码,后面四字节是 DIO2/DIO3 的掩码。实际只用 DIO1 的话,把全局掩码和 DIO1 掩码设成相同值即可,其他位留 0。这样 GPIO 中断只有一个来源,事件类型靠读中断状态寄存器区分。
void sx126x_dio1_isr(void) { uint16_t st = sx126x_get_irq_status(); /* 先清中断,再分发业务,防重入和漏事件 */ sx126x_clear_irq_status(st); if (st & SX126X_IRQ_TX_DONE) { tx_done_handler(); } if (st & SX126X_IRQ_RX_DONE) { rx_done_handler(); } if (st & SX126X_IRQ_CRC_ERR) { crc_err_cnt++; } }ISR 里三个动作有先后:先读中断状态,再清中断,最后处理业务。清中断放在读之后、业务之前,这样即使处理过程中又来新的中断,状态寄存器也不会被旧事件盖住。SX126X_IRQ_TX_DONE这类掩码值要按数据手册定义成头文件宏,不同事件之间尽量用位域分开。ISR 里不要做打印或延时,payload 解析放到主循环。
4.4 状态机:IDLE/RX/TX/CAD 之间别乱跳
驱动写到一定规模,必须有状态变量。我维护四个状态:IDLE、RX、TX、CAD。应用层请求发送时,只有 IDLE 才允许进 TX;TX_DONE 回调里强制回 IDLE;应用层请求接收时,从 IDLE 进 RX;RX_DONE 后回 IDLE。CAD 用在需要省电的场景:先做信道检测,CAD_DONE 且 CAD_OK 才进 RX,否则继续休眠。状态机的核心价值是任何回调里都能知道当前在哪个环节,不会出现发完一包立刻又进 TX 把射频占住的情况,尤其是在 LoRaWAN 这种有时间窗的协议里,状态错了会直接错过窗口期。
状态转移只有四条主线:IDLE 到 TX 由应用发起,TX 到 IDLE 由 TX_DONE 触发,IDLE 到 RX 由应用发起,RX 到 IDLE 由 RX_DONE 或超时触发。驱动代码始终围绕这四条线写,新增功能时先想清楚它属于哪个状态,再动手加回调。这样看起来多写了一点代码,但后期定位问题会非常快。
5. SX126x 驱动调试避坑:五条血泪记录与排查清单
5.1 现象:SPI 写任何命令都石沉大海,读寄存器全是 0xFF
最常见的原因是 BUSY 握手形同虚设,上电后没等芯片从复位中走出来就开始发命令;其次是 SPI 的时钟极性和相位与模块要求不一致。解决方法是先跑一遍通信探针流程:RESET 拉低至少 5ms,拉高后延时 10ms,每次收发前调用busy_wait。如果读出来还是 0xFF,用逻辑分析仪抓 CS、SCK、MOSI 三根线,确认 MOSI 上的命令字真的发出去了,别一上来就怀疑芯片坏了。我遇到过两次所谓“坏片”,最后都是 SPI 时序配置错一个边沿。
5.2 现象:发送成功了接收端却收不到包,示波器看射频有波形
原因通常是频率或调制参数两端不一致,最常见是带宽和扩频因子不同;也可能是显式头模式下 CRC 配置不一致,接收端校验失败后直接丢包。解决方法是先把接收端参数改成和发送端完全一致,再用 CAD 模式确认信道上有无合法前导码;如果 CAD 能检测到而 RX 收不到,把发送端 CRC 关掉、改成隐式头对比。这里有个容易忽略的 LDRO,SF11/SF12 配 125kHz 时如果发送端开了 LDRO 而接收端没开,会出现“能 CAD 但收不到包”的诡异现象,浪费我整整一个下午。
5.3 现象:Sleep 模式电流高达几百微安,远大于手册标称值
原因一般是 DIO 引脚悬空导致漏电,或者 TCXO 供电还在继续,或者电源管理没有切到 DC-DC。解决方法是进入 Sleep 前把 DIO1 配成固定电平,DIO3 如果用作 TCXO 供电要在 Sleep 命令里一并关断,然后对比 LDO 和 DC-DC 两种模式的电流。实测经验是,单单把 DIO1 从浮空改到固定电平,电流就能降一个量级。测量时记得把模块周围的 LED 和电平转换芯片也断电,否则测到的是整板电流,不是芯片电流。
5.4 现象:第一包正常,第二包开始连发失败,必须复位才恢复
原因是 TX_DONE 中断产生后芯片没有回到 STANDBY,第二次 SetTx 被拒绝;或者上一包的 IRQ 状态没清,事件累积导致状态错乱。解决方法是 TX_DONE 回调里先清 IRQ,再显式发一次 SetStandby,然后才允许状态机回到 IDLE。驱动代码里要把“发送完成回待机”做成固定流程,不要在应用层发完一包马上又调用发送函数,中间至少让出几百微秒给芯片内部状态切换。
5.5 现象:偶发初始化失败,冷启动时尤其多
原因是晶振起振时间不足,复位后立刻校准或设置频率,芯片内部时钟还没稳定。解决方法是把初始化等待时间拉长,校准前加一个 10ms 固定延时;另外把校准步骤放在 SetPacketType 之后,不要在之前。这个顺序问题我在不同两家模块厂商的板子上都验证过,先定包类型再校准,结果的稳定性更好。如果把这段初始化代码放到产品量产测试里,建议给它加失败重试机制,连续三次失败才真正上报故障。
6. 把驱动写进产品:状态机分层、休眠唤醒与 CAD 快检
驱动源码最后一定要分层。底层是 SPI 与命令原语,中间是芯片功能函数,上层是业务状态机。我习惯把芯片层设计成不依赖具体 RTOS,靠函数指针回调通知事件,后面换 MCU 或接入 LoRaWAN 协议栈时能少改很多代码。分层可以参照这个最小划分:底层只负责字节收发;芯片层只负责把业务层传入的参数翻译成指令;业务层只维护状态和回调。禁止底层直接调用状态机函数,否则项目一复杂就变成一锅粥。
休眠唤醒有个技巧:SX126x 的 Sleep 命令有两个模式位,一个保留上下文,一个复位配置。如果选了保留上下文,唤醒后不用重新校准和重设调制参数,但 DIO 中断映射往往需要重新配置,因为芯片从深度休眠醒过来后 DIO 状态机会复位。我每次唤醒后的固定三件事:发 SetStandby,再 SetDioIrqParams,最后清一次 IRQ。按这个顺序做,掉电再唤醒几十次都稳定,没有出现事件残留导致的假死。
另一个常用技巧是 CAD 快速检测。低功耗节点想知道信道有没有人,与其长时间开 RX 等,不如先做 CAD 检测前导码,检测到了再切 RX。CAD 完成以 CAD_DONE 中断为准,如果同时 CAD_OK 置位,再调用 SetRx(0) 进入接收。这一个技巧可以把平均接收功耗压缩到原来的三分之一以下,代价是增加几十毫秒的检测时间,对大部分传感器上报场景完全可接受。
早期我做过一批表计节点,就是漏了唤醒后清 IRQ,每隔几次唤醒就假死一次,排查了两天才发现中断事件一直停在状态寄存器里。从那以后,每个唤醒入口第一行一定是 ClearIrqStatus,然后才处理业务。这个习惯救了我很多次,希望帮到你。
本文还有配套的精品资源,点击获取