// Angesteuert wird ein Display des Typs 'Alphanumerisches LCD-Modul Gleichmann GE-C1602B-YYH-JT/R Zeichenformat 16 x 2 Zeichenhöhe 5.55 mm Gelb-Grün' // Conrad Best-Nr: '183043 - 62'. // Datenblatt: http://www.msc-ge.com/download/displays/dabla_allg/ge-c1602b-tmi-ct-r.pdf // Controller-Chip ist: ST7066U // Datenblatt: http://www.crystalfontz.com/controllers/ST7066U.pdf // Register-Verwendung: // r16: Im normalen Programmablauf, und als Anzahl der wait-Schleifen in der Unterfunktion longwait + mediumwait + shortwait // r17: Befehlsregister, verwendet werden nur die hinteren sechs Bits: 00XXXXXX // r18: Buchstabenregister für sendchar // r24: In der Unterfunktion longwait + mediumwait // r25: In der Unterfunktion longwait // Verkabelung LCD-Display: // Vss - Arduino GROUND // Vdd - Arduino 5V // V0 - Arduino D-Pin 10 (Display Kontrast per PWM steuern) / Atmel PB2 // RS - Arduino D-Pin 9 / Atmel PB1 (RS = 0 -> Jetzt kommt ein Befehl, RS = 1 -> Jetzt kommt ein Datenbyte) // R/W - Arduino D-Pin 8 / Atmel PB0 (R/W = 0 -> Aufs Display schreiben, R/W = 1 -> Vom Display lesen) // E - Arduino D-Pin 11 / Atmel PB3 (E = 1 -> ENABLE, losgehts !) // -> Tipp des Tages: Will man vom Display nichts lesen (derzeit angezeigter Text, oder Busy-Flag womit man erkennen kann ob das Display gerade arbeitet) // so kann man den R/W Anschluss vom Arduino D-Pin 8 dauerhaft auf GND verbinden und spart sich einen I/O-Pin vom Arduino ein ! // DB7 - Arduino D-Pin 2 / Atmel PD2 // DB6 - Arduino D-Pin 3 / Atmel PD3 // DB5 - Arduino D-Pin 4 / Atmel PD4 // DB4 - Arduino D-Pin 5 / Atmel PD5 // Wartezeiten für Wartefunktionen: // shortwait: r16 x 3 // mediumwait: r16 x 1023 // longwait: r16 x 326404 // Kleine Anmerkung noch am Rande, statt hier immer mit Hex-Werten für binäre Werte zu arbeiten, // kann man den Binärwert 00001000 (0x08(hex) - 8(dez)) auch direkt so kodieren: 0b00001000 // Interrupts: jmp start // Reset-Interrupt, Einsprungpunkt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt reti // Unbenutzer Interrupt start: // Interrupts global deaktivieren cli // Stapel einrichten (beim Interrupt sichert sich der Prozessor hier die Rücksprungadresse // Der Stackpointer ist ein Doppelregister SPH:SPL, er wächst nach unten, also können wir ihn auf das Ende unseres RAMs setzen // Unser Speicher ist 2k groß, genaugenommen 2303 (dezimal), zumindest ist das der Wert für 'RAMEND' im AVRStudio für den Atmega328p // 2303 ist in Hex 8FF, 100011111111 - ins High Register muss also 1000, und ins Low-Register 11111111 // Leider können wir unser Register SPH und SPL nicht direkt adressieren, wir müssen also über die sts-Anweisung (Store Register an I/O-Location) gehen ldi r16, 0x08 sts 0x5E, r16 ldi r16, 0xFF sts 0x5D, r16 // Na dann mal initialisieren ! call preinitlcd call initlcd /*****************************************************************/ /* Nun senden wir das erste Zeichen. Probieren wir mal ein T :-) */ /* Das H ist in der ASCII-Tabelle 0x48, heißt also 01001000 */ /* Um einen Buchstaben zu schicken müssen wir RS auf '1' setzen */ /* und R/W auf '0' also 10XXXX - dann kommt zuerst das erste */ /* (HIGH) Nibble vom T '0100' und danach '1000' */ /* Es sollte also gesendet werden: */ /* 1: 100100 */ /* 2: 101000 */ /*****************************************************************/ ldi r18, 'H' call sendchar ldi r18, 'e' call sendchar ldi r18, 'l' call sendchar ldi r18, 'l' call sendchar ldi r18, 'o' call sendchar ldi r18, ' ' call sendchar ldi r18, 'W' call sendchar ldi r18, 'o' call sendchar ldi r18, 'r' call sendchar ldi r18, 'l' call sendchar ldi r18, 'd' call sendchar ldi r18, ' ' call sendchar ldi r18, ':' call sendchar ldi r18, '-' call sendchar ldi r18, ')' call sendchar /*****************************************************************/ /* ENDE ZEICHEN 1 :-) */ /*****************************************************************/ // Wir sind fertig, LED anmachen als Zeichen // (Die Led ist PORTB5) // Laden des Registers PORTB (0x25) lds r16, 0x25 // Logisches OR mit 00100000 um nur das Bit 5 auf 1 zu setzen ori r16, 0x20 // Und zurückschreiben sts 0x25, r16 donothing: rjmp donothing preinitlcd: // Vor dem ersten Befehl, WARTEN, und zwar ca. 50 Millisekunden, 50000000 Nanosekunden // 50000000 Nanosekunden = 800000 Takte (3 x 326404 Takte => 979212) ldi r16, 0x10 call longwait // Unsere Arduino-Digital Pin 2+3+4+5+8+9+10+11 auf Output schalten // Pin-Entsprechungen: // 02 = PD2 // 03 = PD3 // 04 = PD4 // 05 = PD5 // 08 = PB0 // 09 = PB1 // 10 = PB2 // 11 = PB3 // Hardware-Adresse von DDRB: 0x24 // DDRB soll entsprechen: 00101111 = 0x2F // Den PIN5 setzen wir auf Output weil dort unsere LED angeschlossen ist (Digital Pin 13) ldi r16, 0x2F sts 0x24, r16 // Hardware-Adresse von DDRD: 0x2A // DDRD soll entsprechen: 00111100 = 0x3C ldi r16, 0x3C sts 0x2A, r16 // Alle unsere LCD-Pins auf LOW schalten: // (Dies gibt auch den lustigen Display-schwarz-Effekt beim Init !) // Unser Zielregister PORTB (I/O-Adresse 0x25) nach r16 laden lds r16, 0x25 // Unsere hinteren beiden Bits (PB1 + PB0) auf 0 setzen, indem wir ein AND mit 11111100 (0xFC) machen andi r16, 0xFC // Und PORTB zurückschreiben sts 0x25, r16 // PORTD (I/O-Adresse 0x2B) laden lds r16, 0x0B // Unsere bits (DB7+DB6+DB5+DB4) auf 0 setzen durch ein AND mit 00001111 (0x0F) andi r16, 0x0F // Und PORTD zurückschreiben sts 0x2B, r16 // Hier setzen wir den Display-Kontrast, dieser wird über D-Din 10 gesteuert, wir müssen diesen hier pulsen (das machen wir mit PWM). // Wegen eines Bugs im AVR-Studio setzen wir als allererstes unseren Compare-Wert ins Compare-Register ! // -> Siehe: http://support.atmel.no/knowledgebase/avrstudiohelp/mergedProjects/AVRStudio4/Html/Knownissues.htm // --> Shadow register support is missing in AVR Studio. As a consequence when operating PWM in fast- and phase correct mode, the OCR register should not be updated until TCNT is at TOP. // Wir müssen also in OCR1BH (I/O-Adresse: 0x8B) (High-Byte für Compare) 0x00 schreiben, // und in OCR1BL (/O-Adresse: 0x8A) (Low-Byte für Compare) 0x80 für 128 // Das heißt immer wenn der Timer den Wert 0x80 (128) erreicht, schaltet er um (von LOW auf HIGH oder umgekehrt) // Gemessene Volt-Werte für Compare LOW-Byte (da wir 8bit PWM haben, wird das HIGH Byte sowieso nie ziehen !): // 0x05: 0,10 V // 0x0F: 0,29 V // 0x50: 1,55 V // 0xA0: 3,08 V ldi r16, 0x00 sts 0x8B, r16 ldi r16, 0x80 sts 0x8A, r16 // PB2 wird über den Timer1 angesteuert, also setzen wir im TCCR1A (I/O-Adresse 0x80) den Wert 00100001 (0x21), // damit wird der PIN OC1B (PB2) bei Compare-Match auf LOW geschalten (wenn gerade hochgezählt wird), // und es wird auf HIGH geschalten wenn Compare Match beim runterzählen auftritt !) // -> Clear OC1A/OC1B on Compare Match when upcounting. Set OC1A/OC1B on Compare Match when downcounting. // - das letzte Bit aktiviert PWM, Phase-Correct, 8 Bit-Mode // Bit 0 ist WGM10 und aktiviert den PWM, Phase Correct, 8-bit - wenn WGM11 auf 0 ist // Bit 5 ist COM1B1 und setzt den Modus auf 'Clear OC1A/OC1B on Compare Match when upcounting. Set OC1A/OC1B on Compare Match when downcounting.' // wenn COM1B0 auf 0 steht. // Hier übrigens eine nervige Sache. I/0-Adressen bis 0x63 müssen wir mit 'out' befüllen, // wenns drüber geht müssen wir 'sts' nehmen, das kann dafür 16Bit Adressen akzeptieren ! // UPDATE: Wir befüllen nun ALLE I/O-Adressen mit sts, und lesen sie mit lds, das ist einfach durchgängiger // Leider muss man dann 0x20 zu der I/O-Adresse hinzuzählen - aber das habe ich hier überall schon gemacht ! ldi r16, 0x21 sts 0x80, r16 // Nun füllen wir r16 mit 00000010 (0x02) und setzen dies nach TCCR1B (I/O-Adresse: 0x81) // Mit dem Clock-Prescaler 010(bin) nehmen wir einen Prescaler von 8(dez), es wird also bei jedem // 8. Prozessorzyklus ein Timer-Ereignis ausgelöst // Das heißt Bit CS12 ist 0, CS11 ist 1 und CS10 ist 0 - gibt also den Prescaler von 8 ldi r16, 0x02 sts 0x81, r16 // Fertig mit preinitlcd ret // ERKLÄRUNG: // Wir haben hier also den Prescaler auf 8 gesesetzt, // Der Prozessor läuft mit 16 mHz, das sind 16000 kHz. // Der Timer wird ausgelöst alle 2000 kHz und zählt eins hoch oder runter. // Die allgemeine PWM-Frequenz berechnet sich so: Ausgangsfrequenz = (Quarzfrequenz/Prescale ) /(Timerauflösung*2) // Unsere Timer-Auflösung ist 8bit (wir haben ja 'PWM, Phase-Correct, 8 Bit-Mode'), heißt also: // Ausgangsfrequenz = (16000/8) /(8*2) // Ausgangsfrequenz = 2000 / 16 = 125 kHz // Das heißt also unser Timer zählt (8-Bit) ständig von 0 bis 255 rauf und wenn er oben ist wieder runter // Immer wenn er nun an unserem Compare-Wert vorbeikommt (0x000F also 16) schaltet er den Eingang // Ist er dabei gerade beim hochzählen so schaltet er seinen PIN (OC1B / PB2) auf LOW, // Ist er beim runterzählen so schaltet er ihn auf HIGH // (-> Clear OC1A/OC1B on Compare Match when upcounting. Set OC1A/OC1B on Compare Match when downcounting.) // Netterweise kümmert sich der Timer selbst um das umschalten seines Pins - wir brauchen also keinen Interrupt ! // Das heißt wir haben bei einem Compare von 0x0F, einem Prescaler von 8, und einer Auflösung von 8bit: // Ausgangsfrequenz (eine Periode): 125 kHz - das heißt alle 8 Mikrosekunden hat der Timer von unten nach oben und zurück gezählt // Dabei wird beim überschreiten beim Hochweg von 16 auf LOW geschalten, beim Runterweg auf HIGH // Er braucht also 4 Mikrosekunden zum hochzählen, dabei sind wir ca. 0,25 Mikrosekunden auf HIGH (4/255*16), und 3,75 Mikrosekunden auf LOW // Beim runterzählen bleibt er bis zum Compare-Match auf LOW (3,75 Mikrosekunden) und geht danach auf HIGH (0,25 Mikrosekunden) // Heißt also von unserer 8 Mikrosekunden Periode sind wir 7,5 Mikrosekunden auf LOW, und 0,5 Mikrosekunden auf HIGH // Das Tastverhältniss in Prozent ist also (0,5/7,5*100): 6,66 % // Um nun (in etwa) den Volt-Wert zu berechnen gehen wir wie folgt vor: // Wir haben 5V, diese sind aber zu 93,34 % abgeschalten, und zu 6,66 % angeschalten // 6,66% von 5V ist(5/100*6,66): 0,33 Volt // Berechnung für den Compare von 0xA0 (160): // 4 Mikrosekunden pro Halbperiode, davon 2,5 Mikrosekunden auf HIGH (4/255*160), und ca. 1,5 Mikrosekunden auf LOW // Wir sind also pro Periode 5 Mikrosekunden HIGH, und 3 Mikrosekunden LOW, das heißt also wir sind zu 30% auf LOW (1,5/5*100), // Somit sind wir also zu 70% auf HIGH, die Abtastrate ist also 70% // Unsere 5V sind also nur zu 70% angeschalten, und das gibt (5/100*70): 3,5V // UPDATE 20120419: Berechnungen für den aktuellen Compare-Wert für 0x80 - 128 (dez): // 4 Mikrosekunden pro Halbperiode, davon 1,25 Mikrosekunden auf HIGH (4/255*80) und 2,75 Mikrosekunden auf LOW // Pro Periode: 2,5 Mikrosekunden HIGH, 5,5 Mikrosekunden LOW. // Das heißt wir sind zu ca. 45 / auf High, die Abtastrate ist also 45% // Unsere 5V sind also nur zu 45% angechalten, gibt: ca. 2,25 Volt // Alter Programmcode - Hier wurde der PIN PB2 simpel auf HIGH gestellt //ldi r16, 0x04 //out 0x05, r16 initlcd: // (der 'ldi r16, 0x03' und 'call longwait' ist schon im preinit gemacht worden !) ldi r17, 0b00000011 call prepcommand // Wait 4500 Microseconds ldi r16, 0x47 call mediumwait ldi r17, 0b00000011 call prepcommand // Wait 4500 Microseconds ldi r16, 0x47 call mediumwait ldi r17, 0b00000011 call prepcommand // Wait 150 Microseconds ldi r16, 0x03 call mediumwait // Activate 4Bit ldi r17, 0b00000010 call prepcommand // Function Set ldi r17, 0b00000010 call prepcommand ldi r17, 0b00001000 call prepcommand // Display-Control ldi r17, 0b00000000 call prepcommand ldi r17, 0b00001100 call prepcommand // Displayclear ldi r17, 0b00000000 call prepcommand ldi r17, 0b00000001 call prepcommand // Entrymode ldi r17, 0b00000000 call prepcommand ldi r17, 0b00000110 call prepcommand // Und noch ein bisschen warten ldi r16, 0xFF call mediumwait ret // Sendet einen Buchstaben, der Buchstabe steht in r18, wir müssen zuerst die ersten 4 (oberen) Bits senden, // danach die nächsten 4. Also zuerst Bit 7+6+5+4 und danach Bit 3+2+1+0 - vor den 4 Bits muss in r17 immer '10' stehen, // Das steht für RS = 1 R/W = 0 sendchar: // Zuerst setzen wir r17 auf 00100000 - damit haben wir RS schonmal auf 1, und R/W auf 0 ldi r17, 0x20 // Checken Bit 7, das steht in r18 hier: 0XXXXXXX - wir nutzen dazu den Befehl // SBRC ?Skip if Bit in Register is Cleared // Dieser überspringt den nächsten Befehl wenn das Bit in r18 0 ist... einfach, ne ? sbrc r18, 0x07 // Dieses hier wird nur aufgerufen wenn das Bit 7 nicht 0 (also 1) war ! // Logisches OR mit 00001000 um das Bit 3 auf eins zu setzen ori r17, 0x08 // Bit 6 sbrc r18, 0x06 ori r17, 0x04 // Bit 5 sbrc r18, 0x05 ori r17, 0x02 // Bit 4 sbrc r18, 0x04 ori r17, 0x01 // Und senden ! call prepcommand // Nun das gleiche Spielchen für Bit 3+2+1+0 des Buchstabens ldi r17, 0x20 // Bit 3 sbrc r18, 0x03 ori r17, 0x08 // Bit 2 sbrc r18, 0x02 ori r17, 0x04 // Bit 1 sbrc r18, 0x01 ori r17, 0x02 // Bit 0 sbrc r18, 0x00 ori r17, 0x01 // Und senden call prepcommand // Buchstabe gesendet, Ende ret // Sendet einen Befehl an das LCD, der Befehl kommt aus den letzten 6 Bits des Registers r17 // Bit-Nummerierung: 76543210 // Pin-Belegungen: // ATMEL / ARDUINO / LCD-Display / Bit im r17 Register // PB1 / D-Pin 9 / RS / 5 // PB0 / D-Pin 8 / R/W / 4 // PD2 / D-Pin 2 / DB7 / 3 // PD3 / D-Pin 3 / DB6 / 2 // PD4 / D-Pin 4 / DB5 / 1 // PD5 / D-Pin 5 / DB4 / 0 prepcommand: // Unser Zielregister PORTB (I/O-Adresse 0x25) nach r16 laden lds r16, 0x25 // Unsere hinteren beiden Bits (PB1 + PB0) auf 0 setzen, indem wir ein AND mit 11111100 (0xFC) machen andi r16, 0xFC // Checken vom RS-Bit, das steht in r17 hier (das 5. Bit !): XX0XXXXX - wir nutzen dazu den Befehl // SBRC ?Skip if Bit in Register is Cleared // Dieser überspringt den nächsten Befehl (das setzen von PB1 auf 1 im Register r16) // Wenn das Bit in r17 0 ist... einfach, ne ? sbrc r17, 0x05 // Dieses hier wird nur aufgerufen wenn das Bit 5 nicht 0 (also 1) war ! // Logisches OR mit 00000010 um das PB1 auf eins zu setzen ori r16, 0x02 // Weiter mit dem R/W: sbrc r17, 0x04 ori r16, 0x01 // Und PORTB zurückschreiben sts 0x25, r16 // PORTD (I/O-Adresse 0x2B) laden lds r16, 0x0B // Unsere bits (DB7+DB6+DB5+DB4) auf 0 setzen durch ein AND mit 00001111 (0x0F) andi r16, 0x0F // Checken der Bits sbrc r17, 0x03 ori r16, 0x04 sbrc r17, 0x02 ori r16, 0x08 sbrc r17, 0x01 ori r16, 0x10 sbrc r17, 0x00 ori r16, 0x20 // Und PORTD zurückschreiben sts 0x2B, r16 // Tatsächlich senden call sendcommand // Und zurück ret // Lässt das LCD das (schon an den Bits anliegende) Kommando verarbeiten indem es den Enable-Pin auf '1' setzt // und dann lange genug (20,4 Millisekunden) wartet um ihn dann wieder zu deaktivieren sendcommand: // Zuerst Chip-Enable auf 0 // PORTB holen lds r16, 0x25 // Logisches AND mit 11110111 um nur den PB3 auf 0 zu setzen andi r16, 0xF7 // Und zurückschreiben sts 0x25, r16 // Etwas länger warten (6x3 Zyklen zu 62,5 Nanosekunden pro Zyklus) ldi r16, 0x06 call shortwait // Nun Chip-Enable auf 1 (PB3) // PORTB holen (0x25) lds r16, 0x25 // Logisches OR mit 00001000 um nur den PB3 auf eins zu setzen ori r16, 0x08 // Und zurückschreiben sts 0x25, r16 // Etwas länger warten (6x3 Zyklen zu 62,5 Nanosekunden pro Zyklus) ldi r16, 0x06 call shortwait // PORTB wieder holen (vielleicht hat sich r16 ja verändert beim warten ... ?) lds r16, 0x25 // Logisches AND mit 11110111 um nur den PB3 auf 0 zu setzen andi r16, 0xF7 // Und zurückschreiben sts 0x25, r16 // Länger warten ldi r16, 0x02 call mediumwait // Raus hier ret // Wartet r16 Prozessorzyklen, bei 16 Millionen Zyklen pro Sekunde (16 Mhz) sind das 62.5 Nanosekunden pro Zyklus // Gemessene Zeit für diese Funktion r16 x 3 Zyklen shortwait: swouterloop: subi r16, 0x01 cpi r16, 0x00 brne swouterloop ret // Wartet r16 x 255 Prozessorzyklen, bei 16 Millionen Zyklen pro Sekunde (16 Mhz) sind das 62.5 Nanosekunden pro Zyklus // Gemessene Zeit für diese Funktion r16 x 1023 Zyklen mediumwait: mwouterloop: ser r24 mwinnerloop: subi r24, 0x01 cpi r24, 0x00 brne mwinnerloop subi r16, 0x01 cpi r16, 0x00 brne mwouterloop ret // Wartet r16 x 255:255 Prozessorzyklen, bei 16 Millionen Zyklen pro Sekunde (16 Mhz) sind das 62.5 Nanosekunden pro Zyklus // Gemessene Zeit für diese Funktion r16 x 326404 Zyklen longwait: lwouterloop: // r16 hat sich erniedrigt, nächster Durchlauf: r24:r25 wieder auf 255:255 stellen ser r24 ser r25 lwinnerloop: // Unsere Register um eins erniedrigen (wenn es 0 wird, wird das obere Register um eins erniedrigt, das erledigt der Atmel für uns !) sbiw r24, 0x01 // Wenn wir nicht bei 0 sind, nochmal hier rein ! // Ist r25 0 ? cpi r25, 0x00 // Wenn nein, dann zurück zu innerloop brne lwinnerloop // Wenn ja dann: r16 = r16 - 1 subi r16, 0x01 // Ist r16 = 0 ? cpi r16, 0x00 // Nein ? Dann zurück zu outerloop brne lwouterloop // Wenn ja, dann zurück ret