Controlador de Interrupção e Sistema de Interrupções no Linux Embarcado

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.

Tags: linux-embarcado interrupcoes gic-v3 device-tree driver-kernel

Publicado em 9-3 11:35