目录
[开篇先搞懂: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 层代码)
[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,集成电路总线,是一种两线式串行总线,只需要两根线,就能实现主机和多个从机之间的通信,是嵌入式里最常用的外设通信总线之一。
核心特点,小白必记
- 两根线就能通信:只需要一根 SCL(串行时钟线)和一根 SDA(串行数据线),就能实现通信,极大节省了 GPIO 引脚;
- 支持多从机:一条 I2C 总线上,可以挂载多个从设备,每个从设备都有一个唯一的 7 位地址,主机通过地址来区分不同的从设备,不用额外的片选线;
- 半双工通信:SDA 数据线同一时间只能发送或者接收数据,不能同时收发;
- 主从架构:I2C 总线分为主机和从机,主机负责发起通信、提供时钟信号,从机响应主机的通信,我们的 RK3568 开发板就是主机,OLED 屏、传感器就是从机;
- 常用速率:标准模式 100kbps,快速模式 400kbps,高速模式 3.4Mbps,入门用 100kbps/400kbps 就足够了。
I2C 总线的硬件连接
- 两根线:SCL(时钟线)和 SDA(数据线),主机和所有从机的 SCL 都连在一起,SDA 都连在一起;
- 上拉电阻:SCL 和 SDA 线必须各接一个 4.7K 的上拉电阻,接到 3.3V 电源上。因为 I2C 总线是开漏输出,必须用上拉电阻才能输出高电平,不然总线无法正常工作。我们常用的 OLED 模块、传感器模块,一般都已经自带了上拉电阻,不用我们自己外接;
- 共地:主机和所有从机必须共地,不然电平会乱跳,通信失败。
小白红线警告: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 从机写数据,完整的流程是:
- 主机发送起始信号;
- 主机发送从机的 7 位地址 + 写标志位(第 0 位是 0,代表写操作);
- 从机发送应答信号 ACK;
- 主机发送要写入的寄存器地址;
- 从机发送应答信号 ACK;
- 主机发送要写入的数据字节;
- 从机发送应答信号 ACK;
- 重复 6-7 步骤,可以写入多个字节;
- 主机发送停止信号,结束通信。
读操作的流程类似,只是地址字节的第 0 位是 1,代表读操作。
二、RK3568 I2C 控制器详解
RK3568 芯片内置了6 路硬件 I2C 控制器,分别是 I2C0~I2C5,每一路都独立工作,支持标准模式 100kbps 和快速模式 400kbps,完全满足我们的开发需求。
核心特点
- 官方 SDK 里已经实现了完整的 Linux I2C 子系统驱动,我们不用从零写 I2C 控制器的时序驱动,不用手动操作寄存器模拟时序,只需要调用内核提供的 I2C 子系统 API,就能实现 I2C 通信,极大降低了开发难度;
- 每一路 I2C 都对应了固定的 GPIO 引脚,我们只需要在设备树里把对应的引脚配置为 I2C 功能,就能使用;
- 我们这次实战用I2C1 ,对应的引脚是:
- SCL:GPIO0_B2,复用功能 1;
- SDA:GPIO0_B3,复用功能 1。
三、实战前的硬件准备
我们这次的实战目标:基于 RK3568 的 I2C1 控制器,驱动 0.96 寸 SSD1306 OLED 屏(128*64 分辨率,I2C 接口),实现字符、图形显示,并且通过安卓 App 控制屏幕显示的内容。
硬件清单
- RK3568 开发板 1 块;
- 0.96 寸 SSD1306 OLED 屏 1 个(I2C 接口,3.3V 供电);
- 杜邦线 4 根;
- 面包板 1 个(可选)。
硬件接线,一步都不能错
表格
| RK3568 开发板引脚 | OLED 屏引脚 | 说明 |
|---|---|---|
| 3.3V | VCC | 屏幕供电,必须 3.3V,别接 5V,不然烧屏幕 |
| GND | GND | 共地,必须接 |
| GPIO0_B2(I2C1_SCL) | SCL | I2C 时钟线 |
| GPIO0_B3(I2C1_SDA) | SDA | I2C 数据线 |
小白避坑:
- 先确认你的 OLED 屏是 3.3V 供电的,别接 5V,不然直接烧屏;
- 确认屏幕的 I2C 地址,SSD1306 的默认地址一般是 0x3C 或者 0x3D,后面驱动里要对应上;
- 接线的时候,SCL 接 SCL,SDA 接 SDA,别交叉接反了,不然通信失败。
四、第一步:设备树配置
我们需要在设备树里添加 OLED 设备节点,配置 I2C 引脚复用,使能 I2C1 控制器。
1. 修改板级设备树文件
-
进入设备树目录,打开你的开发板对应的.dts 文件: bash
运行
cd ~/RK3568_Android11_SDK/kernel/arch/arm64/boot/dts/rockchip/ vim rk3568-firefly.dts -
在 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"; }; };
核心属性讲解
clock-frequency = <400000>:设置 I2C 总线的速率为 400kbps,也就是快速模式,SSD1306 支持这个速率;reg = <0x3c>:OLED 屏的 I2C 从机地址,这个是核心,必须和你的屏幕实际地址一致,不然驱动匹配不上,通信失败。如果 0x3C 不行,试试 0x3D;- 引脚复用配置里的
rockchip,pins,把 GPIO0_B2 和 B3 配置为复用功能 1,也就是 I2C1 的 SCL 和 SDA 功能。
2. 编译烧录验证
-
编译设备树,打包 boot.img,烧录到开发板,重启;
-
验证 I2C 控制器使能成功:
bash
运行
adb shell su ls /sys/bus/i2c/devices/i2c-1能看到 i2c-1 目录,说明 I2C1 控制器已经正常使能;
-
扫描 I2C 总线上的设备,确认 OLED 屏能被识别:
bash
运行
i2cdetect -y 1这个命令会扫描 I2C1 总线上的所有从设备,如果能看到 3C 这个地址,说明硬件接线正常,屏幕能被识别到,我们就可以开始写驱动了。
小白提示:如果 i2cdetect 命令找不到,用
busybox i2cdetect -y 1。
五、第二步:I2C 驱动内核代码开发
Linux 内核给我们提供了两种 I2C 驱动的开发方式:
- 用户空间驱动 :通过
/dev/i2c-x设备文件,在用户空间用 ioctl 实现 I2C 通信,不用写内核驱动,简单快速,适合调试和简单的应用; - 内核空间驱动:基于 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 向芯片写入对应的命令和数据就行:
- 命令写入:向 0x00 寄存器写入命令字节,用来初始化屏幕、设置显示坐标、开关显示等;
- 数据写入:向 0x40 寄存器写入数据字节,也就是要显示的内容,每个字节对应屏幕上的 8 个像素点;
- 屏幕分辨率 128*64,分为 8 个页(Page0~Page7),每个页 8 行,128 列,每个字节对应一列的 8 个像素。
3. 完整驱动代码编写
-
创建驱动文件: bash
运行
cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers touch oled_drv.c -
完整驱动代码,全注释详解: 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. 编译驱动,烧录验证
-
修改 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 -
编译内核,打包 boot.img,烧录到开发板,重启;
-
验证驱动加载成功: bash
运行
adb shell su dmesg | grep oled_drv能看到「OLED 驱动加载成功」的日志,同时 OLED 屏上会显示我们写的欢迎语,说明驱动工作正常!
-
查看设备文件,设置权限: bash
运行
ls -l /dev/oled_drv chmod 777 /dev/oled_drv
六、第三步:HAL 层适配 + 安卓 App 开发
我们完成 HAL 层适配,然后写一个安卓 App,实现:
- 清屏、开关屏幕显示;
- 输入文本,点击按钮,就能在 OLED 屏上显示对应的内容;
- 可以设置显示的坐标和字体大小。
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:I2C 通信完全没反应,i2cdetect 扫不到设备 90% 的情况是这几个问题:
- 接线错了,SCL 和 SDA 接反了,或者没共地;
- 引脚复用配置错了,没有把引脚配置为 I2C 功能;
- 屏幕供电不对,3.3V 的屏接了 5V,烧了;
- 没有上拉电阻,总线无法正常工作。
- 坑 2:i2cdetect 能扫到地址,但是驱动 probe 函数不执行设备树里的 compatible 属性,和驱动里的 of_match_table 里的字符串不一致,差一个字符都不行,必须完全一致。
- 坑 3:I2C 读写返回错误码从机地址不对,比如屏幕实际地址是 0x3D,你写的 0x3C,就会读写失败。用 i2cdetect 扫出来的地址是多少,设备树里的 reg 就写多少。
- 坑 4:屏幕能初始化,但是显示乱码字体库不对,或者坐标设置错了,页地址模式和列地址模式搞混了。检查 SSD1306 的初始化命令里的内存地址模式,确保和你的显示代码匹配。
- 坑 5:多线程调用 I2C 函数,导致内核崩溃I2C 子系统的 API 不是线程安全的,不能在多个线程里同时调用,必须加锁保护,不然会导致内核崩溃。
结尾说两句
这篇文章,我们彻底搞懂了 I2C 总线的核心原理,完成了 RK 平台 I2C 设备驱动的完整开发,实现了 SSD1306 OLED 屏的驱动和显示,打通了安卓 App 控制屏幕的全链路。掌握了 I2C 驱动开发,你就能适配市面上绝大多数的传感器和外设,比如温湿度传感器、加速度计、陀螺仪、触控芯片等等,一通百通。
下一篇,我们进入串行总线的第二部分:SPI 设备驱动开发,以 SPI 接口的 LCD 屏为例,教你怎么用 Linux SPI 子系统,实现高速 SPI 设备的驱动开发,对比 I2C 的区别和优势。
我是黒漂技术佬,关注我,带你零基础入门 RK 安卓驱动开发,不踩坑。有任何 I2C 驱动的问题,评论区留言,我都会一一回复。