diff --git a/CLAUDE.md b/CLAUDE.md
index 31350cb..4c73dee 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -85,6 +85,7 @@ python doc_builder/build.py
| §5.5 | `chapters/05-05-exc-upconversion.md` | EXC 上变频电路配置 |
| §5.6 | `chapters/05-06-pump-config.md` | Pump 模拟电路配置 |
| §5.7 | `chapters/05-07-exc-pipeline-demo.md` | EXC-Pump 全流程控制示例 |
+| 附录A | `chapters/80-appendix-ids-table.md` | IDS 寄存器索引表(DAQ_REG / AWG_REG / 地址映射) |
## 编辑工作流
diff --git a/chapters/03-overview.md b/chapters/03-overview.md
index b6fba1c..b6c127d 100644
--- a/chapters/03-overview.md
+++ b/chapters/03-overview.md
@@ -26,7 +26,7 @@ ez-Q 2.5测控系统包含5种物理通道,对应5类硬件接口,分别是A
- 测控系统的寄存器、配置数据和模拟电路配置返回数据统一采用**大端字节序**。
通道寄存器配置数据支持微控制器实时修改,从而让通道的输入/输出控制具备动态控制能力。
-ACQ和EXC-Pump通道配置寄存器定义参考[读出子系统IDS表.xls](TODO)的`DAQ_REG`和`AWG_REG`页。
+ACQ和EXC-Pump通道配置寄存器的地址索引定义详见[附录A. IDS寄存器索引表](#a-ids)的 [A.2 DAQ_REG](#a2-daq_reg-acq) 和 [A.3 AWG_REG](#a3-awg_reg-exc-pump) 节。
本文档通过对不同编程模型下码字指令、通道寄存器配置数据、通道SRAM配置数据和通道模拟电路配置进行介绍,
旨在让用户掌握对读出激励信号的产生和采集信号处理的编程方法。
@@ -61,7 +61,7 @@ ez-Q 2.5 ASIC平台读出子系统由读出基带板、读出混频板和读出
通过扩展地址指定同一个板卡内的多个通道、通过地址指定一个通道内的不同配置项。
```
-索引基地址由《读出子系统IDS表.xls》的mapping页定义,本文不对地址翻译进行赘述。
+索引基地址定义详见[附录A. IDS寄存器索引表](#a-ids)的 [A.1 地址空间映射](#a1) 节。
```
本文所述的三类编程通道具体定义如下:
diff --git a/chapters/80-appendix-ids-table.md b/chapters/80-appendix-ids-table.md
new file mode 100644
index 0000000..d182a65
--- /dev/null
+++ b/chapters/80-appendix-ids-table.md
@@ -0,0 +1,61 @@
+# 附录A. IDS寄存器索引表
+
+本附录提供读出子系统通道配置寄存器的地址索引(IDS, Index Data Sheet),
+供软件编程时通过基地址 + 偏移地址访问具体寄存器。
+
+以下所有表格数据来源于 IDS JSON 数据文件(对应2509批次读出芯片),
+由构建时自动渲染生成,确保与硬件设计数据库保持同步。
+
+## A.1. 地址空间映射
+
+读出子系统的配置访问采用四级寻址层级,从网络到寄存器的完整链路如下:
+
+1. **网络地址(IP)** — 定位机箱。软件通过指定目标机箱的 IP 地址来确定与哪个机箱内的板卡通信。一个机箱内可安装多块 PXIe 板卡,IP 地址是整个通信链路的起点。
+
+2. **扩展地址(Exaddr)** — 定位槽位和总线。[7:3]对应槽位号,exaddr[2:0]对应总线号。读出基带板允许安装在4或者15槽,泵浦板和混频板分别在左右相邻位置,因此软件仅需指定基带板槽位号即可定位全部三个板卡。
+
+3. **片选地址(Cid)** — 定位总线上芯片ID。采用5比特编码最大支持32颗芯片挂载到同一个spi总线上,在当前的实现中,直接使用总线编址通道号,每个芯片独占一个总线,因此Cid固定为0。
+
+4. **配置基地址(Base)** — 定位具体配置项。采用25比特编码,其中高5比特用来定位不同模块,配合20比特的偏移地址用来具体指定配置项目。
+
+
+芯片控制存在MCU视角和配置视角,区别是基地址不同。从MCU操控视角来看,AWG和DAQ模块寄存器统一映射在对应 MCU 的 **`0x200000`** 地址空间。从上位机配置视角看,基地址和偏移地址定义如下表所示:
+
+### A.1.1. ACQ 通道模块基地址
+
+对应 `msmt_acq` 通道类型(采集通道),共 4 个通道(ch0~ch3),
+每个通道包含 5 个功能模块:
+
+@import "../../rbpu_datasheet/data/ids.json" using render_ids_acq_map
+
+### A.1.2. EXC 通道模块基地址
+
+对应 `msmt_exc` 通道类型(激励通道),共 4 个通道(ch0~ch3),
+每个通道包含 5 个功能模块:
+
+@import "../../rbpu_datasheet/data/ids.json" using render_ids_exc_map
+
+## A.2. DAQ_REG — ACQ 寄存器
+
+ACQ 通道配置寄存器,对应采集通道的 DAQ 模块。寄存器基地址参见
+[A.1.1 节](#a11-acq) 中 `DAQ_REG` 行。
+各寄存器通过偏移地址在模块内部索引,如下表所示。
+
+@import "../../rbpu_datasheet/data/ids.json" using render_ids_daq_reg
+
+## A.3. AWG_REG — EXC-Pump 寄存器
+
+EXC-Pump 通道配置寄存器,对应激励通道的 AWG 模块。寄存器基地址参见
+[A.1.2 节](#a12-exc) 中 `AWG_REG` 行。
+各寄存器通过偏移地址在模块内部索引,如下表所示。
+
+@import "../../rbpu_datasheet/data/ids.json" using render_ids_awg_reg
+
+## A.4. 寄存器访问说明
+
+- 所有寄存器支持 **SPI** 和 **MCU** 双端口访问
+- 静态参数建议通过 SPI 配置后保持不变,动态参数通过 MCU 实时更新
+- 寄存器位宽均为 **32 位**(不足 32 位的高位保留)
+- 返回数据统一采用 **大端字节序**
+- MCU 以固定 **3 个时钟周期/指令** 的速率执行
+- 权限缩写:`RW` = 读写,`RO` = 只读,`WC` = 只写(写清除)
diff --git a/doc_builder/build.py b/doc_builder/build.py
index 4ec5c26..f2ce49d 100644
--- a/doc_builder/build.py
+++ b/doc_builder/build.py
@@ -72,11 +72,15 @@ def load_plugin_modules(directory):
if spec is None or spec.loader is None:
continue
mod = importlib.util.module_from_spec(spec)
+ # 注册到 sys.modules 以支持插件间交叉导入
+ sys.modules[py_file.stem] = mod
spec.loader.exec_module(mod)
modules[py_file.stem] = mod
print(f" 已加载: {py_file.stem}")
except Exception as e:
print(f" 警告: 加载 {py_file.name} 失败: {e}")
+ # 加载失败时清理 sys.modules
+ sys.modules.pop(py_file.stem, None)
return modules
@@ -424,15 +428,19 @@ def postprocess_table_rowspan(html):
- 同一行左右连续相同 → colspan
与 preprocess_mpe_tables 配合:MPE 标记展平后产生重复内容,此处恢复为视觉合并。
+ 带有 class="no-rowspan" 的
会被跳过,不进行合并处理。
注意:仅处理 内的 (Python markdown tables 扩展的输出格式)。
"""
+ # 匹配整个 元素,检查其 class 属性
+ TABLE_RE = re.compile(r'()', re.DOTALL)
+
def merge_tbody(match):
tbody = match.group(1)
rows = TR_RE.findall(tbody)
if len(rows) < 2:
return tbody
- # 解析所有单元格
+ # 解析所有单元格(统一补齐到最大列数,处理 rowspan 导致的列数不一致)
row_cells = []
for row_html in rows:
cells = []
@@ -448,6 +456,11 @@ def postprocess_table_rowspan(html):
if num_cols == 0:
return tbody
+ # 统一补齐:不足 num_cols 的行用空单元格补齐
+ for row in row_cells:
+ while len(row) < num_cols:
+ row.append({"attrs": "", "text": ""})
+
# covered[r][c]:该单元格已被 rowspan 或 colspan 覆盖,渲染时跳过
covered = [[False] * num_cols for _ in range(num_rows)]
rowspan = [[1] * num_cols for _ in range(num_rows)]
@@ -457,14 +470,10 @@ def postprocess_table_rowspan(html):
for col in range(num_cols):
row = 0
while row < num_rows:
- if col >= len(row_cells[row]):
- row += 1
- continue
count = 1
r = row + 1
while r < num_rows:
- if (col < len(row_cells[r])
- and row_cells[r][col]["text"] == row_cells[row][col]["text"]
+ if (row_cells[r][col]["text"] == row_cells[row][col]["text"]
and row_cells[row][col]["text"] != ""):
count += 1
covered[r][col] = True
@@ -473,18 +482,18 @@ def postprocess_table_rowspan(html):
break
if count > 1:
rowspan[row][col] = count
- row = r # 跳过已被当前 rowspan 覆盖的行
+ row = r
# ---- 计算 colspan(逐行扫描,跳过已覆盖单元格) ----
for row in range(num_rows):
col = 0
- while col < len(row_cells[row]):
+ while col < num_cols:
if covered[row][col]:
col += 1
continue
count = 1
c = col + 1
- while c < num_cols and c < len(row_cells[row]):
+ while c < num_cols:
if (not covered[row][c]
and row_cells[row][c]["text"] == row_cells[row][col]["text"]
and row_cells[row][col]["text"] != ""):
@@ -495,14 +504,14 @@ def postprocess_table_rowspan(html):
break
if count > 1:
colspan[row][col] = count
- col = c # 跳过已被当前 colspan 覆盖的列
+ col = c
# ---- 生成带 rowspan / colspan 的 HTML ----
new_rows = []
for row in range(num_rows):
new_cells = []
for col in range(num_cols):
- if covered[row][col] or col >= len(row_cells[row]):
+ if covered[row][col]:
continue
cell = row_cells[row][col]
rs = rowspan[row][col]
@@ -512,12 +521,27 @@ def postprocess_table_rowspan(html):
attrs += f' rowspan="{rs}"'
if cs > 1:
attrs += f' colspan="{cs}"'
- new_cells.append(f'| {cell["text"]} | ')
+ # 修复: attrs 非空时加前导空格,避免 畸形标签
+ if attrs:
+ new_cells.append(f'{cell["text"]} | ')
+ else:
+ new_cells.append(f'{cell["text"]} | ')
new_rows.append("" + "".join(new_cells) + "
")
return "" + "".join(new_rows) + ""
- return TBODY_RE.sub(merge_tbody, html)
+ def process_table(match):
+ table_open = match.group(1)
+ body = match.group(2)
+ table_close = match.group(3)
+ # 跳过带有 no-rowspan class 的表格
+ if 'no-rowspan' in table_open:
+ return match.group(0)
+ # 处理表格内的
+ body = TBODY_RE.sub(merge_tbody, body)
+ return table_open + body + table_close
+
+ return TABLE_RE.sub(process_table, html)
# ---------- Markdown → HTML ----------
@@ -597,21 +621,28 @@ def slugify(text):
def generate_toc(html):
- """从 HTML 中提取 H1-H4 标题,生成目录。"""
+ """从 HTML 中提取 H1-H4 标题,生成目录。
+ 锚点直接从标题已有的 id 属性读取,确保与 HTML 中的实际 ID 一致。"""
toc = []
- for m in re.finditer(r']*>(.*?)', html, re.DOTALL):
+ # 匹配带 id 属性的标题标签
+ for m in re.finditer(r']*\bid="([^"]*)"[^>]*>(.*?)', html, re.DOTALL):
level = int(m.group(1))
- text = re.sub(r'<.*?>', '', m.group(2)).strip()
- toc.append({"level": level, "text": text, "anchor": slugify(text)})
+ anchor = m.group(2)
+ text = re.sub(r'<.*?>', '', m.group(3)).strip()
+ toc.append({"level": level, "text": text, "anchor": anchor})
return toc
def inject_anchors(html):
- """为所有 H1-H4 标签注入 id 属性,使侧边栏目录可跳转。"""
+ """为所有 H1-H4 标签注入 id 属性,使侧边栏目录可跳转。
+ 如果标题已有 id 属性(如自定义渲染器预设的锚点),则保留原 id。"""
def repl(match):
level = match.group(1)
attrs = match.group(2)
inner = match.group(3)
+ # 保留已有的 id 属性(例如渲染器手动设置的锚点)
+ if re.search(r'\bid\s*=', attrs):
+ return f'{inner}'
anchor = slugify(re.sub(r'<.*?>', '', inner).strip())
return f'{inner}'
return re.sub(
diff --git a/doc_builder/renderers/render_ids_acq_map.py b/doc_builder/renderers/render_ids_acq_map.py
new file mode 100644
index 0000000..271c568
--- /dev/null
+++ b/doc_builder/renderers/render_ids_acq_map.py
@@ -0,0 +1,8 @@
+"""ACQ 通道模块基地址表渲染器。"""
+from render_ids import _load_ids, render_mapping_acq_table
+
+
+def render(filepath):
+ """返回 ACQ 通道模块基地址 HTML 表格(不含标题和描述)。"""
+ data = _load_ids(filepath)
+ return render_mapping_acq_table(data)
diff --git a/doc_builder/renderers/render_ids_awg_reg.py b/doc_builder/renderers/render_ids_awg_reg.py
new file mode 100644
index 0000000..1b36176
--- /dev/null
+++ b/doc_builder/renderers/render_ids_awg_reg.py
@@ -0,0 +1,8 @@
+"""AWG_REG 寄存器表渲染器。"""
+from render_ids import _load_ids, render_awg_reg_table
+
+
+def render(filepath):
+ """返回 AWG_REG 寄存器汇总表 + 位域明细 HTML(不含标题和描述)。"""
+ data = _load_ids(filepath)
+ return render_awg_reg_table(data)
diff --git a/doc_builder/renderers/render_ids_daq_reg.py b/doc_builder/renderers/render_ids_daq_reg.py
new file mode 100644
index 0000000..f8c65e3
--- /dev/null
+++ b/doc_builder/renderers/render_ids_daq_reg.py
@@ -0,0 +1,8 @@
+"""DAQ_REG 寄存器表渲染器。"""
+from render_ids import _load_ids, render_daq_reg_table
+
+
+def render(filepath):
+ """返回 DAQ_REG 寄存器汇总表 + 位域明细 HTML(不含标题和描述)。"""
+ data = _load_ids(filepath)
+ return render_daq_reg_table(data)
diff --git a/doc_builder/renderers/render_ids_exc_map.py b/doc_builder/renderers/render_ids_exc_map.py
new file mode 100644
index 0000000..c0466ff
--- /dev/null
+++ b/doc_builder/renderers/render_ids_exc_map.py
@@ -0,0 +1,8 @@
+"""EXC 通道模块基地址表渲染器。"""
+from render_ids import _load_ids, render_mapping_exc_table
+
+
+def render(filepath):
+ """返回 EXC 通道模块基地址 HTML 表格(不含标题和描述)。"""
+ data = _load_ids(filepath)
+ return render_mapping_exc_table(data)
diff --git a/project.yaml b/project.yaml
index d06cddc..b5587d6 100644
--- a/project.yaml
+++ b/project.yaml
@@ -25,4 +25,5 @@ chapters:
- 05-04-exc-waveform-store.md
- 05-05-exc-upconversion.md
- 05-06-pump-config.md
- - 05-07-exc-pipeline-demo.md
\ No newline at end of file
+ - 05-07-exc-pipeline-demo.md
+ - 80-appendix-ids-table.md
\ No newline at end of file