Level 46: Memory-Mapped I/O

Task

Implement a memory-mapped I/O controller. The IOController module connects to the processor's address and data bus:

  • Write to address 0xFF (addr=0xFF, we=1) — stores data_in in the led_out register, lighting the LEDs.
  • Read from address 0xFE (addr=0xFE, we=0) — outputs the switch states switches_in to data_out.

Use generate-for for the 8-DFF array and AND-trees for address decoding. The interface will show a "Peripherals" panel with 8 LEDs and 8 switches.

Related materials

Solution

The controller has four parts:

  1. 0xFF address detector: AND-tree of all 8 addr bits → addr_ff. Write strobe: wr_en = addr_ff AND we.
  2. led_out register: generate-for (i=0..7) with DFF and hold logic: when wr_en=1, load data_in[i]; otherwise hold current led_out[i].
  3. 0xFE address detector: addr[7:1]=1, addr[0]=0 → addr_fe.
  4. data_out output: generate-for (j=0..7): data_out[j] = switches_in[j] AND addr_fe (when addr_fe=1, output switches; otherwise 0).
module IOController(
    input wire [7:0] addr,
    input wire [7:0] data_in,
    input wire we,
    input wire clk,
    input wire [7:0] switches_in,
    output wire [7:0] data_out,
    output wire [7:0] led_out
);
  // 0xFF detector
  wire t0, t1, t2, t3, t4, t5, addr_ff;
  and a1(t0, addr[0], addr[1]);
  and a2(t1, addr[2], addr[3]);
  and a3(t2, addr[4], addr[5]);
  and a4(t3, addr[6], addr[7]);
  and a5(t4, t0, t1);
  and a6(t5, t2, t3);
  and a7(addr_ff, t4, t5);
  wire wr_en, wr_not;
  and aw(wr_en, addr_ff, we);
  not nw(wr_not, wr_en);

  // 0xFE detector
  wire fe_a1, fe_a2, fe_a3, fe_a4, fe_a5, fe_hi, addr_0n, addr_fe;
  and b1(fe_a1, addr[1], addr[2]);
  and b2(fe_a2, addr[3], addr[4]);
  and b3(fe_a3, addr[5], addr[6]);
  and b4(fe_a4, fe_a1, fe_a2);
  and b5(fe_a5, fe_a3, addr[7]);
  and b6(fe_hi, fe_a4, fe_a5);
  not b7(addr_0n, addr[0]);
  and b8(addr_fe, fe_hi, addr_0n);

  // LED register
  generate
    genvar i;
    for (i = 0; i < 8; i = i + 1) begin : led_gen
      wire t_on, t_hold, led_d;
      and d1(t_on, data_in[i], wr_en);
      and d2(t_hold, led_out[i], wr_not);
      or  d3(led_d, t_on, t_hold);
      dff dff_i(led_out[i], led_d, clk);
    end
  endgenerate

  // data_out
  generate
    genvar j;
    for (j = 0; j < 8; j = j + 1) begin : data_gen
      and gm(data_out[j], switches_in[j], addr_fe);
    end
  endgenerate
endmodule

The key insight: wr_not = NOT wr_en. When wr_en=1: led_d = data_in[i] AND 1 = data_in[i] (load). When wr_en=0: led_d = led_out[i] AND 1 = led_out[i] (hold). DFF captures led_d on the rising edge of clk.