【STM32F407学习笔记】模拟IIC协议


通信总线在嵌入式系统有着极其重要的地位,它是单片机与单片机通信,单片机与外围设备通信的一个协议,如果没有这些协议,我们就不能保证数据的有效性。我们常用的总线有USART、IIC、SPI、CAN等。
涉及外设:GPIO、MPU9250

1. IIC协议简介

IIC通信协议(Inter-Integrated Circuit)是由飞利浦公司开发的,由于它的引脚少,硬件实现简单,可扩展性强,不需要USART、CAN等通信协议的外部收发设备,被广泛地应用在系统内多个集成电路(IC)间的通信。下面我们分别对IIC协议的物理层及协议层进行讲解。

1.1 物理层

在这里插入图片描述
物理层特点:
1)支持多设备的总线。在一个IIC通信总线中,可连接多个IIC通信设备,支持多个通信主机及多个通信从机。
2)一个IIC总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。数据线用于传输数据,时钟线用于数据收发同步。
3)每个连接到总线的设备都有一个独立的地址。主机可以利用这个地址进行不同设备之间的访问。
4)总线通过上拉电阻连接到电源。当IIC设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,有上拉电阻将总线变为高电平。
5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定哪个设备占用总线。
6)具有三种传输模式:标准模式传输速率为100kbit/s,快速模式为400kbit/s,高速模式可达3.4Mbit/s,但目前大部分I2C设备尚不支持高速模式。
7)连接到相同总线的IC数量收到总线最大电容400pF限制。

1.2 协议层

IIC的协议定义了通讯的起始信号、停止信号、数据有效性、仲裁、时钟同步和地址广播等环节。

1.2.1 I2C基本读写流程

I2C的通信流程可以分为三部分,如途中所示:
在这里插入图片描述
其中灰色代表:数据从主机到从机,白色表示:数据从从机到主机,S:开始信号,SLAVE ADDRESS:从机地址,R/W:传输方向选择位(1读0写),A/Ā:应答(ACK)/非应答(NACK)信号,P:停止信号。这三个序列分别代表:主机写数据到从机、主机读取从机数据、复合通信模式。
起始信号(S):表示由主机的I2C接口产生的传输,这时连接到I2C总线上的所有从机都会接收到这个信号。
从机地址(SLAVE ADDRESS):起始信号产生后,所有从机就开始等待主机紧接着广播下来的从机地址(SLAVE ADDRESS)。在IIC总线上每个设备的地址都是唯一的,当主机广播的地址与某个从机地址相同时,这个设备就被选中了,没有被选中的设备将忽略后续的数据信号。
传输方向选择位(R/W):在地址位之后是传输方向选择位,该位为0时,表示后面的数据传输方向是有主机写数据到从机、该位为1时,即主机读取从机数据。从机地址+传输方向位一般是8位。
应答信号(A):第一种:从机接收到匹配的地址,从机返回应答信号(ACK)表示找到对应地址的从机设备;第二种,当从机被寻址,主机写数据到从机时:要求从机每接收到一个字节都需要给主机一个应答(ACK),主机只有接收到应答信号后,才能继续发送数据。主机读从机数据时:主机每接收一个字节都需要给从机一个应答(ACK),从机只有接收到应答之后,才能继续发送数据。
非应答信号(NACK):主机读取从机数据时:主机接收完最后一个字节,必须产生一个非应答信号(NACK),用来通知从机不要再发送数据了,这样从机也就释放了SDA。主机写入从机数据时:主机发送到最后一个字节时,从机给主机返回非应答信号(NACK),用来通知主机不用发送数据了,这样释放从机SDA。
停止信号(P):实验数据传输完成时,主机会给出一个停止信号,用于结束此次通信。
最常用的是复合通信模式,如图中所示的流程为:主机发出起始信号S,并产生从机地址,传输方向选择为写,从机产生一个应答信号ACK,主机写入一个字节的数据,从机产生一个ACK应答信号;再给一个起始信号,传输方向选择为读,从机产生一个ACK应答信号,并输出数据给主机,从机产生NACK,最后主机产生停止信号P。
总结:I2C通信中,SDA和SCL都是由主机控制的,从机只能将SDA线拉低而已,对于SCL线,从机是没有任何能力去控制的。

1.2.2 I2C通信各信号分解

  • 起始和停止信号
    在这里插入图片描述
    如图中所示,当IIC是空闲时SCL和SDA都是高电平。当SCL是高电平时,SDA由高电平向低电平切换,这个情况表示通信的起始;当SCL时高电平时,SDA由低电平向高电平切换,这个情况表示通信的结束。起始停止信号由主机产生。
  • 地址与数据方向
    在这里插入图片描述
    I2C总线上的每个设备都有自己的独立地址,主机发起通信时,通过SDA信号线发送从机地址来查找从机。IIC协议规定设备地址可以是7位或10位,实际中7位应用较为广泛。紧跟从机地址的一个数据位用于表示数据的传输方向,它是数据方向位(R/W),第8位或第11位。数据方向位为1,表示主机读取从机数据;数据方向位为0,表示主机向从机写入数据。
  • 数据有效性
    在这里插入图片描述
    IIC使用SDA信号线来传输数据,使用SCL信号线进行数据同步。SDA在SCL的每个时钟周期传输一位数据。传输时:SCL为高电平SDA表示的数据有效,即此时的SDA高电平表示数据1,低电平表示数据0。当SCL为低电平时,SDA的数据无效,一般在这时候(SCL低电平时)进行SDA电平切换,为下一位数据做好准备。
  • 应答与非应答
    在这里插入图片描述
    I2C的数据和地址传输都需要带响应。响应包括“应答ACK”和“非应答信号NACK”两种。作为数据接收端,当设备(无论主从机)接收到I2C传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答信号(ACK)”,发送方会继续发送下一个数据;若接收方希望结束数据传输,则向对方发送“非应答信号(NACK)”,发送方接收到该信号后会产生一个停止信号,结束信号传输。

2. GPIO模拟IIC时序

STM32F4自带IIC硬件接口,通信速度能达到400kHz。但是其缺点是通用性不好,不同芯片配置不同,引脚固定。所以我们选择使用IO口模拟IIC,它具有通用性强,移植方便,可用任意IO口模拟等特点。

2.1 GPIO模式初始化

IIC一共两条总线SDA与SCL,所以只需要两个IO口,其中SDA为数据线,主机从机都可以控制,既需要输出也需要输入,所以模拟时序时需要有IO输入与输出模式切换。SCL为时钟线,仅由主机控制,所以只需配置成输出模式。
因为在IIC总线上,有上拉电阻对电平进行拉高,在空闲时总线上是高电平,因此只需要配置输出为开漏模式(只能输出低电平)

#define RCC_IIC_SCL RCC_AHB1Periph_GPIOB // 端口时钟
#define IIC_SCL_PORT GPIOB               // 端口
#define IIC_SCL GPIO_Pin_6               // 引脚

#define RCC_IIC_SDA RCC_AHB1Periph_GPIOB // 端口时钟
#define IIC_SDA_PORT GPIOB               // 端口
#define IIC_SDA GPIO_Pin_7               // 引脚

// IO操作函数
#define IIC_SCL_H IIC_SCL_PORT->BSRR |= IIC_SCL                 // SCL置1
#define IIC_SCL_L IIC_SCL_PORT->BSRR |= (uint32_t)IIC_SCL << 16 // SCL置0
#define IIC_SDA_H IIC_SDA_PORT->BSRR |= IIC_SDA                 // SDA置1
#define IIC_SDA_L IIC_SDA_PORT->BSRR |= (uint32_t)IIC_SDA << 16 // SDA置0
#define READ_SDA (IIC_SDA_PORT->IDR & IIC_SDA) ? 1 : 0          // 读取SDA输入(IIC_SDA_PORT->IDR & IIC_SDA表示IIC_SDA口的IIC_SDA引脚)

void IIC_init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    // GPIO时钟
    RCC_AHB1PeriphClockCmd(RCC_IIC_SCL, ENABLE);
    RCC_AHB1PeriphClockCmd(RCC_IIC_SDA, ENABLE);
    // GPIO初始化
    GPIO_InitStructure.GPIO_Pin = IIC_SCL;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;      // 输出模式
    GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;     // 开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度
    GPIO_Init(IIC_SCL_PORT, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = IIC_SDA;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;      // 输出模式
    GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;     // 开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 速度
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;       // 仅对输入时有效
    GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure);

    IIC_SCL_H;
    IIC_SDA_H;
}

2.2 模拟IIC时序

  • 起始信号

    void IIC_Start(void)
    {
    	// 输出
    	SDA_OUT();
    	IIC_SDA_H;
    	IIC_SCL_H;
    	delay_us(4);
    	IIC_SDA_L;// 拉低SDA
    	delay_us(4);
    	IIC_SCL_L;// 钳住IIC总线,准备发送或接收数据
    }
    
  • 停止信号

    void IIC_Stop(void)
    {
    	SDA_OUT();
    	IIC_SCL_L;
    	IIC_SDA_L;
    	delay_us(4);
    	IIC_SCL_H;
    	IIC_SDA_H;
    	delay_us(4);
    }
    
  • 产生ACK应答

    void IIC_ACK(void)
    {
    	IIC_SCL_L;
    	SDA_OUT();
    	IIC_SDA_L;
    	delay_us(1);
    	IIC_SCL_H;
    	delay_us(2);
    	IIC_SCL_L;
    }
    
  • 产生NACK应答

    void IIC_NACK(void)
    {
    	IIC_SCL_L;
    	SDA_OUT();
    	IIC_SDA_H;
    	delay_us(1);
    	IIC_SCL_H;
    	delay_us(1);
    	IIC_SCL_L;
    }
    
  • 等待应答信号

    uint8_t IIC_waitACK(void)
    {
    	uint8_t ucErrTime=0SDA_IN();
    	IIC_SDA_H;
    	delay_us(1);
    	IIC_SCL_H;
    	delay_us(1);
    	while(READ_SDA)
    	{
    		ucErrTime++;
    		if(ucErrTime>250)
    		{
    			IIC_Stop();
    			return 1;
    		}
    	}
    	IIC_SCL_L;
    	return 0;
    }
    

3. 读取MPU6050 WHO AM I 寄存器

MPU6050中有一个WHO AM I寄存器,能够帮我们快速验证模拟IIC的实现是否正确,寄存器的位图如下:
在这里插入图片描述
这个寄存器用于验证设备的标识。WHO AM I的内容是一个8位的设备ID,寄存器的地址是0x75,寄存器的默认值是0x68。MPU6050在AD0引脚接GND(或为空)时,读取用的I2C地址为:0x68。下面实现通过IIC读取从机一个字节数据的函数:

/// @brief 读取指定设备,指定寄存器的一个值
/// @param I2C_Addr 目标设备地址
/// @param reg 寄存器地址
/// @param buf 读取数据存放地址
/// @return 1失败 0成功
uint8_t IIC_ReadByteFromSlave(uint8_t I2C_Addr, uint8_t reg, uint8_t *buf)
{
    IIC_Start();
    IIC_SendByte((I2C_Addr << 1) | 0); // 写入模式 发送从机地址:七位地址,左移一位并与上0,表示写
    if (IIC_waitACK())                 // 等待从机应答
    {
        // 非应答
        IIC_Stop();
        return 1;
    }
    IIC_SendByte(reg); // 发送寄存器地址
    IIC_waitACK();

    IIC_Start();
    IIC_SendByte((I2C_Addr << 1) | 1); // 接收模式
    IIC_waitACK();
    *buf = IIC_ReadByte(0); // 仅读取一个字节
    IIC_Stop();
    return 0;
}

然后调用函数即可读取寄存器的一个字节内容:

uint8_t ID;
IIC_ReadByteFromSlave(0x68, 0x75, &ID);
  • 4
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32F407模拟IIC代码可以使用软件模拟的方式实现。以下是一个简单的示例代码: ``` #include "stm32f4xx.h" #define IIC_SCL_PIN GPIO_Pin_6 #define IIC_SDA_PIN GPIO_Pin_7 #define IIC_SCL_PORT GPIOB #define IIC_SDA_PORT GPIOB // 初始化IIC总线 void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = IIC_SCL_PIN | IIC_SDA_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(IIC_SCL_PORT, &GPIO_InitStructure); } // IIC起始信号 void IIC_Start(void) { GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN); GPIO_SetBits(IIC_SDA_PORT, IIC_SDA_PIN); delay_us(4); GPIO_ResetBits(IIC_SDA_PORT, IIC_SDA_PIN); delay_us(4); GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); delay_us(4); } // IIC停止信号 void IIC_Stop(void) { GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); GPIO_ResetBits(IIC_SDA_PORT, IIC_SDA_PIN); delay_us(4); GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN); GPIO_SetBits(IIC_SDA_PORT, IIC_SDA_PIN); delay_us(4); } // IIC等待应答信号 // 返回: 1,接收应答失败 // 0,接收应答成功 uint8_t IIC_Wait_Ack(void) { uint8_t ucErrTime = 0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure); GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN); delay_us(1); while (GPIO_ReadInputDataBit(IIC_SDA_PORT, IIC_SDA_PIN)) { ucErrTime++; if (ucErrTime > 250) { IIC_Stop(); return 1; } } GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure); return 0; } // IIC发送一个字节 void IIC_Send_Byte(uint8_t txd) { uint8_t t; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure); GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); for (t = 0; t < 8; t++) { GPIO_WriteBit(IIC_SDA_PORT, IIC_SDA_PIN, (txd & 0x80) >> 7); txd <<= 1; delay_us(2); GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN); delay_us(2); GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); delay_us(2); } } // IIC读取一个字节 // ack=1,发送ACK // ack=0,发送NACK uint8_t IIC_Read_Byte(uint8_t ack) { uint8_t i, receive = 0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure); for (i = 0; i < 8; i++) { GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN); delay_us(2); receive <<= 1; receive |= GPIO_ReadInputDataBit(IIC_SDA_PORT, IIC_SDA_PIN); GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); delay_us(1); } if (!ack) IIC_NAck(); else IIC_Ack(); return receive; } // IIC发送应答信号 void IIC_Ack(void) { GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure); GPIO_ResetBits(IIC_SDA_PORT, IIC_SDA_PIN); delay_us(2); GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN); delay_us(2); GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); } // IIC发送非应答信号 void IIC_NAck(void) { GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_Init(IIC_SDA_PORT, &GPIO_InitStructure); GPIO_SetBits(IIC_SDA_PORT, IIC_SDA_PIN); delay_us(2); GPIO_SetBits(IIC_SCL_PORT, IIC_SCL_PIN); delay_us(2); GPIO_ResetBits(IIC_SCL_PORT, IIC_SCL_PIN); } ``` 以上是一个简单的STM32F407模拟IIC代码,可以使用该代码来实现IIC通信。需要注意的是,由于是软件模拟,因此需要延时函数来模拟IIC的时序。这里的延时函数需要根据具体的系统时钟频率进行相应的调整。此外,还需根据实际的硬件连接,在代码中正确配置IIC的引脚和端口。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值