Friday 24 June 2016

Snakes and displays part II: Writing text on the screen

Last time we learned how to talk with the I/O expander chip via I2C protocol. Now we'll see whether we can figure out how to get some text on the screen.

First we have to know how the data bits sent to the I2C chip are mapped to the pins of the LCD controller. Let's take another look at the schematic:


So, when a data byte is sent to the expander, it sets the pins P7...0 accordingly. Apparently the high bits 7...4 are mapped one-to-one to the 4 highest bits of the display's data bus. Bit 3 is controls the backlight of the screen through an NPN transistor as we concluded in the previous post. 

Bit 2 is connected to CS which is supposed to stand for Chip Select, I guess. In the datasheet the pin is called E for Enable and is used to indicate the chip that we want to read/write data to it. Bit 1 controls the RW (Read/Write) pin, indicating the direction of data, and bit 0 is RS (Register Select) which tells whether we want to access the settings of the display or the data (text) memory.

Okay but what about the low half DB3...0? According to the datasheet there are two interface modes. In the 8-bit mode the instructions and data are transferred using all the DB pins. However, it also has the 4-bit mode where the byte going through DB is split into two 4-bit parts that are consecutively pushed through DB7...4. So, an instruction consisting of RS, RW, and DB7...0 bit is converted to two bytes and sent through the I2C like this:


Pulling this all together, a hasty (or stupid, can't decide which one I am) person might think that one can control the screen by setting the E pin HIGH and sending the bytes over the I2C. Well, it turned out you can't.

I partly blame the schematic where the pin was misleadingly labelled as CS. To my best knowledge, the Chip Select pin usually works so that it makes the chip to listen incoming signals. Thus I thought it is simply enough to set it HIGH. If one thinks about this a bit further before implementing it (and wasting a couple of hours), they would have figured out why it can't work.

If my first though was the case, the chip couldn't differentiate between the proper input signal and, say, the transition between the aforementioned two bytes. There has to be a way to tell the chip that "The input is now settled. Go get it, boy!". Like with a clock pulse or something like that. The proper use of E pin is actually described in the datasheet, if I were to read it more carefully:

A plateful of timing spaghetti, s'il vous plâit!
So, in order to write on the LCD controller, I first have to set the RS and RW bits, then wait awhile, and send a pulse on the E pin that on the descending edge records the state of the data bus. Then using the write_bytes function from the previous post, the Python code to send an instruction looks like this:
 def write_instruction(cmd):  
     db74 = [cmd[2],cmd[3],cmd[4],cmd[5]]  
     db30 = [cmd[6],cmd[7],cmd[8],cmd[9]]  
     rwrs = [cmd[1],cmd[0]]  
   
     write_bytes([db74+[1,0]+rwrs, db74+[1,1]+rwrs, db74+[1,0]+rwrs])  
     write_bytes([db30+[1,0]+rwrs, db30+[1,1]+rwrs, db30+[1,0]+rwrs])
Both parts of the command are sent three times to the chip toggling the E bit. I didn't add any additional delays to the code, since controlling the GPIO with RPi.GPIO is rather slow (according to this the max frequency is 70 kHz = 14 microseconds per pulse) and the datasheet says the waiting times should be order of 10 nanoseconds.

However, the write_instruction function can't be used immediately after the power up, as the display will be in the 8-bit mode. The 4-bit mode has to be set with a separate command that in principle is 8-bit but it doesn't care the state of the unconnected pins. In addition, the datasheet describes a boot up sequence that has to be executed before the display can listen to other instructions. The function to initialize the display is
 def init_display():  
 
     #Init command  
     write_bytes([[0,0,1,1,1,0,0,0], [0,0,1,1,1,1,0,0], [0,0,1,1,1,0,0,0]])  
   
     #5 ms delay (required) 
     time.sleep(0.005)  
  
     #Init command repeated twice more according to datasheet 
     write_bytes([[0,0,1,1,1,0,0,0], [0,0,1,1,1,1,0,0], [0,0,1,1,1,0,0,0]])  
     write_bytes([[0,0,1,1,1,0,0,0], [0,0,1,1,1,1,0,0], [0,0,1,1,1,0,0,0]])  
   
     #Set 4-bit interface  
     write_bytes([[0,0,1,0,1,0,0,0], [0,0,1,0,1,1,0,0], [0,0,1,0,1,0,0,0]])  
   
With these we are finally all set! Let's test it out by writing some text on the screen. Sending an instruction 10XXXXXXXX writes a single character on the screen and increments/decrements the cursor position by 1. X:s stand for the 8-bit memory address of the character that are tabulated in the datasheet. For example A is 0100 0001 and & is 0010 0110 (these actually follow the ASCII coding, which will be useful later on). It is also possible to define own character graphics but I haven't yet tried it out. Here's my test code:
 #Init  
 init_display()  
 
 #Settings (part of the boot up sequence in the datasheet) 
 write_instruction([0,0,0,0,1,0,0,1,0,0]) #Function set (4-bit, 1-line, 5x10 dots)  
 write_instruction([0,0,0,0,0,0,1,0,0,0]) #Display Off, cursor off, blinking of cursor off  
 write_instruction([0,0,0,0,0,0,0,0,0,1]) #Clear display  
 write_instruction([0,0,0,0,0,0,0,1,1,0]) #Entry mode (left to right, don't shift display)  
   
 #set on display  
 write_instruction([0,0,0,0,0,0,1,1,0,0]) #Display on  
 
 #write text on the screen  
 write_instruction([1,0,0,0,1,1,1,0,0,0]) #8  
 write_instruction([1,0,0,0,1,0,1,1,0,1]) #-  
 write_instruction([1,0,0,1,0,0,0,0,1,0]) #B  
 write_instruction([1,0,0,1,0,0,1,0,0,1]) #I  
 write_instruction([1,0,0,1,0,1,0,1,0,0]) #T  
 write_instruction([1,0,0,0,1,0,0,0,0,0]) #  
 write_instruction([1,0,0,1,0,0,0,0,1,1]) #C  
 write_instruction([1,0,0,1,0,0,1,1,1,1]) #O  
 write_instruction([1,0,0,1,0,0,1,1,1,1]) #O  
 write_instruction([1,0,0,1,0,0,1,0,1,1]) #K  
 write_instruction([1,0,0,1,0,0,0,0,1,0]) #B  
 write_instruction([1,0,0,1,0,0,1,1,1,1]) #O  
 write_instruction([1,0,0,1,0,0,1,1,1,1]) #O  
 write_instruction([1,0,0,1,0,0,1,0,1,1]) #K  
   
 #Clear pins  
 GPIO.cleanup()  
   
and the magnificently unsurprising result:


Splendid. I also tried using the screen in the two-line mode but the contrast went really poor and turning the contrast screw didn't do a thing. I guess the screen requires more power in the two-line mode than the GPIO can provide. At least not through the 3.3V pin. With 5V volts the contrast was way better but then the I2C communication didn't work anymore (Rasp's IO pins are 3.3V in the HIGH state). I found out later that it is not necessarily a good idea to put 5V to GPIO pins anyway, so I advice not to mix the voltages and risk getting the smoke out of the GPIO bus.

References:

LCD 1602 Datasheet

No comments:

Post a Comment