概述
笔者近些年在做了一些LTE相关的项目,很多知识都是从LTE理论开始一点点看起,也在网络上找了很多的资料,目前在网络上对LTE协议,5G相关的协议介绍资料比较多,但是对于协议栈实现的部分内容较少,这对于开发人员,很多内容理解的不是太直观。为了能对自己的学习的知识作一个总结,同时也希望对相关的开发人员提供一些帮助,不要再走一些弯路。因此,笔者选择了srsLTE的开源代码作为LTE学习的切入点,来分析LTE协议栈的详细实现过程。具体的srsLTE代码读者可以自行到github上下载,由于能力和理解有限,文中可能会有一些错误,还请读者们见谅。
srsLTE的代码是基于C++实现的,文中用了很多C++11/14的特性,后续的讲解中有相关的内容需要特殊说明的,我们再作详细的介绍。整个系列文章,将先介绍srsUE的代码,后续再介绍srsENB的代码。
作为整个UE系列的第一篇文章,前面的几篇,我们先从宏观的角度去理解代码,比如:有哪些线程?线程之间是怎么协调共工作的?协议栈是如何工作起来的?数据流程是怎么走的?文档将近尽可能的先将这些整体的框架先做一些介绍。后续,再结合协议内容,分析一些具体的协议和模块:调度、PHR、BSR、功控、HARQ、MUX、逻辑信道、SR、随机接入、attach流程等;文章也会按照协议,如NAS、RRC、PDCP、RLC、MAC等协议代码详细讲解。
首先,要搞清楚的一点就是,代码中有哪些主要的线程?这些线程之间的是如何协调的?这点对于理解协议栈的实现是非常重要的,不理解这些线程就无法清楚的了解整个协议栈是如何工作的。
UE协议栈关键线程 |
先介绍gw线程,该线程是整个UE的数据出口,srslte的实现是基于Linux系统实现的, Linux系统中应用程序的业务数据 (http、ping)的数据包,是要经过Linux的TCP/IP协议栈,经过系统路由之后,最终数据会发给某个网卡设备,再由网卡设备的驱动程序进行转发。
细心的读者可能发现一个问题,由于整个协议栈是运行在应用层,而Linux系统的应用程序的数据会默认发给内核设备,srsLTE采用了Linux系统的tun设备来实现的,tun设备的使用主要有几个步骤,在attach成功之后:
- 在gw中open /dev/net/tun创建设备,
- 设置IP,并设置为默认路由;
- 使用read系统函数,就可以捕获到应用程序的业务数据包;
- 使用write系统函数,就可以将PDCP的IP数据包发给内核,再经过Linux的TCP/IP协议栈将数据包转给应用程序。
gw线程在读到应用数据包之后,会通过 ue_stack_lte的write_sdu接口,将数据进一步送给协议栈进行处理,最后通过LTE的uu接口发送给LTE基站。关于tun的编程方法,有不理解的读者可以自行查一些资料,这里就不详细赘述,后续有需要的话,可以专门的来介绍下该部分的知识点。
// 打开内核的tun设备 tun_fd = open("/dev/net/tun", O_RDWR); log.info("TUN file descriptor = %dn", tun_fd); if (0 > tun_fd) { err_str = strerror(errno); log.error("Failed to open TUN device: %sn", err_str); return SRSLTE_ERROR_CANT_START; } // 设置tun的设备名 memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = IFF_TUN | IFF_NO_PI; strncpy( ifr.ifr_ifrn.ifrn_name, args.tun_dev_name.c_str(), std::min(args.tun_dev_name.length(), (size_t)(IFNAMSIZ - 1))); ifr.ifr_ifrn.ifrn_name[IFNAMSIZ - 1] = 0; if (0 > ioctl(tun_fd, TUNSETIFF, &ifr)) { err_str = strerror(errno); log.error("Failed to set TUN device name: %sn", err_str); close(tun_fd); return SRSLTE_ERROR_CANT_START; } |
// Bring up the interface // 通过ioctl接口激活tun设备,类似ifconfig tun0 up命令 sock = socket(AF_INET, SOCK_DGRAM, 0); if (0 > ioctl(sock, SIOCGIFFLAGS, &ifr)) { err_str = strerror(errno); log.error("Failed to bring up socket: %sn", err_str); close(tun_fd); return SRSLTE_ERROR_CANT_START; } ifr.ifr_flags |= IFF_UP | IFF_RUNNING; if (0 > ioctl(sock, SIOCSIFFLAGS, &ifr)) { err_str = strerror(errno); log.error("Failed to set socket flags: %sn", err_str); close(tun_fd); return SRSLTE_ERROR_CANT_START; } |
// 设置tun的IP地址,Linux系统的路由和协议栈会用到 sock = socket(AF_INET, SOCK_DGRAM, 0); ifr.ifr_addr.sa_family = AF_INET; ((struct sockaddr_in*)&ifr.ifr_addr)->sin_addr.s_addr = htonl(ip_addr); if (0 > ioctl(sock, SIOCSIFADDR, &ifr)) { err_str = strerror(errno); log.debug("Failed to set socket address: %sn", err_str); close(tun_fd); return SRSLTE_ERROR_CANT_START; } ifr.ifr_netmask.sa_family = AF_INET; ((struct sockaddr_in*)&ifr.ifr_netmask)->sin_addr.s_addr = inet_addr(args.tun_dev_netmask.c_str()); if (0 > ioctl(sock, SIOCSIFNETMASK, &ifr)) { err_str = strerror(errno); log.debug("Failed to set socket netmask: %sn", err_str); close(tun_fd); return SRSLTE_ERROR_CANT_START; } |
ue_stack_lte线程是高层的调度的入口,他是由sync线程进入触发唤醒的,sync线程每个TTI(1ms)都会触发Ue_stack_lte线程运行run_tti,在run_tti线程里面,会调度mac、rrc、nas以及timer的运行。这里是一个比较难理解点,下面我们还是通过代码来分析下具体的过程。
Sync在IDLE状态直接调用radio_recv_fnc,radio_recv_fnc函数会调用stack->run_tti;
case sync_state::IDLE: if (radio_h->is_init()) { uint32_t nsamples = 1920; if (current_srate > 0) { nsamples = current_srate / 1000; } Debug("Discarding %d samplesn", nsamples); srslte_timestamp_t rx_time = {}; // sync IDLE状态会在radio_recv_fnc函数里面调用stack->run_tti接口 if (!radio_recv_fnc(dummy_buffer, nsamples, &rx_time)) { log_h->console("SYNC: Receiving from radio while in IDLE_RXn"); } |
Sync在CAMPING状态直接调用srslte_ue_sync_zerocopy,srslte_ue_sync_zerocopy函数调用recv_samples;recv_samples函数会通过回调函数的方式调用radio_recv_fnc,最终也会调用stack->run_tti。
case sync_state::CAMPING: worker = (sf_worker*)workers_pool->wait_worker(tti); if (worker) { // Map carrier/antenna buffers to worker buffers for (uint32_t c = 0; c < worker_com->args->nof_carriers; c++) { for (uint32_t i = 0; i < worker_com->args->nof_rx_ant; i++) { sync_buffer.set(c, i, worker_com->args->nof_rx_ant, worker->get_buffer(c, i)); } } // Primary Cell (PCell) Synchronization // srslte_ue_sync_zerocopy函数 -> recv_samples -> q->recv_callback -> stack->run_tti switch (srslte_ue_sync_zerocopy(&ue_sync, sync_buffer.to_cf_t(), |
stack->run_tti接口函数做的事情比较简单,它的主要任务就是往线程的任务队列里面push一个run_tti_impl任务,ue_stack_lte线程主要就是等待任务执行,当push任务之后,Ue_stack_lte线程就会被唤醒,然后执行对应的run_tti_impl函数。Ue_stack_lte线程每个TTI被唤醒一次,执行一次run_tti_impl。
void ue_stack_lte::run_tti(uint32_t tti, uint32_t tti_jump) { pending_tasks.push(sync_queue_id, [this, tti, tti_jump]() { run_tti_impl(tti, tti_jump); }); } |
Sync线程,这个线程是整个协议栈(包括PHY层)的驱动引擎,整个协议栈都是通过他来trigger的,物理层的上下行调度都是靠它来触发执行的,换句话讲,是sync线程唤醒了其他的线程执行。这里先不展开介绍这个线程的代码。后续在介绍具体流程的时候再详细介绍。
---------------------------------------------------------------------------------------------------------------------------------
好了,作为sysLTE的第一篇文章,先写到这里,后续会继续进行更新,有问题的读者可以在评论区留言。
最后
以上就是务实果汁为你收集整理的srsLTE 源码分析 – UE_01的全部内容,希望文章能够帮你解决srsLTE 源码分析 – UE_01所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复