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.