嵌入式硬件——基于IMX6ULL的I2C实现

一、I2C 基础概念与硬件特性

1.1 I2C 总线核心定义

I2C(Inter-Integrated Circuit)是飞利浦提出的串行半双工通信总线,核心特点是两根信号线实现多设备互联:

  • SDA(Serial Data):双向数据线,用于传输数据;
  • SCL(Serial Clock):双向时钟线,由主设备产生,同步数据传输;
  • 上拉电阻:SDA 和 SCL 需外接(或引脚内部配置)上拉电阻(通常 4.7KΩ),空闲时保持高电平;
  • 主从架构:同一总线中仅 1 个主设备(如 I.MX6ULL),可挂载多个从设备(如 AT24C02、LM75),通过设备地址区分从设备。

1.2 I.MX6ULL I2C 硬件特性

  • 控制器数量:共 4 路 I2C 控制器(I2C1~I2C4),支持主 / 从模式;
  • 传输速率:标准模式(100Kbps)、快速模式(400Kbps);
  • 时钟源:默认使用IPG_CLK_ROOT(66MHz),通过分频器得到 I2C 工作时钟;
  • FIFO 支持:部分控制器含 TX/RXFIFO(如 ECSPI 关联的 I2C 无 FIFO,需软件模拟时序);
  • 中断支持:可配置 FIFO 空、传输完成、仲裁丢失等中断;
  • 器件兼容性:支持 I2C 标准从设备(EEPROM、传感器、时钟芯片等),本次重点适配AT24C02(EEPROM) 和LM75(温度传感器)。

二、I2C 核心通信时序

2.1 基础时序单元

  • 起始信号(S):SCL 为高电平时,SDA 从高电平拉低(下降沿),标志通信开始;
  • 停止信号(P):SCL 为高电平时,SDA 从低电平拉高(上升沿),标志通信结束;
  • 数据传输:SCL 高电平时,SDA 电平需稳定(数据有效);SCL 低电平时,SDA 可切换电平(准备下一位数据);
  • 应答(ACK):主设备发送 1 字节后,释放 SDA;从设备在 SCL 高电平时拉低 SDA,表示数据接收成功;
  • 非应答(NACK):主设备接收最后 1 字节后,SCL 高电平时保持 SDA 高电平,表示无需继续接收。

2.2 核心操作时序

从设备写操作(主→从,如向 AT24C02 写数据)
  • 主设备发送起始信号(S);
  • 主设备发送从设备地址 + 写标志(最低位为 0),等待从设备应答(ACK);
  • 主设备发送从设备内部寄存器地址(如 AT24C02 的存储地址),等待应答;
  • 主设备发送数据(1~N 字节),每字节后等待应答;
  • 主设备发送停止信号(P),结束写操作。
从设备读操作(从→主,如从 LM75 读温度)
  • 主设备发送起始信号(S);
  • 主设备发送从设备地址 + 写标志(0),等待应答(此时目的是 "告知读哪个寄存器");
  • 主设备发送目标寄存器地址(如 LM75 的温度寄存器 0x00),等待应答;
  • 主设备发送重复起始信号(S)(不发停止信号,避免总线释放);
  • 主设备发送从设备地址 + 读标志(1),等待应答;
  • 主设备接收数据(1~N 字节):
    • 接收前 N-1 字节后,主设备发送 ACK;
    • 接收最后 1 字节后,主设备发送 NACK(告知从设备停止发送);
  • 主设备发送停止信号(P),结束读操作。

三、I.MX6ULL I2C 寄存器详解

核心寄存器

  • I2Cx_IADR:从设备地址寄存器;
  • I2Cx_IFDR:分频寄存器(决定 I2C 波特率);
  • I2Cx_I2CR:控制寄存器;
  • I2Cx_I2SR:状态寄存器;
  • I2Cx_I2DR:数据寄存器。

四、I2C 完整实现流程

4.1 I2C 引脚初始化

  • 配置复用功能和电气特性,以 I2C1 为例(SDA=UART4_RX,SCL=UART4_TX);
  • 初始化 I2C 控制器(先关闭,再配置分频)。

4.2 I2C 通用读写函数

  • i2c_write:向指定从设备的指定寄存器写入 N 字节数据;
  • i2c_read:从指定从设备的指定寄存器读取 N 字节数据。
I2C 写函数(i2c_write)

功能:向指定从设备的指定寄存器写入 N 字节数据

c 复制代码
// base:I2C控制器基地址(如I2C1)
// device_address:从设备地址(如LM75=0x48)
// reg_address:从设备寄存器地址(如LM75温度寄存器=0x00)
// reg_len:寄存器地址长度(1或2字节)
// data:待写入数据指针
// len:数据长度
void i2c_write(I2C_Type *base, unsigned char device_address, unsigned short reg_address, int reg_len, const unsigned char *data, int len) 
{
    // 1. 清除仲裁丢失和中断标志,等待总线空闲
    base->I2SR &= ~((1 << 4) | (1 << 1));  // 清除IAL(bit4)和IIF(bit1)
    while((base->I2SR & (1 << 7)) == 0);  // 等待ICF(bit7)=1(总线空闲)
    
    // 2. 配置为主设备发送模式,发送ACK
    base->I2CR |= (1 << 5) | (1 << 4);  // MSTA=1(主模式)、MTX=1(发送)
    base->I2CR &= ~(1 << 3);           // TXAK=0(发送ACK)
    
    // 3. 发送从设备地址(写模式:最低位=0)
    base->I2SR &= ~(1 << 1);           // 清除IIF(中断标志)
    base->I2DR = device_address << 1;   // 设备地址+写标志
    while((base->I2SR & (1 << 1)) == 0);  // 等待传输完成(IIF=1)
    
    // 4. 发送寄存器地址(支持1/2字节)
    for(int i = 0; i < reg_len; ++i) 
    {
        base->I2SR &= ~(1 << 1);       // 清除IIF
        // 高位在前:若reg_len=2,先发高8位,再发低8位
        base->I2DR = reg_address >> (reg_len - i - 1) * 8;
        while((base->I2SR & (1 << 1)) == 0);  // 等待传输完成
    }
    
    // 5. 发送数据(N字节)
    while (len--) 
    {
        base->I2SR &= ~(1 << 1);       // 清除IIF
        base->I2DR = *data++;          // 写入1字节数据
        while((base->I2SR & (1 << 1)) == 0);  // 等待传输完成
    }
    
    // 6. 发送停止信号(清除主模式)
    base->I2CR &= ~(1 << 5);           // MSTA=0(释放主模式,产生停止信号)
    while((base->I2SR & (1 << 5)) != 0);  // 等待IBB=0(总线空闲)
    delayus(100);  // 短暂延时,确保停止信号稳定
}
I2C 读函数(i2c_read)

功能:从指定从设备的指定寄存器读取 N 字节数据

c 复制代码
void i2c_read(I2C_Type *base, unsigned char device_address, unsigned short reg_address, int reg_len, unsigned char *data, int len) 
{
    // 1. 清除标志,等待总线空闲(同写函数)
    base->I2SR &= ~((1 << 4) | (1 << 1));
    while((base->I2SR & (1 << 7)) == 0);
    
    // 2. 配置为主设备发送模式,先写寄存器地址
    base->I2CR |= (1 << 5) | (1 << 4);  // MSTA=1、MTX=1
    base->I2CR &= ~(1 << 3);           // TXAK=0(发送ACK)
    
    // 3. 发送从设备地址(写模式)
    base->I2SR &= ~(1 << 1);
    base->I2DR = device_address << 1;
    while((base->I2SR & (1 << 1)) == 0);
    
    // 4. 发送寄存器地址(同写函数)
    for(int i = 0; i < reg_len; ++i) 
    {
        base->I2SR &= ~(1 << 1);
        base->I2DR = reg_address >> (reg_len - i - 1) * 8;
        while((base->I2SR & (1 << 1)) == 0);
    }
    
    // 5. 发送重复起始信号,切换为读模式
    base->I2CR |= (1 << 2);            // RSTA=1(产生重复起始)
    base->I2SR &= ~(1 << 1);
    base->I2DR = device_address << 1 | 1;  // 设备地址+读标志(最低位=1)
    while((base->I2SR & (1 << 1)) == 0);
    
    // 6. 切换为接收模式
    base->I2CR &= ~(1 << 4);           // MTX=0(接收)
    base->I2SR &= ~(1 << 1);
    
    // 7. 若仅读1字节,提前发送NACK
    if(len == 1) 
    {
        base->I2CR |= (1 << 3);        // TXAK=1(发送NACK)
    }
    *data = base->I2DR;  // 虚假读:触发接收(I2C全双工,发送时已接收无效数据)
    
    // 8. 接收N字节数据
    while(len-- != 0) 
    {
        while ((base->I2SR & (1 << 1)) == 0);  // 等待接收完成
        base->I2SR &= ~(1 << 1);
        
        // 处理最后1字节:发送停止信号
        if(len == 0) 
        {
            base->I2CR &= ~((1 << 5) | (1 << 3));  // MSTA=0(停止)、TXAK=0
            while((base->I2SR & (1 << 5)) != 0);    // 等待总线空闲
        } 
        // 处理倒数第2字节:提前发送NACK
        else if (len == 1) 
        {
            base->I2CR |= (1 << 3);  // TXAK=1(下一字节发NACK)
        }
        
        *data++ = base->I2DR;  // 读取接收数据
    }
}

4.3 封装传输函数(xfer)

封装I2C_MSG结构体,统一管理传输参数,提高代码复用性:

c 复制代码
// i2c.h 中定义结构体和枚举
enum I2C_Direction 
{
    I2C_Write = 0,  // 写方向
    I2C_Read = 1    // 读方向
};

struct I2C_MSG 
{
    unsigned char dev_address;   // 从设备地址
    unsigned short reg_address;  // 寄存器地址
    int reg_len;                 // 寄存器地址长度(1/2)
    unsigned char *data;         // 数据指针
    int len;                     // 数据长度
    enum I2C_Direction direction;// 传输方向
};

// i2c.c 中实现传输函数
void xfer(I2C_Type *base, struct I2C_MSG *msg) 
{
    if(msg->direction == I2C_Write) 
    {
        i2c_write(base, msg->dev_address, msg->reg_address, msg->reg_len, msg->data, msg->len);
    } 
    else 
    {
        i2c_read(base, msg->dev_address, msg->reg_address, msg->reg_len, msg->data, msg->len);
    }
}

五、LM75 温度传感器驱动(基于 I2C)

LM75 是 I2C 接口的温度传感器,设备地址为0x48,温度寄存器(0x00)存储 16 位数据(高 9 位为温度值,单位 0.5℃)。

5.1 温度读取函数(lm75.c

cs 复制代码
#include "lm75.h"
#include "i2c.h"
#include "MCIMX6Y2.h"

// 读取LM75温度(返回值:℃,如25.5℃返回25.5)
float lm75_get_temperature(void) 
{
    unsigned char buffer[2] = {0};  // 存储16位温度数据
    short temp_raw;                 // 原始温度值(16位)
    
    // 1. 构造I2C传输参数
    struct I2C_MSG msg = 
    {
        .direction = I2C_Read,      // 读方向
        .dev_address = 0x48,        // LM75设备地址
        .reg_address = 0x00,        // 温度寄存器地址(1字节)
        .reg_len = 1,               // 寄存器地址长度=1
        .data = buffer,             // 数据缓冲区
        .len = 2                    // 读取2字节
    };
    
    // 2. 调用I2C传输函数
    xfer(I2C1, &msg);
    
    // 3. 解析温度数据(LM75数据格式:高8位+低8位,高9位有效)
    temp_raw = (buffer[0] << 8) | buffer[1];  // 组合16位原始数据
    temp_raw >>= 7;                          // 右移7位,保留高9位(符号位+8位数值)
    return temp_raw * 0.5f;                  // 0.5℃/LSB,转换为实际温度
}

5.2 头文件声明(lm75.h

cs 复制代码
#ifndef __LM75_H__
#define __LM75_H__

// 读取LM75温度,返回值单位:℃
extern float lm75_get_temperature(void);

#endif

5.3 主函数测试(main.c

初始化 I2C、UART 和 LM75,通过 UART 打印温度数据:

cs 复制代码
#include "led.h"
#include "uart.h"
#include "i2c.h"
#include "lm75.h"
#include "delay.h"
#include "stdio.h"

int main(void) 
{
    // 1. 初始化系统时钟、UART(用于打印)、I2C1
    init_clock();    // 初始化系统时钟(IPG_CLK=66MHz)
    init_uart1();    // 初始化UART1(115200bps,用于打印温度)
    init_i2c1();     // 初始化I2C1(100Kbps)
    
    while(1) 
    {
        // 2. 读取LM75温度
        float temp = lm75_get_temperature();
        
        // 3. 格式化温度数据(避免浮点数打印误差)
        int temp_int = (int)temp;          // 整数部分(如25.5→25)
        int temp_dec = (int)((temp - temp_int) * 10);  // 小数部分(如25.5→5)
        
        // 4. 通过UART打印温度
        printf("LM75 Temperature: %d.%d℃\n", temp_int, temp_dec);
        
        delayms(1000);  // 1秒刷新一次
    }
    return 0;
}

六、关键注意事项

  1. 上拉电阻配置 :I2C 总线必需上拉,可通过引脚电气属性配置内部上拉(如IOMUXC_SetPinConfigPUS位),或外接 4.7KΩ 电阻;
  2. 仲裁丢失处理 :若多主设备竞争总线,I2SR->IAL会置 1,需清除该位后重新初始化 I2C;
  3. 应答判断 :传输过程中需检查I2SR->RXAK,若为 1(接收 NACK),需重新发送或终止通信;
  4. 寄存器地址长度 :不同器件的寄存器地址长度不同(如 AT24C02 为 1 字节,某些传感器为 2 字节),需在I2C_MSG中正确设置reg_len
  5. SION 位使能 :部分 I2C 引脚需使能SION(软件输入路径),否则无法读取 SDA 电平(如IOMUXC_SetPinMux的第 2 个参数设为 1);
  6. 时钟分频计算:I2C 波特率 = IPG_CLK / 分频系数,标准模式(100Kbps)推荐分频系数 = 640(66MHz/640≈103Kbps),快速模式(400Kbps)推荐分频系数 = 160(66MHz/160≈412.5Kbps)。

七、总结

I.MX6ULL 的 I2C 开发的核心是严格遵循时序规范熟练操作寄存器 ,关键流程可概括为:引脚复用配置 → I2C控制器初始化(分频、使能) → 封装通用读写函数 → 适配具体I2C器件(LM75/AT24C02) → 测试验证。通过结构体封装传输参数(如I2C_MSG)可显著提高代码复用性,这一思想也与 Linux 内核 I2C 子系统的设计一致,为后续驱动开发打下基础。

相关推荐
常州晟凯电子科技15 小时前
君正T32开发笔记之固件烧写
人工智能·笔记·嵌入式硬件·物联网
李永奉16 小时前
51单片机-驱动LCD1602液晶显示屏教程
单片机·嵌入式硬件·51单片机
straw_hat.19 小时前
PCB学习——STM32F103VET6-STM32接口部分
stm32·嵌入式硬件·学习
张人玉1 天前
C# TCP 开发笔记(TcpListener/TcpClient)
stm32·单片机·嵌入式硬件
A9better1 天前
嵌入式开发学习日志30——stm32之定时器中断简单项目练习
stm32·单片机·嵌入式硬件·学习
充哥单片机设计1 天前
【STM32项目开源】基于STM32的智能电子秤
stm32·单片机·嵌入式硬件
XINVRY-FPGA1 天前
XCVU9P-2FLGA2104E Xilinx AMD Virtex UltraScale+ FPGA
人工智能·嵌入式硬件·fpga开发·硬件工程·dsp开发·射频工程·fpga
10001hours1 天前
(基于江协科技)51单片机入门:7.LED点阵屏
科技·嵌入式硬件·51单片机
10001hours1 天前
(基于江协科技)51单片机入门:9.蜂鸣器
科技·嵌入式硬件·51单片机