Skip to content

Commit c1573d9

Browse files
authored
RFC amaranth-lang#49: GPIO peripheral
2 parents 0852d0e + cdb4b9c commit c1573d9

File tree

6 files changed

+250
-0
lines changed

6 files changed

+250
-0
lines changed

text/0049-soc-gpio-peripheral.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
- Start Date: 2024-03-08
2+
- RFC PR: [amaranth-lang/rfcs#49](https://github.com/amaranth-lang/rfcs/pull/49)
3+
- Amaranth Issue: [amaranth-lang/amaranth-soc#77](https://github.com/amaranth-lang/amaranth-soc/issues/77)
4+
5+
# GPIO peripheral RFC
6+
7+
## Summary
8+
[summary]: #summary
9+
10+
Add a SoC peripheral to control GPIO pins.
11+
12+
## Motivation
13+
[motivation]: #motivation
14+
15+
[GPIOs](https://en.wikipedia.org/wiki/General-purpose_input/output) are useful for a wide range of scenarios, such as driving external circuitry or acting as fallback for unimplemented/misbehaving peripherals in early iterations of a design.
16+
17+
Amaranth SoC seems like an appropriate place for a GPIO peripheral, which depends on features that are already provided by the library. Due to its relative simplicity, it is also a good candidate for using the recent CSR register API in realistic conditions.
18+
19+
## Guide-level explanation
20+
[guide-level-explanation]: #guide-level-explanation
21+
22+
### Usage
23+
24+
```python3
25+
from amaranth import *
26+
from amaranth.lib import wiring
27+
from amaranth.lib.wiring import connect
28+
29+
from amaranth_soc import csr
30+
from amaranth_soc import gpio
31+
32+
33+
class MySoC(wiring.Component):
34+
def elaborate(self, platform):
35+
m = Module()
36+
37+
# ...
38+
39+
# Use a GPIO peripheral to control four LEDs:
40+
41+
m.submodules.led_gpio = led_gpio = gpio.Peripheral(pin_count=4, addr_width=8, data_width=8)
42+
43+
for n in range(4):
44+
connect(m, led_gpio.pins[n], platform.request("led", n, dir="io"))
45+
46+
# Add the peripheral to a CSR bus decoder:
47+
48+
m.submodules.csr_decoder = csr_decoder = csr.Decoder(addr_width=31, data_width=8)
49+
50+
csr_decoder.add(led_gpio.bus, addr=0x1000)
51+
52+
# ...
53+
54+
return m
55+
```
56+
57+
### Overview
58+
59+
The following figure is a simplified diagram of the peripheral. CSR registers are on the left-hand side, a single pin is on the right side:
60+
61+
<img src="./0049-soc-gpio-peripheral/overview.svg">
62+
63+
### Registers
64+
65+
#### Mode (read/write)
66+
67+
<img src="./0049-soc-gpio-peripheral/reg-mode.svg"
68+
alt="bf([
69+
{name: 'pin_0', bits: 2, attr: 'RW'},
70+
{name: 'pin_1', bits: 2, attr: 'RW'},
71+
{name: 'pin_2', bits: 2, attr: 'RW'},
72+
{name: 'pin_3', bits: 2, attr: 'RW'},
73+
], {bits: 8})">
74+
75+
Each `Mode.pin_x` field can hold the following values:
76+
77+
```python3
78+
class Mode(enum.Enum, shape=unsigned(2)):
79+
INPUT_ONLY = 0b00
80+
PUSH_PULL = 0b01
81+
OPEN_DRAIN = 0b10
82+
ALTERNATE = 0b11
83+
```
84+
85+
Each `Mode.pin_x` field resets to `INPUT_ONLY`.
86+
87+
If `Mode.pin_x` is `INPUT_ONLY`:
88+
- `pins[x].oe` is 0.
89+
- `pins[x].o` is connected to `Output.pin_x`.
90+
- `Input.pin_x` is connected to `pins[x].i`.
91+
- `alt_mode[x]` is 0.
92+
93+
If `Mode.pin_x` is `PUSH_PULL`:
94+
- `pins[x].oe` is 1.
95+
- `pins[x].o` is conencted to `Output.pin_x`.
96+
- `Input.pin_x` is connected to `pins[x].i`.
97+
- `alt_mode[x]` is 0.
98+
99+
If `Mode.pin_x` is `OPEN_DRAIN`:
100+
- `pins[x].oe` is connected to `~Output.pin_x`.
101+
- `pins[x].o` is 0.
102+
- `Input.pin_x` is connected to `pins[x].i`.
103+
- `alt_mode[x]` is 0.
104+
105+
If `Mode.pin_x` is `ALTERNATE`:
106+
- `pins[x].oe` is 0.
107+
- `pins[x].o` is connected to `Output.pin_x`.
108+
- `Input.pin_x` is connected to `pins[x].i`.
109+
- `alt_mode[x]` is 1.
110+
111+
When `alt_mode[x]` is 1, a component connected to the GPIO peripheral (such as a pin multiplexer) may assign implementation-specific functions to `Input.pin_x` and `Output.pin_x`.
112+
113+
#### Input (read-only)
114+
115+
<img src="./0049-soc-gpio-peripheral/reg-input.svg"
116+
alt="bf([
117+
{name: 'pin_0', bits: 1, attr: 'R'},
118+
{name: 'pin_1', bits: 1, attr: 'R'},
119+
{name: 'pin_2', bits: 1, attr: 'R'},
120+
{name: 'pin_3', bits: 1, attr: 'R'},
121+
], {bits: 4})">
122+
123+
The number of synchronization stages between `pins[x].i` and `Input.pin_x` is defined by the `input_stages` parameter, which defaults to 2. Synchronization is done on rising edges of `ClockSignal("sync")`.
124+
125+
#### Output (read/write)
126+
127+
<img src="./0049-soc-gpio-peripheral/reg-output.svg"
128+
alt="bf([
129+
{name: 'pin_0', bits: 1, attr: 'RW'},
130+
{name: 'pin_1', bits: 1, attr: 'RW'},
131+
{name: 'pin_2', bits: 1, attr: 'RW'},
132+
{name: 'pin_3', bits: 1, attr: 'RW'},
133+
], {bits: 4})">
134+
135+
Each `Output.pin_x` field resets to 0.
136+
137+
#### SetClr (write-only)
138+
139+
<img src="./0049-soc-gpio-peripheral/reg-setclr.svg"
140+
alt="bf([
141+
{name: 'pin_0', bits: 2, attr: 'W'},
142+
{name: 'pin_1', bits: 2, attr: 'W'},
143+
{name: 'pin_2', bits: 2, attr: 'W'},
144+
{name: 'pin_3', bits: 2, attr: 'W'},
145+
], {bits: 8})">
146+
147+
- Writing `0b01` to `SetClr.pin_x` sets `Output.pin_x`.
148+
- Writing `0b10` to `SetClr.pin_x` clears `Output.pin_x`.
149+
- Writing `0b00` or `0b11` to `SetClr.pin_x` has no effect.
150+
151+
## Reference-level explanation
152+
[reference-level-explanation]: #reference-level-explanation
153+
154+
### `amaranth_soc.gpio.PinSignature`
155+
156+
The `gpio.PinSignature` class is a `wiring.Signature` describing the interface between the GPIO peripheral and a single pin.
157+
158+
The members of a `gpio.PinSignature` are defined as follows:
159+
160+
```python3
161+
{
162+
"i": In(unsigned(1)),
163+
"o": Out(unsigned(1)),
164+
"oe": Out(unsigned(1)),
165+
}
166+
```
167+
168+
### `amaranth_soc.gpio.Peripheral`
169+
170+
The `gpio.Peripheral` class is a `wiring.Component` implementing a GPIO controller, with:
171+
- a `.__init__(self, *, pin_count, addr_width, data_width, name=None, input_stages=2)` constructor, where:
172+
* `pin_count` is a non-negative integer.
173+
* `input_stages` is a non-negative integer.
174+
* `addr_width`, `data_width` and `name` are passed to a `csr.Builder`
175+
- a `.signature` property, that returns a `wiring.Signature` with the following members:
176+
177+
```python3
178+
{
179+
"bus": In(csr.Signature(addr_width, data_width)),
180+
"pins": Out(gpio.PinSignature()).array(pin_count),
181+
"alt_mode": Out(unsigned(pin_count)),
182+
}
183+
```
184+
185+
- a `.elaborate(self, platform)` method, that connects each pin in `self.pins` to its associated fields in the registers exposed by `self.bus`.
186+
187+
## Drawbacks
188+
[drawbacks]: #drawbacks
189+
190+
While existing implementations (such as STM32 GPIOs) have features like pin multiplexing and configurable pull-up/down resistors, in the proposed design, those would have to be implemented in a separate component.
191+
192+
## Rationale and alternatives
193+
[rationale-and-alternatives]: #rationale-and-alternatives
194+
195+
The proposed design moves platform-specific details outside of its scope, which:
196+
- reduces the amount of non-portable code to maintain, while allowing implementation freedom for users needing it.
197+
- avoids introducing dependencies on upstream APIs that are deprecated or expected to evolve soon (such as `amaranth.build`).
198+
199+
As an alternative:
200+
- do not host any peripheral in amaranth-soc and always develop them downstream.
201+
- include a pin multiplexer inside the GPIO peripheral.
202+
203+
## Prior art
204+
[prior-art]: #prior-art
205+
206+
While they can be found in most microcontollers, the design of GPIOs in STM32 has inspired part of this RFC.
207+
208+
## Unresolved questions
209+
[unresolved-questions]: #unresolved-questions
210+
211+
- ~~Should we support synchronizing a pin input on falling edges of the clock ?~~ (@whitequark) Users can synchronize pin inputs on falling edges by instantiating a `gpio.Peripheral` with `input_stages=0`, and providing their own synchronization mechanism.
212+
213+
- What is our policy for backward-compatible extensions of the peripheral ? (@whitequark) If or when we add registers for new optional features, such as pull-ups, switchable schmitt triggers, switchable output driver strengths, etc, each register will always reside at the same fixed (for a given pin count) address regardless of which features are enabled, and each of these registers will be all-0s after reset, where such all-0s value will provide behavior identical to the behavior of the peripheral without the optional feature. Slots in the address space will never be reallocated with a different meaning once allocated upstream in Amaranth SoC.
214+
* This will be important to industry users caring about forward and cross-family/cross-configuration compatibility.
215+
* In a perfect world this would be our policy for every peripheral. Realistically, we'll only be able to provide this strongest guarantee for only a subset of peripherals.
216+
217+
## Future possibilities
218+
[future-possibilities]: #future-possibilities
219+
220+
- Implement a pin multiplexer peripheral, that can be composed with this one to allow reusing other pins of a SoC as GPIOs.
221+
- Add support for interrupts.
222+
223+
## Acknowledgements
224+
225+
@whitequark and @tpwrules provided valuable feedback while this RFC was being drafted.

0 commit comments

Comments
 (0)