Interfacing an LCD to the STM32F4 Discovery – Part 2: Using Interrupts

Interrupt Driven Display

Interrupt Driven Display

One day when I was in my mid teens holed away in the school computer room getting to grips with programming on BBC Micros, I was sat there working on a new program when I heard music coming from one of the other machines. It was coming from a computer that was being used by Nick Waterman, one of the other guys who was well into programming and now is a top geek working in R&D at Research in Motion. Nick was a year ahead of me in school but way ahead in terms of what he knew about coding. He was one of the first of us to get into assembler and a “go to” guy if you had a question before the days of the internet.

Now it wasn’t unusual to hear music coming out of BBC Micros, at the time it was considered pretty cool to be able to do that (this was the 80’s after all!) but what was really cool in this case was that while the music was playing in the background, the computer was sat there blinking away like it wasn’t doing anything at all.

“How the hell did you do that?!”, I asked.
“Ah, it’s interrupt driven music.”, Nick replied.

Whoa, that was a new one to me. I had no idea what that meant but it sounded really cool.

“So hang on”, I replied, “does that mean you can just let it run in the background while you’re running another program?”
“Yep.”

I was completely taken by this idea. Over the course of the next few weeks I went out and found every book I could that talked about programming and interrupts (which wasn’t many given I only had access to the school library!) What I learned was that the BBC Micro interrupts are all vectored through a set of pointers held in memory and that you could, if you were careful, repoint them at your own routines in order to execute code tucked away in memory. I learned about Interrupt Service Routines, pushing register values onto the stack and then passing control back to the system via the original vector which I had previously squirrelled away.

Well those principles that applied in the old 8 bit days still hold true today and while things are a bit more complex 25 years on, the interrupts on the STM32F4 MCU are vectored through memory in pretty much the same way.

What this means is that we can take effort away from the main program loop and let our Interrupt Service Routines handle the grunt work of, for example, sending a buffer load of character data to a SerLCD device. To do this requires a bit of refactoring though.

I decided to approach the refactoring in two stages to make things easier to control. The first stage was to refactor the code to use the USART1 interrupt handler that would check to see if the USART1 TX register was ready to accept a new character for sending and if so to send it. The second stage was to refactor the SerLCD code into a separate set of files so I can re-use it in other projects more easily.

Stage 1 – Getting the Interrupts Working

The first thing I did was to move the declaration of the message text outside of the main function so that it can be accessed from everywhere in the program.

Next I added a new init structure for the interrupt configuration underneath he ones I already declared. I then called the ITConfig method for USART1 after the command to enable it, deleted all the previous code to write the message and finally I added the new method to handle the interrupts on USART1.

The resulting code for main.c was as shown below.

/**
*****************************************************************************
**
** File : main.c
**
** Abstract : main function.
**
** Functions : main
**
** Environment : Atollic TrueSTUDIO(R)
** STMicroelectronics STM32F4xx Standard Peripherals Library
**
** Distribution: The file is distributed “as is,” without any warranty
** of any kind.
**
** (c)Copyright Atollic AB.
** You may use this file as-is or modify it according to the needs of your
** project. This file may only be built (assembled or compiled and linked)
** using the Atollic TrueSTUDIO(R) product. The use of this file together
** with other tools than Atollic TrueSTUDIO(R) is not permitted.
**
*****************************************************************************
*/

/* Includes */
#include "stm32f4xx.h"
#include "stm32f4_discovery.h"
#include <string.h>

/* Private macro */
/* Private variables */

// something to display
char* messageText = "Testing Part 2";

/* Private function prototypes */
/* Private functions */

/**
**===========================================================================
**
** Abstract: main program
**
**===========================================================================
*/
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 <system_*.c> file and how the SystemInit() function updates
    * SCB->VTOR register.
    * E.g. SCB->VTOR = 0x20000000;
    */

    // 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, &GPIO_InitStruct);

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

    // fill in the interrupt configuration (priority 0 is the highest priority)
    NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&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, &USART_InitStruct);

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

    // configure USART1 to use interrupts
    USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

    // Infinite loop, note that I'm not explicitly calling a display method
    while (1){}
}

/*
* Handles all interrupts for USART1.
*/
void USART1_IRQHandler(void)
{
    // index into the display string
    static int i = 0;

    // is this interrupt telling us that we can send a new character?
    if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
    {
        // yes, send the next character in the buffer
        USART_SendData(USART1, messageText[i++]);
        if (i >= strlen(messageText))
        {
            // disable the interrupts (or we'll go round again!)
            USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
        }
    }
}

/*
* Callback used by stm32f4_discovery_audio_codec.c.
* Refer to stm32f4_discovery_audio_codec.h for more info.
*/
void EVAL_AUDIO_TransferComplete_CallBack(uint32_t pBuffer, uint32_t Size){
    /* TODO, implement your code here */
    return;
}

/*
* Callback used by stm324xg_eval_audio_codec.c.
* Refer to stm324xg_eval_audio_codec.h for more info.
*/
uint16_t EVAL_AUDIO_GetSampleCallBack(void){
    /* TODO, implement your code here */
    return -1;
}

This is a lot better now because apart from the code that configures it all, the work is all done as and when the USART is ready for more data. This means we can either do other work in the main loop or in a more sophisticated application we could put the Discovery to sleep which would reduce power consumption of the unit.

Stage 2 – Making the code re-usable

The next thing I wanted to do was to refactor the code that interfaces to the SerLCD into a separate code file so I can re-use it in other applications. To do this I created a new .h and .c file to hold the code and copied the relevant sections across.

To do this I took the following steps:
– I created a new Library folder called STM32F4xx_ExtPeriph_Driver (an attempt to match the existing naming structure!)
– I added a new ‘inc’ and ‘src’ folder underneath to hold the new files
– I created new files called stm32f4xx_serlcd.h and stm32f4xx_serlcd.c in the relevant folders

As I had added a new location for header files, I needed to update my build settings so that the compiler could find it. In the Atollic environment I did this by clicking on the ‘Project’ menu and selecting ‘Build Settings…’. I then selected the ‘Tool Settings’ tab and selected ‘Directories’ under ‘C Compiler’ which gave me a list of include paths. From there I was able to add in the new workspace folder. This all seemed to work fine when I eventually came to do the build.

The next thing I did was to create a new method called SERLCD_Init() which was declared in stm32f4xx_serlcd.h and copied the initialisation code into it. In the main.c I replaced that code with a call to the new method. The other thing I did was copy the interrupt handler into the new .c file so that everything is held in one convenient place.

The updated main.c file ended up looking like this:

/**
*****************************************************************************
**
** File : main.c
**
** Abstract : main function.
**
** Functions : main
**
** Environment : Atollic TrueSTUDIO(R)
** STMicroelectronics STM32F4xx Standard Peripherals Library
**
** Distribution: The file is distributed “as is,” without any warranty
** of any kind.
**
** (c)Copyright Atollic AB.
** You may use this file as-is or modify it according to the needs of your
** project. This file may only be built (assembled or compiled and linked)
** using the Atollic TrueSTUDIO(R) product. The use of this file together
** with other tools than Atollic TrueSTUDIO(R) is not permitted.
**
*****************************************************************************
*/

/* Includes */
#include "stm32f4xx.h"
#include "stm32f4_discovery.h"
#include "stm32f4xx_serlcd.h"

/* Private macro */
/* Private variables */
/* Private function prototypes */
/* Private functions */

/**
**===========================================================================
**
** Abstract: main program
**
**===========================================================================
*/
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 <system_*.c> file and how the SystemInit() function updates
    * SCB->VTOR register.
    * E.g. SCB->VTOR = 0x20000000;
    */
    // initialise the SerLCD
    SERLCD_Init();
    
    // Infinite loop
    while (1){}
}

/*
* Callback used by stm32f4_discovery_audio_codec.c.
* Refer to stm32f4_discovery_audio_codec.h for more info.
*/
void EVAL_AUDIO_TransferComplete_CallBack(uint32_t pBuffer, uint32_t Size){
    /* TODO, implement your code here */
    return;
}

/*
* Callback used by stm324xg_eval_audio_codec.c.
* Refer to stm324xg_eval_audio_codec.h for more info.
*/
uint16_t EVAL_AUDIO_GetSampleCallBack(void){
    /* TODO, implement your code here */
    return -1;
}

The new header file for the SerLCD driver is as follows:

/**
******************************************************************************
* @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 functions --------------------------------------------------------*/
/* Initialization and Configuration functions *********************************/
void SERLCD_Init();

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

Finally the new stm32f4xx_serlcd.c driver code file is as follows.


/**
 ******************************************************************************
 * @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 and display the test message.

 @endverbatim
*/

/* Includes ------------------------------------------------------------------*/
#include "stm32f4xx_serlcd.h"
#include "string.h"

// Test message to display
char* messageText = " Testing with interrupts ";

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, &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(&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, &USART_InitStruct);

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

    // configure USART1 to use interrupts
    USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
}

/*
* Handles all interrupts for USART1.
*/
void USART1_IRQHandler(void)
{
    // index into the display string
    static int i = 0;

    // is this interrupt telling us that we can send a new character?
    if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET)
    {
        // yes, send the next character in the buffer
        USART_SendData(USART1, messageText[i++]);
        
        if (i >= strlen(messageText))
        {
            // disable the interrupts (or we'll go round again!)
            USART_ITConfig(USART1, USART_IT_TXE, DISABLE);
        }
    }
}

So there you have it, an interrupt driven SerLCD driver. It’s not very flexible though, it can only display one message and you don’t have any control over when it happens. Not very flexible if you want to re-use it in another application! I’ll start to deal with that in the next instalment. Meanwhile if you have any questions or feedback please feel free to comment below, it’s all 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: