398 lines
26 KiB
Python
398 lines
26 KiB
Python
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 MHz(PS 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 System;PL 指 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_array;eeg_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、开关与 SPI;trans_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~int3:stim_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_v2:c64pro_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()
|