• Pic® Basic

  • Code for Driving 2 to 8 seven segment displays

    I have recovered from the forum background a code to drive 4 seven segment displays. The author is unknown, it could be a Les Johnson code. I have renewed the code to adapt it to the new versions of the compiler (V3603). I have tried to make the code as efficient and fast as possible. Now I have been able to extend the code to scan 6 digits. See later in this article for 8 digit displays.

    Writing such a code in the main program is a very difficult task almost impossible without imposing a very strict timing for its application, restricting its possibilities. It is for this reason it is better to use interrupts to scan the digits.

    The devices that use this type of display are commonly found in digital clocks, electronic meters, counters, signaling and other equipment to show only numerical data. The display segments are usually referred to by the letters "a" through "g". To multiplex the 7 segment displays with the microcontroller, you must determine or look for the pattern corresponding to the digit to will be shown . This can be something like a table to interpret a number into the corresponding segments for display. This interpreted number is sent to the port where the display is connected.

    Multiplexing parameters:
    The most important parameter is the number of digits you want to activate. The parameters will depend upon:

    - The speed of scanning.
    With more connected digits more speed is required in the scan to avoid blinking.

    - The amount of luminosity of the segments.
    For example in the case of 4 digits, the segments will be activated for 25% of the time. So, for this type of application it is absolutely necessary to use a high brightness display and adjust the current sufficiently.

    - The speed of the code:
    The writing of the code is also important to avoid blinking. Parameters must be calculated and transferred without interruptions causing value errors. In my opinion it would be highly recommended to use a PIC with the maximum speed e.g. 40Mhz, 48Mhz or 64Mhz depending on the family. We will see an example in the code.
    The variable used also depends on the number of digits, "DISPVAL" in our case.
    A Byte will be used for a value up to 99 (2 digits).
    A Word will be used to value up to 999 (3 digits).
    A Word will be used for a value up to 9999 (4 digits).
    A Dword will be used for a value up to 99999 (5 digits).
    A Dword will be used for value up to 999999 (6 digits).
    You can intuit that to calculate the values of the 6 digits with a variable Dword the number of lines of code will be much more important. It is another argument to operate the clock of the PIC at maximum frequency.

    -The hardware:
    The demonstration program is prepared to use common cathode or common anode displays. If each segment could, on peak, consume 15mA, then the consumption of one digit could reach 15 x 8 = 120mA. Consequently, to enable a display it is necessary to use a transistor that will support this current. Consequently, high performance and high brightness displays with low consumption must be used.

    Parameters of the test program:
    You can configure all possible versions: 2, 3, 4, 5 or 6 displays, Common cathode, common anode with transistors. Likewise you can configure the decimal point in the position you want.
    The code has been tested with Proteus V8.20. It can be adapted to any CPU, and interrupt timing of 10mS (100Hz) must be generated by TMR0 or TMR2. You could use another Timer if you choose. The scanning and writing on the PORT is done by interrupt. The code of the Timer0 and Timer2 has been written as simply as possible for the tests. But the possibility of refining the value of timing is more complicated. Great precision is not very important either. It is the visual test that will matter.

    Configuration of Timer:
    I have written 2 versions of Timer (Timer0 and Timer2), the most common in all the PICs.
    To reduce the number of code lines in the interrupt routine, I have not reloaded the Timer0.
    This way you can save 6 execution cycles.
    Context Save                
        If DisPlayInterruptFlag = 1 Then
            DisPlayInterruptFlag = 0        '/ Reset Timer interrupt flag
    Context Restore                    '/ Exit the interrupt routine
    "DisPlayInterruptFlag" is generic for the timer interrupt flag. This way you do not need to change the code when changing the timer. The Timer2 is easier to adjust by PR2 SFR but only uses one byte for the counter. It does not reach 10mS for a clock of 40, 48 and 64Mhz (4 or 6 mS)
    As the PICs have many variations the PDS user should review the PIC datasheet that will be used and modify the timer configuration accordingly.

    Configuration of the DisplayInit_Sub:
    This routine contains all the PIC and hardware parameters. The code will depend on each PIC and it will be the responsibility of the user PDS to adapt it. In particular putting the pins to digital, which is not so simple for some PICs.

    Configuration of the PORTs:
    The configuration of the ports must be done at the beginning of the program.
        '/ Definition of the PORTS for Display segments.
        Symbol SEGMENTS = LATD
        '/ Definition of the pins to enable displays to write.
        $if NumberDigits = 6
        Symbol DIGIT1 = LATE.0
        Symbol DIGIT2 = LATE.1
        Symbol DIGIT3 = LATE.2
        Symbol DIGIT4 = LATC.0 
        Symbol DIGIT5 = LATC.1
        Symbol DIGIT6 = LATC.2
    Data Table:
    To perform the conversion of numbers to segments, a data table is needed.
    In the original code, this table was written in ROM with CData. But I found out that by placing it in the eeprom, access to the data could be faster. Provided that the table starts at address 0 of the eeprom.
    For beginners, it is necessary to know when the entry or exit of a routine is a byte, the compiler almost always places the result to the WREG SFR. In this code, one line could be deleted.
    Transformation of the original code into new code to read the ROM:

    With this code for example:
        TempByte = Dig DISPVAL,0
        ONES = CRead8 SegmentsData[TempByte]
    Change to:
        WREG = Dig DISPVAL,0                ' Using WREG 1 line of code is saved.
        ONES = CRead8 SegmentsData[WREG]
    You get this code in asm:
        movff DISPVALH,PP0H
        movff DISPVAL,PP0
        movlw 0
        rcall Dig
        clrf TBLPTRLH,0
        movwf TBLPTRL,0
        movlw (SegmentsData & 0XFF)
        addwf TBLPTRL,F,0
        movlw ((SegmentsData >> 8) & 0XFF)
        addwfc TBLPTRH,F,0
        clrf EECON1,0
        bsf EECON1,PP_EEPGD,0
        movff TABLAT,ONES
    You can see that "movwf TempByte,0" has disappeared. Using this technique, you could save 1 line of code.

    These codes must be repeated for each digit. It is more effective if the eeprom is read in this way:
    Considering that "EECON1 = 0" can be written only once in the routine, then the following could be done. The Eread command is more complex and uses more lines of code.
        EEADR = Dig DISPVAL,0               
        EECON1bits_RD = 1
        ONES = EEDATA
    You get this code in much more efficient asm:
        movff DISPVALH,PP0H
        movff DISPVAL,PP0
        movlw 0
        rcall Dig
        movwf EEADR,0
        bsf EECON1,0,0
        movff EEDATA,ONES
    By reading the asm code generated by the compiler you can learn how it works and write your basic code accordingly to optimize it to the maximum. That is why I consider that the Proton Basic is a very good compiler.
    I use this technique a lot in writing my libraries.

    Interrupt routine:
    To do the scan and write the PORT:
    I have modified the original code to improve the flicker, in the old case it could be some microseconds. The displays are off when the value of the PORT is changed.
        DIGIT6 = 0        '/ Turn OFF the Display 6.
        SEGMENTS = PRINT_ONES    '/ Change the value of the PORT (segments)
        DIGIT1 = 1        '/ Turn On the Display 1.
    Originally the table was written for displays with common anode. Using a very fast trick the code can be adapted to display with common cathode.
    SegmentsData: CData Byte 192,249,164,176,153,146,131,248,128,152,255

    Common anode:
        DIGIT6 = 1        '/ Turn OFF the Display 6.
        SEGMENTS = PRINT_ONES    '/ Change the value of the PORT (segments)
        DIGIT1 = 0        '/ Turn On the Display 1.
    Common cathode:
        DIGIT6 = 0        '/ Turn OFF the Display 6.
        SEGMENTS = ~PRINT_ONES    '/ Change the value of the PORT (segments)
        DIGIT1 = 1        '/ Turn On the Display 1.
    Only inverting the bits of the PORT and the ON/OFF polarity of DIGIT1 to DIGIT6.

    Avoid the blinking of interruptions:
    If the interrupt occurs during the execution of the routine "Build_Digits_Sub" we could send the digits some old values for the high and new digits for the low digits at the same time. To solve this problem I have created some buffers whose load is independent of the interrupts in the following way. The interrupt generated by the Timer stops for a very short time.
            ' Make a copy to print the values for the interrupt routine.
            DisPlayInterruptEnable = 0           '/ Avoid blinking.
            PRINT_ONES = ONES
            PRINT_TENS = TENS
            PRINT_THOUSANDS = THOUSANDS        
            DisPlayInterruptEnable = 1
    Configuration of the program:
    The test program is completely configurable to obtain the necessary code to drive 2 to 6 displays.

    $define DisplayMode 0
    Displays with common cathode.

    $define NumberDigits 4
    Displays with 4 digits.

    $define DecimalP 1
    Decimal point before the first digit from the right.

    $if _eeprom > 0
    $define TableData 1
    $define TableData 0
    The eeprom for Table Data is taken automatically if exists.

    The Timer0 or Timer2 can be choosen manually.
    '/ Configuration of the displays hardware.
        '/ Declare Display Mode (0 => Common Cathode) (1 => Common Anode) with transistors
        $define DisplayMode 0    
        '/ Declare Number of Digits (2,3,4,5,6)
        $define NumberDigits 4
        '/ Declare DecimalP position (0,1,2,3,4,5) (0 = No DP)
        $define DecimalP 1
    '/ Declare the Timer used for scanning. (one only)
    '    $define ScanTimer0
        $define ScanTimer2
    '/ Declare the interruption flags (automatic).
        $ifdef ScanTimer0
        $define DisPlayInterruptFlag INTCONBits_TMR0IF
        $define DisPlayInterruptEnable INTCONBits_TMR0IE
        $ifdef ScanTimer2    
        $define DisPlayInterruptFlag PIR1Bits_TMR2IF
        $define DisPlayInterruptEnable PIE1Bits_TMR2IE
    '/ Calculate the better option for Table Data (automaticly).
        '/ Declare Table Data (0 from ROM, 1 from eeprom)
        $if _eeprom > 0
        $define TableData 1    
        $define TableData 0    
    06/19/2018 Update code for driving 7 displays + sign. (total 8 displays)
    Thanks to the very good structure of the code written by the original author (Les Johnson I suppose) I have been able to write this new template. The code seems complicated, but everything is based on modules that can be adapted.
    The program manages input signed numbers such as SByte / SWord and variable SDword variables.

    I have also added the possibility to write Hex characters in the displays.
    Thanks to the total handling on the displays I have written some macros that allow to shift the numbers to the right and to the left.

    If the value of the input variable exceeds the maximum allowed, an error code [E] is sent in the last digit on the left.

    06/20/2018 Update new version 2.2. I have expanded to 8 digits for integer variables.

    06/22/2018 Update code for driving 8 displays with one PORT only with the help of simple and economical logic. (74HC5411 + 74HC238)
    Some improvements have been introduced in all the libraries.
    Note: It is important to remember to adjust the delays in the interrupt routine to adapt them to the real circuits for better efficiency.
    Also I have updated the previous codes. All codes are built as 3 libraries.

    06/28/2018 update version 3.0
    In the main program I had left the variable DISPVAL as a test variable. Using a library variable is not the most convenient. To expand the possibilities for the PDS user I have modified the macro "BuilDigits (MyDword)" so you can enter any variable from your main program.
    BuilDigits (MyWord)
    BuilDigits (MyByte)

    06/30/2018 New library for 3 to 8 seven segment displays.
    The idea of further increasing the possibilities of the 7 segment displays has occurred to me, using all the wonderful possibilities offered by the Proton Development Suite compiler. The trick is taking a command capable of extracting and sending each digit of a specific number. Then any of the serial communication commands could be used. Thanks to Les Johnson for leaving us the possibility of modifying the low level routines, we can get some small wonders.

    Proton allows software functions to be modified. I have modified RSOUT to continue to use modifiers, but instead of outputting to a serial pin the output goes to the pins driving the 7 segment displays. This has made it easier to send integers, floats, strings etc to the pins connected to the displays.

    What is the RsOut command?
    According to the PDS manual.

    Rsout Item {, Item... }

    Send one or more Items to a predetermined pin at a predetermined baud rate in standard
    asynchronous format using 8 data bits, no parity and 1 stop bit (8N1). The pin is automatically
    made an output. Of course, we are not going to use any PIC pin.

    Item may be a constant, variable, expression, or string list. There are no operators as such, instead there are modifiers.

    The modifiers allowed are listed below:
    Modifier Operation
    Bin{1..to number of display digits} Send binary digits
    Dec{0..to number of display digits} Send decimal digits (amount of digits after decimal point with floating point)
    Hex{1..to number of display digits} Send hexadecimal digits
    Sbin{1..to number of display digits} Send signed binary digits
    Sdec{0..to number of display digits} Send signed decimal digits
    Shex{1..to number of display digits} Send signed hexadecimal digits

    The numbers after the Bin, Dec, and Hex modifiers are optional. If they are omitted, then the
    default is all the digits that make up the value will be displayed, then always write the number of digits (very important) The RsOut command must be terminated by "CR" (Carriage Return). That ends the process of sending the bytes to the display.

    If a floating point variable is to be displayed, then the digits after the Dec modifier determine
    how many remainder digits are send. i.e. numbers after the decimal point.
    Dim MyFloat as Float
    MyFloat = 3.145
    Rsout Dec2 MyFloat,CR ' Send 2 values after the decimal point
    The above program will send 3.14. If the digit after the Dec modifier is omitted, then 3 values will be displayed after the decimal point.
    ' Display a negative value on the display.
    Symbol Negative = -200
    Rsout Sdec Negative,CR
    MySWord = -200
    Rsout Sdec MySWord,CR
    For the display application it is not advisable to use other modifiers. Other modifiers could cause a crash of the PIC program because you can not override/modify the other options that are used for other commands.

    Operation mode:
    It is preferable to use these forms only.
    RsOut Dec MyWord,CR
    RsOut Dec3 MyFloat,CR
    RsOut Hex3 MyWord,CR
    RsOut Bin4 MyWord,CR

    If you prefer to have a control of the variables to avoid errors; use my macros
    PrintDispInt(Dec, MyDword) ' Only for Integer.
    PrintDispInt(Bin8, MyWord) ' Only for Integer.
    PrintDispFloat(Dec3, MyFloat) ' Only for Float.
    PrintDispSign(SDec, MySDword) ' Only for Signed.
    PrintDispSign(Bin7, MySDword) ' Only for Signed.

    At the last minute, I have thought about incorporating the possibility to write ASCII characters in the display. The code is built in such a way that characters must be entered in higher case only as the compiler sends higher case for HEX characters.
    You will always have to take into account the number of displays used. Again, the RsOut command must be terminated with "CR". Without these, the PICmicro™ will continue to transmit data until an error is detected.

    Error detection:
    As typos are very common, I have planned a detection of the number of incoming bytes to avoid printing erroneous values on the display. Since I cannot know what your code will be like, the only way to perform the operation is to use interrupts. You can check that your program will continue running while printing the text "Error" on the displays for a configurable time ().

    Principle of operation:
    The biggest problem is transforming the data sent by the RsOut command with the MSB first.
    To solve this problem, an array of (Number of Displays + 1) bytes is used. The MSB of the number is written first in the top of the array and it keeps recording until position zero. As the
    characters move within the array, a blank character is written in the free positions. In this way, digits without numbers will always be turned off. Then the incoming number becomes LSB first.
    I have written many comments in the code to understand the operation of the library.

    07/04/2018 New version 1.1 of library for 3 to 8 seven segment displays.
    I have modified the data table. In this way it has been possible to improve the efficiency of the code, less lines and faster. In the multiplexer library I was able to remove 8 lines in the interrupt routine. The characters of Strings must be entered in higher case only.

    I hope that my comments on my code are accurate enough so that PDS users can modify the test code to make it their own.
    Good luck for your project.

    You can get the code HERE.