For this project, I wanted to drive a 7-segment display directly with the Arduino Nano Every with a few goals in mind:

  • Capable of driving displays with variable number of digits
  • Compatible with any led-display architecture: CC (Common Cathode) and CA (Common Anode)
  • Reusable/Portable: can be used along with other sketches without affecting their normal operation
  • Use the Arduino Nano Every with very basic components (no shift registers or dedicated display-drivers)
  • Keep it simple: the same principle can be used for alpha-numeric displays but this may leave fewer GPIO available for other purposes.

For this particular project I will be using the Arduino IDE and will be operating the registers directly with Bitwise operations. When I decided to work with the new Arduino Nano (with the ATmega4809 micro-controller) this proposed a challenge and a learning experience at the same time since this new processor uses a different set of registers than the old Arduinos (which I'm more familiar with).

 

 

Hardware Setup

Arduino Nano Every Pinout

Arduino Nano Every Pinout

Common Cathode Schematic

Nano Every Led Display CC schematic

CC - BOM

  • Arduino Nano EveryArduino Nano Every
  • DS1: Common Cathode display. TDCG1060mTDCG1060m
  • R1, R2, R3, R4, R5, R6, R7, R8: 220 ohm resistors
  • R9, R10, R11, R12: 10k ohm resistors
  • Q1, Q1, Q2, Q4: Any N-Channel Signal Mosfet like the BSS138BSS138

 

Common Anode Schematic

Nano Every Led Display CA schematic

CA - BOM

  • Arduino Nano EveryArduino Nano Every
  • DS1: Common Anode display. LDQ-M3604RILDQ-M3604RI
  • R1, R2, R3, R4, R5, R6, R7, R8: 220 ohm resistors
  • R9, R10, R11, R12: 10k ohm resistors
  • Q1, Q1, Q2, Q4: Any P-Channel Signal Mosfet like the NTR1P02NTR1P02

 

Source code overview

I want to expand on the key aspects of the code. One of the reasons I decided to write directly to the port registers is because I wanted to use this source code along with other projects. To make this process simpler, I will be using masks in order to change the GPIO in use by the display only.

 

Setup

The most important part of the code initialization comes in the form of two arrays:

  • segmentChar: defines how each segment of the display is switchen On/Off to display each digit
  • dirMask: defines which digital pins will be used as outputs for each segment.
// Common Cathode  Segment GFEDCBA
byte segmentChar[10]  = {B00111111, // 0
                         B00000110, // 1
                         B01011011, // 2
                         B01001111, // 3
                         B01100110, // 4
                         B01101101, // 5
                         B01111101, // 6
                         B00000111, // 7
                         B01111111, // 8
                         B01101111, // 9
                        };

      /*   |-- A-PE0 --|
       *   |           |
       *  F| PF4   PB1 |B
       *   |           |
       *   |-- G-PB2 --|
       *   |           |
       *  E| PA1   PB0 |C
       *   |           |
       *   |-- D-PE3 --| o PC6
      */
byte dirMask[5] = {B00000010, // PORTA ATmega4809
                   B00000111, // PORTB ATmega4809
                   B01000000, // PORTC ATmega4809
                   B00001001, // PORTE ATmega4809
                   B00010000  // PORTF ATmega4809
                  };

Each time a CA (Common Anode) display needs to be operated, the segmentChar array will be inverted with a simple operation.

 

Working with CC (Common Cathode) and CA (Common Anode) displays

Unfortunately, there isn't a simple Bitwise operation that can fit "universally" both types of displays. To work around this problem, there are simple operations using the masks previously defined, like the following example:

void clearDigit() {                   // Turns off all segments 
  if ( this->displayType == 'A' ) {   // Common Anode
    VPORTA.OUT |= this->dirMask[0];
    VPORTB.OUT |= this->dirMask[1];
    VPORTC.OUT |= this->dirMask[2];
    VPORTE.OUT |= this->dirMask[3];
    VPORTF.OUT |= this->dirMask[4];
  } else {                             // Common Cathode
    VPORTA.OUT &= ~this->dirMask[0];
    VPORTB.OUT &= ~this->dirMask[1];
    VPORTC.OUT &= ~this->dirMask[2];
    VPORTE.OUT &= ~this->dirMask[3];
    VPORTF.OUT &= ~this->dirMask[4];
  }
}

 

Applying Persistence of Vision (PoV)

Since the 7-segment-displays with more than one digit share a Common terminal, we will need to apply PoV to correctly display anything accurately: alternating every digit for a period of time, long enough to be visible to the eye, but short enough to cover all digits without any ghosting or flickering effect.

 

To correctly apply PoV to the problem we need to do the following:

  • Every time there is a change in the number being displayed, the display is turned OFF and the refresh loop should start over from the least significant digit.
  • A bitwise rotation (also known as circular shift) to refresh one digit at a time (starting from the least to the most significant digit), leaving the outputs unused by the display OFF or untouched.
void refresh(uint16_t newNumber) {
  if (newNumber != this->currentNumber) {        // Forces refresh from the first digit when there is a new number to display
    this->currentNumber = newNumber;
    // clear display
    // Split the new number into individual digits and calculate the new number of digits
  }
  //display digit
  //Bitwise circular shift to the left

 

Bitwise circular shift

Let's assume the following scenario:

A 3-digit number needs to be displayed in a 4-digit display, in which case we would like to apply PoV only to 3 digits. For this, a Circular shift operation is needed to turn ON one digit at a time, but this circular shift should apply to 3 bits only. Also, this bitwise-rotation should be a left-shift since it needs to start with the least significant Digit first, all illustrated below:

Bitwise circular shift to the left (few bits)

Such operations are perhaps the most complex but critical part of the code, and can be summarized in these two instructions:

// digitCount holds the the number of digits to be displayed
digitShiftMask = 0xFF >> (8 - digitCount);

// Bitwise rotation
VPORTD.OUT = (VPORTD.OUT & ~digitShiftMask) | (digitShiftMask & ((VPORTD.OUT << 1) | ((VPORTD.OUT & digitShiftMask) >> (digitCount - 1))));

 

Improving the Persistence of Vision (PoV)

To complete this project, there is one last change needed -Adjusting the refresh rate of each digit-. This is a very important step, but first let me illustrate what is happening with the code as is when we try to display the number "1234" without any change.

7-Segment Display no POV

As you can see in the picture above, each digit is displayed for a very short period of time, so short it doesn't provide the Mosfets enough time to switch state (on-off). Each digit displayed looks like it's mixed with the next digit refreshed (ghosting), so our "1234" example turns like these digits are merged together "1+4, 2+1, 3+2, 4+3".

P-channel mosfet switch time

Yellow: Mosfet - Gate (Digit1, Arduino Pin A0), Blue: Mosfet  - Drain (Common Anode - Digit1 of the display)

 

To solve the problem, each digit needs to be displayed for a very short time but long enough that it will provide each Mosfet time to switch to OFF state. Such time should not be long or it may negatively affect the display adding flickering. To make this project more useful for many different uses I wanted to add a mechanism to accurately refresh the display without affecting other pieces of code. Naturally the best way to achieve this without using millis() or micros() in the Arduino is to use timers.

 

Timers in the new Arduinos with the  ATmega4809 micro-controller are a little different to handle in the code than their older siblings (different set of registers), of course, there are other ways to do this but I just implemented the easiest I could come by without changing a lot of code and without changing the default Prescaler value which may negatively affect the behavior of time dependent Arduino functions like PWM, the Servo library, and functions like delay(), micros(), millis(), etc.

 

Timer/Counter type B (TCB)

The megaAVR® 0-series of micro-controllers are equipped with powerful timers that cover a wide area of applications. The Timer/Counter Type B (TCB) offers a variety of features, operation modes and flexibility to perform the very basic functions of a simple timer.

megaAVR 0-series Overview

 

First, I want to find out what the default Prescaler division is and the overflow information, for which I wrote a simple program to verify such information:

 

void setup() {
  Serial.begin(9600); // start serial for output
  while (!Serial);    // If the Leonardo or Micro is used, wait for the serial monitor to open.
  Serial.println("Ready!");
  
  Serial.print("TCA0.SINGLE.CTRLA\t");
  Serial.println(TCA0.SINGLE.CTRLA, BIN);


  TCB0.CCMP = 0xFFFF;                                // Value to compare with. Highest value
  TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // Use Timer/Counter type A (TCA), enable timer
  TCB0.CTRLB = TCB_CNTMODE_INT_gc;                   // Use timer compare mode
  TCB0.INTCTRL = TCB_CAPT_bm;                        // Enable the interrupt
}


void loop() {
}


ISR(TCB0_INT_vect) {
  TCB0.INTFLAGS = TCB_CAPT_bm; // Clear the interrupt flag
  Serial.println(millis());
}

 

According to output, the default Prescaler division is 64 when using the TCA clock.

ATmega4809 TCB0 CTRLA

ATmega4808_4809 Prescaler register

Period calculation

Timer period

The 16-bit timer overflow calculation, which pretty much matches the Serial output (milliseconds)

Timer overflow

Timer overflow serial output

With those calculations in mind, I found that 2ms is enough time to display each digit providing good Persistence of Vision (POV) and providing each Mosfet enough time to switch state:

Overflow calculation

With 2ms and our current setup, the overflow value of 500 (0x01F4) will require the following changes to the code

// Timer Setup
TCB0.CCMP = 0x01F4; // Value to compare with (500 cycles). 2ms

ISR(TCB0_INT_vect) {
  LedDisplay.refresh(rpm);
  TCB0.INTFLAGS = TCB_CAPT_bm;
}

7 segment display, 2ms digit refersh

Yellow: Mosfet - Gate (Digit1, Arduino Pin A0), Blue: Mosfet  - Drain (Common Anode - Digit1 of the display)

As an added bonus, there is no flickering in the project demo video which lead us to conclude that we are achieving a nice refresh rate.

 

Final source code

With all the challenges this project represented, I wrote a complete piece of code which can handle any type of display (CC - Common Cathode and CA - Common Anode) and can be reused pretty much with any project. This particular example will display a number (starting with zero) and will increment it by one every quarter of a second (250ms).

Note that this 250ms interval is calculated with the delay() function demonstrating that the display is refreshed normally and works with other pieces of code.

 

//Code for the Arduino Uno
/*
          |--A--|   |-----| L1|-----| o |-----|
          |F   B|   |     | o |     | L3|     |
          |--G--|   |-----| L2|-----|   |-----|
          |E   C|   |     | o |     |   |     |
          |--D--|oDP|-----|o  |-----|o  |-----|
             D1        D2        D3       D4
*/


#define DISPLAY_DIGITS 4 // Number of digits of 7-Segment display


class Led7SegmentDisplay {
  private:
    // Common Cathode  Segment GFEDCBA
    byte segmentChar[10]  = {B00111111, // 0
                             B00000110, // 1
                             B01011011, // 2
                             B01001111, // 3
                             B01100110, // 4
                             B01101101, // 5
                             B01111101, // 6
                             B00000111, // 7
                             B01111111, // 8
                             B01101111, // 9
                            };


    byte dirMask[5] = {B00000010, // PORTA ATmega4809
                       B00000111, // PORTB ATmega4809
                       B01000000, // PORTC ATmega4809
                       B00001001, // PORTE ATmega4809
                       B00010000  // PORTF ATmega4809
                      };


    uint8_t displayDigits; // Number of Digits of the Display
    uint8_t digitCount;    // Number of digits of the Number being/to be displayed
    uint8_t digitIndex;    // Controls the Digit that is being refreshed at the moment
    char    displayType;   // Display type: a,A (Common Anode), else (Common Cathode)
    byte    dispTypeMask;  // Mask according to the number of digits of the display




    uint8_t digitShiftMask;


    uint16_t currentNumber; // Current number displayed


    uint8_t split[5] = {0, 0, 0, 0, 0}; // to Split the number into individual digits


  public:
    /* function Led7SegmentDisplay
     *   digits    : display digits
     *   common_pin: 'a', 'A' Common Anode, else common cathode
     */


    Led7SegmentDisplay(uint8_t digits, char display_type) {
      this->displayDigits   = digits;


      this->currentNumber  = pow(10, this->displayDigits);


      this->digitCount = 0;
      this->digitIndex  = 0;
      this->displayType = toupper(display_type);


      this->dispTypeMask = 0xFF >> (8 - this->displayDigits);


      init();
    }


    void init() {
      VPORTA.DIR |= this->dirMask[0]; // sets ATmega4809 digital pins as outputs (Segment E)
      VPORTB.DIR |= this->dirMask[1]; // sets ATmega4809 digital pins as outputs (Segment G, B, C)
      VPORTC.DIR |= this->dirMask[2]; // sets ATmega4809 digital pins as outputs (Segment DP - Decimal Point)
      VPORTE.DIR |= this->dirMask[3]; // sets ATmega4809 digital pins as outputs (Segment D, A)
      VPORTF.DIR |= this->dirMask[4]; // sets ATmega4809 digital pins as outputs (Segment F)


      VPORTD.DIR |= this->dispTypeMask; //sets ATmega analog pins A# as Outputs // Digit D1 .. Dn (Dn = A0)
    }


    void clearDigit() {                   // Turns off all segments 
      if ( this->displayType == 'A' ) {   // Common Anode
        VPORTA.OUT |= this->dirMask[0];
        VPORTB.OUT |= this->dirMask[1];
        VPORTC.OUT |= this->dirMask[2];
        VPORTE.OUT |= this->dirMask[3];
        VPORTF.OUT |= this->dirMask[4];
      } else {                             // Common Cathode
        VPORTA.OUT &= ~this->dirMask[0];
        VPORTB.OUT &= ~this->dirMask[1];
        VPORTC.OUT &= ~this->dirMask[2];
        VPORTE.OUT &= ~this->dirMask[3];
        VPORTF.OUT &= ~this->dirMask[4];
      }
    }


    void refresh(uint16_t newNumber) {
      if (newNumber != this->currentNumber) {        // Forces refresh from the first digit when there is a new number to display
        this->currentNumber = newNumber;
        digitCount = 0;
        while (newNumber > 0 || digitCount == 0) {   // Split the new number into individual digits and calculate the new number of digits//
          split[digitCount++] = newNumber % 10;
          newNumber     /= 10;
        }
        digitIndex = 0;


        clearDigit();
        if ( this->displayType == 'A' )
          VPORTD.OUT = (VPORTD.OUT | this->dispTypeMask) & (0xFF ^ (0x01 << (digitCount - 1)));
        else
          VPORTD.OUT = (VPORTD.OUT & ~this->dispTypeMask) | (0x01 << (digitCount - 1));
        digitShiftMask = 0xFF >> (8 - digitCount);
      }


      /*    |-- A-PE0 --|
       *    |           |
       *   F| PF4   PB1 |B
       *    |           |
       *    |-- G-PB2 --|
       *    |           |
       *   E| PA1   PB0 |C
       *    |           |
       *    |-- D-PE3 --| o PC6
      */


      clearDigit(); //turns off digit


      byte character = segmentChar[split[digitIndex]];
      if ( this->displayType == 'A' ) {
        VPORTA.OUT = (VPORTA.OUT | dirMask[0]) ^ (bitRead(character, 4) << 1);
        VPORTB.OUT = (VPORTB.OUT | dirMask[1]) ^ (bitRead(character, 6) << 2 | bitRead(character, 1) << 1 | bitRead(character, 2) << 0);
        VPORTE.OUT = (VPORTE.OUT | dirMask[3]) ^ (bitRead(character, 0) << 0 | bitRead(character, 3) << 3);
        VPORTF.OUT = (VPORTF.OUT | dirMask[4]) ^ (bitRead(character, 5) << 4);
      } else {
        VPORTA.OUT = (VPORTA.OUT & ~dirMask[0]) ^ (bitRead(character, 4) << 1);
        VPORTB.OUT = (VPORTB.OUT & ~dirMask[1]) ^ (bitRead(character, 6) << 2 | bitRead(character, 1) << 1 | bitRead(character, 2) << 0);
        VPORTE.OUT = (VPORTE.OUT & ~dirMask[3]) ^ (bitRead(character, 0) << 0 | bitRead(character, 3) << 3);
        VPORTF.OUT = (VPORTF.OUT & ~dirMask[4]) ^ (bitRead(character, 5) << 4);
      }


      // Bitwise rotation to the left (just for the number of bits equivalent to the number)
      VPORTD.OUT = (VPORTD.OUT & ~digitShiftMask) | (digitShiftMask & ((VPORTD.OUT << 1) | ((VPORTD.OUT & digitShiftMask) >> (digitCount - 1))));
      digitIndex = ++digitIndex % digitCount;
    };
};


//Led7SegmentDisplay LedDisplay(4, 'C'); // Common Cathode
Led7SegmentDisplay LedDisplay(4, 'A'); // Common Anode


uint16_t      rpm = 0;


void setup() {
  TCB0.CCMP = 0x01F4;                                // Value to compare with (500). 2ms
  TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // Use Timer/Counter type A (TCA), enable timer
  TCB0.CTRLB = TCB_CNTMODE_INT_gc;                   // Use timer compare mode
  TCB0.INTCTRL = TCB_CAPT_bm;                        // Enable the interrupt
}


void loop() {
  rpm++;
  delay(250);
}


ISR(TCB0_INT_vect) {
   LedDisplay.refresh(rpm);
   TCB0.INTFLAGS = TCB_CAPT_bm; // Clear the interrupt flag
}

 

Technical details:

  • Digit display time: 2ms
  • Display refresh rate: approx. 500Hz / digits_displayed. e.g: 2 digits = 1 / (2digits x 2ms) = 250Hz
{gallery:width=648,height=432,autoplay=false} 7-segment display demo

7-segment display CA arduino nano every

Common Anode demo: 7-segment display

7-segment display CC Arduino nano every

Common Cathode demo: 7-segment display

7-segment display animation - Arduino Nano Every

Thanks for reading and a big thanks to Element14 for sponsoring my project!

 

Luis