Lab 2: Multiplexed 7-Segment Display

Introduction

In this lab, time multiplexing was used to efficiently drive two seven-segment displays with a single set of FPGA I/O pins. The sum of both numbers were displayed on five separate LEDs. Additionally, transistors were used to drive large currents from the FPGA pins.

Design and Testing

The design of the time multiplexing scheme was one of the main components of this lab. Time multiplexing is a technique that is used to share a common expensive hardware resource for several purposes at different times. In this lab, time multiplexing was used to drive a single seven-segment decoder module to run both halves of a dual display.

In terms of wiring, this is done by connecting a single GPIO pin on the FPGA to the same segment (e.g. segment e) on both the seven-segment displays. However, in order to control which half of the dual display is active, the common anode of only one display at a time is turned ON. The power to these two displays are controlled by two separate GPIO pins, which are toggled at a certain speed in order for two distinct numbers to appear simultaneously on each display. Due to the anode requiring more current than the FPGA output pin can drive, a 2N3906 PNP transistor was used to limit the base current and not draw too much from the FPGA pin. Both of the dual displays are controlled by four bit DIP switches, and the sum of both numbers is displayed as a five bit number on five separate LEDs. The hardware layout, with the breadboard and wiring as described above, is shown in Figure 1.

Figure 1. Wiring of the dual seven-segment display, LEDs, and transistor circuits

The wiring and breadboard layout was intuitively done to enable easy debugging and be visually appealing. In order to justify resistor choices, calcuations were done, as shown in Figure 2, to ensure proper brightness of the five individual LEDs and that the current draw/sink on all the FPGA pins was below the currents specified in the recommended operating conditions.

Figure 2. Calculations to justify chosen resistor values for the dual seven-segment display, the five individual LEDs, and the PNP transistor

From a software standpoint, important decisions were made in order to split up the required tasks of the lab into respective modules. A top level module was used to instantiate the on-board high-speed oscillator (HSOSC) from the iCE40 UltraPlus primitive library, which was the 48 MHz clock that was divided and used throughout the system. The top level module also contained the combinational logic for the five individual LEDs, as they were simple and therefore did not require a separate module.

One of the most important design considerations was the speed at which the two displays were toggled. This was done using a clock divider and 32-bit counter, where choosing different bits of the counter in the clock division allowed for different switching speeds. After testing, it was determined that the 18th bit of the counter produced a toggling speed that ensured that both hexadecimal digits lit up simultaneously without visible oscillation and without the digits bleeding into each other. At this speed, the clock divider enabled power to each of the displays in the dual display and the four bit inputs were sent one at a time to the seven segment module.

In order to verify the software, three testbenches were written with one for each module——top level, seven-segment, and multiplexer. The seven-segment testbench checked that the correct segments displayed with the corresponding four bit inpupts. The top level testbench checked that the combinational logic for the five LEDs functioned as properly. The multiplexer module checked that the input to the seven segment display was the correct four bit input corresponding to which display was enabled. The testbenches also checked the toggling of the enable signals. The waveforms are shown in the Results and Discussion section. In order to test the hardware, all switches and their summing combinational logic was tested on the breadboard. A picture from one of these tests is shown below in Figure 3.

Figure 3. Image from testing displaying functioning hardware. The hex digit “b” is displayed on the left, and “0” is displayed on the right, with their sum equalling 11 and being displayed on the five individual LEDs.

Technical Documentation

The source code for the project can be found in the associated Github repository.

Block Diagram

Figure 4. Block diagram of the Verilog module

The block diagram shown in Figure 4 demonstrates the overall architecture of the design. The top-level module includes three submodules: the high-speed oscillator block (HSOSC), the seven-segment module to operate the 7-segment display, and the multiplexer module to operate the correct switching/toggling of the dual displays.

Schematic

Figure 5. Schematic of the physical circuit

The physical layout of the design is shown in Figure 5. The left side of the UPduino shows the inputs, which are the reset and two sets of four-input DIP switches with an internal 100 kΩ pullup resistor to ensure the active low reset pin was not floating. The right side shows the outputs, which are the GPIO pins connecting the transistors to power, the two enables, the seven segments, and the five LEDs. All of the LEDs and transistors are connected to current-limiting resistors, the calculations for which are shown above in Figure 2.

Results and Discussion

Testbench Simulation

Three testbenches were written in SystemVerilog, one for each of the modules (top level, multiplexer, and seven-segment), in order to show that the combinational logic works and that the enables toggle and produce the correct, corresponding display.

As shown in the simulation images below, all the simulations passed and produced the expected outputs. Figures 6 and 7 show the simulation waveforms for the top level module. Figure 6 shows that the led variable is the correct sum of the 8-bit counter input. Figure 7 shows that when zoomed out, the enable toggles and tests the other cases. Figure 8 shows that s (input to the seven-segment module) gets the correct four bit input during the correct enable. In the waveforms, it is visible that the value of four bits of s_counter (the input) match the value of s one clock cycle later. This makes sense because s is an output of the multiplexer module, and thus displays its value one clock cycle later. Similar to the top level, zooming out shows that the enable switches as expected to test all the other cases. Figure 10 shows the seven-segment waveforms also producing the expected outputs for the segment values on the display.

Figure 6. Top-level module QuestaSim simulation showing correct combinational logic

Figure 7. Zoomed-out view of top-level module QuestaSim simulation showing the enables switching

Figure 8. Multiplexer module QuestaSim simulation showing the seven-segment getting the correct input

Figure 9. Zoomed-out view of multiplexer module QuestaSim simulation showing the enables switching

Figure 10. Seven-segment module QuestaSim simulation

Therefore, the design met all of the intended design objectives, and performed quickly and reliably. The simulations shown below ran in a few seconds, and the hardware displayed all the required LEDs brightly.

Conclusion

My FPGA designs successfully controlled a time multiplexing scheme to drive two 7-segment displays with a single set of FPGA I/O pins. A transistor to drive large currents from the FPGA pins was also successfully implemented. Additionally, I gained a lot of experience with building testbenches and modular Verilog systems.

In total, I spent 21 hours on this lab.

AI Prototype

Upon typing the following prompt into ChatGPT, the following code was generated:

Prompt: Write SystemVerilog HDL to time multiplex a single seven segment decoder (that decodes from four bits to a common anode seven segment display) to decode two sets of input bits and drive two sets of seven output bits.

Code:

// -----------------------------------------------------------------------------
// Common–anode 7-segment decoder (active LOW segments)
// seg_n order = {a,b,c,d,e,f,g}
// -----------------------------------------------------------------------------
module sevenseg_ca_decode (
  input  logic [3:0] x,
  output logic [6:0] seg_n
);
  always_comb begin
    unique case (x)
      4'h0: seg_n = 7'b0000001;
      4'h1: seg_n = 7'b1001111;
      4'h2: seg_n = 7'b0010010;
      4'h3: seg_n = 7'b0000110;
      4'h4: seg_n = 7'b1001100;
      4'h5: seg_n = 7'b0100100;
      4'h6: seg_n = 7'b0100000;
      4'h7: seg_n = 7'b0001111;
      4'h8: seg_n = 7'b0000000;
      4'h9: seg_n = 7'b0000100;
      4'hA: seg_n = 7'b0001000;
      4'hB: seg_n = 7'b1100000; // 'b' as 11
      4'hC: seg_n = 7'b0110001;
      4'hD: seg_n = 7'b1000010; // 'd' as 13
      4'hE: seg_n = 7'b0110000;
      4'hF: seg_n = 7'b0111000;
      default: seg_n = 7'b1111111; // all OFF
    endcase
  end
endmodule


// -----------------------------------------------------------------------------
// Two-channel time-multiplexer using a single decoder instance
//  - Alternates between in0 and in1 at a programmable rate
//  - Latches each decoded pattern into seg0_n/seg1_n registers
//  - Outputs are stable between refresh ticks (no comb fanout from muxed input)
// -----------------------------------------------------------------------------
module sevenseg_twoch_tmux #(
  // Adjust these to set the update rate.
  // Example: at 25 MHz, DIV=125_000 gives a 200 Hz tick (each channel ~100 Hz).
  parameter int unsigned DIV = 125_000
) (
  input  logic        clk,
  input  logic        rst_n,

  input  logic [3:0]  in0,
  input  logic [3:0]  in1,

  output logic [6:0]  seg0_n,  // decoded output for in0 (active LOW)
  output logic [6:0]  seg1_n   // decoded output for in1 (active LOW)
);

  // Clock divider to make a refresh "tick"
  logic [$clog2(DIV)-1:0] div_cnt;
  logic                   tick;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      div_cnt <= '0;
      tick    <= 1'b0;
    end else if (div_cnt == DIV-1) begin
      div_cnt <= '0;
      tick    <= 1'b1;
    end else begin
      div_cnt <= div_cnt + 1'b1;
      tick    <= 1'b0;
    end
  end

  // Channel select toggles each tick
  logic sel;  // 0 -> service in0, 1 -> service in1
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) sel <= 1'b0;
    else if (tick) sel <= ~sel;
  end

  // Single decoder instance, input multiplexed
  logic [3:0] muxed_in;
  logic [6:0] dec_out_n;

  assign muxed_in = sel ? in1 : in0;

  sevenseg_ca_decode u_dec (
    .x     (muxed_in),
    .seg_n (dec_out_n)
  );

  // Register each channel's pattern when it is serviced
  // This removes glitches and gives each output a steady value
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      seg0_n <= 7'h7F;  // all off
      seg1_n <= 7'h7F;
    end else if (tick) begin
      if (sel)  seg1_n <= dec_out_n; // just decoded in1
      else      seg0_n <= dec_out_n; // just decoded in0
    end
  end

endmodule

The LLM-generated code was synthesized the first time around in Radiant. It did take a longer time to synthesize than code that I’ve written, but there were no errors.

The hierarchy of the code it generated was different than mine, as it only had two modules, and mine had three. The seven segment module it wrote was pretty identical to my seven segment module, but the top level in the LLM-generated code (which included my multiplexer module functionality as well) was quite different. One of the main differences was that it didn’t use the on-board high speed oscillator for the clock and instead generated and used a new clock divider. It used a variable called “DIV,” which is told us to adjust to set the update or switching rate. The LLM-generated code also had two signals for enabling that performed the multiplexing, but it had two seven bit outputs instead of one. The generated code was pretty different than mine in terms of syntax, hierarchy, and implementation, but it was interesting to see that it does the same function (more or less) and compiles without errors.

Next, I started a new conversation in ChatGPT and uploaded the following prompt along with my lab 1 files (all source files) as context to get the LLM-generated code shown below.

Prompt: Write SystemVerilog HDL to time multiplex a single seven segment decoder (that decodes from four bits to a common anode seven segment display) to decode two sets of input bits and drive two sets of seven output bits. Use the seven segment decoder and oscillator provided in the attached files.

Code:

// dual_sevenseg_mux.sv
// Time-multiplex a single 7-seg decoder across two 4-bit inputs and
// drive two independent 7-bit (common-anode) segment buses.

module dual_sevenseg_mux #(
    // Internal oscillator nominal frequency (UP5K HFOSC ~48 MHz)
    parameter int unsigned CLK_HZ      = 48_000_000,
    // Per-digit refresh rate (how often each digit is updated)
    parameter int unsigned REFRESH_HZ  = 1_000
)(
    input  logic        rst_n,          // active-low async reset (tie high if unused)

    input  logic [3:0]  in0,            // nibble for "left" display
    input  logic [3:0]  in1,            // nibble for "right" display

    output logic [6:0]  seg0,           // segments for display 0 (common anode: active-low segments)
    output logic [6:0]  seg1            // segments for display 1
);

    // ----------------------------------------------------------------
    // Internal high-speed oscillator (Lattice iCE40 UP family primitive)
    // ----------------------------------------------------------------
    logic clk_hf;
    HSOSC hf_osc (
        .CLKHFPU (1'b1),   // power up
        .CLKHFEN (1'b1),   // enable
        .CLKHF   (clk_hf)  // ~48 MHz clock out
    );

    // ---------------------------------------------------------------
    // Refresh tick generator (sets the multiplexing cadence)
    // Each digit is updated at REFRESH_HZ, so the decoder input
    // switches at 2*REFRESH_HZ overall.
    // ---------------------------------------------------------------
    localparam int unsigned TICKS_PER_REFRESH = CLK_HZ / REFRESH_HZ;
    localparam int unsigned CNTW = $clog2(TICKS_PER_REFRESH);

    logic [CNTW-1:0] tick_cnt;
    logic            refresh_tick;

    always_ff @(posedge clk_hf or negedge rst_n) begin
        if (!rst_n) begin
            tick_cnt     <= '0;
            refresh_tick <= 1'b0;
        end else begin
            if (tick_cnt == TICKS_PER_REFRESH-1) begin
                tick_cnt     <= '0;
                refresh_tick <= 1'b1;
            end else begin
                tick_cnt     <= tick_cnt + 1'b1;
                refresh_tick <= 1'b0;
            end
        end
    end

    // ---------------------------------------------------------------
    // Single decoder instance, input mux, and latched outputs
    // ---------------------------------------------------------------
    logic        sel;           // 0 -> in0 path active, 1 -> in1 path active
    logic [3:0]  mux_in;        // presented to the decoder
    logic [6:0]  dec_out;       // raw decoder output (active-low segments)

    // Feed one nibble at a time into the single decoder
    assign mux_in = (sel == 1'b0) ? in0 : in1;

    // Your provided seven-seg decoder (common-anode) — adjust names if needed.
    seven_seg_sj u_dec (
        .value (mux_in),
        .seg   (dec_out)
    );

    // Registers to hold each display’s last-decoded segments
    logic [6:0] seg0_r, seg1_r;

    // On each refresh tick:
    //  - latch current decoder output into the register corresponding to `sel`
    //  - then toggle `sel` so the *other* nibble is decoded next period
    always_ff @(posedge clk_hf or negedge rst_n) begin
        if (!rst_n) begin
            sel   <= 1'b0;
            seg0_r <= 7'h7F;   // all OFF for common-anode (active-low)
            seg1_r <= 7'h7F;
        end else if (refresh_tick) begin
            if (sel == 1'b0) begin
                seg0_r <= dec_out; // we were decoding in0; capture for seg0
            end else begin
                seg1_r <= dec_out; // we were decoding in1; capture for seg1
            end
            sel <= ~sel; // switch to decode the other input next
        end
    end

    // Drive outputs
    assign seg0 = seg0_r;
    assign seg1 = seg1_r;

endmodule

This LLM-generated code did synthesize relatively quickly but had one error—that it was instantiating an unknown module seven_seg_sj. This was a mistake on my part because I didn’t add my module into the project. Besides this, the code looks pretty similar to mine. It uses the high speed oscillator and chooses the correct dual display based on the enable. Once again, it does have two 7-bit outputs while I have one, and it also uses syntax I’m not familiar with. But overall, it was efficient code, and was even more similar to mine when I inputted my lab 1 files.