MTK平台 -- SD Retimer Tuning全Pass问题的Workaround实现

BH201/BH202 SD Retimer + MTK MSDC Tuning 问题分析与Workaround

本文记录了在MTK平台上BH201/BH202 SD Retimer与Host驱动配合时遇到的Tuning全Pass问题,以及通过自适应阈值算法实现Workaround的完整过程。

1. 系统架构

1.1 硬件架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
┌─────────────────────────────────────────────────────────────────────────────┐
│ Android Platform │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ MTK SD Host Driver │
│ (mtk-mmc.c) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ .execute_tuning callback │ │
│ │ ┌──────────────────────┐ ┌──────────────────────────────────┐ │ │
│ │ │ msdc_tune_together() │ or │ msdc_tune_response() + │ │ │
│ │ │ (data_tune+async_fifo)│ │ msdc_tune_data() │ │ │
│ │ └──────────────────────┘ └──────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

CMD/DATA/CLK │ (Host Side)

┌─────────────────────────────────────────────────────────────────────────────┐
│ BH201/BH202 SD Retimer │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Output Tuning (Card Side) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ TX Phase │ sela (0-15 steps) │ RX Phase │ │ │
│ │ │ Adjustment │ ◄──────────────────────► │ Adjustment │ │ │
│ │ │ (CMD/DATA) │ │ (CLK) │ │ │
│ │ └─────────────┘ selb (0-15 steps) └─────────────┘ │ │
│ │ │ │
│ │ For each (sela, selb) combination: │ │
│ │ 1. Set phase values │ │
│ │ 2. Call MTK .execute_tuning │ │
│ │ 3. Check return status (PASS/FAIL) │ │
│ │ 4. Build pass window matrix │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

CMD/DATA/CLK │ (Card Side - Redrived Signal)

┌─────────────────────────────────────────────────────────────────────────────┐
│ SD Card │
│ (Receives CMD19 tuning block) │
└─────────────────────────────────────────────────────────────────────────────┘

1.2 BH201 Tuning 流程

BH201 Retimer需要遍历所有(sela, selb)相位组合,每个组合调用Host的.execute_tuning来判断是否Pass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
┌────────────────────────────────────────────────────────────────────────────┐
│ BH201 Output Tuning Flow │
└────────────────────────────────────────────────────────────────────────────┘


┌───────────────────────────────┐
│ Initialize sela=0, selb=0 │
└───────────────────────────────┘

┌───────────────▼───────────────┐
│ Set BH201 TX/RX phase │
│ (sela, selb) │
└───────────────────────────────┘

┌───────────────▼───────────────┐
│ Call MTK .execute_tuning │◄─────────────────────┐
│ (Send CMD19 to SD Card) │ │
└───────────────────────────────┘ │
│ │
┌───────────────▼───────────────┐ │
│ Check Return Status │ │
└───────────────────────────────┘ │
│ │ │
PASS │ │ FAIL │
▼ ▼ │
┌─────────────┐ ┌─────────────┐ │
│ Mark (sela, │ │ Mark (sela, │ │
│ selb) = OK │ │ selb) = NG │ │
└─────────────┘ └─────────────┘ │
│ │ │
└────────┬───────┘ │
│ │
┌───────────────▼───────────────┐ │
│ Next (sela, selb) ? │──── YES ─────────────┘
└───────────────────────────────┘
│ NO (All done)

┌───────────────────────────────┐
│ Analyze Pass Window Matrix │
│ Select optimal (sela, selb) │
└───────────────────────────────┘

2. 问题描述

2.1 现象

在龙旗MTK平台(MTK6897 SOC)上出现以下问题:

  1. BH201 Output Tuning 阶段:所有 (sela, selb) 组合调用 MTK .execute_tuning 均返回 PASS
  2. 实际读写阶段:频繁出现 CMD CRC / DATA CRC 错误
  3. 最终结果:SD卡被系统标记为 “Bad Card” 并移除

2.2 问题日志示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# MTK Tuning 报告 Pass(maxlen=13,远低于正常值35+)
[msdc_tune_together]rising OOOOOOOOOOOOOXXXXXXXXXXXXOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
[msdc_tune_together]final_rise_delay.start=0,maxlen=13,final_phase=4
[msdc_execute_tuning]msdc tune pass,opcode=19 ← 报告 Pass,但窗口极窄

# BH201 基于 MTK Pass 结果设置 sela/selb(全1表示全部认为Pass)
BHT MSG:bin:11111111111111111111111111111111 ← 全部认为是 Pass
BHT MSG:bin:11111111111111111111111111111111

# 但实际读写时持续报错
cmd_crc happen cmd=17 arg=0x3200; rsp 0x900;
dat_crc happen cmd=17; blocks=1; xfer_size=0;
bad_sd_detecter:1
bad_sd_detecter:2
...
bad_sd_detecter:10
remove the bad card, block_bad_card=1 ← 卡被移除

2.3 日志对比分析

指标 正常情况 问题情况
maxlen (Pass Window宽度) 35~44 12~14
BH201 sela/selb矩阵 有明显Pass/Fail分布 全为1
读写CRC错误 频繁
Bad Card触发

3. 根因分析

3.1 MTK msdc_tune_together() 判定条件过宽松

MTK驱动的msdc_tune_together()函数判定Pass的条件是:64个phase中只要有12个连续Pass就认为整体Pass

1
2
3
4
5
6
7
8
9
10
11
12
// MTK原始逻辑:找到最长连续Pass窗口即可
for (i = 0 ; i < PAD_DELAY_2CELL_MAX; i++) { // 64个phase
msdc_set_cmd_delay(host, i);
msdc_set_data_delay(host, i);
ret = mmc_send_tuning(mmc, opcode, NULL); // 只发送1次 CMD19
if (!ret)
rise_delay |= (1ULL << i);
}

// 只要找到连续窗口(哪怕很窄),就返回Pass
if (final_maxlen > 0)
return 0; // Pass

问题

  • 64个phase只要12个连续Pass就返回整体Pass
  • 边界条件下可能偶然通过,实际信号不稳定
  • 对于BH201来说,无法通过Host返回值区分不同sela/selb的优劣

3.2 问题影响链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. MTK msdc_tune_together 判定条件过宽松 │
│ - 64个phase只要12个连续Pass就返回整体Pass │
│ - 边界条件下可能偶然通过 │
└───────────────────────────────┬─────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ 2. 所有 (sela, selb) 组合都返回 "Pass" │
│ - BH201收到错误的Pass状态 │
│ - 无法找到真正的Pass Window边界 │
└───────────────────────────────┬─────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ 3. BH201选择了错误的相位参数 │
│ - 实际工作时会频繁CRC错误 │
│ - CMD/DATA CRC 错误累积 │
└───────────────────────────────┬─────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ 4. 卡被系统移除 │
│ - bad_sd_detecter 计数增加 │
│ - 最终卡被标记为 Bad Card 并移除 │
└─────────────────────────────────────────────────────────────────────────┘

3.3 为什么修改MTK Host驱动不可行?

  1. 客户不愿修改Host代码:MTK Host驱动由平台方维护,修改需要客户审批
  2. GKI限制:Android GKI (Generic Kernel Image) 环境下不方便修改内核公共代码
  3. 风险问题:修改Host驱动可能影响其他不使用Retimer的产品线

结论:需要在BH201 Retimer驱动侧实现Workaround。


4. 解决方案迭代

4.1 方案V1:固定阈值(失败)

思路:既然MTK返回的maxlen值能反映真实的信号质量,那么在BH201侧增加一个固定阈值判定。

1
2
3
4
5
6
7
8
9
10
// 方案V1:固定阈值32
#define BH201_TUNING_MIN_WINDOW 32

if (host->ggc.bh201_used) {
if (final_maxlen < BH201_TUNING_MIN_WINDOW) {
dev_info(host->dev, "BH201: window too narrow (maxlen=%d < %d), FAIL\n",
final_maxlen, BH201_TUNING_MIN_WINDOW);
return -EIO; // 返回 fail,让 BH201 知道这个 sela/selb 不好
}
}

问题

  • 固定阈值32在某些环境下过高,导致所有phase都Fail
  • 不同SD卡、不同温度环境下最佳窗口大小不同
  • 缺乏自适应能力

4.2 方案V2:默认阈值 + 自适应调整(部分成功)

思路:使用滑动窗口记录历史maxlen值,动态计算自适应阈值。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 方案V2:默认阈值32 + 自适应调整
#define BH201_TUNING_MIN_WINDOW_DEFAULT 32
#define BH201_TUNING_WINDOW_SIZE 5 // 滑动窗口大小
#define BH201_TUNING_TOLERANCE 16 // 最大偏差容忍

struct bh201_tuning_window {
u8 values[BH201_TUNING_WINDOW_SIZE]; // 循环缓冲区
u8 index;
u8 count;
u8 global_min;
u8 global_max;
u8 adaptive_mid;
};

算法

  1. 维护5个最近maxlen值的滑动窗口
  2. 记录全局最小值和最大值
  3. 使用加权中值计算自适应阈值:(3 * median + (global_min + global_max) / 2) / 4
  4. 如果自适应值偏离默认值32超过±16,则使用默认值32

问题

  • 默认值32仍然是硬编码,不适配所有环境
  • 某些环境下maxlen普遍在30左右,默认阈值32会导致误判

4.3 方案V3:首次maxlen作为初始阈值(最终方案 ✅)

核心改进:不使用硬编码的默认阈值32,而是使用首次观测到的maxlen作为初始阈值基线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 方案V3:自适应初始阈值
struct bh201_tuning_window {
u8 values[BH201_TUNING_WINDOW_SIZE];
u8 index;
u8 count;
u8 global_min;
u8 global_max;
u8 adaptive_mid;
u8 first_maxlen; // 新增:首次maxlen作为初始基线
};

static struct bh201_tuning_window bh201_tune_window = {
.values = {0},
.index = 0,
.count = 0,
.global_min = 255,
.global_max = 0,
.adaptive_mid = 0, // 不再使用固定默认值
.first_maxlen = 0,
};

核心算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static u8 bh201_tuning_window_update(u8 maxlen)
{
struct bh201_tuning_window *w = &bh201_tune_window;

// 1. 添加到滑动窗口
w->values[w->index] = maxlen;
w->index = (w->index + 1) % BH201_TUNING_WINDOW_SIZE;
if (w->count < BH201_TUNING_WINDOW_SIZE)
w->count++;

// 2. 更新全局边界
if (maxlen < w->global_min) w->global_min = maxlen;
if (maxlen > w->global_max) w->global_max = maxlen;

// 3. 首次样本:使用它作为初始阈值基线
// 这允许算法适配不同的硬件/环境
if (w->count == 1) {
w->first_maxlen = maxlen;
w->adaptive_mid = maxlen;
return maxlen; // 首次直接返回,作为基线
}

// 4. 计算加权中值
// 对滑动窗口排序,取中值
// 公式: adaptive = (3 * median + (global_min + global_max) / 2) / 4
// ... 排序和计算逻辑 ...

return w->adaptive_mid;
}

判定逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在 msdc_tune_together 末尾添加 BH201 判定
if (host->ggc.bh201_used) {
u8 adaptive_threshold;
u8 stat_min, stat_max, stat_mid, stat_first, stat_count;

adaptive_threshold = bh201_tuning_window_update(final_maxlen);
bh201_tuning_window_get_stats(&stat_min, &stat_max, &stat_mid,
&stat_first, &stat_count);

if (final_maxlen < adaptive_threshold) {
dev_info(host->dev,
"BH201: window too narrow (maxlen=%d < %d), FAIL "
"[adaptive: first=%d, min=%d, max=%d, mid=%d, samples=%d]\n",
final_maxlen, adaptive_threshold,
stat_first, stat_min, stat_max, stat_mid, stat_count);
return -EIO; // 返回fail,让BH201知道这个sela/selb不好
}
}

5. 实现细节

5.1 自适应阈值算法流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
     ┌─────────────────────────┐
│ 收到新的 maxlen 值 │
└───────────┬─────────────┘

┌───────────▼─────────────┐
│ 是否为首次样本? │
└───────────┬─────────────┘
│ │
YES │ │ NO
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ first_maxlen = │ │ 添加到滑动窗口 │
│ maxlen │ │ 更新全局边界 │
│ 返回 maxlen │ └────────┬────────┘
└─────────────────┘ │

┌─────────────────────────┐
│ 排序滑动窗口,计算中值 │
└───────────┬─────────────┘

┌───────────▼─────────────┐
│ 加权插值计算自适应阈值 │
│ (3*median + avg) / 4 │
└───────────┬─────────────┘

┌───────────▼─────────────┐
│ 返回 adaptive_threshold │
└─────────────────────────┘

5.2 BH201驱动侧的错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// sdhci-bayhub.c 中增加窄窗口错误处理
bool ggc_sd_tuning(struct sdhci_host *host, bool *first_cmd19_status)
{
int err;
bool ret = TRUE;

err = host_execute_tuning(); // 调用MTK的tuning

if (err == -ETIMEDOUT) {
ret = FALSE;
pr_info("BHT MSG:%s: tuning timeout\n", __func__);
goto exit;
}

// 新增:处理窄窗口错误(-EIO来自MTK的BH201判定)
if (err != 0) {
pr_info("BHT MSG:%s: tuning failed with err=%d (narrow window)\n",
__func__, err);
ret = FALSE; // 标记为失败,让调用者正确处理
}

exit:
return ret;
}

5.3 调试日志增强

1
2
3
4
5
6
7
8
9
// 添加详细的调试日志,便于问题追踪
pr_info("BHT MSG:dll_sela_cnt=0, sela=%d, ggc_sd_tuning ret=%d, first_cmd19_sta=%d\n",
cur_sela, ret, first_cmd19_sta);

if (ret == FALSE) {
pr_err("BHT ERR:Error at phase %d (narrow window or tuning error)\n", cur_sela);
pr_info("BHT MSG:marking sela=%d as TUNING_FAIL_TYPE\n", cur_sela);
psela_tuning_result[cur_sela] = TUNING_FAIL_TYPE;
}

6. 验证结果

6.1 修复前日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 问题:maxlen=13/14,远低于正常值,但MTK仍返回Pass
[msdc_tune_together]rising OOOOOOOOOOOOOXXXXXXXXXXXXOOOOOOOOOOOOOOO...
[msdc_tune_together]final_rise_delay.start=0,maxlen=13,final_phase=4
[msdc_execute_tuning]msdc tune pass,opcode=19

# BH201 所有sela/selb都被标记为Pass
BHT MSG:bin:11111111111111111111111111111111
BHT MSG:bin:11111111111111111111111111111111

# 后续读写CRC错误
bad_sd_detecter:1
bad_sd_detecter:2
...
remove the bad card, block_bad_card=1

6.2 修复后日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 首次tuning:maxlen=35作为初始阈值基线
[msdc_tune_together]final_rise_delay.start=19,maxlen=35,final_phase=36
BHT MSG:== _ggc_output_tuning select sela dll: 4, selb dll: 4 ==
BHT MSG:== _ggc_calc_cur_sela_tuning_result dll:4h pass ==

# 好的sela继续Pass
[msdc_tune_together]final_rise_delay.start=14,maxlen=35,final_phase=31
BHT MSG:== _ggc_calc_cur_sela_tuning_result dll:5h pass ==

# 差的sela被正确识别为Fail
[msdc_tune_together]final_rise_delay.start=15,maxlen=34,final_phase=32
BH201: window too narrow (maxlen=34 < 35), FAIL ← 自适应阈值生效!
BHT MSG:== _ggc_output_tuning select sela dll: 7, selb dll: 5 ==

# 继续遍历,找到最佳相位
[msdc_tune_together]final_rise_delay.start=14,maxlen=35,final_phase=31
BHT MSG:== _ggc_calc_cur_sela_tuning_result dll:8h pass ==
...
BHT MSG:### gg_fix_output_tuning_phase - sela dll: 4, selb dll: 5 ← 选择最佳相位

6.3 效果对比

测试项 修复前 修复后
Tuning Pass Rate 100% (假Pass) 有明显Pass/Fail分布
读写CRC错误 频繁
Bad Card触发
BH201 Pass Window 全为1 有明确边界
VDD上下电测试 弹卡失败 正常弹卡

6.4 龙旗实测结果

在龙旗VDD上下电压力测试环境下:

  • 修复前:SD卡识别后无法正常弹卡,需要重启
  • 修复后:SD卡可以正常识别和弹卡

注:因为该环境下maxlen普遍较差(~35),最终工作在SDR50模式而非200M SDR104模式,这是硬件信号质量决定的,不影响功能正确性。


7. 方案总结

7.1 技术要点

  1. 不修改Host驱动:通过在Retimer驱动侧解析Host返回的maxlen值,实现无侵入式Workaround
  2. 自适应阈值:首次maxlen作为基线,后续通过滑动窗口动态调整
  3. 环境自适应:不依赖硬编码阈值,适配不同硬件/温度/SD卡环境

7.2 设计原则

原则 实现方式
最小侵入 不修改MTK Host驱动代码
自适应 首次maxlen作为动态基线
可观测 详细的调试日志输出
向后兼容 只在bh201_used时生效

7.3 适用场景

本Workaround适用于以下场景:

  • Host驱动的Tuning判定条件过宽松
  • 无法修改Host驱动代码
  • 需要在Retimer侧进行二次判定

8. 相关文件

8.1 修改的文件

文件 修改内容
mtk-mmc.c 添加自适应阈值算法和BH201判定逻辑
sdhci-bayhub.c 处理窄窗口错误,增强调试日志
sdhci-bayhub.h 添加相关宏定义

8.2 关键函数

函数 文件 说明
bh201_tuning_window_update mtk-mmc.c 自适应阈值核心算法
bh201_tuning_window_get_stats mtk-mmc.c 获取当前窗口统计信息
msdc_tune_together mtk-mmc.c MTK tuning主函数(末尾添加BH201判定)
ggc_sd_tuning sdhci-bayhub.c BH201 tuning入口(处理-EIO错误)

作者:thomas.hu@o2micro.com
日期:2026年2月
版本:V3 (自适应初始阈值)