1. 项目概述:为什么选择OLED显示图片?

大家好,今天我想和大家分享一个非常实用的嵌入式项目——如何在OLED屏幕上显示自定义图片。这个项目看似简单,但涉及了Python图像处理、C语言编程、STM32硬件驱动等多个技术领域,非常适合嵌入式初学者理解软硬件协同工作的原理。

OLED屏幕因其高对比度、低功耗和快速响应等特点,在嵌入式设备中广泛应用。0.96寸的OLED模块价格便宜,接口简单(通常使用I2C通信),非常适合DIY项目和产品原型开发。通过这个项目,你不仅能学会如何驱动OLED屏幕,还能掌握将任意图片转换为单片机可识别的数据格式的方法。

我在实际项目中多次使用这种技术,比如为智能家居设备设计状态显示界面,或者为小型机器人添加视觉反馈。虽然市面上有很多现成的图形库,但自己动手实现一遍会让你对底层原理有更深刻的理解。下面我就带你一步步完成这个有趣的项目。

2. 硬件准备与连接

2.1 所需硬件清单

要完成这个项目,你需要准备以下硬件设备:

  • 0.96寸OLED显示屏:选择I2C接口的版本,通常有4个引脚(VCC、GND、SCL、SDA)。这种屏幕分辨率一般是128x64,只能显示黑白两色,但正是这种简单性让它非常适合初学者。
  • STM32F103C8T6最小系统板:这是STM32系列中非常流行的型号,价格便宜且资源丰富。当然,你也可以使用其他单片机,只要它能运行基本的点灯程序并支持I2C通信。
  • 连接线杜邦线:用于连接OLED和单片机。如果有一个底板或者面包板会更方便,但不是必须的。
  • ST-Link下载器:用于将程序烧录到STM32单片机中。如果你使用的是其他开发板,可能需要对应的下载工具。

我在最初尝试时曾经因为连接线接触不良浪费了很多时间,所以建议你使用质量好一点的杜邦线,并确保连接牢固。硬件连接其实很简单:将OLED的VCC接3.3V,GND接地,SCL接PB8,SDA接PB9。如果你用的是其他单片机,只需要找到对应的I2C引脚即可。

2.2 硬件连接详解

硬件连接是项目的基础,如果连接错误,后面的所有工作都无法进行。STM32F103C8T6的I2C1接口通常对应PB6(SCL)和PB7(SDA),但也可以重映射到PB8和PB9。我更喜欢使用重映射后的引脚,因为它们通常不会与其他功能冲突。

连接时一定要注意电压匹配!大多数OLED模块的工作电压是3.3V,而STM32F103的IO口也是3.3V电平,所以直接连接就可以。如果你使用的是5V单片机,可能需要电平转换电路,否则可能会损坏OLED模块。这是我曾经踩过的坑,希望你不要重蹈覆辙。

电源方面,STM32F103C8T6可以通过USB供电,或者通过3.3V稳压芯片供电。OLED的功耗很低,通常不超过20mA,所以可以直接从STM32的3.3V引脚取电。但如果你的项目中有其他外设,最好单独为OLED供电,避免电流不足导致显示异常。

3. 图像处理:从PNG到C数组

3.1 准备源图像

首先你需要准备一张想要显示的图片。由于OLED屏幕的分辨率是128x64,而且只能显示黑白两色,所以源图像最好也是这个尺寸的二值图像(只有纯黑和纯白)。你可以使用任何图像编辑软件(如Photoshop、GIMP甚至Windows画图)来创建或编辑图像。

我建议使用简单的图形或图标,而不是复杂的照片,因为单色显示对细节的表现有限。文字也是一个不错的选择,但要注意字体大小至少为8像素高,否则可能无法清晰显示。保存图像时,建议使用PNG格式,因为它是无损压缩,不会引入额外的噪点或失真。

在实际项目中,我经常先设计一个128x64的草图,明确哪些区域需要黑色(点亮),哪些区域需要白色(熄灭)。这种前期规划可以避免后续反复修改图像,节省很多时间。如果你没有设计灵感,可以从简单的表情符号、Logo或者进度条开始。

3.2 Python图像处理脚本

接下来是最关键的一步:使用Python将图像转换为C语言数组。你需要安装Python和Pillow库(Python图像处理库)。安装Pillow很简单,只需在命令行中运行pip install Pillow即可。

下面是完整的Python脚本,我会逐行解释其工作原理:

from PIL import Image

# 打开图像文件
img = Image.open("D:\\path\\to\\your\\image.png")

# 创建输出文件
f = open("D:\\path\\to\\output\\pic.h", "w")
f.write("#include <stdint.h>\n")
f.write("uint8_t pic[8][128] = {")

# 创建空列表存储像素数据
pixel_data = []

# 遍历图像中的每个像素
for y in range(64):  # 高度方向
    line = []
    for x in range(128):  # 宽度方向
        # 获取当前像素值
        pixel = img.getpixel((x, y))
        
        # 判断像素亮度,转换为1或0
        if pixel[0] < 128:  # 如果是暗色
            line.append(0)
        else:  # 如果是亮色
            line.append(1)
    pixel_data.append(line)

# 将像素数据转换为OLED所需的字节格式
for page in range(8):  # OLED分8页,每页8行
    f.write("{")
    for col in range(128):  # 每页128列
        byte_value = 0
        for bit in range(8):  # 每字节8位
            if pixel_data[page * 8 + bit][col] == 1:
                byte_value |= (1 << bit)
        f.write(str(byte_value))
        if col != 127:
            f.write(",")
    f.write("}")
    if page != 7:
        f.write(",")

f.write("};")
f.close()

这个脚本的工作原理是:首先读取图像中的每个像素,根据亮度将其转换为0或1(0表示熄灭,1表示点亮)。然后按照OLED的寻址方式,将每8行像素组合成一个字节(垂直方向),最终生成一个8x128的二维数组,每个元素是一个字节,对应屏幕上的一个8像素高的列块。

我最初理解这个转换过程花了一些时间,但一旦明白了OLED的寻址机制,就会发现这种设计很巧妙。OLED屏幕将64行分成8页,每页8行。当你向某个地址写入数据时,实际上是在设置该列当前位置开始的8个像素的状态。

4. STM32开发环境搭建

4.1 安装必要的软件工具

为了在STM32上开发程序,你需要搭建相应的开发环境。我推荐使用STM32CubeMX和Keil MDK-ARM的组合,这是目前最流行的STM32开发工具链。

首先下载并安装STM32CubeMX,这是一个图形化配置工具,可以帮你生成初始化代码。然后安装Keil MDK-ARM,这是ARM内核单片机的集成开发环境(IDE)。这两个软件都有免费版本,对于学习和个人项目来说足够了。

安装过程中可能会遇到一些坑,比如需要注册账号、申请license等。我建议跟着网上详细的教程一步步操作,不要跳过任何步骤。有时候安装路径中有中文或特殊字符也会导致问题,所以最好使用默认的安装路径。

4.2 创建STM32工程

打开STM32CubeMX,选择STM32F103C8T6型号。首先配置时钟源:在RCC选项中,将HSE设置为Crystal/Ceramic Resonator。然后配置I2C接口:找到I2C1,选择I2C模式,如果你使用PB8和PB9作为SCL和SDA,需要先激活相应的重映射功能。

接下来配置GPIO引脚:找到PB8和PB9,将它们设置为GPIO_Output模式(虽然I2C功能会自动配置引脚,但 explicit设置可以避免 confusion)。在Clock Configuration选项卡中,设置系统时钟为72MHz,这是STM32F103的最高运行频率。

在Project Manager选项卡中,设置项目名称和路径,选择MDK-ARM作为Toolchain/IDE。点击Generate Code按钮生成初始化代码。第一次生成代码时会提示安装相应的软件包,同意并等待安装完成。

5. OLED驱动与代码集成

5.1 添加OLED驱动程序

生成了基础工程后,你需要添加OLED的驱动程序。网上有很多开源的OLED驱动代码,我推荐使用经过广泛测试的版本,比如江协科技提供的驱动。这些驱动通常包含OLED.c和OLED.h两个文件,提供了初始化和基本绘图功能。

将驱动文件复制到你的工程目录中,然后在Keil中右键点击项目,选择"Add Existing Files to Group",添加OLED.c文件。接着需要配置驱动代码以适应你的硬件连接:打开OLED.c文件,找到I2C写数据的函数,确保它使用正确的I2C接口和引脚。

驱动中通常会有几个关键函数:

  • OLED_Init():初始化OLED屏幕,设置参数和通信协议
  • OLED_SetCursor():设置写入位置(页和列)
  • OLED_WriteData():向当前地址写入数据
  • OLED_Clear():清空屏幕

我建议在添加驱动后先编写一个简单的测试程序,比如显示一些文字或绘制基本图形,确保驱动工作正常后再进行图像显示。这样可以排除硬件连接或驱动配置的问题。

5.2 集成图像数据并显示

将Python生成的pic.h文件添加到工程中,在main.c文件中包含必要的头文件:

#include "OLED.h"
#include "pic.h"

在主函数的初始化部分,添加OLED初始化和图像显示代码:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    OLED_Init();  // 初始化OLED
    
    // 显示图像
    for(uint8_t page = 0; page < 8; page++) {
        for(uint8_t col = 0; col < 128; col++) {
            OLED_SetCursor(page, col);
            OLED_WriteData(pic[page][col]);
        }
    }
    
    while(1) {
        // 主循环
    }
}

这段代码遍历图像数组中的每个字节,将其写入OLED的相应位置。由于OLED的写入操作相对较慢,你可以考虑使用DMA传输来提高效率,但对于静态图像显示,直接写入已经足够了。

我在实际项目中发现,有时候图像显示会出现错位或乱码,这通常是因为生成图像数组时出现了问题。建议在生成数组后检查一下数据是否合理:数组应该有8行128列,每个元素的值应该在0-255之间。如果某个值明显超出这个范围,很可能是图像处理脚本有问题。

6. 项目编译与调试

6.1 编译工程与解决常见错误

点击Keil中的Build按钮编译工程。第一次编译可能会遇到一些错误,最常见的是头文件路径问题。如果提示找不到OLED.h或pic.h,需要在Options for Target中的C/C++选项卡的Include Paths中添加头文件所在的目录。

另一个常见错误是链接错误,提示某些函数未定义。这通常是因为没有将对应的源文件添加到工程中,或者没有包含必要的库文件。STM32CubeMX生成的代码已经包含了标准外设库,但如果你使用了其他功能,可能需要手动添加库文件。

编译成功后,点击Download按钮将程序烧录到STM32中。确保ST-Link连接正确,单片机已供电。如果下载失败,检查ST-Link驱动是否安装,连接线是否完好,以及单片机是否处于复位状态。

6.2 调试技巧与性能优化

程序下载成功后,按复位键运行。如果OLED没有显示预期图像,首先检查电源指示灯是否亮起,然后使用逻辑分析仪或示波器检查I2C信号是否正常。如果没有这些工具,可以尝试简化问题:先确保OLED能显示简单文字,再尝试显示图像。

如果图像显示有问题,可能是数据写入顺序错误。OLED的寻址方式有点特别:它按页存储数据,每页8行,数据字节的LSB对应页的上部,MSB对应下部。如果你发现图像上下颠倒,可能需要在图像处理脚本中调整比特顺序。

为了提高显示速度,可以考虑以下优化:

  • 使用硬件I2C而不是软件模拟
  • 采用DMA传输减少CPU开销
  • 只更新屏幕上变化的部分而不是全屏刷新
  • 使用垂直寻址模式(如果OLED支持)来提高连续写入速度

我在一个实际项目中需要频繁更新OLED显示,最初使用软件I2C每秒只能更新几次屏幕,后来切换到硬件I2C加DMA后,刷新率提高了十倍以上。这种优化对于动态显示或动画非常重要。

7. 进阶应用与扩展思路

掌握了基本图像显示后,你可以尝试更复杂的应用。比如创建多帧动画:预先转换多张图像,按顺序显示它们形成动画效果。你可以使用Python脚本批量处理图像序列,生成一个三维数组,然后在STM32上实现简单的动画播放器。

另一个有趣的应用是实时生成图像。STM32的计算能力虽然有限,但足以生成一些简单的图形,如波形图、进度条或文本菜单。你可以结合传感器数据,实时更新显示内容,创建一个完整的用户界面。

如果你觉得128x64分辨率太低,可以考虑使用更大尺寸的OLED屏幕,如1.3寸的256x64屏幕或更高分辨率的版本。这些屏幕的驱动原理类似,但可能需要调整初始化参数和数据传输方式。

对于更复杂的图形应用,可以考虑使用嵌入式图形库,如u8g2、LVGL等。这些库提供了丰富的绘图功能和控件,但需要更多的存储空间和计算资源。STM32F103C8T6的Flash和RAM有限,可能无法运行完整的图形库,但更高端的STM32型号可以胜任。

最后,不要局限于I2C接口。很多OLED屏幕还支持SPI接口,SPI的传输速度比I2C快得多,适合更高刷新率的应用。切换接口只需要修改驱动代码中的底层通信函数,上层应用代码可以保持不变。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐