Introdução ao Sistema de Interrupções
O controle de interrupções em microcontroladores bare-metal, como o STM32, opera de forma direta através do NVIC (Nested Vectored Interrupt Controller). O fluxo é simples: periférico → NVIC → CPU, quase sem camadas intermediárias.
No Linux embarcado, a arquitetura é mais complexa e hierárquica:
sinal do periférico
↓
Controlador GIC (roteamento / prioridade / núcleo alvo)
↓
Ponto de entrada de exceção da CPU (EL1 IRQ)
↓
Subsistema de Interrupções do Linux
↓
Função de tratamento registrada via request_irq()
O Controlador de Interrupção Genérico (GIC)
O GIC (Generic Interrupt Controller) é um módulo de hardware independente projetado pela ARM para sistemas com múltiplos núcleos e sistema operacional. Ele não é uma pequena lógica interna da CPU. Sua função principal é receber sinais de interrupção de hardware e distribuí-los para os núcleos de CPU apropriados baseado em políticas configuráveis.
Na versão GIC v3, os tipos de fontes de interrupção são:
- SGI (Software Generated Interrupt): Interrupção gerada por software, tipicamente para comunicação entre núcleos (IPI - Inter-Processor Interrupts).
- PPI (Private Peripheral Interrupt): Interrupção privada de periféricos, destinada a um núcleo específico.
- SPI (Shared Peripheral Interrupt): Interrupção de periféricos compartilhada por todos os núcleos, podendo ser direcionada a qualquer um.
- LPI (Locality-specific Peripheral Interrupt): Introduzido no GICv3, baseado em mensagens e configurado via tabelas em memória ao invés de registradores.
Arquitetura do GIC v3
Os principais componentes são:
- Distribuidor (Distributor): Gerencia interrupções SPI, realizando arbitragem e distribuição para os Redistribuidores.
- Interface da CPU (CPU Interface): Fornece uma interface para cada núcleo do processador, habilitando ou desabilitando o envio de requisições de interrupção.
- Redistribuidor (Redistributor): Gernecia interrupções SGI, PPI e LPI, encaminhando-as para a Interface da CPU correspondente.
- ITS (Interrupt Translation Service): Módulo opcional que converte interrupções baseadas em mensagem (MSI) para LPIs.
O estado de cada interrupção é mantido por uma máquina de estados, com os seguintes estados possíveis: Inativo, Pendente, Ativo, e Ativo e Pendente.
Configuração do GIC via Registradores
A configuração é realizada escrevendo em registradores específicos. Por exemplo:
- GICD_ISENABLERn: Habilita ou desabilita uma interrupção (escrita de 1 habilita, escrita de 0 não tem efeito).
- GICD_IPRIORITYRn: Configura a prioridade da interrupção.
- GICC_PMR: Define uma máscara de prioridade; apenas interrupções com prioridade mais alta são passadas para a CPU.
- GICC_BPR: Agrupa os bits de prioridade em prioridade de preempção e subprioridade.
No SoC RK3588, os endereços base desses registradores são definidos no device tree:
reg = <0x0 0xfe600000 0 0x10000>, /* GICD */
<0x0 0xfe680000 0 0x100000>; /* GICR */
GICD (Distributor): Endereço base 0xfe600000, tamanho 64KB.
GICR (Redistributor): Endereço base 0xfe680000, tamanho 1MB, para os 8 núcleos.
Representação no Device Tree
O nó do controlador de interrupção no device tree (exemplo para RK3588) define o hardware e como ele é usado:
gic: interrupt-controller@fe600000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
interrupt-controller;
reg = <0x0 0xfe600000 0 0x10000>, /* GICD */
<0x0 0xfe680000 0 0x100000>; /* GICR */
interrupts = <gic_ppi irq_type_level_high="">;
its0: msi-controller@fe640000 {
compatible = "arm,gic-v3-its";
msi-controller;
#msi-cells = <1>;
reg = <0x0 0xfe640000 0x0 0x20000>;
};
// ... possivelmente its1
};</gic_ppi>
compatible: Usado para casar o driver correto no kernel.#interrupt-cells = <3>: Especifica que interrupções são descritas por 3 valores: tipo, número e flags de trigger.interrupt-controller: Declara que este nó é um controlador de interrupções.interrupts: Define a própria interrupção de manutenção do GIC (PPI 9).its0: Sub-nó opcional para o controlador MSI (Message Signaled Interrupt).
Um periférico que usa uma interrupção referencia este controlador e descreve sua interrupção específica. Por exemplo, um UART:
uart3: serial@fe670000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
interrupts = <gic_spi irq_type_level_high="">;
// ... outras propriedades
};</gic_spi>
Aqui, GIC_SPI indica o tipo (interrupção compartilhada de periférico), 119 é o número da interrupção, e IRQ_TYPE_LEVEL_HIGH define o trigger de nível alto.
APIs do Kernel Linux para Interrupções
O driver no espaço do usuário interage com o subsistema de interrupções através de funções específicas.
Obtendo o Número da Interrupção (IRQ)
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
Extrai o número IRQ da propriedade interrupts no device tree. Para GPIOs:
int gpio_to_irq(unsigned int gpio)
Registrando uma Rotina de Tratamento
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev)
Flags comuns incluem:
IRQF_SHARED: Para compartilhar uma linha de interrupção entre múltiplos dispositivos.IRQF_TRIGGER_RISING: Trigger na borda de subida.IRQF_TRIGGER_FALLING: Trigger na borda de descida.IRQF_TRIGGER_HIGH: Trigger em nível alto.IRQF_TRIGGER_LOW: Trigger em nível baixo.
Liberando a Interrupção
void free_irq(unsigned int irq, void *dev)
Habilitando e Desabilitando Interrupções
void enable_irq(unsigned int irq);
void disable_irq(unsigned int irq);
disable_irq aguarda a conclusão do handler atual. Para uma desabilitação imediata, use:
void disable_irq_nosync(unsigned int irq)
Exemplo Prático: Driver para Botão
Um driver simples para um botão conectado a um GPIO ilustra o uso das APIs.
Device Tree Overlay
/dts-v1/;
/plugin/;
#include <dt-bindings>
#include <dt-bindings>
#include <dt-bindings>
&{/} {
button_interrupt: button_interrupt {
status = "okay";
compatible = "button_interrupt";
button-gpios = <&gpio1 RK_PB7 GPIO_ACTIVE_LOW>;
interrupt-parent = <&gpio1>;
interrupts = <rk_pb7 irq_type_edge_falling="">;
pinctrl-names = "default";
pinctrl-0 = <&button_interrupt_pin>;
};
};
&{/pinctrl} {
pinctrl_button {
button_interrupt_pin: button_interrupt_pin {
rockchip,pins = <1 RK_PB7 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
};</rk_pb7></dt-bindings></dt-bindings></dt-bindings>
Estrutura do Driver (Trechos Principais)
Probe e Inicialização: Obtém recursos do device tree, configura GPIO e registra o handler de interrupção.
static int button_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
int gpio_num, irq_num, ret;
gpio_num = of_get_named_gpio(dev->of_node, "button-gpios", 0);
ret = gpio_request(gpio_num, "button_irq_gpio");
ret = gpio_direction_input(gpio_num);
irq_num = gpio_to_irq(gpio_num);
ret = request_irq(irq_num, button_isr, IRQF_TRIGGER_FALLING,
"button_irq", NULL);
// ... registro de dispositivo de caractere, etc.
return 0;
}
Handler de Interrupção: Deve ser rápido e usar operações atômicas para contar eventos.
static atomic_t press_counter = ATOMIC_INIT(0);
static irqreturn_t button_isr(int irq, void *dev_id)
{
atomic_inc(&press_counter);
return IRQ_HANDLED;
}
Operação de Leitura: Copia o contador seguro para o espaço do usuário.
static ssize_t button_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
int counter_val = atomic_read(&press_counter);
if (copy_to_user(buf, &counter_val, sizeof(counter_val)))
return -EFAULT;
return sizeof(counter_val);
}
Remove: Libera todos os recursos.
static int button_remove(struct platform_device *pdev)
{
// ... obtém irq_num e gpio_num do contexto
free_irq(irq_num, NULL);
gpio_free(gpio_num);
// ... limpeza do dispositivo de caractere
return 0;
}
O Controlador MSI e ITS
Dispositivos de alto desempenho como controladores PCIe usam interrupções sinalizadas por mensagem (MSI). O módulo ITS dentro do GICv3 é responsável por traduzir essas mensagens em interrupções tradicionais que o GIC pode processar.
Ele mantém tabelas de mapeamento que associam um ID de evento (enviado pelo dispositivo via escrita em memória) a um número de interrupção SPI e a um núcleo de CPU alvo.