├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── adat ├── __init__.py ├── nrzidecoder.py ├── receiver.py └── transmitter.py ├── doc ├── adat.rst ├── conf.py └── generate_doc.py ├── generate.py ├── setup.py └── tests ├── nrzi-decoder-bench-44100.gtkw ├── nrzi-decoder-bench-48000.gtkw ├── nrzidecoder-bench.py ├── receiver-bench.py ├── receiver-smoke-test-44100.gtkw ├── receiver-smoke-test-48000.gtkw ├── testdata.py ├── transmitter-bench.py └── transmitter-smoke-test-48000.gtkw /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vscode/ 3 | **/.eggs/** 4 | env/ 5 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10.0 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=1 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape 142 | 143 | # Enable the message, report, category or checker with the given id(s). You can 144 | # either give multiple identifier separated by comma (,) or put this option 145 | # multiple time (only on the command line, not in the configuration file where 146 | # it should appear only once). See also the "--disable" option for examples. 147 | enable=c-extension-no-member 148 | 149 | 150 | [REPORTS] 151 | 152 | # Python expression which should return a score less than or equal to 10. You 153 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 154 | # which contain the number of messages in each category, as well as 'statement' 155 | # which is the total number of statements analyzed. This score is used by the 156 | # global evaluation report (RP0004). 157 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 158 | 159 | # Template used to display messages. This is a python new-style format string 160 | # used to format the message information. See doc for all details. 161 | #msg-template= 162 | 163 | # Set the output format. Available formats are text, parseable, colorized, json 164 | # and msvs (visual studio). You can also give a reporter class, e.g. 165 | # mypackage.mymodule.MyReporterClass. 166 | output-format=text 167 | 168 | # Tells whether to display a full report or only the messages. 169 | reports=no 170 | 171 | # Activate the evaluation score. 172 | score=yes 173 | 174 | 175 | [REFACTORING] 176 | 177 | # Maximum number of nested blocks for function / method body 178 | max-nested-blocks=5 179 | 180 | # Complete name of functions that never returns. When checking for 181 | # inconsistent-return-statements if a never returning function is called then 182 | # it will be considered as an explicit return statement and no message will be 183 | # printed. 184 | never-returning-functions=sys.exit 185 | 186 | 187 | [BASIC] 188 | 189 | # Naming style matching correct argument names. 190 | argument-naming-style=snake_case 191 | 192 | # Regular expression matching correct argument names. Overrides argument- 193 | # naming-style. 194 | #argument-rgx= 195 | 196 | # Naming style matching correct attribute names. 197 | attr-naming-style=snake_case 198 | 199 | # Regular expression matching correct attribute names. Overrides attr-naming- 200 | # style. 201 | #attr-rgx= 202 | 203 | # Bad variable names which should always be refused, separated by a comma. 204 | bad-names=foo, 205 | bar, 206 | baz, 207 | toto, 208 | tutu, 209 | tata 210 | 211 | # Bad variable names regexes, separated by a comma. If names match any regex, 212 | # they will always be refused 213 | bad-names-rgxs= 214 | 215 | # Naming style matching correct class attribute names. 216 | class-attribute-naming-style=any 217 | 218 | # Regular expression matching correct class attribute names. Overrides class- 219 | # attribute-naming-style. 220 | #class-attribute-rgx= 221 | 222 | # Naming style matching correct class names. 223 | class-naming-style=PascalCase 224 | 225 | # Regular expression matching correct class names. Overrides class-naming- 226 | # style. 227 | #class-rgx= 228 | 229 | # Naming style matching correct constant names. 230 | const-naming-style=UPPER_CASE 231 | 232 | # Regular expression matching correct constant names. Overrides const-naming- 233 | # style. 234 | #const-rgx= 235 | 236 | # Minimum line length for functions/classes that require docstrings, shorter 237 | # ones are exempt. 238 | docstring-min-length=-1 239 | 240 | # Naming style matching correct function names. 241 | function-naming-style=snake_case 242 | 243 | # Regular expression matching correct function names. Overrides function- 244 | # naming-style. 245 | #function-rgx= 246 | 247 | # Good variable names which should always be accepted, separated by a comma. 248 | good-names=i, 249 | j, 250 | k, 251 | m, 252 | n, 253 | ex, 254 | Run, 255 | _ 256 | 257 | # Good variable names regexes, separated by a comma. If names match any regex, 258 | # they will always be accepted 259 | good-names-rgxs= 260 | 261 | # Include a hint for the correct naming format with invalid-name. 262 | include-naming-hint=no 263 | 264 | # Naming style matching correct inline iteration names. 265 | inlinevar-naming-style=any 266 | 267 | # Regular expression matching correct inline iteration names. Overrides 268 | # inlinevar-naming-style. 269 | #inlinevar-rgx= 270 | 271 | # Naming style matching correct method names. 272 | method-naming-style=snake_case 273 | 274 | # Regular expression matching correct method names. Overrides method-naming- 275 | # style. 276 | #method-rgx= 277 | 278 | # Naming style matching correct module names. 279 | module-naming-style=snake_case 280 | 281 | # Regular expression matching correct module names. Overrides module-naming- 282 | # style. 283 | #module-rgx= 284 | 285 | # Colon-delimited sets of names that determine each other's naming style when 286 | # the name regexes allow several styles. 287 | name-group= 288 | 289 | # Regular expression which should only match function or class names that do 290 | # not require a docstring. 291 | no-docstring-rgx=^_ 292 | 293 | # List of decorators that produce properties, such as abc.abstractproperty. Add 294 | # to this list to register other decorators that produce valid properties. 295 | # These decorators are taken in consideration only for invalid-name. 296 | property-classes=abc.abstractproperty 297 | 298 | # Naming style matching correct variable names. 299 | variable-naming-style=snake_case 300 | 301 | # Regular expression matching correct variable names. Overrides variable- 302 | # naming-style. 303 | #variable-rgx= 304 | 305 | 306 | [TYPECHECK] 307 | 308 | # List of decorators that produce context managers, such as 309 | # contextlib.contextmanager. Add to this list to register other decorators that 310 | # produce valid context managers. 311 | contextmanager-decorators=contextlib.contextmanager 312 | 313 | # List of members which are set dynamically and missed by pylint inference 314 | # system, and so shouldn't trigger E1101 when accessed. Python regular 315 | # expressions are accepted. 316 | generated-members= 317 | 318 | # Tells whether missing members accessed in mixin class should be ignored. A 319 | # mixin class is detected if its name ends with "mixin" (case insensitive). 320 | ignore-mixin-members=yes 321 | 322 | # Tells whether to warn about missing members when the owner of the attribute 323 | # is inferred to be None. 324 | ignore-none=yes 325 | 326 | # This flag controls whether pylint should warn about no-member and similar 327 | # checks whenever an opaque object is returned when inferring. The inference 328 | # can return multiple potential results while evaluating a Python object, but 329 | # some branches might not be evaluated, which results in partial inference. In 330 | # that case, it might be useful to still emit no-member and other checks for 331 | # the rest of the inferred objects. 332 | ignore-on-opaque-inference=yes 333 | 334 | # List of class names for which member attributes should not be checked (useful 335 | # for classes with dynamically set attributes). This supports the use of 336 | # qualified names. 337 | ignored-classes=optparse.Values,thread._local,_thread._local 338 | 339 | # List of module names for which member attributes should not be checked 340 | # (useful for modules/projects where namespaces are manipulated during runtime 341 | # and thus existing member attributes cannot be deduced by static analysis). It 342 | # supports qualified module names, as well as Unix pattern matching. 343 | ignored-modules= 344 | 345 | # Show a hint with possible names when a member name was not found. The aspect 346 | # of finding the hint is based on edit distance. 347 | missing-member-hint=yes 348 | 349 | # The minimum edit distance a name should have in order to be considered a 350 | # similar match for a missing member name. 351 | missing-member-hint-distance=1 352 | 353 | # The total number of similar names that should be taken in consideration when 354 | # showing a hint for a missing member. 355 | missing-member-max-choices=1 356 | 357 | # List of decorators that change the signature of a decorated function. 358 | signature-mutators= 359 | 360 | 361 | [SPELLING] 362 | 363 | # Limits count of emitted suggestions for spelling mistakes. 364 | max-spelling-suggestions=4 365 | 366 | # Spelling dictionary name. Available dictionaries: none. To make it work, 367 | # install the python-enchant package. 368 | spelling-dict= 369 | 370 | # List of comma separated words that should not be checked. 371 | spelling-ignore-words= 372 | 373 | # A path to a file that contains the private dictionary; one word per line. 374 | spelling-private-dict-file= 375 | 376 | # Tells whether to store unknown words to the private dictionary (see the 377 | # --spelling-private-dict-file option) instead of raising a message. 378 | spelling-store-unknown-words=no 379 | 380 | 381 | [SIMILARITIES] 382 | 383 | # Ignore comments when computing similarities. 384 | ignore-comments=yes 385 | 386 | # Ignore docstrings when computing similarities. 387 | ignore-docstrings=yes 388 | 389 | # Ignore imports when computing similarities. 390 | ignore-imports=no 391 | 392 | # Minimum lines number of a similarity. 393 | min-similarity-lines=4 394 | 395 | 396 | [VARIABLES] 397 | 398 | # List of additional names supposed to be defined in builtins. Remember that 399 | # you should avoid defining new builtins when possible. 400 | additional-builtins= 401 | 402 | # Tells whether unused global variables should be treated as a violation. 403 | allow-global-unused-variables=yes 404 | 405 | # List of strings which can identify a callback function by name. A callback 406 | # name must start or end with one of those strings. 407 | callbacks=cb_, 408 | _cb 409 | 410 | # A regular expression matching the name of dummy variables (i.e. expected to 411 | # not be used). 412 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 413 | 414 | # Argument names that match this expression will be ignored. Default to name 415 | # with leading underscore. 416 | ignored-argument-names=_.*|^ignored_|^unused_|^platform$ 417 | 418 | # Tells whether we should check for unused import in __init__ files. 419 | init-import=no 420 | 421 | # List of qualified module names which can have objects that can redefine 422 | # builtins. 423 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 424 | 425 | 426 | [LOGGING] 427 | 428 | # The type of string formatting that logging methods do. `old` means using % 429 | # formatting, `new` is for `{}` formatting. 430 | logging-format-style=old 431 | 432 | # Logging modules to check that the string format arguments are in logging 433 | # function parameter format. 434 | logging-modules=logging 435 | 436 | 437 | [STRING] 438 | 439 | # This flag controls whether inconsistent-quotes generates a warning when the 440 | # character used as a quote delimiter is used inconsistently within a module. 441 | check-quote-consistency=no 442 | 443 | # This flag controls whether the implicit-str-concat should generate a warning 444 | # on implicit string concatenation in sequences defined over several lines. 445 | check-str-concat-over-line-jumps=no 446 | 447 | 448 | [MISCELLANEOUS] 449 | 450 | # List of note tags to take in consideration, separated by a comma. 451 | notes=FIXME, 452 | XXX, 453 | TODO 454 | 455 | # Regular expression of note tags to take in consideration. 456 | #notes-rgx= 457 | 458 | 459 | [FORMAT] 460 | 461 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 462 | expected-line-ending-format= 463 | 464 | # Regexp for a line that is allowed to be longer than the limit. 465 | ignore-long-lines=^\s*(# )??$ 466 | 467 | # Number of spaces of indent required inside a hanging or continued line. 468 | indent-after-paren=4 469 | 470 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 471 | # tab). 472 | indent-string=' ' 473 | 474 | # Maximum number of characters on a single line. 475 | max-line-length=100 476 | 477 | # Maximum number of lines in a module. 478 | max-module-lines=1000 479 | 480 | # Allow the body of a class to be on the same line as the declaration if body 481 | # contains single statement. 482 | single-line-class-stmt=no 483 | 484 | # Allow the body of an if to be on the same line as the test if there is no 485 | # else. 486 | single-line-if-stmt=no 487 | 488 | 489 | [IMPORTS] 490 | 491 | # List of modules that can be imported at any level, not just the top level 492 | # one. 493 | allow-any-import-level= 494 | 495 | # Allow wildcard imports from modules that define __all__. 496 | allow-wildcard-with-all=no 497 | 498 | # Analyse import fallback blocks. This can be used to support both Python 2 and 499 | # 3 compatible code, which means that the block might have code that exists 500 | # only in one or another interpreter, leading to false positives when analysed. 501 | analyse-fallback-blocks=no 502 | 503 | # Deprecated modules which should not be used, separated by a comma. 504 | deprecated-modules=optparse,tkinter.tix 505 | 506 | # Create a graph of external dependencies in the given file (report RP0402 must 507 | # not be disabled). 508 | ext-import-graph= 509 | 510 | # Create a graph of every (i.e. internal and external) dependencies in the 511 | # given file (report RP0402 must not be disabled). 512 | import-graph= 513 | 514 | # Create a graph of internal dependencies in the given file (report RP0402 must 515 | # not be disabled). 516 | int-import-graph= 517 | 518 | # Force import order to recognize a module as part of the standard 519 | # compatibility libraries. 520 | known-standard-library= 521 | 522 | # Force import order to recognize a module as part of a third party library. 523 | known-third-party=enchant 524 | 525 | # Couples of modules and preferred modules, separated by a comma. 526 | preferred-modules= 527 | 528 | 529 | [CLASSES] 530 | 531 | # List of method names used to declare (i.e. assign) instance attributes. 532 | defining-attr-methods=__init__, 533 | __new__, 534 | setUp, 535 | __post_init__ 536 | 537 | # List of member names, which should be excluded from the protected access 538 | # warning. 539 | exclude-protected=_asdict, 540 | _fields, 541 | _replace, 542 | _source, 543 | _make 544 | 545 | # List of valid names for the first argument in a class method. 546 | valid-classmethod-first-arg=cls 547 | 548 | # List of valid names for the first argument in a metaclass class method. 549 | valid-metaclass-classmethod-first-arg=cls 550 | 551 | 552 | [DESIGN] 553 | 554 | # Maximum number of arguments for function / method. 555 | max-args=5 556 | 557 | # Maximum number of attributes for a class (see R0902). 558 | max-attributes=7 559 | 560 | # Maximum number of boolean expressions in an if statement (see R0916). 561 | max-bool-expr=5 562 | 563 | # Maximum number of branch for function / method body. 564 | max-branches=12 565 | 566 | # Maximum number of locals for function / method body. 567 | max-locals=15 568 | 569 | # Maximum number of parents for a class (see R0901). 570 | max-parents=7 571 | 572 | # Maximum number of public methods for a class (see R0904). 573 | max-public-methods=20 574 | 575 | # Maximum number of return / yield for function / method body. 576 | max-returns=6 577 | 578 | # Maximum number of statements in function / method body. 579 | max-statements=50 580 | 581 | # Minimum number of public methods for a class (see R0903). 582 | min-public-methods=2 583 | 584 | 585 | [EXCEPTIONS] 586 | 587 | # Exceptions that will emit a warning when being caught. Defaults to 588 | # "BaseException, Exception". 589 | overgeneral-exceptions=BaseException, 590 | Exception 591 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CERN Open Hardware Licence Version 2 - Weakly Reciprocal 2 | 3 | 4 | Preamble 5 | 6 | CERN has developed this licence to promote collaboration among 7 | hardware designers and to provide a legal tool which supports the 8 | freedom to use, study, modify, share and distribute hardware designs 9 | and products based on those designs. Version 2 of the CERN Open 10 | Hardware Licence comes in three variants: CERN-OHL-P (permissive); and 11 | two reciprocal licences: this licence, CERN-OHL-W (weakly reciprocal) 12 | and CERN-OHL-S (strongly reciprocal). 13 | 14 | The CERN-OHL-W is copyright CERN 2020. Anyone is welcome to use it, in 15 | unmodified form only. 16 | 17 | Use of this Licence does not imply any endorsement by CERN of any 18 | Licensor or their designs nor does it imply any involvement by CERN in 19 | their development. 20 | 21 | 22 | 1 Definitions 23 | 24 | 1.1 'Licence' means this CERN-OHL-W. 25 | 26 | 1.2 'Compatible Licence' means 27 | 28 | a) any earlier version of the CERN Open Hardware licence, or 29 | 30 | b) any version of the CERN-OHL-S or the CERN-OHL-W, or 31 | 32 | c) any licence which permits You to treat the Source to which 33 | it applies as licensed under CERN-OHL-S or CERN-OHL-W 34 | provided that on Conveyance of any such Source, or any 35 | associated Product You treat the Source in question as being 36 | licensed under CERN-OHL-S or CERN-OHL-W as appropriate. 37 | 38 | 1.3 'Source' means information such as design materials or digital 39 | code which can be applied to Make or test a Product or to 40 | prepare a Product for use, Conveyance or sale, regardless of its 41 | medium or how it is expressed. It may include Notices. 42 | 43 | 1.4 'Covered Source' means Source that is explicitly made available 44 | under this Licence. 45 | 46 | 1.5 'Product' means any device, component, work or physical object, 47 | whether in finished or intermediate form, arising from the use, 48 | application or processing of Covered Source. 49 | 50 | 1.6 'Make' means to create or configure something, whether by 51 | manufacture, assembly, compiling, loading or applying Covered 52 | Source or another Product or otherwise. 53 | 54 | 1.7 'Available Component' means any part, sub-assembly, library or 55 | code which: 56 | 57 | a) is licensed to You as Complete Source under a Compatible 58 | Licence; or 59 | 60 | b) is available, at the time a Product or the Source containing 61 | it is first Conveyed, to You and any other prospective 62 | licensees 63 | 64 | i) with sufficient rights and information (including any 65 | configuration and programming files and information 66 | about its characteristics and interfaces) to enable it 67 | either to be Made itself, or to be sourced and used to 68 | Make the Product; or 69 | ii) as part of the normal distribution of a tool used to 70 | design or Make the Product. 71 | 72 | 1.8 'External Material' means anything (including Source) which: 73 | 74 | a) is only combined with Covered Source in such a way that it 75 | interfaces with the Covered Source using a documented 76 | interface which is described in the Covered Source; and 77 | 78 | b) is not a derivative of or contains Covered Source, or, if it 79 | is, it is solely to the extent necessary to facilitate such 80 | interfacing. 81 | 82 | 1.9 'Complete Source' means the set of all Source necessary to Make 83 | a Product, in the preferred form for making modifications, 84 | including necessary installation and interfacing information 85 | both for the Product, and for any included Available Components. 86 | If the format is proprietary, it must also be made available in 87 | a format (if the proprietary tool can create it) which is 88 | viewable with a tool available to potential licensees and 89 | licensed under a licence approved by the Free Software 90 | Foundation or the Open Source Initiative. Complete Source need 91 | not include the Source of any Available Component, provided that 92 | You include in the Complete Source sufficient information to 93 | enable a recipient to Make or source and use the Available 94 | Component to Make the Product. 95 | 96 | 1.10 'Source Location' means a location where a Licensor has placed 97 | Covered Source, and which that Licensor reasonably believes will 98 | remain easily accessible for at least three years for anyone to 99 | obtain a digital copy. 100 | 101 | 1.11 'Notice' means copyright, acknowledgement and trademark notices, 102 | Source Location references, modification notices (subsection 103 | 3.3(b)) and all notices that refer to this Licence and to the 104 | disclaimer of warranties that are included in the Covered 105 | Source. 106 | 107 | 1.12 'Licensee' or 'You' means any person exercising rights under 108 | this Licence. 109 | 110 | 1.13 'Licensor' means a natural or legal person who creates or 111 | modifies Covered Source. A person may be a Licensee and a 112 | Licensor at the same time. 113 | 114 | 1.14 'Convey' means to communicate to the public or distribute. 115 | 116 | 117 | 2 Applicability 118 | 119 | 2.1 This Licence governs the use, copying, modification, Conveying 120 | of Covered Source and Products, and the Making of Products. By 121 | exercising any right granted under this Licence, You irrevocably 122 | accept these terms and conditions. 123 | 124 | 2.2 This Licence is granted by the Licensor directly to You, and 125 | shall apply worldwide and without limitation in time. 126 | 127 | 2.3 You shall not attempt to restrict by contract or otherwise the 128 | rights granted under this Licence to other Licensees. 129 | 130 | 2.4 This Licence is not intended to restrict fair use, fair dealing, 131 | or any other similar right. 132 | 133 | 134 | 3 Copying, Modifying and Conveying Covered Source 135 | 136 | 3.1 You may copy and Convey verbatim copies of Covered Source, in 137 | any medium, provided You retain all Notices. 138 | 139 | 3.2 You may modify Covered Source, other than Notices, provided that 140 | You irrevocably undertake to make that modified Covered Source 141 | available from a Source Location should You Convey a Product in 142 | circumstances where the recipient does not otherwise receive a 143 | copy of the modified Covered Source. In each case subsection 3.3 144 | shall apply. 145 | 146 | You may only delete Notices if they are no longer applicable to 147 | the corresponding Covered Source as modified by You and You may 148 | add additional Notices applicable to Your modifications. 149 | 150 | 3.3 You may Convey modified Covered Source (with the effect that You 151 | shall also become a Licensor) provided that You: 152 | 153 | a) retain Notices as required in subsection 3.2; 154 | 155 | b) add a Notice to the modified Covered Source stating that You 156 | have modified it, with the date and brief description of how 157 | You have modified it; 158 | 159 | c) add a Source Location Notice for the modified Covered Source 160 | if You Convey in circumstances where the recipient does not 161 | otherwise receive a copy of the modified Covered Source; and 162 | 163 | d) license the modified Covered Source under the terms and 164 | conditions of this Licence (or, as set out in subsection 165 | 8.3, a later version, if permitted by the licence of the 166 | original Covered Source). Such modified Covered Source must 167 | be licensed as a whole, but excluding Available Components 168 | contained in it or External Material to which it is 169 | interfaced, which remain licensed under their own applicable 170 | licences. 171 | 172 | 173 | 4 Making and Conveying Products 174 | 175 | 4.1 You may Make Products, and/or Convey them, provided that You 176 | either provide each recipient with a copy of the Complete Source 177 | or ensure that each recipient is notified of the Source Location 178 | of the Complete Source. That Complete Source includes Covered 179 | Source and You must accordingly satisfy Your obligations set out 180 | in subsection 3.3. If specified in a Notice, the Product must 181 | visibly and securely display the Source Location on it or its 182 | packaging or documentation in the manner specified in that 183 | Notice. 184 | 185 | 4.2 Where You Convey a Product which incorporates External Material, 186 | the Complete Source for that Product which You are required to 187 | provide under subsection 4.1 need not include any Source for the 188 | External Material. 189 | 190 | 4.3 You may license Products under terms of Your choice, provided 191 | that such terms do not restrict or attempt to restrict any 192 | recipients' rights under this Licence to the Covered Source. 193 | 194 | 195 | 5 Research and Development 196 | 197 | You may Convey Covered Source, modified Covered Source or Products to 198 | a legal entity carrying out development, testing or quality assurance 199 | work on Your behalf provided that the work is performed on terms which 200 | prevent the entity from both using the Source or Products for its own 201 | internal purposes and Conveying the Source or Products or any 202 | modifications to them to any person other than You. Any modifications 203 | made by the entity shall be deemed to be made by You pursuant to 204 | subsection 3.2. 205 | 206 | 207 | 6 DISCLAIMER AND LIABILITY 208 | 209 | 6.1 DISCLAIMER OF WARRANTY -- The Covered Source and any Products 210 | are provided 'as is' and any express or implied warranties, 211 | including, but not limited to, implied warranties of 212 | merchantability, of satisfactory quality, non-infringement of 213 | third party rights, and fitness for a particular purpose or use 214 | are disclaimed in respect of any Source or Product to the 215 | maximum extent permitted by law. The Licensor makes no 216 | representation that any Source or Product does not or will not 217 | infringe any patent, copyright, trade secret or other 218 | proprietary right. The entire risk as to the use, quality, and 219 | performance of any Source or Product shall be with You and not 220 | the Licensor. This disclaimer of warranty is an essential part 221 | of this Licence and a condition for the grant of any rights 222 | granted under this Licence. 223 | 224 | 6.2 EXCLUSION AND LIMITATION OF LIABILITY -- The Licensor shall, to 225 | the maximum extent permitted by law, have no liability for 226 | direct, indirect, special, incidental, consequential, exemplary, 227 | punitive or other damages of any character including, without 228 | limitation, procurement of substitute goods or services, loss of 229 | use, data or profits, or business interruption, however caused 230 | and on any theory of contract, warranty, tort (including 231 | negligence), product liability or otherwise, arising in any way 232 | in relation to the Covered Source, modified Covered Source 233 | and/or the Making or Conveyance of a Product, even if advised of 234 | the possibility of such damages, and You shall hold the 235 | Licensor(s) free and harmless from any liability, costs, 236 | damages, fees and expenses, including claims by third parties, 237 | in relation to such use. 238 | 239 | 240 | 7 Patents 241 | 242 | 7.1 Subject to the terms and conditions of this Licence, each 243 | Licensor hereby grants to You a perpetual, worldwide, 244 | non-exclusive, no-charge, royalty-free, irrevocable (except as 245 | stated in subsections 7.2 and 8.4) patent licence to Make, have 246 | Made, use, offer to sell, sell, import, and otherwise transfer 247 | the Covered Source and Products, where such licence applies only 248 | to those patent claims licensable by such Licensor that are 249 | necessarily infringed by exercising rights under the Covered 250 | Source as Conveyed by that Licensor. 251 | 252 | 7.2 If You institute patent litigation against any entity (including 253 | a cross-claim or counterclaim in a lawsuit) alleging that the 254 | Covered Source or a Product constitutes direct or contributory 255 | patent infringement, or You seek any declaration that a patent 256 | licensed to You under this Licence is invalid or unenforceable 257 | then any rights granted to You under this Licence shall 258 | terminate as of the date such process is initiated. 259 | 260 | 261 | 8 General 262 | 263 | 8.1 If any provisions of this Licence are or subsequently become 264 | invalid or unenforceable for any reason, the remaining 265 | provisions shall remain effective. 266 | 267 | 8.2 You shall not use any of the name (including acronyms and 268 | abbreviations), image, or logo by which the Licensor or CERN is 269 | known, except where needed to comply with section 3, or where 270 | the use is otherwise allowed by law. Any such permitted use 271 | shall be factual and shall not be made so as to suggest any kind 272 | of endorsement or implication of involvement by the Licensor or 273 | its personnel. 274 | 275 | 8.3 CERN may publish updated versions and variants of this Licence 276 | which it considers to be in the spirit of this version, but may 277 | differ in detail to address new problems or concerns. New 278 | versions will be published with a unique version number and a 279 | variant identifier specifying the variant. If the Licensor has 280 | specified that a given variant applies to the Covered Source 281 | without specifying a version, You may treat that Covered Source 282 | as being released under any version of the CERN-OHL with that 283 | variant. If no variant is specified, the Covered Source shall be 284 | treated as being released under CERN-OHL-S. The Licensor may 285 | also specify that the Covered Source is subject to a specific 286 | version of the CERN-OHL or any later version in which case You 287 | may apply this or any later version of CERN-OHL with the same 288 | variant identifier published by CERN. 289 | 290 | You may treat Covered Source licensed under CERN-OHL-W as 291 | licensed under CERN-OHL-S if and only if all Available 292 | Components referenced in the Covered Source comply with the 293 | corresponding definition of Available Component for CERN-OHL-S. 294 | 295 | 8.4 This Licence shall terminate with immediate effect if You fail 296 | to comply with any of its terms and conditions. 297 | 298 | 8.5 However, if You cease all breaches of this Licence, then Your 299 | Licence from any Licensor is reinstated unless such Licensor has 300 | terminated this Licence by giving You, while You remain in 301 | breach, a notice specifying the breach and requiring You to cure 302 | it within 30 days, and You have failed to come into compliance 303 | in all material respects by the end of the 30 day period. Should 304 | You repeat the breach after receipt of a cure notice and 305 | subsequent reinstatement, this Licence will terminate 306 | immediately and permanently. Section 6 shall continue to apply 307 | after any termination. 308 | 309 | 8.6 This Licence shall not be enforceable except by a Licensor 310 | acting as such, and third party beneficiary rights are 311 | specifically excluded. 312 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adat-core 2 | FPGA implementation of an ADAT receiver/transmitter using amaranth HDL 3 | 4 | Status: 5 | 6 | * The receiver core has been tested working on FPGA 7 | * The transmitter core has been tested working on FPGA 8 | -------------------------------------------------------------------------------- /adat/__init__.py: -------------------------------------------------------------------------------- 1 | from .receiver import * 2 | from .transmitter import * -------------------------------------------------------------------------------- /adat/nrzidecoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Hans Baier 4 | # SPDX-License-Identifier: CERN-OHL-W-2.0 5 | # 6 | """Find bit timing and decode NRZI""" 7 | 8 | import math 9 | 10 | from amaranth import Elaboratable, Signal, Module 11 | from amaranth.lib.cdc import FFSynchronizer 12 | 13 | from amlib.utils.dividingcounter import DividingCounter 14 | 15 | class NRZIDecoder(Elaboratable): 16 | """Converts a NRZI encoded ADAT stream into a synchronous stream of bits""" 17 | def __init__(self, clk_freq: int): 18 | self.nrzi_in = Signal() 19 | self.invalid_frame_in = Signal() 20 | self.data_out = Signal() 21 | self.data_out_en = Signal() 22 | self.recovered_clock_out = Signal() 23 | self.running = Signal() 24 | self.clk_freq = clk_freq 25 | 26 | @staticmethod 27 | def adat_freq(samplerate: int = 48000) -> int: 28 | """calculate the ADAT bit rate for the given samplerate""" 29 | return samplerate * ((24 + 6) * 8 + 1 + 10 + 1 + 4) 30 | 31 | def elaborate(self, platform) -> Module: 32 | """assemble the module""" 33 | m = Module() 34 | 35 | comb = m.d.comb 36 | sync = m.d.sync 37 | 38 | nrzi = Signal() 39 | nrzi_prev = Signal() 40 | got_edge = Signal() 41 | 42 | m.submodules.cdc = FFSynchronizer(self.nrzi_in, nrzi) 43 | sync += nrzi_prev.eq(nrzi) 44 | comb += got_edge.eq(nrzi_prev ^ nrzi) 45 | 46 | # we are looking for 10 non changing bits 47 | # and those will be ~900ns long @48kHz 48 | # and if we clock at not more than 100MHz 49 | # the counter will run up to 900ns/10ns = 90 50 | # so 7 bits will suffice for the counter 51 | sync_counter = DividingCounter(divisor=12, width=7) 52 | m.submodules.sync_counter = sync_counter 53 | bit_time = sync_counter.divided_counter_out 54 | 55 | with m.FSM(): 56 | with m.State("SYNC"): 57 | comb += self.running.eq(0) 58 | sync += [ 59 | self.data_out.eq(0), 60 | self.data_out_en.eq(0), 61 | sync_counter.reset_in.eq(0) 62 | ] 63 | self.find_bit_timings(m, sync_counter, got_edge) 64 | 65 | with m.State("DECODE"): 66 | comb += self.running.eq(1) 67 | self.decode_nrzi(m, bit_time, got_edge, sync_counter) 68 | 69 | return m 70 | 71 | def find_bit_timings(self, m: Module, sync_counter: DividingCounter, got_edge: Signal): 72 | """Waits for the ten zero bits of the SYNC section to determine the length of an ADAT bit""" 73 | sync = m.d.sync 74 | bit_time_44100 = math.ceil(110 * (self.clk_freq/self.adat_freq(44100) / 100)) 75 | 76 | # as long as the input does not change, count up 77 | # else reset 78 | with m.If(got_edge): 79 | # if the sync counter is 10% over the sync time @44100Hz, then 80 | # the signal just woke up from the dead. Start counting again. 81 | with m.If(sync_counter.counter_out > 10 * bit_time_44100): 82 | sync += sync_counter.reset_in.eq(1) 83 | 84 | # if we are in the middle of the signal, 85 | # and got an edge, then we reset the counter on each edge 86 | with m.Else(): 87 | # when the counter is bigger than 3/4 of the old max, then we have a sync frame 88 | with m.If(sync_counter.counter_out > 7 * bit_time_44100): 89 | sync += sync_counter.active_in.eq(0) # stop counting, we found it 90 | m.next = "DECODE" 91 | with m.Else(): 92 | sync += sync_counter.reset_in.eq(1) 93 | 94 | # when we have no edge, count... 95 | with m.Else(): 96 | sync += [ 97 | sync_counter.reset_in.eq(0), 98 | sync_counter.active_in.eq(1) 99 | ] 100 | 101 | def decode_nrzi(self, m: Module, bit_time: Signal, got_edge: Signal, sync_counter: DividingCounter): 102 | """Do the actual decoding of the NRZI bitstream""" 103 | sync = m.d.sync 104 | bit_counter = Signal(7) 105 | # this counter is used to detect a dead signal 106 | # to determine when to go back to SYNC state 107 | dead_counter = Signal(8) 108 | output = Signal(reset=1) 109 | 110 | # recover ADAT clock 111 | with m.If(bit_counter <= (bit_time >> 1)): 112 | m.d.comb += self.recovered_clock_out.eq(1) 113 | with m.Else(): 114 | m.d.comb += self.recovered_clock_out.eq(0) 115 | 116 | # when the frame decoder got garbage 117 | # then we need to go back to SYNC state 118 | with m.If(self.invalid_frame_in): 119 | sync += [ 120 | sync_counter.reset_in.eq(1), 121 | bit_counter.eq(0), 122 | dead_counter.eq(0) 123 | ] 124 | m.next = "SYNC" 125 | 126 | sync += bit_counter.eq(bit_counter + 1) 127 | with m.If(got_edge): 128 | sync += [ 129 | # latch 1 until we read it in the middle of the bit 130 | output.eq(1), 131 | # resynchronize at each bit edge, 1 to compensate 132 | # for sync delay 133 | bit_counter.eq(1), 134 | # when we get an edge, the signal is alive, reset counter 135 | dead_counter.eq(0) 136 | ] 137 | with m.Else(): 138 | sync += dead_counter.eq(dead_counter + 1) 139 | 140 | # wrap the counter 141 | with m.If(bit_counter == bit_time): 142 | sync += bit_counter.eq(0) 143 | # output at the middle of the bit 144 | with m.Elif(bit_counter == (bit_time >> 1)): 145 | sync += [ 146 | self.data_out.eq(output), 147 | self.data_out_en.eq(1), # pulse out_en 148 | output.eq(0) # edge has been output, wait for new edge 149 | ] 150 | with m.Else(): 151 | sync += self.data_out_en.eq(0) 152 | 153 | # when we had no edge for 16 bits worth of time 154 | # then we go back to sync state 155 | with m.If(dead_counter >= bit_time << 4): 156 | sync += dead_counter.eq(0) 157 | m.next = "SYNC" 158 | -------------------------------------------------------------------------------- /adat/receiver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Hans Baier 4 | # SPDX-License-Identifier: CERN-OHL-W-2.0 5 | # 6 | """ADAT receiver core""" 7 | from amaranth import Elaboratable, Signal, Module, Mux 8 | 9 | from adat.nrzidecoder import NRZIDecoder 10 | from amlib.utils import InputShiftRegister, EdgeToPulse 11 | 12 | class ADATReceiver(Elaboratable): 13 | """ 14 | implements the ADAT protocol 15 | """ 16 | def __init__(self, clk_freq): 17 | # I/O 18 | self.adat_in = Signal() 19 | self.addr_out = Signal(3) 20 | self.sample_out = Signal(24) 21 | self.output_enable = Signal() 22 | self.user_data_out = Signal(4) 23 | self.recovered_clock_out = Signal() 24 | self.synced_out = Signal() 25 | 26 | # Parameters 27 | self.clk_freq = clk_freq 28 | 29 | def elaborate(self, platform) -> Module: 30 | """build the module""" 31 | m = Module() 32 | sync = m.d.sync 33 | comb = m.d.comb 34 | 35 | nrzidecoder = NRZIDecoder(self.clk_freq) 36 | m.submodules.nrzi_decoder = nrzidecoder 37 | 38 | framedata_shifter = InputShiftRegister(24) 39 | m.submodules.framedata_shifter = framedata_shifter 40 | 41 | output_pulser = EdgeToPulse() 42 | m.submodules.output_pulser = output_pulser 43 | 44 | active_channel = Signal(3) 45 | # counts the number of bits output 46 | bit_counter = Signal(8) 47 | # counts the bit position inside a nibble 48 | nibble_counter = Signal(3) 49 | # counts, how many 0 bits it got in a row 50 | sync_bit_counter = Signal(4) 51 | 52 | comb += [ 53 | nrzidecoder.nrzi_in.eq(self.adat_in), 54 | self.synced_out.eq(nrzidecoder.running), 55 | self.recovered_clock_out.eq(nrzidecoder.recovered_clock_out), 56 | ] 57 | 58 | with m.FSM(): 59 | # wait for SYNC 60 | with m.State("WAIT_SYNC"): 61 | # reset invalid frame bit to be able to start again 62 | with m.If(nrzidecoder.invalid_frame_in): 63 | sync += nrzidecoder.invalid_frame_in.eq(0) 64 | 65 | with m.If(nrzidecoder.running): 66 | sync += [ 67 | bit_counter.eq(0), 68 | nibble_counter.eq(0), 69 | active_channel.eq(0), 70 | output_pulser.edge_in.eq(0) 71 | ] 72 | 73 | with m.If(nrzidecoder.data_out_en): 74 | m.d.sync += sync_bit_counter.eq(Mux(nrzidecoder.data_out, 0, sync_bit_counter + 1)) 75 | with m.If(sync_bit_counter == 9): 76 | m.d.sync += sync_bit_counter.eq(0) 77 | m.next = "READ_FRAME" 78 | 79 | with m.State("READ_FRAME"): 80 | # at which bit of bit_counter to output sample data at 81 | output_at = Signal(8) 82 | 83 | # user bits have been read 84 | with m.If(bit_counter == 5): 85 | sync += [ 86 | # output user bits 87 | self.user_data_out.eq(framedata_shifter.value_out[0:4]), 88 | # at bit 35 the first channel has been read 89 | output_at.eq(35) 90 | ] 91 | 92 | # when each channel has been read, output the channel's sample 93 | with m.If((bit_counter > 5) & (bit_counter == output_at)): 94 | sync += [ 95 | self.output_enable.eq(1), 96 | self.addr_out.eq(active_channel), 97 | self.sample_out.eq(framedata_shifter.value_out), 98 | output_at.eq(output_at + 30), 99 | active_channel.eq(active_channel + 1) 100 | ] 101 | with m.Else(): 102 | sync += self.output_enable.eq(0) 103 | 104 | # we work and count only when we get 105 | # a new bit fron the NRZI decoder 106 | with m.If(nrzidecoder.data_out_en): 107 | comb += [ 108 | framedata_shifter.bit_in.eq(nrzidecoder.data_out), 109 | # skip sync bit, which is first 110 | framedata_shifter.enable_in.eq(~(nibble_counter == 0)) 111 | ] 112 | sync += [ 113 | nibble_counter.eq(nibble_counter + 1), 114 | bit_counter.eq(bit_counter + 1), 115 | ] 116 | 117 | # check 4b/5b sync bit 118 | with m.If((nibble_counter == 0) & ~nrzidecoder.data_out): 119 | sync += nrzidecoder.invalid_frame_in.eq(1) 120 | m.next = "WAIT_SYNC" 121 | with m.Else(): 122 | sync += nrzidecoder.invalid_frame_in.eq(0) 123 | 124 | with m.If(nibble_counter >= 4): 125 | sync += nibble_counter.eq(0) 126 | 127 | # 239 channel bits and 5 user bits (including sync bits) 128 | with m.If(bit_counter >= (239 + 5)): 129 | sync += [ 130 | bit_counter.eq(0), 131 | output_pulser.edge_in.eq(1) 132 | ] 133 | m.next = "READ_SYNC" 134 | 135 | with m.Else(): 136 | comb += framedata_shifter.enable_in.eq(0) 137 | 138 | with m.If(~nrzidecoder.running): 139 | m.next = "WAIT_SYNC" 140 | 141 | # read the sync bits 142 | with m.State("READ_SYNC"): 143 | sync += [ 144 | self.output_enable.eq(output_pulser.pulse_out), 145 | self.addr_out.eq(active_channel), 146 | self.sample_out.eq(framedata_shifter.value_out), 147 | ] 148 | 149 | with m.If(nrzidecoder.data_out_en): 150 | sync += [ 151 | nibble_counter.eq(0), 152 | bit_counter.eq(bit_counter + 1), 153 | ] 154 | 155 | with m.If(bit_counter == 9): 156 | comb += [ 157 | framedata_shifter.enable_in.eq(0), 158 | framedata_shifter.clear_in.eq(1), 159 | ] 160 | 161 | #check last sync bit before sync trough 162 | with m.If((bit_counter == 0) & ~nrzidecoder.data_out): 163 | sync += nrzidecoder.invalid_frame_in.eq(1) 164 | m.next = "WAIT_SYNC" 165 | #check all the null bits in the sync trough 166 | with m.Elif((bit_counter > 0) & nrzidecoder.data_out): 167 | sync += nrzidecoder.invalid_frame_in.eq(1) 168 | m.next = "WAIT_SYNC" 169 | with m.Elif((bit_counter == 10) & ~nrzidecoder.data_out): 170 | sync += [ 171 | bit_counter.eq(0), 172 | nibble_counter.eq(0), 173 | active_channel.eq(0), 174 | output_pulser.edge_in.eq(0), 175 | nrzidecoder.invalid_frame_in.eq(0) 176 | ] 177 | m.next = "READ_FRAME" 178 | with m.Else(): 179 | sync += nrzidecoder.invalid_frame_in.eq(0) 180 | 181 | with m.If(~nrzidecoder.running): 182 | m.next = "WAIT_SYNC" 183 | 184 | return m -------------------------------------------------------------------------------- /adat/transmitter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Hans Baier 4 | # SPDX-License-Identifier: CERN-OHL-W-2.0 5 | # 6 | """ ADAT transmitter. 7 | Inputs are in the sync clock domain, 8 | ADAT output is in the ADAT clock domain 9 | """ 10 | 11 | from amaranth import Elaboratable, Signal, Module, Cat, Const, Array, Memory 12 | from amaranth.lib.fifo import AsyncFIFO 13 | 14 | from amlib.utils import NRZIEncoder 15 | 16 | 17 | class ADATTransmitter(Elaboratable): 18 | """transmit ADAT from a multiplexed stream of eight audio channels 19 | 20 | Parameters 21 | ---------- 22 | fifo_depth: capacity of the FIFO containing the ADAT frames to be transmitted 23 | 24 | Attributes 25 | ---------- 26 | adat_out: Signal 27 | the ADAT signal to be transmitted by the optical transmitter 28 | This input is unused at the moment. Instead the caller needs to ensure 29 | addr_in: Signal 30 | contains the ADAT channel number (0-7) of the current sample to be written 31 | into the currently assembled ADAT frame 32 | sample_in: Signal 33 | the 24 bit sample to be written into the channel slot given by addr_in 34 | in the currently assembled ADAT frame. The samples need to be committed 35 | in order of channel number (0-7) 36 | user_data_in: Signal 37 | the user data bits of the currently assembled frame. Will be committed, 38 | when ``last_in`` is strobed high 39 | valid_in: Signal 40 | commits the data at sample_in into the currently assembled frame, 41 | but only if ``ready_out`` is high 42 | ready_out: Signal 43 | outputs if there is space left in the transmit FIFO. It also will 44 | prevent any samples to be committed into the currently assembled ADAT frame 45 | last_in: Signal 46 | needs to be strobed when the last sample has been committed into the currently 47 | assembled ADAT frame. This will commit the user bits to the current ADAT frame 48 | fifo_level_out: Signal 49 | outputs the number of entries in the transmit FIFO 50 | underflow_out: Signal 51 | this underflow indicator will be strobed, when a new ADAT frame needs to be 52 | transmitted but the transmit FIFO is empty. In this case, the last 53 | ADAT frame will be transmitted again. 54 | """ 55 | 56 | def __init__(self, fifo_depth=9*4): 57 | self._fifo_depth = fifo_depth 58 | self.adat_out = Signal() 59 | self.addr_in = Signal(3) 60 | self.sample_in = Signal(24) 61 | self.user_data_in = Signal(4) 62 | self.valid_in = Signal() 63 | self.ready_out = Signal() 64 | self.last_in = Signal() 65 | self.fifo_level_out = Signal(range(fifo_depth+1)) 66 | self.underflow_out = Signal() 67 | 68 | self.mem = Memory(width=24, depth=8, name="sample_buffer") 69 | 70 | @staticmethod 71 | def chunks(lst: list, n: int): 72 | """Yield successive n-sized chunks from lst.""" 73 | for i in range(0, len(lst), n): 74 | yield lst[i:i + n] 75 | 76 | def elaborate(self, platform) -> Module: 77 | m = Module() 78 | sync = m.d.sync 79 | adat = m.d.adat 80 | comb = m.d.comb 81 | 82 | samples_write_port = self.mem.write_port() 83 | samples_read_port = self.mem.read_port(domain='comb') 84 | m.submodules += [samples_write_port, samples_read_port] 85 | 86 | # the highest bit in the FIFO marks a frame border 87 | frame_border_flag = 24 88 | m.submodules.transmit_fifo = transmit_fifo = AsyncFIFO(width=25, depth=self._fifo_depth, w_domain="sync", r_domain="adat") 89 | 90 | # needed for output processing 91 | m.submodules.nrzi_encoder = nrzi_encoder = NRZIEncoder() 92 | 93 | transmitted_frame = Signal(30) 94 | transmit_counter = Signal(5) 95 | 96 | comb += [ 97 | self.ready_out .eq(transmit_fifo.w_rdy), 98 | self.fifo_level_out .eq(transmit_fifo.w_level), 99 | self.adat_out .eq(nrzi_encoder.nrzi_out), 100 | nrzi_encoder.data_in .eq(transmitted_frame.bit_select(transmit_counter, 1)), 101 | self.underflow_out .eq(0) 102 | ] 103 | 104 | # 105 | # Fill the transmit FIFO in the sync domain 106 | # 107 | channel_counter = Signal(3) 108 | 109 | # make sure, en is only asserted when explicitly strobed 110 | comb += samples_write_port.en.eq(0) 111 | 112 | write_frame_border = [ 113 | transmit_fifo.w_data .eq((1 << frame_border_flag) | self.user_data_in), 114 | transmit_fifo.w_en .eq(1) 115 | ] 116 | 117 | with m.FSM(): 118 | with m.State("DATA"): 119 | with m.If(self.ready_out): 120 | with m.If(self.valid_in): 121 | comb += [ 122 | samples_write_port.data.eq(self.sample_in), 123 | samples_write_port.addr.eq(self.addr_in), 124 | samples_write_port.en.eq(1) 125 | ] 126 | 127 | with m.If(self.last_in): 128 | sync += channel_counter.eq(0) 129 | comb += write_frame_border 130 | m.next = "COMMIT" 131 | 132 | # underflow: repeat last frame 133 | with m.Elif(transmit_fifo.w_level == 0): 134 | sync += channel_counter.eq(0) 135 | comb += self.underflow_out.eq(1) 136 | comb += write_frame_border 137 | m.next = "COMMIT" 138 | 139 | with m.State("COMMIT"): 140 | with m.If(transmit_fifo.w_rdy): 141 | comb += [ 142 | self.ready_out.eq(0), 143 | samples_read_port.addr .eq(channel_counter), 144 | transmit_fifo.w_data .eq(samples_read_port.data), 145 | transmit_fifo.w_en .eq(1) 146 | ] 147 | sync += channel_counter.eq(channel_counter + 1) 148 | 149 | with m.If(channel_counter == 7): 150 | m.next = "DATA" 151 | 152 | # 153 | # Read the FIFO and send data in the adat domain 154 | # 155 | # 4b/5b coding: Every 24 bit channel has 6 nibbles. 156 | # 1 bit before the sync pad and one bit before the user data nibble 157 | filler_bits = [Const(1, 1) for _ in range(7)] 158 | 159 | adat += transmit_counter.eq(transmit_counter - 1) 160 | comb += transmit_fifo.r_en.eq(0) 161 | 162 | with m.If(transmit_counter == 0): 163 | with m.If(transmit_fifo.r_rdy): 164 | comb += transmit_fifo.r_en.eq(1) 165 | 166 | with m.If(transmit_fifo.r_data[frame_border_flag] == 0): 167 | adat += [ 168 | transmit_counter.eq(29), 169 | # generate the adat data for one channel 0b1dddd1dddd1dddd1dddd1dddd1dddd where d is the PCM audio data 170 | transmitted_frame.eq(Cat(zip(list(self.chunks(transmit_fifo.r_data[:25], 4)), filler_bits))) 171 | ] 172 | with m.Else(): 173 | adat += [ 174 | transmit_counter.eq(15), 175 | # generate the adat sync_pad along with the user_bits 0b100000000001uuuu where u is user_data 176 | transmitted_frame.eq((1 << 15) | (1 << 4) | transmit_fifo.r_data[:5]) 177 | ] 178 | 179 | with m.Else(): 180 | # this should not happen: panic / stop transmitting. 181 | adat += [ 182 | transmitted_frame.eq(0x00), 183 | transmit_counter.eq(4) 184 | ] 185 | 186 | return m 187 | -------------------------------------------------------------------------------- /doc/adat.rst: -------------------------------------------------------------------------------- 1 | ADAT Receiver and Transmitter Cores 2 | =================================== 3 | 4 | NRZI Decoder 5 | ------------ 6 | 7 | .. hdl-diagram:: ../adat/nrzidecoder.v 8 | :type: netlistsvg 9 | :module: nrzi_decoder 10 | 11 | ADAT Transmitter 12 | ---------------- 13 | .. hdl-diagram:: ../adat/transmitter.v 14 | :type: netlistsvg 15 | :module: adat_transmitter 16 | 17 | ADAT Receiver 18 | ---------------- 19 | .. hdl-diagram:: ../adat/receiver.v 20 | :type: netlistsvg 21 | :module: adat_receiver -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, '/devel/HDL/adat/adat/') 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'ADAT Receiver and Transmitter' 21 | copyright = '2021, Hans Baier' 22 | author = 'Hans Baier' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinxcontrib_hdl_diagrams', 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = [] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = [] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = 'alabaster' 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | 57 | master_doc = 'adat' 58 | '' -------------------------------------------------------------------------------- /doc/generate_doc.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import shutil 4 | 5 | from jinja2 import Environment, FileSystemLoader 6 | from sphinx.application import Sphinx 7 | from sphinx.util.docutils import docutils_namespace 8 | 9 | DOCDIR = os.path.join(os.path.abspath("."), "doc") 10 | BUILD_DIR = os.path.join(DOCDIR, "build") 11 | 12 | TEST_JINJA_DICT = { 13 | "hdl_diagrams_path": "'{}'".format(DOCDIR), 14 | "master_doc": "'adat'", 15 | "custom_variables": "''" 16 | } 17 | 18 | sphinx_dirs = { 19 | "srcdir": DOCDIR, 20 | "confdir": DOCDIR, 21 | "outdir": BUILD_DIR, 22 | "doctreedir": os.path.join(BUILD_DIR, "doctrees") 23 | } 24 | 25 | # Run the Sphinx 26 | with docutils_namespace(): 27 | app = Sphinx(buildername="html", warningiserror=True, **sphinx_dirs) 28 | app.build(force_all=True) 29 | -------------------------------------------------------------------------------- /generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Hans Baier 4 | # SPDX-License-Identifier: CERN-OHL-W-2.0 5 | # 6 | import sys 7 | from amaranth.cli import main 8 | from adat import * 9 | 10 | if __name__ == "__main__": 11 | modulename = sys.argv[1] 12 | sys.argv[1] = "generate" 13 | 14 | if modulename == "nrzidecoder": 15 | module = NRZIDecoder(100e6) 16 | main(module, name="nrzi_decoder", ports=[module.nrzi_in, module.data_out]) 17 | elif modulename == "receiver": 18 | r = ADATReceiver(100e6) 19 | main(r, name="adat_receiver", ports=[ 20 | r.clk, r.reset_in, 21 | r.adat_in, r.addr_out, 22 | r.sample_out, r.output_enable, r.user_data_out]) 23 | elif modulename == "transmitter": 24 | t = ADATTransmitter() 25 | main(t, name="adat_transmitter", ports=[ 26 | t.addr_in, 27 | t.sample_in, 28 | t.user_data_in, 29 | t.valid_in, 30 | t.ready_out, 31 | t.last_in, 32 | t.adat_out, 33 | ]) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | def scm_version(): 5 | def local_scheme(version): 6 | return version.format_choice("+{node}", "+{node}.dirty") 7 | return { 8 | "relative_to": __file__, 9 | "version_scheme": "guess-next-dev", 10 | "local_scheme": local_scheme, 11 | } 12 | 13 | setup( 14 | name="adat", 15 | use_scm_version=scm_version(), 16 | author="Hans Baier", 17 | author_email="hansfbaier@gmail.com", 18 | description="ADAT transmitter and receiver FPGA cores implemented in amaranth HDL", 19 | license="CERN-OHL-W-2.0", 20 | setup_requires=["wheel", "setuptools", "setuptools_scm"], 21 | install_requires=[ 22 | "amaranth>=0.2,<0.5", 23 | "importlib_metadata; python_version<'3.8'", 24 | ], 25 | packages=find_packages(), 26 | project_urls={ 27 | "Source Code": "https://github.com/hansfbaier/adat-core", 28 | "Bug Tracker": "https://github.com/hansfbaier/adat-core/issues", 29 | }, 30 | ) 31 | 32 | -------------------------------------------------------------------------------- /tests/nrzi-decoder-bench-44100.gtkw: -------------------------------------------------------------------------------- 1 | [*] 2 | [*] GTKWave Analyzer v3.3.108 (w)1999-2020 BSI 3 | [*] Fri Apr 9 22:04:26 2021 4 | [*] 5 | [dumpfile] "nrzi-decoder-bench-44100.vcd" 6 | [dumpfile_mtime] "Thu Jan 21 06:54:11 2021" 7 | [dumpfile_size] 1576011 8 | [savefile] "nrzi-decoder-bench-44100.gtkw" 9 | [timestart] 7440000 10 | [size] 3822 2022 11 | [pos] -1 -1 12 | *-19.631739 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 13 | [treeopen] top. 14 | [sst_width] 453 15 | [signals_width] 435 16 | [sst_expanded] 1 17 | [sst_vpaned_height] 622 18 | @28 19 | top.clk 20 | top.fsm_state 21 | [color] 3 22 | top.invalid_frame_in 23 | [color] 3 24 | top.nrzi 25 | [color] 2 26 | top.nrzi_prev 27 | @29 28 | [color] 3 29 | top.recovered_clock_out 30 | @28 31 | top.sync_counter.active_in 32 | top.got_edge 33 | @8022 34 | top.bit_counter[6:0] 35 | top.dead_counter[7:0] 36 | @8024 37 | top.sync_counter.counter_out[6:0] 38 | @20000 39 | - 40 | @8024 41 | top.sync_counter.divided_counter_out[6:0] 42 | @28 43 | top.sync_counter.active_in 44 | [color] 3 45 | top.sync_counter.reset_in 46 | top.sync_counter.rst 47 | top.nrzi_prev 48 | [color] 3 49 | top.data_out_en 50 | [color] 2 51 | top.data_out 52 | [color] 2 53 | top.running 54 | @20000 55 | - 56 | @8022 57 | top.dead_counter[7:0] 58 | @20000 59 | - 60 | [pattern_trace] 1 61 | [pattern_trace] 0 62 | -------------------------------------------------------------------------------- /tests/nrzi-decoder-bench-48000.gtkw: -------------------------------------------------------------------------------- 1 | [*] 2 | [*] GTKWave Analyzer v3.3.106 (w)1999-2020 BSI 3 | [*] Fri Jan 21 17:31:27 2022 4 | [*] 5 | [dumpfile] "./nrzi-decoder-bench-48000.vcd" 6 | [dumpfile_mtime] "Fri Jan 21 17:31:12 2022" 7 | [dumpfile_size] 1966637 8 | [savefile] "./nrzi-decoder-bench-48000.gtkw" 9 | [timestart] 0 10 | [size] 1850 1011 11 | [pos] -1 -1 12 | *-25.631739 48195000 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 13 | [treeopen] bench. 14 | [treeopen] bench.top. 15 | [treeopen] bench.top.nrzidecoder. 16 | [sst_width] 453 17 | [signals_width] 435 18 | [sst_expanded] 1 19 | [sst_vpaned_height] 622 20 | @28 21 | bench.top.clk 22 | bench.top.adat_clk 23 | bench.top.nrzidecoder.invalid_frame_in 24 | bench.top.nrzi_in 25 | bench.top.nrzidecoder.nrzi 26 | bench.top.nrzidecoder.data_out 27 | @29 28 | bench.top.nrzidecoder.data_out_en 29 | @28 30 | bench.top.nrzidecoder.recovered_clock_out 31 | @24 32 | bench.top.nrzidecoder.bit_counter[6:0] 33 | bench.top.nrzidecoder.counter_out[6:0] 34 | bench.top.nrzidecoder.dead_counter[7:0] 35 | @22 36 | bench.top.nrzidecoder.divided_counter_out[6:0] 37 | @28 38 | bench.top.nrzidecoder.fsm_state 39 | bench.top.nrzidecoder.got_edge 40 | bench.top.nrzidecoder.nrzi_prev 41 | bench.top.nrzidecoder.output 42 | bench.top.nrzidecoder.reset_in 43 | bench.top.nrzidecoder.running 44 | [pattern_trace] 1 45 | [pattern_trace] 0 46 | -------------------------------------------------------------------------------- /tests/nrzidecoder-bench.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | from amaranth.hdl.cd import ClockDomain 4 | sys.path.append('.') 5 | from amaranth.sim import Simulator, Tick 6 | from amaranth import Elaboratable, Signal, Module 7 | 8 | from adat.nrzidecoder import NRZIDecoder 9 | from testdata import one_empty_adat_frame, \ 10 | sixteen_frames_with_channel_num_msb_and_sample_num, \ 11 | encode_nrzi, validate_output 12 | 13 | # This class simplifies testing since the nrzidecoder does not use the adat 14 | # domain. Therefore we simulate the input from the adat domain with this wrapper class. 15 | class NRZIDecoderTester(Elaboratable): 16 | def __init__(self, clk_freq: int): 17 | self.nrzi_in = Signal() 18 | self.invalid_frame_in = Signal() 19 | self.data_out = Signal() 20 | self.data_out_en = Signal() 21 | self.recovered_clock_out = Signal() 22 | self.clk_freq = clk_freq 23 | 24 | def elaborate(self, platform) -> Module: 25 | m = Module() 26 | m.submodules.nrzidecoder = nrzidecoder = NRZIDecoder(self.clk_freq) 27 | m.d.adat += [ 28 | nrzidecoder.nrzi_in.eq(self.nrzi_in), 29 | nrzidecoder.invalid_frame_in.eq(self.invalid_frame_in) 30 | ] 31 | m.d.sync += [ 32 | self.data_out.eq(nrzidecoder.data_out), 33 | self.data_out_en.eq(nrzidecoder.data_out_en), 34 | self.recovered_clock_out.eq(nrzidecoder.recovered_clock_out) 35 | ] 36 | return m 37 | 38 | def test_with_samplerate(samplerate: int=48000): 39 | """run adat signal simulation with the given samplerate""" 40 | # 24 bit plus the 6 nibble separator bits for eight channel 41 | # then 1 separator, 10 sync bits (zero), 1 separator and 4 user bits 42 | 43 | clk_freq = 100e6 44 | dut = NRZIDecoderTester(clk_freq) 45 | adat_freq = NRZIDecoder.adat_freq(samplerate) 46 | clockratio = clk_freq / adat_freq 47 | 48 | 49 | sim = Simulator(dut) 50 | sim.add_clock(1.0/clk_freq, domain="sync") 51 | sim.add_clock(1.0/adat_freq, domain="adat") 52 | 53 | print(f"FPGA clock freq: {clk_freq}") 54 | print(f"ADAT clock freq: {adat_freq}") 55 | print(f"FPGA/ADAT freq: {clockratio}") 56 | 57 | sixteen_adat_frames = sixteen_frames_with_channel_num_msb_and_sample_num() 58 | interrupted_adat_stream = [0] * 64 59 | 60 | testdata = one_empty_adat_frame() + \ 61 | sixteen_adat_frames[0:256] + \ 62 | interrupted_adat_stream + \ 63 | sixteen_adat_frames[256:] 64 | 65 | testdata_nrzi = encode_nrzi(testdata) 66 | 67 | no_cycles = len(testdata_nrzi) 68 | 69 | # Send the adat stream 70 | def adat_process(): 71 | bitcount :int = 0 72 | for bit in testdata_nrzi: #[224:512 * 2]: 73 | if (bitcount == 4 * 256 + 64): 74 | yield dut.invalid_frame_in.eq(1) 75 | yield Tick("adat") 76 | yield dut.invalid_frame_in.eq(0) 77 | for _ in range(20): 78 | yield Tick("adat") 79 | else: 80 | yield dut.invalid_frame_in.eq(0) 81 | 82 | yield dut.nrzi_in.eq(bit) 83 | yield Tick("adat") 84 | bitcount += 1 85 | 86 | # Process the adat stream and validate output 87 | def sync_process(): 88 | # Obtain the output data 89 | out_data = [] 90 | for _ in range(int(clockratio) * no_cycles): 91 | yield Tick("sync") 92 | if (yield dut.data_out_en == 1): 93 | bit = yield dut.data_out 94 | yield out_data.append(bit) 95 | 96 | # 97 | # Validate output 98 | # 99 | 100 | # omit a 1 at the end of the sync pad 101 | out_data = out_data[1:] 102 | 103 | # Whenever the state machine switches from SYNC to DECODE we need to omit the first 11 sync bits 104 | validate_output(out_data[:256 - 12], one_empty_adat_frame()[12:256]) 105 | out_data = out_data[256-12:] 106 | 107 | validate_output(out_data[:256], sixteen_adat_frames[:256]) 108 | out_data = out_data[256:] 109 | 110 | # now the adat stream was interrupted, it continues to output zeroes, until it enters the SYNC state 111 | validate_output(out_data[:10], interrupted_adat_stream[:10]) 112 | out_data = out_data[10:] 113 | 114 | # followed by 2 well formed adat frames 115 | 116 | # omit the first 11 sync bits 117 | validate_output(out_data[:256 - 12], sixteen_adat_frames[256 + 12:2 * 256]) 118 | out_data = out_data[256 - 12:] 119 | 120 | validate_output(out_data[:256], sixteen_adat_frames[2 * 256:3 * 256]) 121 | out_data = out_data[256:] 122 | 123 | # followed by one invalid frame - the state machine SYNCs again 124 | 125 | # followed by 13 well-formed frames 126 | 127 | # omit the first 11 sync bits 128 | validate_output(out_data[:256 - 12], sixteen_adat_frames[3 * 256 + 12:4 * 256]) 129 | out_data = out_data[256-12:] 130 | 131 | for i in range(4, 16): 132 | validate_output(out_data[:256], sixteen_adat_frames[i * 256:(i + 1) * 256]) 133 | out_data = out_data[256:] 134 | 135 | print("Success!") 136 | 137 | 138 | sim.add_sync_process(sync_process, domain="sync") 139 | sim.add_sync_process(adat_process, domain="adat") 140 | with sim.write_vcd(f'nrzi-decoder-bench-{str(samplerate)}.vcd'): 141 | sim.run() 142 | 143 | 144 | if __name__ == "__main__": 145 | test_with_samplerate(48000) 146 | test_with_samplerate(44100) 147 | -------------------------------------------------------------------------------- /tests/receiver-bench.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Hans Baier 4 | # SPDX-License-Identifier: CERN-OHL-W-2.0 5 | # 6 | import sys 7 | sys.path.append(".") 8 | 9 | from amaranth.sim import Simulator, Tick 10 | 11 | from adat.receiver import ADATReceiver 12 | from adat.nrzidecoder import NRZIDecoder 13 | from testdata import one_empty_adat_frame, \ 14 | sixteen_frames_with_channel_num_msb_and_sample_num, \ 15 | encode_nrzi, print_frame 16 | from amaranth import Elaboratable, Signal, Module 17 | 18 | # This class simplifies testing since the receiver does not use the adat domain. 19 | # Therefore we simulate the input from the adat domain with this wrapper class. 20 | class ADATReceiverTester(Elaboratable): 21 | def __init__(self, clk_freq: int): 22 | self.adat_in = Signal() 23 | self.addr_out = Signal(3) 24 | self.sample_out = Signal(24) 25 | self.output_enable = Signal() 26 | self.user_data_out = Signal(4) 27 | self.recovered_clock_out = Signal() 28 | self.synced_out = Signal() 29 | self.clk_freq = clk_freq 30 | 31 | def elaborate(self, platform) -> Module: 32 | m = Module() 33 | m.submodules.receiver = receiver = ADATReceiver(self.clk_freq) 34 | 35 | m.d.adat += receiver.adat_in.eq(self.adat_in) 36 | 37 | m.d.sync += [ 38 | self.addr_out.eq(receiver.addr_out), 39 | self.sample_out.eq(receiver.sample_out), 40 | self.output_enable.eq(receiver.output_enable), 41 | self.user_data_out.eq(receiver.user_data_out), 42 | self.recovered_clock_out.eq(receiver.recovered_clock_out), 43 | self.synced_out.eq(receiver.synced_out) 44 | ] 45 | return m 46 | 47 | 48 | def test_with_samplerate(samplerate: int=48000): 49 | """run adat signal simulation with the given samplerate""" 50 | # 24 bit plus the 6 nibble separator bits for eight channel 51 | # then 1 separator, 10 sync bits (zero), 1 separator and 4 user bits 52 | 53 | clk_freq = 100e6 54 | dut = ADATReceiverTester(clk_freq) 55 | adat_freq = NRZIDecoder.adat_freq(samplerate) 56 | clockratio = clk_freq / adat_freq 57 | 58 | sim = Simulator(dut) 59 | sim.add_clock(1.0/clk_freq, domain="sync") 60 | sim.add_clock(1.0/adat_freq, domain="adat") 61 | 62 | sixteen_adat_frames = sixteen_frames_with_channel_num_msb_and_sample_num() 63 | 64 | testdata = \ 65 | one_empty_adat_frame() + \ 66 | sixteen_adat_frames[0:256] + \ 67 | [0] * 64 + \ 68 | sixteen_adat_frames[256:] 69 | 70 | testdata_nrzi = encode_nrzi(testdata) 71 | 72 | print(f"FPGA clock freq: {clk_freq}") 73 | print(f"ADAT clock freq: {adat_freq}") 74 | print(f"FPGA/ADAT freq: {clockratio}") 75 | 76 | no_cycles = len(testdata_nrzi) + 500 77 | 78 | # Send the adat stream 79 | def adat_process(): 80 | for bit in testdata_nrzi: # [224:512 * 2]: 81 | yield dut.adat_in.eq(bit) 82 | yield Tick("adat") 83 | 84 | # Process the adat stream and validate output 85 | def sync_process(): 86 | # Obtain the output data 87 | out_data = [[0 for x in range(9)] for y in range(16)] #store userdata in the 9th column 88 | sample = 0 89 | for _ in range(int(clockratio) * no_cycles): 90 | yield Tick("sync") 91 | if (yield dut.output_enable == 1): 92 | channel = yield dut.addr_out 93 | 94 | out_data[sample][channel] = yield dut.sample_out 95 | 96 | if (channel == 7): 97 | out_data[sample][8] = yield dut.user_data_out 98 | sample += 1 99 | 100 | #print(out_data) 101 | 102 | 103 | # 104 | # The receiver needs 2 sync pads before it starts outputting data: 105 | # * The first sync pad is needed for the nrzidecoder to sync 106 | # * The second sync pad is needed for the receiver to sync 107 | # Therefore each time after the connection was lost the first frame will be lost while syncing. 108 | # In our testdata we loose the initial one_empty_adat_frame and the second sample (#1, count starts with 0) 109 | # 110 | 111 | sampleno = 0 112 | for i in range(16): 113 | if (sampleno == 1): #skip the first frame while the receiver syncs after an interruption 114 | sampleno += 1 115 | elif (sampleno == 16): #ensure the data ended as expected 116 | assert out_data[i] == [0, 0, 0, 0, 0, 0, 0, 0, 0], "Sample {} was: {}".format(sampleno, print_frame(out_data[sampleno])) 117 | else: 118 | assert out_data[i] == [((0 << 20) | sampleno), ((1 << 20) | sampleno), ((2 << 20) | sampleno), 119 | ((3 << 20) | sampleno), ((4 << 20) | sampleno), ((5 << 20) | sampleno), 120 | ((6 << 20) | sampleno), ((7 << 20) | sampleno), 0b0101]\ 121 | , "Sample #{} was: {}".format(sampleno, print_frame(out_data[sampleno])) 122 | sampleno += 1 123 | 124 | print("Success!") 125 | 126 | sim.add_sync_process(sync_process, domain="sync") 127 | sim.add_sync_process(adat_process, domain="adat") 128 | with sim.write_vcd(f'receiver-smoke-test-{str(samplerate)}.vcd'): 129 | sim.run() 130 | 131 | if __name__ == "__main__": 132 | test_with_samplerate(48000) 133 | test_with_samplerate(44100) 134 | -------------------------------------------------------------------------------- /tests/receiver-smoke-test-44100.gtkw: -------------------------------------------------------------------------------- 1 | [*] 2 | [*] GTKWave Analyzer v3.3.108 (w)1999-2020 BSI 3 | [*] Sat Jan 23 06:54:25 2021 4 | [*] 5 | [dumpfile] "receiver-smoke-test-44100.vcd" 6 | [dumpfile_mtime] "Sat Jan 23 06:54:12 2021" 7 | [dumpfile_size] 1750334 8 | [savefile] "receiver-smoke-test-44100.gtkw" 9 | [timestart] 51933600 10 | [size] 3822 2022 11 | [pos] -1 -1 12 | *-16.252075 52136700 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 13 | [treeopen] top. 14 | [treeopen] top.nrzi_decoder. 15 | [sst_width] 427 16 | [signals_width] 435 17 | [sst_expanded] 1 18 | [sst_vpaned_height] 632 19 | @28 20 | top.clk 21 | top.nrzi_decoder.cdc.nrzi 22 | @200 23 | -NRZI Decoder 24 | @29 25 | top.nrzi_decoder.running 26 | @28 27 | [color] 3 28 | top.nrzi_decoder.data_out 29 | [color] 3 30 | top.nrzi_decoder.data_out_en 31 | @200 32 | -State Machine 33 | @28 34 | [color] 2 35 | top.fsm_state 36 | @24 37 | top.bit_counter[7:0] 38 | top.nibble_counter[2:0] 39 | @200 40 | -Frame Shifter 41 | @28 42 | [color] 3 43 | top.framedata_shifter.bit_in 44 | [color] 3 45 | top.framedata_shifter.enable_in 46 | @22 47 | top.framedata_shifter.value_out[23:0] 48 | @200 49 | -Output 50 | @24 51 | top.output_at[7:0] 52 | @22 53 | top.user_data_out[3:0] 54 | @28 55 | [color] 2 56 | top.output_enable 57 | @24 58 | [color] 2 59 | top.addr_out[2:0] 60 | @22 61 | [color] 2 62 | top.sample_out[23:0] 63 | top.user_data_out[3:0] 64 | [pattern_trace] 1 65 | [pattern_trace] 0 66 | -------------------------------------------------------------------------------- /tests/receiver-smoke-test-48000.gtkw: -------------------------------------------------------------------------------- 1 | [*] 2 | [*] GTKWave Analyzer v3.3.108 (w)1999-2020 BSI 3 | [*] Sat Jan 23 02:47:43 2021 4 | [*] 5 | [dumpfile] "receiver-smoke-test-48000.vcd" 6 | [dumpfile_mtime] "Sat Jan 23 02:45:03 2021" 7 | [dumpfile_size] 1612141 8 | [savefile] "receiver-smoke-test-48000.gtkw" 9 | [timestart] 0 10 | [size] 3822 2022 11 | [pos] -1 -1 12 | *-22.114462 345670000 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 13 | [treeopen] top. 14 | [sst_width] 427 15 | [signals_width] 315 16 | [sst_expanded] 1 17 | [sst_vpaned_height] 632 18 | @28 19 | top.clk 20 | top.adat_in 21 | @200 22 | -NRZI Decoder 23 | @28 24 | [color] 3 25 | top.nrzi_decoder.data_out 26 | [color] 3 27 | top.nrzi_decoder.data_out_en 28 | @200 29 | -State Machine 30 | @28 31 | [color] 2 32 | top.fsm_state 33 | @24 34 | top.bit_counter[7:0] 35 | top.nibble_counter[2:0] 36 | @200 37 | -Frame Shifter 38 | @28 39 | [color] 3 40 | top.framedata_shifter.bit_in 41 | [color] 3 42 | top.framedata_shifter.enable_in 43 | @22 44 | top.framedata_shifter.value_out[23:0] 45 | @200 46 | -Output 47 | @24 48 | top.output_at[7:0] 49 | @22 50 | top.user_data_out[3:0] 51 | @28 52 | [color] 2 53 | top.output_enable 54 | @24 55 | [color] 2 56 | top.addr_out[2:0] 57 | @22 58 | [color] 2 59 | top.sample_out[23:0] 60 | top.user_data_out[3:0] 61 | [pattern_trace] 1 62 | [pattern_trace] 0 63 | -------------------------------------------------------------------------------- /tests/testdata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Hans Baier 4 | # SPDX-License-Identifier: CERN-OHL-W-2.0 5 | # 6 | """generate ADAT signals for simulation""" 7 | 8 | from functools import reduce 9 | 10 | def concatenate_lists(lists): 11 | """concatenate the elements of a list of lists""" 12 | return reduce(lambda a, b: a + b, lists) 13 | 14 | class TestDataGenerator: 15 | """generate ADAT input data for simulation""" 16 | sync_sequence = 10 * [0] 17 | sync = [1] 18 | 19 | @staticmethod 20 | def preamble(userbits: list = None) -> list: 21 | """append sync bits and user bits""" 22 | if userbits is None: 23 | userbits = 4 * [0] 24 | return TestDataGenerator.sync + \ 25 | TestDataGenerator.sync_sequence + \ 26 | TestDataGenerator.sync + \ 27 | userbits 28 | 29 | @staticmethod 30 | def chunks(lst: list, n: int): 31 | """Yield successive n-sized chunks from lst.""" 32 | for i in range(0, len(lst), n): 33 | yield lst[i:i + n] 34 | 35 | @staticmethod 36 | def convert_sample(sample24bit: int) -> list: 37 | """convert a 24 bit sample into an ADAT data bitstring""" 38 | bitstring = [ int(b) for b in concatenate_lists( 39 | ['1' + s for s in 40 | TestDataGenerator.chunks("{0:024b}".format(sample24bit), 4)])] 41 | return bitstring 42 | 43 | @staticmethod 44 | def generate_adat_frame(sample_8channels: list) -> list: 45 | """converts an eight channel sample into an ADAT frame""" 46 | frame = TestDataGenerator.preamble([0, 1, 0, 1]) + concatenate_lists( 47 | [TestDataGenerator.convert_sample(sample_1ch) for sample_1ch in sample_8channels]) 48 | return frame 49 | 50 | def generate_adat_frame(sample_8channels: list) -> list: 51 | """convenience method for converting an eight channel sample into an ADAT frame""" 52 | return TestDataGenerator.generate_adat_frame(sample_8channels) 53 | 54 | def one_empty_adat_frame() -> list: 55 | """generate bits of one empty adat frame (all zero content)""" 56 | return generate_adat_frame(8 * [0]) 57 | 58 | def generate_one_frame_with_channel_numbers_as_samples() -> list: 59 | """return an ADAT frame whose samples are the channel numbers""" 60 | return generate_adat_frame(range(8)) 61 | 62 | def sixteen_frames_with_channel_num_msb_and_sample_num(): 63 | """ 64 | generate sixteen ADAT frames with channel numbers in the MSBs 65 | and sample numbers in the LSBs 66 | """ 67 | samples_8ch = list(TestDataGenerator.chunks( 68 | [(channelno << 20 | sampleno) 69 | for sampleno in range(16) 70 | for channelno in range(8) 71 | ], 8)) 72 | 73 | return concatenate_lists([generate_adat_frame(sample_8ch) for sample_8ch in samples_8ch]) 74 | 75 | def encode_nrzi(bits_in: list, initial_bit: int = 1) -> list: 76 | """NRZI-encode a list of bits""" 77 | result = [initial_bit] 78 | for bit in bits_in: 79 | last_bit = result[-1] 80 | result.append(last_bit if bit == 0 else (~last_bit) & 1) 81 | return result 82 | 83 | def decode_nrzi(signal): 84 | """NRZI-decode a list of bits""" 85 | result = [] 86 | last_bit = 0 87 | for bit in signal: 88 | result.append(1 if last_bit != bit else 0) 89 | last_bit = bit 90 | return result 91 | 92 | def bits_to_int(bitlist): 93 | """convert a list of bits to integer, msb first""" 94 | out = 0 95 | for bit in bitlist: 96 | out = (out << 1) | bit 97 | return out 98 | 99 | def print_frame(frame): 100 | return ", ".join("0x{}".format(num) for num in frame) 101 | 102 | 103 | def print_assert_failure(receivedFrame, expectedFrame = None): 104 | if expectedFrame is not None: 105 | return "Expected:\n{}\nResult was:\n{}".format(print_frame(expectedFrame), print_frame(receivedFrame)) 106 | else: 107 | return "Result was: {}".format(print_frame(receivedFrame)) 108 | 109 | def print_assert_failure_context(signal): 110 | return "Next 256 signal bits are: {}".format(signal[:256]) 111 | 112 | def validate_output(receivedFrame, expectedFrame): 113 | assert receivedFrame == expectedFrame, print_assert_failure(receivedFrame, expectedFrame) 114 | 115 | 116 | def adat_decode(signal): 117 | """decode adat frames, after NRZI-decoding""" 118 | result = [] 119 | def decode_nibble(signal): 120 | nibble = signal[:4] 121 | return bits_to_int(nibble) 122 | 123 | while len(signal) >= 255: 124 | current_frame = [] 125 | assert signal.pop(0) == 1, print_assert_failure_context(signal) 126 | 127 | for i in range(10): 128 | assert signal.pop(0) == 0, "Counter was {}; {}".format(i, print_assert_failure_context(signal)) 129 | 130 | assert signal.pop(0) == 1, print_assert_failure_context(signal) 131 | 132 | user_data = decode_nibble(signal) 133 | signal = signal[4:] 134 | print("user data: " + hex(user_data)) 135 | 136 | current_frame.append(user_data) 137 | 138 | for channel in range(8): 139 | print("channel " + str(channel)) 140 | sample = 0 141 | for nibble_no in range(6): 142 | assert signal.pop(0) == 1, print_assert_failure_context(signal) 143 | nibble = decode_nibble(signal) 144 | signal = signal[4:] 145 | print("nibble: " + bin(nibble), end=" ") 146 | sample += nibble << (4 * (5 - nibble_no)) 147 | print("got sample " + hex(sample)) 148 | current_frame.append(sample) 149 | 150 | result.append(current_frame) 151 | 152 | return result 153 | 154 | if __name__ == "__main__": 155 | print(list(sixteen_frames_with_channel_num_msb_and_sample_num())) 156 | -------------------------------------------------------------------------------- /tests/transmitter-bench.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2021 Hans Baier 4 | # SPDX-License-Identifier: CERN-OHL-W-2.0 5 | # 6 | import sys 7 | sys.path.append('.') 8 | 9 | from amaranth.sim import Simulator, Tick 10 | 11 | from adat.transmitter import ADATTransmitter 12 | from adat.nrzidecoder import NRZIDecoder 13 | from testdata import * 14 | 15 | def test_with_samplerate(samplerate: int=48000): 16 | clk_freq = 50e6 17 | dut = ADATTransmitter() 18 | adat_freq = NRZIDecoder.adat_freq(samplerate) 19 | clockratio = clk_freq / adat_freq 20 | 21 | print(f"FPGA clock freq: {clk_freq}") 22 | print(f"ADAT clock freq: {adat_freq}") 23 | print(f"FPGA/ADAT freq: {clockratio}") 24 | 25 | sim = Simulator(dut) 26 | sim.add_clock(1.0/clk_freq, domain="sync") 27 | sim.add_clock(1.0/adat_freq, domain="adat") 28 | 29 | def write(addr: int, sample: int, last: bool = False, drop_valid: bool = False): 30 | while (yield dut.ready_out == 0): 31 | yield from wait(1) 32 | if last: 33 | yield dut.last_in.eq(1) 34 | yield dut.addr_in.eq(addr) 35 | yield dut.sample_in.eq(sample) 36 | yield dut.valid_in.eq(1) 37 | yield Tick("sync") 38 | if drop_valid: 39 | yield dut.valid_in.eq(0) 40 | if last: 41 | yield dut.last_in.eq(0) 42 | 43 | def wait(n_cycles: int): 44 | for _ in range(int(clockratio) * n_cycles): 45 | yield Tick("sync") 46 | 47 | def sync_process(): 48 | yield Tick("sync") 49 | yield Tick("sync") 50 | yield dut.user_data_in.eq(0xf) 51 | for i in range(4): 52 | yield from write(i, i, drop_valid=True) 53 | for i in range(4): 54 | yield from write(4 + i, 0xc + i, i == 3, drop_valid=True) 55 | yield dut.user_data_in.eq(0xa) 56 | yield Tick("sync") 57 | for i in range(8): 58 | yield from write(i, (i + 1) << 4, i == 7) 59 | yield dut.user_data_in.eq(0xb) 60 | yield Tick("sync") 61 | for i in range(8): 62 | yield from write(i, (i + 1) << 8, i == 7) 63 | yield dut.user_data_in.eq(0xc) 64 | yield Tick("sync") 65 | for i in range(8): 66 | yield from write(i, (i + 1) << 12, i == 7) 67 | yield dut.user_data_in.eq(0xd) 68 | yield Tick("sync") 69 | for i in range(8): 70 | yield from write(i, (i + 1) << 16, i == 7) 71 | yield dut.user_data_in.eq(0xe) 72 | yield Tick("sync") 73 | for i in range(8): 74 | yield from write(i, (i + 1) << 20, i == 7, drop_valid=True) 75 | yield from wait(900) 76 | 77 | def adat_process(): 78 | nrzi = [] 79 | i = 0 80 | while i < 1800: 81 | yield Tick("adat") 82 | out = yield dut.adat_out 83 | nrzi.append(out) 84 | i += 1 85 | 86 | # skip initial zeros 87 | nrzi = nrzi[nrzi.index(1):] 88 | signal = decode_nrzi(nrzi) 89 | decoded = adat_decode(signal) 90 | print(decoded) 91 | user_bits = [decoded[frame][0] for frame in range(7)] 92 | assert user_bits == [0x0, 0xf, 0xa, 0xb, 0xc, 0xd, 0xe], print_assert_failure(user_bits) 93 | assert decoded[0][1:] == [0, 0, 0, 0, 0, 0, 0, 0], print_assert_failure(decoded[0][1:]) 94 | assert decoded[1][1:] == [0, 1, 2, 3, 0xc, 0xd, 0xe, 0xf], print_assert_failure(decoded[1][1:]) 95 | assert decoded[2][1:] == [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80], print_assert_failure(decoded[2][1:]) 96 | assert decoded[3][1:] == [0x100, 0x200, 0x300, 0x400, 0x500, 0x600, 0x700, 0x800], print_assert_failure(decoded[3][1:]) 97 | assert decoded[4][1:] == [0x1000, 0x2000, 0x3000, 0x4000, 0x5000, 0x6000, 0x7000, 0x8000], print_assert_failure(decoded[4][1:]) 98 | assert decoded[5][1:] == [0x10000, 0x20000, 0x30000, 0x40000, 0x50000, 0x60000, 0x70000, 0x80000], print_assert_failure(decoded[5][1:]) 99 | 100 | sim.add_sync_process(sync_process, domain="sync") 101 | sim.add_sync_process(adat_process, domain="adat") 102 | 103 | with sim.write_vcd(f'transmitter-smoke-test-{str(samplerate)}.vcd'): 104 | sim.run() 105 | 106 | if __name__ == "__main__": 107 | test_with_samplerate(48000) -------------------------------------------------------------------------------- /tests/transmitter-smoke-test-48000.gtkw: -------------------------------------------------------------------------------- 1 | [*] 2 | [*] GTKWave Analyzer v3.4.0 (w)1999-2022 BSI 3 | [*] Fri Jan 7 22:04:56 2022 4 | [*] 5 | [dumpfile] "transmitter-smoke-test-48000.vcd" 6 | [dumpfile_mtime] "Fri Jan 7 22:01:09 2022" 7 | [dumpfile_size] 283263 8 | [savefile] "transmitter-smoke-test-48000.gtkw" 9 | [timestart] 0 10 | [size] 3828 2090 11 | [pos] -1 -1 12 | *-22.000000 40900000 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 13 | [treeopen] bench. 14 | [treeopen] bench.top. 15 | [treeopen] bench.top.transmit_fifo. 16 | [sst_width] 347 17 | [signals_width] 425 18 | [sst_expanded] 1 19 | [sst_vpaned_height] 284 20 | @24 21 | bench.top.adat_clk 22 | @28 23 | bench.top.clk 24 | @22 25 | bench.top.sample_in[23:0] 26 | @28 27 | bench.top.user_data_in[3:0] 28 | bench.top.valid_in 29 | bench.top.last_in 30 | bench.top.ready_out 31 | [color] 2 32 | bench.top.underflow_out 33 | [color] 1 34 | bench.top.adat_out 35 | bench.top.transmit_fifo.w_en 36 | @23 37 | bench.top.transmit_fifo.w_data[24:0] 38 | @800022 39 | bench.top.r_data[24:0] 40 | @1001200 41 | -group_end 42 | @28 43 | bench.top.nrzi_encoder.data_in 44 | bench.top.nrzi_encoder.nrzi_out 45 | @24 46 | bench.top.transmit_counter[4:0] 47 | [pattern_trace] 1 48 | [pattern_trace] 0 49 | --------------------------------------------------------------------------------