Bare-metal programming with 595 and LCD display

Hi there ! i'm learning to use a LCD display, using a 595 and baremetal programming (so juste AVR registers, no arduino commands)

first of all : i do this to learn. And i know there is easier ways, and baremetal programming is not easy. so please dont tell me to do an other way. that won't help me in this case.

my code is based on two tutorial codes : one "arduino code" to drive a 595 to control an LCD

and an AVR baremetal code to drive a 595 to control LEDs.

basically the 595 do the same job, sending parallel data out of serial data. so i just added LCD print functions and tried to adapt.

But it doesn't work. i didn't find why yet. any suggestion ?

here's my code :

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
#include <stdint.h>
typedef uint8_t bits_type;
//defines IOs 
#define CFG_SHIFT_DDR DDRD
#define CFG_SHIFT_PORT PORTD
#define CFG_SHIFT_SRCLK PD3 //arduino pin 3 for Clock
#define CFG_SHIFT_RCLK PD4 //arduino pin 4 for Latch
#define CFG_SHIFT_SER PD2 //arduino pin 2 for Data
//number of pins in the shift register
#define CFG_SHIFT_REGISTER_PIN_COUNT 8

void shift_bits_init(void);
void shift_bits_out(bits_type b, size_t len);
void LCDinit();
void LCDprint(char _line, char _string[16]);

void shift_bits_init(void)
{
  CFG_SHIFT_DDR |= _BV(CFG_SHIFT_RCLK)
            | _BV(CFG_SHIFT_SRCLK)
            | _BV(CFG_SHIFT_SER);
}

void shift_bits_out(bits_type b, size_t len)
{
      CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_RCLK);
      for (size_t i = 0; i < CFG_SHIFT_REGISTER_PIN_COUNT; i++){
            CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_SRCLK);
            if(i < len && (b & ((bits_type)1 << i)))
                    CFG_SHIFT_PORT |= _BV(CFG_SHIFT_SER);
            CFG_SHIFT_PORT |= _BV(CFG_SHIFT_SRCLK);
            CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_SER);
      }
      CFG_SHIFT_PORT |= _BV(CFG_SHIFT_RCLK);
}

void LCDSend_byte(char d, char RS)
{
 	char dH, dL, temp;
  //keep data on upper nybble
  dH = d & 0xF0; 	//get MSB
  dL = d & 0x0F;
  dL = d << 4;
  //Send MSB with E=clock
  temp=0;
  temp=dH | RS | 0x02; //MSB with RS+E bit
  shift_bits_out(temp, 4);
  //Send LSB with E = clock
  temp = 0;
  temp = dL | RS | 0x02; //MSB with RS+E bit
  shift_bits_out(temp, 4);
  //send LSB with E = 0 
  temp=0;
  temp=dL | RS;
  shift_bits_out(temp, 4);  
}

void LCDinit()
{
  int count;
  char t[]={0x43,0x03,0x03,0x02,0x28,0x01,0x0C,0x06,0x02,0x02};
  for(count = 0; count <= 9; count ++)
    {
      LCDSend_byte(t[count],0);
    }

}

void LCDprint(char _line, char _string[16])
{
  int len, count;
  if(_line ==0){
    LCDSend_byte(0x80, 0); //command RS=0 move cursor to home
  }
  else{
    LCDSend_byte(0x80, 0); //command RS=0 Move cursor to 2nd line
  }
      len = strlen(_string);
      for (count = 0;count<len;count++)
      {
        LCDSend_byte(_string[count], 1); //Data RS = 1
      }
}


int main(void){
      shift_bits_init();
      LCDinit();
      while(1)
      {
        LCDprint(0,"This is a test");
        LCDprint(1,"Helloorld");
        _delay_ms(500);
      }
      return 0;
}

and here is the schematic :

Cause and effect.

Do it another way. Get it working. Then you will know if your circuit and code are correct and your components are not faulty.

After that, change your code to AVR baremetal code. But do it in small stages and test at each stage.

Analyse the sourcecode of LiqidCrystal_I2C and replace the PCF8574 parts. If this is working replace Print.h

And I haven't found a reason why you posted this in "avrdude, stk500, bootloader issues" :wink: Hence your topic has been moved to a more suitable location on the forum.

Well prepared post showing code and schematics!
Does the +5 volt come from USB?

Do you have a scope? See what happens on the output pins and whether that matches your expectations.

Also do as @PaulRB says so you can decouple hardware testing from the baremetal programming exercise.

unfortunately i don't (my dream to have one :sob: )

for now, i am using tinkercad, because i don't have 595 ICs yet.

so except simulator incoherences, there is no hardware issue possible

simply because i have seen "avr" in the title, so i thought it could be the right place

i am not used to the forum yet. sorry

Take a look at the information and the examples here.

None of the examples are implemented using a shift register but the instructions that have to be sent, the sequence of those instructions, and the timing would be the same.

Note that although you are using the four-bit LCD interface the initialization always starts out in the 8-bit mode. This means that the 'send byte' technique is not the same at the start as it is after you make the actual switch to 4-bit mode. If this doesn't make sense to you then you should reread the 'LCD Initialization' information at the above link.

Don

No worries.

yes, instead a sending informations through board pins, you send it trough the 595 input. just have to write the right code to drive it.

thank you for the documents ! I wish i could find more of these for others components

Hi, @gowbow1

Can I suggest you look at "ben eater" YouTube channel.

He has a very good series of videos on a 6502 controlled LCD, and goes thought how to communicate with the LCD like you wish to.

Tom.. :smiley: :+1: :coffee: :australia:

of course Ben Eater ! i didn't know he made such a video !

for now, i am trying the 4-bits init example, sended through the 595. i tried with the first step, no errors.

i type all the rest .. weird errors !

"bits_type was not declared in this scope" concerning my instruction function (which is shit_bits_out + delay functions)

or a big bunch of error codes concerning repertories i dont know. the error description is not the same at all depending which IDE i am using.

except i've found this : "__builtin_avr_delay_cycles expects a compile time integer constant"

and i discovered that it doesn't do that until step 4. i tried step by step, and the error happens in when i type more than 3 steps.

which is very weird !

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
#include <stdint.h>
typedef uint8_t bits_type;
//defines IOs 
#define CFG_SHIFT_DDR DDRD
#define CFG_SHIFT_PORT PORTD
#define CFG_SHIFT_SRCLK PD3 //arduino pin 3 for Clock
#define CFG_SHIFT_RCLK PD4 //arduino pin 4 for Latch
#define CFG_SHIFT_SER PD2 //arduino pin 2 for Data
//number of pins in the shift register
#define CFG_SHIFT_REGISTER_PIN_COUNT 8

void shift_bits_init(void);
void shift_bits_out(bits_type b, size_t len);
//void LCDinit();
//void LCDprint(char _line, char _string[16]);

void shift_bits_init(void)
{
  CFG_SHIFT_DDR |= _BV(CFG_SHIFT_RCLK)
            | _BV(CFG_SHIFT_SRCLK)
            | _BV(CFG_SHIFT_SER);
}

void shift_bits_out(bits_type b, size_t len)
{
      CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_RCLK);
      for (size_t i = 0; i < CFG_SHIFT_REGISTER_PIN_COUNT; i++){
            CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_SRCLK);
            if(i < len && (b & ((bits_type)1 << i)))
                    CFG_SHIFT_PORT |= _BV(CFG_SHIFT_SER);
            CFG_SHIFT_PORT |= _BV(CFG_SHIFT_SRCLK);
            CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_SER);
      }
      CFG_SHIFT_PORT |= _BV(CFG_SHIFT_RCLK);
}

void instruction(bits_type b, size_t len, float _delay)
{
    shift_bits_out(b, len);
    _delay_ms(_delay);  
}

void LCDInit()
{
    instruction(0x03, 4, 100.0); //step 1
    instruction(0x03, 4, 4.1);   //step 2
    instruction(0x03, 4, 0.1);   //step 3
    instruction(0x03, 4, 0.1);   //step 4
    instruction(0x02, 4, 0.1);   //step 5
    shift_bits_out(0x02, 4);     //step 6
    instruction(0x08, 4, 0.053); //step 6-bis
    shift_bits_out(0x00, 4);     //step 7
    instruction(0x08, 4, 0.053); //step 7-bis
    shift_bits_out(0x00, 4);     //step 8
    instruction(0x01, 4, 3); //step 8-bis
    shift_bits_out(0x00, 4);     //step 9
    instruction(0x06, 4, 0.053); //step 9-bis
    //step 10
    shift_bits_out(0x00, 4);     //step 7
    instruction(0x0C, 4, 0.053); //step 7-bis  
}

int main()
{
  shift_bits_init();
  LCDInit();
  while(1)
  {
    continue;

  }
  return 0;
}

ok i got it :

the AVR delay library takes integers constants. i wanted to put floats

:x:

void _instructionm(bits_type b, size_t len, float _delay)
{
    shift_bits_out(b, len);
    _delay_ms(_delay);  
}
void _instructionu(bits_type b, size_t len, float _delay)
{
    shift_bits_out(b, len);
    _delay_us(_delay);  
}

:heavy_check_mark:

void _instructionm(bits_type b, size_t len, int _delay)
{
    shift_bits_out(b, len);
    _delay_ms(_delay);  
}
void _instructionu(bits_type b, size_t len, int _delay)
{
    shift_bits_out(b, len);
    _delay_us(_delay);  
}

nice ! i will take a look at it.

problem is, the SPI protocol takes 4 wires to work. i2C takes only two but it doesn't appears to be the cheapest method for LCD display (or maybe other stuff)

that's why i'm trying with 7400 series circuits. only 3 wires required ! and cost less that i2c LCD display. a good compromise maybe

i've seen that it is oftenly used in applications such as digital oscilloscope

i recommend you EEVblog's recent video on the subject :wink:

1 Like

Update :

Currently, my code looks like this

....and for real, it is supposed to work ! :angry:

The print command And the init command uses the SendByte command which uses the shift register bits_out command

so when a command is ready, it is sent to the 595

i don't know where is my mistake.

because the 595 works ! i have data outputing

so it looks like the commands are not sent properly.

this is where i am lost ! i read tons of documentations but it has been more confusing than anything else lol.

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
#include <stdint.h>
typedef uint8_t bits_type;
//defines IOs 
#define CFG_SHIFT_DDR DDRD
#define CFG_SHIFT_PORT PORTD
#define CFG_SHIFT_SRCLK PD3 //arduino pin 3 for Clock
#define CFG_SHIFT_RCLK PD4 //arduino pin 4 for Latch
#define CFG_SHIFT_SER PD2 //arduino pin 2 for Data
//number of pins in the shift register
#define CFG_SHIFT_REGISTER_PIN_COUNT 8

void shift_bits_init(void);
void shift_bits_out(bits_type b, size_t len);
//void LCDinit();
//void LCDprint(char _line, char _string[16]);

void shift_bits_init(void)
{
  CFG_SHIFT_DDR |= _BV(CFG_SHIFT_RCLK)
            | _BV(CFG_SHIFT_SRCLK)
            | _BV(CFG_SHIFT_SER);
}

void shift_bits_out(bits_type b, size_t len)
{
      CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_RCLK);
      for (size_t i = 0; i < CFG_SHIFT_REGISTER_PIN_COUNT; i++){
            CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_SRCLK);
            if(i < len && (b & ((bits_type)1 << i)))
                    CFG_SHIFT_PORT |= _BV(CFG_SHIFT_SER);
            CFG_SHIFT_PORT |= _BV(CFG_SHIFT_SRCLK);
            CFG_SHIFT_PORT &= ~_BV(CFG_SHIFT_SER);
      }
      CFG_SHIFT_PORT |= _BV(CFG_SHIFT_RCLK);
}

void LCDSendByte(char d,char RS)
{
  char dH,dL,temp;
  //Keep Data on upper nybble
  dH = d & 0xF0;         //Get MSB
  dL = d & 0x0F;
  dL = d << 4;           //Get LSB
//Send MSB with E=clock  
  temp=0;
  temp=dH | RS | 0x02;  //MSB With RS+E bit
  shift_bits_out(temp, 8);
//Send MSB with E=0
  temp=0;
  temp=dH | RS;  //MSB With RS bit
  shift_bits_out(temp, 8);
//Send LSB with E=clock
  temp=0;
  temp=dL | RS | 0x02;  //MSB With RS+E bit
  shift_bits_out(temp, 8);
//Send LSB with E=0
  temp=0;
  temp=dL | RS;  //MSB With RS bit
  shift_bits_out(temp, 8);
}

void LCDInit()
{
  int count;
  char t[] = {0x02,0x28,0x01,0x0C,0x06};
  for(count = 0; count <=9; count ++)
  {
    LCDSendByte(t[count],0);  //command=0;
  }
}
void LCDPrint(char line, char string[16])
{
  int len,count;
  if(line==0){
    LCDSendByte(0x80,0); //RS = 0 Move Cursor to home
  }
  else{
    LCDSendByte(0xC0,0); //RS = 0 Move Cursor to Second Line
  }
  len = strlen(string);
  for(count=0;count<len;count++)
  {
    LCDSendByte(string[count],1);
  }
}

int main()
{
  shift_bits_init();
  LCDInit();
  _delay_ms(1000);    
  while(1)
  {
    LCDPrint(0, "Hello There");
    LCDPrint(1, "was hard !");
    _delay_ms(500);
  }
  return 0;
}

You have much to learn Grasshopper if you want to play at this level.

I can say fairly confidently that the code you have will not work.
Not only is not sending the proper initialization sequence to get the LCD into 8 bit mode and then into 4 bit mode but it also will violate the hd44780 inter instruction timings.
For example, the first few instructions sent to the LCD when using only 4 data pins are sent as a single nibble not two and then there is no delay between instructions.

The majority of what you will need to focus on is learning the hd44780 interface, its instructions and how to initialize it.
Interfacing to the LCD using a serial register is the easier part.

You need to spend some serious time with the hd44780 datasheet to understand the various h/w and instruction timings as well as the proper initialization sequence.
https://www.sparkfun.com/datasheets/LCD/HD44780.pdf

You will need to understand all the various timings.
There are timings at multiple levels that must be honored.
There are no guard rails at this level and you can't just slam nibbles at the LCD as it will do things too fast and will violate those timing requirements which means it won't work.

There are timings for the AVR instructions - that will be in the AVR datasheet.
You will need to look this up to know how fast you will be flipping pins.

There are timings for the 595 - that will be in that datasheet.
There are low level BUS timings for the hd44780 interface - that will be in figure 25 of the hd44780 datasheet.
In particular, pay attention to things like tAS, PWEH, tcycE,
There are 4 bit mode instruction sequences and timing - that is in figure 24 page 26.

And then there are timings for instructions once the LCD is initialized.
Those are in table 6 on page 24.

If you send an instruction to the LCD before the previous one has completed, it will be lost. Even worse is if in 4 bit mode and the timing is just right, you might lose just the first nibble, and now the host and the LCD are out of nibble sync and will never recover until the LCD is re-initialized.

Until you get the LCD to initialize it doesn't make sense to try to print anything.
I.e. once you can initialized and clear the LCD, you can then start to try to print characters to it.
From my looking, the code you currently using will violate the hd44780 LCD instruction timing and likely some of the BUS timing.

IMO, if you want to play at this low of a level you need appropriate tools.
My suggestion is to get a logic analyzer - you can use this to verify all your signals.
You can also use the logic analyzer to look at the various timings of all the signals going to the shift register and to the LCD to make sure you are not violating any of various timings - which is easy to accidental do when working at this level - and to see if the nibbles are what you are expecting them to be.
You can pick up inexpensive USB based analyzers for under $20 USD.

You should also be looking at the assembler output from the compiler to see what is really happening.

Its all part of playing at the bare metal level.

I have written several shift register libraries for controlling a hd44780 display.
BTW, with some additional components it can be done with fewer pins.
Including using only a single pin.
More commonly it is done with some additional components (resistor and diode) and controlled with only two pins.

I will also say that the 595 can be problematic in that it is quite sensitive to noise.
i.e. if you don't have good clean power an good grounds and proper decoupling it will never work because you will get phantom clocking happening.
In your schematic there is no decoupling so it may have issues and depending on wire lengths may suffer from ground bounce issues.
Also, if you clock the 595 too fast it can also cause issues.

Having spent around 15 years writing various hd44780 libraries including for the Arduino, I'm vary familiar with the hd44780 and how to make it work on the AVR and in the Arduino environment. (I'm the Author of the Arduino hd44780 library and was the author of several of the shift register i/o classes for the NewLiquidCrystal library)

I've written code to talk to the hd44780 pretty much any way possible.
bit banging on lots of different processors, through i2c, shift registers, etc..
And using raw port access or Arduino core library i/o functions.

I was also involved with the AVR libC developers to correct some issues in the util/delay.h routines. This came as a direct result of some of the timing issues I ran into developing the newLiquidCrytal library code for shift registers like the 595.

In the hd44780 library I actually don't include a shift register i/o class.
I have the code.
I just chose not to include it in the release since it is more difficult to get working than an I2C backpack.
For a while, it was cheaper to DIY a serial backpack, but as of about 8 years ago the i2c backpacks got so cheap that you can't even DIY your own shift register backpack for less than you can buy a PCF8574 based one.
Yeah a shift register based interface can be faster particularly if using raw port i/o to bit bang it, but it also comes with some issues and on some of the faster platforms like the ESP based ones, the big banging is too fast for the shift register so you have to insert delays between bit toggles - which makes it slower.
These are some of the reasons why I decided to not include a serial shift register i/o class in the hd44780 library.

Playing at this level requires a very good understanding of the hd44780 chip interface and instructions. It takes quite a bit of time to read through the datasheet to grasp all the various timings and the 4 bit instruction sequence.
And spending an enormous amount of time with a logic analyzer to verify that all the signals are properly conforming to the timing specifications.

Also, from my looking at various authors hd44780 LCD libraries, over the years, the majority do not understand the 4 bit initialization sequence.
It isn't a "magic" sequence. It is simply a sequence of regular instructions in a specific order that will first put the LCD into 8 bit mode and then into 4 bit mode. And this will work regardless of what mode the LCD is in or what nibble state it is in when the sequence is started.

If you want to full explanation of this initialization sequence, you can look at the hd44780.cpp module in the hd44780 library.
See the comments starting around line 240

--- bill

That was my impression as well. There has been much discussion about the sensitivity of the displays to the timing of commands (unless you also pay attention to the BUSY signal, which is very uncommon.) Especially since there are many HD44780 "clones", some of which apparently have slightly different timing.

(Now, you COULD have a problem with your shift register code. For instance, have you checked that the bits are going out in the correct order?)

Even if you wanted to use the BUSY signal, it can't be used during the initial 4 bit mode initialization sequence since you can't be assured what state the LCD is in.

In Arduino, using BUSY can actually be slower, particularly on the AVR when using the Arduino as it was meant to be used, which means using the core library digital i/o functions like digitalWrite(), digitalRead(), pinMode().
This is because the time it takes to flip the data pins around from output to input and the setup the control pins (RS, and RW) and then toggle E to read the BUSY status takes longer than the typical hd44780 instruction time and then you still need to flip the data pins back to output mode.

(Now, you COULD have a problem with your shift register code. For instance, have you checked that the bits are going out in the correct order?)

That's where it is very handy to have a logic analyzer to look at the pins.

--- bill