├── IpyNotebooks
├── dds_capture.png
├── ideal_recording_capture.png
├── iio_context_test.ipynb
├── pluto_test.ipynb
└── rx_recording_capture.png
├── LGPLv3_LICENSE.txt
├── LICENSE
├── README.md
├── pluto
├── VersionHistory.txt
├── __init__.py
├── controls.py
├── fir_tools.py
├── iio_lambdas.py
├── iio_tools.py
├── pluto_dds.py
├── pluto_fir.py
├── pluto_sdr.py
├── readFilter.py
└── version.py
├── setup.py
└── test
├── LTE1p4_MHz.ftr
├── __init__.py
├── testDdsClass.py
├── testDdsTone.py
├── testFilterRead.py
├── testFirConfig.py
└── testPlutoSdr.py
/IpyNotebooks/dds_capture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radiosd/PlutoSdr/11b23f2f07b87454926e931ad5dfef4e9cad0ab6/IpyNotebooks/dds_capture.png
--------------------------------------------------------------------------------
/IpyNotebooks/ideal_recording_capture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radiosd/PlutoSdr/11b23f2f07b87454926e931ad5dfef4e9cad0ab6/IpyNotebooks/ideal_recording_capture.png
--------------------------------------------------------------------------------
/IpyNotebooks/iio_context_test.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 39,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "# checking connection via iio library and looking at internal devices\n",
10 | "# an iio context has internal attributes as well as 1 or more devices, then\n",
11 | "# each iio device has internal attributes as well as 1 or more channels, and\n",
12 | "# channels are for streaming data, input or output with attributes for control "
13 | ]
14 | },
15 | {
16 | "cell_type": "code",
17 | "execution_count": 40,
18 | "metadata": {},
19 | "outputs": [],
20 | "source": [
21 | "import iio\n",
22 | "pluto = iio.Context('ip:pluto.local') # connect to pluto hardware"
23 | ]
24 | },
25 | {
26 | "cell_type": "code",
27 | "execution_count": 41,
28 | "metadata": {},
29 | "outputs": [
30 | {
31 | "name": "stdout",
32 | "output_type": "stream",
33 | "text": [
34 | "adm1177 2\n",
35 | "ad9361-phy 9\n",
36 | "xadc 10\n",
37 | "cf-ad9361-dds-core-lpc 6\n",
38 | "cf-ad9361-lpc 2\n"
39 | ]
40 | }
41 | ],
42 | "source": [
43 | "# there are 5 devices in the pluto iio context each with various channels\n",
44 | "for dev in pluto.devices:\n",
45 | " print('{:24s}{:d}'.format(dev.name, len(dev.channels)))\n",
46 | " \n",
47 | "# there are 3 relevant RF devices:\n",
48 | "# ad9361-phy where the internals control the analogue RF params of rx and rx \n",
49 | "# e.g. filter BWs, gain, agc, LO \n",
50 | "# cf-ad9361-lpc where the 2 channels are the IQ data from the ADC\n",
51 | "# cf-ad9361-dds... where 2 channels are the DAC IQ and the other 4 are IQ, F1/F2 of the "
52 | ]
53 | },
54 | {
55 | "cell_type": "code",
56 | "execution_count": 42,
57 | "metadata": {},
58 | "outputs": [],
59 | "source": [
60 | "# picking these out for futher examination\n",
61 | "phy = pluto.find_device('ad9361-phy')\n",
62 | "rx = pluto.find_device('cf-ad9361-lpc')\n",
63 | "tx = pluto.find_device('cf-ad9361-dds-core-lpc')"
64 | ]
65 | },
66 | {
67 | "cell_type": "code",
68 | "execution_count": 43,
69 | "metadata": {},
70 | "outputs": [],
71 | "source": [
72 | "# in the phy, these are the most interesting\n",
73 | "rx_control = phy.find_channel('voltage0', is_output=False)\n",
74 | "tx_control = phy.find_channel('voltage0', is_output=True)"
75 | ]
76 | },
77 | {
78 | "cell_type": "code",
79 | "execution_count": 44,
80 | "metadata": {},
81 | "outputs": [
82 | {
83 | "name": "stdout",
84 | "output_type": "stream",
85 | "text": [
86 | "rx control\n",
87 | " dict_keys(['bb_dc_offset_tracking_en', 'filter_fir_en', 'gain_control_mode', 'gain_control_mode_available', 'hardwaregain', 'hardwaregain_available', 'quadrature_tracking_en', 'rf_bandwidth', 'rf_bandwidth_available', 'rf_dc_offset_tracking_en', 'rf_port_select', 'rf_port_select_available', 'rssi', 'sampling_frequency', 'sampling_frequency_available'])\n",
88 | "tx control\n",
89 | " dict_keys(['filter_fir_en', 'hardwaregain', 'hardwaregain_available', 'rf_bandwidth', 'rf_bandwidth_available', 'rf_port_select', 'rf_port_select_available', 'rssi', 'sampling_frequency', 'sampling_frequency_available'])\n"
90 | ]
91 | }
92 | ],
93 | "source": [
94 | "# the attrs dict shows what can be controlled\n",
95 | "print('rx control\\n', rx_control.attrs.keys())\n",
96 | "print('tx control\\n', tx_control.attrs.keys())"
97 | ]
98 | },
99 | {
100 | "cell_type": "code",
101 | "execution_count": 45,
102 | "metadata": {},
103 | "outputs": [
104 | {
105 | "name": "stdout",
106 | "output_type": "stream",
107 | "text": [
108 | "rx_adc\n",
109 | " dict_keys(['calibbias', 'calibphase', 'calibscale', 'samples_pps', 'sampling_frequency', 'sampling_frequency_available'])\n"
110 | ]
111 | }
112 | ],
113 | "source": [
114 | "# for the rx, the channels are IQ data paths with attrs for dc offset (bias) and phase calibration\n",
115 | "print('rx_adc\\n', rx.channels[0].attrs.keys())"
116 | ]
117 | },
118 | {
119 | "cell_type": "code",
120 | "execution_count": 46,
121 | "metadata": {},
122 | "outputs": [
123 | {
124 | "name": "stdout",
125 | "output_type": "stream",
126 | "text": [
127 | "tx_dac\n",
128 | " dict_keys(['calibphase', 'calibscale', 'sampling_frequency', 'sampling_frequency_available'])\n"
129 | ]
130 | }
131 | ],
132 | "source": [
133 | "# for the tx, 2 of the channels are IQ data paths for the dac\n",
134 | "tx_dac = tx.find_channel('voltage0', True)\n",
135 | "print('tx_dac\\n', tx_dac.attrs.keys())\n",
136 | "# the Q channel is 'voltage1'"
137 | ]
138 | },
139 | {
140 | "cell_type": "code",
141 | "execution_count": 47,
142 | "metadata": {},
143 | "outputs": [
144 | {
145 | "name": "stdout",
146 | "output_type": "stream",
147 | "text": [
148 | "dds_F1\n",
149 | " dict_keys(['frequency', 'phase', 'raw', 'sampling_frequency', 'scale'])\n",
150 | "dds_F1\n",
151 | " dict_keys(['frequency', 'phase', 'raw', 'sampling_frequency', 'scale'])\n"
152 | ]
153 | }
154 | ],
155 | "source": [
156 | "# the others control internally generated IQ data for F1 and/or F2 DDS signals \n",
157 | "tx_f1 = tx.find_channel('altvoltage0', True)\n",
158 | "print('dds_F1\\n', tx_f1.attrs.keys())\n",
159 | "tx_f2 = tx.find_channel('altvoltage1', True)\n",
160 | "print('dds_F1\\n', tx_f2.attrs.keys())\n",
161 | "# the corresponding Q channels are altvoltage2 and altvoltage3"
162 | ]
163 | },
164 | {
165 | "cell_type": "code",
166 | "execution_count": null,
167 | "metadata": {},
168 | "outputs": [],
169 | "source": []
170 | }
171 | ],
172 | "metadata": {
173 | "kernelspec": {
174 | "display_name": "Python 3",
175 | "language": "python",
176 | "name": "python3"
177 | },
178 | "language_info": {
179 | "codemirror_mode": {
180 | "name": "ipython",
181 | "version": 3
182 | },
183 | "file_extension": ".py",
184 | "mimetype": "text/x-python",
185 | "name": "python",
186 | "nbconvert_exporter": "python",
187 | "pygments_lexer": "ipython3",
188 | "version": "3.7.3"
189 | }
190 | },
191 | "nbformat": 4,
192 | "nbformat_minor": 2
193 | }
194 |
--------------------------------------------------------------------------------
/IpyNotebooks/rx_recording_capture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radiosd/PlutoSdr/11b23f2f07b87454926e931ad5dfef4e9cad0ab6/IpyNotebooks/rx_recording_capture.png
--------------------------------------------------------------------------------
/LGPLv3_LICENSE.txt:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PlutoSDR
5 | ========
6 |
7 | A package for access and control the PlutSDR hardware. The PlutoSdr file defines a class for control of
8 | the hardware. There is a 2 tone DDS generator built into the Tx firmware and a separate file and
9 | class used to define the control of that. An instance of PlutoSdr automatically creates the DDS
10 |
11 | Getting Started
12 | ---------------
13 | Dependancies:
14 |
15 | iio
16 | - clone from https://github.com/analogdevicesinc/libiio.git and
17 | - follow standard cmake, built etc
18 | - the iio library is installed using setup.py in bindings/python
19 |
20 | changeExt
21 | - clone from https://github.com/radiosd/rsdLib.git
22 | - use setup.py to install
23 |
24 | Installation
25 | ------------
26 | Installation of PlutoSdr is via the standard python setup.py install. Then:
27 |
28 | ```python
29 | from pluto.pluto_sdr import PlutoSdr
30 |
31 | sdr = PlutoSdr()
32 | ```
33 |
34 | The example above uses the default url for creating the PlutoSdr class instance. The instance has properties to control RF functions of both the Rx and the Tx as well as the internal DDS to transmit up to 2 tones for testing. In general, frequency controls are in MHz and amplitude controls are dBfs. There are also functions to readRx() and writeTx() samples, providing a straight forward interface to the RF hardware. Data can be transferred via numpy arrays either as interleaved IQ np.int16 or complex floats via np.complex128.
35 |
36 | Testing
37 | -------
38 | Basic unittests are included, but are limited to confirming the operation of properies and simple functions.
39 |
40 | python -m unittest discover
41 |
42 | Tests using the hardware such as transmitting and receiving data can be tried using the ipython notebooks included.
43 | * iio_context_test.ipynb
44 | * Demonstrating access to the internal devices using the iio module
45 | * pluto_test.ipynb
46 | * Demonstrating readRx and writeTx functions
47 |
48 | License
49 | -------
50 | This software is Copyright (C) 2018 Radio System Design Ltd. and released under GNU Lesser General Public License. See the license file in the repository for details.
--------------------------------------------------------------------------------
/pluto/VersionHistory.txt:
--------------------------------------------------------------------------------
1 | PlutoSdr
2 |
3 | Ver 1.0.1x Initial release good control of all RF parameters for both Rx
4 | abd Tx. Some useful tests, but see ToDo.
5 |
6 | Ver 1.0.2 Corrections and modifications with inclusion of LGPLv2 license
7 | conditions
8 |
9 | Ver 1.1.x More thorough debugging of readRx, writeTx, implementation of
10 | capture and playback. Some hardware testing (demontration)
11 | using ipython notebooks
12 |
13 | ToDo:
14 | *) Incomplete testing of PlutoSdr
15 | - cannot see how to test code without the device connected
16 | *) Limited implementation of rx/tx fir control
17 | - Unclear how to creat filter files without matlab
18 |
--------------------------------------------------------------------------------
/pluto/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radiosd/PlutoSdr/11b23f2f07b87454926e931ad5dfef4e9cad0ab6/pluto/__init__.py
--------------------------------------------------------------------------------
/pluto/controls.py:
--------------------------------------------------------------------------------
1 | """
2 | Constants for contol of pluto sdr and dds
3 | rgr01aug18
4 | * Copyright (C) 2018 Radio System Design Ltd.
5 | * Author: Richard G. Ranson, richard@radiosystemdesign.com
6 | *
7 | * This library is free software; you can redistribute it and/or
8 | * modify it under the terms of the GNU Lesser General Public
9 | * License as published by the Free Software Foundation under
10 | * version 2.1 of the License.
11 | *
12 | * This library is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 | * Lesser General Public License for more details.
16 | """
17 |
18 | # for setting the state of devices
19 | ON = True
20 | OFF = False
21 |
22 | # for data conversion
23 | import numpy
24 | FLOAT = numpy.float64 # keep 2:1 proportion so that np.view() performs
25 | COMPLEX = numpy.complex128 # a fast in place conversion of interleaved IQ
26 |
27 | def devFind(ctx, name):
28 | """find an iio_device by name or raise and exception"""
29 | dev = ctx.find_device(name)
30 | if dev is None:
31 | raise NameError('device '+name+' not found')
32 | return dev
33 |
34 |
--------------------------------------------------------------------------------
/pluto/fir_tools.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | Basic FIR designs and plotting
4 | rgr14Aug18
5 | * Copyright (C) 2018 Radio System Design Ltd.
6 | * Author: Richard G. Ranson, richard@radiosystemdesign.com
7 | *
8 | * This library is free software; you can redistribute it and/or
9 | * modify it under the terms of the GNU Lesser General Public
10 | * License as published by the Free Software Foundation under
11 | * version 2.1 of the License.
12 | *
13 | * This library is distributed in the hope that it will be useful,
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 | * Lesser General Public License for more details.
17 | """
18 | from __future__ import print_function
19 |
20 | from scipy import signal as sig
21 |
22 | PASS_TYPES = ('LPF', 'HPF', 'BPF')
23 | PLOT_COLOURS = ('b', 'r')
24 |
25 | def lpf(n, cutoff, window='hamming'):
26 | return sig.firwin(n, cutoff, window=window)
27 |
28 | def hpf(n, cutoff, window='hanning'):
29 | return sig.firwin(n, cutoff, window=window, pass_zero=False)
30 |
31 | def bpf(n, lower_cutoff, upper_cutoff, window='blackmanharris'):
32 | a1 = lpf(n, lower_cutoff, window)
33 | a2 = hpf(n, upper_cutoff, window)
34 | # combine to for BPF
35 | return -(a1 + a2)
36 |
37 |
38 | def fir_taps(pass_type, n, cutoff, window):
39 | if not pass_types.upper() in PASS_TYPES:
40 | print('unknown: filter type ' + pass_type)
41 | return
42 | pass
43 |
44 | from matplotlib import pyplot as plt
45 | import numpy as np
46 |
47 | def fir_plot(b, a=1, grid=True, phase=False):
48 | """plot magnitude in dB and optionally phase together"""
49 | w, h = sig.freqz(b, a)
50 | w_norm = w/max(w)
51 | fig = plt.figure()
52 | ax1 = fig.add_subplot(111)
53 | plt.plot(w_norm, 20*np.log10(np.abs(h)), PLOT_COLOURS[0])
54 | plt.title('FIR Frequency Response')
55 | plt.xlabel('Normalised Frequency [rads/sample]')
56 | plt.ylabel('Amplidude [dB]', color=PLOT_COLOURS[0])
57 | if phase:
58 | ax2 = ax1.twinx()
59 | rads = np.unwrap(np.angle(h))
60 | plt.plot(w_norm, rads, PLOT_COLOURS[1])
61 | plt.ylabel('Phase [rads/sample]', color=PLOT_COLOURS[1])
62 | if grid:
63 | plt.grid()
64 | #plt.axis('tight')
65 | plt.show()
66 |
67 | if __name__=='__main__':
68 | pass
69 |
70 |
--------------------------------------------------------------------------------
/pluto/iio_lambdas.py:
--------------------------------------------------------------------------------
1 | """
2 | Functions to read and write values tovarious iio Attribute classes
3 | They all use strings to represent numbers, some with value scaling
4 | and some with units appended to the end.
5 | rgrjul2518
6 | * Copyright (C) 2018 Radio System Design Ltd.
7 | * Author: Richard G. Ranson, richard@radiosystemdesign.com
8 | *
9 | * This library is free software; you can redistribute it and/or
10 | * modify it under the terms of the GNU Lesser General Public
11 | * License as published by the Free Software Foundation under
12 | * version 2.1 of the License.
13 | *
14 | * This library is distributed in the hope that it will be useful,
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 | * Lesser General Public License for more details.
18 | """
19 |
20 | # frequencies are in MHz, but the value set is a str in Hz
21 | _M2Str = lambda x: str(int(x*1e6)) # convert MHz float to string in Hz
22 | _Str2M = lambda x: float(x)/1e6 # and back
23 | # phase is set in degrees to 3 decimal places but stored as a str in degs*1000
24 | _P2Str = lambda x: str(int(round(x,3)*1e3)) # convert degs float*1000 to string
25 | _Str2P = lambda x :float(x)/1e3 # and back
26 | # phase is normalised to 0 <= x <= 360
27 | _PNorm = lambda x: _PNorm(x+360) if x<0 else _PNorm(x-360) if x>360 else x
28 | # amplitude value
29 | _Str2A = lambda x :float(x) # convert string amplitude to float
30 |
31 |
--------------------------------------------------------------------------------
/pluto/iio_tools.py:
--------------------------------------------------------------------------------
1 | """
2 | Tools to access iio structures and internal attributes
3 | Context is the top level with attributes and a list of devices
4 | Devices have attributes and a list of channels
5 | Channels are input or output to transfer data with control attibutes
6 |
7 | rgr15jul18
8 | * Copyright (C) 2018 Radio System Design Ltd.
9 | * Author: Richard G. Ranson, richard@radiosystemdesign.com
10 | *
11 | * This library is free software; you can redistribute it and/or
12 | * modify it under the terms of the GNU Lesser General Public
13 | * License as published by the Free Software Foundation under
14 | * version 2.1 of the License.
15 | *
16 | * This library is distributed in the hope that it will be useful,
17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 | * Lesser General Public License for more details.
20 | """
21 | from __future__ import print_function
22 |
23 | import iio
24 |
25 | def iioFind(iio_item, name):
26 | """locate and return the named item from that given"""
27 | if iio_item is None:
28 | raise NameError('device '+name+' not found')
29 | if isinstance(iio_item, iio.Context):
30 | found = iio_item.find_device(name)
31 | if isinstance(iio_item, iio.Device):
32 | found = iio_item.find_channel(name)
33 | return found
34 |
35 |
36 | def iioList(item):
37 | """show information on the iio_class instance given"""
38 | # info is appropriate for the class supplied
39 | if isinstance(item, iio.Context):
40 | print(listContext(item))
41 | elif isinstance(item, iio.Device):
42 | print(listDevice(item))
43 | elif isinstance(item, iio.Channel):
44 | print(listChannel(item))
45 | elif isinstance(item, list):
46 | for each in item:
47 | iioList(each)
48 | elif isinstance(item, dict): # all the various Attr classes
49 | for k, v in item.items():
50 | if isinstance(v, str):
51 | value = v
52 | else:
53 | value = _showAttr(v)
54 | print(k, ':', value)
55 | else:
56 | print('unknown item:')
57 | for k, v in item.items():
58 | print(k, v.value, ',', end='')
59 |
60 | def _showAttr(attr):
61 | try:
62 | v = attr.value
63 | except Exception as e:
64 | v = type(e) # + ' '.join(e.args)
65 | return v
66 |
67 | def _getAttrs(item):
68 | try:
69 | ats = len(item.attrs)
70 | except AttributeError:
71 | ats = 0
72 | return ats
73 |
74 | def _getDebugAttrs(item):
75 | try:
76 | d_ats = len(item.debug_attrs)
77 | except AttributeError:
78 | d_ats = 0
79 | return d_ats
80 |
81 | def listContext(item):
82 | """return summary of context properties"""
83 | attrs = _getAttrs(item)
84 | debug_attrs = _getDebugAttrs(item)
85 | return 'name:{:s}, attrs({:d}), devices({:d}), debug_attrs({:d})'\
86 | .format(item.name, attrs, len(item.devices), debug_attrs)
87 |
88 | def listDevice(dev):
89 | """return summary of device properties"""
90 | name = '_' if dev.name is None else dev.name
91 | idx = '_' if dev.id is None else dev.id
92 | try:
93 | ats = len(dev.attrs)
94 | except AttributeError:
95 | ats = 0
96 | chs = len(dev.channels)
97 | try:
98 | d_ats = len(dev.debug_attrs)
99 | except AttributeError:
100 | d_ats = 0
101 | return '{:s}, name:{:s}, attrs({:d}), channels({:d}), debug_attrs({:d})'\
102 | .format(idx, name, ats, chs, d_ats)
103 |
104 | def listChannel(item):
105 | """return summary of channel properties"""
106 | attrs = _getAttrs(item)
107 | idx = '_' if item.id is None else item.id
108 | name = '_' if item.name is None else item.name
109 | io = 'output' if item.output else 'input'
110 | return 'id:{:s} name:{:s}, attrs({:d}), {:s}'\
111 | .format(idx, name, attrs, io)
112 |
113 | if __name__=='__main__':
114 | from pluto_sdr import PlutoSdr
115 | pp = PlutoSdr()
116 | iioList(pp.ctx) # a context
117 | iioList(pp.dds) # a device
118 | iioList(pp.ctx.devices) # device list
119 | iioList(pp.dds.channels[0]) # a channel
120 | cc0 = pp.dds.channels[0]
121 | catt = cc0.attrs['phase'] # ChannelAttr
122 | iioList(pp.dds.channels) # a channel list
123 |
124 | dd1 = pp.ctx.devices[1]
125 | datt = dd1.attrs['calib_mode_available'] # device DebugAttr
126 | ddatt = dd1.debug_attrs['digital_tune']
127 | cc = pp.dds.channels
128 |
129 | #iioList(dd1.attrs)
130 | #iioList(dd1.debug_attrs)
131 |
--------------------------------------------------------------------------------
/pluto/pluto_dds.py:
--------------------------------------------------------------------------------
1 | """
2 | Tools to access dds device in pluto
3 | rgr15jul18
4 | * Copyright (C) 2018 Radio System Design Ltd.
5 | * Author: Richard G. Ranson, richard@radiosystemdesign.com
6 | *
7 | * This library is free software; you can redistribute it and/or
8 | * modify it under the terms of the GNU Lesser General Public
9 | * License as published by the Free Software Foundation under
10 | * version 2.1 of the License.
11 | *
12 | * This library is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 | * Lesser General Public License for more details.
16 | """
17 | from __future__ import print_function
18 |
19 | import logging
20 |
21 | from math import copysign, log10
22 | from pluto import iio_lambdas as iiol
23 | from pluto.controls import ON, OFF
24 |
25 | class DdsTone(object):
26 | def __init__(self, owner, name):
27 | self.i_ch = owner.find_channel('TX1_I_'+ name)
28 | self.q_ch = owner.find_channel('TX1_Q_'+ name)
29 | self.amplitude = 0 # initially off
30 | self._freq = 0
31 | self._phase = 0
32 |
33 | def getSamplingFreq(self):
34 | """convenience function for testing the valid range for setFreq"""
35 | return iiol._Str2M(self.i_ch.attrs['sampling_frequency'].value)
36 |
37 | def getFreq(self):
38 | """get the actual frequency set in MHz with I/Q phase giving +/-"""
39 | i_phase = self.getPhase()
40 | q_phase = self.getPhase('Q')
41 | f = iiol._Str2M(self.i_ch.attrs['frequency'].value)
42 | if iiol._PNorm(i_phase - q_phase)<180:
43 | return f
44 | else:
45 | return -f
46 |
47 | def setFreq(self, f):
48 | """set to approximately the frequency given in MHz"""
49 | # approx because the actual freq is constrained by the algorithm
50 | # read back from the device to determine the actual value set
51 | self._freq = f
52 | # must be 0 <= f < fs/2
53 | if abs(f)>self.getSamplingFreq()/2:
54 | half_fs = '{:2.3f}'.format(self.getSamplingFreq()/2)
55 | raise ValueError('frequency not within +/-Fs/2 i.e '+half_fs+'MHz')
56 | # for positive frequencies phase(Q) = phase(I) - 90
57 | # for negative frequencies abs(f) and phase(Q) = phase(I) + 90
58 | logging.debug('freq:>' + iiol._M2Str(f))
59 | self.__setFreq()
60 | self.__setPhase()
61 |
62 | def __setFreq(self):
63 | self.i_ch.attrs['frequency'].value = iiol._M2Str(abs(self._freq))
64 | # read back the actual value
65 | self._freq = copysign(self.getFreq(), self._freq)
66 | logging.debug('i_ch:< '+iiol._M2Str(abs(self._freq)))
67 | self.q_ch.attrs['frequency'].value = iiol._M2Str(abs(self._freq))
68 | logging.debug('q_ch:< '+iiol._M2Str(abs(self._freq)))
69 | frequency = property(getFreq, setFreq)
70 |
71 | def getPhase(self, ch='I'):
72 | """get the actual phase set in degs"""
73 | if ch.upper()=='I':
74 | return iiol._Str2P(self.i_ch.attrs['phase'].value)
75 | else:
76 | return iiol._Str2P(self.q_ch.attrs['phase'].value)
77 |
78 | def setPhase(self, phi):
79 | """set the phase given in degrees"""
80 | # 0 <= phi < 360
81 | self._phase = iiol._PNorm(phi) # silent error, PNorm resolves it
82 | logging.debug('phase:> ' + str(int(round(phi,3)*1000))+'->'+
83 | str(int(round(self._phase,3)*1000)))
84 | self.__setPhase()
85 | self.__setFreq()
86 |
87 | def __setPhase(self):
88 | self.i_ch.attrs['phase'].value = iiol._P2Str(self._phase)
89 | # read back the actual value
90 | self._phase = self.getPhase()
91 | logging.debug('i_ch:< '+iiol._P2Str(self._phase))
92 | ph2 = iiol._PNorm(self._phase - copysign(90, self._freq))
93 | self.q_ch.attrs['phase'].value = iiol._P2Str(ph2)
94 | logging.debug('q_ch:< '+iiol._P2Str(ph2))
95 | phase = property(getPhase, setPhase)
96 |
97 | def getAmplitude(self):
98 | """get the actual amplitude set in dBs"""
99 | return iiol._Str2A(self.i_ch.attrs['scale'].value)
100 |
101 | def setAmplitude(self, amp):
102 | """set the amplitude given with 0 <= amp <= 1"""
103 | if amp<0 or amp>1:
104 | raise ValueError('amplitude must be positive and unity maximum')
105 | # self._amp = amp
106 | self._setAmplitude(amp)
107 |
108 | def _setAmplitude(self, amp):
109 | logging.debug('amp:>'+'{:1.6f}'.format(round(amp,6)))
110 | self.i_ch.attrs['scale'].value = '{:1.6f}'.format(round(amp,6))
111 | self.q_ch.attrs['scale'].value = '{:1.6f}'.format(round(amp,6))
112 | amplitude = property(getAmplitude, setAmplitude)
113 |
114 | ## def state(self, value):
115 | ## """control the on/off state of the tone"""
116 | ## if value==OFF:
117 | ## logging.debug('off')
118 | ## self._amp = self.amplitude
119 | ## self._setAmplitude(0)
120 | ## else:
121 | ## logging.debug('on')
122 | ## self.amplitude = self._amp
123 |
124 | def status(self):
125 | return (self.amplitude, self.frequency, self.phase)
126 |
127 | def _showCh(self, iq):
128 | if iq.upper()=='I':
129 | ch = self.i_ch.attrs
130 | else:
131 | ch = self.q_ch.attrs
132 | print('{:s} {:s} {:s} {:s}'.format(iq.upper(),
133 | ch['scale'].value, ch['frequency'].value, ch['phase'].value))
134 |
135 | def showTone(self):
136 | """ ... """
137 | self._showCh('I')
138 | self._showCh('Q')
139 |
140 | class Dds(object):
141 | def __init__(self, dev):
142 | self.device = dev
143 | logging.debug('create Dds instance')
144 | self.t1 = DdsTone(dev, 'F1')
145 | self.t2 = DdsTone(dev, 'F2')
146 | # 4 dac output channels (i, q) each for t1 and t2
147 | self.channels = [dev.find_channel('altvoltage0', True)]
148 | self.channels.append(dev.find_channel('altvoltage1', True))
149 | self.channels.append(dev.find_channel('altvoltage2', True))
150 | self.channels.append(dev.find_channel('altvoltage3', True))
151 | # initialise with both off and init with 0 amplitude
152 | self.state(OFF);
153 |
154 | def getSamplingFreq(self):
155 | """return the sampling freq in MHz. Read only"""
156 | return self.t1.getSamplingFreq()
157 |
158 | def setAmplitude(self, amp1, amp2=None):
159 | """set amplitude of the tones in dB, deprecated, default None turns off """
160 | # now use state(value) to turn on/off
161 | if amp1<=0:
162 | self.t1.amplitude = 10**(amp1/10.0)
163 | else:
164 | raise ValueError('tone amplitudes set in -dB levels')
165 | if amp2 is None:
166 | self.t2.setAmplitude(0)
167 | elif amp2<=0:
168 | self.t2.amplitude = 10**(amp2/10.0)
169 | else:
170 | raise ValueError('tone amplitudes set in -dB levels')
171 |
172 | def setFrequency(self, f1, f2=None):
173 | self.t1.setFreq(f1)
174 | if f2 is not None:
175 | self.t2.setFreq(f2)
176 |
177 | def setPhase(self, ph1, ph2=None):
178 | self.t1.setPhase(ph1)
179 | if ph2 is not None:
180 | self.t2.setphase(ph2)
181 |
182 | def state(self, value):
183 | """set on/off state for both DDS tones"""
184 | # this is independent of the amplidude
185 | # possibly only need to switch 1 but do all 4
186 | for ch in self.channels:
187 | if value==OFF:
188 | ch.attrs['raw'].value = '0'
189 | else:
190 | ch.attrs['raw'].value = '1'
191 |
192 | def isOff(self):
193 | return self.channels[0].attrs['raw'].value=='0' # only test 1
194 |
195 | def status(self, tone=1):
196 | tone = self.t1 if tone==1 else self.t2
197 | amp = 99.9 if tone.amplitude==0 else tone.amplitude
198 | return (tone.frequency, 'MHz', tone.phase, 10*log10(amp),'dBFS')
199 |
200 | if __name__=='__main__':
201 | import sys
202 | import iio
203 | logging.basicConfig(format='%(module)-12s.%(funcName)-12s:%(levelname)s - %(message)s',
204 | stream=sys.stdout, level=logging.DEBUG)
205 | try:
206 | ctx = iio.Context('ip:pluto.local')
207 | dds = ctx.find_device('cf-ad9361-dds-core-lpc')
208 | t1 = DdsTone(dds, 'F1')
209 | except:
210 | # warning: more than 1 thing can go wrong!!
211 | print('requires a pluto to be connected')
212 |
--------------------------------------------------------------------------------
/pluto/pluto_fir.py:
--------------------------------------------------------------------------------
1 | """
2 | Access to and control of pluto fir filters
3 | rgr11Aug18
4 | * Copyright (C) 2018 Radio System Design Ltd.
5 | * Author: Richard G. Ranson, richard@radiosystemdesign.com
6 | *
7 | * This library is free software; you can redistribute it and/or
8 | * modify it under the terms of the GNU Lesser General Public
9 | * License as published by the Free Software Foundation under
10 | * version 2.1 of the License.
11 | *
12 | * This library is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 | * Lesser General Public License for more details.
16 | """
17 | from __future__ import print_function
18 | from rsdLib.fileUtils import changeExt
19 |
20 | # SPI control register locations
21 | TX_COEFF_ADDR = 0x60 # read/write from/to this coeff offset
22 | TX_WRITE_REG = 0x61 # this is lsb, with usb at 0x62
23 | TX_READ_REG = 0x63 # this is lsb, with usb at 0x64
24 | TX_FIR_CONFIG = 0x65 # see txConfig()
25 |
26 | RX_COEFF_ADDR = 0xF0 # read/write from/to this coeff offset
27 | RX_WRITE_REG = 0xF1 # this is lsb, with usb at 0xF2
28 | RX_READ_REG = 0xF3 # this is lsb, with usb at 0xF4
29 | RX_FIR_CONFIG = 0xF5 # see rxConfig()
30 | RX_FIR_GAIN = 0xF6 # <1:0> filter gain
31 |
32 | # registers hold 16 bit 2s compliment values in consecutive LSB, MSB addresses
33 | # helper functions hard coded for two 8 bit bytes
34 | import numpy as np
35 |
36 | def twosC2Int(b1, b0):
37 | """integer from 2 byte twos compliment representation"""
38 | ## v = ((b1<<8) + b0 )
39 | ## return v -(1<<16) if b1 & (1<<7) else v
40 | return np.int16((b1<<8) + b0)
41 |
42 | def int2TwosC(value):
43 | """2 byte twos compliment representation of value given"""
44 | ## b1, b0 = divmod(value, 1<<8)
45 | ## if b1<0 :
46 | ## b1 += 1<<8
47 | b1, b0 = divmod(np.int16(value), 1<<8)
48 | return (np.ubyte(b1), np.ubyte(b0))
49 |
50 | def setBit(index, value, on):
51 | mask = 1< in T or R config register"""
84 | c_reg = self.configReg(trx)
85 | value = self.dev.reg_read(c_reg)
86 | self.dev.reg_write(c_reg, setBit(1, value, on))
87 |
88 | def write(self, trx, on):
89 | """write control is <2> in T or R config register"""
90 | c_reg = self.configReg(trx)
91 | value = self.dev.reg_read(c_reg)
92 | self.dev.reg_write(c_reg, setBit(2, value, on))
93 |
94 | def txConfig(self, no_taps, gain):
95 | """pluto has only 1 tx channel"""
96 | if no_taps>128 and not (no_taps % 16==0):
97 | raise ValueError('invalid number of taps')
98 | # format data for the register
99 | b7_5 = ((no_taps//16) - 1)<<5 # <7:5> number of taps
100 | b4_3 = 3<<3 # <4:3> select TX 1, 2 or 3=both
101 | b2 = 0 # <2> write
102 | b1 = 0 # <1> Start clock
103 | b0 = 0 if gain==0 else 1 # <0> filter gain set = -6dB
104 | value = b7_5 + b4_3 + b2 + b1 + b0
105 | logging.info('Tx config 0x{:x}'.format(value))
106 | self.dev.reg_write(TX_FIR_CONFIG, value)
107 |
108 | def rxConfig(self, no_taps, gain):
109 | """pluto has only 1 rx channel"""
110 | if no_taps>128 and not (no_taps % 16==0):
111 | raise ValueError('invalid number of taps')
112 | # format data for the register
113 | b7_5 = ((no_taps//16) - 1)<<5 # <7:5> number of taps
114 | b4_3 = 3<<3 # <4:3> select RX 1, 2 or 3=both
115 | b2 = 0 # <2> write
116 | b1 = 0 # <1> Start clock
117 | b0 = 0 # reserved
118 | value = b7_5 + b4_3 + b2 + b1 + b0
119 | logging.info('Rx config 0x{:x}'.format(value))
120 | self.dev.reg_write(RX_FIR_CONFIG, value)
121 | # rx gain settings are +6, 0, -6, -12dB
122 | gain = (gain+6)//6
123 | # <1:0> filter gain +6, 0, -6, -12dB
124 | logging.info('Rx gain 0x{:x}'.format(gain))
125 | self.dev.reg_write(RX_FIR_GAIN, gain)
126 | # ------------------------ write to fir ----------------------------
127 | def pushCoeffs(self, trx):
128 | """trigger write of values to T or R fir registers"""
129 | self.dev.reg_write(self.configReg(trx), 0xFE)
130 |
131 | def writeRegPair(self, addr, value):
132 | b1, b0 = int2TwosC(value)
133 | self.dev.reg_write(addr, b0)
134 | self.dev.reg_write(addr+1, b1)
135 |
136 | def writeTx(self, coeffs):
137 | """write coeff vector to the TX fir"""
138 | self.disable()
139 | self.clock('tx', ON)
140 | self.write('tx', ON)
141 | # loop through all writing the values from offset 0
142 | for i in range(len(coeffs)):
143 | self.dev.reg_write(TX_COEFF_ADDR, i)
144 | self.writeRegPair(TX_WRITE_REG, coeffs[i])
145 | self.pushCoeffs('tx')
146 | for i in range(len(coeffs), 128): # zero any remaining
147 | self.dev.reg_write(TX_COEFF_ADDR, i)
148 | self.writeRegPair(TX_WRITE_REG, 0)
149 | self.pushCoeffs('tx')
150 | # disable writing
151 | self.clock('tx', OFF)
152 | self.write('tx', OFF)
153 | self.enable()
154 |
155 | def writeRx(self, coeffs):
156 | """write coeff vector to the RX fir"""
157 | self.disable()
158 | self.clock('rx', ON)
159 | self.write('rx', ON)
160 | # loop through all writing the values from offset 0
161 | for i in range(len(coeffs)):
162 | self.dev.reg_write(RX_COEFF_ADDR, i)
163 | self.writeRegPair(RX_WRITE_REG, coeffs[i])
164 | self.pushCoeffs('rx')
165 | for i in range(len(coeffs), 128): # zero any remaining
166 | self.dev.reg_write(RX_COEFF_ADDR, i)
167 | self.writeRegPair(RX_WRITE_REG, 0)
168 | self.pushCoeffs('rx')
169 | # disable writing
170 | self.clock('rx', OFF)
171 | self.write('rx', OFF)
172 | self.enable()
173 | # ------------------------ read from fir ---------------------------
174 | def readRegPair(self, start_add):
175 | """read consecutive registers, LSB, MSB"""
176 | lsb = self.dev.reg_read(start_add)
177 | usb = self.dev.reg_read(start_add+1)
178 | return twosC2Int(usb, lsb)
179 |
180 | def readTx(self):
181 | """read and return all the TX FIR coeffs"""
182 | self.dev.reg_write(TX_FIR_CONFIG, 0xEA)
183 | coeffs = []
184 | for addr in range(0x80):
185 | self.dev.reg_write(TX_COEFF_ADDR, addr)
186 | coeffs.append(self.readRegPair(TX_READ_REG))
187 | return np.trim_zeros(coeffs) # remove leading/trailing zeros
188 |
189 | def readRx(self):
190 | """read and return all the RX FIR coeffs"""
191 | self.dev.reg_write(RX_FIR_CONFIG, 0xEA)
192 | coeffs = []
193 | for addr in range(0x80):
194 | self.dev.reg_write(RX_COEFF_ADDR, addr)
195 | coeffs.append(self.readRegPair(RX_READ_REG))
196 | return np.trim_zeros(coeffs) # remove leading/trailing zeros
197 | # ------------------- read Tx and Rx firs from file-----------------
198 | def loadFile(self, filename):
199 | """read filter and config data from a ftr file"""
200 | self.disable()
201 | filename = changeExt(filename, 'ftr')
202 | if os.path.isfile(filename):
203 | with open(filename, 'r') as fin:
204 | ftr_file = fin.read()
205 | self.dev.attrs['filter_fir_config'].value = ftr_file
206 | else:
207 | raise FileNotFoundError(filename+' not found')
208 | self.enable()
209 |
210 | if __name__=='__main__':
211 | import sys
212 | import iio
213 | logging.basicConfig(
214 | format='%(module)-12s.%(funcName)-12s:%(levelname)s - %(message)s',
215 | stream=sys.stdout, level=logging.DEBUG)
216 | try: # iio returns None if items are not found
217 | ctx = iio.Context('ip:pluto.local')
218 | if ctx is not None:
219 | dev = ctx.find_device('ad9361-phy')
220 | else:
221 | raise IOError('iio context not found')
222 | fir = FirConfig(dev)
223 |
224 | #fir.loadFile('test/GSM.ftr')
225 | except IOError as er1:
226 | print('requires an ADALM Pluto to be connected')
227 |
--------------------------------------------------------------------------------
/pluto/pluto_sdr.py:
--------------------------------------------------------------------------------
1 | """
2 | Basic class to simplifiy interaction with pluto as an iio device
3 | rgr12jan18
4 | * Copyright (C) 2018 Radio System Design Ltd.
5 | * Author: Richard G. Ranson, richard@radiosystemdesign.com
6 | *
7 | * This library is free software; you can redistribute it and/or
8 | * modify it under the terms of the GNU Lesser General Public
9 | * License as published by the Free Software Foundation under
10 | * version 2.1 of the License.
11 | *
12 | * This library is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 | * Lesser General Public License for more details.
16 | """
17 | # basic class to simplifiy interaction with pluto as an iio device
18 | from __future__ import print_function
19 |
20 | import logging
21 |
22 | import iio
23 | import numpy as np
24 |
25 | PLUTO_ID = 'ip:pluto.local'
26 | NO_BITS = 12 # internal ADC and DAC width
27 |
28 | # properties are in MHz, but the value set is a str
29 | from pluto.iio_lambdas import _M2Str
30 |
31 | from pluto import pluto_dds
32 | from pluto.controls import ON, OFF, FLOAT, COMPLEX
33 |
34 | class PlutoSdr(object):
35 | """Encapsulation of Pluto SDR device
36 | iio lib interface used to expose common functionality
37 | RF signal data read/write capabilities for rx and tx"""
38 | no_bits = NO_BITS
39 | TX_OFF = 0
40 | TX_DMA = 1
41 | TX_DDS = 2
42 | def __init__(self, uri=PLUTO_ID):
43 | # access to internal devices
44 | try:
45 | self.ctx = iio.Context(uri)
46 | except OSError:
47 | self.ctx = None
48 | print('exception: no iio device context found at',uri)
49 | return
50 | logging.debug('found context for pluto device')
51 | self.name = 'plutosdr'
52 | self.phy = self.ctx.find_device('ad9361-phy')
53 | # individual TRx controls
54 | self.phy_rx = self.phy.find_channel('voltage0', is_output=False)
55 | self.phy_tx = self.phy.find_channel('voltage0', is_output=True)
56 | # access to data channels for Rx
57 | self.adc = self.ctx.find_device('cf-ad9361-lpc')
58 | # access to data channels for Tx
59 | self.dac = self.ctx.find_device('cf-ad9361-dds-core-lpc')
60 | self.tx_channels = [self.dac.find_channel('voltage0', True)]
61 | self.tx_channels.append(self.dac.find_channel('voltage1', True))
62 | # also access to the internal 2 tone generator
63 | self.dds = pluto_dds.Dds(self.dac)
64 | # tx buffer, created in writeTx and retained for continuous output
65 | self._tx_buff = None
66 | self.tx_state = self.TX_OFF
67 |
68 | # ----------------- TRx Physical Layer controls -----------------------
69 | def _get_SamplingFreq(self):
70 | """internal sampling frequency in MHz (ADC and DAC are the same)"""
71 | # available from many channel none of which are named
72 | # changes to [4] seem to alter them all
73 | value = self.phy_rx.attrs['sampling_frequency'].value
74 | return int(value)/1e6
75 |
76 | def _set_SamplingFreq(self, value):
77 | """set internal sampling freq in MHz"""
78 | # 2.1' + str(options[_enable]))
139 | else:
140 | raise ValueError('bool expected: only 2 options for rx_sampling')
141 |
142 | rx_decimation = property(_get_rxDownSampling, _set_rxDownSampling)
143 |
144 | def _get_rxLoFreq(self):
145 | """get receiver LO frequency property in MHz"""
146 | value = self.phy.find_channel('RX_LO').attrs['frequency'].value
147 | return int(value)/1e6
148 |
149 | def _set_rxLoFreq(self, value):
150 | """set receiver LO frequency property in MHz"""
151 | self.phy.find_channel('RX_LO').attrs['frequency'].value = _M2Str(value)
152 | rx_lo_freq = property(_get_rxLoFreq, _set_rxLoFreq)
153 |
154 | def _get_rxBW(self):
155 | """get receiver analogue RF bandwidth in MHz"""
156 | value = self.phy_rx.attrs['rf_bandwidth'].value
157 | return int(value)/1e6
158 |
159 | def _set_rxBW(self, value):
160 | """set receiver analogue RF bandwidth in MHz"""
161 | self.phy_rx.attrs['rf_bandwidth'].value = _M2Str(value)
162 | rx_bandwidth = property(_get_rxBW, _set_rxBW, doc='RF bandwidth of rx path')
163 |
164 | def _get_rx_gain(self):
165 | """read the rx RF gain in dB"""
166 | value = self.phy_rx.attrs['hardwaregain'].value.split(' ')
167 | return float(value[0])
168 |
169 | # to set rx gain need to also control the gain mode
170 | def _set_rx_gain(self, value=None):
171 | """set the rx RF gain in dB or to auto, slow attack"""
172 | if value is None:
173 | print('mode set to "slow_attack", other controls not yet available')
174 | self.phy_rx.attrs['gain_control_mode']\
175 | .value = 'slow_attack'
176 | else:
177 | self.phy_rx.attrs['gain_control_mode'].value = 'manual'
178 | self.phy_rx.attrs['hardwaregain']\
179 | .value = '{:2.3f} dB'.format(value)
180 | rx_gain = property(_get_rx_gain, _set_rx_gain)
181 |
182 | def _set_rx_gain_mode(self, mode):
183 | """set the gain mode to one of those available"""
184 | avail = self.phy_rx.attrs['gain_control_mode_available'].value
185 | avail = avail.split()
186 | # allow setting with just the first letter
187 | _mode = '' if len(mode)==0 else mode[0].upper()
188 | options = [av.capitalize()[0] for av in avail]
189 | if _mode in options:
190 | res = avail[options.index(_mode)]
191 | self.phy_rx.attrs['gain_control_mode'].value = res
192 | logging.debug('gain mode set:', res)
193 | else:
194 | print('error: available modes are', avail)
195 |
196 | def _get_rx_gain_mode(self):
197 | """get the gain mode to one of those available"""
198 | return self.phy_rx.attrs['gain_control_mode'].value
199 | rx_gain_mode = property(_get_rx_gain_mode, _set_rx_gain_mode)
200 |
201 | def _get_rx_rssi(self):
202 | """return rx rssi value""" # this is 'ddd.dd dB'
203 | return self._phy.channels[5].attrs['rssi'].value
204 | rsssi = property(_get_rx_rssi, None) # read only
205 |
206 | # getting data from the rx
207 | def readRx(self, no_samples, raw=True):
208 | # enable the channels
209 | for ch in self.adc.channels:
210 | ch.enabled = True
211 | try: # create a buffer of the right size to use
212 | buff = iio.Buffer(self.adc, no_samples)
213 | buff.refill()
214 | buffer = buff.read()
215 | iq = np.frombuffer(buffer, np.int16)
216 | except OSError:
217 | for ch in self.adc.channels:
218 | ch.enabled = True
219 | raise OSError('failed to create iio buffer')
220 | if raw:
221 | return iq
222 | else:
223 | return self.raw2complex(iq)
224 |
225 | def raw2complex(self, data):
226 | """return a scaled complex float version of the raw data"""
227 | # convert to float64, view performs an in place recast of the data
228 | # from SO 5658047
229 | # are the #bits available from some debug attr?
230 | # scale for 11 bits (signed 12)
231 | iq = 2**-(self.no_bits-1)*data.astype(FLOAT)
232 | return iq.view(COMPLEX)
233 |
234 | def capture(self, no_samples=0x4000, raw=False, desc=''):
235 | """read data from the rx and save with other RF params in a dict"""
236 | ans = {'desc':desc}
237 | ans['fs'] = self.sampling_frequency
238 | ans['fc'] = self.rx_lo_freq
239 | ans['rx_bw'] = self.rx_bandwidth
240 | ans['rx_gain'] = self.rx_gain
241 | ans['data'] = self.readRx(no_samples, raw=raw)
242 | # for raw data the device must provide the no of bits
243 | if raw:
244 | ans['bits'] = self.no_bits
245 | return ans
246 | # -------------------- Transmitter control------------------------
247 | # 3 mutually exclusive states off, dma - transmit data using writeTx()
248 | # or dds - 1 or 2 tone output controlled via dds instance
249 | def _txDMA(self, value):
250 | """control DMA channels"""
251 | for ch in self.tx_channels:
252 | ch.enabled = value
253 |
254 | def _set_tx_state(self, value):
255 | """states are OFF, DMA (via a buffer) or DDS"""
256 | self._tx_state = value
257 | if value==self.TX_DMA:
258 | self._txDMA(ON)
259 | self.dds.state(OFF)
260 | elif value==self.TX_DDS:
261 | self._txDMA(OFF)
262 | self.dds.state(ON)
263 | else: # any other value is TX_OFF
264 | self.dds.state(False) # this must be first don't know why
265 | self._txDMA(False)
266 | self._tx_buff = None
267 |
268 | def _get_tx_state(self):
269 | return ('off', 'dma', 'dds')[self._tx_state]
270 | tx_state = property(_get_tx_state, _set_tx_state)
271 |
272 | def txStatus(self, showDds=False):
273 | """print the key parameters for the receive signal chain"""
274 | print('Tx Fs:{:5.1f}MHz\tBB: {:7.2f}MHz'\
275 | .format(self.sampling_frequency, self.txBBSampling()))
276 | print(' BW:{:5.1f}MHz\tLO: {:7.2f}MHz'\
277 | .format(self.tx_bandwidth, self.tx_lo_freq))
278 | print(' Gain:{:4.1f}dB\tState: {:s}'\
279 | .format(self.tx_gain, self._get_tx_state()))
280 | if not(self._tx_buff is None):
281 | print(' Data:{:d}'.format(len(self._tx_buff)//4))
282 | if self._tx_state==self.TX_DDS or showDds:
283 | fmt = ' dds{:n}: {:5.3f}V\t{:5.3f}MHz\t{:5.2f}degs'
284 | print(fmt.format(1, *(self.dds.t1.status())))
285 | print(fmt.format(2, *(self.dds.t2.status())))
286 |
287 | def txBBSampling(self):
288 | """the base band sampling rate in MHz"""
289 | fs = self.sampling_frequency
290 | return fs/8 if self.tx_interpolation else fs
291 |
292 | def _get_txUpSampling(self):
293 | """control transmitter output interpolation"""
294 | # only 2 options dac_rate or dac_rate/8
295 | _dac = self.tx_channels[0].attrs
296 | value = _dac['sampling_frequency'].value
297 | options = _dac['sampling_frequency_available'].value.split(' ')
298 | logging.debug('get: tx_decimation:<' + value)
299 | return value==options[1]
300 |
301 | def _set_txUpSampling(self, enable):
302 | if isinstance(enable, bool) or isinstance(enable, int):
303 | # only 2 options dac_rate or dac_rate/8
304 | _enable = enable!=0
305 | _dac = self.tx_channels[0].attrs
306 | options = _dac['sampling_frequency_available'].value.split(' ')
307 | _dac['sampling_frequency'].value = options[_enable]
308 | logging.debug('set: tx_decimation:>' + str(options[_enable]))
309 | else:
310 | raise ValueError('bool expected: only 2 options for tx_sampling')
311 |
312 | tx_interpolation = property(_get_txUpSampling, _set_txUpSampling)
313 |
314 | def _get_txLoFreq(self):
315 | """transmitterer LO frequency property in MHz"""
316 | value = self.phy.find_channel('TX_LO').attrs['frequency'].value
317 | return int(value)/1e6
318 |
319 | def _set_txLoFreq(self, value):
320 | self.phy.find_channel('TX_LO').attrs['frequency'].value = _M2Str(value)
321 |
322 | tx_lo_freq = property(_get_txLoFreq, _set_txLoFreq)
323 |
324 | def _get_tx_gain(self):
325 | """get the tx RF gain in dB, it is always neg as an attenuation"""
326 | value = self.phy_tx.attrs['hardwaregain'].value
327 | return float(value.split()[0])
328 |
329 | def _set_tx_gain(self, value):
330 | """set the tx RF gain in dB"""
331 | if value>0:
332 | raise ValueError('tx gain is an attenuation, so always negative')
333 | self.phy_tx.attrs['hardwaregain']\
334 | .value = '{:2.3f} dB'.format(value)
335 |
336 | tx_gain = property(_get_tx_gain, _set_tx_gain)
337 |
338 | def _get_txBW(self):
339 | """transmitter analogue RF bandwidth in MHz"""
340 | # available from channel [4] or [5] which are not named
341 | # iio-scope only changes [5], so use that
342 | value = self.phy_tx.attrs['rf_bandwidth'].value
343 | return int(value)/1e6
344 |
345 | def _set_txBW(self, value):
346 | self.phy_tx.attrs['rf_bandwidth'].value = _M2Str(value)
347 |
348 | tx_bandwidth = property(_get_txBW, _set_txBW)
349 |
350 | def complex2raw(self, data, no_bits):
351 | iq = np.round((2**(no_bits-1))*data.view(FLOAT)).astype(np.int16)
352 | return iq
353 |
354 | def writeTx(self, samples): #, raw=False): use samples.dtype
355 | """write to the Tx buffer and make it cyclic"""
356 | if self._tx_buff is not None:
357 | self._tx_buff = None # turn off any previous signal
358 | if not(isinstance(samples, np.ndarray)):
359 | logging.debug('tx: off') # leave with transmitter off
360 | self.tx_state = self.TX_OFF
361 | return
362 | if samples.dtype==np.int16:
363 | data = samples<<4 # align 12 bit raw to msb
364 | else: # samples can come from some DiscreteSignalSource, if so
365 | # data is complex IQ and scaled to +/-1.0 float range
366 | # use 16 **not** self.no_bits to align data to msb
367 | data = self.complex2raw(samples, 16)
368 | # samples are 2 bytes each with interleaved I/Q value (no_samples = len/4)
369 | self.tx_state = self.TX_DMA # enable the tx channels
370 | try: # create a cyclic iio buffer for continuous tx output
371 | self._tx_buff = iio.Buffer(self.dac, len(data)//4, True)
372 | count = self._tx_buff.write(data)
373 | logging.debug(str(count)+' samples transmitted')
374 | self._tx_buff.push()
375 | except OSError as oserr:
376 | self.tx_state = self.TX_OFF
377 | raise OSError('failed to create an iio buffer')
378 | # buffer retained after a successful call
379 | return count # just for now
380 |
381 | def playback(self, data, level=-10):
382 | """transmit data captured from a similar device, level in dBFS"""
383 | if not isinstance(data, dict):
384 | # logging.error('unexpected data type')
385 | raise ValueError('dict data type expected')
386 | return
387 | self.tx_lo_freq = data['fc']
388 | self.sampling_rate = data['fs']
389 | self.tx_gain = level
390 | samples = data['data']
391 | raw = not samples.dtype==COMPLEX
392 | if raw: # samples are interleaved int IQ possibly from another device
393 | if 'bits' in data.keys():
394 | re_scale = self.no_bits - data['bits']
395 | if rescale>0:
396 | samples = samples << re_scale
397 | if rescale<0:
398 | samples = samples >> re_scale
399 | self.tx_state = self.TX_OFF # may not be needed
400 | self.tx_state = self.TX_DMA
401 | self.writeTx(samples)
402 |
403 | def txOutputFreq(self):
404 | # read only for now - confused as to which one is the controlling value
405 | # look at the various possibilities
406 | dac = self.dac.device
407 | various = {}
408 | various['TX1_I_F1'] = dac.channels[0].attrs['sampling_frequency'].value
409 | various['TX1_I_F2'] = dac.channels[1].attrs['sampling_frequency'].value
410 | various['TX1_Q_F1'] = dac.channels[2].attrs['sampling_frequency'].value
411 | various['TX1_Q_F2'] = dac.channels[3].attrs['sampling_frequency'].value
412 |
413 | various['volt0'] = dac.channels[4].attrs['sampling_frequency'].value
414 | various['volt1'] = dac.channels[5].attrs['sampling_frequency'].value
415 | return various
416 | # ------------------------ DDS Control ---------------------------
417 | ## def ddsState(self, value): # now control via tx_state
418 | ## """turn the dds signal(s) on/off"""
419 | ## if value==OFF:
420 | ## self.dds.t1.state(OFF)
421 | ## self.dds.t2.state(OFF)
422 | ## else:
423 | ## self.dds.t1.state(ON)
424 | ## self.dds.t2.state(ON)
425 |
426 | def ddsAmplitude(self, amp1, amp2=None):
427 | """control the DDS output level in dB"""
428 | self.dds.setAmplitude(amp1, amp2)
429 |
430 | def ddsFrequ(self, f1, f2=None):
431 | """control the DDS output level"""
432 | self.dds.setFrequency(f1, f2)
433 |
434 | def ddsPhase(self, ph1, ph2):
435 | self.dds.setPhase(phi, phi2)
436 |
437 | def ddsStatus(self):
438 | """show a summary of the dds system"""
439 | state = 'off' if self.dds.isOff() else 'on'
440 | print('dds:',state)
441 | if state=='on':
442 | fmt = ' dds{:n}: {:5.3f}V {: 5.3f}MHz {:5.2f}degs'
443 | print(fmt.format(1, *(self.dds.t1.status())))
444 | print(fmt.format(2, *(self.dds.t2.status())))
445 |
446 | if __name__=='__main__':
447 | pp = PlutoSdr(PLUTO_ID)
448 | pp.tx_lo_freq = 430
449 | pp.rx_lo_freq = 430
450 | pp.sampling_frequency = 10
451 |
452 | def _sampling(ch):
453 | return float(ch.attrs['sampling_frequency'].value)/1e6
454 |
455 | def _available(ch):
456 | ans = [float(v)/1e6 for v in
457 | ch.attrs['sampling_frequency_available'].value.split()]
458 | return ans
459 |
460 | def fittedSin(no_samples, no_cycles):
461 | """Exact no_cycles in the sample length"""
462 | nn = np.arange(no_samples)
463 | si = np.sin(2*np.pi*nn*no_cycles/len(nn))
464 | ci = np.cos(2*np.pi*nn*no_cycles/len(nn))
465 | return ci + 1j*si
466 |
467 | def plotCS(sig, centre=0, span=100):
468 | """plot a slice of sig"""
469 | xx = range(centre-span//2, centre+span//2)
470 | return (xx, np.take(sig.real, xx), xx, np.take(sig.imag, xx))
471 |
472 | def querySampling(dev):
473 | adc1 = dev.adc.channels[0]
474 | adc2 = dev.adc.channels[1]
475 | dac1 = dev.dac.find_channel('voltage0', True)
476 | dac2 = dev.dac.find_channel('voltage1', True)
477 | print(' adc1: {:5.2f}MHz available:[{:5.2f}, {:4.2f}]'.\
478 | format(_sampling(adc1), *_available(adc1)))
479 | print(' adc2: {:5.2f}MHz available:[{:5.2f}, {:4.2f}]'.\
480 | format(_sampling(adc2), *_available(adc1)))
481 | print(' dac1: {:5.2f}MHz available:[{:5.2f}, {:4.2f}]'.\
482 | format(_sampling(dac1), *_available(adc1)))
483 | print(' dac2: {:5.2f}MHz available:[{:5.2f}, {:4.2f}]'.\
484 | format(_sampling(dac2), *_available(adc1)))
485 |
486 | def queryTx(dev):
487 | dds_channels = [dev.dac.find_channel('altvoltage0', True)]
488 | dds_channels.append(dev.dac.find_channel('altvoltage1', True))
489 | dds_channels.append(dev.dac.find_channel('altvoltage2', True))
490 | dds_channels.append(dev.dac.find_channel('altvoltage3', True))
491 | for ch in dev.tx_channels:
492 | print(ch.name, ch.id, ch.enabled)
493 | for ch in dds_channels:
494 | print(ch.name, ch.id, ch.enabled, ch.attrs['raw'].value)
495 |
496 | def txChs(sdr):
497 | if sdr._tx_buff is None:
498 | print('DMA:0')
499 | else:
500 | print('DMA:', len(sdr._tx_buff)//4)
501 | print('tx channels')
502 | for ch in sdr.dds.device.channels: #sdr.tx_channels:
503 | print(ch.id, 'enabled:',ch.enabled)
504 | print('dds channels')
505 | for ch in sdr.dds.device.channels:
506 | if 'raw' in ch.attrs.keys():
507 | print(ch.id, ch.name, 'scale:', ch.attrs['scale'].value, 'raw:', ch.attrs['raw'].value)
508 |
509 |
--------------------------------------------------------------------------------
/pluto/readFilter.py:
--------------------------------------------------------------------------------
1 | """
2 | Read the .ftr file from ADI/Matlab and return a dict of contents
3 | rgr15Aug18
4 | * Copyright (C) 2018 Radio System Design Ltd.
5 | * Author: Richard G. Ranson, richard@radiosystemdesign.com
6 | *
7 | * This library is free software; you can redistribute it and/or
8 | * modify it under the terms of the GNU Lesser General Public
9 | * License as published by the Free Software Foundation under
10 | * version 2.1 of the License.
11 | *
12 | * This library is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 | * Lesser General Public License for more details.
16 | """
17 | from __future__ import print_function
18 |
19 | import logging
20 |
21 | from os import path
22 | from pluto.iio_lambdas import _Str2M
23 | from rsdLib.fileUtils import changeExt
24 |
25 | def _readGain(trx, a_line):
26 | """channel and gain information"""
27 | logging.info(a_line)
28 | params = a_line.split()
29 | names = params[0::2]
30 | values = params[1::2]
31 | names[0] = 'CH'
32 | return {trx+n:int(v) for n,v in zip(names, values)}
33 |
34 | def _readSynth(a_line):
35 | """synthesiser and internal filer interpolation/decimation"""
36 | logging.info(a_line)
37 | if a_line[1:3].upper()=='TX':
38 | names = ('TXPLL','DAC', 'T2', 'T2', 'TF', 'TXSAMP')
39 | elif a_line[1:3].upper()=='RX':
40 | names = ('RXPLL','ADC', 'R2', 'R1,', 'RF', 'RXSAMP')
41 | else:
42 | raise IOError('unknown synthesiser setting parameters '+a_line)
43 | values = a_line.split()[1:]
44 | return {n:_Str2M(v) for n,v in zip(names, values)}
45 |
46 | def _readBwidth(a_line):
47 | """RF bandwidth for Rx and Tx"""
48 | logging.info(a_line)
49 | xx = a_line.split()
50 | trx = xx[0][2:4].lower()
51 | return {trx+'_bw':_Str2M(xx[1])}
52 |
53 | def readFilter(filename):
54 | """read an flt file, parsing the lines and collecting data to a dict"""
55 | filename = changeExt(filename, 'ftr')
56 | ans = {'file':path.basename(filename)}
57 | taps = []
58 | with open(filename, 'r') as fin:
59 | # read line by line
60 | line = fin.readline().strip()
61 | while line:
62 | logging.debug(str(len(line))+': '+line)
63 | if len(line)>1 and not(line[0]=='#'):
64 | #logging.debug('line: '+line)
65 | # process the line depending on the content
66 | if line[:2].upper()=='TX':
67 | ans.update(_readGain('tx_', line))
68 | elif line[:2].upper()=='RX':
69 | ans.update(_readGain('rx_', line))
70 | elif line[0].upper()=='R':
71 | ans.update(_readSynth(line))
72 | elif line[0].upper()=='B':
73 | ans.update(_readBwidth(line))
74 | else: # assume line contains tap values
75 | try:
76 | taps.append([int(x) for x in line.split(',')])
77 | except:
78 | raise IOError('invalid tap values '+line)
79 | # next line
80 | line = fin.readline().strip()
81 | # assume only 2 sets of taps in rx, tx order
82 | # i.e. shape(taps)[1]==2
83 | # also shape(taps)[0] should be a multiple of 16
84 | ans['rx_taps'] = [t[0] for t in taps]
85 | logging.info('rx taps: {:}'.format(len(ans['rx_taps'])))
86 | ans['tx_taps'] = [t[1] for t in taps]
87 | logging.info('tx taps: {:}'.format(len(ans['tx_taps'])))
88 | return ans
89 |
90 | if __name__=='__main__':
91 | import sys
92 | logging.basicConfig(format='%(module)-12s.%(funcName)-12s:%(levelname)s - %(message)s',
93 | stream=sys.stdout, level=logging.ERROR)
94 |
95 |
--------------------------------------------------------------------------------
/pluto/version.py:
--------------------------------------------------------------------------------
1 | # version control file for PlutoSdr package
2 | # v 1.0.x - 24aug18 - initial release, functional with some errors and difficulties
3 | # v 1.1.x - 28may19 - Corrections, testing of read/write, capture playback via IpyNBs
4 | # v 1.1.x - 27may20 - corrections to test/notebooks from user feedback
5 |
6 | __version__ = '1.1.2'
7 | __DATE__ = '27may20'
8 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!usr/bin/env python
2 | """
3 | setup for code to access and control the PlutSDR hardware
4 |
5 | rgr05Aug18
6 | """
7 |
8 | from distutils.core import setup
9 |
10 | VERSION_FILE = 'pluto/version.py'
11 | # read version and other information from the package
12 | version = {}
13 | with open(VERSION_FILE) as fin:
14 | exec(fin.read(), version)
15 |
16 | setup(name='PlutoSDR',
17 | version = version['__version__'],
18 | description = \
19 | 'The library package for access and control the PlutSDR hardware',
20 | author = 'Richard Ranson',
21 | scripts = [], # add name(s) of script(s)
22 | packages = ['pluto'] # add name(s) of package(s)
23 | )
24 |
25 | # I'm sure there is more to add, but for now ok to install basic packages and scripts
26 |
--------------------------------------------------------------------------------
/test/LTE1p4_MHz.ftr:
--------------------------------------------------------------------------------
1 | # Generated with the MATLAB AD9361 Filter Design Wizard
2 | # Generated 14-Aug-2015 13:50:15
3 | # Inputs:
4 | # Data Sample Frequency = 1920000.000000 Hz
5 | TX 3 GAIN 0 INT 4
6 | RX 3 GAIN -12 DEC 4
7 | RTX 737280000 92160000 30720000 15360000 7680000 1920000
8 | RRX 737280000 92160000 30720000 15360000 7680000 1920000
9 | BWTX 1613786
10 | BWRX 1613792
11 | 5,26
12 | -21,12
13 | -51,14
14 | -120,-30
15 | -212,-90
16 | -338,-198
17 | -471,-323
18 | -599,-465
19 | -683,-581
20 | -702,-649
21 | -630,-633
22 | -469,-526
23 | -233,-328
24 | 34,-77
25 | 281,181
26 | 448,380
27 | 493,471
28 | 399,421
29 | 185,240
30 | -94,-30
31 | -361,-312
32 | -536,-521
33 | -558,-582
34 | -406,-463
35 | -112,-182
36 | 245,187
37 | 559,536
38 | 726,749
39 | 678,743
40 | 407,493
41 | -24,55
42 | -494,-452
43 | -857,-869
44 | -983,-1050
45 | -805,-908
46 | -345,-452
47 | 280,209
48 | 887,879
49 | 1277,1342
50 | 1297,1419
51 | 896,1036
52 | 150,260
53 | -741,-703
54 | -1505,-1561
55 | -1875,-2016
56 | -1676,-1861
57 | -893,-1060
58 | 305,219
59 | 1587,1625
60 | 2547,2712
61 | 2814,3066
62 | 2177,2436
63 | 675,846
64 | -1375,-1372
65 | -3417,-3619
66 | -4779,-5157
67 | -4831,-5284
68 | -3157,-3531
69 | 311,195
70 | 5241,5540
71 | 10931,11740
72 | 16447,17768
73 | 20812,22548
74 | 23222,25189
75 | 23222,25189
76 | 20812,22548
77 | 16447,17768
78 | 10931,11740
79 | 5241,5540
80 | 311,195
81 | -3157,-3531
82 | -4831,-5284
83 | -4779,-5157
84 | -3417,-3619
85 | -1375,-1372
86 | 675,846
87 | 2177,2436
88 | 2814,3066
89 | 2547,2712
90 | 1587,1625
91 | 305,219
92 | -893,-1060
93 | -1676,-1861
94 | -1875,-2016
95 | -1505,-1561
96 | -741,-703
97 | 150,260
98 | 896,1036
99 | 1297,1419
100 | 1277,1342
101 | 887,879
102 | 280,209
103 | -345,-452
104 | -805,-908
105 | -983,-1050
106 | -857,-869
107 | -494,-452
108 | -24,55
109 | 407,493
110 | 678,743
111 | 726,749
112 | 559,536
113 | 245,187
114 | -112,-182
115 | -406,-463
116 | -558,-582
117 | -536,-521
118 | -361,-312
119 | -94,-30
120 | 185,240
121 | 399,421
122 | 493,471
123 | 448,380
124 | 281,181
125 | 34,-77
126 | -233,-328
127 | -469,-526
128 | -630,-633
129 | -702,-649
130 | -683,-581
131 | -599,-465
132 | -471,-323
133 | -338,-198
134 | -212,-90
135 | -120,-30
136 | -51,14
137 | -21,12
138 | 5,26
139 |
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radiosd/PlutoSdr/11b23f2f07b87454926e931ad5dfef4e9cad0ab6/test/__init__.py
--------------------------------------------------------------------------------
/test/testDdsClass.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | Using unittest to validate code for pluto_dds.Dds class
4 | It relies on having a device connected.
5 | Using mock was too troublesome
6 | rgr16jul18
7 | look for #!# lines where corrections are pending
8 | """
9 | from __future__ import print_function
10 |
11 | import logging
12 |
13 | import unittest
14 |
15 | # for numpy operations, there are additional assertTests in the numpy module
16 | import numpy.testing as npt
17 |
18 | import iio
19 | from pluto import pluto_dds
20 | from pluto.controls import ON, OFF
21 |
22 | class TestDdsClass(unittest.TestCase):
23 |
24 | def setUp(self):
25 | self.longMessage = True # enables "test != result" in error message
26 | self.ctx = iio.Context('ip:pluto.local')
27 | self.dev = self.ctx.find_device('cf-ad9361-dds-core-lpc')
28 |
29 | def tearDown(self):
30 | pass
31 |
32 | # everything starting test is run, but in no guaranteed order
33 | def testDdsCreate(self):
34 | """create a Dds instance"""
35 | self.assertIsInstance(self.dev, iio.Device, 'ok')
36 | dds = pluto_dds.Dds(self.dev)
37 | self.assertIsInstance(dds.t1, pluto_dds.DdsTone,
38 | 'Dds Tone1 instance created')
39 | self.assertIsInstance(dds.t2, pluto_dds.DdsTone,
40 | 'Dds Tone2 instance created')
41 | self.assertTrue(dds.isOff(), "initial state is both off")
42 |
43 | def testSetFrequency(self):
44 | """confirm read and write to properties"""
45 | dds = pluto_dds.Dds(self.dev)
46 | fs = dds.getSamplingFreq() # test tones must be within +/- hald Fs
47 | dds.setFrequency(fs/4)
48 | npt.assert_almost_equal(dds.t1.frequency, fs/4, decimal=3,
49 | err_msg='set f1 frequency')
50 | dds.setFrequency(fs/4, -fs/8)
51 | npt.assert_almost_equal(dds.t1.frequency, fs/4, decimal=3,
52 | err_msg='set f1 frequency')
53 | npt.assert_almost_equal(dds.t2.frequency, -fs/8, decimal=3,
54 | err_msg='set f2 frequency')
55 |
56 | def testSetPhase(self):
57 | """confirm the relative I/Q phases for +/- frequencies"""
58 | dds = pluto_dds.Dds(self.dev)
59 | tone = pluto_dds.DdsTone(self.dev, 'F1')
60 | tone.frequency = 1
61 | tone.phase = 180 # set phase mid way
62 | i_phase = tone.getPhase()
63 | q_phase = tone.getPhase('Q')
64 |
65 | def testSetAmplitude(self):
66 | """confirm amplitude values read/write and amp <= 0 dB"""
67 | dds = pluto_dds.Dds(self.dev)
68 | # dds.setAmplitude() should be created OFF
69 | self.assertEqual(dds.t1.amplitude, 0,
70 | 'created with amplitude 1 off')
71 | self.assertEqual(dds.t2.amplitude, 0,
72 | 'created with amplitude 2 off')
73 | with self.assertRaises(ValueError, msg='values set in -dB '):
74 | dds.setAmplitude(10)
75 | dds.setAmplitude(-10) # this is in dB
76 | npt.assert_almost_equal(dds.t1.amplitude, 0.1, decimal=3,
77 | err_msg='first arguments sets f1 amplitude')
78 | self.assertEqual(dds.t2.amplitude, 0,
79 | 'no arguments turn amplitude 2 off')
80 | dds.setAmplitude(-13, -13)
81 | npt.assert_almost_equal(dds.t1.amplitude, 0.05, decimal=3,
82 | err_msg='first arguments sets f1 amplitude')
83 | npt.assert_almost_equal(dds.t2.amplitude, 0.05, decimal=3,
84 | err_msg='second arguments sets f2 amplitude')
85 | self.assertTrue(dds.isOff(), 'created OFF, independent of amplitude')
86 | dds.state(ON);
87 | self.assertFalse(dds.isOff(), 'have to explicitly turn on')
88 |
89 | if __name__=='__main__':
90 | # for now need a device connected to do tests
91 | from os import path
92 | import sys
93 | try:
94 | iio.Context('ip:pluto.local') # just to find whether it is connected
95 | except:
96 | print('testDdsClass requires a pluto device connected')
97 | sys.exit(1)
98 |
99 | # show what is being tested and from where
100 | print('\nTesting class Dds in module:\n',path.abspath(pluto_dds.__file__))
101 |
102 | logging.basicConfig(format='%(module)-12s.%(funcName)-12s:%(levelname)s - %(message)s',
103 | stream=sys.stdout, level=logging.INFO)
104 | unittest.main()
105 |
106 |
--------------------------------------------------------------------------------
/test/testDdsTone.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | Using unittest to validate code for pluto_dds.DdsTone
4 | It relies on having a device connected.
5 | Using mock was too troublesome
6 | rgr16jul18
7 | look for #!# lines where corrections are pending
8 | """
9 | from __future__ import print_function
10 |
11 | import logging
12 |
13 | import unittest
14 |
15 | # for numpy operations, there are additional assertTests in the numpy module
16 | import numpy.testing as npt
17 |
18 | import iio
19 | from pluto import pluto_dds
20 | from pluto.controls import ON, OFF
21 |
22 | class TestDdsTone(unittest.TestCase):
23 |
24 | def setUp(self):
25 | self.longMessage = True # enables "test != result" in error message
26 | self.ctx = iio.Context('ip:pluto.local')
27 | self.dev = self.ctx.find_device('cf-ad9361-dds-core-lpc')
28 |
29 | def tearDown(self):
30 | pass
31 |
32 | # everything starting test is run, but in no guaranteed order
33 | def testToneCreate(self):
34 | """create a Dds with a DdsTone instance"""
35 | self.assertIsInstance(self.dev, iio.Device, 'ok')
36 | tone = pluto_dds.DdsTone(self.dev, 'F1')
37 | self.assertIsInstance(tone, pluto_dds.DdsTone,
38 | 'ddsTone instance created')
39 | tone.setFreq(1)
40 | npt.assert_almost_equal(tone.getFreq(), 1.0, decimal=4,
41 | err_msg='initial frequency value')
42 | tone.setPhase(0)
43 | npt.assert_almost_equal(tone.getPhase(), 0.0, decimal=4,
44 | err_msg='initial phase value')
45 | npt.assert_almost_equal(tone.getPhase('Q'), 270.0, decimal=4,
46 | err_msg='initial phase value')
47 | tone.setAmplitude(0.5)
48 | npt.assert_almost_equal(tone.getAmplitude(), 0.5, decimal=4,
49 | err_msg='initial amplitude value')
50 |
51 | def testProperties(self):
52 | """confirm read and write to properties"""
53 | tone = pluto_dds.DdsTone(self.dev, 'F1')
54 | fs = tone.getSamplingFreq()
55 | tone.frequency = -fs/4
56 | npt.assert_almost_equal(tone.frequency, -fs/4, decimal=3,
57 | err_msg='using frequency property')
58 | tone.phase = 20
59 | npt.assert_almost_equal(tone.phase, 20.0, decimal=3,
60 | err_msg='using phase property')
61 | tone.amplitude = 0.2
62 | npt.assert_almost_equal(tone.amplitude, 0.2, decimal=3,
63 | err_msg='using amplitude property')
64 | with self.assertRaises(ValueError,
65 |
66 | msg='value set out of 0 .. 1 range'):
67 | tone.amplitude = 10
68 |
69 | def testPosNegFreq(self):
70 | """confirm the relative I/Q phases for +/- frequencies"""
71 | tone = pluto_dds.DdsTone(self.dev, 'F1')
72 | fs = tone.getSamplingFreq()
73 | tone.frequency = fs/8
74 | tone.phase = 180 # set phase mid way
75 | i_phase = tone.getPhase()
76 | q_phase = tone.getPhase('Q')
77 | tone.frequency = -fs/4
78 | self.assertEqual(i_phase, tone.getPhase(),
79 | 'I phase equal for +/- freq')
80 | self.assertEqual(abs(q_phase - tone.getPhase('Q')), 180,
81 | 'Q phase opposite for +/- freq')
82 |
83 | def testOnOff(self):
84 | """confirm amplitude settings are preserved from off to on"""
85 | tone = pluto_dds.DdsTone(self.dev, 'F1')
86 | tone.amplitude = 0.5 # some initial setting
87 | self.assertEqual(tone.amplitude, 0.5, "setting attribute ok")
88 | tone.i_ch.attrs['raw'].value = '0'
89 | self.assertEqual(tone.amplitude, 0.5, "off preserves amplitude")
90 | tone.i_ch.attrs['raw'].value = '1'
91 | self.assertEqual(tone.amplitude, 0.5, "return to previous setting")
92 | tone.i_ch.attrs['raw'].value = '0'
93 | tone.amplitude = 0.1
94 | tone.i_ch.attrs['raw'].value = '1'
95 | npt.assert_almost_equal(tone.amplitude, 0.1, decimal=4,
96 | err_msg="ok setting change even when off")
97 |
98 | def testPhaseNormalisation(self):
99 | """confirm phase values restricted to 0 <= phi <= 360"""
100 | tone = pluto_dds.DdsTone(self.dev, 'F1')
101 | tone.phase = -10
102 | npt.assert_almost_equal(tone.phase, 350, decimal=2,
103 | err_msg='phase norm -10 -> 350 degs')
104 |
105 | def testWithinFs(self):
106 | """confirm freq values are within +/- half fs"""
107 | tone = pluto_dds.DdsTone(self.dev, 'F1')
108 | tone.amplitude = 0.5
109 | tone.frequency = 0.5
110 | fs = tone.getSamplingFreq()
111 | with self.assertRaises(ValueError,
112 | msg='when setting f outside +/-fs/2'):
113 | tone.setFreq(fs)
114 |
115 | if __name__=='__main__':
116 | from os import path
117 | import sys
118 | # tests require a device to be connected
119 | try:
120 | iio.Context('ip:pluto.local') # just to find whether it is connected
121 | except:
122 | print('testDdsTone requires a pluto device connected')
123 | sys.exit(1)
124 |
125 | # show what is being tested and from where
126 | print('\nTesting class DdsTone in module:\n',path.abspath(pluto_dds.__file__))
127 |
128 |
129 | logging.basicConfig(format='%(module)-12s.%(funcName)-12s:%(levelname)s - %(message)s',
130 | stream=sys.stdout, level=logging.INFO)
131 |
132 | unittest.main()
133 |
134 |
--------------------------------------------------------------------------------
/test/testFilterRead.py:
--------------------------------------------------------------------------------
1 | """
2 | Using unittest to validate code for readFilter
3 | #It relies on having a device connected.
4 | rgr15Aug18
5 | look for #!# lines where corrections are pending
6 | """
7 | from __future__ import print_function
8 |
9 | import logging
10 |
11 | import unittest
12 | from pluto import readFilter
13 |
14 | TEST_FILE = 'test/LTE1p4_MHz'
15 | # a represetative sample of data is hard coded in testFunction()
16 | RX_TAPS = [5, -21, -51, -120, -212, -338, -471, -599] # first and last 8
17 | TX_TAPS = [26, 12, 14, -30, -90, -198, -323, -465]
18 |
19 | class TestFilterRead(unittest.TestCase):
20 |
21 | def setUp(self):
22 | self.longMessage = True # enables "test != result" in error message
23 |
24 | def tearDown(self):
25 | pass
26 |
27 | def testFunction(self): # only one function really
28 | """confirm a sample set of data that should be read in"""
29 | res = readFilter.readFilter(TEST_FILE)
30 | self.assertEqual(res['file'], 'LTE1p4_MHz.ftr', 'correct file extension')
31 | self.assertEqual(res['tx_CH'], 3, 'reading ch info')
32 |
33 | self.assertEqual(res['rx_bw'], 1.613792, 'reading BW info')
34 |
35 | self.assertEqual(res['TXPLL'], 737.28, 'reading PLL info')
36 | self.assertEqual(res['ADC'], 92.16, 'reading ADC info')
37 |
38 | self.assertEqual(len(res['rx_taps']), 128, 'no of taps in rx fir')
39 | self.assertListEqual(res['rx_taps'][:8], RX_TAPS, 'some rx taps')
40 | self.assertListEqual(res['rx_taps'][-8:],
41 | RX_TAPS[::-1], 'reverse symmetric rx taps at the end')
42 |
43 | self.assertEqual(len(res['tx_taps']), 128, 'no of taps in tx fir')
44 | self.assertListEqual(res['tx_taps'][:8], TX_TAPS, 'some tx taps')
45 |
46 |
47 | if __name__=='__main__':
48 | # for now need a device connected to do tests
49 | from os import path
50 | import sys
51 | # show what is being tested and from where
52 | print('\nTesting class FilterRead in pluto.filterRead:\n',
53 | path.abspath(readFilter.__file__))
54 |
55 | logging.basicConfig(
56 | format='%(module)-12s.%(funcName)-12s:%(levelname)s - %(message)s',
57 | stream=sys.stdout, level=logging.ERROR)
58 | unittest.main()
59 |
--------------------------------------------------------------------------------
/test/testFirConfig.py:
--------------------------------------------------------------------------------
1 | """
2 | Using unittest to validate code for FirConfig class in pluto_fir
3 | Just the twos compliment stuf for now
4 | #It relies on having a device connected.
5 | rgr11Aug18
6 | look for #!# lines where corrections are pending
7 | """
8 | from __future__ import print_function
9 |
10 | import logging
11 |
12 | import unittest
13 | from pluto import pluto_fir
14 | from pluto.pluto_fir import setBit, twosC2Int, int2TwosC # just these 3 first
15 |
16 | class TestFirConfigClass(unittest.TestCase):
17 |
18 | def setUp(self):
19 | self.longMessage = True # enables "test != result" in error message
20 | ## self.ctx = iio.Context('ip:pluto.local')
21 | ## self.dev = self.ctx.find_device('cf-ad9361-dds-core-lpc')
22 |
23 | def tearDown(self):
24 | pass
25 |
26 | # everything starting test is run, but in no guaranteed order
27 | def testControlBit(self):
28 | """confirm set and clear bits by index"""
29 | value = 0xFF
30 | self.assertEqual(setBit(4, value, 0), 0xEF, 'clear bit 4')
31 | self.assertEqual(setBit(0, value, 0), 0xFE, 'clear bit 0')
32 | value = 0x00
33 | self.assertEqual(setBit(4, value, 1), 0x10, 'set bit 4')
34 | self.assertEqual(setBit(0, value, 1), 0x01, 'set bit 0')
35 |
36 | def testTwosComplimentStuff(self):
37 | """check translation to and from twos compliment"""
38 | self.assertEqual(twosC2Int(255, 250), -6, 'bytes to int')
39 | self.assertEqual(twosC2Int(0, 6), 6, 'bytes to int')
40 | #!# check edge values
41 | self.assertTupleEqual(int2TwosC(-6), (255, 250), 'int to bytes')
42 |
43 | if __name__=='__main__':
44 | # for now need a device connected to do tests
45 | from os import path
46 | import sys
47 | ## try:
48 | ## iio.Context('ip:pluto.local') # just to find whether it is connected
49 | ## except:
50 | ## print('testDdsClass requires a pluto device connected')
51 | ## sys.exit(1)
52 |
53 | # show what is being tested and from where
54 | print('\nTesting class FirConfig in module:\n',
55 | path.abspath(pluto_fir.__file__))
56 |
57 | logging.basicConfig(format='%(module)-12s.%(funcName)-12s:%(levelname)s - %(message)s',
58 | stream=sys.stdout, level=logging.INFO)
59 | unittest.main()
60 |
--------------------------------------------------------------------------------
/test/testPlutoSdr.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | Using unittest to validate code for pluto_sdr
4 | It relies on having a device connected. But ncanot validate RF control
5 | rgr29jul18
6 | look for #!# lines where corrections are pending
7 | """
8 | from __future__ import print_function
9 |
10 | import logging
11 |
12 | import unittest
13 |
14 | # for numpy operations, there are additional assertTests in the numpy module
15 | import numpy.testing as npt
16 |
17 | import iio
18 | from pluto import pluto_sdr
19 | from pluto.controls import ON, OFF
20 |
21 | class TestplutoSdr(unittest.TestCase):
22 |
23 | def setUp(self):
24 | self.longMessage = True # enables "test != result" in error message
25 | self.sdr = pluto_sdr.PlutoSdr('ip:pluto.local')
26 |
27 | def tearDown(self):
28 | pass
29 |
30 | # everything starting test is run, but in no guaranteed order
31 | def testPlutoSdrCreate(self):
32 | """create a PlutoSdr instance"""
33 | self.assertIsInstance(self.sdr.ctx, iio.Context, 'ok')
34 |
35 | def testSysAttributes(self):
36 | """confirm read and write to system properties"""
37 | sdr = self.sdr
38 | fs = sdr.sampling_frequency
39 | sdr.sampling_frequency = 10 # set in MHz
40 | self.assertEqual(sdr.sampling_frequency, 10.0, 'Fs set in MHz')
41 | sdr.sampling_frequency = fs
42 | self.assertEqual(sdr.sampling_frequency, fs, 're-set to original')
43 |
44 | def testRxAttributes(self):
45 | """confirm read and write to rx properties"""
46 | sdr = self.sdr
47 | fs = sdr.sampling_frequency
48 | decimate = sdr.rx_decimation
49 | sdr.rx_decimation = True
50 | ## self.assertEqual(sdr.rxBBSampling(), fs, 'decimation off')
51 | self.assertEqual(sdr.rxBBSampling(), fs/8, 'decimation on')
52 | sdr.rx_decimation = decimate
53 | bw = sdr.rx_bandwidth # this is the turn on value
54 | sdr.rx_bandwidth = 12.2 # set in MHz
55 | self.assertEqual(sdr.rx_bandwidth, 12.2, 'BW set in MHz')
56 | sdr.rx_bandwidth = bw
57 | self.assertEqual(sdr.rx_bandwidth, bw, 're-set to original BW')
58 | # some values are truncated due to available synth settings
59 | #!# no check on an out of range value
60 | lo = sdr.rx_lo_freq # this is the turn on value
61 | sdr.rx_lo_freq = 430.1 # set in MHz
62 | npt.assert_almost_equal(sdr.rx_lo_freq, 430.1, decimal=6,
63 | err_msg='setting rx lo in MHz')
64 | sdr.rx_lo_freq = lo
65 | npt.assert_almost_equal(sdr.rx_lo_freq, lo, decimal=6,
66 | err_msg='re-set to original LO')
67 | gain = sdr.rx_gain
68 | sdr.rx_gain = 20.0 # set value in dB
69 | self.assertEqual(sdr.rx_gain, 20.0, 'gain set in dB')
70 | sdr.rx_gain = gain
71 | self.assertEqual(sdr.rx_gain, gain, 're-set to original gain')
72 | mode = sdr.rx_gain_mode
73 | sdr.rx_gain_mode = 'f'
74 | self.assertEqual(sdr.rx_gain_mode[:4], 'fast', 'alter gain mode by first letter')
75 | sdr.rx_gain_mode = mode
76 |
77 |
78 | def testTxAttributes(self):
79 | """confirm read and write to tx properties"""
80 | sdr = self.sdr
81 | fs = sdr.sampling_frequency
82 | interpolate = sdr.tx_interpolation
83 | sdr.tx_interpolation = True
84 | self.assertEqual(sdr.txBBSampling(), fs/8, 'interpolation on')
85 | sdr.interpolation = interpolate
86 | bw = sdr.tx_bandwidth # this is the turn on value
87 | sdr.tx_bandwidth = 10.2 # set in MHz
88 | self.assertEqual(sdr.tx_bandwidth, 10.2, 'BW set in MHz')
89 | sdr.tx_bandwidth = bw
90 | self.assertEqual(sdr.tx_bandwidth, bw, 're-set to original BW')
91 | lo = sdr.tx_lo_freq # this is the turn on value
92 | sdr.tx_lo_freq = 330.1 # set in MHz
93 | npt.assert_almost_equal(sdr.tx_lo_freq, 330.1, decimal=6,
94 | err_msg='setting tx lo in MHz')
95 | sdr.tx_lo_freq = lo
96 | npt.assert_almost_equal(sdr.tx_lo_freq, lo, decimal=6,
97 | err_msg='re-set to original LO')
98 | gain = sdr.tx_gain
99 | sdr.tx_gain = -20 # set value in dB
100 | self.assertEqual(sdr.tx_gain, -20, 'gain set in (neg) dB')
101 | sdr.tx_gain = gain
102 | self.assertEqual(sdr.tx_gain, gain, 're-set to original gain')
103 |
104 | def testDdsControl(self):
105 | """confirm higher level control of DDS"""
106 | sdr = self.sdr
107 | state = sdr.dds.isOff()
108 | sdr.dds.state(OFF)
109 | self.assertTrue(sdr.dds.isOff(), 'dds off from sdr function')
110 | sdr.dds.state(ON) # on with 0 amplituide is still off
111 | sdr.dds.setAmplitude(-1, -1) # set some level
112 | self.assertFalse(sdr.dds.isOff(), 'dds on from sdr function')
113 | npt.assert_almost_equal(sdr.dds.t1.amplitude, 10**(-1.0/10), decimal=4,
114 | err_msg='t1 amplitude set correctly')
115 | npt.assert_almost_equal(sdr.dds.t2.amplitude, 10**(-1.0/10), decimal=4,
116 | err_msg='t2 amplitude set correctly')
117 | sdr.dds.state(state)
118 |
119 | if __name__=='__main__':
120 | # for now need a device connected to do tests
121 | from os import path
122 | import sys
123 | try:
124 | iio.Context('ip:pluto.local') # just to find whether it is connected
125 | except:
126 | print('testPlutoSdr requires a pluto device connected')
127 | sys.exit(1)
128 |
129 | # show what is being tested and from where
130 | print('\nTesting class plutoSdr in module:\n',path.abspath(pluto_sdr.__file__))
131 |
132 | logging.basicConfig(
133 | format='%(module)-12s.%(funcName)-12s:%(levelname)s - %(message)s',
134 | stream=sys.stdout, level=logging.INFO)
135 | class LogFilter(logging.Filter):
136 | def __init__(self, module):
137 | self.module = module
138 |
139 | def filter(self, record):
140 | return path.basename(record.pathname)==self.module
141 |
142 | logging.root.addFilter(LogFilter('pluto_sdr.py'))
143 | unittest.main()
144 |
145 |
--------------------------------------------------------------------------------