1 概述
Microchip公司的24AA16/24LC16B (24XX16*)是一个16kbit的电可擦PROM。该设备由8块(共256×8位)内存和2线串行接口组成。低压设计允许运行到1.7 v。24XX16还具有最多16字节数据的页写入功能。
我们使用的24LC16封装类型如下图所示:
2 引脚连接及I2C从器件地址、时序
2.1 引脚连接
使用的硬件平台: Atmel SAMA5d4,Cortex-A5处理器。
EEPROM 24LC16芯片与处理器引脚连接关系如下,其中地址位A0,A1,A2均置低电平:
序号 | 24LC16芯片引脚定义 | 处理器引脚定义 |
---|---|---|
1 | SCL | PE30 |
2 | SDL | PE29 |
在A5处理器芯片手册中查找处理器引脚(PE30、PE29)相关的信息。可知,A5处理器有4条I2C总线,EEPROM被挂载到总线TWI1(i2c1)下。
2.2 I2C从器件地址、时序
阅读EEPROM的芯片手册,查看从器件地址以及I2C读写时序。
IIC总线是采用两线式串行通讯接口,分别为SDA数据线以及SCL时钟信号,其中每个I2C设备有个7位的从设备地址,7位的地址中前4位是设备识别地址(根据上图可知为:1010),后面3位为地址位,由电路上决定 (B0~B2对应2.1节的A0-A2,可知全部为0,即后3位为:000),由以上分析可知,24LC16 EEPROM的从设备地址为(0b1010000, 0x50)。
最低位为读写标示位(R/W),写为0,读为1,整体8位来描述一个设备,也就是说相同的器件(前四位相同)最多只能有7个。所以可以确定24LC16的写地址为 0xA0(0b10100000)、读地址为0xA1(0x10100001)。
下图为写的过程时序图,具体描述可以阅读datasheet。
3 驱动调试
3.1 I2C子系统
Linux下的I2C子系统架构如下图。
可以看到在Linux下可以用两种方式来操作i2c。一种是透过/dev 下的设备文件直接透过i2c设备驱动来操作。另外一种是透过用户态驱动程序使用通用驱动 i2c-dev来操作。
3.2 驱动调试过程
3.2.1 使用内核源代码提供的at24.c驱动
在linux内核里已经实现了i2c接口的eeprom设备驱动,我们直接使用内核源代码提供的at24.c驱动来操作。下面进行介绍。
- 内核配置选项在:
make menuconfig
Device Drivers —>
Misc devices —>
EEPROM support —>
<*> I2C EEPROMs from most vendors - Driver驱动源码
使用内核源代码提供的at24.c驱动来操作I2C的24LC16 EEPROM。drivers/misc/eeprom/at24.c文件支持大多数I2C接口的EEPROM,一个具体的I2C设备驱动由i2c_driver的形式进行组织,用于将设备挂载于I2C总线,组织好之后,再完成设备本身所属类型的驱动。对于EEPROM而言,设备本身的驱动以二进制sysfs节点形式呈现(我们的板子上节点路径:/sys/bus/i2c/devices/1-0050/eeprom)。
以下代码给出了该驱动的框架。文件at24.c,逐步分析。
首先看at24.c驱动支持的设备。
static const struct i2c_device_id at24_ids[] = {
/* needs 8 addresses as A0-A2 are ignored */
{ "24c00", AT24_DEVICE_MAGIC(128 / 8, AT24_FLAG_TAKE8ADDR) },
/* old variants can't be handled with this generic entry! */
{ "24c01", AT24_DEVICE_MAGIC(1024 / 8, 0) },
{ "24cs01", AT24_DEVICE_MAGIC(16,
AT24_FLAG_SERIAL | AT24_FLAG_READONLY) },
{ "24c02", AT24_DEVICE_MAGIC(2048 / 8, 0) },
{ "24cs02", AT24_DEVICE_MAGIC(16,
AT24_FLAG_SERIAL | AT24_FLAG_READONLY) },
{ "24mac402", AT24_DEVICE_MAGIC(48 / 8,
AT24_FLAG_MAC | AT24_FLAG_READONLY) },
{ "24mac602", AT24_DEVICE_MAGIC(64 / 8,
AT24_FLAG_MAC | AT24_FLAG_READONLY) },
/* spd is a 24c02 in memory DIMMs */
{ "spd", AT24_DEVICE_MAGIC(2048 / 8,
AT24_FLAG_READONLY | AT24_FLAG_IRUGO) },
{ "24c04", AT24_DEVICE_MAGIC(4096 / 8, 0) },
{ "24cs04", AT24_DEVICE_MAGIC(16,
AT24_FLAG_SERIAL | AT24_FLAG_READONLY) },
/* 24rf08 quirk is handled at i2c-core */
{ "24c08", AT24_DEVICE_MAGIC(8192 / 8, 0) },
{ "24cs08", AT24_DEVICE_MAGIC(16,
AT24_FLAG_SERIAL | AT24_FLAG_READONLY) },
{ "24c16", AT24_DEVICE_MAGIC(16384 / 8, 0) }, // here
{ "24cs16", AT24_DEVICE_MAGIC(16,
AT24_FLAG_SERIAL | AT24_FLAG_READONLY) },
{ "24c32", AT24_DEVICE_MAGIC(32768 / 8, AT24_FLAG_ADDR16) },
{ "24cs32", AT24_DEVICE_MAGIC(16,
AT24_FLAG_ADDR16 |
AT24_FLAG_SERIAL |
AT24_FLAG_READONLY) },
{ "24c64", AT24_DEVICE_MAGIC(65536 / 8, AT24_FLAG_ADDR16) },
{ "24cs64", AT24_DEVICE_MAGIC(16,
AT24_FLAG_ADDR16 |
AT24_FLAG_SERIAL |
AT24_FLAG_READONLY) },
{ "24c128", AT24_DEVICE_MAGIC(131072 / 8, AT24_FLAG_ADDR16) },
{ "24c256", AT24_DEVICE_MAGIC(262144 / 8, AT24_FLAG_ADDR16) },
{ "24c512", AT24_DEVICE_MAGIC(524288 / 8, AT24_FLAG_ADDR16) },
{ "24c1024", AT24_DEVICE_MAGIC(1048576 / 8, AT24_FLAG_ADDR16) },
{ "at24", 0 },
{ /* END OF LIST */ }
};
MODULE_DEVICE_TABLE(i2c, at24_ids);
我们使用的Microchip公司的24LC16和Atmel公司的at24c16存储及操作基本是一样的。在i2c_device_id结构中,有对24c16的描述。有一项{ “24c16”, AT24_DEVICE_MAGIC(16384/ 8, 0) }是EEPROM at24c16的id项,其中AT24_DEVICE_MAGIC 是在求at24c16这个设备独一无二的魔幻数,at24c16有 16384bits,所以16384/8的意思是2048个字节。
初始化代码
static struct i2c_driver at24_driver = {
.driver = {
.name = "at24",
.acpi_match_table = ACPI_PTR(at24_acpi_ids),
},
.probe = at24_probe,
.remove = at24_remove,
.id_table = at24_ids,
};
static int __init at24_init(void)
{
if (!io_limit) {
pr_err("at24: io_limit must not be 0!\n");
return -EINVAL;
}
io_limit = rounddown_pow_of_two(io_limit);
return i2c_add_driver(&at24_driver);
}
module_init(at24_init);
static void __exit at24_exit(void)
{
i2c_del_driver(&at24_driver);
}
module_exit(at24_exit);
可以看到这是采用总线的驱动,总线的过程是当有设备加入总线,会调用驱动的 probe 方法来捕获设备,驱动已经实现了probe 方法。
static int at24_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct at24_platform_data chip;
kernel_ulong_t magic = 0;
bool writable;
int use_smbus = 0;
int use_smbus_write = 0;
struct at24_data *at24;
int err;
unsigned i, num_addresses;
u8 test_byte;
//i2c设备可以在platform_data里提供eeprom芯片的信息
if (client->dev.platform_data) {
chip = *(struct at24_platform_data *)client->dev.platform_data;
} else {
if (id) {
//如果没有在platform_data里提供,则使用匹配上的i2c_device_id对象里的driver_data
magic = id->driver_data;
} else {
const struct acpi_device_id *aid;
aid = acpi_match_device(at24_acpi_ids, &client->dev);
if (aid)
magic = aid->driver_data;
}
if (!magic)
return -ENODEV;
chip.byte_len = BIT(magic & AT24_BITMASK(AT24_SIZE_BYTELEN));
magic >>= AT24_SIZE_BYTELEN;
chip.flags = magic & AT24_BITMASK(AT24_SIZE_FLAGS);
/*
* This is slow, but we can't know all eeproms, so we better
* play safe. Specifying custom eeprom-types via platform_data
* is recommended anyhow.
*/
chip.page_size = 1;
/* update chipdata if OF is present */
at24_get_ofdata(client, &chip);
chip.setup = NULL;
chip.context = NULL;
}
if (!is_power_of_2(chip.byte_len)) //---------------------数据检查
dev_warn(&client->dev,
"byte_len looks suspicious (no power of 2)!\n");
if (!chip.page_size) {
dev_err(&client->dev, "page_size must not be 0!\n");
return -EINVAL;
}
if (!is_power_of_2(chip.page_size))
dev_warn(&client->dev,
"page_size looks suspicious (no power of 2)!\n");
/*
* REVISIT: the size of the EUI-48 byte array is 6 in at24mac402, while
* the call to ilog2() in AT24_DEVICE_MAGIC() rounds it down to 4.
*
* Eventually we'll get rid of the magic values altoghether in favor of
* real structs, but for now just manually set the right size.
*/
if (chip.flags & AT24_FLAG_MAC && chip.byte_len == 4)
chip.byte_len = 6;
/* Use I2C operations unless we're stuck with SMBus extensions. */
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
//------是否具备I2C_FUNC_I2C,如果不具备则判断是否具备下面能力。然后决定是否使用SMBus。
if (chip.flags & AT24_FLAG_ADDR16)
return -EPFNOSUPPORT;
if (i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_READ_I2C_BLOCK)) {
use_smbus = I2C_SMBUS_I2C_BLOCK_DATA;
} else if (i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_READ_WORD_DATA)) {
use_smbus = I2C_SMBUS_WORD_DATA;
} else if (i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_READ_BYTE_DATA)) {
use_smbus = I2C_SMBUS_BYTE_DATA;
} else {
return -EPFNOSUPPORT;
}
if (i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_WRITE_I2C_BLOCK)) {
use_smbus_write = I2C_SMBUS_I2C_BLOCK_DATA;
} else if (i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_WRITE_BYTE_DATA)) {
use_smbus_write = I2C_SMBUS_BYTE_DATA;
chip.page_size = 1;
}
}
if (chip.flags & AT24_FLAG_TAKE8ADDR)
num_addresses = 8;
else
num_addresses = DIV_ROUND_UP(chip.byte_len,
(chip.flags & AT24_FLAG_ADDR16) ? 65536 : 256);
at24 = devm_kzalloc(&client->dev, sizeof(struct at24_data) +
num_addresses * sizeof(struct i2c_client *), GFP_KERNEL);
if (!at24)
return -ENOMEM;
mutex_init(&at24->lock);
at24->use_smbus = use_smbus;
at24->use_smbus_write = use_smbus_write;
at24->chip = chip;
at24->num_addresses = num_addresses;
if ((chip.flags & AT24_FLAG_SERIAL) && (chip.flags & AT24_FLAG_MAC)) {
dev_err(&client->dev,
"invalid device data - cannot have both AT24_FLAG_SERIAL & AT24_FLAG_MAC.");
return -EINVAL;
}
if (chip.flags & AT24_FLAG_SERIAL) {
//-----根据flags,以及是否使用SMBus,选择合适的read_func()/write_func()。
at24->read_func = at24_eeprom_read_serial;
} else if (chip.flags & AT24_FLAG_MAC) {
at24->read_func = at24_eeprom_read_mac;
} else {
at24->read_func = at24->use_smbus ? at24_eeprom_read_smbus
: at24_eeprom_read_i2c;
}
if (at24->use_smbus) {
if (at24->use_smbus_write == I2C_SMBUS_I2C_BLOCK_DATA)
at24->write_func = at24_eeprom_write_smbus_block;
else
at24->write_func = at24_eeprom_write_smbus_byte;
} else {
at24->write_func = at24_eeprom_write_i2c;
}
writable = !(chip.flags & AT24_FLAG_READONLY);
if (writable) {
//------------------write_max决定后面i2c写大小,大于此数字会被拆分。
if (!use_smbus || use_smbus_write) {
unsigned write_max = chip.page_size;
if (write_max > io_limit)
write_max = io_limit;
if (use_smbus && write_max > I2C_SMBUS_BLOCK_MAX)
write_max = I2C_SMBUS_BLOCK_MAX;
at24->write_max = write_max;
/* buffer (data + address at the beginning) */
at24->writebuf = devm_kzalloc(&client->dev,
write_max + 2, GFP_KERNEL);
if (!at24->writebuf)
return -ENOMEM;
} else {
dev_warn(&client->dev,
"cannot write due to controller restrictions.");
}
}
at24->client[0] = client;
/* use dummy devices for multiple-address chips */
for (i = 1; i < num_addresses; i++) {
at24->client[i] = i2c_new_dummy(client->adapter,
client->addr + i);
if (!at24->client[i]) {
dev_err(&client->dev, "address 0x%02x unavailable\n",
client->addr + i);
err = -EADDRINUSE;
goto err_clients;
}
}
i2c_set_clientdata(client, at24);
/*
* Perform a one-byte test read to verify that the
* chip is functional.
*/
err = at24_read(at24, 0, &test_byte, 1); //----------读一字节进行测试。
if (err) {
err = -ENODEV;
goto err_clients;
}
at24->nvmem_config.name = dev_name(&client->dev); //---注册nvmem设备,对应设备/sys/bus/i2c/devices/x-0050/eeprom。
at24->nvmem_config.dev = &client->dev;
at24->nvmem_config.read_only = !writable;
at24->nvmem_config.root_only = true;
at24->nvmem_config.owner = THIS_MODULE;
at24->nvmem_config.compat = true;
at24->nvmem_config.base_dev = &client->dev;
at24->nvmem_config.reg_read = at24_read; //----对于非SMBus设备,对应at24_eeprom_read_i2c()。
at24->nvmem_config.reg_write = at24_write; //----对非SMBus设备,对应at24_eeprom_write_i2c()。
at24->nvmem_config.priv = at24;
at24->nvmem_config.stride = 1;
at24->nvmem_config.word_size = 1;
at24->nvmem_config.size = chip.byte_len;
at24->nvmem = nvmem_register(&at24->nvmem_config); ---注册nvmem设备
if (IS_ERR(at24->nvmem)) {
err = PTR_ERR(at24->nvmem);
goto err_clients;
}
dev_info(&client->dev, "%u byte %s EEPROM, %s, %u bytes/write\n",
chip.byte_len, client->name,
writable ? "writable" : "read-only", at24->write_max);
if (use_smbus == I2C_SMBUS_WORD_DATA ||
use_smbus == I2C_SMBUS_BYTE_DATA) {
dev_notice(&client->dev, "Falling back to %s reads, "
"performance will suffer\n", use_smbus ==
I2C_SMBUS_WORD_DATA ? "word" : "byte");
}
/* export data to kernel code */
if (chip.setup)
chip.setup(at24->nvmem, chip.context);
return 0;
err_clients:
for (i = 1; i < num_addresses; i++)
if (at24->client[i])
i2c_unregister_device(at24->client[i]);
return err;
}
at24的probe主要做数据有效检查、根据i2c的capability决定读写函数、读1字节验证功能、最后注册对应的nvmem设备。
注册对应的nvmem设备。展开nvmem_register()函数。
nvmem_register()函数中的 device_add()。device_add就是将设备加入到Linux设备模型的关键,它的内部将找到它的bus,然后让它的bus给它找到它的driver。(可参考:https://blog.csdn.net/qq_20678703/article/details/52841706)
DTS 设备树
以上对at24.c驱动源码进行了简单分析,at24.c不依赖于具体的CPU和I2C控制器。由于我们使用的 24LC16(at24c16)包含在at24.c的设备列表中,因此,只需要在 .dts 文件中添加一个24LC16的节点即可。
在设备树\sama5d4.dtsi文件中查找处理器引脚(PE30、PE29)相关的信息。
sama5d4.dtsi文件中对i2c1节点的描述如下。
at91-sama5d4_xplained.dts文件中没有使能i2c1,故没有对i2c1节点的描述,只有i2c0节点的描述。
为使能i2c1,参考sama5d4.dtsi.dtsi文件中对i2c1节点的描述信息,在at91-sama5d4_xplained.dts中添加i2c1节点。并在该节点下添加24lc16子节点。
修改后,重新编译内核和设备树文件,烧至开发板,可到/sys/bus/i2c/devices/1-0050/eeprom目录下,对eeprom节点进行eeprom的读写测试。
3.2.2 使用内核提供的I2C通用设备驱动
后续更新。
4 EEPROM读写测试程序
- 方法1:使用系统自带的 i2c-tools 进行eeprom测试;
(参考:https://blog.csdn.net/mantis_1984/article/details/18254767/)
若系统没有自带i2c-tools工具,需要下载并安装。
1) 用i2cdetect检测有几组i2c总线在系统上,输入: i2cdetect -l
下图显示了i2c总线1
2)用i2cdetect检测挂载在i2c总线上器件,输入 i2cdetect -r -y 1(检测i2c-1上的挂载情况)
3)用i2cdump查看器件所有寄存器的值,这个很有用,输入 i2cdump -f -y 1 0x50 (查看eeprom的寄存器值)
4)用i2cset设置单个寄存器值,用i2cget读取单个寄存器值,
i2cset和i2cget使用方法:
- i2cset -f -y 1 0x50 0x00 0x3f (设置i2c-1上0x50器件的0x00寄存器值为0x3f)
- i2cget -f -y 1 0x50 0x00 (读取i2c-1上0x50器件的0x00寄存器值)
- 方法2:通过sysfs文件系统访问I2C设备,对eeprom进行测试;
eeprom的属性文件创建在 /sys/bus/i2c/devices/1-0050/eeprom(或/sys/devices/platform/ahb/ahb:apb/f8018000.i2c/i2c-1/1-0050/eeprom,这两个路径的eeprom节点是通过符号链接建立的,操作任何一个都可以)。
因为eeprom已经映射为一个文件了,可以通过文件I/O写应用程序对其进行简单的访问测试。比如以下程序对特定地址(0x40)写入特定数据(Hi,this is an eeprom test!),然后再把写入的数据在此地址上读出来。注意连续页写或字节写的时候,时间间隔要在5ms以上。
测试代码:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main(void){
int fd, size, len, i;
char buf[500]= {0};
char *bufw="Hi,this is an eepromtest!";//要写入的数据
len=strlen(bufw);//数据长度
fd= open("/sys/devices/platform/ahb/ahb:apb/f8018000.i2c/i2c-1/1-0050/eeprom",O_RDWR);//打开文件
if(fd< 0)
{
printf("#### i2c test device open failed ####/n");
return(-1);
}
//写操作
lseek(fd,0x50,SEEK_SET); //定位地址,地址是0x50
if((size=write(fd,bufw, len))<0)//写入数据
{
printf("write error\n");
return 1;
}
printf("write ok\n");
//读操作
lseek(fd,0x50, SEEK_SET);//准备读,首先定位地址,因为前面写入的时候更新了当前文件偏移量,所以这边需要重新定位到0x50.
if((size=read(fd,buf,len))<0)//读数据
{
printf("readerror\n");
return 1;
}
printf("readok\n");
for(i=0; i< len; i++)
printf("buff[%d]=%x\n",i, buf[i]);//打印数据
close(fd);
return 0;
}