
简介这是一份面向嵌入式开发与无线通信学习者的 Si4432 射频驱动源码包包含完整的发送与接收程序适用于 Sub-1GHz 窄带应用。驱动基于 STM32 平台配套工程文件齐全覆盖 SPI 配置、定时器、Flash 等底层模块便于工程移植与二次开发。包内共 254 个文件以 h 头文件、c 源文件为主另有 o 目标文件、d/crf 编译中间文件、uvproj 工程文件、hex 烧录文件等压缩包约 3.48MB目录结构便于检索。资源同时提示了 Si4463 模块的关键指标接收电流 10.7mA、灵敏度 -126dBm、最大速率 1Mbps、实测通信距离 2km并具备窄带抗干扰、四路 GPIO 等特点可帮助读者快速评估模块选型与驱动移植成本。已有 334 人学习使用适合需要开展无线传感、遥控数传或低功耗链路调试的开发者参考。1. 别急着调天线先把 si4432 驱动这三件事理顺去年做一个 433MHz 无线采集项目节点端用 MCU 裸机跑无线通信技术方案射频部分就靠这颗 Sub-1GHz 芯片。本以为找个驱动改改就完事结果栽在一个玄学问题上发送中断都进了接收端愣是一个包都收不到折腾三天才定位到是中断标志没清干净。这个项目资源就是一套完整的 si4432 驱动程序不绑定具体 MCU 型号把寄存器读写、射频参数配置、发送接收流程和中断处理封装得干干净净。适合两类人一类是板子刚画完、想把无线链路尽快调通的嵌入式工程师另一类是接手老产品需要把这颗射频芯片的驱动逻辑吃透的开发者。代码量不大但值得逐行读完再上板。2. 驱动骨架寄存器、协议、回调三层到底怎么分拿到驱动源码如果直接从头到尾硬读很容易被一堆寄存器操作绕晕。我拆这类驱动有一个习惯先看文件划分和数据结构再进具体函数。si4432 虽然是十几年前的射频方案但寄存器数量多、功能重叠中断状态、包处理、调制配置各自分散在好几个地址不分层直接写业务后面每个功能都要在寄存器层面反复打转。2.1 源码包里有哪些文件每份该看什么一份典型的 si4432 驱动源码包通常按下面这种方式组织文件文件职责看的时候重点si4432.h对外接口、状态机枚举、回调结构体声明先看这里搞清能调什么函数si4432_reg.h寄存器地址宏、常用位掩码定义遇到看不懂的地址来查si4432_core.c寄存器读写、FIFO 操作、收发流程全文重点花最多时间在这si4432_config.c射频参数配置表频点、速率、功率按项目需要改的主要位置si4432_port.cSPI、GPIO、延时等平台适配函数移植时只动这个文件我一般建议按这个顺序读先读头文件里的接口和回调定义再读 config 的配置表最后进 core 的收发实现。不要从寄存器读写函数开始读那样容易陷进细节里半天出不来。2.2 数据流怎么走从协议栈到天线只需要过三个口这套驱动把整个数据通路切成了三个清晰的层级。最上层是协议或应用层它不关心 si4432 内部怎么调制只负责把要发的数据帧准备好调用驱动暴露的发送接口中间层是驱动核心负责把帧数据写进芯片 FIFO并管理发送完成、接收完成这些事件最底层是 SPI 和 GPIO 操作把所有芯片交互收敛到几个原语函数里。发送路径上数据是这样流动的协议层拼好帧 → 驱动把帧写入 TX FIFO → 芯片自动加上前导码、同步字和 CRC → 调制后从射频引脚发出去。接收路径则相反芯片检测前导码和同步字 → 硬件剥掉协议头 → 校验通过后置起中断 → 驱动在中断里从 RX FIFO 读出净荷交给注册的回调函数。这个设计有一个好处你在驱动里几乎看不到协议解析代码。前导码、同步字、CRC 这些都由芯片内部包处理器完成驱动只做搬运和状态管理。所以业务层拿到的就是完整净荷不需要重复处理无线帧细节。2.3 回调与状态机驱动不替业务做决定核心驱动层会暴露一个回调注册接口让上层决定数据来了怎么处理typedef struct { void (*on_send_done)(void); /* 发送完成 */ void (*on_receive)(const uint8_t *data, uint8_t len, int16_t rssi); void (*on_error)(uint8_t irq_status); /* 接收错误或异常 */ } si4432_callbacks_t; void si4432_set_callbacks(const si4432_callbacks_t *cb);这几个回调在驱动中断服务函数里被调用但回调里不应该做耗时业务。常见做法是在 on_receive 里把数据拷贝到环形缓冲区置一个事件标志真正的协议处理放到主循环或任务里。这样中断处理时间短不容易丢中断。驱动内部还维护一个简单的状态机枚举大体是 IDLE、TX、RX、SLEEP 四种状态。发送和接收是两个互斥流程内部会在状态切换时做 FIFO 清空和中断标志清理避免上一次操作的残留影响下一次。之前提到“发送成功但收不到”的坑就是状态转换时中断标志没处理干净导致的。3. 核心代码拆解SPI 读写、频率计算与完整收发链路这一章是整套驱动的灵魂。si4432 所有功能都通过 SPI 寄存器访问完成物理层参数频率、速率、功率又是一组必须在初始化时精确写入的寄存器组合。我把核心代码按读写原语、频率计算、发送、接收、中断处理五块拆开每块都能直接对着源码看。3.1 SPI 底层的读写原语寄存器、FIFO 两种命令序列si4432 的 SPI 读写命令共用一个字节最高位为 1 表示读为 0 表示写低 7 位是寄存器地址。读写正常寄存器时命令字节后跟一个数据字节访问 FIFO 时命令字节后可以连续读写多个字节。uint8_t si4432_read_reg(uint8_t addr) { uint8_t val 0xFF; si4432_spi_cs(LOW); si4432_spi_transfer(0x80 | (addr 0x7F)); /* 读命令最高位置 1 */ val si4432_spi_transfer(0x00); /* 第二个字节返回寄存器值 */ si4432_spi_cs(HIGH); return val; } void si4432_write_reg(uint8_t addr, uint8_t value) { si4432_spi_cs(LOW); si4432_spi_transfer(addr 0x7F); /* 写命令最高位清 0 */ si4432_spi_transfer(value); si4432_spi_cs(HIGH); } void si4432_write_fifo(const uint8_t *data, uint8_t len) { uint8_t i; si4432_spi_cs(LOW); si4432_spi_transfer(0x7F); /* FIFO 写命令 */ for (i 0; i len; i) { si4432_spi_transfer(data[i]); } si4432_spi_cs(HIGH); } void si4432_read_fifo(uint8_t *buf, uint8_t len) { uint8_t i; si4432_spi_cs(LOW); si4432_spi_transfer(0xFF); /* FIFO 读命令 */ for (i 0; i len; i) { buf[i] si4432_spi_transfer(0x00); /* 每写一个哑字节读回一个字节 */ } si4432_spi_cs(HIGH); }读写寄存器时CS 拉低后第一个字节是命令第二个字节才是数据。读操作第二个字节也需要提供 SPI 时钟所以si4432_spi_transfer(0x00)那个 dummy 字节不能省。FIFO 读写的命令地址固定是 0x7F写用 0x7F读用 0xFF这是和普通寄存器访问最容易混淆的地方。这套驱动里还有一个关键点SPI 模式必须工作在 CPOL0、CPHA0。如果 MCU 的硬件 SPI 默认配置是 CPOL1读回来的数据全会是 0xFF后面专门讲这个坑。3.2 载波频率算出寄存器值一次移位看清 21 位拆分si4432 的载波频率由 0x08、0x09、0x0A 三个寄存器组成的 21 位频率字决定。计算公式是载波频率 30 MHz 参考时钟 / 2^19 × 频率字。反过来给定目标频点频率字 目标频率 × 2^19 / 30 MHz。#define SI4432_XTAL_FREQ_HZ 30000000UL static void si4432_set_frequency(uint32_t freq_hz) { uint32_t freq_word (freq_hz 19) / SI4432_XTAL_FREQ_HZ; /* 21 位频率字拆成三个字节0x08 高 3 位0x09 中 8 位0x0A 低 8 位 */ si4432_write_reg(0x08, (uint8_t)((freq_word 16) 0x07)); si4432_write_reg(0x09, (uint8_t)(freq_word 8)); si4432_write_reg(0x0A, (uint8_t)freq_word); }举例来说433 MHz 算出来的频率字大约在 756 万左右这个值不会超过 21 位所以写入时要对 0x08 取低 3 位高位留给其它控制位。如果项目要支持 868MHz 或 915MHz把目标频点作为参数传进来即可。频率字算完还差一组物理层参数数据速率对应 0x6D、0x6E 两个寄存器频偏对应 0x0B、0x0C接收带宽和调制方式在 0x70、0x71、0x72 这几处。常见做法是直接把一组写好的配置数组烧进 config 表初始化时循环写入不逐个手工计算。核心公式就是上面这个频率字换算这也是整套配置里唯一需要动态计算的地方。3.3 发送一条完整帧先就绪、再装弹、后点火发送流程的核心顺序是确保芯片回到 Ready 状态 → 设置包长度 → 清空 TX FIFO → 写入帧数据 → 切到发送模式 → 等待发送完成中断。这个顺序不能乱否则可能出现 FIFO 残留数据被一并发出去的情况。int si4432_send(const uint8_t *payload, uint8_t len) { si4432_set_state(SI4432_OP_READY); /* 先回 Ready清掉残留状态 */ si4432_write_reg(REG_PKT_LEN, len); /* 包长度寄存器固定包模式用 */ si4432_clear_txfifo(); si4432_write_fifo(payload, len); /* 数据写进 TX FIFO */ si4432_clear_interrupts(); /* 清干净旧中断标志 */ si4432_set_tx_mode(); /* 进入发送模式 */ return 0; }REG_PKT_LEN 对应的寄存器地址是 0x3E它告诉芯片这一包数据有多长。芯片发送时会自动在净荷前面补前导码和同步字在结尾附加 CRC这些都是硬件完成的。发送完成后驱动回到 Ready 状态等待下一次操作。如果项目需要连续发送建议在on_send_done回调里直接准备下一包数据不要让发送链路有长时间空闲。这里还要补一个超时保护发送命令下发后如果在几十毫秒内没等到发送完成中断必须主动清 FIFO、回 Ready否则芯片可能卡在发送态之后所有发送都会异常。3.4 接收路径先读中断状态再读长度最后读 FIFO接收链路比发送多一个步骤中断触发后要先读中断状态寄存器判断事件类型再读接收长度寄存器确认实际数据量最后才从 RX FIFO 把数据读出来。void si4432_isr_handler(void) { uint8_t irq1 si4432_read_reg(REG_IRQ_STATUS1); /* 0x03 */ uint8_t irq2 si4432_read_reg(REG_IRQ_STATUS2); /* 0x04 */ uint8_t len; int16_t rssi; if (irq2 IRQ_VALID_PACKET) { len si4432_read_reg(REG_RX_PKT_LEN); /* 本次收到的净荷长度 */ rssi si4432_read_reg(REG_RSSI); /* RSSI 值用于链路评估 */ si4432_read_fifo(rx_buf, len); si4432_clear_interrupts(); if (g_callbacks.on_receive) { g_callbacks.on_receive(rx_buf, len, rssi); } } }接收模式启动后芯片会持续监听前导码。每收到完整一包都会触发 VALID_PACKET 中断这时 FIFO 里存放的是去掉前导码和同步字之后的净荷。注意接收模式下如果长时间收不到包会触发 RXDONE 超时或 RSSI 阈值中断这些事件的标志也需要清理否则会一直触发无意义的中断。接收路径最常见的错误是中断触发后没有判断事件类型就直接读 FIFO。比如收到 CRC 错误包时 FIFO 内容可能不完整直接读取会拿到脏数据。正确做法是先读中断状态确认是 VALID_PACKET 再读 FIFO。3.5 中断处理与回调分发为什么超时要写在驱动里最后一小块是中断的“售后服务”。si4432 的 nIRQ 引脚高电平表示有中断事件外部中断触发沿应配置为上升沿或高电平触发。中断状态寄存器读完后事件标志不会自动清除必须向对应位写 1 才能清除。这是整套驱动里最容易被忽略、也最容易出问题的环节。void si4432_clear_interrupts(void) { /* 写 1 清标志把两个中断状态寄存器读到的值原样写回即可 */ uint8_t irq1 si4432_read_reg(REG_IRQ_STATUS1); uint8_t irq2 si4432_read_reg(REG_IRQ_STATUS2); si4432_write_reg(REG_IRQ_STATUS1, irq1); si4432_write_reg(REG_IRQ_STATUS2, irq2); }如果不清中断后果很直接nIRQ 引脚一直保持高电平MCU 外部中断被反复触发驱动以为有新事件实际只是旧事件没消化。我在项目里遇到的那个“发送成功但接收端收不到”的问题就是接收端上次接收中断没有清后续中断一直被吞掉芯片实际停在错误状态里。从那以后每次收发成功或失败第一件事就是调si4432_clear_interrupts()。4. 移植到自己的 MCU只改 4 个接口函数就能跑驱动代码千好万好接不到自己板子上等于零。好消息是这套驱动的移植成本非常低——所有平台相关操作都被收敛到一个 port 文件里只要把 4 个函数填好驱动程序就能在目标 MCU 上跑起来。4.1 引脚连接与最小硬件清单先看硬件连接。si4432 与 MCU 之间只需要 6 根线再加上天线和电源最小系统就齐了si4432 引脚方向接 MCU说明SDN输入GPIO 输出高电平芯片断电低电平工作SCLK输入SPI 时钟必须是 CPOL0 / CPHA0SDI输入SPI MOSI数据进芯片SDO输出SPI MISO数据出芯片nIRQ输出GPIO 外部中断上升沿触发中断CS输入GPIO 输出片选低有效SDN 引脚是芯片的总开关拉高之后芯片直接掉电所有寄存器会丢所以正常工作时必须拉低。nIRQ 是高有效中断这和很多无线芯片低有效中断不一样移植时别惯性接反了。4.2 port 层接口每个函数怎么实现port 文件里需要实现的核心函数其实就 4 个。我用一套统一的命名写出来uint8_t si4432_spi_transfer(uint8_t byte); /* SPI 全双工收发一个字节 */ void si4432_gpio_write(uint8_t pin, uint8_t level); /* 拉高或拉低引脚 */ void si4432_delay_ms(uint32_t ms); void si4432_delay_us(uint32_t us);SPI 函数是最重要的它同时完成发和收内部直接调 MCU 硬件 SPI 的一次收发即可。如果你的平台没有硬件 SPI也可以用 GPIO 模拟速率放在几百 kHz 以内完全没问题。GPIO 写函数需要支持 CS、SDN 这两个引脚的控制实现时用一个 pin 参数区分避免每个引脚写一个函数。延时函数用来等芯片上电和晶振稳定ms级延时建议用系统 tickus级用忙等。文件里还会暴露一个硬复位函数内部就是操作 SDN 引脚void si4432_hw_reset(void) { si4432_gpio_write(PIN_SDN, 1); si4432_delay_ms(10); si4432_gpio_write(PIN_SDN, 0); si4432_delay_ms(10); }SDN 拉高 10ms 再拉低芯片完成一次彻底掉电和上电复位。这个操作在排查异常时很有用能把芯片恢复到出厂状态。4.3 初始化到底做了什么配置装载顺序不能乱初始化函数负责把 config 表里的寄存器配置逐条写入并启动芯片void si4432_init(const uint8_t *config_table, uint8_t len) { uint8_t i; si4432_hw_reset(); /* 先硬复位 */ si4432_write_reg(REG_OPERATION_MODE, SI4432_OP_XTAL_ON); /* 开启晶振 */ /* 配置表每两个字节为一项寄存器地址 寄存器值 */ for (i 0; i len; i) { si4432_write_reg(config_table[i * 2], config_table[i * 2 1]); } si4432_clear_interrupts(); si4432_set_state(SI4432_OP_READY); }配置表的格式是一组连续的“地址 值”对长度按实际项数算。装载顺序上有讲究先开晶振再写调制参数然后写频点最后设置包处理参数。原因在于芯片内部某些寄存器写入时需要时钟已稳定顺序错了可能出现个别寄存器写不进去。初始化完成后驱动默认在 Ready 状态。应用层需要收数据就调si4432_start_rx()需要发数据就调si4432_send()。不要在 init 里直接打开接收模式除非你的业务确定上电就要听。4.4 收发验证先用两块板对测别信一块板的自测移植完成后第一轮验证我强烈建议用两块板子对测一块配置成持续发送模式另一块配置成接收模式统计正确包和错误包的比例。只看一块板子是测不出无线链路问题的。void rx_loop_test(void) { uint32_t good 0; uint32_t bad 0; si4432_start_rx(); while (1) { if (rx_done) { if (rx_crc_ok) { good; } else { bad; } /* 串口打印当前统计 */ printf(good%lu bad%lu rssi%d\r\n, good, bad, last_rssi); si4432_start_rx(); /* 收到一包后重新进入接收 */ } } }判断好坏包的依据是 CRC 校验结果。si4432 硬件会把 CRC 校验结果反映在中断状态里驱动里读出后直接判断即可。如果 bad 占比明显优先怀疑频率配置和接收带宽不要急着怀疑天线。5. 避坑与排查SPI 读全 0xFF、唤醒死机、误码率高这套驱动移植过程中最容易翻车的几个位置我按出现概率从高到低写下来。每个都是实际踩过的坑现象、原因、解决一条龙写清楚遇到类似问题可以直接照方抓药。5.1 发送端显示发送成功接收端一个包都收不到现象发送板进入发送中断IPK_SENT 标志正常置位但接收板串口没有任何输出RSSI 也读不到有效值。原因发送链路本身没问题问题几乎都出在接收侧。要么接收板初始化后没有真正进入 RX 模式要么上次接收中断状态残留芯片停在错误模式里不再监听。另一种可能接收端虽然进了 RX但中断引脚触发沿配置错误中断状态寄存器一直有值但 MCU 根本不知道驱动自然没有机会处理。解决先把接收板复位确保进入 RX 模式后 IRQ 引脚上有高电平。再用逻辑分析仪或示波器看 nIRQ 引脚在收到数据后是否拉高。如果硬件中断没触发检查外部中断配的是上升沿还是高电平。软件层面确认每次中断处理完都调用了si4432_clear_interrupts()并且在每次接收完成回调里再次调用si4432_start_rx()维持接收态。5.2 低功耗唤醒后第一次能收包第二次芯片直接死掉现象开启唤醒定时器和低功耗模式后第一次唤醒还能正常收包下一次唤醒后 SPI 读写全部无响应像是芯片彻底没电了。原因低功耗实现方式选错了。有些驱动为了方便休眠时把 SDN 引脚拉高让芯片断电唤醒时再拉低。SDN 拉高会清掉芯片全部配置唤醒后如果不重新完整初始化芯片状态是未定义的继续用旧配置直接收发必然出问题。另外如果只是让芯片进 sleep 模式而没控制唤醒等待时间晶振没稳定就操作寄存器也会出现类似假死现象。解决低功耗场景不要用 SDN 断电而是用芯片内部 sleep 模式保留配置不丢。唤醒后先延时 10ms 左右等晶振稳定再读取 0x07 操作状态寄存器确认芯片就绪最后重新清中断、进 RX。如果项目允许唤醒后重新初始化走完整 init 流程最保险代价是多耗几毫秒时间。5.3 两块板配置一样近距离也疯狂误码现象发送板和接收板用的是同一套配置表放在同一张桌子上CRC 错误包比例却高得离谱RSSI 显示信号强度足够。原因表面看配置一致实际是某个寄存器在运行过程中被覆盖了。最常见是两块板启动顺序不同某段代码在收发过程中额外写了数据速率或频偏寄存器把初始配置冲掉。另一个常见原因是接收带宽和频偏不匹配FSK 频偏设得很大接收带宽却配得很窄频谱被切掉一部分误码自然上来。解决在连接建立前两块板各跑一遍寄存器回读函数把 0x6D、0x6E数据速率、与接收带宽相关的寄存器打印出来逐项对比。如果值不一致找是谁在启动后动了这些寄存器。如果值一致但误码仍高把接收带宽放宽一档再测同时确认频偏设置和带宽设置来自同一版本的配置表。5.4 SPI 读任何寄存器都是 0xFF写什么都无效现象第一步读 0x07 操作状态寄存器就返回 0xFF所有寄存器读写像打在棉花上。原因三个方向都有可能。第一芯片没有真正上电SDN 引脚一直为高芯片处于掉电状态。第二SPI 极性配错si4432 要求 CPOL0而不少 MCU 硬件 SPI 默认 CPOL1数据采在错误的时钟沿上。第三SDOMISO引脚没有被配置成输入模式读回来的电平一直是高。解决按顺序排查。先用万用表量 SDN 引脚电压确保为低再把 SPI 配置明确写成 CPOL0、CPHA0最后检查 SDO 引脚方向。如果这三步都正常还读 0xFF用 GPIO 模拟 SPI 只读一个寄存器能排除硬件 SPI 控制器配置问题。最后给一个排查顺序清单芯片供电和 SDN → SPI 模式 → 读 0x07 看返回值 → 中断引脚电平 → 收发状态切换。按这个顺序走大多数问题能在十分钟内定位别一上来就怀疑天线。6. 二次开发改造把固定配置表升级成多频段 profile如果你要在这个驱动基础上做产品第一个建议的改造点是把配置表从“固定写死”变成“运行期可选”。很多项目初期只跑 433MHz写着写着要支持 868MHz 或 915MHz如果初始化代码里全是一大批魔数换频段就要改源码重新编译测试效率非常低。6.1 引入 profile 结构体把频点、速率、功率打包改造思路是定义一个radio_profile_t结构体把一组物理层参数装进去运行时按需切换typedef struct { uint32_t carrier_hz; /* 目标载波频点 */ uint8_t rate_reg[2]; /* 数据速率寄存器配置 */ uint8_t deviation_reg; /* 频偏配置 */ uint8_t power_cfg; /* 输出功率档位 */ } radio_profile_t; static const radio_profile_t profile_433 { 433000000UL, { 0x03, 0x1C }, 0x10, 0x20 }; static const radio_profile_t profile_868 { 868000000UL, { 0x03, 0x1C }, 0x10, 0x20 }; void radio_switch_profile(const radio_profile_t *p) { si4432_set_state(SI4432_OP_READY); si4432_set_frequency(p-carrier_hz); si4432_write_reg(0x6D, p-rate_reg[0]); /* 速率高字节 */ si4432_write_reg(0x6E, p-rate_reg[1]); /* 速率低字节 */ si4432_write_reg(0x0D, p-power_cfg); /* 输出功率 */ si4432_clear_interrupts(); }这样改造之后收发流程代码完全不用动切换频段只是换一个结构体指针。如果产品还有不同区域法规的频段需求可以把 profile 做成一张表按区域索引加载代码结构更清晰。另一个值得改的地方是把on_receive回调接到协议栈上。市面上大部分轻量协议栈都有独立的接收到上层接口改造时把回调里拿到的数据先拷贝进协议栈的接收缓冲再置事件标志而不是在回调里做完整协议解析。驱动保持薄协议逻辑保持厚这两层职责分开后后面加加密、加地址过滤、加重传都方便不少。这个改造让我后续新项目都先写 profile 再写流程配置和逻辑彻底分开。遇到测试不过时直接导出寄存器表逐行对比省掉大量玄学排查时间。从那以后我移植这套驱动都强制把自己的改动圈在 port 层和 profile 层核心 FIFO 收发代码一行不动。希望帮到你。本文还有配套的精品资源点击获取