Commit ce169aca authored by Nick Kossifidis's avatar Nick Kossifidis Committed by John W. Linville

ath5k: Calibration re-work

Noise floor calibration does not interfere with traffic and should run more
often as part of our "short calibration". The full calibration is not the
noise floor calibration but the AGC + Gain_F (on RF5111 and RF5112) calibration
and should run less often because it does interfere with traffic.

So

Short calibration -> I/Q & NF Calibration
Long calibration -> Short + AGC + Gain_F

This patch was for some time on my pub/ dir on www.kernel.org and has been tested
by a few people and me. I think it's O.K. to go in.

I also changed ah_calibration to ah_iq_cal_needed to make more sense.

v2 Use a workqueue instead of a tasklet for calibration
Signed-off-by: default avatarNick Kossifidis <mickflemm@gmail.com>
Signed-off-by: default avatarJohn W. Linville <linville@tuxdriver.com>
parent 34ce644a
...@@ -187,10 +187,9 @@ ...@@ -187,10 +187,9 @@
#define AR5K_TUNE_MAX_TXPOWER 63 #define AR5K_TUNE_MAX_TXPOWER 63
#define AR5K_TUNE_DEFAULT_TXPOWER 25 #define AR5K_TUNE_DEFAULT_TXPOWER 25
#define AR5K_TUNE_TPC_TXPOWER false #define AR5K_TUNE_TPC_TXPOWER false
#define ATH5K_TUNE_CALIBRATION_INTERVAL_FULL 10000 /* 10 sec */ #define ATH5K_TUNE_CALIBRATION_INTERVAL_FULL 60000 /* 60 sec */
#define ATH5K_TUNE_CALIBRATION_INTERVAL_SHORT 10000 /* 10 sec */
#define ATH5K_TUNE_CALIBRATION_INTERVAL_ANI 1000 /* 1 sec */ #define ATH5K_TUNE_CALIBRATION_INTERVAL_ANI 1000 /* 1 sec */
#define ATH5K_TUNE_CALIBRATION_INTERVAL_NF 60000 /* 60 sec */
#define ATH5K_TX_COMPLETE_POLL_INT 3000 /* 3 sec */ #define ATH5K_TX_COMPLETE_POLL_INT 3000 /* 3 sec */
#define AR5K_INIT_CARR_SENSE_EN 1 #define AR5K_INIT_CARR_SENSE_EN 1
...@@ -896,7 +895,8 @@ enum ath5k_int { ...@@ -896,7 +895,8 @@ enum ath5k_int {
enum ath5k_calibration_mask { enum ath5k_calibration_mask {
AR5K_CALIBRATION_FULL = 0x01, AR5K_CALIBRATION_FULL = 0x01,
AR5K_CALIBRATION_SHORT = 0x02, AR5K_CALIBRATION_SHORT = 0x02,
AR5K_CALIBRATION_ANI = 0x04, AR5K_CALIBRATION_NF = 0x04,
AR5K_CALIBRATION_ANI = 0x08,
}; };
/* /*
...@@ -1098,6 +1098,7 @@ struct ath5k_hw { ...@@ -1098,6 +1098,7 @@ struct ath5k_hw {
led_on; /* pin setting for LED on */ led_on; /* pin setting for LED on */
struct work_struct reset_work; /* deferred chip reset */ struct work_struct reset_work; /* deferred chip reset */
struct work_struct calib_work; /* deferred phy calibration */
struct list_head rxbuf; /* receive buffer */ struct list_head rxbuf; /* receive buffer */
spinlock_t rxbuflock; spinlock_t rxbuflock;
...@@ -1114,8 +1115,6 @@ struct ath5k_hw { ...@@ -1114,8 +1115,6 @@ struct ath5k_hw {
struct ath5k_rfkill rf_kill; struct ath5k_rfkill rf_kill;
struct tasklet_struct calib; /* calibration tasklet */
spinlock_t block; /* protects beacon */ spinlock_t block; /* protects beacon */
struct tasklet_struct beacontq; /* beacon intr tasklet */ struct tasklet_struct beacontq; /* beacon intr tasklet */
struct list_head bcbuf; /* beacon buffer */ struct list_head bcbuf; /* beacon buffer */
...@@ -1145,7 +1144,7 @@ struct ath5k_hw { ...@@ -1145,7 +1144,7 @@ struct ath5k_hw {
enum ath5k_int ah_imr; enum ath5k_int ah_imr;
struct ieee80211_channel *ah_current_channel; struct ieee80211_channel *ah_current_channel;
bool ah_calibration; bool ah_iq_cal_needed;
bool ah_single_chip; bool ah_single_chip;
enum ath5k_version ah_version; enum ath5k_version ah_version;
...@@ -1235,8 +1234,8 @@ struct ath5k_hw { ...@@ -1235,8 +1234,8 @@ struct ath5k_hw {
/* Calibration timestamp */ /* Calibration timestamp */
unsigned long ah_cal_next_full; unsigned long ah_cal_next_full;
unsigned long ah_cal_next_short;
unsigned long ah_cal_next_ani; unsigned long ah_cal_next_ani;
unsigned long ah_cal_next_nf;
/* Calibration mask */ /* Calibration mask */
u8 ah_cal_mask; u8 ah_cal_mask;
......
...@@ -2112,16 +2112,29 @@ static void ...@@ -2112,16 +2112,29 @@ static void
ath5k_intr_calibration_poll(struct ath5k_hw *ah) ath5k_intr_calibration_poll(struct ath5k_hw *ah)
{ {
if (time_is_before_eq_jiffies(ah->ah_cal_next_ani) && if (time_is_before_eq_jiffies(ah->ah_cal_next_ani) &&
!(ah->ah_cal_mask & AR5K_CALIBRATION_FULL)) { !(ah->ah_cal_mask & AR5K_CALIBRATION_FULL) &&
/* run ANI only when full calibration is not active */ !(ah->ah_cal_mask & AR5K_CALIBRATION_SHORT)) {
/* Run ANI only when calibration is not active */
ah->ah_cal_next_ani = jiffies + ah->ah_cal_next_ani = jiffies +
msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_ANI); msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_ANI);
tasklet_schedule(&ah->ani_tasklet); tasklet_schedule(&ah->ani_tasklet);
} else if (time_is_before_eq_jiffies(ah->ah_cal_next_full)) { } else if (time_is_before_eq_jiffies(ah->ah_cal_next_short) &&
ah->ah_cal_next_full = jiffies + !(ah->ah_cal_mask & AR5K_CALIBRATION_FULL) &&
msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_FULL); !(ah->ah_cal_mask & AR5K_CALIBRATION_SHORT)) {
tasklet_schedule(&ah->calib);
/* Run calibration only when another calibration
* is not running.
*
* Note: This is for both full/short calibration,
* if it's time for a full one, ath5k_calibrate_work will deal
* with it. */
ah->ah_cal_next_short = jiffies +
msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_SHORT);
ieee80211_queue_work(ah->hw, &ah->calib_work);
} }
/* we could use SWI to generate enough interrupts to meet our /* we could use SWI to generate enough interrupts to meet our
* calibration interval requirements, if necessary: * calibration interval requirements, if necessary:
...@@ -2286,41 +2299,58 @@ ath5k_intr(int irq, void *dev_id) ...@@ -2286,41 +2299,58 @@ ath5k_intr(int irq, void *dev_id)
* for temperature/environment changes. * for temperature/environment changes.
*/ */
static void static void
ath5k_tasklet_calibrate(unsigned long data) ath5k_calibrate_work(struct work_struct *work)
{ {
struct ath5k_hw *ah = (void *)data; struct ath5k_hw *ah = container_of(work, struct ath5k_hw,
calib_work);
/* Should we run a full calibration ? */
if (time_is_before_eq_jiffies(ah->ah_cal_next_full)) {
/* Only full calibration for now */ ah->ah_cal_next_full = jiffies +
msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_FULL);
ah->ah_cal_mask |= AR5K_CALIBRATION_FULL; ah->ah_cal_mask |= AR5K_CALIBRATION_FULL;
ATH5K_DBG(ah, ATH5K_DEBUG_CALIBRATE, "channel %u/%x\n", ATH5K_DBG(ah, ATH5K_DEBUG_CALIBRATE,
ieee80211_frequency_to_channel(ah->curchan->center_freq), "running full calibration\n");
ah->curchan->hw_value);
if (ath5k_hw_gainf_calibrate(ah) == AR5K_RFGAIN_NEED_CHANGE) { if (ath5k_hw_gainf_calibrate(ah) == AR5K_RFGAIN_NEED_CHANGE) {
/* /*
* Rfgain is out of bounds, reset the chip * Rfgain is out of bounds, reset the chip
* to load new gain values. * to load new gain values.
*/ */
ATH5K_DBG(ah, ATH5K_DEBUG_RESET, "calibration, resetting\n"); ATH5K_DBG(ah, ATH5K_DEBUG_RESET,
"got new rfgain, resetting\n");
ieee80211_queue_work(ah->hw, &ah->reset_work); ieee80211_queue_work(ah->hw, &ah->reset_work);
} }
/* TODO: On full calibration we should stop TX here,
* so that it doesn't interfere (mostly due to gain_f
* calibration that messes with tx packets -see phy.c).
*
* NOTE: Stopping the queues from above is not enough
* to stop TX but saves us from disconecting (at least
* we don't lose packets). */
ieee80211_stop_queues(ah->hw);
} else
ah->ah_cal_mask |= AR5K_CALIBRATION_SHORT;
ATH5K_DBG(ah, ATH5K_DEBUG_CALIBRATE, "channel %u/%x\n",
ieee80211_frequency_to_channel(ah->curchan->center_freq),
ah->curchan->hw_value);
if (ath5k_hw_phy_calibrate(ah, ah->curchan)) if (ath5k_hw_phy_calibrate(ah, ah->curchan))
ATH5K_ERR(ah, "calibration of channel %u failed\n", ATH5K_ERR(ah, "calibration of channel %u failed\n",
ieee80211_frequency_to_channel( ieee80211_frequency_to_channel(
ah->curchan->center_freq)); ah->curchan->center_freq));
/* Noise floor calibration interrupts rx/tx path while I/Q calibration /* Clear calibration flags */
* doesn't. if (ah->ah_cal_mask & AR5K_CALIBRATION_FULL) {
* TODO: We should stop TX here, so that it doesn't interfere. ieee80211_wake_queues(ah->hw);
* Note that stopping the queues is not enough to stop TX! */
if (time_is_before_eq_jiffies(ah->ah_cal_next_nf)) {
ah->ah_cal_next_nf = jiffies +
msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_NF);
ath5k_hw_update_noise_floor(ah);
}
ah->ah_cal_mask &= ~AR5K_CALIBRATION_FULL; ah->ah_cal_mask &= ~AR5K_CALIBRATION_FULL;
} else if (ah->ah_cal_mask & AR5K_CALIBRATION_SHORT)
ah->ah_cal_mask &= ~AR5K_CALIBRATION_SHORT;
} }
...@@ -2639,7 +2669,6 @@ static void ath5k_stop_tasklets(struct ath5k_hw *ah) ...@@ -2639,7 +2669,6 @@ static void ath5k_stop_tasklets(struct ath5k_hw *ah)
ah->tx_pending = false; ah->tx_pending = false;
tasklet_kill(&ah->rxtq); tasklet_kill(&ah->rxtq);
tasklet_kill(&ah->txtq); tasklet_kill(&ah->txtq);
tasklet_kill(&ah->calib);
tasklet_kill(&ah->beacontq); tasklet_kill(&ah->beacontq);
tasklet_kill(&ah->ani_tasklet); tasklet_kill(&ah->ani_tasklet);
} }
...@@ -2743,9 +2772,24 @@ ath5k_reset(struct ath5k_hw *ah, struct ieee80211_channel *chan, ...@@ -2743,9 +2772,24 @@ ath5k_reset(struct ath5k_hw *ah, struct ieee80211_channel *chan,
ath5k_ani_init(ah, ani_mode); ath5k_ani_init(ah, ani_mode);
ah->ah_cal_next_full = jiffies + msecs_to_jiffies(100); /*
ah->ah_cal_next_ani = jiffies; * Set calibration intervals
ah->ah_cal_next_nf = jiffies; *
* Note: We don't need to run calibration imediately
* since some initial calibration is done on reset
* even for fast channel switching. Also on scanning
* this will get set again and again and it won't get
* executed unless we connect somewhere and spend some
* time on the channel (that's what calibration needs
* anyway to be accurate).
*/
ah->ah_cal_next_full = jiffies +
msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_FULL);
ah->ah_cal_next_ani = jiffies +
msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_ANI);
ah->ah_cal_next_short = jiffies +
msecs_to_jiffies(ATH5K_TUNE_CALIBRATION_INTERVAL_SHORT);
ewma_init(&ah->ah_beacon_rssi_avg, 1024, 8); ewma_init(&ah->ah_beacon_rssi_avg, 1024, 8);
/* clear survey data and cycle counters */ /* clear survey data and cycle counters */
...@@ -2895,11 +2939,11 @@ ath5k_init(struct ieee80211_hw *hw) ...@@ -2895,11 +2939,11 @@ ath5k_init(struct ieee80211_hw *hw)
tasklet_init(&ah->rxtq, ath5k_tasklet_rx, (unsigned long)ah); tasklet_init(&ah->rxtq, ath5k_tasklet_rx, (unsigned long)ah);
tasklet_init(&ah->txtq, ath5k_tasklet_tx, (unsigned long)ah); tasklet_init(&ah->txtq, ath5k_tasklet_tx, (unsigned long)ah);
tasklet_init(&ah->calib, ath5k_tasklet_calibrate, (unsigned long)ah);
tasklet_init(&ah->beacontq, ath5k_tasklet_beacon, (unsigned long)ah); tasklet_init(&ah->beacontq, ath5k_tasklet_beacon, (unsigned long)ah);
tasklet_init(&ah->ani_tasklet, ath5k_tasklet_ani, (unsigned long)ah); tasklet_init(&ah->ani_tasklet, ath5k_tasklet_ani, (unsigned long)ah);
INIT_WORK(&ah->reset_work, ath5k_reset_work); INIT_WORK(&ah->reset_work, ath5k_reset_work);
INIT_WORK(&ah->calib_work, ath5k_calibrate_work);
INIT_DELAYED_WORK(&ah->tx_complete_work, ath5k_tx_complete_poll_work); INIT_DELAYED_WORK(&ah->tx_complete_work, ath5k_tx_complete_poll_work);
ret = ath5k_hw_common(ah)->bus_ops->eeprom_read_mac(ah, mac); ret = ath5k_hw_common(ah)->bus_ops->eeprom_read_mac(ah, mac);
......
...@@ -338,9 +338,6 @@ static void ath5k_hw_wait_for_synth(struct ath5k_hw *ah, ...@@ -338,9 +338,6 @@ static void ath5k_hw_wait_for_synth(struct ath5k_hw *ah,
* And this is the MadWiFi bug entry related to the above * And this is the MadWiFi bug entry related to the above
* http://madwifi-project.org/ticket/1659 * http://madwifi-project.org/ticket/1659
* with various measurements and diagrams * with various measurements and diagrams
*
* TODO: Deal with power drops due to probes by setting an appropriate
* tx power on the probe packets ! Make this part of the calibration process.
*/ */
/* Initialize ah_gain during attach */ /* Initialize ah_gain during attach */
...@@ -372,10 +369,9 @@ int ath5k_hw_rfgain_opt_init(struct ath5k_hw *ah) ...@@ -372,10 +369,9 @@ int ath5k_hw_rfgain_opt_init(struct ath5k_hw *ah)
* tx power and a Peak to Average Power Detector (PAPD) will try * tx power and a Peak to Average Power Detector (PAPD) will try
* to measure the gain. * to measure the gain.
* *
* XXX: How about forcing a tx packet (bypassing PCU arbitrator etc) * TODO: Force a tx packet (bypassing PCU arbitrator etc)
* just after we enable the probe so that we don't mess with * just after we enable the probe so that we don't mess with
* standard traffic ? Maybe it's time to use sw interrupts and * standard traffic.
* a probe tasklet !!!
*/ */
static void ath5k_hw_request_rfgain_probe(struct ath5k_hw *ah) static void ath5k_hw_request_rfgain_probe(struct ath5k_hw *ah)
{ {
...@@ -575,9 +571,7 @@ static s8 ath5k_hw_rf_gainf_adjust(struct ath5k_hw *ah) ...@@ -575,9 +571,7 @@ static s8 ath5k_hw_rf_gainf_adjust(struct ath5k_hw *ah)
/* Main callback for thermal RF gain calibration engine /* Main callback for thermal RF gain calibration engine
* Check for a new gain reading and schedule an adjustment * Check for a new gain reading and schedule an adjustment
* if needed. * if needed.
* */
* TODO: Use sw interrupt to schedule reset if gain_F needs
* adjustment */
enum ath5k_rfgain ath5k_hw_gainf_calibrate(struct ath5k_hw *ah) enum ath5k_rfgain ath5k_hw_gainf_calibrate(struct ath5k_hw *ah)
{ {
u32 data, type; u32 data, type;
...@@ -1390,6 +1384,8 @@ void ath5k_hw_update_noise_floor(struct ath5k_hw *ah) ...@@ -1390,6 +1384,8 @@ void ath5k_hw_update_noise_floor(struct ath5k_hw *ah)
return; return;
} }
ah->ah_cal_mask |= AR5K_CALIBRATION_NF;
ee_mode = ath5k_eeprom_mode_from_channel(ah->ah_current_channel); ee_mode = ath5k_eeprom_mode_from_channel(ah->ah_current_channel);
/* completed NF calibration, test threshold */ /* completed NF calibration, test threshold */
...@@ -1434,6 +1430,8 @@ void ath5k_hw_update_noise_floor(struct ath5k_hw *ah) ...@@ -1434,6 +1430,8 @@ void ath5k_hw_update_noise_floor(struct ath5k_hw *ah)
ah->ah_noise_floor = nf; ah->ah_noise_floor = nf;
ah->ah_cal_mask &= ~AR5K_CALIBRATION_NF;
ATH5K_DBG(ah, ATH5K_DEBUG_CALIBRATE, ATH5K_DBG(ah, ATH5K_DEBUG_CALIBRATE,
"noise floor calibrated: %d\n", nf); "noise floor calibrated: %d\n", nf);
} }
...@@ -1547,12 +1545,19 @@ ath5k_hw_rf511x_iq_calibrate(struct ath5k_hw *ah) ...@@ -1547,12 +1545,19 @@ ath5k_hw_rf511x_iq_calibrate(struct ath5k_hw *ah)
s32 iq_corr, i_coff, i_coffd, q_coff, q_coffd; s32 iq_corr, i_coff, i_coffd, q_coff, q_coffd;
int i; int i;
if (!ah->ah_calibration || /* Skip if I/Q calibration is not needed or if it's still running */
ath5k_hw_reg_read(ah, AR5K_PHY_IQ) & AR5K_PHY_IQ_RUN) if (!ah->ah_iq_cal_needed)
return 0; return -EINVAL;
else if (ath5k_hw_reg_read(ah, AR5K_PHY_IQ) & AR5K_PHY_IQ_RUN) {
ATH5K_DBG_UNLIMIT(ah, ATH5K_DEBUG_CALIBRATE,
"I/Q calibration still running");
return -EBUSY;
}
/* Calibration has finished, get the results and re-run */ /* Calibration has finished, get the results and re-run */
/* work around empty results which can apparently happen on 5212 */
/* Work around for empty results which can apparently happen on 5212:
* Read registers up to 10 times until we get both i_pr and q_pwr */
for (i = 0; i <= 10; i++) { for (i = 0; i <= 10; i++) {
iq_corr = ath5k_hw_reg_read(ah, AR5K_PHY_IQRES_CAL_CORR); iq_corr = ath5k_hw_reg_read(ah, AR5K_PHY_IQRES_CAL_CORR);
i_pwr = ath5k_hw_reg_read(ah, AR5K_PHY_IQRES_CAL_PWR_I); i_pwr = ath5k_hw_reg_read(ah, AR5K_PHY_IQRES_CAL_PWR_I);
...@@ -1570,9 +1575,13 @@ ath5k_hw_rf511x_iq_calibrate(struct ath5k_hw *ah) ...@@ -1570,9 +1575,13 @@ ath5k_hw_rf511x_iq_calibrate(struct ath5k_hw *ah)
else else
q_coffd = q_pwr >> 7; q_coffd = q_pwr >> 7;
/* protect against divide by 0 and loss of sign bits */ /* In case i_coffd became zero, cancel calibration
* not only it's too small, it'll also result a divide
* by zero later on. */
if (i_coffd == 0 || q_coffd < 2) if (i_coffd == 0 || q_coffd < 2)
return 0; return -ECANCELED;
/* Protect against loss of sign bits */
i_coff = (-iq_corr) / i_coffd; i_coff = (-iq_corr) / i_coffd;
i_coff = clamp(i_coff, -32, 31); /* signed 6 bit */ i_coff = clamp(i_coff, -32, 31); /* signed 6 bit */
...@@ -1613,10 +1622,43 @@ int ath5k_hw_phy_calibrate(struct ath5k_hw *ah, ...@@ -1613,10 +1622,43 @@ int ath5k_hw_phy_calibrate(struct ath5k_hw *ah,
return ath5k_hw_rf5110_calibrate(ah, channel); return ath5k_hw_rf5110_calibrate(ah, channel);
ret = ath5k_hw_rf511x_iq_calibrate(ah); ret = ath5k_hw_rf511x_iq_calibrate(ah);
if (ret) {
ATH5K_DBG_UNLIMIT(ah, ATH5K_DEBUG_CALIBRATE,
"No I/Q correction performed (%uMHz)\n",
channel->center_freq);
if ((ah->ah_radio == AR5K_RF5111 || ah->ah_radio == AR5K_RF5112) && /* Happens all the time if there is not much
(channel->hw_value != AR5K_MODE_11B)) * traffic, consider it normal behaviour. */
ret = 0;
}
/* On full calibration do an AGC calibration and
* request a PAPD probe for gainf calibration if
* needed */
if (ah->ah_cal_mask & AR5K_CALIBRATION_FULL) {
AR5K_REG_ENABLE_BITS(ah, AR5K_PHY_AGCCTL,
AR5K_PHY_AGCCTL_CAL);
ret = ath5k_hw_register_timeout(ah, AR5K_PHY_AGCCTL,
AR5K_PHY_AGCCTL_CAL | AR5K_PHY_AGCCTL_NF,
0, false);
if (ret) {
ATH5K_ERR(ah,
"gain calibration timeout (%uMHz)\n",
channel->center_freq);
}
if ((ah->ah_radio == AR5K_RF5111 ||
ah->ah_radio == AR5K_RF5112)
&& (channel->hw_value != AR5K_MODE_11B))
ath5k_hw_request_rfgain_probe(ah); ath5k_hw_request_rfgain_probe(ah);
}
/* Update noise floor
* XXX: Only do this after AGC calibration */
if (!(ah->ah_cal_mask & AR5K_CALIBRATION_NF))
ath5k_hw_update_noise_floor(ah);
return ret; return ret;
} }
...@@ -3433,9 +3475,9 @@ int ath5k_hw_phy_init(struct ath5k_hw *ah, struct ieee80211_channel *channel, ...@@ -3433,9 +3475,9 @@ int ath5k_hw_phy_init(struct ath5k_hw *ah, struct ieee80211_channel *channel,
/* At the same time start I/Q calibration for QAM constellation /* At the same time start I/Q calibration for QAM constellation
* -no need for CCK- */ * -no need for CCK- */
ah->ah_calibration = false; ah->ah_iq_cal_needed = false;
if (!(mode == AR5K_MODE_11B)) { if (!(mode == AR5K_MODE_11B)) {
ah->ah_calibration = true; ah->ah_iq_cal_needed = true;
AR5K_REG_WRITE_BITS(ah, AR5K_PHY_IQ, AR5K_REG_WRITE_BITS(ah, AR5K_PHY_IQ,
AR5K_PHY_IQ_CAL_NUM_LOG_MAX, 15); AR5K_PHY_IQ_CAL_NUM_LOG_MAX, 15);
AR5K_REG_ENABLE_BITS(ah, AR5K_PHY_IQ, AR5K_REG_ENABLE_BITS(ah, AR5K_PHY_IQ,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment