我用VSCODE中的Platformio中的arduino框架开发。
先说几点感悟,感觉这个模块有点玄学,我是淘宝买的模块,不知道是否质量稳定。
有时程序仅仅加上读FIFO寄存器的一行代码,就通讯失灵;同一块版烧录同一段代码,有时通讯成功有时失败,不知为何。
我没用库,自己写的驱动程序,感觉效果也不错,多长的句子也能立刻接收。
#include <Arduino.h>
#include <SPI.h>
#include "nrf24l01.h"
#include <string>
/*ESP32普通版是接收机*/
#define pinCE 32 //这两个引脚要单独定义,其他引脚直接用SPI库的默认定义就可。
#define pinIRQ 33
SPIClass *vspi = NULL; //这行代码就能启动SPI库的通讯协议了
void write_Register(uint8_t addr, uint8_t data);
void write_Register(uint8_t addr, uint8_t* data, uint8_t data_len);
void write_Register(uint8_t cmd);
uint8_t read_Register(uint8_t addr);
void read_Register(uint8_t addr, uint8_t byte_num, uint8_t *a);
void RX_Payload(char *rec_data, uint8_t len);
void TX_Payload(const char data[]);
void carrier_wave();
void init_PTX();
void init_PRX();
void check_SPI();
uint8_t TX_ADDRESS[3]={0x1A,0x1A,0x1A};
uint8_t RX_ADDRESS[3]={0x1A,0x1A,0x1A};
bool wave_detected=false;
上面是本次项目的所有函数了。
接收机主函数:
void setup() {
Serial.begin(115200);
vspi = new SPIClass(VSPI);
vspi->begin(14,12,13,15); // 绑定端口引脚
pinMode(vspi->pinSS(), OUTPUT);
pinMode(pinCE, OUTPUT);
pinMode(pinIRQ, INPUT);
init_PRX();
attachInterrupt(pinIRQ,carrier_wave,FALLING);
digitalWrite(pinCE,HIGH);
write_Register(FLUSH_RX);
}
void loop() {
if (wave_detected)
{
char *rec_data = new char[32]{0};
uint8_t len = read_Register(R_RX_PL_WID);
RX_Payload(rec_data, len);
uint8_t i;
for (i = 0; i < len; i++)
Serial.print(rec_data[i]);
delete[] rec_data;
write_Register(STATUS_NRF, _BV(RX_DR) | _BV(TX_DS)); // 清除RX中断标志
write_Register(FLUSH_RX);
wave_detected = false;
}
}
接收机我用了中断,nrf24l01的IRQ脚在接收完成正确的信号后会从高电平转低电平,触发中断。接收机只有一种情况会中断,就是RX_DR=1时。注意esp32的中断引脚不要设置成INPUT_PULLUP,强上拉效力有点强,好多时候导致nrf24l01失灵。
这次通讯我用了nrf24l01的动态字节长度功能,所以接收机需要先用R_RX_PL_WID读取字节长度。
void write_Register(uint8_t addr, uint8_t data)
{
digitalWrite(vspi->pinSS(), LOW);
vspi->transfer(W_REGISTER | addr);
vspi->transfer(data);
digitalWrite(vspi->pinSS(), HIGH);
vspi->endTransaction();
}
uint8_t read_Register(uint8_t addr)
{
uint8_t val;
digitalWrite(vspi->pinSS(), LOW);
vspi->transfer(R_REGISTER | addr);
val = vspi->transfer(NOPP);
digitalWrite(vspi->pinSS(), HIGH);
vspi->endTransaction();
return val;
}
这些间接使用的函数我就拿两个出来做例子,看看SPI库如何使用以及nrf24l01的SPI传输机制。
只有几个情况比较特殊,一个是写入/读取FIFO寄存器的信息,一个是使用Activate0x73这种功能。读取RX_FIFO寄存器的信息代码如下:
void RX_Payload(char *rec_data,uint8_t len)
{
digitalWrite(vspi->pinSS(), LOW);
vspi->transfer(R_RX_PAYLOAD);
while (len--)
{
*(rec_data++) = vspi->transfer(NOPP);
}
digitalWrite(vspi->pinSS(), HIGH);
vspi->endTransaction();
}
因为payLoad长度是不定的,所以要单独写一个函数。我试过开发芯片手册说的三层FIFO功能,最多96字节,写入payload在TX_FIFO时可以一口气写入96字节不中断,但接收RX_FIFO时,直接一口气读出来是不行的,我也试过分别三次上拉下拉CSN线来读取,也是不行,结果都是只有一层FIFO内的32字节信息。搞了很长时间都不行,我放弃这样做,采用一次读32字节,完成一次操作,重新等待下一次信号传输这样的手法,效果很好。
void TX_Payload(const char data[])
{
/*往TX FIFO里面填写传输信息*/
int len = strlen(data);
Serial.printf("TX payload length is %d\n",len);
write_Register(FLUSH_TX);
uint8_t TX_times=(len-1)/32+1;
while (TX_times--)
{
uint8_t k;
k = len < 32 ? len : 32;
digitalWrite(vspi->pinSS(), LOW);
vspi->transfer(W_TX_PAYLOAD);
while (k--)
{
vspi->transfer(*(data++));
}
digitalWrite(vspi->pinSS(), HIGH);
vspi->endTransaction();
digitalWrite(pinCE, HIGH);
while (digitalRead(pinIRQ));
digitalWrite(pinCE, LOW);
uint8_t status_info = read_Register(STATUS_NRF);
if (status_info & _BV(TX_DS))
{
Serial.printf("Transmited %d\n", TX_times);
write_Register(STATUS_NRF, _BV(TX_DS) | _BV(RX_DR) | _BV(MAX_RT));
}
if (status_info & _BV(MAX_RT))
{
Serial.println("Retransmit timeout");
write_Register(STATUS_NRF, _BV(TX_DS) | _BV(RX_DR) | _BV(MAX_RT));
return;
}
delayMicroseconds(300);
}
}
写入TX_FIFO时,允许一次发很长的信息,几百字节都行,我的思路是将TX_Payload拆包,拆成一个个32字节逐次发送,还比较实用可靠,看下面的传输结果。
左边是发送机的反馈信息,右边是接收机接收的信息。发送机我用了esp32s3,接收机我用esp32普通版,注意引脚会有稍稍不同,其他大抵一样。222字节程序拆了7次传输,但接收机还是很快全部接收出来。
最后附上初始化nrf24l01的代码:
void init_PTX()
{
write_Register(CONFIG, _BV(EN_CRC) | _BV(PWR_UP));
write_Register(EN_AA, _BV(ENAA_P0));//使能pipe0的自动应答
write_Register(EN_RXADDR, _BV(ERX_P0)); // 用Pipe0的通道接收
write_Register(SETUP_AW, 0x01);//地址位长度3bytes
write_Register(SETUP_RETR,0x03);//3次自动重发送,250us重发延迟时间
write_Register(RF_CH, 0x02);//发射频率为2402Mhz
// write_Register(RF_SETUP, _BV(LNA_HCURR) | _BV(1) | _BV(2));// 空中速率调慢至1Mbps
write_Register(STATUS_NRF,_BV(RX_DR) | _BV(TX_DS) | _BV(MAX_RT));//状态寄存器的几个触发值复位
write_Register(RX_ADDR_P0,TX_ADDRESS,3);//设置pipe0的通讯地址值
write_Register(TX_ADDR,TX_ADDRESS,3);
// write_Register(RX_PW_P0,0x20);//设置pipe0接收32byte长度的payload
digitalWrite(vspi->pinSS(), LOW);
vspi->transfer(ACTIVATE);
vspi->transfer(0x73);
digitalWrite(vspi->pinSS(), HIGH);
write_Register(FEATURE,_BV(EN_DPL));
write_Register(DYNPD,_BV(DPL_P0));//pipe0设置动态长度Payload
}
注意下半部分activate的操作,跟普通写入寄存器不一样。写入activate后要马上写入0x73,拉高CSN线后就成功开启nrf24l01的隐藏功能,后面可以像操作普通寄存器一样随意激活nrf24l01的其他隐藏功能,不用重新activate,我这儿只激活了动态payload长度这一个隐藏功能。
总结:发送机和接收机设置成一样的有:发送接收地址、地址长度、RF_CH的通讯频率、payload长度、空中传输速率、自动应答。但个人觉得nrf24l01的传输有时有点玄学,代码都不变的情况下,有时行有时不行,重新烧录同一套代码也是有时行有时不行,按esp32上的rst按钮重新上程序后,又行,不知道是不是我有什么遗漏了。