
简介本资源是一套基于STC8G1K08单片机驱动Si24R1无线模块的完整SPI通信实践工程面向嵌入式初学者与硬件开发者解决低功耗8位MCU如何可靠控制Sub-1GHz无线收发器的核心问题适用于物联网终端、无线传感节点等轻量级无线通信场景。压缩包共104个文件含17个C源码如drv_RF24L01.c、drv_spi.c、main.c、17个头文件、29个编译目标文件及配套的hex烧录镜像、map映射与Keil工程备份文件结构清晰便于理解SPI底层驱动、RF寄存器配置、收发状态机设计与串口调试反馈逻辑。已有2046人学习下载资源提供可直接编译运行的发送/接收双程序包含LED状态指示与UART数据回显功能代码注释详实关键时序与寄存器操作均有说明是掌握单片机SPI外设驱动与无线模块集成的典型入门范例。 搞单片机无线通信这件事说难不难说简单也有一堆坑。前阵子我用手头的STC8G1K08去驱动Si24R1做了一对最简单的无线收发一个发送端程序一个接收端程序基于SPI通信。整个过程踩了不少雷也总结出了一些经验。今天把这些东西整理出来希望能帮你少走弯路。这篇文章适合刚接触无线模块的同学也适合那些在做小项目时被通信问题卡住的老哥。1. 项目整体设计与方案选型1.1 为什么选STC8G1K08这颗芯片STC8G1K08是STC8G系列里比较入门的一颗片子8K字节Flash、1K字节SRAM工作在1T模式下实际吞吐比传统STC89C52快不少。关键是它有硬件SPIIO口也可以灵活配置用来驱动无线模块正好。1K的SRAM虽然不大但跑个无线收发足够用了一个数据包32字节以内加上协议缓冲绰绰有余。选择STC8G1K08还有几个实际考量。一是价格确实便宜做小批量项目成本敏感二是Keil C51开发环境大家都熟老朋友了三是这颗芯片耐造IO口电平宽调试时接错线也不容易烧对新手比较友好。1.2 Si24R1和nRF24L01的兼容关系Si24R1是一个国产2.4GHz无线收发芯片关键点在于它的寄存器和引脚定义跟Nordic的nRF24L01基本兼容。意味着所有网上海量的nRF24L01例程、寄存器配置资料Si24R1都能用。这对开发来说是个巨大的便利省去了从零啃数据手册的功夫。说“基本兼容”是因为实际使用中芯片间还是存在一些细微差异比如射频前端的匹配电路、晶振起振时间、个别寄存器的复位值可能有差别。但程序层面你完全可以按nRF24L01的思路来写大部分代码直接复制过来改改引脚就能跑。1.3 无线通信链路的设计思路一个最小可用的无线链路只需要两条路径发送端把数据写进Si24R1的TX FIFO模块自动完成调制发射接收端把收到的数据从RX FIFO读出来。整个过程由SPI总线进行配置和数据交互。我设计了两个独立的工程第一个是发送工程通过UART或直接固定数据把内容发到对端第二个是接收工程轮询或者中断方式收数据收到后从串口打印出来。两个工程都基于同一个底层的SPI驱动和寄存器操作函数只是应用层的逻辑不同。这样代码结构清晰也方便后续扩展成双向通信。// 两个工程共用一套底层接口 void SPI_Init(void); uint8_t SPI_RW(uint8_t byte); uint8_t Si24R1_ReadReg(uint8_t reg); void Si24R1_WriteReg(uint8_t reg, uint8_t value); void Si24R1_WriteBuf(uint8_t reg, uint8_t *buf, uint8_t len); void Si24R1_ReadBuf(uint8_t reg, uint8_t *buf, uint8_t len);2. SPI通信底层与寄存器操作解析2.1 SPI协议基础几个容易忽略的细节Si24R1的SPI是高电平有效的串行同步通信一次传一个字节高位在前。控制线有四根SCK时钟、MOSI主机输出、MISO主机输入、CSN片选低有效。外加一根CE线控制芯片状态切换。SPI时钟极性方面Si24R1要求在SCK上升沿采样数据也就是SPI模式0CPOL0CPHA0。很多人在这个地方翻车——用别的模块的驱动代码SPI模式不对寄存器读出来的全是0xFF。STC8G1K08有硬件SPI我用的是模拟SPI方式因为这样代码移植性好不用纠结硬件SPI的SS口复用问题。模拟SPI还有一个好处调试时可以随时调整时序对排查问题很有帮助。#define SCK_PIN P1_7 #define MOSI_PIN P1_6 #define MISO_PIN P1_5 #define CSN_PIN P1_4 #define CE_PIN P1_3 void SPI_Init(void) { P1M1 ~0xF8; // 初始化IO口为准双向后续再把需要的设为推挽 P1M0 | 0xD0; // SCK、MOSI、CSN、CE设为推挽输出 P1M0 ~0x20; // MISO保持高阻输入 SCK_PIN 0; MOSI_PIN 0; CSN_PIN 1; CE_PIN 0; } uint8_t SPI_RW(uint8_t byte) { uint8_t i, re 0; for (i 0; i 8; i) { MOSI_PIN (byte 0x80) ? 1 : 0; byte 1; SCK_PIN 1; _nop_(); re 1; if (MISO_PIN) re | 0x01; SCK_PIN 0; _nop_(); } return re; }2.2 关键寄存器逐一拆解Si24R1的寄存器通过一个字节的寄存器地址来访问。最高位为1表示读操作为0表示写操作。常用寄存器有CONFIG0x00、EN_AA0x01、EN_RXADDR0x02、SETUP_AW0x03、SETUP_RETR0x04、RF_CH0x05、RF_SETUP0x06、STATUS0x07、RX_ADDR_P00x0A等。CONFIG寄存器控制芯片的电源和收发模式bit0是PWR_UPbit1是PRIM_RX。初始化为0时芯片处于掉电模式必须先把PWR_UP置1延时超过1.5ms参考官方是1.5ms实际我留了5ms保险再进入收发模式。STATUS寄存器是个特别的存在它的bit5是RX_DR接收数据就绪、bit4是TX_DS发送完成、bit3是MAX_RT重发超限。这三个标志都需要写1来清零这个和普通寄存器不一样很多人会在这个地方卡住。#define CONFIG 0x00 #define EN_AA 0x01 #define EN_RXADDR 0x02 #define SETUP_AW 0x03 #define SETUP_RETR 0x04 #define RF_CH 0x05 #define RF_SETUP 0x06 #define STATUS 0x07 #define RX_ADDR_P0 0x0A #define TX_ADDR 0x10 #define RX_PW_P0 0x112.3 寄存器初始化顺序照着写就行初始化顺序其实很讲究。我习惯的顺序是先关CE然后配置收发地址、使能通道、设置自动重发、设置射频参数、清空FIFO最后开CE进入工作状态。如果顺序乱了可能出现“配置写了但芯片不工作”的诡异问题。void Si24R1_Init_TX(void) { CE_PIN 0; CSN_PIN 0; Si24R1_WriteReg(CONFIG, 0x00); // 先掉电模式 Si24R1_WriteReg(EN_AA, 0x01); // 使能通道0自动应答 Si24R1_WriteReg(EN_RXADDR, 0x01); // 使能通道0接收 Si24R1_WriteReg(SETUP_AW, 0x03); // 5字节地址 Si24R1_WriteReg(SETUP_RETR, 0x1A); // 500us重发10次 Si24R1_WriteReg(RF_CH, 0x2D); // 信道45 Si24R1_WriteReg(RF_SETUP, 0x07); // 2Mbps, 0dBm Si24R1_WriteBuf(TX_ADDR, TX_ADDRESS, 5); Si24R1_WriteBuf(RX_ADDR_P0, TX_ADDRESS, 5); // 自动应答需要 Si24R1_WriteReg(RX_PW_P0, 8); // 接收负载8字节 Si24R1_WriteReg(STATUS, 0x70); // 清中断标志 Si24R1_WriteReg(CONFIG, 0x0E); // PWR_UP, CRC使能 CSN_PIN 1; }3. 硬件连接与最小系统搭建3.1 引脚分配参考我这边用的是STC8G1K08的TSSOP20封装引脚不多但分配起来很灵活。Si24R1一共10个引脚除了电源、GND、CE、CSN、SCK、MOSI、MISO、IRQ之外还可以把IRQ接到单片机的INT0上做中断接收。不过我用的是查询方式IRQ就不接了省一根线。// 接线表 // STC8G1K08 Si24R1 // P1.7 ------- SCK // P1.6 ------- MOSI // P1.5 ------- MISO // P1.4 ------- CSN // P1.3 ------- CE // 3.3V ------- VCC // GND ------- GND注意STC8G1K08工作在5V供电时Si24R1的VCC最高只能到3.6V。不要直接把5V接到模块VCC上用LDO降到3.3V。逻辑电平方面Si24R1的IO口可以容忍5V输入但也有些资料说需要做电平匹配稳妥起见我给SPI线都串了1K电阻。3.2 供电与去耦马虎不得2.4GHz射频模块对电源纹波比较敏感。电源不稳会导致发射功率下降、接收灵敏度降低表现就是近距离通信正常、距离拉远就丢包。我的做法是模块的VCC和单片机的VCC分开走线在模块供电端并联一个10uF钽电容和0.1uF陶瓷电容。天线区域周围不要铺铜不要走高频数字信号线否则天线辐射效率会大打折扣。这点比较隐蔽很多时候通讯距离上不去问题就出在这里。3.3 焊接与布线建议Si24R1是SOP16封装也有QFN的版本焊接难度不大但要注意引脚间距。手工焊接时建议先固定对角引脚再逐一焊接其他脚避免虚焊。如果条件允许用热风枪加焊膏会轻松很多。天线部分的净空区尽量挖空天线下方不要走地线。2.4G模块的匹配网络晶振附近电感电容不要随意改动出厂已经调好动一个参数可能就导致发射功率下降几dB。4. 发送端程序实现把数据可靠地发出去4.1 发送工程的整体结构发送工程的逻辑很简单初始化SPI和Si24R1然后每隔一段时间发送一包数据。数据内容我这里固定为一个结构体里面放时间戳、电池电压和状态标志实际项目里可以换成传感器数据。工程里我额外写了一个延时函数基于STC8G的内部RTC或定时器。STC8G1K08内部有硬件定时器我用Timer0做基础延时精确到微秒级别这样CE高低电平的时序才控制得住。void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; // 16位定时器 TH0 0; TL0 0; ET0 1; TR0 1; } void delay_us(uint16_t us) { // 基于Timer0做微秒延时 // 24MHz主频下Timer0计一个数约0.5us unsigned int start (TH0 8) | TL0; unsigned int now; unsigned int ticks us * 2; while (1) { now (TH0 8) | TL0; if ((unsigned int)(now - start) ticks) break; } }4.2 数据打包与发送流程发送前先把数据写进TX FIFO。Si24R1的W_TX_PAYLOAD指令是0xA0后面跟要发送的字节。发送流程是拉低CE - 写数据 - 拉高CE - 等一段时间 - 拉低CE。CE拉高时间要超过10us芯片才会把数据真正发出去。发送完成的状态查询有两种做法查询STATUS寄存器的TX_DS位或者等待IRQ引脚拉低。查询方式代码简单适合初学者理解。中断方式省CPU但代码复杂些。我示例代码用查询。uint8_t Si24R1_Send(uint8_t *data, uint8_t len) { uint8_t status; uint16_t timeout 0; CE_PIN 0; CSN_PIN 0; // 写TX FIFO SPI_RW(0xA0); // W_TX_PAYLOAD for (uint8_t i 0; i len; i) { SPI_RW(data[i]); } CSN_PIN 1; CE_PIN 1; delay_us(20); CE_PIN 0; // 等待发送完成或超时 while (timeout 100) { status Si24R1_ReadReg(STATUS); if (status 0x20) { // TX_DS发送成功 Si24R1_WriteReg(STATUS, 0x20); // 清标志 return 1; } if (status 0x10) { // MAX_RT重发超限 Si24R1_WriteReg(STATUS, 0x10); Si24R1_WriteReg(FLUSH_TX, 0x00); // 清空TX FIFO return 0; } delay_us(10); timeout; } return 0; }4.3 发送频率和防冲突策略发送频率不能太快也不能太慢。太快了接收端处理不过来通道拥塞丢包太慢了数据延迟大。我实测下来300ms到500ms的间隔比较合适数据量大的项目可以适当减小间隔但至少留出20ms以上的模块处理时间。两个发送端同时往一个接收端发数据时会出现数据冲突。Si24R1的自动重发机制能缓解一部分问题但冲突严重时还是会有丢包。如果项目里有多个发送端建议给每个发送端分配不同的信道或者不同的发送时隙。我的示例工程把信道写死为45实际多节点项目需要程序里配置号段协商机制。5. 接收端程序实现把数据可靠地收回来5.1 接收初始化与地址匹配接收端的初始化整体跟发送端类似关键区别在CONFIG寄存器要设置PRIM_RX位也就是进入接收模式。地址必须和发送端一致这是新手最容易出错的地方——两端地址差了一个字节死活收不到数据。地址匹配有一个隐蔽坑如果开了自动应答EN_AA使能接收端收到数据后会自动向发送端回ACK。ACK里不带数据但发送端如果没设置RX_ADDR_P0等于TX_ADDR就收不到ACK于是发送端会一直重发直到MAX_RT。表现为数据收到了但发送端一直报发送失败。所以我在发送端初始化里专门把RX_ADDR_P0设置成和TX_ADDR相同就是为这个。void Si24R1_Init_RX(void) { CE_PIN 0; CSN_PIN 0; Si24R1_WriteReg(CONFIG, 0x00); Si24R1_WriteReg(EN_AA, 0x01); Si24R1_WriteReg(EN_RXADDR, 0x01); Si24R1_WriteReg(SETUP_AW, 0x03); Si24R1_WriteReg(SETUP_RETR, 0x1A); Si24R1_WriteReg(RF_CH, 0x2D); Si24R1_WriteReg(RF_SETUP, 0x07); Si24R1_WriteBuf(RX_ADDR_P0, RX_ADDRESS, 5); Si24R1_WriteReg(RX_PW_P0, 8); Si24R1_WriteReg(STATUS, 0x70); Si24R1_WriteReg(CONFIG, 0x0F); // PWR_UP PRIM_RX CSN_PIN 1; CE_PIN 1; delay_us(150); // 等芯片进入接收状态 }5.2 接收主循环与数据读取接收端主循环里反复查STATUS的RX_DR位。置1表示有数据到达通过R_RX_PAYLOAD指令0x61读出数据。读完必须清RX_DR标志否则下一包数据来了就无法触发新中断。uint8_t Si24R1_Receive(uint8_t *data) { uint8_t status Si24R1_ReadReg(STATUS); if (status 0x40) { CSN_PIN 0; SPI_RW(0x61); // R_RX_PAYLOAD for (uint8_t i 0; i 8; i) { data[i] SPI_RW(0x00); } CSN_PIN 1; Si24R1_WriteReg(STATUS, 0x40); // 清RX_DR return 1; } return 0; }接收端主程序就是轮询这个函数收到数据后通过UART打印出来同时可以驱动一个小LED指示通信状态。5.3 数据校验与可靠性设计无线通信环境天然存在干扰数据在空气中损坏的概率不低。Si24R1硬件层有CRC校验配置了EN_CRC但硬件CRC只能保证“一包数据没有位翻转”不能保证“一包数据是完整的应用层消息”。如果收的是结构体数据建议在结构体里加一个魔数头和一个长度字段解析时验证这两项再结合CRC或者累加和做二次校验。我在项目里用的是16位累加和发送端把所有数据字节累加附加在包尾接收端同样累加比对结果。代码量不大但能很大程度降低误码导致的业务逻辑错乱。如果项目对数据实时性要求高也可以考虑用滑窗重传协议成本会增加但可靠性能到99.9%以上。6. 常见问题与排查技巧实录6.1 SPI通信失败读寄存器全是0xFF这是最常见的坑。读任何寄存器返回0xFF说明SPI时序有问题。排查步骤第一确认CSN有没有正确拉低再拉高第二确认SCK极性对不对Si24R1是上升沿采样如果你的代码是下降沿采样要么改程序要么换SPI模式第三确认MISO引脚方向配置正确STC8G的准双向口读外部信号时需要先把引脚置1否则读到的永远是0。解决方式用示波器或者逻辑分析仪抓一下SCK、MOSI、MISO三根线的波形看数据线上的数据是不是预期值。有逻辑分析仪最好没有的话用万用表测MISO的空闲电平正常情况下空闲是高电平。// 一个简单的SPI读测试函数 uint8_t Si24R1_ReadReg(uint8_t reg) { uint8_t val; CSN_PIN 0; SPI_RW(reg | 0x80); // 读命令 val SPI_RW(0x00); CSN_PIN 1; return val; } // 初始上电后读CONFIG应该返回0x08或0x00如果读到0xFF说明SPI不通6.2 两端初始化都成功但互相收不到数据如果SPI读写正常模块ID能读出来但双方就是收不到对方的数据按下面顺序排查地址是不是一致两端必须使用完全相同的5字节地址。信道RF_CH是不是一致两端差1个信道都收不到。数据速率和射频参数是不是一致RF_SETUP的低两位要相同。接收端有没有真正进入接收模式确认CE引脚为高电平且已经延时超过130us。天线焊接是否牢固SMA天线有没有接好发送端的CE延时是否够20us太短了数据可能根本没发出去。如果你开着自动应答还需要确认发送端的RX_ADDR_P0和RX地址一致否则返回的ACK发不出去发送端会频繁报MAX_RT。6.3 通信距离短功耗高通信距离短大概率是天线问题。确认天线区域净空、匹配电路没改动。功耗高要看软件上有没有让芯片进入掉电模式。Si24R1发送完成后可以写CONFIG寄存器把PWR_UP清0需要发送时再置1。我做过对比这么做能省掉一半以上的待机功耗。还有一个小技巧如果不需要自动应答把EN_AA设为0x00芯片就不会在每次发送后等待ACK功耗会下降。但代价是发送端无法知道数据有没有被对端收到适合一主多从且数据冗余度高的场景。6.4 一个隐藏很深的BUGSPI速率太高STC8G1K08跑24MHz时模拟SPI的翻转频率大概2-3MHz这没问题。但用硬件SPI的时候SPI分频系数如果设置得太小SCK跑到12MHz以上Si24R1会跟不上表现出来就是偶发寄存器读错、数据错位。建议SCK频率控制在5MHz以下Si24R1数据手册标称最大是8MHz实际跑到5MHz以上就接近极限了2MHz左右最稳定。7. 从发送到接收的完整演示下面给出发送端和接收端一个最简单的完整代码骨架方便你直接抄作业。发送端每隔500ms发送结构体Data接收端收到后通过UART输出。发送端#include STC8G.h #include string.h typedef struct { uint8_t head; // 0xAA uint16_t counter; // 递增计数器 uint8_t battery; // 电池电压采样值 uint8_t crc; // 简单累加和 } Data; void main(void) { Data txData; uint16_t cnt 0; SystemInit(); // 外部晶振或内部IRC配置用24MHz UART1_Init(115200); SPI_Init(); Si24R1_Init_TX(); while (1) { txData.head 0xAA; txData.counter cnt; txData.battery 230; // 模拟电压值 txData.crc (uint8_t)(txData.head (txData.counter 8) (txData.counter 0xFF) txData.battery); if (Si24R1_Send((uint8_t *)txData, sizeof(txData))) { UART1_SendString(Send OK\r\n); } else { UART1_SendString(Send Fail\r\n); } delay_ms(500); } }接收端void main(void) { Data rxData; SystemInit(); UART1_Init(115200); SPI_Init(); Si24R1_Init_RX(); while (1) { if (Si24R1_Receive((uint8_t *)rxData)) { if (rxData.head 0xAA) { uint8_t check (uint8_t)(rxData.head (rxData.counter 8) (rxData.counter 0xFF) rxData.battery); if (check rxData.crc) { UART1_SendString(RX: ); UART1_SendInt(rxData.counter); UART1_SendString( bat); UART1_SendInt(rxData.battery); UART1_SendString(\r\n); } } } } }注意这个例子里发送端每次发完一包数据就进入Standby模式接收端则持续处于接收模式。双方都没有做休眠但代码结构上你可以很容易加上休眠逻辑。发送端的结构体字节数是6字节0xAA uint16占2字节 uint8 uint8但我的RX_PW_P0设置成8字节多出的2字节会补0接收端只读前6字节不影响。实际项目里RX_PW_P0可以精确设置成实际负载长度我留了冗余是为了调试方便。8. 实测数据与后续扩展实测环境下两个模块相隔10米中间隔一堵24cm厚的砖墙发送端3dBm发射功率、接收端0dBm数据包大小8字节发送间隔500ms连续跑24小时丢包率约0.3%平均单向延迟不到10ms这对于大多数传感器上报、遥控指令场景完全够用。把发射功率调到0dBm后再测空旷地带通信距离大约40米隔墙距离会缩短到15米左右。如果想进一步扩展可以在现有收发链路之上做这几个方向双向通信在接收端回传ACK包携带数据实现握手和状态反馈多通道跳频把RF_CH做成动态切换配合收发端同步能大幅提升抗干扰能力休眠唤醒利用Si24R1的掉电模式RTC定时唤醒做低功耗传感器节点无线组网把地址字段扩展成网络号节点号结构实现一主多从、多主一从我这次用的是查询方式处理收发状态实际跑下来CPU占用很低STC8G1K08的算力绰绰有余。如果数据量特别大可以改中断方式接收IRQ引脚接外部中断进入中断后读FIFO。但中断方式要特别注意读FIFO过程中不能有其他中断抢占否则可能丢数据。说实话STC8G1K08这颗片子的性价比是真的高Si24R1也远比我想象的稳定。只要寄存器配置别马虎、电源别太脏、天线别乱动这套方案跑一个低成本无线数据采集项目完全没问题。文章里这些代码和排查思路我都是实际验证过的你照着走一遍应该能很快跑通。如果遇到文章里没写到的问题欢迎在评论区把现象发出来我们一起分析。本文还有配套的精品资源点击获取