Přeskočit na hlavní obsah

Přerušení

Přerušení je způsob, jak může hardware říct procesoru: "teď mě prosím obsluž".

Bez přerušení by program musel pořád dokola kontrolovat, jestli se něco nestalo. Tomu se říká polling.

while (1) {
if (uart_prijal_byte()) {
zpracuj_byte();
}
}

S přerušením může procesor dělat něco jiného a hardware ho vyruší až ve chvíli, kdy je opravdu potřeba.

ISR

Kód, který se spustí při přerušení, se jmenuje interrupt service routine, zkráceně ISR.

V AVR kódu vypadá třeba takto:

ISR(USART0_RXC_vect)
{
const uint8_t byte = USART0.RXDATAL;
}

USART0_RXC_vect je vektor přerušení. Říká, která hardwarová událost tuto funkci spouští. Tady jde o příjem bajtu přes USART0.

Povolení konkrétního přerušení

Nestačí napsat ISR. Musíme také říct periferii, že má přerušení vyvolávat.

Povolení přerušení příjmu přes UART může vypadat takto:

USART0.CTRLA = USART_RXCIE_bm;

USART_RXCIE_bm je bitová maska. Zapíná přerušení při přijetí znaku.

Názorně:

Povolení přerušení příjmu přes USART
76543210
před
00000000
RXCIE
10000000
po
10000000

V tomto konkrétním zápisu se do registru zapisuje jen jedna volba. Kdybychom chtěli přidat bit bez ovlivnění ostatních, psali bychom:

USART0.CTRLA |= USART_RXCIE_bm;

Podrobné vysvětlení |=, &= a masek je v kapitole Ovládání registrů.

Globální povolení přerušení

Mikrokontrolér má ještě hlavní vypínač přerušení. Na AVR se zapíná funkcí:

sei();

V hlavním programu bývá pořadí podobné. Celkový tvar main je podrobněji popsaný v kapitole Struktura embedded programu:

int main(void)
{
clock_init();
uart_init();
communication_init();
sensors_init();
sei();

while (1) {
communication_process_rx();
communication_task();
}
}

Po sei() už mohou povolená přerušení opravdu běžet. Před tím si program připraví hodiny, UART, komunikaci a senzory.

To je důležité: nechceme, aby přerušení přišlo dřív, než jsou připravené proměnné a periferie, které ISR používá.

Příklad: příjem UARTu

UART často používá přijímací buffer. Když přijde bajt, spustí se přerušení:

ISR(USART0_RXC_vect)
{
const uint8_t byte = USART0.RXDATAL;
const uint8_t next_head = (uint8_t)((uart_rx_head + 1U) % UART_RX_BUFFER_SIZE);

if (next_head == uart_rx_tail) {
uart_rx_overflow = 1;
return;
}

uart_rx_buffer[uart_rx_head] = byte;
uart_rx_head = next_head;
}

Tady ISR udělá jen rychlou práci:

  1. přečte přijatý bajt z USART0.RXDATAL,
  2. spočítá další místo v kruhovém bufferu,
  3. když je buffer plný, nastaví příznak přetečení,
  4. jinak bajt uloží.

Hlavní program pak může data z bufferu číst později. To je dobrý styl: v přerušení udělat minimum a delší práci nechat na hlavní smyčce.

Příklad: ADC hotovo

Další časté přerušení je od ADC převodníku:

ADC0.INTCTRL = ADC_RESRDY_bm;

Tento řádek povolí přerušení, když je výsledek ADC měření připravený.

Obsluha vypadá zjednodušeně takto:

ISR(ADC0_RESRDY_vect)
{
sensors_accumulator[sensor_current_channel] += (int32_t)ADC0.RESULT;

++sensor_current_channel;
if (sensor_current_channel >= SENSORS_CHANNEL_COUNT) {
sensor_current_channel = 0;
++sensor_sample_index;
}

sensors_start_conversion(sensor_current_channel);
}

ADC tedy nemusí program pořád kontrolovat otázkou: "už jsi doměřil?"

ADC samo zavolá přerušení, jakmile má výsledek.

Sdílené proměnné a volatile

Proměnná měněná v přerušení musí být volatile

Proměnné, které mění přerušení a zároveň je čte hlavní program, musí být označené jako volatile.

Typické sdílené proměnné pro UART buffer mohou vypadat takto:

static volatile uint8_t uart_rx_head;
static volatile uint8_t uart_rx_tail;
static volatile uint8_t uart_rx_overflow;

volatile říká překladači: "tato hodnota se může změnit i mimo běžný tok programu".

Bez volatile by si překladač mohl myslet, že se hodnota sama od sebe nemění, a udělat optimalizaci, která by v programu způsobila těžko hledatelnou chybu.

Kritická sekce

Někdy hlavní program potřebuje přečíst více hodnot, které může ISR zrovna měnit. Pak je potřeba přerušení na chvíli vypnout.

Kritická sekce může vypadat například takto:

const uint8_t sreg = SREG;
cli();

snapshot->values_sequence = sensors_values_sequence;
snapshot->pair_sequence = sensors_pair_sequence;

SREG = sreg;

cli() vypne globální přerušení. SREG = sreg vrátí předchozí stav registru SREG, tedy i původní nastavení přerušení.

Takové místo se jmenuje kritická sekce. Má být co nejkratší. Když přerušení vypneme na dlouho, hardware může mezitím čekat a některé události se mohou ztratit.

Co do ISR nepatří

Do obsluhy přerušení nepatří dlouhé čekání. Špatný nápad je třeba:

ISR(USART0_RXC_vect)
{
_delay_ms(100);
}

Když je procesor uvnitř ISR, hlavní program stojí. Často stojí i další přerušení.

V ISR je dobré:

  • přečíst data,
  • uložit je do bufferu,
  • nastavit příznak,
  • rychle skončit.

Delší výpočty, posílání odpovědí nebo práce s protokolem patří raději do hlavní smyčky.

Obecný postup

Když chcete použít přerušení, typicky uděláte toto:

  1. Nastavíte periferii.
  2. Povolíte konkrétní interrupt bit v registru periferie.
  3. Připravíte sdílené proměnné jako volatile.
  4. Napíšete ISR(...).
  5. Až nakonec zavoláte sei().

Přerušení jsou silný nástroj. Program díky nim nemusí pořád dokola hlídat každou událost. Ale platí za to tím, že musíme pečlivě přemýšlet, co se může změnit "mezi dvěma řádky" hlavního programu.