概述
NRF24L01/NRF24L01+ 是挪威NordicVLSI公司出品的2.4G射频收发器件,主打高速率,低功耗,应用简单功能。其性能卓越(我说的原厂真货),深受国内开发者欢迎。原厂芯片价格一直比较贵,加上国内不往优质走就往便宜靠这德性,市场上有各种假冒货。也有专业模仿的替换芯片,价格亲民同时品质较好。
说简单易用是相对其它射频芯片,射频芯片本身有一定的门槛,小白阶段调试时遇到收、发不通问题时,缺乏了调试手段,大多都傻眼了。本文尽管往细里讲,希望别人看后少进坑。
硬件
射频芯片对硬件要求比较高,而本人是软件开发,只能简单讲讲。
第一个事情是天线要严格按照参考电路来,除非你是大牛,否则别乱改。我硬件同事用频谱分析仪分析过不同的板子,得到的结论就是人家的参考电路还是比较优质的。
第二个事情就是我们用国产的芯片时,射频连接不稳定,有丢包问题。经过定位是VCC没有经过电容滤波。
准备知识
我的项目是两个NRF24L01芯片分别作为主端和从端,双向通讯。而芯片同一时间只能收,或者只能发,也是就半双工。要实现全双工只能通过软件分时切换。我没有使用芯片的自动应答,自动重发功能,都是由上层软件保障的。更没有用到Enhanced ShockBurst功能。
开工第一步就是看芯片资料,要了解这个芯频率范围,最好设置时能绕过wifi。还有射频参数及对应寄存器的值。发送及接收缓存,通道个数,中断接收等等都要了解清楚,否则容易出问题。
开工前要了解SPI接口,如下图(红框里面的几个管脚):
如果是使用MCU的SPI接口,就要先初始化。如果像我这样直接用GPIO模拟就要初始化管脚的GPIO属性。都要满足以下时序图。
后面的代码会注释如何实现这个时序,这里先跳过。像我这样要使这个芯片TX、RX切换,就一定要了解以下这个图:
上图讲述了芯片的状态机,RX、TX状态切换是需要一点时间的,要遵守。否则会出现问题,我当时软件也有些bug,两个问题加在一起出现各种没收到,发多了塞在rx管道里面,flush还flush不掉之类的诡异现象,说多了都是泪。
下面直接贴代码,注释丰富,仅供参考。
/**********************************************************************************
文件描述 : NRF 24L01芯片驱动
作者 : gavinpeng
时间 : 2017.6.8
***********************************************************************************/
// 头文件,略。。。
#define NRF_ADR_WIDTH 5 // 5 bytes TX(RX) address width
#define TX_PLOAD_WIDTH 32 // 32 bytes TX payload
#define RX_PLOAD_WIDTH 32 // 32 bytes RX payload
#define NRF_RX_DR (1 << 6) // Data Ready Rx FIFO interrupt
#define NRF_TX_DS (1 << 5) // Data Send Tx FIFO
#define NRFW_REG 0x20 // Define write command to register
#define RD_RX_PLOAD_W 0x60
#define RD_RX_PLOAD 0x61 // Define RX payload register address
#define WR_TX_PLOAD 0xA0 // Define TX payload register address
#define FLUSH_TX 0xE1 // Define flush TX register command
#define FLUSH_RX 0xE2 // Define flush RX register command
#define REUSE_TX_PL 0xE3 // Define reuse TX payload register command
#define NOP 0xFF // Define No Operation, might be used to read status register
//***************************************************//
// SPI(nRF24L01) registers(addresses)
#define CONFIG 0x00 // 'Config' register address
#define EN_AA 0x01 // 'Enable Auto Acknowledgment' register address
#define EN_RXADDR 0x02 // 'Enabled RX addresses' register address
#define SETUP_AW 0x03 // 'Setup address width' register address
#define SETUP_RETR 0x04 // 'Setup Auto. Retrans' register address
#define RF_CH 0x05 // 'RF channel' register address
#define RF_SETUP 0x06 // 'RF setup' register address
#define STATUS 0x07 // 'Status' register address
#define OBSERVE_TX 0x08 // 'Observe TX' register address
#define CD 0x09 // 'Carrier Detect' register address
#define RX_ADDR_P0 0x0A // 'RX address pipe0' register address
#define RX_ADDR_P1 0x0B // 'RX address pipe1' register address
#define RX_ADDR_P2 0x0C // 'RX address pipe2' register address
#define RX_ADDR_P3 0x0D // 'RX address pipe3' register address
#define RX_ADDR_P4 0x0E // 'RX address pipe4' register address
#define RX_ADDR_P5 0x0F // 'RX address pipe5' register address
#define TX_ADDR 0x10 // 'TX address' register address
#define RX_PW_P0 0x11 // 'RX payload width, pipe0' register address
#define RX_PW_P1 0x12 // 'RX payload width, pipe1' register address
#define RX_PW_P2 0x13 // 'RX payload width, pipe2' register address
#define RX_PW_P3 0x14 // 'RX payload width, pipe3' register address
#define RX_PW_P4 0x15 // 'RX payload width, pipe4' register address
#define RX_PW_P5 0x16 // 'RX payload width, pipe5' register address
#define FIFO_STATUS 0x17 // 'FIFO Status Register' register address
#define DNYPD 0x1C // 动态包长使能,对应于各个通道
#define NRF_FEATURE 0x1D // 特性
#define NRF_RX_EMPTY 0x0E // rXFIFO为空
#define STA_MARK_RX 0X40
#define STA_MARK_TX 0X20
#define STA_MARK_MX 0X10
// Define SPI pins
sbit CE = P1^2; // Chip Enable pin signal (output)
sbit CSN = P1^1; // Slave Select pin, (output to CSN, nRF24L01)
sbit SCK = P1^0; // Serial Clock pin, (output)
/* 片选函数 */
#define nrf_cs_enable() (CSN = 0)
#define nrf_cs_disable() (CSN = 1)
/* CE(去)使能函数 */
#define nrf_ce_enable() (CE = 1)
#define nrf_ce_disable() (CE = 0)
extern void delay_ms(UINT16 n);
extern void delay_us(UINT16 n);
/**********************************************************
函数描述 : spi接口 gpio 管脚初始化
输入参数 :
输出参数 :
返回值 :
作者/时间: gavinpeng / 2017.6.8
************************************************************/
void nrf_spi_io_init(void)
{
P11_PushPull_Mode; // P11 (CS for tx) 推挽输出
P12_PushPull_Mode; // P12 (CE for tx) 推挽输出
P10_PushPull_Mode; // P10 (SPCLK) 推挽输出
P00_PushPull_Mode; // P00 (MOSI) 推挽输出
P01_Input_Mode; // P01 (MISO) 输入
P11 = 1; //CS for tx
P12 = 0; //CE for tx
P10 = 0; //SPCLK
P00 = 0; //MOSI
clr_SPIEN; // 禁止SPI
}
/**********************************************************
函数描述 : spi接口读写一个字节
输入参数 : val -- 要写出spi 数据寄存器的值
输出参数 :
返回值 : spi 数据寄存器读出数据
作者/时间: gavinpeng / 2017.6.8
************************************************************/
unsigned char nrf_spi_rw(unsigned char val)
{
unsigned char i;
for ( i = 0; i < 8; i++ ) // 循环8次
{
MOSI = (val & 0x80); // byte最高位输出到MOSI
val <<= 1; // 低一位移位到最高位
SCK = 1; // 拉高SCK,nRF24L01从MOSI读入1位数据,同时从MISO输出1位数据
val |= MISO; // 读MISO到byte最低位
SCK = 0; // SCK置低
}
return(val); // 返回读出的一字节
}
/**********************************************************
函数描述 : 写一段寄存器
输入参数 : reg -- 寄存器地址
buf -- 数据首指针
num -- 要写出的数据的字节数
输出参数 :
返回值 : 状态值
作者/时间: gavinpeng / 2017.6.8
************************************************************/
void nrf_buf_write(unsigned char reg, unsigned char *buf, unsigned char num)
{
unsigned char i;
nrf_cs_enable();
/* 写命令要求最高3个bit的值是001 */
(void)nrf_spi_rw(((reg & 0x1F) | NRFW_REG));
for ( i = 0; i < num; i++ )
(void)nrf_spi_rw(buf[i]);
nrf_cs_disable();
return;
}
/**********************************************************
函数描述 : 读芯片 状态 寄存器的值
输入参数 :
输出参数 :
返回值 : 寄存器值
作者/时间: gavinpeng / 2017.6.8
************************************************************/
unsigned char nrf_reg_read_status(void)
{
unsigned char val;
nrf_cs_enable();
/* 读状态寄存器,省略了" & 0x1F" */
(void)nrf_spi_rw(STATUS);
val = nrf_spi_rw(NOP);
nrf_cs_disable();
return val;
}
/**********************************************************
函数描述 : 读芯片寄存器的值
输入参数 : reg -- 寄存器地址
输出参数 :
返回值 : 寄存器值
作者/时间: gavinpeng / 2017.6.8
************************************************************/
unsigned char nrf_reg_read(unsigned char reg)
{
nrf_cs_enable();
/* 读命令要求最高3个bit都是0 */
(void)nrf_spi_rw(reg & 0x1F);
reg = nrf_spi_rw(NOP);
nrf_cs_disable();
return reg;
}
/**********************************************************
函数描述 : 写芯片寄存器的值
输入参数 : reg -- 寄存器地址, 带有读写标志
value -- 要写入的值
输出参数 :
返回值 : 寄存器值
作者/时间: gavinpeng / 2017.6.8
************************************************************/
void nrf_reg_write(unsigned char reg, unsigned char value)
{
nrf_cs_enable();
/* 写命令要求最高3个bit的值是001 */
(void)nrf_spi_rw(((reg & 0x1F) | NRFW_REG));
(void)nrf_spi_rw(value);
nrf_cs_disable();
return;
}
/**********************************************************
函数描述 : 读接收到的数据
输入参数 :
输出参数 : buf -- 缓存
返回值 : 高8位是通道号,低8位是字节数
作者/时间: gavinpeng / 2017.6.8
************************************************************/
unsigned char nrf_payload_read(unsigned char *buf)
{
unsigned char i;
unsigned char len;
/* 1. 读数据长度 */
nrf_cs_enable();
(void)nrf_spi_rw(RD_RX_PLOAD_W);
len = nrf_spi_rw(NOP);
nrf_cs_disable();
if ( len == 0 )
return 0;
/* 2. 读数据 */
nrf_cs_enable();
(void)nrf_spi_rw(RD_RX_PLOAD); /* 读payload命令 */
for ( i = 0; i < len; i++ )
buf[i] = nrf_spi_rw(NOP);
nrf_cs_disable();
return len;
}
/**********************************************************
函数描述 : 发送数据出去
输入参数 : buf -- 缓存
num -- 要写出内容字节数
输出参数 :
返回值 :
作者/时间: gavinpeng / 2017.6.8
************************************************************/
void nrf_payload_write(unsigned char *buf, unsigned char num)
{
unsigned char i;
nrf_cs_enable(); /* 使能 + 延时 */
/* 写命令 */
(void)nrf_spi_rw(WR_TX_PLOAD);
/* 写数据 */
for ( i = 0; i < num; i++ )
(void)nrf_spi_rw(buf[i]);
nrf_cs_disable();
}
/**********************************************************
函数描述 : flush rx fifo
输入参数 :
输出参数 :
返回值 :
作者/时间: gavinpeng / 2017.6.8
************************************************************/
void nrf_flush_buf_rx(void)
{
nrf_cs_enable();
(void)nrf_spi_rw(FLUSH_RX);
nrf_cs_disable();
}
/**********************************************************
函数描述 : flush tx fifo
输入参数 :
输出参数 :
返回值 :
作者/时间: gavinpeng / 2017.6.8
************************************************************/
void nrf_flush_buf_tx(void)
{
nrf_cs_enable();
(void)nrf_spi_rw(FLUSH_TX);
nrf_cs_disable();
}
/**********************************************************
函数描述 : 将模块初始化为接收模式
输入参数 :
输出参数 :
返回值 :
作者/时间: gavinpeng / 2017.6.8
************************************************************/
void nrf_init_mode_rx(void)
{
unsigned char nrf_buf[RX_PLOAD_WIDTH];
unsigned char nrf_buf_len = 0;
unsigned char status = 0;
nrf_ce_disable();
nrf_reg_write(CONFIG, 0x7F);// power up ,RX模式
nrf_ce_enable();
/* 没有接收标志但是有通道号可能是fifo error了, 要flush */
status = nrf_reg_read_status();
if ( ((status & 0x0E) == 0) && ((status & 0x40) == 0) )
{
nrf_buf_len = nrf_payload_read(nrf_buf);
nrf_flush_buf_rx();
}
}
/**********************************************************
函数描述 : 初始化nrf
输入参数 :
输出参数 :
返回值 :
作者/时间: gavinpeng / 2017.6.8
************************************************************/
int nrf_init(void)
{
unsigned char chn = 0;
unsigned char addr_rx[5] = {0xA5, 0x5A, 0xA5, 0x01, 0x01}; // 重点:对端的rx,tx地址跟本端交换
unsigned char addr_tx[5] = {0xA5, 0x5A, 0xA5, 0x02, 0x02};
chn = nrf_chn_get(); // 通道号从别的地方获取
nrf_ce_disable();
nrf_buf_write(TX_ADDR, addr_tx, NRF_ADR_WIDTH); // 要发送的目的地址
nrf_buf_write(RX_ADDR_P0, addr_rx, NRF_ADR_WIDTH); // 自身(接收)地址
// 射频参数要求收、发双方要一致
nrf_reg_write(EN_AA, 0x0); // 去使能通道0的自动应答
nrf_reg_write(EN_RXADDR, 0x01); // Enable Pipe0
nrf_reg_write(SETUP_RETR, 0x10); // 500us + 86us, 0 retrans...
nrf_reg_write(RF_CH, chn); // 接收频率 2400 + chn MHz
nrf_reg_write(RX_PW_P0, TX_PLOAD_WIDTH); // Select same RX payload width as TX Payload width
nrf_reg_write(RF_SETUP, 0x07); // 0x07 TX_PWR:0dBm, Datarate:1Mbps, LNA:HCURR
nrf_reg_write(DNYPD,0x01); // 0通道使能动态长度
nrf_reg_write(NRF_FEATURE, 0x04); // 特性: 使能动态长度
nrf_ce_enable();
return 0;
}
/**********************************************************
函数描述 : 接收从端数据,这个函数会被频繁调用
输入参数 :
输出参数 :
返回值 :
作者/时间: gavinpeng / 2017.6.8
************************************************************/
void nrf_recv_data(void)
{
unsigned char nrf_buf[RX_PLOAD_WIDTH];
unsigned char status;
unsigned char nrf_buf_len;
// 读状态寄存器,如果有数据就接收处理
status = nrf_reg_read_status();
if ( status & 0x40 )
{
nrf_buf_len = nrf_payload_read(nrf_buf);
nrf_reg_write(STATUS, status);
// 长度检查。要注意,因为本项目没有小于2字节的数据帧,所以认为是异常。
if ( (nrf_buf_len < 2) || (nrf_buf_len > RX_PLOAD_WIDTH) )
{
nrf_flush_buf_rx();
return;
}
else
{
// 这里是处理接收到的数据
chan_nrf_recv(nrf_buf, nrf_buf_len);
}
}
}
/**********************************************************
函数描述 : 向对端发送数据
输入参数 : buf -- 缓存
len -- 要写入内容的字节数
输出参数 :
返回值 : 成功返回0,失败返回<0
作者/时间: gavinpeng / 2017.6.8
************************************************************/
int nrf_send_data(unsigned char *buf, unsigned char len)
{
char ret;
unsigned char status = 0;
unsigned short count = 0;
nrf_ce_disable();
/* 数据内容 */
nrf_payload_write(buf, len);
nrf_reg_write(CONFIG, 0x7E); // power up ,TX
nrf_ce_enable();
delay_us(10);
nrf_ce_disable();
// 等发送完成
count = 0;
ret = -1;
while ( ++count < 30000 )
{
status = nrf_reg_read_status();
if ( status & STA_MARK_TX )
{
nrf_reg_write(STATUS, status);
ret = 0;
break;
}
else if ( status & STA_MARK_MX )
{
nrf_reg_write(STATUS, status);
nrf_flush_buf_tx();
break;
}
}
return ret;
}
上面的代码有要注意的地方。
一是发送数据后,通过读状态寄存器知道发送完成,然后返回。实测是对端在更早一点的时候就已经读到数据,大概是这个样子:"一一_____||___一一",红色部分代表对端已收到,本端延时了一点才有寄存器状态变更(我没有使用中断的方式)。
二是跟我没有使用中断的方式有关,我用的轮询方式不断去查是否有收到数据,所以会很频繁调用nrf_recv_data函数。
打完收工。
自己用SI24R1(国产NRF24L01)芯片做的基于2.4G无线串口USB转TTL工具:
https://item.taobao.com/item.htm?spm=a1z38n.10677092.0.0.558c1debH1EHHx&id=579411892273