Skip to content

Commit 5f89cd4

Browse files
committed
Add RFC for an UART peripheral.
1 parent 2c7ad0a commit 5f89cd4

8 files changed

+336
-0
lines changed

text/0000-soc-uart-peripheral.md

+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
- Start Date: (fill me in with today's date, YYYY-MM-DD)
2+
- RFC PR: [amaranth-lang/rfcs#0000](https://github.com/amaranth-lang/rfcs/pull/0000)
3+
- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000)
4+
5+
# UART peripheral RFC
6+
7+
## Summary
8+
[summary]: #summary
9+
10+
Add a SoC peripheral for UART devices.
11+
12+
## Motivation
13+
[motivation]: #motivation
14+
15+
An UART is a generally useful peripheral for serial communication between devices.
16+
17+
## Guide-level explanation
18+
[guide-level-explanation]: #guide-level-explanation
19+
20+
### Usage
21+
22+
```python3
23+
from amaranth import *
24+
from amaranth.lib import wiring
25+
from amaranth.lib.wiring import connect
26+
27+
from amaranth_stdio.serial import AsyncSerialRX, AsyncSerialTX
28+
29+
from amaranth_soc import csr
30+
from amaranth_soc import uart
31+
32+
33+
class MySoC(wiring.Component):
34+
def elaborate(self, platform):
35+
m = Module()
36+
37+
# ...
38+
39+
# Instantiate an UART peripheral:
40+
41+
uart_divisor = int(platform.default_clk_frequency / 115200)
42+
43+
m.submodules.uart = uart = uart.Peripheral(divisor_init=uart_divisor, addr_width=8, data_width=8)
44+
45+
# Instantiate and connect the UART PHYs:
46+
47+
uart_pins = platform.request("uart", 0)
48+
49+
uart_phy_rx = AsyncSerialRX(uart_divisor, divisor_bits=16, pins=uart_pins)
50+
uart_phy_tx = AsyncSerialTX(uart_divisor, divisor_bits=16, pins=uart_pins)
51+
52+
m.submodules.uart_phy_rx = uart_phy_rx
53+
m.submodules.uart_phy_tx = uart_phy_tx
54+
55+
m.d.comb += [
56+
uart_phy_rx.divisor.eq(uart.rx.divisor),
57+
58+
uart.rx.stream.data.eq(uart_phy_rx.data),
59+
uart.rx.stream.valid.eq(uart_phy_rx.rdy),
60+
uart_phy_rx.ack.eq(uart.stream.ready),
61+
62+
uart.rx.overflow.eq(uart_phy_rx.err.overflow),
63+
uart.rx.error.eq(uart_phy_rx.err.frame),
64+
]
65+
66+
m.d.comb += [
67+
uart_phy_tx.divisor.eq(uart.tx.divisor),
68+
69+
uart_phy_tx.data.eq(uart.tx.stream.data),
70+
uart_phy_tx.ack.eq(uart.tx.stream.valid),
71+
uart.tx.stream.ready.eq(uart_phy_tx.rdy),
72+
]
73+
74+
# Add the UART peripheral to a CSR bus decoder:
75+
76+
m.submodules.csr_decoder = csr_decoder = csr.Decoder(addr_width=31, data_width=8)
77+
78+
csr_decoder.add(uart.bus, addr=0x1000)
79+
80+
# ...
81+
82+
return m
83+
84+
```
85+
86+
### Registers
87+
88+
#### Receiver
89+
90+
##### Enable (read/write)
91+
92+
<img src="./0000-soc-uart-peripheral/reg-enable.svg"
93+
alt="bf([
94+
{name: 'en', bits: 1, attr: 'RW'},
95+
], {bits: 1})">
96+
97+
- If `Enable.en` is 0, the receiver is held in reset state.
98+
99+
- `Enable.en` is initialized to 0.
100+
101+
##### Divisor (read/write)
102+
103+
<img src="./0000-soc-uart-peripheral/reg-divisor.svg"
104+
alt="bf([
105+
{name: 'cnt', bits: 13, attr: 'RW'},
106+
{name: 'psc', bits: 3, attr: 'RW'},
107+
], {bits: 16})">
108+
109+
- If `Enable.en` is 0, `Divisor` is read-only.
110+
111+
- `Divisor.cnt` is initialized to `divisor_cnt_init`.
112+
- `Divisor.psc` is initialized to `divisor_psc_init`.
113+
114+
Assuming a clock frequency of 100MHz, the receiver baudrate is computed like so:
115+
116+
```python3
117+
baudrate = ((1 << psc) * 100e6) // (cnt + 1)
118+
```
119+
120+
##### Status (read/write)
121+
122+
<img src="./0000-soc-uart-peripheral/reg-rx-status.svg"
123+
alt="bf([
124+
{ name: 'rdy', bits: 1, attr: 'R' },
125+
{ name: 'ovf', bits: 1, attr: 'RW1C' },
126+
{ name: 'err', bits: 1, attr: 'RW1C' },
127+
{ bits: 5, attr: 'ResR0W0' },
128+
], {bits: 8})">
129+
130+
- `Status.rdy` indicates that the receive buffer contains at least one character.
131+
- `Status.ovf` is set if a new frame was received while the receive buffer is full.
132+
- `Status.err` is set if any implementation-specific error condition occured.
133+
134+
- `Status.ovf` and `Status.err` are initialized to 0.
135+
136+
##### Data (read-only)
137+
138+
<img src="./0000-soc-uart-peripheral/reg-rx-data.svg"
139+
alt="bf([
140+
{name: 'data', bits: 8, attr: 'R'},
141+
], {bits: 8})">
142+
143+
- If `Status.rdy` is 1, reading from `Data` consumes one character from the receive buffer.
144+
145+
#### Transmitter
146+
147+
##### Enable (read/write)
148+
149+
<img src="./0000-soc-uart-peripheral/reg-enable.svg"
150+
alt="bf([
151+
{name: 'en', bits: 1, attr: 'RW'},
152+
], {bits: 1})">
153+
154+
- If `Enable.en` is 0, the transmitter is held in reset state.
155+
156+
- `Enable.en` is initialized to 0.
157+
158+
##### Divisor (read/write)
159+
160+
<img src="./0000-soc-uart-peripheral/reg-divisor.svg"
161+
alt="bf([
162+
{name: 'cnt', bits: 13, attr: 'RW'},
163+
{name: 'psc', bits: 3, attr: 'RW'},
164+
], {bits: 16})">
165+
166+
- If `Enable.en` is 0, `Divisor` is read-only.
167+
168+
- `Divisor.cnt` is initialized to `divisor_cnt_init`.
169+
- `Divisor.psc` is initialized to `divisor_psc_init`.
170+
171+
Assuming a clock frequency of 100MHz, the transmitter baudrate is computed like so:
172+
173+
```python3
174+
baudrate = ((2 ** psc) * 100e6) // (cnt + 1)
175+
```
176+
177+
##### Status (read-only)
178+
179+
<img src="./0000-soc-uart-peripheral/reg-tx-status.svg"
180+
alt="bf([
181+
{ name: 'rdy', bits: 1, attr: 'R' },
182+
{ bits: 7, attr: 'ResR0W0' },
183+
], {bits: 8})">
184+
185+
- `Status.rdy` indicates that the transmit buffer has available space for at least one character.
186+
187+
##### Data (write-only)
188+
189+
<img src="./0000-soc-uart-peripheral/reg-tx-data.svg"
190+
alt="bf([
191+
{name: 'data', bits: 8, attr: 'W'},
192+
], {bits: 8})">
193+
194+
- If `Status.rdy` is 1, writing to `Data` adds one character to the transmit buffer.
195+
196+
## Reference-level explanation
197+
[reference-level-explanation]: #reference-level-explanation
198+
199+
### `amaranth_soc.uart.ReceiverPHYSignature`
200+
201+
The `uart.ReceiverPHYSignature` class is a `wiring.Signature` describing the interface between the UART peripheral and its receiver PHY, with:
202+
- a `.__init__(self, *, data_bits)` constructor, where `data_bits` is a non-negative integer.
203+
204+
Its members are defined as follows:
205+
206+
```python3
207+
{
208+
"divisor": In(unsigned(20)),
209+
"stream": Out(wiring.Signature({
210+
"data": Out(unsigned(data_bits)),
211+
"valid": Out(unsigned(1)),
212+
"ready": In(unsigned(1)),
213+
})),
214+
"overflow": Out(unsigned(1)),
215+
"error": Out(unsigned(1)),
216+
}
217+
```
218+
219+
### `amaranth_soc.uart.TransmitterPHYSignature`
220+
221+
The `uart.TransmitterSignature` class is a `wiring.Signature` describing the interface between the UART peripheral and its transmitter PHY, with:
222+
- a `.__init__(self, *, data_bits)` constructor, where `data_bits` is a non-negative integer.
223+
224+
Its members are defined as follows:
225+
226+
```python3
227+
{
228+
"divisor": In(unsigned(20)),
229+
"stream": In(wiring.Signature({
230+
"data": Out(unsigned(data_bits)),
231+
"valid": Out(unsigned(1)),
232+
"ready": In(unsigned(1)),
233+
})),
234+
}
235+
```
236+
237+
### `amaranth_soc.uart.ReceiverPeripheral`
238+
239+
The `uart.ReceiverPeripheral` class is a `wiring.Component` implementing the receiver of an UART peripheral, with:
240+
- a `.__init__(self, *, divisor_init, addr_width, data_width=8, name=None, data_bits=8)` constructor, where:
241+
* `divisor_init` is a positive integer used as initial value for `Divisor`. It is a 16-bit value, where the lower 13 bits are assigned to `Divisor.cnt`, and the upper 3 bits are assigned to `Divisor.psc` as a log2.
242+
* `addr_width`, `data_width` and `name` are passed to a `csr.Builder`.
243+
* `data_bits` is a non-negative integer passed to `Data` and `ReceiverPHYSignature`.
244+
- a `.signature` property, that returns a `wiring.Signature` with the following members:
245+
246+
```python3
247+
{
248+
"bus": In(csr.Signature(addr_width, data_width)),
249+
"phy": In(ReceiverPHYSignature(data_bits)),
250+
}
251+
```
252+
253+
### `amaranth_soc.uart.TransmitterPeripheral`
254+
255+
The `uart.TransmitterPeripheral` class is a `wiring.Component` implementing the transmitter of an UART peripheral, with:
256+
- a `.__init__(self, *, divisor_init, addr_width, data_width=8, name=None, data_bits=8)` constructor, where:
257+
* `divisor_init` is a positive integer used as initial value for `Divisor`. It is a 16-bit value, where the lower 13 bits are assigned to `Divisor.cnt`, and the upper 3 bits are assigned to `Divisor.psc` as a log2.
258+
* `addr_width`, `data_width` and `name` are passed to a `csr.Builder`.
259+
* `data_bits` is a non-negative integer passed to `Data` and `TransmitterPHYSignature`.
260+
- a `.signature` property, that returns a `wiring.Signature` with the following members:
261+
262+
```python3
263+
{
264+
"bus": In(csr.Signature(addr_width, data_width)),
265+
"phy": In(TransmitterPHYSignature(data_bits)),
266+
}
267+
```
268+
269+
### `amaranth_soc.uart.Peripheral`
270+
271+
The `uart.Peripheral` class is a `wiring.Component` implementing an UART peripheral, with:
272+
- a `.__init__(self, *, divisor_init, addr_width, data_width=8, name=None, data_bits=8)` constructor, where:
273+
* `divisor_init` is a positive integer used as initial value for `Divisor`. It is a 16-bit value, where the lower 13 bits are assigned to `Divisor.cnt`, and the upper 3 bits are assigned to `Divisor.psc` as a log2.
274+
* `addr_width`, `data_width` and `name` are passed to a `csr.Builder`. `addr_width` must be at least 1. The peripheral address space is split in two, with the lower half occupied by a `ReceiverPeripheral` and the upper by a `TransmitterPeripheral`.
275+
* `data_bits` is a non-negative integer passed to `ReceiverPeripheral`, `TransmitterPeripheral`, `ReceiverPHYSignature` and `TransmitterPHYSignature`.
276+
277+
- a `.signature` property, that returns a `wiring.Signature` with the following members:
278+
279+
```python3
280+
{
281+
"bus": In(csr.Signature(addr_width, data_width)),
282+
"rx": In(ReceiverPHYSignature(data_bits)),
283+
"tx": In(TransmitterPHYSignature(data_bits)),
284+
}
285+
```
286+
287+
## Drawbacks
288+
[drawbacks]: #drawbacks
289+
290+
- This design decouples the UART peripheral from its PHY, which must be provided by the user.
291+
- The receiver and transmitter have separate `Divider` registers, despite using identical values
292+
in most cases.
293+
- Configuring the baudrate through the `Divider` register requires knowledge of the clock frequency used by the peripheral.
294+
295+
## Rationale and alternatives
296+
[rationale-and-alternatives]: #rationale-and-alternatives
297+
298+
- This design is intended to be minimal and work reliably for the most common use-cases (i.e. 8-N-1).
299+
- Decoupling the peripheral from the PHY allows flexibility in implementations. For example, it is easy to add FIFOs between the PHYs and the peripheral.
300+
- A standalone `ReceiverPeripheral` or `TransmitterPeripheral` can be instantiated.
301+
302+
- The choice of a 16-bit `Divisor` register with a 3-bit prescaler covers the most common frequency/baudrate combinations with an error rate (due to quantization) below 1%.
303+
304+
*TODO: a table showing frequency/baudrate combinations*
305+
306+
- As an alternative:
307+
* implement the PHY in the peripheral itself, and expose pin interfaces in a similar manner as the GPIO peripheral of RFC 49.
308+
309+
## Prior art
310+
[prior-art]: #prior-art
311+
312+
UART peripherals are commonly found in microcontrollers.
313+
314+
## Unresolved questions
315+
[unresolved-questions]: #unresolved-questions
316+
317+
None.
318+
319+
## Future possibilities
320+
[future-possibilities]: #future-possibilities
321+
322+
- Add a separate 16550-compatible UART peripheral.
323+
- Expand this peripheral with additional features, such as:
324+
* parity
325+
* auto baudrate
326+
* oversampling
327+
* hardware flow control
328+
* interrupts
329+
* DMA
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)