Real Time Embedded Code On Small Microcontrollers (Part 2)

Introduction

This is the second of two articles in which I discuss some simple but very practical ways of building projects on small microcontrollers, particularly Microchip's PIC series. In the first one, I discussed how to get a board running with some basic debug output, using the PIC's ICP connector for both programming and debug. In this one, we improve the software by adding a simple task scheduler.

(Although the code presented here is for PIC's, it should be possible to adapt it to work on other platforms without too much trouble.)

clock mechanism

A scheduler makes system design easier, even with no RTOS

Getting away from the loops

The basic code that I described in Part 1 made heavy use of loops. The bit-bash UART code used a for loop with an integer to set the baud rate, and the main code ran in a while(1) loop.

Obviously this is only really useful for the most trivial of projects. Usually, the next step up will be to add an RTOS of some kind. This will give the device the possibility to appear as if it does several things at once, by sharing CPU time between a number of process.

But an RTOS can be too much for some projects which run on small platforms. Perhaps you don't really need context switching, or semaphores and queue, or any of that. Perhaps, with only a few hundreds of bytes of RAM in your cheap and cheerful chip, you can't, and don't want to, be doing three things at once.

(I based this code quite heavily on the scheduler RIOS, which was developed by Messrs. Vahid, Givargis and Miller at the University of California. I think that it was intended mostly as a teaching aid, but it is certainly useful. Kudos and thanks to those guys. It is well worth reading what they have to say.)

Am I interrupting?

The big difference between this system and that of Part 1 is that it is interrupt driven. That means that we have to write an ISR.

The RIOS interrupt is only used to create timer ticks, which are then used in the main loop. However, we want our system to also have debug output, as before. We would also like to get away from the for() loop that we used to generate baud rate previously. For that reason, the ISR presented here is used to create timer ticks, and also the UART baud rate.

Before going into that, it is important to clarify what this system is and is not doing, a bit.

Getting things done on time

It is important to understand that this simple, but still powerful, scheduler-based system is not in any real way, an operating system. It certainly doesn't do context switching, or anything like that.

This a cooperative system. Tasks do not preempt (i.e. interupt) each other. Everything works on the basis that all scheduled tasks complete, every single timer tick. If they don't, a "fatal error" (signalled by the light coming on and the device sleeping) will occur.

This means that it is your responsibility, as designer, to make sure that the system and its tasks behave in a way that ensure this. If you don't, things will not work.

if you do, though, you get a system which has very predictable and reliable timing characteristics. In effect, you have a "hard real time system" without an RTOS.

Notes On The Code

1. Because of the way the PIC works, I decided to use one single ISR for timer ticks AND the UART in this app. Although the PIC18F that I used can have two levels of interrupt priority, there didn't to be much point. This is because - remember - we have to ensure that all tasks run to completion anyway. That includes any debug statements that we write. So, no point to have interrupt priorities, as they should never happen together. (It would have been nice to have separate ISR's from a coding point of view, but this is no big deal.

2. Because of 1 above, I increased both clock speed and the baud rate considerably from the turgid 500kHz / 300 bauds of Part 1. Now we have 8MHz and 19200 baud, which is positively blazing in comparison. Timer ticks are every 100ms.

3. Timer 1 is for timer ticks. It is reloaded in the ISR. Timer 2 has a preload function, and is used for the UART.

4. Timer 2 interrupts are only enabled while a character is being sent. This minimises debug overhead. put_char() waits until timer 2 ints are turned off, passes the next byte into a (volatile) global, and turns them on. The ISR bashes the bits then turns its ints off when done. This means that code is still blocking while serial data is being written, even though it is interrupt driven. But - again because the task has to complete in this timer tick, anyway - this is not really an issue.

The Code

DOWNLOAD

main.c

#include "BBUart.h"
#include  "xprintf.h"
#include "hardware.h"

typedef struct task {
   unsigned long period;      // Rate at which the task should tick
   unsigned long elapsedTime; // Time since task's last tick
   void (*TickFct)(void);       // Function to call for task's tick
} task;

// GLOBALS
unsigned int power_is_on = 0;
unsigned int battery_volts = 0;

// TASKS

// 0 : update uP LED
void task_blink(void){
    static int i = 0;
    if (i == 40){
        i = 0;
    } else {
        i++;
    }
    if (power_is_on){
       if (i < 20){
           BLINK_ON;
       } else {
           BLINK_OFF;
       }
    } else {
        if (i == 1){
            BLINK_ON;
        } else {
            BLINK_OFF;
        }
    }
}

// 1 : handle power switch
void task_switch_press(void){
    static int i = 0;
    if (PWR_SW == 0){
        i++;
    } else {
        i = 0;
    }
    if (i == 5){
        power_is_on = !power_is_on;
    }
}

// 2 : handle power changeover
void task_power_change(void){
    static int last_power_state = 0;
    if (power_is_on && last_power_state == 0){ // power up
        D xputs("power ON\r\n");
    } else if (power_is_on == 0 && last_power_state){ // power down
        D xputs("power OFF\r\n");
    }
    last_power_state = power_is_on;
}

// 3  : read battery voltage
void task_read_battery(void){
    battery_volts = read_battery_volts();
    D xprintf("battery:%d\r\n", battery_volts);
}

#define NUM_TASKS 4
int main(void)
{
    task tasks[NUM_TASKS];

    init_hardware();
    D xputs("\r\nPIC18 Scheduler with Debug\r\n");

    // initialise tasks
    tasks[0].elapsedTime = 0;
    tasks[0].period = 1;
    tasks[0].TickFct = &task_blink;
    tasks[1].elapsedTime = 0;
    tasks[1].period = 1;
    tasks[1].TickFct = &task_switch_press;
    tasks[2].elapsedTime = 0;
    tasks[2].period = 2;
    tasks[2].TickFct = &task_power_change;
    tasks[3].elapsedTime = 8;
    tasks[3].period = 10;
    tasks[3].TickFct = &task_read_battery;
    SCHEDULER_INTS_ON;

    // main loop
    while(1){
     // Scheduler
      for (unsigned char t = 0; t < NUM_TASKS; ++t) {
         if (tasks[t].elapsedTime >= tasks[t].period) { // Ready
            tasks[t].TickFct(); // Go
            tasks[t].elapsedTime = 0;
         }
         tasks[t].elapsedTime++;
      }
      timer_flag = 0;
      while (!timer_flag) {}
    }
 return 0;
}

hardware.h

#ifndef HARDWARE_H
#define	HARDWARE_H
#include 

unsigned char timer_flag = 0;
volatile unsigned char char_to_send = 0;

void init_hardware(void);
unsigned int read_battery_volts(void);

// INTERRUPT MACROS

#define ALL_INTS_ON INTCONbits.GIE = 1
#define ALL_INTS_OFF INTCONbits.GIE = 0
#define SCHEDULER_INTS_ON PIE1bits.TMR1IE = 1
#define SCHEDULER_INTS_OFF PIE1bits.TMR1IE = 0
#define DEBUG_INTS PIE1bits.TMR2IE
#define DEBUG_INTS_ON PIE1bits.TMR2IE = 1
#define DEBUG_INTS_OFF PIE1bits.TMR2IE = 0

// HARDWARE IO MACROS

// uP LED
#define BLINK_ON LATBbits.LATB5 = 0
#define BLINK_OFF LATBbits.LATB5 = 1

// logic inputs
#define PWR_SW PORTAbits.RA3
#define BLINK_LED LATBbits.LATB5

// enable or disable terminal reporting in real time
# define D if(PORTAbits.RA0==1)

#endif	/* HARDWARE_H */

hardware.c

#include "hardware.h"
#include "BBUart.h"

#define BBUART_MARK LATAbits.LA1 = 0;
#define BBUART_SPACE  LATAbits.LA1 = 1;

void init_hardware(void){
    // internal 8MHz clock
    OSCCON = 0x62;
    OSCCON2 = 0x00;
    OSCTUNE = 0x00;
    // interrupts off to start
    INTCON = 0x00;
    INTCON2 = 0x00;
    INTCON3 = 0x00;
    PIE1 = 0x00;
    PIE2 = 0x00;
    // no int priorities is the default but we define it anyway
    RCONbits.IPEN = 0;

    // preset IO ports;
    BLINK_OFF;
    BBUART_MARK; // idle the UART

    // configure I/Os
    TRISAbits.RA0 = 1; //PGD or /DEBUG
    TRISAbits.RA1 = 0; //PGC or TxD
    TRISAbits.RA2 = 1;
    TRISAbits.RA4 = 1; 
    TRISAbits.RA5 = 1;
    TRISBbits.RB4 = 1;
    TRISBbits.RB5 = 0; // BLINK LED (uP)
    TRISBbits.RB6 = 1;
    TRISBbits.RB7 = 1;
    TRISCbits.RC0 = 1;
    TRISCbits.RC1 = 1;
    TRISCbits.RC2 = 1;
    TRISCbits.RC3 = 1;
    TRISCbits.RC4 = 1;
    TRISCbits.RC5 = 1;
    TRISCbits.RC6 = 1;
    TRISCbits.RC7 = 1;

    // no weak pullups
    WPUA =0x00;
    WPUB = 0x00;
    // ADCs all off
    // (if we don't do this, dig ips don't work)
    ANSEL = 0x00;
    ANSELH = 0x00;

    // Timer 1 is for scheduler ticks
    // tick interval is reloaded in ISR itself
    // prescaler incs T1 each 4us
    T1CONbits.RD16 = 1;
    T1CONbits.T1RUN = 0;
    T1CONbits.T1CKPS0 = 1;
    T1CONbits.T1CKPS1 = 1;
    T1CONbits.T1OSCEN = 0;
    T1CONbits.TMR1CS = 0;
    T1CONbits.TMR1ON = 1;

    // Timer 2 is for debug UART
    PR2 = 104; // 19200baud
    T2CON = 0x00;
    T2CONbits.TMR2ON = 1;

    // interrupts will be enabled at application level
    SCHEDULER_INTS_OFF;
    DEBUG_INTS_OFF;
    INTCONbits.PEIE_GIEL = 1;
    ALL_INTS_ON;
}


void interrupt ISR(void){
    static int uart_ctr = -1;
    ALL_INTS_OFF;
    if (PIR1bits.TMR1IF){ // runs in about 6us @ 8MHZ
      PIR1bits.TMR1IF = 0;
      if (timer_flag){
          // bad news, tasks did not get completed!
          // C'est merde! we go on strike due to overwork!
          BLINK_ON;
          Sleep();
      }
      // magic numbers for scheduler ticks
      // 100ms = 25000 x 4us
      // 65536 - 25000 = 40536
      // 40536d = 0x9E58
      TMR1H = 0x9E;
      TMR1L = 0x58;
      timer_flag = 1;
    } else if (PIR1bits.TMR2IF){ // send a UART bit
        PIR1bits.TMR2IF = 0;
        if (uart_ctr > 0){
            if(char_to_send & 0x01 ){
              BBUART_MARK;
            } else {
              BBUART_SPACE;
            }
            char_to_send = (char_to_send >> 1);
            uart_ctr--;
        } else if (uart_ctr == 0) {
            BBUART_MARK; // stop bit
            uart_ctr = -1;
            DEBUG_INTS_OFF; // we are done
        } else if (uart_ctr == -1){
            BBUART_SPACE; // start bit
            uart_ctr = 8;
        }
    }
    ALL_INTS_ON;
}

unsigned int read_battery_volts(void){
    unsigned int reading = 0x00;
    // do stuff here
    return reading;
}

BBUart.c

#include "BBUart.h"
#include "hardware.h"

// pass a character to the ISR for sending
void put_char(unsigned char b){
  while (DEBUG_INTS == 1){} // wait until last char has been sent
  char_to_send = b;
  TMR2 = 0;
  DEBUG_INTS_ON; // enable timer 2 ints
}

What does it do?

It should be pretty obvious, but here it is anyhow. There are four tasks:

  1. Manage the "blink" LED. The LED always blinks, but the pattern depends on whether the system power is on. This runs every tick.
  2. Polls and debounces the power switch, and toggles the power. Runs every tick.
  3. Actually switches the power on or off.
  4. polls an IO port to read battery voltage, and updates a global.

This is a skeleton app, for demonstration purposes, but with a bit of imagination you can see that it could be expanded to do some powerful things. basically, it turns your PIC chip into a collection of state machines, running at different rates, and interacting in pretty much any way you please.

Observations

  1. In the original RIOS code, they implemented state machines by storing a state variable in the actual task structure. I did it simply by using static variables in the tasks themselves. I can't see the difference (unless you want tasks to directly interfere with each others' states, which seems a bad idea), and my way looks simpler to me. Am I missing something here?
  2. It's not that obvious here, but you can get tasks to run in different phases, by initialising the elapsed time. So, if you have 5 long tasks, each running every 10 ticks, you can sequence them to follow each other in a certain order and intervals. With careful design, this is quite a powerful feature.
  3. It would be possible to use the dead time that is getting lost waiting for the UART. This could be done by allocating a buffer, and then having the last task, running every tick, feed put_char, until the next tick. But, honestly, in a system at this kind of scale, this seems a bridge too far to me. All things are possible though - the beauty of this is that it is small enough to be easily hacked to suit a specific project. You can see what everything is doing quite easily.

That's it! I hope this is useful for you. Feel free to reuse the code, and hack it about as you need to.

This is the second of two articles which deal with small embedded systems. The first is here.