
简介围绕3GPP TS 37.324 g20版本整理的SDAP协议详解文档聚焦5G NR服务数据适配协议适合协议栈研发、测试验证及无线协议学习者对照查阅。文档以SDAP架构和实体为切入点系统说明QoS流到DRB的映射关系、UL/DL SDAP数据PDU的构造与解析、末端标记控制PDU的作用以及反射QoS流到DRB映射中RDI/QFI/RQI字段的处理规则。对于SDAP实体建立与释放、上行默认DRB映射、下行RQI处理等过程也给出了贴近规范的步骤梳理能帮助读者快速建立5G用户面数据从QoS流到数据无线承载的完整认知。资源为docx格式共1个文件压缩包仅242KB便于直接阅读或打印。已有584人学习浏览可作为学习5G NR协议栈时的重点参考资料。1. 从一份docx认识SDAPTS 37.324 g20到底解决了什么问题调一个5G视频卡顿的问题抓包看了半天最后发现是SDAP头里的QFI字段没对齐UE把带头的包当裸IP解析整个业务流直接废掉。这种问题在4G时代不存在因为4G没有SDAP这一层到了5G用户面协议栈在PDCP之上多了一个服务数据适配协议也就是SDAP而它的全部行为规则都写在一份叫TS 37.324的3GPP协议文档里。文件名里的37324就是这份规范的编号g20是版本号g开头对应Release 16系列SDAP是协议名docx是3GPP官网发布的标准文档格式。这篇文章就围绕这份文档讲清楚SDAP是什么、TS 37.324该怎么读、映射机制怎么落地成代码、参数怎么配、以及实现时最容易踩的五个坑。适合协议栈开发、测试和无线协议优化工程师目标是让你读完能直接上手而不是停留在看图说话。2. 先读懂TS 37.324的文档结构从目录到关键章节的快速定位2.1 3GPP协议文档的固定骨架从Scope到PDU Format3GPP的协议规范有一套固定的章节结构TS 37.324也不例外。打开这份docx第一件事不是从头读而是先看目录确认你要的内容落在哪个章节。规范的正文一般从Scope开始接着是References、Definitions、Symbols and Abbreviations然后是General、Architecture、Procedures最后是PDU Format。SDAP的核心内容集中在架构、流程和PDU格式这三块架构章节告诉你SDAP实体长什么样、放在协议栈哪个位置流程章节告诉你QoS flow到DRB的映射在上下行各怎么走PDU格式章节告诉你SDAP头里每一个bit的含义。这个固定骨架意味着你不需要通读全文。一个做协议栈实现的人日常只需要反复翻三到五页一个做测试的人重点看流程章节和PDU格式章节就够。g20这个版本号也值得留意3GPP在文件名里用字母加数字标识版本g开头表示这是Release 16阶段的产物g20是这一系列里的一个具体修订版。不同小版本之间的差异通常只是措辞澄清和边界条件修正但后文会讲到恰恰是这些澄清最容易让实现翻车。2.2 用docx的检索特性快速筛出该读的章节3GPP官方发布的docx文件有个好处标题全部用的是Word的标准Heading样式。你在Word里打开这份文档左侧导航窗格可以按层级展开全部章节比PDF版本好用得多。很多人不知道的是Windows资源管理器的搜索框能直接搜docx正文但3GPP这类长文件名文档经常不被搜索索引覆盖更靠谱的做法是打开文档后直接CtrlF检索关键词。QFI、reflective、SDAP header、unmapped DRB这几个词是高频入口搜到哪就在哪附近精读。如果你的工作流偏自动化需要批量提取文档结构用python-docx按Heading样式遍历是最常见的做法from docx import Document doc Document(37324-g20.docx) for para in doc.paragraphs: # 3GPP官方文档的标题层级映射到Word自带的Heading 14 if para.style.name.startswith(Heading): level para.style.name.replace(Heading , ) print(f{level} {para.text})这段代码的逻辑很简单遍历docx里的所有段落只打印样式名以Heading开头的段落这些就是文档的标题行。3GPP规范在生成docx时保留了规范自身的层级编号比如5.2、7.1.3所以你在输出里看到的会是5 SDAP Architecture、7 SDAP PDU format这类原生编号配合Word自带的heading层级一起看定位速度比PDF快很多。如果电签工具或在线转换工具把docx转成JSON也能拿到类似结果但没必要——协议文档的价值在措辞本身转成JSON并不会让shall和should的区别更好理解。常见的做法是保留docx原格式用Word导航加脚本辅助两个手段配合。2.3 SDAP在协议栈里的位置RRC之下、PDCP之上SDAP只存在于NR用户面控制面没有这一层。从下往上看用户面协议栈MAC处理调度和复用RLC处理分段和重传PDCP处理加密和序号管理SDAP坐在PDCP上面直接面对IP包。IP层的数据包到了SDAPSDAP根据QoS flow标识QFI决定把它塞进哪个数据无线承载DRB然后交给PDCP往下传。换句话说SDAP解决的是这个包该走哪条路的问题PDCP和它以下解决的是这条路怎么把包安全送到的问题。SDAP实体是按PDU session粒度建立的。一个PDU session对应一个SDAP实体一个SDAP实体可以管理多个DRB而一个DRB上可以复用多个QoS flow。理解这个层级关系非常重要因为后面实现时你会发现映射表的查询键是QFI但表本身是挂在SDAP实体下的不是挂在DRB下的。很多人第一次写代码时把映射表挂在DRB上两个PDU session一交叉映射就乱了。2.4 端到端是错觉SDAP只管空口这一跳在4G的EPS架构里QoS的处理靠bearereNB直接在空口上把bearer映射到无线承载中间没有独立的适配层。到了5G核心网把QoS粒度细化成了QoS flow空口这一侧需要有一个机制把flow映射到DRBSDAP就是为这个需求生出来的。QFI从核心网的下行包GTP-U头里带过来gNB收到后查映射规则决定这个flow走哪个DRB同时把QFI填进SDAP头里发给UE。这里有个最常见的误解很多人以为QFI是端到端透传的UE是从SDAP头里读到QFI才知道自己在哪个flow里。实际上QFI的权威来源是核心网SDAP头里的QFI只是gNB为了让UE做上行映射而附带的信息。UE上行时自己决定某个QFI走哪个DRBgNB收到上行的SDAP PDU再往核心网转发此时核心网看的是GTP-U头里的QFISDAP头已经没用了。所以SDAP是严格的一跳协议只管UE和gNB之间这一段别把它当成端到端协议来设计。3. SDAP的核心机制QoS flow到DRB的映射怎么落地成代码3.1 上下行两条路径gNB决策、UE跟随下行方向映射的决策权在gNB。gNB从核心网收到下行数据GTP-U头里携带QFIgNB的SDAP实体查本地映射配置找到这个QFI对应的DRB把数据包映射到该DRB的PDCP实体。如果RRC配置要求下行带SDAP头gNB还要在IP包前面加一个SDAP头头里放QFI这样UE收到后能知道这个包属于哪个QoS flow。上行方向映射的决策权在UE。UE的应用层数据进入SDAP实体时SDAP根据当前已知的映射关系选择合适的DRB。问题是UE怎么知道哪个QFI该走哪个DRB两种途径一种是RRC直接在配置里告诉UE比如QFI 3、5、7走DRB 2这是静态映射另一种是反射映射UE通过下行收到的SDAP头里的QFI和承载它的DRB反推映射关系这是动态学习。上行没有映射的flow默认走default DRB。这张映射表是SDAP实现的核心数据结构下行靠它查DRB上行靠它选DRB。3.2 SDAP数据PDU头格式从1字节到2字节SDAP数据PDU的头格式是TS 37.324里最需要抠细节的部分。最小形态是1字节最高1位是D/C指示1表示数据PDU0表示控制PDU如果没开反射QoS相关特性剩下的7位里只有低6位是QFI。QFI取值范围是0到636个bit刚好够用。开了反射QoS之后头会扩到2字节多出来的位分别携带RDI和RSPI两个指示一个用于下行反射映射学习另一个用于上行反射策略指示。用C语言描述这个头结构常见做法是这样typedef struct { uint8_t d_c : 1; // 1: 数据PDU0: 控制PDU uint8_t rdi : 1; // 反射QoS下行映射指示1表示UE需要学习 uint8_t rspi : 1; // 反射QoS上行策略指示 uint8_t qfi : 6; // QoS flow标识0~63 } sdap_data_hdr_t; // 解析SDAP数据PDU头假设已确认是数据PDU int sdap_parse_hdr(const uint8_t *buf, sdap_data_hdr_t *hdr) { // 第一个字节的低6位就是QFI hdr-qfi buf[0] 0x3F; hdr-d_c (buf[0] 0x80) ? 1 : 0; hdr-rdi (buf[0] 0x40) ? 1 : 0; hdr-rspi (buf[0] 0x20) ? 1 : 0; if (!hdr-d_c) { // 控制PDU不在这里处理走单独分支 return -1; } return 0; }这段代码演示的是头解析里最容易做错的部分按位取值。很多人拿到第一个字节直接赋值给uint8_t变量结果QFI值被高位的D/C、RDI、RSPI污染了。正确做法是先用掩码0x3F把低6位隔离出来。这里有个细节D/C位在第7位从0开始数的第7位也就是bit7RDI和RSPI紧随其后不同厂家实现时位域顺序可能不同但只要是按3GPP规范走bit分配就是这个顺序。解析函数的返回值用来区分数据PDU和控制PDU数据PDU走正常映射流程控制PDU走另外一条极短流程——控制PDU在g20版本里几乎没有存在感大多数业务流根本用不到。3.3 反射QoS省信令的代价是状态同步反射QoS是SDAP里最有特色也最容易做错的机制。它的设计初衷是省掉RRC重配信令gNB希望UE把某个QFI的上行流量切到新的DRB时不需要专门发一条RRC消息只需要在下行SDAP头里把RDI置1UE收到后就知道这个QFI原来走的是这个DRB以后上行也这么走。这个学习过程在实现里是一个查表加写表操作。UE在收到带RDI指示的下行SDAP PDU时以QFI为键记录当前这个QFI是从哪个DRB下来的写入本地上行映射表。问题是这个表什么时候失效规范里没有定义反射映射的老化定时器但实际产品里必须加否则UE的映射表会随着业务波动越积越大老映射迟迟不更新新映射又进不来。常见的做法是给每条反射学习来的映射加一个超时时间超时后回退到default DRB。这是实现侧的经验值规范上没有硬性数字但你不上这个老化机制现网跑一段时间就会出问题。3.4 最小实现代码骨架一个SDAP实体的核心流程把上文的逻辑串起来一个最小SDAP实体在数据面也就三个函数下行入口、上行出口、映射表管理。下行入口负责查表加头上行出口负责选DRB映射表管理负责增删查。typedef struct { uint8_t qfi; uint8_t drb_id; uint8_t learned; // 1表示反射学习0表示RRC静态配置 uint32_t last_used; // 老化时间戳反射项专用 } sdap_map_entry_t; typedef struct { sdap_map_entry_t maps[64]; // QFI最多64个直接数组索引 uint8_t header_dl; // RRC配置下行是否带头 uint8_t header_ul; // RRC配置上行是否带头 uint8_t default_drb; // 默认DRB } sdap_entity_t; int sdap_dl_data(sdap_entity_t *ent, uint8_t *pkt, uint16_t len, uint8_t qfi) { // 1. 查映射表找不到就落到default DRB uint8_t drb ent-default_drb; for (int i 0; i 64; i) { if (ent-maps[i].qfi qfi) { drb ent-maps[i].drb_id; break; } } // 2. 配置了带头才加SDAP头头放在IP包前面 if (ent-header_dl) { // 这里的pkt要预留1~2字节头部空间 pkt[0] (1 7) | (qfi 0x3F); pkt 1; len - 1; // 实际载荷长度减少 } // 3. 交给对应DRB的PDCP实体发送 return pdcp_send(drb, pkt, len); }这段代码把下行主流程拆成了三步查映射、加头、下发PDCP。第二步的头部空间是调用方上层IP协议栈预先留好的否则直接移动指针会把缓冲区搞乱。注意这里len - 1和pkt 1是配套的头占了一个字节有效载荷指针后移长度也要减掉。实际产品里还要处理头部空间不足的异常情况以及头部扩展成2字节的分支。这个骨架没有处理控制PDU和反射学习但主路径已经完整后续扩展反射QoS只需要在查表之前加一个学习分支。4. SDAP实现的关键参数与配置哪些参数不调对上层业务就翻车4.1 配置从哪里来RRC里的SDAP-ConfigSDAP协议自己没有独立的配置信令所有参数都由RRC层通过无线资源控制消息下发。具体来说是RadioBearerConfig里带的SDAP-Config这是TS 38.331里定义的IETS 37.324只管SDAP层内部行为不定义配置语法。这意味着SDAP实现必须要跟RRC模块对接RRC解析完配置成结构体SDAP实体拿着这份配置初始化自己。很多做PDCP/RLC出身的人第一次写SDAP时习惯性地去SDAP规范里找配置结构翻遍全文找不到然后才意识到SDAP配置在外头。这个对接点是最容易漏的架构边界。常见做法是RRC层在建立DRB时把SDAP-Config解析成一个配置结构体连同DRB ID一起传给SDAP实体。配置里最关键的三样东西上下行头开关、default DRB指示、QFI映射列表。这三样决定了SDAP实体能不能正常工作也是踩坑重灾区。4.2 三个必调参数头开关、default DRB、QFI映射第一个必调参数是上下行SDAP头开关。规范里对应sdap-HeaderDL和sdap-HeaderUL两个字段取值为present或absent。present表示这一侧要加SDAP头absent表示不加。这两个字段在UE和gNB两侧必须对齐gNB下行配置了presentUE解析下行数据时就必须按带头处理否则UE把SDAP头当成IP头首字节整个包全错。反过来UE上行配置了presentgNB在接收方向也要按带头解析。实现时必须检查RRC配置的重配流程保证切换、重建之后这两个值不被重置成默认值。第二个必调参数是default DRB。当一个上行QFI在映射表里查不到对应DRB时SDAP实体把包往default DRB上塞。这个参数给错了所有未被显式映射的流量会直接走到错误的承载上QoS优先级完全失效。参数本身是一个DRB IDRRC在SDAP-Config里通过defaultDRB字段指定。第三个必调参数是QFI到DRB的映射列表。RRC在配置DRB时通过mappedQoS-FlowsToDRB字段告诉UE和gNB哪些QFI归这个DRB管。这张表是SDAP实体初始化映射表的唯一来源。这里有一个容易被忽略的边界一个QFI只能映射到一个DRB一个DRB可以映射多个QFI但下行和上行可以各自不同。实现时要支持上下行独立查表。参数作用取值范围常见翻车点sdap-HeaderDL / sdap-HeaderUL控制数据面是否携带SDAP头present / absent对端配置不一致导致整包解析错位defaultDRB未映射QFI的兜底承载DRB ID指向不存在的DRB上行丢包mappedQoS-FlowsToDRBQFI与DRB的静态映射关系QFI列表0~63同一个QFI出现在多个DRB配置里4.3 头精简在协议正确性和带宽开销之间取舍SDAP头不是必须的。如果某个DRB上只有一个QoS flow或者所有QoS flow的映射关系都已经是确定的加头反而浪费空口资源。RRC配置里把sdap-HeaderDL和sdap-HeaderUL设为absentSDAP层就直接透传IP包不做任何头操作。这是规范允许的也是大多数业务早期的默认形态。但在配置为absent时必须注意一个前提接收方向必须能区分带SDAP头的包和不带SDAP头的包。这个区分靠的是配置对齐而不是包内容里的某个标志。也就是说一个DRB要么全部收带头包要么全部收不带头包不能混着收。实际产品里常见的问题是在DRB重配过程中RRC配置已经改成absent但gNB侧还在继续加头UE按新配置解析就错位了。解决方法是重配生效的时机要跟PDCP层同步确保空中接口上从某个PDCP序号开始统一使用新的头配置格式。4.4 没有定时器但有时序约束TS 37.324正文里没有定义任何SDAP层定时器这和RLC有重传定时器、PDCP有丢弃定时器很不一样。但这不代表实现侧什么都不用管。反射QoS学习到的映射条目要有老化机制这一点在3.3节已经讲过另一个隐性的时序约束是SDAP PDU的处理不能打乱PDCP的序号顺序。SDAP层在数据面上对上层IP包做映射和加头做完之后交给PDCPPDCP按到达顺序分配序号。如果SDAP内部因为查表等待或映射表更新出现乱序交付PDCP层的序号分配就会错乱接收方向的重排序和重复包检测全都会出问题。所以SDAP实体的处理函数必须是同步且无阻塞的映射表查询不能加锁等待更不能用可能在运行时阻塞的操作。5. SDAP落地避坑五个让协议栈返工的真实场景5.1 现象下行流量全断UE侧IP层解析失败原因gNB侧下行配置了带SDAP头但UE侧对应DRB的sdap-HeaderDL配置成了absent。UE把收到的SDAP头第一个字节当成IP头版本号来解析拿到的IP头完全错乱IP层直接丢包。这类问题在切换场景里特别常见切换后RRC重配没带SDAP头配置UE回退到默认值而gNB没同步回退。解决排查时先在UE侧日志里确认该DRB的SDAP头配置值再对比gNB侧两个方向的配置必须一致。如果切换后才出现重点查切换后的RadioBearerConfig里SDAP-Config有没有被正确携带。5.2 现象UE上行大量流量走default DRBQoS weight失效原因反射QoS开关没有生效。gNB设备发了带RDI的包但UE侧SDAP实体的反射学习功能没打开或者RRC配置里压根没开启反射QoS特性。反射学习不仅仅是头里有RDI就学它依赖RRC配置里的总开关开关关着RDI置1也白搭。很多人只看到SDAP头格式里有RDI位以为解析到位就等于生效了忽略了上层开关。解决配置检查两步走第一步确认RRC里的反射QoS总开关是使能的第二步确认下行SDAP头里RDI位在实际抓包里确实为1。两步同时满足UE才会把映射写入表。5.3 现象QFI映射错乱业务走到完全无关的DRB上原因QFI解析时没做掩码。QFI是6bit字段取值范围0到63但很多实现直接从缓冲区里读一个uint8_t赋值给QFI变量。当头里还有D/C位和RDI位时读出来的值变成0x80以上的数字查表直接查空或查到错误条目。这类问题在初期联调阶段极难发现因为空映射落default DRB表现只是QoS不对整体还是通的。解决解析入口统一做掩码处理qfi byte 0x3F同时在校验处拒绝大于63的QFI值。建议在SDAP实体入口加一个断言任何QFI超过63直接报错返回也方便测试尽早发现抓包里的异常。5.4 现象按旧版本实现的行为在g20版本设备联调时行为异常原因g20版本相对早期g版本在某些边界条件上做了澄清。典型的是未定义的QFI值和无映射DRB场景——旧版本里措辞模糊实现可以做A也可以做B新版本改用shall明确要求。只读正文不读变更记录沿用旧行为就会在联调时对不上设备厂家的实际行为。解决拿到g20后先翻变更记录。3GPP的docx版本在文档前部会列出相对上一版本的变更条款逐条核对哪些内容影响你的实现。经验是每次都把shall和should的句子单独筛出来过一遍这些地方是协议测试最喜欢出题的点。5.5 现象多个PDU session同时活动时SDAP映射表互相污染原因SDAP实体按PDU session建立但实际实现里如果多个实体共用一份映射表结构体或者实体的DRB归属没区分清楚就会出现session A的QFI映射到session B的DRB上。尤其是双连接或载波聚合场景下DRB归属跨小区、跨实体管理代码里一个全局变量随手传错线上就是疑难杂症。解决每个SDAP实体持有自己的映射表不允许共享。所有接口函数显式传入实体指针禁止用全局映射表。排查时先确认日志里的实体ID和PDU session ID是不是一一对应再看映射表条目里记录的是不是本实体的DRB。6. 用抓包和日志验证SDAP映射是否真的生效验证SDAP映射不能只靠看代码最直接的手段是抓包加查日志。空口抓包拿到的是RLC层之后的数据Wireshark对SDAP的协议解析支持很有限但你可以用最笨也最可靠的办法手动验证找到PDCP payload的第一个字节最高位如果是1说明SDAP头在低6位就是QFI。把这套判断固化成一个小的解析脚本每次联调都跑一遍比肉眼盯十六进制靠谱得多。import sys # 读入一段二进制数据假设第0个字节是SDAP头的第一字节 data sys.stdin.buffer.read() if not data: sys.exit(0) first data[0] is_data (first 0x80) 0x80 qfi first 0x3F rdi (first 0x40) 0x40 rspi (first 0x20) 0x20 print(fis_data{is_data}, qfi{qfi}, rdi{rdi}, rspi{rspi})验证流程建议按四步走。第一步在核心网侧找到业务流的QFI值比如在GTP-U头里确认这个流的QFI等于5。第二步在gNB日志里查这个QFI对应的DRB ID常见格式是类似qfi: 5, drb: 4, action: map的行。第三步在空口抓包里找到同一个包的下行数据解析SDAP头确认QFI确实是5且承载它的逻辑信道对应的DRB ID确实是4。第四步如果开了反射QoS再发一个上行包确认UE是否按新学习的映射把包发到DRB 4。这套流程里最花时间的一般是第三步抓包里的DRB ID不直观需要结合RLC header或MAC层的逻辑信道信息来反推。我还在摸索更顺手的自动化方法目前的经验是gNB侧日志里的映射表打印比抓包解析更快因为厂家一般会在RRC重配生效时把整张映射表打出来。建议你在测试环境里主动触发一次RRC重配让某个QFI从DRB A切到DRB B然后看两点SDAP头里的QFI是否保持不变以及承载的逻辑信道是否变了。如果QFI变了说明映射表更新和头解析之间有时序问题如果逻辑信道没变说明PDCP绑定错了。我最初做SDAP实现时在头开关对齐上栽过一次gNB侧配置了带头UE侧没配结果整条业务流黑匣子一样什么都看不见。后来养成一个习惯每接一个新版本协议第一件事是把变更记录里所有涉及shallshould的句子摘出来第二件事是写一个20行的头解析脚本每轮联调先跑脚本再调业务。这两个习惯帮我避免了不少返工。希望帮到你。本文还有配套的精品资源点击获取