7-segmentdisplay på AVR med C

Etter suksessen med LCD på AVR, var jeg ivrig på å gå i gang med 7-segmentdisplay. Årsaken er at denne type display ofte ble brukt i MacGyver, så jeg fikk tidlig en viss fascinasjon for den.

Valget stod mellom 4-sifret og 1-sifret display. Jeg valgte 4-sifret fordi dette displayet ganske opplagt har flere bruksområder. Man kan vise tellere, klokka, temperatur, turtall, ja i grunn det meste.

Teori

Forskjellen mellom 4-sifret og 1-sifret er ikke stor, de kobles omtrent likt. De har samme antallet segmenter og kommer med felles katode(r) eller felles anode(r).

Segment

Hvert segmentdisplay har 7+1 lysdioder fordi hver strek (7 til sammen) har hver sin diode og dermed sin egen pinne. I tillegg kommer desimalpunktet som benyttes for å vise desimaltall:

Så for å vise forskjellige tall trenger man bare å tenne de riktige diodene. Rekkefølge har ingenting å si. Tallet 8 vil f.eks. benytte alle diodene (ABCDEFG), mens tallet 1 kun benytter B og C. Osv.

Trenger man å vise bokstaver eller mer kompliserte tegn, må man i stedet benytte minst 14-segmentdisplay eller noe lignende. Da blir det enda flere segmenter og pinner å holde styr på.

Katode eller anode type

Hver lysdiode styres som sagt via sin egen pinne på displayet. Diodepinne nr. 2 derimot, som er internt i displayet, er parallellkoblet med pinne nr. 2 på alle de andre diodene i samme 1-siffer display (se bildet over). Dermed deler de "utgangspinne" på displayet for å danne en krets.

Avhengig av type er det enten minus/jord eller en spenningskilde som skal kobles til denne "utgangspinnen", eller mer riktig fellespinnen. Og når det er minus/jord så er displayet av typen med felles katode. Når det i stedet er en spenningskilde, så er det felles anode. På engelsk blir dette «common cathode» og «common anode» type.

Så minus/jord på felles pinne ➜ spenningskilde på segmentpinner. (Com. cathode)

Og spenningskilde på felles pinne ➜ minus/jord på segmentpinner. (Com. anode)

Husk også å koble resistor mellom, hvis spenningen er for høy fra spenningskilden. Selv dioder i segmentdisplay tåler ikke allverden av spenning.

I mikrokontrollersammenheng skaper man en spenningskilde ved å sette en pinne til høy. For å skape minus/jord setter man pinnen i stedet til lav.

Multipleksing

Hvordan kan man styre hvert segment på et mangesifret display, når det åpenbart ikke er nok pinner til dette? Vel, det kan man ikke! På et display med flere siffer er disse faktisk parallellkoblet, så lenge det er snakk om multipleksing. Hvert sifferdisplay viser da samme siffer, man kan ikke individuelt styre de.

Men man gis anledning til å bestemme om et valgt sifferdisplay skal være påslått eller ikke. Trikset blir derfor å signalisere hvilke segmenter som skal brukes, også tenne kun ønsket sifferdisplay – f.eks. nr. 1.. Deretter slukker man så valgt sifferdisplay og beveger seg til neste.

Hvis dette skjer raskt klarer ikke det menneskelige øyet å oppfatte at et sifferdisplay slukkes. Det vil se ut som hvert sifferdisplay alltid er påslått og med sitt eget siffer. (På film derimot kan man oppdage rask slukking og tenning, fordi et kamera kan ta bilder raskt.)

Arduino + segmentdisplay

En ting er å vite teorien for hvordan et display fungerer. En annen er å koble til et display i praksis og styre det fra en mikrokontroller. Så for å først bli kjent med segmentdisplay, fant jeg et passende Arduino-prosjekt som forklarte det meste jeg trengte å vite.

Dette var for et 4-sifret display, noe jeg fikk når jeg kjøpte Arduino-startsett:

Navnet på displayet er 5461AS-1. Den har 4 stk. felleskatoder (D1 D2 D3 D4). Hvert segment (ABCDEFG og DP) styres via pinne med samme navnet (se bilde).

Krets

Jeg måtte bruke en blanding av 220 \Omega og 1k \Omega resistorer, siden jeg ikke hadde nok av kun den ene typen:

I alt ble 8 stk. resistorer benyttet – 1 stk. per segment. Alternativet er å bruke 1 resistor per felleskatode (en for hvert sifferdisplay). Men da ville tall med mange segmenter fått svakere lysstyrke, enn tall med veldig få. (Omtrent samme spenning, men flere dioder = mindre strøm gjennom hver diode ..)

Kode

Med unntak av noen feilkoblede pinner, noe som er veldig fort gjort, var det rimelig smertefritt å få det til å fungere. For å bruke displayet riktig, er det viktig å vite hvilke segmenter på displayet som er koblet til hvilke pinner på Arduino-en.

Mitt oppsett ble omtrent som i Arduino-prosjektet over:

// LED names and Arduino pins
const int a = 2;
const int b = 3;
const int c = 4;
const int d = 5;
const int e = 6;
const int f = 7;
const int g = 8;
const int dp = 9; // decimal point

// On-off switch for each digit, and with Arduino pins
const int d1 = 13; // digit 1, etc.
const int d2 = 12;
const int d3 = 11;
const int d4 = 10;

Deretter må man lage nødvendig funksjonalitet, så hvert tall bruker de riktige segmentene, når displayet skal brukes:

// Digit configurations, 0-9
struct Digit {
   int pins[8]; // Arduino pins, 2-13
   char states[8]; // LOW or HIGH, in same order as pins!
};
const Digit digits[10] = {    // a     b     c     d     e     f     g     dp
   { {a, b, c, d, e, f, g, dp}, {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW,  LOW} },
   { {a, b, c, d, e, f, g, dp}, {LOW,  HIGH, HIGH, LOW,  LOW,  LOW,  LOW,  LOW} },
   { {a, b, c, d, e, f, g, dp}, {HIGH, HIGH, LOW,  HIGH, HIGH, LOW,  HIGH, LOW} },
   { {a, b, c, d, e, f, g, dp}, {HIGH, HIGH, HIGH, HIGH, LOW,  LOW,  HIGH, LOW} },
   { {a, b, c, d, e, f, g, dp}, {LOW,  HIGH, HIGH, LOW,  LOW,  HIGH, HIGH, LOW} },
   { {a, b, c, d, e, f, g, dp}, {HIGH, LOW,  HIGH, HIGH, LOW,  HIGH, HIGH, LOW} },
   { {a, b, c, d, e, f, g, dp}, {HIGH, LOW,  HIGH, HIGH, HIGH, HIGH, HIGH, LOW} },
   { {a, b, c, d, e, f, g, dp}, {HIGH, HIGH, HIGH, LOW,  LOW,  LOW,  LOW,  LOW} },
   { {a, b, c, d, e, f, g, dp}, {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW} },
   { {a, b, c, d, e, f, g, dp}, {HIGH, HIGH, HIGH, HIGH, LOW,  HIGH, HIGH, LOW} }
};

// ---------
// Functions

void writeDigit(int digit, bool dp) {
   struct Digit selectedDigit = digits[digit]; // 0-9

   for (int led = 0; led < 8; led++) {
      if (dp && led == 7)
         digitalWrite(selectedDigit.pins[led], "HIGH");
      else
         digitalWrite(selectedDigit.pins[led], selectedDigit.states[led]);
   }
}

void turnOnDisplay(int number) {

   // The 7-segment LED display is a common-cathode type
   // Set pins d1-d4 to HIGH to switch off all displays
   digitalWrite(d1, HIGH);
   digitalWrite(d2, HIGH);
   digitalWrite(d3, HIGH);
   digitalWrite(d4, HIGH);

   // Turn a display back on
   switch(number)
   {
      case 0: 
         digitalWrite(d1, LOW); // Display 1 on, etc. 
         break;

      case 1:
         digitalWrite(d2, LOW);
         break;

      case 2: 
         digitalWrite(d3, LOW);
         break;

      default: 
         digitalWrite(d4, LOW);
         break;
   }
}

Her gjorde jeg ting ganske annerledes enn i Arduino-prosjektet. For jeg ønsket ikke funksjoner for hvert siffer som skulle vises på et display. Jeg lagde i stedet en tabell av struct som kunne holde all nødvendig informasjon. Deretter trengte jeg bare en for-løkke for å tenne de riktige segmentene og skru på valgt sifferdisplay.

Testing

Ved bruk må Arduino-pinnene først settes til ‘ut’:

void setup()
{
   pinMode(a, OUTPUT);
   pinMode(b, OUTPUT);
   pinMode(c, OUTPUT);
   pinMode(d, OUTPUT);
   pinMode(e, OUTPUT);
   pinMode(f, OUTPUT);
   pinMode(g, OUTPUT);
   pinMode(dp, OUTPUT);

   pinMode(d1, OUTPUT);
   pinMode(d2, OUTPUT);
   pinMode(d3, OUTPUT);
   pinMode(d4, OUTPUT);
}

Deretter kan man begynne å ta i bruk displayet. Siden man nå har evnen til å skru på hvert enkelt sifferdisplay og vise et bestemt tall.

Teller

Først lagde jeg en enkel teller, slik som i Arduino-prosjektet. Den går fra 0 til 9999 før den begynner på 0 igjen:

long count = 0;

unsigned long updateTime = 0; // sec
unsigned long breakTime = 50; // ms, controls speed
int flashingDelay = 5;

void loop()
{  
   // Show numbers
   turnOnDisplay(0);
   writeDigit((count/1000), false);
   delay(flashingDelay);

   turnOnDisplay(1);
   writeDigit((count%1000)/100, false);
   delay(flashingDelay);

   turnOnDisplay(2);
   writeDigit(count%100/10, false);
   delay(flashingDelay);

   turnOnDisplay(3);
   writeDigit(count%10, false);
   delay(flashingDelay);

   // Increment count
   if (millis() >= updateTime) {
      updateTime = millis() + breakTime;

      count++;
      if (count == 10000) count = 0;
   }
}

Resultatet:

Hastigheten kan selvsagt justeres. I klippet venter den 50 ms mellom hver gang den teller oppover.

Klokke

Jeg lagde også ei klokke som bruker kompileringstidspunkt for å vise tiden:

char buildTime[] = __TIME__; // example 18:48:58
int hours = (buildTime[0]-'0')*10 + (buildTime[1]-'0');
int minutes = (buildTime[3]-'0')*10 + (buildTime[4]-'0');

unsigned long breakTime = 60; // sec, 1 min
unsigned long updateTime = 0; // sec
int flashingDelay = 5;

void loop()
{
  // Show clock
  // ----------

  int clock = hours*100 + minutes;

  turnOnDisplay(0);
  writeDigit((clock/1000), false);
  delay(flashingDelay);

  turnOnDisplay(1);
  writeDigit((clock%1000)/100, (millis() / 1000) % 2 == 0);
  delay(flashingDelay);

  turnOnDisplay(1);
  writeDigit((clock%1000)/100, 0); // shadow decimal point fix
  delay(flashingDelay);

  turnOnDisplay(2);
  writeDigit(clock%100/10, false);
  delay(flashingDelay);

  turnOnDisplay(3);
  writeDigit(clock%10, false);
  delay(flashingDelay);


  // Update clock each min
  // ---------------------

  if (millis()/1000 >= updateTime) {
    updateTime = millis()/1000 + breakTime;

    minutes++;
    if (minutes == 60) {
      minutes = 0;
      
      hours++;
      if (hours == 24) {
        hours = 0;
      }
    }
  }
}

Resultatet:

En ekstra fiks ble desimalpunktet i midten på displayet, for å vise sekunder.

I begge eksempler over vil det for menneskeøyet se ut som alle sifferdisplay er påslått samtidig. Fordi mikrokontrolleren skifter meget raskt mellom dem, flashingDelay er tross alt mindre enn 10 ms.

Hvis flashingDelay derimot settes til flere hundre millisekunder, ser man øyeblikkelig hvordan et segmentdisplay egentlig fungerer:

Ved flashingDelay lik f.eks. 20 ms vil hvert siffer blinke meget raskt. Man klarer ikke lengre fange opp at bare ett sifferdisplay er påslått om gangen.

Embedded C

Å mekke segmentdisplay i Arduino er ikke komplisert eller vanskelig. Som med LCD-prosjektet antok jeg at den reelle utfordringen ville bli å gjøre det i C for mikrokontrollere. Men denne gangen ble det ikke noen stor jobb, da jeg valgte å gjenbruke tabellen av struct for segmenter i hvert tall.

Alt som manglet da var tilsvarende funksjoner som de jeg skrev for Arduino.

For å erstatte millis() fant jeg avr-millis-function på GitHub som fikset biffen.

Med pinneoversikt for Arduino til ATmega328P, ble pinnene som følger:

// LED segments and connection to Arduino pins
const int A = 2; // PD2 on AVR
const int B = 3; // PD3 on AVR
const int C = 4; // PD4 on AVR
const int D = 5; // PD5 on AVR
const int E = 6; // PD6 on AVR
const int F = 7; // PD7 on AVR
// --
const int G = 8; // PB0 on AVR
const int DP = 9; // decimal point, PB1 on AVR

// On-off switch for each digit display and connection to Arduino pins
const int D1 = 13; // PB5 on AVR
const int D2 = 12; // PB4 on AVR
const int D3 = 11; // PB3 on AVR
const int D4 = 10; // PB2 on AVR

Nødvendig funksjonalitet for å tenne segmenter, når et tall er valgt:

// Digit configuration 0-9, each digit uses slightly different LED segments
struct Segment {
   volatile uint8_t *port; // PORTB, etc.
   volatile uint8_t pin; // PB1, etc.
};
struct Digit {
   struct Segment segments[8];
   int states[8]; // 0 or 1, in same order as pins
};																														    // LED segments:
const struct Digit DIGITS[] = {
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {1, 1, 1, 1, 1, 1, 0, 0} }, // 0
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {0, 1, 1, 0, 0, 0, 0, 0} }, // 1
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {1, 1, 0, 1, 1, 0, 1, 0} }, // 2
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {1, 1, 1, 1, 0, 0, 1, 0} }, // 3
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {0, 1, 1, 0, 0, 1, 1, 0} }, // 4
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {1, 0, 1, 1, 0, 1, 1, 0} }, // 5
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {1, 0, 1, 1, 1, 1, 1, 0} }, // 6
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {1, 1, 1, 0, 0, 0, 0, 0} }, // 7
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {1, 1, 1, 1, 1, 1, 1, 0} }, // 8
   { {{&PORTD, PD2}, {&PORTD, PD3}, {&PORTD, PD4}, {&PORTD, PD5}, {&PORTD, PD6}, {&PORTD, PD7}, {&PORTB, PB0}, {&PORTB, PB1}}, {1, 1, 1, 1, 0, 1, 1, 0} }  // 9
};

void showDigit(int number, int dp) { // number: 0-9, dp: 0 or 1
   const struct Digit selectedDigit = DIGITS[number];

   for (int s = 0; s < 8; s++) {
      if (selectedDigit.states[s] || (s == 7 && dp))
         *selectedDigit.segments[s].port |= 1<<selectedDigit.segments[s].pin;
      else
         *selectedDigit.segments[s].port &= ~(1<<selectedDigit.segments[s].pin);
   }
}


// -------------
// Displays, 1-4

struct Display {
   volatile uint8_t *port; // PORTB, etc.
   volatile uint8_t pin; // PB1, etc.
};
const struct Display DISPLAYS[4] = {{&PORTB, PB5}, {&PORTB, PB4}, {&PORTB, PB3}, {&PORTB, PB2}};

void enableDisplay(int number) { // 1-4
   for (int d = 0; d < 4; d++) {
      if (d+1 == number)
         *DISPLAYS[d].port &= ~(1<<DISPLAYS[d].pin); // low signal = on, when common cathode, because you ground it
      else
         *DISPLAYS[d].port |= 1<<DISPLAYS[d].pin; // high signal = off, when common cathode
   }
}

Også for å teste, her ved å vise klokka:

int main(void) {

   // Set data direction to output
   DDRD |= 1<<PD2;
   DDRD |= 1<<PD3;
   DDRD |= 1<<PD4;
   DDRD |= 1<<PD5;
   DDRD |= 1<<PD6;
   DDRD |= 1<<PD7;
   // --
   DDRB |= 1<<PB0;
   DDRB |= 1<<PB1;
   DDRB |= 1<<PB2;
   DDRB |= 1<<PB3;
   DDRB |= 1<<PB4;
   DDRB |= 1<<PB5;

   // Clock controls (HH.MM)
   init_millis(F_CPU);
   char buildTime[] = __TIME__; // example 18:48:58
   int hours = (buildTime[0]-'0')*10 + (buildTime[1]-'0');
   int minutes = (buildTime[3]-'0')*10 + (buildTime[4]-'0');

   // Loop controls
   unsigned long breakTime = 60; // sec, 1 min
   unsigned long updateTime = 0; // sec, stored millis()/1000
   int flashingDelay = 5; // ms
   while (1) {

      // Show clock
      int clock = hours*100 + minutes;

      enableDisplay(1);
      showDigit((clock/1000), 0);
      _delay_ms(flashingDelay);

      enableDisplay(2);
      showDigit((clock%1000)/100, ((millis() / 1000) % 2 == 0 ? 1 : 0)); // enable decimal point each every other sec
      _delay_ms(flashingDelay);

      enableDisplay(2);
      showDigit((clock%1000)/100, 0); // shadow decimal point fix
      _delay_ms(flashingDelay);

      enableDisplay(3);
      showDigit(clock%100/10, 0);
      _delay_ms(flashingDelay);

      enableDisplay(4);
      showDigit(clock%10, 0);
      _delay_ms(flashingDelay);

      // Update clock each min
      if (millis()/1000 >= updateTime) {
         updateTime = millis()/1000 + breakTime;

         minutes++;
         if (minutes == 60) {
            minutes = 0;

            hours++;
            if (hours == 24) {
               hours = 0;
            }
         }
      }
   }

   return 0;
}

Dette ga samme resultat som for Arduino. Displayet oppførte seg identisk.

TBC

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *