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);
    
}
相关推荐
悠哉悠哉愿意4 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
Lester_11014 天前
STM32霍尔传感器输入口设置为复用功能输入口时,还能用GPIO函数直接读取IO的状态吗
stm32·单片机·嵌入式硬件·电机控制
LCG元4 天前
低功耗显示方案:STM32L0驱动OLED,动态波形绘制与优化
stm32·嵌入式硬件·信息可视化
三佛科技-187366133974 天前
120W小体积碳化硅电源方案(LP8841SC极简方案12V10A/24V5A输出)
单片机·嵌入式硬件
z20348315204 天前
STM32F103系列单片机定时器介绍(二)
stm32·单片机·嵌入式硬件
古译汉书4 天前
【IoT死磕系列】Day 7:只传8字节怎么控机械臂?学习工业控制 CANopen 的“对象字典”(附企业级源码)
数据结构·stm32·物联网·http
Alaso_shuang4 天前
STM32 核心输入、输出模式
stm32·单片机·嵌入式硬件
2501_918126914 天前
stm32死锁是怎么实现的
stm32·单片机·嵌入式硬件·学习·个人开发
z20348315204 天前
STM32F103系列单片机定时器介绍(一)
stm32·单片机
星马梦缘4 天前
驱动层开发——蜂鸣器驱动
stm32·单片机·嵌入式硬件·hal·驱动