上两篇追踪了扫描请求从 Framework 经 wificond 到 Supplicant 的完整软件路径。现在来到最底层——驱动和固件。Probe Request 是怎么变成空口电磁波的?息屏时 PNO 怎么做到几乎不耗电?WiFi 6E 的 59 个信道怎么高效扫描?驱动层是扫描性能和安全性的最后一道关口。
上篇回顾:在 05(上)我们追踪了 Framework 的节流和状态机、wificond 的参数翻译、nl80211 的命令构建。在 05(中)我们深入了 Supplicant 的扫描引擎——eloop 调度、350 行参数构建、radio work 排队、BSS 缓存管理。现在,Supplicant 已经把
NL80211_CMD_TRIGGER_SCAN发给了内核……驱动收到后怎么做?
本章导读
本章比喻:如果说 Framework 是雷达指挥中心、Supplicant 是信号处理机,那么驱动就是雷达的天线控制器和发射机。它把来自上层的扫描参数翻译成天线能执行的物理操作——调到哪个频率、停留多久、用什么增益、发什么帧。QCOM 的 SCM(Scan Manager)像一台精心调校的工业控制器,所有扫描操作必须通过序列化队列排队。MTK 的 Scan FSM 像一个紧凑的嵌入式状态机——只有两个状态,简单直接。PNO 是雷达的低功耗值守模式——不必一直开满功率,只在预设信道上周期性侦听。WiFi 6E/7 的新机制则是为新的频段设计的更高效的扫描策略。
你将学到:
•QCOM 驱动扫描全链路:HDD → OSIF → SCM → 序列化 → WMI → 固件 → inform_bss → cfg80211_scan_done•MTK 驱动扫描全链路:cfg80211 → ioctl → OID → AIS FSM → HEM mailbox → SCN FSM → 固件•QCOM vs MTK 架构对比:通信机制、FSM 风格、恢复策略的差异•PNO 的完整四层链路:Framework PnoScanStateMachine → wificond IWifiScannerImpl.startPnoScan → NL80211_CMD_START_SCHED_SCAN → 固件(supplicant 的 wpas_start_pno 是独立路径)•PNO 指数退避策略:Android 12+ 基于移动状态的动态间隔•WiFi 6E/7 扫描新机制:PSC、RNR、Multi-BSSID、FILS 发现帧•四层日志对照方法和常见问题排查
数据来源:
•QCOM qcacld-3.0(高通 WiFi 驱动,原 Code Aurora 源已停运,当前镜像见 CodeLinaro)•MTK gen4m[1](联发科 WiFi 驱动)•external/wpa_supplicant_8[2]•802.11-2024 §11.1.4(扫描过程)、§9.4.2.166(RNR)、§11.10.14(Multi-BSSID)
代码说明:本文代码来自真实源码,关键路径保留,细节有精简(精简处已标注)。QCOM 和 MTK 路径各有标注。
一、QCOM 驱动:SCM → 序列化 → WMI → 固件
一句话总结:QCOM 的扫描路径经过 5 层——cfg80211 回调(HDD)→ OSIF 转换 → Scan Manager(SCM)调度 → 序列化队列 → WMI 命令下发固件。每层有明确的职责边界。

1.1 扫描请求怎么进入 SCM?
内核收到 NL80211_CMD_TRIGGER_SCAN 后调用注册在 cfg80211_ops 表中的 scan 回调。QCOM 驱动注册的是 wlan_hdd_cfg80211_scan():
// qcacld-3.0/core/hdd/src/wlan_hdd_scan.cint wlan_hdd_cfg80211_scan(struct wiphy *wiphy,struct cfg80211_scan_request *request){int errno;struct osif_vdev_sync *vdev_sync;errno = osif_vdev_sync_op_start(request->wdev->netdev, &vdev_sync);if (errno)return errno;errno = __wlan_hdd_cfg80211_scan(wiphy, request, NL_SCAN);osif_vdev_sync_op_stop(vdev_sync);return errno;}
主要功能:
•osif_vdev_sync_op_start/stop 是 vdev(虚拟设备)级别的同步锁——防止并发操作同一个接口•__wlan_hdd_cfg80211_scan() 构建 scan_params 桥接结构(包含 source=NL_SCAN、default_ie、vendor_ie 等),然后调用 OSIF 层的 wlan_cfg80211_scan()
1.2 wlan_cfg80211_scan() —— 参数转换中枢

wlan_cfg80211_scan() 是内核 cfg80211_scan_request 到 QCOM 内部 scan_start_request 的转换中心:
request->n_ssidsrequest->ssids[] | req->num_ssids / req->ssid[] | WLAN_SCAN_MAX_NUM_SSID |
request->n_channels | req->chan_list.num_chan | |
request->ie / ie_len | req->extra_ie / extra_ie_len | |
request->flags | req->scan_policy_* | NL80211_SCAN_FLAG_HIGH_ACCURACYhigh_accuracy=true |
转换完成后调用 wlan_schedule_scan_start_request() → 通过 QDF(QCOM Driver Framework)的消息调度器投递到 scm_scan_start_req()。
1.3 scm_scan_start_req() —— Scan Manager 调度
// qca-wifi-host-cmn/umac/scan/core/src/wlan_scan_manager.cQDF_STATUS scm_scan_start_req(struct scheduler_msg *msg){struct wlan_serialization_command cmd = {0};struct scan_start_request *req = msg->bodyptr;struct wlan_scan_obj *scan_obj;QDF_STATUS status; // 省略初始值 QDF_STATUS_SUCCESSif (!scm_is_scan_allowed(req->vdev)) {status = QDF_STATUS_E_NULL_VALUE;goto err;}scan_obj = wlan_vdev_get_scan_obj(req->vdev);// ...更新扫描参数...if (!req->scan_req.chan_list.num_chan) {scm_info("Reject 0 channel Scan");goto err;}cmd.cmd_type = WLAN_SER_CMD_SCAN;cmd.cmd_id = req->scan_req.scan_id;cmd.cmd_cb = scm_scan_serialize_callback; // 激活时的回调cmd.umac_cmd = req;cmd.source = WLAN_UMAC_COMP_SCAN;cmd.cmd_timeout_duration = req->scan_req.max_scan_time+ SCAN_TIMEOUT_GRACE_PERIOD;cmd.vdev = req->vdev;ser_cmd_status = wlan_serialization_request(&cmd);// ACTIVE → 直接执行 / PENDING → 等待 / 其他 → 失败}
主要功能:
•前置校验:scm_is_scan_allowed()(扫描是否被禁用)、信道列表不能为空•超时设置:max_scan_time + GRACE_PERIOD——如果固件在此时限内没有回复扫描完成,序列化框架触发超时。注意如果 scan_obj->disable_timeout 为 true(如 P2P 扫描场景),超时被禁用(cmd_timeout_duration = 0)•提交到序列化队列:wlan_serialization_request() 返回 ACTIVE(立即执行)、PENDING(排队等待)、或 DENIED(拒绝,含多种细分原因:规则失败、队列满等)
1.4 序列化机制:为什么最多 8 个并发扫描命令?
⚠️ 关键澄清:"8 个并发"指序列化框架 Active Queue 的软件深度,不是 8 个 FEM 同时工作。物理上 DBS(Dual Band Simultaneous)只有 2 个 FEM,真正的并发射频操作只有 2 路。其他 6 个 active 命令是在等待 FEM 资源或与其他操作时分复用——序列化框架通过
cmd_timeout_duration和wlan_serialization_timer管理超时和取消。
回到雷达比喻——QCOM 的序列化框架就像工厂的流水线调度系统:每个工位(射频)同一时间只能处理一个工件(扫描/连接/P2P 操作)。Active Queue 是正在加工中的工件(最多 8 个),Pending Queue(最多 24 个)是等待上工位的队列。SCM 通过 scm_scan_serialize_callback 在四个时刻被通知:轮到你了、被取消了、超时了、释放资源。
QCOM 的序列化框架(wlan_serialization)是一个通用的命令排队系统,不是扫描专用的。它维护两个队列:
Active Queue (最多 WLAN_SER_MAX_ACTIVE_SCAN_CMDS = 8 个) ← wlan_serialization_main_i.h├── cmd[0]: SCAN scan_id=42 (运行中...)├── cmd[1]: SCAN scan_id=43 (运行中...)└── cmd[2]: CONNECT net_id=5 (运行中...)Pending Queue (最多 WLAN_SER_MAX_PENDING_SCAN_CMDS = 24 个) ← wlan_serialization_main_i.h│ 注:24 是 SCAN 专用常量,与通用命令的 pending 队列(由 AP/STA config 控制)独立├── cmd[0]: SCAN scan_id=44 (等待中...)├── cmd[1]: P2P_LISTEN freq=2437 (等待中...)└── cmd[2]: ROC freq=5180 (等待中...)
激活条件(wlan_serialization_is_active_scan_cmd_allowed()):
1.Active Queue 中同类型命令数 < MAX_ACTIVE_SCAN_CMDS(默认 8)2.Pending Queue 为空(先到先服务,不插队)
为什么是 8? 这个数字来自固件的并发扫描能力——高通固件最多支持 8 个 vdev 同时执行扫描。驱动侧的上限与之匹配。
状态机驱动:scm_scan_serialize_callback() 是序列化框架通知 SCM 的回调——它根据 cb_reason 分发:
WLAN_SER_CB_ 前缀) | ||
ACTIVATE_CMD | scm_activate_scan_request() → tgt_scan_start() → WMI 下发固件 | |
CANCEL_CMD | ||
ACTIVE_CMD_TIMEOUT | scm_cancel_scan_request() → 发送 WMI stop 到固件 | |
RELEASE_MEM_CMD |
1.5 WMI 命令下发了哪些参数给固件?
Tips:如果你只关心扫描流程,可以跳过下面的参数表,直接看 §1.6 扫描结果回来。参数表供需要调参或排查问题的读者参考。
当 scan work 被激活后,tgt_scan_start() → send_scan_start_cmd_tlv() 构建 WMI 命令。以下是 QCOM WMI 层 wmi_unified_tlv.c:4563-4676 中传递给固件的参数:
基础参数:
scan_id | |||
vdev_id | |||
scan_type | |||
scan_ctrl_flags | |||
scan_priority |
信道与定时参数:
dwell_time_active | CFG_ACTIVE_MAX_CHANNEL_TIME) | |
dwell_time_passive | CFG_PASSIVE_MAX_CHANNEL_TIME) | |
dwell_time_active_6g | 60 ms | CFG_ACTIVE_MAX_6G_CHANNEL_TIME) |
dwell_time_passive_6g | 60 ms | CFG_PASSIVE_MAX_6G_CHANNEL_TIME) |
min_dwell_time_6g | CFG_MIN_6G_CHANNEL_TIME) |
来源说明:这些值来自
cfg.ini参数经由scan_def结构体到scan_req再到 WMI 的完整链路——cfg_get(CFG_ACTIVE_MAX_CHANNEL_TIME)→scan_def.active_dwell→scan_req.dwell_time_active→send_scan_start_cmd_tlv()。不存在WMI_DWELL_TIME_ACTIVE_DEFAULT这个编译期常量——WMI 层是纯透传。并发场景下conc_active_dwell这个独立参数(40ms)会覆盖主动 dwell,但 6GHz 的并发值和默认值都是60ms。 |probe_time| 20 ms | 每次 Probe Request 的探测时间 | |n_probes|0(移动)/ 2(非移动) |CFG_SCAN_NUM_PROBES(cfg_scan.h)——PLATFORM_VALUE(0, 2)。移动平台默认 0 表示由固件自主决定 Probe 数量(固件有内置最小值),并非完全不发 Probe Request;非移动平台默认 2 | |rest_time| min 50ms / max 100ms | home 信道的恢复时间(扫描其他信道后回到工作信道) | |probe_spacing| 0 | Probe 之间的最小间隔 | |idle_time| 25 ms | 信道上的空闲等待时间 | |probe_delay| 0 | 信道调谐后的稳定等待时间 | |adaptive_dwell_mode| 4 | 自适应驻留模式 | |burst_duration| 0 | 突发扫描持续时间(0 = 禁用) |
SSID 与 BSSID 限制:
WLAN_SCAN_MAX_NUM_SSID | 16 | 10 | |
WLAN_SCAN_MAX_NUM_BSSID | 4 | 10 | |
MAX_RNR_BSS | 33 | 5 | |
MAX_DEFAULT_SCAN_IE_LEN |
序列化队列容量(wlan_serialization_main_i.h):
WLAN_SER_MAX_ACTIVE_SCAN_CMDS | 8 | |
WLAN_SER_MAX_PENDING_SCAN_CMDS | 24 | |
WLAN_MAX_ACTIVE_SCANS_ALLOWED | 8 | |
WMA_HW_DEF_SCAN_MAX_DURATION |
一个典型全频段扫描的耗时分解(26 个信道,主动 40ms × 22 + 被动 110ms × 4,含 BSS dwell):
2.4G active channels: 13 × 40ms = 520ms5G active channels: 9 × 40ms = 360ms5G passive (DFS): 4 × 110ms = 440ms─────────────────────────────────────────纯信道 dwell total: 1320msBSS dwell (有 AP 时额外停留): 11 × ~100ms ≈ 1100ms(100ms 为典型环境实测经验值——BSS dwell 时间由固件根据 AP 密度和 Beacon 间隔动态决定,非编译期常量。在 AP 稀疏或 Beacon 间隔长的环境中可能更短,在密集环境中可能更长)固件处理开销: ~500ms─────────────────────────────────────────总扫描耗时(1320+1100+500=2920ms): ~2.9s → 最终被 `scan_max_duration`(30s)截断前自然完成
关键洞察:BSS dwell 在有 AP 的信道上额外消耗了大量时间——每发现一个 AP 就要额外停留 100ms 来收集完整的 Beacon/Probe Response 数据。这就是为什么信号密集环境中扫描更慢。
1.6 扫描结果回来——双管道上报
固件完成扫描后,结果通过两条独立的管道回传:
管道 A:逐 BSS 实时上报
固件每扫到一个 AP,立即通过 WMI 发送 beacon/probe response 帧数据:
固件 → WMI 事件 → tgt_scan_bcn_probe_rx_callback()→ 构建 scan_bcn_probe_event→ SCM 调度 → scm_handle_bcn_probe()→ scm_add_update_entry()→ wlan_cfg80211_inform_bss_frame()→ cfg80211_inform_bss_frame_data() ← 内核 API
这条管道让内核的 BSS 数据库在扫描过程中就逐步被填充——不需要等扫描全部完成。
管道 B:扫描完成事件
固件的 WMI_SCAN_EVENTID 表示扫描生命周期事件(一次扫描完成、被取消、启动失败等):
固件 WMI_SCAN_EVENTID → target_if_scan_event_handler()→ tgt_scan_event_handler()→ scm_scan_event_handler()→ scm_scan_post_event() → 通知所有注册的 listener→ wlan_cfg80211_scan_done_callback()→ cfg80211_scan_done(request, &info) ← 内核 API
cfg80211_scan_done() 是扫描的最终信号——内核收到后通过 nl80211 multicast 通知 Supplicant:扫描完成,可以来取结果了。
1.7 扫描结果返回全链路(连贯起来)
把下发和返回放在一起看,一次 QCOM 扫描的完整时序(对照上方时序图 05c-qcom-scan-sequence.png):

1.8 扫描取消与超时
QCOM 提供两种取消方式:
| 异步取消 | wlan_abort_scan()ucfg_scan_cancel() | |
| 同步取消 | ucfg_scan_cancel_sync() | SCM_CANCEL_SCAN_WAIT_ITERATION 次) |
超时路径:序列化框架在 cmd_timeout_duration 到期后自动调用 scm_scan_serialize_callback(ACTIVE_CMD_TIMEOUT) → 发 WMI stop → 标记扫描失败。
二、MTK 驱动:Scan FSM → 7 层调用 → 固件
一句话总结:MTK 的扫描路径比 QCOM 长——7 层调用,但 FSM 更简单——只有 IDLE 和 SCANNING 两个状态。MTK 更贴近硬件(直接操作 DMA 环),容错设计(§2.4 三级恢复)更精细。

2.1 7 层调用链
cfg80211_ops.scan = mtk_cfg_scan [gl_init.c]→ mtk_cfg80211_scan() [gl_cfg80211.c]→ kalIoctl(wlanoidSetBssidListScanAdv) [私有 ioctl]→ wlanoidSetBssidListScanAdv() [wlan_oid.c]→ aisFsmScanRequestAdv() [ais_fsm.c: AIS FSM]→ mboxSendMsg(MID_AIS_SCN_SCAN_REQ_V2) [hem_mbox.c: 邮箱]→ scnFsmMsgStart() [scan_fsm.c: SCN FSM]→ scnSendScanReqV2() [下发 CMD_SCAN_REQ_V2 → 固件]
每层职责一句话:
1.gl_init.c — 注册 cfg80211_ops2.gl_cfg80211.c — cfg80211 回调 → MTK 内部格式3.wlan_oid.c — OID(Object ID)接口,参数第一重转换4.ais_fsm.c — AIS FSM(STA 模式状态机),协调扫描与连接的关系5.hem_mbox.c — HEM(Hardware Event Mailbox),模块间消息传递6.scan_fsm.c — SCN FSM(扫描状态机),管理扫描生命周期7.固件 — 执行射频操作
参数的三重转换:
PARAM_SCAN_REQUEST_ADV | ||
MSG_SCN_SCAN_REQ_V2 | ||
CMD_SCAN_REQ_V2 |
2.2 Scan FSM — 只有两个状态
回到比喻——MTK 的 Scan FSM 像一个紧凑的嵌入式控制器,不像 QCOM 的工业流水线那样层层抽象。它只有两个状态,但通过 do-while 链式转换在一次调用中完成状态切换和命令下发——这是一种典型的嵌入式设计哲学:简单、直接、不浪费资源。
MTK 的扫描状态机出奇地简单:
// scan_fsm.c: scnFsmSteps() — 真实源码使用命名的 enum ENUM_SCAN_STATE 类型,// 且 SCAN_STATE_SCANNING 后有 SCAN_STATE_NUM 用于边界检查(此处省略)enum {SCAN_STATE_IDLE = 0, // 空闲——等待扫描请求SCAN_STATE_SCANNING // 正在扫描——固件执行信道探测};
没有独立的 Start/Done/Abort 状态。真实代码通过 do-while 顶部的无条件赋值prScanInfo->eCurrentState = eNextState + IDLE case 内覆写 eNextState 参数实现链式转换——不是 case 内的 if 条件分支:
// scan_fsm.c: scnFsmSteps() — 教学简化版// 省略:pending list 消息取出(LINK_REMOVE_HEAD)、V1/V2 消息分发、cnmMemFree 释放消息;// 真实源码处理 8 种消息类型(MID_AIS_SCN_SCAN_REQ、MID_AIS_SCN_SCAN_REQ_V2、MID_BOW_*、// MID_P2P_*、MID_RLM_*),此处直接展示 V2 路径;函数签名含 prAdapter 和 eNextState 参数do {fgIsTransition = FALSE;prScanInfo->eCurrentState = eNextState; // 顶部无条件赋值——这是链式转换的核心switch (prScanInfo->eCurrentState) {case SCAN_STATE_IDLE:scnFsmHandleScanMsgV2(); // 解析参数,填充 SCAN_PARAM 结构体// scnFsmSteps() IDLE case 在 scnFsmHandleScanMsgV2() 返回后,// 将 eNextState 设为 SCAN_STATE_SCANNING(由函数内部的 eNextState 赋值实现)if (eNextState != SCAN_STATE_IDLE) {fgIsTransition = TRUE; // do-while 再次循环,进入 SCANNING 分支}break;case SCAN_STATE_SCANNING:scnSendScanReqV2(); // 🟢 下发扫描到固件(第二次循环才到这里)break;}} while (fgIsTransition);
同时,扫描完成和取消的处理在独立函数 scnEventScanDone() 和 scnFsmMsgAbort() 中——它们不在 FSM 的 switch-case 里。
关键函数详解:
scnFsmHandleScanMsgV2() | scan_fsm.c | SCAN_PARAM 结构体。IDLE 状态下调用,仅解析参数——eNextState 是 scnFsmSteps() 的局部参数,该函数无法直接访问;状态转移由 scnFsmSteps() IDLE case 在调用返回后直接赋值 eNextState = SCAN_STATE_SCANNING 完成 |
scnSendScanReqV2() | scan_fsm.c | SCAN_PARAM 打包为 CMD_SCAN_REQ_V2,通过 WFDMA DMA 环发送到固件。设置扫描超时定时器——如果固件在超时前没有回复完成事件,触发 §2.4 的恢复链 |
scnEventScanDone() | scan_fsm.c | scanReportBss2Cfg80211() 逐个 BSS 上报给 cfg80211,最终调用 cfg80211_scan_done() 通知内核 |
scnFsmSteps() | scan_fsm.c | do-while 循环执行状态转换。IDLE case 调用 scnFsmHandleScanMsgV2(),SCANNING case 调用 scnSendScanReqV2()。循环直到 fgIsTransition == FALSE |
scanReportBss2Cfg80211() | scan.c | cfg80211_bss 格式,逐个调用 cfg80211_inform_bss_frame() 上报给内核 |
MTK 扫描的完整生命周期:
请求到达:mtk_cfg80211_scan()→ aisFsmScanRequestAdv() // AIS FSM 检查是否允许扫描→ mboxSendMsg() // 投递到 SCN FSM 邮箱执行:scnFsmSteps() [IDLE]→ scnFsmHandleScanMsgV2() // 解析参数→ eNextState = SCANNING // 状态转换scnFsmSteps() [SCANNING]→ scnSendScanReqV2() // 下发固件 + 启动超时定时器完成:固件中断 → scnEventScanDone()→ scanReportBss2Cfg80211() // 逐 BSS 上报→ cfg80211_scan_done() // 通知内核扫描完成取消:scnFsmMsgAbort()→ CMD_ID_SCAN_CANCEL → 固件→ scnFsmGenerateScanDoneMsg(CANCELLED) // 立即生成内部完成消息
2.3 QCOM vs MTK 架构对比
| 通信机制 | ||
| 下发层数 | ||
| FSM 风格 | ||
| 并发控制 | ||
| 参数格式 | scan_start_request 结构体,一次转换 | |
| 错误恢复 | ||
| 扫描缓存 | scan_cache.c——缓存结果跳过固件扫描 | |
| 架构哲学 |
核心差异:
•QCOM 像一座设计精良的工厂——每层有明确职责,序列化框架是所有命令的统一调度器•MTK 像一个紧凑的嵌入式系统——层次深但每层职责薄,FSM 极简
2.4 MTK 扫描容错:三级恢复机制
一句话总结:MTK 的扫描容错是嵌入在扫描 FSM 中的双链恢复设计——超时链从轻到重(L1 dump → L2 SER),零信道链直接走最重的 L3 芯片复位。正常扫描后计数器清零,只有连续异常才会触发。
MTK 的扫描容错实际上是两条独立的恢复链,不是单一的三级升级链:
恢复链 A:超时恢复(scnDoScanTimeoutRecoveryCheck())
当扫描超时(固件没有在预期时间内返回结果)时,计数器 ucScnTimeoutTimes 累加:
| L1: FW Dump | ucScnTimeoutTimes == 2 | HIF_TRIGGER_FW_DUMP | |
| L2: L1 SER | ucScnTimeoutTimes >= ucScanNoApRecoverThucScnTimeoutSubsysResetCnt < 1(只执行一次)且eConnectionState == MEDIA_STATE_DISCONNECTED(仅无连接时)。注意:L1 和 L2 是顺序触发的两个独立 if 判断,不是同一个条件的 AND——L1 在 count==2 时触发 FW Dump,L2 在 count>=3 时才检查连接状态 | HIF_DRV_SER |
恢复链 B:零信道恢复(scnDoZeroChRecoveryCheck())
当扫描完成但结果为空(固件报告 0 个信道被扫描)时,计数器 ucScnZeroChannelCnt 累加:
| L3: Chip Reset | ucScnZeroChannelCnt > 3ucScnZeroChSubsysResetCnt < 1(只执行一次) | RST_FLAG_CHIP_RESET | 连接断开 |
为什么分两条链? 超时(固件卡死)和零信道(射频硬件异常)是两种完全不同的故障模式——超时可能是固件调度问题(L1 dump + L2 SER 通常能恢复),零信道则指向硬件层面的问题(只能芯片复位)。两条链独立计数,正常扫描后各自清零。
对比 QCOM 的 SSR(SubSystem Restart——基于阈值触发整个 WiFi 子系统的崩溃恢复),MTK 的分级设计更精细——在 L1/L2 阶段就能解决问题,不需要走到 L3。
2.5 MTK 扫描取消
MTK 的扫描取消通过 scnFsmMsgAbort() 实现——它在 SCN FSM 内部处理取消请求:
取消请求来源:├── AIS FSM: 连接请求到来时需要取消正在进行的扫描├── cfg80211: 用户/框架层主动取消(如切换网络)└── 超时: SCN FSM 内部扫描超时scnFsmMsgAbort() [scan_fsm.c]→ 向固件发送 CMD_ID_SCAN_CANCEL // 通知固件停止当前扫描→ 不等待固件回复(fire-and-forget)→ scnFsmGenerateScanDoneMsg(CANCELLED) // 立即生成内部完成消息→ scanReportBss2Cfg80211() // 上报已收集的部分结果→ cfg80211_scan_done(aborted) // 通知内核扫描被取消
与 QCOM 的对比:QCOM 提供异步取消(wlan_abort_scan())和同步取消(ucfg_scan_cancel_sync())两种模式,MTK 只有一种取消路径——通过邮箱消息触发,固件确认后回调。QCOM 的序列化框架还会在超时后自动取消(ACTIVE_CMD_TIMEOUT),MTK 的超时恢复则走 §2.4 的分级容错链。
三、PNO — 息屏时的低功耗值守
一句话总结:PNO(Preferred Network Offload)是息屏时发现已保存网络的关键机制——扫描由固件代劳,CPU 休眠,只在发现匹配网络时才唤醒。Android Framework 的 PNO 直接走 wificond(IWifiScannerImpl.startPnoScan → NL80211_CMD_START_SCHED_SCAN),不经过 wpa_supplicant。 supplicant 的
wpas_start_pno()是另一条独立路径——用于 supplicant 自己内部的网络发现(如 bgscan 场景的 scheduled scan)。
回到雷达比喻——PNO 是雷达的低功耗值守模式。不需要一直开满功率全频段扫描,只在预设的信道上、按预设间隔、周期性侦听已知目标信号。就像哨兵每隔几分钟用望远镜扫一眼已知的敌方据点——不费电,也不错过目标。
3.1 Framework 层 PNO:走 wificond,不经 supplicant
在 05(上)中我们讲到 WifiScanningServiceImpl 有三个状态机。PNO 由 WifiPnoScanStateMachine 管理:
WifiPnoScanStateMachine:mDefaultState└── mStartedState(父状态,处理 PNO 启停)├── mHwPnoScanState(硬件 PNO——固件执行)│ └── mSingleScanState(PNO 匹配后补做单次扫描获取完整 IE)└── mSwPnoScanState(软件 PNO——定时器驱动周期扫描 + 退避算法)
决策树(StartedState.processMessage(CMD_START_PNO_SCAN)):
硬件支持 PNO?├── 是 → mHwPnoScanState(固件执行,功耗最低)└── 否 → 软件 PNO 开关打开?├── 是 → mSwPnoScanState(AlarmManager 定时器 + 扫描 + 退避)└── 否 → 返回 "not supported"
HwPno 匹配后的关键行为:PNO 结果只包含 BSSID 和 SSID——没有完整的 IE(如 RSN、HT/VHT/HE 能力)。因此收到 CMD_PNO_NETWORK_FOUND 后,会补做一次单次全频段扫描来获取完整 IE——这就是 mSingleScanState 的作用。
3.2 Supplicant 内部的 PNO(独立路径,非 Framework 路径)
wpas_start_pno() 是 supplicant 内部的 PNO 实现——用于 supplicant 触发的网络发现(如 bgscan 场景),与 Framework 的 wificond PNO 互不相关。但底层 scheduled scan 机制相同:
// external_wpa_supplicant_8/wpa_supplicant/scan.cint wpas_start_pno(struct wpa_supplicant *wpa_s){struct wpa_driver_scan_params params;struct sched_scan_plan scan_plan;if (!wpa_s->sched_scan_supported)return -1; // 驱动不支持 scheduled scanif (wpa_s->pno || wpa_s->pno_sched_pending)return 0; // 已经在 PNO 或等待中if ((wpa_s->wpa_state > WPA_SCANNING) &&(wpa_s->wpa_state < WPA_COMPLETED))return -EAGAIN; // 正在关联中,推迟// 以下为教学简化版——合并了源码中两个独立循环(计数 + 填充)的遍历逻辑。// 源码第一个循环用 ssid->next(全局扁平链表)遍历计数(scan.c:3462),// 第二个循环用 pssid[]+pnext(按优先级链表)遍历填充(scan.c:3498)——// next 和 pnext 是不同的链表指针:next 是全局扁平链表,pnext 是同优先级内的链表。// 这里展示的是第二个循环的核心模式num_ssid = num_match_ssid = 0;prio = 0;ssid = wpa_s->conf->pssid[prio];while (ssid) {if (!wpas_network_disabled(wpa_s, ssid)) {num_match_ssid++;if (ssid->scan_ssid)num_ssid++;}if (ssid->pnext)ssid = ssid->pnext; // 同优先级的下一个网络else if (prio + 1 == wpa_s->conf->num_prio)break; // 最后一个优先级,遍历结束elsessid = wpa_s->conf->pssid[++prio]; // 切到下一个优先级的头指针}// ...填充 params.ssids[] 和 params.filter_ssids[]...// 设置扫描计划if (wpa_s->sched_scan_plans_num) {params.sched_scan_plans = wpa_s->sched_scan_plans;} else {scan_plan.interval = wpa_s->conf->sched_scan_interval? wpa_s->conf->sched_scan_interval : 10;scan_plan.iterations = 0; // 0 = 无限循环params.sched_scan_plans = &scan_plan;params.sched_scan_plans_num = 1;}// MAC 随机化(PNO专用,与扫描的随机化策略分离)if ((wpa_s->mac_addr_rand_enable & MAC_ADDR_RAND_PNO) &&wpa_s->wpa_state <= WPA_SCANNING)wpa_setup_mac_addr_rand_params(¶ms, wpa_s->mac_addr_pno);ret = wpa_supplicant_start_sched_scan(wpa_s, ¶ms);if (ret == 0)wpa_s->pno = 1;}
主要功能:
•filter_ssids vs ssids:filter_ssids 包含所有已保存 SSID(用于匹配),ssids 只包含需要主动扫描的 SSID(隐藏网络)•scan_plan 支持多段计划——如前 3 次用 20s 间隔,之后用 60s•iterations = 0 表示无限循环——PNO 会一直执行,直到被停止或用户打开屏幕•PNO 的 MAC 随机化是独立配置的(MAC_ADDR_RAND_PNO vs MAC_ADDR_RAND_SCAN)•SSID 数量有两道裁剪:max_sched_scan_ssids = min(wpa_s->max_sched_scan_ssids, WPAS_MAX_SCAN_SSIDS)(驱动能力上限,典型值 16)和 num_match_ssid > wpa_s->max_match_sets(supplicant match set 上限)。超出部分被静默裁剪——这就是为什么排查表中会有「SSID 数量超出固件上限」的问题
3.3 MTK 驱动的 PNO:NLO 状态机
MTK gen4m 驱动的 PNO 实现基于 NLO(Network List Offload) 状态机——固件内部维护了三个专用状态,独立于普通扫描的 IDLE/SCANNING 状态:
// include/mgmt/scan.h — MTK 扫描 FSM 完整状态枚举enum ENUM_FW_SCAN_STATE {FW_SCAN_STATE_IDLE = 0,FW_SCAN_STATE_SCAN_START, // 普通扫描// ... 普通扫描状态省略 ...FW_SCAN_STATE_SCAN_DONE, // 普通扫描完成FW_SCAN_STATE_NLO_START, // ⑧ NLO 开始——固件启动周期性侦听FW_SCAN_STATE_NLO_HIT_CHECK, // ⑨ NLO 匹配检查——发现匹配网络时触发FW_SCAN_STATE_NLO_STOP, // ⑩ NLO 停止——用户亮屏或主动取消FW_SCAN_STATE_BATCH_START, // 批量扫描(另有用途)// ...};
NLO 的三状态生命周期:
固件收到 NLO 命令:→ FW_SCAN_STATE_NLO_START→ 固件按预设间隔在指定信道上周期性侦听→ 收到 Beacon/Probe Response → FW_SCAN_STATE_NLO_HIT_CHECK→ 匹配 SSID → 上报匹配事件到驱动 → 继续侦听→ 用户亮屏 / 驱动取消 → FW_SCAN_STATE_NLO_STOP
驱动层入口:MTK 的 cfg80211 回调注册了 sched_scan_start 和 sched_scan_stop:
// os/linux/gl_cfg80211.cint mtk_cfg80211_sched_scan_start(struct wiphy *wiphy,struct net_device *ndev,struct cfg80211_sched_scan_request *request){// 提取 SSID 列表、匹配集、信道、间隔等参数// 构建 PARAM_SCHED_SCAN_REQUEST → 通过 OID 下发到固件// 固件进入 FW_SCAN_STATE_NLO_START}
关键行为:
•LowLatency 拒绝:已连接状态下如果启用了低延迟模式,MTK 会拒绝 PNO 请求(return -EBUSY)——保证前台连接质量优先•单实例限制:prGlueInfo->prSchedScanRequest != NULL 时拒绝新请求——MTK 同一时间只允许一个 PNO 实例•固件侧匹配:匹配逻辑在固件内部完成,驱动只负责下发 SSID 列表和接收匹配通知
与 QCOM PNO 的对比:
scm_handle_pno_hit() | ||
3.4 指数退避策略
Android 12+ 引入基于移动状态的 PNO 间隔调整:
DEVICE_MOBILITY_STATE_STATIONARY | ||
DEVICE_MOBILITY_STATE_LOW_MVMT | ||
DEVICE_MOBILITY_STATE_HIGH_MVMT | ||
DEVICE_MOBILITY_STATE_UNKNOWN |
设计原理:静止设备周围的热点环境不会变化——180 秒扫一次也不会错过新 AP。但移动设备(走路/开车)可能在 1 分钟内穿越多个 AP 覆盖区——需要更频繁地扫描。
这个行为在不同的 Android 版本中通过配置 overlay 控制:
config_wifiStationaryPnoScanIntervalMillis | ||
config_wifiMovingPnoScanIntervalMillis | ||
3.5 PNO 全链路总结

息屏 + 断连 + 有已保存网络→ WifiConnectivityManager.handleScreenStateChanged()→ startDisconnectedPnoScan()→ retrievePnoNetworkList() // 获取已保存网络列表→ WifiPnoScanStateMachine (CMD_START_PNO_SCAN)→ HwPnoScanState: ScannerImplsTracker.setHwPnoList()→ WificondScannerImpl.startPnoScan()→ AIDL: IWifiScannerImpl.startPnoScan()→ wificond ScannerImpl::startPnoScan()→ ScanUtils::StartScheduledScan()→ NL80211_CMD_START_SCHED_SCAN // 下发到内核→ 驱动 → 固件固件发现匹配网络:→ wificond: IPnoScanEvent.OnPnoNetworkFound()→ WifiPnoScanStateMachine (CMD_PNO_NETWORK_FOUND)→ mSingleScanState: addSingleScanRequest() // 补做全频段扫描→ onResults() → 通知 Framework
四、WiFi 6E/7 扫描新机制
一句话总结:6GHz 新增 59 个 20MHz 信道,传统的逐个信道主动扫描将耗时 ~3 秒。802.11ax 引入了 PSC(减少 75% 信道)、RNR(跨频段发现)、Multi-BSSID(一个 Beacon 多 SSID)、FILS 发现帧(20ms 被动发现)四种机制来应对这个挑战。
4.1 问题:59 个信道怎么高效扫描?
回到比喻——WiFi 6E 相当于雷达新增了一个大频段。传统逐个信道扫描就像地毯式轰炸——太慢。新的搜索策略包括:只搜哨站信道(PSC)、利用友军情报(RNR)、一帧多信息(Multi-BSSID)、快速应答信号(FILS)。
| ~3 秒 | |||
| ~0.75 秒 | |||
| ~1.2 秒 |
另外,RNR(Reduced Neighbor Report)不是一种扫描方式——它是 AP 在 2.4/5GHz Beacon 中附带的 6GHz 邻居信息。STA 仍在 2.4/5GHz 上扫描,结果中顺带获得了 6GHz AP 的信息——不需要在 6GHz 上做任何主动扫描。
4.2 PSC(Preferred Scanning Channels)
PSC 的核心思想是:只在每 4 个 20MHz 信道中选 1 个作为「哨站信道」,STA 只需在这些哨站上发送 Probe Request:
6GHz 信道: 1 5 9 13 17 21 25 29 33 37 41 45 49 53 57 ...^ ^ ^ ^ ^PSC PSC PSC PSC PSCPSC 信道列表(以 FCC/CE 为例,因 regulatory domain 变化): 5, 21, 37, 53, 69, 85, 101, 117,133, 149, 165, 181, 197, 213, 229
严格限制:在非 PSC 信道上,STA 不能发送广播 Probe Request(wildcard SSID + wildcard BSSID)。只有收听到该信道上 AP 的 Beacon 或 FILS 发现帧后,才能发送单播 Probe Request。这防止了 6GHz 频段的「probe storm」。
源码级:内核如何实现 PSC 过滤? 当 Framework 设置 NL80211_SCAN_FLAG_COLOCATED_6GHZ 标志时,内核 cfg80211_scan_6ghz() 函数(net/wireless/scan.c)执行以下逻辑(注:此为内核代码,来自 AOSP kernel common 分支,非本仓库可验证的驱动源码):
// net/wireless/scan.c — cfg80211_scan_6ghz() 核心逻辑(简化)// ① 遍历已知 BSS,从 RNR IE 中提取同位置 6GHz AP 列表if (rdev_req->flags & NL80211_SCAN_FLAG_COLOCATED_6GHZ) {list_for_each_entry(intbss, &rdev->bss_list, list) {cfg80211_parse_colocated_ap(ies, &coloc_ap_list); // 解析 RNR}}// ② 如果定向扫描(1 个 SSID)且已知同 ESS 的同位置 AP → 跳过 PSCif (count && request->n_ssids == 1 && request->ssids[0].ssid_len) {list_for_each_entry(ap, &coloc_ap_list, list) {if (ap->colocated_ess && cfg80211_find_ssid_match(ap, request)) {need_scan_psc = false; // 已知目标在同位置,不需要扫 PSCbreak;}}}// ③ 只添加 PSC 信道(或未设 COLOCATED_6GHZ 时添加全部 6GHz 信道)for (i = 0; i < rdev_req->n_channels; i++) {if (rdev_req->channels[i]->band == NL80211_BAND_6GHZ &&((need_scan_psc && cfg80211_channel_is_psc(rdev_req->channels[i])) ||!(rdev_req->flags & NL80211_SCAN_FLAG_COLOCATED_6GHZ))) {cfg80211_scan_req_add_chan(request, rdev_req->channels[i], false);}}
QCOM 驱动侧在 WMI 扫描命令中通过 chan_p->psc_channel = 1 标记 PSC 信道(wma_scan_roam.c),固件据此决定在哪些 6GHz 信道上发送 Probe Request。
4.3 RNR(Reduced Neighbor Report)
RNR 是跨频段发现的核心——AP 在 2.4/5GHz 的 Beacon 和 Probe Response 中附带其 6GHz 同位置 AP 的信息:
2.4GHz Beacon:└── Reduced Neighbor Report element:├── Operating Class + Channel Number(6GHz 信道)├── TBTT Information(目标 Beacon 发送时间)├── BSSID(6GHz AP 的 MAC 地址)├── Short SSID(SSID 的 4 字节哈希)└── BSS Parameters(是否同位置、是否支持 Multiple BSSID 等)
关键价值:STA 在 2.4/5GHz 上扫描时,自动获取 6GHz AP 的信息——不需要在 6GHz 上做任何扫描就发现了 6GHz 网络。这就是 enable_6ghz_rnr 标志的作用(对应 NL80211_SCAN_FLAG_COLOCATED_6GHZ,在 05(上)中我们见过)。
源码级:RNR 信息如何驱动扫描决策? 内核 cfg80211_parse_colocated_ap() 从已知 BSS 的 Beacon/Probe Response 中解析 RNR IE,构建 coloc_ap_list。这个列表有两个用途:
1.决定扫哪些 6GHz 信道:只扫列表中 AP 所在的信道 + PSC 信道(如上面 PSC 的源码所示)2.避免重复扫描:如果已知目标 AP 在同位置(colocated_ess = true),连 PSC 都可以跳过
这意味着 RNR 不仅是"发现"机制——它还是一种扫描优化机制,通过已知信息减少需要扫描的 6GHz 信道数量。
4.4 Multi-BSSID
Multi-BSSID 允许一个 AP 在单个 Beacon 中携带多个虚拟 AP(SSID)的信息:
单个 Beacon 帧:└── transmitted BSSID(主 AP)└── Multiple BSSID element:├── non-transmitted BSSID profile 1(SSID "Guest")├── non-transmitted BSSID profile 2(SSID "IoT")└── non-transmitted BSSID profile 3(SSID "Enterprise")
对扫描的影响:STA 收到一个 Beacon 就知道这个 AP 上有 4 个 SSID——不需要收到 4 个独立的 Beacon。这在大密度部署中节省了大量空口时间。
4.5 FILS 发现帧(802.11ai)
FILS(Fast Initial Link Setup)定义了一种轻量 Beacon——每 20ms 发送一次(比 Beacon 的 ~100ms 快 5 倍):
对于 59 个 6GHz 信道的被动扫描:传统方式 ~6 秒,FILS 方式 ~1.2 秒。
五、调试与日志
5.1 代码锚点与日志关键字速查
使用说明:下面两张表用途不同——代码锚点是函数名/常量名,用于在源码中定位(grep 源码),它们在 dmesg/logcat 中不会以这个形式出现;日志关键字是
scm_err()、hdd_err()等宏的实际输出字符串片段,用于在运行时日志中搜索(grep dmesg/logcat)。
表 A:代码锚点(grep 源码时用)
| Framework | WifiScanningServiceImpl | ||
| Framework | ScanRequestProxy | ||
| Supplicant | wpa_supplicant_scan() | ||
| Supplicant | wpas_trigger_scan_cb() | ||
| Supplicant | wpa_bss_update_scan_res() | ||
| QCOM | wlan_hdd_cfg80211_scan() | ||
| QCOM | scm_scan_start_req() | ||
| QCOM | wlan_serialization_request() | ||
| QCOM | WMI_START_SCAN_CMDID | ||
| QCOM | WMI_SCAN_EVENTID | ||
| MTK | scnFsmSteps() | ||
| MTK | scnEventScanDone() | ||
| MTK | scnDoScanTimeoutRecoveryCheck() | ||
| MTK | HIF_DRV_SER | ||
| MTK | RST_FLAG_CHIP_RESET |
表 B:日志关键字(grep dmesg/logcat 时用)
| Framework | Log.d(TAG, ...) | WifiScanningServiceImpl | |
| Framework | Log.i(TAG, ...) | throttled | |
| Supplicant | wpa_dbg() | nl80211: scan request | |
| Supplicant | wpa_msg() | CTRL-EVENT-SCAN-RESULTS | |
| Supplicant | wpa_dbg() | Reject scan trigger since one is already pending | |
| Supplicant | wpa_msg() | CTRL-EVENT-SCAN-FAILED ret= | |
| Supplicant | wpa_dbg() | BSS: Add new idBSS: Remove | |
| QCOM | hdd_err()hdd_info() | wlan_hdd_cfg80211_scan | |
| QCOM | scm_err()scm_info() / scm_debug() | scm_scan_start_req | |
| QCOM | scm_err()scm_info() | wlan_serialization_request | |
| MTK | log_dbg(SCN, STATE, ...) | [SCAN]TRANSITION: | |
| MTK | log_dbg(SCN, EVENT, ...) | scnEventScanDone | |
| MTK | log_err(SCN, ...) | scnDoScanTimeoutRecoveryCheck | |
| 内核 | nl80211 | NL80211_CMD_TRIGGER_SCAN | |
| 内核 | nl80211 | NL80211_CMD_NEW_SCAN_RESULTS |
5.2 常见问题排查
| 扫描不返回结果 | ChannelHelper 日志——DFS 信道静默过滤 | |
| 扫描太慢 | ScanRequestProxy 日志——是否触发节流 | |
| PNO 不工作 | wpa_s->sched_scan_supported | |
num_match_ssid == 0——没有已保存网络 | ||
max_sched_scan_ssids——SSID 数量超出固件上限 | ||
| PNO 耗电高 | scan_plan——是否正确配置了 interval/iterations | |
mSingleScanState) |
六、总结
6.1 扫描三部曲覆盖的完整调用链
App.startScan()→ WifiServiceImpl→ ScanRequestProxy.startScan() [上篇: 节流检查]→ WifiScanningServiceImpl [上篇: 状态机]→ WifiNl80211Manager.startScan2() [上篇: AIDL]→ wificond ScannerImpl::scanRequest() [上篇: 参数转换]→ ScanUtils::Scan() [上篇: NL80211_CMD_TRIGGER_SCAN]→ 内核 cfg80211→ wpa_supplicant_scan() [中篇: Supplicant 参数构建]→ radio_add_work("scan") [中篇: radio work 排队]→ wpas_trigger_scan_cb() [中篇: 实际下发 + MAC 随机化]→ driver_nl80211_scan() [中篇: nl80211 构建]→ QCOM: scm_scan_start_req() [下篇: SCM 调度]→ wlan_serialization [下篇: 序列化队列]→ WMI_START_SCAN_CMDID [下篇: WMI → 固件]→ MTK: scnFsmSteps() [下篇: Scan FSM]→ CMD_SCAN_REQ_V2 [下篇: 固件命令]→ 固件执行 Probe Request→ Probe Response / Beacon 回来→ QCOM: WMI_SCAN_EVENTID → cfg80211_scan_done→ MTK: scnEventScanDone → cfg80211_scan_done→ 内核 NL80211_CMD_NEW_SCAN_RESULTS→ Supplicant EVENT_SCAN_RESULTS [中篇]→ wpa_bss_update_scan_res() [中篇: BSS 更新]→ wpas_select_network() [中篇: 网络选择 → 06 章]→ wificond OnScanResultReady() [上篇: 回调]→ Framework onResults() [上篇: 分发]
6.2 QCOM vs MTK:扫描结果上报的差异
§1.6 讲了 QCOM 的双管道上报,§2.1 讲了 MTK 的 7 层调用链。这里做一个收口对比——两家的结果上报机制差异显著:
| 逐 BSS 上报时机 | WMI_EVENT_BCN_PROB_RESP) | scnEventScanDone() → scanReportBss2Cfg80211()) |
| 完成信号 | WMI_SCAN_EVENTIDcfg80211_scan_done() | scnEventScanDone()cfg80211_scan_done() |
| 上报函数 | cfg80211_inform_bss_frame_data() | cfg80211_inform_bss_frame() |
| 用户体验差异 |
¹ 两个内核 API 功能相同,区别在于参数传递方式——cfg80211_inform_bss_frame_data() 接受预解析的 struct cfg80211_inform_bss 元数据(较新的 API),cfg80211_inform_bss_frame() 接受原始帧数据由内核解析(较旧的 API)。这不是 QCOM/MTK 的设计选择差异,更多是内核 API 版本适配的差异。
QCOM 双管道的设计意图:如果只有完成事件(管道 B),用户在 Settings 中点击刷新后要等 2-3 秒才能看到任何结果。管道 A 让内核的 BSS 数据库在扫描过程中就逐步被填充——Framework 甚至可以在扫描还在进行时就拿到部分结果。
6.3 完整的扫描请求 → 结果返回全链路
把三篇的内容串起来,一次完整的扫描从 App 到固件再回到 UI 的全链路:

【扫描请求下发】App.startScan() ← 用户操作 / Settings 10s 定时→ WifiServiceImpl ← 权限检查→ ScanRequestProxy.startScan() ← [上篇] 节流:FG 4次/120s,BG 1次/30min→ WifiScanningServiceImpl ← [上篇] 状态机 Idle→Scanning→ WifiNl80211Manager.startScan2() ← [上篇] AIDL 跨进程→ wificond ScannerImpl ← [上篇] 参数翻译 + MAC 随机化→ NL80211_CMD_TRIGGER_SCAN ← [上篇] netlink 到内核【驱动执行(QCOM 路径)】内核 cfg80211→ wlan_hdd_cfg80211_scan() ← [下篇] HDD 入口→ scm_scan_start_req() ← [下篇] SCM 调度→ wlan_serialization_request() ← [下篇] 序列化队列→ WMI_START_SCAN_CMDID ← [下篇] 下发固件【驱动执行(MTK 路径)】内核 cfg80211→ mtk_cfg80211_scan() ← [下篇] cfg80211 回调→ aisFsmScanRequestAdv() ← [下篇] AIS FSM→ scnFsmSteps() → scnSendScanReqV2() ← [下篇] SCN FSM → 固件【固件执行】固件逐信道 dwell + Probe Request/Response→ 26 信道 dwell + BSS 收集 → ~2.9s(详见 §1.5)【扫描结果返回】QCOM: WMI_SCAN_EVENTID / beacon/probe 帧→ cfg80211_scan_done() ← 内核 APIMTK: scnEventScanDone()→ scanReportBss2Cfg80211() ← 逐 BSS 上报→ cfg80211_scan_done() ← 内核 API→ NL80211_CMD_NEW_SCAN_RESULTS ← netlink multicastSupplicant: EVENT_SCAN_RESULTS ← [中篇]→ wpa_bss_update_scan_res() ← BSS 缓存更新→ wpas_select_network() ← 网络选择 → 06 章wificond: OnScanResultReady() ← [上篇] AIDL 回调→ Framework onResults() ← 分发给 Settings / WifiConnectivityManager / App
6.4 各层关键设计决策总结
References
[1] MTK gen4m: https://android.googlesource.com/kernel/common/+/refs/heads/android-mainline/[2] external/wpa_supplicant_8: https://android.googlesource.com/platform/external/wpa_supplicant_8/
夜雨聆风