一、背景与现象
硬件平台:NXP FRDM-RW612 + LCD-PAR-S035 显示屏扩展板(ST7796S,480x320,通过 RW612 的 LCDIC 外设以 4 线 SPI + DMA 驱动;触摸芯片 GT911,挂在 flexcomm2 I2C 总线上,地址 0x5D)。
软件平台:Zephyr 4.4.0,应用为官方 LVGL 示例(samples/subsys/display/lvgl)的移植。
现象:启用 LVGL 后烧录固件,屏幕满屏彩色噪点,串口一条日志都没有,系统看似彻底死亡。
而同一套硬件:
跑 Zephyr 裸 display 示例(不用 LVGL)→ 正常
跑 NXP 官方 MCUXpresso SDK 的 LVGL 工程 → 显示和触摸都正常
二、排查过程
2.1 排除 LVGL 本身
最初怀疑 LVGL 渲染缓冲、flush 线程栈(默认仅 1024 字节)、像素格式转换等。通过加大栈、启动时用 display_write 直接刷屏(绕过 LVGL)等手段,确认显示链路正常——问题不在 LVGL。
2.2 关键分叉点:CONFIG_INPUT
二分配置发现:只要 CONFIG_INPUT=n,系统完全正常;INPUT=y 就死机。
原因藏在 shield 的 Kconfig.defconfig 里:
if SHIELD_LCD_PAR_S035 if LVGL config INPUT default y # LVGL=y 时 INPUT 默认打开 endif endif
也就是说只有 LVGL 构建才会启用 GT911 触摸驱动的初始化——裸 display 示例从不碰触摸,所以一直正常。这解释了"加 LVGL 就坏"的表象。
2.3 为什么连日志都没有?
这是本问题最阴险的地方。GT911 初始化发生在 shield 的 SYS_INIT(POST_KERNEL 阶段,优先级 85),早于 main():
挂死在 SYS_INIT → main() 永远不执行
Zephyr 默认 deferred 日志:日志由专门的后台线程输出,系统卡在启动阶段时日志线程还没调度起来 → 缓冲区里的日志一条都打不出来
ST7796S 初始化没跑完 → 屏幕显示的是显存上电随机值 = 满屏噪点

对策:CONFIG_LOG_MODE_IMMEDIATE=y(同步日志)+ 在 shield.c 和 GT911 驱动里埋 LOG_INF 标记。同步日志下,挂死前打印的最后一行就是案发地点。
2.4 调试器的陷阱
用 Ozone/J-Link 单步时发现停在各种奇怪位置,故障转储显示 MSP=0xFFFFFFE0、PC 是垃圾值。但这些转储里都有一个共同标记:Debug event——Cortex-M33 在 DebugMonitor 异常未使能时,调试器的 halt/单步动作会被升级成 HardFault。这些"崩溃"全是调试器制造的假象,真实挂死点只能靠自由运行 + 同步日志埋点确定。
经验:调中断密集的驱动初始化代码时,用"运行到断点"(F5),不要单步(F10/F11)。
2.5 埋点定位
自由运行 + 埋点日志,最后一行输出:
[00:00:00.254,956] <inf> lcd_par_s035: shield: gt911 init... [00:00:00.261,126] <inf> gt911: gt911: init entry [00:00:00.266,337] <inf> gt911: gt911: INT pin low, entering sleeps [00:00:00.343,232] <inf> gt911: gt911: GPIO done, reading ID via I2C addr 0x5d
← 死在这里:第一笔 I2C 传输
挂死在 GT911 读取芯片 ID 的第一笔 I2C 传输,且 CONFIG_I2C_NXP_TRANSFER_TIMEOUT=1000 的 1 秒超时也没有生效。
2.6 I2C 扫描实验:问题比 GT911 更底层
暂时禁用 GT911 节点让系统能启动到 main(),在应用里加 I2C 地址扫描(探测 0x48 板载温度传感器、0x5D、0x14 两个 GT911 候选地址):
[00:03:28.449] i2c probe 0x48 ... [00:03:29.455] i2c probe 0x48 -> ret 0 ← 耗时整整 1005ms! [00:03:29.460] i2c probe 0x5d ... ← 永久卡死
解读:
板载传感器 0x48 必然在线,正常应答只需几百微秒,却等满 1000ms 超时——而且驱动忽略了 k_sem_take 的超时返回值,把陈旧状态(初始值 0 = 成功)当成结果返回。说明 I2C 完成中断从未触发,ret 0 是假象
第二笔传输直接把系统拖死(硬件状态机留在中途,中断风暴,主线程冻结)
结论:这块板子上 flexcomm I2C 的传输链路压根就没工作过,GT911 只是第一个实际发起传输的受害者。
三、根因
3.1 上游已知的 RW612 回归
搜索发现上游 issue zephyrproject-rtos/zephyr#107017,报告者同样在 RW612 + Zephyr 4.4 上遇到 I2C 卡死,并指出是 commit 8f24beb 引入的回归——它在 PM_DEVICE_ACTION_RESUME 路径上新增了一次 pinctrl_apply_state()。
3.2 触发路径
Zephyr 的设备 PM 初始化函数 pm_device_driver_init()(subsys/pm/device.c)在启动时会连续执行两个 PM 动作:
/* Run power-up logic */
rc = action_cb(dev, PM_DEVICE_ACTION_TURN_ON); // → mcux_flexcomm_init_common()
...
/* If device has no PM structure */
if (pm == NULL) {
/* Device should always be active */
return action_cb(dev, PM_DEVICE_ACTION_RESUME); // → 又一次 pinctrl_apply_state()
}于是每次启动,flexcomm I2C 的初始化时序变成:
TURN_ON: pinctrl_apply_state() → I2C_MasterInit() → 注册中断 → 使能中断 RESUME: pinctrl_apply_state() ← 又来了第二次!(此时 MasterInit 已经做完了)
3.3 为什么第二次 pinctrl 是致命的
RW61x 的引脚复用由 MCI_IO_MUX 控制,pinctrl_apply_state() 对 flexcomm 功能引脚的操作是"先清零功能选择位、再重新置位"。在 I2C_MasterInit() 完成后再来这么一次,flexcomm 的功能选择被中途打断重建,I2C 外设状态机进入损坏状态:
传输发起后永远等不到完成中断(第一笔传输死等超时)
外设残留状态导致后续传输触发中断风暴,CPU 冻结(第二笔传输系统死机)
issue 报告者验证:删掉 RESUME 里的 pinctrl_apply_state(),或者在它之后重新执行 I2C_MasterInit(),I2C 即恢复正常。
四、修复
修改 Zephyr 树 drivers/i2c/i2c_mcux_flexcomm.c,让 RESUME 走完整初始化(保证 pinctrl 永远发生在 I2C_MasterInit 之前):
static int i2c_mcux_flexcomm_pm_action(const struct device *dev, enum pm_device_action action)
{
const struct mcux_flexcomm_config *config = dev->config;
int error;
switch (action) {
case PM_DEVICE_ACTION_RESUME:
- error = pinctrl_apply_state(config->pincfg, PINCTRL_STATE_DEFAULT);
- if (error < 0 && error != -ENOENT) {
- return error;
- }
- break;
+ /* Workaround for zephyrproject-rtos/zephyr#107017: on RW61x,
+ * re-applying pinctrl after I2C_MasterInit() leaves the flexcomm
+ * I2C peripheral in a bad state (transfers never complete, device
+ * can get stuck in its ISR). Run the full init instead so that
+ * pinctrl is always applied before I2C_MasterInit().
+ */
+ return mcux_flexcomm_init_common(dev);
case PM_DEVICE_ACTION_SUSPEND:
error = pinctrl_apply_state(config->pincfg, PINCTRL_STATE_SLEEP);
if (error < 0 && error != -ENOENT) {mcux_flexcomm_init_common() 内部顺序为:pinctrl → I2C_MasterInit() → 创建传输句柄 → 配置波特率 → 连接并使能中断。重复执行是幂等的(IRQ_CONNECT/irq_enable 重复调用无副作用),因此这个修复同时兼容带 PM 的运行时挂起/恢复路径。
五、验证
修复后同样的 I2C 扫描:
[00:40:47.039] i2c probe 0x48 ... [00:40:47.044] i2c probe 0x48 -> ret 0 ← 5ms 完成(修复前 1005ms) [00:40:47.050] i2c probe 0x5d ... [00:40:47.055] i2c probe 0x5d -> ret 0 ← GT911 在 0x5D 正常应答! [00:40:47.060] i2c probe 0x14 ... [00:40:47.066] i2c probe 0x14 -> ret -5 ← 干净利落的 NAK,不再死锁
地址 修复前 修复后 说明
0x48 1005ms 超时,假成功 5ms,ret 0 板载 p3t1755 温度传感器
0x5D 系统死锁 5ms,ret 0 GT911 真实地址
0x14 (未执行到) 5ms,ret -5 无应答,正常返回错误
恢复 GT911 节点后:启动正常、显示正常、GT911 初始化成功,触摸事件经 Zephyr input 子系统进入 LVGL(shield 的 zephyr,touch chosen 节点使 CONFIG_LV_Z_POINTER_INPUT 自动生效),点击屏幕上的按钮可正常响应。
六、经验总结
"无任何日志"本身就是高价值线索:说明系统挂在 main() 之前的 SYS_INIT 阶段,且默认的 deferred 日志在这种场景下必然沉默。CONFIG_LOG_MODE_IMMEDIATE=y 是排查启动期问题的第一步。
调试器会撒谎:Cortex-M 下单步/halt 可能触发 Debug event 型 HardFault,表现出栈损坏、PC 乱飞等骇人假象。自由运行 + 埋点日志才是启动类问题的金标准。
分层二分:LVGL → INPUT 子系统 → GT911 驱动 → I2C 驱动,逐层排除,每一层都用"能跑通的最小配置"验证。
用已知良好的参照物:板载传感器(0x48)验证了"I2C 传输整体坏掉"而非 GT911 单体问题,避免了在触摸驱动里空耗。
先搜上游 issue:RW612 是相对新的平台,Zephyr 4.4 的回归 issue #107017 几乎描述了完全相同的现象,直接锁定了根因方向。
附:相关配置
应用 prj.conf 关键项:
CONFIG_INPUT=y # 启用 GT911 触摸 CONFIG_I2C_NXP_TRANSFER_TIMEOUT=1000 # I2C 传输超时兜底(驱动默认 K_FOREVER 死等) CONFIG_LVGL=y CONFIG_LV_Z_FLUSH_THREAD_STACK_SIZE=4096 # 默认 1024 对 LCDIC DMA 写链偏紧
注意:该修复在 Zephyr 树内(drivers/i2c/i2c_mcux_flexcomm.c),不在应用仓库中。更新 Zephyr 树时需检查上游是否已修复 #107017,未修复则需重新打上此补丁。
我要赚赏金
