Linux驱动BSP(I2C 驱动实验)

一、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");

五、测试

总结

联系

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值