分析平台:全志 A64
内核版本:Linux 4.9
数据手册:Allwinner_A64_User_Manual_V1.1.pdf (whycan.com)
I2C 设备驱动
作为方案应用来说,我们是最经常要动的地方,这一层主要与具体的芯片功能强关联,不同的芯片具有不同的使用方法,如触摸屏设备驱动。
核心框架层
Linux 提供的硬件抽象层,起到承上启下的作用,对上提供注册设备驱动的统一接口,对下提供硬件控制器接入统一接口,负责维护众多的设备驱动和适配器驱动。
适配器层
由 Soc 芯片原厂提供,通常 Soc 支持多少路 I2C 总线,就会有多少个硬件控制器,这些硬件控制器才是真正实现与外设芯片通信的地方。我们也可以通过 GPIO 模拟 I2C 时序来实现一个硬件适配器,对于设备驱动来说,它不需要关心 Soc 是通过何种方式产生通信时序来跟外设芯片通信的。
本文主要分析位于适配器层的全志 i2c-sunxi.c 硬件控制器驱动程序,目的在于了解 I2C 适配器驱动的使用方法。
I2C 控制器驱动是通过平台驱动的方式注册到系统中:
lichee\linux-4.9\drivers\i2c\busses\i2c-sunxi.c
// 匹配条件:只要与 compatible 所指向的字符串完全相同即可
static const struct of_device_id sunxi_i2c_match[] = {
{
.compatible = "allwinner,sun8i-twi", },
{
.compatible = "allwinner,sun50i-twi", },
{
},
};
MODULE_DEVICE_TABLE(of, sunxi_i2c_match);
static struct platform_driver sunxi_i2c_driver = {
.probe = sunxi_i2c_probe, // 匹配成功后会被调用
.remove = sunxi_i2c_remove, // 驱动移除时会被调用
.driver = {
.name = SUNXI_TWI_DEV_NAME,
.owner = THIS_MODULE,
.pm = SUNXI_I2C_DEV_PM_OPS,
.of_match_table = sunxi_i2c_match, // 指定平台设备资源匹配调节(dts)
},
};
static int __init sunxi_i2c_adap_init(void)
{
// 注册平台驱动
return platform_driver_register(&sunxi_i2c_driver);
}
static void __exit sunxi_i2c_adap_exit(void)
{
// 卸载平台驱动
platform_driver_unregister(&sunxi_i2c_driver);
}
fs_initcall(sunxi_i2c_adap_init);
module_exit(sunxi_i2c_adap_exit);
A64 有四路 I2C 控制器,取其中一路 dts 内容如下:
lichee\linux-4.9\arch\arm64\boot\dts\sunxi\sun50iw1p1.dtsi
设备树(dts)里面的配置信息,都会在内核被解析为平台设备(platform_device)注册到系统里面,其本质还是平台设备(platform_device)。
twi0: twi@0x01c2ac00{
#address-cells = <1>;
#size-cells = <0>;
compatible = "allwinner,sun50i-twi";
device_type = "twi0";
reg = <0x0 0x01c2ac00 0x0 0x400>;
interrupts = <GIC_SPI 6 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_twi0>;
clock-frequency = <400000>;
pinctrl-names = "default", "sleep";
pinctrl-0 = <&twi0_pins_a>;
pinctrl-1 = <&twi0_pins_b>;
status = "disabled";
};
lichee\linux-4.9\include\linux\i2c.h
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
};
lichee\linux-4.9\drivers\i2c\busses\i2c-sunxi.c
static int sunxi_i2c_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct sunxi_i2c *i2c = NULL;
struct resource *mem_res = NULL;
struct sunxi_i2c_platform_data *pdata = NULL;
int ret, irq;
unsigned long int_flag = 0;
const char *str_vcc_twi;
// 创建一个 I2C 控制器对象
i2c = kzalloc(sizeof(struct sunxi_i2c), GFP_KERNEL);
if (!i2c)
return -ENOMEM;
// 开辟一个用于存储平台相关的数据内存
pdata = kzalloc(sizeof(struct sunxi_i2c_platform_data), GFP_KERNEL);
if (pdata == NULL) {
kfree(i2c);
return -ENOMEM;
}
i2c->dev = &pdev->dev;
pdev->dev.platform_data = pdata;
pdev->dev.driver_data = i2c;
// 通过 dts 里面的 aliases 来确定总线编号
//aliases {
// twi0 = &twi0;
// ...};
pdev->id = of_alias_get_id(np, "twi");
if (pdev->id < 0) {
I2C_ERR("I2C failed to get alias id\n");
ret = -EINVAL;
goto emem;
}
pdata->bus_num = pdev->id;
// 从 dts 获取寄存器地址段资源,对应 dts: reg = <0x0 0x01c2ac00 0x0 0x400>
mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (mem_res == NULL) {
I2C_ERR("[I2C%d] failed to get MEM res\n", pdev->id);
ret = -ENXIO;
goto emem;
}
// 申请占用该段寄存器内存
if (!request_mem_region(mem_res->start, resource_size(mem_res),
mem_res->name)) {
I2C_ERR("[I2C%d] failed to request mem region\n", pdev->id);
ret = -EINVAL;
goto emem;
}
// 将寄存器地址段映射出来,方便后续进行操作
i2c->base_addr = ioremap(mem_res->start, resource_size(mem_res));
if (!i2c->base_addr) {
ret = -EIO;
goto eiomap;
}
// 从 dts 获取中断信息,对应 dts: interrupts =
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
I2C_ERR("[I2C%d] failed to get irq\n", pdev->id);
ret = -EINVAL;
goto eiomap;
}
// 从 dts 获取时钟频率,如需要 I2C 总线工作在 100KHz 就可以修改该参数
ret = of_property_read_u32(np, "clock-frequency", &pdata->frequency);
if (ret) {
I2C_ERR("[I2C%d] failed to get clock frequency\n", pdev->id);
ret = -EINVAL;
goto eiomap;
}
// 从 dts 获取电源相关的配置,一般是对应该 I2C 的 SCL\SDA GPIO 的供电
ret = of_property_read_string(np, "twi_regulator", &str_vcc_twi);
if (ret)
I2C_ERR("[I2C%d] failed to get regulator id\n", pdev->id);
else {
pr_info("[I2C%d] twi_regulator: %s\n", pdev->id, str_vcc_twi);
strcpy(pdata->regulator_id, str_vcc_twi);
}
// 初始化适配器相关的接口
pdev->dev.release = sunxi_i2c_release; // 关闭时用于清理的接口
i2c->adap.owner = THIS_MODULE;
i2c->adap.nr = pdata->bus_num; // 指定适配器编号
i2c->adap.retries = 3; // 指定通信失败时重试的次数
i2c->adap.timeout = 5*HZ; // 配置等待设备响应的超时时间