Diesen Text habe ich erst 2015 aus dem alten CMS übernommen, der war 2011 noch nicht im Blog.
Ich habe noch nicht viele Mikrokontroller-Projekte gemacht, aber mehr als eines beinhaltete die Steuerung von LEDs mittels Pulsweitenmodulation (PWM). Mit einer oder mehr LEDs ist das eine Sache die den kleinen Controller schon halbwegs stresst, aber wenn der Controller neben dem Dimmen von LEDs auch noch andere mehr oder weniger komplizierte Aufgaben hat geht einem schnell das Timing in die Brüche mit denen die LEDs angesteuert werden. Ganz zu schweigen vom Programmcode, der mit jeder weiteren Aufgabe unleserlicher wird die ‚zur gleichen Zeit‘ ablaufen soll.In meinem nächsten Projekt muss ich wieder LEDs faden, daher habe ich nach einem einfacheren Weg gesucht das zu machen. Das Projekt umfasst das Lesen von Speicherkarten, Kommunikation mit einer Echtzeituhr und die Ausgabe von Text auf einem LCD-Display, also bin ich fast sicher dass ich es nicht hinkriegen würde fünf Kanäle zuverlässig zu steuern. Und so viele werden es wohl mindestens werden.
Der erste Plan war, einen fertigen Chip dafür zu benutzen. Ich habe mich umgesehen und das beste was ich fand war ein Teil von Philips (PCA-irgendwas, ich habe die Nummer vergessen), das über einen I2C-Bus gesteuert werden kann. Das Teil kann bis zu acht LEDs ansprechen, aber neben ‚an‘ und ‚aus‘ gibt es nur zwei Helligkeiten die man den Ausgängen zuweisen kann. Diese sind dann wiederum variabel, aber trotzdem kann man so nicht einen Kanal mit 20%, einen mit 50% und einen mit 80% belegen. Außerdem gibt es das Teil nur als SMD, und meine Fähigkeiten am Lötkolben reichen für die Verarbeitung so kleiner Teile leider nicht aus.
Also wuchs der Plan, einen separaten Controller für die Ansteuerung der LEDs zu nehmen, der von einer Art ‚Hauptprozessor‘ angesprochen wird — idealerweise mittels I2C-Bus, da ich ja später noch mehrere andere Geräte benutzen möchte die an den gleichen Bus kommen könnten. Also habe ich einen ATtiny2313 in mein Breadboard gesteckt, den mit einem 20MHz-Quarz getaktet, und wir haben versucht so viele LEDs wie möglich zu steuern…
Pulsweitenmodulation
Die Helligkeitssteuerung von LEDs wird üblicherweise mit Pulsweitenmodulation gemacht. Ich habe das auch schon in mehreren Projekten benutzt.
Was bisher geschah
Bis jetzt habe ich immer alle LEDs eingeschaltet die eine Helligkeit grösser als 0 annehmen sollten, abgewartet bis die erste LED ausgeschaltet werden muss, die LED ausgeschaltet, auf die nächste gewartet, und so weiter. Nach einer bestimmten Zeit sind alle LEDs aus und ich fange von vorne an.
Ich versuche mal, das an einem kleinen Bild zu verdeutlichen:
1 2 3 4 5 6 |
. . . . .| . . 1 *************************************************|************************ 2 *************************************** |************************ 3 ********* |********** 4 | 5 ***************************** |************************ |
In diesem Beispiel dauert ein kompletter Durchlauf des PWM-Zyklus 50 Zeiteinheiten. Die erste LED ist die ganze Zeit durch eingeschaltet (100%), die zweiter für 40 von 50 Zeiteinheiten (80%), die dritte für zehn (20%) und die fünfte für 30 Zeiteinheiten (60%). Die vierte LED ist aus (0%). Man sieht, dass der PWM-Zyklus nach 50 Zeiteinheiten von vorn beginnt.
Ein Nachteil dieser Technik ist, dass sie langsam ist. Und sie wird mit jedem weiteren Kanal den man ansteuern möchte langsamer. Wir haben es versucht, aber wir waren auf diese Weise nicht in der Lage mehr als fünf LEDs anzusteuern ohne dass sie sichtbar anfingen zu flackern.
Wir haben auch versucht, alle Zustände der Pulsweitenmodulation in einem Array abzulegen, so dass der Algorithmus nur noch durch das Array wandern und die jeweiligen Werte darstellen muss. Das ging aber auch nicht, weil unser Controller nicht über genügend RAM verfügt um so ein Array abzulegen.
Thomas‘ Idee
Nach einigen Tests die mehr oder weniger schlechte Ergebnisse brachten hatte Thomas eine großartige Idee wie man die PWM implementieren könnte. Sie arbeitet auch mit einem Array für alle Zustände, allerdings werden die Zustände nicht gleich lange dargestellt. Der erste Status wird nur für eine Zeiteinheit angezeigt, der zweite für zwei Einheiten, der dritte für vier, und so weiter. Auf die Weise werden die LEDs zwar während eines PWM-Zyklus‘ mehr als nur einmal an- und ausgeschaltet, aber das tut niemandem weh.
Ich versuche mich noch einmal an einem Bildchen:
1 2 3 4 5 6 7 8 9 10 |
. . . . . . | . .. . . . . |.. . . 1 * |* 2 ** | ** 3 *** |*** 4 **** | **** 5 * **** |* **** 6 ****** | ****** 7 ******* |******* 8 ******** | **** |
Hier sehen wir also eine Pulsweitenmodulation mit acht Kanälen, die bis zu 64 verschiedene Helligkeiten annehmen können. Kanal 1 wird nur für eine Zeiteinheit eingeschaltet, Kanal 2 für zwei Einheiten, und so weiter. Interessant ist der fünfte Kanal: die LED wird für eine Zeiteinheit ein-, dann für zwei Einheiten aus-, und dann wieder für vier Einheiten eingeschaltet.
Wir sehen uns mal ein komplexeres Beispiel an — in dem auch hellere LEDs vorkommen:
1 2 3 4 5 6 7 8 9 10 |
. . . . . . | . .. . . . . |.. . . 1 * *******************************|* 2 ** **************** | ** 3 ******* **************** |******* 4 *******************************| 5 * **** **************** |* **** 6 *************************************************************| ********** 7 **************************************************************|*********** 8 ************************ | **** |
Die Kanäle 1 bis 8 zeigen die Helligkeiten 33, 18, 23, 32, 21, 63, 64 und 24.
Der Vorteil dieser Technik ist, dass man sich einerseits nur eine begrenzte Anzahl von Zuständen merken muss (sechs in diesem Beispiel), und andererseits die Schleife durch die Zustände sehr einfach ist: Status n wird an die Ausgänge geschickt, dann wird für 2^(n-1) Zeiteinheiten gewartet, dann folgt der nächste Zustand.Jeder Zustand wird durch ein Bitmuster dargestellt, das im jeweiligen Schritt an die Ausgänge geschickt wird. Mit anderen Worten: eine Spalte die im obigen Bild am Anfang einer Wartezeit steht. In diesem Beispiel haben wir also sechs Zustände: 01010101, 01100110, 01110100, 11100000, 11110110 und 01101001. Der erste wird für eine Zeiteinheit dargestellt, der zweite für zwei Einheiten, der dritte für vier, und so weiter…
Ein weiterer Vorteil dieser Technik ist, dass die Anzahl der Ausgänge fast unerheblich für die Systemlast ist. Nur wenn neue Helligkeiten angefordert werden muss der Algorithmus die Zustände neu berechnen, aber das ist schnell gemacht. Mit diesem Algorithmus ist es also möglich, unterschiedliche Helligkeiten auf allen freien Pins eines Controllers anzuzeigen. Für einen ATtiny2313 heisst das, dass man 13 LEDs faden kann während man immer noch in der Lage ist mittels I2C mit anderen Geräten zu kommunizieren!
Kommunikation via I2C
I2C zu sprechen ist zwar keine Raketenwissenschaft, aber da das eine Menge mit Bit-Shifterei zu tun hat wenn man es implementieren will habe ich da eine fertige Bibliothek benutzt.
Genommen habe ich die Bibliothek von Donald R. Blake, er hat die freundlicherweise unter die GPL gestellt und sie auf avrfreaks.net veröffentlicht. Man findet das originale Posting im Thread ‚8 bit communication between AVR using TWI‚, und einige Nachträge in ‚I2C Slave on an ATtiny45‚.
Danke für die Arbeit, Donald! Und dafür dass Du eine freie Lizenz benutzt hast.
Da sein Paket nur als Attachment im Forum erhältlich zu sein scheint, und ich mir nicht sicher bin für wie lange das der Fall sein wird, habe ich es in den Tarball dieses Projektes mit aufgenommen.
Benutzung
Man sollte dieses Gerät benutzen können wie jeden anderen I2C-Slave auch:
Anschließen
Die folgenden Pins des Controllers müssen mit der Schaltung verbunden werden:
- Pin 1 – Reset – sollte über einen 10k-Widerstand an VCC angeschlossen werden
- Pin 4 und 5 – XTAL1 und XTAL2 – verbunden mit einem 20MHz-Quarz, dann mit 22p-Kondensatoren an GND
- Pin 10 – GND – Masse
- Pin 17 – SDA – I2C-Daten
- Pin 19 – SCL – I2C-Takt
- Pin 20 – VCC – 5V
Die I2C-Daten und -Taktleitungen sollten mit 4,7k-Widerständen terminiert werden um die Leitungen auf VCC hoch zu ziehen. An alle übrigen Pins können LEDs angeschlossen werden. Die Anordnung ist wie folgt:
- Pin 2 – PD0 – Kanal 0
- Pin 3 – PD1 – Kanal 1
- Pin 6 – PD2 – Kanal 2
- Pin 7 – PD3 – Kanal 3
- Pin 8 – PD4 – Kanal 4
- Pin 9 – PD5 – Kanal 5
- Pin 11 – PD6 – Kanal 6
- Pin 12 – PB0 – Kanal 7
- Pin 13 – PB1 – Kanal 8
- Pin 14 – PB2 – Kanal 9
- Pin 15 – PB3 – Kanal 10
- Pin 16 – PB4 – Kanal 11
- Pin 18 – PB6 – Kanal 12
Ansprechen
In meinen Tests habe ich einen ATmega8 mit der I2C-Bibliotkek von Peter Fleury (http://jump.to/fleury — auch an Ihn Dank für die freie Lizenz!) als I2C-Master benutzt.
Eine typische Funktion um den Dimmer anzusprechen sieht wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#define I2C_FADER 0x10 void sendi2cBytes(uint8_t address, uint8_t brightness) { // address: number of the LED to set (0..12) // brightness: value between 0 and 127 // start the communication... i2c_start_wait((I2C_FADER < < 1) + I2C_WRITE); // write a byte with the address. we want the highest bit of the // address to be 1, so the slave can be sure that this is an address. i2c_write(address | 0x80); // calculate the actual duration the LED should light up. we could do // this on the slave's side, but we assume that the device is more // flexible when it is done on the master side. uint16_t duration = (brightness + 1) * (brightness + 1) - 1; // calculate the low- and the high-byte and send it. note that we split // the duration into 7-bit-values, not 8 bit! in this way the highest // bit of the transferred bytes is always low, allowing the slave to // recognize the transmitted bytes as values, not as addresses. i2c_write(duration & 0x7f); // low byte i2c_write((duration >> 7) & 0x7f); // high byte // stop the communication... i2c_stop(); } |
Beispiele
I2C-Dimmer auf dem Testbrett
Hier sieht man alle LEDs in verschiedenen Geschwindigkeiten faden.
Der Code auf dem I2C-Master der dieses Muster erzeugt sieht in etwa wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
void sendi2cBytes(uint8_t address, uint8_t brightness) { i2c_start_wait((I2C_FADER << 1) + I2C_WRITE); i2c_write(address | 0x80); uint16_t duration = (brightness + 1) * (brightness + 1) - 1; i2c_write(duration & 0x7f); // low byte i2c_write((duration >> 7) & 0x7f); // high byte i2c_stop(); } void fadeall(void) { uint8_t i = 0; int16_t brightness[13]; int16_t speed[13]; for (i = 0; i < 13; i++) { brightness[i] = 64<<8; speed[i] = random() & 0xff + 0x40; } while (1) { for(i= 0; i < 13; i++){ sendi2cBytes(i, brightness[i]>>8); brightness[i]+= speed[i]; if(brightness[i]>>8 > 127 || brightness[i]>>8 < 0){ brightness[i]-= speed[i]; speed[i] = -speed[i]; } } timerPause(1); } } |
Sichtbare PWM
Hier sieht man das Signal einer LED, die von 0 bis 127 fadet. Leider hat mein Oldtimer-Oszilloskop im mittleren Teil Probleme korrekt zu triggern.
Dies ist der Code der auf dem I2C-Master lief:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void sendi2cBytes(uint8_t address, uint8_t brightness) { i2c_start_wait((I2C_FADER < < 1) + I2C_WRITE); i2c_write(address | 0x80); uint16_t duration = (brightness + 1) * (brightness + 1) - 1; i2c_write(duration & 0x7f); // low byte i2c_write((duration >> 7) & 0x7f); // high byte i2c_stop(); } void fade(void) { uint8_t i; for(i= 0; i < = 127; i++){ sendi2cBytes(0, i); timerPause(100); } } |
Nachteile
Bis jetzt hat das Teil in allen getesteten Situationen prima funktioniert. Alles läuft wie geplant.
Ich schätze, dass dieser Dimmer im Vergleich zu fertigen Controller-ICs etwas langsam ist. Ich kann zwar kein Flackern in den LEDs erkennen, da sie immer noch ziemlich schnell geschaltet werden (etwa einmal alle 6ms, also ein Flackern mit 166Hz — zu schnell für mich).
Danke!
Wieder mal geht mein besonderer Dank an Thomas Stegemann. Er hatte die tolle Idee für den PWM-Algorithmus, und ich bin immer wieder beeindruckt von der Geduld mit der er mir eine dermaßen kranke Sprache wie C näher bringt…
Lizenz
Dieses Projekt steht unter der GNU General Public License (GPL). Eine Kopie der GPL liegt dem Paket in der Datei License.txt bei.
Download
- i2c-dimmer_111210.tar.gz – Sourcecode und Dokumentation, 232kB