简介:CI24R1单片机收发例程是一份面向单片机开发者的无线通信实战代码包,围绕STC8H系列单片机与CI24R1模块的串口通信,讲解NOACK无应答模式下的数据收发流程,适合需要快速上手2.4GHz无线数据传输的嵌入式初学者或项目工程师。压缩包内共220个文件,主要包含68个h头文件与50个c源文件,另有lst、obj、lib、asm等编译中间文件与工程配置,可帮助开发者从源码层面理解初始化、数据包构造、串口中断接收及CRC校验等关键环节,整体约980KB。已有329人学习该资源。例程附带的工程文件可直接在开发环境中加载调试,代码注释清晰,便于对照模块技术手册进行二次开发,也可作为多点通信、功率控制等进阶功能的基础参考。 做无线遥控、数据采集这类项目,最烦的就是通信这块动不动就不通。之前我一直用nRF24L01,芯片是好用,可价格涨得离谱,货期还没谱。后来朋友推荐了国产的CI24R1,说是和nRF24L01引脚兼容、寄存器兼容,改几行代码就能替换,价格却便宜一大截。当时我半信半疑,结果实测下来确实能直接平替,收发稳定度也够用。如果你手头正好有CI24R1模块,或者正纠结选型,这篇就围绕CI24R1单片机收发例程,把硬件连接、寄存器配置、收发逻辑和调试过程中踩过的坑一次性讲清楚。
这套方案适合谁?搞51单片机、STM32做无线透传的,做遥控小车、无线温湿度、智能家居节点,或者毕业设计需要低成本2.4G通信的,可以参考。下面内容以51单片机为例展开,但核心寄存器操作和收发时序在STM32上同样适用,直接平移就行。
1. CI24R1核心特性与方案选型
1.1 为什么选CI24R1而不是nRF24L01
先说结论:CI24R1是一个2.4GHz频段的无线收发芯片,GFSK调制,内部集成链路层协议,支持1Mbps和2Mbps两种空中速率,125个可配置频道,发射功率四档可调。最关键的一点,它的引脚和寄存器映射与nRF24L01+基本一致,所以市面上大量现成的nRF24L01驱动代码拿过来,改一改引脚宏定义就能跑。
价格才是真正的杀手锏。同样在立创商城搜,nRF24L01+模组普遍十几块起步,CI24R1模组几块钱就能拿下。而且CI24R1的PA版本,也就是标着CI24R1-PA的模块,发射功率能做到20dBm左右,空旷环境下传个几百米没太大压力,这对做遥控、农业数据采集这类场景非常实用。
除了成本优势,CI24R1的电源适应性也不错,工作电压2.0V到3.6V都能正常工作,不过需要注意的是,逻辑电平最好是3.3V。51单片机通常是5V供电,所以硬件上必须加电平匹配,不然SPI通信大概率会有偶发乱码。
1.2 芯片内部结构和工作原理
CI24R1内部大致可以分成三个部分:射频前端、基带控制逻辑、SPI寄存器接口。
射频前端负责把数据调制到2.4GHz载波上发出去,同时把接收到的射频信号解调成数字基带信号;基带控制逻辑负责处理地址匹配、CRC校验、自动应答、重发这些链路层协议;SPI寄存器接口则负责和单片机通信,单片机通过读写寄存器来控制芯片的工作模式、配置收发参数。
实际使用中你需要关心的核心寄存器不多,大致就是配置寄存器、状态寄存器、收发地址寄存器、频道寄存器、射频参数寄存器这几个。想要进入发送模式,核心就是两个动作:把PRIM_RX位清零表示发送模式,然后拉高CE引脚启动发送;想要进入接收模式,就是把PRIM_RX置1,同样拉高CE进入接收状态。
整个工作流程可以类比成一个对讲机:你和对方约定好同一个频道、同一个组呼ID,按下发送键讲话,松开就等对方回话。频道对应RF_CH寄存器,组呼ID就是收发地址,CE引脚就相当于对讲机的PTT按键。
2. 硬件连接与最小系统搭建
2.1 51单片机与CI24R1接线方案
CI24R1通信接口是4线SPI,外加CE和IRQ两个控制引脚,一共6根线,接线关系非常固定。
| 单片机引脚 | CI24R1模块引脚 | 说明 |
|---|---|---|
| P1.0 | CSN | 片选,低电平有效 |
| P1.1 | SCK | SPI时钟 |
| P1.2 | MOSI | 主机输出从机输入 |
| P1.3 | MISO | 从机输出主机输入 |
| P1.4 | CE | 收发控制,高电平启动 |
| P1.5 | IRQ | 中断输出,低电平有效 |
我用的51单片机是STC89C52RC,3.3V和5V同时存在。CI24R1模块VCC接3.3V,但SPI引脚直连5V单片机会有风险。稳妥的做法是MISO引脚加一个10kΩ电阻上拉到3.3V,MOSI、SCK、CSN、CE这四个由单片机输出到芯片输入的引脚,串一个1kΩ电阻再连过去,实测跑20cm排线、SPI时钟设到1MHz以下完全没问题。
如果用STM32就直接多了,全部引脚配置为复用推挽输出,STM32F103的引脚耐压是5V容忍的,接线省心不少。
2.2 供电和去耦的细节
CI24R1这种2.4G射频芯片对电源纹波比较敏感,发送瞬间电流能达到几十毫安,供电没处理好会出现发送成功率低、接收灵敏度差等诡异问题。
我习惯在模块的VCC和GND之间并两个电容:一个10μF电解电容,一个0.1μF瓷片电容。电解电容负责稳住瞬间大电流,瓷片电容负责滤掉高频噪声。另外,模块的地线要尽量短,最好是在PCB或面包板上单独拉一条模拟地,和单片机数字地分开走,最后在电源入口处单点汇合。
如果你用的是那种带PA的CI24R1模块,注意天线区域周围不要走线,更不要有铜皮,不然阻抗匹配被破坏,发射功率再高也发射不出去。
3. 软件架构与核心代码实现
3.1 SPI底层通信封装
CI24R1的SPI时序定义为:片选CSN拉低后,SCK上升沿写入数据,下降沿读取数据,一次写操作最多可以连续读写5个字节。和常见的存储芯片SPI时序有些差别,不能用那种"上升沿锁存"的固定模板直接套,需要对照数据手册的时序图写。
下面是51单片机GPIO模拟SPI的代码,我用的是STC89C52,主频12MHz,SPI速率设置到几百kHz,实测足够稳定:
// 引脚定义 sbit CE = P1^4; sbit CSN = P1^0; sbit SCK = P1^1; sbit MOSI = P1^2; sbit MISO = P1^3; // SPI写一个字节 void SPI_WriteByte(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { SCK = 0; if (dat & 0x80) MOSI = 1; else MOSI = 0; SCK = 1; dat <<= 1; } SCK = 0; } // SPI读一个字节 uint8_t SPI_ReadByte(void) { uint8_t i, dat = 0; for (i = 0; i < 8; i++) { SCK = 0; SCK = 1; dat <<= 1; if (MISO) dat |= 0x01; } SCK = 0; return dat; } // 读写寄存器 uint8_t SPI_ReadReg(uint8_t reg) { uint8_t val; CSN = 0; SPI_WriteByte(reg & 0x1F); // R_REGISTER命令 val = SPI_ReadByte(); CSN = 1; return val; } void SPI_WriteReg(uint8_t reg, uint8_t val) { CSN = 0; SPI_WriteByte((0x20) | (reg & 0x1F)); // W_REGISTER命令 SPI_WriteByte(val); CSN = 1; }这里有个容易搞混的点:读写寄存器命令的最高位不一样。读寄存器是0x00开头,写寄存器是0x20开头,也就是最高位为1。如果这个命令字写错了,后面所有操作全是无效的。初始化时可以先读一遍CONFIG寄存器,看看能不能读到默认值0x08,这是检验SPI时序是否正确的最快方法。
3.2 芯片初始化配置流程
初始化流程一般是:检查SPI通信正常、写收发地址、配置自动应答和重发、设置频道和速率、配置CRC、上电、进入接收或发送模式。下面这段代码是完整的初始化:
#define TX_ADDR_WIDTH 5 #define RX_ADDR_WIDTH 5 // 初始化CI24R1 void CI24R1_Init(void) { CE = 0; // 先让芯片待机 CSN = 1; // 写入收发地址,这里用0xE7E7E7E7E7做示例 SPI_WriteReg(0x10, 0xE7); // TX_ADDR寄存器,5字节地址 SPI_WriteReg(0x11, 0xE7); SPI_WriteReg(0x12, 0xE7); SPI_WriteReg(0x13, 0xE7); SPI_WriteReg(0x14, 0xE7); SPI_WriteReg(0x0A, 0xE7); // RX_ADDR_P0寄存器 SPI_WriteReg(0x0B, 0xE7); SPI_WriteReg(0x0C, 0xE7); SPI_WriteReg(0x0D, 0xE7); SPI_WriteReg(0x0E, 0xE7); // 使能通道0接收地址,开启自动应答 SPI_WriteReg(0x01, 0x01); // EN_AA,使能通道0自动应答 SPI_WriteReg(0x02, 0x01); // EN_RX_ADDR,使能通道0 // 地址宽度5字节 SPI_WriteReg(0x03, 0x03); // 自动重发:500次,间隔250us SPI_WriteReg(0x04, 0x1A); // 设置频道,这里用2.4GHz + 40 = 2.440GHz SPI_WriteReg(0x05, 40); // 射频参数:2Mbps,发射功率0dBm,开启LNA SPI_WriteReg(0x06, 0x0E); // 配置寄存器:使能CRC,16位CRC,上电,发送模式 SPI_WriteReg(0x00, 0x0E); }关于地址,是所有入网设备共用的一个概念。同一个网络里的所有模块地址必须完全一致,否则接收端无法产生地址匹配中断,数据就丢了。地址用5字节还是4字节可以在SETUP_AW寄存器里设置,我的建议是直接用5字节,降低误匹配的概率。
频道也是同理,一个系统里所有模块必须工作在同一频道。频道号计算公式是2400 + RF_CH,单位MHz,比如RF_CH=40就是2440MHz。如果周围2.4G环境干扰重,可以试试换个频道,避开WiFi常用的1、6、11信道。
3.3 发送端完整代码实现
发送端的核心逻辑是:把要发的数据写入TX_FIFO,拉高CE至少10μs,芯片自动完成整个数据包的调制发送,发送完成后会产生TX_DS中断。如果开了自动应答,还会在发送后等待接收端的应答包,超时未收到就自动重发,重发超过上限会产生MAX_RT中断。
// 发送一包数据,最多32字节 uint8_t CI24R1_Send(uint8_t *buf, uint8_t len) { uint8_t status; if (len > 32) len = 32; // 拉低CE,进入待机状态 CE = 0; // 清空TX_FIFO CSN = 0; SPI_WriteByte(0xE1); // FLUSH_TX命令 CSN = 1; // 写TX_PAYLOAD CSN = 0; SPI_WriteByte(0xA0); // W_TX_PAYLOAD命令 for (uint8_t i = 0; i < len; i++) { SPI_WriteByte(buf[i]); } CSN = 1; // 拉高CE,启动发送 CE = 1; delay_us(20); CE = 0; // 等待发送完成,这里做个简单超时 uint16_t timeout = 10000; do { status = SPI_ReadReg(0x07); // STATUS寄存器 timeout--; } while (!(status & 0x20) && !(status & 0x10) && timeout > 0); // 清中断标志 SPI_WriteReg(0x07, status | 0x30); if (status & 0x10) { return 1; // 重发超限,发送失败 } if (status & 0x20) { return 0; // 发送成功 } return 2; // 超时 }发送完成标志在STATUS寄存器的TX_DS位(bit5),重发超限标志在MAX_RT位(bit4)。这两个标志位都是写1清零,所以代码里清中断用的是读状态后或上0x30再写回去。这个操作要小心,别把RX_DR位也顺手清了,接收模式下数据还没读出来就清中断,会导致数据丢失。
延时函数要准。CE高电平持续时间在数据手册里写的是10μs以上,我实际测试2Mbps速率下20μs比较稳妥。如果用带PA的模块,这个时间稍微放大一点到50μs,保证PA完全使能。
3.4 接收端完整代码实现
接收端的逻辑就更简单了:拉高CE后芯片自动进入接收状态,检测到有效数据包后把数据存入RX_FIFO,同时置位RX_DR中断标志,单片机读到中断后去读RX_PAYLOAD。
// 进入接收模式 void CI24R1_RXMode(void) { CE = 0; // CONFIG寄存器:使能CRC,16位CRC,上电,接收模式 SPI_WriteReg(0x00, 0x0F); // 清空RX_FIFO CSN = 0; SPI_WriteByte(0xE2); // FLUSH_RX命令 CSN = 1; // 清状态寄存器 SPI_WriteReg(0x07, 0x70); CE = 1; delay_us(100); } // 查询方式接收一包数据 uint8_t CI24R1_Receive(uint8_t *buf, uint8_t *len) { uint8_t status; uint8_t payload_len; uint8_t i; status = SPI_ReadReg(0x07); // 没有收到数据 if (!(status & 0x40)) return 0; // 读RX_PAYLOAD宽度 CSN = 0; SPI_WriteByte(0x60); // R_RX_PL_WID命令 payload_len = SPI_ReadByte(); CSN = 1; // 读取数据 CSN = 0; SPI_WriteByte(0x61); // R_RX_PAYLOAD命令 for (i = 0; i < payload_len; i++) { buf[i] = SPI_ReadByte(); } CSN = 1; *len = payload_len; // 清RX_DR中断 SPI_WriteReg(0x07, status | 0x40); return 1; }接收模式初始化时有个小技巧:先清一次状态寄存器再拉高CE。因为芯片在上电或复位瞬间,STATUS寄存器可能会有残留标志位,不清掉的话第一次查询就可能误判为有数据,读出来的却是空包。
R_RX_PL_WID命令是读取当前RX_FIFO中第一个数据包的长度,这个字节必须在读数据之前先读。有些驱动图省事,直接固定读32字节或者按发送端已知长度去读,这在固定长度通信时没问题,但做动态长度协议时最好每次都读,否则数据错位很难排查。
3.5 自动应答与重发机制
CI24R1的自动应答功能(Enhanced ShockBurst)非常实用。开启EN_AA后,接收端收到数据会自动在极短时间内切换为发送模式回一个ACK包给发送端,发送端如果没收到ACK就会自动重发,直到收到ACK或重发次数达到上限。
这个机制的好处是收发双方不用自己实现应答和超时重传,可靠性大幅提升。比如做无线遥控,数据包丢了重发,接收端如果收到了重复包但没来得及去重,就有问题。好在CI24R1在接收端收到重复数据包时,会把STATUS寄存器的RX_DR位置位,但不会把重复数据写入新FIFO位置,所以需要在应用层判断一下重复包。
我的建议是:在发送的数据格式里加一个包序号字段,接收端判断序号是否变化,没变化就认为是重发包,直接丢弃。这样配合硬件自动重传,整个链路可靠性会非常接近有线通信。
4. 调试过程与实测数据记录
4.1 用逻辑分析仪验证SPI时序
我调试这类模块的习惯是先把逻辑分析仪接到CSN、SCK、MOSI、MISO四个引脚上,抓一发初始化时序看看。
第一次上电时我发现MISO线上完全没有波形,排查了半天,最后发现是模块的VCC接到了5V上。CI24R1标称最大3.6V,5V直接怼进去芯片虽然没烧,但内部LDO可能已经进入保护状态,SPI接口完全不响应。换成3.3V供电后才正常。
之后再看SPI波形,确认写寄存器的命令字是0x20开头,读寄存器是0x00开头,数据在SCK上升沿稳定输出,这样初始化部分才算真正跑通。我建议你调试时优先抓这两个点:命令字最高位对不对、CE拉高的时机对不对。RF芯片调试,90%的问题都出在这两个地方。
4.2 整机收发实测流程
我的测试环境:两块STC89C52开发板,各接一个CI24R1模块,A板按键触发发送,B板上OLED显示收到的数据,距离控制在5米内。
发送端逻辑:上电初始化后进入发送模式,按下按键就发送一帧12字节数据,分别是帧头、包序号、8字节载荷、CRC校验值。发送完成后通过串口打印状态码,0表示成功,1表示重发失败,2表示超时。
接收端逻辑:初始化后进入接收模式,主循环查询STATUS寄存器,有数据就读取并打印到OLED和串口,同时把收到数据的包序号回传给发送端显示。
实测1000包在2Mbps速率下,5米距离成功率100%,拉远到30米穿一堵墙,成功率降到87%左右。改成1Mbps速率后,同样条件下成功率回升到96%。这说明CI24R1在低速率的接收灵敏度确实更好,距离和穿墙性能也更强。如果你的数据量不大,建议优先用1Mbps,换来的可靠性提升非常明显。
4.3 中断方式还是查询方式
前面示例代码用的是查询方式,主循环里不断读STATUS寄存器。这种方法简单,但有个明显问题:单片机在忙其他任务时,可能来不及查询,数据包就积压在FIFO里,FIFO满了之后新包会被丢弃。
如果想提高实时性,推荐用IRQ引脚接单片机外部中断。CI24R1的IRQ引脚在发生RX_DR、TX_DS、MAX_RT这三个事件时会被拉低。51单片机的外部中断INT0是低电平触发,直接把IRQ接P3.2即可。
中断服务函数里要做的事情很少:读STATUS寄存器,根据标志位判断是收到数据还是发送完成,然后处理相应的业务逻辑,最后清状态寄存器。注意,中断服务函数里不要做耗时操作,比如OLED显示、串口打印这些全部放到主循环里用标志位去执行,否则中断里待太久可能导致射频芯片接口超时。
5. 常见问题与避坑经验
5.1 发送失败、读不到状态
最典型的现象是寄存器读出来全是0xFF或者0x00。0xFF大概率是SPI通信没建立,检查CSN、SCK、MOSI接线顺序是否一致,特别注意模块上的丝印引脚顺序,有些模块标号和51单片机开发板上的丝印顺序是镜像的,第一次接反很正常。0x00则大概率是供电电压不对,或者芯片没有正常上电复位,断电重新上电试试。
另一个可能性是模块进入了一种叫"挂起"的异常状态。解决办法是给CE和CSN都拉低,延时几毫秒再重新初始化。如果还不行,就直接断电给芯片复位。
5.2 收发两端无法通信
确认几个参数完全一致:频道RF_CH、收发地址、空中速率、地址宽度、CRC宽度。任何一项不一致,数据都会在物理层就被丢弃,接收端完全收不到任何中断。
我遇到过最坑的一次是两端代码看起来完全相同,但一端用的nRF24L01模块,另一端用的CI24R1模块。虽然两者寄存器兼容,但一个型号的模块上电后默认唤醒模式和另一个型号不一样,导致一端在发送前先进入了睡眠,另一端等不到。解决办法是统一用同一型号的模块,或者在各自初始化代码里显式把PWR_UP位置1。
5.3 近距离通信反而失败
这是2.4G模块比较经典的现象,俗称"近场死区"。两个模块距离非常近(小于10cm)时,发射端的信号会过载接收端的前端电路,导致接收端无法解调。
解决办法是拉远距离,或者适当降低发射功率。把RF_SETUP寄存器里发射功率从0dBm降到-12dBm,近距离通信就正常了。如果你做的是桌面上的无线调试工具,长期短距离工作,建议默认把发射功率调低一档,对功耗和稳定性都有好处。
5.4 数据偶发错位和乱码
排查思路按下面表格快速定位:
| 现象 | 首选排查项 | 解决方案 |
|---|---|---|
| 偶发乱码 | 供电纹波 | 模块供电处加10μF电解电容 |
| 低频错位 | SPI速率太高 | 降低SPI时钟到500kHz以下 |
| 特定频道乱码 | 2.4G环境干扰 | 用频谱仪或软件扫描选干净频道 |
| 距离稍远就错 | 天线损伤 | 检查天线焊点,不要弯折天线 |
| 只发不收 | 收发模式配置 | 检查PRIM_RX位,切换模式 |
如果你的应用场景是电池供电,还要注意发送时瞬间电流较大,电池内阻大的话电压跌落会导致芯片复位。解决办法是加一个大电容储能,或者用锂电池这类内阻小的电源。
6. 总结与实用建议
CI24R1作为nRF24L01的国产平替方案,我认为在成本和供货稳定性上优势非常明显,功能上完全覆盖常规无线透传需求,而且寄存器级兼容让代码迁移成本几乎为零。如果你之前用过nRF24L01,直接跳过来没有任何学习成本。
最后分享两个小技巧:第一,批量生产时,每块模块上电后先读一次芯片版本寄存器,确认芯片正常响应再进入业务逻辑,避免焊接不良导致现场批量故障排查困难;第二,如果数据量大,建议把数据拆成28字节以内的小包分批发,给协议头留出空间,实测这样比一次塞满32字节的成功率更高,因为FIFO操作和天线切换的时间余量更充裕。
我这套例程已经在小批量项目里跑了大半年,稳定性和重连机制都验证过了。你照着上面的代码搭一遍,大概率一次就能通。要是调试过程中有问题,欢迎在评论区交流。
本文还有配套的精品资源,点击获取