梁山派GD32F470移植NRF24L01+无线模块:硬件SPI与中断接收实战

梁山派GD32F470移植NRF24L01+无线模块:硬件SPI与中断接收实战 梁山派GD32F470移植NRF24L01无线模块硬件SPI与中断接收实战最近在做一个物联网小项目需要用到无线通信NRF24L01这个2.4G模块价格便宜、性能稳定是个不错的选择。但网上很多例程都是基于STM32的我手头正好有块梁山派GD32F470开发板就想着把NRF24L01移植过来。整个过程踩了不少坑今天就把完整的移植过程、硬件SPI配置和中断接收的实现经验分享给大家手把手教你搞定。1. 认识NRF24L01模块NRF24L01是一款工作在2.4-2.5GHz ISM频段的单片无线收发芯片简单来说就是咱们常用的2.4G无线模块。它有几个特点特别适合嵌入式项目SPI接口通信只需要4根线SCK、MOSI、MISO、CSN就能和MCU通信接线简单通信速率高最高支持2Mbps的数据传输率对于大多数传感器数据传输完全够用功耗低发射功率为6dBm时电流只有9.0mA接收模式12.3mA待机模式更低工作电压宽1.9~3.6V可以直接用3.3V供电注意市面上有NRF24L01和NRF24L01两种带的是增强版性能更好。咱们这次用的是NRF24L01模块。模块有8个引脚功能如下引脚功能说明GND电源地VCC电源正极1.9-3.6VCE芯片使能高电平使能收发CSNSPI片选低电平选中SCKSPI时钟线MOSI主机输出从机输入MISO主机输入从机输出IRQ中断输出低电平有效2. 硬件连接与引脚选择2.1 为什么选择硬件SPI在开始接线前咱们先搞清楚一个关键选择用硬件SPI还是软件模拟SPI硬件SPI靠MCU内部的SPI控制器硬件完成时序控制CPU占用率低通信速度快且稳定。梁山派GD32F470有6个SPI外设不用白不用。软件SPI用代码控制GPIO引脚模拟SPI时序实现简单但速度慢而且会占用大量CPU时间。对于NRF24L01这种需要实时响应的无线模块我强烈推荐用硬件SPI。下面是我在梁山派上选择的引脚NRF24L01引脚梁山派引脚说明GNDGND接地VCC3V3接3.3V电源CEPB8芯片使能普通GPIO即可CSNPB9SPI片选普通GPIOSCKPB13SPI1时钟必须用SPI复用功能MOSIPB15SPI1主机输出必须用SPI复用功能MISOPB14SPI1主机输入必须用SPI复用功能IRQPE4中断引脚用于接收数据通知提示PB13、PB14、PB15是GD32F470的SPI1默认引脚如果你要用其他SPI外设需要查数据手册确认哪些引脚有SPI复用功能。2.2 接线注意事项电源要稳定NRF24L01对电源比较敏感最好在VCC和GND之间加个10uF的电解电容和0.1uF的瓷片电容天线要外置如果通信距离要求远建议用带外置天线的模块版本IRQ引脚这个引脚在接收数据时会变低咱们后面要用它触发中断3. 硬件SPI驱动编写3.1 SPI初始化配置首先在drv_spi.h中定义引脚和SPI外设#ifndef __DRV_SPI_H__ #define __DRV_SPI_H__ #include gd32f4xx_gpio.h #include gd32f4xx_rcu.h #include main.h // SPI引脚定义 #define SPI_CLK_GPIO_PORT GPIOB #define SPI_CLK_GPIO_CLK RCU_GPIOB #define SPI_CLK_GPIO_PIN GPIO_PIN_13 #define SPI_MISO_GPIO_PORT GPIOB #define SPI_MISO_GPIO_CLK RCU_GPIOB #define SPI_MISO_GPIO_PIN GPIO_PIN_14 #define SPI_MOSI_GPIO_PORT GPIOB #define SPI_MOSI_GPIO_CLK RCU_GPIOB #define SPI_MOSI_GPIO_PIN GPIO_PIN_15 #define SPI_NSS_GPIO_PORT GPIOB #define SPI_NSS_GPIO_CLK RCU_GPIOB #define SPI_NSS_GPIO_PIN GPIO_PIN_9 // 片选操作宏 #define spi_set_nss_high() gpio_bit_write(SPI_NSS_GPIO_PORT, SPI_NSS_GPIO_PIN, SET) #define spi_set_nss_low() gpio_bit_write(SPI_NSS_GPIO_PORT, SPI_NSS_GPIO_PIN, RESET) // 硬件SPI配置 #define RCU_SPI_HARDWARE RCU_SPI1 #define PORT_SPI SPI1 #define LINE_AF_SPI GPIO_AF_5 // 函数声明 void drv_spi_init(void); uint8_t drv_spi_read_write_byte(uint8_t TxByte); void drv_spi_read_write_string(uint8_t* ReadBuffer, uint8_t* WriteBuffer, uint16_t Length); #endif接下来是SPI初始化的核心代码在drv_spi.c中#include drv_spi.h #ifndef __USE_SOFT_SPI_INTERFACE__ // 使用硬件SPI #define SPI_WAIT_TIMEOUT ((uint16_t)0xFFFF) void drv_spi_init(void) { spi_parameter_struct spi_init_struct; // 1. 开启GPIO时钟 rcu_periph_clock_enable(SPI_CLK_GPIO_CLK); rcu_periph_clock_enable(SPI_MISO_GPIO_CLK); rcu_periph_clock_enable(SPI_MOSI_GPIO_CLK); rcu_periph_clock_enable(SPI_NSS_GPIO_CLK); // 2. 开启SPI1时钟 rcu_periph_clock_enable(RCU_SPI_HARDWARE); // 3. 配置SCK引脚为复用功能 gpio_af_set(SPI_CLK_GPIO_PORT, LINE_AF_SPI, SPI_CLK_GPIO_PIN); gpio_mode_set(SPI_CLK_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, SPI_CLK_GPIO_PIN); gpio_output_options_set(SPI_CLK_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, SPI_CLK_GPIO_PIN); // 4. 配置MOSI引脚为复用功能 gpio_af_set(SPI_MOSI_GPIO_PORT, LINE_AF_SPI, SPI_MOSI_GPIO_PIN); gpio_mode_set(SPI_MOSI_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, SPI_MOSI_GPIO_PIN); gpio_output_options_set(SPI_MOSI_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, SPI_MOSI_GPIO_PIN); // 5. 配置MISO引脚为复用功能 gpio_af_set(SPI_MISO_GPIO_PORT, LINE_AF_SPI, SPI_MISO_GPIO_PIN); gpio_mode_set(SPI_MISO_GPIO_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, SPI_MISO_GPIO_PIN); gpio_output_options_set(SPI_MISO_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, SPI_MISO_GPIO_PIN); // 6. 配置CSN引脚为推挽输出软件控制片选 gpio_mode_set(SPI_NSS_GPIO_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, SPI_NSS_GPIO_PIN); gpio_output_options_set(SPI_NSS_GPIO_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, SPI_NSS_GPIO_PIN); gpio_bit_write(SPI_NSS_GPIO_PORT, SPI_NSS_GPIO_PIN, SET); // 初始化为高电平 // 7. 配置SPI参数 spi_init_struct.trans_mode SPI_TRANSMODE_FULLDUPLEX; // 全双工模式 spi_init_struct.device_mode SPI_MASTER; // 主机模式 spi_init_struct.frame_size SPI_FRAMESIZE_8BIT; // 8位数据帧 spi_init_struct.clock_polarity_phase SPI_CK_PL_LOW_PH_1EDGE; // 时钟极性相位 spi_init_struct.nss SPI_NSS_SOFT; // 软件控制NSS spi_init_struct.prescale SPI_PSC_32; // 32分频 spi_init_struct.endian SPI_ENDIAN_MSB; // MSB先行 spi_init(PORT_SPI, spi_init_struct); // 8. 使能SPI spi_enable(PORT_SPI); }这里有几个关键点需要注意时钟极性相位SPI_CK_PL_LOW_PH_1EDGE表示时钟空闲时为低电平在第一个时钟边沿采样。NRF24L01的SPI模式是Mode0这个配置正好对应。软件NSS设置为SPI_NSS_SOFT这样我们可以用GPIO手动控制CSN引脚更灵活。预分频SPI_PSC_32是32分频如果系统时钟是200MHzSPI时钟就是6.25MHz。NRF24L01最高支持8MHz这个速度很安全。3.2 SPI字节收发函数硬件SPI的收发函数比软件模拟简单多了uint8_t drv_spi_read_write_byte(uint8_t TxByte) { uint8_t l_Data 0; uint16_t l_WaitTime 0; // 等待发送缓冲区为空 while(RESET spi_i2s_flag_get(PORT_SPI, SPI_FLAG_TBE)) { if(SPI_WAIT_TIMEOUT l_WaitTime) { break; // 超时退出 } } l_WaitTime SPI_WAIT_TIMEOUT / 2; // 重新设置接收等待时间 spi_i2s_data_transmit(PORT_SPI, TxByte); // 发送数据 // 等待接收缓冲区非空 while(RESET spi_i2s_flag_get(PORT_SPI, SPI_FLAG_RBNE)) { if(SPI_WAIT_TIMEOUT l_WaitTime) { break; // 超时退出 } } l_Data spi_i2s_data_receive(PORT_SPI); // 读取接收数据 return l_Data; }这个函数实现了全双工通信发送一个字节的同时接收一个字节。加了超时判断是个好习惯防止程序卡死。4. NRF24L01驱动层实现4.1 基础寄存器操作NRF24L01的所有操作都是通过SPI读写寄存器完成的。先看看几个最基本的函数// 读寄存器 uint8_t NRF24L01_Read_Reg(uint8_t RegAddr) { uint8_t btmp; RF24L01_SET_CS_LOW(); // 片选拉低 drv_spi_read_write_byte(NRF_READ_REG | RegAddr); // 发送读命令寄存器地址 btmp drv_spi_read_write_byte(0xFF); // 读取数据 RF24L01_SET_CS_HIGH(); // 片选拉高 return btmp; } // 写寄存器 void NRF24L01_Write_Reg(uint8_t RegAddr, uint8_t Value) { RF24L01_SET_CS_LOW(); // 片选拉低 drv_spi_read_write_byte(NRF_WRITE_REG | RegAddr); // 发送写命令寄存器地址 drv_spi_read_write_byte(Value); // 发送数据 RF24L01_SET_CS_HIGH(); // 片选拉高 }这里有个细节NRF24L01的命令字最高位是命令低5位是寄存器地址。比如读寄存器命令是0x00写寄存器命令是0x20。4.2 模块初始化初始化是让NRF24L01正常工作的关键我一般按这个顺序配置void RF24L01_Init(void) { uint8_t addr[5] {INIT_ADDR}; // 默认地址 RF24L01_SET_CE_HIGH(); // CE置高 NRF24L01_Clear_IRQ_Flag(IRQ_ALL); // 清除所有中断标志 // 动态数据包配置 #if DYNAMIC_PACKET 1 NRF24L01_Write_Reg(DYNPD, (1 0)); // 使能通道0动态数据长度 NRF24L01_Write_Reg(FEATRUE, 0x07); #else NRF24L01_Write_Reg(RX_PW_P0, FIXED_PACKET_LEN); // 固定数据长度 #endif // 核心配置 NRF24L01_Write_Reg(CONFIG, (1 EN_CRC) | (1 PWR_UP)); // 使能CRC上电 NRF24L01_Write_Reg(EN_AA, (1 ENAA_P0)); // 通道0自动应答 NRF24L01_Write_Reg(EN_RXADDR, (1 ERX_P0)); // 使能通道0接收 NRF24L01_Write_Reg(SETUP_AW, AW_5BYTES); // 5字节地址宽度 NRF24L01_Write_Reg(SETUP_RETR, ARD_4000US | (REPEAT_CNT 0x0F)); // 重发配置 NRF24L01_Write_Reg(RF_CH, 00); // 设置通道0 NRF24L01_Write_Reg(RF_SETUP, 0x26); // 2Mbps0dBm发射功率 // 设置收发地址 NRF24L01_Set_TxAddr(addr[0], 5); NRF24L01_Set_RxAddr(0, addr[0], 5); }提示收发双方必须设置相同的地址和通信频率RF_CH寄存器才能正常通信。5. 两种接收方式对比与实现5.1 轮询方式接收简单但低效轮询方式就是不断查询IRQ引脚状态代码简单但效率低uint8_t NRF24L01_RxPacket(uint8_t *rxbuf) { uint8_t l_Status 0, l_RxLength 0, l_100MsTimes 0; // 不断查询状态寄存器 l_Status NRF24L01_Read_Reg(STATUS); NRF24L01_Write_Reg(STATUS, l_Status); // 清中断标志 if(l_Status RX_OK) // 接收到数据 { l_RxLength NRF24L01_Read_Reg(R_RX_PL_WID); // 读取数据长度 NRF24L01_Read_Buf(RD_RX_PLOAD, rxbuf, l_RxLength); // 读取数据 NRF24L01_Write_Reg(FLUSH_RX, 0xff); // 清除RX FIFO return l_RxLength; } return 0; // 没有收到数据 }这种方式的问题很明显CPU要不断查询浪费资源。在实际项目中如果还有其他任务要处理轮询方式就不太合适了。5.2 中断方式接收推荐中断方式才是正解当NRF24L01收到数据时IRQ引脚会变低我们配置这个引脚为外部中断收到数据时自动处理。5.2.1 中断引脚配置首先在NRF24L01.h中定义中断相关宏// IRQ引脚外部中断配置 #define BSP_IRQ_EXTI_IRQN EXTI4_IRQn // 外部中断4 #define BSP_IRQ_EXTI_PORT_SOURCE EXTI_SOURCE_GPIOE // 外部中断端口资源 #define BSP_IRQ_EXTI_PIN_SOURCE EXTI_SOURCE_PIN4 // 外部中断引脚资源 #define BSP_IRQ_EXTI_LINE EXTI_4 // 外部中断线 #define BSP_IRQ_EXTI_IRQHANDLER EXTI4_IRQHandler // 外部中断函数名然后配置中断void IRQ_gpio_config(void) { // 开启时钟 rcu_periph_clock_enable(RF24L01_IRQ_GPIO_CLK); rcu_periph_clock_enable(RCU_SYSCFG); // 系统配置时钟 // 配置为输入模式 gpio_mode_set(RF24L01_IRQ_GPIO_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, RF24L01_IRQ_GPIO_PIN); // 配置外部中断 nvic_irq_enable(BSP_IRQ_EXTI_IRQN, 2U, 2U); // 抢占优先级2子优先级2 syscfg_exti_line_config(BSP_IRQ_EXTI_PORT_SOURCE, BSP_IRQ_EXTI_PIN_SOURCE); exti_init(BSP_IRQ_EXTI_LINE, EXTI_INTERRUPT, EXTI_TRIG_BOTH); // 双边沿触发 exti_interrupt_enable(BSP_IRQ_EXTI_LINE); // 使能中断 exti_interrupt_flag_clear(BSP_IRQ_EXTI_LINE); // 清除中断标志位 }这里配置的是双边沿触发EXTI_TRIG_BOTH因为NRF24L01的IRQ引脚在收到数据时会变低处理完后又恢复高电平。5.2.2 中断服务函数中断来了怎么处理看代码// 接收数据缓存 uint8_t g_RF24L01RxBuffer[250]; void BSP_IRQ_EXTI_IRQHANDLER(void) { // 检查中断标志位 if(exti_interrupt_flag_get(BSP_IRQ_EXTI_LINE) SET) { // IRQ为低电平接收到数据 if(gpio_input_bit_get(RF24L01_IRQ_GPIO_PORT, RF24L01_IRQ_GPIO_PIN) RESET) { NRF24L01_RxPacket(g_RF24L01RxBuffer); // 接收数据 printf(data %s, g_RF24L01RxBuffer); // 输出数据 // 清除RX FIFO RF24L01_SET_CS_LOW(); drv_spi_read_write_byte(FLUSH_RX); RF24L01_SET_CS_HIGH(); } exti_interrupt_flag_clear(BSP_IRQ_EXTI_LINE); // 清中断标志位 } }重要NRF24L01在接收完数据后必须清除接收FIFO否则下次收不到新数据。这个坑我踩过调了半天才发现。6. 发送数据实现发送端相对简单但也要注意一些细节uint8_t NRF24L01_TxPacket(uint8_t *txbuf, uint8_t Length) { uint8_t l_Status 0; uint16_t l_MsTimes 0; // 清空TX FIFO RF24L01_SET_CS_LOW(); drv_spi_read_write_byte(FLUSH_TX); RF24L01_SET_CS_HIGH(); // 发送数据 RF24L01_SET_CE_LOW(); NRF24L01_Write_Buf(WR_TX_PLOAD, txbuf, Length); // 写数据到TX缓冲区 RF24L01_SET_CE_HIGH(); // 启动发送 // 等待发送完成带超时 while(0 ! RF24L01_GET_IRQ_STATUS()) { drv_delay_ms(5); if(500 l_MsTimes) // 500ms超时 { // 超时重新初始化 NRF24L01_Gpio_Init(); RF24L01_Init(); RF24L01_Set_Mode(MODE_TX); break; } } // 读取状态并清除中断标志 l_Status NRF24L01_Read_Reg(STATUS); NRF24L01_Write_Reg(STATUS, l_Status); if(l_Status MAX_TX) // 达到最大重发次数 { NRF24L01_Write_Reg(FLUSH_TX, 0xff); return MAX_TX; } if(l_Status TX_OK) // 发送完成 { return TX_OK; } return 0xFF; // 其他原因发送失败 }这里有个实用技巧加了500ms超时判断。无线通信可能受干扰如果没有超时保护程序可能一直卡在等待发送完成的地方。7. 实际使用与调试心得7.1 收发模式切换NRF24L01需要在发送和接收模式间切换void RF24L01_Set_Mode(nRf24l01ModeType Mode) { uint8_t controlreg 0; controlreg NRF24L01_Read_Reg(CONFIG); if(Mode MODE_TX) { controlreg ~(1 PRIM_RX); // 设置为发送模式 } else if(Mode MODE_RX) { controlreg | (1 PRIM_RX); // 设置为接收模式 } NRF24L01_Write_Reg(CONFIG, controlreg); }注意切换模式后要稍微延时一下给模块一点稳定时间我一般延时5ms。7.2 常见问题排查在实际使用中我遇到过这些问题你也可能会遇到通信不上检查电源用万用表量一下模块VCC脚是不是3.3V检查接线特别是SCK、MOSI、MISO有没有接反检查SPI时序用逻辑分析仪看波形确认时钟极性和相位通信距离短检查天线外置天线比PCB天线效果好调整发射功率NRF24L01_Set_Power(POWER_0DBM)是最大功率降低通信速率2Mbps比1Mbps距离短250Kbps距离最远数据丢包检查重发次数SETUP_RETR寄存器设置重发次数和间隔启用自动应答EN_AA寄存器使能自动应答避开干扰2.4G频段设备多换个信道试试7.3 工程配置要点在main.h中有两个重要的宏需要配置// 选择SPI方式 // #define __USE_SOFT_SPI_INTERFACE__ // 注释掉使用硬件SPI取消注释使用软件SPI // 选择工作模式 #define RECEIVING_MODE 1 // 1为接收模式0为发送模式如果是两个开发板对传一个配置为发送模式RECEIVING_MODE 0一个配置为接收模式RECEIVING_MODE 1。8. 移植验证代码都写好了怎么验证是否成功编译下载将发送端代码烧录到一个梁山派接收端代码烧录到另一个梁山派连接硬件按照前面的引脚表连接两个NRF24L01模块上电测试给两个开发板上电观察输出接收端应该能通过串口打印出发送端发送的数据如果一切正常你会在串口助手上看到发送的数据。如果没看到别着急按下面的步骤排查先用示波器或逻辑分析仪看SPI波形确认通信正常检查NRF24L01的寄存器配置用NRF24L01_check()函数测试确认两个模块的地址和通信频道一致这个移植我在梁山派GD32F470上实测通过通信稳定中断响应及时。对于物联网节点、无线遥控器等应用完全够用。关键是要理解NRF24L01的工作机制特别是中断接收那部分理解了之后调试起来就顺利多了。代码我已经整理好你可以在立创·梁山派的资料中找到完整工程。实际项目中根据需求调整通信速率、发射功率这些参数平衡功耗和距离。无线通信调试需要耐心多试试不同配置找到最适合你项目的参数。