Getting Started with PICs

Getting Started with PICs

At the end of the last post I had managed to cobble something together that replicates the demo code that the PIC on the PICkit 2 demo board comes with complete with chasing LEDs, direction changing and speed control. Having done that it occurred to me that one thing I’d really like to try out is to see if I could get the LEDs to dim using the PIC’s onboard PWM control capabilities. I had a quick check of the data sheet and luckily enough, three of the four PWM outputs happen to be multiplexed with port D GPIO which means the demo board LEDs can be driven from them. Coincidence? Dunno but it made me happy anyway!

Okay so let’s back up a here and get a little perspective on this whole thing by tackling two fairly obvious questions that might pop up if you’re fairly new to this stuff. Firstly, what’s PWM and secondly why do you need it to dim LEDs?

Starting with the second question first, LED brightness can be controlled using variable voltages much like a normal tungsten bulb but in digital circuits signals are either on or off so in order to dim a LED in a digital circuit we need to find another way. This is where PWM (Pulse Width Modulation) come a long and gives us a big wet, sloppy kiss on the lips.

The way this works is basically to say that if you rapidly switch something on and off, say for example an electric motor, it’s a bit like running it at a lower voltage and how fast the motor will go depends on the amount of time it is switched on when compared to the amount of time it’s switched off. The terminology can be seen in the diagram below which comes from the PIC data sheet.

PWM Terminology

PWM Terminology

The total length of time for an on / off cycle is called the ‘period’ and this can also be expressed as a frequency which is 1/period. The amount of time that the circuit is switched on for during the period is called the ‘duty cycle’ and this is where the work gets done, motors get turned and LEDs light up. The stuff on there about timer 2 should become clearer later on.

So how is all this implemented in a PIC16F887 then? Well the PWM functionality comes as part of the ‘Capture Compare PWM’ (CCP) modules of which there are two in this particular PIC. These are cunningly named CCP1 and CCP2 and the one I chose to use was CCP1 because it’s the one that connects to the LEDs.

Configuring the PWM peripheral consists of setting the period, configuring how the duty cycle will be implemented (e.g. active high, active low and on which pins), setting the duty cycle and configuring the timer that’ll keep the whole thing going.

Easy peasy right? Err sort of!

Okay so the next thing to do was to fire up MPLAB and create a new project. After creating the project I grabbed the code from the previous PIC post as a starting point and deleted the button code. The ADC code I kept because I wanted to use it to control the LED dimming. I also deleted out the code that did the chasing so I ended up with an empty while loop. While I was in there I refactored the button initialisation code into a new function to get it out of the way. Basically all that was left in my main function was the oscillator configuration, a call to initialise the variable resistor ADC and an empty while loop.

Once I did that I used a little trick that I use a lot when I have a long and involved procedure or algorithm I need to work through and that is to add in all the comments first and add the code afterwards. The reason I sometimes do this is twofold. Firstly it means I can sketch out everything that needs to happen in comments before I get bogged down in how to actually get it to happen in that language on that platform which means I don’t forget anything. Secondly it allows me to tackle the code in any order I want to so i can start out by filling in the easy stuff and work through the complex stuff later on.

The data sheet helps us out a lot if we take that approach because it documents the exact initialisation procedure in chapter 11.5.7 Setup for PWM Operation. Hurrah! I copied the text out of the data sheet, pasted it into the code editor and converted each step into a comment. Now I just needed to fill in the blanks.

The first step is to temporarily set the pins we’re outputting to as inputs. The PWM output can go to four pins called P1A, P1B, P1C and P1D which are multiplexed with RC2, RD5, RD6 and RD7 respectively. Incidentally, the reason I elected to use CCP1 is because I needed to get output to the pins with LEDs attached and that can be done by using a trick called ‘Pulse Steering Mode’. Anyhow, as I had pins on both port B and port C and I’m not using them for anything else, I simply set them all to input.

    // Disable the PWM pin (CCPx) output drivers as an input by setting
    // the associated TRIS bit.
    TRISC = 0xff;  // blat the lot, we're not using them
    TRISD = 0xff;  // for anything else

The next job is to set the period. This is a bit of an awkward process because the period has dependencies not only on the period register (PR2) but also on the oscillator frequency and any pre-scaling on timer 2 that may or may not be set up. There is a full discussion including equations for calculating everything you need in the data sheet in section 11.5.1, 11.5.2 and 11.5.3. There are also a few examples in table 11-4 which I used as a starting point for subsequent tweakery.

    // Set the PWM period by loading the PR2 register.
    PR2 = 0x40; // this combined with a TMR2 prescale of 16 gives us
                // a PWM frequency of just under 2kHz

The value 0x40 is decimal 64 which is a quarter of 255, the maximum value for a single byte such as the one returned by my variable resistor reading function. If you check out equation 11-3 you’ll see that to get a fully on signal you need to supply a value for the duty cycle that is four times the value in the PR2 register. I ran through the calculations to see what my period frequency would end up as and it came out at just under 2KHz which seemed pretty good to me.

The next thing to do is to configure the CCP module so that it knows what kind of output you’re after. As I wanted to use pulse steering mode to direct the same output to RD5-7 I needed to use single output mode and that’s configured with the P1M bits in the CCP1CON register. I also set the extended CCP mode bits to tell that I wanted all output to be active high and finally set the pulse steering mode register PSTRCON to send output to the three LEDs.

    // Configure the CCP module for the PWM mode by loading the CCPxCON
    // register with the appropriate values.
    CCP1CONbits.P1M = 0b00;     // Single output mode
    CCP1CONbits.CCP1M = 0b1100; // ECCP Mode PWM P1A-D active high
    PSTRCON = 0b00011110;       // set up pulse steering mode on port D pins

Next I loaded up the duty cycle which is a 10bit value so it’s stored across a couple of registers. The least significant bits are held in the DC1B bits of the CCP1CON register if you’re using CCP1 and the most significant bits are written to CCPR1L.

    // Set the PWM duty cycle by loading the CCPRxL register and DCxB
    // bits of the CCPxCON register.
    CCP1CONbits.DC1B = 0x80 & 0b11;
    CCPR1L = 0x80 >> 2; // 0x80 is approx 50% duty cycle... I think!

In the code above I took an initial duty cycle value 0x80, masked off the two LSBs and wrote them to DC1B. I then shifted the same initial duty cycle value, shifted right by two places (they’re written to the other register) and wrote the result to the CCPR1L register.

The next job is to configure timer 2 which is going to drive the whole thing. The timer speed depends on the oscillator frequency and you can vary it by using pre-scalers and post-scalers. As mentioned above I used a pre-scaler value of 16 which is configured by setting the T2CKPS bits in the T2CON register to 0b10.

    // Configure and start Timer2:
    //  • Clear the TMR2IF interrupt flag bit of the PIR1 register.
    PIR1bits.TMR2IF = 0;

    //  • Set the Timer2 prescale value by loading the T2CKPS bits of the
    //    T2CON register.
    T2CONbits.T2CKPS = 0b10; // 16

    //  • Enable Timer2 by setting the TMR2ON bit of the T2CON register.
    T2CONbits.TMR2ON = 1;

Once all that is done the timer is switched on but we need to wait until a new PWM cycle has started before carrying on.

    // Enable PWM output after a new PWM cycle has started:
    //  • Wait until Timer2 overflows(TMR2IF bit of the PIR1 register is set).
    while(PIR1bits.TMR2IF == 0){}

Finally we let the output flow out of the pins.

    //  • Enable the CCPxp in output driver by clearing the associated
    //    TRIS bit.
    TRISD = 0;

One last thing I did was to set the LED adjacent to the dimmed ones to on so that I could compare brightness levels and get some idea of how well it was working.

    // switch on one of the adjacent LEDs for brightness comparison
    PORTD = 0b00010000;

Then of course you have your empty, infinite while loop.

If you run the code at this point the LEDs on RD5-7 display but are clearly dimmer than the one that is fully on which is great, it means the thing actually works. The next step though is to get that variable resistor into the mix so we can use that to set the amount of dimming.

Well we already have the code to get a byte value from the variable resistor and we already have the code to set the duty cycle. All we have to do is combine it together.

    char duty = 0;  // the duty cycle value derived from the VR value

    // loop forever
    while(1)
    {
        // read an 8-bit value from the VR
        duty = readVR();

        // set the new duty cycle
        CCP1CONbits.DC1B = duty & 0b11; // LSB
        CCPR1L = duty >> 2;             // MSB
    }

I then built the code, connected my PICKit 2 and demo board and gave it a spin. Sure enough it all seemed to work just fine. Turning the variable resistor allows a full range of dimming control from completely off to completely on.

#include <pic.h>
#include <pic16f887.h>

// PIC16F887 Configuration Bit Settings

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RE3/MCLR pin function select bit (RE3/MCLR pin function is MCLR)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown Out Reset Selection bits (BOR enabled)
#pragma config IESO = ON        // Internal External Switchover bit (Internal/External Switchover mode is enabled)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is enabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

#define _XTAL_FREQ 8000000

void initVR()
{
    // configure RA0 as analog AN0
    ADCON1bits.ADFM = 0; // left justified result
    ADCON1bits.VCFG0 = 0; // use VDD as reference voltage
    ADCON1bits.VCFG1 = 0; // use VCC as reference gnd

    TRISA = 1;
    ANSELbits.ANS0 = 1;

    ADCON0bits.ADCS = 0b10; // clock / 32
    ADCON0bits.CHS = 0;     // AN0 selected
    ADCON0bits.ADON = 1;    // turn on the ad converter
}

char readVR()
{
    ADCON0bits.GO = 1;
    while(ADCON0bits.GO == 1){}
    return ADRESH;
}

void main()
{
    // set up the oscillator to run at 8MHz
    OSCCONbits.IRFC = 0b111;

    // init the ADC to read the VR
    initVR();

    // Disable the PWM pin (CCPx) output drivers as an input by setting
    // the associated TRIS bit.
    TRISC = 0xff;  // blat the lot, we're not using them
    TRISD = 0xff;  // for anything else

    // Set the PWM period by loading the PR2 register.
    PR2 = 0x40; // this combined with a TMR2 prescale of 16 gives us
                // a PWM frequency of just under 2kHz

    // Configure the CCP module for the PWM mode by loading the CCPxCON
    // register with the appropriate values.
    CCP1CONbits.P1M = 0b00;     // Single output mode
    CCP1CONbits.CCP1M = 0b1100; // ECCP Mode PWM P1A-D active high
    PSTRCON = 0b00011110;       // set up pulse steering mode on port D pins

    // Set the PWM duty cycle by loading the CCPRxL register and DCxB
    // bits of the CCPxCON register.
    CCP1CONbits.DC1B = 0x80 & 0b11;
    CCPR1L = 0x80 >> 2; // 0x80 is approx 50% duty cycle... I think!

    // Configure and start Timer2:
    //  • Clear the TMR2IF interrupt flag bit of the PIR1 register.
    PIR1bits.TMR2IF = 0;

    //  • Set the Timer2 prescale value by loading the T2CKPS bits of the
    //    T2CON register.
    T2CONbits.T2CKPS = 0b10; // 16

    //  • Enable Timer2 by setting the TMR2ON bit of the T2CON register.
    T2CONbits.TMR2ON = 1;

    // Enable PWM output after a new PWM cycle has started:
    //  • Wait until Timer2 overflows(TMR2IF bit of the PIR1 register is set).
    while(PIR1bits.TMR2IF == 0){}

    //  • Enable the CCPxp in output driver by clearing the associated
    //    TRIS bit.
    TRISD = 0;

    // switch on one of the adjacent LEDs for brightness comparison
    PORTD = 0b00010000;

    char duty = 0;  // the duty cycle value derived from the VR value

    // loop forever
    while(1)
    {
        // read an 8-bit value from the VR
        duty = readVR();

        // set the new duty cycle
        CCP1CONbits.DC1B = duty & 0b11; // LSB
        CCPR1L = duty >> 2;             // MSB
    }
}

The full, final code is above if you fancy picking through it (no pun intended). I hope there’s something vaguely useful in there. As always questions, comments and suggestions are more than welcome otherwise feel free just to say hi.

Getting Started with PICs

Getting Started with PICs

In my last PIC post I wrote about how to get the LEDs on the PICKit 2 demo board to chase by configuring the clock and using the delay macro to update the GPIO port. I also added some code to read the button and reverse the direction of the chasing mimicking the behaviour of the program on the PIC that is provided with the board. The other feature that program has though is for the speed of the chasing LEDs to be varied by turning the variable resistor.

PIC Demo Board Input

PIC Demo Board Input

This is a classic job for an Analog to Digital Converter (ADC) and these work by sampling a voltage and converting it to a number that can be used to represent some kind of quantity in the code. Before going ahead and getting the code going though, we need to go back to the demo board data sheet to find out where and how the variable resistor is connected to the PIC.

The pin we’re interested in is RA0 which is multiplexed with AN0, our analog input. Input from the variable resistor will range from +5V (VDD) to 0V (VCC) as I’m running the board at 5V. Next it’s off to the PIC16F887 data sheet to figure out how to get this thing done.

The first thing to do is to perform some configuration. We need to tell the PIC what the input range is going to be and how we’d like the 10 bit result laid out in the two 8 bit registers it’ll be written to.

    // configure RA0 as analog AN0
    ADCON1bits.ADFM = 0; // left justified result
    ADCON1bits.VCFG0 = 0; // use VDD as reference voltage
    ADCON1bits.VCFG1 = 0; // use VCC as reference gnd

I chose to left justify the result because I wanted to turn the value into a single byte that spanned the full range of the variable resistor. Using the highest 8 bits of the result gives me that and I can simply ignore the two least significant bits. The other two values tell the ADC that I’ll be using an input voltage range between VDD and VSS (GND). For details on the other options you’ll need to refer to the data sheet. This is probably a good point to remind anyone out there that while clearly I have no issue whatsoever with anyone pinching this code (or it wouldn’t be here!) this is code for the PIC16F887 and things can be different on other PICs. Learn to love those data sheets!

Next I set up the RA0 / AN0 pin so that it acts as an analogy input.

    TRISA = 1;
    ANSELbits.ANS0 = 1;

This is done by configuring the TRISA register so that bit 0, which maps to the pin, is set to input and then make sure it’s selected in the ANSEL register. It should be set anyway after a power on but this way we can be sure…

In the final part of the configuration I had to do some fiddling with the clock because I’m running at 8MHz and the ADC can’t go that fast. Dividing the clock by 32 puts us inside the recommended range of values.

    ADCON0bits.ADCS = 0b10; // clock / 32
    ADCON0bits.CHS = 0;     // AN0 selected
    ADCON0bits.ADON = 1;    // turn on the ad converter

Next I connect the AN0 channel to the ADC. I did this as part of the configuration stage because I’m only using one channel so there’s no need to keep changing it. Finally the ADC is switched on.

So that’s the setup work done, the next thing is to take the readings from the ADC. I created a separate function to do this so I can re-use it more easily.

char readVR()
{
    ADCON0bits.GO = 1;
    while(ADCON0bits.GO == 1){}
    return ADRESH;
}

To start a conversion all you have to do is to set the ‘GO’ bit in the ADCON register and wait until it gets cleared after the conversion is complete. The result is then stored as a 10 bit number across the ADRESH and ADRESL registers. All I need are the top 8 bits so I grab them from ADRESH and return the byte. That byte is then used in a delay in the chase code to provide a delay of between 0 and 255 milliseconds which is good enough for me.

The final code is listed below:

#include <pic.h>
#include <pic16f887.h>

// PIC16F887 Configuration Bit Settings

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RE3/MCLR pin function select bit (RE3/MCLR pin function is MCLR)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown Out Reset Selection bits (BOR enabled)
#pragma config IESO = ON        // Internal External Switchover bit (Internal/External Switchover mode is enabled)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is enabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

#define _XTAL_FREQ 8000000

int buttonPressed()
{
    // flag to tell us when the button is
    // being pressed
    static int buttonDown = 0;

    // is the button being pressed?
    if (PORTBbits.RB0 == 0)
    {
        // yes, set the flag
        buttonDown = 1;
    }

    // has the button been released?
    if (PORTBbits.RB0 == 1 && buttonDown == 1)
    {
        // yes, clear the flag
        buttonDown = 0;

        // tell the calling method
        return 1;
    }

    return 0;
}

char readVR()
{
    ADCON0bits.GO = 1;
    while(ADCON0bits.GO == 1){}
    return ADRESH;
}

void main()
{
    // set up the oscillator to run at 8MHz
    OSCCONbits.IRFC = 0b111;

    // configure port d as output
    TRISD = 0;

    // Configure bit 0 (AN12) on PORTB as a digital input
    PORTB = 1;
    ANSELHbits.ANS12 = 0;
    TRISB = 0b00000001;

    // configure RA0 as analog AN0
    ADCON1bits.ADFM = 0; // left justified result
    ADCON1bits.VCFG0 = 0; // use VDD as reference voltage
    ADCON1bits.VCFG1 = 0; // use VCC as reference gnd

    TRISA = 1;
    ANSELbits.ANS0 = 1;

    ADCON0bits.ADCS = 0b10; // clock / 32
    ADCON0bits.CHS = 0;     // AN0 selected
    ADCON0bits.ADON = 1;    // turn on the ad converter

    int d = 1;  // sets the direction of the chasing
    int i = 0;  // the index of the led that will be lit

    // loop forever
    while(1)
    {
        // if the button is pushed...
        if (buttonPressed() == 1)
        {
            // ...reverse the direction
            d = -d;
        }

        // select the next LED wrapping around
        // if required
        i = (i + d) % 8;

        // light up the correct LED
        PORTD = 1 << i;

        // read a delay value from the variable resistor
        // analog channel
        int del = readVR();

        // do the delay
        for (int l=0; l<del;l++)
        {
            __delay_ms(1);
        }
    }
}

So that’s that then, we have a version of the demo program that chases the LEDs, allows you to swap the direction of the chasing and also change the speed. I’m sure we can get more out of this board than that though but that can be a subject for another post.

As always I’d be really interested to hear your feedback. Feel free to post comments, suggestions, errors etc, they are gratefully received.

Nothing kills innovation quite like the words, “That’s all very well but let’s be realistic here!”
Nothing kills a project quite like unrealistic expectations and requirement creep.

So does this represent an impossible dichotomy that acts to prevent us from building the innovative new applications that all engineering businesses need to stay on top of their game? Well, a lot of the time yes it does. It can seem as though the choices are either to take a risk, bite off more than you can chew and deal with escalating costs, requirement creep and late delivery in order to get ahead of the market or, like a tortoise peeking out and sensing impending danger, tuck your head back under the shell of your technical comfort zone and hope you can hold out for a little longer in a rapidly changing ecosystem.

Of the two options I can understand why the second may seem more attractive but the reality is that the longer that carries on, the further and further behind the rest of the world you get and the more risk you’ll need to take in order to re-establish yourself later on. But hey, no-one wants that kind of risk on their watch right? Leave it to the next person.

I’ve recently adopted a different view though. I think you can have both and that key to the whole thing is the ability to clearly differentiate between ideas and commitments when talking to customers about their needs.

Over the last few weeks I have been involved in a series of customer workshops in a sort of combined role of business analyst and system architect. It’s clear from the brief we’ve been given that the customer is very keen to see innovation in any solution they will end up buying but at the same time, delivery timescales are very short. At one point during the discussion I was talking to some of the users about some of the cutting edge things the solution we were proposing could deliver when I heard the fateful words, “That’s all very well but let’s be realistic here!”

Talk about throwing a bucket of cold water over proceedings!

The comment actually really irritated me. The reason for this was that we had clearly been talking about possibilities, exploring ideas to see if there was any mileage in them. I was trying to figure out what excited them, what was important to them and how new features might impact on the way they currently work. In that moment I realised that while most of the room knew and understood what was going on, it was clear that many didn’t. Worse than that, the ones that didn’t seemed to believe I was committing to features in the end solution. A situation ripe for confusion later on!

Later on as I sat back down after the discussion I started to mull over what had happened in that moment. Why had some of them not realised that we had been in ‘exploration’ mode? After all, the whole point of the workshop was to poke at the limits of the requirements and find out where they were. One thing was for sure, no-one was going to dare to suggest new ideas now. Suddenly the project had nothing exciting in it anymore.

It occurred to me that while I had been speaking, my mind had been jumping between several different modes and I couldn’t even remember at what points I had switched from one to another. No surprise then that I hadn’t brought everyone in the room along with me. If they had snoozed even for a moment (the view from the top floor meeting room was quite impressive too) or they didn’t understand some of the deeper techie stuff we’d been discussing, they could quite easily have been left behind.

As a long time geeky kind of a chap, I began to think about these different modes as a set of states in a finite state machine. I thought about how the conversation had traversed these various states and what those states might be. I came up with three and popped them into a note on my iPhone.

Stages of Requirement Capture

Stages of Requirement Capture

What’s possible?
What’s important?
What’s feasible?

When thinking about what’s possible you’re thinking innovatively.

“Technology is now at this point here and our systems currently look like this but imagine if…”

This is the ‘opening up’ part of the discussion whereas the others are the ‘nailing down’ parts. When thinking about what’s important we take the ideas and concepts discussed in the ‘what’s possible’ phase and start to focus in on the ones that make the most difference to the customer. Having figured out what’s important we then need to look at our constraints and build a project around what’s feasible. This is where we finally nail down the project scope.

It seems to me that if you want to create something great then a discussion like this must include all of these things. By dwelling on what’s feasible you close yourself off to innovation or what the users might really need. By dwelling on what’s possible you can lose track of what’s important and by dwelling on what’s important you can become too bogged down in the status quo and shut yourself off from new ideas that could offer real benefits to the customer.

The trick then has to be in finding a way to let other participants know where the discussion is at any particular moment in time. The next challenge for me is to find a way in which to do that. Perhaps some kind of discussion board divided into three areas could be used and ideas written on sticky notes placed on it. Perhaps a simple diagram showing the three states and a sticky movable arrow that can be pointed at the one describing where the conversation currently is. Something to explore I think. Please feel free to let me know if you have any ideas!

So how does this help with keeping your technology business alive and on the cutting edge? Well I believe that this creates a framework that allows us to engage in new thinking without the fear that it will get out of control because everybody involved knows whether the discussion is about something exploratory or not and that it doesn’t matter if the ideas may seem a little trivial at the time because the subsequent phases of the discussion will filter these into what is important and finally what is feasible. Good innovation will make it through all of these filters and what you hopefully end up with something that combines the best of the established with the best of the new.

Getting Started with PICs

Getting Started with PICs

In the last PIC post I described how I got started with my PICKit 2 and MPLAB toolchain and got something running on the demo board. The next thing I wanted to do was to replicate the program that the demo board PIC comes with. This program makes the LEDs chase with the speed determined by the variable resistor and the direction determined by the push button. I decided to start with the chasing LEDs as this is a simple GPIO exercise.

PIC Demo Board LEDs

PIC Demo Board LEDs

Before I could get started with this job however, I realised that I was going to need a delay function of some kind. If you include pic.h then one is available to you. Well two actually, you have __delay_ms() and __delay_us(). Both of these take an argument which is cast to an unsigned long with __delay_ms() giving you a delay measured in milliseconds and __delay_us() giving you a delay in microseconds. For these to work properly though you need to define a value called _XTAL_FREQ which is the speed of your clock. This means you need to know your clock speed which in turn means you need to tell the PIC what you want the speed to be.

To keep things simple I’ve used the internal PIC clock. On the PIC16F887 the internal clock can run at several different speeds. These are described in the data sheet in the oscillator overview section and range from 31KHz through to 8MHz. While there is nothing about this application that really warrants it, I decided to go for broke and run the thing at the full 8MHz. You do this from inside the program by changing the three IRFC bits in the OSCCON register. The options are documented in the data sheet in figure 4.1. Using the OSCCONbits structure defined in PIC16f887.h makes this very easy.

// set up the oscillator to run at 8MHz
    OSCCONbits.IRFC = 0b111;

This gives us the basic tools to get the chasing working, the only other thing is to find a neat way to get the LEDs to light up in turn. To do this I simple held an LED index that is incremented in the main loop and use that to shift a bit through the port register. I used an additional variable (d) to hold the amount to increment the index (i) with rather than just using the constant value 1. The reason for this was to set up the code ready to chase in either direction later on.

The simple chasing code is as below:

#include <pic.h>

// PIC16F887 Configuration Bit Settings

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RE3/MCLR pin function select bit (RE3/MCLR pin function is MCLR)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown Out Reset Selection bits (BOR enabled)
#pragma config IESO = ON        // Internal External Switchover bit (Internal/External Switchover mode is enabled)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is enabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

#define _XTAL_FREQ 8000000

void main()
{

    // set up the oscillator to run at 8MHz
    OSCCONbits.IRFC = 0b111;

    // configure port d as output
    TRISD = 0;
    
    int d = 1;  // sets the direction of the chasing
    int i = 0;  // the index of the led that will be lit

    // loop forever
    while(1)
    {
        // select the next LED wrapping around
        // if required
        i = (i + d) % 8;

        // light up the correct LED
        PORTD = 1 << i;

        __delay_ms(100);
    }
}

The next thing to do was to add in the code to deal with the button press. When the user presses the button the LED chasing needs to switch direction. This is also a pretty simple GPIO job with the difference being that this time we use the GPIO as an input rather than an output.

PIC Demo Board Button

PIC Demo Board Button

As can be seen in the circuit diagram above taken from the demo board data sheet, the button is connected to RB0. When the switch is open, the pin is held high by the 10K resistor R2. Pushing the switch connects it to ground which makes the pin go low. The sequence of events for a button press then is a transition from high to low then from low back to high again. This is seen by the program running on the PIC as a change in state on RB0 from 1 to 0 then back to 1 again.

Ok so we know what the sequence of events is that we’re looking for, the next thing is to find out how to get input from a pin in a PIC program so back to the data sheet, this time section 3.3 where there is a description of port B and how to use it.

First off, we write 1 to PORTB so that RB0 initialises high. The next thing to do is to set up the input as a digital rather than an analog input. The pin RB0 doubles up as AN12 and configuration for AN12 is done using the ANSELH register. To set this pin as a digital input we’ll need to set the AN12 bit to 0. The final thing that needs to happen is that we need to configure RB0 as an input.This is done by writing 1 to the TRISB register. Note that this configuration procedure comes directly from the data sheet. If you’re working with a different PIC make sure you’re going back to the correct data sheet for whatever PIC you’re using and make sure that you know what you need to do.

Once this configuration is complete we’re ready to receive digital input from the push button and this is a simple case of looking for the 1 0 1 sequence. Now there’s something really important to understand here that I’ve seen people new to programming come unstuck on many times. What needs to be implemented here is not a sequence of lines of code that goes something along the lines of:

// wrong approach!!
if (PORTBbits.RB0 == 1) {
     if (PORTBbits.RB0 == 0) {
          If (PORTBbits.RB0 == 1) {
               // do something!
          }
     }
}

If you do that the chances of getting to the ‘do something’ code are very slim indeed! The reason is that the code is running at 8MHz and you’d be mighty lucky to time it just right to get the sequence bang on time to make that code work. You also don’t want to hold up the program while the user has their finger on the button. What you need is to be able to allow the user to hold down and release the button at any time which could be many thousands of times around the ‘while’ loop.

The way to solve this is to think of the program in terms of a state machine. The program can be in one of three states:

1. Initialising which is basically everything before the start of the ‘while’ loop;
2. The user is not pressing the button;
3. The user is pressing the button.

We track the state by using a static state flag called buttonDown and the event that we need to identify is the transition from buttonDown to ‘not’ buttonDown. When the transition is detected, the variable ‘d’ which changes the index of the active LED is negated so it changes from 1 to -1 or -1 to 1 causing the chasing to switch direction.

#include <pic.h>
#include <pic16f887.h> 

// PIC16F887 Configuration Bit Settings

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RE3/MCLR pin function select bit (RE3/MCLR pin function is MCLR)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown Out Reset Selection bits (BOR enabled)
#pragma config IESO = ON        // Internal External Switchover bit (Internal/External Switchover mode is enabled)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is enabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

#define _XTAL_FREQ 8000000

int buttonPressed()
{
    // flag to tell us when the button is
    // being pressed
    static int buttonDown = 0;

    // is the button being pressed?
    if (PORTBbits.RB0 == 0)
    {
        // yes, set the flag
        buttonDown = 1;
    }

    // has the button been released?
    if (PORTBbits.RB0 == 1 && buttonDown == 1)
    {
        // yes, clear the flag
        buttonDown = 0;

        // tell the calling method
        return 1;
    }

    return 0;
}

void main()
{
    // set up the oscillator to run at 8MHz
    OSCCONbits.IRFC = 0b111;

    // configure port d as output
    TRISD = 0;

    // Configure bit 0 (AN12) on PORTB as a digital input
    PORTB = 1;
    ANSELHbits.ANS12 = 0;
    TRISB = 0b00000001;

    int d = 1;  // sets the direction of the chasing
    int i = 0;  // the index of the led that will be lit

    // loop forever
    while(1)
    {
        // if the button is pushed...
        if (buttonPressed() == 1)
        {
            // ...reverse the direction
            d = -d;
        }

        // select the next LED wrapping around
        // if required
        i = (i + d) % 8;

        // light up the correct LED
        PORTD = 1 << i;

        __delay_ms(100);
    }
}

So that’s the chasing working. In the next post I’ll add in the variable resistor so that we can control the speed as well as the direction.

In the meantime, if you have any comments or suggestions please feel free to post below. Feedback is always welcome!

Getting Started with PICs

Getting Started with PICs

I’ve been spending a fair amount of time recently playing with micro controller boards but for some reason I’ve never had a play with PICs. I decided that it was about time to change all that so I finally got around to ordering a PIC programmer, downloaded a copy of Microchip MPLAB and gave it a go.

I bought the programmer from RS Online. It’s a PICkit2 Debug Express and it arrives with a 44-pin demo board complete with a PIC16F887 (not the PIC16F877 advertised!) and a few additional components for experimentation purposes which are LEDs, a push button and a variable resistor.

I’m working on a Mac so I was relieved to discover that the Microchip development environment, the MPLAB IDE, is now available on OS X and Linux as well as Windows. The software is available from the Microchip website and while I was there I also downloaded and installed the pk2cmd utility. You’ll need to scroll to the bottom of the page to find it.

The PIC on the demo board comes with a program already installed so the first thing I did after scanning through the various read me files was to try and get that running starting with the command line tool to make sure that this was all working correctly. I plugged the supplied USB cable into my Mac and the other end into the PICKit2. I then plugged the PICKit2 into the board making sure that the guide arrows lined up. I then ran the command line tool without parameters to get the help to display and had a look at what was available.

To get power to the board from the programmer you need to call the command line as follows:

pk2cmd -A3.3 -P -HK

The -A parameter sets the voltage to 3.3V, the -P parameter auto-detects the target device and the -HK keeps the command running until you hit a key.

What you then get is a program where the LEDs chase with the speed controlled by the variable resistor and the direction controlled by the push button.

Having got that going I fired up MPLAB to take a look and armed with the data sheets for the PIC and the demo board, I decided to try and get those LEDs to light up. It turned out to be pretty straightforward although it took a few goes to sort out the config settings and get them right for debugging. It wasn’t helped by the 887 vs 877 confusion!

I started by creating a new project using the category ‘Microchip Embedded’ and ‘Standalone Project’. After clicking ‘Next’ I chose the ‘Mid-Range 8-bit MCUs’ option and selected the ‘PIC16F887’ MCU. After clicking ‘Next’ I chose the PICKit2 device from under the PICKit2 category and clicked ‘Next’. I chose the XC8 tool chain, clicked ‘Next’ and gave the project the name ‘Simple LEDs’. I clicked ‘Finish’ and my project was created.

The project is created without any files so the next thing was to add a source file. I did this by right clicking on the ‘Source Files’ folder in the ‘Projects’ tab on the left and selecting ‘New’ followed by ‘C Source File’. From there I created a file called main.c.

Having done that I included the pic header.

#include <pic.h>

I then generated the configuration information for the project which is done using a series of #pragma directives in the source code. Fortunately there is a cunning tool in the IDE that generates the code for you so you can paste it into a source file. To open the tool, click on the ‘Window’ menu and select ‘PIC Memory Views’ and ‘Configuration Bits’. This pops up the screen below in the IDE.

PIC Configuration Settings

PIC Configuration Settings

I made a couple of changes to the default configuration by setting FOSC to INTRC_NOCLKOUT as I wanted to use the internal oscillator (there isn’t an external one on the demo board) and I switched off LVP because the first time I tried debugging a program MPLAB complained about it.

Generated Settings Code

Generated Settings Code

Once I’d done that I hit the ‘Generate Source Code to Output’ button and transferred the generated code to my main.c file as below.

#include <pic.h>

// PIC16F887 Configuration Bit Settings

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RE3/MCLR pin function select bit (RE3/MCLR pin function is MCLR)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown Out Reset Selection bits (BOR enabled)
#pragma config IESO = ON        // Internal External Switchover bit (Internal/External Switchover mode is enabled)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is enabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

The next thing to do was to add a main function which is defined as void with no parameters as follows.

void main()
{
   
}

Right, now for the interesting bit which is to get those LEDs lit up so time to open up those data sheets.

The first sheet I looked at was the one for the demo board because although I could sort of see the tracks on the board connecting them to port D, I wanted to be sure. The relevant part of the diagram is shown below and as you can see, the LEDs are most definitely connected to port D.

PIC Demo Board LEDs

PIC Demo Board LEDs

The next thing then was to grab the PIC16F887 data sheet and look at what we need to do to get port D working.

Well it turns out to be pretty easy. We simply need to configure the TRISD register to make all of the pins output pins and then write to the PORTD register to set the states.

The TRISD register is 8 bits wide and each bit represents one of the pins. Setting a bit to 0 makes it an output whereas setting it to 1 makes it an input. We want all of our pins to be outputs so that’s pretty easy, we just set the register to 0. To make all of the LEDs light up we need to write 1 to all of the pins so that’s pretty easy too, we just set the PORTD register to 0xff.

The last thing that needs to happen is to add in an infinite loop because we don’t want an embedded program to stop executing and that’s easily achieved by using an infinite while loop.

The final code for main.c then ends up looking like this:

#include <pic.h>

// PIC16F887 Configuration Bit Settings

// CONFIG1
#pragma config FOSC = INTRC_NOCLKOUT// Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA6/OSC2/CLKOUT pin, I/O function on RA7/OSC1/CLKIN)
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled and can be enabled by SWDTEN bit of the WDTCON register)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON       // RE3/MCLR pin function select bit (RE3/MCLR pin function is MCLR)
#pragma config CP = OFF         // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown Out Reset Selection bits (BOR enabled)
#pragma config IESO = ON        // Internal External Switchover bit (Internal/External Switchover mode is enabled)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is enabled)
#pragma config LVP = OFF        // Low Voltage Programming Enable bit (RB3 pin has digital I/O, HV on MCLR must be used for programming)

// CONFIG2
#pragma config BOR4V = BOR40V   // Brown-out Reset Selection bit (Brown-out Reset set to 4.0V)
#pragma config WRT = OFF        // Flash Program Memory Self Write Enable bits (Write protection off)

void main()
{
    // configure port d as output
    TRISD = 0;

    // set the pins high to display the leds
    PORTD = 0xff;

    // loop forever
    while(1){}
}

Finally I made sure that the project was the ‘main’ project by right clicking on it in the ‘Projects’ view and selecting ‘Set as Main Project’ and then ran it by clicking on the ‘Debug’ menu and selecting ‘Debug Main Project’. A few moments later all eight of the LEDs on my board were alight. Success!

Having done that I thought it might be cool to try to replicate the demo program by writing from scratch using the data sheets for reference and this will be the subject of my next few posts.

Ban the button logo
A couple of months ago I was visiting my grandmother with my father and son and we thought it would be great to get a family photo with all four generations together. My dad passed his Android phone to my aunt and she lined up a shot.

Click…burrrrr

Something struck me as I heard the camera’s tinny rendition of a camera shutter complete with winding film. I turned to my dad, “Why on earth do they insist on using a camera shutter noise on these things? I bet no-one over the age of 25 even knows what a camera shutter sounds like!”

“What’s a camera shutter?”, my son asked as if on cue.

Imagine if we’d had the same conversation about cut and paste. Even I don’t remember a time when people editing documents had to actually do it with scissors and glue. The metaphor has now become the reality as generations of computer users grow up whose only experience of cutting and pasting has been when editing documents on a computer.

Having cut out sections of documents with virtual scissors and pasted them into place with virtual glue we usually need to ‘save’ them as virtual files in virtual folders. We ‘open’ virtual documents on a virtual desktop and throw things away in a virtual bin. Call me a curmudgeonly old git but it all just seems a bit… boring. I’m sure that many office jobs in the 1950’s and 60’s were that mandrolic but in an age when our computing platforms are so powerful that we could have pretty much any form of user interface we can dream up, why do we continue to insist on bringing that kind of tedium into the 21st century?

Now it would be unfair of me to try to imply that there has been no progress at all. There have been some notable examples of new technology designed to try and promote the use of new ideas for human computer interaction, most of which appear to come from the manufacturers of game consoles. I’m thinking of things like the Kinect or Wiimote but while these make for interesting toys I don’t see much of a take up in the boringly sensible corporate world. At the office we still want desktops and our old fashioned WIMP environments.

One of the trends that is becoming apparent now is the way that information is managed and consumed. We seem to prefer to deal with information as a series of narratives, not expressed in one huge document but rather as a thread of smaller pieces linked together, stored and consumed across many systems and devices potentially between several different applications. If we follow that idea through, I reckon we’ll see the documents of the future becoming more like a living web of facts and dimensions brought together within some kind of context. This would make it a lot easier to assemble and keep them up to date, they would effectively do it themselves.

I do wonder if this is a place where the next set of metaphors for human computer interaction can emanate from. I mean what exactly is a document in that context? Certainly not something you could photocopy and stick in a drawer. It would seem to me to be more of a mind map or a tagged snapshot of our thoughts or knowledge at one instant in time. Where is it stored? In a folder in a filing system? Well, no, it’s everywhere I need it.

So how do we encourage innovative thought in this area then? Well one way to get people to be inventive is, ironically, to give them constraints. Innovation comes from the need to adapt and overcome problems. I was involved in a project for a mobile app a while back where we sat down and specified the UI in a document for use by an external company we sometimes outsource to. Everything was all signed up and agreed and they went off and started developing it for us. As the project was nearing completion they provided me with a couple of screenshots that I could include in a weekly report. There was something about them though that didn’t sit right with me. Then it struck me. I didn’t want to include them in the report because they looked… well… boring. Now this was no fault of the team who built the app, we had provided the spec and it was far too late to change anything. It did get me thinking about how it could be done differently though.

One idea I came up with was to ban certain types of control from the UI. For example, I think many developers use far too many buttons. Buttons certainly have their place but they can be misused in my view. Buttons are a way of capturing the user’s intent but I’m convinced that there are often much more subtle and powerful ways of doing that than simply presenting the user with a shed load of things to click on. Anyway, I thought, what would happen if we banned them from the UI? Entirely. Would that force the developers to come up with something new? I’m sure the rest of engineering thought I had completely lost the plot when I offered that as an idea but I really do think it’d work because it would force them to think laterally and creatively to solve the problem. I think I’m tempted to run with this one and start a campaign to Ban the Button!

Ok so back to the photograph: having some kind of a noise come out of your mobile device when you’re taking a photo is actually pretty useful. What would be a good replacement noise for a mechanical shutter then? A beep, a loud voice saying, “Smile and say cheese”, a few bars of Def Leppard’s rock ballad Photograph perhaps? I don’t know, that’s a tough one. Let me know if you have any ideas!

In the last post we put together some driver code for the SerLCD 2.5 device and got it displaying a message in a test program. The SerLCD has quite a bit more functionality to it though and in this post we’ll add a few extra methods to the driver and then put together a bit of a demo program that will showcase a few of them.

The SerLCD provides access to much of the functionality of the underlying HD44780 LCD controller through a set of command codes described in the SparkFun data sheet. These are passed to the controller by first sending one of two different control codes, either 0xFE or 0x7C, followed by a command parameter byte. The control codes are shown in the table below.

Command Codes
Clear the screen 0xFE, 0x01
Move the cursor right one 0xFE, 0x14
Move the cursor left one 0xFE, 0x10
Scroll Right 0xFE, 0x1C
Scroll Left 0xFE, 0x18
Turn visual display on 0xFE, 0x0C
Turn visual display off 0xFE, 0x08
Underline cursor on 0xFE, 0x0E
Underline cursor off 0xFE, 0x0C
Blinking box cursor on 0xFE, 0x0D
Blinking box cursor off 0xFE, 0x0C

You can also adjust the backlight brightness by sending a different set of codes.

Command Codes
Backlight Off 0x7C, 0x80
Backlight 40% 0x7C, 0x8C
Backlight 73% 0x7C, 0x96
Backlight 100% 0x7C, 0x9D

In addition there are other codes that can be used to set up the SerLCD for different display widths and heights and to change the baud rate of the interface. I’ve not played with these but if you need them, you have the source code!

Updating the Driver Code

Well the driver code just wouldn’t be complete without some of these extra commands taken care of so I decided to implement them. To do this I added an additional four methods to the driver. These are as follows:

Function Description
SERLCD_WriteChar Writes a single character to the device
SERLCD_SendCommand Sends a command to the device
SERLCD_SetCursorPosition Sets the cursor position
SERLCD_SetBacklight Sets the backlight brightness

For the SendCommand and SetBacklight methods I created a set of constants that can be used to send the specific commands without having to refer back to the data sheet for the details.

I started by adding the new methods and constants to the stm32f4xx_serlcd.h file. The resulting code is given in the listing below. I declared the display commands as arrays of bytes and they are defined in the stm32f4xx_serlcd.c file. I took a slightly different approach for the backlight commands (I suppose I could have done them all the same way) and defined only the specific control bytes. The function prepends it with the correct control byte. If your OCD can’t handle the fact that they work differently feel free to go ahead and change it, I won’t be upset!

/**
******************************************************************************
* @file stm32f4xx_serlcd.h
* @author Jon Masters
* @version V1.0
* @date 13-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_Ok ((uint16_t)0x0000)
#define SERLCD_BufferOverrun ((uint16_t)0x0001)
#define SERLCD_BufferUnderrun ((uint16_t)0x0002)

/* Backlight levels ----------------------------------------------------------*/
#define SERLCD_Backlight_Off ((char)0x80)
#define SERLCD_Backlight_On40 ((char)0x8c)
#define SERLCD_Backlight_On73 ((char)0x96)
#define SERLCD_Backlight_On100 ((char)0x9d)

/* Display commands ------------------------------------------------------*/
extern const char SERLCD_CLRSCRN[]; /* Clear the screen */
extern const char SERLCD_MVCURSR[]; /* Move the cursor right one */
extern const char SERLCD_MVCURSL[]; /* Move the cursor left one */
extern const char SERLCD_SCROLLR[]; /* Scroll Right */
extern const char SERLCD_SCROLLL[]; /* Scroll Left */
extern const char SERLCD_DISPON[]; /* Turn visual display on */
extern const char SERLCD_DISPOFF[]; /* Turn visual display off */
extern const char SERLCD_UNDRCURSON[]; /* Underline cursor on */
extern const char SERLCD_UNDRCURSOFF[]; /* Underline cursor off */
extern const char SERLCD_BLNKCURSON[]; /* Blinking box cursor on */
extern const char SERLCD_BLNKCURSOFF[]; /* Blinking box cursor off */

/* Exported types ------------------------------------------------------------*/
/* Exported functions --------------------------------------------------------*/

/* Initialization and Configuration functions *********************************/
void SERLCD_Init();
uint16_t SERLCD_IsBufferFull();
uint16_t SERLCD_WriteChar(char c);
uint16_t SERLCD_WriteMessage(const char* text, uint16_t length);
uint16_t SERLCD_SendCommand(const char* cmd);
uint16_t SERLCD_SetCursorPosition(uint16_t row, uint16_t col);
uint16_t SERLCD_SetBacklight(char cmd);

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

The updated code for the implementation file is below. The changes include adding the definition of the control character arrays and the declaration of the new methods. The only method in there that probably warrants further discussion in the SERLCD_SetCursorPosition method that can be called to set the position of the cursor in the display. It is at this location, regardless of whether you have either of the two cursors on or not, that any new text output will appear. The reason that you might want to change the implementation here is that the code currently only works for the 2×16 character display. The SerLCD board also supports 2×20, 4×16 and 4×20 displays. I did toy with the idea of updating the Init method to pass in the display size and then work around it but decided in the end to leave that as an exercise for you to try out. If you are looking to do this and you’re struggling let me know and I’d be happy to help adapt it.

/**
******************************************************************************
* @file stm32f4xx_serlcd.c
* @author Jon Masters
* @version V1.0
* @date 13-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.
(#) SERLCD_IsBufferFull returns a value greater than 0 if the write buffer is
full.
(#) To write a single character to the display use the SERLCD_WriteChar method.
(#) Commands can be sent using the SERLCD_SendCommand method. The commands
available are listed in the Display Commands section of the header file.
(#) SERLCD_SetCursorPosition sets the cursor to a specified position in the
display. Note that the order of parameters is row then column and the indexes
are zero based. Note also that currently, this driver only supports 2x16
displays. To use displays of other sizes you'll need to update the driver
code.
(#) The method SERLCD_SetBacklight sets the backlight level to one of the
values specified by constants in the header file section Backlight levels.
@endverbatim
*/

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

/* Private constants */
#define SERLCD_CTRLLENGTH ((uint16_t)0x0002)

/* Display commands ------------------------------------------------------*/
const char SERLCD_CLRSCRN[] = {0xFE, 0x01}; /* Clear the screen */
const char SERLCD_MVCURSR[] = {0xFE, 0x14}; /* Move the cursor right one */
const char SERLCD_MVCURSL[] = {0xFE, 0x10}; /* Move the cursor left one */
const char SERLCD_SCROLLR[] = {0xFE, 0x1C}; /* Scroll Right */
const char SERLCD_SCROLLL[] = {0xFE, 0x18}; /* Scroll Left */
const char SERLCD_DISPON[] = {0xFE, 0x0C}; /* Turn visual display on */
const char SERLCD_DISPOFF[] = {0xFE, 0x08}; /* Turn visual display off */
const char SERLCD_UNDRCURSON[] = {0xFE, 0x0E}; /* Underline cursor on */
const char SERLCD_UNDRCURSOFF[] = {0xFE, 0x0C}; /* Underline cursor off */
const char SERLCD_BLNKCURSON[] = {0xFE, 0x0D}; /* Blinking box cursor on */
const char SERLCD_BLNKCURSOFF[] = {0xFE, 0x0C}; /* Blinking box cursor off */

/* 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 -----------------------------------------------------------*/

/**
* Initialises the SERLCD driver. Must be called before any other calls to the
* buffer are made.
*/
void SERLCD_Buffer_Init()
{
    cb.size = SERLCD_BufferLength;
    cb.start = 0;
    cb.end = 0;
    cb.elems = calloc(cb.size, sizeof(char));
}

/**
* Call this if you need to free the buffer associated with the driver. If you
* do this, you'll need to call the Init method again before using it.
*/
void SERLCD_Buffer_Free()
{
    free(cb.elems);
}

/**
* Returns a value greater than zero if the circular buffer is full.
*/
uint16_t SERLCD_Buffer_IsFull()
{
    return (cb.end + 1) % cb.size == cb.start;
}

/**
* Returns a value greater than zewro if the circular buffer is empty.
*/
uint16_t SERLCD_Buffer_IsEmpty()
{
    return cb.end == cb.start;
}

/**
* Writes a character to the circular buffer. Returns either SERLCD_Ok
* or SERLCD_BufferOverrun depending on whether the character could be written
* or not.
*/
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_Ok;
}

/**
* Reads the next character from the buffer. Returns SERLCD_Ok
* if the read happened or SERLCD_BufferUnderrun if the read would
* result in a buffer underrun.
*/
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;
    }

    return SERLCD_Ok;
}

/* Public Methods -----------------------------------------------------------*/

/**
* Initialises the driver. Call this method before calling any other driver
* method.
*/
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);

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

    // prepare the buffer
    SERLCD_Buffer_Init();
}

/**
* Call this method if for some reason you want to free the memory
* associated with the driver.
*/
void SERLCD_DeInit()
{
    // disable the interrupts
    USART_ITConfig(USART1, USART_IT_TXE, DISABLE);

    // free the buffer
    SERLCD_Buffer_Free();
}

/**
* Returns a value greater than zero if the send buffer is already full.
*/
uint16_t SERLCD_IsBufferFull()
{
    return SERLCD_Buffer_IsFull();
}

/**
* Writes a single character to the SerLCD device. The method will return
* SERLCD_Ok if this is successful or SERLCD_BufferOverrun if it results in
* a buffer overrun.
*/
uint16_t SERLCD_WriteChar(char c)
{
    uint16_t rv = SERLCD_Buffer_Write(c);
    
    if (rv == SERLCD_Ok)
    {
        // enable the interrupt to send the message
        USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
    }

    return rv;
}

/**
* Writes a string of characters to the SerLCD of a given length.
* The method will return SERLCD_Ok if this is successful or
* SERLCD_BufferOverrun if it results in a buffer overrun.
*/
uint16_t SERLCD_WriteMessage(const char* text, uint16_t length)
{
    // index into the character array
    uint16_t i = 0;

    // return value
    uint16_t rv = SERLCD_Ok;

    // write until we get to the end of the
    // text or overrun the buffer
    while (i < length && rv == SERLCD_Ok)
    {
        rv = SERLCD_Buffer_Write(text[i++]);
    }

    // did we add characters?
    if (i > 0)
    {
        // enable the interrupt to send the message
        USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
    }

    return rv;
}

/**
* Clears the display and sets the write position to the top
* left position.
*/
uint16_t SERLCD_SendCommand(const char* cmd)
{
    return SERLCD_WriteMessage(cmd, SERLCD_CTRLLENGTH);
}

/**
* Sets the cursor to the specified row and column. Note that
* the row and column are both zero indexed. Currently this is
* set up for a 2x16 character display and will need to be
* adapted for other display sizes.
*/
uint16_t SERLCD_SetCursorPosition(uint16_t row, uint16_t col)
{
    char cmd[] = {0xFE, 0x80 + (row == 0 ? 0 : 64) + col};
    return SERLCD_WriteMessage(cmd, SERLCD_CTRLLENGTH);
}

/**
* Sets the brightness of the backlight to the specified level.
* Backlight level options are defined in the header file.
*/
uint16_t SERLCD_SetBacklight(char cmd)
{
    char c[] = {0x7C, cmd};
    return SERLCD_WriteMessage(c, SERLCD_CTRLLENGTH);
}

/*
* 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(&c);
    
            // send it to the device
            USART_SendData(USART1, c);
        }
    }
}

So that’s that. We now have a driver that abstracts away a lot of the work of talking to a SerLCD connected display. It is interrupt driven so you don’t need to worry about that, it includes a circular buffer and also provides access to many of the control codes needed to create a really great display for your users. All we need now is some kind of demo program to show it off.

Implementing the SerLCD Driver Demo

In the previous post the code that drives the demo is all interrupt driven with only the initial configuration work carried out in the main method. This means that all of our demo code can also be written in the stm32f4xx_it.c file. I’ve included the main.c file below for completeness though so if you’ve dived straight into this post you don’t have to go searching for it.

/**
*****************************************************************************
**
** 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 SysTick with a 10ms period */
    RCC_ClocksTypeDef RCC_Clocks;
    RCC_GetClocksFreq(&RCC_Clocks);

    // 10ms tick
    SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);

    /* initialise the SerLCD */
    SERLCD_Init();

    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;
}

Similarly the stm32f4xx_it.h file hasn’t changed either but as this remains as it is when a project is first created, I’ve not included it here. Instead we go straight onto the implementation file.

The way I’ve chosen to do this is by enumerating a set of program states that the demo iterates through. When it gets to the end it simply starts back at the beginning again. Well almost at the beginning, it misses out the first state which is to wait for the SerLCD splash screen to complete. There’s no need to do that again obviously!

I’m going to leave it to you to figure out the detail of how the demo works. There’s nothing in there that should be too difficult to work out and I think it shows off some of the capabilities of the driver quite nicely and hopefully gives you a few ideas of how you can use it yourself in your own projects.

/**
******************************************************************************
* @file Project/STM32F4xx_StdPeriph_Template/stm32f4xx_it.c 
* @author MCD Application Team
* @version V1.1.0
* @date 18-January-2013
* @brief Main Interrupt Service Routines.
* This file provides template for all exceptions handler and 
* peripherals interrupt service routine.
******************************************************************************
* @attention
*
* <h2><center>&copy; COPYRIGHT 2013 STMicroelectronics</center></h2>
*
* Licensed under MCD-ST Liberty SW License Agreement V2, (the "License");
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.st.com/software_license_agreement_liberty_v2
*
* Unless required by applicable law or agreed to in writing, software 
* distributed under the License is distributed on an "AS IS" BASIS, 
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************
*/

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

/** @addtogroup Template_Project
* @{
*/

/* Private typedef -----------------------------------------------------------*/
enum DEMO_STATE
{
    DS_WAIT_FOR_SPLASH,
    DS_SIMPLE,
    DS_SIMPLE_WITH_CURSOR,
    DS_SCROLL_LEFT,
    DS_SCROLL_RIGHT,
    DS_FLASH,
    DS_BACKLIGHT,
    DS_ERROR
} DemoState = DS_WAIT_FOR_SPLASH;

/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
static uint8_t DSWC_i = 0;

/* Private function prototypes -----------------------------------------------*/
/* Private functions ---------------------------------------------------------*/
void DoSimpleInit()
{
    SERLCD_SendCommand(SERLCD_CLRSCRN);
    SERLCD_SetCursorPosition(0, 0);
    SERLCD_WriteMessage("Simple text can ", 16);
    SERLCD_SetCursorPosition(1, 0);
    SERLCD_WriteMessage(" be displayed ", 16);
}

void DoSimpleWithCursorInit()
{
    SERLCD_SendCommand(SERLCD_CLRSCRN);
    SERLCD_SendCommand(SERLCD_BLNKCURSON);
    DSWC_i = 0;
}

void DoSimpleWithCursor()
{
    static char CursorMessage[] = "A cursor is used for user input ";

    if (DSWC_i < 32)
    {
        SERLCD_WriteChar(CursorMessage[DSWC_i]);
        DSWC_i++;
    }
}

void DoSimpleWithCursorDeinit()
{
    SERLCD_SendCommand(SERLCD_BLNKCURSOFF);
}

void DoScrollLeftInit()
{
    SERLCD_SendCommand(SERLCD_CLRSCRN);
    SERLCD_SetCursorPosition(0, 0);
    SERLCD_WriteMessage(" You can scroll ", 16);
    SERLCD_SetCursorPosition(1, 0);
    SERLCD_WriteMessage(" to the left ", 16);
}

void DoScrollLeft()
{
    SERLCD_SendCommand(SERLCD_SCROLLL);
}

void DoScrollRightInit()
{
    SERLCD_SendCommand(SERLCD_CLRSCRN);
    SERLCD_SetCursorPosition(0, 0);
    SERLCD_WriteMessage(" Or scroll to ", 16);
    SERLCD_SetCursorPosition(1, 0);
    SERLCD_WriteMessage(" the right ", 16);
}

void DoScrollRight()
{
    SERLCD_SendCommand(SERLCD_SCROLLR);
}

void DoFlashInit()
{
    SERLCD_SendCommand(SERLCD_CLRSCRN);
}

void DoFlash()
{
    static uint8_t FlashOn = 1;

    if (FlashOn)
    {
        SERLCD_SetCursorPosition(0, 0);
        SERLCD_WriteMessage(" Important info ", 16);
        SERLCD_SetCursorPosition(1, 0);
        SERLCD_WriteMessage(" can be flashed ", 16);
        FlashOn = 0;
    }
    else
    {
        SERLCD_SendCommand(SERLCD_CLRSCRN);
        FlashOn = 1;
    }
}

void DoBacklightInit()
{
    SERLCD_SendCommand(SERLCD_CLRSCRN);
    SERLCD_SetCursorPosition(0, 0);
    SERLCD_WriteMessage("Backlight level ", 16);
    SERLCD_SetCursorPosition(1, 0);
    SERLCD_WriteMessage(" is now at % ", 16);
}

void DoBacklight()
{
    static uint8_t BacklightLevel = SERLCD_Backlight_On100;
    static uint8_t Countdown = 0;

    if (Countdown == 0)
    {
        switch (BacklightLevel)
        {

        case SERLCD_Backlight_On40:
            BacklightLevel = SERLCD_Backlight_On73;
            SERLCD_SetCursorPosition(1, 11);
            SERLCD_WriteMessage("73% ", 4);
            break;

        case SERLCD_Backlight_On73:
            BacklightLevel = SERLCD_Backlight_On100;
            SERLCD_SetCursorPosition(1, 11);
            SERLCD_WriteMessage("100%", 4);
            break;

        case SERLCD_Backlight_On100:
            BacklightLevel = SERLCD_Backlight_On40;
            SERLCD_SetCursorPosition(1, 11);
            SERLCD_WriteMessage("40% ", 4);
            break;
        }

        SERLCD_SetBacklight(BacklightLevel);
        Countdown = 2;
    }

    Countdown--;
}

void DoErrorInit()
{
    SERLCD_SendCommand(SERLCD_CLRSCRN);
    SERLCD_SetCursorPosition(0, 0);
    SERLCD_WriteMessage(" You can do ", 16);
    SERLCD_SetCursorPosition(1, 0);
    SERLCD_WriteMessage(" this too! ", 16);
}

void DoError()
{
    static uint8_t FlashState = 0;

    if (FlashState == 0)
    {
        SERLCD_SetCursorPosition(0, 0);
        SERLCD_WriteChar('*');
        SERLCD_SetCursorPosition(0, 15);
        SERLCD_WriteChar(' ');

        SERLCD_SetCursorPosition(1, 0);
        SERLCD_WriteChar(' ');
        SERLCD_SetCursorPosition(1, 15);
        SERLCD_WriteChar('*');

        FlashState = 1;
    }
    else
    {
        SERLCD_SetCursorPosition(0, 0);
        SERLCD_WriteChar(' ');
        SERLCD_SetCursorPosition(0, 15);
        SERLCD_WriteChar('*');

        SERLCD_SetCursorPosition(1, 0);
        SERLCD_WriteChar('*');
        SERLCD_SetCursorPosition(1, 15);
        SERLCD_WriteChar(' ');

        FlashState = 0;
    }
}

/******************************************************************************/
/* Cortex-M4 Processor Exceptions Handlers */
/******************************************************************************/
/**
* @brief This function handles NMI exception.
* @param None
* @retval None
*/
void NMI_Handler(void)
{
}

/**
* @brief This function handles Hard Fault exception.
* @param None
* @retval None
*/
void HardFault_Handler(void)
{
    /* Go to infinite loop when Hard Fault exception occurs */
    while (1)
    {
    }
}

/**
* @brief This function handles Memory Manage exception.
* @param None
* @retval None
*/
void MemManage_Handler(void)
{
    /* Go to infinite loop when Memory Manage exception occurs */
    while (1)
    {
    }
}

/**
* @brief This function handles Bus Fault exception.
* @param None
* @retval None
*/
void BusFault_Handler(void)
{
    /* Go to infinite loop when Bus Fault exception occurs */
    while (1)
    {
    }
}

/**
* @brief This function handles Usage Fault exception.
* @param None
* @retval None
*/
void UsageFault_Handler(void)
{
    /* Go to infinite loop when Usage Fault exception occurs */
    while (1)
    {
    }
}

/**
* @brief This function handles SVCall exception.
* @param None
* @retval None
*/
void SVC_Handler(void)
{
}

/**
* @brief This function handles Debug Monitor exception.
* @param None
* @retval None
*/
void DebugMon_Handler(void)
{
}

/**
* @brief This function handles PendSVC exception.
* @param None
* @retval None
*/
void PendSV_Handler(void)
{
}

/**
* @brief This function handles SysTick Handler.
* @param None
* @retval None
*/
void SysTick_Handler(void)
{
    // we want the routine to run every 500ms and we're getting
    // called every 10ms
    static uint8_t delay = 50;

    delay--;

    if (delay > 0)
    {
        return;
    }

    delay = 50;

    // this is the state change countdown and each unit
    // is half a second
    static int16_t Countdown = 10;

    Countdown--;

    static uint8_t Init = 0;

    switch (DemoState)
    {
    case DS_WAIT_FOR_SPLASH:
        if (Countdown <= 0)
        {
            DemoState = DS_SIMPLE;
            Countdown = 5;
        }
        break;

    case DS_SIMPLE:
        {
            if (!Init)
            {
                DoSimpleInit();
                Init = 1;
            }

            if (Countdown <= 0)
            {
                Init = 0;
                DemoState = DS_SIMPLE_WITH_CURSOR;
                Countdown = 32;
            }
            break;
        }

    case DS_SIMPLE_WITH_CURSOR:
        {
            if (!Init)
            {
                DoSimpleWithCursorInit();
                Init = 1;
            }

            DoSimpleWithCursor();

            if (Countdown <= 0)
            {
                DoSimpleWithCursorDeinit();
                DemoState = DS_SCROLL_LEFT;
                Countdown = 14;
                Init = 0;
            }
            break;
        }

    case DS_SCROLL_LEFT:
        {
            if (!Init)
            {
                DoScrollLeftInit();
                Init = 1;
            }

            DoScrollLeft();

            if (Countdown <= 0)
            {
                DemoState = DS_SCROLL_RIGHT;
                Countdown = 14;
                Init = 0;
            }
            break;
        }

    case DS_SCROLL_RIGHT:
        {
            if (!Init)
            {
                DoScrollRightInit();
                Init = 1;
            }

            DoScrollRight();

            if (Countdown <= 0)
            {
                DemoState = DS_FLASH;
                Countdown = 10;
                Init = 0;
            }
            break;
        }

    case DS_FLASH:
        {
            if (!Init)
            {
                DoFlashInit();
                Init = 1;
            }

            DoFlash();

            if (Countdown <= 0)
            {
                DemoState = DS_BACKLIGHT;
                Countdown = 12;
                Init = 0;
            }
            break;
        }

    case DS_BACKLIGHT:
        {
            if (!Init)
            {
                DoBacklightInit();
                Init = 1;
            }

            DoBacklight();

            if (Countdown <= 0)
            {
                // restore the backlight back to 100%
                SERLCD_SetBacklight(SERLCD_Backlight_On100);
                DemoState = DS_ERROR;
                Countdown = 10;
                Init = 0;
            }
            break;
        }

    case DS_ERROR:
        {
            if (!Init)
            {
                DoErrorInit();
                Init = 1;
            }

            DoError();
        
            if (Countdown <= 0)
            {
                DemoState = DS_SIMPLE;
                Countdown = 10;
                Init = 0;
            }
            break;
        }
    }
}

/******************************************************************************/
/* STM32F4xx Peripherals Interrupt Handlers */
/* Add here the Interrupt Handler for the used peripheral(s) (PPP), for the */
/* available peripheral interrupt handler's name please refer to the startup */
/* file (startup_stm32f40xx.s/startup_stm32f427x.s). */
/******************************************************************************/
/**
* @brief This function handles PPP interrupt request.
* @param None
* @retval None
*/
/*void PPP_IRQHandler(void)
{
}*/
/**
* @}
*/

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

When you now run this code, you should see the display go through a number of demos including simple message display, scrolling, flashing and using the cursors. One thing I did have to do though, remember I said in the first article that I broke out the connection between the Discovery and the SerLCD in case I needed a pull-up resistor on the Tx / Rx link? Well when I ran it using an external power supply I got garbage on the screen because the pins on the Discovery started in an undefined state. I popped a 10K pull-up from the data line to the breadboard’s +5V line and it fixed the problem. I suspect you’ll need to do the same thing.

Well that’s the end of this series of posts. As always please let me know if any of this has been useful or not by commenting below. I’m always very keen to receive feedback and I’d be especially interested to know how you’ve used and modified this in your own projects.