十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于STM32F103的SX1268/LLCC68 LoRa驱动开发实战

基于STM32F103的SX1268/LLCC68 LoRa驱动开发实战 简介这套STM32F103平台的LoRa驱动程序面向物联网嵌入式开发者提供SX1268与LLCC68模组间的直接数据收发方案适用于低功耗远程传感、无线抄表、工业数据采集等场景。压缩包共157个文件、约3.74MB核心是40个h头文件和37个c源码文件涵盖STM32标准外设库的定时器、RCC时钟、ADC、I2C、CAN、USART等驱动模块同时包含Keil工程文件uvprojx/uvoptx、hex烧录文件、map与axf编译输出、sct分散加载文件及txt说明拿到后可直接在MDK中打开、编译并烧录验证crf、lst、o等中间文件也可帮助排查编译细节。已有1341人学习使用适合具备基础单片机知识、想快速入手LoRa无线通信的工程师与学生。整个包保留了完整标准库框架和常用外设初始化代码除了能直接支撑SX1268/LLCC68通信也可作为后续低功耗物联网项目移植的参考模板省去从零搭建工程的时间。 做了大半年LoRa项目手头同时拿到过SX1268和LLCC68两颗芯片的模块一开始没多想就直接在STM32F103上各写了一套驱动结果后来发现代码几乎可以通用等于白写了两遍。这篇文章就把我在这套驱动上的整体思路、关键寄存器操作、收发流程和踩坑经历完整梳理一遍给正在做LoRa点对点、低功耗传感器节点或者想复现一套最小可用驱动的朋友做个参考。内容不涉及RTOS全部基于裸机标准外设库实现如果你用的是HAL库思路完全一致目录结构稍作调整就行。1. 硬件选型逻辑STM32F103与SX1268/LLCC68的搭配凭什么流行1.1 两颗芯片的定位差异和共同点SX1268和LLCC68都是Semtech家的LoRa收发器前者主要面向中国市场覆盖150MHz到960MHz的频段范围国内常见的470MHz~510MHz组网、780MHz、868MHz、915MHz模块基本都是它后者的定位更偏向低成本场景同样支持LoRa调制但压根就不支持FSK/GFSK数据手册上也明确写着只能跑LoRa包格式。从驱动开发的角度看这两颗芯片用的是同一套SPI接口协议和同一套寄存器命令集命令码、数据格式、时序要求几乎完全一样。很多国产模块厂商在丝印上写SX1268实际上焊的是LLCC68因为引脚兼容、软件兼容成本还更低。所以如果你在淘宝买了一块写SX1268的模块拿LLCC68的驱动去初始化大概率也能跑起来。从射频特性上看两颗芯片的接收灵敏度都在-137dBm左右SF12/125kHz配置下发射功率都支持到22dBm配合STM32F103这种主频72MHz的MCU做节点端数据采集和上报绰绰有余。真正决定性能上限的地方反而不是芯片本身而是你写驱动时对校准、PA配置、接收超时这些细节的处理。1.2 STM32F103的资源需求和接线规划SX1268/LLCC68和MCU之间只需要4根SPI线加3个控制引脚NSS、RST、BUSY外加一个可选的中断引脚DIO1。我实际项目里的接线是这样规划的信号STM32F103引脚说明SPI1_SCKPB13SPI时钟最高10MHzSPI1_MISOPB14芯片数据输出SPI1_MOSIPB15芯片数据输入NSSPA4GPIO模拟片选不用硬件NSSRSTPB0复位引脚低电平有效BUSYPB1忙状态检测高电平表示忙DIO1PB10收发中断标志可接EXTI这里特别说明一下为什么NSS用GPIO模拟而不是直接用SPI的硬件NSS。SX126x的命令时序要求在拉低NSS后立刻发送命令字节命令执行期间NSS必须全程保持低电平读操作时又需要在拉低NSS后先发命令码、再读若干字节整个过程如果要靠硬件NSS去管理受SPI外设的状态机限制比较多灵活性差。用GPIO模拟之后所有引脚都可以自由重映射后期换板子改布局也方便。SPI1的时钟模式必须是CPOL0、CPHA0也就是空闲时SCK为低电平第一个时钟沿采样数据。SX1268的SPI最高支持10MHz但STM32F103的APB2总线最大36MHzSPI预分频设2就是18MHz超出芯片上限了所以分频设4跑9MHz比较稳。1.3 决定共用底层驱动的时机我在刚开始写驱动时犯过一个错误就是把SX1268和LLCC68当成两颗完全不同的芯片各建了一套工程底层SPI读写函数复制粘贴上层收发逻辑改来改去。后来对比寄存器手册才发现两边的命令码、状态标志、中断标志几乎一字不差唯一明显的差异是LLCC68不支持FSKSetPacketType的时候只能写LoRa模式。从那之后我改成了一套驱动兼容两颗芯片初始化函数里加一个芯片型号参数编译时通过宏定义区分运行时正常读状态字完全不影响使用。如果你现在才起步建议从一开始就按这个思路做省掉后面大量重复劳动。2. 驱动分层设计SPI底层、寄存器操作、应用接口2.1 设备句柄的定义和引脚抽象我习惯把驱动拆成三层最底层是SPI字节读写和GPIO操作中间层是芯片命令封装最上层才是给业务逻辑用的发送、接收、休眠接口。中间层和上层之间通过一个结构体把引脚和SPI实例串起来方便换板子时只改配置不改逻辑。typedef struct { SPI_TypeDef *spi; GPIO_TypeDef *nss_port; uint16_t nss_pin; GPIO_TypeDef *rst_port; uint16_t rst_pin; GPIO_TypeDef *busy_port; uint16_t busy_pin; GPIO_TypeDef *dio1_port; uint16_t dio1_pin; } lora_sx126x_t;实际在STM32F103上我用的是标准外设库底层SPI读写函数就一个static uint8_t lora_spi_rw(uint8_t dat) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, dat); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); }这里有个容易忽略的点SX126x的SPI读操作不是靠MISO线上单独读而是主机持续发送0x00或任意字节同时从MISO上接收数据。换句话说读一个字节必须先发一个字节。很多新手在写ReadBuffer时只发命令码然后干等接收结果读回来的全是0xFF或者错位数据。2.2 命令读写的规范写法SX126x所有的操作都通过命令码完成命令分两大类一类是设置类命令比如SetStandby、SetRfFrequency、SetTx这类命令MCU往SPI写命令码加参数另一类是读取类命令比如GetStatus、ReadBuffer、GetIrqStatus这类命令MCU先写命令码再连续读若干字节。封装一个通用的命令写入函数static void lora_cmd(lora_sx126x_t *dev, uint8_t cmd, uint8_t *params, uint8_t len) { lora_wait_busy(dev); GPIO_WriteBit(dev-nss_port, dev-nss_pin, Bit_RESET); lora_spi_rw(cmd); for (uint8_t i 0; i len; i) { lora_spi_rw(params[i]); } GPIO_WriteBit(dev-nss_port, dev-nss_pin, Bit_SET); lora_wait_busy(dev); }注意前后各调用了一次lora_wait_busy。第一次等待是为了确保芯片空闲、可以接收新命令第二次等待是为了确保命令真正执行完毕尤其像Calibrate、SetStandby这种需要芯片内部处理的命令执行完BUSY拉低之后才算完成。很多人只等一次后面马上发下一条命令偶发性的时序竞争问题就是这么来的。2.3 BUSY引脚为什么是命门BUSY引脚是整个驱动里最不能轻视的信号。SX126x内部有一个状态机它处理命令的时候BUSY引脚输出高电平处理完毕输出低电平。MCU在NSS拉低之前必须确认BUSY为低否则命令字节会被芯片直接忽略SPI通信流程整个错乱。为了不因为某个异常状态下死循环我习惯给等待函数加一个超时计数static int lora_wait_busy(lora_sx126x_t *dev) { uint32_t timeout 100000; while (GPIO_ReadInputDataBit(dev-busy_port, dev-busy_pin) Bit_SET) { if (--timeout 0) { return -1; } } return 0; }返回值给上层一个处理错误的机会而不是让程序卡死。实战中遇到过几次BUSY一直拉高的状况后面专门有一节讲排查方法这里先把结论说清楚90%以上是上电后没有等RST释放完就开始发命令或者SPI模式配错。3. 收发全流程初始化、发射、接收的代码级拆解3.1 上电初始化序列初始化顺序有讲究不能乱来。我总结了一套固定的顺序每步都有明确目的拉高RST延时1ms拉低RST再延时1ms拉高RST等待至少100ms让芯片完成内部上电复位。调用SetStandby进入待机模式参数写0x01表示使用RC振荡器时钟。调用Calibrate执行全频带校准参数写0x7F表示校准所有模块。芯片出厂后内部校准数据不会永久保存每次上电后、配置频率之前必须重新校准否则频率精度和接收灵敏度都会下降。调用SetPacketType设置为LoRa模式参数为0x01。调用SetRfFrequency设置载波频率这是整套驱动里最关键的寄存器计算。调用SetTxParams设置发射功率和斜坡时间。调用SetPaConfig配置功放这个参数和模块的频段、供电方式直接相关。调用SetBufferBaseAddress将收发缓冲区基地址设为0x00。调用SetDioIrqParams配置中断掩码把需要的收发完成中断映射到DIO1引脚上。SetRfFrequency的频率计算是很多新手容易算错的地方。SX126x的频率寄存器是24位步进是15625Hz也就是15.625kHz。寄存器值的计算公式如下uint32_t freq_reg (uint32_t)((double)freq_hz / 15625.0); uint8_t rf_freq[4]; rf_freq[0] (freq_reg 24) 0xFF; rf_freq[1] (freq_reg 16) 0xFF; rf_freq[2] (freq_reg 8) 0xFF; rf_freq[3] freq_reg 0xFF; lora_cmd(dev, 0x86, rf_freq, 4);以470MHz为例470000000除以15625等于30080十六进制是0x007580按上面的顺序写入就是00 00 75 80。这个值如果算错一位载波频率偏出去几十千赫兹接收端直接解调不出来而且用肉眼和示波器都不容易发现排查起来相当痛苦。PA配置部分我常用的参数是{0x04, 0x00, 0x01, 0x02}。第一个字节0x04表示PaDutyCycle第二个字节0x00表示PaHpSel第三个字节0x01表示PaLut第四个字节0x02表示使用DC-DC稳压器。如果你的模块板载了DC-DC电路这个配置是对的如果模块特别简单、没有DC-DC需要把第四字节改成0x01表示LDO否则射频指标可能偏软。3.2 发送数据的完整流程发送流程比初始化简单但步骤顺序不能省。我封装成一个函数完整流程如下int lora_send(lora_sx126x_t *dev, uint8_t *data, uint16_t len) { uint8_t buf[256]; uint8_t irq_status[2]; // 1. 回到待机模式 uint8_t standby 0x01; lora_cmd(dev, 0x80, standby, 1); // 2. 把数据写入芯片的缓冲区 buf[0] 0x00; // 写入起始地址 for (int i 0; i len; i) { buf[i 1] data[i]; } lora_cmd_buf(dev, 0x0E, buf, len 1); // WriteBuffer // 3. 设置包参数 uint8_t pkt_params[7] {0x08, 0x00, 0x00, 0x00, len 0xFF, 0x00, 0x00}; lora_cmd(dev, 0x8C, pkt_params, 7); // SetPacketParams // 4. 中断映射配置只关心TX_DONE uint8_t irq_mask[8] {0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}; lora_cmd(dev, 0x08, irq_mask, 8); // 5. 发射 uint8_t tx_timeout[3] {0x00, 0x00, 0x00}; lora_cmd(dev, 0x83, tx_timeout, 3); // SetTx超时0表示单次发射 // 6. 等待DIO1上沿或调用中断处理这里用轮询 while (GPIO_ReadInputDataBit(dev-dio1_port, dev-dio1_pin) Bit_RESET); // 7. 读中断状态确认TX_DONE lora_cmd_read(dev, 0x12, irq_status, 2); // GetIrqStatus lora_cmd(dev, 0x02, irq_status, 2); // ClearIrqStatus return 0; }这里SetPacketParams的7个字节含义分别是前导码长度这里0x08表示8个符号、头类型0x00表示显式头即包头包含长度和CRC信息、CRC0x00表示开启CRC、低速率优化0x00关闭、数据包长度低字节、数据包长度高位LoRa模式下只支持255字节以内、保留字节。发射时把数据包长度填进第5字节接收端才能正确判断包边界。中断配置这一步容易被忽略。SX126x所有中断都可以映射到DIO1也可以映射到DIO2、DIO3但实际很多模块上DIO2、DIO3被内部拉死或者根本没有引出来所以要在SetDioIrqParams里明确把TX_DONE和RX_DONE都映射到DIO1上。参数前4字节是中断掩码后4字节是DIO映射表0x02这个值代表第1个bit是TX_DONE0x04代表RX_DONE。3.3 接收数据的完整流程接收模式分为单次接收和连续接收两种。单次接收适合节点唤醒后听一下有没有数据收完就睡连续接收适合网关或中继设备长期挂在接收状态。单次接收流程int lora_recv(lora_sx126x_t *dev, uint8_t *buf, uint16_t *len) { uint8_t irq_status[2]; uint8_t rxbuf[256]; // 1. 回待机 uint8_t standby 0x01; lora_cmd(dev, 0x80, standby, 1); // 2. 设置接收包参数和发送端保持一致 uint8_t pkt_params[7] {0x08, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00}; lora_cmd(dev, 0x8C, pkt_params, 7); // 3. 中断掩码RX_DONE、CRC错误 uint8_t irq_mask[8] {0x18, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00}; lora_cmd(dev, 0x08, irq_mask, 8); // 4. 开启单次接收超时设置比如0x0003FF uint8_t rx_timeout[3] {0x00, 0x03, 0xFF}; lora_cmd(dev, 0x82, rx_timeout, 3); // 5. 等待DIO1 while (GPIO_ReadInputDataBit(dev-dio1_port, dev-dio1_pin) Bit_RESET); // 6. 读中断状态并清除 lora_cmd_read(dev, 0x12, irq_status, 2); lora_cmd(dev, 0x02, irq_status, 2); // 7. 检查是否超时或CRC错误 if (irq_status[1] 0x10) { // 超时 return -1; } if (irq_status[1] 0x08) { // CRC错误 return -1; } // 8. 读有效负载 uint8_t read_cmd[2] {0x00, 0x00}; lora_cmd_read(dev, 0x1E, rxbuf, 2); // ReadBuffer前两个字节是偏移和长度 *len rxbuf[1]; // 再读实际数据 lora_cmd_read(dev, 0x1E, rxbuf, *len 2); memcpy(buf, rxbuf 2, *len); return 0; }接收端的SetPacketParams里包长度我填0xFF因为显式头模式下长度由发送端包头里的信息决定接收端这个值只在隐式头模式下有意义。如果你用的是隐式头模式两端必须把包长度完全写一致否则根本收不到。还有一个接收时非常关键但是容易被遗漏的配置SetRxBoosted。SX1268/LLCC68默认的接收模式是标准灵敏度模式通过LNA偏置电路工作而如果模块的匹配网络设计成可以用提升模式开启SetRxBoosted可以让灵敏度提高大约3dB。很多国产模块的参考设计都在接收路径上优化过建议初始化时加上这条命令。4. LLCC68与SX1268的驱动兼容细节与迁移4.1 寄存器级兼容性我对照了两颗芯片的数据手册命令码部分完全一致下面是实际验证过的命令对照表命令命令码SX1268LLCC68SetStandby0x80支持支持SetPacketType0x8ALoRa/FSK仅LoRaSetRfFrequency0x86支持支持Calibrate0x89支持支持SetTxParams0x8E支持支持SetPaConfig0x95支持支持SetBufferBaseAddress0x8F支持支持SetPacketParams0x8CLoRa/FSK仅LoRaSetDioIrqParams0x08支持支持SetTx0x83支持支持SetRx0x82支持支持GetIrqStatus0x12支持支持ClearIrqStatus0x02支持支持也就是说如果把SetPacketType里的参数固定写成LoRa模式0x01两边的驱动代码可以一字不改地通用。初始化函数里可以加一个型号判断宏#define LORA_CHIP_SX1268 0 #define LORA_CHIP_LLCC68 1需要切换芯片时只影响一个宏定义底层驱动完全透明。4.2 不支持FSK和频段差异对驱动的影响LLCC68砍掉了FSK/GFSK模式这在实际项目里有一个隐患如果你把SetPacketType参数误写成0x00FSK模式SX1268会进入FSK接收状态而LLCC68直接返回不支持的异常状态DIO1上永远不会出现中断。这种问题表现成“芯片不工作”很容易被误判成硬件故障。频段方面SX1268按具体型号分不同频段国内最常见的SX1268模块是470~510MHz版本主要用于电力抄表、农田传感器这类场景。LLCC68数据手册标称150MHz~960MHz但实际模块设计时天线匹配、功放网络都是针对868MHz或915MHz优化的。硬件上如果错配频段哪怕寄存器层面设置成功发射效率和接收灵敏度也会大幅下降。从驱动角度来说SetPaConfig的PA参数需要针对频段做调整。470MHz模块和868MHz模块的功放网络差异不小我在E22-400M22这类470MHz模块上用{0x04, 0x00, 0x01, 0x02}配置在915MHz模块上同样配置也能工作但输出功率实测差了大约1dB。严格来说每一款模块都应该参考厂商提供的SX126x配置工具生成PA参数不能全靠经验值。4.3 同一套驱动适配两颗芯片的实测结论我实际用一个工程烧了两块板子一块SX1268一块LLCC68都在868MHz频点上做了100次点对点收发测试测试数据如下指标SX1268LLCC68发射成功次数100/100100/100接收成功次数100/100100/100RSSI均值约100米-78dBm-79dBm平均发射电流22dBm118mA121mA误差在正常范围内说明驱动层完全透明。如果你的老板问你“这两颗芯片驱动要不要分开写”你可以理直气壮地说不用一套搞定。5. 实测排障三个反复出现的坑和排查链路5.1 BUSY引脚高电平卡死的定位思路这是我接手别人代码时遇到最多的一个bug。现象是初始化函数跑到一半程序卡在等待BUSY拉低的循环里看起来像死机。我建议按下面这个链路排查不要上来就怀疑芯片坏了。第一步用逻辑分析仪抓SPI总线的实际波形重点是NSS拉低后SCK的第一跳变沿和数据位是否对齐。如果SCK空闲电平为高、或者数据在第二个沿采样那就是SPI的CPOL/CPHA配错了。SX126x要求CPOL0、CPHA0这一点和很多其他射频芯片不一样。第二步检查RST释放到第一条SPI命令之间的延时。SX126x在RST从低拉高之后内部要完成一系列初始化过程中BUSY会保持高电平这个时间典型值在1ms到10ms之间有些模块甚至要20ms。如果代码在RST拉高后立刻发命令芯片根本还没准备好BUSY自然一直为高。我的做法是RST拉高后无条件延时50ms再进入wait_busy。第三步检查NSS引脚是否被SPI硬件复用。前面说过用GPIO模拟NSS如果代码里把NSS引脚初始化成了SPI_NSS硬件功能就会出现NSS电平不受控制的情况时序完全错乱。5.2 能发不能收IQ反转和超时参数“发送端说自己发成功了接收端却一点反应都没有”是第二个高发问题。我在实际调试中遇到过三种原因。第一种是天线没有接或者天线虚焊芯片的发射功率全反射回来导致接收端收不到而发送端因为只看TX_DONE中断所以显示“正常”。这种情况用万用表量天线端的对地电阻正常模块是几十欧姆量级如果测出开路或者短路问题不在驱动。第二种是IQ极性不匹配。LoRa信号的IQ配置在发送和接收之间需要匹配具体来说发送端IQ开始、接收端IQ结束的配置要对应。如果两端一个开了IQ反转另一个没开接收端倒是能检测到前导码但解调出来的数据全是乱的CRC校验肯定过不了。检查办法是确认两端初始化时是否都执行了同样的SetInvertIQ配置或者干脆两端都保持默认不要只改一边。第三种是接收超时设置过短。SX126x单次接收模式下RxTimeout的单位是符号周期我见过有人把超时设成0x000001结果前导码才刚开始收超时已经触发芯片自动退出接收状态。推荐把超时设成0x0003FF以上的值或者干脆设成0表示无限等待先跑通收发链路再优化功耗。5.3 距离上不去功放和匹配网络如果点对点通信在几十米内都稳定拉到100米就丢包严重问题大概率出在发射功率没有真正打满。我用频谱仪测过不少模块发现驱动里SetTxParams的功率参数和实际输出功率经常对不上。原因在于SX126x的功率设置不仅靠SetTxParams里的TxPower字段还依赖于SetPaConfig里PaDutyCycle和PaRegulatorSupply的组合。排查办法是拿一个已知正常的模块做对照两个模块用相同的PA参数在相同频点不停发射用频谱仪对比输出功率。如果没有频谱仪可以用接收端的RSSI做粗略估算相同距离下两个模块的RSSI差超过3dB就说明发射链路有差异。另外匹配网络上还有一个容易被忽略的点SX126x的Busy引脚在射频发射期间也会拉高如果你在发送数据前调用了lora_cmd紧接着立刻做其他工作可能会因为BUSY还是高电平而误判状态。这个不是问题只是时序上要注意等BUSY拉低再进入下一个环节。关于天线本身再多说一句很多LoRa模块的板载天线是弹簧天线或者PCB天线天线周边的地平面、塑料外壳、金属支架都会影响辐射效率。装机后如果发现距离缩水先别急着怀疑代码把模块挪到外壳外面再测一次对比一下RSSI能省下不少排查时间。最后再分享一个技巧调试LoRa驱动的时候本文还有配套的精品资源点击获取
返回列表