Timestamp 是 OpenBTS 实现TDMA时隙同步的一个非常精彩的方案,对于其他SDR系统有很多参考价值。更多关于硬件接口部分代码的解读总结在一个小文档中(http://www.gnuradio.cc/read.php?tid-707.html) 本文整理自 www.gnuradio.cc 论坛中meteor的一些帖子,加上了我的理解。
由于计算机不能精确定时,OpenBTS采用时间戳Timestamp的方式来保证计算机能得到精确的定时。也就是计算机告诉USRP,我要在某个准确的时间发送信号,USRP就会在那个时间发送,不会受到计算机内部处理时延不确定的影响。
USRP与OpenBTS的接口
OpenBTS软件与USRP硬件通过USB交换IQ数据。两者之间交换的数据遵循以下格式。
首先,每个数据包的长度都是512字节,其中前8个字节是控制字段,后面504个字节是IQ数据。
接收方向
下图是数据包的构成:包括一些控制比特,数据包长度,时间戳,数据。
前两个字节的控制比特包括:
在IQ数据部分,I路和Q路数据交替存放,每一个采样值用16bit即2字节表示。
在接收方向上,每次都收126个sample,连续不断的读入一个缓冲区,然后再从缓冲区中读出需要的数据,一般是一次读出4个时隙。对应的函数是USRPDevice.cpp中的:
1 | int USRPDevice::readSamples(short *buf, int len, bool *overrun,TIMESTAMP timestamp,bool *underrun, unsigned *RSSI) |
发射方向
与接收方向类似:
发射信号时,OpenBTS系统每次发送4个时隙的数据,一共625个sample。这625个sample被拆成5个包发送出去,126/126/126/126/121。对应的函数是USRPDevice.cpp中的:
1 | int USRPDevice::writeSamples(short *buf, int len, bool *underrun, |
接口时延
在Transceiver,Radiointerface以及USRPDevice中都有各自的时间变量,用于记录这个模块当前的时刻。每个时间变量,都会根据输入时间参数来更新当前时间。
GSM::Time mTransmitDeadlineClock 表示目前该发送什么时间的数据包了,因为待发送的数据是按时间顺序排列在缓存里的,因此可以理解为是当前时间需要发送的数据包的序号,所以每发送完一次都需要加一个时隙。
因为PC到USRP有时延,因此我们必须提前把要发送的数据包发送到USRP中,USRP中也按时间顺序存储,这时候时间标号被翻译成了timestamp,而不是用gsm:time了。
但是我们也不能给数据太早,怎么办?这里采用了一个发送时延mtransmitlatency,用来控制给数据的速度,如果当前时间为radio->clock() 那么我们我们需要发送的数据是 radio->clock()+mtransmitlatency>mTransmitDeadlineClock 了,这个说明mTransmitDeadlineClock -radio->clock()已经小于我们的传送时延了,所以必须马上将数据发送出去。如果mTransmitDeadlineClock -radio->clock()仍然大于mtransmitlatency,那么说明数据包还不急着给USRP。
用Timestamp实现精确定时
计算机和USRP都维持一个timestamp的64位变量作为时间标记,为了便于区分
- 计算机端的timestamp我们称之为c_timestamp
- USRP端的timestamp 我们称之为 u_timestamp
之前说到,在不同的模块中有不同的变量来记录时间。c_timestamp假设为其中某个模块的时间变量。它可以通过接收sample的数目进行更新,比如接收到了625个sample以后,c_timestamp = c_timestamp + 625。另外c_timestamp 也成了时间戳与GSM时钟的桥梁,因为c_timestamp每增加625,则时隙GSM时钟增加4个时隙。
u_timestamp 是USRP的FPGA中的一个硬件计数器,以采样速率计数。每增加一个sample,计数器值会增加1。
在接收方向上,即往计算机打包数据时,会将每一个包中第一个sample对应的u_timestamp封装在数据包头处。而这个值可以通过USRPDevice中的readsamples获取。
在发送方向上,发送的timestamp是PC在每发一个包的时候添加的,如果当前的timestamp=1000,那么我如果需要在1000个sample以后发送一组数据,那么发送数据的timestamp就是2000,USRP的FPGA的定时器到达2000的时候,就会将需要的数据发送出去。
下面以一个实例来说明。
首先下行方向。在USRPDevice.cpp的writeSamples函数中添加log。在
1 | pkt[1] = timestamp & 0x0ffffffffll; |
之后,记录timestamp的值。它表示,计算机发送给USRP的每一个数据包中,第一个sample的时刻。
于是我们在日志文件test.TRX.out中可以看到以下记录:
1 | 1337762965.3137 DEBUG 3078122352 USRPDevice.cpp:566:writeSamples: TIMESTAMP: 25000 |
可以看出从25000这一时刻开始,依次发送了时间间隔为126, 126, 126, 126, 121, 126, 126, 126, 126, 121…的数据包。4*126+121=625个samples。这恰好是4个时隙的数据。因此可以看出4个时隙的数据是被拆分成5个数据包发出的。
再看上行方向。在USRPDevice.cpp的readSamples函数中添加log。在
1 | TIMESTAMP pktTimestamp = usrp_to_host_u32(tmpBuf[1]); |
之后,记录pktTimestamp的值。它表示,USRP发送给USRP的每一个数据包的第一个sample的时刻。
在日志文件test.TRX.out中可以看到以下记录:
1 | 1337762965.3179 DEBUG 3078122352 USRPDevice.cpp:413:readSamples: timestamp: 24822 |
可以发现,当计算机读取USB数据包时,并没有以625个samples为单位来读,而是每次都读取126个samples,存入临时缓冲区readBuf。然后再拷贝到USRPDevice的一个循环缓冲区data中。readSamples这个函数则会根据输入参数timestamp,从循环缓冲区data中读出625个sample。
时延校准
这一节是想说明USRPDevice中updateAlignment这个函数的作用。updateAlignment用于校准时延,这个时延指的是从FPGA的计数器到信号出现在天线之间的时延。因为这部分时延不在计数器的控制范围之内,所以需要校准一下。它的目的是要知道发送时间戳和接收时间戳之间的偏差是多少。
上图是时延问题的说明。
FPGA中有两个计数器分别是发射和接收的时间戳计数器。两个计数器是同步的,即同步增加,而且数值是一样的。但是一个数据包从FPGA出来,到真正完成DA变换,加上一些模拟器件的时延,到达天线发送出来,是有一定时延的。所以当发送和接收的时间戳相同时,它们并不是同时出现在空中的。
假设数据包A到达发射天线的时刻是A’,此时收到一个数据包B,时间是B’。 A’=B’。
当数据包B到达FPGA,打上了时间戳B。那么B与A实际上就是在同一时刻出现在空中,但B≠A。有一个变量记录了这个偏差,timestampOffset = B-A,它是USRPDevice的一个成员变量。
接下来看看updateAlighment做了些什么。updateAlignment函数会发送一个“Control”类型的数据包,其中包含时间戳timestamp_tx = T1。FGPA收到这个数据包之后,马上给数据包打上一个新的时间戳timestamp_rx = T2。接着在PC侧,收到一个返回的“Control”类型的数据包。然后更新如下变量:
1 | timestamp -= timestampOffset; |
PINGOFFSET是一个常数,等于272。它反映的是DA到AD之间的时延,是一个固定的时延。不同的FPGA image这个常数是不同的。所以 timestampOffset = T2-T1+PINGOFFSET。
用另外一个方法也可以测量这个timestampOffset。用USRPping来发送一个特殊的序列,把TX和RX设为同频,同时接收这个序列,然后检测这个特殊序列出现的时刻。同样可以算出timestampOffset。
USRP中FPGA的Verilog代码
FPGA的Verilog代码在这里:
http://gnuradio.org/redmine/projects/gnuradio/wiki/USRP_inband_FPGA
https://github.com/closehaulcom/usrp1_openbts
有不明白的地方可以看看源码。