ARM-09-I.MX6U-I2C

ARM-09-I.MX6U-I2C I.MX6U I2C 驱动学习笔记一、GPIO 输出模式基础回顾推挽输出电路原理GPIO_VCC | [上MOS] | GPIO ─────┤────→ 负载开/关 | [下MOS] | GND开关逻辑GPIO 输出高电平→ 上方 MOS 管导通 → 电流从 GPIO_VCC → 上MOS → 负载 → 下MOS → GND → 形成回路 →设备工作开GPIO 输出低电平→ MOS 管截止 → 回路断开 →设备停止关为什么用两个 MOS推挽提高驱动能力可主动驱动高低电平实现双向控制正反向电流增强稳定性三种输出模式对比模式描述电路表现常见场景高阻态输出既不驱动高也不驱动低相当于断开连接引脚呈现高阻抗对外相当于开路三态门、总线挂载、I2C数据线悬空引脚没有连接任何电平电平不确定容易受干扰电平随机浮动可能感应到噪声未连接的输入引脚应避免推挽用一对MOS管上拉/下拉主动驱动高低电平可输出强0或强1驱动能力强GPIO输出、驱动LED、数字信号二、I2C 总线基础概念什么是线与“线与” 任何一方输出低电平总线就是低电平只有全部输出高电平总线才是高电平。I2C 的 SDA 和 SCL 线都采用线与因此主从设备都可以拉低总线实现多主仲裁谁先拉低谁赢为什么 I2C 必须用开漏输出ODI2C 的 SDA/SCL 引脚必须配置为开漏输出OD而不是推挽输出。原因允许多个设备共享一根线靠外部上拉电阻实现线与避免短路若两设备同时一个输出高一个输出低推挽会短路支持多主多从通信IIC 推挽 vs 开漏示意推挽输出 H(高) L(低) 开漏输出 X(高阻) L(低) ← 高电平靠上拉电阻提供推挽可以主动输出高或低开漏只能主动拉低高电平由外部上拉电阻接VCC提供总线空闲时所有设备释放总线 → 上拉电阻将总线拉高任意设备要发低拉低总线即可I2C 总线拓扑VCC | [上拉电阻] | ├── SCL ── master ── slave1 ── slave2 ── slave3 | [上拉电阻] | └── SDA ── master ── slave1 ── slave2 ── slave3主从应答轮询主设备按顺序逐个访问从设备因为 I2C 是同步半双工主设备必须轮着来三、I2C 通信规则基本规则重要通信只能由主机发起时钟线 SCL 永远由主机控制数据线传输数据时由发送方控制应答信号由接收方控制主机写主机控制数据位从机控制应答主机读从机控制数据位主机控制应答主机第一次发送的数据永远是 7位从机地址 1位读写标志I2C 时序图说明SCL: ‾‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_‾‾ SDA: ‾‾| 数 据 位 传 输 |ack/nck‾ ↑起始 ↑停止 空闲 总线保持高电平 起始信号SCL为高时SDA由低到高 [起始条件] ← 注意SCL高时SDA变化 特殊信号起始/停止 数据传输SCL为低时SDA可以跳变写入数据 SCL为高时SDA保持不变接收方采样 每次传输以8bit为基本单位先发高位 数据发完由接收方发出应答信号1bit的低电平 ACK 如果没有应答通信强制停止 停止信号SCL为高时SDA由低到高ACK/NACK 握手机制主设备发完一个字节地址或数据后从设备在第9个时钟周期拉低 SDA 表示收到ACK如果从设备没准备好或不想收就释放 SDA高电平表示没收到NACK这是 I2C 的握手机制确保每一步都有反馈四、I2C 寄存器详解I.MX6UI.MX6U 有 4 个 I2C 控制器I2C1~I2C4对应寄存器为 I2Cx_xxx。4.1 I2Cx_IADR — 地址寄存器位名称说明bit7:1ADR从设备地址有效位bit0保留始终为0访问某个 I2C 从设备时将其设备地址写入 ADR 字段。4.2 I2Cx_IFDR — 频率分频寄存器波特率位名称说明bit5:0ICI2C 时钟分频值查手册第1464页的分频表时钟来源PLL2 528 MHz PLL2_PFD2 528 × 18/24 396 MHz IPG_CLK_ROOT PLL2_PFD2 / ahb_podf / ipg_podf 396/4/2 49.5 MHz PER_CLK_ROOT IPG_CLK_ROOT / perclk_podf 49.5/1 49.5 MHz默认 ← 也可以选择 IPG_CLK_ROOT 66MHz另一种配置波特率计算示例时钟源 66MHz目标波特率 100KHzIC 设置为0x15→ 640分频66,000,000 / 640 103.125 KHz ≈ 100KHz✓⚠️ IC 的值不能随意设置必须查手册给出的固定分频表选择合适的值。4.3 I2Cx_I2CR — 控制寄存器位名称说明bit7IENI2C 使能位1使能0关闭bit6IIENI2C 中断使能位1使能中断0关闭bit5MSTA主从模式选择1主模式0从模式bit4MTX传输方向0接收1发送bit3TXAK传输应答使能0发送ACK1发送NACKbit2RSTA重复开始信号写1产生Repeated Start4.4 I2Cx_I2SR — 状态寄存器位名称说明bit7ICF数据传输状态0正在传输1传输完成bit6IAAS1I2Cx_IADR中的地址是从设备地址bit5IBBI2C 总线忙标志0总线空闲1总线忙bit4IAL仲裁丢失位1发生仲裁丢失多主竞争失败bit2SRW从机读写状态0主机要写1主机要读bit1IIFI2C 中断挂起标志1有中断挂起需软件清零bit0RXAK应答信号标志0收到ACK1收到NACK仲裁丢失说明多个设备同时抢总线时硬件自动让仲裁失败的设备切换到Slave Receive模式。因此发完起始位后应检查 IAL 是否为1。4.5 I2Cx_I2DR — 数据寄存器位名称说明bit7:0DATA发送/接收数据只有低8位有效使用规则发送将要发送的数据写入此寄存器I2C控制器开始传输数据传输完毕收到ACK或NACK后会使 IIF(I2SR[bit1]) 置位接收直接读取此寄存器即可得到接收到的数据⚠️注意对于接收数据来说当从该寄存器读走一个字节后I2C控制器就开始传输数据直到IIF置位。因此在读取过程中第一个读到的字节实际上是无效的五、I2C 初始化代码第一步初始化 I2C 引脚init_i2c_io初始化包括两方面配置引脚复用功能和电器特性初始化I2C控制器。关键点需要将 SION 置位关于SION官方手册说明不清楚但至少对于SDA来说需要将其作为输出有作为输入。因此必须使能SION。voidinit_i2c_io(I2C_Type*base){if(baseI2C1){/* 配置I2C1的SDA引脚 */IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA,1);// SION1/* 配置I2C1的SCL引脚 */IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL,1);// SION1/* 配置SDA引脚电气属性开漏输出速度100KHz驱动能力R0/6上拉/下拉使能上拉 */IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA,0x70B0);/* 配置SCL引脚电气属性开漏输出速度100KHz驱动能力R0/6上拉/下拉使能上拉 */IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL,0x70B0);}elseif(baseI2C2){IOMUXC_SetPinMux(IOMUXC_UART5_RX_DATA_I2C2_SDA,1);IOMUXC_SetPinMux(IOMUXC_UART5_TX_DATA_I2C2_SCL,1);IOMUXC_SetPinConfig(IOMUXC_UART5_RX_DATA_I2C2_SDA,0x70B0);IOMUXC_SetPinConfig(IOMUXC_UART5_TX_DATA_I2C2_SCL,0x70B0);}}第二步初始化 I2C 控制器init_i2c/** * brief 初始化I2C控制器 * param base - I2C基地址指针(I2C1, I2C2等) */voidinit_i2c(I2C_Type*base){/* 初始化I2C引脚 */init_i2c_io(base);/* 关闭I2C模块 */base-I2CR~(17);/* 设置I2C时钟分频为0x15生成100KHz的SCL时钟 */base-IFDR0x15;/* 使能I2C模块 */base-I2CR|(17);}六、I2C 读写函数主发送流程I2C Master TransmitReset ↓ Program IFDR设置波特率 ↓ Enable I2CIEN1, IIEN1 ↓ Program IADR设置地址 ↓ IBB0? → N → 等待 ↓ Y Program MSTA and MTX主机模式发送 ↓ IBB1? → N → 等待若IAL1 → 仲裁失败重试 ↓ Y Write Slave Address to I2DR ↓ Data Transmission循环发数据等IIF ↓ 清IIF → 检查RXAK → 写下一字节 → 直到完成 ↓ Generate STOP Signal或 Repeated START6.1 向 24C02 写入数据i2c_write寄存器地址可能不止一个字节所以需要分别考虑。voidi2c_write(I2C_Type*base,unsignedchardev_addr,// 从设备地址unsignedcharreg_addr,// 寄存器地址unsignedcharreg_addr_len,// 寄存器地址字节数constunsignedchar*data,// 要写入的数据unsignedintlen)// 数据长度{/* 清除中断标志位和仲裁丢失标志位 */base-I2SR~((14)|(11));/* 等待总线空闲 */while((base-I2SR(17))!0);/* 设置为主机发送模式 */base-I2CR|(14)|(15);/* 主机模式和发送模式 */base-I2CR~(13);/* 发送ACK *//* 发送设备地址写模式 */base-I2SR~(11);/* 清除中断标志位 */base-I2DR(dev_addr1);/* 写入设备地址和写标志(0) */while((base-I2SR(11))0);/* 等待传输完成 *//* 发送寄存器地址 */if(1reg_addr_len){base-I2SR~(11);/* 清除中断标志位 */base-I2DRreg_addr;/* 写入寄存器地址 */while((base-I2SR(11))0);/* 等待传输完成 */}else{base-I2SR~(11);base-I2DRreg_addr8;/* 写入寄存器地址高字节 */while((base-I2SR(11))0);base-I2SR~(11);base-I2DRreg_addr0xFF;/* 写入寄存器地址低字节 */while((base-I2SR(11))0);}/* 循环发送数据 */while(len--){base-I2SR~(11);/* 清除中断标志位 */base-I2DR*data;/* 写入数据 */while((base-I2SR(11))0);/* 等待传输完成 */}/* 发送停止信号 */intt0;base-I2CR~(15);/* 清除主机模式产生停止信号 *//* 等待STOP信号完成 */while((base-I2SR|(15))t10){t;delay_us(100);}}6.2 从 24C02 读取数据i2c_read读取时序相对复杂发送完写入地址之后需要重新发送 Repeated Start修改数据流向为读。voidi2c_read(I2C_Type*base,unsignedchardev_addr,unsignedshortreg_addr,unsignedcharreg_addr_len,unsignedchar*data,unsignedintlen){/* 清除中断标志位和仲裁丢失标志位 */base-I2SR~((14)|(11));/* 等待总线空闲 */while((base-I2SR(17))!0);/* 设置为主机发送模式先发地址 */base-I2CR|(14)|(15);/* 主机模式和发送模式 */base-I2CR~(13);/* 发送ACK *//* 发送设备地址写模式用于先指定寄存器地址 */base-I2SR~(11);base-I2DRdev_addr1;/* 写入设备地址和写标志(0) */while((base-I2SR(11))0);/* 发送寄存器地址 */if(reg_addr_len1){base-I2SR~(11);base-I2DRreg_addr;while((base-I2SR(11))0);}else{base-I2SR~(11);base-I2DRreg_addr8;/* 写入寄存器地址高字节 */while((base-I2SR(11))0);base-I2SR~(11);base-I2DRreg_addr0xFF;/* 写入寄存器地址低字节 */while((base-I2SR(11))0);}/* ★ 产生重复起始信号切换为读模式 ★ */base-I2CR|(12);/* 产生重复起始信号 */base-I2SR~(11);base-I2DRdev_addr1|1;/* 写入设备地址和读标志(1) */while((base-I2SR(11))0);/* 切换为接收模式 */base-I2CR~(14);/* 切换为接收模式 */base-I2SR~(11);/* 清除中断标志位 *//* 如果只读取一个字节需要提前设置置NACK */if(1len){base-I2CR|(13);/* 发送NACK */}/* 虚假读触发数据接收 */*database-I2DR;/* 第二次发送完设备地址后便转换成接收方了 此时读取I2DR寄存器就会让I2C产生一次数据传输 因此第一个字节读出来的数据实际上是无效的 *//* 开始连续读取若干字节注意一定要在读到最后一个字节时回复NACK */while(len--){while((base-I2SR(11))0);/* 等待传输完成 */base-I2SR~(11);/* 最后一个字节处理 */if(0len){intt0;base-I2CR~((15)|(13));/* 清除主机模式和NACK *//* 等待STOP信号完成 */while((base-I2SR|(15))t10){t;delay_us(100);}}/* 倒数第二个字节时需要发送NACK */if(1len){base-I2CR|(13);/* 发送NACK */}/* 读取数据 */*database-I2DR;}}七、封装通用传输接口屏蔽底层为了让上层代码不用关心底层读写细节定义一个结构体和传输函数7.1 I2C_Msg 结构体structI2C_Msg{unsignedchardev_addr;// 从设备地址unsignedshortreg_addr;// 寄存器地址unsignedcharreg_addr_len;// 寄存器地址字节数unsignedchar*data;// 数据缓冲区指针unsignedintlen;// 数据长度enumI2C_Directiondirection;// 方向I2C_READ 或 I2C_WRITE};7.2 通用传输函数i2c_transfervoidi2c_transfer(I2C_Type*base,structI2C_Msg*msg){/* 参数检查 */if(0base){return;}/* 根据传输方向选择读或写操作 */if(msg-directionI2C_READ){i2c_read(base,msg-dev_addr,msg-reg_addr,msg-reg_addr_len,msg-data,msg-len);}else{i2c_write(base,msg-dev_addr,msg-reg_addr,msg-reg_addr_len,msg-data,msg-len);}}八、实战应用读取 LM75 温度传感器LM75 基本信息设备地址0x48寄存器地址0温度寄存器寄存器地址长度1字节每次读取2字节16位温度数据温度计算t (data[0] 8 | data[1]); return (t 7) * 0.5;注意LM75 内部没有什么读数数据存放的位置转而代之的是寄存器地址和24C02的存储位置是完全一样的用法。这里的寄存器指的是LM75内部的寄存器但用法和24C02的存储位置是完全一样的。floatlm75_get_temperature(void){unsignedchardata[2];structI2C_Msgmsg{.dev_addr0x48,.reg_addr0,.reg_addr_len1,.datadata,.len2,.directionI2C_READ};i2c_transfer(I2C1,msg);shortt(data[0]8)|data[1];return(t7)*0.5;}如此一来LM75的温度采集程序就变得十分简单完全不需要关心底层 I2C 时序九、关键寄存器速查表寄存器位名称功能I2Cx_IADRbit7:1ADR从设备地址I2Cx_IFDRbit5:0ICI2C 波特率分频值I2Cx_I2CRbit7IENI2C 使能I2Cx_I2CRbit6IIENI2C 中断使能I2Cx_I2CRbit5MSTA主/从模式选择I2Cx_I2CRbit4MTX传输方向0收1发I2Cx_I2CRbit3TXAK应答使能0ACK1NACKI2Cx_I2CRbit2RSTA重复起始信号I2Cx_I2SRbit7ICF传输状态1完成I2Cx_I2SRbit5IBB总线忙标志1忙I2Cx_I2SRbit4IAL仲裁丢失1丢失I2Cx_I2SRbit1IIF中断挂起软件清零I2Cx_I2SRbit0RXAK应答标志0ACK1NACKI2Cx_I2DRbit7:0DATA发送/接收数据寄存器十、注意事项总结I2C 引脚必须配置为开漏输出OD外部需要上拉电阻SION 位必须置1使 SDA/SCL 引脚同时作为输入用于采样IIF 标志位需要软件清零每次传输前后都要处理读操作第一个字节无效触发读后要丢弃第一次虚读的结果最后一个字节必须回复 NACK否则从机不知道何时停止发完起始位后检查 IAL判断是否发生仲裁丢失波特率不能随意设置必须查手册提供的分频表IFDR 寄存器封装思想底层写好 i2c_write/i2c_read上层只需填 I2C_Msg 结构体代码复用性强移植方便