完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
nRF24L01 1对1通讯
1、SPI通讯 在调试nRF24L01模块前,要先保证自己的SPI通讯是正常的,我这里使用的是SPI1,配置按正常的配置就可以了,但是要注意的是时钟极性CPOL和时钟相位CPHA需根据nRF的时序图确定,串行时钟稳态要是低电平。 我第一次测试的时候就是复制以前的代码,时钟稳态是高电平结果,nRF模块都没有回应,后来仔细查找才发现的是这个问题 SPI_InitStructure.SPI_BaudRatePrescaler = speed; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //数据捕获于第一个时钟沿 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //串行时钟稳态 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //每次收发数据大小 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI单向/双向 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI模式,主/从模式 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)控制 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式 以上是SPI的配置,管脚配置就不多说了,记得开启复用时钟就是了 SPI配置好后只需提供一个读写函数 //SPI读写函数 u8 SPI1_ReadWriteByte(u8 data) { u8 temp = 0; //等待发送数据寄存器清空,temp防止程序卡死 while(!(SPI1->SR & SPI_I2S_FLAG_TXE)){ temp++; if (temp > 200){return 0xFF;} } //发送数据 SPI1->DR = data; temp = 0; //等待接收数据寄存器非空,temp防止程序卡死 while(!(SPI1->SR & SPI_I2S_FLAG_RXNE)){ temp++; if (temp > 200){return 0xFF;} } return SPI1->DR; } //野火的源码 u8 SPI1_ReadWriteByte(u8 TxData) { u8 tmp = 0; while (!SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE)){ //等待发送结束 tmp++; if (tmp > 200) return 0; } SPI_I2S_SendData(SPI1, TxData); tmp = 0; while (!SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE)){ //等待接收完成 tmp++; if (tmp > 200) return 0; } return SPI_I2S_ReceiveData(SPI1); } 这是我在野火的例程里拿来用的,为了提高读写速度,我将所有库函数调用都替换成了寄存器操作,看不懂的可以直接用野火的源码 到此,SPI底层配置以及搞定 2、nRF24L01模块介绍 一共有8个引出的接口,CLK、MISO、MOSI、CSN是连接SPI接口的 CE:使能nRF模块发送或接收功能 IRQ:当芯片发送数据成功(指发送出去并且接收到自动应答信号)或接收到有效数据的时候会产生一个低电平信号,这个管脚接到stm32的一个外部中断GPIO,就可以及时检查数据 3、nRF模块GPIO配置 在前面的SPI接口配置的时候,只配置了CLK、MISO、MOSI,NSS信号由是软件(使用SSI位)控制,因此还要配置CSN、CE、IRQ管脚,这是针对该模块的专用接口配置,所以我选择放在NRF.c文件里配置 /******************************************************************************** * @brief NRF_SPI的GPIO配置 * @param none * @retval none *******************************************************************************/ void NRF_SPI_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; //SPI1基础配置,注意时钟极性CPOL和时钟相位CPHA需根据nRF的时序图确定 //nRF模块最高频率可达10MHz,但是建议不要超过8MHz,这里是9MHz SPI_Config(SPI_BaudRatePrescaler_8); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); /* 配置nRF的CE和CSN引脚 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_0; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); /*配置nRF的IRQ引脚*/ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU ; //上拉输入,IRQ引脚会产生一个低电平中断 GPIO_Init(GPIOA, &GPIO_InitStructure); /* IRQ EXTI line mode config */ GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource15); EXTI_InitStructure.EXTI_Line = EXTI_Line15; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿中断 EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); /* config the NVIC */ NVIC_EXTI_GPIO_Config(); /* 拉高csn引脚,nRF进入空闲状态 */ NRF_CSN_HIGH; } 这里调用了底层SPI的配置【SPI_Config(SPI_BaudRatePrescaler_8);】,并设置了SPI频率,nRF模块最高频率可达10MHz,但是建议不要超过8MHz,这里是9MHz。 NRF_CSN_HIGH是宏定义,放在NRF.h里面,提高可移植性 /********************************************************************** 底层链接 ***********************************************************************/ /******************************CSN CE IRQ接口配置*******************************************/ #define NRF_CSN_HIGH GPIOB->BSRR = GPIO_Pin_2 #define NRF_CSN_LOW GPIOB->BRR = GPIO_Pin_2 #define NRF_CE_HIGH GPIOB->BSRR = GPIO_Pin_0 #define NRF_CE_LOW GPIOB->BRR = GPIO_Pin_0 #define NRF_CE_HeartBeart NRF_CE_HIGH;delay_us(15);NRF_CE_LOW //产生一个15us的脉冲,使nRF发送数据,发送单个数据包时可以使用 #define NRF_Read_IRQ (GPIOA->IDR & GPIO_Pin_15) //中断引脚,轮询时会用到,这里用外部中断方式 /********************************链接SPI读写接口******************************************/ #define SPI_NRF_RW(data) SPI1_ReadWriteByte(data) 在这里提供了所有nRF模块的GPIO定义,以及SPI接口,通过简单的修改就可以更换,方便移植 由于IRQ是采用外部中断的方式,所以还要配置NVIC /******************************************************************************* * @brief GPIO外部中断NVIC配置,与使用的GPIO保持一致 * @param none * @retval none ******************************************************************************/ static void NVIC_EXTI_GPIO_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; /* Configure one bit for preemption priority */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); /* 配置中断源 */ NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } 到此,nRF的GPIO以及全部配置完成,可以对模块进行读写操作了 4、nRF读写API 要想和nRF模块进行正常的通讯,首先要提供读写四个API 1、在寄存器里读出1byte数据 2、在寄存器里读出多byte数据 3、往寄存器里写入1byte数据 4、往寄存器里写入多byte数据 //头文件宏定义 //----------------------------------------------------------- //nRF寄存器读写 调用以下API均会刷新status的值 u8 NRF_SPI_ReadReg(u8 reg); u8 NRF_SPI_ReadBuf(u8 reg,u8 *buf,u8 len); u8 NRF_SPI_WriteReg(u8 reg,u8 dat); u8 NRF_SPI_WriteBuf(u8 reg ,u8 *buf,u8 len); //----------------------------------------------------------- 读寄存器 /******************************************************************************** * @brief 用于从NRF特定的寄存器读出1byte数据 * @param * @arg reg : NRF的指令+寄存器地址 * @retval 寄存器中的数据 *******************************************************************************/ u8 NRF_SPI_ReadReg(u8 reg) { u8 reg_val; NRF_CSN_LOW; /*置低CSN,使能SPI传输*/ status = SPI_NRF_RW(reg); /*发送寄存器号,顺便获得status值*/ reg_val = SPI_NRF_RW(NOP); /*读取寄存器的值*/ NRF_CSN_HIGH; /*CSN拉高,完成*/ return reg_val; } /******************************************************************************** * @brief 用于向NRF的寄存器中写入(len)byte数据 * @param * @arg reg : NRF的命令+寄存器地址 * @arg buf :用于存储将被读出的寄存器数据的数组,外部定义 * @arg len : buf的数据长度 * @retval NRF的status寄存器的状态 *******************************************************************************/ u8 NRF_SPI_ReadBuf(u8 reg,u8 *buf,u8 len) { u8 cnt; NRF_CSN_LOW; /*置低CSN,使能SPI传输*/ status = SPI_NRF_RW(reg); /*发送寄存器号,顺便获得status值*/ /*读取缓冲区数据*/ for(cnt = 0; cnt < len; cnt++){ buf[cnt] = SPI_NRF_RW(NOP); /*读取寄存器的值*/ } NRF_CSN_HIGH; /*CSN拉高,完成*/ return status; /*返回STATUS寄存器状态值*/ } 写寄存器 /******************************************************************************** * @brief 用于向NRF特定的寄存器写入1byte数据 * @param * @arg reg : NRF的命令+寄存器地址 * @arg dat : 将要向寄存器写入的数据 * @retval NRF的STATUS寄存器的状态 *******************************************************************************/ u8 NRF_SPI_WriteReg(u8 reg,u8 data) { NRF_CSN_LOW; /*置低CSN,使能SPI传输*/ status = SPI_NRF_RW(reg); /*发送寄存器号,顺便获得status值*/ SPI_NRF_RW(data); /*向寄存器写入数据*/ NRF_CSN_HIGH; /*CSN拉高,完成*/ return status; /*返回STATUS寄存器的值*/ } /******************************************************************************** * @brief 用于向NRF的寄存器中写入(len)byte数据 * @param * @arg reg : NRF的命令+寄存器地址 * @arg buf :存储了将要写入写寄存器数据的数组,外部定义 * @arg len : buf的数据长度 * @retval NRF的status寄存器的状态 *******************************************************************************/ u8 NRF_SPI_WriteBuf(u8 reg ,u8 *buf,u8 len) { u8 byte_cnt; NRF_CSN_LOW; /*置低CSN,使能SPI传输*/ status = SPI_NRF_RW(reg); /*发送寄存器号,顺便获得status值*/ /*向缓冲区写入数据*/ for(byte_cnt = 0; byte_cnt < len; byte_cnt++){ SPI_NRF_RW(*buf++); //写数据到缓冲区 } NRF_CSN_HIGH;/*CSN拉高,完成*/ return status; //返回NRF24L01的状态 } 这四个函数已经有具体的注释,就不多说了,主要是先拉低CSN–发送寄存器号–发送数据/接收数据–拉高CSN,SPI时序见数据手册8.3.2章节 读寄存器的时候发送完寄存器号,接着发送【NOP】,这是NRF数据手册里提供的空指令,然后会返回寄存器的值,在头文件里定义(关于操作指令和寄存器地址在后面会详细讲解) #define NOP 0xFF // 保留 注意的是这个发送寄存器号,并不单纯地发送【寄存器地址】,而是【操作指令+寄存器地址】 值得注意的是,这四个函数里在发送寄存器号的时候,都将返回值赋给一个status变量,而这个变量是一个u8类型的全局变量,这里涉及到nRF的一个寄存器【STATUS】在后面将nRF寄存器再讲 到此已经可以和nRF模块进行通讯,可以写个简单的函数进行测试: /******************************************************************************** * @brief 主要用于NRF与MCU是否正常连接 * @param none * @retval SUCCESS/ERROR 连接正常/连接失败 *******************************************************************************/ u8 NRF_Check(void) { u8 buf[5]={0xC2,0xC2,0xC2,0xC2,0xC2}; u8 buf1[5]; u8 i; NRF_CE_LOW; /*写入5个字节的地址. */ NRF_SPI_WriteBuf(W_REGISTER+TX_ADDR,buf,5); /*读出写入的地址 */ NRF_SPI_ReadBuf(R_REGISTER+TX_ADDR,buf1,5); NRF_CE_HIGH; /*比较*/ for(i=0;i<5;i++){ if(buf1!=0xC2){return ERROR ;}//MCU与NRF不正常连接 } return SUCCESS ; //MCU与NRF成功连接 } 这里面的【W_REGISTER = 0x00】【R_REGISTER = 0x20】是nRF的操作指令 【TX_ADDR = 0x10】是nRF的寄存器地址,关于操作指令和寄存器再后面会讲到,这里可以替换进去测试能否进行正常的通讯。 除了SPI配置的那部分函数没有完整的给出来以外,以上所有的程序都是完整可用的,如果不能,就要查看自己的SPI接口和nRF接口配置有没有问题。 5、nRF配置 前面讲到SPI和nRF接口的配置,以及提供了4个读写API接口,由于都是比较简单的常规操作,我没有详细地讲解每行代码,接下来我会结合NRF数据手册对NRF配置函数进行详细的解释 另外提一点,网上的资料里有中文的数据手册,但是对比官方原版的英文数据手册可以看见,中文版的翻译不齐全,漏掉好多内容,接下来讲到的东西以原版数据手册为准 NRF寄存器 这里我先贴出完整的头文件,再详细讲解 /** ****************************************************************************** * @file NRF.h * @author Elliot-C * @version V1.0 * @date * @brief nrf24l01+ master-slaver bsp ****************************************************************************** * @attention * * * * ****************************************************************************** */ #ifndef __NRF_H #define __NRF_H #include "stm32f10x.h" #include "bsp_conf.h" /********************************************************************** 底层链接 ***********************************************************************/ /******************************CSN CE IRQ接口配置*******************************************/ #define NRF_CSN_HIGH GPIOB->BSRR = GPIO_Pin_2 #define NRF_CSN_LOW GPIOB->BRR = GPIO_Pin_2 #define NRF_CE_HIGH GPIOB->BSRR = GPIO_Pin_0 #define NRF_CE_LOW GPIOB->BRR = GPIO_Pin_0 #define NRF_CE_HeartBeart NRF_CE_HIGH;delay_us(15);NRF_CE_LOW //产生一个15us的脉冲,使nRF发送数据,发送单个数据包时可以使用 #define NRF_Read_IRQ (GPIOA->IDR & GPIO_Pin_15) //中断引脚,轮询时会用到,这里用外部中断方式 /********************************链接SPI读写接口******************************************/ #define SPI_NRF_RW(data) SPI1_ReadWriteByte(data) |
|
|
|
/**********************************************************************
宏定义 ***********************************************************************/ //==========================NRF24L01============================================ #define RX_MULIT_CHANNEL 1 //【0:单通道(默认通道0)】【1:多通道(6通道全开)】 #define DYNAMIC_PLOAD_LENGTH 1 //【0:固定数据长度】【1:动态数据长度】 #define STATIC_PLOAD_LENGTH 5 //设置数据长度(固定数据长度下使用) #define ADDRESS_WIDTH 5 //通道地址宽度 //#define TX_ADR_WIDTH 5 //发送端的接收通道地址宽度 //#define RX_ADR_WIDTH 5 //接收端的接收通道地址宽度 //#define TX_PLOAD_WIDTH 5 //要发送的有效数据长度(固定长度) //#define RX_PLOAD_WIDTH 5 //要接收的有效数据长度(固定长度),这个决定RX端FIFO达到多少数据量后触发中断 //------------------------RX通道地址【低字节在前!!!】----------------------------------------------------------------------- extern u8 RX_ADDRESS_0[ADDRESS_WIDTH]; extern u8 RX_ADDRESS_1[ADDRESS_WIDTH]; extern u8 RX_ADDRESS_2[ADDRESS_WIDTH]; extern u8 RX_ADDRESS_3[ADDRESS_WIDTH]; extern u8 RX_ADDRESS_4[ADDRESS_WIDTH]; extern u8 RX_ADDRESS_5[ADDRESS_WIDTH]; //=========================NRF24L01寄存器指令=================================== #define R_REGISTER 0x00 // 读寄存器指令 #define W_REGISTER 0x20 // 写寄存器指令 #define RD_RX_PLOAD 0x61 // 读取接收数据指令(PTX.PRX) #define WR_TX_PLOAD 0xA0 // 写待发数据指令(PTX) #define FLUSH_TX 0xE1 // 清空TX_FIFO内的数据指令(PTX.PRX) #define FLUSH_RX 0xE2 // 清空RX_FIFO内的数据指令(PTX.PRX) #define REUSE_TX_PL 0xE3 // 定义重复装载数据指令(PTX) #define R_RX_PL_WID 0x60 // 接收到的有效数据宽度 //#define W_ACK_PAYLOAD 0x?? // 没用到 //#define W_TX_PAYLOAD_NO_ACK 0xA0 // 没用到 #define NOP 0xFF // 保留 //========================SPI(nRF24L01)寄存器地址=============================== #define CONFIG 0x00 // 配置收发状态,CRC校验模式以及收发状态响应方式 #define EN_AA 0x01 // 自动应答功能设置 #define EN_RXADDR 0x02 // 可用信道设置 #define SETUP_AW 0x03 // 收发地址宽度设置 #define SETUP_RETR 0x04 // 自动重发功能设置 #define RF_CH 0x05 // 工作频率设置 #define RF_SETUP 0x06 // 发射速率、功耗功能设置 #define STATUS 0x07 // 状态寄存器 #define OBSERVE_TX 0x08 // 发送监测功能 #define CD 0x09 // 地址检测 #define RX_ADDR_P0 0x0A // 频道0接收数据地址 #define RX_ADDR_P1 0x0B // 频道1接收数据地址 #define RX_ADDR_P2 0x0C // 频道2接收数据地址 #define RX_ADDR_P3 0x0D // 频道3接收数据地址 #define RX_ADDR_P4 0x0E // 频道4接收数据地址 #define RX_ADDR_P5 0x0F // 频道5接收数据地址 #define TX_ADDR 0x10 // 发送地址寄存器 #define RX_PW_P0 0x11 // 接收频道0接收数据长度 #define RX_PW_P1 0x12 // 接收频道0接收数据长度 #define RX_PW_P2 0x13 // 接收频道0接收数据长度 #define RX_PW_P3 0x14 // 接收频道0接收数据长度 #define RX_PW_P4 0x15 // 接收频道0接收数据长度 #define RX_PW_P5 0x16 // 接收频道0接收数据长度 #define FIFO_STATUS 0x17 // FIFO栈入栈出状态寄存器 #define DYNPD 0x1C // 动态有效数据长度寄存器 #define FEATURE 0x1D // 与动态有效数据长度相关 //=============================RF24l01状态===================================== //CONFIG寄存器 #define PWR_UP 0x02 //Power Up #define PWR_DOWN 0x00 //Power Down #define PRIM_RX 0x01 //PRX #define PRIM_TX 0x00 //PTX //发生中断时,根据STATUS寄存器中的值来判断是哪个中断源触发了IRQ中断 #define IRQ_STATUS 0x70 //提取中断标志位 #define IRQ_CLEAR 0x7E //清除中断标志位 #define RX_DR 0x40 //数据接收完成中断 #define TX_DS 0x20 //数据发送完成中断 #define MAX_RT 0x10 //数据包重发次数超过设定值中断 //nRF配置 void NRF_SPI_Config(void); //nRF的SPI及CE CSN IRQ接口配置 void NRF_Config(void); //nRF初始化 u8 NRF_Check(void); //检查MCU和nRF的连接 //----------------------------------------------------------- //nRF寄存器读写 调用以下API均会刷新status的值 u8 NRF_SPI_ReadReg(u8 reg); u8 NRF_SPI_ReadBuf(u8 reg,u8 *buf,u8 len); u8 NRF_SPI_WriteReg(u8 reg,u8 dat); u8 NRF_SPI_WriteBuf(u8 reg ,u8 *buf,u8 len); //----------------------------------------------------------- //----------------------------------------------------------- //先调用【NRF_CE_LOW;】拉低进入待机模式或掉电模式 //再调用以下API void NRF_RX_Mode(void); //配置成PRX void NRF_TX_Mode(void); //配置成PTX //----------------------------------------------------------- //nRF_API void NRF_ReceivePacket(void); void NRF_SendPacket(u8 *RX_ADDRESS_X,u8 *TX_BUFF); //发送固定长度有效数据包 void NRF_SendPacket_DPL(u8 *RX_ADDRESS_X,u8 * TX_BUFF, u32 length); //发送动态长度有效数据包 #endif /* __NRF_H */ 对于 #include "bsp_conf.h" 这是包含了我工程的所有BSP头文件,但这里只用到了【delay.h】你们可用直接替换成自己的延时文件 底层链接和SPI链接前面讲过了,不再累述 通道地址是在多通道模式下发送数据时外部会调用到的,这里设置为extern,后面会讲解 接下来就是NRF的操作指令,对应的值都是可以在数据手册里第51页8.3.1章节里得到 【PTX:主发射模式】【PRX:主接收模式】 括号里面的表示该指令在那种模式下会用到 操作指令分为两种【只需操作指令】【操作指令+地址信息】 在前面的【void NRF_Check(void)】函数里就调用了一句 NRF_SPI_WriteBuf(W_REGISTER+TX_ADDR,buf,5); 这就是第二种操作指令,写指令【W_REGISTER】+寄存器地址【TX_ADDR】 length = NRF_SPI_ReadReg(R_RX_PL_WID); 上面这类型就是第一种操作指令,只需要写入指令【R_RX_PL_WID】就能得到数据 代码接下来的就是寄存器地址了,同样可以在数据手册9.1章节可以查到 常用寄存器功能 CONFIG 芯片的三种中断 【使能/失能】 CRC校验 【使能/失能】 CRC位数 【1byte/2byte】 芯片进入 【掉电模式/上电模式】 芯片进入 【发射模式/接收模式】 EN_AA 【使能/失能】通道0~5的自动应答功能 EN_RXADDR 【使能/失能】通道0~5接收通道,使其在PRX模式下能够接收数据 SETUP_AW 设置地址宽度,常用的是5byte长度,也就是前面见到的 u8 RX_ADDRESS_0[ADDRESS_WIDTH]; 这些地址数组的长度【ADDRESS_WIDTH】 SETUP_RETR 设置超时自动重发的时间和重发次数,具体看数据手册解释 RF_CH 设置使用的通信频段 RF_SETUP 设置射频通信速率和功率等 STATUS 这是前面提到过的一个很重要的寄存器 bit 说明 7 保留,必须为0 6 RX_DR 接收标志位,当接收到有效数据时置1 5 TX_DS 发送标志位,当发送成功时置1 4 MAX_RT 超时标志位,当重发超过设定次数时置1 以上三位任何一种情况都会触发IRQ 3:1 RX_P_NO 可以判断接收到的数据通道是来自哪个通道的 000~101,对应的通道 0 TX_FULL 判断TX_FIFO是否装满 注意 当时间成功发送出去,TX_FIFO会自动清空 在发送超时的情况下,TX_FIFO不会自动清空,当TX_FIFO填满,就不用能再发送数据给TX_FIFO了,必须清空 RX_ADDR_P0 RX_ADDR_P1 RX_ADDR_P2 RX_ADDR_P3 RX_ADDR_P4 RX_ADDR_P5 这六个寄存器是用来【存放】接收通道地址的,也就是前面提到的六个地址 //------------------------RX通道地址【低字节在前!!!】--------------------------------- u8 RX_ADDRESS_0[ADDRESS_WIDTH] = {0x00,0x34,0x22,0x1D,0x10}; //NRF的RX0通道的地址 u8 RX_ADDRESS_1[ADDRESS_WIDTH] = {0x01,0x34,0x22,0x1D,0x10}; //NRF的RX1通道的地址 u8 RX_ADDRESS_2[ADDRESS_WIDTH] = {0x02,0x34,0x22,0x1D,0x10}; //NRF的RX2通道的地址 u8 RX_ADDRESS_3[ADDRESS_WIDTH] = {0x03,0x34,0x22,0x1D,0x10}; //NRF的RX3通道的地址 u8 RX_ADDRESS_4[ADDRESS_WIDTH] = {0x04,0x34,0x22,0x1D,0x10}; //NRF的RX4通道的地址 u8 RX_ADDRESS_5[ADDRESS_WIDTH] = {0x05,0x34,0x22,0x1D,0x10}; //NRF的RX5通道的地址 在数据手册7.6章节里说明 通道0和通道1的值是任意的,但是不能相同 通道2~5和通道1共用高4位,最低1位必须不同,不然是接收不到数据的 而且是低字节在前!!!本人一开始设置的地址是高字节在在前,导致通道0和通道1都能通信,一切换到通道2~5就没有任何反应了 TX_ADDR 用来存放发送通道地址 RX_PW_P0 RX_PW_P1 RX_PW_P2 RX_PW_P3 RX_PW_P4 RX_PW_P5 这是配置每个通道的一次接收有效数据长度,最大值为32byte 当该通道接收到的数据等于设定的值,就会触发IRQ中断 FIFO_STATUS 存放TX_FIFO和RX_FIFO相关状态的寄存器,详见数据手册 DYNPD 【使能/失能】各个通道的动态数据接收功能 FEATURE 配合动态数据长度功能使用,开启动态数据长度自动应答功能等,详见数据手册 上面就是对常用的寄存器的功能解释,没有讲到的寄存器可以看数据手册 只要在正确时刻正确配置好以上的寄存器,就能顺利使用NRF模块的所有通道、动态数据长度等功能, nRF初始化 这里正式开始对nRF模块进行初始化配置 首先要数据手册6.1.1章节对于芯片的运行流程解释,不清晰的话还请看数据手册里的,这个流程图告诉了我们应该如何配置才能正常使用芯片 首先是芯片上电,1m后处于默认掉电模式,也可以理解为低功耗模式,当PWR_UP = 1的时候,进入standby-1模式,也就是待机模式 在待机模式下,若PRIM_RX = 1且CE = 1,会进入接收模式,接收数据 在待机模式下,若PRIM_RX = 0且CE = 1且TX_FIFO非空,会进入发射模式,发送数据 对这张图更详细的解释可以看其他博主的文章【NRF】 |
|
|
|
下面就可以根据这个流程图来编写配置函数了
//拉低CE,注意:需要将CE拉低,使其进入待机或者掉电模式才能读/写nRF寄存器 NRF_CE_LOW; //初始化NRF(看数据手册) NRF_SPI_WriteReg(W_REGISTER | SETUP_AW, 0x03); //配置通信地址的长度,默认值时0x03,即地址长度为5字节 NRF_SPI_WriteReg(W_REGISTER | SETUP_RETR, 0x15); //自动重发延迟为500+86us,重发次数5次 NRF_SPI_WriteReg(W_REGISTER | RF_SETUP, 0x07); //设置发射速率为1MHZ,发射功率为最大值0dB NRF_SPI_WriteReg(W_REGISTER | RF_CH, 30); //设置通道通信频率,工作通道频率可由以下公式计算得出:Fo=(2400+RF_CH)MHz.并且射频收发器工作的频率范围从2.400-2.525GHz ▲先拉低CE,在芯片处于待机或掉电模式,才能对NRF芯片进行寄存器操作 这里配置了通信地址宽度,自动重发,发射速率以及通信频段,具体看注释 //---------------------------PRX:单通道配置------------------------------------------------------------------------ NRF_SPI_WriteBuf(W_REGISTER | RX_ADDR_P0, RX_ADDRESS_0, ADDRESS_WIDTH); //配置本机接收通道0的接收数据的地址 NRF_SPI_WriteReg(W_REGISTER | EN_AA, 0x01); //接收数据后,只允许频道0自动应答 NRF_SPI_WriteReg(W_REGISTER | EN_RXADDR, 0x01); //只允许频道0接收数据 NRF_SPI_WriteReg(W_REGISTER | RX_PW_P0, STATIC_PLOAD_LENGTH); ▲设置PRX模型单通道(默认通道0)的接收地址宽度,自动应答、使能通道0接收功能、接收数据长度(当接收到这么多个数据就会触发IRQ中断) NRF_SPI_WriteReg(W_REGISTER | CONFIG, 0x0C | PWR_UP | PRIM_RX); //默认处于接收模式 ▲然后就是CONFIG寄存器的配置了,该寄存器的最低两位正是前面提到的【PWR_UP】和【PRIM_RX】,而在头文件里我没有解释的一段 //CONFIG寄存器 #define PWR_UP 0x02 //Power Up #define PWR_DOWN 0x00 //Power Down #define PRIM_RX 0x01 //PRX #define PRIM_TX 0x00 //PTX 这正是给CONFIG寄存器配置使用的,配置成【待机/掉电】【PTX/PRX】组合模式,这样提高了代码的阅读性,当然也可以选择直接赋值,由于这是主从一体的程序,初始化默认配置从接收模式,只是在需要发送的时候才进入发送模式 NRF_SPI_WriteBuf(W_REGISTER | TX_ADDR, RX_ADDRESS_0, ADDRESS_WIDTH);//发送的数据包中被一块打包进去的接收端NRF的接收通道的地址 ▲接下来配置PTX模式下的发送地址 看到这里估计大家会有疑问,为什么发送地址写的是接收地址,我觉得这也是很多教程里面存在的一个误导 细心的朋友可能会发现在我贴出来的头文件【NRF.h】里有这么一段 //==========================NRF24L01============================================ #define RX_MULIT_CHANNEL 1 //【0:单通道(默认通道0)】【1:多通道(6通道全开)】 #define DYNAMIC_PLOAD_LENGTH 1 //【0:固定数据长度】【1:动态数据长度】 #define STATIC_PLOAD_LENGTH 5 //设置数据长度(固定数据长度下使用) #define ADDRESS_WIDTH 5 //通道地址宽度 //#define TX_ADR_WIDTH 5 //发送端的接收通道地址宽度 //#define RX_ADR_WIDTH 5 //接收端的接收通道地址宽度 //#define TX_PLOAD_WIDTH 5 //要发送的有效数据长度(固定长度) //#define RX_PLOAD_WIDTH 5 //要接收的有效数据长度(固定长度),这个决定RX端FIFO达到多少数据量后触发中断 这里有四句注释掉的宏定义,在很多教程和例程里都有这么定义,而且在那些例程里除了发送地址【RX_ADDRESS_0[ADDRESS_WIDTH];】之外还有一个接收地址【RX_ADDRESS[ADDRESS_WIDTH];】这两者的赋值是一样的,一个发送地址,一个接收地址 所以那些例程里是这么写的 NRF_SPI_WriteBuf(W_REGISTER | TX_ADDR, TX_ADDRESS, ADDRESS_WIDTH); 但是在我深入了解NRF和编程的途中了解到,其实是不存在所谓的发送地址 【RX_ADDRESS[ADDRESS_WIDTH];】 要知道,两个模块要通信,首先要处于相同的频段、相同的通信速率,也就是一开始那些配置 接着,还要在PTX端发射数据前,先在【TX_ADDR寄存器】填入一个地址,这个地址是会附带在数据前面广播出去,如果PRX端接收到这个地址,并且和自己的【RX_ADDR_Px(x = 0~5)寄存器】里面任意一个地址对应上了,那么PRX端会接收接下来的数据,否则等待下一次数据的到来。 这就说明了【TX_ADDR寄存器】里存放的地址,是PTX端这一次发送希望PRX端用哪一个通道接收,因此填入的是接收端的一个通道地址【RX_ADDRESS_x[ADDRESS_WIDTH]】 NRF_CE_HIGH; delay_ms(2); ▲最后拉高CE,告知NRF芯片,已经完成对你的寄存器操作了 ▼上面的代码整合起来及时NRF模块的初始化函数了 void NRF_Config(void) { NRF_SPI_Config(); //拉低CE,注意:需要将CE拉低,使其进入待机或者掉电模式才能读/写nRF寄存器 NRF_CE_LOW; //初始化NRF(看数据手册) NRF_SPI_WriteReg(W_REGISTER | SETUP_AW, 0x03); //配置通信地址的长度,默认值时0x03,即地址长度为5字节 NRF_SPI_WriteReg(W_REGISTER | SETUP_RETR, 0x15); //自动重发延迟为500+86us,重发次数5次 NRF_SPI_WriteReg(W_REGISTER | RF_SETUP, 0x07); //设置发射速率为1MHZ,发射功率为最大值0dB NRF_SPI_WriteReg(W_REGISTER | RF_CH, 30); //设置通道通信频率,工作通道频率可由以下公式计算得出:Fo=(2400+RF_CH)MHz.并且射频收发器工作的频率范围从2.400-2.525GHz //---------------------------PRX:单通道配置------------------------------------------------------------------------ NRF_SPI_WriteBuf(W_REGISTER | RX_ADDR_P0, RX_ADDRESS_0, ADDRESS_WIDTH); //配置本机接收通道0的接收数据的地址 NRF_SPI_WriteReg(W_REGISTER | EN_AA, 0x01); //接收数据后,只允许频道0自动应答 NRF_SPI_WriteReg(W_REGISTER | EN_RXADDR, 0x01); //只允许频道0接收数据 //---------------------------PRX:数据接收长度配置---------------------------------------------------------------------------- NRF_SPI_WriteReg(W_REGISTER | RX_PW_P0, STATIC_PLOAD_LENGTH); //---------------------------PTX:单通道配置------------------------------------------------------------------------ //发送通道地址,由于是单通道,在这里配置一次就可以了(默认通道0) NRF_SPI_WriteBuf(W_REGISTER | TX_ADDR, RX_ADDRESS_0, ADDRESS_WIDTH); //发送的数据包中被一块打包进去的接收端NRF的接收通道的地址 NRF_SPI_WriteReg(W_REGISTER | CONFIG, 0x0C | PWR_UP | PRIM_RX); //默认处于接收模式 NRF_CE_HIGH; delay_ms(2); } PTX/PRX(发送模式/接收模式) 这是一份主从一体的代码,NRF模块会一直运行在PRX模式下,等待接收数据,只有主动切换到发送模式才能发送数据,发射完成后就切换回接收模式,等待数据 因此接下来是发送和接收模式切换函数 /******************************************************************************** * @brief 配置发送模式(PTX)并进入待机1模式(standby-1),先调用【NRF_CE_LOW】再调用此API 往TX_FIFO写好数据后调用再【NRF_CE_HIGH】,nRF自动延迟130us后会进入发送模式,发送数据 CE为高电平、TX_FIFO为空,会进入待机2模式(standby-2) * @param none * @retval none *******************************************************************************/ void NRF_TX_Mode(void) { NRF_MODE = 0; //模式转换标志位 NRF_SPI_WriteReg(W_REGISTER | STATUS, IRQ_CLEAR); //清除所有中断标志,防止一进入发射模式就触发中断 NRF_SPI_WriteReg(W_REGISTER | CONFIG, 0x0C | PWR_UP | PRIM_TX); //将NRF配置成待机发射模式(PTX模式) delay_ms(2); //nRF延时1.5ms后进入待机1模式(standby-1) // NRF_CE_HIGH; //CE拉高后,nRF自动延迟130us后,进入发送模式 // delay_us(150); //PTX模式下这个延时没用 } /******************************************************************************** * @brief 配置接收模式(PRX)并进入待机1模式(standby-1),先调用【NRF_CE_LOW】再调用此API 在PRX下只有CE保存高电平的期间可以接收数据 * @param none * @retval none *******************************************************************************/ void NRF_RX_Mode(void) { NRF_MODE = 1; //模式转换标志位 // NRF_CE_LOW; //拉低CE,进入待机模式或掉电模式 NRF_SPI_WriteReg(W_REGISTER | STATUS, IRQ_CLEAR); //清除所有中断标志,防止一进入接收模式就触发中断 NRF_SPI_WriteReg(W_REGISTER | CONFIG, 0x0C | PWR_UP | PRIM_RX); //将NRF配置为待机接收模式(PRX) delay_ms(2); //nRF延时1.5ms后进入待机1模式(standby-1) // NRF_CE_HIGH; //CE拉高后,nRF自动延迟130us后,进入接收模式 // delay_us(150); //PRX模式下该延时也可以做其他的处理 } ▲注释都很详细,这里我为了精简代码,将CE的置高置低都注释了,采用外部操作,当然不放心的话也是可以取消注释的,这样做是考虑到调用这两个函数之后还会对其他寄存器进行操作,因此CE先拉高,等所有操作完成才拉低,这在后面的中断函数会具体讲。 这两个函数开头都有对NRF_MODE进行赋值,这是一个u8类型的全局变量,用来判断目前处于那种模式 u8 NRF_MODE = 1; //模式标志位【0:PTX 1:PRX】 |
|
|
|
中断服务处理函数
当芯片接收到数据、发送数据成功、发送失败,都会在IRQ引脚产生一个低电平,而我们对连接IRQ引脚的GPIO设置成了外部中断触发(下降沿),每当有信息来的时候就可以触发中断服务程序 /******************************************************************************** * @brief IQR中断服务处理函数 * @param none * @retval none *******************************************************************************/ void EXTI15_10_IRQHandler(void) { NRF_CE_LOW; //拉低CE,以便读取NRF中STATUS中的数据 status = NRF_SPI_ReadReg(R_REGISTER | STATUS); //读取STATUS的值 status &= IRQ_STATUS; //提取IRQ标志位 switch(status){ case MAX_RT : NRF_SPI_WriteReg(FLUSH_TX, 0xff); //数据发出去没有回应 printf("NO ACKrn"); break; case TX_DS : printf("Send okrn"); //发送数据完成 break; case RX_DR : printf("receivern");NRF_ReceivePacket(); //接收到数据 break; case TX_DS | RX_DR : printf("Auto-ACK-PAYLOADrn"); //发送完成并且接收到的Auto-ACK带有【有效数据】 break; } NRF_SPI_WriteReg(W_REGISTER | STATUS,IRQ_CLEAR);//清除STATUS中断标志位 if(!NRF_MODE){ NRF_RX_Mode(); } NRF_CE_HIGH; EXTI->PR = EXTI_Line15; //清除GPIO的中断标志位 } 还记得在nRF读写API那章节里提到了一个全局变量status吗 u8 status; //接收从STATUS寄存器中返回的值,只要对nRF寄存器进行读写操作,都会刷新该值 在中断处理服务函数里面就有用到这个变量了,是用来存放当前【STATUS寄存器】的值,通过对该值进行分析就可以知道中断的类型,这里用到的宏定义在【NRF.h】里出现过 //发生中断时,根据STATUS寄存器中的值来判断是哪个中断源触发了IRQ中断 #define IRQ_STATUS 0x70 //提取中断标志位 #define IRQ_CLEAR 0x7E //清除中断标志位 #define RX_DR 0x40 //数据接收完成中断 #define TX_DS 0x20 //数据发送完成中断 #define MAX_RT 0x10 //数据包重发次数超过设定值中断 没错,这和配置【CONFIG寄存器】的方式是一样的,至于为什么是这些值,可以根据数据手册对【STATUS寄存器】的解释来计算出,清除中断标志位是要往7:5bit写入1,没有看错,就是写入1 产生中断的时候7:5bit对应的位会置1,清除也是要写1,而且IRQ在产生低电平后,是一直保持低电平的,直到清除【STATUS寄存器】,才会恢复高电平,所以退出中断服务程序前记得清除寄存器 判断为MAX_RT中断的时候,要及时清空【TX_FIFO】内的数据,这个在寄存器解释那里提过了,否则你测试时如果第一次发送数据没有应答,再发送两次也没有成功就会写满【TX_FIFO】,再也发送不了。 判断为TX_DS中断,只有PTX方有这个中断,说明收发双方都正常运行,PTX方的TX_FIFO会自动清除 判断为RX_DR中断,一般是PRX方会有这个中断,这时要主动读取【RX_FIFO】内的数据,读取后nRF会自动清除数据。如果不读取的话,数据会一直保留,接收到三次数据后【RX_FIFO】会写满,不再接收数据(阻塞),等待数据被读走,只有当【RX_FIFO】有空间,才会再次接收数据,因此这里调用了一个接收函数 我在一开始测试的时候是没有设置接收函数的,测试发送三次成功后,就一直都是发送失败,后来才知道是【RX_FIFO】装满了这个原因 在switch判断里还有种中断TX_DS | RX_DR,这是涉及到自动应答Auto-ACK数据包里有没有携带有效数据的判断; 对于PTX,发送数据后会等待ACK,PRX接收到数据后会触发IRQ中断,同时等待130us后,发送Auto-ACK数据包,如果此时PRX的TX_FIFO中有【有效数据】,那么Auto-ACK数据包会携带【有效数据】一起发送给PTX,这时PTX接收到ACK,判断发送成功,TX_DS置1,同时数据包里带有【有效数据】,RX_DR置1,然后产生IRQ中断,那么就会触发这个判断 对于PRX,当PTX再一次发送新的数据包过来时,如果上一次发给PTX的Auto-ACK数据包会携带【有效数据】那么会就会触发该中断,其中TX_DS置1:说明收到新的数据包,RX_DR置1:说明上一次Auto-ACK的有效数据PTX已经接收到了(因为这次接收到的是新的数据包,PTX没有触发超时重发) 在该博主的文章里对中断机制有更详细的介绍【11. nrf24l01的中断详解】 回到中断服务程序,判断中断类型并处理后,就可以清空【STATUS寄存器】 如果进入中断服务程序前是处于PTX模式【NRF_MODE = 0】那么就进入PRX模式,就是说nRF芯片会一直处于接收模式,只有需要发送的时候才会进入接收模式,发送完又进入接收模式 到此,中断服务程序已经结束,退出前记得拉高CE和清除IO口的中断标志就行了 nRF配置总结 这一章节给出了几个函数 void NRF_Config(void); //nRF初始化 void NRF_RX_Mode(void); //配置成PRX void NRF_TX_Mode(void); //配置成PTX void EXTI15_10_IRQHandler(void) //中断服务 但这只是基础的配置函数以及对于IRQ的处理函数,下一章节才开始正式介绍的发送接收函数编写 6、nRF发送数据(单通道) /******************************************************************************** * @brief PTX模式下发送一个固定长度的数据包 * @param RX_ADDRESS_X 发送到的通道地址,在单通道模式下该值无效,默认通过通道0发送 * @param TX_BUFF 待发送的数据缓冲区 * @retval none *******************************************************************************/ void NRF_SendPacket(u8 *TX_BUFF) { NRF_CE_LOW; //拉低CE,进入待机模式或掉电模式 NRF_TX_Mode(); //配置成PTX待机模式 //---------------------------单通道发送地址------------------------------------------------------------ //在NRF_Config里设置一次就行 //将数据写入TX端的FIFO中,写入的个数与TX_PLOAD_WIDTH设置值相同 NRF_SPI_WriteBuf(WR_TX_PLOAD, TX_BUFF, STATIC_PLOAD_LENGTH); NRF_CE_HIGH; //拉高CE,准备发射TX端FIFO中的数据 //CE拉高后,nRF自动延迟130us后,发送数据 } 没错,对于单通道来说,发送函数就这么简单 拉低CE-----调用指令【WR_TX_PLOAD】,把TX_BUFF数组的数据发送到nRF芯片的【TX_FIFO】,这是发送固定长度【STATIC_PLOAD_LENGTH】的数据-----拉高CE 回顾nRF初始化那章节关于芯片运行流程的图,可以知道当CE拉高,处于发送模式,TX_FIFO非空,那么在130us后,芯片会进入发送模式将数据发送出去 7、nRF接收数据(单通道) /******************************************************************************** * @brief PRX模式下从RX_FIFO中接收一个数据包 同时读出通道值 * @param none * @retval none *******************************************************************************/ void NRF_ReceivePacket(void) { u8 i; u32 length; NRF_CE_LOW; length = STATIC_PLOAD_LENGTH; //从RX端的FIFO中读取数据,并存入指定的区域 //注意:读取完FIFO中的数据后,NRF会自动清除其中的数据 NRF_SPI_ReadBuf(RD_RX_PLOAD,RX_FIFO_Buff,STATIC_PLOAD_LENGTH); //读取通道值 RX_pipe = (status & 0x0E) >> 1; //打印数据 printf("pipe%d length:%drn",RX_pipe,length); for(i = 0; i < length; i++){ printf("%x ",RX_FIFO_Buff); } NRF_CE_HIGH; //重新拉高CE,进入接收模式,准备接收下一个数据 } 接收函数也是这么简单 拉低CE-----调用指令【RD_RX_PLOAD】从nRF芯片的RX_FIFO读取【STATIC_PLOAD_LENGTH】个数据,并存放在RX_FIFO_Buff里面-----拉高CE 然后读取通道值,存放在【u8 RX_pipe】用来判断是哪个通道的数据 最后通过串口打印数据 8、nRF24L01一对一通信小结 通过前面几章节介绍,我贴出一个完整头文件以及以下的函数 //nRF配置 void NRF_SPI_Config(void); //nRF的SPI及CE CSN IRQ接口配置 void NRF_Config(void); //nRF初始化 u8 NRF_Check(void); //检查MCU和nRF的连接 //----------------------------------------------------------- //nRF寄存器读写 调用以下API均会刷新status的值 u8 NRF_SPI_ReadReg(u8 reg); u8 NRF_SPI_ReadBuf(u8 reg,u8 *buf,u8 len); u8 NRF_SPI_WriteReg(u8 reg,u8 dat); u8 NRF_SPI_WriteBuf(u8 reg ,u8 *buf,u8 len); //----------------------------------------------------------- //----------------------------------------------------------- //先调用【NRF_CE_LOW;】拉低进入待机模式或掉电模式 //再调用以下API void NRF_RX_Mode(void); //配置成PRX void NRF_TX_Mode(void); //配置成PTX //----------------------------------------------------------- //nRF_API void NRF_ReceivePacket(void); //接收数据包 void NRF_SendPacket(u8 *TX_BUFF); //发送固定长度有效数据包 注意这里的接收函数不同于【NRF.h】里的 void NRF_SendPacket(u8 *RX_ADDRESS_X,u8 *TX_BUFF); 因为现在只是测试一对一通讯是否正常,不需要传递地址进去 到此,单通道固定长度的nRF配置以及完成,可以进行测试了 在主函数里调用以下函数完成配置 NRF_SPI_Config(); NRF_Config(); 然后可以选择循环发送或者按键按一次发送一次数据的方式调用以下函数,记得在定义一个大小等于【STATIC_PLOAD_LENGTH】的数组传递进入 u8 pack[] = {0x11,0x12,0x13,0x14,0x11,0x12,0x13,0x14}; NRF_SendPacket(pack); 以下是我的主函数,采用按键发送,按一次发送一次 u8 pack[] = {0x11,0x12,0x13,0x14,0x11,0x12,0x13,0x14}; /** * @brief 主函数 * @param 无 * @retval 无 */ int main(void) { USART1_Config(115200); KEY_Config(); printf("NRF master testrn"); NRF_SPI_Config(); if(NRF_Check() == SUCCESS){printf("nRF connected with MCU successfulrn");} else{printf("errorrn");} NRF_Config(); delay_ms(100); while(1){ if(Key_flag){ Key_flag = 0; NRF_SendPacket(RX_ADDRESS_1,pack); } } /* add your code here ^_^. */ } 9、nRF多通道配置 另开新帖 |
|
|
|
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1632 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1559 浏览 1 评论
985 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
688 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1605 浏览 2 评论
1869浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
655浏览 4评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
525浏览 3评论
540浏览 3评论
stm32cubemx生成mdk-arm v4项目文件无法打开是什么原因导致的?
512浏览 3评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-25 16:35 , Processed in 0.817909 second(s), Total 55, Slave 48 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号