公司动态
CI24R1无线收发模块51单片机驱动例程:兼容nRF24L01P
简介CI24R1单片机收发例程是一份面向单片机开发者的无线通信实践资料重点演示STC8H系列单片机与CI24R1无线模块在NOACK无应答模式下的发射与接收流程。资源包共220个文件以C语言源码和头文件为核心同时包含汇编文件、库文件、工程配置文件及编译生成的hex、obj、lst等中间文件可完整支撑代码阅读、编译烧录与二次调试压缩包整体约980KB轻量易用。目前已受到329人关注学习。例程从串口初始化、数据包构造到接收解析与打印验证均有清晰实现并注重CRC校验和抗干扰思路能帮助开发者快速上手CI24R1模块的基础通信理解无线收发中的数据格式、确认机制与调试方法为后续扩展多点通信、功率控制或加密通信等复杂功能提供可靠起点。 CI24R1这颗芯片我这两年做无线数据采集和遥控项目时用了不少说实话它跟nRF24L01P的寄存器基本兼容管脚定义也几乎一样但价格和供货更稳所以不少做量产的朋友会把它作为替代方案。简单来说CI24R1是一颗2.4GHz频段的无线收发芯片通过SPI接口跟单片机通信支持1Mbps和2Mbps两种速率内部带FIFO、自动应答和自动重发机制很适合做短距离无线透传、遥控开关、传感器数据回传这类应用。下面我就把基于51单片机的CI24R1收发例程完整拆开讲一遍从硬件接线、驱动移植到收发代码、调试排错全程用我实际跑过的工程说话保证你在自己板子上也能复现得出来。这篇文章适合谁准备用51、STM32或者国产M0核单片机做无线通信的同学尤其是想快速跑通第一版数据链路的朋友。我会以51单片机为主控来演示核心逻辑移植到STM32其实也就是改个SPI底层的问题不用太担心平台差异。1. 无线收发模块选型对比与CI24R1核心特性1.1 为什么选CI24R1而不是直接用nRF24L01P很多第一次接触无线收发的人都会问既然CI24R1兼容nRF24L01P为什么不直接用原版我实际对比下来的体验是这样的nRF24L01P虽然是老牌经典但市面上拆机翻新料多、批次差异大有时候买回来的模块连内部频率校准值都不一样而CI24R1作为国产替代手册写得还算清楚寄存器地址和操作时序完全对齐nRF24L01P直接替换不需要改驱动代码这对我这种做软硬件一起搞的人特别友好。在项目选型时还有个更现实的问题——成本。CI24R1的裸片价格和模块价格都比nRF24L01P有优势批量走货也相对稳定不用天天担心现货被扫。再加上它的发射功率最高能做到0dBm左右接收灵敏度也在-90dBm上下做10米到30米级别的室内通信绰绰有余。如果你的项目里本来就在用国产单片机做成本控制那CI24R1跟整个供应链的调性会很搭。当然nRF24L01P的生态更成熟网上资料多尤其是各种Arduino库。但CI24R1的寄存器兼容性意味着这些现成驱动基本拿过来就能用遇到问题大不了对照数据手册逐寄存器查调试思路完全一致。1.2 芯片关键参数与引脚功能说明CI24R1的关键参数我整理了一份速查表方便你快速评估它适不适合自己的项目参数数值说明工作电压1.9V~3.6V模块一般板载3.3V稳压5V单片机上需要电平转换或串电阻工作频率2.400GHz~2.483GHz共126个射频信道通过RF_CH寄存器设置通信速率250kbps / 1Mbps / 2Mbps速率越低灵敏度越高通信距离更远发射功率-18dBm / -12dBm / -6dBm / 0dBm通过RF_SETUP寄存器配置接口SPI最高约10MHz8位命令字跟普通SPI外设一致接收灵敏度约-82dBm2Mbps/ -90dBm1Mbps不同速率下不同参考手册自动重发支持最多重发15次Enhanced ShockBurst机制数据管道6个接收管道1个发送管道可实现一对多组网引脚方面模块常用的有8个VCC、GND、CE、CSN、SCK、MOSI、MISO、IRQ。其中CE是芯片使能引脚高电平工作时进入收发状态CSN是SPI片选低电平有效IRQ是中断输出有RX_DR、TX_DS、MAX_RT等事件时拉低可以通过外部中断去读取。有一个点需要特别提醒这颗芯片的IRQ引脚是开漏输出也就是说它本身不会主动推高到单片机上最好接一个10k左右的上拉电阻到3.3V否则你读中断标志的时候可能会踩到不确定电平的坑。这个我后面调中断时踩过一次后面会细说。2. 硬件电路设计与最小系统搭建2.1 51单片机与CI24R1的接线方案我先给出一份我用得最多的接线表主控是STC89C52RC或者AT89S52这类标准51单片机SPI直接用IO口模拟。你可能会问为什么不用51的硬件SPI因为标准51大部分型号根本没有硬件SPI就算有引脚位置也不一定方便模拟SPI在1Mbps的无线速率下完全够用IO口翻转速度完全顶得住。单片机引脚CI24R1模块引脚说明P1.0CE片选使能高电平工作P1.1CSNSPI片选低电平有效P1.2SCKSPI时钟P1.3MOSISPI主机输出从机输入P1.4MISOSPI主机输入从机输出P3.2INT0IRQ可选接外部中断03.3VVCC给模块供电注意别直接接5VGNDGND共地这里要特别说明供电问题。CI24R1的工作电压上限是3.6V而51单片机系统通常是5V直接接到VCC上大概率会把芯片搞挂。最稳的方式是模块单独用3.3V LDO供电比如AMS1117-3.3然后MISO线用电阻分压或者方向控制芯片把5V逻辑转成3.3V。如果是临时测试可以在MISO线上串联一个1k电阻再在模块侧并联一个10k电阻到3.3V这样能凑合工作但不建议量产照抄。IRQ引脚我建议一开始调试就接上哪怕你准备用轮询方式读状态标志。因为发数据时如果一直轮询STATUS寄存器CPU会被占死而且无法及时捕捉到重发超时这类异常。用外部中断0下降沿触发IRQ拉低时进入中断去处理逻辑清爽很多。2.2 电源滤波和射频走线注意事项无线模块跟纯数字电路不一样射频部分的电源干净程度直接影响通信距离。模块VCC引脚旁边至少要并联一个10uF电解电容和一个0.1uF陶瓷电容电容尽量靠近模块引脚。我实测过如果VCC上纹波大接收端的灵敏度会明显下降具体表现就是同样的距离丢包率变高。走线方面如果你自己画PCB模块天线区域正下方不要铺铜天线周围2mm内不要放高频器件。如果直接用现成的模块插杜邦线飞线测试那也要注意天线不要贴近单片机晶振或者电源线。飞线越短越好SCK和MOSI两条线尽量分开不要绑在一起走否则SPI时钟边沿会耦合噪声到数据线上。另外千万不要在Proteus这类仿真软件里跑这个例程。无线通信是射频行为仿真软件没有射频模型你仿真出的“数据收发”完全是理想化的逻辑电平变化对实际通信没有任何参考价值。我见过好几个同学在Proteus里调通了上实物却怎么都不出数据就是这个原因。3. 驱动移植与寄存器配置详解3.1 模拟SPI读写函数实现写驱动之前先把底层IO操作定义好。CI24R1的SPI是模式0也就是CPOL0、CPHA0空闲时SCK为低电平数据在SCK上升沿采样。这个千万不能搞错如果你的单片机硬件SPI支持模式配置记得设成SPI_MODE0如果跟我一样用IO模拟那直接按下面这段代码来就行sbit CE P1^0; sbit CSN P1^1; sbit SCK P1^2; sbit MOSI P1^3; sbit MISO P1^4;最基础的两个函数分别是写入一个字节和读取一个字节。对于写我习惯从高位开始先送最高位然后拉高时钟依次往下推。对于读逻辑刚好反过来的在SCK的上升沿把MISO的电平读进来。代码如下unsigned char SPI_RW(unsigned char byte) { unsigned char i; for(i 0; i 8; i) { if(byte 0x80) MOSI 1; else MOSI 0; SCK 1; byte 1; if(MISO) byte | 0x01; SCK 0; } return byte; }然后是基于这条基础函数封装出来的寄存器读写操作。CI24R1的命令字格式跟nRF24L01P完全一样读寄存器是0x00加上寄存器地址写寄存器是0x20加上寄存器地址。读的时候发送命令字后继续发一个任意字节同时把返回的字节收回来写的时候发送命令字后跟着要写入的数据字节。unsigned char CI24R1_ReadReg(unsigned char reg) { unsigned char val; CSN 0; SPI_RW(reg 0x1F); val SPI_RW(0); CSN 1; return val; } void CI24R1_WriteReg(unsigned char reg, unsigned char val) { CSN 0; SPI_RW((0x20 | (reg 0x1F))); SPI_RW(val); CSN 1; }写寄存器时有个细节CSN拉低之后每送一个字节地址指针会自动加一所以连续写多字节寄存器比如写5字节地址时可以连续送5个数据字节不需要重复操作CSN。后续写收发地址的时候会用到这个特性。3.2 初始化寄存器配置流程CI24R1上电之后默认是待机模式寄存器都是复位值必须经过完整初始化才能通信。初始化流程说白了就是做几件事先进入待机模式然后清空FIFO再配置收发地址、射频信道、速率、发射功率、自动应答等参数最后决定是开接收还是开发送。我工程里常用的初始化函数是这样的void CI24R1_Init(void) { CE 0; // 进入待机模式 CSN 1; CI24R1_WriteReg(CONFIG, 0x0E); // PWR_UP1, EN_CRC1, CRC1(2字节CRC) CI24R1_WriteReg(EN_AA, 0x01); // 使能自动应答通道0 CI24R1_WriteReg(EN_RXADDR, 0x01);// 开启接收通道0 CI24R1_WriteReg(SETUP_AW, 0x03); // 地址宽度5字节 CI24R1_WriteReg(SETUP_RETR, 0x1A); // 重发延时1500us最多重发10次 CI24R1_WriteReg(RF_CH, 0x40); // 射频信道0x40 2464MHz CI24R1_WriteReg(RF_SETUP, 0x07); // 1Mbps0dBm发射功率 CI24R1_WriteReg(RX_PW_P0, 0x08); // 接收通道0有效数据宽度8字节 // 设置收发地址 CI24R1_WriteReg(TX_ADDR, 0xE7); CI24R1_WriteReg(TX_ADDR1, 0xE7); CI24R1_WriteReg(TX_ADDR2, 0xE7); CI24R1_WriteReg(TX_ADDR3, 0xE7); CI24R1_WriteReg(TX_ADDR4, 0xE7); CI24R1_WriteReg(RX_ADDR_P0, 0xE7); CI24R1_WriteReg(RX_ADDR_P01, 0xE7); CI24R1_WriteReg(RX_ADDR_P02, 0xE7); CI24R1_WriteReg(RX_ADDR_P03, 0xE7); CI24R1_WriteReg(RX_ADDR_P04, 0xE7); CI24R1_WriteReg(STATUS, 0x70); // 清中断标志 CE 0; }这里有个新手经常踩的坑EN_AA如果使能了自动应答发送方发出数据后接收方必须在收到数据后自动回一个ACK发送方才会认为发送成功。如果接收方没有开启EN_AA或者你俩配置不一致发送方就会一直重发直到MAX_RT中断看起来像“发不出去”其实只是应答链断了。所以第一次调例程时我建议两边寄存器配置保持一致先跑通再按需裁剪。还有一个技巧地址可以不是全0xE7你可以自己定一串5字节的数但发送方地址和接收方地址必须完全一致。每次收数组件要换成一对新地址时只要改TX_ADDR和RX_ADDR_P0这两组寄存器就行。3.3 收发函数与状态机设计初始化好了接下来就是核心的发送和接收函数。先说发送CI24R1发送有三步第一步往发送地址寄存器写入对端地址第二步把数据写进TX FIFO第三步拉高CE引脚启动发送。发送完成后芯片会拉低IRQ引脚同时STATUS寄存器的TX_DS位置1。这里我写成函数方便直接调用unsigned char CI24R1_Send(unsigned char *buf, unsigned char len) { unsigned char status; CE 0; // 写发送地址 CSN 0; SPI_RW(0x30); // 写TX_ADDR命令字 for(unsigned char i 0; i 5; i) { SPI_RW(0xE7); } CSN 1; // 写TX FIFO CSN 0; SPI_RW(0xA0); // 写TX_PAYLOAD命令字 for(unsigned char i 0; i len; i) { SPI_RW(buf[i]); } CSN 1; CE 1; // 启动发送 delay_10us(1); // 至少10us高电平脉冲 CE 0; // 轮询等待发送完成或者用标志位跳出 status CI24R1_ReadReg(STATUS); while((status 0x20) 0) // 等待TX_DS置1 { status CI24R1_ReadReg(STATUS); if(status 0x10) // MAX_RT重发超限 { CI24R1_WriteReg(STATUS, 0x10); return 0; } } CI24R1_WriteReg(STATUS, 0x20); // 清TX_DS标志 CI24R1_WriteReg(FLUSH_TX, 0x00); // 清空TX FIFO return 1; }接收函数也类似但方向相反。先把芯片切到接收模式PRIM_RX1然后CE拉高进入RX模式当检测到IRQ变低之后读STATUS寄存器如果RX_DR为1说明有数据可读接着从RX FIFO里把数据捞出来unsigned char CI24R1_Receive(unsigned char *buf) { unsigned char status; unsigned char len; status CI24R1_ReadReg(STATUS); if(status 0x40) // RX_DR { len CI24R1_ReadReg(RX_PW_P0); CSN 0; SPI_RW(0x61); // 读RX_PAYLOAD命令字 for(unsigned char i 0; i len; i) { buf[i] SPI_RW(0); } CSN 1; CI24R1_WriteReg(STATUS, 0x40); // 清RX_DR CI24R1_WriteReg(FLUSH_RX, 0x00); // 清空RX FIFO return len; } return 0; }这里出现了一个关键点读载荷长度之前一定要先读RX_PW_P0因为FIFO里的数据长度就是指初始化时配置的RX_PW_P0值。如果你发8字节、收8字节那这个值就是8。如果收发长度不固定你可以把RX_PW_P0设成最大字节数然后在数据包的第一个字节里放实际长度。4. 收发例程实战主机和从机如何配合4.1 发送端完整代码主机我习惯把发送端称为主机接收端称为从机。这种叫法不是通信协议规定的只是为了代码里好区分。主机端主循环里每隔一段时间发一包数据发送成功之后切换到接收模式等待从机回包回包收到之后再切回发送模式。这样一主一从一问一答的模式非常适合传感器数据采集这种场景。主机完整流程代码void main(void) { unsigned char txbuf[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; unsigned char rxbuf[8]; unsigned char i; UART_Init(); // 串口初始化用来打印调试信息 CI24R1_Init(); // 发送模式 CI24R1_WriteReg(CONFIG, 0x0E); // PRIM_RX0, 发送模式 CE 1; delay_ms(1); CE 0; while(1) { if(CI24R1_Send(txbuf, 8)) { printf(send ok\n); } else { printf(send fail\n); } // 切到接收模式等从机应答 CI24R1_WriteReg(CONFIG, 0x0F); // PRIM_RX1 CE 1; delay_ms(10); if(CI24R1_Receive(rxbuf)) { printf(recv: ); for(i 0; i 8; i) { printf(%02x , rxbuf[i]); } printf(\n); } CI24R1_WriteReg(CONFIG, 0x0E); // 切回发送模式 CE 1; delay_ms(500); } }这里有个值得注意的地方从发送模式切到接收模式时CONFIG寄存器的PRIM_RX位要从0改成1但改完这个位之后芯片并不会立刻进入接收状态必须把CE拉高才能进入RX模式。同理从接收切回发送时也必须先CE拉低再改CONFIG、再拉高CE。顺序搞反了会导致芯片状态机错乱后面数据收发全部乱套。4.2 接收端完整代码从机从机端就简单多了初始化完成之后一直保持接收模式有数据进来就通过串口打印出来顺便回一个应答包给主机。回包这个动作很重要因为前面初始化开了自动应答如果从机不回ACK主机的发送会被自动重发机制拖住。从机主函数这样写void main(void) { unsigned char rxbuf[8]; unsigned char txbuf[8] {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22}; unsigned char i; UART_Init(); CI24R1_Init(); // 接收模式 CI24R1_WriteReg(CONFIG, 0x0F); // PRIM_RX1 CE 1; // 进入RX模式 while(1) { if(CI24R1_Receive(rxbuf)) { printf(recv: ); for(i 0; i 8; i) { printf(%02x , rxbuf[i]); } printf(\n); // 回一个应答包给主机 CI24R1_Send(txbuf, 8); } } }这里从机回包的时候直接调了CI24R1_Send但是这个函数里面会先把CE拉低再拉高实际上已经把从机从接收模式切走了。回包完成后从机并不会自动回到接收模式。所以如果你测下来发现从机只能收到第一包数据后面就收不到了大概率就是这个原因。解决办法是在发完应答包之后重新把CONFIG设为接收模式、CE拉高。我修正后的从机收包代码如下if(CI24R1_Receive(rxbuf)) { printf(recv: ); // 处理数据... CI24R1_Send(txbuf, 8); // 切回接收 CE 0; CI24R1_WriteReg(CONFIG, 0x0F); CE 1; }4.3 双工通信的实现思路CI24R1本质是半双工芯片同一时刻只能发或者只能收但我们可以通过快速切换模拟出双工通信的效果。最简单的思路就是时分复用定义一个通信周期比如100ms前50ms主机发送、从机接收后50ms从机发送、主机接收。两边的代码用定时器来切换收发模式。这个思路在简单问答模式下很好用但有一个隐患如果主机在从机还没切到发送模式时就准备接收那从机的数据就会丢掉。所以我在实际工程里通常不用时间槽对称切换而是用“主机询问-从机应答”模式。主机问完必须等从机回包收到回包后再问下一轮这样天然不会有半双工切换冲突。扩展一下如果你要做一对多轮询可以把地址管道机制用起来。一个接收端可以同时开启6个接收管道每个管道用不同地址对应一个发送端。这样接收端不需要切换地址就能收到多个节点的数据。具体做法是把EN_RXADDR寄存器的第0到第5位置1然后给每个管道分别写地址。发送端则保持发往同一个地址即可。5. 调试技巧与常见问题排查实录5.1 用逻辑分析仪抓SPI时序我自己调CI24R1的时候最常用的调试工具不是串口打印而是逻辑分析仪。8通道的几十块钱那种就够用把SCK、MOSI、MISO、CSN、CE五根线挂上采样率调到20MHz以上然后看波形。主要看几个点CSN每次拉低之后SCK是不是稳定输出了8个脉冲MOSI的数据跟SCK上升沿是不是严格对齐MISO在对应时刻有没有正确回数据。如果SPI时序乱了后面一切都是白搭。有个小技巧在调试代码里加一个测试寄存器读写函数初始化之后先写一个值进CONFIG寄存器再读回来如果读回的值跟写进去的一致说明SPI底层没有问题了然后再去调无线收发。这个步骤我一直放在所有无线调试的最前面能省掉大量无效排查时间。TEST寄存器不是每个版本芯片都有但可以用RF_CH这种普通寄存器来测试。比如写RF_CH0x40读完如果等于0x40就说明SPI通路是通的。5.2 常见问题速查表我在这个例程上前后带过好几个同学做项目也帮人远程排查过不少次问题把最常见的几类问题整理成了下面的速查表现象可能原因解决方法初始化读寄存器全为0xFFSPI时序错误或模块未上电检查SCK/MOSI/MISO接线确认模块VCC是3.3V发送端一直报MAX_RT接收端没开自动应答或地址不匹配核对EN_AA寄存器核对收发地址是否一致接收端IRQ不触发IRQ引脚未上拉或MISO被占用IRQ接10k上拉到3.3V检查中断配置能收能发但丢包严重电源纹波大或天线附近有金属加大VCC滤波电容天线区域净空两个模块相隔1米反而比5米差近场反射导致驻波恶化测试时拉开距离或模块架高错开朝向数据长度超过8字节就出错RX_PW_P0配置过小配置成你需要的最长数据长度从机回包后收不到后续数据发送函数把模式切走了回包后重新配置PRIM_RX1并拉高CE表格里每一项基本都是我在实际调板时遇到过的。其中“两个模块相隔很近反而通信变差”这个现象第一次遇到时我没反应过来后来拆了模块用频谱仪看发射波形才发现是天线近场效应。无线调试遇到距离近反而丢包时优先把两个模块拉开到2米以上再试。5.3 Keil在线调试监控引脚电平如果你用的是Keil5调试51单片机想把CE、IRQ这些引脚电平实时看变化可以在调试状态下打开View菜单里的Logic Analyzer窗口然后把引脚比如CE、IRQ拖进去设置好触发方式就能配合断点观察引脚翻转。这个方法查“CE引脚到底拉高没有”特别好用。不过要提醒一句在线仿真模式下IO口状态是实时反映的但如果你在断点处停下来太久CI24R1内部的射频状态机可能会因为SPI时钟停顿而超时导致后续数据异常。所以在线调试无线模块时尽量少打断点多靠串口打印和逻辑分析仪数据来判断问题。6. 一轮实际调测记录与性能参考6.1 室外视距通信距离实测我把这个例程跑在一对CI24R1模块上主控是STC89C52RC天线用的是模块板上自带的PCB天线速率配成1Mbps发射功率0dBm。在室外开阔环境下两个模块架在1.2米高的三脚架上实测在30米左右能稳定收发丢包率在1%以内拉到45米左右开始出现偶发丢包但通信还能维持超过60米就基本连不上了。室内隔一堵砖墙距离大概能到12米左右隔两堵墙就只有5米不到还跟墙体厚度有关系。这个数据只能作为参考因为天线方向和周围反射体对距离影响很大。如果你的项目要求跑更远可以把速率降到250kbps或者换外置天线模块距离还能再上一个台阶。测试时我还发现一个有趣的现象天线摆放方向对距离影响很大。PCB天线水平方向和垂直方向辐射特性不一样最好的做法是两块模块天线尽量保持同一个朝向比如都垂直朝上或者都水平朝外。实测中天线互相垂直时距离衰减能有30%以上。所以固定安装类产品天线朝向一定要在结构设计阶段就定好。6.2 功耗调优与低功耗应用扩展如果你的设备是电池供电CI24R1的功耗控制也值得研究。芯片在接收模式下电流大概十几毫安发送模式看发射功率0dBm时大概11mA左右待机模式电流很小掉电模式下更是可以控制在1uA以下。实际工程里我习惯用一颗IO口直接控制CE引脚不需要通信时把CE拉低让芯片进入待机模式。如果单片机也有睡眠模式可以定时唤醒单片机先拉高CE、初始化无线配置、收一次数据然后再拉低CE、进入睡眠。这样一套流程下来设备的平均功耗可以做到很低适合用在无线门磁、温湿度传感器这类需要年抛电池的产品上。低功耗模式开发时特别要注意CI24R1退出掉电模式后需要重新初始化寄存器吗答案是寄存器内容在待机和掉电模式下都能保持但PWR_UP位重新拉高后芯片内部状态机会重新启动建议等待至少2ms再操作CE。我最初做低功耗时就没有等这个时间导致每次唤醒后第一包数据经常丢失排查了很久才发现是上电时序的问题。这个例程本身是个起点做透之后你可以往里面加的东西还有很多比如多字节大包拆包重组、CRC校验增强、跳频抗干扰、发送端休眠轮询等。但是先把最基本的收发闭环跑稳定后面所有扩展才有意义。我个人的建议是拿到模块先别急着写业务逻辑按上面这套初始化、发送、接收、回包全部跑通一遍再慢慢往上叠需求这个节奏最不容易把自己绕晕。本文还有配套的精品资源点击获取