3 times 8 results in 16

Introduction

Sooner or later one always comes to the problem that the used microcontroller or but also CPLD, or FPGA has too few IOs. You have just 3 free IOs, but you want to connect 5 or more LEDs.

Now you can simply buy a bigger (and therefore more expensive) chip, or you use the help of a shift register.

Since there are many ready-to-use libraries and demo programs for microcontrollers, I will use a CPLD here and will execute the examples in VHDL.

We push some bits around

The 2 typical representatives of shift registers have either one serial input and several parallel outputs, or several parallel inputs and one serial output.

Of course there are several other versions, but my example is about a shift register, which has one serial input and 8 parallel outputs. My choice is the 74LS595, a very often used type.

This shift register has a total of two registers. Once the shift register itself and additionally a memory register.

This has the advantage that the data can already be loaded into the shift register, but the data from the memory register are still output at the outputs.

Both registers are 8-bit large on this chip.

As you can see on the picture, there are 5 inputs on the left side, and 9 outputs on the right side.

The assignment is as follows:

Inputs

  • PIN 14: SER
    • Serial input of the shift register. Here the data is loaded bit by bit into the shift register.
  • PIN 11: SRCLK
    • The clock for loading the individual bits into the shift register. At each rising edge, the respective state at the serial input (HIGH/LOW) is taken over.
  • PIN 10: SRCLR
    • If this line is pulled to LOW, or “0”, then the shift register is cleared immediately.
  • PIN 12: RCLK
    • If this input is set to HIGH, or “1”, then the content of the shift register is transferred to the memory register, and thus the outputs are set accordingly.
  • PIN 13: OE
    • The Output Enable input. This allows the shift register to be “switched on” as soon as the input is pulled to LOW, or “0”. Otherwise the chip is deactivated, respectively the outputs are in High-Z state.

Outputs

  • PIN 1-7 & 15: Qa-h
    • These are the 8 parallel outputs of the memory register. Here the bits are present in parallel in the order in which they were loaded into the shift register AND transferred into the memory register.
  • PIN 9: Qh’
    • This is the overflow, or carryover. With this you have the possibility to cascade several shift registers, i.e. to switch them one after the other. So you can use not only 8 parallel outputs, but 16, 24, 32, etc.. 8 outputs per 74xx595.

Power

  • PIN 16: VCC
    • Here the +5V power supply is connected.
  • PIN 8: GND
    • And the ground is connected to this pin.

How a shift register works

You have to think of a shift register like a conveyor belt, on which there is room for exactly 8 packages. This belt is driven by the clock. And with each stroke of the clock, the belt moves one position further. Since only 8 positions are free on this belt, the content of the first position falls off the belt and disappears.

This is exactly how a shift register works. You put a packet with the content “1” or “0” on the front and turn the tape with the help of the clock.

At the rising edge of the clock, the content of this position is remembered. And the 8-bit shift register can remember exactly the content of 8 positions. If all 8 positions are stored and the clock strikes the next beat, the content of the first position is forgotten and the last added content is remembered instead.

As you can see on this picture, the content 11010111 was loaded into the shift register. However, nothing happens at the outputs yet. Because the shift register must still be taken over into the memory register. To do this, the RCLK is briefly pulled to HIGH. Then the content is taken over into the memory register and is available at the output.

The timing is important, of course. Because if the clock is HIGH again before the data has been accepted, the bit that was loaded first is lost. Therefore the timing must be correct.

And speaking of timing. According to the data sheet the 74LS595 should not be clocked higher than 20MHz. It often manages more (I have clocked it in the test already up to 50MHz), but then you have to expect data losses, or errors.

8-bits are just not enough

Now the question will surely arise, how to switch 16 LEDs with only 8 outputs, and this also independently. Well, that it works is the fault of Charlie Allen, who had a great idea in 1995. The method was therefore later also called Charlieplexing.

For this purpose, a matrix of rows and columns is formed, at the crosspoints of which an LED is placed (or whatever you want to control or query).

If all outputs are connected in the same way, i.e. have the same voltage level, no current can flow. Referring to the shift register, none of the LEDs would be able to light up at “11111111” or “00000000”.

Now we divide these 8 bits into 4 rows and 4 columns. The first 4 bits would be the rows, and the next 4 bits the columns.

If you now apply a “1” to row 1, or output Qa, which corresponds to +5V, and a “0” to column 1, which corresponds to ground potential, a current can flow through this one LED, and the LED lights up.

So that now the entire row 1 lights up, the other rows must be switched to “0”, as well as the remaining columns to “1”.

Thus the first 4 bits would correspond to “1000” and the next 4 bits to “0111”, which then corresponds to “10000111” at the output of the shift register.

So now exactly this one LED lights up and the others remain dark. And this is how you can proceed with each LED.

In this way you can build up a table containing the assignment of LEDs to the set BITs of the shift register:

  • LED 1 -> 10000111
  • LED 2 -> 10001011
  • LED 3 -> 10001101
  • LED 4 -> 10001110
  • LED 5 -> 01000111
  • LED 6 -> 01001011
  • LED 7 -> 01001101
  • LED 8 -> 01001110
  • LED 9 -> 00100111
  • LED 10 -> 00101011
  • LED 11 -> 00101101
  • LED 12 -> 00101110
  • LED 13 -> 00010111
  • LED 14 -> 00011011
  • LED 15 -> 00011101
  • LED 16 -> 00011110

As soon as this bit sequence is serially loaded into the shift register and then transferred into the memory register, the respective LED can be controlled.

Now more than one LED

If you have tried to build the example on a breadboard, which is also possible without shift registers, you will quickly notice that you can only light up one LED at a time, or several at once.

Because as soon as you add another row or column, all corresponding LEDs wired there will light up. So what to do?

The slow eye

That’s where our rather slow eye comes in wonderfully. Like from the old days of cinema, when the film did not come out of the projector, 24 frames were shown every second, which resulted in a stable and reasonably smooth picture in our eyes.

And in this way, this is also done here. We simply let the LEDs light up one after the other. If we do this fast enough, we get the impression that different LEDs light up in a targeted manner.

And since we’ll be doing this at a fairly high frequency, the LEDs all seem to be controllable independently of each other. In reality, we’ll only turn on one LED at a time, then turn it off again and turn on the next one instead, and so on. And we repeat this sequence until the eye perceives only lit LEDs.

The circuit

The circuit to switch 16 LEDs with only 3 lines looks like this:

As you can see, you now have 16 LEDs, which are ultimately controlled by only 3 lines.

If you have read the data sheet, you will have noticed that the outputs are not very loadable. So you can’t connect power hungry LEDs. Fortunately, there are nowadays LEDs that get along with 2-3mA.

Control with a CPLD

Now a CPLD is still needed. I personally like to use the XC9500XL series from Xilinx for this. These are still very well available, and are very cheap. If you buy them in China, one chip costs the equivalent of just under a dollar.

They are also quite indestructible. ESD is virtually no problem, and you also have to make a great effort to destroy an IO port or the entire chip.

But the biggest advantage is that they are still 5V tolerant. This means that the chip itself is operated with 3.3V, and also the IOs only output 3.3V (which is then called LVTTL – Low Voltage TTL), but incoming 5V tolerated without problems. So you can integrate these CPLDs wonderfully into TTL circuits.

So ideal for small tinkering. I had made a small development board for such things a long time ago, where the IOs are all led out, and additionally the voltage converter for the 3.3V and an oscillator are on it.

Of course you can use any other board as long as it is 5V tolerant.

VHDL code

I tried to keep the code as simple as possible. The whole thing could be kept much more optimized and shorter. However, this would be less understandable for beginners.

3 outputs and a clock generator are required. I recommend here simply to use a 16MHz oscillator. If the frequency should be higher than 20MHz, then simply divide down accordingly, until a max. frequency of 20MHz is reached.

    entity SR_LED_Test is

        Port    (
                CLK               : in  std_logic;
                SR_CLK            : out std_logic;
                SR_DATA_LOAD      : out std_logic;
                SR_SERIAL_DATA    : out std_logic
                );

    end SR_LED_Test;

CLK is the input of the oscillator and the 3 outputs go to the corresponding lines of the shift register (see above schematic).

    LED_PATTERN(0)  <= "10000111";
    LED_PATTERN(1)  <= "10001011";
    LED_PATTERN(2)  <= "10001101";
    LED_PATTERN(3)  <= "10001110";
    LED_PATTERN(4)  <= "01000111";
    LED_PATTERN(5)  <= "01001011";
    LED_PATTERN(6)  <= "01001101";
    LED_PATTERN(7)  <= "01001110";
    LED_PATTERN(8)  <= "00100111";
    LED_PATTERN(9)  <= "00101011";
    LED_PATTERN(10) <= "00101101";
    LED_PATTERN(11) <= "00101110";
    LED_PATTERN(12) <= "00010111";
    LED_PATTERN(13) <= "00011011";
    LED_PATTERN(14) <= "00011101";
    LED_PATTERN(15) <= "00011110";

As already mentioned above, I simply wrote the bit pattern for each of the 16 LEDs into an 8 bit wide element of the type “STD_LOGIC_VECTOR“.

The whole thing is controlled with the signal LED. This element is of type “STD_LOGIC_VECTOR” and is 16-bit wide. LED(0) corresponds to the first LED and LED(15) to the 16th and last LED.

If a LED should light up, then one needs to write for example only LED(0) <= '1';.

    if (LED(LED_POSITION) = '1') then
        SR_DATA <= LED_PATTERN(LED_POSITION);
    else
        SR_DATA <= (others => '0');
    end if;
    LED_POSITION <= LED_POSITION + 1;

Now the status of the individual set states for the LEDs is simply queried.

  • Line 1 asks whether the LED with the respective position (start at 0) contains a ‘1’. If yes, then
  • in line 2 the corresponding LED_PATTERN (with the corresponding position) is taken over into the element “SR_DATA“, which is again of the type “STD_LOGIC_VECTOR” with a width of 8-bit.
  • Line 3 is then branched to if the query from line 1 resulted in a ‘0’, in order to then branch to
  • Line 4 simply SR_DATA to “fill” everything with ‘0’ (others => ‘0’) means nothing else, than independently of the bit width of the signal, to fill all with the appropriate value, here thus with “00000000“).
  • Line 5 terminates the loop in order to then
  • Line 6 increments the counter for the number of the LED by one.

So we now have in SR_DATA which bits must be set in the shift register so that the corresponding LED can be switched on.

    SR_DATA_LOAD <= '0';
    SR_SERIAL_DATA <= SR_DATA(SR_COUNTER);
    SR_COUNTER <= SR_COUNTER + 1;
    if (SR_COUNTER = 8) then
        SR_DATA_LOAD <= '1';
        SR_COUNTER <= 0;
    end if;

Now the shift register is filled to be transferred to the memory register afterwards. To do this, the first step is to enter

  • Line 1 the latch is pulled to LOW, so that the data does not already affect the outputs during loading.
  • Then in line 2 the element “SR_DATA” filled with 8 bits before is put to the serial input of the shift register bit by bit with the help of a counter.
  • Afterwards in line 3 the counter is increased by one, so that the next bit can be read in. Since the counter is incremented only after reading, there is then an ‘8’ in the counter when all bits for an LED have been read,
  • which is then also queried in line 4. So if all 8 bits are read, the following is displayed in
  • Line 5 the latch is set to HIGH, which causes the content of the shift register to be taken over into the memory register and then this bit sequence is also set accordingly at the outputs. The LED lights up. So that the whole thing can be repeated
  • in line 6 the counter is set back again
  • and in line 7 this query loop is terminated.

Now two important points are missing. On the one hand the clocking, so that the individual bits can be read and the counters can continue to count at all. And a lock is missing, so that the setting of the bit pattern and the loading into the shift register does not happen at the same time.

The best way to do this is to use a so-called state machine. One creates, similarly as with a traffic light, a dependence among themselves.

Combined, this would look like the following, including the last two code snippets:

    process (SR_CLOCK)
    begin
        if (rising_edge(SR_CLOCK)) then
            case SR_STATUS is
                when LOADING =>
                    SR_DATA_LOAD <= '0';
                    SR_SERIAL_DATA <= SR_DATA(SR_COUNTER);
                    SR_COUNTER <= SR_COUNTER + 1;
                    if (SR_COUNTER = 8) then
                        SR_DATA_LOAD <= '1';
                        SR_COUNTER <= 0;
                        SR_STATUS <= IDLE;
                    end if;
                when IDLE =>
                    if (LED(LED_POSITION) = '1') then
                        SR_DATA <= LED_PATTERN(LED_POSITION);
                    else
                        SR_DATA <= (others => '0');
                    end if;
                        LED_POSITION <= LED_POSITION + 1;
                        SR_STATUS <= LOADING;
            end case;
        end if;
    end process;

On the one hand, the whole thing was embedded in a process so that the instructions in it are not processed in parallel. And the whole process is clocked. Whenever the clock “SR_CLOCK” changes from LOW to HIGH, i.e. at a rising edge, this process is run through. Line 3 is responsible for this.

Then a CASE loop is executed, and with it the state machine. For this I defined two states (I will talk about the declaration of the signals and types later), LOADING and IDLE. With LOADING data is loaded into the shift register and with IDLE the shift register has nothing to do at the moment and the data is prepared as the BIT pattern for the respective LED is selected.

The whole thing is controlled by the SR_STATUS signal. There is either LOADING or IDLE in it. And one part sets the status so that the other part can work.

After the first current is applied to the circuit, respectively the initialization of the CPLD, IDLE is given (the reason for this I will explain later).

Thus the CASE instruction branches into the second half of the process. There the part is processed, which searches out the suitable bit pattern for the respective LED and writes it into SR_DATA.

At the end there is the setting of a new SR_STATUS to LOADING. Only then the first part can work. At the same time it is prevented that the second part can be executed as long as the first part is working.

Again, after reading all 8-bits into the shift register, transferring this data into the memory register and setting the latch to HIGH, the SR_STATUS is set to IDLE again. So the bit pattern for the next LED can be read in. And until this is done, the first half cannot continue, because for this ST_STATUS must first be set to LOADING again.

And so it is constantly switched back and forth between these two states and the LED is switched on and off. And this happens so fast that you can’t see afterwards that actually only one LED is on at a time.

Here still the declaration of the internally used signals:

    type   SR_STATUS_TYPE    is	(LOADING, IDLE);
    signal SR_STATUS         :  SR_STATUS_TYPE := IDLE;
    signal SR_CLOCK          :  std_logic;
    signal SR_DATA           :  std_logic_vector (7 downto 0) := (others => '0');
    signal SR_COUNTER        :  integer range 0 to 8 := 0;

    signal LED               :  std_logic_vector (15 downto 0);
    signal LED_POSITION      :  integer range 0 to 15 := 0;

    type   LED_PATTERN_ARRAY is array (0 to 15) of std_logic_vector (7 downto 0);
    signal LED_PATTERN       :  LED_PATTERN_ARRAY;

For the state machine, LOADING and IDLE were used as switching criteria. This is much more readable than 1 and 0. But such signal types do not exist in VHDL. But you can define this yourself. For this purpose a new element type named SR_STATUS_TYPE is defined in the first line. This element knows the states LOADING and IDLE. One could also define more, but were not needed for this.

In line 2 a signal is then defined as usual with the new element type just defined before. And at the same time a first initialization value is set, in this case IDLE. So when the CPLD starts for the first time, IDLE is already set as value.

Line 3 is for the clock signal, which is used internally. Because the SR_CLK defined at the very beginning was defined as output, this cannot be used simply also reading.

Line 4 is the 8.-bit wide vector, in which the bit pattern of the respective LED is written.

Line 5 is for the counter of the shift register, which shifts the 8 bits one by one bit by bit.

In line 7 are the actual signals for the LEDs. Since we want to control 16 pieces, this is also a corresponding vector.

And in line 8 the second counter for the 16 LEDs is set.

Line 10 and 11 is an array defined. Because each bit pattern has 8-bits (7 downto 0) and from these bit patterns there is one for each LED (is array (0 to 15). Simplified you could compare this with a table, which has 16 rows, one per LED and each row has 8 columns, for the bits per LED.

One more thing…

I still have one, or rather something is still missing, and that is the assignments for the clock.

    SR_CLOCK <= CLK;
    SR_CLK <= SR_CLOCK;

In the first line the incoming clock signal is routed to an internal signal.

This would basically be superfluous. But if you have to divide down the clock signal before, because it is faster than 20MHz, you could simply go in between and assign the divided clock signal.

And in the second line this internal signal is passed on to the clock output for the shift register to work.

The entire code then looks like this:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity SR_LED_Test is

    Port    (
            CLK               : in  std_logic;
            SR_CLK            : out std_logic;
            SR_DATA_LOAD      : out std_logic;
            SR_SERIAL_DATA    : out std_logic
            );

end SR_LED_Test;

architecture Retrotinker of SR_LED_Test is

    type   SR_STATUS_TYPE    is	(LOADING, IDLE);
    signal SR_STATUS         :  SR_STATUS_TYPE := IDLE;
    signal SR_CLOCK          :  std_logic;
    signal SR_DATA           :  std_logic_vector (7 downto 0) := (others => '0');
    signal SR_COUNTER        :  integer range 0 to 8 := 0;

    signal LED               :  std_logic_vector (15 downto 0);
    signal LED_POSITION      :  integer range 0 to 15 := 0;

    type   LED_PATTERN_ARRAY is array (0 to 15) of std_logic_vector (7 downto 0);
    signal LED_PATTERN       :  LED_PATTERN_ARRAY;

begin
    SR_CLOCK <= CLK;
    SR_CLK <= SR_CLOCK;

    LED_PATTERN(0)  <= "10000111";
    LED_PATTERN(1)  <= "10001011";
    LED_PATTERN(2)  <= "10001101";
    LED_PATTERN(3)  <= "10001110";
    LED_PATTERN(4)  <= "01000111";
    LED_PATTERN(5)  <= "01001011";
    LED_PATTERN(6)  <= "01001101";
    LED_PATTERN(7)  <= "01001110";
    LED_PATTERN(8)  <= "00100111";
    LED_PATTERN(9)  <= "00101011";
    LED_PATTERN(10) <= "00101101";
    LED_PATTERN(11) <= "00101110";
    LED_PATTERN(12) <= "00010111";
    LED_PATTERN(13) <= "00011011";
    LED_PATTERN(14) <= "00011101";
    LED_PATTERN(15) <= "00011110";

    process (SR_CLOCK)
    begin
        if (rising_edge(SR_CLOCK)) then
            case SR_STATUS is
                when LOADING =>
                    SR_DATA_LOAD <= '0';
                    SR_SERIAL_DATA <= SR_DATA(SR_COUNTER);
                    SR_COUNTER <= SR_COUNTER + 1;
                    if (SR_COUNTER = 8) then
                        SR_DATA_LOAD <= '1';
                        SR_COUNTER <= 0;
                        SR_STATUS <= IDLE;
                    end if;
                when IDLE =>
                    if (LED(LED_POSITION) = '1') then
                        SR_DATA <= LED_PATTERN(LED_POSITION);
                    else
                        SR_DATA <= (others => '0');
                    end if;
                        LED_POSITION <= LED_POSITION + 1;
                        SR_STATUS <= LOADING;
            end case;
        end if;
    end process;

    LED(0) <= '1';
    LED(1) <= '0';
    LED(2) <= '1';
    LED(3) <= '0';
    LED(4) <= '1';
    LED(5) <= '0';
    LED(6) <= '1';
    LED(7) <= '0';
    LED(8) <= '1';
    LED(9) <= '0';
    LED(10) <= '1';
    LED(11) <= '0';
    LED(12) <= '1';
    LED(13) <= '0';
    LED(14) <= '1';
    LED(15) <= '0';

end Retrotinker;

Summary

In the last section, the individual LEDs are switched on or off. If you set a ‘1’ at the corresponding LED, it will light up.

You can also add a running light or other effects here. Or use it exactly as a display for another project.

As already mentioned at the beginning, this code is very bloated. For example, you would not need to set LEDs that are not supposed to light up at all.

But I hope that this makes the code much more readable, especially for beginners.

Are such gimmicks interesting for you? If so, then just leave a comment, then I would do something like this more often.

Or otherwise, of course, I would be very happy about a comment.

5 2 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x