Reduce 5G WLAN TX power for selected channels and datarates on GFRG210/200
This patch provides the mechanism to patch the QCA8990 transmit power rates.
If it detects a mis-calibrated system, it will write a calibration patch out
to /tmp, which will be read when the driver is reloaded.
See b/30893979 for details.
Change-Id: If6f3065d6f0608d90c51b11f3e4150ff0efdc7d3
diff --git a/wifi/qca9880_cal.py b/wifi/qca9880_cal.py
new file mode 100755
index 0000000..4e5cc0c
--- /dev/null
+++ b/wifi/qca9880_cal.py
@@ -0,0 +1,240 @@
+#!/usr/bin/python -S
+
+"""Check and fix mis-calibrated QCA9880 modules on gfrg200/gfrg210.
+
+ Some modules were delivered to customers mis-calibrated. This script will
+ check if the module is affected, and if so, generate a patch that will be
+ used after driver reload.
+"""
+import glob
+import os
+import os.path
+import experiment
+import utils
+
+NO_CAL_EXPERIMENT = 'WifiNoCalibrationPatch'
+PLATFORM_FILE = '/etc/platform'
+CALIBRATION_DIR = '/tmp/ath10k_cal'
+CAL_PATCH_FILE = 'cal_data_patch.bin'
+ATH10K_CAL_DATA = '/sys/kernel/debug/ieee80211/phy[0-9]*/ath10k/cal_data'
+OUI_OFFSET = 6
+OUI_LEN = 3
+VERSION_OFFSET = 45
+VERSION_LEN = 3
+SUSPECT_OUIS = ((0x28, 0x24, 0xff), (0x48, 0xa9, 0xd2), (0x60, 0x02, 0xb4),
+ (0xbc, 0x30, 0x7d), (0xbc, 0x30, 0x7e))
+MISCALIBRATED_VERSION_FIELD = (0x0, 0x0, 0x0)
+MODULE_PATH = '/sys/class/net/{}/device/driver/module'
+
+
+def _log(msg):
+ utils.log('ath10k calibration: {}'.format(msg))
+
+
+def _is_ath10k(interface):
+ """Check if interface is driven by the ath10k driver.
+
+ Args:
+ interface: The interface to be checked. eg wlan1
+
+ Returns:
+ True if ath10k, otherwise False.
+ """
+ try:
+ return os.readlink(MODULE_PATH.format(interface)).find('ath10k')
+ except OSError:
+ return False
+
+
+def _oui_string(oui):
+ """Convert OUI from bytes to a string.
+
+ Args:
+ oui: OUI in byte format.
+
+ Returns:
+ OUI is string format separated by ':'. Eg. 88:dc:96.
+ """
+ return ':'.join('{:02x}'.format(ord(b)) for b in oui)
+
+
+def _version_string(version):
+ """Convert version from bytes to a string.
+
+ Args:
+ version: version in byte format.
+
+ Returns:
+ Three byte version string in hex format: 0x00 0x00 0x00
+ """
+
+ return ' '.join('0x{:02x}'.format(ord(b)) for b in version)
+
+
+def _is_module_miscalibrated():
+ """Check the QCA8990 module to see if it is improperly calibrated.
+
+ There are two manufacturers of the modules, Senao and Wistron of which only
+ Wistron modules are suspect. Wistron provided a list of OUIs manufactured
+ which are listed in SUSPECT_OUIS. Modules manufactured by Winstron containing
+ V02 at offset VERSION_OFFSET have been corrected, while those containing 3
+ zero's at this offset are still suspect and will be considered mis-calibrated.
+
+ Returns:
+ True if module is mis-calibrated, None if it can't be determined, and False
+ otherwise.
+ """
+
+ try:
+ cal_data_path = _ath10k_cal_data_path()
+ if cal_data_path is None:
+ return None
+
+ with open(cal_data_path, mode='rb') as f:
+ f.seek(OUI_OFFSET)
+ oui = f.read(OUI_LEN)
+ f.seek(VERSION_OFFSET)
+ version = f.read(VERSION_LEN)
+
+ except IOError as e:
+ _log('unable to open cal_data {}: {}'.format(cal_data_path, e.strerror))
+ return None
+
+ if oui not in (bytearray(s) for s in SUSPECT_OUIS):
+ _log('OUI {} is properly calibrated.'.format(_oui_string(oui)))
+ return False
+
+ if version != (bytearray(MISCALIBRATED_VERSION_FIELD)):
+ _log('version field {} signals proper calibration.'.
+ format(_version_string(version)))
+ return False
+
+ _log('May be mis-calibrated. OUI: {} version: {}'.
+ format(_oui_string(oui), _version_string(version)))
+
+ return True
+
+
+def _is_previously_calibrated():
+ """Check if this calibration script already ran since the last boot.
+
+ Returns:
+ True if calibration checks already ran, False otherwise.
+ """
+ return os.path.exists(CALIBRATION_DIR)
+
+
+def _create_calibration_dir():
+ """Create calibration directory.
+
+ Calibration directory contains the calibration patch file.
+ If the directory is empty it signals that calibration checks have already
+ run.
+
+ Returns:
+ True if directory exists or is created, false if any error.
+ """
+ try:
+ if not os.path.isdir(CALIBRATION_DIR):
+ os.makedirs(CALIBRATION_DIR)
+ return True
+ except OSError as e:
+ _log('unable to create calibration dir {}: {}.'.
+ format(CALIBRATION_DIR, e.strerror))
+ return False
+
+ return True
+
+
+def _ath10k_cal_data_path():
+ """Find the current path to cal data.
+
+ This path encodes the phy number, which is usually phy1, but if the
+ driver load order changed or if this runs after a reload, the phy
+ number will change.
+
+ Returns:
+ Path to cal_data in debugfs.
+ """
+
+ return glob.glob(ATH10K_CAL_DATA)[0]
+
+
+def _generate_calibration_patch():
+ """Create calibration patch and write to storage.
+
+ Returns:
+ True for success or False for failure.
+ """
+ try:
+ with open(_ath10k_cal_data_path(), mode='rb') as f:
+ cal_data = bytearray(f.read())
+ except IOError as e:
+ _log('cal patch: unable to open for read {}: {}.'.
+ format(_ath10k_cal_data_path(), e.strerror))
+ return False
+
+ # Patch cal_data here once we get the actual calibration data.
+ # For now just return False until we get the data.
+ _log('patch not generated as data not supplied yet.')
+ # pylint: disable=unreachable
+ return False
+
+ if not _create_calibration_dir():
+ return False
+
+ try:
+ patched_file = os.path.join(CALIBRATION_DIR, CAL_PATCH_FILE)
+ open(patched_file, 'wb').write(cal_data)
+ except IOError as e:
+ _log('unable to open for writing {}: {}.'.format(patched_file, e.strerror))
+ return False
+
+ return True
+
+
+def _reload_driver():
+ """Reload the ath10k driver so it picks up modified calibration file."""
+ ret = utils.subprocess_quiet(('rmmod', 'ath10k_pci'))
+ if ret != 0:
+ _log('rmmod ath10k_pci failed: {}.'.format(ret))
+ return
+
+ ret = utils.subprocess_quiet(('modprobe', 'ath10k_pci'))
+ if ret != 0:
+ _log('modprobe ath10k_pci failed: {}.'.format(ret))
+ return
+
+ _log('reload ath10k driver complete')
+
+
+def qca8990_calibration():
+ """Main QCA8990 calibration check."""
+
+ if experiment.enabled(NO_CAL_EXPERIMENT):
+ _log('experiment {} on. Skip calibration check.'.format(NO_CAL_EXPERIMENT))
+ return
+
+ if _is_previously_calibrated():
+ _log('calibration check completed earlier.')
+ return
+
+ if not _is_ath10k('wlan1'):
+ _log('this platform does not use ath10k.')
+ return
+
+ cal_result = _is_module_miscalibrated()
+ if cal_result is None:
+ _log('unknown if miscalibrated.')
+ elif not cal_result:
+ _log('module is NOT miscalibrated.')
+ # Creating an empty directory signals that this script has already run.
+ _create_calibration_dir()
+ else:
+ if _generate_calibration_patch():
+ _log('generated new patch.')
+ _reload_driver()
+
+
+if __name__ == '__main__':
+ qca8990_calibration()
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 4204791..34bb5e8 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -19,6 +19,7 @@
import iw
import options
import persist
+import qca9880_cal
import quantenna
import utils
@@ -254,6 +255,9 @@
'no wifi interface found for band=%s channel=%s suffix=%s',
band, channel, opt.interface_suffix)
+ # Check for calibration errors on ath10k.
+ qca9880_cal.qca8990_calibration()
+
found_active_config = False
for other_interface in (set(iw.find_all_interfaces_from_phy(phy)) -
set([interface])):