Go Down

Topic: LCD 1602 (and similar) databus sniffer (Read 128 times) previous topic - next topic

6v6gt

Sep 24, 2020, 01:18 pm Last Edit: Sep 25, 2020, 04:49 pm by 6v6gt Reason: Added files
This is a basic sketch to demonstrate sniffing the data bus of an LCD 1602 display to interpret the contents of the display. This could be useful if, for instance, you are using a 3rd party product for which you have no source code and which incorporates one of these displays, and you need to extract some information from it.

Actually, I started this project as an experiment to get familiar with a new logic analyser and have no current plans to develop this further. I've, however, seen a couple of cases where such a tool could be useful so , maybe, this helps someone.

The first thing to say is that this is a basic sketch and demonstrates the main points of capturing and interpreting the data. However, the chip family on which these displays are based (Hitachi HD44780) offers quite a rich feature set, including scrolling support, custom character definition  etc. Depending on how the developer has used these features, and which library he has used, it may be quite challenging to interpret the data. In the worst case, it would be necessary to more or less emulate the functionality of the entire chip. Fortunately, you are more likely to encounter a simpler use case.

The HD44780 data sheet is attached as are the demonstration sketches.

The demonstration environment is extremely simple. It consists of two main parts. Part 1 is an Arduino Nano connected to a 5 volt LCD 1602 with an I2C backpack. It simply writes to the LCD 1602 to emulate the behaviour of, say, a third party application. I used an I2C backpack for simplicity of wiring. The I2C protocol is otherwise irrelevant here. The second part consists of an Arduino Uno connected to the data bus and control pins of the LCD 1602 (seven pins marked RS, RW, E, D7, D6, D5, D4 plus a common ground). This second part runs the sniffer sketch. That is it.

Before proceeding with the details of the sniffer sketch, I'm presenting a picture from the logic analyser showing the status of the pins under some test conditions. The first part of the data stream is the initialisation of the LCD 1602 and then the first message "hello world!" . The initialisation, mainly forcing the display into 4 bit mode and cleaning up is interesting but you don't have to understand it.



See post #2 below for an improved picture with protocol specific analysis data


Reading example: The seven main pin of the LCD 1602 are represented by channels on the left margin (E, RS, RW, D7 etc.). A simultaneous trace for each channel is shown. The enable pin of the LCD 1602 is pin 'E'. The falling edge of the enable signal indicates that the data on the remaining channels is valid. So, if we take, say, the second visible pulse on channel E ( the one under the 'm' of + 20 ms on the header) and look vertically downwards, we see that RS is low (0), as is RW, D7, and D6. However, both D5 and D4 are high (1). Since the display is, in this case, driven in 4 bit mode, the LCD pins  D3, D2, D1 and D0 have no connections to them. The display still needs this data, however, so the whole bus is loaded twice. That is, you see two pulses of E, once for the four high order and once of the low four low order bits. The trick in the sniffer program is to join these up.

The data presented in the trace matches the sample sniffer output below.

Of course, all the information appearing above is also visible from both the HD44780 data sheet and also by reversing engineering the Arduino Liquid Crystal libraries, but the diagram should help to make it clearer.

The sketch sending the test data is quite trivial. It simply does the standard initialisation, clears, the screen and writes "hello world!". Then, at one second intervals, it writes a counter to a specific part of the display. It is attached below: LCD1602_TestWrite_V0_02.ino


This is a sample of the output from the sniffer when the above mentioned test program is dumping data to the display. It has been annotated to help explain what is going on.

Code: [Select]


10:46:32.517 -> starting sniffer ...

 
//                            0xMM  0xNN   MM bit 7 is error bit , bit 1 is RS, bit 0 is R/W
//                            ----  ----   NN is LCD DB7 through to DB0

                                           // Start sequence . . .
10:46:35.873 -> control  data 0x00  0x33   // 0x03 sent twice (presented as combined)
10:46:35.873 -> control  data 0x00  0x32   // 0x03 then 0x02   ( 0x02 = return home ? )
10:46:35.873 -> control  data 0x00  0x28   // Function Set 4bit, 2 lines, 5x8 dots
10:46:35.873 -> control  data 0x00  0x0C   // Display on, cursor home, blink off
10:46:35.873 -> control  data 0x00  0x01   // Display Clear
10:46:35.873 -> control  data 0x00  0x06   // Entry mode set - cursor increment mode, no display shift ?
10:46:35.873 -> control  data 0x00  0x02   // Return Home


10:46:36.391 -> control  data 0x00  0x01   // Display Clear
10:46:36.391 -> control  data 0x02  0x68   // 'h' 0x02 (RS=1,RW=0 - write data )  0x68 = 'h'
10:46:36.391 -> control  data 0x02  0x65   // 'e'
10:46:36.391 -> control  data 0x02  0x6C   // 'l'
10:46:36.391 -> control  data 0x02  0x6C   // 'l'
10:46:36.391 -> control  data 0x02  0x6F   // 'o'
10:46:36.391 -> control  data 0x02  0x20   // ' '
10:46:36.391 -> control  data 0x02  0x77   // 'w'
10:46:36.391 -> control  data 0x02  0x6F   // 'o'
10:46:36.391 -> control  data 0x02  0x72   // 'r'
10:46:36.391 -> control  data 0x02  0x6C   // 'l'
10:46:36.391 -> control  data 0x02  0x64   // 'd'
10:46:36.391 -> control  data 0x02  0x21   // '!'

10:46:36.391 -> hello world!   // The following data item 0xC5 is not detected as a character
   // so the sniffer prints out the accumulated printable characters.

10:46:36.391 -> control  data 0x00  0xC5   // 0xC5 ( setCursor( 5, 1 ) ) Sets DDRAM address
10:46:36.391 -> control  data 0x02  0x30   // '0'
10:46:36.391 -> control  data 0x02  0x30   // '0'
10:46:36.391 -> control  data 0x02  0x30   // '0'
10:46:36.391 -> control  data 0x02  0x30   // '0'
10:46:36.438 -> control  data 0x02  0x30   // '0'


10:46:37.418 -> 00000   // again, the sniffer recognises the end of a character stream and
   // prints out the accumulated printable characters, the counter
   // value in this case.


10:46:37.418 -> control  data 0x00  0xC5
10:46:37.418 -> control  data 0x02  0x30
10:46:37.418 -> control  data 0x02  0x30
10:46:37.418 -> control  data 0x02  0x30
10:46:37.418 -> control  data 0x02  0x30
10:46:37.418 -> control  data 0x02  0x31

10:46:37.511 -> errorCount= 0   // The sniffer prints an error total every few seconds. Errors are
   // Queue overflows and indicate that the loop() processing is too slow.

10:46:38.397 -> 00001

10:46:38.397 -> control  data 0x00  0xC5
10:46:38.397 -> control  data 0x02  0x30
10:46:38.397 -> control  data 0x02  0x30
10:46:38.397 -> control  data 0x02  0x30
10:46:38.397 -> control  data 0x02  0x30
10:46:38.444 -> control  data 0x02  0x32
10:46:39.423 -> 00002

// etc.



General description of the sniffer. The sniffer sketch simply watches the enable pin, captures the data on the LCD bus, combines data from 2 consecutive reads of the bus (4 bit mode) and puts the data in a queue. The queue is read in the loop and formatted output is written to the serial console.

The queue is necessary because a synchronous operation of reading the data bus in a data burst and writing it to the serial console overwhelms the serial port, leading to data loss. The sketch can be easily adapted to handle 8 bit mode, if necessary.

I've interpreted the display characters as ASCII, however, there may be some inconsistencies between the standard ASCII table and the font table of the LCD. The sniffer sketch appears  in the attached zip file together with its queue library: LCD1602_sniffer_V0_03.zip


If you are attempting to interpret complex patterns, it may help to define an array which matches the display canvas, in this case 2 x 16, and keeping track of screen control commands such as clear and cursor movements etc. On each change to the display, or at regular intervals, check if the part of interest has changed in that iteration. Process such changes accordingly.


Edit:
1.   LCD1602_sniffer_V0_04.zip has been added to correct the issues described later in this thread. I've retained the older version LCD1602_sniffer_V0_03.zip for those who prefer a simpler version and understand its limitations.

david_prentice

Your Saleae software can decode the HD44780 code.  And display it on the display.   
From memory it might even check your initialisation sequence.

Following the I2C backpack is harder.   Common backpacks have the data pins on contiguous bits.   e.g. DB4-7 on expander P4-7
So letter 6 from 6v6gt is 0x36 will be 0x3- on the I2C hi bytes and 0x6- on the lo bytes.

The Saleae software can help you understand how the logic signals work.

David.


6v6gt

#2
Sep 24, 2020, 04:11 pm Last Edit: Sep 24, 2020, 04:13 pm by 6v6gt
Many thanks for that. The whole thing was a learning exercise and now I have learned something more. I could even use the 44780 protocol analyser retrospectively on a saved trace. I'm becoming more impressed with this logic analyser.

It is clear that the I2C side is less useful for a real sniffing task because of the variety of  chips and wiring permutations. The advantage is that there are only 2 lines to tap.


Trace with 44780 protocol analysis:


bperrybap

I'd be curious if this works with the hd44780 library with the hd44780_I2Cexp i/o class.


I'd be concerned about two things.
When using auto configuration, it probes the i2c expander chip to detect the i/o expander chip type.
And then it probes the LCD through the expander to determine the pin mapping between the LCD and the expander chip.
These probes may throw off the host to LCD nibble synchronization.
The way to ensure the sniffer is always in nibble sync would be to look at the commands being sent to the LCD and run a state machine and process the function set commands used during LCD initialization.
Those function set commands are very carefully chosen to first put the LCD into 8 bit mode, then put it into 4 bit mode.
The sequence of commands will always work no matter what state the LCD is in and work even when the host can only control the upper 4 data pins.
So when the function set command sequence completes the LCD will be 4 bit mode and the host and the LCD will always be in nibble sync.


The other thing that would be interesting is can this sniffer keep up when using the hd44780 library and hd44780_I2Cexp i/o class.
The hd44780 library is quite a bit faster than other libraries like the LiquidCrystal_I2C.
A quick test could be done by not using the auto location and auto configuration to avoid the potential issue above.
While undocumented, the hd44780_I2Cexp i/o class supports the same constructor arguments as LiquiCrystal_I2C and you can use the came init() call to initialize the LCD rather then begin().
So the changes to LCD1602_TestWrite_V0_02.ino would just be changing the header file names and the lcd object class name.

--- bill

bperrybap

#4
Sep 25, 2020, 09:06 am Last Edit: Sep 25, 2020, 09:08 am by bperrybap
Very cool project.

I'd be curious if this works with the hd44780 library with the hd44780_I2Cexp i/o class.

I'd be concerned about two things.
When using auto configuration, it probes the i2c expander chip to detect the i/o expander chip type.
And then it probes the LCD through the expander to determine the pin mapping between the LCD and the expander chip.
These probes may throw off the host to LCD nibble synchronization.
The way to ensure the sniffer is always in nibble sync would be to look at the commands being sent to the LCD and run a state machine and process the function set commands used during LCD initialization.
Those function set commands are very carefully chosen to first put the LCD into 8 bit mode, then put it into 4 bit mode.
The sequence of commands will always work no matter what state the LCD is in and work even when the host can only control the upper 4 data pins.
So when the function set command sequence completes the LCD will be 4 bit mode and the host and the LCD will always be in nibble sync.


The other thing that would be interesting is can this sniffer keep up when using the hd44780 library and hd44780_I2Cexp i/o class.
The hd44780 library is quite a bit faster than other libraries like the LiquidCrystal_I2C.
A quick test could be done by not using the auto location and auto configuration to avoid the potential issue above.
While undocumented, the hd44780_I2Cexp i/o class supports the same constructor arguments as LiquiCrystal_I2C and you can use the same init() call to initialize the LCD rather then call begin().
So the changes to LCD1602_TestWrite_V0_02.ino would just be changing the header file names and the lcd object class name.

--- bill

6v6gt

#5
Sep 25, 2020, 04:37 pm Last Edit: Sep 25, 2020, 04:38 pm by 6v6gt
Thanks. You are correct that LCD1602_sniffer_V0_03 does not respect the LCD's special initialisation sequence to force the mode from the initial unknown state, and would have exactly the synchronisation problem you described if the enable pin were toggled an odd number of times before that start up sequence. I've corrected this and also made the sniffer handle 8 bit mode, but 8 bit mode is, as yet,  untested. I'll add the new version to the OP (now LCD1602_sniffer_V0_04) but I'll leave the original version as well, because it is substantially simpler, as long as its limitations are understood.

I'll do some speed trials, as suggested, when I get a chance sometime later.

bperrybap

#6
Sep 25, 2020, 07:09 pm Last Edit: Sep 25, 2020, 07:12 pm by bperrybap
I haven't gone through the state code in full details, but my initial impression is that it could have issues.
The 4 bit/8 bit mode selection using function set commands is not about seeing or processing 3 function set commands.
While many people mistakenly think that the 3 function sets are retries or a specific 3 command sequence that the LCD detects, that isn't how it really works.
All LCD instructions are always internally processed 8 bits a time.
The only difference between 8 bit mode and 4 bit mode is how the 8 bits are constructed before processing the 8 bit instructions.
And the function set commands are to be processed whenever they are seen with no concern if the host or the LCD are in nibble sync just like any other LCD instruction.

The magic is that bit position the DL bit for 8 bit and 4 bit mode was very carefully chosen to ensure that when processing the 8 bit instructions, even if the host and the LCD are out of sync with each other, the instructions will eventually put the LCD back into 8 bit mode then into 4 bit mode.
The LCD can end up processing a garbage/unknown instruction during this process if there is a synchronization issue when the host and the LCD are in different 8/4 modes or out of nibble sync in 4 bit mode.
I have a very long description of this process in the hd44780 library file hd44780.cpp in the begin() function.
It is definitely worth a read if you want to deep dive into how the function set 4/8 bit initialization really works.

I would recommend keeping collect() very simple.
Its job should be to only que up 8 bit data transferred to the LCD. This keeps it simple as well as fast.
collect() should not be concerned about getting into nibble sync with the host.
LCD Instructions can be processed by code in loop()
You pop off a bus transfer byte to always get an 8 bit instruction to process.
You process the instructions you know how to process and toss the ones you don't.
If you see a function set instruction with DL set then you set something that tells collect() to process in 8 bit mode.
If you see a function set instruction with DL clear, then you set something that tells collect() to process in 4 bit mode and the nibble state is for the first nibble.
It really can be that simple.


Also by handling instruction processing in loop, you can spit out messages about processing the function set messages and the 8/4 bit mode changes.
The instruction processing could easily be expanded to process more instructions as needed/desired.


For development, tracking, and potentially others contributing, it could be useful to put the project into a git repository and up on
github, bitbucket, atlassian, etc...

--- bill

Go Up