Este artículo detalla la configuración y el manejo de interrupciones para el dispositivo UART (Universal Asynchronous Receiver-Transmitter) en un entorno de sistema embebido, enfocándose en la arquitectura RISC-V.
Configuración de Interrupciones
1. Inicialización del Sistema (start.c)
La rutina de arranque inicializa el manejo de interrupciones. Por defecto, las interrupciones se manejan en el modo de máquina. Sin embargo, para permitir que el sistema operativo (en modo supervisor) las gestione, se configuran los registros medeleg y mideleg para delegar las interrupciones a este modo. Además, se habilita el bit SIE (Supervisor Interrupt Enable) en el registro sie para permitir la recepción de interrupciones externas (SIE_SEIE), de software (SIE_SSIE) y de temporizador (SIE_STIE).
// Mover el manejo de traps al modo Supervisor
w_medeleg(0xffff);
w_mideleg(0xffff);
// Habilitar interrupciones externas, de software y de temporizador
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
// Inicializar el temporizador
timerinit();
2. Punto de Entrada Principle (main.c)
En el proceso principal, si el procesador es el núcleo 0 (cpuid() == 0), se inicializa la consola (consoleinit()), el controlador de interrupciones PLIC (plicinit()) y se configuran las interrupciones de dispositivo específicas para el núcleo (plicinithart()). Otros núcleos también ejecutarán plicinithart() para declarar su interés en las interrupciones de dispositivo.
void main(){
if(cpuid() == 0){
consoleinit();
// ... otras inicializaciones ...
plicinit(); // Configurar el controlador de interrupciones PLIC
plicinithart(); // Solicitar interrupciones de dispositivo al PLIC para este núcleo
// ...
} else {
// ...
plicinithart(); // Solicitar interrupciones de dispositivo al PLIC para este núcleo
}
scheduler(); // Iniciar el planificador
}
3. Inicialización de la Consola (console.c)
La inicialización de la consola configura un semáforo (cons.lock) y llama a uartinit(). Luego, asocia las operaciones de lectura y escritura del sistema de archivos para el dispositivo de consola con las funciones consoleread y consolewrite.
void consoleinit(void)
{
initlock(&cons.lock, "cons");
uartinit(); // Inicializar el hardware UART
// Conectar llamadas al sistema de lectura/escritura a las funciones de consola
devsw[CONSOLE].read = consoleread;
devsw[CONSOLE].write = consolewrite;
}
4. Inicialización del UART (uart.c)
Esta función prepara el hardware UART para operar. Configura el divisor de baudios y la longitud de los datos (8 bits). Deshabilita temporalmente las interrupciones, las reconfigura para habilitar las interrupciones de transmisión (IER_TX_ENABLE) y recepción (IER_RX_ENABLE), y luego las vuelve a habilitar. Es importante notar que aunque el UART ahora puede generar interrupciones, el PLIC no ha sido configurado para enrutarlas a la CPU, lo cual se realiza en plicinit().
void uartinit(void)
{
// Deshabilitar interrupciones temporalmente
WriteReg(IER, 0x00);
// Configurar divisor para baud rate
WriteReg(LCR, LCR_BAUD_LATCH);
WriteReg(0, 0x03); // Divisor LSB
WriteReg(1, 0x00); // Divisor MSB
// Configurar longitud de datos a 8 bits
WriteReg(LCR, LCR_EIGHT_BITS);
// Habilitar y limpiar FIFOs
WriteReg(FCR, FCR_FIFO_ENABLE | FCR_FIFO_CLEAR);
// Rehabilidar interrupciones de transmisión y recepción
WriteReg(IER, IER_TX_ENABLE | IER_RX_ENABLE);
initlock(&uart_tx_lock, "uart"); // Inicializar semáforo para la transmisión
}
5. Inicialización del PLIC (plic.c)
El PLIC (Platform-Level Interrupt Controller) se mapea en una dirección de E/S. La función plicinit() configura qué interrupciones de los dispositivos deben ser consideradas por el sistema. Aquí, se habilitan las interrupciones provenientes del UART0 y del disco VirtIO.
void plicinit(void)
{
// Habilitar interrupción UART0 en el PLIC
*(uint32*)(PLIC + UART0_IRQ*4) = 1;
// Habilitar interrupción VirtIO0 en el PLIC (no tratado en este artículo)
*(uint32*)(PLIC + VIRTIO0_IRQ*4) = 1;
}
6. Configuración por Núcleo del PLIC (plic.c)
plicinithart() es llamada por cada núcleo para especificar qué interupciones de dispositivo quiere recibir. Configura el registro PLIC_SENABLE para habilitar las interrupciones del UART0 y VirtIO0 para el núcleo actual (hart). Los niveles de prioridad se establecen a 0, asumiendo que todas las interrupciones tienen la misma importancia en este contexto.
void plicinithart(void)
{
int hart = cpuid();
// Habilitar interrupciones UART0 y VirtIO0 para este núcleo
*(uint32*)PLIC_SENABLE(hart) = (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);
// Ignorar prioridades, establecer a 0
*(uint32*)PLIC_SPRIORITY(hart) = 0;
}
7. Habilitación de Interrupciones a Nivel de Supervisor (riscv.h)
La función intr_on(), llamada dentro del planificador para cada proceso, habilita las interrupciones a nivel de supervisor configurando el bit SIE en el registro sstatus. Una vez que este bit está activo y el PLIC ha sido configurado, la CPU puede recibir y procesar interrupciones pendientes.
static inline void intr_on(){
w_sstatus(r_sstatus() | SSTATUS_SIE);
}
Parte Superior del Controlador UART: Transmisión de Datos
Esta sección describe el flujo de datos desde una aplicación de usuario hasta el hardware UART para su transmisión.
1. Proceso de Inicialización (init.c)
El proceso init crea el archivo especial console si no existe, lo abre y luego duplica sus descriptores de archivo (0, 1, 2) para que stdout y stderr también apunten a la consola. Esto permite que los programas posteriores, como el shell, redirijan su salida a la consola.
int
main(void)
{
if(open("console", O_RDWR) < 0){
// Crear nodo de dispositivo para la consola
mknod("console", CONSOLE, 0);
open("console", O_RDWR);
}
// Duplicar descriptor 0 para stdout (1) y stderr (2)
dup(0); // stdout
dup(0); // stderr
// Iniciar el shell
for(;;){
// ...
if(pid == 0){
exec("sh", argv);
// ...
}
// ...
}
}
2. Programa Shell (sh.c)
El shell, al iniciarse, interactúa con el usuario. La función getcmd escribe el prompt "$ " en el descriptor de archivo 2 (stderr), lo que eventualmente activará la cadena de llamadas del sistema de escritura.
int getcmd(char *buf, int nbuf){
// Llamada al sistema write para mostrar el prompt
write(2, "$ ", 2);
// ... leer comando ...
}
3. Llamada al Sistema sys_write (sysfile.c)
Cuando un programa de usuario llama a write, el kernel intercepta la llamada a través de sys_write. Esta función obtiene el descriptor de archivo y los datos a escribir, y luego llama a filewrite para manejar la operación de escritura en el archivo o dispositivo correspondiente.
uint64 sys_write(void){
struct file *f;
int n;
uint64 p;
argaddr(1, &p); // Obtener puntero a los datos
argint(2, &n); // Obtener tamaño de los datos
if(argfd(0, 0, &f) < 0) // Obtener puntero al descriptor de archivo
return -1;
return filewrite(f, p, n); // Delegar a filewrite
}
4. Escritura de Archivos (file.c)
filewrite determina el tipo de archivo. Si es un dispositivo (FD_DEVICE), llama a la función write registrada para ese dispositivo en la tabla devsw.
int filewrite(struct file *f, uint64 addr, int n) {
int r, ret = 0;
if(f->writable == 0) return -1; // No es escribible
if(f->type == FD_PIPE){
ret = pipewrite(f->pipe, addr, n);
} else if(f->type == FD_DEVICE){
// Si es un dispositivo, llamar a la función de escritura específica
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
return -1;
ret = devsw[f->major].write(1, addr, n); // Llamar a la función de escritura del dispositivo
} else if(f->type == FD_INODE){
// ... manejo para archivos regulares ...
}
return ret;
}
5. Escritura en Consola (console.c)
La función consolewrite actúa como la "parte superior" del controlador UART. Itera sobre los bytes a escribir, los copia desde el espacio de usuario (usando either_copyin) y llama a uartputc para que cada carácter sea procesado para su transmisión.
int consolewrite(int user_src, uint64 src, int n)
{
int i;
for(i = 0; i < n; i++)
{
char c;
// Copiar un carácter desde el espacio de usuario
if(either_copyin(&c, user_src, src+i, 1) == -1)
break;
// Enviar el carácter al controlador UART
uartputc(c);
}
return i;
}
6. Copia de Datos (proc.c)
either_copyin maneja la copia de datos, ya sea desde el espacio de usuario (usando copyin y la tabla de páginas del proceso) o desde el espacio del kernel.
int either_copyin(void *dst, int user_src, uint64 src, uint64 len)
{
struct proc *p = myproc();
if(user_src){
// Copiar desde espacio de usuario
return copyin(p->pagetable, dst, src, len);
} else {
// Copiar desde espacio de kernel
memmove(dst, (char*)src, len);
return 0;
}
}
7. Poner Carácter en Buffer de Transmisión (uart.c)
uartputc es responsable de colocar caracteres en un buffer circular de transmisión (uart_tx_buf). Si el buffer está lleno, el proceso actual se pone a dormir (sleep) hasta que haya espacio disponible. Luego, llama a uartstart() para intentar enviar datos inmediatamente si el registro de transmisión está libre.
#define UART_TX_BUF_SIZE 32
char uart_tx_buf[UART_TX_BUF_SIZE];
uint64 uart_tx_w; // Índice de escritura
uint64 uart_tx_r; // Índice de lectura
void uartputc(int c)
{
acquire(&uart_tx_lock);
if(panicked){ // Si el sistema está en pánico, no hacer nada
for(;;) ;
}
// Esperar si el buffer de transmisión está lleno
while(uart_tx_w == uart_tx_r + UART_TX_BUF_SIZE){
sleep(&uart_tx_r, &uart_tx_lock); // Esperar a que se libere espacio
}
uart_tx_buf[uart_tx_w % UART_TX_BUF_SIZE] = c; // Añadir carácter al buffer
uart_tx_w += 1;
uartstart(); // Intentar iniciar la transmisión
release(&uart_tx_lock);
}
8. Inicio de Transmisión UART (uart.c)
uartstart() vacía el buffer de transmisión colocando caracteres en el registro THR (Transmit Holding Register) del UART, pero solo si el registro está listo para recibir datos (LSR_TX_IDLE). Cada vez que se coloca un carácter en THR, el control puede regresar al espacio de usuario. Si algún proceso estaba esperando espacio en el buffer (uartputc), se le notifica con wakeup.
#define THR 0 // Registro de transmisión
void uartstart()
{
while(1)
{
// Si el buffer está vacío, salir
if(uart_tx_w == uart_tx_r) return;
// Si el registro de transmisión no está inactivo, salir
if((ReadReg(LSR) & LSR_TX_IDLE) == 0) return;
// Tomar carácter del buffer
int c = uart_tx_buf[uart_tx_r % UART_TX_BUF_SIZE];
uart_tx_r += 1;
// Notificar a quien espere por espacio en el buffer
wakeup(&uart_tx_r);
WriteReg(THR, c); // Escribir carácter en el registro de transmisión
}
}
Parte Inferior del Controlador UART: Recepción de Interrupciones
Esta sección explica cómo se maneja una interrupción del UART cuando llega un carácter.
1. Manejo de Interrupciones de Usuario (trap.c)
La rutina usertrap se ejecuta cuando ocurre una interrupción o excepción en el espacio de usuario. Primero, determina si la causa es una interrupción de dispositivo llamando a devintr().
void usertrap(void)
{
// ...
} else if((which_dev = devintr()) != 0){
// La interrupción fue de un dispositivo, manejarla
} else {
// ... manejar otras interrupciones ...
}
// ...
}
2. Interrupciones de Dispositivos (trap.c)
devintr() verifica el registro scause para identificar el tipo de interrupción. Si es una interrupción externa de supervisor (vía PLIC), llama a plic_claim() para obtener el número de IRQ del dispositivo que generó la interrupción. Si el IRQ corresponde al UART0, se llama a uartintr().
int devintr()
{
uint64 scause = r_scause();
// Verificar si es una interrupción externa de supervisor a través del PLIC
if((scause & 0x8000000000000000L) &&
(scause & 0xff) == 9){
// Consultar al PLIC qué interrupción servir
int irq = plic_claim();
// Manejar interrupción del UART0
if(irq == UART0_IRQ){
uartintr();
} else if(irq == VIRTIO0_IRQ){
virtio_disk_intr();
} else if(irq){
printf("interrupción inesperada irq=%d\n", irq);
}
// ...
return 1; // Indica que fue una interrupción de dispositivo
}
// ...
return 0; // No reconocida
}
3. Reclamar Interrupción del PLIC (plic.c)
plic_claim() lee el registro PLIC_SCLAIM para el núcleo actual, lo que le indica al PLIC que la CPU está sirviendo la interrupción y obtiene el número de IRQ correspondiente. El IRQ para UART0 es típicamente 10.
int plic_claim(void)
{
int hart = cpuid();
// Leer el registro de reclamación del PLIC para obtener el IRQ
int irq = *(uint32*)PLIC_SCLAIM(hart);
return irq;
}
4. Manejador de Interrupciones UART (uart.c)
uartintr() se llama cuando el UART genera una interrupción. Lee todos los caracteres disponibles del UART usando uartgetc() y los pasa a consoleintr() para su procesamiento. Después, si hay caracteres pendientes en el buffer de transmisión, llama a uartstart() para continuar enviándolos.
void uartintr(void)
{
// Procesar caracteres entrantes
while(1){
int c = uartgetc();
if(c == -1) break; // No hay más caracteres
consoleintr(c); // Pasar carácter al controlador de consola
}
// Enviar caracteres del buffer si es necesario
acquire(&uart_tx_lock);
uartstart(); // Continuar transmisión
release(&uart_tx_lock);
}
5. Obtener Carácter del UART (uart.c)
uartgetc() comprueba el estado del registro LSR (Line Status Register) para ver si hay datos de entrada disponibles (bit 0). Si los hay, lee el carácter del registro RHR (Receive Holding Register) y lo devuelve; de lo contrario, devuelve -1.
int uartgetc(void)
{
// Comprobar si hay datos de entrada disponibles
if(ReadReg(LSR) & 0x01){
return ReadReg(RHR); // Leer carácter del registro de recepción
} else {
return -1; // No hay datos disponibles
}
}