PiOS: ARM Timer und Interrupts

Seit einiger Zeit arbeite ich an einem prototypischem Betriebssystem, PiOS (“früher” PilotOS) [1]. Ich habe früher schon einige Artikel über Prozesse [2][3] geschrieben und will nun hier den aktuellen Stand diskutieren. Neben einem Rewrite des Codes, also entfernen quasi aller alten Assembler-Sourcen und Neuschreiben der Funktionalität in C, sind auch Treiber entstanden für i2c und einem LCD character display, die aktuell aufgrund fehlender Testmöglichkeiten lediglich Versuchsballons sind. Weiter habe ich heute den ARM Timer [4, S. 196 ff] implementiert und meine ersten Interrupts erhalten.

Zunächst lege ich i.d.R. eine Map an für die Register, die im Speicheraddressraum eingebunden sind, sodass ich eine benannte Datenstruktur (i.d.R. ein struct) habe, welches ich benutzen kann um die Register der Peripherie anzusprechen (Stichwort: Memory Mapped I/O). Der ARM-Timer verfügt über 9 32-Bit Register:

struct _pios_arm_timer_t
{
    uint32_t load;          ///< load a specific value into the counter, after counted-down an IRQ occurs
    uint32_t value;         ///< a value, counted down. On 0 IRQ is set and the value is reloaded from reload-register
    uint32_t control;       ///< control register, which allows for setting some settings
    uint32_t irqack;        ///< write only for clearing the IRQ-line
    uint32_t irqraw;        ///< is an interrupt pending? (i.e. LSB is set to 1)
    uint32_t irqmasked;     ///< is the IRQ pending bit set and the interrupt enable bit?
    uint32_t reload;        ///< copy of load, but writing will not trigger an overwrite of value, just when value reaches 0
    uint32_t prescale;      ///< setting a prescaler for the timer - LSB 10 Bits (timer_clock = apb_clock/(pre_divider+1))
    uint32_t freerunning;   ///< a free running value (has its own prescaler in control-register)
} typedef pios_arm_timer_t;

Der Timer ist recht einfach. Er hat ein Register, welches freilaufend ist, d.h. bei jedem Timer-Tick (beachte Prescaler!) hochgezählt wird. Außerdem besitzt der Timer über eine Art Wecker-Funktion. Ein Wert kann im Register load eingespeichert werden, welcher dekrementiert wird bei jedem Timer-Tick, solange bis das Register den Wert 0 erreicht. Ist 0 erreicht, wird der eingespeicherte Wert erneut eingetragen und eine Interrupt-Leitung gesetzt.

Die Einstellungen des Timers sind komplett über das control-Register möglich. Die Bedeutung der entsprechenden Bitstellen ist im BCM2835-Peripherie-Handbuch angegeben. Die Bits sind hier als C-Präprozessor-Konstanten angegeben:

#define PIOS_ARM_TIMER_32BIT 2

#define PIOS_ARM_TIMER_PRESCALE_1 (0 << 2)
#define PIOS_ARM_TIMER_PRESCALE_16 (1 << 2)
#define PIOS_ARM_TIMER_PRESCALE_256 (2 << 2)
#define PIOS_ARM_TIMER_PRESCALE_UNDEF (3 << 2)

#define PIOS_ARM_TIMER_IRQ_ENABLE (1 << 5)
#define PIOS_ARM_TIMER_ENABLE (1 << 7)
#define PIOS_ARM_TIMER_HALT_IN_DEBUG (1 << 8)
#define PIOS_ARM_TIMER_FREERUNNING_ENABLE (1 << 9)
#define PIOS_ARM_TIMER_FREERUNNING_PRESCALER(a) ((a&0xff) << 16)

Das freerunning-Register hat einen eigenen Prescaler, sodass das Register unabhängig vom “Wecker” betrachtet werden kann. Beachte, dass die Konstante dazu einen 8-Bit-Wert annimmt und um 16 Bits nach links schiebt – eine bessere Möglichkeit wäre schön und kommt vielleicht irgendwann(tm).

Funktionsweise eines Timers

Ein Timer funktioniert prinzipiell so, dass im Chip ein Oszillator verbaut ist, bspw. ein Quarz-Oszillator. Dieser Quarzkristall wird durch Spannung angeregt sich zu verformen (siehe Piezo-elektrischer Effekt). Diese Verformung führt dazu, dass sich eine Ladung des Quarzes ergibt. Der Kristall schwingt gewissermaßen in einer mehr oder weniger genauen Frequenz und erzeugt somit ein periodisches Signal, im Idealfall bereits ein Rechteckssignal, aber zur Sicherheit kann das Signal durch einen Schmidt-Trigger zum Rechteck gemacht werden. Außerdem kann der Schmidt-Trigger die Spannungspegel so regeln, dass die Spannungen durch die digitale Hardware erkannt werden.

Der eigentliche Timer oder Zähler besteht aus einer Reihe von Flip-Flops (also 1 Bit-Speicherbausteinen). Vorstellbar sind bspw. T-Flip-Flops (Trigger-Flip-Flops), die bei einer Eingabe des logischen Wertes 1 ihren Wert wechseln. Die Speicherbausteine sind so verschaltet, dass beim Schalten des ersten (Least Significant Bit) von 1 auf 0, der nächst höhere umschaltet, etc. bspw. indem die Ausgabe des LSB-Flip-Flops als Clock-Leitung genutzt wird. Flip-Flops reagieren i.d.R. auf eine Flanke, nicht auf einen (HIGH/LOW)-Pegel.

Der Prescaler

Ein Prescaler, oder auch Vorteiler, teilt die schnelleren Spannungswechsel, die vom Schmidt-Trigger erzeugt werden, mehrfach vor, sodass eine langsamere Frequenz entsteht. Für Teilungsfaktoren um den Faktor zwei werden wiederum Flip-Flops benutzt, die vor den eigentlichen Zähler geschaltet werden. Die Ausgabe der Vorteilerschaltung wird an die Eingabe des Zählers geschaltet. Die Vorteiler-Flip-Flops strecken somit die LOW bzw. HIGH-Phasen des Eingangssignals für den Zähler.

Interrupts

Der ARM-Prozessor des Raspberry Pi kann genau acht Interrupt-Quellen unterscheiden. Dabei sind nicht alle Quellen tatsächlich externe Hardware, sondern Interrupts können auch vom Prozessor selbst erzeugt werden. Damit der Prozessor weiß, was zu tun ist, wenn ein Interrupt auftritt, gibt es im Speicher eine so genannte Interrupt Tabelle, die angibt, wo der Prozessor hinspringen muss (bzw. welche Befehle auszuführen sind), wenn ein bestimmter Interupt auftritt. Diese Tabelle liegt beim Raspberry Pi an der physischen Adresse 0x0000 0000, d.h. genau am Nullpunkt und ist 8 mal 8 Byte groß (2 * 4 Byte pro Interrupt-Quelle). Die Tabelle muss in Software gesetzt werden, sie kann soweit ich weiß nicht zur Compilezeit angelegt werden, wie bei den AVR-Prozessoren. Der folgende Code (von [5]) funktioniert dafür:

start:
    ldr pc, _reset_h
    ldr pc, _undefined_instruction_vector_h
    ldr pc, _software_interrupt_vector_h
    ldr pc, _prefetch_abort_vector_h
    ldr pc, _data_abort_vector_h
    ldr pc, _unused_handler_h
    ldr pc, _interrupt_vector_h
    ldr pc, _fast_interrupt_vector_h

_reset_h:                           .word   _reset_
_undefined_instruction_vector_h:    .word   undef_vector
_software_interrupt_vector_h:       .word   swi_vector
_prefetch_abort_vector_h:           .word   abort_vector
_data_abort_vector_h:               .word   abort_vector
_unused_handler_h:                  .word   _reset_
_interrupt_vector_h:                .word   irq_vector
_fast_interrupt_vector_h:           .word   fiq_vector

_reset_:
    mov     r0, #0x8000
    mov     r1, #0x0000
    ldmia   r0!,{r2, r3, r4, r5, r6, r7, r8, r9}    /** load 32 Byte worth of data **/
    stmia   r1!,{r2, r3, r4, r5, r6, r7, r8, r9}    /** store 32 Byte **/
    ldmia   r0!,{r2, r3, r4, r5, r6, r7, r8, r9}
    stmia   r1!,{r2, r3, r4, r5, r6, r7, r8, r9}

In der ersten Näherung lege ich mir folgende Interrupt-Service-Routinen an, die eine Ausgabe über UART tätigen und dann in einer Endlosschleife hängen bleiben:

void __attribute__((interrupt("UNDEF"))) undef_vector(void)
{
    pios_uart_puts ("UNDEF 🙁 \n");
    while( 1 )
    {
        /* Do Nothing! */
    }
}
void __attribute__((interrupt("IRQ"))) irq_vector(void)
{
    pios_uart_puts (" -> ! IRQ 🙂 \n");
    while( 1 )
    {
        /* Do Nothing! */
    }
    
}
void __attribute__((interrupt("FIQ"))) fiq_vector(void)
{
    pios_uart_puts ("FIQ 🙁 \n");
    while( 1 )
    {
        /* Do Nothing! */
    }
}
void __attribute__((interrupt("SWI"))) swi_vector(void)
{
    pios_uart_puts ("SWI 🙁 \n");
    while( 1 )
    {
        /* Do Nothing! */
    }
}
void __attribute__((interrupt("ABORT"))) abort_vector(void)
{
    pios_uart_puts ("ABORT :(\n");
    while( 1 )
    {
        /* Do Nothing! */
    }
}

Interrupts anschalten

Nun weiß also mein Prozessor was zu tun ist, wenn ein Interrupt kommt. Aber er wird noch nichts machen, weil die Interrupts im Prozessor noch angeschalten werden müssen. D.h. wir können unseren Prozessor anweisen externe Interrupt (wie auch Fast Interrupts) zu ignorieren. Das ist der Standardzustand, wenn der Raspberry Pi angeschalten wird. [7] gibt an, dass das IRQ-disable-Bit mit 0x80 angesprochen werden kann. Also aktivieren wir die Interruptbehandlung, indem wir das Bit auf 0 setzen (clear):

pios_irq_enable:
    mrs r0, cpsr
    bic r0, r0, #0x80
    msr cpsr_c, r0
    
    mov pc, lr

Beachte, dass das Programm status register (cpsr) nur durch bestimmte Befehle bearbeitet werden kann (mrs, msr). Diese Befehle sind nur in bestimmten Rechteeinstellungen des Prozessors erlaubt und führen bspw. im User-Modus des Prozessors zu einem Interrupts. Das soll sicherstellen, dass Nutzerprogramme nicht in das Statusregister schreiben können und den Modus wechseln können.

Interrupt-Controller

Der ARM1176jzf-s, der im Raspberry Pi verbaut ist, hat aber noch einen weiteren Trick im Ärmel. Richtig ist, dass der ARM-Kern nur 8 Interrupt-Quellen behandeln kann. Dennoch kann ein Interrupt durch mehrere (externe) Geräte erzeugt werden oder verschiedene Gründe haben. Um präziser Interrupts ein- bzw. ausschalten zu können, hat der Pi einen Interrupt-Controller, der zusammen mit dem ARM-Kern arbeiten soll.

Laut Handbuch [4, S. 109 ff] ist der Controller unter der Adresse 0x2000 B200 zu erreichen (Beachte: die angegebene Adresse im Handbuch ist die, die über den ARM-Bus geht, nicht aber die Adresse, die vom ARM-Kern aus sichtbar ist). Hier sind bestimmte Register dafür zuständig Interruptquellen ein- bzw. auszuschalten. Wir wollen hier den Timer aktivieren, also schreiben wir den passenden Wert in das Base Interrupt enable register:

//*(0x2000B218) = (1<<0)
RPI_GetIrqController()->Enable_Basic_IRQs = RPI_BASIC_ARM_TIMER_IRQ;

Den Wecker stellen

Der Timer sollte zuvor gestellt werden. Dazu schreiben wir einen gewünschten Startwert in das load-Register und aktivieren den Timer mit den für uns passenden Werten (32-Bit Modus, mit aktivierten Interrupts und angeschaltetem Timer):

// pios_arm_timer->load = load;
pios_arm_timer_setLoad ( 0x2000 );

/**
 * if ( !(prescaler == PIOS_ARM_TIMER_PRESCALE_1 || prescaler == PIOS_ARM_TIMER_PRESCALE_16 
 *     || prescaler == PIOS_ARM_TIMER_PRESCALE_256 || prescaler == PIOS_ARM_TIMER_PRESCALE_UNDEF) )
 *   {
 *       prescaler = PIOS_ARM_TIMER_PRESCALE_256;
 *   }
 *   uint32_t val = PIOS_ARM_TIMER_32BIT | prescaler | (irq ? PIOS_ARM_TIMER_IRQ_ENABLE : 0) | PIOS_ARM_TIMER_FREERUNNING_ENABLE | (enable ? PIOS_ARM_TIMER_ENABLE:0);
 *   pios_arm_timer->control = val;
**/
pios_arm_timer_init ( true, PIOS_ARM_TIMER_PRESCALE_256, true );

Nun erhalten wir Interrupts vom ARM-Timer.

[1] https://github.com/naums/PiOS
[2] https://www.stefannaumann.de/de/2016/05/pilotos-prozesse/
[3] https://www.stefannaumann.de/de/2016/05/pilotos-prozesse-2-zustaende-und-blockierung/
[4] https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf
[5] http://www.valvers.com/open-software/raspberry-pi/step04-bare-metal-programming-in-c-pt4/
[6] http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0333h/ch02s09s01.html
[7] http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0333h/I2837.html

Das könnte Dich auch interessieren...