 |
 |
| View previous topic :: View next topic |
| Author |
Message |
Sam_40
Joined: 07 Jan 2015 Posts: 132
|
| 2X16 LCD Sniffer |
Posted: Thu May 07, 2026 4:52 pm |
|
|
Hello,
I am working on a project involving an older industrial control board that uses a standard 2x16 character LCD based on the HD44780 controller.
The issue I am facing is that the LCD is located in an inconvenient position, so I am considering removing it and instead capturing the LCD data signals and transmitting them to a remote display about 20 feet away.
The LCD interface has the typical 16-pin configuration (GND, VDD, VEE, RS, RW, E, and D0–D7, plus backlight pins A and K). All data lines D0–D7 are currently connected between the controller board and the LCD.
I have tried probing the signals with an oscilloscope, mainly focusing on RS, E, and the data lines, but it is difficult to clearly determine whether the system is operating in 4-bit or 8-bit mode. I do see valid square pulses on RS and E, but the data lines are not easy to interpret due to timing and overlap.
I also noticed if the LCD is plugged in after the system is already powered on, it displays solid blocks on the first line and nothing on the second line. However, if the system is reset with the LCD already connected, it works correctly. This makes me believe the initialization sequence is only executed at startup.
My main questions are:
What is the best practical method to reliably capture HD44780 LCD data in this type of system using PIC microcontroller?
Is there a recommended way to determine whether the interface is running in 4-bit or 8-bit mode without a logic analyzer?
For a 20-foot distance, would you recommend directly extending the LCD signals, or is it better to decode and retransmit the data using a microcontroller? I tried to use a shielded cables but will not work past 3 feet. I also prefer to get it on a PC monitor.
My goal is to reliably replicate the LCD output at a remote location without affecting the original system’s operation.
Thank you for your time and any guidance you can provide. |
|
 |
newguy
Joined: 24 Jun 2004 Posts: 1937
|
|
Posted: Fri May 08, 2026 3:32 pm |
|
|
Have a look in the code library for PCM programmer's flex LCD driver. Understand that and you'll understand everything to do with your current predicament. Probe & examine the 8 data lines, RW and E lines. Nothing else has to do with the data/driver itself.
For your remote display issue, it all starts with decoding the lines first and foremost and then tying everything back to display commands (i.e. erase screen, cursor position, etc.) and actual data to display. Again, using the driver as a starting point you'll be able to interpret the commands. Once you've worked out the interpretation, it's up to you to come up with your own bespoke comm link to relay what's happening on the main display so that it can be copied on the remote display. 20' is a good distance and I recommend you implement some form of differential signalling between main display and remote display unit. The remote will require a microcontroller and your main display will likewise require some custom PCBA with micro running some sort of 'sniff & transmit' code. I recommend differential signalling due to the noise immunity such a format affords. |
|
 |
temtronic
Joined: 01 Jul 2010 Posts: 9641 Location: Greensville,Ontario
|
|
Posted: Fri May 08, 2026 5:24 pm |
|
|
to add to Newguy's post......
Be sure to get the HD44780 data sheet ! You'll need it to decode how the LCD module is setup and the tricky part, to 'sync' the PIC with the LCD data.
for the hardware...
Be sure to use 'buffers' between the 'industrial control board' and your 'sniffer' PIC. That way you won't destroy anything.
Probably best to use 'serial' and RS-485 chips between your PICs. Easy to do and allows a PC running a terminal program to display and confirm 'data' is correct. |
|
 |
Ttelmah
Joined: 11 Mar 2010 Posts: 20087
|
|
Posted: Sat May 09, 2026 5:56 am |
|
|
I think you are missing what he wants to do!!...
He wants to extend the data cables driving an existing Hitachi style
LCD controller 20'. He is talking about two possible routes:
1) transceivers to give the extended length.
or
2) 'sniff' the LCD transactions with a PIC, and then recreate these at
the other end. Obviously PCM Programmer's driver could help with
the 'recreation', but not with any of the other stuff.
Now the big question is how noisy the environment actually 'is'. TTL
signals can even ho this far in a low noise situation, but as distance goes
up you have increasing need for protection. It is 'borderline far'.
On sniffing with a scope, you should be able to turn off 'auto' on the scope
and sync to the 'E' signal, and then see what is going on. If all eight
data lines are connected, they are probably used.
Doing a PIC based sniffer would require using a fast chip, and handling
the sampling 'in line'. not using an interrupt, since the speed could easily
be faster than the PIC can easily sample. You need to work out if the
connection to the display is a simple 'output', or bidirectional. If the
latter, it makes all approaches much harder.
Find out how many lines are actually used, whether the data is bi-directional,
and the speed being used. We may then be able to suggest a way to go. |
|
 |
temtronic
Joined: 01 Jul 2010 Posts: 9641 Location: Greensville,Ontario
|
|
Posted: Sun May 10, 2026 5:05 pm |
|
|
hmm, thinking on this again..
..it's only 20'.
74LS244 buffer should do the trick as an 'extender'. It's a fast device with good current capability.
Might be a 'one chip' solution ?? |
|
 |
Sam_40
Joined: 07 Jan 2015 Posts: 132
|
|
Posted: Sun May 10, 2026 7:34 pm |
|
|
Thank you for the reply.
I’m familiar with the PCM programmer's flex LCD driver and the 8-bit mode, as I’ve used both in the past. However, I’m not sure how they relate to my current project. The target board is running in 8-bit mode with all data pins connected. I also confirmed the 8 bit mode myself. The LCD datasheet indicates it uses the ST7066 processor, which I understand is compatible with the HD44780. I tried using a buffer, but even with an LCD at the other end, it failed because I couldn't change the direction of the busy flag, and the parallel data cable length is limited. My current plan is to use a PIC18F4685 to emulate the LCD, then send the captured data via a MAX232 to a PC terminal. Is this doable? If so, what would be the best approach?
Thanks in advance! |
|
 |
Ttelmah
Joined: 11 Mar 2010 Posts: 20087
|
|
Posted: Mon May 11, 2026 1:11 am |
|
|
Exactly.
I realised this was not really applicable for that you wanted.
The big question/problem, is whether the controller uses the R/W connection?.
If it doesn't, then as Jay says, I think the easiest way, would just be
to add a data buffer chip, and send the signals directly. I'd probably
suggest adding some trapping/suppression at the far end.
If however the unit does use the R/W connection, the problem appears
of the buffering having to be bi-directional, and the signalling will get a
lot more complex. In this case, trying to 'snoop' the LCD might be worth
trying. For this you would actually have to program the PIC to be a direct
emulation of the Hitachi controller. It'd need to support 4bit mode and 8 bit
mode (remember the controller wakes in 4 bit mode, and is then turned
up to 8 bit mode as part of the initialisation), and basically grab the
commands and data, then forward these to a second unit at the display.
This link will be relatively complex (since the data rate will need to be
quite high), so probably worth using something like RS485 at a high rate.
At this 'slave' you could then use PCM Programmer's code to actually
drive the display here.
So you need to find out is the R/W connection is used, and answer about
the noise.
Last edited by Ttelmah on Mon May 11, 2026 6:50 am; edited 1 time in total |
|
 |
Sam_40
Joined: 07 Jan 2015 Posts: 132
|
|
Posted: Mon May 11, 2026 4:30 am |
|
|
| Yes, The RW pin is used and I saw it toggling. what should be the sequence of operation in your opinion? I read both datasheets and I saw everything start at the rising edge of E, but i also noticed in the ST7066 that the data is valid from the rising edge of E to the second rising? |
|
 |
Ttelmah
Joined: 11 Mar 2010 Posts: 20087
|
|
Posted: Mon May 11, 2026 7:40 am |
|
|
The sniffer approach is going to be fairly complex. You'd need to have the
PIC give a software emulation of the display controller. Have E connect to the
interrupt input, but don't use an interrupt handler. Instead poll the
interrupt flag bit and as soon as it sets read the data. A lot will depend on
just how fast the data transfers are actually done. Ideally you would really
need to get access to a logic analyser, and see if the R/W is only used to
read from the controller, or is used to read from the display RAM. If the
latter, then I'd suggest you actually run a copy of the RAM in the PIC's
memory and return the data from this, rather than across the serial bus.
The timing of the latter might well make it too slow. You'd then have
a routine to send the display changes to the slave PIC. and have it send
these to the physical display. Some things can be ignored. for instance the
controller will send after boot the commands to switch the display to
the correct settings to handle the display, and switch the interface to
8bit. These can all be ignored, and you just handle these in the slave. |
|
 |
Sam_40
Joined: 07 Jan 2015 Posts: 132
|
|
Posted: Sat Jun 06, 2026 9:14 pm |
|
|
Hello, I have been working on this for a while now with no luck. As soon as I connect the PIC to the target board, the PIC freezes. Would you please look through my code and see if you see the issue? Thank you!
| Code: |
// =============================================================================
// PIC18F4685 HD44780 LCD EMULATOR
// =============================================================================
// This code makes the PIC18F4685 behave exactly like an HD44780 compatible
// LCD module. The target board connects directly to the PIC and thinks it is
// talking to a real LCD. The PIC captures all display data and sends it to
// an external device via UART for display.
//
// HARDWARE REQUIREMENTS:
// - PIC18F4685 powered at 3.3V to match the target board.
// - 10k resistor from MCLR (pin 1) to VDD (3.3V)
// - 100nF decoupling capacitor between VDD (pin 11) and GND (pin 12)
// - 100nF decoupling capacitor between VDD (pin 32) and GND (pin 31)
//
// TARGET BOARD TO PIC18F4685 CONNECTIONS:
// ----------------------------------------
// Target RS (Register Select) --> PIC RB0 (pin 33)
// Target RW (Read/Write) --> PIC RB1 (pin 34)
// Target E (Enable) --> PIC RB2 (pin 35)
// Target D0 (Data bit 0) --> PIC RD0 (pin 19)
// Target D1 (Data bit 1) --> PIC RD1 (pin 20)
// Target D2 (Data bit 2) --> PIC RD2 (pin 21)
// Target D3 (Data bit 3) --> PIC RD3 (pin 22)
// Target D4 (Data bit 4) --> PIC RD4 (pin 27)
// Target D5 (Data bit 5) --> PIC RD5 (pin 28)
// Target D6 (Data bit 6) --> PIC RD6 (pin 29)
// Target D7 (Data bit 7) --> PIC RD7 (pin 30) ** BIDIRECTIONAL **
// Target GND --> PIC GND (pin 12) and GND (pin 31)
//
//
// UART OUTPUT (sends LCD content to external device):
// ---------------------------------------------------
// PIC RC6 (pin 25) --> UART TX --> External device RX
// PIC RC7 (pin 26) --> UART RX --> External device TX
// Baud rate: 9600, 8N1
// Protocol: sends "LCD:row1data|row2data\r\n" when display changes
//
// D7 BIDIRECTIONAL OPERATION:
// ---------------------------
// During WRITE cycles: RD7 is INPUT - reads data from target
// During READ cycles: RD7 is OUTPUT - drives LOW to signal "not busy"
// The HD44780 busy flag is on D7. When target reads with RW=HIGH,
// it expects D7=LOW meaning "LCD is ready". We always respond LOW.
// =============================================================================
#include <18F4685.h>
// -- Oscillator and watchdog configuration -----------------------------------
#fuses INTRC_IO // Using internal oscillator
#fuses NOWDT // Watchdog timer disabled
#fuses NOLVP // Low voltage programming disabled
#fuses NOPROTECT // Code protection disabled
#fuses PUT // Power up timer enabled - ensures clean startup
// -- Clock and UART setup ----------------------------------------------------
#use delay(clock=8000000) // 8MHz internal oscillator
// UART on RC6(TX) and RC7(RX) - connects to external display device
// stream=UART allows fprintf(UART,...) syntax throughout code
#use rs232(baud=9600, xmit=PIN_C6, rcv=PIN_C7, stream=UART, ERRORS)
// ============================================================================
// PIN DEFINITIONS
// ============================================================================
// LCD control signals from target board - all inputs
#define PIN_RS PIN_B0 // Register Select: HIGH=data, LOW=command
#define PIN_RW PIN_B1 // Read/Write: HIGH=read, LOW=write
#define PIN_E PIN_B2 // Enable: data latched on falling edge
// D7 is on PORTD bit 7 - needs direction control for busy flag
// During reads we drive it LOW, during writes we read it as input
// All other PORTD pins (D0-D6) are always inputs
// Heartbeat LED - shows PIC is running
// Connect LED + 330 ohm resistor from this pin to GND
#define LED_HB PIN_A0
// ============================================================================
// HD44780 EMULATION STATE VARIABLES
// ============================================================================
// Bus mode - set by Function Set command from target
#define MODE_8BIT 0 // All 8 data pins used, one E pulse per byte
#define MODE_4BIT 1 // Only D4-D7 used, two E pulses per byte
int8 busMode = MODE_8BIT; // Default 8-bit, updated by Function Set cmd
int1 gotUpperNib = 0; // 4-bit mode: have we received upper nibble?
int8 upperNibble = 0; // 4-bit mode: stored upper nibble value
int8 upperRS = 0; // 4-bit mode: RS value of upper nibble
// DDRAM - Display Data RAM, same as real HD44780
// Row 1 addresses: 0x00 to 0x27 (stored at DDRAM index 0 to 39)
// Row 2 addresses: 0x40 to 0x67 (stored at DDRAM index 64 to 103)
// Total: 104 bytes covers full HD44780 address space
int8 DDRAM[104];
// Address counter - tracks current read/write position
int8 addrCounter = 0x00;
int1 addrIsDDRAM = 1; // 1=DDRAM addressing, 0=CGRAM addressing
int1 entryIncr = 1; // 1=increment after write, 0=decrement
// Flag to indicate display content has changed - triggers UART send
int1 lcdDirty = 0;
// ============================================================================
// HEARTBEAT VARIABLES
// ============================================================================
int16 hbCount = 0; // Counter for heartbeat timing
int1 hbState = 0; // Current LED state
// ============================================================================
// BUSY FLAG CONTROL
// Controls direction of RD7 (D7) pin
// ============================================================================
// Call this before responding to a busy flag read (RW=HIGH)
// Switches D7 to output and drives it LOW = "LCD not busy, ready"
void busy_flag_on(void) {
// 0x7F = 0111 1111: RD7=output(0), RD0-RD6=input(1)
set_tris_d(0x7F);
output_low(PIN_D7); // D7 LOW = busy flag clear = LCD ready
}
// Call this after busy flag read is complete
// Releases D7 back to input so target can drive it during writes
void busy_flag_off(void) {
set_tris_d(0xFF); // 0xFF = all PORTD pins are inputs
}
// ============================================================================
// SEND LCD CONTENT VIA UART
// Sends current display content to external device
// Format: "LCD:1234567890123456|1234567890123456\r\n"
// LCD: = identifier
// 16 chars = row 1 content (DDRAM 0x00-0x0F)
// | = row separator
// 16 chars = row 2 content (DDRAM 0x40-0x4F)
// \r\n = line ending
// ============================================================================
void send_lcd(void) {
int8 i;
int8 c;
// Send row 1 - DDRAM addresses 0x00 to 0x0F (index 0 to 15)
fprintf(UART, "LCD:");
for(i = 0; i < 16; i++) {
c = DDRAM[i];
// Only send printable ASCII characters, replace others with space
fputc((c >= 32 && c < 127) ? c : ' ', UART);
}
// Row separator
fputc('|', UART);
// Send row 2 - DDRAM addresses 0x40 to 0x4F (index 64 to 79)
for(i = 0; i < 16; i++) {
c = DDRAM[64 + i];
fputc((c >= 32 && c < 127) ? c : ' ', UART);
}
// Line ending
fprintf(UART, "\r\n");
}
// ============================================================================
// HD44780 COMMAND DECODER
// Processes all standard HD44780 commands
// Called when RS=LOW (command register selected)
// ============================================================================
void eval_command(int8 cmd) {
int8 i;
// -- Clear Display (0x01) ------------------------------------------------
// Fills all DDRAM with spaces, resets address counter to 0x00
// Execution time on real LCD: 1.52ms (we handle instantly)
if(cmd == 0x01) {
for(i = 0; i < 104; i++) DDRAM[i] = ' ';
addrCounter = 0x00;
addrIsDDRAM = 1;
entryIncr = 1;
lcdDirty = 1;
return;
}
// -- Return Home (0x02 or 0x03) ------------------------------------------
// Moves cursor to DDRAM address 0x00, display content unchanged
if((cmd & 0xFE) == 0x02) {
addrCounter = 0x00;
addrIsDDRAM = 1;
lcdDirty = 1;
return;
}
// -- Entry Mode Set (0x04 to 0x07) ---------------------------------------
// Bit 1: I/D = 1 means increment address after each write
// 0 means decrement address after each write
// Bit 0: S = display shift (we track but don't implement shifting)
if((cmd & 0xFC) == 0x04) {
entryIncr = (cmd & 0x02) ? 1 : 0;
return;
}
// -- Display ON/OFF Control (0x08 to 0x0F) -------------------------------
// Bit 2: D = display on/off
// Bit 1: C = cursor on/off
// Bit 0: B = cursor blink on/off
// We just mark dirty so external device updates
if((cmd & 0xF8) == 0x08) {
lcdDirty = 1;
return;
}
// -- Function Set (0x20 to 0x3F) -----------------------------------------
// Bit 4: DL = 1 means 8-bit bus, 0 means 4-bit bus
// Bit 3: N = number of lines (1 or 2)
// Bit 2: F = font size (5x8 or 5x11)
// This command determines how we read the data bus
if((cmd & 0xE0) == 0x20) {
if(cmd & 0x10) {
// DL=1: 8-bit mode - one E pulse per complete byte
busMode = MODE_8BIT;
} else {
// DL=0: 4-bit mode - two E pulses per byte (upper then lower nibble)
busMode = MODE_4BIT;
}
gotUpperNib = 0; // Reset nibble state on mode change
return;
}
// -- Set CGRAM Address (0x40 to 0x7F) ------------------------------------
// Switches address counter to CGRAM (custom character RAM)
// Subsequent data writes go to CGRAM until DDRAM address is set
if((cmd & 0xC0) == 0x40) {
addrCounter = cmd & 0x3F;
addrIsDDRAM = 0;
return;
}
// -- Set DDRAM Address (0x80 and above) ----------------------------------
// Moves cursor to specified DDRAM address
// Row 1: addresses 0x00-0x27
// Row 2: addresses 0x40-0x67
if(cmd & 0x80) {
addrCounter = cmd & 0x7F; // Strip bit 7 to get address
addrIsDDRAM = 1;
return;
}
}
// ============================================================================
// PROCESS ONE COMPLETE BYTE
// Called after a full byte has been received (both nibbles in 4-bit mode)
// rs=1 means data write to DDRAM/CGRAM
// rs=0 means command write to instruction register
// ============================================================================
void process_byte(int8 rs, int8 data) {
if(rs == 1) {
// -- Data write - store character in DDRAM ----------------------------
if(addrIsDDRAM) {
// Row 1: DDRAM 0x00-0x27 stored at array index 0-39
if(addrCounter <= 0x27) {
DDRAM[addrCounter] = data;
lcdDirty = 1;
}
// Row 2: DDRAM 0x40-0x67 stored at array index 64-103
else if(addrCounter >= 0x40 && addrCounter <= 0x67) {
DDRAM[addrCounter] = data;
lcdDirty = 1;
}
// Addresses outside valid range are ignored
}
// Advance address counter after each write
if(entryIncr) addrCounter++;
else addrCounter--;
} else {
// -- Command write - decode and execute -------------------------------
eval_command(data);
}
}
// ============================================================================
// MAIN PROGRAM
// ============================================================================
void main(void) {
int8 i;
int8 rs; // Register Select value read from target
int8 rw; // Read/Write value read from target
int8 data; // Data byte read from PORTD
int8 nib; // Nibble for 4-bit mode
int32 timeout; // Timeout counter for E wait loops
// -- Initialize DDRAM with spaces ----------------------------------------
for(i = 0; i < 104; i++) DDRAM[i] = ' ';
// -- Configure PORTD as inputs (data bus) --------------------------------
// All 8 data pins start as inputs
// RD7 switches to output only during busy flag reads
set_tris_d(0xFF);
// -- Configure PORTB as inputs (control signals) -------------------------
// RB0=RS, RB1=RW, RB2=E all driven by target board
// External 10k pullups on RB0, RB1, RB2 required
set_tris_b(0xFF);
// -- Disable analog inputs -----------------------------------------------
// PORTB and PORTA default to analog on PIC18F4685
// Must disable analog to use as digital inputs
setup_adc_ports(NO_ANALOGS);
// -- Configure heartbeat LED on RA0 --------------------------------------
output_low(LED_HB);
set_tris_a(0x00); // All PORTA as outputs
// -- Stabilization delay -------------------------------------------------
// Allow crystal oscillator and power supply to stabilize
delay_ms(500);
// -- Signal ready to external device via UART ----------------------------
fprintf(UART, "READY\r\n");
// =========================================================================
// MAIN POLLING LOOP
// Polls E pin continuously looking for falling edges from target board
// =========================================================================
while(1) {
// -- Heartbeat LED ---------------------------------------------------
// Toggles approximately every 0.5 seconds to show PIC is alive
// If LED stops blinking, PIC is frozen in a wait loop
hbCount++;
if(hbCount > 20000) {
hbCount = 0;
hbState = !hbState;
if(hbState) output_high(LED_HB);
else output_low(LED_HB);
}
// -- Check for E falling edge ----------------------------------------
// Target drives E LOW to start a transaction
if(!input(PIN_E)) {
// Read control signals immediately while E is LOW
// Data on PORTD is valid and stable while E is LOW
rs = input(PIN_RS); // 1=data register, 0=command register
rw = input(PIN_RW); // 1=read from LCD, 0=write to LCD
if(rw == 1) {
// ---------------------------------------------------------------
// READ CYCLE - target is reading from LCD (busy flag check)
// Target sets RW=HIGH, RS=LOW, then pulses E HIGH then LOW
// We must drive D7 LOW during the entire E HIGH period
// D7=LOW means "LCD not busy, ready for next command"
// ---------------------------------------------------------------
busy_flag_on(); // Drive D7 LOW immediately
// Wait for E to go HIGH - target samples D7 while E is HIGH
// No timeout here - we MUST hold D7 LOW until E goes HIGH
// This is required by HD44780 protocol
while(!input(PIN_E));
// E went HIGH - target has sampled our D7=LOW response
// Now wait for E to go LOW again (end of read cycle)
while(input(PIN_E));
busy_flag_off(); // Release D7 back to input
} else {
// ---------------------------------------------------------------
// WRITE CYCLE - target is writing command or data to LCD
// Target has placed data on D0-D7 and driven E LOW
// We read PORTD in one instruction - all 8 bits simultaneously
// This is the critical advantage of using PIC with PORTD
// ---------------------------------------------------------------
data = input_d(); // Read all 8 data pins in ONE instruction
// Wait for E to go HIGH - data is valid while E is LOW
// Use timeout to prevent freeze if target has wiring issue
timeout = 0;
while(!input(PIN_E)) {
timeout++;
if(timeout > 100000L) break;
}
// Process the received byte
if(busMode == MODE_8BIT) {
// 8-bit mode: one E pulse = one complete byte
process_byte(rs, data);
} else {
// 4-bit mode: two E pulses = one complete byte
// First pulse carries upper nibble on D4-D7
// Second pulse carries lower nibble on D4-D7
nib = (data >> 4) & 0x0F;
if(!gotUpperNib) {
// First nibble received - store and wait for second
upperNibble = nib;
upperRS = rs;
gotUpperNib = 1;
} else {
// Second nibble received - assemble and process full byte
data = (upperNibble << 4) | nib;
gotUpperNib = 0;
process_byte(upperRS, data);
}
}
}
// Wait for E to return HIGH before processing next transaction
// Prevents detecting same pulse twice
timeout = 0;
while(!input(PIN_E)) {
timeout++;
if(timeout > 100000L) break;
}
// Brief settling time before next sample
delay_us(5);
}
// -- Send LCD update via UART when content changes -------------------
// Only sends when lcdDirty flag is set by process_byte or eval_command
// Sending only on change keeps UART traffic minimal
if(lcdDirty) {
lcdDirty = 0;
send_lcd();
}
}
}
|
|
|
 |
Ttelmah
Joined: 11 Mar 2010 Posts: 20087
|
|
Posted: Sat Jun 06, 2026 10:58 pm |
|
|
Several things:
First you delay 500mSec at boot. The LCD must be ready in 90mSec
from POR. You may well be ready too late.
Then you have 8bit mode as the default. This is not true. All these LCD's
wake up in 4bit mode, and have to be switched to 8bit by their
initialisation code.
Then speed. The Hitachi format requires the E pulse to only be 1uSec
minimum. At 8MHz master clock, you are just not fast enough to handle
things if the main CPU is sending stuff at all quickly.
Then speed #2. You are sending a huge number of bytes for a single
arriving byte. You either need to increase the transmission speed a lot
or have a lot of transmit buffering. In fact this latter is required even
if the speed is raised. |
|
 |
Sam_40
Joined: 07 Jan 2015 Posts: 132
|
|
Posted: Sun Jun 07, 2026 5:42 pm |
|
|
Ttelmah,
Thank you for the detailed information. I added a 20MHz crystal and completed all the updates according to your guidance. However, I am still experiencing the same problem. As soon as I connect and reset both the target and the PIC (by cycling the power), the PIC freezes. Below is my updated code:
| Code: |
// =============================================================================
// PIC18F4685 HD44780 LCD EMULATOR - REVISED
// =============================================================================
// Fixes based on Ttelmah review:
// 1. Boot delay reduced to 50ms (well within 90ms LCD ready requirement)
// 2. Default mode is 4-BIT (all HD44780 controllers wake in 4-bit)
// 3. 20MHz crystal required - 8MHz too slow for 1us E pulse minimum
// 4. UART transmission buffered - never blocks during LCD activity
// CRYSTAL: 20MHz between OSC1 (pin 9) and OSC2 (pin 10)
// 18pF cap from each crystal pin to GND
//
// UART: PIC RC6 (pin 25) TX --> External RX
// PIC RC7 (pin 26) RX --> External TX
// Baud: 115200 (increased from 9600 for faster transmission)
//
// HEARTBEAT LED: RA0 (pin 2) --> 330 ohm --> LED --> GND
// =============================================================================
#include <18F4685.h>
#fuses HS, NOWDT, NOLVP, NOPROTECT, PUT
// 20MHz crystal - required for 1us E pulse timing
#use delay(clock=20000000)
// 115200 baud - much faster than 9600 to reduce transmission time
// Buffered UART output using CCS C built-in buffering
#use rs232(baud=115200, xmit=PIN_C6, rcv=PIN_C7, stream=UART, ERRORS)
// ============================================================================
// PIN DEFINITIONS
// ============================================================================
#define PIN_RS PIN_B0 // Register Select
#define PIN_RW PIN_B1 // Read/Write
#define PIN_E PIN_B2 // Enable - data latched on falling edge
#define LED_HB PIN_A0 // Heartbeat LED
// ============================================================================
// HD44780 STATE
// ============================================================================
// IMPORTANT: HD44780 always wakes up in 4-BIT mode after power on reset
// The init sequence from the target controller switches to 8-bit if needed
// We start in 4-bit and switch when we see Function Set with DL=1
#define MODE_4BIT 0
#define MODE_8BIT 1
int8 busMode = MODE_4BIT; // START IN 4-BIT - this is correct per spec
int1 gotUpperNib = 0;
int8 upperNibble = 0;
int8 upperRS = 0;
int8 addrCounter = 0x00;
int1 addrIsDDRAM = 1;
int1 entryIncr = 1;
// DDRAM - full HD44780 address space
// Row 1: 0x00-0x27 stored at index 0-39
// Row 2: 0x40-0x67 stored at index 64-103
int8 DDRAM[104];
int1 lcdDirty = 0;
// ============================================================================
// TRANSMIT BUFFER
// LCD activity must NEVER be blocked by UART transmission
// We store LCD content here and send it only when E is idle
// This is a simple double buffer - fill one while sending other
// ============================================================================
#define TX_BUF_SIZE 40 // "LCD:" + 16 + "|" + 16 + "\r\n" = 40 bytes
int8 txBuf[TX_BUF_SIZE];
int8 txLen = 0;
int8 txIdx = 0;
int1 txPending = 0; // 1 = data waiting to be sent
// Heartbeat
int16 hbCount = 0;
int1 hbState = 0;
// ============================================================================
// FUNCTION PROTOTYPES
// ============================================================================
void busy_flag_on(void);
void busy_flag_off(void);
void prepare_lcd_packet(void);
void send_pending(void);
void eval_command(int8 cmd);
void process_byte(int8 rs, int8 data);
// ============================================================================
// BUSY FLAG CONTROL
// ============================================================================
void busy_flag_on(void) {
// RD7 = output LOW = busy flag clear = LCD ready
set_tris_d(0x7F);
output_low(PIN_D7);
}
void busy_flag_off(void) {
// RD7 = input - releases bus for target writes
set_tris_d(0xFF);
}
// ============================================================================
// PREPARE LCD PACKET INTO TRANSMIT BUFFER
// Builds the LCD: packet in txBuf without any UART calls
// Actual transmission happens in send_pending() only when E is idle
// ============================================================================
void prepare_lcd_packet(void) {
int8 i;
int8 c;
int8 pos;
pos = 0;
// Header
txBuf[pos++] = 'L';
txBuf[pos++] = 'C';
txBuf[pos++] = 'D';
txBuf[pos++] = ':';
// Row 1 - DDRAM 0x00-0x0F
for(i = 0; i < 16; i++) {
c = DDRAM[i];
txBuf[pos++] = (c >= 32 && c < 127) ? c : ' ';
}
// Separator
txBuf[pos++] = '|';
// Row 2 - DDRAM 0x40-0x4F
for(i = 0; i < 16; i++) {
c = DDRAM[64 + i];
txBuf[pos++] = (c >= 32 && c < 127) ? c : ' ';
}
// Line ending
txBuf[pos++] = '\r';
txBuf[pos++] = '\n';
txLen = pos;
txIdx = 0;
txPending = 1;
}
// ============================================================================
// SEND ONE BYTE FROM TRANSMIT BUFFER
// Called from main loop ONLY when E is idle (not being driven LOW)
// Sends one byte per call to avoid blocking
// ============================================================================
void send_pending(void) {
if(!txPending) return;
if(!input(PIN_E)) return; // E is LOW - do NOT send now, LCD is active
// Send one byte from buffer
fputc(txBuf[txIdx], UART);
txIdx++;
if(txIdx >= txLen) {
// All bytes sent
txPending = 0;
txIdx = 0;
txLen = 0;
}
}
// ============================================================================
// HD44780 COMMAND DECODER
// ============================================================================
void eval_command(int8 cmd) {
int8 i;
// Clear Display (0x01)
if(cmd == 0x01) {
for(i = 0; i < 104; i++) DDRAM[i] = ' ';
addrCounter = 0x00;
addrIsDDRAM = 1;
entryIncr = 1;
lcdDirty = 1;
return;
}
// Return Home (0x02-0x03)
if((cmd & 0xFE) == 0x02) {
addrCounter = 0x00;
addrIsDDRAM = 1;
lcdDirty = 1;
return;
}
// Entry Mode Set (0x04-0x07)
// Bit1: increment/decrement address after write
if((cmd & 0xFC) == 0x04) {
entryIncr = (cmd & 0x02) ? 1 : 0;
return;
}
// Display ON/OFF (0x08-0x0F)
if((cmd & 0xF8) == 0x08) {
lcdDirty = 1;
return;
}
// Function Set (0x20-0x3F)
// Bit4 DL: 1=8-bit bus, 0=4-bit bus
// This is how the target switches us from 4-bit to 8-bit
if((cmd & 0xE0) == 0x20) {
if(cmd & 0x10) {
// DL=1: switching to 8-bit mode
busMode = MODE_8BIT;
} else {
// DL=0: staying in or switching to 4-bit mode
busMode = MODE_4BIT;
}
gotUpperNib = 0;
return;
}
// Set CGRAM Address (0x40-0x7F)
if((cmd & 0xC0) == 0x40) {
addrCounter = cmd & 0x3F;
addrIsDDRAM = 0;
return;
}
// Set DDRAM Address (0x80+)
if(cmd & 0x80) {
addrCounter = cmd & 0x7F;
addrIsDDRAM = 1;
return;
}
}
// ============================================================================
// PROCESS COMPLETE BYTE
// ============================================================================
void process_byte(int8 rs, int8 data) {
if(rs == 1) {
if(addrIsDDRAM) {
if(addrCounter <= 0x27) {
DDRAM[addrCounter] = data;
lcdDirty = 1;
} else if(addrCounter >= 0x40 && addrCounter <= 0x67) {
DDRAM[addrCounter] = data;
lcdDirty = 1;
}
}
if(entryIncr) addrCounter++;
else addrCounter--;
} else {
eval_command(data);
}
}
// ============================================================================
// MAIN
// ============================================================================
void main(void) {
int8 i;
int8 rs, rw, data, nib;
int32 timeout;
// Initialize DDRAM with spaces
for(i = 0; i < 104; i++) DDRAM[i] = ' ';
// PORTD all inputs - data bus D0-D7
set_tris_d(0xFF);
// PORTB all inputs - RS RW E from target
set_tris_b(0xFF);
// Disable analog inputs on all ports
setup_adc_ports(NO_ANALOGS);
// Heartbeat LED output
output_low(LED_HB);
set_tris_a(0x00);
// CRITICAL: Boot delay must be less than 90ms
// Real HD44780 is ready within 40ms of power on
// We must be ready before target starts sending init commands
delay_ms(50);
// Signal ready via UART
fprintf(UART, "READY\r\n");
// ==========================================================================
// MAIN LOOP
// Priority order:
// 1. E pin polling - highest priority, must never miss a pulse
// 2. Busy flag response - time critical, must respond immediately
// 3. UART transmission - lowest priority, only when E is idle
// ==========================================================================
while(1) {
// -- Heartbeat ---------------------------------------------------------
hbCount++;
if(hbCount > 20000) {
hbCount = 0;
hbState = !hbState;
if(hbState) output_high(LED_HB);
else output_low(LED_HB);
}
// -- E Pin Polling -----------------------------------------------------
if(!input(PIN_E)) {
rs = input(PIN_RS);
rw = input(PIN_RW);
if(rw == 1) {
// ------------------------------------------------------------------
// READ CYCLE - busy flag response
// Target polls this between EVERY command during init
// We MUST respond with D7=LOW before target samples it
// No timeout on this wait - protocol requires we hold until E=HIGH
// ------------------------------------------------------------------
busy_flag_on(); // Drive D7 LOW immediately
while(!input(PIN_E)); // Wait for E HIGH - target samples D7 here
while(input(PIN_E)); // Wait for E LOW - end of read cycle
busy_flag_off(); // Release D7
} else {
// ------------------------------------------------------------------
// WRITE CYCLE
// Read PORTD in one instruction - all 8 bits simultaneously
// At 20MHz this read happens within 50ns of E going LOW
// Well within HD44780 data setup/hold requirements
// ------------------------------------------------------------------
data = input_d();
// Wait for E to go HIGH - end of write cycle
timeout = 0;
while(!input(PIN_E)) {
if(++timeout > 100000L) break;
}
// Process the received data
if(busMode == MODE_8BIT) {
// 8-bit: one E pulse = one complete byte
process_byte(rs, data);
} else {
// 4-bit: two E pulses = one byte
// Upper nibble arrives first on D4-D7
nib = (data >> 4) & 0x0F;
if(!gotUpperNib) {
upperNibble = nib;
upperRS = rs;
gotUpperNib = 1;
} else {
data = (upperNibble << 4) | nib;
gotUpperNib = 0;
process_byte(upperRS, data);
}
}
}
// Wait for E to settle HIGH before next transaction
timeout = 0;
while(!input(PIN_E)) {
if(++timeout > 100000L) break;
}
delay_us(2); // Brief settling time
}
// -- Prepare UART packet when LCD content changes ----------------------
// Only prepare - actual sending happens below when E is idle
if(lcdDirty && !txPending) {
lcdDirty = 0;
prepare_lcd_packet();
}
// -- Send one byte of pending UART data --------------------------------
// send_pending() checks E is HIGH before sending each byte
// This guarantees LCD activity is never blocked by UART
send_pending();
}
}
|
|
|
 |
Ttelmah
Joined: 11 Mar 2010 Posts: 20087
|
|
Posted: Tue Jun 09, 2026 12:21 am |
|
|
OK.
There are a lot of issues with your timings. First, you read the input data
immediately you see the rising edge. A device driving an LCD does not
guarantee the data will be available then It is available at least 60nSec
before the falling edge of the E pulse, and for a few nSec after this edge
but may well not be right at this point. I'd say you need to logic sniff
the data stream from the controller and verify how soon after the rising
edge it readies this data, and only sniff after this time, or have a hardware
latch for the data (the way I'd approach it). Have a 8bit latch on the
incoming data, and clock this on the falling edge of E, and read your data
from this, not the signals directly from the controller.
Then I suspect the host _will_ expect the BF to be driven at some points.
There are commands like the clear, that specify BF will go busy for
several mSec. If the master is expecting to see this, it may well never
continue, since it thinks the display is not responding to commands.
This is commonly how controllers 'know' the LCD is connected. Part
of the spec is that busy will be held by the LCD for 10mSec on
initialisation. |
|
 |
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
Powered by phpBB © 2001, 2005 phpBB Group
|