Saturday, December 5, 2009

Mixing Assembler and C on the STM8S-Discovery board

So, been playing with my STM8S-discovery boards, here's some stuff you might like. This builds on Ben's stuff here - http://www.benryves.com/journal/3567231

Now, one of my projects is to do with making a multi-channel drone synthesizer. I should really do it with discrete logic and amps, but hey, it's a quickie. What I want to do is 8 channels of audio-range square waves, at differing and varying frequencies. How we go about varying the frequencies is another matter, but the main problem is doing 8 channels - I could do 4 with just the timers, but 8 is pretty tricksy.

Basically, the approach I'm taking is to use a timer firing at a regular frequency, and do additive synthesis to make the final output volume (I'll initially be using a simple approach with no fading of the 8 pseudo-oscillators, so the maximum output volume is 8; that lets us get away with a 4 pin R-2R DAC (but more on that later).

Now, we need to work out how much we can do, and how long we have to do it. The clock of the disco board runs at up to 16 MHz, so we can work out easily enough what frequencies we can call our hypothtical interrupt at. The prescaler value gives us, direcly, the number of clocks we have to play with at that frequency

  1. 1024 = 16kHz
  2. 512 = 32kHz
  3. 256 = 64kHz
  4. 128 = 128kHz
and so on. Bearing in mind that human audio range peaks at ~20kHz, we'd probably like to be using a 32kHz or better clock. Indeed, due to the way my software works, I can only generate up to 16kHz with a 32kHz clock, and there's a really coarse granularity on the high end - it's basically not much cop above 1kHz or so, around 2 octaves up from middle C.

Anyway, 512 cycles is not much to play with. We're gonna need some assembler. C code is waaaay too fat. My first cut interrupt routine has a "worst case" of just over 200 cycles, so that cuts out 64kHz if we want to do anything clever elsewhere. And we do. So, 32kHz it is, and my interrupt is gonna use around 1/3 of total CPU all by itself. I should be able to streamline it a bit (or even a lot), but I doubt it's ever going to be able to run at 64kHz. As an idea of how fat C code is, the equivalent C routine had a worst case of 500 cycles. Yowza.

So, how do we make our assembler code work with existing "C"? Well, first thing to do is take a trivial program and compile it; in the "Debug" folder of the project you'll find the generated assembler code (for example, main.ls) - look at this and you can find out how the name mangling works.

In my case, I was interested in the names of functions and global variables, so I compiled up a quick program with one global variable and one function.

For a function name of "MyFunction", the actual label generated in the assembler (and thus the format of the name we need to use) is "f_MyFunction". Groovy. Variables have an underscore, so "my_var" becomes "_my_var". Now we know that, we can do some work.

First, we edit the interrupt table, adding an "extern" reference to our assembler routine. This stops the C compiler kicking up a fuss.
extern @far @interrupt void timer_interrupt(void);
Now, we add a reference to that routine to the table itself.
...
{0x82, timer_interrupt}, /* irq11 */
...
I'm using irq11 because that's the timer 1 overflow interrupt.

Now. I'm going to need access to the interrupt's data from elsewhere, so we need to add some references to them, too. I've shoved them in main.c.
// For our purposes
typedef struct {
u16 ticks_left;
u16 ticks;
} channel;

// Extern, these are defined in synth.asm
extern channel channels[8];
extern u8 volume;
So, we have an array of 8 "channel data" structures, with "ticks" defining the number of interrupt "ticks" before we roll over, and "ticks_left" being the number of interrupt ticks this channel has had since last rollover. Pretty simple, really. There's also a single 8-bit "volume" field. Note the "extern" on both of these - this tells the compiler they are actually defined elsewhere.

As I haven't bothered to wire up an R2R ladder on my board yet, I decided to test with a bit of PWM on the LED. this follows on directly from Ben's stuff.

main()
{
// Set the internal high-speed oscillator to 1 to run at 16/1=16MHz.
CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1);

// Reset ("de-initialise") TIM1.
TIM1_DeInit();
// Set TIM1 to use a prescaler of 512 and to have a period of 1.
TIM1_TimeBaseInit(512, TIM1_COUNTERMODE_UP, 1, 0);
// Set TIM1 to generate interrupts every time the counter overflows. With
// prescaling of 512, this is a frequency of 32kHz
TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE);
// Enable TIM1.
TIM1_Cmd(ENABLE);

// For visualisation only
// 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);

// Set up our channels
channels[0].ticks_left = 0;
channels[1].ticks_left = 0;
channels[2].ticks_left = 0;
channels[3].ticks_left = 0;
channels[4].ticks_left = 0;
channels[5].ticks_left = 0;
channels[6].ticks_left = 0;
channels[7].ticks_left = 0;
channels[0].ticks = 0x4000;
channels[1].ticks = 0x4010;
channels[2].ticks = 0x4020;
channels[3].ticks = 0x4040;
channels[4].ticks = 0x4080;
channels[5].ticks = 0x4100;
channels[6].ticks = 0x4200;
channels[7].ticks = 0x4400;

enableInterrupts();

while (1) {
TIM3_SetCompare2(volume * (1000 / 8));
}
}

What's interesting is the setting of the various channels to different (very low frequency) rollover periods - it starts off with everything together, and the channels slowly end up "beating" against one another, which does "interesting" stuff to the LED. The LED itself is PWM controlled to set its brightness to one of 8 levels.

Of course, none of this is any good without the interrupt routine itself. This goes in a totally separate file with a ".asm" extension. The toolkit knows how to deal with it.

; names mangled to C code specs
; uninitialised data in page zero
switch .ubsct
_channels:
ds.w 16
_volume:
ds.b 1

; make them visible to C code
xdef _channels
xdef _volume

; code
switch .text
f_timer_interrupt:
clr _volume ; 1 ; volume to zero
ld a, #0x07 ; 1 ; set initial channel flag
ldw x, #_channels ; 2 ; load y register with address of channel data
dochannel:
ldw y, x ; 1 ; Load y register with ticks_left
ldw y, (y) ; 2
jrmi br3 ; 1 / 2 ; skip if channel is off (bit 15 of ticks_left set)
decw y ; 2 ; decrement
jrpl br1 ; 1 / 2 ; if not negative, skip
ldw y, x ; 1 ; reset ticks_left
ldw y, (0x02, y) ; 2
ldw (x), y ; 2 ; store ticks left
jra br2 ; 2 ; and skip unnecessary work
br1: ldw (x), y ; 2 ; store ticks_left
ldw y, x ; 1 ; get number of ticks per cycle
ldw y, (0x02, y) ; 2
br2: srlw y ; 2 ; divide by 2
cpw y, (x) ; 2 ; compare with ticks_left
jrmi br3 ; 1 / 2
inc _volume ; 1 ; increment volume
br3: addw x, #0x0004 ; 2 ; go 4 bytes up the channel data list
dec a ; 1
jrpl dochannel ; 1 / 2 ; go around if we have more channels to do

bres 0x5255, #0x00 ; 1 ; Clear TIM1 Interrupt pending bit
iret ; 11 ; and return

; make function visible to C code
xdef f_timer_interrupt
You'll note the name mangling, and the "xdef" lines to make the labels externally visible. Also, that I've somewhat anally added the number of cycles per operation to the source.

This routine allows me to kill any one channel at the next call to the interrupt by setting its "ticks" value to 0x8000, and "ticks_left" to 0x0000.

Hope this is of some use to people.

2 comments:

  1. Hi, could you tell me were I can find a list of assembler instructions for STM8?

    ReplyDelete
  2. MichaƂ, read the manual:
    http://www.st.com/stonline/products/literature/pm/13590.pdf

    ReplyDelete

Followers