├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE.txt
├── README.md
├── amaranth_stdio
├── __init__.py
└── serial.py
├── docs
├── .gitignore
├── _static
│ ├── custom.css
│ └── logo.png
├── conf.py
├── cover.rst
└── index.rst
├── pdm_build.py
├── pyproject.toml
└── tests
├── __init__.py
└── test_serial.py
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | pull_request:
4 | merge_group:
5 | schedule:
6 | - cron: '0 0 * * *' # test daily against git HEAD of dependencies
7 |
8 | name: CI
9 | jobs:
10 |
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python-version:
16 | - '3.9'
17 | - '3.10'
18 | - '3.11'
19 | - '3.12'
20 | - '3.13'
21 | - 'pypy-3.9'
22 | - 'pypy-3.10'
23 | # this version range needs to be synchronized with the one in pyproject.toml
24 | amaranth-version:
25 | - '0.5'
26 | - 'git'
27 | allow-failure:
28 | - false
29 | continue-on-error: '${{ matrix.allow-failure }}'
30 | name: 'test (${{ matrix.python-version }}, HDL ${{ matrix.amaranth-version }})'
31 | steps:
32 | - name: Check out source code
33 | uses: actions/checkout@v4
34 | with:
35 | fetch-depth: 0
36 | - name: Set up PDM
37 | uses: pdm-project/setup-pdm@v4
38 | with:
39 | python-version: ${{ matrix.python-version }}
40 | - name: Install dependencies
41 | run: |
42 | pip install codecov
43 | pdm install --dev
44 | - name: Install Amaranth release
45 | if: ${{ matrix.amaranth-version != 'git' }}
46 | run: |
47 | pip install 'amaranth==${{ matrix.amaranth-version }}'
48 | - name: Install Amaranth from git
49 | if: ${{ matrix.amaranth-version == 'git' }}
50 | run: |
51 | pip install git+https://github.com/amaranth-lang/amaranth.git
52 | - name: Run tests
53 | run: |
54 | pdm run test
55 | - name: Submit code coverage
56 | run:
57 | codecov
58 |
59 | document:
60 | runs-on: ubuntu-latest
61 | steps:
62 | - name: Check out source code
63 | uses: actions/checkout@v4
64 | with:
65 | fetch-depth: 0
66 | - name: Fetch tags from upstream repository
67 | run: |
68 | git fetch --tags https://github.com/amaranth-lang/amaranth-stdio.git
69 | - name: Set up PDM
70 | uses: pdm-project/setup-pdm@v4
71 | with:
72 | python-version: '3.12'
73 | - name: Install dependencies
74 | run: |
75 | pdm install --dev
76 | - name: Build documentation
77 | run: |
78 | pdm run document
79 | - name: Upload documentation archive
80 | uses: actions/upload-artifact@v4
81 | with:
82 | name: docs
83 | path: docs/_build
84 |
85 | required: # group all required workflows into one to avoid reconfiguring this in Actions settings
86 | needs:
87 | - test
88 | - document
89 | if: always() && !contains(needs.*.result, 'cancelled')
90 | runs-on: ubuntu-latest
91 | steps:
92 | - run: ${{ contains(needs.*.result, 'failure') && 'false' || 'true' }}
93 |
94 | publish-docs:
95 | needs: document
96 | if: github.repository == 'amaranth-lang/amaranth-stdio'
97 | runs-on: ubuntu-latest
98 | steps:
99 | - name: Check out source code
100 | uses: actions/checkout@v4
101 | with:
102 | fetch-depth: 0
103 | - name: Download documentation archive
104 | uses: actions/download-artifact@v4
105 | with:
106 | name: docs
107 | path: docs/
108 | - name: Publish development documentation
109 | if: github.event_name == 'push' && github.event.ref == 'refs/heads/main'
110 | uses: JamesIves/github-pages-deploy-action@releases/v4
111 | with:
112 | repository-name: amaranth-lang/amaranth-lang.github.io
113 | ssh-key: ${{ secrets.PAGES_DEPLOY_KEY }}
114 | branch: main
115 | folder: docs/
116 | target-folder: docs/amaranth-stdio/latest/
117 | - name: Publish release documentation
118 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
119 | uses: JamesIves/github-pages-deploy-action@releases/v4
120 | with:
121 | repository-name: amaranth-lang/amaranth-lang.github.io
122 | ssh-key: ${{ secrets.PAGES_DEPLOY_KEY }}
123 | branch: main
124 | folder: docs/
125 | target-folder: docs/amaranth-stdio/${{ github.ref_name }}/
126 |
127 | publish-docs-dev:
128 | needs: document
129 | if: github.repository != 'amaranth-lang/amaranth-stdio'
130 | runs-on: ubuntu-latest
131 | steps:
132 | - name: Check out source code
133 | uses: actions/checkout@v4
134 | with:
135 | fetch-depth: 0
136 | - name: Download documentation archive
137 | uses: actions/download-artifact@v4
138 | with:
139 | name: docs
140 | path: pages/docs/${{ github.ref_name }}/
141 | - name: Disable Jekyll
142 | run: |
143 | touch pages/.nojekyll
144 | - name: Publish documentation for a branch
145 | uses: JamesIves/github-pages-deploy-action@releases/v4
146 | with:
147 | folder: pages/
148 | clean: false
149 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.egg-info
4 | /dist
5 |
6 | # pdm
7 | /.pdm-plugins
8 | /.pdm-python
9 | /.venv
10 | /pdm.lock
11 |
12 | # coverage
13 | /.coverage
14 | /htmlcov
15 |
16 | # tests
17 | *.vcd
18 | *.gtkw
19 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (C) 2019-2021 Amaranth HDL contributors
2 |
3 | Redistribution and use in source and binary forms, with or without modification,
4 | are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this
7 | list of conditions and the following disclaimer.
8 | 2. Redistributions in binary form must reproduce the above copyright notice,
9 | this list of conditions and the following disclaimer in the documentation
10 | and/or other materials provided with the distribution.
11 |
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Industry standard I/O for Amaranth HDL
2 |
3 | TODO
4 |
5 | ## License
6 |
7 | Amaranth is released under the very permissive two-clause BSD license. Under the terms of this license, you are authorized to use Amaranth for closed-source proprietary designs.
8 |
9 | See LICENSE file for full copyright and license info.
10 |
--------------------------------------------------------------------------------
/amaranth_stdio/__init__.py:
--------------------------------------------------------------------------------
1 | # Extract version for this package from the environment package metadata. This used to be a lot
2 | # more difficult in earlier Python versions, and the `__version__` field is a legacy of that time.
3 | import importlib.metadata
4 | try:
5 | __version__ = importlib.metadata.version(__package__)
6 | except importlib.metadata.PackageNotFoundError:
7 | # No importlib metadata for this package. This shouldn't normally happen, but some people
8 | # prefer not installing packages via pip at all. Although not recommended we still support it.
9 | __version__ = "unknown" # :nocov:
10 | del importlib
--------------------------------------------------------------------------------
/amaranth_stdio/serial.py:
--------------------------------------------------------------------------------
1 | from amaranth import *
2 | from amaranth.lib import enum, data, wiring
3 | from amaranth.lib.wiring import In, Out, connect, flipped
4 | from amaranth.lib.cdc import FFSynchronizer
5 | from amaranth.utils import bits_for
6 |
7 |
8 | __all__ = ["Parity", "AsyncSerialRX", "AsyncSerialTX", "AsyncSerial"]
9 |
10 |
11 | class Parity(enum.Enum):
12 | """Asynchronous serial parity mode."""
13 | NONE = "none"
14 | MARK = "mark"
15 | SPACE = "space"
16 | EVEN = "even"
17 | ODD = "odd"
18 |
19 | def _compute_bit(self, data):
20 | cast_data = Value.cast(data)
21 | if self == self.NONE:
22 | return Const(0, 0)
23 | if self == self.MARK:
24 | return Const(1, 1)
25 | if self == self.SPACE:
26 | return Const(0, 1)
27 | if self == self.EVEN:
28 | return cast_data.xor()
29 | if self == self.ODD:
30 | return ~cast_data.xor()
31 | assert False # :nocov:
32 |
33 |
34 | class _FrameLayout(data.StructLayout):
35 | def __init__(self, data_bits, parity):
36 | super().__init__({
37 | "start": unsigned(1),
38 | "data": unsigned(data_bits),
39 | "parity": unsigned(0 if parity == Parity.NONE else 1),
40 | "stop": unsigned(1),
41 | })
42 |
43 |
44 | class AsyncSerialRX(wiring.Component):
45 | class Signature(wiring.Signature):
46 | """Asynchronous serial receiver signature.
47 |
48 | Parameters
49 | ----------
50 | divisor : int
51 | Clock divisor initial value. Should be set to ``int(clk_frequency // baudrate)``.
52 | divisor_bits : int
53 | Clock divisor width. Optional. If omitted, ``bits_for(divisor)`` is used instead.
54 | data_bits : int
55 | Data bits per frame.
56 | parity : :class:`Parity`
57 | Parity mode.
58 |
59 | Interface attributes
60 | --------------------
61 | divisor : Signal, in
62 | Clock divisor.
63 | data : Signal, out
64 | Read data. Valid only when ``rdy`` is asserted.
65 | err.overflow : Signal, out
66 | Error flag. A new frame has been received, but the previous one was not acknowledged.
67 | err.frame : Signal, out
68 | Error flag. The received bits do not fit in a frame.
69 | err.parity : Signal, out
70 | Error flag. The parity check has failed.
71 | rdy : Signal, out
72 | Read strobe.
73 | ack : Signal, in
74 | Read acknowledge. Must be held asserted while data can be read out of the receiver.
75 | i : Signal, in
76 | Serial input.
77 |
78 | Raises
79 | ------
80 | See :meth:`AsyncSerialRX.Signature.check_parameters`.
81 | """
82 | def __init__(self, *, divisor, divisor_bits=None, data_bits=8, parity="none"):
83 | self.check_parameters(divisor=divisor, divisor_bits=divisor_bits, data_bits=data_bits,
84 | parity=parity)
85 | self._divisor = divisor
86 | self._divisor_bits = divisor_bits if divisor_bits is not None else bits_for(divisor)
87 | self._data_bits = data_bits
88 | self._parity = Parity(parity)
89 |
90 | super().__init__({
91 | "divisor": In(unsigned(self._divisor_bits), init=self._divisor),
92 | "data": Out(unsigned(self._data_bits)),
93 | "err": Out(data.StructLayout({"overflow": 1, "frame": 1, "parity": 1})),
94 | "rdy": Out(unsigned(1)),
95 | "ack": In(unsigned(1)),
96 | "i": In(unsigned(1), init=1),
97 | })
98 |
99 | @classmethod
100 | def check_parameters(cls, *, divisor, divisor_bits=None, data_bits=8, parity="none"):
101 | """Validate signature parameters.
102 |
103 | Raises
104 | ------
105 | :exc:`TypeError`
106 | If ``divisor`` is not an integer greater than or equal to 5.
107 | :exc:`TypeError`
108 | If ``divisor_bits`` is not `None` and not an integer greater than or equal to
109 | ``bits_for(divisor)``.
110 | :exc:`TypeError`
111 | If ``data_bits`` is not an integer greater than or equal to 0.
112 | :exc:`ValueError`
113 | If ``parity`` is not a :class:`Parity` member.
114 | """
115 | # The clock divisor must be >= 5 to keep the receiver FSM synchronized with its input
116 | # during a DONE->IDLE->BUSY transition.
117 | AsyncSerial.Signature._check_divisor(divisor, divisor_bits, min_divisor=5)
118 | if not isinstance(data_bits, int) or data_bits < 0:
119 | raise TypeError(f"Data bits must be a non-negative integer, not {data_bits!r}")
120 | # Raise a ValueError if parity is invalid.
121 | Parity(parity)
122 |
123 | @property
124 | def divisor(self):
125 | return self._divisor
126 |
127 | @property
128 | def divisor_bits(self):
129 | return self._divisor_bits
130 |
131 | @property
132 | def data_bits(self):
133 | return self._data_bits
134 |
135 | @property
136 | def parity(self):
137 | return self._parity
138 |
139 | def __eq__(self, other):
140 | """Compare signatures.
141 |
142 | Two signatures are equal if they have the same divisor value, divisor bits,
143 | data bits, and parity.
144 | """
145 | return (isinstance(other, AsyncSerialRX.Signature) and
146 | self.divisor == other.divisor and
147 | self.divisor_bits == other.divisor_bits and
148 | self.data_bits == other.data_bits and
149 | self.parity == other.parity)
150 |
151 | def __repr__(self):
152 | return f"AsyncSerialRX.Signature({self.members!r})"
153 |
154 | """Asynchronous serial receiver.
155 |
156 | Parameters
157 | ----------
158 | divisor : int
159 | Clock divisor initial value. Should be set to ``int(clk_frequency // baudrate)``.
160 | divisor_bits : int
161 | Clock divisor width. Optional. If omitted, ``bits_for(divisor)`` is used instead.
162 | data_bits : int
163 | Data bits per frame.
164 | parity : :class:`Parity`
165 | Parity mode.
166 | pins : :class:`amaranth.lib.io.Pin`
167 | UART pins. Optional. See :class:`amaranth_boards.resources.UARTResource` for layout.
168 | If provided, the ``i`` port of the receiver is internally connected to ``pins.rx.i``.
169 |
170 | Raises
171 | ------
172 | See :meth:`AsyncSerialRX.Signature.check_parameters`.
173 | """
174 | def __init__(self, *, divisor, divisor_bits=None, data_bits=8, parity="none", pins=None):
175 | super().__init__(self.Signature(divisor=divisor, divisor_bits=divisor_bits,
176 | data_bits=data_bits, parity=parity))
177 | self._pins = pins
178 |
179 | def elaborate(self, platform):
180 | m = Module()
181 |
182 | timer = Signal.like(self.divisor)
183 | shreg = Signal(_FrameLayout(len(self.data), self.signature.parity))
184 | bitno = Signal(range(len(shreg.as_value())))
185 |
186 | if self._pins is not None:
187 | m.submodules += FFSynchronizer(self._pins.rx.i, self.i, init=1)
188 |
189 | with m.FSM() as fsm:
190 | with m.State("IDLE"):
191 | with m.If(~self.i):
192 | m.d.sync += [
193 | bitno.eq(len(shreg.as_value()) - 1),
194 | timer.eq(self.divisor >> 1),
195 | ]
196 | m.next = "BUSY"
197 |
198 | with m.State("BUSY"):
199 | with m.If(timer != 0):
200 | m.d.sync += timer.eq(timer - 1)
201 | with m.Else():
202 | m.d.sync += [
203 | shreg.eq(Cat(shreg.as_value()[1:], self.i)),
204 | bitno.eq(bitno - 1),
205 | timer.eq(self.divisor - 1),
206 | ]
207 | with m.If(bitno == 0):
208 | m.next = "DONE"
209 |
210 | with m.State("DONE"):
211 | with m.If(self.ack):
212 | m.d.sync += [
213 | self.data.eq(shreg.data),
214 | self.err.frame .eq(~((shreg.start == 0) & (shreg.stop == 1))),
215 | self.err.parity.eq(~(shreg.parity ==
216 | self.signature.parity._compute_bit(shreg.data))),
217 | ]
218 | m.d.sync += self.err.overflow.eq(~self.ack)
219 | m.next = "IDLE"
220 |
221 | with m.If(self.ack):
222 | m.d.sync += self.rdy.eq(fsm.ongoing("DONE"))
223 |
224 | return m
225 |
226 |
227 | class AsyncSerialTX(wiring.Component):
228 | class Signature(wiring.Signature):
229 | """Asynchronous serial transmitter signature.
230 |
231 | Parameters
232 | ----------
233 | divisor : int
234 | Clock divisor initial value. Should be set to ``int(clk_frequency // baudrate)``.
235 | divisor_bits : int
236 | Clock divisor width. Optional. If omitted, ``bits_for(divisor)`` is used instead.
237 | data_bits : int
238 | Data bits per frame.
239 | parity : :class:`Parity`
240 | Parity mode.
241 |
242 | Interface attributes
243 | --------------------
244 | divisor : Signal, in
245 | Clock divisor.
246 | data : Signal, in
247 | Write data. Valid only when ``ack`` is asserted.
248 | rdy : Signal, out
249 | Write ready. Asserted when the transmitter is ready to transmit data.
250 | ack : Signal, in
251 | Write strobe. Data gets transmitted when both ``rdy`` and ``ack`` are asserted.
252 | o : Signal, out
253 | Serial output.
254 |
255 | Raises
256 | ------
257 | See :meth:`AsyncSerialTX.Signature.check_parameters`.
258 | """
259 | def __init__(self, *, divisor, divisor_bits=None, data_bits=8, parity="none"):
260 | self.check_parameters(divisor=divisor, divisor_bits=divisor_bits, data_bits=data_bits,
261 | parity=parity)
262 | self._divisor = divisor
263 | self._divisor_bits = divisor_bits if divisor_bits is not None else bits_for(divisor)
264 | self._data_bits = data_bits
265 | self._parity = Parity(parity)
266 |
267 | super().__init__({
268 | "divisor": In(unsigned(self._divisor_bits), init=self._divisor),
269 | "data": In(unsigned(self._data_bits)),
270 | "rdy": Out(unsigned(1)),
271 | "ack": In(unsigned(1)),
272 | "o": Out(unsigned(1), init=1),
273 | })
274 |
275 | @classmethod
276 | def check_parameters(cls, *, divisor, divisor_bits=None, data_bits=8, parity="none"):
277 | """Validate signature parameters.
278 |
279 | Raises
280 | ------
281 | :exc:`TypeError`
282 | If ``divisor`` is not an integer greater than or equal to 1.
283 | :exc:`TypeError`
284 | If ``divisor_bits`` is not `None` and not an integer greater than or equal to
285 | ``bits_for(divisor)``.
286 | :exc:`TypeError`
287 | If ``data_bits`` is not an integer greater than or equal to 0.
288 | :exc:`ValueError`
289 | If ``parity`` is not a :class:`Parity` member.
290 | """
291 | AsyncSerial.Signature._check_divisor(divisor, divisor_bits, min_divisor=1)
292 | if not isinstance(data_bits, int) or data_bits < 0:
293 | raise TypeError(f"Data bits must be a non-negative integer, not {data_bits!r}")
294 | # Raise a ValueError if parity is invalid.
295 | Parity(parity)
296 |
297 | @property
298 | def divisor(self):
299 | return self._divisor
300 |
301 | @property
302 | def divisor_bits(self):
303 | return self._divisor_bits
304 |
305 | @property
306 | def data_bits(self):
307 | return self._data_bits
308 |
309 | @property
310 | def parity(self):
311 | return self._parity
312 |
313 | def __eq__(self, other):
314 | """Compare signatures.
315 |
316 | Two signatures are equal if they have the same divisor value, divisor bits,
317 | data bits, and parity.
318 | """
319 | return (isinstance(other, AsyncSerialTX.Signature) and
320 | self.divisor == other.divisor and
321 | self.divisor_bits == other.divisor_bits and
322 | self.data_bits == other.data_bits and
323 | self.parity == other.parity)
324 |
325 | def __repr__(self):
326 | return f"AsyncSerialTX.Signature({self.members!r})"
327 |
328 | """Asynchronous serial transmitter.
329 |
330 | Parameters
331 | ----------
332 | divisor : int
333 | Clock divisor initial value. Should be set to ``int(clk_frequency // baudrate)``.
334 | divisor_bits : int
335 | Clock divisor width. Optional. If omitted, ``bits_for(divisor)`` is used instead.
336 | data_bits : int
337 | Data bits per frame.
338 | parity : :class:`Parity`
339 | Parity mode.
340 | pins : :class:`amaranth.lib.io.Pin`
341 | UART pins. Optional. See :class:`amaranth_boards.resources.UARTResource` for layout.
342 | If provided, the ``o`` port of the transmitter is internally connected to ``pins.tx.o``.
343 |
344 | Raises
345 | ------
346 | See :class:`AsyncSerialTX.Signature.check_parameters`.
347 | """
348 | def __init__(self, *, divisor, divisor_bits=None, data_bits=8, parity="none", pins=None):
349 | super().__init__(signature=self.Signature(divisor=divisor, divisor_bits=divisor_bits,
350 | data_bits=data_bits, parity=parity))
351 | self._pins = pins
352 |
353 | def elaborate(self, platform):
354 | m = Module()
355 |
356 | timer = Signal.like(self.divisor)
357 | shreg = Signal(_FrameLayout(len(self.data), self.signature.parity))
358 | bitno = Signal(range(len(shreg.as_value())))
359 |
360 | if self._pins is not None:
361 | m.d.comb += self._pins.tx.o.eq(self.o)
362 |
363 | with m.FSM():
364 | with m.State("IDLE"):
365 | m.d.comb += self.rdy.eq(1)
366 | with m.If(self.ack):
367 | m.d.sync += [
368 | shreg.start .eq(0),
369 | shreg.data .eq(self.data),
370 | shreg.parity.eq(self.signature.parity._compute_bit(self.data)),
371 | shreg.stop .eq(1),
372 | bitno.eq(len(shreg.as_value()) - 1),
373 | timer.eq(self.divisor - 1),
374 | ]
375 | m.next = "BUSY"
376 |
377 | with m.State("BUSY"):
378 | with m.If(timer != 0):
379 | m.d.sync += timer.eq(timer - 1)
380 | with m.Else():
381 | m.d.sync += [
382 | Cat(self.o, shreg).eq(shreg),
383 | bitno.eq(bitno - 1),
384 | timer.eq(self.divisor - 1),
385 | ]
386 | with m.If(bitno == 0):
387 | m.next = "IDLE"
388 |
389 | return m
390 |
391 |
392 | class AsyncSerial(wiring.Component):
393 | class Signature(wiring.Signature):
394 | """Asynchronous serial transceiver signature.
395 |
396 | Parameters
397 | ----------
398 | divisor : int
399 | Clock divisor initial value. Should be set to ``int(clk_frequency // baudrate)``.
400 | divisor_bits : int
401 | Clock divisor width. Optional. If omitted, ``bits_for(divisor)`` is used instead.
402 | data_bits : int
403 | Data bits per frame.
404 | parity : :class:`Parity`
405 | Parity mode.
406 |
407 | Interface attributes
408 | --------------------
409 | divisor : Signal, in
410 | Clock divisor. It is internally connected to ``rx.divisor`` and ``tx.divisor``.
411 | rx : :class:`wiring.Interface`
412 | Receiver interface. See :class:`AsyncSerialRX.Signature`.
413 | tx : :class:`wiring.Interface`
414 | Transmitter interface. See :class:`AsyncSerialTX.Signature`.
415 |
416 | Raises
417 | ------
418 | See :meth:`AsyncSerial.Signature.check_parameters`.
419 | """
420 | def __init__(self, *, divisor, divisor_bits=None, data_bits=8, parity="none"):
421 | rx_sig = AsyncSerialRX.Signature(divisor=divisor, divisor_bits=divisor_bits,
422 | data_bits=data_bits, parity=parity)
423 | tx_sig = AsyncSerialTX.Signature(divisor=divisor, divisor_bits=divisor_bits,
424 | data_bits=data_bits, parity=parity)
425 |
426 | assert rx_sig.members["divisor"] == tx_sig.members["divisor"]
427 | divisor_shape = rx_sig.members["divisor"].shape
428 | divisor_init = rx_sig.members["divisor"].init
429 |
430 | super().__init__({
431 | "divisor": In(divisor_shape, init=divisor_init),
432 | "rx": Out(rx_sig),
433 | "tx": Out(tx_sig),
434 | })
435 |
436 | @classmethod
437 | def _check_divisor(cls, divisor, divisor_bits, min_divisor=1):
438 | if not isinstance(divisor, int) or divisor < min_divisor:
439 | raise TypeError(f"Divisor initial value must be an integer greater than or equal "
440 | f"to {min_divisor}, not {divisor!r}")
441 | if divisor_bits is not None:
442 | min_divisor_bits = bits_for(divisor)
443 | if not isinstance(divisor_bits, int) or divisor_bits < min_divisor_bits:
444 | raise TypeError(f"Divisor bits must be an integer greater than or equal to "
445 | f"{min_divisor_bits}, not {divisor_bits!r}")
446 |
447 | @classmethod
448 | def check_parameters(cls, *, divisor, divisor_bits=None, data_bits=8, parity="none"):
449 | """Validate signature parameters.
450 |
451 | Raises
452 | ------
453 | :exc:`TypeError`
454 | If ``divisor`` is not an integer greater than or equal to 5.
455 | :exc:`TypeError`
456 | If ``divisor_bits`` is not `None` and not an integer greater than or equal to
457 | ``bits_for(divisor)``.
458 | :exc:`TypeError`
459 | If ``data_bits`` is not an integer greater than or equal to 0.
460 | :exc:`ValueError`
461 | If ``parity`` is not a :class:`Parity` member.
462 | """
463 | AsyncSerialRX.Signature.check_parameters(divisor=divisor, divisor_bits=divisor_bits,
464 | data_bits=data_bits, parity=parity)
465 | AsyncSerialTX.Signature.check_parameters(divisor=divisor, divisor_bits=divisor_bits,
466 | data_bits=data_bits, parity=parity)
467 |
468 | @property
469 | def divisor(self):
470 | return self.members["rx"].signature.divisor
471 |
472 | @property
473 | def divisor_bits(self):
474 | return self.members["rx"].signature.divisor_bits
475 |
476 | @property
477 | def data_bits(self):
478 | return self.members["rx"].signature.data_bits
479 |
480 | @property
481 | def parity(self):
482 | return self.members["rx"].signature.parity
483 |
484 | def __eq__(self, other):
485 | """Compare signatures.
486 |
487 | Two signatures are equal if they have the same divisor value, divisor bits,
488 | data bits, and parity.
489 | """
490 | return (isinstance(other, AsyncSerial.Signature) and
491 | self.divisor == other.divisor and
492 | self.divisor_bits == other.divisor_bits and
493 | self.data_bits == other.data_bits and
494 | self.parity == other.parity)
495 |
496 | def __repr__(self):
497 | return f"AsyncSerial.Signature({self.members!r})"
498 |
499 | """Asynchronous serial transceiver.
500 |
501 | Parameters
502 | ----------
503 | divisor : int
504 | Clock divisor initial value. Should be set to ``int(clk_frequency // baudrate)``.
505 | divisor_bits : int
506 | Clock divisor width. Optional. If omitted, ``bits_for(divisor)`` is used instead.
507 | data_bits : int
508 | Data bits per frame.
509 | parity : :class:`Parity`
510 | Parity mode.
511 | pins : :class:`amaranth.lib.io.Pin`
512 | UART pins. Optional. See :class:`amaranth_boards.resources.UARTResource` for layout.
513 | If provided, the ``rx.i`` and ``tx.o`` ports of the transceiver are internally connected
514 | to ``pins.rx.i`` and ``pins.tx.o``, respectively.
515 |
516 | Raises
517 | ------
518 | See :meth:`AsyncSerial.Signature.check_parameters`.
519 | """
520 | def __init__(self, *, divisor, divisor_bits=None, data_bits=8, parity="none", pins=None):
521 | super().__init__(self.Signature(divisor=divisor, divisor_bits=divisor_bits,
522 | data_bits=data_bits, parity=parity))
523 | self._pins = pins
524 |
525 | def elaborate(self, platform):
526 | m = Module()
527 |
528 | rx = AsyncSerialRX(divisor=self.signature.divisor,
529 | divisor_bits=self.signature.divisor_bits,
530 | data_bits=self.signature.data_bits,
531 | parity=self.signature.parity)
532 | tx = AsyncSerialTX(divisor=self.signature.divisor,
533 | divisor_bits=self.signature.divisor_bits,
534 | data_bits=self.signature.data_bits,
535 | parity=self.signature.parity)
536 | m.submodules.rx = rx
537 | m.submodules.tx = tx
538 |
539 | m.d.comb += [
540 | self.rx.divisor.eq(self.divisor),
541 | self.tx.divisor.eq(self.divisor),
542 | ]
543 |
544 | if self._pins is not None:
545 | m.submodules += FFSynchronizer(self._pins.rx.i, self.rx.i, init=1)
546 | m.d.comb += self._pins.tx.o.eq(self.tx.o)
547 |
548 | connect(m, flipped(self.rx), rx)
549 | connect(m, flipped(self.tx), tx)
550 |
551 | return m
552 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _build/
2 |
--------------------------------------------------------------------------------
/docs/_static/custom.css:
--------------------------------------------------------------------------------
1 | /* Links in text should be underlined. */
2 | a { text-decoration: underline; }
3 | .wy-menu-vertical a, .wy-side-nav-search > a { text-decoration: none; }
4 |
5 | /* Match the logo colors in the background. */
6 | .wy-nav-top, .wy-side-nav-search { background-color: #784b9a; }
7 |
8 | /* Make the logo more reasonably sized. */
9 | .wy-side-nav-search > a img.logo { width: 160px; }
10 |
11 | /* Some of our section titles are looong */
12 | @media screen and (min-width:769px) {
13 | .wy-nav-side, .wy-side-scroll, .wy-menu-vertical { width: 340px; }
14 | .wy-side-nav-search { width: 325px; }
15 | .wy-nav-content-wrap { margin-left: 340px; }
16 | }
17 |
18 | /* We don't have a version picker widget */
19 | .wy-nav-side { padding-bottom: 0; }
20 |
21 | /* Many of our diagnostics are even longer */
22 | .rst-content pre.literal-block, .rst-content div[class^="highlight"] pre, .rst-content .linenodiv pre { white-space: pre-wrap; }
23 |
--------------------------------------------------------------------------------
/docs/_static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amaranth-lang/amaranth-stdio/618a13fb7108f69197d698df4d64c203553deaae/docs/_static/logo.png
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | import os, sys
2 | sys.path.insert(0, os.path.abspath("."))
3 |
4 | import time
5 | import amaranth_stdio
6 |
7 | project = "Amaranth standard I/O components"
8 | version = amaranth_stdio.__version__.replace(".editable", "")
9 | release = version.split("+")[0]
10 | copyright = time.strftime("2020—%Y, Amaranth project contributors")
11 |
12 | extensions = [
13 | "sphinx.ext.intersphinx",
14 | "sphinx.ext.doctest",
15 | "sphinx.ext.todo",
16 | "sphinx.ext.autodoc",
17 | "sphinx.ext.napoleon",
18 | "sphinx_rtd_theme",
19 | ]
20 |
21 | with open(".gitignore") as f:
22 | exclude_patterns = [line.strip() for line in f.readlines()]
23 |
24 | root_doc = "cover"
25 |
26 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
27 |
28 | todo_include_todos = True
29 |
30 | autodoc_member_order = "bysource"
31 | autodoc_default_options = {
32 | "members": True
33 | }
34 | autodoc_preserve_defaults = True
35 |
36 | napoleon_google_docstring = False
37 | napoleon_numpy_docstring = True
38 | napoleon_use_ivar = True
39 | napoleon_include_init_with_doc = True
40 | napoleon_include_special_with_doc = True
41 |
42 | html_theme = "sphinx_rtd_theme"
43 | html_static_path = ["_static"]
44 | html_css_files = ["custom.css"]
45 | html_logo = "_static/logo.png"
46 |
47 | rst_prolog = """
48 | .. role:: pc(code)
49 | :language: python
50 | """
51 |
--------------------------------------------------------------------------------
/docs/cover.rst:
--------------------------------------------------------------------------------
1 | .. This page is loaded if you click on the Amaranth logo.
2 |
3 | Amaranth project documentation
4 | ##############################
5 |
6 | .. toctree::
7 | :maxdepth: 1
8 |
9 | Language & toolchain
10 | index
11 | System on Chip toolkit
12 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Standard I/O components
2 | #######################
3 |
4 | .. warning::
5 |
6 | This manual is a work in progress and is seriously incomplete!
7 |
--------------------------------------------------------------------------------
/pdm_build.py:
--------------------------------------------------------------------------------
1 | from pdm.backend._vendor.packaging.version import Version
2 |
3 |
4 | # This is done in a PDM build hook without specifying `dynamic = [..., "version"]` to put all
5 | # of the static metadata into pyproject.toml. Tools other than PDM will not execute this script
6 | # and will use the generic version of the documentation URL (which redirects to /latest).
7 | def pdm_build_initialize(context):
8 | version = Version(context.config.metadata["version"])
9 | if version.is_prerelease:
10 | url_version = "latest"
11 | else:
12 | url_version = f"v{version}"
13 | context.config.metadata["urls"]["Documentation"] += url_version
14 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # Project metadata
2 |
3 | [tool.pdm.version]
4 | source = "scm"
5 |
6 | [project]
7 | dynamic = ["version"]
8 |
9 | name = "amaranth-stdio"
10 | description = "Industry standard I/O for Amaranth HDL"
11 | authors = [{name = "Amaranth HDL contributors"}]
12 | license = {file = "LICENSE.txt"}
13 |
14 | requires-python = "~=3.9"
15 | dependencies = [
16 | # this version requirement needs to be synchronized with the one in .github/workflows/main.yml
17 | "amaranth>=0.5,<0.6",
18 | ]
19 |
20 | [project.urls]
21 | "Homepage" = "https://amaranth-lang.org/"
22 | "Documentation" = "https://amaranth-lang.org/docs/amaranth-stdio/" # modified in pdm_build.py
23 | "Source Code" = "https://github.com/amaranth-lang/amaranth-stdio"
24 | "Bug Tracker" = "https://github.com/amaranth-lang/amaranth-stdio/issues"
25 |
26 | # Build system configuration
27 |
28 | [build-system]
29 | requires = ["pdm-backend"]
30 | build-backend = "pdm.backend"
31 |
32 | # Development workflow configuration
33 |
34 | [tool.pdm.dev-dependencies]
35 | test = [
36 | "coverage",
37 | ]
38 | docs = [
39 | "sphinx~=7.1",
40 | "sphinx-rtd-theme~=1.2",
41 | "sphinx-autobuild",
42 | ]
43 |
44 | [tool.pdm.scripts]
45 | test.composite = ["test-code"]
46 | # TODO: Once the amaranth requirement is bumped to 0.6, remove the RFC 66 warning filter and fix the deprecations.
47 | test-code.env = {PYTHONWARNINGS = "error,ignore:Per RFC 66,:DeprecationWarning"}
48 | test-code.cmd = "python -m coverage run -m unittest discover -t . -s tests -v"
49 | test-docs.cmd = "sphinx-build -b doctest docs/ docs/_build"
50 |
51 | document.cmd = "sphinx-build docs/ docs/_build/"
52 | document-live.cmd = "sphinx-autobuild docs/ docs/_build/ --watch amaranth_stdio"
53 |
54 | coverage-text.cmd = "python -m coverage report"
55 | coverage-html.cmd = "python -m coverage html"
56 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amaranth-lang/amaranth-stdio/618a13fb7108f69197d698df4d64c203553deaae/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_serial.py:
--------------------------------------------------------------------------------
1 | # amaranth: UnusedElaboratable=no
2 |
3 | from unittest import TestCase
4 |
5 | from amaranth import *
6 | from amaranth.lib.data import StructLayout
7 | from amaranth.lib.fifo import SyncFIFO
8 | from amaranth.lib.wiring import *
9 | from amaranth.sim import *
10 |
11 | from amaranth_stdio.serial import *
12 |
13 |
14 | class _DummyPins:
15 | def __init__(self):
16 | self.rx = Signal(StructLayout({"i": 1}), init={"i": 1})
17 | self.tx = Signal(StructLayout({"o": 1}), init={"o": 1})
18 |
19 |
20 | class AsyncSerialRXSignatureTestCase(TestCase):
21 | def test_simple(self):
22 | sig = AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even")
23 | self.assertEqual(sig.divisor, 10)
24 | self.assertEqual(sig.divisor_bits, 8)
25 | self.assertEqual(sig.data_bits, 7)
26 | self.assertEqual(sig.parity, Parity.EVEN)
27 | self.assertEqual(sig.members, Signature({
28 | "divisor": In(unsigned(8), init=10),
29 | "data": Out(unsigned(7)),
30 | "err": Out(StructLayout({"overflow": 1, "frame": 1, "parity": 1})),
31 | "rdy": Out(unsigned(1)),
32 | "ack": In(unsigned(1)),
33 | "i": In(unsigned(1), init=1),
34 | }).members)
35 |
36 | def test_defaults(self):
37 | sig = AsyncSerialRX.Signature(divisor=10)
38 | self.assertEqual(sig.divisor, 10)
39 | self.assertEqual(sig.divisor_bits, 4)
40 | self.assertEqual(sig.data_bits, 8)
41 | self.assertEqual(sig.parity, Parity.NONE)
42 |
43 | def test_eq(self):
44 | self.assertEqual(AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
45 | AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"))
46 | # different divisor
47 | self.assertNotEqual(AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
48 | AsyncSerialRX.Signature(divisor= 8, divisor_bits=8, data_bits=7, parity="even"))
49 | # different divisor_bits
50 | self.assertNotEqual(AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
51 | AsyncSerialRX.Signature(divisor=10, divisor_bits=4, data_bits=7, parity="even"))
52 | # different data_bits
53 | self.assertNotEqual(AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
54 | AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="even"))
55 | # different parity
56 | self.assertNotEqual(AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="even"),
57 | AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="none"))
58 |
59 | def test_repr(self):
60 | sig = AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even")
61 | self.assertEqual(repr(sig), "AsyncSerialRX.Signature(SignatureMembers({"
62 | "'divisor': In(unsigned(8), init=10), "
63 | "'data': Out(unsigned(7)), "
64 | "'err': Out(StructLayout({"
65 | "'overflow': 1, "
66 | "'frame': 1, "
67 | "'parity': 1})), "
68 | "'rdy': Out(unsigned(1)), "
69 | "'ack': In(unsigned(1)), "
70 | "'i': In(unsigned(1), init=1)}))")
71 |
72 | def test_wrong_divisor(self):
73 | with self.assertRaisesRegex(TypeError,
74 | r"Divisor initial value must be an integer greater than or equal to 5, not 4"):
75 | AsyncSerialRX.Signature(divisor=4)
76 | with self.assertRaisesRegex(TypeError,
77 | r"Divisor initial value must be an integer greater than or equal to 5, not 4"):
78 | AsyncSerialRX.Signature.check_parameters(divisor=4)
79 |
80 | def test_wrong_divisor_bits(self):
81 | with self.assertRaisesRegex(TypeError,
82 | r"Divisor bits must be an integer greater than or equal to 4, not 3"):
83 | AsyncSerialRX.Signature(divisor=8, divisor_bits=3)
84 | with self.assertRaisesRegex(TypeError,
85 | r"Divisor bits must be an integer greater than or equal to 4, not 3"):
86 | AsyncSerialRX.Signature.check_parameters(divisor=8, divisor_bits=3)
87 |
88 | def test_wrong_data_bits(self):
89 | with self.assertRaisesRegex(TypeError,
90 | r"Data bits must be a non-negative integer, not -1"):
91 | AsyncSerialRX.Signature(divisor=10, data_bits=-1)
92 | with self.assertRaisesRegex(TypeError,
93 | r"Data bits must be a non-negative integer, not -1"):
94 | AsyncSerialRX.Signature.check_parameters(divisor=10, data_bits=-1)
95 |
96 | def test_wrong_parity(self):
97 | with self.assertRaisesRegex(ValueError, r"'foo' is not a valid Parity"):
98 | AsyncSerialRX.Signature(divisor=10, parity="foo")
99 | with self.assertRaisesRegex(ValueError, r"'foo' is not a valid Parity"):
100 | AsyncSerialRX.Signature.check_parameters(divisor=10, parity="foo")
101 |
102 |
103 | class AsyncSerialRXTestCase(TestCase):
104 | async def _rx_period(self, ctx, dut):
105 | await ctx.tick().repeat(ctx.get(dut.divisor))
106 |
107 | async def _rx_bits(self, ctx, dut, bits, pins=None):
108 | rx_i = dut.i if pins is None else pins.rx.i
109 | for bit in bits:
110 | await self._rx_period(ctx, dut)
111 | ctx.set(rx_i, bit)
112 |
113 | def _rx_test(self, dut, bits, *, data=None, errors=None, pins=None):
114 | async def testbench(ctx):
115 | self.assertFalse(ctx.get(dut.rdy))
116 | ctx.set(dut.ack, 1)
117 | await self._rx_bits(ctx, dut, bits, pins)
118 | await ctx.tick().until(dut.rdy)
119 | if data is not None:
120 | self.assertFalse(ctx.get(dut.err.overflow))
121 | self.assertFalse(ctx.get(dut.err.frame))
122 | self.assertFalse(ctx.get(dut.err.parity))
123 | self.assertEqual(ctx.get(dut.data), data)
124 | if errors is not None:
125 | for error in errors:
126 | self.assertTrue(ctx.get(getattr(dut.err, error)))
127 |
128 | sim = Simulator(dut)
129 | sim.add_clock(1e-6)
130 | sim.add_testbench(testbench)
131 | with sim.write_vcd(vcd_file="test.vcd"):
132 | sim.run()
133 |
134 | def test_signature(self):
135 | dut = AsyncSerialRX(divisor=10, divisor_bits=8, data_bits=7, parity="even")
136 | self.assertIsInstance(dut.signature, AsyncSerialRX.Signature)
137 | self.assertEqual(dut.signature.divisor, 10)
138 | self.assertEqual(dut.signature.divisor_bits, 8)
139 | self.assertEqual(dut.signature.data_bits, 7)
140 | self.assertEqual(dut.signature.parity, Parity.EVEN)
141 |
142 | def test_defaults(self):
143 | dut = AsyncSerialRX(divisor=10)
144 | self.assertEqual(dut.signature.divisor, 10)
145 | self.assertEqual(dut.signature.divisor_bits, 4)
146 | self.assertEqual(dut.signature.data_bits, 8)
147 | self.assertEqual(dut.signature.parity, Parity.NONE)
148 |
149 | def test_8n1(self):
150 | dut = AsyncSerialRX(divisor=5, data_bits=8, parity="none")
151 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 1], data=0b01110101)
152 |
153 | def test_16n1(self):
154 | dut = AsyncSerialRX(divisor=5, data_bits=16, parity="none")
155 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0,1,1,1,1,0,0,0,0, 1],
156 | data=0b0000111101110101)
157 |
158 | def test_8m1(self):
159 | dut = AsyncSerialRX(divisor=5, data_bits=8, parity="mark")
160 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 1, 1], data=0b01110101)
161 | self._rx_test(dut, [0, 1,0,1,0,1,1,0,0, 1, 1], data=0b00110101)
162 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 0, 1], errors={"parity"})
163 |
164 | def test_8s1(self):
165 | dut = AsyncSerialRX(divisor=5, data_bits=8, parity="space")
166 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 0, 1], data=0b01110101)
167 | self._rx_test(dut, [0, 1,0,1,0,1,1,0,0, 0, 1], data=0b00110101)
168 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 1, 1], errors={"parity"})
169 |
170 | def test_8e1(self):
171 | dut = AsyncSerialRX(divisor=5, data_bits=8, parity="even")
172 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 1, 1], data=0b01110101)
173 | self._rx_test(dut, [0, 1,0,1,0,1,1,0,0, 0, 1], data=0b00110101)
174 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 0, 1], errors={"parity"})
175 |
176 | def test_8o1(self):
177 | dut = AsyncSerialRX(divisor=5, data_bits=8, parity="odd")
178 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 0, 1], data=0b01110101)
179 | self._rx_test(dut, [0, 1,0,1,0,1,1,0,0, 1, 1], data=0b00110101)
180 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 1, 1], errors={"parity"})
181 |
182 | def test_8n1_pins(self):
183 | pins = _DummyPins()
184 | dut = AsyncSerialRX(divisor=5, data_bits=8, parity="none", pins=pins)
185 | self._rx_test(dut, [0, 1,0,1,0,1,1,1,0, 1], data=0b01110101, pins=pins)
186 |
187 | def test_err_frame(self):
188 | dut = AsyncSerialRX(divisor=5)
189 | self._rx_test(dut, [0, 0,0,0,0,0,0,0,0, 0], errors={"frame"})
190 |
191 | def test_err_overflow(self):
192 | dut = AsyncSerialRX(divisor=5)
193 |
194 | async def testbench(ctx):
195 | self.assertFalse(ctx.get(dut.rdy))
196 | await self._rx_bits(ctx, dut, [0, 0,0,0,0,0,0,0,0, 1])
197 | await self._rx_period(ctx, dut)
198 | await ctx.tick()
199 | self.assertFalse(ctx.get(dut.rdy))
200 | self.assertTrue(ctx.get(dut.err.overflow))
201 |
202 | sim = Simulator(dut)
203 | sim.add_clock(1e-6)
204 | sim.add_testbench(testbench)
205 | with sim.write_vcd(vcd_file="test.vcd"):
206 | sim.run()
207 |
208 | def test_fifo(self):
209 | m = Module()
210 | m.submodules.dut = dut = AsyncSerialRX(divisor=5)
211 | m.submodules.fifo = fifo = SyncFIFO(width=8, depth=4)
212 |
213 | m.d.comb += [
214 | dut.ack.eq(fifo.w_rdy),
215 | fifo.w_en.eq(dut.rdy),
216 | fifo.w_data.eq(dut.data),
217 | ]
218 |
219 | async def testbench(ctx):
220 | await self._rx_bits(ctx, dut, [0, 1,0,1,0,1,0,1,0, 1,
221 | 0, 0,1,0,1,0,1,0,1, 1])
222 | self.assertTrue(ctx.get(fifo.r_rdy))
223 | self.assertEqual(ctx.get(fifo.r_data), 0x55)
224 | ctx.set(fifo.r_en, 1)
225 | await ctx.tick()
226 | fifo_r_data_value, = await ctx.tick().sample(fifo.r_data).until(fifo.r_rdy)
227 | self.assertEqual(fifo_r_data_value, 0xAA)
228 | self.assertFalse(ctx.get(fifo.r_rdy))
229 |
230 | sim = Simulator(m)
231 | sim.add_clock(1e-6)
232 | sim.add_testbench(testbench)
233 | with sim.write_vcd(vcd_file="test.vcd"):
234 | sim.run()
235 |
236 |
237 | class AsyncSerialTXSignatureTestCase(TestCase):
238 | def test_simple(self):
239 | sig = AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even")
240 | self.assertEqual(sig.divisor, 10)
241 | self.assertEqual(sig.divisor_bits, 8)
242 | self.assertEqual(sig.data_bits, 7)
243 | self.assertEqual(sig.parity, Parity.EVEN)
244 | self.assertEqual(sig.members, Signature({
245 | "divisor": In(unsigned(8), init=10),
246 | "data": In(unsigned(7)),
247 | "rdy": Out(unsigned(1)),
248 | "ack": In(unsigned(1)),
249 | "o": Out(unsigned(1), init=1),
250 | }).members)
251 |
252 | def test_defaults(self):
253 | sig = AsyncSerialTX.Signature(divisor=10)
254 | self.assertEqual(sig.divisor, 10)
255 | self.assertEqual(sig.divisor_bits, 4)
256 | self.assertEqual(sig.data_bits, 8)
257 | self.assertEqual(sig.parity, Parity.NONE)
258 |
259 | def test_eq(self):
260 | self.assertEqual(AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
261 | AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"))
262 | # different divisor
263 | self.assertNotEqual(AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
264 | AsyncSerialTX.Signature(divisor= 8, divisor_bits=8, data_bits=7, parity="even"))
265 | # different divisor_bits
266 | self.assertNotEqual(AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
267 | AsyncSerialTX.Signature(divisor=10, divisor_bits=4, data_bits=7, parity="even"))
268 | # different data_bits
269 | self.assertNotEqual(AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
270 | AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="even"))
271 | # different parity
272 | self.assertNotEqual(AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="even"),
273 | AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="none"))
274 |
275 | def test_repr(self):
276 | sig = AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even")
277 | self.assertEqual(repr(sig), "AsyncSerialTX.Signature(SignatureMembers({"
278 | "'divisor': In(unsigned(8), init=10), "
279 | "'data': In(unsigned(7)), "
280 | "'rdy': Out(unsigned(1)), "
281 | "'ack': In(unsigned(1)), "
282 | "'o': Out(unsigned(1), init=1)}))")
283 |
284 | def test_wrong_divisor(self):
285 | with self.assertRaisesRegex(TypeError,
286 | r"Divisor initial value must be an integer greater than or equal to 1, not 0"):
287 | AsyncSerialTX.Signature(divisor=0)
288 | with self.assertRaisesRegex(TypeError,
289 | r"Divisor initial value must be an integer greater than or equal to 1, not 0"):
290 | AsyncSerialTX.Signature.check_parameters(divisor=0)
291 |
292 | def test_wrong_divisor_bits(self):
293 | with self.assertRaisesRegex(TypeError,
294 | r"Divisor bits must be an integer greater than or equal to 4, not 3"):
295 | AsyncSerialTX.Signature(divisor=8, divisor_bits=3)
296 | with self.assertRaisesRegex(TypeError,
297 | r"Divisor bits must be an integer greater than or equal to 4, not 3"):
298 | AsyncSerialTX.Signature.check_parameters(divisor=8, divisor_bits=3)
299 |
300 | def test_wrong_data_bits(self):
301 | with self.assertRaisesRegex(TypeError,
302 | r"Data bits must be a non-negative integer, not -1"):
303 | AsyncSerialTX.Signature(divisor=10, data_bits=-1)
304 | with self.assertRaisesRegex(TypeError,
305 | r"Data bits must be a non-negative integer, not -1"):
306 | AsyncSerialTX.Signature.check_parameters(divisor=10, data_bits=-1)
307 |
308 | def test_wrong_parity(self):
309 | with self.assertRaisesRegex(ValueError, r"'foo' is not a valid Parity"):
310 | AsyncSerialTX.Signature(divisor=10, parity="foo")
311 | with self.assertRaisesRegex(ValueError, r"'foo' is not a valid Parity"):
312 | AsyncSerialTX.Signature.check_parameters(divisor=10, parity="foo")
313 |
314 |
315 | class AsyncSerialTXTestCase(TestCase):
316 | async def _tx_period(self, ctx, dut):
317 | await ctx.tick().repeat(ctx.get(dut.divisor))
318 |
319 | def _tx_test(self, dut, data, *, bits, pins=None):
320 | tx_o = dut.o if pins is None else pins.tx.o
321 |
322 | async def testbench(ctx):
323 | self.assertTrue(ctx.get(dut.rdy))
324 | ctx.set(dut.data, data)
325 | ctx.set(dut.ack, 1)
326 | await ctx.tick().until(dut.rdy)
327 | for bit in bits:
328 | await self._tx_period(ctx, dut)
329 | self.assertEqual(ctx.get(tx_o), bit)
330 |
331 | sim = Simulator(dut)
332 | sim.add_clock(1e-6)
333 | sim.add_testbench(testbench)
334 | with sim.write_vcd(vcd_file="test.vcd"):
335 | sim.run()
336 |
337 | def test_signature(self):
338 | dut = AsyncSerialTX(divisor=10, divisor_bits=8, data_bits=7, parity="even")
339 | self.assertIsInstance(dut.signature, AsyncSerialTX.Signature)
340 | self.assertEqual(dut.signature.divisor, 10)
341 | self.assertEqual(dut.signature.divisor_bits, 8)
342 | self.assertEqual(dut.signature.data_bits, 7)
343 | self.assertEqual(dut.signature.parity, Parity.EVEN)
344 |
345 | def test_defaults(self):
346 | dut = AsyncSerialTX(divisor=10)
347 | self.assertEqual(dut.signature.divisor, 10)
348 | self.assertEqual(dut.signature.divisor_bits, 4)
349 | self.assertEqual(dut.signature.data_bits, 8)
350 | self.assertEqual(dut.signature.parity, Parity.NONE)
351 |
352 | def test_8n1(self):
353 | dut = AsyncSerialTX(divisor=1, data_bits=8, parity="none")
354 | self._tx_test(dut, 0b01110101, bits=[0, 1,0,1,0,1,1,1,0, 1])
355 |
356 | def test_16n1(self):
357 | dut = AsyncSerialTX(divisor=1, data_bits=16, parity="none")
358 | self._tx_test(dut, 0b0000111101110101, bits=[0, 1,0,1,0,1,1,1,0,1,1,1,1,0,0,0,0, 1])
359 |
360 | def test_8m1(self):
361 | dut = AsyncSerialTX(divisor=1, data_bits=8, parity="mark")
362 | self._tx_test(dut, 0b01110101, bits=[0, 1,0,1,0,1,1,1,0, 1, 1])
363 | self._tx_test(dut, 0b00110101, bits=[0, 1,0,1,0,1,1,0,0, 1, 1])
364 |
365 | def test_8s1(self):
366 | dut = AsyncSerialTX(divisor=1, data_bits=8, parity="space")
367 | self._tx_test(dut, 0b01110101, bits=[0, 1,0,1,0,1,1,1,0, 0, 1])
368 | self._tx_test(dut, 0b00110101, bits=[0, 1,0,1,0,1,1,0,0, 0, 1])
369 |
370 | def test_8e1(self):
371 | dut = AsyncSerialTX(divisor=1, data_bits=8, parity="even")
372 | self._tx_test(dut, 0b01110101, bits=[0, 1,0,1,0,1,1,1,0, 1, 1])
373 | self._tx_test(dut, 0b00110101, bits=[0, 1,0,1,0,1,1,0,0, 0, 1])
374 |
375 | def test_8o1(self):
376 | dut = AsyncSerialTX(divisor=1, data_bits=8, parity="odd")
377 | self._tx_test(dut, 0b01110101, bits=[0, 1,0,1,0,1,1,1,0, 0, 1])
378 | self._tx_test(dut, 0b00110101, bits=[0, 1,0,1,0,1,1,0,0, 1, 1])
379 |
380 | def test_8n1_pins(self):
381 | pins = _DummyPins()
382 | dut = AsyncSerialTX(divisor=1, data_bits=8, parity="none", pins=pins)
383 | self._tx_test(dut, 0b01110101, bits=[0, 1,0,1,0,1,1,1,0, 1], pins=pins)
384 |
385 | def test_fifo(self):
386 | m = Module()
387 | m.submodules.tx = dut = AsyncSerialTX(divisor=1)
388 | m.submodules.fifo = fifo = SyncFIFO(width=8, depth=4)
389 |
390 | m.d.comb += [
391 | dut.ack.eq(fifo.r_rdy),
392 | dut.data.eq(fifo.r_data),
393 | fifo.r_en.eq(fifo.r_rdy & dut.rdy),
394 | ]
395 |
396 | async def testbench(ctx):
397 | self.assertTrue(ctx.get(fifo.w_rdy))
398 | ctx.set(fifo.w_en, 1)
399 | ctx.set(fifo.w_data, 0x55)
400 | await ctx.tick()
401 | self.assertTrue(ctx.get(fifo.w_rdy))
402 | ctx.set(fifo.w_data, 0xAA)
403 | await ctx.tick()
404 | ctx.set(fifo.w_en, 0)
405 | for bit in [0, 1,0,1,0,1,0,1,0, 1]:
406 | await self._tx_period(ctx, dut)
407 | self.assertEqual(ctx.get(dut.o), bit)
408 | await ctx.tick()
409 | for bit in [0, 0,1,0,1,0,1,0,1, 1]:
410 | await self._tx_period(ctx, dut)
411 | self.assertEqual(ctx.get(dut.o), bit)
412 |
413 | sim = Simulator(m)
414 | sim.add_clock(1e-6)
415 | sim.add_testbench(testbench)
416 | with sim.write_vcd(vcd_file="test.vcd"):
417 | sim.run()
418 |
419 |
420 | class AsyncSerialSignatureTestCase(TestCase):
421 | def test_simple(self):
422 | sig = AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even")
423 | self.assertEqual(sig.divisor, 10)
424 | self.assertEqual(sig.divisor_bits, 8)
425 | self.assertEqual(sig.data_bits, 7)
426 | self.assertEqual(sig.parity, Parity.EVEN)
427 | self.assertEqual(sig.members, Signature({
428 | "divisor": In(unsigned(8), init=10),
429 | "rx": Out(AsyncSerialRX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even")),
430 | "tx": Out(AsyncSerialTX.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even")),
431 | }).members)
432 |
433 | def test_defaults(self):
434 | sig = AsyncSerial.Signature(divisor=10)
435 | self.assertEqual(sig.divisor, 10)
436 | self.assertEqual(sig.divisor_bits, 4)
437 | self.assertEqual(sig.data_bits, 8)
438 | self.assertEqual(sig.parity, Parity.NONE)
439 |
440 | def test_eq(self):
441 | self.assertEqual(AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
442 | AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"))
443 | # different divisor
444 | self.assertNotEqual(AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
445 | AsyncSerial.Signature(divisor= 8, divisor_bits=8, data_bits=7, parity="even"))
446 | # different divisor_bits
447 | self.assertNotEqual(AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
448 | AsyncSerial.Signature(divisor=10, divisor_bits=4, data_bits=7, parity="even"))
449 | # different data_bits
450 | self.assertNotEqual(AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even"),
451 | AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="even"))
452 | # different parity
453 | self.assertNotEqual(AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="even"),
454 | AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=8, parity="none"))
455 |
456 | def test_repr(self):
457 | sig = AsyncSerial.Signature(divisor=10, divisor_bits=8, data_bits=7, parity="even")
458 | self.assertEqual(repr(sig), "AsyncSerial.Signature(SignatureMembers({"
459 | "'divisor': In(unsigned(8), init=10), "
460 | "'rx': Out(AsyncSerialRX.Signature(SignatureMembers({"
461 | "'divisor': In(unsigned(8), init=10), "
462 | "'data': Out(unsigned(7)), "
463 | "'err': Out(StructLayout({"
464 | "'overflow': 1, "
465 | "'frame': 1, "
466 | "'parity': 1})), "
467 | "'rdy': Out(unsigned(1)), "
468 | "'ack': In(unsigned(1)), "
469 | "'i': In(unsigned(1), init=1)}))), "
470 | "'tx': Out(AsyncSerialTX.Signature(SignatureMembers({"
471 | "'divisor': In(unsigned(8), init=10), "
472 | "'data': In(unsigned(7)), "
473 | "'rdy': Out(unsigned(1)), "
474 | "'ack': In(unsigned(1)), "
475 | "'o': Out(unsigned(1), init=1)})))}))")
476 |
477 | def test_wrong_divisor(self):
478 | with self.assertRaisesRegex(TypeError,
479 | r"Divisor initial value must be an integer greater than or equal to 5, not 4"):
480 | AsyncSerial.Signature(divisor=4)
481 | with self.assertRaisesRegex(TypeError,
482 | r"Divisor initial value must be an integer greater than or equal to 5, not 4"):
483 | AsyncSerial.Signature.check_parameters(divisor=4)
484 |
485 | def test_wrong_divisor_bits(self):
486 | with self.assertRaisesRegex(TypeError,
487 | r"Divisor bits must be an integer greater than or equal to 4, not 3"):
488 | AsyncSerial.Signature(divisor=8, divisor_bits=3)
489 | with self.assertRaisesRegex(TypeError,
490 | r"Divisor bits must be an integer greater than or equal to 4, not 3"):
491 | AsyncSerial.Signature.check_parameters(divisor=8, divisor_bits=3)
492 |
493 | def test_wrong_data_bits(self):
494 | with self.assertRaisesRegex(TypeError,
495 | r"Data bits must be a non-negative integer, not -1"):
496 | AsyncSerial.Signature(divisor=10, data_bits=-1)
497 | with self.assertRaisesRegex(TypeError,
498 | r"Data bits must be a non-negative integer, not -1"):
499 | AsyncSerial.Signature.check_parameters(divisor=10, data_bits=-1)
500 |
501 | def test_wrong_parity(self):
502 | with self.assertRaisesRegex(ValueError, r"'foo' is not a valid Parity"):
503 | AsyncSerial.Signature(divisor=10, parity="foo")
504 | with self.assertRaisesRegex(ValueError, r"'foo' is not a valid Parity"):
505 | AsyncSerial.Signature.check_parameters(divisor=10, parity="foo")
506 |
507 |
508 | class AsyncSerialTestCase(TestCase):
509 | def test_signature(self):
510 | dut = AsyncSerial(divisor=10, divisor_bits=8, data_bits=7, parity="even")
511 | self.assertIsInstance(dut.signature, AsyncSerial.Signature)
512 | self.assertEqual(dut.signature.divisor, 10)
513 | self.assertEqual(dut.signature.divisor_bits, 8)
514 | self.assertEqual(dut.signature.data_bits, 7)
515 | self.assertEqual(dut.signature.parity, Parity.EVEN)
516 |
517 | def test_defaults(self):
518 | dut = AsyncSerial(divisor=10)
519 | self.assertEqual(dut.signature.divisor, 10)
520 | self.assertEqual(dut.signature.divisor_bits, 4)
521 | self.assertEqual(dut.signature.data_bits, 8)
522 | self.assertEqual(dut.signature.parity, Parity.NONE)
523 |
524 | def test_loopback(self):
525 | m = Module()
526 | m.submodules.dut = dut = AsyncSerial(divisor=5)
527 |
528 | m.d.comb += dut.rx.i.eq(dut.tx.o)
529 |
530 | async def testbench(ctx):
531 | self.assertTrue(ctx.get(dut.tx.rdy))
532 | ctx.set(dut.tx.data, 0xAA)
533 | ctx.set(dut.tx.ack, 1)
534 | await ctx.tick()
535 | ctx.set(dut.tx.ack, 0)
536 | ctx.set(dut.rx.ack, 1)
537 | await ctx.tick()
538 | await ctx.tick().until(dut.rx.rdy)
539 | self.assertEqual(ctx.get(dut.rx.data), 0xAA)
540 |
541 | sim = Simulator(m)
542 | sim.add_clock(1e-6)
543 | sim.add_testbench(testbench)
544 | with sim.write_vcd(vcd_file="test.vcd"):
545 | sim.run()
546 |
547 | def test_loopback_pins(self):
548 | pins = _DummyPins()
549 |
550 | m = Module()
551 | m.submodules.dut = dut = AsyncSerial(divisor=5, pins=pins)
552 |
553 | m.d.comb += pins.rx.i.eq(pins.tx.o)
554 |
555 | async def testbench(ctx):
556 | self.assertTrue(ctx.get(dut.tx.rdy))
557 | ctx.set(dut.tx.data, 0xAA)
558 | ctx.set(dut.tx.ack, 1)
559 | await ctx.tick()
560 | ctx.set(dut.tx.ack, 0)
561 | ctx.set(dut.rx.ack, 1)
562 | await ctx.tick()
563 | await ctx.tick().until(dut.rx.rdy)
564 | self.assertEqual(ctx.get(dut.rx.data), 0xAA)
565 |
566 | sim = Simulator(m)
567 | sim.add_clock(1e-6)
568 | sim.add_testbench(testbench)
569 | with sim.write_vcd(vcd_file="test.vcd"):
570 | sim.run()
571 |
--------------------------------------------------------------------------------