USART to LCD (register-level programming) but it is not working

Hi there !

i am trying to send Serial information to an LCD display (hd44780)

there is two main part in my code : the USART part and the LCD part. both are working well, separately.

but when i want to send USART ascii data to my LCD it is not working.

if i send a string, there is nothing

and if i send a char, it displays a 'ä' character (which is weird, i didn't know this was in ASCII table lol.

here's my code :

//--------------------------------------------------------
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
//--------------------------------------------------------
#ifndef F_CPU
#define F_CPU 16000000UL
#endif
#define BAUD 9600
#define MYUBRR(F_CPU,BAUD)((F_CPU/16/BAUD)-1)
//--------------------------------------------------------
#define E  (1<<PD3)
#define RS (1<<PD2)
//--------------------------------------------------------
void USART_Init(uint32_t ubrr)
{
  /*Set BaudRate*/
  UBRR0L = MYUBRR(F_CPU,ubrr);
  UBRR0H = (MYUBRR(F_CPU,ubrr) >> 8);
  /*Enable Receiver and Transmitter*/
  UCSR0B = (1<<RXEN0) | (1<<TXEN0);
  /*Set Frame format: 8data, 2stop bit*/
  UCSR0C = (1<<USBS0) | (3<<UCSZ00);
}
uint32_t USART_Receive(void)
{
  while(!(UCSR0A & (1<<RXC0)));
  return UDR0;
}
void USART_Transmit(unsigned char _data)
{
  //Wait for empty transmit buffer
  while(!(UCSR0A & (1<<UDRE0)));
  //Put data into buffer, sends the data
  UDR0 = _data;
}
//--------------------------------------------------------
//latch E pin command
void latch(void)
{
  PORTD |= E;     //send high
  _delay_us(500);  //wait
  PORTD &= ~E;    //send low
  _delay_us(500); //wait
}
//to send commands in the right order
void lcd_cmd(uint8_t cmd)
{
  PORTD = (PORTD & 0x0F) | (cmd & 0xF0); //send high nibble
  PORTD &= ~RS;                          //send 0 to select command register
  latch();                               //latch the data
  PORTD = (PORTD & 0x0F) | (cmd << 4);   //send low nibble
  latch();                               //latch the data
}
//init sequence
void lcd_init()
{
  //Init ports for LCD
  DDRD |= (1<<PD2) | (1<<PD3) | (1<<PD4) | (1<<PD5) | (1<<PD6) | (1<<PD7);
  //send pulse to latch the data
  latch();
  _delay_ms(2); //delay for stable power
  //command to set up the LCD
  lcd_cmd(0x33);
  _delay_us(100);
  lcd_cmd(0x32);
  _delay_us(100);
  lcd_cmd(0x28); //2 lines 5x7 matrix dot
  _delay_us(100);
  //lcd_cmd(0x0E); // display ON, cursor ON
  lcd_cmd(0x0C);   //display ON, cursor ON
  _delay_us(100);
  lcd_cmd(0x01);   //clear LCD
  _delay_ms(20);   //wait
  lcd_cmd(0x06);   //shift cursor to right
  _delay_ms(1);
}

void lcd_setcursor(uint8_t x, uint8_t y)
{
  uint8_t firstcharadr[] = {0x80,0xC0,0x94,0xD4};
  lcd_cmd(firstcharadr[y-1]+ x-1);
  _delay_us(1000);
}
uint16_t lcd_char(uint8_t data)
{
  PORTD = (PORTD & 0x0F) | (data & 0xF0); //send high nibble
  PORTD |= RS;                            //send one to select command register
  latch();
  PORTD = (PORTD & 0x0F) | (data<<4);     //send high nibble
  latch();
  return data;
}

uint32_t lcd_print(char *str)
{
  uint8_t k = 0;
  while(str[k] != 0)
  {
    lcd_char(str[k]);
    k++;
  }
  return str;
}
int main()
{
  USART_Init(BAUD);
  lcd_init();
  lcd_setcursor(1,1);
  USART_Transmit('R');
  lcd_print("USART Test :");
  lcd_setcursor(1,2);
  
  //UCSR0B |= (1<<RXCIE0);
  //sei();

  while(1){
    while(!(UCSR0A & (1<<UDRE0)));
    lcd_char(USART_Receive());
    }
  return 0;
}

i've read somewhere that i cannot do this that way because an LCD display is not a serial monitor. which make sens. it works like old terminals, i guess. a terminal receive serial data and have internal logics that displays the text one character at a time.

if this is the solution, how can i create sort of a buffer and put it to the display ? like so :

this is a practice for low-level programming on Atmega328p.
i am not using arduino high-level code here.

If you use Arduino code then you can find out how the display works. Then build the Conversion part the same way.

1 Like

what makes you think that you need a buffer?

You receive a character on the Serial side - you transmit the character to your LCD.
a buffer of size 1 should be sufficient for most of the characters.

if I try to just echo the received character to your USART_Transmit I get also some weird characters on serial. So I suspect your Serial is not working correct.

i agree. but if i write

lcd_print(USART_Receive());

nothing happens

may be post 9 is of help:

To everyone : thanks for your answers ! it gives me clues to solve this problem :smiling_face:

i think it is a matter of simulation. i'd loooove to have proteus 8 but it is way too expansive. so i have to rely on wokwi.com and tinkercad. which are not the best solutions unfortunately.

i don't have an arduino board, temporarily. i will try again when i get this.

Nor does it take data in a serial stream. They're usually parallel devices, not serial.

There is a great LCD library for Arduino. Perhaps you could use that? Or at least use it to learn how the LCD works.

i'm already on the hd44780 documentation for a while by now :face_with_spiral_eyes:

but as i did say, i can manually display characters on the LCD. that part works

the init sequence works, all subroutines i typed works.

now i want to take serial data, turn it to strings i can display on the LCD.

this is the tricky part. but maybe some problems are due to the simulator i use.

i will try on real gear as soon as i can.

Is the serial data not already a string of characters? That's what comes on the serial line. Just output those characters to the screen if you have code that works to do that.

yes of course

but it looks like sending the serial data directly, is not stable or not working at all.

which is weird. i will try with real gear when i can

as mentioned already, your receiving part is the problem.

if you would have shared your wokwi link the others don't need to do the same on their own and you will get more helpers...

UPDATE :

here's my code now :


//--------------------------------------------------------
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
//--------------------------------------------------------
#ifndef F_CPU
#define F_CPU 16000000UL
#endif
#define BAUD 9600
#define MYUBRR(F_CPU,BAUD)((F_CPU/16/BAUD)-1)
//--------------------------------------------------------
#define E  (1<<PD3)
#define RS (1<<PD2)
//--------------------------------------------------------
ISR(USART_RX_vect)
{
  while(!(UCSR0A & (1<<RXC0))){};
  unsigned char received = USART_Receive();
  unsigned char _destination[32] = "";
  strcpy1(_destination ,received);
  lcd_setcursor(1,2);  
  lcd_print("                ");
  _delay_ms(200);  
  lcd_setcursor(1,2);    
  lcd_print(_destination);
  //lcd_char(_destination);
  _delay_ms(200);
}
//--------------------------------------------------------
void USART_Init(uint32_t ubrr)
{
  /*Set BaudRate*/
  UBRR0L = MYUBRR(F_CPU,ubrr);
  UBRR0H = (MYUBRR(F_CPU,ubrr) >> 8);
  /*Enable Receiver and Transmitter*/
  UCSR0B = (1<<RXEN0) | (1<<TXEN0);
  /*Set Frame format: 8data, 2stop bit*/
  UCSR0C = (1<<USBS0) | (3<<UCSZ00);
}
uint32_t USART_Receive(void)
{
  while(!(UCSR0A & (1<<RXC0)));
  return UDR0;
}
void USART_Transmit(unsigned char _data)
{
  //Wait for empty transmit buffer
  while(!(UCSR0A & (1<<UDRE0)));
  //Put data into buffer, sends the data
  UDR0 = _data;
}
//--------------------------------------------------------
//latch E pin command
void latch(void)
{
  PORTD |= E;     //send high
  _delay_us(500);  //wait
  PORTD &= ~E;    //send low
  _delay_us(500); //wait
}
//to send commands in the right order
void lcd_cmd(uint8_t cmd)
{
  PORTD = (PORTD & 0x0F) | (cmd & 0xF0); //send high nibble
  PORTD &= ~RS;                          //send 0 to select command register
  latch();                               //latch the data
  PORTD = (PORTD & 0x0F) | (cmd << 4);   //send low nibble
  latch();                               //latch the data
}
//init sequence
void lcd_init()
{
  //Init ports for LCD
  DDRD |= (1<<PD2) | (1<<PD3) | (1<<PD4) | (1<<PD5) | (1<<PD6) | (1<<PD7);
  //send pulse to latch the data
  latch();
  _delay_ms(2); //delay for stable power
  //command to set up the LCD
  lcd_cmd(0x33);
  _delay_us(100);
  lcd_cmd(0x32);
  _delay_us(100);
  lcd_cmd(0x28); //2 lines 5x7 matrix dot
  _delay_us(100);
  //lcd_cmd(0x0E); // display ON, cursor ON
  lcd_cmd(0x0C);   //display ON, cursor ON
  _delay_us(100);
  lcd_cmd(0x01);   //clear LCD
  _delay_ms(20);   //wait
  lcd_cmd(0x06);   //shift cursor to right
  _delay_ms(1);
}

void lcd_setcursor(uint8_t x, uint8_t y)
{
  uint8_t firstcharadr[] = {0x80,0xC0,0x94,0xD4};
  lcd_cmd(firstcharadr[y-1]+ x-1);
  _delay_us(1000);
}
void lcd_char(uint8_t data)
{
  PORTD = (PORTD & 0x0F) | (data & 0xF0); //send high nibble
  PORTD |= RS;                            //send one to select command register
  latch();
  PORTD = (PORTD & 0x0F) | (data<<4);     //send high nibble
  latch();
  
}

void lcd_print(char *str)
{
  uint8_t k = 0;
  while(str[k] != 0)
  {
    lcd_char(str[k]);
    k++;
  }
}

char *strcpy1(char dst[], char src[]) {
    int i = 0;
    while(src[i] != '\0')
    {
        dst[i]=src[i];
        i++;
    }
    dst[i]='\0';
    return dst;
}

int main()
{
  USART_Init(BAUD);
  lcd_init();
  lcd_setcursor(1,1);
  USART_Transmit('R');
  lcd_print("USART Test :");
  
  UCSR0B |= (1<<RXCIE0);
  sei();
  while(1)
  {
    lcd_setcursor(1,2);
    for(int i=0;i<16;i++){
      lcd_print("-");
      _delay_ms(200);
    }
    lcd_setcursor(1,2);
    lcd_print("                ");
    _delay_ms(200);
    
  }
  return 0;
}

i manually added a strcpy() function. i tried in a separate C code, and it works very well.

in my atmega/arduino code, this strcpy function is supposed to do the (maybe) needed bridge between ASCII serial data and output-to-lcd.

but still nothing. using lcd_char, i get a random character, and lcd_print, i get simply nothing.

something's missing in the chain. and yes, as mentionned before, the receiving part is the problem.

here's the wokwi link :

Hello there,

i get some progress but i am not done.

i can now test in real life with a atmega328p based nano board, and a 16x2 LCD display.

Now i try with a simple arduino code, based on an arduino example :

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

void setup() {
  Serial.begin(9600);
  lcd.begin(16,2);
  
}

void loop() {

  if(Serial.available())
  {
    delay(100);
    lcd.clear();
    while(Serial.available() > 0){
      lcd.write(Serial.read());
    }
  }
}

Here's what i get :

it displays whatever i type but with this weird character at the end.

with my bare-metal code, it is even worse

//--------------------------------------------------------
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdlib.h>
//--------------------------------------------------------
#ifndef F_CPU
#define F_CPU 16000000UL
#endif
#define BAUD 9600
#define MYUBRR(F_CPU,BAUD)((F_CPU/16/BAUD)-1)
//--------------------------------------------------------
#define E  (1<<PD3)
#define RS (1<<PD2)
//--------------------------------------------------------
void adc_init();
int adc_read(char channel);
//--------------------------------------------------------
void lcd_init();
void latch(void);
void lcd_cmd(uint8_t cmd);
void lcd_char(uint8_t data);
void lcd_print(char *str);
void lcd_setcursor(uint8_t x, uint8_t y);
//--------------------------------------------------------
char temp[10];
float celsius;
void latch(void)
{
  PORTD |= E;     //send high
  _delay_us(500);  //wait
  PORTD &= ~E;    //send low
  _delay_us(500); //wait
}
//to send commands in the right order
void lcd_cmd(uint8_t cmd)
{
  PORTD = (PORTD & 0x0F) | (cmd & 0xF0); //send high nibble
  PORTD &= ~RS;                          //send 0 to select command register
  latch();                               //latch the data
  PORTD = (PORTD & 0x0F) | (cmd << 4);   //send low nibble
  latch();                               //latch the data
}
//init sequence
void lcd_init()
{
  //Init ports for LCD
  DDRD |= (1<<PD2) | (1<<PD3) | (1<<PD4) | (1<<PD5) | (1<<PD6) | (1<<PD7);
  //send pulse to latch the data
  latch();
  _delay_ms(2); //delay for stable power
  //command to set up the LCD
  lcd_cmd(0x33);
  _delay_us(100);
  lcd_cmd(0x32);
  _delay_us(100);
  lcd_cmd(0x28); //2 lines 5x7 matrix dot
  _delay_us(100);
  //lcd_cmd(0x0E); // display ON, cursor ON
  lcd_cmd(0x0C);   //display ON, cursor ON
  _delay_us(100);
  lcd_cmd(0x01);   //clear LCD
  _delay_ms(20);   //wait
  lcd_cmd(0x06);   //shift cursor to right
  _delay_ms(1);
}

void lcd_setcursor(uint8_t x, uint8_t y)
{
  uint8_t firstcharadr[] = {0x80,0xC0,0x94,0xD4};
  lcd_cmd(firstcharadr[y-1]+ x-1);
  _delay_us(1000);
}
void lcd_char(uint8_t data)
{
  PORTD = (PORTD & 0x0F) | (data & 0xF0); //send high nibble
  PORTD |= RS;                            //send one to select command register
  latch();
  PORTD = (PORTD & 0x0F) | (data<<4);     //send high nibble
  latch();
  
}

void lcd_print(char *str)
{
  uint8_t k = 0;
  while(str[k] != 0)
  {
    lcd_char(str[k]);
    k++;
  }
  return str;
}
//--------------------------------------------------------
void adc_init(void)
{
  //set the voltage reference using REFS1 and REFS0 bits and select the ADC channel using MUX bits
  ADMUX = 0b01000000; //REFS1=0,REFS0=1(Vref as AVCC pin), ADLAR = 0(right adjusted),MUX4 to MUX0 is 0000 for ADC0
  //enable ADC module, set prescalar of 128 which gives CLK/128
  ADCSRA |= (1<<ADEN) | (1<<ADIE) | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0);
}
int adc_read(char channel)
{
  ADMUX = 0x40 | (channel & 0x07); //0100 0000 | (channel & 0000 0100)
  _delay_ms(1);                    //Wait for 1ms

  return ADCW;                     //Return ADC word
} 
//--------------------------------------------------------
ISR(ADC_vect)
{
  celsius = (adc_read(2)*4.9);
  celsius = (celsius/10.00);
  dtostrf(celsius,6,2,temp);
  lcd_setcursor(1,2);
  lcd_print(temp);
  lcd_char(0xDF);
  lcd_print("C");
  ADCSRA |= (1<<ADSC); //start ADC conversion
  _delay_ms(150);
}
//--------------------------------------------------------
int main()
{
  lcd_init();
  adc_init();
  sei();                          //enable global interrupt
  lcd_setcursor(1,1);
  lcd_print("Temperature:");
  ADCSRA |= (1<<ADSC);            //start ADC conversion
  while(1){}
  return 0;
}

i know now that for Serial display, i need to use my Char writing funciton instead a String printing function.

then it works if i type one character. typing 'a' in a monitor displays 'a' on the LCD, typing 'b' displays 'b' and so on.

but first, even with one character, it is still followed by this weird character (as shown on the photo), then it does the same that

second, if i want to type multiple characters, here what it does :
it displays the two first characters, one at a time and only on the first column of the row.

let's say i send "hello" from the serial monitor, it displays
'h' then replace by 'e' and then replace by the weird character, and that's it. Doesn't even get all the characters

is there something i forget to set with the Serial protocol ?