4、像素操作与画点:设置光标位置、画点函数、区域填充、清屏函数

好,咱们进入第四讲。这一讲可以说是整个LCD驱动开发的地基——像素操作。你想想看,屏幕上所有的文字、图标、波形,归根结底都是一个个像素点拼出来的。所以,怎么控制这些点,怎么高效地填充区域,怎么一键清屏,这些基本功必须扎实。

我个人习惯把这一章叫做「点线面的艺术」。因为从画一个点开始,到画一条线,再到填充一个面,逻辑是一脉相承的。今天咱们就把「点」和「面」搞定。

4.1 设置光标位置——指哪打哪

在画任何东西之前,你得先告诉屏幕:「我要在哪个位置动手」。这就是设置光标位置(SetCursor)的作用。

大多数LCD控制器(比如常见的SSD1306、ST7735、ILI9341)内部都有一个「写指针」。你往这个指针指向的地址写数据,像素就显示在那里。写完之后,指针会自动+1,指向下一个位置。

设置光标位置的函数,通常需要两个参数:X坐标和Y坐标。但这里有个坑——不同控制器的坐标系统不一样。有的以左上角为(0,0),X向右,Y向下;有的则是列和页(Page)的概念。

我曾经踩过的坑: 有一次我用的OLED屏,数据手册上写的是「Set Page Address」和「Set Column Address」。我一开始没仔细看,直接用X/Y坐标去设置,结果画出来的点全跑偏了。后来才发现,它需要先设置页地址(Y方向,每页8个像素),再设置列地址(X方向)。所以,拿到一块新屏,第一件事就是搞清楚它的坐标映射方式。

下面是一个典型的设置光标位置函数,以ST7735为例:

// 设置光标位置(窗口模式)
void LCD_SetCursor(uint16_t Xstart, uint16_t Ystart, uint16_t Xend, uint16_t Yend)
{
    // 写命令:设置列地址范围
    LCD_WriteCmd(0x2A);
    LCD_WriteData(Xstart >> 8);
    LCD_WriteData(Xstart & 0xFF);
    LCD_WriteData(Xend >> 8);
    LCD_WriteData(Xend & 0xFF);

    // 写命令:设置行地址范围
    LCD_WriteCmd(0x2B);
    LCD_WriteData(Ystart >> 8);
    LCD_WriteData(Ystart & 0xFF);
    LCD_WriteData(Yend >> 8);
    LCD_WriteData(Yend & 0xFF);

    // 写命令:开始写入显存
    LCD_WriteCmd(0x2C);
}

嗯,这里要注意:很多初学者会忽略高8位和低8位的拆分。如果你的屏幕分辨率超过256像素(比如320x240),坐标值就必须拆成两个字节发送。我见过有人直接传一个16位整数,结果屏幕只认了低8位,画面直接错位。

4.2 画点函数——最原子的操作

画点,说白了就是往指定位置写一个像素的颜色值。它是所有绘图操作的基础。

画点函数的逻辑很简单:

  1. 设置光标位置到目标点
  2. 写入该点的颜色数据

但这里有个性能问题。你想想看,每画一个点就要发送好几条指令(设置列、设置行、开始写入),如果画1000个点,就得发3000多条指令。这在嵌入式系统里是很慢的。

我的优化建议: 如果你要连续画很多点(比如画一条线或一个波形),不要一个一个地画。先设置好一个矩形区域,然后连续写入所有点的颜色数据。这样只需要发送一次设置命令,后面全是数据流,速度能快好几倍。

来看一个基本的画点函数:

// 画一个像素点
void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color)
{
    // 边界检查,防止越界
    if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return;

    // 设置光标到该点位置(窗口大小为1x1)
    LCD_SetCursor(x, y, x, y);

    // 写入颜色数据
    LCD_WriteData(color >> 8);
    LCD_WriteData(color & 0xFF);
}

为什么颜色也要拆成两个字节?因为大部分彩色LCD用的是RGB565格式,16位色。高5位是红色,中间6位是绿色,低5位是蓝色。你直接写一个16位数,控制器会懵的。

4.3 区域填充——批量操作的艺术

区域填充,就是给屏幕上的一块矩形区域涂上统一的颜色。这在做背景、按钮、进度条时特别常用。

实现思路很简单:

  1. 设置光标位置为矩形区域的左上角和右下角
  2. 循环写入 (宽度 × 高度) 个颜色数据

但这里有个性能关键点:循环写入时,是用一个for循环一个一个写,还是用DMA批量传输?

我个人建议,如果MCU支持DMA(直接存储器访问),一定要用。我曾经在一个项目里,用for循环填充一个320x240的全屏,花了将近200毫秒。改用DMA之后,直接降到20毫秒以内。用户体验完全不一样。

// 区域填充函数
void LCD_Fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
    uint32_t i, total;
    uint8_t colorH, colorL;

    // 计算总像素数
    total = (x2 - x1 + 1) * (y2 - y1 + 1);

    // 设置窗口区域
    LCD_SetCursor(x1, y1, x2, y2);

    // 拆分颜色
    colorH = color >> 8;
    colorL = color & 0xFF;

    // 连续写入颜色数据
    for (i = 0; i < total; i++)
    {
        LCD_WriteData(colorH);
        LCD_WriteData(colorL);
    }
}
避坑指南: 我曾经在填充大面积区域时,发现屏幕出现「撕裂」现象——上半部分和下半部分颜色不一样。后来排查发现,是因为写入速度太快,LCD控制器来不及处理。解决办法是在每次写入后加一个微小的延时,或者降低SPI时钟频率。具体多少合适?得根据你的屏和MCU来调,一般SPI时钟不要超过屏的额定频率。

4.4 清屏函数——一键恢复出厂设置

清屏,其实就是填充整个屏幕。它是最常用的函数之一,每次切换界面、刷新数据时都要用到。

实现方式有两种:

  • 调用区域填充函数:把整个屏幕当作一个矩形区域,传入(0,0)到(最大X,最大Y)以及背景色。
  • 专用清屏指令:有些LCD控制器有专门的清屏命令(比如SSD1306的0xAE/0xAF配合显存清零),速度更快。

我个人更推荐第一种方式,因为通用性强。你只要把区域填充函数写好了,清屏就是它的一个特例。

// 清屏函数
void LCD_Clear(uint16_t color)
{
    // 直接调用区域填充,覆盖整个屏幕
    LCD_Fill(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1, color);
}

就这么简单?对,就这么简单。但有个细节要注意:清屏的颜色通常用黑色(0x0000)或白色(0xFFFF),具体看你屏幕的底色。如果是做血糖仪,我建议用黑色背景,因为白色背景在强光下反光严重,老人看不清。

小技巧: 如果你发现清屏速度太慢,可以试试「局部清屏」。比如你只需要刷新数值区域,就不要清全屏。只清除那一个小矩形,然后重新绘制。这样画面不会闪烁,用户看着也舒服。

4.5 实战经验总结

好了,这一讲的内容就这些。咱们来捋一捋重点:

函数 核心作用 性能关键点
设置光标 定义操作窗口 注意坐标拆分和映射方式
画点 单个像素控制 连续画点时用窗口模式
区域填充 批量涂色 DMA传输可大幅提速
清屏 全屏重置 局部刷新避免闪烁

我记得刚开始做LCD驱动时,总觉得画点函数是最简单的,结果在区域填充上栽了跟头。后来才明白,嵌入式开发里,越是基础的操作,越要关注性能。因为上层所有的UI交互,都建立在这些底层函数之上。地基不稳,楼盖得再高也得塌。

下一讲,咱们会基于这些基础函数,开始画字符和数字。到时候你就知道,今天的「点」和「面」有多重要了。