Serial Device Driver ​
By far, you have studied the kernel thoroughly, which is the most important component of operating systems. Specifically, you have studied all the code in the grass
directory of egos-2000 which holds the kernel. You have also studied cpu_intr.c
and cpu_mmu.c
in the earth
directory which hold the code for interrupts and virtual memory (i.e., the CPU support on top of user-level ISA that is crucial to implementing the kernel).
In this project, you will further read earth/dev_disk.c
and earth/dev_tty.c
. The first file contains the driver for an SD card and the second file contains the driver for a terminal (i.e., keyboard input and screen output). While the 2 files combined have fewer than 200 lines of code, they give great examples of memory-mapped I/O, an important concept to learn.
I/O device and serial bus ​
Typically, a computer must be able to read keyboard input and print characters on a screen in order to interact with the outside world. In the old days, these functionalities are handled by a terminal device which is separate from the main body of the computer.
Terminal and UART ​
The photo below, taken at the Computer History Museum, shows the VT100 video terminal.
A serial bus connects the CPU with devices (e.g., VT100) and transfers data between the two. The most commonly used serial bus is likely the Universal Serial Bus, or USB. USB is powerful but complex: there are at least 9 variants of USB connectors with various numbers of hardware pins, according to Wikipedia.
In this project, we start by introducing the much simpler UART serial bus which has only 2 hardware pins. UART stands for Universal Asynchronous Receiver/Transmitter and thus one hardware pin is used to receive bytes while the other is used to send (transmit) bytes.
When pressing a key on the keyboard, the terminal sends the corresponding character as a byte through the UART serial bus to the CPU and the operating system could read this byte through the Receiver hardware pin. When the operating system prints a character, it sends a byte through the Transmitter pin to the terminal. Asynchronous here means that electrical signals for keyboard input and screen output on the two pins are unrelated.
The code below shows how an operating system could use UART to control a terminal.
#define UART_BASE 0x10010000UL
#define SIFIVE_UART_TXDATA 0UL
#define SIFIVE_UART_RXDATA 4UL
void uart_getc(char* c) {
int ch;
while ((ch = REGW(UART_BASE, SIFIVE_UART_RXDATA)) & (1 << 31));
/* Read memory address 0x10010004 */
*c = ch & 0xFF;
}
void uart_putc(char c) {
while ((REGW(UART_BASE, SIFIVE_UART_TXDATA) & (1 << 31)));
/* Read memory address 0x10010000 */
REGW(UART_BASE, SIFIVE_UART_TXDATA) = c;
/* Write memory address 0x10010000 */
}
In short, the CPU provides two special memory addresses 0x10010000
and 0x10010004
for sending and receiving bytes through UART. When receiving bytes, uart_getc
reads 4 bytes from 0x10010004
and the most significant bit indicates whether a byte is received. If the most significant bit is 1, the lowest byte is the byte read from UART (i.e., what is pressed on the terminal keyboard). When sending bytes, uart_putc
first waits for the UART bus to become idle (i.e., UART has finished sending the previous byte) and then writes to memory address 0x10010000
with the byte to be printed on the terminal screen.
The code above looks very simple because most of the complexities have been hidden by the hardware. For example, when running egos-2000 on the Arty FPGA board and using a laptop (e.g., the screen
command in MacOS) as the terminal, the UART/USB bridge chip on the board converts the electrical signals between UART and USB, making it possible for egos-2000 to use the simple UART serial bus and thus avoid the complexity of USB.
Still, the code above is a concrete example of memory-mapped I/O. Specifically, hardware manufacturers can define special memory regions that are used to control I/O devices. And different manufacturers can use different regions. Indeed, egos-2000 supports both QEMU and the Arty board which use different regions for the memory-mapped UART terminal. You can see such differences in earth/dev_tty.c
: the macros with prefix SIFIVE
and LITEX
correspond to two different hardware manufacturers.
For Sifive, you can find the 0x10010000
address and the TXDATA/RXDATA
offsets defined in Chapter 13.2 and 13.3 of their CPU reference manual.
SD card and SPI ​
In addition to a terminal, a computer typically needs a disk which stores blocks of data even when the computer is powered off. In this project, we use an SD card as the disk device and you will play with the SD card driver code in egos-2000.
An SD card is connected to the CPU through the Serial Peripheral Interface, or SPI. SPI is a serial bus with 4 hardware pins, a bit more complex than UART. The picture below is copied from Wikipedia which shows the 4 hardware pins.
Specifically, consider the CPU as the "SPI Main" and the SD card device as the "SPI Sub". Each side has 4 hardware pins and their functionalities are listed below.
- Chip Select (CS) is used to reset the SD card before using it.
- Serial Clock (SCLK) provides clock signals from the CPU (e.g., 20MHz).
- Main Out Sub In (MOSI) is used by the CPU to send bytes to the SD card.
- Main In Sub Out (MISO) is used by the SD card to send bytes to the CPU.
Similar to UART, the CPU provides memory-mapped I/O regions for communicating with the SD card through SPI. Different from UART, the SPI Main and SPI Sub exchanges the byte in their Shift Register during each communication. The code below explains how this works.
char spi_exchange(char byte) {
uint rxdata;
/* Send a byte through MOSI */
while (REGW(SPI_BASE, SIFIVE_SPI_TXDATA) & (1 << 31));
REGW(SPI_BASE, SIFIVE_SPI_TXDATA) = byte;
/* Receive a byte through MISO */
while ((rxdata = REGW(SPI_BASE, SIFIVE_SPI_RXDATA)) & (1 << 31));
return (char)(rxdata & 0xFF);
}
We omitted the macro definitions in the code for the memory-mapped I/O regions, and you can find them in egos-2000 or Chapter 16 of this CPU reference manual.
The first while
loop is similar to the waiting logic in uart_putc
. After sending each byte, the SPI bus receives a byte from the SD card device as the return value of spi_exchange
. While spi_exchange
uses the MOSI
and MISO
pins, the spi_set_clock
function controls the clock signal sending out through the SCLK
pin from the CPU (i.e., the SPI Main).
void spi_set_clock(uint freq) {
#define CPU_CLOCK_RATE 100000000 /* 100MHz */
uint div = CPU_CLOCK_RATE / freq + 1;
REGW(SPI_BASE, LITEX_SPI_CLKDIV) = div;
}
As an exercise, please scan the sd_init
function in earth/dev_disk.c
in which you will see how spi_set_clock
is used as well as some other initialization logic.
We have seen how to exchange a single byte between the CPU and the SD card. In the SD card standard, every SD card command takes 6 bytes, so the operating system can ask the SD card to complete a particular task by sending the corresponding 6-byte command. Such commands form the basis of the SD card driver code in egos-2000.
Run an SD card command ​
Given a 6-byte command, the sd_exec_cmd
function sends this command to the SD card and then waits for a reply.
char sd_exec_cmd(char* cmd) {
for (uint i = 0; i < 6; i++) spi_exchange(cmd[i]);
#define TIME_OUT 8000
for (uint reply, i = 0; i < TIME_OUT; i++)
if ((reply = spi_exchange(0xFF)) != 0xFF) return reply;
return 0xFF;
}
With helper function sd_exec_cmd
, we can start to read some SD card driver code.
SD card driver ​
An SD card block is typically 512 bytes. When reading or writing an SD card, the operating system will read or write whole blocks. This is different from a terminal device which reads or writes in the granularity of bytes. Many operating systems thus distinguish block device and character device as two device types.
Read an SD card block ​
The sd_read
function reads a block from the SD card into memory address dst
. And the offset
argument specifies which block to read. For example, if offset==0
, sd_read
will read the starting block on the SD card.
static void sd_read(uint offset, char* dst) {
/* Wait until SD card is not busy */
while (spi_exchange(0xFF) != 0xFF);
/* Send read request with cmd17 */
char* arg = (void*)&offset;
char cmd17[] = {0x51, arg[3], arg[2], arg[1], arg[0], 0xFF};
if (sd_exec_cmd(cmd17) != 0) FATAL("CMD17 has non-zero reply");
/* Wait for the data packet and ignore the 2-byte checksum */
while (spi_exchange(0xFF) != 0xFE);
for (uint i = 0; i < 512; i++) dst[i] = spi_exchange(0xFF);
spi_exchange(0xFF);
spi_exchange(0xFF);
}
On the high-level, reading a block of 512 bytes involves the following steps.
- Wait for the SD card to become idle.
- Send command
cmd17
to the SD card. Out of this command's 6 bytes, 4 bytes encode a block number (i.e.,offset
) indicating which block to read. - Wait for the SD card to be ready to send back the 512 bytes of data.
- Receive 512 bytes from the SD card as the block data and 2 bytes as the checksum.
As an exercise, please also read the sd_write
function in earth/dev_disk.c
which writes a block of data into the SD card using a similar control flow.
Multi-block read and write ​
You will implement an improved version of the SD card driver. The current driver uses SD card command 17 and 24 for reading and writing a single block. And the disk_read
and disk_write
functions will use command 17 and 24 multiple times within a loop.
The SD card standard also provides command 18 and 25 for reading and writing multiple continuous blocks. Your job is to replace the loops in disk_read
and disk_write
with your own SD card driver code using SD command 18 and 25. The details of command 18 and 25 can be found in this blog which contains some useful tables and figures. You can certainly find other online materials about these SD card commands.
After your code modifications, egos-2000 should be able to run normally on both QEMU and the Arty board since both of them provide an SD card device through the SPI bus. In addition, it could be very useful to write some unit tests for your driver code.
Accomplishments ​
You have learned about serial bus and memory-mapped I/O by reading through the device driver code in egos-2000. You have also written some driver code yourself for reading and writing SD card blocks which is the basis for the next project on file system.