软件i2c的实现

本文详细介绍了STM32如何通过GPIO模拟I2C通信协议,包括I2C总线的特性、数据传输流程、起始和停止条件、数据有效性以及GPIO的配置。代码示例展示了发送数据、接收数据、等待应答和发送非应答等关键操作,还涵盖了I2C寻址过程。通过对STM32的GPIO设置为开漏输出,实现了I2C通信的硬件基础。
摘要由CSDN通过智能技术生成

I2C 总线的一些特征

• 只要求两条总线线路 一条串行数据线 SDA 一条串行时钟线 SCL

• 每个连接到总线的器件都可以通过唯一的地址和一直存在的简单的主机 从机关系软件设定地
址 主机可以作为主机发送器或主机接收器

• 它是一个真正的多主机总线 如果两个或更多主机同时初始化数据传输可以通过冲突检测和仲裁
防止数据被破坏

• 串行的 8 位双向数据传输位速率在标准模式下可达 100kbit/s 快速模式下可达 400kbit/s 高速
模式下可达 3.4Mbit/s

• 片上的滤波器可以滤去总线数据线上的毛刺波 保证数据完整

• 连接到相同总线的 IC 数量只受到总线的最大电容 400pF 限制

I2C 总线术语的定义

术语描述
发送器发送数据到总线的器件
接收器从总线接收数据的器件
主机初始化发送 产生时钟信号和终止发送的器件
从机被主机寻址的器件
多主机同时有多于一个主机尝试控制总线 但不破坏报文
仲裁是一个在有多个主机同时尝试控制总线 但只允许其中一个控制总线并使报文不被破坏的过程
同步两个或多个器件同步时钟信号的过程

发送器和接收器外,器件在执行数据传输时也可以被看作是主机或从机,见上表,主机是初始化总线的数据传输并产生允许传输的时钟信号的器件,此时,任何被寻址的器件都被认为是从机

总体特征

SDASCL 都是双向线路,都通过一个电流源或上拉电阻连接到正的电源电压,见图 3 当总线空
闲时,这两条线路都是高电平,连接到总线的器件输出级必须是漏极开路集电极开路才能执行线与的功能

因为连接到总线的器件输出级必须是漏极开路集电极开路才能执行线与的功能 ,所以我们在配置IO模式时,配置成开漏输出

数据的有效性

SDA 线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟
信号是低电平时才能改变

在这里插入图片描述

.h内容如下

#ifndef _BSP_I2C_H
#define _BSP_I2C_H

#include"stm32f10x.h"
#include"stm32f10x_gpio.h"

#define I2C_CLK_FUN        RCC_APB2PeriphClockCmd
#define I2C_CLK            RCC_APB2Periph_GPIOB

//SCL
#define I2C_SCL_PORT   GPIOB
#define I2C_SCL_PIN    GPIO_Pin_6

#define I2C_SCL_Hight  GPIO_SetBits(I2C_SCL_PORT,I2C_SCL_PIN);
#define I2C_SCL_Low    GPIO_ResetBits(I2C_SCL_PORT,I2C_SCL_PIN);  

//SDA
#define I2C_SDA_PORT   GPIOB
#define I2C_SDA_PIN    GPIO_Pin_7

#define I2C_SDA_Hight GPIO_SetBits(I2C_SDA_PORT,I2C_SDA_PIN);
#define I2C_SDA_Low   GPIO_ResetBits(I2C_SDA_PORT,I2C_SDA_PIN);

	
#define I2C_SDA_READ()  ((GPIOB->IDR & I2C_SDA_PIN) != 0)	/* 读SDA口线状态 */


void I2C_Start(void);
void I2C_Stop(void);
uint8_t I2C_Wait_ACK(void);
void I2C_Send_Byte(uint8_t data);
uint8_t I2C_Read_Data(void);
void I2C_Send_Ack(void);
void I2C_Send_NAck(void);
uint8_t I2C_Check_Ack(uint8_t data);


#endif /*_BSP_I2C_H*/

GPIO初始化

void I2C_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    I2C_CLK_FUN(I2C_CLK,ENABLE);

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    //SDA
    GPIO_InitStruct.GPIO_Pin = I2C_SDA_PIN;
    GPIO_Init(I2C_SDA_PORT,&GPIO_InitStruct);
    //SCL
    GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN;
    GPIO_Init(I2C_SCL_PORT,&GPIO_InitStruct);

    I2C_Stop();
}

SDA与SCL都配置成开漏输出

起始和停止

起始条件:在 SCL 线是高电平时,SDA 线从高电平向低电平切换
停止条件:当 SCL 是高电平时 SDA 线由低电平向高电平切换

//i2c起始信号
//在SCL为高电平期间,SDA由高电平转化为低电平
void I2C_Start(void)
{
    I2C_SCL_Hight;
    I2C_SDA_Hight;
    //延时一段时间
    Delay_Us(10);
    I2C_SDA_Low;
    Delay_Us(10);
    I2C_SCL_Low;
    Delay_Us(10);
}
//i2c结束信号
//在SCL为高电平期间,SDA由低电平转化为高电平
void I2C_Stop(void)
{
    I2C_SCL_Hight;
    I2C_SDA_Low;
    Delay_Us(10);
    I2C_SDA_Hight;
}

等待响应

等待响应就好比生活中两个人打电话,拨打方打通电话之后,等待接收方回应:“喂,你好”(ACK),然后进行下面的通话。
而在I2C通信过程中,每次发送方传输一字节后,接收方就会回应一个ACK

响应时序图如下:
在这里插入图片描述

//等待响应  1:非应答 0:应答
uint8_t I2C_Wait_ACK(void)
{
    uint8_t ack;

    I2C_SDA_Hight;
    Delay_Us(10);
    I2C_SCL_Hight;
    Delay_Us(10);

    if(I2C_SDA_READ()) //非应答
        ack = 1;
    else
        ack = 0;
    I2C_SCL_Low;
    Delay_Us(10);

    return ack;
}

发送NACK

在24C02芯片手册中,观察Current Address Read这个时序,会发现在数据传输结束后,不再传输时,发送方会发送一个非应答(NACK),所以我们编写一个发送NACK的函数

在这里插入图片描述

//发送非应答
void I2C_Send_NAck(void)
{
	//拉高SDA,生成NACK
    I2C_SDA_Hight;
    Delay_Us(10);
    //读取
    I2C_SCL_Hight;
    Delay_Us(10);
    I2C_SCL_Low;
    Delay_Us(10);
}

发送数据

由上面的数据有效性知:SDA线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变

在这里插入图片描述

//发送数据
void I2C_Send_Byte(uint8_t data)
{
    uint8_t mask;  //发送数据时高位在前

    for(mask=0x80;mask!=0x00;mask>>=1)
    {
        if(data&mask)
        {
            I2C_SDA_Hight;  
        }
        else
            I2C_SDA_Low;

        Delay_Us(10);
        I2C_SCL_Hight;
        Delay_Us(10);
        I2C_SCL_Low;
    }
    I2C_SDA_Hight;     //释放总线,等待ACK

}

发送过程中,由上图可知,高位在前,低位在后
所以定义一个变量mask,这个for循环for(mask=0x80;mask!=0x00;mask>>=1)表示mask从高位为1,到最低为1的过程:10000000、01000000、00100000、、、、00000001,紧接着用data&mask,如果结果为1,即表示data相应位为1,反之为0

读取数据

读取数据过程中,SCL线必须保持高电平,当一位读取结束后,释放SCL,等待一段时间,让SDA改变数据,重复上述过程

//读取数据
uint8_t I2C_Read_Data(void)
{
    uint8_t data=0,mask;

    for(mask=0x01;mask!=0;mask>>=1)
    {
        I2C_SCL_Hight;
        Delay_Us(10);
        if(GPIO_ReadInputDataBit(I2C_SDA_PORT,I2C_SDA_PIN))
            data |= mask;     //读取结束为1
        else
            data &= ~mask;    //读取结束为0
        
        
        I2C_SCL_Low;
        Delay_Us(10);
    }

    return data;
}

mask的用法与发送数据时的类似

寻址

在这里插入图片描述
在这里插入图片描述
由图可以得到
一、寻址过程是通常在起始条件后的第一个字节决定了主机选择哪一个从机
二、当发送了一个地址后 系统中的每个器件都在起始条件后将头 7 位与它自己的地址比较
三、如果一样器件会任务它被主机寻址 至于是从机 接收器还是从机 发送器都由 R/ W 位决定
四、使用这个地址时 理论上所有器件都会发出一个响应

所以可以总结出寻址的步骤:发送起始信号->发送设备地址->等待ACK->发送停止信号

若是代码无误,设备地址正确,会等到一个数值:0

得到设备地址
打开开发板配套的原理图,找到EEPROM(这里是以野火霸道开发板为例)电路,然后再在24C02芯片手册中找到Figure 1. Device Address,由二者可以得到EEPROM的前七位为1010000,然后最低位使用W,所以得到地址:0xA0
在这里插入图片描述
在这里插入图片描述
寻址代码如下

//检测应答
uint8_t I2C_Check_Ack(uint8_t data)
{
    uint8_t Ack;

    I2C_GPIO_Config();
    I2C_Start();

    I2C_Send_Byte(data);
    Ack = I2C_Wait_ACK();

    I2C_Stop();

    return Ack;
}

main.c中调用寻址函数,传入形参为:0xA0
在这里插入图片描述
输出结果如下:
在这里插入图片描述经结果知,驱动代码正确

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

点灯大师~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值