-
Notifications
You must be signed in to change notification settings - Fork 13
Add RFC for an UART peripheral. #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
jfng
wants to merge
9
commits into
amaranth-lang:main
Choose a base branch
from
jfng:soc-uart-peripheral
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 8 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
0858791
Add RFC for an UART peripheral.
jfng ae049e6
RFC #60: fix data stream signature and clock divisor.
jfng 5c324aa
RFC #60: remove white background of SVG diagrams.
jfng eb38d71
RFC #60: fix SVG backgrounds.
jfng d2c1834
RFC #60: address some feedback.
jfng 705e307
RFC #60: fix formatting.
jfng 5638b9d
RFC #60: add a `rst` member to the peripheral signature.
jfng 64a9e12
RFC #60: rename `symbol` to `symbols` and `bus` to `csr_bus`.
jfng 0066536
RFC #60: replace `Control/`Divisor` with `Config`/`PhyConfig`.
jfng File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,357 @@ | ||
| - Start Date: (fill me in with today's date, YYYY-MM-DD) | ||
| - RFC PR: [amaranth-lang/rfcs#60](https://github.com/amaranth-lang/rfcs/pull/60) | ||
| - Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000) | ||
|
|
||
| # UART peripheral RFC | ||
|
|
||
| ## Summary | ||
| [summary]: #summary | ||
|
|
||
| Add a SoC peripheral for UART devices. | ||
|
|
||
| ## Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| An UART is a generally useful peripheral for serial communication between devices. | ||
|
|
||
| ## Guide-level explanation | ||
| [guide-level-explanation]: #guide-level-explanation | ||
|
|
||
| ### Usage | ||
|
|
||
| ```python3 | ||
| from amaranth import * | ||
| from amaranth.lib import wiring | ||
| from amaranth.lib.wiring import connect | ||
|
|
||
| from amaranth_stdio.serial import AsyncSerialRX, AsyncSerialTX | ||
|
|
||
| from amaranth_soc import csr | ||
| from amaranth_soc import uart | ||
|
|
||
|
|
||
| class MySoC(wiring.Component): | ||
| def elaborate(self, platform): | ||
| m = Module() | ||
|
|
||
| # ... | ||
|
|
||
| # Instantiate an UART peripheral: | ||
|
|
||
| uart_divisor = int(platform.default_clk_frequency / 115200) | ||
|
|
||
| m.submodules.uart = uart = uart.Peripheral(divisor_init=uart_divisor, addr_width=8, data_width=8) | ||
|
|
||
| # Instantiate and connect the UART PHYs: | ||
|
|
||
| uart_pins = platform.request("uart", 0) | ||
|
|
||
| uart_phy_rx = AsyncSerialRX(uart_divisor, divisor_bits=16, pins=uart_pins) | ||
| uart_phy_tx = AsyncSerialTX(uart_divisor, divisor_bits=16, pins=uart_pins) | ||
|
|
||
| m.submodules.uart_phy_rx = ResetInserter(uart.rx.rst)(uart_phy_rx) | ||
| m.submodules.uart_phy_tx = ResetInserter(uart.tx.rst)(uart_phy_tx) | ||
|
|
||
| m.d.comb += [ | ||
| uart_phy_rx.divisor.eq(uart.rx.divisor), | ||
|
|
||
| uart.rx.symbols.payload.eq(uart_phy_rx.data), | ||
| uart.rx.symbols.valid.eq(uart_phy_rx.rdy), | ||
| uart_phy_rx.ack.eq(uart.rx.symbols.ready), | ||
|
|
||
| uart.rx.overflow.eq(uart_phy_rx.err.overflow), | ||
| uart.rx.error.eq(uart_phy_rx.err.frame), | ||
| ] | ||
|
|
||
| m.d.comb += [ | ||
| uart_phy_tx.divisor.eq(uart.tx.divisor), | ||
|
|
||
| uart_phy_tx.data.eq(uart.tx.symbols.payload), | ||
| uart_phy_tx.ack.eq(uart.tx.symbols.valid), | ||
| uart.tx.symbols.ready.eq(uart_phy_tx.rdy), | ||
| ] | ||
|
|
||
| # Add the UART peripheral to a CSR bus decoder: | ||
|
|
||
| m.submodules.csr_decoder = csr_decoder = csr.Decoder(addr_width=31, data_width=8) | ||
|
|
||
| csr_decoder.add(uart.csr_bus, addr=0x1000) | ||
|
|
||
| # ... | ||
|
|
||
| return m | ||
|
|
||
| ``` | ||
|
|
||
| ### Registers | ||
|
|
||
| #### Receiver | ||
|
|
||
| ##### Control (read/write) | ||
|
|
||
| <img src="./0060-soc-uart-peripheral/reg-control.svg" | ||
| alt="bf([ | ||
| { name: 'enable', bits: 1, attr: 'RW' }, | ||
| { bits: 7, attr: 'ResR0W0' }, | ||
| ], {bits: 8})"> | ||
|
|
||
| `Control.enable` is initialized to 0 on reset. | ||
|
|
||
| - If `Control.enable` is 0, the receiver PHY should be held in reset state. | ||
| - If `Control.enable` is 1, the receiver PHY should operate normally. | ||
|
|
||
| ##### Status (read/write) | ||
|
|
||
| <img src="./0060-soc-uart-peripheral/reg-rx-status.svg" | ||
| alt="bf([ | ||
| { name: 'ready', bits: 1, attr: 'R' }, | ||
| { name: 'overflow', bits: 1, attr: 'RW1C' }, | ||
| { name: 'error', bits: 1, attr: 'RW1C' }, | ||
| { bits: 5, attr: 'ResR0W0' }, | ||
| ], {bits: 8})"> | ||
|
|
||
| - `Status.ready` indicates that the receive buffer contains at least one character. | ||
| - `Status.overflow` is set and latched if a new frame was received while the receive buffer is full. | ||
| - `Status.error` is set and latched if any implementation-specific error condition occured. | ||
|
|
||
| `Status.overflow` and `Status.error` are initialized to 0 on reset. | ||
|
|
||
| The value of the `Status` register is not affected by `Control.enable`. | ||
|
|
||
| ##### Divisor (read/write) | ||
|
|
||
| <img src="./0060-soc-uart-peripheral/reg-divisor.svg" | ||
| alt="bf([ | ||
| {name: 'divisor', bits: 16, attr: 'RW'}, | ||
| ], {bits: 16})"> | ||
|
|
||
| The `Divisor` register exposes an implementation-specific mechanism to control the receiver baudrate. | ||
|
|
||
| For example, assuming a clock frequency of 100MHz, an implementation may configure its baudrate like so: | ||
|
|
||
| ```python3 | ||
| baudrate = int(100e6 / divisor) | ||
| ``` | ||
|
|
||
| An implementation may also choose to ignore the `Divisor` register and configure the baudrate through unspecified means. | ||
|
|
||
| - If `Control.enable` is 0, `Divisor` is read/write. | ||
| - If `Control.enable` is 1, `Divisor` is read-only. | ||
|
|
||
| `Divisor` is initialized to `divisor_init` on reset. | ||
|
|
||
| ##### Data (read-only) | ||
|
|
||
| <img src="./0060-soc-uart-peripheral/reg-rx-data.svg" | ||
| alt="bf([ | ||
| {name: 'data', bits: 8, attr: 'R'}, | ||
| ], {bits: 8})"> | ||
|
|
||
| - If `Control.enable` is 0 or `Status.ready` is 0: | ||
| * reading from `Data` has no side-effect and returns an unspecified value. | ||
| - If `Control.enable` is 1 and `Status.ready` is 1: | ||
| * reading from `Data` consumes one symbol from the receive buffer and returns it. | ||
|
|
||
| #### Transmitter | ||
|
|
||
| ##### Control (read/write) | ||
|
|
||
| <img src="./0060-soc-uart-peripheral/reg-control.svg" | ||
| alt="bf([ | ||
| { name: 'enable', bits: 1, attr: 'RW' }, | ||
| { bits: 7, attr: 'ResR0W0' }, | ||
| ], {bits: 1})"> | ||
|
|
||
| `Control.enable` is initialized to 0 on reset. | ||
|
|
||
| - If `Control.enable` is 0, the transmitter PHY should be held in reset state. | ||
| - If `Control.enable` is 1, the transmitter PHY should operate normally. | ||
|
|
||
| ##### Status (read-only) | ||
|
|
||
| <img src="./0060-soc-uart-peripheral/reg-tx-status.svg" | ||
| alt="bf([ | ||
| { name: 'ready', bits: 1, attr: 'R' }, | ||
| { bits: 7, attr: 'ResR0W0' }, | ||
| ], {bits: 8})"> | ||
|
|
||
| - `Status.ready` indicates that the transmit buffer has available space for at least one character. | ||
|
|
||
| ##### Divisor (read/write) | ||
|
|
||
| <img src="./0060-soc-uart-peripheral/reg-divisor.svg" | ||
| alt="bf([ | ||
| {name: 'divisor', bits: 16, attr: 'RW'}, | ||
| ], {bits: 16})"> | ||
|
|
||
| The `Divisor` register exposes an implementation-specific mechanism to control the transmitter baudrate. | ||
|
|
||
| For example, assuming a clock frequency of 100MHz, an implementation may configure its baudrate like so: | ||
|
|
||
| ```python3 | ||
| baudrate = int(100e6 / divisor) | ||
| ``` | ||
|
|
||
| An implementation may also choose to ignore the `Divisor` register and configure the baudrate through unspecified means. | ||
|
|
||
| - If `Control.enable` is 0, `Divisor` is read/write. | ||
| - If `Control.enable` is 1, `Divisor` is read-only. | ||
|
|
||
| `Divisor` is initialized to `divisor_init` on reset. | ||
|
|
||
| ##### Data (write-only) | ||
|
|
||
| <img src="./0060-soc-uart-peripheral/reg-tx-data.svg" | ||
| alt="bf([ | ||
| {name: 'data', bits: 8, attr: 'W'}, | ||
| ], {bits: 8})"> | ||
|
|
||
| - If `Status.rdy` is 1, writing to `Data` adds one character to the transmit buffer. | ||
|
|
||
| - If `Control.enable` is 0 or `Status.ready` is 0: | ||
| * writing to `Data` has no side-effect. | ||
| - If `Control.enable` is 1 and `Status.ready` is 1: | ||
| * writing to `Data` adds one symbol to the transmit buffer. | ||
|
|
||
| ## Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| ### `amaranth_soc.uart.ReceiverPHYSignature` | ||
|
|
||
| The `uart.ReceiverPHYSignature` class is a `wiring.Signature` describing the interface between the UART peripheral and its receiver PHY, with: | ||
| - a `.__init__(self, *, symbol_width)` constructor, where `symbol_width` is a positive integer. | ||
|
|
||
| Its members are defined as follows: | ||
|
|
||
| ```python3 | ||
| { | ||
| "rst": Out(1), | ||
| "divisor": Out(unsigned(16)), | ||
| "symbols": In(wiring.Signature({ | ||
| "payload": Out(unsigned(symbol_width)), | ||
| "valid": Out(1), | ||
| "ready": In(1), | ||
| })), | ||
| "overflow": In(1), | ||
| "error": In(1), | ||
| } | ||
| ``` | ||
|
|
||
| The `rst` port is driven to 1 if `Control.enable` is 0, and 0 if `Control.enable` is 1. | ||
|
|
||
| ### `amaranth_soc.uart.TransmitterPHYSignature` | ||
|
|
||
| The `uart.TransmitterSignature` class is a `wiring.Signature` describing the interface between the UART peripheral and its transmitter PHY, with: | ||
| - a `.__init__(self, *, symbol_width)` constructor, where `symbol_width` is a positive integer. | ||
|
|
||
| Its members are defined as follows: | ||
|
|
||
| ```python3 | ||
| { | ||
| "rst": Out(1), | ||
| "divisor": Out(unsigned(16)), | ||
| "symbols": Out(wiring.Signature({ | ||
| "payload": Out(unsigned(symbol_width)), | ||
| "valid": Out(1), | ||
| "ready": In(1), | ||
| })), | ||
| } | ||
| ``` | ||
|
|
||
| The `rst` port is driven to 1 if `Control.enable` is 0, and 0 if `Control.enable` is 1. | ||
|
|
||
| ### `amaranth_soc.uart.ReceiverPeripheral` | ||
|
|
||
| The `uart.ReceiverPeripheral` class is a `wiring.Component` implementing the receiver of an UART peripheral, with: | ||
| - a `.__init__(self, *, divisor_init, addr_width, data_width=8, name=None, symbol_width=8)` constructor, where: | ||
| * `divisor_init` is a positive integer used as initial values for `Divisor.divisor`. | ||
| * `addr_width`, `data_width` and `name` are passed to a `csr.Builder`. | ||
| * `symbol_width` is a positive integer passed to `Data` and `ReceiverPHYSignature`. | ||
| - a `.signature` property, that returns a `wiring.Signature` with the following members: | ||
|
|
||
| ```python3 | ||
| { | ||
| "csr_bus": In(csr.Signature(addr_width, data_width)), | ||
| "phy": Out(ReceiverPHYSignature(symbol_width)), | ||
| } | ||
| ``` | ||
|
|
||
| ### `amaranth_soc.uart.TransmitterPeripheral` | ||
|
|
||
| The `uart.TransmitterPeripheral` class is a `wiring.Component` implementing the transmitter of an UART peripheral, with: | ||
| - a `.__init__(self, *, divisor_init, addr_width, data_width=8, name=None, symbol_width=8)` constructor, where: | ||
| * `divisor_init` is a positive integer used as initial values for `Divisor.divisor`. | ||
| * `addr_width`, `data_width` and `name` are passed to a `csr.Builder`. | ||
| * `symbol_width` is a positive integer passed to `Data` and `TransmitterPHYSignature`. | ||
| - a `.signature` property, that returns a `wiring.Signature` with the following members: | ||
|
|
||
| ```python3 | ||
| { | ||
| "csr_bus": In(csr.Signature(addr_width, data_width)), | ||
| "phy": Out(TransmitterPHYSignature(symbol_width)), | ||
| } | ||
| ``` | ||
|
|
||
| ### `amaranth_soc.uart.Peripheral` | ||
|
|
||
| The `uart.Peripheral` class is a `wiring.Component` implementing an UART peripheral, with: | ||
| - a `.__init__(self, *, divisor_init, addr_width, data_width=8, name=None, symbol_width=8)` constructor, where: | ||
| * `divisor_init` is a positive integer used as initial values for `Divisor.divisor`. | ||
| * `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`. | ||
| * `symbol_width` is a positive integer passed to `ReceiverPeripheral`, `TransmitterPeripheral`, `ReceiverPHYSignature` and `TransmitterPHYSignature`. | ||
|
|
||
| - a `.signature` property, that returns a `wiring.Signature` with the following members: | ||
|
|
||
| ```python3 | ||
| { | ||
| "csr_bus": In(csr.Signature(addr_width, data_width)), | ||
| "rx": Out(ReceiverPHYSignature(symbol_width)), | ||
| "tx": Out(TransmitterPHYSignature(symbol_width)), | ||
| } | ||
| ``` | ||
|
|
||
| ## Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| - This design decouples the UART peripheral from its PHY, which must be provided by the user. | ||
| - The receiver and transmitter have separate `Divider` registers, despite using identical values | ||
| in most cases. | ||
| - Configuring the baudrate through the `Divisor` register requires knowledge of the clock frequency used by the peripheral. | ||
|
|
||
| ## Rationale and alternatives | ||
| [rationale-and-alternatives]: #rationale-and-alternatives | ||
|
|
||
| - This design is intended to be minimal yet useful for the most common use-cases (i.e. 8-N-1). | ||
| - Decoupling the peripheral from the PHY allows flexibility in implementations. For example, it is easy to add FIFOs between the PHYs and the peripheral. | ||
| - A standalone `ReceiverPeripheral` or `TransmitterPeripheral` can be instantiated. | ||
|
|
||
| - The choice of a 16-bit divisor with an (otherwise) unspecified encoding allows implementation freedom: | ||
| * some may not care about a clock divisor at all (e.g. a behavioral model of an UART PHY, interfacing with a pseudo-TTY). | ||
| * some may provide their own divisor encoding scheme (e.g. a 13-bit base value with a 3-bit scale, that can cover common frequency/baudrate combinations with a [<1% error rate](https://github.com/amaranth-lang/rfcs/files/14672989/baud.py.txt) (credit: [@whitequark](https://github.com/whitequark))). | ||
|
|
||
|
|
||
| - As an alternative: | ||
| * implement the PHY in the peripheral itself, and expose pin interfaces in a similar manner as the GPIO peripheral of [RFC 49](https://amaranth-lang.org/rfcs/0049-soc-gpio-peripheral.html). | ||
|
|
||
| ## Prior art | ||
| [prior-art]: #prior-art | ||
|
|
||
| UART peripherals are commonly found in microcontrollers. | ||
|
|
||
| ## Unresolved questions | ||
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| None. | ||
|
|
||
| ## Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
|
|
||
| - Add a separate 16550-compatible UART peripheral. | ||
| - Add a separate peripheral with additional features, such as: | ||
| * parity | ||
| * auto baudrate | ||
| * oversampling | ||
| * hardware flow control | ||
| * interrupts | ||
| * DMA | ||
| - Add support for interrupts to this peripheral. | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.