Files
BSI-901-fpga-software/tmp/fpga_doc2/build_document.py
T
2026-09-11 16:18:36 +08:00

398 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from copy import deepcopy
from pathlib import Path
import shutil
import re
from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt
from PIL import Image
ROOT = Path(r"D:\Project\spinal_stim")
TMP = ROOT / "pl" / "spinal_stim_v2" / "tmp" / "fpga_doc2"
TEMPLATE = TMP / "template.docx"
SYSTEM = ROOT / "详细设计" / "【SpinalStim】系统软件设计与调试 .docx"
OUTPUT = ROOT / "详细设计" / "spinal_stim_FPGA详细设计文档2.docx"
MEDIA = TMP / "system_media"
def set_cell_shading(cell, fill):
tc_pr = cell._tc.get_or_add_tcPr()
shd = tc_pr.find(qn("w:shd"))
if shd is None:
shd = OxmlElement("w:shd")
tc_pr.append(shd)
shd.set(qn("w:fill"), fill)
def repeat_table_header(row):
tr_pr = row._tr.get_or_add_trPr()
tbl_header = OxmlElement("w:tblHeader")
tbl_header.set(qn("w:val"), "true")
tr_pr.append(tbl_header)
def add_page_number(paragraph):
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = paragraph.add_run()
begin = OxmlElement("w:fldChar")
begin.set(qn("w:fldCharType"), "begin")
instr = OxmlElement("w:instrText")
instr.set(qn("xml:space"), "preserve")
instr.text = " PAGE "
end = OxmlElement("w:fldChar")
end.set(qn("w:fldCharType"), "end")
run._r.extend([begin, instr, end])
def clear_body(doc):
body = doc._element.body
for child in list(body):
if child.tag != qn("w:sectPr"):
body.remove(child)
def add_heading(doc, text, level=1):
style = f"Heading {min(level, 5)}"
# The retained template supplies multilevel numbering for Heading 1-5.
text = re.sub(r"^\d+(?:\.\d+)*\s+", "", text)
return doc.add_paragraph(text, style=style)
def add_body(doc, text, bold_lead=None):
p = doc.add_paragraph(style="Normal")
p.paragraph_format.first_line_indent = Cm(0.74)
p.paragraph_format.line_spacing = 1.25
if bold_lead and text.startswith(bold_lead):
p.add_run(bold_lead).bold = True
p.add_run(text[len(bold_lead):])
else:
p.add_run(text)
return p
def add_bullets(doc, items):
for item in items:
p = doc.add_paragraph(style="List Bullet" if "List Bullet" in [s.name for s in doc.styles] else "Normal")
if p.style.name == "Normal":
p.add_run("• ")
p.add_run(item)
def add_table(doc, headers, rows, widths=None, font_size=8.5):
table = doc.add_table(rows=1, cols=len(headers))
table.style = "Table Grid"
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = False
hdr = table.rows[0]
repeat_table_header(hdr)
for i, value in enumerate(headers):
cell = hdr.cells[i]
cell.text = str(value)
set_cell_shading(cell, "D9E2F3")
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
for run in cell.paragraphs[0].runs:
run.bold = True
run.font.size = Pt(font_size)
if widths:
cell.width = Cm(widths[i])
for row_data in rows:
cells = table.add_row().cells
for i, value in enumerate(row_data):
cells[i].text = str(value)
cells[i].vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
if widths:
cells[i].width = Cm(widths[i])
for p in cells[i].paragraphs:
p.paragraph_format.space_after = Pt(0)
for run in p.runs:
run.font.size = Pt(font_size)
return table
def add_caption(doc, text):
p = doc.add_paragraph(text, style="Caption")
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
return p
def copy_system_table(doc, source_table, title):
add_caption(doc, title)
headers = [c.text.replace("\n", " / ") for c in source_table.rows[0].cells]
rows = [[c.text.replace("\n", " / ") for c in r.cells] for r in source_table.rows[1:]]
widths = [1.0, 2.5, 1.1, 1.2, 2.6, 5.9, 3.0][:len(headers)]
return add_table(doc, headers, rows, widths=widths, font_size=7.2)
def main():
system_diagram = TMP / "system_diagram.png"
if not system_diagram.exists():
Image.open(MEDIA / "image2.jpeg").save(system_diagram)
shutil.copy2(TEMPLATE, OUTPUT)
doc = Document(OUTPUT)
src = Document(SYSTEM)
clear_body(doc)
sec = doc.sections[0]
sec.top_margin = Cm(2.2)
sec.bottom_margin = Cm(2.0)
sec.left_margin = Cm(2.2)
sec.right_margin = Cm(2.0)
header = sec.header
header.is_linked_to_previous = False
header.paragraphs[0].text = "SpinalStim FPGA 详细设计文档"
header.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.RIGHT
footer = sec.footer
footer.is_linked_to_previous = False
footer.paragraphs[0].clear()
add_page_number(footer.paragraphs[0])
# Cover
for _ in range(5):
doc.add_paragraph()
p = doc.add_paragraph(style="Title")
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p.add_run("SpinalStim FPGA 详细设计文档")
r.bold = True
r.font.size = Pt(28)
p2 = doc.add_paragraph()
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = p2.add_run("文档编号:spinal_stim_FPGA详细设计文档2")
r.font.size = Pt(14)
doc.add_paragraph()
meta = [
("硬件平台", "Xilinx Zynq-7000 / XC7Z020CLG400-2"),
("FPGA 顶层", "c64pro_top"),
("逻辑版本", "32'h20000005"),
("系统时钟", "100 MHzPS FCLK_CLK0"),
("工程工具", "Vivado 2023.1"),
("编制日期", "2026年8月"),
]
t = add_table(doc, ["项目", "内容"], meta, widths=[4.0, 10.0], font_size=10)
doc.add_page_break()
add_heading(doc, "目录", 1)
toc = [
"1 术语、定义与缩略语", "2 模块功能", "2.1 技术要求", "2.2 输入/输出接口要求",
"2.3 技术条件", "3 模块设计思想和设计准则", "4 模块设计", "4.1 逻辑层次结构",
"4.2 数据流、时钟与复位", "4.3 子模块详细设计", "4.4 FPGA 外设接口设计",
"4.5 模块寄存器说明", "5 模块测试", "6 其他主要设计问题", "附录A 代码文件与模块映射",
]
for item in toc:
doc.add_paragraph(item, style="toc 1" if " " in item[:3] else "Normal")
doc.add_page_break()
add_heading(doc, "1 术语、定义与缩略语", 1)
add_heading(doc, "1.1 术语、定义", 2)
add_body(doc, "PS 指 Zynq Processing SystemPL 指 Programmable Logic。本文档中的“采集包”指一次采样形成的通道数据单元,“帧”指由配置数量的小包、帧头、CRC 和尾部信息组成的 DMA 传输单元。")
add_heading(doc, "1.2 缩略语", 2)
add_table(doc, ["缩略语", "含义"], [
("AXI4-Stream", "片上流式数据接口,使用 TDATA/TVALID/TREADY/TLAST 握手"),
("BRAM", "Block RAM,本文用于 PS-PL 配置与状态映射"),
("EEG/EMG", "脑电/肌电采集数据"), ("SPI", "串行外设接口"),
("DMA", "直接存储器访问"), ("CRC16", "数据帧循环冗余校验"),
("DRDY", "ADC 数据就绪信号"), ("ZCHECK", "电极阻抗测量链路"),
], widths=[3.0, 11.0])
add_heading(doc, "2 模块功能", 1)
add_heading(doc, "2.1 技术要求", 2)
add_body(doc, "SpinalStim 采用 Zynq PS+PL 架构。PL 在统一 100 MHz 时钟域内完成 EEG/EMG 采集、刺激相关控制、实时电流监测、通道选择、数据封包与 AXI DMA 上传;PS 通过 BRAM、AXI GPIO、AXI SPI 和 DMA 完成配置、状态读取和数据搬运。")
add_bullets(doc, [
"支持 4 片 EEG 前端和 1 片差分/EMG 前端的配置、采样与数据整理。",
"支持 AD7689 电流采样、通道映射、正/负/总电流阈值判断和故障锁存。",
"支持 EEG、EMG 单独上传或合帧上传,并根据通道选择形成可变长度数据帧。",
"支持 AD5941 阻抗测量链路、CSI040 刺激 SPI、模拟开关和电源/指示灯控制。",
"提供中断、BRAM 寄存器和 AXI4-Stream 数据通路供 PS 软件使用。",
])
add_heading(doc, "2.2 系统框图与总体原理", 2)
doc.add_picture(str(system_diagram), width=Cm(16.0))
add_caption(doc, "图 2-1 系统硬件/控制框图(复制自《【SpinalStim】系统软件设计与调试》)")
add_body(doc, src.paragraphs[36].text)
add_body(doc, src.paragraphs[37].text)
add_body(doc, src.paragraphs[39].text)
add_body(doc, "代码实现以 c64pro_top 为集成顶层:design_a9_1_wrapper 提供时钟、复位、BRAM、GPIO、SPI、DMA 和中断连接;采集链路输出先进入 upload_top 封包,再由 eeg_emg_upload 按选择配置合并,最终以 8 bit AXI4-Stream 写入 PS 侧 DMA。")
add_heading(doc, "2.3 输入/输出接口要求", 2)
add_table(doc, ["接口组", "方向", "主要信号", "电路/器件", "功能"], [
("EEG", "PL↔AFE", "CS[3:0], SCLK[1:0], MOSI[1:0], MISO[1:0], DRDY#[1:0], START[1:0]", "ADS1299/AFE968", "4片、32通道单端采集,两组共享 SPI 时钟/数据"),
("EMG/差分", "PL↔AFE", "CS, SCLK, MOSI, MISO, DRDY#, START, RESET#", "ADS1299", "8通道差分采集与原始数据上传"),
("电流监测", "PL↔ADC", "CNV, SCLK, MOSI, MISO", "AD7689", "刺激回路电流采样与阈值保护"),
("阻抗测量", "PS/PL↔AFE", "SPI2, RST_AD5941#, ZIO_R0, CHANN_SEL/EN, ELEC_MUX_IN", "AD5941+MUX36S16+ADG1212", "电极选择和阻抗测量"),
("刺激控制", "PS/PL↔刺激芯片", "STIM_SPI, GPIO[2:0], 400 kHz CLK", "CSI040/隔离器", "寄存器访问、触发/中断、刺激时钟"),
("上传", "PL→PS", "AXIS TDATA[7:0], TVALID, TREADY, TLAST", "AXI DMA", "EEG/EMG 帧和透明数据搬运"),
("电源/灯", "PL→板级", "REC_PWR_EN, STIM_PWR_EN, MUX_PWR_EN, PUDC, LED_*", "电源芯片/LED", "上电域控制与状态指示"),
], widths=[2.0, 1.8, 5.0, 3.2, 4.0], font_size=7.8)
add_heading(doc, "2.4 技术条件", 2)
add_body(doc, "PL 主时钟为 100 MHz,周期 10 ns,由 PS FCLK_CLK0 输出。顶层参数 CLK_FREQ=100、CLK_PERIOD=10;全局低有效复位 rst_n 由 PS 侧输出。外设串行时钟由主时钟计数分频产生,接口工作频率必须同时满足 RTL 分频参数、隔离器传播延迟与器件数据手册时序。")
add_body(doc, "工程器件为 XC7Z020CLG400-2,逻辑版本参数 VERSION=32'h20000005。原理图版本为 SPINAL_STIM_REC_V001A8。")
add_heading(doc, "3 模块设计思想和设计准则", 1)
add_heading(doc, "3.1 设计思想", 2)
add_body(doc, "设计按“配置面”和“数据面”分离:PS 通过 BRAM/AXI 外设写入低速配置,PL 以状态机和流接口执行确定性采集、封包及保护;高速数据只通过 AXI4-Stream/DMA 传输。各采集前端先输出统一流格式,再在上传层完成通道筛选和帧组合,从而降低 PS 实时负担。")
add_heading(doc, "3.2 设计准则", 2)
add_bullets(doc, [
"所有配置写入以 bram_en 且 bram_we=4'hF 为有效写,以 bram_we=0 为有效读。",
"外部异步 DRDY/MISO/中断信号在接口状态机边界采样;跨域 FIFO/AXI IP 负责 PS/PL 数据域隔离。",
"停止采样或复位必须清除包计数、FIFO 读使能、TLAST 和 CRC 中间状态,防止跨帧污染。",
"电流故障状态与实时值分离,软件通过 clear_int 明确清除锁存状态。",
"板级电源和模拟开关电源按软件顺序启用;采集/刺激功能未准备前保持关闭。",
])
add_heading(doc, "4 模块设计", 1)
add_heading(doc, "4.1 模块的逻辑设计", 2)
add_heading(doc, "4.1.1 逻辑层次结构划分", 3)
hierarchy = [
("c64pro_top", "系统集成顶层;连接 Zynq PS、采集、上传、电源、LED、阻抗与刺激接口"),
("design_a9_1_wrapper", "Vivado Block Design 封装;提供 FCLK/复位、BRAM 控制器、AXI GPIO/SPI、DMA 与中断"),
("eeg_top / emg_top", "ADS1299 配置、SPI 接口、采样结果汇聚与采集流输出"),
("upload_top", "帧头、通道映射/选择、包计数、CRC 和 TLAST 生成"),
("eeg_emg_upload", "EEG/EMG 选择、FIFO 缓冲、去头/去尾、CRC 重算与合帧"),
("cur_mon_top", "AD7689 采样、通道映射、滤波/统计、阈值比较、故障状态和透明上传"),
("stim_config", "EEG/EMG 合并上传控制寄存器"),
("rhs2116_top(工程保留)", "RHS2116 刺激波形、SPI 分发、监控与事件采集;当前 c64pro_top 的对应板级端口被注释"),
("switch / trigger / accel", "通道开关、触发检测、LIS2DW12 加速度;部分端口在当前顶层配置中关闭"),
("led_ctrl / utils", "LED 状态、PWM、呼吸灯、消抖和运行时间等通用逻辑"),
]
add_table(doc, ["模块", "职责"], hierarchy, widths=[4.5, 10.5], font_size=8.5)
add_heading(doc, "4.1.2 模块的数据流", 3)
add_body(doc, "EEG 路径:ADS1299 DRDY# → ads1299_spi/ads1299_if → ads1299_mod/arrange → eeg_top → upload_top → eeg_emg_upload → AXI DMA。EMG 路径与 EEG 类似,但使用单片差分前端;合帧模式下去除 EMG 帧头和 EEG 原尾部,在组合数据末尾重新生成 CRC/TLAST。")
add_body(doc, "电流路径:AD7689 CNV/SPI → ad7689_dual_if → acq_collect → cur_monitor → 电流实时值/故障寄存器;同时 filter_i_fifoout 形成 32 bit 透明流,每 1000 个字产生 TLAST,经 axi_dma_2 上传用于阻抗/诊断处理。")
add_heading(doc, "4.1.3 模块的时钟说明", 3)
add_table(doc, ["时钟/周期", "来源", "使用范围", "说明"], [
("clk / 10 ns", "PS FCLK_CLK0", "全部自研 RTL", "100 MHz 主同步时钟"),
("bram_*_clk", "PS/AXI BRAM Controller", "各 config 模块", "BRAM 信号进入模块后按 clk 条件判定读写;需保持控制器配置一致"),
("ADS1299 SCLK", "ads1299_spi 分频", "EEG/EMG 前端", "SPI_CLK_DIV=3;实际边沿关系以模块状态机和器件模式为准"),
("AD7689 SCK/CNV", "ad7689_dual_ctrl/if", "电流采样", "由 100 MHz 计数控制转换与串行读取"),
("stim_clk400k", "顶层计数器", "刺激芯片", "100 MHz 分频得到约 400 kHz"),
], widths=[3.0, 3.2, 4.2, 5.1])
add_heading(doc, "4.1.4 模块的复位说明", 3)
add_body(doc, "rst_n 为系统低有效异步复位,来自 PS。各模块采用 always @(posedge clk or negedge rst_n) 模式:复位沿异步进入,释放后随 clk 工作。BRAM 控制器另有 bram_rst 信号,但多数自研配置寄存器的最终复位状态由 rst_n 决定。外设专用复位(emg_ads1299_reset_n、rst_ad5941、刺激 GPIO)由 PL 或 PS GPIO 单独控制,不应替代全局逻辑复位。")
add_heading(doc, "4.2 子模块详细设计", 2)
modules = [
("4.2.1 eeg_top / eeg_config", "eeg_config 解析 13 bit BRAM 地址,输出 eeg_er/eeg_cr/eeg_tr_array,并回读 eeg_rr_arrayeeg_top 以两组 SPI 总线管理四片采集前端。send_cs_n 和 send_length 从 eeg_cr 分组解析,配置事务和连续采样事务复用 SPI 接口。DRDY 到达后读取每片 8 通道、24 bit 数据,整理为统一 AXI 流。"),
("4.2.2 emg_top / emg_config", "EMG 逻辑复用 ADS1299 采集结构,面向单片 8 通道差分前端。配置表与 EEG 独立,BRAM 基地址为 0x4400_0000;当前 send_length 字段支持 4 字节配置格式。顶层同时输出 START 和 RESET#。"),
("4.2.3 upload_top / upload_config", "upload_config 保存 bram_addr_sel、upload_round_qty_set、pre_message_length、reset_package_id、package_overtime_ms、192 bit pre_message、512 bit ch_mapping2pc 和 64 bit upload_ch。upload_sort 按映射/选择保留通道,upload_protocol_* 生成帧头、包计数、CRC 和 TLAST。"),
("4.2.4 eeg_emg_upload", "模块接收两个 8 bit 流。eeg_emg_sel=01/10 时透传单路;=11 时先输出 EEG 去尾数据,再从 EMG FIFO 读取并跳过 85 字节帧头,使用配置的 emg_send_package_num 控制长度,在组合尾部插入重算 CRC。stop_sample 清零计数和读状态。"),
("4.2.5 cur_mon_top / cur_monitor", "cur_mon_config 提供采样使能、通道映射及阈值;ad7689_dual_ctrl 产生转换/采集节拍,ad7689_dual_if 完成串行读取,acq_collect 聚合数据,cur_monitor 计算通道、正向、负向和总电流状态。故障状态、实时值及各通道故障值可由 PS 回读。"),
("4.2.6 stim_config", "该模块不是刺激波形发生器,而是上传组合的全局控制寄存器:选择 EEG/EMG、设置两路帧字节数、选择差分打包或 8 通道原始数据。只读测试常数为 0x12345678。"),
("4.2.7 RHS2116 刺激逻辑", "rhs2116_top 在工程中保留完整实现:transmit_config 配置刺激、DDS、开关与 SPItrans_timer 产生更新周期;sine_gen 生成 16 路波形;trans_distribute/transmit_if 向多片 RHS2116 分发 32 bit SPI 命令;stim_monitor 和 stim_event_collect 形成监控/事件数据。当前 c64pro_top 中 RHS2116 板级端口与实例路径需以实际综合配置为准。"),
("4.2.8 LED、电源与辅助模块", "led_ctrl 根据 PS 命令、刺激状态和系统状态产生 LED;顶层 GPIO 输出 REC_PWR_EN、STIM_PWR_EN、MUX_PWR_EN、PUDC。utils 目录提供 breath_gen、pwm_gen、debouncing 和 run_time 等复用模块。"),
]
for title, body in modules:
add_heading(doc, title, 3)
add_body(doc, body)
add_heading(doc, "4.3 FPGA 外设接口设计", 2)
add_heading(doc, "4.3.1 ADS1299/AFE968 采集接口", 3)
add_body(doc, "原理图第 8~9 页显示 EEG 单端链路由 4 片 ADS1299/AFE968 构成,REC1/REC3 两组分别覆盖 ELEC1~16、ELEC17~32;差分链路使用独立 ADS1299,提供 INP/INN、START、DRDY#、CS#、SCLK、DIN/DOUT 与 RESET#。FPGA 在 DRDY# 有效后启动连续 24 bit 通道读取;CS# 为片选,SCLK/MOSI/MISO 为 SPI Mode 时序信号。START 用于同步转换,RESET# 在配置前保持有效并满足器件复位恢复时间。")
add_heading(doc, "4.3.2 AD7689 电流采样接口", 3)
add_body(doc, "AD7689 接口包含 CNV、SCK、SDI(MOSI) 和 SDO(MISO)。CNV 上升沿启动转换,接口状态机等待转换窗口后串行读取结果。原理图将该接口同时接入 PS AXI SPI/PL 控制路径,顶层端口定义为 inout;系统集成时必须保证任一时刻只有一个主控驱动,避免总线冲突。")
add_heading(doc, "4.3.3 AD5941 阻抗接口与通道选择", 3)
add_body(doc, "阻抗链路由 AD5941、两片 MUX36S16 和 ADG1212 组成。CHANN_SEL[3:0]/CHANN_EN[1:0] 选择电极通道,ELEC_MUX_IN[3:0] 选择 ZCHECK、ELEC_REF、ELEC_BIAS 与 ZCHK_IN 的连接,SPI2 完成 AD5941 寄存器访问,RST_AD5941# 完成硬件复位,ZIO_R0 为辅助控制。启用测量前先打开 MUX_PWR_EN,再配置开关,最后释放 AD5941 复位。")
add_heading(doc, "4.3.4 CSI040 刺激与隔离 SPI", 3)
add_body(doc, "PS 通过 STIM_SPI 对 CSI040 读写,gpio[2:0] 分别承载 rstn_stim、stim_trig、stim_int(具体方向按 Block Design GPIO 配置)。顶层另输出约 400 kHz stim_clk400k。原理图中隔离器 NSI8241/NSI8240 位于控制器与刺激域之间,SPI 频率和采样边沿应预留隔离器传播延迟及通道偏差裕量。")
add_heading(doc, "4.3.5 USB 与板级控制", 3)
doc.add_picture(str(MEDIA / "image3.png"), width=Cm(13.5))
add_caption(doc, "图 4-1 USB 隔离接口(复制自系统设计与调试文档)")
add_body(doc, "USB 2.0 由 PS/USB 控制器与板级隔离/收发电路实现,PL 不直接处理 USB PHY 协议。PL 侧的职责是通过 DMA 将帧写入 PS 内存,并提供 LED_USB 等状态输出。系统调试记录指出 USB 隔离器 VCC1 必须有正确电源输入,板级装配和上电检查应纳入接口验证。")
add_heading(doc, "4.4 PS-PL 通信与地址映射", 2)
add_table(doc, ["功能", "PS 地址范围", "PL 端模块/接口"], [
("EEG_CONFIG", "0x4200_0000~0x4200_1FFF", "eeg_config / BRAM_CTRL_1"),
("EMG_CONFIG", "0x4400_0000~0x4400_1FFF", "emg_config / BRAM_CTRL_2"),
("EEG_UPLOAD_CONFIG", "0x4800_0000~0x4800_1FFF", "upload_config / BRAM_CTRL_4"),
("EMG_UPLOAD_CONFIG", "0x4A00_0000~0x4A00_1FFF", "upload_config / BRAM_CTRL_5"),
("CUR_CONFIG", "0x4C00_0000~0x4C00_1FFF", "cur_mon_config / BRAM_CTRL_6"),
("GLOBAL/STIM_CONFIG", "0x4E00_0000~0x4E00_1FFF", "stim_config / BRAM_CTRL_7"),
("透明数据 DMA", "0x4042_0000~0x4042_FFFF", "axi_dma_2"),
], widths=[4.0, 5.0, 6.0])
add_body(doc, "中断由顶层连接到 PS int0~int3stim_int、eeg_int、emg_int、accel_int。数据 DMA 使用 AXI4-Stream 握手;只有 TVALID 与 TREADY 同时为 1 时数据传输,TLAST 标记一帧结束。")
add_heading(doc, "4.5 模块寄存器说明", 2)
add_body(doc, "本节寄存器定义复制自《【SpinalStim】系统软件设计与调试》,并与当前 RTL 中 stim_config、eeg_config、upload_config、cur_mon_config 的地址常量核对。表内保留源文档的方向、位宽、地址和备注。")
titles = [
"表 4-1 EEG 数据采集寄存器", "表 4-2 EEG 数据上传寄存器",
"表 4-3 EMG 数据采集寄存器", "表 4-4 EMG 数据上传寄存器",
"表 4-5 电流配置与状态寄存器", "表 4-6 EEG/EMG 合并上传配置寄存器",
"表 4-7 AXI SPI/GPIO 地址映射", "表 4-8 EEG/EMG 组合包字节计算",
]
for i, title in enumerate(titles, start=1):
copy_system_table(doc, src.tables[i], title)
doc.add_paragraph()
add_heading(doc, "4.5.1 包长度与带宽约束", 3)
for idx in range(59, 71):
text = src.paragraphs[idx].text.strip()
if text:
add_body(doc, text)
add_heading(doc, "5 模块的测试", 1)
add_heading(doc, "5.1 RTL 仿真与检查", 2)
add_bullets(doc, [
"工程包含 eeg_top、ads1299_spi、cur_mon_top、ad7689_dual_if、channel_switch、trigger_top、RHS2116 transmit/arrange 等 testbench。",
"寄存器测试应覆盖测试常数、读写回环、非法地址、字节写使能、复位默认值和连续访问。",
"采集接口测试应覆盖 DRDY 临界时刻、SPI 配置/采样切换、TREADY 背压、停止采样和复位中断。",
"合帧测试应覆盖 eeg_emg_sel 四种组合、不同包长、FIFO 空/满、CRC 重算和 TLAST 唯一性。",
"电流监测测试应注入单通道、正向、负向和总电流越限,验证锁存、回读和 clear_int。",
])
add_heading(doc, "5.2 板级联调", 2)
add_body(doc, "板级验证按电源→时钟/复位→SPI 读写→ADC 数据就绪→DMA 帧→刺激/阻抗的顺序执行。使用 ILA 观察 DRDY、SPI、AXI TVALID/TREADY/TLAST、FIFO 状态和中断。原理图接口测试需同时核对电平域、隔离器供电、模拟电源和参考/偏置连接。")
add_heading(doc, "6 其他主要设计问题", 1)
add_bullets(doc, [
"当前顶层存在若干已注释功能端口(RHS2116、ADG1414、LIS2DW12、触发等),文档将其标为工程保留功能;发布 bitstream 前应以综合后的顶层端口和 Block Design 为准。",
"部分 RTL 中文注释存在编码损坏,不影响综合但降低维护性;建议统一为 UTF-8 并通过代码审查修复。",
"顶层若使用 inout 连接 PS AXI SPI 与 PL 控制器,必须有明确的三态/主控互斥策略。",
"eeg_emg_upload 中存在固定 85 字节帧头、14 字节尾部等协议常数,软件配置与协议版本必须同步。",
"寄存器源文档中个别地址书写大小写/前缀不一致(如 x4400_0000),本文地址总表已规范为 0x4400_0000,寄存器偏移仍保留源表。",
])
add_heading(doc, "附录A 代码文件与模块映射", 1)
add_table(doc, ["目录", "主要文件", "设计职责"], [
("rtl/", "c64pro_top.v, stim_config.v, led_ctrl.v", "顶层、全局配置与状态控制"),
("rtl/eeg", "eeg_top.v, eeg_config.v, ads1299_*.v, eeg_upload.v", "EEG 采集与整理"),
("rtl/emg", "emg_top.v, emg_config.v", "差分/EMG 采集"),
("rtl/upload", "upload_top.v, upload_config.v, upload_sort.v, upload_protocol_*.v, eeg_emg_upload.v", "通道选择、协议封包与合帧"),
("rtl/current_monitor", "cur_mon_top.v, cur_mon_config.v, ad7689_*.v, cur_monitor.v", "电流采样、处理与保护"),
("rtl/rhs2116", "rhs2116_top.v, transmit_*.v, stim_*.v, sine_gen.v", "刺激配置、波形、传输与监控"),
("rtl/switch", "channel_switch.v, adg1414_if.v", "刺激/电极通道开关"),
("rtl/trigger", "trigger_top.v, trigger_detection.v", "外部触发采集与编码"),
("rtl/accel", "accel_top.v, accel_config.v, lis2dw12.v", "加速度计配置与采集"),
("rtl/utils", "spi_full_duplex_master.v, pwm_gen.v, breath_gen.v, debouncing.v", "通用 SPI、PWM、LED 与消抖"),
], widths=[3.0, 6.5, 6.0], font_size=8)
add_heading(doc, "附录B 参考资料", 1)
add_bullets(doc, [
"《DTCD PRO-S FPGA详细设计》:章节结构与版式模板。",
"《【SpinalStim】系统软件设计与调试》:系统框图、总体原理、寄存器与包长度说明。",
"《SPINAL_STIM_REC_V001A8》:板级接口、器件连接、电源域与网络名称。",
"工程 spinal_stim_v2c64pro_top 及各 RTL 子目录的当前实现。",
"器件接口依据:ADS1299/AFE968、AD7689、AD5941、CSI040、LIS2DW12、ADG1212、MUX36S16、NSI824x 对应数据手册。具体电气极限和最小时序以量产采用的器件版本数据手册为准。",
])
# Update fields on open and keep rows together where practical.
settings = doc.settings._element
update = settings.find(qn("w:updateFields"))
if update is None:
update = OxmlElement("w:updateFields")
settings.append(update)
update.set(qn("w:val"), "true")
doc.core_properties.title = "SpinalStim FPGA 详细设计文档2"
doc.core_properties.subject = "SpinalStim FPGA detailed design"
doc.core_properties.keywords = "SpinalStim, FPGA, Zynq, ADS1299, AD7689, AD5941"
doc.save(OUTPUT)
print(OUTPUT)
if __name__ == "__main__":
main()