Saturday 18 June 2016

Snakes and displays part I: Communication with I2C

Technology grows old so fast nowadays so it's really hard to keep up. My first "game console" was outdated even before it got out for consumers to enjoy! Maybe my timing was just poor. I couldn't get my system ready to be presented in this year's E3 Expo letting all those small garage companies like Nintendo, Sony, and Microsoft steal my market share.

For the next model I surely need to spice up specs too. What is the funniest thing one can make with three LEDs? Traffic lights probably, which gives away the biggest shortcoming of my console. But what would be the best choice for graphics? 3D's popular but I believe it is nothing more but a passing fad. Like who ever talks about how they remember particulary nice polygons or shaders from their favorite games? No one.

No, the best games are all about good gameplay, fascinating storylines or memorable characters like Super Mario, Link and Sonic. And what style of graphics has more characters than ASCII graphics? That's why I ordered this 16 x 2 character display LCD 1602 from DealExtreme for less than 5 euros.

Front side of the LCD with power and data wires attached.
The display consists of two clearly separable parts: the screen and its controlling electronics, and the I2C I/O expander circuit. The last one is quite handy, as it allows to move data from to the microcontroller to the display using only two wires instead of a half fuckton of pins you can see right below the screen. With 5 pin I/O of ATtiny85 (not counting the RESET pin), that's more than welcome.

Back side of the circuit. The I2C expander is the black circuit.
Before I can use this for anything, I must first learn how to use it. The manufacturer provides the C++ library to be used with Arduino development board. I read somewhere that it might work somehow with avr-gcc as well but I decided not to use it anyway. It will be more educative to figure out from scratch how to control the display.

To ease out my task, I'll begin programming with Raspberry Pi and Python instead of AVR and C. Compared to C language, Python is much more flexible and faster to write, making it ideal for prototyping. It is an interpreted language, meaning that it doesn't require compiling but on the downside isn't generally as fast to execute. However, there are powerful and efficient libraries for pretty much everything that makes Python useful for computationally demanding applications as well. At work I use Python for measurement control, data analysis and simulations, for example. And the best part is that it is free and also very easy to learn (you can find Python tutorials for example at Codecademy).

I decided to use RPi.GPIO module to control GPIO pins. On the display, there are four pins to be connected to the Pi. I connected VCC to pin 1, GND to pin 6 and the I2C lines SCL and SDA to pins 3 and 5 respectively. There are two different numbering schemes for the Pi's GPIO pins that RPi.GPIO supports. I decided to use the physical numbering i.e. how the pins are arranged on the board, as I find it easier to use.

Physical numbering of Raspberry Pi GPIO pins [source]
The connector on the screen was a bit bent during the delivery but the screen seemed to work ok as the backlight turned on when the screen was connected.

The display lights up immediately when connected to the Pi. 
Next I'd like to write some text on the display which took me two nights to figure out how. The first night was spend on understanding how to communicate through I2C protocol. I2C or IIC (Inter-Integrated Circuit) is a two-wire serial communication protocol invented by Philips Semiconductors. There are many sources out there where I2C is described in detail (e.g. this nice tutorial), so I'll just cover here the parts I need.

In the idle state both SDA (serial data) and SCL (serial clock) lines are in HIGH state. The communication starts when the master device (Raspberry Pi) sets the SDA line down while SCL is kept HIGH. This tells the slave (I2C chip of the display) to get ready to receive data. Normally SDA shouldn't change when SCL is HIGH.

As I2C supports multiple devices on the same bus, we need a way to tell which device we want to communicate with. Therefore the first byte send after the start condition contains a 7-bit address that matches the correct device. The 8th bit is a read/write bit indicating whether the master wants to send to or receive data from the slave (HIGH for read, LOW for write). The master sets the state of the SDA line according to each bit and sends the clock pulse on SCL when the bit is ready to be read.

After the 8th bit, the master gives the control of the SDA line to the slave. If there is a slave device with the correct address, it sets the SDA line LOW, indicating that it is ready to receive or send data. If not, then SDA should stay HIGH and the master decides how to continue. This as called as the acknowledge bit (ACK). The master reads the ACK bit and sends the 9th clock pulse on SCL.
If the ACK was LOW then it is time to read or write some data. The procedure for writing is pretty much the same as with the previous step; the master sets the SDA line and informs the slave with SCL pulses. After 8 bits the control of SDA is given to the slave and the ACK bit is read. Reading the data is the same, except now the slave controls the SDA line and the master sets the ACK bit. In both cases SCL is controlled by the master, however.

This can be repeated for arbitrary number of bytes. When the last byte is written/read, the master sets the SDA line LOW. Then it sets the SCL to the HIGH state and sets the SDA line HIGH as well. Change of SDA from LOW to HIGH when SCL is HIGH tells the slave that the communication is over.

And here is how I implemented it in Python:
 import RPi.GPIO as GPIO  
   
 #Pin numbering mode  
 GPIO.setmode(GPIO.BOARD)  
   
 #pins  
 SCL = 3  
 SDA = 5  
   
 #set pins to output  
 GPIO.setup(SCL,GPIO.OUT)  
 GPIO.setup(SDA,GPIO.OUT)  
   
 #set the bus to ready state  
 GPIO.output(SDA,True)  
 GPIO.output(SCL,True)  
   
 #chip address  
 ADDRESS = [0,1,0,0,1,1,1]  
   
 def write_bytes(array_of_bytes):  
  #add read/write bit to address and add the result to array   
  array_of_bytes = [ADDRESS+[0]] + array_of_bytes  
   
  #start condition (bus should be ready)  
  GPIO.output(SDA,False)   
   
  #write bytes  
  GPIO.output(SCL,False) #Clock LOW
   
  for byte in array_of_bytes:  
   for bit in byte:  
    GPIO.output(SDA,bit)  #Set data line     
    GPIO.output(SCL,True) #Clock HIGH  
    GPIO.output(SCL,False) #Clock LOW  
      
   if not byte is array_of_bytes[-1]:   
    #if the byte is not the last one -> acknowledge  
   
    GPIO.setup(SDA,GPIO.IN) #Give SDA contol to slave     
    GPIO.output(SCL,True)  #Clock HIGH
   
    print 'ACK: ', GPIO.input(SDA) #read the ACK bit   
   
    GPIO.output(SCL,False)  #Clock LOW  
   
    GPIO.setup(SDA,GPIO.OUT) #Regain the control of SDA  
    GPIO.output(SDA,True)  
   
   else:  
    #stop condition  
    GPIO.output(SDA,False)  
    GPIO.output(SCL,True)   
    GPIO.output(SDA,True)  
   

ADDRESS = 0100111 comes partly from the datasheet of PCF8574 which converts the serial data sent through I2C to parallel pin states and vice versa. The beginning 0100 is fixed but the last three bits are set through A2...0 pins of the chip. Luckily the manufacturer was kind enough to provide the circuit diagram of the display and the expander chip. Here's its essential part: 

We see all address pins are connected to VCC, so the ending of the address is 111. Now we have everything to test the code out.

Before getting into the display itself, I first wanted to try something simple. From the datasheets and the diagram above, we can see that the pin P3 of the I2C chip controls the backlight LED of the display through a transistor. So writing something like 00000000 to the chip would turn it off and 00001000 back on. Using my I2C code it looks like this:
 write_bytes([[0,0,0,0,0,0,0,0]])  #turn backlight OFF
 time.sleep(1)                     #1 sec delay, remember to import time
 write_bytes([[0,0,0,0,1,0,0,0]])  #turn backlight ON
   
 GPIO.cleanup() #Clears the pin setup on exit 

And the light turns off and on again! I guess I would qualify as an IT support person. I didn't took video this time but I'm sure you can imagine how it looks like.

So the I2C communication is up and running. In the next post I'll take this step further and actually write something on the screen.

---

Ps. Now, when I'm writing these things down, they seem and feel straightforward but it's very misleading. There are some tedious stuff that are necessary to go through, like gathering information from datasheets and internet, but typically most of the time goes to hunting down errors and mistakes. Usually stupid ones.

Like here I first thought that I don't have to read the ACK bit, because I'll notice it anyway whether the screen works or not. I'll save a bit of effort just neglecting it. As a result I spend all night (until 4 am) trying to fix my code in vain. The next morning I started to think that maybe giving the control to the slave is actually important and I got it working in less than 10 minutes.

Lesson learned: Respect the protocol.


References:

LCD 1602 datasheet

PCF8574 datasheet

I2C on Wikipedia: https://en.wikipedia.org/wiki/I²C

Python logo https://commons.wikimedia.org/wiki/File:Python_logo_and_wordmark.svg

No comments:

Post a Comment