4. Kernel DSI驱动:DSI控制器驱动框架、DCS命令发送、读写寄存器
好,我们进入第四章节。DSI驱动,说白了就是让CPU和屏幕能“对话”。你想想看,上层应用要显示一张图片,最终得通过DSI总线把像素数据刷到屏上。这中间,DSI控制器就是那个“翻译官”和“快递员”。
我个人习惯把DSI驱动分成三层来看:控制器驱动框架、DCS命令的收发、以及寄存器的读写。这三层搞明白了,DSI驱动这块你就拿下了大半。
4.1 DSI控制器驱动框架
联发科的DSI控制器,在Linux内核里通常被抽象成一个mipi_dsi_host。这个结构体是核心,它负责管理物理层(D-PHY)和协议层。
驱动框架的初始化流程,我总结为三步走:
- 设备树解析:从dts里拿到DSI控制器的基地址、中断号、时钟频率等。
- D-PHY配置:设置lane数、速率、时钟极性。这一步错了,屏幕直接黑屏,连log都看不到。
- 回调函数注册:实现
transfer、attach、detach等回调,供上层panel驱动调用。
来看一个典型的驱动入口代码片段:
static const struct mipi_dsi_host_ops mtk_dsi_ops = {
.attach = mtk_dsi_attach,
.detach = mtk_dsi_detach,
.transfer = mtk_dsi_transfer,
};
static int mtk_dsi_probe(struct platform_device *pdev)
{
struct mtk_dsi *dsi;
// 分配结构体、获取资源、初始化锁
dsi->host.ops = &mtk_dsi_ops;
mipi_dsi_host_register(&dsi->host);
return 0;
}
嗯,这里要注意:transfer函数是核心中的核心。所有发给屏幕的命令,最终都会走到这个函数里。
关键点:DSI控制器驱动不直接操作屏幕,它只负责“怎么发”。至于“发什么”,那是panel驱动的事。这种分层设计,让代码复用性很高。
4.2 DCS命令发送
DCS(Display Command Set)是MIPI联盟定义的标准命令集。说白了,就是一套屏幕能听懂的“指令”。比如0x11是退出睡眠,0x29是开启显示。
发送DCS命令,通常有两种方式:
- 短包(Short Packet):用于无参数或带1-2字节参数的命令。比如
0x11就是短包。 - 长包(Long Packet):用于带大量参数的命令,比如初始化序列(几十个字节)。
我在项目中遇到过一个问题:发送初始化序列时,屏幕死活不亮。后来发现是长包的CRC校验没算对。联发科的硬件其实会自动计算CRC,但前提是你得把DCS_LONG_PACKET的包头格式写对。
来看一个发送DCS命令的典型实现:
static int mtk_dsi_transfer(struct mipi_dsi_host *host,
const struct mipi_dsi_msg *msg)
{
struct mtk_dsi *dsi = host_to_dsi(host);
u32 config;
// 根据消息类型,配置硬件寄存器
if (msg->type == MIPI_DSI_DCS_SHORT_WRITE) {
// 写无参数命令
writel(msg->tx_buf[0], dsi->regs + DSI_TX_DATA0);
config = DSI_CMD_SHORT | DSI_CMD_WRITE;
} else if (msg->type == MIPI_DSI_DCS_LONG_WRITE) {
// 写长包命令
// 先写长度,再写数据
writel(msg->tx_len, dsi->regs + DSI_TX_LEN);
// 循环写入数据到FIFO
for (i = 0; i < msg->tx_len; i++)
writel(msg->tx_buf[i], dsi->regs + DSI_TX_FIFO);
config = DSI_CMD_LONG | DSI_CMD_WRITE;
}
// 触发发送
writel(config, dsi->regs + DSI_CMD_CTRL);
// 等待发送完成中断或轮询状态位
return 0;
}
个人经验:发送DCS命令后,一定要等待硬件“忙”状态清除。我曾经因为没加等待,连续发两条命令,结果第二条被吞了。调试了两天才发现是时序问题。
4.3 读写寄存器
DSI控制器的寄存器,说白了就是一堆内存映射的地址。联发科的DSI寄存器通常包括:
- 控制寄存器:使能、复位、模式选择。
- 状态寄存器:FIFO空/满、传输完成、错误标志。
- 数据寄存器:发送FIFO、接收FIFO。
- 时序寄存器:HSA、HBP、HFP、VSA等。
读写寄存器,我建议直接用readl/writel。但要注意,有些寄存器是自清零的,你读到的值和写进去的值可能不一样。
举个例子,读取DSI控制器的版本号:
static u32 mtk_dsi_read_version(struct mtk_dsi *dsi)
{
u32 ver;
ver = readl(dsi->regs + DSI_VERSION);
dev_info(dsi->dev, "DSI version: 0x%08x\n", ver);
return ver;
}
写寄存器时,我习惯用readl_modify模式,避免影响其他位:
static void mtk_dsi_set_bits(struct mtk_dsi *dsi, u32 reg, u32 bits)
{
u32 val = readl(dsi->regs + reg);
val |= bits;
writel(val, dsi->regs + reg);
}
避坑指南:我曾经在调试时,直接往一个保留位写了1,结果DSI控制器直接挂死,连中断都进不去。所以,永远不要假设保留位是0。读-改-写才是安全的做法。
4.4 实战中的常见问题
讲几个我实际调试中遇到的坑:
- 时钟频率不匹配:DSI的bit clock和panel要求的频率不一致,导致花屏。解决办法是用示波器量一下,或者读PLL的锁定状态寄存器。
- Lane数配置错误:panel是4 lane,你配成了2 lane,带宽不够,屏幕闪。这个在设备树里配好就行。
- DCS命令顺序:有些panel要求先发
0x11(退出睡眠),等120ms,再发0x29(开启显示)。顺序错了,屏幕不亮。
嗯,说到这,我想起一个案例。有一次客户反馈,屏幕在低温下不亮。我查了半天,发现是初始化序列里有个延时太短。联发科的DSI控制器在低温下,D-PHY锁定时间会变长。把延时从10ms改成50ms,问题就解决了。
核心总结:DSI驱动框架是骨架,DCS命令是血肉,寄存器读写是神经。三者配合好,屏幕才能正常点亮。调试时,多用dev_dbg打印关键寄存器的值,能省很多时间。
下一章,我们会深入Panel驱动,讲讲怎么点亮一块具体的屏幕。到时候,我会把初始化序列的配置细节掰开揉碎了讲。