RSS

Archive for the ‘STM8’ Category

STM8S I2C Slave Device

Sunday, May 24th, 2015

Having succeeded at getting a basic I2C master working on the STM8S it is not time to start to look at I2C slave devices on the STM8S.

The project will be a simple one, the STM8S will take a stream of bytes and perform simple addition. When requested, the STM8S will return the total as a 16-bit integer. Each new write request will clear the current total and start the whole process again.

Simple enough so let’s get started.

This post is a fairly long as it contains a lot of code so it might be a good time to grab a beer and settle down.

Simple Slave Adder

The slave adder is a simple device running over I2C. The devices has the following properties:

  1. Acts as an I2C slave device with address 0x50
  2. I2C bus speed is 50 KHz
  3. Upon receiving a valid start condition and address for a write operation the device will clear the current total.
  4. Bytes written to the device will be summed and a running total kept.
  5. A start condition with a valid address for a read operation will return the current total as a 16-bit integer, MSB first.

A Netduino 3 will be used as the I2C bus master device. This runs the .NET Microframework and has an implementation of the I2C protocol built into the framework. This gives a good reference point for the master device i.e. someone else has debugged that so we can assume that the device is working as per the protocol specification.

As mentioned in the previous article on I2C Master devices, there is a really good article about the I2C protocol on Wikipedia. If you want more information about the protocol then I suggest you head over there.

Netduino Code

The initial version of the I2C master device will simply send two bytes of data for the I2C slave device to sum. The code is simple enough:

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace I2CMaster
{
    public class Program
    {
        public static void Main()
        {
            //
            //  Create a new I2C object on address 0x50 with the clock running at 50 KHz.
            //
            I2CDevice i2cBus = new I2CDevice(new I2CDevice.Configuration(0x50, 50));
            //
            //  Create a transaction to write two bytes of data to the I2C bus.
            //
            byte[] buffer = { 1, 2 };
            I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[1];
            transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
            while (true)
            {
                int bytesRead = i2cBus.Execute(transactions, 100);
                Thread.Sleep(1000);
            }
        }
    }
}

The above application creates a new instance of the I2CDevice with a device address of )x50 and a clock frequency of 50 KHz. A single transaction is created and the master writes the same two bytes to the I2C bus every second.

STM8S Code

As with the I2C Master article, much of the action happens in two places:

  1. I2C device initialisation method
  2. I2C Interrupt Service Routine (ISR)

The initialisation method sets up the I2C peripheral on the STM8S and enters the waiting state, waiting for the master to put data onto the I2C bus.

The ISR will deal with the actual data processing and in a full application it will also deal with any error conditions that may arise.

I2C Initialisation

The initialisation method sets up the I2C bus by performing the following tasks:

  1. Setting the expected clock frequency
  2. Setting the device address
  3. Turning the interrupts on

The I2C peripheral must be disabled before configuring certain aspects of the peripheral, namely clock speeds:

I2C_CR1_PE = 0;

The peripheral needs to know the current master clock frequency, the I2C mode (Standard or Fast) and the clock divider values. These must all be configured whilst the peripheral is disabled.

I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
I2C_CCRH_CCR = 0x00;

The device assumes that we will be using the standard 16 MHz clock speed which has been used in the other tutorials in this series. This is indicated by the I2C_FREQR register.

The values for the clock control register (I2C_CCRL and I2C_CCRH_CCR were simply taken from the STM8S programming reference manual. There is no coincidence that a bus speed of 50 KHz was chose, it is simply one of the reference values in the table. No point in creating work if you do not have to. If you want different speeds then you can either use one of the defined values in the remainder of the table or use the formulae provided.

The next step is to define the device address and addressing mode. I2C allows both 7 and 10 bit addressing modes. For simplicity this device will use a 7-bit device address:

I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
I2C_OARH_ADD = 0;                   //  Set this device address to be 0x50.
I2C_OARL_ADD = 0x50;
I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.

The device also allows for the maximum rise time to be configured. This example uses 17uS as the maximum time:

I2C_TRISER = 17;

The I2C peripheral allows for three different interrupt conditions to be defined, buffer interrupts, event interrupts (start condition etc.) and error interrupts. For convenience we will turn all of these on:

I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
I2C_ITR_ITERREN = 1;

Now that the peripheral is configured we need to re-enable it:

I2C_CR1_PE = 1;

The last bit of initialisation is to configure the device to return an ACK after each byte:

I2C_CR2_ACK = 1;

At this point the I2C peripheral should be listening to the I2C bus for a start condition and the address 0x50.

I2C Interrupt Service Routine (ISR)

The ISR contains the code which processes the data and error conditions for the I2C peripheral. All of the I2C events share the same ISR and the ISR will need to interrogate the status registers in order to determine the exact reason for the interrupt.

Starting with an empty ISR we have the following code:

#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
}

There are several other vector names we could have chosen, they all contain the same value and the value I2C_RXNE_vector was chosen arbitrarily.

Assuming that there are no errors on the bus, the I2C master code above will cause the following events to be generated:

  1. Address detection
  2. Receive buffer not empty

This initial simple implementation can use these two events to clear the total when a new Start condition is received and add the current byte to the total when data is received.

if (I2C_SR1_ADDR)
{
    //
    //  In master mode, the address has been sent to the slave.
    //  Clear the status registers and wait for some data from the salve.
    //
    reg = I2C_SR1;
    reg = I2C_SR3;
    _total = 0;                 // New addition so clear the total.
    return;
}

I2C_SR1_ADDR should be set when an address is detected on the bus which matches the address currently in the address registers. As a starter application the code can simply assume that any address is going to be a write condition. The code can clear the totals and get ready to receive data. Note that this will be expanded later to take into consideration the fact that the master can perform both read and write operations.

The next event we need to consider is the receipt of data from the master. This will trigger the Receive Buffer Not Empty interrupt. This simple application should just read the buffer and add the current byte to the running total:

if (I2C_SR1_RXNE)
{
    //
    //  Received a new byte of data so add to the running total.
    //
    _total += I2C_DR;
    return;
}

As indicated earlier, the I2C ISR is a generic ISRT and is triggered for all I2C events. The initialisation code above has turned on the error interrupts as well as the data capture interrupts. Whilst none of the code in this article will handle error conditions, the ISR will output the status registers for diagnostic purposes:

PIN_ERROR = 1;
__no_operation();
PIN_ERROR = 0;
reg = I2C_SR1;
BitBang(reg);
reg = I2C_SR3;
BitBang(reg);

This will of course require suitable definitions and support methods.

Putting this all together gives an initial implementation of a slave device as:

//
//  This application demonstrates the principles behind developing an
//  I2C slave device on the STM8S microcontroller.  The application
//  will total the byte values written to it and then send the total
//  to the master when the device is read.
//
//  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>
#else
    #include <iostm8s103f3.h>
#endif
#include <intrinsics.h>

//
//  Define some pins to output diagnostic data.
//
#define PIN_BIT_BANG_DATA       PD_ODR_ODR4
#define PIN_BIT_BANG_CLOCK      PD_ODR_ODR5
#define PIN_ERROR               PD_ODR_ODR6

//
//  Somewhere to hold the sum.
//
int _total;

//
//  Bit bang data on the diagnostic pins.
//
void BitBang(unsigned char byte)
{
    for (short bit = 7; bit >= 0; bit--)
    {
        if (byte & (1 << bit))
        {
            PIN_BIT_BANG_DATA = 1;
        }
        else
        {
            PIN_BIT_BANG_DATA = 0;
        }
        PIN_BIT_BANG_CLOCK = 1;
        __no_operation();
        PIN_BIT_BANG_CLOCK = 0;
    }
    PIN_BIT_BANG_DATA = 0;
}

//
//  Set up 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.
}

//
//  Initialise the I2C system.
//
void InitialiseI2C()
{
    I2C_CR1_PE = 0;                     //  Disable I2C before configuration starts.
    //
    //  Set up the clock information.
    //
    I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
    I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
    I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
    I2C_CCRH_CCR = 0x00;
    //
    //  Set the address of this device.
    //
    I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
    I2C_OARH_ADD = 0;                   //  Set this device address to be 0x50.
    I2C_OARL_ADD = 0x50;
    I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.
    //
    //  Set up the bus characteristics.
    //
    I2C_TRISER = 17;
    //
    //  Turn on the interrupts.
    //
    I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
    I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
    I2C_ITR_ITERREN = 1;
    //
    //  Configuration complete so turn the peripheral on.
    //
    I2C_CR1_PE = 1;
    //
    //  Acknowledge each byte with an ACK signal.
    //
    I2C_CR2_ACK = 1;
}

//
//  I2C interrupts all share the same handler.
//
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
    unsigned char reg;

    if (I2C_SR1_ADDR)
    {
        //
        //  Clear the status registers and wait for some data from the salve.
        //
        reg = I2C_SR1;
        reg = I2C_SR3;
        _total = 0;                 // New addition so clear the total.
        return;
    }
    if (I2C_SR1_RXNE)
    {
        //
        //  Received a new byte of data so add to the running total.
        //
        _total += I2C_DR;
        return;
    }
    //
    //  Send a diagnostic signal to indicate we have cleared
    //  the error condition.
    //
    PIN_ERROR = 1;
    __no_operation();
    PIN_ERROR = 0;
    //
    //  If we get here then we have an error so clear
    //  the error, output the status registers and continue.
    //
    reg = I2C_SR1;
    BitBang(reg);
    reg = I2C_SR3;
    BitBang(reg);
}

//
//  Main program loop.
//
int main()
{
    _total = 0;
    __disable_interrupt();
    //
    //  Initialise Port D.
    //
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR_DDR4 = 1;        //  Port D, bit 4 is output.
    PD_CR1_C14 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C24 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR5 = 1;        //  Port D, bit 5 is output.
    PD_CR1_C15 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C25 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR6 = 1;        //  Port D, bit 6 is output.
    PD_CR1_C16 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C26 = 1;         //  Pin can run up to 10 MHz.
    //
    InitialiseSystemClock();
    InitialiseI2C();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

Executing the Applications

At this stage a quick test with the two devices connected and the logic analyser will show the master device outputting data to the I2C bus. The correct operation of the I2C salve device can be verified by setting break points with in the two if statements in the ISR. Note that this will generate some errors but it is good enough to verify that the ISRs are being triggered correctly.

STM8 I2C Write Condition

STM8 I2C Write Condition

The above shows the output from the Saleae Logic Analyser when the write transaction is sent to the bus by the Netduino.

Reading and Writing with I2C Slaves

At this point the above code should be accepting data from the Netduino 3. It is now time to expand the code to take into account the requirement to read back data from the slave device.

Netduino Code

The Netduino code will need to be modified to generate both a write transaction to send data to the I2C slave device and a read transaction to retrieve the results from the calculation.

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace I2CMaster
{
    public class Program
    {
        public static void Main()
        {
            //
            //  Create a new I2C object on address 0x50 with the clock running at 50 KHz.
            //
            I2CDevice i2cBus = new I2CDevice(new I2CDevice.Configuration(0x50, 50));
            //
            //  Create a transaction to write two bytes of data to the I2C bus.
            //
            byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            byte[] resultBuffer = new byte[2];
            I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[2];
            transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
            transactions[1] = I2CDevice.CreateReadTransaction(resultBuffer);
            while (true)
            {
                int bytesRead = i2cBus.Execute(transactions, 100);
                Thread.Sleep(1000);
            }
        }
    }
}

The transaction array has been expanded to contain a second transaction to read the data from the device. The data to be summed has also been expanded to include a further eight elements.

STM8S Code

A state machine will be used to allow the STM8S to work out what action should be taken within the ISR. This is required as the i”c peripheral will generate an event for the address detection for both the write transaction and the read transaction. The application will need to be able to differentiate between the two conditions as one requires the total to be cleared, the other requires that the total is sent back to the master device.

The state machine contains the following states:

  1. Waiting for a write condition to start
  2. Adding data to the running total
  3. Sending the MSB of the total
  4. Sending the LSB of the total

This is represented in code by an enum and a global variable:

typedef enum
{
    ISWaiting,                    //  Waiting for comms to start.
    ISAdding,                     //  Adding bytes of data.
    ISSendingMSB,                 //  Sending MSB of total.
    ISSendingLSB                  //  Sending LSB of total.
} I2CStateType;
I2CStateType _i2cState;

The ISR will need to be changed in order to deal with the state machine and also handle the slave transmitting data to the master device. The first change is to the address detection. This assumes that the main program loop initialises the state to ISWaiting:

if (I2C_SR1_ADDR)
{
    //
    //  Slave address received, work out if to expect a read or a write.
    //
    if (_i2cState == ISWaiting)
    {
        _i2cState = ISAdding;
        _total = 0;             // New addition so clear the total.
    }
    reg = I2C_SR1;
    reg = I2C_SR3;
    return;
}

The code above works out if the application is waiting for the first write condition (IsWaiting) or if the address detection has been triggered by a read condition (any other state).

Next, any other data reception is assumed to be data to be added to the total. The state is changed to IsAdding just in case:

if (I2C_SR1_RXNE)
{
    //
    //  Receiving data from the master so we must be adding.
    //
    _total += I2C_DR;
    _i2cState = ISAdding;
    return;
}

Next we have two new conditions we have not considered so far, the first is the fact that we need to transmit the result to the master. The request from the client will trigger a Transmit Buffer Empty event:

if (I2C_SR1_TXE)
{
    if (_i2cState == ISAdding)
    {
        I2C_DR = (_total >> 8) & 0xff;
        _i2cState = ISSendingMSB;
    }
    else
    {
        I2C_DR = _total & 0xff;
        _i2cState = ISSendingLSB;
    }
    return;
}

The master is asking for two bytes and the application needs to track if we are transmitting the most significant byte (MSB) or least significant byte (LSB).

The next new condition is the acknowledge event. This is generated at the end of the master read operation. This will set the system back into a waiting condition:

if (I2C_SR2_AF)
{
    I2C_SR2_AF = 0;             //  End of slave transmission.
    _i2cState = ISWaiting;
    return;
}

The full application becomes:

//
//  This application demonstrates the principles behind developing an
//  I2C slave device on the STM8S microcontroller.  The application
//  will total the byte values written to it and then send the total
//  to the master when the device is read.
//
//  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>
#else
    #include <iostm8s103f3.h>
#endif
#include <intrinsics.h>

//
//  Define some pins to output diagnostic data.
//
#define PIN_BIT_BANG_DATA       PD_ODR_ODR4
#define PIN_BIT_BANG_CLOCK      PD_ODR_ODR5
#define PIN_ERROR               PD_ODR_ODR6

//
//  State machine for the I2C communications.
//
typedef enum
{
    ISWaiting,                    //  Waiting for comms to start.
    ISAdding,                     //  Adding bytes of data.
    ISSendingMSB,                 //  Sending MSB of total.
    ISSendingLSB                  //  Sending LSB of total.
} I2CStateType;
I2CStateType _i2cState;

//
//  Somewhere to hold the sum.
//
int _total;

//
//  Bit bang data on the diagnostic pins.
//
void BitBang(unsigned char byte)
{
    for (short bit = 7; bit >= 0; bit--)
    {
        if (byte & (1 << bit))
        {
            PIN_BIT_BANG_DATA = 1;
        }
        else
        {
            PIN_BIT_BANG_DATA = 0;
        }
        PIN_BIT_BANG_CLOCK = 1;
        __no_operation();
        PIN_BIT_BANG_CLOCK = 0;
    }
    PIN_BIT_BANG_DATA = 0;
}

//
//  Set up 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 Port D GPIO for diagnostics.
//
void InitialisePortD()
{
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR_DDR4 = 1;        //  Port D, bit 4 is output.
    PD_CR1_C14 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C24 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR5 = 1;        //  Port D, bit 5 is output.
    PD_CR1_C15 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C25 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR6 = 1;        //  Port D, bit 6 is output.
    PD_CR1_C16 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C26 = 1;         //  Pin can run up to 10 MHz.
}

//
//  Initialise the I2C system.
//
void InitialiseI2C()
{
    I2C_CR1_PE = 0;                     //  Disable I2C before configuration starts.
    //
    //  Set up the clock information.
    //
    I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
    I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
    I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
    I2C_CCRH_CCR = 0x00;
    //
    //  Set the address of this device.
    //
    I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
    I2C_OARH_ADD = 0;                   //  Set this device address to be 0x50.
    I2C_OARL_ADD = 0x50;
    I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.
    //
    //  Set up the bus characteristics.
    //
    I2C_TRISER = 17;
    //
    //  Turn on the interrupts.
    //
    I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
    I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
    I2C_ITR_ITERREN = 1;
    //
    //  Configuration complete so turn the peripheral on.
    //
    I2C_CR1_PE = 1;
    //
    //  Set the acknowledge to be ACK.
    //
    I2C_CR2_ACK = 1;
}

//
//  I2C interrupts all share the same handler.
//
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
    unsigned char reg;

    if (I2C_SR1_ADDR)
    {
        //
        //  Slave address received, work out if to expect a read or a write.
        //
        if (_i2cState == ISWaiting)
        {
            _i2cState = ISAdding;
            _total = 0;             // New addition so clear the total.
        }
        reg = I2C_SR1;
        reg = I2C_SR3;
        return;
    }
    if (I2C_SR1_RXNE)
    {
        //
        //  Receiving data from the master so we must be adding.
        //
        _total += I2C_DR;
        _i2cState = ISAdding;
        return;
    }
    if (I2C_SR1_TXE)
    {
        if (_i2cState == ISAdding)
        {
            I2C_DR = (_total >> 8) & 0xff;
            _i2cState = ISSendingMSB;
        }
        else
        {
            I2C_DR = _total & 0xff;
            _i2cState = ISSendingLSB;
        }
        return;
    }
    if (I2C_SR2_AF)
    {
        I2C_SR2_AF = 0;             //  End of slave transmission.
        _i2cState = ISWaiting;
        return;
    }
    //
    //  Send a diagnostic signal to indicate we have cleared
    //  the error condition.
    //
    PIN_ERROR = 1;
    __no_operation();
    PIN_ERROR = 0;
    //
    //  If we get here then we have an error so clear
    //  the error, output the status registers and continue.
    //
    reg = I2C_SR1;
    BitBang(reg);
    BitBang(I2C_SR2);
    reg = I2C_SR3;
    BitBang(reg);
}

//
//  Main program loop.
//
int main()
{
    _total = 0;
    _i2cState = ISWaiting;
    __disable_interrupt();
    InitialisePortD();
    InitialiseSystemClock();
    InitialiseI2C();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

The above two pieces of code are fuller applications and if we execute these and hook up the logic anayser to the I2C bus we see the following output:

Adding 1 To 10

Adding 1 To 10

Reading From Multiple Devices

The I2C protocol allows for one or more devices with different slave addresses to be connected to the same I2C bus. The previous article used the TMP102 temperature sensor with slave address 0x48 and this article has created a slave device with address 0x50. It should therefore be possible to connect the two devices to the same bus and talk to each selectively.

Netduino Application

As with the previous examples, the Netduino 3 will be used as the I2C bus master. The applicaiton above will need to be merged with the code in the previous article.

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace I2CMaster
{
    public class Program
    {
        public static void Main()
        {
            //
            //  Create a new I2C object and the configurations for the STM8S and the TMP102.
            //
            I2CDevice.Configuration stm8s = new I2CDevice.Configuration(0x50, 50);
            I2CDevice.Configuration tmp102 = new I2CDevice.Configuration(0x48, 50);
            I2CDevice i2cBus = new I2CDevice(stm8s);
            //
            //  Create a transaction to write several bytes of data to the I2C bus.
            //
            byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            byte[] resultBuffer = new byte[2];
            I2CDevice.I2CTransaction[] transactions = new I2CDevice.I2CTransaction[2];
            transactions[0] = I2CDevice.CreateWriteTransaction(buffer);
            transactions[1] = I2CDevice.CreateReadTransaction(resultBuffer);
            //
            //  Create a transaction to read two bytes of data from the TMP102 sensor.
            //
            byte[] temperatureBuffer = new byte[2];
            I2CDevice.I2CTransaction[] reading = new I2CDevice.I2CTransaction[1];
            reading[0] = I2CDevice.CreateReadTransaction(temperatureBuffer);
            while (true)
            {
                //
                //  Read data from the I2C bus.
                //
                i2cBus.Config = stm8s;
                int bytesRead = i2cBus.Execute(transactions, 100);
                i2cBus.Config = tmp102;
                bytesRead = i2cBus.Execute(reading, 100);
                //
                //  Convert the reading into Centigrade and Fahrenheit.
                //
                int sensorReading = ((temperatureBuffer[0] << 8) | temperatureBuffer[1]) >> 4;
                double centigrade = sensorReading * 0.0625;
                double fahrenheit = centigrade * 1.8 + 32;
                //
                //  Display the readings in the debug window and pause before repeating.
                //
                Debug.Print(centigrade.ToString() + " C / " + fahrenheit.ToString() + " F");
                //
                //  Now display the results of the addition.
                //
                string message = "";
                for (int index = 0; index < buffer.Length; index++)
                {
                    message += buffer[index].ToString();
                    if (index == (buffer.Length - 1))
                    {
                        message += " = " + ((resultBuffer[0] * 256) + resultBuffer[1]).ToString();
                    }
                    else
                    {
                        message += " + ";
                    }
                }
                Debug.Print(message);
                Thread.Sleep(1000);
            }
        }
    }
}

The above code creates two I2C configuration object, one for the TMP102 and one for the STM8S device. The I2C bus object has the configuration changed depending upon which device is required.

Wiring up the Devices

Firstly, wire up the TMP102 sensor as described in the article on I2C master devices. The SDA and SCK lines should be connected to 3.3V via 4K7 resistors. Next connect the SDA and SCK lines from the STM8S device to the same point as SDA and SCK lines from the TMP102 sensor. Throw in the logic analyser connections and you get something like this:

TMP102 and STM8S I2C slave devices

TMP102 and STM8S I2C slave devices

Running the Application

Running the above two applications and starting the logic analyser generated the following output:

Reading From Two I2C Slaves

Reading From Two I2C Slaves

The write operation to the far left (W0x50) sends the 10 bytes to the STM8S device. This is followed by a read (R0x50) of two bytes from the same device, this can be seen about two thirds of the way from the left-hand side of the trace. The final operation is the read of the current temperature, R0x48, to the right of the trace.

Conclusion

The STM8S slave device above is a simple device. There are still plenty of modification which need to be made:

  1. Add error handling
  2. Allow for trapping conditions such as clock stretching

to name but a few. The code above should allow for simple devices to be put together is a short space of time.

STM8S I2C Master Devices

Saturday, May 23rd, 2015

I have finally managed to dig out the TMP102 temperature sensor from the back of the electronic breakout board cupboard. Why am I interested in this sensor, well it is about the only I2C device I actually own and I2C is one of the few areas I have not really looked at on the STM8S.

This article will explore the basics of creating a I2C master device using the STM8S as the bus master and the TMP102 as the slave device.

I2C Protocol

I2C (Inter-Integrated Circuit) is a communication protocol allowing bi-directional communication between two or more devices over two wires. The protocol allows a device to be in one of four modes:

  1. Master transmit
  2. Master receive
  3. Slave transmit
  4. Slave receive

The Wikipedia article contains a good description of the protocol and the various modes and the bus characteristics.

For the purposes of this post the STM8S will need to be in master mode as it will be controlling the communication flow with the temperature sensor which is essentially a dumb device.

Communicating with the TMP102

The TMP102 is a simple device which returns two bytes of data which represent the current reading from a temperature sensor. The process for reading the temperature is as follows:

  1. Master device transmits the slave address on the SDA line along with a bit indicating that it wishes to read data from the slave device
  2. Master device enters Master Receive mode
  3. Slave device transmits two bytes of data representing the temperature
  4. Master device closes down the communication sequence

The temperature can be calculated according to the following formula:

Temperature in centigrade = (((byte0 * 256) + byte1) / 16) * 0.0625

Wiring up the TMP102

The sensor breakout I have has 6 connections of which we will connect five:

Sensor Breakout Pin Connected to
SDA Microcontroller SDA line
SCK Microcontroller SCK
Vcc 3.3 V
GND Ground
ADR0 Ground

This should result in the breakout having an I2C address of 0x48.

Netduino Code

The Netduino 3 runs the .NET Microframework (NETMF). This has built in class for communicating over I2C. A simple application will give a reference point for how the protocol should work.

using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using System.Threading;

namespace TMP102
{
    public class Program
    {
        public static void Main()
        {
            //
            //  Create a new I2C device for the TMP102 on address 0x48 with the clock
            //  running at 50 KHz.
            //
            I2CDevice tmp102 = new I2CDevice(new I2CDevice.Configuration(0x48, 50));
            //
            //  Create a transaction to read two bytes of data from the TMP102 sensor.
            //
            byte[] buffer = new byte[2];
            I2CDevice.I2CTransaction[] reading = new I2CDevice.I2CTransaction[1];
            reading[0] = I2CDevice.CreateReadTransaction(buffer);
            while (true)
            {
                //
                //  Read the temperature.
                //
                int bytesRead = tmp102.Execute(reading, 100);
                //
                //  Convert the reading into Centigrade and Fahrenheit.
                //
                int sensorReading = ((buffer[0] << 8) | buffer[1]) >> 4;
                double centigrade = sensorReading * 0.0625;
                double fahrenheit = centigrade * 1.8 + 32;
                //
                //  Display the readings in the debug window and pause before repeating.
                //
                Debug.Print(centigrade.ToString() + " C / " + fahrenheit.ToString() + " F");
                Thread.Sleep(1000);
            }
        }
    }
}

Running this on the Netduino gives the following output in the debug window:

20.6875 C / 69.237500000000011 F
20.625 C / 69.125 F
20.6875 C / 69.237500000000011 F
20.6875 C / 69.237500000000011 F
20.625 C / 69.125 F
20.6875 C / 69.237500000000011 F
20.625 C / 69.125 F
20.625 C / 69.125 F
20.625 C / 69.125 F
20.625 C / 69.125 F
20.625 C / 69.125 F

Hooking up the Saleae Logic 16 gives the following output for a single reading:

I2C communication with a TMP102 and a Netduino 3

I2C communication with a TMP102 and a Netduino 3

STM8S Implementation

The NETMF class used above hides a lot of the low level work which the STM8S will have to manage. In order to communicate with the TMP102 the STM8S will have to perform the following:

  1. Enter master transmit mode
  2. Send the 7-bit address of the sensor
  3. Send a single bit indicating that we want to read from the sensor
  4. Wait for the slave to respond with an ACK
  5. Enter master receiver mode
  6. Read the bytes from the slave device. Send an ACK signal for all bytes except the last one.
  7. Send a NAK signal at the end of the sequence

The first task is to initialise the I2C system on the STM8S:

//
//  Initialise the I2C system.
//
void InitialiseI2C()
{
    I2C_CR1_PE = 0;                     //  Diable I2C before configuration starts.
    //
    //  Setup the clock information.
    //
    I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
    I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
    I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
    I2C_CCRH_CCR = 0x00;
    //
    //  Set the address of this device.
    //
    I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
    I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.
    //
    //  Setup the bus characteristics.
    //
    I2C_TRISER = 17;
    //
    //  Turn on the interrupts.
    //
    I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
    I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
    I2C_ITR_ITERREN = 1;
    //
    //  Configuration complete so turn the peripheral on.
    //
    I2C_CR1_PE = 1;
    //
    //  Enter master mode.
    //
    I2C_CR2_ACK = 1;
    I2C_CR2_START = 1;
}

Some of the initialisation for the I2C bus needs to be performed whilst the peripheral is disabled, notably the setup of the clock speed. The method above diables the I2C bus, sets up the clock and addressing mode, turns on the interrupts for the peripheral and then enables the I2C bus. Finally, the method sets up the system to transmit ACKs following data reception and then sends the Start bit.

The communication with the slave device is handled by an Interrupt Service Routine (ISR). The initialisation method above will have taken control of the bus and set the start condition. An interrupt will be generated once the start condition has been set.

The master then needs to send the 7-bit address followed by a 1 to indicate the intention to read data from the bus. These two are normally combined into a single byte, the top 7-bits containing the device address and the lower bit indicating the mode (read – 1 or write – 0).

if (I2C_SR1_SB)
{
    //
    //  Master mode, send the address of the peripheral we
    //  are talking to.  Reading SR1 clears the start condition.
    //
    reg = I2C_SR1;
    //
    //  Send the slave address and the read bit.
    //
    I2C_DR = (DEVICE_ADDRESS << 1) | I2C_READ;
    //
    //  Clear the address registers.
    //
    I2C_OARL_ADD = 0;
    I2C_OARH_ADD = 0;
    return;
}

This above code checks the status registers to see if the interrupt has been generated because of a start condition. If it has then the STM8S is setup to send the address of the TMP102 along with the read bit.

Assuming that no error conditions are generated, the next interrupt generated will indicate that the address has been sent to the slave successfully. This condition is dealt with by reading two of the status registers and clearing the address registers:

if (I2C_SR1_ADDR)
{
    //
    //  In master mode, the address has been sent to the slave.
    //  Clear the status registers and wait for some data from the salve.
    //
    reg = I2C_SR1;
    reg = I2C_SR3;
    return;
}

At this point the address has been sent successfully and the I2C peripheral should be ready to start to receive data.

The I2C protocol requires that the data bytes are acknowledged by the master device with an ACK signal. All except for the last data byte, this must be acknowledged with a NAK signal. The I2C_CR2_ACK bit determines if an ACK or NAK is sent following each byte.

The master device can then continue to hold control of the bus or it can send a STOP signal indicating that the flow of communication has ended.

These two conditions are dealt with by resetting the I2C_CR2_ACK bit and setting the I2C_CR2_STOP bit.

The fulll ISR for the I2C bus looks like this:

//
//  I2C interrupts all share the same handler.
//
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
    if (I2C_SR1_SB)
    {
        //
        //  Master mode, send the address of the peripheral we
        //  are talking to.  Reading SR1 clears the start condition.
        //
        reg = I2C_SR1;
        //
        //  Send the slave address and the read bit.
        //
        I2C_DR = (DEVICE_ADDRESS << 1) | I2C_READ;
        //
        //  Clear the address registers.
        //
        I2C_OARL_ADD = 0;
        I2C_OARH_ADD = 0;
        return;
    }
    if (I2C_SR1_ADDR)
    {
        //
        //  In master mode, the address has been sent to the slave.
        //  Clear the status registers and wait for some data from the salve.
        //
        reg = I2C_SR1;
        reg = I2C_SR3;
        return;
    }
    if (I2C_SR1_RXNE)
    {
        //
        //  The TMP102 temperature sensor returns two bytes of data
        //
        _buffer[_nextByte++] = I2C_DR;
        if (_nextByte == 1)
        {
            I2C_CR2_ACK = 0;
            I2C_CR2_STOP = 1;
        }
        return;
    }
}

Running the Application

The application needs to be rounded out a little in order to read and store the two data bytes in a buffer. Some diagnostics can also be provided by setting one of the ports on the STM8S to output and bit banging the data ready through this port. Add to this some initialisation code and the full application looks as follows:

//
//  This application demonstrates the principles behind developing an
//  I2C master device on the STM8S microcontroller.  The application
//  will read the temperature from a TMP102 I2C sensor.
//
//  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>
#else
    #include <iostm8s103f3.h>
#endif
#include <intrinsics.h>

//
//  Define some pins to output diagnostic data.
//
#define PIN_BIT_BANG_DATA       PD_ODR_ODR4
#define PIN_BIT_BANG_CLOCK      PD_ODR_ODR5
#define PIN_ERROR               PD_ODR_ODR6

//
//  I2C device related constants.
//
#define DEVICE_ADDRESS          0x48
#define I2C_READ                1
#define I2C_WRITE               0

//
//  Buffer to hold the I2C data.
//
unsigned char _buffer[2];
int _nextByte = 0;

//
//  Bit bang data on the diagnostic pins.
//
void BitBang(unsigned char byte)
{
    for (short bit = 7; bit >= 0; bit--)
    {
        if (byte & (1 << bit))
        {
            PIN_BIT_BANG_DATA = 1;
        }
        else
        {
            PIN_BIT_BANG_DATA = 0;
        }
        PIN_BIT_BANG_CLOCK = 1;
        __no_operation();
        PIN_BIT_BANG_CLOCK = 0;
    }
    PIN_BIT_BANG_DATA = 0;
}

//
//  Set up 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.
}

//
//  Initialise the I2C system.
//
void InitialiseI2C()
{
    I2C_CR1_PE = 0;                     //  Diable I2C before configuration starts.
    //
    //  Setup the clock information.
    //
    I2C_FREQR = 16;                     //  Set the internal clock frequency (MHz).
    I2C_CCRH_F_S = 0;                   //  I2C running is standard mode.
    I2C_CCRL = 0xa0;                    //  SCL clock speed is 50 KHz.
    I2C_CCRH_CCR = 0x00;
    //
    //  Set the address of this device.
    //
    I2C_OARH_ADDMODE = 0;               //  7 bit address mode.
    I2C_OARH_ADDCONF = 1;               //  Docs say this must always be 1.
    //
    //  Setup the bus characteristics.
    //
    I2C_TRISER = 17;
    //
    //  Turn on the interrupts.
    //
    I2C_ITR_ITBUFEN = 1;                //  Buffer interrupt enabled.
    I2C_ITR_ITEVTEN = 1;                //  Event interrupt enabled.
    I2C_ITR_ITERREN = 1;
    //
    //  Configuration complete so turn the peripheral on.
    //
    I2C_CR1_PE = 1;
    //
    //  Enter master mode.
    //
    I2C_CR2_ACK = 1;
    I2C_CR2_START = 1;
}

//
//  I2C interrupts all share the same handler.
//
#pragma vector = I2C_RXNE_vector
__interrupt void I2C_IRQHandler()
{
    if (I2C_SR1_SB)
    {
        //
        //  Master mode, send the address of the peripheral we
        //  are talking to.  Reading SR1 clears the start condition.
        //
        reg = I2C_SR1;
        //
        //  Send the slave address and the read bit.
        //
        I2C_DR = (DEVICE_ADDRESS << 1) | I2C_READ;
        //
        //  Clear the address registers.
        //
        I2C_OARL_ADD = 0;
        I2C_OARH_ADD = 0;
        return;
    }
    if (I2C_SR1_ADDR)
    {
        //
        //  In master mode, the address has been sent to the slave.
        //  Clear the status registers and wait for some data from the salve.
        //
        reg = I2C_SR1;
        reg = I2C_SR3;
        return;
    }
    if (I2C_SR1_RXNE)
    {
        //
        //  The TMP102 temperature sensor returns two bytes of data
        //
        _buffer[_nextByte++] = I2C_DR;
        if (_nextByte == 1)
        {
            I2C_CR2_ACK = 0;
            I2C_CR2_STOP = 1;
        }
        else
        {
            BitBang(_buffer[0]);
            BitBang(_buffer[1]);
        }
        return;
    }
    //
    //  If we get here then we have an error so clear
    //  the error and continue.
    //
    unsigned char reg = I2C_SR1;
    reg = I2C_SR3;
    //
    //  Send a diagnostic signal to indicate we have cleared 
    //  the error condition.
    //
    PIN_ERROR = 1;
    __no_operation();
    PIN_ERROR = 0;
}

//
//  Main program loop.
//
int main()
{
    __disable_interrupt();
    //
    //  Initialise Port D.
    //
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR_DDR4 = 1;        //  Port D, bit 4 is output.
    PD_CR1_C14 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C24 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR5 = 1;        //  Port D, bit 5 is output.
    PD_CR1_C15 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C25 = 1;         //  Pin can run up to 10 MHz.
    //
    PD_DDR_DDR6 = 1;        //  Port D, bit 6 is output.
    PD_CR1_C16 = 1;         //  Pin is set to Push-Pull mode.
    PD_CR2_C26 = 1;         //  Pin can run up to 10 MHz.
    //
    InitialiseSystemClock();
    InitialiseI2C();
    __enable_interrupt();
    while (1)
    {
        __wait_for_interrupt();
    }
}

Putting this in a project and running on the STM8S gives the following output on the Saleae logic analyser:

I2C Communication between STM8S and a TMP102

I2C Communication between STM8S and a TMP102

The output looks similar to that from the Netduino application above. Breaking out the calculator and using the readings in the above screen shot gives a temperature of 19.6 C which is right according to the thermometer in the room.

Conclusion

The above application shows the basics of a master I2C application. The code needs to be expanded to add some error handling to detect some of the errors that can occur (bus busy, acknowledge failures etc.) but the basics are there.

On to STM8S slave devices…

OpenIR Interrupts

Sunday, March 1st, 2015

A few nights ago I was working on implementing the processing the command sequences stored in the STM8S into IR pulses. The Win32 configuration application takes a series of high / low transitions measured in microseconds and turns the infra-red LED on and off accordingly. Doing this would require a signal capable of being triggered to the nearest microsecond.

This is where the plot fell apart.

Looking at the application now I think it is a case of over engineering the problem. By modifying the design parameters to be more realistic the problem can be overcome.

How Fast Do We Need To Be?

Previous articles have shown how a 2-3 MHz pulse can be generated by setting the system clock to 16MHz and toggling a port in a while loop. It has also been shown how Timer 2 can be used to generate a square wave using an interrupt based upon the clock pulse. The challenge is to generate and interrupt at a high enough frequency.

From the start it was recognised that the generation of a 1 MHz signal was ambitious using interrupts. Raising an interrupt on the STM8S takes 9 clock cycles and working on a 16MHz clock this only leaves 5 clock cycles for the work of the Interrupt SubRoutine (ISR) before the microcontroller returns to sleep and the whole cycle restarts.

So the question becomes, is this a problem?

Breaking out the calculator reveals that a 38KHz signal (a common frequency used for infra-red signals) has a period of 26 microseconds. So if we were to send a single pulse the ISR would have to respond within 26 microseconds. Having a carrier signal would not make sense if there were less than two carrier signal pulses within the infra-red signal. This would result in a minimum frequency of 19KHz which is well within the capabilities of the STM8S.

As I said, over engineering, or more likely, worrying about a problem which does not really exist.

Recreating the Nikon Shutter Trigger Signal

The changes which have been made are all aimed at making the remote control a universal remote control. It is i=now time to get back to the orignal aims of the project, namely to be able to trigger a Nikon camera.

The last post demonstrated the ability to create a configurable sequences of pulses / pauses and store these in the EEPROM. The next step is to convert these into a pulse sequence based upon a 38KHz carrier signal.

Although the predominant carrier frequency for an infra-red remote control is 38KHz, this implementation allows for the carrier frequency to be configured. In order to do this the software will need to change the counter and prescalar values of Timer 1, Channel 4. Four bytes of the EEPROM are reserved for these two values. The Windows application performs most of the work by calculating the required values:

int count = 0;
int prescalar = 0;
bool error = false;

if (value < 1)
{
    error = true;
}
else
{
    count = (int) (CLOCK_FREQUENCY / value);
    while ((prescalar < 65536) &amp;&amp; (count > 65535))
    {
        prescalar++;
        count = (int) (CLOCK_FREQUENCY / (value * prescalar));
    }
    error = (prescalar == 65536);
}

if (error)
{
    float maxFrequency = CLOCK_FREQUENCY / 65535;
    throw new ArgumentOutOfRangeException("CarrierFrequency", string.Format("Carrier frequency should be between 1 and {0} Hz.", maxFrequency));
}
else
{
    _memory[PULSE_DURATION_OFFSET] = (byte) (count >> 8);
    _memory[PULSE_DURATION_OFFSET + 1] = (byte) (count &amp; 0xff);
    _memory[PRESCALAR_OFFSET] = (byte) (prescalar >> 8);
    _memory[PRESCALAR_OFFSET + 1] = (byte) (prescalar &amp; 0xff);
}

value is the desired frequency in Hz. The code cycles through the possible prescalar values until the right combination of counter and prescalar vales are obtained. The prescalar for Timer 1 is a value between 0 and 65535. One thing to note is that the value of the counter and the prescalar may not always return an exact frequency, for example, a carrier signal of 38KHz results in counter and prescalar values which generate a 38,004Hz signal. Not exact but given that the remote is using the internal oscillator we cannot expect an exact output anyway.

The perfectionist in me finds this to be disturbing but it is something I’ll have to live with.

Back on the remote control the application needs to use the configuration information in the EEPROM in order to regenerate the carrier signal for the remote control.

//
//  Set up Timer 1, channel 4 to output a PWM signal (the carrier signal).
//
void SetupTimer1()
{
    TIM1_ARRH = *((unsigned char *) EEPROM_PULSE_DURATION);
    TIM1_ARRL = *((unsigned char *) (EEPROM_PULSE_DURATION + 1));
    TIM1_PSCRH = *((unsigned char *) EEPROM_PRESCALAR_OFFSET);
    TIM1_PSCRL = *((unsigned char *) (EEPROM_PRESCALAR_OFFSET + 1));
    //
    //  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.
    //
    //  Work out the 50% duty cycle based upon the count.
    //
    unsigned short fiftyPercentDutyCycle;
    fiftyPercentDutyCycle = *((unsigned char *) EEPROM_PULSE_DURATION);
    fiftyPercentDutyCycle <<= 8;
    fiftyPercentDutyCycle += *((unsigned char *) (EEPROM_PULSE_DURATION + 1));
    fiftyPercentDutyCycle >>= 1;
    TIM1_CCR4H = ((fiftyPercentDutyCycle >> 8) &amp; 0xff);
    TIM1_CCR4L = fiftyPercentDutyCycle &amp; 0xff;
    //
    TIM1_BKR_MOE = 0;       //  Disable the main output.
    TIM1_CR1_CEN = 1;       //  Enable the timer.
}

Much of this code uses the counter values from the EEPROM to set up the PWM on Timer 1 Channel 4.

Two changes which have been made which do not impact the frequency of the carrier signal are the last two lines of the method. Previous versions of the code turned the time output on and the timer off. Here the Timer is turned on and the output off. This has been done to remove the need for the AND gate in the LED driver section of the circuit:

LED Driver With AND Gate

LED Driver With AND Gate

The AND gate takes the PWM signal from the STM8S along with an output enable signal (Port D, pin 3) from the STM8S. Changing the code to disable the timer removes the need for the output enable signal. The timer output is set to active high and when the timer is disabled the output pin of the timer goes into a high state. This problem is resolved by leaving the timer active and disabling the main output of the timer.

Another small change is to Timer 2, the timer used to determine the duration of the output of the carrier frequency. The Windows application takes the output and pause periods in microseconds. The Nikon shutter signal looks like this:

Nikon Pulse Sequence in Windows Application

Nikon Pulse Sequence in Windows Application

When this is encoded in the EEPROM the sequence data looks as follows:

Nikon IR Sequence in EEPROM

Nikon IR Sequence in EEPROM

The memory is encoded as follows:

Memory Offset Length Description
0x00 0x10 Name of the remote control.
0x10 0x02 Timer 1 counter value for the carrier signal.
0x12 0x02 Timer 1 prescalar for the carrier signal.
0x14 0x01 Number of pulse sequences / commands in the EEPROM.
0x20 0x40 Length of the sequences / commands in the sequence table.
0x60 0x1f8 Sequence / command data.

Each of the commands is encoded as follows:

Memory Offset Length Description
0x00 0x08 Name of the command.
0x08 1 Number of pulses / pauses in the command (n).
0x09 n * 2 Command / pause duration in microseconds.

Now let’s look at how this data can be retrieved and used.

Timer 2 can be configured with a clock prescalar in the range 1 to 32,768. With a clock frequency of 16MHz a prescalar of 16 would drop the clock frequency to 1MHz. Doing this makes the calculation of the counter values as simple as recording the number of microseconds in the 16-bit values used for the counters. The setup code for Timer 2 becomes:

//
//  Setup Timer 2 ready to process the pulse data.
//
void SetupTimer2()
{
    TIM2_PSCR = 4;
    TIM2_ARRH = 0;
    TIM2_ARRL = 0;
    TIM2_EGR_UG = 1;
    TIM2_IER_UIE = 1;       //  Enable the update interrupts.
}

The Timer 2 interrupt handler currently looks are follows:

//
//  Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    _currentPulse++;
    if (_currentPulse == _numberOfPulses)
    {
        //
        //  We have processed the pulse data so stop now.
        //
        PD_ODR_ODR3 = 0;
        TIM2_CR1_CEN = 0;
        TIM1_BKR_MOE = 0;
        _currentState = STATE_WAITING_FOR_USER;
    }
    else
    {
        TIM2_ARRH = *_pulseDataAddress++;
        TIM2_ARRL = *_pulseDataAddress++;
        TIM1_BKR_MOE = !TIM1_BKR_MOE;       //  Toggle the T1 output.
        TIM2_CR1_URS = 1;
        TIM2_EGR_UG = 1;
    }
    //
    //  Reset the interrupt otherwise it will fire again straight away.
    //
    TIM2_SR1_UIF = 0;
}

Where _pulseDataAddress points to the next pulse to be processed and _numberOfPulses holds the count of the number of pulses in this sequence.

The code below walks through the list of commands looking for the pulse data for the sequence number held in the command variable.

//
//  We have enough command data.  Now work out where the command is.
//
_pulseDataAddress = (unsigned char *) (EEPROM_SEQUENCE_DATA_OFFSET);
unsigned char *length = (unsigned char *) EEPROM_SEQUENCE_LENGTH_TABLE_OFFSET;
for (unsigned char index = 0; index < command; index++)
{
    _pulseDataAddress += *((unsigned char *) length);
}

Now we have the pulse sequence is is necessary to set up Timer 2 and the global variables pointing to the pulse data and the number of pulses.

//
//  Now start processing the pulse data for the specified command.
//
_currentState = STATE_RUNNING;
_pulseDataAddress += MAX_PULSE_NAME_LENGTH;     //  Skip the name.
_numberOfPulses = *_pulseDataAddress++;
_currentPulse = 0;
TIM2_ARRH = *_pulseDataAddress++;
TIM2_ARRL = *_pulseDataAddress++;

The final step is to turn on Timer 1 output and start Timer 2 running:

//
//  Now we have everything ready we need to force the Timer 2 counters to
//  reload and enable Timers 1 &amp; 2.
//
TIM2_CR1_URS = 1;
TIM2_EGR_UG = 1;
TIM1_BKR_MOE = 1;
TIM2_CR1_CEN = 1;

Calling TransmitPulseData should allow the command sequence to be output. Adding the line of code TransmitPulseData(0); after the configuration code and hooking up the logic analyser results in the following output:

Nikon IR Sequence in Logic Analyser

Nikon IR Sequence in Logic Analyser

Conclusion

OpenIR has reached the point where command sequences can be edited and stored in the EEPROM and a single command sequence can be transmitted on a configurable carrier frequency.

Next steps:

  1. The pulse sequence storage in the Windows application still needs to be tidied up.
  2. When loading the EEPROM data the Windows application needs to regenerate the data for the list box containing the list of sequences.
  3. Add commands to send a determined sequence / command number.
  4. Redesign the board to remove the AND gate from the LED output.

Once the above has been completed additional connectivity options can be investigated.

For those who are interested, the source code for STM8S running the remote control is available for download.

Editing OpenIR Commands

Sunday, February 22nd, 2015

The OpenIR application can now store and retrieve the contents of the EEPROM. The previous article demonstrated how the basic IR parameters can be stored and retrieved from the STM8S EEPROM. It is now time to start to store and retrieve command / pulse sequences in the EEPROM ready for the STM8S to process.

Pulse Sequences

In the January design update a template for the EEPROM layout was presented. The final part of the EEPROM was the pulse sequences data. What has become apparent is that in addition to the pulse count data we should also be storing a meaningful name for the command. The names are not required by the STM8S as it can simply take a command number but they make the commands more meaningful when viewed in the Windows application. It is for this reason that we need to store the command name in the remote control.

Pulse Sequence Length Table

The Pulse Sequence Length table contains a list of the number of bytes in each pulse sequence. Each entry gives the offset from the start of the pulse data for each of the commands the remote control can transmit to a remote device.

Pulse Data

The pulse data table contains the information about the commands themselves. The first eight bytes contain the name of the command. The STM8S will ignore this as it is not needed in order to transmit the command to the remote device. The next byte contains the number of pulses/transitions in the command. The following pairs of bytes contain the counter values for the timer on the STM8S. The counter values determine the number of high / low sequences and their duration.

One important consideration is the length of the command name. If the name is too short then the name is not meaningful to the user, too long and the number of commands is reduced and the STM8S may become unusable.

Editing Pulses in the Configuration Application

The lower part of the configuration application contains a section for the creation, modification and deletion of commands in the remote control:

OpenIR Main Form

OpenIR Main Form

Each remote control command is a series of pulses (IR signal is on) and spaces (IR signal is off). Each on /off sequence can be represented by a number of microsecond periods, the sequence starts with an on pulse and each subsequent number represents a change from on to off and so on.

Editing the wave form has been facilitated with a simple form containing a list box to hold the pulse durations and a user control presenting a graphical representation of the pulse waveform:

Add Pulse Sequence

Add Pulse Sequence

The above waveform is the sequence required to activate the shutter on a Nikon camera.

Clicking on the Add button adds the name to the main form.

Main Form With Command

Main Form With Command

Clicking on the Show EEPROM displays the raw bytes in the EEPROM:

EEPROM Memory With Pulse Data

EEPROM Memory With Pulse Data

Conclusion

The changes to the application now allow for the storing of sequences in the EEPROM. Each command is given a name which is meaningful to the user.

The current implementation is not perfect and a number of changes are in the pipeline:

  1. The pulse sequences are currently stored as microsecond values. Exact counter values for Timer 2 will make the STM8S application smaller.
  2. Pulses are stored in the EEPROM s bytes but the configuration application uses ArrayLists and List objects for the command edit forms. It may be possible to provide a more elegant method for moving this data around the application.

EEPROM Memory Dump

Sunday, February 15th, 2015

One debugging feature I have been keen to add is the ability to see the EEPROM memory. This will aid the debugging of the code on the STM8S as it will be easier to see the data being consumed by the application running on the remote control. The EEPROM memory dump feature simply displays a grid of memory locations along with the contents.

Main Form

The main form has been modified to take values from the user and then to translate the contents of the user controls into data for the EEPROM.

For example, the carrier frequency needs to be translated into counter values for the PWM function in Timer 1. This could be sent over to the remote control as a frequency but then it would not be possible to verify if the remote control could generate the desired frequency until the remote control tried to use the values. If the Windows application is going to perform checks on the user input then it is logical that it should send over just the counter values and not the frequency. This offloads the code necessary to perform the translation from the remote control to the user application. Doing this makes the application on the STM8S smaller. Remember, we have an 8K code limit on the remote control when using the IAR compiler.

The first step is to make the controls on the user interface respond to the values being entered and also add a mechanism to show the form which will display the EEPROM memory:

OpenIR Main Form

OpenIR Main Form

EEPROM Class

The application will also need a class in order to hold the contents of the EEPROM. This class acts as an intermediary between the raw bytes in the EEPROM and the data displayed in the user controls. The properties in the class translate the bytes into data types which can be used by a C# application and visa versa.

For instance, consider the remote control name. The C# application would like to see a C# string object. In the remote control EEPROM this is a series of up to 16 bytes each holding one character. A property presents a convenient way of performing this translation. The EEPROM class will require a series of bytes to hold memroy contents:

private byte[] _memory;

The Name property can then translate the C# interface requirements into those required by the STM8S application:

public string Name
{
    get
    {
        string result;

        if (_memory != null)
        {
            int index = NAME_OFFSET;
            result = "";

            while ((index < NAME_LENGTH) && (_memory[index] != 0))
            {
                result += (char) _memory[index++];
            }
        }
        else
        {
            result = null;
        }
        return (result);
    }
    set
    {
        if (_memory == null)
        {
            _memory = new byte[EEPROM_LENGTH];
        }
        if (value.Length > NAME_LENGTH)
        {
            throw new ArgumentOutOfRangeException("Name", string.Format("Name must be less than {0} characters in length.", NAME_LENGTH));
        }
        for (int index = NAME_OFFSET; index < (NAME_OFFSET + NAME_LENGTH); index++)
        {
            if (index < value.Length)
            {
                _memory[index] = (byte) value[index];
            }
            else
            {
                _memory[index] = 0;
            }
        }
    }
}

Similar properties can be added for the carrier frequency etc.

EEPROM Form

The form displaying the EEPROM memory makes use of a Grid control in the Syncfusion Essential Studio Enterprise Edition Community edition. This suite of tools contains 650+ controls for a variety of platforms and has recently been made available at no charge to individual developers and small organisations with a low turnover (< $1m).

The form showing the EEPROM memory is simple and contains the memory contents and a button to close the form

EEPROM Memory Dump Form

EEPROM Memory Dump Form

The image above shows the memory when the following properties have been set:

  1. Name of the remote control (row 0x000, 16 bytes)
  2. Counter value for the PWM function on Timer1 (row 0x0010, first two bytes)
  3. Power LED status (row 0x0010, offset 5)

Conclusion

Development of the Windows interface is proceeding steadily. As much work as possible is being offloaded to the Windows application in order to streamline the code which needs to be written for the STM8S.

OpenIR Bidirectional Communication

Sunday, February 1st, 2015

Progress on the OpenIR project has been a little slow recently, Christmas has come and gone and now a heavy workload is slowing things down further. Having said that, today has seen the project pass another milestone with a Windows configuration application communicating with the an STM8S Discovery board over a TTL serial port.

This post will give an overview of the current progress.

Windows Configuration Application

One of the goals of the OpenIR project is to create a universal remote control. To this end the project will require a configuration application. Having a number of years experience in Windows programming it made sense for the Windows platform to host the first generation of the configuration application for the remote control.

In the previous post a number of command functions were identified as being essential to this project. The main concern for the initial development is the size of the application which IAR can support on the STM8S. This is limited to 8KB and the EEPROM transfer function is likely to consume the most memory and code space. This function has been targeted first as it is likely to identify any issues early on in the software development phase of the project.

The Windows configuration application is a classic WinForms application with three distinct areas:

  1. Communication settings (serial port)
  2. General configuration
  3. Commands (IR sequences)

This currently looks as follows:

Windows Configuration Application

Windows Configuration Application

The upper panel allows the user to select the COM port and the communication settings (baud rate, parity etc.). The two buttons allow the user to request the EEPROM data and write send the updated the EEPROM configuration back to the IR remote control module.

The middle section contains the controls which will show and allow the editing of the static configuration such as the name, carrier frequency etc.

The lower panel contains the command list the remote control can send. More on this in a future port.

The current application allows the communication settings to be changed and implements the Read EEPROM request.

Data Packet Format

The initial design of the data packets allows for a request or response to be transferred with an optional data packet. The basic format is as follows:

Offset Length Description
0 1 Data packet header (0xaa).
1 2 Length of the data packet (unsigned short), high byte first.
3 1 Command to be executed.
4 n Data required for this command.
4 + n 1 Checksum for the entire packet.

The packet header is an arbitrary value and 0xaa has been chosen as it is an alternating sequence of bits.

The initial design packet size was expected to be less than 256 bytes. As the design progressed it became apparent that is was desirable for the packets to be greater larger than 256 bytes.

There are a limited number of commands which have been identified for this project. At the current time this is set to be 7 and a single byte is sufficient.

The data packet is optional and in the case of the EEPROM read/write functions this will be the contents of the EEPROM either being read or written.

The checksum byte is a simple exclusive OR of all of the bytes in the packet from the initial packet header through the the end of the data packet. The starting value for the checksum is 0xaa.

The configuration application will first scan the PC for COM ports. Any available ports will be added to the drop down list of COM port names. Selecting a COM port will populate the fields with the default COM port configuration.

Clicking on the Read EEPROM button send a request to the STM8S. The STM8S will respond with the contents of the EEPROM. This can be seen in the following traces from the logic analyser.

The first trace shows the request packet (top trace) and the response from the IR remote control (lower trace):

EEPROM Request and Exchange

EEPROM Request and Exchange

Zooming on on the request trace we can see that the command 1 (request for EEPROM data) is sent to the IR remote control:

EEPROM Request

EEPROM Request

Moving along the timeline the lower trace shows the response from the module expanded:

EEPROM Data

EEPROM Data

Conclusion

The initial communications with the IR remote control and a PC has been successful. At this point in time the source code requires some clean up work. The next step is to enhance the EEPROM configuration allowing the EEPROM to be rewritten upon command from the Windows configuration application.

OpenIR Design Update

Sunday, January 4th, 2015

Revision A of the board is now working and can send a single IR sequence out to a device in the real world when the on board switch is pressed. If OpenIR is to be truly universal the system needs to be able to send a multitude of commands not just a single command. In order to do this we need to be able to store IR command sequences and also allow the user to select which IR sequence is transmitted.

The STM8S has been set up to connect the TTL serial port to the FTDI and RedBear BLE board ports. Doing this allows communication with the outside world (PC, iPhone etc.). The proposed solution uses the serial TTL port to send commands to the STM8S and for the STM8S to store details of the IR signals (carrier frequency, active period etc.) in the on chip EEPROM.

The chip along with the chosen have a limit built into them, the fact that the free version of the IAR tools have an 8 KByte limit. This limits what can be achieved on the STM8S microcontroller.

Serial Commands

The STM8S will listen on the serial TTL port for commands from the outside world. The following list of commands are proposed as a starting point:

Command ID Description
1 Get Remote control ID. This returns a text string which identified the remote control.
2 Set the remote control ID.
3 Get the carrier frequency. This gets the two bytes which are used by Timer 1 to determine the frequency of the PWM signal.
4 Set the carrier frequency. This set the two bytes which are used by Timer 1 to determine the frequency of the PWM signal.
5 Get the contents of the EEPROM pulse data store.
6 Set the contents of the EEPROM pulse data store on the STM8S.
7 Transmit pulses for sequence number x where x is the item in the payload.
8 Transmit pulses. This transmits and arbitrary sequences of pulses which are contained in the remainder of the payload.
9 Time Lapse mode. Send the pulses for sequence x after y seconds.
10 Reset the remote control.
11 Enable or disable the on board power LED.

A close look at the above shows that commands 1, 3 and 4 are related as are commands 2, 4 and 6. They are either getting or setting blocks of memory in the STM8S EEPROM. Given the reduced memory available and the limits of the tools it may be optimal reduce this to reading and writing the contents of the EEPROM. The configuration data would be processed on a device with more memory (PC, iPhone etc.) and the EEPROM image built and transmitted to the STM8S. The STM8S then simply needs to update the EEPROM. The final command set becomes:

Command ID Description
1 Get the contents of the EEPROM pulse data store.
2 Set the contents of the EEPROM pulse data store on the STM8S.
3 Transmit pulses for sequence number x where x is the item in the payload.
4 Transmit pulses. This transmits and arbitrary sequences of pulses which are contained in the remainder of the payload.
5 Time Lapse mode. Send the pulses for sequence x after y seconds.
6 Reset the remote control.
7 Enable or disable the on board power LED.

Layout of the EEPROM

The STM8S on the EEPROM stores the configuration of the remote control. The data stored is a mixture of basic configuration along details of the pulses for each command the remote control can transmit.

Offset Length Description
0x00 16 Text ID of the remote control
0x10 2 Two bytes which are used by Timer 1 to determine the frequency of the carrier signal. The carrier signal is assumed to be 50% duty cycle.
0x12 1 Number of command sequences stored in the EEPROM.
0x13 1 Number of seconds to use for the time lapse sequence.
0x14 12 Unused.
0x20 64 Length of the pulse sequences (0x20 = length of sequence 0, 0x21 = length of sequence 1 etc.).
0x60 512 Pulse data. It is assumed that the pulse sequences will start with an on period followed by and off period until the number of sequences have been consumed.

Conclusion

The basic layout of the EEPROM has been determined along with a proposed command sequence. The next step is to implement the STM8S code and some sample Windows code to configure the remote control.

Window Watchdog

Saturday, July 5th, 2014

Window watchdogs provide a mechanism for detecting software failures in two ways, firstly an early reset of the watchdog and secondly a failure to reset the watchdog in time. In this post we investigate how the window watchdog can be use and illustrate with some examples.

Hardware

Window Watchdog Control Register – WWDG_CR

This register has two components, the timer counter and the enable bit (WWDG_CR_WDGA – see below). The microcontroller will be reset when one of two possible conditions:

  • The counter switches from 0x40 to 0x3f (i.e. bit 6 in the counter changes from 1 to 0)
  • The counter is reset when the counter value is greater than the watchdog window register

Writing 0 to bit 6 will cause the microcontroller to be reset immediately.

Assuming that WWDG_WR contains the default reset value then the time out period (in milliseconds) is defined as follows:

tWWDG = tCPU * 12288 * (WWDG_CR & 0x3f)

where tCPU = 1 / fmaster

On the STM8S running at 16MHz a value of 0x40 represents one count which is equal to 0.768ms. So at 16MHz the time out period is:

tWWDG = 0.768 * (WWDG_CR & 0x3f)

Window Watchdog Enable Register – WWDG_CR_WDGA

Switch the Window Watchdog on (set to 1) or off (set to 0).

Window Watchdog Window Register – WWDG_WR

This register defines a time period where the watchdog counter should not be reset. If the counter (WWDG_CR) is reset when the counter value is greater than the value in this register the microcontroller will be reset. This can be illustrated as follows:

Watchdog Sequence

Watchdog Sequence

We can calculate the value of tWindowStart and ttimeout as follows (assuming a 16MHz clock):

tWindowStart = 0.768 * ((WWDG_CRinitial & 0x3f) – WWDG_WR)

and

ttimeout = 0.768 * (WWDG_CR & 0x3f)

where WWDG_CRinitial is the initial value in the WWDG_CR register.

The default reset value for this register is 0x7f which means that the counter can be reset at any time. In this case, a reset will only be generated if the counter drops below 0x40.

One important point to note is that when the window register is used the value written to the counter (WWDG_CR) must be between 0xc0 and 0x7f. This causes the counter to be reset and the counter value to be reset simultaneously.

Software

The function of the Window Watchdog will be illustrated using the following three examples:

  • WWDG_CR not reset
  • WWDG_CR reset outside the reset window
  • WWDG_CR reset inside the reset window

The first thing we need to do is add some code which will be used in all of the examples.

Common Code

Firstly, lets add the code which will be common to all of the examples:

//
//  This program demonstrates how to use the Window Watchdog 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
//
#include <iostm8S105c6.h>
#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.
}

//--------------------------------------------------------------------------------
//
//  Initialise the ports.
//
//  Configure all of Port D for output.
//
void InitialisePorts()
{
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR = 0xff;          //  All pins are outputs.
    PD_CR1 = 0xff;          //  Push-Pull outputs.
    PD_CR2 = 0xff;          //  Output speeds up to 10 MHz.
}

This code has been used many times in The Way of the Register series of posts. It simply sets the system clock to the high speed internal clock and configures Port D for output.

Example 1 – Continuous Reset

This example sets the Windows Watchdog running and then waits for the watchdog to trigger the system reset. We indicate that the application is running by generating a pulse on Port D, pin 2.

//--------------------------------------------------------------------------------
//
//  Initialise the Windows Watchdog.
//
void InitialiseWWDG()
{
    PD_ODR_ODR2 = 1;
    __no_operation();
    __no_operation();
    __no_operation();
    __no_operation();
    PD_ODR_ODR2 = 0;
    WWDG_CR = 0xc0;
}

The __no_operation() instruction in the above code allow the pulse to stabilise on the pin.

The WWDG_CR is set to 0xc0 to both set the value in the counter and enable the watchdog at the same time. This sets bit 6 in the counter to 11 and the remaining bits to 0 (i.e. the counter is set to 0x40). The effect of this is that the first down count event will cause bit 6 to be cleared and the counter to be set to 0x3f. This will trigger the reset event.

The main program simply sets everything up and then “pauses” by simply waiting for an interrupt:

//--------------------------------------------------------------------------------
//
//  Main program loop.
//
int main()
{
    //
    //  Initialise the system.
    //
    __disable_interrupt();
    InitialiseSystemClock();
    InitialisePorts();
    InitialiseWWDG();
    __enable_interrupt();
    //
    //  Main program loop.
    //
    while (1)
    {
        __wait_for_interrupt();
    }
}

If we run this application and connect PD2 and NRST to the oscilloscope we see the following trace:

Initial continuous reset

Initial continuous reset

The yellow trace shows the reset line being pulled low and gradually returning to high. The drop shows where the watchdog has caused the reset pin to be pulled low and the microcontroller to be reset. The blue trace shows the pulse on PD2. If we measure the time difference between the pulse on PD2 and the time that the reset pin is pulled low we find that this is 770uS. This is very close to the time for one count, 768uS.

To verify this we can change the value in the counter to say 0x48. In this case we should see the watchdog running for 9 counts and the system running for 6.912mS. Changing WWDG_CR = 0xc0 to WWDG_CR = 0xc8 gives the following output on the oscilloscope:

Continuous reset

Continuous reset

Measuring the time difference we get a value of 6.9mS.

Example 2 – Reset Outside Watchdog Window

Using the above code as a starting point we will look at the effects of the watch dog window register (WWDG_WR). This defines when the application is allowed to change the value in the counter. The first task is to change the initial value in the control register to 0xc1 and verify that we get a rest every 1.54ms (2 x timer period). So change the InitialiseWWDG method to the following:

void InitialiseWWDG()
{
    PD_ODR_ODR2 = 1;
    __no_operation();
    __no_operation();
    __no_operation();
    __no_operation();
    PD_ODR_ODR2 = 0;
    WWDG_CR = 0xc1;
}

Running this application on the STM8S Discovery board results in the following traces:

Reset Outside window

Reset Outside window

Now we have two counts (1.54mS) in order to change the value in the control register. First task is to modify the InitialiseWWDG method to define the window. We will define this to be 0x40:

void InitialiseWWDG()
{
    PD_ODR_ODR2 = 1;
    __no_operation();
    __no_operation();
    __no_operation();
    __no_operation();
    PD_ODR_ODR2 = 0;
    WWDG_CR = 0xc1;
    WWDG_WR = 0x40;
}

This means that for the first 768uS the control register should not be changed. If the register is changed during this period a reset will be triggered. To demonstrate this we will change the value in the control register immediately after the microcontroller has been initialised:

int main()
{
    //
    //  Initialise the system.
    //
    __disable_interrupt();
    InitialiseSystemClock();
    InitialisePorts();
    InitialiseWWDG();
    __enable_interrupt();
    //
    //  Main program loop.
    //
    while (1)
    {
        WWDG_CR = 0xc1;             //  Trigger a reset.
        __wait_for_interrupt();
    }
}

Deploying this application to the microcontroller results in the following trace on the oscilloscope:

Watchdog immediate reset

Watchdog immediate reset

As you can see, the system is reset almost immediately (there is virtually no time between the pulse on PD2 and the reset line being pulled low).

Example 3 – Reset Within the Watchdog Window

Starting with the common code we initialise the Window Watchdog with the following method:

//--------------------------------------------------------------------------------
//
//  Initialise the Windows Watchdog.
//
void InitialiseWWDG()
{
    WWDG_CR = 0x5b;         //  Approx 70ms total window.
    WWDG_WR = 0x4c;         //  Approx 11.52ms window where cannot reset
    WWDG_CR_WDGA = 1;       //  Enable the watchdog.
}

This code defines a period of 11.52ms where we cannot reset the window watchdog counter followed by a period of 9.216ms during which the watchdog counter must be reset in order to prevent the microcontroller from being reset.

A simple main application loop would look something like this:

//--------------------------------------------------------------------------------
//
//  Main program loop.
//
int main()
{
    //
    //  Initialise the system.
    //
    __disable_interrupt();
    InitialiseSystemClock();
    InitialisePorts();
    PD_ODR_ODR4 = 1;
    __no_operation();
    PD_ODR_ODR4 = 0;
    InitialiseWWDG();
    __enable_interrupt();
    //
    //  Main program loop.
    //
    while (1)
    {
        unsigned char counter = (unsigned char) WWDG_CR;
        if ((counter & 0x7f) < WWDG_WR)
        {
            WWDG_CR = 0xdb;     //  Reset the Window Watchdog counter.
            PD_ODR_ODR2 = !PD_ODR_ODR2;
        }
        //
        //  Do something here.
        //
    }
}

The initial pulse on PD4 indicates that the application has started. We can use this to detect the reset of the microcontroller. In this trivial application the main program loop simply checks to see if the current value of the counter is less than the value in WWDG_WR. If the counter is less than WWDG_WR then the system write a new value into the counter. The value written is 0x5b anded with 0x80, this brings the reset value into the value range (0xc0 – 0xff).

This application also pulses PD2 to indicate that the watchdog counter has been reset.

Deploying this application and hooking up the Saleae logic analyser gives the following trace:

Window watchdog initial trace

Window watchdog initial trace

As you can see, there is an initial pulse showing that the board is reset (top trace) and then a series of pulses showing that the counter is being reset (lower trace). Each up/down transition represents a watchdog counter reset.

This is a relatively trivial example so let’s spice this up and add in a timer.

To the code above add the following code:

//--------------------------------------------------------------------------------
//
//  Setup Timer 2 to generate a 12.5ms interrupt.
//
void SetupTimer2()
{
    TIM2_PSCR = 0x02;       //  Prescaler = 4.
    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.
}

This method will set up timer 2 to generate an interrupt every 12.5ms.

Adding the following will catch the interrupt:

//--------------------------------------------------------------------------------
//
//  Timer 2 Overflow handler.
//
#pragma vector = TIM2_OVR_UIF_vector
__interrupt void TIM2_UPD_OVF_IRQHandler(void)
{
    if (_firstTime)
    {
        InitialiseWWDG();
        _firstTime = 0;
    }
    else
    {
        unsigned char counter = (unsigned char) WWDG_CR;
        unsigned char window = WWDG_WR;
        BitBangByte(counter & 0x7f);
        BitBangByte(window);
        WWDG_CR = 0xdb;     //  Reset the Window Watchdog counter.
        counter = (unsigned char) WWDG_CR;
        BitBangByte(counter);
    }
    PD_ODR_ODR2 = !PD_ODR_ODR2;
    TIM2_SR1_UIF = 0;       //  Reset the interrupt otherwise it will fire again straight away.
}

The interrupt will, on first invocation, initialise the window watchdog. Subsequent invocations will output the values of the registers and reset the window watchdog.

We need some code to bit bang the register values:

#define SR_CLOCK            PD_ODR_ODR5
#define SR_DATA             PD_ODR_ODR3

//
//  BitBang the data through the GPIO ports.
//
void BitBangByte(unsigned char b)
{
    //
    //  Initialise the clock and data lines into known states.
    //
    SR_DATA = 0;                    //  Set the data line low.
    SR_CLOCK = 0;                   //  Set the clock low.
    //
    //  Output the data.
    //
    for (int index = 7; index >= 0; index--)
    {
        SR_DATA = ((b >> index) & 0x01);
        SR_CLOCK = 1;               //  Send a clock pulse.
        __no_operation();
        SR_CLOCK = 0;
    }
    //
    //  Set the clock and data lines into a known state.
    //
    SR_CLOCK = 0;                   //  Set the clock low.
    SR_DATA = 0;
}

The main program loop needs to be modified to set up the timer and registers etc. So replace the main program loop with the following:

//--------------------------------------------------------------------------------
//
//  Main program loop.
//
int main()
{
    //
    //  Initialise the system.
    //
    __disable_interrupt();
    InitialiseSystemClock();
    InitialisePorts();
    PD_ODR_ODR4 = 1;
    __no_operation();
    PD_ODR_ODR4 = 0;
    SetupTimer2();
    InitialiseWWDG();
    __enable_interrupt();
    //
    //  Main program loop.
    //
    while (1)
    {
        __wait_for_interrupt();
    }
}

Deploying and running this application gives the following output on the Saleae logic analyser:

Window watchdog full trace

Window watchdog full trace

Zooming in produces the following

Window watchdog zoomed in

Window watchdog zoomed in

Starting with the top trace and descending we can see the following values:

  • Reset pulse
  • Timer 2 interrupt triggers (up/down transitions)
  • Data (register values)
  • Clock signal

The decoded register values can be seen above the data trace. The first value is the current value of the counter. The second value is the value in the window watchdog register and the final value is the new value in the counter register.

Conclusion

The Window Watchdog provides a mechanism for the developer to detect software faults similar to the Independent Watchdog but further constrains the developer by defining a window where a counter reset by the application is not allowed.

STM8S Beep Function

Tuesday, July 1st, 2014

The beep function uses the 128 KHz LSI to generate a beep signal on the beep pin of the STM8S. This post demonstrates how to use this function.

Hardware

The signal is output on the beep pin. On the STM8S discovery board this is an alternative function on PD4 (port D, pin 4).

Setting the Alternative Function

The alternative functions are assigned to a pin by reprogramming the option bytes. This can be performed using the ST Visual Programmer. Start this application and read the option bytes from the STM8S:

Reading the options bytes using ST Visual Developer

Reading the options bytes using ST Visual Developer

  1. Select the STM8S model. On the STM8S Discovery board this is STM8S105C6
  2. Select the OPTION BYTE tab
  3. Read the option bytes from the microcontroller
  4. Check the value of the AFR7 bit

AFR7 needs to be set to Port D4 Alternative Function = BEEP on the STM8S Discovery board. This may be different on your STM8S so check the documentation for your microcontroller. To set the alternative function to beep:

Writing new options bytes using ST Visual Developer

Writing new options bytes using ST Visual Developer

  1. Select Port D4 Alternative Function = BEEP from the list of values in the alternative functions
  2. Write the option bytes back to the microcontroller

The beep function should now be assigned to PD4.

Registers

The beep function is controlled by three registers:

  • BEEP_CSR_BEEPSEL
  • BEEP_CSR_BEEPDIV
  • BEEP_CSR_BEEPEN

Beep Selection – BEEP_CSR_BEEPSEL

These bits set the multiplier for the Beep Divider.

BEEP_CSR_BEEPSEL Frequency (KHz)
0 fLSI / (8 * BEEPDIV)
1 fLSI / (4 * BEEPDIV)
2 or 3 fLSI / (2 * BEEPDIV)

Beep Divider – BEEP_CSR_BEEPDIV

Value for the prescalar divider.

BEEPDIV = 2 + BEEP_CSR_BEEPDIV

The documentation states that this should not be left set to the reset value (0x1f) and the value should be in the range 0 – 0x1e.

Beep Enable – BEEP_CSR_BEEPEN

Enable the beep function by setting this to 1, disable by setting this to 0.

Software

We start by providing a method for resetting the system clock to use the HSI:

//
//  This program demonstrates how to use the beep function 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
//
#include <iostm8S105c6.h>
#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.
}

The next task is to configure the beep function:

//--------------------------------------------------------------------------------
//
//  Initialise the Beep function.
//
void InitialiseBeep()
{
    BEEP_CSR_BEEPEN = 0;    //  Turn off the beep.
    BEEP_CSR_BEEPSEL = 0;   //  Set beep to fls / (8 * BEEPDIV) KHz.
    BEEP_CSR_BEEPDIV = 0;   //  Set beep divider to 2.
    BEEP_CSR_BEEPEN = 1;    //  Re-enable the beep.
}

The final piece of code is the main program loop. This sets up the clock and the beep function:

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

Compiling this code and deploying to the STM8S results in the following trace on the oscilloscope:

Beep Oscilloscope Output

Beep Oscilloscope Output

As you can see, the output is not exactly 8KHz.

Conclusion

The documentation states that the beep function can generate 1KHz, 2KHz and 4KHz. By changing the values of the prescalar and the selection register it appears that you can also go as low as 500Hz and as high as 32KHz.

STM8S Independent Watchdog

Saturday, June 21st, 2014

The Independent Watchdog (IWDG) allows the developer to detect software failures and reset the STM8S microcontroller restarting the application. This post demonstrates how this feature works and how the developer can change the time window for the watchdog.

Watchdogs offer the application developer a simple to use method of determining if an application has entered a state which means that it is locked or taking too long to perform an operation.

The first task is to define what we mean by too long. This will vary from one application to another but the key point is that this should be longer than the longest operation the application will perform plus a margin. Once we have this time period defined we know that the application must reset the IWDG before the time period has elapsed.

Hardware

Independent Watchdog Key Register – IWDG_KR

The Key Register controls the operation of the IWDG. The allowed values are as follows:

Value Description
0x55 Enable writing to the IWDG_PR and IWDG_RLR registers. These registers are write protected by default.
0xAA Reset the IWDG counter to the reload value. This value must be written to the key register periodically in order to prevent the watchdog from resetting the microcontroller.
0xCC Enable and start the IWDG.
0x00 Disable writing to the IWDG_PR and IWDG_RLR registers.

The final value in the table is not present in revision 9 of RM0016 – Reference Manual but does appear in the header files for the STD Peripheral Library.

In researching this post a key point in the documentation was missed, namely IWDG_PR and IWDG_RLR can only be written when the IWDG has actually been enabled. This means that the IWDG needs to be running before the prescaler and reload registers can be loaded with the values required by the application. If the IWDG is not running then the system will resort to the default reset values for these registers. This gives a watchdog window of 16ms. It also means that the application has 16ms to set the desired time window through the IWDG_PR and IWDG_RLR registers before the microcontroller is reset.

Independent Watchdog Prescaler – IWDG_PR and Independent Watchdog Reload Register – IWDG_RLR

These two registers define the watchdog window and can. The default values are IWDG_PR = 0 and IWDG_RLR = 0xff. This gives a watchdog window of approximately 16ms. To change the size of the watchdog window use the table below to determine the values which are appropriate for your application:

Prescaler Divider Prescaler Value (IWDG_PR) Period when RL = 0 Period when RL = 0xff
4 0 62.5 us 15.90 ms
8 1 125 us 31.90 ms
16 2 250 us 63.70 ms
32 3 500 us 127 ms
64 4 1.00 ms 255 ms
128 5 2.00 ms 510 ms
256 6 4.00 ms 1.02 s

The programming reference for the STM8S gives the following formula for determining the exact reset period:

T = 2 x TLSI x P x R

where:

T Timeout period
TLSI 1 / fLSI
P 2(IWDG_PR + 2)
R IWDG_RLR + 1

Additionally, the time between the last reset of the key register (i.e. writing 0xAA to IWDG_KR) is T + 6 x TLSI.

Software

So lets look at how we can implement this:

//
//  This program demonstrates how to use the Independent Watchdog timer
//  on the STM8S microcontroller in order to detect software failures and
//  reset the 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
//
#include <iostm8S105c6.h>
#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.
}

//--------------------------------------------------------------------------------
//
//  Initialise the ports.
//
//  Configure all of Port D for output.
//
void InitialisePorts()
{
    PD_ODR = 0;             //  All pins are turned off.
    PD_DDR = 0xff;          //  All pins are outputs.
    PD_CR1 = 0xff;          //  Push-Pull outputs.
    PD_CR2 = 0xff;          //  Output speeds up to 10 MHz.
}

The code above should be pretty familiar to anyone who has been following The Way of the Register series of posts. This sets up the system clock to run at 16MHz and sets port D as an output port.

//--------------------------------------------------------------------------------
//
//  Initialise the Independent Watchdog (IWDG)
//
void InitialiseIWDG()
{
    IWDG_KR = 0xcc;         //  Start the independent watchdog.
    IWDG_KR = 0x55;         //  Allow the IWDG registers to be programmed.
    IWDG_PR = 0x02;         //  Prescaler is 2 => each count is 250uS
    IWDG_RLR = 0x04;        //  Reload counter.
    IWDG_KR = 0xaa;         //  Reset the counter.
}

We follow this up by setting up the IWDG. The prescaler and counter values should give a 1ms window.

//--------------------------------------------------------------------------------
//
//  Main program loop.
//
int main()
{
    //
    //  Initialise the system.
    //
    __disable_interrupt();
    InitialiseSystemClock();
    InitialisePorts();
    InitialiseIWDG();
    __enable_interrupt();
    __halt();
}

The main program configures the system and then halts the microcontroller. Without the watchdog this would the end of the application. With the watchdog we should see the microcontroller continuously resetting. This can be verified by connecting an oscilloscope to Port D. If we do this we see the output on the port rise as the microcontroller is reset and the InitialisePorts method is called.

Independent Watchdog

Independent Watchdog

Now replace the __halt(); statement with the following code:

while (1)
{
    IWDG_KR = 0xaa;
}

This code continuously writes the I am alive message to the IWDG key register. Doing this forces the IWDG to reset the counters with the reload values and start again. Running this application we see a flat line following the initial reset of the microcontroller. The flat line indicates that the microcontroller is not being reset.

Conclusion

The Independent Watchdog provides a simple method for detecting software failures merely writing a reset value into the key register.

Earlier the point was made that it appears that the IWDG must be enabled (and hence running) before the prescaler and reload registers can be modified. This point is critical as the microcontroller will resort to the reset values otherwise.

Although not demonstrated here it is possible for the application to determine if the application has been started through the application of power to the microcontroller or because of a reset by the IWDG. This can be determined by checking the value in RST_SR_IWDGF. This will be set to 1 if the IWDG has caused the microcontroller to be reset or 0 otherwise. This is left as an exercise for the reader.

Download

The source code for this post is available for download.