14、位操作与图形学:用位运算实现像素点的设置与清除

图形学,听起来很高大上对吧?其实说白了,就是跟像素打交道。而像素,在底层就是一个个的位(bit)。

我刚开始接触嵌入式图形开发时,总觉得画点、清点这些操作很神秘。后来发现,用位运算来处理像素,简直是天作之合。今天我就带你看看,怎么用位操作来搞定像素点的设置与清除。

14.1 像素点的本质:一个位的故事

先想一个问题:一个像素点需要多少存储空间?

如果是黑白屏,一个像素只需要1个bit——0表示黑,1表示白。如果是灰度屏,可能需要4个bit(16级灰度)。如果是彩色屏,那就需要更多了,比如RGB565格式需要16个bit。

我们今天先聚焦在最简单的黑白屏场景。一个字节有8个bit,可以表示8个像素点。你想想看,一个128×64的单色OLED屏,总共需要多少字节?

128 × 64 ÷ 8 = 1024字节。刚好1KB。

这就是位操作的用武之地。我们要在1KB的内存里,精确地控制每一个像素点。

14.2 像素坐标到内存地址的映射

假设我们的屏幕是128像素宽、64像素高。每个字节管理8个像素(水平方向)。那么,坐标(x, y)对应的字节地址怎么算?

我习惯这样计算:

// 屏幕参数
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64

// 计算像素(x, y)所在的字节索引
uint16_t byte_index = y * (SCREEN_WIDTH / 8) + (x / 8);

// 计算该像素在字节中的位位置(0~7)
uint8_t bit_position = x % 8;

这里有个细节要注意:x / 8 得到的是该像素在第几个字节里,x % 8 得到的是该字节内的第几个位。

我的经验: 很多新手会搞混位顺序。有的屏幕是高位对应左边像素,有的是低位对应左边。一定要先看数据手册,或者写个测试程序验证一下。我曾经在这个坑里栽过跟头。

14.3 设置像素点:用位或运算

设置一个像素点,就是把对应的位变成1。怎么做?用位或(|)运算。

原理很简单:

  • 先把1左移到目标位置
  • 然后跟当前字节做位或
// 设置像素点(x, y)为白色(1)
void set_pixel(uint8_t *framebuffer, uint16_t x, uint16_t y) {
    uint16_t byte_index = y * (SCREEN_WIDTH / 8) + (x / 8);
    uint8_t bit_position = x % 8;
    
    // 1 << bit_position 生成一个只有目标位为1的掩码
    // 位或操作:目标位变成1,其他位保持不变
    framebuffer[byte_index] |= (1 << bit_position);
}

你看,核心代码就一行。这就是位操作的魅力——简洁、高效。

关键点: 1 << bit_position 这个表达式,生成的是一个「只有目标位为1,其他位都是0」的掩码。位或运算保证了只有目标位被置1,其他位不受影响。

14.4 清除像素点:用位与运算

清除像素点,就是把对应的位变成0。这次我们用位与(&)运算。

思路是这样的:

  • 先生成一个掩码:目标位为0,其他位为1
  • 然后跟当前字节做位与
// 清除像素点(x, y)(设置为黑色,即0)
void clear_pixel(uint8_t *framebuffer, uint16_t x, uint16_t y) {
    uint16_t byte_index = y * (SCREEN_WIDTH / 8) + (x / 8);
    uint8_t bit_position = x % 8;
    
    // ~(1 << bit_position) 生成一个只有目标位为0的掩码
    // 位与操作:目标位变成0,其他位保持不变
    framebuffer[byte_index] &= ~(1 << bit_position);
}

这里用到了取反运算符(~)。~(1 << bit_position) 把掩码翻转了——原来为1的位变成0,原来为0的位变成1。然后位与运算就把目标位清零了。

注意: 取反运算符(~)的优先级高于位与(&),所以 framebuffer[byte_index] &= ~(1 << bit_position); 不需要加括号。但为了代码可读性,我建议还是加上括号:framebuffer[byte_index] &= (~(1 << bit_position));

14.5 读取像素点:用位与运算

有时候我们还需要读取某个像素点的状态。这同样用位与运算:

// 读取像素点(x, y)的状态
// 返回1表示白色,0表示黑色
uint8_t get_pixel(uint8_t *framebuffer, uint16_t x, uint16_t y) {
    uint16_t byte_index = y * (SCREEN_WIDTH / 8) + (x / 8);
    uint8_t bit_position = x % 8;
    
    // 位与运算提取目标位,然后右移到最低位
    return (framebuffer[byte_index] & (1 << bit_position)) ? 1 : 0;
}

这里 framebuffer[byte_index] & (1 << bit_position) 的结果,要么是0(目标位为0),要么是 1 << bit_position(目标位为1)。用三元运算符转成0或1,方便后续判断。

14.6 批量操作:整行像素的填充

单个像素操作是基础。但在实际项目中,我们经常需要批量操作。比如画一条水平线,或者填充一个矩形。

如果逐像素调用 set_pixel,效率太低了。更好的做法是直接操作字节:

// 画一条水平线:从(x1, y)到(x2, y)
void draw_hline(uint8_t *framebuffer, uint16_t x1, uint16_t x2, uint16_t y) {
    uint16_t byte_start = y * (SCREEN_WIDTH / 8) + (x1 / 8);
    uint16_t byte_end   = y * (SCREEN_WIDTH / 8) + (x2 / 8);
    
    // 如果起点和终点在同一个字节内
    if (byte_start == byte_end) {
        uint8_t mask = (0xFF >> (x1 % 8)) & (0xFF << (7 - (x2 % 8)));
        framebuffer[byte_start] |= mask;
        return;
    }
    
    // 处理第一个字节(可能不是完整的8位)
    framebuffer[byte_start] |= (0xFF >> (x1 % 8));
    
    // 处理中间的完整字节
    for (uint16_t i = byte_start + 1; i < byte_end; i++) {
        framebuffer[i] = 0xFF;  // 整字节全置1
    }
    
    // 处理最后一个字节(可能不是完整的8位)
    framebuffer[byte_end] |= (0xFF << (7 - (x2 % 8)));
}

这段代码看起来复杂,其实逻辑很清晰:

  • 第一个字节:只设置从x1开始的位
  • 中间字节:全部置1
  • 最后一个字节:只设置到x2结束的位

这种批量操作,比逐像素调用快了不止一个数量级。

14.7 知识体系总览

下面这张图总结了像素点操作的核心逻辑:

像素点操作核心逻辑 坐标映射 (x, y) → byte_index + bit_pos 设置像素 |= (1 << bit_pos) 清除像素 &= ~(1 << bit_pos) 读取像素 & (1 << bit_pos) 批量操作 整行/整块填充 核心思想 用位掩码(bit mask)精确控制每一位,不干扰相邻像素 批量操作时,优先处理整字节,再处理边界不完整字节

14.8 实际项目中的避坑指南

我在实际项目中遇到过不少跟像素操作相关的问题,这里分享几个典型的:

我曾经踩过的坑:
  1. 位序搞反了——有的屏幕是MSB在左,有的是LSB在左。不确认清楚,画出来的图形是镜像的。
  2. 忘记考虑字节对齐——有些MCU对非对齐访问会触发异常。如果framebuffer的地址不是4字节对齐,某些优化写法可能会出问题。
  3. 批量操作时边界处理错误——画水平线时,第一个字节和最后一个字节的掩码计算最容易出错。我建议写个测试函数,画几条线然后打印出framebuffer内容来验证。
我的调试技巧: 写一个 dump_framebuffer() 函数,把framebuffer的内容以十六进制打印出来。然后用肉眼检查每个字节的位模式是否正确。虽然原始,但非常有效。

14.9 性能优化建议

如果你在资源受限的MCU上做图形开发,性能优化很重要。这里有几个方向:

优化方向 具体做法 效果
减少函数调用 将set_pixel/clear_pixel定义为内联函数或宏 减少调用开销,适合频繁调用场景
批量操作 用整字节操作代替逐像素操作 速度提升5~10倍
查表法 预计算常用掩码,用查表代替移位运算 减少CPU计算时间
DMA传输 用DMA将framebuffer刷到屏幕 释放CPU,提升整体性能

我个人最推荐的是「批量操作」这个方向。因为它的优化效果最明显,而且代码逻辑清晰,不容易出错。

好了,关于像素点的设置与清除,就讲到这里。位操作在图形学中的应用远不止这些,后面我们还会聊到更高级的话题,比如图形裁剪、字体渲染等等。但今天这些基础操作,是后续所有内容的地基。

记住一句话:图形学的底层,就是位操作的天下。


公众号:蓝海资料掘金营,微信deep3321