Portapack应用开发教程(十七)nrf24l01发射 C

2023-12-12 06:32

本文主要是介绍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又对应另一个频率。

一开头还做了重采样,唯一要关心的是这一段:

for (i=0; i<(num_bit*SAMPLE_PER_SYMBOL); i++) {if (i%SAMPLE_PER_SYMBOL == 0) {tmp_phy_bit_over_sampling_int8[i+(LEN_GAUSS_FILTER*SAMPLE_PER_SYMBOL-1)] = ( bit[i/SAMPLE_PER_SYMBOL] ) * 2 - 1;} else {tmp_phy_bit_over_sampling_int8[i+(LEN_GAUSS_FILTER*SAMPLE_PER_SYMBOL-1)] = 0;}}

它把原始的bit数据传到了tmp_phy_bit_over_sampling_int8数组里了,也就是说后面调制工作都是针对tmp_phy_bit_over_sampling_int8里的数据。

接下来是真正的调制算法:

  int16_t tmp = 0;sample[0] = cos_table_int8[tmp];sample[1] = sin_table_int8[tmp];int len_conv_result = num_sample - 1;for (i=0; i<len_conv_result; i++) {int16_t acc = 0;for (j=3; j<(LEN_GAUSS_FILTER*SAMPLE_PER_SYMBOL-4); j++) {acc = acc + gauss_coef_int8[(LEN_GAUSS_FILTER*SAMPLE_PER_SYMBOL)-j-1]*tmp_phy_bit_over_sampling_int8[i+j];}tmp = (tmp + acc)&1023;sample[(i+1)*2 + 0] = cos_table_int8[tmp];sample[(i+1)*2 + 1] = sin_table_int8[tmp];}

一开头的sample[0]和sample[1]只是在算初始值,没那么重要。从第一个for循环开始比较重要。

第一个(外部)for循环在做调制,第二个(内部)for循环只是在做高斯滤波

我对高斯滤波其实没那么关心,因为以前解调的经验是,我不管这个高斯滤波器,直接用FSK(FM)解调算法也能解到正确的数据。大概看一下就是说本来应该用下面的公式去增加acc

acc = acc + tmp_phy_bit_over_sampling_int8[i];

现在要做高斯滤波就是在phy bit前乘了一个系数,再做了个小循环而已。为了简化,我们可以令系数为1并且去掉内部的小循环。 

在每次进入内部小循环之前acc都初始化为0,如果没内部小循环的话,acc的值就等于tmp_phy_bit_over_sampling_int8,也就是说acc就直接对应了phy bit的0和1。

    tmp = (tmp + acc)&1023;sample[(i+1)*2 + 0] = cos_table_int8[tmp];sample[(i+1)*2 + 1] = sin_table_int8[tmp];

再结合上面的代码看一下,基本就可以明白。

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

int frequency_modulator_fc_impl::work(int noutput_items,gr_vector_const_void_star& input_items,gr_vector_void_star& output_items)
{const float* in = (const float*)input_items[0];gr_complex* out = (gr_complex*)output_items[0];for (int i = 0; i < noutput_items; i++) {d_phase = d_phase + d_sensitivity * in[i];// place phase in [-pi, +pi[
#define F_PI ((float)(M_PI))d_phase = std::fmod(d_phase + F_PI, 2.0f * F_PI) - F_PI;float oi, oq;int32_t angle = gr::fxpt::float_to_fixed(d_phase);gr::fxpt::sincos(angle, &oq, &oi);out[i] = gr_complex(oi, oq);}return noutput_items;
}

上面是它的主要实现。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函数。

for (j=0; j<num_repeat; j++ ) {for (i=0; i<num_packet; i++) {time_pre_pkt = time_current_pkt;gettimeofday(&time_current_pkt, NULL);if ( tx_one_buf(packets[i].phy_sample, 2*packets[i].num_phy_sample, packets[i].channel_number) == -1 ){close_board();goto main_out;}

就是这个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硬件。

inline int tx_one_buf(char *buf, int length, int channel_number) {int result;set_freq_by_channel_number(channel_number);//tx_buf = tx_zeros;//tx_len = HACKRF_USB_BUF_SIZE-NUM_PRE_SEND_DATA;tx_buf = buf;tx_len = length;// open the board-----------------------------------------if (open_board() == -1) {printf("tx_one_buf: open_board() failed\n");return(-1);}// first round TX---------------------------------stop_tx = 0;result = hackrf_start_tx(device, tx_callback, NULL);if( result != HACKRF_SUCCESS ) {printf("tx_one_buf: hackrf_start_tx() failed: %s (%d)\n", hackrf_error_name(result), result);return(-1);}while( (hackrf_is_streaming(device) == HACKRF_TRUE) &&(do_exit == false) ){if (stop_tx>=9) {break;}}if (do_exit){printf("\ntx_one_buf: Exiting...\n");return(-1);}if (close_board() == -1) {printf("tx_one_buf: close_board() failed\n");return(-1);}do_exit = false;return(0);
}

如果你继续看上面代码的下半部分,回发现它在tx_one_buf里要先开启hackrf硬件设置回调函数,然后再关闭这块硬件板子。

这种做法效率不高,因为num_repeat数量比较大,比如50次时,就要调用tx_one_buf 50次,开关硬件板子50次。这样时间会很长。完全可以一开始就初始化好板子,然后让回调函数不停发同样的采样点,等待50次回调就行了。最后发完了再关板子。

因此这个项目的发射间隔确实是有优化空间的。

这篇关于Portapack应用开发教程(十七)nrf24l01发射 C的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/483581

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu1394(线段树点更新的应用)

题意:求一个序列经过一定的操作得到的序列的最小逆序数 这题会用到逆序数的一个性质,在0到n-1这些数字组成的乱序排列,将第一个数字A移到最后一位,得到的逆序数为res-a+(n-a-1) 知道上面的知识点后,可以用暴力来解 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#in

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof