各位大佬们,大家好,这是我关于嵌入式方向的第一篇文章。本文是基于江协科技的NRF24L01(NRF24L0O1+)无线通信模块的b站视频,大家如果在看到本篇文章后有不懂的可以在评论区和我交流或者去看b站上江协科技的视频,本篇文章涉及到的代码也是参考江协科技的代码,以及一些图片都是截取的是江协科技的PPT,自己写这一篇博客主要是用来复习,以后如果有能力的话,我也会自己更新一些自己摸索出来的模块。下面是江协科技在b站上关于NRF24L01模块的视频链接:【[模块教程] 第2期 NRF24L01无线通信模块】https://www.bilibili.com/video/BV16bN1zZES6

正文开始

1.NRF24L01无线通信模块

NRF24L01无线通信模块在电子开发中非常常用,比如做一个遥控小车,遥控四轴飞行器或者多个单片机需要无线通信,都可以使用这个模块。另一个无线通信模块是蓝牙串口,这个我也会在之后的博客中写到,相比较蓝牙通信,NRF24L01的优点是通信更加直接,不需要配对、连接等繁琐的操作,距离也更远。本篇博客写的代码适用于NRF24L01+模块、NRF24L01模块、Si24L01模块(这个是国产替代芯片),NRF24L01+模块是NRF24L01的升级版本,多了些高级功能。下面是模块的一些图片

第一个图片是引出8个阵脚,第二个图片是贴片式的,可以以贴片的形式安装在PCB上,不过用第二个的模块,会发现经常丢包,通信距离也很短,如果你也遇到这个问题,要在这个模块上的VCC和GND之间加个电容,加个电源滤波效果就好多了。由于这个模块遵循的是SPI通信协议,所以我会先把SPI通信协议讲透,然后再细讲这个模块。

2.SPI通信

2.1 SPI通信简介

SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线

四根通信线:SCKSerial Clock)、MOSIMaster Output Slave Input)、MISOMaster Input Slave Output)、SSSlave Select

同步,全双工,同步的意思是双方在同一个时钟信号的控制下,进行数据的接收和发送,来一个时钟,发送端发送,接收端接收,他们彼此之间的工作状态是一致的。全双工通信。允许数据同时在两个方向上传输,即有两个信道,因此允许同时进行双向传输。

支持总线挂载多设备(一主多从)

2.2硬件电路

所有SPI设备的SCKMOSIMISO分别连在一起

主机另外引出多条SS控制线,分别接到各从机的SS引脚

输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

推挽输出高低电平都会有很强的驱动能力,这将使得SPI引脚的下降沿非常迅速,上升沿也非常迅速,得益于推挽输出的高驱动能力,SPI信号变化的快,那么自然就能达到更高的传输速度,一般SPI的信号都能轻易的达到Mhz的速度级别。如果细心的同学已经发现,对于MISO引脚,主机一个是输入,但是三个从机全都是输出,如果三个从机都是推挽输出,那么势必会导致冲突,所以SPI协议规定当从机的引脚为高电平(即从机未被选中的时候,它的MISO引脚必须切换为高阻态),高阻态就是引脚断开,不输出任何电平,这样就可以防止一条线有多个输出而导致的电平冲突问题了。

下面是硬件电路图

MOSI:主机输出从机输入  MISO :主机输入从机输出,从机输出的数据通过MISO移位到主机,主机输出的数据通过MOSI输出到从机,而且这个移位是高位先行。

2.3移位示意图

每来一个时钟信号,移位寄存器都会向左进行移位,移位寄存的时钟源是由主机提供的,这里叫做波特率发生器,它产生的时钟驱动主机的移位寄存器进行移位,同时,这个时钟也通过SCK引脚进行输出,接到从机的移位寄存器中,假设在SCK上升沿移出数据,在SCK下降沿移入数据,当SCK上升沿时主机移出的数据放到MOSI上,从机移出的数据放到MISO上,当SCK下降沿时,从机在MOSI上移入数据,主机在MISO上移入数据,数据放到通信线上,实际上是放到了输出数据寄存器。如下图所示

重复8次就成功移位了。

2.4SPI时序基本单元

起始单元:SS从高电平切换到低电平

终止单元:SS从低电平切换到高电平

SS是低电平有效的,SS从高电平到低电平就代表选中了某个从机,就是通信的开始。SS从低电平变到高电平就是结束了从机的选中状态,就是通信的结束。

交换一个字节(模式0)

CPOL=0:空闲状态时,SCK为低电平

CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

CPOL就是时钟极性,CPHA时钟相位 MISO在SS引脚未被选中的时候,MISO为高阻态,即当SS为高电平的时候,MISO就为高阻态。或许你看到SCK第一个边沿移入数据这句话会有疑惑,数据不是先要移出才能移入吗,其实这里是SCK第一个边沿之前,就要提前开始移出数据了,或者你可以理解为在第0个边沿移出数据,在第1个边沿移入数据。在这种模式下,SCK上升沿移入数据,下降沿移出数据。在实际应用中,模式0的应用还是比较广泛的,在这里重点了解模式0就行了,后面的程序也是以模式0的代码来写的。

交换一个字节(模式1)

CPOL=0:空闲状态时,SCK为低电平

CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

模式1更符合我们的常识,在这种模式下,在SCK上升沿主机和从机都移出数据,SCK下降沿主机和从机都移入数据。主机移出的B7进入从机移位寄存器的最低位,从机移出的B7进入主机移位寄存器的最低位。这样一个时钟脉冲产生完毕,一个数据位传输完毕

交换一个字节(模式2)

CPOL=1:空闲状态时,SCK为高电平

CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

模式2和模式0的区别就是SCK极性反了,在这种情况下是SCK下降沿移入数据,SCK上升沿移出数据。

交换一个字节(模式3)

CPOL=1:空闲状态时,SCK为高电平

CPHA=1SCK第一个边沿移出数据,第二个边沿移入数据

模式3和模式1的区别就是SCK的极性反了,在这种情况下SCK下降沿移出数据,SCK上升沿移入数据。

综上:模式0和模式3都是SCK上升沿移入数据(采样),下降沿移出数据

模式1和模式2都是SCK上升沿移出数据,下降沿移入数据(采样)

到这里SPI的基础知识我们已经讲完了,这里我们最后讲一下软件SPI和硬件SPI,软件SPI是我们用代码手动翻转电平,来实现时序,硬件SPI(以stm32为例)就是使用STM32内的SPI外设,来实现时序。软件SPI主要的优是方便灵活,硬件SPI主要的优点是高性能,节省软件资源。下面我们写NRF24L01用的是软件SPI,关于硬件SPI就不过多介绍了,各位如果感兴趣的话可以去看看江协科技的视频,讲的非常全面适合新手,下面是链接

【STM32入门教程-2023版 细致讲解 中文字幕】https://www.bilibili.com/video/BV1th411z7sn

3.NRF24L01模块

3.1NRF24L01简介

这篇博客的代码是基于NRF24L01+数据手册-v1..0来写的,这个手册在江协科技的网站上有,在文章最后我会放上链接。下面开始NRF24L01模块的介绍

1.NRF24L01是Nordic Semiconductor公司开发的一款单芯片2.4GHz无线收发器

2.可配置工作频率:2.400GHz~2.525GHz2.400~2.4835GHz为全球ISM频段)

3.可配置数据传输速率:250kbps(仅+版本支持),1Mbps2Mbps

4.可配置3~5字节地址宽度,1~32字节有效载荷宽度

5.可配置发射功率:0dBm-6dBm-12dBm-18dBm

6.1个发送通道,6个接收通道,支持一对多通信

7.Enhanced ShockBurst™,自动数据包组装和定时,自动应答,自动重传

8.其他高级功能:动态包长,应答附加载荷,动态应答

动态包长可以实现数据包包长的任意改变;应答附加载荷就是接收方在应答的同时,也会给发送方传一个数据包;动态应答是发送方发送一个数据包,发送方可以指定接收方需不需要应答。但这些高级功能,我在程序中还不会使用这些功能,在目前的程序中完成基本的功能

3.2NRF24L01框图

现在我们来分析一下这个框图。首先,芯片内部分成三个部分,右边一块是Baseband,基带信号处理,基带信号可以简单理解成1010...这样的二进制数据流,左上角是RF Transmitter,射频发送部分,它的作用是把像1001这样的数据流调制成适合天线发送的高频信号,随后通过天线发送出去,左下角是RF Receiver,是射频接收部分,它的作用是从天线接收高频信号,随后解调成1010...这样的数据流,传入到右边进行处理。

右边的6的引脚就是模块引出的6个引脚,CSN是SPI的片选,加上N之后表示低电平有效,和我们前面讲到的SS是一样的作用;SCK、MISO、MOSI是SPI的通信引脚;IRQ是中断信号输出,当收到数据,发送成功,发送失败这些事件发生时,IRQ引脚会输出低电平跳变,IRQ可以配合STM32的外部中断,来实现中断式收发数据,也可以通过IRQ引脚电平来查询事件有没有发生,我们下面的程序就不使用IRQ引脚了,因为通过读取状态寄存器的值,也可以实现查询的功能。CE引脚主要是用来控制芯片的工作模式,CE置高电平表示进入收发模式,CE置低电平,表示退出收发模式,进入待机或停机模式。通过内部的总线线路,SPI可以读写这个Register map(即寄存器表)

FIFO是一段存储器,可以称之为队列,Tx FIFOs和Rx FIFOs都有三个层级,意思就是可以有三个数据包可以在这里排队,这一点类似于STM32 CAN控制器的发送FIFO和接收FIFO,关于CAN总线的知识,我会在后面的博客中提到,在FIFO里存储的就是最关键的有效数据载荷,也就是待传输的1~32字节数据。

接下来看下面的引脚,VSS是电源负,VDD电源正,IREF是参考电流,接一个电阻到GND就行,DVDD是内部数字电源输出,接个去耦电容就可以了,VDD_PA是功率放大器的电源输出,外面需要通过电路接到天线部分。

XC1和XC2是晶振引脚,外部接晶振,给芯片提供射频信号的时钟;ANT1和ANT2就是天线。

再看发送部分,基带这里提供一个1010...的二进制数据流进入射频发射器后,先进行GFSK调制,将数据流调制到2.4GHz的工作频率下,再通过发送滤波器和PA功率放大器后输出到天线;再看接收部分,天线的信号经过LNA低噪声放大器,接收滤波器后,通过GFSK(高斯频移键控)解调,还原为基带的1010..二进制数据流,随后写入到Rx FIF0中。下图是我从手册中截取的引脚功能图

关于引脚的大概功能,我上面也已经说过,如图有不懂的,可以看一下手册的描述。

3.3 NRF24L01硬件电路设计

该图是芯片的封装,QFN封装,体积比较小。

1到6控制引脚直接引出来就行了,XC引脚接晶振,晶振频率是16Mhz,VDD接电源正,VSS接电源负,大概就是这样,剩下了的部分大家看应该能看懂,具体设计的时候我们就抄这个电路图就行了。

3.4主要工作流程

先看左边,左边是发送数据流程,我们需要使用SPI进行写入,写入寄存器,我们可以指定发送地址,写入TX FIFO,我们可以指定待发送的数据,然后我们控制芯片进入发送模式,进入发送模式后接下来的工作由芯片自动完成。 

再看右边,当接收方通过天线获得调制后的信号时,首先会完成GFSK解调,还原成发送方调制前的1010...数据流,然后接收方拆解数据包,得到地址和数据,之后接收方会比对我们事先写在寄存器中的接收地址,判断这个数据包是不是发给我的,如果地址不匹配,接收方就会把数据包丢掉,如果地址匹配,接收方就会把其中的有效数据拿出来,得到接收数据,写入到 Rx FIFO中,同时置标志位。

3.5数据包格式

这个我们了解一下就行,因为这都是芯片自动完成的,但是看一下这个会对我们理解芯片的工作,会有很大的帮助。

ShockBurst数据包(旧版)

Enhanced ShockBurst数据包

先看旧版的数据包,首先是1个字节的前导码,它是10101010或者01010101,1和0交替变换,目的是方便接收方进行同步,确认数据流1和0的边界,然后加上3到5字节的地址,后面加上1到32位有效数据载荷(也就是要传输的1到32字节有效数据),最后再加上1到2字节CRC效验码。

NRF24L01使用的是增强型ShockBurst数据包,其与旧版的区别就是多了个Packet Control Filed 9 bit,这个前6位是动态包长的长度,这个是为动态包长设计的,它的值表示后面的有效载荷有几个字节;后面是2位的PID(Packet Identity),包标识符,当发送方每发一个新的数据包的时候,PID就会自动自增,而重发的包,PID不会自动增加,这样接收方连续收到两个PID一样的数据包,那它就可以知道,这就是重发的数据包。发送方通过配置NO_ACK来配置接收方是否要应答,如果NO_ACK等于1接收方收到数据时就不要应答。

3.6状态转移图

接下来还有关于状态转移图,由于内容过多,大家就在江协科技的视频看看,或者自己研究手册的第22页,研究手册能帮助你更全面低了解芯片。

下面是我用网易有道词典进行的翻译,大家可以看看。

POWER_DOWN模式

在关机模式下,nRF24L01+ 会以极低的电流消耗被禁用。所有可用的寄存器值都会保持不变,SPI 也会保持激活状态,从而能够实现配置的更改以及数据寄存器的上传/下载操作。有关启动时间,请参见第 24 页表 16 中的内容。.通过将“PWR_UP”位设为“低电平”来进入关机模式。

Standby-I模式

通过将 CONFIG 寄存器中的 PWR_UP 位设为 1,设备将进入待机-I 模式。待机-I 模式用于在保持短启动时间的同时最大限度地减少平均电流消耗。在此模式下,仅部分晶体振荡器处于工作状态。只有在 CE 为高电平时才会切换到活动模式,而当 CE 为低电平时,nRF24L01 会从 TX 和 RX 模式切换回待机-I 模式。

Standby-II模式

在备用-II模式下,额外的时钟缓冲器处于工作状态,所使用的电流比备用-I模式下更多。
如果在具有空闲 TX FIFO 的 PTX 设备上保持 CE 为高电平,nRF24L01+ 将进入备用-II模式。如果向 TX FIFO 上传了一个新的数据包,PLL 立即启动,并在正常的 PLL 稳定延迟(130 微秒)后传输该数据包。
寄存器值保持不变,在这两种备用模式下都可以激活 SPI。有关启动时间,请参见第 24 页的表 16


RX-Mode

RX 模式是一种主动模式,在此模式下,nRF24L01+ 无线电模块用作接收器。要进入此模式,nRF24L01+ 必须将 PWR_UP 位、PRIM_RX 位和 CE 引脚设置为高电平。
在 RX 模式下,接收器会解调来自射频通道的信号,并持续将解调后的数据传递给基带协议引擎。基带协议引擎会持续搜索有效的数据包。如果找到了有效的数据包(通过匹配的地址和有效的 CRC),则数据包的负载会在 RX FIFO 中的空闲槽位中呈现出来。如果 RX FIFO 已满,接收到的数据包将被丢弃。
nRF24L01+ 会一直保持在 RX 模式,直到微控制器将其配置为待机-I 模式或关机模式。然而,如果基带协议引擎中的自动协议功能(增强型冲击突发™)已启用,nRF24L01+ 可以进入其他模式以执行协议。
在 RX 模式下,有一个接收功率检测器(RPD)信号可用。RPD 是当在接收频率通道内检测到高于 -64 dBm 的射频信号时设置为高电平的信号。内部 RPD 信号在传递到 RPD 寄存器之前会进行滤波。射频信号必须存在至少 40 微秒。在将 RPD 设置为较高值之前。关于如何使用 RPD 的说明,请参见第 6.4 节(第 25 页)。

TX-Mode

TX 模式是一种用于传输数据包的激活模式。要进入此模式,nRF24L01+ 必须将 PWR_UP 位设置为高电平、PRIM_RX 位设置为低电平、TX FIFO 中有数据,并且 CE 引脚保持高电平超过 10 微秒。
nRF24L01+ 会一直处于 TX 模式,直到完成数据包的传输。如果 CE = 0,nRF24L01+ 会返回到待机-I 模式。如果 CE = 1,TX FIFO 的状态将决定下一步的操作。如果 TX FIFO 不为空,nRF24L01+ 会继续在 TX 模式下并传输下一个数据包。如果 TX FIFO 为空,nRF24L01+ 会进入待机-II 模式。nRF24L01+ 发射器的 PLL 在 TX 模式下处于开环工作状态。重要的是,切勿让 nRF24L01+ 在一次操作中保持在 TX 模式超过 4 毫秒。如果增强型 ShockBurst™ 功能已启用,nRF24L01+ 在发送模式下的持续时间绝不会超过 4 毫秒。

3.7模式控制总结

掉电模式:  PWR_UP = 0

待机模式I: PWR_UP = 1,CE = 0

接收模式:  PWR_UP = 1,CE = 1,PRIM_RX = 1

发送模式:  PWR_UP = 1,CE = 1,PRIM_RX = 0

待机模式II:PWR_UP = 1,CE = 1,PRIM_RX = 0,发送FIFO空

在这个表里有个细节就是在Tx ModE模式下,如果CE持续等于1,则TX FIFO三个层级的数据会全部清空;如果CE引脚给一个最小10us的高脉冲,则TX FIFO只会清空一个层级,这里CE引脚的持续时间应该小于只发出一个数据包的时间,在一个数据包还没传输完成就把CE指0,这样就能实现只发出一个数据包的目的,不过这里不用担心一个数据包还没发送出去,CE就置0,那么会不会这个数据包不会完全传输出去,答案是不会的。原因如下

3.8一对多

PTX设备发出数据包,PRX设备地址匹配的接收通道收到数据包

PRX设备以相同地址发出应答包,PTX设备接收通道0收到应答包

在这里,我们注意到,PTX设备在发送完数据后会固定在它的接收通道0等待应答包,因此PTX在发送数据包时,除了要指定发送地址和发送数据,还要指定它的接收通道0的接收地址,且接收地址要和发送地址保持一致;只有通道0和通道1的五个字节可以任意配置,通道2到通道5的高四位字节必须和通道1保持一致。

第一个示意图上画的时多发一收,一发多收的情况比较简单,只需要在发送的时候指定不同的发送地址就行了。PTX1的发送地址为0xB3B4B5B6F1,和PRX的接收通道1 一样,所有会被PRX的接收通道1收到,同时PTX1发出数据后要接收应答,它自己的接收通道0的地址为也要设置成相同的0xB3B4B5B6F1,后面的几个设备原理类似。关于一对多的更多描述,可以看手册的39页。

3.9自动应答和自动重传

第一个图是进行正常的数据传输的过程,UP就是Upload,发送方单片机通过SPI写入要发送的数据,写入数据后,PTX开始产生发送波形,PRX接收波形,接收完成的时候PRX的IRQ引脚输出信号RX_DR,表示接收到数据了.MCU PRX可以通过SPI进行DL即Download,在等待130us的时候,PTX进入接收状态,PRX进入发送状态,PRX设备会自动向PRX设备发送一个应答包,PTX设备收到应答包后确认数据发送成功,此时PTX的IRQ引脚产生TX_DS中断,MCU_PTX读到TX_DS后就知道发送成功了。

第二个图显示的就是发送数据包丢包的示意图,第一次发送PID=1的数据包,但这个数据包在传输中丢失了,PTX转入接收模式后没收到应答,在ARD(auto Retransmit Delay)时间后会自动重传,PID任然是1,表示仍然是上一个数据包,后面的过程和上面类似。寄存器里可以配置最大自动重传次数,达到最大自动重传次数后,发送方状态寄存器的MAT_RT位置1,表示达到了最大重传次数但还没有发送成功。

第三图是应答包丢失的例子,首先PTX发送PID等于1的数据包,PRX接收正常,但是PRX在返回应答包的时候出现问题,PTX没有收到应答包,它以为发送失败,PTX会自动重传这个数据包,而PRX重复接收这个数据包,它会直接丢弃。PID设计的目的就是防止应答丢失,造成PRX设备的重复接收。

关于自动应答和自动重传的更多知识在手册的45页。

3.10NRF24L01指令

先看R_REGISTER指令,高三位是指令码000,低五位是指定读取的寄存器的地址。

W_REGISTER指令码高三位是001,低五位是寄存器地址,发完写寄存器指令后,就进行SPI交换发送字节。但这个指令只能在掉电或者待机模式下执行。

R_RX_PAYLOAD是读接收有效载荷,就是读接收FIFO,它的指令码是0110 0001,就是16进制的0x61,发完这个指令后后续可以跟1到32位字节的SPI交换接收。FIFO里的有效载荷会在读取后会被删除,这个指令在Rx模式中使用。

W_TX_PAYLOAD是写发送有效载荷,指令码是1010 0000,是16进制的0xA0,这个指令后续跟1到32位字节的写操作,写入的数据会进入TX FIFO后。

FLUSH_TX是清空发送FIFO所有层级的所有数据,指令码是1110 0001,十六进制0xE1,Data Bytes为0意思是这个指令是单独指令后续不用跟任何的写操作和读操作。

FLUSH_RX是清空接收FIFO所有层级的所有数据,指令码是1110 0010,是16进制的0xE2

REUSE_TX_PL是重新使用发送有效载荷,指令码是1110 0011,就是16进制的0xE3

R_RX_PL_WID用于动态包长模式,接收方使用这个指令可以知道读出来的数据包,它的动态包长度是多少

W_ACK_PAYLOAD用于应答附加载荷模式,接收方使用这个指令可以写入应答时要附加的载荷是什么

W_TX_PAYLOAD_NOACK用于动态应答模式,发送使用这个指令写入发送数据时,接收方收到时将不会进行应答。

NOP是空指令,指令码是1111 1111,当主机发送这个指令后,从机收到空指令后什么都不做,但主机会得到从机状态寄存器的内容,也就相当于“抛砖引玉”了。

3.11NRF24L01寄存器

在后续的程序中,我们用宏定义来定义每个寄存器。下面我们对照手册一个个来看

手册中第9章讲的就是寄存器的定义

CONFIG的地址是00,描述是配置寄存器CONFIG的位0PRIM_RX写0是主发送模式,写1是主接收模式,次低位PWR_UP可读可写,1表示上电,0表示掉电.最低两位非常重要,在切换工作模式的时候需要和CE引脚配合。CRC0的作用是控制CRC的字节数,EN_CRC使能CRC功能。Bit4,5,6用于配置IRQ引脚。

该地址的寄存器的作用是使能自动应答功能。

该地址寄存器的作用是使能接收通道,相当于接收通道的开关。

03地址下的寄存器的作用是设置地址宽度,具体描述见图。

04地址下的寄存器的作用是设置自动重传

05地址下的寄存器是射频通道的配置,默认的工作频率是2.402GHz

07地址下寄存器的作用是折射频参数设置

这个寄存器比较重要,我们写代码时要用到很多次数,Bit0是发送FIFO满,写三次FIFO后,此标志位会被置1,这一位是只读的。然后Bit1到Bit3是RX_FIFO读取有效载荷,它的通道号,用于一对多的接收。RX_DR当新数据到达RX FIFO时置1.TX_DS的意思时数据已发送TX中断,当数据包在TX上成功发送时置1,如果激活了自动应答,则只有在收到ACK时才将次位置1.MAX_RT最大TX重传中断,写1清除此位。

该寄存器的作用是发送观察寄存器

该寄存器的作用是接收器功率检测。

RX_ADDR_P0寄存器比较特殊,一个地址下就有5个字节,描述数据通道0的地址,最大长度为5个字节,该寄存器写入SETUP_AW定义的字节数,SETUP_AW是设置地址宽度,3到5字节,低位先行;同理RX_ADDR_P1也是一样的分析方法;后面几个配置的是2,3,4,5的最低一个字节,因为之前说过2,3,4,5的接收地址的高四位字节数据与接收通道1的高四位字节数据一样。

发送地址寄存器,一次性可以写入5个字节,表示发送地址

这些寄存器分别设置对应接收通道的数据宽度,写入0表示通道0不使用,有效值是1到32位字节。这里需要注意的是发送的字节数要和接收通道配置的字节数保持一致,不然接收方解析数据包就会出错

RX_EMPTY,1表示RX FIFO空,0表示RX FIFO有数据,其他具体细节看寄存器。读取这个寄存器可以知道发送和接受FIFO的状态。 

3.12操作实例

红色部分是需要关注的发送数据,绿色部分是需要关注的接受数据,黑色数据是无关数据。因为到现在实在写不动了,大家自己看看吧或者去看江协科技的视频。

4.代码部分

本代码涉及到的单片的是stm32f103c8t6,标准库,软件是keil5, 本代码借鉴了江协科技的代码。这里我直接把最后的代码写上去,把我认为难理解的加上注释,下面是接线图。

NRF24L01模块最终实现现象

NRF24L01.h

#ifndef __NRF24L01_H
#define __NRF24L01_H

extern uint8_t NRF24L01_TxPacket[];
extern uint8_t NRF24L01_RxPacket[];

#include "NRF24L01_Define.h"
//引脚配置
void NRF24L01_W_CE(uint8_t BitValue);
void NRF24L01_W_CSN(uint8_t BitValue);
void NRF24L01_W_SCK(uint8_t BitValue);
void NRF24L01_W_MOSI(uint8_t BitValue);
uint8_t NRF24L01_R_MISO(void);
void NRF24L01_GPIO_Init(void);
uint8_t NRF24L01_SPI_SwapByte(uint8_t Byte);

//指令实现
void NRF24L01_WriteReg(uint8_t RegAddress,uint8_t Data);
uint8_t NRF24L01_ReadReg(uint8_t RegAddress);
void NRF24L01_WriteRegs(uint8_t RegAddress,uint8_t *DataArray,uint8_t Count);
void  NRF24L01_ReadRegs(uint8_t RegAddress,uint8_t *DataArray,uint8_t Count);
void NRF24L01_WriteTxPayload(uint8_t *DataArray,uint8_t Count);
void NRF24L01_ReadRxPayload(uint8_t *DataArray,uint8_t Count);
void NRF24L01_FlushTx(void);
void NRF24L01_FlushRx(void);
uint8_t NRF24L01_ReadStatus(void);


//功能函数
void NRF24L01_PowerDown(void);
void NRF24L01_StandbyI(void);
void NRF24L01_RxMode(void);
void NRF24L01_TxMode(void);

void NRF24L01_Init(void);
uint8_t NRF24L01_Send(void);
uint8_t NRF24L01_Receive(void);
void NRF24L01_UpdateRxAddress(void);
#endif

NRF24L01.c文件

#include "stm32f10x.h"                  // Device header
#include "NRF24L01_Define.h"
//这里使用的是软件SPI
//引脚配置

/*发送部分*/
uint8_t NRF24L01_TxAddress[5] = {0x11, 0x22, 0x33, 0x44, 0x55};		//发送地址,固定5字节
#define NRF24L01_TX_PACKET_WIDTH		4							//发送数据包宽度,范围:1~32字节
uint8_t NRF24L01_TxPacket[NRF24L01_TX_PACKET_WIDTH];				//发送数据包

/*接收部分*/
uint8_t NRF24L01_RxAddress[5] = {0x11, 0x22, 0x33, 0x44, 0x55};		//接收通道0地址,固定5字节
#define NRF24L01_RX_PACKET_WIDTH		4							//接收通道0数据包宽度,范围:1~32字节
uint8_t NRF24L01_RxPacket[NRF24L01_RX_PACKET_WIDTH];				//接收数据包


void NRF24L01_W_CE(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)BitValue);
}

void NRF24L01_W_CSN(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_1,(BitAction)BitValue);
}

void NRF24L01_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_2,(BitAction)BitValue);
}

void NRF24L01_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA,GPIO_Pin_3,(BitAction)BitValue);
}

uint8_t NRF24L01_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_4);
}

void NRF24L01_GPIO_Init(void)
{
	/*开启GPIO时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP; //推挽输出
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0| GPIO_Pin_1 | GPIO_Pin_2 |GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//这个参数选什么都没什么太大影响
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉输入
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//这个参数选什么都没什么太大影响
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	NRF24L01_W_CE(0); //CE默认为0,退出收发模式
	NRF24L01_W_CSN(1); //CSN默认为1,不选中从机
	NRF24L01_W_SCK(0); //SCK默认为0,对应SPI模式0
	NRF24L01_W_MOSI(0); //MOSI默认电平随意,1和0均可
}


//通信协议
uint8_t NRF24L01_SPI_SwapByte(uint8_t Byte)
{
	uint8_t i=0;
	for(i=0;i<8;i++)
	{
		//SPI移出数据
		if(Byte& 0x80)
		{
			NRF24L01_W_MOSI(1);
		}
		else
		{
			NRF24L01_W_MOSI(0);
		}
		Byte<<=1;
		//SCK置高电平
		NRF24L01_W_SCK(1);
		//SPI移入数据
		if(NRF24L01_R_MISO())
		{
			Byte |=0x01;
		}
		else
		{	
			Byte &= ~0x01;
		}	 
		//SCK置低电平
		NRF24L01_W_SCK(0);		
	}
	return Byte;
}

//指令实现
void NRF24L01_WriteReg(uint8_t RegAddress,uint8_t Data)
{
	NRF24L01_W_CSN(0);
	NRF24L01_SPI_SwapByte(NRF24L01_W_REGISTER|RegAddress);
	NRF24L01_SPI_SwapByte(Data);
	NRF24L01_W_CSN(1);
}


uint8_t NRF24L01_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	/*CSN置低,通信开始*/
	NRF24L01_W_CSN(0);
	
	/*交换发送一个字节,通信开始的第一个字节为指令码,读寄存器(低5位为寄存器地址)*/
	NRF24L01_SPI_SwapByte(NRF24L01_R_REGISTER|RegAddress);
	
	/*发送读寄存器指令后,开始交换接收,得到指定地址的数据*/
	Data = NRF24L01_SPI_SwapByte(NRF24L01_NOP);
	
	/*CSN置高,通信结束*/
	NRF24L01_W_CSN(1);
	
	/*返回读到的一个字节数据*/
	return Data;
}


void NRF24L01_WriteRegs(uint8_t RegAddress,uint8_t *DataArray,uint8_t Count)
{
	uint8_t i;
	NRF24L01_W_CSN(0);
	NRF24L01_SPI_SwapByte(NRF24L01_W_REGISTER|RegAddress);
	for(i=0;i<Count;i++)
	{
		NRF24L01_SPI_SwapByte(DataArray[i]);
	}
	NRF24L01_W_CSN(1);
}

void NRF24L01_ReadRegs(uint8_t RegAddress,uint8_t *DataArray,uint8_t Count)
{
	uint8_t i;
	NRF24L01_W_CSN(0);
	NRF24L01_SPI_SwapByte(NRF24L01_R_REGISTER|RegAddress);
	for(i=0;i<Count;i++)
	{
		DataArray[i] = NRF24L01_SPI_SwapByte(NRF24L01_NOP);
	}
	NRF24L01_W_CSN(1);
}

//写发送有效载荷
void NRF24L01_WriteTxPayload(uint8_t *DataArray,uint8_t Count)
{
	uint8_t i;
	NRF24L01_W_CSN(0);
	NRF24L01_SPI_SwapByte(NRF24L01_W_TX_PAYLOAD);
	for(i=0;i<Count;i++)
	{
		NRF24L01_SPI_SwapByte(DataArray[i]);
	}
	NRF24L01_W_CSN(1);
}

//读接收有效载荷
void NRF24L01_ReadRxPayload(uint8_t *DataArray,uint8_t Count)
{
	uint8_t i;
	NRF24L01_W_CSN(0);
	NRF24L01_SPI_SwapByte(NRF24L01_R_RX_PAYLOAD);
	for(i=0;i<Count;i++)
	{
		DataArray[i] = NRF24L01_SPI_SwapByte(NRF24L01_NOP);
	}
	NRF24L01_W_CSN(1);
}

//清空发送FIFO
void NRF24L01_FlushTx(void)
{
	NRF24L01_W_CSN(0);
	NRF24L01_SPI_SwapByte(NRF24L01_FLUSH_TX);
	NRF24L01_W_CSN(1);
}

//清空接收FIFO
void NRF24L01_FlushRx(void)
{
	NRF24L01_W_CSN(0);
	NRF24L01_SPI_SwapByte(NRF24L01_FLUSH_RX);
	NRF24L01_W_CSN(1);
}

//快速读取状态寄存器
uint8_t NRF24L01_ReadStatus(void)
{
	uint8_t Status;
	NRF24L01_W_CSN(0);
	Status=NRF24L01_SPI_SwapByte(NRF24L01_NOP);
	NRF24L01_W_CSN(1);
	return Status;
}

//功能函数

//NRF24L01进入掉电模式
void NRF24L01_PowerDown(void)
{
	uint8_t Config;
	
	NRF24L01_W_CE(0);/*CE置0,退出收发模式*/
	
	Config=NRF24L01_ReadReg(NRF24L01_CONFIG);
	Config &=~0x02;
	NRF24L01_WriteReg(NRF24L01_CONFIG,Config);
}


//NRF24L01进入待机模式1(CE = 0,PWR_UP = 1)
void NRF24L01_StandbyI(void)
{
	uint8_t Config;
	
	NRF24L01_W_CE(0);/*CE置0,退出收发模式*/
	
	Config=NRF24L01_ReadReg(NRF24L01_CONFIG);
	Config |=0x02;
	NRF24L01_WriteReg(NRF24L01_CONFIG,Config);
}

//NRF24L01进入接收模式(CE = 1,PWR_UP = 1,PRIM_RX = 1)
void NRF24L01_RxMode(void)
{
	uint8_t Config;
	
	NRF24L01_W_CE(0);/*CE置0,退出收发模式*/
	
	Config=NRF24L01_ReadReg(NRF24L01_CONFIG);
	Config |=0x03;
	NRF24L01_WriteReg(NRF24L01_CONFIG,Config);
	
	NRF24L01_W_CE(1);
}

//NRF24L01进入发送模式(CE = 1,PWR_UP = 1,PRIM_RX = 0)
void NRF24L01_TxMode(void)
{
	uint8_t Config;
	
	NRF24L01_W_CE(0);/*CE置0,退出收发模式*/
	
	Config=NRF24L01_ReadReg(NRF24L01_CONFIG);
	Config |=0x02;
	Config &=~0x01;
	NRF24L01_WriteReg(NRF24L01_CONFIG,Config);
	
	NRF24L01_W_CE(1);
}


void NRF24L01_Init(void)
{
	NRF24L01_GPIO_Init();
	
	NRF24L01_WriteReg(NRF24L01_CONFIG, 0x08);		//配置寄存器,不屏蔽中断,使能CRC,CRC为1字节,PWR_UP = 0,PRIM_RX = 0
	NRF24L01_WriteReg(NRF24L01_EN_AA, 0x3F);		//使能自动应答,开启接收通道0~通道5的自动应答
	NRF24L01_WriteReg(NRF24L01_EN_RXADDR, 0x01);	//使能接收通道,只开启接收通道0
	NRF24L01_WriteReg(NRF24L01_SETUP_AW, 0x03);		//设置地址宽度,地址宽度为5字节
	NRF24L01_WriteReg(NRF24L01_SETUP_RETR, 0x03);	//设置自动重传,间隔250us,重传3次
	NRF24L01_WriteReg(NRF24L01_RF_CH, 0x02);		//射频通道,频率为(2400 + 2)MHz = 2.402GHz
	NRF24L01_WriteReg(NRF24L01_RF_SETUP, 0x0E);		//射频设置,通信速率为2Mbps,发射功率为0dBm
	
	/*接收通道0地址,设置为全局数组NRF24L01_RxAddress指定的地址,地址宽度固定为5字节*/
	NRF24L01_WriteRegs(NRF24L01_RX_ADDR_P0, NRF24L01_RxAddress, 5);
	
	/*接收通道0的数据包宽度,设置为宏定义NRF24L01_RX_PACKET_WIDTH指定的值*/
	NRF24L01_WriteReg(NRF24L01_RX_PW_P0, NRF24L01_RX_PACKET_WIDTH);
	
	/*清空Tx FIFO的所有数据*/
	NRF24L01_FlushTx();
	
	/*清空Rx FIFO的所有数据*/
	NRF24L01_FlushRx();
	
	/*给状态寄存器的位4(MAX_RT)、位5(TX_DS)和位6(RX_DR)写1,清标志位*/
	NRF24L01_WriteReg(NRF24L01_STATUS, 0x70);
	
	/*初始化配置完成,芯片默认进入接收模式*/
	NRF24L01_RxMode();
}

uint8_t NRF24L01_Send(void)
{
	uint8_t Status;
	uint8_t SendFlag;
	uint32_t Timeout;
	
	/*发送地址,设置为全局数组NRF24L01_TxAddress指定的地址,地址宽度固定为5字节*/
	NRF24L01_WriteRegs(NRF24L01_TX_ADDR, NRF24L01_TxAddress, 5);
	
	/*写发送有效载荷,写入全局数组NRF24L01_TxPacket指定的数据,数据宽度为NRF24L01_TX_PACKET_WIDTH*/
	NRF24L01_WriteTxPayload(NRF24L01_TxPacket, NRF24L01_TX_PACKET_WIDTH);
	
	/*接收通道0地址,此处必须也设置为发送地址,用于接收应答*/
	NRF24L01_WriteRegs(NRF24L01_RX_ADDR_P0, NRF24L01_TxAddress, 5);
	
	/*发送的地址和有效载荷写入完成,进入发送模式,开始发送数据*/
	NRF24L01_TxMode();
	
	/*指定超时时间,即循环读取状态寄存器的次数,具体值可以实测确定*/
	Timeout = 10000;
	
	/*循环读取状态寄存器*/
	while (1)
	{
		/*读取状态寄存器,保存至Status变量*/
		Status = NRF24L01_ReadStatus();
		
		/*超时计次*/
		Timeout --;
		if (Timeout == 0)			//如果计次减至0
		{
			SendFlag = 4;			//发送超时,置标志位为4
			NRF24L01_Init();		//发送出错,重新初始化一次设备,这样有助于设备从错误中恢复正常
			break;					//跳出循环
		}
		
		/*根据状态寄存器的值,判断发送状态*/
		if ((Status & 0x30) == 0x30)		//状态寄存器位4(MAX_RT)和位5(TX_DS)同时为1
		{
			SendFlag = 3;			//状态寄存器的值不合法,置标志位为3
			NRF24L01_Init();		//发送出错,重新初始化一次设备,这样有助于设备从错误中恢复正常
			break;					//跳出循环
		}
		else if ((Status & 0x10) == 0x10)	//状态寄存器位4(MAX_RT)为1
		{
			SendFlag = 2;			//达到了最大重发次数仍未收到应答,置标志位为2
			NRF24L01_Init();		//发送出错,重新初始化一次设备,这样有助于设备从错误中恢复正常
			break;					//跳出循环
		}
		else if ((Status & 0x20) == 0x20)	//状态寄存器位5(TX_DS)为1
		{
			SendFlag = 1;			//发送成功,无错误,置标志位为1
			break;					//跳出循环
		}
	}
	
	/*给状态寄存器的位4(MAX_RT)和位5(TX_DS)写1,清标志位*/
	NRF24L01_WriteReg(NRF24L01_STATUS, 0x30);
	
	/*清空Tx FIFO的所有数据*/
	NRF24L01_FlushTx();
	
	/*发送完成后,恢复接收通道0原来的地址*/
	/*如果发送地址和接收通道0地址设置相同,则可不执行这一句*/
	NRF24L01_WriteRegs(NRF24L01_RX_ADDR_P0, NRF24L01_RxAddress, 5);
	
	/*发送完成,芯片恢复为接收模式*/
	NRF24L01_RxMode();
		
	/*返回发送标志位*/
	return SendFlag;
}

//NRF24L01接收数据包
uint8_t NRF24L01_Receive(void)
{
	uint8_t Status, Config;
	uint8_t ReceiveFlag;
	
	/*读取状态寄存器,保存至Status变量*/
	Status = NRF24L01_ReadStatus();
	
	/*读取配置寄存器,保存至Config变量*/
	Config = NRF24L01_ReadReg(NRF24L01_CONFIG);
	
	/*根据配置寄存器和状态寄存器的值,判断接收状态*/
	if ((Config & 0x02) == 0x00)		//配置寄存器位1(PWR_UP)为0
	{
		ReceiveFlag = 3;				//设备仍处于掉电模式,置标志位为3
		NRF24L01_Init();				//接收出错,重新初始化一次设备,这样有助于设备从错误中恢复正常
	}
	else if ((Status & 0x30) == 0x30)	//状态寄存器位4(MAX_RT)和位5(TX_DS)同时为1
	{
		ReceiveFlag = 2;				//状态寄存器的值不合法,置标志位为2
		NRF24L01_Init();				//接收出错,重新初始化一次设备,这样有助于设备从错误中恢复正常
	}
	else if ((Status & 0x40) == 0x40)	//状态寄存器位6(RX_DR)为1
	{
		ReceiveFlag = 1;				//接收到数据,置标志位为1
		
		/*读接收有效载荷,存放在全局数组NRF24L01_RxPacket中,数据宽度为NRF24L01_RX_PACKET_WIDTH*/
		NRF24L01_ReadRxPayload(NRF24L01_RxPacket, NRF24L01_RX_PACKET_WIDTH);
		
		/*给状态寄存器的位6(RX_DR)写1,清标志位*/
		NRF24L01_WriteReg(NRF24L01_STATUS, 0x40);

		/*清空Rx FIFO的所有数据*/
		NRF24L01_FlushRx();
	}
	else
	{
		ReceiveFlag = 0;				//未接收到数据,置标志位为0
	}
	
	/*返回接收标志位*/
	return ReceiveFlag;
}


//NRF24L01更新接收地址
void NRF24L01_UpdateRxAddress(void)
{
	/*接收通道0地址,设置为全局数组NRF24L01_RxAddress指定的地址,地址宽度固定为5字节*/
	NRF24L01_WriteRegs(NRF24L01_RX_ADDR_P0, NRF24L01_RxAddress, 5);
}

main函数

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "NRF24L01.h"
#include "Key.h"

uint8_t KeyNum;

uint8_t SendFlag;								//发送标志位
uint8_t SendSuccessCount, SendFailedCount;		//发送成功计次,发送失败计次

uint8_t ReceiveFlag;							//接收标志位
uint8_t ReceiveSuccessCount, ReceiveFailedCount;//接收成功计次,接收失败计次

int main(void)
{
	/*初始化*/
	OLED_Init();
	Key_Init();
	NRF24L01_Init();
	
	/*OLED显示静态字符串*/
	OLED_ShowString(1, 1, "T:000-000-0");		//格式为:T:发送成功计次-发送失败计次-发送标志位
	OLED_ShowString(3, 1, "R:000-000-0");		//格式为:R:接收成功计次-接收失败计次-接收标志位
	
	/*初始化测试数据,此处值为任意设定,便于观察实验现象*/
	NRF24L01_TxPacket[0] = 0x00;
	NRF24L01_TxPacket[1] = 0x01;
	NRF24L01_TxPacket[2] = 0x02;
	NRF24L01_TxPacket[3] = 0x03;
	
	while (1)
	{
		KeyNum = Key_GetNum();			//读取按键,获取键码
		
		if (KeyNum == 1)				//K1按下
		{
			/*变换测试数据,便于观察实验现象*/
			/*实际项目中,可以将待发送的数据赋值给NRF24L01_TxPacket数组*/
			NRF24L01_TxPacket[0] ++;
			NRF24L01_TxPacket[1] ++;
			NRF24L01_TxPacket[2] ++;
			NRF24L01_TxPacket[3] ++;
			
			/*调用NRF24L01_Send函数,发送数据,同时返回发送标志位,方便用户了解发送状态*/
			/*发送标志位与发送状态的对应关系,可以转到此函数定义上方查看*/
			SendFlag = NRF24L01_Send();
			
			if (SendFlag == 1)			//发送标志位为1,表示发送成功
			{
				SendSuccessCount ++;	//发送成功计次变量自增
			}
			else						//发送标志位不为1,即2/3/4,表示发送不成功
			{
				SendFailedCount ++;		//发送失败计次变量自增
			}
			
			OLED_ShowNum(1, 3, SendSuccessCount, 3);	//显示发送成功次数
			OLED_ShowNum(1, 7, SendFailedCount, 3);		//显示发送失败次数
			OLED_ShowNum(1, 11, SendFlag, 1);			//显示最近一次的发送标志位
			
			/*显示发送数据*/
			OLED_ShowHexNum(2, 1, NRF24L01_TxPacket[0], 2);
			OLED_ShowHexNum(2, 4, NRF24L01_TxPacket[1], 2);
			OLED_ShowHexNum(2, 7, NRF24L01_TxPacket[2], 2);
			OLED_ShowHexNum(2, 10, NRF24L01_TxPacket[3], 2);
			
			/*TX字符串闪烁一次,表明发送了一次数据*/
			OLED_ShowString(1, 15, "TX");
			Delay_ms(100);
			OLED_ShowString(1, 15, "  ");
		}
		
		/*主循环内循环执行NRF24L01_Receive函数,接收数据,同时返回接收标志位,方便用户了解接收状态*/
		/*接收标志位与接收状态的对应关系,可以转到此函数定义上方查看*/
		ReceiveFlag = NRF24L01_Receive();
		
		if (ReceiveFlag)				//接收标志位不为0,表示收到了一个数据包
		{
			if (ReceiveFlag == 1)		//接收标志位为1,表示接收成功
			{
				ReceiveSuccessCount ++;	//接收成功计次变量自增
			}
			else	//接收标志位不为0也不为1,即2/3,表示此次接收产生了错误,错误接收的数据不应该使用
			{
				ReceiveFailedCount ++;	//接收失败计次变量自增
			}
			
			OLED_ShowNum(3, 3, ReceiveSuccessCount, 3);	//显示接收成功次数
			OLED_ShowNum(3, 7, ReceiveFailedCount, 3);	//显示接收失败次数
			OLED_ShowNum(3, 11, ReceiveFlag, 1);		//显示最近一次的接收标志位
			
			/*显示接收数据*/
			OLED_ShowHexNum(4, 1, NRF24L01_RxPacket[0], 2);
			OLED_ShowHexNum(4, 4, NRF24L01_RxPacket[1], 2);
			OLED_ShowHexNum(4, 7, NRF24L01_RxPacket[2], 2);
			OLED_ShowHexNum(4, 10, NRF24L01_RxPacket[3], 2);
			
			/*RX字符串闪烁一次,表明接收到了一次数据*/
			OLED_ShowString(3, 15, "RX");
			Delay_ms(100);
			OLED_ShowString(3, 15, "  ");
		}
	}
}

在视频的最后,我再说一下,本篇博客是基于江协科技的NRF24L01的模块讲解视频的,代码和其中的一些图片也是用的江协科技的,自己写这篇博客主要是用来复习,由于后面实在写不下去了,所以寄存器的内容有点少,大家可以自己看手册或者看江协科技的视频,下面是江协科技提供的所有文件的链接:

https://pan.baidu.com/s/117wvKBy6rFLge7M-m-TqCg?pwd=6pwu

最后提醒一下大家在写初始化函数的时候千万别忘了把时钟打开,我就是因为这个检查了好长时间都没检查出来,直到今天无意间才看出来,之前一直以为是模块问题,还重新买了模块,耽误了好长时间。

由于自己要准备比赛,在很长时间内我都可能不会更新博客,还请大家见谅。

下面是我关于嵌入式的仓库,这次的代码已经放在上面了,以后我也会把一些有关嵌入式的代码放在仓库里面,大家可以关注一下。

https://gitee.com/qi-jun-study/embedded.git

最后,谢谢大家能看到这里,感谢大家的支持!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐