4、像素操作与画点:设置光标位置、画点函数、区域填充、清屏函数
好,咱们进入第四讲。这一讲可以说是整个LCD驱动开发的地基——像素操作。你想想看,屏幕上所有的文字、图标、波形,归根结底都是一个个像素点拼出来的。所以,怎么控制这些点,怎么高效地填充区域,怎么一键清屏,这些基本功必须扎实。
我个人习惯把这一章叫做「点线面的艺术」。因为从画一个点开始,到画一条线,再到填充一个面,逻辑是一脉相承的。今天咱们就把「点」和「面」搞定。
4.1 设置光标位置——指哪打哪
在画任何东西之前,你得先告诉屏幕:「我要在哪个位置动手」。这就是设置光标位置(SetCursor)的作用。
大多数LCD控制器(比如常见的SSD1306、ST7735、ILI9341)内部都有一个「写指针」。你往这个指针指向的地址写数据,像素就显示在那里。写完之后,指针会自动+1,指向下一个位置。
设置光标位置的函数,通常需要两个参数:X坐标和Y坐标。但这里有个坑——不同控制器的坐标系统不一样。有的以左上角为(0,0),X向右,Y向下;有的则是列和页(Page)的概念。
下面是一个典型的设置光标位置函数,以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 画点函数——最原子的操作
画点,说白了就是往指定位置写一个像素的颜色值。它是所有绘图操作的基础。
画点函数的逻辑很简单:
- 设置光标位置到目标点
- 写入该点的颜色数据
但这里有个性能问题。你想想看,每画一个点就要发送好几条指令(设置列、设置行、开始写入),如果画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 区域填充——批量操作的艺术
区域填充,就是给屏幕上的一块矩形区域涂上统一的颜色。这在做背景、按钮、进度条时特别常用。
实现思路很简单:
- 设置光标位置为矩形区域的左上角和右下角
- 循环写入 (宽度 × 高度) 个颜色数据
但这里有个性能关键点:循环写入时,是用一个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);
}
}
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交互,都建立在这些底层函数之上。地基不稳,楼盖得再高也得塌。
下一讲,咱们会基于这些基础函数,开始画字符和数字。到时候你就知道,今天的「点」和「面」有多重要了。