Interface LCD 16×2 with STM32 without I2C

As the topic states, today in this tutorial we are going to interface LCD 16×2 with STM32, and this time we are not going to use the I2C to do so. If you are looking for LCD 16×2 with I2C, goto

Basically we are going to use the parallel connection between STM32, and the LCD itself. LCD 16×2 can be connected in the parallel mode either using 4 data pins (LCD 4 bit MODE) or using all 8 data pins (8 bit MODE). In this tutorial, we are going to use the 4 bit MODE to connect LCD with our microcontroller.


lcd connection
lcd connection

As you can see in the picture above, the higher 4 pins of the LCD ( DB4-DB7) are connected to the microcontroller pins (PA4-PA7).

  • RS is connected to PA1
  • RW is connected to PA2
  • EN is connected to PA3

You can connect the pins anywhere you want. It doesn’t have to be in order or in a single port. We will define these pins later in our code.

CubeMx Setup

Here we need to do 2 things. First we have set all the pins, that we want to use, as output. It is shown in the picture below.

lcd cubemx

Second, we have to setup timer to create delay in microseconds. If you are using HCLK less than 100 MHz, you do not need this. I am setting it up just in case. If you don’t know how to use timer to create delay in microseconds, visit

lcd delay timer

Now the Cubemx setup is done, let’s take a look at the functions that we are going to use

Some insight into the CODE

First of all we need to define the Pins and Ports that we are going to use for the LCD. As i mentioned in the beginning, you can use any port and pins for the connection. Just make sure you define it in this LCD1602.c file, as shown below

#define RS_PIN GPIO_PIN_1
#define RW_PIN GPIO_PIN_2
#define EN_PIN GPIO_PIN_3
#define D4_PORT GPIOA
#define D4_PIN GPIO_PIN_4
#define D5_PORT GPIOA
#define D5_PIN GPIO_PIN_5
#define D6_PORT GPIOA
#define D6_PIN GPIO_PIN_6
#define D7_PORT GPIOA
#define D7_PIN GPIO_PIN_7

Next is the Timer definition, to give us the delay in microseconds. You might not need to do this for Lower HCLK. Just replace htim1 with the timer handler that you are using.

#define timer htim1
extern TIM_HandleTypeDef timer;
void delay (uint16_t us)
  __HAL_TIM_SET_COUNTER(&timer, 0);
  while (__HAL_TIM_GET_COUNTER(&timer) < us);

I have created a function to write the data/command to the respective pins of the LCD. This function takes 2 parameters, first one is the data that we are going to write, and second one is the RS, which can be set as 0 in case of command, and 1 if we want to write data.

void send_to_lcd (char data, int rs)
    HAL_GPIO_WritePin(RS_GPIO_Port, RS_Pin, rs);  // rs = 1 for data, rs=0 for command
    /* write the data to the respective pin */
    HAL_GPIO_WritePin(D7_GPIO_Port, D7_Pin, ((data>>3)&0x01));
    HAL_GPIO_WritePin(D6_GPIO_Port, D6_Pin, ((data>>2)&0x01));
    HAL_GPIO_WritePin(D5_GPIO_Port, D5_Pin, ((data>>1)&0x01));
    HAL_GPIO_WritePin(D4_GPIO_Port, D4_Pin, ((data>>0)&0x01));
    /* Toggle EN PIN to send the data
     * if the HCLK > 100 MHz, use the  20 us delay
     * if the LCD still doesn't work, increase the delay to 50, 80 or 100..
    HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, 1);
//  delay (20);
    HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, 0);
//  delay (20);
  • All we have to do is, take the useful data, which is 4 bit long, and write the first bit to the DB4, second bit to DB5, third to DB6, and fourth to DB7.
  • Then we need to perform the enable pin strobe, to update this data to the respective pins. Which means that first pull the pin HIGH, and than pull it LOW.
  • If you are using a controller with HCLK more than 100 MHz, than you need to un-comment that delay line, as shown in the code above.
  • This is because We have to wait for the enable pin to settle. If the clock is very high, increase the delay.

The following is the function to send the command to the LCD

void lcd_send_cmd (char cmd)
    char datatosend;
    /* send upper nibble first */
    datatosend = ((cmd>>4)&0x0f);
    send_to_lcd(datatosend,0);  // RS must be while sending command
    /* send Lower Nibble */
    datatosend = ((cmd)&0x0f);
    send_to_lcd(datatosend, 0);
  • LCD is connected in the 4 bit mode, where only 4 data pins of the LCD are being used.
  • The command that we send should also be 4 bit wide. Therefore we have to first send the higher nibble, and than lower nibble. RS pin must be 0, for the command operation.

Below is the function to send data to the LCD

void lcd_send_data (char data)
    char datatosend;
    /* send higher nibble */
    datatosend = ((data>>4)&0x0f);
    send_to_lcd(datatosend, 1);  // rs =1 for sending data
    /* send Lower nibble */
    datatosend = ((data)&0x0f);
    send_to_lcd(datatosend, 1);

It is similar to the command function, except RS pin is HIGH to indicate the data operation.

void lcd_put_cur(int row, int col)
    switch (row)
        case 0:
            col |= 0x80;
        case 1:
            col |= 0xC0;
    lcd_send_cmd (col);

The above function puts the cursor at the desired row and column. The row can be either 0 or 1, and the column can vary from 0 to 15.

Initialize the LCD in the 4 bit mode

void lcd_init (void)
    // 4 bit initialisation
    HAL_Delay(50);  // wait for >40ms
    lcd_send_cmd (0x30);
    HAL_Delay(5);  // wait for >4.1ms
    lcd_send_cmd (0x30);
    HAL_Delay(1);  // wait for >100us
    lcd_send_cmd (0x30);
    lcd_send_cmd (0x20);  // 4bit mode

  // dislay initialisation
    lcd_send_cmd (0x28); // Function set --> DL=0 (4 bit mode), N = 1 (2 line display) F = 0 (5x8 characters)
    lcd_send_cmd (0x08); //Display on/off control --> D=0,C=0, B=0  ---> display off
    lcd_send_cmd (0x01);  // clear display
    lcd_send_cmd (0x06); //Entry mode set --> I/D = 1 (increment cursor) & S = 0 (no shift)
    lcd_send_cmd (0x0C); //Display on/off control --> D = 1, C and B = 0. (Cursor and blink, last two bits)

The above code is with reference to the pattern given for the initialization in the datasheet of the device.

The following is the code inside the main function. First I have initialized the LCD, and than printing the strings.

  lcd_put_cur(0, 0);
  lcd_send_string("HELLO ");
  lcd_send_string("WORLD ");
  lcd_put_cur(1, 0);


lcd result

Check out the Video Below


You can help with the development by DONATING
To download the code, click DOWNLOAD button and view the Ad. The project will download after the Ad is finished.

44 Comments. Leave new

  • please, why I can’t load the the download website

  • Thamanoon Kedwiriyakarn
    March 3, 2023 6:29 PM

    Hello, thank you very much, Sir.
    Would you mind, can I translate your tutorial to Thai language?

    • I guess google translate does the job good enough. But anyway if you want you can do it. Make sure to give credits though.

  • Hi,

    When I tried this with my stm32 and the maximum clock is 32MHz, it is showing weird character. It seems I have problems with the LCD initialize. I tried with the board that can be set to 72MHz, the LCD works like a charm.
    Do I need to adjust the delay?

    • Check the microsecond delay part. You need to change setting for it (delay) to work properly.

      • Thank you for the help. If I understand correctly that you said with HCLK below 100MHz, then we don’t need to use microsecond delay?
        But I did set the timer as your setting but just change the PSC = 32-1 instead of 72-1 and the rest is the same as yours. Is there any factor leading to this problem?

        • contact me on telegram or discord. I need to see the code first

          • Hello, I’m using STM32G030K6T6, its maximum clock is 64MHz and I’m facing the same problem, could you help me?

          • just give some delay inside the send command and send data functions.
            You can also use something like
            uint32_t del = 10000;
            while (del–);

    • hey, you managed to fix it? my clock can’t exceed 32MHz too and I cant find the right delay to make it work well

  • Hi, I’d like to ask, what are those logic ands with 0x0f in sending commands for?

  • My code run on STM32F100RB With

    void LCDInit(void)
    //4 bit initialisation
    //display initialisation
    LCDSendCommand(0x02); // Return Home
    LCDSendCommand(0x28); //4 bit mode
    LCDSendCommand(0x0C); //Display on/cursor off
    LCDSendCommand(0x01); // clear display

    rest is same
    now lets enjoy the coffee

  • i am using 16*4 display, can i use this code for my implementaion, what are all things i have to do? please anyone answer me

  • Can you make the tutorial with the f303k8 as well? I followed the same tutorial but had some errors in the end saying failed to start GDB server, other errors were: Target Held under Reset

    Error in initializing ST-LINK device.
    Reason: Target held under reset.

    • “I folowed the tutorial” ? Well your error have nothing to do with tutorial. This is specific to your board.
      I would suggest that run saome basic things first, and then go higher.
      “Can you make the tutorial with the f303k8 as well”. Well send me the board, and I will make it.
      ST have hundreds of boards, and I can’t make for all of them. That’s why I show the process, instead of just uploading the code

    • hi bro, can i go for 16*4 display,could i take this code for my implementation?

  • hey admin , i have a nucleo board G474 and i burn the program and i try to change the delay , and still not work , can you help me please?

  • Karthick Reddy
    July 21, 2021 5:16 AM

    Surely Coffee my friend

  • hi, how do i use variables inside the print function?

    • first change the variable value
      then convert it to the characters using the sprintf function
      then print on the display

  • Hello, for me everything fine, the debuging is completed, but the lcd doesn’t work.
    the stm program go directly to the sentence : HAL_Init();
    can u maybe help me?

    • I have the same problem, probably u dont put the V0 pin (LCD) to GND, is not in the diagram, but u have to put it

  • Hi, The lcd shows a blank screen.

  • I want to get a data sheet
  • hi, where is the function lcd_send_string described?

  • Hmm! I’m wondering if the 1602A is different from the one mentioned in this tutorial. While going over the init from Arduino I noticed the RS and E pins were set to 12 and 11, and I don’t see those in the init code here. Meh, I’ll have to go through it all thoroughly and then disassemble the arduino code.

    My mistake. Still going through the code, though. As it’s still not running on this.

    • All the pins are declared in LCD1602.c file. This information is mentioned in this article also.

      • I know. Should have mentioned I was attempting to translate from a cpp library file that is called from a c file so kind of got confused.

        Anyway, other than removing some redundant code and renaming/re-assigning the routines and pins from the sources here, the only thing that is really different is I don’t quite understand how to handle the timers as of yet (if needed for this example on my stm32l152c board). So I used HAL_Delay (which I’ve read isn’t the best choice), uwTick, and for loops.

        Well, once I can get around to it. I’ll go through everything again and see what happens. Might even hook it up to the arduino and use the example code with it just to be sure the device works.

        I’m still learning.

        <Edit> AHA! I forgot to ground RW. That was the problem.

  • Really I have not seen you like your usual .. it is a very good professional explanation … Thanks for the effort you made

  • LCD.c(30): warning: #188-D: enumerated type mixed with another type
    HAL_GPIO_WritePin(RS_GPIO_Port, RS_Pin, rs); // rs = 1 for data, rs=0 for command
    LCD.c(33): warning: #188-D: enumerated type mixed with another type
    HAL_GPIO_WritePin(D7_GPIO_Port, D7_Pin, ((data>>3)&0x01));
    LCD.c(34): warning: #188-D: enumerated type mixed with another type
    HAL_GPIO_WritePin(D6_GPIO_Port, D6_Pin, ((data>>2)&0x01));
    LCD.c(35): warning: #188-D: enumerated type mixed with another type
    HAL_GPIO_WritePin(D5_GPIO_Port, D5_Pin, ((data>>1)&0x01));
    LCD.c(36): warning: #188-D: enumerated type mixed with another type
    HAL_GPIO_WritePin(D4_GPIO_Port, D4_Pin, ((data>>0)&0x01));
    LCD.c(42): warning: #188-D: enumerated type mixed with another type
    HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, 1);
    LCD.c(44): warning: #188-D: enumerated type mixed with another type
    HAL_GPIO_WritePin(EN_GPIO_Port, EN_Pin, 0);

    HOW TO FIX IT ???

    • Yeah i have the same problem and i try to run program, I have hardfault interrupt.

      • You can’t run into hardfault with those warnings. They are there because I have used ‘1’ instead of GPIO_STATE_SET, and ‘0’ instead of GPIO_STATE_RESET. That’s completely fine.
        Debug you code step by step and check what’s causing the hardfault and report

        •  /* Check whether or not the timer instance supports internal trigger input */

             TIM_ITRx_SetConfig(htim->Instance, sClockSourceConfig->ClockSource);

          after here (this was from stm32f1xx_hal_tim.c (also disassembly table 4606 column)) itgoes into hardfault interrupt w1_hardfault_ırqn 0 .

          Also my lcd just glow i tried to change delay but no change.

          • Can u post pictures of the clock setup and timer setup.
            Better join the telegram group (link on the top right). You will get better 1-1 support there

  • I have programmed the same to my STM32F103 Nucleo Board. Everything is fine. However, no character is being displayed at the LCD. It just glow, but nothing to display.

  • Awesome post! Keep up the great work! 🙂

  • hello admin……
    thank you for your information…….
    can you please explain the audio player using sd card

    thank you

  • Hello ! I have already repeated your interface lcd 1602 project without I2C. Everything works great. Now I will add ds18b20 to it. Thank you very much.


Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.


Adblocker detected! Please consider reading this notice.

We've detected that you are using AdBlock Plus or some other adblocking software which is preventing the page from fully loading.

We don't have any banner, Flash, animation, obnoxious sound, or popup ad. We do not implement these annoying types of ads!

We need money to operate the site, and almost all of it comes from our online advertising.

Please add to your ad blocking whitelist or disable your adblocking software.