=== 16Bit SoftPWM 'Step by Step' (exemplarisch mit
AVR)=====braincode@gmx.de 09/2009 Thomas AHlendorf
Hier mal ein typischer Fall von etwas worüber man monatelang nachdenkt
und wenn man dann aufgibt (oder keine Zeit hat) dann fällts einem
einfach ein ...
>Ein Hinweis für den Leser:
< Die unten genannten Ideen sind auf meinem Mist gewachsen, ich stelle
es aber jedem frei sie zu benutzen und für sich zu modifizieren
< Sollte ein unten eingebrachter Gedanke nutzbringend in ein Grösseres
oder komerzielles Projekt einfliessen wäre die
< Erwähnung des Urhebers und dessen Benachrichtigung ein netter Zug.
< Sollte das Geschriebene unten hahnebüchen und unnütz sein will ichs
gar nicht wissen...
< Falls jemand die gleichen Ideen gerade 5min vorher veröffentlicht hat
soll es mir eine Ehre sein in Seinen Fusstapfen zu stolpern...:-}
>Ende des Hinweises>noch ein Hinweis für den Leser:
<Für Rechtschreibfehler und grammatikalische Ausrutscher bin ich leider
auch verantwortlich und bitte um nachsicht
<(hätt ich doch WORD genommen,hab ich aber nicht!)
<Der Code ist nicht vollständig und evl. sind Variablen nicht deklariert
(schande). Der Text ist mehr ein Tutorial und als Anregung gedacht, soll
aber wohl hierhin gehören.
>Ende des 2.Hinweises
// Erst mal 8Bit SoftPWM
//#####################################################
Pulsweitenmodulation an sich ist ja eigentlich nicht weiter dramatisch,
ich setz die Grundzüge einfach mal vorraus.
Die technische Umsetzung bei nutzung der Hardware z.B. des AVR ist auch
machbar - dann aber kommen die Einschränkungen:
- Hardware PWM ist super, aber es gibt leider nicht genügend kanäle (für
einige Anwendungen / beim AVR)
- Also Sofware-PWM; geht ganz gut - aber wenn man einfach die HW-PWM
sofwaremässig abbildet geht mit
hängen und biegen nur 8 Bit bevor z.B. LEDs flimmern.
- Glühlampen und Motoren sind kein Problem; aber LEDs die mit 8Bit
Auflösung gedimmt werden schnippen
dann einfach beim übergang von 0 zu 1 (von 255) an, weil man sie
einfach schon sieht wenn sie
~30us bei 100Hz an sind.
- 256 Stufen sind einfach zu wenig, zumal LEDs eigentlich noch
expotentiell gedimmt werden müssten um einen
gleichmässigen Helligkeitsanstieg zu ermöglichen.
- Also höhere Auflösung - wo wir wieder beim anfang der Schwierigkeiten
wären
Der erste Ansatz zur Lösung war ein Artikel in einem Forum (weiss leider
nicht mehr wo) in dem vorgeschlagen wurde die PWM-Werte vorher zu
berechnen, in
einem Array abzulegen (LookUp) und dann im IRQ schnell auszugeben.
(immer gleich 8 Kanäle --> ein Port)
..braucht schon bei 256 Stufen ziemlich viel Speicher, aber immerhin.
Es ist ja eigentlich auch ineffektiv die Werte jedesmal neuzuberechnen
auch wenn sie sich nicht ändern und noch dazu
in der Zeitkritischen IRQ Routine. Also mal losgelegt:
//#####################################################
1
uint8_tKanal[8];//Kanalwert 0..255
2
uint8_tpwm_arr[255];//0=alles aus, 1 ist nur das Bit in arr[0] an (je nach Kanal), bei 255 alle an
3
4
ISR(TIMER0_COMPA_vect)//~30us *255 ~ 8ms ~ 125Hz
5
{
6
staticuint_8pwm=0;//lokal and statisch
7
PORTD=pwm_arr[pwm++];//
8
if(pwm>=254)pwm=0;//!!
9
}
10
11
voidcalc_pwm()
12
{
13
uint8_tpwmpos;
14
15
for(pwmpos=0;pwmpos<255;++pwmpos)
16
{
17
uint8_tval=0,chn;
18
for(chn=0;chn<8;++chn)
19
{
20
if(Kanal[chn]>pwmpos)
21
val|=1<<chn;
22
}
23
pwm_arr[pwmpos]=val;
24
}
25
}
26
27
intmain()
28
{
29
for(;;)
30
{
31
//..mach irgendwas
32
calc_pwm();
33
}
34
}
Das pwm_arr uebrigens sieht so aus:
1.Byte 2.Byte 3.Byte | | 255.Byte
index 0 index 1 index 2 \ \ index 254
Kanal0 [1.255stel] [2.255stel] [3.255stel] [\ \54.255stel]
Kanal1 [1.255stel] [2.255stel] [3.255stel] [| |54.255stel]
Kanal2 [1.255stel] [2.255stel] [3.255stel] [| |54.255stel]
Kanal3 [1.255stel] [2.255stel] [3.255stel] [| |54.255stel]
Kanal4 [1.255stel] [2.255stel] [3.255stel] / /254.255stel]
Kanal5 [1.255stel] [2.255stel] [3.255stel]/ /[254.255stel]
Kanal6 [1.255stel] [2.255stel] [3.255stel]| |[254.255stel]
Kanal7 [1.255stel] [2.255stel] [3.255stel]| |[255.255stel]
//#####################################################
Kleine Anmerkung: man kann die PWM-Frequenz einfach erhöhen indem man
die angeschalteten Bits mehr oder minder regelmässig über das Array
verteilt.
Dabei Tauchen Begriffe wie Puls-Dichte-Modulation,Delta-Sigma bzw
1Bit-DA wandlung auf. Aus Gründen die später noch offensichtlich werden
nutze ich einfach
ein vorberechnetes Feld mit verteilten Werten. Welche und ob überhautpt
eine der o.g. Methoden darstellt weis ich gar nicht. Ich lass es erst
mal aus dem Code raus damit es übersichtlicher bleibt.
//#####################################################
Soweit so gut, aber 8Bit bringen's leider nicht. Man müsste, dachte ich
zuerst, einen der 255 PWM Positionen herausnehmen
(oder einen 256sten dazu) und die Bits nur mal kurz anschalten um eine
höhere auflösung zu bekommen.
Aber dann fiel mir ein das der IRQ ja sowieso 256 mal pro Zyklus
aufgerufen wird und wenn man bei jedem
Aufruf die Pins für einen Zeitraum der den 256sten Teil der IRQ-Zeit
vorschaltet und dann die restliche Zeit den normalen
PWM-Wert dann hat man quasi schon mal 16 Bit Auflösung!
1
//prozessor läuft mit 8MHz -> 1Zyklus=125ns
2
3
//16bit Kanalwerte
4
uint16_tKanal[8];//Kanalwert 0..6535
5
6
//vorberechnetes Feld für die Oberen 8Bit des Kanalwertes
7
uint8_tpwm_arrhi[255];//0=alles aus, 1 ist nur das Bit in arr[0] an (je nach Kanal), bei 255 alle an
8
9
//vorberechnetes Feld für die unteren 8Bit des Kanalwertes
10
uint8_tpwm_arrlo[255];//0=alles aus, 1 ist nur das Bit in arr[0] an (je nach Kanal), bei 255 alle an
11
12
13
//sollte alle 31,87us sein , geht aber auch 32
14
ISR(TIMER0_COMPA_vect)//~30us *255 ~ 8ms ~ 125Hz
15
{
16
staticuint_8pwm=0;//lokal and statisch
17
18
uint8_tvalhi=pwm_arrhi[pwm];//schon mal holen damit das ausgeben nachher nur einen Takt benötigt!!
19
20
PORTD=pwm_arrlo[pwm++];//maske für die unteren 8 Bit ausgeben
21
22
PORTD=valhi;//nach einem Takt (125ns@8MHz) den wert bis zum neuen IRQ
23
24
if(pwm>=254)pwm=0;//!!
25
}
26
27
voidcalc_pwm()
28
{
29
uint8_tpwmpos;
30
31
for(pwmpos=0;pwmpos<255;++pwmpos)
32
{
33
uint8_tvallo=0,valhi=0,chn;
34
35
for(chn=0;chn<8;++chn)
36
{
37
if((Kanal[chn]&255)>pwmpos)//schön getrennt nach Bytes
38
vallo|=1<<chn;
39
if((Kanal[chn]>>8)>pwmpos)
40
valhi|=1<<chn;
41
}
42
43
pwm_arrlo[pwmpos]=vallo;
44
pwm_arrhi[pwmpos]=valhi;
45
46
}
47
}
Ich hab die 16Bit des Kanalwertes in 2 x 8Bit Werte aufgeteilt die je
255 bytes (bei 8 Kanälen) also insgesamt
510 Bytes einnehmen!!.
Schon mal ein kleines VOILA für 16BitPWM, aber der Speicherbedarf ist
enorm und im Grunde sind (mir) die IRQs noch viel zu häufig und der
Speicher zu schade.
//#####################################################
Nun sind das ja alles nur Codefragmente die zwar 16Bit PWM beschreiben
aber immer nur mit 8 Kanälen =1Port!
Wenn man auf allen Ports eines AVR PWM ausgeben möchte (für zB eine
Handvoll RGB-LEDs) wird das Speicherproblem bald erdrückend.
//#####################################################
Nächster Ansatz:
Wenn man anstelle 2 Gruppen zu je 8Bit einfach 4 Gruppen zu 4Bit nehmen
würde käme man auf 4x15 - also 60 Bytes.
klingt schon moderater; die Berechnung verkompliziert sich etwas und der
IRQ wird anders gestaltet:
Der 16 Bit Kanalwert wird in 4 Gruppen geteilt und für jede Gruppe wird,
für einfacheren Zugriff, hinterinander ein 4Bit-Pwm im Array abgelegt
[Wert1.0] [Wert16.0] [Wert256.0] [Wert4096.0] [Wert1.1] [Wert16.1] ...
[Wert1.14] [Wert16.14] [Wert256.14] [Wert4096.14]
Alle 250µs(@16MHz) wird der IRQ aufgerufen, und zwar 15 mal, danach
gehts von vorn los.
Im IRQ werden immer 4 Werte ausgegeben:
zuerst Wert1.x für 1Takt = 62,5ns@16MHz
dann Wert16.x für 15Takte = ~1µs@16MHz
dann Wert256.x für 225Takte = ~16µs@16MHz
dann Wert4096.x bis zum neuen IRQ= ~256µs
(Die Takte und Zeiten hab ich mit delay_us experimentell eingestellt
(weil es ja immer 15 PWM-Einheiten sind, was sich doof rechnet),
wer Lust hat kanns ja mal genau ausrechnen - aber manchmal geht
probieren schneller als studieren)
-EIN CODE SAGT MEHR ALS 1000 WORTE-!?
//#####################################################
1
//prozessor läuft (jetzt) mit 16MHz -> 1Zyklus=62,5ns
uint8_tval1=*(pp++);//schon mal holen damit das ausgeben nachher nur einen Takt benötigt!!
20
21
PORTD=val0;//maske für die bits 0..3 ausgeben steht 1Takt 62,5us@16MHz
22
23
PORTD=val1;//maske für die bits 4..7 ausgeben
24
delay_us(0.9);//ca 1µs
25
26
PORTD=*(pp++);//maske für die bits 8..11 ausgeben
27
delay_us(14.8);// ca 15µs
28
29
PORTD=*(pp++);//maske für die bits 12..15 ausgeben
30
//bis zum nächsten irq , etwa 256µs
31
32
if((++pwm)==16)//nach 15 IRQs neu laden
33
{
34
pwm=0;//!!
35
pp=pwm_arr;
36
}
37
38
}
39
40
//regelmässig in Main aufrufen (oder bei Kanaländerungen)
41
voidcalc_pwm()
42
{
43
//FEHLT NOCH -- EINFÜGEN
44
uint8_tpwmpos;
45
46
for(pwmpos=0;pwmpos<15;++pwmpos)
47
{
48
uint8_tpwmmap=sidemask16[pwmpos];//ähhm: naja, der wert halt den die Kanal-bit-gruppe haben muss um an dieser stelle ein Bit zu setzen..phuu
49
50
uint8_tvalo=0,val1=0,val2=0,val3=0;//Die nach Aktiv-Zeit sortierten PORT-Bitmasken
51
52
uint8_tchn;
53
54
for(chn=0;chn<8;++chn)
55
{
56
if((kanal[chn]&0x0f)>pwmmap)//bits0..3
57
val0|=1<<chn;
58
59
if(((kanal[chn]&0x0f)>>4)>pwmmap)//bits4..7
60
val1|=1<<chn;
61
62
if(((kanal[chn]&0x0f)>>8)>pwmmap)//bits8..11
63
val2|=1<<chn;
64
65
if(((kanal[chn]&0x0f)>>4)>pwmmap)//bits12..15
66
val3|=1<<chn;
67
}
68
pwm_arr[pwmpos<<2]=val0;
69
pwm_arr[pwmpos<<2+1]=val1;
70
pwm_arr[pwmpos<<2+2]=val2;
71
pwm_arr[pwmpos<<2+3]=val3;
72
}
73
74
}
So, sieht schon ganz ordentlich aus; der IRQ ist etwas länger wird aber
nur noch alle 256µs aufgerufen.
Man könnte nun auch für mehr Ports die Daten aus anderen LookUps
ausgeben.
Calc_PWM muss übrigens nur aufgerufen werden wenn sich ein Kanalwert
geändert hat und hat ausserdem noch reichlich Optimierungspotential.
Das verteilen der Bits auf das Feld übernimmt hier Calc_pwm auch schon
mit.
//#####################################################
---BASTELECKE---
Aber erst mal noch ein Gedankenspiel: Wenn mann die Idee mit den
Bitgruppen weiterverfolgt kommt man noch zu einer anderen Variante die
hier keinesfalls verschwiegen werden soll.
1.Variante 16Bit*1 =Ein Feld mit 65536 Bytes die nacheinander ausgegeben
Werden (Purer denkfauler speicherundrechenzeitfresserluxus)
2.Variante 8Bit*2 =2Felder mit je 255 Bytes für jeweis immer 1
Zeitanteil und 255 Zeitanteile
3.Variante 4Bit*4 =4Felder mit je 16 Bytes für je 1,15,225 und 3375
Zeitanteile
4.Variante 1Bit*16 =16Felder, Je 1Byte mit je 1Takt,2Takte
4Takte,8Takte,16Takte,32Takte...?? geht das
1
//16bit Kanalwerte
2
uint16_tKanal[8];//Kanalwert 0..65535
3
4
//Kanalpwm LookUp
5
uint8_tpwm_arr[16];//byte0 ist jeweils Bit0 vom Kanalx, byte1 ist jeweils Bit1 vom Kanalx...
6
7
ISR(TIMER0_COMPA_vect)//
8
{
9
staticuint_8pwm=0;//lokal and statisch
10
staticuint8_t*pp=pwm_arr;
11
12
if(pwm++<10)//zB
13
{
14
val0=*(pp++);
15
val1=*(pp++);//schon mal holen damit das ausgeben nachher nur einen Takt benötigt!!
16
val2=*(pp++);
17
val3=*(pp++);
18
19
PORT=val0;//62,5µs@16MHz Bit0
20
21
PORT=val1;
22
PORT=val1;//125µs@16MHz Bit1
23
24
PORT=val2;
25
PORT=val2;
26
PORT=val2;
27
PORT=val2;//250µs@16MHz Bit2
28
29
PORT=val3;
30
PORT=val3;
31
PORT=val3;
32
PORT=val3;
33
PORT=val3;
34
PORT=val3;
35
PORT=val3;
36
PORT=val3;//500µs@16MHz BIT3
37
38
PORTD=pwm_arr[*(pp++)];
39
delay_us(~.9);// ca 1µs@16MHz BIT4
40
41
PORTD=pwm_arr[*(pp++)];
42
delay_us(~1.8);// ca 2µs@16MHz BIT5
43
44
PORTD=pwm_arr[*(pp++)];
45
delay_us(~3.6);// ca 4µs@16MHz BIT6
46
47
PORTD=pwm_arr[*(pp++)];
48
delay_us(~7.2);// ca 8µs@16MHz BIT7
49
50
PORTD=pwm_arr[*(pp++)];
51
delay_us(~15);// ca 16µs@16MHz BIT8
52
53
PORTD=pwm_arr[*(pp++)];//Bit 9
54
55
//
56
!-RestartTimermit32µs
57
//
58
}
59
else
60
{
61
PORTD=pwm_arr[*(pp++)];//Bit 10..15
62
//
63
!-RestartTimermit
64
62µsfürBit10
65
125usfürBit11
66
250usfürBit12
67
500usfürBit13
68
1msfürBit14
69
2msfürBit15
70
//
71
if((++pwm)==16)//nach 15 IRQs neu laden
72
{
73
pwm=0;//!!
74
pp=pwm_arr;
75
}
76
77
}
78
79
voidcalc_pwm()
80
{
81
uint8_tpwmpos;
82
83
for(pwmpos=0;pwmpos<16;++pwmpos)
84
{
85
val=0;
86
87
for(chn=0;chn<8;++chn)
88
{
89
if(kanal[chn]&(1<<pwmpos))
90
val|=1<<chn;
91
}
92
pwm_arr[pwmpos]=val;
93
}
94
}
Sehr sparsame Variante. Je nachdem wie weit man den 1.IRQ ausweiten kann
(kürzer würd ich nicht machen) könnte man noch Bit 10 oder auch 11 mit
oben
reinnehmen. Ich hab meist noch DMX zu empfangen und/oder zu senden und
da sind u.U, alle 44µs Interrupts fällig, also denk ich sind 32us ein
guter
Mittelweg.
Der Timer muss immer nachgestellt werden, ist eben etwas Tricky..
Ein Nachteil (wenn es einer ist) bei dieser Variante: man kann die Bits
nicht gleichmässig über die IRQs verteilen und die PWM-Frequenz liegt
fest.
(na gut sind hier ca 250Hz, aber wenn man mit dem Blick über eine Wand
mit LEDs huscht sieht manns evl doch |-) )
//#####################################################
Es gibt natürlich noch viel mehr Varianten z.B. für eine höhere oder
niedrigere Auflösung und dem Experimetierfreudigen sei hiermit ein
kleiner Anstoss zuteil geworden sein...;-}
//EOF ;-}
Hallo,
ist zwar schon etwas alt der Thread, aber mach hoffe ich mal nix.
Hat evtl. einer einen fertigen Code-Schnippsel, der z.B. auf einem
Atmega8 laufen würde?
Ja gut, aber da stehen einem nur 8bit zur Verfügung. Da ich momentan mit
RGB-LED's experimentiere, sind mir 8bit etwas wenig, vor allem in den
"dunkleren" Bereichen...