Interfacing an LCD to the STM32F4 Discovery – Part 3: Displaying Custom Messages

Custom Message Display

Custom Message Display

In the previous two posts I wrote about interfacing to the SerLCD device and getting a simple message displayed and then described a way to do that using interrupts that keeps the processing away from the work going on in the main program loop. There’s a problem in the existing library though which is that there is currently no way to display your own messages and if the objective here is to build a re-usable code library then we need to deal with that.

This job is harder than it might first appear though and the reason for this is not only that the data is sent to the SerCLD asynchronously using the interrupt driven routine but also, with a default baud rate of 9600 bps, the Discovery board is able to attempt to send data a lot faster than the SerLCD can receive it. If we attempted to send data too quickly, we’d need more and more memory allocated to hold the messages and the SerLCD would never catch up.

The way to solve this problem is to create a circular buffer. What this does is to allocate a fixed sized memory block and write any data we want to send to that memory. Each time we write to it, we append to the data already in there and when we get to the end of the buffer we simply wrap around to the start again. The program must keep track of the available buffer position for writing to. This is the current ‘start’ point. The interrupt handler that is reading the buffer also needs to keep track of where it has got to, the current ‘end’ point. The end point will be always trying to catch up with the start point.

If the interrupt handler is sending data too slowly, the start point will eventually wrap around and try to overtake the end point. This is the equivalent of a buffer overrun and we need to protect against that in some way. Likewise, if the end point overtakes the start point, this is a buffer under run and we need to take care of that situation too.

Implementing the Circular Buffer

I started off this bit of coding by implementing the circular buffer. I chose to do this inside the stm32f4xx_serlcd.c file rather than create a new one. You could argue that it should be refactored out into a separate file and made more generic and if that’s the case, go ahead and do that. What I didn’t do however, was expose the methods for the circular buffer to the outside world by declaring them in the header file. They are tucked away as private methods for use by the SerLCD driver code.

I started by declaring a bunch of constants in the header file that I’d use later on in the exposed methods.

/* Exported constants ------------------------------------------------------*/
#define SERLCD_BufferLength ((uint16_t)0x0040)
#define SERLCD_WriteOk ((uint16_t)0x0000)
#define SERLCD_BufferOverrun ((uint16_t)0x0001)
#define SERLCD_BufferUnderrun ((uint16_t)0x0002)

The first defines the size of the circular buffer and is set to 64 characters. I exposed this as part of the interface so that it is available to code outside the driver that may want to use the buffer size in calculations. I chose 64 bytes rather arbitrarily. The size of my display is 2×16 characters which would be 32 bytes however there are some commands you can send that require 2 bytes: a control code followed by a value. A buffer size of 64 bytes gives me plenty of space to account for that too.

SERLCD_WriteOk is a constant that is returned from functions to indicate that there was no problem encountered during the process of writing to the buffer. The other two constants are returned on the overrun or under run conditions.

I also declared a type to hold information about the buffer in one place.

/* Private types -------------------------------------------------------------*/
typedef struct
{
    uint16_t size;  /* The size of the buffer */
    uint16_t start; /* The index of the next character to send */
    uint16_t end;   /* The index at which to write the next character */
    char* elems;    /* The location in memory of the buffer */
} SERLCD_BufferTypeDef;

/* Private variables ----------------------------------------------------------*/

SERLCD_BufferTypeDef cb;

This is all pretty self explanatory. The three integers hold the size of the buffer and the current start and end points. The actual buffer itself is pointed to by the elems field.

The methods added to the stmf4xx_serlcd.c file to implement the circular buffer are as follows.

/* Private Methods -----------------------------------------------------------*/
void SERLCD_Buffer_Init()
{
    cb.size = SERLCD_BufferLength;
    cb.start = 0;
    cb.end = 0;
    cb.elems = calloc(cb.size, sizeof(char));
}

void SERLCD_Buffer_Free()
{
    free(cb.elems);
}

uint16_t SERLCD_Buffer_IsFull()
{
    return (cb.end + 1) % cb.size == cb.start;
}

uint16_t SERLCD_Buffer_IsEmpty()
{
    return cb.end == cb.start;
}

uint16_t SERLCD_Buffer_Write(char c)
{
    // check for a buffer overrun
    if (SERLCD_Buffer_IsFull())
    {
        return SERLCD_BufferOverrun;
    }
    else
    {
        cb.elems[cb.end] = c;
        cb.end = (cb.end + 1) % cb.size;
    }

    return SERLCD_WriteOk;
}

uint16_t SERLCD_Buffer_Read(char* c)
{
    // check for a buffer underrun
    if (SERLCD_Buffer_IsEmpty())
    {
        return SERLCD_BufferUnderrun;
    }
    else
    {
        *c = cb.elems[cb.start];
        cb.start = (cb.start + 1) % cb.size;
    }
}

The init method is pretty straightforward. It simply initialises a structure to hold the details of the buffer and allocates the memory needed to hold the character data.

There are two methods that can return the status of the buffer. The SERLCD_Buffer_IsFull function and the SERLCD_Buffer_IsEmpty function. Both work by checking the relative positions of the start and end points. An empty buffer is identified when the start and end points are the same. In order to distinguish an empty from a full buffer though, I indicate a full buffer by defining it as the situation where the end point is one less than the start point. This is quite a common approach as it’s very easy to implement even though you effectively waste one record of your buffer.

Reading and writing are exactly what you might expect to see with characters written at the end of the buffer and read from the start of the buffer. The only other slightly cunning thing is the use of the modulus operator to wrap the two indexes around to the start. A neat little trick worth tucking away for another day.

Updating the Driver Methods

Now that the circular buffer was implemented, the next job was to update the driver to use it. I made a minor change to the SERLCD_Init function adding in a call to SERLCD_Buffer_Init right at the end, added a public SERLCD_IsBufferFull function that just returns whatever it gets from the private SERLCD_Buffer_IsFull function and updated the SERLCD_WriteMessage method to allow the message to be passed in. The SERLCD_WriteMessage simply adds characters from the provided string until either it successfully writes the number of characters indicated by the length parameter or there is a buffer overflow.

Finally I made the biggest change which was to the USART1 interrupt handler where the buffer is read and the characters written to the SerLCD device. This method is a little more involved as it now checks to see if there are characters to write in the buffer and if there are it sends them. Otherwise it disables the interrupt to prevent a buffer underrun. The interrupt is reenabled by the SERLCD_WriteMessage when another message is provided.

The resulting code for stm32f4xx_serlcd.h and stm32f4xx_serlcd.c is below.

stm32f4xx_serlcd.h:

/**
******************************************************************************
* @file stm32f4xx_serlcd.h
* @author Jon Masters
* @version V1.0
* @date 06-July-2013
* @brief This file contains all the functions prototypes for the SerLCD 
* library.
******************************************************************************
*/
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __STM32F4xx_SERLCD_H
#define __STM32F4xx_SERLCD_H
#ifdef __cplusplus
extern "C" {
#endif

/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx.h"

/* Exported constants ------------------------------------------------------*/
#define SERLCD_BufferLength ((uint16_t)0x0040)
#define SERLCD_WriteOk ((uint16_t)0x0000)
#define SERLCD_BufferOverrun ((uint16_t)0x0001)
#define SERLCD_BufferUnderrun ((uint16_t)0x0002)

/* Exported types ------------------------------------------------------------*/
/* Exported functions --------------------------------------------------------*/
/* Initialization and Configuration functions *********************************/
void SERLCD_Init();
void SERLCD_DeInit();
uint16_t SERLCD_IsBufferFull();
uint16_t SERLCD_WriteMessage(char* text, uint16_t length);

#ifdef __cplusplus
}
#endif
#endif /*__STM32F4xx_SERLCD_H */

stm32f4xx_serlcd.c:

/**
******************************************************************************
* @file stm32f4xx_serlcd.c
* @author Jon Masters
* @version V1.0
* @date 06-July-2013
* @brief This file provides firmware functions to initialise and write output
* to a SparkFun SerLCD module v2.5 using USART1 on an STM32F4 device.
*
@verbatim
===================================================================
##### How to use this driver #####
===================================================================
[..]
(#) This driver assumes that the SerLCD module is connected to USART1
and will therefore take ownership of the USART1 interrupt service routine.
(#) NOTE: There is a half second delay in starting up the SerLCD while it
displays a splash screen. This driver won't deal with that for you so make sure
you have something in place to cope with that.
(#) Start by calling the SERLCD_Init() method which will perform the necessary
initialisation.
(#) Call SERLCD_WriteMessage() to write output to the device.

@endverbatim
*/

/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx_serlcd.h"
#include <string.h>
#include <malloc.h>

/* Private types -------------------------------------------------------------*/
typedef struct
{
    uint16_t size;  /* The size of the buffer */
    uint16_t start; /* The index of the next character to send */
    uint16_t end;   /* The index at which to write the next character */
    char* elems;    /* The location in memory of the buffer */
} SERLCD_BufferTypeDef;

/* Private variables ----------------------------------------------------------*/
SERLCD_BufferTypeDef cb;

/* Private Methods -----------------------------------------------------------*/
void SERLCD_Buffer_Init()
{
    cb.size = SERLCD_BufferLength;
    cb.start = 0;
    cb.end = 0;
    cb.elems = calloc(cb.size, sizeof(char));
}

void SERLCD_Buffer_Free()
{
    free(cb.elems);
}

uint16_t SERLCD_Buffer_IsFull()
{
    return (cb.end + 1) % cb.size == cb.start;
}

uint16_t SERLCD_Buffer_IsEmpty()
{
    return cb.end == cb.start;
}

uint16_t SERLCD_Buffer_Write(char c)
{
    // check for a buffer overrun
    if (SERLCD_Buffer_IsFull())
    {
        return SERLCD_BufferOverrun;
    }
    else
    {
        cb.elems[cb.end] = c;
        cb.end = (cb.end + 1) % cb.size;
    }

    return SERLCD_WriteOk;
}

uint16_t SERLCD_Buffer_Read(char* c)
{
    // check for a buffer underrun
    if (SERLCD_Buffer_IsEmpty())
    {
        return SERLCD_BufferUnderrun;
    }
    else
    {
        *c = cb.elems[cb.start];
        cb.start = (cb.start + 1) % cb.size;
    }
}

/* Public Methods -----------------------------------------------------------*/
void SERLCD_Init()
{
    // Structures to hold the initialisation data
    GPIO_InitTypeDef GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    // enable the peripherals we're going to use
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);

    // Usart1 Tx is on GPIOB pin 6 as an alternative function
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOB, &amp;GPIO_InitStruct);

    // Connect pin 6 to the USART
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_USART1);

    // fill in the interrupt configuration
    NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&amp;NVIC_InitStruct);

    // init the USART to 8:N:1 at 9600 baud as specified in the
    // SerLCD data sheet
    USART_InitStruct.USART_BaudRate = 9600;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode = USART_Mode_Tx;
    USART_Init(USART1, &amp;USART_InitStruct);

    // Enable USART1 peripheral
    USART_Cmd(USART1, ENABLE);

    // ensure USART1 interrupts are off until we have data
    USART_ITConfig(USART1, USART_IT_TXE, DISABLE);

    // prepare the buffer
    SERLCD_Buffer_Init();
}

void SERLCD_DeInit()
{
    // disable the interrupts
    USART_ITConfig(USART1, USART_IT_TXE, DISABLE);

    // free the buffer
    SERLCD_Buffer_Free();
}

uint16_t SERLCD_IsBufferFull()
{
    return SERLCD_Buffer_IsFull();
}

uint16_t SERLCD_WriteMessage(char* text, uint16_t length)
{
    // index into the character array
    uint16_t i = 0;

    // return value
    uint16_t rv = SERLCD_WriteOk;

    // write until we get to the end of the
    // text or overrun the buffer
    while (i  0)
    {
        // enable the interrupt to send the message
        USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
    }

    return rv;
}

/*
* Handles all interrupts for USART1.
*/
void USART1_IRQHandler(void)
{
    // is this interrupt telling us that we can send a new character?
    if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
    {
        // is there something for us to read?
        if (SERLCD_Buffer_IsEmpty())
        {
            // no, disable the interrupt
            USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
        }
        else
        {
            // yes, get the next character from the buffer
            char c = 0x00;
            SERLCD_Buffer_Read(&amp;c);

            // send it to the device
            USART_SendData(USART1, c);
        }
    }
}

Giving the driver a workout

In order to give the new driver a bit of a workout I set up a SysTick interrupt handler running every 10ms and added some code to display messages in one second intervals after an initial gap that gives the SerLCD time to display its splash screen.

The updated main method is as below.

int main(void)
{
    /**
    * IMPORTANT NOTE!
    * The symbol VECT_TAB_SRAM needs to be defined when building the project
    * if code has been located to RAM and interrupts are used.
    * Otherwise the interrupt table located in flash will be used.
    * See also the  file and how the SystemInit() function updates
    * SCB-&gt;VTOR register.
    * E.g. SCB-&gt;VTOR = 0x20000000;
    */

    /* initialise SysTick with a 10ms period */
    RCC_ClocksTypeDef RCC_Clocks;
    RCC_GetClocksFreq(&amp;RCC_Clocks);
    SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);

    /* initialise the SerLCD */
    SERLCD_Init();

    while (1){}
}

The rest of the code went into the stm32f4xx_it.c file with the rest of the interrupt handlers. Firstly I added a new method to choose a message to send and call the SERLCD_WriteMessage method on the driver.

void DisplayMessage(void)
{
    /* this variable holds the next message to display */
    static int16_t NextMessage = 0;
    
    switch (NextMessage)
    {
    case 0:
        SERLCD_WriteMessage(" Here I am ", 16);
        break;

    case 1:
        SERLCD_WriteMessage("Rock you like a ", 16);
        break;

    case 2:
        SERLCD_WriteMessage(" Hurricane! ", 16);
        break;
    }
    
    /* update to point at the next message */
    NextMessage = (NextMessage + 1) % 3;
}

Finally I updated the SysTick_Handler method as follows.

void SysTick_Handler(void)
{
    // set up my initial state. I want to wait 5 seconds for the
    // splash screen to clear before sending any data
    static uint16_t WaitingForSplash = 1;
    static int16_t Countdown = 500;
    
    Countdown--;
    
    if (Countdown &lt;= 0)
    {
        /* are we waiting for the splash screen? */
        if (WaitingForSplash)
        {
            /* yep, switch off the splash wait */
            WaitingForSplash = 0;
        }

        /* display the message and reset the count down to 1 sec */
        DisplayMessage();
        Countdown = 100;
    }
}

If you flash the board with the updated code you should now see the SerLCD splash screen displayed followed by lyrics from a song by one of the most awesome rock bands that ever was (I've seen them twice including once performing 'The Wall' with the mighty Roger Waters in Berlin!)

So there we have it. A functioning, re-usable SerLCD driver that can be re-used in other projects. The only problem with it now though is that simply displaying lines of text isn't exactly inspiring. In the next post I'll take things a bit further and see if we can get things looking a little more exciting. We'll also take a look at some of the control codes and see if we can integrate those into the driver in a way that's a bit more user friendly.

As always, if you have any questions, comments or suggestions please feel free to let me know. All comments are gratefully received.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: