第 11 篇 RK 平台安卓驱动实战 4:I2C 设备驱动开发,以 OLED 屏为例

目录

[开篇先搞懂:I2C 总线到底是什么?](#开篇先搞懂:I2C 总线到底是什么?)

大白话定义

核心特点,小白必记

[I2C 总线的硬件连接](#I2C 总线的硬件连接)

[一、I2C 通信的核心时序,小白必须懂](#一、I2C 通信的核心时序,小白必须懂)

[1. 起始信号(Start)](#1. 起始信号(Start))

[2. 停止信号(Stop)](#2. 停止信号(Stop))

[3. 数据传输](#3. 数据传输)

[4. 应答信号(ACK/NACK)](#4. 应答信号(ACK/NACK))

[一次完整的 I2C 写操作流程](#一次完整的 I2C 写操作流程)

[二、RK3568 I2C 控制器详解](#二、RK3568 I2C 控制器详解)

核心特点

三、实战前的硬件准备

硬件清单

硬件接线,一步都不能错

四、第一步:设备树配置

[1. 修改板级设备树文件](#1. 修改板级设备树文件)

核心属性讲解

[2. 编译烧录验证](#2. 编译烧录验证)

[五、第二步:I2C 驱动内核代码开发](#五、第二步:I2C 驱动内核代码开发)

[1. 核心 I2C 子系统 API 讲解](#1. 核心 I2C 子系统 API 讲解)

[2. SSD1306 驱动核心知识点](#2. SSD1306 驱动核心知识点)

[3. 完整驱动代码编写](#3. 完整驱动代码编写)

[4. 编译驱动,烧录验证](#4. 编译驱动,烧录验证)

[六、第三步:HAL 层适配 + 安卓 App 开发](#六、第三步:HAL 层适配 + 安卓 App 开发)

[1. HAL 层代码](#1. HAL 层代码)

头文件my_oled_hal.h

实现文件my_oled_hal.c

[2. 安卓 App 核心功能](#2. 安卓 App 核心功能)

[七、小白 I2C 驱动必踩的坑,提前规避](#七、小白 I2C 驱动必踩的坑,提前规避)

结尾说两句


大家好,我是黒漂技术佬。前几篇我们搞定了 GPIO、中断、PWM 这些单引脚的外设驱动,后台很多兄弟问:

"佬,我手里有很多传感器、OLED 屏,都是 I2C 接口的,该怎么写驱动?"

I2C 总线是嵌入式开发里最常用的串行总线,几乎所有的传感器(温度、湿度、加速度、陀螺仪)、显示屏、存储芯片、触控芯片,都用 I2C 接口。掌握 I2C 驱动开发,你就能适配市面上 90% 的低速率外设,是嵌入式工程师必备的核心技能。

今天这篇,我就用大白话给你讲透 I2C 总线的核心原理,手把手带你完成RK3568 平台 I2C 设备驱动的完整开发,以最常用的 0.96 寸 SSD1306 OLED 屏为例,实现屏幕字符、图形显示,并且打通安卓 App 控制屏幕显示内容的全链路,学完就能直接适配其他 I2C 传感器。


开篇先搞懂:I2C 总线到底是什么?

大白话定义

I2C 的全称是 Inter-Integrated Circuit,集成电路总线,是一种两线式串行总线,只需要两根线,就能实现主机和多个从机之间的通信,是嵌入式里最常用的外设通信总线之一。

核心特点,小白必记

  1. 两根线就能通信:只需要一根 SCL(串行时钟线)和一根 SDA(串行数据线),就能实现通信,极大节省了 GPIO 引脚;
  2. 支持多从机:一条 I2C 总线上,可以挂载多个从设备,每个从设备都有一个唯一的 7 位地址,主机通过地址来区分不同的从设备,不用额外的片选线;
  3. 半双工通信:SDA 数据线同一时间只能发送或者接收数据,不能同时收发;
  4. 主从架构:I2C 总线分为主机和从机,主机负责发起通信、提供时钟信号,从机响应主机的通信,我们的 RK3568 开发板就是主机,OLED 屏、传感器就是从机;
  5. 常用速率:标准模式 100kbps,快速模式 400kbps,高速模式 3.4Mbps,入门用 100kbps/400kbps 就足够了。

I2C 总线的硬件连接

  1. 两根线:SCL(时钟线)和 SDA(数据线),主机和所有从机的 SCL 都连在一起,SDA 都连在一起;
  2. 上拉电阻:SCL 和 SDA 线必须各接一个 4.7K 的上拉电阻,接到 3.3V 电源上。因为 I2C 总线是开漏输出,必须用上拉电阻才能输出高电平,不然总线无法正常工作。我们常用的 OLED 模块、传感器模块,一般都已经自带了上拉电阻,不用我们自己外接;
  3. 共地:主机和所有从机必须共地,不然电平会乱跳,通信失败。

小白红线警告:RK3568 的 I2C 引脚是 3.3V 电平的,绝对不能直接接 5V 的 I2C 设备,不然会烧芯片引脚。如果要接 5V 设备,必须加电平转换模块。


一、I2C 通信的核心时序,小白必须懂

I2C 通信的核心,就是 4 个基本时序:起始信号、停止信号、应答信号、数据传输,所有的 I2C 通信,都是由这 4 个基本时序组成的。

1. 起始信号(Start)

主机发起通信的信号,当 SCL 是高电平时,SDA 从高电平拉到低电平,就是起始信号,代表一次 I2C 通信的开始。

2. 停止信号(Stop)

主机结束通信的信号,当 SCL 是高电平时,SDA 从低电平拉到高电平,就是停止信号,代表一次 I2C 通信的结束。

3. 数据传输

I2C 的数据是按字节传输的,每个字节 8 位,高位在前,低位在后。在 SCL 的低电平期间,主机改变 SDA 的电平,在 SCL 的高电平期间,从机读取 SDA 的电平,这样就完成了 1 位数据的传输,8 个时钟周期,就传输完 1 个字节。

4. 应答信号(ACK/NACK)

每传输完 1 个字节(8 位),接收方会在第 9 个时钟周期,把 SDA 拉低,发送一个应答信号 ACK,告诉发送方 "我已经收到数据了"。如果接收方没有拉低 SDA,就是非应答信号 NACK,代表数据传输失败。

一次完整的 I2C 写操作流程

我们要给 I2C 从机写数据,完整的流程是:

  1. 主机发送起始信号;
  2. 主机发送从机的 7 位地址 + 写标志位(第 0 位是 0,代表写操作);
  3. 从机发送应答信号 ACK;
  4. 主机发送要写入的寄存器地址;
  5. 从机发送应答信号 ACK;
  6. 主机发送要写入的数据字节;
  7. 从机发送应答信号 ACK;
  8. 重复 6-7 步骤,可以写入多个字节;
  9. 主机发送停止信号,结束通信。

读操作的流程类似,只是地址字节的第 0 位是 1,代表读操作。


二、RK3568 I2C 控制器详解

RK3568 芯片内置了6 路硬件 I2C 控制器,分别是 I2C0~I2C5,每一路都独立工作,支持标准模式 100kbps 和快速模式 400kbps,完全满足我们的开发需求。

核心特点

  1. 官方 SDK 里已经实现了完整的 Linux I2C 子系统驱动,我们不用从零写 I2C 控制器的时序驱动,不用手动操作寄存器模拟时序,只需要调用内核提供的 I2C 子系统 API,就能实现 I2C 通信,极大降低了开发难度;
  2. 每一路 I2C 都对应了固定的 GPIO 引脚,我们只需要在设备树里把对应的引脚配置为 I2C 功能,就能使用;
  3. 我们这次实战用I2C1 ,对应的引脚是:
    • SCL:GPIO0_B2,复用功能 1;
    • SDA:GPIO0_B3,复用功能 1。

三、实战前的硬件准备

我们这次的实战目标:基于 RK3568 的 I2C1 控制器,驱动 0.96 寸 SSD1306 OLED 屏(128*64 分辨率,I2C 接口),实现字符、图形显示,并且通过安卓 App 控制屏幕显示的内容。

硬件清单

  1. RK3568 开发板 1 块;
  2. 0.96 寸 SSD1306 OLED 屏 1 个(I2C 接口,3.3V 供电);
  3. 杜邦线 4 根;
  4. 面包板 1 个(可选)。

硬件接线,一步都不能错

表格

RK3568 开发板引脚 OLED 屏引脚 说明
3.3V VCC 屏幕供电,必须 3.3V,别接 5V,不然烧屏幕
GND GND 共地,必须接
GPIO0_B2(I2C1_SCL) SCL I2C 时钟线
GPIO0_B3(I2C1_SDA) SDA I2C 数据线

小白避坑

  1. 先确认你的 OLED 屏是 3.3V 供电的,别接 5V,不然直接烧屏;
  2. 确认屏幕的 I2C 地址,SSD1306 的默认地址一般是 0x3C 或者 0x3D,后面驱动里要对应上;
  3. 接线的时候,SCL 接 SCL,SDA 接 SDA,别交叉接反了,不然通信失败。

四、第一步:设备树配置

我们需要在设备树里添加 OLED 设备节点,配置 I2C 引脚复用,使能 I2C1 控制器。

1. 修改板级设备树文件

  1. 进入设备树目录,打开你的开发板对应的.dts 文件: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK/kernel/arch/arm64/boot/dts/rockchip/
    vim rk3568-firefly.dts
  2. 在 I2C1 控制器节点里,添加 OLED 设备节点,同时配置引脚复用: dts

    复制代码
    // 配置I2C1的引脚复用
    &pinctrl {
        i2c1 {
            i2c1_xfer: i2c1-xfer {
                rockchip,pins = <0 RK_PB2 1 &pcfg_pull_none_smt>,
                                <0 RK_PB3 1 &pcfg_pull_none_smt>;
                // 说明:GPIO0_B2和B3,复用功能1,也就是I2C1功能
            };
        };
    };
    
    // 使能I2C1控制器,添加OLED设备节点
    &i2c1 {
        status = "okay";
        clock-frequency = <400000>; // I2C速率400kbps
        pinctrl-names = "default";
        pinctrl-0 = <&i2c1_xfer>;
    
        // OLED设备节点
        oled: ssd1306@3c {
            compatible = "ssd1306,oled"; // 兼容匹配属性
            reg = <0x3c>; // OLED的I2C地址,根据你的屏幕实际地址修改
            status = "okay";
        };
    };

核心属性讲解

  1. clock-frequency = <400000>:设置 I2C 总线的速率为 400kbps,也就是快速模式,SSD1306 支持这个速率;
  2. reg = <0x3c>:OLED 屏的 I2C 从机地址,这个是核心,必须和你的屏幕实际地址一致,不然驱动匹配不上,通信失败。如果 0x3C 不行,试试 0x3D;
  3. 引脚复用配置里的rockchip,pins,把 GPIO0_B2 和 B3 配置为复用功能 1,也就是 I2C1 的 SCL 和 SDA 功能。

2. 编译烧录验证

  1. 编译设备树,打包 boot.img,烧录到开发板,重启;

  2. 验证 I2C 控制器使能成功:

    bash

    运行

    复制代码
    adb shell
    su
    ls /sys/bus/i2c/devices/i2c-1

    能看到 i2c-1 目录,说明 I2C1 控制器已经正常使能;

  3. 扫描 I2C 总线上的设备,确认 OLED 屏能被识别:

    bash

    运行

    复制代码
    i2cdetect -y 1

    这个命令会扫描 I2C1 总线上的所有从设备,如果能看到 3C 这个地址,说明硬件接线正常,屏幕能被识别到,我们就可以开始写驱动了。

    小白提示:如果 i2cdetect 命令找不到,用busybox i2cdetect -y 1


五、第二步:I2C 驱动内核代码开发

Linux 内核给我们提供了两种 I2C 驱动的开发方式:

  1. 用户空间驱动 :通过/dev/i2c-x设备文件,在用户空间用 ioctl 实现 I2C 通信,不用写内核驱动,简单快速,适合调试和简单的应用;
  2. 内核空间驱动:基于 Linux I2C 子系统,写标准的内核驱动,性能更高,更稳定,适合量产产品。

我们这次先讲标准的内核驱动开发方式,基于 I2C 子系统,实现 SSD1306 OLED 屏的驱动,封装成字符设备驱动,给上层调用。

1. 核心 I2C 子系统 API 讲解

Linux I2C 子系统给我们提供了一套标准的 API,不用我们关心底层的时序,直接调用就能实现 I2C 读写:

表格

API 函数 作用
i2c_master_send() 主机向 I2C 从机发送数据,写操作
i2c_master_recv() 主机从 I2C 从机接收数据,读操作
i2c_smbus_write_byte_data() 向从机的指定寄存器写入 1 个字节数据,最常用
i2c_smbus_read_byte_data() 从从机的指定寄存器读取 1 个字节数据

对于 SSD1306 OLED 屏,我们最常用的就是i2c_smbus_write_byte_data(),因为我们需要向屏幕的指定寄存器写入命令和数据,来控制屏幕显示。

2. SSD1306 驱动核心知识点

SSD1306 是 OLED 屏的驱动芯片,我们要控制屏幕显示,只需要通过 I2C 向芯片写入对应的命令和数据就行:

  1. 命令写入:向 0x00 寄存器写入命令字节,用来初始化屏幕、设置显示坐标、开关显示等;
  2. 数据写入:向 0x40 寄存器写入数据字节,也就是要显示的内容,每个字节对应屏幕上的 8 个像素点;
  3. 屏幕分辨率 128*64,分为 8 个页(Page0~Page7),每个页 8 行,128 列,每个字节对应一列的 8 个像素。

3. 完整驱动代码编写

  1. 创建驱动文件: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers
    touch oled_drv.c
  2. 完整驱动代码,全注释详解: c

    运行

    复制代码
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/uaccess.h>
    #include <linux/device.h>
    #include <linux/i2c.h>
    #include <linux/delay.h>
    
    // 驱动信息声明
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("黒漂技术佬");
    MODULE_DESCRIPTION("RK3568 I2C SSD1306 OLED Driver");
    MODULE_VERSION("1.0");
    
    // 宏定义
    #define DEVICE_NAME "oled_drv"
    #define CLASS_NAME "oled_class"
    #define OLED_WIDTH 128
    #define OLED_HEIGHT 64
    
    // OLED控制命令
    #define OLED_CMD  0x00  // 写命令
    #define OLED_DATA 0x40  // 写数据
    
    // 屏幕显示控制命令
    #define CMD_OLED_ON  0x3001  // 打开屏幕显示
    #define CMD_OLED_OFF 0x3002  // 关闭屏幕显示
    #define CMD_OLED_CLR 0x3003  // 清屏
    #define CMD_OLED_SHOW_STR 0x3004 // 显示字符串
    
    // 字符串显示结构体,和用户空间交互
    struct oled_str {
        unsigned char x;     // 起始x坐标(0~127)
        unsigned char y;     // 起始页(0~7)
        unsigned char size;  // 字体大小(12/16)
        char str[64];        // 要显示的字符串
    };
    
    // 全局变量
    static dev_t oled_devno;
    static struct cdev oled_cdev;
    static struct class *oled_class;
    static struct device *oled_device;
    static struct i2c_client *oled_client; // I2C客户端句柄
    
    // 12*6 ASCII字体库,8*16的可以自己扩展
    const unsigned char F12x6[][12] = {
        // 这里只放了数字和字母的部分,完整字体库可以自己补充
        {' ', 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
        {'0', 0x00,0x00,0x3E,0x41,0x41,0x41,0x41,0x41,0x41,0x3E,0x00},
        {'1', 0x00,0x00,0x00,0x00,0x21,0x41,0x7F,0x01,0x01,0x00,0x00},
        {'2', 0x00,0x00,0x21,0x43,0x45,0x49,0x51,0x61,0x41,0x00,0x00},
        {'A', 0x00,0x00,0x7F,0x08,0x08,0x08,0x08,0x08,0x7F,0x00,0x00},
        {'B', 0x00,0x00,0x7F,0x49,0x49,0x49,0x49,0x49,0x36,0x00,0x00},
        {'C', 0x00,0x00,0x3E,0x41,0x41,0x41,0x41,0x41,0x22,0x00,0x00},
        // 完整的字体库可以自己补充,这里为了篇幅简化
    };
    
    // ====================== OLED底层操作函数 ======================
    // 向OLED写入一个字节的命令
    static int oled_write_cmd(unsigned char cmd)
    {
        return i2c_smbus_write_byte_data(oled_client, OLED_CMD, cmd);
    }
    
    // 向OLED写入一个字节的数据
    static int oled_write_data(unsigned char data)
    {
        return i2c_smbus_write_byte_data(oled_client, OLED_DATA, data);
    }
    
    // 设置显示坐标
    static void oled_set_pos(unsigned char x, unsigned char y)
    {
        oled_write_cmd(0xb0 + y);                // 设置页地址
        oled_write_cmd(((x & 0xf0) >> 4) | 0x10); // 设置x坐标高4位
        oled_write_cmd(x & 0x0f);                 // 设置x坐标低4位
    }
    
    // 清屏函数
    static void oled_clear(void)
    {
        unsigned char y, x;
        for (y = 0; y < 8; y++) {
            oled_set_pos(0, y);
            for (x = 0; x < 128; x++) {
                oled_write_data(0x00); // 所有像素点清零
            }
        }
    }
    
    // 屏幕初始化
    static int oled_init(void)
    {
        msleep(100); // 等待屏幕上电稳定
    
        // 初始化命令序列,SSD1306标准初始化流程
        oled_write_cmd(0xAE); // 关闭显示
        oled_write_cmd(0xD5); // 设置时钟分频因子,震荡频率
        oled_write_cmd(0x80); // [3:0],分频因子;[7:4],震荡频率
        oled_write_cmd(0xA8); // 设置驱动路数
        oled_write_cmd(0x3F); // 1/64 duty
        oled_write_cmd(0xD3); // 设置显示偏移
        oled_write_cmd(0x00); // 无偏移
        oled_write_cmd(0x40); // 设置显示开始行 [5:0],行数
        oled_write_cmd(0x8D); // 电荷泵设置
        oled_write_cmd(0x14); // 开启电荷泵
        oled_write_cmd(0x20); // 内存地址模式
        oled_write_cmd(0x02); // 页地址模式
        oled_write_cmd(0xA1); // 段重定义设置,bit0:0,0->0;1,127->0
        oled_write_cmd(0xC8); // COM扫描方向;bit3:0,普通模式;1,重定义模式
        oled_write_cmd(0xDA); // 设置COM硬件引脚配置
        oled_write_cmd(0x12);
        oled_write_cmd(0x81); // 对比度设置
        oled_write_cmd(0xCF);
        oled_write_cmd(0xD9); // 设置预充电周期
        oled_write_cmd(0xF1);
        oled_write_cmd(0xDB); // 设置VCOMH取消选择级别
        oled_write_cmd(0x30);
        oled_write_cmd(0xA4); // 全局显示开启;bit0:1,开启;0,关闭
        oled_write_cmd(0xA6); // 设置显示方式;bit0:1,反相显示;0,正常显示
        oled_write_cmd(0xAF); // 开启显示
    
        oled_clear(); // 清屏
        printk("【oled_drv】OLED初始化完成\n");
        return 0;
    }
    
    // 显示单个字符
    static void oled_show_char(unsigned char x, unsigned char y, unsigned char chr, unsigned char size)
    {
        unsigned char c = 0, i = 0;
        c = chr - ' '; // 得到偏移后的位置
        if (c > sizeof(F12x6)/sizeof(F12x6[0])) c = 0;
    
        if (size == 12) {
            oled_set_pos(x, y);
            for (i = 0; i < 6; i++) {
                oled_write_data(F12x6[c][i+1]);
            }
        }
        // 16号字体可以自己扩展
    }
    
    // 显示字符串
    static void oled_show_string(unsigned char x, unsigned char y, unsigned char size, char *str)
    {
        while (*str) {
            oled_show_char(x, y, *str, size);
            x += 6; // 6点宽
            if (x > 122) {
                x = 0;
                y += 1;
            }
            str++;
        }
    }
    
    // ====================== 字符设备核心函数 ======================
    static int oled_open(struct inode *inode, struct file *filp)
    {
        printk("【oled_drv】设备被打开\n");
        return 0;
    }
    
    static long oled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
    {
        struct oled_str str;
        int ret = 0;
    
        switch (cmd) {
            case CMD_OLED_ON:
                oled_write_cmd(0xAF); // 开启显示
                printk("【oled_drv】屏幕显示开启\n");
                break;
            case CMD_OLED_OFF:
                oled_write_cmd(0xAE); // 关闭显示
                printk("【oled_drv】屏幕显示关闭\n");
                break;
            case CMD_OLED_CLR:
                oled_clear();
                printk("【oled_drv】屏幕清屏完成\n");
                break;
            case CMD_OLED_SHOW_STR:
                // 从用户空间拷贝字符串参数
                ret = copy_from_user(&str, (struct oled_str __user *)arg, sizeof(struct oled_str));
                if (ret) {
                    printk("【oled_drv】参数拷贝失败\n");
                    return -EFAULT;
                }
                oled_show_string(str.x, str.y, str.size, str.str);
                printk("【oled_drv】显示字符串:%s\n", str.str);
                break;
            default:
                printk("【oled_drv】无效命令\n");
                return -EINVAL;
        }
    
        return ret;
    }
    
    static int oled_release(struct inode *inode, struct file *filp)
    {
        printk("【oled_drv】设备被关闭\n");
        return 0;
    }
    
    static const struct file_operations oled_fops = {
        .owner = THIS_MODULE,
        .open = oled_open,
        .unlocked_ioctl = oled_ioctl,
        .release = oled_release,
    };
    
    // ====================== I2C驱动框架 ======================
    static int oled_probe(struct i2c_client *client, const struct i2c_device_id *id)
    {
        int ret;
        printk("【oled_drv】驱动和I2C设备匹配成功\n");
        oled_client = client;
    
        // 初始化OLED屏幕
        ret = oled_init();
        if (ret) {
            dev_err(&client->dev, "OLED初始化失败\n");
            return ret;
        }
    
        // 注册字符设备
        ret = alloc_chrdev_region(&oled_devno, 0, 1, DEVICE_NAME);
        if (ret < 0) {
            dev_err(&client->dev, "设备号申请失败\n");
            return ret;
        }
    
        cdev_init(&oled_cdev, &oled_fops);
        oled_cdev.owner = THIS_MODULE;
        ret = cdev_add(&oled_cdev, oled_devno, 1);
        if (ret < 0) {
            dev_err(&client->dev, "字符设备注册失败\n");
            goto err_devno_free;
        }
    
        oled_class = class_create(THIS_MODULE, CLASS_NAME);
        if (IS_ERR(oled_class)) {
            ret = PTR_ERR(oled_class);
            dev_err(&client->dev, "设备类创建失败\n");
            goto err_cdev_del;
        }
    
        oled_device = device_create(oled_class, NULL, oled_devno, NULL, DEVICE_NAME);
        if (IS_ERR(oled_device)) {
            ret = PTR_ERR(oled_device);
            dev_err(&client->dev, "设备创建失败\n");
            goto err_class_destroy;
        }
    
        // 开机显示欢迎语
        oled_show_string(0, 0, 12, "Hello RK3568!");
        oled_show_string(0, 2, 12, "黒漂技术佬");
        oled_show_string(0, 4, 12, "I2C OLED Demo");
    
        dev_info(&client->dev, "OLED驱动加载成功!\n");
        return 0;
    
    err_class_destroy:
        class_destroy(oled_class);
    err_cdev_del:
        cdev_del(&oled_cdev);
    err_devno_free:
        unregister_chrdev_region(oled_devno, 1);
        return ret;
    }
    
    static int oled_remove(struct i2c_client *client)
    {
        printk("【oled_drv】驱动开始卸载\n");
        oled_clear();
        oled_write_cmd(0xAE); // 关闭显示
        device_destroy(oled_class, oled_devno);
        class_destroy(oled_class);
        cdev_del(&oled_cdev);
        unregister_chrdev_region(oled_devno, 1);
        dev_info(&client->dev, "OLED驱动卸载成功!\n");
        return 0;
    }
    
    // I2C设备ID匹配表
    static const struct i2c_device_id oled_id[] = {
        {"ssd1306,oled", 0},
        {}
    };
    MODULE_DEVICE_TABLE(i2c, oled_id);
    
    // 设备树匹配表
    static const struct of_device_id oled_of_match[] = {
        { .compatible = "ssd1306,oled" },
        {}
    };
    MODULE_DEVICE_TABLE(of, oled_of_match);
    
    // I2C驱动结构体
    static struct i2c_driver oled_driver = {
        .driver = {
            .name = "ssd1306_oled",
            .of_match_table = oled_of_match,
        },
        .probe = oled_probe,
        .remove = oled_remove,
        .id_table = oled_id,
    };
    
    // ====================== 驱动入口和出口 ======================
    static int __init oled_drv_init(void)
    {
        printk("【oled_drv】OLED驱动开始加载\n");
        return i2c_add_driver(&oled_driver);
    }
    
    static void __exit oled_drv_exit(void)
    {
        i2c_del_driver(&oled_driver);
    }
    
    module_init(oled_drv_init);
    module_exit(oled_drv_exit);

4. 编译驱动,烧录验证

  1. 修改 Makefile,添加 OLED 驱动的编译: makefile

    复制代码
    obj-y += hello_drv.o
    obj-y += gpio_drv.o
    obj-y += key_irq_drv.o
    obj-y += pwm_drv.o
    obj-y += oled_drv.o
  2. 编译内核,打包 boot.img,烧录到开发板,重启;

  3. 验证驱动加载成功: bash

    运行

    复制代码
    adb shell
    su
    dmesg | grep oled_drv

    能看到「OLED 驱动加载成功」的日志,同时 OLED 屏上会显示我们写的欢迎语,说明驱动工作正常!

  4. 查看设备文件,设置权限: bash

    运行

    复制代码
    ls -l /dev/oled_drv
    chmod 777 /dev/oled_drv

六、第三步:HAL 层适配 + 安卓 App 开发

我们完成 HAL 层适配,然后写一个安卓 App,实现:

  1. 清屏、开关屏幕显示;
  2. 输入文本,点击按钮,就能在 OLED 屏上显示对应的内容;
  3. 可以设置显示的坐标和字体大小。

1. HAL 层代码

头文件my_oled_hal.h

c

运行

复制代码
#ifndef MY_OLED_HAL_H
#define MY_OLED_HAL_H

#ifdef __cplusplus
extern "C" {
#endif

struct oled_str {
    unsigned char x;
    unsigned char y;
    unsigned char size;
    char str[64];
};

int oled_on(void);
int oled_off(void);
int oled_clear(void);
int oled_show_string(struct oled_str *str);

#ifdef __cplusplus
}
#endif

#endif
实现文件my_oled_hal.c

c

运行

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "my_oled_hal.h"

#define DEVICE_PATH "/dev/oled_drv"
#define CMD_OLED_ON  0x3001
#define CMD_OLED_OFF 0x3002
#define CMD_OLED_CLR 0x3003
#define CMD_OLED_SHOW_STR 0x3004

static int fd = -1;

static int oled_dev_init(void)
{
    if (fd < 0) {
        fd = open(DEVICE_PATH, O_RDWR);
        if (fd < 0) {
            printf("【oled_hal】打开设备文件失败\n");
            return -1;
        }
    }
    return 0;
}

int oled_on(void)
{
    if (oled_dev_init() < 0) return -1;
    return ioctl(fd, CMD_OLED_ON, 0);
}

int oled_off(void)
{
    if (oled_dev_init() < 0) return -1;
    return ioctl(fd, CMD_OLED_OFF, 0);
}

int oled_clear(void)
{
    if (oled_dev_init() < 0) return -1;
    return ioctl(fd, CMD_OLED_CLR, 0);
}

int oled_show_string(struct oled_str *str)
{
    if (oled_dev_init() < 0) return -1;
    return ioctl(fd, CMD_OLED_SHOW_STR, str);
}

2. 安卓 App 核心功能

App 里添加输入框、坐标选择、按钮,用户输入文本后,点击「显示」按钮,就会调用 HAL 层的函数,在 OLED 屏上显示对应的内容,核心代码:

java

运行

复制代码
// 显示按钮点击事件
btnShow.setOnClickListener(v -> {
    String text = etInput.getText().toString();
    if (text.isEmpty()) return;

    struct oled_str str = new struct oled_str();
    str.x = Integer.parseInt(etX.getText().toString());
    str.y = Integer.parseInt(etY.getText().toString());
    str.size = 12;
    str.str = text;

    oledJni.showString(str);
});

// 清屏按钮
btnClear.setOnClickListener(v -> {
    oledJni.clear();
    etInput.setText("");
});

七、小白 I2C 驱动必踩的坑,提前规避

  1. 坑 1:I2C 通信完全没反应,i2cdetect 扫不到设备 90% 的情况是这几个问题:
    • 接线错了,SCL 和 SDA 接反了,或者没共地;
    • 引脚复用配置错了,没有把引脚配置为 I2C 功能;
    • 屏幕供电不对,3.3V 的屏接了 5V,烧了;
    • 没有上拉电阻,总线无法正常工作。
  2. 坑 2:i2cdetect 能扫到地址,但是驱动 probe 函数不执行设备树里的 compatible 属性,和驱动里的 of_match_table 里的字符串不一致,差一个字符都不行,必须完全一致。
  3. 坑 3:I2C 读写返回错误码从机地址不对,比如屏幕实际地址是 0x3D,你写的 0x3C,就会读写失败。用 i2cdetect 扫出来的地址是多少,设备树里的 reg 就写多少。
  4. 坑 4:屏幕能初始化,但是显示乱码字体库不对,或者坐标设置错了,页地址模式和列地址模式搞混了。检查 SSD1306 的初始化命令里的内存地址模式,确保和你的显示代码匹配。
  5. 坑 5:多线程调用 I2C 函数,导致内核崩溃I2C 子系统的 API 不是线程安全的,不能在多个线程里同时调用,必须加锁保护,不然会导致内核崩溃。

结尾说两句

这篇文章,我们彻底搞懂了 I2C 总线的核心原理,完成了 RK 平台 I2C 设备驱动的完整开发,实现了 SSD1306 OLED 屏的驱动和显示,打通了安卓 App 控制屏幕的全链路。掌握了 I2C 驱动开发,你就能适配市面上绝大多数的传感器和外设,比如温湿度传感器、加速度计、陀螺仪、触控芯片等等,一通百通。

下一篇,我们进入串行总线的第二部分:SPI 设备驱动开发,以 SPI 接口的 LCD 屏为例,教你怎么用 Linux SPI 子系统,实现高速 SPI 设备的驱动开发,对比 I2C 的区别和优势。

我是黒漂技术佬,关注我,带你零基础入门 RK 安卓驱动开发,不踩坑。有任何 I2C 驱动的问题,评论区留言,我都会一一回复。

相关推荐
阿拉斯攀登2 小时前
第 9 篇 RK 平台安卓驱动实战 2:中断驱动开发,按键中断的完整实现
驱动开发·嵌入式硬件·rk3568·中断·瑞芯微·rk3576·rk安卓驱动
段娇娇2 小时前
Android jetpack LiveData (二) 原理篇
android·android jetpack
_muffinman2 小时前
LED点阵8*8驱动开发笔记(Ai8051U单片机)
驱动开发·笔记·单片机
徐先生 @_@|||2 小时前
AI 大模型编程的软件开发范式:SDD(Specification-Driven Development)模式驱动开发
人工智能·驱动开发
我命由我123452 小时前
Android 多进程开发 - FileDescriptor、Uri、AIDL 接口定义不能抛出异常
android·java·java-ee·kotlin·android studio·android-studio·android runtime
阿拉斯攀登3 小时前
第 14 篇 显示驱动(MIPI/LVDS 屏)适配与调试,DRM 框架详解
android·驱动开发·rk3568·瑞芯微·rk安卓驱动
阿拉斯攀登4 小时前
第 18 篇 综合项目实战:基于 RK3568 的安卓智能门禁系统,全栈开发
android·驱动开发·瑞芯微·嵌入式驱动·rk3576·安卓驱动
UXbot5 小时前
APP原型生成工具测评
android·前端·人工智能·低代码·ios·开发·app原型
q***75185 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql