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。

扫描二维码关注公众号,回复: 14740639 查看本文章

接下来看一下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次回调就行了。最后发完了再关板子。

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

猜你喜欢

转载自blog.csdn.net/shukebeta008/article/details/122914773