Pulse Width Modulation and interrupts: a Knight Rider mbed!

When an mbed recently landed on my desk I was pondering what to do with it. It was the works of the great David Hasselhoff that inspired me. Before his lifeguard gig at Baywatch, he was Michael Knight. He drove around in a sentient supercar called KITT and together they solved crime. I decided to make an mbed version of KITTs scanner with the four onboard LEDs. /media/uploads/rorydog1/kitt.png KITT's distinctive scanner has a red dot that sweeps from side to side. The TV car used filament bulbs and these take a visible time to dim. As the dot sweeps back and forth this delay causes a comet tail effect that follows the main sweeping light.

Using timers to make the LEDs sweep is a simple case of an up/down counter. The comet tail requires a bit more work. LEDs switch much faster then their filament cousins. To get them to dim at a rate slow enough for the human eye to see and produce the comet tail effect pulse width modulation is needed.

This would be easy enough to code in a common high level manner. The timing would be done through code in a loop and keeps checking the system clock to see when to trigger the timed functions – dimming or the sweep for example. I wanted to try something different. Continuously reading a clock to check how much time has passed is a operation called polling. It requires the instruction pointer to sit in a loop checking an input. The alternative is to use interrupts. Counters that are separate from the instruction pointer, called ••Tickers•• in the mbed world, generate interrupts at predefined times. These interrupts redirect direct the instruction pointer, and hence the program control, when needed. This is subtly different as we no longer need to keep looping the instruction pointer to checking external values. The instruction pointer can go off and do something else between interrupts.

Interrupts are the bread and butter of embedded programming. They release the instruction pointer to do other tasks or and free up power as the micro controller can sit idle by lowering its clock speed.

Program overview

Interrupts from ••Tickers•• trigger functions called Interrupt Service Requests (ISR). An interrupts pulls the instruction pointer out of the main loop to process an ISR and then returns it back to the main loop to carry on from where it left off. In these program the interrupts are fired from clocks on board LPC1768 micro controller. I'm not too sure of the details but you can have as many if them as you like. They are external to the instruction pointer. The interrupts arrive as follows:

  • Drive an LED to the correct intensity with Pulse Width Modulation. Fast frequency as flicker should not be seen by the human eye (20ms/100steps=200us)
  • Keep dimming all the LEDs apart from the one that is on. This generates the comet tail effect (8ms)
  • Select the next LED that is “On”. This sweeps the LEDs left or right. If it reaches the end, change direction (200ms)
  • Read the users input and display current settings. This is the slowest clock (250ms).

Using Pulse Width Modulation to change the intensity of an LED

The LED can switch much faster than the eye can see. To dim the LED at a visible rate we pulse the LED and use persistence of vision. Consider pulsing an LED for a percentage of a 50Hz period. This 50Hz period rate is faster than most eye can distinguish under normal viewing conditions. If the LED is on that whole period, it is at full intensity. Now consider if it is only on for 10% of the time period. In reality it is flashing off and on very fast but our persistence of vision will smooth these flashes. It will appear to be dimmer. In effect, our persistence of vision is acting like a low pass filter smoothing the LED pluses. The less of the total period it remains on, the dimmer it will look. The percentage of the time period that the power is supplied is called the “duty cycle”.

If we want to dim the LED from fully off to fully on in 1% steps, and the total period is 50Hz, the LED period is sampled in 100 steps. Hence, to have a duty cycle of 50Hz step in 1% units, the PWM function will need to be sampled at a rate of 50Hz*100 = 5KHz. I have included a chart from those nice chaps over at Arduino showing the relation of the duty cycle to the period: /media/uploads/rorydog1/duty_cycle.png http://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM

Interrupts

Notice that the main loop is empty. This was quite deliberate to prove a point about interrupts. This whole program could be written in the main loop using a polling routine to check the clock as mentioned above. Using poling to check that enough time had passed wastes a lot of time with the instruction pointer running around a loop doing nothing and consuming power at full clock speed. The loop polling is a bit like making a cup of tea by boiling water. If I am sitting reading in another room every now and again I need to pop into the kitchen and look to see if the water is boiling.

Now lets boil the water in a kettle with a whistle. As it boils we will hear the whistle and can put the book down and tend to it. Interrupts are similar to this, they are triggered by external events and grab the instruction pointer no matter what it is doing. The interrupt makes the instruction pointer runs an interrupt service request, (ISR) and when it is done it is sent back to the main program.

Interrupts are often the used to manage the interface between hardware and software. Because they do not need loops to keep checking an input or counter, the micro controller can wait in a low power state or do something else until an interrupt is triggered. However, they are not timed to the micro controller's clock. They are asynchronous and bring all the problems associated with untimed signals. Multiple interrupts can arrive at once causing race conditions and undefined states being entered. They can be tricky to debug as they can pop up anywhere. This could end up with a system that works perfectly until extra interrupts are added or random interrupt collisions generate seemingly random errors. Program flow is no longer jumping neatly from one instruction to the next as you would expect - the interrupt can be triggered anywhere. Consider an interrupt fired as you are writing or reading to a file in the main loop. The ISR may change the stage of the data being written and when the instruction pointer returns data may be corrupted.

If you are new to interrupts like I am, read this excellent write up by Andy Kirkham: https://developer.mbed.org/users/AjK/notebook/regarding-interrupts-use-and-blocking/

Also, for an overview of the nitty-gritty of the problems you face with interrupts this article by Priyadeep Kaur is a great introduction:

http://www.embedded.com/design/programming-languages-and-tools/4397803/Interrupts-short-and-simple--Part-1---Good-programming-practices

So, for this example I used a source of timed interrupts. Using these timed interrupts, I can trigger my ISR functions with exact time definitions. The mbed uses Ticker to send an interrupt at set times and fire an ISR function of choice.

The full program

I cheated a bit for the PWM of the LEDs. Instead of setting up my own interrupts and ISR I found that the mbed has the build in function called PwmOut. An array led[4] holds the brightness level of the led. current_led holds the current led that should be lit. Serial pc sets up an interface to a serial port for user interaction with the program

The Ticker Dimmer will trigger the LedDim IRS that dims each LED apart from the one that is selected, current_led. The Ticker Sweeper will trigger LedSweep increments or decrement current_led, sweeping it from side to side. The Ticker IO will trigger IO_check that read the user input. It tests to see what key has been pushed and either changes the sweep speed, the comet tail length, resets to default values or prints the current settings.

IO_check is not as clean as it should be. First of all, the function reads the user input and this would freeze the mbed in the IRS until a key was pressed. To get around this a line was added to test if the key had been pushed before reading the buffer:

if(pc.readable()==1)

The next issue is much worse. Inside I change the sweep or dimming rate and user output. Also there a few print statements in there to inform the user of what is happening. This is bad practice!!!!. You should never do these sorts of things inside an IRS:

//Print results and reset timers
             pc.printf("\033[2J\033[1;1H");
             pc.printf("u and d to change sweep speed, i j to change decay, r to reset\n");
             pc.printf("Sweep: %f Decay: %f \n", sweep_speed,dim_time);
             Sweeper.detach();
             Sweeper.attach(&LedSweep,sweep_speed);
             Dimmer.detach(); 
             Dimmer.attach(&LedDim,dim_time); 

The type of operations should be in the main loop. The ISR should have flags that tell the main program to process these operations. Why is this a bad thing to do? Because it takes time! An IRS function should be lean and fast! If an ADC were added to this system and an interrupt used, data may be lost if changes are made while this IRS is dealt with its code. If heavy code were in the main loop and triggered by a flag from IO_check, the ADC could interrupt these operations smoothly.

So why did I do it like this? Well this example was simple enough. I wanted to write the code with nothing in the main loop just for the hell of it ;) If you spot any glaring errors let me know and thanks for reading my first contribution to the mbed.org.

Meet KITT

#include "mbed.h"
#define LED_MAX 4   //LEDs
#define IO_FREQ .25 //Check IO (sec)
#define SWEEP_SPEED 0.2 //Sweep default (sec)
#define DIM_TIME 0.008 //Decay default (sec)

//Define timer variables that can change
float sweep_speed = SWEEP_SPEED;
float dim_time  = DIM_TIME;

//PC IO
Serial  pc(USBTX, USBRX); // tx, rx

//PWM led intensity
PwmOut led[4]= {LED1,LED2,LED3,LED4};
float volatile PWMledBright[LED_MAX];

Ticker Dimmer;
Ticker Sweeper;
Ticker IO;
int volatile current_led=0;
int volatile direction=1;

void LedDim()
{
    for(int i=0; i<4; i++) {
        i!=current_led?led[i]=led[i]*.90:led[i]=1.0;
    }
}

void LedSweep()
{
    //Sweep LED in directon
    current_led=current_led+direction;
//Check for ends and change direction
    if(current_led==LED_MAX-1) {
        direction=-1;
    }
    if(current_led==0) {
        direction=+1;
    }
}

void IO_check()
{
//Take care of IO
    char c;
    if(pc.readable()==1) {
        c=pc.getc();
    } else {
        return;
    };
    if(c == 'r') {
        sweep_speed=SWEEP_SPEED;
        dim_time=DIM_TIME;
    }
    if(c == 'u') {
        sweep_speed=sweep_speed*1.1;
    }
    if(c == 'd') {
        sweep_speed=sweep_speed*0.9;
    }
    if(c == 'i') {
        dim_time=dim_time*1.1;
    }
    if(c == 'j') {
        dim_time=dim_time*0.9;
    }
//Print results and reset Tickers
    pc.printf("\033[2J\033[1;1H");
    pc.printf("u and d to change sweep speed, i j to change decay, r to reset\n");
    pc.printf("Sweep: %f Decay: %f \n", sweep_speed,dim_time);
    Sweeper.detach();
    Sweeper.attach(&LedSweep,sweep_speed);
    Dimmer.detach();
    Dimmer.attach(&LedDim,dim_time);
}


int main()
{
//Test the LED PWM

//Set LEDs at different intensity
    led[0]=0.015;
    led[1]=0.025;
    led[2]=0.050;
    led[3]=1.00;

//Test the dimmer
    //User info
    //Timers
    Dimmer.attach(&LedDim,dim_time);
    Sweeper.attach(&LedSweep,sweep_speed);
    IO.attach(&IO_check,IO_FREQ);
    while(1) {
        //Do nothing


    }
}

Import programKITT_LED

Make the four LED look like KITT from Knight Rider


1 comment on Pulse Width Modulation and interrupts: a Knight Rider mbed!:

21 Nov 2021 This post is awaiting moderation

what is pin out or whats pins used ?

Please log in to post comments.