OAI开发记录——多UE接入下的物理层行为

本文最后更新于 2024年8月6日 凌晨

最近有一个开发任务,要求在接入多个UE时提取一些物理层的信息。之前的开发针对都是单个UE,所以多个UE按理来说应该不会很难,但是还是遇到了一些问题…

在物理层标识UE

在有多个UE接入的时候,物理层是使用RNTI对不同的UE进行标识的,但是RNTI并不是和真实的COTSUE一一对应进行绑定的。在UE重新接入,或者在连接时通信中断触发了随机接入后RNTI会被重新计算和分配,RNTI的生成参考参见openair2/LAYER2/NR_MAC_gNB/gNB_scheduler_RA.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int loop = 0;
if (ra->rnti == 0) { // This condition allows for the usage of a preconfigured rnti for the CFRA
do {
// 3GPP TS 38.321 version 15.13.0 Section 7.1 Table 7.1-1: RNTI values
ra->rnti = (taus() % 0xffef) + 1;
loop++;
} while (loop != 100
&& !((find_nr_UE(&nr_mac->UE_info, ra->rnti) == NULL) && (find_nr_RA_id(module_idP, CC_id, ra->rnti) == -1)
&& ra->rnti >= 0x1 && ra->rnti <= 0xffef));
if (loop == 100) {
LOG_E(NR_MAC, "[RAPROC] initialisation random access aborted\n");
abort();
}
}

开发环境配置

首先硬件设备就不多说了,因为我在四楼工作,所以我干脆把整套系统都放到一台机子上运行。怎么All in One进行开发研究请见之前的文章。另外一个需要配置的就是VSCode里面的GDB:首先在VSCode中配置GDB(就是按下Ctrl+Shift+D,然后新建launch.json文件)。写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "IMSI-TEST",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/cmake_targets/ran_build/build/nr-softmodem",
"args": [
"-O", "../o-band78-106.conf",
"--gNBs.[0].min_rxtxtime", "6",
"--sa",
"--usrp-tx-thread-config", "1"
],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/cmake_targets",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "${workspaceFolder}/cmake_targets/sudo-gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}

这里面关注args里面的内容,这些都是执行OAI程序时需要的命令行参数,自行对应就好,OAI运行时的基站配置文件也需要自己调整一下。

在项目的一些乱七八糟的目录新建一个文件,只要和上面的miDebuggerPath字段对应上就好。比如说我在cmake_targets文件夹中新建了sudo-gdb写入以下内容:

1
sudo /usr/bin/gdb "$@"

然后保存后变更执行权限:

1
sudo chmod 777 sudo-gdb

就可以开始断点测试了。按下那个播放按钮后可能也会报错,但不要担心,停止debug,然后在弹出的窗口随便运行个sudo命令输入密码后让这个shell获取权限就好了,接着gdb就可以正常运行了。

解决思路

IMSI和物理ID的相互转换

RNTI是不唯一的,但是IMSI是唯一的,可惜的是IMSI的获取是在Layer3,不能直接在Layer1中获取。所以我们可以构建一个全局的哈希表,用于存储RNTI和IMSI的匹配关系,然后在层1中根据RNTI去搜索IMSI进而完成物理UE上的对应关系。我进行尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void extract_imsi(uint8_t *pdu_buf, uint32_t pdu_len, rrc_eNB_ue_context_t *ue_context_pP) {
/* Process NAS message locally to get the IMSI */
nas_message_t nas_msg;
memset(&nas_msg, 0, sizeof(nas_message_t));
int size = 0;
nas_message_security_header_t *header = &nas_msg.header;
/* Decode the first octet of the header (security header type or EPS
* bearer identity, and protocol discriminator) */
DECODE_U8((char *) pdu_buf, *(uint8_t *) (header), size);

/* Decode NAS message only if decodable*/
if (!(header->security_header_type <= SECURITY_HEADER_TYPE_INTEGRITY_PROTECTED
&& header->protocol_discriminator == EPS_MOBILITY_MANAGEMENT_MESSAGE
&& pdu_len > NAS_MESSAGE_SECURITY_HEADER_SIZE))
return;

// ......
}

但是这个解码好像不对。经过和OAI 团队的沟通发现,IMSI确实不能在gNB侧解,我服了…所以我又绕回来了,其实可以通过接入时的CU/DU id 对用户进行区分。那么思路就是找到CU_ID 和 DU_ID的映射关系,显然这个关系应该要用Hash表实现。

CU/DU下的UE标识

本来想着自己实现一个Hash Table,但是我想了想像OAI这样的系统肯定有自己的实现,所以我用grep命令搜索Hashtable,然后在./common/utils/hashtable/hashtable.c找到了其实现。具体的使用用例可以在其他地方搜索到,我就不展开了,我们主要需要用到这个类中的cu_get_f1_ue_data以及du_get_f1_ue_data函数,用于获取CU_UE_IDDU_UE_ID,简而言之,他们可以实现RNTI<—>普通ID(e.g., 1,2,3,…N)的相互转换。

获取UE_ID

注意对这个表的操作是一定要加锁的,否则可能会有同步的问题。但是同步加锁也带来一个新的问题,那就是可能会增加读取的时间。总之我们现在可以分辨出来哪些数据归属于哪个UE了,下一个需要解决的就是如何同时获取对应时间内的数据,因为原本编写的代码是针对一个UE每做一次信道估计发送一次,现在有两个UE同时接入的情况下,需要重新考虑一下编写的方式。

多UE情况下的Protobuf编写

把最外层的信息包裹在if ((srs->active == 1) && (srs->frame == frame_rx) && (srs->slot == slot_rx))的代码块里。此时如果:

1
LOG_I(NR_PHY, "(%d.%d) gNB is waiting for SRS, id = %i\n", frame_rx, slot_rx, i);

那么打印出来的ID将会对应的成为SRS估计的顺序标号。接下来就是顺理成章的代码coding了。我将Proto文件保存在openair1/SCHED_NR/MESSAGES/channel_matrix.proto下,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto2";
package MultiUEMatrix;

message UE_SRS_PACK{
repeated NR_SRS_PACK UE_SRS= 1;
}

message NR_SRS_PACK{
required int32 ue_id = 1;
repeated NR_SRS_INFO Matrix = 2;
}
message NR_SRS_INFO{
repeated RESULT PRB_ITEM = 1;
}
// RESULT存放的其实是和信道相关的复数结构,由于以上指标都是复数形式所以就统一命名为RESULT
message RESULT {
required int32 image = 1;
required int32 real = 2;
}

编译的命令为:

1
2
protoc-c -I ./ channel_matrix.proto --c_out ./
protoc -I ./ channel_matrix.proto --python_out ./

复杂的循环逻辑

循环内有两个if条件判断,进入了这个判断的才是对应的SRS流的位置。到这里都很好解决,问题出在最后结合多个UE打包发送的时候。首先,发送Proto不可以在虚拟换的最外层,因为最外层会每时每刻都在进行数据的接收,我们需要把发送的逻辑放在两个if条件内的区域。

1
2
3
4
5
if (srs) {
if ((srs->active == 1) && (srs->frame == frame_rx) && (srs->slot == slot_rx)) {
...
}
}

只有(srs->active ==1) && (srs->slot == slot_rx)为真的时候,才会进行SRS相关的操作,其余的时候,因为循环写在条件外侧,因此需要在确认该时刻有SRS后才初始化protobuf的外层大包。具体的代码因为保密缘故我不能贴出来,但是我来阐述一下出现的问题:

当只有一个UE接入的时候,数据可以正常的在接收端被接收。但是当第二个UE也开始发送业务时,Protobuf编码出了问题。这很难想象到底是哪一步的缘故,因为Protobuf编码不可能会有错的,我感觉可能是有信号竞争,所以导致内部混乱。但是GDB下查看不了太多信息,所以以防万一,我们编写个逻辑方面的单元测试看看是个什么情况。

单元测试

首先编写CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cmake_minimum_required(VERSION 3.10)
project(prototest)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)


include_directories(protobuf-c)

add_executable(prototest
test.c
protobuf-c/protobuf-c/protobuf-c.c
protobuf-c/protobuf-c/protobuf-c.h
MESSAGES/channel_matrx.pb-c.h
MESSAGES/channel_matrx.pb-c.c
)

然后新建一个 build 文件夹用于存放C的程序以及中间结果。编写测试用的test.c,我提取了SRS运算的相关流程,代码比较长,可参考Github仓库内的test.c。测试仓库的目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cybersh1t@DESKTOP-BLEL2PT:~/prog/Protobuf-test$ tree -I protobuf-c -L 2
.
├── CMakeLists.txt
├── MESSAGES
│ ├── __pycache__
│ ├── channel_matrx.pb-c.c
│ ├── channel_matrx.pb-c.h
│ ├── channel_matrx.proto
│ └── channel_matrx_pb2.py
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── Makefile
│ ├── cmake_install.cmake
│ └── prototest
├── receiver.py
└── test.c

4 directories, 11 files

在项目根目录下接着输入如下命令:

1
2
3
cd build
cmake ../
cmake --build .

编译成功后,可以进行如下的操作来运行测试:

1
2
3
4
5
// C test
sudo ./prototest

// Python Receiver
python3 receiver.py

模拟GNB处理的行为

看到这里就应该可以啦!看上去我的逻辑是没问题的,那应该是多线程出了些问题(?我尝试加了线程锁,但是并没有太大改观,那么问题到底处在哪里了我请问了…

分析与代码修改

在函数的phy_procedures_gNB_TX(processingData_L1tx_t *msgTx,int frame, int slot,int do_meas)这个函数中,相关的逻辑代码是放在这样的结构下运行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (int i = 0; i < gNB->max_nb_srs; i++) {
if (srs) {
if ((srs->active == 1) && (srs->frame == frame_rx) && (srs->slot == slot_rx)) {
for (int uI = 0; uI < nr_srs_channel_iq_matrix.num_ue_srs_ports; uI++) {
for (int gI = 0; gI < nr_srs_channel_iq_matrix.num_gnb_antenna_elements; gI++) {
for (int pI = 0; pI < nr_srs_channel_iq_matrix.num_prgs; pI++) {
channel_matrix[i][uI][gI][pI].r = xxx.r;
channel_matrix[i][uI][gI][pI].i = xxx.i;
}// for (int pI = 0; pI < nr_srs_channel_iq_matrix.num_prgs; pI++)
}// for (int gI = 0; gI < nr_srs_channel_iq_matrix.num_gnb_antenna_elements; gI++)
}// for (int uI = 0; uI < nr_srs_channel_iq_matrix.num_ue_srs_ports; uI++)
}// if ((srs->active == 1) && (srs->frame == frame_rx) && (srs->slot == slot_rx))
}// if (srs)
}// for (int i = 0; i < gNB->max_nb_srs; i++)

并且最外层的这个i变量也确实对应的是UE的id。但是我突然想到一点:有没有可能,虽然i代表的是UE,但是循环里只有一个i才是有UE,而其他i的取值并没有对应的SRS信息?因为假设是这样的话,那么从打印的结果来看,一个for循环时打印两次和i相关的值,以及不同次下for循环时打出唯一一个i的效果是一样的,为了验证我的猜想,我额外做了一些测试,发现确实就是我想的那种情况,那么真相大白了:就是这个OAI的代码逻辑的问题,导致每一次进行SRS相关信息的计算时,函数内只会进行一次计算,得到的结果也是只有一台UE的。

这下就好办了,由于我们需要发送同一时刻的两台UE的信息,所以我们肯定要把这两个UE的数据都拿到再打包。我们可以建立一个N维数组来存储数据,不过比较遗憾的是暂时不能动态的分配内存,因为这样的代码写起来很麻烦,并且我还想使用到一些静态的特性。Anyway,静态声明一个4维数组,第一个维度可以是期望接入的UE数量,第二个维度是天线数,第三个维度是子载波数,最后一个维度就是2,对应了IQ数据中的实部虚部。

数据的赋值在逻辑中即可实现,并在最后判断:当处理晚目标UE数的数据后时再把数据发送出去,这样就可以获得多UE下的数据流了。

稳定性与驱动更新

主要的问题就是进行Proto业务时UE很容易断链,但还不确定是哪部分的原因。尝试更新UHD的驱动,现版本开发的OAI基于4.6,目前最新的UHD驱动为4.7,但我使用的是一年前的4.4版本。使用如下命令安装UHD的驱动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# https://files.ettus.com/manual/page_build_guide.html
sudo apt install -y autoconf automake build-essential ccache cmake cpufrequtils doxygen ethtool g++ git inetutils-tools libboost-all-dev libncurses5 libncurses5-dev libusb-1.0-0 libusb-1.0-0-dev libusb-dev python3-dev python3-mako python3-numpy python3-requests python3-scipy python3-setuptools python3-ruamel.yaml

git clone https://github.com/EttusResearch/uhd.git ~/uhd
cd ~/uhd
git checkout v4.6.0.0
cd host
mkdir build
cd build
cmake ../
make -j $(nproc)
make test # This step is optional
sudo make install
sudo ldconfig
sudo uhd_images_downloader

这一步完成后,由于操作的USRP是N310,需要自己把镜像给捣鼓进去,所以下一步就是配置N310.下载N310的文件系统,下载地址:https://files.ettus.com/binaries/cache/n3xx/ 。找到和驱动版本对应的文件系统下载即可,这里我下载的是mender:

Ettus的文件系统列表

下载结束后,将压缩包内的.mender文件传输至N310的ssh接口内:

1
scp C:\Users\Cybersh1t\Desktop\n3xx_common_mender_default-v4.6.0.0\usrp_n3xx_fs.mender root@192.168.3.7:~/.

其中的root@192.168.3.7指的是N310的操作系统,IP地址通过uhd_find_devices可以获取,这一步骤可能有两个IP,需要使用和你当前电脑同一网段的IP才可。文件传输过去后,使用如下命令更新操作系统:

1
mender install ./usrp_n3xx_fs.mender

然后需要经过漫长的等待其安装,完毕后reboot一下,然后移除掉旧的ssh私钥,重新生成新的。如果能顺利进入系统,那么使用mender -commit命令确认更改即可。更新镜像通过如下命令执行:

1
ls /usr/share/uhd/images | grep n310

其中XG HG WG分别代表不同的sfp接口模式:

  • 1Gb SFP0:将光转电模块插到SFP0上,用网线连接linux主机,设置网口mtu=1500(镜像使用HG)
  • 10Gb SFP1:使用光纤或者万兆直连线将sfp1和服务器光口相连,mtu=8000(镜像使用HG或XG)
  • 双10Gb SFP0/SFP1:使用光纤或者万兆直连线将sfp0 sfp1和服务器光口相连,mtu=8000(usrp n310的镜像使用XG)

在N310的文件系统中更新镜像:

1
uhd_image_loader --args "type=n3xx,fpga=XG" --fpga-path="/usr/share/uhd/images/usrp_n310_fpga_XG.bit"

最后的最后,不要忘记优化MTU:

1
2
3
4
5
6
for ((i=0;i<$(nproc);i++)); do sudo cpufreq-set -c $i -r -g performance; done
sudo sysctl -w net.core.wmem_max=62500000
sudo sysctl -w net.core.rmem_max=62500000
sudo sysctl -w net.core.wmem_default=62500000
sudo sysctl -w net.core.rmem_default=62500000
sudo ethtool -G <N310_if> tx 4096 rx 4096

其中的<N310_if>为手动设置的N310的DHCP地址所对应的网口(也就是光口的名称),如果不进行这一步的话,会在运行时弹出一大堆的L。使用回默认的radio库设置并重新编译后,稳定性得到了巨大的提升,看来就是UHD驱动的问题。至此,开发结束!


OAI开发记录——多UE接入下的物理层行为
https://cybercolyce.cn/2024/07/09/OAI-Multi_UEs_behaviors/
作者
L4k3d22
发布于
2024年7月9日
许可协议