Writing C++ for the STM32F0 to talk to an LCD

Thread Starter

brbeardm

Joined Oct 26, 2016
9
I'm trying to learn embedded dev in C++ using CoIDE. I have a STM32F0 chip on a break out board. i've done some LED tutorials etc. I'm stuck on this code that is supposed to write some simple text strings to the LCD. The tutorial I'm following is at eeherald http://www.eeherald.com/section/design-guide/sample_lcd_c_programs.html

i've adapted the code to my STM32F0 chip. I think I'm close, but the LCD just stays in initialization mode with all the cells lit up. the screen never clears to write the text. here's my code... any help to point me in the right direction would be APPRECIATED!

I "think" the problem may be in the initGPIO() for the data direction code, but I'm not sure... i've tried so many different things with no luck.

Code:
//************************************************************************ here's my code *******************************************************************
#include "stm32f0xx_hal.h"
#include "stm32f0xx_hal_gpio.h"
#include "stm32f0xx_hal_rcc.h"
uint32_t TickValue=0;

#define PLL_MUL_X    3
#define EN     12 // EN Enable on PortB chip pin#53
#define RW    11 // RW Read Write on PortB chip pin#52
#define RS    10 // RS Register Select on PortB chip pin#51

void initGPIO(void);
void TimingDelay_Decrement(void);
void delay_ms(uint32_t n_ms);
void s_init(void);
void s_data(void);
void s_latch(void);

//------------------------------------------------------------------------------
// Function Name : Init GPIO
// Description : pins ,port clock & mode initialization.
//------------------------------------------------------------------------------
void initGPIO()
{
    // Define GPIO Structures
    GPIO_InitTypeDef GPIO_InitStructure;

    // Reset and clock control - Advanced high-performance bus - Enabling GPIO Port B and Port C
    __GPIOB_CLK_ENABLE();
    __GPIOC_CLK_ENABLE();

    // Set Data Direction for DataDir_MrLCDsControl
    GPIO_InitStructure.Pin = RS | RW | EN;
    GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStructure);
    delay_ms(15);

    GPIO_InitStructure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
    delay_ms(15);
}

//------------------------------------------------------------------------------
// Function Name : s_init
// Description : Send Instruction Function (RS=0 & RW=0)
//------------------------------------------------------------------------------
void s_init()
{
    GPIOC->BRR=RS;
    GPIOC->BRR=RW;
}

//------------------------------------------------------------------------------
// Function Name : s_data
// Description : Send Data Select routine(RS=1 & RW=0)
//------------------------------------------------------------------------------
void s_data()
{
    GPIOC->BSRR=RS;
    GPIOC->BRR=RW;
}

//------------------------------------------------------------------------------
// Function Name : s_latch
// Description : Latch Data/Instruction on LCD Databus.
//------------------------------------------------------------------------------

void s_latch()
{
    GPIOC->BSRR=EN;
    delay_ms(10);
    GPIOC->BRR=EN;
    delay_ms(10);
}

/*******************************************************************************
* Function Name : main
* Description : Main program.
*******************************************************************************/
int main(void) //Main function
{
    initGPIO();

    int k=0;
    char a[]="[URL='http://WWW.EEHERALD.COM']WWW.EEHERALD.COM[/URL]";
    char b[]="EMBEDDED SYSTEMS";

    GPIOC->BRR=RS; //Initialize RS=0 for selecting instruction Send
    GPIOC->BRR=RW; // Select RW=0 to write Instruction/data on LCD
    GPIOC->BSRR=EN; // EN=1 for unlatch. (used at initial condition)

    delay_ms(10);

    s_init(); //Call Instruction Select routine
    GPIOB->ODR=0x0001; // Clear Display, Cursor to Home
    s_latch(); //Latch the above instruction
    GPIOB->ODR=0x0038; // Display Function (2 rows for 8-bit data; small)
    s_latch(); //Latch this above instruction 4 times
    s_latch();
    s_latch();
    s_latch();
    GPIOB->ODR=0x000E; // Display and Cursor on, Cursor Blink off
    s_latch(); //Latch the above instruction
    GPIOB->ODR=0x0010; // Cursor shift left
    s_latch(); //Latch the above instruction
    GPIOB->ODR=0x0006; // Cursor Increment, Shift off
    s_data(); //Change the input type to Data.(before it was instruction input)
    s_latch(); //Latch the above instruction

    for(k=0;a[k];k++)
    {
        GPIOB->ODR=a[k]; //It will send a[0]='P' as = '0x0050' on Port A.
        s_latch(); //Latch the above instruction only once. Or it will clone each character twice if you latch twice.
    }
    GPIOC->BRR=RS; //Initialize RS=0 for selecting instruction Send
    GPIOC->BRR=RW; // Select RW=0 to write Instruction/data on LCD
    GPIOC->BSRR=EN; // EN=1 for unlatch. (used at initial condition)

    delay_ms(10);
    GPIOB->ODR=0x00C0; // Move cursor to beginning of second row
    s_latch(); //Latch the above instruction
    s_data(); //Change the input type to Data.(before it was instruction input)
    for(k=0;b[k];k++)
    {
        GPIOB->ODR=b[k]; //It will send b[0]='E' as = '0x0044' on Port A.
        s_latch();//Latch the above instruction only once. Or it will clone each character twice if you latch twice.
    }
    s_init();
}

//------------------------------------------------------------------------------
// Function Name : delay_ms
// Description : delay for some time in ms unit(accurate)
// Input : n_ms is how many ms of time to delay
//------------------------------------------------------------------------------
void TimingDelay_Decrement(void)
{
    TickValue--;
}

void delay_ms(uint32_t n_ms)
{
    SysTick_Config(8000*PLL_MUL_X - 30);
    TickValue = n_ms;
    while(TickValue == n_ms);

    SysTick_Config(8000*PLL_MUL_X);
    while(TickValue != 0);
}
Edit: code tags added by moderator.
 
Last edited by a moderator:

MrChips

Joined Oct 2, 2009
30,781
You will need to post a circuit diagram showing how the LCD is interfaced to the MCU.
Can you provide the part number for the LCD and the pinouts?

For starters, make sure you install a 100nF capacitor between Vcc and GND at the LCD.
 

MrChips

Joined Oct 2, 2009
30,781
There are basic errors in your code which I will identify. But before we get to that you need to be able to confirm that things are working by applying a step by step test procedure.

Do you have an oscilloscope?
If not, you will have to build a simple test probe consisting of a 100Ω resistor in series with an LED.
 

MrChips

Joined Oct 2, 2009
30,781
Creating working, functional and reliable code is not a trial and error, hit and miss process.
There is a sound engineering approach to software development. This involves conforming to device specifications and confirmation by proper testing. I will teach you how to apply this approach to your particular problem.

The following will not make or break your situation but identifies structural deficiencies in your code.

You have a number of delay_ms( ) statements in your code that are totally unnecessary.

Your utility functions s_init( ), s_data( ) and s_latch( ) are useless and should be abandoned.

What you need are two functions, one to send a command to the LCD controller instruction register, and another to send data to the data register. These two operations can be combined into one function. I will keep them as separate functions for this exercise.

The specifications for the classic Hitachi HD44780 LCD controller require the following:

Set RW (Read/Write) = 0 for WRITE operation
Set RS (RegisterSelect) = 0 for INSTRUCTION register, = 1 for DATA register
Set DATA bits
Set E (enable) = 1
Delay
Set E (enable) = 0

Of critical importance is the setup times for RW, RS and DATA before the leading edge of E, the pulse width of E, and the hold times of RW, RW, and DATA.

Minimum SETUP times are of the order or 200ns.
Minimum HOLD times are of the order of 10ns.

Minimum pulse width of E is 450ns.

What this means to you is that delay_ms( ) with a minimum setting of 1ms is not appropriate for your application.
An STM32F030 running at 48MHz will be too fast to toggle E and will require a 1μs delay.

But first we have to verify that your GPIO pins are working properly.
 

MrChips

Joined Oct 2, 2009
30,781
The first fatal error in your code is to assume the GPIO BRR and BSRR take a bit number.
These registers are 16 bits wide and hence require a 16-bit bit mask.

Change your code to:
Code:
#define EN 0x1000  // bit-12
#define RW 0x0800  // bit-11
#define RS 0x0400  // bit-10
Next, test that you can toggle the GPIO pin.
Code:
void main(void)
{
  initGPIO();
  while(1)
  {
    GPIOC->BSRR = EN;
    delay_ms(500);
    GPIOC->BRR = EN;
    delay_ms(500);
  }
}
An LED with 100Ω current limiting resistor can be used to test E (PC12). The LED should flash once every second.
If you have an oscilloscope, delete the delay statements and measure the pulse width and pulse interval in order to determine your code execution times.

We will continue following your confirmation.
 

dannyf

Joined Sep 13, 2015
2,197
the lcd takes commands / data through a set of patterns on its pins.

So all you need to do is:

1) be able to set or clear a pin;
2) be able to read and understand the patterns in which you need to set/clear the pins to communicate with the lcd.
 

Thread Starter

brbeardm

Joined Oct 26, 2016
9
MrChips, much gratitude! Yes I have an oscilloscope. I'd like to say I'm a beginner trending to novice on skill set, so I appreciate your explanations so I know the "why" behind what I'm doing. I'm an avid learner and I want to write high quality C++ embedded code!

Ok, I completed this exercise above. Ran into challenges on the API to use... STM32Cube --vs-- previous API on the M0. I went back to the older API since that's what Keil supports and I wanted to use their IDE.

I got "Mr_blinky" working per your instruction above and I even added a few other LED's to have them turn on/off... a little fun...

Then I disabled all the delays and hooked up my oscilloscope and got the readings on PortC PC12

Freq=350kHz
Period=2.880μs
+Width=1.120μs
-Width=1.760μs
Rise=<20ns (fluctuates between <20 and <30)
Fall=<20ns (fluctuates between <20 and <30)
+Pulses=4
-Pulses=4

this video is a little blurry, but. I'm new to an Oscilloscope so getting used to it. Let me know if this reading does not look right.

thanks,
Brian
 

MrChips

Joined Oct 2, 2009
30,781
That's great!
Next step is to reinsert the delay_ms(500).
Try different values from 1 to 500 and measure the pulse widths, high and low, on the oscilloscope.
The ojective is to check the accuracy of the delay_ms( ) function.
 

MrChips

Joined Oct 2, 2009
30,781
As for getting the LCD working, we will need the following functions:

initGPIO( )
LCD_init( )
LCD_command( )
LCD_data( )
LCD_ready( )

Our test program will look like:
Code:
void main(void)
// output letter 'A' once each second
{
  initGPIO();
  LCD_init();
  while(1)
  {
    LCD_data(0x41); // output 'A'
    delay_ms(1000);
  }
}
Observe that I am using Top-Down Design.
 

Thread Starter

brbeardm

Joined Oct 26, 2016
9
got it working with the STMCube API's HAL_Delay all in sync now.... and firing nice and clean, no noise like the previous vids.

Here's an updated video

now I'm on to framing out the functions and test program
 

MrChips

Joined Oct 2, 2009
30,781
After initializing the GPIO with initGPIO( ), you need to initialize the LCD.
This is the critical part. If this is not done correctly nothing else will work with regards the LCD.
There is no way of testing this stage except getting the test program to work.

Code:
void LCD_init(void)
{
  LCD_command(0x38); // initialize 8-bit mode
  LCD_command(0x06); // increment mode
  LCD_command(0x0C); // cursor off
  LCD_command(0x10); // cursor right
  LCD_command(0x01); // clear screen
}
You will need these three support functions:

Code:
// wait for LCD ready
void LCD_ready(void)
{
  uint16_t busy;
  GPIOB->MODER = 0;  // PORTB as input
  GPIOC->BSRR = RW;  // RW = 1;
  GPIOC->BRR  = RS;  // RS = 0;
  do
  {
    GPIOC->BSRR = EN; // E = 1
    busy = 0x0080 & GPIOB->IDR; // read data
    GPIOC->BRR  = EN; // E = 0
  } while (busy);
  GPIOB->ODR = 0;
  GPIOB->MODER = 0x0000AAAA; // PORTB as 8-bit output
}

// send command to LCD instruction register
void LCD_command(uint16_t ch)
{
  LCD_ready();
  GPIOB->ODR  = ch; // write command
  GPIOC->BRR  = RS | RW; // RS = 0, RW = 0
  GPIOC->BSRR = EN;  // E = 1
  GPIOC->BRR  = EN;  // E = 0
}

// send data to LCD data register
void LCD_data(uint16_t ch)
{
  LCD_ready();
  GPIOB->ODR  = ch;  // write data
  GPIOC->BSRR = RS;  // RS = 1
  GPIOC->BRR  = RW;  // RW = 0
  GPIOC->BSRR = EN;  // E = 1
  GPIOC->BRR  = EN;  // E = 0
}
 

dannyf

Joined Sep 13, 2015
2,197
  • GPIOC->BSRR = RW; // RW = 1;
  • GPIOC->BRR = RS; // RS = 0;
That is not a good piece of code, for two reasons.

1. What if you want to run the code on a different chip where gpiob and gpioc have a different structure?
2. What if you want to change pin assignment so en and rw are on different ports?

As a general rule, you should try to avoid highly hardware dependent, and tightly platform coupled code in s user applications.

Instead, use middle layers to isolate the hardware from your code as much as you can.
 

MrChips

Joined Oct 2, 2009
30,781
That is not a good piece of code, for two reasons.

1. What if you want to run the code on a different chip where gpiob and gpioc have a different structure?
2. What if you want to change pin assignment so en and rw are on different ports?

As a general rule, you should try to avoid highly hardware dependent, and tightly platform coupled code in s user applications.

Instead, use middle layers to isolate the hardware from your code as much as you can.
You are absolutely correct. If the TS is using ST's CubeMX and HAL (hardware abstraction layer) software these tools will address your concerns. The TS needs to adopt code examples using HAL.
 

Thread Starter

brbeardm

Joined Oct 26, 2016
9
I plan to add a structure for GPIO channels so I can move stuff around, if needed as I build out my project.
structure for the RS, RW, EN pins will be GPIO_LCD_Control

I don't have BRR in STM32Cube HAL... and I can't find good education on the replacement approach, so I'm going with something like this...

HAL_GPIO_WritePin(GPIO_LCD_Control, RW, GPIO_PIN_SET); // Set LCD RW to 1
HAL_GPIO_WritePin(GPIO_LCD_Control, RS, GPIO_PIN_RESET); // Set RS to 0

is there a better way, this is a lot of code... I could do bitwise operations, right?

GPIO_LCD_Control |= (1<<RW); // Set LCD RW to 1
GPIO_LCD_Control &= ~ (1<<RS); // Set LCD RS to 0

would either work, do I understand this right.... or is one better.... personally, I like the more readable (longer) version...
 

MrChips

Joined Oct 2, 2009
30,781
In a constant.h header file:
Code:
#define LCD_CONTROL_PORT GPIOC
#define LCD_DATA_PORT GPIOB
#define EN GPIO_PIN_12
#define RW GPIO_PIN_11
#define RS GPIO_PIN_10
#define BF GPIO_PIN_7
Now you can use, for example:
Code:
HAL_GPIO_WritePin(LCD_CONTROL_PORT, EN, GPIO_PIN_SET);
HAL_GPIO_WritePin(LCD_CONTROL_PORT, EN, GPIO_PIN_RESET);
LCD_ready becomes
Code:
void LCD_ready(void)
{
  GPIO_PinState busy;
  LCD_DATA_PORT->MODER = 0;  // set as input
  HAL_GPIO_WritePin(LCD_CONTROL_PORT, RW, GPIO_PIN_SET);
  HAL_GPIO_WritePin(LCD_CONTROL_PORT, RS, GPIO_PIN_RESET);
  do
  {
    HAL_GPIO_WritePin(LCD_CONTROL_PORT, EN, GPIO_PIN_SET);
    busy = HAL_GPIO_ReadPin(LCD_DATA_PORT, BF);
    HAL_GPIO_WritePin(LCD_CONTROL_PORT, EN, GPIO_PIN_RESET);
  } while (busy);
  LCD_DATA_PORT->MODER = 0x0000AAAA; // set as 8-bit output
}
 

dannyf

Joined Sep 13, 2015
2,197
I'm sure that there are different ways of doing it but I would do something like this:

first, define various pins / ports
Code:
//in lcd.h
#define LCDRS_PORT GPIOC
#define LCDRS (1<<12) //lcdrs on gpioc.12

#define LCDRW_PORT GPIOA
#define LCDRW (1<<7) //lcdrw on gpioa.7
so when you move pins around, you can simply respecify them in lcd.h, hit recompile and the code is good to go.

you can then write lcd.c and specify operations with regards to those "logic" names:

Code:
//in lcd.c
#include "gpio.h" //your own IO operation code
[LIST=1]
[*]IO_SET(LEDRS_PORT, LCDRS);  //set RS. GPIOC->BSRR = RS;  // RS = 1
[*]IO_CLR(LEDRW_PORT, LCDRW); //clear RW.  GPIOC->BRR  = RW;  // RW = 0
[/LIST]
Code:
//in gpio.c/.h
#define IO_SET(port, pins) port->BSRR = (pins) //set pins
#define IO_CLR(port, pins) port->BRR = (pins) //clear pins
or if you want to use hal:
Code:
//in gpio.c/.h
#define IO_SET(port, pins) HAL_GPIO_WritePin(port, pins, pins)
#define IO_CLR(port, pins) HAL_GPIO_WritePin(port, pins, 0)
Here, by linking in the different gpio.c/.h, you can run the code directly on registers, or run them through HAL. Or if you link the corresponding gpio.c/.h for your target chip, the sale lcd.c/.h is then already ported to that chip.
 

dannyf

Joined Sep 13, 2015
2,197
The reason of not using vendor library directly in your code is so that if you do change out vendor library in the future, your user code is portable -> you are not tied down to any 3rd party code.

so having those middle layers is immensely helpful.
 

MrSoftware

Joined Oct 29, 2013
2,195
Middle layer is very useful for multi-platform code. I worked on libraries that were used on Windows, Linux, Solaris, Irix, AIX and OSX, all the same code base. The middle layer makes it possible to port your code to new platforms simply by changing the lower level code, you don't have to re-write everything. The same thing applies for using 3rd party libraries. As mentioned above, sometimes having that middle layer allows you to swap libraries without having to rewrite everything.

Also, for preprocessor defines specifically, I would avoid this comment style:

#define IO_SET(port, pins) port->BSRR = (pins) //set pins

In favor of this comment style:

#define IO_SET(port, pins) port->BSRR = (pins) /* set pins */

With some less-robust preprocessors, when they search + replace on your defines they will also copy the comment section. If you use the // style then it can break your code, but /* */ works fine. You won't know this until someone decides your code must be built with a new compiler in the 11th hour and surprise nothing works. ;)
 
Top