资讯详情 CC1101 433MHz无线模块寄存器配置与收发调试实战
📅 2026/11/6 7:12:31
简介面向Sub-1GHz无线通信开发者与嵌入式初学者的CC1101收发程序包重点解决芯片初始化、寄存器配置及发送接收链路打通问题可作为智能家居、无线抄表、工业控制等低功耗短距通信原型的参考。压缩包共31个文件约658KB以C源码、头文件、IAR工程与调试配置为主并含汇编文件、批处理脚本及说明文本便于直接编译、在线调试和对照寄存器设置。已有383人学习下载。工程围绕水表计量示例组织涵盖CC1100/CC2500驱动、主程序、LCD与RTC等模块发送与接收代码配套可直接用于联调读者可据此理解SPI配置、数据打包收发和参数调整思路快速复用到自己的无线节点中。1. 一块 433 MHz 模块被“调通”了但真正的麻烦从改参数开始手上这套 CC1101 程序最早只是让两块小板子互相点个灯上电、发一串字节、另一端收到就翻转 IO。真正拿去做产品才会发现问题——换个频点就哑、距离一远就时好时坏、连续跑一晚上第二天不响应了。这份已经调通的 CC1101 工程解决的就是这段“从能跑到敢用”的距离一份完整的寄存器配置表、可复用的初始化与收发流程、GDO 中断与状态机判读以及调不通时按现象反查的排查顺序。适合正在用 433/868 MHz 做无线遥控、传感器采集、低功耗电池节点的嵌入式工程师也适合刚拿到模块、被寄存器手册劝退的新手照着抄一遍。2. 寄存器地图24 位频率字与空口参数的换算CC1101 之所以让新手觉得像“黑匣子”是因为它把几乎所有空口行为都摊在三十多个配置寄存器里任何一个字节写错现象都不是报错而是静默。所以第一步不是写收发而是把“我要什么频点、多快速率、多大频偏、多宽接收带宽”翻译成寄存器值并且能用公式反算回来。2.1 SPI 帧格式与寄存器分类一次读对才能谈调试访问 CC1101 的每一帧都以一个头字节开始位 7 是读写方向位 6 是连续访问标志低 6 位是地址。读单个寄存器写0x80 | addr读连续写0xC0 | addr写单个直接写addr写连续写0x40 | addr。命令选通复位、进 RX、进 TX、清 FIFO 等更简单只发地址、不发数据。这里有个高频误解状态寄存器版本号、MARCSTATE、RSSI、RXBYTES 这些与命令选通共用0x30~0x3D这段地址但读状态寄存器时必须带连续访问位也就是用0xC0 | addr。少写这一位读回来的就是“刚才那条命令有没有被接受”而不是你想要的状态值。先把三个底座函数写对后面所有调试才有意义/* 底层 SPI一次交换一个字节CPOL0, CPHA0MSB first */ static uint8_t spi_xfer(uint8_t dat); /* 写单个配置寄存器 */ void cc1101_write_reg(uint8_t addr, uint8_t val) { CS_LOW(); while (SO_PIN_IS_HIGH()) { } /* 等 CHIP_RDYn说明晶振已起振 */ spi_xfer(addr 0x3F); /* 位70 写位60 单次 */ spi_xfer(val); CS_HIGH(); } /* 读状态寄存器必须带 0xC0否则读到的是命令选通的返回值 */ uint8_t cc1101_read_status(uint8_t addr) { uint8_t v; CS_LOW(); while (SO_PIN_IS_HIGH()) { } spi_xfer(0xC0 | (addr 0x3F)); /* 位71 读位61 连续 */ v spi_xfer(0xFF); CS_HIGH(); return v; }SO就是 MISO它在片选拉低后还会兼作芯片就绪指示高电平表示内部还没准备好此时发过去的字节会被丢掉。很多“读版本号恒为 0x00”的现场问题就出在拉低片选后没有等这一下。0x3F的掩码是为了防止调用方把读写位混进地址值本身应当小于 0x40。2.2 频率字433.92 MHz 是怎么算出来的载波由一个 24 位频率字决定公式是FREQ f_carrier × 2^16 / f_xosc晶振按 26 MHz 算。把结果拆成 FREQ2、FREQ1、FREQ0 三个字节高字节在前。信道偏移不写进 FREQ而是由 CHANNR 乘以信道间隔得到这样改信道不用重算频率字。XTAL 26e6 def freq_words(fc): v round(fc * (1 16) / XTAL) 0xFFFFFF return (v 16) 0xFF, (v 8) 0xFF, v 0xFF def chanspc_words(spacing_hz): # f_chsp XTAL/2^18 * (256M) * 2^E for e in range(4): m round(spacing_hz * (1 18) / (XTAL * (1 e))) - 256 if 0 m 255: return e, m raise ValueError(信道间隔超出可表示范围) print([hex(x) for x in freq_words(433.92e6)]) # [0x10, 0xb0, 0x71] print(chanspc_words(200e3)) # (2, 248) - E2, M0xF8跑出来0x10 0xB0 0x71代回公式是 433.9198 MHz误差在晶振精度以内完全可以接受。chanspc_words里从最小的指数往上找第一个落在 0~255 区间的解就是最省事的一组如果做多信道跳频把 CHANNR 从 0 递增实际频点就是f_carrier CHANNR × spacing不必每个信道存一份配置。注意模块上的晶振实际偏差可能有几十 ppm频点算得再准也压不住晶振本身。批量产品要么选 ±10 ppm 以内的晶振要么在产线上做一次频偏校准并写进设备参数。2.3 数据率、频偏、接收带宽的三方匹配这三个参数是一组不能单独调。数据率决定DRATE_E/DRATE_M频偏决定DEVIATN接收带宽决定CHANBW_E/CHANBW_M。经验规则是接收带宽至少覆盖2 × (频偏 数据率/2)再留 30% 以上余量带宽开太窄频偏漂一点就解不出来开太宽则噪声进来得更多灵敏度反而下降。def drate_words(baud): # R (256M) * 2^E * XTAL / 2^28 best None for e in range(16): m round(baud * (1 28) / (XTAL * (1 e))) - 256 if 0 m 255: err abs((256 m) * (1 e) * XTAL / (1 28) - baud) if best is None or err best[0]: best (err, e, m) return best[1], best[2] def deviatn_words(dev_hz): # f_dev XTAL/2^14 * (8M) * 2^E best None for e in range(8): m round(dev_hz * (1 14) / (XTAL * (1 e))) - 8 if 0 m 7: err abs(XTAL / (1 14) * (8 m) * (1 e) - dev_hz) if best is None or err best[0]: best (err, e, m) return best[1], best[2] print(drate_words(4800)) # (7, 131) - MDMCFG4 低4位7, MDMCFG30x83 print(deviatn_words(20000)) # (0, 5) - DEVIATN0x05, 实际约 20.6 kHz接收带宽按BW f_xosc / (8 × (4 M) × 2^E)反推4.8 kBaud、±20 kHz 频偏这组参数下E2, M3给出约 116 kHz扣掉两侧频偏还剩三十多 kHz 余量实测在温漂 ±20 ppm 的模块上很稳。如果把带宽压到 58 kHz常温能通、放到夏天中午就开始丢包——这类“玄学”现象基本都是带宽余量被吃干净了。2.4 包格式PKTCTRL0/1 里每一位都要对得上包格式不一致是“双方都调通了却互相收不到”的头号原因。PKTCTRL0的低两位决定定长还是变长位 2~3 控制 CRC位 6 是数据白化PKTCTRL1的位 2 决定是否把 RSSI 和 LQI 两个字节追加在 payload 后面。这两处只要收发两端有一个字节不同现象就是“收到数据但全是乱的”或者“长度字段离谱”。寄存器本项目取值含义改错的后果PKTCTRL00x05变长包 CRC 使能 不白化定长端会把长度字节当数据PKTCTRL10x04追加 RSSI/LQI少读两字节导致下一包错位PKTLEN0x20定长模式下最大 32 字节超过会被截断SYNC1/SYNC00xD3/0x914 字节同步字的高两位两端不同则永远等不到同步MDMCFG20x13GFSK 30/32 同步检测改成 OOK 端完全收不到同步字实际是前导码之后的 16 位可扩展为 32 位接收端靠它对齐比特边界。前导码长度由MDMCFG1的 NUM_PREAMBLE 决定4.8 kBaud 下用 4 字节对应 8 字节前导足够让对端 AGC 收敛如果把前导砍到 2 字节实验室里能通一进金属外壳就大量丢包。3. 把配置落进固件初始化、发送、接收三段代码公式算完只是纸面参数真正让模块跑起来的是上电时序和 FIFO 操作顺序。这一章的三段代码可以直接替换掉例程里那些“能跑但不知道为什么”的部分。3.1 上电复位与批量写配置上电后第一件事是发 SRES 命令选通等晶振稳定再写配置。配置表用二维数组存写完回读 VERSION 校验0x14是 CC1101 的版本号不同批次可能是 0x04、0x14 等只要不是 0x00/0xFF 就说明 SPI 通了。static const uint8_t cfg[][2] { {0x00, 0x06}, /* IOCFG2: GDO2 在同步字处拉高、包结束拉低 */ {0x02, 0x06}, /* IOCFG0: 同上给 GDO0 用 */ {0x03, 0x47}, /* FIFOTHR: RX 阈值 32TX 阈值 33 */ {0x04, 0xD3}, /* SYNC1 */ {0x05, 0x91}, /* SYNC0 */ {0x06, 0x20}, /* PKTLEN: 32 */ {0x07, 0x04}, /* PKTCTRL1: 追加 RSSI/LQI不做地址过滤 */ {0x08, 0x05}, /* PKTCTRL0: 变长 CRC */ {0x0B, 0x06}, /* FSCTRL1: 中频约 152 kHz */ {0x0D, 0x10}, {0x0E, 0xB0}, {0x0F, 0x71}, /* 433.92 MHz */ {0x10, 0xB7}, /* MDMCFG4: 带宽 116 kHz, DRATE_E7 */ {0x11, 0x83}, /* MDMCFG3: DRATE_M131 - 4.8 kBaud */ {0x12, 0x13}, /* MDMCFG2: GFSK 30/32 同步 */ {0x13, 0x48}, /* MDMCFG1: 8 字节前导, CHANSPC_E2 */ {0x14, 0xF8}, /* MDMCFG0: 信道间隔 200 kHz */ {0x15, 0x05}, /* DEVIATN: 约 20.6 kHz */ {0x17, 0x0C}, /* MCSM1: 收完留在 RX发完回 IDLE */ {0x18, 0x18}, /* MCSM0: IDLE-RX/TX 自动校准 */ {0x19, 0x1D}, {0x1A, 0x1C}, /* 频偏补偿 */ {0x1B, 0xC7}, {0x1C, 0x00}, {0x1D, 0xB2}, /* AGC */ {0x21, 0xB6}, {0x22, 0x10}, /* FREND1/FREND0: 功率档 0 */ {0x23, 0xEA}, {0x24, 0x2A}, {0x25, 0x00}, {0x26, 0x1F}, /* 频率校准 */ {0x2C, 0x88}, {0x2D, 0x31}, {0x2E, 0x0B}, /* TEST2/1/0: 433 MHz 专用 */ }; int cc1101_init(void) { CS_HIGH(); delay_ms(10); /* 等电源和晶振稳定 */ cc1101_strobe(0x30); /* SRES */ delay_ms(2); for (int i 0; i sizeof(cfg) / sizeof(cfg[0]); i) cc1101_write_reg(cfg[i][0], cfg[i][1]); cc1101_write_burst(0x3F, pa_table, 8); /* PATABLE: 8 档功率 */ uint8_t ver cc1101_read_status(0x31); /* VERSION */ return (ver 0x00 || ver 0xFF) ? -1 : 0; }cfg里的 TEST2/TEST1/TEST0 三个寄存器是原厂针对 433 MHz 频段给的固定值别随手改成 868 MHz 的那一组否则发射频谱会明显变差。pa_table是 8 字节功率表索引由FREND0的低 3 位选择433 MHz 下常用0xC0作为最大档具体数值以模块的功放和天线匹配为准。cc1101_init返回 -1 时不要继续往下跑先解决 SPI 通路否则后面所有“收不到”都是假象。注意SPWD0x39进入睡眠后配置寄存器内容不保证保留唤醒后要么重写整张表要么只用 SIDLE 而不用 SPWD 做低功耗。3.2 发送FIFO 填充与 TX 完成的判定发送流程是固定套路进 IDLE、清 TX FIFO、写 payload、发 STX、等结束。坑在于“等结束”这一句——TXBYTES读到 0 只代表 FIFO 空了最后几个比特可能还在空中。稳妥的做法是等 MARCSTATE 回到 IDLE或者用 GDO0 在包结束时的下降沿做中断。int cc1101_send(const uint8_t *buf, uint8_t len) { if (len 0 || len 61) return -2; /* FIFO 只有 64 字节要留头 */ cc1101_strobe(0x36); /* SIDLE */ cc1101_strobe(0x3B); /* SFTX 清 TX FIFO */ cc1101_write_reg(0x3F, len); /* 变长模式首字节是长度 */ cc1101_write_burst(0x3F, buf, len); cc1101_strobe(0x35); /* STX */ uint32_t t0 millis(); while (millis() - t0 50) { /* 50 ms 超时保护 */ uint8_t st cc1101_read_status(0x35); /* MARCSTATE */ if (st 0x01) return 0; /* 回到 IDLE 才算发完 */ if (st 0x11 || st 0x10) continue;/* TX / 切换中 */ delay_ms(1); } cc1101_strobe(0x36); return -1; }FIFO 深度 64 字节变长模式下第一个字节被长度占用所以单包有效载荷上限按 63 算更安全代码里取 61 是给后续可能的地址字节留位置。50 ms 超时是兜底4.8 kBaud 发 62 字节大约 130 ms实际项目里要根据速率放大这个值超时后必须发 SIDLE否则状态机会卡在 TX 半个周期。MARCSTATE的0x11是 TX、0x01是 IDLE中间还有切换态所以判断要写成“等它变成 IDLE”不能写成“等它不等于 TX”。3.3 接收中断、读 FIFO 与追加字节接收端建议用 GDO 中断而不是死轮询。配置成0x06后GDO 在检测到同步字时拉高、包结束时拉低正好给 MCU 一个“整包已入 FIFO”的信号。中断里只置标志实际读取放到主循环避免在中断里做长 SPI 操作。volatile uint8_t rx_flag 0; void EXTI_Handler(void) { if (GDO0_IS_LOW()) rx_flag 1; } int cc1101_recv(uint8_t *buf, uint8_t *rssi, uint8_t *lqi) { uint8_t n cc1101_read_status(0x3B) 0x7F; /* RXBYTES */ if (n 0 || n 64) { cc1101_strobe(0x3A); return -1; } /* SFRX */ uint8_t len; cc1101_read_burst(0x3F, len, 1); /* 变长模式首字节 */ if (len 0 || len 2 n) { cc1101_strobe(0x3A); return -1; } cc1101_read_burst(0x3F, buf, len); cc1101_read_burst(0x3F, rssi, 1); /* APPEND_STATUS 追加 */ cc1101_read_burst(0x3F, lqi, 1); cc1101_strobe(0x3A); /* SFRX 清残留 */ return len; }RXBYTES的位 7 是溢出标志掩掉再判断长度。len 2 n这句是防错位的关键如果上电时序没对齐或者上一包没读完长度字节可能是垃圾值此时直接清 FIFO 重来比继续解析安全得多。追加的 RSSI 是补码形式换算成 dBm 时分两段大于等于 128 时dBm rssi/2 - 74小于 128 时dBm (rssi - 256)/2 - 74。LQI 的低 7 位是链路质量位 7 是 CRC 校验结果为 0 说明这包 CRC 没过应当丢弃。4. GDO 引脚与状态机中断、轮询和低功耗的取舍同一份配置用轮询能跑用中断也能跑功耗差一个数量级。选哪种取决于节点是插电还是电池供电而这个决定会反过来影响你怎么读状态。4.1 MARCSTATE 与 FIFO 计数的正确读法状态机的关键值不多0x01IDLE、0x0DRX、0x11TX其余是校准、切换、结束之类的过渡态。判断“能不能发下一包”要看是否回到 IDLE判断“收到没有”不要看 MARCSTATE而要看 RXBYTES 和 GDO 沿。把这两件事混在一起就会出现“状态显示 RX 但一直接不到数据”的错觉因为芯片确实在 RX只是没等到同步字。TXBYTES和RXBYTES都是 7 位有效位 7 是溢出/欠载标志。读取时先读一次计数再连续读 FIFO中间不要插别的命令否则计数会变。需要更细的调试信息时可以读PKTSTATUS0x38它的位 4 表示当前是否处于 GDO0 有效状态位 0 表示是否检测到同步字。4.2 GDO0/GDO2 的四种常用映射两个 GDO 引脚可以映射成十几种内部信号实际项目里常用的就四种IOCFG 值信号典型用途0x06同步字到达拉高包结束拉低收包中断、唤醒 MCU0x00RX FIFO 达到阈值 / TX FIFO 低于阈值大包分段搬运0x07检测到前导码信道活动检测、快速唤醒0x2E高阻态不用时关掉省电也防干扰收包用0x06最省心因为它是“整包就绪”本文还有配套的精品资源点击获取