5G NR 的调度逻辑全都压在一条信道上,就是 PDCCH——物理下行控制信道。终端每次收发数据之前,得先在这儿拿到一张写着"去哪儿收、用什么调制编码、占多少 RB、什么时候反馈 HARQ"的条子,这张条子就是 DCI(Downlink Control Information)。基站侧做调度器、终端侧做物理层解析,甚至做路测和故障排查的人,只要跟 NR 打交道,就绕不开 PDCCH 和 DCI 这两个词。很多刚转过来的同行会觉得它比 PDSCH 难啃:PDSCH 好歹有 CSI 反馈兜底,PDCCH 完全靠盲检,位置事先不知道,格式还一堆,尺寸还有对齐规则。这篇文章我打算把 PDCCH DCI 从"它是什么"一路讲到"参数怎么配、脚本怎么算、出问题怎么查",把 CORESET、REG、CCE、聚合等级、搜索空间、盲检、DCI 格式、尺寸对齐、链路处理这些碎块拼成一张完整的图。不管你是刚上手 NR 协议栈的工程师,还是做外场优化的,看完至少能对着配置和日志说出个所以然。
1. 从一条调度指令说起:PDCCH 和 DCI 到底是什么关系
1.1 用"车站广播"打个比方
我一般跟新人这么解释:PDSCH、PUSCH 是月台上的旅客,PDCCH 是候车厅的广播喇叭,DCI 就是广播里念出来的那条具体内容——"某某乘客,请到 3 号站台,14:05 发车,行李限额 20 公斤"。广播本身只是载体,真正有价值的信息全在那条内容里;而广播喇叭用的频段、音量、语速,就是 PDCCH 的物理层参数。这个比方能解释很多设计选择:广播得让所有人都能听清,所以广播(CSS,公共搜索空间)用的功率和聚合等级都偏保守;而针对某个人的私信(USS,终端专属搜索空间)可以省着点用资源。同时广播的覆盖范围有限,这也对应了 PDCCH 天然比 PDSCH 更早遇到覆盖瓶颈的现象。
从协议分层看,DCI 属于物理层信令,它不做重传、不做 ARQ,一次发出去就是一次,UE 没解出来就当作没收到。这一点决定了 PDCCH 的可靠性设计思路和 PDSCH 完全不同:PDSCH 可以靠 HARQ 重传补回来,PDCCH 只能靠提高聚合等级、加大功率、多配候选位置来提高一次成功的概率。理解了这一层,后面所有的参数取舍都好理解了——为什么远点要往上抬聚合等级,为什么搜索空间超配要按优先级丢,本质都是在"一次就得成功"这个约束下做资源分配。
1.2 PDCCH 占的是时频网格上哪块地
NR 和 LTE 在控制信道的位置安排上走了完全不同的路。LTE 把 PDCCH 摊在整带宽的前 1 到 3 个符号里,全带宽铺开,好处是简单,坏处是窄带终端也得听全带宽。NR 换了个思路:控制信道的时频资源被"容器化"了,这个容器就叫CORESET(Control Resource Set,控制资源集合)。一个 CORESET 在频域上占若干组 PRB(以 6 个 RB 为一个粒度,用 45 bit 的位图描述,最多覆盖 275 个 RB),在时域上占连续的 1 到 3 个 OFDM 符号。UE 只在自己被配置的 CORESET 里去找 PDCCH,不需要盯着整个带宽看。
这个设计带来几个直接好处。第一,UE 的接收带宽可以收窄,省电,这对物联网类终端很关键。第二,同一个载波里不同业务、不同参数集可以配不同的 CORESET,切片和业务隔离做起来自然。第三,频域位置可以灵活避让,遇到干扰可以选择在哪个 RB 段上放控制信道。
代价是复杂度上来了。LTE 时代 UE 知道"PDCCH 就在前 3 个符号里",NR 时代 UE 得先知道"我的 CORESET 长什么样"。所以就有了一个鸡生蛋的问题:UE 刚开机、还没建立任何 RRC 连接的时候,怎么知道 CORESET 配置?答案是CORESET#0,它的配置在 MIB 里通过pdcch-ConfigSIB1这个 8 bit 的字段携带,高 4 bit 是 CORESET#0 的索引,低 4 bit 是搜索空间#0 的索引,两者分别查协议里预定义的表格(38.213 第 13 章那一堆表)得到具体的时频资源。这就是为什么做小区初搜的人必须把那张表背下来。
1.3 谁在发、谁在听、听多久
发送侧永远是网络,也就是 gNB。接收侧是所有被调度到的 UE。这里有个容易忽略的点:PDCCH 是"一对多"的广播式信道,但 DCI 内容是"一对一"的。基站把多个 UE 的 DCI 拼在同一个 CORESET 里发出去,每个 UE 靠什么区分哪条是自己的?靠RNTI(Radio Network Temporary Identifier)加扰 CRC。基站在计算 DCI 的 CRC 时,把目标 UE 的 RNTI 混进去,UE 收到后拿自己手里的 RNTI 去解 CRC,对得上才认为这条 DCI 是给自己的。这个机制很巧妙:UE 不需要在 DCI 里额外传一个 16 bit 的地址字段,省了开销,又天然实现了寻址。
常见的 RNTI 有那么几类。系统消息用 SI-RNTI,取值固定为 FFFF;寻呼用 P-RNTI,取值 FFFE;随机接入过程用 RA-RNTI 和 TC-RNTI;正常业务用 C-RNTI;还有半静态调度和免调度用 CS-RNTI,语音这类业务的 MCS 专用 C-RNTI 等。剩下的 0001 到 FFEF 这个区间,是动态分配给各种用途的。搞清 RNTI 的归属很重要,因为在抓包分析的时候,第一步往往就是"这条 DCI 是哪类 RNTI 加扰的",它直接决定了 DCI 用什么格式、字段怎么解。
"听多久"这件事也得说清楚。UE 不是一直在听。什么时候听,由搜索空间配置里的monitoringSlotPeriodicityAndOffset(监控的时隙周期和偏移)、duration(连续监控多少个时隙)、monitoringSymbolsWithinSlot(14 bit 位图,指定时隙内哪些符号上开始听)三个参数共同决定。周期可以配成每 1、2、4、5、8、10、16、20、40、80、160、320、640、1280、2560 个时隙监控一次。周期越长越省电,但调度灵活性越差、时延越大。做物联网或省电特性的时候,这个参数是重点调优对象。
2. DCI 格式族谱:不同场景该拿哪张条子
2.1 上下行调度的主力:0_x 与 1_x 两大家族
DCI 格式多,但归类其实很简单。0_x 系列管上行(调度 PUSCH),1_x 系列管下行(调度 PDSCH),数字后面的横杠代表版本或复杂度档位。0_0 和 1_0 是回退格式,字段少、尺寸固定,用在初始接入、公共消息调度、以及 RRC 重配期间的过渡阶段。0_1 和 1_1 是功能完整的调度格式,支持多天线端口、多传输块、带宽部分切换、载波聚合指示等全套能力,是连接态业务的主力。Rel-16 又加了 0_2 和 1_2 两个"精简版",字段更少、尺寸更小,专门服务于低时延高可靠和对功耗敏感的终端——字段少意味着终端解析快、省电,代价是调度灵活性下降。
区分上下行还有个不起眼但很关键的位置:DCI 的第一个比特。对 0_x 和 1_1 这些格式来说,DCI 里有一个叫Identifier for DCI formats的 1 bit 字段,取 0 表示上行、取 1 表示下行。做解析脚本的时候,如果你的格式判断逻辑搞错了,这一个 bit 会让后面所有字段全错位,症状就是"解出来的东西看起来是乱数"。我踩过这个坑,当时排查了半小时才发现是格式判断分支写反了。
2.2 回退格式为什么必须一直保留
有人会问,既然 0_1/1_1 功能那么全,为什么还要留一套"功能残缺"的回退格式?这背后是可靠性兜底的设计哲学。设想一下:UE 和基站之间对于完整格式的字段配置产生了理解偏差——比如 RRC 重配正好在切换的临界点,或者某个可选字段网络侧以为配了、终端侧以为没配。这时候如果只有完整格式,双方就彻底失联了,只能靠重连。而回退格式的字段数量和含义是协议硬性规定的,不依赖任何 RRC 可选配置,双方一定能对齐。只要还能解出回退格式的 DCI,链路就有恢复的可能。
这个思路的实操价值在于:排查"UE 突然收不到调度"的问题时,先确认回退格式的调度是否正常。如果回退格式能调通、完整格式调不通,那基本可以锁定是 RRC 配置理解不一致的问题,方向就明确了。如果连回退格式都收不到,那问题在更底层——覆盖、CORESET 配置、加扰参数这些。这个二分法我用了很多次,非常省时间。
2.3 不带调度的 2_x/3_x:那些"管理类"指令
2_x 系列和 3_x 系列不是用来调度数据传输的,它们承载的是控制面信息。简单梳理一下:
| DCI 格式 | 承载内容 | 加扰 RNTI | 典型用途 |
|---|---|---|---|
| 2_0 | 时隙格式指示(SFI) | SFI-RNTI | 动态告诉 UE 本时隙哪些符号是上行、下行、灵活 |
| 2_1 | 下行抢占指示 | INT-RNTI | 高优先级业务抢占资源时通知低优先级 UE 别解了 |
| 2_2 | PUCCH/PUSCH 功控命令 | TPC-PUCCH-RNTI / TPC-PUSCH-RNTI | 组播式功控,一条 DCI 带多个 UE 的 TPC |
| 2_3 | SRS 功控命令 | TPC-SRS-RNTI | 同上,针对探测参考信号 |
| 2_4 | 上行取消指示 | CI-RNTI | Rel-16 引入,通知 UE 撤回已发或待发的上行传输 |
| 2_5 | 可用性指示 | AI-RNTI | Rel-17 引入,配合网络节能特性 |
| 2_6 | 节能指示 | PS-RNTI | Rel-16 引入,指示 UE 在下一个 DRX 周期是否需要监听 |
| 3_0 / 3_1 | 侧行链路调度 | SL-RNTI / SL-CS-RNTI | V2X 场景 |
这张表建议直接存一份在手边。做外场排查的时候,"UE 明明没被调度数据,为什么它的接收状态在变"这类现象,很多时候就是 2_0 的时隙格式指示或者 2_6 的节能指示在起作用,跟业务调度没关系。
2.4 拿 DCI format 1_1 做一次字段级拆解
理论说再多不如把字段列出来。以最常见的 DCI format 1_1 为例,它的字段构成大致是这样(具体位数随配置变化):
| 字段 | 位数 | 说明 |
|---|---|---|
| 格式标识 | 1 | 固定为 1,表示下行 |
| 载波指示 | 0 或 3 | 只有配置了跨载波调度时才有 |
| 带宽部分指示 | 0 / 1 / 2 | 指示切换到哪个下行 BWP |
| 频域资源分配 | 可变 | 决定用哪种资源分配类型,位数随 BWP 大小变化 |
| 时域资源分配 | 0 到 4 | 索引到 RRC 配置的时域资源分配表 |
| VRB 到 PRB 映射 | 0 或 1 | 交织还是非交织 |
| PRB 捆绑尺寸指示 | 0 或 1 | 影响信道估计的粒度 |
| 速率匹配指示 | 0 / 1 / 2 | 指示哪些 RE 要打孔 |
| ZP CSI-RS 触发 | 0 / 1 / 2 | 触发零功率参考信号 |
| MCS(TB1) | 5 | 调制编码等级 |
| NDI(TB1) | 1 | 新数据指示,用于判断是新传还是重传 |
| RV(TB1) | 2 | 冗余版本 |
| MCS/NDI/RV(TB2) | 8 | 双码字场景才有 |
| HARQ 进程号 | 4 | 指示用的是哪个 HARQ 进程 |
| 下行分配索引 | 0 / 1 / 2 / 4 | 主要是给载波聚合下的 HARQ 反馈定序 |
| PUCCH 功控命令 | 2 | |
| PUCCH 资源指示 | 3 | 决定 HARQ 反馈用哪个 PUCCH 资源 |
| PDSCH 到 HARQ 反馈定时 | 3 | 即 K1,决定几个时隙后反馈 |
| 天线端口 | 4 / 5 / 6 | DMRS 端口和 CDM 组 |
| 传输配置指示 | 0 到 3 | 即 TCI,波束指示 |
| SRS 请求 | 2 或 3 | 触发非周期 SRS |
| DMRS 序列初始化 | 1 | |
| 优先级指示 | 1 | Rel-16 引入,用于业务优先级区分 |
看这张表能明白一件事:DCI 里最"值钱"的三个字段是频域资源分配、时域资源分配、MCS。频域和时域资源分配决定了 UE 去哪个时频位置收数据,MCS 决定了用什么调制编码。这三个字段一旦译错,UE 就会到错误的位置用错误的速率去解调,结果必然是解不出来。而这三个字段里,频域资源分配的位数是动态的、最容易被搞错的,因为它取决于 BWP 的 PRB 数量。
顺便说个链路自适应上的差别:PDSCH 的 MCS 有 CSI 反馈做依据,网络侧知道信道质量;但PDCCH 的聚合等级没有直接的 CSI 反馈支撑,只能靠上行 RSRP/SINR 估计加上 HARQ 的统计做外环调整。这个差别是做 PDCCH 优化时最重要的一条认知——它意味着 PDCCH 的链路自适应天然比 PDSCH 迟钝,需要留更大的余量。
3. 把 PDCCH 的资源掰开算:CORESET、REG、CCE 与聚合等级
3.1 CORESET:先给控制信道划一块地
CORESET 的配置项不多,但每一项都直接影响容量和覆盖。频域用 45 bit 位图描述,每个 bit 对应 6 个 RB,从 BWP 的起始位置开始数。这个粒度限制意味着 CORESET 的频域资源数一定是 6 的倍数,最小时 6 个 RB。时域用duration描述,取值 1 到 3 个符号。Rel-15 里每个下行 BWP 最多配 3 个 CORESET、10 个搜索空间,这个数字在做密集调度和频谱效率优化的时候经常成为瓶颈。
CORESET 的时域长度怎么选?1 个符号的控制信道开销最小,但如果小区覆盖半径大、需要多个符号做更长的编码,3 个符号更合适。我一般的经验是:密集城区、小站、室内覆盖,1 到 2 个符号,把资源留给数据;广覆盖、高铁、远海这类场景,2 到 3 个符号,优先保控制信道的可靠解调。另外符号数还影响 REG 捆绑尺寸的取值——1 个和 2 个符号时可取 2 或 6,3 个符号时可取 3 或 6,这个后面细说。
还有两个容易被忽略的配置项:precoderGranularity决定 UE 在做信道估计时能不能假设整个 REG 捆绑内用同一套预编码(sameAsREG-bundle)还是所有连续 RB 都用同一套(allContiguousRBs)。选前者信道估计精度更高但资源调度灵活度受限,选后者相反。另一个是pdcch-DMRS-ScramblingID,也就是 PDCCH 的加扰 ID,不配就默认用小区 ID。这个参数的坑在于:如果网络侧配了、终端侧没收到,或者反过来,那 UE 解出来的就是一堆噪声,而且从日志上看不出任何异常,只会表现为"盲检全失败"。做参数核查时这是必查项。
3.2 从 REG 到 CCE:一个 CCE 到底能装多少比特
这部分的账一定要算清楚,不然没法评估 PDCCH 容量。
最小的资源单位是REG:1 个 PRB × 1 个 OFDM 符号,共 12 个 RE。这 12 个 RE 里有 3 个要拿去做解调参考信号,剩下 9 个 RE 才承载控制信息。PDCCH 固定用 QPSK 调制,每个 RE 装 2 个比特,所以1 个 REG 能装 18 个比特。
往上一层是CCE,1 个 CCE 等于 6 个 REG,也就是 108 个比特。再往上是聚合等级(Aggregation Level,AL),就是这条 PDCCH 用几个 CCE 来发。NR 支持 AL = 1、2、4、8、16 五档。这么一算,各档的编码后比特数就出来了:
| 聚合等级 AL | CCE 数 | REG 数 | 数据 RE 数 | 编码后比特数 E | 极化码 N |
|---|---|---|---|---|---|
| 1 | 1 | 6 | 54 | 108 | 128 |
| 2 | 2 | 12 | 108 | 216 | 256 |
| 4 | 4 | 24 | 216 | 432 | 512 |
| 8 | 8 | 48 | 432 | 864 | 512 |
| 16 | 16 | 96 | 864 | 1728 | 512 |
这张表是整个 PDCCH 容量分析的基石。比如你想知道"一个 2 符号、20 个 RB 的 CORESET 里能塞多少条 AL4 的 PDCCH",那就先算这个 CORESET 总共有几个 CCE:频域 20 个 PRB 按 6 的倍数向下取整是 18 个 PRB,也就是 3 个 REG 束位(每个 6 RB),时域 2 个符号,所以总 REG 数是 3 × 2 = 6 个前后……不对,这里要按 REG 算:18 个 PRB × 2 个符号 = 36 个 REG,除以 6 得 6 个 CCE。那么理论上最多能放 1 条 AL4 加 1 条 AL2,或者 6 条 AL1。这就是这个 CORESET 的容量上限。
3.3 聚合等级的选型逻辑与三个约束
聚合等级本质上是"用资源换可靠性"。AL 越大,编码后的冗余越多,抗噪能力越强,但占用的 CCE 也越多,能同时服务的 UE 越少。选型要同时满足三个约束。
第一个约束是覆盖。信道质量差的时候必须抬 AL。工程上的粗略估计是:AL1 适合 SINR 较好的近点,AL2 到 AL4 适合中点,AL8 和 AL16 给小区边缘。但这不是绝对标准,还要看 CORESET 的时域符号数和 REG 捆绑尺寸,因为频率分集的效果不一样。
第二个约束是容量。CORESET 里的 CCE 总数有限,如果所有 UE 都抬到 AL8,一个 CORESET 只能服务一两条 PDCCH,调度器立刻被憋死。所以实际网络里都有一个"聚合等级分布"的目标:比如近点 50% 在 AL1/2,中点 35% 在 AL4,边缘 15% 在 AL8/16。这个分布偏离太多,通常意味着链路自适应参数有问题。
第三个约束是候选位置数。搜索空间里每个 AL 配几个候选(nrofCandidates),如果某个 AL 只配了 1 个候选,那这个 AL 在同一时隙内只能发一条 PDCCH,多用户冲突时调度器会排队。我见过一个现场的配置,AL8 只配了 1 个候选,结果小区边缘用户一多就开始出现"调度时延抖动",把 AL8 候选加到 2 个就缓解了。这种问题看 KPI 是看不出来的,得看配置。
3.4 CCE-to-REG 映射:交织映射带来的频率分集
CCE 和 REG 之间怎么对应,有两种方式。非交织映射就是把 REG 束按顺序编号,CCE 依次占用相邻的 REG 束,映射关系接近恒等映射。这种方式的优点是实现简单、基站侧调度自由度高,缺点是同一条 PDCCH 占的 RB 都挤在一起,如果这段频率正好遇到衰落,整条 PDCCH 就废了。交织映射则是通过一个交织矩阵(交织器尺寸 R 取 2、3 或 6,再加一个 shiftIndex 偏移),把 REG 束打散到整个 CORESET 频域上,让一条 PDCCH 跨越较宽的带宽,从而获得频率分集增益。
选择依据很直接:覆盖受限的场景用交织映射,容量受限的场景用非交织映射。为什么?因为交织映射要求 UE 在整个 CORESET 带宽上都保持较好的信道估计质量,终端需要更宽带的参考信号处理;而且在某些情况下,交织映射会限制基站把 REG 束分给不同 PDCCH 的灵活性。反过来,在小区边缘,频率分集带来的增益比灵活性更重要。
有一个实操细节值得记一笔:非交织映射时,REG 束的编号是"束内连续",CCE 之间的对应关系相对直接;而交织映射涉及一个二维矩阵的行列读写过程,做链路级仿真的时候这一步很容易写错。我自己写仿真代码时,是先拿协议里的示例数值手工验算一遍矩阵结果,确认无误再往上搭,这个习惯省了很多返工时间。
4. 搜索空间与盲检:UE 不知道位置,怎么找到自己的 DCI
4.1 哈希函数把候选位置定下来
UE 不知道自己的 DCI 在 CORESET 的哪几个 CCE 上,它只能"试"。试的位置不能随便定,否则基站就不知道往哪儿发。协议给出的解法是用一个哈希函数把候选位置算出来,基站和终端各自算一遍,算出来的结果必须一致。
对于公共搜索空间(CSS),这个哈希值恒为 0,也就是说 CSS 的候选位置是固定的,每个时隙都一样——这正是 SIB1、寻呼、随机接入这些公共消息能被可靠接收的基础。对于终端专属搜索空间(USS),哈希值随每个时隙变化:
Y(p, n) = (A_p × Y(p, n-1)) mod D 其中: Y(p, -1) = n_RNTI D = 65537 A_p = 39827 当 (p mod 3) = 0 = 39829 当 (p mod 3) = 1 = 39839 当 (p mod 3) = 2 p 是 CORESET 的索引,n 是帧内的时隙号算出 Y 之后,第 m 个候选占用的 CCE 编号是:
L × { (Y + floor(m × N_CCE / (L × M)) + n_CI) mod floor(N_CCE / L) } + i 其中 L 是聚合等级,M 是该聚合等级下的候选数, N_CCE 是 CORESET 里的 CCE 总数,n_CI 是载波指示,i 取 0 到 L-1这套机制的作用是随机化:如果两个 UE 的候选位置总是撞在一起,会持续冲突;随时隙变化的哈希让冲突在时间上被打散。理解了这一点,就能解释一个现象——为什么同一个小区的两个 UE,一个偶尔调度时延大、另一个很稳定,很有可能就是 RNTI 的哈希值让其中一个总是撞在同一个候选上。
4.2 盲检次数的上限是怎么算出来的
盲检是终端的计算负担大头,所以要设上限。协议按子载波间隔规定了每时隙、每服务小区最多监控的 PDCCH 候选数和 CCE 数:
| 子载波间隔 | μ | 最大候选数/时隙 | 最大非重叠 CCE 数/时隙 |
|---|---|---|---|
| 15 kHz | 0 | 44 | 56 |
| 30 kHz | 1 | 36 | 56 |
| 60 kHz | 2 | 22 | 48 |
| 120 kHz | 3 | 20 | 32 |
这张表有两层约束:候选数是一层,CCE 数(且要求非重叠)是另一层。为什么还要限制 CCE 数?因为终端做盲检时,解码的主要成本是极化码译码和 CRC 校验,而一条 AL16 的候选消耗的计算量和资源量是 AL1 的 16 倍。只限制候选数不够,还得限制总资源量。
实际配置时,把所有搜索空间的候选数加起来不应该超过表里的值。举个常见配置:USS 里 AL1 配 4 个、AL2 配 4 个、AL4 配 2 个、AL8 配 1 个,合计 11 个候选,占用 CCE 数 4×1 + 4×2 + 2×4 + 1×8 = 28 个。距离 44 和 56 还有余量,属于比较健康的配置。我见过有现场把 AL1 配到 6 个、AL2 配到 6 个,加起来虽然没超,但因为频繁的 AL1 调度让终端的活跃度上去了,功耗测试数据不好看——盲检次数和终端功耗是直接相关的。
4.3 搜索空间超配时的丢弃优先级
如果配置的候选数或 CCE 数超了上限怎么办?总不能要求终端"超能力工作"。协议的做法是:按优先级丢弃低优先级的搜索空间,终端只监控排在前面的那些。优先级的大致顺序是:
- 索引为 0 的公共搜索空间(承载 SIB1 调度、寻呼、随机接入这些最不能丢的消息)
- 索引不为 0 的公共搜索空间,对应 SI-RNTI、RA-RNTI、TC-RNTI、P-RNTI 这类
- 索引不为 0 的公共搜索空间,对应 C-RNTI、CS-RNTI 等
- 终端专属搜索空间
具体到每个级别内部的排序规则和丢弃算法,还是要对着 38.213 第 10.1 节逐条核对,因为里面还涉及 CSS 和 USS 之间的 CCE 重叠判定等细节。
这个优先级机制给我们的启示是:不要把关键业务的调度全压在 USS 上。如果一个现场配置里 USS 塞得满满当当,同时又要保证时延敏感业务的可靠性,那可以在 Type3 公共搜索空间里也配置一些 C-RNTI 的监控机会做备份。代价是多占一点控制信道资源,收益是关键业务在超配丢弃时依然有调度通道。
4.4 DCI size 对齐:3 种和 4 种两条红线
这是 PDCCH 里最"烧脑"的一块,但也是最容易出低级错误的地方。问题是这样的:终端不知道收到的 DCI 是什么格式,只能按自己配置的每种可能的尺寸去试。如果网络侧配置了很多不同的 DCI 尺寸,终端的盲检组合数会爆炸。所以协议设了两条红线:用 C-RNTI 加扰的终端专属搜索空间里,不同的 DCI 尺寸不超过 3 种;所有 DCI 尺寸合计不超过 4 种。
超过红线怎么办?靠尺寸对齐把数量压下来。基本手段有两条。
第一条是回退格式强制对齐:DCI format 0_0 和 1_0 的尺寸必须相同,小的一方补零。这样一对上下行回退格式只算一种尺寸。
第二条是完整格式之间对齐:如果 USS 里同时配了 0_1 和 1_1,它们的尺寸要凑成一样,同样是小的补零。而且回退格式的大小不能超过完整格式的大小(超了就截断)。此外,回退格式的尺寸还被限制在一组固定的档位上(大约 12、14、16、20、24、26、32、40、44、56 比特这几个级别),如果实际算出来的尺寸不落在这些档位上,就补零凑到最近的档位。这一条我建议你对着手上的 38.212 第 7.3.1.0 节再确认一遍具体表格,不同版本在细节上可能有补充说明。
这些规则带来一个很实际的后果:DCI 里会存在一些"填充比特"或"预留比特",它们不承载任何信息。做 DCI 解析脚本的时候必须把这些位算进去,否则字段偏移会错。我见过一个团队的解析工具,就是因为漏掉了 0_0 对齐补的零,导致在某个配置下解出来的 MCS 总是比实际值小 4,排查了两天才定位到。
5. 端到端走一遍:从 RRC 配置到空口上解出 DCI
5.1 发送链路全景:CRC、极化码、加扰、QPSK、REG 映射
一条 DCI 从比特到空口,要过这么几道工序:
- 尺寸对齐与补零:按前面说的规则,把 DCI 载荷补齐到最终尺寸 A。
- CRC 附着:计算 24 bit 的 CRC 并附在载荷后面,同时 CRC 要跟 RNTI 绑定——UE 侧用自己手里的 RNTI 去解这个 CRC,能对上就说明这条 DCI 是给自己的。这个设计同时完成了检错和寻址两件事,非常经济。
- 极化编码:NR 的 PDCCH 用极化码(Polar Code)。编码前先根据码长和码率确定母码长度 N(2 的幂次,最大 512),做信道极化、子信道映射、以及基于可靠度排序的信息位选择,最后按 E 的长度做速率匹配(子块交织加上循环缓冲区的比特选择)。
- 加扰:对编码后的 E 个比特做伪随机加扰,加扰序列由 RNTI 和加扰 ID 共同决定。这一步的作用是让不同 UE 的 PDCCH 在统计上看起来像随机序列,降低相互干扰。
- QPSK 调制:固定 QPSK,不做高阶调制。为什么?因为控制信道要保可靠,用低阶调制换取解调余量是划算的。
- 层映射与预编码:PDCCH 只用一个天线端口,端口号固定为 2000,多天线场景下靠波束赋形而不是空间复用来提升性能。
- REG 映射:按 CCE-to-REG 映射规则,把调制符号填到 CORESET 的 REG 上,同时避开 DMRS 占用的那 3 个 RE。
把这七步走通一遍,你对 PDCCH 的理解就会有质的提升。做链路仿真的人最容易被卡住的是第三步的速率匹配和第七步的映射,因为这两步涉及比较多的索引运算。
5.2 用脚本把每个聚合等级的 E 和 N 算清楚
手工算上面那张表容易出错,写个脚本一劳永逸:
import math def pdcch_coded_bits(al): """返回某聚合等级下 PDCCH 编码后的比特数 E""" regs = 6 * al # 1 个 CCE = 6 个 REG data_re = regs * 9 # 每个 REG 12 个 RE,其中 3 个给 DMRS return data_re * 2 # QPSK,每 RE 2 bit def polar_mother_n(E, n_min=5, n_max=9): """根据 E 反推极化码母码长度 N = 2^n""" n1 = math.ceil(math.log2(E)) if E <= (9 / 8) * 2 ** (n1 - 1): n1 -= 1 n1 = max(n_min, min(n1, n_max)) return 1 << n1 for al in (1, 2, 4, 8, 16): E = pdcch_coded_bits(al) N = polar_mother_n(E) print(f"AL={al:2d} E={E:5d} bit N={N:4d} 有效码率 K/N 参考值")跑出来的结果跟前面那张表一致。有了这个脚本,你在评估"某个 AL 能不能装下某条特定尺寸的 DCI"时就有底了:DCI 载荷加上 24 bit CRC 得到 K,K 除以 N 就是实际码率。码率超过 0.8 左右的时候,译码性能会明显下降,这时候要么抬聚合等级,要么精简 DCI 字段。这个经验阈值在链路预算的时候很好用。
顺便提醒一个细节:当 DCI 载荷尺寸很小(比如小于等于 11 比特)的时候,协议里还有额外的重复处理,目的是保证极化码有足够的输入长度、维持分集效果。这一段的具体处理规则建议直接翻 38.212 的第 7.3.2 到 7.3.3 小节,不同格式的处理顺序不完全一样。
5.3 加扰序列与 DMRS 序列的生成细节
这两个序列的初值公式经常被搞混,我把它们写在一起对比:
PDCCH 数据加扰序列初值: c_init = (n_RNTI × 2^16 + n_ID) mod 2^31 其中 n_ID 优先取 pdcch-DMRS-ScramblingID,未配置时取物理小区 ID PDCCH 解调参考信号序列初值: c_init = (2^17 × (N_slot_symbol + l + 1) × (2 × n_ID + 1) + 2 × n_ID) mod 2^31 其中 N_slot_symbol 是时隙内的符号数(常规 CP 下为 14), l 是当前符号在时隙内的编号注意数据加扰的初值里带了 RNTI,而 DMRS 的初值里没带——这是一个很关键的差别。它意味着:即使两个 UE 的 RNTI 不同,只要它们在同一个 CORESET 的同一个符号上,DMRS 序列是一样的。同一时隙内不同符号的 DMRS 不同,靠的是l这个变量。所以在做干扰分析的时候,如果发现两个小区的 DMRS 序列总是相同,那就要检查它们的加扰 ID 是不是都用了同一个小区 ID——邻近小区配相同的加扰 ID 会造成 DMRS 之间的持续碰撞,信道估计精度会掉。
5.4 一份可以直接抄的 CORESET / SearchSpace 配置
下面是一份 FR1、30 kHz 子载波间隔下的参考配置,一个 2 符号、60 个 RB 的 CORESET,加一个每时隙监控的终端专属搜索空间:
controlResourceSetToAddModList { controlResourceSetId 2, -- 45 bit 位图,每 bit 对应 6 个 RB,前 10 个 bit 置 1 表示占 60 个 RB frequencyDomainResources '111111111100000000000000000000000000000000000', duration 2, -- 2 个 OFDM 符号 cce-REG-MappingType interleaved { interleaved { reg-BundleSize n6, -- REG 捆绑尺寸 6 interleaverSize n2, -- 交织器尺寸 R = 2 shiftIndex 0 } }, precoderGranularity sameAsREG-bundle, pdcch-DMRS-ScramblingID 511, tci-StatesPDCCH-ToAddList { 3 } } searchSpacesToAddModList { searchSpaceId 3, controlResourceSetId 2, monitoringSlotPeriodicityAndOffset { sl1: NULL }, -- 每个时隙都监控 duration 1, monitoringSymbolsWithinSlot '10000000000000', -- 时隙内第 0 个符号 nrofCandidates { aggregationLevel1 n4, aggregationLevel2 n4, aggregationLevel4 n2, aggregationLevel8 n1, aggregationLevel16 n0 }, searchSpaceType ue-Specific { dci-Formats formats0-1-And-1-1 } }这份配置里的候选数合计 11 个,占用 CCE 数为 4×1 + 4×2 + 2×4 + 1×8 = 28,都在上限之内。CORESET 的容量算一下:60 个 RB 除以 6 得 10 个频域 REG 束位,乘以 2 个符号得到 20 个 CCE。这个容量支撑 11 个候选、28 个 CCE 的配置是够的,但如果同一时隙要调度的用户数再多,就得考虑扩容或者把部分用户挪到另一个 CORESET。
几个配置上的心得。第一,monitoringSymbolsWithinSlot选第 0 个符号是最省时的做法,控制信道越早发,UE 越早开始解 PDSCH,但符号 0 也可能被用于同步信号等场景,要看具体帧结构。第二,reg-BundleSize选 6 时 REG 束跨 2 个符号、共 6 个 REG,这种"时频二维捆绑"能同时获得时间和频率分集,但要求信道在 2 个符号内基本不变,高速移动场景要谨慎。第三,shiftIndex在同一小区的不同 CORESET 之间最好不要一样,能进一步降低小区内碰撞概率。
5.5 三组对比实测:聚合等级与 PDCCH 开销的变化
下面这组数字是我在一次室内衰减可控的对比测试里记下来的,测试方式是把终端放在不同衰减档位上,跑满缓冲业务,统计 PDCCH 的聚合等级分布和控制信道开销占比。
| 测试档位 | 主要聚合等级分布 | PDCCH 控制开销占比 | 平均盲检次数/时隙 | 观察到的现象 |
|---|---|---|---|---|
| 近点(低衰减) | AL1 约 55%、AL2 约 35% | 8% 到 12% | 4 到 6 | 调度连续,几乎无丢包 |
| 中点(中等衰减) | AL2 约 30%、AL4 约 50%、AL8 约 20% | 15% 到 20% | 6 到 9 | 偶尔出现调度时延抖动 |
| 远点(高衰减) | AL8 约 40%、AL16 约 45% | 22% 到 30% | 8 到 12 | 控制信道成为覆盖瓶颈 |
绝对值一定跟设备、带宽、帧结构、业务模型有关,但趋势是普遍成立的:聚合等级每抬一档,控制信道占用的资源大致翻倍。这就解释了一个常见的容量问题——小区边缘用户比例一高,PDCCH 的开销会非线性上升,因为边缘用户既要用高聚合等级,又因为速率低而占用更多的调度机会。这时候光调 PDSCH 的 MCS 是没用的,得从控制信道入手:增加 CORESET 的符号数、扩容 CCE 数、或者调整聚合等级分布的目标。
还有一个细节值得分享:远点场景下 AL16 用到 45% 这个比例偏高,我后来把外环调整的目标 BLER 从 1% 放宽到 2% 到 3%,AL16 的占比明显下降,整体吞吐反而有改善。原因是 PDCCH 虽然有保护,但过高的聚合等级吃掉了本可以给数据用的资源。PDCCH 的目标 BLER 不一定要死守 1%,具体取值要看业务对控制面可靠性的需求,这个后面还会再说。
6. 排查实录:PDCCH/DCI 出问题时从哪儿下手
6.1 UE 一直检不到 DCI 的五个方向
这是最常见也最让人头大的问题。终端侧表现为"随机接入过不去"或者"连上了但一直没数据"。我一般按下面的顺序倒推,从最可能到最不可能:
第一,加扰 ID 是否一致。网络侧配了pdcch-DMRS-ScramblingID,终端侧如果没拿到或者拿了默认值,解出来就是纯噪声。这个问题的特点是"完全没有规律"——不是概率性失败,而是百分之百失败。排查方法是对比两侧的配置,或者临时把这个参数去掉,让双方都用小区 ID 试试。
第二,CORESET 的频域位图是否一致。45 bit 位图,一个 bit 错了,UE 就会在错误的 RB 上找控制信道。这个问题在跨厂家对接的时候特别容易出,因为位图的组织方式(从 BWP 起始还是从载波起始,位序方向)在不同实现里可能有细微差别。
第三,搜索空间配置是否对得上。包括监控周期、时隙内监控符号的位置、每个聚合等级的候选数。如果这些不一致,UE 就会在错误的时间点去看错误的候选位置,表现也是完全收不到。
第四,RNTI 是否匹配。尤其是 CS-RNTI、MCS-C-RNTI 这类需要专门分配的 RNTI,如果分配过程出了问题,UE 手上没有正确的值,CRC 永远校验不过。
第五,覆盖是否真的不够。前面四项都排除了,才会怀疑覆盖。这时候看的是 RSRP/SINR 和配置的聚合等级是否匹配——有时候是调度器给的聚合等级太低,UE 在边缘用 AL1 硬扛,失败率自然高。
这个顺序的价值在于它是"从确定性问题到概率性问题"的排列。前四项错了一个就是百分百失败,跟概率无关;第五项才是概率性的。先把确定性问题排干净,效率最高。
6.2 DCI 检到了但字段全乱:尺寸与字段顺序的坑
比"检不到"更隐蔽的问题是"DCI 解出来了,CRC 也过了,但字段值是错的"。CRC 能过说明前面的加扰、解调、译码、RNTI 都是对的,问题一定在比特到字段的映射上。可能的原因有三个。
第一个是尺寸对齐的补零位置搞错。补零是在 DCI 载荷的末尾补,不是在开头,也不是插在中间。有些实现习惯性地在开头补零,结果整个字段序列偏移。
第二个是可选字段的存在性判断错。DCI format 1_1 里有一堆"0 或 1 或 2 比特"的可变字段,它们的实际位数取决于 RRC 配置(比如是否配了载波聚合、带宽部分指示的比特数取决于配了几个 BWP、速率匹配指示的比特数取决于配了几个速率匹配图案)。如果解析脚本硬编码了位数,换个配置就崩。这就是为什么解析器一定要跟配置绑定。
第三个是字段的排列顺序记错。协议里每个格式的字段顺序是固定的,而且不同格式之间差距不小。比如上行格式里有 SRS 资源指示、预编码信息这些下行没有的字段;下行格式里有 PUCCH 功控、HARQ 反馈定时这些上行没有的。建议的做法是:先写一张"格式到字段列表"的配置表,解析时按表逐字段推进偏移量,而不是把偏移量写死在代码里。
# 以 format 1_0 为例,把字段定义成表,解析时逐项推进 FIELDS_1_0 = [ ("format_flag", 1), ("fdra", None), # None 表示位数需要按 BWP 大小算 ("tdra", 4), ("vrb_to_prb", 1), ("mcs", 5), ("ndi", 1), ("rv", 2), ("harq_id", 4), ("dai", 2), ("tpc_pucch", 2), ("pucch_res_ind", 3), ("k1", 3), ] def fdra_bits(n_rb): import math return math.ceil(math.log2(n_rb * (n_rb + 1) / 2)) def parse_dci(bits, fields, n_rb=None): pos, out = 0, {} for name, width in fields: if width is None: # 动态位数的字段 width = fdra_bits(n_rb) out[name] = int(bits[pos:pos + width], 2) pos += width return out, pos这段代码的核心思路是"字段定义和解析逻辑分离"。换个 DCI 格式,改一下字段表就行,不用动解析函数。字段表里的None表示需要运行时计算的动态位数,这个设计能避免硬编码带来的隐患。
6.3 覆盖优先还是容量优先:聚合等级的现场取舍
这个问题没有标准答案,但有几个判断依据。
看业务类型。eMBB 业务对时延不敏感,控制信道可以省着点用,让聚合等级分布尽量往低调;URLLC 业务要求一次成功,宁可多占资源也要保证可靠性,聚合等级往上抬是合理选择。
看小区边缘用户比例。如果边缘用户占比高,PDCCH 开销会急剧上升,这时候单纯抬聚合等级会形成恶性循环——边缘用户占用大量 CCE,导致控制信道拥塞,调度时延上升,边缘用户感知更差。破解办法是扩容 CORESET(增加符号数或频域 RB 数),先把池子做大。
看终端的盲检能力。增加候选数能提高调度灵活度,但也增加终端计算量和功耗。测试机和商用机的盲检能力差别很大,配置的时候要按商用机的规格来。
我自己的经验参数是:PDCCH 的控制开销占比控制在 15% 以内比较健康,超过 25% 就要警惕了。这个值可以在基站侧统计,也可以在路测仪上直接读出来。另外我通常会盯着"AL16 占比"这一个指标,它超过 20% 基本就说明控制信道的覆盖或容量已经出问题了。
还有前面提到的那条:目标 BLER 不一定非要 1%。PDCCH 是一次性的,但业务本身有 HARQ 兜底,偶尔漏检一条 DCI 无非是这次调度浪费了,下个时隙重新调就是。把目标 BLER 从 1% 放宽到 2% 到 3%,可以让聚合等级分布整体往下走一档,省下来的资源给数据信道,整体吞吐通常是涨的。这个调整需要观察端到端的时延和吞吐指标变化,不能只看 PDCCH 自己的成功率。
6.4 PDCCH 问题速查表
把前面这些整理成一张速查表,出问题的时候按顺序过一遍:
| 现象 | 可能性最高的原因 | 首选排查动作 |
|---|---|---|
| 完全收不到任何 DCI | 加扰 ID 不一致 | 对比两侧 pdcch-DMRS-ScramblingID,临时置空重试 |
| 完全收不到任何 DCI | CORESET 频域位图错 | 逐 bit 核对 45 bit 位图与 RB 对应关系 |
| 收不到专属调度、但公共消息正常 | USS 配置错 | 检查搜索空间 ID、监控周期、DCI 格式组合 |
| DCI CRC 过不了 | RNTI 不匹配 | 核对 RNTI 分配流程和取值 |
| DCI 能解但字段值全错 | 尺寸对齐补零位置错 | 检查补零是否在载荷末尾、预留位是否计入偏移 |
| DCI 能解但个别字段错 | 可变位数字段硬编码 | 把位数改成按 RRC 配置动态计算 |
| 边缘用户调度时延抖动 | 高聚合等级候选数不足 | 检查 AL8/AL16 的 nrofCandidates 是否只有 1 |
| 整体吞吐上不去、控制开销高 | 聚合等级分布偏高 | 统计 AL 分布,复核目标 BLER 设置 |
| 终端功耗测试不达标 | 盲检次数过多 | 核算候选总数与 CCE 总数,精简 USS 配置 |
| 同小区两用户表现差异大 | 哈希碰撞 | 检查两用户 RNTI 是否导致候选位置长期重叠 |
这张表用起来有个技巧:先把"完全收不到"和"能收到但不对"分开。前者的问题百分百在物理层和配置层,后者的问题基本在解析层和尺寸规则层。分成两大类之后,排查范围直接砍一半。
前面几节里我埋了不少"以你手上的协议版本为准"的提醒,不是客套话。NR 的协议从 Rel-15 到 Rel-17 一直在演进,DCI 格式有新增(2_4、2_5、3_0、3_1),盲检能力有增强(Rel-16 按不同的监控能力分档),搜索空间的配置约束也在变。我自己做方案的时候有个固定习惯:凡是涉及位数和取值的结论,都要在当前的协议版本里再确认一遍,尤其是跨版本对接的场景。曾经有一次就是因为一方按 Rel-16 的盲检能力算容量、另一方按 Rel-15 的规矩配,对接时才发现两边的候选数上限对不上,白折腾了一轮。做 PDCCH 这一块,资源账算得细不细,最后都会反映在调度器的自由度和终端的实测指标上,这个功夫省不得。