// // 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 three interrupt flags (3-1); the FPGA CPU additionally // has a fourth interrupt condition (0) which indicates DRAM timing // overrun/underrun. // // The SPI command byte is: // Bit [7:5] - reserved, must be 0 // Bit 4 - read/write# // Bit [3:2] - clear upstream (FPGA->ESP) interrupt flag if nonzero // Bit [1: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] = adjusted memory address // Bit [15:14] = 2'b10 // Bit [13: 8] = 0 reserved // Bit [ 7: 5] = upstream interrupt status // Bit 4 = 0 reserved // Bit [ 3: 1] = downstream interrupt status // Bit 0 = underrun error // 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 ); 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_cmd, // Reading command st_addr, // Reading address st_io // I/O (including read dummy bits) } state_t; state_t spi_state; reg [ 4:0] spi_cmd; reg [31:0] spi_shr; reg [ 3:0] spi_ctr; reg [ 3:0] cpu_irq; reg [ 3:1] cpu_set_irq; // CPU IRQs to set once idle reg [ 3:1] spi_irq; reg [ 3:1] latched_spi_irq; // SPI IRQ as of transition start 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 assign spi_io = spi_oe ? spi_out : 2'bzz; assign mem_write = ~spi_cmd[4]; 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_cmd; spi_cmd <= 'b0; spi_ctr <= 4'd3; // 8 bits needed for this state cpu_irq <= 'b0; cpu_set_irq <= '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; 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; if (~mem_valid) begin cpu_irq <= cpu_irq | { cpu_set_irq, 1'b0 }; cpu_set_irq <= 'b0; end end else if (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 // Transfer data to/from memory controller, but // we have to shuffle endianness... if (spi_state == st_io) begin // Memory output spi_shr[31:24] <= mem_rdata[ 7: 0]; spi_shr[23:16] <= mem_rdata[15: 8]; spi_shr[15: 8] <= mem_rdata[23:16]; spi_shr[ 7: 0] <= mem_rdata[31:24]; end else begin // Status output spi_shr[31:16] <= mem_addr_out[31:16]; spi_shr[15: 8] <= 8'h80; spi_shr[ 7: 4] <= { latched_spi_irq, 1'b0 }; spi_shr[ 3: 0] <= cpu_irq; end // else: !if(spi_state == st_io) if (mem_valid && spi_state != st_cmd) cpu_irq[0] <= 1'b1; // Overrun/underrun case (spi_state) st_cmd: begin spi_cmd <= spi_indata[5:0]; spi_state <= st_addr; latched_spi_irq <= spi_irq; for (int i = 1; i < 4; i++) begin if (spi_indata[3:2] == i) spi_irq[i] <= 1'b0; if (spi_indata[1:0] == i) cpu_set_irq[i] <= 1'b1; end end st_addr: begin mem_addr <= spi_indata & mem_addr_mask; spi_state <= st_io; mem_valid <= ~mem_write; 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 <= 1'b1; spi_wbe <= 3'b000; end endcase end // case: 4'b0000 default: ; // Nothing endcase // case (spi_ctr) end // if (spi_clk_q == 2'b01) else if (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 <= 1'b1; 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[1:0]) 2'b00: cpu_irq <= cpu_wdata[3:0]; 2'b01: for (int i = 0; i < 4; i++) if (cpu_wdata[i]) cpu_irq[i] <= 1'b0; 2'b10: spi_irq <= cpu_wdata[3:1]; 2'b11: for (int i = 1; i < 4; i++) if (cpu_wdata[i]) spi_irq[i] <= 1'b1; endcase // case (cpu_addr[1:0]) end // else: !if(~rst_n) always @(posedge sys_clk) irq <= |cpu_irq; always @(*) casez (cpu_addr[1:0]) 2'b0?: cpu_rdata = { 28'b0, cpu_irq }; 2'b1?: cpu_rdata = { 28'b0, spi_irq, 1'b0 }; endcase // casez (cpu_addr[1:0]) endmodule // esp