RSS

Archive for September, 2012

Single Conversion ADC on the STM8S

Monday, September 17th, 2012

In this post we will have a look at the Analog to Digital Converter (ADC) on the STM8S microcontroller. The number of ADCs available will depend upon the STM8S you are using. We will be using ADC1 which should be present on all STM8S microcontrollers.

In order to show how the ADC works we will be using the STM8S as a dimmer switch for an LED. This simple example will demonstrate how we can read an analog value and use a PWM signal to control the brightness of an LED.

In order to do this we will need the following:

We will also be using Timer 1, Channel 4 to generate a PWM signal to control the brightness of the LED (see Generating PWM Signals using the STM8S.

The algorithm we will be using is as follows:

  1. Configure the system
  2. Read the value from the ADC
  3. Set the PWM output based upon the analog reading
  4. Pause for 1/10th second
  5. Repeat from step 2

We will achieve this by using interrupts from the following resources:

  • Timer 3 – Generates the PWM signal which will be used to control the LED
  • Timer 2 – 1/10th second interrupt which triggers the ADC process
  • ADC – Conversion is completed, adjust the PWM output

ADC Features

The ADC has several modes of operations. We will be using the simplest, namely single conversion mode. As the name suggests, this mode performs a conversion on a specific channel. We will also instruct the microcontroller to generate an interrupt once the conversion is complete.

Amongst the other features and modes on the STM8S are the following:

  • Single scan mode – Perform a single conversion on a number of channels.
  • Continuous and Buffered Continuous – Perform continuous conversions. New conversions start as soon as the current conversion has completed.
  • Continuous Scan – Similar to the Single Scan but operating on a number of channels. Conversion restarts from channel 0 when the last channel has been converted.
  • Watchdog – Set upper and lower limits for the conversion. An interrupt can be generated if a conversion is above the upper or below the lower values.
  • External Trigger – An external trigger is used to start a conversion.

The conversion takes 14uS after a stabilisation period. Once the stabilisation is complete, readings are available without any further pauses.

The Registers

So let’s have a look at the registers we will be using in order to control the ADC:

  • ADC_CR2_ALIGN – Data alignment
  • ADC_CSR_CH – Channel selection
  • ADC_DRH/L – Analog conversion result
  • ADC_CR1_ADON – Turn ADC on / off, trigger conversion
  • ADC_CR3_DBUF – Data buffer Availability
  • ADC_CSR_EOCIE- Enable ADC interrupts
  • ADC_CSR_EOC – End of Conversion

ADC_CR1_ADON – ADC On/Off

The ADON flag determines of the ADC is on or off. It also determines if a conversion has been triggered. Setting ADON to 0 turns the ADC off. Setting ADON to 1 the first time turns the ADC on. Setting this value a second (or subsequent time) starts a conversion.

ADC_CSR_CH – Channel Selection

The CH flag determines the channel which should be converted.

ADC_CR2_ALIGN – Data Alignment

The ALIGN flag determines the type of alignment in the result registers. We will be setting this to right align (set to 1) the data in the registers.

ADC_DRH/L – Conversion Result

This pair of registers holds the result of the conversion. The order the registers should be read is dependent upon the alignment of the data in the registers. For right aligned data we need to read the DRL before DRH.

ADC_CSR_EOCIE – Enable ADC Interrupts

Turn the ADC interrupts on / off.

ADC_CSR_EOC – End of Conversion

This bit is set by the hardware when the conversion has completed. It should be reset by the software in the Interrupt Service Routine (ISR).

Unused Registers

The application we are going to be writing is simple and only performs a conversion once every 1/10th second. This is plenty of time to perform a conversion and process the data before the next conversion starts. As such, we do not need to check the overrun register. This register indicates if the data generated in the continuous mode was overwritten before it was used.

Hardware

This post requires some additional hardware to be added to the circuit containing the STM8S:

  • LED which is being controller through a transistor configured as a switch
  • Potentiometer to provide an analog signal for conversion

LED Output

Use the LED Output circuit in the post External Interrupts on the STM8S. Connect the base of the transistor to the output of Timer 1, Channel 3.

Potentiometer

Connect the potentiometer so that one pin is connected to ground and one to 3.3V. The output (the wiper) should be connected to AIN4 on the STM8S. I used a 10K potentiometer for this example.

Software

As we have already noted, we will be driving the application by using interrupts. We will also use a few techniques/methods from previous posts. So let’s look at each of the elements we will be using.

Timer 2 – Start Conversions

Timer 2 is used to generate 10 interrupts per second. Each interrupt will trigger a new conversion. Setting up the timer should look familiar:

void SetupTimer2()
{
    TIM2_PSCR = 0x05;       //  Prescaler = 32.
    TIM2_ARRH = 0xc3;       //  High byte of 50,000.
    TIM2_ARRL = 0x50;       //  Low byte of 50,000.
    TIM2_IER_UIE = 1;       //  Enable the update interrupts.
    TIM2_CR1_CEN = 1;       //  Finally enable the timer.
}

The ISR is a simple method, it has only one main function, namely to start the conversion.

#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    PD_ODR_ODR5 = !PD_ODR_ODR5;        //  Indicate that the ADC has completed.

    ADC_CR1_ADON = 1;       //  Second write starts the conversion.

    TIM2_SR1_UIF = 0;       //  Reset the interrupt otherwise it will fire again straight away.
}

Note the comment on the ADC register Second write starts the conversion. This method assumes that we have set ADC_CR1_ADON at least once previously. As you will see later, we set this register in the setup method for the ADC.

In addition to the starting of the conversion, we have also added a line of code to toggle PD5. This will show us when the ISR has been triggered and is really only there for debugging.

Timer 1, Channel 4 – PWM Signal

The ADC generates a 10 bit value. We will therefore set up Timer 1 to generate a PWM signal which is 1024 (210) clock signals in width. We can therefore use the value from the conversion to directly drive the PWM duty cycle.

void SetupTimer1()
{
    TIM1_ARRH = 0x03;       //  Reload counter = 1023 (10 bits)
    TIM1_ARRL = 0xff;
    TIM1_PSCRH = 0;         //  Prescalar = 0 (i.e. 1)
    TIM1_PSCRL = 0;
    TIM1_CR1_DIR = 1;       //  Down counter.
    TIM1_CR1_CMS = 0;       //  Edge aligned counter.
    TIM1_RCR = 0;           //  Repetition count.
    TIM1_CCMR4_OC4M = 7;    //  Set up to use PWM mode 2.
    TIM1_CCER2_CC4E = 1;    //  Output is enabled.
    TIM1_CCER2_CC4P = 0;    //  Active is defined as high.
    TIM1_CCR4H = 0x03;      //  Start with the PWM signal off.
    TIM1_CCR4L = 0xff;
    TIM1_BKR_MOE = 1;       //  Enable the main output.
    TIM1_CR1_CEN = 1;
}

Note that the Auto-reload registers is set to 1023 (0x3ff). We also set the capture compare registers to 1023 at the start. This will turn the LED off when the program starts.

System Clock

The program will be generating a PWM signal with a reasonably high clock frequency. In order to do this we will set the clock to use the internal oscillator running at 16MHz.

void InitialiseSystemClock()
{
    CLK_ICKR = 0;                       //  Reset the Internal Clock Register.
    CLK_ICKR_HSIEN = 1;                 //  Enable the HSI.
    CLK_ECKR = 0;                       //  Disable the external clock.
    while (CLK_ICKR_HSIRDY == 0);       //  Wait for the HSI to be ready for use.
    CLK_CKDIVR = 0;                     //  Ensure the clocks are running at full speed.
    CLK_PCKENR1 = 0xff;                 //  Enable all peripheral clocks.
    CLK_PCKENR2 = 0xff;                 //  Ditto.
    CLK_CCOR = 0;                       //  Turn off CCO.
    CLK_HSITRIMR = 0;                   //  Turn off any HSIU trimming.
    CLK_SWIMCCR = 0;                    //  Set SWIM to run at clock / 2.
    CLK_SWR = 0xe1;                     //  Use HSI as the clock source.
    CLK_SWCR = 0;                       //  Reset the clock switch control register.
    CLK_SWCR_SWEN = 1;                  //  Enable switching.
    while (CLK_SWCR_SWBSY != 0);        //  Pause while the clock switch is busy.
}

GPIO – Debug Signals

As with previous examples, we will configure some of the output ports so that we can generate debug signals:

void SetupOutputPorts()
{
    PD_ODR = 0;             //  All pins are turned off.
    //
    //  PD5 indicates when the ADC is triggered.
    //
    PD_DDR_DDR5 = 1;
    PD_CR1_C15 = 1;
    PD_CR2_C25 = 1;
    //
    //  PD4 indicated when the ADC has completed.
    //
    PD_DDR_DDR4 = 1;
    PD_CR1_C14 = 1;
    PD_CR2_C24 = 1;
}

ADC

The setup method for the ADC is relatively simple as many of the settings we will be using are the defaults after a reset. This method is as follows:

void SetupADC()
{
    ADC_CR1_ADON = 1;       //  Turn ADC on, note a second set is required to start the conversion.

#if defined PROTOMODULE
    ADC_CSR_CH = 0x03;      //  Protomodule uses STM8S105 - no AIN4.
#else
    ADC_CSR_CH = 0x04;      //  ADC on AIN4 only.
#endif

    ADC_CR3_DBUF = 0;
    ADC_CR2_ALIGN = 1;      //  Data is right aligned.
    ADC_CSR_EOCIE = 1;      //  Enable the interrupt after conversion completed.
}

After calling this method, the ADC should be powered on and ready to perform a conversion. Note that the ADC will not perform a conversion until ADC_CR1_ADON is set for a second time. This will be performed by the Timer 2 interrupt.

Another point to note is that on the Protomodule board the version of the STM8S does not have the AIN4 channel and so we use AIN3 instead.

The next method we will consider is the ADC ISR. This is where the real work of changing the values for the PWM signal takes place.

#pragma vector = ADC1_EOC_vector
__interrupt void ADC1_EOC_IRQHandler()
{
    unsigned char low, high;
    int reading;

    ADC_CR1_ADON = 0;       //  Disable the ADC.
    TIM1_CR1_CEN = 0;       //  Disable Timer 1.
    ADC_CSR_EOC = 0;        //     Indicate that ADC conversion is complete.

    low = ADC_DRL;            //    Extract the ADC reading.
    high = ADC_DRH;
    //
    //  Calculate the values for the capture compare register and restart Timer 1.
    //
    reading = 1023 - ((high * 256) + low);
    low = reading & 0xff;
    high = (reading >> 8) & 0xff;
    TIM1_CCR3H = high;      //  Reset the PWM counters.
    TIM1_CCR3L = low;
    TIM1_CR1_CEN = 1;       //  Restart Timer 1.

    PD_ODR_ODR4 = !PD_ODR_ODR4;     //  Indicate we have processed an ADC interrupt.
}

Note that we once again use a GPIO port to indicate when the ISR has been called.

Main Program Loop

The main program loop looks pretty much like the programs we have written in previous examples:

void main()
{
    //
    //  Initialise the system.
    //
    __disable_interrupt();
    InitialiseSystemClock();
    SetupTimer1();
    SetupTimer2();
    SetupOutputPorts();
    SetupADC();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

Results

If we put all of this together we can do the following:

The LED indicates the duty cycle of the PWM signal. The output on the oscilloscope confirms the changes being made to the PWM signal (the wider the high component of the signal, the brighter the LED should be).

By adjusting the trimmer potentiometer to ground (turning to the right) the LED becomes dimmer as the duty cycle becomes biased towards ground (off more than on). Turning to the left does the reverse, the PWM signal becomes biased to +3.3V (more on than off).

Conclusion

This example may be trivial as we could have easily just connected the LED and the potentiometer together. However, it does show how we can take a reading from an ADC and change the output of the microcontroller based upon the value.

As always, the source code is available for download. This application is compatible with my reference platform, the Variable Labs Protomodule and the STM8S Discovery board.

Source Code Compatibility

System Compatible?
STM8S103F3 (Breadboard)
Variable Lab Protomodule
STM8S Discovery

Timer 1 Counting Modes

Friday, September 14th, 2012

In this article we will continue to look at Timer 1, specifically:

  • counting modes
  • repetition counter
  • update/overflow events

This article will assume some knowledge from the following two posts published previously:

Unlike previous posts we will not be resetting the system clock but will instead leave this running using the default 2 MHz internal HSI oscillator.

Test Environment

In order to demonstrate the features of the timer I will be using my Saleae Logic Analyser as we will be observing events which occur many times a second. I have the following connections set up:

Saleae STM8S Pin Description
Ground N/A Ground
Black PD5 Indicate the state of the Timer 2 overflow interrupt
Brown Timer 1, Channel 4 PWM signal from Timer 1
Red PD4 Indicate state of Timer 1 (running or halted)

This set up will result in a series of charts from the Logic software which look like this:

Logic Analyser Output

Logic Analyser Output

The top portion of the display (labelled TIM2 Overflow) indicates when an overflow event has occurred on Timer 2. This timer is used to control the application. A change from low to high starts Timer 1 and the timer window for the observation runs to the next transition from high back to low. There is then a pause and then the whole cycle starts again.

The centre portion (labelled TIM1 PWM) shows the PWM output of Timer 1, channel 4. This is used to demonstrate what happens as we change the various values in the timer registers.

The lower portion (labelled TIM1 Overflow) shows when the Timer 1 overflow event occurs.

One thing that you can do to make the capture of these traces easier is to set a sample trigger on one of the traces. If you look at the trace labelled TIM2 Overflow you will see that one of the four square buttons is highlighted. This is showing that capture of data will begin when the logic analyser detects a signal which changes from low to high. You can make use of this by deploying and starting the application as follows:

  1. Compile and deploy the application
  2. Start data capture on the logic analyser
  3. Start the application

When you click the Start button on the logic analyser (step 2) a window will appear which indicates that the software is monitoring the data looking for a trigger (in this case rising edge on PD5) before it will start to capture data.

The Registers

The example code we will use shows how we can change the properties of the output and also the frequency of the interrupts generated by using the following registers:

  • TIM1_CR1_DIR – Counter Direction
  • TIM1_RCR – Repetition Counter

TIM1_CR1_DIR – Counter Direction

This register determines the direction of the counter, either up from 0 to the auto-reload values (set to 0) or down from the auto-reload value (set to 1).

TIM1_RCR – Repetition Counter

This counter can be used to determine the frequency of the overflow interrupts. Normally (i.e. by setting this register to 0) an overflow/underflow interrupt is generated every time the counter is reloaded from the auto-reload registers. By setting this register to any number other than zero (i.e. n), the overflow/underflow interrupt will only be generated after n + 1 overflow/underflows have been detected.

Software

We will use the registers described above to generate a series of 5 pulses every 50 mS. The code to do this is as follows:

#if defined DISCOVERY
    #include <iostm8S105c6.h>
#elif defined PROTOMODULE
    #include <iostm8s103k3.h>
#else
    #include <iostm8s103f3.h>
#endif
#include <intrinsics.h>

//--------------------------------------------------------------------------------
//
//  Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    if (PD_ODR_ODR5 == 1)
    {
        TIM1_CR1_CEN = 0;
        PD_ODR_ODR5 = 0;
        PD_ODR_ODR4 = 0;
    }
    else
    {
        //
        //  Force Timer 1 to update without generating an interrupt.
        //  This is necessary to makes sure we start off with the correct
        //  number of PWM pulses for the first instance only.
        //
        TIM1_CR1_URS = 1;
        TIM1_EGR_UG = 1;
        //
        //  Reset the indicators.
        //
        PD_ODR_ODR5 = 1;
        PD_ODR_ODR4 = 1;
        //
        //  Enable Timer 1
        //
        TIM1_CR1_CEN = 1;           //  Start Timer 1.
    }
    TIM2_SR1_UIF = 0;               //  Reset the interrupt otherwise it will fire again straight away.
}

//--------------------------------------------------------------------------------
//
//  Timer 1 Overflow handler.
//
#pragma vector = TIM1_OVR_UIF_vector
__interrupt void TIM1_UPD_OVF_IRQHandler(void)
{
    PD_ODR_ODR4 = !PD_ODR_ODR4; //0;                //  Signal to the user that Timer 1 has stopped.
    TIM1_CR1_CEN = 0;               //  Stop Timer 1.
    TIM1_SR1_UIF = 0;               //  Reset the interrupt otherwise it will fire again straight away.
}

//--------------------------------------------------------------------------------
//
//  Set up Timer 1, channel 3 to output a single pulse lasting 240 uS.
//
void SetupTimer1()
{
    TIM1_ARRH = 0x03;       //  Reload counter = 960
    TIM1_ARRL = 0xc0;
    TIM1_PSCRH = 0;         //  Prescalar = 0 (i.e. 1)
    TIM1_PSCRL = 0;
    //
    //  Select 0 for up counting or 1 for down counting.
    //
    TIM1_CR1_DIR = 0;       //  Up counter.
    //
    //  Select 0 for edge aligned, 1 for mode 1 centre aligned,
    //  2 for mode 2 centre aligned or 3 for mode 3 centre aligned.
    //
    TIM1_CR1_CMS = 0;       //  Edge aligned counter.
    //
    //  Set the following depending upon the number of PWM pulses, note
    //  n + 1 pulses will be generated before the interrupt.
    //
    TIM1_RCR = 4;           //  Repetition count.
    //
    //  Now configure Timer 1, channel 4.
    //
    TIM1_CCMR4_OC4M = 7;    //  Set up to use PWM mode 2.
    TIM1_CCER2_CC4E = 1;    //  Output is enabled.
    TIM1_CCER2_CC4P = 0;    //  Active is defined as high.
    TIM1_CCR4H = 0x01;      //  480 = 50% duty cycle (based on TIM1_ARR).
    TIM1_CCR4L = 0xe0;
    TIM1_BKR_MOE = 1;       //  Enable the main output.
    TIM1_IER_UIE = 1;       //  Turn interrupts on.
}

//--------------------------------------------------------------------------------
//
//  Setup Timer 2 to generate a 40 Hz interrupt based upon a 2 MHz timer.  This
//	will result in a signal with a frequency of 20Hz.
//
void SetupTimer2()
{
    TIM2_PSCR = 0x00;       //  Prescaler = 1.
    TIM2_ARRH = 0xc3;       //  High byte of 50,000.
    TIM2_ARRL = 0x50;       //  Low byte of 50,000.
    TIM2_IER_UIE = 1;       //  Enable the update interrupts.
    TIM2_CR1_CEN = 1;       //  Finally enable the timer.
}

//--------------------------------------------------------------------------------
//
//  Now set up the output ports.
//
//  Setup the port used to signal to the outside world that a timer event has
//  been generated.
//
void SetupOutputPorts()
{
    PD_ODR = 0;             //  All pins are turned off.
    //
    //  PD5 is used to indicate the firing of the update/overflow event for Timer 2
    //
    PD_DDR_DDR5 = 1;
    PD_CR1_C15 = 1;
    PD_CR2_C25 = 1;
    //
    //  PD4 is used to indicate the firing of the update/overflow event for Timer 1
    //
    PD_DDR_DDR4 = 1;
    PD_CR1_C14 = 1;
    PD_CR2_C24 = 1;
}

//--------------------------------------------------------------------------------
//
//  Main program loop.
//
void main()
{
    __disable_interrupt();
    SetupTimer1();
    SetupTimer2();
    SetupOutputPorts();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

If we now have a look at the output on the Logic Analyser we can see how the various signals indicate what is happening with the Timers.

Five Pulses

Five Pulses

The top trace of the output shows us when Timer 2 has overflowed (i.e. every 25mS). The code above goes through a cycle of enabling Timer 1 and 25mS later disabling Timer 1. You can see the output from Timer 1, Channel 3 in the middle of the three traces. The final (bottom) trace shows when Timer 1 overflows.

We have used much of the code presented above in previous examples in the series. We are setting up the two timers (Timer 1 and Timer 2, Channel 3) and adding interrupt handlers for the counter overflows. We are also setting up Port D as an output port to give us some diagnostic traces.

One difference from previous examples is that we are running this code at a slower clock frequency. By not setting up the system clock we are running at the default clock rate (2 MHz clock from the internal clock source).

Timer 1 Overflow Handler

This handler is really simple and does nothing more than use PD4 to indicate it has been called and then turns the timer off.

Timer 2 Overflow Handler

This handler is a little more complex. The first things to note is that it uses PD5 and works out if we are starting timers or pausing (PD5 = 1).

The next thing to note is the use of two more registers TIM1_CR1_URS and TIM1_EGR_UG. These two registers are used together to force the counter register to be reloaded without generating an interrupt. This is necessary to ensure that we start from a known value.

Conclusion

We have seen how we can use the logic analyser and some output pins (PD4 and PD5) to give us information about the sequence of events (interrupts). This is a very useful diagnostic tool and often the only one which is available to you when operating in this sort of environment.

I would also suggest trying some of the following and viewing the output on a logic analyser:

  • Changing the counting mode
  • Removing the update of TIM1_CR1_URS and/or TIM1_EGR_UG

As always, the source code is available for download.

Source Code Compatibility

System Compatible?
STM8S103F3 (Breadboard)
Variable Lab Protomodule
STM8S Discovery

Single Pulse Generation with the STM8S

Monday, September 3rd, 2012

I have recently been looking at using a sensor which uses a one-wire communication protocol. The protocol uses a single pulse of a defined length to trigger the unit to send the sensor reading back down the same wire. This lead me on to thinking about how I could achieve this, the results of which are documented here.

Whilst the main purpose of the code we will be developing in this post remains the same, i.e. to produce a single pulse of a defined length, I felt it important to show the two fundamental ways in which this can be achieved:

  • Interrupts and GPIO
  • Timers

Much of the first method, using interrupts and GPIO signals is a relatively straight forward case of modifying one of the previous examples, namely Using Timers on the STM8S.

The second method is more interesting as we look at using Timers to solve this problem. This will start us looking at using Timer 1. This is probably the most flexible and powerful of the Timers on the STM8S. This power and flexibility comes with a price, it is also the most complex of the timers we have at our disposal.

As an aside, we will look at measuring the length of the pulses we can generate with the aim of defining the minimum pulse length we can create using each of the methods.

So let’s start with a common problem definition. We will use both methods to generate a single pulse lasting 30 uS.

Method 1 – Interrupts and GPIO

This method requires only slight modifications to the code presented in Using Timers on the STM8S. So let’s start by downloading the example and modifying the code.

The first thing we will need to do is to modify the duration of the timer in order to generate and interrupt every 30 uS. In the original program we setup Timer 2 as follows:

//
//  Setup Timer 2 to generate a 20 Hz interrupt based upon a 16 MHz timer.
//
void SetupTimer2()
{
    TIM2_PSCR = 0x03;       //  Prescaler = 8.
    TIM2_ARRH = 0xc3;       //  High byte of 50,000.
    TIM2_ARRL = 0x50;       //  Low byte of 50,000.
    TIM2_IER_UIE = 1;       //  Enable the update interrupts.
    TIM2_CR1_CEN = 1;       //  Finally enable the timer.
}

From the previous article we know that the following formula applies:

(2TIM2_PSCR * counter) = fmaster / finterrupt

Now we are looking at generating a high frequency (low duration) pulse and so it is not unreasonable to set the prescalar to 1 (i.e. TIM2_PSCR = 0). This simplifies the formula to:

counter = fmaster / finterrupt

We also know that finterrupt is given by the following formula:

finterrupt = 1 / pulse duration

Putting the two together gives:

counter = fmaster * pulse duration

counter = 16,000,000 * 30 * 10-6

counter = 480 (0x1e0)

So our code becomes:

//
//  Setup Timer 2 to generate an interrupt every 480 clock ticks (30 uS).
//
void SetupTimer2()
{
    TIM2_PSCR = 0x00;       //  Prescaler = 1.
    TIM2_ARRH = 0x01;       //  High byte of 480.
    TIM2_ARRL = 0xe0;       //  Low byte of 480.
    TIM2_IER_UIE = 1;       //  Turn on the interrupts.
    TIM2_CR1_CEN = 1;       //  Finally enable the timer.
}

If you hook up oscilloscope and deploy the code you should find that the STM8S is generating square wave on Post D, Pin 5. The frequency of the signal should be 60 uS (see the previous article for an explanation where this comes from) with a duty cycle of 50%. Each of the components should have a width of 30 uS.

The next thing we need to do is to make the system generate a single pulse instead of a square wave. The solution is shockingly simple; in this case we turn off the timer interrupt after the first pulse has been generated.

The code in the Interrupt Service Routine (ISR) currently looks like this:

//
//  Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    PD_ODR_ODR4 = !PD_ODR_ODR4;     //  Toggle Port D, pin 4.
    TIM2_SR1_UIF = 0;               //  Reset the interrupt otherwise it will fire again straight away.
}

When we initialise the GPIO port we start with the output set to low. The timer interrupt code then toggles the GPIO port. So the first time this ISR is called the GPIO port goes high, the second time the GPIO port goes low etc. This means we need to turn off the interrupt when we transition from high to low for the first time. This results in the following code:

//
//  Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    unsigned char data;

    data = PD_ODR_ODR4;
    PD_ODR_ODR4 = !data;            //  Toggle Port D, pin 5.
    if (data == 1)
    {
        TIM2_IER_UIE = 0;           //  Only allow the pulse to happen once.
    }
    TIM2_SR1_UIF = 0;               //  Reset the interrupt otherwise it will fire again straight away.
}

This method turns off the Timer 2 interrupt and only the Timer 2 interrupt but resetting the interrupt enable flag for Timer 2 (TIM2_IER_UIE = 0). We could have called __disable_interrupt() here but this would have turned off all interrupts.

Deploying this code results in the following output on the oscilloscope:

And just to prove that the application generated a single pulse I hooked up the logic analyser and set this up to capture over 10 seconds worth of data. This resulted in the following output:

As you can see, we have what looks like a single pulse (see the logic analyser output). Zooming in on the pulse on the logic analyser output confirmed that there is indeed only a single pulse. A quick check of the oscilloscope output confirmed that the duration of the pulse is 30 uS.

Method 2 – Timers and PWM

In Generating PWM Signals using the STM8S we saw how we can generate a PWM signal without having to use interrupts. Here we will extend the principle to generating a single pulse using the One Pulse Mode (OPM) feature of Timer 1 (note that OPM is not available on Timer 2). As with the above, we will do this is two stages, namely to generate a PWM signal and then to restrict the output to a single pulse.

So let’s start by looking at Timer 1 and what we will need for this example.

TIM1_ARRH & TIM1_ARRL – Timer 1 Auto Reload Registers

As with Timer 2, these are two 8-bit registers which when combined make up the 16-bit counter value. To reset the 16-bit value we need to write to TIM1_ARRH before writing to TIM_ARRL as writing to TIM1_ARRL triggers the update of the registers.

TIM1_PSCRH & TIM1_PSCRL – Timer 1 Prescalar

This is a 16-bit register and allows finer control over the prescalar than we had with Timer 2. In this case the value can be any value from 0 to 65535. The frequency of the counter (fcounter) is given by the following frequency:

fcounter = fmaster / (Prescalar + 1)

This means that the range of the divisor used is actually 1 to 65536.

As with the auto-reload register, we should load the high bits before the low bits (i.e. TIM1_PSCRH before TIM1_PSCRL).

TIM1_RCR – Timer 1 Repetition Counter

The repetition counter allows for the timer to generate update events only when a number of repetitions of the counter underflow and overflow have occurred. This is a topic which is outside of the scope of this example and so we will set this to 0 for the moment and return to this topic in future examples.

TIM1_CR1 – Timer 1 Control Register 1

We will be ensuring that two bits in this register are set; namely TIM1_CR1_DIR and TIM1_CR1_CMS.

TIM1_CR1_DIR controls the direction of the counter as counter 1 can count from 0 upwards or from TIM_ARR down to 0. Setting this value to 0 means count upwards whilst 1 means count downwards.

TIM1_CR1_CMS determines the counter alignment. For this example we will be using edge aligned counting and will be setting this to 0. Note that this value is a two bit value and the meaning of the remaining values is left for a future discussion.

TIM1_CCRM4 – Timer 1 Capture/Compare Mode Register 4

As with Timer 2, we can control the PWM mode setting this to either mode 1 or mode 2. We will configure this channel to be operating in PWM mode 2. In this mode OC3 will be inactive as long as the counter < TIM1_CCR3.

TIM1_CCR4H & TIM1_CCR4L – Timer 1 Capture Compare Register 4

These registers together form a 16-bit value for use in Capture/Compare/PWM mode. In PWM mode, these values coupled with the TIM1_ARR registers will allow control of the duty cycle of a PWM signal.

TIM1_CCER2 – Timer 1 Capture/Compare Register 2

This register determines the output polarity and availability of Timer 1, channel 4 (amongst other things). The bits we are really interested in are the availability and the polarity of the output.

TIM1_CCER2_CC4E determines if the output is enabled or disabled; 0 is disabled, 1 is enabled.

TIM2_CCER2_CC4P determines the polarity of the active stage of the output. A polarity of 0 means that the active stage gives a high (logic 1) output, whilst a polarity of 1 gives a low (logic 0) output.

Software

So if we put all of this together we get an application which looks something like this:

//
//  This program shows how you can generate a single pulse using
//  timers on the STM8S microcontroller.
//
//  This software is provided under the CC BY-SA 3.0 licence.  A
//  copy of this licence can be found at:
//
//  http://creativecommons.org/licenses/by-sa/3.0/legalcode
//
#if defined DISCOVERY
    #include <iostm8S105c6.h>
#elif defined PROTOMODULE
    #include <iostm8s103k3.h>
#else
    #include <iostm8s103f3.h>
#endif
#include <intrinsics.h>

//
//  Setup the system clock to run at 16MHz using the internal oscillator.
//
void InitialiseSystemClock()
{
    CLK_ICKR = 0;                       //  Reset the Internal Clock Register.
    CLK_ICKR_HSIEN = 1;                 //  Enable the HSI.
    CLK_ECKR = 0;                       //  Disable the external clock.
    while (CLK_ICKR_HSIRDY == 0);       //  Wait for the HSI to be ready for use.
    CLK_CKDIVR = 0;                     //  Ensure the clocks are running at full speed.
    CLK_PCKENR1 = 0xff;                 //  Enable all peripheral clocks.
    CLK_PCKENR2 = 0xff;                 //  Ditto.
    CLK_CCOR = 0;                       //  Turn off CCO.
    CLK_HSITRIMR = 0;                   //  Turn off any HSIU trimming.
    CLK_SWIMCCR = 0;                    //  Set SWIM to run at clock / 2.
    CLK_SWR = 0xe1;                     //  Use HSI as the clock source.
    CLK_SWCR = 0;                       //  Reset the clock switch control register.
    CLK_SWCR_SWEN = 1;                  //  Enable switching.
    while (CLK_SWCR_SWBSY != 0);        //  Pause while the clock switch is busy.
}

//
//  Set up Timer 1, channel 4 to output a single pulse lasting 30 uS.
//
void SetupTimer1()
{
    TIM1_ARRH = 0x03;       //  Reload counter = 960
    TIM1_ARRL = 0xc0;
    TIM1_PSCRH = 0;         //  Prescalar = 0 (i.e. 1)
    TIM1_PSCRL = 0;
    TIM1_CR1_DIR = 0;       //  Up counter.
    TIM1_CR1_CMS = 0;       //  Edge aligned counter.
    TIM1_RCR = 0;           //  No repetition.
    //
    //  Now configure Timer 1, channel 4.
    //
    TIM1_CCMR4_OC4M = 7;    //  Set up to use PWM mode 2.
    TIM1_CCER2_CC4E = 1;    //  Output is enabled.
    TIM1_CCER2_CC4P = 0;    //  Active is defined as high.
    TIM1_CCR4H = 0x01;      //  480 = 50% duty cycle (based on TIM1_ARR).
    TIM1_CCR4L = 0xe0;
    TIM1_BKR_MOE = 1;       //  Enable the main output.
	//
	//	Uncomment the following line to produce a single pulse.
	//
//    TIM1_CR1_OPM = 1;
    TIM1_CR1_CEN = 1;
}

//
//  Main program loop.
//
void main()
{
    //
    //  Initialise the system.
    //
    __disable_interrupt();
    InitialiseSystemClock();
    SetupTimer1();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

If we run this application and hook up the oscilloscope to Timer 1, channel 4 (Pin 13 on the STM8S103F3 TSSOP20 package) we should find we get a PWM signal with a 60 uS period and a 50% duty cycle.

Timers and One Pulse Mode

Now that we have PWM functioning as expected we really only have to make one minor code modification, namely to set the timer generating a single pulse. For this we only need to add one line of code to the above application, SetupTimer1 becomes:

//
//  Set up Timer 1, channel 4 to output a single pulse lasting 30 uS.
//
void SetupTimer1()
{
    TIM1_ARRH = 0x03;       //  Reload counter = 960
    TIM1_ARRL = 0xc0;
    TIM1_PSCRH = 0;         //  Prescalar = 0 (i.e. 1)
    TIM1_PSCRL = 0;
    TIM1_CR1_DIR = 0;       //  Up counter.
    TIM1_CR1_CMS = 0;       //  Edge aligned counter.
    TIM1_RCR = 0;           //  No repetition.
    //
    //  Now configure Timer 1, channel 4.
    //
    TIM1_CCMR3_OC3M = 7;    //  Set up to use PWM mode 2.
    TIM1_CCER2_CC3E = 1;    //  Output is enabled.
    TIM1_CCER2_CC3P = 1;    //  Active is defined as high.
    TIM1_CCR3H = 0x01;      //  480 = 50% duty cycle (based on TIM1_ARR).
    TIM1_CCR3L = 0xe0;
    TIM1_BKR_MOE = 1;       //  Enable the main output.
    TIM1_CR1_OPM = 1;		//	Enable single pulse mode.
    TIM1_CR1_CEN = 1;
}

How Fast Can We Go?

Each of the above programs has their limitations. Assuming the same clock speed, the interrupt method is restricted by the number of instructions which must be executed in order to toggle the GPIO pin and work out if this is the first or seconded invocation of the ISR. The second is really only restricted by the speed of the system clock. This does not mean we cannot experiment to determine which is faster.

In both cases the programs were modified changing the timer auto-reload registers and the capture compare registers. The auto-reload register was always set to a value twice that of the capture compare register. For the interrupt method the fastest pulse which could be achieved was in the order of 2.5 uS (ARR = 0x0004) whilst the OPM method resulted in a pulse width of 146 nS (TIM1_ARR = 0x0002).

Conclusion

In this article we have looked at two methods which we can use to generate a single pulse. I am not offering advice on which is better, I’ll leave this to you as the application developer to decide.

Hopefully you will have gained an appreciation of the power of Timer 1. You should also have realised that using Timer 1 is not as simple as using Timer 2. There are a number of features we have not touched upon including (but not restricted to):

  • Capture/Compare
  • PWM Modes
  • Timer synchronisation

I am sure that we shall return to Timer 1 in future posts.

As always, the source code is available for download.

Source Code Compatibility

System Compatible?
STM8S103F3 (Breadboard)
Variable Lab Protomodule
STM8S Discovery

Interrupts on the STM8S

Sunday, September 2nd, 2012

A while ago I wrote about using interrupts on the STM8S (see External Interrupts on the STM8S and hinted there that I would come back to the topic and here we are. In this article we will cover the following:

  • List of interrupts available and the interrupt table
  • Writing your own Interrupt Service Routine (ISR)
  • Setting/Changing interrupt priorities

It is probably a good time to remind you that the STM8S Reference Manual (document number RM0016), available from ST’s web site is handy, to keep available for reference. This post is really meant for the application developer who wants to know which interrupts are available and how to use them.

Interrupt Table

The STM8S holds a list of interrupt vectors in a table in memory. As a programmer you are able to add your own ISRs to your application. These are really just conventional methods with a slightly different entry and exit mechanism. You don’t have to worry about how this mechanism works as you can make the compiler do all of the work for you. We shall see how later. The compiler is also be good enough to ensure that the table of interrupt vectors is also updated to point to your code as part of the application start up.

The following table lists the interrupts which are available on the STM8S103F3 microcontroller:

Vector Number Abbreviation Description
1 (0x01) TRAP TRAP
2 (0x02) TLI Top Level Interrupt
3 (0x03) AWU Auto Wake Up
4 (0x04) CLK Clock
5 (0x05) EXTI_PORTA External Interrupts for Port A (Pins 2 through 6 inclusive)
6 (0x06) EXTI_PORTB External Interrupts for Port B (All pins)
7 (0x07) EXTI_PORTC External Interrupts for Port C (All pins)
8 (0x08) EXTI_PORTD External Interrupts for Port D (Pins 0 through 6 inclusive)
9 (0x09) N/A Not used
10 (0x0a) N/A Not used
11 (0x0b) N/A Not used
12 (0x0c) SPI SPI
13 (0x0d) TIM1_UPD_OVF_TRG_BRK Timer 1 Update/Overflow/Trigger/Break
14 (0x0e) TIM1_CAP_COM Timer 1 Capture/Compare
15 (0x0f) TIM2_UPD_OVF_BRK Timer 2 Update/Overflow/Break
16 (0x10) TIM2_CAP_COM Timer 2 Capture/Compare
17 (0x11) TIM3_UPD_OVF_BRK Timer 5 Update/Overflow/Break
18 (0x12) TIM3_CAP_COM Timer 3 Capture/Compare
19 (0x13) UART_RX UART Rx
20 (0x14) UART_TX UART Tx
21 (0x15) I2C I2C
22 (0x16) UART2_RX UART 2 Rx
23 (0x17) UART2_TX UART 2 Tx
24 (0x18) ADC1 ADC1
25 (0x19) TIM4_UPD_OVF Timer 1 Update/Overflow
26 (0x1a) EEPROM_EEC EEPROM EEC

One thing to remember is that while the interrupt vector numbers remain unchanged the availability of the interrupt vectors will change depending upon the chip you are using. For instance, vector 10 is not used here but on the STM8S208 this is available to process one of the CAN interrupts. The simplest way to find out if an interrupt is available is to look at the header file for your chip. So let’s

So how do you know which interrupts are available for your microcontroller?

The first thing to note is that the vector numbers for interrupts 1-9 are usually the same as these features are available on most of the controllers. Note that whilst interrupt 9 is not available on my chip it is the EXTI_PORTE vector. I have not been able to locate any standard definitions (i.e. #define’s etc) for these interrupt vectors in any of the header files supplied with the compiler.

For the rest of the vectors we will have to start to look through the header files for the microcontroller. Opening up <iostm8s103f3.h> and going to the very end of the file we find a section with the comment Interrupt vector numbers. There should be one or more definitions for each of the features which allow the use of interrupts and which are available on the microcontroller.

One of the things to note about the list of available interrupts is that there are more than one #define statements for each feature. Consider the following extract:

/*-------------------------------------------------------------------------
 *      Interrupt vector numbers
 *-----------------------------------------------------------------------*/
#define SPI_TXE_vector                       0x0C
#define SPI_RXNE_vector                      0x0C
#define SPI_WKUP_vector                      0x0C
#define SPI_MODF_vector                      0x0C
#define SPI_CRCERR_vector                    0x0C
#define SPI_OVR_vector                       0x0C
#define TIM1_CAPCOM_TIF_vector               0x0D
#define TIM1_CAPCOM_BIF_vector               0x0D
#define TIM1_OVR_UIF_vector                  0x0D

If we look at the SPI definitions we can see that all of the vectors map to the same interrupt number. This is because there is only one ISR for this (SPI) feature. So the important point to take away is that your ISR must work out which condition it is working with.

Consider the example from the earlier post External Interrupts on the STM8S. This used the following ISR:

#pragma vector = 8
__interrupt void EXTI_PORTD_IRQHandler(void)
{
    PD_ODR_ODR3 = !PD_ODR_ODR3;     //  Toggle Port D, pin 3.
}

This method performed the same action every time this ISR was called. Now this did not matter for the example as we only had one switch attached to Port D. If we had another switch and LED attached to the same port then we would have had to work out which switch had been pressed in order to work out which action to take. Whilst this is simple in the case of switches the same principle applies to other features like SPI. In the case of SPI, the application should interrogate the status registers in order to work out why the ISR has been called.

Writing your own Interrupt Service Routine (ISR)

Writing your own ISR is really no different from writing any other method in C. There are a few extra rules which need to be followed but the majority of the techniques are the same. So let us return to the external interrupt code example:

#pragma vector = 8
__interrupt void EXTI_PORTD_IRQHandler(void)
{
    PD_ODR_ODR3 = !PD_ODR_ODR3;     //  Toggle Port D, pin 3.
}

The first thing you notice is the addition of the #pragma vector = 8 statement. This tells the compiler which interrupt vector we are going to be writing. In this case it is vector 8 which is the EXTI_PORTD interrupt vector (see the table above). You can also use the values found in the header file for you microcontroller. So you could write the following:

#pragma vector = SPI_TXE_vector

instead of:

#pragma vector = 0x0c

If you are using the same method for multiple vectors then you can provide the list of vector numbers as a comma separated list thus:

#pragma vector = SPI_TXE_vector, TIM1_OVR_UIF_vector

The next thing to notice is the __interrupt decoration which has been applied to the method. This tells the compiler that the method is to be used as an ISR. Knowing this, the compiler will ensure that the preamble and exit from the method are set up correctly as these are different from those of any other method which you might call.

Something which is not obvious from the above code is the fact the an ISR cannot take any parameters nor can it return a value. Hence the method definition takes a void parameter list and returns void.

You should also note that it is possible to write an interrupt method (i.e. decorate a method with __interrupt) without providing a #pragma vector statement. In this case the compiler will not generate an entry in the interrupt vector table.

You should also consider how fast the ISR needs to be. In this case, a single button press, we do not need any real efficiency as the microcontroller is not really doing anything overly complex. This may not be the case in non-trivial applications. In which case you will need to analyse your system to determine how fast the ISR needs to be. You also need to take into account the fact that one ISR may be interrupted by a higher priority ISR.

Setting/Changing interrupt priorities

Interrupts can be assigned a software priority from 0 to 3. This allows the interrupts to be nested ensuring that the most important interrupts receive attention over less important interrupts.

Interrupt priority is given by the following:

Priority Bit Value
0 10
1 01
2 00
3 11

Where the priority increases from the top of the table towards the bottom.

By default, all of the interrupts are assigned the same software priority, 3. This effectively means that the priority is disabled and all interrupts are treated as equal by the software. When the software priority is disabled or when two or more interrupts have the same priority then the interrupts are queued for processing

The software interrupt priority for the interrupt vectors (and hence your ISR) is stored in a group of registers. Each register (ITC_SPR1 through ITC_SPR8) holds the priority for four interrupt vectors. At reset all of these priorities are reset to 11 – software priority disabled. The register mapping is given by the following table:

Register Bits 7:6 Bits 5:4 Bits 3:2 Bits 1:0
ITC_SPR1 VECT3SPR VECT2SPR VECT1SPR VECT0SPR
ITC_SPR2 VECT7SPR VECT6SPR VECT5SPR VECT4SPR
ITC_SPR3 VECT11SPR VECT10SPR VECT9SPR VECT8SPR
ITC_SPR4 VECT15SPR VECT14SPR VECT13SPR VECT12SPR
ITC_SPR5 VECT19SPR VECT18SPR VECT17SPR VECT16SPR
ITC_SPR6 VECT23SPR VECT22SPR VECT21SPR VECT20SPR
ITC_SPR7 VECT27SPR VECT26SPR VECT25SPR VECT24SPR
ITC_SPR8 VECT29SPR VECT28SPR

Changing the priority is a simple matter of identifying the vector and changing the appropriate register (ITC_SPR1_VECT1SPR for vector 1, ITC_SPR1_VECT2SPR for vector 2 etc.).

Conclusion

Much of the information we have covered here is adequate for the average user intending to develop a module for the Netduino GO!. For more advanced topics such as what happens in low power modes, how interrupts are handled, nested interrupt handling etc. then you should refer to Chapter 6 in RM0016.