资讯详情

资讯详情

PIC16F676 软件 SPI 驱动 nRF24L01 无线收发实战

简介这份资源面向嵌入式初学者与PIC单片机开发者提供16F676通过SPI接口驱动nRF24L01无线模块完成收发通信的完整程序可用于智能家居、无线遥控、物联网设备等短距离2.4GHz通信场景的学习与二次开发。压缩包共22个文件约56KB以C源码、头文件、HEX烧录文件、MPLAB工程文件及编译中间文件为主另含少量说明文本覆盖从源码到可烧录固件的完整链路。目前已有324人学习下载。读者可从中获取SPI初始化、nRF24L01工作模式与通道频率配置、数据收发缓冲区操作、中断标志处理及CRC错误与重传监控等关键实现思路并借助工程文件在MPLAB X IDE中直接编译调试快速理解PIC单片机与无线模块协同工作的软硬件配合方式。1. 用 PIC16F676 驱动 nRF24L018 位小单片机也能跑无线收发手头只有一颗 8 引脚、Flash 才 1K 字的 PIC16F676却想让它通过 nRF24L01 收发数据这事听起来有点勉强但确实能跑通。我最早做这个组合是因为一个电池供电的遥控采集节点主控只能用最便宜的小片子无线部分又不想用红外或者 433 超再生那种玄学方案最后选了 nRF24L01。PIC16F676 没有硬件 SPI所有时序都得靠软件模拟这是整个方案里最需要耐心的地方。这篇文章面向的是手里有 PIC16F676 和 nRF24L01 模块、想自己写收发程序但被时序和寄存器卡住的嵌入式开发者。我会把 SPI 模拟、寄存器配置、收发流程、常见翻车点都拆开讲代码可以直接抄去改。2. 先搞清楚 PIC16F676 和 nRF24L01 的硬件边界2.1 为什么这两颗芯片搭配起来需要特别处理PIC16F676 是 Microchip 的一款 8 位闪存单片机14 引脚封装实际可用 I/O 是 12 个1K 字程序存储器64 字节 RAM没有硬件 SPI、没有硬件 UART只有一个 8 位定时器和比较器。nRF24L01 是 Nordic 的 2.4GHz 收发芯片通过 SPI 接口访问内部寄存器支持 125 个频道、6 个管道、自动应答和自动重传。两者放在一起最大的矛盾在于nRF24L01 的 SPI 时钟最高可以到 10MHz但 PIC16F676 在 4MHz 晶振下指令周期是 1μs软件模拟 SPI 时一个时钟周期至少需要几个指令周期实际能跑到的 SPI 速率大概在 100kHz 到 250kHz 之间。这个速率对 nRF24L01 来说完全够用因为它的寄存器操作本身就不需要高速真正影响的是收发切换时的响应延迟。另一个边界是 RAM。PIC16F676 只有 64 字节 RAMnRF24L01 的一包数据最多 32 字节加上状态变量、指针、临时缓冲很容易就把 RAM 吃满。我一般会留出至少 8 字节给栈和临时变量实际可用于收发缓冲的也就 40 字节左右。这意味着你不能在中断里做大数组操作也不能嵌套太多函数调用。2.2 引脚分配与最小系统接线PIC16F676 的引脚不多必须精打细算。下面是我常用的接线方案用 PORTA 和 PORTC 的部分引脚PIC16F676 引脚功能接 nRF24L01 引脚说明RA0模拟输入/数字 I/O不接留作电池电压检测RA1数字 I/OCE使能收发模式RA2数字 I/OCSN片选低有效RA3数字 I/OSCKSPI 时钟RA4数字 I/OMOSI主机输出RA5数字 I/OMISO主机输入RC0数字 I/OIRQ中断输出可选VDD电源VCC3.3VVSS地GND共地注意 nRF24L01 的供电是 3.3V不能直接接 5V。PIC16F676 如果跑在 5VSPI 信号线上需要做电平转换最简单的方法是在 MOSI、SCK、CSN、CE 上串 1kΩ 电阻再并 3.3V 稳压管或者直接用 3.3V 给 PIC 供电。我一般让整个系统跑 3.3V省掉电平转换的麻烦。2.3 软件模拟 SPI 的时序要求nRF24L01 的 SPI 模式是 Mode 0CPOL0CPHA0也就是时钟空闲为低数据在上升沿采样。PIC16F676 模拟 SPI 时需要手动控制 SCK 的高低和数据的输出/采样。下面是最基础的三个函数// SPI 写一个字节返回同时读到的字节 unsigned char spi_transfer(unsigned char data) { unsigned char i; unsigned char recv 0; for (i 0; i 8; i) { // 准备 MOSI先放数据再拉高 SCK if (data 0x80) MOSI 1; else MOSI 0; data 1; SCK 1; // 上升沿nRF24L01 采样 MOSI __delay_us(1); // 保持高电平至少 1us recv 1; if (MISO) recv | 0x01; // 下降沿前读取 MISO SCK 0; // 下降沿 __delay_us(1); } return recv; }这段代码里__delay_us(1)是为了保证 SCK 高低电平各维持至少 1μs对应大约 250kHz 的 SPI 时钟。如果你把 PIC 超频到 8MHz可以把延时缩短但没必要因为 nRF24L01 的寄存器操作对速度不敏感。关键点是数据必须在 SCK 上升沿之前稳定MISO 在 SCK 下降沿之前读取。很多新手翻车是因为先拉高 SCK 再改 MOSI导致 nRF24L01 采样到错误的数据。3. nRF24L01 的寄存器配置与收发流程3.1 必须配置的五个核心寄存器nRF24L01 有 20 多个寄存器但实际做点对点收发只需要重点配置下面五个寄存器地址名称配置值示例作用0x00CONFIG0x0E 或 0x0F使能 CRC、16 位 CRC、上电、收发模式0x01EN_AA0x01使能管道 0 自动应答0x02EN_RXADDR0x01使能管道 0 接收0x03SETUP_AW0x03地址宽度 5 字节0x05RF_CH0x4C射频频道 762.476GHz0x06RF_SETUP0x070dBm 发射功率、2Mbps 速率CONFIG 寄存器的 0x0E 表示EN_CRC1CRCO116 位 CRCPWR_UP1PRIM_RX0发射模式。如果要切到接收模式把最低位改成 1即 0x0F。RF_SETUP 的 0x07 表示 2Mbps 速率、0dBm 功率如果通信距离不够可以改成 0x061Mbps或 0x05250kbps速率越低灵敏度越高。3.2 写寄存器与读寄存器的命令格式nRF24L01 的 SPI 命令分两类写寄存器是 0x20 | 寄存器地址读寄存器是 0x00 | 寄存器地址。写数据到发送缓冲区是 0xA0读接收缓冲区是 0x61。下面是对应的封装函数// 写一个寄存器 void nrf_write_reg(unsigned char reg, unsigned char value) { CSN 0; // 片选拉低 spi_transfer(0x20 | reg); // 写命令 寄存器地址 spi_transfer(value); // 写入的值 CSN 1; // 片选拉高锁存 } // 读一个寄存器 unsigned char nrf_read_reg(unsigned char reg) { unsigned char value; CSN 0; spi_transfer(0x00 | reg); // 读命令 寄存器地址 value spi_transfer(0xFF); // 发送哑数据同时读回 CSN 1; return value; } // 写发送缓冲区payload 是数据指针len 是长度 void nrf_write_payload(unsigned char *payload, unsigned char len) { unsigned char i; CSN 0; spi_transfer(0xA0); // 写 TX FIFO 命令 for (i 0; i len; i) spi_transfer(payload[i]); CSN 1; }注意spi_transfer(0xFF)里的 0xFF 是哑数据因为 SPI 是全双工主机必须发送一个字节才能产生时钟从而读回从机的数据。CSN 必须在整个命令序列期间保持低电平命令字节和数据字节之间不能拉高否则 nRF24L01 会认为命令结束。3.3 发射流程与接收流程的代码实现发射一包数据的完整流程是拉低 CE 进入待机、写 TX 地址、写 payload、拉高 CE 触发发射、等待 IRQ 或查询 STATUS 寄存器。接收流程是配置为接收模式、拉高 CE、等待 IRQ、读 payload、清中断标志。下面是简化后的收发函数// 发射一包数据返回 1 表示成功0 表示失败 unsigned char nrf_tx_packet(unsigned char *data, unsigned char len) { unsigned char status; CE 0; nrf_write_reg(0x00, 0x0E); // 发射模式 nrf_write_reg(0x04, 0x00); // 清 STATUS 中断标志 nrf_write_payload(data, len); // 写数据到 TX FIFO CE 1; // 触发发射 __delay_us(20); // 保持 CE 高至少 10us CE 0; // 等待发射完成最多等 10ms for (unsigned int timeout 0; timeout 1000; timeout) { status nrf_read_reg(0x07); // 读 STATUS if (status 0x20) // TX_DS 置位发射成功 return 1; if (status 0x10) // MAX_RT 置位重传失败 return 0; __delay_us(10); } return 0; } // 接收一包数据返回数据长度0 表示没收到 unsigned char nrf_rx_packet(unsigned char *buf) { unsigned char status, len; status nrf_read_reg(0x07); if (status 0x40) { // RX_DR 置位收到数据 CSN 0; spi_transfer(0x61); // 读 RX FIFO 命令 len spi_transfer(0xFF); // 第一个字节是长度 for (unsigned char i 0; i len; i) buf[i] spi_transfer(0xFF); CSN 1; nrf_write_reg(0x07, 0x40); // 清 RX_DR 标志 return len; } return 0; }发射函数里CE 拉高后必须保持至少 10μsnRF24L01 才会真正启动发射。等待 TX_DS 或 MAX_RT 的超时时间我设了 10ms实际在 2Mbps 速率下一包 32 字节的数据发射时间不到 200μs加上自动重传10ms 足够。接收函数里读 RX FIFO 的第一个字节是数据长度这个长度是发射端写入的 payload 长度不是固定值。4. 避坑与常见问题排查4.1 现象发射端一直返回失败STATUS 的 MAX_RT 置位原因通常是接收端没有上电、地址不匹配、频道不一致或者自动应答配置错误。nRF24L01 的自动重传机制要求接收端开启 EN_AA 并且地址匹配否则发射端会一直重传直到 MAX_RT。解决方法是先用nrf_read_reg读回接收端的 CONFIG、EN_AA、RF_CH、RX_ADDR_P0 四个寄存器确认和发射端一致。特别是 RX_ADDR_P0 必须和发射端的 TX_ADDR 相同很多人只配了 TX_ADDR 忘了配 RX_ADDR_P0。4.2 现象接收端收不到数据但发射端显示成功这种情况多半是接收端的 CE 没有拉高或者 PRIM_RX 位没有置 1。nRF24L01 在接收模式下CE 必须保持高电平才能持续监听。另外接收端的 EN_RXADDR 必须使能对应的管道如果用的是管道 0EN_RXADDR 的最低位必须是 1。我遇到过因为nrf_write_reg(0x02, 0x01)写成了0x00接收端完全不响应查了半天才发现是寄存器值写错。4.3 现象通信距离很短几厘米就断先检查电源。nRF24L01 在发射瞬间电流会冲到 12mA 以上如果电源去耦不好电压会跌落导致芯片复位。我一般会在 nRF24L01 的 VCC 和 GND 之间并一个 10μF 钽电容和一个 100nF 陶瓷电容越靠近模块越好。其次检查 RF_SETUP 的速率和功率设置2Mbps 速率虽然快但灵敏度低改成 1Mbps 或 250kbps 能明显增加距离。最后检查天线PCB 天线周围不能铺地也不能有金属遮挡。4.4 现象SPI 读写寄存器返回值全是 0x00 或 0xFF这是典型的 SPI 时序问题。先用示波器看 SCK、MOSI、CSN 三根线确认 CSN 拉低后 SCK 才有波形MOSI 在 SCK 上升沿之前已经稳定。如果没有示波器可以写一个最简单的测试向 CONFIG 寄存器写 0x0E再读回来如果读回的不是 0x0E说明 SPI 通信本身有问题。常见原因是 MISO 方向配置错误PIC16F676 的 TRIS 寄存器没有把 MISO 对应的引脚设为输入。4.5 现象程序跑一段时间后死机或数据错乱PIC16F676 的 RAM 只有 64 字节如果收发缓冲定义得太大或者中断里用了太多局部变量栈会溢出。我一般把收发缓冲定义为全局数组大小不超过 32 字节中断里只置标志位不做数据处理。另外nRF24L01 的 STATUS 寄存器在每次操作后最好读一次并清除否则中断标志累积会导致误判。5. 进阶技巧用状态机把收发逻辑串起来5.1 为什么裸写收发函数容易翻车上面给的nrf_tx_packet和nrf_rx_packet是阻塞式的发射时要等 10ms接收时要轮询。在实际项目里如果主循环还有其他任务比如读传感器、驱动数码管阻塞式收发会让整个系统响应变慢。更麻烦的是如果发射失败需要重发或者接收和发射需要切换裸写函数很容易把状态搞乱。我早期做的一个遥控器就是因为发射函数里等了太久按键响应延迟到肉眼可见。5.2 用状态机管理收发切换状态机的思路是把收发过程拆成几个状态空闲、配置为发射、写数据、等待发射完成、配置为接收、等待接收。每个状态只做一件事主循环每次进来推进一步。下面是一个简化的状态机框架// 收发状态定义 typedef enum { STATE_IDLE, STATE_TX_SETUP, STATE_TX_SEND, STATE_TX_WAIT, STATE_RX_SETUP, STATE_RX_WAIT } rf_state_t; rf_state_t rf_state STATE_IDLE; unsigned char tx_buf[32]; unsigned char tx_len; unsigned char rx_buf[32]; unsigned char rx_len; // 主循环里调用每次推进一步 void rf_task(void) { unsigned char status; switch (rf_state) { case STATE_IDLE: // 空闲时切到接收模式 rf_state STATE_RX_SETUP; break; case STATE_TX_SETUP: CE 0; nrf_write_reg(0x00, 0x0E); // 发射模式 nrf_write_reg(0x04, 0x00); // 清中断 nrf_write_payload(tx_buf, tx_len); rf_state STATE_TX_SEND; break; case STATE_TX_SEND: CE 1; __delay_us(20); CE 0; rf_state STATE_TX_WAIT; break; case STATE_TX_WAIT: status nrf_read_reg(0x07); if (status 0x20) { // 发射成功 nrf_write_reg(0x07, 0x20); rf_state STATE_RX_SETUP; } else if (status 0x10) { // 重传失败 nrf_write_reg(0x07, 0x10); rf_state STATE_RX_SETUP; } // 否则继续等不改变状态 break; case STATE_RX_SETUP: CE 0; nrf_write_reg(0x00, 0x0F); // 接收模式 nrf_write_reg(0x07, 0x40); // 清 RX_DR CE 1; rf_state STATE_RX_WAIT; break; case STATE_RX_WAIT: status nrf_read_reg(0x07); if (status 0x40) { // 收到数据 rx_len nrf_rx_packet(rx_buf); rf_state STATE_IDLE; } break; } }这个状态机里STATE_TX_WAIT和STATE_RX_WAIT不会阻塞每次调用只查一次 STATUS没完成就下次再查。主循环可以以 1ms 或更快的周期调用rf_task按键和传感器处理放在主循环其他位置互不干扰。注意STATE_TX_WAIT里没有超时计数实际使用时需要加一个计数器超过一定次数强制回到STATE_RX_SETUP否则发射失败会卡死。5.3 用 IRQ 引脚减少轮询开销如果 PIC16F676 的引脚够用可以把 nRF24L01 的 IRQ 接到 RC0 并配置外部中断。IRQ 在发射成功、接收成功、重传失败时都会拉低中断里只置一个标志位主循环根据标志位决定下一步。这样rf_task里就不需要频繁读 STATUS 寄存器SPI 总线占用更少。PIC16F676 的 RB0/INT 引脚也可以用作外部中断但要注意中断触发边沿配置。5.4 地址和频道的动态切换点对点通信时地址和频道固定就行。但如果要做一对多或者避免干扰可以在运行时切换。nRF24L01 的 RX_ADDR_P0 到 RX_ADDR_P5 可以分别设置不同地址发射时通过 TX_ADDR 指定目标。频道切换直接写 RF_CH 寄存器即可但切换后需要重新进入接收模式。我一般把频道和地址做成一个配置表存在 PIC16F676 的 EEPROM 里上电时读出来初始化。5.5 一个我踩过的坑CE 和 CSN 的时序关系最后说一个血泪经验。nRF24L01 的 CE 和 CSN 不能同时拉低否则芯片会进入不确定状态。正确的顺序是先拉低 CSN 做 SPI 操作操作完拉高 CSN再拉高 CE 触发发射或接收。我早期写代码时在nrf_write_payload里忘了拉高 CSN紧接着就拉高 CE结果 nRF24L01 一直不工作查了两天才发现是 CSN 没有释放。后来我养成了一个习惯每个 SPI 操作函数结束时都强制CSN 1不管之前是什么状态。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →