
简介这份PPT课件面向5G NR平台开发与运维方向的工程师及通信专业学习者系统讲解OAM操作、管理与维护子系统的整体架构与关键实现机制帮助读者建立对基站侧运维管理模块的完整认知。压缩包内仅含1个pptx文件约890KB以图文与代码片段结合的方式呈现便于课堂讲解与自学翻阅。内容覆盖Makefile工程构建规则、RT实时补丁与IOS平台框架、初始化配置表、Mem内存结构以及调度任务配置等核心模块并给出T_START_CONFIG、T_SYS_PWRON_CFG、T_SCHE_TASK_CFG等典型配置表实例同时涉及通讯路由、消息收发接口与定时器接口的调用方式。目前已有339人学习适合希望深入理解5G NR平台底层运行机制、补齐OAM知识短板的开发者参考。1. 从一份 OAM 子系统资料说起5GNR 平台里最容易被低估的“地基”如果你正在找 5GNR 平台相关的课程资源大概率已经翻过不少讲协议栈、讲物理层的材料但真正落到平台工程实现层面OAM 子系统的资料其实相当稀缺。这份“5GNR平台-OAM子系统介绍”的课程资源核心不是讲 OAM 的协议定义而是拆开了一个 NR 平台从 Makefile 构建、IOS 跨平台抽象层、内存管理、调度任务、通讯路由到 DBS 数据库的完整工程骨架。它适合谁适合已经了解 5G 基本概念、但面对一个真实 NR 平台代码仓库不知道从哪下手的开发者也适合做嵌入式平台移植、想把 OAM 模块从“黑匣子”变成可调试对象的工程师。一句话它解决的是“平台怎么搭起来、任务怎么跑起来、消息怎么传过去”这三个落地问题。2. Makefile 工程框架与 IOS 抽象层编译入口和跨平台底座2.1 Makefile 在 NR 平台工程里到底管什么很多人对 Makefile 的理解停留在“gcc 命令的包装”但在 NR 平台这种多子系统、多目标、多架构的工程里Makefile 承担的是整个编译链路的编排。这份资料里提到的 Makefile 框架核心思路是顶层 Makefile 负责定义工具链、编译选项和全局变量各子系统目录下的 Makefile 负责描述自己模块的源文件列表、头文件搜索路径和依赖关系最终通过递归调用完成全量构建。常见做法是顶层 Makefile 里定义类似CROSS_COMPILE、TARGET_ARCH、BUILD_TYPE这样的变量子模块 Makefile 通过include $(TOP_DIR)/rules.mk引入公共规则。这样做的好处是换平台时只需要改顶层变量不用动每个子模块。一个典型的子模块 Makefile 结构大概长这样# 子模块 Makefile定义本模块的源文件和编译目标 LOCAL_PATH : $(call my-dir) # 源文件列表新增文件必须加到这里否则不会参与编译 SRCS : oam_init.c \ oam_msg.c \ oam_timer.c # 头文件搜索路径顺序影响同名头文件的优先级 INC_DIRS : $(LOCAL_PATH)/include \ $(TOP_DIR)/ios/include \ $(TOP_DIR)/dbs/include # 编译目标静态库最终由顶层链接进可执行文件 TARGET : liboam.a include $(TOP_DIR)/rules.mk逻辑说明LOCAL_PATH用my-dir函数拿到当前 Makefile 所在目录避免硬编码路径。SRCS列出本模块所有.c文件漏加文件是新手最常见的翻车点——编译不报错但功能就是不对。INC_DIRS的顺序很关键如果两个目录下有同名头文件排在前面的会先被找到。TARGET定义产出物类型NR 平台里 OAM 模块通常编译成静态库由顶层统一链接。参数方面BUILD_TYPE一般控制-O2还是-O0 -g调试版本务必关掉优化否则断点跳来跳去会让你怀疑人生。CROSS_COMPILE前缀决定了用哪套工具链ARM 和 x86 的切换就靠这个。2.2 IOS 抽象层跨操作系统、跨硬件平台的粘合剂IOSIntegrated Operating System是这份资料里最值得反复看的部分。它不是 iOS而是 NR 平台自己的一套跨平台抽象层位于 RTOS 之上、业务模块之下。IOS 要解决的核心问题是上层业务代码不应该关心底层是哪个 RTOS、跑在哪个硬件上所有平台相关的操作都通过 IOS 接口统一封装。IOS 提供的功能包括上电管理、内存资源管理、二次调度SP、定时器资源、进程和线程间通讯以及异常解析保存EXC 和 BBX。其中 EXC 和 BBX 是异常处理的两个模块EXC 负责异常现场捕获BBX 负责异常信息保存两者配合可以在系统崩溃后留下可追溯的日志。上电管理通过初始化配置表驱动资料里给出了T_START_CONFIG和T_SYS_PWRON_CFG两个结构/* 初始化配置表每项包含模块ID、初始化函数、执行标志和错误提示 */ T_START_CONFIG g_atInitConfigList[] { {0x0001, TEST_Init, 1, TEST_Init is error!}, {0x0001, TEST_Init2, 1, TEST_Init2 is error!}, {0x0001, TEST_Init3, 1, TEST_Init3 is error!} }; /* 上电表定义上电时需要启动的 SP 及其 ID */ T_SYS_PWRON_CFG g_atSysPwrOnCfg[] { {SP_USE, 0x0001}, {SP_USE, 0x0002}, {SP_USE, 0x0003} };逻辑说明g_atInitConfigList是一个函数指针数组系统启动时按顺序遍历逐个调用初始化函数。第三个字段1表示该初始化项是否使能改成0就可以临时跳过某个模块的初始化调试时非常实用。g_atSysPwrOnCfg定义上电时需要激活的 SPSP_USE是状态标志0x0001是 SP ID。上电流程最终通过IOS_SendMsg(PLAT_MASTER_POWER_ON, NULL, 0, g_atSysPwrOnCfg[u16Indx].u16SPID)把上电消息发给对应的 SP。这里有个容易忽略的细节初始化配置表里多个项可以共用同一个模块 ID比如都是0x0001但初始化函数不同。这意味着模块 ID 不是唯一键遍历时不能拿它做去重判断。3. 内存管理、调度任务与通讯路由平台运行时的三条命脉3.1 Mem 内存结构UB Head 与 UB Body 分离的设计意图NR 平台的内存管理采用了一种 UBUser Buffer结构核心设计是 UB Head 和 UB Body 分离。UB Head 存放描述信息比如 buffer 长度、使用状态、映射关系UB Body 存放实际数据。两者在物理上分开存储好处是即使 UB Body 出现非法写入UB Head 到 UB Body 的映射关系仍然完好系统可以通过 Head 信息定位到被破坏的 Body而不是整个内存池一起崩掉。申请 UB 时只需要递增Used Indx这是一个典型的无锁环形分配思路。释放 UB 时需要交换CurUsedIndx与当前释放的 UB 位置这个操作保证了空闲块可以重新被后续申请复用。我一般会建议在调试阶段打开内存越界检测具体做法是在 UB Body 前后各加一个哨兵字节释放时检查哨兵是否被改写。如果被改写说明有越界写结合 UB Head 里的模块 ID 可以快速定位是哪个模块写越界了。3.2 调度任务、私有任务与 SP 的层级关系NR 平台的调度机制分三层调度任务Schedule Task、私有任务Private Task和 SPSecondary Process。资料里给出了三张配置表分别对应这三种任务形态。调度任务配置表/* 调度任务配置任务ID、优先级、核心绑定、任务名、栈大小、入口函数 */ T_SCHE_TASK_CFG g_atSchTaskCfg[] { {1, {20, IOS_BIND_CORE_INVALID, {SCH1}, 20*2048, SCHD_TaskEntry}}, {2, {30, IOS_BIND_CORE_INVALID, {SCH2}, 20*2048, SCHD_TaskEntry}}, {3, {40, IOS_BIND_CORE_INVALID, {SCH3}, 20*2048, SCHD_TaskEntry}} };参数说明第一个字段是任务 ID第二个字段是一个嵌套结构包含优先级20/30/40数值越小优先级越高还是越低取决于 RTOS 实现常见做法是数值越小优先级越高、核心绑定IOS_BIND_CORE_INVALID表示不绑定特定核心、任务名、栈大小20*2048即 40KB和入口函数。栈大小给少了会栈溢出给多了浪费内存40KB 对于一般调度任务够用但如果入口函数里有大局部数组需要适当加大。私有任务配置表/* 私有任务配置索引、使能、模块ID、任务属性、入口函数 */ T_PRIVATE_TASK_CFG g_atPrivateTaskCfg[] { 0, 1, IRAN_PDCP, {70, 0, TestT, 20*2048, Test_TaskEntry1}, 1, 1, IRAN_WLMT_PROC_SRV, {70, 0, LMT_PROC_SRV, 2*2048, COMM_WBSAgentTask}, 2, 1, IRAN_WLMT_PROC_DATA, {70, 0, LMT_PROC_DATA, 2*2048, COMM_WBSAgentProcDataTask} };私有任务和调度任务的区别在于私有任务可以配置是否具备邮箱也可以设置私有定时器资源。IRAN_PDCP、IRAN_WLMT_PROC_SRV这些是模块 ID用于在消息路由时识别目标。栈大小 2KB 对于 LMT 处理任务来说偏紧如果处理的消息体较大建议加到 4KB 以上。SP 配置表/* SP 配置状态、SP ID、SP 名称、优先级、入口函数 */ T_SP_CFG g_atSPCfg[] { {SP_USE, 0x0001, TEST_SP, 1, Test_SPEntry}, {SP_USE, 0x0002, TEST_SP2, 2, Test_SPEntry1}, {SP_USE, 0x0003, TEST_SP2, 2, Test_SPEntry3} };SP 可以理解为一个函数调用由消息驱动具备自身私有的消息队列和定时器资源。SP 使用过程中只需管理自己申请的内存其它内存由 IOS 管理。对于一个子系统下的多个 SP它们挂在同一个调度任务下天然串行执行不需要额外加锁。但不同调度任务下的 SP需要考虑同步和互斥常见做法是用消息串行化来避免竞态。3.3 通讯路由与消息收发接口通讯模块的核心是路由配置表T_SYS_COMM_CFG/* 通讯路由配置源模块、目的模块、消息类型、队列优先级、长度获取函数、接收处理函数 */ T_SYS_COMM_CFG g_atSysCommCfg[] { {0, 4, E_UPF_TO_PDCP, E_RES_PRI_QUEUE, Test_GetRsvLen, Test_PdcpRecvMsg} };参数说明0和4是源和目的模块编号E_UPF_TO_PDCP是消息类型枚举E_RES_PRI_QUEUE指定使用保留优先级队列Test_GetRsvLen是接收前获取消息长度的回调Test_PdcpRecvMsg是实际的消息处理函数。新增一条通讯路径就是在这张表里加一行然后实现对应的长度获取和处理函数。消息收发接口有三个/* 发送消息到指定 SP */ S32 IOS_SendMsg(U16 u16MsgType, S8 *ps8MsgBuf, U16 u16MsgLen, U16 u16DstSPID); /* 发送引用消息消息体不拷贝只传指针 */ S32 IOS_SendRefMsg(S8 *ps8MsgBuf, U16 u16MsgLen, S32 s32QID); /* 发送扩展消息支持指定通讯类型 */ S32 IOS_SendExtMsg(U6 u6MsgType, S8 *ps8MsgBuf, U32 u32MsgLen, E_NR_SYS_COMM_TYPE eCommType);IOS_SendMsg是最常用的消息体拷贝到内部缓冲区发送后调用方可以立即释放自己的 buffer。IOS_SendRefMsg不拷贝只传指针适合大消息体场景但调用方必须保证在接收方处理完之前不能释放 buffer否则就是 use-after-free。IOS_SendExtMsg多了一个通讯类型参数用于区分 CU-DU 分离部署时的内部 SCTP 和 UDP 通道。消息结构体定义/* 普通消息只含 buffer 指针 */ typedef struct _T_QMSG { S8 *ps8Buf; } T_QMSG; /* 引用消息含 buffer 指针和长度 */ typedef struct _T_QREF_MSG { S8 *ps8Buf; U16 u16MsgLen; } T_QREF_MSG;注意T_QMSG里没有长度字段长度信息在消息头里接收方需要通过Test_GetRsvLen这类回调提前获取。T_QREF_MSG显式带了长度接收方可以直接读。4. 定时器、TS 传输与 DBS 数据库OAM 周边的三个支撑模块4.1 定时器接口与时间源管理定时器模块提供了一组设置接口按用途分三类Agent 定时器IOS_SetAgentLoopTimer/IOS_SetAgentRelTimer、普通定时器IOS_SetLoopTimer/IOS_SetRelativeTimer和任务定时器IOS_SetTaskLoopTimer/IOS_SetTaskRelTimer。Loop 表示循环定时器Rel 表示单次相对定时器。/* 设置 Agent 循环定时器定时器号、周期毫秒、参数、目标 SP ID */ U16 IOS_SetAgentLoopTimer(U8 u8TimerNo, U32 u32Duration, U32 u32TimerPara, U16 u16DstSPID); /* 设置任务单次定时器 */ U16 IOS_SetTaskRelTimer(U8 u8TimerNo, U32 u32Duration, U32 u32TimerPara);参数说明u8TimerNo是定时器编号同一个编号重复设置会覆盖之前的定时器。u32Duration单位通常是毫秒但具体取决于平台 tick 配置。u32TimerPara是透传参数定时器到期后这个消息会带回给处理函数。u16DstSPID指定定时器到期后消息发给哪个 SP。定时器源的产生和管理涉及一个环形计数逻辑/* 计算定时器的 scale 和 circle用于处理定时器溢出 */ *pu16Scale (u16CurScale u32Scal) % TIMER_SCALE_COUNT; *pu32Circle u32Scal / TIMER_SCALE_COUNT;逻辑说明TIMER_SCALE_COUNT是定时器刻度总数u32Scal是需要的总刻度数。取模得到在当前圈内的位置整除得到需要绕多少圈。这个逻辑保证了即使定时器计数溢出也能正确计算到期时间。打点接口TMR_GetCurSoft1MSCnt()和VOS_GetElapsedJiffies()用于获取当前时间戳前者精度到 1ms后者是系统 jiffies。做性能统计时在函数入口和出口各打一个点差值就是耗时。4.2 TS 传输功能TCP/UDP/SCTP 与 RRU 通道TSTransport Subsystem需要实现 TCP、UDP、SCTP 之上的单播和广播通讯同时考虑 IPv4 和 IPv6。对于 RRU 通道涉及 RRU 版本升级、RRU 配置、RRU 告警及状态监控。NR 内部考虑 CU、DU 分离CU-CP 和 CU-DP 分离部署会存在内部的 SCTP 和 UDP 通讯接口。常见做法是 TS 层对上提供统一的 socket 抽象接口对下根据配置选择 TCP、UDP 或 SCTP。SCTP 用于需要可靠传输且多流控制的场景比如 CU-CP 和 DU 之间的 F1 接口。UDP 用于对时延敏感但可以容忍少量丢包的场景比如某些实时控制消息。IPv4 和 IPv6 的双栈支持需要在 socket 创建时指定地址族或者使用AF_INET6配合IPV6_V6ONLY选项来控制。RRU 通道通常走 UDP因为 RRU 数量多TCP 的连接维护开销太大。4.3 DBS 数据库表的增删改查与触发器DBSDatabase Subsystem主要实现对表的增删改查操作和触发器操作保证数据库的数据一致性、可靠存储和加载。在 OAM 子系统里DBS 通常用于存储配置数据、告警状态和性能计数器。常见做法是 DBS 提供一套类似 SQL 的接口但底层可能是内存表加持久化文件。触发器用于在数据变更时自动执行关联操作比如配置表更新后自动通知相关模块重新加载配置。使用 DBS 时需要注意批量操作要放在事务里否则中途失败会导致数据不一致。持久化频率要可配置太频繁影响性能太稀疏掉电会丢数据。5. 避坑与排查OAM 子系统调试中常见的五个翻车现场5.1 初始化配置表遍历时拿模块 ID 做去重现象系统启动后某个模块的初始化函数没有被调用但日志里没有任何报错。原因初始化配置表里多个项共用同一个模块 ID遍历代码用模块 ID 做了去重判断导致后续同 ID 的初始化项被跳过。解决遍历时不要用模块 ID 做唯一性判断改用数组索引或者初始化函数指针作为唯一标识。如果确实需要去重在配置表里加一个唯一的序列号字段。5.2 SP 间共享内存释放后未置空导致 use-after-free现象系统运行一段时间后随机崩溃崩溃点不固定有时在消息处理函数里有时在内存分配函数里。原因两个 SP 之间通过IOS_SendRefMsg传递消息发送方在接收方处理完之前就释放了 buffer接收方读到的是已释放内存。或者发送方释放后没有把指针置空后续代码误判为有效指针再次使用。解决使用IOS_SendRefMsg时发送方必须等接收方回确认消息后才能释放 buffer。更稳妥的做法是改用IOS_SendMsg让 IOS 内部做拷贝。如果必须用引用消息在释放后立即把指针置为 NULL并在每次使用前做非空判断。5.3 定时器编号冲突导致定时器被意外覆盖现象某个周期性任务执行几次后就不再执行了但定时器设置接口返回成功。原因两个模块使用了相同的u8TimerNo后设置的定时器覆盖了先设置的。由于接口返回成功调用方无法感知覆盖发生。解决在平台层面维护一张定时器编号分配表每个模块使用独立的编号段。或者在IOS_SetAgentLoopTimer内部加冲突检测如果编号已被占用且未到期返回错误码而不是静默覆盖。5.4 调度任务栈大小不足导致栈溢出现象任务运行到某个函数时崩溃崩溃地址在栈空间附近或者表现为莫名其妙的变量值被改写。原因调度任务配置表里栈大小给少了入口函数调用链较深或者局部变量较大时栈溢出踩到了相邻内存区域。解决在调试阶段打开栈使用量统计或者在任务切换时检查栈指针是否越界。对于调用链深的函数把大局部变量改成静态变量或者动态分配。栈大小建议留 30% 余量。5.5 通讯路由表新增条目后消息仍然走默认路径现象在g_atSysCommCfg里新增了一条路由但消息还是按原来的路径处理。原因路由匹配逻辑是按顺序遍历新加的条目放在了默认条目之后默认条目先匹配成功就返回了。或者新条目的源/目的模块编号写错了根本没匹配上。解决把特殊路由放在默认路由之前确保匹配优先级正确。新增条目后在路由匹配函数里加日志打印实际匹配到的条目索引确认走的是新路径。6. 从能跑到能调试OAM 子系统日志与异常定位的进阶手法OAM 子系统真正难的不是让它跑起来而是出问题的时候能快速定位。这份资料里提到的IOS_LOG宏是一个很好的切入点/* IOS 日志宏自动带入文件名和行号 */ #define IOS_LOG(u16CurSPID, eLogLevel, ...) \ { \ IOS_Printf(u16CurSPID, eLogLevel, __FILENAME__, __LINE__, ##__VA_ARGS__); \ }这个宏的巧妙之处在于__FILENAME__和__LINE__是编译期常量不增加运行时开销但能在日志里精确定位到代码位置。##__VA_ARGS__是 GCC 的扩展允许可变参数为空。我一般会在这个基础上做两层增强。第一层是加时间戳和 SP ID 过滤/* 增强版日志宏带时间戳和 SP 过滤 */ #define IOS_LOG_TS(u16CurSPID, eLogLevel, ...) \ { \ if (IOS_IsLogEnabled(u16CurSPID, eLogLevel)) { \ IOS_Printf(u16CurSPID, eLogLevel, [%u] %s:%d , \ TMR_GetCurSoft1MSCnt(), __FILENAME__, __LINE__, ##__VA_ARGS__); \ } \ }IOS_IsLogEnabled在运行时检查当前 SP 和日志级别是否在允许列表里避免高频日志拖慢系统。TMR_GetCurSoft1MSCnt()提供毫秒级时间戳分析时序问题时必不可少。第二层是异常现场保存。EXC 和 BBX 模块配合使用时可以在系统崩溃前把关键寄存器和调用栈保存到非易失存储里。常见做法是在 EXC 里注册一个异常钩子崩溃时先调用VOS_AccessFile打开一个预留的日志文件把现场信息写进去然后触发复位。复位后 BBX 负责读取这个文件并解析。验证日志和异常保存是否生效可以手动触发一次断言/* 手动触发断言验证异常保存流程 */ ASSERT(0);ASSERT在调试版本里会触发异常流程如果 EXC 和 BBX 配置正确复位后应该能在日志文件里看到断言的文件名和行号。如果看不到检查VOS_AccessFile的返回值确认文件系统在异常上下文中仍然可访问。还有一个实用技巧是VOS_GetElapsedJiffies()配合VOS_IsBigEndian()做跨平台时间统计。大端和小端平台下 jiffies 的读取方式可能不同VOS_IsBigEndian()可以在运行时判断字节序避免时间计算错误。从那以后我每次拿到一个新的 NR 平台代码仓库都强制走一遍“编译→上电→发消息→设定时器→触发断言→读异常日志”这条链路确认每个环节都有可观测的输出。这条链路走通了后面调业务逻辑才有底气。希望帮到你。本文还有配套的精品资源点击获取