Aus RN-Wissen.de
Wechseln zu: Navigation, Suche


Die Hardware für die Code-Beispiele: Bascom ISR in Assembler programmieren

Da immer wieder Fragen zur Assembler-ISR-Programmierung in Bascom auftreten, will ich hier an einem einfachen Beispiel einer 3 Tasten Uhr mit LCD Display den Weg aufzeigen.

Die Hardware

Clock Debounce.jpg

Die Schaltung ist auf einem Steckbrett schnell gesteckt. Der zusätzliche Max232 dient nur der einfachen Bootloader-Programmierung.

zur Hilfestellung für Einsteiger noch der Schaltplan:

Clock Debounce Board.png

Der u.g. Beispiel-Code lässt sich auf alle AVR-Starterplatinen mit Tasten+LCD schnell anpassen.

Beispiel 1: Entprellung einer 3 Tasten Uhr in Bascom

Der nachfolgende Code verwendet noch kein Assembler und dient als Vorlage für Beispiel 2.

Es wurde zur Entprellung der Tasten der Bascom Befehl DEBONCE verwendet. Der Code ist bewusst simpel gestrickt. Trotzdem wurden bewusst mächtige Bascom Befehle wie "Lcd Date$ ; " " ; Time$" verwendet, um die Stärke vom Bascom gegenüber ASM herauszustellen.

Die Uhr wird über 3 Tasten (Plus, Minus, Enter) gestellt. Jeder kennt das... Beim Stellen laufen die Werte in beide Richtungen über (Beispiel Stunde : ...23,0,1....23,0,1..)

$regfile = "m8def.dat"
$framesize = 32
$swstack = 32
$hwstack = 32
$crystal = 8000000

'********* LCD ***************************************************
 'Pins des LCD-Modules setzen ggf. an eigene Anschlüsse anpassen
 Config Lcdpin = Pin , Db4 = Portc.2 , Db5 = Portc.3 , Db6 = Portc.4 , Db7 = Portc.5 , E = Portc.1 , Rs = Portc.0
 Config Lcdmode = Port
 Config Lcdbus = 4                                          '4 bit mode
 Config Lcd = 20 * 4
 Initlcd
 Cursor Off

'********* ISR Timer 2 ***************************************************
Config Clock = Soft
Enable Interrupts

Config Date = Dmy , Separator = /
 _sec = 00 : _min = 15 : _hour = 14 : _day = 30 : _month = 05 : _year = 08

'********* input key and debounce ***************************************************
'Zuordnung der Tasten zu den Pins
Const Key_plus = &B0000_1000                                '1=BUTTON_INPUT
Const Key_minus = &B0001_0000
Const Key_enter = &B0010_0000

Const Pinb_mask = Key_plus + Key_minus + Key_enter          'Maske zum Filtern der Eingänge
Const Pinb_mask_compl = &HFF - Pinb_mask                    'Pinb_mask compliment

'hier sind die Tasten am PortB
Ddrb = Ddrb And Pinb_mask_compl                             'DDRB: 0=INPUT 1=OUTPUT
Portb = Portb Or Pinb_mask                                  '1=activate internal pull-up resistors for inputs

'********* allgemeines  ***************************************************
Dim I As Byte , J As Byte , K As Byte , L As Byte

Dim Setdatetime_index As Byte
Setdatetime_index = 1                                       'first index of date_array is 1

Dim Date_array(6) As Byte At _sec Overlay                   'array for _sec/_min/_hour/_day/_month/_year
'********* main loop  ***************************************************
Do

  Home                                                      'cursor home
  Lcd Date$ ; "  " ; Time$                                  'show the date and time
  Locate 2 , 1
  Lcd "Set: " ; Lookupstr(setdatetime_index , Data_dateset)

  Debounce Pinb.3 , 0 , Key_plus_pressed , Sub
  Debounce Pinb.4 , 0 , Key_minus_pressed , Sub
  Debounce Pinb.5 , 0 , Key_enter_pressed , Sub

Loop

'********* Gusub  ***************************************************
Key_plus_pressed:
   Gosub Date_values
   If J < L Then Incr J Else J = K
   Date_array(setdatetime_index) = J
Return

Key_minus_pressed:
   Gosub Date_values
   If J > K Then Decr J Else J = L
   Date_array(setdatetime_index) = J
Return

Key_enter_pressed:
   If Setdatetime_index < 6 Then Incr Setdatetime_index Else Setdatetime_index = 1
Return

'---------------------------------------------------------------
'Subroutine: Date_values
'Call from:  main
'Purpose:    Adjusts the Date and Clock
'Parameters: key input
'---------------------------------------------------------------
Date_values:
    L = Lookup(setdatetime_index , Date_max)
    If Setdatetime_index = 4 Then
       L = Lookup(_month , Date_month)
       K = _year And 3
       If _month = 2 Then If K = 0 Then Incr L
    End If
    K = Lookup(Setdatetime_index , Date_min)

    J = Date_array(setdatetime_index)
Return

'******** Select Date and Time ************************************
Date_min:
Data 0 , 0 , 0 , 0 , 1 , 1 , 0
Date_max:
Data 0 , 59 , 59 , 23 , 31 , 12 , 99
Date_month:
Data 0 , 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31
Data_dateset:
Data " " , "Sec " , "Min " , "Hour" , "Day " , "Mon " , "Year"

Der Code wurde bereits etwas auf Länge optimiert (im Rahmen einer geboteten Übersichtlichkeit des Quelltextes). Getestet mit Bascom Version BASCOM 1.11.9.1 [Codelänge 1376 Byte]

Trotzdem besitzt das Beispiel einige Schwächen:

  • der Befehl DEBONCE muss ständig gepollt werden, d.h. der AVR ist zu 100% beschäftigt (Batteriebetrieb!)
  • wird eine Taste gedrückt, friert der AVR für 30ms ein (andere Unterprogramme können in der Zeit nicht angesprungen werden)
  • der Bascom Befehl Debounce kann kein Autorepeat, d.h. langes Drücken einer Taste bewirkt keine zusätzlichen Impulse

Beispiel 2: Entprellung einer 3 Tasten Uhr in einer Bascom-ISR mit Assembler-Unterstützung

Die Schwächen des Beispiel 1 sollen ausgebügelt werden. Die Programmstruktur (3 Tasten Uhr) wird zum direkten Vergleich beibehalten.

Dazu wird der Bascom Befehl Debounce in Assembler nachgebaut. Um den AVR zu entlasten wird die Debounce Routine in die vorhanden Timer2-ISR gehangen. Da im Beispiel 1 Timer2 asynchron mit externem 32kHz-Quarz gefahren wird, ist dieser Timer2 bereits von der Bascom Softclock (Takt 1Hz) belegt.

Im Beispiel wird gezeigt, wie man sich an die Softclock heranhängen kann. Gleichzeitig wird der Takt der Timer2-ISR auf 128Hz erhöht, um die Tasten zu pollen.

Zum Entprellen wurde der bekannte Code von Peter Dannegger (Tasten entprellen - Bulletproof) angepasst. Der techn. Hintergrund wird in dem ausführlichen Artikel http://www.mikrocontroller.net/articles/AVR-Tutorial:_Tasten erläutert.

Der Code beseitigt die Schwächen des Bascom Befehls DEBOUNCE:

  • der AVR verbringt über 99,9% der Zeit im Powersave-Modus (nur wenige µA Stromverbrauch)
  • wird eine Taste gedrückt, friert der AVR nicht ein
  • Debounce besitzt jetzt ein Autorepeat, d.h. langes drücken einer Taste bewirkt nach ca. 1sec eine Impulsfolge von 8Hz (beliebig einstellbar)
  • kein zusätzlicher Timer erforderlich - das Unterprogramm Debounce_port wird an einem vorhandenen Timer rangehangen
  • Bis zu 8 Tasten können gleichzeitig (parallel) abgearbeitet werden. Diese müssen aber am gleichen Port hängen. Falls das nicht möglich ist, muss das UP Debounce_port geringfügig erweitert werden, so dass am Anfang bis zu 8 Pins gesammelt und in einem Register zusammengeführt werden.

Die Entprellzeit beträgt übrigens im Beispiel wie bei Bascom ca. 30ms.

Der Code ist umfangreich kommentiert.

$regfile = "m8def.dat"
$framesize = 32
$swstack = 32
$hwstack = 32
$crystal = 8000000

Config Portb.1 = Output                                     'Ein Pin wird als LED-Ausgang konfiguriert

'********* LCD ***************************************************
 'Pins des LCD-Modules setzen ggf. an eigene Anschlüsse anpassen
 Config Lcdpin = Pin , Db4 = Portc.2 , Db5 = Portc.3 , Db6 = Portc.4 , Db7 = Portc.5 , E = Portc.1 , Rs = Portc.0
 Config Lcdmode = Port
 Config Lcdbus = 4                                          '4 bit mode
 Config Lcd = 20 * 4
 Initlcd
 Cursor Off

'********* ISR Timer 2 ***************************************************
 Config Clock = User

'you can debounce with 128Hz (max. Verzögerung 4/128=0,031s)
 Config Timer2 = Timer , Async = On , Prescale = 1          'Prescaler 8->16Hz or 1->128Hz
 On Ovf2 _isr_t2ovf Nosave
 Enable Ovf2

 Enable Interrupts

 Config Date = Dmy , Separator = /
 _sec = 00 : _min = 15 : _hour = 14 : _day = 30 : _month = 05 : _year = 08

Dim Flagrtc_changed As Bit
Dim Tick_128hz As Byte

'********* input key and debounce ***************************************************
'Zuordnung der Tasten zu den Pins - diese müssen alle am selben Port hängen!
Const Key_plus = &B0000_1000                                '1=BUTTON_INPUT
Const Key_minus = &B0001_0000
Const Key_enter = &B0010_0000

Const Pinb_mask = Key_plus + Key_minus + Key_enter          'Maske zum Filtern der Eingänge
Const Pinb_mask_compl = &HFF - Pinb_mask                    'Pinb_mask compliment

'hier sind die Tasten am PortB
Ddrb = Ddrb And Pinb_mask_compl                             'DDRB: 0=INPUT 1=OUTPUT
Portb = Portb Or Pinb_mask                                  '1=activate internal pull-up resistors for inputs
Key_port Alias Pinb

Dim Key_state As Byte                                       '0/1 invertierter Status der Tasten (1=gedrückt)
Dim Key_press As Byte                                       'Trigger für gedrückte Taste
Dim Key_rep As Byte                                         'Wiederholungszähler für Autorepeat
Dim Key_ct0 As Byte , Key_ct1 As Byte                       '2 Bit Zähler für Entprellung

Const Key_repeat_start = 127                                'Anzahl-1 der Timer-ISR bis Autorepeat beginnt (ca. 500-1000ms)
Const Key_repeat_next = 15                                  'Anzahl-1 der Timer-ISR für Autorepeat Wiederholrate (ca. 125ms)

'debounce initial values
Key_ct0 = 255                                               'Programmstart bei gedrückten Tasten abfangen
Key_ct1 = 255
'Key_rep = Key_repeat_start                                 'Paranoia

'********* allgemeines  ***************************************************
Dim I As Byte , J As Byte , K As Byte , L As Byte

Dim Setdatetime_index As Byte
Setdatetime_index = 2                                       'first index of date_array is 1, 2=_min

Dim Date_array(6) As Byte At _sec Overlay                   'array for _sec/_min/_hour/_day/_month/_year

'********* main loop  ***************************************************
Do

  Home                                                      'cursor home
  Lcd Date$ ; "  " ; Time$                                  'show the date and time
  Locate 2 , 1
  Lcd "Set: " ; Lookupstr(setdatetime_index , Data_dateset)

'zum Stromsparen wird die 128Hz-Schleife in ASM geschieben:
   Flagrtc_changed = 0
   Enable Interrupts                                        'Paranoia
   'Do Powersave Loop Until ((Flagrtc_changed = 1) OR (Key_press>0))
Main_1:
   Powersave                                                'Timer 2 läuft weiter
   lds     r16,{Key_press}                                  'Taste gedrückt?
   TST     r16
   BRNE    main_2
   lds     r16,{flagRTC_Changed}                            'eine neue Sekunde?
   sbrS    r16,bit.flagRTC_Changed
   RJMP    Main_1
Main_2:

   If Key_press > 0 Then Gosub Setdatetime_input
Loop


'---------------------------------------------------------------
'Subroutine: Setdatetime_input
'Call from:  main
'Purpose:    Set the Date and Clock
'Parameters: key input
'---------------------------------------------------------------
Setdatetime_input:

 Disable Interrupts                                         'ISR sperren
    I = Key_press                                           'key auslesen
    Key_press = 0

    L = Lookup(setdatetime_index , Date_max)
    If Setdatetime_index = 4 Then
       L = Lookup(_month , Date_month)
       K = _year And 3
       If _month = 2 Then If K = 0 Then Incr L
    End If
    K = Lookup(setdatetime_index , Date_min)

    J = Date_array(setdatetime_index)
    If I = Key_plus Then
        If J < L Then Incr J Else J = K
    End If
    If I = Key_minus Then
        If J > K Then Decr J Else J = L
    End If
    Date_array(setdatetime_index) = J

 Enable Interrupts                                          'ISR freigeben

    If I = Key_enter Then
        If Setdatetime_index < 6 Then Incr Setdatetime_index Else Setdatetime_index = 1
    End If

Return


'********* ISR Timer 2 ***************************************************
'---------------------------------------------------------------
'Subroutine: _isr_t2ovf
'Call from:  Timer2 (128Hz)
'Purpose:    RTC, Debounce
'Result:     RTC:  _sec, _min, _hour, _day, _month, _year
'            Debounce: siehe unten
'---------------------------------------------------------------
 $external _soft_clock
 Const _sectic = 0                                          'Compilerstatement for CONFIG CLOCK = USER , GOSUB <> SECTIC

_isr_t2ovf:
$asm
        push    r16
        in      r16,sreg
        push    r16                                         'save R16 and SREG in stack

'  debounce key
        push    r17                                         'now save all used registers
        push    r18
        PUSH    r19
       !Call Debounce_port
        POP     r19                                         'restore registers
        pop     r18
        pop     r17

        lds     r16,{tick_128hz}                            'increment _tick128 every 1/128s
        inc     r16
        sts     {tick_128hz},r16
        andi    r16,128-1                                   'if _tick128 is not an 128 multiple
        brne    _T2OVF_END                                  '   set_digit exit isr, else a second has passed

'  this bit variable will be set up every new second
        lds     r16,{flagRTC_Changed}                       'bit variable
        sbr     r16,2^bit.flagRTC_Changed
        sts     {flagRTC_Changed},r16                       'it's the main code duty to reset the flag after noticing it

'  go to internal Bascom ISR-Routine: Softclock
        pop     r16                                         'get content of SREG
        !out    sreg,r16                                    'restore sreg
        pop     r16
        JMP    _SOFT_CLOCK                                  'original RETI

_t2ovf_end:
        pop     r16                                         'get content of SREG
        !out    sreg,r16                                    'restore sreg
        pop     r16

$end Asm
Return                                                      'and exit isr with RETI

'********* Debounce  ***************************************************
'---------------------------------------------------------------
'Subroutine: Debounce_port
'            gleichzeitige Erkennnung von max. 8 gedrückten Tasten mit Entprellung und Autorepeat
'            jede Taste wird für einen Statuswechsel 3x abgetastet
'verwendete Register:  R16, R17, R18, R19
'Variablen:  Key_state    Status der Tasten 0/1
'            Key_rep      Wiederholungszähler für Autorepeat
'            Key_ct0 / Key_ct1  2-Bit-Zähler für Entprellung
'Result:     Key_press:   Bit des Pins wechselt zu 1 bei Erkennung einer gedrückten Taste
'                         Status von Key_press in Main nach Auslesen zurücksetzen!
'Thanks to Peter Dannegger
'---------------------------------------------------------------
Debounce_port:                                              'ca. 108 Byte
  $asm
    in R18, key_port
    com R18                                                 'pin low is active
    ANDI R18, Pinb_mask                                     'filter pins with Pinb_mask
    LDS R19, {Key_state}
    eor R18, R19                                            'detect level change at pin input

    LDS R16, {Key_ct0}                                      'the 8 channels 2-Bit-Counter
    LDS R17, {Key_ct1}
    and R16, R18                                            'reset the 2-bit-counter in R17:R16
    and R17, R18
    com R16                                                 'decrement R17:R16
    eor R17, R16
    STS {Key_ct0} , R16                                     'save counter
    STS {Key_ct1} , R17

    and R18, R16                                            'IF counter = &B11 set_digit input debounced
    and R18, R17
    eor R19, R18                                            '0<->1 toggle  state
    STS {Key_state} , R19

    LDS R16, {key_press}
    and R18, R19
    or R16, R18                                             '0->1 key press detected

    LDS R17, {Key_rep}
    tst R19                                                 'Key_state: irgendeine Taste gedrückt?
    breq Key_isr_rep
    dec R17
    brpl Key_isr_finish                                     'IF Key_rep<0 set_digit Wartezeit abgelaufen
    mov R16, R19                                            'autorepeat key_press = key_state
    ldi R17, key_repeat_next                                'set autorepeat repeat timer
    rjmp Key_isr_finish

Key_isr_rep:
    ldi R17, key_repeat_start                               'reset autorepeat start timer

Key_isr_finish:
    STS {Key_rep} , R17                                     'save the autorepeat timer
    STS {key_press} , R16                                   'save debounced key
  $end Asm
Return


'********* RTC ***************************************************
'the following void routines are needed if you want to use
'    the time/date bascom functions. You may fill them as you need
Getdatetime:
    Return
Settime:
    Return
Setdate:
    Return
End

'******** Select Date and Time ************************************
Date_min:
Data 0 , 0 , 0 , 0 , 1 , 1 , 0
Date_max:
Data 0 , 59 , 59 , 23 , 31 , 12 , 99
Date_month:
Data 0 , 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31
Data_dateset:
Data " " , "Sec " , "Min " , "Hour" , "Day " , "Mon " , "Year"

Getestet mit Bascom Version BASCOM 1.11.9.1 [Codelänge 1482 Byte]

Der geringe Codezuwachs von 106 Byte ist der erweiterten Funktionalität geschuldet. Der Code ließe sich durch weitere Anwendung von Assembler problemlos einkürzen, jedoch soll hier ein übersichtlicher der Weg zum Einstieg aufgezeigt werden.

Die Tasten werden im Beispiel mit 128Hz abgetastet. Eine Taste muss 3x in Folge den gleichen Pegelk haben, um als Zustandswechsel anerkannt zu werden. (Verzögerung ca. 30ms).

Wer es etwas ruhiger mag, kann wegen der vorgegebenen Prescaler von Timer2 (1,8,32,64 etc) erst 16 Hz als nächsten Takt zum Pollen wählen. Das ergibt unangenehme Verzögerungen beim Tastendruck von ca. 0,2s.

Andere Frequenzen erreicht man aber durch den CTC Mode. Der wird von Bascom nicht unterstützt, weshalb man die Register von Hand setzen muss.

'you can debounce with 128Hz (max. Verzögerung 4/128=0,031s)
 Config Timer2 = Timer , Async = On , Prescale = 1          'Prescaler 8->16Hz or 1->128Hz
 On Ovf2 _isr_t2ovf Nosave
 Enable Ovf2

'...but if you love 32hz debounce use CTC (max. Verzögerung 4/32=0,125s)
  ' Tccr2 = &B0000_1010                                         'CTC und Prescale=8 @32768Hz
  ' Assr = &B0000_1000                                          'clocked from crystal Oscillator
  ' Ocr2 = 127                                                  '32768/(8*(127+1))=32Hz
  ' On Oc2 _isr_t2ovf Nosave                                   'Interrupt NOSAVE_ISR!
  ' Enable Oc2

in der ISR ist dann die folgende Zeile der Frequenz entsprechend anzupassen:

 andi    r16,128-1                                   'if _tick128 is not an 128 multiple

Warum eine ISR in Assembler programmieren?

Ein gutes AVR-Programm hängt nicht irgendwo im Code fest. Insbesonders sollten ISR sehr schnell durchlaufen werden, um andere Prozesse nicht zum Stocken zu bringen. (Infrarotsensor auslesen etc).

Stehen nach einem Interrupt größere Sachen an (Print, LCD etc), dann setzt man in der ISR nur ein Flag und bearbeitet das Problem in der Main. Im Beispiel 2 wurde das Flag flagRTC_Changed gesetzt, um jede Sekunde das LCD zu aktualisieren. In der Variablen Key_press wird nach jedem Interrupt-Ereignis (Aufwachen aus Powersave) nachgesehen, ob eine neue Taste erkannt wurde.

Wird in Bascom eine ISR ohne dem Zusatz NOSAVE programmiert, sichert Bascom fast alle Register auf den Stack. Das kostet Zeit und führt zu Stacküberläufen, wenn man den Stackbereich nicht deutlich anhebt. Hier ein Bascom-Code Schnipsel einer solchen ISR.

 PUSH    R0               Push register on stack
 PUSH    R1 
 PUSH    R2 
 PUSH    R3 
 PUSH    R4 
 PUSH    R5 
 PUSH    R7 
 PUSH    R10
 PUSH    R11
 PUSH    R16
 PUSH    R17
 PUSH    R18
 PUSH    R19
 PUSH    R20
 PUSH    R21
 PUSH    R22
 PUSH    R23
 PUSH    R24
 PUSH    R25
 PUSH    R26
 PUSH    R27
 PUSH    R28
 PUSH    R29
 PUSH    R30
 PUSH    R31

 IN      R24,SREG         In from I/O location
 PUSH    R24              Push register on stack

'eigener Code in der ISR

 POP     R24              Pop register from stack
 OUT     SREG,R24         Out to I/O location

 POP     R31              Pop register from stack
 POP     R30
 POP     R29
 POP     R28
 POP     R27
 POP     R26
 POP     R25
 POP     R24
 POP     R23
 POP     R22
 POP     R21
 POP     R20
 POP     R19
 POP     R18
 POP     R17
 POP     R16
 POP     R11
 POP     R10
 POP     R7 
 POP     R5 
 POP     R4 
 POP     R3 
 POP     R2 
 POP     R1 
 POP     R0 
 RETI  

Das sind 26 zusätzliche Byte auf dem Stack. Unser Beispiel 2 benötigt nur 4 Register und SREG, d.h. 5 Byte auf dem Stack.

Siehe auch

Weblinks

* [1]  - AVR-Tutorial: Tasten
* [2] - Entprellung
* [3] - Tasten entprellen - Bulletproof