美烦资源网

专注技术文章分享,涵盖编程教程、IT 资源与前沿资讯

从零玩转模拟I^2C:小白也能搞懂的通信协议

你是否遇到过单片机硬件I^2C资源不够用的尴尬?
是否好奇如何用几行代码“凭空”造出一个I^2C接口?
今天带你揭开模拟I^2C的神秘面纱,从理论到实战一网打尽!

一、为什么要用软件模拟IIC?

大多数现代MCU(如STM32、ESP32)都内置了硬件IIC外设,但软件模拟IIC依然有它的价值:

  • 灵活性:在没有硬件IIC支持的芯片上也能实现通信。
  • 学习性:手写代码能帮助你深入理解IIC的每一步时序。
  • 调试性:可以更精细地控制信号,方便排查问题。

软件IIC的核心是通过GPIO模拟SCL和SDA的电平变化,严格遵循IIC协议的时序规则。接下来,我们将按IIC的时序要求,逐步实现以下功能:起始信号、停止信号、发送字节、接收字节和应答处理。

二、IIC时序回顾:代码的“蓝图”

在编写代码前,我们先快速回顾IIC的时序规则,确保代码逻辑与之吻合:

  1. 起始信号(START):SCL高电平时,SDA从高到低跳变。
  2. 停止信号(STOP):SCL高电平时,SDA从低到高跳变。
  3. 数据传输:SCL低电平时,SDA设置数据;SCL高电平时,数据被采样。每8位数据后跟1位应答(ACK/NACK)。
  4. 应答信号(ACK):接收方拉低SDA表示确认,释放SDA(高电平)表示非确认。
  5. 时钟同步:SCL由主设备控制,从设备可通过拉低SCL进行时钟拉伸。

这些规则将直接体现在我们的代码中。假设我们使用两个GPIO引脚(例如,PB6作为SCL,PB7作为SDA),下面是完整的实现。

三、代码实现:从时序到功能

以下代码以C语言为基础,适用于大多数嵌入式平台。为了清晰起见,我们假设硬件平台支持基本的GPIO操作(具体引脚配置需根据实际MCU调整)。代码分为初始化、基础时序函数和高层通信函数三部分。

1. 头文件与宏定义

#ifndef __IIC_H
#define __IIC_H


#include "stm32f10x.h" // 根据实际MCU替换


// 定义SCL和SDA的GPIO引脚
#define IIC_SCL_PORT GPIOB
#define IIC_SDA_PORT GPIOB
#define IIC_SCL_PIN  GPIO_Pin_6
#define IIC_SDA_PIN  GPIO_Pin_7


// 宏定义GPIO电平操作
#define IIC_SCL_H()  GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN)
#define IIC_SCL_L()  GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN)
#define IIC_SDA_H()  GPIO_SetBits(IIC_SDA_PORT, IIC_SDA_PIN)
#define IIC_SDA_L()  GPIO_ResetBits(IIC_SDA_PORT, IIC_SDA_PIN)
#define IIC_SDA_READ() GPIO_ReadInputDataBit(IIC_SDA_PORT, IIC_SDA_PIN)


// 函数声明
void IIC_Init(void);
void IIC_Start(void);
void IIC_Stop(void);
void IIC_SendByte(uint8_t data);
uint8_t IIC_ReadByte(void);
uint8_t IIC_WaitAck(void);
void IIC_Ack(void);
void IIC_NAck(void);
void IIC_Delay(void);


#endif

2. 初始化函数

IIC的SCL和SDA需要配置为开漏输出,并加上拉电阻(通常4.7kΩ)。初始状态下,两线保持高电平。

void IIC_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;


    // 使能GPIO时钟(根据MCU调整)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);


    // 配置SCL和SDA为开漏输出
    GPIO_InitStructure.GPIO_Pin = IIC_SCL_PIN | IIC_SDA_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(IIC_SCL_PORT, &GPIO_InitStructure);


    // 初始化为高电平
    IIC_SCL_H();
    IIC_SDA_H();
}

3. 延时函数

软件IIC需要精确控制时序,延时函数用于模拟适当的时钟周期(例如,100kHz对应10μs周期)。

void IIC_Delay(void) {
    volatile uint8_t i = 10; // 根据实际时钟频率调整
    while (i--);
}

4. 起始信号

严格按照时序:SCL高时,SDA从高到低。

void IIC_Start(void) {
    IIC_SDA_H(); // 确保SDA初始为高
    IIC_SCL_H(); // 确保SCL初始为高
    IIC_Delay();
    IIC_SDA_L(); // SDA拉低,产生起始信号
    IIC_Delay();
    IIC_SCL_L(); // SCL拉低,为后续数据准备
    IIC_Delay();
}

5. 停止信号

SCL高时,SDA从低到高。

void IIC_Stop(void) {
    IIC_SDA_L(); // 确保SDA初始为低
    IIC_SCL_H(); // SCL拉高
    IIC_Delay();
    IIC_SDA_H(); // SDA拉高,产生停止信号
    IIC_Delay();
}

6. 发送一个字节

数据在SCL低电平时设置,SCL高电平时被采样。

void IIC_SendByte(uint8_t data) {
    uint8_t i;
    for (i = 0; i < 8; i++) {
        IIC_SCL_L(); // SCL拉低,准备设置数据
        IIC_Delay();
        if (data & 0x80) // 发送最高位
            IIC_SDA_H();
        else
            IIC_SDA_L();
        data <<= 1; // 数据左移
        IIC_Delay();
        IIC_SCL_H(); // SCL拉高,采样数据
        IIC_Delay();
    }
    IIC_SCL_L(); // 传输结束,SCL拉低
}

7. 接收一个字节

主设备读取SDA上的数据,同样在SCL高电平时采样。

uint8_t IIC_ReadByte(void) {
    uint8_t i, data = 0;
    IIC_SDA_H(); // 释放SDA,准备接收
    for (i = 0; i < 8; i++) {
        data <<= 1; // 数据左移
        IIC_SCL_L();
        IIC_Delay();
        IIC_SCL_H(); // SCL高电平,采样SDA
        IIC_Delay();
        if (IIC_SDA_READ())
            data |= 0x01; // 读取数据位
    }
    IIC_SCL_L();
    return data;
}

8. 应答与非应答

主设备检查从设备的ACK,或发送ACK/NACK给从设备。

uint8_t IIC_WaitAck(void) {
    uint8_t timeout = 200;
    IIC_SDA_H(); // 释放SDA
    IIC_Delay();
    IIC_SCL_H(); // SCL拉高,等待ACK
    IIC_Delay();
    while (IIC_SDA_READ()) { // 检查SDA是否被拉低
        timeout--;
        if (timeout == 0) {
            IIC_Stop(); // 超时,发送停止信号
            return 1; // 无应答
        }
    }
    IIC_SCL_L();
    return 0; // 收到ACK
}


void IIC_Ack(void) {
    IIC_SCL_L();
    IIC_SDA_L(); // 拉低SDA,发送ACK
    IIC_Delay();
    IIC_SCL_H();
    IIC_Delay();
    IIC_SCL_L();
}


void IIC_NAck(void) {
    IIC_SCL_L();
    IIC_SDA_H(); // 释放SDA,发送NACK
    IIC_Delay();
    IIC_SCL_H();
    IIC_Delay();
    IIC_SCL_L();
}

四、代码使用示例:读写EEPROM

以AT24C02 EEPROM为例,展示如何用上述代码实现写操作。

void EEPROM_WriteByte(uint8_t addr, uint8_t data) {
    IIC_Start();
    IIC_SendByte(0xA0); // 器件地址+写(0x50 << 1)
    IIC_WaitAck();
    IIC_SendByte(addr); // 寄存器地址
    IIC_WaitAck();
    IIC_SendByte(data); // 数据
    IIC_WaitAck();
    IIC_Stop();
}

读取操作类似,只需将器件地址改为0xA1(读模式),并调用IIC_ReadByte。

五、调试与优化

  1. 时序调试:用示波器检查SCL和SDA的波形,确保起始/停止信号和数据位的时序正确。
  2. 延时调整:根据MCU主频和IIC速率(如100kHz或400kHz),调整IIC_Delay中的循环次数。
  3. 错误处理:在IIC_WaitAck中增加超时机制,避免程序卡死。
  4. 总线负载:确保上拉电阻合适(4.7kΩ为常见值),否则可能导致信号失真。

六、总结:从时序到代码的完美落地

通过手写软件IIC,不仅实现了完整的通信功能,还深入理解了IIC协议的每一步时序。这种从“原理到代码”的过程,正是嵌入式开发的乐趣所在!希望这篇文章能帮你快速上手IIC编程,并在项目中灵活运用。

关注我,获取更多技术干货

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言