1. 从零开始:为什么选择树莓派和OLED屏幕?

如果你手头有一块树莓派4B,正琢磨着用它做点有趣的小项目,但又觉得直接操作Linux系统或者写复杂的网络应用有点门槛,那么从硬件入手,点亮一块小小的OLED屏幕,绝对是一个成就感爆棚的起点。我自己刚开始玩树莓派的时候,也是从驱动各种小屏幕、小传感器开始的,那种几行代码就让硬件“活”过来的感觉,特别能激发继续探索的兴趣。今天,我就带你用最通俗的方式,搞定树莓派4B通过I2C协议驱动0.96寸OLED屏幕这件事。

简单来说,这就像给你的树莓派装上一个迷你显示屏。它不像HDMI接口的大显示器那样需要复杂的配置和驱动,而是通过两根线(没错,就两根!)和树莓派“说悄悄话”,告诉它该显示什么。这个“悄悄话”的协议,就是我们今天的主角——I2C。你可能会问,这有什么用?用处可大了。你可以用它来显示系统状态(比如CPU温度、IP地址)、做一个迷你天气预报站、或者作为智能家居控制面板的显示部分。它体积小、功耗低,非常适合嵌入式项目。

那么,为什么是I2C呢?在树莓派的世界里,和外设通信有好几种方式,比如SPI、UART。I2C最大的优点就是“省线”。它只需要两根线(一根时钟线SCL,一根数据线SDA),就能连接多个设备,特别适合树莓派这种GPIO引脚数量有限的板子。对于0.96寸OLED这种常见的小屏,I2C驱动方案非常成熟,网上资料多,库函数丰富,对新手极其友好。所以,无论你是想入门嵌入式开发的学生,还是想给智能家居项目加个显示的爱好者,这个实战指南都能让你快速上手。

2. 硬件准备与连接:别让线接错了

万事开头难,但硬件连接这一步,我们争取让它变得最简单。你需要准备的东西不多:一块树莓派4B(其他型号如3B+也基本通用)、一块0.96寸I2C接口的OLED屏幕(通常是SSD1306驱动芯片)、几根母对母的杜邦线。这里有个小坑我踩过:市面上有些OLED屏的引脚顺序可能略有不同,最常见的是四针的:VCC、GND、SCL、SDA。拿到屏幕后,最好先看一眼产品说明或者PCB板上的丝印。

现在,我们来接线。记住一个原则:电源线(VCC和GND)绝对不能接错,信号线(SDA和SCL)接对了才能通信。对于树莓派4B,我们通常使用其物理引脚编号为3和5的那一组I2C接口,它们对应的功能就是SDA和SCL。具体连接如下:

  • OLED的VCC -> 树莓派的Pin 2 (5V)。这里注意,有些OLED模块工作电压是3.3V,接5V可能会烧坏!一定要确认你的屏幕支持5V输入。大多数常见的0.96寸屏是支持3.3V-5V宽电压的,接5V没问题,亮度还更高。如果不确定,保险起见接在树莓派的Pin 1 (3.3V)上。
  • OLED的GND -> 树莓派的Pin 6 (GND)。接地,这个没得商量,必须接。
  • OLED的SDA -> 树莓派的Pin 3 (SDA1)。这是数据线。
  • OLED的SCL -> 树莓派的Pin 5 (SCL1)。这是时钟线。

接好之后,最好检查两遍。硬件连接是基础,这里错了,后面软件怎么调都没用。我遇到过好几次屏幕不亮,最后发现是GND线虚接了。接好后,就可以给树莓派上电了。

2.1 确认I2C接口是否就绪

线接好了,我们得先确认树莓派系统本身已经准备好了I2C这个“聊天工具”。树莓派的Raspbian系统默认可能没有开启I2C接口,我们需要手动开启它。有两种主流方法,图形界面和命令行,任选其一就行。

如果你用的是带桌面的完整版系统,方法很简单:点击左上角的树莓派图标 -> Preferences -> Raspberry Pi Configuration。在弹出的窗口里,切换到 Interfaces 标签页,找到 I2C 这一项,选择 Enable,然后点击 OK。系统会提示你需要重启,照做就行。

我更习惯用命令行,因为更快捷,而且对于用SSH远程操作树莓派的朋友来说,这是唯一的方法。打开终端,输入下面这个命令:

sudo raspi-config

你会看到一个蓝色的配置界面。用键盘上下键移动到 3 Interface Options,回车。然后再选择 I5 I2C,回车。系统会问你是否要启用ARM I2C接口,果断选择 <Yes> 再回车。接着可能会有一个关于是否自动加载内核模块的提示,同样选是。最后按几次 Tab 键让光标移动到 Finish 上,回车退出。它也会问你是否重启,选择重启。等树莓派重新启动,I2C功能就激活了。

3. 软件环境搭建:安装必要的工具和库

硬件通道打通了,我们还需要软件上的“翻译官”和“侦察兵”。翻译官就是编程库,它把我们要显示的文字、图片翻译成屏幕能懂的I2C命令。侦察兵就是检测工具,帮我们看看屏幕是否真的挂载到了I2C总线上,以及它的“门牌号”(设备地址)是多少。

首先,我们来安装“侦察兵” i2c-tools。打开终端,输入:

sudo apt update
sudo apt install i2c-tools -y

这个工具包里的 i2cdetect 命令非常有用。安装完成后,我们先看看系统识别到了几个I2C总线:

sudo i2cdetect -l

你会看到类似 i2c-1 这样的输出,这表示I2C-1总线可用。接下来,我们用这条总线来探测设备:

sudo i2cdetect -y 1

注意,如果你的树莓派是老型号,可能用的是 i2c-0,把上面的 1 换成 0 试试。执行这个命令后,你会看到一个表格。如果一切正常,你应该能在表格的某一格(例如 3c)看到一个数字,这就是你的OLED屏幕的I2C设备地址。最常见的是 0x3C,也有些是 0x3D。记下这个地址,后面编程要用。如果表格全是横线(--),那就要回头检查硬件连接和I2C接口是否真的启用了。

3.1 选择你的编程武器:C语言还是Python?

接下来是“翻译官”的部分。你可以选择用C语言,也可以选择用Python。两者各有优劣,我来简单分析一下。

C语言(使用wiringPi库):优点是执行效率高,直接操作硬件,适合对性能有要求或者想深入了解底层通信过程的项目。wiringPi库的I2C函数用起来和单片机编程很像,对于有嵌入式背景的朋友会很亲切。但缺点是需要编译,步骤稍多。

Python(使用Adafruit或luma.oled库):优点是脚本语言,写起来快,改起来方便,不需要编译,库函数封装得非常高级,几行代码就能显示文字和图形,对新手极度友好。缺点是运行效率相对C语言低一些,但对于显示屏幕这种应用,完全够用,你根本感觉不到差别。

我个人的建议是,如果你是纯粹的新手,想最快看到效果,强烈推荐从Python开始。它的快速反馈能给你巨大的信心。下面,我将分别详细介绍这两种方式,你可以根据自己的喜好和项目需求来选择。

4. 实战编程:用C语言驱动OLED(wiringPi方案)

如果你决定挑战一下C语言,那我们就用wiringPi这个经典的库。首先确保安装了它:

sudo apt install wiringpi -y

安装后,可以用 gpio -v 和 gpio readall 命令验证一下,顺便再看看我们之前提到的SDA和SCL引脚对应的wiringPi编号。

C语言编程的核心,就是把原来在单片机上写的OLED驱动代码,里面的I2C底层读写函数,替换成wiringPi提供的函数。主要用到的就三个函数:

  1. wiringPiI2CSetup(int devId):初始化I2C通信,参数 devId 就是我们刚才用 i2cdetect 查到的设备地址(比如0x3c)。这个函数会返回一个文件描述符 fd,后续操作都要用它。
  2. wiringPiI2CWriteReg8(int fd, int reg, int data):向屏幕的某个寄存器 reg 写入一个字节的数据 data。在OLED驱动中,我们通常用 0x00 表示命令寄存器,0x40 表示数据寄存器。
  3. wiringPiI2CReadReg8(int fd, int reg):从寄存器读一个字节。对于单纯的显示,读操作用得比较少。

我在这里给你一个最精简、最核心的代码框架,去掉了复杂的取模和图形显示,只专注于“点亮并显示文字”这个基本目标。你可以在树莓派上创建一个新文件,比如叫 oled_simple.c,然后把下面的代码粘贴进去。

#include <stdio.h>
#include <wiringPi.h>
#include <wiringPiI2C.h>

int fd; // I2C设备描述符

// 向OLED发送命令
void WriteCmd(unsigned char cmd) {
    wiringPiI2CWriteReg8(fd, 0x00, cmd); // 0x00是命令寄存器地址
}

// 向OLED发送数据
void WriteData(unsigned char dat) {
    wiringPiI2CWriteReg8(fd, 0x40, dat); // 0x40是数据寄存器地址
}

// OLED初始化序列
void OLED_Init(void) {
    delay(50); // 上电延时
    WriteCmd(0xAE); // 关闭显示
    WriteCmd(0xD5); // 设置显示时钟分频比/振荡器频率
    WriteCmd(0x80);
    WriteCmd(0xA8); // 设置多路复用率
    WriteCmd(0x3F); // 对应128x64的屏幕
    WriteCmd(0xD3); // 设置显示偏移
    WriteCmd(0x00);
    WriteCmd(0x40); // 设置显示开始行
    WriteCmd(0x8D); // 电荷泵设置
    WriteCmd(0x14); // 开启电荷泵
    WriteCmd(0x20); // 设置内存地址模式
    WriteCmd(0x00); // 水平地址模式
    WriteCmd(0xA1); // 段重映射设置 (0xA1左右反置,0xA0正常)
    WriteCmd(0xC8); // 扫描方向设置 (0xC8上下反置,0xC0正常)
    WriteCmd(0xDA); // 设置COM引脚硬件配置
    WriteCmd(0x12);
    WriteCmd(0x81); // 设置对比度控制
    WriteCmd(0xCF); // 对比度值
    WriteCmd(0xD9); // 设置预充电周期
    WriteCmd(0xF1);
    WriteCmd(0xDB); // 设置VCOMH电压倍率
    WriteCmd(0x40);
    WriteCmd(0xA4); // 全局显示开启
    WriteCmd(0xA6); // 设置正常显示 (0xA7为反色显示)
    WriteCmd(0xAF); // 开启OLED显示
}

// 清屏函数
void OLED_Clear(void) {
    unsigned char i, n;
    for (i = 0; i < 8; i++) {
        WriteCmd(0xB0 + i); // 设置页地址
        WriteCmd(0x00);      // 设置列地址低四位
        WriteCmd(0x10);      // 设置列地址高四位
        for (n = 0; n < 128; n++) {
            WriteData(0x00); // 写入0,清空该点
        }
    }
}

// 在指定位置显示一个字符(使用内置的ASCII字模,这里简化)
// 实际项目需要包含一个完整的字模数组
void OLED_ShowChar(unsigned char x, unsigned char y, char chr) {
    // 这里需要字模数组,例如:unsigned char Font8x16[][16] = {...};
    // 根据chr算出字模索引,然后循环WriteData
    // 此处为示意,略去具体字模数据
    printf("Show char at (%d, %d): %c\n", x, y, chr);
}

int main() {
    // 初始化wiringPi
    if (wiringPiSetup() < 0) {
        printf("wiringPi setup failed!\n");
        return -1;
    }

    // 初始化I2C,地址设为0x3c
    fd = wiringPiI2CSetup(0x3c);
    if (fd < 0) {
        printf("I2C setup failed! Check connection and address.\n");
        return -1;
    }
    printf("I2C init success, fd = %d\n", fd);

    // 初始化OLED
    OLED_Init();
    OLED_Clear();
    printf("OLED Init & Clear Done.\n");

    // 这里可以调用OLED_ShowChar等函数显示内容
    // 例如:OLED_ShowChar(0, 0, 'H'); OLED_ShowChar(8, 0, 'i');

    // 保持程序运行
    while (1) {
        delay(1000);
        // 可以在这里实现动态显示效果
    }
    return 0;
}

保存代码后,在终端里进入代码所在目录,编译它:

gcc -o oled_simple oled_simple.c -lwiringPi

如果编译没有报错,就会生成一个叫 oled_simple 的可执行文件。运行它需要sudo权限:

sudo ./oled_simple

如果屏幕被成功初始化并清屏(变全黑),那么恭喜你,最难的底层通信部分已经打通了!剩下的工作就是丰富字模库,实现真正的文字和图形显示函数。你可以网上搜索“SSD1306字模”找到现成的C语言数组,或者用PCtoLCD2005这类软件自己取模。

5. 更快捷的路径:用Python轻松驾驭OLED

如果你觉得C语言那一套编译、链接有点麻烦,那么Python会让你感觉像坐上了火箭。我们使用一个非常流行的库:luma.oled。它封装了所有底层细节,你只需要关心“显示什么”。

首先,安装必要的库。打开终端,依次执行:

sudo apt update
sudo apt install python3-pip python3-pil -y
sudo pip3 install luma.oled

安装过程可能需要一点时间。完成后,我们就可以写一个超级简单的测试脚本了。新建一个文件,比如叫 oled_test.py:

from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import ImageFont, ImageDraw
import time

# 1. 初始化I2C通信,指定端口为1(对应/dev/i2c-1),地址为0x3C
serial = i2c(port=1, address=0x3C)

# 2. 创建设备对象,指定是SSD1306驱动的128x64分辨率屏幕
device = ssd1306(serial)

# 3. 尝试加载一个字体(如果系统没有,可以指定字体文件路径)
# 这里使用一个默认的字体
try:
    font = ImageFont.load_default()
except:
    font = None
    print("Using default font.")

print("OLED Screen Ready! Press Ctrl+C to exit.")

try:
    while True:
        # 使用canvas在屏幕上绘制
        with canvas(device) as draw:
            # 画一个矩形边框
            draw.rectangle(device.bounding_box, outline="white", fill="black")
            # 显示文字
            draw.text((10, 10), "Hello Pi!", fill="white", font=font)
            draw.text((10, 30), "OLED Test", fill="white", font=font)
            # 显示当前时间
            draw.text((10, 50), time.strftime("%H:%M:%S"), fill="white", font=font)
        time.sleep(1) # 每秒刷新一次时间
except KeyboardInterrupt:
    # 按Ctrl+C退出时,清空屏幕
    device.clear()
    print("\nProgram exited, screen cleared.")

保存后,直接运行:

python3 oled_test.py

你立刻就能在屏幕上看到带边框的“Hello Pi!”和实时刷新的时间了!不需要编译,修改代码后直接再次运行就能看到新效果,这种即时反馈对学习来说太棒了。luma.oled库的功能远不止显示文字,它还能画线、画圆、显示图片。你可以轻松地把网络获取的天气信息、系统监控数据(用psutil库获取)显示在这块小屏上。

5.1 Python进阶:显示系统信息和图片

让我们再深入一点,写一个更实用的脚本,显示树莓派的IP地址和CPU温度,并显示一张小图片(需要先将图片转换成适合OLED的格式)。

首先,安装获取系统信息的库:

sudo pip3 install psutil

然后准备一张128x64像素的黑白BMP图片,命名为logo.bmp,放在和脚本相同的目录。接着创建 oled_system.py:

from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from PIL import Image, ImageFont, ImageDraw
import psutil
import socket
import time

# 初始化
serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)

# 加载一个小字体(可以自己下载.pil或.ttf字体文件放到目录)
# font = ImageFont.truetype("arial.ttf", 12)
font_small = ImageFont.load_default()

# 加载Logo图片
try:
    logo = Image.open('logo.bmp').convert('1') # 转换为1位黑白模式
except:
    logo = None
    print("Logo image not found.")

def get_ip_address():
    """获取树莓派的IP地址(有线优先)"""
    try:
        # 获取有线网卡eth0的IP
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "No Network"

def get_cpu_temp():
    """获取CPU温度"""
    try:
        with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
            temp = float(f.read()) / 1000.0
        return f"{temp:.1f}'C"
    except:
        return "N/A"

print("System Monitor Running...")

try:
    while True:
        with canvas(device) as draw:
            # 第一行:显示Logo或标题
            if logo:
                draw.bitmap((0, 0), logo, fill="white")
            else:
                draw.text((0, 0), "Pi Monitor", fill="white", font=font_small)

            # 第二行:显示IP地址
            ip = get_ip_address()
            draw.text((0, 20), f"IP: {ip}", fill="white", font=font_small)

            # 第三行:显示CPU温度
            cpu_temp = get_cpu_temp()
            draw.text((0, 35), f"CPU: {cpu_temp}", fill="white", font=font_small)

            # 第四行:显示CPU使用率进度条
            cpu_percent = psutil.cpu_percent()
            draw.text((0, 50), "Load:", fill="white", font=font_small)
            # 画一个简单的进度条
            bar_width = 60
            bar_filled = int((cpu_percent / 100.0) * bar_width)
            draw.rectangle([40, 50, 40+bar_width, 58], outline="white")
            draw.rectangle([40, 50, 40+bar_filled, 58], fill="white")

        time.sleep(2) # 每2秒更新一次
except KeyboardInterrupt:
    device.clear()
    print("Monitor stopped.")

运行这个脚本,你的OLED屏幕就变成了一个迷你的系统监控仪表盘!这比单纯显示“Hello World”要有趣和实用得多。通过这个例子,你就能举一反三,把任何你想监控的数据(比如内存使用率、磁盘空间、MQTT消息等)都搬上屏幕。

6. 避坑指南与性能优化

在折腾的过程中,你肯定会遇到一些“坑”。我把常见的几个问题和解决办法列出来,希望能帮你节省时间。

问题一:运行Python脚本时报错 ImportError: No module named 'luma.core' 这说明库没有安装成功。请确保你用的是 pip3 而不是 pip,并且安装命令前面加了 sudo。也可以尝试用 sudo pip3 install --upgrade luma.oled 升级安装。

问题二:屏幕显示乱码、错位或者花屏 这通常是初始化序列不对造成的。不同批次的SSD1306屏幕,其初始化命令可能有个别参数差异。如果你用的是C语言,可以尝试调整初始化函数 OLED_Init() 里的命令,特别是对比度(0x81后面的值)、扫描方向(0xA1/0xC8)这几个。Python的 luma.oled 库通常会自动处理,但如果遇到,可以在创建设备时指定参数,如 device = ssd1306(serial, rotate=2) 来旋转显示。

问题三:I2C地址不对,检测不到设备 这是最常遇到的问题。首先用 sudo i2cdetect -y 1 反复确认地址。有些屏幕模块上有一个电阻焊点,可以切换地址(如0x3C和0x3D)。如果地址确实不是0x3C,记得在代码里修改。另外,确保接线牢固,尤其是GND线。

问题四:显示刷新慢,有拖影 这是I2C通信速率限制导致的。I2C标准模式是100kHz,对于刷新整屏图形确实有点慢。优化方法有:1. 在C语言中,可以尝试提高树莓派的I2C总线速度(修改 /boot/config.txt,添加 dtparam=i2c_arm_baudrate=400000,将速率提升到400kHz,然后重启)。2. 只刷新屏幕上变化的部分,而不是全屏刷新。3. 如果对刷新率要求高,可以考虑换用SPI接口的OLED屏幕,SPI的速率要快得多。

问题五:屏幕一会儿正常,一会儿不正常 检查电源。如果屏幕和树莓派共用USB电源,当树莓派CPU高负载时,电压可能会有轻微波动,导致屏幕工作不稳定。尝试给屏幕单独供电,或者使用一个外部质量更好的5V电源给树莓派供电。

最后,关于性能,对于大多数信息显示类应用,I2C驱动OLED是完全够用的。我的个人经验是,用Python显示动态数据(如秒表、滚动文字)也很流畅。真正的瓶颈在于你想用它播放动画,那就会比较卡顿。所以,根据你的项目目标来选择协议和优化方向。玩树莓派硬件就是这样,一边动手,一边解决问题,乐趣和知识都在这个过程中。希望这篇超详细的指南能帮你顺利点亮第一块屏幕,打开树莓派硬件世界的大门。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐