// --------------------------------------------------------------------------- // spi_slave_regfile.sv — QX-250 FPGA block B7 (host register interface) // FPGA-QX250-001 · ADR-001 (SPI slave) · traces REQ-SYS-004, functional F7 // // The MCU's single window into the FPGA. SPI SLAVE, MODE 0 (CPOL=0/CPHA=0), // MSB first. First byte after CS-low = command {rw, addr[6:0]} (rw=1 read, // rw=0 write); address auto-increments for every subsequent byte. Reading byte 0 // returns STATUS "for free". // // Single-clock design: SCLK/CS/MOSI are OVERSAMPLED in the clk_sys (48 MHz) // domain via 2-flop synchronizers and edge detection — NO separate SPI clock // domain (consistent with FPGA-QX250-001 §8). Host SCLK must be <= 4 MHz so the // >=12x oversample ratio leaves margin for MISO read-back turnaround (ADR-001). // // Protocol/register logic is a 1:1 transcription of the validated /tmp/b7_model.py. // --------------------------------------------------------------------------- `timescale 1ns/1ps `default_nettype none module spi_slave_regfile #( parameter [7:0] WHOAMI = 8'hC2 ) ( input wire clk, // clk_sys (48 MHz) input wire rst_n, // ---- SPI slave pins (from MCU master) ---- input wire host_sclk, input wire host_cs_n, input wire host_mosi, output wire host_miso, output wire host_miso_oe, // drive MISO only while selected // ---- read-only inputs from other blocks ---- input wire [15:0] imu_ax, imu_ay, imu_az, input wire [15:0] imu_temp, input wire [15:0] imu_gx, imu_gy, imu_gz, input wire [31:0] imu_ts, input wire imu_initialized, input wire pll_locked, input wire armed, input wire imu_data_valid, // pulse: sets STATUS.data_valid sticky input wire [7:0] fault_flags, // ---- write outputs to other blocks ---- output reg [15:0] motor1, motor2, motor3, motor4, output reg [7:0] control_reg, // b0 = arm_request output wire arm_request, output reg heartbeat_strobe, // pulse: MCU wrote HEARTBEAT (0x49) output reg setpoints_updated // pulse: MCU wrote MOTOR4 low byte (0x47) ); // ---- input synchronizers + edge detect --------------------------------- reg sclk_m, sclk_s, sclk_d; reg cs_m, cs_s, cs_d; reg mosi_m, mosi_s; always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin {sclk_m,sclk_s,sclk_d} <= 3'b000; {cs_m, cs_s, cs_d} <= 3'b111; // CS idles high {mosi_m,mosi_s} <= 2'b00; end else begin sclk_m <= host_sclk; sclk_s <= sclk_m; sclk_d <= sclk_s; cs_m <= host_cs_n; cs_s <= cs_m; cs_d <= cs_s; mosi_m <= host_mosi; mosi_s <= mosi_m; end end wire sclk_rise = ( sclk_s & ~sclk_d); wire sclk_fall = (~sclk_s & sclk_d); wire cs_active = ~cs_s; wire cs_fall = (~cs_s & cs_d); // CS just went low (frame start) assign arm_request = control_reg[0]; assign host_miso_oe = cs_active; // ---- STATUS + data_valid sticky ---------------------------------------- reg dv_sticky; wire [7:0] status = {4'b0, dv_sticky, armed, pll_locked, imu_initialized}; // ---- read mux (combinational, from the current address) ---------------- reg [6:0] addr; wire [111:0] sensor_vec = {imu_ax,imu_ay,imu_az,imu_temp,imu_gx,imu_gy,imu_gz}; wire [63:0] motor_vec = {motor1,motor2,motor3,motor4}; reg [7:0] rd_data; always_comb begin rd_data = 8'h00; if (addr == 7'h00) rd_data = WHOAMI; else if (addr == 7'h01) rd_data = status; else if (addr == 7'h02) rd_data = fault_flags; else if (addr >= 7'h10 && addr <= 7'h1D) rd_data = sensor_vec[ (111 - 8*(addr-7'h10)) -: 8 ]; else if (addr >= 7'h1E && addr <= 7'h21) rd_data = imu_ts[ (31 - 8*(addr-7'h1E)) -: 8 ]; else if (addr >= 7'h40 && addr <= 7'h47) rd_data = motor_vec[ (63 - 8*(addr-7'h40)) -: 8 ]; else if (addr == 7'h48) rd_data = control_reg; end // ---- shift engine ------------------------------------------------------ reg [7:0] in_sr; // MOSI shift-in reg [7:0] out_sr; // MISO shift-out (MSB is the live bit) reg [2:0] bitcnt; // 0..7 within a byte reg is_cmd; // next completed byte is the command byte reg rw; // 1=read, 0=write reg load_out; // reload out_sr from rd_data on the next falling edge logic [7:0] shifted; assign host_miso = out_sr[7]; // MSB-first, valid while selected always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin in_sr <= 0; out_sr <= 0; bitcnt <= 0; is_cmd <= 1'b1; rw <= 1'b0; load_out <= 1'b0; addr <= 0; motor1 <= 0; motor2 <= 0; motor3 <= 0; motor4 <= 0; control_reg <= 0; heartbeat_strobe <= 1'b0; setpoints_updated <= 1'b0; dv_sticky <= 1'b0; end else begin heartbeat_strobe <= 1'b0; // 1-cycle pulses default low setpoints_updated <= 1'b0; if (imu_data_valid) dv_sticky <= 1'b1; if (cs_fall) begin // frame start: next byte is the command; MISO byte0 = STATUS bitcnt <= 3'd0; is_cmd <= 1'b1; out_sr <= status; // presents status[7] on MISO immediately load_out <= 1'b0; end else if (cs_active) begin // ---- sample MOSI on rising edge ---- if (sclk_rise) begin shifted = {in_sr[6:0], mosi_s}; in_sr <= shifted; if (bitcnt == 3'd7) begin bitcnt <= 3'd0; if (is_cmd) begin rw <= shifted[7]; addr <= shifted[6:0]; is_cmd <= 1'b0; load_out <= 1'b1; // preload first data byte end else begin if (!rw) begin // write byte to the addressed register case (addr) 7'h40: motor1[15:8] <= shifted; 7'h41: motor1[7:0] <= shifted; 7'h42: motor2[15:8] <= shifted; 7'h43: motor2[7:0] <= shifted; 7'h44: motor3[15:8] <= shifted; 7'h45: motor3[7:0] <= shifted; 7'h46: motor4[15:8] <= shifted; 7'h47: motor4[7:0] <= shifted; 7'h48: control_reg <= shifted; default: ; endcase if (addr == 7'h49) heartbeat_strobe <= 1'b1; if (addr == 7'h47) setpoints_updated <= 1'b1; end else if (addr == 7'h01) begin dv_sticky <= 1'b0; // clear sticky on STATUS read end addr <= addr + 7'd1; // auto-increment load_out <= 1'b1; end end else begin bitcnt <= bitcnt + 3'd1; end end // ---- shift MISO out on falling edge ---- if (sclk_fall) begin if (load_out) begin out_sr <= rd_data; // first bit of the new data byte load_out <= 1'b0; end else begin out_sr <= {out_sr[6:0], 1'b0}; end end end end end endmodule `default_nettype wire