├── .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 | --------------------------------------------------------------------------------