CSDN仅用于增加百度收录权重,排版未优化,日常不维护。请访问:www.hceng.cn 查看、评论。
本博文对应地址: https://hceng.cn/2017/09/11/AM437x——I2C驱动/#more
记录AM437x的I2C适配器驱动和I2C设备驱动,以及去除am437x EEPROM验证ID功能。
1.I2C驱动
在Linux中,I2C驱动有I2C适配器驱动(就是SOC的I2C控制器驱动)和I2C设备驱动(就是I2C设备,比如AT24C256)。
本次写驱动的过程,先是利用内核自带的I2C适配器驱动写了I2C设备驱动,待I2C设备驱动验证好了,再写的I2C适配器驱动。因此记录的顺序也是先是I2C设备驱动,再是I2C适配器驱动。
2.去除am437x EEPROM验证ID功能
前面裸机的对EEPROM随意的测试读写,改变了里面的一些数据。
Ti发布的SDK需要在不更改代码的情况下运行在Ti不同的EVM上,所以Ti在代码中会添加对EVM板上EEPROM内部ID的判断。
这就尴尬了,没人给我说过不能改啊 = = 。验证ID的数据被我改了,现在Uboot没法引导内核了。
= = 没办法,要么改回EEPROM的数据,要么去除Uboot的验证,权衡后,选择了后者。
通过查阅和测试,相关代码在:
~/ti-processor-sdk-linux-am437x-evm-01.00.00.03/board-support/u-boot-2014.07+gitAUTOINC+fb6ab76dad-gfb6ab76/board/ti/am43xx/
路径下的board.c
和board.h
。
修改原理是,屏蔽掉去读eeprom的数据,手动指定板子和版本信息。
在read_eeprom()
函数中,注释掉原来的验证,在后面的strncpy()
函数中指定板子信息。
修改后该部分代码如下:
{% codeblock lang:c%}
/*
-
Read header information from EEPROM into global structure.
*/
static int read_eeprom(struct am43xx_board_id header)
{
#if 0
/ Check if baseboard eeprom is available */
if (i2c_probe(CONFIG_SYS_I2C_EEPROM_ADDR)) {
printf(“Could not probe the EEPROM at 0x%x\n”,
CONFIG_SYS_I2C_EEPROM_ADDR);
return -ENODEV;
}/* read the eeprom using i2c */
if (i2c_read(CONFIG_SYS_I2C_EEPROM_ADDR, 0, 2, (uchar *)header,
sizeof(struct am43xx_board_id))) {
printf(“Could not read the EEPROM\n”);
return -EIO;
}if (header->magic != 0xEE3355AA) {
/*
* read the eeprom using i2c again,
* but use only a 1 byte address
*/
if (i2c_read(CONFIG_SYS_I2C_EEPROM_ADDR, 0, 1, (uchar *)header,
sizeof(struct am43xx_board_id))) {
printf(“Could not read the EEPROM at 0x%x\n”,
CONFIG_SYS_I2C_EEPROM_ADDR);
return -EIO;
}if (header->magic != 0xEE3355AA) { printf("Incorrect magic number (0x%x) in EEPROM\n", header->magic); return -EINVAL; }
}
#endif//strncpy(am43xx_board_name, (char *)header->name, sizeof(header->name));
strncpy(am43xx_board_name, “AM43__SK”, sizeof(header->name));
am43xx_board_name[sizeof(header->name)] = 0;//strncpy(am43xx_board_rev, (char *)header->version, sizeof(header->version));
strncpy(am43xx_board_rev, “1.2”,sizeof(header->version));
am43xx_board_rev[sizeof(header->version)] = 0;return 0;
}
{% endcodeblock %}
修改后,重新编译Uboot,重新下载,又能愉快启动了。
3.I2C设备驱动
3.1 编写I2C设备驱动的四种方法
在内核linux-3.14.43+gitAUTOINC+875c69b2c3-g875c69b\Documentation\i2c\instantiating-devices
里面介绍了How to instantiate I2C devices
。
说明了I2C设备驱动有以下四种编写方式:
Method 1:
Method 1a: Declare the I2C devices by bus number
Method 1b: Declare the I2C devices via devicetree
Method 1c: Declare the I2C devices via ACPI
Method 2: Instantiate the devices explicitly
Method 3: Probe an I2C bus for certain devices
Method 4: Instantiate from user-space
- Method 1a中,先定义一个板载信息结构体
i2c_board_info
,然后通过i2c_register_board_info()
注册板载信息。
然后i2c_board_info
就被放在了i2c_board_list
链表中。
但调用i2c_ragister_adapter()
时,会使用i2c_scan_static_board_info
对i2c_board_list
进行扫描,调用i2c_new_device()
创建client。
该方式的i2c_scan_static_board_info
在i2c_ragister_adapter()
之前,不适合动态加载(insmod方式)。
-
Method 1b中,就是把资源信息,以设备树的方式提供,换汤不换药。
-
Method 1c中,ACPI不懂,跳过。
-
Method 2中,翻译的意思是 明确实例化设备。还是先定义一个板载信息结构体
i2c_board_info
,然后通过i2c_new_device()
或i2c_new_probed_device()
去创建client。这两个函数前者认为设备一定存在,根据i2c_board_info
的地址创建设备。而i2c_new_probed_device()
则会先检查i2c_board_info
描述的地址设备是否真的存在,只有存在才会创建client。
本次I2C设备驱动就是用的这种方式。 -
Method 3中,不需要事先确定适配器,内核不太推荐该方法,跳过。
-
Method 4,这个就NB了,直接从用户层创建I2C设备。但需要下个
i2c-tools
使用里面的头文件i2c-dev.h
,才好在应用程序中,调用i2c_smbus_read_word_data
进行访问。感觉统一了驱动,减少了驱动的工作量,但稍微加大了应用程序设计的工作量。这里我没使用,以后遇到了再说。
从用户空间创建设备的方法:
//创建
echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-03/new_device
//删除
# echo 0x50 > /sys/bus/i2c/devices/i2c-0/new_device
3.2 采用Method 2编写驱动
采用设备平台驱动模型编写。
- 首先分析
at24cxx_dev.c
,按照前面介绍的步骤:
1.先定义一个板载信息结构体i2c_board_info
:
static struct i2c_board_info at24cxx_info = {
I2C_BOARD_INFO("at24c256", 0x50),
};
2.通过i2c_new_device()
创建client:
static int at24cxx_dev_init(void)
{
struct i2c_adapter *i2c_adap;
printk(KERN_INFO"%s OK.\n",__func__);
i2c_adap = i2c_get_adapter(0);
at24cxx_client = i2c_new_device(i2c_adap, &at24cxx_info);
i2c_put_adapter(i2c_adap);
return 0;
}
这里还通过i2c_get_adapter()
获取了适配器0(AM437X的I2C控制器0),然后调用i2c_put_adapter
再将其挂起。
现在I2C设备就有了。
- 再分析
at24cxx_drv.c
:
1.分配/设置一个i2c_driver
:
static struct i2c_driver at24cxx_driver = {
.driver = {
.name = "ti_i2c0",
.owner = THIS_MODULE,
},
.probe = at24cxx_probe,
.remove = at24cxx_remove,
.id_table = at24cxx_id_table,
};
name无所谓,这里还有probe()
和remove()
函数,分别在模块加载和卸载时调用。这里的probe()
加载,必须要id_table
里面的设备名字与i2c_board_info
相同才能加载。
2.注册i2c_driver
:
i2c_add_driver(&at24cxx_driver);
3.注册字符设备:
待id_table
里面的设备名字与i2c_board_info
相同,调用probe()
函数。
probe()
函数进行字符设备注册那一套操作。
3.1申请设备号:
if(alloc_chrdev_region(&devid, 0, TI_EEPROM_CNT, "at24cxx") < 0)
{
printk(KERN_INFO"%s ERROR.\n",__func__);
goto error;
}
major = MAJOR(devid);
3.2绑定操作函数、创建类、创建设备:
cdev_init(&at24cxx_cdev, &at24cxx_fops);
cdev_add(&at24cxx_cdev, devid, TI_EEPROM_CNT);
at24cxx_cls = class_create(THIS_MODULE, "ti_at24cxx");
device_create(at24cxx_cls, NULL, MKDEV(major, 0), NULL, "at24cxx");
4.释放资源:
在卸载模块时,释放probe()
申请的资源:
for(i=0;i<TI_EEPROM_CNT;i++)
{
device_destroy(at24cxx_cls, MKDEV(major, i));
}
class_destroy(at24cxx_cls);
unregister_chrdev(major, "at24cxx");
5.完善操作函数:
这里只实现了EEPROM的随机读写。
先说写EEPROM,前面裸机的时候,写EEPROM是先发送一个8位的设备地址和写命令(这个控制器自动的,不管),然后是发送两个8位的数据地址,最后是一个8位要写的数据。
因此,用户层传进来的的参数是地址和数据,需要分解成两个8位地址和数据再发送,一共三个数据。
if (copy_from_user(&ker_buf, user_buf, count))
return -EFAULT;
send[0] = ker_buf[0]>>8;
send[1] = ker_buf[0];
send[2] = ker_buf[1];
if(i2c_master_send(at24cxx_client, send, 3))
return 2;
else
return -EIO;
再分析下写,裸机的时候对EEPROM写,需要先发送一个8位的设备地址和写命令(这个控制器自动的,不管),再发送两个数据8位地址,再发送一个8位的设备地址和读命令(这个控制器自动的,也不管),再读取8位数据。
因此,用户层传进来的是一个地址数据,需要分解成两个8位地址,发送出去,再接收一个8位数据。
if (copy_from_user(&addr, user_buf, count))
return -EFAULT;
send[0] = addr>>8;
send[1] = addr;
i2c_master_send(at24cxx_client, send, 2);
i2c_master_recv(at24cxx_client, &data, 1);
ret = copy_to_user(user_buf, &data, 1);
- 最后看看应用程序
app_at24cxx.c
:
读取EEPROM格式是./app_at24cxx r addr
,写EEPROM格式是./app_at24cxx w addr val
。
1.先判断传入的参数:
if ((argc != 3) && (argc != 4))
{
print_usage(argv[0]);
return -1;
}
2.打开EEPROM设备:
fd = open("/dev/at24cxx", O_RDWR);
3.判断如果是读操作:
if (strcmp(argv[1], "r") == 0)
{
buf[0] = strtoul(argv[2], NULL, 0);
read(fd, buf, 1);
printf("data: %c, %d, 0x%2x\n", buf[0], buf[0], buf[0]);
}
strcmp()
比较字符串,strtoul()
将字符串转换成整型。
4.判断如果是写操作:
else if ((strcmp(argv[1], "w") == 0) && (argc == 4))
{
buf[0] = strtoul(argv[2], NULL, 0);
buf[1] = strtoul(argv[3], NULL, 0);
if (write(fd, buf, 2) == 2)
printf("write ok, addr = 0x%02x, data = 0x%02x\n", buf[0], buf[1]);
else
printf("write err, addr = 0x%02x, data = 0x%02x\n", buf[0], buf[1]);
}
write()
的写成功返回的是实际写入的字节数,这个靠驱动的写函数的返回值。
3.3 完整代码
{% codeblock lang:c [at24cxx_dev.c] https://github.com/hceng/am437x/blob/master/drive/3th_i2c/v1.0/at24cxx_dev.c %}
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/regmap.h>
#include <linux/slab.h>
static struct i2c_board_info at24cxx_info = {
I2C_BOARD_INFO(“at24c256”, 0x50),
};
static struct i2c_client *at24cxx_client;
static int at24cxx_dev_init(void)
{
struct i2c_adapter *i2c_adap;
printk(KERN_INFO"%s OK.\n",__func__);
i2c_adap = i2c_get_adapter(0);
at24cxx_client = i2c_new_device(i2c_adap, &at24cxx_info);
i2c_put_adapter(i2c_adap);
return 0;
}
static void at24cxx_dev_exit(void)
{
printk(KERN_INFO"%s OK.\n",func);
i2c_unregister_device(at24cxx_client);
}
module_init(at24cxx_dev_init);
module_exit(at24cxx_dev_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“hceng huangcheng.job@foxmail.com”);
MODULE_DESCRIPTION(“TI am437x board i2c device to at24c256”);
MODULE_ALIAS(“platform:device tree:ti_i2c”);
MODULE_VERSION(“V1.0”);
{% endcodeblock %}
{% codeblock lang:c [at24cxx_drv.c] https://github.com/hceng/am437x/blob/master/drive/3th_i2c/v1.0/at24cxx_drv.c %}
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/regmap.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#define TI_EEPROM_CNT 1
static int major;
static struct cdev at24cxx_cdev;
static struct class *at24cxx_cls;
static struct i2c_client *at24cxx_client;
/*
-
传入: buf[0] : addr
-
输出: buf[0] : data
*/
static ssize_t at24cxx_read(struct file * file, char __user *user_buf, size_t count, loff_t *off)
{
unsigned char addr, data;
char send[2];
int ret;if(count != 1){
printk(KERN_INFO"at24cxx_read count != 1.\n");
return 1;
}if (copy_from_user(&addr, user_buf, count))
return -EFAULT;send[0] = addr>>8;
send[1] = addr;i2c_master_send(at24cxx_client, send, 2);
i2c_master_recv(at24cxx_client, &data, 1);
ret = copy_to_user(user_buf, &data, 1);
return 1;
}
/* buf[0] : addr
-
buf[1] : data
*/
static ssize_t at24cxx_write(struct file *file, const char __user *user_buf, size_t count, loff_t *off)
{
unsigned char ker_buf[2];
char send[3];printk(KERN_INFO"%s OK.\n",func);
if(count != 2){
printk(KERN_INFO"at24cxx_write count != 2.\n");
return 1;
}if (copy_from_user(&ker_buf, user_buf, count))
return -EFAULT;//printk(“ker_buf[0]= 0x%02x, ker_buf[1]= x%02x\n”, ker_buf[0], ker_buf[1]);
send[0] = ker_buf[0]>>8;
send[1] = ker_buf[0];
send[2] = ker_buf[1];if(i2c_master_send(at24cxx_client, send, 3))
return 2;
else
return -EIO;
}
static struct file_operations at24cxx_fops = {
.owner = THIS_MODULE,
.read = at24cxx_read,
.write = at24cxx_write,
};
static int at24cxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
dev_t devid;
at24cxx_client = client;
printk(KERN_INFO"%s OK.\n",__func__);
//1.申请设备号
if(alloc_chrdev_region(&devid, 0, TI_EEPROM_CNT, "at24cxx") < 0)
{
printk(KERN_INFO"%s ERROR.\n",__func__);
goto error;
}
major = MAJOR(devid);
//2.注册到系统中
cdev_init(&at24cxx_cdev, &at24cxx_fops);
cdev_add(&at24cxx_cdev, devid, TI_EEPROM_CNT);
at24cxx_cls = class_create(THIS_MODULE, "ti_at24cxx");
device_create(at24cxx_cls, NULL, MKDEV(major, 0), NULL, "at24cxx");
error:
unregister_chrdev_region(MKDEV(major, 0), TI_EEPROM_CNT);
return 0;
}
static int at24cxx_remove(struct i2c_client *client)
{
unsigned int i;
printk(KERN_INFO"%s OK.\n",__func__);
for(i=0;i<TI_EEPROM_CNT;i++)
{
device_destroy(at24cxx_cls, MKDEV(major, i));
}
class_destroy(at24cxx_cls);
unregister_chrdev(major, "at24cxx");
return 0;
}
static const struct i2c_device_id at24cxx_id_table[] = {
{ “at24c256”, 0 },
{}
};
/* 1. 分配/设置i2c_driver */
static struct i2c_driver at24cxx_driver = {
.driver = {
.name = “ti_i2c0”,
.owner = THIS_MODULE,
},
.probe = at24cxx_probe,
.remove = at24cxx_remove,
.id_table = at24cxx_id_table,
};
static int at24cxx_drv_init(void)
{
/* 2. 注册i2c_driver */
printk(KERN_INFO"%s OK.\n",func);
i2c_add_driver(&at24cxx_driver);
return 0;
}
static void at24cxx_drv_exit(void)
{
printk(KERN_INFO"%s OK.\n",func);
i2c_del_driver(&at24cxx_driver);
}
module_init(at24cxx_drv_init);
module_exit(at24cxx_drv_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“hceng huangcheng.job@foxmail.com”);
MODULE_DESCRIPTION(“TI am437x board i2c drvice to at24c256”);
MODULE_ALIAS(“platform:device tree:ti_i2c”);
MODULE_VERSION(“V1.0”);
{% endcodeblock %}
{% codeblock lang:c [app_at24cxx.c] https://github.com/hceng/am437x/blob/master/drive/3th_i2c/v1.0/app_at24cxx.c %}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/*
- ./app_at24cxx r addr
- ./app_at24cxx w addr val
*/
void print_usage(char *file)
{
printf("%s r addr\n", file);
printf("%s w addr val\n", file);
}
int main(int argc, char **argv)
{
int fd;
unsigned char buf[2];
int x;
if ((argc != 3) && (argc != 4))
{
print_usage(argv[0]);
return -1;
}
fd = open("/dev/at24cxx", O_RDWR);
if (fd < 0)
{
printf("can't open /dev/at24cxx\n");
return -1;
}
if (strcmp(argv[1], "r") == 0)
{
buf[0] = strtoul(argv[2], NULL, 0);
read(fd, buf, 1);
printf("data: %c, %d, 0x%2x\n", buf[0], buf[0], buf[0]);
}
else if ((strcmp(argv[1], "w") == 0) && (argc == 4))
{
buf[0] = strtoul(argv[2], NULL, 0);
buf[1] = strtoul(argv[3], NULL, 0);
if (write(fd, buf, 2) == 2)
printf("write ok, addr = 0x%02x, data = 0x%02x\n", buf[0], buf[1]);
else
printf("write err, addr = 0x%02x, data = 0x%02x\n", buf[0], buf[1]);
}
else
{
print_usage(argv[0]);
return -1;
}
close(fd);
return 0;
}
{% endcodeblock %}
4.I2C适配器驱动
下面开始比较好玩的I2C适配器驱动。
去除内核的I2C适配器模块,删除设备树文件里相关的I2C资源信息。
4.1 编写I2C适配器驱动
I2C适配器驱动采用最原始的方法编写。
1.入口函数
入口函数做了四件事情:硬件相关设置、注册中断、初始化等待队列、注册I2C适配器。
static const struct i2c_algorithm am437x_i2c_algo = {
// .smbus_xfer = ,
.master_xfer = am437x_i2c_xfer,
.functionality = am437x_i2c_func,
};
static struct i2c_adapter am437x_i2c_adapter = {
.name = "i2c_adapter",
.algo = &am437x_i2c_algo,//i2c总线通信方法
.owner = THIS_MODULE,
};
static void i2c_init(void)
{
printk(KERN_INFO"%s OK.\n",__func__);
/*clk*/
PRCM_CM_WKUP_I2C0_CLKCTRL = ioremap(0x44DF2800+0x340, 0x04*1);
*PRCM_CM_WKUP_I2C0_CLKCTRL |= (0x02<<0);
PRCM_CM_WKUP_CLKSTCTRL = ioremap(0x44DF2800+0x300, 0x04*1);
*PRCM_CM_WKUP_CLKSTCTRL |= (0x01<<14);
PRCM_CM_PER_L4LS_CLKSTCTRL = ioremap(0x44DF8800+0x400, 0x04*1);
*PRCM_CM_PER_L4LS_CLKSTCTRL |= (0x01<<27);
/*GPIO:gpio3_5->I2C0_SDA;gpio3_6->I2C0_SCL*/
CTRL_CONF_I2C0_SDA = ioremap(0x44E10000+0x0988, 0x04*1);
CTRL_CONF_I2C0_SCL = ioremap(0x44E10000+0x098C, 0x04*1);
*CTRL_CONF_I2C0_SDA &= ~(0x07<<0 | 0x01<<16 | 0x01<<19);
*CTRL_CONF_I2C0_SCL &= ~(0x07<<0 | 0x01<<16 | 0x01<<19);
/*I2C0 set*/
I2C0 = ioremap(0x44E0B000, sizeof(struct am437x_i2c_regs));
I2C0->CON &= ~(0x01<<15);//reset
I2C0->SYSC &= ~(0x01<<0);//Auto Idle disabled.
I2C0->PSC = 3;//the module divided by (PSC + 1) -> 48M/(3+1)=12M
I2C0->SCLL = 63;//tLOW = (SCLL + 7) * ICLK time period
I2C0->SCLH = 65;//tHIGH = (SCLH + 5) * ICLK time period
//I2C0->SA = 0x50;//Slave address.x 1010 000
I2C0->CON |= (0x01<<15);//Module enabled
}
static int i2c_bus_drv_init(void)
{
printk(KERN_INFO"%s OK.\n",__func__);
//硬件相关的设置
i2c_init();
if (request_irq(102, i2c_xfer_irq, 0, "am437x-i2c", NULL))
return -EAGAIN;
init_waitqueue_head(&i2c_data.wait);
i2c_add_adapter(&am437x_i2c_adapter);//会在/sys/class/i2c-adapter下生成i2c-x
return 0;
}
硬件设置部分,初始化了相关时钟,设置GPIO复用,设置了I2C0控制器的一些设置。
这里设置直接用ioremap()映射寄存器进程操作,是很不对的,内核是个整体,相关之间有关联,理论上时钟部分内核已经封装好了,我现在只需要调用相关就行。但秉着先调通,再优化改进的思想,映射是最快,最方便的= =。
同理GPIO的设置和中断号也是不规范的。
i2c_add_adapter()
会调用i2c_register_adapter()
注册适配器,自动完成字符设备注册那一套。
绑定了结构体i2c_adapter
,i2c_adapter
里面有I2C总线通信方法i2c_algorithm
。i2c_algorithm
里面两个函数:
.master_xfer = am437x_i2c_xfer,
:传输函数;
.functionality = am437x_i2c_func,
:支持的协议;
I2C适配器驱动的关键就是实现这两个函数。
2.出口函数
与入口函数相反操作:
static void i2c_bus_drv_exit(void)
{
printk(KERN_INFO"%s OK.\n",__func__);
iounmap(CTRL_CONF_I2C0_SDA);
iounmap(CTRL_CONF_I2C0_SCL);
iounmap(PRCM_CM_WKUP_I2C0_CLKCTRL);
iounmap(PRCM_CM_WKUP_CLKSTCTRL);
iounmap(PRCM_CM_PER_L4LS_CLKSTCTRL);
iounmap(I2C0);
free_irq(102, NULL);
i2c_del_adapter(&am437x_i2c_adapter);
}
3.实现关键函数一:am437x_i2c_func()
虽说是关键函数,但这个是送分题,返回支持协议即可。
static u32 am437x_i2c_func(struct i2c_adapter *adap)
{
printk(KERN_INFO"%s OK.\n",__func__);
//用于返回总线支持的协议
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL | I2C_FUNC_PROTOCOL_MANGLING;
}
4.实现关键函数二:am437x_i2c_xfer()
先定义个传输数据的结构体:
struct am437x_i2c_xfer_data {
struct i2c_msg *msgs;
uint32_t cur_rec;
uint32_t cur_send;
uint32_t reg_cnt;
uint32_t err;
wait_queue_head_t wait;
};
static struct am437x_i2c_xfer_data i2c_data;
里面包含了i2c_msg
结构体、当前接收位置cur_rec
、当前发送位置cur_send
、CNT寄存器计数reg_cnt
、状态标志err
、唤醒队列wait
;
i2c_msg
结构体比较重要,包含了从机地址,发送/接收标志、数据长度和数据指针。
struct i2c_msg {
__u16 addr; // 从机地址
__u16 flags; // 标志
#define I2C_M_TEN 0x0010 // 十位地址标志
#define I2C_M_RD 0x0001 // 接收数据标志
__u16 len; // 数据长度
__u8 *buf; // 数据指针
};
前面at24cxx_drv.c
中调用发送i2c_master_send()
和接收i2c_master_recv()
时,就会调用这里的传输函数am437x_i2c_xfer()
。
先初始化相关变量:
i2c_data.msgs = msgs;
i2c_data.cur_rec = 0;
i2c_data.cur_send = 0;
i2c_data.err = -ENODEV;
I2C0->CNT = i2c_data.msgs->len;
i2c_data.reg_cnt = I2C0->CNT;
I2C0->SA = i2c_data.msgs->addr;
保存传入的i2c_msg
,设置当前接收位置cur_rec
和当前发送位置cur_send
为0,赋值给I2C0->CNT
寄存器传输长度,因为I2C0->CNT
在后面不好把握什么时候会清零,再将I2C0->CNT
赋值给reg_cnt
,作为要传输数据的总长度。再将设备地址传给寄存器。
接着开始传输:
static void am437x_i2c_start(void)
{
printk(KERN_INFO"%s OK.\n",__func__);
I2C0->IRQSTS |= 0x7FFF;
I2C0->IRQEN_CLR |= 0x7FFF;
if(i2c_data.msgs->flags & I2C_M_RD) //read
{
I2C0->CON |= (0x01<<10 | (0x01<<15)); //MST=1 TRX=0
I2C0->CON &= ~(0x01<<9);
I2C0->IRQEN_SET |= (0x01<<3);
}
else//write
{
I2C0->CON |= (0x01<<9 | 0x01<<10 | 0x01<<15); //MST=1 TRX=1
I2C0->IRQEN_SET |= (0x01<<4);
}
I2C0->CON |= (0x01<<0);
}
先清除所有中断,利用i2c_data.msgs->flags
判断时读还是写,设置对应模式和对应中断,最后启动传输。
随后进入休眠,中断就会根据情况发送/接收数据,待满足 当前传输长度等于总传输长度时,唤醒该函数,完成本次传输。
5.中断函数
前面am437x_i2c_start()
中设置I2C0->CON |= (0x01<<0);
后,AM437X的I2C控制器就会自动传输数据,等完成后,生成对应的标志位。
但发生中断后,进入中断函数,先清除除 接收标志和发送标志 以外的中断(更好的方式保留ACK响应标志和bus空闲标志,这里简化,暂不管)。
判断是接收完成还是发送完成,
如果是接收完成:
清除接收中断,保存I2C0->DATA
数据,同时,当前接收标志i2c_data.cur_rec
加1,直到 当前接收等于总传输,即可关闭中断,发送停止信号。
如果是发送完成:
清除发送中断,赋值I2C0->DATA
数据,同时,当前接收标志i2c_data.cur_send
加1,直到 当前发送等于总传输,即可关闭中断,发送停止信号。
static irqreturn_t i2c_xfer_irq(int irq, void *dev_id)
{
unsigned int status = 0;
printk(KERN_INFO"%s OK.\n",__func__);
status = I2C0->IRQSTS;
//I2C0->IRQSTS = (status & (0x01<<1 |0x01<<3 | 0x01<<4));
I2C0->IRQSTS = (status & (0x01<<3 | 0x01<<4));
if(status & (0x01<<3))//receive
{
//printk(KERN_INFO"i2c receive.\n");
I2C0->IRQSTS |= (0x01<<3);
i2c_data.msgs->buf[i2c_data.cur_rec] = I2C0->DATA;
i2c_data.cur_rec++;
if(i2c_data.cur_rec == i2c_data.reg_cnt)
{
I2C0->IRQEN_CLR |= (0x01<<3);
am437x_i2c_stop(1);
}
}
if (status & (0x01<<4))//send
{
//printk(KERN_INFO"i2c send.\n");
I2C0->IRQSTS |= (0x01<<4);
I2C0->DATA = i2c_data.msgs->buf[i2c_data.cur_send];
i2c_data.cur_send++;
if(i2c_data.cur_send == i2c_data.reg_cnt)
{
I2C0->IRQEN_CLR |= (0x01<<4);
am437x_i2c_stop(1);
}
}
//判断是否有ack和bus空闲
I2C0->IRQSTS |= 0x7FFF;
return IRQ_HANDLED;
}
在am437x_i2c_stop()
里,除了要发送停止信号,还要唤醒队列。
static void am437x_i2c_stop(int err)
{
printk(KERN_INFO"%s OK.\n",__func__);
i2c_data.err = err;
I2C0->CON |= (0x01<<1); //stop
ndelay(50);
/*唤醒*/
wake_up(&i2c_data.wait);
}
这就完了,还是比较清晰的。
4.2 完整代码
{% codeblock lang:c [i2c_bus_am437x.c] https://github.com/hceng/am437x/blob/master/drive/3th_i2c/v2.0/i2c_bus_am437x.c %}
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/time.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/err.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <linux/clk.h>
#include <linux/cpufreq.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <asm/irq.h>
struct am437x_i2c_xfer_data {
struct i2c_msg *msgs;
uint32_t cur_rec;
uint32_t cur_send;
uint32_t reg_cnt;
uint32_t err;
wait_queue_head_t wait;
};
static struct am437x_i2c_xfer_data i2c_data;
static volatile unsigned long *CTRL_CONF_I2C0_SDA;
static volatile unsigned long *CTRL_CONF_I2C0_SCL;
static volatile unsigned long *PRCM_CM_WKUP_I2C0_CLKCTRL;
static volatile unsigned long *PRCM_CM_WKUP_CLKSTCTRL;
static volatile unsigned long *PRCM_CM_PER_L4LS_CLKSTCTRL;
struct am437x_i2c_regs {
uint32_t REVNB_LO; //00h
uint32_t REVNB_HI; //04h
uint32_t RESERVED0[2]; //08h
uint32_t SYSC; //10h
uint32_t RESERVED1[4]; //14h
uint32_t IRQSTS_RAW; //24h
uint32_t IRQSTS; //28h
uint32_t IRQEN_SET; //2ch
uint32_t IRQEN_CLR; //30h
uint32_t WE; //34h
uint32_t DMARXEN_SET; //38h
uint32_t DMATXEN_SET; //3ch
uint32_t DMARXEN_CLR; //40h
uint32_t DMATXEN_CLR; //44h
uint32_t DMARXWAKE_EN; //48h
uint32_t DMATXWAKE_EN; //4ch
uint32_t RESERVED2[16]; //50h
uint32_t SYSS; //90h
uint32_t BUF; //94h
uint32_t CNT; //98h
uint32_t DATA; //9ch
uint32_t RESERVED3; //a0h
uint32_t CON; //a4h
uint32_t OA; //a8h
uint32_t SA; //ach
uint32_t PSC; //b0h
uint32_t SCLL; //b4h
uint32_t SCLH; //b8h
uint32_t SYSTEST; //bch
uint32_t BUFSTAT; //c0h
uint32_t OA1; //c4h
uint32_t OA2; //c8h
uint32_t OA3; //cch
uint32_t ACTOA; //d0h
};
static volatile struct am437x_i2c_regs *I2C0;
static void am437x_i2c_start(void)
{
printk(KERN_INFO"%s OK.\n",func);
I2C0->IRQSTS |= 0x7FFF;
I2C0->IRQEN_CLR |= 0x7FFF;
if(i2c_data.msgs->flags & I2C_M_RD) //read
{
I2C0->CON |= (0x01<<10 | (0x01<<15)); //MST=1 TRX=0
I2C0->CON &= ~(0x01<<9);
I2C0->IRQEN_SET |= (0x01<<3);
}
else//write
{
I2C0->CON |= (0x01<<9 | 0x01<<10 | 0x01<<15); //MST=1 TRX=1
I2C0->IRQEN_SET |= (0x01<<4);
}
I2C0->CON |= (0x01<<0);
}
static void am437x_i2c_stop(int err)
{
printk(KERN_INFO"%s OK.\n",func);
i2c_data.err = err;
mdelay(2);//为了防止读EEPROM的时候,写操作后,马上就读操作,导致错误;
I2C0->CON |= (0x01<<1); //stop
ndelay(50);//等待停止信号发完
/*唤醒*/
wake_up(&i2c_data.wait);
}
static irqreturn_t i2c_xfer_irq(int irq, void *dev_id)
{
unsigned int status = 0;
printk(KERN_INFO"%s OK.\n",__func__);
status = I2C0->IRQSTS;
//I2C0->IRQSTS = (status & (0x01<<1 |0x01<<3 | 0x01<<4));
I2C0->IRQSTS = (status & (0x01<<3 | 0x01<<4));
if(status & (0x01<<3))//receive
{
//printk(KERN_INFO"i2c receive.\n");
I2C0->IRQSTS |= (0x01<<3);
i2c_data.msgs->buf[i2c_data.cur_rec] = I2C0->DATA;
i2c_data.cur_rec++;
if(i2c_data.cur_rec == i2c_data.reg_cnt)
{
I2C0->IRQEN_CLR |= (0x01<<3);
am437x_i2c_stop(1);
}
}
if (status & (0x01<<4))//send
{
//printk(KERN_INFO"i2c send.\n");
I2C0->IRQSTS |= (0x01<<4);
I2C0->DATA = i2c_data.msgs->buf[i2c_data.cur_send];
i2c_data.cur_send++;
if(i2c_data.cur_send == i2c_data.reg_cnt)
{
I2C0->IRQEN_CLR |= (0x01<<4);
am437x_i2c_stop(1);
}
}
//判断是否有ack和bus空闲
I2C0->IRQSTS |= 0x7FFF;
return IRQ_HANDLED;
}
static int am437x_i2c_xfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num)
{
//用于实现I2C协议,将i2c_msg消息传给i2c设备
unsigned long timeout;
printk(KERN_INFO"%s OK.\n",__func__);
//把num个i2c_msg的I2C数据发送出去/读进来
i2c_data.msgs = msgs;
i2c_data.cur_rec = 0;
i2c_data.cur_send = 0;
i2c_data.err = -ENODEV;
I2C0->CNT = i2c_data.msgs->len;
i2c_data.reg_cnt = I2C0->CNT;
I2C0->SA = i2c_data.msgs->addr;
am437x_i2c_start();
/*休眠*/
if(i2c_data.msgs->flags & I2C_M_RD) //读
{
timeout = wait_event_timeout(i2c_data.wait,(i2c_data.cur_rec == i2c_data.reg_cnt), HZ * 5);//5S
}
else
{
timeout = wait_event_timeout(i2c_data.wait, (i2c_data.cur_send == i2c_data.reg_cnt), HZ * 5);//5S
}
if(0 == timeout)
{
printk("am437x i2c timeout.\n");
return -ETIMEDOUT;
}
else
{
return i2c_data.err;
}
}
static u32 am437x_i2c_func(struct i2c_adapter *adap)
{
printk(KERN_INFO"%s OK.\n",func);
//用于返回总线支持的协议
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL | I2C_FUNC_PROTOCOL_MANGLING;
}
static const struct i2c_algorithm am437x_i2c_algo = {
// .smbus_xfer = ,
.master_xfer = am437x_i2c_xfer,
.functionality = am437x_i2c_func,
};
static struct i2c_adapter am437x_i2c_adapter = {
.name = “i2c_adapter”,
.algo = &am437x_i2c_algo,//i2c总线通信方法
.owner = THIS_MODULE,
};
static void i2c_init(void)
{
printk(KERN_INFO"%s OK.\n",func);
/*clk*/
PRCM_CM_WKUP_I2C0_CLKCTRL = ioremap(0x44DF2800+0x340, 0x04*1);
*PRCM_CM_WKUP_I2C0_CLKCTRL |= (0x02<<0);
PRCM_CM_WKUP_CLKSTCTRL = ioremap(0x44DF2800+0x300, 0x04*1);
*PRCM_CM_WKUP_CLKSTCTRL |= (0x01<<14);
PRCM_CM_PER_L4LS_CLKSTCTRL = ioremap(0x44DF8800+0x400, 0x04*1);
*PRCM_CM_PER_L4LS_CLKSTCTRL |= (0x01<<27);
/*GPIO:gpio3_5->I2C0_SDA;gpio3_6->I2C0_SCL*/
CTRL_CONF_I2C0_SDA = ioremap(0x44E10000+0x0988, 0x04*1);
CTRL_CONF_I2C0_SCL = ioremap(0x44E10000+0x098C, 0x04*1);
*CTRL_CONF_I2C0_SDA &= ~(0x07<<0 | 0x01<<16 | 0x01<<19);
*CTRL_CONF_I2C0_SCL &= ~(0x07<<0 | 0x01<<16 | 0x01<<19);
/*I2C0 set*/
I2C0 = ioremap(0x44E0B000, sizeof(struct am437x_i2c_regs));
I2C0->CON &= ~(0x01<<15);//reset
I2C0->SYSC &= ~(0x01<<0);//Auto Idle disabled.
I2C0->PSC = 3;//the module divided by (PSC + 1) -> 48M/(3+1)=12M
I2C0->SCLL = 63;//tLOW = (SCLL + 7) * ICLK time period
I2C0->SCLH = 65;//tHIGH = (SCLH + 5) * ICLK time period
//I2C0->SA = 0x50;//Slave address.x 1010 000
I2C0->CON |= (0x01<<15);//Module enabled
}
static int i2c_bus_drv_init(void)
{
printk(KERN_INFO"%s OK.\n",func);
//硬件相关的设置
i2c_init();
if (request_irq(102, i2c_xfer_irq, 0, "am437x-i2c", NULL))
return -EAGAIN;
init_waitqueue_head(&i2c_data.wait);
i2c_add_adapter(&am437x_i2c_adapter);//会在/sys/class/i2c-adapter下生成i2c-x
return 0;
}
static void i2c_bus_drv_exit(void)
{
printk(KERN_INFO"%s OK.\n",func);
iounmap(CTRL_CONF_I2C0_SDA);
iounmap(CTRL_CONF_I2C0_SCL);
iounmap(PRCM_CM_WKUP_I2C0_CLKCTRL);
iounmap(PRCM_CM_WKUP_CLKSTCTRL);
iounmap(PRCM_CM_PER_L4LS_CLKSTCTRL);
iounmap(I2C0);
free_irq(102, NULL);
i2c_del_adapter(&am437x_i2c_adapter);
}
module_init(i2c_bus_drv_init);
module_exit(i2c_bus_drv_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“hceng huangcheng.job@foxmail.com”);
MODULE_DESCRIPTION(“TI am437x board i2c bus drvice.”);
MODULE_ALIAS(“ti_i2c”);
MODULE_VERSION(“V2.0”);
{% endcodeblock %}
5.测试结果
加载i2c0总线驱动,再加载at24cxx设备驱动,最后运行应用程序。
写1地址1,2地址2,3地址3,4地址4; 再读取1地址数据,2地址数据,3地址数据,4地址数据。
再看下逻辑分析仪效果:
向地址0x02写入数据2:
向地址0x02读出数据2:
(PS:图片经过处理,让其在一张图上显示,因此时间标识有误。)
6.心得
本次I2C适配器驱动,算是第一次写一个较复杂的驱动,目的是先写通,其次才是优化,因此存在以下一些问题:
1.没有使用系统提供的时钟、GPIO配置等函数,全是自己映射;
2.中断没有对总线空闲、没有ack进行判断;
3.有些函数返回值没有处理;
收获:
1.先调通再优化,现在调通了,优化等下次有机会在优化;
2.调不动的时候,拿逻辑分析仪抓,再对照标准波形,一找一个准;
在写博客的时候,发现个BUG:
printk()打印调试的时候,一切正常。
在关闭printk()打印后,先对EEPROM写操作,再去读操作,发现怎么都是255。
这种情况,多半是哪里延时时序问题。
用逻辑分析仪抓波形,一个个printk()的关,最终发现问题在am437x_i2c_stop()。
波形如下:
可以看到地址发错了,加上一个2ms的延时后,即正常。
I2C驱动完了,下一个计划,LCD驱动。?
7. i2c-tools的使用
i2c-tools是Linux应用层,用于对i2c总线、设备进行简单测试、操作的工具。
包含的工具有:
i2cdetect – 用来列举I2C Bus上的所有设备
i2cdump – 显示指定I2C设备的寄存器值
i2cget – 读取指定I2C设备的某个寄存器的值
i2cset – 写入指定I2C设备的某个寄存器的值
eg:
- 1.用i2cdetect检测有几组i2c总线在系统上:
# i2cdetect -l
i2c-0 i2c OMAP I2C adapter I2C adapter
i2c-1 i2c OMAP I2C adapter I2C adapter
- 2.用i2cdetect检测挂载在i2c总线上器件:
# i2cdetect -r -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- UU -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
可以看到0x1b
和0x50
地址上挂有有I2C设备;
UU
的代表该设备有被侦测到并正在被内核驱动器使用着;
- 3.用i2cdump查看器件所有寄存器的值:
# i2cdump -f -y 1 0x50
No size specified (using byte-data access)
0 1 2 3 4 5 6 7 8 9 a b c d e f 0123456789abcdef
00: 00 08 04 06 08 ff ff ff ff ff ff ff ff ff ff ff .????...........
10: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
20: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
30: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
40: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
50: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
60: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
70: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
80: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
90: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
a0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
b0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
c0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
d0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
e0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
f0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
- 3.用i2cset设置单个寄存器值,用i2cget读取单个寄存器值:
# i2cget -f -y 1 0x50 0x01
0x08
# i2cset -f -y 1 0x50 0x01 0x44
# i2cget -f -y 1 0x50 0x01
0x44