Over the years and over 1oo plus Hackster projects we have looked at some very depths and complex solutions from robotics to vision processing, creating your own hardware and of course projects which cover concepts such as math, and filtering in FPGAs.
In this project I thought it would be fun to go back the basics and look at how we can interact with different kinds of Switch's, Buttons, LEDs and displays so we can understand a little more about we can best address them when we are working with them in other applications.
So in this project using we will use the Basys 3 development board along with the some Pmods to provide additional interface.
For this project we will use the
- Pmod ENC - Quadrature Encoder
- Push Button
- LEDs
The goal is to teach the following
- How to de-bounce a switches
- How to read Quadrature Encoders
- How to drive LEDs to have varying intensities using Pulse Width Modulation
Lets get started, of course these techniques and code will be able to be used on any FPGA board which has similar switches, displays and LEDs.
De-BouncingThe Basys 3 has two kinds of switch, a push button and slider, when ever a switch is flipped, slid or pressed what happens is we are breaking or making an electrical connection. The switch does not make a clean instant connection, instead they make contact intermittently at first before settling. This intermittent initial contact causes which is known as switch bounce.
Switch bounce looks like in the image below, where the there are a number of transition following the switch making or breaking. The bounce might be a full or partial amplitude bounce.
When we want to sample the switch in our application we need to take account of any switch bounce which might occur. Failure to do so will mean we might registers the action several times which will be difficult.
In a real world hardware a switch bounce might look as below, which is a oscilloscope capture of a push button switch on the Basys 3.
Typically a switch might bounce for several milliseconds so we need to take this into account in our FPGA.
Thankfully there are several techniques we can use when working with switches in FPGAs to de-boucne them. All of these include using a synchronous design so the first thing we need to do is synchronise the switch input to the correct clock domain.
If you are not familiar with why we need to synchronise to the clock domain. This is because all registers have a set up and hold time around clock edge which updates the register.
should the input change within this window the output of the register will go metastable. Which means a state between 1 and 0, it will randomly recover to either a 1 or 0 but it bares no relation to the input level.
As such we use at least a two stage register to synchronise a signal coming into the FPGA which is not synchronised to the clock. This allows for the first register to go metastable and recover before the next clock and updating of the second register. This ensure the registers which are down stream of the synchroniser do not risk seeing a metastable value and the issue propagating within the design.
We need to therefore synchronise the incoming switch signal using such a synchroniser.
Once we have synchronised the signal we then need to de-bounce the signal, as bounces will still make it through the synchroniser they will just be synchronised to the clock.
We can use a simple synchroniser and counter to de-bounce the switch input
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
use IEEE.math_real.all;
entity switch_debouncer is
generic (
g_CLK_FREQ : integer := 100_000_000; -- Clock frequency in Hz
g_DBNC_TIME : integer := 10 -- Debounce time in ms
);
port (
i_clk : in std_logic;
i_switch : in std_logic;
o_switch : out std_logic
);
end switch_debouncer;
architecture Behavioral of switch_debouncer is
constant c_COUNT_MAX : integer := (g_CLK_FREQ / 1000) * g_DBNC_TIME;
constant c_COUNT_WIDTH : integer := integer(ceil(log2(real(c_COUNT_MAX))));
signal s_sync_ff1 : std_logic := '0';
signal s_sync_ff2 : std_logic := '0';
signal s_counter : unsigned(c_COUNT_WIDTH-1 downto 0) := (others => '0');
signal s_curr_state : std_logic := '0';
begin
process(i_clk)
begin
if rising_edge(i_clk) then
-- Double-flop synchronizer
s_sync_ff1 <= i_switch;
s_sync_ff2 <= s_sync_ff1;
-- Debounce logic
if s_sync_ff2 /= s_curr_state then
-- Input changed - reset counter
s_counter <= (others => '0');
s_curr_state <= s_sync_ff2;
elsif s_counter < c_COUNT_MAX then
-- Keep counting until we reach c_COUNT_MAX
s_counter <= s_counter + 1;
else
-- Input stable for c_COUNT_MAX cycles
o_switch <= s_curr_state;
end if;
end if;
end process;
end Behavioral;
To test this code we can create a Vivado project targeting the Basys 3 board, create a new VHDL file and simulate it.
We will see a waveform as below where the input switch changes state, it is synchronised using two registers then a 10 ms wait before the input is output.
Now we have de-bounced a switch input we can look at how we can work with a more complicated switch such as a rotary encoder.
Rotary EncoderA rotary encoder is a quick way to switch between different options, one common example would be to use its rotation to control the brightness of the LED or display screen.
It is often also called a quadrature encoder as the two outputs A and B are located at 90 degrees to each other e.g. in quadrature.
As the switch is rotated either clock or anti clock wise you will see the A and B change and of course you will get bounce.
Depending on which one of the two outputs A or B has the first edge on it we can determine which way the rotation is occurring.
Normally the rotary encoder also has a push button which can be used to confirm selections etc.
When we rotate the rotary encoder we will get glitches as can be seen below but these are shorter duration as the wheel rotates.
Note, the encoder used is normally high as it is pulled up.
To read the encoder and determine the direction of rotation we can use the edge detection.
The example of code below allows us to increment of decrement a vectors value depending on the direction of rotation of the dial.
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity quad_decoder is
generic (
g_COUNT_WIDTH : integer := 16
);
port (
i_clk : in std_logic;
i_quad_a : in std_logic;
i_quad_b : in std_logic;
o_count : out signed(g_COUNT_WIDTH-1 downto 0);
o_dir : out std_logic
);
end quad_decoder;
architecture Behavioral of quad_decoder is
signal s_position : signed(g_COUNT_WIDTH-1 downto 0) := (others => '0');
signal s_quad_a_d : std_logic := '1';
signal s_quad_b_d : std_logic := '1';
signal s_lockout : std_logic := '0';
signal s_count : integer range 0 to 100_000 := 0;
signal s_clear : std_logic := '0';
begin
process(i_clk)
begin
if rising_edge(i_clk) then
s_clear <= '0';
if s_count = 99999 then
s_count <= 0;
s_clear <= '1';
else
s_count <= s_count +1;
end if;
end if;
end process;
process(i_clk)
begin
if rising_edge(i_clk) then
s_quad_a_d <= i_quad_a;
s_quad_b_d <= i_quad_b;
if s_clear = '1' then
s_lockout <= '0';
end if;
if (i_quad_a = '0' and s_quad_a_d = '1') and s_lockout = '0' then
s_position <= s_position + 1;
o_dir <= '1';
s_lockout <= '1';
elsif (i_quad_b = '0' and s_quad_b_d = '1') and s_lockout = '0' then
s_position <= s_position - 1;
o_dir <= '0';
s_lockout <= '1';
end if;
end if;
end process;
We can add this into our Vivado project and simulate its behaviour using the test bench below
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity quad_decoder_tb is
end quad_decoder_tb;
architecture Behavioral of quad_decoder_tb is
-- Component declaration
component quad_decoder is
generic (
g_COUNT_WIDTH : integer := 16
);
port (
i_clk : in std_logic;
i_quad_a : in std_logic;
i_quad_b : in std_logic;
o_count : out signed(g_COUNT_WIDTH-1 downto 0);
o_dir : out std_logic
);
end component;
-- Test signals
signal s_clk : std_logic := '0';
signal s_quad_a : std_logic := '1';
signal s_quad_b : std_logic := '1';
signal s_count : signed(15 downto 0);
signal s_dir : std_logic;
-- Clock period definition
constant c_CLK_PERIOD : time := 10 ns;
begin
-- Instantiate the Unit Under Test (UUT)
uut: quad_decoder
generic map (
g_COUNT_WIDTH => 16
)
port map (
i_clk => s_clk,
i_quad_a => s_quad_a,
i_quad_b => s_quad_b,
o_count => s_count,
o_dir => s_dir
);
-- Clock process
p_clk: process
begin
s_clk <= '0';
wait for c_CLK_PERIOD/2;
s_clk <= '1';
wait for c_CLK_PERIOD/2;
end process;
-- Stimulus process
p_stim: process
begin
wait for 100 ns;
for i in 0 to 255 loop
s_quad_a <= '0';
wait for 100 ns;
s_quad_b <= '0';
wait for 100 ns;
s_quad_a <= '1';
wait for 100 ns;
s_quad_b <= '1';
wait for 2 ms;
end loop;
for i in 0 to 255 loop
s_quad_b <= '0';
wait for 100 ns;
s_quad_a <= '0';
wait for 100 ns;
s_quad_b <= '1';
wait for 100 ns;
s_quad_a <= '1';
wait for 2 ms;
end loop;
report "simulation complete" severity failure;
end process;
end Behavioral;
Running the simulation you can see the count increment and then decrement as the encoder rotation changes.
By switching the setting of the ramp value to analogue we can see the count increment and then decrement.
Now we have two elements of what we require for our LED dimming circuit. The next element is to create the drive circuit.
Pulse Width ModulationPulse width modulation is great for delivering both variable power to loads and reconstituting analogue signals with simple post processing with R and C.
A PWM signal has two key elements
- Period - The repetition period of the signal
- Duty Cycle - How much of period the signal is high
For this application we are going to use a 50Hz PWM period and make the PWM period controllable using the rotary encoder.
The code the PWM is
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity pwm_counter is
generic (
g_CLK_FREQ : integer := 100_000_000 -- 100 MHz system clock
);
port (
i_clk : in std_logic;
i_count : in unsigned(7 downto 0); -- 8-bit input from decoder
o_pwm : out std_logic
);
end pwm_counter;
architecture Behavioral of pwm_counter is
constant c_COUNT_MAX : integer := g_CLK_FREQ/50; -- For 50 Hz
signal s_period_counter : integer range 0 to c_COUNT_MAX := 0;
signal s_duty_cycle : integer range 0 to c_COUNT_MAX := 0;
begin
-- Map 8-bit count to duty cycle
process(i_clk)
begin
if rising_edge(i_clk) then
s_duty_cycle <= (to_integer(i_count) * c_COUNT_MAX)/256;
end if;
end process;
-- PWM counter
process(i_clk)
begin
if rising_edge(i_clk) then
if s_period_counter >= c_COUNT_MAX-1 then
s_period_counter <= 0;
else
s_period_counter <= s_period_counter + 1;
end if;
if s_period_counter < s_duty_cycle then
o_pwm <= '1';
else
o_pwm <= '0';
end if;
end if;
end process;
end Behavioral;
While the test bench will set several different PWM outputs
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;
entity pwm_counter_tb is
end pwm_counter_tb;
architecture Behavioral of pwm_counter_tb is
-- Component declaration
component pwm_counter is
generic (
g_CLK_FREQ : integer := 100_000_000
);
port (
i_clk : in std_logic;
i_count : in unsigned(7 downto 0);
o_pwm : out std_logic
);
end component;
-- Clock period for simulation
constant c_CLK_PERIOD : time := 10 ns; -- 100 MHz
-- Test bench signals
signal s_clk : std_logic := '0';
signal s_count : unsigned(7 downto 0) := (others => '0');
signal s_pwm : std_logic;
-- Use reduced clock frequency for simulation
constant c_SIM_CLK_FREQ : integer := 1_000_000; -- 1 MHz
begin
-- Instantiate the Unit Under Test (UUT)
uut: pwm_counter
generic map (
g_CLK_FREQ => c_SIM_CLK_FREQ -- Use reduced frequency for simulation
)
port map (
i_clk => s_clk,
i_count => s_count,
o_pwm => s_pwm
);
-- Clock process
p_clk: process
begin
s_clk <= '0';
wait for c_CLK_PERIOD/2;
s_clk <= '1';
wait for c_CLK_PERIOD/2;
end process;
-- Stimulus process
p_stim: process
begin
-- Initialize count
s_count <= X"00"; -- 0% duty cycle
wait for 25 ms;
s_count <= X"40"; -- 25% duty cycle (64/256)
wait for 25 ms;
s_count <= X"80"; -- 50% duty cycle (128/256)
wait for 25 ms;
s_count <= X"C0"; -- 75% duty cycle (192/256)
wait for 25 ms;
s_count <= X"FF"; -- ~100% duty cycle (255/256)
wait for 25 ms;
report "simulation complete" severity failure;
wait;
end process;
end Behavioral;
Running this in our Vivado project shows the following we can see the Pulse Width change as the input demand is changed.
Now we are ready to pull this all together into a Vivado design and try it on the hardware.
Integrating the Design.To get started with this we will be created in Vivado a new block diagram project. On to this we will be adding in the RTL course code we have just created.
With the IP cores added on to the block diagram we are able to create the top level wrapper and synthesise the design.
I have connected the rotary encoder to the A and B inputs, the rotary encoder also includes a 1 ms lock out for de-bouncing. I also slightly changed the code to increment / decrement by 8 every rotation to stop the need for 256 adjustments.
I also added the second LED to be connected to the push button switch when the rotary switch is pushed. This is connected to the de-bouncing circuit.
Let Vivado manage the wrapper
Once synthesis is completed we are able to assign the IO in a XDC file
set_property IOSTANDARD LVCMOS33 [get_ports i_clk]
set_property IOSTANDARD LVCMOS33 [get_ports A]
set_property IOSTANDARD LVCMOS33 [get_ports B]
set_property IOSTANDARD LVCMOS33 [get_ports o_pwm_0]
set_property PACKAGE_PIN W5 [get_ports i_clk]
set_property PACKAGE_PIN U16 [get_ports o_pwm_0]
set_property PACKAGE_PIN J1 [get_ports A]
set_property PACKAGE_PIN L2 [get_ports B]
Once the bit stream is completed we are able to download and test on the hardware.
We can see the LED values change on the ILA while the rotary encoder is changed.
While a simple project I hope this project has been informative and useful especially if you are just starting out on your FPGA journey. Interfacing with switches and handling bounces can cause issues with unexpected commands in more complex solutions.
Comments
Please log in or sign up to comment.