You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: _posts/2019-07-08-riscv-from-scratch-3.markdown
+17-19Lines changed: 17 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,9 +15,7 @@ description: A post describing how UARTs work, and the beginning of an implement
15
15
16
16
Welcome to the third post in the *RISC-V from scratch* series! As a quick recap, throughout *RISC-V from scratch* we will explore various low-level concepts (compilation and linking, primitive runtimes, assembly, and more), typically through the lens of RISC-V and its ecosystem.
17
17
18
-
In [the first post of this series]({% post_url 2019-03-10-riscv-from-scratch-1 %}), we introduced RISC-V, explained why it's important, set up the full GNU RISC-V toolchain, and built and ran a simple program on an emulated version of a RISC-V processor. In the [second post of this series]({% post_url 2019-04-27-riscv-from-scratch-2 %}), we reviewed devicetree layouts, linker scripts, basic RISC-V assembly, minimal C runtimes, and more, all in an effort to understand how we get to the `main` function.
19
-
20
-
In the previous post, we used the [dtc](http://manpages.ubuntu.com/manpages/disco/man1/dtc.1.html) tool to inspect the layout of various hardware components in the `virt` QEMU virtual machine. At that point, our intention was to determine at what address the RAM lived at within that machine, but you may also recall that `virt` had lots of other interesting components, one of which being an onboard UART.
18
+
In the [previous post]({% post_url 2019-04-27-riscv-from-scratch-2 %}), we used the [dtc](http://manpages.ubuntu.com/manpages/disco/man1/dtc.1.html) tool to inspect the layout of various hardware components in the `virt` QEMU virtual machine. At that point, our intention was to determine at what address the RAM lived at within that machine, but you may also recall that `virt` had other interesting components, one of which being an onboard UART.
21
19
22
20
In order to further expand our knowledge of RISC-V assembly, we'll spend the next three posts writing a driver for this UART, deeply exploring important concepts such as ABIs, function prologues and epilogues, and low-level stack manipulation along the way.
23
21
@@ -27,17 +25,17 @@ So, without further ado, let's begin.
27
25
28
26
### What is a UART?
29
27
30
-
UART stands for "**U**niversal **A**synchronous **R**eceiver-**T**ransmitter", and is a physical hardware device (_not_ a protocol, à la [I2C](https://en.wikipedia.org/wiki/I%C2%B2C) or [SPI](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface)) used to transmit and receive serial data. Serial data transmission is the process of sending data sequentially, bit-by-bit. In contrast, parallel data transmission is the process of sending multiple bits all at once. This image from the [serial communication Wikipedia page](https://en.wikipedia.org/wiki/Serial_communication) illustrates this concept well:
28
+
UART stands for "**U**niversal **A**synchronous **R**eceiver-**T**ransmitter", and is a physical hardware device (_not_ a protocol, à la [I2C](https://en.wikipedia.org/wiki/I%C2%B2C) or [SPI](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface)) used to transmit and receive serial data. Serial data transmission is the process of sending data sequentially, bit-by-bit. In contrast, parallel data transmission is the process of sending multiple bits all at once. This image from the [serial communication Wikipedia page](https://en.wikipedia.org/wiki/Serial_communication) illustrates the difference well:
31
29
32
30
{:refdef: style="text-align: center;"}
33
31
<ahref="/assets/img/riscv-from-scratch-pt-3/Parallel_and_Serial_Transmission.gif"></a>
34
32
{: refdef}
35
33
36
-
UARTs never specify a rate at which data should be received or transmitted (also called a *clock rate* or *clock signal*), which is what makes them asynchronous rather than synchronous. Instead, UARTs use start and stop bits around each transmitted packet of data to inform the receiving UART of when to start reading data.
34
+
UARTs never specify a rate at which data should be received or transmitted (also called a *clock rate* or *clock signal*), which is what makes them asynchronous rather than synchronous. Instead, transmitting UARTs frame each packet of data with start and stop bits, which informs receiving UARTs of when to start and stop reading data.
37
35
38
-
You may also be familiar with the term "USART", which stands for "**U**niversal **S**ynchronous/**A**synchronous **R**eceiver-**T**ransmitter". As you might imagine, USARTs are capable of acting asynchronously in the same way UARTs do, but also come with the option of operating synchronously. When operating synchronously, USARTs forgo the usage of the start and stop bits and instead transmit a clock signal on a separate line that allows transmitting and receiving USARTs to sync up. Our driver will be for a UART, not a USART, so we won't dive into too much more detail on USARTs, but it's good to know of their existence and some basic differences.
36
+
You may also be familiar with USARTs (**U**niversal **S**ynchronous/**A**synchronous **R**eceiver-**T**ransmitter), which are capable of acting both synchronously and asynchronously. When operating synchronously, USARTs forgo the usage of the start and stop bits and instead transmit a clock signal on a separate line that allows transmitting and receiving USARTs to sync up.
39
37
40
-
UARTs and USARTs are all around you, even if you may not realize it. They are built into nearly every modern microcontroller, our `virt` machine included. UARTs and USARTs help power the traffic lights you yield to, the refrigerator that cools your food, the satellites that orbit the Earth for years on end...the list goes on and on.
38
+
UARTs and USARTs are all around you, even if you may not realize it. They are built into nearly every modern microcontroller, our `virt` machine included. These devices help power the traffic lights you yield to, the refrigerator that cools your food, and the satellites that orbit the Earth for years on end.
At the top of our `grep` output, we find a node called `chosen` which uses the onboard UART to display any output it may produce. According to [this documentation](https://elinux.org/Device_Tree_Usage#chosen_Node), the `chosen` node is special in that it doesn't represent physical hardware. `chosen` is used to exchange data between firmware and a bare-metal program, such as an operating system. We won't be using an operating system in this post, instead flashing our UART driver and a `main` function utilizing that driver directly onto the `virt` QEMU machine.
106
+
At the top of our `grep` output, we find a node called `chosen` which uses the onboard UART to display any output it may produce. According to [this documentation](https://elinux.org/Device_Tree_Usage#chosen_Node), the `chosen` node is special in that it doesn't represent physical hardware. `chosen` is used to exchange data between firmware and a bare-metal program, such as an operating system. We won't need to make use of `chosen`in this post, so let's ignore it for now.
109
107
110
108
Next we find exactly what we're looking for - the `uart` node. We see that this UART is accessible at memory address `0x10000000`, indicated by the `@10000000` portion of `uart@10000000`. We also see `interrupts` and `interrupt-parent` properties, indicating to us that this onboard UART is capable of generating interrupts.
111
109
112
110
For those unfamiliar, an interrupt is a signal to the processor emitted by hardware or software indicating an event needs immediate attention. For example, a UART may generate an interrupt when:
113
111
114
-
1. New data has entered the receive buffer
115
-
2. When the transmitter has finished sending all data in its buffer
116
-
3. When the UART encounters a transmission error
112
+
* New data has entered the receive buffer
113
+
* When the transmitter has finished sending all data in its buffer
114
+
* When the UART encounters a transmission error
117
115
118
-
These interrupts act as hooks so programmers can write code that responds to these events appropriately. We won't be using any interrupts in this initial driver, so let's skip these properties for now.
116
+
These interrupts act as hooks so programmers can write code that responds to these events appropriately. We won't be using any interrupts in this initial driver, so let's skip these properties.
119
117
120
118
The next property down the list is `clock-frequency = <0x384000>;`. [Referencing the devicetree specification](https://buildmedia.readthedocs.org/media/pdf/devicetree-specification/latest/devicetree-specification.pdf), `clock-frequency` represents the frequency of the internal clock powering the UART (and likely the rest of the `virt` machine). The value is hexadecimal `0x384000`, which is `3686400` in decimal format. This frequency is measured in [hertz](https://en.wikipedia.org/wiki/Hertz), and converting this value to megahertz results in 3.6864 MHz, or 3.6864 million clock ticks a second, which is a [standard crystal oscillator frequency](https://en.wikipedia.org/wiki/Crystal_oscillator_frequencies).
121
119
122
-
Our next property is `reg = <0x00 0x10000000 0x00 0x100>;`, which determines the memory location of our UART and for how long its memory extends. The `#address-cells` and `#size-cells` properties in the root node of our `riscv64-virt.dts` file are both set to `<0x02>`, which tells us it takes the addition of two `<u32>` cells to determine the address the `reg` begins at and two `<u32>` cells to determine the length the `reg` extends. Given the values present in our `reg` field, we know our UART registers begin at memory address `0x00 + 0x10000000 = 0x10000000` and extend `0x00 + 0x100 = 0x100` bytes. If this still is a little unclear, you can read about these properties in [the devicetree specification](https://buildmedia.readthedocs.org/media/pdf/devicetree-specification/latest/devicetree-specification.pdf).
120
+
Our next property is `reg = <0x00 0x10000000 0x00 0x100>;`, which determines the memory location of our UART and for how long its memory extends. The `#address-cells` and `#size-cells` properties in the root node of our `riscv64-virt.dts` file are both set to `<0x02>`. This tells us it takes the addition of two `<u32>` cells to determine the address the `reg` begins at and two `<u32>` cells to determine the length the `reg` extends. Given the values present in our `reg` field, we know our UART registers begin at memory address `0x00 + 0x10000000 = 0x10000000` and extend `0x00 + 0x100 = 0x100` bytes. If this still is a little unclear, you can read about these properties in [the devicetree specification](https://buildmedia.readthedocs.org/media/pdf/devicetree-specification/latest/devicetree-specification.pdf).
123
121
124
-
This brings us to the last property in our `uart` node, `compatible = "ns16550a";`. This property is particularly important, as it informs us what programming model the UART in question is compatible with. Operating systems use this property to determine what device drivers it can use for a peripheral. There are a litany of good resources showing all the details necessary to implement a NS16550A-compatible UART, including [this one](https://www.lammertbies.nl/comm/info/serial-uart.html) which we'll be referencing from here on out.
122
+
This brings us to the last property in our `uart` node, `compatible = "ns16550a";`, which informs us what programming model our UART is compatible with. Operating systems use this property to determine what device drivers it can use for a peripheral. There are plentiful resources showing all the details necessary to implement a NS16550A-compatible UART, including [this one](https://www.lammertbies.nl/comm/info/serial-uart.html) which we'll be referencing from here on out.
125
123
126
124
### Creating the basic skeleton of our driver
127
125
@@ -131,7 +129,7 @@ We have all we need to begin writing our driver, so let's begin. Start by ensur
131
129
cd some/path/to/riscv-from-scratch/work
132
130
{% endhighlight %}
133
131
134
-
Now create a file called `ns16550a.s`, which will contain the code for our NS16550A UART driver. In this file, let's start with a basic skeleton containing the functions we want to expose. For now, we'll limit this driver to simply reading and writing chars, or bytes, without worrying about other available capabilities of the NS16550A, such as interrupts.
132
+
Now create a file called `ns16550a.s`, in which we will start with a basic skeleton of the functions we want to expose for our driver. For now, we'll limit this driver to simply reading and writing chars, or bytes, without worrying about other available capabilities of NS16550A UARTs, such as interrupts.
135
133
136
134
{% highlight nasm %}
137
135
.global uart_put_char
@@ -148,7 +146,7 @@ uart_put_char:
148
146
.end
149
147
{% endhighlight %}
150
148
151
-
Walking through this, we use the `.global` assembler directive to declare `uart_put_char` and `uart_get_char` as symbols we want accessible to other object files that are linked with this one. All lines that begin with `.`s are assembler directives, meaning they provide information to the assembler rather than acting as executable code. A detailed description of all the basic GNU assembler directives can be found [here](https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_chapter/as_7.html).
149
+
We begin with the `.global` assembler directive to declare `uart_put_char` and `uart_get_char` as symbols accessible to other files linked with this one. All lines that begin with `.`s are assembler directives, meaning they provide information to the assembler rather than acting as executable code. A detailed description of all the basic GNU assembler directives can be found [here](https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_chapter/as_7.html).
152
150
153
151
Next, you'll see definitions of each of these symbols, currently only containing `.cfi` assembler directives. These `.cfi` directives [inform tools](https://stackoverflow.com/a/33732119/2421349), such as the assembler or exception unwinder, about the structure of the frame and how to unwind it. `.cfi_startproc` and `.cfi_endproc` respectively signal the start and end of a function.
154
152
@@ -195,7 +193,7 @@ _start:
195
193
.end
196
194
{% endhighlight %}
197
195
198
-
This is an easy fix - we simply need to link a file that defines the `main` symbol. We would've wanted to do this anyways at some point, as we need some way to exercise our UART driver, and we can easily do so from `main`. Create a new file called `main.c` in our working directory (`riscv-from-scratch/work`) and define a main function. We'll also call `uart_put_char` to ensure that `main` is able to find our definition of it in `ns16550a.s`.
196
+
This is an easy fix - we simply need to link a file that defines the `main` symbol. We would've eventually wanted to do this anyways, as we need some way to exercise our UART driver, and we can easily do so from our `main` entrypoint. Create a new file called `main.c` in our working directory (`riscv-from-scratch/work`) and define a `main` function. We'll also call `uart_put_char` to ensure that `main` is able to find our definition of it in `ns16550a.s`.
And this results in success! We now have a file in our working directory called `a.out`, which is our executable. We can also use [`nm`](https://sourceware.org/binutils/docs/binutils/nm.html) to see all of the symbols we've defined so far, including the ones we just created, `main`, `uart_get_char`, and `uart_put_char`. If you haven't already, make sure you have the `riscv64` version of `nm` installed and available on your path, or linked into your `/usr/local/bin` folder. [This section]({% post_url 2019-04-27-riscv-from-scratch-2 %}#qemu-and-risc-v-toolchain-setup) in the first post in this series describes in detail how to accomplish this.
212
+
And this results in success! We can use [`nm`](https://sourceware.org/binutils/docs/binutils/nm.html)on our newly created executable, `a.out`, to see all the symbols it defines, which includes the ones we just created, `main`, `uart_get_char`, and `uart_put_char`. Note that you may need to revisit [these instructions]({% post_url 2019-03-10-riscv-from-scratch-1 %}#qemu-and-risc-v-toolchain-setup) to ensure `riscv64-unknown-elf-nm` is installed and available on your path or linked into your `/usr/local/bin` directory.
215
213
216
214
{% highlight bash %}
217
215
riscv64-unknown-elf-nm a.out
@@ -232,7 +230,7 @@ riscv64-unknown-elf-nm a.out
232
230
233
231
### Setting the base address
234
232
235
-
Again referencing [this resource](https://www.lammertbies.nl/comm/info/serial-uart.html), NS16550A UARTs have twelve registers, each accessible from some number byte offset of the base address. We will be digging into these registers to implement `uart_put_char` and `uart_get_char`, but first we'll need to define a symbol representing this base address for use in our driver. As we discovered from the decompiled devicetree file above, `riscv64-virt.dts`, the base address is located at `0x00 + 0x10000000 = 0x10000000`, as that is what is in the `reg` property:
233
+
Again referencing [this resource](https://www.lammertbies.nl/comm/info/serial-uart.html), NS16550A UARTs have twelve registers, each accessible from some number byte offset of the base address. In order to be able to get at these registers from our driver code, we'll first need to define a symbol representing this base address. As we discovered from the decompiled devicetree file above, `riscv64-virt.dts`, the base address is located at `0x00 + 0x10000000 = 0x10000000`, as that is what is in the `reg` property:
0 commit comments