STM32驱动CC1101无线发射模块:SPI配置与调试实战
简介这套STM32连接CC1101模块的发射程序面向需要快速搭建Sub-GHz无线通信链路的嵌入式开发者。STM32基于ARM Cortex-M内核CC1101则是TI公司的低功耗无线收发器程序通过SPI总线将两者相连覆盖SCK、MISO、MOSI、NSS引脚配置以及IOCFG2、FREQUENCY_AGc、PA_LEVEL等关键寄存器的初始化并实现了数据打包、CRC校验、写入FIFO和启动发射的完整流程。作者强调已经过实际电路测试并非网络上常见的不可用驱动工程具备较强的参考价值。压缩包内共113个文件大小约1007KB包含C/H源文件、Keil工程配置uvproj/uvopt、编译产物axf/hex/o/map和文本说明等既支持直接编译烧录也便于逐模块阅读代码进行二次开发。目前已有784人学习下载适合正在调试CC1101模块或学习STM32 SPI通信的开发者参考可帮助规避天线匹配、信号干扰和寄存器配置等常见问题显著缩短无线通信项目的验证周期。 拿到CC1101模块那会儿我第一反应也是满网搜发射例程。搜出来的代码确实不少但大多数是直接贴一串寄存器配置然后往SPI里丢几个字节至于为什么是这串值、为什么发射前要先做手册里那几个步骤、GDO0到底怎么用基本没人讲清楚。这篇东西我就按自己实际调通STM32连接CC1101模块发射程序的完整过程来写把原理、代码、坑一起捋一遍。如果你手里正好有STM32和CC1101模块想做无线遥控或者传感器数据上行这篇文章可以直接当参考手册用。1. 项目定位与整体设计思路1.1 STM32CC1101能做什么为什么选这套组合CC1101是TI推出的一颗Sub-1GHz无线收发芯片和2.4G的nRF24L01不同它工作在433MHz、868MHz、915MHz这几个ISM频段。Sub-1GHz最大的优势就是绕射能力和穿墙能力明显强于2.4G同样几毫瓦的发射功率在楼宇里跑个几十米甚至上百米很常见比2.4G在复杂环境里稳定不少。加上模块价格便宜一颗SPI接口的CC1101模块也就十来块钱所以用来做遥控器、无线门铃、传感器节点上报、低速数传这类项目非常合适。STM32这边就更不用多说了哪怕是入门级的F103跑CC1101这套SPI通信也是绰绰有余。整个项目说白了就是一个简单的无线发射端STM32通过SPI接口把要发的数据写入CC1101的发送FIFO再给CC1101发一个“开始发送”的命令芯片就会自动完成调制、前导码、同步字、CRC校验把数据变成射频信号从天线发出去。你能控制的是数据内容、发射频率、速率、调制方式这些参数剩下的射频工作交给CC1101处理。1.2 硬件接线与模块选型市面上常见的CC1101模块有两种裸模块和带PA的模块。如果你只是在室内短距离测试裸模块就够如果要做远距离或者穿墙场景建议选带PA的版本比如常见的E07-433MSB这类发射功率可以做得更大。不管哪种SPI接口引脚都是一样的。我用的是一块STM32F103C8T6最小系统板配上常见的CC1101模块接线如下STM32引脚CC1101引脚说明PA5SPI1_SCKSCKSPI时钟PA7SPI1_MOSISI主机输出从机输入PA6SPI1_MISOSO从机输出主机输入PA4普通GPIOCS片选低电平有效PC7普通GPIO输入GDO0发送完成/接收中断状态输出3.3VVCC供电GNDGND共地这里要注意几点。第一CC1101模块务必接3.3V接5V大概率直接烧模块。第二GDO0这根线不能省发射完成要靠它来通知STM32要么接普通GPIO轮询要么接外部中断引脚我用轮询方式就够了。第三模块天线尽量拉开不要贴着金属面尤其调试的时候天线附近不要有手指和大面积覆铜否则信号会被严重吸收。1.3 SPI通信基础CC1101是“从机”一切靠命令CC1101的SPI接口不像普通外设那样“写寄存器就完事”它的所有操作都围绕“命令”展开每次通信都是拉低CS发出一个指令字节然后根据指令类型决定是读写数据还是结束本次传输。指令字节分三类。第一类是寄存器读写地址0x00到0x2E最高位为1表示读为0表示写第二类是FIFO读写比如写发送缓冲区TXFIFO的地址是0x7F读接收缓冲区RXFIFO的地址是0xFF这两个都属于突发模式发出地址后可以连续传多个数据字节第三类是strobe命令也就是以0x30开头的单字节命令比如0x35是“开始发射”STX0x36是“开始接收”SRX0x34是进入IDLE状态。SPI速率先不要拉太高STM32的SPI1分频到3.25MHz左右足够用CC1101虽然支持到10MHz但速度高了对布线要求也高调试阶段没必要给自己找麻烦。另外SPI模式必须是CPOL0、CPHA0也就是空闲时钟为低、第一个边沿采样这个是CC1101手册里写死的用错就直接全读不到数据。2. CC1101发射核心原理与配置参数2.1 发射数据流的完整过程很多人一开始容易犯的错就是把CC1101当成一个SPI转串口的透传芯片认为数据写进去就自己出去了。实际上CC1101发射一包数据要经过好几个环节先把数据包写进TXFIFO然后执行STX命令这时芯片开始按配置生成前导码、自动补充同步字、根据配置决定是否加CRC和扰码最后把所有内容经过调制电路变成射频信号发出。发送完成后GDO0引脚会产生一个电平跳变。整个过程芯片内部自动做但主控端必须按顺序操作先让芯片回到IDLE状态然后清空TXFIFO写入数据再发STX。顺序如果乱了比如刚上电直接往FIFO里写数据而不先确认芯片状态非常容易出现数据写进去了但根本没发出去的情况。这里有个很关键的细节TXFIFO最大61字节。如果一包数据超过61字节就必须分多次发送或者在寄存器里开启无限包模式。刚开始调试时我建议一包就发几个字节先把链路跑通再谈大数据包。2.2 频率、速率、调制三大关键参数的计算CC1101的载波频率不是直接写一个“433”进去而是通过三个8位寄存器FREQ2、FREQ1、FREQ0组合计算出来的公式是FREQ 载波频率 / 晶振频率 × 65536以26MHz晶振、目标433MHz为例433000000 / 26000000 × 65536 ≈ 1091121转成十六进制就是0x10A531。所以FREQ2 0x10FREQ1 0xA5FREQ0 0x31。这一步如果你手里晶振不是标准的26MHz比如用的是24MHz或者其他频点算出来的值就不一样。不过好在TI有官方工具可以自动生成不需要手算。数据率和调制方式同样不用硬算它们分散在MDMCFG4、MDMCFG3、MDMCFG2这些寄存器里涉及分频系数、信道间隔、同步字模式、调制格式等一堆位域手算非常容易出错。发射和接收两端的数据率必须一致否则哪怕只差一点点接收端也只是偶尔能解调出一两个字节然后疯狂丢包。2.3 配置寄存器怎么获取与下发最好的办法是用TI官方的SmartRF Studio 7软件。选好CC1101型号填上晶振频率把目标频点、数据率、调制方式、信道间隔这些参数拖一拖软件会立刻生成完整寄存器配置表照着抄进代码里就行。这比任何网上流传的“万能配置表”都靠谱因为不同模块的晶体频率、板子走线分布电容都不一样直接抄别人配置有时候能通但频偏可能跟你实际板子对不上。下发配置的方式也值得注意不要上电后立刻一次把所有寄存器写完就认为初始化完成了。正确做法是芯片上电后先等待晶振稳定然后往配置寄存器表里逐个写入值最后执行一次手动校准。手动校准的操作是确认芯片处于IDLE状态写FSCAL30xE9、FSCAL20x2A、FSCAL10x00、FSCAL00x1F再发送SCAL校准命令等待芯片重新回到IDLE状态。出厂芯片虽然也会自动校准但手动校准能确保当前环境温度、电压下的频率准确度更好这一步在发射端尤其值得做。3. 发射程序完整实现3.1 SPI底层驱动和CC1101基础操作我习惯用STM32标准库写工程结构更直观。SPI1初始化的核心代码void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; SPI_InitTypeDef SPI_InitStruct {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // SCK PA5, MOSI PA7 推挽复用输出 GPIO_InitStruct.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); // MISO PA6 浮空输入 GPIO_InitStruct.GPIO_Pin GPIO_Pin_6; GPIO_InitStruct.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStruct); SPI_InitStruct.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_Mode SPI_Mode_Master; SPI_InitStruct.SPI_DataSize SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL SPI_CPOL_Low; SPI_InitStruct.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStruct.SPI_NSS SPI_NSS_Soft; SPI_InitStruct.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_16; SPI_InitStruct.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStruct); SPI_Cmd(SPI1, ENABLE); }然后是最底层的三件套SPI读写一个字节、执行strobe命令、读写寄存器。static uint8_t CC1101_ReadWriteByte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); } void CC1101_Strobe(uint8_t cmd) { CC1101_CS_LOW(); CC1101_ReadWriteByte(cmd); CC1101_CS_HIGH(); } void CC1101_WriteReg(uint8_t addr, uint8_t val) { CC1101_CS_LOW(); CC1101_ReadWriteByte(addr); // 写寄存器最高位为0 CC1101_ReadWriteByte(val); CC1101_CS_HIGH(); }对应读单个寄存器的函数是把地址最高位置1然后多发一个空字节收数据。这里每次操作前后拉低、拉高CS是不可省略的而且CS拉高后最好加一个极短延时给芯片时间处理当前指令频率不算高的时候不加延时也能跑但加了更稳。3.2 初始化配置初始化函数里我先把芯片拉回IDLE再写入寄存器配置数组。这里贴一组我在433MHz、GFSK调制、38.4kbps数据率下实际用过的配置注意这只是参考最准确的做法是拿SmartRF Studio 7按你的板子重新生成一次const uint8_t cc1101_cfg[][2] { {0x00, 0x0D}, // IOCFG2 {0x01, 0x2E}, // IOCFG1 {0x02, 0x09}, // IOCFG0 {0x03, 0x07}, // FIFOTHR {0x06, 0xFF}, // PKTLEN {0x07, 0x04}, // PKTCTRL1 {0x08, 0x05}, // PKTCTRL0 {0x09, 0x00}, // ADDR {0x0A, 0x00}, // CHANNR {0x0B, 0x06}, // FSCTRL1 {0x0D, 0x10}, // FREQ2 {0x0E, 0xA5}, // FREQ1 {0x0F, 0x31}, // FREQ0 {0x10, 0x2C}, // MDMCFG4 {0x11, 0x12}, // MDMCFG3 {0x12, 0x04}, // MDMCFG2 {0x13, 0x02}, // MDMCFG1 {0x14, 0x3A}, // MDMCFG0 {0x15, 0x34}, // DEVIATN {0x16, 0x07}, // MCSM2 {0x17, 0x30}, // MCSM1 {0x18, 0x18}, // MCSM0 {0x19, 0x16}, // FOCCFG {0x1A, 0x6C}, // BSCFG {0x1B, 0x43}, // AGCCTRL2 {0x1C, 0x40}, // AGCCTRL1 {0x1D, 0x91}, // AGCCTRL0 {0x21, 0xB6}, // FREND1 {0x22, 0x11}, // FREND0 {0x23, 0xE9}, // FSCAL3 {0x24, 0x2A}, // FSCAL2 {0x25, 0x00}, // FSCAL1 {0x26, 0x1F}, // FSCAL0 };初始化主逻辑void CC1101_Init(void) { uint8_t i, marcState; CC1101_Strobe(CC1101_SIDLE); // 先回IDLE Delay_Us(50); for (i 0; i sizeof(cc1101_cfg) / 2; i) { CC1101_WriteReg(cc1101_cfg[i][0], cc1101_cfg[i][1]); } // 手动校准 CC1101_WriteReg(0x23, 0xE9); CC1101_WriteReg(0x24, 0x2A); CC1101_WriteReg(0x25, 0x00); CC1101_WriteReg(0x26, 0x1F); CC1101_Strobe(CC1101_SCAL); // 等待校准完成MARCSTATE回到IDLE do { marcState CC1101_ReadReg(0x35) 0x1F; } while (marcState ! 0x01); }0x35地址读出来的是MARCSTATE寄存器低5位是芯片当前状态机的状态0x01表示IDLE。校准期间芯片会在高频状态机里来回切换我们不用管具体状态等它回到IDLE就代表校准完成。3.3 发射一帧数据的核心函数发射函数是整个程序的核心逻辑是回IDLE、清空TXFIFO、写入数据、发STX、等GDO0跳变、回IDLE。完整代码void CC1101_SendPacket(uint8_t *buf, uint8_t len) { if (len 61) len 61; // TXFIFO最大61字节 CC1101_Strobe(CC1101_SIDLE); CC1101_Strobe(CC1101_SFTX); // 清空发送FIFO // 突发写TXFIFO地址0x7F CC1101_CS_LOW(); CC1101_ReadWriteByte(0x7F); for (uint8_t i 0; i len; i) { CC1101_ReadWriteByte(buf[i]); } CC1101_CS_HIGH(); CC1101_Strobe(CC1101_STX); // 开始发射 // 等待GDO0电平变化表示发送完成 // 如果GDO0没接也可以改成轮询MARCSTATE直到脱离TX状态 while (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_7) RESET); CC1101_Strobe(CC1101_SIDLE); // 发送完回到IDLE }这里有个容易踩的细节STX命令发出后芯片先要经历从IDLE进入TX状态的过程然后才开始发前导码和同步字。GDO0默认状态下会被配置成“发射完成时产生低脉冲或者高电平变化”我这里用的配置把GDO0配成了发送完成时产生高电平有效信号所以代码里等待的是引脚变高。如果你的GDO0配置不一样这里等待的极性也可能相反最简单的确认方法是用示波器或者逻辑分析仪看GDO0引脚的实际波形。3.4 主程序与整体测试主程序很简单初始化完直接循环发一包固定数据int main(void) { uint8_t packet[4] {0xAA, 0x01, 0x02, 0x03}; Delay_Init(); SPI1_Init(); CC1101_Init(); while (1) { CC1101_SendPacket(packet, 4); Delay_Ms(500); } }测试接收端时最好先把接收端的程序也配置好用另一块STM32CC1101模块或者USB转串口模块接CC1101接收程序打开接收模式然后看能不能稳定收到0xAA 01 02 03。如果手头只有一块板子也可以用射频信号分析仪或者SDR看有没有波形但没有条件的话最省事的还是弄两块模块对测。4. 实测问题与排查技巧4.1 最常见的故障现象我调试时遇到的第一类问题是SPI读版本号读不出数据。CC1101有个版本寄存器地址是0x30正常读出来是0x04或者0x14。如果读出来是0x00或者0xFF基本可以确定SPI配置或者硬件连接有问题。常见的具体原因SPI的CPOL/CPHA配置反了、CS引脚没正确控制、模块的VCC和GND接反了、晶体没起振。第二类问题是数据能发射但接收端收不到或者偶尔收到一两包。这种问题大多出在收发两端的配置不一致上尤其是频率、数据率、同步字这三个。频率差太多接收端解调根本锁不到速率不一致即使锁到频谱解调出来的位流也是错的同步字不一致接收端解调完但匹配不上同步头直接丢弃整包数据。第三类问题是距离特别近几米就丢包。除了发射功率寄存器配置偏低之外最常见的原因是天线问题。CC1101是单端天线输出模块上的天线要按对应频段匹配433MHz模块要接433MHz的天线长度差太多效果会大打折扣。另外模块放在桌上时天线附近不要有金属桌面、大面积GND覆铜这些都会严重劣化辐射效率。4.2 排查流程和建议我给一个自己调试时的固定排查顺序照着走通常能快速定位上电后先读版本号0x30确认SPI通信是通的。如果读不到不要往下做任何事先解决硬件和SPI底层问题。用示波器或逻辑分析仪抓SPI时序确认写寄存器时CS时序正确数据位没有错位。初始化完成后读回几组关键配置寄存器比如FREQ2、MDMCFG4确认写入的值和预期一致防止代码里寄存器地址写错。发射时用频谱仪或者SDR看433MHz附近有没有频谱确认射频确实有信号出来。双模块对测时务必两颗模块共地SPI调试时模块之间不共地会导致信号不稳定尤其是两个板子分别供电的时候。排查过程中建议用逻辑分析仪把SCK、MOSI、CS、GDO0四根线一起抓上发一包数据看整个时序流程是否和预期一致。比如CS拉低之后是不是立即发了0x7F地址然后连续发了4个数据字节最后CS拉高GDO0是不是在STX之后一段时间内出现了电平跳变。这个办法比反复改代码看现象高效得多。4.3 实操心得和避坑调试这套系统我个人体会最深的三件事第一不要在网上随便抄一份“万能配置”一定要用SmartRF Studio 7针对自己的晶体频率和频点重新生成。我一开始图省事直接在别人工程里复制了868MHz的配置结果自己的433MHz接收端各种丢包改成工具生成配置后问题当场消失。配置这件事花十分钟跑一下软件比花一天佛系调参靠谱。第二TXFIFO的写入地址0x7F是突发模式意思是发出地址后连续写多少字节都算数据直到CS拉高结束。这和普通寄存器写操作不一样普通寄存器写一个地址只能带一个数据字节而FIFO突发写可以带很多个。很多人写寄存器的函数和写FIFO的函数混着用结果数据写到一半CS就拉高了或者地址字节被当成数据塞进了FIFO现象就是发了数据但接收端解析出来全是乱码。第三发送完成后一定要把芯片拉回IDLE不要让它一直停在TX状态。如果STX后MCU不干预芯片发完当前包会根据MCSM1寄存器的配置决定进入RX还是继续留在TX。如果你后续要连续发多包数据不先回IDLE清FIFO下次写数据前就必须强制先SFTX清一次否则FIFO满了或者残留上一包数据调试时会非常迷惑。我的习惯是每一次发送都严格走“SIDLE - SFTX - 写FIFO - STX - 等待GDO0 - SIDLE”这个闭环一包一包来结构清晰问题也好定位。另一个小技巧是调试时可以把GDO0也接到逻辑分析仪上配合SPI四根线一起看。发送完成时GDO0的波形和代码里等待的逻辑对不对一眼就能看出来比反复猜测状态寄存器里读出来的值可靠很多。等整套逻辑跑稳定了再把这个调试引脚换成其他功能也不迟。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →