FPGA Seven-Segment Display Control

    Hello, Habr! I want to make my contribution to the advancement of FPGAs. In this article I will try to explain how to describe in VHDL a device that controls a seven-segment display. But before starting, I want to briefly talk about how I came to FPGA and why I chose the VHDL language.

    About half a year ago I decided to try my hand at programming FPGAs. Before that, I had never encountered circuitry. There was little experience using microcontrollers (Atmega328p, STM32). Immediately after the decision to get comfortable with FPGAs, the question arose of choosing the language that I would use. The choice fell on VHDL because of its strict typing. As a beginner, I wanted to catch as many possible problems as possible at the synthesis stage, and not on a working device.

    Why exactly a seven-segment display? The LED blinking is already tired, and the logic of blinking it does not represent anything interesting. The logic of controlling the display is on the one hand more complicated than blinking an LED (i.e. writing it is more interesting), and on the other hand it is quite simple to implement.

    What I used in the process of creating the device:

    • FPGA Altera Cyclone II (I know that it is hopelessly outdated, but the Chinese can buy it for a penny)
    • Quartus II version 13.0.0 (as far as I know, this is the latest version supporting Cyclone II)
    • Simulator ModelSim
    • Seven segment display with shift register

    Task


    Create a device that will show the numbers 0 - 9 in a cycle. Once a second, the value displayed on the display should increase by 1.

    This logic can be implemented in different ways. I will divide this device into modules, each of which will perform some action and the result of this action will be transmitted to the next module.

    Modules


    • This device should be able to count the time. To count the time, I created a “delay” module. This module has 1 incoming and 1 outgoing signal. The module receives the FPGA frequency signal and, after a specified number of periods of the incoming signal, changes the value of the outgoing signal to the opposite.
    • The device should read from 0 to 9. The bcd_counter module will be used for this.
    • In order to light a segment on the display, you need to set the bit corresponding to the segment to 0 in the shift register of the display, and in order to clear the segment into bits, write 1 (my display has inverted logic). The bcd_2_7seg decoder will deal with the installation and reset of the desired bits.
    • The transmitter module will be responsible for the data transfer.

    The host device will control the correct transmission of signals between the modules, as well as generate an rclk signal upon completion of data transfer.

    For clarity, I give a diagram of this device
    scheme

    As you can see from the diagram, the device has 1 incoming signal (clk) and 3 outgoing signals (sclk, dio, rclk). The clk signal comes in 2 signal dividers (sec_delay and transfer_delay). An outgoing signal with a period of 1s leaves the sec_delay device. On the rising edge of this signal, the counter (bcd_counter1) begins to generate the next number for display. After the number is generated, the decoder (bcd_2_7seg1) converts the binary representation of the number into lit and not lit segments on the display. Which, using the transmitter (transmitter1), are transmitted to the display. The transmitter is clocked using the transfer_delay device.

    The code


    To create a device in VHDL, a construction of two components of entity and architecture is used. The entity declares an interface for working with the device. Architecture describes the logic of the device.

    Here's what the entity of a delay device looks like
    entity delay is
        -- При объявлении entity, поле generic не является обязательным
        generic (delay_cnt: integer);
        -- Описываем входные и выходные сигналы устройства
        port(clk: in std_logic; out_s: out std_logic := '0');	
    end entity delay;
    


    Through the generic field, we can set the device to the desired delay. And in the ports field we describe the incoming and outgoing signals of the device.

    The delay device architecture is as follows
    -- В секции architecture описывается то, как устройство будет работать
    -- С одной entity может быть связано 0 или более архитектур
    architecture delay_arch of delay is
    begin
        delay_proc: process(clk)
            variable clk_cnt: integer range 0 to delay_cnt := 0;
            variable out_v: std_logic := '0';
        begin
            -- Если имеем дело с передним фронтом сигнала
            if(rising_edge(clk)) then
                clk_cnt := clk_cnt + 1;					
      	    if(clk_cnt >= delay_cnt) then
                    -- switch/case в языке VHDL
    	        case out_v is
    	    	    when '0' => 
                            out_v := '1';
    	   	    when others =>
    			out_v := '0';
    		end case;
    		clk_cnt := 0;
                    -- Устанавливаем в сигнал out_s значение переменной out_v
    		out_s <= out_v;
                end if;
    	end if;
        end process delay_proc;
    end delay_arch;
    


    The code inside the process section is executed sequentially, any other code is executed in parallel. In brackets, after the process keyword, signals are indicated, by changing which the given process will start (sensivity list).

    The bcd_counter device, in terms of execution logic, is identical to the delay device. Therefore, I will not dwell on it in detail.

    Here is the entity and architecture of the decoder
    entity bcd_to_7seg is
        port(bcd: in std_logic_vector(3 downto 0) := X"0";
               disp_out: out std_logic_vector(7 downto 0) := X"00");
    end entity bcd_to_7seg;
    architecture bcd_to_7seg_arch of bcd_to_7seg is
        signal not_bcd_s: std_logic_vector(3 downto 0) := X"0";
    begin
        not_bcd_s <= not bcd;
        disp_out(7) <= (bcd(2) and not_bcd_s(1) and not_bcd_s(0)) or 
    			   (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1) 
                                and bcd(0));
        disp_out(6) <= (bcd(2) and not_bcd_s(1) and bcd(0)) or
    			   (bcd(2) and bcd(1) and not_bcd_s(0));
        disp_out(5) <= not_bcd_s(2) and bcd(1) and not_bcd_s(0);
        disp_out(4) <= (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1) 
                                 and bcd(0)) or
    			   (bcd(2) and not_bcd_s(1) and not_bcd_s(0)) or
    			   (bcd(2) and bcd(1) and bcd(0));
        disp_out(3) <= (bcd(2) and not_bcd_s(1)) or bcd(0);
        disp_out(2) <= (not_bcd_s(3) and not_bcd_s(2) and bcd(0)) or
    		    	   (not_bcd_s(3) and not_bcd_s(2) and bcd(1)) or
    			   (bcd(1) and bcd(0));
        disp_out(1) <= (not_bcd_s(3) and not_bcd_s(2) and not_bcd_s(1)) or
    		           (bcd(2) and bcd(1) and bcd(0));
        disp_out(0) <= '1';
    end bcd_to_7seg_arch;
    


    All the logic of this device is executed in parallel. I talked about how to get formulas for this device in one of the videos on my channel. Who cares, here is a link to the video .

    In the transmitter device, I combine serial and parallel logic
    entity transmitter is
        port(enable: in boolean; 
               clk: in std_logic; 
               digit_pos: in std_logic_vector(7 downto 0) := X"00"; 
               digit: in std_logic_vector(7 downto 0) := X"00"; 
               sclk, dio: out std_logic := '0'; 
               ready: buffer boolean := true);
    end entity transmitter;
    architecture transmitter_arch of transmitter is
        constant max_int: integer := 16;
    begin
        sclk <= clk when not ready else '0';		
        send_proc: process(clk, enable, ready)
    	variable dio_cnt_v: integer range 0 to max_int := 0;
    	variable data_v: std_logic_vector((max_int - 1) downto 0);
        begin
    	-- Установка сигнала dio происходит по заднему фронту сигнала clk
    	if(falling_edge(clk) and (enable or not ready)) then
    	    if(dio_cnt_v = 0) then
    		-- Прежде всего передаем данные, потом позицию на дисплее
    		-- Нулевой бит данных идет в нулевой бит объединенного вектора
    		data_v := digit_pos & digit;
    		ready <= false;
    	    end if;			
    	    if(dio_cnt_v = max_int) then				
    		dio_cnt_v := 0;
    		ready <= true;
    		dio <= '0';
    	    else	
    		dio <= data_v(dio_cnt_v);
    		dio_cnt_v := dio_cnt_v + 1;
    	    end if;
    	end if;
        end process send_proc;
    end transmitter_arch;
    


    To the sclk signal, I redirect the value of the clk signal entering the transmitter, but only if the device is currently transmitting data (signal ready = false). Otherwise, the sclk signal value will be 0. At the beginning of the data transfer (enable = true signal), I combine the data from two 8-bit vectors (digit_pos and digit) entering the device into a 16-bit vector (data_v) and transmit data from this vector is one bit per clock, setting the value of the transmitted bit in the outgoing signal dio. Of the interesting things about this device, I want to note that the data in dio is set to the trailing edge of the clk signal, and the data from pin dio will be written to the shift register of the display when the leading edge of the sclk signal arrives. Upon completion of the transmission, by setting the signal ready <= true, I signal to other devices,

    Here's what the entity and architecture of a display device looks like
    entity display is
        port(clk: in std_logic; sclk, rclk, dio: out std_logic := '0');
    end entity display;
    architecture display_arch of display is
        component delay is
    	generic (delay_cnt: integer); 
    	port(clk: in std_logic; out_s: out std_logic := '0');
        end component;
        component bcd_counter is
    	port(clk: in std_logic; bcd: out std_logic_vector(3 downto 0));
        end component;
        component bcd_to_7seg is
    	port(bcd: in std_logic_vector(3 downto 0); 
                   disp_out: out std_logic_vector(7 downto 0));
        end component;
        component transmitter is
    	port(enable: in boolean; 
                   clk: in std_logic; 
                   digit_pos: in std_logic_vector(7 downto 0); 
                   digit: in std_logic_vector(7 downto 0); 
                   sclk, dio: out std_logic; 
                   ready: buffer boolean);
        end component;
        signal sec_s: std_logic := '0';
        signal bcd_counter_s: std_logic_vector(3 downto 0) := X"0";
        signal disp_out_s: std_logic_vector(7 downto 0) := X"00";
        signal tr_enable_s: boolean;
        signal tr_ready_s: boolean;
        signal tr_data_s: std_logic_vector(7 downto 0) := X"00";
        -- Этот флаг, совместно с tr_ready_s контролирует 
        -- установку и сброс rclk сигнала 
        signal disp_refresh_s: boolean;
        signal transfer_clk: std_logic := '0';
    begin
        sec_delay: delay generic map(25_000_000)	
    				port map(clk, sec_s);
        transfer_delay: delay generic map(10)
    				port map(clk, transfer_clk);
        bcd_counter1: bcd_counter port map(sec_s, bcd_counter_s);
        bcd_to_7seg1: bcd_to_7seg port map(bcd_counter_s, disp_out_s);
        transmitter1: transmitter port map(tr_enable_s, 
                                                        transfer_clk, 
                                                        X"10", 
                                                        tr_data_s, 
                                                        sclk, 
                                                        dio,
                                                        tr_ready_s);
        tr_proc: process(transfer_clk)
    	variable prev_disp: std_logic_vector(7 downto 0);
    	variable rclk_v: std_logic := '0';
        begin
    	if(rising_edge(transfer_clk)) then
                -- Если передатчик готов к передаче следующей порции данных
    	    if(tr_ready_s) then	
                    -- Если передаваемые данные не были только что переданы
    	        if(not (prev_disp = disp_out_s)) then		 
    		    prev_disp := disp_out_s;
                        -- Помещаем передаваемые данные в шину данных передатчика
    	            tr_data_s <= disp_out_s;	
                        -- Запускаем передачу данных			
    		    tr_enable_s <= true;
    		end if;
    	    else
    		disp_refresh_s <= true;
    		-- Флаг запуска передачи данных нужно снять 
                    -- до завершения передачи,
                    -- поэтому снимаю его по приходу следующего частотного сигнала
    		tr_enable_s <= false;
    	    end if;
    	    if(rclk_v = '1') then
    		disp_refresh_s <= false;
    	    end if;
    	    if(tr_ready_s and disp_refresh_s) then			 
    		rclk_v := '1';
                else
    		rclk_v := '0';
    	    end if;
    	    rclk <= rclk_v;
    	end if;		
        end process tr_proc;
    end display_arch;
    


    This device controls other devices. Here, before declaring auxiliary signals, I declare the components that I will use. In the architecture itself (after the begin keyword) I create device instances:

    • sec_delay - an instance of the delay component. The outgoing signal is routed to sec_s.
    • transfer_delay - an instance of the delay component. The outgoing signal is sent to the signal transfer_clk.
    • bcd_counter1 - an instance of the bcd_counter component. The outgoing signal is routed to bcd_counter_s.
    • bcd_to_7seg1 - an instance of the bcd_to_7seg component. The outgoing signal is routed to disp_out_s.
    • transmitter1 is an instance of the transmitter component. Outgoing signals are sent to sclk, dio, tr_ready_s signals.

    After component instances, a process is declared. This process solves several problems:

    1. If the transmitter is not busy, the process initializes the start of data transfer.
                  if(tr_ready_s) then
      		if(not (prev_disp = disp_out_s)) then
      		    prev_disp := disp_out_s;
                          -- Помещаем передаваемые данные в 
                          -- шину данных передатчика
      		    tr_data_s <= disp_out_s;
                          -- Запускаем передачу данных
      		    tr_enable_s <= true;	
      		end if;
      	    else
                      ...
                  


    2. If the transmitter is busy (tr_ready_s = false), then the process sets the value of the signal disp_refresh_s <= true (this signal indicates that after the transfer is completed, the data on the display must be updated). The signal value tr_enable_s <= false is also set, if this is not done before the transmission is completed, then the data downloaded to the transmitter will be transmitted
    3. Sets and resets the rclk signal after data transfer is complete
                      if(rclk_v = '1') then
      		    disp_refresh_s <= false;
      		end if;
      		if(tr_ready_s and disp_refresh_s) then			 
      		    rclk_v := '1';
      		else
      		    rclk_v := '0';
      		end if;
      		rclk <= rclk_v;
                      



    Timing chart


    Here is the timing diagram of transferring the number 1 to the first position of the display
    timing diagram

    First, the data “10011111“ is transmitted. Then the position of the number on the display is “00010000“ (this parameter arrives at the transmitter as constant X ”10”). In both cases, the rightmost bit (lsb) is transmitted first.

    All code can be viewed on github . Files with a subscript * _tb.vhd are debug files for the corresponding components (for example, transmitter_tb.vhd is a debug file for a transmitter). Just in case, I also uploaded them to github. This code was downloaded and worked on a real board. Who cares, you can see an illustration of the code here (starting at 15:30). Thanks for attention.

    Also popular now: