用OLED屏幕播放视频(2): 为OLED屏幕开发I2C驱动

下面的系列文章记录了如何使用一块linux开发扳和一块OLED屏幕实现视频的播放:

  1. 项目介绍
  2. 为OLED屏幕开发I2C驱动
  3. 使用cuda编程加速视频处理

这是此系列文章的第2篇, 主要总结和记录一个I2C从设备的驱动, 在linux内核中如何实现, 如何给用户态的程序暴露合适的接口, 让用户态有机会操作真实的硬件设备. 可以通过下面的视频快速了解最终达到的效果和实现的总体思路.

跳转到6:48, 直接观看演示

1). I2C驱动架构

I2C总线是一种主从, 同步, 半双工的低速通信总线, 硬件标准可以参考这里. 这篇文章只讨论I2C总线上从设备的驱动在linux平台下如何实现, 下图是linux中I2C总线相关的软件模块, 其中i2c core提供给驱动开发人员重要的数据结构和接口函数:

  • i2c_adapter: 表示总线上的主设备, 或者说总线控制器
  • i2c_algorithm: 当主设备想要通信时, 它负责具体硬件时序的实现, 比如, 在总线上产生开始/结束条件, 发送/接收数据
  • i2c_client: 表示总线上的从设备
  • i2c_driver: 表示从设备对应的驱动, 需要实现其中的接口函数之后, 把驱动注册到i2c core之中
  • i2c_add_driver: 注册i2c_driver到i2c core, 一般在模块初始化函数中调用
  • i2c_del_driver: 删除i2c_driver, 一般在模块退出函数中调用
  • i2c_master_send/recv: 主设备发送/接收数据, 实际上为了驱动从设备, 需要让主设备向从设备发送合适的命令, 或者读取从设备的状态, 具体发送或者接收什么, 参考从设备的datasheet即可

2). 实现ssd1306屏幕的I2C驱动

  1. 注册i2c_driver
    使用module_i2c_driver宏, 并传递我们实现的i2c_driver, 该宏能够为我们生成模块的init和exit函数, 在函数中自动注册和删除传递进来的i2c_driver. 如果需要在init和exit中做一些其他工作, 则需要自己实现, 不能使用这个宏.
module_i2c_driver(ssd130x_driver);
  1. 实现i2c_driver中的接口
static struct i2c_driver ssd130x_driver = {
    .driver = {
        .owner = THIS_MODULE,
        .name = "ssd130x_driver",
    },
    .probe = ssd130x_probe,
    .remove = ssd130x_remove,
    .id_table = ssd130x_id_table,
};

这里只实现了i2c_driver中的probe和remove. 当驱动和设备匹配成功时, probe函数被调用, 在probe函数中, 完成了字符设备的相关的操作, 包括:

  • 分配设备号
  • 初始化字符设备结构体
  • 添加字符设备到内核
  • 创建设备文件
  1. 实现字符设备接口, 暴露给用户态程序
static struct file_operations ssd130x_fops = {
	.owner = THIS_MODULE,
	.open = ssd130x_open,
	.release = ssd130x_close,
	.write = ssd130x_write,
};

用户态程序可以对设备文件进行打开, 关闭, 写入3种操作. 当打开设备文件时, ssd130x_open被调用, 完成OLED屏幕的初始化; 关闭设备文件时, ssd130x_close被调用, 屏幕被关闭; 当向设备文件写入数据时, ssd130x_write被调用, 一帧数据被发送到ssd1306的RAM上, 屏幕显示的内容被更新. 以上3种操作, 底层都是通过i2c_master_send向从设备发送特定的命令或者数据实现的.

2.1). 阅读数据手册

ssd1306的数据手册参考这里, 手册内容较多, 不宜通读, 主要关注以下几点:

  • 基本硬件参数: 屏幕分辨率, 支持的通信接口, 支持哪些显示相关的功能(比如滚动, 反转等) ...
  • 基本工作原理: 通过向RAM中写入数据, 控制屏幕像素点的亮灭
  • 基本使用方法: 支持哪些命令? 分别能控制它的什么功能?
  • Application Note: 典型硬件电路, 示例代码

2.2). 设备的初始化

在数据手册的Application Note中包含使用ssd1306时的初始化流程, 如下图所示. 在此基础上, 可以做一些调整, 比如我在驱动中关闭了屏幕滚动.

2.3). 调整I2C的频率

我在beaglebone black板子上刷入的debian系统, 其设备树中的i2c时钟频率是100kbits/s, 内核中的i2c_algorithm会根据这个频率计算在i2c总线上发送数据时使用的延时. 实际测试之后发现按照这个频率播放视频存在一些卡顿, 因此需要对i2c时钟频率做修改, 有两种方式:

  • 在uboot启动时, 进入uboot的shell, 使用fdt相关的命令修改始终频率
  • 备份原来的设备树文件, 使用dtc编译器从dtb得到dts, 在dts中修改始终频率, 再编译得到新的dtb, 替换原来的设备树文件

我这里采用的是dtc的方式, 这样就不需要每次系统启动都手动修改了, 修改之后的时钟频率为400kbits/s, 播放视频流畅很多.

3). 测试驱动功能

驱动代码编写完成之后, 需要实际测试一下功能, 下面代码首先打开OLED屏幕的设备文件, 写入一帧数据, 每个字节都填充为0x88, 这样屏幕上会显示出预期的条纹, sleep两秒之后, 关闭设备文件, 屏幕熄灭.

#define FRAME_SIZE (128 * 8)

int main(int argc, char **argv)
{
	int device_fd = open("/dev/ssd130x0", O_WRONLY);
	if (device_fd < 0) {
		return -1;
	}

	char *frame = malloc(FRAME_SIZE);
	memset(frame, 0x88, FRAME_SIZE);
	write(device_fd, frame, FRAME_SIZE);
	sleep(2);

	free(frame);
	close(device_fd);
	return 0;
}

4). 文末推广

欢迎关注我的B站账号, 或者加入QQ群838923389, 一起研究计算机底层技术, 一起搞事情:P

其实还有很多实现的细节没有在博客中写出来, 只有自己在做的时候遇到了才能够体会的到, 需要完整代码的老铁直接在qq群中问一下.

热门相关:英雄联盟之巅峰王座   隐婚试爱:娇妻,好甜!   宠物小精灵之庭树   名门盛婚:首席,别来无恙!   宠物小精灵之庭树