Skip to content

Commit

Permalink
Rewrite reading from UART to be interrupt-driven
Browse files Browse the repository at this point in the history
* Rewrite UART code into a driver
* Introduce platform-level interrupt (PLIC) handling, which UART
  interrupts come in through
* Handle incoming characters in the uart driver and accumulate them in a
  buffer
* When reading from stdin, try reading from the uart driver's buffer and
  put the process to sleep if the buffer has less than at least one full
  line of data (or is full)
* When the driver sees an incoming newline (or fills the buffer to the
  brim), wake up the process waiting on the driver
  • Loading branch information
rtfb committed Aug 29, 2023
1 parent dde7e37 commit 0c8a901
Show file tree
Hide file tree
Showing 19 changed files with 346 additions and 115 deletions.
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ TEST_DEPS = src/baremetal-fib.s src/uart-print.s src/baremetal-poweroff.s
USER_DEPS = src/boot.s src/uart-print.s src/baremetal-poweroff.s src/kernel.c \
src/syscalls.c src/pmp.c src/riscv.c src/fdt.c src/string.c \
src/proc_test.c src/spinlock.c src/proc.c src/context.s \
src/pagealloc.c src/uart.c src/fs.c src/bakedinfs.c src/runflags.c \
src/pipe.c \
src/pagealloc.c src/fs.c src/bakedinfs.c src/runflags.c \
src/pipe.c src/drivers/drivers.c src/drivers/uart/uart.c \
src/plic.c \
user/src/userland.c user/src/usyscalls.S user/src/user-printf.s \
user/src/user-printf.c user/src/shell.c user/src/ustr.c
TEST_SIFIVE_U_DEPS = $(TEST_DEPS)
Expand Down
6 changes: 6 additions & 0 deletions include/drivers/drivers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef _DRIVERS_H_
#define _DRIVERS_H_

void drivers_init();

#endif // ifndef _DRIVERS_H_
67 changes: 67 additions & 0 deletions include/drivers/uart/uart.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#ifndef _UART_H_
#define _UART_H_

#include "fs.h"

#ifndef UART_BASE
#define UART_BASE 0x10010000
#endif

#define UART_TXDATA 0x00
#define UART_RXDATA 0x04
#define UART_TXCTRL 0x08
#define UART_RXCTRL 0x0c
#define UART_IE 0x10
#define UART_IP 0x14
#define UART_BAUD_RATE_DIVISOR 0x18

#define UART_IE_TXWM 1
#define UART_IE_RXWM 2

#define ASCII_DEL 0x7f

#define UART_BUF_SZ 64

typedef struct uart_state_s {
// TODO: spinlock lock;
char rxbuf[UART_BUF_SZ];
char txbuf[UART_BUF_SZ];
int rx_rpos;
int rx_wpos;
int tx_rpos;
int tx_wpos;
int rx_num_newlines; // number of '\n' chars in the buffer
int rx_buff_full; // indicates that rxbuf got full
} uart_state_t;

extern uart_state_t uart0; // defined in uart.c

void uart_init();

// uart_enqueue_chars reads characters from rx FIFO and enqueues them into the
// uart0's rxbuf. If rxbuf is full, rx_buff_full is set to 1 and all
// incoming data is dropped on the floor (unless it's a backspace, which frees
// up some space instead of using up more buffer space).
//
// When a newline character gets enqueued, all processes waiting on uart0 are
// woken up to get them a chance to continue reading.
int uart_enqueue_chars();

// uart_writechar writes a single char to UART synchronously.
void uart_writechar(char ch);

// uart_readline attempts to read a full line of characters from rxbuf. If
// rxbuf is empty or does not contain a newline character, the calling process
// yields execution and goes to sleep.
int32_t uart_readline(char* buf, uint32_t bufsize);

int32_t uart_print(char const* data, uint32_t size);

int32_t uart_read(file_t* f, uint32_t pos, void* buf, uint32_t bufsize);
int32_t uart_write(file_t* f, uint32_t pos, void* buf, uint32_t bufsize);

// implemented in uart-print.s
extern int32_t uart_prints(char const* data);
extern int32_t uart_printc(char c);

#endif // ifndef _UART_H_
4 changes: 4 additions & 0 deletions include/machine/hifive1-revb.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@
// system cost, can be driven by a factorytrimmed on-chip oscillator."
#define ONE_SECOND 32768

// https://github.com/sifive/freedom-e-sdk/blob/master/bsp/sifive-hifive1-revb/core.dts#L166
// interrupts = <3>;
#define UART0_IRQ_NO 3

#endif // ifndef _HIFIVE1_REVB_
4 changes: 4 additions & 0 deletions include/machine/qemu_e.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@
// It's used in riscv/sifive_e and in riscv/virt machines.
#define ONE_SECOND (10*1000*1000)

// https://github.com/qemu/qemu/blob/master/include/hw/riscv/sifive_e.h#L82
// SIFIVE_E_UART0_IRQ = 3,
#define UART0_IRQ_NO 3

#endif // ifndef _QEMU_E_H_
4 changes: 4 additions & 0 deletions include/machine/qemu_u.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@
// https://github.com/qemu/qemu/blob/master/hw/riscv/sifive_u.c
#define ONE_SECOND (1*1000*1000)

// https://github.com/qemu/qemu/blob/master/include/hw/riscv/sifive_u.h#L106
// SIFIVE_U_UART0_IRQ = 4,
#define UART0_IRQ_NO 4

#endif // ifndef _QEMU_U_H_
28 changes: 28 additions & 0 deletions include/plic.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#ifndef _PLIC_H_
#define _PLIC_H_

#define PLIC_BASE 0x0c000000
#define PLIC_PRIORITY (PLIC_BASE + 0x00000000)
#define PLIC_PENDING (PLIC_BASE + 0x00001000)
#define PLIC_ENABLE (PLIC_BASE + 0x00002000)
#define PLIC_THRESHOLD (PLIC_BASE + 0x00200000)
#define PLIC_CLAIM_RW (PLIC_BASE + 0x00200004)

#define PLIC_MAX_PRIORITY 7

// plic_enable_intr enables a specified interrupt.
void plic_enable_intr(int intr_no);

// plic_set_intr_priority sets a priority on a given interrupt. 7 is the
// highest priority, while a zero priority effectively disables an interrupt.
void plic_set_intr_priority(int intr_no, int priority);

// plic_set_threshold sets a global threshold for PLIC interrupts. Only the
// interrupts with priority>threshold will be triggered.
void plic_set_threshold(int threshold);

// plic_dispatch_interrupts does the actual job of handling a PLIC interrupt
// and calling the respective subsystems.
void plic_dispatch_interrupts();

#endif // ifndef _PLIC_H_
5 changes: 5 additions & 0 deletions include/riscv.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
#define TRAP_DIRECT 0x00
#define TRAP_VECTORED 0x01

#define MSTATUS_MPIE_BIT 7

#define MIE_MTIE_BIT 7 // mie.MTIE (Machine Timer Interrupt Enable) bit
#define MIE_MEIE_BIT 11 // mie.MEIE (Machine External Interrupt Enable) bit

unsigned int get_mhartid();
unsigned int get_mstatus();
void set_mstatus(unsigned int mstatus);
Expand Down
30 changes: 0 additions & 30 deletions include/uart.h

This file was deleted.

2 changes: 1 addition & 1 deletion src/boot.s
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ interrupt_vector:
.balign 4
j interrupt_noop # 10: reserved
.balign 4
j interrupt_noop # 11: machine external interrupt
j k_interrupt_plic # 11: machine external interrupt

exception_vector: # 3.1.20 Machine Cause Register (mcause), Table 3.6: Machine cause register (mcause) values after trap.
.balign 4
Expand Down
7 changes: 7 additions & 0 deletions src/context.s
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ k_interrupt_timer:
# This will restore user registers from trap_frame and then mret:
j ret_to_user

.globl k_interrupt_plic
k_interrupt_plic:
call kernel_plic_handler

# This will restore user registers from trap_frame and then mret:
j ret_to_user

.globl ret_to_user
ret_to_user:
# load a pointer to trap_frame into t6:
Expand Down
5 changes: 5 additions & 0 deletions src/drivers/drivers.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#include "drivers/uart/uart.h"

void drivers_init() {
uart_init();
}
167 changes: 167 additions & 0 deletions src/drivers/uart/uart.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#include "sys.h"
#include "proc.h"
#include "drivers/uart/uart.h"
#include "plic.h"

uart_state_t uart0;

void uart_init() {
uart0.rx_rpos = 0;
uart0.rx_wpos = 0;
uart0.tx_rpos = 0;
uart0.tx_wpos = 0;
uart0.rx_num_newlines = 0;
uart0.rx_buff_full = 0;

// enable reading:
*(uint32_t*)(UART_BASE + UART_RXCTRL) = 1;

// enable read interrupts:
*(uint32_t*)(UART_BASE + UART_IE) = UART_IE_RXWM;

// set baud divisor (the SiFive FE310-G002 manual lists a table of possible
// values in Section 18.9, determined this particular choice
// experimentally. Furthermore, it's the default on HiFive1-revB board):
*(uint32_t*)(UART_BASE + UART_BAUD_RATE_DIVISOR) = 138;

plic_enable_intr(UART0_IRQ_NO);
plic_set_intr_priority(UART0_IRQ_NO, PLIC_MAX_PRIORITY);
plic_set_threshold(PLIC_MAX_PRIORITY - 1);
}

// _decrement_wpos attempts to decrement a given wpos. It doesn't decrement
// behind rx_rpos and doesn't decrement onto the last newline. It also wraps
// around zero.
int _decrement_wpos(int wpos) {
int orig_wpos = wpos;
if (wpos == uart0.rx_rpos) {
return orig_wpos;
}
wpos--;
if (wpos < 0) {
wpos = UART_BUF_SZ - 1;
}
if (uart0.rxbuf[wpos] == '\n') {
return orig_wpos;
}
return wpos;
}

int uart_enqueue_chars() {
volatile int32_t* rx = (int32_t*)(UART_BASE + UART_RXDATA);
int wpos = uart0.rx_wpos;
int num_enqueued = 0;
while (1) {
int32_t word = *rx;
if (word < 0) {
break;
}
char ch = word & 0xff;
if (ch == ASCII_DEL) {
int new_wpos = _decrement_wpos(wpos);
if (new_wpos != wpos) {
wpos = new_wpos;
uart_writechar('\b');
uart_writechar(' ');
uart_writechar('\b');
// we've decremented wpos, so unmark the buffer as full if it was such
if (uart0.rx_buff_full) {
uart0.rx_buff_full = 0;
}
}
continue;
}
if (uart0.rx_buff_full) {
proc_mark_for_wakeup(&uart0);
continue; // if rxbuf is full, just keep draining rx FIFO
}
if (ch == '\r') {
ch = '\n';
uart0.rx_num_newlines++;
proc_mark_for_wakeup(&uart0);
}
uart_writechar(ch); // echo back to console
uart0.rxbuf[wpos] = ch;
wpos++;
num_enqueued++;
if (wpos >= UART_BUF_SZ) {
wpos = 0;
}
if (wpos == uart0.rx_rpos) {
uart0.rx_buff_full = 1;
}
}
uart0.rx_wpos = wpos;
return num_enqueued;
}

void uart_writechar(char ch) {
volatile int32_t* tx = (int32_t*)(UART_BASE + UART_TXDATA);
while ((int32_t)(*tx) < 0);
*tx = ch;
}

// _can_read_from checks whether the internal state of a given uart device
// allows to read from it.
int _can_read_from(uart_state_t *uart) {
if (uart->rx_num_newlines > 0) {
return 1;
}
if (uart->rx_buff_full == 1) {
return 1;
}
return 0;
}

int32_t uart_readline(char* buf, uint32_t bufsize) {
while (!_can_read_from(&uart0)) {
proc_yield(&uart0);
}
int32_t nread = 0;
int32_t rpos = uart0.rx_rpos;
int32_t wpos = uart0.rx_wpos;
for (;;) {
if (nread >= bufsize) {
break;
}
if (rpos == wpos && nread > 0) {
break;
}
char ch = uart0.rxbuf[rpos];
rpos++;
if (rpos == UART_BUF_SZ) {
rpos = 0;
}
if (ch == '\n') {
uart0.rx_num_newlines--;
break;
}
buf[nread] = ch;
nread++;
}
if (uart0.rx_rpos != rpos || nread > 0) {
uart0.rx_buff_full = 0; // at least one char was read, so it's no longer full
}
uart0.rx_rpos = rpos;
return nread;
}

int32_t uart_print(char const* data, uint32_t size) {
if (size == -1) {
return uart_prints(data);
}
int i = 0;
while (i < size) {
uart_printc(data[i]);
i++;
}
return i;
}

int32_t uart_read(file_t* f, uint32_t pos, void* buf, uint32_t bufsize) {
return uart_readline(buf, bufsize);
}

int32_t uart_write(file_t* f, uint32_t pos, void* buf, uint32_t bufsize) {
return uart_print(buf, bufsize);
}
2 changes: 1 addition & 1 deletion src/fs.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#include "fs.h"
#include "pipe.h"
#include "bakedinfs.h"
#include "uart.h"
#include "drivers/uart/uart.h"

file_table_t ftable;
file_t stdin;
Expand Down
Loading

0 comments on commit 0c8a901

Please sign in to comment.