PWM på AVR med C

For å fake analoge signaler fra en mikrokontroller, benyttes en teknikk kalt pulsbreddemodulasjon. På engelsk er dette «Pulse-Width Modulation» (PWM).

Her sender man ut korte pulser (gjerne på 5V), også ingenting mellom hver puls. Tidsrommet mellom hver puls bestemmer hvor høy gjennomsnittsspenningen blir per periode. Sagt på en annen måte; bredden på pulsen (opp til 100%), styrer hvor høy gjennomsnittsspenningen blir per periode .. (Hver periode er veldig kort, den kan som regel måles i millisekunder.)

En eksempelillustrasjon som viser slike pulser grafisk:

Øverst har man en pulsbredde på 50%, da blir gjennomsnittsspenningen 2.5V. Og nedenfor gir 33% bredde ca 1.67V. Videre ville man fått 0.5V med 10% bredde, 1.25V med 25% bredde, osv.

Altså kan man fake hvilken som helst spenning man ønsker, så lenge man klarer å sette riktig størrelse/bredde på hver puls.

Arduino

Det finnes sikkert mange dokumenterte Arduino-prosjekter på Internett, som viser PWM i praksis. Men når arduino.cc har en side hvor analogWrite() forklares og det er eksempelkode rett nedenfor som viser riktig bruk. Da trengs det ikke noe mer.

Som da jeg lærte om ADC, er det også her kun en nødvendig kodelinje i Arduino:

analogWrite(3, pwmVerdi / 4);
// pwmVerdi er 0-1023, mens analogWrite() trenger 0-255. Tallet 3 er digital pinne nr. 3, denne støtter PWM

Ved å koble til en lysdiode kan man bruke denne for å teste PWM i praksis:

Et komplett kodeeksempel, hvor PWM benyttes for å sakte tenne/slukke en lysdiode:

/*
 * Settings (feel free to change and experiment)
 */
float secPerPeriod = 2; // # of seconds for each completed sine wave period
float frequency = 1.0/secPerPeriod; // Hz, the # of completed sine wave periods in 1 sec
float numberOfIncrements = 1000; // many increments makes for smoother LED lighting

/*
 * Sine wave constants (don't touch!)
 */
const float CIRCLE_RADS = 2*3.145; // 2 times PI constant
const float INCREMENT = CIRCLE_RADS / numberOfIncrements; // how much more to feed into sin() each time
const float TIME_PER_INCREMENT_MS = ((1.0/frequency) * 1000) / numberOfIncrements;

/*
 * Sine wave controls when program runs (don't touch!)
 */
float circleDegreesInRads = 0; // input for sin(), added to during loop
unsigned long timeLastRun = 0; // controls next execution time, updated during loop

void loop() {

   if (timeLastRun == 0 || timeLastRun + TIME_PER_INCREMENT_MS <= millis()) {
      timeLastRun = millis();

      // Update sine wave
      float sinVal = sin(circleDegreesInRads);
      circleDegreesInRads += INCREMENT;
      if (circleDegreesInRads >= CIRCLE_RADS) circleDegreesInRads = 0;

      // Write value to pin connected to LED
      float pwmVal = ((sinVal + 1) / 2) * 1024;
      analogWrite(3, pwmVal / 4);
   }

   delay(1);
}

void setup() {} // unused here

For morroskyld tok jeg i bruk sin()-funksjonen for å styre lysdioda, da det ville blitt for kjedelig å la være. Hastigheten kan enkelt styres ved å angi antall sekunder den periodiske funksjonen skal bruke. Dette gjøres med secPerPeriod øverst.

Embedded C

I Arduino er PWM tydeligvis enkelt, men hvordan arter det seg når man skal gjøre samme jobben via embedded C?

For å finne ut av dette måtte jeg studere hvordan PWM fungerer. Samt lese deler av dataarket (fra side 116 og utover) for mikrokontrolleren. Her er dette ATmega328P, den mye brukte chip-en for Arduino.

Praksis

Samme lysdiode og pinne ble brukt her som over. Den valgte pinnen (digital pinne 3 på Arduino), er koblet til pinne PD3 på ATmega328P. Denne pinnen støtter som nevnt PWM.

Den første kodesnutten som ble ferdig – her med «Fast PWM» uten invertering:


#define F_CPU 16000000UL

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
   // Enable pin output
   DDRD |= (1<<PD3); // PD3, also named OC2B

   // PWM Mode: 11 = Fast PWM, 01 = Phase correct PWM
   TCCR2A = 0x00; // Clear entire register
   TCCR2A |= 1<<WGM21 | 1<<WGM20;

   // Counter mode:
   // 10 = start the pulse when counter at bottom
   // 11 = start the pulse when counter matches
   // TCCR2A |= 1<<COM2B1; // OLD solution: Not inverted, the LED still lit when pin was LOW
   TCCR2A |= 1<<COM2B1 | 1<<COM2B0; // inverted mode

   // Set prescaler
   TCCR2B |= 1<<CS21; // 1024 division, don't know what I'm doing here really ..

   int max = 255;
   while (1)
   {
      // Testing ...

      // Increasing voltage
      for (int voltagePercent = 0; voltagePercent <= 100; voltagePercent++) {

         // OCR2B = 255 * voltagePercent/100; // OLD solution: Not inverted, the LED still lit when pin was LOW
         OCR2B = max - (max * voltagePercent/100); // 255 is max, and when inverted this is the starting point instead of 0
         _delay_ms(20); // enough time to see the slow and soft voltage increase on the LED
      }

      _delay_ms(200); // enough time to see the HIGH state on the pin/LED

      // Decreasing voltage
      for (int voltagePercent = 100; voltagePercent >= 0; voltagePercent--) {

         OCR2B = max - (max * voltagePercent/100); // when 0% the pin is actually LOW (0V) and the LED is completely off
         _delay_ms(20);
      }

      _delay_ms(200); // enough time to see the LOW state
   }
}

Her oppdaget jeg raskt at lysdioda ikke ble helt slukket, selv om PWM-verdien var 0.

Men ved å skru på invertering løste det seg:

Etterpå prøvde jeg «phase correct PWM» uten invertering:


#define F_CPU 16000000UL

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
   // Enable pin output
   DDRD |= (1<<PD3); // PD3, also named OC2B

   // PWM Mode: 11 = Fast PWM, 01 = Phase correct PWM
   TCCR2A = 0x00; // Clear entire register
   TCCR2A |= 1<<WGM20;

   // Counter mode:
   // 10 = start the pulse when counter at bottom
   // 11 = start the pulse when counter matches
   TCCR2A |= 1<<COM2B1;

   // Set prescaler
   TCCR2B |= 1<<CS21; // 1024 division, don't know what I'm doing here really ..

   while (1)
   {
      // Testing ...

      // Increasing voltage
      for (int voltagePercent = 0; voltagePercent <= 100; voltagePercent++) {

         OCR2B = 255 * voltagePercent/100;
         _delay_ms(20); // enough time to see the slow and soft voltage increase on the LED
      }

      _delay_ms(200); // enough time to see the HIGH state on the pin/LED

      // Decreasing voltage
      for (int voltagePercent = 100; voltagePercent >= 0; voltagePercent--) {

         OCR2B = 255 * voltagePercent/100;
         _delay_ms(20);
      }

      _delay_ms(200); // enough time to see the LOW state
   }
}

Resultatet ble som i videoklippet over. Kanskje jeg ville oppdaget forskjell om jeg hadde koblet til noe annet, som en motor.

Teorien

For å sakte tenne og slukke en lysdiode, fant jeg ut at man må manipulere minst 4 stk. registre. Til sammen blir det en del forskjellige bits å holde styr på, da det er mange mulige innstillinger her. Deler av registrene – altså enkelte bits, brukes til å styre timere/tellere. Disse kan brukes bare til å telle, eller man kan også benytte de for å skape spenningspulser i forskjellige størrelser/bredder.

For ATmega328P og 8-bit registeret brukt i dette prosjektet, telles det fra 0 til 255. Og den sammenligner stadig tellerverdien med en gitt verdi fra programmet som kjører. Ved treff utføres det en handling bestemt av programmet.

Valg av pinne for utsignalet styrer direkte hvilke registre og bits man må forholde seg til. Og siden PD3 var valgt her, er det OC2B (annet navn for PD3), med registre og bits tilknyttet denne pinnen, som måtte brukes.

Dataarket prøver å forklare alt dette, men det tar tid å sette seg inn i hver enkelt del, og forstå hvilken rolle hvert register og bit spiller.

Data Direction Register D (DDRD)

Som vanlig må man sette ønsket pinne til ut («output»), når man ønsker å sende signal ut. Derfor ble PD3/OC2B (pinne 3 på port D) satt til ut i DDRD-registeret. Veldig enkelt.

Timer/Counter Control Register A og B (TCCR2 A og B)

ATmega328P har flere kontrollregistre for timere/tellere og bruken av PWM. Og både register A (TCCR2A) og B (TCCR2B) måtte brukes i denne sammenhengen.

Først i register A har man bit-ene WGM21 og WGM20, som sammen med bit WGM22 i register B, muliggjør å skape pulser. Man kan benytte enten «Fast PWM» eller «phase correct PWM». (Side 130 i dataarket.)

I register A har man også bit-ene COM2B1 og COM2B0, som styrer oppførselen på pinne OC2B (altså PD3). Her angir man ønskede handlinger for telleren. Og ikke bare når den får et treff, men også ved start og stopp av tellingen. (Side 129 i dataarket.)

I register B er det også bits (CS22, CS21 og CS20) for å sette «prescaler». Så PWM-en får lavere klokkefrekvens enn det mikrokontrollerens hovedklokke har.

Jeg prøvde de fleste tenkelige kombinasjonene i disse to registrene. Men bare for å tenne ei lysdiode så er alle pulsinnstillingene i grunn OK. Det trengs særere prosjekter for å virkelig dra nytte av det disse registrene muliggjør, antar jeg.

Output Compare Register 2B (OCR2B)

Verdien som telleren skal sammenligne mot, oppbevares i 8-bit registret OCR2B.

Med 8-bit blir det plass til alle bitverdier fra 000 000 og til 1111 1111. Dette er 0-255 i desimaltallform. Samme som tellerverdien kan være, så lenge det er snakk om 8-bit.

Konklusjon

De mange registrene og bitsene gjør PWM vanskelig å fullt ut forstå. Det er for mange muligheter og dataarket er tungt å lese. Det skulle vært tatt med eksempler, som viste kode og forskjellige konfigurasjoner i praksis.

Min intuisjon når det gjelder bruk av PWM-funksjonalitet er derfor lik 0 akkurat nå. Men med tiden vil jeg få mer erfaring. Da vil dette forhåpentligvis forbedre seg. Som med matte, er også dette faget et modningsfag.

Legg igjen en kommentar

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