STM32+CC1101实现433MHz无线发射:硬件接线到寄存器配置实战
2026/9/3 19:57:34 网站建设 项目流程

简介:STM32连接CC1101模块的发射程序是一份经过实际电路测试的无线通信工程代码,面向需要快速实现Sub-GHz无线发射功能的嵌入式开发者。程序基于STM32的SPI总线驱动CC1101,覆盖寄存器初始化、数据打包、FIFO写入与发射启动等关键环节,可直接用于无线传感器网络、遥控系统或物联网节点开发。压缩包共113个文件,约1MB,以h头文件、c源码、o目标文件及hex/axf固件为主,同时包含uvproj工程配置、map映射和lst列表等辅助文件,便于查看编译细节与代码结构。已有784人学习下载,说明这一程序受到一定关注。相比网上常见的残缺驱动,该工程完整且经过硬件验证,学习者可对照实际电路理解SPI通信与CC1101配置流程,也能在现有代码基础上调整频率、功率等参数,适用于中高级嵌入式开发者进行二次开发与调试。 STM32 和 CC1101 这个组合,做无线发射其实比很多人想象中简单,但也比想象中容易踩坑。最近在调一个远程数据采集的小项目,通信方案就选了 CC1101 做 433MHz 无线发射,这两天把 SPI 时序、寄存器配置、发射流程这几个坎儿都趟过去了。这篇文章就把整套发射程序从头到尾拆开讲清楚,包括硬件连接、工程配置、核心代码和实际调试点,如果你手上正好有 STM32 和 CC1101 模块,照着往下抄,大概率一次跑通。

这套方案适合的场景其实挺明确:低速、远距离、低功耗的无线数据传输。比如温湿度采集器、遥控开关、农业大棚监控、智能家居里面的一些传感器节点,都是 CC1101 的典型用途。Sub-1GHz 频段绕射能力强、穿墙效果好,比 2.4G 的 nRF24L01 更适合在这种场景下干活。发射速率不需要高,几百 bps 到几十 kbps 完全够用。最适合的读者就是手里有 STM32 开发板、想快速把无线发射跑起来的人,不需要对射频有多深的理论基础,但基本的 SPI 和寄存器操作得有点概念。

1. 项目背景与整体设计思路

1.1 为什么选 CC1101 而不是其他无线方案

做无线方案选型的时候,大部分人第一反应是 nRF24L01,因为它便宜、资料多、网上代码遍地都是。但我在这个项目里没选它,原因是 2.4GHz 频段在室外复杂环境里绕射能力真的不行,穿一堵墙信号就掉得厉害,更别说隔着几堵墙的工业现场。而 CC1101 工作在 410~464MHz 这个 Sub-1GHz 频段,波长更长,绕射和穿透能力明显更强,空旷环境下配合好一点的天线,收发距离可以跑到几百米甚至更远,接收灵敏度能做到 -112dBm 左右,发射功率也能通过寄存器配置,从 -30dBm 到 +10dBm 可调,功耗控制很灵活。

和 LoRa(比如 SX1278)相比,CC1101 的极限距离确实不如 LoRa,但 LoRa 模块价格更贵、配置也更复杂,而且很多应用根本不需要那几公里的距离。CC1101 的成本和复杂度刚好卡在中间,几十块钱就能买到不错的模块,配置虽然有一堆寄存器,但实际要关心的就那么几个。这个项目里传输的是温湿度和设备状态这种低速数据,我用的是 38.4kbps 的 GFSK 调制,兼顾了传输速率和抗干扰能力,接收端用另一个 CC1101 模块配对接收,验证完全没问题。

1.2 整体任务拆解

整个工程拆开看,其实就四件事。第一件是硬件连接,把 STM32 的 SPI 引脚和 CC1101 模块对应接好,再把 GDO0 状态引脚接出来。第二件是软件工程搭建,用 STM32CubeMX 生成 SPI 基础工程,然后自己封装寄存器读写函数。第三件是参数计算,根据工作频率和晶振频率算出 FREQ、MDMCFG 这些核心寄存器值。第四件是发射流程实现,把数据写进 TX FIFO,发 STX 命令让芯片进入发射状态,再通过 GDO0 引脚确认数据包发送完成。

这里多说一句,很多新手一上来就急着在网上找“现成的完整程序”,结果拿过来发现频率不对、晶振不对、模块不一样,完全跑不通。CC1101 的程序不像普通单片机 GPIO 控制那么“拿来即用”,因为它涉及射频参数,晶振频率差一点、寄存器值差一个字节,结果就是“看着都对了但就是没信号”。所以这篇里我会重点讲清楚参数怎么算、寄存器为什么这么配,这样你换成别的频率或者别的模块也能自己调。

2. 硬件连接与工程配置

2.1 引脚接线

CC1101 模块引脚不多,真正用到的也就 7 根,其中 SPI 占 4 根,GDO0 状态输出必须接,另外就是电源和地。我用 STM32F103 的 SPI1,接线表如下:

CC1101 引脚STM32 引脚说明
CSNPA4片选,软件控制,低电平有效
SCKPA5SPI1 时钟
MOSIPA7主发从收
MISOPA6主收从发
GDO0PA3状态输出,用于判断发送完成
VCC3.3V注意不要接 5V
GNDGND共地

GDO0 这根线特别提醒一下,一定不要省。CC1101 在发送过程中会通过 GDO0 引脚输出状态信号,比如同步字已经发出去、数据包发送完成,MCU 就是靠这个引脚来判断什么时候发完、什么时候能发下一包。如果你不接,就只能靠延时猜时间,要么等太久了浪费效率,要么等太短了数据还没发完就切走了。

电平匹配也要注意。CC1101 模块基本都是 3.3V 供电,STM32F103 的 IO 也是 3.3V 电平,可以直接连。但如果你用的是 5V 供电的单片机(比如 Arduino UNO)去控制它,就必须加电平转换,不然长期使用很容易把芯片搞坏,这不是“临时能用就行”的问题。

2.2 CubeMX 工程配置要点

在 STM32CubeMX 里配置 SPI1 的时候,有几个点需要说清楚。模式选 Full-Duplex Master,硬件 NSS 这里我建议直接 Disable,片选用软件 GPIO 控制。数据宽度 8 位,MSB First,CPOL 和 CPHA 都选 Low 和 1 Edge,也就是 SPI Mode 0,这是 CC1101 要求的通信模式,搞错了通信必然失败。

时钟分频我习惯配成 32 分频,在 72MHz 主频下 SPI 时钟大概是 2.8MHz。CC1101 数据手册标称支持最高 10MHz 的 SPI 时钟,但实际模块走线、芯片批次都有差异,频率拉太高容易出怪问题。无线发射这个场景对 SPI 速率要求真不高,2.8MHz 完全够用,稳定压倒一切。

GPIO 方面,PA3 和 PA4 都设为推挽输出,PA4(片选)初始化为高电平,PA3(GDO0 输入)这里有个细节:我在 CubeMX 里直接把 PA3 配成了 GPIO_Input,用来检测 CC1101 的状态变化。有些朋友会把 GDO0 配成外部中断,这个也可以,但发射流程里轮询读取就够用了,没必要上中断。

2.3 底层 SPI 读写实现

底层 SPI 读写是整个程序的地基,我直接用 HAL 库实现。这里有个小经验:读写两个方向最好封装成一个函数,因为 SPI 是全双工,每次发送一个字节的同时必然收到一个字节,而且 CC1101 的状态字节就是靠这个返回路径带回来的。

uint8_t CC1101_ReadWriteByte(uint8_t data) { uint8_t rx = 0; HAL_SPI_TransmitReceive(&hspi1, &data, &rx, 1, 100); return rx; } void CC1101_CS_LOW(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } void CC1101_CS_HIGH(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }

片选的拉低和拉高之间,我建议稍微给一点延时,尤其是从高到低的瞬间,要确保芯片先进入片选状态再送时钟,否则偶尔会出现第一个字节写不进的情况。调试的时候如果发现寄存器写进去读出来不对,先怀疑片选时序,这个坑我踩过不止一次。

3. CC1101 核心寄存器配置与发射流程

3.1 寄存器读写命令格式

CC1101 的 SPI 读写和普通 SPI 器件不太一样,命令字节本身就有讲究。第一个字节的高两位用来表示操作类型:bit7 是读写方向(1 读、0 写),bit6 是突发模式标志(1 表示突发读写,连续访问多个字节),bit5~bit0 是寄存器地址。

#define CC1101_READ_SINGLE 0x80 #define CC1101_WRITE_BURST 0x40 #define CC1101_READ_BURST 0xC0

我封装了三个最常用的函数:写单字节寄存器、读单字节寄存器、发送命令。

void CC1101_WriteReg(uint8_t addr, uint8_t value) { CC1101_CS_LOW(); CC1101_ReadWriteByte(addr); CC1101_ReadWriteByte(value); CC1101_CS_HIGH(); } uint8_t CC1101_ReadReg(uint8_t addr) { uint8_t value; CC1101_CS_LOW(); CC1101_ReadWriteByte(addr | CC1101_READ_SINGLE); value = CC1101_ReadWriteByte(0xFF); CC1101_CS_HIGH(); return value; } uint8_t CC1101_Strobe(uint8_t cmd) { uint8_t status; CC1101_CS_LOW(); status = CC1101_ReadWriteByte(cmd); CC1101_CS_HIGH(); return status; }

Strobe 命令是 CC1101 特有的一种操作,比如 SIDLE、SFTX、SFRX、STX,这些命令通过单独一个字节就能让芯片切换状态机。注意 Strobe 函数返回的字节其实是芯片状态字,里面包含了当前芯片状态,调试的时候可以读出来看芯片是不是卡在哪个状态里了。

3.2 频率和数据速率参数计算

这就是很多人觉得 CC1101 难的地方,其实公式很简单。频率寄存器 FREQ2、FREQ1、FREQ0 共同组成一个 24 位的值,计算公式是:

FREQ = (目标频率 x 2^16) / 晶振频率

以 433.92MHz 工作频率、26MHz 晶振为例:

(433,920,000 x 65,536) / 26,000,000 约等于 1,093,752

转成十六进制是 0x10B078,所以 FREQ2=0x10、FREQ1=0xB0、FREQ0=0x78。

数据速率配置用的是 MDMCFG4 和 MDMCFG3 两个寄存器,公式是:

R = (256 + DRATE_M) x 2^DRATE_E x f_xosc / 2^28

反过来推,38.4kbps 对应 DRATE_M=0x83、DRATE_E=10,也就是 MDMCFG3=0x83,MDMCFG4 低四位=0x0A。这个手算过程比较繁琐,我实际开发中是先用 TI 官方的 SmartRF Studio 生成一组参考配置,再对照公式把关键参数核对一遍。这个工具是免费的,选好芯片型号、输入晶振频率、工作频率、调制方式和数据速率,它直接给出整套寄存器值,强烈建议每个人都下载一个备用。

下面是这个工程里实际使用的配置数组,433.92MHz、38.4kbps、GFSK、26MHz 晶振,直接复制就能用:

const uint8_t cc1101_cfg[] = { 0x0D, // IOCFG2 GDO2 输出 0x2E, // IOCFG1 高阻 0x06, // IOCFG0 GDO0 同步字检测 0x07, // FIFOTHR FIFO 阈值 0x00, // PKTCTRL1 关闭地址过滤,不附加状态字节 0x32, // PKTCTRL0 可变长度包,CRC 使能,数据白化 0x06, // FSCTRL1 中频频率 0x10, // FREQ2 433.92MHz 配置 0xB0, // FREQ1 0x78, // FREQ0 0x0A, // MDMCFG4 38.4kbps 0x83, // MDMCFG3 0x03, // MDMCFG2 GFSK 调制 0x22, // MDMCFG1 前导码最低 2 字节 0xF8, // MDMCFG0 信道间隔 0x35, // DEVIATN 频偏 0x07, // MCSM2 0x30, // MCSM1 发送完成后回到 IDLE 0x18, // MCSM0 自动校准 0x16, // FOCCFG 0x6C, // BSCFG 0x43, // AGCCTRL2 0x40, // AGCCTRL1 0x91, // AGCCTRL0 0x87, // WOREVT 0xFB, // WORCTRL 0x56, // FREND1 0x11, // FREND0 0xE9, // FSCAL3 0x2A, // FSCAL2 0x00, // FSCAL1 0x1F, // FSCAL0 0x81, // TEST2 0x35, // TEST1 0x09, // TEST0 0xFF, // PATABLE 输出功率表 };

这个数组里面大部分值直接用 SmartRF Studio 生成的,我只改了几个自己关心的:PKTCTRL1 改成了 0x00 关闭地址过滤,这样收发双方不需要配地址;PKTCTRL0 用 0x32 开启可变长度包模式;MCSM1 设为 0x30,让芯片发送完自动回到 IDLE。发射功率表 PATABLE 这里我填的是 0xFF,对应最大发射功率,实际调试的时候如果发现距离太近,先不要急着找硬件问题,检查一下是不是功率没配上去。

3.3 初始化与发射函数实现

初始化函数其实做的事情很少,就是 SPI 准备好之后,把上面的配置数组按顺序写入芯片寄存器。写完后发一个 SIDLE 命令稳妥进入空闲态,再发一个 SFTX 命令清空发送 FIFO。

void CC1101_Init(void) { CC1101_CS_HIGH(); HAL_Delay(10); CC1101_Strobe(CC1101_SIDLE); CC1101_Strobe(CC1101_SFTX); for (uint8_t i = 0; i < sizeof(cc1101_cfg); i++) { CC1101_WriteReg(i, cc1101_cfg[i]); } // 写入 PATABLE,地址 0x3E 是 PATABLE 的起始地址 CC1101_CS_LOW(); CC1101_ReadWriteByte(0x3E | CC1101_WRITE_BURST); CC1101_ReadWriteByte(0xFF); CC1101_CS_HIGH(); CC1101_Strobe(CC1101_SCAL); // 手动校准 HAL_Delay(2); }

发射函数是整个程序的核心,逻辑其实很直白:清空 FIFO、写数据、拉高片选后发 STX 命令、等待 GDO0 指示完成。

void CC1101_TX_Packet(uint8_t *data, uint8_t len) { // 1. 回到 IDLE,清空 FIFO,避免残留数据干扰 CC1101_Strobe(CC1101_SIDLE); CC1101_Strobe(CC1101_SFRX); CC1101_Strobe(CC1101_SFTX); // 2. 写长度字节 + 数据 CC1101_CS_LOW(); CC1101_ReadWriteByte(0x3F | CC1101_WRITE_BURST); // TXFIFO 突发写 CC1101_ReadWriteByte(len); for (uint8_t i = 0; i < len; i++) { CC1101_ReadWriteByte(data[i]); } CC1101_CS_HIGH(); // 3. 进入发送状态 CC1101_Strobe(CC1101_STX <p> <a href="https://download.csdn.net/download/hxg791206/3837855" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

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

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

立即咨询