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