STM32之IIC

1. 串口的缺点

• 如图,这是一幅STM32的实物图:

• 两个设备可以通过串口来进行通信,通过对TX和RX引脚交叉接线。
• 从这里可以看出,在这块开发板上面 可以知道 有3个串口可以用来通信,但是串口通信是一对一的,也就是说仅仅只能接三个串口设备通信 。所以这会导致通信接口不够用。

2. IIC总线

• IIC总线的电路结构,如图:

• IIC串行总线一般有两根信号线,一根是双向通信的数据线SDA,另一根是时钟线SCL,其中时钟信号是由主机产生。所有接到IIC总线上的设备 的 串行数据线SDA 都接到总线的SDA上,各个设备的时钟线SCL接到总线的SCL上。对于并联在总线上的每个从机设备都有唯一的地址(主机是通过地址来和从机进行通信)。
• SCL是传输时钟信号的,由主机产生,发给从机。在每个时钟周期传输一个数据。时钟信号的频率会影响数据传输的速度。
• SDA是传输的有效数据,低电压是0,高电压是1.
• 这里的地址分配是0000000到1111111之间一共有128个,也就是说IIC总线最多可以接一百多个设备。

2.1 逻辑线与

• 如图:

• 逻辑与其实就是一种与运算,其结果就是0或者1。

• 逻辑线与的结果(0或者1),可以反映出IIC总线的状态(高电压还是低电压)。

• 逻辑线与在硬件电路上的表现,其关键是总线的上拉电阻和输出模式设置为开漏,如图所示

• 这其中关键是,要发送的设备可以发送 0 或者 1 ,但是其他设备应该全部设置为 1( 满足逻辑线与 )

• 如图:

• 时钟信号是由主机发出的,所以此时让所有的从机设备都输出1,相当于让从机的SCL引脚和SCL总线断连(此时从机的SCL引脚表现出来的是高阻抗状态)。

• 如果此时的主机输出1,此时主机的SCL引脚也和总线断开连接,主机的SCL引脚也表现为高阻抗状态,此时原本SCL总线是浮空状态,但是由于上拉电阻的作用,把SCL总线拉高,此时SCL的状态是高电压状态(1)。

2.2 第二种情况

• 如图:

• 如果此时主机SCL输出的0,这就说明此时SCL总线呈现出低电压状态(0),此时的上拉电阻是不起作用的了,这就相当于总线SCL接地了,接在主机的SCL引脚上。

2.2.1 主机如何发送时钟信号,如下图:

• 如图,SCL信号是由主机产生,此时让所有从机设备都输出是1,相当于从机设备的SCL引脚与SCL总线全部断开,此时从机的SCL引脚的状态是高阻抗。

• 主机输出0,相当于让SCL总线是接地的了,在主机的SCL处,表现出是低电压状态,上拉电阻此时不起作用的。

• 第二种情况,如图:

• 如果主机输出的是1,相当于SCL总线上所有设备都断开,IIC总线的所有设备的SCL引脚表现出高阻抗状态,由于上拉电阻的作用,此时总线的状态是高电压。

• 上图,逻辑与运算的值,完全由主机决定。

2.2.2 主机如何发送数据,如图:

• 如图,让此时的从机设备的SDA引脚都输出是1(SDA引脚表现出高阻抗状态),相当于让从机设备的SDA引脚和SDA总线断开。

• 主机的SDA引脚发0,相当于SDA总线此时是接地的了,此时SDA总线表现的状态为低电压。

• 第二种情况:

• 如图,主机输出1,相当于总线所有设备的SDA引脚和总线的SDA断开。

• 此时SDA总线表现的状态为高电压。由上拉电阻提供的。

2.2.3 从机如何发送数据,如图:

• 让主机和部分从机输出1,相当于这部分的SDA引脚和总线SDA断连了,如果此时有一个从机设备输出的是0,那么此时总线的SDA总线表现出是低电压的状态。因为此时输出0的那一个设备,让总线接地了,此时的上拉电阻不起作用。

• 第二种情况:

• 如果此时有一个从机设备输出的是1,相当于这个从机设备的SDA引脚与总线的SDA断连,那么此时总线的SDA总线表现出是高电压的状态,这是由上拉电阻提供的。

3. IIC的数据格式

3.1 IIC通信的基本流程,如图:

• 基本流程是:主机发送开始信号(起始位) -》然后再发送从机地址(7位 + 传输方向位(是写还是读)) -》 然后开始发送或者读取 -》如果接收完数据,发送结束信号(停止信号)。

3.2 IIC的数据帧格式,如图:

• IIC的数据帧格式:如上图,起始位 -》地址 + 应答信号 -》 数据 + 应答信号 -》停止位。

• 串口的数据位格式和IIC数据位格式不一样的。

• 串口的数据位格式,每发完一次数据后面需要有停止位。

• IIC发完一个字节的数据后跟这应答信号,然后在紧接发送下一个字节数据,直到发完所有数据才是停止位。

• IIC是完整发完一个字节数据,而串口的数据位可以是8个bit也可以是9个bit

• IIC是高位先行,串口是低位先行。

3.3 起始位和停止位,如图:

• 起始位的条件是:SCL为高电平,SDA是由高电平变为低电平,有一个下降的过程。

• 停止位的条件是:SCL为高电平,SDA是由低电平变为高电平,有一个上升的过程。

• 注意:在发送数据的过程(SCL高电压期间),SDA的数据不能发生改变。

3.4 寻址,如图:

• 在发完起始位之后,然后发送的地址是0x78,变为二进制是0111 1000,最后一位要变为1(因为是读的方向),所以变成了0111 1001。

• ACK是应答信号,这里主机发完地址之后,从机需要回复一个应答(类似于喊"到")。

• 这里的ACK是主机释放SDA之后,然后由从机拉低SDA,表示有效应答。

• 解释:主机释放SDA总线后,原本SDA应该是浮空,但是由于上拉电阻的作用,此时SDA表现为高电压状态,之后由从机拉低SDA,表示有效应答。

3.5 传输数据,如图:

• 例子,如图:


• 第二个其实发的不是ACK,而是NACK就是主机不想应答,SDA就一直保持高电压,之后主机在发送一个停止信号(就仅仅接收一个字节,之后就接收结束)。

4. IIC的使用方法

4.1 IIC的速度模式

• 波特率:每秒钟传输的位数,如图:

• IIC的波特率有一下几种,如图:

• 但是仅仅只有前两个是单片机有的,也就是单片机是不支持超过400kbps的波特率。

4.2 时钟信号的占空比,如图:

• 一般情况,没特殊要求是选择用第一种

• 注:只有在快速模式下才可以设置时钟信号的占空比。

• 作用:

• 在IIC通信时,只有在时钟信号(SCL)为高电平时,数据线(SDA)上对应的数据才是有效数据,所以若高电平持续时间短的话,读写时间短,数据可能没有读写完,导致数据传输不准确;若高电平持续时间长的话,读写时间相应变长,可能导致时间的浪费,通信效率变低。

4.3 编程接口

4.4 IIC框图

4.5 初始化IIC的代码

cpp 复制代码
#include "stm32f10x.h"

void My_I2C_Init();

int main(void)
{
    My_I2C_Init();//IIC的初始化
    
    while(1)
    {
    }
}


void My_I2C_Init(){
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//开启APIO的时钟
    
    GPIO_PinRemapConfig(GPIO_Remap_I2C1,ENABLE);//重映射IIC1
    
    //初始化GPIO引脚,PB8 == SCL PB9 == SDA
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//开启GPIOB时钟
    
    GPIO_InitTypeDef gpio_initStruct = {0};
    
    gpio_initStruct.GPIO_Mode = GPIO_Mode_AF_OD;//实现逻辑线与
    gpio_initStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    gpio_initStruct.GPIO_Speed = GPIO_Speed_2MHz;
    
    GPIO_Init(GPIOB,&gpio_initStruct);
    
    
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);//开启IIC时钟
   //在使用IIC之前需要对IIC重启
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,ENABLE);//添加复位信号
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,DISABLE);//释放复位信号
    
    I2C_InitTypeDef i2c_initStruct = {0};
    
    i2c_initStruct.I2C_ClockSpeed = 400000;
    i2c_initStruct.I2C_DutyCycle = I2C_DutyCycle_2;
    i2c_initStruct.I2C_Mode = I2C_Mode_I2C;
    //单片机一般是作为IIC的主机,很少是作为从机的。
    I2C_Init(I2C1,&i2c_initStruct);
    I2C_Cmd(I2C1,ENABLE);//使能I2C
    
}

5. 发送数据

• I2C模块的内部结构框图:

• 这是IIC模块的内部框图:

• 把要发送的数据 放到 发送数据寄存器里面,然后通过并行传输,发送到发送数据移位寄存器里面,然后一位一位地发送出去,通过SDA引脚。

• 要接收数据要通过SDA引脚来接收,数据一位一位地接收到接收数据移位寄存器,然后到达一个字节后会并行传输给接收数据寄存器。

• SCL引脚是是发送时钟信号的。

• SR是状态寄存器,有两个,表示IIC的工作状态。

• 上图黄色那部分是控制寄存器,控制IIC模块的起始,终止等。

• SDA控制:将要发送的数据变成波形,通过SDA引脚发送出去,或者通过SDA引脚接收波形,然后解析接收到的波形,变为一个字节的数据。

• SCL控制就是将时钟信号变成波形发送出去。

• SCL控制寄存器,可以设置波特率,占空比和波特率的模式。

5.1 数据发送过程简介,如图:

• 数据发送的过程:

• 首先控制寄存器里面的START位写1,表示起始信号。

• 发送地址,将要发送的地址,先发到发送数据寄存器,然后通过SDA发出,等待从机的响应。

• 发送数据,主机发送数据,从机回复ACK,一直将数据发送完为止。

• 最后主机发送停止位(在控制寄存器里面的STOP位写1)。

5.2 等待总线空闲,如图:

• 要等待上次总线发完数据,才能开始发送。

5.3 发送起始位,如图:

• 发送起始位,需要在控制寄存器里面的START写1,然后再查看状态寄存器里面的SB位,起始位是否发送成功。

5.4 发送地址,如图:

• 再发送数据之前,需要清除状态寄存器里面的AF位,然后开始发送地址。

• 之后再判断是否寻址成功和是否有从机回复ACK,通过查询AF位和ADDR位置

• 如果寻址成功,如图:

5.5 发送数据,如图:

• 发送数据之前需要判断,发送数据寄存器是否为空,还有上一次的数据是否被拒收。

• 再发完之后,如图:

• 需要判断上一次是否被拒收,还有移位寄存器是否为空,如果为空就说明数据已经发完了。

5.6 发送停止位,如图:

• 要在控制寄存器里面的STOP位写1,表示传输的结束。

6. 读取数据的流程:

• 如图:

• 这是主机接收数据的流程图

• 这其中,前半部分和发送数据几乎一样的。

• 首先控制寄存器里面的START位写1,表示起始信号,数据接收的开始。

• 发送地址,将要发送的地址,先发到发送数据寄存器,然后通过SDA发出,等待从机的响应。如果从机响应了,主机准备开始接收数据,如果没有从机响应,主机就得发送停止位,表示传输的结束。

• 但是RW方向和发送的不一样。

• 数据的传输方向是,从机发送数据,主机就收数据。

• 在数据传输的时候,从机发送一个数据,主机就回复一个ACK。但是在发送最后一个字节的时候,主机回复的是NAK。

• 如果主机回复的是ACK,那么从机就认为主机还需要继续接收数据,那么从机就会继续发数据。然后这有可能影响到停止位,可能导致停止位发不出来。

• 主机发送NAK可以避免 和 停止位发生冲突。

• 最后主机发送停止位(在控制寄存器里面的STOP位写1)。

6.1 发送起始位和地址

6.1.1 发送起始位

• 在控制寄存器里面的START位写1就可以了。如图:

6.1.2 发送地址,如图:

• 再发送数据之前,需要清除状态寄存器里面的AF位,然后开始发送地址。

• 之后再判断是否寻址成功 和 是否有从机回复ACK,通过查询AF位和ADDR位置,如果没有寻址成功,主机就会直接发送停止位结束,寻址成功就进行下一步操作。

6.2 如何发送ACK和NAK

• 发送ACK是在控制寄存器里面的ACK位,写0是NAK,写1是ACK。这里面的值,只作用于正在被接收的字节

• 如图,例1

• 如图此时字节2正在被接收,这就需要"赶紧"在ACK里面写1,表示的是这次数据的ACK,如果不"赶紧"写的话,可能就会来不及,导致这次ACK跑到了另外的字节那里。

• 当前在ACK位写数据,作用的是字节2。

• 如图,例2

• 如果,此时在控制寄存器ACK里面写值,控制的不是字节2,而是字节3,因为字节2的值已经被接收完了,此时的ACK或者NAK已经发出去了。

• 所以,发送ACK或者NAK一定要"赶紧发"。

6.3 如何发送停止位

• 在控制寄存器里面的STOP位写1。

• 如图:

• 作用的也是当前正在被接收的字节。

• 如上图,字节1正在被接收,等字节1发完之后,后面才跟STOP位。也就说等字节1接收完才在后面写1,但是也是作用在这个字节上的。

6.4 Size = 1,如图:

6.5 Size > 2,如图:

cpp 复制代码
#include "stm32f10x.h"

void i2c1_init();
int i2c1_sendBytes(I2C_TypeDef* I2Cx,uint8_t addr,uint8_t* pdata,uint16_t size);
int i2c1_receiveBytes(I2C_TypeDef* I2Cx,uint8_t addr,uint8_t* pdata,uint16_t size);
void led_init();


int main(void)
{
    i2c1_init();
    led_init();
    uint8_t command[] = {0x00,0x8d,0x14,0xaf,0xa5};
    i2c1_sendBytes(I2C1,0x78,command,5);
    
    uint8_t recv;
    i2c1_receiveBytes(I2C1,0x78,&recv,1);
    
    if((recv & (0x01 << 6)) == 0){
        GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
    }else {
         GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
    }
    
    
	while(1)
	{
	}
}


void led_init(){
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
    
    GPIO_InitTypeDef gpio_initstruct = {0};
    
    gpio_initstruct.GPIO_Mode = GPIO_Mode_Out_OD;//实现逻辑线与
    gpio_initstruct.GPIO_Pin = GPIO_Pin_13;
    gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
    
    
    GPIO_Init(GPIOC,&gpio_initstruct);
    
    GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}

int i2c1_receiveBytes(I2C_TypeDef* I2Cx,uint8_t addr,uint8_t* pBuffer,uint16_t size){
    //返回值  0:成功,-1:寻址失败
    
    //发送起始位
    
    I2C_GenerateSTART(I2Cx,ENABLE);
    while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB) == RESET);//等待起始位发送完成
    
    //发送地址
    I2C_ClearFlag(I2Cx,I2C_FLAG_AF);//发送地址之前需要将AF位清0
    
    I2C_SendData(I2Cx,addr | 0x01);//发送地址
    
    //判断是否发送成功,还是失败
    while(1){
        if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET){
            I2C_GenerateSTOP(I2Cx,ENABLE);
            return -1;//寻址失败
        }
        
        if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET){
            break;//寻址成功
        }
    }
    
    //当size = 1的时候,也就是说接收一个字节的时候
    if(size == 1){
        //先清零ADDR位,后面的数据才可能进来
        I2C_ReadRegister(I2Cx,I2C_Register_SR1);
        I2C_ReadRegister(I2Cx,I2C_Register_SR2);
        
        //然后数据就开始进来接收移位寄存器了
        //这时候是要"赶紧"在ack写0和stop写1
        //先ACK写0,主机发送NAK
        I2C_AcknowledgeConfig(I2Cx,DISABLE);
        //STOP写1,停止位
        I2C_GenerateSTOP(I2Cx,ENABLE);
        
        //等待接收数据寄存器有值
        while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
        
        pBuffer[0] = I2C_ReceiveData(I2Cx);
        
      
        
    }else if(size == 2){//当size = 2的时候,也就是说接收2个字节的时候
        //先清零ADDR位,后面的数据才可能进来
        I2C_ReadRegister(I2Cx,I2C_Register_SR1);
        I2C_ReadRegister(I2Cx,I2C_Register_SR2);
        
        //然后数据就开始进来 接收移位寄存器了
        //这时候是要"赶紧"在ack写1,因为这是第一接收到的数据
        //先ACK写1,主机发送ACK
        I2C_AcknowledgeConfig(I2Cx,ENABLE);
       
        
        //等待接收数据寄存器有值
        while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
        //读取第一个数据
        pBuffer[0] = I2C_ReceiveData(I2Cx);
        
        //此时第二个数据就进来了,这时候需要"赶紧"对ACK写0和STOP写1
        //先ACK写0,主机发送NAK
        I2C_AcknowledgeConfig(I2Cx,DISABLE);
        
         //STOP写1,停止位
        I2C_GenerateSTOP(I2Cx,ENABLE);
        
        //等待接收数据寄存器有值
        while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
        //读取第二个数据
        pBuffer[1] = I2C_ReceiveData(I2Cx);
    
    }else {//当size > 2的时候,也就是说接收多个字节的时候
        //先清零ADDR位,后面的数据才可能进来
        I2C_ReadRegister(I2Cx,I2C_Register_SR1);
        I2C_ReadRegister(I2Cx,I2C_Register_SR2);
        
        //然后数据就开始进来 接收移位寄存器了
        //这时候是要"赶紧"在ack写1,因为这是第一个接收到的数据
        //先ACK写1,主机发送ACK
        I2C_AcknowledgeConfig(I2Cx,ENABLE);
        //此时的控制器里面的ACK位是1,也就说一个字节接收完后面会发ACK响应。
        
        //因为前n - 1个的数据都需要主机发送ACK的
        for(uint16_t i = 0;i < size - 1;i++){
            //等待接收数据寄存器有值
            while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
            //读取数据
            pBuffer[i] = I2C_ReceiveData(I2Cx);
        }
    
        //等到前n - 1个数据读取完之后,最后一个数据来到接收移位寄存器钟
        //这时候需要"赶紧"对ACK写0和STOP写1
        
        //先ACK写0,主机发送NAK
        I2C_AcknowledgeConfig(I2Cx,DISABLE);
        
         //STOP写1,停止位
        I2C_GenerateSTOP(I2Cx,ENABLE);
        
        //等待接收数据寄存器有值
        while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
        //读取最后一个数据
        pBuffer[size - 1] = I2C_ReceiveData(I2Cx);
        
        return 0;
        
    }
    
}

int i2c1_sendBytes(I2C_TypeDef* I2Cx,uint8_t addr,uint8_t* pdata,uint16_t size){
    //返回值  0:成功,-1:寻址失败  -2:发送数据被拒绝。
    
    //等待总线空闲
    while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BUSY) == SET);
    
    //发送起始位
    
    I2C_GenerateSTART(I2Cx,ENABLE);
    while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB) == RESET);//等待起始位发送完成
    
    //发送地址
    I2C_ClearFlag(I2Cx,I2C_FLAG_AF);//在发送地址前,需要给AF位清零
    
    I2C_SendData(I2Cx,addr & 0xfe);//发送地址,写是最后一位是0
    
    while(1){//判断是否寻址成功,从机是否返回ACK
        if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET){//寻址成功
            break;
        }
        
        if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET){//没有返回ACK,寻址失败
            I2C_GenerateSTOP(I2Cx,ENABLE);
            return -1;
        }
    }
    //清零ADDR,两个状态寄存器
    I2C_ReadRegister(I2Cx,I2C_Register_SR1);
    I2C_ReadRegister(I2Cx,I2C_Register_SR2);
    
    //发送数据
    for(uint16_t i = 0;i < size;i++){
        //在发送之前需要判断前一个是否发送成功,还有发送数据寄存器里面有没有值
        while(1){
            if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET){
                I2C_GenerateSTOP(I2Cx,ENABLE);
                return -2;
            }
            
            if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_TXE) == SET){
                break;
            }
        }
        I2C_SendData(I2Cx,pdata[i]);
    }
    //在发送之前需要判断前一个是否发送成功,
    //还有发送数据寄存器和发送移位寄存器里面有没有值
    while(1){
        if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET){
            I2C_GenerateSTOP(I2Cx,ENABLE);
            return -2;
        }
        
        if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BTF) == SET){
            break;
        }
        
    }
    //发送停止位
    I2C_GenerateSTOP(I2Cx,ENABLE);
    return 0;
}



void i2c1_init(){
    
    //配置GPIO
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
    
    GPIO_PinRemapConfig(GPIO_Remap_I2C1,ENABLE);
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
    
    GPIO_InitTypeDef gpio_initstruct = {0};
    
    gpio_initstruct.GPIO_Mode = GPIO_Mode_AF_OD;//实现逻辑线与
    gpio_initstruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
    
    
    GPIO_Init(GPIOB,&gpio_initstruct);
    
    //配置IIC
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
    //在使用IIC之前,需要对IIC重启
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,ENABLE);
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,DISABLE);
    
    I2C_InitTypeDef i2c_initstruct = {0};
    
    i2c_initstruct.I2C_ClockSpeed = 400000;
    i2c_initstruct.I2C_DutyCycle = I2C_DutyCycle_2;
    i2c_initstruct.I2C_Mode = I2C_Mode_I2C;
    
    I2C_Init(I2C1,&i2c_initstruct);
    
    I2C_Cmd(I2C1,ENABLE);
    
}

7. 软件IIC

7.1 硬件IIC的缺点

• 操作繁琐,参考上面的代码。

• 引脚位置受到限制。如果刚好这个引脚被其他模块使用就没法使用IIC了。

7.2 怎么解决软件IIC的缺点,如图:

• 从该图可以知道使用IIC传输数据的时候,SCL和SDA的波形,还有发送数据时的数据格式,还有每个阶段的波形,所以可以通过软件的形式模拟IIC。

• 软件IIC的引脚:在软件IIC中,GPIO引脚是可以随便选择的,GPIO的模式是通过开漏,可以由GPIO直接输出0或者1。

7.3 发送起始位和停止位,如图:

• 起始位:

• 在发送起始位之前,总线SCL和SDA是处于空闲的状态,为高电压状态。

• 起始位,需要SDA在SCL为高电压的时候,产生一个下降沿的信号,SDA由高电压变为低电压,表示数据传输的开始。

• 停止位:

• 如上图,我们知道停止位,需要SDA在SCL为高电压的时候,产生一个上升沿的信号,SDA由低电压变为高电压,表示数据传输的结束。

• 在SCL为高电压的时候,SDA的状态是不确定的,是高还是低不确定

• 然后先把SCL拉低和SDA拉低

• 经过1us之后,拉高SCL

• 在经过1us之后,

• 拉高sda,拉高时间为1us。

7.4 发送一个字节

• 如图,这是IIC数据传输的格式:

• 在SCL = 0处,发送方准备数据,放到SDA里。

• 在SCL = 1处,接收方读取数据,在这个期间SDA的数据不能发生改变,不然误以为是一个起始信号或者停止信号。

• ACK处,如图:

• 接收完一个字节数据之后,接收方需要给发送方回复一个ACK(0)或者NAK(1)。

• 需要先把SCL拉低,然后释放SDA,然后延时1us,此时SDA主动权不在主机,在从机手里。

• 拉高SCL,1us之后读取SDA总线上的数据。

• 过程如图:

• 位运算的图:

7.5 接收一个字节

• 接收一个字节和发送一个字节的流程是差不多的。

• 如图:

  • 首先是拉低SCL,然后释放SDA。
  • 经过1us延时之后,这个时间发送方是准备数据。
  • 然后拉高SCL,延时1us之后,接收方开始接收数据。
  • 接收完一个字节需要回复一个ACK或者NAK。
  • 这里是的ACK是1。
  • 先拉低SCL,准备数据,延时1us后。
  • 拉高SCL,接收方发送ACK,延时1us
  • 发送方读取ACK。

7.6 代码

cpp 复制代码
#include "stm32f10x.h"
#include "usart.h"
#include "delay.h"

void SIIC_Init();
void scl_write(uint8_t level);//向scl写0或者写1
void sda_write(uint8_t level);//向sda写0或者写1
uint8_t sda_read();//向sda读0或者读1
void delay_us(uint32_t us);//毫秒级别的延时
void sendStart();//起始位
void sendStop();//停止位
uint8_t send_byte(uint8_t byte);//发送一个字节,0表示ACK
uint8_t receive_byte(uint8_t ack);//读取一个数据
int SI2C_sendBytes(uint8_t addr,uint8_t * pdata,uint16_t size);//读
int SI2C_receiveBytes(uint8_t addr,uint8_t * pBuffer,uint16_t size);//写
void usart1_init();


void led_init(){
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
    
    GPIO_InitTypeDef gpio_initstruct = {0};
    
    gpio_initstruct.GPIO_Mode = GPIO_Mode_Out_OD;//实现逻辑线与
    gpio_initstruct.GPIO_Pin = GPIO_Pin_13;
    gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
    
    
    GPIO_Init(GPIOC,&gpio_initstruct);
    
    GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}


int main(void)
{
    SIIC_Init();
    usart1_init();
    uint8_t command[] = {0x00,0x8d,0x14,0xaf,0xa5};
    SI2C_sendBytes(0x78,command,5);
    led_init();
    uint8_t recv;
   
    //My_USART_Printf(USART1,"recv:%d\r\n",recv);
    SI2C_receiveBytes(0x78,&recv,1);
    My_USART_Printf(USART1,"recv:%d,%x\r\n",recv,recv);
    if((recv & (0x01 << 6)) == 0){
        GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
    }else {
        GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
    }
	while(1)
	{
	}
}



void usart1_init(){
    //配置串口的GPIO
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIOA的时钟
    
    GPIO_InitTypeDef gpio_initstruct = {0};
    
    gpio_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio_initstruct.GPIO_Pin = GPIO_Pin_9;
    gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
    
    GPIO_Init(GPIOA,&gpio_initstruct);
    
    //配置USART
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
    
    USART_InitTypeDef usart_initstruct = {0};
    
    usart_initstruct.USART_BaudRate = 115200;
    usart_initstruct.USART_Mode = USART_Mode_Tx;
    usart_initstruct.USART_Parity = USART_Parity_No;
    usart_initstruct.USART_StopBits = USART_StopBits_1;
    usart_initstruct.USART_WordLength = USART_WordLength_8b;
    
    USART_Init(USART1,&usart_initstruct);
    
    USART_Cmd(USART1,ENABLE);
    
}

int SI2C_sendBytes(uint8_t addr,uint8_t * pdata,uint16_t size){
    sendStart();//发送起始位
    
    if(send_byte(addr & 0xfe) != 0){//发送地址
        sendStop();
        return -1;//寻址失败
    }
    for(uint16_t i = 0;i < size;i++){//发送数据
        if(send_byte(pdata[i]) != 0){
            sendStop();
            return -2;//发送数据失败
        }

    }
    
    sendStop();
    return 0;//数据发完
    
}

int SI2C_receiveBytes(uint8_t addr,uint8_t * pBuffer,uint16_t size){
    sendStart();//发送起始位
    
    
    if(send_byte(addr | 0x01) != 0){//发送地址
        sendStop();
        return -1;//寻址失败
    }
    
    
    for(uint16_t i = 0;i < size - 1;i++){//接收数据
       pBuffer[i] = receive_byte(1);
    }
    
    pBuffer[size - 1] = receive_byte(0);
    sendStop();
    return 0;
}

uint8_t receive_byte(uint8_t ack){
     uint8_t byte = 0;
     for(int8_t i = 7;i >= 0;i--){
        scl_write(0);//先把scl拉低,让发送方准备数据
        sda_write(1);//主机释放SDA,主动权交出去
        delay_us(1);//从机准备数据
        scl_write(1);//开始传输
        delay_us(1);//经过1us之后,读取
        if(sda_read() != 0){
            byte |= (0x01 << i);
            
        }
     }
    //一个字节读完,需要给从机发送ack或者nak
    scl_write(0);//先把scl拉低,
    sda_write(!ack);
    delay_us(1);
    scl_write(1);
    delay_us(1);
    return byte;
}

uint8_t send_byte(uint8_t byte){
    
    for(int8_t i = 7;i >= 0;i--){//数据一位一位发送,而且IIC是高位先行
        scl_write(0);//先把scl拉低,让发送方准备数据
        if((byte & (0x01 << i)) != 0){
            sda_write(1);
        }else {
            sda_write(0);
        }
        //sda_write(x);
        delay_us(1);//延时1us,让数据放到SDA上
        scl_write(1);//拉高scl,让接收方准备接收数据
        delay_us(1);//延时1us,接收数据
    }
    //一个字节发送完,需要接收接收方的ACK
    scl_write(0);//先把scl拉低,
    sda_write(1);//释放SDA,把发送的主动权交给从机
    delay_us(1);//延时1us,等待从机准备数据
    
    scl_write(1);//把scl拉高,
    delay_us(1);//延时1us,从机传输ACK或者NAK
    
    return sda_read();//主机读取这时SDA的值。是ACK还是NAK
    
}

void sendStart(){
    //在发送起始位之前,总线SCL和SDA是处于空闲的状态,为高电压状态
    sda_write(0);
    delay_us(1);
  
    
}

void sendStop(){
    scl_write(0);
    sda_write(0);
    delay_us(1);
    
    scl_write(1);
    delay_us(1);
    
    sda_write(1);
    delay_us(1);
}

void scl_write(uint8_t level){//向scl写0或者写1
    if(level == 0){
        GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
    }else {
        GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
    }
    
}

void sda_write(uint8_t level){//向sda写0或者写1
    if(level == 0){
        GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_RESET);
    }else {
        GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET);
    }

}

uint8_t sda_read(){//向sda读0或者读1
   if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1) == Bit_SET){
        return 1;
   }else {
        return 0;
   }
}

void delay_us(uint32_t us){//毫秒级别的延时
    uint32_t n = us * 8;//对于我们现在这个单片机的配置来说,执行一次for就需要消耗八分之一us
    for(uint32_t i = 0;i < n;i++);
}


void SIIC_Init(){
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    
    GPIO_InitTypeDef gpio_initstruct = {0};
    
    gpio_initstruct.GPIO_Mode = GPIO_Mode_Out_OD;
    gpio_initstruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
    
    GPIO_Init(GPIOA,&gpio_initstruct);
    
    //默认给两个总线,在空闲的时候下,为高电压状态。
    
    GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
    GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET);
    
}
相关推荐
梁朝辉3 小时前
STM32H750VBT6中ADCINP与INN什么区别
stm32·单片机·嵌入式硬件
崇山峻岭之间5 小时前
单片机无感FOC驱动之ADC
单片机·嵌入式硬件
Ligocious5 小时前
1.点亮一颗小小的LED
单片机·嵌入式硬件
KaifuZeng9 小时前
通信与接口协议面试八、CAN通信
单片机·嵌入式硬件·面试·通信与接口协议
智源单片机设计10 小时前
基于单片机的直流电机双闭环调速系统设计
单片机·嵌入式硬件
kebidaixu10 小时前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片详解
stm32
kebidaixu11 小时前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片调试实录:四个关键问题与解决方案
stm32
wuyk5551 天前
24. C 语言模块化:不是拆几个.c 文件那么简单
c语言·开发语言·stm32·单片机
lzqrzpt1 天前
LED驱动电源选型标准与工程应用技术要点解析
python·单片机·嵌入式硬件·物联网