/***************************************************************************//** * @file * @brief PA power conversion functions provided to the customer as source for * highest level of customization. * @details This file contains the curves and logic that convert PA power * levels to dBm powers. ******************************************************************************* * # License * Copyright 2020 Silicon Laboratories Inc. www.silabs.com ******************************************************************************* * * SPDX-License-Identifier: Zlib * * The licensor of this software is Silicon Laboratories Inc. * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. * ******************************************************************************/ #include "em_device.h" #include "em_cmu.h" #include "em_common.h" #include "pa_conversions_efr32.h" #include "rail.h" static RAIL_TxPowerCurvesConfigAlt_t powerCurvesState; #if defined(_SILICON_LABS_32B_SERIES_1) || defined(_SILICON_LAS_32B_SERIES_2_CONFIG_1) #define PA_CONVERSION_MINIMUM_PWRLVL 1 #else #define PA_CONVERSION_MINIMUM_PWRLVL 0 #endif // This macro is defined when Silicon Labs builds this into the library as WEAK // to ensure it can be overriden by customer versions of these functions. It // should *not* be defined in a customer build. #ifdef RAIL_PA_CONVERSIONS_WEAK SL_WEAK #endif const RAIL_TxPowerCurves_t *RAIL_GetTxPowerCurve(RAIL_TxPowerMode_t mode) { static RAIL_TxPowerCurves_t powerCurves; // Check for an invalid Tx power mode if (mode >= RAIL_TX_POWER_MODE_NONE) { return NULL; } // Check for *_HIGHEST invalid Tx power modes on series 2 chips #if defined(RAIL_TX_POWER_MODE_2P4GIG_HIGHEST) if (mode == RAIL_TX_POWER_MODE_2P4GIG_HIGHEST) { return NULL; } #endif // RAIL_TX_POWER_MODE_2P4GIG_HIGHEST #if defined(RAIL_TX_POWER_MODE_SUBGIG_HIGHEST) if (mode == RAIL_TX_POWER_MODE_SUBGIG_HIGHEST) { return NULL; } #endif // RAIL_TX_POWER_MODE_SUBGIG_HIGHEST RAIL_TxPowerCurveAlt_t const *curve = powerCurvesState.curves[mode].conversion.powerCurve; // Check for an invalid power curve if (curve == NULL) { return NULL; } powerCurves.maxPower = curve->maxPower; powerCurves.minPower = curve->minPower; powerCurves.powerParams = &curve->powerParams[0]; return &powerCurves; } // This function will not be supported for any parts after efr32xg1x #ifdef RAIL_PA_CONVERSIONS_WEAK SL_WEAK #endif RAIL_Status_t RAIL_InitTxPowerCurves(const RAIL_TxPowerCurvesConfig_t *config) { #ifdef _SILICON_LABS_32B_SERIES_1 // First PA is 2.4 GHz high power, using a piecewise fit RAIL_PaDescriptor_t *current = &powerCurvesState.curves[RAIL_TX_POWER_MODE_2P4_HP]; current->algorithm = RAIL_PA_ALGORITHM_PIECEWISE_LINEAR; current->segments = config->piecewiseSegments; current->min = RAIL_TX_POWER_LEVEL_2P4_HP_MIN; current->max = RAIL_TX_POWER_LEVEL_2P4_HP_MAX; static RAIL_TxPowerCurveAlt_t txPower2p4 = { .minPower = 0U, .maxPower = 0U, .powerParams = { // The current max number of piecewise segments is 8 { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, } }; txPower2p4.maxPower = config->txPowerSgCurves->maxPower; txPower2p4.minPower = config->txPowerSgCurves->minPower; memcpy(&txPower2p4.powerParams[0], config->txPowerSgCurves->powerParams, config->piecewiseSegments * sizeof(RAIL_TxPowerCurveSegment_t)); current->conversion.powerCurve = &txPower2p4; // Second PA is 2.4 GHz low power, using a mapping table current = &powerCurvesState.curves[RAIL_TX_POWER_MODE_2P4_LP]; current->algorithm = RAIL_PA_ALGORITHM_MAPPING_TABLE; current->segments = 0U; current->min = RAIL_TX_POWER_LEVEL_2P4_LP_MIN; current->max = RAIL_TX_POWER_LEVEL_2P4_LP_MAX; current->conversion.mappingTable = config->txPower24LpCurves; // Third and final PA is Sub-GHz, using a piecewise fit current = &powerCurvesState.curves[RAIL_TX_POWER_MODE_SUBGIG]; current->algorithm = RAIL_PA_ALGORITHM_PIECEWISE_LINEAR; current->segments = config->piecewiseSegments; current->min = RAIL_TX_POWER_LEVEL_SUBGIG_MIN; current->max = RAIL_TX_POWER_LEVEL_SUBGIG_HP_MAX; static RAIL_TxPowerCurveAlt_t txPowerSubGig = { .minPower = 0U, .maxPower = 0U, .powerParams = { // The current max number of piecewise segments is 8 { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, { 0U, 0U, 0U }, } }; txPowerSubGig.maxPower = config->txPowerSgCurves->maxPower; txPowerSubGig.minPower = config->txPowerSgCurves->minPower; memcpy(&txPowerSubGig.powerParams[0], config->txPowerSgCurves->powerParams, config->piecewiseSegments * sizeof(RAIL_TxPowerCurveSegment_t)); current->conversion.powerCurve = &txPowerSubGig; return RAIL_STATUS_NO_ERROR; #else (void) config; return RAIL_STATUS_INVALID_CALL; #endif } #ifdef RAIL_PA_CONVERSIONS_WEAK SL_WEAK #endif RAIL_Status_t RAIL_InitTxPowerCurvesAlt(const RAIL_TxPowerCurvesConfigAlt_t *config) { RAIL_VerifyTxPowerCurves(config); powerCurvesState = *config; return RAIL_STATUS_NO_ERROR; } #ifdef RAIL_PA_CONVERSIONS_WEAK SL_WEAK #endif RAIL_TxPowerLevel_t RAIL_ConvertDbmToRaw(RAIL_Handle_t railHandle, RAIL_TxPowerMode_t mode, RAIL_TxPower_t power) { uint32_t powerLevel; int16_t powerIndex = 0; uint32_t minPowerLevel; (void)railHandle; // This function is called internally from the RAIL library, // so if the user never calls RAIL_InitTxPowerCurves - even // if they never intend to use dBm values in their code - // they'll always hit the assert below. Give the user a way // to not have to call RAIL_InitTxPowerCurves if they don't // care about dBm values by picking a dBm value that returns the // highest RAIL_TxPowerLevel_t possible. In other words, when // a channel dBm limitation greater than or equal to \ref RAIL_TX_POWER_MAX // is converted to raw units, the max RAIL_TxPowerLevel_t will be // returned. When compared to the current power level of the PA, // it will always be greater, indicating that no power coercion // is necessary to comply with channel limitations. if (power >= RAIL_TX_POWER_MAX) { return 255U; } // Check for an invalid Tx power mode if (mode >= RAIL_TX_POWER_MODE_NONE) { return 0U; } RAIL_PaDescriptor_t const *modeInfo = &powerCurvesState.curves[mode]; minPowerLevel = SL_MAX(modeInfo->min, PA_CONVERSION_MINIMUM_PWRLVL); // If we're in low power mode, just use the simple lookup table if (modeInfo->algorithm == RAIL_PA_ALGORITHM_MAPPING_TABLE) { // Loop through the lookup table to find the closest power level // without going over. for (powerIndex = (int16_t)(modeInfo->max - minPowerLevel); (powerIndex != 0) && (power < modeInfo->conversion.mappingTable[powerIndex]); powerIndex--) { // Searching... } return powerIndex + minPowerLevel; } // Here we know we're using the piecewise linear conversion RAIL_TxPowerCurveSegment_t const *powerParams; RAIL_TxPowerCurveAlt_t const *paParams = modeInfo->conversion.powerCurve; // Check for valid paParams before using them if (paParams == NULL) { return 0U; } // Cap the power based on the PA settings. if (power > paParams->maxPower) { // If we go above the maximum dbm the chip supports // Then provide maximum powerLevel power = paParams->maxPower; } else if (power < paParams->minPower) { // If we go below the minimum we want included in the curve fit, force it. power = paParams->minPower; } else { } // Map the power value to a 0 - 7 powerIndex value //There are 8 segments of step size of RAIL_TX_POWER_CURVE_INCREMENT in deci dBm //starting from maximum RAIL_TX_POWER_CURVE_MAX in deci dBm // These are just starting points to give the code // a rough idea of which segment to use, based on // how they were fit. Adjustments are made later on // if this turns out to be incorrect. RAIL_TxPower_t txPowerMax = RAIL_TX_POWER_CURVE_DEFAULT_MAX; RAIL_TxPower_t txPowerIncrement = RAIL_TX_POWER_CURVE_DEFAULT_INCREMENT; // if the first curve segment starts with RAIL_TX_POWER_LEVEL_INVALID //It is an extra curve segment to depict the maxpower and increment // (in deci-dBm) used while generating the curves. // The extra segment is only present when curve segment is generated by //using values different than the default - RAIL_TX_POWER_CURVE_DEFAULT_MAX // and RAIL_TX_POWER_CURVE_DEFAULT_INCREMENT. if ((paParams->powerParams[0].maxPowerLevel) == RAIL_TX_POWER_LEVEL_INVALID) { powerIndex += 1; txPowerMax = paParams->powerParams[0].slope; txPowerIncrement = paParams->powerParams[0].intercept; } powerIndex += ((txPowerMax - power) / txPowerIncrement); if (powerIndex > (int16_t)(modeInfo->segments - 1U)) { powerIndex = (int16_t)(modeInfo->segments - 1U); } do { // Select the correct piecewise segment to use for conversion. powerParams = &paParams->powerParams[powerIndex]; // powerLevel can only go down to 0. if (powerParams->intercept + powerParams->slope * power < 0) { powerLevel = 0U; } else { powerLevel = powerParams->intercept + powerParams->slope * power; } // Add 500 to do rounding correctly, as opposed to just rounding towards 0 powerLevel = ((powerLevel + 500U) / 1000U); // In case it turns out the resultant power level was too low and we have // to recalculate with the next curve... powerIndex++; } while ((powerIndex < (int16_t)modeInfo->segments) && (powerLevel <= paParams->powerParams[powerIndex].maxPowerLevel)); // We already know that powerIndex is at most modeInfo->segments if (powerLevel > paParams->powerParams[powerIndex - 1].maxPowerLevel) { powerLevel = paParams->powerParams[powerIndex - 1].maxPowerLevel; } // If we go below the minimum we want included in the curve fit, force it. if (powerLevel < minPowerLevel) { powerLevel = minPowerLevel; } return (RAIL_TxPowerLevel_t)powerLevel; } #ifdef RAIL_PA_CONVERSIONS_WEAK SL_WEAK #endif RAIL_TxPower_t RAIL_ConvertRawToDbm(RAIL_Handle_t railHandle, RAIL_TxPowerMode_t mode, RAIL_TxPowerLevel_t powerLevel) { (void)railHandle; // Check for an invalid Tx power mode if (mode >= RAIL_TX_POWER_MODE_NONE) { return RAIL_TX_POWER_MIN; } RAIL_PaDescriptor_t const *modeInfo = &powerCurvesState.curves[mode]; if (modeInfo->algorithm == RAIL_PA_ALGORITHM_MAPPING_TABLE) { // Limit the max power level if (powerLevel > modeInfo->max) { powerLevel = modeInfo->max; } // We 1-index low power PA power levels, but of course arrays are 0 indexed powerLevel -= SL_MAX(modeInfo->min, PA_CONVERSION_MINIMUM_PWRLVL); return modeInfo->conversion.mappingTable[powerLevel]; } else { #if defined(_SILICON_LABS_32B_SERIES_1) || defined(_SILICON_LAS_32B_SERIES_2_CONFIG_1) // Although 0 is a legitimate power on non-2.4 LP PA's and can be set via // "RAIL_SetTxPower(railHandle, 0)" it is MUCH lower than power // level 1 (approximately -50 dBm). Including it in the piecewise // linear fit would skew the curve substantially, so we exclude it // from the conversion. if (powerLevel == 0U) { return -500; } #endif int32_t power; RAIL_TxPowerCurveAlt_t const *powerCurve = modeInfo->conversion.powerCurve; // Check for a valid powerCurve pointer before using it if (powerCurve == NULL) { return RAIL_TX_POWER_MIN; } RAIL_TxPowerCurveSegment_t const *powerParams = powerCurve->powerParams; // Check for a valid powerParams pointer before using it if (powerParams == NULL) { return RAIL_TX_POWER_MIN; } // Hard code the extremes (i.e. don't use the curve fit) in order // to make it clear that we are reaching the extent of the chip's // capabilities if (powerLevel <= powerCurve->minPower) { return powerCurve->minPower; } else if (powerLevel >= modeInfo->max) { return powerCurve->maxPower; } else { } // Figure out which parameter to use based on the power level uint8_t x; uint8_t upperBound = modeInfo->segments - 1U; for (x = 0; x < upperBound; x++) { if (powerParams[x + 1U].maxPowerLevel < powerLevel) { break; } } power = ((1000 * (int32_t)(powerLevel)) - powerParams[x].intercept); power = ((power + (powerParams[x].slope / 2)) / powerParams[x].slope); if (power > powerCurve->maxPower) { return powerCurve->maxPower; } else if (power < powerCurve->minPower) { return powerCurve->minPower; } else { return (RAIL_TxPower_t)power; } } } #ifdef RAIL_PA_CONVERSIONS_WEAK SL_WEAK #endif RAIL_Status_t RAIL_GetTxPowerCurveLimits(RAIL_Handle_t railHandle, RAIL_TxPowerMode_t mode, RAIL_TxPower_t *maxPower, RAIL_TxPower_t *increment) { (void)railHandle; // Check for an invalid Tx power mode if (mode >= RAIL_TX_POWER_MODE_NONE) { return RAIL_STATUS_INVALID_PARAMETER; } //The power max info only for available Linear fit RAIL_PaDescriptor_t const *modeInfo = &powerCurvesState.curves[mode]; if (modeInfo->algorithm == RAIL_PA_ALGORITHM_MAPPING_TABLE) { return RAIL_STATUS_INVALID_CALL; } *maxPower = RAIL_TX_POWER_CURVE_DEFAULT_MAX; *increment = RAIL_TX_POWER_CURVE_DEFAULT_INCREMENT; RAIL_TxPowerCurveAlt_t const *paParams = modeInfo->conversion.powerCurve; if ((paParams->powerParams[0].maxPowerLevel) == RAIL_TX_POWER_LEVEL_INVALID) { *maxPower = paParams->powerParams[0].slope; *increment = (RAIL_TxPower_t)paParams->powerParams[0].intercept; } return RAIL_STATUS_NO_ERROR; } // This macro is defined when Silicon Labs builds curves into the library as WEAK // to ensure it can be overriden by customer versions of these functions. It // should *not* be defined in a customer build. #if !defined(RAIL_PA_CONVERSIONS_WEAK) #include "sl_rail_util_pa_config.h" void sl_rail_util_pa_init(void) { #if SL_RAIL_UTIL_PA_VOLTAGE_MV > 1800 (void)RAIL_InitTxPowerCurvesAlt(&RAIL_TxPowerCurvesVbat); #else (void)RAIL_InitTxPowerCurvesAlt(&RAIL_TxPowerCurvesDcdc); #endif #if SL_RAIL_UTIL_PA_CALIBRATION_ENABLE RAIL_EnablePaCal(true); #endif } static RAIL_TxPowerConfig_t txPowerConfig2p4Ghz = { .mode = SL_RAIL_UTIL_PA_SELECTION_2P4GHZ, .voltage = SL_RAIL_UTIL_PA_VOLTAGE_MV, .rampTime = SL_RAIL_UTIL_PA_RAMP_TIME_US, }; static RAIL_TxPowerConfig_t txPowerConfigSubGhz = { .mode = SL_RAIL_UTIL_PA_SELECTION_SUBGHZ, .voltage = SL_RAIL_UTIL_PA_VOLTAGE_MV, .rampTime = SL_RAIL_UTIL_PA_RAMP_TIME_US, }; RAIL_TxPowerConfig_t *sl_rail_util_pa_get_tx_power_config_2p4ghz(void) { return &txPowerConfig2p4Ghz; } RAIL_TxPowerConfig_t *sl_rail_util_pa_get_tx_power_config_subghz(void) { return &txPowerConfigSubGhz; } void sl_rail_util_pa_on_channel_config_change(RAIL_Handle_t rail_handle, const RAIL_ChannelConfigEntry_t *entry) { if (!RAIL_IsPaAutoModeEnabled(rail_handle)) { RAIL_TxPowerConfig_t currentTxPowerConfig; RAIL_TxPowerConfig_t *newTxPowerConfigPtr; RAIL_TxPower_t txPowerDeciDbm; RAIL_Status_t status; // Get current TX Power Config. status = RAIL_GetTxPowerConfig(rail_handle, ¤tTxPowerConfig); if (status != RAIL_STATUS_NO_ERROR) { while (true) { } // Error: Can't get TX Power Config } // Determine new TX Power Config. if (entry->baseFrequency < 1000000000UL) { newTxPowerConfigPtr = &txPowerConfigSubGhz; } else { newTxPowerConfigPtr = &txPowerConfig2p4Ghz; } // Call RAIL_ConfigTxPower only if TX Power Config mode has changed. if (currentTxPowerConfig.mode != newTxPowerConfigPtr->mode) { // Save current TX power before RAIL_ConfigTxPower (because not preserved). if (currentTxPowerConfig.mode == RAIL_TX_POWER_MODE_NONE) { txPowerDeciDbm = SL_RAIL_UTIL_PA_POWER_DECI_DBM; } else { txPowerDeciDbm = RAIL_GetTxPowerDbm(rail_handle); } // Apply new TX Power Config. status = RAIL_ConfigTxPower(rail_handle, newTxPowerConfigPtr); if (status != RAIL_STATUS_NO_ERROR) { while (true) { } // Error: Can't set TX Power Config } // Restore TX power after RAIL_ConfigTxPower. status = RAIL_SetTxPowerDbm(rail_handle, txPowerDeciDbm); if (status != RAIL_STATUS_NO_ERROR) { while (true) { } // Error: Can't set TX Power } } } // !RAIL_IsPaAutoModeEnabled } #endif // !RAIL_PA_CONVERSIONS_WEAK