
引言我们在前面学习完了USART我们知道串口是一对一进行通信的。接下来我们学习的I2C是支持多对多和一对多的但是我们学习过程中只考虑一对多。特点两线制一根SCL线用于传输时钟信号一根SDA用于传输数据信号。多主机/多从机支持多对多和一对多但是我们考虑的和绝大多数情况都是一对多。设备地址一般为七位加一位读写位。同步串行通信所有数据都靠SCL时钟来控制。半双工同一时刻支持一个方向的传输。速度模式标准模式100kbps快速模式400kbps和快速模式1MbpsSTM32F103ZET6和I2C首先I2C是MCU上的片上外设有两个I2C接口其接线位置如下和USART相同在使用I2C的时候需要复用GPIO接口这里是对输出进行了复用且使用开漏模式。但是在前面我们说一个GPIO的引脚要么是输入要么输出为什么这里输入和输出用的是一个引脚呢这里对输入并不是设置只是当外部电压变化内部可以感知到并没有去配置输入模式之类的。I2C基本通信原理这个图中我们除了SCL和SDA外还看到了接VDD的两个上拉电阻这个本质其实和I2C无关主要是由于设置开漏模式导致的。这里在输出的时候就是和前面讲GPIO开漏输出是一样的只是采用复用的方式。主要是对于输入大家可能有些疑问。这里输出后其实在接收方可以认为是导线直接连接类似浮空输入对输入进行整形后复用。这里主要是可以感知到这个变化如果需要就可以直接使用。时钟与数据那么时钟与数据是怎么配合协同工作的呢这里规定只有在时钟处于低电平期间才可以作数据的跳变。在高电平期间会对数据进行读取。为了达到这个要求也就要求数据的稳定时间要高于时钟高电平稳定时间低电平。线与Wried-AND我们刚才只在单个通信的角度看待I2C现在我们在全局来看I2C当有任何一个设备拉低SCL/SDA的时候整条线都会接地。这个和逻辑与或者按位与是有点相似之处的。线与的好处时钟同步比如主设发送数据过快从设备来不及处理就可以拉低SCL来告诉主设备。这时主设备拉高电平也没有作用知直到从设备释放SCL才会继续发送。总线仲裁当有多台主设备竞争总线如果自己发送1由于其他设备发送0会拉低总线那么它就知道自己仲裁失败了。为什么选择开漏首先为什么推挽模式不可以其一是因为当主设备输出1然后从设备拉低电压会造成短路还有就是无法实现线与逻辑。而这两个问题正是开漏模式可以解决的。I2C协议①为空闲状态空闲状态默认为高电平。②为开始状态我们前面说只有在SCL为低电平SDA才可以跳变来改变数据这里在SCL为高电平时SDA进行了跳变表示开始。③为结束状态同样在SCL为高电平的时候SDA进行了从低向高的跳变恢复空闲状态。Master发送从设备地址在I2C中一般由主设备产生时钟信号也由主设备选定从设备。在最开始会发送从设备的地址发送后如果从设备接收会返回一个ACK而没有匹配上的从设备不做任何回应。而这个地址一般为七位地址加一位读写位构成。Master写数据开始位-发送地址和写-接收方应答-发送数据-接收方应答-...-停止Master读数据开始位-发送地址和读-接收方应答-接收方发送数据-主机应答-...-收方发送数据-主机不应答-停止这里最后一个为什么是NACK因为在写的时候主机是知道自己要写多少写完就结束但是在读的时候从机并不知道你要读多少所以需要主机去进行控制。它在最后一个字节到达后不进行确认从机就不会在发送数据了。I2C功能结构图将其简化整合到全局首先比较器是用来进行地址匹配的。然后控制逻辑电路输出时钟控制信息数据控制包含移位寄存器和数据寄存器数据通过APB1来从DR寄存器读取数据。不过这里的发送和USART不同是MSB其余基本保持不变。这里和前面还是一样控制I2C其实就是控制它下面的寄存器。DR寄存器用于存储数据在发送的过程中如果TxE为表示不能写入1表示可以写入。在接收过程中RxNE为0表示不能读取数据为1表示可以读取数据。实战与源码我们这里使用AT24C02来作从设备我们要实现的是由USART来控制上位机当写入的时候把配置信息存到AT24C02当中。AT24C02在读取当作一个一维数组来读取即可但是在写入的时候不能跨页写入会造成回卷。我们整体的架构分为硬件HAL库板级驱动控制逻辑调用逻辑。我们创建两个组一个APP用于控制逻辑BSP_Drivers为板级驱动HAL库和调用逻辑main生成中是带好的。整体逻辑为HAL库控制寄存器来控制硬件板级驱动调用HAL库实现各自功能控制逻辑通过调用板级驱动相关函数实现具体项目功能最后main函数调用控制逻辑。源码解析初始化这里和USART一样也是使用了句柄。void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c) { GPIO_InitTypeDef GPIO_InitStruct {0}; if(hi2c-InstanceI2C2) { /* USER CODE BEGIN I2C2_MspInit 0 */ /* USER CODE END I2C2_MspInit 0 */ __HAL_RCC_GPIOB_CLK_ENABLE(); /**I2C2 GPIO Configuration PB10 ------ I2C2_SCL PB11 ------ I2C2_SDA */ GPIO_InitStruct.Pin GPIO_PIN_10|GPIO_PIN_11; GPIO_InitStruct.Mode GPIO_MODE_AF_OD; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); /* Peripheral clock enable */ __HAL_RCC_I2C2_CLK_ENABLE(); /* USER CODE BEGIN I2C2_MspInit 1 */ /* USER CODE END I2C2_MspInit 1 */ } }首先开启GPIO时钟初始化GPIO随后打开I2C2的时钟。随后先关闭I2C的使能进行配置这里不是时钟配置这里是设备自己的配置。随后配置寄存器信息最后打开设备使能。HAL_I2C_IsDeviceReadytmp1 __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR); tmp2 __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF); while ((hi2c-State ! HAL_I2C_STATE_TIMEOUT) (tmp1 RESET) (tmp2 RESET)) { if (((HAL_GetTick() - tickstart) Timeout) || (Timeout 0U)) { hi2c-State HAL_I2C_STATE_TIMEOUT; } tmp1 __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR); tmp2 __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF); }这里主要来判断有没有收到正确的应答。这两位互斥不可能全为1或全为0ADDR为1表示成功ADDR为0表示失败AF则相反。随后循环尝试设定次数。if (hi2c-State HAL_I2C_STATE_READY) { /* Wait until BUSY flag is reset */ if (I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) ! HAL_OK) { return HAL_BUSY; }这里是检查设备状态是否就绪。HAL_I2C_Mem_Write写操作其实和检查状态类似只不过后面要在加要写入的数据。while (hi2c-XferSize 0U) { /* Wait until TXE flag is set */ if (I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, tickstart) ! HAL_OK) { if (hi2c-ErrorCode HAL_I2C_ERROR_AF) { /* Generate Stop */ SET_BIT(hi2c-Instance-CR1, I2C_CR1_STOP); } return HAL_ERROR; } /* Write data to DR */ hi2c-Instance-DR *hi2c-pBuffPtr; /* Increment Buffer pointer */ hi2c-pBuffPtr; /* Update counter */ hi2c-XferSize--; hi2c-XferCount--; if ((__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) SET) (hi2c-XferSize ! 0U)) { /* Write data to DR */ hi2c-Instance-DR *hi2c-pBuffPtr; /* Increment Buffer pointer */ hi2c-pBuffPtr; /* Update counter */ hi2c-XferSize--; hi2c-XferCount--; } }最下面连写入两次相当于双缓冲区。主要靠TxE判断数据寄存器是否为空。HAL_I2C_Mem_Read读这里比较麻烦做了很多的分类包括为回复NACK专门设置标志位。本质都是为了实现最后一个回复是NACK。