一、IIC驱动实验
简介
Linux 下的 I2C 驱动框架,按照指定的框架去编写 I2C 设备驱动总线驱动实现CPU与器件通信的底层,类似单片机IIC协议的底层时序实现过程,单片机需要自己编写但是linux下不需要自己编写,自己需要实现的只是设备树的设备信息和驱动器件的底层寄存器(这里要求我们按照IIC框架编写)
i2c 总线驱动由芯片厂商提供(驱动复杂,官方提供了经过测试的驱动,我们直接用) 。本篇文章编写的是IIC的oled设备驱动
二、Linux I2C 驱动框架简介
1、 I2C 总线驱动
属于CPU级别的代码,CPU核心算法外设级别
- 一般 SOC 的 I2C 总线驱动都是由半导体厂商编写的,比如 I.MX6U 的 I2C 适配器驱动 NXP已经编写好了,这个不需要用户去编写。
大体工作过程:
I2C 总线驱动重点是 I2C 适配器(也就是 SOC 的 I2C 接口控制器)驱动,这里要用到两个重要的数据结构:i2c_adapter 和 i2c_algorithm,Linux 内核将 SOC 的 I2C 适配器(控制器)抽象成 i2c_adapter,实现初始化结构体的成员变量到达实现功能的效果。i2c_adapter 结构体定义在 include/linux/i2c.h 文件中
- i2c_adapter is the structure used to identify a physical i2c bus along
- with the access algorithms necessary to access it.
struct i2c_adapter {
struct module *owner;
unsigned int class; /* classes to allow probing for */
const struct i2c_algorithm *algo; /* the algorithm to access the bus */
void *algo_data;
/* data fields that are valid for all devices */
struct rt_mutex bus_lock;
int timeout; /* in jiffies */
int retries;
struct device dev; /* the adapter device */
int nr;
char name[48];
struct completion dev_released;
struct mutex userspace_clients_lock;
struct list_head userspace_clients;
struct i2c_bus_recovery_info *bus_recovery_info;
const struct i2c_adapter_quirks *quirks;
};
struct i2c_algorithm {
/* If an adapter algorithm can't do I2C-level access, set master_xfer
to NULL. If an adapter algorithm can do SMBus access, set
smbus_xfer. If set to NULL, the SMBus protocol is simulated
using common I2C messages */
/* master_xfer should return the number of messages successfully
processed, or a negative value on error */
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
int num);
int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);
/* To determine what the adapter supports */
u32 (*functionality) (struct i2c_adapter *);
#if IS_ENABLED(CONFIG_I2C_SLAVE)
int (*reg_slave)(struct i2c_client *client);
int (*unreg_slave)(struct i2c_client *client);
#endif
};
在这个结构体里面有i2c_algorithm 结构体,就是具体实现设备驱动程序可以使用这些 API 函数来完成读写操作。i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法。
- 综上所述,I2C 总线驱动,或者说 I2C 适配器驱动的主要工作就是初始化 i2c_adapter 结构体变量,然后设置
i2c_algorithm 中的 master_xfer 函数。完成以后通过 i2c_add_numbered_adapter或
i2c_add_adapter 这两个函数向系统注册设置好的 i2c_adapte
2、I2C 设备驱动
i2c_client 就是描述设备信息的,i2c_driver 描述驱动内容,类似于 platform_driver。
- 一个设备对应一个 i2c_client,每检测到一个 I2C 设备就会给这个 I2C 设备分配一个i2c_client。
- i2c_driver 类似 platform_driver,是我们编写 I2C 设备驱动重点要处理的内容。
- 对于我们 I2C 设备驱动编写人来说,重点工作就是构建 i2c_driver,构建完成以后需要向Linux 内核注册这个 i2c_driver。i2c_driver 注册函数为 int i2c_register_driver
三、设备驱动编写
这里以OLED为例驱动获取这个芯片数据
1、设备树编写
因为有设备树统一管理硬件设备信息,所以直接采用设备树编写驱动先修改设备树信息,第一就是IO 修改或添加,用于配置CPU外设信息以及引脚的电气属性。
首先查找iomuxc设备节点pinctrl 子系统编写引脚属性,添加属性数据
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
第二步追加设备节点到总线
追加 ap3216c 子节点挂载到iic总线上面
记住同一总线不能挂载相同slave adress的从机ic设备
比如这里使用NXP官方板子evk设备树,其板子上面具有这个芯片
这个设备需要删除
追加信息
OLED@3c{
compatible = "liqi,oled";
reg =<0x3c>;
}
2、设备树编写测试
- make dtbs
- 编译成功以后拷贝dtb到nfs目录远程加载
- reboot重启开发板,进入IIC总线观察是否添加成功
总线挂载目录cd/sys/bus
i2c总线下挂载的设备目录
- ls设备驱动都可以查看
从这里设备就编写成功了
四、驱动编写
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "oledfont.h"
#define OLED_CNT 1
#define OLED_NAME "oled_iic"
#define OLED_CMD 0x00 //OLED写命令
#define OLED_DATA 0x40 //OLED写数据
#define Max_Column 128
/* 字符设备结构体 */
struct oled_dev {
struct i2c_client *client; /* i2c 设备 */
dev_t devid; /* 设备号 */
int major; /* 主设备号 */
int minor; /* ci设备号 */
struct cdev cdev; /*cdev 结构体变量,这个变量就表示一个字符设备*/
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
void *private_data; /* 私有数据 */
};
static struct oled_dev oledcdev;
static s32 oled_write_byte(u8 reg, u8 para, u8 len)
{
u8 data[2];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client *)oledcdev.private_data;
//此处将.probe函数中所保存的私有数据强制转换为i2c_client结构体
data[0] = reg; //寄存器
data[1] = para; //参数
msg.addr = client->addr; //ap3216c地址, 设备树中的地址
msg.flags = 0; //标记为写
msg.buf = data; //要写入的数据缓冲区
msg.len = len + 1; //要写入的数据长度
return i2c_transfer(client->adapter, &msg, 1);//用于发送的client
}
void oled_init(void)
{
u8 i;
u8 data[] = {0xAE, 0x00, 0x10, 0x40, 0xB0, 0x81, 0xFF, 0xA1, 0xA6,
0xA8, 0x3F, 0xC8, 0xD3, 0x00, 0xD5, 0x80, 0xD8, 0x05,
0xD9, 0xF1, 0xDA, 0x12, 0xDB, 0x30, 0x8D, 0x14, 0xAF};
for (i = 0; i < sizeof(data); i++)
{
oled_write_byte(OLED_CMD, data[i], 1);
}
}
void oled_clear(void)
{
u8 i, n;
for (i = 0; i < 8; i++)
{
oled_write_byte(OLED_CMD, 0xb0 + i, 1); //设置页地址(0~7)
oled_write_byte(OLED_CMD, 0x00, 1); //设置显示位置—列低地址
oled_write_byte(OLED_CMD, 0x10, 1); //设置显示位置—列高地址
for (n = 0; n < 128; n++)
{
oled_write_byte(OLED_DATA, 0x00, 1);
}
}
}
void oled_set_pos(u8 x, u8 y)
{
oled_write_byte(OLED_CMD, 0xb0 + y, 1);
oled_write_byte(OLED_CMD, ((x & 0xf0) >> 4) | 0x10, 1);
oled_write_byte(OLED_CMD, x & 0x0f, 1);
}
void oled_showchar(u8 x, u8 y, u8 chr)
{
u8 c = 0, i = 0;
c = chr - ' ';
if (x > Max_Column - 1)
{
x = 0;
y = y + 2;
}
oled_set_pos(x, y);
for (i = 0; i < 8; i++)
{
oled_write_byte(OLED_DATA, F8X16[c * 16 + i], 1);
}
oled_set_pos(x, y + 1);
for (i = 0; i < 8; i++)
{
oled_write_byte(OLED_DATA, F8X16[c * 16 + i + 8], 1);
}
}
void oled_showstring(u8 x, u8 y, u8 *chr)
{
unsigned char j = 0;
while (chr[j] != '\0')
{
oled_showchar(x, y, chr[j]);
x += 8;
if (x > 120)
{
x = 0;
y += 2;
}
j++;
}
}
static int oled_open(struct inode *inode, struct file *filp)
{
oled_init();
oled_clear();
printk("oled_init ");
oled_showstring(0,2,"hello Linux iic");
printk("oled_init success");
return 0;
}
struct display_stru
{
//IIC从用户控件接收的数据格式
int x;
int y;
char *buf;
};
static ssize_t oled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off)
{
int ret;
struct display_stru dis_format;
ret = copy_from_user(&dis_format, buf, cnt);
printk("dis_format.x = %d \r\n", dis_format.x);
//调试打印,用于观察从用户控件获取的数据是否正确,可删除
printk("dis_format.y = %d \r\n", dis_format.y);
printk("dis_format.buf = %s \r\n", dis_format.buf);
oled_showstring(dis_format.x, dis_format.y, dis_format.buf);
return 0;
}
static int oled_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* Ops操作函数 */
static const struct file_operations oled_ops = {
.owner = THIS_MODULE,
.open = oled_open,
.read = oled_write,
.release = oled_release,
};
/*
* @description : i2c驱动的probe函数,当驱动与
* 设备匹配以后此函数就会执行
* @param - client : i2c设备
* @param - id : i2c设备ID
* @return : 0,成功;其他负值,失败
*/
static int oled_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
/* 1、构建设备号 */
if (oledcdev.major) {
oledcdev.devid = MKDEV(oledcdev.major, 0);
//MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号
register_chrdev_region(oledcdev.devid, OLED_CNT, OLED_NAME);
//参数 from 是要申请的起始设备号,也就是给定的设备号;
//参数 count 是要申请的数量,一般都是一个;参数 name 是申请设备名字
} else {
alloc_chrdev_region(&oledcdev.devid, 0, OLED_CNT, OLED_NAME);
oledcdev.major = MAJOR(oledcdev.devid);
oledcdev.minor = MINOR(oledcdev.devid);
}
/* 2、注册设备 */
cdev_init(&oledcdev.cdev, &oled_ops);/* 初始化 cdev 结构体变量 */
//cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合
cdev_add(&oledcdev.cdev, oledcdev.devid, OLED_CNT);
/* 3、创建类用于自动生成设备节点 */
oledcdev.class = class_create(THIS_MODULE, OLED_NAME);
//默认参数、类名与申请的设备名字相同即可
if (IS_ERR(oledcdev.class)) {
return PTR_ERR(oledcdev.class);
}
/* 4、创建设备 */
oledcdev.device = device_create(oledcdev.class, NULL, oledcdev.devid, NULL, OLED_NAME);
if (IS_ERR(oledcdev.device)) {
return PTR_ERR(oledcdev.device);
}
oledcdev.private_data = client;
printk("match success");
return 0;
}
/*
* @description : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
* @param - client : i2c设备
* @return : 0,成功;其他负值,失败
*/
static int oled_remove(struct i2c_client *client)
{
/* 删除设备 */
cdev_del(&oledcdev.cdev);
unregister_chrdev_region(oledcdev.devid, 1);
/* 注销掉类和设备 */
device_destroy(oledcdev.class, oledcdev.devid);
class_destroy(oledcdev.class);
return 0;
}
/* 传统匹配方式ID列表 */
static const struct i2c_device_id oled_id[] = {
{"liqi,iic_oled", 0},
{}
};
/* 设备树匹配列表 */
static const struct of_device_id oled_of_match[] = {
{ .compatible = "liqi,iic_oled"},
{ /* Sentinel */ }
};
/* i2c驱动结构体 */
static struct i2c_driver oled_driver = {
.probe = oled_probe,
.remove = oled_remove,
.driver = {
.owner = THIS_MODULE,
.name = "liqi,iic_oled",
.of_match_table = oled_of_match,
},
.id_table = oled_id,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init liqi_oled_init(void)
{
int ret = 0;
ret = i2c_add_driver(&oled_driver);
return ret;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit liqi_oled_exit(void)
{
i2c_del_driver(&oled_driver);
}
module_init(liqi_oled_init);
module_exit(liqi_oled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("liqi");