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

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

一、为什么要用软件模拟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编程,并在项目中灵活运用。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值