diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 1e12724..a51d54b 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -38,6 +38,6 @@ jobs:
for f in output/*.html; do
echo "上传: $(basename "$f")"
curl -sS --retries 3 -u "${WEBDAV_USER}:${WEBDAV_PASS}" \
- -T "$f" "${WEBDAV_URL}/$(basename "$f")"
+ -T "$f" "${WEBDAV_URL}/docs/$(basename "$f")"
done
echo "部署完成"
diff --git a/README.md b/README.md
index 3c4f632..be1975e 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](http://114.214.202.87:9030/readout/readout_program/actions)
-> 📖 **在线文档**: [https://gc-home.top/manual-doc-readout/](https://gc-home.top/manual-doc-readout/)
+> 📖 **在线文档**: [https://gc-home.top/docs/manual-doc-readout/](https://gc-home.top/docs/manual-doc-readout/)
本项目为 ez-Q 2.5 读出子系统的编程控制模型文档。采用 **"文档即代码" (Docs as Code)** 工作方式,
通过 Markdown 纯文本写作、Git 版本控制和 Python 构建管道生成自包含 HTML 报告。
diff --git a/chapters/03-overview.md b/chapters/03-overview.md
index 35416c7..ccec790 100644
--- a/chapters/03-overview.md
+++ b/chapters/03-overview.md
@@ -18,10 +18,10 @@ ez-Q 2.5测控系统包含5种物理通道,对应5类硬件接口,分别是A
|Pump通道|Pump信号|^|
- 测控系统中的每种控制模型实现的功能都是通过以下四类数据进行定义,
- - 通道微控制器码字指令
- - 通道寄存器配置数据
- - 通道SRAM配置数据
- - 通道模拟电路配置
+ - 通道微控制器码字指令
+ - 通道寄存器配置数据
+ - 通道SRAM配置数据
+ - 通道模拟电路配置
- 测控系统的寄存器、配置数据和模拟电路配置返回数据统一采用**大端字节序**。
@@ -60,9 +60,7 @@ ez-Q 2.5 ASIC平台读出子系统由读出基带板、读出混频板和读出
当软件需要对不同通道编程时,其通过ip地址指定机箱、通过槽位号指定板卡、
通过扩展地址指定同一个板卡内的多个通道、通过地址指定一个通道内的不同配置项。
-```
索引基地址定义详见[附录A. IDS寄存器索引表](#a-ids)的 [A.1 地址空间映射](#a1) 节。
-```
本文所述的三类编程通道具体定义如下:
* ACQ 编程通道定义从混频板`rf_in`输入端口到基带板卡内部DAQ模块;
diff --git a/chapters/04-02-acq-codeword.md b/chapters/04-02-acq-codeword.md
index 6b13f11..0589621 100644
--- a/chapters/04-02-acq-codeword.md
+++ b/chapters/04-02-acq-codeword.md
@@ -5,7 +5,7 @@ ACQ通道利用MCU产生32位的码字来控制读出行为,32位的操控码
|比特位|名字|功能描述|
|:-|:-|:-|
|[31:16]|QUBIT_EN| 16个量子比特使能,高电平使能对应位量子比特|
-|[15]|COUNT_SAVE_EN| 态计数据存储使能, 高电平使能态计数存储|
+|[15]|COUNT_SAVE_EN| 态计数数据存储使能, 高电平使能态计数存储|
|[14]|STATE_SAVE_EN| 态数据存储使能, 高电平使能态数据存储|
|[13]|IQ_SAVE_EN| 解模数据存储使能, 高电平使能解模数据存储|
|[12]|WAVE_SAVE_EN| 波形数据存储使能, 高电平使能波形数据存储|
@@ -34,18 +34,18 @@ ACQ通道利用MCU产生32位的码字来控制读出行为,32位的操控码
**注意码字存在优先级和时序:**
* 当同一个指令中同时对多个量子比特解模/读出时,
- * 解模时间等于最长那个比特的解模时间
- * 读出时间等于最长那个比特的读出时间
+ * 解模时间等于最长那个比特的解模时间
+ * 读出时间等于最长那个比特的读出时间
* 在一个指令中同时使能四种数据存储时,存储的先后顺序为:
- 1. 波形数据
- 1. 解模IQ数据
- 1. 态判断数据
- 1. 态计数数据
+ 1. 波形数据
+ 1. 解模IQ数据
+ 1. 态判断数据
+ 1. 态计数数据
* 在一个指令中同时使能了解模求和清零、解模求和和求和存储时,顺序为:
- 1. 先执行IQ结果清零,
- 1. 再执行IQ结果求和
- 1. 最后执行IQ结果存储
+ 1. 先执行IQ结果清零,
+ 1. 再执行IQ结果求和
+ 1. 最后执行IQ结果存储
* 在一个指令中同时使能了态计数清零、态计数和计数存储时,顺序为:
- 1. 先执行态计数清零
- 1. 再执行态计数
- 1. 最后执行计数结果存储
\ No newline at end of file
+ 1. 先执行态计数清零
+ 1. 再执行态计数
+ 1. 最后执行计数结果存储
\ No newline at end of file
diff --git a/chapters/04-03-acq-registers.md b/chapters/04-03-acq-registers.md
index a9e90d6..b58c326 100644
--- a/chapters/04-03-acq-registers.md
+++ b/chapters/04-03-acq-registers.md
@@ -15,11 +15,11 @@ ACQ通道寄存器的地址空间可以被SPI和MCU同时访问,
- sample_depth用于控制波形采集模式下采集波形的长度,单位是时钟周期;
- mtf_idx_q在使能寄存器控制情况下,用于直接索引匹配滤波器权重/系数;
- - 高16比特对应索引地址,单位是时钟周期
- - 低16比特对应索引长度,单位是时钟周期
+ - 高16比特对应索引地址,单位是时钟周期
+ - 低16比特对应索引长度,单位是时钟周期
- dds_fpw_q在使能寄存器控制情况下,用于控制解模载波的频率和相位;
- - 高20位对应载波频率控制字fcw,$F_{c}=fcw/2^{20}*sample\\_rate$
- - 低12位对应载波相位控制字pcw,$\phi_{c} = pcw/2^{12}*2*\pi$
+ - 高20位对应载波频率控制字fcw,$F_{c}=fcw/2^{20}*sample\\_rate$
+ - 低12位对应载波相位控制字pcw,$\phi_{c} = pcw/2^{12}*2*\pi$
- function用来控制整个读出基带板数据处理行为;
function功能寄存器与实验控制相关的具体控制位包括:
@@ -34,20 +34,20 @@ function功能寄存器与实验控制相关的具体控制位包括:
* WEIGHT_IQ 常数权重值,可用于长时间解模
* CONST_EN 常数权重使能
- * `CONST_EN=0`时,权重数据通过`mtf_idx_q`来索引得到
- * `CONST_EN=1`来启用常量权重功能,此时权重值为`WEIGHT_IQ`。
+ * `CONST_EN=0`时,权重数据通过`mtf_idx_q`来索引得到
+ * `CONST_EN=1`来启用常量权重功能,此时权重值为`WEIGHT_IQ`。
* STEP_CTRL 权重点步长控制
- - `STEP_CTRL==0` 1个权重数据点可对应1个时钟周期采样点。
- - `STEP_CTRL==1` 1个权重数据点可对应2个时钟周期采样点。
- - `STEP_CTRL==2` 1个权重数据点可对应4个时钟周期采样点。
- - `STEP_CTRL==3` 1个权重数据点可对应8个时钟周期采样点。
- - `STEP_CTRL==4` 1个权重数据点可对应16个时钟周期采样点。
+ - `STEP_CTRL==0` 1个权重数据点可对应1个时钟周期采样点。
+ - `STEP_CTRL==1` 1个权重数据点可对应2个时钟周期采样点。
+ - `STEP_CTRL==2` 1个权重数据点可对应4个时钟周期采样点。
+ - `STEP_CTRL==3` 1个权重数据点可对应8个时钟周期采样点。
+ - `STEP_CTRL==4` 1个权重数据点可对应16个时钟周期采样点。
* IQ_SCALE 解模结果截断位置控制
- - `IQ_SCALE =0` IQ结果截位[27:8]
- - `IQ_SCALE =1` IQ结果截位[31:12]
+ - `IQ_SCALE =0` IQ结果截位[27:8]
+ - `IQ_SCALE =1` IQ结果截位[31:12]
* TWO_STA_EN 两态读出使能
- - `TWO_STA_EN==0` 默认三态读出
- - `TWO_STA_EN==1` 使能二态读出
+ - `TWO_STA_EN==0` 默认三态读出
+ - `TWO_STA_EN==1` 使能二态读出
> **注意:** ez-Q 2.5 FPGA平台采用系数直读模式,因此不支持系数生产相关功能。
diff --git a/chapters/04-04-acq-matched-filter.md b/chapters/04-04-acq-matched-filter.md
index 24ea9cd..edc78ba 100644
--- a/chapters/04-04-acq-matched-filter.md
+++ b/chapters/04-04-acq-matched-filter.md
@@ -23,7 +23,7 @@ $$ \text{Qubit }N\text{ DAQ\_PAR 基地址} = \mathtt{0x00500000} + N \times \ma
|:-|:-|:-|:-|
|`0x00`|dds_fpw|[31:12] fcw|解模载波频率控制字:$F_c = fcw / 2^{20} \times F_s$|
|^|^|[11:0] pcw|解模载波相位控制字:$\phi_c = pcw / 2^{12} \times 2\pi$|
-|`0x04`|mtf_idx|[31:16] addr|权重数据起始地址(单位:时钟周期|
+|`0x04`|mtf_idx|[31:16] addr|权重数据起始地址(单位:时钟周期)|
|^|^|[15:0] len|权重数据样本长度(单位:时钟周期)|
|`0x08`|line0_ab|[31:16] a0|直线0 斜率系数 a(16位有符号数)|
|^|^|[15:0] b0|直线0 斜率系数 b(16位有符号数)|
@@ -57,7 +57,7 @@ ez-Q 2.5 FPGA平台使用系数直读模式,其需要额外的存储空间来
- 其中mtf_idx的含义从索引权重数据变为索引匹配滤波器系数。
- 匹配滤波器系数索引的粒度是时钟周期,
- - 在FPGA平台下每个时钟周期对应16个采样点数据。
+ - 在FPGA平台下每个时钟周期对应16个采样点数据。
- 匹配滤波器系数采样点采用8比特数据位宽,因此1个周期数据位宽为128 bit,
匹配滤波器系数存储结构如下图所示。
diff --git a/chapters/04-06-acq-pipeline-demo.md b/chapters/04-06-acq-pipeline-demo.md
index b9e0d2a..e681a11 100644
--- a/chapters/04-06-acq-pipeline-demo.md
+++ b/chapters/04-06-acq-pipeline-demo.md
@@ -104,8 +104,8 @@ mtf_idx 的 addr 和 len 单位为时钟周期(ASIC 平台每周期 8 采样
|地址|寄存器|字段|值|说明|
|:-|:-|:-|:-|:-|
|`0x00400044`|func_ctrl|iq_scale [3]|`0`|IQ 结果截位 [27:8]|
-|`0x00400044`|func_ctrl|two_sta_en [2]|`0`|三态读出模式|
-|`0x00400044`|func_ctrl|const_en [7]|`0`|使用查找表权重(非常数权重)|
+|^|^|two_sta_en [2]|`0`|三态读出模式|
+|^|^|const_en [7]|`0`|使用查找表权重(非常数权重)|
|`0x00400080`|mtf_idx_q0|addr [31:16] / len [15:0]|fallback 值|q0 匹配滤波器索引(查找表模式的 fallback)|
|`0x004000C0`|dds_fpw_q0|fcw [31:12] / pcw [11:0]|fallback 值|q0 DDS参数(查找表模式的 fallback)|
diff --git a/chapters/05-00-exc-model.md b/chapters/05-00-exc-model.md
index 233fbac..2e88531 100644
--- a/chapters/05-00-exc-model.md
+++ b/chapters/05-00-exc-model.md
@@ -27,9 +27,9 @@ MCU的指令、MCU的数据、控制寄存器、波形索引表、波形仓库
上述过程中模式选择器可以选择原始波形数据、希尔伯特虚部数据、正交调制数据以及NCO自身产生的单音数据。
- 原始波形模式:用于量子实验,通过直接输出包含多个读出频率的波形,可以实现对多个量子比特的并行读出;
- - 正交调制模式:用于腔频扫描等应用,通过实时修改NCO频率,可以实时改变输出频率,配合DAQ进行实时读取,可以实现扫频功能。若基带信号包含多个频点,还能实现多频点并行扫描功能;
- - NCO Only模式:可以输出连续波形,方便连接外部仪器上进行测试,用于芯片本身性能的测试。
- - 希尔伯特虚部模式:输出波形经过希尔伯特变换后的正交部分,仅用于调试。
+ - 正交调制模式:用于腔频扫描等应用,通过实时修改NCO频率,可以实时改变输出频率,配合DAQ进行实时读取,可以实现扫频功能。若基带信号包含多个频点,还能实现多频点并行扫描功能;
+ - NCO Only模式:可以输出连续波形,方便连接外部仪器上进行测试,用于芯片本身性能的测试。
+ - 希尔伯特虚部模式:输出波形经过希尔伯特变换后的正交部分,仅用于调试。
最后数字信号经过DAC转换成基带信号,基带信号再和外部本振信号模拟混频后输出读出激励波形。
读出芯片不同模式输出的频响曲线如下图所示:
diff --git a/chapters/05-01-exc-codeword.md b/chapters/05-01-exc-codeword.md
index cd61634..511cfa5 100644
--- a/chapters/05-01-exc-codeword.md
+++ b/chapters/05-01-exc-codeword.md
@@ -10,7 +10,7 @@ AWG模块仅使用码字指令的低13位,其余位保留,
|[10]|NCO_CLR_EN| NCO清零使能, 高有效|
|[9]|MARK_EN| 标记脉冲输出使能, 高有效|
|[8]|PUMP_EN| 泵浦脉冲输出使能, 高有效|
-|[7:0]|WAVE_ID| 波形编号, 最大索256个波形输出|
+|[7:0]|WAVE_ID| 波形编号, 最大索引256个波形输出|
- 码字比特[12]用于禁用波形输出,通过使能该比特可以仅输出Pump或者Marker脉冲而不输出该指令码字[7:0]索引的波形。
- 码字比特[11]用于使能动态波形输出功能,该功能直接使用wave_ctrl寄存器中来索引波形,暂不使用该功能;
diff --git a/chapters/05-02-exc-registers.md b/chapters/05-02-exc-registers.md
index 27bd18a..061eef7 100644
--- a/chapters/05-02-exc-registers.md
+++ b/chapters/05-02-exc-registers.md
@@ -14,21 +14,21 @@ EXC-Pump通道的全部寄存器可以被SPI和MCU同时访问,
|mark_ctrl|标记使能脉冲控制|
- `wave_ctrl`波形输出直接控制,可直接从波形仓库取采样点输出。
- - 高16位为索引地址,单位是时钟周期
- - 低16为为索引长度,单位是时钟周期
+ - 高16位为索引地址,单位是时钟周期
+ - 低16位为索引长度,单位是时钟周期
- `amplitude`调制幅度控制字
- - 高16为幅度控制字acw,范围-32768~32767,归一化幅度$Amp = acw/2^{14}$
+ - 高16位为幅度控制字acw,范围-32768~32767,归一化幅度$Amp = acw/2^{14}$
- `frequency`调制频率控制
- - 32比特频率控制字fcw,$F_{nco}= fcw/2^{32}*F_s$,其中$F_s$是输出采样率。
+ - 32比特频率控制字fcw,$F_{nco}= fcw/2^{32}*F_s$,其中$F_s$是输出采样率。
- `phase`调制相位控制
- - 高16位相位控制字pcw: $\phi_{nco} = pcw/2^{16}*2*\pi$。
+ - 高16位相位控制字pcw: $\phi_{nco} = pcw/2^{16}*2*\pi$。
- `function`寄存器用于设置AWG的工作模式
- `pump_ctrl`泵浦脉冲使能控制
- - 高16位控制输出延迟时钟周期,范围(1~65535)
- - 低16位控制附加持续时钟周期,范围(1~65535)
+ - 高16位控制输出延迟时钟周期,范围(1~65535)
+ - 低16位控制附加持续时钟周期,范围(1~65535)
- `mark_ctrl`标记脉冲使能控制
- - 高16位控制输出延迟时钟周期,范围(1~65535)
- - 低16位控制脉冲持续时钟周期,范围(1~65535)
+ - 高16位控制输出延迟时钟周期,范围(1~65535)
+ - 低16位控制脉冲持续时钟周期,范围(1~65535)
function的详细控制如下所示:
diff --git a/chapters/05-04-exc-waveform-store.md b/chapters/05-04-exc-waveform-store.md
index ac3f3af..2258371 100644
--- a/chapters/05-04-exc-waveform-store.md
+++ b/chapters/05-04-exc-waveform-store.md
@@ -6,4 +6,4 @@
* 波形仓库的容量为128 KB,在FPGA平台和ASIC平台下最大分别支持16 us和10 us波形输出。
对于EXC输出频率$F_{out}$(例如6.7 GHz),在本振为$F_{LO}$(例如5.5GHz)本振频率下,
-则存储区描绘的基带波形频率$F_{s}$为1.2 GHz ,$F_s = F_{out} - F_{LO}$。
+则存储区描绘的基带波形频率$F_{IF}$为1.2 GHz ,$F_{IF} = F_{out} - F_{LO}$。
diff --git a/chapters/05-07-exc-pipeline-demo.md b/chapters/05-07-exc-pipeline-demo.md
index 41fde20..e464818 100644
--- a/chapters/05-07-exc-pipeline-demo.md
+++ b/chapters/05-07-exc-pipeline-demo.md
@@ -150,7 +150,7 @@ MCU程序加载到 MCU_INS(`0x00700000`),触发后MCU执行程序并生成
|:-|:-|:-|:-|:-|
|1|`0x00B00000`|AWG_WVE|波形采样点|256 × 16 bit|
|2|`0x00A00000`|AWG_IDX[0]|addr / len|`0x0000` / `0x0020`|
-|3|`0x00900034`|amplitude|acw|`8192`|
+|3|`0x00900034`|amplitude|acw|`16384`|
|3|`0x00900038`|frequency|fcw|`0`|
|3|`0x0090003C`|phase|pcw|`0`|
|3|`0x00900044`|func_ctrl|awg_mode, mix_mode, intp_sel|`0x0`|
diff --git a/doc_builder/build.py b/doc_builder/build.py
index f2ce49d..d8b133f 100644
--- a/doc_builder/build.py
+++ b/doc_builder/build.py
@@ -213,26 +213,49 @@ def process_image_attrs(text):
# ---------- KaTeX 预处理 ----------
+# 公式占位符存储(preprocess → postprocess 传递)
+_MATH_PLACEHOLDERS = {}
+
+
def preprocess_katex(text):
"""
- 将 Markdown 中的 LaTeX 公式包装为原始 HTML,
+ 将 Markdown 中的 LaTeX 公式替换为唯一占位符,
防止 Markdown 解析器错误解释公式中的 _ * 等字符。
+ 占位符在 postprocess_katex 中还原为 HTML 包装的公式。
"""
+ _MATH_PLACEHOLDERS.clear()
+
# 块级公式 $$...$$
def protect_display(match):
- latex = match.group(1)
- return f'
$${latex}$$
'
+ latex = match.group(1).strip()
+ idx = len(_MATH_PLACEHOLDERS)
+ key = f"\x00MATH_DISPLAY_{idx}\x00"
+ _MATH_PLACEHOLDERS[key] = f'$${latex}$$
'
+ return key
+
text = re.sub(r'\$\$\s*(.+?)\s*\$\$', protect_display, text, flags=re.DOTALL)
# 行内公式 $...$
def protect_inline(match):
latex = match.group(1)
- return f'${latex}$'
+ idx = len(_MATH_PLACEHOLDERS)
+ key = f"\x00MATH_INLINE_{idx}\x00"
+ _MATH_PLACEHOLDERS[key] = f'${latex}$'
+ return key
+
text = re.sub(r'(? : 与右侧单元格合并(colspan,右侧单元格被吸收)
-# 策略:展平时将合并标记替换为被合并单元格的内容,
-# 再由 postprocess_table_rowspan 检测重复内容生成 rowspan/colspan。
+# 策略:展平时将合并标记替换为被合并单元格的内容并附加哨兵标记,
+# 再由 postprocess_table_rowspan 根据哨兵标记生成 rowspan/colspan。
+# 仅显式标记了 ^ / < / > 的单元格才会参与合并,避免跨逻辑组的过度合并。
+
+MERGE_MARKER = ""
# 表格行: | cell | cell | ... |
TABLE_ROW_RE = re.compile(r'^\|.+\|$')
@@ -387,22 +413,26 @@ def _flatten_mpe_table(table_lines):
for row in parsed:
for col in range(num_cols - 2, -1, -1):
if row[col].strip() == ">":
- row[col] = row[col + 1]
+ row[col] = MERGE_MARKER + row[col + 1]
# 第 2 遍:处理 <(左→右,复制左侧单元格内容)
for row in parsed:
for col in range(1, num_cols):
if row[col].strip() == "<":
- row[col] = row[col - 1]
+ row[col] = MERGE_MARKER + row[col - 1]
- # 第 3 遍:处理 ^(上→下,复制上方单元格内容)
+ # 第 3 遍:处理 ^(上→下,复制上方单元格内容,清除旧哨兵避免重复)
prev_cells = _split_table_cells(header)
while len(prev_cells) < num_cols:
prev_cells.append("")
for row in parsed:
for col in range(num_cols):
if row[col].strip() == "^":
- row[col] = prev_cells[col] if col < len(prev_cells) else row[col]
+ content = prev_cells[col] if col < len(prev_cells) else row[col]
+ # 剥离 prev_cells 中已有的哨兵标记,避免链式累积
+ if content.startswith(MERGE_MARKER):
+ content = content[len(MERGE_MARKER):]
+ row[col] = MERGE_MARKER + content
prev_cells = list(row)
# 重建表格行
@@ -423,10 +453,11 @@ TBODY_RE = re.compile(r'(.*?)', re.DOTALL)
def postprocess_table_rowspan(html):
"""
- 扫描 HTML 表格的 ,将连续内容相同的单元格合并:
- - 同一列上下连续相同 → rowspan
- - 同一行左右连续相同 → colspan
- 与 preprocess_mpe_tables 配合:MPE 标记展平后产生重复内容,此处恢复为视觉合并。
+ 扫描 HTML 表格的 ,将带有 MERGE_MARKER 哨兵的单元格合并:
+ - 同一列上下连续相同(且下方含哨兵) → rowspan
+ - 同一行左右连续相同(且右侧含哨兵) → colspan
+ 与 preprocess_mpe_tables 配合:仅显式标记了 ^ / < / > 的单元格生成哨兵,
+ 避免跨逻辑组的过度合并。
带有 class="no-rowspan" 的 会被跳过,不进行合并处理。
注意:仅处理 内的 (Python markdown tables 扩展的输出格式)。
@@ -467,16 +498,23 @@ def postprocess_table_rowspan(html):
colspan = [[1] * num_cols for _ in range(num_rows)]
# ---- 计算 rowspan(逐列扫描) ----
+ # 仅当下方单元格带 MERGE_MARKER 哨兵且内容匹配时才合并
for col in range(num_cols):
row = 0
while row < num_rows:
count = 1
r = row + 1
while r < num_rows:
- if (row_cells[r][col]["text"] == row_cells[row][col]["text"]
- and row_cells[row][col]["text"] != ""):
+ below_text = row_cells[r][col]["text"]
+ above_text = row_cells[row][col]["text"]
+ # 下方单元格必须带哨兵标记,且去除哨兵后与上方内容一致
+ if (below_text.startswith(MERGE_MARKER)
+ and below_text[len(MERGE_MARKER):] == above_text
+ and above_text != ""):
count += 1
covered[r][col] = True
+ # 将合并单元格的内容统一为去除哨兵的版本
+ row_cells[r][col]["text"] = above_text
r += 1
else:
break
@@ -494,11 +532,16 @@ def postprocess_table_rowspan(html):
count = 1
c = col + 1
while c < num_cols:
+ right_text = row_cells[row][c]["text"]
+ left_text = row_cells[row][col]["text"]
+ # 右侧单元格必须带哨兵标记,且去除哨兵后与左侧内容一致
if (not covered[row][c]
- and row_cells[row][c]["text"] == row_cells[row][col]["text"]
- and row_cells[row][col]["text"] != ""):
+ and right_text.startswith(MERGE_MARKER)
+ and right_text[len(MERGE_MARKER):] == left_text
+ and left_text != ""):
count += 1
covered[row][c] = True
+ row_cells[row][c]["text"] = left_text
c += 1
else:
break
@@ -808,6 +851,9 @@ def main():
# Markdown → HTML
chapter_html = markdown_to_html(text)
+ # KaTeX 还原(占位符 → HTML 包装的公式)
+ chapter_html = postprocess_katex(chapter_html)
+
# 图片 base64 内嵌
base_dirs = [ch_path.parent, ASSETS_DIR, PROJECT_ROOT]
chapter_html = embed_images(chapter_html, base_dirs)
diff --git a/doc_builder/renderers/render_ids.py b/doc_builder/renderers/render_ids.py
new file mode 100644
index 0000000..a45b748
--- /dev/null
+++ b/doc_builder/renderers/render_ids.py
@@ -0,0 +1,185 @@
+"""IDS 数据共享模块。
+
+提供 JSON 加载和 HTML 表格渲染函数,
+供 render_ids_acq_map / render_ids_exc_map / render_ids_daq_reg / render_ids_awg_reg 复用。
+"""
+
+import json
+from pathlib import Path
+from collections import OrderedDict
+
+# 输出顺序(先列出的模块排在前面)
+ACQ_MODULE_ORDER = ["MCU_INS", "MCU_DAT", "DAQ_REG", "DAQ_PAR", "DAQ_FLT"]
+EXC_MODULE_ORDER = ["MCU_INS", "MCU_DAT", "AWG_REG", "AWG_IDX", "AWG_WVE"]
+
+
+# ---------- 数据加载 ----------
+
+def _load_ids(filepath: str) -> dict:
+ """加载 IDS JSON 文件并返回解析后的 dict。"""
+ path = Path(filepath)
+ if not path.exists():
+ raise FileNotFoundError(f"IDS 文件不存在: {path}")
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+
+# ---------- HTML 工具 ----------
+
+def _td(text: str, **kwargs) -> str:
+ """生成 | 单元格,支持 rowspan / style 等属性。"""
+ attrs = " ".join(f'{k}="{v}"' for k, v in kwargs.items() if v is not None)
+ return f" | {text} | " if attrs else f"{text} | "
+
+
+def _th(text: str) -> str:
+ return f"{text} | "
+
+
+def _bit_range_str(bits: list) -> str:
+ """将 [15, 8] 或 [7] 转为位段字符串。"""
+ if len(bits) == 1:
+ return str(bits[0])
+ return f"{bits[0]}:{bits[-1]}"
+
+
+# ---------- 映射表渲染 ----------
+
+def render_mapping_acq_table(data: dict) -> str:
+ """渲染 msmt_acq 通道模块基地址表。"""
+ mi = data.get("MappingInfo", {}).get("msmt_acq", {})
+ return _render_mapping_table(mi, ACQ_MODULE_ORDER, "ACQ")
+
+
+def render_mapping_exc_table(data: dict) -> str:
+ """渲染 msmt_exc 通道模块基地址表。"""
+ mi = data.get("MappingInfo", {}).get("msmt_exc", {})
+ return _render_mapping_table(mi, EXC_MODULE_ORDER, "EXC")
+
+
+def _render_mapping_table(mapping: dict, module_order: list, label: str) -> str:
+ """通用的通道模块基地址渲染。"""
+ rows = []
+ # 收集所有模块名
+ all_modules = set()
+ for ch_info in mapping.values():
+ all_modules.update(ch_info.get("Modules", {}).keys())
+
+ # 按 module_order 排序,未在列表中的放最后
+ ordered = [m for m in module_order if m in all_modules]
+ ordered += sorted(all_modules - set(module_order))
+
+ for mod_name in ordered:
+ for ch in sorted(mapping.keys(), key=int):
+ info = mapping[ch]
+ modules = info.get("Modules", {})
+ if mod_name not in modules:
+ continue
+ m = modules[mod_name]
+ exaddr = info.get("Exaddr", "")
+ cid = info.get("Cid", "")
+ rows.append(
+ "
"
+ f"| ch{ch} | "
+ f"{exaddr} | "
+ f"{cid} | "
+ f"{mod_name} | "
+ f"{m.get('StartAddress', '')} | "
+ f"{m.get('Size', '')} | "
+ f"{m.get('Info', '')} | "
+ "
"
+ )
+
+ if not rows:
+ return "(无映射数据)
"
+
+ header = (
+ ""
+ "| 通道 | Exaddr | Cid | "
+ "模块 | 起始地址 | 大小 | 说明 | "
+ "
"
+ )
+ return f""
+
+
+# ---------- 寄存器表渲染 ----------
+
+def render_daq_reg_table(data: dict) -> str:
+ """渲染 DAQ_REG 寄存器汇总表 + 位域明细。"""
+ regs = data.get("Modules", {}).get("DAQ_REG", {})
+ return _render_reg_table(regs, "DAQ_REG")
+
+
+def render_awg_reg_table(data: dict) -> str:
+ """渲染 AWG_REG 寄存器汇总表 + 位域明细。"""
+ regs = data.get("Modules", {}).get("AWG_REG", {})
+ return _render_reg_table(regs, "AWG_REG")
+
+
+def _render_reg_table(regs: dict, label: str) -> str:
+ """通用寄存器表渲染(汇总表 + 每位域明细)。"""
+ if not regs:
+ return "(无寄存器数据)
"
+
+ # 按偏移地址排序
+ sorted_regs = sorted(regs.values(), key=lambda r: int(r.get("OffsetAddress", "0x0"), 16))
+
+ # ---- 汇总表 ----
+ summary_rows = []
+ for r in sorted_regs:
+ name = list(regs.keys())[list(regs.values()).index(r)]
+ offset = r.get("OffsetAddress", "")
+ perm = r.get("Permission", "")
+ desc = r.get("SegDescription", "")
+ summary_rows.append(
+ ""
+ f"| {name} | "
+ f"{offset} | "
+ f"{perm} | "
+ f"{desc} | "
+ "
"
+ )
+
+ summary = (
+ ""
+ "| 寄存器名 | 偏移地址 | 权限 | 描述 |
"
+ f"{''.join(summary_rows)}"
+ "
"
+ )
+
+ # ---- 位域明细表(仅含字段数 > 0 的寄存器) ----
+ detail_parts = []
+ for r in sorted_regs:
+ fields = r.get("Fields", [])
+ if not fields:
+ continue
+ name = list(regs.keys())[list(regs.values()).index(r)]
+ offset = r.get("OffsetAddress", "")
+
+ field_rows = []
+ for f in fields:
+ bits = f.get("Bits", [])
+ field_name = f.get("FieldName", "")
+ fdesc = (f.get("FieldDescription", "") or "").replace("\n", "
")
+ reset = f.get("ResetValue", "")
+ field_rows.append(
+ ""
+ f"[{_bit_range_str(bits)}] | "
+ f"{field_name} | "
+ f"{reset} | "
+ f"{fdesc} | "
+ "
"
+ )
+
+ if field_rows:
+ detail_parts.append(
+ f"{name}({offset})
"
+ ""
+ "| 位段 | 字段名 | 复位值 | 描述 |
"
+ f"{''.join(field_rows)}"
+ "
"
+ )
+
+ if detail_parts:
+ return f"{summary}
{''.join(detail_parts)}"
+ return summary