The 7 segment display unit is a most basic type of display used in Embedded systems. And if the segments are made up of LEDs, then it becomes much easier to drive this display using the GPIOs of microcontrollers directly.
7 Segment Display driven using MCU is a great example of multiplexing and "How Engineers take advantage of limitations of Human Eyes!"
As seen from the pinout in Figure 1, a 7 Seven Segment Display unit has (indeed !) 8 segments (a, b, c, d, e, f, g, dp) and a pair of common pins (which are also common !). It is the common pin which is useful for multiplexing a number of such units together to display multi-digit number.
There are 2 ways the common pin is connected to all the segments :
- Common Anode
- Common Cathode
As seen from the Figure 2, if the your display has a common anode configuration, then to display a digit, the required segments which make up a digit are connected to logical LOW and then common anode is connected to logical HIGH ( with resistors in series of course ) of the microcontroller. If the configuration is common cathode then apply opposite voltages as compared to the common anode display.
In order to multiplex multiple 7 segement units, we will short (or connect together) respective segments of all units together. And we will use the common pins as the Select Lines.
If we have a 3-digit common anode display, and we want to display "58.4", then :
- First we will bring LOW the required segments to display "5" (segments a, c, d, f, g), and only bring HIGH the first anode, for a few milliseconds. Then we make the first anode LOW
- Then we will make LOW the segments required to display "8 and." (segments a, b, c, d, e, f, g, dp), and only the second anode will be HIGH for next few milliseconds. Then we make the second anode LOW.
- At last, we will make LOW the segments required to display "4" (segments b, c, f, g), and only the third anode will be HIGH for next few milliseconds. Then we make the third anode LOW.
- We will continue this cycle as per the number to be displayed changes.
As per the process above, only a one digit is displayed at a given instance of time, but since all the digits are being swept at such a high speed (just a few milliseconds of delay) that the number is displayed multiple times a second (kind of Frames per Second - FPS) and our eyes are not able distinguish between each on/off of the digits.
It is called Persistence of Vision - POV. Engineers use such techniques (actually itself a great technology) to optimize the available resources for given system.
In our case of a seven segment display, it has helped reducing the pin count required for driving the display.
- If we hadn't multiplexed the units, then to drive a 5 digit display, it would require 45 pins ( 5 * (9 pins per digit = 8 segments + 1 common pin) )
- But, since we have multiplexed the segments, we only require 13 pins (8 segments + 5 * 1 common pin per digit) for a 5 digit display.
There are also 7 segments display modules like this, which are interfaced using the i2c bus and can be useful if you are running low on pins. But since STM32 Discovery has a ton of GPIOs, I decided to create my custom display controlled directly by the GPIOs of STM32 and create a driver for it.
I had many common anode seven segments LED units, which I had de-soldered from an old display. So, I decided to make use of these. The circuit is very simple 5-digit display as shown in Figure 3, although it involves a lot of soldering if you are doing it on a prototyping PCB just as I did. I would recommend designing a PCB if you're gonna make multiple such displays.
The display is interfaced with STM32F4 Discovery board (containing the STM32F407VGT6 MCU) as per the GPIO mapping defined in file display_config.ads :
Segment_a : segment_pin renames PA0;
Segment_b : segment_pin renames PA1;
Segment_c : segment_pin renames PA2;
Segment_d : segment_pin renames PA3;
Segment_e : segment_pin renames PA4;
Segment_f : segment_pin renames PA5;
Segment_g : segment_pin renames PC5;
Segment_dp : segment_pin renames PA7;
anode_1 : common_pin renames PE7;
anode_2 : common_pin renames PE8;
anode_3 : common_pin renames PE9;
anode_4 : common_pin renames PE10;
anode_5 : common_pin renames PE11;
The CodeThe code is written in Ada for STM32F407 using the Ravenscar profile. The structure of the code is as follows :
- display_config.ads and display_config.ads contain the definition and functions regarding mapping of the GPIO to segments and anodes, Initializing the GPIOs, functions to control GPIOs and current frame to be shown on the display. STM32 Discovery board has tons of GPIOs, you can choose any of them as you want, no need to stick to the GPIOs that I've defined.
Since, I am using common anode display then to display a digit common anode pin should be connected to HIGH and respective segment pins should be connected to LOW. Hence, I have defined the functions to control GPIOs like :
procedure Segment_Off (This : in out segment_pin) renames STM32.GPIO.Set;
procedure Segment_On (This : in out segment_pin) renames STM32.GPIO.Clear;
procedure Digit_On (This : in out common_pin) renames STM32.GPIO.Set;
procedure Digit_Off (This : in out common_pin) renames STM32.GPIO.Clear;
If you have a display with common cathode configuration, then change these lines as :
procedure Segment_On (This : in out segment_pin) renames STM32.GPIO.Set;
procedure Segment_Off (This : in out segment_pin) renames STM32.GPIO.Clear;
procedure Digit_Off (This : in out common_pin) renames STM32.GPIO.Set;
procedure Digit_On (This : in out common_pin) renames STM32.GPIO.Clear;
This package also contains the sub-frames of the digits as the array of the boolean values, representing whether the respective segment should be on or off for the given digit.
frame_0 :constant digit_subframe :=(True, True, True, True, True, True, False, False);
frame_1 :constant digit_subframe :=(False, True, True, False, False, False, False, False);
......
......
- display_driver.ads and display_driver.adb contain the definition of a record display_parameters, which is used to define output format of your display. (signed or unsigned, integer or real number and how many digits to show).
type display_parameters(decimal_point : precision_display) is limited
record
signed_number : signed_display;
Fore_digits : Positive;
case decimal_point is
when real_no =>
Aft_digits : Positive;
when Others =>
null;
end case;
end record;
Then there are functions which manipulate the given input number based on the instance of the record of your display, into string representation of this number, which is similar to what you will see on display. These two files are kind of core logic of this project. We will talk about them later.
- display_po.ads and display_po.adbcontain the protected object store_data, which is used store the number to be displayed. store_data provides protected procedure put_data for modifying the protected data. also, it provides protected function get_data to retrieve the protected data without changing it. The implementation of store_data here is as per the section 5.5 of Ravenscar Article. store_data doesn't has any protected entry, because we are not expecting any task synchronization through it. Instead, store_data is just used for access to a shared data, here the variable current_data.
protected store_data is
function get_data return Float;
procedure put_data(num_rx : in Float);
private
current_data : Float;
end store_data;
- display_value.ads and display_value.adb contain the task send_data, which pushes the number to the protected object store_data using the protected procedure put_data. This number can be any reading from sensor like temperature, any processed data like pitch angle calculated from values of accelerometer, or any number of your choice which you wish to output on the display.
Here in this example, it is just a simple counter adding 0.1 to the existing number each second and then pushing it to store_data.
task body send_data is
begin
num_tx := -12.2;
infinite_loop :
loop
num_tx := num_tx + 0.1;
store_data.put_data (num_tx);
delay until Clock + Milliseconds (1000);
end loop infinite_loop;
end send_data;
- display_task.ads and display_task.adb first create the instance of the display_parameters called my_display and a string called string_representation_data having the length exactly as per the format defined by my_display.
my_display : constant display_parameters := (decimal_point => real_no, signed_number => signed_no, Fore_digits => 3, Aft_digits => 1);
string_representation_data : String( 1 .. output_string_length(my_display) ) := (Others => ' ');
Then the task called display_data fetches the number from store_data using the protected function get_data.
task body display_data is
delay_digit_show : constant Natural := 5;
......
begin
beyond_infinity:
loop
raw_data := store_data.get_data;
....
.......
Then converts this number into its equivalent string representation and stores it into the string_representation_data.
string_representation_data := to_display_string(raw_data, my_display);
Then generates current_frame to be displayed.
generate_frame(string_representation_data);
At last display_data displays the current_frame by sweeping one digit at a time being displayed for 5 milliseconds each.
sweep_digits :
for i in digit_anodes'Range loop
sweep_segments :
for j in seven_segs'Range loop
if (current_frame(i, j) = True) then
Segment_On(seven_segs(j));
else
Segment_Off(seven_segs(j));
end if;
end loop sweep_segments;
Digit_On(digit_anodes(i));
My_Delay(delay_digit_show);
Digit_Off(digit_anodes(i));
end loop sweep_digits;
- seven_segs_test.adb is the main file for this project, which kinds of acts as the container to include all the tasks and objects defined in other files. Besides that, it has the lowest priority.
display_driver.ads and display_driver.adb contain the necessary code for all manipulations on number to be converted to a frame. The logic is simple. Let's say you've 5 digit display and you want to output number in this format > "-XXX.X".So, it is a sign of a number first, then 3 digits before decimal point and single digit after decimal point.
- Function to_display_string first converts the float into an integer such that we don't lose digits required to be displayed after the decimal point. Let's say if float is -15.273, then its lossless integer representation is -153 ( = Integer ((-15.273) * (10 ^ Aft_digits) ) )
- Then checks if the Integer representation is within the range possible as per the current configuration of the display defined by my_display. For this case the range is -9999 to 9999, and hence if the Integer representation of number lies outside this range then it is bracketed to these values.
- And at last from the value of Integer representation, it starts filling the string as per the the output format of "-XXX.X"
package display_confighas a function called generate_frame, which generates a frame based on the output string provided by to_display_string.
procedure generate_frame (String_data : in String);
A bit more....Although, the architecture and structure of the code for this project seems to be simple and easy to understand (I hope !), the reason for this simplicity is the tasking support provided by the Ravenscar profile. If we examine closely at the tasks and send_data and display_data :
- send_data adds 0.1 to the num_tx and pushes this number into protected object store_data, every second. So, send_data completes one iteration in 1 second = 1000 ms.
- display_data fetches the number from store_data and displays 5 digits on the display one by one, each digit being "on" for 5 ms. So, the display_data completes one iteration in 25 ms.
Now, the amazing thing is that I haven't spent any efforts in determining how the different cycle times of these tasks will need to be taken care of. And I can change this cycle times as per my wish, the code will continue to work.
Consider doing the same in Arduino environment, where tasking support is not available, then everything in your code will run at a single delay which is at the end of your code. Of course it is a different story if you are using the timers in Arduino.
Ada Ravenscar profile for ARM MCUs provides great tasking support on bare-metal, which is generally not available without RTOS kernel. If you are interested to know more, read a little more here.
Future Plans- Add SPARK code to the package display_driver.
- Utilize this display in some other project like temperature display or Inclinometer.
- To create an RC Car that does have an IMU like MPU-9250 to adapt to various driving conditions.
Comments