☰
STC8G1K08驱动Si24R1无线收发实战:SPI通信与寄存器配置全解析
2026/10/14 1:21:57 网站建设 项目流程

简介:本资源是一套基于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快不少。关键是它有硬件SPI,IO口也可以灵活配置,用来驱动无线模块正好。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模式0(CPOL=0,CPHA=0)。很多人在这个地方翻车——用别的模块的驱动代码,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表示写操作。常用寄存器有CONFIG(0x00)、EN_AA(0x01)、EN_RXADDR(0x02)、SETUP_AW(0x03)、SETUP_RETR(0x04)、RF_CH(0x05)、RF_SETUP(0x06)、STATUS(0x07)、RX_ADDR_P0(0x0A)等。

CONFIG寄存器控制芯片的电源和收发模式,bit0是PWR_UP,bit1是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 0x11

2.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能读出来,但双方就是收不到对方的数据,按下面顺序排查:

  1. 地址是不是一致?两端必须使用完全相同的5字节地址。
  2. 信道RF_CH是不是一致?两端差1个信道都收不到。
  3. 数据速率和射频参数是不是一致?RF_SETUP的低两位要相同。
  4. 接收端有没有真正进入接收模式?确认CE引脚为高电平且已经延时超过130us。
  5. 天线焊接是否牢固?SMA天线有没有接好?
  6. 发送端的CE延时是否够20us?太短了数据可能根本没发出去。

如果你开着自动应答,还需要确认发送端的RX_ADDR_P0和RX地址一致,否则返回的ACK发不出去,发送端会频繁报MAX_RT。

6.3 通信距离短,功耗高

通信距离短大概率是天线问题。确认天线区域净空、匹配电路没改动。功耗高要看软件上有没有让芯片进入掉电模式。Si24R1发送完成后可以写CONFIG寄存器把PWR_UP清0,需要发送时再置1。我做过对比,这么做能省掉一半以上的待机功耗。

还有一个小技巧:如果不需要自动应答,把EN_AA设为0x00,芯片就不会在每次发送后等待ACK,功耗会下降。但代价是发送端无法知道数据有没有被对端收到,适合一主多从且数据冗余度高的场景。

6.4 一个隐藏很深的BUG:SPI速率太高

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也远比我想象的稳定。只要寄存器配置别马虎、电源别太脏、天线别乱动,这套方案跑一个低成本无线数据采集项目完全没问题。文章里这些代码和排查思路,我都是实际验证过的,你照着走一遍应该能很快跑通。如果遇到文章里没写到的问题,欢迎在评论区把现象发出来,我们一起分析。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询