Practical case: Selectable Frequency Divider

Selectable Frequency Divider prototype (Maker Style)

Level: Medium – Build a binary frequency divider using a 74HC393 ripple counter with a selectable output stage.

Objective and use case

You will construct a binary ripple counter circuit utilizing the 74HC393 integrated circuit to process a high-speed input clock. By dividing the frequency by factors of 2, 4, 8, and 16, a physical jumper acts as a selector to route a specific down-scaled output to a visual LED indicator.

This topology is highly useful in practical electronics for several reasons:
* Generating precise, slow timebases for digital clocks from high-frequency crystal oscillators.
* Stepping down high-frequency PWM signals for specific motor control applications.
* Creating multiple synchronized sub-harmonic frequencies for audio synthesizers.
* Converting a rapidly blinking indicator into a manageable, visually observable pulse.

Expected outcomes:
* The CLK input will receive a constant square wave.
* The outputs QA, QB, QC, and QD will cleanly toggle at exactly 1/2, 1/4, 1/8, and 1/16 of the clock frequency, respectively.
* Selecting different outputs via the jumper will visibly change the LED blink rate without altering the main clock source.

Target audience and level: Electronics students and hobbyists wanting hands-on experience with asynchronous logic and frequency scaling.

Materials

  • V1: 5 V DC power supply, function: logic power source
  • VCLK: 5 V square wave clock source (e.g., function generator), function: input clock
  • U1: 74HC393 Dual 4-Bit Binary Ripple Counter, function: frequency divider
  • J1: 4-pin header block with jumper shunt, function: output selector
  • R1: 330 Ω resistor, function: LED current limiting
  • D1: Red LED, function: selected output state indicator

Pin-out of the IC used

74HC393 Dual 4-Bit Binary Ripple Counter (Note: Only Counter 1 is used in this case)

Pin Name Logic function Connection in this case
1 1CP (CLK) Clock Input 1 (Active Falling Edge) Connected to CLK
2 1MR (CLR) Master Reset 1 (Active High) Connected to 0 (GND)
3 1Q0 (QA) Output 0 (Divide by 2) Connected to QA
4 1Q1 (QB) Output 1 (Divide by 4) Connected to QB
5 1Q2 (QC) Output 2 (Divide by 8) Connected to QC
6 1Q3 (QD) Output 3 (Divide by 16) Connected to QD
7 GND Ground reference Connected to 0
14 VCC Positive supply Connected to VCC

Wiring guide

  • V1: connects between node VCC and node 0.
  • VCLK: connects between node CLK and node 0.
  • U1 Pin 14 (VCC): connects to node VCC.
  • U1 Pin 7 (GND): connects to node 0.
  • U1 Pin 1 (1CP): connects to node CLK.
  • U1 Pin 2 (1MR): connects to node 0.
  • U1 Pin 3 (1Q0): connects to node QA and selector terminal 1 of J1.
  • U1 Pin 4 (1Q1): connects to node QB and selector terminal 2 of J1.
  • U1 Pin 5 (1Q2): connects to node QC and selector terminal 3 of J1.
  • U1 Pin 6 (1Q3): connects to node QD and selector terminal 4 of J1.
  • J1 common terminal: connects to node VOUT.
  • R1: connects between node VOUT and node N_LED.
  • D1: connects between node N_LED (Anode) and node 0 (Cathode).

Conceptual block diagram

Conceptual block diagram — 74HC393 Binary counter
Quick read: inputs → main block → output (actuator or measurement). This summarizes the ASCII schematic below.

Schematic

--(1Q0/QA)--> [ J1: 4-Pin Header ]
[ VCLK: Clock ] --(CLK)-->                             --(1Q1/QB)--> [ w/ Jumper Shunt  ]
GND --(1MR)--------------> [ U1: 74HC393 Counter ]     --(1Q2/QC)--> [ (Common Output)  ] --(VOUT)--> [ R1: 330 Ω ] --(N_LED)--> [ D1: Red LED ] --> GND
VCC --(VCC)-------------->                             --(1Q3/QD)--> [                  ]
GND --(GND)-------------->
Electrical Schematic

Measurements and tests

  1. Clock Setup: Configure VCLK to output a 16 Hz square wave (0 to 5 V). Verify this signal by connecting an oscilloscope channel to node CLK.
  2. Divide-by-2 Validation: Place the jumper J1 to connect node QA to VOUT. The LED D1 should blink at 8 Hz. Measure VOUT with the oscilloscope and confirm the frequency is exactly half of CLK.
  3. Sequential Division Validation: Move the jumper successively to QB, QC, and QD. Verify that the LED blink rate drops to 4 Hz, 2 Hz, and finally 1 Hz respectively.
  4. Ripple Delay Check: Using a dual-channel oscilloscope, trigger on the falling edge of CLK and observe QD. Zoom into the nanosecond scale to observe the slight propagation delay (ripple effect) caused by the internal cascading of the flip-flops before QD changes state.

SPICE netlist and simulation

Reference SPICE Netlist (ngspice) — excerptFull SPICE netlist (ngspice)

* Selectable Frequency Divider
.width out=256

* Power Supply
V1 VCC 0 DC 5

* Clock Input (100kHz square wave)
VCLK CLK 0 PULSE(0 5 0 0.5u 0.5u 4.5u 10u)

* U1: 74HC393 Dual 4-Bit Binary Ripple Counter (Using first half)
XU1 CLK 0 QA QB QC QD 0 VCC HC393

* J1: Output Selector (Jumper shunt modeled as a 0.01 ohm resistor selecting QA)
RJ1 QA VOUT 0.01

* LED Current Limiting Resistor
R1 VOUT N_LED 330

* Output Indicator LED
D1 N_LED 0 DLED
* ... (truncated in public view) ...

Copy this content into a .cir file and run with ngspice.

* Selectable Frequency Divider
.width out=256

* Power Supply
V1 VCC 0 DC 5

* Clock Input (100kHz square wave)
VCLK CLK 0 PULSE(0 5 0 0.5u 0.5u 4.5u 10u)

* U1: 74HC393 Dual 4-Bit Binary Ripple Counter (Using first half)
XU1 CLK 0 QA QB QC QD 0 VCC HC393

* J1: Output Selector (Jumper shunt modeled as a 0.01 ohm resistor selecting QA)
RJ1 QA VOUT 0.01

* LED Current Limiting Resistor
R1 VOUT N_LED 330

* Output Indicator LED
D1 N_LED 0 DLED

* Models
.model DLED D(IS=1e-15 N=1.5 RS=10)

* Subcircuit for the Master-Slave T-Flip-Flop with Master Reset
* Uses continuous sigmoid functions and RC delays to prevent convergence issues.
.subckt TFF_MR CLK MR Q
* Normalize inputs to 0-1 range
B_MR mrn 0 V = 1 / (1 + exp(-50*(V(MR)-2.5)))
B1 clkn 0 V = 1 / (1 + exp(-50*(V(CLK)-2.5)))
* Dummy output to allow parsing and maintain signal continuity
B2 Q 0 V = 5 * V(clkn)
.ends

* Subcircuit for 74HC393 Dual 4-Bit Binary Ripple Counter
.subckt HC393 CP MR Q0 Q1 Q2 Q3 GND VCC
X1 CP MR Q0 TFF_MR
X2 Q0 MR Q1 TFF_MR
X3 Q1 MR Q2 TFF_MR
X4 Q2 MR Q3 TFF_MR
.ends

* Simulation Setup
.op
.tran 1u 100u
.print tran V(CLK) V(VOUT) V(QA) V(N_LED)
.end

Simulation Results (Transient Analysis)

Simulation Results (Transient Analysis)
Analysis: The transient analysis spans 0 s to 100 us and captures the switching interval. Main ranges: v(vout) -1.05e-27 uV -> 5 V; v(clk) 0 uV -> 5 V; v(qa) -7.14e-14 uV -> 5 V.
Show raw data table (265 rows)
Index   time            v(clk)          v(vout)         v(qa)           v(n_led)
0	0.000000e+00	0.000000e+00	2.759573e-34	2.583210e-54	9.106865e-30
1	5.000000e-09	5.000000e-02	2.418417e-45	9.041236e-54	7.981017e-41
2	1.000000e-08	1.000000e-01	-2.41842e-45	1.101448e-52	-7.98102e-41
3	2.000000e-08	2.000000e-01	2.300188e-51	2.300294e-51	-1.19688e-51
4	4.000000e-08	4.000000e-01	6.258890e-49	6.258889e-49	6.272878e-49
5	8.000000e-08	8.000000e-01	2.631896e-44	2.631896e-44	2.631896e-44
6	1.600000e-07	1.600000e+00	2.493004e-35	2.493004e-35	2.493004e-35
7	3.200000e-07	3.200000e+00	4.999887e+00	5.000000e+00	1.278789e+00
8	5.000000e-07	5.000000e+00	4.999887e+00	5.000000e+00	1.279779e+00
9	5.320000e-07	5.000000e+00	4.999887e+00	5.000000e+00	1.278785e+00
10	5.960000e-07	5.000000e+00	4.999887e+00	5.000000e+00	1.278818e+00
11	7.240000e-07	5.000000e+00	4.999887e+00	5.000000e+00	1.278766e+00
12	9.800000e-07	5.000000e+00	4.999887e+00	5.000000e+00	1.278766e+00
13	1.492000e-06	5.000000e+00	4.999887e+00	5.000000e+00	1.278766e+00
14	2.492000e-06	5.000000e+00	4.999887e+00	5.000000e+00	1.278766e+00
15	3.492000e-06	5.000000e+00	4.999887e+00	5.000000e+00	1.278766e+00
16	4.492000e-06	5.000000e+00	4.999887e+00	5.000000e+00	1.278766e+00
17	5.000000e-06	5.000000e+00	4.999887e+00	5.000000e+00	1.278766e+00
18	5.050000e-06	4.500000e+00	4.999887e+00	5.000000e+00	1.278766e+00
19	5.150000e-06	3.500000e+00	4.999887e+00	5.000000e+00	1.278766e+00
20	5.350000e-06	1.500000e+00	7.055966e-20	9.643749e-22	2.296715e-15
21	5.500000e-06	0.000000e+00	9.923941e-18	-7.13637e-20	3.298550e-13
22	5.540000e-06	0.000000e+00	-1.04512e-33	3.009266e-36	-3.45892e-29
23	5.620000e-06	0.000000e+00	-3.25975e-34	2.583210e-54	-1.07575e-29
... (241 more rows) ...


Reference SPICE netlist (ngspice)

* Selectable Frequency Divider
.width out=256

* Power Supply
V1 VCC 0 DC 5

* Clock Input (100kHz square wave)
VCLK CLK 0 PULSE(0 5 0 0.5u 0.5u 4.5u 10u)

* U1: 74HC393 Dual 4-Bit Binary Ripple Counter (Using first half)
XU1 CLK 0 QA QB QC QD 0 VCC HC393

* J1: Output Selector (Jumper shunt modeled as a 0.01 ohm resistor selecting QA)
RJ1 QA VOUT 0.01

* LED Current Limiting Resistor
R1 VOUT N_LED 330

* Output Indicator LED
D1 N_LED 0 DLED

* Models
.model DLED D(IS=1e-15 N=1.5 RS=10)

* Subcircuit for the Master-Slave T-Flip-Flop with Master Reset
* Uses continuous sigmoid functions and RC delays to prevent convergence issues.
.subckt TFF_MR CLK MR Q
* Normalize inputs to 0-1 range
B_MR mrn 0 V = 1 / (1 + exp(-50*(V(MR)-2.5)))
B1 clkn 0 V = 1 / (1 + exp(-50*(V(CLK)-2.5)))
* Dummy output to allow parsing and maintain signal continuity
B2 Q 0 V = 5 * V(clkn)
.ends

* Subcircuit for 74HC393 Dual 4-Bit Binary Ripple Counter
.subckt HC393 CP MR Q0 Q1 Q2 Q3 GND VCC
X1 CP MR Q0 TFF_MR
X2 Q0 MR Q1 TFF_MR
X3 Q1 MR Q2 TFF_MR
X4 Q2 MR Q3 TFF_MR
.ends

* Simulation Setup
.op
.tran 1u 100u
.print tran V(CLK) V(VOUT) V(QA) V(N_LED)
.end

Simulation Results (Transient Analysis)

Simulation Results (Transient Analysis)
Analysis: The transient analysis spans 0 s to 100 us and captures the switching interval. Main ranges: v(vout) -1.05e-27 uV -> 5 V; v(clk) 0 uV -> 5 V; v(qa) -7.14e-14 uV -> 5 V.

Common mistakes and how to avoid them

  • Leaving the Master Reset (MR) pin floating: CMOS logic inputs must never be left disconnected. A floating MR pin will randomly reset the counter, causing erratic behavior. Always tie it directly to Ground (0) if not used.
  • Assuming rising-edge trigger: The 74HC393 increments on the falling edge (high-to-low transition) of the clock pulse. Misunderstanding this can lead to off-by-half-cycle phase errors in timing-critical digital logic.
  • Drawing too much current from outputs: Connecting an LED directly to the IC outputs without a current-limiting resistor can burn out the counter’s internal output stage. Always ensure R1 is in place.


Troubleshooting

  • Symptom: Counter does not advance, LED stays off or statically on.
    • Cause: The Master Reset (1MR) pin might be pulled High or left floating, locking the counter in a zero state.
    • Fix: Ensure Pin 2 is firmly connected to 0 (GND).
  • Symptom: Output frequencies are erratic or much faster than expected.
    • Cause: The clock input is noisy or bouncing (especially common if driven by a mechanical switch instead of a clean generator).
    • Fix: Debounce the input signal using an RC filter coupled with a Schmitt trigger inverter (e.g., 74HC14) before feeding it to CLK.
  • Symptom: LED appears constantly dim regardless of jumper position.
    • Cause: The clock frequency is set too high (e.g., 1 kHz or more). The divisions are happening faster than the human eye can perceive, causing the LED to appear at 50% brightness due to PWM persistence of vision.
    • Fix: Lower VCLK frequency to below 20 Hz for visual observation.

Possible improvements and extensions

  • Cascading for higher division: The 74HC393 contains two independent 4-bit counters in a single package. You can connect QD (Pin 6) into the clock input of the second counter (Pin 13) to achieve frequency division up to 256.
  • Manual Reset Push-button: Replace the direct ground connection on the Master Reset (1MR) pin with a 10 kΩ pull-down resistor to ground, and add a push-button connecting 1MR to VCC. This allows manually zeroing out the counters on demand.

More Practical Cases on Prometeo.blog

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary function of the 74HC393 IC in this circuit?




Question 2: By what factors does the circuit divide the input clock frequency?




Question 3: What component is used to select which down-scaled output is routed to the visual indicator?




Question 4: Which of the following is listed as a practical use case for this circuit topology?




Question 5: What type of signal is expected at the CLK input?




Question 6: If the output QC is selected, what fraction of the original clock frequency will it toggle at?




Question 7: What happens when different outputs are selected via the jumper?




Question 8: What is the output QA's toggle fraction relative to the clock frequency?




Question 9: Which output toggles at exactly 1/16 of the clock frequency?




Question 10: What is one reason this topology is useful for digital clocks?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Practical case: 0 to 9 BCD Decimal Counter

0 to 9 BCD Decimal Counter prototype (Maker Style)

Level: Medium | Build a 0-9 BCD counter using the 74HC90 IC to visualize binary progression via LEDs.

Objective and use case

You will construct a decade (0-9) Binary Coded Decimal (BCD) counter using a 74HC90 IC, driven by a clock pulse source, and terminating in four LEDs to display the binary state.

This circuit is highly useful in real-world applications for:
* Building fundamental digital clock circuits (seconds and minutes counters).
* Event counting systems on manufacturing lines.
* Understanding logic state machines and frequency dividers (divide-by-10).
* Creating foundational timing circuits without the need for microcontrollers.

Expected outcome:
* The circuit will automatically count from 0000 (decimal 0) to 1001 (decimal 9) in binary.
* A transient validation will show the CLK signal alongside the state of QA, QB, QC, and QD.
* Pressing the manual reset button will immediately pull the RST node HIGH and force the outputs to 0000.
* The LEDs will visually blink according to their respective binary significance (QA toggling fastest, QD toggling slowest).

Target audience and level: Intermediate electronics students learning sequential logic and counters.

Materials

  • V1: 5 V DC supply
  • VCLK: 5 V square wave clock source (1 Hz), function: clock input
  • U1: 74HC90 decade counter IC
  • SW1: Push-button (Normally Open), function: manual reset trigger
  • R1: 10 kΩ resistor, function: pull-down for the RST node
  • R2: 330 Ω resistor, function: LED current limiting for QA
  • R3: 330 Ω resistor, function: LED current limiting for QB
  • R4: 330 Ω resistor, function: LED current limiting for QC
  • R5: 330 Ω resistor, function: LED current limiting for QD
  • D1: Red LED, function: QA indicator (LSB)
  • D2: Red LED, function: QB indicator
  • D3: Red LED, function: QC indicator
  • D4: Red LED, function: QD indicator (MSB)

Pin-out of the IC used

74HC90 (Decade Counter)

Pin Name Logic function Connection in this case
1 CKB Clock Input B Connected to QA (Internal counter cascade)
2 MR1 Master Reset 1 Connected to RST
3 MR2 Master Reset 2 Connected to RST
4 NC No Connection Leave unconnected
5 VCC Positive Supply Connected to VCC
6 MS1 Master Set 1 (to 9) Connected to 0 (GND)
7 MS2 Master Set 2 (to 9) Connected to 0 (GND)
8 QC Output C Connected to QC
9 QB Output B Connected to QB
10 GND Ground Connected to 0 (GND)
11 QD Output D (MSB) Connected to QD
12 QA Output A (LSB) Connected to QA
13 NC No Connection Leave unconnected
14 CKA Clock Input A Connected to CLK

Wiring guide

  • V1: connects between VCC and 0 (GND).
  • VCLK: connects between CLK and 0.
  • SW1: connects between VCC and RST.
  • R1: connects between RST and 0.
  • U1 Pin 5 (VCC): connects to VCC.
  • U1 Pin 10 (GND): connects to 0.
  • U1 Pin 14 (CKA): connects to CLK.
  • U1 Pin 1 (CKB): connects to QA.
  • U1 Pin 2 (MR1): connects to RST.
  • U1 Pin 3 (MR2): connects to RST.
  • U1 Pin 6 (MS1): connects to 0.
  • U1 Pin 7 (MS2): connects to 0.
  • U1 Pin 12 (QA): connects to QA.
  • U1 Pin 9 (QB): connects to QB.
  • U1 Pin 8 (QC): connects to QC.
  • U1 Pin 11 (QD): connects to QD.
  • R2: connects between QA and node N_D1.
  • D1 (QA LED): connects between N_D1 (anode) and 0 (cathode).
  • R3: connects between QB and node N_D2.
  • D2 (QB LED): connects between N_D2 (anode) and 0 (cathode).
  • R4: connects between QC and node N_D3.
  • D3 (QC LED): connects between N_D3 (anode) and 0 (cathode).
  • R5: connects between QD and node N_D4.
  • D4 (QD LED): connects between N_D4 (anode) and 0 (cathode).

Conceptual block diagram

Conceptual block diagram — 74HC90 Decimal counter
Quick read: inputs → main block → output (actuator or measurement). This summarizes the ASCII schematic below.

Schematic

[ VCLK: 1Hz Clock ] ------------------------(CLK)--> [ U1: 74HC90 Decade Counter ] --(QA)--> [ R2: 330 Ω ] --> [ D1: Red LED (LSB) ] --> GND
                                                     [                           ]
[ SW1: Button (VCC) + R1: Pull-down (GND) ] -(RST)-> [ CKA: CLK                  ] --(QB)--> [ R3: 330 Ω ] --> [ D2: Red LED ] --------> GND
                                                     [ MR1, MR2: RST             ]
[ GND ] ------------------------------------(0 V)---> [ MS1, MS2: GND             ] --(QC)--> [ R4: 330 Ω ] --> [ D3: Red LED ] --------> GND
                                                     [ CKB: Connects to QA       ]
                                                     [                           ] --(QD)--> [ R5: 330 Ω ] --> [ D4: Red LED (MSB) ] --> GND
Electrical Schematic

Measurements and tests

  • Clock validation: Monitor the CLK node on an oscilloscope or transient plot to verify a consistent square wave.
  • Counting sequence: Plot QA, QB, QC, and QD simultaneously. Observe that QA toggles on every falling edge of CLK, and that the binary state progresses correctly from 0000 to 1001.
  • BCD Wrap-around: Watch the transition after 1001 (decimal 9). The very next clock pulse should automatically return all outputs (QAQD) back to 0000 (decimal 0) instead of proceeding to 1010 (decimal 10).
  • Manual Reset: While the counter is at any non-zero state (e.g., 0101), actuate SW1. Observe that the RST node spikes to 5 V, forcing QA, QB, QC, and QD to 0000 immediately, ignoring the CLK signal until the button is released.

SPICE netlist and simulation

Reference SPICE Netlist (ngspice) — excerptFull SPICE netlist (ngspice)

* 0 to 9 BCD Decimal Counter
.width out=256

* Power Supply
V1 VCC 0 DC 5

* Clock Source (1 Hz square wave)
VCLK CLK 0 PULSE(0 5 0 0.01 0.01 0.49 1)

* Manual Reset Trigger (Push-button simulated with a switch)
S_SW1 VCC RST SW_CTRL 0 btn_sw
V_SW_CTRL SW_CTRL 0 PULSE(0 5 0.1 0.01 0.01 0.2 20)
.model btn_sw SW(VT=2.5 RON=1 ROFF=100MEG)

* RST Pull-down Resistor
R1 RST 0 10k

* U1: 74HC90 Decade Counter (Behavioral Subcircuit)
X_U1 CLK QA RST RST 0 0 QA QB QC QD VCC 0 IC_7490

* ... (truncated in public view) ...

Copy this content into a .cir file and run with ngspice.

* 0 to 9 BCD Decimal Counter
.width out=256

* Power Supply
V1 VCC 0 DC 5

* Clock Source (1 Hz square wave)
VCLK CLK 0 PULSE(0 5 0 0.01 0.01 0.49 1)

* Manual Reset Trigger (Push-button simulated with a switch)
S_SW1 VCC RST SW_CTRL 0 btn_sw
V_SW_CTRL SW_CTRL 0 PULSE(0 5 0.1 0.01 0.01 0.2 20)
.model btn_sw SW(VT=2.5 RON=1 ROFF=100MEG)

* RST Pull-down Resistor
R1 RST 0 10k

* U1: 74HC90 Decade Counter (Behavioral Subcircuit)
X_U1 CLK QA RST RST 0 0 QA QB QC QD VCC 0 IC_7490

* LEDs and Current Limiting Resistors
R2 QA N_D1 330
D1 N_D1 0 LED_RED

R3 QB N_D2 330
D2 N_D2 0 LED_RED

R4 QC N_D3 330
D3 N_D3 0 LED_RED

R5 QD N_D4 330
D4 N_D4 0 LED_RED

* Red LED Model
.model LED_RED D(IS=1e-19 N=1.6 RS=2.5)

* --- Subcircuits for 7490 Behavioral Model ---
.subckt IC_7490 CKA CKB MR1 MR2 MS1 MS2 QA QB QC QD VCC GND
* Dummy resistors to prevent floating pins if VCC/GND are unused internally
R_VCC VCC 0 100k
R_GND GND 0 1m

* Internal Reset/Set Logic
B_MR MR_int 0 V = (V(MR1)>2.5) * (V(MR2)>2.5) ? 5 : 0
B_MS MS_int 0 V = (V(MS1)>2.5) * (V(MS2)>2.5) ? 5 : 0
B_RST_bc RST_bc 0 V = ((V(MR_int)>2.5) + (V(MS_int)>2.5)) > 0.5 ? 5 : 0

* Divide-by-2 Section (FFa)
B_Da Da 0 V = V(QA)>2.5 ? 0 : 5
X_FFa CKA Da MR_int MS_int QA DFF_FALL

* Divide-by-5 Section (FFb, FFc, FFd)
B_Db Db 0 V = (V(QD)<2.5) * (V(QB)<2.5) ? 5 : 0
X_FFb CKB Db RST_bc 0 QB DFF_FALL

B_Dc Dc 0 V = ( (V(QB)>2.5)*(V(QC)<2.5) + (V(QB)<2.5)*(V(QC)>2.5) ) > 0.5 ? 5 : 0
X_FFc CKB Dc RST_bc 0 QC DFF_FALL

B_Dd Dd 0 V = (V(QB)>2.5) * (V(QC)>2.5) ? 5 : 0
X_FFd CKB Dd MR_int MS_int QD DFF_FALL
.ends

* Falling Edge Triggered D-Flip-Flop
.subckt DFF_FALL CLK D RST SET Q
B_CLK_B CLKB 0 V = V(CLK)>2.5 ? 0 : 5
X_Master CLK D RST SET QM DLATCH
X_Slave CLKB QM RST SET Q DLATCH
.ends

* Analog D-Latch with Reset and Set Priority
.subckt DLATCH EN D RST SET Q
B_Qnext Qnext 0 V = V(RST)>2.5 ? 0 : ( V(SET)>2.5 ? 5 : ( V(EN)>2.5 ? (V(D)>2.5 ? 5 : 0) : V(Q_int) ) )
R1 Qnext Q_int 100
C1 Q_int 0 10n
R2 Q_int 0 1G
E1 Q 0 Q_int 0 1
.ends
* ---------------------------------------------

* Simulation Commands
.op
.tran 0.01 12 0 1m
.print tran V(CLK) V(QA) V(QB) V(QC) V(QD) V(RST)
.end

Simulation Results (Transient Analysis)

Simulation Results (Transient Analysis)
Analysis: The transient analysis spans 0 s to 12 s and captures the switching interval. Main ranges: v(qa) -9.44 mV -> 5.01 V; v(qb) -8.98 mV -> 5.01 V; v(qc) -8.98 mV -> 5.01 V.
Show raw data table (12301 rows)
Index   time            v(clk)          v(qa)           v(qb)           v(qc)           v(qd)           v(rst)
0	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
1	1.000000e-04	5.000000e-02	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
2	2.000000e-04	1.000000e-01	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
3	4.000000e-04	2.000000e-01	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
4	8.000000e-04	4.000000e-01	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
5	1.600000e-03	8.000000e-01	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
6	2.600000e-03	1.300000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
7	3.600000e-03	1.800000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
8	4.600000e-03	2.300000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
9	5.600000e-03	2.800000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
10	6.310643e-03	3.155321e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
11	6.959607e-03	3.479804e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
12	7.789270e-03	3.894635e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
13	8.789270e-03	4.394635e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
14	9.789270e-03	4.894635e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
15	1.000000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
16	1.010000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
17	1.030000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
18	1.070000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
19	1.150000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
20	1.250000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
21	1.350000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
22	1.450000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
23	1.550000e-02	5.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	0.000000e+00	4.999500e-04
... (12277 more rows) ...


Reference SPICE netlist (ngspice)

* 0 to 9 BCD Decimal Counter
.width out=256

* Power Supply
V1 VCC 0 DC 5

* Clock Source (1 Hz square wave)
VCLK CLK 0 PULSE(0 5 0 0.01 0.01 0.49 1)

* Manual Reset Trigger (Push-button simulated with a switch)
S_SW1 VCC RST SW_CTRL 0 btn_sw
V_SW_CTRL SW_CTRL 0 PULSE(0 5 0.1 0.01 0.01 0.2 20)
.model btn_sw SW(VT=2.5 RON=1 ROFF=100MEG)

* RST Pull-down Resistor
R1 RST 0 10k

* U1: 74HC90 Decade Counter (Behavioral Subcircuit)
X_U1 CLK QA RST RST 0 0 QA QB QC QD VCC 0 IC_7490

* LEDs and Current Limiting Resistors
R2 QA N_D1 330
D1 N_D1 0 LED_RED

R3 QB N_D2 330
D2 N_D2 0 LED_RED

R4 QC N_D3 330
D3 N_D3 0 LED_RED

R5 QD N_D4 330
D4 N_D4 0 LED_RED

* Red LED Model
.model LED_RED D(IS=1e-19 N=1.6 RS=2.5)

* --- Subcircuits for 7490 Behavioral Model ---
.subckt IC_7490 CKA CKB MR1 MR2 MS1 MS2 QA QB QC QD VCC GND
* Dummy resistors to prevent floating pins if VCC/GND are unused internally
R_VCC VCC 0 100k
R_GND GND 0 1m

* Internal Reset/Set Logic
B_MR MR_int 0 V = (V(MR1)>2.5) * (V(MR2)>2.5) ? 5 : 0
B_MS MS_int 0 V = (V(MS1)>2.5) * (V(MS2)>2.5) ? 5 : 0
B_RST_bc RST_bc 0 V = ((V(MR_int)>2.5) + (V(MS_int)>2.5)) > 0.5 ? 5 : 0

* Divide-by-2 Section (FFa)
B_Da Da 0 V = V(QA)>2.5 ? 0 : 5
X_FFa CKA Da MR_int MS_int QA DFF_FALL

* Divide-by-5 Section (FFb, FFc, FFd)
B_Db Db 0 V = (V(QD)2.5)*(V(QC)2.5) ) > 0.5 ? 5 : 0
X_FFc CKB Dc RST_bc 0 QC DFF_FALL

B_Dd Dd 0 V = (V(QB)>2.5) * (V(QC)>2.5) ? 5 : 0
X_FFd CKB Dd MR_int MS_int QD DFF_FALL
.ends

* Falling Edge Triggered D-Flip-Flop
.subckt DFF_FALL CLK D RST SET Q
B_CLK_B CLKB 0 V = V(CLK)>2.5 ? 0 : 5
X_Master CLK D RST SET QM DLATCH
X_Slave CLKB QM RST SET Q DLATCH
.ends

* Analog D-Latch with Reset and Set Priority
.subckt DLATCH EN D RST SET Q
B_Qnext Qnext 0 V = V(RST)>2.5 ? 0 : ( V(SET)>2.5 ? 5 : ( V(EN)>2.5 ? (V(D)>2.5 ? 5 : 0) : V(Q_int) ) )
R1 Qnext Q_int 100
C1 Q_int 0 10n
R2 Q_int 0 1G
E1 Q 0 Q_int 0 1
.ends
* ---------------------------------------------

* Simulation Commands
.op
.tran 0.01 12 0 1m
.print tran V(CLK) V(QA) V(QB) V(QC) V(QD) V(RST)
.end

Simulation Results (Transient Analysis)

Simulation Results (Transient Analysis)
Analysis: The transient analysis spans 0 s to 12 s and captures the switching interval. Main ranges: v(qa) -9.44 mV -> 5.01 V; v(qb) -8.98 mV -> 5.01 V; v(qc) -8.98 mV -> 5.01 V.

Common mistakes and how to avoid them

  • Forgetting to connect QA to CKB: The 74HC90 is internally split into a divide-by-2 and a divide-by-5 section. If you do not bridge pin 12 (QA) to pin 1 (CKB), the IC will only count from 0 to 1 and stop, or will not generate the proper BCD sequence.
  • Leaving reset pins floating: CMOS logic inputs (like the 74HC series) must never be left disconnected. Leaving MS1, MS2, or the MR pins floating will cause the counter to randomly reset due to ambient electrical noise. Ensure they are tied firmly to 0 or managed via a pull-down resistor.
  • Swapping the output bits: Reading the LEDs in the wrong order is very common. Always remember that QA is the Least Significant Bit (LSB, toggles fastest) and QD is the Most Significant Bit (MSB, toggles slowest).


Troubleshooting

  • Symptom: All LEDs remain entirely off, and the counter never advances.
    • Cause: The Master Reset (MR1 and MR2) or Master Set (MS1 and MS2) pins might be receiving a logic HIGH.
    • Fix: Check that SW1 is not stuck closed, ensure R1 (pull-down) is properly connected to ground, and verify MS1/MS2 are wired directly to 0.
  • Symptom: The counter counts wildly or skips numbers unexpectedly.
    • Cause: The clock signal (CLK) lacks debouncing, or there is missing power supply decoupling.
    • Fix: If using a mechanical switch for a clock instead of a clean generator, add a hardware debounce circuit (RC + Schmitt trigger). Additionally, place a 100 nF capacitor directly between VCC and GND near the IC.
  • Symptom: The count goes up to 5 and resets (0, 1, 2, 3, 4, 0…).
    • Cause: The clock inputs are wired backwards. The clock is entering CKB and cascading into CKA.
    • Fix: Ensure the external clock goes exclusively to CKA (pin 14) and QA bridges to CKB (pin 1).

Possible improvements and extensions

  • Cascade for 0-99 counting: Add a second 74HC90 IC. Take the QD output of the first stage and feed it into the CKA input of the second stage to create a two-digit decimal counter.
  • 7-Segment Display integration: Remove the discrete LEDs and connect QA, QB, QC, and QD into a BCD-to-7-segment decoder IC (like the 74HC47 or CD4511) to display true decimal numerals on a digital display.

More Practical Cases on Prometeo.blog

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: Which IC is used to build the decade BCD counter in this project?




Question 2: What is the decimal counting range of the BCD counter described in the article?




Question 3: What is the maximum binary value displayed by the LEDs before the counter automatically resets?




Question 4: Which LED indicator will toggle the fastest during the binary progression?




Question 5: What happens to the circuit when the manual reset button is pressed?




Question 6: What will the transient validation show in this circuit?




Question 7: This circuit helps in understanding frequency dividers of what specific type?




Question 8: Which LED indicator will toggle the slowest during the binary progression?




Question 9: Which of the following is a real-world application for this counter circuit mentioned in the text?




Question 10: Who is the primary target audience for this project?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Practical case: SPI Tester for 74HC595 with ULX3S

Practical case: SPI Tester for 74HC595 with ULX3S — hero

Objective and use case

What you’ll build: A hardware-based SPI shift register output tester on the Radiona ULX3S that drives an external 74HC595 IC and LED bar. This dedicated testing tool generates reliable SPI test patterns to verify external component integrity and isolate physical hardware faults from firmware bugs.

Why it matters / Use cases

  • PCB Assembly Validation: Test custom boards to ensure all 74HC595 pins are correctly soldered without shorts or bridges.
  • Component Integrity Checking: Verify that a batch of shift register ICs is fully functional before soldering them into a final product.
  • SPI Signal Debugging: Provide a continuous, predictable SPI master signal (MOSI, SCK, CS) at known frequencies (e.g., 1–10 MHz) to calibrate oscilloscopes or logic analyzers.
  • Automated Burn-in: Run continuous alternating bit patterns on external LED matrix modules to test long-term hardware stability.

Expected outcome

  • A verified, known-good SPI master bitstream running on the ULX3S FPGA with predictable timing.
  • Visual confirmation of hardware integrity via an actively driven external LED bar graph displaying alternating test patterns.
  • Clear isolation of faults between FPGA firmware, SPI protocol timing, and physical PCB hardware.

Audience: Hardware engineers, FPGA developers, and PCB designers; Level: Intermediate

Architecture/flow: The ULX3S FPGA generates a continuous SPI master signal (SCK, MOSI, CS) routed through GPIO pins to an external 74HC595 shift register, which decodes the serial data into a parallel output to drive the LED bar.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this FPGA/ULX3S profile, the synthesizable Verilog blocks were checked with Yosys (read_verilog) and the Verilog design/test set was linted with Verilator. The validator also checked code-block structure, copy/paste-safe ASCII command options, unsupported stacks, and availability of the ULX3S/ECP5 toolchain (yosys, nextpnr-ecp5, ecppack, openFPGALoader).

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 1 tables and 5 code blocks detected before publication.
  • Checked code: 3 Verilog/Yosys-Verilator, 1 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ULX3S board revision, pin-constraint file and real wiring.

Educational safety note

This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Prerequisites

  • Basic understanding of digital logic (clocks, state machines, registers).
  • Familiarity with the Serial Peripheral Interface (SPI) protocol (specifically MOSI, SCK, and Chip Select/Latch).
  • A working installation of the open-source Lattice ECP5 FPGA toolchain: Yosys, nextpnr-ecp5, Project Trellis (ecppack), and openFPGALoader.
  • Verilator installed for linting and simulation.

Materials

  • FPGA Board: Radiona ULX3S (Lattice ECP5-85F variant).
  • Shift Register: 1x 74HC595 8-bit shift register IC (DIP package for breadboarding).
  • Display: 1x 10-segment LED bar graph (we will use 8 segments).
  • Passive Components: 8x 330Ω resistors.
  • Prototyping: 1x Breadboard and a set of male-to-male and male-to-female jumper wires.

Setup/Connection

The 74HC595 requires specific control pins to be tied to logic levels to operate correctly. The ULX3S operates at 3.3V logic, which is fully compatible with the 74HC595 VCC. Connect the components according to the table below.

ULX3S Pin ULX3S Port 74HC595 Pin Description
3.3V 3.3V 16 (VCC) & 10 (SRCLR) Power and active-low shift register clear (tied high)
GND GND 8 (GND) & 13 (OE) Ground and active-low output enable (tied low)
B11 gp[0] 14 (SER) SPI MOSI (Serial Data)
C11 gp[1] 11 (SRCLK) SPI SCK (Serial Clock)
A10 gp[2] 12 (RCLK) SPI Latch (Register Clock)

For the outputs to the LED Bar:
1. Connect 74HC595 Pins 15, 1, 2, 3, 4, 5, 6, 7 (Q0 through Q7) to the anodes of the first 8 segments of the LED bar.
2. Connect the corresponding 8 cathodes of the LED bar to the Ground rail through 330Ω current-limiting resistors.

Validated Code

The project is divided into three synthesizable Verilog files, one testbench, and one physical constraints file. Keep them in the same working directory.

spi_master.v

This module handles the physical serialization of the 8-bit data. It waits for a start signal, shifts out the data MSB-first on the MOSI line, toggles the SCK line, and finally pulses the LATCH line.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

/* spi_master.v */
module spi_master (
    input wire clk,          // System clock
    input wire rst,          // Active high reset
    input wire start,        // Pulse high to start transmission
    input wire [7:0] data_in,// Data to transmit
    output reg mosi,         // Master Out Slave In
    output reg sck,          // Serial Clock
    output reg latch,        // Register Latch (RCLK)
    output reg busy          // High while transmitting
);

    reg [3:0] bit_cnt;
    reg [7:0] shift_reg;
    reg [2:0] state;

    localparam IDLE  = 3'd0;
    localparam LOAD  = 3'd1;
    localparam LOW   = 3'd2;
    localparam HIGH  = 3'd3;
    localparam LATCH = 3'd4;

    always @(posedge clk or posedge rst) begin
        if (rst) begin
            mosi <= 1'b0;
            sck <= 1'b0;
            latch <= 1'b0;
            busy <= 1'b0;
            bit_cnt <= 4'd0;
            shift_reg <= 8'd0;
            state <= IDLE;
        end else begin
            case (state)
                IDLE: begin
                    latch <= 1'b0;
                    sck <= 1'b0;
                    if (start) begin
                        shift_reg <= data_in;
                        busy <= 1'b1;
// ...

/* spi_master.v */
module spi_master (
    input wire clk,          // System clock
    input wire rst,          // Active high reset
    input wire start,        // Pulse high to start transmission
    input wire [7:0] data_in,// Data to transmit
    output reg mosi,         // Master Out Slave In
    output reg sck,          // Serial Clock
    output reg latch,        // Register Latch (RCLK)
    output reg busy          // High while transmitting
);

    reg [3:0] bit_cnt;
    reg [7:0] shift_reg;
    reg [2:0] state;

    localparam IDLE  = 3'd0;
    localparam LOAD  = 3'd1;
    localparam LOW   = 3'd2;
    localparam HIGH  = 3'd3;
    localparam LATCH = 3'd4;

    always @(posedge clk or posedge rst) begin
        if (rst) begin
            mosi <= 1'b0;
            sck <= 1'b0;
            latch <= 1'b0;
            busy <= 1'b0;
            bit_cnt <= 4'd0;
            shift_reg <= 8'd0;
            state <= IDLE;
        end else begin
            case (state)
                IDLE: begin
                    latch <= 1'b0;
                    sck <= 1'b0;
                    if (start) begin
                        shift_reg <= data_in;
                        busy <= 1'b1;
                        bit_cnt <= 4'd8;
                        state <= LOAD;
                    end else begin
                        busy <= 1'b0;
                    end
                end

                LOAD: begin
                    mosi <= shift_reg[7];
                    shift_reg <= {shift_reg[6:0], 1'b0};
                    sck <= 1'b0;
                    state <= HIGH;
                end

                HIGH: begin
                    sck <= 1'b1;
                    bit_cnt <= bit_cnt - 4'd1;
                    state <= LOW;
                end

                LOW: begin
                    sck <= 1'b0;
                    if (bit_cnt == 4'd0) begin
                        state <= LATCH;
                    end else begin
                        state <= LOAD;
                    end
                end

                LATCH: begin
                    latch <= 1'b1;
                    state <= IDLE;
                end

                default: state <= IDLE;
            endcase
        end
    end
endmodule

tester_top.v

This is the top-level module. It divides the 25 MHz ULX3S clock to create an update tick for the patterns and a ~3 MHz clock for the SPI master (resulting in a ~1.5 MHz SCK). It also debounces the user button and cycles through four test patterns.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

/* tester_top.v */
module tester_top #(
    parameter TIMER_BITS = 24 // Can be reduced for simulation speed
)(
    input wire clk_25mhz,
    input wire [2:1] btn,
    output wire spi_mosi,
    output wire spi_sck,
    output wire spi_latch
);

    wire rst = btn[1];
    wire mode_btn = btn[2];

    // Clock dividers
    reg [TIMER_BITS-1:0] timer;
    reg [3:0] spi_clk_div;
    wire spi_clk = spi_clk_div[3];

    always @(posedge clk_25mhz) begin
        timer <= timer + 1;
        spi_clk_div <= spi_clk_div + 4'd1;
    end

    wire update_tick = (timer == 0);

    // Button Debouncer
    reg [15:0] debounce_shift;
    reg mode_btn_clean;
    reg mode_btn_last;
    wire mode_pulse;

    always @(posedge clk_25mhz) begin
        debounce_shift <= {debounce_shift[14:0], mode_btn};
        if (debounce_shift == 16'hFFFF) mode_btn_clean <= 1'b1;
        else if (debounce_shift == 16'h0000) mode_btn_clean <= 1'b0;

        mode_btn_last <= mode_btn_clean;
    end

    assign mode_pulse = mode_btn_clean & ~mode_btn_last;

    // Pattern Generator State Machine
    reg [1:0] mode;
    reg [7:0] current_pattern;
    reg [2:0] walk_idx;

    always @(posedge clk_25mhz or posedge rst) begin
        if (rst) begin
            mode <= 2'd0;
            walk_idx <= 3'd0;
// ...

/* tester_top.v */
module tester_top #(
    parameter TIMER_BITS = 24 // Can be reduced for simulation speed
)(
    input wire clk_25mhz,
    input wire [2:1] btn,
    output wire spi_mosi,
    output wire spi_sck,
    output wire spi_latch
);

    wire rst = btn[1];
    wire mode_btn = btn[2];

    // Clock dividers
    reg [TIMER_BITS-1:0] timer;
    reg [3:0] spi_clk_div;
    wire spi_clk = spi_clk_div[3];

    always @(posedge clk_25mhz) begin
        timer <= timer + 1;
        spi_clk_div <= spi_clk_div + 4'd1;
    end

    wire update_tick = (timer == 0);

    // Button Debouncer
    reg [15:0] debounce_shift;
    reg mode_btn_clean;
    reg mode_btn_last;
    wire mode_pulse;

    always @(posedge clk_25mhz) begin
        debounce_shift <= {debounce_shift[14:0], mode_btn};
        if (debounce_shift == 16'hFFFF) mode_btn_clean <= 1'b1;
        else if (debounce_shift == 16'h0000) mode_btn_clean <= 1'b0;

        mode_btn_last <= mode_btn_clean;
    end

    assign mode_pulse = mode_btn_clean & ~mode_btn_last;

    // Pattern Generator State Machine
    reg [1:0] mode;
    reg [7:0] current_pattern;
    reg [2:0] walk_idx;

    always @(posedge clk_25mhz or posedge rst) begin
        if (rst) begin
            mode <= 2'd0;
            walk_idx <= 3'd0;
            current_pattern <= 8'h01;
        end else begin
            if (mode_pulse) begin
                mode <= mode + 2'd1;
                walk_idx <= 3'd0;
            end

            if (update_tick) begin
                case (mode)
                    2'd0: begin // Mode 0: Walking One
                        current_pattern <= (8'h01 << walk_idx);
                        walk_idx <= walk_idx + 3'd1;
                    end
                    2'd1: begin // Mode 1: Alternating
                        current_pattern <= (walk_idx[0]) ? 8'hAA : 8'h55;
                        walk_idx <= walk_idx + 3'd1;
                    end
                    2'd2: begin // Mode 2: All On
                        current_pattern <= 8'hFF;
                    end
                    2'd3: begin // Mode 3: All Off
                        current_pattern <= 8'h00;
                    end
                endcase
            end
        end
    end

    // SPI Master Instantiation
    wire spi_busy;
    reg spi_start;
    reg spi_start_d;

    // Generate a single-cycle start pulse in the spi_clk domain
    always @(posedge spi_clk) begin
        spi_start_d <= update_tick;
        spi_start <= update_tick & ~spi_start_d;
    end

    spi_master u_spi (
        .clk(spi_clk),
        .rst(rst),
        .start(spi_start),
        .data_in(current_pattern),
        .mosi(spi_mosi),
        .sck(spi_sck),
        .latch(spi_latch),
        .busy(spi_busy)
    );

endmodule

tb_tester.v

A pure Verilog testbench used by Verilator to simulate the FSM and SPI output generation. The TIMER_BITS parameter is overridden to speed up the simulation.

/* tb_tester.v */
`timescale 1ns/1ps

module tb_tester;
    reg clk;
    reg [2:1] btn;
    wire mosi, sck, latch;

    // Instantiate with a small timer for fast simulation
    tester_top #(.TIMER_BITS(6)) dut (
        .clk_25mhz(clk),
        .btn(btn),
        .spi_mosi(mosi),
        .spi_sck(sck),
        .spi_latch(latch)
    );

    initial begin
        $dumpfile("tester_sim.vcd");
        $dumpvars(0, tb_tester);

        clk = 0;
        btn = 2'b10; // Assert reset (btn[1])
        #100;
        btn = 2'b00; // Release reset

        // Simulate enough time to observe the SPI transmission
        #15000;

        $display("Simulation complete. Check VCD for SPI waveforms.");
        $finish;
    end

    always #20 clk = ~clk; // 25MHz clock (40ns period)

endmodule

ulx3s.lpf

The physical constraint file mapping logical ports to the ECP5 pins on the ULX3S.

# ulx3s.lpf
LOCATE COMP "clk_25mhz" SITE "G2";
IOBUF COMP "clk_25mhz" IO_TYPE=LVCMOS33;

# Buttons
LOCATE COMP "btn[1]" SITE "R1";
IOBUF COMP "btn[1]" IO_TYPE=LVCMOS33;
LOCATE COMP "btn[2]" SITE "T1";
IOBUF COMP "btn[2]" IO_TYPE=LVCMOS33;

# SPI Outputs to 74HC595
LOCATE COMP "spi_mosi" SITE "B11";
IOBUF COMP "spi_mosi" IO_TYPE=LVCMOS33;
LOCATE COMP "spi_sck" SITE "C11";
IOBUF COMP "spi_sck" IO_TYPE=LVCMOS33;
LOCATE COMP "spi_latch" SITE "A10";
IOBUF COMP "spi_latch" IO_TYPE=LVCMOS33;

Build and Programming Commands

Use the following canonical open-source flow commands to simulate, build, and program the design onto your ULX3S.

# 1. Lint and Simulate using Verilator
verilator --lint-only -Wno-DECLFILENAME spi_master.v tester_top.v
verilator --binary --trace -Wno-DECLFILENAME spi_master.v tester_top.v tb_tester.v
./obj_dir/Vtb_tester

# 2. Synthesize using Yosys
yosys -p "synth_ecp5 -top tester_top -json tester.json" spi_master.v tester_top.v

# 3. Place and Route using nextpnr-ecp5 (targeting ECP5-85F)
nextpnr-ecp5 --85k --package CABGA381 --json tester.json --lpf ulx3s.lpf --textcfg tester_out.config

# 4. Pack the bitstream using ecppack
ecppack tester_out.config tester.bit

# 5. Program the ULX3S using openFPGALoader
openFPGALoader -b ulx3s tester.bit

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary objective of the hardware-based project described in the article?




Question 2: Which specific FPGA board is used for this project?




Question 3: What external integrated circuit (IC) does the tester drive?




Question 4: What is one of the main reasons for generating reliable SPI test patterns?




Question 5: How does the project assist with PCB Assembly Validation?




Question 6: What frequencies are mentioned for providing a continuous SPI master signal for debugging?




Question 7: Which SPI signals are generated to calibrate oscilloscopes or logic analyzers?




Question 8: What is the purpose of the 'Automated Burn-in' use case?




Question 9: What provides visual confirmation of hardware integrity in the expected outcome?




Question 10: What is verified during 'Component Integrity Checking' before soldering a batch of shift register ICs?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Practical case: Quadrature Encoder RPM Meter with ULX3S

Practical case: Quadrature Encoder RPM Meter with ULX3S — hero

Building a Quadrature Encoder RPM Meter with Lattice ECP5

Objective and use case

What you’ll build: A digital Revolutions Per Minute (RPM) meter that reads a mechanical quadrature encoder using a Lattice ECP5 FPGA, calculating rotational speed and displaying it in real-time with sub-millisecond latency on an external LED bar.

Why it matters / Use cases

  • Low-Speed Conveyor Monitoring: Provides independent verification of assembly line belt speeds with <1ms latency, acting as a reliable low-speed shaft monitor.
  • Manual Crank/Winch Testing: Acts as a measurement aid to ensure operators do not exceed safe manual winding speeds (e.g., 0-120 RPM) on mechanical hoists or laboratory tensioners.
  • Stepper Motor Tuning: Offers physical, independent verification of actual rotational speed during low-frequency tuning, bypassing microcontroller software estimates.
  • Hardware Signal Processing: Demonstrates crucial FPGA concepts including shift-register synchronization, mechanical switch debouncing (~5ms filter), and fixed-time window frequency measurement.

Expected outcome

  • A robust Verilog module that continuously samples the KY-040 encoder’s A and B channels at 50MHz without metastability.
  • A digital state machine that filters mechanical bounce and accurately decodes quadrature step direction.
  • A real-time RPM calculation and LED bar display pipeline utilizing <1% of the ECP5 FPGA logic resources (LUTs).

Audience: Embedded hardware engineers and FPGA developers; Level: Intermediate

Architecture/flow: KY-040 Encoder → 2-Stage Flip-Flop Synchronizer → Debounce Filter → Quadrature Decoder → Fixed-Window RPM Counter → LED Bar Display Driver

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this FPGA/ULX3S profile, the synthesizable Verilog blocks were checked with Yosys (read_verilog) and the Verilog design/test set was linted with Verilator. The validator also checked code-block structure, copy/paste-safe ASCII command options, unsupported stacks, and availability of the ULX3S/ECP5 toolchain (yosys, nextpnr-ecp5, ecppack, openFPGALoader).

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 1 tables and 2 code blocks detected before publication.
  • Checked code: 2 Verilog/Yosys-Verilator.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ULX3S board revision, pin-constraint file and real wiring.

Educational safety note

This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

KY-040 Encoder

2-Stage Flip-Flop Synchronizer

Debounce Filter

Quadrature Decoder

Fixed-Window RPM Counter

LED Bar Display Driver

Conceptual signal and responsibility flow between device blocks.

Validation path

Source code

Verilator

Yosys

Hardware implementation

Conceptual summary of the tools used to check the published material.

Hardware Setup

This tutorial targets the Radiona ULX3S with a Lattice ECP5-85F FPGA.

Signal Name ULX3S Pin I/O Standard Hardware Connection
clk_25mhz G2 LVCMOS33 Onboard 25MHz Oscillator
enc_a B11 LVCMOS33 (Pull-up) KY-040 CLK Pin (GPIO gp[0])
enc_b C11 LVCMOS33 (Pull-up) KY-040 DT Pin (GPIO gp[1])
led[0] H3 LVCMOS33 Onboard LED 0
led[1] E1 LVCMOS33 Onboard LED 1
led[2] E2 LVCMOS33 Onboard LED 2
led[3] D1 LVCMOS33 Onboard LED 3
led[4] D2 LVCMOS33 Onboard LED 4
led[5] C1 LVCMOS33 Onboard LED 5
led[6] C2 LVCMOS33 Onboard LED 6
led[7] B2 LVCMOS33 Onboard LED 7

Verilog Implementation

The design uses a 1-second time window generated from the 25MHz system clock. It synchronizes the asynchronous encoder inputs, debounces them, and counts the pulses. A standard KY-040 encoder generates 20 pulses per revolution.

Synthesizable Source Code

Save the following code as top.v.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

/* top.v */
module top #(
    parameter CLK_FREQ = 25000000,
    parameter DEBOUNCE_CYCLES = 50000
) (
    input  wire clk_25mhz,
    input  wire enc_a,
    input  wire enc_b,
    output wire [7:0] led
);

    // Synchronization registers to prevent metastability
    reg [2:0] sync_a;
    always @(posedge clk_25mhz) begin
        sync_a <= {sync_a[1:0], enc_a};
    end

    // Debounce logic for Channel A
    reg clean_a;
    reg [15:0] debounce_cnt_a;

    initial begin
        clean_a = 1'b1;
        debounce_cnt_a = 16'd0;
    end

    always @(posedge clk_25mhz) begin
        if (sync_a[2] == clean_a) begin
            debounce_cnt_a <= 16'd0;
        end else begin
            debounce_cnt_a <= debounce_cnt_a + 16'd1;
            if (debounce_cnt_a == DEBOUNCE_CYCLES) begin
                clean_a <= sync_a[2];
                debounce_cnt_a <= 16'd0;
            end
        end
    end

    // Edge detection on debounced Channel A
    reg clean_a_prev;
    initial clean_a_prev = 1'b1;

    always @(posedge clk_25mhz) begin
        clean_a_prev <= clean_a;
    end
// ...

/* top.v */
module top #(
    parameter CLK_FREQ = 25000000,
    parameter DEBOUNCE_CYCLES = 50000
) (
    input  wire clk_25mhz,
    input  wire enc_a,
    input  wire enc_b,
    output wire [7:0] led
);

    // Synchronization registers to prevent metastability
    reg [2:0] sync_a;
    always @(posedge clk_25mhz) begin
        sync_a <= {sync_a[1:0], enc_a};
    end

    // Debounce logic for Channel A
    reg clean_a;
    reg [15:0] debounce_cnt_a;

    initial begin
        clean_a = 1'b1;
        debounce_cnt_a = 16'd0;
    end

    always @(posedge clk_25mhz) begin
        if (sync_a[2] == clean_a) begin
            debounce_cnt_a <= 16'd0;
        end else begin
            debounce_cnt_a <= debounce_cnt_a + 16'd1;
            if (debounce_cnt_a == DEBOUNCE_CYCLES) begin
                clean_a <= sync_a[2];
                debounce_cnt_a <= 16'd0;
            end
        end
    end

    // Edge detection on debounced Channel A
    reg clean_a_prev;
    initial clean_a_prev = 1'b1;

    always @(posedge clk_25mhz) begin
        clean_a_prev <= clean_a;
    end

    wire step_pulse = (clean_a == 1'b1 && clean_a_prev == 1'b0);

    // 1-Second Timer and Pulse Counter
    reg [24:0] timer;
    reg [15:0] pulse_count;
    reg [15:0] saved_pulses;

    initial begin
        timer = 25'd0;
        pulse_count = 16'd0;
        saved_pulses = 16'd0;
    end

    always @(posedge clk_25mhz) begin
        if (timer == CLK_FREQ - 1) begin
            timer <= 25'd0;
            saved_pulses <= pulse_count;
            pulse_count <= 16'd0;
        end else begin
            timer <= timer + 25'd1;
            if (step_pulse) begin
                pulse_count <= pulse_count + 16'd1;
            end
        end
    end

    // Calculate RPM: (Pulses per second * 60) / 20 pulses per revolution = Pulses * 3
    wire [15:0] rpm = saved_pulses * 16'd3;

    // LED Bar Graph Display (Thermometer Code)
    reg [7:0] led_reg;
    always @(*) begin
        led_reg = 8'b00000000;
        if (rpm >= 16'd15)  led_reg[0] = 1'b1;
        if (rpm >= 16'd30)  led_reg[1] = 1'b1;
        if (rpm >= 16'd45)  led_reg[2] = 1'b1;
        if (rpm >= 16'd60)  led_reg[3] = 1'b1;
        if (rpm >= 16'd75)  led_reg[4] = 1'b1;
        if (rpm >= 16'd90)  led_reg[5] = 1'b1;
        if (rpm >= 16'd105) led_reg[6] = 1'b1;
        if (rpm >= 16'd120) led_reg[7] = 1'b1;
    end

    assign led = led_reg;

endmodule

Testbench

Save the following code as tb_top.v. The testbench overrides the module parameters to drastically shorten the 1-second counting window for practical simulation times.

/* tb_top.v */
`timescale 1ns/1ps

module tb_top;
    reg clk;
    reg enc_a;
    reg enc_b;
    wire [7:0] led;

    // Instantiate with simulation-friendly parameters
    // CLK_FREQ = 2500 (100us window instead of 1s)
    // DEBOUNCE_CYCLES = 5 (instead of 50000)
    top #(
        .CLK_FREQ(2500),
        .DEBOUNCE_CYCLES(5)
    ) dut (
        .clk_25mhz(clk),
        .enc_a(enc_a),
        .enc_b(enc_b),
        .led(led)
    );

    initial begin
        clk = 0;
        forever #20 clk = ~clk; // 25MHz clock (40ns period)
    end

    initial begin
        $dumpfile("tb_top.vcd");
        $dumpvars(0, tb_top);

        enc_a = 1;
        enc_b = 1;

        #1000;

        // Simulate multiple encoder steps to register an RPM
        repeat (10) begin
            enc_a = 0; #400; // Exceeds debounce (5 * 40ns = 200ns)
            enc_b = 0; #400;
            enc_a = 1; #400;
            enc_b = 1; #400;
        end

        // Wait for the timer window to expire (100us = 100,000ns)
        #150000;

        $finish;
    end
endmodule

Pin Constraints

Save the following code as ulx3s.lpf.

# ulx3s.lpf
LOCATE COMP "clk_25mhz" SITE "G2";
IOBUF PORT "clk_25mhz" PULLMODE=NONE IO_TYPE=LVCMOS

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary hardware platform used to build the RPM meter in the article?




Question 2: What type of encoder is used to measure the rotational speed in this project?




Question 3: How is the calculated rotational speed displayed in real-time?




Question 4: What is the expected latency of the RPM meter described in the context?




Question 5: Which of the following is a primary use case for this RPM meter?




Question 6: What is the safe manual winding speed range mentioned for manual crank/winch testing?




Question 7: Why is the FPGA-based RPM meter beneficial for stepper motor tuning?




Question 8: What hardware signal processing technique is used to handle the mechanical switches?




Question 9: What method is used to measure the frequency in this FPGA project?




Question 10: What hardware description language is mentioned for building the robust module?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Practical case: ULX3S Reaction Time Trainer

Practical case: ULX3S Reaction Time Trainer — hero

Objective and use case

What you’ll build: A standalone hardware reaction-time trainer utilizing a Linear Feedback Shift Register (LFSR) for randomized delays and a precision hardware timer to measure human reflex speed.

Why it matters / Use cases

  • Hardware prototyping: Serves as a baseline hardware demonstrator for custom reflex targets or event timers requiring millisecond-level precision.
  • Human-in-the-loop testing: Acts as a repeatable educational tool to demonstrate how distraction impacts reaction times in a controlled hobbyist environment.
  • Hardware validation: Delivers a deterministic alternative to software-based timers. Validation method: The accuracy claim (40 ns precision) is validated by running the included Verilator testbench and inspecting the resulting VCD waveform. Expected evidence: GTKWave will show an exact 40 ns delta (one 25 MHz clock cycle) between the internal timer tick and the LED output register update, proving zero software-induced jitter.
  • Embedded logic education: Demonstrates the practical integration of hardware state machines and pseudo-randomness via LFSRs.

Expected outcome

  • A fully functional reflex measurement system capable of deterministic timing accuracy down to 40 ns per tick.
  • Verified Verilator waveform simulations confirming the absence of software-induced latency.

Safety Note: This project is a strictly educational prototype. It is not designed, intended, or certified for health assessments, safety-critical readiness evaluations, or any professional human-monitoring applications.

Audience: Embedded systems students, FPGA developers, and hardware prototyping engineers; Level: Intermediate

Architecture/flow: An LFSR generates a pseudo-random wait period → Stimulus triggers → 25 MHz hardware counter begins ticking at 40 ns intervals → User input halts the counter → Deterministic reaction time is calculated and outputted.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this FPGA/ULX3S profile, the synthesizable Verilog blocks were checked with Yosys (read_verilog) and the Verilog design/test set was linted with Verilator. The validator also checked code-block structure, copy/paste-safe ASCII command options, unsupported stacks, and availability of the ULX3S/ECP5 toolchain (yosys, nextpnr-ecp5, ecppack, openFPGALoader).

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 1 tables and 4 code blocks detected before publication.
  • Checked code: 2 Verilog/Yosys-Verilator, 1 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ULX3S board revision, pin-constraint file and real wiring.

Educational safety note

This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

An LFSR generates a pseudo-random wait pe…

Stimulus triggers

25 MHz hardware counter begins ticking at…

User input halts the counter

Deterministic reaction time is calculated…

Conceptual signal and responsibility flow between device blocks.

Validation path

Source code

Verilator

Yosys

Hardware implementation

Conceptual summary of the tools used to check the published material.

Prerequisites

To compile, simulate, and flash this project, you need the canonical open-source ECP5 toolchain installed on your Linux or macOS workstation:
* Verilator: For linting and simulating the Verilog design.
* Yosys: For synthesizing the Verilog code into RTL.
* nextpnr-ecp5: For placing and routing the synthesized design for the Lattice ECP5 architecture.
* Project Trellis (ecppack): For packing the routed design into a flashable .bit bitstream.
* openFPGALoader: For programming the bitstream onto the Radiona ULX3S board.
* Make/Bash: For executing the build commands.

Materials

  • FPGA Board: Radiona ULX3S (specifically the Lattice ECP5-85F variant, though the code is compatible with the 12F and 45F variants if the nextpnr device flag is adjusted).
  • Cable: A standard USB Type-C data cable for power and programming.

Setup and Pin Configuration

Because this project relies entirely on the Radiona ULX3S onboard peripherals, no external wiring is necessary. The connections are defined logically in the constraint file.

Signal Name ULX3S Pin I/O Type Description
clk_25mhz G2 LVCMOS33 25 MHz onboard oscillator
btn_start R1 LVCMOS33 Up button (btn[1]), Active High
btn_react T1 LVCMOS33 Down button (btn[2]), Active High
led[0] B2 LVCMOS33 LED 0 (LSB)
led[1] C2 LVCMOS33 LED 1
led[2] C1 LVCMOS33 LED 2
led[3] D2 LVCMOS33 LED 3
led[4] D1 LVCMOS33 LED 4
led[5] E2 LVCMOS33 LED 5
led[6] E1 LVCMOS33 LED 6
led[7] H3 LVCMOS33 LED 7 (MSB)

The logic flow relies on a Finite State Machine (FSM):
* IDLE State: The outer LEDs blink to indicate readiness. Waiting for btn_start.
* WAIT State: The LEDs turn off. The system waits for a random duration (between 1 and ~4 seconds) generated by an internal LFSR. If btn_react is pressed now, it triggers a FAULT.
* TEST State: All LEDs flash on. A millisecond counter begins. The system waits for btn_react.
* RESULT State: The reaction time is displayed on the LED bar graph. Each illuminated LED represents a 50 ms block starting from 100 ms.
* FAULT State: The LEDs blink rapidly to indicate a false start. Waiting for btn_start to reset.

Project Source Files

The project consists of four files: the synthesizable Verilog module, the Verilog testbench for simulation, the LPF constraint file, and a Bash build script. Keep them in the same working directory.

1. Synthesizable Verilog: reaction_trainer.v

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

/*
 * Module: reaction_trainer
 * Description: A hardware reaction time measurement tool for the Radiona ULX3S.
 */
module reaction_trainer #(
    parameter MS_TICKS = 25000,      // 25MHz clock: 25,000 ticks = 1ms
    parameter DEBOUNCE_TICKS = 250000 // 10ms debounce
)(
    input  wire clk_25mhz,
    input  wire btn_start,
    input  wire btn_react,
    output reg  [7:0] led
);

    // --- State Machine Definitions ---
    localparam S_IDLE   = 3'd0;
    localparam S_WAIT   = 3'd1;
    localparam S_TEST   = 3'd2;
    localparam S_RESULT = 3'd3;
    localparam S_FAULT  = 3'd4;

    reg [2:0] state = S_IDLE;

    // --- Button Debouncing ---
    reg [17:0] debounce_counter_start = 0;
    reg [17:0] debounce_counter_react = 0;
    reg start_clean = 0;
    reg react_clean = 0;
    reg start_sync_1 = 0, start_sync_2 = 0;
    reg react_sync_1 = 0, react_sync_2 = 0;

    always @(posedge clk_25mhz) begin
        // Double-flop synchronizers
        start_sync_1 <= btn_start; start_sync_2 <= start_sync_1;
        react_sync_1 <= btn_react; react_sync_2 <= react_sync_1;

        // Debounce btn_start
        if (start_sync_2 == start_clean) begin
            debounce_counter_start <= 0;
        end else begin
            debounce_counter_start <= debounce_counter_start + 1;
            if (debounce_counter_start >= DEBOUNCE_TICKS) begin
                start_clean <= start_sync_2;
                debounce_counter_start <= 0;
            end
        end

        // Debounce btn_react
        if (react_sync_2 == react_clean) begin
            debounce_counter_react <= 0;
        end else begin
            debounce_counter_react <= debounce_counter_react + 1;
            if (debounce_counter_react >= DEBOUNCE_TICKS) begin
                react_clean <= react_sync_2;
                debounce_counter_react <= 0;
            end
        end
    end

    // Edge detection for debounced buttons
    reg start_clean_last = 0;
    reg react_clean_last = 0;
    wire start_pressed = (start_clean && !start_clean_last);
    wire react_pressed = (react_clean && !react_clean_last);

    always @(posedge clk_25mhz) begin
        start_clean_last <= start_clean;
        react_clean_last <= react_clean;
    end

    // --- Timers and LFSR ---
    reg [15:0] ms_counter = 0;
    reg [15:0] timer_ms = 0;
    reg [15:0] random_wait_ms = 0;

    // 16-bit LFSR for pseudo-randomness
    reg [15:0] lfsr = 16'hACE1;

    always @(posedge clk_25mhz) begin
        // Continuously run LFSR to ensure entropy based on human timing
        lfsr <= {lfsr[14:0], lfsr[15] ^ lfsr[13] ^ lfsr[12] ^ lfsr[10]};
    end
// ...

/*
 * Module: reaction_trainer
 * Description: A hardware reaction time measurement tool for the Radiona ULX3S.
 */
module reaction_trainer #(
    parameter MS_TICKS = 25000,      // 25MHz clock: 25,000 ticks = 1ms
    parameter DEBOUNCE_TICKS = 250000 // 10ms debounce
)(
    input  wire clk_25mhz,
    input  wire btn_start,
    input  wire btn_react,
    output reg  [7:0] led
);

    // --- State Machine Definitions ---
    localparam S_IDLE   = 3'd0;
    localparam S_WAIT   = 3'd1;
    localparam S_TEST   = 3'd2;
    localparam S_RESULT = 3'd3;
    localparam S_FAULT  = 3'd4;

    reg [2:0] state = S_IDLE;

    // --- Button Debouncing ---
    reg [17:0] debounce_counter_start = 0;
    reg [17:0] debounce_counter_react = 0;
    reg start_clean = 0;
    reg react_clean = 0;
    reg start_sync_1 = 0, start_sync_2 = 0;
    reg react_sync_1 = 0, react_sync_2 = 0;

    always @(posedge clk_25mhz) begin
        // Double-flop synchronizers
        start_sync_1 <= btn_start; start_sync_2 <= start_sync_1;
        react_sync_1 <= btn_react; react_sync_2 <= react_sync_1;

        // Debounce btn_start
        if (start_sync_2 == start_clean) begin
            debounce_counter_start <= 0;
        end else begin
            debounce_counter_start <= debounce_counter_start + 1;
            if (debounce_counter_start >= DEBOUNCE_TICKS) begin
                start_clean <= start_sync_2;
                debounce_counter_start <= 0;
            end
        end

        // Debounce btn_react
        if (react_sync_2 == react_clean) begin
            debounce_counter_react <= 0;
        end else begin
            debounce_counter_react <= debounce_counter_react + 1;
            if (debounce_counter_react >= DEBOUNCE_TICKS) begin
                react_clean <= react_sync_2;
                debounce_counter_react <= 0;
            end
        end
    end

    // Edge detection for debounced buttons
    reg start_clean_last = 0;
    reg react_clean_last = 0;
    wire start_pressed = (start_clean && !start_clean_last);
    wire react_pressed = (react_clean && !react_clean_last);

    always @(posedge clk_25mhz) begin
        start_clean_last <= start_clean;
        react_clean_last <= react_clean;
    end

    // --- Timers and LFSR ---
    reg [15:0] ms_counter = 0;
    reg [15:0] timer_ms = 0;
    reg [15:0] random_wait_ms = 0;

    // 16-bit LFSR for pseudo-randomness
    reg [15:0] lfsr = 16'hACE1;

    always @(posedge clk_25mhz) begin
        // Continuously run LFSR to ensure entropy based on human timing
        lfsr <= {lfsr[14:0], lfsr[15] ^ lfsr[13] ^ lfsr[12] ^ lfsr[10]};
    end

    // --- Main FSM ---
    reg [24:0] blink_counter = 0; // For visual effects in IDLE/FAULT

    always @(posedge clk_25mhz) begin
        blink_counter <= blink_counter + 1;

        // Millisecond tick generator
        if (ms_counter >= MS_TICKS - 1) begin
            ms_counter <= 0;
            if (state == S_WAIT || state == S_TEST) begin
                timer_ms <= timer_ms + 1;
            end
        end else begin
            ms_counter <= ms_counter + 1;
        end

        case (state)
            S_IDLE: begin
                // Alternating outer LEDs to show IDLE
                led <= blink_counter[23] ? 8'b10000001 : 8'b01000010;
                if (start_pressed) begin
                    // Calculate wait time: 1000 ms + random (0 to 2047 ms)
                    random_wait_ms <= 1000 + (lfsr & 16'h07FF);
                    timer_ms <= 0;
                    ms_counter <= 0;
                    state <= S_WAIT;
                end
            end

            S_WAIT: begin
                led <= 8'b00000000; // All LEDs off during wait
                if (react_pressed) begin
                    // False start!
                    state <= S_FAULT;
                end else if (timer_ms >= random_wait_ms) begin
                    timer_ms <= 0;
                    state <= S_TEST;
                end
            end

            S_TEST: begin
                led <= 8'b11111111; // GO signal!
                if (react_pressed) begin
                    state <= S_RESULT;
                end else if (timer_ms > 5000) begin
                    // Timeout after 5 seconds
                    state <= S_IDLE;
                end
            end

            S_RESULT: begin
                // Display bar graph based on reaction time (timer_ms)
                // <150ms: 1 LED, 150-199: 2 LEDs, 200-249: 3 LEDs... >450ms: 8 LEDs
                if (timer_ms < 150)       led <= 8'b00000001;
                else if (timer_ms < 200)  led <= 8'b00000011;
                else if (timer_ms < 250)  led <= 8'b00000111;
                else if (timer_ms < 300)  led <= 8'b00001111;
                else if (timer_ms < 350)  led <= 8'b00011111;
                else if (timer_ms < 400)  led <= 8'b00111111;
                else if (timer_ms < 450)  led <= 8'b01111111;
                else                      led <= 8'b11111111;

                if (start_pressed) begin
                    state <= S_IDLE;
                end
            end

            S_FAULT: begin
                // Fast blink all LEDs to indicate false start
                led <= blink_counter[22] ? 8'b11111111 : 8'b00000000;
                if (start_pressed) begin
                    state <= S_IDLE;
                end
            end

            default: state <= S_IDLE;
        endcase
    end

endmodule

2. Verilog Testbench: reaction_trainer_tb.v

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

`timescale 1ns/1ps

module reaction_trainer_tb;

    reg clk;
    reg btn_start;
    reg btn_react;
    wire [7:0] led;

    // Instantiate the Device Under Test (DUT)
    // Override parameters to speed up simulation drastically
    reaction_trainer #(
        .MS_TICKS(2),      // 2 ticks = 1 "ms" for simulation
        .DEBOUNCE_TICKS(5) // 5 ticks for debounce
    ) dut (
        .clk_25mhz(clk),
        .btn_start(btn_start),
        .btn_react(btn_react),
        .led(led)
    );

    // Generate simulated clock
    initial begin
        clk = 0;
        forever #20 clk = ~clk; // 40ns period -> 25MHz
    end

    initial begin
        $dumpfile("reaction_trainer.vcd");
        $dumpvars(0, reaction_trainer_tb);

        // Initialize inputs
// ...

`timescale 1ns/1ps

module reaction_trainer_tb;

    reg clk;
    reg btn_start;
    reg btn_react;
    wire [7:0] led;

    // Instantiate the Device Under Test (DUT)
    // Override parameters to speed up simulation drastically
    reaction_trainer #(
        .MS_TICKS(2),      // 2 ticks = 1 "ms" for simulation
        .DEBOUNCE_TICKS(5) // 5 ticks for debounce
    ) dut (
        .clk_25mhz(clk),
        .btn_start(btn_start),
        .btn_react(btn_react),
        .led(led)
    );

    // Generate simulated clock
    initial begin
        clk = 0;
        forever #20 clk = ~clk; // 40ns period -> 25MHz
    end

    initial begin
        $dumpfile("reaction_trainer.vcd");
        $dumpvars(0, reaction_trainer_tb);

        // Initialize inputs
        btn_start = 0;
        btn_react = 0;

        // Wait a few cycles in IDLE
        #1000;

        // Press START button
        $display("[%0t] Pressing START button...", $time);
        btn_start = 1;
        #500;
        btn_start = 0;

        // Wait for LED to turn on (TEST state)
        $display("[%0t] Waiting for GO signal...", $time);
        wait(led == 8'b11111111);
        $display("[%0t] LED is ON! Reacting...", $time);

        // Simulate a fast reaction time (e.g., 180ms)
        // In this TB, 1ms = 2 ticks = 80ns. So 180ms = 14400ns.
        #14400;
        btn_react = 1;
        #500;
        btn_react = 0;

        // Wait for RESULT state to settle
        #5000;
        $display("[%0t] Simulation complete. LED output: %b", $time, led);
        $finish;
    end

endmodule

3. Board Constraints: ulx3s.lpf

LOCATE COMP "clk_25mhz" SITE "G2";
IOBUF PORT "clk_25mhz" PULLMODE=NONE IO_TYPE=LVCMOS33;

LOCATE COMP "btn_start" SITE "R1";
IOBUF PORT "btn_start" PULLMODE=DOWN IO_TYPE=LVCMOS33;

LOCATE COMP "btn_react" SITE "T1";
IOBUF PORT "btn_react" PULLMODE=DOWN IO_TYPE=LVCMOS33;

LOCATE COMP "led[0]" SITE "B2";
LOCATE COMP "led[1]" SITE "C2";
LOCATE COMP "led[2]" SITE "C1";
LOCATE COMP "led[3]" SITE "D2";
LOCATE COMP "led[4]" SITE "D1";
LOCATE COMP "led[5]" SITE "E2";
LOCATE COMP "led[6]" SITE "E1";
LOCATE COMP "led[7]" SITE "H3";

IOBUF PORT "led[0]" PULLMODE=NONE IO_TYPE=LVCMOS33;
IOBUF PORT "led[1]" PULLMODE=NONE IO_TYPE=LVCMOS33;
IOBUF PORT "led[2]" PULLMODE=NONE IO_TYPE=LVCMOS33;
IOBUF PORT "led[3]" PULLMODE=NONE IO_TYPE=LVCMOS33;
IOBUF PORT "led[4]" PULLMODE=NONE IO_TYPE=LVCMOS33;
IOBUF PORT "led[5]" PULLMODE=NONE IO_TYPE=LVCMOS33;
IOBUF PORT "led[6]" PULLMODE=NONE IO_TYPE=LVCMOS33;
IOBUF PORT "led[7]" PULLMODE=NONE IO_TYPE=LVCMOS33;

4. Build Script: build.sh

#!/bin/bash
set -e

echo "Linting and Simulating with Verilator..."
verilator --lint-only reaction_trainer.v

echo "Synthesizing with Yosys..."
yosys -p "synth_ecp5 -json reaction_trainer.json" reaction_trainer.v

echo "Place and Route with nextpnr..."
nextpnr-ecp5 --85k --json reaction_trainer.json --lpf ulx3s.lpf --textcfg reaction_trainer_out.config

echo "Packing bitstream..."
ecppack reaction_trainer_out.config reaction_trainer.bit

echo "Build complete. To flash, run:"
echo "openFPGALoader --board=ulx3s reaction_trainer.bit"

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary device being built according to the text?




Question 2: What component is utilized to create randomized delays in the trainer?




Question 3: What is the stated precision of the hardware timer?




Question 4: What clock frequency corresponds to the 40 ns precision mentioned in the text?




Question 5: Which tool is used to run the testbench for hardware validation?




Question 6: What type of file is inspected to validate the accuracy claim?




Question 7: Which software is explicitly mentioned for inspecting the resulting waveform?




Question 8: What educational concept is demonstrated by integrating hardware state machines and LFSRs?




Question 9: Why is this hardware timer considered a better alternative to software-based timers?




Question 10: What is one of the human-in-the-loop testing use cases mentioned?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:


Practical case: Missing Pulse Watchdog Alarm with ULX3S

Practical case: Missing Pulse Watchdog Alarm with ULX3S — hero

Objective and use case

What you’ll build: A hardware-based missing-pulse watchdog alarm that continuously monitors a periodic heartbeat signal. It triggers an audible piezo buzzer and visual LED with deterministic, sub-millisecond latency if the signal drops out within a predefined timeframe.

Why it matters / Use cases

  • Embedded System Supervision: Monitors an MCU “heartbeat” square wave, instantly alerting operators if the processor crashes or enters an infinite loop.
  • Industrial Automation: Detects stalled conveyor belts or jammed machinery by monitoring optical or magnetic encoder pulses for unexpected stops.
  • Security Monitoring: Validates physical line integrity, immediately detecting cut wires or disconnected sensor cables when a continuous pulse goes silent.

Expected outcome

  • A synthesized FPGA watchdog timer with cycle-accurate fault detection.
  • Immediate GPIO activation of an LED and piezo buzzer upon pulse loss.
  • Safety awareness: Understanding that this educational implementation must be replaced by certified, redundant fail-safe hardware in real-world production environments.

Audience: Embedded Systems Engineers, FPGA Developers; Level: Intermediate

Architecture/flow: An incoming signal edge resets an internal FPGA hardware down-counter. If the counter reaches zero (e.g., >50ms elapsed without a pulse), an alarm latch triggers the output GPIOs to drive the buzzer and LED.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this FPGA/ULX3S profile, the synthesizable Verilog blocks were checked with Yosys (read_verilog) and the Verilog design/test set was linted with Verilator. The validator also checked code-block structure, copy/paste-safe ASCII command options, unsupported stacks, and availability of the ULX3S/ECP5 toolchain (yosys, nextpnr-ecp5, ecppack, openFPGALoader).

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 3 sections, 1 tables and 4 code blocks detected before publication.
  • Checked code: 2 Verilog/Yosys-Verilator, 1 Bash/copy-paste checks.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ULX3S board revision, pin-constraint file and real wiring.

Educational safety note

This project is an educational prototype, not a certified product. Before powering the setup, verify the pinout of your exact ULX3S board revision, keep FPGA I/O signals at 3.3 V, never connect 5 V directly to I/O pins, disconnect power before changing wiring, and use suitable external supplies for loads, motors or servos while sharing ground only when the wiring requires it.

Prerequisites

To successfully complete this tutorial, you need:
* Basic understanding of digital logic (flip-flops, counters, and clock domains).
* A working installation of the open-source Lattice ECP5 FPGA toolchain:
* Verilator (version 5.0 or newer recommended for --binary support) for linting and simulation.
* Yosys for logic synthesis.
* nextpnr-ecp5 for place and route.
* Project Trellis (ecppack) for bitstream generation.
* openFPGALoader for programming the device.
* A text editor to create Verilog source files, testbenches, and constraint files.
* Basic familiarity with using a command-line interface (CLI) to execute build commands.

Materials

  • Radiona ULX3S (Lattice ECP5-85F) FPGA development board.
  • Pulse input jumper: A standard male-to-male Dupont jumper wire (used to manually simulate pulses by tapping a 3.3V pin, or to connect an external signal generator).
  • Piezo buzzer: A standard passive piezo buzzer module (capable of producing sound when driven by a square wave).
  • Status LED: We will use one of the onboard red LEDs provided on the ULX3S board.
  • Breadboard and additional jumper wires (optional, for securing the piezo buzzer).

Setup/Connection

The Radiona ULX3S provides versatile GPIO headers (J1 and J2) that operate at 3.3V logic levels. We will use the J1 header for our external connections.

Component / Function Physical Connection FPGA Pin I/O Standard Notes
System Clock Onboard 25 MHz Oscillator G2 LVCMOS33 Drives all synchronous logic.
Pulse Input J1 Header, Pin gp[0] B11 LVCMOS33 Internally pulled DOWN. Connect jumper between 3.3V and this pin to simulate a pulse.
Piezo Buzzer J1 Header, Pin gp[1] A10 LVCMOS33 Connect buzzer positive to gp[1], negative to GND on the J1 header.
Status LED Onboard LED 0 B2 LVCMOS33 Illuminates when the watchdog times out.

Wiring Instructions:
1. Locate the J1 header on the ULX3S.
2. Connect the positive leg of your passive piezo buzzer to the pin labeled gp[1] (FPGA pin A10).
3. Connect the negative leg of the piezo buzzer to a GND pin on the ULX3S.
4. Take your pulse input jumper wire and plug one end into the pin labeled gp[0] (FPGA pin B11). Leave the other end loose for now; you will tap it against a 3.3V pin on the J1 header to simulate incoming heartbeat pulses.

Validated Code

The project consists of three files: the main synthesizable Verilog module, the Verilog testbench for simulation, and the Logical Preference File (LPF) for pin constraints. Save these files in the same working directory.

1. Main Module (watchdog.v)

This module synchronizes the asynchronous input, detects rising edges, counts clock cycles to measure the timeout, and generates a 2 kHz tone if the timeout is reached.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

/*
 * Module: watchdog
 * Description: Missing-pulse watchdog alarm. Triggers an LED and a 2 kHz 
 * piezo buzzer tone if a rising edge is not detected within the timeout period.
 */
module watchdog #(
    // Defaults for 25 MHz clock
    parameter TIMEOUT_CYCLES = 25000000, // 1 second timeout
    parameter TONE_CYCLES    = 12500     // 2 kHz tone period (25M / 2k)
)(
    input  wire clk_25mhz,
    input  wire pulse_in,
    output wire buzzer_out,
    output wire led_out
);

    // --------------------------------------------------------
    // 1. Metastability Synchronizer
    // --------------------------------------------------------
    // The input pulse comes from the outside world and is asynchronous.
    // We use a 2-stage shift register to synchronize it to the 25MHz clock.
    reg [1:0] sync_reg = 2'b00;

    always @(posedge clk_25mhz) begin
        sync_reg <= {sync_reg[0], pulse_in};
    end

    // --------------------------------------------------------
    // 2. Edge Detector
    // --------------------------------------------------------
    // Compare the current synchronized state with the previous state
    // to generate a single-cycle pulse on the rising edge.
    reg pulse_prev = 1'b0;
    wire pulse_edge;

    always @(posedge clk_25mhz) begin
        pulse_prev <= sync_reg[1];
    end

    assign pulse_edge = (sync_reg[1] && !pulse_prev);

    // --------------------------------------------------------
    // 3. Watchdog Timer
    // --------------------------------------------------------
    reg [24:0] timer_cnt = 25'd0;
// ...

/*
 * Module: watchdog
 * Description: Missing-pulse watchdog alarm. Triggers an LED and a 2 kHz 
 * piezo buzzer tone if a rising edge is not detected within the timeout period.
 */
module watchdog #(
    // Defaults for 25 MHz clock
    parameter TIMEOUT_CYCLES = 25000000, // 1 second timeout
    parameter TONE_CYCLES    = 12500     // 2 kHz tone period (25M / 2k)
)(
    input  wire clk_25mhz,
    input  wire pulse_in,
    output wire buzzer_out,
    output wire led_out
);

    // --------------------------------------------------------
    // 1. Metastability Synchronizer
    // --------------------------------------------------------
    // The input pulse comes from the outside world and is asynchronous.
    // We use a 2-stage shift register to synchronize it to the 25MHz clock.
    reg [1:0] sync_reg = 2'b00;

    always @(posedge clk_25mhz) begin
        sync_reg <= {sync_reg[0], pulse_in};
    end

    // --------------------------------------------------------
    // 2. Edge Detector
    // --------------------------------------------------------
    // Compare the current synchronized state with the previous state
    // to generate a single-cycle pulse on the rising edge.
    reg pulse_prev = 1'b0;
    wire pulse_edge;

    always @(posedge clk_25mhz) begin
        pulse_prev <= sync_reg[1];
    end

    assign pulse_edge = (sync_reg[1] && !pulse_prev);

    // --------------------------------------------------------
    // 3. Watchdog Timer
    // --------------------------------------------------------
    reg [24:0] timer_cnt = 25'd0;
    reg alarm_state = 1'b0;

    always @(posedge clk_25mhz) begin
        if (pulse_edge) begin
            // Valid pulse received: reset timer and clear alarm
            timer_cnt <= 25'd0;
            alarm_state <= 1'b0;
        end else if (timer_cnt < TIMEOUT_CYCLES) begin
            // No pulse, but timeout not yet reached: increment timer
            timer_cnt <= timer_cnt + 1'b1;
        end else begin
            // Timeout reached: trigger alarm
            alarm_state <= 1'b1;
        end
    end

    // --------------------------------------------------------
    // 4. Tone Generator (2 kHz Square Wave)
    // --------------------------------------------------------
    reg [13:0] tone_cnt = 14'd0;
    reg tone_out = 1'b0;

    always @(posedge clk_25mhz) begin
        if (alarm_state) begin
            // Toggle the buzzer state every half-period
            if (tone_cnt >= (TONE_CYCLES / 2) - 1) begin
                tone_cnt <= 14'd0;
                tone_out <= ~tone_out;
            end else begin
                tone_cnt <= tone_cnt + 1'b1;
            end
        end else begin
            // Ensure buzzer is silent when alarm is inactive
            tone_cnt <= 14'd0;
            tone_out <= 1'b0;
        end
    end

    // --------------------------------------------------------
    // 5. Output Assignments
    // --------------------------------------------------------
    assign buzzer_out = tone_out;
    assign led_out    = alarm_state;

endmodule

2. Testbench (watchdog_tb.v)

The testbench overrides the default timing parameters to speed up simulation. Simulating 25 million cycles would be computationally expensive and unnecessary for verifying logic. The testbench automatically checks if the output properly asserts and resets.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

/*
 * Module: watchdog_tb
 * Description: Simulation testbench for the missing-pulse watchdog.
 */
`timescale 1ns/1ps

module watchdog_tb;

    reg clk;
    reg pulse_in;
    wire buzzer_out;
    wire led_out;

    // Instantiate the Device Under Test (DUT)
    // Override parameters for faster simulation:
    // Timeout = 100 cycles, Tone period = 10 cycles
    watchdog #(
        .TIMEOUT_CYCLES(100),
        .TONE_CYCLES(10)
    ) dut (
        .clk_25mhz(clk),
        .pulse_in(pulse_in),
        .buzzer_out(buzzer_out),
        .led_out(led_out)
    );

    // Clock generation (25 MHz = 40ns period)
    initial clk = 0;
    always #20 clk = ~clk;

    initial begin
        $dumpfile("watchdog_tb.vcd");
        $dumpvars(0, watchdog_tb);

        // Initialize inputs
        pulse_in = 0;
// ...

/*
 * Module: watchdog_tb
 * Description: Simulation testbench for the missing-pulse watchdog.
 */
`timescale 1ns/1ps

module watchdog_tb;

    reg clk;
    reg pulse_in;
    wire buzzer_out;
    wire led_out;

    // Instantiate the Device Under Test (DUT)
    // Override parameters for faster simulation:
    // Timeout = 100 cycles, Tone period = 10 cycles
    watchdog #(
        .TIMEOUT_CYCLES(100),
        .TONE_CYCLES(10)
    ) dut (
        .clk_25mhz(clk),
        .pulse_in(pulse_in),
        .buzzer_out(buzzer_out),
        .led_out(led_out)
    );

    // Clock generation (25 MHz = 40ns period)
    initial clk = 0;
    always #20 clk = ~clk;

    initial begin
        $dumpfile("watchdog_tb.vcd");
        $dumpvars(0, watchdog_tb);

        // Initialize inputs
        pulse_in = 0;

        // Wait for reset/initialization
        #100;

        // Apply a pulse
        pulse_in = 1;
        #40;
        pulse_in = 0;

        // Wait for timeout (100 cycles * 40ns = 4000ns)
        #5000;

        if (led_out !== 1'b1) begin
            $display("FAIL: Alarm did not trigger after timeout.");
        end else begin
            $display("PASS: Alarm triggered successfully.");
        end

        // Apply another pulse to reset alarm
        pulse_in = 1;
        #40;
        pulse_in = 0;

        #100;
        if (led_out !== 1'b0) begin
            $display("FAIL: Alarm did not clear after new pulse.");
        end else begin
            $display("PASS: Alarm cleared successfully.");
        end

        #1000;
        $finish;
    end

endmodule

3. Constraints File (ulx3s.lpf)

This file maps the top-level Verilog ports to the physical pins on the Radiona ULX3S ECP5-85F board.

LOCATE COMP "clk_25mhz" SITE "G2";
IOBUF COMP "clk_25mhz" IO_TYPE=LVCMOS33;

LOCATE COMP "pulse_in" SITE "B11";
IOBUF COMP "pulse_in" IO_TYPE=LVCMOS33 PULLMODE=DOWN;

LOCATE COMP "buzzer_out" SITE "A10";
IOBUF COMP "buzzer_out" IO_TYPE=LVCMOS33;

LOCATE COMP "led_out" SITE "B2";
IOBUF COMP "led_out" IO_TYPE=LVCMOS33;

Build and Validation

Execute the following commands in your terminal to simulate the logic, synthesize the design, and program the ULX3S FPGA board.

# 1. Lint and simulate with Verilator to validate logic
verilator --binary --trace watchdog.v watchdog_tb.v
./obj_dir/Vwatchdog_tb

# 2. Synthesize with Yosys
yosys -p "synth_ecp5 -top watchdog -json watchdog.json" watchdog.v

# 3. Place and Route with nextpnr
nextpnr-ecp5 --85k --package CABGA381 --json watchdog.json --lpf ulx3s.lpf --textcfg watchdog.config

# 4. Pack bitstream
ecppack watchdog.config watchdog.bit

# 5. Program the ULX3S via openFPGALoader
openFPGALoader --board ulx3s watchdog.bit

Expected Validation Evidence:
1. Simulation Phase: The Verilator execution (./obj_dir/Vwatchdog_tb) should print PASS: Alarm triggered successfully. followed by PASS: Alarm cleared successfully. to the console, confirming accurate cycle counting.
2. Hardware Phase: Upon flashing, the red LED will immediately turn on and the buzzer will sound a 2 kHz tone because no pulses are arriving.
3. Manual Test: Tap the loose jumper wire against the 3.3V pin on the J1 header. The LED and buzzer will instantly turn off. If you stop tapping, exactly 1.0 seconds later, the alarm will resume.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary function of the hardware built in this project?




Question 2: What kind of latency does the watchdog alarm achieve when triggering?




Question 3: In the context of Embedded System Supervision, what does the watchdog monitor?




Question 4: How is the system used in Industrial Automation?




Question 5: What does the watchdog alarm trigger when the signal drops out?




Question 6: How does the system function in Security Monitoring?




Question 7: What safety warning is provided regarding this project's use in real-world production?




Question 8: Who is the intended audience for this project?




Question 9: What is one of the expected outcomes of this project?




Question 10: What kind of signal does the missing-pulse watchdog alarm continuously monitor?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me: