SX1268单片机驱动开发:从BUSY与DIO1看懂SPI时序与收发闭环
简介SX1268芯片的STM8单片机驱动参考例程面向物联网设备开发与LoRa通信应用入门者重点解决射频参数配置、SPI收发、中断响应与低功耗切换等核心问题适合远程抄表、智慧农业、工业传感等长距离无线场景尤其适合对低功耗和通信距离要求较高的产品原型验证。压缩包共37个文件大小仅114KBrar内h/c文件构成可直接阅读的驱动源码lkf/map/s19等为IAR工程链接与烧录/调试辅助文件txt与readme说明文档则帮助快速了解工程结构。目前已有570人学习下载对刚接触SX1268的开发者有很好的参考价值。结合main.c、sx1276_7_8.c等示例代码可掌握频点、扩频因子、编码率的整定方法了解SPI读写时序与DIO中断映射并借鉴其状态机管理与抗干扰策略工程内还包含完整编译输出与调试信息便于对照分析驱动运行过程从而缩短开发周期、提升通信稳定性。1. SX1268 demo code 与单片机客户端开发的起点先看懂 BUSY 与 DIO1很多从 SX1276 迁移到 SX1268 的工程师第一次跑 LoRa 透传 demo 就卡在同一个地方SPI 能写寄存器IRQ 却迟迟不来。SX1268 与 SX1276 同属 LoRa 收发器但中断模型已经变了DIO1 负责事件映射BUSY 引脚负责命令忙状态。只对着 SX1276 的例程改引脚名一般会漏帧或者彻底丢包。client_sx126* 这个命名在嵌入式论坛里通常指“客户端单片机侧的 SX1268 demo code”和无线模块固件没有关系。它要解决的是 STM32、STC、51 这类单片机如何用 SPI 把 SX1268 驱动到发送和接收闭环。下面按最小硬件接线、SPI 命令封装、TX/RX 状态机、调试验证的顺序往下写代码可以作为裸机工程的基础。2. SX1268 单片机端接线与 SPI 时序把 client 的 BUSY 输入先接对SX1268 不是无线 MCU它是 LoRa 调制解调器。外部单片机通过 SPI 接口控制它的频率、包格式、FIFO 和收发状态。client_sx126* 源码里的“client”指的就是这颗单片机。接线阶段最常见的错误是照抄 SX1276把 DIO0 接进 EXTI然后忽略 BUSY。SX1268 没有 DIO0RF 事件统一从 DIO1 输出命令忙状态由 BUSY 引脚反馈。2.1 最小硬件连接表DIO1 进中断BUSY 必接一个能跑 demo code 的最小系统只需要这样一组信号SX1268 引脚单片机引脚举例方向作用NSS/CSPB12STM32/ P3.5STC输出SPI 片选低电平进入命令帧SCKPB13 / P3.4输出SPI 时钟MOSIPB15 / P3.6输出主发从收MISOPB14 / P3.7输入主收从发BUSYPB11 / P3.2输入命令忙状态高电平期间不能操作 SPIDIO1PB10 / P3.3输入中断事件输出NRESETPB9 / P3.1输出硬件复位低有效3.3V/GND3.3V/GND电源模块供电SX1268 模块通常带 32 MHz 晶振外部单片机不参与射频时钟所以只要把 NRESET 拉高、供电稳定芯片就会自动启动内部状态机。DIO2 和 DIO3 一般不需要接DIO3 用于 TCXO 电源控制如果模块厂商做了内部接线软件里把 TCXO 相关寄存器配好即可硬件上不用额外拉线。2.2 带超时的 WaitOnBusy而不是 while 死等BUSY 引脚在芯片上电、校准、收发切换、SPI 命令处理期间都可能拉高。demo code 里的每个 SPI 命令前后都要调用等待函数。如果 BUSY 悬空或者接错脚主控发出的命令就会被 SX1268 忽略后续读寄存器全返回 0xFF。等待函数至少要有超时参数否则芯片睡眠或损坏时整个单片机系统会卡死在死循环里。#define SX1268_BUSY_PORT GPIOB #define SX1268_BUSY_PIN GPIO_PIN_11 #define SX1268_NSS_PORT GPIOB #define SX1268_NSS_PIN GPIO_PIN_12 static void SX1268_WaitOnBusy(uint32_t timeout_ms) { uint32_t start HAL_GetTick(); while (HAL_GPIO_ReadPin(SX1268_BUSY_PORT, SX1268_BUSY_PIN) GPIO_PIN_SET) { if ((HAL_GetTick() - start) timeout_ms) { // 超时直接返回由上层打印或设置错误标志不要在中断里死等 return; } } }这段代码把等待时间限制在 100 ms 以内。参数 timeout_ms 建议第一次调试用 100业务运行后用 10。原因是 SX1268 从 Sleep 或者低频状态恢复时需要较长时间第一次上电给足 100 ms 能避免“假死”误判。等到调试稳定后再缩短可以让异常广播快速反映到总线监控上。2.3 单片机 SPI 参数Mode 0、软 NSS、先低频SX1268 的 SPI 接口同时支持 Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1大多数 client_sx126* 例程默认 Mode 0。CubeMX 里的对应关系是 CLKPolarityLOW、CLKPhase1EDGENSS 选 SOFT由普通 GPIO 手动拉低拉高。这样做的原因是软件可以在 NSS 拉高后再等一次 BUSY保证“命令结束”这个语义不被硬件自动片选机制吞掉。SPI 参数配置值理由SPI ModeMode 0与多数 demo 一致失败再切 Mode 3波特率分频64 或 128先降到 1 MHz 以下数据位8 bitSX1268 命令都是 8 bitNSSSOFTGPIO 控制插入 BUSY 等待和错误处理CubeMX 生成后的初始代码如下hspi2.Init.Mode SPI_MODE_MASTER; hspi2.Init.Direction SPI_DIRECTION_2LINES; hspi2.Init.DataSize SPI_DATASIZE_8BIT; hspi2.Init.CLKPolarity SPI_POLARITY_LOW; hspi2.Init.CLKPhase SPI_PHASE_1EDGE; hspi2.Init.NSS SPI_NSS_SOFT; hspi2.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi2);SPI 时钟不是越高越好。SX1268 在内部处理 FIFO 和状态机时NSS 上升沿之后还需要一小段时间才能准备好下一份数据。把波特率设为 8 MHz 可以减少单条命令耗时但第一次调 demo 时看不出区别反而会让相位余量变小。先用低波特率把业务跑通再逐步提高这是客户端驱动最稳的路径。3. 拆解 SX1268 的 SPI 命令封装操作码、读寄存器与 GetStatus 状态字SX1268 的 SPI 命令可以分成写命令、读命令、写 FIFO、读 FIFO 四类。所有命令都通过 NSS 低电平期间发送操作码开始NSS 高电平表示命令结束。demo code 里最容易被改坏的部分就是“读”类命令的时序很多人在读过操作码之后立刻拉高 NSS结果读回来的是垃圾数据。下面先封装两个基础函数再去配合片选控制。3.1 一条命令由两次 WaitOnBusy 包裹写命令的常见封装是先等 BUSY再拉低 NSS依次发送操作码和参数最后拉高 NSS再等一次 BUSY。第二次等待的作用是确保 SX1268 已经把参数写进内部寄存器直接发下一条命令时它可能还在忙命令会被丢弃。static uint8_t SPI_RW(uint8_t data) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi2, data, rx, 1, 10); return rx; } static void SX1268_SendCommand(uint8_t opcode, const uint8_t *param, uint8_t len) { SX1268_WaitOnBusy(100); HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_RESET); SPI_RW(opcode); for (uint8_t i 0; i len; i) { SPI_RW(param[i]); } HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_SET); SX1268_WaitOnBusy(100); }len 参数要注意上限。SX1268 的写 FIFO 命令可以带很多字节但其它命令一般不超过 8 字节。如果直接把串口缓冲区的长度传给这个函数会在意外丢字节时把后续的 RAM 内容一起发出去。更安全的是限定参数数组长度或者为 WriteBuffer 单独写一个专用函数。3.2 ReadRegister 和 GetStatus 的读时序要单帧完成读寄存器的操作码是 0x1D读命令是 0xC0。它们都不是“发了操作码再去读”那么简单而是要在同一个 NSS 低电平期间完成写地址和读 MISO 两个动作。SX1268 的 SPI 是全双工主机每写一个 0x00就会从 MISO 收到一个字节所以读一个寄存器通常要发 4 到 5 个字节。static uint8_t SX1268_ReadRegister(uint16_t addr) { uint8_t val 0; SX1268_WaitOnBusy(100); HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_RESET); SPI_RW(0x1D); /* ReadRegister */ SPI_RW((uint8_t)(addr 8)); SPI_RW((uint8_t)addr); SPI_RW(0x00); /* 空写为 MISO 腾出一个时钟周期 */ val SPI_RW(0x00); /* 真正的数据在第二个空写里返回 */ HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_SET); SX1268_WaitOnBusy(100); return val; }这里逐字节调用了 SPI_RW看起来代码量偏多但它让时序清楚NSS 低电平期间不能插入任何 GPIO 操作也不能开中断去处理串口数据。若在中间调用 HAL_Delay会破坏 SPI 帧边界。调试时可以先读 0x2AC0 寄存器很多 SX1268 模块会返回一个固定标识字节把它打印到串口就能确认 SPI 通路是通的。3.3 常用操作码速查表操作码命令参数长度用途0x80SetStandby1进入 STBY_RC/STBY_XOSC0x86SetFrequency3设置 RF 频率0x8ASetPacketType1选择 LoRa 或 FSK0x8BSetModulationParams4设置 SF/BW/CR0x8CSetPacketParams6前导码、包头、CRC0x8FSetBufferBaseAddress2设置 FIFO 偏移0x0EWriteBuffern1写入 FIFO0x1EReadBuffern2读 FIFO 状态和数据0x83SetTx3开始发射0x82SetRx3开始接收0x12GetIrqStatus0读 IRQ 状态0xC0GetStatus0读芯片模式状态参数长度中WriteBuffer 的 n1 表示“1 个偏移字节 实际数据”ReadBuffer 是“1 个偏移字节 返回状态和长度”所以实际读取的数据长度需要从 FIFO 里先读出来。GetIrqStatus 和 GetStatus 虽然没有参数但读操作要额外发一个空字节把数据从 MISO 引出来。3.4 用 GetStatus 代替固定延时做上电等待SX1268 上电后不是立刻就能接收命令。晶振启动、内部校准、TCXO 稳定这几段时间芯片的 SPI 寄存器并不可用。很多 demo code 用 HAL_Delay(10) 直接等这在同一批模块上可能勉强能跑换一批晶振启动慢一点的模块就出问题。更可靠的做法是发送 SetStandby 之后循环读 GetStatus直到状态字表示已经进入 STBY_RC。GetStatus的状态字节高 4 位是芯片模式低 4 位是最近一次命令状态。STBY_RC 对应的高 4 位值是 0x10看到 0x10 说明晶振已经稳定可以继续配置 RF 参数。这个办法比延时更有确定性也更容易在串口里判断芯片到底停在哪个环节。第 5 章会给出一个可直接用的检查函数。4. 把 SX1268 demo code 跑成收发闭环LoRa 参数、DIO1 中断和串口组帧SX1268 的收发代码层次通常是“配置参数 → 写入 FIFO → 触发发送 → 等待 DIO1 → 读回 FIFO”。client_sx126* 的 demo 一般不会把射频部分和业务部分拆得多复杂关键是把 DIO1 中断服务程序写短只做“读状态、搬数据、清标志、重新进 RX”四件事。4.1 LoRa 参数三段式配置配置 LoRa 发射参数要按 SetPacketType、SetModulationParams、SetPacketParams 的顺序来。SetPacketType 把芯片从 FSK 切换到 LoRaSetModulationParams 决定空中的扩频因子和带宽SetPacketParams 决定前导码、头模式和 CRC。顺序反了部分驱动会返回命令阻塞状态。参数项写入值含义PacketType0x01LoRaSpreadingFactor0x07SF7Bandwidth0x07125 kHzCodeRate0x01CR 4/5Preamble0x000C12 个符号HeaderType0x00可变长度头CRC0x01开启 CRC对应代码static void SX1268_LoRaConfig(void) { uint8_t p[8] {0}; p[0] 0x01; /* LoRa */ SX1268_SendCommand(0x8A, p, 1); /* SetPacketType */ p[0] 0x07; /* SF7 */ p[1] 0x07; /* BW 125 kHz */ p[2] 0x01; /* CR 4/5 */ p[3] 0x00; /* LowDataRateOptimize 关闭 */ SX1268_SendCommand(0x8B, p, 4); /* SetModulationParams */ p[0] 0x00; /* Preamble 高字节 */ p[1] 0x0C; /* Preamble 低字节 */ p[2] 0x00; /* 可变长度头 */ p[3] 0x00; /* payload length发送时再填 */ p[4] 0x01; /* CRC 打开 */ p[5] 0x00; /* InvertIQ 关闭 */ SX1268_SendCommand(0x8C, p, 6); /* SetPacketParams */ }这段代码里的 BW 编码 0x07 对应 125 kHz不是 SX1276 里的 0x07 对应 500 kHz 那套表。SX1268 的带宽编码在 125/250/500 kHz 三个常用档位上分别是 0x07、0x08、0x09移植时不要直接把 SX1276 的寄存器值搬过来。SF 和 CR 参数基本兼容但 BW 编码最容易埋坑。4.2 发送一帧SetBufferBaseAddress WriteBuffer SetTx每个写命令都由操作码、偏移字节、长度字节、数据组成。发送前先调用 SetBufferBaseAddress 把 FIFO 起点设为 0再把 payload 写到相同地址最后用 SetTx 触发发射。SetTx 的 3 个字节是超时时间设为 0 表示超时禁用芯片会一直等到发送完成或进入中断。static void SX1268_SendPacket(uint8_t *data, uint8_t len) { uint8_t tmp[3]; tmp[0] 0x00; /* TX base address */ tmp[1] 0x00; /* RX base address */ SX1268_SendCommand(0x8F, tmp, 2); /* SetBufferBaseAddress */ SX1268_WaitOnBusy(100); HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_RESET); SPI_RW(0x0E); /* WriteBuffer */ SPI_RW(0x00); /* offset */ SPI_RW(len); for (uint8_t i 0; i len; i) { SPI_RW(data[i]); } HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_SET); SX1268_WaitOnBusy(100); tmp[0] 0x00; /* 超时高字节 */ tmp[1] 0x00; tmp[2] 0x00; /* 超时禁用 */ SX1268_SendCommand(0x83, tmp, 3); /* SetTx */ }需要注意WriteBuffer 的 len 参数是实际数据长度而不是“长度-1”。SX1268 的 FIFO 地址空间是 256 字节但 demo code 一般只用到 0 到 255所以 payload 别超过 250 字节留一点余量给状态和长度字节。发送完成后 DIO1 会拉高如果发现 DIO1 没反应先检查 SetDioIrqParams 有没有把 TX_DONE 映射到 DIO1。4.3 接收路径DIO1 中断服务程序里读 IRQ_STATUS 和 FIFO接收的初始化要设置 DIO1 中断掩码为 RX_DONE然后再执行 SetRx。RX_DONE 在 IRQ_STATUS 里的位值是 0x02TX_DONE 是 0x01。中断服务程序里先读 GetIrqStatus确认 RX_DONE 后再读 FIFO最后用 ClearIrqStatus 清掉中断位。static void SX1268_OnDio1(void) { uint8_t irq[2]; uint8_t size 0; HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_RESET); SPI_RW(0x12); /* GetIrqStatus */ irq[0] SPI_RW(0x00); irq[1] SPI_RW(0x00); HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_SET); if (irq[1] 0x02) { /* RX_DONE */ HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_RESET); SPI_RW(0x1E); /* ReadBuffer */ SPI_RW(0x00); /* offset 0 */ /* 下面三个字节分别是状态、长度、数据 */ SPI_RW(0x00); size SPI_RW(0x00); for (uint8_t i 0; i size; i) { g_rx_buf[i] SPI_RW(0x00); } HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_SET); /* 清 RX_DONE再重新进入接收 */ irq[0] 0x00; irq[1] 0x02; SX1268_SendCommand(0x02, irq, 2); SX1268_EnableRx(); } }这里的读 FIFO 不再调用 SX1268_SendCommand而是把 NSS 拉低后手工收发因为读到的第二个字节是 payload 长度长度直接影响后续循环次数不能放在统一的命令封装里。SX1268 的 ReadBuffer 会先返回一个状态字节再返回长度字节最后才是数据。如果只读一个字节就去拉高 NSS会丢掉整个数据包这也是 RX 收不到完整 payload 的常见原因之一。4.4 串口到 LoRa 的组帧用空闲超时截包串口收到一帧数据后最好把“截帧”和“发送 LoRa”分开。和 Modbus 单片机帧接收数据程序的做法类似用两个字节时间的空闲间隔来判断一帧结束收到一个字节就重置超时超时归零后把缓存里的数据整体丢给 SX1268_SendPacket。这样可以避免每次串口中断都调用 SPI也能自然过滤掉 Modbus RTU 的 3.5 字符空闲间隔。static void UART_RxByte(uint8_t c) { if (g_uart_len 0) { g_uart_len 0; } if (g_uart_len sizeof(g_uart_buf)) { g_uart_buf[g_uart_len] c; } g_uart_timeout 3; /* 3 个字符时间的空闲窗口 */ } static void UART_Tick1ms(void) { if (g_uart_timeout 0) { g_uart_timeout--; if (g_uart_timeout 0 g_uart_len 0) { SX1268_SendPacket(g_uart_buf, g_uart_len); g_uart_len 0; } } }g_uart_timeout 的单位是毫秒要根据波特率调整。9600 波特率下 3 个字符时间约 3.1 ms用 3115200 波特率下 3 个字符时间不到 0.3 ms用 1 即可。这个参数不是射频参数但它决定了串口分包和 LoRa 发射的节奏配得太大会把两帧数据粘在一起配得太小会把一帧拆成两段。5. 用 GetStatus 和 IRQ_STATUS 验证 SX1268 demo 的 3 个高频坑很多 client_sx126* 移植问题不是射频参数问题而是 SPI 状态没有确认。这里给出一个可放进取样点的检查函数和三个排错方向。5.1 初始化末尾加一个状态自检static int SX1268_AssertReady(void) { uint8_t st; for (uint8_t i 0; i 20; i) { SX1268_WaitOnBusy(10); HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_RESET); SPI_RW(0xC0); /* GetStatus */ SPI_RW(0x00); st SPI_RW(0x00); HAL_GPIO_WritePin(SX1268_NSS_PORT, SX1268_NSS_PIN, GPIO_PIN_SET); if ((st 0xF0) 0x10) { /* STBY_RC芯片空闲 */ return 0; } HAL_Delay(2); } return -1; }如果返回值是 0说明 SX1268 的 32 MHz 晶振已经起来SPI 通路没有把芯片“饿死”。如果一直超时先检查 NSS 是不是被别的外设占用再检查 BUSY 引脚有没有接到单片机。5.2 用 IRQ_STATUS 而不是只数灯来定位收发问题把两块板子背靠背做回环测试时不要只盯着串口软件的数看有没有字。把收到的包里的 IRQ_STATUS 直接打印出来能区分是发送没出来还是接收校验失败。读取 IRQ_STATUS 的操作码是 0x12两个字节里的位含义如下IRQ 位位值通常原因TX_DONE0x01发送路径正常DIO1 映射已生效RX_DONE0x02收到一个完整前导码和包头HEADER_ERROR0x20包头长度或 CRC 配置不一致CRC_ERROR0x40频率偏差过大或 SNR 太低如果 RX_DONE 置位而 CRC_ERROR 也置位说明调制参数和频率基本正确只是信号质量不够如果只有 RX_DONE 没有 CRC_ERROR说明链路正常。如果 RX_DONE 都不置位问题在频率或前导码不在串口组帧。5.3 用逻辑分析仪量 BUSY 的下降沿最后一个技巧是量波形NSS 拉低后BUSY 应该很快拉高NSS 拉高后 BUSY 再降下来。若 BUSY 一直高可能是 SX1268 没有退出 Sleep若 BUSY 一直低可能是 SPI 模式配错芯片根本没进入命令处理状态。把 SCK 降到 400 kHz 重测一次如果波形正常再逐步提回高频。把上面的检查函数放到初始化末尾串口能稳定打印SX1268 ready, state0x10后再去接射频业务逻辑。这一步能快速把“单片机时序问题”和“射频链路问题”切开。本文还有配套的精品资源点击获取