4、像素操作与画点:设置单个像素点、区域像素填充、反色与滚动效果

好,咱们进入第四讲。这一章可以说是整个OLED驱动里最基础、也最核心的部分。你想想看,屏幕上显示的任何东西——文字、图标、波形、曲线——归根结底,都是由一个个像素点组成的。说白了,你学会了怎么控制一个像素点,就等于掌握了整个屏幕的「生杀大权」。

我个人习惯把像素操作比作「在网格纸上涂格子」。OLED屏幕就是一张密密麻麻的网格纸,每个格子要么亮(白色),要么不亮(黑色)。我们的任务,就是精确地告诉屏幕:哪个格子该亮,哪个不该亮。

4.1 像素点的坐标体系

先搞清楚坐标。我用的这块0.96寸OLED,分辨率是128×64。什么意思?横向128个像素点,纵向64个像素点。

坐标原点(0,0)在左上角。X轴向右增加,Y轴向下增加。这一点和很多图形库不一样,但OLED驱动芯片(比如SSD1306)就是这么定义的。我第一次用的时候也犯过嘀咕,后来习惯了反而觉得顺手。

方向 范围 说明
X轴(列) 0 ~ 127 从左到右
Y轴(行) 0 ~ 63 从上到下
注意: 我曾经在项目里犯过一个低级错误——把Y轴坐标写反了。结果画出来的波形图上下颠倒,排查了半天才发现是坐标搞反了。所以,写代码前先确认一下你的坐标系方向。

4.2 设置单个像素点

这是最基础的操作。SSD1306内部把屏幕分成了8页(Page),每页8个像素点。为什么这么设计?因为它的数据总线是8位的,一次可以操作8个像素点。

画一个像素点的流程是这样的:

  1. 计算这个像素点属于哪一页(Page = Y / 8)
  2. 计算在这一页内的偏移位置(Bit = Y % 8)
  3. 读取当前页的整字节数据
  4. 修改对应的位(置1或清0)
  5. 把修改后的字节写回去

听起来有点绕?看代码就清楚了:

// 设置单个像素点
// x: 0-127, y: 0-63, color: 1=亮, 0=灭
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
    if (x >= 128 || y >= 64) return;  // 超出范围,直接返回
    
    uint8_t page = y / 8;      // 计算页号
    uint8_t bit = y % 8;       // 计算位偏移
    uint8_t byte_val;
    
    // 读取当前页的字节
    byte_val = OLED_Buffer[page][x];
    
    if (color) {
        byte_val |= (1 << bit);   // 置1:点亮像素
    } else {
        byte_val &= ~(1 << bit);  // 清0:熄灭像素
    }
    
    OLED_Buffer[page][x] = byte_val;
}

这里有个关键点:我用了缓冲区(OLED_Buffer)。为什么不直接写屏幕?因为SSD1306的读写速度有限,频繁操作屏幕会拖慢系统。我的做法是先在内存里画好,然后一次性刷新到屏幕。这个技巧在嵌入式开发里很常用,叫「双缓冲」。

小技巧: 如果你需要频繁画点(比如画波形),建议先操作缓冲区,最后调用一次刷新函数。我在做血压计脉搏波形图时,就是这么干的,刷新率能到30帧以上。

4.3 区域像素填充

单个像素点画起来太慢了。实际项目中,我们经常需要填充一个矩形区域。比如血压计里的状态栏背景、数值显示框等。

区域填充的思路很简单:循环遍历指定范围内的所有像素点,逐个设置颜色。但要注意效率。

// 填充矩形区域
// x1,y1: 左上角坐标, x2,y2: 右下角坐标
void OLED_FillRect(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t color) {
    uint8_t x, y;
    
    // 边界检查
    if (x1 > x2) swap(&x1, &x2);
    if (y1 > y2) swap(&y1, &y2);
    
    for (y = y1; y <= y2; y++) {
        for (x = x1; x <= x2; x++) {
            OLED_DrawPixel(x, y, color);
        }
    }
}

嗯,这里要注意。上面的代码虽然能用,但效率不高。为什么?因为每个像素点都要做一次页计算和位操作。我优化过一版,按页来填充,一次操作8个像素点,速度能快8倍。

优化思路: 对于整页(8个像素点)的填充,可以直接操作字节。比如把一整行都设为0xFF(全亮)或0x00(全灭),比逐像素操作快得多。

4.4 反色效果

反色,就是把屏幕上的颜色反转。白的变黑,黑的变白。这个效果在血压计里很有用——比如高亮显示当前测量的数值,或者做闪烁提示。

实现反色有两种方式:

  1. 全局反色: 直接操作SSD1306的寄存器,整个屏幕反转。速度快,但不够灵活。
  2. 局部反色: 对缓冲区中的指定区域进行取反操作。灵活,但需要自己实现。

我一般用局部反色,因为可以精确控制哪些区域反色:

// 局部反色:对指定矩形区域取反
void OLED_InvertRect(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) {
    uint8_t x, y;
    
    for (y = y1; y <= y2; y++) {
        for (x = x1; x <= x2; x++) {
            uint8_t page = y / 8;
            uint8_t bit = y % 8;
            
            // 取反操作:异或1
            OLED_Buffer[page][x] ^= (1 << bit);
        }
    }
}

看到那个 ^= 了吗?异或操作是反色的精髓。1变0,0变1,干净利落。

避坑指南: 我曾经在做一个闪烁效果时,直接用延时+反色来做。结果发现闪烁频率不稳定,因为反色操作本身也需要时间。后来我改用定时器+状态机,才解决了这个问题。所以,涉及到时间相关的效果,别用延时,用定时器。

4.5 滚动效果

滚动效果在血压计里很常见——比如测量结果太长,一屏显示不下,就需要上下滚动查看。

SSD1306内置了硬件滚动功能,但我个人不太喜欢用。为什么?因为硬件滚动的方向和速度是固定的,不够灵活。我更喜欢用软件滚动,想怎么滚就怎么滚。

软件滚动的核心思想:移动缓冲区中的数据。比如向上滚动一行,就是把第1行的数据复制到第0行,第2行复制到第1行,以此类推,最后一行清空。

// 向上滚动一行
void OLED_ScrollUp(void) {
    uint8_t page, x;
    
    // 从第1页开始,把每一页的数据复制到上一页
    for (page = 1; page < 8; page++) {
        for (x = 0; x < 128; x++) {
            OLED_Buffer[page - 1][x] = OLED_Buffer[page][x];
        }
    }
    
    // 最后一行清空
    for (x = 0; x < 128; x++) {
        OLED_Buffer[7][x] = 0x00;
    }
    
    // 刷新屏幕
    OLED_Refresh();
}

你看,代码其实不复杂。但要注意一点:滚动后要立即刷新屏幕,否则用户看不到变化。

经验之谈: 我做血压计的历史记录界面时,用了滚动效果。一开始是逐像素滚动,发现太慢了。后来改成逐行滚动,速度刚好。所以,滚动粒度要根据实际需求调整——内容重要时用细粒度,快速浏览时用粗粒度。

4.6 综合应用:血压计数值闪烁提示

最后,咱们把今天学的知识串起来,做一个血压计里常用的效果——数值闪烁提示。比如当测量值超过正常范围时,让数值闪烁,提醒用户注意。

// 数值闪烁效果
void OLED_FlashValue(uint8_t x, uint8_t y, uint16_t value, uint8_t flash_count) {
    uint8_t i;
    
    for (i = 0; i < flash_count; i++) {
        // 显示数值(正常颜色)
        OLED_ShowNum(x, y, value, 3);
        OLED_Refresh();
        delay_ms(300);
        
        // 反色区域(让数值闪烁)
        OLED_InvertRect(x, y, x + 24, y + 16);
        OLED_Refresh();
        delay_ms(300);
        
        // 再反色回来
        OLED_InvertRect(x, y, x + 24, y + 16);
    }
}

这个例子里,我们用到了画点(显示数字本质就是画点)、区域填充(数字的背景)、反色(闪烁效果)。你看,基础操作组合起来,就能做出实用的功能。

好了,这一章的内容就到这里。像素操作是OLED驱动的基石,一定要亲手写代码试试。下一章,我们会讲如何用这些基础操作来绘制字符和汉字——这可是做血压计界面的关键技能。