forked from M-Labs/artiq
Merge branch 'master' (43be383c86
) into k7-drtio
This commit is contained in:
commit
cff7bcc122
|
@ -1 +0,0 @@
|
|||
artiq/_version.py export-subst
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report a bug in ARTIQ
|
||||
|
||||
---
|
||||
|
||||
<!-- Above are non-Markdown tags for Github auto-prompting issue type. Template based on pylint: https://raw.githubusercontent.com/PyCQA/pylint/master/.github/ISSUE_TEMPLATE/ -->
|
||||
|
||||
# Bug Report
|
||||
|
||||
<!-- Thanks for reporting a bug report to ARTIQ! You can also discuss issues and ask questions on IRC (the [#m-labs channel on freenode](https://webchat.freenode.net/?channels=m-labs) or on the [forum](https://forum.m-labs.hk). Please check Github/those forums to avoid posting a repeat issue.
|
||||
|
||||
Context helps us fix issues faster, so please include the following when relevant:
|
||||
-->
|
||||
|
||||
## One-Line Summary
|
||||
|
||||
Short summary.
|
||||
|
||||
## Issue Details
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
1. Step 1.
|
||||
2. Step 2.
|
||||
3. Step 3.
|
||||
|
||||
### Expected Behavior
|
||||
|
||||
Behavior
|
||||
|
||||
### Actual (undesired) Behavior
|
||||
|
||||
* Text description
|
||||
* Log message, tracebacks, screen shots where relevant
|
||||
|
||||
### Your System (omit irrelevant parts)
|
||||
|
||||
* Operating System:
|
||||
* ARTIQ version: (with recent versions of ARTIQ, run ``artiq_client --version``)
|
||||
* Version of the gateware and runtime loaded in the core device: (in the output of ``artiq_coremgmt -D .... log``)
|
||||
* If using Conda, output of `conda list` (please submit as a file attachment, as this tends to be long)
|
||||
* Hardware involved:
|
||||
|
||||
<!--
|
||||
For in-depth information on bug reporting, see:
|
||||
|
||||
http://www.chiark.greenend.org.uk/~sgtatham/bugs.html https://developer.mozilla.org/en-US/docs/Mozilla/QA/Bug_writing_guidelines
|
||||
-->
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for ARTIQ
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there! Thank you for wanting to make ARTIQ better.
|
||||
|
||||
Before you submit this, make sure that this feature wasn't
|
||||
already requested or if it is not already implemented in the master branch.
|
||||
|
||||
Based on pylint: https://raw.githubusercontent.com/PyCQA/pylint/master/.github/ISSUE_TEMPLATE/2_Feature_request.md
|
||||
-->
|
||||
|
||||
# ARTIQ Feature Request
|
||||
|
||||
## Problem this request addresses
|
||||
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
## Describe the solution you'd like
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
## Additional context
|
||||
|
||||
Add any other context about the feature request here.
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
name: Support question
|
||||
about: Questions about ARTIQ that are not covered in the documentation
|
||||
|
||||
---
|
||||
|
||||
# Question
|
||||
|
||||
<!--
|
||||
Make sure you check the ARTIQ documentation before posting a question.
|
||||
Don't forget you can search it!
|
||||
|
||||
Beta version: https://m-labs.hk/artiq/manual-beta/
|
||||
Stable version: https://m-labs.hk/artiq/manual/
|
||||
|
||||
The forum is also a very good place for questions: https://forum.m-labs.hk/
|
||||
Can also ask on IRC: https://webchat.freenode.net/?channels=m-labs or
|
||||
check mailing list archives: https://ssl.serverraum.org/lists-archive/artiq/
|
||||
|
||||
Remember: if you have this question then others probably do too! The best way of thanking the people who help you with this issue is to contribute to ARTIQ by submitting a pull request to update the documentation.
|
||||
-->
|
||||
|
||||
## Category: FILL_IN
|
||||
|
||||
<!-- One-word category this question falls into: GUI, installation/setup, devices, development, documentation, etc. -->
|
||||
|
||||
## Description
|
||||
|
||||
Question text
|
|
@ -0,0 +1,69 @@
|
|||
<!--
|
||||
|
||||
Thank you for submitting a PR to ARTIQ!
|
||||
|
||||
To ease the process of reviewing your PR, do make sure to complete the following boxes.
|
||||
|
||||
You can also read more about contributing to ARTIQ in this document:
|
||||
https://github.com/m-labs/artiq/blob/master/CONTRIBUTING.rst#contributing-code
|
||||
|
||||
Based on https://raw.githubusercontent.com/PyCQA/pylint/master/.github/PULL_REQUEST_TEMPLATE.md
|
||||
-->
|
||||
|
||||
# ARTIQ Pull Request
|
||||
|
||||
## Description of Changes
|
||||
|
||||
### Related Issue
|
||||
|
||||
<!--
|
||||
If this PR fixes a particular issue, use the following to automatically close that issue
|
||||
once this PR gets merged:
|
||||
|
||||
Closes #XXX
|
||||
-->
|
||||
|
||||
## Type of Changes
|
||||
|
||||
<!-- Leave ONLY the corresponding lines for the applicable type of change: -->
|
||||
| | Type |
|
||||
| ------------- | ------------- |
|
||||
| ✓ | :bug: Bug fix |
|
||||
| ✓ | :sparkles: New feature |
|
||||
| ✓ | :hammer: Refactoring |
|
||||
| ✓ | :scroll: Docs |
|
||||
|
||||
## Steps (Choose relevant, delete irrelevant before submitting)
|
||||
|
||||
### All Pull Requests
|
||||
|
||||
- [x] Use correct spelling and grammar.
|
||||
- [ ] Update [RELEASE_NOTES.rst](../RELEASE_NOTES.rst) if there are noteworthy changes, especially if there are changes to existing APIs.
|
||||
- [ ] Close/update issues.
|
||||
- [ ] Check the copyright situation of your changes and sign off your patches (`git commit --signoff`, see [copyright](../CONTRIBUTING.rst#copyright-and-sign-off)).
|
||||
|
||||
### Code Changes
|
||||
|
||||
- [ ] Run `flake8` to check code style (follow PEP-8 style). `flake8` has issues with parsing Migen/gateware code, ignore as necessary.
|
||||
- [ ] Test your changes or have someone test them. Mention what was tested and how.
|
||||
- [ ] Add and check docstrings and comments
|
||||
- [ ] Check, test, and update the [unittests in /artiq/test/](../artiq/test/) or [gateware simulations in /artiq/gateware/test](../artiq/gateware/test)
|
||||
|
||||
### Documentation Changes
|
||||
|
||||
- [ ] Check, test, and update the documentation in [doc/](../doc/). Build documentation (`cd doc/manual/; make html`) to ensure no errors.
|
||||
|
||||
### Git Logistics
|
||||
|
||||
- [ ] Split your contribution into logically separate changes (`git rebase --interactive`). Merge/squash/fixup commits that just fix or amend previous commits. Remove unintended changes & cleanup. See [tutorial](https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase).
|
||||
- [ ] Write short & meaningful commit messages. Review each commit for messages (`git show`). Format:
|
||||
```
|
||||
topic: description. < 50 characters total.
|
||||
|
||||
Longer description. < 70 characters per line
|
||||
```
|
||||
|
||||
### Licensing
|
||||
|
||||
See [copyright & licensing for more info](https://github.com/m-labs/artiq/blob/master/CONTRIBUTING.rst#copyright-and-sign-off).
|
||||
ARTIQ files that do not contain a license header are copyrighted by M-Labs Limited and are licensed under LGPLv3+.
|
|
@ -7,6 +7,7 @@ __pycache__/
|
|||
*.elf
|
||||
*.fbi
|
||||
*.pcap
|
||||
*.prof
|
||||
.ipynb_checkpoints
|
||||
/doc/manual/_build
|
||||
/build
|
||||
|
@ -17,6 +18,7 @@ __pycache__/
|
|||
/artiq/binaries
|
||||
/artiq/firmware/target/
|
||||
/misoc_*/
|
||||
/artiq_*/
|
||||
|
||||
/artiq/test/results
|
||||
/artiq/examples/*/results
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
python:
|
||||
version: 3
|
||||
pip_install: false
|
||||
conda:
|
||||
file: conda/artiq-doc.yaml
|
|
@ -8,8 +8,8 @@ Reporting Issues/Bugs
|
|||
Thanks for `reporting issues to ARTIQ
|
||||
<https://github.com/m-labs/artiq/issues/new>`_! You can also discuss issues and
|
||||
ask questions on IRC (the `#m-labs channel on freenode
|
||||
<https://webchat.freenode.net/?channels=m-labs>`_) or on the `mailing list
|
||||
<https://ssl.serverraum.org/lists/listinfo/artiq>`_.
|
||||
<https://webchat.freenode.net/?channels=m-labs>`_), the `Mattermost chat
|
||||
<https://chat.m-labs.hk>`_, or on the `forum <https://forum.m-labs.hk>`_.
|
||||
|
||||
The best bug reports are those which contain sufficient information. With
|
||||
accurate and comprehensive context, an issue can be resolved quickly and
|
||||
|
@ -17,19 +17,20 @@ efficiently. Please consider adding the following data to your issue
|
|||
report if possible:
|
||||
|
||||
* A clear and unique summary that fits into one line. Also check that
|
||||
this issue has not jet been reported. If it has, add additional information there.
|
||||
this issue has not yet been reported. If it has, add additional information there.
|
||||
* Precise steps to reproduce (list of actions that leads to the issue)
|
||||
* Expected behavior (what should happen)
|
||||
* Actual behavior (what happens instead)
|
||||
* Logging message, trace backs, screen shots where relevant
|
||||
* Components involved:
|
||||
* Components involved (omit irrelevant parts):
|
||||
|
||||
* Operating system
|
||||
* Conda version
|
||||
* ARTIQ version (package or git commit id, versions for bitstream, BIOS,
|
||||
runtime and host software)
|
||||
* Operating System
|
||||
* ARTIQ version (with recent versions of ARTIQ, run ``artiq_client --version``)
|
||||
* Version of the gateware and runtime loaded in the core device (in the output of ``artiq_coremgmt -D .... log``)
|
||||
* If using Conda, output of `conda list`
|
||||
* Hardware involved
|
||||
|
||||
|
||||
For in-depth information on bug reporting, see:
|
||||
|
||||
http://www.chiark.greenend.org.uk/~sgtatham/bugs.html
|
||||
|
@ -42,10 +43,31 @@ Contributing Code
|
|||
ARTIQ welcomes contributions. Write bite-sized patches that can stand alone,
|
||||
clean them up, write proper commit messages, add docstrings and unittests. Then
|
||||
``git rebase`` them onto the current master or merge the current master. Verify
|
||||
that the testsuite passes. Then prepare a pull request or send patches to the
|
||||
`mailing list <https://ssl.serverraum.org/lists/listinfo/artiq>`_ to be
|
||||
discussed. Expect your contribution to be held up to coding standards (e.g. use
|
||||
``flake8`` to check yourself).
|
||||
that the testsuite passes. Then submit a pull request. Expect your contribution
|
||||
to be held up to coding standards (e.g. use ``flake8`` to check yourself).
|
||||
|
||||
Checklist for Code Contributions
|
||||
--------------------------------
|
||||
|
||||
- Test your changes or have someone test them. Mention what was tested and how.
|
||||
- Use correct spelling and grammar. Use your code editor to help you with
|
||||
syntax, spelling, and style
|
||||
- Style: PEP-8 (``flake8``)
|
||||
- Add, check docstrings and comments
|
||||
- Split your contribution into logically separate changes (``git rebase
|
||||
--interactive``). Merge (squash, fixup) commits that just fix previous commits
|
||||
or amend them. Remove unintended changes. Clean up your commits.
|
||||
- Check the copyright situation of your changes and sign off your patches
|
||||
(``git commit --signoff``, see also below)
|
||||
- Write meaningful commit messages containing the area of the change
|
||||
and a concise description (50 characters or less) in the first line.
|
||||
Describe everything else in the long explanation.
|
||||
- Review each of your commits for the above items (``git show``)
|
||||
- Update ``RELEASE_NOTES.md`` if there are noteworthy changes, especially if
|
||||
there are changes to existing APIs
|
||||
- Check, test, and update the documentation in `doc/`
|
||||
- Check, test, and update the unittests
|
||||
- Close and/or update issues
|
||||
|
||||
Copyright and Sign-Off
|
||||
----------------------
|
||||
|
@ -95,4 +117,4 @@ then you just add a line saying
|
|||
using your legal name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
ARTIQ files that do not contain a license header are copyrighted by M-Labs Limited
|
||||
and are licensed under GNU GPL version 3.
|
||||
and are licensed under GNU LGPL version 3 or later.
|
||||
|
|
|
@ -1,69 +1,17 @@
|
|||
Release process
|
||||
===============
|
||||
|
||||
Maintain ``RELEASE_NOTES.rst`` with a list of new features and API changes in each major release.
|
||||
|
||||
Major releases
|
||||
--------------
|
||||
|
||||
1. Create branch release-X from master.
|
||||
2. Tag the next commit in master X+1.0.dev.
|
||||
3. Ensure that release versions of all packages required are available under the ``main`` label in conda. Ensure that new packages in ``main`` do not break older ARTIQ releases.
|
||||
4. In the release-X branch, remove any unfinished features.
|
||||
5. Test and fix any problems found. Apply fixes to both master and release-X.
|
||||
6. If you have willing testers for release candidates, tag X.0rc1 in the release-X branch (generally use signed annotated tags, i.e. ``git tag -sa X.0rc1``), have it build, and point testers there. Iterate over the previous points with new release candidates if necessary.
|
||||
7. Tag X.0 in the release-X branch, build it, and copy its packages to ``main`` channel.
|
||||
8. Mint a new DOI from Zenodo and update the README/introduction.
|
||||
9. Update the m-labs.hk/artiq/manual redirect to point to m-labs.hk/artiq/manual-release-X (edit /artiq/.htaccess).
|
||||
10. "Draft a new release" and close the milestone on GitHub.
|
||||
11. Deprecate the old release documentation with a banner in
|
||||
doc/manual/_templates/layout.html in the old ``release-(X-1)`` branch.
|
||||
|
||||
Minor (bugfix) releases
|
||||
-----------------------
|
||||
|
||||
1. Backport bugfixes from the master branch or fix bugs specific to old releases into the currently maintained release-X branch(es).
|
||||
2. When significant bugs have been fixed, tag X.Y+1.
|
||||
3. To help dealing with regressions, no new features or refactorings should be implemented in release-X branches. Those happen in the master branch, and then a new release-X+1 branch is created.
|
||||
4. "Draft a new release" and close the milestone on GitHub.
|
||||
|
||||
Sharing development boards
|
||||
==========================
|
||||
|
||||
To avoid conflicts for development boards on the server, while using a board you must hold the corresponding lock file present in ``/run/board``. Holding the lock file grants you exclusive access to the board.
|
||||
To avoid conflicts for development boards on the server, while using a board you must hold the corresponding lock file present in the ``/tmp`` folder of the machine to which the board is connected. Holding the lock file grants you exclusive access to the board.
|
||||
|
||||
To lock the KC705 for 30 minutes or until Ctrl-C is pressed:
|
||||
For example, to lock the KC705 until ENTER is pressed:
|
||||
::
|
||||
flock --verbose /run/boards/kc705 sleep 1800
|
||||
Check that the command acquires the lock, i.e. prints something such as:
|
||||
::
|
||||
flock: getting lock took 0.000003 seconds
|
||||
flock: executing sleep
|
||||
|
||||
To lock the KC705 for the duration of the execution of a shell:
|
||||
::
|
||||
flock /run/boards/kc705 bash
|
||||
|
||||
You may also use this script:
|
||||
::
|
||||
#!/bin/bash
|
||||
exec flock /run/boards/$1 bash --rcfile <(cat ~/.bashrc; echo PS1=\"[$1\ lock]\ \$PS1\")
|
||||
ssh rpi-1.m-labs.hk "flock /tmp/board_lock-kc705-1 -c 'echo locked; read; echo unlocked'"
|
||||
|
||||
If the board is already locked by another user, the ``flock`` commands above will wait for the lock to be released.
|
||||
|
||||
To determine which user is locking a board, use:
|
||||
To determine which user is locking a board, use a command such as:
|
||||
::
|
||||
fuser -v /run/boards/kc705
|
||||
|
||||
|
||||
Selecting a development board with artiq_flash
|
||||
==============================================
|
||||
|
||||
Use the ``bus:port`` notation::
|
||||
|
||||
artiq_flash --preinit-command "ftdi_location 5:2" # Sayma 1
|
||||
artiq_flash --preinit-command "ftdi_location 3:10" # Sayma 2
|
||||
artiq_flash --preinit-command "ftdi_location 5:1" # Sayma 3
|
||||
ssh rpi-1.m-labs.hk "fuser -v /tmp/board_lock-kc705-1"
|
||||
|
||||
|
||||
Deleting git branches
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
6
|
27
README.rst
27
README.rst
|
@ -4,25 +4,23 @@
|
|||
.. image:: https://raw.githubusercontent.com/m-labs/artiq/master/doc/logo/artiq.png
|
||||
:target: https://m-labs.hk/artiq
|
||||
|
||||
ARTIQ (Advanced Real-Time Infrastructure for Quantum physics) is the next-generation control system for quantum information experiments.
|
||||
It is maintained and developed by `M-Labs <https://m-labs.hk>`_ and the initial development was for and in partnership with the `Ion Storage Group at NIST <https://www.nist.gov/pml/time-and-frequency-division/ion-storage>`_. ARTIQ is free software and offered to the entire research community as a solution equally applicable to other challenging control tasks, including outside the field of ion trapping. Several other laboratories (e.g. at the University of Oxford, the Army Research Lab, and the University of Maryland) have later adopted ARTIQ as their control system and have contributed to it.
|
||||
ARTIQ (Advanced Real-Time Infrastructure for Quantum physics) is a leading-edge control and data acquisition system for quantum information experiments.
|
||||
It is maintained and developed by `M-Labs <https://m-labs.hk>`_ and the initial development was for and in partnership with the `Ion Storage Group at NIST <https://www.nist.gov/pml/time-and-frequency-division/ion-storage>`_. ARTIQ is free software and offered to the entire research community as a solution equally applicable to other challenging control tasks, including outside the field of ion trapping. Many laboratories around the world have adopted ARTIQ as their control system, with over a hundred Sinara hardware crates deployed, and some have `contributed <https://m-labs.hk/experiment-control/funding/>`_ to it.
|
||||
|
||||
The system features a high-level programming language that helps describing complex experiments, which is compiled and executed on dedicated hardware with nanosecond timing resolution and sub-microsecond latency. It includes graphical user interfaces to parametrize and schedule experiments and to visualize and explore the results.
|
||||
|
||||
ARTIQ uses FPGA hardware to perform its time-critical tasks.
|
||||
It is designed to be portable to hardware platforms from different vendors and FPGA manufacturers.
|
||||
Currently, several different configurations of a `high-end FPGA evaluation kit <http://www.xilinx.com/products/boards-and-kits/ek-k7-kc705-g.html>`_ are used and supported. This FPGA platform can be combined with any number of additional peripherals, either already accessible from ARTIQ or made accessible with little effort.
|
||||
ARTIQ uses FPGA hardware to perform its time-critical tasks. The `Sinara hardware <https://github.com/sinara-hw>`_, and in particular the Kasli FPGA carrier, is designed to work with ARTIQ.
|
||||
ARTIQ is designed to be portable to hardware platforms from different vendors and FPGA manufacturers.
|
||||
Several different configurations of a `FPGA evaluation kit <https://www.xilinx.com/products/boards-and-kits/ek-k7-kc705-g.html>`_ and of a `Zynq evaluation kit <https://www.xilinx.com/products/boards-and-kits/ek-z7-zc706-g.html>`_ are also used and supported. FPGA platforms can be combined with any number of additional peripherals, either already accessible from ARTIQ or made accessible with little effort.
|
||||
|
||||
Custom hardware components with widely extended capabilities and advanced support for scalable and fully distributed real-time control of experiments `are being designed <https://github.com/m-labs/sinara>`_.
|
||||
|
||||
ARTIQ and its dependencies are available in the form of `conda packages <https://conda.anaconda.org/m-labs/label/main>`_ for both Linux and Windows.
|
||||
ARTIQ and its dependencies are available in the form of Nix packages (for Linux) and Conda packages (for Windows and Linux). See `the manual <https://m-labs.hk/experiment-control/resources/>`_ for installation instructions.
|
||||
Packages containing pre-compiled binary images to be loaded onto the hardware platforms are supplied for each configuration.
|
||||
Like any open source software ARTIQ can equally be built and installed directly from `source <https://github.com/m-labs/artiq>`_.
|
||||
|
||||
ARTIQ is supported by M-Labs and developed openly.
|
||||
Components, features, fixes, improvements, and extensions are funded by and developed for the partnering research groups.
|
||||
Components, features, fixes, improvements, and extensions are often `funded <https://m-labs.hk/experiment-control/funding/>`_ by and developed for the partnering research groups.
|
||||
|
||||
Technologies employed include `Python <https://www.python.org/>`_, `Migen <https://github.com/m-labs/migen>`_, `MiSoC <https://github.com/m-labs/misoc>`_/`mor1kx <https://github.com/openrisc/mor1kx>`_, `LLVM <http://llvm.org/>`_/`llvmlite <https://github.com/numba/llvmlite>`_, and `Qt5 <http://www.qt.io/>`_.
|
||||
Core technologies employed include `Python <https://www.python.org/>`_, `Migen <https://github.com/m-labs/migen>`_, `Migen-AXI <https://github.com/peteut/migen-axi>`_, `Rust <https://www.rust-lang.org/>`_, `MiSoC <https://github.com/m-labs/misoc>`_/`mor1kx <https://github.com/openrisc/mor1kx>`_, `LLVM <https://llvm.org/>`_/`llvmlite <https://github.com/numba/llvmlite>`_, and `Qt5 <https://www.qt.io/>`_.
|
||||
|
||||
Website: https://m-labs.hk/artiq
|
||||
|
||||
|
@ -31,7 +29,7 @@ Website: https://m-labs.hk/artiq
|
|||
License
|
||||
=======
|
||||
|
||||
Copyright (C) 2014-2017 M-Labs Limited.
|
||||
Copyright (C) 2014-2020 M-Labs Limited.
|
||||
|
||||
ARTIQ is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
|
@ -50,9 +48,10 @@ The ARTIQ manifesto
|
|||
===================
|
||||
|
||||
The free and open dissemination of methods and results is central to scientific progress.
|
||||
The ARTIQ authors, contributors, and supporters consider the free and open exchange of scientific tools to be equally important and have chosen the licensing terms of ARTIQ accordingly.
|
||||
ARTIQ, including its gateware, the firmware, and the ARTIQ tools and libraries are licensed as LGPLv3+.
|
||||
This ensures that a user of ARTIQ obtains broad rights to use, redistribute, and modify it.
|
||||
|
||||
The ARTIQ and Sinara authors, contributors, and supporters consider the free and open exchange of scientific tools to be equally important and have chosen the licensing terms of ARTIQ and Sinara accordingly. ARTIQ, including its gateware, the firmware, and the ARTIQ tools and libraries are licensed as LGPLv3+. The Sinara hardware designs are licensed under CERN OHL.
|
||||
This ensures that a user of ARTIQ or Sinara hardware designs obtains broad rights to use, redistribute, study, and modify them.
|
||||
|
||||
The following statements are intended to clarify the interpretation and application of the licensing terms:
|
||||
|
||||
* There is no requirement to distribute any unmodified, modified, or extended versions of ARTIQ. Only when distributing ARTIQ the source needs to be made available.
|
||||
|
|
|
@ -3,25 +3,250 @@
|
|||
Release notes
|
||||
=============
|
||||
|
||||
4.0 (unreleased)
|
||||
----------------
|
||||
ARTIQ-6
|
||||
-------
|
||||
|
||||
Highlights:
|
||||
|
||||
* New hardware support:
|
||||
- Zynq SoC core devices, enabling kernels to run on 1 GHz CPU core with a floating-point
|
||||
unit for faster computations. This currently requires an external
|
||||
repository (https://git.m-labs.hk/m-labs/artiq-zynq) and only supports the ZC706.
|
||||
- Mirny 4-channel wide-band PLL/VCO-based microwave frequency synthesiser
|
||||
- Fastino 32-channel, 3MS/s per channel, 16-bit DAC EEM
|
||||
- Kasli 2.0
|
||||
* ARTIQ Python (core device kernels):
|
||||
- Multidimensional arrays are now available on the core device, using NumPy syntax.
|
||||
Elementwise operations (e.g. ``+``, ``/``), matrix multiplication (``@``) and
|
||||
multidimensional indexing are supported; slices and views are not yet.
|
||||
- Trigonometric and other common math functions from NumPy are now available on the
|
||||
core device (e.g. ``numpy.sin``), both for scalar arguments and implicitly
|
||||
broadcast across multidimensional arrays.
|
||||
- Failed assertions now raise ``AssertionError``\ s instead of aborting kernel
|
||||
execution.
|
||||
* Performance improvements:
|
||||
- SERDES TTL inputs can now detect edges on pulses that are shorter
|
||||
than the RTIO period (https://github.com/m-labs/artiq/issues/1432)
|
||||
- Improved performance for kernel RPC involving list and array.
|
||||
* Coredevice SI to mu conversions now always return valid codes, or raise a ``ValueError``.
|
||||
* Zotino now exposes ``voltage_to_mu()``
|
||||
* ``ad9910``: The maximum amplitude scale factor is now ``0x3fff`` (was ``0x3ffe``
|
||||
before).
|
||||
* Dashboard:
|
||||
- Applets now restart if they are running and a ccb call changes their spec
|
||||
- A "Quick Open" dialog to open experiments by typing part of their name can
|
||||
be brought up Ctrl-P (Ctrl+Return to immediately submit the selected entry
|
||||
with the default arguments).
|
||||
* Experiment results are now always saved to HDF5, even if run() fails.
|
||||
* Core device: ``panic_reset 1`` now correctly resets the kernel CPU as well if
|
||||
communication CPU panic occurs.
|
||||
* NumberValue accepts a ``type`` parameter specifying the output as ``int`` or ``float``
|
||||
* A parameter ``--identifier-str`` has been added to many targets to aid
|
||||
with reproducible builds.
|
||||
* Python 3.7 support in Conda packages.
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* ``artiq_netboot`` has been moved to its own repository at
|
||||
https://git.m-labs.hk/m-labs/artiq-netboot
|
||||
* Core device watchdogs have been removed.
|
||||
* The ARTIQ compiler now implements arrays following NumPy semantics, rather than as a
|
||||
thin veneer around lists. Most prior use cases of NumPy arrays in kernels should work
|
||||
unchanged with the new implementation, but the behavior might differ slightly in some
|
||||
cases (for instance, non-rectangular arrays are not currently supported).
|
||||
* ``quamash`` has been replaced with ``qasync``.
|
||||
|
||||
|
||||
ARTIQ-5
|
||||
-------
|
||||
|
||||
Highlights:
|
||||
|
||||
* Performance improvements:
|
||||
- Faster RTIO event submission (1.5x improvement in pulse rate test)
|
||||
See: https://github.com/m-labs/artiq/issues/636
|
||||
- Faster compilation times (3 seconds saved on kernel compilation time on a typical
|
||||
medium-size experiment)
|
||||
See: https://github.com/m-labs/artiq/commit/611bcc4db4ed604a32d9678623617cd50e968cbf
|
||||
* Improved packaging and build system:
|
||||
- new continuous integration/delivery infrastructure based on Nix and Hydra,
|
||||
providing reproducibility, speed and independence.
|
||||
- rolling release process (https://github.com/m-labs/artiq/issues/1326).
|
||||
- firmware, gateware and device database templates are automatically built for all
|
||||
supported Kasli variants.
|
||||
- new JSON description format for generic Kasli systems.
|
||||
- Nix packages are now supported.
|
||||
- many Conda problems worked around.
|
||||
- controllers are now out-of-tree.
|
||||
- split packages that enable lightweight applications that communicate with ARTIQ,
|
||||
e.g. controllers running on non-x86 single-board computers.
|
||||
* Improved Urukul support:
|
||||
- AD9910 RAM mode.
|
||||
- Configurable refclk divider and PLL bypass.
|
||||
- More reliable phase synchronization at high sample rates.
|
||||
- Synchronization calibration data can be read from EEPROM.
|
||||
* A gateware-level input edge counter has been added, which offers higher
|
||||
throughput and increased flexibility over the usual TTL input PHYs where
|
||||
edge timestamps are not required. See ``artiq.coredevice.edge_counter`` for
|
||||
the core device driver and ``artiq.gateware.rtio.phy.edge_counter``/
|
||||
``artiq.gateware.eem.DIO.add_std`` for the gateware components.
|
||||
* With DRTIO, Siphaser uses a better calibration mechanism.
|
||||
See: https://github.com/m-labs/artiq/commit/cc58318500ecfa537abf24127f2c22e8fe66e0f8
|
||||
* Schedule updates can be sent to influxdb (artiq_influxdb_schedule).
|
||||
* Experiments can now programatically set their default pipeline, priority, and flush flag.
|
||||
* List datasets can now be efficiently appended to from experiments using
|
||||
``artiq.language.environment.HasEnvironment.append_to_dataset``.
|
||||
* The core device now supports IPv6.
|
||||
* To make development easier, the bootloader can receive firmware and secondary FPGA
|
||||
gateware from the network.
|
||||
* Python 3.7 compatibility (Nix and source builds only, no Conda).
|
||||
* Various other bugs from 4.0 fixed.
|
||||
* Preliminary Sayma v2 and Metlino hardware support.
|
||||
|
||||
Breaking changes:
|
||||
|
||||
* The ``artiq.coredevice.ad9910.AD9910`` and
|
||||
``artiq.coredevice.ad9914.AD9914`` phase reference timestamp parameters
|
||||
have been renamed to ``ref_time_mu`` for consistency, as they are in machine
|
||||
units.
|
||||
* The controller manager now ignores device database entries without the
|
||||
``command`` key set to facilitate sharing of devices between multiple
|
||||
masters.
|
||||
* The meaning of the ``-d/--dir`` and ``--srcbuild`` options of ``artiq_flash``
|
||||
has changed.
|
||||
* Controllers for third-party devices are now out-of-tree.
|
||||
* ``aqctl_corelog`` now filters log messages below the ``WARNING`` level by default.
|
||||
This behavior can be changed using the ``-v`` and ``-q`` options like the other
|
||||
programs.
|
||||
* On Kasli the firmware now starts with a unique default MAC address
|
||||
from EEPROM if `mac` is absent from the flash config.
|
||||
* The ``-e/--experiment`` switch of ``artiq_run`` and ``artiq_compile``
|
||||
has been renamed ``-c/--class-name``.
|
||||
* ``artiq_devtool`` has been removed.
|
||||
* Much of ``artiq.protocols`` has been moved to a separate package ``sipyco``.
|
||||
``artiq_rpctool`` has been renamed to ``sipyco_rpctool``.
|
||||
|
||||
|
||||
ARTIQ-4
|
||||
-------
|
||||
|
||||
4.0
|
||||
***
|
||||
|
||||
* The ``artiq.coredevice.ttl`` drivers no longer track the timestamps of
|
||||
submitted events in software, requiring the user to explicitly specify the
|
||||
timeout for ``count()``/``timestamp_mu()``. Support for ``sync()`` has been dropped.
|
||||
|
||||
Now that RTIO has gained DMA support, there is no longer a reliable way for
|
||||
the kernel CPU to track the individual events submitted on any one channel.
|
||||
Requiring the timeouts to be specified explicitly ensures consistent API
|
||||
behavior. To make this more convenient, the ``TTLInOut.gate_*()`` functions
|
||||
now return the cursor position at the end of the gate, e.g.::
|
||||
|
||||
ttl_input.count(ttl_input.gate_rising(100 * us))
|
||||
|
||||
In most situations – that is, unless the timeline cursor is rewound after the
|
||||
respective ``gate_*()`` call – simply passing ``now_mu()`` is also a valid
|
||||
upgrade path::
|
||||
|
||||
ttl_input.count(now_mu())
|
||||
|
||||
The latter might use up more timeline slack than necessary, though.
|
||||
|
||||
In place of ``TTL(In)Out.sync``, the new ``Core.wait_until_mu()`` method can
|
||||
be used, which blocks execution until the hardware RTIO cursor reaches the
|
||||
given timestamp::
|
||||
|
||||
ttl_output.pulse(10 * us)
|
||||
self.core.wait_until_mu(now_mu())
|
||||
* RTIO outputs use a new architecture called Scalable Event Dispatcher (SED),
|
||||
which allows building systems with large number of RTIO channels more
|
||||
efficiently.
|
||||
From the user perspective, collision errors become asynchronous, and non-
|
||||
monotonic timestamps on any combination of channels are generally allowed
|
||||
(instead of producing sequence errors).
|
||||
RTIO inputs are not affected.
|
||||
* The DDS channel number for the NIST CLOCK target has changed.
|
||||
* The dashboard configuration files are now stored one-per-master, keyed by the
|
||||
server address argument and the notify port.
|
||||
* The master now has a ``--name`` argument. If given, the dashboard is labelled
|
||||
with this name rather than the server address.
|
||||
* ``artiq_flash --adapter`` has been changed to ``artiq_flash --variant``.
|
||||
* ``artiq_flash`` targets Kasli by default. Use ``-t kc705`` to flash a KC705
|
||||
instead.
|
||||
* ``artiq_flash -m/--adapter`` has been changed to ``artiq_flash -V/--variant``.
|
||||
* The ``proxy`` action of ``artiq_flash`` is determined automatically and should
|
||||
not be specified manually anymore.
|
||||
* ``kc705_dds`` has been renamed ``kc705``.
|
||||
* The ``-H/--hw-adapter`` option of ``kc705`` has been renamed ``-V/--variant``.
|
||||
* SPI masters have been switched from misoc-spi to misoc-spi2. This affects
|
||||
all out-of-tree RTIO core device drivers using those buses. See the various
|
||||
commits on e.g. the ``ad53xx`` driver for an example how to port from the old
|
||||
to the new bus.
|
||||
* The ``ad5360`` coredevice driver has been renamed to ``ad53xx`` and the API
|
||||
has changed to better support Zotino.
|
||||
* ``artiq.coredevice.dds`` has been renamed to ``artiq.coredevice.ad9914`` and
|
||||
simplified. DDS batch mode is no longer supported. The ``core_dds`` device
|
||||
is no longer necessary.
|
||||
* The configuration entry ``startup_clock`` is renamed ``rtio_clock``. Switching
|
||||
clocks dynamically (i.e. without device restart) is no longer supported.
|
||||
* ``set_dataset(..., save=True)`` has been renamed
|
||||
``set_dataset(..., archive=True)``.
|
||||
* On the AD9914 DDS, when switching to ``PHASE_MODE_CONTINUOUS`` from another mode,
|
||||
use the returned value of the last ``set_mu`` call as the phase offset for
|
||||
``PHASE_MODE_CONTINUOUS`` to avoid a phase discontinuity. This is no longer done
|
||||
automatically. If one phase glitch when entering ``PHASE_MODE_CONTINUOUS`` is not
|
||||
an issue, this recommendation can be ignored.
|
||||
|
||||
|
||||
ARTIQ-3
|
||||
-------
|
||||
|
||||
3.7
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
3.6
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
3.5
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
3.4
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
3.3
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
3.2
|
||||
***
|
||||
|
||||
* To accommodate larger runtimes, the flash layout as changed. As a result, the
|
||||
contents of the flash storage will be lost when upgrading. Set the values back
|
||||
(IP, MAC address, startup kernel, etc.) after the upgrade.
|
||||
|
||||
|
||||
3.1
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
3.0
|
||||
---
|
||||
***
|
||||
|
||||
* The ``--embed`` option of applets is replaced with the environment variable
|
||||
``ARTIQ_APPLET_EMBED``. The GUI sets this enviroment variable itself and the
|
||||
|
@ -74,50 +299,53 @@ No further notes.
|
|||
* Packages are no longer available for 32-bit Windows.
|
||||
|
||||
|
||||
ARTIQ-2
|
||||
-------
|
||||
|
||||
2.5
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
2.4
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
2.3
|
||||
---
|
||||
***
|
||||
|
||||
* When using conda, add the conda-forge channel before installing ARTIQ.
|
||||
|
||||
|
||||
2.2
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
2.1
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
2.0
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
2.0rc2
|
||||
------
|
||||
******
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
2.0rc1
|
||||
------
|
||||
******
|
||||
|
||||
* The format of the influxdb pattern file is simplified. The procedure to
|
||||
edit patterns is also changed to modifying the pattern file and calling:
|
||||
|
@ -166,39 +394,42 @@ No further notes.
|
|||
receives a numpy type.
|
||||
|
||||
|
||||
ARTIQ-1
|
||||
-------
|
||||
|
||||
1.3
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
1.2
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
1.1
|
||||
---
|
||||
***
|
||||
|
||||
* TCA6424A.set converts the "outputs" value to little-endian before programming
|
||||
it into the registers.
|
||||
|
||||
|
||||
1.0
|
||||
---
|
||||
***
|
||||
|
||||
No further notes.
|
||||
|
||||
|
||||
1.0rc4
|
||||
------
|
||||
******
|
||||
|
||||
* setattr_argument and setattr_device add their key to kernel_invariants.
|
||||
|
||||
|
||||
1.0rc3
|
||||
------
|
||||
******
|
||||
|
||||
* The HDF5 format has changed.
|
||||
|
||||
|
@ -212,7 +443,7 @@ No further notes.
|
|||
|
||||
|
||||
1.0rc2
|
||||
------
|
||||
******
|
||||
|
||||
* The CPU speed in the pipistrello gateware has been reduced from 83 1/3 MHz to
|
||||
75 MHz. This will reduce the achievable sustained pulse rate and latency
|
||||
|
@ -222,7 +453,7 @@ No further notes.
|
|||
|
||||
|
||||
1.0rc1
|
||||
------
|
||||
******
|
||||
|
||||
* Experiments (your code) should use ``from artiq.experiment import *``
|
||||
(and not ``from artiq import *`` as previously)
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
from ._version import get_versions
|
||||
__version__ = get_versions()['version']
|
||||
del get_versions
|
||||
from ._version import get_version
|
||||
__version__ = get_version()
|
||||
del get_version
|
||||
|
||||
import os
|
||||
__artiq_dir__ = os.path.dirname(os.path.abspath(__file__))
|
||||
del os
|
||||
|
||||
from ._version import get_versions
|
||||
__version__ = get_versions()['version']
|
||||
del get_versions
|
||||
|
|
|
@ -1,520 +1,13 @@
|
|||
|
||||
# This file helps to compute a version number in source trees obtained from
|
||||
# git-archive tarball (such as those provided by githubs download-from-tag
|
||||
# feature). Distribution tarballs (built by setup.py sdist) and build
|
||||
# directories (produced by setup.py build) will contain a much shorter file
|
||||
# that just contains the computed version number.
|
||||
|
||||
# This file is released into the public domain. Generated by
|
||||
# versioneer-0.18 (https://github.com/warner/python-versioneer)
|
||||
|
||||
"""Git implementation of _version.py."""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def get_keywords():
|
||||
"""Get the keywords needed to look up the version information."""
|
||||
# these strings will be replaced by git during git-archive.
|
||||
# setup.py/versioneer.py will grep for the variable names, so they must
|
||||
# each be defined on a line of their own. _version.py will just call
|
||||
# get_keywords().
|
||||
git_refnames = "$Format:%d$"
|
||||
git_full = "$Format:%H$"
|
||||
git_date = "$Format:%ci$"
|
||||
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
|
||||
return keywords
|
||||
|
||||
|
||||
class VersioneerConfig:
|
||||
"""Container for Versioneer configuration parameters."""
|
||||
|
||||
|
||||
def get_config():
|
||||
"""Create, populate and return the VersioneerConfig() object."""
|
||||
# these strings are filled in when 'setup.py versioneer' creates
|
||||
# _version.py
|
||||
cfg = VersioneerConfig()
|
||||
cfg.VCS = "git"
|
||||
cfg.style = "pep440"
|
||||
cfg.tag_prefix = ""
|
||||
cfg.parentdir_prefix = "artiq-"
|
||||
cfg.versionfile_source = "artiq/_version.py"
|
||||
cfg.verbose = False
|
||||
return cfg
|
||||
|
||||
|
||||
class NotThisMethod(Exception):
|
||||
"""Exception raised if a method is not valid for the current scenario."""
|
||||
|
||||
|
||||
LONG_VERSION_PY = {}
|
||||
HANDLERS = {}
|
||||
|
||||
|
||||
def register_vcs_handler(vcs, method): # decorator
|
||||
"""Decorator to mark a method as the handler for a particular VCS."""
|
||||
def decorate(f):
|
||||
"""Store f in HANDLERS[vcs][method]."""
|
||||
if vcs not in HANDLERS:
|
||||
HANDLERS[vcs] = {}
|
||||
HANDLERS[vcs][method] = f
|
||||
return f
|
||||
return decorate
|
||||
|
||||
|
||||
def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
|
||||
env=None):
|
||||
"""Call the given command(s)."""
|
||||
assert isinstance(commands, list)
|
||||
p = None
|
||||
for c in commands:
|
||||
try:
|
||||
dispcmd = str([c] + args)
|
||||
# remember shell=False, so use git.cmd on windows, not just git
|
||||
p = subprocess.Popen([c] + args, cwd=cwd, env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=(subprocess.PIPE if hide_stderr
|
||||
else None))
|
||||
break
|
||||
except EnvironmentError:
|
||||
e = sys.exc_info()[1]
|
||||
if e.errno == errno.ENOENT:
|
||||
continue
|
||||
if verbose:
|
||||
print("unable to run %s" % dispcmd)
|
||||
print(e)
|
||||
return None, None
|
||||
else:
|
||||
if verbose:
|
||||
print("unable to find command, tried %s" % (commands,))
|
||||
return None, None
|
||||
stdout = p.communicate()[0].strip()
|
||||
if sys.version_info[0] >= 3:
|
||||
stdout = stdout.decode()
|
||||
if p.returncode != 0:
|
||||
if verbose:
|
||||
print("unable to run %s (error)" % dispcmd)
|
||||
print("stdout was %s" % stdout)
|
||||
return None, p.returncode
|
||||
return stdout, p.returncode
|
||||
|
||||
|
||||
def versions_from_parentdir(parentdir_prefix, root, verbose):
|
||||
"""Try to determine the version from the parent directory name.
|
||||
|
||||
Source tarballs conventionally unpack into a directory that includes both
|
||||
the project name and a version string. We will also support searching up
|
||||
two directory levels for an appropriately named parent directory
|
||||
"""
|
||||
rootdirs = []
|
||||
|
||||
for i in range(3):
|
||||
dirname = os.path.basename(root)
|
||||
if dirname.startswith(parentdir_prefix):
|
||||
return {"version": dirname[len(parentdir_prefix):],
|
||||
"full-revisionid": None,
|
||||
"dirty": False, "error": None, "date": None}
|
||||
else:
|
||||
rootdirs.append(root)
|
||||
root = os.path.dirname(root) # up a level
|
||||
|
||||
if verbose:
|
||||
print("Tried directories %s but none started with prefix %s" %
|
||||
(str(rootdirs), parentdir_prefix))
|
||||
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
|
||||
|
||||
|
||||
@register_vcs_handler("git", "get_keywords")
|
||||
def git_get_keywords(versionfile_abs):
|
||||
"""Extract version information from the given file."""
|
||||
# the code embedded in _version.py can just fetch the value of these
|
||||
# keywords. When used from setup.py, we don't want to import _version.py,
|
||||
# so we do it with a regexp instead. This function is not used from
|
||||
# _version.py.
|
||||
keywords = {}
|
||||
try:
|
||||
f = open(versionfile_abs, "r")
|
||||
for line in f.readlines():
|
||||
if line.strip().startswith("git_refnames ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["refnames"] = mo.group(1)
|
||||
if line.strip().startswith("git_full ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["full"] = mo.group(1)
|
||||
if line.strip().startswith("git_date ="):
|
||||
mo = re.search(r'=\s*"(.*)"', line)
|
||||
if mo:
|
||||
keywords["date"] = mo.group(1)
|
||||
f.close()
|
||||
except EnvironmentError:
|
||||
pass
|
||||
return keywords
|
||||
|
||||
|
||||
@register_vcs_handler("git", "keywords")
|
||||
def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
||||
"""Get version information from git keywords."""
|
||||
if not keywords:
|
||||
raise NotThisMethod("no keywords at all, weird")
|
||||
date = keywords.get("date")
|
||||
if date is not None:
|
||||
# git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
|
||||
# datestamp. However we prefer "%ci" (which expands to an "ISO-8601
|
||||
# -like" string, which we must then edit to make compliant), because
|
||||
# it's been around since git-1.5.3, and it's too difficult to
|
||||
# discover which version we're using, or to work around using an
|
||||
# older one.
|
||||
date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
|
||||
refnames = keywords["refnames"].strip()
|
||||
if refnames.startswith("$Format"):
|
||||
if verbose:
|
||||
print("keywords are unexpanded, not using")
|
||||
raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
|
||||
refs = set([r.strip() for r in refnames.strip("()").split(",")])
|
||||
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
|
||||
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
|
||||
TAG = "tag: "
|
||||
tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
|
||||
if not tags:
|
||||
# Either we're using git < 1.8.3, or there really are no tags. We use
|
||||
# a heuristic: assume all version tags have a digit. The old git %d
|
||||
# expansion behaves like git log --decorate=short and strips out the
|
||||
# refs/heads/ and refs/tags/ prefixes that would let us distinguish
|
||||
# between branches and tags. By ignoring refnames without digits, we
|
||||
# filter out many common branch names like "release" and
|
||||
# "stabilization", as well as "HEAD" and "master".
|
||||
tags = set([r for r in refs if re.search(r'\d', r)])
|
||||
if verbose:
|
||||
print("discarding '%s', no digits" % ",".join(refs - tags))
|
||||
if verbose:
|
||||
print("likely tags: %s" % ",".join(sorted(tags)))
|
||||
for ref in sorted(tags):
|
||||
# sorting will prefer e.g. "2.0" over "2.0rc1"
|
||||
if ref.startswith(tag_prefix):
|
||||
r = ref[len(tag_prefix):]
|
||||
if verbose:
|
||||
print("picking %s" % r)
|
||||
return {"version": r,
|
||||
"full-revisionid": keywords["full"].strip(),
|
||||
"dirty": False, "error": None,
|
||||
"date": date}
|
||||
# no suitable tags, so version is "0+unknown", but full hex is still there
|
||||
if verbose:
|
||||
print("no suitable tags, using unknown + full revision id")
|
||||
return {"version": "0+unknown",
|
||||
"full-revisionid": keywords["full"].strip(),
|
||||
"dirty": False, "error": "no suitable tags", "date": None}
|
||||
|
||||
|
||||
@register_vcs_handler("git", "pieces_from_vcs")
|
||||
def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
||||
"""Get version from 'git describe' in the root of the source tree.
|
||||
|
||||
This only gets called if the git-archive 'subst' keywords were *not*
|
||||
expanded, and _version.py hasn't already been rewritten with a short
|
||||
version string, meaning we're inside a checked out source tree.
|
||||
"""
|
||||
GITS = ["git"]
|
||||
if sys.platform == "win32":
|
||||
GITS = ["git.cmd", "git.exe"]
|
||||
|
||||
out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
|
||||
hide_stderr=True)
|
||||
if rc != 0:
|
||||
if verbose:
|
||||
print("Directory %s not under git control" % root)
|
||||
raise NotThisMethod("'git rev-parse --git-dir' returned error")
|
||||
|
||||
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
|
||||
# if there isn't one, this yields HEX[-dirty] (no NUM)
|
||||
describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
|
||||
"--always", "--long", "--abbrev=8",
|
||||
"--match", "%s*" % tag_prefix],
|
||||
cwd=root)
|
||||
# --long was added in git-1.5.5
|
||||
if describe_out is None:
|
||||
raise NotThisMethod("'git describe' failed")
|
||||
describe_out = describe_out.strip()
|
||||
full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
|
||||
if full_out is None:
|
||||
raise NotThisMethod("'git rev-parse' failed")
|
||||
full_out = full_out.strip()
|
||||
|
||||
pieces = {}
|
||||
pieces["long"] = full_out
|
||||
pieces["short"] = full_out[:8] # maybe improved later
|
||||
pieces["error"] = None
|
||||
|
||||
# parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
|
||||
# TAG might have hyphens.
|
||||
git_describe = describe_out
|
||||
|
||||
# look for -dirty suffix
|
||||
dirty = git_describe.endswith("-dirty")
|
||||
pieces["dirty"] = dirty
|
||||
if dirty:
|
||||
git_describe = git_describe[:git_describe.rindex("-dirty")]
|
||||
|
||||
# now we have TAG-NUM-gHEX or HEX
|
||||
|
||||
if "-" in git_describe:
|
||||
# TAG-NUM-gHEX
|
||||
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
|
||||
if not mo:
|
||||
# unparseable. Maybe git-describe is misbehaving?
|
||||
pieces["error"] = ("unable to parse git-describe output: '%s'"
|
||||
% describe_out)
|
||||
return pieces
|
||||
|
||||
# tag
|
||||
full_tag = mo.group(1)
|
||||
if not full_tag.startswith(tag_prefix):
|
||||
if verbose:
|
||||
fmt = "tag '%s' doesn't start with prefix '%s'"
|
||||
print(fmt % (full_tag, tag_prefix))
|
||||
pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
|
||||
% (full_tag, tag_prefix))
|
||||
return pieces
|
||||
pieces["closest-tag"] = full_tag[len(tag_prefix):]
|
||||
|
||||
# distance: number of commits since tag
|
||||
pieces["distance"] = int(mo.group(2))
|
||||
|
||||
# commit: short hex revision ID
|
||||
pieces["short"] = mo.group(3)
|
||||
|
||||
else:
|
||||
# HEX: no tags
|
||||
pieces["closest-tag"] = None
|
||||
count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
|
||||
cwd=root)
|
||||
pieces["distance"] = int(count_out) # total number of commits
|
||||
|
||||
# commit date: see ISO-8601 comment in git_versions_from_keywords()
|
||||
date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
|
||||
cwd=root)[0].strip()
|
||||
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
|
||||
|
||||
return pieces
|
||||
|
||||
|
||||
def plus_or_dot(pieces):
|
||||
"""Return a + if we don't already have one, else return a ."""
|
||||
if "+" in pieces.get("closest-tag", ""):
|
||||
return "."
|
||||
return "+"
|
||||
|
||||
|
||||
def render_pep440(pieces):
|
||||
"""Build up version string, with post-release "local version identifier".
|
||||
|
||||
Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
|
||||
get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
|
||||
|
||||
Exceptions:
|
||||
1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"] or pieces["dirty"]:
|
||||
rendered += plus_or_dot(pieces)
|
||||
rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dirty"
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0+untagged.%d.g%s" % (pieces["distance"],
|
||||
pieces["short"])
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dirty"
|
||||
return rendered
|
||||
|
||||
|
||||
def render_pep440_pre(pieces):
|
||||
"""TAG[.post.devDISTANCE] -- No -dirty.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. 0.post.devDISTANCE
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"]:
|
||||
rendered += ".post.dev%d" % pieces["distance"]
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post.dev%d" % pieces["distance"]
|
||||
return rendered
|
||||
|
||||
|
||||
def render_pep440_post(pieces):
|
||||
"""TAG[.postDISTANCE[.dev0]+gHEX] .
|
||||
|
||||
The ".dev0" means dirty. Note that .dev0 sorts backwards
|
||||
(a dirty tree will appear "older" than the corresponding clean one),
|
||||
but you shouldn't be releasing software with -dirty anyways.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. 0.postDISTANCE[.dev0]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"] or pieces["dirty"]:
|
||||
rendered += ".post%d" % pieces["distance"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dev0"
|
||||
rendered += plus_or_dot(pieces)
|
||||
rendered += "g%s" % pieces["short"]
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post%d" % pieces["distance"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dev0"
|
||||
rendered += "+g%s" % pieces["short"]
|
||||
return rendered
|
||||
|
||||
|
||||
def render_pep440_old(pieces):
|
||||
"""TAG[.postDISTANCE[.dev0]] .
|
||||
|
||||
The ".dev0" means dirty.
|
||||
|
||||
Eexceptions:
|
||||
1: no tags. 0.postDISTANCE[.dev0]
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"] or pieces["dirty"]:
|
||||
rendered += ".post%d" % pieces["distance"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dev0"
|
||||
else:
|
||||
# exception #1
|
||||
rendered = "0.post%d" % pieces["distance"]
|
||||
if pieces["dirty"]:
|
||||
rendered += ".dev0"
|
||||
return rendered
|
||||
|
||||
|
||||
def render_git_describe(pieces):
|
||||
"""TAG[-DISTANCE-gHEX][-dirty].
|
||||
|
||||
Like 'git describe --tags --dirty --always'.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. HEX[-dirty] (note: no 'g' prefix)
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
if pieces["distance"]:
|
||||
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
|
||||
else:
|
||||
# exception #1
|
||||
rendered = pieces["short"]
|
||||
if pieces["dirty"]:
|
||||
rendered += "-dirty"
|
||||
return rendered
|
||||
|
||||
|
||||
def render_git_describe_long(pieces):
|
||||
"""TAG-DISTANCE-gHEX[-dirty].
|
||||
|
||||
Like 'git describe --tags --dirty --always -long'.
|
||||
The distance/hash is unconditional.
|
||||
|
||||
Exceptions:
|
||||
1: no tags. HEX[-dirty] (note: no 'g' prefix)
|
||||
"""
|
||||
if pieces["closest-tag"]:
|
||||
rendered = pieces["closest-tag"]
|
||||
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
|
||||
else:
|
||||
# exception #1
|
||||
rendered = pieces["short"]
|
||||
if pieces["dirty"]:
|
||||
rendered += "-dirty"
|
||||
return rendered
|
||||
|
||||
|
||||
def render(pieces, style):
|
||||
"""Render the given version pieces into the requested style."""
|
||||
if pieces["error"]:
|
||||
return {"version": "unknown",
|
||||
"full-revisionid": pieces.get("long"),
|
||||
"dirty": None,
|
||||
"error": pieces["error"],
|
||||
"date": None}
|
||||
|
||||
if not style or style == "default":
|
||||
style = "pep440" # the default
|
||||
|
||||
if style == "pep440":
|
||||
rendered = render_pep440(pieces)
|
||||
elif style == "pep440-pre":
|
||||
rendered = render_pep440_pre(pieces)
|
||||
elif style == "pep440-post":
|
||||
rendered = render_pep440_post(pieces)
|
||||
elif style == "pep440-old":
|
||||
rendered = render_pep440_old(pieces)
|
||||
elif style == "git-describe":
|
||||
rendered = render_git_describe(pieces)
|
||||
elif style == "git-describe-long":
|
||||
rendered = render_git_describe_long(pieces)
|
||||
else:
|
||||
raise ValueError("unknown style '%s'" % style)
|
||||
|
||||
return {"version": rendered, "full-revisionid": pieces["long"],
|
||||
"dirty": pieces["dirty"], "error": None,
|
||||
"date": pieces.get("date")}
|
||||
|
||||
|
||||
def get_versions():
|
||||
"""Get version information or return default if unable to do so."""
|
||||
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
|
||||
# __file__, we can work backwards from there to the root. Some
|
||||
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
|
||||
# case we can only use expanded keywords.
|
||||
|
||||
cfg = get_config()
|
||||
verbose = cfg.verbose
|
||||
|
||||
try:
|
||||
return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
|
||||
verbose)
|
||||
except NotThisMethod:
|
||||
pass
|
||||
|
||||
try:
|
||||
root = os.path.realpath(__file__)
|
||||
# versionfile_source is the relative path from the top of the source
|
||||
# tree (where the .git directory might live) to this file. Invert
|
||||
# this to find the root from __file__.
|
||||
for i in cfg.versionfile_source.split('/'):
|
||||
root = os.path.dirname(root)
|
||||
except NameError:
|
||||
return {"version": "0+unknown", "full-revisionid": None,
|
||||
"dirty": None,
|
||||
"error": "unable to find root of source tree",
|
||||
"date": None}
|
||||
|
||||
try:
|
||||
pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
|
||||
return render(pieces, cfg.style)
|
||||
except NotThisMethod:
|
||||
pass
|
||||
|
||||
try:
|
||||
if cfg.parentdir_prefix:
|
||||
return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
|
||||
except NotThisMethod:
|
||||
pass
|
||||
|
||||
return {"version": "0+unknown", "full-revisionid": None,
|
||||
"dirty": None,
|
||||
"error": "unable to compute version", "date": None}
|
||||
def get_version():
|
||||
override = os.getenv("VERSIONEER_OVERRIDE")
|
||||
if override:
|
||||
return override
|
||||
srcroot = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
|
||||
with open(os.path.join(srcroot, "MAJOR_VERSION"), "r") as f:
|
||||
version = f.read().strip()
|
||||
version += ".unknown"
|
||||
if os.path.exists(os.path.join(srcroot, "BETA")):
|
||||
version += ".beta"
|
||||
return version
|
||||
|
|
|
@ -4,11 +4,11 @@ import asyncio
|
|||
import os
|
||||
import string
|
||||
|
||||
from quamash import QEventLoop, QtWidgets, QtCore
|
||||
from qasync import QEventLoop, QtWidgets, QtCore
|
||||
|
||||
from artiq.protocols.sync_struct import Subscriber, process_mod
|
||||
from artiq.protocols import pyon
|
||||
from artiq.protocols.pipe_ipc import AsyncioChildComm
|
||||
from sipyco.sync_struct import Subscriber, process_mod
|
||||
from sipyco import pyon
|
||||
from sipyco.pipe_ipc import AsyncioChildComm
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -114,7 +114,7 @@ class SimpleApplet:
|
|||
self.datasets = {getattr(self.args, arg.replace("-", "_"))
|
||||
for arg in self.dataset_args}
|
||||
|
||||
def quamash_init(self):
|
||||
def qasync_init(self):
|
||||
app = QtWidgets.QApplication([])
|
||||
self.loop = QEventLoop(app)
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
@ -212,7 +212,7 @@ class SimpleApplet:
|
|||
|
||||
def run(self):
|
||||
self.args_init()
|
||||
self.quamash_init()
|
||||
self.qasync_init()
|
||||
try:
|
||||
self.ipc_init()
|
||||
try:
|
||||
|
|
|
@ -3,10 +3,11 @@ import asyncio
|
|||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from sipyco.pc_rpc import AsyncioClient as RPCClient
|
||||
|
||||
from artiq.tools import short_format
|
||||
from artiq.gui.tools import LayoutWidget, QRecursiveFilterProxyModel
|
||||
from artiq.gui.models import DictSyncTreeSepModel
|
||||
from artiq.protocols.pc_rpc import AsyncioClient as RPCClient
|
||||
|
||||
# reduced read-only version of artiq.dashboard.datasets
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@ from collections import OrderedDict
|
|||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
import h5py
|
||||
|
||||
from sipyco import pyon
|
||||
|
||||
from artiq import __artiq_dir__ as artiq_dir
|
||||
from artiq.gui.tools import LayoutWidget, log_level_to_name, get_open_file_name
|
||||
from artiq.gui.entries import procdesc_to_entry
|
||||
from artiq.protocols import pyon
|
||||
from artiq.master.worker import Worker, log_worker_exception
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -258,8 +259,9 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
def dropEvent(self, ev):
|
||||
for uri in ev.mimeData().urls():
|
||||
if uri.scheme() == "file":
|
||||
logger.debug("Loading HDF5 arguments from %s", uri.path())
|
||||
asyncio.ensure_future(self.load_hdf5_task(uri.path()))
|
||||
filename = QtCore.QDir.toNativeSeparators(uri.toLocalFile())
|
||||
logger.debug("Loading HDF5 arguments from %s", filename)
|
||||
asyncio.ensure_future(self.load_hdf5_task(filename))
|
||||
break
|
||||
|
||||
async def compute_arginfo(self):
|
||||
|
|
|
@ -5,7 +5,8 @@ from datetime import datetime
|
|||
import h5py
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
|
||||
from artiq.protocols import pyon
|
||||
from sipyco import pyon
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -41,7 +42,7 @@ class ThumbnailIconProvider(QtWidgets.QFileIconProvider):
|
|||
except KeyError:
|
||||
return
|
||||
try:
|
||||
img = QtGui.QImage.fromData(t.value)
|
||||
img = QtGui.QImage.fromData(t[()])
|
||||
except:
|
||||
logger.warning("unable to read thumbnail from %s",
|
||||
info.filePath(), exc_info=True)
|
||||
|
@ -101,13 +102,13 @@ class Hdf5FileSystemModel(QtWidgets.QFileSystemModel):
|
|||
h5 = open_h5(info)
|
||||
if h5 is not None:
|
||||
try:
|
||||
expid = pyon.decode(h5["expid"].value)
|
||||
start_time = datetime.fromtimestamp(h5["start_time"].value)
|
||||
expid = pyon.decode(h5["expid"][()])
|
||||
start_time = datetime.fromtimestamp(h5["start_time"][()])
|
||||
v = ("artiq_version: {}\nrepo_rev: {}\nfile: {}\n"
|
||||
"class_name: {}\nrid: {}\nstart_time: {}").format(
|
||||
h5["artiq_version"].value, expid["repo_rev"],
|
||||
h5["artiq_version"][()], expid["repo_rev"],
|
||||
expid["file"], expid["class_name"],
|
||||
h5["rid"].value, start_time)
|
||||
h5["rid"][()], start_time)
|
||||
return v
|
||||
except:
|
||||
logger.warning("unable to read metadata from %s",
|
||||
|
@ -173,14 +174,14 @@ class FilesDock(QtWidgets.QDockWidget):
|
|||
logger.debug("loading datasets from %s", info.filePath())
|
||||
with f:
|
||||
try:
|
||||
expid = pyon.decode(f["expid"].value)
|
||||
start_time = datetime.fromtimestamp(f["start_time"].value)
|
||||
expid = pyon.decode(f["expid"][()])
|
||||
start_time = datetime.fromtimestamp(f["start_time"][()])
|
||||
v = {
|
||||
"artiq_version": f["artiq_version"].value,
|
||||
"artiq_version": f["artiq_version"][()],
|
||||
"repo_rev": expid["repo_rev"],
|
||||
"file": expid["file"],
|
||||
"class_name": expid["class_name"],
|
||||
"rid": f["rid"].value,
|
||||
"rid": f["rid"][()],
|
||||
"start_time": start_time,
|
||||
}
|
||||
self.metadata_changed.emit(v)
|
||||
|
@ -189,13 +190,13 @@ class FilesDock(QtWidgets.QDockWidget):
|
|||
info.filePath(), exc_info=True)
|
||||
rd = dict()
|
||||
if "archive" in f:
|
||||
rd = {k: (True, v.value) for k, v in f["archive"].items()}
|
||||
rd = {k: (True, v[()]) for k, v in f["archive"].items()}
|
||||
if "datasets" in f:
|
||||
for k, v in f["datasets"].items():
|
||||
if k in rd:
|
||||
logger.warning("dataset '%s' is both in archive and "
|
||||
"outputs", k)
|
||||
rd[k] = (True, v.value)
|
||||
rd[k] = (True, v[()])
|
||||
if rd:
|
||||
self.datasets.init(rd)
|
||||
self.dataset_changed.emit(info.filePath())
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
from migen import *
|
||||
from misoc.interconnect.csr import *
|
||||
from misoc.integration.builder import *
|
||||
|
||||
from artiq.gateware.amp import AMPSoC
|
||||
from artiq import __version__ as artiq_version
|
||||
from artiq import __artiq_dir__ as artiq_dir
|
||||
|
||||
|
||||
__all__ = ["add_identifier", "build_artiq_soc"]
|
||||
|
||||
|
||||
def get_identifier_string(soc, suffix="", add_class_name=True):
|
||||
r = artiq_version
|
||||
if suffix or add_class_name:
|
||||
r += ";"
|
||||
if add_class_name:
|
||||
r += getattr(soc, "class_name_override", soc.__class__.__name__.lower())
|
||||
r += suffix
|
||||
return r
|
||||
|
||||
|
||||
class ReprogrammableIdentifier(Module, AutoCSR):
|
||||
def __init__(self, ident):
|
||||
self.address = CSRStorage(8)
|
||||
self.data = CSRStatus(8)
|
||||
|
||||
contents = list(ident.encode())
|
||||
l = len(contents)
|
||||
if l > 255:
|
||||
raise ValueError("Identifier string must be 255 characters or less")
|
||||
contents.insert(0, l)
|
||||
|
||||
for i in range(8):
|
||||
self.specials += Instance("ROM256X1", name="identifier_str"+str(i),
|
||||
i_A0=self.address.storage[0], i_A1=self.address.storage[1],
|
||||
i_A2=self.address.storage[2], i_A3=self.address.storage[3],
|
||||
i_A4=self.address.storage[4], i_A5=self.address.storage[5],
|
||||
i_A6=self.address.storage[6], i_A7=self.address.storage[7],
|
||||
o_O=self.data.status[i],
|
||||
p_INIT=sum(1 << j if c & (1 << i) else 0 for j, c in enumerate(contents)))
|
||||
|
||||
|
||||
def add_identifier(soc, *args, gateware_identifier_str=None, **kwargs):
|
||||
if hasattr(soc, "identifier"):
|
||||
raise ValueError
|
||||
identifier_str = get_identifier_string(soc, *args, **kwargs)
|
||||
soc.submodules.identifier = ReprogrammableIdentifier(gateware_identifier_str or identifier_str)
|
||||
soc.config["IDENTIFIER_STR"] = identifier_str
|
||||
|
||||
|
||||
def build_artiq_soc(soc, argdict):
|
||||
firmware_dir = os.path.join(artiq_dir, "firmware")
|
||||
builder = Builder(soc, **argdict)
|
||||
builder.software_packages = []
|
||||
builder.add_software_package("bootloader", os.path.join(firmware_dir, "bootloader"))
|
||||
if isinstance(soc, AMPSoC):
|
||||
builder.add_software_package("libm")
|
||||
builder.add_software_package("libprintf")
|
||||
builder.add_software_package("libunwind")
|
||||
builder.add_software_package("ksupport", os.path.join(firmware_dir, "ksupport"))
|
||||
builder.add_software_package("runtime", os.path.join(firmware_dir, "runtime"))
|
||||
else:
|
||||
# Assume DRTIO satellite.
|
||||
builder.add_software_package("satman", os.path.join(firmware_dir, "satman"))
|
||||
try:
|
||||
builder.build()
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SystemExit("Command {} failed".format(" ".join(e.cmd)))
|
|
@ -82,13 +82,27 @@ class TList(types.TMono):
|
|||
super().__init__("list", {"elt": elt})
|
||||
|
||||
class TArray(types.TMono):
|
||||
def __init__(self, elt=None):
|
||||
def __init__(self, elt=None, num_dims=1):
|
||||
if elt is None:
|
||||
elt = types.TVar()
|
||||
super().__init__("array", {"elt": elt})
|
||||
if isinstance(num_dims, int):
|
||||
# Make TArray more convenient to instantiate from (ARTIQ) user code.
|
||||
num_dims = types.TValue(num_dims)
|
||||
# For now, enforce number of dimensions to be known, as we'd otherwise
|
||||
# need to implement custom unification logic for the type of `shape`.
|
||||
# Default to 1 to keep compatibility with old user code from before
|
||||
# multidimensional array support.
|
||||
assert isinstance(num_dims.value, int), "Number of dimensions must be resolved"
|
||||
|
||||
super().__init__("array", {"elt": elt, "num_dims": num_dims})
|
||||
self.attributes = OrderedDict([
|
||||
("buffer", types._TPointer(elt)),
|
||||
("shape", types.TTuple([TInt32()] * num_dims.value)),
|
||||
])
|
||||
|
||||
def _array_printer(typ, printer, depth, max_depth):
|
||||
return "numpy.array(elt={})".format(printer.name(typ["elt"], depth, max_depth))
|
||||
return "numpy.array(elt={}, num_dims={})".format(
|
||||
printer.name(typ["elt"], depth, max_depth), typ["num_dims"].value)
|
||||
types.TypePrinter.custom_printers["array"] = _array_printer
|
||||
|
||||
class TRange(types.TMono):
|
||||
|
@ -169,6 +183,9 @@ def fn_ValueError():
|
|||
def fn_ZeroDivisionError():
|
||||
return types.TExceptionConstructor(TException("ZeroDivisionError"))
|
||||
|
||||
def fn_RuntimeError():
|
||||
return types.TExceptionConstructor(TException("RuntimeError"))
|
||||
|
||||
def fn_range():
|
||||
return types.TBuiltinFunction("range")
|
||||
|
||||
|
@ -178,6 +195,9 @@ def fn_len():
|
|||
def fn_round():
|
||||
return types.TBuiltinFunction("round")
|
||||
|
||||
def fn_abs():
|
||||
return types.TBuiltinFunction("abs")
|
||||
|
||||
def fn_min():
|
||||
return types.TBuiltinFunction("min")
|
||||
|
||||
|
@ -202,9 +222,6 @@ def obj_interleave():
|
|||
def obj_sequential():
|
||||
return types.TBuiltin("sequential")
|
||||
|
||||
def fn_watchdog():
|
||||
return types.TBuiltinFunction("watchdog")
|
||||
|
||||
def fn_delay():
|
||||
return types.TBuiltinFunction("delay")
|
||||
|
||||
|
@ -300,7 +317,7 @@ def is_iterable(typ):
|
|||
def get_iterable_elt(typ):
|
||||
if is_str(typ) or is_bytes(typ) or is_bytearray(typ):
|
||||
return TInt(types.TValue(8))
|
||||
elif is_iterable(typ):
|
||||
elif types._is_pointer(typ) or is_iterable(typ):
|
||||
return typ.find()["elt"].find()
|
||||
else:
|
||||
assert False
|
||||
|
@ -314,6 +331,6 @@ def is_allocated(typ):
|
|||
return not (is_none(typ) or is_bool(typ) or is_int(typ) or
|
||||
is_float(typ) or is_range(typ) or
|
||||
types._is_pointer(typ) or types.is_function(typ) or
|
||||
types.is_c_function(typ) or types.is_rpc(typ) or
|
||||
types.is_external_function(typ) or types.is_rpc(typ) or
|
||||
types.is_method(typ) or types.is_tuple(typ) or
|
||||
types.is_value(typ))
|
||||
|
|
|
@ -14,7 +14,7 @@ from pythonparser import lexer as source_lexer, parser as source_parser
|
|||
from Levenshtein import ratio as similarity, jaro_winkler
|
||||
|
||||
from ..language import core as language_core
|
||||
from . import types, builtins, asttyped, prelude
|
||||
from . import types, builtins, asttyped, math_fns, prelude
|
||||
from .transforms import ASTTypedRewriter, Inferencer, IntMonomorphizer, TypedtreePrinter
|
||||
from .transforms.asttyped_rewriter import LocalExtractor
|
||||
|
||||
|
@ -166,6 +166,10 @@ class ASTSynthesizer:
|
|||
typ = builtins.TBool()
|
||||
return asttyped.NameConstantT(value=value, type=typ,
|
||||
loc=self._add(repr(value)))
|
||||
elif value is numpy.float:
|
||||
typ = builtins.fn_float()
|
||||
return asttyped.NameConstantT(value=None, type=typ,
|
||||
loc=self._add("numpy.float"))
|
||||
elif value is numpy.int32:
|
||||
typ = builtins.fn_int32()
|
||||
return asttyped.NameConstantT(value=None, type=typ,
|
||||
|
@ -221,21 +225,23 @@ class ASTSynthesizer:
|
|||
return asttyped.ListT(elts=elts, ctx=None, type=builtins.TList(),
|
||||
begin_loc=begin_loc, end_loc=end_loc,
|
||||
loc=begin_loc.join(end_loc))
|
||||
elif isinstance(value, numpy.ndarray):
|
||||
begin_loc = self._add("numpy.array([")
|
||||
elif isinstance(value, tuple):
|
||||
begin_loc = self._add("(")
|
||||
elts = []
|
||||
for index, elt in enumerate(value):
|
||||
elts.append(self.quote(elt))
|
||||
if index < len(value) - 1:
|
||||
self._add(", ")
|
||||
end_loc = self._add("])")
|
||||
|
||||
return asttyped.ListT(elts=elts, ctx=None, type=builtins.TArray(),
|
||||
end_loc = self._add(")")
|
||||
return asttyped.TupleT(elts=elts, ctx=None,
|
||||
type=types.TTuple([e.type for e in elts]),
|
||||
begin_loc=begin_loc, end_loc=end_loc,
|
||||
loc=begin_loc.join(end_loc))
|
||||
elif isinstance(value, numpy.ndarray):
|
||||
return self.call(numpy.array, [list(value)], {})
|
||||
elif inspect.isfunction(value) or inspect.ismethod(value) or \
|
||||
isinstance(value, pytypes.BuiltinFunctionType) or \
|
||||
isinstance(value, SpecializedFunction):
|
||||
isinstance(value, SpecializedFunction) or \
|
||||
isinstance(value, numpy.ufunc):
|
||||
if inspect.ismethod(value):
|
||||
quoted_self = self.quote(value.__self__)
|
||||
function_type = self.quote_function(value.__func__, self.expanded_from)
|
||||
|
@ -749,6 +755,14 @@ class Stitcher:
|
|||
quote_function=self._quote_function)
|
||||
|
||||
def _function_loc(self, function):
|
||||
if isinstance(function, SpecializedFunction):
|
||||
function = function.host_function
|
||||
if hasattr(function, 'artiq_embedded') and function.artiq_embedded.function:
|
||||
function = function.artiq_embedded.function
|
||||
|
||||
if isinstance(function, str):
|
||||
return source.Range(source.Buffer(function, "<string>"), 0, 0)
|
||||
|
||||
filename = function.__code__.co_filename
|
||||
line = function.__code__.co_firstlineno
|
||||
name = function.__code__.co_name
|
||||
|
@ -832,6 +846,16 @@ class Stitcher:
|
|||
|
||||
# Extract function source.
|
||||
embedded_function = host_function.artiq_embedded.function
|
||||
if isinstance(embedded_function, str):
|
||||
# This is a function to be eval'd from the given source code in string form.
|
||||
# Mangle the host function's id() into the fully qualified name to make sure
|
||||
# there are no collisions.
|
||||
source_code = embedded_function
|
||||
embedded_function = host_function
|
||||
filename = "<string>"
|
||||
module_name = "__eval_{}".format(id(host_function))
|
||||
first_line = 1
|
||||
else:
|
||||
source_code = inspect.getsource(embedded_function)
|
||||
filename = embedded_function.__code__.co_filename
|
||||
module_name = embedded_function.__globals__['__name__']
|
||||
|
@ -921,6 +945,9 @@ class Stitcher:
|
|||
return function_node
|
||||
|
||||
def _extract_annot(self, function, annot, kind, call_loc, fn_kind):
|
||||
if annot is None:
|
||||
annot = builtins.TNone()
|
||||
|
||||
if not isinstance(annot, types.Type):
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"type annotation for {kind}, '{annot}', is not an ARTIQ type",
|
||||
|
@ -969,7 +996,7 @@ class Stitcher:
|
|||
self.engine.process(diag)
|
||||
ret_type = types.TVar()
|
||||
|
||||
function_type = types.TCFunction(arg_types, ret_type,
|
||||
function_type = types.TExternalFunction(arg_types, ret_type,
|
||||
name=function.artiq_embedded.syscall,
|
||||
flags=function.artiq_embedded.flags)
|
||||
self.functions[function] = function_type
|
||||
|
@ -1014,7 +1041,7 @@ class Stitcher:
|
|||
|
||||
function_type = types.TRPC(ret_type,
|
||||
service=self.embedding_map.store_object(host_function),
|
||||
async=is_async)
|
||||
is_async=is_async)
|
||||
self.functions[function] = function_type
|
||||
return function_type
|
||||
|
||||
|
@ -1025,7 +1052,11 @@ class Stitcher:
|
|||
host_function = function
|
||||
|
||||
if function in self.functions:
|
||||
pass
|
||||
return self.functions[function]
|
||||
|
||||
math_type = math_fns.match(function)
|
||||
if math_type is not None:
|
||||
self.functions[function] = math_type
|
||||
elif not hasattr(host_function, "artiq_embedded") or \
|
||||
(host_function.artiq_embedded.core_name is None and
|
||||
host_function.artiq_embedded.portable is False and
|
||||
|
|
|
@ -36,6 +36,48 @@ class TKeyword(types.TMono):
|
|||
def is_keyword(typ):
|
||||
return isinstance(typ, TKeyword)
|
||||
|
||||
|
||||
# See rpc_proto.rs and comm_kernel.py:_{send,receive}_rpc_value.
|
||||
def rpc_tag(typ, error_handler):
|
||||
typ = typ.find()
|
||||
if types.is_tuple(typ):
|
||||
assert len(typ.elts) < 256
|
||||
return b"t" + bytes([len(typ.elts)]) + \
|
||||
b"".join([rpc_tag(elt_type, error_handler)
|
||||
for elt_type in typ.elts])
|
||||
elif builtins.is_none(typ):
|
||||
return b"n"
|
||||
elif builtins.is_bool(typ):
|
||||
return b"b"
|
||||
elif builtins.is_int(typ, types.TValue(32)):
|
||||
return b"i"
|
||||
elif builtins.is_int(typ, types.TValue(64)):
|
||||
return b"I"
|
||||
elif builtins.is_float(typ):
|
||||
return b"f"
|
||||
elif builtins.is_str(typ):
|
||||
return b"s"
|
||||
elif builtins.is_bytes(typ):
|
||||
return b"B"
|
||||
elif builtins.is_bytearray(typ):
|
||||
return b"A"
|
||||
elif builtins.is_list(typ):
|
||||
return b"l" + rpc_tag(builtins.get_iterable_elt(typ), error_handler)
|
||||
elif builtins.is_array(typ):
|
||||
num_dims = typ["num_dims"].value
|
||||
return b"a" + bytes([num_dims]) + rpc_tag(typ["elt"], error_handler)
|
||||
elif builtins.is_range(typ):
|
||||
return b"r" + rpc_tag(builtins.get_iterable_elt(typ), error_handler)
|
||||
elif is_keyword(typ):
|
||||
return b"k" + rpc_tag(typ.params["value"], error_handler)
|
||||
elif types.is_function(typ) or types.is_method(typ) or types.is_rpc(typ):
|
||||
raise ValueError("RPC tag for functional value")
|
||||
elif '__objectid__' in typ.attributes:
|
||||
return b"O"
|
||||
else:
|
||||
error_handler(typ)
|
||||
|
||||
|
||||
class Value:
|
||||
"""
|
||||
An SSA value that keeps track of its uses.
|
||||
|
@ -303,6 +345,7 @@ class BasicBlock(NamedValue):
|
|||
|
||||
:ivar instructions: (list of :class:`Instruction`)
|
||||
"""
|
||||
_dump_loc = True
|
||||
|
||||
def __init__(self, instructions, name=""):
|
||||
super().__init__(TBasicBlock(), name)
|
||||
|
@ -378,12 +421,12 @@ class BasicBlock(NamedValue):
|
|||
lines = ["{}:".format(escape_name(self.name))]
|
||||
if self.function is not None:
|
||||
lines[0] += " ; predecessors: {}".format(
|
||||
", ".join([escape_name(pred.name) for pred in self.predecessors()]))
|
||||
", ".join(sorted([escape_name(pred.name) for pred in self.predecessors()])))
|
||||
|
||||
# Annotated instructions
|
||||
loc = None
|
||||
for insn in self.instructions:
|
||||
if loc != insn.loc:
|
||||
if self._dump_loc and loc != insn.loc:
|
||||
loc = insn.loc
|
||||
|
||||
if loc is None:
|
||||
|
@ -409,7 +452,13 @@ class BasicBlock(NamedValue):
|
|||
class Argument(NamedValue):
|
||||
"""
|
||||
A function argument.
|
||||
|
||||
:ivar loc: (:class:`pythonparser.source.Range` or None)
|
||||
source location
|
||||
"""
|
||||
def __init__(self, typ, name):
|
||||
super().__init__(typ, name)
|
||||
self.loc = None
|
||||
|
||||
def as_entity(self, type_printer):
|
||||
return self.as_operand(type_printer)
|
||||
|
@ -731,6 +780,33 @@ class SetAttr(Instruction):
|
|||
def value(self):
|
||||
return self.operands[1]
|
||||
|
||||
class Offset(Instruction):
|
||||
"""
|
||||
An intruction that adds an offset to a pointer (indexes into a list).
|
||||
|
||||
This is used to represent internally generated pointer arithmetic, and must
|
||||
remain inside the same object (see :class:`GetElem` and LLVM's GetElementPtr).
|
||||
"""
|
||||
|
||||
"""
|
||||
:param lst: (:class:`Value`) list
|
||||
:param index: (:class:`Value`) index
|
||||
"""
|
||||
def __init__(self, base, offset, name=""):
|
||||
assert isinstance(base, Value)
|
||||
assert isinstance(offset, Value)
|
||||
typ = types._TPointer(builtins.get_iterable_elt(base.type))
|
||||
super().__init__([base, offset], typ, name)
|
||||
|
||||
def opcode(self):
|
||||
return "offset"
|
||||
|
||||
def base(self):
|
||||
return self.operands[0]
|
||||
|
||||
def index(self):
|
||||
return self.operands[1]
|
||||
|
||||
class GetElem(Instruction):
|
||||
"""
|
||||
An intruction that loads an element from a list.
|
||||
|
@ -748,7 +824,7 @@ class GetElem(Instruction):
|
|||
def opcode(self):
|
||||
return "getelem"
|
||||
|
||||
def list(self):
|
||||
def base(self):
|
||||
return self.operands[0]
|
||||
|
||||
def index(self):
|
||||
|
@ -774,7 +850,7 @@ class SetElem(Instruction):
|
|||
def opcode(self):
|
||||
return "setelem"
|
||||
|
||||
def list(self):
|
||||
def base(self):
|
||||
return self.operands[0]
|
||||
|
||||
def index(self):
|
||||
|
@ -833,6 +909,7 @@ class Arith(Instruction):
|
|||
def rhs(self):
|
||||
return self.operands[1]
|
||||
|
||||
|
||||
class Compare(Instruction):
|
||||
"""
|
||||
A comparison operation on numbers.
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
r"""
|
||||
The :mod:`math_fns` module lists math-related functions from NumPy recognized
|
||||
by the ARTIQ compiler so host function objects can be :func:`match`\ ed to
|
||||
the compiler type metadata describing their core device analogue.
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
import numpy
|
||||
from . import builtins, types
|
||||
|
||||
# Some special mathematical functions are exposed via their scipy.special
|
||||
# equivalents. Since the rest of the ARTIQ core does not depend on SciPy,
|
||||
# gracefully handle it not being present, making the functions simply not
|
||||
# available.
|
||||
try:
|
||||
import scipy.special as scipy_special
|
||||
except ImportError:
|
||||
scipy_special = None
|
||||
|
||||
#: float -> float numpy.* math functions for which llvm.* intrinsics exist.
|
||||
unary_fp_intrinsics = [(name, "llvm." + name + ".f64") for name in [
|
||||
"sin",
|
||||
"cos",
|
||||
"exp",
|
||||
"exp2",
|
||||
"log",
|
||||
"log10",
|
||||
"log2",
|
||||
"fabs",
|
||||
"floor",
|
||||
"ceil",
|
||||
"trunc",
|
||||
"sqrt",
|
||||
]] + [
|
||||
# numpy.rint() seems to (NumPy 1.19.0, Python 3.8.5, Linux x86_64)
|
||||
# implement round-to-even, but unfortunately, rust-lang/libm only
|
||||
# provides round(), which always rounds away from zero.
|
||||
#
|
||||
# As there is no equivalent of the latter in NumPy (nor any other
|
||||
# basic rounding function), expose round() as numpy.rint anyway,
|
||||
# even if the rounding modes don't match up, so there is some way
|
||||
# to do rounding on the core device. (numpy.round() has entirely
|
||||
# different semantics; it rounds to a configurable number of
|
||||
# decimals.)
|
||||
("rint", "llvm.round.f64"),
|
||||
]
|
||||
|
||||
#: float -> float numpy.* math functions lowered to runtime calls.
|
||||
unary_fp_runtime_calls = [
|
||||
("tan", "tan"),
|
||||
("arcsin", "asin"),
|
||||
("arccos", "acos"),
|
||||
("arctan", "atan"),
|
||||
("sinh", "sinh"),
|
||||
("cosh", "cosh"),
|
||||
("tanh", "tanh"),
|
||||
("arcsinh", "asinh"),
|
||||
("arccosh", "acosh"),
|
||||
("arctanh", "atanh"),
|
||||
("expm1", "expm1"),
|
||||
("cbrt", "cbrt"),
|
||||
]
|
||||
|
||||
#: float -> float numpy.* math functions lowered to runtime calls.
|
||||
unary_fp_runtime_calls = [
|
||||
("tan", "tan"),
|
||||
("arcsin", "asin"),
|
||||
("arccos", "acos"),
|
||||
("arctan", "atan"),
|
||||
("sinh", "sinh"),
|
||||
("cosh", "cosh"),
|
||||
("tanh", "tanh"),
|
||||
("arcsinh", "asinh"),
|
||||
("arccosh", "acosh"),
|
||||
("arctanh", "atanh"),
|
||||
("expm1", "expm1"),
|
||||
("cbrt", "cbrt"),
|
||||
]
|
||||
|
||||
scipy_special_unary_runtime_calls = [
|
||||
("erf", "erf"),
|
||||
("erfc", "erfc"),
|
||||
("gamma", "tgamma"),
|
||||
("gammaln", "lgamma"),
|
||||
("j0", "j0"),
|
||||
("j1", "j1"),
|
||||
("y0", "y0"),
|
||||
("y1", "y1"),
|
||||
]
|
||||
# Not mapped: jv/yv, libm only supports integer orders.
|
||||
|
||||
#: (float, float) -> float numpy.* math functions lowered to runtime calls.
|
||||
binary_fp_runtime_calls = [
|
||||
("arctan2", "atan2"),
|
||||
("copysign", "copysign"),
|
||||
("fmax", "fmax"),
|
||||
("fmin", "fmin"),
|
||||
# ("ldexp", "ldexp"), # One argument is an int; would need a bit more plumbing.
|
||||
("hypot", "hypot"),
|
||||
("nextafter", "nextafter"),
|
||||
]
|
||||
|
||||
#: Array handling builtins (special treatment due to allocations).
|
||||
numpy_builtins = ["transpose"]
|
||||
|
||||
|
||||
def fp_runtime_type(name, arity):
|
||||
args = [("arg{}".format(i), builtins.TFloat()) for i in range(arity)]
|
||||
return types.TExternalFunction(
|
||||
OrderedDict(args),
|
||||
builtins.TFloat(),
|
||||
name,
|
||||
# errno isn't observable from ARTIQ Python.
|
||||
flags={"nounwind", "nowrite"},
|
||||
broadcast_across_arrays=True)
|
||||
|
||||
|
||||
math_fn_map = {
|
||||
getattr(numpy, symbol): fp_runtime_type(mangle, arity=1)
|
||||
for symbol, mangle in (unary_fp_intrinsics + unary_fp_runtime_calls)
|
||||
}
|
||||
for symbol, mangle in binary_fp_runtime_calls:
|
||||
math_fn_map[getattr(numpy, symbol)] = fp_runtime_type(mangle, arity=2)
|
||||
for name in numpy_builtins:
|
||||
math_fn_map[getattr(numpy, name)] = types.TBuiltinFunction("numpy." + name)
|
||||
if scipy_special is not None:
|
||||
for symbol, mangle in scipy_special_unary_runtime_calls:
|
||||
math_fn_map[getattr(scipy_special, symbol)] = fp_runtime_type(mangle, arity=1)
|
||||
|
||||
|
||||
def match(obj):
|
||||
return math_fn_map.get(obj, None)
|
|
@ -60,12 +60,14 @@ class Module:
|
|||
ref_period=ref_period)
|
||||
dead_code_eliminator = transforms.DeadCodeEliminator(engine=self.engine)
|
||||
local_access_validator = validators.LocalAccessValidator(engine=self.engine)
|
||||
local_demoter = transforms.LocalDemoter()
|
||||
constant_hoister = transforms.ConstantHoister()
|
||||
devirtualization = analyses.Devirtualization()
|
||||
interleaver = transforms.Interleaver(engine=self.engine)
|
||||
invariant_detection = analyses.InvariantDetection(engine=self.engine)
|
||||
|
||||
cast_monomorphizer.visit(src.typedtree)
|
||||
int_monomorphizer.visit(src.typedtree)
|
||||
cast_monomorphizer.visit(src.typedtree)
|
||||
inferencer.visit(src.typedtree)
|
||||
monomorphism_validator.visit(src.typedtree)
|
||||
escape_validator.visit(src.typedtree)
|
||||
|
@ -77,6 +79,8 @@ class Module:
|
|||
dead_code_eliminator.process(self.artiq_ir)
|
||||
interleaver.process(self.artiq_ir)
|
||||
local_access_validator.process(self.artiq_ir)
|
||||
local_demoter.process(self.artiq_ir)
|
||||
constant_hoister.process(self.artiq_ir)
|
||||
if remarks:
|
||||
invariant_detection.process(self.artiq_ir)
|
||||
|
||||
|
|
|
@ -25,10 +25,12 @@ def globals():
|
|||
"IndexError": builtins.fn_IndexError(),
|
||||
"ValueError": builtins.fn_ValueError(),
|
||||
"ZeroDivisionError": builtins.fn_ZeroDivisionError(),
|
||||
"RuntimeError": builtins.fn_RuntimeError(),
|
||||
|
||||
# Built-in Python functions
|
||||
"len": builtins.fn_len(),
|
||||
"round": builtins.fn_round(),
|
||||
"abs": builtins.fn_abs(),
|
||||
"min": builtins.fn_min(),
|
||||
"max": builtins.fn_max(),
|
||||
"print": builtins.fn_print(),
|
||||
|
@ -42,7 +44,6 @@ def globals():
|
|||
"parallel": builtins.obj_parallel(),
|
||||
"interleave": builtins.obj_interleave(),
|
||||
"sequential": builtins.obj_sequential(),
|
||||
"watchdog": builtins.fn_watchdog(),
|
||||
|
||||
# ARTIQ time management functions
|
||||
"delay": builtins.fn_delay(),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import os, sys, tempfile, subprocess
|
||||
from artiq.compiler import types
|
||||
import os, sys, tempfile, subprocess, io
|
||||
from artiq.compiler import types, ir
|
||||
from llvmlite_artiq import ir as ll, binding as llvm
|
||||
|
||||
llvm.initialize()
|
||||
|
@ -8,40 +8,44 @@ llvm.initialize_all_asmprinters()
|
|||
|
||||
class RunTool:
|
||||
def __init__(self, pattern, **tempdata):
|
||||
self.files = []
|
||||
self.pattern = pattern
|
||||
self.tempdata = tempdata
|
||||
|
||||
def maketemp(self, data):
|
||||
f = tempfile.NamedTemporaryFile()
|
||||
f.write(data)
|
||||
f.flush()
|
||||
self.files.append(f)
|
||||
return f
|
||||
self._pattern = pattern
|
||||
self._tempdata = tempdata
|
||||
self._tempnames = {}
|
||||
self._tempfiles = {}
|
||||
|
||||
def __enter__(self):
|
||||
tempfiles = {}
|
||||
tempnames = {}
|
||||
for key in self.tempdata:
|
||||
tempfiles[key] = self.maketemp(self.tempdata[key])
|
||||
tempnames[key] = tempfiles[key].name
|
||||
for key, data in self._tempdata.items():
|
||||
if data is None:
|
||||
fd, filename = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
self._tempnames[key] = filename
|
||||
else:
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
f.write(data)
|
||||
self._tempnames[key] = f.name
|
||||
|
||||
cmdline = []
|
||||
for argument in self.pattern:
|
||||
cmdline.append(argument.format(**tempnames))
|
||||
for argument in self._pattern:
|
||||
cmdline.append(argument.format(**self._tempnames))
|
||||
|
||||
process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
stdout, stderr = process.communicate()
|
||||
if process.returncode != 0:
|
||||
raise Exception("{} invocation failed: {}".
|
||||
format(cmdline[0], stderr.decode('utf-8')))
|
||||
format(cmdline[0], stderr))
|
||||
|
||||
tempfiles["__stdout__"] = stdout.decode('utf-8')
|
||||
return tempfiles
|
||||
self._tempfiles["__stdout__"] = io.StringIO(stdout)
|
||||
for key in self._tempdata:
|
||||
if self._tempdata[key] is None:
|
||||
self._tempfiles[key] = open(self._tempnames[key], "rb")
|
||||
return self._tempfiles
|
||||
|
||||
def __exit__(self, exc_typ, exc_value, exc_trace):
|
||||
for f in self.files:
|
||||
f.close()
|
||||
for file in self._tempfiles.values():
|
||||
file.close()
|
||||
for filename in self._tempnames.values():
|
||||
os.unlink(filename)
|
||||
|
||||
def _dump(target, kind, suffix, content):
|
||||
if target is not None:
|
||||
|
@ -71,12 +75,23 @@ class Target:
|
|||
:var print_function: (string)
|
||||
Name of a formatted print functions (with the signature of ``printf``)
|
||||
provided by the target, e.g. ``"printf"``.
|
||||
:var little_endian: (boolean)
|
||||
Whether the code will be executed on a little-endian machine. This cannot be always
|
||||
determined from data_layout due to JIT.
|
||||
:var now_pinning: (boolean)
|
||||
Whether the target implements the now-pinning RTIO optimization.
|
||||
"""
|
||||
triple = "unknown"
|
||||
data_layout = ""
|
||||
features = []
|
||||
print_function = "printf"
|
||||
little_endian = False
|
||||
now_pinning = True
|
||||
|
||||
tool_ld = "ld.lld"
|
||||
tool_strip = "llvm-strip"
|
||||
tool_addr2line = "llvm-addr2line"
|
||||
tool_cxxfilt = "llvm-cxxfilt"
|
||||
|
||||
def __init__(self):
|
||||
self.llcontext = ll.Context()
|
||||
|
@ -127,6 +142,9 @@ class Target:
|
|||
print("====== MODULE_SIGNATURE DUMP ======", file=sys.stderr)
|
||||
print(module, file=sys.stderr)
|
||||
|
||||
if os.getenv("ARTIQ_IR_NO_LOC") is not None:
|
||||
ir.BasicBlock._dump_loc = False
|
||||
|
||||
type_printer = types.TypePrinter()
|
||||
_dump(os.getenv("ARTIQ_DUMP_IR"), "ARTIQ IR", ".txt",
|
||||
lambda: "\n".join(fn.as_entity(type_printer) for fn in module.artiq_ir))
|
||||
|
@ -163,10 +181,11 @@ class Target:
|
|||
|
||||
def link(self, objects):
|
||||
"""Link the relocatable objects into a shared library for this target."""
|
||||
with RunTool([self.triple + "-ld", "-shared", "--eh-frame-hdr"] +
|
||||
with RunTool([self.tool_ld, "-shared", "--eh-frame-hdr"] +
|
||||
["{{obj{}}}".format(index) for index in range(len(objects))] +
|
||||
["-x"] +
|
||||
["-o", "{output}"],
|
||||
output=b"",
|
||||
output=None,
|
||||
**{"obj{}".format(index): obj for index, obj in enumerate(objects)}) \
|
||||
as results:
|
||||
library = results["output"].read()
|
||||
|
@ -180,8 +199,8 @@ class Target:
|
|||
return self.link([self.assemble(self.compile(module)) for module in modules])
|
||||
|
||||
def strip(self, library):
|
||||
with RunTool([self.triple + "-strip", "--strip-debug", "{library}", "-o", "{output}"],
|
||||
library=library, output=b"") \
|
||||
with RunTool([self.tool_strip, "--strip-debug", "{library}", "-o", "{output}"],
|
||||
library=library, output=None) \
|
||||
as results:
|
||||
return results["output"].read()
|
||||
|
||||
|
@ -194,11 +213,11 @@ class Target:
|
|||
# inside the call instruction (or its delay slot), since that's what
|
||||
# the backtrace entry should point at.
|
||||
offset_addresses = [hex(addr - 1) for addr in addresses]
|
||||
with RunTool([self.triple + "-addr2line", "--addresses", "--functions", "--inlines",
|
||||
with RunTool([self.tool_addr2line, "--addresses", "--functions", "--inlines",
|
||||
"--demangle", "--exe={library}"] + offset_addresses,
|
||||
library=library) \
|
||||
as results:
|
||||
lines = iter(results["__stdout__"].rstrip().split("\n"))
|
||||
lines = iter(results["__stdout__"].read().rstrip().split("\n"))
|
||||
backtrace = []
|
||||
while True:
|
||||
try:
|
||||
|
@ -216,18 +235,25 @@ class Target:
|
|||
filename, line = location.rsplit(":", 1)
|
||||
if filename == "??" or filename == "<synthesized>":
|
||||
continue
|
||||
if line == "?":
|
||||
line = -1
|
||||
else:
|
||||
line = int(line)
|
||||
# can't get column out of addr2line D:
|
||||
backtrace.append((filename, int(line), -1, function, address))
|
||||
backtrace.append((filename, line, -1, function, address))
|
||||
return backtrace
|
||||
|
||||
def demangle(self, names):
|
||||
with RunTool([self.triple + "-c++filt"] + names) as results:
|
||||
return results["__stdout__"].rstrip().split("\n")
|
||||
with RunTool([self.tool_cxxfilt] + names) as results:
|
||||
return results["__stdout__"].read().rstrip().split("\n")
|
||||
|
||||
class NativeTarget(Target):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.triple = llvm.get_default_triple()
|
||||
host_data_layout = str(llvm.targets.Target.from_default_triple().create_target_machine().target_data)
|
||||
assert host_data_layout[0] in "eE"
|
||||
self.little_endian = host_data_layout[0] == "e"
|
||||
|
||||
class OR1KTarget(Target):
|
||||
triple = "or1k-linux"
|
||||
|
@ -235,3 +261,23 @@ class OR1KTarget(Target):
|
|||
"f64:32:32-v64:32:32-v128:32:32-a0:0:32-n32"
|
||||
features = ["mul", "div", "ffl1", "cmov", "addc"]
|
||||
print_function = "core_log"
|
||||
little_endian = False
|
||||
now_pinning = True
|
||||
|
||||
tool_ld = "or1k-linux-ld"
|
||||
tool_strip = "or1k-linux-strip"
|
||||
tool_addr2line = "or1k-linux-addr2line"
|
||||
tool_cxxfilt = "or1k-linux-c++filt"
|
||||
|
||||
class CortexA9Target(Target):
|
||||
triple = "armv7-unknown-linux-gnueabihf"
|
||||
data_layout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64"
|
||||
features = ["dsp", "fp16", "neon", "vfp3"]
|
||||
print_function = "core_log"
|
||||
little_endian = True
|
||||
now_pinning = False
|
||||
|
||||
tool_ld = "armv7-unknown-linux-gnueabihf-ld"
|
||||
tool_strip = "armv7-unknown-linux-gnueabihf-strip"
|
||||
tool_addr2line = "armv7-unknown-linux-gnueabihf-addr2line"
|
||||
tool_cxxfilt = "armv7-unknown-linux-gnueabihf-c++filt"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import sys, os
|
||||
import sys, os, tokenize
|
||||
|
||||
from artiq.master.databases import DeviceDB
|
||||
from artiq.master.worker_db import DeviceManager
|
||||
|
@ -27,7 +27,7 @@ def main():
|
|||
ddb_path = os.path.join(os.path.dirname(sys.argv[1]), "device_db.py")
|
||||
dmgr = DeviceManager(DeviceDB(ddb_path))
|
||||
|
||||
with open(sys.argv[1]) as f:
|
||||
with tokenize.open(sys.argv[1]) as f:
|
||||
testcase_code = compile(f.read(), f.name, "exec")
|
||||
testcase_vars = {'__name__': 'testbench', 'dmgr': dmgr}
|
||||
exec(testcase_code, testcase_vars)
|
||||
|
@ -38,8 +38,6 @@ def main():
|
|||
core.compile(testcase_vars["entrypoint"], (), {})
|
||||
else:
|
||||
core.run(testcase_vars["entrypoint"], (), {})
|
||||
print(core.comm.get_log())
|
||||
core.comm.clear_log()
|
||||
except CompileError as error:
|
||||
if not diag:
|
||||
exit(1)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import sys, fileinput
|
||||
import sys, os, fileinput
|
||||
from pythonparser import diagnostic
|
||||
from .. import ir
|
||||
from ..module import Module, Source
|
||||
|
||||
def main():
|
||||
if os.getenv("ARTIQ_IR_NO_LOC") is not None:
|
||||
ir.BasicBlock._dump_loc = False
|
||||
|
||||
def process_diagnostic(diag):
|
||||
print("\n".join(diag.render()))
|
||||
if diag.level in ("fatal", "error"):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import sys, os
|
||||
import sys, os, tokenize
|
||||
from pythonparser import diagnostic
|
||||
from ...language.environment import ProcessArgumentManager
|
||||
from ...master.databases import DeviceDB, DatasetDB
|
||||
|
@ -22,7 +22,7 @@ def main():
|
|||
engine = diagnostic.Engine()
|
||||
engine.process = process_diagnostic
|
||||
|
||||
with open(sys.argv[1]) as f:
|
||||
with tokenize.open(sys.argv[1]) as f:
|
||||
testcase_code = compile(f.read(), f.name, "exec")
|
||||
testcase_vars = {'__name__': 'testbench'}
|
||||
exec(testcase_code, testcase_vars)
|
||||
|
|
|
@ -5,6 +5,8 @@ from .cast_monomorphizer import CastMonomorphizer
|
|||
from .iodelay_estimator import IODelayEstimator
|
||||
from .artiq_ir_generator import ARTIQIRGenerator
|
||||
from .dead_code_eliminator import DeadCodeEliminator
|
||||
from .llvm_ir_generator import LLVMIRGenerator
|
||||
from .local_demoter import LocalDemoter
|
||||
from .constant_hoister import ConstantHoister
|
||||
from .interleaver import Interleaver
|
||||
from .typedtree_printer import TypedtreePrinter
|
||||
from .llvm_ir_generator import LLVMIRGenerator
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,13 +11,12 @@ class CastMonomorphizer(algorithm.Visitor):
|
|||
self.engine = engine
|
||||
|
||||
def visit_CallT(self, node):
|
||||
self.generic_visit(node)
|
||||
|
||||
if (types.is_builtin(node.func.type, "int") or
|
||||
types.is_builtin(node.func.type, "int32") or
|
||||
types.is_builtin(node.func.type, "int64")):
|
||||
typ = node.type.find()
|
||||
if (not types.is_var(typ["width"]) and
|
||||
len(node.args) == 1 and
|
||||
builtins.is_int(node.args[0].type) and
|
||||
types.is_var(node.args[0].type.find()["width"])):
|
||||
if isinstance(node.args[0], asttyped.BinOpT):
|
||||
|
@ -29,3 +28,20 @@ class CastMonomorphizer(algorithm.Visitor):
|
|||
|
||||
node.args[0].type.unify(typ)
|
||||
|
||||
if types.is_builtin(node.func.type, "int") or \
|
||||
types.is_builtin(node.func.type, "round"):
|
||||
typ = node.type.find()
|
||||
if types.is_var(typ["width"]):
|
||||
typ["width"].unify(types.TValue(32))
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_CoerceT(self, node):
|
||||
if isinstance(node.value, asttyped.NumT) and \
|
||||
builtins.is_int(node.type) and \
|
||||
builtins.is_int(node.value.type) and \
|
||||
not types.is_var(node.type["width"]) and \
|
||||
types.is_var(node.value.type["width"]):
|
||||
node.value.type.unify(node.type)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
:class:`ConstantHoister` is a code motion transform:
|
||||
it moves any invariant loads to the earliest point where
|
||||
they may be executed.
|
||||
"""
|
||||
|
||||
from .. import types, ir
|
||||
|
||||
class ConstantHoister:
|
||||
def process(self, functions):
|
||||
for func in functions:
|
||||
self.process_function(func)
|
||||
|
||||
def process_function(self, func):
|
||||
entry = func.entry()
|
||||
worklist = set(func.instructions())
|
||||
moved = set()
|
||||
while len(worklist) > 0:
|
||||
insn = worklist.pop()
|
||||
|
||||
if (isinstance(insn, ir.GetAttr) and insn not in moved and
|
||||
types.is_instance(insn.object().type) and
|
||||
insn.attr in insn.object().type.constant_attributes):
|
||||
has_variant_operands = False
|
||||
index_in_entry = 0
|
||||
for operand in insn.operands:
|
||||
if isinstance(operand, ir.Argument):
|
||||
pass
|
||||
elif isinstance(operand, ir.Instruction) and operand.basic_block == entry:
|
||||
index_in_entry = entry.index(operand) + 1
|
||||
else:
|
||||
has_variant_operands = True
|
||||
break
|
||||
|
||||
if has_variant_operands:
|
||||
continue
|
||||
|
||||
insn.remove_from_parent()
|
||||
entry.instructions.insert(index_in_entry, insn)
|
||||
moved.add(insn)
|
||||
|
||||
for use in insn.uses:
|
||||
worklist.add(use)
|
|
@ -33,7 +33,8 @@ class DeadCodeEliminator:
|
|||
# it also has to run after the interleaver, but interleaver
|
||||
# doesn't like to work with IR before DCE.
|
||||
if isinstance(insn, (ir.Phi, ir.Alloc, ir.GetAttr, ir.GetElem, ir.Coerce,
|
||||
ir.Arith, ir.Compare, ir.Select, ir.Quote, ir.Closure)) \
|
||||
ir.Arith, ir.Compare, ir.Select, ir.Quote, ir.Closure,
|
||||
ir.Offset)) \
|
||||
and not any(insn.uses):
|
||||
insn.erase()
|
||||
modified = True
|
||||
|
|
|
@ -7,6 +7,7 @@ from pythonparser import algorithm, diagnostic, ast
|
|||
from .. import asttyped, types, builtins
|
||||
from .typedtree_printer import TypedtreePrinter
|
||||
|
||||
|
||||
class Inferencer(algorithm.Visitor):
|
||||
"""
|
||||
:class:`Inferencer` infers types by recursively applying the unification
|
||||
|
@ -183,6 +184,14 @@ class Inferencer(algorithm.Visitor):
|
|||
if builtins.is_bytes(collection.type) or builtins.is_bytearray(collection.type):
|
||||
self._unify(element.type, builtins.get_iterable_elt(collection.type),
|
||||
element.loc, None)
|
||||
elif builtins.is_array(collection.type):
|
||||
array_type = collection.type.find()
|
||||
elem_dims = array_type["num_dims"].value - 1
|
||||
if elem_dims > 0:
|
||||
elem_type = builtins.TArray(array_type["elt"], types.TValue(elem_dims))
|
||||
else:
|
||||
elem_type = array_type["elt"]
|
||||
self._unify(element.type, elem_type, element.loc, collection.loc)
|
||||
elif builtins.is_iterable(collection.type) and not builtins.is_str(collection.type):
|
||||
rhs_type = collection.type.find()
|
||||
rhs_wrapped_lhs_type = types.TMono(rhs_type.name, {"elt": element.type})
|
||||
|
@ -199,10 +208,9 @@ class Inferencer(algorithm.Visitor):
|
|||
self.generic_visit(node)
|
||||
value = node.value
|
||||
if types.is_tuple(value.type):
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"multi-dimensional slices are not supported", {},
|
||||
node.loc, [])
|
||||
self.engine.process(diag)
|
||||
for elt in value.type.find().elts:
|
||||
self._unify(elt, builtins.TInt(),
|
||||
value.loc, None)
|
||||
else:
|
||||
self._unify(value.type, builtins.TInt(),
|
||||
value.loc, None)
|
||||
|
@ -228,10 +236,39 @@ class Inferencer(algorithm.Visitor):
|
|||
def visit_SubscriptT(self, node):
|
||||
self.generic_visit(node)
|
||||
if isinstance(node.slice, ast.Index):
|
||||
if types.is_tuple(node.slice.value.type):
|
||||
if types.is_var(node.value.type):
|
||||
return
|
||||
if not builtins.is_array(node.value.type):
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error",
|
||||
"multi-dimensional indexing only supported for arrays, not {type}",
|
||||
{"type": types.TypePrinter().name(node.value.type)},
|
||||
node.loc, [])
|
||||
self.engine.process(diag)
|
||||
return
|
||||
num_idxs = len(node.slice.value.type.find().elts)
|
||||
array_type = node.value.type.find()
|
||||
num_dims = array_type["num_dims"].value
|
||||
remaining_dims = num_dims - num_idxs
|
||||
if remaining_dims < 0:
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error",
|
||||
"too many indices for array of dimension {num_dims}",
|
||||
{"num_dims": num_dims}, node.slice.loc, [])
|
||||
self.engine.process(diag)
|
||||
return
|
||||
if remaining_dims == 0:
|
||||
self._unify(node.type, array_type["elt"], node.loc,
|
||||
node.value.loc)
|
||||
else:
|
||||
self._unify(
|
||||
node.type,
|
||||
builtins.TArray(array_type["elt"], remaining_dims))
|
||||
else:
|
||||
self._unify_iterable(element=node, collection=node.value)
|
||||
elif isinstance(node.slice, ast.Slice):
|
||||
self._unify(node.type, node.value.type,
|
||||
node.loc, node.value.loc)
|
||||
self._unify(node.type, node.value.type, node.loc, node.value.loc)
|
||||
else: # ExtSlice
|
||||
pass # error emitted above
|
||||
|
||||
|
@ -265,10 +302,21 @@ class Inferencer(algorithm.Visitor):
|
|||
node.operand.loc)
|
||||
self.engine.process(diag)
|
||||
else: # UAdd, USub
|
||||
if types.is_var(operand_type):
|
||||
return
|
||||
|
||||
if builtins.is_numeric(operand_type):
|
||||
self._unify(node.type, operand_type,
|
||||
node.loc, None)
|
||||
elif not types.is_var(operand_type):
|
||||
self._unify(node.type, operand_type, node.loc, None)
|
||||
return
|
||||
|
||||
if builtins.is_array(operand_type):
|
||||
elt = operand_type.find()["elt"]
|
||||
if builtins.is_numeric(elt):
|
||||
self._unify(node.type, operand_type, node.loc, None)
|
||||
return
|
||||
if types.is_var(elt):
|
||||
return
|
||||
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"expected unary '{op}' operand to be of numeric type, not {type}",
|
||||
{"op": node.op.loc.source(),
|
||||
|
@ -280,6 +328,10 @@ class Inferencer(algorithm.Visitor):
|
|||
self.generic_visit(node)
|
||||
if builtins.is_numeric(node.type) and builtins.is_numeric(node.value.type):
|
||||
pass
|
||||
elif (builtins.is_array(node.type) and builtins.is_array(node.value.type)
|
||||
and builtins.is_numeric(node.type.find()["elt"])
|
||||
and builtins.is_numeric(node.value.type.find()["elt"])):
|
||||
pass
|
||||
else:
|
||||
printer = types.TypePrinter()
|
||||
note = diagnostic.Diagnostic("note",
|
||||
|
@ -305,14 +357,23 @@ class Inferencer(algorithm.Visitor):
|
|||
self.visit(node)
|
||||
return node
|
||||
|
||||
def _coerce_numeric(self, nodes, map_return=lambda typ: typ):
|
||||
def _coerce_numeric(self, nodes, map_return=lambda typ: typ, map_node_type =lambda typ:typ):
|
||||
# See https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex.
|
||||
node_types = []
|
||||
for node in nodes:
|
||||
if isinstance(node, asttyped.CoerceT):
|
||||
# If we already know exactly what we coerce this value to, use that type,
|
||||
# or we'll get an unification error in case the coerced type is not the same
|
||||
# as the type of the coerced value.
|
||||
# Otherwise, use the potentially more specific subtype when considering possible
|
||||
# coercions, or we may get stuck.
|
||||
if node.type.fold(False, lambda acc, ty: acc or types.is_var(ty)):
|
||||
node_types.append(node.value.type)
|
||||
else:
|
||||
node_types.append(node.type)
|
||||
else:
|
||||
node_types.append(node.type)
|
||||
node_types = [map_node_type(typ) for typ in node_types]
|
||||
if any(map(types.is_var, node_types)): # not enough info yet
|
||||
return
|
||||
elif not all(map(builtins.is_numeric, node_types)):
|
||||
|
@ -344,8 +405,125 @@ class Inferencer(algorithm.Visitor):
|
|||
else:
|
||||
assert False
|
||||
|
||||
def _coerce_binary_broadcast_op(self, left, right, map_return_elt, op_loc):
|
||||
def num_dims(typ):
|
||||
if builtins.is_array(typ):
|
||||
# TODO: If number of dimensions is ever made a non-fixed parameter,
|
||||
# need to acutally unify num_dims in _coerce_binop/….
|
||||
return typ.find()["num_dims"].value
|
||||
return 0
|
||||
|
||||
left_dims = num_dims(left.type)
|
||||
right_dims = num_dims(right.type)
|
||||
if left_dims != right_dims and left_dims != 0 and right_dims != 0:
|
||||
# Mismatch (only scalar broadcast supported for now).
|
||||
note1 = diagnostic.Diagnostic("note", "operand of dimension {num_dims}",
|
||||
{"num_dims": left_dims}, left.loc)
|
||||
note2 = diagnostic.Diagnostic("note", "operand of dimension {num_dims}",
|
||||
{"num_dims": right_dims}, right.loc)
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error", "dimensions of '{op}' array operands must match",
|
||||
{"op": op_loc.source()}, op_loc, [left.loc, right.loc], [note1, note2])
|
||||
self.engine.process(diag)
|
||||
return
|
||||
|
||||
def map_node_type(typ):
|
||||
if not builtins.is_array(typ):
|
||||
# This is a single value broadcast across the array.
|
||||
return typ
|
||||
return typ.find()["elt"]
|
||||
|
||||
# Figure out result type, handling broadcasts.
|
||||
result_dims = left_dims if left_dims else right_dims
|
||||
def map_return(typ):
|
||||
elt = map_return_elt(typ)
|
||||
result = builtins.TArray(elt=elt, num_dims=result_dims)
|
||||
left = builtins.TArray(elt=elt, num_dims=left_dims) if left_dims else elt
|
||||
right = builtins.TArray(elt=elt, num_dims=right_dims) if right_dims else elt
|
||||
return (result, left, right)
|
||||
|
||||
return self._coerce_numeric((left, right),
|
||||
map_return=map_return,
|
||||
map_node_type=map_node_type)
|
||||
|
||||
def _coerce_binop(self, op, left, right):
|
||||
if isinstance(op, (ast.BitAnd, ast.BitOr, ast.BitXor,
|
||||
if isinstance(op, ast.MatMult):
|
||||
if types.is_var(left.type) or types.is_var(right.type):
|
||||
return
|
||||
|
||||
def num_dims(operand):
|
||||
if not builtins.is_array(operand.type):
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error",
|
||||
"expected matrix multiplication operand to be of array type, not {type}",
|
||||
{
|
||||
"op": op.loc.source(),
|
||||
"type": types.TypePrinter().name(operand.type)
|
||||
}, op.loc, [operand.loc])
|
||||
self.engine.process(diag)
|
||||
return
|
||||
num_dims = operand.type.find()["num_dims"].value
|
||||
if num_dims not in (1, 2):
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error",
|
||||
"expected matrix multiplication operand to be 1- or 2-dimensional, not {type}",
|
||||
{
|
||||
"op": op.loc.source(),
|
||||
"type": types.TypePrinter().name(operand.type)
|
||||
}, op.loc, [operand.loc])
|
||||
self.engine.process(diag)
|
||||
return
|
||||
return num_dims
|
||||
|
||||
left_dims = num_dims(left)
|
||||
if not left_dims:
|
||||
return
|
||||
right_dims = num_dims(right)
|
||||
if not right_dims:
|
||||
return
|
||||
|
||||
def map_node_type(typ):
|
||||
return typ.find()["elt"]
|
||||
|
||||
def map_return(typ):
|
||||
if left_dims == 1:
|
||||
if right_dims == 1:
|
||||
result_dims = 0
|
||||
else:
|
||||
result_dims = 1
|
||||
elif right_dims == 1:
|
||||
result_dims = 1
|
||||
else:
|
||||
result_dims = 2
|
||||
result = typ if result_dims == 0 else builtins.TArray(
|
||||
typ, result_dims)
|
||||
return (result, builtins.TArray(typ, left_dims),
|
||||
builtins.TArray(typ, right_dims))
|
||||
|
||||
return self._coerce_numeric((left, right),
|
||||
map_return=map_return,
|
||||
map_node_type=map_node_type)
|
||||
elif builtins.is_array(left.type) or builtins.is_array(right.type):
|
||||
# Operations on arrays are element-wise (possibly using broadcasting).
|
||||
|
||||
# TODO: Allow only for integer arrays.
|
||||
# allowed_int_array_ops = (ast.BitAnd, ast.BitOr, ast.BitXor, ast.LShift,
|
||||
# ast.RShift)
|
||||
allowed_array_ops = (ast.Add, ast.Mult, ast.FloorDiv, ast.Mod,
|
||||
ast.Pow, ast.Sub, ast.Div)
|
||||
if not isinstance(op, allowed_array_ops):
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error", "operator '{op}' not valid for array types",
|
||||
{"op": op.loc.source()}, op.loc)
|
||||
self.engine.process(diag)
|
||||
return
|
||||
|
||||
def map_result(typ):
|
||||
if isinstance(op, ast.Div):
|
||||
return builtins.TFloat()
|
||||
return typ
|
||||
return self._coerce_binary_broadcast_op(left, right, map_result, op.loc)
|
||||
elif isinstance(op, (ast.BitAnd, ast.BitOr, ast.BitXor,
|
||||
ast.LShift, ast.RShift)):
|
||||
# bitwise operators require integers
|
||||
for operand in (left, right):
|
||||
|
@ -444,7 +622,7 @@ class Inferencer(algorithm.Visitor):
|
|||
# division always returns a float
|
||||
return self._coerce_numeric((left, right),
|
||||
lambda typ: (builtins.TFloat(), builtins.TFloat(), builtins.TFloat()))
|
||||
else: # MatMult
|
||||
else:
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"operator '{op}' is not supported", {"op": op.loc.source()},
|
||||
op.loc)
|
||||
|
@ -682,25 +860,96 @@ class Inferencer(algorithm.Visitor):
|
|||
pass
|
||||
else:
|
||||
diagnose(valid_forms())
|
||||
elif types.is_builtin(typ, "list") or types.is_builtin(typ, "array"):
|
||||
if types.is_builtin(typ, "list"):
|
||||
elif types.is_builtin(typ, "str"):
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"strings currently cannot be constructed", {},
|
||||
node.loc)
|
||||
self.engine.process(diag)
|
||||
elif types.is_builtin(typ, "array"):
|
||||
valid_forms = lambda: [
|
||||
valid_form("array(x:'a) -> array(elt='b) where 'a is iterable"),
|
||||
valid_form("array(x:'a, dtype:'b) -> array(elt='b) where 'a is iterable")
|
||||
]
|
||||
|
||||
explicit_dtype = None
|
||||
keywords_acceptable = False
|
||||
if len(node.keywords) == 0:
|
||||
keywords_acceptable = True
|
||||
elif len(node.keywords) == 1:
|
||||
if node.keywords[0].arg == "dtype":
|
||||
keywords_acceptable = True
|
||||
explicit_dtype = node.keywords[0].value
|
||||
if len(node.args) == 1 and keywords_acceptable:
|
||||
arg, = node.args
|
||||
|
||||
# In the absence of any other information (there currently isn't a way
|
||||
# to specify any), assume that all iterables are expandable into a
|
||||
# (runtime-checked) rectangular array of the innermost element type.
|
||||
elt = arg.type
|
||||
num_dims = 0
|
||||
result_dims = (node.type.find()["num_dims"].value
|
||||
if builtins.is_array(node.type) else -1)
|
||||
while True:
|
||||
if num_dims == result_dims:
|
||||
# If we already know the number of dimensions of the result,
|
||||
# stop so we can disambiguate the (innermost) element type of
|
||||
# the argument if it is still unknown (e.g. empty array).
|
||||
break
|
||||
if types.is_var(elt):
|
||||
return # undetermined yet
|
||||
if not builtins.is_iterable(elt) or builtins.is_str(elt):
|
||||
break
|
||||
if builtins.is_array(elt):
|
||||
num_dims += elt.find()["num_dims"].value
|
||||
else:
|
||||
num_dims += 1
|
||||
elt = builtins.get_iterable_elt(elt)
|
||||
|
||||
if explicit_dtype is not None:
|
||||
# TODO: Factor out type detection; support quoted type constructors
|
||||
# (TList(TInt32), …)?
|
||||
typ = explicit_dtype.type
|
||||
if types.is_builtin(typ, "int32"):
|
||||
elt = builtins.TInt32()
|
||||
elif types.is_builtin(typ, "int64"):
|
||||
elt = builtins.TInt64()
|
||||
elif types.is_constructor(typ):
|
||||
elt = typ.find().instance
|
||||
else:
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error",
|
||||
"dtype argument of {builtin}() must be a valid constructor",
|
||||
{"builtin": typ.find().name},
|
||||
node.func.loc,
|
||||
notes=[note])
|
||||
self.engine.process(diag)
|
||||
return
|
||||
|
||||
if num_dims == 0:
|
||||
note = diagnostic.Diagnostic(
|
||||
"note", "this expression has type {type}",
|
||||
{"type": types.TypePrinter().name(arg.type)}, arg.loc)
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error",
|
||||
"the argument of {builtin}() must be of an iterable type",
|
||||
{"builtin": typ.find().name},
|
||||
node.func.loc,
|
||||
notes=[note])
|
||||
self.engine.process(diag)
|
||||
return
|
||||
|
||||
self._unify(node.type,
|
||||
builtins.TArray(elt, types.TValue(num_dims)),
|
||||
node.loc, arg.loc)
|
||||
else:
|
||||
diagnose(valid_forms())
|
||||
elif types.is_builtin(typ, "list"):
|
||||
valid_forms = lambda: [
|
||||
valid_form("list() -> list(elt='a)"),
|
||||
valid_form("list(x:'a) -> list(elt='b) where 'a is iterable")
|
||||
]
|
||||
|
||||
self._unify(node.type, builtins.TList(),
|
||||
node.loc, None)
|
||||
elif types.is_builtin(typ, "array"):
|
||||
valid_forms = lambda: [
|
||||
valid_form("array() -> array(elt='a)"),
|
||||
valid_form("array(x:'a) -> array(elt='b) where 'a is iterable")
|
||||
]
|
||||
|
||||
self._unify(node.type, builtins.TArray(),
|
||||
node.loc, None)
|
||||
else:
|
||||
assert False
|
||||
self._unify(node.type, builtins.TList(), node.loc, None)
|
||||
|
||||
if len(node.args) == 0 and len(node.keywords) == 0:
|
||||
pass # []
|
||||
|
@ -798,6 +1047,28 @@ class Inferencer(algorithm.Visitor):
|
|||
arg.loc, None)
|
||||
else:
|
||||
diagnose(valid_forms())
|
||||
elif types.is_builtin(typ, "abs"):
|
||||
fn = typ.name
|
||||
|
||||
valid_forms = lambda: [
|
||||
valid_form("abs(x:numpy.int?) -> numpy.int?"),
|
||||
valid_form("abs(x:float) -> float")
|
||||
]
|
||||
|
||||
if len(node.args) == 1 and len(node.keywords) == 0:
|
||||
(arg,) = node.args
|
||||
if builtins.is_int(arg.type) or builtins.is_float(arg.type):
|
||||
self._unify(arg.type, node.type,
|
||||
arg.loc, node.loc)
|
||||
elif types.is_var(arg.type):
|
||||
pass # undetermined yet
|
||||
else:
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"the arguments of abs() must be of a numeric type", {},
|
||||
node.func.loc)
|
||||
self.engine.process(diag)
|
||||
else:
|
||||
diagnose(valid_forms())
|
||||
elif types.is_builtin(typ, "min") or types.is_builtin(typ, "max"):
|
||||
fn = typ.name
|
||||
|
||||
|
@ -844,21 +1115,69 @@ class Inferencer(algorithm.Visitor):
|
|||
diagnose(valid_forms())
|
||||
elif types.is_builtin(typ, "make_array"):
|
||||
valid_forms = lambda: [
|
||||
valid_form("numpy.full(count:int32, value:'a) -> numpy.array(elt='a)")
|
||||
valid_form("numpy.full(count:int32, value:'a) -> array(elt='a, num_dims=1)"),
|
||||
valid_form("numpy.full(shape:(int32,)*'b, value:'a) -> array(elt='a, num_dims='b)"),
|
||||
]
|
||||
|
||||
self._unify(node.type, builtins.TArray(),
|
||||
node.loc, None)
|
||||
|
||||
if len(node.args) == 2 and len(node.keywords) == 0:
|
||||
arg0, arg1 = node.args
|
||||
|
||||
if types.is_var(arg0.type):
|
||||
return # undetermined yet
|
||||
elif types.is_tuple(arg0.type):
|
||||
num_dims = len(arg0.type.find().elts)
|
||||
self._unify(arg0.type, types.TTuple([builtins.TInt32()] * num_dims),
|
||||
arg0.loc, None)
|
||||
else:
|
||||
num_dims = 1
|
||||
self._unify(arg0.type, builtins.TInt32(),
|
||||
arg0.loc, None)
|
||||
|
||||
self._unify(node.type, builtins.TArray(num_dims=num_dims),
|
||||
node.loc, None)
|
||||
self._unify(arg1.type, node.type.find()["elt"],
|
||||
arg1.loc, None)
|
||||
else:
|
||||
diagnose(valid_forms())
|
||||
elif types.is_builtin(typ, "numpy.transpose"):
|
||||
valid_forms = lambda: [
|
||||
valid_form("transpose(x: array(elt='a, num_dims=1)) -> array(elt='a, num_dims=1)"),
|
||||
valid_form("transpose(x: array(elt='a, num_dims=2)) -> array(elt='a, num_dims=2)")
|
||||
]
|
||||
|
||||
if len(node.args) == 1 and len(node.keywords) == 0:
|
||||
arg, = node.args
|
||||
|
||||
if types.is_var(arg.type):
|
||||
pass # undetermined yet
|
||||
elif not builtins.is_array(arg.type):
|
||||
note = diagnostic.Diagnostic(
|
||||
"note", "this expression has type {type}",
|
||||
{"type": types.TypePrinter().name(arg.type)}, arg.loc)
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error",
|
||||
"the argument of {builtin}() must be an array",
|
||||
{"builtin": typ.find().name},
|
||||
node.func.loc,
|
||||
notes=[note])
|
||||
self.engine.process(diag)
|
||||
else:
|
||||
num_dims = arg.type.find()["num_dims"].value
|
||||
if num_dims not in (1, 2):
|
||||
note = diagnostic.Diagnostic(
|
||||
"note", "argument is {num_dims}-dimensional",
|
||||
{"num_dims": num_dims}, arg.loc)
|
||||
diag = diagnostic.Diagnostic(
|
||||
"error",
|
||||
"{builtin}() is currently only supported for up to "
|
||||
"two-dimensional arrays", {"builtin": typ.find().name},
|
||||
node.func.loc,
|
||||
notes=[note])
|
||||
self.engine.process(diag)
|
||||
else:
|
||||
self._unify(node.type, arg.type, node.loc, None)
|
||||
else:
|
||||
diagnose(valid_forms())
|
||||
elif types.is_builtin(typ, "rtio_log"):
|
||||
valid_forms = lambda: [
|
||||
valid_form("rtio_log(channel:str, args...) -> None"),
|
||||
|
@ -892,9 +1211,6 @@ class Inferencer(algorithm.Visitor):
|
|||
elif types.is_builtin(typ, "at_mu"):
|
||||
simple_form("at_mu(time_mu:numpy.int64) -> None",
|
||||
[builtins.TInt64()])
|
||||
elif types.is_builtin(typ, "watchdog"):
|
||||
simple_form("watchdog(time:float) -> [builtin context manager]",
|
||||
[builtins.TFloat()], builtins.TNone())
|
||||
elif types.is_constructor(typ):
|
||||
# An user-defined class.
|
||||
self._unify(node.type, typ.find().instance,
|
||||
|
@ -978,6 +1294,27 @@ class Inferencer(algorithm.Visitor):
|
|||
self.engine.process(diag)
|
||||
return
|
||||
|
||||
# Array broadcasting for functions explicitly marked as such.
|
||||
if len(node.args) == typ_arity and types.is_broadcast_across_arrays(typ):
|
||||
if typ_arity == 1:
|
||||
arg_type = node.args[0].type.find()
|
||||
if builtins.is_array(arg_type):
|
||||
typ_arg, = typ_args.values()
|
||||
self._unify(typ_arg, arg_type["elt"], node.args[0].loc, None)
|
||||
self._unify(node.type, builtins.TArray(typ_ret, arg_type["num_dims"]),
|
||||
node.loc, None)
|
||||
return
|
||||
elif typ_arity == 2:
|
||||
if any(builtins.is_array(arg.type) for arg in node.args):
|
||||
ret, arg0, arg1 = self._coerce_binary_broadcast_op(
|
||||
node.args[0], node.args[1], lambda t: typ_ret, node.loc)
|
||||
node.args[0] = self._coerce_one(arg0, node.args[0],
|
||||
other_node=node.args[1])
|
||||
node.args[1] = self._coerce_one(arg1, node.args[1],
|
||||
other_node=node.args[0])
|
||||
self._unify(node.type, ret, node.loc, None)
|
||||
return
|
||||
|
||||
for actualarg, (formalname, formaltyp) in \
|
||||
zip(node.args, list(typ_args.items()) + list(typ_optargs.items())):
|
||||
self._unify(actualarg.type, formaltyp,
|
||||
|
@ -999,6 +1336,17 @@ class Inferencer(algorithm.Visitor):
|
|||
elif keyword.arg in typ_optargs:
|
||||
self._unify(keyword.value.type, typ_optargs[keyword.arg],
|
||||
keyword.value.loc, None)
|
||||
else:
|
||||
note = diagnostic.Diagnostic("note",
|
||||
"extraneous argument", {},
|
||||
keyword.loc)
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"this function of type {type} does not accept argument '{name}'",
|
||||
{"type": types.TypePrinter().name(node.func.type),
|
||||
"name": keyword.arg},
|
||||
node.func.loc, [], [note])
|
||||
self.engine.process(diag)
|
||||
return
|
||||
passed_args[keyword.arg] = keyword.arg_loc
|
||||
|
||||
for formalname in typ_args:
|
||||
|
@ -1107,9 +1455,7 @@ class Inferencer(algorithm.Visitor):
|
|||
|
||||
typ = node.context_expr.type
|
||||
if (types.is_builtin(typ, "interleave") or types.is_builtin(typ, "sequential") or
|
||||
types.is_builtin(typ, "parallel") or
|
||||
(isinstance(node.context_expr, asttyped.CallT) and
|
||||
types.is_builtin(node.context_expr.func.type, "watchdog"))):
|
||||
types.is_builtin(typ, "parallel")):
|
||||
# builtin context managers
|
||||
if node.optional_vars is not None:
|
||||
self._unify(node.optional_vars.type, builtins.TNone(),
|
||||
|
|
|
@ -26,22 +26,3 @@ class IntMonomorphizer(algorithm.Visitor):
|
|||
return
|
||||
|
||||
node.type["width"].unify(types.TValue(width))
|
||||
|
||||
def visit_CallT(self, node):
|
||||
self.generic_visit(node)
|
||||
|
||||
if types.is_builtin(node.func.type, "int") or \
|
||||
types.is_builtin(node.func.type, "round"):
|
||||
typ = node.type.find()
|
||||
if types.is_var(typ["width"]):
|
||||
typ["width"].unify(types.TValue(32))
|
||||
|
||||
def visit_CoerceT(self, node):
|
||||
if isinstance(node.value, asttyped.NumT) and \
|
||||
builtins.is_int(node.type) and \
|
||||
builtins.is_int(node.value.type) and \
|
||||
not types.is_var(node.type["width"]) and \
|
||||
types.is_var(node.value.type["width"]):
|
||||
node.value.type.unify(node.type)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
|
|
@ -9,6 +9,7 @@ from pythonparser import ast, diagnostic
|
|||
from llvmlite_artiq import ir as ll, binding as llvm
|
||||
from ...language import core as language_core
|
||||
from .. import types, builtins, ir
|
||||
from ..embedding import SpecializedFunction
|
||||
|
||||
|
||||
llvoid = ll.VoidType()
|
||||
|
@ -22,7 +23,7 @@ llptr = ll.IntType(8).as_pointer()
|
|||
llptrptr = ll.IntType(8).as_pointer().as_pointer()
|
||||
llslice = ll.LiteralStructType([llptr, lli32])
|
||||
llsliceptr = ll.LiteralStructType([llptr, lli32]).as_pointer()
|
||||
llmetadata = ll.MetaData()
|
||||
llmetadata = ll.MetaDataType()
|
||||
|
||||
|
||||
def memoize(generator):
|
||||
|
@ -123,6 +124,35 @@ class DebugInfoEmitter:
|
|||
"scope": scope
|
||||
})
|
||||
|
||||
|
||||
class ABILayoutInfo:
|
||||
"""Caches DataLayout size/alignment lookup results.
|
||||
|
||||
llvmlite's Type.get_abi_{size, alignment}() are implemented in a very
|
||||
inefficient way, in particular _get_ll_pointer_type() used to construct the
|
||||
corresponding llvm::Type is. We thus cache the results, optionally directly
|
||||
using the compiler type as a key.
|
||||
|
||||
(This is a separate class for use with @memoize.)
|
||||
"""
|
||||
|
||||
def __init__(self, lldatalayout, llcontext, llty_of_type):
|
||||
self.cache = {}
|
||||
self.lldatalayout = lldatalayout
|
||||
self.llcontext = llcontext
|
||||
self.llty_of_type = llty_of_type
|
||||
|
||||
@memoize
|
||||
def get_size_align(self, llty):
|
||||
lowered = llty._get_ll_pointer_type(self.lldatalayout, self.llcontext)
|
||||
return (self.lldatalayout.get_pointee_abi_size(lowered),
|
||||
self.lldatalayout.get_pointee_abi_alignment(lowered))
|
||||
|
||||
@memoize
|
||||
def get_size_align_for_type(self, typ):
|
||||
return self.get_size_align(self.llty_of_type(typ))
|
||||
|
||||
|
||||
class LLVMIRGenerator:
|
||||
def __init__(self, engine, module_name, target, embedding_map):
|
||||
self.engine = engine
|
||||
|
@ -133,6 +163,8 @@ class LLVMIRGenerator:
|
|||
self.llmodule.triple = target.triple
|
||||
self.llmodule.data_layout = target.data_layout
|
||||
self.lldatalayout = llvm.create_target_data(self.llmodule.data_layout)
|
||||
self.abi_layout_info = ABILayoutInfo(self.lldatalayout, self.llcontext,
|
||||
self.llty_of_type)
|
||||
self.function_flags = None
|
||||
self.llfunction = None
|
||||
self.llmap = {}
|
||||
|
@ -148,10 +180,6 @@ class LLVMIRGenerator:
|
|||
self.tbaa_tree,
|
||||
ll.Constant(lli64, 1)
|
||||
])
|
||||
self.tbaa_now = self.llmodule.add_metadata([
|
||||
ll.MetaDataString(self.llmodule, "timeline position"),
|
||||
self.tbaa_tree
|
||||
])
|
||||
|
||||
def needs_sret(self, lltyp, may_be_large=True):
|
||||
if isinstance(lltyp, ll.VoidType):
|
||||
|
@ -177,13 +205,13 @@ class LLVMIRGenerator:
|
|||
typ = typ.find()
|
||||
if types.is_tuple(typ):
|
||||
return ll.LiteralStructType([self.llty_of_type(eltty) for eltty in typ.elts])
|
||||
elif types.is_rpc(typ) or types.is_c_function(typ):
|
||||
elif types.is_rpc(typ) or types.is_external_function(typ):
|
||||
if for_return:
|
||||
return llvoid
|
||||
else:
|
||||
return ll.LiteralStructType([])
|
||||
return llunit
|
||||
elif types._is_pointer(typ):
|
||||
return llptr
|
||||
return ll.PointerType(self.llty_of_type(typ["elt"]))
|
||||
elif types.is_function(typ):
|
||||
sretarg = []
|
||||
llretty = self.llty_of_type(typ.ret, for_return=True)
|
||||
|
@ -211,13 +239,17 @@ class LLVMIRGenerator:
|
|||
if for_return:
|
||||
return llvoid
|
||||
else:
|
||||
return ll.LiteralStructType([])
|
||||
return llunit
|
||||
elif builtins.is_bool(typ):
|
||||
return lli1
|
||||
elif builtins.is_int(typ):
|
||||
return ll.IntType(builtins.get_int_width(typ))
|
||||
elif builtins.is_float(typ):
|
||||
return lldouble
|
||||
elif builtins.is_array(typ):
|
||||
llshapety = self.llty_of_type(typ.attributes["shape"])
|
||||
llbufferty = self.llty_of_type(typ.attributes["buffer"])
|
||||
return ll.LiteralStructType([llbufferty, llshapety])
|
||||
elif builtins.is_listish(typ):
|
||||
lleltty = self.llty_of_type(builtins.get_iterable_elt(typ))
|
||||
return ll.LiteralStructType([lleltty.as_pointer(), lli32])
|
||||
|
@ -270,7 +302,7 @@ class LLVMIRGenerator:
|
|||
sanitized_str = re.sub(rb"[^a-zA-Z0-9_.]", b"", as_bytes[:20]).decode('ascii')
|
||||
name = self.llmodule.get_unique_name("S.{}".format(sanitized_str))
|
||||
|
||||
llstr = self.llmodule.get_global(name)
|
||||
llstr = self.llmodule.globals.get(name)
|
||||
if llstr is None:
|
||||
llstrty = ll.ArrayType(lli8, len(as_bytes))
|
||||
llstr = ll.GlobalVariable(self.llmodule, llstrty, name)
|
||||
|
@ -306,7 +338,7 @@ class LLVMIRGenerator:
|
|||
assert False
|
||||
|
||||
def llbuiltin(self, name):
|
||||
llglobal = self.llmodule.get_global(name)
|
||||
llglobal = self.llmodule.globals.get(name)
|
||||
if llglobal is not None:
|
||||
return llglobal
|
||||
|
||||
|
@ -335,15 +367,13 @@ class LLVMIRGenerator:
|
|||
elif name == self.target.print_function:
|
||||
llty = ll.FunctionType(llvoid, [llptr], var_arg=True)
|
||||
elif name == "rtio_log":
|
||||
llty = ll.FunctionType(llvoid, [lli64, llptr], var_arg=True)
|
||||
llty = ll.FunctionType(llvoid, [llptr], var_arg=True)
|
||||
elif name == "__artiq_personality":
|
||||
llty = ll.FunctionType(lli32, [], var_arg=True)
|
||||
elif name == "__artiq_raise":
|
||||
llty = ll.FunctionType(llvoid, [self.llty_of_type(builtins.TException())])
|
||||
elif name == "__artiq_reraise":
|
||||
llty = ll.FunctionType(llvoid, [])
|
||||
elif name in "abort":
|
||||
llty = ll.FunctionType(llvoid, [])
|
||||
elif name == "memcmp":
|
||||
llty = ll.FunctionType(lli32, [llptr, llptr, lli32])
|
||||
elif name == "rpc_send":
|
||||
|
@ -352,12 +382,19 @@ class LLVMIRGenerator:
|
|||
llty = ll.FunctionType(llvoid, [lli32, llsliceptr, llptrptr])
|
||||
elif name == "rpc_recv":
|
||||
llty = ll.FunctionType(lli32, [llptr])
|
||||
|
||||
# with now-pinning
|
||||
elif name == "now":
|
||||
llty = lli64
|
||||
elif name == "watchdog_set":
|
||||
llty = ll.FunctionType(lli32, [lli64])
|
||||
elif name == "watchdog_clear":
|
||||
llty = ll.FunctionType(llvoid, [lli32])
|
||||
|
||||
# without now-pinning
|
||||
elif name == "now_mu":
|
||||
llty = ll.FunctionType(lli64, [])
|
||||
elif name == "at_mu":
|
||||
llty = ll.FunctionType(llvoid, [lli64])
|
||||
elif name == "delay_mu":
|
||||
llty = ll.FunctionType(llvoid, [lli64])
|
||||
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
@ -366,7 +403,6 @@ class LLVMIRGenerator:
|
|||
if name in ("__artiq_raise", "__artiq_reraise", "llvm.trap"):
|
||||
llglobal.attributes.add("noreturn")
|
||||
if name in ("rtio_log", "rpc_send", "rpc_send_async",
|
||||
"watchdog_set", "watchdog_clear",
|
||||
self.target.print_function):
|
||||
llglobal.attributes.add("nounwind")
|
||||
if name.find("__py_") == 0:
|
||||
|
@ -458,7 +494,7 @@ class LLVMIRGenerator:
|
|||
assert False
|
||||
|
||||
def get_function(self, typ, name):
|
||||
llfun = self.llmodule.get_global(name)
|
||||
llfun = self.llmodule.globals.get(name)
|
||||
if llfun is None:
|
||||
llfunty = self.llty_of_type(typ, bare=True)
|
||||
llfun = ll.Function(self.llmodule, llfunty, name)
|
||||
|
@ -499,7 +535,7 @@ class LLVMIRGenerator:
|
|||
llobjects = defaultdict(lambda: [])
|
||||
|
||||
for obj_id, obj_ref, obj_typ in self.embedding_map.iter_objects():
|
||||
llobject = self.llmodule.get_global("O.{}".format(obj_id))
|
||||
llobject = self.llmodule.globals.get("O.{}".format(obj_id))
|
||||
if llobject is not None:
|
||||
llobjects[obj_typ].append(llobject.bitcast(llptr))
|
||||
|
||||
|
@ -524,11 +560,10 @@ class LLVMIRGenerator:
|
|||
print(typ)
|
||||
assert False
|
||||
|
||||
if not (types.is_function(typ) or types.is_method(typ) or types.is_rpc(typ) or
|
||||
name == "__objectid__"):
|
||||
rpctag = b"Os" + self._rpc_tag(typ, error_handler=rpc_tag_error) + b":n"
|
||||
else:
|
||||
if name == "__objectid__":
|
||||
rpctag = b""
|
||||
else:
|
||||
rpctag = b"Os" + ir.rpc_tag(typ, error_handler=rpc_tag_error) + b":n"
|
||||
|
||||
llrpcattrinit = ll.Constant(llrpcattrty, [
|
||||
ll.Constant(lli32, offset),
|
||||
|
@ -552,16 +587,16 @@ class LLVMIRGenerator:
|
|||
llrpcattrs = []
|
||||
for attr in typ.attributes:
|
||||
attrtyp = typ.attributes[attr]
|
||||
size = self.llty_of_type(attrtyp). \
|
||||
get_abi_size(self.lldatalayout, context=self.llcontext)
|
||||
alignment = self.llty_of_type(attrtyp). \
|
||||
get_abi_alignment(self.lldatalayout, context=self.llcontext)
|
||||
size, alignment = self.abi_layout_info.get_size_align_for_type(attrtyp)
|
||||
|
||||
if offset % alignment != 0:
|
||||
offset += alignment - (offset % alignment)
|
||||
|
||||
if types.is_instance(typ) and attr not in typ.constant_attributes:
|
||||
try:
|
||||
llrpcattrs.append(llrpcattr_of_attr(offset, attr, attrtyp))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
offset += size
|
||||
|
||||
|
@ -695,15 +730,19 @@ class LLVMIRGenerator:
|
|||
name=insn.name)
|
||||
else:
|
||||
assert False
|
||||
elif builtins.is_listish(insn.type):
|
||||
elif types._is_pointer(insn.type) or (builtins.is_listish(insn.type)
|
||||
and not builtins.is_array(insn.type)):
|
||||
llsize = self.map(insn.operands[0])
|
||||
lleltty = self.llty_of_type(builtins.get_iterable_elt(insn.type))
|
||||
llalloc = self.llbuilder.alloca(lleltty, size=llsize)
|
||||
if types._is_pointer(insn.type):
|
||||
return llalloc
|
||||
llvalue = ll.Constant(self.llty_of_type(insn.type), ll.Undefined)
|
||||
llvalue = self.llbuilder.insert_value(llvalue, llalloc, 0, name=insn.name)
|
||||
llvalue = self.llbuilder.insert_value(llvalue, llsize, 1)
|
||||
return llvalue
|
||||
elif not builtins.is_allocated(insn.type) or ir.is_keyword(insn.type):
|
||||
elif (not builtins.is_allocated(insn.type) or ir.is_keyword(insn.type)
|
||||
or builtins.is_array(insn.type)):
|
||||
llvalue = ll.Constant(self.llty_of_type(insn.type), ll.Undefined)
|
||||
for index, elt in enumerate(insn.operands):
|
||||
llvalue = self.llbuilder.insert_value(llvalue, self.map(elt), index)
|
||||
|
@ -720,9 +759,8 @@ class LLVMIRGenerator:
|
|||
self.llbuilder.store(lloperand, llfieldptr)
|
||||
return llalloc
|
||||
|
||||
def llptr_to_var(self, llenv, env_ty, var_name, var_type=None):
|
||||
if var_name in env_ty.params and (var_type is None or
|
||||
env_ty.params[var_name] == var_type):
|
||||
def llptr_to_var(self, llenv, env_ty, var_name):
|
||||
if var_name in env_ty.params:
|
||||
var_index = list(env_ty.params.keys()).index(var_name)
|
||||
return self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(var_index)],
|
||||
inbounds=True)
|
||||
|
@ -731,15 +769,15 @@ class LLVMIRGenerator:
|
|||
llptr = self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(outer_index)],
|
||||
inbounds=True)
|
||||
llouterenv = self.llbuilder.load(llptr)
|
||||
llouterenv.set_metadata('unconditionally.invariant.load', self.empty_metadata)
|
||||
llouterenv.set_metadata('invariant.load', self.empty_metadata)
|
||||
llouterenv.set_metadata('nonnull', self.empty_metadata)
|
||||
return self.llptr_to_var(llouterenv, env_ty.params["$outer"], var_name)
|
||||
|
||||
def mark_dereferenceable(self, load):
|
||||
assert isinstance(load, ll.LoadInstr) and isinstance(load.type, ll.PointerType)
|
||||
pointee_size = load.type.pointee.get_abi_size(self.lldatalayout, context=self.llcontext)
|
||||
pointee_size, _ = self.abi_layout_info.get_size_align(load.type.pointee)
|
||||
metadata = self.llmodule.add_metadata([ll.Constant(lli64, pointee_size)])
|
||||
load.set_metadata('unconditionally_dereferenceable', metadata)
|
||||
load.set_metadata('dereferenceable', metadata)
|
||||
|
||||
def process_GetLocal(self, insn):
|
||||
env = insn.environment()
|
||||
|
@ -793,7 +831,7 @@ class LLVMIRGenerator:
|
|||
closure_type = typ.attributes[attr]
|
||||
assert types.is_constructor(typ)
|
||||
assert types.is_function(closure_type) or types.is_rpc(closure_type)
|
||||
if types.is_c_function(closure_type) or types.is_rpc(closure_type):
|
||||
if types.is_external_function(closure_type) or types.is_rpc(closure_type):
|
||||
return None
|
||||
|
||||
llty = self.llty_of_type(typ.attributes[attr])
|
||||
|
@ -839,7 +877,7 @@ class LLVMIRGenerator:
|
|||
if types.is_tuple(typ):
|
||||
return self.llbuilder.extract_value(self.map(insn.object()), attr,
|
||||
name=insn.name)
|
||||
elif not builtins.is_allocated(typ):
|
||||
elif builtins.is_array(typ) or not builtins.is_allocated(typ):
|
||||
return self.llbuilder.extract_value(self.map(insn.object()),
|
||||
self.attr_index(typ, attr),
|
||||
name=insn.name)
|
||||
|
@ -875,7 +913,7 @@ class LLVMIRGenerator:
|
|||
inbounds=True, name="ptr.{}".format(insn.name))
|
||||
llvalue = self.llbuilder.load(llptr, name="val.{}".format(insn.name))
|
||||
if types.is_instance(typ) and attr in typ.constant_attributes:
|
||||
llvalue.set_metadata('unconditionally.invariant.load', self.empty_metadata)
|
||||
llvalue.set_metadata('invariant.load', self.empty_metadata)
|
||||
if isinstance(llvalue.type, ll.PointerType):
|
||||
self.mark_dereferenceable(llvalue)
|
||||
return llvalue
|
||||
|
@ -902,20 +940,28 @@ class LLVMIRGenerator:
|
|||
inbounds=True, name=insn.name)
|
||||
return self.llbuilder.store(llvalue, llptr)
|
||||
|
||||
def process_GetElem(self, insn):
|
||||
lst, idx = insn.list(), insn.index()
|
||||
lllst, llidx = map(self.map, (lst, idx))
|
||||
llelts = self.llbuilder.extract_value(lllst, 0)
|
||||
def process_Offset(self, insn):
|
||||
base, idx = insn.base(), insn.index()
|
||||
llelts, llidx = map(self.map, (base, idx))
|
||||
if not types._is_pointer(base.type):
|
||||
# This is list-ish.
|
||||
llelts = self.llbuilder.extract_value(llelts, 0)
|
||||
llelt = self.llbuilder.gep(llelts, [llidx], inbounds=True)
|
||||
return llelt
|
||||
|
||||
def process_GetElem(self, insn):
|
||||
llelt = self.process_Offset(insn)
|
||||
llvalue = self.llbuilder.load(llelt)
|
||||
if isinstance(llvalue.type, ll.PointerType):
|
||||
self.mark_dereferenceable(llvalue)
|
||||
return llvalue
|
||||
|
||||
def process_SetElem(self, insn):
|
||||
lst, idx = insn.list(), insn.index()
|
||||
lllst, llidx = map(self.map, (lst, idx))
|
||||
llelts = self.llbuilder.extract_value(lllst, 0)
|
||||
base, idx = insn.base(), insn.index()
|
||||
llelts, llidx = map(self.map, (base, idx))
|
||||
if not types._is_pointer(base.type):
|
||||
# This is list-ish.
|
||||
llelts = self.llbuilder.extract_value(llelts, 0)
|
||||
llelt = self.llbuilder.gep(llelts, [llidx], inbounds=True)
|
||||
return self.llbuilder.store(self.map(insn.value()), llelt)
|
||||
|
||||
|
@ -1090,8 +1136,6 @@ class LLVMIRGenerator:
|
|||
def process_Builtin(self, insn):
|
||||
if insn.op == "nop":
|
||||
return self.llbuilder.call(self.llbuiltin("llvm.donothing"), [])
|
||||
if insn.op == "abort":
|
||||
return self.llbuilder.call(self.llbuiltin("abort"), [])
|
||||
elif insn.op == "is_some":
|
||||
lloptarg = self.map(insn.operands[0])
|
||||
return self.llbuilder.extract_value(lloptarg, 0,
|
||||
|
@ -1118,7 +1162,7 @@ class LLVMIRGenerator:
|
|||
llptr = self.llbuilder.gep(llenv, [self.llindex(0), self.llindex(outer_index)],
|
||||
inbounds=True)
|
||||
llouterenv = self.llbuilder.load(llptr)
|
||||
llouterenv.set_metadata('unconditionally.invariant.load', self.empty_metadata)
|
||||
llouterenv.set_metadata('invariant.load', self.empty_metadata)
|
||||
llouterenv.set_metadata('nonnull', self.empty_metadata)
|
||||
return self.llptr_to_var(llouterenv, env_ty.params["$outer"], var_name)
|
||||
else:
|
||||
|
@ -1128,13 +1172,18 @@ class LLVMIRGenerator:
|
|||
return get_outer(self.map(env), env.type)
|
||||
elif insn.op == "len":
|
||||
collection, = insn.operands
|
||||
if builtins.is_array(collection.type):
|
||||
# Return length of outermost dimension.
|
||||
shape = self.llbuilder.extract_value(self.map(collection),
|
||||
self.attr_index(collection.type, "shape"))
|
||||
return self.llbuilder.extract_value(shape, 0)
|
||||
return self.llbuilder.extract_value(self.map(collection), 1)
|
||||
elif insn.op in ("printf", "rtio_log"):
|
||||
# We only get integers, floats, pointers and strings here.
|
||||
lloperands = []
|
||||
for i, operand in enumerate(insn.operands):
|
||||
lloperand = self.map(operand)
|
||||
if i == 0 and insn.op == "printf" or i == 1 and insn.op == "rtio_log":
|
||||
if i == 0 and (insn.op == "printf" or insn.op == "rtio_log"):
|
||||
lloperands.append(self.llbuilder.extract_value(lloperand, 0))
|
||||
elif builtins.is_str(operand.type) or builtins.is_bytes(operand.type):
|
||||
lloperands.append(self.llbuilder.extract_value(lloperand, 1))
|
||||
|
@ -1148,27 +1197,43 @@ class LLVMIRGenerator:
|
|||
# This is an identity cast at LLVM IR level.
|
||||
return self.map(insn.operands[0])
|
||||
elif insn.op == "now_mu":
|
||||
llnow = self.llbuilder.load(self.llbuiltin("now"), name=insn.name)
|
||||
llnow.set_metadata("tbaa", self.tbaa_now)
|
||||
return llnow
|
||||
if self.target.now_pinning:
|
||||
return self.llbuilder.load(self.llbuiltin("now"), name=insn.name)
|
||||
else:
|
||||
return self.llbuilder.call(self.llbuiltin("now_mu"), [])
|
||||
elif insn.op == "at_mu":
|
||||
time, = insn.operands
|
||||
return self.llbuilder.store(self.map(time), self.llbuiltin("now"))
|
||||
lltime = self.map(time)
|
||||
if self.target.now_pinning:
|
||||
lltime_hi = self.llbuilder.trunc(self.llbuilder.lshr(lltime, ll.Constant(lli64, 32)), lli32)
|
||||
lltime_lo = self.llbuilder.trunc(lltime, lli32)
|
||||
llnow_hiptr = self.llbuilder.bitcast(self.llbuiltin("now"), lli32.as_pointer())
|
||||
llnow_loptr = self.llbuilder.gep(llnow_hiptr, [self.llindex(1)])
|
||||
if self.target.little_endian:
|
||||
lltime_hi, lltime_lo = lltime_lo, lltime_hi
|
||||
llstore_hi = self.llbuilder.store_atomic(lltime_hi, llnow_hiptr, ordering="seq_cst", align=4)
|
||||
llstore_lo = self.llbuilder.store_atomic(lltime_lo, llnow_loptr, ordering="seq_cst", align=4)
|
||||
return llstore_lo
|
||||
else:
|
||||
return self.llbuilder.call(self.llbuiltin("at_mu"), [lltime])
|
||||
elif insn.op == "delay_mu":
|
||||
interval, = insn.operands
|
||||
llinterval = self.map(interval)
|
||||
if self.target.now_pinning:
|
||||
llnowptr = self.llbuiltin("now")
|
||||
llnow = self.llbuilder.load(llnowptr, name="now.old")
|
||||
llnow.set_metadata("tbaa", self.tbaa_now)
|
||||
lladjusted = self.llbuilder.add(llnow, self.map(interval), name="now.new")
|
||||
llnowstore = self.llbuilder.store(lladjusted, llnowptr)
|
||||
llnowstore.set_metadata("tbaa", self.tbaa_now)
|
||||
return llnowstore
|
||||
elif insn.op == "watchdog_set":
|
||||
interval, = insn.operands
|
||||
return self.llbuilder.call(self.llbuiltin("watchdog_set"), [self.map(interval)])
|
||||
elif insn.op == "watchdog_clear":
|
||||
id, = insn.operands
|
||||
return self.llbuilder.call(self.llbuiltin("watchdog_clear"), [self.map(id)])
|
||||
lladjusted = self.llbuilder.add(llnow, llinterval, name="now.new")
|
||||
lladjusted_hi = self.llbuilder.trunc(self.llbuilder.lshr(lladjusted, ll.Constant(lli64, 32)), lli32)
|
||||
lladjusted_lo = self.llbuilder.trunc(lladjusted, lli32)
|
||||
llnow_hiptr = self.llbuilder.bitcast(llnowptr, lli32.as_pointer())
|
||||
llnow_loptr = self.llbuilder.gep(llnow_hiptr, [self.llindex(1)])
|
||||
if self.target.little_endian:
|
||||
lladjusted_hi, lladjusted_lo = lladjusted_lo, lladjusted_hi
|
||||
llstore_hi = self.llbuilder.store_atomic(lladjusted_hi, llnow_hiptr, ordering="seq_cst", align=4)
|
||||
llstore_lo = self.llbuilder.store_atomic(lladjusted_lo, llnow_loptr, ordering="seq_cst", align=4)
|
||||
return llstore_lo
|
||||
else:
|
||||
return self.llbuilder.call(self.llbuiltin("delay_mu"), [llinterval])
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
@ -1209,7 +1274,7 @@ class LLVMIRGenerator:
|
|||
llargs.append(llarg)
|
||||
|
||||
llfunname = insn.target_function().type.name
|
||||
llfun = self.llmodule.get_global(llfunname)
|
||||
llfun = self.llmodule.globals.get(llfunname)
|
||||
if llfun is None:
|
||||
llretty = self.llty_of_type(insn.type, for_return=True)
|
||||
if self.needs_sret(llretty):
|
||||
|
@ -1230,47 +1295,6 @@ class LLVMIRGenerator:
|
|||
|
||||
return llfun, list(llargs)
|
||||
|
||||
# See session.c:{send,receive}_rpc_value and comm_generic.py:_{send,receive}_rpc_value.
|
||||
def _rpc_tag(self, typ, error_handler):
|
||||
typ = typ.find()
|
||||
if types.is_tuple(typ):
|
||||
assert len(typ.elts) < 256
|
||||
return b"t" + bytes([len(typ.elts)]) + \
|
||||
b"".join([self._rpc_tag(elt_type, error_handler)
|
||||
for elt_type in typ.elts])
|
||||
elif builtins.is_none(typ):
|
||||
return b"n"
|
||||
elif builtins.is_bool(typ):
|
||||
return b"b"
|
||||
elif builtins.is_int(typ, types.TValue(32)):
|
||||
return b"i"
|
||||
elif builtins.is_int(typ, types.TValue(64)):
|
||||
return b"I"
|
||||
elif builtins.is_float(typ):
|
||||
return b"f"
|
||||
elif builtins.is_str(typ):
|
||||
return b"s"
|
||||
elif builtins.is_bytes(typ):
|
||||
return b"B"
|
||||
elif builtins.is_bytearray(typ):
|
||||
return b"A"
|
||||
elif builtins.is_list(typ):
|
||||
return b"l" + self._rpc_tag(builtins.get_iterable_elt(typ),
|
||||
error_handler)
|
||||
elif builtins.is_array(typ):
|
||||
return b"a" + self._rpc_tag(builtins.get_iterable_elt(typ),
|
||||
error_handler)
|
||||
elif builtins.is_range(typ):
|
||||
return b"r" + self._rpc_tag(builtins.get_iterable_elt(typ),
|
||||
error_handler)
|
||||
elif ir.is_keyword(typ):
|
||||
return b"k" + self._rpc_tag(typ.params["value"],
|
||||
error_handler)
|
||||
elif '__objectid__' in typ.attributes:
|
||||
return b"O"
|
||||
else:
|
||||
error_handler(typ)
|
||||
|
||||
def _build_rpc(self, fun_loc, fun_type, args, llnormalblock, llunwindblock):
|
||||
llservice = ll.Constant(lli32, fun_type.service)
|
||||
|
||||
|
@ -1288,7 +1312,7 @@ class LLVMIRGenerator:
|
|||
{"type": printer.name(arg.type)},
|
||||
arg.loc)
|
||||
self.engine.process(diag)
|
||||
tag += self._rpc_tag(arg.type, arg_error_handler)
|
||||
tag += ir.rpc_tag(arg.type, arg_error_handler)
|
||||
tag += b":"
|
||||
|
||||
def ret_error_handler(typ):
|
||||
|
@ -1302,7 +1326,7 @@ class LLVMIRGenerator:
|
|||
{"type": printer.name(fun_type.ret)},
|
||||
fun_loc)
|
||||
self.engine.process(diag)
|
||||
tag += self._rpc_tag(fun_type.ret, ret_error_handler)
|
||||
tag += ir.rpc_tag(fun_type.ret, ret_error_handler)
|
||||
|
||||
lltag = self.llconst_of_const(ir.Constant(tag, builtins.TStr()))
|
||||
lltagptr = self.llbuilder.alloca(lltag.type)
|
||||
|
@ -1315,7 +1339,7 @@ class LLVMIRGenerator:
|
|||
name="rpc.args")
|
||||
for index, arg in enumerate(args):
|
||||
if builtins.is_none(arg.type):
|
||||
llargslot = self.llbuilder.alloca(ll.LiteralStructType([]),
|
||||
llargslot = self.llbuilder.alloca(llunit,
|
||||
name="rpc.arg{}".format(index))
|
||||
else:
|
||||
llarg = self.map(arg)
|
||||
|
@ -1327,7 +1351,7 @@ class LLVMIRGenerator:
|
|||
llargptr = self.llbuilder.gep(llargs, [ll.Constant(lli32, index)])
|
||||
self.llbuilder.store(llargslot, llargptr)
|
||||
|
||||
if fun_type.async:
|
||||
if fun_type.is_async:
|
||||
self.llbuilder.call(self.llbuiltin("rpc_send_async"),
|
||||
[llservice, lltagptr, llargs])
|
||||
else:
|
||||
|
@ -1337,7 +1361,13 @@ class LLVMIRGenerator:
|
|||
# Don't waste stack space on saved arguments.
|
||||
self.llbuilder.call(self.llbuiltin("llvm.stackrestore"), [llstackptr])
|
||||
|
||||
if fun_type.async:
|
||||
if fun_type.is_async:
|
||||
# If this RPC is called using an `invoke` ARTIQ IR instruction, there will be
|
||||
# no other instructions in this basic block. Since this RPC is async, it cannot
|
||||
# possibly raise an exception, so add an explicit jump to the normal successor.
|
||||
if llunwindblock:
|
||||
self.llbuilder.branch(llnormalblock)
|
||||
|
||||
return ll.Undefined
|
||||
|
||||
# T result = {
|
||||
|
@ -1383,7 +1413,7 @@ class LLVMIRGenerator:
|
|||
|
||||
self.llbuilder.position_at_end(lltail)
|
||||
llret = self.llbuilder.load(llslot, name="rpc.ret")
|
||||
if not builtins.is_allocated(fun_type.ret):
|
||||
if not fun_type.ret.fold(False, lambda r, t: r or builtins.is_allocated(t)):
|
||||
# We didn't allocate anything except the slot for the value itself.
|
||||
# Don't waste stack space.
|
||||
self.llbuilder.call(self.llbuiltin("llvm.stackrestore"), [llstackptr])
|
||||
|
@ -1398,7 +1428,7 @@ class LLVMIRGenerator:
|
|||
functiontyp,
|
||||
insn.arguments(),
|
||||
llnormalblock=None, llunwindblock=None)
|
||||
elif types.is_c_function(functiontyp):
|
||||
elif types.is_external_function(functiontyp):
|
||||
llfun, llargs = self._prepare_ffi_call(insn)
|
||||
else:
|
||||
llfun, llargs = self._prepare_closure_call(insn)
|
||||
|
@ -1414,9 +1444,14 @@ class LLVMIRGenerator:
|
|||
else:
|
||||
llcall = llresult = self.llbuilder.call(llfun, llargs, name=insn.name)
|
||||
|
||||
if isinstance(llresult.type, ll.VoidType):
|
||||
# We have NoneType-returning functions return void, but None is
|
||||
# {} elsewhere.
|
||||
llresult = ll.Constant(llunit, [])
|
||||
|
||||
# Never add TBAA nowrite metadata to a functon with sret!
|
||||
# This leads to miscompilations.
|
||||
if types.is_c_function(functiontyp) and 'nowrite' in functiontyp.flags:
|
||||
if types.is_external_function(functiontyp) and 'nowrite' in functiontyp.flags:
|
||||
llcall.set_metadata('tbaa', self.tbaa_nowrite_call)
|
||||
|
||||
return llresult
|
||||
|
@ -1430,7 +1465,7 @@ class LLVMIRGenerator:
|
|||
functiontyp,
|
||||
insn.arguments(),
|
||||
llnormalblock, llunwindblock)
|
||||
elif types.is_c_function(functiontyp):
|
||||
elif types.is_external_function(functiontyp):
|
||||
llfun, llargs = self._prepare_ffi_call(insn)
|
||||
else:
|
||||
llfun, llargs = self._prepare_closure_call(insn)
|
||||
|
@ -1448,12 +1483,22 @@ class LLVMIRGenerator:
|
|||
llcall = self.llbuilder.invoke(llfun, llargs, llnormalblock, llunwindblock,
|
||||
name=insn.name)
|
||||
|
||||
# See the comment in process_Call.
|
||||
if types.is_c_function(functiontyp) and 'nowrite' in functiontyp.flags:
|
||||
llcall.set_metadata('tbaa', self.tbaa_nowrite_call)
|
||||
# The !tbaa metadata is not legal to use with the invoke instruction,
|
||||
# so unlike process_Call, we do not set it here.
|
||||
|
||||
return llcall
|
||||
|
||||
def _quote_listish_to_llglobal(self, value, elt_type, path, kind_name):
|
||||
llelts = [self._quote(value[i], elt_type, lambda: path() + [str(i)])
|
||||
for i in range(len(value))]
|
||||
lleltsary = ll.Constant(ll.ArrayType(self.llty_of_type(elt_type), len(llelts)),
|
||||
list(llelts))
|
||||
name = self.llmodule.scope.deduplicate("quoted.{}".format(kind_name))
|
||||
llglobal = ll.GlobalVariable(self.llmodule, lleltsary.type, name)
|
||||
llglobal.initializer = lleltsary
|
||||
llglobal.linkage = "private"
|
||||
return llglobal.bitcast(lleltsary.type.element.as_pointer())
|
||||
|
||||
def _quote(self, value, typ, path):
|
||||
value_id = id(value)
|
||||
if value_id in self.llobject_map:
|
||||
|
@ -1481,7 +1526,7 @@ class LLVMIRGenerator:
|
|||
attrvalue = getattr(value, attr)
|
||||
is_class_function = (types.is_constructor(typ) and
|
||||
types.is_function(typ.attributes[attr]) and
|
||||
not types.is_c_function(typ.attributes[attr]))
|
||||
not types.is_external_function(typ.attributes[attr]))
|
||||
if is_class_function:
|
||||
attrvalue = self.embedding_map.specialize_function(typ.instance, attrvalue)
|
||||
if not (types.is_instance(typ) and attr in typ.constant_attributes):
|
||||
|
@ -1530,28 +1575,39 @@ class LLVMIRGenerator:
|
|||
llstr = self.llstr_of_str(as_bytes)
|
||||
llconst = ll.Constant(llty, [llstr, ll.Constant(lli32, len(as_bytes))])
|
||||
return llconst
|
||||
elif builtins.is_array(typ):
|
||||
assert isinstance(value, numpy.ndarray), fail_msg
|
||||
typ = typ.find()
|
||||
assert len(value.shape) == typ["num_dims"].find().value
|
||||
flattened = value.reshape((-1,))
|
||||
lleltsptr = self._quote_listish_to_llglobal(flattened, typ["elt"], path, "array")
|
||||
llshape = ll.Constant.literal_struct([ll.Constant(lli32, s) for s in value.shape])
|
||||
return ll.Constant(llty, [lleltsptr, llshape])
|
||||
elif builtins.is_listish(typ):
|
||||
assert isinstance(value, (list, numpy.ndarray)), fail_msg
|
||||
elt_type = builtins.get_iterable_elt(typ)
|
||||
llelts = [self._quote(value[i], elt_type, lambda: path() + [str(i)])
|
||||
for i in range(len(value))]
|
||||
lleltsary = ll.Constant(ll.ArrayType(self.llty_of_type(elt_type), len(llelts)),
|
||||
list(llelts))
|
||||
|
||||
name = self.llmodule.scope.deduplicate("quoted.{}".format(typ.name))
|
||||
llglobal = ll.GlobalVariable(self.llmodule, lleltsary.type, name)
|
||||
llglobal.initializer = lleltsary
|
||||
llglobal.linkage = "private"
|
||||
|
||||
lleltsptr = llglobal.bitcast(lleltsary.type.element.as_pointer())
|
||||
llconst = ll.Constant(llty, [lleltsptr, ll.Constant(lli32, len(llelts))])
|
||||
lleltsptr = self._quote_listish_to_llglobal(value, elt_type, path, typ.find().name)
|
||||
llconst = ll.Constant(llty, [lleltsptr, ll.Constant(lli32, len(value))])
|
||||
return llconst
|
||||
elif types.is_rpc(typ) or types.is_c_function(typ):
|
||||
# RPC and C functions have no runtime representation.
|
||||
elif types.is_tuple(typ):
|
||||
assert isinstance(value, tuple), fail_msg
|
||||
llelts = [self._quote(v, t, lambda: path() + [str(i)])
|
||||
for i, (v, t) in enumerate(zip(value, typ.elts))]
|
||||
return ll.Constant(llty, llelts)
|
||||
elif types.is_rpc(typ) or types.is_external_function(typ) or types.is_builtin_function(typ):
|
||||
# RPC, C and builtin functions have no runtime representation.
|
||||
return ll.Constant(llty, ll.Undefined)
|
||||
elif types.is_function(typ):
|
||||
return self.get_function_with_undef_env(typ.find(),
|
||||
self.embedding_map.retrieve_function(value))
|
||||
try:
|
||||
func = self.embedding_map.retrieve_function(value)
|
||||
except KeyError:
|
||||
# If a class function was embedded directly (e.g. by a `C.f(...)` call),
|
||||
# but it also appears in a class hierarchy, we might need to fall back
|
||||
# to the non-specialized one, since direct invocations do not cause
|
||||
# monomorphization.
|
||||
assert isinstance(value, SpecializedFunction)
|
||||
func = self.embedding_map.retrieve_function(value.host_function)
|
||||
return self.get_function_with_undef_env(typ.find(), func)
|
||||
elif types.is_method(typ):
|
||||
llclosure = self._quote(value.__func__, types.get_method_function(typ),
|
||||
lambda: path() + ['__func__'])
|
||||
|
@ -1643,7 +1699,7 @@ class LLVMIRGenerator:
|
|||
|
||||
llclauseexnname = self.llconst_of_const(
|
||||
ir.Constant(exnname, builtins.TStr()))
|
||||
llclauseexnnameptr = self.llmodule.get_global("exn.{}".format(exnname))
|
||||
llclauseexnnameptr = self.llmodule.globals.get("exn.{}".format(exnname))
|
||||
if llclauseexnnameptr is None:
|
||||
llclauseexnnameptr = ll.GlobalVariable(self.llmodule, llclauseexnname.type,
|
||||
name="exn.{}".format(exnname))
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
"""
|
||||
:class:`LocalDemoter` is a constant propagation transform:
|
||||
it replaces reads of any local variable with only one write
|
||||
in a function without closures with the value that was written.
|
||||
|
||||
:class:`LocalAccessValidator` must be run before this transform
|
||||
to ensure that the transformation it performs is sound.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from .. import ir
|
||||
|
||||
class LocalDemoter:
|
||||
def process(self, functions):
|
||||
for func in functions:
|
||||
self.process_function(func)
|
||||
|
||||
def process_function(self, func):
|
||||
env_safe = {}
|
||||
env_gets = defaultdict(lambda: set())
|
||||
env_sets = defaultdict(lambda: set())
|
||||
|
||||
for insn in func.instructions():
|
||||
if isinstance(insn, (ir.GetLocal, ir.SetLocal)):
|
||||
if "$" in insn.var_name:
|
||||
continue
|
||||
|
||||
env = insn.environment()
|
||||
|
||||
if env not in env_safe:
|
||||
for use in env.uses:
|
||||
if not isinstance(use, (ir.GetLocal, ir.SetLocal)):
|
||||
env_safe[env] = False
|
||||
break
|
||||
else:
|
||||
env_safe[env] = True
|
||||
|
||||
if not env_safe[env]:
|
||||
continue
|
||||
|
||||
if isinstance(insn, ir.SetLocal):
|
||||
env_sets[(env, insn.var_name)].add(insn)
|
||||
else:
|
||||
env_gets[(env, insn.var_name)].add(insn)
|
||||
|
||||
for (env, var_name) in env_sets:
|
||||
if len(env_sets[(env, var_name)]) == 1:
|
||||
set_insn = next(iter(env_sets[(env, var_name)]))
|
||||
for get_insn in env_gets[(env, var_name)]:
|
||||
get_insn.replace_all_uses_with(set_insn.value())
|
||||
get_insn.erase()
|
|
@ -73,7 +73,7 @@ class TVar(Type):
|
|||
# path compression
|
||||
iter = self
|
||||
while iter.__class__ == TVar:
|
||||
if iter is iter.parent:
|
||||
if iter is root:
|
||||
break
|
||||
else:
|
||||
iter, iter.parent = iter.parent, root
|
||||
|
@ -81,6 +81,8 @@ class TVar(Type):
|
|||
return root
|
||||
|
||||
def unify(self, other):
|
||||
if other is self:
|
||||
return
|
||||
other = other.find()
|
||||
|
||||
if self.parent is self:
|
||||
|
@ -124,6 +126,8 @@ class TMono(Type):
|
|||
return self
|
||||
|
||||
def unify(self, other):
|
||||
if other is self:
|
||||
return
|
||||
if isinstance(other, TMono) and self.name == other.name:
|
||||
assert self.params.keys() == other.params.keys()
|
||||
for param in self.params:
|
||||
|
@ -171,6 +175,8 @@ class TTuple(Type):
|
|||
return self
|
||||
|
||||
def unify(self, other):
|
||||
if other is self:
|
||||
return
|
||||
if isinstance(other, TTuple) and len(self.elts) == len(other.elts):
|
||||
for selfelt, otherelt in zip(self.elts, other.elts):
|
||||
selfelt.unify(otherelt)
|
||||
|
@ -198,8 +204,10 @@ class TTuple(Type):
|
|||
return hash(tuple(self.elts))
|
||||
|
||||
class _TPointer(TMono):
|
||||
def __init__(self):
|
||||
super().__init__("pointer")
|
||||
def __init__(self, elt=None):
|
||||
if elt is None:
|
||||
elt = TMono("int", {"width": 8}) # i8*
|
||||
super().__init__("pointer", params={"elt": elt})
|
||||
|
||||
class TFunction(Type):
|
||||
"""
|
||||
|
@ -237,6 +245,8 @@ class TFunction(Type):
|
|||
return self
|
||||
|
||||
def unify(self, other):
|
||||
if other is self:
|
||||
return
|
||||
if isinstance(other, TFunction) and \
|
||||
self.args.keys() == other.args.keys() and \
|
||||
self.optargs.keys() == other.optargs.keys():
|
||||
|
@ -273,20 +283,29 @@ class TFunction(Type):
|
|||
def __hash__(self):
|
||||
return hash((_freeze(self.args), _freeze(self.optargs), self.ret))
|
||||
|
||||
class TCFunction(TFunction):
|
||||
class TExternalFunction(TFunction):
|
||||
"""
|
||||
A function type of a runtime-provided C function.
|
||||
A type of an externally-provided function.
|
||||
|
||||
:ivar name: (str) C function name
|
||||
:ivar flags: (set of str) C function flags.
|
||||
This can be any function following the C ABI, such as provided by the
|
||||
C/Rust runtime, or a compiler backend intrinsic. The mangled name to link
|
||||
against is encoded as part of the type.
|
||||
|
||||
:ivar name: (str) external symbol name.
|
||||
This will be the symbol linked against (following any extra C name
|
||||
mangling rules).
|
||||
:ivar flags: (set of str) function flags.
|
||||
Flag ``nounwind`` means the function never raises an exception.
|
||||
Flag ``nowrite`` means the function never writes any memory
|
||||
that the ARTIQ Python code can observe.
|
||||
:ivar broadcast_across_arrays: (bool)
|
||||
If True, the function is transparently applied element-wise when called
|
||||
with TArray arguments.
|
||||
"""
|
||||
|
||||
attributes = OrderedDict()
|
||||
|
||||
def __init__(self, args, ret, name, flags={}):
|
||||
def __init__(self, args, ret, name, flags=set(), broadcast_across_arrays=False):
|
||||
assert isinstance(flags, set)
|
||||
for flag in flags:
|
||||
assert flag in {'nounwind', 'nowrite'}
|
||||
|
@ -294,9 +313,12 @@ class TCFunction(TFunction):
|
|||
self.name = name
|
||||
self.delay = TFixedDelay(iodelay.Const(0))
|
||||
self.flags = flags
|
||||
self.broadcast_across_arrays = broadcast_across_arrays
|
||||
|
||||
def unify(self, other):
|
||||
if isinstance(other, TCFunction) and \
|
||||
if other is self:
|
||||
return
|
||||
if isinstance(other, TExternalFunction) and \
|
||||
self.name == other.name:
|
||||
super().unify(other)
|
||||
elif isinstance(other, TVar):
|
||||
|
@ -311,22 +333,24 @@ class TRPC(Type):
|
|||
:ivar ret: (:class:`Type`)
|
||||
return type
|
||||
:ivar service: (int) RPC service number
|
||||
:ivar async: (bool) whether the RPC blocks until return
|
||||
:ivar is_async: (bool) whether the RPC blocks until return
|
||||
"""
|
||||
|
||||
attributes = OrderedDict()
|
||||
|
||||
def __init__(self, ret, service, async=False):
|
||||
def __init__(self, ret, service, is_async=False):
|
||||
assert isinstance(ret, Type)
|
||||
self.ret, self.service, self.async = ret, service, async
|
||||
self.ret, self.service, self.is_async = ret, service, is_async
|
||||
|
||||
def find(self):
|
||||
return self
|
||||
|
||||
def unify(self, other):
|
||||
if other is self:
|
||||
return
|
||||
if isinstance(other, TRPC) and \
|
||||
self.service == other.service and \
|
||||
self.async == other.async:
|
||||
self.is_async == other.is_async:
|
||||
self.ret.unify(other.ret)
|
||||
elif isinstance(other, TVar):
|
||||
other.unify(self)
|
||||
|
@ -343,7 +367,7 @@ class TRPC(Type):
|
|||
def __eq__(self, other):
|
||||
return isinstance(other, TRPC) and \
|
||||
self.service == other.service and \
|
||||
self.async == other.async
|
||||
self.is_async == other.is_async
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
@ -366,6 +390,8 @@ class TBuiltin(Type):
|
|||
return self
|
||||
|
||||
def unify(self, other):
|
||||
if other is self:
|
||||
return
|
||||
if self != other:
|
||||
raise UnificationError(self, other)
|
||||
|
||||
|
@ -388,6 +414,11 @@ class TBuiltin(Type):
|
|||
class TBuiltinFunction(TBuiltin):
|
||||
"""
|
||||
A type of a builtin function.
|
||||
|
||||
Builtin functions are treated specially throughout all stages of the
|
||||
compilation process according to their name (e.g. calls may not actually
|
||||
lower to a function call). See :class:`TExternalFunction` for externally
|
||||
defined functions that are otherwise regular.
|
||||
"""
|
||||
|
||||
class TConstructor(TBuiltin):
|
||||
|
@ -471,6 +502,8 @@ class TValue(Type):
|
|||
return self
|
||||
|
||||
def unify(self, other):
|
||||
if other is self:
|
||||
return
|
||||
if isinstance(other, TVar):
|
||||
other.unify(self)
|
||||
elif self != other:
|
||||
|
@ -561,13 +594,15 @@ def is_mono(typ, name=None, **params):
|
|||
if not isinstance(typ, TMono):
|
||||
return False
|
||||
|
||||
params_match = True
|
||||
if name is not None and typ.name != name:
|
||||
return False
|
||||
|
||||
for param in params:
|
||||
if param not in typ.params:
|
||||
return False
|
||||
params_match = params_match and \
|
||||
typ.params[param].find() == params[param].find()
|
||||
return name is None or (typ.name == name and params_match)
|
||||
if typ.params[param].find() != params[param].find():
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_polymorphic(typ):
|
||||
return typ.fold(False, lambda accum, typ: accum or is_var(typ))
|
||||
|
@ -589,12 +624,12 @@ def is_function(typ):
|
|||
def is_rpc(typ):
|
||||
return isinstance(typ.find(), TRPC)
|
||||
|
||||
def is_c_function(typ, name=None):
|
||||
def is_external_function(typ, name=None):
|
||||
typ = typ.find()
|
||||
if name is None:
|
||||
return isinstance(typ, TCFunction)
|
||||
return isinstance(typ, TExternalFunction)
|
||||
else:
|
||||
return isinstance(typ, TCFunction) and \
|
||||
return isinstance(typ, TExternalFunction) and \
|
||||
typ.name == name
|
||||
|
||||
def is_builtin(typ, name=None):
|
||||
|
@ -605,6 +640,23 @@ def is_builtin(typ, name=None):
|
|||
return isinstance(typ, TBuiltin) and \
|
||||
typ.name == name
|
||||
|
||||
def is_builtin_function(typ, name=None):
|
||||
typ = typ.find()
|
||||
if name is None:
|
||||
return isinstance(typ, TBuiltinFunction)
|
||||
else:
|
||||
return isinstance(typ, TBuiltinFunction) and \
|
||||
typ.name == name
|
||||
|
||||
def is_broadcast_across_arrays(typ):
|
||||
# For now, broadcasting is only exposed to predefined external functions, and
|
||||
# statically selected. Might be extended to user-defined functions if the design
|
||||
# pans out.
|
||||
typ = typ.find()
|
||||
if not isinstance(typ, TExternalFunction):
|
||||
return False
|
||||
return typ.broadcast_across_arrays
|
||||
|
||||
def is_constructor(typ, name=None):
|
||||
typ = typ.find()
|
||||
if name is not None:
|
||||
|
@ -709,12 +761,14 @@ class TypePrinter(object):
|
|||
else:
|
||||
return "%s(%s)" % (typ.name, ", ".join(
|
||||
["%s=%s" % (k, self.name(typ.params[k], depth + 1)) for k in typ.params]))
|
||||
elif isinstance(typ, _TPointer):
|
||||
return "{}*".format(self.name(typ["elt"], depth + 1))
|
||||
elif isinstance(typ, TTuple):
|
||||
if len(typ.elts) == 1:
|
||||
return "(%s,)" % self.name(typ.elts[0], depth + 1)
|
||||
else:
|
||||
return "(%s)" % ", ".join([self.name(typ, depth + 1) for typ in typ.elts])
|
||||
elif isinstance(typ, (TFunction, TCFunction)):
|
||||
elif isinstance(typ, (TFunction, TExternalFunction)):
|
||||
args = []
|
||||
args += [ "%s:%s" % (arg, self.name(typ.args[arg], depth + 1))
|
||||
for arg in typ.args]
|
||||
|
@ -728,13 +782,13 @@ class TypePrinter(object):
|
|||
elif not (delay.is_fixed() and iodelay.is_zero(delay.duration)):
|
||||
signature += " " + self.name(delay, depth + 1)
|
||||
|
||||
if isinstance(typ, TCFunction):
|
||||
if isinstance(typ, TExternalFunction):
|
||||
return "[ffi {}]{}".format(repr(typ.name), signature)
|
||||
elif isinstance(typ, TFunction):
|
||||
return signature
|
||||
elif isinstance(typ, TRPC):
|
||||
return "[rpc{} #{}](...)->{}".format(typ.service,
|
||||
" async" if typ.async else "",
|
||||
" async" if typ.is_async else "",
|
||||
self.name(typ.ret, depth + 1))
|
||||
elif isinstance(typ, TBuiltinFunction):
|
||||
return "<function {}>".format(typ.name)
|
||||
|
|
|
@ -50,3 +50,9 @@ class ConstnessValidator(algorithm.Visitor):
|
|||
node.loc)
|
||||
self.engine.process(diag)
|
||||
return
|
||||
if builtins.is_array(typ):
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"array attributes cannot be assigned to",
|
||||
{}, node.loc)
|
||||
self.engine.process(diag)
|
||||
return
|
||||
|
|
|
@ -51,10 +51,6 @@ class Region:
|
|||
(other.range.begin_pos <= self.range.begin_pos <= other.range.end_pos and \
|
||||
self.range.end_pos > other.range.end_pos)
|
||||
|
||||
def contract(self, other):
|
||||
if not self.range:
|
||||
self.range = other.range
|
||||
|
||||
def outlives(lhs, rhs):
|
||||
if not isinstance(lhs, Region): # lhs lives nonlexically
|
||||
return True
|
||||
|
@ -69,8 +65,11 @@ class Region:
|
|||
|
||||
class RegionOf(algorithm.Visitor):
|
||||
"""
|
||||
Visit an expression and return the list of regions that must
|
||||
be alive for the expression to execute.
|
||||
Visit an expression and return the region that must be alive for the
|
||||
expression to execute.
|
||||
|
||||
For expressions involving multiple regions, the shortest-lived one is
|
||||
returned.
|
||||
"""
|
||||
|
||||
def __init__(self, env_stack, youngest_region):
|
||||
|
@ -100,7 +99,7 @@ class RegionOf(algorithm.Visitor):
|
|||
visit_BinOpT = visit_sometimes_allocating
|
||||
|
||||
def visit_CallT(self, node):
|
||||
if types.is_c_function(node.func.type, "cache_get"):
|
||||
if types.is_external_function(node.func.type, "cache_get"):
|
||||
# The cache is borrow checked dynamically
|
||||
return Global()
|
||||
else:
|
||||
|
@ -157,7 +156,7 @@ class RegionOf(algorithm.Visitor):
|
|||
visit_NameConstantT = visit_immutable
|
||||
visit_NumT = visit_immutable
|
||||
visit_EllipsisT = visit_immutable
|
||||
visit_UnaryOpT = visit_immutable
|
||||
visit_UnaryOpT = visit_sometimes_allocating # possibly array op
|
||||
visit_CompareT = visit_immutable
|
||||
|
||||
# Value lives forever
|
||||
|
@ -301,17 +300,20 @@ class EscapeValidator(algorithm.Visitor):
|
|||
def visit_assignment(self, target, value):
|
||||
value_region = self._region_of(value)
|
||||
|
||||
# If this is a variable, we might need to contract the live range.
|
||||
if isinstance(value_region, Region):
|
||||
for name in self._names_of(target):
|
||||
region = self._region_of(name)
|
||||
if isinstance(region, Region):
|
||||
region.contract(value_region)
|
||||
|
||||
# If we assign to an attribute of a quoted value, there will be no names
|
||||
# in the assignment lhs.
|
||||
target_names = self._names_of(target) or []
|
||||
|
||||
# Adopt the value region for any variables declared on the lhs.
|
||||
for name in target_names:
|
||||
region = self._region_of(name)
|
||||
if isinstance(region, Region) and not region.present():
|
||||
# Find the name's environment to overwrite the region.
|
||||
for env in self.env_stack[::-1]:
|
||||
if name.id in env:
|
||||
env[name.id] = value_region
|
||||
break
|
||||
|
||||
# The assigned value should outlive the assignee
|
||||
target_regions = [self._region_of(name) for name in target_names]
|
||||
for target_region in target_regions:
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
from artiq.coredevice import exceptions, dds, spi
|
||||
from artiq.coredevice.exceptions import (RTIOUnderflow, RTIOSequenceError, RTIOOverflow)
|
||||
from artiq.coredevice.dds import (PHASE_MODE_CONTINUOUS, PHASE_MODE_ABSOLUTE,
|
||||
PHASE_MODE_TRACKING)
|
||||
from artiq.coredevice.exceptions import (RTIOUnderflow, RTIOOverflow)
|
||||
|
||||
__all__ = []
|
||||
__all__ += ["RTIOUnderflow", "RTIOSequenceError", "RTIOOverflow"]
|
||||
__all__ += ["PHASE_MODE_CONTINUOUS", "PHASE_MODE_ABSOLUTE",
|
||||
"PHASE_MODE_TRACKING"]
|
||||
__all__ = ["RTIOUnderflow", "RTIOOverflow"]
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
"""
|
||||
Driver for the AD5360 DAC on RTIO.
|
||||
|
||||
Output event replacement is not supported and issuing commands at the same
|
||||
time is an error.
|
||||
"""
|
||||
|
||||
|
||||
from artiq.language.core import (kernel, portable, delay_mu, delay)
|
||||
from artiq.language.units import ns, us
|
||||
from artiq.coredevice import spi
|
||||
|
||||
# Designed from the data sheets and somewhat after the linux kernel
|
||||
# iio driver.
|
||||
|
||||
_AD5360_SPI_CONFIG = (0*spi.SPI_OFFLINE | 0*spi.SPI_CS_POLARITY |
|
||||
0*spi.SPI_CLK_POLARITY | 1*spi.SPI_CLK_PHASE |
|
||||
0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX)
|
||||
|
||||
_AD5360_CMD_DATA = 3 << 22
|
||||
_AD5360_CMD_OFFSET = 2 << 22
|
||||
_AD5360_CMD_GAIN = 1 << 22
|
||||
_AD5360_CMD_SPECIAL = 0 << 22
|
||||
|
||||
|
||||
@portable
|
||||
def _AD5360_WRITE_CHANNEL(c):
|
||||
return (c + 8) << 16
|
||||
|
||||
_AD5360_SPECIAL_NOP = 0 << 16
|
||||
_AD5360_SPECIAL_CONTROL = 1 << 16
|
||||
_AD5360_SPECIAL_OFS0 = 2 << 16
|
||||
_AD5360_SPECIAL_OFS1 = 3 << 16
|
||||
_AD5360_SPECIAL_READ = 5 << 16
|
||||
|
||||
|
||||
@portable
|
||||
def _AD5360_READ_CHANNEL(ch):
|
||||
return (ch + 8) << 7
|
||||
|
||||
_AD5360_READ_X1A = 0x000 << 7
|
||||
_AD5360_READ_X1B = 0x040 << 7
|
||||
_AD5360_READ_OFFSET = 0x080 << 7
|
||||
_AD5360_READ_GAIN = 0x0c0 << 7
|
||||
_AD5360_READ_CONTROL = 0x101 << 7
|
||||
_AD5360_READ_OFS0 = 0x102 << 7
|
||||
_AD5360_READ_OFS1 = 0x103 << 7
|
||||
|
||||
|
||||
class AD5360:
|
||||
"""
|
||||
Support for the Analog devices AD53[67][0123]
|
||||
multi-channel Digital to Analog Converters
|
||||
|
||||
:param spi_device: Name of the SPI bus this device is on.
|
||||
:param ldac_device: Name of the TTL device that LDAC is connected to
|
||||
(optional). Needs to be explicitly initialized to high.
|
||||
:param chip_select: Value to drive on the chip select lines
|
||||
during transactions.
|
||||
"""
|
||||
|
||||
def __init__(self, dmgr, spi_device, ldac_device=None, chip_select=1):
|
||||
self.core = dmgr.get("core")
|
||||
self.bus = dmgr.get(spi_device)
|
||||
if ldac_device is not None:
|
||||
self.ldac = dmgr.get(ldac_device)
|
||||
self.chip_select = chip_select
|
||||
|
||||
@kernel
|
||||
def setup_bus(self, write_div=4, read_div=7):
|
||||
"""Configure the SPI bus and the SPI transaction parameters
|
||||
for this device. This method has to be called before any other method
|
||||
if the bus has been used to access a different device in the meantime.
|
||||
|
||||
This method advances the timeline by the duration of two
|
||||
RTIO-to-Wishbone bus transactions.
|
||||
|
||||
:param write_div: Write clock divider.
|
||||
:param read_div: Read clock divider.
|
||||
"""
|
||||
# write: 2*8ns >= 10ns = t_6 (clk falling to cs_n rising)
|
||||
# read: 4*8*ns >= 25ns = t_22 (clk falling to miso valid)
|
||||
self.bus.set_config_mu(_AD5360_SPI_CONFIG, write_div, read_div)
|
||||
self.bus.set_xfer(self.chip_select, 24, 0)
|
||||
|
||||
@kernel
|
||||
def write(self, data):
|
||||
"""Write 24 bits of data.
|
||||
|
||||
This method advances the timeline by the duration of the SPI transfer
|
||||
and the required CS high time.
|
||||
"""
|
||||
self.bus.write(data << 8)
|
||||
delay_mu(self.bus.ref_period_mu) # get to 20ns min cs high
|
||||
|
||||
@kernel
|
||||
def write_offsets(self, value=0x1fff):
|
||||
"""Write the OFS0 and OFS1 offset DACs.
|
||||
|
||||
This method advances the timeline by twice the duration of
|
||||
:meth:`write`.
|
||||
|
||||
:param value: Value to set both offset registers to.
|
||||
"""
|
||||
value &= 0x3fff
|
||||
self.write(_AD5360_CMD_SPECIAL | _AD5360_SPECIAL_OFS0 | value)
|
||||
self.write(_AD5360_CMD_SPECIAL | _AD5360_SPECIAL_OFS1 | value)
|
||||
|
||||
@kernel
|
||||
def write_channel(self, channel=0, value=0, op=_AD5360_CMD_DATA):
|
||||
"""Write to a channel register.
|
||||
|
||||
This method advances the timeline by the duration of :meth:`write`.
|
||||
|
||||
:param channel: Channel number to write to.
|
||||
:param value: 16 bit value to write to the register.
|
||||
:param op: Operation to perform, one of :const:`_AD5360_CMD_DATA`,
|
||||
:const:`_AD5360_CMD_OFFSET`, :const:`_AD5360_CMD_GAIN`
|
||||
(default: :const:`_AD5360_CMD_DATA`).
|
||||
"""
|
||||
channel &= 0x3f
|
||||
value &= 0xffff
|
||||
self.write(op | _AD5360_WRITE_CHANNEL(channel) | value)
|
||||
|
||||
@kernel
|
||||
def read_channel_sync(self, channel=0, op=_AD5360_READ_X1A):
|
||||
"""Read a channel register.
|
||||
|
||||
This method advances the timeline by the duration of :meth:`write` plus
|
||||
three RTIO-to-Wishbone transactions.
|
||||
|
||||
:param channel: Channel number to read from.
|
||||
:param op: Operation to perform, one of :const:`_AD5360_READ_X1A`,
|
||||
:const:`_AD5360_READ_X1B`, :const:`_AD5360_READ_OFFSET`,
|
||||
:const:`_AD5360_READ_GAIN` (default: :const:`_AD5360_READ_X1A`).
|
||||
:return: The 16 bit register value.
|
||||
"""
|
||||
channel &= 0x3f
|
||||
self.write(_AD5360_CMD_SPECIAL | _AD5360_SPECIAL_READ | op |
|
||||
_AD5360_READ_CHANNEL(channel))
|
||||
self.bus.set_xfer(self.chip_select, 0, 24)
|
||||
self.write(_AD5360_CMD_SPECIAL | _AD5360_SPECIAL_NOP)
|
||||
self.bus.read_async()
|
||||
self.bus.set_xfer(self.chip_select, 24, 0)
|
||||
return self.bus.input_async() & 0xffff
|
||||
|
||||
@kernel
|
||||
def load(self):
|
||||
"""Pulse the LDAC line.
|
||||
|
||||
This method advances the timeline by two RTIO clock periods (16 ns).
|
||||
"""
|
||||
self.ldac.off()
|
||||
# t13 = 10ns ldac pulse width low
|
||||
delay_mu(2*self.bus.ref_period_mu)
|
||||
self.ldac.on()
|
||||
|
||||
@kernel
|
||||
def set(self, values, op=_AD5360_CMD_DATA):
|
||||
"""Write to several channels and pulse LDAC to update the channels.
|
||||
|
||||
This method does not advance the timeline. Write events are scheduled
|
||||
in the past. The DACs will synchronously start changing their output
|
||||
levels `now`.
|
||||
|
||||
:param values: List of 16 bit values to write to the channels.
|
||||
:param op: Operation to perform, one of :const:`_AD5360_CMD_DATA`,
|
||||
:const:`_AD5360_CMD_OFFSET`, :const:`_AD5360_CMD_GAIN`
|
||||
(default: :const:`_AD5360_CMD_DATA`).
|
||||
"""
|
||||
# compensate all delays that will be applied
|
||||
delay_mu(-len(values)*(self.bus.xfer_period_mu +
|
||||
self.bus.write_period_mu +
|
||||
self.bus.ref_period_mu) -
|
||||
3*self.bus.ref_period_mu -
|
||||
self.core.seconds_to_mu(1.5*us))
|
||||
for i in range(len(values)):
|
||||
self.write_channel(i, values[i], op)
|
||||
delay_mu(3*self.bus.ref_period_mu + # latency alignment ttl to spi
|
||||
self.core.seconds_to_mu(1.5*us)) # t10 max busy low for one channel
|
||||
self.load()
|
||||
delay_mu(-2*self.bus.ref_period_mu) # load(), t13
|
|
@ -0,0 +1,393 @@
|
|||
""""RTIO driver for the Analog Devices AD53[67][0123] family of multi-channel
|
||||
Digital to Analog Converters.
|
||||
|
||||
Output event replacement is not supported and issuing commands at the same
|
||||
time is an error.
|
||||
"""
|
||||
|
||||
# Designed from the data sheets and somewhat after the linux kernel
|
||||
# iio driver.
|
||||
|
||||
from numpy import int32
|
||||
|
||||
from artiq.language.core import (kernel, portable, delay_mu, delay, now_mu,
|
||||
at_mu)
|
||||
from artiq.language.units import ns, us
|
||||
from artiq.coredevice import spi2 as spi
|
||||
|
||||
SPI_AD53XX_CONFIG = (0*spi.SPI_OFFLINE | 1*spi.SPI_END |
|
||||
0*spi.SPI_INPUT | 0*spi.SPI_CS_POLARITY |
|
||||
0*spi.SPI_CLK_POLARITY | 1*spi.SPI_CLK_PHASE |
|
||||
0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX)
|
||||
|
||||
AD53XX_CMD_DATA = 3 << 22
|
||||
AD53XX_CMD_OFFSET = 2 << 22
|
||||
AD53XX_CMD_GAIN = 1 << 22
|
||||
AD53XX_CMD_SPECIAL = 0 << 22
|
||||
|
||||
AD53XX_SPECIAL_NOP = 0 << 16
|
||||
AD53XX_SPECIAL_CONTROL = 1 << 16
|
||||
AD53XX_SPECIAL_OFS0 = 2 << 16
|
||||
AD53XX_SPECIAL_OFS1 = 3 << 16
|
||||
AD53XX_SPECIAL_READ = 5 << 16
|
||||
AD53XX_SPECIAL_AB0 = 6 << 16
|
||||
AD53XX_SPECIAL_AB1 = 7 << 16
|
||||
AD53XX_SPECIAL_AB2 = 8 << 16
|
||||
AD53XX_SPECIAL_AB3 = 9 << 16
|
||||
AD53XX_SPECIAL_AB = 11 << 16
|
||||
|
||||
# incorporate the channel offset (8, table 17) here
|
||||
AD53XX_READ_X1A = 0x008 << 7
|
||||
AD53XX_READ_X1B = 0x048 << 7
|
||||
AD53XX_READ_OFFSET = 0x088 << 7
|
||||
AD53XX_READ_GAIN = 0x0C8 << 7
|
||||
|
||||
AD53XX_READ_CONTROL = 0x101 << 7
|
||||
AD53XX_READ_OFS0 = 0x102 << 7
|
||||
AD53XX_READ_OFS1 = 0x103 << 7
|
||||
AD53XX_READ_AB0 = 0x106 << 7
|
||||
AD53XX_READ_AB1 = 0x107 << 7
|
||||
AD53XX_READ_AB2 = 0x108 << 7
|
||||
AD53XX_READ_AB3 = 0x109 << 7
|
||||
|
||||
|
||||
@portable
|
||||
def ad53xx_cmd_write_ch(channel, value, op):
|
||||
"""Returns the word that must be written to the DAC to set a DAC
|
||||
channel register to a given value.
|
||||
|
||||
:param channel: DAC channel to write to (8 bits)
|
||||
:param value: 16-bit value to write to the register
|
||||
:param op: The channel register to write to, one of
|
||||
:const:`AD53XX_CMD_DATA`, :const:`AD53XX_CMD_OFFSET` or
|
||||
:const:`AD53XX_CMD_GAIN`.
|
||||
:return: The 24-bit word to be written to the DAC
|
||||
"""
|
||||
return op | (channel + 8) << 16 | (value & 0xffff)
|
||||
|
||||
|
||||
@portable
|
||||
def ad53xx_cmd_read_ch(channel, op):
|
||||
"""Returns the word that must be written to the DAC to read a given
|
||||
DAC channel register.
|
||||
|
||||
:param channel: DAC channel to read (8 bits)
|
||||
:param op: The channel register to read, one of
|
||||
:const:`AD53XX_READ_X1A`, :const:`AD53XX_READ_X1B`,
|
||||
:const:`AD53XX_READ_OFFSET`, :const:`AD53XX_READ_GAIN` etc.
|
||||
:return: The 24-bit word to be written to the DAC to initiate read
|
||||
"""
|
||||
return AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_READ | (op + (channel << 7))
|
||||
|
||||
|
||||
# maintain function definition for backward compatibility
|
||||
@portable
|
||||
def voltage_to_mu(voltage, offset_dacs=0x2000, vref=5.):
|
||||
"""Returns the 16-bit DAC register value required to produce a given output
|
||||
voltage, assuming offset and gain errors have been trimmed out.
|
||||
|
||||
The 16-bit register value may also be used with 14-bit DACs. The additional
|
||||
bits are disregarded by 14-bit DACs.
|
||||
|
||||
Also used to return offset register value required to produce a given
|
||||
voltage when the DAC register is set to mid-scale.
|
||||
An offset of V can be used to trim out a DAC offset error of -V.
|
||||
|
||||
:param voltage: Voltage in SI units.
|
||||
Valid voltages are: [-2*vref, + 2*vref - 1 LSB] + voltage offset.
|
||||
:param offset_dacs: Register value for the two offset DACs
|
||||
(default: 0x2000)
|
||||
:param vref: DAC reference voltage (default: 5.)
|
||||
:return: The 16-bit DAC register value
|
||||
"""
|
||||
code = int(round((1 << 16) * (voltage / (4. * vref)) + offset_dacs * 0x4))
|
||||
if code < 0x0 or code > 0xffff:
|
||||
raise ValueError("Invalid DAC voltage!")
|
||||
return code
|
||||
|
||||
|
||||
class _DummyTTL:
|
||||
@portable
|
||||
def on(self):
|
||||
pass
|
||||
|
||||
@portable
|
||||
def off(self):
|
||||
pass
|
||||
|
||||
|
||||
class AD53xx:
|
||||
"""Analog devices AD53[67][0123] family of multi-channel Digital to Analog
|
||||
Converters.
|
||||
|
||||
:param spi_device: SPI bus device name
|
||||
:param ldac_device: LDAC RTIO TTLOut channel name (optional)
|
||||
:param clr_device: CLR RTIO TTLOut channel name (optional)
|
||||
:param chip_select: Value to drive on SPI chip select lines during
|
||||
transactions (default: 1)
|
||||
:param div_write: SPI clock divider for write operations (default: 4,
|
||||
50MHz max SPI clock with {t_high, t_low} >=8ns)
|
||||
:param div_read: SPI clock divider for read operations (default: 8, not
|
||||
optimized for speed, but cf data sheet t22: 25ns min SCLK edge to SDO
|
||||
valid)
|
||||
:param vref: DAC reference voltage (default: 5.)
|
||||
:param offset_dacs: Initial register value for the two offset DACs, device
|
||||
dependent and must be set correctly for correct voltage to mu
|
||||
conversions. Knowledge of his state is not transferred between
|
||||
experiments. (default: 8192)
|
||||
:param core_device: Core device name (default: "core")
|
||||
"""
|
||||
kernel_invariants = {"bus", "ldac", "clr", "chip_select", "div_write",
|
||||
"div_read", "vref", "core"}
|
||||
|
||||
def __init__(self, dmgr, spi_device, ldac_device=None, clr_device=None,
|
||||
chip_select=1, div_write=4, div_read=16, vref=5.,
|
||||
offset_dacs=8192, core="core"):
|
||||
self.bus = dmgr.get(spi_device)
|
||||
self.bus.update_xfer_duration_mu(div_write, 24)
|
||||
if ldac_device is None:
|
||||
self.ldac = _DummyTTL()
|
||||
else:
|
||||
self.ldac = dmgr.get(ldac_device)
|
||||
if clr_device is None:
|
||||
self.clr = _DummyTTL()
|
||||
else:
|
||||
self.clr = dmgr.get(clr_device)
|
||||
self.chip_select = chip_select
|
||||
self.div_write = div_write
|
||||
self.div_read = div_read
|
||||
self.vref = vref
|
||||
self.offset_dacs = offset_dacs
|
||||
self.core = dmgr.get(core)
|
||||
|
||||
@kernel
|
||||
def init(self, blind=False):
|
||||
"""Configures the SPI bus, drives LDAC and CLR high, programmes
|
||||
the offset DACs, and enables overtemperature shutdown.
|
||||
|
||||
This method must be called before any other method at start-up or if
|
||||
the SPI bus has been accessed by another device.
|
||||
|
||||
:param blind: If ``True``, do not attempt to read back control register
|
||||
or check for overtemperature.
|
||||
"""
|
||||
self.ldac.on()
|
||||
self.clr.on()
|
||||
self.bus.set_config_mu(SPI_AD53XX_CONFIG, 24, self.div_write,
|
||||
self.chip_select)
|
||||
self.write_offset_dacs_mu(self.offset_dacs)
|
||||
if not blind:
|
||||
ctrl = self.read_reg(channel=0, op=AD53XX_READ_CONTROL)
|
||||
if ctrl == 0xffff:
|
||||
raise ValueError("DAC not found")
|
||||
if ctrl & 0b10000:
|
||||
raise ValueError("DAC over temperature")
|
||||
delay(25*us)
|
||||
self.bus.write( # enable power and overtemperature shutdown
|
||||
(AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_CONTROL | 0b0010) << 8)
|
||||
if not blind:
|
||||
ctrl = self.read_reg(channel=0, op=AD53XX_READ_CONTROL)
|
||||
if (ctrl & 0b10111) != 0b00010:
|
||||
raise ValueError("DAC CONTROL readback mismatch")
|
||||
delay(15*us)
|
||||
|
||||
@kernel
|
||||
def read_reg(self, channel=0, op=AD53XX_READ_X1A):
|
||||
"""Read a DAC register.
|
||||
|
||||
This method advances the timeline by the duration of two SPI transfers
|
||||
plus two RTIO coarse cycles plus 270 ns and consumes all slack.
|
||||
|
||||
:param channel: Channel number to read from (default: 0)
|
||||
:param op: Operation to perform, one of :const:`AD53XX_READ_X1A`,
|
||||
:const:`AD53XX_READ_X1B`, :const:`AD53XX_READ_OFFSET`,
|
||||
:const:`AD53XX_READ_GAIN` etc. (default: :const:`AD53XX_READ_X1A`).
|
||||
:return: The 16 bit register value
|
||||
"""
|
||||
self.bus.write(ad53xx_cmd_read_ch(channel, op) << 8)
|
||||
self.bus.set_config_mu(SPI_AD53XX_CONFIG | spi.SPI_INPUT, 24,
|
||||
self.div_read, self.chip_select)
|
||||
delay(270*ns) # t_21 min sync high in readback
|
||||
self.bus.write((AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_NOP) << 8)
|
||||
self.bus.set_config_mu(SPI_AD53XX_CONFIG, 24, self.div_write,
|
||||
self.chip_select)
|
||||
# FIXME: the int32 should not be needed to resolve unification
|
||||
return self.bus.read() & int32(0xffff)
|
||||
|
||||
@kernel
|
||||
def write_offset_dacs_mu(self, value):
|
||||
"""Program the OFS0 and OFS1 offset DAC registers.
|
||||
|
||||
Writes to the offset DACs take effect immediately without requiring
|
||||
a LDAC. This method advances the timeline by the duration of two SPI
|
||||
transfers.
|
||||
|
||||
:param value: Value to set both offset DAC registers to
|
||||
"""
|
||||
value &= 0x3fff
|
||||
self.offset_dacs = value
|
||||
self.bus.write((AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_OFS0 | value) << 8)
|
||||
self.bus.write((AD53XX_CMD_SPECIAL | AD53XX_SPECIAL_OFS1 | value) << 8)
|
||||
|
||||
@kernel
|
||||
def write_gain_mu(self, channel, gain=0xffff):
|
||||
"""Program the gain register for a DAC channel.
|
||||
|
||||
The DAC output is not updated until LDAC is pulsed (see :meth load:).
|
||||
This method advances the timeline by the duration of one SPI transfer.
|
||||
|
||||
:param gain: 16-bit gain register value (default: 0xffff)
|
||||
"""
|
||||
self.bus.write(
|
||||
ad53xx_cmd_write_ch(channel, gain, AD53XX_CMD_GAIN) << 8)
|
||||
|
||||
@kernel
|
||||
def write_offset_mu(self, channel, offset=0x8000):
|
||||
"""Program the offset register for a DAC channel.
|
||||
|
||||
The DAC output is not updated until LDAC is pulsed (see :meth load:).
|
||||
This method advances the timeline by the duration of one SPI transfer.
|
||||
|
||||
:param offset: 16-bit offset register value (default: 0x8000)
|
||||
"""
|
||||
self.bus.write(
|
||||
ad53xx_cmd_write_ch(channel, offset, AD53XX_CMD_OFFSET) << 8)
|
||||
|
||||
@kernel
|
||||
def write_offset(self, channel, voltage):
|
||||
"""Program the DAC offset voltage for a channel.
|
||||
|
||||
An offset of +V can be used to trim out a DAC offset error of -V.
|
||||
The DAC output is not updated until LDAC is pulsed (see :meth load:).
|
||||
This method advances the timeline by the duration of one SPI transfer.
|
||||
|
||||
:param voltage: the offset voltage
|
||||
"""
|
||||
self.write_offset_mu(channel, voltage_to_mu(voltage, self.offset_dacs,
|
||||
self.vref))
|
||||
|
||||
@kernel
|
||||
def write_dac_mu(self, channel, value):
|
||||
"""Program the DAC input register for a channel.
|
||||
|
||||
The DAC output is not updated until LDAC is pulsed (see :meth load:).
|
||||
This method advances the timeline by the duration of one SPI transfer.
|
||||
"""
|
||||
self.bus.write(
|
||||
ad53xx_cmd_write_ch(channel, value, AD53XX_CMD_DATA) << 8)
|
||||
|
||||
@kernel
|
||||
def write_dac(self, channel, voltage):
|
||||
"""Program the DAC output voltage for a channel.
|
||||
|
||||
The DAC output is not updated until LDAC is pulsed (see :meth load:).
|
||||
This method advances the timeline by the duration of one SPI transfer.
|
||||
"""
|
||||
self.write_dac_mu(channel, voltage_to_mu(voltage, self.offset_dacs,
|
||||
self.vref))
|
||||
|
||||
@kernel
|
||||
def load(self):
|
||||
"""Pulse the LDAC line.
|
||||
|
||||
Note that there is a <= 1.5us "BUSY" period (t10) after writing to a
|
||||
DAC input/gain/offset register. All DAC registers may be programmed
|
||||
normally during the busy period, however LDACs during the busy period
|
||||
cause the DAC output to change *after* the BUSY period has completed,
|
||||
instead of the usual immediate update on LDAC behaviour.
|
||||
|
||||
This method advances the timeline by two RTIO clock periods.
|
||||
"""
|
||||
self.ldac.off()
|
||||
delay_mu(2*self.bus.ref_period_mu) # t13 = 10ns ldac pulse width low
|
||||
self.ldac.on()
|
||||
|
||||
@kernel
|
||||
def set_dac_mu(self, values, channels=list(range(40))):
|
||||
"""Program multiple DAC channels and pulse LDAC to update the DAC
|
||||
outputs.
|
||||
|
||||
This method does not advance the timeline; write events are scheduled
|
||||
in the past. The DACs will synchronously start changing their output
|
||||
levels `now`.
|
||||
|
||||
If no LDAC device was defined, the LDAC pulse is skipped.
|
||||
|
||||
See :meth load:.
|
||||
|
||||
:param values: list of DAC values to program
|
||||
:param channels: list of DAC channels to program. If not specified,
|
||||
we program the DAC channels sequentially, starting at 0.
|
||||
"""
|
||||
t0 = now_mu()
|
||||
|
||||
# t10: max busy period after writing to DAC registers
|
||||
t_10 = self.core.seconds_to_mu(1500*ns)
|
||||
# compensate all delays that will be applied
|
||||
delay_mu(-t_10-len(values)*self.bus.xfer_duration_mu)
|
||||
for i in range(len(values)):
|
||||
self.write_dac_mu(channels[i], values[i])
|
||||
delay_mu(t_10)
|
||||
self.load()
|
||||
at_mu(t0)
|
||||
|
||||
@kernel
|
||||
def set_dac(self, voltages, channels=list(range(40))):
|
||||
"""Program multiple DAC channels and pulse LDAC to update the DAC
|
||||
outputs.
|
||||
|
||||
This method does not advance the timeline; write events are scheduled
|
||||
in the past. The DACs will synchronously start changing their output
|
||||
levels `now`.
|
||||
|
||||
If no LDAC device was defined, the LDAC pulse is skipped.
|
||||
|
||||
:param voltages: list of voltages to program the DAC channels to
|
||||
:param channels: list of DAC channels to program. If not specified,
|
||||
we program the DAC channels sequentially, starting at 0.
|
||||
"""
|
||||
values = [voltage_to_mu(voltage, self.offset_dacs, self.vref)
|
||||
for voltage in voltages]
|
||||
self.set_dac_mu(values, channels)
|
||||
|
||||
@kernel
|
||||
def calibrate(self, channel, vzs, vfs):
|
||||
""" Two-point calibration of a DAC channel.
|
||||
|
||||
Programs the offset and gain register to trim out DAC errors. Does not
|
||||
take effect until LDAC is pulsed (see :meth load:).
|
||||
|
||||
Calibration consists of measuring the DAC output voltage for a channel
|
||||
with the DAC set to zero-scale (0x0000) and full-scale (0xffff).
|
||||
|
||||
Note that only negative offsets and full-scale errors (DAC gain too
|
||||
high) can be calibrated in this fashion.
|
||||
|
||||
:param channel: The number of the calibrated channel
|
||||
:params vzs: Measured voltage with the DAC set to zero-scale (0x0000)
|
||||
:params vfs: Measured voltage with the DAC set to full-scale (0xffff)
|
||||
"""
|
||||
offset_err = voltage_to_mu(vzs, self.offset_dacs, self.vref)
|
||||
gain_err = voltage_to_mu(vfs, self.offset_dacs, self.vref) - (
|
||||
offset_err + 0xffff)
|
||||
|
||||
assert offset_err <= 0
|
||||
assert gain_err >= 0
|
||||
|
||||
self.core.break_realtime()
|
||||
self.write_offset_mu(channel, 0x8000-offset_err)
|
||||
self.write_gain_mu(channel, 0xffff-gain_err)
|
||||
|
||||
@portable
|
||||
def voltage_to_mu(self, voltage):
|
||||
"""Returns the 16-bit DAC register value required to produce a given
|
||||
output voltage, assuming offset and gain errors have been trimmed out.
|
||||
|
||||
The 16-bit register value may also be used with 14-bit DACs. The
|
||||
additional bits are disregarded by 14-bit DACs.
|
||||
|
||||
:param voltage: Voltage in SI units.
|
||||
Valid voltages are: [-2*vref, + 2*vref - 1 LSB] + voltage offset.
|
||||
:return: The 16-bit DAC register value
|
||||
"""
|
||||
return voltage_to_mu(voltage, self.offset_dacs, self.vref)
|
|
@ -10,9 +10,8 @@ class AD9154:
|
|||
self.chip_select = chip_select
|
||||
|
||||
@kernel
|
||||
def setup_bus(self, write_div=16, read_div=16):
|
||||
self.bus.set_config_mu(0, write_div, read_div)
|
||||
self.bus.set_xfer(self.chip_select, 24, 0)
|
||||
def setup_bus(self, div=16):
|
||||
self.bus.set_config_mu(0, 24, div, self.chip_select)
|
||||
|
||||
@kernel
|
||||
def write(self, addr, data):
|
||||
|
|
|
@ -0,0 +1,926 @@
|
|||
from numpy import int32, int64
|
||||
|
||||
from artiq.language.core import (
|
||||
kernel, delay, portable, delay_mu, now_mu, at_mu)
|
||||
from artiq.language.units import us, ms
|
||||
from artiq.language.types import *
|
||||
|
||||
from artiq.coredevice import spi2 as spi
|
||||
from artiq.coredevice import urukul
|
||||
# Work around ARTIQ-Python import machinery
|
||||
urukul_sta_pll_lock = urukul.urukul_sta_pll_lock
|
||||
urukul_sta_smp_err = urukul.urukul_sta_smp_err
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AD9910",
|
||||
"PHASE_MODE_CONTINUOUS", "PHASE_MODE_ABSOLUTE", "PHASE_MODE_TRACKING",
|
||||
"RAM_DEST_FTW", "RAM_DEST_POW", "RAM_DEST_ASF", "RAM_DEST_POWASF",
|
||||
"RAM_MODE_DIRECTSWITCH", "RAM_MODE_RAMPUP", "RAM_MODE_BIDIR_RAMP",
|
||||
"RAM_MODE_CONT_BIDIR_RAMP", "RAM_MODE_CONT_RAMPUP",
|
||||
]
|
||||
|
||||
|
||||
_PHASE_MODE_DEFAULT = -1
|
||||
PHASE_MODE_CONTINUOUS = 0
|
||||
PHASE_MODE_ABSOLUTE = 1
|
||||
PHASE_MODE_TRACKING = 2
|
||||
|
||||
_AD9910_REG_CFR1 = 0x00
|
||||
_AD9910_REG_CFR2 = 0x01
|
||||
_AD9910_REG_CFR3 = 0x02
|
||||
_AD9910_REG_AUX_DAC = 0x03
|
||||
_AD9910_REG_IO_UPDATE = 0x04
|
||||
_AD9910_REG_FTW = 0x07
|
||||
_AD9910_REG_POW = 0x08
|
||||
_AD9910_REG_ASF = 0x09
|
||||
_AD9910_REG_SYNC = 0x0a
|
||||
_AD9910_REG_RAMP_LIMIT = 0x0b
|
||||
_AD9910_REG_RAMP_STEP = 0x0c
|
||||
_AD9910_REG_RAMP_RATE = 0x0d
|
||||
_AD9910_REG_PROFILE0 = 0x0e
|
||||
_AD9910_REG_PROFILE1 = 0x0f
|
||||
_AD9910_REG_PROFILE2 = 0x10
|
||||
_AD9910_REG_PROFILE3 = 0x11
|
||||
_AD9910_REG_PROFILE4 = 0x12
|
||||
_AD9910_REG_PROFILE5 = 0x13
|
||||
_AD9910_REG_PROFILE6 = 0x14
|
||||
_AD9910_REG_PROFILE7 = 0x15
|
||||
_AD9910_REG_RAM = 0x16
|
||||
|
||||
# RAM destination
|
||||
RAM_DEST_FTW = 0
|
||||
RAM_DEST_POW = 1
|
||||
RAM_DEST_ASF = 2
|
||||
RAM_DEST_POWASF = 3
|
||||
|
||||
# RAM MODES
|
||||
RAM_MODE_DIRECTSWITCH = 0
|
||||
RAM_MODE_RAMPUP = 1
|
||||
RAM_MODE_BIDIR_RAMP = 2
|
||||
RAM_MODE_CONT_BIDIR_RAMP = 3
|
||||
RAM_MODE_CONT_RAMPUP = 4
|
||||
|
||||
|
||||
class SyncDataUser:
|
||||
def __init__(self, core, sync_delay_seed, io_update_delay):
|
||||
self.core = core
|
||||
self.sync_delay_seed = sync_delay_seed
|
||||
self.io_update_delay = io_update_delay
|
||||
|
||||
@kernel
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
|
||||
class SyncDataEeprom:
|
||||
def __init__(self, dmgr, core, eeprom_str):
|
||||
self.core = core
|
||||
|
||||
eeprom_device, eeprom_offset = eeprom_str.split(":")
|
||||
self.eeprom_device = dmgr.get(eeprom_device)
|
||||
self.eeprom_offset = int(eeprom_offset)
|
||||
|
||||
self.sync_delay_seed = 0
|
||||
self.io_update_delay = 0
|
||||
|
||||
@kernel
|
||||
def init(self):
|
||||
word = self.eeprom_device.read_i32(self.eeprom_offset) >> 16
|
||||
sync_delay_seed = word >> 8
|
||||
if sync_delay_seed >= 0:
|
||||
io_update_delay = word & 0xff
|
||||
else:
|
||||
io_update_delay = 0
|
||||
if io_update_delay == 0xff: # unprogrammed EEPROM
|
||||
io_update_delay = 0
|
||||
# With Numpy, type(int32(-1) >> 1) == int64
|
||||
self.sync_delay_seed = int32(sync_delay_seed)
|
||||
self.io_update_delay = int32(io_update_delay)
|
||||
|
||||
|
||||
class AD9910:
|
||||
"""
|
||||
AD9910 DDS channel on Urukul.
|
||||
|
||||
This class supports a single DDS channel and exposes the DDS,
|
||||
the digital step attenuator, and the RF switch.
|
||||
|
||||
:param chip_select: Chip select configuration. On Urukul this is an
|
||||
encoded chip select and not "one-hot": 3 to address multiple chips
|
||||
(as configured through CFG_MASK_NU), 4-7 for individual channels.
|
||||
:param cpld_device: Name of the Urukul CPLD this device is on.
|
||||
:param sw_device: Name of the RF switch device. The RF switch is a
|
||||
TTLOut channel available as the :attr:`sw` attribute of this instance.
|
||||
:param pll_n: DDS PLL multiplier. The DDS sample clock is
|
||||
f_ref/clk_div*pll_n where f_ref is the reference frequency and
|
||||
clk_div is the reference clock divider (both set in the parent
|
||||
Urukul CPLD instance).
|
||||
:param pll_en: PLL enable bit, set to 0 to bypass PLL (default: 1).
|
||||
Note that when bypassing the PLL the red front panel LED may remain on.
|
||||
:param pll_cp: DDS PLL charge pump setting.
|
||||
:param pll_vco: DDS PLL VCO range selection.
|
||||
:param sync_delay_seed: SYNC_IN delay tuning starting value.
|
||||
To stabilize the SYNC_IN delay tuning, run :meth:`tune_sync_delay` once
|
||||
and set this to the delay tap number returned (default: -1 to signal no
|
||||
synchronization and no tuning during :meth:`init`).
|
||||
Can be a string of the form "eeprom_device:byte_offset" to read the value
|
||||
from a I2C EEPROM; in which case, `io_update_delay` must be set to the
|
||||
same string value.
|
||||
:param io_update_delay: IO_UPDATE pulse alignment delay.
|
||||
To align IO_UPDATE to SYNC_CLK, run :meth:`tune_io_update_delay` and
|
||||
set this to the delay tap number returned.
|
||||
Can be a string of the form "eeprom_device:byte_offset" to read the value
|
||||
from a I2C EEPROM; in which case, `sync_delay_seed` must be set to the
|
||||
same string value.
|
||||
"""
|
||||
kernel_invariants = {"chip_select", "cpld", "core", "bus",
|
||||
"ftw_per_hz", "sysclk_per_mu"}
|
||||
|
||||
def __init__(self, dmgr, chip_select, cpld_device, sw_device=None,
|
||||
pll_n=40, pll_cp=7, pll_vco=5, sync_delay_seed=-1,
|
||||
io_update_delay=0, pll_en=1):
|
||||
self.cpld = dmgr.get(cpld_device)
|
||||
self.core = self.cpld.core
|
||||
self.bus = self.cpld.bus
|
||||
assert 3 <= chip_select <= 7
|
||||
self.chip_select = chip_select
|
||||
if sw_device:
|
||||
self.sw = dmgr.get(sw_device)
|
||||
self.kernel_invariants.add("sw")
|
||||
clk = self.cpld.refclk/[4, 1, 2, 4][self.cpld.clk_div]
|
||||
self.pll_en = pll_en
|
||||
self.pll_n = pll_n
|
||||
self.pll_vco = pll_vco
|
||||
self.pll_cp = pll_cp
|
||||
if pll_en:
|
||||
sysclk = clk*pll_n
|
||||
assert clk <= 60e6
|
||||
assert 12 <= pll_n <= 127
|
||||
assert 0 <= pll_vco <= 5
|
||||
vco_min, vco_max = [(370, 510), (420, 590), (500, 700),
|
||||
(600, 880), (700, 950), (820, 1150)][pll_vco]
|
||||
assert vco_min <= sysclk/1e6 <= vco_max
|
||||
assert 0 <= pll_cp <= 7
|
||||
else:
|
||||
sysclk = clk
|
||||
assert sysclk <= 1e9
|
||||
self.ftw_per_hz = (1 << 32)/sysclk
|
||||
self.sysclk_per_mu = int(round(sysclk*self.core.ref_period))
|
||||
self.sysclk = sysclk
|
||||
|
||||
if isinstance(sync_delay_seed, str) or isinstance(io_update_delay, str):
|
||||
if sync_delay_seed != io_update_delay:
|
||||
raise ValueError("When using EEPROM, sync_delay_seed must be equal to io_update_delay")
|
||||
self.sync_data = SyncDataEeprom(dmgr, self.core, sync_delay_seed)
|
||||
else:
|
||||
self.sync_data = SyncDataUser(self.core, sync_delay_seed, io_update_delay)
|
||||
|
||||
self.phase_mode = PHASE_MODE_CONTINUOUS
|
||||
|
||||
@kernel
|
||||
def set_phase_mode(self, phase_mode):
|
||||
r"""Set the default phase mode.
|
||||
|
||||
for future calls to :meth:`set` and
|
||||
:meth:`set_mu`. Supported phase modes are:
|
||||
|
||||
* :const:`PHASE_MODE_CONTINUOUS`: the phase accumulator is unchanged
|
||||
when changing frequency or phase. The DDS phase is the sum of the
|
||||
phase accumulator and the phase offset. The only discontinuous
|
||||
changes in the DDS output phase come from changes to the phase
|
||||
offset. This mode is also knows as "relative phase mode".
|
||||
:math:`\phi(t) = q(t^\prime) + p + (t - t^\prime) f`
|
||||
|
||||
* :const:`PHASE_MODE_ABSOLUTE`: the phase accumulator is reset when
|
||||
changing frequency or phase. Thus, the phase of the DDS at the
|
||||
time of the change is equal to the specified phase offset.
|
||||
:math:`\phi(t) = p + (t - t^\prime) f`
|
||||
|
||||
* :const:`PHASE_MODE_TRACKING`: when changing frequency or phase,
|
||||
the phase accumulator is cleared and the phase offset is offset
|
||||
by the value the phase accumulator would have if the DDS had been
|
||||
running at the specified frequency since a given fiducial
|
||||
time stamp. This is functionally equivalent to
|
||||
:const:`PHASE_MODE_ABSOLUTE`. The only difference is the fiducial
|
||||
time stamp. This mode is also known as "coherent phase mode".
|
||||
The default fiducial time stamp is 0.
|
||||
:math:`\phi(t) = p + (t - T) f`
|
||||
|
||||
Where:
|
||||
|
||||
* :math:`\phi(t)`: the DDS output phase
|
||||
* :math:`q(t) = \phi(t) - p`: DDS internal phase accumulator
|
||||
* :math:`p`: phase offset
|
||||
* :math:`f`: frequency
|
||||
* :math:`t^\prime`: time stamp of setting :math:`p`, :math:`f`
|
||||
* :math:`T`: fiducial time stamp
|
||||
* :math:`t`: running time
|
||||
|
||||
.. warning:: This setting may become inconsistent when used as part of
|
||||
a DMA recording. When using DMA, it is recommended to specify the
|
||||
phase mode explicitly when calling :meth:`set` or :meth:`set_mu`.
|
||||
"""
|
||||
self.phase_mode = phase_mode
|
||||
|
||||
@kernel
|
||||
def write16(self, addr, data):
|
||||
"""Write to 16 bit register.
|
||||
|
||||
:param addr: Register address
|
||||
:param data: Data to be written
|
||||
"""
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 24,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write((addr << 24) | (data << 8))
|
||||
|
||||
@kernel
|
||||
def write32(self, addr, data):
|
||||
"""Write to 32 bit register.
|
||||
|
||||
:param addr: Register address
|
||||
:param data: Data to be written
|
||||
"""
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write(addr << 24)
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write(data)
|
||||
|
||||
@kernel
|
||||
def read16(self, addr):
|
||||
"""Read from 16 bit register.
|
||||
|
||||
:param addr: Register address
|
||||
"""
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write((addr | 0x80) << 24)
|
||||
self.bus.set_config_mu(
|
||||
urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT,
|
||||
16, urukul.SPIT_DDS_RD, self.chip_select)
|
||||
self.bus.write(0)
|
||||
return self.bus.read()
|
||||
|
||||
@kernel
|
||||
def read32(self, addr):
|
||||
"""Read from 32 bit register.
|
||||
|
||||
:param addr: Register address
|
||||
"""
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write((addr | 0x80) << 24)
|
||||
self.bus.set_config_mu(
|
||||
urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT,
|
||||
32, urukul.SPIT_DDS_RD, self.chip_select)
|
||||
self.bus.write(0)
|
||||
return self.bus.read()
|
||||
|
||||
@kernel
|
||||
def read64(self, addr):
|
||||
"""Read from 64 bit register.
|
||||
|
||||
:param addr: Register address
|
||||
:return: 64 bit integer register value
|
||||
"""
|
||||
self.bus.set_config_mu(
|
||||
urukul.SPI_CONFIG, 8,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write((addr | 0x80) << 24)
|
||||
self.bus.set_config_mu(
|
||||
urukul.SPI_CONFIG | spi.SPI_INPUT, 32,
|
||||
urukul.SPIT_DDS_RD, self.chip_select)
|
||||
self.bus.write(0)
|
||||
self.bus.set_config_mu(
|
||||
urukul.SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 32,
|
||||
urukul.SPIT_DDS_RD, self.chip_select)
|
||||
self.bus.write(0)
|
||||
hi = self.bus.read()
|
||||
lo = self.bus.read()
|
||||
return (int64(hi) << 32) | lo
|
||||
|
||||
@kernel
|
||||
def write64(self, addr, data_high, data_low):
|
||||
"""Write to 64 bit register.
|
||||
|
||||
:param addr: Register address
|
||||
:param data_high: High (MSB) 32 bits of the data
|
||||
:param data_low: Low (LSB) 32 data bits
|
||||
"""
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 8,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write(addr << 24)
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 32,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write(data_high)
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write(data_low)
|
||||
|
||||
@kernel
|
||||
def write_ram(self, data):
|
||||
"""Write data to RAM.
|
||||
|
||||
The profile to write to and the step, start, and end address
|
||||
need to be configured before and separately using
|
||||
:meth:`set_profile_ram` and the parent CPLD `set_profile`.
|
||||
|
||||
:param data List(int32): Data to be written to RAM.
|
||||
"""
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR,
|
||||
self.chip_select)
|
||||
self.bus.write(_AD9910_REG_RAM << 24)
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 32,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
for i in range(len(data) - 1):
|
||||
self.bus.write(data[i])
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write(data[len(data) - 1])
|
||||
|
||||
@kernel
|
||||
def read_ram(self, data):
|
||||
"""Read data from RAM.
|
||||
|
||||
The profile to read from and the step, start, and end address
|
||||
need to be configured before and separately using
|
||||
:meth:`set_profile_ram` and the parent CPLD `set_profile`.
|
||||
|
||||
:param data List(int32): List to be filled with data read from RAM.
|
||||
"""
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 8, urukul.SPIT_DDS_WR,
|
||||
self.chip_select)
|
||||
self.bus.write((_AD9910_REG_RAM | 0x80) << 24)
|
||||
n = len(data) - 1
|
||||
if n > 0:
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_INPUT, 32,
|
||||
urukul.SPIT_DDS_RD, self.chip_select)
|
||||
preload = min(n, 8)
|
||||
for i in range(n):
|
||||
self.bus.write(0)
|
||||
if i >= preload:
|
||||
data[i - preload] = self.bus.read()
|
||||
self.bus.set_config_mu(
|
||||
urukul.SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END, 32,
|
||||
urukul.SPIT_DDS_RD, self.chip_select)
|
||||
self.bus.write(0)
|
||||
for i in range(preload + 1):
|
||||
data[(n - preload) + i] = self.bus.read()
|
||||
|
||||
@kernel
|
||||
def set_cfr1(self, power_down=0b0000, phase_autoclear=0,
|
||||
drg_load_lrr=0, drg_autoclear=0,
|
||||
internal_profile=0, ram_destination=0, ram_enable=0,
|
||||
manual_osk_external=0, osk_enable=0, select_auto_osk=0):
|
||||
"""Set CFR1. See the AD9910 datasheet for parameter meanings.
|
||||
|
||||
This method does not pulse IO_UPDATE.
|
||||
|
||||
:param power_down: Power down bits.
|
||||
:param phase_autoclear: Autoclear phase accumulator.
|
||||
:param drg_load_lrr: Load digital ramp generator LRR.
|
||||
:param drg_autoclear: Autoclear digital ramp generator.
|
||||
:param internal_profile: Internal profile control.
|
||||
:param ram_destination: RAM destination
|
||||
(:const:`RAM_DEST_FTW`, :const:`RAM_DEST_POW`,
|
||||
:const:`RAM_DEST_ASF`, :const:`RAM_DEST_POWASF`).
|
||||
:param ram_enable: RAM mode enable.
|
||||
:param manual_osk_external: Enable OSK pin control in manual OSK mode.
|
||||
:param osk_enable: Enable OSK mode.
|
||||
:param select_auto_osk: Select manual or automatic OSK mode.
|
||||
"""
|
||||
self.write32(_AD9910_REG_CFR1,
|
||||
(ram_enable << 31) |
|
||||
(ram_destination << 29) |
|
||||
(manual_osk_external << 23) |
|
||||
(internal_profile << 17) |
|
||||
(drg_load_lrr << 15) |
|
||||
(drg_autoclear << 14) |
|
||||
(phase_autoclear << 13) |
|
||||
(osk_enable << 9) |
|
||||
(select_auto_osk << 8) |
|
||||
(power_down << 4) |
|
||||
2) # SDIO input only, MSB first
|
||||
|
||||
@kernel
|
||||
def init(self, blind=False):
|
||||
"""Initialize and configure the DDS.
|
||||
|
||||
Sets up SPI mode, confirms chip presence, powers down unused blocks,
|
||||
configures the PLL, waits for PLL lock. Uses the
|
||||
IO_UPDATE signal multiple times.
|
||||
|
||||
:param blind: Do not read back DDS identity and do not wait for lock.
|
||||
"""
|
||||
self.sync_data.init()
|
||||
if self.sync_data.sync_delay_seed >= 0 and not self.cpld.sync_div:
|
||||
raise ValueError("parent cpld does not drive SYNC")
|
||||
if self.sync_data.sync_delay_seed >= 0:
|
||||
if self.sysclk_per_mu != self.sysclk*self.core.ref_period:
|
||||
raise ValueError("incorrect clock ratio for synchronization")
|
||||
delay(50*ms) # slack
|
||||
|
||||
# Set SPI mode
|
||||
self.set_cfr1()
|
||||
self.cpld.io_update.pulse(1*us)
|
||||
delay(1*ms)
|
||||
if not blind:
|
||||
# Use the AUX DAC setting to identify and confirm presence
|
||||
aux_dac = self.read32(_AD9910_REG_AUX_DAC)
|
||||
if aux_dac & 0xff != 0x7f:
|
||||
raise ValueError("Urukul AD9910 AUX_DAC mismatch")
|
||||
delay(50*us) # slack
|
||||
# Configure PLL settings and bring up PLL
|
||||
# enable amplitude scale from profiles
|
||||
# read effective FTW
|
||||
# sync timing validation disable (enabled later)
|
||||
self.write32(_AD9910_REG_CFR2, 0x01010020)
|
||||
self.cpld.io_update.pulse(1*us)
|
||||
cfr3 = (0x0807c000 | (self.pll_vco << 24) |
|
||||
(self.pll_cp << 19) | (self.pll_en << 8) |
|
||||
(self.pll_n << 1))
|
||||
self.write32(_AD9910_REG_CFR3, cfr3 | 0x400) # PFD reset
|
||||
self.cpld.io_update.pulse(1*us)
|
||||
if self.pll_en:
|
||||
self.write32(_AD9910_REG_CFR3, cfr3)
|
||||
self.cpld.io_update.pulse(1*us)
|
||||
if blind:
|
||||
delay(100*ms)
|
||||
else:
|
||||
# Wait for PLL lock, up to 100 ms
|
||||
for i in range(100):
|
||||
sta = self.cpld.sta_read()
|
||||
lock = urukul_sta_pll_lock(sta)
|
||||
delay(1*ms)
|
||||
if lock & (1 << self.chip_select - 4):
|
||||
break
|
||||
if i >= 100 - 1:
|
||||
raise ValueError("PLL lock timeout")
|
||||
delay(10*us) # slack
|
||||
if self.sync_data.sync_delay_seed >= 0:
|
||||
self.tune_sync_delay(self.sync_data.sync_delay_seed)
|
||||
delay(1*ms)
|
||||
|
||||
@kernel
|
||||
def power_down(self, bits=0b1111):
|
||||
"""Power down DDS.
|
||||
|
||||
:param bits: Power down bits, see datasheet
|
||||
"""
|
||||
self.set_cfr1(power_down=bits)
|
||||
self.cpld.io_update.pulse(1*us)
|
||||
|
||||
# KLUDGE: ref_time_mu default argument is explicitly marked int64() to
|
||||
# avoid silent truncation of explicitly passed timestamps. (Compiler bug?)
|
||||
@kernel
|
||||
def set_mu(self, ftw, pow_=0, asf=0x3fff, phase_mode=_PHASE_MODE_DEFAULT,
|
||||
ref_time_mu=int64(-1), profile=0):
|
||||
"""Set profile 0 data in machine units.
|
||||
|
||||
This uses machine units (FTW, POW, ASF). The frequency tuning word
|
||||
width is 32, the phase offset word width is 16, and the amplitude
|
||||
scale factor width is 12.
|
||||
|
||||
After the SPI transfer, the shared IO update pin is pulsed to
|
||||
activate the data.
|
||||
|
||||
.. seealso: :meth:`set_phase_mode` for a definition of the different
|
||||
phase modes.
|
||||
|
||||
:param ftw: Frequency tuning word: 32 bit.
|
||||
:param pow_: Phase tuning word: 16 bit unsigned.
|
||||
:param asf: Amplitude scale factor: 14 bit unsigned.
|
||||
:param phase_mode: If specified, overrides the default phase mode set
|
||||
by :meth:`set_phase_mode` for this call.
|
||||
:param ref_time_mu: Fiducial time used to compute absolute or tracking
|
||||
phase updates. In machine units as obtained by `now_mu()`.
|
||||
:param profile: Profile number to set (0-7, default: 0).
|
||||
:return: Resulting phase offset word after application of phase
|
||||
tracking offset. When using :const:`PHASE_MODE_CONTINUOUS` in
|
||||
subsequent calls, use this value as the "current" phase.
|
||||
"""
|
||||
if phase_mode == _PHASE_MODE_DEFAULT:
|
||||
phase_mode = self.phase_mode
|
||||
# Align to coarse RTIO which aligns SYNC_CLK. I.e. clear fine TSC
|
||||
# This will not cause a collision or sequence error.
|
||||
at_mu(now_mu() & ~7)
|
||||
if phase_mode != PHASE_MODE_CONTINUOUS:
|
||||
# Auto-clear phase accumulator on IO_UPDATE.
|
||||
# This is active already for the next IO_UPDATE
|
||||
self.set_cfr1(phase_autoclear=1)
|
||||
if phase_mode == PHASE_MODE_TRACKING and ref_time_mu < 0:
|
||||
# set default fiducial time stamp
|
||||
ref_time_mu = 0
|
||||
if ref_time_mu >= 0:
|
||||
# 32 LSB are sufficient.
|
||||
# Also no need to use IO_UPDATE time as this
|
||||
# is equivalent to an output pipeline latency.
|
||||
dt = int32(now_mu()) - int32(ref_time_mu)
|
||||
pow_ += dt*ftw*self.sysclk_per_mu >> 16
|
||||
self.write64(_AD9910_REG_PROFILE0 + profile,
|
||||
(asf << 16) | (pow_ & 0xffff), ftw)
|
||||
delay_mu(int64(self.sync_data.io_update_delay))
|
||||
self.cpld.io_update.pulse_mu(8) # assumes 8 mu > t_SYN_CCLK
|
||||
at_mu(now_mu() & ~7) # clear fine TSC again
|
||||
if phase_mode != PHASE_MODE_CONTINUOUS:
|
||||
self.set_cfr1()
|
||||
# future IO_UPDATE will activate
|
||||
return pow_
|
||||
|
||||
@kernel
|
||||
def set_profile_ram(self, start, end, step=1, profile=0, nodwell_high=0,
|
||||
zero_crossing=0, mode=1):
|
||||
"""Set the RAM profile settings.
|
||||
|
||||
:param start: Profile start address in RAM.
|
||||
:param end: Profile end address in RAM (last address).
|
||||
:param step: Profile time step in units of t_DDS, typically 4 ns
|
||||
(default: 1).
|
||||
:param profile: Profile index (0 to 7) (default: 0).
|
||||
:param nodwell_high: No-dwell high bit (default: 0,
|
||||
see AD9910 documentation).
|
||||
:param zero_crossing: Zero crossing bit (default: 0,
|
||||
see AD9910 documentation).
|
||||
:param mode: Profile RAM mode (:const:`RAM_MODE_DIRECTSWITCH`,
|
||||
:const:`RAM_MODE_RAMPUP`, :const:`RAM_MODE_BIDIR_RAMP`,
|
||||
:const:`RAM_MODE_CONT_BIDIR_RAMP`, or
|
||||
:const:`RAM_MODE_CONT_RAMPUP`, default:
|
||||
:const:`RAM_MODE_RAMPUP`)
|
||||
"""
|
||||
hi = (step << 8) | (end >> 2)
|
||||
lo = ((end << 30) | (start << 14) | (nodwell_high << 5) |
|
||||
(zero_crossing << 3) | mode)
|
||||
self.write64(_AD9910_REG_PROFILE0 + profile, hi, lo)
|
||||
|
||||
@kernel
|
||||
def set_ftw(self, ftw):
|
||||
"""Set the value stored to the AD9910's frequency tuning word (FTW) register.
|
||||
|
||||
:param ftw: Frequency tuning word to be stored, range: 0 to 0xffffffff.
|
||||
"""
|
||||
self.write32(_AD9910_REG_FTW, ftw)
|
||||
|
||||
@kernel
|
||||
def set_asf(self, asf):
|
||||
"""Set the value stored to the AD9910's amplitude scale factor (ASF) register.
|
||||
|
||||
:param asf: Amplitude scale factor to be stored, range: 0 to 0x3fff.
|
||||
"""
|
||||
self.write32(_AD9910_REG_ASF, asf << 2)
|
||||
|
||||
@kernel
|
||||
def set_pow(self, pow_):
|
||||
"""Set the value stored to the AD9910's phase offset word (POW) register.
|
||||
|
||||
:param pow_: Phase offset word to be stored, range: 0 to 0xffff.
|
||||
"""
|
||||
self.write16(_AD9910_REG_POW, pow_)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def frequency_to_ftw(self, frequency) -> TInt32:
|
||||
"""Return the 32-bit frequency tuning word corresponding to the given
|
||||
frequency.
|
||||
"""
|
||||
return int32(round(self.ftw_per_hz*frequency))
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def ftw_to_frequency(self, ftw):
|
||||
"""Return the frequency corresponding to the given frequency tuning
|
||||
word.
|
||||
"""
|
||||
return ftw / self.ftw_per_hz
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def turns_to_pow(self, turns) -> TInt32:
|
||||
"""Return the 16-bit phase offset word corresponding to the given phase
|
||||
in turns."""
|
||||
return int32(round(turns*0x10000)) & int32(0xffff)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def pow_to_turns(self, pow_):
|
||||
"""Return the phase in turns corresponding to a given phase offset
|
||||
word."""
|
||||
return pow_/0x10000
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def amplitude_to_asf(self, amplitude) -> TInt32:
|
||||
"""Return 14-bit amplitude scale factor corresponding to given
|
||||
fractional amplitude."""
|
||||
code = int32(round(amplitude * 0x3fff))
|
||||
if code < 0 or code > 0x3fff:
|
||||
raise ValueError("Invalid AD9910 fractional amplitude!")
|
||||
return code
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def asf_to_amplitude(self, asf):
|
||||
"""Return amplitude as a fraction of full scale corresponding to given
|
||||
amplitude scale factor."""
|
||||
return asf / float(0x3fff)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def frequency_to_ram(self, frequency, ram):
|
||||
"""Convert frequency values to RAM profile data.
|
||||
|
||||
To be used with :const:`RAM_DEST_FTW`.
|
||||
|
||||
:param frequency: List of frequency values in Hz.
|
||||
:param ram: List to write RAM data into.
|
||||
Suitable for :meth:`write_ram`.
|
||||
"""
|
||||
for i in range(len(ram)):
|
||||
ram[i] = self.frequency_to_ftw(frequency[i])
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def turns_to_ram(self, turns, ram):
|
||||
"""Convert phase values to RAM profile data.
|
||||
|
||||
To be used with :const:`RAM_DEST_POW`.
|
||||
|
||||
:param turns: List of phase values in turns.
|
||||
:param ram: List to write RAM data into.
|
||||
Suitable for :meth:`write_ram`.
|
||||
"""
|
||||
for i in range(len(ram)):
|
||||
ram[i] = self.turns_to_pow(turns[i]) << 16
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def amplitude_to_ram(self, amplitude, ram):
|
||||
"""Convert amplitude values to RAM profile data.
|
||||
|
||||
To be used with :const:`RAM_DEST_ASF`.
|
||||
|
||||
:param amplitude: List of amplitude values in units of full scale.
|
||||
:param ram: List to write RAM data into.
|
||||
Suitable for :meth:`write_ram`.
|
||||
"""
|
||||
for i in range(len(ram)):
|
||||
ram[i] = self.amplitude_to_asf(amplitude[i]) << 18
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def turns_amplitude_to_ram(self, turns, amplitude, ram):
|
||||
"""Convert phase and amplitude values to RAM profile data.
|
||||
|
||||
To be used with :const:`RAM_DEST_POWASF`.
|
||||
|
||||
:param turns: List of phase values in turns.
|
||||
:param amplitude: List of amplitude values in units of full scale.
|
||||
:param ram: List to write RAM data into.
|
||||
Suitable for :meth:`write_ram`.
|
||||
"""
|
||||
for i in range(len(ram)):
|
||||
ram[i] = ((self.turns_to_pow(turns[i]) << 16) |
|
||||
self.amplitude_to_asf(amplitude[i]) << 2)
|
||||
|
||||
@kernel
|
||||
def set_frequency(self, frequency):
|
||||
"""Set the value stored to the AD9910's frequency tuning word (FTW) register.
|
||||
|
||||
:param frequency: frequency to be stored, in Hz.
|
||||
"""
|
||||
return self.set_ftw(self.frequency_to_ftw(frequency))
|
||||
|
||||
@kernel
|
||||
def set_amplitude(self, amplitude):
|
||||
"""Set the value stored to the AD9910's amplitude scale factor (ASF) register.
|
||||
|
||||
:param amplitude: amplitude to be stored, in units of full scale.
|
||||
"""
|
||||
return self.set_asf(self.amplitude_to_asf(amplitude))
|
||||
|
||||
@kernel
|
||||
def set_phase(self, turns):
|
||||
"""Set the value stored to the AD9910's phase offset word (POW) register.
|
||||
|
||||
:param turns: phase offset to be stored, in turns.
|
||||
"""
|
||||
return self.set_pow(self.turns_to_pow(turns))
|
||||
|
||||
@kernel
|
||||
def set(self, frequency, phase=0.0, amplitude=1.0,
|
||||
phase_mode=_PHASE_MODE_DEFAULT, ref_time_mu=int64(-1), profile=0):
|
||||
"""Set profile 0 data in SI units.
|
||||
|
||||
.. seealso:: :meth:`set_mu`
|
||||
|
||||
:param frequency: Frequency in Hz
|
||||
:param phase: Phase tuning word in turns
|
||||
:param amplitude: Amplitude in units of full scale
|
||||
:param phase_mode: Phase mode constant
|
||||
:param ref_time_mu: Fiducial time stamp in machine units
|
||||
:param profile: Profile to affect
|
||||
:return: Resulting phase offset in turns
|
||||
"""
|
||||
return self.pow_to_turns(self.set_mu(
|
||||
self.frequency_to_ftw(frequency), self.turns_to_pow(phase),
|
||||
self.amplitude_to_asf(amplitude), phase_mode, ref_time_mu,
|
||||
profile))
|
||||
|
||||
@kernel
|
||||
def set_att_mu(self, att):
|
||||
"""Set digital step attenuator in machine units.
|
||||
|
||||
This method will write the attenuator settings of all four channels.
|
||||
|
||||
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att_mu`
|
||||
|
||||
:param att: Attenuation setting, 8 bit digital.
|
||||
"""
|
||||
self.cpld.set_att_mu(self.chip_select - 4, att)
|
||||
|
||||
@kernel
|
||||
def set_att(self, att):
|
||||
"""Set digital step attenuator in SI units.
|
||||
|
||||
This method will write the attenuator settings of all four channels.
|
||||
|
||||
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att`
|
||||
|
||||
:param att: Attenuation in dB.
|
||||
"""
|
||||
self.cpld.set_att(self.chip_select - 4, att)
|
||||
|
||||
@kernel
|
||||
def cfg_sw(self, state):
|
||||
"""Set CPLD CFG RF switch state. The RF switch is controlled by the
|
||||
logical or of the CPLD configuration shift register
|
||||
RF switch bit and the SW TTL line (if used).
|
||||
|
||||
:param state: CPLD CFG RF switch bit
|
||||
"""
|
||||
self.cpld.cfg_sw(self.chip_select - 4, state)
|
||||
|
||||
@kernel
|
||||
def set_sync(self, in_delay, window):
|
||||
"""Set the relevant parameters in the multi device synchronization
|
||||
register. See the AD9910 datasheet for details. The SYNC clock
|
||||
generator preset value is set to zero, and the SYNC_OUT generator is
|
||||
disabled.
|
||||
|
||||
:param in_delay: SYNC_IN delay tap (0-31) in steps of ~75ps
|
||||
:param window: Symmetric SYNC_IN validation window (0-15) in
|
||||
steps of ~75ps for both hold and setup margin.
|
||||
"""
|
||||
self.write32(_AD9910_REG_SYNC,
|
||||
(window << 28) | # SYNC S/H validation delay
|
||||
(1 << 27) | # SYNC receiver enable
|
||||
(0 << 26) | # SYNC generator disable
|
||||
(0 << 25) | # SYNC generator SYS rising edge
|
||||
(0 << 18) | # SYNC preset
|
||||
(0 << 11) | # SYNC output delay
|
||||
(in_delay << 3)) # SYNC receiver delay
|
||||
|
||||
@kernel
|
||||
def clear_smp_err(self):
|
||||
"""Clear the SMP_ERR flag and enables SMP_ERR validity monitoring.
|
||||
|
||||
Violations of the SYNC_IN sample and hold margins will result in
|
||||
SMP_ERR being asserted. This then also activates the red LED on
|
||||
the respective Urukul channel.
|
||||
|
||||
Also modifies CFR2.
|
||||
"""
|
||||
self.write32(_AD9910_REG_CFR2, 0x01010020) # clear SMP_ERR
|
||||
self.cpld.io_update.pulse(1*us)
|
||||
self.write32(_AD9910_REG_CFR2, 0x01010000) # enable SMP_ERR
|
||||
self.cpld.io_update.pulse(1*us)
|
||||
|
||||
@kernel
|
||||
def tune_sync_delay(self, search_seed=15):
|
||||
"""Find a stable SYNC_IN delay.
|
||||
|
||||
This method first locates a valid SYNC_IN delay at zero validation
|
||||
window size (setup/hold margin) by scanning around `search_seed`. It
|
||||
then looks for similar valid delays at successively larger validation
|
||||
window sizes until none can be found. It then decreases the validation
|
||||
window a bit to provide some slack and stability and returns the
|
||||
optimal values.
|
||||
|
||||
This method and :meth:`tune_io_update_delay` can be run in any order.
|
||||
|
||||
:param search_seed: Start value for valid SYNC_IN delay search.
|
||||
Defaults to 15 (half range).
|
||||
:return: Tuple of optimal delay and window size.
|
||||
"""
|
||||
if not self.cpld.sync_div:
|
||||
raise ValueError("parent cpld does not drive SYNC")
|
||||
search_span = 31
|
||||
# FIXME https://github.com/sinara-hw/Urukul/issues/16
|
||||
# should both be 2-4 once kasli sync_in jitter is identified
|
||||
min_window = 0
|
||||
margin = 1 # 1*75ps setup and hold
|
||||
for window in range(16):
|
||||
next_seed = -1
|
||||
for in_delay in range(search_span - 2*window):
|
||||
# alternate search direction around search_seed
|
||||
if in_delay & 1:
|
||||
in_delay = -in_delay
|
||||
in_delay = search_seed + (in_delay >> 1)
|
||||
if in_delay < 0 or in_delay > 31:
|
||||
continue
|
||||
self.set_sync(in_delay, window)
|
||||
self.clear_smp_err()
|
||||
# integrate SMP_ERR statistics for a few hundred cycles
|
||||
delay(100*us)
|
||||
err = urukul_sta_smp_err(self.cpld.sta_read())
|
||||
delay(100*us) # slack
|
||||
if not (err >> (self.chip_select - 4)) & 1:
|
||||
next_seed = in_delay
|
||||
break
|
||||
if next_seed >= 0: # valid delay found, scan next window
|
||||
search_seed = next_seed
|
||||
continue
|
||||
elif window > min_window:
|
||||
# no valid delay found here, roll back and add margin
|
||||
window = max(min_window, window - 1 - margin)
|
||||
self.set_sync(search_seed, window)
|
||||
self.clear_smp_err()
|
||||
delay(100*us) # slack
|
||||
return search_seed, window
|
||||
else:
|
||||
break
|
||||
raise ValueError("no valid window/delay")
|
||||
|
||||
@kernel
|
||||
def measure_io_update_alignment(self, delay_start, delay_stop):
|
||||
"""Use the digital ramp generator to locate the alignment between
|
||||
IO_UPDATE and SYNC_CLK.
|
||||
|
||||
The ramp generator is set up to a linear frequency ramp
|
||||
(dFTW/t_SYNC_CLK=1) and started at a coarse RTIO time stamp plus
|
||||
`delay_start` and stopped at a coarse RTIO time stamp plus
|
||||
`delay_stop`.
|
||||
|
||||
:param delay_start: Start IO_UPDATE delay in machine units.
|
||||
:param delay_stop: Stop IO_UPDATE delay in machine units.
|
||||
:return: Odd/even SYNC_CLK cycle indicator.
|
||||
"""
|
||||
# set up DRG
|
||||
self.set_cfr1(drg_load_lrr=1, drg_autoclear=1)
|
||||
# DRG -> FTW, DRG enable
|
||||
self.write32(_AD9910_REG_CFR2, 0x01090000)
|
||||
# no limits
|
||||
self.write64(_AD9910_REG_RAMP_LIMIT, -1, 0)
|
||||
# DRCTL=0, dt=1 t_SYNC_CLK
|
||||
self.write32(_AD9910_REG_RAMP_RATE, 0x00010000)
|
||||
# dFTW = 1, (work around negative slope)
|
||||
self.write64(_AD9910_REG_RAMP_STEP, -1, 0)
|
||||
# delay io_update after RTIO edge
|
||||
t = now_mu() + 8 & ~7
|
||||
at_mu(t + delay_start)
|
||||
# assumes a maximum t_SYNC_CLK period
|
||||
self.cpld.io_update.pulse_mu(16 - delay_start) # realign
|
||||
# disable DRG autoclear and LRR on io_update
|
||||
self.set_cfr1()
|
||||
# stop DRG
|
||||
self.write64(_AD9910_REG_RAMP_STEP, 0, 0)
|
||||
at_mu(t + 0x1000 + delay_stop)
|
||||
self.cpld.io_update.pulse_mu(16 - delay_stop) # realign
|
||||
ftw = self.read32(_AD9910_REG_FTW) # read out effective FTW
|
||||
delay(100*us) # slack
|
||||
# disable DRG
|
||||
self.write32(_AD9910_REG_CFR2, 0x01010000)
|
||||
self.cpld.io_update.pulse_mu(8)
|
||||
return ftw & 1
|
||||
|
||||
@kernel
|
||||
def tune_io_update_delay(self):
|
||||
"""Find a stable IO_UPDATE delay alignment.
|
||||
|
||||
Scan through increasing IO_UPDATE delays until a delay is found that
|
||||
lets IO_UPDATE be registered in the next SYNC_CLK cycle. Return a
|
||||
IO_UPDATE delay that is as far away from that SYNC_CLK edge
|
||||
as possible.
|
||||
|
||||
This method assumes that the IO_UPDATE TTLOut device has one machine
|
||||
unit resolution (SERDES).
|
||||
|
||||
This method and :meth:`tune_sync_delay` can be run in any order.
|
||||
|
||||
:return: Stable IO_UPDATE delay to be passed to the constructor
|
||||
:class:`AD9910` via the device database.
|
||||
"""
|
||||
period = self.sysclk_per_mu * 4 # SYNC_CLK period
|
||||
repeat = 100
|
||||
for i in range(period):
|
||||
t = 0
|
||||
# check whether the sync edge is strictly between i, i+2
|
||||
for j in range(repeat):
|
||||
t += self.measure_io_update_alignment(i, i + 2)
|
||||
if t != 0: # no certain edge
|
||||
continue
|
||||
# check left/right half: i,i+1 and i+1,i+2
|
||||
t1 = [0, 0]
|
||||
for j in range(repeat):
|
||||
t1[0] += self.measure_io_update_alignment(i, i + 1)
|
||||
t1[1] += self.measure_io_update_alignment(i + 1, i + 2)
|
||||
if ((t1[0] == 0 and t1[1] == 0) or
|
||||
(t1[0] == repeat and t1[1] == repeat)):
|
||||
# edge is not close to i + 1, can't interpret result
|
||||
raise ValueError(
|
||||
"no clear IO_UPDATE-SYNC_CLK alignment edge found")
|
||||
else:
|
||||
# the good delay is period//2 after the edge
|
||||
return (i + 1 + period//2) & (period - 1)
|
||||
raise ValueError("no IO_UPDATE-SYNC_CLK alignment edge found")
|
|
@ -1,55 +1,200 @@
|
|||
"""
|
||||
Driver for the AD9912 DDS.
|
||||
"""
|
||||
from numpy import int32, int64
|
||||
|
||||
from artiq.language.types import TInt32, TInt64
|
||||
from artiq.language.core import kernel, delay, portable
|
||||
from artiq.language.units import ms, us, ns
|
||||
from artiq.coredevice.ad9912_reg import *
|
||||
|
||||
from artiq.language.core import kernel, delay_mu
|
||||
from artiq.language.units import ns, us
|
||||
from artiq.coredevice import spi
|
||||
|
||||
|
||||
_AD9912_SPI_CONFIG = (0*spi.SPI_OFFLINE | 0*spi.SPI_CS_POLARITY |
|
||||
0*spi.SPI_CLK_POLARITY | 0*spi.SPI_CLK_PHASE |
|
||||
0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX)
|
||||
from artiq.coredevice import spi2 as spi
|
||||
from artiq.coredevice import urukul
|
||||
|
||||
|
||||
class AD9912:
|
||||
"""
|
||||
Support for the Analog devices AD9912 DDS
|
||||
AD9912 DDS channel on Urukul
|
||||
|
||||
:param spi_device: Name of the SPI bus this device is on.
|
||||
:param chip_select: Value to drive on the chip select lines
|
||||
during transactions.
|
||||
This class supports a single DDS channel and exposes the DDS,
|
||||
the digital step attenuator, and the RF switch.
|
||||
|
||||
:param chip_select: Chip select configuration. On Urukul this is an
|
||||
encoded chip select and not "one-hot".
|
||||
:param cpld_device: Name of the Urukul CPLD this device is on.
|
||||
:param sw_device: Name of the RF switch device. The RF switch is a
|
||||
TTLOut channel available as the :attr:`sw` attribute of this instance.
|
||||
:param pll_n: DDS PLL multiplier. The DDS sample clock is
|
||||
f_ref/clk_div*pll_n where f_ref is the reference frequency and clk_div
|
||||
is the reference clock divider (both set in the parent Urukul CPLD
|
||||
instance).
|
||||
"""
|
||||
kernel_invariants = {"chip_select", "cpld", "core", "bus", "ftw_per_hz"}
|
||||
|
||||
def __init__(self, dmgr, spi_device, chip_select):
|
||||
self.core = dmgr.get("core")
|
||||
self.bus = dmgr.get(spi_device)
|
||||
def __init__(self, dmgr, chip_select, cpld_device, sw_device=None,
|
||||
pll_n=10):
|
||||
self.cpld = dmgr.get(cpld_device)
|
||||
self.core = self.cpld.core
|
||||
self.bus = self.cpld.bus
|
||||
assert 4 <= chip_select <= 7
|
||||
self.chip_select = chip_select
|
||||
if sw_device:
|
||||
self.sw = dmgr.get(sw_device)
|
||||
self.kernel_invariants.add("sw")
|
||||
self.pll_n = pll_n
|
||||
sysclk = self.cpld.refclk/[1, 1, 2, 4][self.cpld.clk_div]*pll_n
|
||||
assert sysclk <= 1e9
|
||||
self.ftw_per_hz = 1/sysclk*(int64(1) << 48)
|
||||
|
||||
@kernel
|
||||
def setup_bus(self, write_div=5, read_div=20):
|
||||
"""Configure the SPI bus and the SPI transaction parameters
|
||||
for this device. This method has to be called before any other method
|
||||
if the bus has been used to access a different device in the meantime.
|
||||
def write(self, addr, data, length):
|
||||
"""Variable length write to a register.
|
||||
Up to 4 bytes.
|
||||
|
||||
This method advances the timeline by the duration of two
|
||||
RTIO-to-Wishbone bus transactions.
|
||||
|
||||
:param write_div: Write clock divider.
|
||||
:param read_div: Read clock divider.
|
||||
:param addr: Register address
|
||||
:param data: Data to be written: int32
|
||||
:param length: Length in bytes (1-4)
|
||||
"""
|
||||
# write: 5*8ns >= 40ns = t_clk (typ clk rate)
|
||||
# read: 2*8*ns >= 25ns = t_dv (clk falling to miso valid) + RTT
|
||||
self.bus.set_config_mu(_AD9912_SPI_CONFIG, write_div, read_div)
|
||||
self.bus.set_xfer(self.chip_select, 24, 0)
|
||||
assert length > 0
|
||||
assert length <= 4
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 16,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write((addr | ((length - 1) << 13)) << 16)
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, length*8,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write(data << (32 - length*8))
|
||||
|
||||
@kernel
|
||||
def write(self, data):
|
||||
"""Write 24 bits of data.
|
||||
def read(self, addr, length):
|
||||
"""Variable length read from a register.
|
||||
Up to 4 bytes.
|
||||
|
||||
This method advances the timeline by the duration of the SPI transfer
|
||||
and the required CS high time.
|
||||
:param addr: Register address
|
||||
:param length: Length in bytes (1-4)
|
||||
:return: Data read
|
||||
"""
|
||||
self.bus.write(data << 8)
|
||||
delay_mu(self.bus.ref_period_mu) # get to 20ns min cs high
|
||||
assert length > 0
|
||||
assert length <= 4
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 16,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write((addr | ((length - 1) << 13) | 0x8000) << 16)
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END
|
||||
| spi.SPI_INPUT, length*8,
|
||||
urukul.SPIT_DDS_RD, self.chip_select)
|
||||
self.bus.write(0)
|
||||
data = self.bus.read()
|
||||
if length < 4:
|
||||
data &= (1 << (length*8)) - 1
|
||||
return data
|
||||
|
||||
@kernel
|
||||
def init(self):
|
||||
"""Initialize and configure the DDS.
|
||||
|
||||
Sets up SPI mode, confirms chip presence, powers down unused blocks,
|
||||
and configures the PLL. Does not wait for PLL lock. Uses the
|
||||
IO_UPDATE signal multiple times.
|
||||
"""
|
||||
# SPI mode
|
||||
self.write(AD9912_SER_CONF, 0x99, length=1)
|
||||
self.cpld.io_update.pulse(2*us)
|
||||
# Verify chip ID and presence
|
||||
prodid = self.read(AD9912_PRODIDH, length=2)
|
||||
if (prodid != 0x1982) and (prodid != 0x1902):
|
||||
raise ValueError("Urukul AD9912 product id mismatch")
|
||||
delay(50*us)
|
||||
# HSTL power down, CMOS power down
|
||||
self.write(AD9912_PWRCNTRL1, 0x80, length=1)
|
||||
self.cpld.io_update.pulse(2*us)
|
||||
self.write(AD9912_N_DIV, self.pll_n//2 - 2, length=1)
|
||||
self.cpld.io_update.pulse(2*us)
|
||||
# I_cp = 375 µA, VCO high range
|
||||
self.write(AD9912_PLLCFG, 0b00000101, length=1)
|
||||
self.cpld.io_update.pulse(2*us)
|
||||
delay(1*ms)
|
||||
|
||||
@kernel
|
||||
def set_att_mu(self, att):
|
||||
"""Set digital step attenuator in machine units.
|
||||
|
||||
This method will write the attenuator settings of all four channels.
|
||||
|
||||
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att_mu`
|
||||
|
||||
:param att: Attenuation setting, 8 bit digital.
|
||||
"""
|
||||
self.cpld.set_att_mu(self.chip_select - 4, att)
|
||||
|
||||
@kernel
|
||||
def set_att(self, att):
|
||||
"""Set digital step attenuator in SI units.
|
||||
|
||||
This method will write the attenuator settings of all four channels.
|
||||
|
||||
.. seealso:: :meth:`artiq.coredevice.urukul.CPLD.set_att`
|
||||
|
||||
:param att: Attenuation in dB. Higher values mean more attenuation.
|
||||
"""
|
||||
self.cpld.set_att(self.chip_select - 4, att)
|
||||
|
||||
@kernel
|
||||
def set_mu(self, ftw, pow):
|
||||
"""Set profile 0 data in machine units.
|
||||
|
||||
After the SPI transfer, the shared IO update pin is pulsed to
|
||||
activate the data.
|
||||
|
||||
:param ftw: Frequency tuning word: 48 bit unsigned.
|
||||
:param pow: Phase tuning word: 16 bit unsigned.
|
||||
"""
|
||||
# streaming transfer of FTW and POW
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 16,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write((AD9912_POW1 << 16) | (3 << 29))
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG, 32,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write((pow << 16) | (int32(ftw >> 32) & 0xffff))
|
||||
self.bus.set_config_mu(urukul.SPI_CONFIG | spi.SPI_END, 32,
|
||||
urukul.SPIT_DDS_WR, self.chip_select)
|
||||
self.bus.write(int32(ftw))
|
||||
self.cpld.io_update.pulse(10*ns)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def frequency_to_ftw(self, frequency) -> TInt64:
|
||||
"""Returns the 48-bit frequency tuning word corresponding to the given
|
||||
frequency.
|
||||
"""
|
||||
return int64(round(self.ftw_per_hz*frequency)) & ((int64(1) << 48) - 1)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def ftw_to_frequency(self, ftw):
|
||||
"""Returns the frequency corresponding to the given
|
||||
frequency tuning word.
|
||||
"""
|
||||
return ftw/self.ftw_per_hz
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def turns_to_pow(self, phase) -> TInt32:
|
||||
"""Returns the 16-bit phase offset word corresponding to the given
|
||||
phase.
|
||||
"""
|
||||
return int32(round((1 << 14)*phase)) & 0xffff
|
||||
|
||||
@kernel
|
||||
def set(self, frequency, phase=0.0):
|
||||
"""Set profile 0 data in SI units.
|
||||
|
||||
.. seealso:: :meth:`set_mu`
|
||||
|
||||
:param ftw: Frequency in Hz
|
||||
:param pow: Phase tuning word in turns
|
||||
"""
|
||||
self.set_mu(self.frequency_to_ftw(frequency),
|
||||
self.turns_to_pow(phase))
|
||||
|
||||
@kernel
|
||||
def cfg_sw(self, state):
|
||||
"""Set CPLD CFG RF switch state. The RF switch is controlled by the
|
||||
logical or of the CPLD configuration shift register
|
||||
RF switch bit and the SW TTL line (if used).
|
||||
|
||||
:param state: CPLD CFG RF switch bit
|
||||
"""
|
||||
self.cpld.cfg_sw(self.chip_select - 4, state)
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
"""
|
||||
Driver for the AD9914 DDS (with parallel bus) on RTIO.
|
||||
"""
|
||||
|
||||
|
||||
from artiq.language.core import *
|
||||
from artiq.language.types import *
|
||||
from artiq.language.units import *
|
||||
from artiq.coredevice.rtio import rtio_output
|
||||
|
||||
from numpy import int32, int64
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AD9914",
|
||||
"PHASE_MODE_CONTINUOUS", "PHASE_MODE_ABSOLUTE", "PHASE_MODE_TRACKING"
|
||||
]
|
||||
|
||||
|
||||
_PHASE_MODE_DEFAULT = -1
|
||||
PHASE_MODE_CONTINUOUS = 0
|
||||
PHASE_MODE_ABSOLUTE = 1
|
||||
PHASE_MODE_TRACKING = 2
|
||||
|
||||
AD9914_REG_CFR1L = 0x01
|
||||
AD9914_REG_CFR1H = 0x03
|
||||
AD9914_REG_CFR2L = 0x05
|
||||
AD9914_REG_CFR2H = 0x07
|
||||
AD9914_REG_CFR3L = 0x09
|
||||
AD9914_REG_CFR3H = 0x0b
|
||||
AD9914_REG_CFR4L = 0x0d
|
||||
AD9914_REG_CFR4H = 0x0f
|
||||
AD9914_REG_DRGFL = 0x11
|
||||
AD9914_REG_DRGFH = 0x13
|
||||
AD9914_REG_DRGBL = 0x15
|
||||
AD9914_REG_DRGBH = 0x17
|
||||
AD9914_REG_DRGAL = 0x19
|
||||
AD9914_REG_DRGAH = 0x1b
|
||||
AD9914_REG_POW = 0x31
|
||||
AD9914_REG_ASF = 0x33
|
||||
AD9914_REG_USR0 = 0x6d
|
||||
AD9914_FUD = 0x80
|
||||
AD9914_GPIO = 0x81
|
||||
|
||||
|
||||
class AD9914:
|
||||
"""Driver for one AD9914 DDS channel.
|
||||
|
||||
The time cursor is not modified by any function in this class.
|
||||
|
||||
Output event replacement is not supported and issuing commands at the same
|
||||
time is an error.
|
||||
|
||||
:param sysclk: DDS system frequency. The DDS system clock must be a
|
||||
phase-locked multiple of the RTIO clock.
|
||||
:param bus_channel: RTIO channel number of the DDS bus.
|
||||
:param channel: channel number (on the bus) of the DDS device to control.
|
||||
"""
|
||||
|
||||
kernel_invariants = {"core", "sysclk", "bus_channel", "channel",
|
||||
"rtio_period_mu", "sysclk_per_mu", "write_duration_mu",
|
||||
"dac_cal_duration_mu", "init_duration_mu", "init_sync_duration_mu",
|
||||
"set_duration_mu", "set_x_duration_mu", "exit_x_duration_mu"}
|
||||
|
||||
def __init__(self, dmgr, sysclk, bus_channel, channel, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.sysclk = sysclk
|
||||
self.bus_channel = bus_channel
|
||||
self.channel = channel
|
||||
self.phase_mode = PHASE_MODE_CONTINUOUS
|
||||
|
||||
self.rtio_period_mu = int64(8)
|
||||
self.sysclk_per_mu = int32(self.sysclk * self.core.ref_period)
|
||||
|
||||
self.write_duration_mu = 5 * self.rtio_period_mu
|
||||
self.dac_cal_duration_mu = 147000 * self.rtio_period_mu
|
||||
self.init_duration_mu = 13 * self.write_duration_mu + self.dac_cal_duration_mu
|
||||
self.init_sync_duration_mu = 21 * self.write_duration_mu + 2 * self.dac_cal_duration_mu
|
||||
self.set_duration_mu = 7 * self.write_duration_mu
|
||||
self.set_x_duration_mu = 7 * self.write_duration_mu
|
||||
self.exit_x_duration_mu = 3 * self.write_duration_mu
|
||||
|
||||
@kernel
|
||||
def write(self, addr, data):
|
||||
rtio_output((self.bus_channel << 8) | addr, data)
|
||||
delay_mu(self.write_duration_mu)
|
||||
|
||||
@kernel
|
||||
def init(self):
|
||||
"""Resets and initializes the DDS channel.
|
||||
|
||||
This needs to be done for each DDS channel before it can be used, and
|
||||
it is recommended to use the startup kernel for this purpose.
|
||||
"""
|
||||
delay_mu(-self.init_duration_mu)
|
||||
self.write(AD9914_GPIO, (1 << self.channel) << 1);
|
||||
|
||||
# Note another undocumented "feature" of the AD9914:
|
||||
# Programmable modulus breaks if the digital ramp enable bit is
|
||||
# not set at the same time.
|
||||
self.write(AD9914_REG_CFR1H, 0x0000) # Enable cosine output
|
||||
self.write(AD9914_REG_CFR2L, 0x8900) # Enable matched latency
|
||||
self.write(AD9914_REG_CFR2H, 0x0089) # Enable profile mode + programmable modulus + DRG
|
||||
self.write(AD9914_REG_DRGAL, 0) # Programmable modulus A = 0
|
||||
self.write(AD9914_REG_DRGAH, 0)
|
||||
self.write(AD9914_REG_DRGBH, 0x8000) # Programmable modulus B == 2**31
|
||||
self.write(AD9914_REG_DRGBL, 0x0000)
|
||||
self.write(AD9914_REG_ASF, 0x0fff) # Set amplitude to maximum
|
||||
self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||
self.write(AD9914_FUD, 0)
|
||||
delay_mu(self.dac_cal_duration_mu)
|
||||
self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||
self.write(AD9914_FUD, 0)
|
||||
|
||||
@kernel
|
||||
def init_sync(self, sync_delay):
|
||||
"""Resets and initializes the DDS channel as well as configures
|
||||
the AD9914 DDS for synchronisation. The synchronisation procedure
|
||||
follows the steps outlined in the AN-1254 application note.
|
||||
|
||||
This needs to be done for each DDS channel before it can be used, and
|
||||
it is recommended to use the startup kernel for this.
|
||||
|
||||
This function cannot be used in a batch; the correct way of
|
||||
initializing multiple DDS channels is to call this function
|
||||
sequentially with a delay between the calls. 10ms provides a good
|
||||
timing margin.
|
||||
|
||||
:param sync_delay: integer from 0 to 0x3f that sets the value of
|
||||
SYNC_OUT (bits 3-5) and SYNC_IN (bits 0-2) delay ADJ bits.
|
||||
"""
|
||||
delay_mu(-self.init_sync_duration_mu)
|
||||
self.write(AD9914_GPIO, (1 << self.channel) << 1)
|
||||
|
||||
self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||
self.write(AD9914_FUD, 0)
|
||||
delay_mu(self.dac_cal_duration_mu)
|
||||
self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||
self.write(AD9914_FUD, 0)
|
||||
self.write(AD9914_REG_CFR2L, 0x8b00) # Enable matched latency and sync_out
|
||||
self.write(AD9914_FUD, 0)
|
||||
# Set cal with sync and set sync_out and sync_in delay
|
||||
self.write(AD9914_REG_USR0, 0x0840 | (sync_delay & 0x3f))
|
||||
self.write(AD9914_FUD, 0)
|
||||
self.write(AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||
self.write(AD9914_FUD, 0)
|
||||
delay_mu(self.dac_cal_duration_mu)
|
||||
self.write(AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||
self.write(AD9914_FUD, 0)
|
||||
self.write(AD9914_REG_CFR1H, 0x0000) # Enable cosine output
|
||||
self.write(AD9914_REG_CFR2H, 0x0089) # Enable profile mode + programmable modulus + DRG
|
||||
self.write(AD9914_REG_DRGAL, 0) # Programmable modulus A = 0
|
||||
self.write(AD9914_REG_DRGAH, 0)
|
||||
self.write(AD9914_REG_DRGBH, 0x8000) # Programmable modulus B == 2**31
|
||||
self.write(AD9914_REG_DRGBL, 0x0000)
|
||||
self.write(AD9914_REG_ASF, 0x0fff) # Set amplitude to maximum
|
||||
self.write(AD9914_FUD, 0)
|
||||
|
||||
@kernel
|
||||
def set_phase_mode(self, phase_mode):
|
||||
"""Sets the phase mode of the DDS channel. Supported phase modes are:
|
||||
|
||||
* :const:`PHASE_MODE_CONTINUOUS`: the phase accumulator is unchanged when
|
||||
switching frequencies. The DDS phase is the sum of the phase
|
||||
accumulator and the phase offset. The only discrete jumps in the
|
||||
DDS output phase come from changes to the phase offset.
|
||||
|
||||
* :const:`PHASE_MODE_ABSOLUTE`: the phase accumulator is reset when
|
||||
switching frequencies. Thus, the phase of the DDS at the time of
|
||||
the frequency change is equal to the phase offset.
|
||||
|
||||
* :const:`PHASE_MODE_TRACKING`: when switching frequencies, the phase
|
||||
accumulator is set to the value it would have if the DDS had been
|
||||
running at the specified frequency since the start of the
|
||||
experiment.
|
||||
|
||||
.. warning:: This setting may become inconsistent when used as part of
|
||||
a DMA recording. When using DMA, it is recommended to specify the
|
||||
phase mode explicitly when calling :meth:`set` or :meth:`set_mu`.
|
||||
"""
|
||||
self.phase_mode = phase_mode
|
||||
|
||||
@kernel
|
||||
def set_mu(self, ftw, pow=0, phase_mode=_PHASE_MODE_DEFAULT,
|
||||
asf=0x0fff, ref_time_mu=-1):
|
||||
"""Sets the DDS channel to the specified frequency and phase.
|
||||
|
||||
This uses machine units (FTW and POW). The frequency tuning word width
|
||||
is 32, the phase offset word width is 16, and the amplitude scale factor
|
||||
width is 12.
|
||||
|
||||
The "frequency update" pulse is sent to the DDS with a fixed latency
|
||||
with respect to the current position of the time cursor.
|
||||
|
||||
:param ftw: frequency to generate.
|
||||
:param pow: adds an offset to the phase.
|
||||
:param phase_mode: if specified, overrides the default phase mode set
|
||||
by :meth:`set_phase_mode` for this call.
|
||||
:param ref_time_mu: reference time used to compute phase. Specifying this
|
||||
makes it easier to have a well-defined phase relationship between
|
||||
DDSes on the same bus that are updated at a similar time.
|
||||
:return: Resulting phase offset word after application of phase
|
||||
tracking offset. When using :const:`PHASE_MODE_CONTINUOUS` in
|
||||
subsequent calls, use this value as the "current" phase.
|
||||
"""
|
||||
if phase_mode == _PHASE_MODE_DEFAULT:
|
||||
phase_mode = self.phase_mode
|
||||
if ref_time_mu < 0:
|
||||
ref_time_mu = now_mu()
|
||||
delay_mu(-self.set_duration_mu)
|
||||
|
||||
self.write(AD9914_GPIO, (1 << self.channel) << 1)
|
||||
|
||||
self.write(AD9914_REG_DRGFL, ftw & 0xffff)
|
||||
self.write(AD9914_REG_DRGFH, (ftw >> 16) & 0xffff)
|
||||
|
||||
# We need the RTIO fine timestamp clock to be phase-locked
|
||||
# to DDS SYSCLK, and divided by an integer self.sysclk_per_mu.
|
||||
if phase_mode == PHASE_MODE_CONTINUOUS:
|
||||
# Do not clear phase accumulator on FUD
|
||||
# Disable autoclear phase accumulator and enables OSK.
|
||||
self.write(AD9914_REG_CFR1L, 0x0108)
|
||||
else:
|
||||
# Clear phase accumulator on FUD
|
||||
# Enable autoclear phase accumulator and enables OSK.
|
||||
self.write(AD9914_REG_CFR1L, 0x2108)
|
||||
fud_time = now_mu() + 2 * self.write_duration_mu
|
||||
pow -= int32((ref_time_mu - fud_time) * self.sysclk_per_mu * ftw >> (32 - 16))
|
||||
if phase_mode == PHASE_MODE_TRACKING:
|
||||
pow += int32(ref_time_mu * self.sysclk_per_mu * ftw >> (32 - 16))
|
||||
|
||||
self.write(AD9914_REG_POW, pow)
|
||||
self.write(AD9914_REG_ASF, asf)
|
||||
self.write(AD9914_FUD, 0)
|
||||
return pow
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def frequency_to_ftw(self, frequency):
|
||||
"""Returns the 32-bit frequency tuning word corresponding to the given
|
||||
frequency.
|
||||
"""
|
||||
return int32(round(float(int64(2)**32*frequency/self.sysclk)))
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def ftw_to_frequency(self, ftw):
|
||||
"""Returns the frequency corresponding to the given frequency tuning
|
||||
word.
|
||||
"""
|
||||
return ftw*self.sysclk/int64(2)**32
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def turns_to_pow(self, turns):
|
||||
"""Returns the 16-bit phase offset word corresponding to the given
|
||||
phase in turns."""
|
||||
return round(float(turns*2**16)) & 0xffff
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def pow_to_turns(self, pow):
|
||||
"""Returns the phase in turns corresponding to the given phase offset
|
||||
word."""
|
||||
return pow/2**16
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def amplitude_to_asf(self, amplitude):
|
||||
"""Returns 12-bit amplitude scale factor corresponding to given
|
||||
amplitude."""
|
||||
code = round(float(amplitude * 0x0fff))
|
||||
if code < 0 or code > 0xfff:
|
||||
raise ValueError("Invalid AD9914 amplitude!")
|
||||
return code
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def asf_to_amplitude(self, asf):
|
||||
"""Returns the amplitude corresponding to the given amplitude scale
|
||||
factor."""
|
||||
return asf/0x0fff
|
||||
|
||||
@kernel
|
||||
def set(self, frequency, phase=0.0, phase_mode=_PHASE_MODE_DEFAULT,
|
||||
amplitude=1.0):
|
||||
"""Like :meth:`set_mu`, but uses Hz and turns."""
|
||||
return self.pow_to_turns(
|
||||
self.set_mu(self.frequency_to_ftw(frequency),
|
||||
self.turns_to_pow(phase), phase_mode,
|
||||
self.amplitude_to_asf(amplitude)))
|
||||
|
||||
# Extended-resolution functions
|
||||
@kernel
|
||||
def set_x_mu(self, xftw, amplitude=0x0fff):
|
||||
"""Set the DDS frequency and amplitude with an extended-resolution
|
||||
(63-bit) frequency tuning word.
|
||||
|
||||
Phase control is not implemented in this mode; the phase offset
|
||||
can assume any value.
|
||||
|
||||
After this function has been called, exit extended-resolution mode
|
||||
before calling functions that use standard-resolution mode.
|
||||
"""
|
||||
delay_mu(-self.set_x_duration_mu)
|
||||
|
||||
self.write(AD9914_GPIO, (1 << self.channel) << 1)
|
||||
|
||||
self.write(AD9914_REG_DRGAL, xftw & 0xffff)
|
||||
self.write(AD9914_REG_DRGAH, (xftw >> 16) & 0x7fff)
|
||||
self.write(AD9914_REG_DRGFL, (xftw >> 31) & 0xffff)
|
||||
self.write(AD9914_REG_DRGFH, (xftw >> 47) & 0xffff)
|
||||
self.write(AD9914_REG_ASF, amplitude)
|
||||
|
||||
self.write(AD9914_FUD, 0)
|
||||
|
||||
@kernel
|
||||
def exit_x(self):
|
||||
"""Exits extended-resolution mode."""
|
||||
delay_mu(-self.exit_x_duration_mu)
|
||||
self.write(AD9914_GPIO, (1 << self.channel) << 1)
|
||||
self.write(AD9914_REG_DRGAL, 0)
|
||||
self.write(AD9914_REG_DRGAH, 0)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def frequency_to_xftw(self, frequency):
|
||||
"""Returns the 63-bit frequency tuning word corresponding to the given
|
||||
frequency (extended resolution mode).
|
||||
"""
|
||||
return int64(round(2.0*float(int64(2)**62)*frequency/self.sysclk)) & (
|
||||
(int64(1) << 63) - 1)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def xftw_to_frequency(self, xftw):
|
||||
"""Returns the frequency corresponding to the given frequency tuning
|
||||
word (extended resolution mode).
|
||||
"""
|
||||
return xftw*self.sysclk/(2.0*float(int64(2)**62))
|
||||
|
||||
@kernel
|
||||
def set_x(self, frequency, amplitude=1.0):
|
||||
"""Like :meth:`set_x_mu`, but uses Hz and turns.
|
||||
|
||||
Note that the precision of ``float`` is less than the precision
|
||||
of the extended frequency tuning word.
|
||||
"""
|
||||
self.set_x_mu(self.frequency_to_xftw(frequency),
|
||||
self.amplitude_to_asf(amplitude))
|
|
@ -0,0 +1,569 @@
|
|||
"""RTIO driver for the Analog Devices ADF[45]35[56] family of GHz PLLs
|
||||
on Mirny-style prefixed SPI buses.
|
||||
"""
|
||||
|
||||
# https://github.com/analogdevicesinc/linux/blob/master/Documentation/devicetree/bindings/iio/frequency/adf5355.txt
|
||||
# https://github.com/analogdevicesinc/linux/blob/master/drivers/iio/frequency/adf5355.c
|
||||
# https://www.analog.com/media/en/technical-documentation/data-sheets/ADF5355.pdf
|
||||
# https://www.analog.com/media/en/technical-documentation/data-sheets/ADF5355.pdf
|
||||
# https://www.analog.com/media/en/technical-documentation/user-guides/EV-ADF5355SD1Z-UG-1087.pdf
|
||||
|
||||
|
||||
from artiq.language.core import kernel, portable, delay
|
||||
from artiq.language.units import us, GHz, MHz
|
||||
from artiq.language.types import TInt32, TInt64
|
||||
from artiq.coredevice import spi2 as spi
|
||||
from artiq.coredevice.adf5356_reg import *
|
||||
|
||||
from numpy import int32, int64, floor, ceil
|
||||
|
||||
|
||||
SPI_CONFIG = (
|
||||
0 * spi.SPI_OFFLINE
|
||||
| 0 * spi.SPI_END
|
||||
| 0 * spi.SPI_INPUT
|
||||
| 1 * spi.SPI_CS_POLARITY
|
||||
| 0 * spi.SPI_CLK_POLARITY
|
||||
| 0 * spi.SPI_CLK_PHASE
|
||||
| 0 * spi.SPI_LSB_FIRST
|
||||
| 0 * spi.SPI_HALF_DUPLEX
|
||||
)
|
||||
|
||||
|
||||
ADF5356_MIN_VCO_FREQ = int64(3.4 * GHz)
|
||||
ADF5356_MAX_VCO_FREQ = int64(6.8 * GHz)
|
||||
ADF5356_MAX_FREQ_PFD = int32(125.0 * MHz)
|
||||
ADF5356_MODULUS1 = int32(1 << 24)
|
||||
ADF5356_MAX_MODULUS2 = int32(1 << 28) # FIXME: ADF5356 has 28 bits MOD2
|
||||
ADF5356_MAX_R_CNT = int32(1023)
|
||||
|
||||
|
||||
class ADF5356:
|
||||
"""Analog Devices AD[45]35[56] family of GHz PLLs.
|
||||
|
||||
:param cpld_device: Mirny CPLD device name
|
||||
:param sw_device: Mirny RF switch device name
|
||||
:param channel: Mirny RF channel index
|
||||
:param ref_doubler: enable/disable reference clock doubler
|
||||
:param ref_divider: enable/disable reference clock divide-by-2
|
||||
:param core_device: Core device name (default: "core")
|
||||
"""
|
||||
|
||||
kernel_invariants = {"cpld", "sw", "channel", "core", "sysclk"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dmgr,
|
||||
cpld_device,
|
||||
sw_device,
|
||||
channel,
|
||||
ref_doubler=False,
|
||||
ref_divider=False,
|
||||
core="core",
|
||||
):
|
||||
self.cpld = dmgr.get(cpld_device)
|
||||
self.sw = dmgr.get(sw_device)
|
||||
self.channel = channel
|
||||
self.core = dmgr.get(core)
|
||||
|
||||
self.ref_doubler = ref_doubler
|
||||
self.ref_divider = ref_divider
|
||||
self.sysclk = self.cpld.refclk
|
||||
assert 10 <= self.sysclk / 1e6 <= 600
|
||||
|
||||
self._init_registers()
|
||||
|
||||
@kernel
|
||||
def init(self, blind=False):
|
||||
"""
|
||||
Initialize and configure the PLL.
|
||||
|
||||
:param blind: Do not attempt to verify presence.
|
||||
"""
|
||||
if not blind:
|
||||
# MUXOUT = VDD
|
||||
self.write(ADF5356_REG4_MUXOUT(1) | 4)
|
||||
delay(5000 * us)
|
||||
if not self.read_muxout():
|
||||
raise ValueError("MUXOUT not high")
|
||||
delay(1000 * us)
|
||||
|
||||
# MUXOUT = DGND
|
||||
self.write(ADF5356_REG4_MUXOUT(2) | 4)
|
||||
delay(5000 * us)
|
||||
if self.read_muxout():
|
||||
raise ValueError("MUXOUT not low")
|
||||
delay(1000 * us)
|
||||
|
||||
@kernel
|
||||
def set_att_mu(self, att):
|
||||
"""Set digital step attenuator in machine units.
|
||||
|
||||
:param att: Attenuation setting, 8 bit digital.
|
||||
"""
|
||||
self.cpld.set_att_mu(self.channel, att)
|
||||
|
||||
@kernel
|
||||
def write(self, data):
|
||||
self.cpld.write_ext(self.channel | 4, 32, data)
|
||||
|
||||
@kernel
|
||||
def read_muxout(self):
|
||||
"""
|
||||
Read the state of the MUXOUT line.
|
||||
|
||||
By default, this is configured to be the digital lock detection.
|
||||
"""
|
||||
return bool(self.cpld.read_reg(0) & (1 << (self.channel + 8)))
|
||||
|
||||
@kernel
|
||||
def set_output_power_mu(self, n):
|
||||
"""
|
||||
Set the power level at output A of the PLL chip in machine units.
|
||||
|
||||
This driver defaults to `n = 3` at init.
|
||||
|
||||
:param n: output power setting, 0, 1, 2, or 3 (see ADF5356 datasheet, fig. 44).
|
||||
"""
|
||||
if n not in [0, 1, 2, 3]:
|
||||
raise ValueError("invalid power setting")
|
||||
self.regs[6] = ADF5356_REG6_RF_OUTPUT_A_POWER_UPDATE(self.regs[6], n)
|
||||
self.sync()
|
||||
|
||||
@portable
|
||||
def output_power_mu(self):
|
||||
"""
|
||||
Return the power level at output A of the PLL chip in machine units.
|
||||
"""
|
||||
return ADF5356_REG6_RF_OUTPUT_A_POWER_GET(self.regs[6])
|
||||
|
||||
@kernel
|
||||
def enable_output(self):
|
||||
"""
|
||||
Enable output A of the PLL chip. This is the default after init.
|
||||
"""
|
||||
self.regs[6] |= ADF5356_REG6_RF_OUTPUT_A_ENABLE(1)
|
||||
self.sync()
|
||||
|
||||
@kernel
|
||||
def disable_output(self):
|
||||
"""
|
||||
Disable output A of the PLL chip.
|
||||
"""
|
||||
self.regs[6] &= ~ADF5356_REG6_RF_OUTPUT_A_ENABLE(1)
|
||||
self.sync()
|
||||
|
||||
@kernel
|
||||
def set_frequency(self, f):
|
||||
"""
|
||||
Output given frequency on output A.
|
||||
|
||||
:param f: 53.125 MHz <= f <= 6800 MHz
|
||||
"""
|
||||
freq = int64(round(f))
|
||||
|
||||
if freq > ADF5356_MAX_VCO_FREQ:
|
||||
raise ValueError("Requested too high frequency")
|
||||
|
||||
# select minimal output divider
|
||||
rf_div_sel = 0
|
||||
while freq < ADF5356_MIN_VCO_FREQ:
|
||||
freq <<= 1
|
||||
rf_div_sel += 1
|
||||
|
||||
if (1 << rf_div_sel) > 64:
|
||||
raise ValueError("Requested too low frequency")
|
||||
|
||||
# choose reference divider that maximizes PFD frequency
|
||||
self.regs[4] = ADF5356_REG4_R_COUNTER_UPDATE(
|
||||
self.regs[4], self._compute_reference_counter()
|
||||
)
|
||||
f_pfd = self.f_pfd()
|
||||
|
||||
# choose prescaler
|
||||
if freq > int64(6e9):
|
||||
self.regs[0] |= ADF5356_REG0_PRESCALER(1) # 8/9
|
||||
n_min, n_max = 75, 65535
|
||||
|
||||
# adjust reference divider to be able to match n_min constraint
|
||||
while n_min * f_pfd > freq:
|
||||
r = ADF5356_REG4_R_COUNTER_GET(self.regs[4])
|
||||
self.regs[4] = ADF5356_REG4_R_COUNTER_UPDATE(self.regs[4], r + 1)
|
||||
f_pfd = self.f_pfd()
|
||||
else:
|
||||
self.regs[0] &= ~ADF5356_REG0_PRESCALER(1) # 4/5
|
||||
n_min, n_max = 23, 32767
|
||||
|
||||
# calculate PLL parameters
|
||||
n, frac1, (frac2_msb, frac2_lsb), (mod2_msb, mod2_lsb) = calculate_pll(
|
||||
freq, f_pfd
|
||||
)
|
||||
|
||||
if not (n_min <= n <= n_max):
|
||||
raise ValueError("Invalid INT value")
|
||||
|
||||
# configure PLL
|
||||
self.regs[0] = ADF5356_REG0_INT_VALUE_UPDATE(self.regs[0], n)
|
||||
self.regs[1] = ADF5356_REG1_MAIN_FRAC_VALUE_UPDATE(self.regs[1], frac1)
|
||||
self.regs[2] = ADF5356_REG2_AUX_FRAC_LSB_VALUE_UPDATE(self.regs[2], frac2_lsb)
|
||||
self.regs[2] = ADF5356_REG2_AUX_MOD_LSB_VALUE_UPDATE(self.regs[2], mod2_lsb)
|
||||
self.regs[13] = ADF5356_REG13_AUX_FRAC_MSB_VALUE_UPDATE(
|
||||
self.regs[13], frac2_msb
|
||||
)
|
||||
self.regs[13] = ADF5356_REG13_AUX_MOD_MSB_VALUE_UPDATE(self.regs[13], mod2_msb)
|
||||
|
||||
self.regs[6] = ADF5356_REG6_RF_DIVIDER_SELECT_UPDATE(self.regs[6], rf_div_sel)
|
||||
self.regs[6] = ADF5356_REG6_CP_BLEED_CURRENT_UPDATE(
|
||||
self.regs[6], int32(floor(24 * f_pfd / (61.44 * MHz)))
|
||||
)
|
||||
self.regs[9] = ADF5356_REG9_VCO_BAND_DIVISION_UPDATE(
|
||||
self.regs[9], int32(ceil(f_pfd / 160e3))
|
||||
)
|
||||
|
||||
# commit
|
||||
self.sync()
|
||||
|
||||
@kernel
|
||||
def sync(self):
|
||||
"""
|
||||
Write all registers to the device. Attempts to lock the PLL.
|
||||
"""
|
||||
f_pfd = self.f_pfd()
|
||||
|
||||
if f_pfd <= 75.0 * MHz:
|
||||
for i in range(13, 0, -1):
|
||||
self.write(self.regs[i])
|
||||
delay(200 * us)
|
||||
self.write(self.regs[0] | ADF5356_REG0_AUTOCAL(1))
|
||||
else:
|
||||
# AUTOCAL AT HALF PFD FREQUENCY
|
||||
|
||||
# calculate PLL at f_pfd/2
|
||||
n, frac1, (frac2_msb, frac2_lsb), (mod2_msb, mod2_lsb) = calculate_pll(
|
||||
self.f_vco(), f_pfd >> 1
|
||||
)
|
||||
|
||||
self.write(
|
||||
13
|
||||
| ADF5356_REG13_AUX_FRAC_MSB_VALUE(frac2_msb)
|
||||
| ADF5356_REG13_AUX_MOD_MSB_VALUE(mod2_msb)
|
||||
)
|
||||
|
||||
for i in range(12, 4, -1):
|
||||
self.write(self.regs[i])
|
||||
|
||||
self.write(
|
||||
ADF5356_REG4_R_COUNTER_UPDATE(self.regs[4], 2 * self.ref_counter())
|
||||
)
|
||||
|
||||
self.write(self.regs[3])
|
||||
self.write(
|
||||
2
|
||||
| ADF5356_REG2_AUX_MOD_LSB_VALUE(mod2_lsb)
|
||||
| ADF5356_REG2_AUX_FRAC_LSB_VALUE(frac2_lsb)
|
||||
)
|
||||
self.write(1 | ADF5356_REG1_MAIN_FRAC_VALUE(frac1))
|
||||
|
||||
delay(200 * us)
|
||||
self.write(ADF5356_REG0_INT_VALUE(n) | ADF5356_REG0_AUTOCAL(1))
|
||||
|
||||
# RELOCK AT WANTED PFD FREQUENCY
|
||||
|
||||
for i in [4, 2, 1]:
|
||||
self.write(self.regs[i])
|
||||
|
||||
# force-disable autocal
|
||||
self.write(self.regs[0] & ~ADF5356_REG0_AUTOCAL(1))
|
||||
|
||||
@portable
|
||||
def f_pfd(self) -> TInt64:
|
||||
"""
|
||||
Return the PFD frequency for the cached set of registers.
|
||||
"""
|
||||
r = ADF5356_REG4_R_COUNTER_GET(self.regs[4])
|
||||
d = ADF5356_REG4_R_DOUBLER_GET(self.regs[4])
|
||||
t = ADF5356_REG4_R_DIVIDER_GET(self.regs[4])
|
||||
return self._compute_pfd_frequency(r, d, t)
|
||||
|
||||
@portable
|
||||
def f_vco(self) -> TInt64:
|
||||
"""
|
||||
Return the VCO frequency for the cached set of registers.
|
||||
"""
|
||||
return int64(
|
||||
self.f_pfd()
|
||||
* (
|
||||
self.pll_n()
|
||||
+ (self.pll_frac1() + self.pll_frac2() / self.pll_mod2())
|
||||
/ ADF5356_MODULUS1
|
||||
)
|
||||
)
|
||||
|
||||
@portable
|
||||
def pll_n(self) -> TInt32:
|
||||
"""
|
||||
Return the PLL integer value (INT) for the cached set of registers.
|
||||
"""
|
||||
return ADF5356_REG0_INT_VALUE_GET(self.regs[0])
|
||||
|
||||
@portable
|
||||
def pll_frac1(self) -> TInt32:
|
||||
"""
|
||||
Return the main fractional value (FRAC1) for the cached set of registers.
|
||||
"""
|
||||
return ADF5356_REG1_MAIN_FRAC_VALUE_GET(self.regs[1])
|
||||
|
||||
@portable
|
||||
def pll_frac2(self) -> TInt32:
|
||||
"""
|
||||
Return the auxiliary fractional value (FRAC2) for the cached set of registers.
|
||||
"""
|
||||
return (
|
||||
ADF5356_REG13_AUX_FRAC_MSB_VALUE_GET(self.regs[13]) << 14
|
||||
) | ADF5356_REG2_AUX_FRAC_LSB_VALUE_GET(self.regs[2])
|
||||
|
||||
@portable
|
||||
def pll_mod2(self) -> TInt32:
|
||||
"""
|
||||
Return the auxiliary modulus value (MOD2) for the cached set of registers.
|
||||
"""
|
||||
return (
|
||||
ADF5356_REG13_AUX_MOD_MSB_VALUE_GET(self.regs[13]) << 14
|
||||
) | ADF5356_REG2_AUX_MOD_LSB_VALUE_GET(self.regs[2])
|
||||
|
||||
@portable
|
||||
def ref_counter(self) -> TInt32:
|
||||
"""
|
||||
Return the reference counter value (R) for the cached set of registers.
|
||||
"""
|
||||
return ADF5356_REG4_R_COUNTER_GET(self.regs[4])
|
||||
|
||||
@portable
|
||||
def output_divider(self) -> TInt32:
|
||||
"""
|
||||
Return the value of the output A divider.
|
||||
"""
|
||||
return 1 << ADF5356_REG6_RF_DIVIDER_SELECT_GET(self.regs[6])
|
||||
|
||||
def info(self):
|
||||
"""
|
||||
Return a summary of high-level parameters as a dict.
|
||||
"""
|
||||
prescaler = ADF5356_REG0_PRESCALER_GET(self.regs[0])
|
||||
return {
|
||||
# output
|
||||
"f_outA": self.f_vco() / self.output_divider(),
|
||||
"f_outB": self.f_vco() * 2,
|
||||
"output_divider": self.output_divider(),
|
||||
# PLL parameters
|
||||
"f_vco": self.f_vco(),
|
||||
"pll_n": self.pll_n(),
|
||||
"pll_frac1": self.pll_frac1(),
|
||||
"pll_frac2": self.pll_frac2(),
|
||||
"pll_mod2": self.pll_mod2(),
|
||||
"prescaler": "4/5" if prescaler == 0 else "8/9",
|
||||
# reference / PFD
|
||||
"sysclk": self.sysclk,
|
||||
"ref_doubler": self.ref_doubler,
|
||||
"ref_divider": self.ref_divider,
|
||||
"ref_counter": self.ref_counter(),
|
||||
"f_pfd": self.f_pfd(),
|
||||
}
|
||||
|
||||
@portable
|
||||
def _init_registers(self):
|
||||
"""
|
||||
Initialize cached registers with sensible defaults.
|
||||
"""
|
||||
# fill with control bits
|
||||
self.regs = [int32(i) for i in range(ADF5356_NUM_REGS)]
|
||||
|
||||
# REG2
|
||||
# ====
|
||||
|
||||
# avoid divide-by-zero
|
||||
self.regs[2] |= ADF5356_REG2_AUX_MOD_LSB_VALUE(1)
|
||||
|
||||
# REG4
|
||||
# ====
|
||||
|
||||
# single-ended reference mode is recommended
|
||||
# for references up to 250 MHz, even if the signal is differential
|
||||
if self.sysclk <= 250 * MHz:
|
||||
self.regs[4] |= ADF5356_REG4_REF_MODE(0)
|
||||
else:
|
||||
self.regs[4] |= ADF5356_REG4_REF_MODE(1)
|
||||
|
||||
# phase detector polarity: positive
|
||||
self.regs[4] |= ADF5356_REG4_PD_POLARITY(1)
|
||||
|
||||
# charge pump current: 0.94 mA
|
||||
self.regs[4] |= ADF5356_REG4_CURRENT_SETTING(2)
|
||||
|
||||
# MUXOUT: digital lock detect
|
||||
self.regs[4] |= ADF5356_REG4_MUX_LOGIC(1) # 3v3 logic
|
||||
self.regs[4] |= ADF5356_REG4_MUXOUT(6)
|
||||
|
||||
# setup reference path
|
||||
if self.ref_doubler:
|
||||
self.regs[4] |= ADF5356_REG4_R_DOUBLER(1)
|
||||
|
||||
if self.ref_divider:
|
||||
self.regs[4] |= ADF5356_REG4_R_DIVIDER(1)
|
||||
|
||||
r = self._compute_reference_counter()
|
||||
self.regs[4] |= ADF5356_REG4_R_COUNTER(r)
|
||||
|
||||
# REG5
|
||||
# ====
|
||||
|
||||
# reserved values
|
||||
self.regs[5] = int32(0x800025)
|
||||
|
||||
# REG6
|
||||
# ====
|
||||
|
||||
# reserved values
|
||||
self.regs[6] = int32(0x14000006)
|
||||
|
||||
# enable negative bleed
|
||||
self.regs[6] |= ADF5356_REG6_NEGATIVE_BLEED(1)
|
||||
|
||||
# charge pump bleed current
|
||||
# self.regs[6] |= ADF5356_REG6_CP_BLEED_CURRENT(
|
||||
# int32(floor(24 * self.f_pfd / (61.44 * MHz)))
|
||||
# )
|
||||
|
||||
# direct feedback from VCO to N counter
|
||||
self.regs[6] |= ADF5356_REG6_FB_SELECT(1)
|
||||
|
||||
# mute until the PLL is locked
|
||||
self.regs[6] |= ADF5356_REG6_MUTE_TILL_LD(1)
|
||||
|
||||
# enable output A
|
||||
self.regs[6] |= ADF5356_REG6_RF_OUTPUT_A_ENABLE(1)
|
||||
|
||||
# set output A power to max power, is adjusted by extra attenuator
|
||||
self.regs[6] |= ADF5356_REG6_RF_OUTPUT_A_POWER(3) # +5 dBm
|
||||
|
||||
# REG7
|
||||
# ====
|
||||
|
||||
# reserved values
|
||||
self.regs[7] = int32(0x10000007)
|
||||
|
||||
# sync load-enable to reference
|
||||
self.regs[7] |= ADF5356_REG7_LE_SYNC(1)
|
||||
|
||||
# frac-N lock-detect precision: 12 ns
|
||||
self.regs[7] |= ADF5356_REG7_FRAC_N_LD_PRECISION(3)
|
||||
|
||||
# REG8
|
||||
# ====
|
||||
|
||||
# reserved values
|
||||
self.regs[8] = int32(0x102D0428)
|
||||
|
||||
# REG9
|
||||
# ====
|
||||
|
||||
# default timeouts (from eval software)
|
||||
self.regs[9] |= (
|
||||
ADF5356_REG9_SYNTH_LOCK_TIMEOUT(13)
|
||||
| ADF5356_REG9_AUTOCAL_TIMEOUT(31)
|
||||
| ADF5356_REG9_TIMEOUT(0x67)
|
||||
)
|
||||
|
||||
# REG10
|
||||
# =====
|
||||
|
||||
# reserved values
|
||||
self.regs[10] = int32(0xC0000A)
|
||||
|
||||
# ADC defaults (from eval software)
|
||||
self.regs[10] |= (
|
||||
ADF5356_REG10_ADC_ENABLE(1)
|
||||
| ADF5356_REG10_ADC_CLK_DIV(256)
|
||||
| ADF5356_REG10_ADC_CONV(1)
|
||||
)
|
||||
|
||||
# REG11
|
||||
# =====
|
||||
|
||||
# reserved values
|
||||
self.regs[11] = int32(0x61200B)
|
||||
|
||||
# REG12
|
||||
# =====
|
||||
|
||||
# reserved values
|
||||
self.regs[12] = int32(0x15FC)
|
||||
|
||||
@portable
|
||||
def _compute_pfd_frequency(self, r, d, t) -> TInt64:
|
||||
"""
|
||||
Calculate the PFD frequency from the given reference path parameters
|
||||
"""
|
||||
return int64(self.sysclk * ((1 + d) / (r * (1 + t))))
|
||||
|
||||
@portable
|
||||
def _compute_reference_counter(self) -> TInt32:
|
||||
"""
|
||||
Determine the reference counter R that maximizes the PFD frequency
|
||||
"""
|
||||
d = ADF5356_REG4_R_DOUBLER_GET(self.regs[4])
|
||||
t = ADF5356_REG4_R_DIVIDER_GET(self.regs[4])
|
||||
r = 1
|
||||
while self._compute_pfd_frequency(r, d, t) > ADF5356_MAX_FREQ_PFD:
|
||||
r += 1
|
||||
return int32(r)
|
||||
|
||||
|
||||
@portable
|
||||
def gcd(a, b):
|
||||
while b:
|
||||
a, b = b, a % b
|
||||
return a
|
||||
|
||||
|
||||
@portable
|
||||
def split_msb_lsb_28b(v):
|
||||
return int32((v >> 14) & 0x3FFF), int32(v & 0x3FFF)
|
||||
|
||||
|
||||
@portable
|
||||
def calculate_pll(f_vco: TInt64, f_pfd: TInt64):
|
||||
"""
|
||||
Calculate fractional-N PLL parameters such that
|
||||
|
||||
``f_vco`` = ``f_pfd`` * (``n`` + (``frac1`` + ``frac2``/``mod2``) / ``mod1``)
|
||||
|
||||
where
|
||||
``mod1 = 2**24`` and ``mod2 <= 2**28``
|
||||
|
||||
:param f_vco: target VCO frequency
|
||||
:param f_pfd: PFD frequency
|
||||
:return: ``(n, frac1, (frac2_msb, frac2_lsb), (mod2_msb, mod2_lsb))``
|
||||
"""
|
||||
f_pfd = int64(f_pfd)
|
||||
f_vco = int64(f_vco)
|
||||
|
||||
# integral part
|
||||
n, r = int32(f_vco // f_pfd), f_vco % f_pfd
|
||||
|
||||
# main fractional part
|
||||
r *= ADF5356_MODULUS1
|
||||
frac1, frac2 = int32(r // f_pfd), r % f_pfd
|
||||
|
||||
# auxiliary fractional part
|
||||
mod2 = f_pfd
|
||||
|
||||
while mod2 > ADF5356_MAX_MODULUS2:
|
||||
mod2 >>= 1
|
||||
frac2 >>= 1
|
||||
|
||||
gcd_div = gcd(frac2, mod2)
|
||||
mod2 //= gcd_div
|
||||
frac2 //= gcd_div
|
||||
|
||||
return n, frac1, split_msb_lsb_28b(frac2), split_msb_lsb_28b(mod2)
|
|
@ -0,0 +1,642 @@
|
|||
# auto-generated, do not edit
|
||||
from artiq.language.core import portable
|
||||
from artiq.language.types import TInt32
|
||||
from numpy import int32
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_AUTOCAL_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 21) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_AUTOCAL(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 21)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_AUTOCAL_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 21)) | ((x & 0x1) << 21))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_INT_VALUE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0xffff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_INT_VALUE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0xffff) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_INT_VALUE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0xffff << 4)) | ((x & 0xffff) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_PRESCALER_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 20) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_PRESCALER(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 20)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG0_PRESCALER_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 20)) | ((x & 0x1) << 20))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG1_MAIN_FRAC_VALUE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0xffffff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG1_MAIN_FRAC_VALUE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0xffffff) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG1_MAIN_FRAC_VALUE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0xffffff << 4)) | ((x & 0xffffff) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG2_AUX_FRAC_LSB_VALUE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 18) & 0x3fff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG2_AUX_FRAC_LSB_VALUE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3fff) << 18)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG2_AUX_FRAC_LSB_VALUE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3fff << 18)) | ((x & 0x3fff) << 18))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG2_AUX_MOD_LSB_VALUE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0x3fff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG2_AUX_MOD_LSB_VALUE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3fff) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG2_AUX_MOD_LSB_VALUE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3fff << 4)) | ((x & 0x3fff) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_ADJUST_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 28) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_ADJUST(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 28)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_ADJUST_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 28)) | ((x & 0x1) << 28))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_RESYNC_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 29) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_RESYNC(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 29)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_RESYNC_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 29)) | ((x & 0x1) << 29))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_VALUE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0xffffff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_VALUE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0xffffff) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_PHASE_VALUE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0xffffff << 4)) | ((x & 0xffffff) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_SD_LOAD_RESET_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 30) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_SD_LOAD_RESET(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 30)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG3_SD_LOAD_RESET_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 30)) | ((x & 0x1) << 30))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_COUNTER_RESET_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_COUNTER_RESET(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_COUNTER_RESET_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 4)) | ((x & 0x1) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_CP_THREE_STATE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 5) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_CP_THREE_STATE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 5)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_CP_THREE_STATE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 5)) | ((x & 0x1) << 5))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_CURRENT_SETTING_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 10) & 0xf)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_CURRENT_SETTING(x: TInt32) -> TInt32:
|
||||
return int32((x & 0xf) << 10)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_CURRENT_SETTING_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0xf << 10)) | ((x & 0xf) << 10))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_DOUBLE_BUFF_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 14) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_DOUBLE_BUFF(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 14)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_DOUBLE_BUFF_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 14)) | ((x & 0x1) << 14))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_MUX_LOGIC_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 8) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_MUX_LOGIC(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 8)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_MUX_LOGIC_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 8)) | ((x & 0x1) << 8))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_MUXOUT_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 27) & 0x7)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_MUXOUT(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x7) << 27)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_MUXOUT_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x7 << 27)) | ((x & 0x7) << 27))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_PD_POLARITY_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 7) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_PD_POLARITY(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 7)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_PD_POLARITY_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 7)) | ((x & 0x1) << 7))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_POWER_DOWN_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 6) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_POWER_DOWN(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 6)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_POWER_DOWN_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 6)) | ((x & 0x1) << 6))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_COUNTER_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 15) & 0x3ff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_COUNTER(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3ff) << 15)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_COUNTER_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3ff << 15)) | ((x & 0x3ff) << 15))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_DIVIDER_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 25) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_DIVIDER(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 25)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_DIVIDER_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 25)) | ((x & 0x1) << 25))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_DOUBLER_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 26) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_DOUBLER(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 26)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_R_DOUBLER_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 26)) | ((x & 0x1) << 26))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_REF_MODE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 9) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_REF_MODE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 9)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG4_REF_MODE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 9)) | ((x & 0x1) << 9))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_BLEED_POLARITY_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 31) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_BLEED_POLARITY(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 31)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_BLEED_POLARITY_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 31)) | ((x & 0x1) << 31))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_CP_BLEED_CURRENT_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 13) & 0xff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_CP_BLEED_CURRENT(x: TInt32) -> TInt32:
|
||||
return int32((x & 0xff) << 13)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_CP_BLEED_CURRENT_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0xff << 13)) | ((x & 0xff) << 13))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_FB_SELECT_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 24) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_FB_SELECT(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 24)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_FB_SELECT_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 24)) | ((x & 0x1) << 24))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_GATE_BLEED_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 30) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_GATE_BLEED(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 30)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_GATE_BLEED_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 30)) | ((x & 0x1) << 30))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_MUTE_TILL_LD_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 11) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_MUTE_TILL_LD(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 11)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_MUTE_TILL_LD_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 11)) | ((x & 0x1) << 11))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_NEGATIVE_BLEED_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 29) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_NEGATIVE_BLEED(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 29)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_NEGATIVE_BLEED_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 29)) | ((x & 0x1) << 29))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_DIVIDER_SELECT_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 21) & 0x7)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_DIVIDER_SELECT(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x7) << 21)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_DIVIDER_SELECT_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x7 << 21)) | ((x & 0x7) << 21))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_A_ENABLE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 6) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_A_ENABLE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 6)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_A_ENABLE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 6)) | ((x & 0x1) << 6))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_A_POWER_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0x3)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_A_POWER(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_A_POWER_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3 << 4)) | ((x & 0x3) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_B_ENABLE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 10) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_B_ENABLE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 10)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG6_RF_OUTPUT_B_ENABLE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 10)) | ((x & 0x1) << 10))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_FRAC_N_LD_PRECISION_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 5) & 0x3)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_FRAC_N_LD_PRECISION(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3) << 5)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_FRAC_N_LD_PRECISION_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3 << 5)) | ((x & 0x3) << 5))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LD_CYCLE_COUNT_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 8) & 0x3)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LD_CYCLE_COUNT(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3) << 8)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LD_CYCLE_COUNT_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3 << 8)) | ((x & 0x3) << 8))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LD_MODE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LD_MODE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LD_MODE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 4)) | ((x & 0x1) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LE_SEL_SYNC_EDGE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 27) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LE_SEL_SYNC_EDGE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 27)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LE_SEL_SYNC_EDGE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 27)) | ((x & 0x1) << 27))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LE_SYNC_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 25) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LE_SYNC(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 25)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LE_SYNC_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 25)) | ((x & 0x1) << 25))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LOL_MODE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 7) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LOL_MODE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 7)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG7_LOL_MODE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 7)) | ((x & 0x1) << 7))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_AUTOCAL_TIMEOUT_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 9) & 0x1f)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_AUTOCAL_TIMEOUT(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1f) << 9)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_AUTOCAL_TIMEOUT_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1f << 9)) | ((x & 0x1f) << 9))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_SYNTH_LOCK_TIMEOUT_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0x1f)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_SYNTH_LOCK_TIMEOUT(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1f) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_SYNTH_LOCK_TIMEOUT_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1f << 4)) | ((x & 0x1f) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_TIMEOUT_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 14) & 0x3ff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_TIMEOUT(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3ff) << 14)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_TIMEOUT_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3ff << 14)) | ((x & 0x3ff) << 14))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_VCO_BAND_DIVISION_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 24) & 0xff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_VCO_BAND_DIVISION(x: TInt32) -> TInt32:
|
||||
return int32((x & 0xff) << 24)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG9_VCO_BAND_DIVISION_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0xff << 24)) | ((x & 0xff) << 24))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_CLK_DIV_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 6) & 0xff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_CLK_DIV(x: TInt32) -> TInt32:
|
||||
return int32((x & 0xff) << 6)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_CLK_DIV_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0xff << 6)) | ((x & 0xff) << 6))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_CONV_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 5) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_CONV(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 5)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_CONV_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 5)) | ((x & 0x1) << 5))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_ENABLE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_ENABLE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG10_ADC_ENABLE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 4)) | ((x & 0x1) << 4))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG11_VCO_BAND_HOLD_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 24) & 0x1)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG11_VCO_BAND_HOLD(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x1) << 24)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG11_VCO_BAND_HOLD_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x1 << 24)) | ((x & 0x1) << 24))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG12_PHASE_RESYNC_CLK_VALUE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 12) & 0xfffff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG12_PHASE_RESYNC_CLK_VALUE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0xfffff) << 12)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG12_PHASE_RESYNC_CLK_VALUE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0xfffff << 12)) | ((x & 0xfffff) << 12))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG13_AUX_FRAC_MSB_VALUE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 18) & 0x3fff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG13_AUX_FRAC_MSB_VALUE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3fff) << 18)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG13_AUX_FRAC_MSB_VALUE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3fff << 18)) | ((x & 0x3fff) << 18))
|
||||
|
||||
|
||||
@portable
|
||||
def ADF5356_REG13_AUX_MOD_MSB_VALUE_GET(reg: TInt32) -> TInt32:
|
||||
return int32((reg >> 4) & 0x3fff)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG13_AUX_MOD_MSB_VALUE(x: TInt32) -> TInt32:
|
||||
return int32((x & 0x3fff) << 4)
|
||||
|
||||
@portable
|
||||
def ADF5356_REG13_AUX_MOD_MSB_VALUE_UPDATE(reg: TInt32, x: TInt32) -> TInt32:
|
||||
return int32((reg & ~(0x3fff << 4)) | ((x & 0x3fff) << 4))
|
||||
|
||||
ADF5356_NUM_REGS = 14
|
|
@ -0,0 +1,79 @@
|
|||
from artiq.language.core import kernel, portable, delay
|
||||
from artiq.language.units import us, ms
|
||||
from artiq.coredevice.shiftreg import ShiftReg
|
||||
|
||||
|
||||
@portable
|
||||
def to_mu(att):
|
||||
return round(att*2.0) ^ 0x3f
|
||||
|
||||
@portable
|
||||
def from_mu(att_mu):
|
||||
return 0.5*(att_mu ^ 0x3f)
|
||||
|
||||
|
||||
class BaseModAtt:
|
||||
def __init__(self, dmgr, rst_n, clk, le, mosi, miso):
|
||||
self.rst_n = dmgr.get(rst_n)
|
||||
self.shift_reg = ShiftReg(dmgr,
|
||||
clk=clk, ser=mosi, latch=le, ser_in=miso, n=8*4)
|
||||
|
||||
@kernel
|
||||
def reset(self):
|
||||
# HMC's incompetence in digital design and interfaces means that
|
||||
# the HMC542 needs a level low on RST_N and then a rising edge
|
||||
# on Latch Enable. Their "latch" isn't a latch but a DFF.
|
||||
# Of course, it also powers up with a random attenuation, and
|
||||
# that cannot be fixed with simple pull-ups/pull-downs.
|
||||
self.rst_n.off()
|
||||
self.shift_reg.latch.off()
|
||||
delay(1*us)
|
||||
self.shift_reg.latch.on()
|
||||
delay(1*us)
|
||||
self.shift_reg.latch.off()
|
||||
self.rst_n.on()
|
||||
delay(1*us)
|
||||
|
||||
@kernel
|
||||
def set_mu(self, att0, att1, att2, att3):
|
||||
"""
|
||||
Sets the four attenuators on BaseMod.
|
||||
The values are in half decibels, between 0 (no attenuation)
|
||||
and 63 (31.5dB attenuation).
|
||||
"""
|
||||
word = (
|
||||
(att0 << 2) |
|
||||
(att1 << 10) |
|
||||
(att2 << 18) |
|
||||
(att3 << 26)
|
||||
)
|
||||
self.shift_reg.set(word)
|
||||
|
||||
@kernel
|
||||
def get_mu(self):
|
||||
"""
|
||||
Retrieves the current settings of the four attenuators on BaseMod.
|
||||
"""
|
||||
word = self.shift_reg.get()
|
||||
att0 = (word >> 2) & 0x3f
|
||||
att1 = (word >> 10) & 0x3f
|
||||
att2 = (word >> 18) & 0x3f
|
||||
att3 = (word >> 26) & 0x3f
|
||||
return att0, att1, att2, att3
|
||||
|
||||
@kernel
|
||||
def set(self, att0, att1, att2, att3):
|
||||
"""
|
||||
Sets the four attenuators on BaseMod.
|
||||
The values are in decibels.
|
||||
"""
|
||||
self.set_mu(to_mu(att0), to_mu(att1), to_mu(att2), to_mu(att3))
|
||||
|
||||
@kernel
|
||||
def get(self):
|
||||
"""
|
||||
Retrieves the current settings of the four attenuators on BaseMod.
|
||||
The values are in decibels.
|
||||
"""
|
||||
att0, att1, att2, att3 = self.get_mu()
|
||||
return from_mu(att0), from_mu(att1), from_mu(att2), from_mu(att3)
|
|
@ -27,9 +27,9 @@ class ExceptionType(Enum):
|
|||
legacy_o_sequence_error_reset = 0b010001
|
||||
legacy_o_collision_reset = 0b010010
|
||||
legacy_i_overflow_reset = 0b100000
|
||||
legacy_o_sequence_error = 0b010101
|
||||
|
||||
o_underflow = 0b010100
|
||||
o_sequence_error = 0b010101
|
||||
|
||||
i_overflow = 0b100001
|
||||
|
||||
|
@ -92,16 +92,16 @@ DecodedDump = namedtuple(
|
|||
def decode_dump(data):
|
||||
parts = struct.unpack(">IQbbb", data[:15])
|
||||
(sent_bytes, total_byte_count,
|
||||
overflow_occured, log_channel, dds_onehot_sel) = parts
|
||||
error_occured, log_channel, dds_onehot_sel) = parts
|
||||
|
||||
expected_len = sent_bytes + 15
|
||||
if expected_len != len(data):
|
||||
raise ValueError("analyzer dump has incorrect length "
|
||||
"(got {}, expected {})".format(
|
||||
len(data), expected_len))
|
||||
if overflow_occured:
|
||||
logger.warning("analyzer FIFO overflow occured, "
|
||||
"some messages have been lost")
|
||||
if error_occured:
|
||||
logger.warning("error occured within the analyzer, "
|
||||
"data may be corrupted")
|
||||
if total_byte_count > sent_bytes:
|
||||
logger.info("analyzer ring buffer has wrapped %d times",
|
||||
total_byte_count//sent_bytes)
|
||||
|
@ -211,9 +211,8 @@ class TTLClockGenHandler:
|
|||
|
||||
|
||||
class DDSHandler:
|
||||
def __init__(self, vcd_manager, dds_type, onehot_sel, sysclk):
|
||||
def __init__(self, vcd_manager, onehot_sel, sysclk):
|
||||
self.vcd_manager = vcd_manager
|
||||
self.dds_type = dds_type
|
||||
self.onehot_sel = onehot_sel
|
||||
self.sysclk = sysclk
|
||||
|
||||
|
@ -227,7 +226,6 @@ class DDSHandler:
|
|||
self.vcd_manager.get_channel(name + "/frequency", 64)
|
||||
dds_channel["vcd_phase"] = \
|
||||
self.vcd_manager.get_channel(name + "/phase", 64)
|
||||
if self.dds_type == "DDSChannelAD9914":
|
||||
dds_channel["ftw"] = [None, None]
|
||||
dds_channel["pow"] = None
|
||||
self.dds_channels[dds_channel_nr] = dds_channel
|
||||
|
@ -252,9 +250,9 @@ class DDSHandler:
|
|||
self.selected_dds_channels = self._gpio_to_channels(message.data)
|
||||
for dds_channel_nr in self.selected_dds_channels:
|
||||
dds_channel = self.dds_channels[dds_channel_nr]
|
||||
if message.address == 0x2d:
|
||||
if message.address == 0x11:
|
||||
dds_channel["ftw"][0] = message.data
|
||||
elif message.address == 0x2f:
|
||||
elif message.address == 0x13:
|
||||
dds_channel["ftw"][1] = message.data
|
||||
elif message.address == 0x31:
|
||||
dds_channel["pow"] = message.data
|
||||
|
@ -273,7 +271,6 @@ class DDSHandler:
|
|||
logger.debug("DDS write @%d 0x%04x to 0x%02x, selected channels: %s",
|
||||
message.timestamp, message.data, message.address,
|
||||
self.selected_dds_channels)
|
||||
if self.dds_type == "DDSChannelAD9914":
|
||||
self._decode_ad9914_write(message)
|
||||
|
||||
|
||||
|
@ -344,6 +341,56 @@ class SPIMasterHandler(WishboneHandler):
|
|||
raise ValueError("bad address %d", address)
|
||||
|
||||
|
||||
class SPIMaster2Handler(WishboneHandler):
|
||||
def __init__(self, vcd_manager, name):
|
||||
self._reads = []
|
||||
self.channels = {}
|
||||
with vcd_manager.scope("spi2/{}".format(name)):
|
||||
self.stb = vcd_manager.get_channel("{}/{}".format(name, "stb"), 1)
|
||||
for reg_name, reg_width in [
|
||||
("flags", 8),
|
||||
("length", 5),
|
||||
("div", 8),
|
||||
("chip_select", 8),
|
||||
("write", 32),
|
||||
("read", 32)]:
|
||||
self.channels[reg_name] = vcd_manager.get_channel(
|
||||
"{}/{}".format(name, reg_name), reg_width)
|
||||
|
||||
def process_message(self, message):
|
||||
self.stb.set_value("1")
|
||||
self.stb.set_value("0")
|
||||
if isinstance(message, OutputMessage):
|
||||
data = message.data
|
||||
address = message.address
|
||||
if address == 1:
|
||||
logger.debug("SPI config @%d data=0x%08x",
|
||||
message.timestamp, data)
|
||||
self.channels["chip_select"].set_value(
|
||||
"{:08b}".format(data >> 24))
|
||||
self.channels["div"].set_value(
|
||||
"{:08b}".format(data >> 16 & 0xff))
|
||||
self.channels["length"].set_value(
|
||||
"{:08b}".format(data >> 8 & 0x1f))
|
||||
self.channels["flags"].set_value(
|
||||
"{:08b}".format(data & 0xff))
|
||||
elif address == 0:
|
||||
logger.debug("SPI write @%d data=0x%08x",
|
||||
message.timestamp, data)
|
||||
self.channels["write"].set_value("{:032b}".format(data))
|
||||
else:
|
||||
raise ValueError("bad address", address)
|
||||
# process untimed reads and insert them here
|
||||
while (self._reads and
|
||||
self._reads[0].rtio_counter < message.timestamp):
|
||||
read = self._reads.pop(0)
|
||||
logger.debug("SPI read @%d data=0x%08x",
|
||||
read.rtio_counter, read.data)
|
||||
self.channels["read"].set_value("{:032b}".format(read.data))
|
||||
elif isinstance(message, InputMessage):
|
||||
self._reads.append(message)
|
||||
|
||||
|
||||
def _extract_log_chars(data):
|
||||
r = ""
|
||||
for i in range(4):
|
||||
|
@ -395,16 +442,17 @@ def get_vcd_log_channels(log_channel, messages):
|
|||
|
||||
|
||||
def get_single_device_argument(devices, module, cls, argument):
|
||||
ref_period = None
|
||||
found = None
|
||||
for desc in devices.values():
|
||||
if isinstance(desc, dict) and desc["type"] == "local":
|
||||
if (desc["module"] == module
|
||||
and desc["class"] in cls):
|
||||
if ref_period is None:
|
||||
ref_period = desc["arguments"][argument]
|
||||
else:
|
||||
return None # more than one device found
|
||||
return ref_period
|
||||
value = desc["arguments"][argument]
|
||||
if found is None:
|
||||
found = value
|
||||
elif value != found:
|
||||
return None # more than one value/device found
|
||||
return found
|
||||
|
||||
|
||||
def get_ref_period(devices):
|
||||
|
@ -413,8 +461,8 @@ def get_ref_period(devices):
|
|||
|
||||
|
||||
def get_dds_sysclk(devices):
|
||||
return get_single_device_argument(devices, "artiq.coredevice.dds",
|
||||
("DDSGroupAD9914",), "sysclk")
|
||||
return get_single_device_argument(devices, "artiq.coredevice.ad9914",
|
||||
("AD9914",), "sysclk")
|
||||
|
||||
|
||||
def create_channel_handlers(vcd_manager, devices, ref_period,
|
||||
|
@ -430,23 +478,20 @@ def create_channel_handlers(vcd_manager, devices, ref_period,
|
|||
and desc["class"] == "TTLClockGen"):
|
||||
channel = desc["arguments"]["channel"]
|
||||
channel_handlers[channel] = TTLClockGenHandler(vcd_manager, name, ref_period)
|
||||
if (desc["module"] == "artiq.coredevice.dds"
|
||||
and desc["class"] in {"DDSChannelAD9914"}):
|
||||
if (desc["module"] == "artiq.coredevice.ad9914"
|
||||
and desc["class"] == "AD9914"):
|
||||
dds_bus_channel = desc["arguments"]["bus_channel"]
|
||||
dds_channel = desc["arguments"]["channel"]
|
||||
if dds_bus_channel in channel_handlers:
|
||||
dds_handler = channel_handlers[dds_bus_channel]
|
||||
if dds_handler.dds_type != desc["class"]:
|
||||
raise ValueError("All DDS channels must have the same type")
|
||||
else:
|
||||
dds_handler = DDSHandler(vcd_manager, desc["class"],
|
||||
dds_onehot_sel, dds_sysclk)
|
||||
dds_handler = DDSHandler(vcd_manager, dds_onehot_sel, dds_sysclk)
|
||||
channel_handlers[dds_bus_channel] = dds_handler
|
||||
dds_handler.add_dds_channel(name, dds_channel)
|
||||
if (desc["module"] == "artiq.coredevice.spi" and
|
||||
if (desc["module"] == "artiq.coredevice.spi2" and
|
||||
desc["class"] == "SPIMaster"):
|
||||
channel = desc["arguments"]["channel"]
|
||||
channel_handlers[channel] = SPIMasterHandler(
|
||||
channel_handlers[channel] = SPIMaster2Handler(
|
||||
vcd_manager, name)
|
||||
return channel_handlers
|
||||
|
||||
|
@ -455,10 +500,12 @@ def get_message_time(message):
|
|||
return getattr(message, "timestamp", message.rtio_counter)
|
||||
|
||||
|
||||
def decoded_dump_to_vcd(fileobj, devices, dump):
|
||||
def decoded_dump_to_vcd(fileobj, devices, dump, uniform_interval=False):
|
||||
vcd_manager = VCDManager(fileobj)
|
||||
ref_period = get_ref_period(devices)
|
||||
|
||||
if ref_period is not None:
|
||||
if not uniform_interval:
|
||||
vcd_manager.set_timescale_ps(ref_period*1e12)
|
||||
else:
|
||||
logger.warning("unable to determine core device ref_period")
|
||||
|
@ -481,6 +528,12 @@ def decoded_dump_to_vcd(fileobj, devices, dump):
|
|||
vcd_log_channels = get_vcd_log_channels(dump.log_channel, messages)
|
||||
channel_handlers[dump.log_channel] = LogHandler(
|
||||
vcd_manager, vcd_log_channels)
|
||||
if uniform_interval:
|
||||
# RTIO event timestamp in machine units
|
||||
timestamp = vcd_manager.get_channel("timestamp", 64)
|
||||
# RTIO time interval between this and the next timed event
|
||||
# in SI seconds
|
||||
interval = vcd_manager.get_channel("interval", 64)
|
||||
slack = vcd_manager.get_channel("rtio_slack", 64)
|
||||
|
||||
vcd_manager.set_time(0)
|
||||
|
@ -490,10 +543,17 @@ def decoded_dump_to_vcd(fileobj, devices, dump):
|
|||
if start_time:
|
||||
break
|
||||
|
||||
for message in messages:
|
||||
t0 = 0
|
||||
for i, message in enumerate(messages):
|
||||
if message.channel in channel_handlers:
|
||||
t = get_message_time(message) - start_time
|
||||
if t >= 0:
|
||||
if uniform_interval:
|
||||
interval.set_value_double((t - t0)*ref_period)
|
||||
vcd_manager.set_time(i)
|
||||
timestamp.set_value("{:064b}".format(t))
|
||||
t0 = t
|
||||
else:
|
||||
vcd_manager.set_time(t)
|
||||
channel_handlers[message.channel].process_message(message)
|
||||
if isinstance(message, OutputMessage):
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import struct
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import traceback
|
||||
import numpy
|
||||
import socket
|
||||
from enum import Enum
|
||||
from fractions import Fraction
|
||||
from collections import namedtuple
|
||||
|
@ -15,69 +14,39 @@ from artiq import __version__ as software_version
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _H2DMsgType(Enum):
|
||||
LOG_REQUEST = 1
|
||||
LOG_CLEAR = 2
|
||||
LOG_FILTER = 13
|
||||
class Request(Enum):
|
||||
SystemInfo = 3
|
||||
|
||||
SYSTEM_INFO_REQUEST = 3
|
||||
SWITCH_CLOCK = 4
|
||||
LoadKernel = 5
|
||||
RunKernel = 6
|
||||
|
||||
LOAD_KERNEL = 5
|
||||
RUN_KERNEL = 6
|
||||
|
||||
RPC_REPLY = 7
|
||||
RPC_EXCEPTION = 8
|
||||
|
||||
FLASH_READ_REQUEST = 9
|
||||
FLASH_WRITE_REQUEST = 10
|
||||
FLASH_ERASE_REQUEST = 11
|
||||
FLASH_REMOVE_REQUEST = 12
|
||||
|
||||
HOTSWAP = 14
|
||||
RPCReply = 7
|
||||
RPCException = 8
|
||||
|
||||
|
||||
class _D2HMsgType(Enum):
|
||||
LOG_REPLY = 1
|
||||
class Reply(Enum):
|
||||
SystemInfo = 2
|
||||
|
||||
SYSTEM_INFO_REPLY = 2
|
||||
CLOCK_SWITCH_COMPLETED = 3
|
||||
CLOCK_SWITCH_FAILED = 4
|
||||
LoadCompleted = 5
|
||||
LoadFailed = 6
|
||||
|
||||
LOAD_COMPLETED = 5
|
||||
LOAD_FAILED = 6
|
||||
KernelFinished = 7
|
||||
KernelStartupFailed = 8
|
||||
KernelException = 9
|
||||
|
||||
KERNEL_FINISHED = 7
|
||||
KERNEL_STARTUP_FAILED = 8
|
||||
KERNEL_EXCEPTION = 9
|
||||
RPCRequest = 10
|
||||
|
||||
RPC_REQUEST = 10
|
||||
|
||||
FLASH_READ_REPLY = 11
|
||||
FLASH_OK_REPLY = 12
|
||||
FLASH_ERROR_REPLY = 13
|
||||
|
||||
WATCHDOG_EXPIRED = 14
|
||||
CLOCK_FAILURE = 15
|
||||
|
||||
HOTSWAP_IMMINENT = 16
|
||||
|
||||
|
||||
class _LogLevel(Enum):
|
||||
OFF = 0
|
||||
ERROR = 1
|
||||
WARN = 2
|
||||
INFO = 3
|
||||
DEBUG = 4
|
||||
TRACE = 5
|
||||
ClockFailure = 15
|
||||
|
||||
|
||||
class UnsupportedDevice(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LoadError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RPCReturnValueError(ValueError):
|
||||
pass
|
||||
|
||||
|
@ -85,37 +54,109 @@ class RPCReturnValueError(ValueError):
|
|||
RPCKeyword = namedtuple('RPCKeyword', ['name', 'value'])
|
||||
|
||||
|
||||
def set_keepalive(sock, after_idle, interval, max_fails):
|
||||
if sys.platform.startswith("linux"):
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, after_idle)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, max_fails)
|
||||
elif sys.platform.startswith("win") or sys.platform.startswith("cygwin"):
|
||||
# setting max_fails is not supported, typically ends up being 5 or 10
|
||||
# depending on Windows version
|
||||
sock.ioctl(socket.SIO_KEEPALIVE_VALS,
|
||||
(1, after_idle*1000, interval*1000))
|
||||
def _receive_fraction(kernel, embedding_map):
|
||||
numerator = kernel._read_int64()
|
||||
denominator = kernel._read_int64()
|
||||
return Fraction(numerator, denominator)
|
||||
|
||||
|
||||
def _receive_list(kernel, embedding_map):
|
||||
length = kernel._read_int32()
|
||||
tag = chr(kernel._read_int8())
|
||||
if tag == "b":
|
||||
buffer = kernel._read(length)
|
||||
return list(buffer)
|
||||
elif tag == "i":
|
||||
buffer = kernel._read(4 * length)
|
||||
return list(struct.unpack(">%sl" % length, buffer))
|
||||
elif tag == "I":
|
||||
buffer = kernel._read(8 * length)
|
||||
return list(struct.unpack(">%sq" % length, buffer))
|
||||
elif tag == "f":
|
||||
buffer = kernel._read(8 * length)
|
||||
return list(struct.unpack(">%sd" % length, buffer))
|
||||
else:
|
||||
logger.warning("TCP keepalive not supported on platform '%s', ignored",
|
||||
sys.platform)
|
||||
fn = receivers[tag]
|
||||
elems = []
|
||||
for _ in range(length):
|
||||
# discard tag, as our device would still send the tag for each
|
||||
# non-primitive elements.
|
||||
kernel._read_int8()
|
||||
item = fn(kernel, embedding_map)
|
||||
elems.append(item)
|
||||
return elems
|
||||
|
||||
|
||||
def initialize_connection(host, port):
|
||||
sock = socket.create_connection((host, port), 5.0)
|
||||
sock.settimeout(None)
|
||||
set_keepalive(sock, 3, 2, 3)
|
||||
logger.debug("connected to host %s on port %d", host, port)
|
||||
return sock
|
||||
def _receive_array(kernel, embedding_map):
|
||||
num_dims = kernel._read_int8()
|
||||
shape = tuple(kernel._read_int32() for _ in range(num_dims))
|
||||
tag = chr(kernel._read_int8())
|
||||
fn = receivers[tag]
|
||||
length = numpy.prod(shape)
|
||||
if tag == "b":
|
||||
buffer = kernel._read(length)
|
||||
elems = numpy.ndarray((length, ), 'B', buffer)
|
||||
elif tag == "i":
|
||||
buffer = kernel._read(4 * length)
|
||||
elems = numpy.ndarray((length, ), '>i4', buffer)
|
||||
elif tag == "I":
|
||||
buffer = kernel._read(8 * length)
|
||||
elems = numpy.ndarray((length, ), '>i8', buffer)
|
||||
elif tag == "f":
|
||||
buffer = kernel._read(8 * length)
|
||||
elems = numpy.ndarray((length, ), '>d', buffer)
|
||||
else:
|
||||
fn = receivers[tag]
|
||||
elems = []
|
||||
for _ in range(numpy.prod(shape)):
|
||||
# discard the tag
|
||||
kernel._read_int8()
|
||||
item = fn(kernel, embedding_map)
|
||||
elems.append(item)
|
||||
elems = numpy.array(elems)
|
||||
return elems.reshape(shape)
|
||||
|
||||
|
||||
def _receive_range(kernel, embedding_map):
|
||||
start = kernel._receive_rpc_value(embedding_map)
|
||||
stop = kernel._receive_rpc_value(embedding_map)
|
||||
step = kernel._receive_rpc_value(embedding_map)
|
||||
return range(start, stop, step)
|
||||
|
||||
|
||||
def _receive_keyword(kernel, embedding_map):
|
||||
name = kernel._read_string()
|
||||
value = kernel._receive_rpc_value(embedding_map)
|
||||
return RPCKeyword(name, value)
|
||||
|
||||
|
||||
receivers = {
|
||||
"\x00": lambda kernel, embedding_map: kernel._rpc_sentinel,
|
||||
"t": lambda kernel, embedding_map:
|
||||
tuple(kernel._receive_rpc_value(embedding_map)
|
||||
for _ in range(kernel._read_int8())),
|
||||
"n": lambda kernel, embedding_map: None,
|
||||
"b": lambda kernel, embedding_map: bool(kernel._read_int8()),
|
||||
"i": lambda kernel, embedding_map: numpy.int32(kernel._read_int32()),
|
||||
"I": lambda kernel, embedding_map: numpy.int64(kernel._read_int64()),
|
||||
"f": lambda kernel, embedding_map: kernel._read_float64(),
|
||||
"s": lambda kernel, embedding_map: kernel._read_string(),
|
||||
"B": lambda kernel, embedding_map: kernel._read_bytes(),
|
||||
"A": lambda kernel, embedding_map: kernel._read_bytes(),
|
||||
"O": lambda kernel, embedding_map:
|
||||
embedding_map.retrieve_object(kernel._read_int32()),
|
||||
"F": _receive_fraction,
|
||||
"l": _receive_list,
|
||||
"a": _receive_array,
|
||||
"r": _receive_range,
|
||||
"k": _receive_keyword
|
||||
}
|
||||
|
||||
|
||||
class CommKernelDummy:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def switch_clock(self, external):
|
||||
pass
|
||||
|
||||
def load(self, kernel_library):
|
||||
pass
|
||||
|
||||
|
@ -128,23 +169,31 @@ class CommKernelDummy:
|
|||
def check_system_info(self):
|
||||
pass
|
||||
|
||||
def get_log(self):
|
||||
return ""
|
||||
|
||||
def clear_log(self):
|
||||
pass
|
||||
|
||||
|
||||
class CommKernel:
|
||||
warned_of_mismatch = False
|
||||
|
||||
def __init__(self, host, port=1381):
|
||||
self._read_type = None
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.read_buffer = bytearray()
|
||||
self.write_buffer = bytearray()
|
||||
|
||||
self.unpack_int32 = struct.Struct(">l").unpack
|
||||
self.unpack_int64 = struct.Struct(">q").unpack
|
||||
self.unpack_float64 = struct.Struct(">d").unpack
|
||||
|
||||
self.pack_header = struct.Struct(">lB").pack
|
||||
self.pack_int32 = struct.Struct(">l").pack
|
||||
self.pack_int64 = struct.Struct(">q").pack
|
||||
self.pack_float64 = struct.Struct(">d").pack
|
||||
|
||||
def open(self):
|
||||
if hasattr(self, "socket"):
|
||||
return
|
||||
self.socket = initialize_connection(self.host, self.port)
|
||||
self.socket = socket.create_connection((self.host, self.port))
|
||||
logger.debug("connected to %s:%d", self.host, self.port)
|
||||
self.socket.sendall(b"ARTIQ coredev\n")
|
||||
|
||||
def close(self):
|
||||
|
@ -154,37 +203,39 @@ class CommKernel:
|
|||
del self.socket
|
||||
logger.debug("disconnected")
|
||||
|
||||
def read(self, length):
|
||||
r = bytes()
|
||||
while len(r) < length:
|
||||
rn = self.socket.recv(min(8192, length - len(r)))
|
||||
if not rn:
|
||||
raise ConnectionResetError("Connection closed")
|
||||
r += rn
|
||||
return r
|
||||
|
||||
def write(self, data):
|
||||
self.socket.sendall(data)
|
||||
|
||||
#
|
||||
# Reader interface
|
||||
#
|
||||
|
||||
def _read(self, length):
|
||||
# cache the reads to avoid frequent call to recv
|
||||
while len(self.read_buffer) < length:
|
||||
# the number is just the maximum amount
|
||||
# when there is not much data, it would return earlier
|
||||
diff = length - len(self.read_buffer)
|
||||
flag = 0
|
||||
if diff > 8192:
|
||||
flag |= socket.MSG_WAITALL
|
||||
self.read_buffer += self.socket.recv(8192, flag)
|
||||
result = self.read_buffer[:length]
|
||||
self.read_buffer = self.read_buffer[length:]
|
||||
return result
|
||||
|
||||
def _read_header(self):
|
||||
self.open()
|
||||
|
||||
# Wait for a synchronization sequence, 5a 5a 5a 5a.
|
||||
sync_count = 0
|
||||
while sync_count < 4:
|
||||
(sync_byte, ) = struct.unpack("B", self.read(1))
|
||||
sync_byte = self._read(1)[0]
|
||||
if sync_byte == 0x5a:
|
||||
sync_count += 1
|
||||
else:
|
||||
sync_count = 0
|
||||
|
||||
# Read message header.
|
||||
(raw_type, ) = struct.unpack("B", self.read(1))
|
||||
self._read_type = _D2HMsgType(raw_type)
|
||||
raw_type = self._read(1)[0]
|
||||
self._read_type = Reply(raw_type)
|
||||
|
||||
logger.debug("receiving message: type=%r",
|
||||
self._read_type)
|
||||
|
@ -198,30 +249,26 @@ class CommKernel:
|
|||
self._read_header()
|
||||
self._read_expect(ty)
|
||||
|
||||
def _read_chunk(self, length):
|
||||
return self.read(length)
|
||||
|
||||
def _read_int8(self):
|
||||
(value, ) = struct.unpack("B", self._read_chunk(1))
|
||||
return value
|
||||
return self._read(1)[0]
|
||||
|
||||
def _read_int32(self):
|
||||
(value, ) = struct.unpack(">l", self._read_chunk(4))
|
||||
(value, ) = self.unpack_int32(self._read(4))
|
||||
return value
|
||||
|
||||
def _read_int64(self):
|
||||
(value, ) = struct.unpack(">q", self._read_chunk(8))
|
||||
(value, ) = self.unpack_int64(self._read(8))
|
||||
return value
|
||||
|
||||
def _read_float64(self):
|
||||
(value, ) = struct.unpack(">d", self._read_chunk(8))
|
||||
(value, ) = self.unpack_float64(self._read(8))
|
||||
return value
|
||||
|
||||
def _read_bool(self):
|
||||
return True if self._read_int8() else False
|
||||
|
||||
def _read_bytes(self):
|
||||
return self._read_chunk(self._read_int32())
|
||||
return self._read(self._read_int32())
|
||||
|
||||
def _read_string(self):
|
||||
return self._read_bytes().decode("utf-8")
|
||||
|
@ -230,38 +277,49 @@ class CommKernel:
|
|||
# Writer interface
|
||||
#
|
||||
|
||||
def _write(self, data):
|
||||
self.write_buffer += data
|
||||
# if the buffer is already pretty large, send it
|
||||
# the block size is arbitrary, tuning it may improve performance
|
||||
if len(self.write_buffer) > 4096:
|
||||
self._flush()
|
||||
|
||||
def _flush(self):
|
||||
self.socket.sendall(self.write_buffer)
|
||||
self.write_buffer.clear()
|
||||
|
||||
def _write_header(self, ty):
|
||||
self.open()
|
||||
|
||||
logger.debug("sending message: type=%r", ty)
|
||||
|
||||
# Write synchronization sequence and header.
|
||||
self.write(struct.pack(">lB", 0x5a5a5a5a, ty.value))
|
||||
self._write(self.pack_header(0x5a5a5a5a, ty.value))
|
||||
|
||||
def _write_empty(self, ty):
|
||||
self._write_header(ty)
|
||||
|
||||
def _write_chunk(self, chunk):
|
||||
self.write(chunk)
|
||||
self._write(chunk)
|
||||
|
||||
def _write_int8(self, value):
|
||||
self.write(struct.pack("B", value))
|
||||
self._write(value)
|
||||
|
||||
def _write_int32(self, value):
|
||||
self.write(struct.pack(">l", value))
|
||||
self._write(self.pack_int32(value))
|
||||
|
||||
def _write_int64(self, value):
|
||||
self.write(struct.pack(">q", value))
|
||||
self._write(self.pack_int64(value))
|
||||
|
||||
def _write_float64(self, value):
|
||||
self.write(struct.pack(">d", value))
|
||||
self._write(self.pack_float64(value))
|
||||
|
||||
def _write_bool(self, value):
|
||||
self.write(struct.pack("B", value))
|
||||
self._write(b'\x01' if value else b'\x00')
|
||||
|
||||
def _write_bytes(self, value):
|
||||
self._write_int32(len(value))
|
||||
self.write(value)
|
||||
self._write(value)
|
||||
|
||||
def _write_string(self, value):
|
||||
self._write_bytes(value.encode("utf-8"))
|
||||
|
@ -270,126 +328,53 @@ class CommKernel:
|
|||
# Exported APIs
|
||||
#
|
||||
|
||||
def reset_session(self):
|
||||
self.write(struct.pack(">ll", 0x5a5a5a5a, 0))
|
||||
|
||||
def check_system_info(self):
|
||||
self._write_empty(_H2DMsgType.SYSTEM_INFO_REQUEST)
|
||||
self._write_empty(Request.SystemInfo)
|
||||
self._flush()
|
||||
|
||||
self._read_header()
|
||||
self._read_expect(_D2HMsgType.SYSTEM_INFO_REPLY)
|
||||
runtime_id = self._read_chunk(4)
|
||||
if runtime_id != b"AROR":
|
||||
raise UnsupportedDevice("Unsupported runtime ID: {}"
|
||||
.format(runtime_id))
|
||||
|
||||
gateware_version = self._read_string()
|
||||
if gateware_version != software_version:
|
||||
self._read_expect(Reply.SystemInfo)
|
||||
runtime_id = self._read(4)
|
||||
if runtime_id == b"AROR":
|
||||
gateware_version = self._read_string().split(";")[0]
|
||||
if gateware_version != software_version and not self.warned_of_mismatch:
|
||||
logger.warning("Mismatch between gateware (%s) "
|
||||
"and software (%s) versions",
|
||||
gateware_version, software_version)
|
||||
CommKernel.warned_of_mismatch = True
|
||||
|
||||
finished_cleanly = self._read_bool()
|
||||
if not finished_cleanly:
|
||||
logger.warning("Previous kernel did not cleanly finish")
|
||||
|
||||
def switch_clock(self, external):
|
||||
self._write_header(_H2DMsgType.SWITCH_CLOCK)
|
||||
self._write_int8(external)
|
||||
|
||||
self._read_empty(_D2HMsgType.CLOCK_SWITCH_COMPLETED)
|
||||
|
||||
def flash_storage_read(self, key):
|
||||
self._write_header(_H2DMsgType.FLASH_READ_REQUEST)
|
||||
self._write_string(key)
|
||||
|
||||
self._read_header()
|
||||
self._read_expect(_D2HMsgType.FLASH_READ_REPLY)
|
||||
return self._read_string()
|
||||
|
||||
def flash_storage_write(self, key, value):
|
||||
self._write_header(_H2DMsgType.FLASH_WRITE_REQUEST)
|
||||
self._write_string(key)
|
||||
self._write_bytes(value)
|
||||
|
||||
self._read_header()
|
||||
if self._read_type == _D2HMsgType.FLASH_ERROR_REPLY:
|
||||
raise IOError("Flash storage is full")
|
||||
elif runtime_id == b"ARZQ":
|
||||
pass
|
||||
else:
|
||||
self._read_expect(_D2HMsgType.FLASH_OK_REPLY)
|
||||
|
||||
def flash_storage_erase(self):
|
||||
self._write_empty(_H2DMsgType.FLASH_ERASE_REQUEST)
|
||||
|
||||
self._read_empty(_D2HMsgType.FLASH_OK_REPLY)
|
||||
|
||||
def flash_storage_remove(self, key):
|
||||
self._write_header(_H2DMsgType.FLASH_REMOVE_REQUEST)
|
||||
self._write_string(key)
|
||||
|
||||
self._read_empty(_D2HMsgType.FLASH_OK_REPLY)
|
||||
raise UnsupportedDevice("Unsupported runtime ID: {}"
|
||||
.format(runtime_id))
|
||||
|
||||
def load(self, kernel_library):
|
||||
self._write_header(_H2DMsgType.LOAD_KERNEL)
|
||||
self._write_header(Request.LoadKernel)
|
||||
self._write_bytes(kernel_library)
|
||||
self._flush()
|
||||
|
||||
self._read_header()
|
||||
if self._read_type == _D2HMsgType.LOAD_FAILED:
|
||||
if self._read_type == Reply.LoadFailed:
|
||||
raise LoadError(self._read_string())
|
||||
else:
|
||||
self._read_expect(_D2HMsgType.LOAD_COMPLETED)
|
||||
self._read_expect(Reply.LoadCompleted)
|
||||
|
||||
def run(self):
|
||||
self._write_empty(_H2DMsgType.RUN_KERNEL)
|
||||
self._write_empty(Request.RunKernel)
|
||||
self._flush()
|
||||
logger.debug("running kernel")
|
||||
|
||||
_rpc_sentinel = object()
|
||||
|
||||
# See session.c:{send,receive}_rpc_value and llvm_ir_generator.py:_rpc_tag.
|
||||
# See rpc_proto.rs and compiler/ir.py:rpc_tag.
|
||||
def _receive_rpc_value(self, embedding_map):
|
||||
tag = chr(self._read_int8())
|
||||
if tag == "\x00":
|
||||
return self._rpc_sentinel
|
||||
elif tag == "t":
|
||||
length = self._read_int8()
|
||||
return tuple(self._receive_rpc_value(embedding_map) for _ in range(length))
|
||||
elif tag == "n":
|
||||
return None
|
||||
elif tag == "b":
|
||||
return bool(self._read_int8())
|
||||
elif tag == "i":
|
||||
return numpy.int32(self._read_int32())
|
||||
elif tag == "I":
|
||||
return numpy.int64(self._read_int64())
|
||||
elif tag == "f":
|
||||
return self._read_float64()
|
||||
elif tag == "F":
|
||||
numerator = self._read_int64()
|
||||
denominator = self._read_int64()
|
||||
return Fraction(numerator, denominator)
|
||||
elif tag == "s":
|
||||
return self._read_string()
|
||||
elif tag == "B":
|
||||
return self._read_bytes()
|
||||
elif tag == "A":
|
||||
return self._read_bytes()
|
||||
elif tag == "l":
|
||||
length = self._read_int32()
|
||||
return [self._receive_rpc_value(embedding_map) for _ in range(length)]
|
||||
elif tag == "a":
|
||||
length = self._read_int32()
|
||||
return numpy.array([self._receive_rpc_value(embedding_map) for _ in range(length)])
|
||||
elif tag == "r":
|
||||
start = self._receive_rpc_value(embedding_map)
|
||||
stop = self._receive_rpc_value(embedding_map)
|
||||
step = self._receive_rpc_value(embedding_map)
|
||||
return range(start, stop, step)
|
||||
elif tag == "k":
|
||||
name = self._read_string()
|
||||
value = self._receive_rpc_value(embedding_map)
|
||||
return RPCKeyword(name, value)
|
||||
elif tag == "O":
|
||||
return embedding_map.retrieve_object(self._read_int32())
|
||||
if tag in receivers:
|
||||
return receivers.get(tag)(self, embedding_map)
|
||||
else:
|
||||
raise IOError("Unknown RPC value tag: {}".format(repr(tag)))
|
||||
|
||||
|
@ -405,7 +390,7 @@ class CommKernel:
|
|||
args.append(value)
|
||||
|
||||
def _skip_rpc_value(self, tags):
|
||||
tag = tags.pop(0)
|
||||
tag = chr(tags.pop(0))
|
||||
if tag == "t":
|
||||
length = tags.pop(0)
|
||||
for _ in range(length):
|
||||
|
@ -439,7 +424,7 @@ class CommKernel:
|
|||
elif tag == "b":
|
||||
check(isinstance(value, bool),
|
||||
lambda: "bool")
|
||||
self._write_int8(value)
|
||||
self._write_bool(value)
|
||||
elif tag == "i":
|
||||
check(isinstance(value, (int, numpy.int32)) and
|
||||
(-2**31 < value < 2**31-1),
|
||||
|
@ -477,10 +462,45 @@ class CommKernel:
|
|||
check(isinstance(value, list),
|
||||
lambda: "list")
|
||||
self._write_int32(len(value))
|
||||
tag_element = chr(tags[0])
|
||||
if tag_element == "b":
|
||||
self._write(bytes(value))
|
||||
elif tag_element == "i":
|
||||
self._write(struct.pack(">%sl" % len(value), *value))
|
||||
elif tag_element == "I":
|
||||
self._write(struct.pack(">%sq" % len(value), *value))
|
||||
elif tag_element == "f":
|
||||
self._write(struct.pack(">%sd" % len(value), *value))
|
||||
else:
|
||||
for elt in value:
|
||||
tags_copy = bytearray(tags)
|
||||
self._send_rpc_value(tags_copy, elt, root, function)
|
||||
self._skip_rpc_value(tags)
|
||||
elif tag == "a":
|
||||
check(isinstance(value, numpy.ndarray),
|
||||
lambda: "numpy.ndarray")
|
||||
num_dims = tags.pop(0)
|
||||
check(num_dims == len(value.shape),
|
||||
lambda: "{}-dimensional numpy.ndarray".format(num_dims))
|
||||
for s in value.shape:
|
||||
self._write_int32(s)
|
||||
tag_element = chr(tags[0])
|
||||
if tag_element == "b":
|
||||
self._write(value.reshape((-1,), order="C").tobytes())
|
||||
elif tag_element == "i":
|
||||
array = value.reshape((-1,), order="C").astype('>i4')
|
||||
self._write(array.tobytes())
|
||||
elif tag_element == "I":
|
||||
array = value.reshape((-1,), order="C").astype('>i8')
|
||||
self._write(array.tobytes())
|
||||
elif tag_element == "f":
|
||||
array = value.reshape((-1,), order="C").astype('>d')
|
||||
self._write(array.tobytes())
|
||||
else:
|
||||
for elt in value.reshape((-1,), order="C"):
|
||||
tags_copy = bytearray(tags)
|
||||
self._send_rpc_value(tags_copy, elt, root, function)
|
||||
self._skip_rpc_value(tags)
|
||||
elif tag == "r":
|
||||
check(isinstance(value, range),
|
||||
lambda: "range")
|
||||
|
@ -501,35 +521,39 @@ class CommKernel:
|
|||
return msg
|
||||
|
||||
def _serve_rpc(self, embedding_map):
|
||||
async = self._read_bool()
|
||||
is_async = self._read_bool()
|
||||
service_id = self._read_int32()
|
||||
args, kwargs = self._receive_rpc_args(embedding_map)
|
||||
return_tags = self._read_bytes()
|
||||
|
||||
if service_id is 0:
|
||||
service = lambda obj, attr, value: setattr(obj, attr, value)
|
||||
if service_id == 0:
|
||||
def service(obj, attr, value): return setattr(obj, attr, value)
|
||||
else:
|
||||
service = embedding_map.retrieve_object(service_id)
|
||||
logger.debug("rpc service: [%d]%r%s %r %r -> %s", service_id, service,
|
||||
(" (async)" if async else ""), args, kwargs, return_tags)
|
||||
(" (async)" if is_async else ""), args, kwargs, return_tags)
|
||||
|
||||
if async:
|
||||
if is_async:
|
||||
service(*args, **kwargs)
|
||||
return
|
||||
|
||||
try:
|
||||
result = service(*args, **kwargs)
|
||||
logger.debug("rpc service: %d %r %r = %r", service_id, args, kwargs, result)
|
||||
logger.debug("rpc service: %d %r %r = %r",
|
||||
service_id, args, kwargs, result)
|
||||
|
||||
self._write_header(_H2DMsgType.RPC_REPLY)
|
||||
self._write_header(Request.RPCReply)
|
||||
self._write_bytes(return_tags)
|
||||
self._send_rpc_value(bytearray(return_tags), result, result, service)
|
||||
self._send_rpc_value(bytearray(return_tags),
|
||||
result, result, service)
|
||||
self._flush()
|
||||
except RPCReturnValueError as exn:
|
||||
raise
|
||||
except Exception as exn:
|
||||
logger.debug("rpc service: %d %r %r ! %r", service_id, args, kwargs, exn)
|
||||
logger.debug("rpc service: %d %r %r ! %r",
|
||||
service_id, args, kwargs, exn)
|
||||
|
||||
self._write_header(_H2DMsgType.RPC_EXCEPTION)
|
||||
self._write_header(Request.RPCException)
|
||||
|
||||
if hasattr(exn, "artiq_core_exception"):
|
||||
exn = exn.artiq_core_exception
|
||||
|
@ -545,7 +569,7 @@ class CommKernel:
|
|||
self._write_string(function)
|
||||
else:
|
||||
exn_type = type(exn)
|
||||
if exn_type in (ZeroDivisionError, ValueError, IndexError) or \
|
||||
if exn_type in (ZeroDivisionError, ValueError, IndexError, RuntimeError) or \
|
||||
hasattr(exn, "artiq_builtin"):
|
||||
self._write_string("0:{}".format(exn_type.__name__))
|
||||
else:
|
||||
|
@ -568,6 +592,7 @@ class CommKernel:
|
|||
self._write_int32(line)
|
||||
self._write_int32(-1) # column not known
|
||||
self._write_string(function)
|
||||
self._flush()
|
||||
|
||||
def _serve_exception(self, embedding_map, symbolizer, demangler):
|
||||
name = self._read_string()
|
||||
|
@ -597,14 +622,12 @@ class CommKernel:
|
|||
def serve(self, embedding_map, symbolizer, demangler):
|
||||
while True:
|
||||
self._read_header()
|
||||
if self._read_type == _D2HMsgType.RPC_REQUEST:
|
||||
if self._read_type == Reply.RPCRequest:
|
||||
self._serve_rpc(embedding_map)
|
||||
elif self._read_type == _D2HMsgType.KERNEL_EXCEPTION:
|
||||
elif self._read_type == Reply.KernelException:
|
||||
self._serve_exception(embedding_map, symbolizer, demangler)
|
||||
elif self._read_type == _D2HMsgType.WATCHDOG_EXPIRED:
|
||||
raise exceptions.WatchdogExpired
|
||||
elif self._read_type == _D2HMsgType.CLOCK_FAILURE:
|
||||
elif self._read_type == Reply.ClockFailure:
|
||||
raise exceptions.ClockFailure
|
||||
else:
|
||||
self._read_expect(_D2HMsgType.KERNEL_FINISHED)
|
||||
self._read_expect(Reply.KernelFinished)
|
||||
return
|
||||
|
|
|
@ -14,15 +14,32 @@ class Request(Enum):
|
|||
SetLogFilter = 3
|
||||
SetUartLogFilter = 6
|
||||
|
||||
ConfigRead = 12
|
||||
ConfigWrite = 13
|
||||
ConfigRemove = 14
|
||||
ConfigErase = 15
|
||||
|
||||
StartProfiler = 9
|
||||
StopProfiler = 10
|
||||
GetProfile = 11
|
||||
|
||||
Hotswap = 4
|
||||
Reboot = 5
|
||||
|
||||
DebugAllocator = 8
|
||||
|
||||
|
||||
class Reply(Enum):
|
||||
Success = 1
|
||||
Error = 6
|
||||
Unavailable = 4
|
||||
|
||||
LogContent = 2
|
||||
|
||||
ConfigData = 7
|
||||
|
||||
Profile = 5
|
||||
|
||||
RebootImminent = 3
|
||||
|
||||
|
||||
|
@ -35,13 +52,6 @@ class LogLevel(Enum):
|
|||
TRACE = 5
|
||||
|
||||
|
||||
def initialize_connection(host, port):
|
||||
sock = socket.create_connection((host, port), 5.0)
|
||||
sock.settimeout(None)
|
||||
logger.debug("connected to host %s on port %d", host, port)
|
||||
return sock
|
||||
|
||||
|
||||
class CommMgmt:
|
||||
def __init__(self, host, port=1380):
|
||||
self.host = host
|
||||
|
@ -50,7 +60,8 @@ class CommMgmt:
|
|||
def open(self):
|
||||
if hasattr(self, "socket"):
|
||||
return
|
||||
self.socket = initialize_connection(self.host, self.port)
|
||||
self.socket = socket.create_connection((self.host, self.port))
|
||||
logger.debug("connected to %s:%d", self.host, self.port)
|
||||
self.socket.sendall(b"ARTIQ management\n")
|
||||
|
||||
def close(self):
|
||||
|
@ -81,6 +92,9 @@ class CommMgmt:
|
|||
self._write_int32(len(value))
|
||||
self._write(value)
|
||||
|
||||
def _write_string(self, value):
|
||||
self._write_bytes(value.encode("utf-8"))
|
||||
|
||||
def _read(self, length):
|
||||
r = bytes()
|
||||
while len(r) < length:
|
||||
|
@ -143,6 +157,66 @@ class CommMgmt:
|
|||
self._write_int8(getattr(LogLevel, level).value)
|
||||
self._read_expect(Reply.Success)
|
||||
|
||||
def config_read(self, key):
|
||||
self._write_header(Request.ConfigRead)
|
||||
self._write_string(key)
|
||||
self._read_expect(Reply.ConfigData)
|
||||
return self._read_string()
|
||||
|
||||
def config_write(self, key, value):
|
||||
self._write_header(Request.ConfigWrite)
|
||||
self._write_string(key)
|
||||
self._write_bytes(value)
|
||||
ty = self._read_header()
|
||||
if ty == Reply.Error:
|
||||
raise IOError("Flash storage is full")
|
||||
elif ty != Reply.Success:
|
||||
raise IOError("Incorrect reply from device: {} (expected {})".
|
||||
format(ty, Reply.Success))
|
||||
|
||||
def config_remove(self, key):
|
||||
self._write_header(Request.ConfigRemove)
|
||||
self._write_string(key)
|
||||
self._read_expect(Reply.Success)
|
||||
|
||||
def config_erase(self):
|
||||
self._write_header(Request.ConfigErase)
|
||||
self._read_expect(Reply.Success)
|
||||
|
||||
def start_profiler(self, interval, edges_size, hits_size):
|
||||
self._write_header(Request.StartProfiler)
|
||||
self._write_int32(interval)
|
||||
self._write_int32(edges_size)
|
||||
self._write_int32(hits_size)
|
||||
self._read_expect(Reply.Success)
|
||||
|
||||
def stop_profiler(self):
|
||||
self._write_header(Request.StopProfiler)
|
||||
self._read_expect(Reply.Success)
|
||||
|
||||
def stop_profiler(self):
|
||||
self._write_header(Request.StopProfiler)
|
||||
self._read_expect(Reply.Success)
|
||||
|
||||
def get_profile(self):
|
||||
self._write_header(Request.GetProfile)
|
||||
self._read_expect(Reply.Profile)
|
||||
|
||||
hits = {}
|
||||
for _ in range(self._read_int32()):
|
||||
addr = self._read_int32()
|
||||
count = self._read_int32()
|
||||
hits[addr] = count
|
||||
|
||||
edges = {}
|
||||
for _ in range(self._read_int32()):
|
||||
caller = self._read_int32()
|
||||
callee = self._read_int32()
|
||||
count = self._read_int32()
|
||||
edges[(caller, callee)] = count
|
||||
|
||||
return hits, edges
|
||||
|
||||
def hotswap(self, firmware):
|
||||
self._write_header(Request.Hotswap)
|
||||
self._write_bytes(firmware)
|
||||
|
@ -151,3 +225,6 @@ class CommMgmt:
|
|||
def reboot(self):
|
||||
self._write_header(Request.Reboot)
|
||||
self._read_expect(Reply.RebootImminent)
|
||||
|
||||
def debug_allocator(self):
|
||||
self._write_header(Request.DebugAllocator)
|
||||
|
|
|
@ -51,10 +51,14 @@ class CommMonInj:
|
|||
del self._reader
|
||||
del self._writer
|
||||
|
||||
def monitor(self, enable, channel, probe):
|
||||
def monitor_probe(self, enable, channel, probe):
|
||||
packet = struct.pack(">bblb", 0, enable, channel, probe)
|
||||
self._writer.write(packet)
|
||||
|
||||
def monitor_injection(self, enable, channel, overrd):
|
||||
packet = struct.pack(">bblb", 3, enable, channel, overrd)
|
||||
self._writer.write(packet)
|
||||
|
||||
def inject(self, channel, override, value):
|
||||
packet = struct.pack(">blbb", 1, channel, override, value)
|
||||
self._writer.write(packet)
|
||||
|
|
|
@ -11,7 +11,7 @@ from artiq.language.units import *
|
|||
|
||||
from artiq.compiler.module import Module
|
||||
from artiq.compiler.embedding import Stitcher
|
||||
from artiq.compiler.targets import OR1KTarget
|
||||
from artiq.compiler.targets import OR1KTarget, CortexA9Target
|
||||
|
||||
from artiq.coredevice.comm_kernel import CommKernel, CommKernelDummy
|
||||
# Import for side effects (creating the exception classes).
|
||||
|
@ -43,6 +43,10 @@ class CompileError(Exception):
|
|||
def rtio_init() -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def rtio_get_destination_status(linkno: TInt32) -> TBool:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def rtio_get_counter() -> TInt64:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
@ -58,8 +62,6 @@ class Core:
|
|||
clocked at 125MHz and a SERDES multiplication factor of 8, the
|
||||
reference period is 1ns.
|
||||
The time machine unit is equal to this period.
|
||||
:param external_clock: whether the core device should switch to its
|
||||
external RTIO clock input instead of using its internal oscillator.
|
||||
:param ref_multiplier: ratio between the RTIO fine timestamp frequency
|
||||
and the RTIO coarse timestamp frequency (e.g. SERDES multiplication
|
||||
factor).
|
||||
|
@ -67,14 +69,17 @@ class Core:
|
|||
|
||||
kernel_invariants = {
|
||||
"core", "ref_period", "coarse_ref_period", "ref_multiplier",
|
||||
"external_clock",
|
||||
}
|
||||
|
||||
def __init__(self, dmgr, host, ref_period, external_clock=False,
|
||||
ref_multiplier=8):
|
||||
def __init__(self, dmgr, host, ref_period, ref_multiplier=8, target="or1k"):
|
||||
self.ref_period = ref_period
|
||||
self.external_clock = external_clock
|
||||
self.ref_multiplier = ref_multiplier
|
||||
if target == "or1k":
|
||||
self.target_cls = OR1KTarget
|
||||
elif target == "cortexa9":
|
||||
self.target_cls = CortexA9Target
|
||||
else:
|
||||
raise ValueError("Unsupported target")
|
||||
self.coarse_ref_period = ref_period*ref_multiplier
|
||||
if host is None:
|
||||
self.comm = CommKernelDummy()
|
||||
|
@ -102,7 +107,7 @@ class Core:
|
|||
module = Module(stitcher,
|
||||
ref_period=self.ref_period,
|
||||
attribute_writeback=attribute_writeback)
|
||||
target = OR1KTarget()
|
||||
target = self.target_cls()
|
||||
|
||||
library = target.compile_and_link([module])
|
||||
stripped_library = target.strip(library)
|
||||
|
@ -125,7 +130,6 @@ class Core:
|
|||
|
||||
if self.first_run:
|
||||
self.comm.check_system_info()
|
||||
self.comm.switch_clock(self.external_clock)
|
||||
self.first_run = False
|
||||
|
||||
self.comm.load(kernel_library)
|
||||
|
@ -136,7 +140,7 @@ class Core:
|
|||
|
||||
@portable
|
||||
def seconds_to_mu(self, seconds):
|
||||
"""Converts seconds to the corresponding number of machine units
|
||||
"""Convert seconds to the corresponding number of machine units
|
||||
(RTIO cycles).
|
||||
|
||||
:param seconds: time (in seconds) to convert.
|
||||
|
@ -145,7 +149,7 @@ class Core:
|
|||
|
||||
@portable
|
||||
def mu_to_seconds(self, mu):
|
||||
"""Converts machine units (RTIO cycles) to seconds.
|
||||
"""Convert machine units (RTIO cycles) to seconds.
|
||||
|
||||
:param mu: cycle count to convert.
|
||||
"""
|
||||
|
@ -153,8 +157,35 @@ class Core:
|
|||
|
||||
@kernel
|
||||
def get_rtio_counter_mu(self):
|
||||
"""Retrieve the current value of the hardware RTIO timeline counter.
|
||||
|
||||
As the timing of kernel code executed on the CPU is inherently
|
||||
non-deterministic, the return value is by necessity only a lower bound
|
||||
for the actual value of the hardware register at the instant when
|
||||
execution resumes in the caller.
|
||||
|
||||
For a more detailed description of these concepts, see :doc:`/rtio`.
|
||||
"""
|
||||
return rtio_get_counter()
|
||||
|
||||
@kernel
|
||||
def wait_until_mu(self, cursor_mu):
|
||||
"""Block execution until the hardware RTIO counter reaches the given
|
||||
value (see :meth:`get_rtio_counter_mu`).
|
||||
|
||||
If the hardware counter has already passed the given time, the function
|
||||
returns immediately.
|
||||
"""
|
||||
while self.get_rtio_counter_mu() < cursor_mu:
|
||||
pass
|
||||
|
||||
@kernel
|
||||
def get_rtio_destination_status(self, destination):
|
||||
"""Returns whether the specified RTIO destination is up.
|
||||
This is particularly useful in startup kernels to delay
|
||||
startup until certain DRTIO destinations are up."""
|
||||
return rtio_get_destination_status(destination)
|
||||
|
||||
@kernel
|
||||
def reset(self):
|
||||
"""Clear RTIO FIFOs, release RTIO PHY reset, and set the time cursor
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
class DAC34H84:
|
||||
"""DAC34H84 settings and register map.
|
||||
|
||||
For possible values, documentation, and explanation, see the DAC datasheet
|
||||
at https://www.ti.com/lit/pdf/slas751
|
||||
"""
|
||||
qmc_corr_ena = 0 # msb ab
|
||||
qmc_offset_ena = 0 # msb ab
|
||||
invsinc_ena = 0 # msb ab
|
||||
interpolation = 1 # 2x
|
||||
fifo_ena = 1
|
||||
alarm_out_ena = 1
|
||||
alarm_out_pol = 1
|
||||
clkdiv_sync_ena = 1
|
||||
|
||||
iotest_ena = 0
|
||||
cnt64_ena = 0
|
||||
oddeven_parity = 0 # even
|
||||
single_parity_ena = 1
|
||||
dual_parity_ena = 0
|
||||
rev_interface = 0
|
||||
dac_complement = 0b0000 # msb A
|
||||
alarm_fifo = 0b111 # msb 2-away
|
||||
|
||||
dacclkgone_ena = 1
|
||||
dataclkgone_ena = 1
|
||||
collisiongone_ena = 1
|
||||
sif4_ena = 1
|
||||
mixer_ena = 0
|
||||
mixer_gain = 1
|
||||
nco_ena = 0
|
||||
revbus = 0
|
||||
twos = 1
|
||||
|
||||
coarse_dac = 9 # 18.75 mA, 0-15
|
||||
sif_txenable = 0
|
||||
|
||||
mask_alarm_from_zerochk = 0
|
||||
mask_alarm_fifo_collision = 0
|
||||
mask_alarm_fifo_1away = 0
|
||||
mask_alarm_fifo_2away = 0
|
||||
mask_alarm_dacclk_gone = 0
|
||||
mask_alarm_dataclk_gone = 0
|
||||
mask_alarm_output_gone = 0
|
||||
mask_alarm_from_iotest = 0
|
||||
mask_alarm_from_pll = 0
|
||||
mask_alarm_parity = 0b0000 # msb a
|
||||
|
||||
qmc_offseta = 0 # 12b
|
||||
fifo_offset = 2 # 0-7
|
||||
qmc_offsetb = 0 # 12b
|
||||
|
||||
qmc_offsetc = 0 # 12b
|
||||
|
||||
qmc_offsetd = 0 # 12b
|
||||
|
||||
qmc_gaina = 0 # 11b
|
||||
|
||||
cmix_fs8 = 0
|
||||
cmix_fs4 = 0
|
||||
cmix_fs2 = 0
|
||||
cmix_nfs4 = 0
|
||||
qmc_gainb = 0 # 11b
|
||||
|
||||
qmc_gainc = 0 # 11b
|
||||
|
||||
output_delayab = 0b00
|
||||
output_delaycd = 0b00
|
||||
qmc_gaind = 0 # 11b
|
||||
|
||||
qmc_phaseab = 0 # 12b
|
||||
|
||||
qmc_phasecd = 0 # 12b
|
||||
|
||||
phase_offsetab = 0 # 16b
|
||||
phase_offsetcd = 0 # 16b
|
||||
phase_addab_lsb = 0 # 16b
|
||||
phase_addab_msb = 0 # 16b
|
||||
phase_addcd_lsb = 0 # 16b
|
||||
phase_addcd_msb = 0 # 16b
|
||||
|
||||
pll_reset = 0
|
||||
pll_ndivsync_ena = 1
|
||||
pll_ena = 1
|
||||
pll_cp = 0b01 # single charge pump
|
||||
pll_p = 0b100 # p=4
|
||||
|
||||
pll_m2 = 1 # x2
|
||||
pll_m = 8 # m = 8
|
||||
pll_n = 0b0001 # n = 2
|
||||
pll_vcotune = 0b01
|
||||
|
||||
pll_vco = 0x3f # 4 GHz
|
||||
bias_sleep = 0
|
||||
tsense_sleep = 0
|
||||
pll_sleep = 0
|
||||
clkrecv_sleep = 0
|
||||
dac_sleep = 0b0000 # msb a
|
||||
|
||||
extref_ena = 0
|
||||
fuse_sleep = 1
|
||||
atest = 0b00000 # atest mode
|
||||
|
||||
syncsel_qmcoffsetab = 0b1001 # sif_sync and register write
|
||||
syncsel_qmcoffsetcd = 0b1001 # sif_sync and register write
|
||||
syncsel_qmccorrab = 0b1001 # sif_sync and register write
|
||||
syncsel_qmccorrcd = 0b1001 # sif_sync and register write
|
||||
|
||||
syncsel_mixerab = 0b1001 # sif_sync and register write
|
||||
syncsel_mixercd = 0b1001 # sif_sync and register write
|
||||
syncsel_nco = 0b1000 # sif_sync
|
||||
syncsel_fifo_input = 0b10 # external lvds istr
|
||||
sif_sync = 1
|
||||
|
||||
syncsel_fifoin = 0b0010 # istr
|
||||
syncsel_fifoout = 0b0100 # ostr
|
||||
clkdiv_sync_sel = 0 # ostr
|
||||
|
||||
path_a_sel = 0
|
||||
path_b_sel = 1
|
||||
path_c_sel = 2
|
||||
path_d_sel = 3
|
||||
# swap dac pairs (CDAB) for layout
|
||||
# swap I-Q dacs for spectral inversion
|
||||
dac_a_sel = 3
|
||||
dac_b_sel = 2
|
||||
dac_c_sel = 1
|
||||
dac_d_sel = 0
|
||||
|
||||
dac_sleep_en = 0b1111 # msb a
|
||||
clkrecv_sleep_en = 1
|
||||
pll_sleep_en = 1
|
||||
lvds_data_sleep_en = 1
|
||||
lvds_control_sleep_en = 1
|
||||
temp_sense_sleep_en = 1
|
||||
bias_sleep_en = 1
|
||||
|
||||
data_dly = 2
|
||||
clk_dly = 0
|
||||
|
||||
ostrtodig_sel = 0
|
||||
ramp_ena = 0
|
||||
sifdac_ena = 0
|
||||
|
||||
grp_delaya = 0x00
|
||||
grp_delayb = 0x00
|
||||
|
||||
grp_delayc = 0x00
|
||||
grp_delayd = 0x00
|
||||
|
||||
sifdac = 0
|
||||
|
||||
def __init__(self, updates=None):
|
||||
if updates is None:
|
||||
return
|
||||
for key, value in updates.items():
|
||||
if not hasattr(self, key):
|
||||
raise KeyError("invalid setting", key)
|
||||
setattr(self, key, value)
|
||||
|
||||
def get_mmap(self):
|
||||
mmap = []
|
||||
mmap.append(
|
||||
(0x00 << 16) |
|
||||
(self.qmc_offset_ena << 14) | (self.qmc_corr_ena << 12) |
|
||||
(self.interpolation << 8) | (self.fifo_ena << 7) |
|
||||
(self.alarm_out_ena << 4) | (self.alarm_out_pol << 3) |
|
||||
(self.clkdiv_sync_ena << 2) | (self.invsinc_ena << 0))
|
||||
mmap.append(
|
||||
(0x01 << 16) |
|
||||
(self.iotest_ena << 15) | (self.cnt64_ena << 12) |
|
||||
(self.oddeven_parity << 11) | (self.single_parity_ena << 10) |
|
||||
(self.dual_parity_ena << 9) | (self.rev_interface << 8) |
|
||||
(self.dac_complement << 4) | (self.alarm_fifo << 1))
|
||||
mmap.append(
|
||||
(0x02 << 16) |
|
||||
(self.dacclkgone_ena << 14) | (self.dataclkgone_ena << 13) |
|
||||
(self.collisiongone_ena << 12) | (self.sif4_ena << 7) |
|
||||
(self.mixer_ena << 6) | (self.mixer_gain << 5) |
|
||||
(self.nco_ena << 4) | (self.revbus << 3) | (self.twos << 1))
|
||||
mmap.append((0x03 << 16) | (self.coarse_dac << 12) | (self.sif_txenable << 0))
|
||||
mmap.append(
|
||||
(0x07 << 16) |
|
||||
(self.mask_alarm_from_zerochk << 15) | (1 << 14) |
|
||||
(self.mask_alarm_fifo_collision << 13) |
|
||||
(self.mask_alarm_fifo_1away << 12) |
|
||||
(self.mask_alarm_fifo_2away << 11) |
|
||||
(self.mask_alarm_dacclk_gone << 10) |
|
||||
(self.mask_alarm_dataclk_gone << 9) |
|
||||
(self.mask_alarm_output_gone << 8) |
|
||||
(self.mask_alarm_from_iotest << 7) | (1 << 6) |
|
||||
(self.mask_alarm_from_pll << 5) | (self.mask_alarm_parity << 1))
|
||||
mmap.append(
|
||||
(0x08 << 16) | (self.qmc_offseta << 0))
|
||||
mmap.append(
|
||||
(0x09 << 16) | (self.fifo_offset << 13) | (self.qmc_offsetb << 0))
|
||||
mmap.append((0x0a << 16) | (self.qmc_offsetc << 0))
|
||||
mmap.append((0x0b << 16) | (self.qmc_offsetd << 0))
|
||||
mmap.append((0x0c << 16) | (self.qmc_gaina << 0))
|
||||
mmap.append(
|
||||
(0x0d << 16) |
|
||||
(self.cmix_fs8 << 15) | (self.cmix_fs4 << 14) |
|
||||
(self.cmix_fs2 << 12) | (self.cmix_nfs4 << 11) |
|
||||
(self.qmc_gainb << 0))
|
||||
mmap.append((0x0e << 16) | (self.qmc_gainc << 0))
|
||||
mmap.append(
|
||||
(0x0f << 16) |
|
||||
(self.output_delayab << 14) | (self.output_delaycd << 12) |
|
||||
(self.qmc_gaind << 0))
|
||||
mmap.append((0x10 << 16) | (self.qmc_phaseab << 0))
|
||||
mmap.append((0x11 << 16) | (self.qmc_phasecd << 0))
|
||||
mmap.append((0x12 << 16) | (self.phase_offsetab << 0))
|
||||
mmap.append((0x13 << 16) | (self.phase_offsetcd << 0))
|
||||
mmap.append((0x14 << 16) | (self.phase_addab_lsb << 0))
|
||||
mmap.append((0x15 << 16) | (self.phase_addab_msb << 0))
|
||||
mmap.append((0x16 << 16) | (self.phase_addcd_lsb << 0))
|
||||
mmap.append((0x17 << 16) | (self.phase_addcd_msb << 0))
|
||||
mmap.append(
|
||||
(0x18 << 16) |
|
||||
(0b001 << 13) | (self.pll_reset << 12) |
|
||||
(self.pll_ndivsync_ena << 11) | (self.pll_ena << 10) |
|
||||
(self.pll_cp << 6) | (self.pll_p << 3))
|
||||
mmap.append(
|
||||
(0x19 << 16) |
|
||||
(self.pll_m2 << 15) | (self.pll_m << 8) | (self.pll_n << 4) |
|
||||
(self.pll_vcotune << 2))
|
||||
mmap.append(
|
||||
(0x1a << 16) |
|
||||
(self.pll_vco << 10) | (self.bias_sleep << 7) |
|
||||
(self.tsense_sleep << 6) |
|
||||
(self.pll_sleep << 5) | (self.clkrecv_sleep << 4) |
|
||||
(self.dac_sleep << 0))
|
||||
mmap.append(
|
||||
(0x1b << 16) |
|
||||
(self.extref_ena << 15) | (self.fuse_sleep << 11) |
|
||||
(self.atest << 0))
|
||||
mmap.append(
|
||||
(0x1e << 16) |
|
||||
(self.syncsel_qmcoffsetab << 12) |
|
||||
(self.syncsel_qmcoffsetcd << 8) |
|
||||
(self.syncsel_qmccorrab << 4) |
|
||||
(self.syncsel_qmccorrcd << 0))
|
||||
mmap.append(
|
||||
(0x1f << 16) |
|
||||
(self.syncsel_mixerab << 12) | (self.syncsel_mixercd << 8) |
|
||||
(self.syncsel_nco << 4) | (self.syncsel_fifo_input << 2) |
|
||||
(self.sif_sync << 1))
|
||||
mmap.append(
|
||||
(0x20 << 16) |
|
||||
(self.syncsel_fifoin << 12) | (self.syncsel_fifoout << 8) |
|
||||
(self.clkdiv_sync_sel << 0))
|
||||
mmap.append(
|
||||
(0x22 << 16) |
|
||||
(self.path_a_sel << 14) | (self.path_b_sel << 12) |
|
||||
(self.path_c_sel << 10) | (self.path_d_sel << 8) |
|
||||
(self.dac_a_sel << 6) | (self.dac_b_sel << 4) |
|
||||
(self.dac_c_sel << 2) | (self.dac_d_sel << 0))
|
||||
mmap.append(
|
||||
(0x23 << 16) |
|
||||
(self.dac_sleep_en << 12) | (self.clkrecv_sleep_en << 11) |
|
||||
(self.pll_sleep_en << 10) | (self.lvds_data_sleep_en << 9) |
|
||||
(self.lvds_control_sleep_en << 8) |
|
||||
(self.temp_sense_sleep_en << 7) | (1 << 6) |
|
||||
(self.bias_sleep_en << 5) | (0x1f << 0))
|
||||
mmap.append(
|
||||
(0x24 << 16) | (self.data_dly << 13) | (self.clk_dly << 10))
|
||||
mmap.append(
|
||||
(0x2d << 16) |
|
||||
(self.ostrtodig_sel << 14) | (self.ramp_ena << 13) |
|
||||
(0x002 << 1) | (self.sifdac_ena << 0))
|
||||
mmap.append(
|
||||
(0x2e << 16) | (self.grp_delaya << 8) | (self.grp_delayb << 0))
|
||||
mmap.append(
|
||||
(0x2f << 16) | (self.grp_delayc << 8) | (self.grp_delayd << 0))
|
||||
mmap.append((0x30 << 16) | self.sifdac)
|
||||
return mmap
|
|
@ -1,401 +0,0 @@
|
|||
"""
|
||||
Drivers for direct digital synthesis (DDS) chips on RTIO.
|
||||
|
||||
Output event replacement is not supported and issuing commands at the same
|
||||
time is an error.
|
||||
"""
|
||||
|
||||
|
||||
from artiq.language.core import *
|
||||
from artiq.language.types import *
|
||||
from artiq.language.units import *
|
||||
from artiq.coredevice.rtio import rtio_output
|
||||
from artiq.coredevice.exceptions import DDSError
|
||||
|
||||
from numpy import int32, int64
|
||||
|
||||
|
||||
_PHASE_MODE_DEFAULT = -1
|
||||
PHASE_MODE_CONTINUOUS = 0
|
||||
PHASE_MODE_ABSOLUTE = 1
|
||||
PHASE_MODE_TRACKING = 2
|
||||
|
||||
|
||||
class DDSParams:
|
||||
def __init__(self):
|
||||
self.bus_channel = 0
|
||||
self.channel = 0
|
||||
self.ftw = 0
|
||||
self.pow = 0
|
||||
self.phase_mode = 0
|
||||
self.amplitude = 0
|
||||
|
||||
|
||||
class BatchContextManager:
|
||||
kernel_invariants = {"core", "core_dds", "params"}
|
||||
|
||||
def __init__(self, core_dds):
|
||||
self.core_dds = core_dds
|
||||
self.core = self.core_dds.core
|
||||
self.active = False
|
||||
self.params = [DDSParams() for _ in range(16)]
|
||||
self.count = 0
|
||||
self.ref_time = int64(0)
|
||||
|
||||
@kernel
|
||||
def __enter__(self):
|
||||
"""Starts a DDS command batch. All DDS commands are buffered
|
||||
after this call, until ``batch_exit`` is called.
|
||||
|
||||
The time of execution of the DDS commands is the time cursor position
|
||||
when the batch is entered."""
|
||||
if self.active:
|
||||
raise DDSError("DDS batch entered twice")
|
||||
|
||||
self.active = True
|
||||
self.count = 0
|
||||
self.ref_time = now_mu()
|
||||
|
||||
@kernel
|
||||
def append(self, bus_channel, channel, ftw, pow, phase_mode, amplitude):
|
||||
if self.count == len(self.params):
|
||||
raise DDSError("Too many commands in DDS batch")
|
||||
|
||||
params = self.params[self.count]
|
||||
params.bus_channel = bus_channel
|
||||
params.channel = channel
|
||||
params.ftw = ftw
|
||||
params.pow = pow
|
||||
params.phase_mode = phase_mode
|
||||
params.amplitude = amplitude
|
||||
self.count += 1
|
||||
|
||||
@kernel
|
||||
def __exit__(self, type, value, traceback):
|
||||
"""Ends a DDS command batch. All buffered DDS commands are issued
|
||||
on the bus."""
|
||||
if not self.active:
|
||||
raise DDSError("DDS batch exited twice")
|
||||
|
||||
self.active = False
|
||||
at_mu(self.ref_time - self.core_dds.batch_duration_mu())
|
||||
for i in range(self.count):
|
||||
param = self.params[i]
|
||||
self.core_dds.program(self.ref_time,
|
||||
param.bus_channel, param.channel, param.ftw,
|
||||
param.pow, param.phase_mode, param.amplitude)
|
||||
|
||||
|
||||
class DDSGroup:
|
||||
"""Core device Direct Digital Synthesis (DDS) driver.
|
||||
|
||||
Gives access to the DDS functionality of the core device.
|
||||
|
||||
:param sysclk: DDS system frequency. The DDS system clock must be a
|
||||
phase-locked multiple of the RTIO clock.
|
||||
"""
|
||||
|
||||
kernel_invariants = {"core", "sysclk", "batch"}
|
||||
|
||||
def __init__(self, dmgr, sysclk, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.sysclk = sysclk
|
||||
self.batch = BatchContextManager(self)
|
||||
|
||||
@kernel
|
||||
def batch_duration_mu(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@kernel
|
||||
def init(self, bus_channel, channel):
|
||||
raise NotImplementedError
|
||||
|
||||
@kernel
|
||||
def program(self, ref_time, bus_channel, channel, ftw, pow, phase_mode, amplitude):
|
||||
raise NotImplementedError
|
||||
|
||||
@kernel
|
||||
def set(self, bus_channel, channel, ftw, pow, phase_mode, amplitude):
|
||||
if self.batch.active:
|
||||
self.batch.append(bus_channel, channel, ftw, pow, phase_mode, amplitude)
|
||||
else:
|
||||
ref_time = now_mu()
|
||||
at_mu(ref_time - self.program_duration_mu)
|
||||
self.program(ref_time,
|
||||
bus_channel, channel, ftw, pow, phase_mode, amplitude)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def frequency_to_ftw(self, frequency):
|
||||
"""Returns the frequency tuning word corresponding to the given
|
||||
frequency.
|
||||
"""
|
||||
return round(float(int64(2)**32*frequency/self.sysclk))
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def ftw_to_frequency(self, ftw):
|
||||
"""Returns the frequency corresponding to the given frequency tuning
|
||||
word.
|
||||
"""
|
||||
return ftw*self.sysclk/int64(2)**32
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def turns_to_pow(self, turns):
|
||||
"""Returns the phase offset word corresponding to the given phase
|
||||
in turns."""
|
||||
return round(float(turns*2**self.pow_width))
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def pow_to_turns(self, pow):
|
||||
"""Returns the phase in turns corresponding to the given phase offset
|
||||
word."""
|
||||
return pow/2**self.pow_width
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def amplitude_to_asf(self, amplitude):
|
||||
"""Returns amplitude scale factor corresponding to given amplitude."""
|
||||
return round(float(amplitude*0x0fff))
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def asf_to_amplitude(self, asf):
|
||||
"""Returns the amplitude corresponding to the given amplitude scale
|
||||
factor."""
|
||||
return asf/0x0fff
|
||||
|
||||
|
||||
class DDSChannel:
|
||||
"""Core device Direct Digital Synthesis (DDS) channel driver.
|
||||
|
||||
Controls one DDS channel managed directly by the core device's runtime.
|
||||
|
||||
This class should not be used directly, instead, use the chip-specific
|
||||
drivers such as ``DDSChannelAD9914``.
|
||||
|
||||
The time cursor is not modified by any function in this class.
|
||||
|
||||
:param bus: name of the DDS bus device that this DDS is connected to.
|
||||
:param channel: channel number of the DDS device to control.
|
||||
"""
|
||||
|
||||
kernel_invariants = {
|
||||
"core", "core_dds", "bus_channel", "channel",
|
||||
}
|
||||
|
||||
def __init__(self, dmgr, bus_channel, channel, core_dds_device="core_dds"):
|
||||
self.core_dds = dmgr.get(core_dds_device)
|
||||
self.core = self.core_dds.core
|
||||
self.bus_channel = bus_channel
|
||||
self.channel = channel
|
||||
self.phase_mode = PHASE_MODE_CONTINUOUS
|
||||
|
||||
@kernel
|
||||
def init(self):
|
||||
"""Resets and initializes the DDS channel.
|
||||
|
||||
This needs to be done for each DDS channel before it can be used, and
|
||||
it is recommended to use the startup kernel for this.
|
||||
|
||||
This function cannot be used in a batch; the correct way of
|
||||
initializing multiple DDS channels is to call this function
|
||||
sequentially with a delay between the calls. 2ms provides a good
|
||||
timing margin."""
|
||||
self.core_dds.init(self.bus_channel, self.channel)
|
||||
|
||||
@kernel
|
||||
def set_phase_mode(self, phase_mode):
|
||||
"""Sets the phase mode of the DDS channel. Supported phase modes are:
|
||||
|
||||
* ``PHASE_MODE_CONTINUOUS``: the phase accumulator is unchanged when
|
||||
switching frequencies. The DDS phase is the sum of the phase
|
||||
accumulator and the phase offset. The only discrete jumps in the
|
||||
DDS output phase come from changes to the phase offset.
|
||||
|
||||
* ``PHASE_MODE_ABSOLUTE``: the phase accumulator is reset when
|
||||
switching frequencies. Thus, the phase of the DDS at the time of
|
||||
the frequency change is equal to the phase offset.
|
||||
|
||||
* ``PHASE_MODE_TRACKING``: when switching frequencies, the phase
|
||||
accumulator is set to the value it would have if the DDS had been
|
||||
running at the specified frequency since the start of the
|
||||
experiment.
|
||||
"""
|
||||
self.phase_mode = phase_mode
|
||||
|
||||
@kernel
|
||||
def set_mu(self, frequency, phase=0, phase_mode=_PHASE_MODE_DEFAULT,
|
||||
amplitude=0x0fff):
|
||||
"""Sets the DDS channel to the specified frequency and phase.
|
||||
|
||||
This uses machine units (FTW and POW). The frequency tuning word width
|
||||
is 32, whereas the phase offset word width depends on the type of DDS
|
||||
chip and can be retrieved via the ``pow_width`` attribute. The amplitude
|
||||
width is 12.
|
||||
|
||||
The "frequency update" pulse is sent to the DDS with a fixed latency
|
||||
with respect to the current position of the time cursor.
|
||||
|
||||
:param frequency: frequency to generate.
|
||||
:param phase: adds an offset, in turns, to the phase.
|
||||
:param phase_mode: if specified, overrides the default phase mode set
|
||||
by ``set_phase_mode`` for this call.
|
||||
"""
|
||||
if phase_mode == _PHASE_MODE_DEFAULT:
|
||||
phase_mode = self.phase_mode
|
||||
self.core_dds.set(self.bus_channel, self.channel, frequency, phase, phase_mode, amplitude)
|
||||
|
||||
@kernel
|
||||
def set(self, frequency, phase=0.0, phase_mode=_PHASE_MODE_DEFAULT,
|
||||
amplitude=1.0):
|
||||
"""Like ``set_mu``, but uses Hz and turns."""
|
||||
self.set_mu(self.core_dds.frequency_to_ftw(frequency),
|
||||
self.core_dds.turns_to_pow(phase), phase_mode,
|
||||
self.core_dds.amplitude_to_asf(amplitude))
|
||||
|
||||
|
||||
AD9914_REG_CFR1L = 0x01
|
||||
AD9914_REG_CFR1H = 0x03
|
||||
AD9914_REG_CFR2L = 0x05
|
||||
AD9914_REG_CFR2H = 0x07
|
||||
AD9914_REG_CFR3L = 0x09
|
||||
AD9914_REG_CFR3H = 0x0b
|
||||
AD9914_REG_CFR4L = 0x0d
|
||||
AD9914_REG_CFR4H = 0x0f
|
||||
AD9914_REG_FTWL = 0x2d
|
||||
AD9914_REG_FTWH = 0x2f
|
||||
AD9914_REG_POW = 0x31
|
||||
AD9914_REG_ASF = 0x33
|
||||
AD9914_REG_USR0 = 0x6d
|
||||
AD9914_FUD = 0x80
|
||||
AD9914_GPIO = 0x81
|
||||
|
||||
|
||||
class DDSGroupAD9914(DDSGroup):
|
||||
"""Driver for AD9914 DDS chips. See ``DDSGroup`` for a description
|
||||
of the functionality."""
|
||||
kernel_invariants = DDSGroup.kernel_invariants.union({
|
||||
"pow_width", "rtio_period_mu", "sysclk_per_mu", "write_duration_mu", "dac_cal_duration_mu",
|
||||
"init_duration_mu", "init_sync_duration_mu", "program_duration_mu",
|
||||
"first_dds_bus_channel", "dds_channel_count", "continuous_phase_comp"
|
||||
})
|
||||
|
||||
pow_width = 16
|
||||
|
||||
def __init__(self, *args, first_dds_bus_channel, dds_bus_count, dds_channel_count, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.first_dds_bus_channel = first_dds_bus_channel
|
||||
self.dds_bus_count = dds_bus_count
|
||||
self.dds_channel_count = dds_channel_count
|
||||
|
||||
self.rtio_period_mu = int64(8)
|
||||
self.sysclk_per_mu = int32(self.sysclk * self.core.ref_period)
|
||||
|
||||
self.write_duration_mu = 5 * self.rtio_period_mu
|
||||
self.dac_cal_duration_mu = 147000 * self.rtio_period_mu
|
||||
self.init_duration_mu = 8 * self.write_duration_mu + self.dac_cal_duration_mu
|
||||
self.init_sync_duration_mu = 16 * self.write_duration_mu + 2 * self.dac_cal_duration_mu
|
||||
self.program_duration_mu = 6 * self.write_duration_mu
|
||||
|
||||
self.continuous_phase_comp = [0] * (self.dds_bus_count * self.dds_channel_count)
|
||||
|
||||
@kernel
|
||||
def batch_duration_mu(self):
|
||||
return self.batch.count * (self.program_duration_mu +
|
||||
self.write_duration_mu) # + FUD time
|
||||
|
||||
@kernel
|
||||
def write(self, bus_channel, addr, data):
|
||||
rtio_output(now_mu(), bus_channel, addr, data)
|
||||
delay_mu(self.write_duration_mu)
|
||||
|
||||
@kernel
|
||||
def init(self, bus_channel, channel):
|
||||
delay_mu(-self.init_duration_mu)
|
||||
self.write(bus_channel, AD9914_GPIO, (1 << channel) << 1);
|
||||
|
||||
self.write(bus_channel, AD9914_REG_CFR1H, 0x0000) # Enable cosine output
|
||||
self.write(bus_channel, AD9914_REG_CFR2L, 0x8900) # Enable matched latency
|
||||
self.write(bus_channel, AD9914_REG_CFR2H, 0x0080) # Enable profile mode
|
||||
self.write(bus_channel, AD9914_REG_ASF, 0x0fff) # Set amplitude to maximum
|
||||
self.write(bus_channel, AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
delay_mu(self.dac_cal_duration_mu)
|
||||
self.write(bus_channel, AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
|
||||
@kernel
|
||||
def init_sync(self, bus_channel, channel, sync_delay):
|
||||
delay_mu(-self.init_sync_duration_mu)
|
||||
self.write(bus_channel, AD9914_GPIO, (1 << channel) << 1)
|
||||
|
||||
self.write(bus_channel, AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
delay_mu(self.dac_cal_duration_mu)
|
||||
self.write(bus_channel, AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
self.write(bus_channel, AD9914_REG_CFR2L, 0x8b00) # Enable matched latency and sync_out
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
# Set cal with sync and set sync_out and sync_in delay
|
||||
self.write(bus_channel, AD9914_REG_USR0, 0x0840 | (sync_delay & 0x3f))
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
self.write(bus_channel, AD9914_REG_CFR4H, 0x0105) # Enable DAC calibration
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
delay_mu(self.dac_cal_duration_mu)
|
||||
self.write(bus_channel, AD9914_REG_CFR4H, 0x0005) # Disable DAC calibration
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
self.write(bus_channel, AD9914_REG_CFR1H, 0x0000) # Enable cosine output
|
||||
self.write(bus_channel, AD9914_REG_CFR2H, 0x0080) # Enable profile mode
|
||||
self.write(bus_channel, AD9914_REG_ASF, 0x0fff) # Set amplitude to maximum
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
|
||||
@kernel
|
||||
def program(self, ref_time, bus_channel, channel, ftw, pow, phase_mode, amplitude):
|
||||
self.write(bus_channel, AD9914_GPIO, (1 << channel) << 1)
|
||||
|
||||
self.write(bus_channel, AD9914_REG_FTWL, ftw & 0xffff)
|
||||
self.write(bus_channel, AD9914_REG_FTWH, (ftw >> 16) & 0xffff)
|
||||
|
||||
# We need the RTIO fine timestamp clock to be phase-locked
|
||||
# to DDS SYSCLK, and divided by an integer self.sysclk_per_mu.
|
||||
dds_bus_index = bus_channel - self.first_dds_bus_channel
|
||||
phase_comp_index = dds_bus_index * self.dds_channel_count + channel
|
||||
if phase_mode == PHASE_MODE_CONTINUOUS:
|
||||
# Do not clear phase accumulator on FUD
|
||||
# Disable autoclear phase accumulator and enables OSK.
|
||||
self.write(bus_channel, AD9914_REG_CFR1L, 0x0108)
|
||||
pow += self.continuous_phase_comp[phase_comp_index]
|
||||
else:
|
||||
# Clear phase accumulator on FUD
|
||||
# Enable autoclear phase accumulator and enables OSK.
|
||||
self.write(bus_channel, AD9914_REG_CFR1L, 0x2108)
|
||||
fud_time = now_mu() + 2 * self.write_duration_mu
|
||||
pow -= int32((ref_time - fud_time) * self.sysclk_per_mu * ftw >> (32 - self.pow_width))
|
||||
if phase_mode == PHASE_MODE_TRACKING:
|
||||
pow += int32(ref_time * self.sysclk_per_mu * ftw >> (32 - self.pow_width))
|
||||
self.continuous_phase_comp[phase_comp_index] = pow
|
||||
|
||||
self.write(bus_channel, AD9914_REG_POW, pow)
|
||||
self.write(bus_channel, AD9914_REG_ASF, amplitude)
|
||||
self.write(bus_channel, AD9914_FUD, 0)
|
||||
|
||||
|
||||
class DDSChannelAD9914(DDSChannel):
|
||||
"""Driver for AD9914 DDS chips. See ``DDSChannel`` for a description
|
||||
of the functionality."""
|
||||
@kernel
|
||||
def init_sync(self, sync_delay=0):
|
||||
"""Resets and initializes the DDS channel as well as configures
|
||||
the AD9914 DDS for synchronisation. The synchronisation procedure
|
||||
follows the steps outlined in the AN-1254 application note.
|
||||
|
||||
This needs to be done for each DDS channel before it can be used, and
|
||||
it is recommended to use the startup kernel for this.
|
||||
|
||||
This function cannot be used in a batch; the correct way of
|
||||
initializing multiple DDS channels is to call this function
|
||||
sequentially with a delay between the calls. 10ms provides a good
|
||||
timing margin.
|
||||
|
||||
:param sync_delay: integer from 0 to 0x3f that sets the value of
|
||||
SYNC_OUT (bits 3-5) and SYNC_IN (bits 0-2) delay ADJ bits.
|
||||
"""
|
||||
self.core_dds.init_sync(self.bus_channel, self.channel, sync_delay)
|
|
@ -34,7 +34,7 @@ def dma_playback(timestamp: TInt64, ptr: TInt32) -> TNone:
|
|||
|
||||
|
||||
class DMARecordContextManager:
|
||||
"""Context manager returned by ``CoreDMA.record()``.
|
||||
"""Context manager returned by :meth:`CoreDMA.record()`.
|
||||
|
||||
Upon entering, starts recording a DMA trace. All RTIO operations are
|
||||
redirected to a newly created DMA buffer after this call, and ``now``
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
"""
|
||||
DRTIO debugging functions.
|
||||
|
||||
Those syscalls are intended for ARTIQ developers only.
|
||||
"""
|
||||
|
||||
from artiq.language.core import syscall
|
||||
from artiq.language.types import TTuple, TInt32, TInt64, TNone
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def drtio_get_channel_state(channel: TInt32) -> TTuple([TInt32, TInt64]):
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def drtio_reset_channel_state(channel: TInt32) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def drtio_get_fifo_space(channel: TInt32) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def drtio_get_packet_counts(linkno: TInt32) -> TTuple([TInt32, TInt32]):
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def drtio_get_fifo_space_req_count(linkno: TInt32) -> TInt32:
|
||||
raise NotImplementedError("syscall not simulated")
|
|
@ -0,0 +1,236 @@
|
|||
"""Driver for RTIO-enabled TTL edge counter.
|
||||
|
||||
Like for the TTL input PHYs, sensitivity can be configured over RTIO
|
||||
(``gate_rising()``, etc.). In contrast to the former, however, the count is
|
||||
accumulated in gateware, and only a single input event is generated at the end
|
||||
of each gate period::
|
||||
|
||||
with parallel:
|
||||
doppler_cool()
|
||||
self.pmt_counter.gate_rising(1 * ms)
|
||||
|
||||
with parallel:
|
||||
readout()
|
||||
self.pmt_counter.gate_rising(100 * us)
|
||||
|
||||
print("Doppler cooling counts:", self.pmt_counter.fetch_count())
|
||||
print("Readout counts:", self.pmt_counter.fetch_count())
|
||||
|
||||
For applications where the timestamps of the individual input events are not
|
||||
required, this has two advantages over ``TTLInOut.count()`` beyond raw
|
||||
throughput. First, it is easy to count events during multiple separate periods
|
||||
without blocking to read back counts in between, as illustrated in the above
|
||||
example. Secondly, as each count total only takes up a single input event, it
|
||||
is much easier to acquire counts on several channels in parallel without
|
||||
risking input FIFO overflows::
|
||||
|
||||
# Using the TTLInOut driver, pmt_1 input events are only processed
|
||||
# after pmt_0 is done counting. To avoid RTIOOverflows, a round-robin
|
||||
# scheme would have to be implemented manually.
|
||||
|
||||
with parallel:
|
||||
self.pmt_0.gate_rising(10 * ms)
|
||||
self.pmt_1.gate_rising(10 * ms)
|
||||
|
||||
counts_0 = self.pmt_0.count(now_mu()) # blocks
|
||||
counts_1 = self.pmt_1.count(now_mu())
|
||||
|
||||
#
|
||||
|
||||
# Using gateware counters, only a single input event each is
|
||||
# generated, greatly reducing the load on the input FIFOs:
|
||||
|
||||
with parallel:
|
||||
self.pmt_0_counter.gate_rising(10 * ms)
|
||||
self.pmt_1_counter.gate_rising(10 * ms)
|
||||
|
||||
counts_0 = self.pmt_0_counter.fetch_count() # blocks
|
||||
counts_1 = self.pmt_1_counter.fetch_count()
|
||||
|
||||
See :mod:`artiq.gateware.rtio.phy.edge_counter` and
|
||||
:meth:`artiq.gateware.eem.DIO.add_std` for the gateware components.
|
||||
"""
|
||||
|
||||
from artiq.language.core import *
|
||||
from artiq.language.types import *
|
||||
from artiq.coredevice.rtio import (rtio_output, rtio_input_data,
|
||||
rtio_input_timestamped_data)
|
||||
from numpy import int32, int64
|
||||
|
||||
CONFIG_COUNT_RISING = 0b0001
|
||||
CONFIG_COUNT_FALLING = 0b0010
|
||||
CONFIG_SEND_COUNT_EVENT = 0b0100
|
||||
CONFIG_RESET_TO_ZERO = 0b1000
|
||||
|
||||
|
||||
class CounterOverflow(Exception):
|
||||
"""Raised when an edge counter value is read which indicates that the
|
||||
counter might have overflowed."""
|
||||
pass
|
||||
|
||||
|
||||
class EdgeCounter:
|
||||
"""RTIO TTL edge counter driver driver.
|
||||
|
||||
Like for regular TTL inputs, timeline periods where the counter is
|
||||
sensitive to a chosen set of input transitions can be specified. Unlike the
|
||||
former, however, the specified edges do not create individual input events;
|
||||
rather, the total count can be requested as a single input event from the
|
||||
core (typically at the end of the gate window).
|
||||
|
||||
:param channel: The RTIO channel of the gateware phy.
|
||||
:param gateware_width: The width of the gateware counter register, in
|
||||
bits. This is only used for overflow handling; to change the size,
|
||||
the gateware needs to be rebuilt.
|
||||
"""
|
||||
|
||||
kernel_invariants = {"core", "channel", "counter_max"}
|
||||
|
||||
def __init__(self, dmgr, channel, gateware_width=31, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.channel = channel
|
||||
self.counter_max = (1 << (gateware_width - 1)) - 1
|
||||
|
||||
@kernel
|
||||
def gate_rising(self, duration):
|
||||
"""Count rising edges for the given duration and request the total at
|
||||
the end.
|
||||
|
||||
The counter is reset at the beginning of the gate period. Use
|
||||
:meth:`set_config` directly for more detailed control.
|
||||
|
||||
:param duration: The duration for which the gate is to stay open.
|
||||
|
||||
:return: The timestamp at the end of the gate period, in machine units.
|
||||
"""
|
||||
return self.gate_rising_mu(self.core.seconds_to_mu(duration))
|
||||
|
||||
@kernel
|
||||
def gate_falling(self, duration):
|
||||
"""Count falling edges for the given duration and request the total at
|
||||
the end.
|
||||
|
||||
The counter is reset at the beginning of the gate period. Use
|
||||
:meth:`set_config` directly for more detailed control.
|
||||
|
||||
:param duration: The duration for which the gate is to stay open.
|
||||
|
||||
:return: The timestamp at the end of the gate period, in machine units.
|
||||
"""
|
||||
return self.gate_falling_mu(self.core.seconds_to_mu(duration))
|
||||
|
||||
@kernel
|
||||
def gate_both(self, duration):
|
||||
"""Count both rising and falling edges for the given duration, and
|
||||
request the total at the end.
|
||||
|
||||
The counter is reset at the beginning of the gate period. Use
|
||||
:meth:`set_config` directly for more detailed control.
|
||||
|
||||
:param duration: The duration for which the gate is to stay open.
|
||||
|
||||
:return: The timestamp at the end of the gate period, in machine units.
|
||||
"""
|
||||
return self.gate_both_mu(self.core.seconds_to_mu(duration))
|
||||
|
||||
@kernel
|
||||
def gate_rising_mu(self, duration_mu):
|
||||
"""See :meth:`gate_rising`."""
|
||||
return self._gate_mu(
|
||||
duration_mu, count_rising=True, count_falling=False)
|
||||
|
||||
@kernel
|
||||
def gate_falling_mu(self, duration_mu):
|
||||
"""See :meth:`gate_falling`."""
|
||||
return self._gate_mu(
|
||||
duration_mu, count_rising=False, count_falling=True)
|
||||
|
||||
@kernel
|
||||
def gate_both_mu(self, duration_mu):
|
||||
"""See :meth:`gate_both_mu`."""
|
||||
return self._gate_mu(
|
||||
duration_mu, count_rising=True, count_falling=True)
|
||||
|
||||
@kernel
|
||||
def _gate_mu(self, duration_mu, count_rising, count_falling):
|
||||
self.set_config(
|
||||
count_rising=count_rising,
|
||||
count_falling=count_falling,
|
||||
send_count_event=False,
|
||||
reset_to_zero=True)
|
||||
delay_mu(duration_mu)
|
||||
self.set_config(
|
||||
count_rising=False,
|
||||
count_falling=False,
|
||||
send_count_event=True,
|
||||
reset_to_zero=False)
|
||||
return now_mu()
|
||||
|
||||
@kernel
|
||||
def set_config(self, count_rising: TBool, count_falling: TBool,
|
||||
send_count_event: TBool, reset_to_zero: TBool):
|
||||
"""Emit an RTIO event at the current timeline position to set the
|
||||
gateware configuration.
|
||||
|
||||
For most use cases, the `gate_*` wrappers will be more convenient.
|
||||
|
||||
:param count_rising: Whether to count rising signal edges.
|
||||
:param count_falling: Whether to count falling signal edges.
|
||||
:param send_count_event: If `True`, an input event with the current
|
||||
counter value is generated on the next clock cycle (once).
|
||||
:param reset_to_zero: If `True`, the counter value is reset to zero on
|
||||
the next clock cycle (once).
|
||||
"""
|
||||
config = int32(0)
|
||||
if count_rising:
|
||||
config |= CONFIG_COUNT_RISING
|
||||
if count_falling:
|
||||
config |= CONFIG_COUNT_FALLING
|
||||
if send_count_event:
|
||||
config |= CONFIG_SEND_COUNT_EVENT
|
||||
if reset_to_zero:
|
||||
config |= CONFIG_RESET_TO_ZERO
|
||||
rtio_output(self.channel << 8, config)
|
||||
|
||||
@kernel
|
||||
def fetch_count(self) -> TInt32:
|
||||
"""Wait for and return count total from previously requested input
|
||||
event.
|
||||
|
||||
It is valid to trigger multiple gate periods without immediately
|
||||
reading back the count total; the results will be returned in order on
|
||||
subsequent fetch calls.
|
||||
|
||||
This function blocks until a result becomes available.
|
||||
"""
|
||||
count = rtio_input_data(self.channel)
|
||||
if count == self.counter_max:
|
||||
raise CounterOverflow(
|
||||
"Input edge counter overflow on RTIO channel {0}",
|
||||
int64(self.channel))
|
||||
return count
|
||||
|
||||
@kernel
|
||||
def fetch_timestamped_count(
|
||||
self, timeout_mu=int64(-1)) -> TTuple([TInt64, TInt32]):
|
||||
"""Wait for and return the timestamp and count total of a previously
|
||||
requested input event.
|
||||
|
||||
It is valid to trigger multiple gate periods without immediately
|
||||
reading back the count total; the results will be returned in order on
|
||||
subsequent fetch calls.
|
||||
|
||||
This function blocks until a result becomes available or the given
|
||||
timeout elapses.
|
||||
|
||||
:return: A tuple of timestamp (-1 if timeout elapsed) and counter
|
||||
value. (The timestamp is that of the requested input event –
|
||||
typically the gate closing time – and not that of any input edges.)
|
||||
"""
|
||||
timestamp, count = rtio_input_timestamped_data(timeout_mu,
|
||||
self.channel)
|
||||
if count == self.counter_max:
|
||||
raise CounterOverflow(
|
||||
"Input edge counter overflow on RTIO channel {0}",
|
||||
int64(self.channel))
|
||||
return timestamp, count
|
|
@ -10,6 +10,8 @@ from artiq.coredevice.runtime import source_loader
|
|||
ZeroDivisionError = builtins.ZeroDivisionError
|
||||
ValueError = builtins.ValueError
|
||||
IndexError = builtins.IndexError
|
||||
RuntimeError = builtins.RuntimeError
|
||||
AssertionError = builtins.AssertionError
|
||||
|
||||
|
||||
class CoreException:
|
||||
|
@ -71,20 +73,13 @@ class CacheError(Exception):
|
|||
|
||||
|
||||
class RTIOUnderflow(Exception):
|
||||
"""Raised when the CPU fails to submit a RTIO event early enough
|
||||
(with respect to the event's timestamp).
|
||||
"""Raised when the CPU or DMA core fails to submit a RTIO event early
|
||||
enough (with respect to the event's timestamp).
|
||||
|
||||
The offending event is discarded and the RTIO core keeps operating.
|
||||
"""
|
||||
artiq_builtin = True
|
||||
|
||||
class RTIOSequenceError(Exception):
|
||||
"""Raised when an event is submitted on a given channel with a timestamp
|
||||
not larger than the previous one.
|
||||
|
||||
The offending event is discarded and the RTIO core keeps operating.
|
||||
"""
|
||||
artiq_builtin = True
|
||||
|
||||
class RTIOOverflow(Exception):
|
||||
"""Raised when at least one event could not be registered into the RTIO
|
||||
|
@ -96,26 +91,28 @@ class RTIOOverflow(Exception):
|
|||
"""
|
||||
artiq_builtin = True
|
||||
|
||||
|
||||
class RTIODestinationUnreachable(Exception):
|
||||
"""Raised with a RTIO operation could not be completed due to a DRTIO link
|
||||
being down.
|
||||
"""
|
||||
artiq_builtin = True
|
||||
|
||||
|
||||
class DMAError(Exception):
|
||||
"""Raised when performing an invalid DMA operation."""
|
||||
artiq_builtin = True
|
||||
|
||||
class DDSError(Exception):
|
||||
"""Raised when attempting to start a DDS batch while already in a batch,
|
||||
when too many commands are batched, and when DDS channel settings are
|
||||
incorrect.
|
||||
"""
|
||||
|
||||
class WatchdogExpired(Exception):
|
||||
"""Raised when a watchdog expires."""
|
||||
|
||||
class ClockFailure(Exception):
|
||||
"""Raised when RTIO PLL has lost lock."""
|
||||
|
||||
|
||||
class I2CError(Exception):
|
||||
"""Raised when a I2C transaction fails."""
|
||||
pass
|
||||
|
||||
|
||||
class SPIError(Exception):
|
||||
"""Raised when a SPI transaction fails."""
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
"""RTIO driver for the Fastino 32channel, 16 bit, 2.5 MS/s per channel,
|
||||
streaming DAC.
|
||||
"""
|
||||
|
||||
from artiq.language.core import kernel, portable, delay
|
||||
from artiq.coredevice.rtio import (rtio_output, rtio_output_wide,
|
||||
rtio_input_data)
|
||||
from artiq.language.units import us
|
||||
from artiq.language.types import TInt32, TList
|
||||
|
||||
|
||||
class Fastino:
|
||||
"""Fastino 32-channel, 16-bit, 2.5 MS/s per channel streaming DAC
|
||||
|
||||
The RTIO PHY supports staging DAC data before transmitting them by writing
|
||||
to the DAC RTIO addresses, if a channel is not "held" by setting its bit
|
||||
using :meth:`set_hold`, the next frame will contain the update. For the
|
||||
DACs held, the update is triggered explicitly by setting the corresponding
|
||||
bit using :meth:`set_update`. Update is self-clearing. This enables atomic
|
||||
DAC updates synchronized to a frame edge.
|
||||
|
||||
The `log2_width=0` RTIO layout uses one DAC channel per RTIO address and a
|
||||
dense RTIO address space. The RTIO words are narrow. (32 bit) and
|
||||
few-channel updates are efficient. There is the least amount of DAC state
|
||||
tracking in kernels, at the cost of more DMA and RTIO data.
|
||||
The setting here and in the RTIO PHY (gateware) must match.
|
||||
|
||||
Other `log2_width` (up to `log2_width=5`) settings pack multiple
|
||||
(in powers of two) DAC channels into one group and into one RTIO write.
|
||||
The RTIO data width increases accordingly. The `log2_width`
|
||||
LSBs of the RTIO address for a DAC channel write must be zero and the
|
||||
address space is sparse. For `log2_width=5` the RTIO data is 512 bit wide.
|
||||
|
||||
If `log2_width` is zero, the :meth:`set_dac`/:meth:`set_dac_mu` interface
|
||||
must be used. If non-zero, the :meth:`set_group`/:meth:`set_group_mu`
|
||||
interface must be used.
|
||||
|
||||
:param channel: RTIO channel number
|
||||
:param core_device: Core device name (default: "core")
|
||||
:param log2_width: Width of DAC channel group (logarithm base 2).
|
||||
Value must match the corresponding value in the RTIO PHY (gateware).
|
||||
"""
|
||||
kernel_invariants = {"core", "channel", "width"}
|
||||
|
||||
def __init__(self, dmgr, channel, core_device="core", log2_width=0):
|
||||
self.channel = channel << 8
|
||||
self.core = dmgr.get(core_device)
|
||||
self.width = 1 << log2_width
|
||||
|
||||
@kernel
|
||||
def init(self):
|
||||
"""Initialize the device.
|
||||
|
||||
This clears reset, unsets DAC_CLR, enables AFE_PWR,
|
||||
clears error counters, then enables error counting
|
||||
"""
|
||||
self.set_cfg(reset=0, afe_power_down=0, dac_clr=0, clr_err=1)
|
||||
delay(1*us)
|
||||
self.set_cfg(reset=0, afe_power_down=0, dac_clr=0, clr_err=0)
|
||||
delay(1*us)
|
||||
|
||||
@kernel
|
||||
def write(self, addr, data):
|
||||
"""Write data to a Fastino register.
|
||||
|
||||
:param addr: Address to write to.
|
||||
:param data: Data to write.
|
||||
"""
|
||||
rtio_output(self.channel | addr, data)
|
||||
|
||||
@kernel
|
||||
def read(self, addr):
|
||||
"""Read from Fastino register.
|
||||
|
||||
TODO: untested
|
||||
|
||||
:param addr: Address to read from.
|
||||
:return: The data read.
|
||||
"""
|
||||
rtio_output(self.channel | addr | 0x80)
|
||||
return rtio_input_data(self.channel >> 8)
|
||||
|
||||
@kernel
|
||||
def set_dac_mu(self, dac, data):
|
||||
"""Write DAC data in machine units.
|
||||
|
||||
:param dac: DAC channel to write to (0-31).
|
||||
:param data: DAC word to write, 16 bit unsigned integer, in machine
|
||||
units.
|
||||
"""
|
||||
self.write(dac, data)
|
||||
|
||||
@kernel
|
||||
def set_group_mu(self, dac: TInt32, data: TList(TInt32)):
|
||||
"""Write a group of DAC channels in machine units.
|
||||
|
||||
:param dac: First channel in DAC channel group (0-31). The `log2_width`
|
||||
LSBs must be zero.
|
||||
:param data: List of DAC data pairs (2x16 bit unsigned) to write,
|
||||
in machine units. Data exceeding group size is ignored.
|
||||
If the list length is less than group size, the remaining
|
||||
DAC channels within the group are cleared to 0 (machine units).
|
||||
"""
|
||||
if dac & (self.width - 1):
|
||||
raise ValueError("Group index LSBs must be zero")
|
||||
rtio_output_wide(self.channel | dac, data)
|
||||
|
||||
@portable
|
||||
def voltage_to_mu(self, voltage):
|
||||
"""Convert SI Volts to DAC machine units.
|
||||
|
||||
:param voltage: Voltage in SI Volts.
|
||||
:return: DAC data word in machine units, 16 bit integer.
|
||||
"""
|
||||
data = int(round((0x8000/10.)*voltage)) + 0x8000
|
||||
if data < 0 or data > 0xffff:
|
||||
raise ValueError("DAC voltage out of bounds")
|
||||
return data
|
||||
|
||||
@portable
|
||||
def voltage_group_to_mu(self, voltage, data):
|
||||
"""Convert SI Volts to packed DAC channel group machine units.
|
||||
|
||||
:param voltage: List of SI Volt voltages.
|
||||
:param data: List of DAC channel data pairs to write to.
|
||||
Half the length of `voltage`.
|
||||
"""
|
||||
for i in range(len(voltage)):
|
||||
v = self.voltage_to_mu(voltage[i])
|
||||
if i & 1:
|
||||
v = data[i // 2] | (v << 16)
|
||||
data[i // 2] = v
|
||||
|
||||
@kernel
|
||||
def set_dac(self, dac, voltage):
|
||||
"""Set DAC data to given voltage.
|
||||
|
||||
:param dac: DAC channel (0-31).
|
||||
:param voltage: Desired output voltage.
|
||||
"""
|
||||
self.write(dac, self.voltage_to_mu(voltage))
|
||||
|
||||
@kernel
|
||||
def set_group(self, dac, voltage):
|
||||
"""Set DAC group data to given voltage.
|
||||
|
||||
:param dac: DAC channel (0-31).
|
||||
:param voltage: Desired output voltage.
|
||||
"""
|
||||
data = [int32(0)] * (len(voltage) // 2)
|
||||
self.voltage_group_to_mu(voltage, data)
|
||||
self.set_group_mu(dac, data)
|
||||
|
||||
@kernel
|
||||
def update(self, update):
|
||||
"""Schedule channels for update.
|
||||
|
||||
:param update: Bit mask of channels to update (32 bit).
|
||||
"""
|
||||
self.write(0x20, update)
|
||||
|
||||
@kernel
|
||||
def set_hold(self, hold):
|
||||
"""Set channels to manual update.
|
||||
|
||||
:param hold: Bit mask of channels to hold (32 bit).
|
||||
"""
|
||||
self.write(0x21, hold)
|
||||
|
||||
@kernel
|
||||
def set_cfg(self, reset=0, afe_power_down=0, dac_clr=0, clr_err=0):
|
||||
"""Set configuration bits.
|
||||
|
||||
:param reset: Reset SPI PLL and SPI clock domain.
|
||||
:param afe_power_down: Disable AFE power.
|
||||
:param dac_clr: Assert all 32 DAC_CLR signals setting all DACs to
|
||||
mid-scale (0 V).
|
||||
:param clr_err: Clear error counters and PLL reset indicator.
|
||||
This clears the sticky red error LED. Must be cleared to enable
|
||||
error counting.
|
||||
"""
|
||||
self.write(0x22, (reset << 0) | (afe_power_down << 1) |
|
||||
(dac_clr << 2) | (clr_err << 3))
|
||||
|
||||
@kernel
|
||||
def set_leds(self, leds):
|
||||
"""Set the green user-defined LEDs
|
||||
|
||||
:param leds: LED status, 8 bit integer each bit corresponding to one
|
||||
green LED.
|
||||
"""
|
||||
self.write(0x23, leds)
|
|
@ -0,0 +1,51 @@
|
|||
# Definitions for using the "FMC DIO 32ch LVDS a" card with the VHDCI-EEM breakout v1.1
|
||||
|
||||
eem_fmc_connections = {
|
||||
0: [0, 8, 2, 3, 4, 5, 6, 7],
|
||||
1: [1, 9, 10, 11, 12, 13, 14, 15],
|
||||
2: [17, 16, 24, 19, 20, 21, 22, 23],
|
||||
3: [18, 25, 26, 27, 28, 29, 30, 31],
|
||||
}
|
||||
|
||||
|
||||
def shiftreg_bits(eem, out_pins):
|
||||
"""
|
||||
Returns the bits that have to be set in the FMC card direction
|
||||
shift register for the given EEM.
|
||||
|
||||
Takes a set of pin numbers (0-7) at the EEM. Return values
|
||||
of this function for different EEMs should be ORed together.
|
||||
"""
|
||||
r = 0
|
||||
for i in range(8):
|
||||
if i not in out_pins:
|
||||
lvds_line = eem_fmc_connections[eem][i]
|
||||
# lines are swapped in pairs to ease PCB routing
|
||||
# at the shift register
|
||||
shift = lvds_line ^ 1
|
||||
r |= 1 << shift
|
||||
return r
|
||||
|
||||
|
||||
dio_bank0_out_pins = set(range(4))
|
||||
dio_bank1_out_pins = set(range(4, 8))
|
||||
urukul_out_pins = {
|
||||
0, # clk
|
||||
1, # mosi
|
||||
3, 4, 5, # cs_n
|
||||
6, # io_update
|
||||
7, # dds_reset
|
||||
}
|
||||
urukul_aux_out_pins = {
|
||||
4, # sw0
|
||||
5, # sw1
|
||||
6, # sw2
|
||||
7, # sw3
|
||||
}
|
||||
zotino_out_pins = {
|
||||
0, # clk
|
||||
1, # mosi
|
||||
3, 4, # cs_n
|
||||
5, # ldac_n
|
||||
7, # clr_n
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
from numpy import int32, int64
|
||||
|
||||
from artiq.language.core import *
|
||||
from artiq.language.types import *
|
||||
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
||||
|
||||
|
||||
class OutOfSyncException(Exception):
|
||||
"""Raised when an incorrect number of ROI engine outputs has been
|
||||
retrieved from the RTIO input FIFO."""
|
||||
pass
|
||||
|
||||
|
||||
class Grabber:
|
||||
"""Driver for the Grabber camera interface."""
|
||||
kernel_invariants = {"core", "channel_base", "sentinel"}
|
||||
|
||||
def __init__(self, dmgr, channel_base, res_width=12, count_shift=0,
|
||||
core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.channel_base = channel_base
|
||||
|
||||
count_width = min(31, 2*res_width + 16 - count_shift)
|
||||
# This value is inserted by the gateware to mark the start of a series of
|
||||
# ROI engine outputs for one video frame.
|
||||
self.sentinel = int32(int64(2**count_width))
|
||||
|
||||
@kernel
|
||||
def setup_roi(self, n, x0, y0, x1, y1):
|
||||
"""
|
||||
Defines the coordinates of a ROI.
|
||||
|
||||
The coordinates are set around the current position of the RTIO time
|
||||
cursor.
|
||||
|
||||
The user must keep the ROI engine disabled for the duration of more
|
||||
than one video frame after calling this function, as the output
|
||||
generated for that video frame is undefined.
|
||||
|
||||
Advances the timeline by 4 coarse RTIO cycles.
|
||||
"""
|
||||
c = int64(self.core.ref_multiplier)
|
||||
rtio_output((self.channel_base << 8) | (4*n+0), x0)
|
||||
delay_mu(c)
|
||||
rtio_output((self.channel_base << 8) | (4*n+1), y0)
|
||||
delay_mu(c)
|
||||
rtio_output((self.channel_base << 8) | (4*n+2), x1)
|
||||
delay_mu(c)
|
||||
rtio_output((self.channel_base << 8) | (4*n+3), y1)
|
||||
delay_mu(c)
|
||||
|
||||
@kernel
|
||||
def gate_roi(self, mask):
|
||||
"""
|
||||
Defines which ROI engines produce input events.
|
||||
|
||||
At the end of each video frame, the output from each ROI engine that
|
||||
has been enabled by the mask is enqueued into the RTIO input FIFO.
|
||||
|
||||
This function sets the mask at the current position of the RTIO time
|
||||
cursor.
|
||||
|
||||
Setting the mask using this function is atomic; in other words,
|
||||
if the system is in the middle of processing a frame and the mask
|
||||
is changed, the processing will complete using the value of the mask
|
||||
that it started with.
|
||||
|
||||
:param mask: bitmask enabling or disabling each ROI engine.
|
||||
"""
|
||||
rtio_output((self.channel_base + 1) << 8, mask)
|
||||
|
||||
@kernel
|
||||
def gate_roi_pulse(self, mask, dt):
|
||||
"""Sets a temporary mask for the specified duration (in seconds), before
|
||||
disabling all ROI engines."""
|
||||
self.gate_roi(mask)
|
||||
delay(dt)
|
||||
self.gate_roi(0)
|
||||
|
||||
@kernel
|
||||
def input_mu(self, data):
|
||||
"""
|
||||
Retrieves the accumulated values for one frame from the ROI engines.
|
||||
Blocks until values are available.
|
||||
|
||||
The input list must be a list of integers of the same length as there
|
||||
are enabled ROI engines. This method replaces the elements of the
|
||||
input list with the outputs of the enabled ROI engines, sorted by
|
||||
number.
|
||||
|
||||
If the number of elements in the list does not match the number of
|
||||
ROI engines that produced output, an exception will be raised during
|
||||
this call or the next.
|
||||
"""
|
||||
channel = self.channel_base + 1
|
||||
|
||||
sentinel = rtio_input_data(channel)
|
||||
if sentinel != self.sentinel:
|
||||
raise OutOfSyncException
|
||||
|
||||
for i in range(len(data)):
|
||||
roi_output = rtio_input_data(channel)
|
||||
if roi_output == self.sentinel:
|
||||
raise OutOfSyncException
|
||||
data[i] = roi_output
|
|
@ -33,6 +33,110 @@ def i2c_read(busno: TInt32, ack: TBool) -> TInt32:
|
|||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@kernel
|
||||
def i2c_poll(busno, busaddr):
|
||||
"""Poll I2C device at address.
|
||||
|
||||
:param busno: I2C bus number
|
||||
:param busaddr: 8 bit I2C device address (LSB=0)
|
||||
:returns: True if the poll was ACKed
|
||||
"""
|
||||
i2c_start(busno)
|
||||
ack = i2c_write(busno, busaddr)
|
||||
i2c_stop(busno)
|
||||
return ack
|
||||
|
||||
|
||||
@kernel
|
||||
def i2c_write_byte(busno, busaddr, data, ack=True):
|
||||
"""Write one byte to a device.
|
||||
|
||||
:param busno: I2C bus number
|
||||
:param busaddr: 8 bit I2C device address (LSB=0)
|
||||
:param data: Data byte to be written
|
||||
:param nack: Allow NACK
|
||||
"""
|
||||
i2c_start(busno)
|
||||
try:
|
||||
if not i2c_write(busno, busaddr):
|
||||
raise I2CError("failed to ack bus address")
|
||||
if not i2c_write(busno, data) and ack:
|
||||
raise I2CError("failed to ack write data")
|
||||
finally:
|
||||
i2c_stop(busno)
|
||||
|
||||
|
||||
@kernel
|
||||
def i2c_read_byte(busno, busaddr):
|
||||
"""Read one byte from a device.
|
||||
|
||||
:param busno: I2C bus number
|
||||
:param busaddr: 8 bit I2C device address (LSB=0)
|
||||
:returns: Byte read
|
||||
"""
|
||||
i2c_start(busno)
|
||||
data = 0
|
||||
try:
|
||||
if not i2c_write(busno, busaddr | 1):
|
||||
raise I2CError("failed to ack bus read address")
|
||||
data = i2c_read(busno, ack=False)
|
||||
finally:
|
||||
i2c_stop(busno)
|
||||
return data
|
||||
|
||||
|
||||
@kernel
|
||||
def i2c_write_many(busno, busaddr, addr, data, ack_last=True):
|
||||
"""Transfer multiple bytes to a device.
|
||||
|
||||
:param busno: I2c bus number
|
||||
:param busaddr: 8 bit I2C device address (LSB=0)
|
||||
:param addr: 8 bit data address
|
||||
:param data: Data bytes to be written
|
||||
:param ack_last: Expect I2C ACK of the last byte written. If `False`,
|
||||
the last byte may be NACKed (e.g. EEPROM full page writes).
|
||||
"""
|
||||
n = len(data)
|
||||
i2c_start(busno)
|
||||
try:
|
||||
if not i2c_write(busno, busaddr):
|
||||
raise I2CError("failed to ack bus address")
|
||||
if not i2c_write(busno, addr):
|
||||
raise I2CError("failed to ack data address")
|
||||
for i in range(n):
|
||||
if not i2c_write(busno, data[i]) and (
|
||||
i < n - 1 or ack_last):
|
||||
raise I2CError("failed to ack write data")
|
||||
finally:
|
||||
i2c_stop(busno)
|
||||
|
||||
|
||||
@kernel
|
||||
def i2c_read_many(busno, busaddr, addr, data):
|
||||
"""Transfer multiple bytes from a device.
|
||||
|
||||
:param busno: I2c bus number
|
||||
:param busaddr: 8 bit I2C device address (LSB=0)
|
||||
:param addr: 8 bit data address
|
||||
:param data: List of integers to be filled with the data read.
|
||||
One entry ber byte.
|
||||
"""
|
||||
m = len(data)
|
||||
i2c_start(busno)
|
||||
try:
|
||||
if not i2c_write(busno, busaddr):
|
||||
raise I2CError("failed to ack bus address")
|
||||
if not i2c_write(busno, addr):
|
||||
raise I2CError("failed to ack data address")
|
||||
i2c_restart(busno)
|
||||
if not i2c_write(busno, busaddr | 1):
|
||||
raise I2CError("failed to ack bus read address")
|
||||
for i in range(m):
|
||||
data[i] = i2c_read(busno, ack=i < m - 1)
|
||||
finally:
|
||||
i2c_stop(busno)
|
||||
|
||||
|
||||
class PCA9548:
|
||||
"""Driver for the PCA9548 I2C bus switch.
|
||||
|
||||
|
@ -48,34 +152,24 @@ class PCA9548:
|
|||
self.address = address
|
||||
|
||||
@kernel
|
||||
def set(self, channel):
|
||||
"""Select one channel.
|
||||
def select(self, mask):
|
||||
"""Enable/disable channels.
|
||||
|
||||
Selecting multiple channels at the same time is not supported by this
|
||||
driver.
|
||||
:param mask: Bit mask of enabled channels
|
||||
"""
|
||||
i2c_write_byte(self.busno, self.address, mask)
|
||||
|
||||
@kernel
|
||||
def set(self, channel):
|
||||
"""Enable one channel.
|
||||
|
||||
:param channel: channel number (0-7)
|
||||
"""
|
||||
i2c_start(self.busno)
|
||||
try:
|
||||
if not i2c_write(self.busno, self.address):
|
||||
raise I2CError("PCA9548 failed to ack address")
|
||||
if not i2c_write(self.busno, 1 << channel):
|
||||
raise I2CError("PCA9548 failed to ack control word")
|
||||
finally:
|
||||
i2c_stop(self.busno)
|
||||
self.select(1 << channel)
|
||||
|
||||
@kernel
|
||||
def readback(self):
|
||||
i2c_start(self.busno)
|
||||
r = 0
|
||||
try:
|
||||
if not i2c_write(self.busno, self.address | 1):
|
||||
raise I2CError("PCA9548 failed to ack address")
|
||||
r = i2c_read(self.busno, False)
|
||||
finally:
|
||||
i2c_stop(self.busno)
|
||||
return r
|
||||
return i2c_read_byte(self.busno, self.address)
|
||||
|
||||
|
||||
class TCA6424A:
|
||||
|
@ -92,19 +186,9 @@ class TCA6424A:
|
|||
self.address = address
|
||||
|
||||
@kernel
|
||||
def _write24(self, command, value):
|
||||
i2c_start(self.busno)
|
||||
try:
|
||||
if not i2c_write(self.busno, self.address):
|
||||
raise I2CError("TCA6424A failed to ack address")
|
||||
if not i2c_write(self.busno, command):
|
||||
raise I2CError("TCA6424A failed to ack command")
|
||||
for i in range(3):
|
||||
if not i2c_write(self.busno, value >> 16):
|
||||
raise I2CError("TCA6424A failed to ack data")
|
||||
value <<= 8
|
||||
finally:
|
||||
i2c_stop(self.busno)
|
||||
def _write24(self, addr, value):
|
||||
i2c_write_many(self.busno, self.address, addr,
|
||||
[value >> 16, value >> 8, value])
|
||||
|
||||
@kernel
|
||||
def set(self, outputs):
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
from numpy import int32
|
||||
|
||||
from artiq.experiment import *
|
||||
from artiq.coredevice.i2c import i2c_write_many, i2c_read_many, i2c_poll
|
||||
|
||||
|
||||
port_mapping = {
|
||||
"EEM0": 7,
|
||||
"EEM1": 5,
|
||||
"EEM2": 4,
|
||||
"EEM3": 3,
|
||||
"EEM4": 2,
|
||||
"EEM5": 1,
|
||||
"EEM6": 0,
|
||||
"EEM7": 6,
|
||||
"EEM8": 12,
|
||||
"EEM9": 13,
|
||||
"EEM10": 15,
|
||||
"EEM11": 14,
|
||||
"SFP0": 8,
|
||||
"SFP1": 9,
|
||||
"SFP2": 10,
|
||||
"LOC0": 11,
|
||||
}
|
||||
|
||||
|
||||
class KasliEEPROM:
|
||||
def __init__(self, dmgr, port, busno=0,
|
||||
core_device="core", sw0_device="i2c_switch0", sw1_device="i2c_switch1"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.sw0 = dmgr.get(sw0_device)
|
||||
self.sw1 = dmgr.get(sw1_device)
|
||||
self.busno = busno
|
||||
self.port = port_mapping[port]
|
||||
self.address = 0xa0 # i2c 8 bit
|
||||
|
||||
@kernel
|
||||
def select(self):
|
||||
mask = 1 << self.port
|
||||
self.sw0.select(mask)
|
||||
self.sw1.select(mask >> 8)
|
||||
|
||||
@kernel
|
||||
def deselect(self):
|
||||
self.sw0.select(0)
|
||||
self.sw1.select(0)
|
||||
|
||||
@kernel
|
||||
def write_i32(self, addr, value):
|
||||
self.select()
|
||||
try:
|
||||
data = [0]*4
|
||||
for i in range(4):
|
||||
data[i] = (value >> 24) & 0xff
|
||||
value <<= 8
|
||||
i2c_write_many(self.busno, self.address, addr, data)
|
||||
i2c_poll(self.busno, self.address)
|
||||
finally:
|
||||
self.deselect()
|
||||
|
||||
@kernel
|
||||
def read_i32(self, addr):
|
||||
self.select()
|
||||
try:
|
||||
data = [0]*4
|
||||
i2c_read_many(self.busno, self.address, addr, data)
|
||||
value = int32(0)
|
||||
for i in range(4):
|
||||
value <<= 8
|
||||
value |= data[i]
|
||||
finally:
|
||||
self.deselect()
|
||||
return value
|
|
@ -0,0 +1,110 @@
|
|||
"""RTIO driver for Mirny (4 channel GHz PLLs)
|
||||
"""
|
||||
|
||||
from artiq.language.core import kernel, delay
|
||||
from artiq.language.units import us
|
||||
|
||||
from numpy import int32
|
||||
|
||||
from artiq.coredevice import spi2 as spi
|
||||
|
||||
|
||||
SPI_CONFIG = (
|
||||
0 * spi.SPI_OFFLINE
|
||||
| 0 * spi.SPI_END
|
||||
| 0 * spi.SPI_INPUT
|
||||
| 1 * spi.SPI_CS_POLARITY
|
||||
| 0 * spi.SPI_CLK_POLARITY
|
||||
| 0 * spi.SPI_CLK_PHASE
|
||||
| 0 * spi.SPI_LSB_FIRST
|
||||
| 0 * spi.SPI_HALF_DUPLEX
|
||||
)
|
||||
|
||||
# SPI clock write and read dividers
|
||||
SPIT_WR = 4
|
||||
SPIT_RD = 16
|
||||
|
||||
SPI_CS = 1
|
||||
|
||||
WE = 1 << 24
|
||||
|
||||
|
||||
class Mirny:
|
||||
"""
|
||||
Mirny PLL-based RF generator.
|
||||
|
||||
:param spi_device: SPI bus device
|
||||
:param refclk: Reference clock (SMA, MMCX or on-board 100 MHz oscillator)
|
||||
frequency in Hz
|
||||
:param clk_sel: Reference clock selection.
|
||||
valid options are: 0 - internal 100MHz XO; 1 - front-panel SMA; 2 -
|
||||
internal MMCX
|
||||
:param core_device: Core device name (default: "core")
|
||||
"""
|
||||
|
||||
kernel_invariants = {"bus", "core"}
|
||||
|
||||
def __init__(self, dmgr, spi_device, refclk=100e6, clk_sel=0, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.bus = dmgr.get(spi_device)
|
||||
|
||||
self.refclk = refclk
|
||||
assert 10 <= self.refclk / 1e6 <= 600, "Invalid refclk"
|
||||
|
||||
self.clk_sel = clk_sel & 0b11
|
||||
assert 0 <= self.clk_sel <= 3, "Invalid clk_sel"
|
||||
|
||||
# TODO: support clk_div on v1.0 boards
|
||||
|
||||
@kernel
|
||||
def read_reg(self, addr):
|
||||
"""Read a register"""
|
||||
self.bus.set_config_mu(
|
||||
SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END, 24, SPIT_RD, SPI_CS
|
||||
)
|
||||
self.bus.write((addr << 25))
|
||||
return self.bus.read() & int32(0xFFFF)
|
||||
|
||||
@kernel
|
||||
def write_reg(self, addr, data):
|
||||
"""Write a register"""
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 24, SPIT_WR, SPI_CS)
|
||||
self.bus.write((addr << 25) | WE | ((data & 0xFFFF) << 8))
|
||||
|
||||
@kernel
|
||||
def init(self, blind=False):
|
||||
"""
|
||||
Initialize and detect Mirny.
|
||||
|
||||
:param blind: Do not attempt to verify presence and compatibility.
|
||||
"""
|
||||
if not blind:
|
||||
reg0 = self.read_reg(0)
|
||||
if reg0 & 0b11 != 0b11:
|
||||
raise ValueError("Mirny HW_REV mismatch")
|
||||
if (reg0 >> 2) & 0b11 != 0b00:
|
||||
raise ValueError("Mirny PROTO_REV mismatch")
|
||||
delay(100 * us) # slack
|
||||
|
||||
# select clock source
|
||||
self.write_reg(1, (self.clk_sel << 4))
|
||||
delay(1000 * us)
|
||||
|
||||
@kernel
|
||||
def set_att_mu(self, channel, att):
|
||||
"""Set digital step attenuator in machine units.
|
||||
|
||||
:param att: Attenuation setting, 8 bit digital.
|
||||
"""
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 16, SPIT_WR, SPI_CS)
|
||||
self.bus.write(((channel | 8) << 25) | (att << 16))
|
||||
|
||||
@kernel
|
||||
def write_ext(self, addr, length, data):
|
||||
"""Perform SPI write to a prefixed address"""
|
||||
self.bus.set_config_mu(SPI_CONFIG, 8, SPIT_WR, SPI_CS)
|
||||
self.bus.write(addr << 25)
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, length, SPIT_WR, SPI_CS)
|
||||
if length < 32:
|
||||
data <<= 32 - length
|
||||
self.bus.write(data)
|
|
@ -0,0 +1,174 @@
|
|||
from artiq.language.core import kernel, delay, portable
|
||||
from artiq.language.units import ns
|
||||
|
||||
from artiq.coredevice import spi2 as spi
|
||||
|
||||
|
||||
SPI_CONFIG = (0*spi.SPI_OFFLINE | 0*spi.SPI_END |
|
||||
0*spi.SPI_INPUT | 0*spi.SPI_CS_POLARITY |
|
||||
0*spi.SPI_CLK_POLARITY | 0*spi.SPI_CLK_PHASE |
|
||||
0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX)
|
||||
|
||||
|
||||
SPI_CS_ADC = 1
|
||||
SPI_CS_SR = 2
|
||||
|
||||
|
||||
@portable
|
||||
def adc_ctrl(channel=1, softspan=0b111, valid=1):
|
||||
"""Build a LTC2335-16 control word"""
|
||||
return (valid << 7) | (channel << 3) | softspan
|
||||
|
||||
|
||||
@portable
|
||||
def adc_softspan(data):
|
||||
"""Return the softspan configuration index from a result packet"""
|
||||
return data & 0x7
|
||||
|
||||
|
||||
@portable
|
||||
def adc_channel(data):
|
||||
"""Return the channel index from a result packet"""
|
||||
return (data >> 3) & 0x7
|
||||
|
||||
|
||||
@portable
|
||||
def adc_data(data):
|
||||
"""Return the ADC value from a result packet"""
|
||||
return (data >> 8) & 0xffff
|
||||
|
||||
|
||||
@portable
|
||||
def adc_value(data, v_ref=5.):
|
||||
"""Convert a ADC result packet to SI units (Volt)"""
|
||||
softspan = adc_softspan(data)
|
||||
data = adc_data(data)
|
||||
g = 625
|
||||
if softspan & 4:
|
||||
g *= 2
|
||||
if softspan & 2:
|
||||
h = 1 << 15
|
||||
else:
|
||||
h = 1 << 16
|
||||
data = -(data & h) + (data & ~h)
|
||||
if softspan & 1:
|
||||
h *= 500
|
||||
else:
|
||||
h *= 512
|
||||
v_per_lsb = v_ref*g/h
|
||||
return data*v_per_lsb
|
||||
|
||||
|
||||
class Novogorny:
|
||||
"""Novogorny ADC.
|
||||
|
||||
Controls the LTC2335-16 8 channel ADC with SPI interface and
|
||||
the switchable gain instrumentation amplifiers using a shift
|
||||
register.
|
||||
|
||||
:param spi_device: SPI bus device name
|
||||
:param cnv_device: CNV RTIO TTLOut channel name
|
||||
:param div: SPI clock divider (default: 8)
|
||||
:param gains: Initial value for PGIA gains shift register
|
||||
(default: 0x0000). Knowledge of this state is not transferred
|
||||
between experiments.
|
||||
:param core_device: Core device name
|
||||
"""
|
||||
kernel_invariants = {"bus", "core", "cnv", "div", "v_ref"}
|
||||
|
||||
def __init__(self, dmgr, spi_device, cnv_device, div=8, gains=0x0000,
|
||||
core_device="core"):
|
||||
self.bus = dmgr.get(spi_device)
|
||||
self.core = dmgr.get(core_device)
|
||||
self.cnv = dmgr.get(cnv_device)
|
||||
self.div = div
|
||||
self.gains = gains
|
||||
self.v_ref = 5. # 5 Volt reference
|
||||
|
||||
@kernel
|
||||
def set_gain_mu(self, channel, gain):
|
||||
"""Set instrumentation amplifier gain of a channel.
|
||||
|
||||
The four gain settings (0, 1, 2, 3) corresponds to gains of
|
||||
(1, 10, 100, 1000) respectively.
|
||||
|
||||
:param channel: Channel index
|
||||
:param gain: Gain setting
|
||||
"""
|
||||
gains = self.gains
|
||||
gains &= ~(0b11 << (channel*2))
|
||||
gains |= gain << (channel*2)
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END,
|
||||
16, self.div, SPI_CS_SR)
|
||||
self.bus.write(gains << 16)
|
||||
self.gains = gains
|
||||
|
||||
@kernel
|
||||
def configure(self, data):
|
||||
"""Set up the ADC sequencer.
|
||||
|
||||
:param data: List of 8 bit control words to write into the sequencer
|
||||
table.
|
||||
"""
|
||||
if len(data) > 1:
|
||||
self.bus.set_config_mu(SPI_CONFIG,
|
||||
8, self.div, SPI_CS_ADC)
|
||||
for i in range(len(data) - 1):
|
||||
self.bus.write(data[i] << 24)
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END,
|
||||
8, self.div, SPI_CS_ADC)
|
||||
self.bus.write(data[len(data) - 1] << 24)
|
||||
|
||||
@kernel
|
||||
def sample_mu(self, next_ctrl=0):
|
||||
"""Acquire a sample:
|
||||
|
||||
Perform a conversion and transfer the sample.
|
||||
|
||||
:param next_ctrl: ADC control word for the next sample
|
||||
:return: The ADC result packet (machine units)
|
||||
"""
|
||||
self.cnv.pulse(40*ns) # t_CNVH
|
||||
delay(560*ns) # t_CONV max
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END,
|
||||
24, self.div, SPI_CS_ADC)
|
||||
self.bus.write(next_ctrl << 24)
|
||||
return self.bus.read()
|
||||
|
||||
@kernel
|
||||
def sample(self, next_ctrl=0):
|
||||
"""Acquire a sample
|
||||
|
||||
.. seealso:: :meth:`sample_mu`
|
||||
|
||||
:param next_ctrl: ADC control word for the next sample
|
||||
:return: The ADC result packet (Volt)
|
||||
"""
|
||||
return adc_value(self.sample_mu(), self.v_ref)
|
||||
|
||||
@kernel
|
||||
def burst_mu(self, data, dt_mu, ctrl=0):
|
||||
"""Acquire a burst of samples.
|
||||
|
||||
If the burst is too long and the sample rate too high, there will be
|
||||
RTIO input overflows.
|
||||
|
||||
High sample rates lead to gain errors since the impedance between the
|
||||
instrumentation amplifier and the ADC is high.
|
||||
|
||||
:param data: List of data values to write result packets into.
|
||||
In machine units.
|
||||
:param dt: Sample interval in machine units.
|
||||
:param ctrl: ADC control word to write during each result packet
|
||||
transfer.
|
||||
"""
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END,
|
||||
24, self.div, SPI_CS_ADC)
|
||||
for i in range(len(data)):
|
||||
t0 = now_mu()
|
||||
self.cnv.pulse(40*ns) # t_CNVH
|
||||
delay(560*ns) # t_CONV max
|
||||
self.bus.write(ctrl << 24)
|
||||
at_mu(t0 + dt_mu)
|
||||
for i in range(len(data)):
|
||||
data[i] = self.bus.read()
|
|
@ -0,0 +1,47 @@
|
|||
from artiq.experiment import kernel
|
||||
from artiq.coredevice.i2c import (
|
||||
i2c_start, i2c_write, i2c_read, i2c_stop, I2CError)
|
||||
|
||||
|
||||
class PCF8574A:
|
||||
"""Driver for the PCF8574 I2C remote 8-bit I/O expander.
|
||||
|
||||
I2C transactions not real-time, and are performed by the CPU without
|
||||
involving RTIO.
|
||||
"""
|
||||
def __init__(self, dmgr, busno=0, address=0x7c, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.busno = busno
|
||||
self.address = address
|
||||
|
||||
@kernel
|
||||
def set(self, data):
|
||||
"""Drive data on the quasi-bidirectional pins.
|
||||
|
||||
:param data: Pin data. High bits are weakly driven high
|
||||
(and thus inputs), low bits are strongly driven low.
|
||||
"""
|
||||
i2c_start(self.busno)
|
||||
try:
|
||||
if not i2c_write(self.busno, self.address):
|
||||
raise I2CError("PCF8574A failed to ack address")
|
||||
if not i2c_write(self.busno, data):
|
||||
raise I2CError("PCF8574A failed to ack data")
|
||||
finally:
|
||||
i2c_stop(self.busno)
|
||||
|
||||
@kernel
|
||||
def get(self):
|
||||
"""Retrieve quasi-bidirectional pin input data.
|
||||
|
||||
:return: Pin data
|
||||
"""
|
||||
i2c_start(self.busno)
|
||||
ret = 0
|
||||
try:
|
||||
if not i2c_write(self.busno, self.address | 1):
|
||||
raise I2CError("PCF8574A failed to ack address")
|
||||
ret = i2c_read(self.busno, False)
|
||||
finally:
|
||||
i2c_stop(self.busno)
|
||||
return ret
|
|
@ -41,7 +41,7 @@ class CorePCU:
|
|||
"""
|
||||
Configure and clear the kernel CPU performance counters.
|
||||
|
||||
The eight counters are configures to count the folloging events:
|
||||
The eight counters are configured to count the following events:
|
||||
* Load or store
|
||||
* Instruction fetch
|
||||
* Data cache miss
|
||||
|
|
|
@ -0,0 +1,945 @@
|
|||
from artiq.language.core import kernel, delay_mu, delay
|
||||
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
||||
from artiq.language.units import us, ns, ms, MHz, dB
|
||||
from artiq.language.types import TInt32
|
||||
from artiq.coredevice.dac34h84 import DAC34H84
|
||||
from artiq.coredevice.trf372017 import TRF372017
|
||||
|
||||
|
||||
PHASER_BOARD_ID = 19
|
||||
PHASER_ADDR_BOARD_ID = 0x00
|
||||
PHASER_ADDR_HW_REV = 0x01
|
||||
PHASER_ADDR_GW_REV = 0x02
|
||||
PHASER_ADDR_CFG = 0x03
|
||||
PHASER_ADDR_STA = 0x04
|
||||
PHASER_ADDR_CRC_ERR = 0x05
|
||||
PHASER_ADDR_LED = 0x06
|
||||
PHASER_ADDR_FAN = 0x07
|
||||
PHASER_ADDR_DUC_STB = 0x08
|
||||
PHASER_ADDR_ADC_CFG = 0x09
|
||||
PHASER_ADDR_SPI_CFG = 0x0a
|
||||
PHASER_ADDR_SPI_DIVLEN = 0x0b
|
||||
PHASER_ADDR_SPI_SEL = 0x0c
|
||||
PHASER_ADDR_SPI_DATW = 0x0d
|
||||
PHASER_ADDR_SPI_DATR = 0x0e
|
||||
PHASER_ADDR_SYNC_DLY = 0x0f
|
||||
|
||||
PHASER_ADDR_DUC0_CFG = 0x10
|
||||
# PHASER_ADDR_DUC0_RESERVED0 = 0x11
|
||||
PHASER_ADDR_DUC0_F = 0x12
|
||||
PHASER_ADDR_DUC0_P = 0x16
|
||||
PHASER_ADDR_DAC0_DATA = 0x18
|
||||
PHASER_ADDR_DAC0_TEST = 0x1c
|
||||
|
||||
PHASER_ADDR_DUC1_CFG = 0x20
|
||||
# PHASER_ADDR_DUC1_RESERVED0 = 0x21
|
||||
PHASER_ADDR_DUC1_F = 0x22
|
||||
PHASER_ADDR_DUC1_P = 0x26
|
||||
PHASER_ADDR_DAC1_DATA = 0x28
|
||||
PHASER_ADDR_DAC1_TEST = 0x2c
|
||||
|
||||
PHASER_SEL_DAC = 1 << 0
|
||||
PHASER_SEL_TRF0 = 1 << 1
|
||||
PHASER_SEL_TRF1 = 1 << 2
|
||||
PHASER_SEL_ATT0 = 1 << 3
|
||||
PHASER_SEL_ATT1 = 1 << 4
|
||||
|
||||
PHASER_STA_DAC_ALARM = 1 << 0
|
||||
PHASER_STA_TRF0_LD = 1 << 1
|
||||
PHASER_STA_TRF1_LD = 1 << 2
|
||||
PHASER_STA_TERM0 = 1 << 3
|
||||
PHASER_STA_TERM1 = 1 << 4
|
||||
PHASER_STA_SPI_IDLE = 1 << 5
|
||||
|
||||
PHASER_DAC_SEL_DUC = 0
|
||||
PHASER_DAC_SEL_TEST = 1
|
||||
|
||||
PHASER_HW_REV_VARIANT = 1 << 4
|
||||
|
||||
|
||||
class Phaser:
|
||||
"""Phaser 4-channel, 16-bit, 1 GS/s DAC coredevice driver.
|
||||
|
||||
Phaser contains a 4 channel, 1 GS/s DAC chip with integrated upconversion,
|
||||
quadrature modulation compensation and interpolation features.
|
||||
|
||||
The coredevice produces 2 IQ (in-phase and quadrature) data streams with 25
|
||||
MS/s and 14 bit per quadrature. Each data stream supports 5 independent
|
||||
numerically controlled IQ oscillators (NCOs, DDSs with 32 bit frequency, 16
|
||||
bit phase, 15 bit amplitude, and phase accumulator clear functionality)
|
||||
added together. See :class:`PhaserChannel` and :class:`PhaserOscillator`.
|
||||
|
||||
Together with a data clock, framing marker, a checksum and metadata for
|
||||
register access the streams are sent in groups of 8 samples over 1.5 Gb/s
|
||||
FastLink via a single EEM connector from coredevice to Phaser.
|
||||
|
||||
On Phaser in the FPGA the data streams are buffered and interpolated
|
||||
from 25 MS/s to 500 MS/s 16 bit followed by a 500 MS/s digital upconverter
|
||||
with adjustable frequency and phase. The interpolation passband is 20 MHz
|
||||
wide, passband ripple is less than 1e-3 amplitude, stopband attenuation
|
||||
is better than 75 dB at offsets > 15 MHz and better than 90 dB at offsets
|
||||
> 30 MHz.
|
||||
|
||||
The four 16 bit 500 MS/s DAC data streams are sent via a 32 bit parallel
|
||||
LVDS bus operating at 1 Gb/s per pin pair and processed in the DAC (Texas
|
||||
Instruments DAC34H84). On the DAC 2x interpolation, sinx/x compensation,
|
||||
quadrature modulator compensation, fine and coarse mixing as well as group
|
||||
delay capabilities are available.
|
||||
|
||||
The latency/group delay from the RTIO events setting
|
||||
:class:`PhaserOscillator` or :class:`PhaserChannel` DUC parameters all the
|
||||
way to the DAC outputs is deterministic. This enables deterministic
|
||||
absolute phase with respect to other RTIO input and output events.
|
||||
|
||||
The four analog DAC outputs are passed through anti-aliasing filters.
|
||||
|
||||
In the baseband variant, the even/in-phase DAC channels feed 31.5 dB range
|
||||
attenuators and are available on the front panel. The odd outputs are
|
||||
available at MMCX connectors on board.
|
||||
|
||||
In the upconverter variant, each IQ output pair feeds one quadrature
|
||||
upconverter (Texas Instruments TRF372017) with integrated PLL/VCO. This
|
||||
digitally configured analog quadrature upconverter supports offset tuning
|
||||
for carrier and sideband suppression. The output from the upconverter
|
||||
passes through the 31.5 dB range step attenuator and is available at the
|
||||
front panel.
|
||||
|
||||
The DAC, the analog quadrature upconverters and the attenuators are
|
||||
configured through a shared SPI bus that is accessed and controlled via
|
||||
FPGA registers.
|
||||
|
||||
.. note:: Various register settings of the DAC and the quadrature
|
||||
upconverters are available to be modified through the `dac`, `trf0`,
|
||||
`trf1` dictionaries. These can be set through the device database
|
||||
(`device_db.py`). The settings are frozen during instantiation of the
|
||||
class and applied during `init()`. See the :class:`DAC34H84` and
|
||||
:class:`TRF372017` source for details.
|
||||
|
||||
.. note:: To establish deterministic latency between RTIO time base and DAC
|
||||
output, the DAC FIFO read pointer value (`fifo_offset`) must be
|
||||
fixed. If `tune_fifo_offset=True` (the default) a value with maximum
|
||||
margin is determined automatically by `dac_tune_fifo_offset` each time
|
||||
`init()` is called. This value should be used for the `fifo_offset` key
|
||||
of the `dac` settings of Phaser in `device_db.py` and automatic
|
||||
tuning should be disabled by `tune_fifo_offset=False`.
|
||||
|
||||
:param channel: Base RTIO channel number
|
||||
:param core_device: Core device name (default: "core")
|
||||
:param miso_delay: Fastlink MISO signal delay to account for cable
|
||||
and buffer round trip. Tuning this might be automated later.
|
||||
:param tune_fifo_offset: Tune the DAC FIFO read pointer offset
|
||||
(default=True)
|
||||
:param clk_sel: Select the external SMA clock input (1 or 0)
|
||||
:param sync_dly: SYNC delay with respect to ISTR.
|
||||
:param dac: DAC34H84 DAC settings as a dictionary.
|
||||
:param trf0: Channel 0 TRF372017 quadrature upconverter settings as a
|
||||
dictionary.
|
||||
:param trf1: Channel 1 TRF372017 quadrature upconverter settings as a
|
||||
dictionary.
|
||||
|
||||
Attributes:
|
||||
|
||||
* :attr:`channel`: List of two :class:`PhaserChannel`
|
||||
To access oscillators, digital upconverters, PLL/VCO analog
|
||||
quadrature upconverters and attenuators.
|
||||
"""
|
||||
kernel_invariants = {"core", "channel_base", "t_frame", "miso_delay",
|
||||
"dac_mmap"}
|
||||
|
||||
def __init__(self, dmgr, channel_base, miso_delay=1, tune_fifo_offset=True,
|
||||
clk_sel=0, sync_dly=0, dac=None, trf0=None, trf1=None,
|
||||
core_device="core"):
|
||||
self.channel_base = channel_base
|
||||
self.core = dmgr.get(core_device)
|
||||
# TODO: auto-align miso-delay in phy
|
||||
self.miso_delay = miso_delay
|
||||
# frame duration in mu (10 words, 8 clock cycles each 4 ns)
|
||||
# self.core.seconds_to_mu(10*8*4*ns) # unfortunately this returns 319
|
||||
assert self.core.ref_period == 1*ns
|
||||
self.t_frame = 10*8*4
|
||||
self.clk_sel = clk_sel
|
||||
self.tune_fifo_offset = tune_fifo_offset
|
||||
self.sync_dly = sync_dly
|
||||
|
||||
self.dac_mmap = DAC34H84(dac).get_mmap()
|
||||
|
||||
self.channel = [PhaserChannel(self, ch, trf)
|
||||
for ch, trf in enumerate([trf0, trf1])]
|
||||
|
||||
@kernel
|
||||
def init(self, debug=False):
|
||||
"""Initialize the board.
|
||||
|
||||
Verifies board and chip presence, resets components, performs
|
||||
communication and configuration tests and establishes initial
|
||||
conditions.
|
||||
"""
|
||||
board_id = self.read8(PHASER_ADDR_BOARD_ID)
|
||||
if board_id != PHASER_BOARD_ID:
|
||||
raise ValueError("invalid board id")
|
||||
delay(.1*ms) # slack
|
||||
|
||||
hw_rev = self.read8(PHASER_ADDR_HW_REV)
|
||||
delay(.1*ms) # slack
|
||||
is_baseband = hw_rev & PHASER_HW_REV_VARIANT
|
||||
|
||||
gw_rev = self.read8(PHASER_ADDR_GW_REV)
|
||||
delay(.1*ms) # slack
|
||||
|
||||
# allow a few errors during startup and alignment since boot
|
||||
if self.get_crc_err() > 20:
|
||||
raise ValueError("large number of frame CRC errors")
|
||||
delay(.1*ms) # slack
|
||||
|
||||
# reset
|
||||
self.set_cfg(dac_resetb=0, dac_sleep=1, dac_txena=0,
|
||||
trf0_ps=1, trf1_ps=1,
|
||||
att0_rstn=0, att1_rstn=0)
|
||||
self.set_leds(0x00)
|
||||
self.set_fan_mu(0)
|
||||
# bring dac out of reset, keep tx off
|
||||
self.set_cfg(clk_sel=self.clk_sel, dac_txena=0,
|
||||
trf0_ps=1, trf1_ps=1,
|
||||
att0_rstn=0, att1_rstn=0)
|
||||
delay(.1*ms) # slack
|
||||
|
||||
# crossing dac_clk (reference) edges with sync_dly
|
||||
# changes the optimal fifo_offset by 4
|
||||
self.set_sync_dly(self.sync_dly)
|
||||
|
||||
# 4 wire SPI, sif4_enable
|
||||
self.dac_write(0x02, 0x0080)
|
||||
if self.dac_read(0x7f) != 0x5409:
|
||||
raise ValueError("DAC version readback invalid")
|
||||
delay(.1*ms)
|
||||
if self.dac_read(0x00) != 0x049c:
|
||||
raise ValueError("DAC config0 reset readback invalid")
|
||||
delay(.1*ms)
|
||||
|
||||
t = self.get_dac_temperature()
|
||||
delay(.1*ms)
|
||||
if t < 10 or t > 90:
|
||||
raise ValueError("DAC temperature out of bounds")
|
||||
|
||||
for data in self.dac_mmap:
|
||||
self.dac_write(data >> 16, data)
|
||||
delay(40*us)
|
||||
|
||||
# pll_ndivsync_ena disable
|
||||
config18 = self.dac_read(0x18)
|
||||
delay(.1*ms)
|
||||
self.dac_write(0x18, config18 & ~0x0800)
|
||||
|
||||
patterns = [
|
||||
[0xf05a, 0x05af, 0x5af0, 0xaf05], # test channel/iq/byte/nibble
|
||||
[0x7a7a, 0xb6b6, 0xeaea, 0x4545], # datasheet pattern a
|
||||
[0x1a1a, 0x1616, 0xaaaa, 0xc6c6], # datasheet pattern b
|
||||
]
|
||||
# A data delay of 2*50 ps heuristically and reproducibly matches
|
||||
# FPGA+board+DAC skews. There is plenty of margin (>= 250 ps
|
||||
# either side) and no need to tune at runtime.
|
||||
# Parity provides another level of safety.
|
||||
for i in range(len(patterns)):
|
||||
delay(.5*ms)
|
||||
errors = self.dac_iotest(patterns[i])
|
||||
if errors:
|
||||
raise ValueError("DAC iotest failure")
|
||||
|
||||
delay(2*ms) # let it settle
|
||||
lvolt = self.dac_read(0x18) & 7
|
||||
delay(.1*ms)
|
||||
if lvolt < 2 or lvolt > 5:
|
||||
raise ValueError("DAC PLL lock failed, check clocking")
|
||||
|
||||
if self.tune_fifo_offset:
|
||||
fifo_offset = self.dac_tune_fifo_offset()
|
||||
if debug:
|
||||
print(fifo_offset)
|
||||
self.core.break_realtime()
|
||||
|
||||
# self.dac_write(0x20, 0x0000) # stop fifo sync
|
||||
# alarm = self.get_sta() & 1
|
||||
# delay(.1*ms)
|
||||
self.clear_dac_alarms()
|
||||
delay(2*ms) # let it run a bit
|
||||
alarms = self.get_dac_alarms()
|
||||
delay(.1*ms) # slack
|
||||
if alarms & ~0x0040: # ignore PLL alarms (see DS)
|
||||
if debug:
|
||||
print(alarms)
|
||||
self.core.break_realtime()
|
||||
# ignore alarms
|
||||
else:
|
||||
raise ValueError("DAC alarm")
|
||||
|
||||
# power up trfs, release att reset
|
||||
self.set_cfg(clk_sel=self.clk_sel, dac_txena=0)
|
||||
|
||||
for ch in range(2):
|
||||
channel = self.channel[ch]
|
||||
# test attenuator write and readback
|
||||
channel.set_att_mu(0x5a)
|
||||
if channel.get_att_mu() != 0x5a:
|
||||
raise ValueError("attenuator test failed")
|
||||
delay(.1*ms)
|
||||
channel.set_att_mu(0x00) # minimum attenuation
|
||||
|
||||
# test oscillators and DUC
|
||||
for i in range(len(channel.oscillator)):
|
||||
oscillator = channel.oscillator[i]
|
||||
asf = 0
|
||||
if i == 0:
|
||||
asf = 0x7fff
|
||||
# 6pi/4 phase
|
||||
oscillator.set_amplitude_phase_mu(asf=asf, pow=0xc000, clr=1)
|
||||
delay(1*us)
|
||||
# 3pi/4
|
||||
channel.set_duc_phase_mu(0x6000)
|
||||
channel.set_duc_cfg(select=0, clr=1)
|
||||
self.duc_stb()
|
||||
delay(.1*ms) # settle link, pipeline and impulse response
|
||||
data = channel.get_dac_data()
|
||||
delay(.1*ms)
|
||||
sqrt2 = 0x5a81 # 0x7fff/sqrt(2)
|
||||
data_i = data & 0xffff
|
||||
data_q = (data >> 16) & 0xffff
|
||||
# allow ripple
|
||||
if (data_i < sqrt2 - 30 or data_i > sqrt2 or
|
||||
abs(data_i - data_q) > 2):
|
||||
raise ValueError("DUC+oscillator phase/amplitude test failed")
|
||||
|
||||
if is_baseband:
|
||||
continue
|
||||
|
||||
if channel.trf_read(0) & 0x7f != 0x68:
|
||||
raise ValueError("TRF identification failed")
|
||||
delay(.1*ms)
|
||||
|
||||
delay(.2*ms)
|
||||
for data in channel.trf_mmap:
|
||||
channel.trf_write(data)
|
||||
|
||||
delay(2*ms) # lock
|
||||
if not (self.get_sta() & (PHASER_STA_TRF0_LD << ch)):
|
||||
raise ValueError("TRF lock failure")
|
||||
delay(.1*ms)
|
||||
if channel.trf_read(0) & 0x1000:
|
||||
raise ValueError("TRF R_SAT_ERR")
|
||||
delay(.1*ms)
|
||||
|
||||
# enable dac tx
|
||||
self.set_cfg(clk_sel=self.clk_sel)
|
||||
|
||||
@kernel
|
||||
def write8(self, addr, data):
|
||||
"""Write data to FPGA register.
|
||||
|
||||
:param addr: Address to write to (7 bit)
|
||||
:param data: Data to write (8 bit)
|
||||
"""
|
||||
rtio_output((self.channel_base << 8) | (addr & 0x7f) | 0x80, data)
|
||||
delay_mu(int64(self.t_frame))
|
||||
|
||||
@kernel
|
||||
def read8(self, addr) -> TInt32:
|
||||
"""Read from FPGA register.
|
||||
|
||||
:param addr: Address to read from (7 bit)
|
||||
:return: Data read (8 bit)
|
||||
"""
|
||||
rtio_output((self.channel_base << 8) | (addr & 0x7f), 0)
|
||||
response = rtio_input_data(self.channel_base)
|
||||
return response >> self.miso_delay
|
||||
|
||||
@kernel
|
||||
def write32(self, addr, data: TInt32):
|
||||
"""Write 32 bit to a sequence of FPGA registers."""
|
||||
for offset in range(4):
|
||||
byte = data >> 24
|
||||
self.write8(addr + offset, byte)
|
||||
data <<= 8
|
||||
|
||||
@kernel
|
||||
def read32(self, addr) -> TInt32:
|
||||
"""Read 32 bit from a sequence of FPGA registers."""
|
||||
data = 0
|
||||
for offset in range(4):
|
||||
data <<= 8
|
||||
data |= self.read8(addr + offset)
|
||||
delay(20*us) # slack
|
||||
return data
|
||||
|
||||
@kernel
|
||||
def set_leds(self, leds):
|
||||
"""Set the front panel LEDs.
|
||||
|
||||
:param leds: LED settings (6 bit)
|
||||
"""
|
||||
self.write8(PHASER_ADDR_LED, leds)
|
||||
|
||||
@kernel
|
||||
def set_fan_mu(self, pwm):
|
||||
"""Set the fan duty cycle.
|
||||
|
||||
:param pwm: Duty cycle in machine units (8 bit)
|
||||
"""
|
||||
self.write8(PHASER_ADDR_FAN, pwm)
|
||||
|
||||
@kernel
|
||||
def set_fan(self, duty):
|
||||
"""Set the fan duty cycle.
|
||||
|
||||
:param duty: Duty cycle (0. to 1.)
|
||||
"""
|
||||
pwm = int32(round(duty*255.))
|
||||
if pwm < 0 or pwm > 255:
|
||||
raise ValueError("duty cycle out of bounds")
|
||||
self.set_fan_mu(pwm)
|
||||
|
||||
@kernel
|
||||
def set_cfg(self, clk_sel=0, dac_resetb=1, dac_sleep=0, dac_txena=1,
|
||||
trf0_ps=0, trf1_ps=0, att0_rstn=1, att1_rstn=1):
|
||||
"""Set the configuration register.
|
||||
|
||||
Each flag is a single bit (0 or 1).
|
||||
|
||||
:param clk_sel: Select the external SMA clock input
|
||||
:param dac_resetb: Active low DAC reset pin
|
||||
:param dac_sleep: DAC sleep pin
|
||||
:param dac_txena: Enable DAC transmission pin
|
||||
:param trf0_ps: Quadrature upconverter 0 power save
|
||||
:param trf1_ps: Quadrature upconverter 1 power save
|
||||
:param att0_rstn: Active low attenuator 0 reset
|
||||
:param att1_rstn: Active low attenuator 1 reset
|
||||
"""
|
||||
self.write8(PHASER_ADDR_CFG,
|
||||
((clk_sel & 1) << 0) | ((dac_resetb & 1) << 1) |
|
||||
((dac_sleep & 1) << 2) | ((dac_txena & 1) << 3) |
|
||||
((trf0_ps & 1) << 4) | ((trf1_ps & 1) << 5) |
|
||||
((att0_rstn & 1) << 6) | ((att1_rstn & 1) << 7))
|
||||
|
||||
@kernel
|
||||
def get_sta(self):
|
||||
"""Get the status register value.
|
||||
|
||||
Bit flags are:
|
||||
|
||||
* :const:`PHASER_STA_DAC_ALARM`: DAC alarm pin
|
||||
* :const:`PHASER_STA_TRF0_LD`: Quadrature upconverter 0 lock detect
|
||||
* :const:`PHASER_STA_TRF1_LD`: Quadrature upconverter 1 lock detect
|
||||
* :const:`PHASER_STA_TERM0`: ADC channel 0 termination indicator
|
||||
* :const:`PHASER_STA_TERM1`: ADC channel 1 termination indicator
|
||||
* :const:`PHASER_STA_SPI_IDLE`: SPI machine is idle and data registers
|
||||
can be read/written
|
||||
|
||||
:return: Status register
|
||||
"""
|
||||
return self.read8(PHASER_ADDR_STA)
|
||||
|
||||
@kernel
|
||||
def get_crc_err(self):
|
||||
"""Get the frame CRC error counter.
|
||||
|
||||
:return: The number of frames with CRC mismatches sind the reset of the
|
||||
device. Overflows at 256.
|
||||
"""
|
||||
return self.read8(PHASER_ADDR_CRC_ERR)
|
||||
|
||||
@kernel
|
||||
def set_sync_dly(self, dly):
|
||||
"""Set SYNC delay.
|
||||
|
||||
:param dly: DAC SYNC delay setting (0 to 7)
|
||||
"""
|
||||
if dly < 0 or dly > 7:
|
||||
raise ValueError("SYNC delay out of bounds")
|
||||
self.write8(PHASER_ADDR_SYNC_DLY, dly)
|
||||
|
||||
@kernel
|
||||
def duc_stb(self):
|
||||
"""Strobe the DUC configuration register update.
|
||||
|
||||
Transfer staging to active registers.
|
||||
This affects both DUC channels.
|
||||
"""
|
||||
self.write8(PHASER_ADDR_DUC_STB, 0)
|
||||
|
||||
@kernel
|
||||
def spi_cfg(self, select, div, end, clk_phase=0, clk_polarity=0,
|
||||
half_duplex=0, lsb_first=0, offline=0, length=8):
|
||||
"""Set the SPI machine configuration
|
||||
|
||||
:param select: Chip selects to assert (DAC, TRF0, TRF1, ATT0, ATT1)
|
||||
:param div: SPI clock divider relative to 250 MHz fabric clock
|
||||
:param end: Whether to end the SPI transaction and deassert chip select
|
||||
:param clk_phase: SPI clock phase (sample on first or second edge)
|
||||
:param clk_polarity: SPI clock polarity (idle low or high)
|
||||
:param half_duplex: Read MISO data from MOSI wire
|
||||
:param lsb_first: Transfer the least significant bit first
|
||||
:param offline: Put the SPI interfaces offline and don't drive voltages
|
||||
:param length: SPI transfer length (1 to 8 bits)
|
||||
"""
|
||||
if div < 2 or div > 257:
|
||||
raise ValueError("divider out of bounds")
|
||||
if length < 1 or length > 8:
|
||||
raise ValueError("length out of bounds")
|
||||
self.write8(PHASER_ADDR_SPI_SEL, select)
|
||||
self.write8(PHASER_ADDR_SPI_DIVLEN, (div - 2 >> 3) | (length - 1 << 5))
|
||||
self.write8(PHASER_ADDR_SPI_CFG,
|
||||
((offline & 1) << 0) | ((end & 1) << 1) |
|
||||
((clk_phase & 1) << 2) | ((clk_polarity & 1) << 3) |
|
||||
((half_duplex & 1) << 4) | ((lsb_first & 1) << 5))
|
||||
|
||||
@kernel
|
||||
def spi_write(self, data):
|
||||
"""Write 8 bits into the SPI data register and start/continue the
|
||||
transaction."""
|
||||
self.write8(PHASER_ADDR_SPI_DATW, data)
|
||||
|
||||
@kernel
|
||||
def spi_read(self):
|
||||
"""Read from the SPI input data register."""
|
||||
return self.read8(PHASER_ADDR_SPI_DATR)
|
||||
|
||||
@kernel
|
||||
def dac_write(self, addr, data):
|
||||
"""Write 16 bit to a DAC register.
|
||||
|
||||
:param addr: Register address
|
||||
:param data: Register data to write
|
||||
"""
|
||||
div = 34 # 100 ns min period
|
||||
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0)
|
||||
self.spi_write(addr)
|
||||
delay_mu(t_xfer)
|
||||
self.spi_write(data >> 8)
|
||||
delay_mu(t_xfer)
|
||||
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=1)
|
||||
self.spi_write(data)
|
||||
delay_mu(t_xfer)
|
||||
|
||||
@kernel
|
||||
def dac_read(self, addr, div=34) -> TInt32:
|
||||
"""Read from a DAC register.
|
||||
|
||||
:param addr: Register address to read from
|
||||
:param div: SPI clock divider. Needs to be at least 250 (1 µs SPI
|
||||
clock) to read the temperature register.
|
||||
"""
|
||||
t_xfer = self.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=0)
|
||||
self.spi_write(addr | 0x80)
|
||||
delay_mu(t_xfer)
|
||||
self.spi_write(0)
|
||||
delay_mu(t_xfer)
|
||||
data = self.spi_read() << 8
|
||||
delay(20*us) # slack
|
||||
self.spi_cfg(select=PHASER_SEL_DAC, div=div, end=1)
|
||||
self.spi_write(0)
|
||||
delay_mu(t_xfer)
|
||||
data |= self.spi_read()
|
||||
return data
|
||||
|
||||
@kernel
|
||||
def get_dac_temperature(self) -> TInt32:
|
||||
"""Read the DAC die temperature.
|
||||
|
||||
:return: DAC temperature in degree Celsius
|
||||
"""
|
||||
return self.dac_read(0x06, div=257) >> 8
|
||||
|
||||
@kernel
|
||||
def get_dac_alarms(self):
|
||||
"""Read the DAC alarm flags.
|
||||
|
||||
:return: DAC alarm flags (see datasheet for bit meaning)
|
||||
"""
|
||||
return self.dac_read(0x05)
|
||||
|
||||
@kernel
|
||||
def clear_dac_alarms(self):
|
||||
"""Clear DAC alarm flags."""
|
||||
self.dac_write(0x05, 0x0000)
|
||||
|
||||
@kernel
|
||||
def dac_iotest(self, pattern) -> TInt32:
|
||||
"""Performs a DAC IO test according to the datasheet.
|
||||
|
||||
:param patterm: List of four int32 containing the pattern
|
||||
:return: Bit error mask (16 bits)
|
||||
"""
|
||||
if len(pattern) != 4:
|
||||
raise ValueError("pattern length out of bounds")
|
||||
for addr in range(len(pattern)):
|
||||
self.dac_write(0x25 + addr, pattern[addr])
|
||||
# repeat the pattern twice
|
||||
self.dac_write(0x29 + addr, pattern[addr])
|
||||
delay(.1*ms)
|
||||
for ch in range(2):
|
||||
channel = self.channel[ch]
|
||||
channel.set_duc_cfg(select=1) # test
|
||||
# dac test data is i msb, q lsb
|
||||
data = pattern[2*ch] | (pattern[2*ch + 1] << 16)
|
||||
channel.set_dac_test(data)
|
||||
if channel.get_dac_data() != data:
|
||||
raise ValueError("DAC test data readback failed")
|
||||
delay(.1*ms)
|
||||
cfg = self.dac_read(0x01)
|
||||
delay(.1*ms)
|
||||
self.dac_write(0x01, cfg | 0x8000) # iotest_ena
|
||||
self.dac_write(0x04, 0x0000) # clear iotest_result
|
||||
delay(.2*ms) # let it rip
|
||||
# no need to go through the alarm register,
|
||||
# just read the error mask
|
||||
# self.clear_dac_alarms()
|
||||
alarms = self.get_dac_alarms()
|
||||
delay(.1*ms) # slack
|
||||
if alarms & 0x0080: # alarm_from_iotest
|
||||
errors = self.dac_read(0x04)
|
||||
delay(.1*ms) # slack
|
||||
else:
|
||||
errors = 0
|
||||
self.dac_write(0x01, cfg) # clear config
|
||||
self.dac_write(0x04, 0x0000) # clear iotest_result
|
||||
return errors
|
||||
|
||||
@kernel
|
||||
def dac_tune_fifo_offset(self):
|
||||
"""Scan through `fifo_offset` and configure midpoint setting.
|
||||
|
||||
:return: Optimal `fifo_offset` setting with maximum margin to write
|
||||
pointer.
|
||||
"""
|
||||
# expect two or three error free offsets:
|
||||
#
|
||||
# read offset 01234567
|
||||
# write pointer w
|
||||
# distance 32101234
|
||||
# error free x xx
|
||||
config9 = self.dac_read(0x09)
|
||||
delay(.1*ms)
|
||||
good = 0
|
||||
for o in range(8):
|
||||
# set new fifo_offset
|
||||
self.dac_write(0x09, (config9 & 0x1fff) | (o << 13))
|
||||
self.clear_dac_alarms()
|
||||
delay(.1*ms) # run
|
||||
alarms = self.get_dac_alarms()
|
||||
delay(.1*ms) # slack
|
||||
if (alarms >> 11) & 0x7 == 0: # any fifo alarm
|
||||
good |= 1 << o
|
||||
# if there are good offsets accross the wrap around
|
||||
# offset for computations
|
||||
if good & 0x81 == 0x81:
|
||||
good = ((good << 4) & 0xf0) | (good >> 4)
|
||||
offset = 4
|
||||
else:
|
||||
offset = 0
|
||||
# calculate mean
|
||||
sum = 0
|
||||
count = 0
|
||||
for o in range(8):
|
||||
if good & (1 << o):
|
||||
sum += o
|
||||
count += 1
|
||||
best = ((sum // count) + offset) % 8
|
||||
self.dac_write(0x09, (config9 & 0x1fff) | (best << 13))
|
||||
return best
|
||||
|
||||
|
||||
class PhaserChannel:
|
||||
"""Phaser channel IQ pair.
|
||||
|
||||
A Phaser channel contains:
|
||||
|
||||
* multiple oscillators (in the coredevice phy),
|
||||
* an interpolation chain and digital upconverter (DUC) on Phaser,
|
||||
* several channel-specific settings in the DAC:
|
||||
* quadrature modulation compensation QMC
|
||||
* numerically controlled oscillator NCO or coarse mixer CMIX,
|
||||
* the analog quadrature upconverter (in the Phaser-Upconverter hardware
|
||||
variant), and
|
||||
* a digitally controlled step attenuator.
|
||||
|
||||
Attributes:
|
||||
|
||||
* :attr:`oscillator`: List of five :class:`PhaserOscillator`.
|
||||
|
||||
.. note:: The amplitude sum of the oscillators must be less than one to
|
||||
avoid clipping or overflow. If any of the DDS or DUC frequencies are
|
||||
non-zero, it is not sufficient to ensure that the sum in each
|
||||
quadrature is within range.
|
||||
|
||||
.. note:: The interpolation filter on Phaser has an intrinsic sinc-like
|
||||
overshoot in its step response. That overshoot is a direct consequence
|
||||
of its near-brick-wall frequency response. For large and wide-band
|
||||
changes in oscillator parameters, the overshoot can lead to clipping
|
||||
or overflow after the interpolation. Either band-limit any changes
|
||||
in the oscillator parameters or back off the amplitude sufficiently.
|
||||
"""
|
||||
kernel_invariants = {"index", "phaser", "trf_mmap"}
|
||||
|
||||
def __init__(self, phaser, index, trf):
|
||||
self.phaser = phaser
|
||||
self.index = index
|
||||
self.trf_mmap = TRF372017(trf).get_mmap()
|
||||
self.oscillator = [PhaserOscillator(self, osc) for osc in range(5)]
|
||||
|
||||
@kernel
|
||||
def get_dac_data(self) -> TInt32:
|
||||
"""Get a sample of the current DAC data.
|
||||
|
||||
The data is split accross multiple registers and thus the data
|
||||
is only valid if constant.
|
||||
|
||||
:return: DAC data as 32 bit IQ. I/DACA/DACC in the 16 LSB,
|
||||
Q/DACB/DACD in the 16 MSB
|
||||
"""
|
||||
return self.phaser.read32(PHASER_ADDR_DAC0_DATA + (self.index << 4))
|
||||
|
||||
@kernel
|
||||
def set_dac_test(self, data: TInt32):
|
||||
"""Set the DAC test data.
|
||||
|
||||
:param data: 32 bit IQ test data, I/DACA/DACC in the 16 LSB,
|
||||
Q/DACB/DACD in the 16 MSB
|
||||
"""
|
||||
self.phaser.write32(PHASER_ADDR_DAC0_TEST + (self.index << 4), data)
|
||||
|
||||
@kernel
|
||||
def set_duc_cfg(self, clr=0, clr_once=0, select=0):
|
||||
"""Set the digital upconverter (DUC) and interpolator configuration.
|
||||
|
||||
:param clr: Keep the phase accumulator cleared (persistent)
|
||||
:param clr_once: Clear the phase accumulator for one cycle
|
||||
:param select: Select the data to send to the DAC (0: DUC data, 1: test
|
||||
data, other values: reserved)
|
||||
"""
|
||||
self.phaser.write8(PHASER_ADDR_DUC0_CFG + (self.index << 4),
|
||||
((clr & 1) << 0) | ((clr_once & 1) << 1) |
|
||||
((select & 3) << 2))
|
||||
|
||||
@kernel
|
||||
def set_duc_frequency_mu(self, ftw):
|
||||
"""Set the DUC frequency.
|
||||
|
||||
:param ftw: DUC frequency tuning word (32 bit)
|
||||
"""
|
||||
self.phaser.write32(PHASER_ADDR_DUC0_F + (self.index << 4), ftw)
|
||||
|
||||
@kernel
|
||||
def set_duc_frequency(self, frequency):
|
||||
"""Set the DUC frequency in SI units.
|
||||
|
||||
:param frequency: DUC frequency in Hz (passband from -200 MHz to
|
||||
200 MHz, wrapping around at +- 250 MHz)
|
||||
"""
|
||||
ftw = int32(round(frequency*((1 << 30)/(125*MHz))))
|
||||
self.set_duc_frequency_mu(ftw)
|
||||
|
||||
@kernel
|
||||
def set_duc_phase_mu(self, pow):
|
||||
"""Set the DUC phase offset.
|
||||
|
||||
:param pow: DUC phase offset word (16 bit)
|
||||
"""
|
||||
addr = PHASER_ADDR_DUC0_P + (self.index << 4)
|
||||
self.phaser.write8(addr, pow >> 8)
|
||||
self.phaser.write8(addr + 1, pow)
|
||||
|
||||
@kernel
|
||||
def set_duc_phase(self, phase):
|
||||
"""Set the DUC phase in SI units.
|
||||
|
||||
:param phase: DUC phase in turns
|
||||
"""
|
||||
pow = int32(round(phase*(1 << 16)))
|
||||
self.set_duc_phase_mu(pow)
|
||||
|
||||
@kernel
|
||||
def set_nco_frequency_mu(self, ftw):
|
||||
"""Set the NCO frequency.
|
||||
|
||||
:param ftw: NCO frequency tuning word (32 bit)
|
||||
"""
|
||||
self.phaser.dac_write(0x15 + (self.index << 1), ftw >> 16)
|
||||
self.phaser.dac_write(0x14 + (self.index << 1), ftw)
|
||||
|
||||
@kernel
|
||||
def set_nco_frequency(self, frequency):
|
||||
"""Set the NCO frequency in SI units.
|
||||
|
||||
:param frequency: NCO frequency in Hz (passband from -400 MHz
|
||||
to 400 MHz, wrapping around at +- 500 MHz)
|
||||
"""
|
||||
ftw = int32(round(frequency*((1 << 30)/(250*MHz))))
|
||||
self.set_nco_frequency_mu(ftw)
|
||||
|
||||
@kernel
|
||||
def set_nco_phase_mu(self, pow):
|
||||
"""Set the NCO phase offset.
|
||||
|
||||
:param pow: NCO phase offset word (16 bit)
|
||||
"""
|
||||
self.phaser.dac_write(0x12 + self.index, pow)
|
||||
|
||||
@kernel
|
||||
def set_nco_phase(self, phase):
|
||||
"""Set the NCO phase in SI units.
|
||||
|
||||
:param phase: NCO phase in turns
|
||||
"""
|
||||
pow = int32(round(phase*(1 << 16)))
|
||||
self.set_duc_phase_mu(pow)
|
||||
|
||||
@kernel
|
||||
def set_att_mu(self, data):
|
||||
"""Set channel attenuation.
|
||||
|
||||
:param data: Attenuator data in machine units (8 bit)
|
||||
"""
|
||||
div = 34 # 30 ns min period
|
||||
t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div,
|
||||
end=1)
|
||||
self.phaser.spi_write(data)
|
||||
delay_mu(t_xfer)
|
||||
|
||||
@kernel
|
||||
def set_att(self, att):
|
||||
"""Set channel attenuation in SI units.
|
||||
|
||||
:param att: Attenuation in dB
|
||||
"""
|
||||
# 2 lsb are inactive, resulting in 8 LSB per dB
|
||||
data = 0xff - int32(round(att*8))
|
||||
if data < 0 or data > 0xff:
|
||||
raise ValueError("attenuation out of bounds")
|
||||
self.set_att_mu(data)
|
||||
|
||||
@kernel
|
||||
def get_att_mu(self) -> TInt32:
|
||||
"""Read current attenuation.
|
||||
|
||||
The current attenuation value is read without side effects.
|
||||
|
||||
:return: Current attenuation in machine units
|
||||
"""
|
||||
div = 34
|
||||
t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div,
|
||||
end=0)
|
||||
self.phaser.spi_write(0)
|
||||
delay_mu(t_xfer)
|
||||
data = self.phaser.spi_read()
|
||||
delay(20*us) # slack
|
||||
self.phaser.spi_cfg(select=PHASER_SEL_ATT0 << self.index, div=div,
|
||||
end=1)
|
||||
self.phaser.spi_write(data)
|
||||
delay_mu(t_xfer)
|
||||
return data
|
||||
|
||||
@kernel
|
||||
def trf_write(self, data, readback=False):
|
||||
"""Write 32 bits to quadrature upconverter register.
|
||||
|
||||
:param data: Register data (32 bit) containing encoded address
|
||||
:param readback: Whether to return the read back MISO data
|
||||
"""
|
||||
div = 34 # 50 ns min period
|
||||
t_xfer = self.phaser.core.seconds_to_mu((8 + 1)*div*4*ns)
|
||||
read = 0
|
||||
end = 0
|
||||
clk_phase = 0
|
||||
if readback:
|
||||
clk_phase = 1
|
||||
for i in range(4):
|
||||
if i == 0 or i == 3:
|
||||
if i == 3:
|
||||
end = 1
|
||||
self.phaser.spi_cfg(select=PHASER_SEL_TRF0 << self.index,
|
||||
div=div, lsb_first=1, clk_phase=clk_phase,
|
||||
end=end)
|
||||
self.phaser.spi_write(data)
|
||||
data >>= 8
|
||||
delay_mu(t_xfer)
|
||||
if readback:
|
||||
read >>= 8
|
||||
read |= self.phaser.spi_read() << 24
|
||||
delay(20*us) # slack
|
||||
return read
|
||||
|
||||
@kernel
|
||||
def trf_read(self, addr, cnt_mux_sel=0) -> TInt32:
|
||||
"""Quadrature upconverter register read.
|
||||
|
||||
:param addr: Register address to read (0 to 7)
|
||||
:param cnt_mux_sel: Report VCO counter min or max frequency
|
||||
:return: Register data (32 bit)
|
||||
"""
|
||||
self.trf_write(0x80000008 | (addr << 28) | (cnt_mux_sel << 27))
|
||||
# single clk pulse with ~LE to start readback
|
||||
self.phaser.spi_cfg(select=0, div=34, end=1, length=1)
|
||||
self.phaser.spi_write(0)
|
||||
delay((1 + 1)*34*4*ns)
|
||||
return self.trf_write(0x00000008 | (cnt_mux_sel << 27),
|
||||
readback=True)
|
||||
|
||||
|
||||
class PhaserOscillator:
|
||||
"""Phaser IQ channel oscillator (NCO/DDS).
|
||||
|
||||
.. note:: Latencies between oscillators within a channel and between
|
||||
oscillator paramters (amplitude and phase/frequency) are deterministic
|
||||
(with respect to the 25 MS/s sample clock) but not matched.
|
||||
"""
|
||||
kernel_invariants = {"channel", "base_addr"}
|
||||
|
||||
def __init__(self, channel, index):
|
||||
self.channel = channel
|
||||
self.base_addr = ((self.channel.phaser.channel_base + 1 +
|
||||
2*self.channel.index) << 8) | index
|
||||
|
||||
@kernel
|
||||
def set_frequency_mu(self, ftw):
|
||||
"""Set Phaser MultiDDS frequency tuning word.
|
||||
|
||||
:param ftw: Frequency tuning word (32 bit)
|
||||
"""
|
||||
rtio_output(self.base_addr, ftw)
|
||||
|
||||
@kernel
|
||||
def set_frequency(self, frequency):
|
||||
"""Set Phaser MultiDDS frequency.
|
||||
|
||||
:param frequency: Frequency in Hz (passband from -10 MHz to 10 MHz,
|
||||
wrapping around at +- 12.5 MHz)
|
||||
"""
|
||||
ftw = int32(round(frequency*((1 << 30)/(6.25*MHz))))
|
||||
self.set_frequency_mu(ftw)
|
||||
|
||||
@kernel
|
||||
def set_amplitude_phase_mu(self, asf=0x7fff, pow=0, clr=0):
|
||||
"""Set Phaser MultiDDS amplitude, phase offset and accumulator clear.
|
||||
|
||||
:param asf: Amplitude (15 bit)
|
||||
:param pow: Phase offset word (16 bit)
|
||||
:param clr: Clear the phase accumulator (persistent)
|
||||
"""
|
||||
data = (asf & 0x7fff) | ((clr & 1) << 15) | (pow << 16)
|
||||
rtio_output(self.base_addr | (1 << 8), data)
|
||||
|
||||
@kernel
|
||||
def set_amplitude_phase(self, amplitude, phase=0., clr=0):
|
||||
"""Set Phaser MultiDDS amplitude and phase.
|
||||
|
||||
:param amplitude: Amplitude in units of full scale
|
||||
:param phase: Phase in turns
|
||||
:param clr: Clear the phase accumulator (persistent)
|
||||
"""
|
||||
asf = int32(round(amplitude*0x7fff))
|
||||
if asf < 0 or asf > 0x7fff:
|
||||
raise ValueError("amplitude out of bounds")
|
||||
pow = int32(round(phase*(1 << 16)))
|
||||
self.set_amplitude_phase_mu(asf, pow, clr)
|
|
@ -0,0 +1,92 @@
|
|||
from collections import defaultdict
|
||||
import subprocess
|
||||
|
||||
|
||||
class Symbolizer:
|
||||
def __init__(self, binary, triple, demangle=True):
|
||||
cmdline = [
|
||||
triple + "-addr2line", "--exe=" + binary,
|
||||
"--addresses", "--functions", "--inlines"
|
||||
]
|
||||
if demangle:
|
||||
cmdline.append("--demangle=rust")
|
||||
self._addr2line = subprocess.Popen(cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
|
||||
def symbolize(self, addr):
|
||||
self._addr2line.stdin.write("0x{:08x}\n0\n".format(addr))
|
||||
self._addr2line.stdin.flush()
|
||||
self._addr2line.stdout.readline() # 0x[addr]
|
||||
|
||||
result = []
|
||||
while True:
|
||||
function = self._addr2line.stdout.readline().rstrip()
|
||||
|
||||
# check for end marker
|
||||
if function == "0x00000000": # 0x00000000
|
||||
self._addr2line.stdout.readline() # ??
|
||||
self._addr2line.stdout.readline() # ??:0
|
||||
return result
|
||||
|
||||
file, line = self._addr2line.stdout.readline().rstrip().split(":")
|
||||
|
||||
result.append((function, file, line, addr))
|
||||
|
||||
|
||||
class CallgrindWriter:
|
||||
def __init__(self, output, binary, triple, compression=True, demangle=True):
|
||||
self._output = output
|
||||
self._binary = binary
|
||||
self._current = defaultdict(lambda: None)
|
||||
self._ids = defaultdict(lambda: {})
|
||||
self._compression = compression
|
||||
self._symbolizer = Symbolizer(binary, triple, demangle=demangle)
|
||||
|
||||
def _write(self, fmt, *args, **kwargs):
|
||||
self._output.write(fmt.format(*args, **kwargs))
|
||||
self._output.write("\n")
|
||||
|
||||
def _spec(self, spec, value):
|
||||
if self._current[spec] == value:
|
||||
return
|
||||
self._current[spec] = value
|
||||
|
||||
if not self._compression or value == "??":
|
||||
self._write("{}={}", spec, value)
|
||||
return
|
||||
|
||||
spec_ids = self._ids[spec]
|
||||
if value in spec_ids:
|
||||
self._write("{}=({})", spec, spec_ids[value])
|
||||
else:
|
||||
spec_ids[value] = len(spec_ids) + 1
|
||||
self._write("{}=({}) {}", spec, spec_ids[value], value)
|
||||
|
||||
def header(self):
|
||||
self._write("# callgrind format")
|
||||
self._write("version: 1")
|
||||
self._write("creator: ARTIQ")
|
||||
self._write("positions: instr line")
|
||||
self._write("events: Hits")
|
||||
self._write("")
|
||||
self._spec("ob", self._binary)
|
||||
self._spec("cob", self._binary)
|
||||
|
||||
def hit(self, addr, count):
|
||||
for function, file, line, addr in self._symbolizer.symbolize(addr):
|
||||
self._spec("fl", file)
|
||||
self._spec("fn", function)
|
||||
self._write("0x{:08x} {} {}", addr, line, count)
|
||||
|
||||
def edge(self, caller, callee, count):
|
||||
edges = self._symbolizer.symbolize(callee) + self._symbolizer.symbolize(caller)
|
||||
for (callee, caller) in zip(edges, edges[1:]):
|
||||
function, file, line, addr = callee
|
||||
self._spec("cfl", file)
|
||||
self._spec("cfn", function)
|
||||
self._write("calls={} 0x{:08x} {}", count, addr, line)
|
||||
|
||||
function, file, line, addr = caller
|
||||
self._spec("fl", file)
|
||||
self._spec("fn", function)
|
||||
self._write("0x{:08x} {} {}", addr, line, count)
|
|
@ -1,16 +1,14 @@
|
|||
from artiq.language.core import syscall
|
||||
from artiq.language.types import TInt64, TInt32, TNone, TList
|
||||
from artiq.language.types import TInt32, TInt64, TList, TNone, TTuple
|
||||
|
||||
|
||||
@syscall(flags={"nowrite"})
|
||||
def rtio_output(time_mu: TInt64, channel: TInt32, addr: TInt32, data: TInt32
|
||||
) -> TNone:
|
||||
def rtio_output(target: TInt32, data: TInt32) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nowrite"})
|
||||
def rtio_output_wide(time_mu: TInt64, channel: TInt32, addr: TInt32,
|
||||
data: TList(TInt32)) -> TNone:
|
||||
def rtio_output_wide(target: TInt32, data: TList(TInt32)) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
|
@ -22,3 +20,12 @@ def rtio_input_timestamp(timeout_mu: TInt64, channel: TInt32) -> TInt64:
|
|||
@syscall(flags={"nowrite"})
|
||||
def rtio_input_data(channel: TInt32) -> TInt32:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nowrite"})
|
||||
def rtio_input_timestamped_data(timeout_mu: TInt64,
|
||||
channel: TInt32) -> TTuple([TInt64, TInt32]):
|
||||
"""Wait for an input event up to timeout_mu on the given channel, and
|
||||
return a tuple of timestamp and attached data, or (-1, 0) if the timeout is
|
||||
reached."""
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
from artiq.language.core import kernel, delay, portable
|
||||
from artiq.language.units import ns
|
||||
|
||||
from artiq.coredevice import spi2 as spi
|
||||
|
||||
|
||||
SPI_CONFIG = (0*spi.SPI_OFFLINE | 0*spi.SPI_END |
|
||||
0*spi.SPI_INPUT | 0*spi.SPI_CS_POLARITY |
|
||||
0*spi.SPI_CLK_POLARITY | 0*spi.SPI_CLK_PHASE |
|
||||
0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX)
|
||||
|
||||
|
||||
SPI_CS_ADC = 0 # no CS, SPI_END does not matter, framing is done with CNV
|
||||
SPI_CS_PGIA = 1 # separate SPI bus, CS used as RCLK
|
||||
|
||||
|
||||
@portable
|
||||
def adc_mu_to_volt(data, gain=0):
|
||||
"""Convert ADC data in machine units to Volts.
|
||||
|
||||
:param data: 16 bit signed ADC word
|
||||
:param gain: PGIA gain setting (0: 1, ..., 3: 1000)
|
||||
:return: Voltage in Volts
|
||||
"""
|
||||
if gain == 0:
|
||||
volt_per_lsb = 20./(1 << 16)
|
||||
elif gain == 1:
|
||||
volt_per_lsb = 2./(1 << 16)
|
||||
elif gain == 2:
|
||||
volt_per_lsb = .2/(1 << 16)
|
||||
elif gain == 3:
|
||||
volt_per_lsb = .02/(1 << 16)
|
||||
else:
|
||||
raise ValueError("invalid gain")
|
||||
return data*volt_per_lsb
|
||||
|
||||
|
||||
class Sampler:
|
||||
"""Sampler ADC.
|
||||
|
||||
Controls the LTC2320-16 8 channel 16 bit ADC with SPI interface and
|
||||
the switchable gain instrumentation amplifiers.
|
||||
|
||||
:param spi_adc_device: ADC SPI bus device name
|
||||
:param spi_pgia_device: PGIA SPI bus device name
|
||||
:param cnv_device: CNV RTIO TTLOut channel name
|
||||
:param div: SPI clock divider (default: 8)
|
||||
:param gains: Initial value for PGIA gains shift register
|
||||
(default: 0x0000). Knowledge of this state is not transferred
|
||||
between experiments.
|
||||
:param core_device: Core device name
|
||||
"""
|
||||
kernel_invariants = {"bus_adc", "bus_pgia", "core", "cnv", "div"}
|
||||
|
||||
def __init__(self, dmgr, spi_adc_device, spi_pgia_device, cnv_device,
|
||||
div=8, gains=0x0000, core_device="core"):
|
||||
self.bus_adc = dmgr.get(spi_adc_device)
|
||||
self.bus_adc.update_xfer_duration_mu(div, 32)
|
||||
self.bus_pgia = dmgr.get(spi_pgia_device)
|
||||
self.bus_pgia.update_xfer_duration_mu(div, 16)
|
||||
self.core = dmgr.get(core_device)
|
||||
self.cnv = dmgr.get(cnv_device)
|
||||
self.div = div
|
||||
self.gains = gains
|
||||
|
||||
@kernel
|
||||
def init(self):
|
||||
"""Initialize the device.
|
||||
|
||||
Sets up SPI channels.
|
||||
"""
|
||||
self.bus_adc.set_config_mu(SPI_CONFIG | spi.SPI_INPUT | spi.SPI_END,
|
||||
32, self.div, SPI_CS_ADC)
|
||||
self.bus_pgia.set_config_mu(SPI_CONFIG | spi.SPI_END,
|
||||
16, self.div, SPI_CS_PGIA)
|
||||
|
||||
@kernel
|
||||
def set_gain_mu(self, channel, gain):
|
||||
"""Set instrumentation amplifier gain of a channel.
|
||||
|
||||
The four gain settings (0, 1, 2, 3) corresponds to gains of
|
||||
(1, 10, 100, 1000) respectively.
|
||||
|
||||
:param channel: Channel index
|
||||
:param gain: Gain setting
|
||||
"""
|
||||
gains = self.gains
|
||||
gains &= ~(0b11 << (channel*2))
|
||||
gains |= gain << (channel*2)
|
||||
self.bus_pgia.write(gains << 16)
|
||||
self.gains = gains
|
||||
|
||||
@kernel
|
||||
def get_gains_mu(self):
|
||||
"""Read the PGIA gain settings of all channels.
|
||||
|
||||
:return: The PGIA gain settings in machine units.
|
||||
"""
|
||||
self.bus_pgia.set_config_mu(SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT,
|
||||
16, self.div, SPI_CS_PGIA)
|
||||
self.bus_pgia.write(self.gains << 16)
|
||||
self.bus_pgia.set_config_mu(SPI_CONFIG | spi.SPI_END,
|
||||
16, self.div, SPI_CS_PGIA)
|
||||
self.gains = self.bus_pgia.read() & 0xffff
|
||||
return self.gains
|
||||
|
||||
@kernel
|
||||
def sample_mu(self, data):
|
||||
"""Acquire a set of samples.
|
||||
|
||||
Perform a conversion and transfer the samples.
|
||||
|
||||
This assumes that the input FIFO of the ADC SPI RTIO channel is deep
|
||||
enough to buffer the samples (half the length of `data` deep).
|
||||
If it is not, there will be RTIO input overflows.
|
||||
|
||||
:param data: List of data samples to fill. Must have even length.
|
||||
Samples are always read from the last channel (channel 7) down.
|
||||
The `data` list will always be filled with the last item
|
||||
holding to the sample from channel 7.
|
||||
"""
|
||||
self.cnv.pulse(30*ns) # t_CNVH
|
||||
delay(450*ns) # t_CONV
|
||||
mask = 1 << 15
|
||||
for i in range(len(data)//2):
|
||||
self.bus_adc.write(0)
|
||||
for i in range(len(data) - 1, -1, -2):
|
||||
val = self.bus_adc.read()
|
||||
data[i] = val >> 16
|
||||
val &= 0xffff
|
||||
data[i - 1] = -(val & mask) + (val & ~mask)
|
||||
|
||||
@kernel
|
||||
def sample(self, data):
|
||||
"""Acquire a set of samples.
|
||||
|
||||
.. seealso:: :meth:`sample_mu`
|
||||
|
||||
:param data: List of floating point data samples to fill.
|
||||
"""
|
||||
n = len(data)
|
||||
adc_data = [0]*n
|
||||
self.sample_mu(adc_data)
|
||||
for i in range(n):
|
||||
channel = i + 8 - len(data)
|
||||
gain = (self.gains >> (channel*2)) & 0b11
|
||||
data[i] = adc_mu_to_volt(adc_data[i], gain)
|
|
@ -10,7 +10,7 @@ Output event replacement is supported except on the configuration channel.
|
|||
|
||||
from artiq.language.types import TInt32, TFloat
|
||||
from numpy import int32, int64
|
||||
from artiq.language.core import kernel, now_mu
|
||||
from artiq.language.core import kernel
|
||||
from artiq.coredevice.spline import Spline
|
||||
from artiq.coredevice.rtio import rtio_output
|
||||
|
||||
|
@ -69,7 +69,7 @@ class Config:
|
|||
``t_sawg_spline/t_rtio_coarse = div + 1``. Default: ``0``.
|
||||
:param n: Current value of the counter. Default: ``0``.
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, _SAWG_DIV, div | (n << 16))
|
||||
rtio_output((self.channel << 8) | _SAWG_DIV, div | (n << 16))
|
||||
delay_mu(self._rtio_interval)
|
||||
|
||||
@kernel
|
||||
|
@ -108,7 +108,7 @@ class Config:
|
|||
:param clr2: Auto-clear phase accumulator of the ``phase2``/
|
||||
``frequency2`` DDS. Default: ``True``
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, _SAWG_CLR, clr0 |
|
||||
rtio_output((self.channel << 8) | _SAWG_CLR, clr0 |
|
||||
(clr1 << 1) | (clr2 << 2))
|
||||
delay_mu(self._rtio_interval)
|
||||
|
||||
|
@ -135,7 +135,7 @@ class Config:
|
|||
DUC-DDS data of this SAWG's *buddy* channel to *this* DAC
|
||||
channel. Default: ``0``.
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, _SAWG_IQ_EN, i_enable |
|
||||
rtio_output((self.channel << 8) | _SAWG_IQ_EN, i_enable |
|
||||
(q_enable << 1))
|
||||
delay_mu(self._rtio_interval)
|
||||
|
||||
|
@ -151,25 +151,25 @@ class Config:
|
|||
|
||||
.. seealso:: :meth:`set_duc_max`
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, _SAWG_DUC_MAX, limit)
|
||||
rtio_output((self.channel << 8) | _SAWG_DUC_MAX, limit)
|
||||
delay_mu(self._rtio_interval)
|
||||
|
||||
@kernel
|
||||
def set_duc_min_mu(self, limit: TInt32):
|
||||
""".. seealso:: :meth:`set_duc_max_mu`"""
|
||||
rtio_output(now_mu(), self.channel, _SAWG_DUC_MIN, limit)
|
||||
rtio_output((self.channel << 8) | _SAWG_DUC_MIN, limit)
|
||||
delay_mu(self._rtio_interval)
|
||||
|
||||
@kernel
|
||||
def set_out_max_mu(self, limit: TInt32):
|
||||
""".. seealso:: :meth:`set_duc_max_mu`"""
|
||||
rtio_output(now_mu(), self.channel, _SAWG_OUT_MAX, limit)
|
||||
rtio_output((self.channel << 8) | _SAWG_OUT_MAX, limit)
|
||||
delay_mu(self._rtio_interval)
|
||||
|
||||
@kernel
|
||||
def set_out_min_mu(self, limit: TInt32):
|
||||
""".. seealso:: :meth:`set_duc_max_mu`"""
|
||||
rtio_output(now_mu(), self.channel, _SAWG_OUT_MIN, limit)
|
||||
rtio_output((self.channel << 8) | _SAWG_OUT_MIN, limit)
|
||||
delay_mu(self._rtio_interval)
|
||||
|
||||
@kernel
|
||||
|
@ -342,7 +342,7 @@ class SAWG:
|
|||
settings.
|
||||
|
||||
This method advances the timeline by the time required to perform all
|
||||
seven writes to the configuration channel.
|
||||
7 writes to the configuration channel, plus 9 coarse RTIO cycles.
|
||||
"""
|
||||
self.config.set_div(0, 0)
|
||||
self.config.set_clr(1, 1, 1)
|
||||
|
@ -352,11 +352,21 @@ class SAWG:
|
|||
self.config.set_out_min(-1.)
|
||||
self.config.set_out_max(1.)
|
||||
self.frequency0.set_mu(0)
|
||||
coarse_cycle = int64(self.core.ref_multiplier)
|
||||
delay_mu(coarse_cycle)
|
||||
self.frequency1.set_mu(0)
|
||||
delay_mu(coarse_cycle)
|
||||
self.frequency2.set_mu(0)
|
||||
delay_mu(coarse_cycle)
|
||||
self.phase0.set_mu(0)
|
||||
delay_mu(coarse_cycle)
|
||||
self.phase1.set_mu(0)
|
||||
delay_mu(coarse_cycle)
|
||||
self.phase2.set_mu(0)
|
||||
delay_mu(coarse_cycle)
|
||||
self.amplitude1.set_mu(0)
|
||||
delay_mu(coarse_cycle)
|
||||
self.amplitude2.set_mu(0)
|
||||
delay_mu(coarse_cycle)
|
||||
self.offset.set_mu(0)
|
||||
delay_mu(coarse_cycle)
|
||||
|
|
|
@ -6,13 +6,15 @@ class ShiftReg:
|
|||
"""Driver for shift registers/latch combos connected to TTLs"""
|
||||
kernel_invariants = {"dt", "n"}
|
||||
|
||||
def __init__(self, dmgr, clk, ser, latch, n=32, dt=10*us):
|
||||
def __init__(self, dmgr, clk, ser, latch, n=32, dt=10*us, ser_in=None):
|
||||
self.core = dmgr.get("core")
|
||||
self.clk = dmgr.get(clk)
|
||||
self.ser = dmgr.get(ser)
|
||||
self.latch = dmgr.get(latch)
|
||||
self.n = n
|
||||
self.dt = dt
|
||||
if ser_in is not None:
|
||||
self.ser_in = dmgr.get(ser_in)
|
||||
|
||||
@kernel
|
||||
def set(self, data):
|
||||
|
@ -34,3 +36,19 @@ class ShiftReg:
|
|||
delay(self.dt)
|
||||
self.latch.off()
|
||||
delay(self.dt)
|
||||
|
||||
@kernel
|
||||
def get(self):
|
||||
delay(-2*(self.n + 1)*self.dt)
|
||||
data = 0
|
||||
for i in range(self.n):
|
||||
data <<= 1
|
||||
self.ser_in.sample_input()
|
||||
if self.ser_in.sample_get():
|
||||
data |= 1
|
||||
delay(self.dt)
|
||||
self.clk.on()
|
||||
delay(self.dt)
|
||||
self.clk.off()
|
||||
delay(self.dt)
|
||||
return data
|
||||
|
|
|
@ -1,346 +0,0 @@
|
|||
"""
|
||||
Driver for generic SPI on RTIO.
|
||||
|
||||
Output event replacement is not supported and issuing commands at the same
|
||||
time is an error.
|
||||
"""
|
||||
|
||||
|
||||
import numpy
|
||||
|
||||
from artiq.language.core import syscall, kernel, portable, now_mu, delay_mu
|
||||
from artiq.language.types import TInt32, TNone
|
||||
from artiq.language.units import MHz
|
||||
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SPI_DATA_ADDR", "SPI_XFER_ADDR", "SPI_CONFIG_ADDR",
|
||||
"SPI_OFFLINE", "SPI_ACTIVE", "SPI_PENDING",
|
||||
"SPI_CS_POLARITY", "SPI_CLK_POLARITY", "SPI_CLK_PHASE",
|
||||
"SPI_LSB_FIRST", "SPI_HALF_DUPLEX",
|
||||
"SPIMaster", "NRTSPIMaster"
|
||||
]
|
||||
|
||||
|
||||
SPI_DATA_ADDR, SPI_XFER_ADDR, SPI_CONFIG_ADDR = range(3)
|
||||
(
|
||||
SPI_OFFLINE,
|
||||
SPI_ACTIVE,
|
||||
SPI_PENDING,
|
||||
SPI_CS_POLARITY,
|
||||
SPI_CLK_POLARITY,
|
||||
SPI_CLK_PHASE,
|
||||
SPI_LSB_FIRST,
|
||||
SPI_HALF_DUPLEX,
|
||||
) = (1 << i for i in range(8))
|
||||
|
||||
SPI_RT2WB_READ = 1 << 2
|
||||
|
||||
|
||||
class SPIMaster:
|
||||
"""Core device Serial Peripheral Interface (SPI) bus master.
|
||||
Owns one SPI bus.
|
||||
|
||||
**Transfer Sequence**:
|
||||
|
||||
* If desired, write the ``config`` register (:meth:`set_config`)
|
||||
to configure and activate the core.
|
||||
* If desired, write the ``xfer`` register (:meth:`set_xfer`)
|
||||
to set ``cs_n``, ``write_length``, and ``read_length``.
|
||||
* :meth:`write` to the ``data`` register (also for transfers with
|
||||
zero bits to be written). Writing starts the transfer.
|
||||
* If desired, :meth:`read_sync` (or :meth:`read_async` followed by a
|
||||
:meth:`input_async` later) the ``data`` register corresponding to
|
||||
the last completed transfer.
|
||||
* If desired, :meth:`set_xfer` for the next transfer.
|
||||
* If desired, :meth:`write` ``data`` queuing the next
|
||||
(possibly chained) transfer.
|
||||
|
||||
**Notes**:
|
||||
|
||||
* In order to chain a transfer onto an in-flight transfer without
|
||||
deasserting ``cs`` in between, the second :meth:`write` needs to
|
||||
happen strictly later than ``2*ref_period_mu`` (two coarse RTIO
|
||||
cycles) but strictly earlier than ``xfer_period_mu + write_period_mu``
|
||||
after the first. Note that :meth:`write` already applies a delay of
|
||||
``xfer_period_mu + write_period_mu``.
|
||||
* A full transfer takes ``write_period_mu + xfer_period_mu``.
|
||||
* Chained transfers can happen every ``xfer_period_mu``.
|
||||
* Read data is available every ``xfer_period_mu`` starting
|
||||
a bit after xfer_period_mu (depending on ``clk_phase``).
|
||||
* As a consequence, in order to chain transfers together, new data must
|
||||
be written before the pending transfer's read data becomes available.
|
||||
|
||||
:param channel: RTIO channel number of the SPI bus to control.
|
||||
"""
|
||||
|
||||
kernel_invariants = {"core", "ref_period_mu", "channel"}
|
||||
|
||||
def __init__(self, dmgr, channel, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.ref_period_mu = self.core.seconds_to_mu(
|
||||
self.core.coarse_ref_period)
|
||||
assert self.ref_period_mu == self.core.ref_multiplier
|
||||
self.channel = channel
|
||||
self.write_period_mu = numpy.int64(0)
|
||||
self.read_period_mu = numpy.int64(0)
|
||||
self.xfer_period_mu = numpy.int64(0)
|
||||
|
||||
@portable
|
||||
def frequency_to_div(self, f):
|
||||
return int(1/(f*self.core.mu_to_seconds(self.ref_period_mu))) + 1
|
||||
|
||||
@kernel
|
||||
def set_config(self, flags=0, write_freq=20*MHz, read_freq=20*MHz):
|
||||
"""Set the configuration register.
|
||||
|
||||
* If ``config.cs_polarity`` == 0 (``cs`` active low, the default),
|
||||
"``cs_n`` all deasserted" means "all ``cs_n`` bits high".
|
||||
* ``cs_n`` is not mandatory in the pads supplied to the gateware core.
|
||||
Framing and chip selection can also be handled independently
|
||||
through other means, e.g. ``TTLOut``.
|
||||
* If there is a ``miso`` wire in the pads supplied in the gateware,
|
||||
input and output may be two signals ("4-wire SPI"),
|
||||
otherwise ``mosi`` must be used for both output and input
|
||||
("3-wire SPI") and ``config.half_duplex`` must to be set
|
||||
when reading data is desired or when the slave drives the
|
||||
``mosi`` signal at any point.
|
||||
* The first bit output on ``mosi`` is always the MSB/LSB (depending
|
||||
on ``config.lsb_first``) of the ``data`` register, independent of
|
||||
``xfer.write_length``. The last bit input from ``miso`` always ends
|
||||
up in the LSB/MSB (respectively) of the ``data`` register,
|
||||
independent of ``xfer.read_length``.
|
||||
* Writes to the ``config`` register take effect immediately.
|
||||
|
||||
**Configuration flags**:
|
||||
|
||||
* :const:`SPI_OFFLINE`: all pins high-z (reset=1)
|
||||
* :const:`SPI_ACTIVE`: transfer in progress (read-only)
|
||||
* :const:`SPI_PENDING`: transfer pending in intermediate buffer
|
||||
(read-only)
|
||||
* :const:`SPI_CS_POLARITY`: active level of ``cs_n`` (reset=0)
|
||||
* :const:`SPI_CLK_POLARITY`: idle level of ``clk`` (reset=0)
|
||||
* :const:`SPI_CLK_PHASE`: first edge after ``cs`` assertion to sample
|
||||
data on (reset=0). In Motorola/Freescale SPI language
|
||||
(:const:`SPI_CLK_POLARITY`, :const:`SPI_CLK_PHASE`) == (CPOL, CPHA):
|
||||
|
||||
- (0, 0): idle low, output on falling, input on rising
|
||||
- (0, 1): idle low, output on rising, input on falling
|
||||
- (1, 0): idle high, output on rising, input on falling
|
||||
- (1, 1): idle high, output on falling, input on rising
|
||||
* :const:`SPI_LSB_FIRST`: LSB is the first bit on the wire (reset=0)
|
||||
* :const:`SPI_HALF_DUPLEX`: 3-wire SPI, in/out on ``mosi`` (reset=0)
|
||||
|
||||
This method advances the timeline by the duration of the
|
||||
RTIO-to-Wishbone bus transaction (three RTIO clock cycles).
|
||||
|
||||
:param flags: A bit map of `SPI_*` flags.
|
||||
:param write_freq: Desired SPI clock frequency during write bits.
|
||||
:param read_freq: Desired SPI clock frequency during read bits.
|
||||
"""
|
||||
self.set_config_mu(flags, self.frequency_to_div(write_freq),
|
||||
self.frequency_to_div(read_freq))
|
||||
|
||||
@kernel
|
||||
def set_config_mu(self, flags=0, write_div=6, read_div=6):
|
||||
"""Set the ``config`` register (in SPI bus machine units).
|
||||
|
||||
.. seealso:: :meth:`set_config`
|
||||
|
||||
:param write_div: Counter load value to divide the RTIO
|
||||
clock by to generate the SPI write clk. (minimum=2, reset=2)
|
||||
``f_rtio_clk/f_spi_write == write_div``. If ``write_div`` is odd,
|
||||
the setup phase of the SPI clock is biased to longer lengths
|
||||
by one RTIO clock cycle.
|
||||
:param read_div: Ditto for the read clock.
|
||||
"""
|
||||
if write_div > 257 or write_div < 2 or read_div > 257 or read_div < 2:
|
||||
raise ValueError('Divider values out of range')
|
||||
rtio_output(now_mu(), self.channel, SPI_CONFIG_ADDR, flags |
|
||||
((write_div - 2) << 16) | ((read_div - 2) << 24))
|
||||
self.write_period_mu = int(write_div*self.ref_period_mu)
|
||||
self.read_period_mu = int(read_div*self.ref_period_mu)
|
||||
delay_mu(3*self.ref_period_mu)
|
||||
|
||||
@kernel
|
||||
def set_xfer(self, chip_select=0, write_length=0, read_length=0):
|
||||
"""Set the ``xfer`` register.
|
||||
|
||||
* Every transfer consists of a write of ``write_length`` bits
|
||||
immediately followed by a read of ``read_length`` bits.
|
||||
* ``cs_n`` is asserted at the beginning and deasserted at the end
|
||||
of the transfer if there is no other transfer pending.
|
||||
* ``cs_n`` handling is agnostic to whether it is one-hot or decoded
|
||||
somewhere downstream. If it is decoded, "``cs_n`` all deasserted"
|
||||
should be handled accordingly (no slave selected).
|
||||
If it is one-hot, asserting multiple slaves should only be attempted
|
||||
if ``miso`` is either not connected between slaves, or open
|
||||
collector, or correctly multiplexed externally.
|
||||
* For 4-wire SPI only the sum of ``read_length`` and ``write_length``
|
||||
matters. The behavior is the same (except for clock speeds) no matter
|
||||
how the total transfer length is divided between the two. For
|
||||
3-wire SPI, the direction of ``mosi`` is switched from output to
|
||||
input after ``write_length`` bits.
|
||||
* Data output on ``mosi`` in 4-wire SPI during the read cycles is what
|
||||
is found in the data register at the time.
|
||||
Data in the ``data`` register outside the least/most (depending
|
||||
on ``config.lsb_first``) significant ``read_length`` bits is what is
|
||||
seen on ``miso`` (or ``mosi`` if ``config.half_duplex``)
|
||||
during the write cycles.
|
||||
* Writes to ``xfer`` are synchronized to the start of the next
|
||||
(possibly chained) transfer.
|
||||
|
||||
This method advances the timeline by the duration of the
|
||||
RTIO-to-Wishbone bus transaction (three RTIO clock cycles).
|
||||
|
||||
:param chip_select: Bit mask of chip selects to assert. Or number of
|
||||
the chip select to assert if ``cs`` is decoded downstream.
|
||||
(reset=0)
|
||||
:param write_length: Number of bits to write during the next transfer.
|
||||
(reset=0)
|
||||
:param read_length: Number of bits to read during the next transfer.
|
||||
(reset=0)
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, SPI_XFER_ADDR,
|
||||
chip_select | (write_length << 16) | (read_length << 24))
|
||||
self.xfer_period_mu = int(write_length*self.write_period_mu +
|
||||
read_length*self.read_period_mu)
|
||||
delay_mu(3*self.ref_period_mu)
|
||||
|
||||
@kernel
|
||||
def write(self, data=0):
|
||||
"""Write data to data register.
|
||||
|
||||
* The ``data`` register and the shift register are 32 bits wide.
|
||||
If there are no writes to the register, ``miso`` data reappears on
|
||||
``mosi`` after 32 cycles.
|
||||
* A wishbone data register write is acknowledged when the
|
||||
transfer has been written to the intermediate buffer.
|
||||
It will be started when there are no other transactions being
|
||||
executed, either beginning a new SPI transfer of chained
|
||||
to an in-flight transfer.
|
||||
* Writes take three ``ref_period`` cycles unless another
|
||||
chained transfer is pending and the transfer being
|
||||
executed is not complete.
|
||||
* The SPI ``data`` register is double-buffered: Once a transfer has
|
||||
started, new write data can be written, queuing a new transfer.
|
||||
Transfers submitted this way are chained and executed without
|
||||
deasserting ``cs`` in between. Once a transfer completes,
|
||||
the previous transfer's read data is available in the
|
||||
``data`` register.
|
||||
* For bit alignment and bit ordering see :meth:`set_config`.
|
||||
|
||||
This method advances the timeline by the duration of the SPI transfer.
|
||||
If a transfer is to be chained, the timeline needs to be rewound.
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, SPI_DATA_ADDR, data)
|
||||
delay_mu(self.xfer_period_mu + self.write_period_mu)
|
||||
|
||||
@kernel
|
||||
def read_async(self):
|
||||
"""Trigger an asynchronous read from the ``data`` register.
|
||||
|
||||
For bit alignment and bit ordering see :meth:`set_config`.
|
||||
|
||||
Reads always finish in two cycles.
|
||||
|
||||
Every data register read triggered by a :meth:`read_async`
|
||||
must be matched by a :meth:`input_async` to retrieve the data.
|
||||
|
||||
This method advances the timeline by the duration of the
|
||||
RTIO-to-Wishbone bus transaction (three RTIO clock cycles).
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, SPI_DATA_ADDR | SPI_RT2WB_READ, 0)
|
||||
delay_mu(3*self.ref_period_mu)
|
||||
|
||||
@kernel
|
||||
def input_async(self):
|
||||
"""Retrieves data read asynchronously from the ``data`` register.
|
||||
|
||||
:meth:`input_async` must match a preeeding :meth:`read_async`.
|
||||
"""
|
||||
return rtio_input_data(self.channel)
|
||||
|
||||
@kernel
|
||||
def read_sync(self):
|
||||
"""Read the ``data`` register synchronously.
|
||||
|
||||
This is a shortcut for :meth:`read_async` followed by
|
||||
:meth:`input_async`.
|
||||
"""
|
||||
self.read_async()
|
||||
return self.input_async()
|
||||
|
||||
@kernel
|
||||
def _get_xfer_sync(self):
|
||||
rtio_output(now_mu(), self.channel, SPI_XFER_ADDR | SPI_RT2WB_READ, 0)
|
||||
return rtio_input_data(self.channel)
|
||||
|
||||
@kernel
|
||||
def _get_config_sync(self):
|
||||
rtio_output(now_mu(), self.channel, SPI_CONFIG_ADDR | SPI_RT2WB_READ,
|
||||
0)
|
||||
return rtio_input_data(self.channel)
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def spi_set_config(busno: TInt32, flags: TInt32, write_div: TInt32, read_div: TInt32) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def spi_set_xfer(busno: TInt32, chip_select: TInt32, write_length: TInt32, read_length: TInt32) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def spi_write(busno: TInt32, data: TInt32) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def spi_read(busno: TInt32) -> TInt32:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
class NRTSPIMaster:
|
||||
"""Core device non-realtime Serial Peripheral Interface (SPI) bus master.
|
||||
Owns one non-realtime SPI bus.
|
||||
|
||||
With this driver, SPI transactions and are performed by the CPU without
|
||||
involving RTIO.
|
||||
|
||||
Realtime and non-realtime buses are separate and defined at bitstream
|
||||
compilation time.
|
||||
|
||||
See :class:`SPIMaster` for a description of the methods.
|
||||
"""
|
||||
def __init__(self, dmgr, busno=0, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.busno = busno
|
||||
|
||||
@kernel
|
||||
def set_config_mu(self, flags=0, write_div=6, read_div=6):
|
||||
"""Set the ``config`` register.
|
||||
|
||||
Note that the non-realtime SPI cores are usually clocked by the system
|
||||
clock and not the RTIO clock. In many cases, the SPI configuration is
|
||||
already set by the firmware and you do not need to call this method.
|
||||
|
||||
The offline bit cannot be set using this method.
|
||||
The SPI bus is briefly taken offline when this method is called.
|
||||
"""
|
||||
spi_set_config(self.busno, flags, write_div, read_div)
|
||||
|
||||
@kernel
|
||||
def set_xfer(self, chip_select=0, write_length=0, read_length=0):
|
||||
spi_set_xfer(self.busno, chip_select, write_length, read_length)
|
||||
|
||||
@kernel
|
||||
def write(self, data=0):
|
||||
spi_write(self.busno, data)
|
||||
|
||||
@kernel
|
||||
def read(self):
|
||||
return spi_read(self.busno)
|
|
@ -0,0 +1,288 @@
|
|||
"""
|
||||
Driver for generic SPI on RTIO.
|
||||
|
||||
This ARTIQ coredevice driver corresponds to the "new" MiSoC SPI core (v2).
|
||||
|
||||
Output event replacement is not supported and issuing commands at the same
|
||||
time is an error.
|
||||
"""
|
||||
|
||||
from artiq.language.core import syscall, kernel, portable, delay_mu
|
||||
from artiq.language.types import TInt32, TNone
|
||||
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SPI_DATA_ADDR", "SPI_CONFIG_ADDR",
|
||||
"SPI_OFFLINE", "SPI_END", "SPI_INPUT",
|
||||
"SPI_CS_POLARITY", "SPI_CLK_POLARITY", "SPI_CLK_PHASE",
|
||||
"SPI_LSB_FIRST", "SPI_HALF_DUPLEX",
|
||||
"SPIMaster", "NRTSPIMaster"
|
||||
]
|
||||
|
||||
SPI_DATA_ADDR = 0
|
||||
SPI_CONFIG_ADDR = 1
|
||||
|
||||
SPI_OFFLINE = 0x01
|
||||
SPI_END = 0x02
|
||||
SPI_INPUT = 0x04
|
||||
SPI_CS_POLARITY = 0x08
|
||||
SPI_CLK_POLARITY = 0x10
|
||||
SPI_CLK_PHASE = 0x20
|
||||
SPI_LSB_FIRST = 0x40
|
||||
SPI_HALF_DUPLEX = 0x80
|
||||
|
||||
|
||||
class SPIMaster:
|
||||
"""Core device Serial Peripheral Interface (SPI) bus master.
|
||||
|
||||
Owns one SPI bus.
|
||||
|
||||
This ARTIQ coredevice driver corresponds to the "new" MiSoC SPI core (v2).
|
||||
|
||||
**Transfer Sequence**:
|
||||
|
||||
* If necessary, set the ``config`` register (:meth:`set_config` and
|
||||
:meth:`set_config_mu`) to activate and configure the core and to set
|
||||
various transfer parameters like transfer length, clock divider,
|
||||
and chip selects.
|
||||
* :meth:`write` to the ``data`` register. Writing starts the transfer.
|
||||
* If the transfer included submitting the SPI input data as an RTIO input
|
||||
event (``SPI_INPUT`` set), then :meth:`read` the ``data``.
|
||||
* If ``SPI_END`` was not set, repeat the transfer sequence.
|
||||
|
||||
A **transaction** consists of one or more **transfers**. The chip select
|
||||
pattern is asserted for the entire length of the transaction. All but the
|
||||
last transfer are submitted with ``SPI_END`` cleared in the configuration
|
||||
register.
|
||||
|
||||
:param channel: RTIO channel number of the SPI bus to control.
|
||||
:param div: Initial CLK divider, see also: :meth:`update_xfer_duration_mu`
|
||||
:param length: Initial transfer length, see also:
|
||||
:meth:`update_xfer_duration_mu`
|
||||
:param core_device: Core device name
|
||||
"""
|
||||
kernel_invariants = {"core", "ref_period_mu", "channel"}
|
||||
|
||||
def __init__(self, dmgr, channel, div=0, length=0, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.ref_period_mu = self.core.seconds_to_mu(
|
||||
self.core.coarse_ref_period)
|
||||
assert self.ref_period_mu == self.core.ref_multiplier
|
||||
self.channel = channel
|
||||
self.update_xfer_duration_mu(div, length)
|
||||
|
||||
@portable
|
||||
def frequency_to_div(self, f):
|
||||
"""Convert a SPI clock frequency to the closest SPI clock divider."""
|
||||
return int(round(1/(f*self.core.mu_to_seconds(self.ref_period_mu))))
|
||||
|
||||
@kernel
|
||||
def set_config(self, flags, length, freq, cs):
|
||||
"""Set the configuration register.
|
||||
|
||||
* If ``SPI_CS_POLARITY`` is cleared (``cs`` active low, the default),
|
||||
"``cs`` all deasserted" means "all ``cs_n`` bits high".
|
||||
* ``cs_n`` is not mandatory in the pads supplied to the gateware core.
|
||||
Framing and chip selection can also be handled independently
|
||||
through other means, e.g. ``TTLOut``.
|
||||
* If there is a ``miso`` wire in the pads supplied in the gateware,
|
||||
input and output may be two signals ("4-wire SPI"),
|
||||
otherwise ``mosi`` must be used for both output and input
|
||||
("3-wire SPI") and ``SPI_HALF_DUPLEX`` must to be set
|
||||
when reading data or when the slave drives the
|
||||
``mosi`` signal at any point.
|
||||
* The first bit output on ``mosi`` is always the MSB/LSB (depending
|
||||
on ``SPI_LSB_FIRST``) of the ``data`` written, independent of
|
||||
the ``length`` of the transfer. The last bit input from ``miso``
|
||||
always ends up in the LSB/MSB (respectively) of the ``data`` read,
|
||||
independent of the ``length`` of the transfer.
|
||||
* ``cs`` is asserted at the beginning and deasserted at the end
|
||||
of the transaction.
|
||||
* ``cs`` handling is agnostic to whether it is one-hot or decoded
|
||||
somewhere downstream. If it is decoded, "``cs`` all deasserted"
|
||||
should be handled accordingly (no slave selected).
|
||||
If it is one-hot, asserting multiple slaves should only be attempted
|
||||
if ``miso`` is either not connected between slaves, or open
|
||||
collector, or correctly multiplexed externally.
|
||||
* Changes to the configuration register take effect on the start of the
|
||||
next transfer with the exception of ``SPI_OFFLINE`` which takes
|
||||
effect immediately.
|
||||
* The SPI core can only be written to when it is idle or waiting
|
||||
for the next transfer data. Writing (:meth:`set_config`,
|
||||
:meth:`set_config_mu` or :meth:`write`)
|
||||
when the core is busy will result in an RTIO busy error being logged.
|
||||
|
||||
This method advances the timeline by one coarse RTIO clock cycle.
|
||||
|
||||
**Configuration flags**:
|
||||
|
||||
* :const:`SPI_OFFLINE`: all pins high-z (reset=1)
|
||||
* :const:`SPI_END`: transfer in progress (reset=1)
|
||||
* :const:`SPI_INPUT`: submit SPI read data as RTIO input event when
|
||||
transfer is complete (reset=0)
|
||||
* :const:`SPI_CS_POLARITY`: active level of ``cs_n`` (reset=0)
|
||||
* :const:`SPI_CLK_POLARITY`: idle level of ``clk`` (reset=0)
|
||||
* :const:`SPI_CLK_PHASE`: first edge after ``cs`` assertion to sample
|
||||
data on (reset=0). In Motorola/Freescale SPI language
|
||||
(:const:`SPI_CLK_POLARITY`, :const:`SPI_CLK_PHASE`) == (CPOL, CPHA):
|
||||
|
||||
- (0, 0): idle low, output on falling, input on rising
|
||||
- (0, 1): idle low, output on rising, input on falling
|
||||
- (1, 0): idle high, output on rising, input on falling
|
||||
- (1, 1): idle high, output on falling, input on rising
|
||||
* :const:`SPI_LSB_FIRST`: LSB is the first bit on the wire (reset=0)
|
||||
* :const:`SPI_HALF_DUPLEX`: 3-wire SPI, in/out on ``mosi`` (reset=0)
|
||||
|
||||
:param flags: A bit map of `SPI_*` flags.
|
||||
:param length: Number of bits to write during the next transfer.
|
||||
(reset=1)
|
||||
:param freq: Desired SPI clock frequency. (reset=f_rtio/2)
|
||||
:param cs: Bit pattern of chip selects to assert.
|
||||
Or number of the chip select to assert if ``cs`` is decoded
|
||||
downstream. (reset=0)
|
||||
"""
|
||||
self.set_config_mu(flags, length, self.frequency_to_div(freq), cs)
|
||||
|
||||
@kernel
|
||||
def set_config_mu(self, flags, length, div, cs):
|
||||
"""Set the ``config`` register (in SPI bus machine units).
|
||||
|
||||
.. seealso:: :meth:`set_config`
|
||||
|
||||
:param flags: A bit map of `SPI_*` flags.
|
||||
:param length: Number of bits to write during the next transfer.
|
||||
(reset=1)
|
||||
:param div: Counter load value to divide the RTIO
|
||||
clock by to generate the SPI clock. (minimum=2, reset=2)
|
||||
``f_rtio_clk/f_spi == div``. If ``div`` is odd,
|
||||
the setup phase of the SPI clock is one coarse RTIO clock cycle
|
||||
longer than the hold phase.
|
||||
:param cs: Bit pattern of chip selects to assert.
|
||||
Or number of the chip select to assert if ``cs`` is decoded
|
||||
downstream. (reset=0)
|
||||
"""
|
||||
if length > 32 or length < 1:
|
||||
raise ValueError("Invalid SPI transfer length")
|
||||
if div > 257 or div < 2:
|
||||
raise ValueError("Invalid SPI clock divider")
|
||||
rtio_output((self.channel << 8) | SPI_CONFIG_ADDR, flags |
|
||||
((length - 1) << 8) | ((div - 2) << 16) | (cs << 24))
|
||||
self.update_xfer_duration_mu(div, length)
|
||||
delay_mu(self.ref_period_mu)
|
||||
|
||||
@portable
|
||||
def update_xfer_duration_mu(self, div, length):
|
||||
"""Calculate and set the transfer duration.
|
||||
|
||||
This method updates the SPI transfer duration which is used
|
||||
in :meth:`write` to advance the timeline.
|
||||
|
||||
Use this method (and avoid having to call :meth:`set_config_mu`)
|
||||
when the divider and transfer length have been configured
|
||||
(using :meth:`set_config` or :meth:`set_config_mu`) by previous
|
||||
experiments and are known.
|
||||
|
||||
This method is portable and can also be called from e.g.
|
||||
:meth:`__init__`.
|
||||
|
||||
.. warning:: If this method is called while recording a DMA
|
||||
sequence, the playback of the sequence will not update the
|
||||
driver state.
|
||||
When required, update the driver state manually (by calling
|
||||
this method) after playing back a DMA sequence.
|
||||
|
||||
:param div: SPI clock divider (see: :meth:`set_config_mu`)
|
||||
:param length: SPI transfer length (see: :meth:`set_config_mu`)
|
||||
"""
|
||||
self.xfer_duration_mu = ((length + 1)*div + 1)*self.ref_period_mu
|
||||
|
||||
@kernel
|
||||
def write(self, data):
|
||||
"""Write SPI data to shift register register and start transfer.
|
||||
|
||||
* The ``data`` register and the shift register are 32 bits wide.
|
||||
* Data writes take one ``ref_period`` cycle.
|
||||
* A transaction consisting of a single transfer (``SPI_END``) takes
|
||||
:attr:`xfer_duration_mu` ``=(n + 1)*div`` cycles RTIO time where
|
||||
``n`` is the number of bits and ``div`` is the SPI clock divider.
|
||||
* Transfers in a multi-transfer transaction take up to one SPI clock
|
||||
cycle less time depending on multiple parameters. Advanced users may
|
||||
rewind the timeline appropriately to achieve faster multi-transfer
|
||||
transactions.
|
||||
* The SPI core will be busy for the duration of the SPI transfer.
|
||||
* For bit alignment and bit ordering see :meth:`set_config`.
|
||||
* The SPI core can only be written to when it is idle or waiting
|
||||
for the next transfer data. Writing (:meth:`set_config`,
|
||||
:meth:`set_config_mu` or :meth:`write`)
|
||||
when the core is busy will result in an RTIO busy error being logged.
|
||||
|
||||
This method advances the timeline by the duration of one
|
||||
single-transfer SPI transaction (:attr:`xfer_duration_mu`).
|
||||
|
||||
:param data: SPI output data to be written.
|
||||
"""
|
||||
rtio_output((self.channel << 8) | SPI_DATA_ADDR, data)
|
||||
delay_mu(self.xfer_duration_mu)
|
||||
|
||||
@kernel
|
||||
def read(self):
|
||||
"""Read SPI data submitted by the SPI core.
|
||||
|
||||
For bit alignment and bit ordering see :meth:`set_config`.
|
||||
|
||||
This method does not alter the timeline.
|
||||
|
||||
:return: SPI input data.
|
||||
"""
|
||||
return rtio_input_data(self.channel)
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def spi_set_config(busno: TInt32, flags: TInt32, length: TInt32, div: TInt32, cs: TInt32) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def spi_write(busno: TInt32, data: TInt32) -> TNone:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
@syscall(flags={"nounwind", "nowrite"})
|
||||
def spi_read(busno: TInt32) -> TInt32:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
|
||||
|
||||
class NRTSPIMaster:
|
||||
"""Core device non-realtime Serial Peripheral Interface (SPI) bus master.
|
||||
Owns one non-realtime SPI bus.
|
||||
|
||||
With this driver, SPI transactions and are performed by the CPU without
|
||||
involving RTIO.
|
||||
|
||||
Realtime and non-realtime buses are separate and defined at bitstream
|
||||
compilation time.
|
||||
|
||||
See :class:`SPIMaster` for a description of the methods.
|
||||
"""
|
||||
def __init__(self, dmgr, busno=0, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.busno = busno
|
||||
|
||||
@kernel
|
||||
def set_config_mu(self, flags=0, length=8, div=6, cs=1):
|
||||
"""Set the ``config`` register.
|
||||
|
||||
Note that the non-realtime SPI cores are usually clocked by the system
|
||||
clock and not the RTIO clock. In many cases, the SPI configuration is
|
||||
already set by the firmware and you do not need to call this method.
|
||||
"""
|
||||
spi_set_config(self.busno, flags, length, div, cs)
|
||||
|
||||
@kernel
|
||||
def write(self, data=0):
|
||||
spi_write(self.busno, data)
|
||||
|
||||
@kernel
|
||||
def read(self):
|
||||
return spi_read(self.busno)
|
|
@ -1,5 +1,5 @@
|
|||
from numpy import int32, int64
|
||||
from artiq.language.core import kernel, now_mu, portable, delay
|
||||
from artiq.language.core import kernel, portable, delay
|
||||
from artiq.coredevice.rtio import rtio_output, rtio_output_wide
|
||||
from artiq.language.types import TInt32, TInt64, TFloat
|
||||
|
||||
|
@ -65,7 +65,7 @@ class Spline:
|
|||
|
||||
:param value: Spline value in integer machine units.
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, 0, value)
|
||||
rtio_output(self.channel << 8, value)
|
||||
|
||||
@kernel(flags={"fast-math"})
|
||||
def set(self, value: TFloat):
|
||||
|
@ -76,9 +76,9 @@ class Spline:
|
|||
if self.width > 32:
|
||||
l = [int32(0)] * 2
|
||||
self.pack_coeff_mu([self.to_mu64(value)], l)
|
||||
rtio_output_wide(now_mu(), self.channel, 0, l)
|
||||
rtio_output_wide(self.channel << 8, l)
|
||||
else:
|
||||
rtio_output(now_mu(), self.channel, 0, self.to_mu(value))
|
||||
rtio_output(self.channel << 8, self.to_mu(value))
|
||||
|
||||
@kernel
|
||||
def set_coeff_mu(self, value): # TList(TInt32)
|
||||
|
@ -86,7 +86,7 @@ class Spline:
|
|||
|
||||
:param value: Spline packed raw values.
|
||||
"""
|
||||
rtio_output_wide(now_mu(), self.channel, 0, value)
|
||||
rtio_output_wide(self.channel << 8, value)
|
||||
|
||||
@portable(flags={"fast-math"})
|
||||
def pack_coeff_mu(self, coeff, packed): # TList(TInt64), TList(TInt32)
|
||||
|
|
|
@ -0,0 +1,564 @@
|
|||
from artiq.language.core import kernel, delay, delay_mu, portable
|
||||
from artiq.language.units import us, ns
|
||||
from artiq.coredevice.rtio import rtio_output, rtio_input_data
|
||||
from artiq.coredevice import spi2 as spi
|
||||
from artiq.coredevice import urukul, sampler
|
||||
|
||||
|
||||
COEFF_WIDTH = 18
|
||||
Y_FULL_SCALE_MU = (1 << (COEFF_WIDTH - 1)) - 1
|
||||
COEFF_DEPTH = 10 + 1
|
||||
WE = 1 << COEFF_DEPTH + 1
|
||||
STATE_SEL = 1 << COEFF_DEPTH
|
||||
CONFIG_SEL = 1 << COEFF_DEPTH - 1
|
||||
CONFIG_ADDR = CONFIG_SEL | STATE_SEL
|
||||
T_CYCLE = (2*(8 + 64) + 2)*8*ns # Must match gateware Servo.t_cycle.
|
||||
COEFF_SHIFT = 11
|
||||
|
||||
|
||||
@portable
|
||||
def y_mu_to_full_scale(y):
|
||||
"""Convert servo Y data from machine units to units of full scale."""
|
||||
return y / Y_FULL_SCALE_MU
|
||||
|
||||
|
||||
@portable
|
||||
def adc_mu_to_volts(x, gain):
|
||||
"""Convert servo ADC data from machine units to Volt."""
|
||||
val = (x >> 1) & 0xffff
|
||||
mask = 1 << 15
|
||||
val = -(val & mask) + (val & ~mask)
|
||||
return sampler.adc_mu_to_volt(val, gain)
|
||||
|
||||
|
||||
class SUServo:
|
||||
"""Sampler-Urukul Servo parent and configuration device.
|
||||
|
||||
Sampler-Urukul Servo is a integrated device controlling one
|
||||
8-channel ADC (Sampler) and two 4-channel DDS (Urukuls) with a DSP engine
|
||||
connecting the ADC data and the DDS output amplitudes to enable
|
||||
feedback. SU Servo can for example be used to implement intensity
|
||||
stabilization of laser beams with an amplifier and AOM driven by Urukul
|
||||
and a photodetector connected to Sampler.
|
||||
|
||||
Additionally SU Servo supports multiple preconfigured profiles per channel
|
||||
and features like automatic integrator hold.
|
||||
|
||||
Notes:
|
||||
|
||||
* See the SU Servo variant of the Kasli target for an example of how to
|
||||
connect the gateware and the devices. Sampler and each Urukul need
|
||||
two EEM connections.
|
||||
* Ensure that both Urukuls are AD9910 variants and have the on-board
|
||||
dip switches set to 1100 (first two on, last two off).
|
||||
* Refer to the Sampler and Urukul documentation and the SU Servo
|
||||
example device database for runtime configuration of the devices
|
||||
(PLLs, gains, clock routing etc.)
|
||||
|
||||
:param channel: RTIO channel number
|
||||
:param pgia_device: Name of the Sampler PGIA gain setting SPI bus
|
||||
:param cpld0_device: Name of the first Urukul CPLD SPI bus
|
||||
:param cpld1_device: Name of the second Urukul CPLD SPI bus
|
||||
:param dds0_device: Name of the AD9910 device for the DDS on the first
|
||||
Urukul
|
||||
:param dds1_device: Name of the AD9910 device for the DDS on the second
|
||||
Urukul
|
||||
:param gains: Initial value for PGIA gains shift register
|
||||
(default: 0x0000). Knowledge of this state is not transferred
|
||||
between experiments.
|
||||
:param core_device: Core device name
|
||||
"""
|
||||
kernel_invariants = {"channel", "core", "pgia", "cpld0", "cpld1",
|
||||
"dds0", "dds1", "ref_period_mu"}
|
||||
|
||||
def __init__(self, dmgr, channel, pgia_device,
|
||||
cpld0_device, cpld1_device,
|
||||
dds0_device, dds1_device,
|
||||
gains=0x0000, core_device="core"):
|
||||
|
||||
self.core = dmgr.get(core_device)
|
||||
self.pgia = dmgr.get(pgia_device)
|
||||
self.pgia.update_xfer_duration_mu(div=4, length=16)
|
||||
self.dds0 = dmgr.get(dds0_device)
|
||||
self.dds1 = dmgr.get(dds1_device)
|
||||
self.cpld0 = dmgr.get(cpld0_device)
|
||||
self.cpld1 = dmgr.get(cpld1_device)
|
||||
self.channel = channel
|
||||
self.gains = gains
|
||||
self.ref_period_mu = self.core.seconds_to_mu(
|
||||
self.core.coarse_ref_period)
|
||||
assert self.ref_period_mu == self.core.ref_multiplier
|
||||
|
||||
@kernel
|
||||
def init(self):
|
||||
"""Initialize the servo, Sampler and both Urukuls.
|
||||
|
||||
Leaves the servo disabled (see :meth:`set_config`), resets and
|
||||
configures all DDS.
|
||||
|
||||
Urukul initialization is performed blindly as there is no readback from
|
||||
the DDS or the CPLDs.
|
||||
|
||||
This method does not alter the profile configuration memory
|
||||
or the channel controls.
|
||||
"""
|
||||
self.set_config(enable=0)
|
||||
delay(3*us) # pipeline flush
|
||||
|
||||
self.pgia.set_config_mu(
|
||||
sampler.SPI_CONFIG | spi.SPI_END,
|
||||
16, 4, sampler.SPI_CS_PGIA)
|
||||
|
||||
self.cpld0.init(blind=True)
|
||||
cfg0 = self.cpld0.cfg_reg
|
||||
self.cpld0.cfg_write(cfg0 | (0xf << urukul.CFG_MASK_NU))
|
||||
self.dds0.init(blind=True)
|
||||
self.cpld0.cfg_write(cfg0)
|
||||
|
||||
self.cpld1.init(blind=True)
|
||||
cfg1 = self.cpld1.cfg_reg
|
||||
self.cpld1.cfg_write(cfg1 | (0xf << urukul.CFG_MASK_NU))
|
||||
self.dds1.init(blind=True)
|
||||
self.cpld1.cfg_write(cfg1)
|
||||
|
||||
@kernel
|
||||
def write(self, addr, value):
|
||||
"""Write to servo memory.
|
||||
|
||||
This method advances the timeline by one coarse RTIO cycle.
|
||||
|
||||
:param addr: Memory location address.
|
||||
:param value: Data to be written.
|
||||
"""
|
||||
addr |= WE
|
||||
value &= (1 << COEFF_WIDTH) - 1
|
||||
value |= (addr >> 8) << COEFF_WIDTH
|
||||
addr = addr & 0xff
|
||||
rtio_output((self.channel << 8) | addr, value)
|
||||
delay_mu(self.ref_period_mu)
|
||||
|
||||
@kernel
|
||||
def read(self, addr):
|
||||
"""Read from servo memory.
|
||||
|
||||
This method does not advance the timeline but consumes all slack.
|
||||
|
||||
:param addr: Memory location address.
|
||||
"""
|
||||
value = (addr >> 8) << COEFF_WIDTH
|
||||
addr = addr & 0xff
|
||||
rtio_output((self.channel << 8) | addr, value)
|
||||
return rtio_input_data(self.channel)
|
||||
|
||||
@kernel
|
||||
def set_config(self, enable):
|
||||
"""Set SU Servo configuration.
|
||||
|
||||
This method advances the timeline by one servo memory access.
|
||||
It does not support RTIO event replacement.
|
||||
|
||||
:param enable (int): Enable servo operation. Enabling starts servo
|
||||
iterations beginning with the ADC sampling stage. The first DDS
|
||||
update will happen about two servo cycles (~2.3 µs) after enabling
|
||||
the servo. The delay is deterministic.
|
||||
This also provides a mean for synchronization of servo updates to
|
||||
other RTIO activity.
|
||||
Disabling takes up to two servo cycles (~2.3 µs) to clear the
|
||||
processing pipeline.
|
||||
"""
|
||||
self.write(CONFIG_ADDR, enable)
|
||||
|
||||
@kernel
|
||||
def get_status(self):
|
||||
"""Get current SU Servo status.
|
||||
|
||||
This method does not advance the timeline but consumes all slack.
|
||||
|
||||
The ``done`` bit indicates that a SU Servo cycle has completed.
|
||||
It is pulsed for one RTIO cycle every SU Servo cycle and asserted
|
||||
continuously when the servo is not ``enabled`` and the pipeline has
|
||||
drained (the last DDS update is done).
|
||||
|
||||
This method returns and clears the clip indicator for all channels.
|
||||
An asserted clip indicator corresponds to the servo having encountered
|
||||
an input signal on an active channel that would have resulted in the
|
||||
IIR state exceeding the output range.
|
||||
|
||||
:return: Status. Bit 0: enabled, bit 1: done,
|
||||
bits 8-15: channel clip indicators.
|
||||
"""
|
||||
return self.read(CONFIG_ADDR)
|
||||
|
||||
@kernel
|
||||
def get_adc_mu(self, adc):
|
||||
"""Get the latest ADC reading (IIR filter input X0) in machine units.
|
||||
|
||||
This method does not advance the timeline but consumes all slack.
|
||||
|
||||
If reading servo state through this method collides with the servo
|
||||
writing that same data, the data can become invalid. To ensure
|
||||
consistent and valid data, stop the servo before using this method.
|
||||
|
||||
:param adc: ADC channel number (0-7)
|
||||
:return: 17 bit signed X0
|
||||
"""
|
||||
# State memory entries are 25 bits. Due to the pre-adder dynamic
|
||||
# range, X0/X1/OFFSET are only 24 bits. Finally, the RTIO interface
|
||||
# only returns the 18 MSBs (the width of the coefficient memory).
|
||||
return self.read(STATE_SEL | (adc << 1) | (1 << 8))
|
||||
|
||||
@kernel
|
||||
def set_pgia_mu(self, channel, gain):
|
||||
"""Set instrumentation amplifier gain of a ADC channel.
|
||||
|
||||
The four gain settings (0, 1, 2, 3) corresponds to gains of
|
||||
(1, 10, 100, 1000) respectively.
|
||||
|
||||
:param channel: Channel index
|
||||
:param gain: Gain setting
|
||||
"""
|
||||
gains = self.gains
|
||||
gains &= ~(0b11 << (channel*2))
|
||||
gains |= gain << (channel*2)
|
||||
self.pgia.write(gains << 16)
|
||||
self.gains = gains
|
||||
|
||||
@kernel
|
||||
def get_adc(self, channel):
|
||||
"""Get the latest ADC reading (IIR filter input X0).
|
||||
|
||||
This method does not advance the timeline but consumes all slack.
|
||||
|
||||
If reading servo state through this method collides with the servo
|
||||
writing that same data, the data can become invalid. To ensure
|
||||
consistent and valid data, stop the servo before using this method.
|
||||
|
||||
The PGIA gain setting must be known prior to using this method, either
|
||||
by setting the gain (:meth:`set_pgia_mu`) or by supplying it
|
||||
(:attr:`gains` or via the constructor/device database).
|
||||
|
||||
:param adc: ADC channel number (0-7)
|
||||
:return: ADC voltage
|
||||
"""
|
||||
val = self.get_adc_mu(channel)
|
||||
gain = (self.gains >> (channel*2)) & 0b11
|
||||
return adc_mu_to_volts(val, gain)
|
||||
|
||||
|
||||
class Channel:
|
||||
"""Sampler-Urukul Servo channel
|
||||
|
||||
:param channel: RTIO channel number
|
||||
:param servo_device: Name of the parent SUServo device
|
||||
"""
|
||||
kernel_invariants = {"channel", "core", "servo", "servo_channel"}
|
||||
|
||||
def __init__(self, dmgr, channel, servo_device):
|
||||
self.servo = dmgr.get(servo_device)
|
||||
self.core = self.servo.core
|
||||
self.channel = channel
|
||||
# FIXME: this assumes the mem channel is right after the control
|
||||
# channels
|
||||
self.servo_channel = self.channel + 8 - self.servo.channel
|
||||
|
||||
@kernel
|
||||
def set(self, en_out, en_iir=0, profile=0):
|
||||
"""Operate channel.
|
||||
|
||||
This method does not advance the timeline. Output RF switch setting
|
||||
takes effect immediately and is independent of any other activity
|
||||
(profile settings, other channels). The RF switch behaves like
|
||||
:class:`artiq.coredevice.ttl.TTLOut`. RTIO event replacement is
|
||||
supported. IIR updates take place once the RF switch has been enabled
|
||||
for the configured delay and the profile setting has been stable.
|
||||
Profile changes take between one and two servo cycles to reach the DDS.
|
||||
|
||||
:param en_out: RF switch enable
|
||||
:param en_iir: IIR updates enable
|
||||
:param profile: Active profile (0-31)
|
||||
"""
|
||||
rtio_output(self.channel << 8,
|
||||
en_out | (en_iir << 1) | (profile << 2))
|
||||
|
||||
@kernel
|
||||
def set_dds_mu(self, profile, ftw, offs, pow_=0):
|
||||
"""Set profile DDS coefficients in machine units.
|
||||
|
||||
.. seealso:: :meth:`set_amplitude`
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param ftw: Frequency tuning word (32 bit unsigned)
|
||||
:param offs: IIR offset (17 bit signed)
|
||||
:param pow_: Phase offset word (16 bit)
|
||||
"""
|
||||
base = (self.servo_channel << 8) | (profile << 3)
|
||||
self.servo.write(base + 0, ftw >> 16)
|
||||
self.servo.write(base + 6, (ftw & 0xffff))
|
||||
self.set_dds_offset_mu(profile, offs)
|
||||
self.servo.write(base + 2, pow_)
|
||||
|
||||
@kernel
|
||||
def set_dds(self, profile, frequency, offset, phase=0.):
|
||||
"""Set profile DDS coefficients.
|
||||
|
||||
This method advances the timeline by four servo memory accesses.
|
||||
Profile parameter changes are not synchronized. Activate a different
|
||||
profile or stop the servo to ensure synchronous changes.
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param frequency: DDS frequency in Hz
|
||||
:param offset: IIR offset (negative setpoint) in units of full scale,
|
||||
see :meth:`dds_offset_to_mu`
|
||||
:param phase: DDS phase in turns
|
||||
"""
|
||||
if self.servo_channel < 4:
|
||||
dds = self.servo.dds0
|
||||
else:
|
||||
dds = self.servo.dds1
|
||||
ftw = dds.frequency_to_ftw(frequency)
|
||||
pow_ = dds.turns_to_pow(phase)
|
||||
offs = self.dds_offset_to_mu(offset)
|
||||
self.set_dds_mu(profile, ftw, offs, pow_)
|
||||
|
||||
@kernel
|
||||
def set_dds_offset_mu(self, profile, offs):
|
||||
"""Set only IIR offset in DDS coefficient profile.
|
||||
|
||||
See :meth:`set_dds_mu` for setting the complete DDS profile.
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param offs: IIR offset (17 bit signed)
|
||||
"""
|
||||
base = (self.servo_channel << 8) | (profile << 3)
|
||||
self.servo.write(base + 4, offs)
|
||||
|
||||
@kernel
|
||||
def set_dds_offset(self, profile, offset):
|
||||
"""Set only IIR offset in DDS coefficient profile.
|
||||
|
||||
See :meth:`set_dds` for setting the complete DDS profile.
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param offset: IIR offset (negative setpoint) in units of full scale
|
||||
"""
|
||||
self.set_dds_offset_mu(profile, self.dds_offset_to_mu(offset))
|
||||
|
||||
@portable
|
||||
def dds_offset_to_mu(self, offset):
|
||||
"""Convert IIR offset (negative setpoint) from units of full scale to
|
||||
machine units (see :meth:`set_dds_mu`, :meth:`set_dds_offset_mu`).
|
||||
|
||||
For positive ADC voltages as setpoints, this should be negative. Due to
|
||||
rounding and representation as two's complement, ``offset=1`` can not
|
||||
be represented while ``offset=-1`` can.
|
||||
"""
|
||||
return int(round(offset * (1 << COEFF_WIDTH - 1)))
|
||||
|
||||
@kernel
|
||||
def set_iir_mu(self, profile, adc, a1, b0, b1, dly=0):
|
||||
"""Set profile IIR coefficients in machine units.
|
||||
|
||||
The recurrence relation is (all data signed and MSB aligned):
|
||||
|
||||
.. math::
|
||||
a_0 y_n = a_1 y_{n - 1} + b_0 (x_n + o)/2 + b_1 (x_{n - 1} + o)/2
|
||||
|
||||
Where:
|
||||
|
||||
* :math:`y_n` and :math:`y_{n-1}` are the current and previous
|
||||
filter outputs, clipped to :math:`[0, 1[`.
|
||||
* :math:`x_n` and :math:`x_{n-1}` are the current and previous
|
||||
filter inputs in :math:`[-1, 1[`.
|
||||
* :math:`o` is the offset
|
||||
* :math:`a_0` is the normalization factor :math:`2^{11}`
|
||||
* :math:`a_1` is the feedback gain
|
||||
* :math:`b_0` and :math:`b_1` are the feedforward gains for the two
|
||||
delays
|
||||
|
||||
.. seealso:: :meth:`set_iir`
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param adc: ADC channel to take IIR input from (0-7)
|
||||
:param a1: 18 bit signed A1 coefficient (Y1 coefficient,
|
||||
feedback, integrator gain)
|
||||
:param b0: 18 bit signed B0 coefficient (recent,
|
||||
X0 coefficient, feed forward, proportional gain)
|
||||
:param b1: 18 bit signed B1 coefficient (old,
|
||||
X1 coefficient, feed forward, proportional gain)
|
||||
:param dly: IIR update suppression time. In units of IIR cycles
|
||||
(~1.2 µs, 0-255).
|
||||
"""
|
||||
base = (self.servo_channel << 8) | (profile << 3)
|
||||
self.servo.write(base + 3, adc | (dly << 8))
|
||||
self.servo.write(base + 1, b1)
|
||||
self.servo.write(base + 5, a1)
|
||||
self.servo.write(base + 7, b0)
|
||||
|
||||
@kernel
|
||||
def set_iir(self, profile, adc, kp, ki=0., g=0., delay=0.):
|
||||
"""Set profile IIR coefficients.
|
||||
|
||||
This method advances the timeline by four servo memory accesses.
|
||||
Profile parameter changes are not synchronized. Activate a different
|
||||
profile or stop the servo to ensure synchronous changes.
|
||||
|
||||
Gains are given in units of output full per scale per input full scale.
|
||||
|
||||
The transfer function is (up to time discretization and
|
||||
coefficient quantization errors):
|
||||
|
||||
.. math::
|
||||
H(s) = k_p + \\frac{k_i}{s + \\frac{k_i}{g}}
|
||||
|
||||
Where:
|
||||
* :math:`s = \\sigma + i\\omega` is the complex frequency
|
||||
* :math:`k_p` is the proportional gain
|
||||
* :math:`k_i` is the integrator gain
|
||||
* :math:`g` is the integrator gain limit
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param adc: ADC channel to take IIR input from (0-7)
|
||||
:param kp: Proportional gain (1). This is usually negative (closed
|
||||
loop, positive ADC voltage, positive setpoint). When 0, this
|
||||
implements a pure I controller.
|
||||
:param ki: Integrator gain (rad/s). When 0 (the default)
|
||||
this implements a pure P controller. Same sign as ``kp``.
|
||||
:param g: Integrator gain limit (1). When 0 (the default) the
|
||||
integrator gain limit is infinite. Same sign as ``ki``.
|
||||
:param delay: Delay (in seconds, 0-300 µs) before allowing IIR updates
|
||||
after invoking :meth:`set`. This is rounded to the nearest number
|
||||
of servo cycles (~1.2 µs). Since the RF switch (:meth:`set`) can be
|
||||
opened at any time relative to the servo cycle, the first DDS
|
||||
update that carries updated IIR data will occur approximately
|
||||
between ``delay + 1 cycle`` and ``delay + 2 cycles`` after
|
||||
:meth:`set`.
|
||||
"""
|
||||
B_NORM = 1 << COEFF_SHIFT + 1
|
||||
A_NORM = 1 << COEFF_SHIFT
|
||||
COEFF_MAX = 1 << COEFF_WIDTH - 1
|
||||
|
||||
kp *= B_NORM
|
||||
if ki == 0.:
|
||||
# pure P
|
||||
a1 = 0
|
||||
b1 = 0
|
||||
b0 = int(round(kp))
|
||||
else:
|
||||
# I or PI
|
||||
ki *= B_NORM*T_CYCLE/2.
|
||||
if g == 0.:
|
||||
c = 1.
|
||||
a1 = A_NORM
|
||||
else:
|
||||
c = 1./(1. + ki/(g*B_NORM))
|
||||
a1 = int(round((2.*c - 1.)*A_NORM))
|
||||
b0 = int(round(kp + ki*c))
|
||||
b1 = int(round(kp + (ki - 2.*kp)*c))
|
||||
if b1 == -b0:
|
||||
raise ValueError("low integrator gain and/or gain limit")
|
||||
|
||||
if (b0 >= COEFF_MAX or b0 < -COEFF_MAX or
|
||||
b1 >= COEFF_MAX or b1 < -COEFF_MAX):
|
||||
raise ValueError("high gains")
|
||||
|
||||
dly = int(round(delay/T_CYCLE))
|
||||
self.set_iir_mu(profile, adc, a1, b0, b1, dly)
|
||||
|
||||
@kernel
|
||||
def get_profile_mu(self, profile, data):
|
||||
"""Retrieve profile data.
|
||||
|
||||
Profile data is returned in the ``data`` argument in machine units
|
||||
packed as: ``[ftw >> 16, b1, pow, adc | (delay << 8), offset, a1,
|
||||
ftw & 0xffff, b0]``.
|
||||
|
||||
.. seealso:: The individual fields are described in
|
||||
:meth:`set_iir_mu` and :meth:`set_dds_mu`.
|
||||
|
||||
This method advances the timeline by 32 µs and consumes all slack.
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param data: List of 8 integers to write the profile data into
|
||||
"""
|
||||
base = (self.servo_channel << 8) | (profile << 3)
|
||||
for i in range(len(data)):
|
||||
data[i] = self.servo.read(base + i)
|
||||
delay(4*us)
|
||||
|
||||
@kernel
|
||||
def get_y_mu(self, profile):
|
||||
"""Get a profile's IIR state (filter output, Y0) in machine units.
|
||||
|
||||
The IIR state is also know as the "integrator", or the DDS amplitude
|
||||
scale factor. It is 17 bits wide and unsigned.
|
||||
|
||||
This method does not advance the timeline but consumes all slack.
|
||||
|
||||
If reading servo state through this method collides with the servo
|
||||
writing that same data, the data can become invalid. To ensure
|
||||
consistent and valid data, stop the servo before using this method.
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:return: 17 bit unsigned Y0
|
||||
"""
|
||||
return self.servo.read(STATE_SEL | (self.servo_channel << 5) | profile)
|
||||
|
||||
@kernel
|
||||
def get_y(self, profile):
|
||||
"""Get a profile's IIR state (filter output, Y0).
|
||||
|
||||
The IIR state is also know as the "integrator", or the DDS amplitude
|
||||
scale factor. It is 17 bits wide and unsigned.
|
||||
|
||||
This method does not advance the timeline but consumes all slack.
|
||||
|
||||
If reading servo state through this method collides with the servo
|
||||
writing that same data, the data can become invalid. To ensure
|
||||
consistent and valid data, stop the servo before using this method.
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:return: IIR filter output in Y0 units of full scale
|
||||
"""
|
||||
return y_mu_to_full_scale(self.get_y_mu(profile))
|
||||
|
||||
@kernel
|
||||
def set_y_mu(self, profile, y):
|
||||
"""Set a profile's IIR state (filter output, Y0) in machine units.
|
||||
|
||||
The IIR state is also know as the "integrator", or the DDS amplitude
|
||||
scale factor. It is 17 bits wide and unsigned.
|
||||
|
||||
This method must not be used when the servo could be writing to the
|
||||
same location. Either deactivate the profile, or deactivate IIR
|
||||
updates, or disable servo iterations.
|
||||
|
||||
This method advances the timeline by one servo memory access.
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param y: 17 bit unsigned Y0
|
||||
"""
|
||||
# State memory is 25 bits wide and signed.
|
||||
# Reads interact with the 18 MSBs (coefficient memory width)
|
||||
self.servo.write(STATE_SEL | (self.servo_channel << 5) | profile, y)
|
||||
|
||||
@kernel
|
||||
def set_y(self, profile, y):
|
||||
"""Set a profile's IIR state (filter output, Y0).
|
||||
|
||||
The IIR state is also know as the "integrator", or the DDS amplitude
|
||||
scale factor. It is 17 bits wide and unsigned.
|
||||
|
||||
This method must not be used when the servo could be writing to the
|
||||
same location. Either deactivate the profile, or deactivate IIR
|
||||
updates, or disable servo iterations.
|
||||
|
||||
This method advances the timeline by one servo memory access.
|
||||
|
||||
:param profile: Profile number (0-31)
|
||||
:param y: IIR state in units of full scale
|
||||
"""
|
||||
y_mu = int(round(y * Y_FULL_SCALE_MU))
|
||||
if y_mu < 0 or y_mu > (1 << 17) - 1:
|
||||
raise ValueError("Invalid SUServo y-value!")
|
||||
self.set_y_mu(profile, y_mu)
|
||||
return y_mu
|
|
@ -0,0 +1,133 @@
|
|||
class TRF372017:
|
||||
"""TRF372017 settings and register map.
|
||||
|
||||
For possible values, documentation, and explanation, see the datasheet.
|
||||
https://www.ti.com/lit/gpn/trf372017
|
||||
"""
|
||||
rdiv = 21 # 13b
|
||||
ref_inv = 0
|
||||
neg_vco = 1
|
||||
icp = 0 # 1.94 mA, 5b
|
||||
icp_double = 0
|
||||
cal_clk_sel = 12 # /16, 4b
|
||||
|
||||
ndiv = 420 # 16b
|
||||
pll_div_sel = 0 # /1, 2b
|
||||
prsc_sel = 1 # 8/9
|
||||
vco_sel = 2 # 2b
|
||||
vcosel_mode = 0
|
||||
cal_acc = 0b00 # 2b
|
||||
en_cal = 1
|
||||
|
||||
nfrac = 0 # 25b
|
||||
|
||||
pwd_pll = 0
|
||||
pwd_cp = 0
|
||||
pwd_vco = 0
|
||||
pwd_vcomux = 0
|
||||
pwd_div124 = 0
|
||||
pwd_presc = 0
|
||||
pwd_out_buff = 1
|
||||
pwd_lo_div = 1
|
||||
pwd_tx_div = 0
|
||||
pwd_bb_vcm = 0
|
||||
pwd_dc_off = 0
|
||||
en_extvco = 0
|
||||
en_isource = 0
|
||||
ld_ana_prec = 0 # 2b
|
||||
cp_tristate = 0 # 2b
|
||||
speedup = 0
|
||||
ld_dig_prec = 1
|
||||
en_dith = 1
|
||||
mod_ord = 2 # 3rd order, 2b
|
||||
dith_sel = 0
|
||||
del_sd_clk = 2 # 2b
|
||||
en_frac = 0
|
||||
|
||||
vcobias_rtrim = 4 # 3b
|
||||
pllbias_rtrim = 2 # 2b
|
||||
vco_bias = 8 # 460 µA, 4b
|
||||
vcobuf_bias = 2 # 2b
|
||||
vcomux_bias = 3 # 2b
|
||||
bufout_bias = 0 # 300 µA, 2b
|
||||
vco_cal_ib = 0 # PTAT
|
||||
vco_cal_ref = 2 # 1.04 V, 2b
|
||||
vco_ampl_ctrl = 3 # 2b
|
||||
vco_vb_ctrl = 0 # 1.2 V, 2b
|
||||
en_ld_isource = 0
|
||||
|
||||
ioff = 0x80 # 8b
|
||||
qoff = 0x80 # 8b
|
||||
vref_sel = 4 # 0.85 V, 3b
|
||||
tx_div_sel = 1 # div2, 2b
|
||||
lo_div_sel = 3 # div8, 2b
|
||||
tx_div_bias = 1 # 37.5 µA, 2b
|
||||
lo_div_bias = 2 # 50 µA, 2b
|
||||
|
||||
vco_trim = 0x20 # 6b
|
||||
vco_test_mode = 0
|
||||
cal_bypass = 0
|
||||
mux_ctrl = 1 # lock detect, 3b
|
||||
isource_sink = 0
|
||||
isource_trim = 4 # 3b
|
||||
pd_tc = 0 # 2b
|
||||
ib_vcm_sel = 0 # ptat
|
||||
dcoffset_i = 2 # 150 µA, 2b
|
||||
vco_bias_sel = 1 # spi
|
||||
|
||||
def __init__(self, updates=None):
|
||||
if updates is None:
|
||||
return
|
||||
for key, value in updates.items():
|
||||
if not hasattr(self, key):
|
||||
raise KeyError("invalid setting", key)
|
||||
setattr(self, key, value)
|
||||
|
||||
def get_mmap(self):
|
||||
mmap = []
|
||||
mmap.append(
|
||||
0x9 |
|
||||
(self.rdiv << 5) | (self.ref_inv << 19) | (self.neg_vco << 20) |
|
||||
(self.icp << 21) | (self.icp_double << 26) |
|
||||
(self.cal_clk_sel << 27))
|
||||
mmap.append(
|
||||
0xa |
|
||||
(self.ndiv << 5) | (self.pll_div_sel << 21) | (self.prsc_sel << 23) |
|
||||
(self.vco_sel << 26) | (self.vcosel_mode << 28) |
|
||||
(self.cal_acc << 29) | (self.en_cal << 31))
|
||||
mmap.append(0xb | (self.nfrac << 5))
|
||||
mmap.append(
|
||||
0xc |
|
||||
(self.pwd_pll << 5) | (self.pwd_cp << 6) | (self.pwd_vco << 7) |
|
||||
(self.pwd_vcomux << 8) | (self.pwd_div124 << 9) |
|
||||
(self.pwd_presc << 10) | (self.pwd_out_buff << 12) |
|
||||
(self.pwd_lo_div << 13) | (self.pwd_tx_div << 14) |
|
||||
(self.pwd_bb_vcm << 15) | (self.pwd_dc_off << 16) |
|
||||
(self.en_extvco << 17) | (self.en_isource << 18) |
|
||||
(self.ld_ana_prec << 19) | (self.cp_tristate << 21) |
|
||||
(self.speedup << 23) | (self.ld_dig_prec << 24) |
|
||||
(self.en_dith << 25) | (self.mod_ord << 26) |
|
||||
(self.dith_sel << 28) | (self.del_sd_clk << 29) |
|
||||
(self.en_frac << 31))
|
||||
mmap.append(
|
||||
0xd |
|
||||
(self.vcobias_rtrim << 5) | (self.pllbias_rtrim << 8) |
|
||||
(self.vco_bias << 10) | (self.vcobuf_bias << 14) |
|
||||
(self.vcomux_bias << 16) | (self.bufout_bias << 18) |
|
||||
(1 << 21) | (self.vco_cal_ib << 22) | (self.vco_cal_ref << 23) |
|
||||
(self.vco_ampl_ctrl << 26) | (self.vco_vb_ctrl << 28) |
|
||||
(self.en_ld_isource << 31))
|
||||
mmap.append(
|
||||
0xe |
|
||||
(self.ioff << 5) | (self.qoff << 13) | (self.vref_sel << 21) |
|
||||
(self.tx_div_sel << 24) | (self.lo_div_sel << 26) |
|
||||
(self.tx_div_bias << 28) | (self.lo_div_bias << 30))
|
||||
mmap.append(
|
||||
0xf |
|
||||
(self.vco_trim << 7) | (self.vco_test_mode << 14) |
|
||||
(self.cal_bypass << 15) | (self.mux_ctrl << 16) |
|
||||
(self.isource_sink << 19) | (self.isource_trim << 20) |
|
||||
(self.pd_tc << 23) | (self.ib_vcm_sel << 25) |
|
||||
(1 << 28) | (self.dcoffset_i << 29) |
|
||||
(self.vco_bias_sel << 31))
|
||||
return mmap
|
|
@ -2,8 +2,8 @@
|
|||
Drivers for TTL signals on RTIO.
|
||||
|
||||
TTL channels (including the clock generator) all support output event
|
||||
replacement. For example, pulses of "zero" length (e.g. ``on()``
|
||||
immediately followed by ``off()``, without a delay) are suppressed.
|
||||
replacement. For example, pulses of "zero" length (e.g. :meth:`TTLInOut.on`
|
||||
immediately followed by :meth:`TTLInOut.off`, without a delay) are suppressed.
|
||||
"""
|
||||
|
||||
import numpy
|
||||
|
@ -29,14 +29,12 @@ class TTLOut:
|
|||
|
||||
:param channel: channel number
|
||||
"""
|
||||
kernel_invariants = {"core", "channel"}
|
||||
kernel_invariants = {"core", "channel", "target_o"}
|
||||
|
||||
def __init__(self, dmgr, channel, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.channel = channel
|
||||
|
||||
# in RTIO cycles
|
||||
self.o_previous_timestamp = numpy.int64(0)
|
||||
self.target_o = channel << 8
|
||||
|
||||
@kernel
|
||||
def output(self):
|
||||
|
@ -44,19 +42,11 @@ class TTLOut:
|
|||
|
||||
@kernel
|
||||
def set_o(self, o):
|
||||
rtio_output(now_mu(), self.channel, 0, 1 if o else 0)
|
||||
self.o_previous_timestamp = now_mu()
|
||||
|
||||
@kernel
|
||||
def sync(self):
|
||||
"""Busy-wait until all programmed level switches have been
|
||||
effected."""
|
||||
while self.core.get_rtio_counter_mu() < self.o_previous_timestamp:
|
||||
pass
|
||||
rtio_output(self.target_o, 1 if o else 0)
|
||||
|
||||
@kernel
|
||||
def on(self):
|
||||
"""Sets the output to a logic high state at the current position
|
||||
"""Set the output to a logic high state at the current position
|
||||
of the time cursor.
|
||||
|
||||
The time cursor is not modified by this function."""
|
||||
|
@ -107,8 +97,8 @@ class TTLInOut:
|
|||
This should be used with bidirectional channels.
|
||||
|
||||
Note that the channel is in input mode by default. If you need to drive a
|
||||
signal, you must call ``output``. If the channel is in output mode most of
|
||||
the time in your setup, it is a good idea to call ``output`` in the
|
||||
signal, you must call :meth:`output`. If the channel is in output mode most of
|
||||
the time in your setup, it is a good idea to call :meth:`output` in the
|
||||
startup kernel.
|
||||
|
||||
There are three input APIs: gating, sampling and watching. When one
|
||||
|
@ -117,20 +107,30 @@ class TTLInOut:
|
|||
|
||||
:param channel: channel number
|
||||
"""
|
||||
kernel_invariants = {"core", "channel"}
|
||||
kernel_invariants = {"core", "channel", "gate_latency_mu",
|
||||
"target_o", "target_oe", "target_sens", "target_sample"}
|
||||
|
||||
def __init__(self, dmgr, channel, core_device="core"):
|
||||
def __init__(self, dmgr, channel, gate_latency_mu=None,
|
||||
core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.channel = channel
|
||||
|
||||
# in RTIO cycles
|
||||
self.o_previous_timestamp = numpy.int64(0)
|
||||
self.i_previous_timestamp = numpy.int64(0)
|
||||
self.queued_samples = 0
|
||||
# With TTLs inputs, the gate control is connected to a high-latency
|
||||
# path through SED. When looking at the RTIO counter to determine if
|
||||
# the gate has closed, we need to take this latency into account.
|
||||
# See: https://github.com/m-labs/artiq/issues/1137
|
||||
if gate_latency_mu is None:
|
||||
gate_latency_mu = 13*self.core.ref_multiplier
|
||||
self.gate_latency_mu = gate_latency_mu
|
||||
|
||||
self.target_o = (channel << 8) + 0
|
||||
self.target_oe = (channel << 8) + 1
|
||||
self.target_sens = (channel << 8) + 2
|
||||
self.target_sample = (channel << 8) + 3
|
||||
|
||||
@kernel
|
||||
def set_oe(self, oe):
|
||||
rtio_output(now_mu(), self.channel, 1, 1 if oe else 0)
|
||||
rtio_output(self.target_oe, 1 if oe else 0)
|
||||
|
||||
@kernel
|
||||
def output(self):
|
||||
|
@ -138,7 +138,11 @@ class TTLInOut:
|
|||
cursor.
|
||||
|
||||
There must be a delay of at least one RTIO clock cycle before any
|
||||
other command can be issued."""
|
||||
other command can be issued.
|
||||
|
||||
This method only configures the direction at the FPGA. When using
|
||||
buffered I/O interfaces, such as the Sinara TTL cards, the buffer
|
||||
direction must be configured separately in the hardware."""
|
||||
self.set_oe(True)
|
||||
|
||||
@kernel
|
||||
|
@ -147,20 +151,16 @@ class TTLInOut:
|
|||
cursor.
|
||||
|
||||
There must be a delay of at least one RTIO clock cycle before any
|
||||
other command can be issued."""
|
||||
other command can be issued.
|
||||
|
||||
This method only configures the direction at the FPGA. When using
|
||||
buffered I/O interfaces, such as the Sinara TTL cards, the buffer
|
||||
direction must be configured separately in the hardware."""
|
||||
self.set_oe(False)
|
||||
|
||||
@kernel
|
||||
def set_o(self, o):
|
||||
rtio_output(now_mu(), self.channel, 0, 1 if o else 0)
|
||||
self.o_previous_timestamp = now_mu()
|
||||
|
||||
@kernel
|
||||
def sync(self):
|
||||
"""Busy-wait until all programmed level switches have been
|
||||
effected."""
|
||||
while self.core.get_rtio_counter_mu() < self.o_previous_timestamp:
|
||||
pass
|
||||
rtio_output(self.target_o, 1 if o else 0)
|
||||
|
||||
@kernel
|
||||
def on(self):
|
||||
|
@ -184,7 +184,7 @@ class TTLInOut:
|
|||
|
||||
@kernel
|
||||
def pulse_mu(self, duration):
|
||||
"""Pulses the output high for the specified duration
|
||||
"""Pulse the output high for the specified duration
|
||||
(in machine units).
|
||||
|
||||
The time cursor is advanced by the specified duration."""
|
||||
|
@ -194,7 +194,7 @@ class TTLInOut:
|
|||
|
||||
@kernel
|
||||
def pulse(self, duration):
|
||||
"""Pulses the output high for the specified duration
|
||||
"""Pulse the output high for the specified duration
|
||||
(in seconds).
|
||||
|
||||
The time cursor is advanced by the specified duration."""
|
||||
|
@ -205,89 +205,172 @@ class TTLInOut:
|
|||
# Input API: gating
|
||||
@kernel
|
||||
def _set_sensitivity(self, value):
|
||||
rtio_output(now_mu(), self.channel, 2, value)
|
||||
self.i_previous_timestamp = now_mu()
|
||||
rtio_output(self.target_sens, value)
|
||||
|
||||
@kernel
|
||||
def gate_rising_mu(self, duration):
|
||||
"""Register rising edge events for the specified duration
|
||||
(in machine units).
|
||||
|
||||
The time cursor is advanced by the specified duration."""
|
||||
The time cursor is advanced by the specified duration.
|
||||
|
||||
:return: The timeline cursor at the end of the gate window, for
|
||||
convenience when used with :meth:`count`/:meth:`timestamp_mu`.
|
||||
"""
|
||||
self._set_sensitivity(1)
|
||||
delay_mu(duration)
|
||||
self._set_sensitivity(0)
|
||||
return now_mu()
|
||||
|
||||
@kernel
|
||||
def gate_falling_mu(self, duration):
|
||||
"""Register falling edge events for the specified duration
|
||||
(in machine units).
|
||||
|
||||
The time cursor is advanced by the specified duration."""
|
||||
The time cursor is advanced by the specified duration.
|
||||
|
||||
:return: The timeline cursor at the end of the gate window, for
|
||||
convenience when used with :meth:`count`/:meth:`timestamp_mu`.
|
||||
"""
|
||||
self._set_sensitivity(2)
|
||||
delay_mu(duration)
|
||||
self._set_sensitivity(0)
|
||||
return now_mu()
|
||||
|
||||
@kernel
|
||||
def gate_both_mu(self, duration):
|
||||
"""Register both rising and falling edge events for the specified
|
||||
duration (in machine units).
|
||||
|
||||
The time cursor is advanced by the specified duration."""
|
||||
The time cursor is advanced by the specified duration.
|
||||
|
||||
:return: The timeline cursor at the end of the gate window, for
|
||||
convenience when used with :meth:`count`/:meth:`timestamp_mu`.
|
||||
"""
|
||||
self._set_sensitivity(3)
|
||||
delay_mu(duration)
|
||||
self._set_sensitivity(0)
|
||||
return now_mu()
|
||||
|
||||
@kernel
|
||||
def gate_rising(self, duration):
|
||||
"""Register rising edge events for the specified duration
|
||||
(in seconds).
|
||||
|
||||
The time cursor is advanced by the specified duration."""
|
||||
The time cursor is advanced by the specified duration.
|
||||
|
||||
:return: The timeline cursor at the end of the gate window, for
|
||||
convenience when used with :meth:`count`/:meth:`timestamp_mu`.
|
||||
"""
|
||||
self._set_sensitivity(1)
|
||||
delay(duration)
|
||||
self._set_sensitivity(0)
|
||||
return now_mu()
|
||||
|
||||
@kernel
|
||||
def gate_falling(self, duration):
|
||||
"""Register falling edge events for the specified duration
|
||||
(in seconds).
|
||||
|
||||
The time cursor is advanced by the specified duration."""
|
||||
The time cursor is advanced by the specified duration.
|
||||
|
||||
:return: The timeline cursor at the end of the gate window, for
|
||||
convenience when used with :meth:`count`/:meth:`timestamp_mu`.
|
||||
|
||||
"""
|
||||
self._set_sensitivity(2)
|
||||
delay(duration)
|
||||
self._set_sensitivity(0)
|
||||
return now_mu()
|
||||
|
||||
@kernel
|
||||
def gate_both(self, duration):
|
||||
"""Register both rising and falling edge events for the specified
|
||||
duration (in seconds).
|
||||
|
||||
The time cursor is advanced by the specified duration."""
|
||||
The time cursor is advanced by the specified duration.
|
||||
|
||||
:return: The timeline cursor at the end of the gate window, for
|
||||
convenience when used with :meth:`count`/:meth:`timestamp_mu`.
|
||||
"""
|
||||
self._set_sensitivity(3)
|
||||
delay(duration)
|
||||
self._set_sensitivity(0)
|
||||
return now_mu()
|
||||
|
||||
@kernel
|
||||
def count(self):
|
||||
"""Poll the RTIO input during all the previously programmed gate
|
||||
openings, and returns the number of registered events.
|
||||
def count(self, up_to_timestamp_mu):
|
||||
"""Consume RTIO input events until the hardware timestamp counter has
|
||||
reached the specified timestamp and return the number of observed
|
||||
events.
|
||||
|
||||
This function does not interact with the time cursor."""
|
||||
This function does not interact with the timeline cursor.
|
||||
|
||||
See the ``gate_*()`` family of methods to select the input transitions
|
||||
that generate events, and :meth:`timestamp_mu` to obtain the timestamp
|
||||
of the first event rather than an accumulated count.
|
||||
|
||||
:param up_to_timestamp_mu: The timestamp up to which execution is
|
||||
blocked, that is, up to which input events are guaranteed to be
|
||||
taken into account. (Events with later timestamps might still be
|
||||
registered if they are already available.)
|
||||
|
||||
:return: The number of events before the timeout elapsed (0 if none
|
||||
observed).
|
||||
|
||||
Examples:
|
||||
To count events on channel ``ttl_input``, up to the current timeline
|
||||
position::
|
||||
|
||||
ttl_input.count(now_mu())
|
||||
|
||||
If other events are scheduled between the end of the input gate
|
||||
period and when the number of events is counted, using ``now_mu()``
|
||||
as timeout consumes an unnecessary amount of timeline slack. In
|
||||
such cases, it can be beneficial to pass a more precise timestamp,
|
||||
for example::
|
||||
|
||||
gate_end_mu = ttl_input.gate_rising(100 * us)
|
||||
|
||||
# Schedule a long pulse sequence, represented here by a delay.
|
||||
delay(10 * ms)
|
||||
|
||||
# Get number of rising edges. This will block until the end of
|
||||
# the gate window, but does not wait for the long pulse sequence
|
||||
# afterwards, thus (likely) completing with a large amount of
|
||||
# slack left.
|
||||
num_rising_edges = ttl_input.count(gate_end_mu)
|
||||
|
||||
The ``gate_*()`` family of methods return the cursor at the end
|
||||
of the window, allowing this to be expressed in a compact fashion::
|
||||
|
||||
ttl_input.count(ttl_input.gate_rising(100 * us))
|
||||
"""
|
||||
count = 0
|
||||
while rtio_input_timestamp(self.i_previous_timestamp, self.channel) >= 0:
|
||||
while rtio_input_timestamp(up_to_timestamp_mu + self.gate_latency_mu, self.channel) >= 0:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@kernel
|
||||
def timestamp_mu(self):
|
||||
"""Poll the RTIO input and returns an event timestamp (in machine
|
||||
units), according to the gating.
|
||||
def timestamp_mu(self, up_to_timestamp_mu):
|
||||
"""Return the timestamp of the next RTIO input event, or -1 if the
|
||||
hardware timestamp counter reaches the given value before an event is
|
||||
received.
|
||||
|
||||
If the gate is permanently closed, returns a negative value.
|
||||
This function does not interact with the timeline cursor.
|
||||
|
||||
This function does not interact with the time cursor."""
|
||||
return rtio_input_timestamp(self.i_previous_timestamp, self.channel)
|
||||
See the ``gate_*()`` family of methods to select the input transitions
|
||||
that generate events, and :meth:`count` for usage examples.
|
||||
|
||||
:param up_to_timestamp_mu: The timestamp up to which execution is
|
||||
blocked, that is, up to which input events are guaranteed to be
|
||||
taken into account. (Events with later timestamps might still be
|
||||
registered if they are already available.)
|
||||
|
||||
:return: The timestamp (in machine units) of the first event received;
|
||||
-1 on timeout.
|
||||
"""
|
||||
return rtio_input_timestamp(up_to_timestamp_mu + self.gate_latency_mu, self.channel)
|
||||
|
||||
# Input API: sampling
|
||||
@kernel
|
||||
|
@ -296,15 +379,15 @@ class TTLInOut:
|
|||
position of the time cursor.
|
||||
|
||||
The time cursor is not modified by this function."""
|
||||
rtio_output(now_mu(), self.channel, 3, 0)
|
||||
rtio_output(self.target_sample, 0)
|
||||
|
||||
@kernel
|
||||
def sample_get(self):
|
||||
"""Returns the value of a sample previously obtained with
|
||||
``sample_input``.
|
||||
:meth:`sample_input`.
|
||||
|
||||
Multiple samples may be queued (using multiple calls to
|
||||
``sample_input``) into the RTIO FIFOs and subsequently read out using
|
||||
:meth:`sample_input`) into the RTIO FIFOs and subsequently read out using
|
||||
multiple calls to this function.
|
||||
|
||||
This function does not interact with the time cursor."""
|
||||
|
@ -324,22 +407,22 @@ class TTLInOut:
|
|||
@kernel
|
||||
def watch_stay_on(self):
|
||||
"""Checks that the input is at a high level at the position
|
||||
of the time cursor and keep checking until ``watch_done``
|
||||
of the time cursor and keep checking until :meth:`watch_done`
|
||||
is called.
|
||||
|
||||
Returns ``True`` if the input is high. A call to this function
|
||||
must always be followed by an eventual call to ``watch_done``
|
||||
must always be followed by an eventual call to :meth:`watch_done`
|
||||
(use e.g. a try/finally construct to ensure this).
|
||||
|
||||
The time cursor is not modified by this function.
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, 3, 2) # gate falling
|
||||
rtio_output(self.target_sample, 2) # gate falling
|
||||
return rtio_input_data(self.channel) == 1
|
||||
|
||||
@kernel
|
||||
def watch_stay_off(self):
|
||||
"""Like ``watch_stay_on``, but for low levels."""
|
||||
rtio_output(now_mu(), self.channel, 3, 1) # gate rising
|
||||
"""Like :meth:`watch_stay_on`, but for low levels."""
|
||||
rtio_output(self.target_sample, 1) # gate rising
|
||||
return rtio_input_data(self.channel) == 0
|
||||
|
||||
@kernel
|
||||
|
@ -352,10 +435,10 @@ class TTLInOut:
|
|||
The time cursor is not modified by this function. This function
|
||||
always makes the slack negative.
|
||||
"""
|
||||
rtio_output(now_mu(), self.channel, 2, 0)
|
||||
rtio_output(self.target_sens, 0)
|
||||
success = True
|
||||
try:
|
||||
while rtio_input_timestamp(now_mu(), self.channel) != -1:
|
||||
while rtio_input_timestamp(now_mu() + self.gate_latency_mu, self.channel) != -1:
|
||||
success = False
|
||||
except RTIOOverflow:
|
||||
success = False
|
||||
|
@ -371,16 +454,16 @@ class TTLClockGen:
|
|||
The time cursor is not modified by any function in this class.
|
||||
|
||||
:param channel: channel number
|
||||
:param acc_width: accumulator width in bits
|
||||
"""
|
||||
kernel_invariants = {"core", "channel", "acc_width"}
|
||||
kernel_invariants = {"core", "channel", "target", "acc_width"}
|
||||
|
||||
def __init__(self, dmgr, channel, core_device="core"):
|
||||
def __init__(self, dmgr, channel, acc_width=24, core_device="core"):
|
||||
self.core = dmgr.get(core_device)
|
||||
self.channel = channel
|
||||
self.target = channel << 8
|
||||
|
||||
# in RTIO cycles
|
||||
self.previous_timestamp = numpy.int64(0)
|
||||
self.acc_width = numpy.int64(24)
|
||||
self.acc_width = numpy.int64(acc_width)
|
||||
|
||||
@portable
|
||||
def frequency_to_ftw(self, frequency):
|
||||
|
@ -414,22 +497,14 @@ class TTLClockGen:
|
|||
Due to the way the clock generator operates, frequency tuning words
|
||||
that are not powers of two cause jitter of one RTIO clock cycle at the
|
||||
output."""
|
||||
rtio_output(now_mu(), self.channel, 0, frequency)
|
||||
self.previous_timestamp = now_mu()
|
||||
rtio_output(self.target, frequency)
|
||||
|
||||
@kernel
|
||||
def set(self, frequency):
|
||||
"""Like ``set_mu``, but using Hz."""
|
||||
"""Like :meth:`set_mu`, but using Hz."""
|
||||
self.set_mu(self.frequency_to_ftw(frequency))
|
||||
|
||||
@kernel
|
||||
def stop(self):
|
||||
"""Stop the toggling of the clock and set the output level to 0."""
|
||||
self.set_mu(0)
|
||||
|
||||
@kernel
|
||||
def sync(self):
|
||||
"""Busy-wait until all programmed frequency switches and stops have
|
||||
been effected."""
|
||||
while self.core.get_rtio_counter_mu() < self.o_previous_timestamp:
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,378 @@
|
|||
from artiq.language.core import kernel, delay, portable, at_mu, now_mu
|
||||
from artiq.language.units import us, ms
|
||||
|
||||
from numpy import int32, int64
|
||||
|
||||
from artiq.coredevice import spi2 as spi
|
||||
|
||||
|
||||
SPI_CONFIG = (0*spi.SPI_OFFLINE | 0*spi.SPI_END |
|
||||
0*spi.SPI_INPUT | 1*spi.SPI_CS_POLARITY |
|
||||
0*spi.SPI_CLK_POLARITY | 0*spi.SPI_CLK_PHASE |
|
||||
0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX)
|
||||
|
||||
# SPI clock write and read dividers
|
||||
SPIT_CFG_WR = 2
|
||||
SPIT_CFG_RD = 16
|
||||
# 30 MHz fmax, 20 ns setup, 40 ns shift to latch (limiting)
|
||||
SPIT_ATT_WR = 6
|
||||
SPIT_ATT_RD = 16
|
||||
SPIT_DDS_WR = 2
|
||||
SPIT_DDS_RD = 16
|
||||
|
||||
# CFG configuration register bit offsets
|
||||
CFG_RF_SW = 0
|
||||
CFG_LED = 4
|
||||
CFG_PROFILE = 8
|
||||
CFG_IO_UPDATE = 12
|
||||
CFG_MASK_NU = 13
|
||||
CFG_CLK_SEL0 = 17
|
||||
CFG_CLK_SEL1 = 21
|
||||
CFG_SYNC_SEL = 18
|
||||
CFG_RST = 19
|
||||
CFG_IO_RST = 20
|
||||
CFG_CLK_DIV = 22
|
||||
|
||||
# STA status register bit offsets
|
||||
STA_RF_SW = 0
|
||||
STA_SMP_ERR = 4
|
||||
STA_PLL_LOCK = 8
|
||||
STA_IFC_MODE = 12
|
||||
STA_PROTO_REV = 16
|
||||
|
||||
# supported hardware and CPLD code version
|
||||
STA_PROTO_REV_MATCH = 0x08
|
||||
|
||||
# chip select (decoded)
|
||||
CS_CFG = 1
|
||||
CS_ATT = 2
|
||||
CS_DDS_MULTI = 3
|
||||
CS_DDS_CH0 = 4
|
||||
CS_DDS_CH1 = 5
|
||||
CS_DDS_CH2 = 6
|
||||
CS_DDS_CH3 = 7
|
||||
|
||||
|
||||
@portable
|
||||
def urukul_cfg(rf_sw, led, profile, io_update, mask_nu,
|
||||
clk_sel, sync_sel, rst, io_rst, clk_div):
|
||||
"""Build Urukul CPLD configuration register"""
|
||||
return ((rf_sw << CFG_RF_SW) |
|
||||
(led << CFG_LED) |
|
||||
(profile << CFG_PROFILE) |
|
||||
(io_update << CFG_IO_UPDATE) |
|
||||
(mask_nu << CFG_MASK_NU) |
|
||||
((clk_sel & 0x01) << CFG_CLK_SEL0) |
|
||||
((clk_sel & 0x02) << (CFG_CLK_SEL1 - 1)) |
|
||||
(sync_sel << CFG_SYNC_SEL) |
|
||||
(rst << CFG_RST) |
|
||||
(io_rst << CFG_IO_RST) |
|
||||
(clk_div << CFG_CLK_DIV))
|
||||
|
||||
|
||||
@portable
|
||||
def urukul_sta_rf_sw(sta):
|
||||
"""Return the RF switch status from Urukul status register value."""
|
||||
return (sta >> STA_RF_SW) & 0xf
|
||||
|
||||
|
||||
@portable
|
||||
def urukul_sta_smp_err(sta):
|
||||
"""Return the SMP_ERR status from Urukul status register value."""
|
||||
return (sta >> STA_SMP_ERR) & 0xf
|
||||
|
||||
|
||||
@portable
|
||||
def urukul_sta_pll_lock(sta):
|
||||
"""Return the PLL_LOCK status from Urukul status register value."""
|
||||
return (sta >> STA_PLL_LOCK) & 0xf
|
||||
|
||||
|
||||
@portable
|
||||
def urukul_sta_ifc_mode(sta):
|
||||
"""Return the IFC_MODE status from Urukul status register value."""
|
||||
return (sta >> STA_IFC_MODE) & 0xf
|
||||
|
||||
|
||||
@portable
|
||||
def urukul_sta_proto_rev(sta):
|
||||
"""Return the PROTO_REV value from Urukul status register value."""
|
||||
return (sta >> STA_PROTO_REV) & 0x7f
|
||||
|
||||
|
||||
class _RegIOUpdate:
|
||||
def __init__(self, cpld):
|
||||
self.cpld = cpld
|
||||
|
||||
@kernel
|
||||
def pulse(self, t):
|
||||
cfg = self.cpld.cfg_reg
|
||||
self.cpld.cfg_write(cfg | (1 << CFG_IO_UPDATE))
|
||||
delay(t)
|
||||
self.cpld.cfg_write(cfg)
|
||||
|
||||
|
||||
class _DummySync:
|
||||
def __init__(self, cpld):
|
||||
self.cpld = cpld
|
||||
|
||||
@kernel
|
||||
def set_mu(self, ftw):
|
||||
pass
|
||||
|
||||
|
||||
class CPLD:
|
||||
"""Urukul CPLD SPI router and configuration interface.
|
||||
|
||||
:param spi_device: SPI bus device name
|
||||
:param io_update_device: IO update RTIO TTLOut channel name
|
||||
:param dds_reset_device: DDS reset RTIO TTLOut channel name
|
||||
:param sync_device: AD9910 SYNC_IN RTIO TTLClockGen channel name
|
||||
:param refclk: Reference clock (SMA, MMCX or on-board 100 MHz oscillator)
|
||||
frequency in Hz
|
||||
:param clk_sel: Reference clock selection. For hardware revision >= 1.3
|
||||
valid options are: 0 - internal 100MHz XO; 1 - front-panel SMA; 2
|
||||
internal MMCX. For hardware revision <= v1.2 valid options are: 0 -
|
||||
either XO or MMCX dependent on component population; 1 SMA. Unsupported
|
||||
clocking options are silently ignored.
|
||||
:param clk_div: Reference clock divider. Valid options are 0: variant
|
||||
dependent default (divide-by-4 for AD9910 and divide-by-1 for AD9912);
|
||||
1: divide-by-1; 2: divide-by-2; 3: divide-by-4.
|
||||
On Urukul boards with CPLD gateware before v1.3.1 only the default
|
||||
(0, i.e. variant dependent divider) is valid.
|
||||
:param sync_sel: SYNC (multi-chip synchronisation) signal source selection.
|
||||
0 corresponds to SYNC_IN being supplied by the FPGA via the EEM
|
||||
connector. 1 corresponds to SYNC_OUT from DDS0 being distributed to the
|
||||
other chips.
|
||||
:param rf_sw: Initial CPLD RF switch register setting (default: 0x0).
|
||||
Knowledge of this state is not transferred between experiments.
|
||||
:param att: Initial attenuator setting shift register (default:
|
||||
0x00000000). See also :meth:`get_att_mu` which retrieves the hardware
|
||||
state without side effects. Knowledge of this state is not transferred
|
||||
between experiments.
|
||||
:param sync_div: SYNC_IN generator divider. The ratio between the coarse
|
||||
RTIO frequency and the SYNC_IN generator frequency (default: 2 if
|
||||
`sync_device` was specified).
|
||||
:param core_device: Core device name
|
||||
|
||||
If the clocking is incorrect (for example, setting ``clk_sel`` to the
|
||||
front panel SMA with no clock connected), then the ``init()`` method of
|
||||
the DDS channels can fail with the error message ``PLL lock timeout``.
|
||||
"""
|
||||
kernel_invariants = {"refclk", "bus", "core", "io_update", "clk_div"}
|
||||
|
||||
def __init__(self, dmgr, spi_device, io_update_device=None,
|
||||
dds_reset_device=None, sync_device=None,
|
||||
sync_sel=0, clk_sel=0, clk_div=0, rf_sw=0,
|
||||
refclk=125e6, att=0x00000000, sync_div=None,
|
||||
core_device="core"):
|
||||
|
||||
self.core = dmgr.get(core_device)
|
||||
self.refclk = refclk
|
||||
assert 0 <= clk_div <= 3
|
||||
self.clk_div = clk_div
|
||||
|
||||
self.bus = dmgr.get(spi_device)
|
||||
if io_update_device is not None:
|
||||
self.io_update = dmgr.get(io_update_device)
|
||||
else:
|
||||
self.io_update = _RegIOUpdate(self)
|
||||
if dds_reset_device is not None:
|
||||
self.dds_reset = dmgr.get(dds_reset_device)
|
||||
if sync_device is not None:
|
||||
self.sync = dmgr.get(sync_device)
|
||||
if sync_div is None:
|
||||
sync_div = 2
|
||||
else:
|
||||
self.sync = _DummySync(self)
|
||||
assert sync_div is None
|
||||
sync_div = 0
|
||||
|
||||
self.cfg_reg = urukul_cfg(rf_sw=rf_sw, led=0, profile=0,
|
||||
io_update=0, mask_nu=0, clk_sel=clk_sel,
|
||||
sync_sel=sync_sel,
|
||||
rst=0, io_rst=0, clk_div=clk_div)
|
||||
self.att_reg = int32(int64(att))
|
||||
self.sync_div = sync_div
|
||||
|
||||
@kernel
|
||||
def cfg_write(self, cfg):
|
||||
"""Write to the configuration register.
|
||||
|
||||
See :func:`urukul_cfg` for possible flags.
|
||||
|
||||
:param data: 24 bit data to be written. Will be stored at
|
||||
:attr:`cfg_reg`.
|
||||
"""
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 24,
|
||||
SPIT_CFG_WR, CS_CFG)
|
||||
self.bus.write(cfg << 8)
|
||||
self.cfg_reg = cfg
|
||||
|
||||
@kernel
|
||||
def sta_read(self):
|
||||
"""Read the status register.
|
||||
|
||||
Use any of the following functions to extract values:
|
||||
|
||||
* :func:`urukul_sta_rf_sw`
|
||||
* :func:`urukul_sta_smp_err`
|
||||
* :func:`urukul_sta_pll_lock`
|
||||
* :func:`urukul_sta_ifc_mode`
|
||||
* :func:`urukul_sta_proto_rev`
|
||||
|
||||
:return: The status register value.
|
||||
"""
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END | spi.SPI_INPUT, 24,
|
||||
SPIT_CFG_RD, CS_CFG)
|
||||
self.bus.write(self.cfg_reg << 8)
|
||||
return self.bus.read()
|
||||
|
||||
@kernel
|
||||
def init(self, blind=False):
|
||||
"""Initialize and detect Urukul.
|
||||
|
||||
Resets the DDS I/O interface and verifies correct CPLD gateware
|
||||
version.
|
||||
Does not pulse the DDS MASTER_RESET as that confuses the AD9910.
|
||||
|
||||
:param blind: Do not attempt to verify presence and compatibility.
|
||||
"""
|
||||
cfg = self.cfg_reg
|
||||
# Don't pulse MASTER_RESET (m-labs/artiq#940)
|
||||
self.cfg_reg = cfg | (0 << CFG_RST) | (1 << CFG_IO_RST)
|
||||
if blind:
|
||||
self.cfg_write(self.cfg_reg)
|
||||
else:
|
||||
proto_rev = urukul_sta_proto_rev(self.sta_read())
|
||||
if proto_rev != STA_PROTO_REV_MATCH:
|
||||
raise ValueError("Urukul proto_rev mismatch")
|
||||
delay(100*us) # reset, slack
|
||||
self.cfg_write(cfg)
|
||||
if self.sync_div:
|
||||
at_mu(now_mu() & ~0xf) # align to RTIO/2
|
||||
self.set_sync_div(self.sync_div) # 125 MHz/2 = 1 GHz/16
|
||||
delay(1*ms) # DDS wake up
|
||||
|
||||
@kernel
|
||||
def io_rst(self):
|
||||
"""Pulse IO_RST"""
|
||||
self.cfg_write(self.cfg_reg | (1 << CFG_IO_RST))
|
||||
self.cfg_write(self.cfg_reg & ~(1 << CFG_IO_RST))
|
||||
|
||||
@kernel
|
||||
def cfg_sw(self, channel, on):
|
||||
"""Configure the RF switches through the configuration register.
|
||||
|
||||
These values are logically OR-ed with the LVDS lines on EEM1.
|
||||
|
||||
:param channel: Channel index (0-3)
|
||||
:param on: Switch value
|
||||
"""
|
||||
c = self.cfg_reg
|
||||
if on:
|
||||
c |= 1 << channel
|
||||
else:
|
||||
c &= ~(1 << channel)
|
||||
self.cfg_write(c)
|
||||
|
||||
@kernel
|
||||
def cfg_switches(self, state):
|
||||
"""Configure all four RF switches through the configuration register.
|
||||
|
||||
:param state: RF switch state as a 4 bit integer.
|
||||
"""
|
||||
self.cfg_write((self.cfg_reg & ~0xf) | state)
|
||||
|
||||
@kernel
|
||||
def set_att_mu(self, channel, att):
|
||||
"""Set digital step attenuator in machine units.
|
||||
|
||||
This method will also write the attenuator settings of the three other channels. Use
|
||||
:meth:`get_att_mu` to retrieve the hardware state set in previous experiments.
|
||||
|
||||
:param channel: Attenuator channel (0-3).
|
||||
:param att: 8-bit digital attenuation setting:
|
||||
255 minimum attenuation, 0 maximum attenuation (31.5 dB)
|
||||
"""
|
||||
a = self.att_reg & ~(0xff << (channel * 8))
|
||||
a |= att << (channel * 8)
|
||||
self.set_all_att_mu(a)
|
||||
|
||||
@kernel
|
||||
def set_all_att_mu(self, att_reg):
|
||||
"""Set all four digital step attenuators (in machine units).
|
||||
|
||||
.. seealso:: :meth:`set_att_mu`
|
||||
|
||||
:param att_reg: Attenuator setting string (32 bit)
|
||||
"""
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 32,
|
||||
SPIT_ATT_WR, CS_ATT)
|
||||
self.bus.write(att_reg)
|
||||
self.att_reg = att_reg
|
||||
|
||||
@kernel
|
||||
def set_att(self, channel, att):
|
||||
"""Set digital step attenuator in SI units.
|
||||
|
||||
This method will write the attenuator settings of all four channels.
|
||||
|
||||
.. seealso:: :meth:`set_att_mu`
|
||||
|
||||
:param channel: Attenuator channel (0-3).
|
||||
:param att: Attenuation setting in dB. Higher value is more
|
||||
attenuation. Minimum attenuation is 0*dB, maximum attenuation is
|
||||
31.5*dB.
|
||||
"""
|
||||
code = 255 - int32(round(att*8))
|
||||
if code < 0 or code > 255:
|
||||
raise ValueError("Invalid urukul.CPLD attenuation!")
|
||||
self.set_att_mu(channel, code)
|
||||
|
||||
@kernel
|
||||
def get_att_mu(self):
|
||||
"""Return the digital step attenuator settings in machine units.
|
||||
|
||||
The result is stored and will be used in future calls of :meth:`set_att_mu`.
|
||||
|
||||
:return: 32 bit attenuator settings
|
||||
"""
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_INPUT, 32,
|
||||
SPIT_ATT_RD, CS_ATT)
|
||||
self.bus.write(0) # shift in zeros, shift out current value
|
||||
self.bus.set_config_mu(SPI_CONFIG | spi.SPI_END, 32,
|
||||
SPIT_ATT_WR, CS_ATT)
|
||||
delay(10*us)
|
||||
self.att_reg = self.bus.read()
|
||||
self.bus.write(self.att_reg) # shift in current value again and latch
|
||||
return self.att_reg
|
||||
|
||||
@kernel
|
||||
def set_sync_div(self, div):
|
||||
"""Set the SYNC_IN AD9910 pulse generator frequency
|
||||
and align it to the current RTIO timestamp.
|
||||
|
||||
The SYNC_IN signal is derived from the coarse RTIO clock
|
||||
and the divider must be a power of two.
|
||||
Configure ``sync_sel == 0``.
|
||||
|
||||
:param div: SYNC_IN frequency divider. Must be a power of two.
|
||||
Minimum division ratio is 2. Maximum division ratio is 16.
|
||||
"""
|
||||
ftw_max = 1 << 4
|
||||
ftw = ftw_max//div
|
||||
assert ftw*div == ftw_max
|
||||
self.sync.set_mu(ftw)
|
||||
|
||||
@kernel
|
||||
def set_profile(self, profile):
|
||||
"""Set the PROFILE pins.
|
||||
|
||||
The PROFILE pins are common to all four DDS channels.
|
||||
|
||||
:param profile: PROFILE pins in numeric representation (0-7).
|
||||
"""
|
||||
cfg = self.cfg_reg & ~(7 << CFG_PROFILE)
|
||||
cfg |= (profile & 7) << CFG_PROFILE
|
||||
self.cfg_write(cfg)
|
|
@ -0,0 +1,53 @@
|
|||
"""RTIO driver for the Zotino 32-channel, 16-bit 1MSPS DAC.
|
||||
|
||||
Output event replacement is not supported and issuing commands at the same
|
||||
time is an error.
|
||||
"""
|
||||
|
||||
from artiq.language.core import kernel
|
||||
from artiq.coredevice import spi2 as spi
|
||||
from artiq.coredevice.ad53xx import SPI_AD53XX_CONFIG, AD53xx
|
||||
|
||||
_SPI_SR_CONFIG = (0*spi.SPI_OFFLINE | 1*spi.SPI_END |
|
||||
0*spi.SPI_INPUT | 0*spi.SPI_CS_POLARITY |
|
||||
0*spi.SPI_CLK_POLARITY | 0*spi.SPI_CLK_PHASE |
|
||||
0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX)
|
||||
|
||||
_SPI_CS_DAC = 1
|
||||
_SPI_CS_SR = 2
|
||||
|
||||
|
||||
class Zotino(AD53xx):
|
||||
""" Zotino 32-channel, 16-bit 1MSPS DAC.
|
||||
|
||||
Controls the AD5372 DAC and the 8 user LEDs via a shared SPI interface.
|
||||
|
||||
:param spi_device: SPI bus device name
|
||||
:param ldac_device: LDAC RTIO TTLOut channel name.
|
||||
:param clr_device: CLR RTIO TTLOut channel name.
|
||||
:param div_write: SPI clock divider for write operations (default: 4,
|
||||
50MHz max SPI clock)
|
||||
:param div_read: SPI clock divider for read operations (default: 8, not
|
||||
optimized for speed, but cf data sheet t22: 25ns min SCLK edge to SDO
|
||||
valid)
|
||||
:param vref: DAC reference voltage (default: 5.)
|
||||
:param core_device: Core device name (default: "core")
|
||||
"""
|
||||
|
||||
def __init__(self, dmgr, spi_device, ldac_device=None, clr_device=None,
|
||||
div_write=4, div_read=8, vref=5., core="core"):
|
||||
AD53xx.__init__(self, dmgr=dmgr, spi_device=spi_device,
|
||||
ldac_device=ldac_device, clr_device=clr_device,
|
||||
chip_select=_SPI_CS_DAC, div_write=div_write,
|
||||
div_read=div_read, core=core)
|
||||
|
||||
@kernel
|
||||
def set_leds(self, leds):
|
||||
""" Sets the states of the 8 user LEDs.
|
||||
|
||||
:param leds: 8-bit word with LED state
|
||||
"""
|
||||
self.bus.set_config_mu(_SPI_SR_CONFIG, 8, self.div_write, _SPI_CS_SR)
|
||||
self.bus.write(leds << 24)
|
||||
self.bus.set_config_mu(SPI_AD53XX_CONFIG, 24, self.div_write,
|
||||
self.chip_select)
|
|
@ -1,3 +1,4 @@
|
|||
import asyncio
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
@ -149,15 +150,16 @@ class AppletsCCBDock(applets.AppletsDock):
|
|||
corresponds to a single group. If ``group`` is ``None`` or an empty
|
||||
list, it corresponds to the root.
|
||||
|
||||
``command`` gives the command line used to run the applet, as if it
|
||||
was started from a shell. The dashboard substitutes variables such as
|
||||
``$python`` that gives the complete file name of the Python
|
||||
interpreter running the dashboard.
|
||||
``command`` gives the command line used to run the applet, as if it was
|
||||
started from a shell. The dashboard substitutes variables such as
|
||||
``$python`` that gives the complete file name of the Python interpreter
|
||||
running the dashboard.
|
||||
|
||||
If the name already exists (after following any specified groups), the
|
||||
command or code of the existing applet with that name is replaced, and
|
||||
the applet is shown at its previous position. If not, a new applet
|
||||
entry is created and the applet is shown at any position on the screen.
|
||||
the applet is restarted and shown at its previous position. If not, a
|
||||
new applet entry is created and the applet is shown at any position on
|
||||
the screen.
|
||||
|
||||
If the group(s) do not exist, they are created.
|
||||
|
||||
|
@ -181,9 +183,17 @@ class AppletsCCBDock(applets.AppletsDock):
|
|||
else:
|
||||
spec = {"ty": "code", "code": code, "command": command}
|
||||
if applet is None:
|
||||
logger.debug("Applet %s does not exist: creating", name)
|
||||
applet = self.new(name=name, spec=spec, parent=parent)
|
||||
else:
|
||||
if spec != self.get_spec(applet):
|
||||
logger.debug("Applet %s already exists: updating existing spec", name)
|
||||
self.set_spec(applet, spec)
|
||||
if applet.applet_dock:
|
||||
asyncio.ensure_future(applet.applet_dock.restart())
|
||||
else:
|
||||
logger.debug("Applet %s already exists and no update required", name)
|
||||
|
||||
if ccbp == "enable":
|
||||
applet.setCheckState(0, QtCore.Qt.Checked)
|
||||
|
||||
|
|
|
@ -7,9 +7,11 @@ from collections import OrderedDict
|
|||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
import h5py
|
||||
|
||||
from artiq.gui.tools import LayoutWidget, log_level_to_name, get_open_file_name
|
||||
from sipyco import pyon
|
||||
|
||||
from artiq.gui.entries import procdesc_to_entry, ScanEntry
|
||||
from artiq.protocols import pyon
|
||||
from artiq.gui.fuzzy_select import FuzzySelectWidget
|
||||
from artiq.gui.tools import LayoutWidget, log_level_to_name, get_open_file_name
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -162,14 +164,14 @@ class _ArgumentEditor(QtWidgets.QTreeWidget):
|
|||
|
||||
async def _recompute_argument(self, name):
|
||||
try:
|
||||
arginfo = await self.manager.compute_arginfo(self.expurl)
|
||||
expdesc = await self.manager.compute_expdesc(self.expurl)
|
||||
except:
|
||||
logger.error("Could not recompute argument '%s' of '%s'",
|
||||
name, self.expurl, exc_info=True)
|
||||
return
|
||||
argument = self.manager.get_submission_arguments(self.expurl)[name]
|
||||
|
||||
procdesc = arginfo[name][0]
|
||||
procdesc = expdesc["arginfo"][name][0]
|
||||
state = procdesc_to_entry(procdesc).default_state(procdesc)
|
||||
argument["desc"] = procdesc
|
||||
argument["state"] = state
|
||||
|
@ -272,7 +274,8 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
scheduling["due_date"] = due_date
|
||||
datetime_en.stateChanged.connect(update_datetime_en)
|
||||
|
||||
pipeline_name = QtWidgets.QLineEdit()
|
||||
self.pipeline_name = QtWidgets.QLineEdit()
|
||||
pipeline_name = self.pipeline_name
|
||||
self.layout.addWidget(QtWidgets.QLabel("Pipeline:"), 1, 2)
|
||||
self.layout.addWidget(pipeline_name, 1, 3)
|
||||
|
||||
|
@ -280,9 +283,10 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
|
||||
def update_pipeline_name(text):
|
||||
scheduling["pipeline_name"] = text
|
||||
pipeline_name.textEdited.connect(update_pipeline_name)
|
||||
pipeline_name.textChanged.connect(update_pipeline_name)
|
||||
|
||||
priority = QtWidgets.QSpinBox()
|
||||
self.priority = QtWidgets.QSpinBox()
|
||||
priority = self.priority
|
||||
priority.setRange(-99, 99)
|
||||
self.layout.addWidget(QtWidgets.QLabel("Priority:"), 2, 0)
|
||||
self.layout.addWidget(priority, 2, 1)
|
||||
|
@ -293,7 +297,8 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
scheduling["priority"] = value
|
||||
priority.valueChanged.connect(update_priority)
|
||||
|
||||
flush = QtWidgets.QCheckBox("Flush")
|
||||
self.flush = QtWidgets.QCheckBox("Flush")
|
||||
flush = self.flush
|
||||
flush.setToolTip("Flush the pipeline (of current- and higher-priority "
|
||||
"experiments) before starting the experiment")
|
||||
self.layout.addWidget(flush, 2, 2, 1, 2)
|
||||
|
@ -386,11 +391,12 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
|
||||
async def _recompute_arguments_task(self, overrides=dict()):
|
||||
try:
|
||||
arginfo = await self.manager.compute_arginfo(self.expurl)
|
||||
expdesc = await self.manager.compute_expdesc(self.expurl)
|
||||
except:
|
||||
logger.error("Could not recompute arguments of '%s'",
|
||||
logger.error("Could not recompute experiment description of '%s'",
|
||||
self.expurl, exc_info=True)
|
||||
return
|
||||
arginfo = expdesc["arginfo"]
|
||||
for k, v in overrides.items():
|
||||
# Some values (e.g. scans) may have multiple defaults in a list
|
||||
if ("default" in arginfo[k][0]
|
||||
|
@ -407,6 +413,28 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
self.argeditor.restore_state(argeditor_state)
|
||||
self.layout.addWidget(self.argeditor, 0, 0, 1, 5)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
reset_sched = menu.addAction("Reset scheduler settings")
|
||||
action = menu.exec_(self.mapToGlobal(event.pos()))
|
||||
if action == reset_sched:
|
||||
asyncio.ensure_future(self._recompute_sched_options_task())
|
||||
|
||||
async def _recompute_sched_options_task(self):
|
||||
try:
|
||||
expdesc = await self.manager.compute_expdesc(self.expurl)
|
||||
except:
|
||||
logger.error("Could not recompute experiment description of '%s'",
|
||||
self.expurl, exc_info=True)
|
||||
return
|
||||
sched_defaults = expdesc["scheduler_defaults"]
|
||||
|
||||
scheduling = self.manager.get_submission_scheduling(self.expurl)
|
||||
scheduling.update(sched_defaults)
|
||||
self.priority.setValue(scheduling["priority"])
|
||||
self.pipeline_name.setText(scheduling["pipeline_name"])
|
||||
self.flush.setChecked(scheduling["flush"])
|
||||
|
||||
def _load_hdf5_clicked(self):
|
||||
asyncio.ensure_future(self._load_hdf5_task())
|
||||
|
||||
|
@ -461,6 +489,60 @@ class _ExperimentDock(QtWidgets.QMdiSubWindow):
|
|||
self.hdf5_load_directory = state["hdf5_load_directory"]
|
||||
|
||||
|
||||
class _QuickOpenDialog(QtWidgets.QDialog):
|
||||
"""Modal dialog for opening/submitting experiments from a
|
||||
FuzzySelectWidget."""
|
||||
closed = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, manager):
|
||||
super().__init__(manager.main_window)
|
||||
self.setModal(True)
|
||||
|
||||
self.manager = manager
|
||||
|
||||
self.setWindowTitle("Quick open...")
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Find matching experiment names. Open experiments are preferred to
|
||||
# matches from the repository to ease quick window switching.
|
||||
open_exps = list(self.manager.open_experiments.keys())
|
||||
repo_exps = set("repo:" + k
|
||||
for k in self.manager.explist.keys()) - set(open_exps)
|
||||
choices = [(o, 100) for o in open_exps] + [(r, 0) for r in repo_exps]
|
||||
|
||||
self.select_widget = FuzzySelectWidget(choices)
|
||||
layout.addWidget(self.select_widget)
|
||||
self.select_widget.aborted.connect(self.close)
|
||||
self.select_widget.finished.connect(self._open_experiment)
|
||||
|
||||
font_metrics = QtGui.QFontMetrics(self.select_widget.line_edit.font())
|
||||
self.select_widget.setMinimumWidth(font_metrics.averageCharWidth() * 70)
|
||||
|
||||
def done(self, r):
|
||||
if self.select_widget:
|
||||
self.select_widget.abort()
|
||||
self.closed.emit()
|
||||
QtWidgets.QDialog.done(self, r)
|
||||
|
||||
def _open_experiment(self, exp_name, modifiers):
|
||||
if modifiers & QtCore.Qt.ControlModifier:
|
||||
try:
|
||||
self.manager.submit(exp_name)
|
||||
except:
|
||||
# Not all open_experiments necessarily still exist in the explist
|
||||
# (e.g. if the repository has been re-scanned since).
|
||||
logger.warning("failed to submit experiment '%s'",
|
||||
exp_name,
|
||||
exc_info=True)
|
||||
else:
|
||||
self.manager.open_experiment(exp_name)
|
||||
self.close()
|
||||
|
||||
|
||||
class ExperimentManager:
|
||||
def __init__(self, main_window,
|
||||
explist_sub, schedule_sub,
|
||||
|
@ -481,6 +563,13 @@ class ExperimentManager:
|
|||
|
||||
self.open_experiments = dict()
|
||||
|
||||
self.is_quick_open_shown = False
|
||||
quick_open_shortcut = QtWidgets.QShortcut(
|
||||
QtCore.Qt.CTRL + QtCore.Qt.Key_P,
|
||||
main_window)
|
||||
quick_open_shortcut.setContext(QtCore.Qt.ApplicationShortcut)
|
||||
quick_open_shortcut.activated.connect(self.show_quick_open)
|
||||
|
||||
def set_explist_model(self, model):
|
||||
self.explist = model.backing_store
|
||||
|
||||
|
@ -508,6 +597,8 @@ class ExperimentManager:
|
|||
"due_date": None,
|
||||
"flush": False
|
||||
}
|
||||
if expurl[:5] == "repo:":
|
||||
scheduling.update(self.explist[expurl[5:]]["scheduler_defaults"])
|
||||
self.submission_scheduling[expurl] = scheduling
|
||||
return scheduling
|
||||
|
||||
|
@ -640,7 +731,7 @@ class ExperimentManager:
|
|||
rids.append(rid)
|
||||
asyncio.ensure_future(self._request_term_multiple(rids))
|
||||
|
||||
async def compute_arginfo(self, expurl):
|
||||
async def compute_expdesc(self, expurl):
|
||||
file, class_name, use_repository = self.resolve_expurl(expurl)
|
||||
if use_repository:
|
||||
revision = self.get_submission_options(expurl)["repo_rev"]
|
||||
|
@ -648,7 +739,7 @@ class ExperimentManager:
|
|||
revision = None
|
||||
description = await self.experiment_db_ctl.examine(
|
||||
file, use_repository, revision)
|
||||
return description[class_name]["arginfo"]
|
||||
return description[class_name]
|
||||
|
||||
async def open_file(self, file):
|
||||
description = await self.experiment_db_ctl.examine(file, False)
|
||||
|
@ -679,3 +770,14 @@ class ExperimentManager:
|
|||
self.submission_arguments = state["arguments"]
|
||||
for expurl in state["open_docks"]:
|
||||
self.open_experiment(expurl)
|
||||
|
||||
def show_quick_open(self):
|
||||
if self.is_quick_open_shown:
|
||||
return
|
||||
|
||||
self.is_quick_open_shown = True
|
||||
dialog = _QuickOpenDialog(self)
|
||||
def closed():
|
||||
self.is_quick_open_shown = False
|
||||
dialog.closed.connect(closed)
|
||||
dialog.show()
|
||||
|
|
|
@ -4,7 +4,8 @@ from collections import namedtuple
|
|||
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
|
||||
from artiq.protocols.sync_struct import Subscriber
|
||||
from sipyco.sync_struct import Subscriber
|
||||
|
||||
from artiq.coredevice.comm_moninj import *
|
||||
from artiq.gui.tools import LayoutWidget
|
||||
from artiq.gui.flowlayout import FlowLayout
|
||||
|
@ -74,6 +75,7 @@ class _TTLWidget(QtWidgets.QFrame):
|
|||
self.cur_level = False
|
||||
self.cur_oe = False
|
||||
self.cur_override = False
|
||||
self.cur_override_level = False
|
||||
self.refresh_display()
|
||||
|
||||
def enterEvent(self, event):
|
||||
|
@ -106,7 +108,9 @@ class _TTLWidget(QtWidgets.QFrame):
|
|||
self.set_mode(self.channel, "0")
|
||||
|
||||
def refresh_display(self):
|
||||
value_s = "1" if self.cur_level else "0"
|
||||
level = self.cur_override_level if self.cur_override else self.cur_level
|
||||
value_s = "1" if level else "0"
|
||||
|
||||
if self.cur_override:
|
||||
value_s = "<b>" + value_s + "</b>"
|
||||
color = " color=\"red\""
|
||||
|
@ -215,17 +219,15 @@ def setup_from_ddb(ddb):
|
|||
force_out = v["class"] == "TTLOut"
|
||||
widget = _WidgetDesc(k, comment, _TTLWidget, (channel, force_out, k))
|
||||
description.add(widget)
|
||||
elif (v["module"] == "artiq.coredevice.dds"
|
||||
and v["class"] == "DDSGroupAD9914"):
|
||||
dds_sysclk = v["arguments"]["sysclk"]
|
||||
elif (v["module"] == "artiq.coredevice.dds"
|
||||
and v["class"] == "DDSChannelAD9914"):
|
||||
elif (v["module"] == "artiq.coredevice.ad9914"
|
||||
and v["class"] == "AD9914"):
|
||||
bus_channel = v["arguments"]["bus_channel"]
|
||||
channel = v["arguments"]["channel"]
|
||||
dds_sysclk = v["arguments"]["sysclk"]
|
||||
widget = _WidgetDesc(k, comment, _DDSWidget, (bus_channel, channel, k))
|
||||
description.add(widget)
|
||||
elif (v["module"] == "artiq.coredevice.ad5360"
|
||||
and v["class"] == "AD5360"):
|
||||
elif ( (v["module"] == "artiq.coredevice.ad53xx" and v["class"] == "AD53XX")
|
||||
or (v["module"] == "artiq.coredevice.zotino" and v["class"] == "Zotino")):
|
||||
spi_device = v["arguments"]["spi_device"]
|
||||
spi_device = ddb[spi_device]
|
||||
while isinstance(spi_device, str):
|
||||
|
@ -242,7 +244,7 @@ def setup_from_ddb(ddb):
|
|||
class _DeviceManager:
|
||||
def __init__(self):
|
||||
self.core_addr = None
|
||||
self.new_core_addr = asyncio.Event()
|
||||
self.reconnect_core = asyncio.Event()
|
||||
self.core_connection = None
|
||||
self.core_connector_task = asyncio.ensure_future(self.core_connector())
|
||||
|
||||
|
@ -267,7 +269,7 @@ class _DeviceManager:
|
|||
|
||||
if core_addr != self.core_addr:
|
||||
self.core_addr = core_addr
|
||||
self.new_core_addr.set()
|
||||
self.reconnect_core.set()
|
||||
|
||||
self.dds_sysclk = dds_sysclk
|
||||
|
||||
|
@ -341,18 +343,20 @@ class _DeviceManager:
|
|||
|
||||
def setup_ttl_monitoring(self, enable, channel):
|
||||
if self.core_connection is not None:
|
||||
self.core_connection.monitor(enable, channel, TTLProbe.level.value)
|
||||
self.core_connection.monitor(enable, channel, TTLProbe.oe.value)
|
||||
self.core_connection.monitor_probe(enable, channel, TTLProbe.level.value)
|
||||
self.core_connection.monitor_probe(enable, channel, TTLProbe.oe.value)
|
||||
self.core_connection.monitor_injection(enable, channel, TTLOverride.en.value)
|
||||
self.core_connection.monitor_injection(enable, channel, TTLOverride.level.value)
|
||||
if enable:
|
||||
self.core_connection.get_injection_status(channel, TTLOverride.en.value)
|
||||
|
||||
def setup_dds_monitoring(self, enable, bus_channel, channel):
|
||||
if self.core_connection is not None:
|
||||
self.core_connection.monitor(enable, bus_channel, channel)
|
||||
self.core_connection.monitor_probe(enable, bus_channel, channel)
|
||||
|
||||
def setup_dac_monitoring(self, enable, spi_channel, channel):
|
||||
if self.core_connection is not None:
|
||||
self.core_connection.monitor(enable, spi_channel, channel)
|
||||
self.core_connection.monitor_probe(enable, spi_channel, channel)
|
||||
|
||||
def monitor_cb(self, channel, probe, value):
|
||||
if channel in self.ttl_widgets:
|
||||
|
@ -373,21 +377,35 @@ class _DeviceManager:
|
|||
|
||||
def injection_status_cb(self, channel, override, value):
|
||||
if channel in self.ttl_widgets:
|
||||
self.ttl_widgets[channel].cur_override = bool(value)
|
||||
widget = self.ttl_widgets[channel]
|
||||
if override == TTLOverride.en.value:
|
||||
widget.cur_override = bool(value)
|
||||
if override == TTLOverride.level.value:
|
||||
widget.cur_override_level = bool(value)
|
||||
widget.refresh_display()
|
||||
|
||||
def disconnect_cb(self):
|
||||
logger.error("lost connection to core device moninj")
|
||||
self.reconnect_core.set()
|
||||
|
||||
async def core_connector(self):
|
||||
while True:
|
||||
await self.new_core_addr.wait()
|
||||
self.new_core_addr.clear()
|
||||
await self.reconnect_core.wait()
|
||||
self.reconnect_core.clear()
|
||||
if self.core_connection is not None:
|
||||
await self.core_connection.close()
|
||||
self.core_connection = None
|
||||
new_core_connection = CommMonInj(self.monitor_cb, self.injection_status_cb,
|
||||
lambda: logger.error("lost connection to core device moninj"))
|
||||
self.disconnect_cb)
|
||||
try:
|
||||
await new_core_connection.connect(self.core_addr, 1383)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("cancelled connection to core device moninj")
|
||||
break
|
||||
except:
|
||||
logger.error("failed to connect to core device moninj", exc_info=True)
|
||||
await asyncio.sleep(10.)
|
||||
self.reconnect_core.set()
|
||||
else:
|
||||
self.core_connection = new_core_connection
|
||||
for ttl_channel in self.ttl_widgets.keys():
|
||||
|
|
|
@ -1,258 +0,0 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
import shlex
|
||||
import socket
|
||||
import os
|
||||
|
||||
from artiq.protocols.sync_struct import Subscriber
|
||||
from artiq.protocols.pc_rpc import AsyncioClient
|
||||
from artiq.protocols.logging import LogParser
|
||||
from artiq.tools import Condition, TaskObject
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Controller:
|
||||
def __init__(self, name, ddb_entry):
|
||||
self.name = name
|
||||
self.command = ddb_entry["command"]
|
||||
self.retry_timer = ddb_entry.get("retry_timer", 5)
|
||||
self.retry_timer_backoff = ddb_entry.get("retry_timer_backoff", 1.1)
|
||||
|
||||
self.host = ddb_entry["host"]
|
||||
self.port = ddb_entry["port"]
|
||||
self.ping_timer = ddb_entry.get("ping_timer", 30)
|
||||
self.ping_timeout = ddb_entry.get("ping_timeout", 30)
|
||||
self.term_timeout = ddb_entry.get("term_timeout", 30)
|
||||
|
||||
self.retry_timer_cur = self.retry_timer
|
||||
self.retry_now = Condition()
|
||||
self.process = None
|
||||
self.launch_task = asyncio.ensure_future(self.launcher())
|
||||
|
||||
async def end(self):
|
||||
self.launch_task.cancel()
|
||||
await asyncio.wait_for(self.launch_task, None)
|
||||
|
||||
async def call(self, method, *args, **kwargs):
|
||||
remote = AsyncioClient()
|
||||
await remote.connect_rpc(self.host, self.port, None)
|
||||
try:
|
||||
targets, _ = remote.get_rpc_id()
|
||||
await remote.select_rpc_target(targets[0])
|
||||
r = await getattr(remote, method)(*args, **kwargs)
|
||||
finally:
|
||||
remote.close_rpc()
|
||||
return r
|
||||
|
||||
async def _ping(self):
|
||||
try:
|
||||
ok = await asyncio.wait_for(self.call("ping"),
|
||||
self.ping_timeout)
|
||||
if ok:
|
||||
self.retry_timer_cur = self.retry_timer
|
||||
return ok
|
||||
except:
|
||||
return False
|
||||
|
||||
async def _wait_and_ping(self):
|
||||
while True:
|
||||
try:
|
||||
await asyncio.wait_for(self.process.wait(),
|
||||
self.ping_timer)
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("pinging controller %s", self.name)
|
||||
ok = await self._ping()
|
||||
if not ok:
|
||||
logger.warning("Controller %s ping failed", self.name)
|
||||
await self._terminate()
|
||||
return
|
||||
else:
|
||||
break
|
||||
|
||||
def _get_log_source(self):
|
||||
return "controller({})".format(self.name)
|
||||
|
||||
async def launcher(self):
|
||||
try:
|
||||
while True:
|
||||
logger.info("Starting controller %s with command: %s",
|
||||
self.name, self.command)
|
||||
try:
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
*shlex.split(self.command),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
env=env, start_new_session=True)
|
||||
asyncio.ensure_future(
|
||||
LogParser(self._get_log_source).stream_task(
|
||||
self.process.stdout))
|
||||
asyncio.ensure_future(
|
||||
LogParser(self._get_log_source).stream_task(
|
||||
self.process.stderr))
|
||||
await self._wait_and_ping()
|
||||
except FileNotFoundError:
|
||||
logger.warning("Controller %s failed to start", self.name)
|
||||
else:
|
||||
logger.warning("Controller %s exited", self.name)
|
||||
logger.warning("Restarting in %.1f seconds",
|
||||
self.retry_timer_cur)
|
||||
try:
|
||||
await asyncio.wait_for(self.retry_now.wait(),
|
||||
self.retry_timer_cur)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
self.retry_timer_cur *= self.retry_timer_backoff
|
||||
except asyncio.CancelledError:
|
||||
await self._terminate()
|
||||
|
||||
async def _terminate(self):
|
||||
if self.process is None or self.process.returncode is not None:
|
||||
logger.info("Controller %s already terminated", self.name)
|
||||
return
|
||||
logger.debug("Terminating controller %s", self.name)
|
||||
try:
|
||||
await asyncio.wait_for(self.call("terminate"), self.term_timeout)
|
||||
await asyncio.wait_for(self.process.wait(), self.term_timeout)
|
||||
logger.info("Controller %s terminated", self.name)
|
||||
return
|
||||
except:
|
||||
logger.warning("Controller %s did not exit on request, "
|
||||
"ending the process", self.name)
|
||||
if os.name != "nt":
|
||||
try:
|
||||
self.process.terminate()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
await asyncio.wait_for(self.process.wait(), self.term_timeout)
|
||||
logger.info("Controller process %s terminated", self.name)
|
||||
return
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Controller process %s did not terminate, "
|
||||
"killing", self.name)
|
||||
try:
|
||||
self.process.kill()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
await asyncio.wait_for(self.process.wait(), self.term_timeout)
|
||||
logger.info("Controller process %s killed", self.name)
|
||||
return
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Controller process %s failed to die", self.name)
|
||||
|
||||
|
||||
def get_ip_addresses(host):
|
||||
try:
|
||||
addrinfo = socket.getaddrinfo(host, None)
|
||||
except:
|
||||
return set()
|
||||
return {info[4][0] for info in addrinfo}
|
||||
|
||||
|
||||
class Controllers:
|
||||
def __init__(self):
|
||||
self.host_filter = None
|
||||
self.active_or_queued = set()
|
||||
self.queue = asyncio.Queue()
|
||||
self.active = dict()
|
||||
self.process_task = asyncio.ensure_future(self._process())
|
||||
|
||||
async def _process(self):
|
||||
while True:
|
||||
action, param = await self.queue.get()
|
||||
if action == "set":
|
||||
k, ddb_entry = param
|
||||
if k in self.active:
|
||||
await self.active[k].end()
|
||||
self.active[k] = Controller(k, ddb_entry)
|
||||
elif action == "del":
|
||||
await self.active[param].end()
|
||||
del self.active[param]
|
||||
self.queue.task_done()
|
||||
if action not in ("set", "del"):
|
||||
raise ValueError
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
if (isinstance(v, dict) and v["type"] == "controller" and
|
||||
self.host_filter in get_ip_addresses(v["host"])):
|
||||
v["command"] = v["command"].format(name=k,
|
||||
bind=self.host_filter,
|
||||
port=v["port"])
|
||||
self.queue.put_nowait(("set", (k, v)))
|
||||
self.active_or_queued.add(k)
|
||||
|
||||
def __delitem__(self, k):
|
||||
if k in self.active_or_queued:
|
||||
self.queue.put_nowait(("del", k))
|
||||
self.active_or_queued.remove(k)
|
||||
|
||||
def delete_all(self):
|
||||
for name in set(self.active_or_queued):
|
||||
del self[name]
|
||||
|
||||
async def shutdown(self):
|
||||
self.process_task.cancel()
|
||||
for c in self.active.values():
|
||||
await c.end()
|
||||
|
||||
|
||||
class ControllerDB:
|
||||
def __init__(self):
|
||||
self.current_controllers = Controllers()
|
||||
|
||||
def set_host_filter(self, host_filter):
|
||||
self.current_controllers.host_filter = host_filter
|
||||
|
||||
def sync_struct_init(self, init):
|
||||
if self.current_controllers is not None:
|
||||
self.current_controllers.delete_all()
|
||||
for k, v in init.items():
|
||||
self.current_controllers[k] = v
|
||||
return self.current_controllers
|
||||
|
||||
|
||||
class ControllerManager(TaskObject):
|
||||
def __init__(self, server, port, retry_master):
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.retry_master = retry_master
|
||||
self.controller_db = ControllerDB()
|
||||
|
||||
async def _do(self):
|
||||
try:
|
||||
subscriber = Subscriber("devices",
|
||||
self.controller_db.sync_struct_init)
|
||||
while True:
|
||||
try:
|
||||
def set_host_filter():
|
||||
s = subscriber.writer.get_extra_info("socket")
|
||||
localhost = s.getsockname()[0]
|
||||
self.controller_db.set_host_filter(localhost)
|
||||
await subscriber.connect(self.server, self.port,
|
||||
set_host_filter)
|
||||
try:
|
||||
await asyncio.wait_for(subscriber.receive_task, None)
|
||||
finally:
|
||||
await subscriber.close()
|
||||
except (ConnectionAbortedError, ConnectionError,
|
||||
ConnectionRefusedError, ConnectionResetError) as e:
|
||||
logger.warning("Connection to master failed (%s: %s)",
|
||||
e.__class__.__name__, str(e))
|
||||
else:
|
||||
logger.warning("Connection to master lost")
|
||||
logger.warning("Retrying in %.1f seconds", self.retry_master)
|
||||
await asyncio.sleep(self.retry_master)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
await self.controller_db.current_controllers.shutdown()
|
||||
|
||||
def retry_now(self, k):
|
||||
"""If a controller is disabled and pending retry, perform that retry
|
||||
now."""
|
||||
self.controller_db.current_controllers.active[k].retry_now.notify()
|
|
@ -1,152 +0,0 @@
|
|||
# Written by Joe Britton, 2016
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import asyncserial
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnexpectedResponse(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class KoradKA3005P:
|
||||
"""The Korad KA3005P is a 1-channel programmable power supply
|
||||
(0-30V/0-5A) with both USB/serial and RS232 connectivity.
|
||||
|
||||
All amplitudes are in volts.
|
||||
All currents are in amperes.
|
||||
"""
|
||||
|
||||
# Serial interface gleaned from the following.
|
||||
# https://github.com/starforgelabs/py-korad-serial
|
||||
# https://sigrok.org/wiki/Korad_KAxxxxP_series
|
||||
|
||||
def __init__(self, serial_dev):
|
||||
if serial_dev is None:
|
||||
self.simulation = True
|
||||
else:
|
||||
self.simulation = False
|
||||
self.port = asyncserial.AsyncSerial(serial_dev, baudrate=9600)
|
||||
|
||||
def close(self):
|
||||
"""Close the serial port."""
|
||||
if not self.simulation:
|
||||
self.port.close()
|
||||
|
||||
async def _ser_read(self, fixed_length=None):
|
||||
""" strings returned by firmware are zero-terminated or fixed length
|
||||
"""
|
||||
r = ""
|
||||
if self.simulation:
|
||||
logger.info("simulation _ser_read()")
|
||||
else:
|
||||
c = (await self.port.read(1)).decode()
|
||||
r = c
|
||||
while len(c) > 0 and ord(c) != 0 and not len(r) == fixed_length:
|
||||
c = (await self.port.read(1)).decode().rstrip('\0')
|
||||
r += c
|
||||
logger.debug("_read %s: ", r)
|
||||
return r
|
||||
|
||||
async def _ser_write(self, cmd):
|
||||
if self.simulation:
|
||||
logger.info("simulation _ser_write(\"%s\")", cmd)
|
||||
else:
|
||||
logger.debug("_ser_write(\"%s\")", cmd)
|
||||
await asyncio.sleep(0.1)
|
||||
await self.port.write(cmd.encode("ascii"))
|
||||
|
||||
async def setup(self):
|
||||
"""Configure in known state."""
|
||||
await self.set_output(False)
|
||||
await self.set_v(0)
|
||||
await self.set_ovp(False)
|
||||
await self.set_i(0)
|
||||
await self.set_ocp(False)
|
||||
|
||||
async def get_id(self):
|
||||
"""Request identification from device.
|
||||
"""
|
||||
if self.simulation:
|
||||
return "KORADKA3005PV2.0"
|
||||
await self._ser_write("*IDN?")
|
||||
return await self._ser_read()
|
||||
|
||||
async def set_output(self, b):
|
||||
"""Enable/disable the power output.
|
||||
"""
|
||||
if b:
|
||||
await self._ser_write("OUT1")
|
||||
else:
|
||||
await self._ser_write("OUT0")
|
||||
|
||||
async def set_v(self, v):
|
||||
"""Set the maximum output voltage."""
|
||||
await self._ser_write("VSET1:{0:05.2f}".format(v))
|
||||
|
||||
async def get_v(self):
|
||||
"""Request the voltage as set by the user."""
|
||||
await self._ser_write("VSET1?")
|
||||
return float(await self._ser_read(fixed_length=5))
|
||||
|
||||
async def measure_v(self):
|
||||
"""Request the actual voltage output."""
|
||||
await self._ser_write("VOUT1?")
|
||||
return float(await self._ser_read(fixed_length=5))
|
||||
|
||||
async def set_ovp(self, b):
|
||||
"""Enable/disable the "Over Voltage Protection", the PS will switch off the
|
||||
output when the voltage rises above the actual level."""
|
||||
if b:
|
||||
await self._ser_write("OVP1")
|
||||
else:
|
||||
await self._ser_write("OVP0")
|
||||
|
||||
async def set_i(self, v):
|
||||
"""Set the maximum output current."""
|
||||
await self._ser_write("ISET1:{0:05.3f}".format(v))
|
||||
|
||||
async def get_i(self):
|
||||
"""Request the current as set by the user. """
|
||||
# Expected behavior of ISET1? is to return 5 bytes.
|
||||
# However, if *IDN? has been previously called, ISET1? replies
|
||||
# with a sixth byte 'K' which should be discarded. For consistency,
|
||||
# always call *IDN? before calling ISET1?.
|
||||
self.get_id()
|
||||
await self._ser_write("ISET1?")
|
||||
r = (await self._ser_read(fixed_length=6)).rstrip('K')
|
||||
return float(r)
|
||||
|
||||
async def measure_i(self):
|
||||
"""Request the actual output current."""
|
||||
await self._ser_write("IOUT1?")
|
||||
r = await self._ser_read(fixed_length=5)
|
||||
if r[0] == "K":
|
||||
r = r[1:-1]
|
||||
return float(r)
|
||||
|
||||
async def set_ocp(self, b):
|
||||
"""Enable/disable the "Over Current Protection", the PS will switch off
|
||||
the output when the current rises above the actual level."""
|
||||
if b:
|
||||
await self._ser_write("OCP1")
|
||||
else:
|
||||
await self._ser_write("OCP0")
|
||||
|
||||
async def ping(self):
|
||||
"""Check if device is responding."""
|
||||
if self.simulation:
|
||||
return True
|
||||
try:
|
||||
id = await self.get_id()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
if id == "KORADKA3005PV2.0":
|
||||
logger.debug("ping successful")
|
||||
return True
|
||||
else:
|
||||
return False
|
|
@ -1,229 +0,0 @@
|
|||
import logging
|
||||
import ctypes
|
||||
import struct
|
||||
|
||||
from artiq.language.units import dB
|
||||
|
||||
|
||||
logger = logging.getLogger("lda")
|
||||
|
||||
|
||||
class HidError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Ldasim:
|
||||
"""Lab Brick Digital Attenuator simulation driver."""
|
||||
|
||||
def __init__(self):
|
||||
self._attenuation = None
|
||||
self._att_max = 63*dB
|
||||
self._att_step_size = 0.25*dB
|
||||
|
||||
def get_att_max(self):
|
||||
return self._att_max
|
||||
|
||||
def get_att_step_size(self):
|
||||
return self._att_step_size
|
||||
|
||||
def close(self):
|
||||
"""Close the device."""
|
||||
pass
|
||||
|
||||
def get_attenuation(self):
|
||||
"""Reads last attenuation value set to the simulated device.
|
||||
|
||||
:return: Returns the attenuation value in dB, or None if it was
|
||||
never set.
|
||||
:rtype: float
|
||||
"""
|
||||
|
||||
return self._attenuation
|
||||
|
||||
def set_attenuation(self, attenuation):
|
||||
"""Stores the new attenuation value.
|
||||
|
||||
:param attenuation: The attenuation value in dB.
|
||||
"""
|
||||
|
||||
step = self.get_att_step_size()
|
||||
att = round(attenuation/step)*step
|
||||
|
||||
if att > self.get_att_max():
|
||||
raise ValueError("Cannot set attenuation {} > {}"
|
||||
.format(att, self.get_att_max()))
|
||||
elif att < 0*dB:
|
||||
raise ValueError("Cannot set attenuation {} < 0".format(att))
|
||||
else:
|
||||
att = round(att*4)/4. * dB
|
||||
self._attenuation = att
|
||||
|
||||
def ping(self):
|
||||
return True
|
||||
|
||||
|
||||
class Lda:
|
||||
"""Lab Brick Digital Attenuator driver.
|
||||
|
||||
This driver depends on the hidapi library.
|
||||
|
||||
On Linux you should install hidapi-libusb shared library in a directory
|
||||
listed in your LD_LIBRARY_PATH or in the conventional places (/usr/lib,
|
||||
/lib, /usr/local/lib). This can be done either from hidapi sources
|
||||
or by installing the libhidapi-libusb0 binary package on Debian-like OS.
|
||||
|
||||
On Windows you should put hidapi.dll shared library in the
|
||||
artiq\\\\devices\\\\lda folder.
|
||||
|
||||
"""
|
||||
_vendor_id = 0x041f
|
||||
_product_ids = {
|
||||
"LDA-102": 0x1207,
|
||||
"LDA-602": 0x1208,
|
||||
"LDA-302P-1": 0x120E,
|
||||
}
|
||||
_att_max = {
|
||||
"LDA-102": 63*dB,
|
||||
"LDA-602": 63*dB,
|
||||
"LDA-302P-1": 63*dB
|
||||
}
|
||||
_att_step_size = {
|
||||
"LDA-102": 0.5*dB,
|
||||
"LDA-602": 0.5*dB,
|
||||
"LDA-302P-1": 1.0*dB
|
||||
}
|
||||
|
||||
def __init__(self, serial=None, product="LDA-102"):
|
||||
"""
|
||||
:param serial: The serial number.
|
||||
:param product: The product identifier string: LDA-102, LDA-602.
|
||||
"""
|
||||
|
||||
from artiq.devices.lda.hidapi import hidapi
|
||||
self.hidapi = hidapi
|
||||
self.product = product
|
||||
self.serial = serial
|
||||
|
||||
if self.serial is None:
|
||||
self.serial = next(self.enumerate(self.product))
|
||||
self._dev = self.hidapi.hid_open(self._vendor_id,
|
||||
self._product_ids[self.product],
|
||||
self.serial)
|
||||
if not self._dev:
|
||||
raise IOError("Device not found")
|
||||
|
||||
def close(self):
|
||||
"""Close the device."""
|
||||
self.hidapi.hid_close(self._dev)
|
||||
|
||||
def get_att_step_size(self):
|
||||
return self._att_step_size[self.product]
|
||||
|
||||
def get_att_max(self):
|
||||
return self._att_max[self.product]
|
||||
|
||||
@classmethod
|
||||
def enumerate(cls, product):
|
||||
from artiq.devices.lda.hidapi import hidapi
|
||||
devs = hidapi.hid_enumerate(cls._vendor_id,
|
||||
cls._product_ids[product])
|
||||
try:
|
||||
dev = devs
|
||||
while dev:
|
||||
yield dev[0].serial
|
||||
dev = dev[0].next
|
||||
finally:
|
||||
hidapi.hid_free_enumeration(devs)
|
||||
|
||||
def _check_error(self, ret):
|
||||
if ret < 0:
|
||||
err = self.hidapi.hid_error(self._dev)
|
||||
raise HidError("{}: {}".format(ret, err))
|
||||
return ret
|
||||
|
||||
def write(self, command, length, data=bytes()):
|
||||
"""Writes a command to the Lab Brick device.
|
||||
|
||||
:param command: command ID.
|
||||
:param length: number of meaningful bytes in the data array.
|
||||
:param data: a byte array containing the payload of the command.
|
||||
"""
|
||||
|
||||
# 0 is report id/padding
|
||||
buf = struct.pack("BBB6s", 0, command, length, data)
|
||||
res = self._check_error(self.hidapi.hid_write(self._dev, buf,
|
||||
len(buf)))
|
||||
if res != len(buf):
|
||||
raise IOError
|
||||
|
||||
def set(self, command, data):
|
||||
"""Sends a SET command to the Lab Brick device.
|
||||
|
||||
:param command: command ID, must have most significant bit set.
|
||||
:param data: payload of the command.
|
||||
"""
|
||||
|
||||
if not data:
|
||||
raise ValueError("Data is empty")
|
||||
if not (command & 0x80):
|
||||
raise ValueError("Set commands must have most significant bit set")
|
||||
self.write(command, len(data), data)
|
||||
|
||||
def get(self, command, length, timeout=1000):
|
||||
"""Sends a GET command to read back some value of the Lab Brick device.
|
||||
|
||||
:param command: Command ID, most significant bit must be cleared.
|
||||
:param length: Length of the command, "count" in the datasheet.
|
||||
:param timeout: Timeout of the HID read in ms.
|
||||
:return: Returns the value read from the device.
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
if command & 0x80:
|
||||
raise ValueError("Get commands must not have most significant bit"
|
||||
" set")
|
||||
status = None
|
||||
self.write(command, length)
|
||||
buf = ctypes.create_string_buffer(8)
|
||||
while status != command:
|
||||
res = self._check_error(self.hidapi.hid_read_timeout(self._dev,
|
||||
buf, len(buf), timeout))
|
||||
if res != len(buf):
|
||||
raise IOError
|
||||
status, length, data = struct.unpack("BB6s", buf.raw)
|
||||
data = data[:length]
|
||||
logger.info("%s %s %r", command, length, data)
|
||||
return data
|
||||
|
||||
def get_attenuation(self):
|
||||
"""Reads attenuation value from Lab Brick device.
|
||||
|
||||
:return: Returns the attenuation value in dB.
|
||||
:rtype: float
|
||||
"""
|
||||
|
||||
return (ord(self.get(0x0d, 1))/4.) * dB
|
||||
|
||||
def set_attenuation(self, attenuation):
|
||||
"""Sets attenuation value of the Lab Brick device.
|
||||
|
||||
:param attenuation: Attenuation value in dB.
|
||||
"""
|
||||
|
||||
step = self.get_att_step_size()
|
||||
att = round(attenuation/step)*step
|
||||
|
||||
if att > self.get_att_max():
|
||||
raise ValueError("Cannot set attenuation {} > {}"
|
||||
.format(att, self.get_att_max()))
|
||||
elif att < 0*dB:
|
||||
raise ValueError("Cannot set attenuation {} < 0".format(att))
|
||||
else:
|
||||
self.set(0x8d, bytes([int(round(att*4))]))
|
||||
|
||||
def ping(self):
|
||||
try:
|
||||
self.get_attenuation()
|
||||
except:
|
||||
return False
|
||||
return True
|
|
@ -1,58 +0,0 @@
|
|||
import os
|
||||
import atexit
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
|
||||
if "." not in os.environ["PATH"].split(";"):
|
||||
os.environ["PATH"] += ";."
|
||||
dir = os.path.split(__file__)[0]
|
||||
if dir not in os.environ["PATH"].split(";"):
|
||||
os.environ["PATH"] += ";{}".format(dir)
|
||||
|
||||
for n in "hidapi-libusb hidapi-hidraw hidapi".split():
|
||||
path = ctypes.util.find_library(n)
|
||||
if path:
|
||||
break
|
||||
if not path:
|
||||
raise ImportError("no hidapi library found")
|
||||
hidapi = ctypes.CDLL(path)
|
||||
|
||||
|
||||
class HidDeviceInfo(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
HidDeviceInfo._fields_ = [
|
||||
("path", ctypes.c_char_p),
|
||||
("vendor_id", ctypes.c_ushort),
|
||||
("product_id", ctypes.c_ushort),
|
||||
("serial", ctypes.c_wchar_p),
|
||||
("release", ctypes.c_ushort),
|
||||
("manufacturer", ctypes.c_wchar_p),
|
||||
("product", ctypes.c_wchar_p),
|
||||
("usage_page", ctypes.c_ushort),
|
||||
("usage", ctypes.c_ushort),
|
||||
("interface", ctypes.c_int),
|
||||
("next", ctypes.POINTER(HidDeviceInfo)),
|
||||
]
|
||||
|
||||
|
||||
hidapi.hid_enumerate.argtypes = [ctypes.c_ushort, ctypes.c_ushort]
|
||||
hidapi.hid_enumerate.restype = ctypes.POINTER(HidDeviceInfo)
|
||||
hidapi.hid_free_enumeration.argtypes = [ctypes.POINTER(HidDeviceInfo)]
|
||||
hidapi.hid_open.argtypes = [ctypes.c_ushort, ctypes.c_ushort,
|
||||
ctypes.c_wchar_p]
|
||||
hidapi.hid_open.restype = ctypes.c_void_p
|
||||
hidapi.hid_close.argtypes = [ctypes.c_void_p]
|
||||
hidapi.hid_read_timeout.argtypes = [ctypes.c_void_p, ctypes.c_char_p,
|
||||
ctypes.c_size_t, ctypes.c_int]
|
||||
hidapi.hid_read.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t]
|
||||
hidapi.hid_write.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t]
|
||||
hidapi.hid_send_feature_report.argtypes = [ctypes.c_void_p, ctypes.c_char_p,
|
||||
ctypes.c_size_t]
|
||||
hidapi.hid_get_feature_report.argtypes = [ctypes.c_void_p, ctypes.c_char_p,
|
||||
ctypes.c_size_t]
|
||||
hidapi.hid_error.argtypes = [ctypes.c_void_p]
|
||||
hidapi.hid_error.restype = ctypes.c_wchar_p
|
||||
|
||||
atexit.register(hidapi.hid_exit)
|
|
@ -1,201 +0,0 @@
|
|||
# Written by Joe Britton, 2015
|
||||
|
||||
import math
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
import asyncserial
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnexpectedResponse(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Novatech409B:
|
||||
"""Driver for Novatech 409B 4-channel DDS.
|
||||
|
||||
All output channels are in range [0, 1, 2, 3].
|
||||
All frequencies are in Hz.
|
||||
All phases are in turns.
|
||||
All amplitudes are in volts.
|
||||
"""
|
||||
|
||||
error_codes = {
|
||||
"?0": "Unrecognized Command",
|
||||
"?1": "Bad Frequency",
|
||||
"?2": "Bad AM Command",
|
||||
"?3": "Input line too long",
|
||||
"?4": "Bad Phase",
|
||||
"?5": "Bad Time",
|
||||
"?6": "Bad Mode",
|
||||
"?7": "Bad Amp",
|
||||
"?8": "Bad Constant",
|
||||
"?f": "Bad Byte"
|
||||
}
|
||||
|
||||
def __init__(self, serial_dev):
|
||||
if serial_dev is None:
|
||||
self.simulation = True
|
||||
else:
|
||||
self.simulation = False
|
||||
self.port = asyncserial.AsyncSerial(
|
||||
serial_dev,
|
||||
baudrate=19200,
|
||||
bytesize=8,
|
||||
parity="N",
|
||||
stopbits=1,
|
||||
xonxoff=0)
|
||||
|
||||
def close(self):
|
||||
"""Close the serial port."""
|
||||
if not self.simulation:
|
||||
self.port.close()
|
||||
|
||||
async def _ser_readline(self):
|
||||
c = await self.port.read(1)
|
||||
r = c
|
||||
while c != b"\n":
|
||||
c = await self.port.read(1)
|
||||
r += c
|
||||
return r
|
||||
|
||||
async def _ser_send(self, cmd, get_response=True):
|
||||
"""Send a string to the serial port."""
|
||||
|
||||
# Low-level routine for sending serial commands to device. It sends
|
||||
# strings and listens for a response terminated by a carriage return.
|
||||
# example:
|
||||
# ser_send("F0 1.0") # sets the freq of channel 0 to 1.0 MHz
|
||||
|
||||
if self.simulation:
|
||||
logger.info("simulation _ser_send(\"%s\")", cmd)
|
||||
else:
|
||||
logger.debug("_ser_send(\"%s\")", cmd)
|
||||
self.port.ser.reset_input_buffer()
|
||||
await self.port.write((cmd + "\r\n").encode())
|
||||
if get_response:
|
||||
result = (await self._ser_readline()).rstrip().decode()
|
||||
logger.debug("got response from device: %s", result)
|
||||
if result != "OK":
|
||||
errstr = self.error_codes.get(result, "Unrecognized reply")
|
||||
s = "Erroneous reply from device: {ec}, {ecs}".format(
|
||||
ec=result, ecs=errstr)
|
||||
raise ValueError(s)
|
||||
else:
|
||||
pass
|
||||
|
||||
async def reset(self):
|
||||
"""Hardware reset of 409B."""
|
||||
await self._ser_send("R", get_response=False)
|
||||
await asyncio.sleep(1)
|
||||
await self.setup()
|
||||
|
||||
async def setup(self):
|
||||
"""Initial setup of 409B."""
|
||||
|
||||
# Setup the Novatech 409B with the following defaults:
|
||||
# * command echo off ("E d")
|
||||
# * external clock ("") 10 MHz sinusoid -1 to +7 dBm
|
||||
|
||||
await self._ser_send("E d", get_response=False)
|
||||
await self.set_phase_continuous(True)
|
||||
await self.set_simultaneous_update(False)
|
||||
|
||||
async def save_state_to_eeprom(self):
|
||||
"""Save current state to EEPROM."""
|
||||
await self._ser_send("S")
|
||||
|
||||
async def set_phase_continuous(self, is_continuous):
|
||||
"""Toggle phase continuous mode.
|
||||
|
||||
Sends the "M n" command. This turns off the automatic
|
||||
clearing of the phase register. In this mode, the phase
|
||||
register is left intact when a command is performed.
|
||||
Use this mode if you want frequency changes to remain
|
||||
phase synchronous, with no phase discontinuities.
|
||||
|
||||
:param is_continuous: True or False
|
||||
"""
|
||||
if is_continuous:
|
||||
await self._ser_send("M n")
|
||||
else:
|
||||
await self._ser_send("M a")
|
||||
|
||||
async def set_simultaneous_update(self, simultaneous):
|
||||
"""Set simultaneous update mode.
|
||||
|
||||
Sends the "I m" command. In this mode an update
|
||||
pulse will not be sent to the DDS chip until
|
||||
an "I p" command is sent. This is useful when it is
|
||||
important to change all the outputs to new values
|
||||
simultaneously.
|
||||
"""
|
||||
if simultaneous:
|
||||
await self._ser_send("I m")
|
||||
else:
|
||||
await self._ser_send("I a")
|
||||
|
||||
async def do_simultaneous_update(self):
|
||||
"""Apply update in simultaneous update mode."""
|
||||
await self._ser_send("I p")
|
||||
|
||||
async def set_freq(self, ch_no, freq):
|
||||
"""Set frequency of one channel."""
|
||||
# Novatech expects MHz
|
||||
await self._ser_send("F{:d} {:f}".format(ch_no, freq/1e6))
|
||||
|
||||
async def set_phase(self, ch_no, phase):
|
||||
"""Set phase of one channel."""
|
||||
# phase word is required by device
|
||||
# N is an integer from 0 to 16383. Phase is set to
|
||||
# N*360/16384 deg; in ARTIQ represent phase in cycles [0, 1]
|
||||
phase_word = round(phase*16383)
|
||||
cmd = "P{:d} {:d}".format(ch_no, phase_word)
|
||||
await self._ser_send(cmd)
|
||||
|
||||
async def set_gain(self, ch_no, volts):
|
||||
"""Set amplitude of one channel."""
|
||||
|
||||
# due to error in Novatech it doesn't generate an error for
|
||||
# dac_value>1024, so need to trap.
|
||||
dac_value = int(math.floor(volts/0.51*1024))
|
||||
if dac_value < 0 or dac_value > 1023:
|
||||
s = "Amplitude out of range {v}".format(v=volts)
|
||||
raise ValueError(s)
|
||||
|
||||
s = "V{:d} {:d}".format(ch_no, dac_value)
|
||||
await self._ser_send(s)
|
||||
|
||||
async def get_status(self):
|
||||
if self.simulation:
|
||||
return ["00989680 2000 01F5 0000 00000000 00000000 000301",
|
||||
"00989680 2000 01F5 0000 00000000 00000000 000301",
|
||||
"00989680 2000 01F5 0000 00000000 00000000 000301",
|
||||
"00989680 2000 01F5 0000 00000000 00000000 000301",
|
||||
"80 BC0000 0000 0102 21"]
|
||||
else:
|
||||
self.port.ser.reset_input_buffer()
|
||||
result = []
|
||||
await self.port.write(("QUE" + "\r\n").encode())
|
||||
for i in range(5):
|
||||
m = (await self._ser_readline()).rstrip().decode()
|
||||
result.append(m)
|
||||
logger.debug("got device status: %s", result)
|
||||
return result
|
||||
|
||||
async def ping(self):
|
||||
try:
|
||||
stat = await self.get_status()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
# check that version number matches is "21"
|
||||
if stat[4][20:] == "21":
|
||||
logger.debug("ping successful")
|
||||
return True
|
||||
else:
|
||||
return False
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,11 @@
|
|||
ARTIQ experiment examples
|
||||
=========================
|
||||
|
||||
This directory contains several sample ARTIQ master configurations
|
||||
and associated experiments that illustrate basic usage of various
|
||||
hardware and software features.
|
||||
|
||||
New users might want to peruse the ``no_hardware`` directory to
|
||||
explore the argument/dataset machinery without needing access to
|
||||
hardware, and the ``kc705_nist_clock`` directory for inspiration
|
||||
on how to coordinate between host and FPGA core device code.
|
|
@ -34,7 +34,7 @@
|
|||
"import pandas as pd\n",
|
||||
"import h5py\n",
|
||||
"\n",
|
||||
"from artiq.protocols.pc_rpc import (Client, AsyncioClient,\n",
|
||||
"from sipyco.pc_rpc import (Client, AsyncioClient,\n",
|
||||
" BestEffortClient, AutoTarget)\n",
|
||||
"from artiq.master.databases import DeviceDB\n",
|
||||
"from artiq.master.worker_db import DeviceManager"
|
||||
|
@ -72,8 +72,8 @@
|
|||
"assert lda.get_attenuation() == 42\n",
|
||||
"\n",
|
||||
"# ... or we can wire it up ourselves if you know where it is\n",
|
||||
"assert ddb.get(\"lda\")[\"host\"] == \"::1\"\n",
|
||||
"assert ddb.get(\"lda\")[\"port\"] == 3253\n",
|
||||
"assert ddb.get(\"lda\", resolve_alias=True)[\"host\"] == \"::1\"\n",
|
||||
"assert ddb.get(\"lda\", resolve_alias=True)[\"port\"] == 3253\n",
|
||||
"\n",
|
||||
"# there are different Client types tailored to different use cases:\n",
|
||||
"\n",
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
core_addr = "sayma1.lab.m-labs.hk"
|
||||
|
||||
device_db = {
|
||||
"core": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.core",
|
||||
"class": "Core",
|
||||
"arguments": {"host": core_addr, "ref_period": 2e-9}
|
||||
},
|
||||
"core_log": {
|
||||
"type": "controller",
|
||||
"host": "::1",
|
||||
"port": 1068,
|
||||
"command": "aqctl_corelog -p {port} --bind {bind} " + core_addr
|
||||
},
|
||||
"core_cache": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.cache",
|
||||
"class": "CoreCache"
|
||||
},
|
||||
|
||||
"led0": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLOut",
|
||||
"arguments": {"channel": 0},
|
||||
},
|
||||
"led1": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLOut",
|
||||
"arguments": {"channel": 1},
|
||||
},
|
||||
"led2": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLOut",
|
||||
"arguments": {"channel": 2},
|
||||
},
|
||||
"led3": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLOut",
|
||||
"arguments": {"channel": 3},
|
||||
},
|
||||
"ttl_sma_out": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLInOut",
|
||||
"arguments": {"channel": 4}
|
||||
},
|
||||
"ttl_sma_in": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLInOut",
|
||||
"arguments": {"channel": 5}
|
||||
},
|
||||
|
||||
"rled0": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLOut",
|
||||
"arguments": {"channel": 0x010000},
|
||||
},
|
||||
"rled1": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLOut",
|
||||
"arguments": {"channel": 0x010001},
|
||||
},
|
||||
"rled2": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLOut",
|
||||
"arguments": {"channel": 0x010002},
|
||||
},
|
||||
"rled3": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLOut",
|
||||
"arguments": {"channel": 0x010003},
|
||||
},
|
||||
"rttl_sma_out": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLInOut",
|
||||
"arguments": {"channel": 0x010004}
|
||||
},
|
||||
"rttl_sma_in": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ttl",
|
||||
"class": "TTLInOut",
|
||||
"arguments": {"channel": 0x010005}
|
||||
},
|
||||
|
||||
"converter_spi": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.spi",
|
||||
"class": "NRTSPIMaster",
|
||||
},
|
||||
"ad9154_spi0": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ad9154_spi",
|
||||
"class": "AD9154",
|
||||
"arguments": {"spi_device": "converter_spi", "chip_select": 2}
|
||||
},
|
||||
"ad9154_spi1": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ad9154_spi",
|
||||
"class": "AD9154",
|
||||
"arguments": {"spi_device": "converter_spi", "chip_select": 3}
|
||||
},
|
||||
"rconverter_spi": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.spi",
|
||||
"class": "NRTSPIMaster",
|
||||
"arguments": {"busno": 0x010000}
|
||||
},
|
||||
"rad9154_spi0": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ad9154_spi",
|
||||
"class": "AD9154",
|
||||
"arguments": {"spi_device": "rconverter_spi", "chip_select": 2}
|
||||
},
|
||||
"rad9154_spi1": {
|
||||
"type": "local",
|
||||
"module": "artiq.coredevice.ad9154_spi",
|
||||
"class": "AD9154",
|
||||
"arguments": {"spi_device": "rconverter_spi", "chip_select": 3}
|
||||
},
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
from artiq.coredevice.ad9154_reg import *
|
||||
from artiq.experiment import *
|
||||
|
||||
|
||||
class Test(EnvExperiment):
|
||||
def build(self):
|
||||
self.setattr_device("core")
|
||||
self.ad9154_spi = self.get_device("ad9154_spi0")
|
||||
self.rad9154_spi = self.get_device("rad9154_spi0")
|
||||
|
||||
@kernel
|
||||
def run(self):
|
||||
self.ad9154_spi.setup_bus()
|
||||
self.rad9154_spi.setup_bus()
|
||||
|
||||
for i in range(5):
|
||||
self.p("local PRODID: 0x%04x", (self.ad9154_spi.read(AD9154_PRODIDH) << 8) |
|
||||
self.ad9154_spi.read(AD9154_PRODIDL))
|
||||
self.p("remote PRODID: 0x%04x", (self.rad9154_spi.read(AD9154_PRODIDH) << 8) |
|
||||
self.rad9154_spi.read(AD9154_PRODIDL))
|
||||
|
||||
def p(self, f, *a):
|
||||
print(f % a)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue