Revenge of the blinking LED

The previous program was created to be the simplest yet easy to read program (it could be made smaller) to blink a LED from the st8sdiscovery board but it made some assumptions that would not exist in a more extensive program. You may have wondered why we ditched all the generated code the IDE created. It was in order to make things as simple as possible. Now we will look at what should be done in initialising a program and the stm8 MCU so it does not take its revenge on us later.

Steps required to initialise the stm8 CPU

  1. Set up the stack pointer to we can call subroutines , use interrupts and perform stack operations.
  2. Clear the ram memory so it is in a defined state.
  3. Set up an interrupt table so interrupts are handled if they should be enabled.
The code has become to big for screenshots so now we will display it in-line.

stm8/

    #include "mapping.inc"

    segment 'rom'

start
    ldw X,#$stack_segment_end ; initialize SP
    ldw SP,X

    
ldw X,#$ram0_segment_start ; clear all the ram
clear_ram

    clr (X)
    incw X
    cpw X,#$stack_segment_end
    jrule clear_ram
    
    bset $5011,#0 ; init our program
loop_forever
    ldw X,#$FFFF
loop_delay
    nop
    decw X
    jrne loop_delay
    bcpl $500F,#0
    jra loop_forever


; the default interupt handler
    interrupt NonHandledInterrupt
NonHandledInterrupt
    iret

; the interupt table
     segment 'vectit'
     dc.l {$82000000+start} ; reset
     dc.l {$82000000+NonHandledInterrupt} ; trap
     dc.l {$82000000+NonHandledInterrupt} ; irq0
     dc.l {$82000000+NonHandledInterrupt} ; irq1
     dc.l {$82000000+NonHandledInterrupt} ; irq2
     dc.l {$82000000+NonHandledInterrupt} ; irq3
     dc.l {$82000000+NonHandledInterrupt} ; irq4
     dc.l {$82000000+NonHandledInterrupt} ; irq5
     dc.l {$82000000+NonHandledInterrupt} ; irq6
     dc.l {$82000000+NonHandledInterrupt} ; irq7
     dc.l {$82000000+NonHandledInterrupt} ; irq8
     dc.l {$82000000+NonHandledInterrupt} ; irq9
     dc.l {$82000000+NonHandledInterrupt} ; irq10
     dc.l {$82000000+NonHandledInterrupt} ; irq11
     dc.l {$82000000+NonHandledInterrupt} ; irq12
     dc.l {$82000000+NonHandledInterrupt} ; irq13
     dc.l {$82000000+NonHandledInterrupt} ; irq14
     dc.l {$82000000+NonHandledInterrupt} ; irq15
     dc.l {$82000000+NonHandledInterrupt} ; irq16
     dc.l {$82000000+NonHandledInterrupt} ; irq17
     dc.l {$82000000+NonHandledInterrupt} ; irq18
     dc.l {$82000000+NonHandledInterrupt} ; irq19
     dc.l {$82000000+NonHandledInterrupt} ; irq20
     dc.l {$82000000+NonHandledInterrupt} ; irq21
     dc.l {$82000000+NonHandledInterrupt} ; irq22
     dc.l {$82000000+NonHandledInterrupt} ; irq23
     dc.l {$82000000+NonHandledInterrupt} ; irq24
     dc.l {$82000000+NonHandledInterrupt} ; irq25
     dc.l {$82000000+NonHandledInterrupt} ; irq26
     dc.l {$82000000+NonHandledInterrupt} ; irq27
     dc.l {$82000000+NonHandledInterrupt} ; irq28
     dc.l {$82000000+NonHandledInterrupt} ; irq29

    end


We have added the include file mapping.inc which was automatically generated for us, It defines stack_segment_end , stack_segment_start and ram_segment_start. We use these to initialise the stack pointer and clear all of the 2k of ram. We have also added a default interrupt handler NonHandledInterupt and set up the interrupt vector table.

Some points to note.

We can only load the Stack Pointer SP from another register not an immediate value , so we load it from the X register which has been set to that value.

The clr(X) (clear) is an example of direct indexed addressing , the X register has the value of the memory location which is cleared to zero then the X Register is incremented to the value of the next memory location and clears that within the loop, This continues until the cpw (compare word) instruction finds that X is greater than the value of stack_segment_end.

The interrupt table is started at the new segment 'vectit' which lies within the flash rom at address 0x8000 the beginning of the flash rom.

The dc.l  directive forces the long word given as its argument into the object code at the current address so the interrupt table is set up from 0x8000 to 0x8080.

The iret instruction is used to return from an interrupt routine back to the code before the interrupt occurred.

Wait a minute shouldn't the LED blink faster ?

Yes it should when the st3s105 boots up it runs on the internal RC oscillator which is set to 1/8 of its frequency @ about 2mhz so in real world application we would probably want this to run as fast a possible as soon as possible.

First lets slow down the LED blinking so if stays on and off for about 8 seconds. Change the code to have another inner loop using the other 16 bit register Y. We will then know if the clock has indeed been set to maximum by the fact the LED will light for about 1 second.




    bset $5011,#0 ; init our program
loop_forever
    ldw X,#$FFFF
loop_delay
    ldw Y,#$0050
loop_inner_delay
    decw Y
    jrne loop_inner_delay
    decw X   
    jrne loop_delay




The STM8S105C6 Clock

"The clock controller is designed to be powerful, very robust, and at the same time easy to use. Its purpose is to allow you to obtain the best performance in your application while at the same time get the full benefit of all the microcontroller’s power saving capabilities. You can manage all the different clock sources independently and distribute them to the CPU and to the various peripherals. Prescalers are available for the master and CPU clocks."

4 different clock sources can be used to drive the master clock:
● 1-24 MHz high speed external crystal oscillator (HSE) ( the discovery board is 16mhz)
● Up to 24 MHz high speed user-external clock (HSE user-ext)
● 16 MHz high speed internal RC oscillator (HSI)
● 128 kHz low speed internal RC (LSI)


In order to speed up the internal clock we need to look at the Clock divider register (CLK_CKDIVR).In order to product the final CPU Clock it is divided twice once by HSIDIV and then by CPUDIV.

CLK_CKDIVR  0x50C6

 Bit Pos
 7 6 5 4 3 2 1 0
  reserved reserved reserved HSIDIV1 HSIDIV0 CPUDIV2 CPUDIV1 CPUDIV0
R/W
 rw rw rw rw rw rw rw rw
Reset Value
 0 0 0 1 1 0 0 0

For HSIDIV1: HSIDIV0

00: fMaster = 16mhz
01: fMaster = 16mhz/2 = 8mhz
10: fMaster = 16mhz/4 = 4mhz
11: fMaster = 16mhz/8 = 2mhz

For CPUDIV2: CPUDIV1: CPUDIV0

000: fCpu = fMaster 
001: fCpu = fMaster / 2 
010: fCpu = fMaster / 4 
011: fCpu = fMaster / 8
100: fCpu = fMaster / 16
101: fCpu = fMaster / 32
110: fCpu = fMaster / 64
111: fCpu = fMaster /128

So to get the maximum speed we simply set this register to zero. By setting the maximum divide (0x1F) we can set the CPU clock as low as 15.625khz if we ever wanted to.


    segment 'rom'

start
        mov $50C6,#$0        ; set the internal rc clock divide to 1
    ldw X,#$stack_segment_end ; initialize SP
    ldw SP,X

So adjust the code to the above and the LED should now blink for 1 second approximately.

Hang on the Discovery has a 16mhz external crystal oscillator wouldn't that be better ?

Yes it should be more stable and accurate than the internal rc clock, here is some code without any explanation ....



    segment 'rom'

start

; switch to hardware clock
   
bset $50C5,#$1
    mov  $50C4,#$B4
wait_hw_clock
    btjf $50C5,#$3,wait_hw_clock
    bres $50C5,#$3
; hardware clock now active

   
ldw X,#$stack_segment_end
; initialize SP
    ldw SP,X


Tidying up the code

To make the code more readable we can use symbolic names for the hardware registers rather than refering to the absolute address in memory, to do this we use the EQU directive of the assembler, for instance

    segment 'rom'

       
CLK_SWR         EQU $50C4        ; Clock master switch register
   
CLK_SWCR        EQU $50C5        ; Clock switch control register
   
CLK_CKDIVR      EQU $50C6        ; Clock divider register
   
PD_ODR          EQU $500F        ; Port D data output latch register

PD_DDR          EQU $5011        ; Port D data direction register



We can also make blink a subroutine , we call it blink_one

blink_once
    ldw X,#$FFFF    
loop_delay
    ldw Y,#$0050    
loop_inner_delay
    decw Y
    jrne loop_inner_delay
    decw X
    jrne loop_delay
    bcpl  PD_ODR,#0    
    ret

The subroutine is called from the main loop, loop_forever

; main program loop
loop_forever                                    
    call blink_once
    jra loop_forever

What happens here is that the call instruction jumps to blink_once and the delay loops are executed, once it has blinked the LED it will find and execute the ret instruction, this tells the CPU to return to whence it came just after the original call instruction and carry on executing the instructions that follow. In this case it will execute the jra loop_forever instruction. Note that the memory address that the CPU will return to is held on the the stack which is pointed to by the SP register , this is why it is important to set-up the stack and SP at the beginning of the program in order to be able to call subroutines and use the push and pop instructions which we have not covered yet but also use the stack.


The complete code of our tidy version

stm8/
   
    #include "mapping.inc"    
   
    segment 'rom'
   
CLK_SWR           EQU $50C4        ; Clock master switch register
CLK_SWCR          EQU $50C5        ; Clock switch control register
CLK_CKDIVR        EQU $50C6        ; Clock divider register
PD_ODR            EQU $500F        ; Port D data output latch register
PD_DDR            EQU $5011        ; Port D data direction register

start

    mov CLK_CKDIVR,#$0                    ; set max internal clock
    bset CLK_SWCR,#$1                     ; enable external clcok (clock is 16mhz)
    mov CLK_SWR,#$B4                      ; switch in external cyrstal clock
wait_hw_clock
    btjf CLK_SWCR,#$3,wait_hw_clock     ; wait swif
    bres CLK_SWCR,#$3                   ; clear swif
   
    ldw X,#$stack_segment_end     ; initialize SP
    ldw SP,X
   
    ldw X,#$ram0_segment_start  ; clear all the ram
clear_ram
    clr (X)
    incw X
    cpw X,#$stack_segment_end
    jrule clear_ram
; it is now safe to use the stack

; init our program
    bset  PD_DDR,#0        ; PD0 is set to output mode                    
   
; main program loop
loop_forever                                    
    call blink_once
    jra loop_forever

; blink is now a subroutine
; this delays for about a second then toggles the
; LED On or Off
blink_once
    ldw X,#$FFFF    
loop_delay
    ldw Y,#$0050    
loop_inner_delay
    decw Y
    jrne loop_inner_delay
    decw X
    jrne loop_delay
    bcpl  PD_ODR,#0    
    ret


; the default interupt handler
    interrupt NonHandledInterrupt
NonHandledInterrupt
    iret

; the interupt table
    segment 'vectit'
    dc.l {$82000000+start}                                ; reset
    dc.l {$82000000+NonHandledInterrupt}    ; trap
    dc.l {$82000000+NonHandledInterrupt}    ; irq0 TLI External top level interupt
    dc.l {$82000000+NonHandledInterrupt}    ; irq1 AWU Auto wake up from halt
    dc.l {$82000000+NonHandledInterrupt}    ; irq2 CLK Clock Controller
    dc.l {$82000000+NonHandledInterrupt}    ; irq3 EXTIO Port A external
    dc.l {$82000000+NonHandledInterrupt}    ; irq4 EXTI1 Port B
    dc.l {$82000000+NonHandledInterrupt}    ; irq5 EXTI2 Port C
    dc.l {$82000000+NonHandledInterrupt}    ; irq6 EXTI3 Port D
    dc.l {$82000000+NonHandledInterrupt}    ; irq7 EXTI4 Port E external
    dc.l {$82000000+NonHandledInterrupt}    ; irq8  reserved
    dc.l {$82000000+NonHandledInterrupt}    ; irq9  reserved
    dc.l {$82000000+NonHandledInterrupt}    ; irq10 SPI end of transfer
    dc.l {$82000000+NonHandledInterrupt}    ; irq11 TIM1 Update/overflow/underflow/trigger/break
    dc.l {$82000000+NonHandledInterrupt}    ; irq12 TIM1 Capture/Compare
    dc.l {$82000000+NonHandledInterrupt}    ; irq13 TIM2 update/overflow
    dc.l {$82000000+NonHandledInterrupt}    ; irq14 TIM2 capture / compare
    dc.l {$82000000+NonHandledInterrupt}    ; irq15 TIM3 Update/ overflow
    dc.l {$82000000+NonHandledInterrupt}    ; irq16 TIM3 Capture / Compare
    dc.l {$82000000+NonHandledInterrupt}    ; irq17 reserved
    dc.l {$82000000+NonHandledInterrupt}    ; irq18 reserved
    dc.l {$82000000+NonHandledInterrupt}    ; irq19 I2C
    dc.l {$82000000+NonHandledInterrupt}    ; irq20 Uart2 Tx Complete
    dc.l {$82000000+NonHandledInterrupt}    ; irq21 Uart2 Recieve Register Data Full
    dc.l {$82000000+NonHandledInterrupt}    ; irq22 ADC1 end of conversion
    dc.l {$82000000+NonHandledInterrupt}    ; irq23 TIM4 Update/Overflow
    dc.l {$82000000+NonHandledInterrupt}    ; irq24 Flash EOP/WR_PG_DIS
    dc.l {$82000000+NonHandledInterrupt}    ; irq25 reserved
    dc.l {$82000000+NonHandledInterrupt}    ; irq26 reserved
    dc.l {$82000000+NonHandledInterrupt}    ; irq27 reserved
    dc.l {$82000000+NonHandledInterrupt}    ; irq28 reserved
    dc.l {$82000000+NonHandledInterrupt}    ; irq29 reserved

    end
    



This code is not very efficient and wastes time and CPU cycels in the delay loop , next we will show how we can have all the time in the world


ċ
blink_tidy.zip
(13k)
Atom Age,
5 Sep 2010, 15:02
Comments