Manejo de Interrupciones de Dispositivos UART en Sistemas Embebidos

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
  }
}

Etiquetas: RISC-V UART Interrupciones PLIC sistemas embebidos

Publicado el 7-4 22:28