// // Communication interface with ESP32-S2 // // This is a DIO (2-bit, including command) SPI slave interface which // allows direct access to content in SDRAM. Additionally, each // direction has 7 interrupt condition flags (7-1); the FPGA CPU // additionally has an internal interrupt condition (0) which // indicates DRAM timing overrun/underrun, also visible in the status // word as bit 0. // // The SPI command byte is: // Bit 7 - save command timestamp // Bit 6 - read/write# // Bit [5:3] - clear upstream (FPGA->ESP) interrupt flag if nonzero // Bit [2:0] - set downstream (ESP->FPGA) interrupt flag if nonzero // // CPU downstream interrupts are set after the transaction completes // (CS# goes high.) // // A 32-bit address follows; for a read, the following 16 cycles // contains dummy/status data: // // Bit [31:16] = 16 LSB of 32 kHz RTC counter // Bit [15: 9] = upstream interrupts pending // Bit 8 = downstream writes enabled // Bit [ 7: 1] = downstream interrupts pending // Bit 0 = underrun error // // The following CPU registers are defined: // // 0 = status bits [7:0] (downstream) // 1 = write-1-clear of status bits [7:0] // 2 = status bits [15:8] (upstream) // 3 = write-1-set of status bits [15:8] // 4 = timestamp (16 LSB of 32 kHz RTC counter) saved by cmd bit 7 // module esp #( parameter dram_bits = 25, parameter [31:0] dram_base = 32'h40000000 ) ( input rst_n, input sys_clk, input sdram_clk, input cpu_valid, input [4:0] cpu_addr, input [3:0] cpu_wstrb, input [31:0] cpu_wdata, output [31:0] cpu_rdata, output reg irq, dram_bus.dstr dram, output reg esp_int, input spi_clk, inout [1:0] spi_io, input spi_cs_n, input [15:0] rtc_ctr ); reg [31:0] mem_addr = 'b0; wire [31:0] mem_addr_mask = (1'b1 << dram_bits) - 3'd4; wire [31:0] mem_addr_out = (mem_addr & mem_addr_mask) | dram_base; reg mem_valid; reg [31:0] mem_wdata; wire mem_write; reg [ 3:0] mem_wstrb; wire mem_ready; wire [31:0] mem_rdata; dram_port #(32) mem ( .bus ( dram ), .prio ( 2'd2 ), .addr ( mem_addr[dram_bits-1:0] ), .valid ( mem_valid ), .wd ( mem_wdata ), .wstrb ( mem_wstrb ), .ready ( mem_ready ), .rd ( mem_rdata ) ); reg [1:0] spi_clk_q; reg spi_cs_n_q; reg [1:0] spi_io_q; always @(posedge sdram_clk) begin spi_clk_q <= { spi_clk_q[0], spi_clk }; spi_cs_n_q <= spi_cs_n; spi_io_q <= spi_io; end typedef enum logic [1:0] { st_dead, // Do nothing until CS# deasserted st_cmd, // Reading command st_addr, // Reading address st_io // I/O (including read dummy bits) } state_t; state_t spi_state; reg [ 7:0] spi_cmd; reg [31:0] spi_shr; reg [ 3:0] spi_ctr; reg [ 7:0] cpu_irq; reg [ 7:0] cpu_set_irq; // CPU IRQs to set once idle reg cpu_send_irq; // Ready to set CPU IRQs reg [ 7:1] spi_irq; reg [ 7:1] latched_spi_irq; // SPI IRQ as of transition start reg [15:0] timestamp_q; reg [ 1:0] spi_out; reg spi_oe; reg [ 2:0] spi_wbe; // Partial word write byte enables reg [23:0] spi_wdata; // Partial word write data reg spi_wr_en; // SPI writes enabled by CPU reg spi_mem_en; // Read, or write enabled at start of trans assign spi_io = spi_oe ? spi_out : 2'bzz; assign mem_write = ~spi_cmd[6]; wire [31:0] spi_indata = { spi_shr[29:0], spi_io_q }; reg cpu_valid_q; always @(negedge rst_n or posedge sdram_clk) if (~rst_n) begin spi_state <= st_dead; spi_cmd <= 'b0; spi_ctr <= 4'd3; // 8 bits needed for this state cpu_irq <= 'b0; cpu_set_irq <= 'b0; cpu_send_irq <= 1'b0; spi_irq <= 'b0; latched_spi_irq <= 'b0; spi_oe <= 1'b0; spi_wbe <= 3'b0; mem_addr <= 'b0; mem_wstrb <= 4'b0; mem_valid <= 1'b0; spi_wr_en <= 1'b0; spi_mem_en <= 1'b0; end else begin esp_int <= ~|spi_irq; if (spi_cs_n_q) begin spi_state <= st_cmd; spi_ctr <= 4'd3; spi_oe <= 1'b0; cpu_send_irq <= |cpu_set_irq; end if (cpu_send_irq & ~mem_valid & ~|spi_wbe) begin cpu_irq <= cpu_irq | cpu_set_irq; cpu_set_irq <= 'b0; cpu_send_irq <= 1'b0; end if (~spi_cs_n_q && spi_clk_q == 2'b01) begin spi_ctr <= spi_ctr - 1'b1; spi_shr <= spi_indata; case (spi_ctr) 4'b1100: if (spi_state == st_io && mem_write) begin spi_wbe[0] <= 1'b1; spi_wdata[7:0] <= spi_indata[7:0]; end 4'b1000: if (spi_state == st_io && mem_write) begin spi_wbe[1] <= 1'b1; spi_wdata[15:8] <= spi_indata[7:0]; end 4'b0100: if (spi_state == st_io && mem_write) begin spi_wbe[2] <= 1'b1; spi_wdata[23:16] <= spi_indata[7:0]; end 4'b0000: begin // Load spi_shr, but account for endianness here... if (spi_state == st_io) spi_shr <= bswap32(mem_rdata); else // Status output spi_shr <= bswap32({ rtc_ctr, latched_spi_irq, spi_wr_en, cpu_irq }); if (mem_valid && spi_state != st_cmd) begin cpu_irq[0] <= 1'b1; // Overrun/underrun spi_wr_en <= 1'b0; // Block further memory writes spi_mem_en <= ~mem_write; end case (spi_state) st_dead: begin // Do nothing end st_cmd: begin spi_cmd <= spi_indata[7:0]; spi_state <= st_addr; latched_spi_irq <= spi_irq; for (int i = 1; i < 8; i++) begin if (spi_indata[5:3] == i) spi_irq[i] <= 1'b0; if (spi_indata[2:0] == i) cpu_set_irq[i] <= 1'b1; end if (spi_indata[7]) timestamp_q <= rtc_ctr; end st_addr: begin mem_addr <= spi_indata & mem_addr_mask; spi_state <= st_io; mem_valid <= ~mem_write; spi_mem_en <= ~mem_write | spi_wr_en; mem_wstrb <= 4'b0; spi_wbe <= 3'b000; // If the first word is partial, skip ahead if (mem_write) spi_ctr[3:2] <= ~spi_indata[1:0]; end st_io: begin if (mem_write) begin mem_wdata[23: 0] <= spi_wdata[23:0]; mem_wdata[31:24] <= spi_indata[7:0]; mem_wstrb <= { 1'b1, spi_wbe }; end else begin mem_wstrb <= 4'b0000; end // else: !if(mem_write) mem_valid <= spi_mem_en; spi_wbe <= 3'b000; end endcase end // case: 4'b0000 default: begin // Do nothing end endcase // case (spi_ctr) end // if (spi_clk_q == 2'b01) else if (~spi_cs_n_q && spi_clk_q == 2'b10) begin spi_out <= spi_shr[31:30]; spi_oe <= (spi_state == st_io) & ~mem_write; end if (mem_valid & mem_ready) begin mem_addr <= mem_addr + 3'd4; mem_valid <= 1'b0; end if (spi_state != st_io & ~mem_valid & |spi_wbe) begin // Complete a partial write terminated by CS# mem_valid <= spi_mem_en; mem_wstrb <= { 1'b0, spi_wbe }; mem_wdata[23:0] <= spi_wdata[23:0]; mem_wdata[31:24] <= 8'hxx; spi_wbe <= 3'b000; end cpu_valid_q <= cpu_valid; if (cpu_valid & ~cpu_valid_q & cpu_wstrb[0]) case (cpu_addr[2:0]) 3'b000: cpu_irq <= cpu_wdata[7:0]; 3'b001: for (int i = 0; i < 8; i++) if (cpu_wdata[i]) cpu_irq[i] <= cpu_send_irq & cpu_set_irq[i]; 3'b010: { spi_irq, spi_wr_en } <= cpu_wdata[7:0]; 3'b011: begin if (cpu_wdata[0]) spi_wr_en <= 1'b1; for (int i = 1; i < 8; i++) if (cpu_wdata[i]) spi_irq[i] <= 1'b1; end default: begin // Do nothing end endcase // case (cpu_addr[2:0]) end // else: !if(~rst_n) always @(posedge sys_clk) irq <= |cpu_irq; always @(*) casez (cpu_addr[2:0]) 3'b000: cpu_rdata = { 28'b0, cpu_irq }; 3'b010: cpu_rdata = { 28'b0, spi_irq, spi_wr_en }; 3'b100: cpu_rdata = { 16'b0, timestamp_q }; default: cpu_rdata = 32'bx; endcase // casez (cpu_addr[2:0]) endmodule // esp