# CSI040 控制模块详细设计文档 ## 1. 文档范围 本文档描述 CSI040 ASIC 控制模块的 RTL 设计,覆盖以下文件: - `c64_pro.srcs/sources_1/rtl/csi040/csi040_ctrl.v` - `c64_pro.srcs/sources_1/rtl/csi040/csi040_spi_master.v` - `c64_pro.srcs/sources_1/rtl/csi040/csi040_ctrl_tb.v` 该模块在 FPGA/PL 侧提供一组 BRAM 风格的控制寄存器。上位逻辑或 PS 通过写这些寄存器来触发 CSI040 ASIC 的 SPI 读写、通道参数配置、模式解锁、触发控制等操作。 ## 2. 总体结构 ```text PS / 软件 | | BRAM 风格寄存器访问 v +------------------+ | csi040_ctrl | | | | 软件寄存器映射 | | shadow 寄存器 | | seq 序列控制器 | | 状态/错误锁存 | +--------+---------+ | | cmd_valid / cmd_ready / cmd_addr / cmd_wdata / cmd_len v +---------------------+ | csi040_spi_master | | | | SPI 组帧 | | SCK/CSN/SI/SO 时序 | +----------+----------+ | | asic_csn / asic_sck / asic_si / asic_so v CSI040 ASIC ``` `csi040_ctrl` 是上层可见的控制模块,负责接收 BRAM 写操作、保存软件配置、发起高层命令序列,并把命令转换为 SPI master 的抽象命令接口。 `csi040_spi_master` 是底层 SPI 引擎,负责把 opcode、ASIC 地址和数据字节按 SPI 时序移出,并在读操作时采样 `asic_so`。 ## 3. 模块参数 ### 3.1 `csi040_ctrl` | 参数 | 默认值 | 说明 | | --- | ---: | --- | | `VERSION` | `32'h04040001` | 模块版本号,读 BRAM 地址 `0x0000` 返回。 | | `SPI_CLK_DIV` | `50` | SPI SCK 半周期分频系数,`f_sck = f_clk / (2 * SPI_CLK_DIV)`。 | | `REG_ADDR_WIDTH` | `16` | ASIC 内部寄存器地址宽度。 | | `SPI_ADDR_BYTES` | `2` | SPI 帧中的地址字节数。 | | `MAX_BURST_BYTES` | `4` | 单次 SPI 最大数据字节数。 | | `SPI_OPCODE_WRITE` | `8'h00` | 默认写 opcode;testbench 中重载为 `8'h02`。 | | `SPI_OPCODE_READ` | `8'h80` | 默认读 opcode;testbench 中重载为 `8'h03`。 | | `CSI_REG_*` | 多个地址 | CSI040 ASIC 内部寄存器地址参数。 | ### 3.2 `csi040_spi_master` | 参数 | 默认值 | 说明 | | --- | ---: | --- | | `CLK_DIV` | `50` | SCK 半周期对应的 `clk` 周期数。 | | `REG_ADDR_WIDTH` | `16` | `cmd_addr` 地址宽度。 | | `ADDR_BYTES` | `2` | SPI 地址字段字节数。 | | `MAX_BURST_BYTES` | `4` | 最大 payload 字节数。 | | `OPCODE_WRITE` | `8'h00` | SPI 写命令 opcode。 | | `OPCODE_READ` | `8'h80` | SPI 读命令 opcode。 | ## 4. 顶层接口 ### 4.1 时钟复位 | 信号 | 方向 | 说明 | | --- | --- | --- | | `clk` | input | 主时钟,当前所有寄存器逻辑都使用该时钟。 | | `rst_n` | input | 异步低有效复位。 | | `bram_clk` | input | 接口保留信号,当前 RTL 未使用。 | | `bram_rst` | input | 接口保留信号,当前 RTL 未使用。 | 注意:虽然接口里有 `bram_clk`,但当前实现没有使用它。因此 BRAM 控制访问应与 `clk` 同步。如果后续接真实 AXI BRAM Controller 且时钟不同,需要补 CDC 或改成 `bram_clk` 域逻辑。 ### 4.2 BRAM 风格控制口 | 信号 | 方向 | 位宽 | 说明 | | --- | --- | ---: | --- | | `bram_addr` | input | 13 | BRAM 字节地址,当前使用 4 字节对齐地址。 | | `bram_din` | input | 32 | 写数据。 | | `bram_dout` | output reg | 32 | 读数据,在读访问时更新。 | | `bram_en` | input | 1 | BRAM 访问使能。 | | `bram_we` | input | 4 | 写使能。当前仅识别 `4'hF` 为写、`4'h0` 为读。 | RTL 中访问判定为: ```verilog assign bram_wr_valid = bram_en && (bram_we == 4'hF); assign bram_rd_valid = bram_en && (bram_we == 4'h0); ``` 因此当前模块不支持 byte enable 局部写。如果软件侧产生 `bram_we = 4'h1/4'h3` 之类的写操作,该写不会被模块识别。 ### 4.3 状态输出 | 信号 | 方向 | 说明 | | --- | --- | --- | | `busy` | output | 模块忙标志。只要高层序列、SPI master 或待发 SPI 命令任一处于忙状态,该信号为高。 | | `spi_done` | output | SPI master 完成脉冲,持续 1 个 `clk` 周期。 | | `error_busy` | output reg | 忙时启动新命令的粘滞错误标志,需要软件写 `STATUS[1]` 清除。 | ### 4.4 ASIC 侧引脚 | 信号 | 方向 | 说明 | | --- | --- | --- | | `asic_rst_n` | output reg | ASIC 复位控制,软件通过 `PIN_CTRL[0]` 控制,复位默认值为 1。 | | `asic_trig` | output reg | ASIC 外部触发引脚,软件通过 `PIN_CTRL[1]` 控制,复位默认值为 0。 | | `asic_csn` | output | SPI 片选,低有效。 | | `asic_sck` | output | SPI 时钟,空闲为低。 | | `asic_si` | output | SPI master 输出到 ASIC 的串行数据。 | | `asic_so` | input | ASIC 输出到 SPI master 的串行数据。 | ## 5. BRAM 寄存器映射 所有地址均为字节地址。 | 地址 | 名称 | 访问 | 说明 | | ---: | --- | --- | --- | | `13'h0000` | `VERSION` | R | 读取模块版本号 `VERSION`。 | | `13'h0004` | `STATUS` | R/W1C | 读取模块状态;写 bit0 清 `done_latched`,写 bit1 清 `error_busy`。 | | `13'h0008` | `SPI_ADDR` | R/W | Direct SPI 访问的 ASIC 地址。 | | `13'h000C` | `SPI_WDATA` | R/W | Direct SPI 写数据。payload 第 1 字节来自 `bram_din[7:0]`。 | | `13'h0010` | `SPI_CTRL` | W | 启动 Direct SPI 访问。`bit0=write`,`bits[15:8]=length`。 | | `13'h0014` | `SPI_RDATA` | R | 最近一次 SPI 读回数据。 | | `13'h0018` | `PIN_CTRL` | R/W | `bit0=asic_rst_n`,`bit1=asic_trig`。 | | `13'h001C` | `LAST_ADDR` | R | 最近一次完成 SPI 操作的 ASIC 地址。 | | `13'h0020` | `CONTROL` | R/W | 更新 `control_shadow`,并写 ASIC 控制寄存器 `0x0002`。 | | `13'h0024` | `CH_ENABLE` | R/W | 更新 `ch_enable_shadow`,并写 ASIC 通道使能寄存器 `0x0007/0x0008`。 | | `13'h0028` | `TRIGGER` | W | 写 ASIC 触发寄存器 `0x0110 = 8'hAA`。 | | `13'h002C` | `SEQ_CMD` | W | 启动特殊序列:DCS unlock、Test unlock、mode exit、控制寄存器快捷写。 | | `13'h0040` | `CH_INDEX` | R/W | 选择后续通道参数写入的 channel index。 | | `13'h0044` | `CH_MISC` | W | 写当前 channel 的 offset `0x0`。 | | `13'h0048` | `CH_CUR_A` | W | 写当前 channel 的 offset `0x1`。 | | `13'h004C` | `CH_CUR_B` | W | 写当前 channel 的 offset `0x2`。 | | `13'h0050` | `CH_CUR_C` | W | 写当前 channel 的 offset `0x3`。 | | `13'h0054` | `CH_CUR_D` | W | 写当前 channel 的 offset `0x4`。 | | `13'h0058` | `CH_STIM_PW` | W | 写当前 channel 的 offset `0x5`。 | | `13'h005C` | `CH_DZ0` | W | 写当前 channel 的 offset `0x6`。 | | `13'h0060` | `CH_DZ1` | W | 写当前 channel 的 offset `0x7`。 | | `13'h0064` | `CH_SKIP_AB` | W | 写当前 channel 的 offset `0x8`。 | | `13'h0068` | `CH_SKIP_CD` | W | 写当前 channel 的 offset `0x9`。 | | `13'h006C` | `CH_STIM_NUM` | W | 写当前 channel 的 offset `0xA`。 | | `13'h0070` | `CH_DZ2` | W | 写当前 channel 的 offset `0xB`。 | | `13'h0074` | `CH_THERAPY_DELAY` | W | 写当前 channel 的 offset `0xC`。 | | `13'h0078` | `CH_THERAPY_NUM` | W | 写当前 channel 的 offset `0xD`。 | | `13'h007C` | `CH_DZ3` | W | 写当前 channel 的 offset `0xE`。 | | `13'h0080` | `CH_MASTER_NUM` | W | 写当前 channel 的 offset `0xF`。 | ### 5.1 `STATUS` 字段 RTL 返回表达式为: ```verilog bram_dout <= {16'd0, seq_id, seq_step, error_busy, done_latched, spi_cmd_ready, ctrl_busy}; ``` 该拼接宽度为 26 bit,赋值给 32 bit 时左侧补 0。因此软件侧实际可按低位理解: | 位 | 名称 | 说明 | | ---: | --- | --- | | `[5]` | `ctrl_busy` | 当前模块忙。 | | `[6]` | `spi_cmd_ready` | SPI master 是否空闲可接收新命令。 | | `[7]` | `done_latched` | 最近至少完成过一次 SPI 操作,写 `STATUS[0]=1` 清除。 | | `[8]` | `error_busy` | 忙时写命令产生的错误,写 `STATUS[1]=1` 清除。 | | `[11:9]` | `seq_step` | 当前高层序列步骤。 | | `[15:12]` | `seq_id` | 当前高层序列编号。 | | 其他 | 0 | 保留。 | ### 5.2 状态清除 写 `BRAM_ADDR_STATUS` 时: | 写入位 | 行为 | | ---: | --- | | `bram_din[0] = 1` | 清除 `done_latched`。 | | `bram_din[1] = 1` | 清除 `error_busy`。 | ## 6. 高层命令序列设计 模块内部用 `seq_id + seq_step + seq_active` 控制多拍或多 SPI 帧操作。 | `seq_id` | 名称 | 触发来源 | | ---: | --- | --- | | `4'h0` | `SEQ_IDLE` | 空闲。 | | `4'h1` | `SEQ_WRITE_CONTROL` | 写 `CONTROL`,或 `SEQ_CMD[3]/[4]`。 | | `4'h2` | `SEQ_CH_ENABLE` | 写 `CH_ENABLE`。 | | `4'h3` | `SEQ_TRIGGER` | 写 `TRIGGER`。 | | `4'h4` | `SEQ_DCS_UNLOCK` | 写 `SEQ_CMD[0]`。 | | `4'h5` | `SEQ_TEST_UNLOCK` | 写 `SEQ_CMD[1]`。 | | `4'h6` | `SEQ_MODE_EXIT` | 写 `SEQ_CMD[2]`。 | | `4'h7` | `SEQ_CH_WRITE` | 写任意通道参数寄存器。 | | `4'h8` | `SEQ_DIRECT` | 写 `SPI_CTRL`。 | ### 6.1 Busy 机制 模块忙信号定义: ```verilog ctrl_busy = seq_active || spi_busy || spi_cmd_valid; busy = ctrl_busy; ``` 对所有会启动 SPI 的 BRAM 写操作,RTL 都先检查 `!ctrl_busy`。若忙时写入命令寄存器: - 不启动新命令; - `error_busy <= 1'b1`; - 该错误保持到软件写 `STATUS[1]` 清除。 推荐软件流程: 1. 轮询 `busy == 0` 或 `STATUS[5] == 0`。 2. 写命令寄存器。 3. 等待 `done_latched == 1` 或等待固定时间。 4. 如有读操作,读取 `SPI_RDATA`。 5. 写 `STATUS[0] = 1` 清除 done 标志。 ### 6.2 写控制寄存器 写 `BRAM_ADDR_CONTROL`: 1. `control_shadow <= bram_din[7:0]` 2. 启动 `SEQ_WRITE_CONTROL` 3. 产生 1 个 SPI write: ```text ASIC addr = CSI_REG_CONTROL = 0x0002 data = control_shadow length = 1 byte ``` testbench 中写: ```text write_bram(13'h0020, 32'h00000010) ``` 期望 SPI 帧: ```text 0x02_0002_10 ``` ### 6.3 写通道使能 写 `BRAM_ADDR_CH_ENABLE`: 1. `ch_enable_shadow <= bram_din[15:0]` 2. 启动 `SEQ_CH_ENABLE` 3. 分两步写 ASIC: ```text seq_step 0: addr 0x0007, data ch_enable_shadow[7:0] seq_step 1: addr 0x0008, data ch_enable_shadow[15:8] ``` testbench 中写 `0xA55A`,期望两帧: ```text 0x02_0007_5A 0x02_0008_A5 ``` ### 6.4 写触发命令 写 `BRAM_ADDR_TRIGGER` 启动 `SEQ_TRIGGER`,SPI 写: ```text ASIC addr = CSI_REG_STIM_TRIGGER = 0x0110 data = 8'hAA ``` 注意:该 SPI trigger 与 `asic_trig` 引脚是两个不同路径。`asic_trig` 只受 `PIN_CTRL[1]` 控制。 ### 6.5 特殊序列 `SEQ_CMD` 写 `BRAM_ADDR_SEQ_CMD` 时,RTL 按 bit 优先级解析: | bit | 序列 | 实际 SPI 操作 | | ---: | --- | --- | | `0` | DCS unlock | 连续向 `0x0009` 写 `55h, B3h, 0Ah`。 | | `1` | Test unlock | 连续向 `0x0009` 写 `AAh, 4Ch, F5h`。 | | `2` | Mode exit | 向 `0x0009` 写 `F9h`。 | | `3` | 控制快捷写 | `control_shadow <= 8'h10`,随后写 `CONTROL`。 | | `4` | 控制快捷写 | `control_shadow <= 8'h08`,随后写 `CONTROL`。 | 若多个 bit 同时为 1,优先级从 bit0 到 bit4。 ### 6.6 通道参数写 通道地址由函数 `ch_addr` 计算: ```verilog ch_addr = CSI_CH_BASE + (ch * CSI_CH_STRIDE) + offset; ``` 默认: ```text CSI_CH_BASE = 0x0010 CSI_CH_STRIDE = 0x0010 ``` 所以: ```text ASIC channel register address = 0x0010 + ch_index * 0x0010 + ch_offset ``` 示例: ```text CH_INDEX = 3 写 CH_CUR_A = 0x7F offset = 0x1 ASIC addr = 0x0010 + 3 * 0x0010 + 0x1 = 0x0041 SPI frame = 0x02_0041_7F ``` 当前所有通道参数写都只发送 `bram_din[7:0]`。 ### 6.7 Direct SPI 访问 Direct SPI 用于软件直接访问任意 ASIC 寄存器。 操作流程: 1. 写 `SPI_ADDR`,设置 ASIC 地址。 2. 若是写操作,写 `SPI_WDATA`。 3. 写 `SPI_CTRL` 启动 SPI: ```text SPI_CTRL[0] = direct_write, 1 为写,0 为读 SPI_CTRL[15:8] = direct_len,单位 byte;0 会被转换为 1 ``` `direct_len` 在 SPI master 内部被限制到 `1..MAX_BURST_BYTES`。 读操作完成后: - `last_rdata <= spi_cmd_rdata[31:0]` - 软件读 `SPI_RDATA` 获取数据 - 软件读 `LAST_ADDR` 获取最近访问地址 testbench 中直接读 Part ID: ```text SPI_ADDR = 0x011D SPI_CTRL = 0x00000100 // write=0, len=1 期望帧 = 0x03_011D_00 读回 = 0x28 ``` ## 7. SPI Master 设计 `csi040_spi_master` 接收抽象命令,并转换成 SPI bit 流。 ### 7.1 命令接口 | 信号 | 方向 | 说明 | | --- | --- | --- | | `cmd_ready` | output | SPI master 空闲时为 1。 | | `cmd_valid` | input | 命令有效。空闲时采样。 | | `cmd_write` | input | 1 为写,0 为读。 | | `cmd_addr` | input | ASIC 寄存器地址。 | | `cmd_wdata` | input | 写 payload。低字节先作为 payload byte0。 | | `cmd_len` | input | payload 字节数。0 转 1,超过最大值则截断。 | | `done` | output reg | SPI 帧完成脉冲,持续 1 个 `clk`。 | | `cmd_rdata` | output reg | 读回 payload。 | | `busy` | output reg | SPI master 忙。 | ### 7.2 SPI 帧格式 默认 `ADDR_BYTES = 2`,帧格式为: ```text byte0 opcode byte1 address high byte byte2 address low byte byte3+ payload bytes ``` 1 字节写示例: ```text opcode = 0x02 addr = 0x0002 data = 0x10 frame = 0x02_0002_10 ``` 地址字节按 MSB first 输出。payload 来自: ```verilog cmd_wdata[8*n +: 8] ``` 即 `cmd_wdata[7:0]` 是第一个 payload byte。 ### 7.3 SPI 状态机 | 状态 | 行为 | | --- | --- | | `IDLE_ST` | `cmd_ready=1`,`csn=1`,`sck=0`。若 `cmd_valid=1`,锁存 frame 并进入传输。 | | `TRANS_ST` | 每 `CLK_DIV` 个 `clk` 翻转一次 `sck`;上升沿采样读数据;下降沿准备下一位输出。 | | `DONE_ST` | `csn` 拉高,`done` 脉冲 1 拍,`busy` 拉低,返回 idle。 | SPI 时钟频率: ```text f_sck = f_clk / (2 * CLK_DIV) ``` testbench 中: ```text f_clk = 100 MHz CLK_DIV = 2 f_sck = 25 MHz ``` 该值用于加速仿真。实际硬件中应根据 CSI040 SPI 时序要求重新设置 `SPI_CLK_DIV`。 ### 7.4 读数据采样 SPI master 在 header 发送完之后开始采样 `so`: ```text HEADER_BITS = 8 * (1 + ADDR_BYTES) ``` 默认地址 2 字节时,header 为 24 bit。读数据从第 25 个 bit 开始采样,采样结果移入 `rx_shift`,帧结束时赋给 `cmd_rdata`。 ## 8. 复位行为 `rst_n = 0` 时,`csi040_ctrl` 主要寄存器复位如下: | 寄存器 | 复位值 | | --- | ---: | | `direct_addr` | `0` | | `direct_wdata` | `0` | | `direct_len` | `1` | | `direct_write` | `1` | | `control_shadow` | `0` | | `ch_enable_shadow` | `0` | | `ch_index` | `0` | | `ch_data` | `0` | | `ch_offset` | `0` | | `seq_id` | `SEQ_IDLE` | | `seq_step` | `0` | | `seq_active` | `0` | | `error_busy` | `0` | | `done_latched` | `0` | | `last_rdata` | `0` | | `last_addr` | `0` | | `asic_rst_n` | `1` | | `asic_trig` | `0` | `csi040_spi_master` 复位后: ```text csn = 1 sck = 0 si = 0 done = 0 busy = 0 state = IDLE_ST ``` ## 9. 当前 Testbench 覆盖 `csi040_ctrl_tb` 实例化 DUT 时重载: ```verilog .SPI_CLK_DIV ( 2 ), .SPI_OPCODE_WRITE ( 8'h02 ), .SPI_OPCODE_READ ( 8'h03 ) ``` testbench 使用 100 MHz clock,复位 8 个周期后开始测试。 覆盖场景: | 测试项 | BRAM 操作 | 期望 SPI 帧 | | --- | --- | --- | | 控制寄存器写 | `write_bram(0x0020, 0x10)` | `0x02000210` | | 通道使能低字节 | `write_bram(0x0024, 0xA55A)` | `0x0200075A` | | 通道使能高字节 | 同上 | `0x020008A5` | | 通道 3 Current A | `CH_INDEX=3`,`CH_CUR_A=0x7F` | `0x0200417F` | | DCS unlock step0 | `SEQ_CMD=1` | `0x02000955` | | DCS unlock step1 | 同上 | `0x020009B3` | | DCS unlock step2 | 同上 | `0x0200090A` | | Direct read Part ID | `SPI_ADDR=0x011D`,`SPI_CTRL=0x0100` | `0x03011D00` | | 读回检查 | ASIC 模型返回 `0x28` | `SPI_RDATA[7:0] == 0x28` | testbench 中 SPI monitor 的行为: - `negedge asic_csn` 清空 bit 计数和 shift 寄存器; - `posedge asic_sck` 采样 `asic_si`; - `posedge asic_csn` 锁存完整帧; - 在读数据阶段从 `asic_so` 返回 `read_response = 8'h28`。 ## 10. 仿真与波形 已验证的命令: ```powershell & 'C:\Xilinx\Vivado\2023.1\bin\xvlog.bat' ` c64_pro.srcs/sources_1/rtl/csi040/csi040_spi_master.v ` c64_pro.srcs/sources_1/rtl/csi040/csi040_ctrl.v ` c64_pro.srcs/sources_1/rtl/csi040/csi040_ctrl_tb.v & 'C:\Xilinx\Vivado\2023.1\bin\xelab.bat' csi040_ctrl_tb -debug typical -snapshot csi040_ctrl_tb_sim_dbg & 'C:\Xilinx\Vivado\2023.1\bin\xsim.bat' csi040_ctrl_tb_sim_dbg -runall ``` 当前仿真结果: ```text $finish called at time : 10940 ns ``` 未触发 testbench 中的 `ERROR ... $stop`。 打开 GUI: ```powershell & 'C:\Xilinx\Vivado\2023.1\bin\xsim.bat' csi040_ctrl_tb_sim_dbg -gui ``` GUI 中可用: ```tcl log_wave -r /csi040_ctrl_tb/* run all ``` 建议观察信号: ```text /csi040_ctrl_tb/bram_addr /csi040_ctrl_tb/bram_din /csi040_ctrl_tb/bram_dout /csi040_ctrl_tb/bram_en /csi040_ctrl_tb/bram_we /csi040_ctrl_tb/busy /csi040_ctrl_tb/spi_done /csi040_ctrl_tb/error_busy /csi040_ctrl_tb/asic_csn /csi040_ctrl_tb/asic_sck /csi040_ctrl_tb/asic_si /csi040_ctrl_tb/asic_so /csi040_ctrl_tb/frame_in /csi040_ctrl_tb/frame_bits /csi040_ctrl_tb/dut/seq_id /csi040_ctrl_tb/dut/seq_step /csi040_ctrl_tb/dut/seq_active /csi040_ctrl_tb/dut/spi_cmd_valid /csi040_ctrl_tb/dut/spi_cmd_ready /csi040_ctrl_tb/dut/spi_cmd_addr /csi040_ctrl_tb/dut/spi_cmd_wdata /csi040_ctrl_tb/dut/spi_cmd_len /csi040_ctrl_tb/dut/u_csi040_spi_master/cstate /csi040_ctrl_tb/dut/u_csi040_spi_master/tx_shift /csi040_ctrl_tb/dut/u_csi040_spi_master/rx_shift /csi040_ctrl_tb/dut/u_csi040_spi_master/sampled_bits ``` ## 11. 软件使用建议 典型写配置流程: ```text 1. 轮询 STATUS[5] == 0 2. 写目标 BRAM 命令寄存器 3. 轮询 STATUS[7] == 1 4. 如是读命令,读取 SPI_RDATA 5. 写 STATUS[0] = 1 清除 done_latched 6. 如 STATUS[8] = 1,记录错误并写 STATUS[1] = 1 清除 error_busy ``` 注意事项: - 忙时不要写命令寄存器,否则命令会被丢弃,并置位 `error_busy`。 - `PIN_CTRL` 是直接引脚控制,不会自动生成 SPI。 - `TRIGGER` 是 SPI 写 ASIC trigger register,不等同于 `asic_trig` 引脚。 - `SPI_CTRL[15:8]` 为直接 SPI payload 长度,最大有效值受 `MAX_BURST_BYTES` 限制。 - 当前 BRAM 接口只支持全字写。 ## 12. 后续可改进点 - 明确 `bram_clk/bram_rst` 的设计意图:要么真正使用它们,要么在接口说明中标记为 unused。 - 增加 byte enable 写支持,方便接 AXI BRAM Controller。 - 为 `STATUS` 位定义软件头文件常量,避免软件侧硬编码 bit 位置。 - 扩展 testbench: - 忙时写命令,检查 `error_busy`; - `PIN_CTRL` 读写; - `TRIGGER` SPI 序列; - Test unlock 和 mode exit; - Direct SPI 多字节 burst; - `cmd_len=0`、超过 `MAX_BURST_BYTES` 的边界行为; - SPI read 多字节返回数据。 - 增加 SystemVerilog assertion,用来约束命令重入、SPI 帧长度和 `csn/sck` 时序。