一、I2C基础知识
1、I2C 使用两条线在主控制器和从机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线)。
2、I2C 是支持多从机的,也就是一个 I2C 控制器下可以挂多个 I2C 从设备,这些不同的 I2C从设备有不同的器件地址。
3、I2C时序相关
①起始位:通信起始标志,SCL 为高电平的时候,SDA 出现下降沿就表示为起始位。
②停止位:停止 I2C 通信的标志位,SCL 位高电平的时候,SDA出现上升沿就表示为停止位。
③数据传输:数据传输时,要保证SCL在高电平期间,SDA 上的数据变化只能在 SCL 低电平期间发生。
④应答信号:当 I2C 主机发送完 8 位数据以后会将 SDA 设置为输入状态,等待 I2C 从机应答,也就是等待 I2C 从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所
需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过将SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
主机通过 I2C 总线与从机之间进行通信包括两个操作:写和读。
二、I2C设备驱动编写
在ST提供的源码中,内核源码 arch/arm/boot/dts/stm32mp151.dtsi 设备树文件中找到
STM32MP1 的 I2C 控制器节点。已经提供了I2C适配器驱动程序。
1、若不使用设备树
在未使用设备树的时候需要在 BSP 里面使用 i2c_board_info 结构体来描述一个具体的 I2C 设备
struct i2c_board_info {char type[I2C_NAME_SIZE];//设置I2C设备名字unsigned short flags;unsigned short addr; //设置I2C设备地址const char *dev_name;void *platform_data;struct device_node *of_node;struct fwnode_handle *fwnode;const struct property_entry *properties;const struct resource *resources;unsigned int num_resources;int irq;
};
2、使用设备树
使用设备树的时候 I2C 设备信息通过创建相应的节点就行了
例如,使用I2C5挂载一个传感器
&i2c5 {pinctrl-names = "default", "sleep";pinctrl-0 = <&i2c5_pins_a>;pinctrl-1 = <&i2c5_pins_sleep_a>;status = "okay";ap3216c@1e {compatible = " alientek,xxx";reg = <0x1e>;};};
修改设备树,打开 stm32mp15-pinctrl.dtsi
找到如下内容:
i2c5_pins_a: i2c5-0 {pins {pinmux = <STM32_PINMUX('A', 11, AF4)>, /* I2C5_SCL */<STM32_PINMUX('A', 12, AF4)>; /* I2C5_SDA */bias-disable;drive-open-drain;slew-rate = <0>;};};i2c5_pins_sleep_a: i2c5-1 {pins {pinmux = <STM32_PINMUX('A', 11, ANALOG)>, /* I2C5_SCL */<STM32_PINMUX('A', 12, ANALOG)>; /* I2C5_SDA */};};
定义了 I2C5 接口的两个 pinmux 配置分别为:i2c5_pins_a 和i2c5_pins_sleep_a。第一个默认的状态下使用,第二个是在 sleep 状态下使用。
打开 stm32mp157d-atk.dts 文件,向 i2c5 节点中添加“ap3216c@1e”子节点,
&i2c5 {pinctrl-names = "default", "sleep";pinctrl-0 = <&i2c5_pins_a>;pinctrl-1 = <&i2c5_pins_sleep_a>;status = "okay";ap3216c@1e { //le是ap3216c的器件地址compatible = "alientek,ap3216c";reg = <0x1e>;//reg 属性也是设置 ap3216c 器件地址的,因此 reg 设置为 0x1e。};};
修改完成后,使用make dtbs编译一下,编译完成后,会在/sys/bus/i2c/devices 目录下看到一个名为“0-001e”的子目录,进入0-001e 目录,可以看到“name”文件,name 文件保存着此设备名字。
最后,编写驱动程序。
//①定义结构体,包括设备,设备号,设备节点等
//②从设备读取多个寄存器数据
//③向多个寄存器写入数据
//④读取指定寄存器值,读取一个寄存器
//⑤指定寄存器写入指定的值,写一个寄存器
//⑥读取数据
void ap3216c_readdata(struct ap3216c_dev *dev){unsigned char i =0;unsigned char buf[6];/* 循环读取所有传感器数据 */for(i = 0; i < 6; i++) {buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);}if(buf[0] & 0X80) /* IR_OF 位为 1,则数据无效 */dev->ir = 0;else/* 读取 IR 传感器的数据 */dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);dev->als = ((unsigned short)buf[3] << 8) | buf[2];if(buf[4] & 0x40) /* IR_OF 位为 1,则数据无效 */dev->ps = 0;else/* 读取 PS 传感器的数据 */dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] &0X0F);}
//从设备读取数据
static ssize_t ap3216c_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *off){short data[3];long err = 0;struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;struct ap3216c_dev *dev = container_of(cdev, struct ap3216c_dev,cdev);ap3216c_readdata(dev);data[0] = dev->ir;data[1] = dev->als;data[2] = dev->ps;err = copy_to_user(buf, data, sizeof(data));return 0;}
//关闭/释放设备
//写i2c 驱动的 probe 函数,当驱动与设备匹配以后此函数就会执行
//i2c 驱动的 remove 函数,移除 i2c 驱动的时候此函数会执行
//驱动入口和出口函数
/* module_i2c_driver(ap3216c_driver) */module_init(ap3216c_init);module_exit(ap3216c_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("ALIENTEK");MODULE_INFO(intree, "Y");
编写测试程序,
编译出来拷贝到对应的目录中,重启开发板,加载驱动模块,看读取的数据变化。