之前一直在用srsLTE软件实现LTE基站,srsLTE不但文档而且代码结构清晰,不过我看的代码主要还是与sdr交互的部分,真正的信号处理和解码部分过于复杂,所以还没深入去看。我发现srsLTE除了可以实现基站外,里面有一些小例子,比如cell_search。可以搜索运营商的商用基站,也就是说我们手头的LimeSDR可以像一个普通手机一样去解调实际的基站信号,而不只是用来听FM广播了,这听起来超酷。问题是cell_search程序还是调用过了srslte内部的很多函数,看起来太复杂杂乱,不好上手。
后来我又找到了另一个叫作了LTE-Cell-Scanner的程序。
https://github.com/Evrytania/LTE-Cell-Scanner
这个程序的功能很简单,就只是搜索基站获得一些广播信息,(它也有LTE-Tracker程序,可以跟踪基站获得更多信息,但是我不打算去看了)。这个LTE-Cell-Scanner本来是一个老外写的,用的硬件是rtlsdr,可以搜索指定频率范围内的LTE基站,把判断出来确定是基站的频点标记出来,每个基站还能进一步获取信息,可以获取cellid,基站功率,CP类型,天线数量,小区带宽,还有PHICH的一些信息,甚至还可以用来校准rtlsdr的频率。
这是这个原作者做的介绍网页
http://www.evrytania.com/lte-tools/78-default-category/77-lte-cell-scanner
我使用的时候看到介绍说如果用低ppm的硬件或者使用上一次的校准值来校准硬件可以提高搜索速度,我本来认为如果硬件不行要不就是解不出,不应该对搜索时间造成影响,但是仔细看了代码之后才理解为什么会这样。
除了这个项目外,还有个中国人对这个程序做了修改,主要有3个修改:1.使它支持hackrf(这样频率范围更大,我们国家有些基站频率2.6GHz远超rtlsdr的支持范围了,但是hackrf就可以支持)2.支持tdd基站(老外原作只支持fdd基站,而且还能判断这个基站是fdd还是tdd)3.支持opencl(可以用显卡加速运算)
我暂时还只看了老外原版的程序,基本流程搞懂了。
基站检测是通过找下行信号的pss和sss来判断的。步骤是它先获取rtlsdr得到的iq数据,然后直接对原始数据用互相关算法检测pss,接下来最大似然算法检测sss,这样就判断出了基站。还得到了很多重要信息:cellid、功率、中心频率,原理上还可以判断出是tdd还是fdd基站,后面还做了频率偏差估计。
这些做完后要做MIB解调,MIB里也有很多重要信息比如基站带宽等。MIB解调前先获取了视频网格(time frequency grid, tfg),然后才是去解调并解码获取进一步的信息。
这个程序调用了一些外部的包,比如itpp,itpp里有很多模仿matlab的函数,还有通信信号处理的算法,LTE的解调和解码其实都是调用itpp里的函数做的。
主要的程序有3个,CellSearch.cpp searcher.cpp lte_lib.cpp。CellSearch.cpp里面有主函数,主函数先获取数据,然后调用信号处理算法一步步做检测。检测到一个基站就print一个出来,直到整个目标频率范围搜索完毕,再输出总结的信息。searcher.cpp也很重要,主函数里调用的几个算法函数就在这里面实现的。最后还有个lte_lib.cpp,它是对itpp的lte解调解码函数的封装,主程序调用它来做mib解码,它会再去调用itpp里的具体函数。
先看看CellSearch里的main函数,调用parse_commandline获取启动时输入的参数,config_usb初始化rtlsdr硬件。然后初始化了一些变量,变量里包括fc_search_set,这是用户输入的要搜索的频率范围。f_search_set,这是在搜索其中一个频点是不是基站的时候,要对频率做的偏移。detected_cells检测出的基站信息都存在这里面。然后就开始进入for循环了,这个for循环遍历的是用户输入的频率范围内的每0.1MHz的各个中心频点,单次循环做的就是判断这个频点是不是基站,如果是基站就把mib解出来打印出来。
然后看一下循环里的操作,先是capture_data,这个函数是在capbuf.cpp里实现的,它会根据自己被输入的参数来决定本次读取的中心频点,然后先设置硬件再进行读取,读取的用法还是调用rtlsdr异步读取,其实跟之前看的kerberossdr很像的,比较有意思的是capturedata的回调函数结束时调用了取消异步读取,这样异步读取函数实际上就跟同步读取差不多了,都是读的时候等待着,读完了就返回。读完了以后还转换了类型,先把0~255的数据映射到-1~1,然后再把一连串的iqiqiqiq转换为了复数,存进capbuf里。这个函数还支持读入到文件或者从文件读取,用来离线分析,一般用不上。另外可以看一下函数开头的声明,这个作者的每个参数都按照输入信息和输出信息分类写在注释上了,可以帮助阅读。
接下来main函数里主要的是xcorr_pss,peak_search,sss_detect。(还有个chi2cdf_inv函数,它是计算阈值,用来给peak_search作为判断标准用的)。这3个函数用来判断当前循环中检测的这个目标频率是不是有基站。
接下来是pss_sss_foe,它利用pss和sss检测结果做频率校准的,精度2.5kHz。然后是extract_tfg,这个是获取time_frequency_grid用的,从capbuf也就是硬件里收到的数据中,定位到时间-频率网格里的数据,用来后续解调用的,这个网格跟参考阅读里的文章对应的。再往下是tfoec,它做了高精度的频率校准和时间校准。最后是decode_mib,它解调了MIB。
到此为止循环内部就做完了,做完后针对范围内的下一个中心频点继续判断是不是基站,是基站就把MIB解出来。
全部做完会用dedup函数去重,然后把所有基站详细信息print出来。
接下来把前面讲的主循环里几个比较重要的函数点开来看看。
1.xorr_pss,它在searcher.cpp里输入是capbuf原始数据,f_search_set要搜索的偏移频率,ds_comb_arm,fc_requested用户设定的中心频率,fc_programmed程序换算的中心频率,fs_programmed程序换算的采样率,这个函数分为好几步,xc_correlate,xc_combine,xc_delay_spread,xc_peak_freq和sp_est。sp_est是一个独立的程序而且输出的量只是调试用的,输入的量就是原始数据,所以单独看,它其实就是把原始数据的采样点用复数模的方法求功率,根据注释计算的是2个ofdm符号的功率,频率用了12个rb,来估计6个rb的功率,6个rb的功率是不随基站收发数据改变的,12个rb会有一些变化造成不准确。
接下来看另外几个xc开头的函数。
1.1 xc_correlate把xorr_pss的参数都传进来了,然后从ROM_TABLES.pss_td中获得3种pss的取值,25,29,34,对应的3个长度都为62个的Zadoff-Chu序列(https://blog.csdn.net/m_052148/article/details/51273636),这3组序列(table)的频域形式是在lte_lib.cpp的pss_fd_calc函数生成的,这是pss_fd_calc的代码:
const int zc_map_donotuse[3]={25,29,34};
const vector zc_map(zc_map_donotuse,zc_map_donotuse+3);
//zc_map里就是25,29,34
cvec r=exp((complex(0,-1)*pi*zc_map[t]/63)*elem_mult(ivec("0:62"),ivec("1:63")));
//zc_map[t]就是公式里的root index(u)
//后面的elem_mult(ivec("0:62"),ivec("1:63")))就是n(n+1)或者(n+1)(n+2)
//总之就是连着的两个整数,0:62和1:63代表整数取值范围
//剩下的左边complex(0,-1)就是=0+(-1)*j=-j,然后放在e的指数上,跟公式一样的
r.del(31);
//公式是分段函数实现的,代码里没分段而是去掉了31对应的情况,就是31*32
生成完毕后再用idft转到时域最后存入temp。 在转换过程前还接了0数组,转换后又用自己的一段接在自己上,这个我还没看懂?反正table里存的就是这3个序列的时域值了,长度是每组137。
xc_correlate获得这3个序列后,每次取出其中一个放到temp里,由于有可能硬件有频率误差,所以要做频率偏移,取出一个可能的误差,把这个序列用fshift函数移动了f_off大小的频率,也就是补偿了这个频率误差,然后再与获取的原始数据capbuf做卷积,temp取conj以及下面的循环中与capbuf滑动相乘再求和再归一化都是在求卷积,但是和卷积公式有点区别,这里有点疑问?
求出来的结果存入xc中,xc的参数xc[t][k][foi]=acc t对应3组序列,k是capbuf的长度-136,foi对应所有的频率偏移。
这个136跟temp长度有关,用来做卷积用的,确保移动相乘的时候计算不超范围。
这里其实看出来了,预估的频率偏移的范围如果越大,foi的取值就越多,那么要做的计算耗时就越长。
1.2 xc_combine,得到xc了以后还不够,由于一次capture是80ms,其实里面包含了好多个frame,我们可以把它们的结果合并起来求平均值。这样做的好处是可以知道频率误差是多少。合并的时候有一点要注意。如果是没有误差情况下,每个frame间隔19200个采样点再次出现,但是如果有误差,下一个frame的开头就不是19200个以后。比如如果中心频率740MHz,误差50kHz,那么就是19200-50*10^3/(740*10^6)=19198.7后。互相关波峰间隔也变为19198.7,只有当频率误差为0的时候才间隔19200采样点,这样就能判断出基站准确频率和接收机频率误差了。也是因为这一点,在合并互相关结果的时候要考虑这一点。
actual_start_index就是计算这个偏差的,具体为什么这么算还不了解,n_comb_xc表示合并了多少个frame。
代码里为什么都是9600不是19200?因为复数代替iq,长度缩小一半19200/9600=2
for (uint16 m=0;m
1.3 xc_delay_spread,接下来是这个函数,跟前面一步有点像,只不过这次合并的是时域上临近的5个点的结果,然后再求平均。这5个点是程序开头define的,填到ds_comb_arm里,ds_comb_arm=2,当前点,当前点前面2点,前面1点,后面1点,后面2点都要加起来。
for (uint8 t=0;t<3;t++)
{
for (uint16 idx=0;idx<9600;idx++)
{
xc_incoherent[t][idx][foi]=xc_incoherent_single[t][idx][foi];
}
}
//先把当前点存好
for (uint8 t=1;t<=ds_comb_arm;t++)
{
for (uint8 k=0;k<3;k++)
{
for (uint16 idx=0;idx<9600;idx++)
{
xc_incoherent[k][idx][foi]+=xc_incoherent_single[k][itpp_ext::matlab_mod(idx-t,9600)][foi]+xc_incoherent_single[k][itpp_ext::matlab_mod(idx+t,9600)][foi];
//t=1时把前1个点和后1个点的结果存进去
//t=2时再把前2个点和后2个点的结果存进去
}
}
}
// Normalize
for (uint8 t=0;t<3;t++)
{
for (uint16 idx=0;idx<9600;idx++)
{
xc_incoherent[t][idx][foi]=xc_incoherent[t][idx][foi]/(2*ds_comb_arm+1);
} //全部存完了要除以(2*2+1=5), 求出平均值
}
1.4 xc_peak_freq,在所有3个PSS编码值,所有时间偏移,所有频率偏移中找最大的波峰,其实也就是xc_incoherent里的最大值,以及这个最大值对应的foi,频率偏移值,3重循环找最大值就行。不过这样找出来的还是各组PSS编码,各个时间上的波峰最高的值对应的频率,PSS编码应该是多少,时间应该是多少还不知道。
2.peak_search,main函数里xcorr_pss做完了差不多就做peak_search了,找到最大互相关波峰还不够,它只是自己跟自己比比较大,但是还要超过阈值才能达标,这个阈值也是计算出来的,用chi2cdf_inv算出来,这里先不细看了。这个函数一开头用transpose转置再用两次max函数找到最高峰对应的PSS编码值,存入peak_n_id_2,还找出了比较接近的波峰时间peak_ind。这个max函数可能是某个库的重载,所以我不太清楚它的用法,只是猜测是这个意思。如果波峰功率超过阈值,就算找到了,但是波峰是上一个函数在好几个时间点前后平均出来的,要把准确的波峰时间点找出了,先用已经知道的2个波峰对应的参数,PSS编码和大概的时间点找出波峰对应频率xc_incoherent_collapsed_frq(peak_n_id_2,peak_ind),然后再去在时间上遍历xc_incoherent_single,得到波峰最高的时间。这些计算全部完成后,可以把波峰功率、波峰对应时间、波峰对应频率、波峰对应PSS编码全部存入cells了,最后会输出给主程序其它部分。接下来要去除一些探测中排除掉的波峰,比如在波峰对应时间前后274个采样内不再出现同一个pss编码相同的波峰了,那么这个PSS编码和这个时间点前后274个点在xc_incoherent_working都设置为0。然后在波峰对应时间的前后274个采样点范围内的其他PSS值,如果它的功率与前面找到的功率之差小于8db也不可能,找一下符合条件的参数在xc_incoherent_working中的值,如果没达到这个阈值也直接设置为0。还有最后一条我还没理解,不过估计也是这么清理的。
这些清理用途不明,因为xc_incoherent_working没有在别的地方用,也没输出过。这个函数输出的只是cells列表。
3.sss_detect,接下来检测SSS,它会利用前面的cells结果,进一步检测。它会调用sss_detect_getce_sss和sss_detect_ml。sss_detect_getce_sss中先利用pss位置来找到sss位置,然后从capbuf找到对应位置的数据用来做信道估计。sss_detect_ml用极大似然算法,估算出cp类型。这样就能知道frame从哪里开始了frame_start。这样再用极大似然的结果与阈值比较,再过滤掉一些cell,并且把最新的到的frame_start,cp类型,还有n_id_1(这个不确定是什么可能对应的是当前sss编码?)存入cells列表。
4.pss_sss_foe,接着再做一次频率精确估计,利用pss和sss信息。如果只有pss信息,估计误差是2.5khz。利用sss可以更精确,估计完存储精确频率到cells列表里。
5.extract_tfg,主函数下一步就用这个获取时频网格了。
cvec dft_out=dft(capbuf.mid(round_i(dft_location),128));
tfg.set_row(t,concat(dft_out.right(36),dft_out.mid(1,36)));
这两句,从capbuf里获得的数据做了dft也就是离散傅立叶变换,然后作为tfg的数据。这是主要的代码,实际还要做频率、时间校准、找帧头等操作。
6.tfoec,主函数里还要做一次最精确的频率和时间校准。它利用TFG中获取到的采样点进一步校准。对于低信噪比情况有明显改善。
7.decode_mib,主函数最后调用这个函数解调解码MIB信息。现在所有信息都在时频网格tfg中,函数一开始用chan_est函数从tfg里获得了信道估计的结果存入ce_tfg,然后调用pbch_extract函数,这个函数从tfg和ce_tfg中抽出了pbch_sym和pbch_ce,也就是pbch的符号和信道估计,然后利用pbch_ce给pbch_sym做了加权计算,得到了最终的syms,这些符号(syms)就可以用lte_demodulate去解调了,解调完了还有decode以及crc校验等操作,这几个函数都在lte_lib.cpp里实现,应该都是调用itpp里的函数。尤其是demodulate。调用lte_demodulate的时候传入的调制方式是qam,在lte_lib.cpp里可以看到mod_map操作的实现,对应于table(0),它是Mod_map最前面的2行生成的,看上去就是一个qpsk的4个点的坐标值。但是我在itpp里没找到qam的demodulate_soft_bits函数。
我大概看了同一个项目里的LTE-Tracker,区别是它是不停跟踪同一个频点的基站的,不是cell_search那样扫完一个扫下一个直到全部扫完就停止的。
另外我还看了中国人改的LTE-Cell-Scanner
https://github.com/JiaoXianjun/LTE-Cell-Scanner
1.hackrf支持只是加了几个hackrf的api调用,模仿rtlsdr的调用写的,它还支持bladerf,原理差不多。
2.tdd支持的实现方式是,先搞了一个tdd_flag,然后假设是tdd基站(主要是跟sss有关的帧偏移有区别),然后根据假设看得到的sss的likelihood是不是大于一个阈值,成功就说明是tdd,然后再假设是fdd基站,做一样的步骤看likelihood,如果可以就是fdd基站。
3.opencl加速应该主要是用于pss的互相关运算上了。它在主循环里用sampling_ppm_f_search_set_by_pss代替了xcorr_pss中的xc_correlate。
打开这个sampling_ppm_f_search_set_by_pss函数看看
#ifdef USE_OPENCL
lte_ocl.filter_mchn(s, pss_fo_set, corr_store);
#else
conv_capbuf_with_pss(s, pss_fo_set, corr_store);
#endif
//s是从硬件采集到的数据,也就是capbuf
//pss_fo_set是3种pss生成的序列
//corr_store是互相关结果
//这段代码先判断是否用opencl,然后使用两种不同方法生成互相关结果