4. Kernel DSI驱动:DSI控制器驱动框架、DCS命令发送、读写寄存器

好,我们进入第四章节。DSI驱动,说白了就是让CPU和屏幕能“对话”。你想想看,上层应用要显示一张图片,最终得通过DSI总线把像素数据刷到屏上。这中间,DSI控制器就是那个“翻译官”和“快递员”。

我个人习惯把DSI驱动分成三层来看:控制器驱动框架DCS命令的收发、以及寄存器的读写。这三层搞明白了,DSI驱动这块你就拿下了大半。

4.1 DSI控制器驱动框架

联发科的DSI控制器,在Linux内核里通常被抽象成一个mipi_dsi_host。这个结构体是核心,它负责管理物理层(D-PHY)和协议层。

驱动框架的初始化流程,我总结为三步走:

  1. 设备树解析:从dts里拿到DSI控制器的基地址、中断号、时钟频率等。
  2. D-PHY配置:设置lane数、速率、时钟极性。这一步错了,屏幕直接黑屏,连log都看不到。
  3. 回调函数注册:实现transferattachdetach等回调,供上层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 实战中的常见问题

讲几个我实际调试中遇到的坑:

  1. 时钟频率不匹配:DSI的bit clock和panel要求的频率不一致,导致花屏。解决办法是用示波器量一下,或者读PLL的锁定状态寄存器。
  2. Lane数配置错误:panel是4 lane,你配成了2 lane,带宽不够,屏幕闪。这个在设备树里配好就行。
  3. DCS命令顺序:有些panel要求先发0x11(退出睡眠),等120ms,再发0x29(开启显示)。顺序错了,屏幕不亮。

嗯,说到这,我想起一个案例。有一次客户反馈,屏幕在低温下不亮。我查了半天,发现是初始化序列里有个延时太短。联发科的DSI控制器在低温下,D-PHY锁定时间会变长。把延时从10ms改成50ms,问题就解决了。

核心总结:DSI驱动框架是骨架,DCS命令是血肉,寄存器读写是神经。三者配合好,屏幕才能正常点亮。调试时,多用dev_dbg打印关键寄存器的值,能省很多时间。

下一章,我们会深入Panel驱动,讲讲怎么点亮一块具体的屏幕。到时候,我会把初始化序列的配置细节掰开揉碎了讲。