| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 嵌入式 -> Portapack应用开发教程(十七)nrf24l01发射 C -> 正文阅读 |
|
[嵌入式]Portapack应用开发教程(十七)nrf24l01发射 C |
接下来看一下几个相关项目的代码。重点看看调制部分是如何实现的。 从难易程度排序,我认为最好先看send_simplified项目,然后再看send和recv项目,最后看BTLE项目(HackRF发射)。 send_simplified项目: btle_nrf24l01/send_simpified.ino at main · jamesshao8/btle_nrf24l01 · GitHub 这个项目十分简单,里面只有一个ino文件,没有调用RF24库,而是直接完成了编码和底层调用。 开头的几个函数btLeCrc, swapbits, btLeWhitten, btLeWhitenStart, btLePacketEncode都是用于蓝牙包编码的。其中btLePacketEncode调用了其他几个函数,实现的功能类似上一篇提到的把info bit重新编码为phy bit,以便进一步调制。 另外spi_byte, nrf_cmd, nrf_simplebyte, nrf_manybytes,这些函数都是用于底层通信的,后面几个函数都在调用spi_byte,它是最终给SPI总线发数据的接口。 运行的主要流程是先在setup里完成一次性的工作,比如设置nrf24l01硬件模式,包括广播包的标志(类似mac地址)也是setup里做的。 另一个loop函数才是后面重复循环运行的主要内容。开头都在打包,把mac地址,显示名称,数据以及它们的包头、长度都打在buf中,相当于buf先存入了info bit,然后用btLePacketEncode把buf里的内容转换为phy bit,最终再用spi_byte发到SPI总线上去,之后的工作应该是调制,但是这部分没有在代码里实现,这是因为这部分工作完全由nrf24l01芯片负责,arduino单片机不用管这部分的工作。(除此之外,还有对CSN CE引脚的操作,但是这部分代码比较简单,直接用我的代码能跑起来就行,不用深入研究。) 这样send_simplified就看完了。另外两个send/recv项目虽然代码量更多,但其实也是类似原理,arduino只负责编码或者解码,不负责调制和解调,往SPI总线写入和从SPI总线读出的都是phy bit,而不是无线电通信里的sample。 接下来看一下send项目: btle_nrf24l01/send at main · jamesshao8/btle_nrf24l01 · GitHub 这个项目功能与前面那个项目差不多,但是结构复杂很多。send.ino是主程序,它会调用BTLE.cpp(这是收发低功耗蓝牙数据包共用的库),它又调用了RF24库,对nrf24l01的底层驱动进行了封装,有了RF24就不需要手动往SPI总线上读写数据了。 send.ino里的工作也比较简单,首先初始化了RF24的radio,定义了CSN CE的引脚,还把radio传给了BTLE类,这个很重要,接下来有趣的硬件操作都是围绕着这个radio展开的,而这两个库的类就是在这里建立了联系。 然后setup函数里,btle.begin函数设定了要发送的名字SHARF。后面loop里btle.advertise就是用于循环重复发射数据包的函数。 btle中的begin,包含了先初始化radio也就是radio.begin(往下看的话,都是在RF24.cpp里,最终都是在往SPI总线发命令设置硬件模式)。然后是setAutoAck?disableCRC等,这是因为nrf和btle数据包要求不一样,所以要禁用nrf硬件里的crc计算,改用单片机里的代码实现crc,另外也要考虑到半双工通信,所以得把ack关掉,要不然没法和hackrf通信。 然后看看btle里的advertise,它会调用prepare_packet和transmit_packet。前者是在设置发送端的mac地址和名称,后者负责调用whiten和swapbuf函数做编码,最终调用radio->write函数把output里的phy bit发出去。而radio->write,而这个write是在RF24库里的,它调用startFastWrite,startFastWrite又调用write_payload,write_payload里是最终调用SPI总线上的数据发送函数。这样就把phy bit经过spi总线送给nrf24l01,并由硬件进行调制发到空中了。 recv项目: https://github.com/jamesshao8/btle_nrf24l01/tree/main/recv 它其实和send结构很像,也是一个recv.ino,然后调用BTLE和RF24,这两个库一模一样,只是接收用到的函数可能不一样。?recv.ino一开始初始化radio后,就不停从btle.buffer.payload[i]读数据,并挑出重要部分显示出来。说明btle.buffer.payload[i]对应的是解调+解码后的info bit。还有个btle.listen也很重要,loop不停调用它,才可以不停收数据。打开BTLE.cpp,找到listen函数,它做的就是不停把radio->read读到的数据存到inbuf里,然后用swapbuf和whitten做蓝牙解码。说明inbuf里一开始是phy bit,解码后就变为info bit了,最后再检查crc没问题的话就算是最终解码出来的数据了,这个inbuf和前面说的btle.buffer是同一段内存空间,可以认为就是同一个东西。 然后,我们看看radio->read是怎样读到phy bit的。在RF24.cpp里read调用read_payload,read_payload也是在操纵SPI总线,从总线里读出的数据。说明nrf24l01已经完成了解调,直接把解调输出的phy bit送给了read函数。 这样这3个简单项目就算都看完了。都只做了编解码,没做调制解调。要看调制解调还是得看HackRF用的BTLE项目。 这个项目虽然代码量不少,但是我们感兴趣的只是btle_tx,只对应一个文件: BTLE/btle_tx.c at master · jamesshao8/BTLE · GitHub 打开btle_tx.c后,发现代码也很长,但是其实我们关心的只是一部分,因为用到的只是其中一种类型的数据包而已。? 先找到main函数,它内部调用了2个主要函数parse_input和init_board,init_board就是初始化HackRF硬件(调用API之类的,我们在别的项目里很熟悉了),它也支持另一款BladeRF,但是现在不常用了。 然后重点就是parse_input函数,它是读取命令参数用的,里面也包含根据命令参数进行编码和调制的操作。 找到parse_input函数,它内部比较重要的是calculate_pkt_info和最后的那些printf部分,printf部分就是对应我们在终端窗口里看到的编码前和编码后的bit数据,说明在calculate_pkt_info函数里,已经完成了参数读取,编码运算等操作(实际连调制操作也完成了)。 calculate_pkt_info函数里有get_next_field,在找"-"这个字符,因为这个字符的位置后面跟的就是数据包类型,比如ADV_IND,找到这个关键位置后,再用calculate_sample_from_pkt_type读取数据包,并判断它的类型。 calculate_sample_from_pkt_type中,如果类型就是ADV_IND,就会继续调用calculate_sample_for_ADV_IND函数,进行编码和调制。值得注意的是原始的参数在这几个calculate_sample开头的函数中,都是用参数pkt_str传输的,说明我在命令行里输入的那些参数,都这样一层一层得传递了下来。 然后再找到calculate_sample_for_ADV_IND函数。 它做了4个工作。 1.读取剩余的参数,比如TXADD RXADD ADVA ADVDATA 2.边读取参数,边打包,把数据存入pkt->info_bit,还把暂时的包长存入pkt->num_info_bit 3.使用fill_adv_pdu_header和crc24_and_scramble_to_gen_phy_bit,完成对info_bit的编码,得到了pkt->phy_bit,也就是phy bit。 4.最终用gen_sample_from_phy_bit函数对刚刚得到的phy bit进行调制。这也是我们最关心的部分,是这个函数完成了之前几个arduino项目里由nrf24l01硬件实现的工作。 那么现在就让我们看看gen_sample_from_phy_bit函数: 其实原作者就给这个工作做了好几种实现,我们真正要关注的实际在使用的代码是从1022~1060行的。它的参数bit就是传进去的phy bit,sample就是调制完成后算出来的采样点,num_bit是bit的数量。 要做GFSK调制,其实就分两步,先对phy bit做高斯滤波,然后就是把1010的数据做fsk调制,也就是比如某一时刻的1对应一个频率,下一时刻的0又对应另一个频率。 一开头还做了重采样,唯一要关心的是这一段:
它把原始的bit数据传到了tmp_phy_bit_over_sampling_int8数组里了,也就是说后面调制工作都是针对tmp_phy_bit_over_sampling_int8里的数据。 接下来是真正的调制算法:
一开头的sample[0]和sample[1]只是在算初始值,没那么重要。从第一个for循环开始比较重要。 第一个(外部)for循环在做调制,第二个(内部)for循环只是在做高斯滤波 我对高斯滤波其实没那么关心,因为以前解调的经验是,我不管这个高斯滤波器,直接用FSK(FM)解调算法也能解到正确的数据。大概看一下就是说本来应该用下面的公式去增加acc
现在要做高斯滤波就是在phy bit前乘了一个系数,再做了个小循环而已。为了简化,我们可以令系数为1并且去掉内部的小循环。? 在每次进入内部小循环之前acc都初始化为0,如果没内部小循环的话,acc的值就等于tmp_phy_bit_over_sampling_int8,也就是说acc就直接对应了phy bit的0和1。
再结合上面的代码看一下,基本就可以明白。 tmp实际上就是正弦波的相位,sample是最终的采样点,由于是iq数据,所以相邻两个一个是cos另一个是sin,我们只看cos,它的参数是tmp,而tmp在不停累加acc,acc就是相位差了。而这些采样点的处理都间隔固定的时间。那么acc就是单位时间的相位差也就是频率了。 如果phy bit是1的话,acc =?1,这样cos table可以输出一个高频正弦波。 而如果phy bit是0,那么acc = 0,那么cos table输出的正弦波频率就是0。如果用频域来理解这两种phy bit对应的信号,就是一个在fft的0Hz,另一个更靠右。这就是2FSK的原理了。 这样这几个项目就都看完了。如果还有更多兴趣,或者对FSK调制理解不深刻,还可以看看gnuradio对应代码。我习惯看3.7版本的gnuradio,里面有gfsk.py,如果你是3.9可能没有。 下面是它的链接: gnuradio/gfsk.py at maint-3.7 · gnuradio/gnuradio · GitHub 点开代码,它有调制和解调部分。只看调制部分的话,也会看到它在调用gnuradio里实现的高斯滤波器filter.firdes.gaussian,以及frequency_modulator_fc,后者就是FSK的核心代码。 搜索这个函数,在gr-analog/lib/下有一个frequency_modulator_fc_impl.cc文件gnuradio/frequency_modulator_fc_impl.cc at maint-3.7 · gnuradio/gnuradio · GitHub
上面是它的主要实现。in是输入,相当于是phy bit,out是输出也就是sample。它在用in里的取值决定d_phase。对应相位(变量开头的d只是数据类型double的意思,不是differential的意思),如果in是1,这个值就增长比较快,如果in是0,就不增长(相位增长快其实就是频率高的意思)。然后把相位做转换,先限制幅度为-pi ~ pi的周期内,然后再转为角度形式anlge,再用angle作为参数用sincos函数直接生成复数的正弦波即可。 实现原理和BTLE里的是差不多的,输入的phy bit是1和0,在控制相位的差分(也就是频率)。然后在把相位作为参数给正弦波查找表LUT。然后把正弦波采样值作为sample输出就行。 看完调制原理后就可以回到btle_tx.c里看看调制后的sample是怎么从hackrf的api里发出去的了。 sample在pkt->phy_sample里,而这个pkt最早其实是parse_input函数里的packets[i]传过来的,它来自命令行的参数。parse_input函数里num_repeat是r后面的数字,代表重复运行次数。num_packet代表一次运行的包数量,我一般一次都只运行1各包。 这样也就是说pkt->phy_sample和packets[i].phy_sample就是一回事。main函数里在调用完parse_input后,会调用tx_one_buf函数。
就是这个tx_one_buf函数在使用packets[i].phy_sample作为参数。外面还套了两层循环,由于num_packet对我来说都是1,所以只是num_repeat循环在根据参数r后的数字在循环调用tx_one_buf。 找到tx_one_buf,其实有2个,一个用于bladerf,我们看下面那个用于hackrf的。 它会把输入的第一个参数buf,拷贝给tx_buf。也就是说pkt->phy_sample就给到了tx_buf。而这个tx_buf最终会在tx_callback回调函数里发给hackrf硬件。
如果你继续看上面代码的下半部分,回发现它在tx_one_buf里要先开启hackrf硬件设置回调函数,然后再关闭这块硬件板子。 这种做法效率不高,因为num_repeat数量比较大,比如50次时,就要调用tx_one_buf 50次,开关硬件板子50次。这样时间会很长。完全可以一开始就初始化好板子,然后让回调函数不停发同样的采样点,等待50次回调就行了。最后发完了再关板子。 因此这个项目的发射间隔确实是有优化空间的。 |
|
嵌入式 最新文章 |
基于高精度单片机开发红外测温仪方案 |
89C51单片机与DAC0832 |
基于51单片机宠物自动投料喂食器控制系统仿 |
《痞子衡嵌入式半月刊》 第 68 期 |
多思计组实验实验七 简单模型机实验 |
CSC7720 |
启明智显分享| ESP32学习笔记参考--PWM(脉冲 |
STM32初探 |
STM32 总结 |
【STM32】CubeMX例程四---定时器中断(附工 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/26 9:46:57- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |