OSI Lab6: SD Card Driver
Devices are essential components of a computer system, enabling programs to interact with the external world, such as through I/O operations. In this lab, you will study how the CPU communicates with devices, the role of device drivers, and how drivers function. In particular, you will implement an SD card driver, which will later serve as the storage medium for our file system.
Fetch lab6 from upstream repo
- fetch
lab6
branch$ git fetch upstream lab6 <you should see:> ... * [new branch] lab6 -> upstream/lab6
- create a local
lab6
branch$ git checkout --no-track -b lab6 upstream/lab6 <you should see:> Switched to a new branch 'lab6'
- confirm you’re on local branch
lab6
$ git status <you should see:> On branch lab6 nothing to commit, working tree clean
- push branch
lab6
to remote repoorigin
$ git push -u origin lab6
- Please do not merge or rebase your previous labs onto the current branch. The CPU (i.e., qemu) will be upgraded, introducing different hardware configurations from those in previous labs.
Section 0: Upgrading the CPU
To work with devices such as an SD card, we need to upgrade the CPU from sifive_e
to sifive_u
(here is the new CPU manual). This upgrade requires updating qemu, as SD card emulation for the sifive_u
CPU is supported starting from QEMU v7.0.0.
This upgrade introduces several “bummers”:
- the memory layout has changed. If you’re interested, please refer to the new memory layout in
library/egos.h
- gdb will no longer display machine-mode CSRs but other functions continue to work as expected.
- the used core (an E-core) of the CPU does not support S-mode, so privilege levels will be ignored for this lab.
Upgrading qemu
The following steps guide you through upgrading your qemu and CPU:
- download the new qemu:
- move the downloaded file (named
xpack-qemu-riscv-....tar.gz
) to your osi folder (~/osi
) - unzip the downloaded files in
~/osi
$ cd ~/osi/ $ tar -zxvf xpack-qemu-....tar.gz
You should see a new folder in
~/osi
with the same name as the downloaded file. - update the environment file:
- open
~/osi/env.sh
with your favorite text editor. - replace the old qemu path (pointing to folder
riscv-qemu-5.2.0.../bin/
) to the new qemu folder (xpack-qemu-.../bin/
)
- open
- confirm the qemu version
$ cd ~/osi/ $ source ./env.sh $ qemu-system-riscv32 --version xPack QEMU emulator version 7.2.5 (v7.2.5-xpack) Copyright (c) 2003-2022 Fabrice Bellard and the QEMU Project developers
- ensure the code runs on new qemu:
$ cd ~/osi/egos/ $ make && make qemu ....
You should see the egos shell. Note: in Makefile, use the default options for all previous labs.
potential final project incorporating previous labs to the new qemu
- You can complete this lab with the default Makefile configurations, which uses the naive scheduler and keep memory protection and virtual memory disabled.
- Of course, these features can be enabled to build a more complete OS kernel, but doing so requires some additional effort.
- Re-implementing all previous labs in the new CPU can be an interesting final project.
Section 1: Understanding SPI and SD card
A. Background: Memory-mapped IO and UART (as an example)
The OS communicates with devices to perform different tasks. For example, the tty
device handles user input and displays output on the screen. Underlying this functionality is the UART protocol, which egos uses to interact with input and output devices such as the keyboard and screen. The file earth/bus_uart.c
shows how to read and write characters from and to a tty
device over the UART protocol.
To understand the uart_getc/uart_putc
in earth/bus_uart.c
, the CPU provides two special memory-mapped addresses for UART communication: 0x10010000
for sending bytes and 0x10010004
for receiving bytes. (Where do these addresses come from? see Ch13.2 of the new CPU manual) When receiving data, uart_getc
reads 4 bytes from 0x10010004
, where the most significant bit (MSB) indicates whether a byte has been received. If the MSB is 0, the lowest byte contains the received data, representing the key pressed on the terminal keyboard. When sending data, uart_putc
waits for the UART bus to become idle, ensuring the previous transmission is complete, before writing the byte to 0x10010000
, which transmits it to the terminal screen.
UART is one protocol. Next, we will study another protocol, Serial Peripheral Interface (SPI), which is the core to the lab. In this lab, egos uses SPI to communicate with an SD card.
B. SPI basics
SPI is a widely used standard for synchronous serial communication. The full SPI specification includes many details and there are many SPI variants, but in this lab, we focus only on its core functionality without exploring the complexities.
SPI follows a mater-slave architecture, as shown in the figure below (borrowed from Wiki). In this lab, the CPU acts as the “master”, while the SD card is the “slave”.
In the figure,
- SCLK indicates serial clock, providing clock signals from the CPU (e.g., 20MHz).
- MOSI means Main Out Sub In, which is used by the CPU to send bytes to the SD card.
- MISO means Main In Sub Out, which is used by the SD card to send bytes to the CPU.
During each SPI clock cycle, a full-duplex transmission of a single bit occurs. The master transmits a bit on the MOSI line while simultaneously receiving a bit from the slave on the MISO line. Both devices read the incoming bit in each cycle, even when communication is intended to be unidirectional. Read SPI’s wiki page for more information.
For a programmer, this means that communication between the CPU and the SD card occurs in rounds. In each round, a fixed amount of data, say a byte, is exchanged between the two. This exchange is usually implemented using an inter-chip circular buffer, as illustrated in the figure above.
C. SD card
The Secure Digital (SD) memory card is the de facto standard for storage in mobile devices. There are many variations, but we will focus only on the one used in this lab. Most of the materials, including figures, are borrowed from this article.
At a high level, the CPU interacts with the SD card by sending commands and receiving responses. These commands specify actions such as reading and writing data. Each command follows a predefined protocol dictating how the SD card should respond. For example, if the command is a read request, the SD card returns the requested data. If the command is a write request, the SD card waits for the CPU to send the data to be written.
Next, we elaborate on the commands and responses.
(a) SPI command
Below is the format of a command. In the figure, “DI” means data in (i.e., MOSI in SPI), and “DO” means data out (i.e., MISO in SPI).
To issue a command, the CPU (your code) must send the following data through the DI (i.e., MOSI) channel:
- the first two bits: 0 followed by 1
- a command index (6 bits): this indicating what command to execute
- an argument (4 bytes): the argument for the command
- a CRC (1 byte): in this lab, we will ignore CRC and replace it with 0xFF.
(b) command response
After sending a command, the CPU expects a response from the DO (i.e., MISO) line. The response consists of a single byte, beginning with a 0
(1 bit) followed by a set of flags. One type of command response used in this lab is the R1 Response, as described below.
The flags are set when the corresponding errors occur. A value of 0x00
indicates a successful command with no errors.
(c) SPI command set
In the lab, you will interact with the following commands. This is a subset of all commands.
Command Index | Argument | Response | Description |
CMD12 | None(0) | R1b | Stop to read data. |
CMD17 | Address[31:0] | R1 | Read a block. |
CMD18 | Address[31:0] | R1 | Read multiple blocks. |
CMD24 | Address[31:0] | R1 | Write a block. |
CMD25 | Address[31:0] | R1 | Write multiple blocks. |
In the table, “CMD12” refers to a command with a command index of 12. The address in the argument represents the byte offset on the SD card where data will be read from or written to. Most commands return a 1-byte R1 response, where 0x00
indicates a successful operation.
Some commands take longer to execute and return an R1b response, which consists of an R1 response followed by a busy flag. In this case, the DO (MISO) line remains busy while the SD card processes the command. The CPU must wait for the process to complete, which is indicated when DO (MISO) goes idle (i.e., a 0xFF
is received).
Notes:
- The command from the CPU to the SD card is a fixed length packet.
- The DI (MOSI) signal must be kept high while reading data (i.e., sending a
0xFF
and get the received data). - DO (MISO) is idle when it goes high (a
0xFF
is received). - The card is ready to receive a command frame when it drives DO (MISO) high (meaning,
0xFF
is received)
Section 2: SD card driver
In this section, you will implement an SD card driver. In particular, you will study the provided example for reading a block from the SD card, then complete the function of writing a block. Finally, you will extend the driver to support multi-block reads and writes.
A. Data transfer
The primary function of an SD card driver is to transfer data between memory and the SD card. In a read or write transaction, the CPU begins by sending a command, followed by the exchange of one or more data blocks after receiving the command response. Each data block is transmitted as a data packet, which consists of a Token, a Data Block, and a CRC (detailed below).
A single-block read: an example
We start with an example of reading a single block (512B) from the SD card. Below is the workflow of reading a single block.
The CPU begins by sending CMD17 to the SD card and waits for the command response. On the device side, after processing CMD17, the SD card first returns the command response, followed by a data packet.
Data packet
A data packet contains (1) a data token of a byte, (2) a data block of size 512B, and (3) a CRC of 2 bytes (we ignore CRC in this later). The format of the data packet is as follows:
A data packet has three types of data tokens:
Stop Tran token is used to terminate a multiple block write transaction, it is used as single byte packet without data block and CRC. You will need it for exercise 4.
B. Understanding single-block read
With the above information, you should be able to understand how SD driver reads a single block from the SD card (sd_single_read()
in earth/dev_sdcard.c
).
Exercise 1 complete single-block read
- Read
spi_exchange()
inearth/dev_sdcard.c
to understand how to read and write a single byte of data from and to the SD card.- Read
sd_exec_cmd()
to understand the process of issuing a command to the SD card.- If this is your first time reading driver code, it might take you some time to read back and forth between code and the instructions.
- Turn on SD card driver by changing
DISK=SDCARD
inMakefile
- Then, you will see,
$ make && make qemu ... [FATAL] sd_single_read(): cmd17 frame is not implemented.
- Read and complete the cmd17 frame in
sd_single_read()
.- After completion, you should see:
$ make && make qemu ... [CRITICAL] Welcome to the egos-2k+ shell! [INFO] proc 5 finished after 0 yields, turnaround time: 0.00, response time: 0.00, cputime: 0.00 ➜ /home/cs6640 %
C. Implementing single-block write
Next, you will implement the write functionality of the driver. Below is the workflow of a single block write.
Notice that, during the write process, the SD card first returns a command response. Then, after receiving the data packet, which includes the data to be written, it returns a data response. The data response is a single-byte message encoded as follows:
Exercise 2 complete single-block write
- Read the skeleton code in
sd_single_write()
ofearth/dev_sdcard.c
and make sense of it.- Complete the function
sd_single_write()
enabling writing data to the SD card.- To test your code, uncomment
sd_test()
indisk_init()
ofearth/dev_disk.c
and run egos.- You should pass the first “single-write test”:
$ make && make qemu ... [CRITICAL] Start SD card testing [INFO] single-write test passed! [FATAL] sd_multi_read is not implemented
D. Multi-block reads and writes
Reading and writing a single block at a time is inefficient. The SPI protocol supports transferring multiple blocks in a single transaction. In this section, your task is to implement multi-block read and write operations for the SD card driver.
Multi-block reads
Here is the workflow of a multi-bock read:
The CMD18 is used to read data blocks sequentially, starting from a specified address (the argument of CMD18). A multi-block read operation is open-ended, meaning the SD card continues transferring data blocks until it receives a termination command, CMD12. The byte received immediately after CMD12 is a stuff byte and should be discarded before receiving the command response for CMD12.
Exercise 3 implement multi-block read
- Implement
sd_multi_read()
inearth/dev_sdcard.c
- After completion, you should pass the second test case:
$ make && make qemu ... [CRITICAL] Start SD card testing [INFO] single-write test passed! [INFO] multi-read test passed! [FATAL] sd_multi_write is not implemented
Multi-block writes
Finally, you will implement multi-block writes. Here is the workflow:
The multi-block write command (CMD25) writes data blocks sequentially, starting at the specified address provided in the command argument. Once the SD card acknowledges CMD25, the CPU sends multiple data packets to the card. Each data packet requires a specific data token. Refer to the previous data packet section for the correct data token used with CMD25. The write transaction continues until it is terminated with a Stop Tran token (also covered in the data packet section). After each data block and the Stop Tran token, the SD card outputs busy flags and the CPU needs to wait until the card is idle (i.e., receiving 0xFF
on DO/MISO).
Exercise 4 implement multi-block write
- Implement
sd_multi_write()
inearth/dev_sdcard.c
- After completion, you should pass the third test:
... [CRITICAL] Start SD card testing [INFO] single-write test passed! [INFO] multi-read test passed! [INFO] multi-write test passed! ... ➜ /home/cs6640 %
Testing your code
Exercise 5 test your code
- You should further test your code with different parameters
- The size of the free SD card space is 1MB.
- Note: we will briefly take a look at your test cases. You will receive full credit for the exercise if you add at least one new test case.
Finally, submit your work
Submitting consists of three steps:
- Executing this checklist:
- Fill in
~/osi/egos/slack/lab6.txt
. - Make sure that your code build with no warnings.
- Fill in
Push your code to GitHub:
$ cd ~/osi/egos/ $ git commit -am 'submit lab6' $ git push origin lab6 Counting objects: ... .... To github.com/NEU-CS6640-labs/egos-<YOUR_ID>.git c1c38e6..59c0c6e lab6 -> lab6
Actually commit your lab (with timestamp and git commit id):
Get the git commit id of your work. A commit id is a 40-character hexadecimal string. You can obtain the commit id for the last commit by running the command
git log -1 --format=oneline
.- Submit a file named
git.txt
to Canvas. (there will be an assignment for this lab on Canvas.) The filegit.txt
contains two lines: the first line is your github repo url; the second line is the git commit id that you want us to grade. Here is an example:git@github.com:NEU-CS6640-labs/egos-<YOUR_ID>.git 29dfdadeadbeefe33421f242b5dd8312732fd3c9
Notice: the repo address must start with
git@github.com:...
(nothttps://...
). You can get your repo address on GitHub repo page by clicking the green “Code” button, then choose “SSH”. - Note: You can submit as many times as you want; we will grade the last commit id submitted to Canvas. Also, you can submit any commit id in your pushed git history; again, we will grade the commit id submitted to Canvas.
Notice: if you submit multiple times, the file name (git.txt
) changes togit-N.txt
whereN
is an integer and represents how many times you’ve submitted. We will grade the file with the largestN
.
NOTE: Ground truth is what and when you submitted to Canvas.
A non-existent repo address or a non-existent commit id in Canvas means that you have not submitted the lab, regardless of what you have pushed to GitHub—we will not grade it. So, please double check your submitted repo and commit id!
The time of your submission for the purposes of tracking lateness is the timestamp on Canvas, not the timestamp on GitHub.
This completes the lab.
Acknowledgments
Some materials are borrowed from Yunhao Zhang’s notes. The SPI introduction borrows materials from SPI wiki page), and the SD card parts are largly adpated from the article, How to Use MMC/SDC.