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)上出现以下问题:
- BH201 Output Tuning 阶段:所有 (sela, selb) 组合调用 MTK
.execute_tuning 均返回 PASS
- 实际读写阶段:频繁出现 CMD CRC / DATA CRC 错误
- 最终结果: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
| for (i = 0 ; i < PAD_DELAY_2CELL_MAX; i++) { msdc_set_cmd_delay(host, i); msdc_set_data_delay(host, i); ret = mmc_send_tuning(mmc, opcode, NULL); if (!ret) rise_delay |= (1ULL << i); }
if (final_maxlen > 0) return 0;
|
问题:
- 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驱动不可行?
- 客户不愿修改Host代码:MTK Host驱动由平台方维护,修改需要客户审批
- GKI限制:Android GKI (Generic Kernel Image) 环境下不方便修改内核公共代码
- 风险问题:修改Host驱动可能影响其他不使用Retimer的产品线
结论:需要在BH201 Retimer驱动侧实现Workaround。
4. 解决方案迭代
4.1 方案V1:固定阈值(失败)
思路:既然MTK返回的maxlen值能反映真实的信号质量,那么在BH201侧增加一个固定阈值判定。
1 2 3 4 5 6 7 8 9 10
| #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; } }
|
问题:
- 固定阈值32在某些环境下过高,导致所有phase都Fail
- 不同SD卡、不同温度环境下最佳窗口大小不同
- 缺乏自适应能力
4.2 方案V2:默认阈值 + 自适应调整(部分成功)
思路:使用滑动窗口记录历史maxlen值,动态计算自适应阈值。
1 2 3 4 5 6 7 8 9 10 11 12 13
| #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; };
|
算法:
- 维护5个最近maxlen值的滑动窗口
- 记录全局最小值和最大值
- 使用加权中值计算自适应阈值:
(3 * median + (global_min + global_max) / 2) / 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
| 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; };
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; w->values[w->index] = maxlen; w->index = (w->index + 1) % BH201_TUNING_WINDOW_SIZE; if (w->count < BH201_TUNING_WINDOW_SIZE) w->count++; if (maxlen < w->global_min) w->global_min = maxlen; if (maxlen > w->global_max) w->global_max = maxlen; if (w->count == 1) { w->first_maxlen = maxlen; w->adaptive_mid = maxlen; return maxlen; } return w->adaptive_mid; }
|
判定逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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; } }
|
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
| bool ggc_sd_tuning(struct sdhci_host *host, bool *first_cmd19_status) { int err; bool ret = TRUE; err = host_execute_tuning(); if (err == -ETIMEDOUT) { ret = FALSE; pr_info("BHT MSG:%s: tuning timeout\n", __func__); goto exit; } 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 技术要点
- 不修改Host驱动:通过在Retimer驱动侧解析Host返回的maxlen值,实现无侵入式Workaround
- 自适应阈值:首次maxlen作为基线,后续通过滑动窗口动态调整
- 环境自适应:不依赖硬编码阈值,适配不同硬件/温度/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 (自适应初始阈值)