显示屏驱动

在一个嵌入式系统中,图形界面从设计到显示需要经过以下 3 个步骤:

  1. 应用程序将界面渲染到渲染缓冲区,这一步通常使用的库是 LVGL 或者 Qt。
  2. 将图像缓冲区的内容发送到显存,一般需要使用 SPI、I2C、或者并口。
  3. 显示器控制器将显存内容显示到屏幕上,根据显示器的特性,输出数量和通道数极多的信号。

这个过程中,图像信息依次以程序信息、渲染缓冲区、显存、屏幕像素四种形式存在,逐步从抽象的状态信息转换为物理存在的可见光信息。

对于直接支持显示屏接口的处理器来说,第二步可以直接在内存中进行。实际上最后一步并不适合于通用的处理器,比如 SSD1306 控制 128x64 的 OLED 屏时,需要对 64 行 128 列都给出单独的引脚,光这部分就需要 192 个引脚。而 TFT LCD 控制器也需要对 RGB888 格式使用 24 个引脚根据显示器的扫描时序,逐像素输出颜色。从引脚和时序的要求来看,这一步对于处理性能的要求非常大,但对于计算的要求不高,适合于硬件电路处理。

而对于其余的处理器而言,需要借助一个外接的显示屏控制器,如 SSD1306、ST7789、ILI9341 来完成。这个控制器的接口主要包括:

  • 初始化和配置屏幕相关各种设置
  • 配置屏幕的方向等信息
  • 和主机通信,修改显存内容

SPI 接口

其中 SPI 通信协议通常会使用 4 线进行通信:

  • CS 片选
  • CLK 时钟
  • MOSI 传输数据
  • D/C 指定传输的是数据还是命令
  • MISO 一般读不到东西,不使用

进行通信时,接口时序基本都一致:

  1. 初始化,将 CS 拉低,DC 设置为命令,逐个发送命令,并在发送每个命令后等待一段时间,让设备有处理的时间
  2. 发送命令指定要更新的显存区域,即一个矩形范围,有时候对于这个范围有一定的约束
  3. DC 设置为数据,开始传输显存内容。

驱动接口

针对于上述要求,可以发现这个通信接口分别依赖于单片机和显示控制器:

  • 进行通信的具体方式,如设置寄存器的部分需要根据使用的单片机确定。
  • 通信的内容需要根据使用的具体控制器型号确定。

这两个部分互相独立。通常来说,利用特定的单片机进行通信是比较简单的,而确定具体的通信内容则比较复杂。因此,今年发布的 LVGL 9 或者是 ESP-IDF 都内置了一些常见显示屏控制器的驱动协议。这些接口的通信方式实际上只需要两种:

  • 发送命令:控制 DC 引脚和通信接口,发送命令。由于命令通常有时序的要求,在发送完毕后才返回。
  • 发送数据:控制 DC 引脚和通信接口,发送数据。通常显存数据内容庞大,因此发送数据时可能是异步的。

移植

上述的接口在最近发布的库中通常都会默契地遵守,即使比较古老的库实际上也按照这个方法编写。因此,移植显示屏控制器代码时需要注意:

  • 接线:显示屏可能有多个引脚,接起来眼花缭乱。可以在 CubeMX 给每个引脚提供标签如LCD_MOSI [LCD P6],用于辅助接线。
  • GPIO:使用了上述的标签后,CubeMX 会提供宏作为标签的接口,可以避免硬编码具体的引脚。对于现有的代码,最好进行修改,只使用宏表示引脚。
  • 接口:实际上底层也通常会使用上述的命令、数据二分的方法定义接口,直接根据这个思路查看使用的通信外设配置即可。

基于这个原理移植的 ILI9341 代码见 https://github.com/melonedo/ILI9341-STM32F103C8