第 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 驱动的问题,评论区留言,我都会一一回复。

相关推荐
恋猫de小郭7 小时前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
plainGeekDev8 小时前
ButterKnife → ViewBinding
android·java·kotlin
成都大菠萝1 天前
Android Car CarProperty 车辆信号链路
android
敲代码的鱼1 天前
PDF 预览与签名批注写回 支持安卓 iOS 鸿蒙 UTS插件
android·前端·ios
时光足迹1 天前
uni-app 视频通话实战:康复师与患者视频问诊的 6 个致命 Bug 与解决方案
android·ios·uni-app
Coffeeee1 天前
闲聊几句,Android老哥们,你们多久没做技改需求了
android·程序员·代码规范
萝卜er1 天前
Fragment 生命周期与状态恢复-《Android深水区(四)》
android
萝卜er1 天前
Intent 显式、隐式与 PendingIntent-《Android深水区(五)》
android
Kapaseker1 天前
一文吃透 Kotlin 集合操作符
android·kotlin
三少爷的鞋1 天前
Main-safe:现代Android 架构真正的分水岭
android