news 2026/9/5 12:50:23

CPM通信仿真:MATLAB实现连续相位调制原理与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPM通信仿真:MATLAB实现连续相位调制原理与工程实践

简介:本资源是一套面向通信工程专业学生及数字信号处理初学者的CPM调制MATLAB实践代码包,聚焦连续相位调制原理实现与可视化分析,解决理论学习后缺乏可运行实例、相位连续性建模困难、调制参数调试无依据等常见问题。压缩包共7个文件,全部为.m脚本,涵盖CPM系统初始化(cpm_setup)、相位轨迹生成(cpm_generate)、调制信号分析(cpm_analysis)、关键参数计算(cpm_bit2dec、cpm_qk)、主流程调度(cpm_main)及结果可视化(cpm_display),结构清晰、模块解耦,便于分步调试与原理验证。目前已有293人学习下载,读者可直接运行获得MSK/GMSK等典型CPM信号的时域波形、相位路径、瞬时频率谱及星座图,掌握调制指数、符号速率对频谱效率与抗噪性能的影响机制,并复现教材中4.3.3章节的核心仿真案例。

1. 项目概述:从“CPM.rar”压缩包到一套完整的通信仿真实践

看到“CPM.rar_CPM调制_MATLAB CPM调制_cpm matlab_cpm通信_连续相位调制”这一串文件名和关键词,我仿佛看到了当年自己刚接触通信仿真时,在论坛和资源站四处搜罗代码包的样子。这个标题组合,典型地指向了一个通信工程或信号处理学习者最核心的实践需求:获取一套能直接运行、可修改、用于学习和研究连续相位调制(CPM)技术的MATLAB仿真代码。那个“CPM.rar”压缩包,就是无数前辈分享的“遗产”,里面可能包含了CPM的调制、解调、误码率分析等核心脚本。

连续相位调制(CPM)可不是什么冷门技术,它是现代数字通信中一类非常重要的恒包络调制方式。简单来说,它发送的信号幅度是恒定的,但相位会平滑、连续地变化来携带信息。这个特性让它对功率放大器的非线性不那么敏感,非常适合用在卫星通信、深空探测、以及一些对功耗和线性度要求苛刻的移动通信场景里。你可能会问,现在不是QPSK、16QAM满天飞吗?没错,但CPM在特定领域的优势依然不可替代,比如其出色的功率效率和频谱效率,以及通过引入关联长度(L)带来的编码增益,使其在低信噪比下表现优异。

这套MATLAB代码的价值,就在于它把教科书上复杂的数学公式和理论框图,变成了屏幕上可以直观观察的时域波形、眼图、频谱和误码率曲线。对于学习者,你可以通过修改调制指数、脉冲形状、关联长度等参数,亲眼看到系统性能如何变化;对于研究者,这是一个可靠的基线(baseline),你可以在其上叠加信道编码、同步算法或更复杂的检测器(如序列检测)。接下来,我就以一名通信系统仿真“老手”的视角,带你彻底拆解这个“CPM.rar”项目,不仅告诉你代码怎么跑,更要说清楚背后每一个参数、每一步操作的“所以然”,并分享那些只有踩过坑才知道的调试技巧。

2. CPM调制原理深度拆解:不止是恒包络

在打开MATLAB脚本之前,我们必须先夯实地基。CPM的核心思想,是让已调信号的相位轨迹 $\phi(t; \mathbf{a})$ 随时间连续变化,这个相位是所有过去和当前输入符号 $\mathbf{a}$ 的函数。

2.1 相位轨迹的数学本质

一个CPM信号的复包络可以表示为: $s(t) = \sqrt{\frac{2E_s}{T}} \exp\left{ j\phi(t; \mathbf{a}) \right}$ 其中,$E_s$是符号能量,$T$是符号周期。而相位轨迹是关键: $\phi(t; \mathbf{a}) = 2\pi h \sum_{i=-\infty}^{n} a_i q(t - iT)$, 当 $nT \leq t < (n+1)T$。

这里有几个决定CPM“性格”的核心参数:

  1. 调制指数 $h$: 它决定了每个符号引起的最大相位变化。$h$ 通常被设计为一个有理数,比如 $h = k/p$。这是为了确保相位网格(Trellis)具有周期性,便于使用维特比算法(VA)进行最大似然序列检测(MLSD)。$h$ 的选择直接影响带宽和误码性能。较小的 $h$ 频谱更集中,但抗噪声能力可能稍弱。
  2. 输入符号 $a_i$: 来自M进制的符号集,例如 ${\pm1, \pm3, ..., \pm(M-1)}$。对于最简单的二进制CPM(也叫2CPM),$a_i \in {\pm1}$。
  3. 相位响应函数 $q(t)$: 这是CPM的“灵魂”。它决定了相位变化的“形状”。$q(t)$ 是某个频率脉冲 $g(t)$ 的积分:$q(t) = \int_{-\infty}^{t} g(\tau) d\tau$。并且约定当 $t \geq LT$ 时,$q(t) = 1/2$。这里的 $L$ 就是关联长度

2.2 关联长度与脉冲形状:CPM家族的族谱

关联长度 $L$ 是CPM区别于其他调制(如PSK、FSK)的精髓所在。当 $L=1$ 时,当前符号的相位只受当前符号影响,这就是全响应CPM,例如MSK(最小频移键控,其 $g(t)$ 是矩形脉冲)。当 $L > 1$ 时,当前符号的相位变化会延续影响到后续 $L-1$ 个符号周期,这就是部分响应CPM。部分响应引入了符号间的“可控”干扰(或记忆性),但这恰恰可以被接收端的序列检测器利用,转化为编码增益。

频率脉冲 $g(t)$ 的形状定义了CPM的具体子类:

  • 矩形脉冲 (REC): $g(t) = 1/(2LT)$ for $0 \leq t \leq LT$。当 $L=1$ 时,就是著名的MSK。
  • 升余弦脉冲 (RC): $g(t) = [1 - \cos(2\pi t / LT)] / (2LT)$ for $0 \leq t \leq LT$。它产生的相位轨迹比REC更平滑,频谱旁瓣衰减更快。
  • 高斯脉冲 (GMSK): 将矩形脉冲通过高斯滤波器得到。这是最著名的CPM商用案例,蓝牙(Bluetooth)使用的就是 $BT=0.5$ 的GMSK,其中B是高斯滤波器的3-dB带宽,T是符号周期。GMSK的频谱效率极高。

注意:在仿真中,脉冲形状的选择会直接影响你代码中“频率脉冲生成”函数的设计。一个健壮的仿真包应该能灵活配置这些参数。

2.3 CPM的信号空间与网格图

由于相位是连续的,CPM的信号空间理论上是无限维的。但得益于其有限的关联长度 $L$ 和有理数 $h$,我们可以用有限状态机来描述它。系统的状态可以定义为: $\mathbf{\sigma}n = (\theta_n, a{n-1}, a_{n-2}, ..., a_{n-L+1})$ 其中 $\theta_n$ 是 $nT$ 时刻的累积相位(对 $2\pi$ 取模后的值),后面 $L-1$ 个是过去的符号。这个状态集合是有限的,因此我们可以画出CPM的网格图(Trellis)。网格图是理解CPM记忆性和实现维特比解调的关键。在仿真中,我们常常需要先根据参数 $(M, h, L)$ 计算出状态总数,这是构建解调器的基础。

3. MATLAB仿真框架设计与核心模块解析

拿到“CPM.rar”后,别急着运行main.m。先花十分钟浏览一下文件结构,一个好的仿真项目通常模块清晰。一个典型的CPM仿真框架包含以下核心模块:

3.1 仿真框架目录结构猜想与解读

虽然我没看到你的具体压缩包,但一个规范的CPM仿真项目很可能包含如下文件:

CPM_Simulation/ ├── main.m % 主脚本,设置参数,调用流程 ├── cpm_modulator.m % CPM调制器核心函数 ├── cpm_demodulator.m % CPM解调器(相干/非相干) ├── generate_freq_pulse.m % 生成频率脉冲g(t) ├── awgn_channel.m % 添加高斯白噪声 ├── ber_counter.m % 计算误码率(BER) ├── plot_results.m % 绘制波形、频谱、眼图、BER曲线 └── utils/ % 工具函数目录 ├── symbol_mapper.m ├── differential_encoder.m (如果需要) └── trellis_generator.m

实操心得: 首先打开main.m,看它开头如何定义系统参数。这就像工程的“总图纸”。通常你会看到类似这样的变量定义:

M = 2; % 调制阶数,2表示二进制 h = 0.5; % 调制指数 L = 3; % 关联长度 pulse_type = 'RC'; % 脉冲类型:'REC', 'RC', 'GMSK' BT = 0.3; % 仅对GMSK有效,高斯滤波器带宽时间积 sps = 8; % 每符号采样点数 (Samples Per Symbol) EbN0_dB = 0:2:10; % 仿真的信噪比范围 num_symbols = 10000; % 仿真的符号数

这些参数是你的“实验旋钮”,后续所有分析都围绕它们展开。

3.2 调制器实现:从比特流到连续相位信号

cpm_modulator.m是这个项目的引擎。其内部逻辑通常遵循以下步骤,理解每一步的意图至关重要:

  1. 比特到符号映射: 将输入的随机比特流(例如bits = randi([0,1], 1, num_bits))按M进制分组并映射为符号 $a_n$。对于2CPM,就是简单的0->-1, 1->+1。

  2. 频率脉冲生成与叠加: 这是CPM调制的核心计算。

    % 伪代码逻辑 % 1. 生成一个符号间隔内频率脉冲g(t)的过采样波形 g = generate_freq_pulse(pulse_type, L, sps, BT); % g的长度通常是 L * sps 点 % 2. 将输入符号序列 a_n 与脉冲 g(t) 进行“连续卷积” % 注意不是离散卷积!每个符号 a_n 会激发一个完整的脉冲 g(t),这些脉冲在时间上叠加。 phase_increment = zeros(1, total_samples); % 初始化相位增量序列 for n = 1:length(symbols) idx_start = (n-1)*sps + 1; idx_end = idx_start + length(g) - 1; phase_increment(idx_start:idx_end) = phase_increment(idx_start:idx_end) + symbols(n) * g; end % 最终 phase_increment 的长度会超过输入符号对应的长度,这是部分响应(L>1)的特性。

    这一步产生的phase_increment序列,对应的是瞬时频率偏移 $2\pi h \sum a_i g(t-iT)$ 的采样值。

  3. 相位积分与信号生成

    % 对相位增量进行累积(积分),得到相位轨迹 phi(t) phi = 2*pi*h * cumsum(phase_increment) / sps; % 注意除以sps是积分步长的归一化 % 生成复基带信号 t = (0:length(phi)-1) / sps; % 时间轴 s_complex = exp(1j * phi); % 这就是恒包络的CPM信号

    注意事项: 这里cumsum的初始相位通常默认为0。在实际系统中,可能需要考虑初始相位或进行差分编码以避免相位模糊。另外,确保phi的计算考虑了时间离散化的尺度因子(/sps),否则频率和相位会错位。

3.3 信道模型:不仅仅是加性高斯白噪声

awgn_channel.m中,我们通常简单地添加高斯白噪声。但这里有一个关键细节:信噪比 Eb/N0 的换算

function [rx_signal] = awgn_channel(tx_signal, EbN0_dB, M, sps) % tx_signal: 发送的复基带信号 % EbN0_dB: 每比特能量与噪声功率谱密度之比 (dB) % 计算信号功率 signal_power = mean(abs(tx_signal).^2); % 计算每符号能量 Es Es = signal_power / (sps); % 因为信号是过采样的,功率需按符号率归一化 % 计算每比特能量 Eb = Es / log2(M) Eb = Es / log2(M); % 将 Eb/N0 (dB) 转换为线性值 EbN0 = 10^(EbN0_dB/10); % 计算噪声功率谱密度 N0 N0 = Eb / EbN0; % 计算复基带噪声的方差:噪声功率 = N0 * 采样率 % 对于复噪声,实部和虚部独立,各占一半功率。通常生成方差为 N0/2 的复高斯噪声。 noise_power = N0 * sps; % 因为采样率为 sps/T noise = sqrt(noise_power/2) * (randn(size(tx_signal)) + 1j*randn(size(tx_signal))); rx_signal = tx_signal + noise; end

踩过的坑: 很多初学者在这里出错,直接对过采样信号用awgn函数,却忘了指定正确的信号功率计算方式,导致仿真的BER曲线与理论值对不上。务必理解Eb/N0每比特能量噪声功率谱密度之比,而仿真是在离散时间、特定采样率下进行的,需要进行正确的功率换算。

3.4 解调器实现:相干检测与维特比算法

解调是CPM仿真的难点和重点。cpm_demodulator.m可能实现了多种检测方式。

3.4.1 相干解调与匹配滤波对于全响应CPM(如MSK),可以使用简单的正交相干解调,类似于PSK。但对于部分响应CPM,最优检测器是基于网格的序列检测

3.4.2 维特比算法(VA)解调这是CPM解调的核心。步骤如下:

  1. 网格生成: 根据 $(M, h, L)$ 计算出所有可能的状态states和状态转移transitions。每个转移对应一个输入符号和一段发送的信号波形(称为路径波形分支波形)。
  2. 分支度量计算: 对于接收到的信号段,计算其与所有可能的分支波形(对应于每个状态转移)的相关度量欧氏距离度量。对于复基带信号,分支度量通常是计算接收信号与候选信号的内积实部。
  3. 维特比递归: 在网格的每一级,对每个状态,保留到达该状态的具有最大累积度量(或最小距离)的路径(幸存路径)。
  4. 回溯与判决: 经过一定延迟后,选择具有最佳度量的最终状态,沿着幸存路径回溯,判决出最初的符号序列。

实操心得: 在MATLAB中实现VA时,效率是关键。避免在循环中重复计算分支度量。可以预先计算好所有可能的分支波形(复基带信号片段)并存储在一个查找表中。在计算分支度量时,直接取对应波形与接收信号做相关。这能极大提升仿真速度,尤其是在高信噪比、需要大量符号仿真以获得低BER时。

3.4.3 非相干解调在某些无法获取精确载波相位的场景(如快衰落信道),可能需要非相干检测。一种常见方法是使用多个符号间隔的差分检测基于Laurent分解的PAM近似法。Laurent分解是一个强大的工具,它将任何二进制CPM信号精确地分解为若干个脉冲幅度调制(PAM)波形的叠加。这样,就可以用一组并行的匹配滤波器加一个简化网格的VA来近似最优检测,复杂度大大降低。如果你的代码包里包含laurent_decomposition.m之类的文件,那说明它实现了更高级的检测算法。

4. 仿真实验与结果分析:如何读懂你的输出

运行main.m后,plot_results.m会生成一系列图。看懂这些图,才能评价你的仿真是否成功,以及系统性能如何。

4.1 时域与频域波形分析

  • 时域波形图: 观察real(s_complex)imag(s_complex),或者直接看abs(s_complex)angle(s_complex)。你应该看到恒定的幅度(包络),以及平滑、连续变化的相位。对于部分响应CPM,相位变化会跨越多个符号周期。
  • 眼图: 将解调后的相位或频率轨迹以符号周期为间隔叠加显示。清晰睁开的“眼睛”表示码间干扰小,系统鲁棒性好。CPM的眼图通常是相位或频率的眼图,而非幅度的。
  • 功率谱密度: 使用pwelch函数估算信号的PSD。重点关注主瓣宽度和旁瓣衰减速度。对比不同脉冲形状(REC vs RC)和不同调制指数 $h$ 下的频谱。你会发现,RC脉冲和较小的 $h$ 通常能获得更紧凑的频谱。

4.2 核心性能指标:误码率曲线

BER vs Eb/N0 曲线是通信系统仿真的“成绩单”。你的代码应该对每一个EbN0_dB点,进行多次蒙特卡洛仿真(或发送足够多的符号),统计误比特数,计算BER。

如何判断仿真是否正确?

  1. 理论界对比: 对于二进制CPM(2CPM)在AWGN信道下的相干解调,其理论误比特率有一个紧致的上界(通常与调制指数和关联长度有关)。将你的仿真点与理论曲线画在同一张图上。在低误码率区域(如BER<1e-4),由于仿真统计误差,点可能会偏离,但中高信噪比区域应该基本吻合。
  2. 趋势正确性: BER曲线应随Eb/N0增加而单调下降。关联长度 $L$ 更大的系统,曲线应该更陡峭(即获得更大的编码增益)。
  3. 采样率影响: 确保sps足够高(通常8-16)。过低的sps会导致波形失真,在高信噪比下BER无法降到很低,出现“错误平层”。

4.3 参数扫描实验设计

不要只满足于运行默认参数。设计实验来观察单个参数的影响:

  • 实验一:调制指数 $h$ 的影响。固定M=2, L=3, pulse_type='RC',让h[0.25, 0.5, 0.75]之间变化。观察频谱如何展宽,BER曲线如何移动。你会发现存在一个最优的 $h$ 使得BER性能最好。
  • 实验二:脉冲形状的影响。固定M=2, h=0.5, L=3,对比'REC''RC''GMSK'(需设置BT)。在功率谱图上,RC和GMSK的旁瓣衰减远快于REC。在BER图上,它们可能略有差异。
  • 实验三:关联长度 $L$ 的影响。固定M=2, h=0.5, pulse_type='RC',对比L=1(全响应)、L=2L=3。观察BER曲线,你会发现 $L$ 增大带来的编码增益。但同时,解调器的状态数呈指数增长,复杂度急剧上升。

5. 常见调试问题与性能优化技巧

仿真不出错只是第一步,跑出正确且高效的结果才是目标。以下是我积累的一些“坑”和技巧。

5.1 仿真结果与理论不符的排查清单

问题现象可能原因排查步骤与解决方法
BER曲线整体偏高信噪比换算错误仔细检查awgn_channel.m中的功率计算,确保Eb/N0定义与理论一致。用简单的BPSK仿真验证你的加噪函数是否正确。
BER曲线在低信噪比吻合,高信噪比出现平层1. 采样率sps过低
2. 解调器实现有误(如相位模糊)
3. 数值精度问题
1. 将sps从8提高到16或32再试。
2. 检查解调器是否考虑了差分编码/解码。对于CPM,有时需要在调制前对符号进行差分编码以避免相位模糊。
3. 检查网格状态定义和分支度量计算是否有累积误差。
频谱形状奇怪,有异常毛刺1. FFT长度不足或未加窗
2. 信号截断不完整
1. 使用pwelch函数时,选择合适的窗函数(如汉宁窗)和重叠段数。
2. 确保用于计算PSD的信号段是平稳的,避免包含未成形的起始和结束部分。
维特比解调器运行极慢在循环中实时计算分支波形预计算分支波形表。根据所有可能的状态转移,预先计算出对应的长度为L*sps的信号片段并存储。在VA循环中直接查表使用。
相位轨迹不连续相位卷绕处理不当MATLAB的angle函数返回范围是 $[-\pi, \pi]$。在跟踪连续相位时,需要使用unwrap函数,或者自己处理相位跨越 $2\pi$ 边界的情况。

5.2 MATLAB代码性能优化实战

CPM仿真,尤其是VA解调,计算量很大。对于需要跑低BER(如1e-6)的情况,优化至关重要。

  1. 向量化操作: 尽可能避免for循环。例如,在调制器生成phase_increment时,可以考虑用conv函数的某种变形(注意边界处理)或使用filter函数来实现符号与脉冲的叠加。
  2. 预计算与查表: 如前所述,分支波形、甚至分支度量(对于固定的接收信号段)都可以预计算。这是提升VA速度最有效的方法。
  3. 使用更快的相关计算: 计算接收信号与分支波形的内积时,使用sum(rx .* conj(branch_waveform))real(rx * branch_waveform')。对于实数运算,可以尝试使用dot函数。
  4. 并行计算: 如果你需要扫描多个信噪比点,可以使用parfor循环(需要Parallel Computing Toolbox)。每个信噪比点的仿真相互独立,非常适合并行。
  5. 减少仿真符号数: 对于高信噪比点,误码很少,需要极长的仿真时间才能获得可靠的统计。可以使用重要性采样等加速仿真技术,但这属于高级话题。一个务实的方法是,对于高信噪比点,如果只是画曲线,可以适当外推或直接连接理论曲线。

5.3 从仿真到理解的进阶思考

当你成功运行并调试好这个CPM仿真包后,可以尝试以下扩展,这能让你从“会用代码”上升到“理解系统”:

  1. 实现Laurent分解: 尝试自己编写代码,将二进制CPM信号分解成PAM波形。观察主分量和副分量的能量分布。这能让你直观理解为什么可以用简化的检测器。
  2. 添加同步模块: 真实的接收机没有完美的同步。尝试在信号中加入载波频偏和定时误差,然后实现一个简单的闭环同步环(如科斯塔斯环用于载波恢复,早迟门用于定时恢复),观察同步误差对BER的影响。
  3. 与PSK/QAM对比: 在相同的带宽(或符号速率)和相同的信噪比下,对比CPM和PSK/QAM的BER和功率谱。你会清楚地看到CPM恒包络和频谱效率的优势,以及其接收机复杂度的代价。
  4. 探索网格编码调制: CPM本身具有内在的记忆性,可以看作一种网格编码。思考它与传统的外加卷积码+TCM(网格编码调制)有何异同。

这个“CPM.rar”项目,不仅仅是一堆MATLAB文件,它是一个完整的通信系统教学与研究的沙盒。通过亲手调整每一个参数,观察每一次波形变化,分析每一条性能曲线,你对连续相位调制技术的理解将从抽象的公式深化为具象的工程直觉。这份直觉,无论是对于从事通信算法开发,还是进行学术研究,都是无比宝贵的财富。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 12:44:36

酷开电视8S26机芯刷机指南:V017.009.270固件安全升级全解析

简介&#xff1a;本资源是酷开14U系列智能电视&#xff08;U49/U55型号&#xff09;专用的整机USB刷机升级固件&#xff0c;适用于8S26主控芯片平台&#xff0c;面向具备基础嵌入式系统操作能力的硬件爱好者、售后维修工程师及固件定制开发者&#xff0c;解决电视系统卡顿、功能…

作者头像 李华
网站建设 2026/9/5 12:44:17

基于Qt/C++的PID实时调参可视化控件设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 12:42:14

nRF24LE1用户态寄存器控制:基于UIO的嵌入式实时开发方案

简介&#xff1a;本资源是面向嵌入式开发者与物联网爱好者的一套nRF24LE1无线温度传感系统完整开发工程&#xff0c;聚焦低功耗2.4GHz射频通信与传感器数据采集实践。压缩包共35个文件&#xff0c;涵盖6个OBJ目标文件、6个LST汇编列表、3个PLG构建日志、2个HEX与2个BIN固件镜像…

作者头像 李华
网站建设 2026/9/5 12:35:14

基于ThinkPHP的收卡网系统开发与运营实战指南

简介&#xff1a;这是一套基于ThinkPHP开发的新版运营级收卡系统源码&#xff0c;面向二手卡券回收平台创业者、电商技术团队及PHP中高级开发者&#xff0c;旨在解决礼品卡闲置浪费与企业资金回流难题&#xff0c;支持商超卡、旅游卡、视频会员卡、餐券等多类电子券线上回收与兑…

作者头像 李华
网站建设 2026/9/5 12:32:03

雷达感知崛起:毫米波雷达如何重塑物联网核心感知层

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/5 12:30:57

AI智能体共建仓库:短剧生产协作的开源实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华