10:IIC通信

1:IIC通信

I2C总线(Inter IC BUS) 是由Philips公司开发的一种通用数据总线,应用广泛,下面是一些指标参数: 

  • 两根通信线:SCL(Serial Clock,串行时钟线)、SDA(Serial Data,串行数据线)。
  • 同步,半双工。
  • 主从机通信过程中,会带数据应答。
  • 支持总线挂载多设备(一主多从、多主多从)。“多主多从”模式更复杂,下面没有特殊说明,默认都是“一主多从”模式。
  • 高位先行。

那同步时序和异步时序的优缺点各是什么呢?

  • 同步时序:优点是对时间要求不严格,可以软件手动翻转电平实现通信,所以可以大幅降低单片机对外围硬件电路的依赖,在一些低端单片机没有硬件资源的情况下,很容易使用软件来模拟时序;缺点就是多一个时钟线。
  • 异步时序:优点是少一根时钟线,节省资源;缺点是对时间要求严格,对硬件电路的依赖严重。

一个通信协议,就必须在硬件和软件上都做出规定 

  • 硬件上规定电路如何连接、端口的输入输出模式等
  • 软件上规定通信时序、字节如何传输、高位/低位先行等

 

硬件电路规定所有I2C设备的SCL连在一起,SDA连在一起。设备的SCL和SDA均要配置成 开漏输出模式。SCL和SDA各添加一个 上拉电阻,阻值一般为4.7KΩ左右。
总结:为防止总线没协调好,导致电源短路,采用 外置弱上拉电阻+开漏输出 的电路结构。

  • CPU:主机,对总线控制权力大。任何时候,都是主机对SCL完全控制。空闲状态下,主机可以主动发起对SDA的控制。
  • 被控ICx:从机,对总线控制权力小。任何时刻,从机都只能被动读取SCL线。只有在从机发送数据、或者从机发送应答位时,从机才短暂的具有对SDA的控制权。
  • 从机设备地址:为了保证通信正常,每个从机设备都具有一个唯一的地址。在I2C协议标准中,从机设备地址分为7位地址、10位地址,其中7位较为简单且应用更广泛。不同的I2C模块出厂时,厂商都会为其分配唯一的7位地址,具体可以在芯片手册中查询,如MPU6050的地址是110_1000,AT24C02的地址是101_0000等,其中器件地址的最后几位是可以在电路中改变的。总线上都是不同模块一般不会有地址冲突,若总线上有相同的模块就需要外界电路来相应的器件地址。
  • 通信引脚结构:输入线都很正常。输出线则采用开漏输出,引脚下拉是“强下拉”,引脚上拉则处于“浮空状态”。于是所有设备都只能输出低电平,为了避免高电平造成的引脚浮空,就需要在总线外面设置上拉电阻(弱上拉)。采用这个模式的好处:
  1.     完全杜绝了电源短路的情况,保证了电路安全。
    2.避免了引脚模式的频繁切换。开漏+弱上拉的模式,同时兼具了输入和输出的功能。要想输出就直接操纵总线;要想输入就直接输出高电平,然后读取总线数据,无需专门将GPIO切换成输入模式。
    3.此模式可以实现“线与”功能。只有总线上所有设备都输出高电平,总线才是高电平;否则只要有一个设备输出低电平,总线就是低电平。I2C可以利用这个特征,执行多主机模式下的时钟同步(所以SCL也采用此模式)和总线仲裁。

下面介绍I2C通信的软件规定-----时序结构(一主多从) 

下面的这些操作的主语都是“主机”。

  • 起始条件:SCL高电平期间,SDA从高电平切换到低电平。
  • 终止条件:SCL高电平期间,SDA从低电平切换到高电平。
  • 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
  • 接收一个字节:主机在接收之前释放SDA,只控制SCL变化。SCL低电平期间,从机将数据位依次放到SDA线上(高位先行,且一般贴着SCL下降沿变化);SCL高电平期间,主机读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节。
  • 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。若从机没有收到主机的应答,就会完全释放SDA的控制权,回到待机模式。
    接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。

 2: MPU6050简介

MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角(欧拉角),常应用于平衡车、飞行器等需要检测自身姿态的场景。以飞机为例,欧拉角就是飞机机身相对于初始3个轴的夹角——俯仰(Pitch)、滚转(Roll)、偏航(Yaw)。但是单一的加速度计、陀螺仪、磁力计都不能获得精确且稳定的欧拉角,只有综合多种传感器进行数据融合、取长补短,才能获得精确且稳定的欧拉角。常见的数据融合算法,一般有互补滤波、卡尔曼滤波等(惯性导航领域——姿态解算)。 下面给出MPU6050芯片的一些参数:
 

3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度。本质上就是弹簧测力计:F = m a F=maF=ma。测量的值是合加速度,由于系统运动时有运动加速度的影响,所以只有在静态时才表示系统的姿态。也就是说,加速度计静态稳定,而动态不稳定。
3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度。若想得到角度,只需要对角速度进行积分即可。陀螺仪的缺点是,物体静止时,会由于噪声导致角速度无法完全归零,于是就会导致积分得到的角度产生缓慢的漂移,积分时间越长漂移越明显,但当物体运动时就可以忽略漂移的大小。也就是说,陀螺仪动态稳定,而静态不稳定。
3轴磁场传感器(选修):测量X、Y、Z轴的磁场强度,某些芯片会有此功能。
气压传感器(选修):测量气压大小,一般气压值反映高度信息(海拔高度),某些芯片会有此功能。
注:于是姿态解算的大题思路就是:将“加速度计”、“陀螺仪”进行互补滤波,融合得到静态和动态都稳定的姿态角。
注:上图所示的机械结构只是便于理解,实际MPU6050芯片中不存在图示的机械结构。

16位ADC采集传感器的模拟信号,量化范围:-32768~32767(有符号数)。
加速度计满量程选择:±2、±4、±8、±16(g,1g=9.8m/s2)。
陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)。
可配置的数字低通滤波器,使输出数据更加平缓。
可配置的时钟源、可配置的采样分频。时钟源经过分频后,为内部的ADC提供转换时钟
I2C从机地址(手册可以查看到):MPU6050的设备号固定为0x68(某些批次可能为0x98),所以可以用于验证I2C读出协议是否正常。对于出厂地址为0x68的芯片来说,调整AD0引脚电压就可以改变其从机地址:1101000(AD0=0)、1101001(AD0=1)。

 

实验1:软件读写MPU6050

 

通过I2C通信协议,对MPU6050内部的寄存器进行读写。写入到配置寄存器,就可以对这个外挂模块进行配置。读出数据寄存器,就可以获取外挂模块的数据。最终将读出的数据显示在OLED屏幕上。要求显示:设备的ID号、加速度传感器的三个输出数据、陀螺仪传感器的三个输出数据(角速度)。

注:串口是异步通信,时序严格,所以一般不用软件模拟;I2C等同步通信,对时序要求不严格,可以很方便的进行软件模拟。
注:软件模拟时,SCL和SDA都是普通的GPIO口,所以可以随便接。
注:SCL和SDA都应该有一个上拉电阻,在模块的电路中已经设计了这个上拉电阻,所以可以跳线直连GPIO口。
 

 main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "MPU6050.h"

int main(void){
    MPU6050_DataStruct SensorData;//存放6轴传感器数据
        
    OLED_Init();   //OLED初始化 
    MPU6050_Init();//MPU6050初始化
    
    //显示一些基本信息
    OLED_ShowString(1,1,"ID:");
    OLED_ShowHexNum(1,4,MPU6050_GetID(),2);
//    OLED_ShowString(1,1,"Acce:  | Gyro:");
    OLED_ShowString(2,1,"       |");
    OLED_ShowString(3,1,"       |");
    OLED_ShowString(4,1,"       |");
    //不断获取传感器值并显示
    while(1){
        MPU6050_GetData(&SensorData);
        //原始数值
//        OLED_ShowSignedNum(2,1,SensorData.Acce_X,5);
//        OLED_ShowSignedNum(3,1,SensorData.Acce_Y,5);
//        OLED_ShowSignedNum(4,1,SensorData.Acce_Z,5);
//        OLED_ShowSignedNum(2,9,SensorData.Gyro_X,5);
//        OLED_ShowSignedNum(3,9,SensorData.Gyro_Y,5);
//        OLED_ShowSignedNum(4,9,SensorData.Gyro_Z,5);
        //转换成小数的数值
        OLED_ShowFloat(2,1,(float)SensorData.Acce_X/32768*20,3,1);
        OLED_ShowFloat(3,1,(float)SensorData.Acce_Y/32768*20,3,1);
        OLED_ShowFloat(4,1,(float)SensorData.Acce_Z/32768*20,3,1);
        OLED_ShowFloat(2,9,(float)SensorData.Gyro_X/32768*500,3,1);
        OLED_ShowFloat(3,9,(float)SensorData.Gyro_Y/32768*500,3,1);
        OLED_ShowFloat(4,9,(float)SensorData.Gyro_Z/32768*500,3,1);
    };
}

I2C_User.h

#ifndef __I2C_USER_H
#define __I2C_USER_H

void  I2C_User_Init(void);
void I2C_User_Start(void);
void I2C_User_Stop(void);
void I2C_User_SendByte(uint8_t send_byte);
uint8_t I2C_User_RecvByte(void);
void I2C_User_SendAck(uint8_t AckBit);
uint8_t I2C_User_RecvAck(void);

#endif

 I2C_User.c

//本文件 定义I2C的6个基本的时序单元,供其他模块调用
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

//引脚操作的封装和改名,方便移植——移植时仅需修改本部分即可
//
//定义I2C通信的两个引脚-PB10为SCL、PB11为SDA
#define I2C_User_SCL_Port GPIOB
#define I2C_User_SDA_Port GPIOB
#define I2C_User_SCL GPIO_Pin_10
#define I2C_User_SDA GPIO_Pin_11
//#define I2C_User_SCL_High GPIO_SetBits  (I2C_User_SCL_Port, I2C_User_SCL)
//#define I2C_User_SCL_Low  GPIO_ResetBits(I2C_User_SCL_Port, I2C_User_SCL)
//#define I2C_User_SDA_High GPIO_SetBits  (I2C_User_SDA_Port, I2C_User_SDA)
//#define I2C_User_SDA_Low  GPIO_ResetBits(I2C_User_SDA_Port, I2C_User_SDA)
//写SCL的操作
void I2C_User_W_SCL(uint8_t BitValue){
    GPIO_WriteBit(I2C_User_SCL_Port,I2C_User_SCL,(BitAction)BitValue);
    Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)
}

//写SDA的操作
void I2C_User_W_SDA(uint8_t BitValue){
    GPIO_WriteBit(I2C_User_SDA_Port,I2C_User_SDA,(BitAction)BitValue);
    Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)
}

//读SDA的操作
uint8_t I2C_User_R_SDA(void){
    uint8_t BitValue=0;
    BitValue = GPIO_ReadInputDataBit(I2C_User_SDA_Port,I2C_User_SDA);
    Delay_us(3);// 延迟一位(I2C最大通信速率400kHz-2.5us)
    return BitValue;
}

//初始化两个GPIO口
void  I2C_User_Init(void){
    // 开启APB2-GPIOB的外设时钟RCC
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    // 初始化PA的输出端口:定义结构体及参数
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = I2C_User_SCL | I2C_User_SDA;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//开漏输出
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    // 默认输出为高电平-释放总线
    GPIO_SetBits(GPIOB, I2C_User_SCL | I2C_User_SDA);
}
//

// 下面是I2C的六个基本时序-移植时无需修改
// 注:除了终止条件,SCL以高电平结束;其他时序都以SCL低电平结束
//
//1. 发送起始位
void I2C_User_Start(void){
    // 调整SCL、SDA输出均为高电平-保险起见,先将SDA拉高
    I2C_User_W_SDA(1);
    I2C_User_W_SCL(1);
    // SDA输出下降沿
    I2C_User_W_SDA(0);
    // 拉低SCL
    I2C_User_W_SCL(0);
}

//2. 发送终止位
void I2C_User_Stop(void){
    // 调整SCL、SDA输出均为低电平
    I2C_User_W_SCL(0);
    I2C_User_W_SDA(0);
    // 拉高SCL
    I2C_User_W_SCL(1);
    // 拉高SDA
    I2C_User_W_SDA(1);  
}
    
//3. 发送一个字节
void I2C_User_SendByte(uint8_t send_byte){
    uint8_t i=0;
    for(i=0;i<8;i++){
        //改变SDA
        I2C_User_W_SDA((0x80>>i) & send_byte);
        //拉高SCL
        I2C_User_W_SCL(1);
        //拉低SCL
        I2C_User_W_SCL(0);
    }
}

//4. 接收一个字节
uint8_t I2C_User_RecvByte(void){
    uint8_t recv_byte=0x00;
    uint8_t i=0;
    //主机释放总线
    I2C_User_W_SDA(1);
    //接收数据
    for(i=0;i<8;i++){
        // 拉高SCL
        I2C_User_W_SCL(1);
        // 读取数据
        if(I2C_User_R_SDA()==1){
            recv_byte |= (0x80>>i);
        }
        // 拉低SCL
        I2C_User_W_SCL(0);
    }
    return recv_byte;
}

//5. 发送应答位
void I2C_User_SendAck(uint8_t AckBit){
    // 将应答位放在SDA上
    I2C_User_W_SDA(AckBit);
    // 拉高SCL
    I2C_User_W_SCL(1);
    // 拉低SCL
    I2C_User_W_SCL(0);
}

//6. 接收应答位
uint8_t I2C_User_RecvAck(void){
    uint8_t AckBit=0;
    //主机释放总线
    I2C_User_W_SDA(1);
    // 拉高SCL
    I2C_User_W_SCL(1);
    // 读取应答信号
    AckBit = I2C_User_R_SDA();
    // 拉低SCL
    I2C_User_W_SCL(0);
    return AckBit;
}
//

  MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H

//定义MPU6050的6轴数据结构体
typedef struct{
    int16_t Gyro_X;
    int16_t Gyro_Y;
    int16_t Gyro_Z;
    int16_t Acce_X;
    int16_t Acce_Y;
    int16_t Acce_Z;
    int16_t Temp;
}MPU6050_DataStruct;

//外部可调用函数
void MPU6050_WriteReg(uint8_t RegAddr, uint8_t wData);
uint8_t MPU6050_ReadReg(uint8_t RegAddr);
void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(MPU6050_DataStruct* MPU6050_Data);

#endif

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "I2C_User.h"
#include "MPU6050.h"

//定义从机地址、寄存器地址
#define MPU6050_ADDRESS           0xD0
#define MPU6050_REG_SMPLRT_DIV    0x19
#define MPU6050_REG_CONFIG        0x1A
#define MPU6050_REG_GYRO_CONFIG   0x1B
#define MPU6050_REG_ACCEL_CONFIG  0x1C

#define MPU6050_REG_ACCEL_XOUT_H  0x3B
#define MPU6050_REG_ACCEL_XOUT_L  0x3C
#define MPU6050_REG_ACCEL_YOUT_H  0x3D
#define MPU6050_REG_ACCEL_YOUT_L  0x3E
#define MPU6050_REG_ACCEL_ZOUT_H  0x3F
#define MPU6050_REG_ACCEL_ZOUT_L  0x40
#define MPU6050_REG_TEMP_OUT_H    0x41
#define MPU6050_REG_TEMP_OUT_L    0x42
#define MPU6050_REG_GYRO_XOUT_H   0x43
#define MPU6050_REG_GYRO_XOUT_L   0x44
#define MPU6050_REG_GYRO_YOUT_H   0x45
#define MPU6050_REG_GYRO_YOUT_L   0x46
#define MPU6050_REG_GYRO_ZOUT_H   0x47
#define MPU6050_REG_GYRO_ZOUT_L   0x48

#define MPU6050_REG_PWR_MGMT_1    0x6B
#define MPU6050_REG_PWR_MGMT_2    0x6C
#define MPU6050_REG_WHO_AM_I      0x75

//MPU6050指定地址写寄存器
void MPU6050_WriteReg(uint8_t RegAddr, uint8_t wData){
    I2C_User_Start();
    I2C_User_SendByte(MPU6050_ADDRESS);
    I2C_User_RecvAck();//暂时不对应答位进行处理
    I2C_User_SendByte(RegAddr);
    I2C_User_RecvAck();
    I2C_User_SendByte(wData);
    I2C_User_RecvAck();
    I2C_User_Stop();
}

//MPU6050指定地址读寄存器
uint8_t MPU6050_ReadReg(uint8_t RegAddr){
    uint8_t rData=0x00;
    
    I2C_User_Start();
    I2C_User_SendByte(MPU6050_ADDRESS);
    I2C_User_RecvAck();//暂时不对应答位进行处理
    I2C_User_SendByte(RegAddr);
    I2C_User_RecvAck();
    
    I2C_User_Start();
    I2C_User_SendByte(MPU6050_ADDRESS | 0x01);
    I2C_User_RecvAck();//暂时不对应答位进行处理
    rData = I2C_User_RecvByte();
    I2C_User_SendAck(1);
    I2C_User_Stop();
    
    return rData;
}

//MPU6050初始化
void MPU6050_Init(void){
    //I2C初始化
    I2C_User_Init();
    //不复位、解除睡眠、不开启循环模式、温度传感器失能、选择陀螺仪x轴时钟
    MPU6050_WriteReg(MPU6050_REG_PWR_MGMT_1,0x01);
    //没有开启循环模式
    MPU6050_WriteReg(MPU6050_REG_PWR_MGMT_2,0x00);
    //采样率10分频
    MPU6050_WriteReg(MPU6050_REG_SMPLRT_DIV,0x09);
    //不使用外部同步、DLPF设置等级6
    MPU6050_WriteReg(MPU6050_REG_CONFIG,0x06);
    //陀螺仪:自测失能、满量程±500°/s-000_01_000
    MPU6050_WriteReg(MPU6050_REG_GYRO_CONFIG,0x08);
    //加速度计:自测失能、满量程±2g、失能运动检测-000_00_000
    MPU6050_WriteReg(MPU6050_REG_ACCEL_CONFIG,0x00);
}

//获取MPU6050的ID号
uint8_t MPU6050_GetID(void){
    return MPU6050_ReadReg(MPU6050_REG_WHO_AM_I);
}

//获取MPU6050的传感器数据
void MPU6050_GetData(MPU6050_DataStruct* MPU6050_Data){
    uint16_t sensor_byte_L, sensor_byte_H;
    //获取加速度计数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_XOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_XOUT_L);
    MPU6050_Data->Acce_X = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_YOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_YOUT_L);
    MPU6050_Data->Acce_Y = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_ACCEL_ZOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_ACCEL_ZOUT_L);
    MPU6050_Data->Acce_Z = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    //获取陀螺仪数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_XOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_XOUT_L);
    MPU6050_Data->Gyro_X = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_YOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_YOUT_L);
    MPU6050_Data->Gyro_Y = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_GYRO_ZOUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_GYRO_ZOUT_L);
    MPU6050_Data->Gyro_Z = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
    //获取温度传感器数据
    sensor_byte_H = MPU6050_ReadReg(MPU6050_REG_TEMP_OUT_H);
    sensor_byte_L = MPU6050_ReadReg(MPU6050_REG_TEMP_OUT_L);
    MPU6050_Data->Temp = (int16_t)((sensor_byte_H<<8) | sensor_byte_L);
}

IIC外设简介

总的来说,由于串口是异步时序,对时序要求极其严格,所以几乎不会软件模拟串口,基本上一边倒的采用硬件实现串口通信。而对于I2C通信来说,尽管用硬件电路可以减轻CPU负担,但是硬件I2C总线数量有限、引脚固定等,也有很多限制;而由于I2C是同步时序,软件模拟I2C足够简单且灵活,所以还是有许多场合都使用软件I2C进行通信。若只是简单应用I2C读写数据,使用软件模拟更加灵活;若对时序要求高,可以使用硬件I2C。本节介绍硬件实现I2C的原理。

STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担,还兼具可以实现完整的多主机通信、时序波形规整、通信速率快等优点,功能非常强大。具体参数如下:

  • 支持多主机模型(固定多主机/可变多主机)。stm32是按照“可变多主机”的模式设置硬件电路的,也就是说,需要自己声明自己是主机。
  • 支持7位/10位地址模式。10位地址发送两字节寻址,高5位固定为11110(这种开头不会在7位地址中出现),低10位作为地址(第一个字节的最低位还是R/W)。
  • 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)。只要不超过最大频率,多少都可以。
  • 支持DMA。多字节传输时可提高传输效率,如指定地址 读/写 多字节。若只有几个字节也没必要配置DMA。
  • 兼容SMBus(System Management Bus)协议。SMBus是基于I2C总线改进而来的,主要用于电源管理系统中。本节用不到。
  • STM32F103C8T6 硬件I2C资源:I2C1、I2C2。
     

 

SDA部分:

  • 发送数据过程:首先将数据写入“数据寄存器DR”,当没有移位时,DR中的数据就被转运到“数据移位寄存器”中,并同时置状态寄存器的TXE位为1(发送寄存器空)。此时就可以继续向DR中写入数据。
  • 接收数据流程:将数据一位一位的写入“数据移位寄存器”,当一个字节的数据收齐后,将会将数据整体从“数据移位寄存器”转运到“数据寄存器DR”,并同时置标志位RXNE(接收寄存器非空)。此时就可以将数据从DR中读出。
  • 比较器和地址寄存器(用不到):从机模式使用。由于stm32的I2C是基于可变多主机模型设计的,不进行通信时默认为“从机”,于是“自身地址寄存器”就用于存放从机地址,可以由用户指定。“双地址寄存器”存储的也是从机地址,于是stm32就可以同时响应两个从机地址。
  • 数据校验模块(用不到):当发送一个多字节的数据帧时,“帧错误校验计算”可以硬件自动执行CRC校验计算,得到一个字节的校验位附加在数据帧的最后。同样的,当接收的校验字节和CRC计算结果不匹配时,就会置校验错误标志位。
  • 评价:整个数据收发流程类似于“串口通信”,只不过串口是全双工通信,收发电路独立;I2C是半双工通信,收发共用一个电路。
  1. SCL部分:
  • 时钟控制:控制SCL线,具体细节无需了解。
  • 时钟控制寄存器CCR:控制“时钟控制”电路执行相应的功能。
  • 控制逻辑电路:写入“控制寄存器”,可以对整个电路进行控制;读取“状态寄存器”,可以得知整个电路的工作状态。
  • 中断:内部某些事情比较紧急时,可以申请中断。
  • DMA请求与响应:在进行很多字节的收发时,可以配合DMA来提高效率。

 

初始化I2C外设时注意四部分:

  • RCC时钟:现实需要初始化GPIO、I2C两个外设的时钟。
  • GPIO外设:都要配置成复用开漏输出模式。“复用”是交给片上外设来控制,“开漏输出”是I2C协议要求的端口配置。
  • I2C外设:老套路了,先定义结构体,再调用一个I2C_Init即可。
  • 开关控制:使能I2C外设。
  • 注:上图简化成“一主多从模型”,所以SCL只有时钟输出。“多主机模型”时,SCL也会有输入。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/267877.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

物联网协议之COAP简介及Java实践

目录 前言 一、COAP简介 1、关于COAP 2、COAP特点 3、基于COAP的NB-IoT接入流程 二、CoAP协议JAVA实践 1、californium介绍 2、Java集成 3、Maven 资源引入 4、定义Server端 5、Client调用 6、运行测试 总结 前言 今天平安夜&#xff0c;祝大家圣诞快乐&#xff0c…

41 sysfs 文件系统

前言 在 linux 中常见的文件系统 有很多, 如下 基于磁盘的文件系统, ext2, ext3, ext4, xfs, btrfs, jfs, ntfs 内存文件系统, procfs, sysfs, tmpfs, squashfs, debugfs 闪存文件系统, ubifs, jffs2, yaffs 文件系统这一套体系在 linux 有一层 vfs 抽象, 用户程序不用…

前端---html 的介绍

1. 网页效果图 --CSDN 2. html的定义 HTML 的全称为&#xff1a;HyperText Mark-up Language, 指的是超文本标记语言。 标记&#xff1a;就是标签, <标签名称> </标签名称>, 比如: <html></html>、<h1></h1> 等&#xff0c;标签大多数都是…

一篇文章拿捏继承多态练习题

继承多态习题 选择题解释 简答题 选择题 下面哪种面向对象的方法可以让你变得富有( ) A: 继承 B: 封装 C: 多态 D: 抽象 ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关&#xff0c; 而对方法的调用则可以关联于具体的对象。 A: 继承 B: 模…

Linux系统编程(六):进程(下)

参考引用 UNIX 环境高级编程 (第3版)嵌入式Linux C应用编程-正点原子 1. 进程与程序 1.1 main() 函数由谁调用&#xff1f; C 语言程序总是从 main 函数开始执行int main(void) int main(int argc, char *argv[]) // 如果需要向应用程序传参&#xff0c;则选择该种写法操作系…

TikTok真题第3天 | 856.括号的分数、2115. 从给定原材料中找到所有可以做出的菜、394.字符串解码

856.括号的分数 题目链接&#xff1a;856.score-of-parentheses 解法&#xff1a; leetcode官方的题解基本是每个字都认得&#xff0c;连起来就看不懂。 使用栈来解决&#xff0c;后进先出&#xff0c;后面加入的左括号&#xff0c;先弹出和右括号去匹配。定义一个记录分数…

部署LNMP动态网站

部署LNMP动态网站 安装LNMP平台相关软件1. 安装软件包2. 启动服务&#xff08;nginx、mariadb、php-fpm&#xff09;3. 修改Nginx配置文件&#xff0c;实现动静分离4. 配置数据库 上线wordpress代码 &#xff08;测试搭建的LNMP环境是否可以使用&#xff09;1. 上线php动态网站…

什么等等? I/O Wait ≠ I/O 瓶颈?

本文地址&#xff1a;什么等等&#xff1f; I/O Wait ≠ I/O 瓶颈&#xff1f; | 深入浅出 eBPF 1. I/O Wait 定义2. 测试验证3. 进一步明确磁盘吞吐和读写频繁进程4. 内核 CPU 统计实现分析5. 总结参考资料 1. I/O Wait 定义 I/O Wait 是针对单个 CPU 的性能指标&#xff0…

Bresenham 算法

1965 年&#xff0c;Bresenham 为数字绘图仪开发了一种绘制直线的算法&#xff0c;该算法同样使用于光栅扫描显示器&#xff0c;被称为 Bresenham 算法。 原理 算法的目标是选择表示直线的最佳光栅位置。Bresenhan 算法在主位移方向上每次递增一个单位。另一个方向的增量为 0…

【java爬虫】基于springboot+jdbcTemplate+sqlite+OkHttp获取个股的详细数据

注&#xff1a;本文所用技术栈为&#xff1a;springbootjdbcTemplatesqliteOkHttp 前面的文章我们获取过沪深300指数的成分股所属行业以及权重数据&#xff0c;本文我们来获取个股的详细数据。 我们的数据源是某狐财经&#xff0c;接口的详细信息在下面的文章中&#xff0c;本…

抖店对接厂家时,厂家不愿提供ERP打单如何解决?相关解答如下

我是王路飞。 现在的抖店已经不能拍单了&#xff0c;只能让厂家使用抖音电子面单发货。 关于这件事&#xff0c;我之前也说过&#xff0c;无货源商家太聪明了&#xff0c;所以平台一定会解决拍单问题的&#xff0c;无非是个时间问题罢了。 而且我认为这对我们商家来说也是个…

关于巴西网络犯罪分子使用LOLBaS和CMD脚本窃取银行账户的动态情报

一、基本内容 最近&#xff0c;一名未知身份的网络犯罪威胁行为者以使用西班牙语和葡萄牙语的用户为目标&#xff0c;破坏墨西哥、秘鲁和葡萄牙等地的网上银行账户。该攻击链主要利用社会工程学技术&#xff0c;利用葡萄牙和西班牙用户的电子邮件&#xff0c;发送带有欺骗性的…

如何使用固定二级子域名公网访问多个本地Windows Web网站

文章目录 1. 下载windows版Nginx2. 配置Nginx3. 测试局域网访问4. cpolar内网穿透5. 测试公网访问6. 配置固定二级子域名7. 测试访问公网固定二级子域名 1. 下载windows版Nginx 进入官方网站(http://nginx.org/en/download.html)下载windows版的nginx 下载好后解压进入nginx目…

图像识别SLIC、Haralick texture features(自备)

SLIC 简单线性迭代聚类(SLIC ),它采用k-means聚类方法来有效地生成超像素。 SLIC超像素分割详解&#xff08;一&#xff09;&#xff08;二&#xff09;&#xff08;三&#xff09;_超像素分割 样本-CSDN博客 超像素分割 & SLIC算法 & 使用示例_slic分割算法matlab-C…

快速剪辑视频软件,视频图像翻转软件

在这个信息爆炸的时代&#xff0c;视频已经成为了人们获取信息、娱乐、学习的主要方式之一。一个好的视频&#xff0c;不仅可以吸引观众的眼球&#xff0c;更可以传达出深层次的意义。那该什么快速的编辑视频&#xff0c;有没有好用的工具推荐呢&#xff1f;今天小编就给大家介…

MySQL数据库——约束

1. 约束 1.1. 概述 概述 约束是MySQL中用于限制表中数据规则的术语。这些规则可以确保数据类型、长度、精度等符合要求&#xff0c;并保持数据的正确性、有效性和完整性。约束可以应用于表中的字段&#xff0c;并帮助保护数据库中的数据免受无效或错误数据的干扰。 分类 约束…

行为型模式

目录 行为型模式1 模板方法模式1.1 概述1.2 结构1.3 案例实现1.3 优缺点1.4 适用场景1.5 JDK源码解析 2 策略模式2.1 概述2.2 结构2.3 案例实现2.4 优缺点2.5 使用场景2.6 JDK源码解析 3 命令模式3.1 概述3.2 结构3.3 案例实现3.4 优缺点3.5 使用场景3.6 JDK源码解析 4 责任链模…

多线程的基本使用与多线程中条件变量的使用——消费者生产者问题实例

多线程的基本使用与多线程中条件变量的使用——消费者生产者问题实例 本文主要涉及多线程的使用方法&#xff0c;通过两个实例来对多线程的使用进行理解&#xff0c; 案例包括&#xff1a; 1.一个线程负责计数&#xff0c;另一个线程负责打印计数值 2.消费者生产者问题 文章目录…

Git常用命令及解释说明

目录 前言1 git config2 git init3 git status4 git add5 git commit6 git reflog7 git log8 git reset结语 前言 Git是一种分布式版本控制系统&#xff0c;广泛用于协作开发和管理项目代码。了解并熟练使用Git的常用命令对于有效地管理项目版本和历史记录至关重要。下面是一些…

[THUPC 2024 初赛] 二进制 (树状数组单点删除+单点查询)(双堆模拟set)

题解 题目本身不难想 首先注意到所有查询的序列长度都是小于logn级别的 我们可以枚举序列长度len&#xff0c;然后用类似滑动窗口的方法&#xff0c;一次性预处理出每种字串的所有出现位置&#xff0c;也就是开N个set去维护所有的位置。预处理会进行O(logn)轮&#xff0c;每…