Zeichendisplay an Raspberry Pi – Implementierung so gut?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Hallo,

wie vor ein paar Tagen schon angekündigt habe ich versucht, ein alphanumerisches Display am Raspberry Pi einzurichten. Jetzt habe ich die Implementierung halbwegs fertig und wollte mal fragen, ob ihr da eventuell etwas rübergucken könntet? :) Es sind noch einige kleinere Fehler drin, aber im Großen und Ganzen klappt das mit dem Display.

Benutzung:

Code: Alles auswählen

databus = [1, 2, 3, 4, 5, 6, 7, 8] (da müssen die Ausgänge vom GPIO eingetragen werden)
lcd_e = 9
lcd_rs = 10
lcd_control = LcdControl(data_bus, lcd_e, lcd_rs, 16, 2)
lcd_control.write("Hallo") 

Code: Alles auswählen

class LcdControl(object):
    def __init__(self, data_bus, lcd_e, lcd_rs, columns, rows):
        self.data_bus = data_bus
        self.lcd_enable = lcd_e
        self.lcd_rs = lcd_rs
        self.columns = columns
        self.rows = rows

        if len(data_bus) == 4:
            self.four_bit = True
        else:
            self.four_bit = False

        #Konstanten
        self.PULSE = 0.00002
        self.PAUSE = 0.0001
        self.LCD_CLEAR_DISPLAY = 0x01   #0b00000001
        self.LCD_CURSOR_HOME = 0x02     #0b00000010
        self.LCD_SOFT_RESET = 0x30      #0b00110000

        #Set Function: 0b001xxxxx
        #Die Werte werden mit bitweisem ODER (|) aufaddiert.
        #Die Optionen werden dann als Maske zusätzlich gesetzt.
        self.LCD_SET_FUNCTION = 0x20    #0b00100000
        self.LCD_FUNCTION_4BIT = 0x00   #0b00000000
        self.LCD_FUNCTION_8BIT = 0x10   #0b00010000
        self.LCD_FUNCTION_1LINE = 0x00  #0b00000000
        self.LCD_FUNCTION_2LINE = 0x08  #0b00001000
        self.LCD_FUNCTION_5X7 = 0x00    #0b00000000
        self.LCD_FUNCTION_5X10 = 0x04   #0b00000100

        #Set Display: 0b00001xxx
        self.LCD_SET_DISPLAY = 0x08     #0b00001000
        self.LCD_DISPLAY_OFF = 0x00     #0b00000000
        self.LCD_DISPLAY_ON = 0x04      #0b00000100
        self.LCD_CURSOR_OFF = 0x00      #0b00000000
        self.LCD_CURSOR_ON = 0x02       #0b00000010
        self.LCD_BLINKING_OFF = 0x00    #0b00000000
        self.LCD_BLINKING_ON = 0x01     #0b00000001

        #Set Entry Mode: 0b000001xx
        self.LCD_SET_ENTRY = 0x04       #0b00000100
        self.LCD_ENTRY_DECREASE = 0x00  #0b00000000
        self.LCD_ENTRY_INCREASE = 0x02  #0b00000010
        self.LCD_ENTRY_NOSHIFT = 0x00   #0b00000000
        self.LCD_ENTRY_SHIFT = 0x01     #0b00000001

        #Set DD RAM Address: 0b1xxxxxxx
        #Zweite Zeile startet mit 40
        self.LCD_DDADR = 0x80           #0b10000000
        self.LCD_DD_ROWS = [0x00, 0x40]
        self.LCD_CGADR = 0x40           #0b01000000

        self._initialize_lcd()

    def _initialize_lcd(self):
        self._set_data_bus_low()

        for i in range(3):
            self.write_command(self.LCD_SOFT_RESET)
            time.sleep(self.PAUSE)

        self.write_command(self.LCD_SET_FUNCTION |
                            self.LCD_FUNCTION_8BIT |
                            self.LCD_FUNCTION_2LINE |
                            self.LCD_FUNCTION_5X7)
        self.write_command(self.LCD_SET_DISPLAY |
                            self.LCD_DISPLAY_ON |
                            self.LCD_CURSOR_OFF |
                            self.LCD_BLINKING_OFF)
        self.write_command(self.LCD_SET_ENTRY |
                            self.LCD_ENTRY_INCREASE |
                            self.LCD_ENTRY_NOSHIFT)
        self.write_command(self.LCD_CLEAR_DISPLAY)

    def _send_command(self, value, command):
        """Schickt einen Befehl an das LCD, wartet eine kleine Weile und setzt
        alles wieder auf Low.
        Bitte nicht direkt aufrufen, sondern write_command() bzw.
        write_character().
        value muss ein Integer zwischen 0-255 sein.
        command = True: Kommando, lcd_rs ist LOW.
        command = False: Text, lcd_rs ist HIGH,
        """
        if command:
            self._send_byte(value, True)
        else:
            self._send_byte(value, False)
        time.sleep(self.PULSE)
        self._set_data_bus_low()
        time.sleep(self.PULSE)

    def _send_enable_pulse(self):
        """Schaltet das Enable-Signal HIGH und nach einer Pause wieder LOW"""
        GPIO.output(self.lcd_enable, GPIO.HIGH)
        time.sleep(self.PULSE)
        GPIO.output(self.lcd_enable, GPIO.LOW)

    def _send_byte(self, value, command):
        """Low-Level-Implementierung um ein Byte zu senden.

        command=True: Ist ein Kommando und RS ist LOW.
        command=False: Ist kein Kommando (=Text) und RS ist HIGH.
        """

        #Wenn es ein Kommando ist, wird RS auf LOW gesetzt. Bei Text auf HIGH.
        if command:
            GPIO.output(self.lcd_rs, GPIO.LOW)
        else:
            GPIO.output(self.lcd_rs, GPIO.HIGH)

        #Logik:
        #Die Schleife zählt die Einträge im Datenbus der Reihe nach hoch.
        #Dabei wird mittels Bitverschiebung (<<) immer ein höherer Bit gezählt,
        #also 0001, 0010, 0100, 1000 usw.
        #Mittels bitweisem UND (&) wird geprüft, ob im zu sendenden Wert (value)
        #genau dieses Bit vorkommt. Wenn das Wahr ist, wird der Ausgang auf HIGH
        #geschaltet.

        for i, pin_number in enumerate(self.data_bus):
            if value & (1 << i):
                GPIO.output(pin_number, GPIO.HIGH)

        #Wenn die Ausgänge fertig geschaltet sind muss man einmal kurz einen
        #Enable-Puls senden, damit der Controller die Daten übernimmt.
        self._send_enable_pulse()

    def _set_data_bus_low(self):
        """Setzt alle Datenkanäle auf Low."""
        for channel in self.data_bus:
            GPIO.output(channel, GPIO.LOW)
        GPIO.output(self.lcd_rs, GPIO.LOW)
        GPIO.output(self.lcd_enable, GPIO.LOW)

    def clear_lcd(self):
        self.write_command(self.LCD_CLEAR_DISPLAY)

    def generate_char(self, position, character_glyph):
        self._send_command(self.LCD_CGADR | position)
        for character_line in character_glyph:
            self.write_character(character_line)

    def return_home(self):
        self.write_command(self.LCD_CURSOR_HOME)

    def set_cursor(self, row, column):
        self.write_command(self.LCD_DDADR | self.LCD_DD_ROWS[row] | column)

    def write_command(self, value):
        """Schreibt ein Kommando ans LCD"""
        self._send_command(value, True)

    def write_character(self, value):
        """Schreibt genau einen Buchstaben ans LCD"""
        self._send_command(value, False)

    def write(self, text):
        """Schreibt einen String ans LCD"""
        self.clear_lcd()
        self.return_home()
        for i, character in enumerate(text.encode("ascii")):
            self.write_character(character)
            if i == self.columns - 1:
                self.set_cursor(1, 0)
Allerdings hätte ich noch eine Frage: In Zeile 161 nutze ich ja ein text.encode("ascii"). Da ich gerne die anderen Zeichen (unter anderem die Umlaute) auch nutzen würde, der Zeichensatz des Displays aber wohl nichts „offizielles“ ist (HD44780) würde ich da gerne einen eigenen Codec schreiben. Geht das irgendwie, so dass ich dann text.encode("mein_eigener_codec") schreiben kann?

Danke :)
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Hellstorm hat geschrieben:Da ich gerne die anderen Zeichen (unter anderem die Umlaute) auch nutzen würde, der Zeichensatz des Displays aber wohl nichts „offizielles“ ist (HD44780) würde ich da gerne einen eigenen Codec schreiben. Geht das irgendwie, so dass ich dann text.encode("mein_eigener_codec") schreiben kann?
Ja, das geht mit codecs.register().
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Hm, ich habe mir jetzt mal die iso8859_1.py kopiert, umbenannt und in der getregentry()-Funktion den Namen zu hd55780 geändert.

Wenn ich das jetzt allerdings importiere, habe ich folgendes Problem:

Code: Alles auswählen

In [1]: import hd55780, codecs                                             
                                                                           
In [2]: codecs.register(hd                                                 
hd55780            hd55780.py         hd55780\ cp1252.py                   
                                                                           
In [2]: codecs.register(hd55780.getregentry)                               
                                                                           
In [3]: "ä".encode("hd55780")                                              
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-5e67858104b7> in <module>()                               
----> 1 "ä".encode("hd55780")                                              
                                                                           
TypeError: getregentry() takes 0 positional arguments but 1 was given      
Wie genau würde ich das denn registrieren? In der Dokumentation steht „Search functions are expected to take one argument, the encoding name in all lower case letters, and return a CodecInfo object having the following attributes:“. Dieses getregentry() scheint ja eine Search function zu sein, da die ein CodecInfo-Objekt zurückgibt. Allerdings nimmt die keine Argumente auf :K

Oder muss ich da erst so eine Funktion schreiben?

Code: Alles auswählen

def hd55780_name(name):                     
    if name == "hd55780":               
        return hd55780.getregentry()
Dann würde es gehen. Davon verstehe ich aber den Sinn nicht.
BlackJack

@Hellstorm: Zum Quelltext: Die ganzen Konstanten würde ich auf die Klasse verschieben. Macht ja keinen Sinn die bei jedem Exemplar aufs neue zu setzen mit immer den gleichen Werten.

`lcd_enable` würde ich in der Argumentliste von `__init__()` nicht abkürzen.

Das `rows`-Attribut scheint nirgends verwendet zu werden‽

Wenn sich der ``if``- und der ``else``-Zweig nur durch ein literales `True` und `False` unterscheiden, dann braucht man kein ``if``/``else`` sondern kann direkt das Ergebnis des Vergleichs verwenden:

Code: Alles auswählen

        if len(data_bus) == 4:
            self.four_bit = True
        else:
            self.four_bit = False

        # =>

        self.four_bit = len(data_bus) == 4
Wobei auch dieses Attribut dann im weiteren Verlauf nicht verwendet wird.

Das mit dem ``if``/``else`` findet man dann noch einmal bei `_send_command()`, wo man das `command`-Argument direkt verwenden kann.

Bei der Methode sind die Namen verwirrend. Sie heisst `_send_command()`, sendet aber nur dann Kommandos wenn `command` wahr ist. Wobei da der Name auch wieder verwirrend ist, denn bei einer Methode die Kommandos sendet, würden die meisten bei dem Namen `command` wohl erwarten, dass es sich bei dem Argument um das Kommando selbst handelt. Ich würde die Methode `_send()` nennen und das Argument `is_command`.

Bei `_send_byte()` gibt es wieder ein ``if``/``else`` was man sich wahrscheinlich sparen kann, wenn man sich mal die Werte von `GPIO.LOW` und `GPIO.HIGH` anschaut. Das Argument würde ich auch hier `is_command` nennen.

In `generate_char()` wird die Sendemethode mit einem Argument zu wenig aufgerufen!
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

@Blackjack:

Danke für die Korrekturen. Einiges ist noch Work in Progress, z.B. habe ich die 4-Bit-Ansteuerung noch nicht eingebaut. Rows braucht man für die neue Zeile, aber bis jetzt ist nur ein 2-Zeilen-Display implementiert :D

Was genau meinst du mit „auf die Klasse verschieben“? Dass man das ganze nicht in __init__ schreibt sondern direkt nach class LcdControl?

Danke für die kleinen Tipps, ich korrigiere das dann mal.
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Hellstorm: Aufgrund der Vielzahl an Konstanten könntest du diese in einen eigenen Namensraum stecken und sie gesondert initialisieren. Das ist aber wohl mehr eine Stilfrage und IMHO kein grober Fehlentwurf. Zudem könnten die Konstanten prinzipiell auch Klassenattribute anstatt Instanzattribute sein, denn sie ändern sich ja per Definition ohnehin nicht. Außerdem müsste `LCD_DD_ROWS` streng genommen ein Tupel anstatt eine Liste sein, um wirklich konstant zu sein.

EDIT: Geschrieben bevor ich BlackJacks Beitrag sah.
BlackJack

@Hellstorm: Zu den Konstanten:

Code: Alles auswählen

class Parrot(object):
    
    def __init__(self):
        self.ANSWER = 42

# =>

class Parrot(object):
    
    ANSWER = 42

    def __init__(self):
        pass
Antworten