// // Copyright 2017 Ettus Research (National Instruments) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // #include "ad937x_device.hpp" #include "adi/mykonos.h" #include "adi/mykonos_gpio.h" #include #include using namespace mpm::ad937x::device; using namespace mpm::ad937x::gpio; using namespace uhd; const double ad937x_device::MIN_FREQ = 300e6; const double ad937x_device::MAX_FREQ = 6e9; const double ad937x_device::MIN_RX_GAIN = 0.0; const double ad937x_device::MAX_RX_GAIN = 30.0; const double ad937x_device::RX_GAIN_STEP = 0.5; const double ad937x_device::MIN_TX_GAIN = 0.0; const double ad937x_device::MAX_TX_GAIN = 41.95; const double ad937x_device::TX_GAIN_STEP = 0.05; static const double RX_DEFAULT_FREQ = 1e9; static const double TX_DEFAULT_FREQ = 1e9; // TODO: get the actual device ID static const uint32_t AD9371_PRODUCT_ID = 0x1F; // TODO: move this to whereever we declare the ARM binary static const size_t ARM_BINARY_SIZE = 98304; static const uint32_t INIT_CAL_TIMEOUT_MS = 10000; static const uint32_t INIT_CALS = TX_BB_FILTER | ADC_TUNER | TIA_3DB_CORNER | DC_OFFSET | TX_ATTENUATION_DELAY | RX_GAIN_DELAY | FLASH_CAL | PATH_DELAY | TX_LO_LEAKAGE_INTERNAL | // TX_LO_LEAKAGE_EXTERNAL | TX_QEC_INIT | LOOPBACK_RX_LO_DELAY | LOOPBACK_RX_RX_QEC_INIT | RX_LO_DELAY | RX_QEC_INIT | // DPD_INIT | // CLGC_INIT | // VSWR_INIT | 0; static const uint32_t TRACKING_CALS = TRACK_RX1_QEC | TRACK_RX2_QEC | TRACK_ORX1_QEC | TRACK_ORX2_QEC | // TRACK_TX1_LOL | // TRACK_TX2_LOL | TRACK_TX1_QEC | TRACK_TX2_QEC | // TRACK_TX1_DPD | // TRACK_TX2_DPD | // TRACK_TX1_CLGC | // TRACK_TX2_CLGC | // TRACK_TX1_VSWR | // TRACK_TX2_VSWR | // TRACK_ORX1_QEC_SNLO | // TRACK_ORX2_QEC_SNLO | // TRACK_SRX_QEC | 0; // helper function to unify error handling void ad937x_device::_call_api_function(std::function func) { auto error = func(); if (error != MYKONOS_ERR_OK) { std::cout << getMykonosErrorMessage(error); // TODO: make UHD exception //throw std::exception(getMykonosErrorMessage(error)); } } // helper function to unify error handling, GPIO version void ad937x_device::_call_gpio_api_function(std::function func) { auto error = func(); if (error != MYKONOS_ERR_GPIO_OK) { std::cout << getGpioMykonosErrorMessage(error); // TODO: make UHD exception //throw std::exception(getMykonosErrorMessage(error)); } } void ad937x_device::_initialize() { // TODO: make this reset actually do something (implement CMB_HardReset or replace) _call_api_function(std::bind(MYKONOS_resetDevice, mykonos_config.device)); if (get_product_id() != AD9371_PRODUCT_ID) { throw runtime_error("AD9371 product ID does not match expected ID!"); } _call_api_function(std::bind(MYKONOS_initialize, mykonos_config.device)); if (!get_pll_lock_status(pll_t::CLK_SYNTH)) { throw runtime_error("AD937x CLK_SYNTH PLL failed to lock in initialize()"); } std::vector binary(98304, 0); _load_arm(binary); // TODO: Add multi-chip sync code tune(RX_DIRECTION, RX_DEFAULT_FREQ); tune(TX_DIRECTION, TX_DEFAULT_FREQ); // TODO: wait 200ms or change to polling if (!get_pll_lock_status(pll_t::RX_SYNTH)) { throw runtime_error("AD937x RX PLL failed to lock in initialize()"); } if (!get_pll_lock_status(pll_t::TX_SYNTH)) { throw runtime_error("AD937x TX PLL failed to lock in initialize()"); } // TODO: ADD GPIO CTRL setup here set_gain(RX_DIRECTION, chain_t::ONE, 0); set_gain(RX_DIRECTION, chain_t::TWO, 0); set_gain(TX_DIRECTION, chain_t::ONE, 0); set_gain(TX_DIRECTION, chain_t::TWO, 0); _run_initialization_calibrations(); // TODO: do external LO leakage calibration here if hardware supports it // I don't think we do? _start_jesd(); _enable_tracking_calibrations(); // radio is ON! _call_api_function(std::bind(MYKONOS_radioOn, mykonos_config.device)); // TODO: ordering of this doesn't seem right, intuitively, verify this works _call_api_function(std::bind(MYKONOS_setObsRxPathSource, mykonos_config.device, OBS_RXOFF)); _call_api_function(std::bind(MYKONOS_setObsRxPathSource, mykonos_config.device, OBS_INTERNALCALS)); _apply_gain_pins(RX_DIRECTION, chain_t::ONE); _apply_gain_pins(RX_DIRECTION, chain_t::TWO); _apply_gain_pins(TX_DIRECTION, chain_t::ONE); _apply_gain_pins(TX_DIRECTION, chain_t::TWO); } // TODO: review const-ness in this function with respect to ADI API void ad937x_device::_load_arm(std::vector & binary) { _call_api_function(std::bind(MYKONOS_initArm, mykonos_config.device)); if (binary.size() == ARM_BINARY_SIZE) { throw runtime_error("ad937x_device ARM is not the correct size!"); } _call_api_function(std::bind(MYKONOS_loadArmFromBinary, mykonos_config.device, &binary[0], binary.size())); } void ad937x_device::_run_initialization_calibrations() { _call_api_function(std::bind(MYKONOS_runInitCals, mykonos_config.device, INIT_CALS)); uint8_t errorFlag = 0; uint8_t errorCode = 0; _call_api_function( std::bind(MYKONOS_waitInitCals, mykonos_config.device, INIT_CAL_TIMEOUT_MS, &errorFlag, &errorCode)); if ((errorFlag != 0) || (errorCode != 0)) { mykonosInitCalStatus_t initCalStatus = { 0 }; _call_api_function(std::bind(MYKONOS_getInitCalStatus, mykonos_config.device, &initCalStatus)); // abort init cals uint32_t initCalsCompleted = 0; _call_api_function(std::bind(MYKONOS_abortInitCals, mykonos_config.device, &initCalsCompleted)); // init cals completed contains mask of cals that did finish uint16_t errorWord = 0; uint16_t statusWord = 0; _call_api_function(std::bind(MYKONOS_readArmCmdStatus, mykonos_config.device, &errorWord, &statusWord)); uint8_t status = 0; _call_api_function(std::bind(MYKONOS_readArmCmdStatusByte, mykonos_config.device, 2, &status)); } } void ad937x_device::_start_jesd() { // Stop and/or disable SYSREF // ensure BBIC JESD is reset and ready to receive CGS characters // prepare to transmit CGS when sysref starts _call_api_function(std::bind(MYKONOS_enableSysrefToRxFramer, mykonos_config.device, 1)); // prepare to transmit CGS when sysref starts //_call_api_function(std::bind(MYKONOS_enableSysrefToObsRxFramer, mykonos_config.device, 1)); // prepare to transmit CGS when sysref starts _call_api_function(std::bind(MYKONOS_enableSysrefToDeframer, mykonos_config.device, 0)); _call_api_function(std::bind(MYKONOS_resetDeframer, mykonos_config.device)); _call_api_function(std::bind(MYKONOS_enableSysrefToDeframer, mykonos_config.device, 1)); // make sure BBIC JESD framer is actively transmitting CGS // Start SYSREF // verify sync code here // verify links uint8_t framerStatus = 0; _call_api_function(std::bind(MYKONOS_readRxFramerStatus, mykonos_config.device, &framerStatus)); uint8_t deframerStatus = 0; _call_api_function(std::bind(MYKONOS_readDeframerStatus, mykonos_config.device, &deframerStatus)); } void ad937x_device::_enable_tracking_calibrations() { _call_api_function(std::bind(MYKONOS_enableTrackingCals, mykonos_config.device, TRACKING_CALS)); } ad937x_device::ad937x_device(spi_iface::sptr iface, gain_pins_t gain_pins) : full_spi_settings(iface), mykonos_config(&full_spi_settings.spi_settings), gain_ctrl(gain_pins) { _initialize(); } uint8_t ad937x_device::get_product_id() { uint8_t id; _call_api_function(std::bind(MYKONOS_getProductId, mykonos_config.device, &id)); return id; } uint8_t ad937x_device::get_device_rev() { uint8_t rev; _call_api_function(std::bind(MYKONOS_getDeviceRev, mykonos_config.device, &rev)); return rev; } api_version_t ad937x_device::get_api_version() { api_version_t api; _call_api_function(std::bind(MYKONOS_getApiVersion, mykonos_config.device, &api.silicon_ver, &api.major_ver, &api.minor_ver, &api.build_ver)); return api; } arm_version_t ad937x_device::get_arm_version() { arm_version_t arm; _call_api_function(std::bind(MYKONOS_getArmVersion, mykonos_config.device, &arm.major_ver, &arm.minor_ver, &arm.rc_ver)); return arm; } double ad937x_device::set_clock_rate(double req_rate) { auto rate = static_cast(req_rate / 1000.0); mykonos_config.device->clocks->deviceClock_kHz = rate; _call_api_function(std::bind(MYKONOS_initDigitalClocks, mykonos_config.device)); return static_cast(rate); } void ad937x_device::enable_channel(direction_t direction, chain_t chain, bool enable) { // TODO: // Turns out the only code in the API that actually sets the channel enable settings // is _initialize(). Need to figure out how to deal with this. } double ad937x_device::tune(direction_t direction, double value) { // I'm not sure why we set the PLL value in the config AND as a function parameter // but here it is mykonosRfPllName_t pll; uint64_t integer_value = static_cast(value); switch (direction) { case TX_DIRECTION: pll = TX_PLL; mykonos_config.device->tx->txPllLoFrequency_Hz = integer_value; break; case RX_DIRECTION: pll = RX_PLL; mykonos_config.device->rx->rxPllLoFrequency_Hz = integer_value; break; default: UHD_THROW_INVALID_CODE_PATH(); } _call_api_function(std::bind(MYKONOS_setRfPllFrequency, mykonos_config.device, pll, integer_value)); // TODO: coercion here causes extra device accesses, when the formula is provided on pg 119 of the user guide // Furthermore, because coerced is returned as an integer, it's not even accurate uint64_t coerced_pll; _call_api_function(std::bind(MYKONOS_getRfPllFrequency, mykonos_config.device, pll, &coerced_pll)); return static_cast(coerced_pll); } double ad937x_device::get_freq(direction_t direction) { mykonosRfPllName_t pll; switch (direction) { case TX_DIRECTION: pll = TX_PLL; break; case RX_DIRECTION: pll = RX_PLL; break; default: UHD_THROW_INVALID_CODE_PATH(); } // TODO: coercion here causes extra device accesses, when the formula is provided on pg 119 of the user guide // Furthermore, because coerced is returned as an integer, it's not even accurate uint64_t coerced_pll; _call_api_function(std::bind(MYKONOS_getRfPllFrequency, mykonos_config.device, pll, &coerced_pll)); return static_cast(coerced_pll); } bool ad937x_device::get_pll_lock_status(pll_t pll) { uint8_t pll_status; _call_api_function(std::bind(MYKONOS_checkPllsLockStatus, mykonos_config.device, &pll_status)); switch (pll) { case pll_t::CLK_SYNTH: return (pll_status & 0x01) > 0; case pll_t::RX_SYNTH: return (pll_status & 0x02) > 0; case pll_t::TX_SYNTH: return (pll_status & 0x04) > 0; case pll_t::SNIFF_SYNTH: return (pll_status & 0x08) > 0; case pll_t::CALPLL_SDM: return (pll_status & 0x10) > 0; default: UHD_THROW_INVALID_CODE_PATH(); return false; } } double ad937x_device::set_bw_filter(direction_t direction, chain_t chain, double value) { // TODO: implement return double(); } // RX Gain values are table entries given in mykonos_user.h // An array of gain values is programmed at initialization, which the API will then use for its gain values // In general, Gain Value = (255 - Gain Table Index) uint8_t ad937x_device::_convert_rx_gain(double gain) { // gain should be a value 0-60, add 195 to make 195-255 return static_cast((gain * 2) + 195); } // TX gain is completely different from RX gain for no good reason so deal with it // TX is set as attenuation using a value from 0-41950 mdB // Only increments of 50 mdB are valid uint16_t ad937x_device::_convert_tx_gain(double gain) { // attenuation is inverted and in mdB not dB return static_cast((MAX_TX_GAIN - (gain)) * 1e3); } double ad937x_device::set_gain(direction_t direction, chain_t chain, double value) { double coerced_value; switch (direction) { case TX_DIRECTION: { uint16_t attenuation = _convert_tx_gain(value); coerced_value = static_cast(attenuation); std::function func; switch (chain) { case chain_t::ONE: func = MYKONOS_setTx1Attenuation; break; case chain_t::TWO: func = MYKONOS_setTx2Attenuation; break; default: UHD_THROW_INVALID_CODE_PATH(); } _call_api_function(std::bind(func, mykonos_config.device, attenuation)); break; } case RX_DIRECTION: { uint8_t gain = _convert_rx_gain(value); coerced_value = static_cast(gain); std::function func; switch (chain) { case chain_t::ONE: func = MYKONOS_setRx1ManualGain; break; case chain_t::TWO: func = MYKONOS_setRx2ManualGain; break; default: UHD_THROW_INVALID_CODE_PATH(); } _call_api_function(std::bind(func, mykonos_config.device, gain)); break; } default: UHD_THROW_INVALID_CODE_PATH(); } return coerced_value; } void ad937x_device::set_agc_mode(direction_t direction, gain_mode_t mode) { switch (direction) { case RX_DIRECTION: switch (mode) { case gain_mode_t::MANUAL: _call_api_function(std::bind(MYKONOS_setRxGainControlMode, mykonos_config.device, MGC)); break; case gain_mode_t::AUTOMATIC: _call_api_function(std::bind(MYKONOS_setRxGainControlMode, mykonos_config.device, AGC)); break; case gain_mode_t::HYBRID: _call_api_function(std::bind(MYKONOS_setRxGainControlMode, mykonos_config.device, HYBRID)); break; default: UHD_THROW_INVALID_CODE_PATH(); } default: UHD_THROW_INVALID_CODE_PATH(); } } void ad937x_device::set_fir( const direction_t direction, const chain_t chain, int8_t gain, const std::vector & fir) { switch (direction) { case TX_DIRECTION: mykonos_config.tx_fir_config.set_fir(gain, fir); break; case RX_DIRECTION: mykonos_config.rx_fir_config.set_fir(gain, fir); break; default: UHD_THROW_INVALID_CODE_PATH(); } } std::vector ad937x_device::get_fir( const direction_t direction, const chain_t chain, int8_t &gain) { switch (direction) { case TX_DIRECTION: return mykonos_config.tx_fir_config.get_fir(gain); case RX_DIRECTION: return mykonos_config.rx_fir_config.get_fir(gain); default: UHD_THROW_INVALID_CODE_PATH(); } } int16_t ad937x_device::get_temperature() { // TODO: deal with the status.tempValid flag mykonosTempSensorStatus_t status; _call_gpio_api_function(std::bind(MYKONOS_readTempSensor, mykonos_config.device, &status)); return status.tempCode; } void ad937x_device::set_enable_gain_pins(direction_t direction, chain_t chain, bool enable) { gain_ctrl.config.at(direction).at(chain).enable = enable; _apply_gain_pins(direction, chain); } void ad937x_device::set_gain_pin_step_sizes(direction_t direction, chain_t chain, double inc_step, double dec_step) { if (direction == RX_DIRECTION) { gain_ctrl.config.at(direction).at(chain).inc_step = static_cast(inc_step / 0.5); gain_ctrl.config.at(direction).at(chain).dec_step = static_cast(dec_step / 0.5); } else if (direction == TX_DIRECTION) { // !!! TX is attenuation direction, so the pins are flipped !!! gain_ctrl.config.at(direction).at(chain).dec_step = static_cast(inc_step / 0.05); gain_ctrl.config.at(direction).at(chain).inc_step = static_cast(dec_step / 0.05); } else { UHD_THROW_INVALID_CODE_PATH(); } _apply_gain_pins(direction, chain); } void ad937x_device::_apply_gain_pins(direction_t direction, chain_t chain) { using namespace std::placeholders; // get this channels configuration auto chan = gain_ctrl.config.at(direction).at(chain); // TX direction does not support different steps per direction if (direction == TX_DIRECTION) { UHD_ASSERT_THROW(chan.inc_step == chan.dec_step); } switch (direction) { case RX_DIRECTION: { std::function func; switch (chain) { case chain_t::ONE: func = MYKONOS_setRx1GainCtrlPin; break; case chain_t::TWO: func = MYKONOS_setRx2GainCtrlPin; break; } _call_gpio_api_function( std::bind(func, mykonos_config.device, chan.inc_step, chan.dec_step, chan.inc_pin, chan.dec_pin, chan.enable)); } case TX_DIRECTION: { // TX sets attenuation, but the configuration should be stored correctly std::function func; switch (chain) { case chain_t::ONE: // TX1 has an extra parameter "useTx1ForTx2" that we do not support func = std::bind(MYKONOS_setTx1AttenCtrlPin, _1, _2, _3, _4, _5, 0); break; case chain_t::TWO: func = MYKONOS_setTx2AttenCtrlPin; break; } _call_gpio_api_function( std::bind(func, mykonos_config.device, chan.inc_step, chan.inc_pin, chan.dec_pin, chan.enable)); } } }