杰理关键词唤醒(jl_kws)
jl_kws 是杰理 AC63 系列蓝牙 SoC SDK 中的关键词唤醒/语音识别公共组件,负责从麦克风采集音频、送入唤醒算法检测,并在命中关键词或触发事件时驱动上层业务动作。该组件以独立内核任务(kws)运行,通过消息队列与信号量实现生命周期管理,并接入系统低功耗(LP)框架。
Purpose and Scope
本页面向工程师完整说明 apps/common/jl_kws/ 目录下关键词唤醒组件的:
- 模块分层与文件职责(
jl_kws_api.h、jl_kws_main.c、jl_kws_common.h及 algo/audio/event 三个子模块的接口契约); - 状态机、任务生命周期与消息驱动的运行机制;
- 从 MIC 采集到算法检测再到事件上报的完整数据流;
- 对外 API、错误码、配置开关与低功耗接入方式。
以下内容不属于本页范围,请参见对应目录的独立文档:
- 具体唤醒词模型的训练与烧录(算法库资源、
cpu/*/tools/jl_kws.cfg配置); - 蓝牙协议栈、音频通路(A2DP/HFP)等其他公共组件;
- 各 CPU 平台(br23/br25/br30)的底层驱动差异。
Overview
关键词唤醒(Keyword Spotting, KWS)是蓝牙耳机/音箱等产品的核心交互入口:设备处于待机或低功耗状态时,持续监听麦克风,检测到特定唤醒词(如“小杰小杰”)后唤醒系统并进入交互流程。
jl_kws 组件把这一能力封装为四个步骤的 API:打开(open)→ 启动识别(start)→ 停止识别(stop)→ 关闭(close)。其内部架构刻意拆分为三个可独立初始化的子模块:
| 子模块 | 职责 | 接口文件声明 |
|---|---|---|
jl_kws_algo | 唤醒算法:帧缓冲管理、检测执行 | jl_kws_common.h L46-L51 |
jl_kws_audio | 音频采集:MIC 数据获取、启动/停止 | jl_kws_common.h L56-L60 |
jl_kws_event | 识别事件上报与状态更新 | jl_kws_common.h L65-L68 |
三者由 jl_kws_main.c 中的 kws 任务统一编排,保证初始化顺序(算法 → 音频 → 事件)与停止顺序(算法 → 音频 → 事件)的一致性,避免出现“算法还在跑但 MIC 已关闭”的竞态。
设计意图上,模块级联采用任务消息 + 状态标志双保险:外部 API 只写 kws_state 并投递消息,真正的资源操作全部在 kws 任务上下文内串行执行,因此上层任意线程调用 API 都是线程安全的。
Architecture
flowchart TD
subgraph sg_User["用户层"]
App["应用程序 / 业务代码"]
Demo["jl_kws_main_user_demo"]
end
subgraph sg_API["API 层 (jl_kws_api.h)"]
Open["jl_kws_speech_recognition_open"]
Start["jl_kws_speech_recognition_start"]
Stop["jl_kws_speech_recognition_stop"]
Close["jl_kws_speech_recognition_close"]
AecOut["kws_aec_data_output"]
end
subgraph sg_Main["主控层 (jl_kws_main.c)"]
Task["kws 任务<br/>kws_speech_recognition_task"]
Run["kws_speech_recognition_run"]
Init["jl_kws_speech_recognition_init"]
LP["kws_lp_target<br/>REGISTER_LP_TARGET"]
end
subgraph sg_Sub["功能子模块 (jl_kws_common.h 接口)"]
Algo["jl_kws_algo<br/>算法检测"]
Audio["jl_kws_audio<br/>MIC 采集"]
Event["jl_kws_event<br/>事件上报"]
end
subgraph sg_HW["硬件/系统层"]
MIC["麦克风 (MIC)"]
AEC["AEC 回声消除"]
Sys["os_taskq / 信号量"]
end
App --> Open
App --> Start
App --> Stop
App --> Close
Demo --> Open
Demo --> Start
Demo --> Stop
Demo --> Close
Open -->|"task_create + ready_sem"| Task
Start -->|"post_msg(RUN)"| Task
Stop -->|"post_msg(STOP)"| Task
Close -->|"post_msg(CLOSE) + del_sem"| Task
Task --> Init
Task --> Run
Run --> Audio
Run --> Algo
Run --> Event
Audio --> MIC
Audio --> AEC
AecOut -->|"外部音频输入"| Audio
Algo -->|"KWS_VOICE_EVENT"| Event
LP -->|"is_idle 查询"| Task
Task --> Sys
架构说明:
- 用户层只接触
jl_kws_api.h暴露的 4 个生命周期函数;jl_kws_main_user_demo()是官方给出的调用示例(jl_kws_api.h L9)。 - 主控层是唯一的“事实来源”:
kws_speech_recognition_task通过os_taskq_pend消费消息,按消息类型调用 run/stop/close,所有子模块操作都发生在此任务内,天然串行化。 - 子模块层彼此解耦:
jl_kws_algo只关心“给我一帧数据,我告诉你有没有命中”;jl_kws_audio只关心“从 MIC 拿一帧数据”;jl_kws_event只关心“事件来了,更新状态并通知上层”。三者仅通过jl_kws_common.h的函数声明耦合,便于在不通算法/不通硬件上替换实现。 - 系统层提供任务队列、信号量(
ready_sem/del_sem)与低功耗注册表(REGISTER_LP_TARGET),kws任务因此可被系统在空闲时挂起。
主内容:状态机与运行机制
状态定义
jl_kws_main.c 中定义了两组状态枚举,分别描述对外状态与任务内部状态:
| 枚举 | 取值 | 含义 |
|---|---|---|
KWS_STATE_IDLE | 0 | 未初始化 |
KWS_STATE_INIT | 1 | 任务已创建、子模块初始化完成 |
KWS_STATE_RUN | 2 | 识别进行中 |
KWS_STATE_STOP | 3 | 已停止 |
KWS_STATE_CLOSE | 4 | 已关闭 |
KWS_TASK_STATE_* 与之一一对应,用于低功耗空闲查询(见下文)。状态切换由外部 API 写入,任务侧只负责执行对应动作,两者通过 struct kws_speech_recognition 共享(jl_kws_main.c L9-L15):
struct kws_speech_recognition {
u8 task_init;
u8 kws_state;
u8 kws_task_state;
};
static struct kws_speech_recognition jl_kws = {0};
任务消息与状态迁移
kws 任务通过 os_taskq_pend 阻塞等待消息,消息类型定义如下:
enum KWS_TASK_MSG {
KWS_SPEECH_RECOGNITION_RUN = 1,
KWS_SPEECH_RECOGNITION_STOP,
KWS_SPEECH_RECOGNITION_CLOSE,
};
stateDiagram-v2
[*] --> IDLE
IDLE --> INIT: open() 创建任务并初始化
INIT --> RUN: start() 投递 RUN 消息
RUN --> STOP: stop() 投递 STOP 消息
STOP --> RUN: start() 再次投递 RUN 消息
RUN --> CLOSE: close() 投递 CLOSE 消息
STOP --> CLOSE: close() 投递 CLOSE 消息
CLOSE --> [*]: task_kill + task_init=0
状态迁移要点:
- 幂等保护:
start()在kws_state == KWS_STATE_RUN时直接返回 0;stop()/close()在对应状态已设置时直接返回,避免重复投递消息(jl_kws_main.c L235-L278)。 - RUN → STOP → RUN 是合法路径:
kws_speech_recognition_run的 while 循环以kws_state != KWS_STATE_RUN为退出条件,停止后再次 start 会重新进入循环。 - CLOSE 是终态:任务收到 CLOSE 消息后执行资源释放,通过
del_sem通知外部调用者,随后外部调用task_kill销毁任务,task_init清零(jl_kws_main.c L263-L278)。
任务创建与初始化
jl_kws_speech_recognition_open() 是模块入口,负责创建任务并等待其就绪:
int jl_kws_speech_recognition_open(void)
{
kws_info("%s", __func__);
OS_SEM ready_sem;
if (__this->task_init == 0) {
__this->task_init = 1;
os_sem_create(&ready_sem, 0);
task_create(kws_speech_recognition_task, (void *)&ready_sem, THIS_TASK_NAME);
//wait task ready
os_sem_pend(&ready_sem, 20);
}
return 0;
}
设计意图:ready_sem 保证 open() 返回时任务已经启动,任务侧在进入消息循环前 os_sem_post 释放该信号量;外部以 20 tick 超时等待,防止任务创建失败导致死等。任务初始化按“算法 → 音频 → 事件”的顺序执行,任一步失败都会走 kws_speech_recognition_close() 清理并 task_kill 自杀(jl_kws_main.c L99-L122):
static int jl_kws_speech_recognition_init(void)
{
kws_info("%s", __func__);
//1.算法初始化
int ret = 0;
ret = jl_kws_algo_init();
if (ret != JL_KWS_ERR_NONE) {
return ret;
}
//2.Audio MIC初始化
ret = jl_kws_audio_init();
if (ret != JL_KWS_ERR_NONE) {
return ret;
}
//3. event初始化
ret = jl_kws_event_init();
if (ret != JL_KWS_ERR_NONE) {
return ret;
}
return JL_KWS_ERR_NONE;
}
识别主循环
kws_speech_recognition_run() 是核心数据通路:先启动音频与算法,然后循环“取帧缓冲 → 填音频数据 → 跑算法检测 → 事件更新”:
static int kws_speech_recognition_run(void)
{
void *rbuf = 0;
u32 rbuf_len = 0;
u32 audio_data_len = 0;
int ret = JL_KWS_ERR_NONE;
int event = KWS_VOICE_EVENT_NONE;
ret = jl_kws_audio_start();
if (ret != JL_KWS_ERR_NONE) {
return ret;
}
ret = jl_kws_algo_start();
if (ret != JL_KWS_ERR_NONE) {
return ret;
}
__this->kws_task_state = KWS_TASK_STATE_RUN;
while (1) {
if (__this->kws_state != KWS_STATE_RUN) {
break;
}
rbuf = jl_kws_algo_get_frame_buf(&rbuf_len);
if (rbuf == NULL) {
ret = JL_KWS_ERR_ALGO_NO_FRAME_BUF;
break;
}
audio_data_len = jl_kws_audio_get_data(rbuf, rbuf_len);
if (audio_data_len == rbuf_len) {
/* kws_putchar('r'); */
event = jl_kws_algo_detect_run(rbuf, rbuf_len);
if (event != KWS_VOICE_EVENT_NONE) {
jl_kws_event_state_update(event);
}
}
}
return ret;
}
关键设计:
- 帧对齐校验:仅当
audio_data_len == rbuf_len时才执行检测,保证算法每次拿到完整的一帧,避免半帧数据造成误检。 - 忙等退出条件:循环体不阻塞,退出靠外部 API 改写
kws_state;因此 stop 消息必须由同任务在下一轮os_taskq_pend处理,而 run 循环在状态变化后自行退出,二者无死锁。 - 事件过滤:
KWS_VOICE_EVENT_NONE不进入事件模块,减少无效开销。
低功耗接入
kws 任务注册为系统低功耗目标,空闲时允许系统挂起:
static u8 kws_idle_query(void)
{
return !(__this->kws_task_state == KWS_TASK_STATE_RUN);
}
REGISTER_LP_TARGET(kws_lp_target) = {
.name = "kws",
.is_idle = kws_idle_query,
};
设计意图:低功耗框架在进入睡眠前遍历所有 LP 目标,kws_idle_query 返回真(即任务不在 RUN 状态)才允许挂起——识别期间必须保持 CPU 运行,否则会漏检唤醒词。同时,KWS_BT_CALL_SYS_FREQUENCE_HZ 定义为 48 MHz(jl_kws_main.c L19),提示调用方在识别期间需保证系统频率满足算法实时性要求。
编译开关
整个模块由 TCFG_KWS_VOICE_RECOGNITION_ENABLE 宏控制:未使能时 jl_kws_main.c 主体不参与编译(jl_kws_main.c L4)。该开关应在 app_config.h 中按产品需求配置。
Core Flow:端到端时序
以下时序图展示了从应用层调用到资源释放的完整交互:
sequenceDiagram
participant App as 应用层
participant API as jl_kws_api
participant Task as kws 任务
participant Audio as jl_kws_audio
participant Algo as jl_kws_algo
participant Event as jl_kws_event
App->>API: open()
API->>Task: task_create + ready_sem
Task-->>API: os_sem_post(ready_sem)
Task->>Task: init: algo_init → audio_init → event_init
App->>API: start()
API->>Task: post_msg(KWS_SPEECH_RECOGNITION_RUN)
Task->>Audio: audio_start()
Task->>Algo: algo_start()
loop 每帧检测
Task->>Algo: algo_get_frame_buf(&len)
Task->>Audio: audio_get_data(buf, len)
Task->>Algo: algo_detect_run(buf, len)
Algo-->>Task: voice_event (YES/NO)
Task->>Event: event_state_update(event)
end
App->>API: stop()
API->>Task: post_msg(KWS_SPEECH_RECOGNITION_STOP)
Task->>Algo: algo_stop()
Task->>Audio: audio_stop()
Task->>Event: event_stop()
App->>API: close()
API->>Task: post_msg(KWS_SPEECH_RECOGNITION_CLOSE, del_sem)
Task->>Algo: algo_close()
Task->>Audio: audio_close()
Task->>Event: event_close()
Task-->>API: os_sem_post(del_sem)
API->>Task: task_kill("kws")
时序要点:
- open 是同步就绪的:任务创建后通过
ready_sem握手,open()返回即代表kws任务已进入消息循环。 - start/stop/close 是异步的:API 只投递消息立即返回,实际资源操作在任务上下文执行;唯一例外是
close()会os_sem_pend(&del_sem, 0xffff)无限等待任务完成清理,确保关闭前所有子模块资源已释放(jl_kws_main.c L263-L278)。 - 检测循环是单线程自驱动:不存在中断回调抢占算法状态的问题;音频数据以轮询方式(
audio_get_data)拉取,与算法帧率严格对齐。
Usage Examples
基础用法:官方演示流程
jl_kws_main_user_demo() 展示了完整的生命周期调用方式——打开后延时启动识别,中途停止再重启,最后关闭:
void jl_kws_main_user_demo(void)
{
//1.打开jl_kws模块
jl_kws_speech_recognition_open();
//2.在某时刻开始识别
os_time_dly(1000);
jl_kws_speech_recognition_start();
//3.在某时刻之后停止识别
os_time_dly(1500);
jl_kws_speech_recognition_stop();
//4.在某时刻之后重新开启识别
os_time_dly(2000);
jl_kws_speech_recognition_start();
//5.在某时刻之后关闭jl_kws模块
os_time_dly(2500);
jl_kws_speech_recognition_close();
}
对外 API 一览
上层业务只需包含 jl_kws_api.h 并使用以下接口:
int jl_kws_speech_recognition_open(void);
int jl_kws_speech_recognition_start(void);
void jl_kws_speech_recognition_stop(void);
void jl_kws_speech_recognition_close(void);
void jl_kws_main_user_demo(void);
//for jl_kws audio extern input
u8 kws_aec_get_state(void);
void kws_aec_data_output(void *priv, s16 *data, int len);
其中 kws_aec_get_state() 与 kws_aec_data_output() 用于外部音频通路(如蓝牙通话 AEC)向 KWS 模块注入回声消除后的参考/输出数据,是扩展“非本地 MIC 输入”场景的预留接口。实际业务中,识别命中后应在事件回调(jl_kws_event 内部)里触发 UI 提示、退出低功耗或启动语音助手会话。
Configuration Options
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
TCFG_KWS_VOICE_RECOGNITION_ENABLE | 宏 | 未定义(需在 app_config.h 打开) | 总开关;未使能时整个 jl_kws_main.c 不编译(jl_kws_main.c L4) |
KWS_BT_CALL_SYS_FREQUENCE_HZ | 宏 | (48 * 1000000) | 识别期间建议的系统 CPU 频率,保证算法实时性(jl_kws_main.c L19) |
KWS_DEBUG_ENABLE | 宏 | 定义 | 使能 kws_debug/kws_putchar 调试输出(jl_kws_common.h L17-L24) |
THIS_TASK_NAME | 宏 | "kws" | kws 内核任务名,用于 os_taskq_post_msg 与 task_kill(jl_kws_main.c L49) |
jl_kws.cfg | 算法配置 | 各平台一份 | br23/br25/br30 平台工具目录下的算法配置,如 cpu/br23/tools/jl_kws.cfg |
说明:jl_kws_algo 的具体算法参数(模型、阈值、增益等)封装在算法库与 jl_kws.cfg 中,其内部解析细节未在本仓库源码中展开,建议按平台工具链的说明进行配置。jl_kws_common.h 顶部被注释的 #pragma bss_seg/.data_seg/.const_seg/.code_seg 表明该模块数据段可单独划分到指定内存区域(如低功耗常驻 RAM),实际链接时可按需放开。
API Reference
int jl_kws_speech_recognition_open(void)
创建并初始化 KWS 识别任务。
- 说明:首次调用时创建
kws任务并通过ready_sem等待其就绪;任务内依次执行算法/音频/事件子模块初始化。已打开时幂等返回。 - 返回:
0表示成功(当前实现恒返回 0;初始化失败由任务内部自杀清理)。 - 调用时机:系统上电初始化阶段,仅需一次。
int jl_kws_speech_recognition_start(void)
启动关键词识别。
- 说明:将状态置为
KWS_STATE_RUN并向任务投递KWS_SPEECH_RECOGNITION_RUN消息;任务随即启动音频与算法并进入逐帧检测循环。若已在 RUN 状态则直接返回。 - 返回:
0。 - 注意:调用前必须先
open()。
void jl_kws_speech_recognition_stop(void)
停止识别,释放音频与算法运行资源(不销毁任务)。
- 说明:状态置为
KWS_STATE_STOP并投递 STOP 消息;任务内依次执行algo_stop → audio_stop → event_stop,检测循环随后退出。 - 注意:停止后可通过
start()重新启动,无需再次open()。
void jl_kws_speech_recognition_close(void)
彻底关闭 KWS 模块并销毁任务。
- 说明:状态置为
KWS_STATE_CLOSE,投递携带del_sem的 CLOSE 消息,并阻塞等待任务完成全部资源释放(algo_close → audio_close → event_close),随后task_kill("kws")销毁任务、task_init清零。 - 注意:该函数会一直等待直到任务清理完毕,不要在中断上下文调用。
子模块接口(jl_kws_common.h 声明,供内部调用/实现替换)
jl_kws_algo(jl_kws_common.h L46-L51):
| 函数 | 说明 |
|---|---|
int jl_kws_algo_init(void) | 算法初始化,失败返回 JL_KWS_ERR_ALGO_NO_BUF 等负值 |
int jl_kws_algo_start(void) | 启动算法运行态 |
void jl_kws_algo_stop(void) | 停止算法 |
void jl_kws_algo_close(void) | 释放算法资源 |
void *jl_kws_algo_get_frame_buf(u32 *buf_len) | 获取一帧检测缓冲,失败返回 NULL |
int jl_kws_algo_detect_run(u8 *buf, u32 len) | 对一帧数据执行检测,返回 KWS_VOICE_EVENT_* |
jl_kws_audio(jl_kws_common.h L56-L60):
| 函数 | 说明 |
|---|---|
int jl_kws_audio_init(void) | 采集通道初始化 |
int jl_kws_audio_start(void) | 启动采集 |
void jl_kws_audio_stop(void) | 停止采集 |
void jl_kws_audio_close(void) | 关闭采集通道 |
int jl_kws_audio_get_data(void *buf, u32 len) | 向缓冲填充一帧 MIC 数据,返回实际长度 |
jl_kws_event(jl_kws_common.h L65-L68):
| 函数 | 说明 |
|---|---|
int jl_kws_event_init(void) | 事件模块初始化 |
void jl_kws_event_stop(void) | 停止事件上报 |
void jl_kws_event_close(void) | 关闭事件模块 |
void jl_kws_event_state_update(u8 voice_event) | 上报识别事件(KWS_VOICE_EVENT_YES/NO) |
Failure Modes, Edge Cases & Concurrency
错误码定义
jl_kws_common.h 统一了模块错误码(jl_kws_common.h L27-L35):
| 错误码 | 值 | 触发场景 |
|---|---|---|
JL_KWS_ERR_NONE | 0 | 成功 |
JL_KWS_ERR_AUDIO_MIC_NO_BUF | -400 | 音频模块 MIC 缓冲缺失 |
JL_KWS_ERR_AUDIO_INIT_STATE_ERR | -401 | 音频初始化状态异常(重复/顺序错误) |
JL_KWS_ERR_AUDIO_MIC_STATE_ERR | -402 | MIC 状态异常(如未初始化即 start) |
JL_KWS_ERR_ALGO_NO_BUF | -300 | 算法缓冲分配失败 |
JL_KWS_ERR_ALGO_NO_FRAME_BUF | -301 | 取帧缓冲返回 NULL(算法未就绪) |
典型失败路径
- 初始化失败自清理:任务内
jl_kws_speech_recognition_init()任一步返回非 0 时,任务调用kws_speech_recognition_close()释放已初始化资源、置task_init = 0并task_kill自杀;但此时__this->kws_state尚未置为 INIT,外部再次open()可重新创建任务(jl_kws_main.c L174-L183)。 - 帧缓冲缺失:
jl_kws_algo_get_frame_buf返回 NULL 时退出检测循环并返回JL_KWS_ERR_ALGO_NO_FRAME_BUF,需要上层重新start()。 - 数据不足:
audio_get_data返回长度不等于帧长时跳过本轮检测(不产生事件),这是半帧数据的正常降级行为。
并发与竞态控制
- 单任务串行化:所有子模块操作都发生在
kws任务上下文,API 与任务之间仅通过os_taskq_post_msg传递,不存在多线程同时操作算法/音频资源的问题。 - 状态标志的可见性:
kws_state由 API 线程写入、任务线程读取。在单核 MCU 且无抢占中断改写的前提下是安全的;若在中断中调用 API,需确认平台os_taskq_post_msg的中断安全性。 - close 的同步语义:
close()通过del_sem无限等待,保证“返回后模块完全不可用”,避免任务被杀时仍在访问音频/算法资源。os_sem_pend(&del_sem, 0xffff)的超时上限同时避免系统异常时永久挂起(jl_kws_main.c L263-L278)。 - LP 与 RUN 互斥:
kws_idle_query在 RUN 状态返回“忙”,系统低功耗框架不会在识别期间挂起 CPU,这是保证不漏检的关键约束。
Performance & Operational Considerations
- CPU 频率:识别期间建议将系统频率提升至
KWS_BT_CALL_SYS_FREQUENCE_HZ(48 MHz),否则逐帧algo_detect_run可能超时造成帧丢弃(jl_kws_main.c L19)。 - 低功耗调度:任务注册为
kws_lp_target,非识别态允许系统睡眠;识别态保持唤醒。产品功耗优化应聚焦缩短 RUN 窗口(如无交互超时自动stop())。 - 调试手段:使能
KWS_DEBUG_ENABLE后可用kws_debug/kws_putchar打印帧检测节奏(循环中的kws_putchar('r')注释);kws_info恒为printf,可用于跟踪 open/init/run/stop/close 调用序列(jl_kws_common.h L15-L24)。 - 内存布局:
jl_kws_common.h预留的#pragma *_seg支持将 KWS 数据/常量/代码段放入专用内存段(例如低功耗常驻 SRAM),批量生产时可按需启用。
Extension Points
- 算法替换:实现
jl_kws_algo_*六个接口即可替换唤醒算法(含模型),jl_kws_main.c无需改动;这是组件与算法解耦的直接收益。 - 音频通路扩展:
kws_aec_data_output()/kws_aec_get_state()允许外部音频模块(如通话 AEC 链路)向 KWS 注入音频数据,适合“唤醒词在通话链路中检测”的产品形态(jl_kws_api.h L12-L13)。 - 事件消费:命中事件在
jl_kws_event_state_update内分发(KWS_VOICE_EVENT_YES = 2/KWS_VOICE_EVENT_NO = 3,jl_kws_common.h L37-L41),业务侧可在此挂接唤醒提示音、LED、蓝牙回连等动作。 - 平台差异:各平台算法配置独立存放于
cpu/br23/tools/jl_kws.cfg、cpu/br25/tools/jl_kws.cfg、cpu/br30/tools/jl_kws.cfg,移植到新平台时复制并调整对应配置即可。
Related Links
- jl_kws_api.h — 对外 API 声明
- jl_kws_main.c — 任务、状态机与生命周期实现
- jl_kws_common.h — 错误码、事件与子模块接口定义
- jl_kws_algo.c / jl_kws_audio.c / jl_kws_event.c — 子模块实现
- br23 平台算法配置 jl_kws.cfg
- 目录
8-common-components下其他公共组件文档(音频通路、事件框架等)请参见对应目录页。