STM8S-Discovery review and tutorial

Thursday, 3rd December 2009

STMicroelectronics recently released the STM8S-Discovery, an exceedingly cheap (RRP $7) evaluation kit for one of their 8-bit microcontrollers. It features the microcontroller itself (an STM8S105C6), running at up to 16MHz and offering 32KB of program memory, 2KB of RAM and 1KB of data EEPROM. This microcontroller has a solid set of on-board peripherals, including four timers (one advanced, one general-purpose, and one basic), SPI, I2C, UART, and ADC – so there are plenty of connectivity options. The device offers up to 38 general-purpose I/O pins.

STM8S-Discovery running a debugging session.

The evaluation board is pretty feature-packed, and includes an integrated ST-LINK for programming and debugging over USB. The circuit board has been designed so that you can simply snap off the ST-LINK part if you'd like to use the microcontroller on its own. Standard 0.1" pin headers are provided to permit you to connect the board to other components, and a small wrapping area is also present. A jumper can be used to select 5V or 3.3V operation.

A green LED and a touch sensitive key are built into the evaluation board; the device comes pre-programmed with a demo program that flashes this LED at different rates when you touch the key.

All in all, a decent piece of kit – but what really sets it apart is its price. ST's recommended price is $7; I bought mine for £4.25 from Farnell. All you need to do is provide a USB A to USB B cable and download the various development tools (Windows only at the time of writing), datasheets and libraries.

STM8S-Discovery in its packagingFirst impressions

I really wasn't expecting much for £4.25, but am very impressed with the hardware. It's solidly built and comes attractively packaged, with clear instructions on the back of the box on how to run the demo program (just plug it in to a USB port!) and where to go next for development tools.

Unfortunately, what appears to be lacking at the moment – not surprisingly for a new product – is guidance for absolute beginners with ST's microcontrollers. Hopefully that will improve as more people get hold of these new kits, as their incredibly low price and self-contained nature makes them ideal for beginners (no need to build up a collection of parts or buy a programmer to get started, just plug and play). I personally found the documentation quite baffling, and judging by a thread on Hack a Day I wasn't the only one.

Disclaimer

I'm not an expert with ST's microcontrollers, but I have at least managed to get something running on the microcontroller after a few frustrating hours spent with the current documentation. The following code may not be the best, but it is simple and it should work (if I've missed anything out or not been clear enough anywhere, please let me know so I can fix it). It doesn't go into any great detail; my assumption is that if you can get some code you've written yourself running on your evaluation board and understand how to use the basic peripherals offered by the microcontroller to work with the board's LED and touch key you should be ready enough to dive into the official documentation and sample code yourself!

Getting started

If you visit the STM8S-Discovery page, you will see options to download various pieces of software and development tools. You'll need to download the following:

There is a document that accompanies the development package, Developing and debugging your STM8S-DISCOVERY application code, which contains instructions on setting up a project – confusingly, these directions don't appear to apply to Raisonance's tool chain, and rely on copying and pasting files from the demo programs. You will need to register the compilers to be able to use them, and Cosmic's registration process is handled by a human so may take some time for you to receive your licence key.

Debugging one of the sample programs

One way to verify that everything is set up correctly and to try out the development tools is to build one of the sample programs included as part of the application development package. In this zip file you will find a directory named STM8S-Discovery_dev; extract this somewhere sensible. Run ST Visual Develop, and select File→Open Workspace. Open the file STM8S-Discovery_dev\Project\Discover\STVD\Cosmic\Discover.stw – this is the project that came pre-loaded onto the evaluation board. If you click Build→Build it should crunch away and after a few seconds should report that there were no errors.

We'll now need to set up the IDE to use your evaluation board's ST-LINK as its debugging instrument. Click Debug instrument→Target Settings and select Swim ST-Link in the dropdown. You can leave the other settings as they are; click OK to close the dialog.

STVD's debug instrument settings form

If you now click Debug→Start Debugging (or the blue "D" in the toolbar) the IDE should program the evaluation board and then enter the debugger in "Break" mode. Press Continue (F5) to start running the program; at this point you should be able to use the touch sensitive key on the board to change the rate at which the green LED flashes. When you're done, click Debug→Stop Debugging (or the red cross button in the toolbar) to stop debugging.

Hitting a breakpoint in the STVD debugger

The debugger shouldn't be especially surprising to anyone who has used a graphical debugger (e.g. Visual Studio) before. Try going to line 148 in main.c – BlinkSpeed++; – and selecting Edit→Insert/Remove Breakpoint. Start debugging as before, and you'll notice that when you touch the key this time the debugger breaks on that line. Click Debug→Continue and the program will continue. Marvellous – all pretty intuitive thus far.

Creating your own project

Creating your own project is a rather more involved process, as there's quite a lot you need to set up first. Hopefully this step-by-step guide should help!

New Project dialog
  1. Firstly, click File→New Workspace, and select Create Workspace and Project from the dialog that appears.
  2. Now, we need to store our workspace (analagous to a solution in Visual Studio) somewhere; create a new directory for the workspace that is preferably not inside Program Files and give the workspace a name. I'm going to go for "Blinkenlight" as my workspace name.
  3. Create a new project with the same name as the solution name. It should default to the same directory; keep this as it is. Select STM8 Cosmic as the toolchain.
  4. Select STM8S105C6 as the microcontroller.

With that done, you should have a shiny new workspace and project containing two files – main.c, containing the entry point for your application, and stm8_interrupt_vector.c, used to associate interrupt requests with interrupt service routines – more on those later. If you build the project and try to debug it you'll note that it does precisely nothing of use. Let's make it do something useful!

Adding the standard firmware library

To access the various peripherals of the microcontroller, ST have provided an extensive standard firmware library. You will need to download this from the STM8S documents and files page; it's the zip archive named STM8S firmware library. Open the zip archive, and copy the FWLib\library folder to your own project folder. You may wish to rename the library folder FWLib, so you should end up with the two folders Project\FWLib\inc and Project\FWLib\src.

There is a folder named project in the firmware library download – copy stm8s_conf.h from this folder into the root of your project folder.

Now, return to ST Visual Develop. Use the workspace panel to the left to create two new folders in your project – Source Files\FWLib and Include Files\FWLib. Add all of the files from FWLib\inc to Include Files\FWLib and stm8s_conf.h to the root of Include Files. You should now have something that looks like this:

A project set up with the firmware library and initial source files.

You will also need to inform the firmware library that you are using an STM8S105 microcontroller as opposed to the default STM8S208. Here are two ways of doing this – pick whichever seems easiest to you.

  1. Open stm8s.h in Project\FWLib\inc, comment out #define STM8S208 and uncomment #define STM8S105 near the top of the file.
  2. Pass the definition directly to the compiler by clicking Project→Settings, selecting the C Compiler tab and typing STM8S105 into the Preprocessor Definitions field. You will need to do this twice; once for the Debug configuration and once for Release (select the configuration with the drop-down box in the top left).
Adding a preprocessor definition to set the microcontroller type

Illuminating the LED using GPIO

The cathode of the evaluation board's LED is connected to PD0 on the microcontroller – that is, pin 0 of port D. By driving this pin low we could therefore illuminate the LED. If you consult the documentation for the standard firmware library – it's the stm8s_fwlib_um.chm file in the zip archive – you can see a number of helper functions dedicated to GPIO, or "general-purpose I/O". To use the GPIO functions, we need to do two things:

  1. Add FWLib\src\stm8s_gpio.c to the project under Source Files\FWLib.
  2. Enable inclusion of the relevant GPIO header files, achieved by uncommenting #define _GPIO (1) in stms8_conf.h

Both steps will need to be carried out whenever you want to use a new peripheral (e.g. a timer or the UART). Once that's done, you can modify main.c to read as follows:

#include "stm8s.h"

int main(void) {
    
    // Reset ("de-initialise") GPIO port D.
    GPIO_DeInit(GPIOD);
    
    // Initialise pin 0 of port D by setting it as:
    // - an output pin,
    // - using a push-pull driver,
    // - at a low logic level (0V), and
    // - 10MHz.
    GPIO_Init(GPIOD, GPIO_PIN_0, GPIO_MODE_OUT_PP_LOW_FAST);
    
    // Infinite loop.
    for(;;);
}

There are more comments than code there – using the firmware library makes life rather easy, once it's set up! If you start debugging that, you'll note that the LED does indeed light up. It's not much, but it's a sign of life.

Flashing the LED using a delay loop

The GPIO library provides a handy GPIO_WriteReverse() function, which inverts the state of a pin. By toggling PD0, we can make the LED flash. To slow this flashing down at a rate we can see, a delay loop is introduced that delays for 50,000 nops, plus overhead of the for loop structure, between calls to GPIO_WriteReverse().

#include "stm8s.h"

int main(void) {
    
    // Reset ("de-initialise") GPIO port D.
    GPIO_DeInit(GPIOD);
    
    // Initialise pin 0 of port D.
    GPIO_Init(GPIOD, GPIO_PIN_0, GPIO_MODE_OUT_PP_LOW_FAST);
    
    // Infinite loop.
    for(;;) {
        
        // Delay for a short while.
        u16 d;
        for (d = 0; d < 50000; ++d) {
            // Without a nop() in here, the entire loop would be optimised away!
            nop();
        }
        
        // Invert the LED pin's state to flash it.
        GPIO_WriteReverse(GPIOD, GPIO_PIN_0);
    }
}

The reason for the nop() is that without it, the compiler optimises away the entire for loop as it does nothing useful.

Well, that's a bit more dynamic, but surely there's a better way to do this than a hard-coded delay loop?

Timers

Timers – of which the STM8S has four with varying capabilities – are an extremely versatile peripheral. They are typically based around a counter, which counts up or down, and various events can be triggered when this counter reaches particular values. We'll start here by using the TIM3 peripheral.

To use TIM3 we need to perform the following steps, as we did previously for GPIO:

  1. Add FWLib\src\stm8s_tim3.c to the project under Source Files\FWLib.
  2. Uncomment #define _TIM3 (1) in stms8_conf.h

By default, the microcontroller uses its internal 16MHz RC oscillator ("HSI", or high-speed internal) divided by eight as a clock source. This results in a base timer frequency of 2MHz. When configuring a timer, you can specify a prescaler to further divide the clock frequency – if you were to divide the 2MHz clock frequency by 2000, the timer would count up once every millisecond.

TIM3 restricts the prescaler to powers of two between 1 and 32,768, so we'll use a prescaler of 2048 (that's close enough for jazz). You also need to specify a timer period; this is the value up to which the timer will count before resetting itself. With a period of 999, the timer will run from 0 to 999 before resetting – approximately one thousand milliseconds, or one second, from start to finish.

// Reset ("de-initialise") TIM3.
TIM3_DeInit();

// Set TIM3 to use a prescaler of 2048 and have a period of 999.
TIM3_TimeBaseInit(TIM3_PRESCALER_2048, 999);

// Enable TIM3.
TIM3_Cmd(ENABLE);

Using the standard firmware library allows for easy timer configuration. Now that we have a timer running, we can query its counter value to flash the LED – if it's in the range 0~499, switch the LED on; if it's in the range 500~999, switch the LED off. Building on the ealier source code, here's a program that does just that:

#include "stm8s.h"

int main(void) {
    
    // Reset ("de-initialise") GPIO port D.
    GPIO_DeInit(GPIOD);    
    // Initialise pin 0 of port D.
    GPIO_Init(GPIOD, GPIO_PIN_0, GPIO_MODE_OUT_PP_LOW_FAST);
    
    // Reset ("de-initialise") TIM3.
    TIM3_DeInit();
    // Set TIM3 to use a prescaler of 2048 and have a period of 999.
    TIM3_TimeBaseInit(TIM3_PRESCALER_2048, 999);
    // Enable TIM3.
    TIM3_Cmd(ENABLE);

    // Infinite loop.
    for(;;) {        
        if (TIM3_GetCounter() < 500) {        
            // Output a low on the LED pin to illuminate it.
            GPIO_WriteLow(GPIOD, GPIO_PIN_0);
        } else {
            // Output a high on the LED pin to switch it off.
            GPIO_WriteHigh(GPIOD, GPIO_PIN_0);
        }
    }
}

Pulse-width modulation for flashing

One of the many features of these timers is the ability to generate pulse-width modulation – PWM – output on dedicated pins. When this feature is enabled, the timer will set the output pin to one logic level when it starts or restarts and to another when it reaches a used-defined threshold. This is effectively what we're doing in our current program, just manually – far better if the timer could do it for us automatically!

The green LED is connected to PD0, which also acts as TIM3_CH2, or TIM3's channel 2. We can remove most of the code from our previous program, including the GPIO code, leaving us with the following:

#include "stm8s.h"

int main(void) {
        
    // Reset ("de-initialise") TIM3.
    TIM3_DeInit();
    
    // Set TIM3 to use a prescaler of 2048 and have a period of 999.
    TIM3_TimeBaseInit(TIM3_PRESCALER_2048, 999);
    
    // Initialise output channel 2 of TIM3, by setting:
    // - PWM1 mode (starts activated, deactivates when capture compare value is hit),
    // - output is enabled,
    // - capture compare value of 500, and
    // - an active signal is low (0V).
    TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 500, TIM3_OCPOLARITY_LOW);
    
    // Enable TIM3.
    TIM3_Cmd(ENABLE);

    // Infinite loop.
    for(;;);
}

In PWM1 mode, the output starts in the activated state. As we have specified that TIM3_OCPOLARITY_LOW is an activated state, this means that the output will start at a logic low (the LED will be illuminated). When the capture compare value (500) is reached, the output will switch to the deactivated state (logic high) and the LED will switch off. If you run this program as before you will see that the LED does indeed flash on and off automatically.

Pulse-width modulation to change brightness

As the LED is on for approximately 500ms and off for approximately 500ms it is on half of the time. On average, therefore, it is at half its possible brightness. If you modify the 500 in the TIM3_OC2Init function call to 250 and run the program again you will see that on for a quarter of the possible time, and at 750 it is on for three quarters of the possible time. By increasing the rate at which the LED flashes so that it appears to be continuously lit we can control its apparent brightness by adjusting the relative amount of time it is switched on in comparison to the time it is switched off.

We can increase the rate at which the LED flashes by reducing the prescaler of TIM3. Try changing the TIM3_TimeBaseInit call to use TIM3_PRESCALER_16 instead of TIM3_PRESCALER_2048, and change the capture compare value in the TIM3_OC2Init call to 100. When you run your program, the LED on the board will appear to be dimly lit. If you pick up the board and very carefully shake it from side to side you should be able to see that the LED is flashing from the dotted trace it leaves in the air. If you drop the prescaler all the way down to TIM3_PRESCALER_1 you will find that you have to shake the board much faster, but take care not to damage anything!

The brightness of the LED can be modified at runtime by changing the value of the capture compare register with the TIM3_SetCompare2 function.

#include "stm8s.h"

// Short delay loop.
void delay(void) {
    u16 d;
    for (d = 0; d < 150; ++d) {
        nop();
    }
}

int main(void) {
        
    // Reset ("de-initialise") TIM3.
    TIM3_DeInit();
    
    // Set TIM3 to use a prescaler of 1 and have a period of 999.
    TIM3_TimeBaseInit(TIM3_PRESCALER_1, 999);    
    
    // Initialise output channel 2 of TIM3.
    TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 0, TIM3_OCPOLARITY_LOW);    
    
    // Enable TIM3.
    TIM3_Cmd(ENABLE);

    // Infinite loop.
    for(;;) {
        
        u16 brightness;
        
        // Set the brightness from 0 to 999 in a loop (fade up).
        for (brightness = 0; brightness < 1000; ++brightness) {

            // Set the brightness of the LED by modifying the capture compare register
            // for TIM3's channel 2.
            TIM3_SetCompare2(brightness);
            
            // Delay a short while.
            delay();
        }
        
        // Set the brightness from 1000 to 1 in a loop (fade down).
        for (brightness = 1000; brightness > 0; --brightness) {

            // Set the brightness of the LED.
            TIM3_SetCompare2(brightness);
            
            // Delay a short while.
            delay();
        }
    }
}

The above program fades the LED up from the minimum brightness to the maximum brightness then back down again in an infinite loop. It also reintroduces our old enemy, the delay loop, which leads us neatly on to the next subject – interrupts.

Interrupts

Interrupts provide a way to respond to events in a way that doesn't require that we constantly check (poll) the event source. One such event is a timer overflowing – we can use this event to update the brightness of the LED every millisecond without having to poll a timer's counter manually or use delay loops.

We'll use TIM1 to generate the interrupt; it provides a few additional features that are not present on the other timers, but we'll need to keep TIM2 and TIM4 free for later. As before, you'll need to add stm8s_tim1.c to your project and uncomment #define _TIM1 (1) in stm8s_conf.h.

We'll start by adding some skeleton interrupt handler code and reference it in the interrupt vector table to ensure that it is called when the timer updates itself. Firstly, add the two following files to your project:

stm8s_it.c

#include "stm8s.h"
#include "stm8s_it.h"

void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void) {
    // TODO: Implement TIM1 update interrupt handler.
}

stm8s_it.h

#ifndef __STM8S_IT_H
#define __STM8S_IT_H

@far @interrupt void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void);

#endif

These stm8s_it files contain the interrupt request handlers. Interrupt handler functions are called via the interrupt vector table, which is defined in the stm8_interrupt_vector.c file that was automatically generated when you created the project. Open this file and add #include "stm8s_it.h" to the top of it so that it can see your interrupt handler functions. According to the STM8S105xx datasheet the TIM1 update/overflow interrupt is mapped to IRQ 11, so scroll down the table of interrupt vectors and change NonHandledInterrupt on the line marked irq11 (some lines omitted for clarity):

struct interrupt_vector const _vectab[] = {
    {0x82, (interrupt_handler_t)_stext}, /* reset */
    {0x82, NonHandledInterrupt}, /* trap  */
    {0x82, NonHandledInterrupt}, /* irq0  */
    /* [...] */
    {0x82, NonHandledInterrupt}, /* irq10 */
    {0x82, (interrupt_handler_t)TIM1_UPD_OVF_TRG_BRK_IRQHandler}, /* irq11 */
    {0x82, NonHandledInterrupt}, /* irq12 */
    /* [...] */
    {0x82, NonHandledInterrupt}, /* irq29 */
};

Now we have that in place we can start writing the interrupt handler code. Internally, interrupts are signalled by setting a flag in a control register, which the microcontroller periodically checks. If you do not clear this flag the microcontroller will call your interrupt handler again as soon as you return from the function, so you must remember to do so – this is done with the TIM1_ClearITPendingBit(TIM1_IT_UPDATE) function. Using a variable to store the current brightness "direction" (positive to get brighter; negative to get dimmer) the LED brightness could be adjusted every time the timer overflowed using the following code:

stm8s_it.c

#include "stm8s.h"
#include "stm8s_it.h"

s16 brightness_direction = +1; // Start by getting brighter.

void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void) {
    
    // Get the current brightness.
    u16 current_brightness = TIM3_GetCapture2();
    
    // Check whether we've hit the maximum/minimum brightness yet.
    if (brightness_direction > 0) {
        // We're currently getting brighter.
        if (current_brightness == 1000) {
            // We're already at the maximum brightness; start getting darker.
            brightness_direction = -1;
        }
    } else {
        // We're currently getting dimmer.
        if (current_brightness == 0) {
            // We're already at the minimum brightness; start getting brighter.
            brightness_direction = +1;
        }
    }
    
    // Update the brightness of the LED according to the brightness "direction".
    TIM3_SetCompare2(current_brightness + brightness_direction);
    
    // Clear the interrupt pending bit for TIM1.
    TIM1_ClearITPendingBit(TIM1_IT_UPDATE);
}

We also need to configure TIM1 to generate interrupts. This can be done with the TIM1_ITConfig function, in addition to the existing code used to configure TIM3:

main.c

#include "stm8s.h"

int main(void) {
    
    // Reset ("de-initialise") TIM3.
    TIM3_DeInit();
    // Set TIM3 to use a prescaler of 1 and have a period of 999.
    TIM3_TimeBaseInit(TIM3_PRESCALER_1, 999);    
    // Initialise output channel 2 of TIM3.
    TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 0, TIM3_OCPOLARITY_LOW);    
    // Enable TIM3.
    TIM3_Cmd(ENABLE);
    
    // Reset ("de-initialise") TIM1.
    TIM1_DeInit();
    // Set TIM1 to:
    // - use an exact prescaler of 1000,
    // - to count up,
    // - to have a period of 1, and
    // - to have a repetition counter of 0.
    TIM1_TimeBaseInit(1000, TIM1_COUNTERMODE_UP, 1, 0);
    // Set TIM1 to generate interrupts every time the counter overflows (every ms).
    TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE);
    // Enable TIM1.
    TIM1_Cmd(ENABLE);
    
    // Enable interrupts (no, really).
    enableInterrupts();

    // Infinite loop.
    for(;;);
}

Interrupts are globally disabled by default, hence the need to call enableInterrupts(). If you run this program, you should find that the LED fades in and out as before, but without the need for hacky delay loops. As you can see, TIM1 takes a few additional parameters to its TIM1_TimeBaseInit function; you aren't limited to powers of two for its prescaler, it can count in a number of different ways and you can specify a "repetition count" that will only update the timer registers after a given number of cycles of the counter (in this case, we've disabled that feature).

Touch key input

As well as an LED for output, the evaluation board provides a touch key for input. This requires considerably more computing power to handle than a conventional push switch, but is considerably more interesting! Thankfully, ST have provided a royalty-free library to handle touch sensing keys, sliders and wheels with their microcontrollers which we can use:

  1. Visit the documents and files page again to download the STM8S Touch Sensing Library; it is packaged as an installer, which should be run.
  2. Go to the installation directory and copy Libraries\STM8_TouchSensing_Driver to your own project folder as you did for FWLib previously.
  3. Move Inc\STM8_TSL_RC_Configuration_TOADAPT.h to the root of your project folder and rename it STM8_TSL_RC_Configuration_TOADAPT.h (remove "_TOADAPT").

When you have copied the files, switch back to your project and follow these steps:

  1. Create a folder Touch Sensing Library under Source Files and add all of the files in STM8_TouchSensing_Driver\Src apart from STM8_TSL_RC_MultiChannelKey.c to it.
  2. Create a folder Touch Sensing Library under Include Files and add all of the files in STM8_TouchSensing_Driver\Inc to it.
  3. Add STM8_TSL_RC_Configuration.h to the root of Include Files.

Due to some functions needing to be aligned to even memory addresses, you will need to modify your linker settings. In the IDE, click Project→Settings and switch to the Linker tab. Set the Category dropdown to Input, expand the Code, Constants section and add a section named .TSL_IO_ALCODE with its options set to -r2. You will need to do this to both Debug and Release configurations.

Adding the .TSL_IO_ALCODE section to the linker.

Now we need to go and configure STM8_TSL_RC_Configuration.h for our particular hardware. Open this file, and make the following amendments:

  1. TIMACQ will need to be changed to TIM2 as we're using TIM3 to drive our LED.
  2. TIMACQ_CNTR_ADD needs to be changed to 0x530A to match the change to TIM2.
  3. The touch key is attached to GPIO port C so LOADREF_PORT_ADDR needs to be changed to GPIOC_BaseAddress.
  4. The load reference is connected to pin PC2, so we need to change LOADREF_BIT to 0x04 (1<<2 = 0x04).
  5. We only have one key, so change SCKEY_P1_KEY_COUNT to 1.
  6. The touch key input is connected to PC1, so leave SCKEY_P1_PORT_ADDR at GPIOC_BaseAddress and SCKEY_P1_A at 0x02 (1<<1 = 0x02). As there are no other keys on that port, set all of the other key masks (SCKEY_P1_B to SCKEY_P1_H) to 0.
  7. As we don't have any keys on a second port, set SCKEY_P2_KEY_COUNT, SCKEY_P2_PORT_ADDR and SCKEY_P2_ASCKEY_P2_H to 0.
  8. As we don't have any multi-channel keys, set NUMBER_OF_MULTI_CHANNEL_KEYS to 0.
  9. The touch key electrodes are connected to PC1 and PC2, so set GPIOC_ELECTRODES_MASK to 0x0A (0b00001010). Set all of the other electrode masks to 0.

Whew, quite a lot of work there! Now we've set that up, we can get programming. Try building your project; it should take a bit longer than before, but not emit any errors if you've set things up correctly!

The first thing we need to change in our program is to switch to running at 16MHz, a requirement of the touch sensing library. To do this, we need to use the CLK peripheral library; add stm8s_clk.c to Source Files\FWLib as before, and uncomment #define _CLK (1) in stm8s_conf.h. Now add CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1) to the start of your main() function to set the internal high-speed oscillator prescaler to 1 (it defaults to eight). If you now build and run your project you will notice that the LED fades up and down much faster – eight times faster, in fact. Change the TIM1 prescaler to 8000 to revert to the old speed:

#include "stm8s.h"

int main(void) {
    
    // Set the internal high-speed oscillator to 1 to run at 16/1=16MHz.
    CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);
    
    // Reset ("de-initialise") TIM3.
    TIM3_DeInit();
    // Set TIM3 to use a prescaler of 1 and have a period of 999.
    TIM3_TimeBaseInit(TIM3_PRESCALER_1, 999);    
    // Initialise output channel 2 of TIM3.
    TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 0, TIM3_OCPOLARITY_LOW);    
    // Enable TIM3.
    TIM3_Cmd(ENABLE);
    
    // Reset ("de-initialise") TIM1.
    TIM1_DeInit();
    // Set TIM1 to use a prescaler of 8000 and to have a period of 1.
    TIM1_TimeBaseInit(8000, TIM1_COUNTERMODE_UP, 1, 0);
    // Set TIM1 to generate interrupts every time the counter overflows (every ms).
    TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE);
    // Enable TIM1.
    TIM1_Cmd(ENABLE);
    
    // Enable interrupts.
    enableInterrupts();

    // Infinite loop.
    for(;;);
}

Now we've got that organised, we can go ahead with using the touch sensing library. Start by adding #include "STM8_TSL_RC_API.h" to the top of main.c. We need to initialise the library and the touch key in our main function; add the following lines after the CLK_HSIPrescalerConfig call:

// Initialise the touch sensing library.
TSL_Init();
// Initialise the key (we only have one key).
sSCKeyInfo[0].Setting.b.IMPLEMENTED = 1; // It's implemented...
sSCKeyInfo[0].Setting.b.ENABLED = 1;     // ...and enabled.

The touch sensing library makes use of a timer interrupt. We've set TIMTICK to TIM4, so need to attach the TIM4 update/overflow interrupt (IRQ 23) to TSL_Timer_ISR. Open stm8_interrupt_vector.c, add #include "STM8_TSL_RC_API.h" to the top of it, then modify the vector marked irq23:

#include "stm8s_it.h"
#include "STM8_TSL_RC_API.h"

/* [...] */

struct interrupt_vector const _vectab[] = {
    {0x82, (interrupt_handler_t)_stext}, /* reset */
    {0x82, NonHandledInterrupt}, /* trap  */
    {0x82, NonHandledInterrupt}, /* irq0  */
    /* [...] */
    {0x82, NonHandledInterrupt}, /* irq22 */
    {0x82, (interrupt_handler_t)TSL_Timer_ISR}, /* irq23 */
    {0x82, NonHandledInterrupt}, /* irq24 */
    /* [...] */
    {0x82, NonHandledInterrupt}, /* irq29 */
};

Some lines are, as before, omitted for clarity.

The infinite loop at the end of the program will need to be modified to call the TSL_Action function to update the touch sensing library's internal state machine, then check the state of the touch sensing library to see if there's any input to be processed:

// Infinite loop.
for(;;) {
    // Update the touch sensing library's state machine.
    TSL_Action();
    // Check to see if something has happened, and that we're in the idle state before handling it.
    if ((TSL_GlobalSetting.b.CHANGED) && (TSLState == TSL_IDLE_STATE)) {
        // Clear the "something has changed" flag.
        TSL_GlobalSetting.b.CHANGED = 0;
        // Has our key been pressed/detected?
        if (sSCKeyInfo[0].Setting.b.DETECTED) {
            nop(); // <-- Set a breakpoint here.
        }
    }
}

Set a breakpoint on the nop() line, then build and run the program. The LED will fade up and down as before, but if all has gone to plan touching the key should break execution on the nop() line. A slightly more useful program is shown below, modifying the current LED fading code to only fade out and using the touch key to set the LED to its maximum brightness when tapped.

main.c

#include "stm8s.h"
#include "STM8_TSL_RC_API.h"

int main(void) {
    
    // Set the internal high-speed oscillator to 1 to run at 16/1=16MHz.
    CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);
    
    // Initialise the touch sensing library.
    TSL_Init();
    // Initialise the key (we only have one key).
    sSCKeyInfo[0].Setting.b.IMPLEMENTED = 1; // It's implemented...
    sSCKeyInfo[0].Setting.b.ENABLED = 1;     // ...and enabled.
    
    // Reset ("de-initialise") TIM3.
    TIM3_DeInit();
    // Set TIM3 to use a prescaler of 1 and have a period of 999.
    TIM3_TimeBaseInit(TIM3_PRESCALER_1, 999);    
    // Initialise output channel 2 of TIM3.
    TIM3_OC2Init(TIM3_OCMODE_PWM1, TIM3_OUTPUTSTATE_ENABLE, 0, TIM3_OCPOLARITY_LOW);    
    // Enable TIM3.
    TIM3_Cmd(ENABLE);
    
    // Reset ("de-initialise") TIM1.
    TIM1_DeInit();
    // Set TIM1 to use a prescaler of 8000 and to have a period of 1.
    TIM1_TimeBaseInit(8000, TIM1_COUNTERMODE_UP, 1, 0);
    // Set TIM1 to generate interrupts every time the counter overflows (every ms).
    TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE);
    // Enable TIM1.
    TIM1_Cmd(ENABLE);
    
    // Enable interrupts.
    enableInterrupts();

    // Infinite loop.
    for(;;) {
        // Update the touch sensing library's state machine.
        TSL_Action();
        // Check to see if something has happened, and that we're in the idle state before handling it.
        if ((TSL_GlobalSetting.b.CHANGED) && (TSLState == TSL_IDLE_STATE)) {
            // Clear the "something has changed" flag.
            TSL_GlobalSetting.b.CHANGED = 0;
            // Has our key been pressed/detected?
            if (sSCKeyInfo[0].Setting.b.DETECTED) {
                TIM3_SetCompare2(1000);
            }
        }
    }
}

stm8s_it.c

#include "stm8s.h"
#include "stm8s_it.h"

void TIM1_UPD_OVF_TRG_BRK_IRQHandler(void) {
    
    // Get the current brightness.
    u16 current_brightness = TIM3_GetCapture2();
    
    // If it's brighter than zero, dim it by one unit.
    if (current_brightness > 0) {    
        TIM3_SetCompare2(current_brightness - 1);
    }
    
    // Clear the interrupt pending bit for TIM1.
    TIM1_ClearITPendingBit(TIM1_IT_UPDATE);
}

Conclusion

Now that you've got this far, you should be able to delve into the documentation and samples provided by ST to find out more about this platform. If ST can keep the price as low as they currently recommend, then this is an extremely attractive platform for hobbyists, especially beginners as you don't need any additional tools barring a USB A to USB B cable – I have deliberately avoided interfacing with external components, for that reason. The hardware is extremely capable, low price or not, so I'm sure we'll see many interesting projects created with this board as a starting point!

Further Reading

ATmega168 Snake

Tuesday, 24th November 2009

In addition to the Tetris game from the previous post, I've added an implementation of snake to the ATmega168 project.

Either game can be selected from a menu that appears when the circuit is powered on. To exit menus I've added a second fire button; this allows you to step back to the main menu to pick a different game if need be. The source code and binary can be downloaded as before.

I've written a number of different Snake implementations in the past. The early versions used a single array to represent every cell that the snake's body lay in (head as the first element, tail as the last element) that I would manually shift every frame and resize when the snake ate some food. This gets slower and slower as the snake gets longer, which isn't very good. When I wrote a version for the TI-83+ calculator in BBC BASIC, I switched to using a ring buffer with a pointer to the head element and another to the tail element that would be shunted along every frame, unless the snake ate some food in which case the tail pointer would stay where it was.

As I have even less memory on the ATmega168, I went for a different tactic again; by using "pretty" graphics for the various parts of the snake in the tilemap, I didn't need to store the snake's path anywhere other than this tilemap. That is, if I wanted to advance the tail one unit, I merely need to look at the current tile graphic being used to represent the tail (which will be pointing up, down, left or right) and follow it along to the tile in front of it. By inspecting this tile, I can see if the snake turned a corner at that point or went straight ahead and so adjust the tail position and graphic accordingly.

void advance_tail(void) {
    // Find the current snake tail graphic.
    char tail = tvtext_buffer[tail_y * TVTEXT_BUFFER_WIDTH + tail_x];
    // Where is the body in relation to the tail?
    int8_t body_x = tail_x, body_y = tail_y;
    switch (tail) {
        case FONT_SNAKE_TAIL_UP:
            --body_y;
            break;
        case FONT_SNAKE_TAIL_DOWN:
            ++body_y;
            break;
        case FONT_SNAKE_TAIL_LEFT:
            --body_x;
            break;
        case FONT_SNAKE_TAIL_RIGHT:
            ++body_x;
            break;
    }
    // Ensure the body is on the buffer.
    if (body_x < WORLD_LEFT) body_x = WORLD_RIGHT;
    if (body_x > WORLD_RIGHT) body_x = WORLD_LEFT;
    if (body_y < WORLD_TOP) body_y = WORLD_BOTTOM;
    if (body_y > WORLD_BOTTOM) body_y = WORLD_TOP;
    // Find the current body graphic.
    char body = tvtext_buffer[body_y * TVTEXT_BUFFER_WIDTH + body_x];
    // Is it a bend? If so, we'll need to rotate the tail graphic.
    switch (body) {
        case FONT_SNAKE_BODY_DOWN_RIGHT:
            tail = (tail == FONT_SNAKE_TAIL_UP) ? FONT_SNAKE_TAIL_RIGHT : FONT_SNAKE_TAIL_DOWN;
            break;
        case FONT_SNAKE_BODY_DOWN_LEFT:
            tail = (tail == FONT_SNAKE_TAIL_UP) ? FONT_SNAKE_TAIL_LEFT : FONT_SNAKE_TAIL_DOWN;
            break;
        case FONT_SNAKE_BODY_UP_RIGHT:
            tail = (tail == FONT_SNAKE_TAIL_DOWN) ? FONT_SNAKE_TAIL_RIGHT : FONT_SNAKE_TAIL_UP;
            break;
        case FONT_SNAKE_BODY_UP_LEFT:
            tail = (tail == FONT_SNAKE_TAIL_DOWN) ? FONT_SNAKE_TAIL_LEFT : FONT_SNAKE_TAIL_UP;
            break;
    }
    // Erase the old tail.
    tvtext_buffer[tail_y * TVTEXT_BUFFER_WIDTH + tail_x] = tvtext_cleared;
    // Draw the new tail.
    tail_x = body_x;
    tail_y = body_y;
    tvtext_buffer[tail_y * TVTEXT_BUFFER_WIDTH + tail_x] = tail;
}

Similar code is used to advance the head and draw the correct tile behind it.

On an unrelated note, I've released a version of BBC BASIC that should run on the Nspire. The Nspire has an emulator on it to run applications for other calculators, but this emulator doesn't implement undocumented instructions. The TI-83+/TI-84+ BBC BASIC host interface makes use of the sl1 instruction, which shifts a register left one bit and sets the least significant bit to 1. Unfortunately, when this code is run on an Nspire it triggers a crash. Apparently the quick fix I've implemented seems to have done the trick, so unless I hear any further bug reports I'll release the latest version formally soon!

ATmega168 Tetris

Sunday, 22nd November 2009

The tvText library I discussed last entry allows you to display text on a PAL TV in black and white using a 20MHz ATmega168 and a pair of resistors. If this doesn't sound terribly exciting, it's probably because it isn't. However, if you bear some limitations in mind and change the font, you can use this text output as a more general tile-mapping system and use it for games that employ simple graphics.

The new circuit, featuring five sloppily-wired input buttons.
The new circuit, featuring five sloppily-wired input buttons.

I added five buttons to the test circuit — up, down, left, right and fire — to act as game input. This circuit is shown in the photograph above. I also added support for 8×8 characters alongside the existing 6×8 characters to the library, set as a compile-time option. This drops the number of characters per line from 32 to 24, but having square tiles makes producing graphics much easier. The reduction in size of the text buffer also frees up more of the precious 1KB of SRAM for the game!

Diagram of the game circuit.

Even though it was always recommended as an excellent game for beginners to write, I don't believe I've ever written a Tetris clone before. Its simple block graphics makes it an ideal candidate for this system, and it always helps to work on a game that's fun to play. Armed with a Game Boy and a stopwatch I attempted to recreate a moderately faithful version of what is probably the most popular rendition of the game.

I think the result plays pretty well, but don't take my word for it — if you have an ATmega168 lying around, you can download the source and binaries here.

USB joypads and text on your TV courtesy of an ATmega168

Saturday, 14th November 2009

Nearly a month since my last update - my, how time flies when you're having fun (or a heavy workload).

I ended up building myself a cheap and cheerful SI Prog programmer for AVR development. After installing the development tools, scanning through the documentation and writing the microcontroller equivalent of Hello, World (flashing an LED on and off) I needed to find a suitable project. The first one was getting to grips with V-USB, a software USB implementation for AVRs. All you need for this are a couple of I/O pins, a few configuration file changes to set your USB device's vendor ID, product ID and device class, and a few lines of C code to actually implement your device. I attached six tactile switches to an ATmega168 and made the most uncomfortable USB joypad I've ever used. I managed two levels of Sonic the Hedgehog before my thumbs admitted defeat, but it's nice to know that building USB devices is very easy with an AVR.

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <util/delay.h>
#include <avr/sleep.h>

#include "usbdrv.h"

/* Joystick port bits */
#define JOY_1     (1<<0)
#define JOY_2     (1<<1)
#define JOY_UP    (1<<2)
#define JOY_DOWN  (1<<3)
#define JOY_LEFT  (1<<4)
#define JOY_RIGHT (1<<5)

/* USB HID report descriptor */
PROGMEM char usbHidReportDescriptor[USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH] = {
    0x05, 0x01,        // USAGE_PAGE (Generic Desktop)
    0x09, 0x05,        // USAGE (Game Pad)
    0xa1, 0x01,        // COLLECTION (Application)
    0x09, 0x01,        //   USAGE (Pointer)
    0xa1, 0x00,        //   COLLECTION (Physical)
    0x09, 0x30,        //     USAGE (X)
    0x09, 0x31,        //     USAGE (Y)
    0x15, 0x00,        //   LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,  //     LOGICAL_MAXIMUM (255)
    0x75, 0x08,        //   REPORT_SIZE (8)
    0x95, 0x02,        //   REPORT_COUNT (2)
    0x81, 0x02,        //   INPUT (Data,Var,Abs)
    0xc0,              // END_COLLECTION
    0x05, 0x09,        // USAGE_PAGE (Button)
    0x19, 0x01,        //   USAGE_MINIMUM (Button 1)
    0x29, 0x02,        //   USAGE_MAXIMUM (Button 2)
    0x15, 0x00,        //   LOGICAL_MINIMUM (0)
    0x25, 0x01,        //   LOGICAL_MAXIMUM (1)
    0x75, 0x01,        // REPORT_SIZE (1)
    0x95, 0x08,        // REPORT_COUNT (8)
    0x81, 0x02,        // INPUT (Data,Var,Abs)
    0xc0               // END_COLLECTION
};

static uchar reportBuffer[3];    /* Buffer for HID reports */
static uchar idleRate;           /* 4 ms units */

uchar usbFunctionSetup(uchar data[8]) {
    usbRequest_t  *rq = (void*)data;
    usbMsgPtr = reportBuffer;
    if ((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS) {
        switch (rq->bRequest) {
            case USBRQ_HID_GET_REPORT:
                return sizeof(reportBuffer);
            case USBRQ_HID_GET_IDLE:
                usbMsgPtr = &idleRate;
                return 1;
            case USBRQ_HID_SET_IDLE:
                idleRate = rq->wValue.bytes[1];
                break;
        }
    }
    return 0;
}

ISR(TIMER0_OVF_vect) {

    /* Fetch input */
    uchar input = ~PINC;
    
    /* X-axis */
    switch (input & (JOY_LEFT | JOY_RIGHT)) {
        case JOY_LEFT:
            reportBuffer[0] = 0;
            break;
        case JOY_RIGHT:
            reportBuffer[0] = 255;
            break;
        default:
            reportBuffer[0] = 128;
            break;
    }

    /* Y-axis */
    switch (input & (JOY_UP | JOY_DOWN)) {
        case JOY_UP:
            reportBuffer[1] = 0;
            break;
        case JOY_DOWN:
            reportBuffer[1] = 255;
            break;
        default:
            reportBuffer[1] = 128;
            break;
    }

    /* Buttons */
    reportBuffer[2] = input & (JOY_1 | JOY_2);

    usbPoll();
    usbSetInterrupt(reportBuffer, sizeof(reportBuffer));
};

int main(void) {

    usbInit();              /* Initialise USB. */

    PORTC = 0b00111111;     /* Pull high PORTC0..PORTC5 */
    
    TCCR0B = 0b00000101;    /* CS2..CS0 = 101:  prescaler = /1024 */
    TIMSK0 |= (1 << TOIE0); /* Enable timer 0 overflow interrupt. */
    sei();                  /* Enable global interrupts. */
    
    for (;;) {
        /* Infinite loop */
    }
}

I should only really call usbSetInterrupt when a button or axis has changed, rather than every loop, but the above code works as is.

One thing that always bothers me when it comes to electronic projects is the difficulty of providing text output. LCDs are generally quite expensive and low resolution, and typically require a great many pins to drive them. Video display processor chips are difficult to find, and appear to require quite complex external circuitry (the best thing I've found thus far are some TMS9918 chips being sold as spares for MSX computers). Having briefly experimented with generating PAL video signals in software before, I thought I'd try the two-resistor approach to getting PAL video output on an ATmega168.

I had a hunt around and found AVGA, which is close to what I wanted - video output from an AVR using cheap hardware. However, it outputs RGB directly, and I don't own a TV or RGB converter so couldn't use that - all I have is a VGA box (accepting composite or S-Video input) and a TV capture card (also only accepting composite or S-Video input). AVGA does work with VGA monitors, but I'd like to keep the hardware interface simple - just two resistors, ideally.

tvText demo screen

In the end, I ended up writing my own library. It currently has the following specifications:

  • 32×16 characters: 512 bytes (half of the total SRAM on the ATmega168) are used to store the text buffer.
  • Full 256 characters at a resolution of 6×8 pixels each.
  • Total screen resolution: 192×128.

The library is interrupt-driven, and uses the sixteen-bit TIMER1 to schedule events. This means that the AVR is only busy generating video signals when it absolutely has to, leaving some CPU time to the user program. When outputting at full quality, the AVR appears to be capable of running user code at 3.3 MIPS, but by skipping alternate scanlines (each scanline is scanned twice anyway, so this mainly just makes the display appear darker) the AVR appears to be running user code at 9.9 MIPS. (I say "appears" as my calculation has been to execute a busy loop that would normally take one second on the AVR running at its normal 20 MIPS then seeing how long it takes with the video output driver enabled).

The above video demonstrates some of the currently rather limited features of the library. The text console handles a subset of the BBC Micro VDU commands - I'd like to support as many of its features as possible. The code behind the BASIC-like part of the demo is simply written like this:

#include "tvtext/tvtext.h"

void type_string_P(const char* s) {
    char c;
    while ((c = pgm_read_byte(s++))) {
        tvtext_putc(c);
        delay_ms(100);
    }
}

int main(void) {

    tvtext_init();

    tvtext_clear();
    tvtext_puts_P(PSTR("AVR Computer 1K\r\n\nATmega 168\r\n\nBASIC\r\n\n>"));
    delay_ms(2000);
    type_string_P(PSTR("10 PRINT \"AVR Rules! \";\r\n"));
    tvtext_putc('>');
    delay_ms(500);
    type_string_P(PSTR("20 GOTO 10\r\n"));
    tvtext_putc('>');
    delay_ms(500);
    type_string_P(PSTR("RUN"));
    delay_ms(1000);
    tvtext_puts_P(PSTR("\r\n"));

    for (int i = 0; i <= 200; ++i) {
        tvtext_puts_P(PSTR("AVR Rules! "));
        delay_ms(20);
    }

    tvtext_puts_P(PSTR("\r\nEscape at line 10\r\n>"));
    delay_ms(1000);
    type_string_P(PSTR("CHAIN \"DEMO\""));
    delay_ms(1000);
    
    // ...

}

All of the high-level console code - text output, viewport scrolling, cursor positioning &c - has been written in C, so should be relatively easy to be customised. The output driver itself has been written in assembly as timing is critically important.

With a few more features and a bit of tidying up I hope that people would find this a useful library. I'd certainly like to get a blinking cursor working within the driver, and if I add support for a reduced 128-character version I could save quite a bit of ROM space and add support for "coloured" - inverted, that is - text. NTSC support would also be quite useful.

64-bit IThumbnailProvider, BBC BASIC matrices and clocks

Friday, 16th October 2009

Work commitments have left me with little time to pursue my own projects of late, hence the lack of updates.

A chap named Jeremy contacted me with problems relating to the IThumbnailProvider code I'd posted here before. We narrowed it down to a 64-bit issue, demonstrated by the fact that the thumbnails appeared in the file open dialog of a 32-bit application, but not in Explorer. Not having a 64-bit version of Windows to tinker with, I was unable to help, but he found the solution was to register the assembly using the 64-bit version of regasm. You can read more about his experiences on his blog.

I had made a mistake in the BBC BASIC (Z80) for TI-83+ documentation, describing the old coordinate system in the graphics documentation rather than the current one (which is more closely aligned to other versions of BBC BASIC). I have uploaded a new version of the documentation to ticalc.org. This build also includes some basic matrix operations via the MAT statement. This statement is rather incomplete, but I've run out of ROM space (not to mention time) to implement it fully. Still, the bits that are there are quite useful, and a half-arsed implementation is better than no implementation... right?

HT1632 Clock

On a whim, I purchased a 32×8 LED display on eBay which I've (very) slowly been turning into a remote-controlled clock. A Sony-compatible remote control is used to type in the time, after which you can cycle through different styles with the channel up/down buttons and change the brightness with the volume and mute buttons. I'm using a 4MHz PIC16F84 to drive the display, with a DS1307 responsible for time-keeping and a 32KB 24LC256 to store the font data and text strings.

As well as dates and times, I thought a thermometer might be a useful addition so I put together an order for a DS18B20. It's silly to just order one thing, so I bulked up the order with one of the snazzy new PICAXE-20X2 chips (yes, they run a BASIC interpreter but the new 64MHz clock speed is certainly impressive). I find PICAXE microcontrollers invaluable for prototyping, being so very easy to use! smile.gif

In an attempt to broaden my horizons, I also purchased two AVRs, as I have zero experience with these popular chips. I went for the two ends of the scale as offered by the supplier - an ATmega168 and an ATtiny13. Having lost a battle with PayPal's cart (it kept forgetting old items as I added new ones) I neglected to purchase a D-sub hood so I'll be waiting until I can go and buy one before I start assembling a programmer. I was intending on going for the simple SI Prog, but if anyone has any suggestions for variations I'd be interested in hearing them!

Page 8 of 44 14 5 6 7 8 9 10 11 1244

Older postsNewer postsLatest posts RSSSearchBrowse by dateIndexTags