So far my Verilog journey has been pretty straightforward. Sure, I did have some minor challenges and issues, but there was always one clear, right way to do it. Not anymore. I am now at the point where I can drive a seven segment display with PWM-brightness control and digit multiplexing. The next step in the evolution of my display module would be to have it display a decimal or hex number from a binary input. Hex is fairly simple: It can be implemented as a look up table that outputs a digit for every 4-bit nibble of the input. Decimal is another beast entirely. To turn a binary integer into a four digit 7-segment representation you first need to convert it to BCD – binary coded decimal.
That in itself isn’t difficult. What’s difficult about this, is figuring out how to implement it nicely. The double-dabble algorithm takes more than a single clock cycle, so it’s not possible to just slap some combinatorial logic into the display module and be done. I needed a strategy. A design.
It’s probably a good idea to properly articulate my requirements first:
- Output must not stop. No matter the Input, the output is always well defined, even if „well defined“ means it’s off.
- No glitches. When I change the input, the output should be either the previous or new value. Nothing else. Once it’s the new value it can’t change back. It can take a couple clock cycles though. Latency is not critical.
- Fast inputs may be skipped. If the display can’t update as fast as the input it’s fine if some inputs are skipped.
Ultimately I ended up with the design below. Not super advanced, but it still required more up front thinking than any of my previous modules:
The arrows in this diagram represent the dataflow from the input to the output pins. There are 3 registers available to be written by any user of this module: Input Register, Display Mode and Brightness. The first two control how the segment decoder works, while the last one controls the PWM duty cycle.
The internal Display Register allows the Output to work independently of the Input. It will only change whenever the segment decoder has fully decoded the input. Another nice feature of this design is that I can simply switch out the segment decoder for any pin-compatible alternative. In fact it’s possible to compartmentalize this design even further: The control timer, display register, digit multiplexer and pwm controller can be combined into a display_driver module. This module is exclusively concerned with driving the display according to the display register value.
I won’t bother walking you through the code of this new module, since it’s fairly boring. The interesting part is the system design, not the Verilog code itself. You’ll find the code at the end of this post if you’re interested. I will show you the result though, just so you have some pretty waveforms to look at:
sp are the registers that will directly drive the digit pins and segment pins.
display_data is a 32 bit register that contains the active segments for each digit.
brightness is a 2 bit value representing the brightness, its four states correspond to fully on/high (
11), medium (
10), and low (
01) brightness, as well as turned off (
00). You might ask yourself „why is
display_data 32 bits wide?“. There isn’t really a good reason for that, other than „it’s historical“. Originally this module did more than it does now, so it actually used the bonus bits.
As you can see in figure 2, whenever
active_digit counts up, the next digit pin is activated and the corresponding segments are turned on. PWM works too, it’s just not visible in this screenshot. You’ll have to trust me with that one (or try the code out yourself).
Finally, here’s the code:
`include "./hardware/pwm.sv" `include "./hardware/timer.sv" module display_driver( input rst, clk, input [31:0] display_data, input [1:0] brightness, input [15:0] pwm_period, output reg [6:0] segment_pins, output reg [3:0] digit_pins ); wire [15:0] timer_value; wire [15:0] duty_cycle = pwm_period >> (3 - brightness); timer #(.BITS(16)) pwmtimer( .rst(rst), .pulse(clk), .count_dir(1'b1), .count_mode(1'b1), .reload_value(pwm_period), .counter(timer_value) ); pwm #(.BITS(16)) pwm_comp( .counter(timer_value), .pwm_set(duty_cycle + 16'b1), .out(enable_digit) ); wire digit_enable_zero_fix = enable_digit && (brightness != 2'b00); reg [1:0] active_digit; wire [6:0] segments1 = display_data[30:24]; wire [6:0] segments2 = display_data[22:16]; wire [6:0] segments3 = display_data[14:8]; wire [6:0] segments4 = display_data[6:0]; always @ (posedge clk) begin if(rst) begin digit_pins <= 0; segment_pins <= 0; active_digit <= 0; end else begin // Output correct data depending on which digit is active if(active_digit == 2'b00) segment_pins <= segments1; else if(active_digit == 2'b01) segment_pins <= segments2; else if(active_digit == 2'b10) segment_pins <= segments3; else segment_pins <= segments4; // Digit pins are PWM modulated digit_pins <= digit_enable_zero_fix ? 4'b0000 | (1'b1 << active_digit) : 0; // move on to next digit if timer hits max if(timer_value == pwm_period) begin active_digit <= active_digit + 1; end end end endmodule