简介:这套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 STM32+CC1101能做什么,为什么选这套组合
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引脚 | 说明 |
|---|---|---|
| PA5(SPI1_SCK) | SCK | SPI时钟 |
| PA7(SPI1_MOSI) | SI | 主机输出,从机输入 |
| PA6(SPI1_MISO) | SO | 从机输出,主机输入 |
| PA4(普通GPIO) | CS | 片选,低电平有效 |
| PC7(普通GPIO输入) | GDO0 | 发送完成/接收中断状态输出 |
| 3.3V | VCC | 供电 |
| GND | GND | 共地 |
这里要注意几点。第一,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是“开始发射”STX,0x36是“开始接收”SRX,0x34是进入IDLE状态。
SPI速率先不要拉太高,STM32的SPI1分频到3.25MHz左右足够用,CC1101虽然支持到10MHz,但速度高了对布线要求也高,调试阶段没必要给自己找麻烦。另外SPI模式必须是CPOL=0、CPHA=0,也就是空闲时钟为低、第一个边沿采样,这个是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 = 0x10,FREQ1 = 0xA5,FREQ0 = 0x31。这一步如果你手里晶振不是标准的26MHz,比如用的是24MHz或者其他频点,算出来的值就不一样。不过好在TI有官方工具可以自动生成,不需要手算。
数据率和调制方式同样不用硬算,它们分散在MDMCFG4、MDMCFG3、MDMCFG2这些寄存器里,涉及分频系数、信道间隔、同步字模式、调制格式等一堆位域,手算非常容易出错。发射和接收两端的数据率必须一致,否则哪怕只差一点点,接收端也只是偶尔能解调出一两个字节,然后疯狂丢包。
2.3 配置寄存器怎么获取与下发
最好的办法是用TI官方的SmartRF Studio 7软件。选好CC1101型号,填上晶振频率,把目标频点、数据率、调制方式、信道间隔这些参数拖一拖,软件会立刻生成完整寄存器配置表,照着抄进代码里就行。这比任何网上流传的“万能配置表”都靠谱,因为不同模块的晶体频率、板子走线分布电容都不一样,直接抄别人配置有时候能通,但频偏可能跟你实际板子对不上。
下发配置的方式也值得注意:不要上电后立刻一次把所有寄存器写完就认为初始化完成了。正确做法是,芯片上电后先等待晶振稳定,然后往配置寄存器表里逐个写入值,最后执行一次手动校准。手动校准的操作是:确认芯片处于IDLE状态,写FSCAL3=0xE9、FSCAL2=0x2A、FSCAL1=0x00、FSCAL0=0x1F,再发送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); } }测试接收端时,最好先把接收端的程序也配置好,用另一块STM32+CC1101模块或者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的波形和代码里等待的逻辑对不对,一眼就能看出来,比反复猜测状态寄存器里读出来的值可靠很多。等整套逻辑跑稳定了,再把这个调试引脚换成其他功能也不迟。
本文还有配套的精品资源,点击获取