blob: 2cf394c494a63300ad8b1a31347a85b5964c88b8 [file] [log] [blame]
#!/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 struct
import experiment
import utils
CAL_EXPERIMENT = 'WifiCalibrationPatch'
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'
SUSPECT_OUIS = ((0x28, 0x24, 0xff), (0x48, 0xa9, 0xd2), (0x60, 0x02, 0xb4),
(0xbc, 0x30, 0x7d), (0xbc, 0x30, 0x7e))
MODULE_PATH = '/sys/class/net/{}/device/driver/module'
# Each tuple starts with an offset, followed by a list of values to be
# patched beginning at that offset.
CAL_PATCH = ((0x050a, (0x5c, 0x68, 0xbd, 0xcd)),
(0x0510, (0x5c, 0x68, 0xbd, 0xcd)),
(0x0516, (0x5c, 0x68, 0xbd, 0xcd)),
(0x051c, (0x5c, 0x68, 0xbd, 0xcd)),
(0x0531, (0x2a, 0x28, 0x26)),
(0x0535, (0x2a, 0x28, 0x26)),
(0x056b, (0xce, 0x8a, 0x66, 0x02, 0x68, 0x26, 0x80, 0x66)),
(0x05b4, (0x8a, 0x46, 0x02, 0x68, 0x24, 0x80, 0x46)),
(0x05c0, (0x8a, 0x46, 0x02, 0x68, 0x24, 0x80, 0x46)),
(0x05fc, (0x8c, 0x68, 0x02, 0x88, 0x26, 0x80)),
(0x0608, (0x8c, 0x68, 0x02, 0x88, 0x26, 0x80)))
FCC_PATCH = ((0x0625, (0x50, 0x58, 0x5c, 0x8c, 0xbd, 0xc1, 0xcd,
0x4c, 0x50, 0x58, 0x5c, 0x8c, 0xbd, 0xc1,
0xcd, 0x4e, 0x56, 0x5e, 0x66, 0x8e)),
(0x06b4, (0x69, 0x6b, 0x6b, 0x62, 0x62, 0x6b, 0x6c,
0x2d, 0x69, 0x6b, 0x6b, 0x62, 0x62, 0x6b,
0x6d, 0x2d, 0x62, 0x6f, 0x68, 0x64, 0x64,
0x68, 0x68, 0x2d, 0x5c, 0x60, 0x60, 0x66)))
def _log(msg):
utils.log('ath10k calibration: {}'.format(msg))
def _is_ath10k(interface):
"""Check if interface is driven by the ath10k driver.
interface: The interface to be checked. eg wlan1
True if ath10k, otherwise False.
return os.readlink(MODULE_PATH.format(interface)).find('ath10k')
except OSError:
return False
def _oui_string(oui):
"""Convert OUI from bytes to a string.
oui: OUI in byte format.
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.
version: version in byte format.
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 suspect OUIs
which are listed in SUSPECT_OUIS.
The version field must also be checked, starting at offset VERSION_OFFSET.
If this fields is all zeros, then it is an implicit indication of V01,
otherwise it contains a version string.
V01 -- (version field contains 0's) These modules need both calibration and
FCC power limits patched.
V02 -- Only FCC power limits need to be patched
V03 -- No patching required.
A tuple containing one or both of: fcc, cal. Or None.
'fcc' -- FCC patching required.
'cal' -- Calibration data patching required.
None -- No patching required.
cal_data_path = _ath10k_cal_data_path()
if cal_data_path is None:
return None
with open(cal_data_path, mode='rb') as f:
oui =
version = struct.unpack('3s',[0]
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):
if not _cal_dir_exists():
_log('OUI {} is properly calibrated.'.format(_oui_string(oui)))
return None
# V01 is retroactively represented not by a string, but by 3 0 value bytes.
if version == '\x00\x00\x00':
if not _cal_dir_exists():
_log('version field is V01. CAL + FCC calibration required.')
return ('fcc', 'cal')
if version == 'V02':
if not _cal_dir_exists():
_log('version field is V02. Only FCC calibration required.')
return ('fcc',)
if version == 'V03':
if not _cal_dir_exists():
_log('version field is V03. No patching required.')
return None
if not _cal_dir_exists():
_log('version field unknown: {}'.format(version))
return None
def _cal_dir_exists():
return os.path.exists(CALIBRATION_DIR)
def _patch_exists():
return os.path.exists(os.path.join(CALIBRATION_DIR, CAL_PATCH_FILE))
def _create_cal_dir():
"""Create calibration directory.
Calibration directory contains the calibration patch file.
If the directory is empty it signals that calibration checks have already
True if directory exists or is created, false if any error.
if not os.path.isdir(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.
Path to cal_data in debugfs.
return glob.glob(ATH10K_CAL_DATA)[0]
def _apply_patch(msg, cal_data, patch):
for offset, values in patch:
cal_data[offset:offset + len(values)] = values
def _generate_calibration_patch(calibration_state):
"""Create calibration patch and write to storage.
calibration_state: data from ath10k to be patched.
True for success or False for failure.
with open(_ath10k_cal_data_path(), mode='rb') as f:
cal_data = bytearray(
except IOError as e:
_log('cal patch: unable to open for read {}: {}.'.
format(_ath10k_cal_data_path(), e.strerror))
return False
# Actual calibration starts here.
if 'cal' in calibration_state:
_apply_patch('Applying CAL patch...', cal_data, CAL_PATCH)
if 'fcc' in calibration_state:
_apply_patch('Applying FCC patch...', cal_data, FCC_PATCH)
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))
ret = utils.subprocess_quiet(('modprobe', 'ath10k_pci'))
if ret != 0:
_log('modprobe ath10k_pci failed: {}.'.format(ret))
_log('reload ath10k driver complete')
def qca8990_calibration():
"""Main QCA8990 calibration check."""
if not _is_ath10k('wlan1'):
if not _cal_dir_exists():
_log('This system does not use ath10k')
if not experiment.enabled(CAL_EXPERIMENT):
if _patch_exists():
os.remove(os.path.join(CALIBRATION_DIR, CAL_PATCH_FILE))
_log('experiment {} removed. Removed patch and reloaded driver.'.
if not _cal_dir_exists():
_log('experiment {} not active. Skipping calibration check.'
# Experiment is enabled.
calibration_state = _is_module_miscalibrated()
if calibration_state is not None and not _patch_exists():
if _generate_calibration_patch(calibration_state):
_log('generated new patch.')
if calibration_state is None:
if not _cal_dir_exists():
_log('This system does not need calibration.')
if __name__ == '__main__':