Você está na página 1de 19

Laboratório de Processadores

Bruno Basseto / 2023

Raspberry Pi
Em 2009 foi fundada no Reino Unido a Raspberry Pi Foundation, uma
organização sem fins lucrativos que, à semelhança da iniciativa do “BBC-Micro”
na década de 80, tem por objetivo a promoção do estudo de Ciência básica da
Computação nas escolas. Uma das metas é colocar os estudantes novamente em
contato com o “hardware” – manuseando placas e componentes eletrônicos,
como acontecia nas décadas de 80 e 90, com os computadores que também
tinham nomes de frutas.
Os “raspberry pi” são computadores de placa única (single board computer
ou SBC) de baixo custo baseados em unidades de processamento altamente
integradas (SoC), produzidas pela Broadcom com exclusividade para a Raspberry
Pi Foundation.
Modelo CPU Arquitetura Clock GPU Memória modelos

RPi-0
BCM2835 ARM1176JZF-S (1) 700 MHz VideoCore IV 512 MiB
RPi-1
RPi-2 BCM2836 Cortex-A7 (4) 900 MHz VideoCore IV 1 GiB
RPi-3 BCM2837 Cortex-A53 (4) 1.4 GHz VideoCore IV 1 GiB
RPi-4 BCM2711 Cortex-A72 (4) 1.8 GHz VideoCore VI 4 GiB

As placas possuem as funcionalidades de um computador pessoal,


incluindo saídas para monitor (HDMI), áudio e vídeo analógicos e entrada para
câmera (CSI); também incluem conectividade USB e ethernet (algumas placas
possuem wifi e Bluetooth). Sua memória DRAM é embutida na placa e não pode ser
periféricos
expandida; a memória secundária é implementada por um cartão SD (que
substitui o “hard-disk” do computador).
O SoC inclui os núcleos de arquitetura ARM, um processador vetorial
gráfico (Videocore da Broadcom) e diversos periféricos integrados: interface
HDMI, aceleradores de compressão/descompressão de mídia, CODECs, interface
de áudio I2S e PCM, controlador de interface USB, controlador de DMA (com 15
canais), interfaces seriais (SDIO, UART, SPI e I2C) e gerenciamento de I/O (54
pinos “GPIO” configuráveis). As interfaces de rede são implementadas por
componentes adicionais nas placas, externamente ao SoC: ethernet (transceiver
controlado via USB), wifi (transceiver controlado via SDIO) e bluetooth (transceiver
controlado via serial UART).
As placas Raspberry Pi normalmente executam o sistema operacional
Linux e podem instalar software proveniente de repositórios convencionais
(debian, ubuntu, gentoo, etc.). No laboratório, não iremos utilizar o Linux, mas
vamos executar as aplicações diretamente sobre o hardware (“bare metal”).

84
Laboratório de Processadores
Bruno Basseto / 2023

Processo de inicialização (boot)


Os processadores BCM283x possuem um software monitor gravado
internamente em memória não volátil, que é permanentemente executado pela
GPU (processador Videocore). Ao alimentar a placa, esse software funciona como bootloader
um bootloader, acessando o cartão SD (memória secundária) em busca da imagem
do sistema operacional: uma vez encontrado o sistema operacional, o bootloader o
carrega na memória RAM em um endereço fixo, juntamente com o arquivo de
descrição do hardware (no formato “device-tree”). Durante todo esse processo, a
CPU permanece no estado de reset. Uma vez finalizada a carga do sistema, a CPU
é liberada e direcionada a executar a partir do primeiro endereço de memória
onde o sistema foi carregado.
Como não é possível interferir nesse processo de inicialização, vamos
criar aplicações com o mesmo formato executável que é esperado pelo bootloader
e substituir a imagem do sistema operacional por esses arquivos. Assim que
exista um programa em execução no Raspberry Pi, poderemos utilizar a interface
J-TAG para controlar o processador, e depurar ou carregar outros programas
diretamente na memória RAM, evitando a repetição do processo de “boot”.
O cartão SD deve conter pelo menos uma partição inicializável no formato do
cartão SD
formato VFAT (Windows 95), contendo os arquivos bootcode.bin ,
fixup.dat , config.txt , start.elf , além do arquivo executável
contendo a aplicação (geralmente nomeado como kernel.img ). O arquivo
config.txt vai conter a configuração inicial para o bootloader realizar a
carga e a inicialização da aplicação:
config.txt
# Arquivo config.txt
kernel=kernel.img # aplicação a carregar
kernel_address=0x8000 # endereço inicial
arm_64bit=0 # usar modo 32 bits
disable_commandline_tags=1 # não criar linha de comando
enable_jtag_gpio=1 # habilitar depuração jtag

Os arquivos binários executáveis são carregados para a memória byte a


15
byte , portanto devemos traduzi-los do formato ELF para um formato “cru”
(“raw”), por exemplo com o utilitário arm-none-eabi-objcpy . O endereço
0x8000 é tradicionalmente utilizado para carregar o Linux; esse endereço poderia
ser zero, o que simplifica a instalação de um vetor de interrupções. No caso do
endereço de carga não ser o zero, será necessário ao programa copiar as
primeiras posições de memória (vetor de interrupções) do endereço inicial para
o endereço físico zero.
Uma vez carregado na memória, a CPU salta para o primeiro endereço
definido para o kernel (0x8000, no nosso exemplo) e passará a executar a nossa
aplicação.

15 É muito provável que as versões mais recentes do bootloader já saibam interpretar o formato
ELF diretamente. Mas vamos manter a tradição e, como bônus, aprender a usar o objcpy.
85
Laboratório de Processadores
Bruno Basseto / 2023

Interfaces de depuração
Vamos utilizar as interfaces J-TAG e UART para interagir com as
aplicações ARM que criarmos. Alguns dos pinos do conector de expansão são
usados para interligar a placa Raspberry Pi ao computador hospedeiro através
dessas interfaces, conforme a figura a seguir.
conexões:
JTAG e UART
1 2

6 gnd
8 rx UART (3.3V)
10 tx
TMS 13
TRST 15 16 RTCK
18 TDO
20 GND J-TAG
22 TCK

TDI 37
39 40

• Interface J-TAG
TMS (marrom) Pino 13 GPIO 27
TCK (laranja) Pino 22 GPIO 25
TDI (amarelo
amarelo) Pino 37 GPIO 26
TDO (verde) Pino 18 GPIO 24
TRST (Cinza) Pino 15 GPIO 22
RTCK (roxo) Pino 16 GPIO 23*
GND (preto) Pino 20
• Interface UART
TX (verde) Pino 10 GPIO 15
RX (branco
branco) Pino 8 GPIO 14
GND (preto) Pino 6
Observe que todos os sinais presentes no conector de expansão são de
nível elétrico 3.3 V e a placa pode ser danificada caso tensões mais altas
apareçam nos pinos.
Para acessar a interface J-TAG vamos empregar o software OpenOCD, com
um arquivo de configuração específico para a versão correta do Raspberry Pi e da
interface (ou “transporte”) a utilizar (placa Segger J-Link). Para acessar a
interface UART usaremos um conversor USB/Serial e qualquer software de
emulação de terminal (por exemplo, screen).

86
Laboratório de Processadores
Bruno Basseto / 2023

Makefile exemplo
FONTES = arquivo.c arquivo.s

# Arquivos de saída
EXEC = kernel.elf
MAP = kernel.map
IMAGE = kernel.img
LIST = kernel.list

PREFIXO = arm-none-eabi-
LDSCRIPT = kernel.ld
AS = ${PREFIXO}as
LD = ${PREFIXO}ld
GCC = ${PREFIXO}gcc
OBJCPY = ${PREFIXO}objcopy
OBJDMP = ${PREFIXO}objdump
OPTS = -march=armv7-a -mtune=cortex-a7 -g
OBJ = $(FONTES:.s=.o)
OBJETOS = $(OBJ:.c=.o)

all: ${EXEC} ${IMAGE} ${LIST}

rebuild: clean all

# Gerar executável
${EXEC}: ${OBJETOS}
${LD} -T ${LDSCRIPT} -M=${MAP} -o $@ ${OBJETOS}

# Gerar imagem
${IMAGE}: ${EXEC}
${OBJCPY} ${EXEC} -O binary ${IMAGE}

# Gerar listagem
${LIST}: ${EXEC}
${OBJDMP} -d ${EXEC} > ${LIST}

# Compilar arquivos em C
.c.o:
${GCC} ${OPTS} -c -o $@ $<

# Montar arquivos em assembler


.s.o:
${AS} -g -o $@ $<
# Limpar tudo
clean:
rm -f *.o ${EXEC} ${MAP} ${LIST} ${IMAGE}

# Iniciar openocd
ocd:
@if pgrep openocd >/dev/null ; then\
echo "openocd já está executando"; \
else openocd -f rpi2.cfg & \
fi

# Iniciar gdb
gdb: ${EXEC}
gdb-multiarch -ex "set architecture arm" \
-ex "target extended-remote :3333" \
-ex "load" \
${EXEC}
stop:
-pkill openocd
-pkill gdb-multiarch

A interface de depuração com o gdb suporta conexões com os quatro


núcleos independentemente. O primeiro núcleo responde na porta “3333”, o
segundo na porta “3334” e assim por diante.

87
Laboratório de Processadores
Bruno Basseto / 2023

Arquitetura do SoC BCM3826


O componente principal do Raspberry Pi modelo 2 é o circuito integrado
BCM3826 da Broadcom, que inclui quatro núcleos ARM Cortex-A7 e uma GPU
VideoCore:
Conector Cartão Hub interno
Expansão SD Ethernet

GPIO SDIO USB I2C0 I2C1 PLL


I2C

Cortex-A7 Cortex-A7 Cortex-A7 Cortex-A7 Videocore IV

L1D L1I L1D L1I L1D L1I L1D L1I OpenGL-ES


cache cache cache cache cache cache cache cache
1080p Codec
L2
cache
H264/Jpeg

AUX Audio Interface Camera Interface Video Interface


UART SPI0 SPI1 I2S PWM CSI LCD PAL HDMI

Conector Conector
Câmera HDMI

Inicialmente o sistema de controle de clock é configurado para produzir


um relógio de 250 MHz para a GPU, que é comum às quatro CPUs e à GPU. As
CPUs podem reconfigurar o seu clock em até 900 MHz. A memória total do
sistema (1 GiB) é dividida entre os núcleos ARM e a GPU, porém todos os
processadores podem acessar a memória de forma compartilhada. Além disso,
existem “mailboxes” para a comunicação entre os processadores, sem a
necessidade de usar a memória RAM.
Quando o sinal de reset é liberado, todos os quatro núcleos ARM passam a
executar simultaneamente. Uma vez que cada processador deverá executar o
seu próprio thread, é necessário que o software identifique cada um deles e
modifique seu contexto apropriadamente; como os núcleos compartilham a
memória, também é importante que cada um possua sua(s) própria(s) pilha(s). Na
arquitetura v.7 existem instruções especiais para acesso atômico a posições de
memória, que devem ser utilizadas para criar semáforos e spin-locks para
possibilitar a sincronização entre os núcleos, nos casos de concorrência.
A identificação do núcleo em execução pode ser obtida do coprocessador
15, em um registrador chamado MPIDR: seus dois bits menos significativos
contém uma identificação (ou “afinidade”) diferente para cada núcleo que ler
esse registrador.

88
Laboratório de Processadores
Bruno Basseto / 2023

reset:
mrc p15, 0, r0, c0, c0, 5 // registrador MPIDR
ands r0, r0, #0x03 // 2 bits menos significativos
beq core0
cmp r0, #1
beq core1
cmp r0, #2
beq core2
b core3

core0: // núcleo zero executa aqui


ldr sp, =stack_core0
// etc...
core1: // núcleo 1 executa aqui
// etc...
// etc...
core2: // núcleo 2 executa aqui
// etc...
// etc...
core3: // núcleo 3 executa aqui
// etc...
// etc...

89
Laboratório de Processadores
Bruno Basseto / 2023

Periféricos
Os periféricos do SoC são configurados e controlados por “registradores” registradores dos
periféricos
que são mapeados na memória do sistema. O endereço inicial na memória física a
partir do qual os registradores de configuração dos periféricos são mapeados é
denominado PERIPH_BASE e seu valor depende do modelo:
◦ PERIPH_BASE é 0x3f000000 no Raspberry Pi modelos 2 e 3.

◦ PERIPH_BASE é 0x20000000 no Raspberry Pi modelos 0 e 1.

GPIOs
O SoC utilizado no Raspberry Pi possui um módulo de controle de GPIO
com as seguintes características:
• 54 pinos configuráveis como entrada ou como saída e eventualmente
compartilhados com sinais de controle de outros periféricos (UART, SPI,
etc.);
• Qualquer dos pinos pode ser configurado individualmente com relação à
presença ou ausência de resistores de pull-up ou pull-down internos;
• O módulo GPIO permite identificar eventos em qualquer das entradas
(bordas de subida ou descida e níveis elétricos) e produzir interrupções
no processador.
Os registradores de controle dos GPIOs começam no endereço
PERIPH_BASE + 0x200000 e são descritos a seguir.

#include <stdint.h>
#define PERIPH_BASE 0x3f000000 // no RPi 2
#define GPIO_ADDR (PERIPH_BASE + 0x200000)

typedef struct {
uint32_t gpfsel[6]; // Function select (3 bits/gpio)
unsigned : 32;
uint32_t gpset[2]; // Output set (1 bit/gpio)
unsigned : 32;
uint32_t gpclr[2]; // Output clear (1 bit/gpio)
unsigned : 32;
uint32_t gplev[2]; // Input read (1 bit/gpio)
unsigned : 32;
uint32_t gpeds[2]; // Event detect status
unsigned : 32;
uint32_t gpren[2]; // Rising-edge detect enable
unsigned : 32;
uint32_t gpfen[2]; // Falling-edge detect enable
unsigned : 32;
uint32_t gphen[2]; // High level detect enable
unsigned : 32;
uint32_t gplen[2]; // Low level detect enable
unsigned : 32;
uint32_t gparen[2]; // Async rising-edge detect
unsigned : 32;
uint32_t gpafen[2]; // Async falling-edge detect
unsigned : 32;
uint32_t gppud; // Pull-up/down enable
uint32_t gppudclk[2]; // Pull-up/down clock enable
} gpio_reg_t;

#define GPIO_REG(X) ((gpio_reg_t*)(GPIO_ADDR))->X

90
Laboratório de Processadores
Bruno Basseto / 2023

A função de cada GPIO é configurada nos registradores gpfsel,


envolvendo três bits para cada pino:

Valor Configuração função dos


GPIO
000 Entrada
001 Saída
010 ALT5
011 ALT4
100 ALT0
101 ALT1
110 ALT2
111 ALT3

Registrador GPIOs
gpfsel[0] 0a9
gpfsel[1] 10 a 19
gpfsel[2] 20 a 29
gpfsel[3] 30 a 39
gpfsel[4] 40 a 49
gpfsel[5] 50 a 53

Por exemplo, o código a seguir configura o GPIO 17 como entrada e o


GPIO 18 como saída:

// zera os bits 21, 22 e 23 de gpfsel[1] (entrada)


GPIO_REG(gpfsel[1]) = GPIO_REG(gpfsel[1]) & (~(0x07<<21));

// zera os bits 25 e 26 e seta o bit 24 de gpfsel[1] (saída)


GPIO_REG(gpfsel[1]) = (GPIO_REG(gpfsel[1]) & (~(0x07<<24)))|(0x01<<24);

A tabela na página seguinte ilustra as funções alternativas (“ALT0” até


“ALT5”) que podem ser assumidas por cada um dos GPIOs, distribuídos pelos
periféricos:
• UART (sinais TXDn, RXDn, CTSn e RTSn);
• SPI (sinais CSn, MISOn, MOSIn e SCKn);
• I2C (sinais SDAn e SCLn);
• Saídas de clock (sinais GPCLKn);
• Saídas PWM (sinais PWMn);
• Interface de áudio PCM (sinais PCMCK, PCMFS, PCMIN e PCMOUT);
• Interface EMMC (sinais SDCLK, SDCMD e SDDATn);
• Interface display paralelo (sinais PCLK, DE, VSYNC, HSYNC e Dn);
• Interface com memória externa (sinais SAn, SOE, SRW, SDn);
• Interface J-TAG (sinais TRST, RTCK, TDO, TDI, TCK e TMS).

91
Laboratório de Processadores
Bruno Basseto / 2023
GPIO Pino ALT0 ALT1 ALT2 ALT3 ALT4 ALT5 funções
*
0 27+ SDA0 SA5 PCLK alternativas
*
1 28+ SCL0 SA4 DE
*
2 3 SDA1 SA3 VSYNC
*
3 5 SCL1 SA2 HSYNC
*
4 7 GPCLK0 SA1 D0 TDI
*
5 29 GPCLK1 SA0 D1 TDO
*
6 31 GPCLK2 SOE/SE D2 RTCK
7 *
26 CS0(1) SWE/SRW D3
*
8 24 CS0(0) SD0 D4

9 21 MISO0 SD1 D5

10 19 MOSI0 SD2 D6

11 23 SCK0 SD3 D7

12 32 PWM0 SD4 D8 TMS

13 33 PWM1 SD5 D9 TCK

14 8 TXD0 SD6 D10 TXD1

15 10 RXD0 SD7 D11 RXD1

16 36 SD8 D12 CTS0 CS1(2) CTS1

17 11 SD9 D13 RTS0 CS1(1) RTS1

18 12 PCMCK SD10 D14 BSDA CS1(0) PWM0

19 35 PCMFS SD11 D15 BSCL MISO1 PWM1

20 38 PCMIN SD12 D16 BMISO MOSI1 GPCLK0

21 40 PCMOUT SD13 D17 BCE SCK1 GPCLK1

22 15 SD14 D18 SDCLK TRST

23 16 SD15 D19 SDCMD RTCK

24 18 SD16 D20 SDDAT0 TDO

25 22 SD17 D21 SDDAT1 TCK

26 37 D22 SDDAT2 TDI

27 13 D23 SDDAT3 TMS
28 SDA0 SA5 PCMCK
29 SCL0 SA4 PCMFS

30 SA3 PCMIN CTS0 CTS1

31 LAN SA2 PCMOUT RTS0 RTS1

32 (cam-12) GPCLK0 SA1 TXD0 TXD1

33 SA0 RXD0 RXD1
*
34 GPCLK0 SOE/SE SDCLK
35 *
(power)
CS0(1) SWE/SRW SDCMD
*
36 CS0(0) SD0 TXD0 SDDAT0

37 MISO0 SD1 RXD0 SDDAT1

38 USB MOSI0 SD2 RTS0 SDDAT2

39 SCK0 SD3 CTS0 SDDAT3

40 (pwm) PWM0 SD4 MISO2 TXD1

41 (cam-11) PWM1 SD5 MOSI2 RXD1

42 SMPS GPCLK1 SD6 SCK2 RTS1

43 SMPS GPCLK2 SD7 CE2(0) CTS1
44 ETH GPCLK1 SDA0 SDA1 CE2(1)
45 (pwm) PWM1 SCL0 SCL1 CE2(2)
*
46 (hdmi-19)
*
47 (led)
*
48 (sd) SDCLK
*
49 (sd) SDCMD
*
50 (sd) SDDAT0
*
51 (sd) SDDAT1
*
52 (sd) SDDAT2
*
53 (sd) SDDAT3
* = pull-up após reset, † = pull-down após reset

92
Laboratório de Processadores
Bruno Basseto / 2023

Para controlar o valor dos GPIOs configurados como saída utilizam-se os gpset
registradores gpset (para ligar uma ou mais saídas) e gpclr (para desligar uma gpclr

ou mais saídas); esses registradores associam cada bit a uma saída digital (em
gpset[0] os GPIOs de 0 a 31 e em gpset[1] os GPIOs de 32 a 53). Usar dois
grupos diferentes de registradores para ligar e para desligar é útil para controlar
um grupo de GPIOs sem interferir com os demais: ao escrever algum bit “1” em
gpset ou gpclr, somente as saídas correspondentes serão afetadas, nada
ocorrendo para os GPIOs cujos bits foram escritos com “0”.

// liga a saída 18 e desliga a saída 19


GPIO_REG(gpset[0]) = 0x01 << 18;
GPIO_REG(gpclr[0]) = 0x01 << 19;

// lê o estado do GPIO 17
int gpio17 = GPIO_REG(gplev[0]) & (0x01 << 17);

Os registradores gplev retornam os valores atuais dos GPIOs (tanto gplev


entradas quanto saídas) em seus bits correspondentes.
Transições e estados específicos de qualquer GPIO podem ser detectados detecção de
pelo SoC e eventualmente produzir uma interrupção. Os registradores gpeds eventos
marcam eventos que foram detectados anteriormente pelos GPIOs, conforme
configurados nos registradores gpren (detectar borda de subida), gpfen
(detectar borda de descida), gphen (detectar nível alto), gplen (detectar nível
baixo), gparen (borda de subida, assíncrono) e gpafen (borda de descida,
assíncrono). A sinalização de eventos pode ser reconhecida nos registradores
gpeds mediante a escrita do valor lógico “1” nos bits correspondentes; isso é
obrigatório, no caso de eventos configurados para produzir interrupções no
processador.
A configuração dos resistores de pull-up ou pull-down é realizada usando os pull-up/
registradores gppud e gppudclk em conjunto. O primeiro registrador deve ser pull-down
escrito com a configuração desejada: “0” (para desabilitar resistores), “1” (para
habilitar resistores de pull-down) ou “2” (para habilitar resistores de pull-up);
após isso, é necessário dar um “pulso de clock” nos flip-flops que controlam os
resistores, ligando e desligando bits nos registradores gppudclk, conforme os
GPIOs desejados.
Em todos os casos, cada bit dos registradores de índice “0” refere-se a um
dos GPIOs de índice “0” a “31” e cada bit dos registradores de índice “1” refere-se
a um dos GPIOs de índice “32” a “53”. Os bits 24-31 dos registradores de índice
“1” não são utilizados.
As interrupções de índices “49”, “50”, “51” e “52” (ou “17”, “18”, “19” e interrupções
“20” do grupo “2”) podem ser habilitadas para ocorrerem conforme eventos
sejam identificados pelo controlador de GPIO. A interrupção “49” ocorre para
eventos no primeiro banco de GPIOs (de 0 a 31); as ambas as interrupções “50” e
“51” ocorrem para eventos no segundo banco de GPIOs (de 32 a 53). Pode-se usar
também a interrupção “52”, que ocorre para eventos de qualquer banco de
GPIOs.
93
Laboratório de Processadores
Bruno Basseto / 2023

O exemplo a seguir configura o GPIO 12 para gerar uma interrupção na


borda de subida do sinal:

void __attribute__((interrupt("IRQ")))
trata_irq(void) {
// Interrupções comuns ("basic")
int basic = IRQ_REG(pending_basic);
if(bit_is_set(basic, 9)) {
// Interrupções do grupo 2
int pend = IRQ_REG(pending_2);
if(bit_is_set(pend, 20)) {
// IRQ 52 = interrupção GPIO
uint32_t ev = GPIO_REG(gpeds[0]);
if(bit_is_set(ev, 12)) trata_evento_gpio_12();
GPIO_REG(gpeds) = ev; // reconhece
}
}
}

void configura_gpio_12(void) {
// GPIO12 = entrada
GPIO_REG(gpfsel[1]) = GPIO_REG(gpfsel[1]) & (~(0x7 << 6));
GPIO_REG(gpren[0]) |= (1 << 12); // detectar borda de subida
IRQ_REG(enable_2) |= (1 << 20); // habilita int. GPIO (52)
}

Os GPIOs de “0” a “27” estão disponíveis para uso através do conector de


expansão. A figura a seguir mostra os pinos e suas posições no conector.

94
Laboratório de Processadores
Bruno Basseto / 2023

Interrupções
O processador do Raspberry Pi é um ARM de arquitetura v.7 (ou v.8, no
caso dos modelos 3 e 4), cujo funcionamento com relação às interrupções é o
mesmo estudado até agora.
O vetor de interrupções sempre é situado a partir do endereço físico
zero. Em cada posição do vetor deve existir uma única instrução de salto
( b <rótulo> ou ldr pc, ... ). Como os serviços de interrupção podem
estar em qualquer posição da memória, normalmente ao vetor de interrupção se
segue uma tabela que contém os endereços absolutos dos serviços, para serem
lidos pelas instruções do vetor de interrupções com endereçamento indireto (por
exemplo, ldr pc, [pc, #24] ).

O arquivo de imagem é carregado pelo bootloader do Raspberry Pi no transposição


endereço 0x8000, portanto, as posições de memória correspondentes ao vetor de do vetor de
interrupções
interrupção e à tabela de endereços dos serviços devem ser copiadas para o
endereço físico zero para que as interrupções funcionem.

.section .init
.global start
start:
// Vetor de interrupções
// (no endereço 0x8000: deve ser copiado)
ldr pc, reset_addr
ldr pc, undef_addr
ldr pc, swi_addr
ldr pc, inst_abort_addr
ldr pc, data_abort_addr
nop
ldr pc, irq_addr
ldr pc, fiq_addr

reset_addr: .word reset


undef_addr: .word undef_service
swi_addr: .word swi_service
inst_abort_addr: .word inst_abort_service
data_abort_addr: .word data_abort_service
irq_addr: .word irq_service
fiq_addr: .word fiq_service

// Instrução inicial após reset


reset:
// Copia o vetor de interrupções
// do endereço 0x8000 para o endereço 0
mov r0, #0x8000
mov r1, #0x0000
ldmia r0!, {r2,r3,r4,r5,r6,r7,r8,r9}
stmia r1!, {r2,r3,r4,r5,r6,r7,r8,r9}
ldmia r0!, {r2,r3,r4,r5,r6,r7,r8,r9}
stmia r1!, {r2,r3,r4,r5,r6,r7,r8,r9}

Deve-se observar que as exceções ou eventos síncronos são tratados interrupções


localmente, pelo próprio núcleo que produziu a exceção (“undef”, “swi”, e os dois em sistemas
multi-core
tipos de “abort”); já as interrupções (tanto “normais” quanto “rápidas”) são
geralmente encaminhadas para apenas um dos núcleos no caso dos modelos 2 e

95
Laboratório de Processadores
Bruno Basseto / 2023

3 do Raspberry Pi16. As únicas situações em que isso não ocorre são as


interrupções específicas de cada núcleo, que são causadas por outro núcleo:
servindo à comunicação entre os dois (interrupções “doorbell” e “mailbox”). O
núcleo configurado para tratar as demais interrupções pode ser especificado em
um registrador apropriado17.
As interrupções produzidas pelo hardware são tratadas pelo mesmo
serviço de interrupção (“IRQ”), sendo necessário à função de tratamento
identificar o(s) periférico(s) causador(es) da interrupção, realizar o seu
tratamento e finalmente reconhecer a(s) interrupção(ões) junto ao(s)
respectivo(s) periférico(s). Uma das interrupções pode ser definida como rápida,
passando a ser tratada de forma exclusiva pelo serviço de interrupção “FIQ”,
separadamente das demais interrupções.
No SoC do Raspberry Pi há um periférico específico para o controle das
interrupções, que é configurado nos dez registradores a partir do endereço
PERIPH_BASE + 0xb200:
registradores
#define IRQ_ADDR (PERIPH_BASE + 0x00B200) de controle
typedef struct { de interrupção
uint32_t pending_basic; // leitura: “1”=interrupção pendente
uint32_t pending_1;
uint32_t pending_2;
uint32_t fiq; // habilita FIQ e define a interrupção
uint32_t enable_1; // escrita: “1”=habilita interrupção
uint32_t enable_2;
uint32_t enable_basic;
uint32_t disable_1; // escrita: “1”=desabilita interrupção
uint32_t disable_2;
uint32_t disable_basic;
} irq_reg_t;
#define IRQ_REG(X) ((irq_reg_t*)(IRQ_ADDR))->X

Os registradores que contém informações sobre as interrupções são


agrupados de três em três: “basic”, “1” e “2”, em um total de 96 bits. Os
registradores “basic” tratam das interrupções consideradas “mais comuns” e as
demais interrupções são distribuídas nos demais registradores. As interrupções
podem ser habilitadas e desabilitadas individualmente nos registradores com os
prefixos “enable_” e “disable_”, respectivamente; escrever o valor “1” em um
bit de um registrador “enable_” (ou “disable_”) habilita (ou desabilita) a
interrupção correspondente, não interferindo com as interrupções cujos bits
foram escritos com o valor “0”.
Uma das interrupções do sistema pode ser definida como rápida,
bastando para isso escrever o número correspondente ao seu índice no
registrador fiq.
Os registradores com o prefixo “pending_” marcam em seus respectivos
bits a ocorrência de cada uma das interrupções possíveis. Dois bits são reservados
no registrador pending_basic para sinalizar a ocorrência de alguma
16 Após o reset, o controlador de interrupções assume o núcleo zero como destino das
interrupções.
17 O modelo Raspberry Pi 4 utiliza o periférico “GIC”, que é o padrão da arquitetura ARM para
fazer esse controle. Ele permite configurações muito mais complexas para o roteamento das
interrupções entre os núcleos.
96
Laboratório de Processadores
Bruno Basseto / 2023

interrupção do grupo “1” (bit “8”) ou do grupo “2” (bit “9”), dispensando assim a
verificação dos registradores pending_1 e pending_2, caso esses bits sejam
iguais a “zero”.
As tabelas a seguir descrevem as todas as interrupções existentes no
Raspberry Pi e os seus respectivos índices:
• Relação das interrupções do grupo “1” (índices “0” até “31”) nos
registradores com sufixo “_1”:
Índice Bit IRQ Índice Bit IRQ primeiro grupo
de interrupções
0 0 system timer (0) 16 16 DMA (0)
1 1 system timer (1) 17 17 DMA (1)
2 2 system timer (2) 18 18 DMA (2)
3 3 system timer (3) 19 19 DMA (3)
4 4 Codec (0) 20 20 DMA (4)
5 5 Codec (1) 21 21 DMA (5)
6 6 Codec (2) 22 22 DMA (6)
7 7 JPEG 23 23 DMA (7)
8 8 ISP 24 24 DMA (8)
9 9 USB 25 25 DMA (9)
10 10 3D 26 26 DMA (10)
11 11 Transposer 27 27 DMA (11-14)
12 12 MC sync (0) 28 28 DMA (comum)
13 13 MC sync (1) 29 29 Aux (mini-uart, spis)
14 14 MC sync (2) 30 30 ARM ?
15 15 MC sync (3) 31 31 VPUDMA

• Relação das interrupções do grupo “2” (índices 32 até 63) nos


registradores com sufixo “_2”:
Índice Bit IRQ Índice Bit IRQ segundo grupo
de interrupções
32 0 Hostport 48 16 smi
33 1 Video scaller 49 17 gpio (0) [0-31]
34 2 CCP2TX 50 18 gpio (1) [32-53]
35 3 SDC 51 19 gpio (2) [32-53]
36 4 DSI (0) 52 20 gpio (3) [0-53]
37 5 AVE 53 21 i2c
38 6 CAM (0) 54 22 spi
39 7 CAM (1) 55 23 i2s/pcm
40 8 HDMI (0) 56 24 sdio
41 9 HDMI (1) 57 25 uart
42 10 PixelValve (1) 58 26 slimbus
43 11 i2c/spi slave 59 27 VEC
44 12 DSI (1) 60 28 CPG
45 13 pwa (0) 61 29 RNG
46 14 pwa (1) 62 30 Arasansdio ?
47 15 CPR 63 31 avspmon?

97
Laboratório de Processadores
Bruno Basseto / 2023

• Relação das interrupções básicas


Bit IRQ Bit IRQ
0 Core Timer 16 GPU irq 54 (spi) interrupções
básicas
1 Mailbox 17 GPU irq 55 (pcm)
2 Doorbell 0 18 GPU irq 56 (sdio)
3 Doorbell 1 19 GPU irq 57 (uart)
4 GPU0 halted 20 GPU irq 62 (?)
5 GPU1 halted 21
6 Illegal access (1) 22
7 Illegal access (0) 23
8 Interrupção do grupo 1 24
9 Interrupção do grupo 2 25
10 GPU irq 7 (jpeg) 26
11 GPU irq 9 (usb) 27
12 GPU irq 10 28
13 GPU irq 18 (dma-2) 29
14 GPU irq 19 (dma-3) 30
15 GPU irq 53 (i2c) 31
◦ Os primeiros oito bits correspondem às interrupções geradas
internamente aos núcleos ARM (timer do núcleo e comunicação entre núcleos).
Todas as demais interrupções são provenientes de periféricos externos ao
núcleo18;
◦ O estado dos bits 8 e 9 permite verificar se qualquer interrupção dos
grupos “1” e “2”, respectivamente, estão pendentes.

// Serviço de interrupção
void __attribute__((interrupt("IRQ")))
irq_service(void) {
if(IRQ_REG(pending_basic) & (1 << 0)) {
// trata interrupção do timer ARM
}
if(IRQ_REG(pending_basic) & (1 << 9)) {
// alguma interrupção do grupo 2 está pendente
if(IRQ_REG(pending_2) & ...) {
// etc...
}
}
}

// ...
IRQ_REG(enable_basic) = (1 << 0); // habilita int. do timer
IRQ_REG(disable_basic) = (1 << 0); // desabilita int. do timer

18 A documentação da Broadcom chama essas interrupções (externas) de “gpu”, sem maiores


explicações. E, sim, essa nomenclatura não parece fazer nenhum sentido.
98
Laboratório de Processadores
Bruno Basseto / 2023

Periférico “auxiliar” e a Mini-UART


O chamado “periférico auxiliar” ou “aux” implementa três interfaces de
comunicação, uma assíncrona (a “mini UART”) e duas síncronas (os canais SPI). A
“mini UART” é uma implementação simplificada do tradicional chip 16550, que
vamos utilizar para comunicar com o computador de desenvolvimento.

// Definição dos registradores


typedef struct {
uint32_t irq;
uint32_t enables;
} aux_reg_t;

typedef struct {
uint32_t io;
uint32_t ier;
uint32_t iir;
uint32_t lcr;
uint32_t mcr;
uint32_t lsr;
uint32_t msr;
uint32_t scratch;
uint32_t cntl;
uint32_t stat;
uint32_t baud;
} mu_reg_t;

#define PERIPH_BASE 0x3f000000


#define AUX_ADDR (PERIPH_BASE + 0x215000)
#define AUX_MU_ADDR (PERIPH_BASE + 0x215040)

#define AUX_REG(X) ((aux_reg_t*)(AUX_ADDR))->X


#define MU_REG(X) ((mu_reg_t*)(AUX_MU_ADDR))->X

Os registradores são razoavelmente compatíveis com suas versões de


mesmo nome no 16550 (sempre os oito bits menos significativos):
• O registrador io pode ser lido (dados recebidos) ou escrito (dado a io
enviar), acessando o buffer de recepção ou de transmissão, conforme o
caso. Cada um dos buffers pode armazenar até oito bytes;
• O registrador ier (interrupt enable register) serve para habilitar ou ier
desabilitar as interrupções de transmissão (bit “1”) e de recepção (bit
“0”). A interrupção de recepção acontece enquanto existir pelo menos
um byte no buffer de recepção; a interrupção de transmissão ocorre
sempre que houver pelo menos uma posição vaga no buffer de
transmissão;
• O registrador iir (interrupt identification register) permite identificar a iir
interrupção pendente através dos bits 1-2: “00” = não há interrupções
pendentes; “01” = interrupção de transmissão pendente; “10” =
interrupção de recepção pendente. A combinação “11” é inválida;
• O registrador lcr (line control register) é usado para a configuração do lcr
canal serial, em termos de número de bits, paridade e número de stop bits;
• O registrador stat (que não existe no 16550) permite consultar stat
resumidamente o estado atual da interface serial:
◦ bit “0” – há dados para serem lidos no buffer de recepção;
99
Laboratório de Processadores
Bruno Basseto / 2023

◦ bit “1” – há espaço no buffer de transmissão;


◦ bit “2” – o receptor está ocioso;
◦ bit “3” – o transmissor está ocioso, etc.
• O registrador baud deve ser escrito com o valor do divisor para a baud
produção do baudrate desejado, que é calculado pela expressão:

◦ O valor de systemclock normalmente é 250000000 (250 MHz).

void mini_uart_init(void) {
// configura GPIO14 e GPIO15 como função ALT5 (mini UART)
uint32_t sel = GPIO_REG(gpfsel[1]);
sel = (sel & (~(7<<12))) | (2<<12);
sel = (sel & (~(7<<15))) | (2<<15);
GPIO_REG(gpfsel[1]) = sel;

// habilita pull-ups em GPIO14 e GPIO15


GPIO_REG(gppud) = 0;
delay(150);
GPIO_REG(gppudclk[0]) = (1 << 14) | (1 << 15);
delay(150);
GPIO_REG(gppudclk[0]) = 0;

AUX_REG(enables) = 1;
MU_REG(cntl) = 0;
MU_REG(ier) = 0;
MU_REG(lcr) = 3; // 8 bits
MU_REG(mcr) = 0;
MU_REG(baud) = 270; // para 115200 bps em 250 MHz
MU_REG(cntl) = 3; // habilita TX e RX
}

void mini_uart_putc(uint8_t c) {
while((MU_REG(stat) & 0x02) == 0) ; // não há espaço
MU_REG(io) = c;
}

uint8_t mini_uart_getc(void) {
while((MU_REG(stat) & 0x01) == 0) ; // não há dados a ler
return MU_REG(io);
}

As interrupções da Mini UART são compartilhadas com as demais interrupções


da mini UART
interrupções do periférico “aux” (interrupção “29” do grupo “1” de
interrupções). O estado da interrupção pode ser lido no registrador AUX_IRQ: bit
“0” ligado = interrupção da mini UART:

// no serviço de interrupção...
int pend = IRQ_REG(pending_1);
if(bit_is_set(pend, 29)) {
// Interrupções do periférico AUX.
int irq_aux = AUX_REG(irq);
if(bit_is_set(irq_aux, 0)) uart_irq(); // mini uart
// outras interrupções (SPI?)...
}

100
Laboratório de Processadores
Bruno Basseto / 2023

Core timer

Cada núcleo do SoC BCM383x possui um timer independente, capaz de


gerar uma interrupção periódica, para a contabilização de tempo ou para
virtualização de CPU baseada em tempo compartilhado. Esse timer é um contador
“free running”, que deve receber uma frequência fixa de 1 MHz; a cada ciclo de
relógio (1 µs), o valor do contador é decrementado e uma interrupção é gerada
quando esse valor chegar a zero. A cada interrupção, o contador é recarregado
com um valor de “reload”, que determina portanto o intervalo de tempo entre
duas interrupções consecutivas. Além do core timer, existe outro periférico,
externo ao núcleo, chamado system timer, que pode ser utilizado para finalidades
semelhantes.
O core timer é configurado em nove registradores, residentes na memória
a partir do endereço PERIPH_BASE + 0xb400, conforme a descrição a seguir.

#define TIMER_ADDR (PERIPH_BASE + 0x00B400)


typedef struct {
uint32_t load; // valor de recarga, decrementado até zero
uint32_t value; // valor atual, entre ‘load’ e zero
uint32_t control; // configuração do timer
uint32_t ack; // reconhecimento da interrupção
uint32_t raw_irq; // indica interrupção pendente
uint32_t masked_irq; // indica interrupção ativa
uint32_t reload; // valor de recarga, não imediato
uint32_t pre; // prescaler para 1MHz (126)
uint32_t counter; // valor atual do contador free-running
} timer_reg_t;
#define TIMER_REG(X) ((timer_reg_t*)(TIMER_ADDR))->X

• O valor atual do timer pode ser lido ou escrito no registrador counter; o counter
valor de recarga pode ser configurado nos registradores load ou reload. load
reload
Uma alteração no registrador reload somente tem efeito após a próxima
interrupção do timer;
• O registrador pre deve conter uma constante para o divisor de pre
frequências, de modo a manter a frequência de clock para o core timer em 1
MHz. Como a frequência padrão do sistema no Raspberry Pi é de 250 MHz
e a frequência do barramento APB é 125 MHz, devemos gravar o valor
“126” nesse registrador; control
• control é o registrador empregado para ativar o timer e sua interrupção; ack

• o registrador ack deve ser escrito pelo serviço de tratamento da


interrupção para reconhecer o seu tratamento.

101
Laboratório de Processadores
Bruno Basseto / 2023

Exemplo de código para o core timer:

void timer_init(void) {
TIMER_REG(load) = 1000; // 1MHz / 1000 = 1kHz
TIMER_REG(pre) = 126;
TIMER_REG(control) = (1 << 9) // free-running counter
| (1 << 7) // habilita timer
| (1 << 5) // habilita interrupção
| (1 << 1); // timer de 23 bits

// habilita a interrupção “básica” 0 (core timer)


IRQ_REG(enable_basic) = (1 << 0);
}

volatile uint64_t ticks;

void __attribute__((interrupt("IRQ")))
interrupt_vector(void) {
if(IRQ_REG(pending_basic) & (1 << 0)) {
// Interrupção do timer ARM a cada 1 ms
TIMER_REG(ack) = 1; // reconhece interrupção
ticks++;
}
// outras interrupções...
}

102

Você também pode gostar