前言:
IIC作为一个基础的通信协议,活跃于各种设备之间。I2C作为两线通信协议,相较于spi来说所需引脚更少,我们可以使用硬件I2C在设备与设备之间通信,但在硬件I2c被其他功能所占据引脚时,也可以使用软件拉高拉低来模拟,32位单片机中我们常用的方式是软件模拟,但实际上软件模拟的I2C速度有时又达不到我们需求,软件的有点在于可以灵活使用各个引脚,硬件的优点在于能够更快的相应。硬件I2C网传存在一些小bug,但作者常常使用硬件I2C也并没有遇到什么bug,只能说具体问题具体看待了,当正遇到了的情况下,再去该软件模拟也可以的。毕竟硬件相对简单,便利。参考文章一般思路先介绍一下IIC后附加代码分析硬件I2C
一、IIC介绍
(一)、简介
(二)、IIC特征

(三)、补充关于IIC的上拉电阻
1、 I2C总线采用 开漏输出(Open-Drain) 或 开集电极输出(Open-Collector) 的设计:
-
SDA(数据线)和SCL(时钟线)在空闲时处于高电平状态。
-
当设备需要发送数据时,会拉低SDA或SCL。
-
由于开漏输出无法主动拉高信号,因此需要通过外部上拉电阻将总线拉高到逻辑高电平。
如果没有上拉电阻,总线将无法正常工作,因为信号无法回到高电平状态
2、上拉电阻的选择
-
总线电容(Bus Capacitance):
-
总线上的电容包括导线电容、设备引脚电容等。
-
电容越大,信号的上升时间越长,通信速度越慢。
-
-
通信速度(Clock Speed):
-
标准模式(100 kHz)和快速模式(400 kHz)对上升时间的要求不同。
-
高速模式(3.4 MHz)需要更小的上拉电阻。
-
-
电源电压(VDD):
-
电源电压越高,上拉电阻可以适当增大。
-
常用阻值范围:
-
标准模式(100 kHz):通常使用 4.7 kΩ 到 10 kΩ 的上拉电阻。
-
快速模式(400 kHz):通常使用 2.2 kΩ 到 4.7 kΩ 的上拉电阻。
-
高速模式(3.4 MHz):通常使用 1 kΩ 到 2.2 kΩ 的上拉电阻。
计算公式:
上拉电阻的阻值可以通过以下公式估算:
Rmax=trise0.8473×CbusRmax=0.8473×Cbustrise其中:
-
trisetrise 是信号的上升时间(由I2C规范决定)。
-
CbusCbus 是总线的电容值
3、上拉电阻布局
-
每个总线都需要上拉电阻:
-
SDA和SCL线都需要单独的上拉电阻。
-
-
避免多个上拉电阻:
-
如果总线上有多个设备,不要为每个设备单独加上拉电阻,否则会导致总阻值过小,影响通信。
-
-
靠近主设备放置:
-
上拉电阻应尽量靠近主设备(Master)放置,以减少信号反射和干扰。
-
4、电源匹配问题
-
上拉电阻的电源电压(VDD)需要与I2C设备的逻辑电平匹配。
-
例如,如果I2C设备的工作电压是3.3V,上拉电阻也应连接到3.3V电源。
5、常见问题
问题1:上拉电阻过小
-
现象:总线电流过大,可能导致设备损坏或电源不稳定。
-
解决方法:增大上拉电阻。
问题2:上拉电阻过大
-
现象:信号上升时间过长,导致通信错误或速度下降。
-
解决方法:减小上拉电阻。
问题3:没有上拉电阻
-
现象:总线无法拉高,通信完全失败。
-
解决方法:添加合适的上拉电阻。
注:补充部分来源与deepseek介绍。
二、硬件IIC连接
1、引脚选择
本次实验所使用的是芯片是stm32f411ceu6的PB9,PB10 I2C2。手册显示
2、原理图
由于板子并未有上拉电阻,所以自己手动焊接了两个2.2K的上拉电阻
三、软件编程
(一)、I 2C 主机:
1、I2C主模式初始化:
I2C_HandleTypeDef gI2c2;//I2C句柄
/*****************************************************************************函 数 名 : AudioI2cInit功能描述 : IIC初始化,速度400000外部添加2.2K的上拉电阻外部实现上拉输入参数 : void返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void MasterI2cInit(void)
{gI2c2.Instance=I2C2;gI2c2.Init.ClockSpeed=400000; /*快速模式400,标准模式100KHZ*/gI2c2.Init.DutyCycle = I2C_DUTYCYCLE_16_9; /*占空比:1/2 */ gI2c2.Init.OwnAddress1=0; /*主机不需要配置地址*/gI2c2.Init.AddressingMode=I2C_ADDRESSINGMODE_7BIT; /*7位地址模式*/gI2c2.Init.DualAddressMode=I2C_DUALADDRESS_DISABLE;/*禁用双地址模式*/gI2c2.Init.OwnAddress2=0; /*地址2未使用*/gI2c2.Init.GeneralCallMode=I2C_GENERALCALL_DISABLE;/*禁用广播呼叫*/gI2c2.Init.NoStretchMode=I2C_NOSTRETCH_DISABLE; /*始终拉伸*/gI2c2.Mode=HAL_I2C_MODE_MASTER; /*配置为主模式*/if(HAL_I2C_Init(&gI2c2)!=HAL_OK){};
}
/*****************************************************************************函 数 名 : HAL_I2C_MspInit功能描述 : IIC引脚初始胡初始化输入参数 : void返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{GPIO_InitTypeDef GPIO_InitStruct;if(hi2c->Instance==I2C1){}if(hi2c->Instance==I2C2){__HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_I2C2_CLK_ENABLE();GPIO_InitStruct.Pin = IIC_SCL_GPIO_PIN;GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;//GPIO_MODE_AF_OD;GPIO_InitStruct.Pull = GPIO_PULLUP;//GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;GPIO_InitStruct.Alternate = GPIO_AF4_I2C2;HAL_GPIO_Init(IIC_SCL_GPIO_PORT,&GPIO_InitStruct);//I2C_SDA configGPIO_InitStruct.Pin = IIC_SDA_GPIO_PIN;GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;//GPIO_MODE_AF_OD;GPIO_InitStruct.Pull = GPIO_PULLUP;//GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;GPIO_InitStruct.Alternate = GPIO_AF9_I2C2;HAL_GPIO_Init(IIC_SDA_GPIO_PORT,&GPIO_InitStruct);}
}
2、I2C读写数据
/*****************************************************************************函 数 名 : I2C_Write功能描述 : 向从机写入数据输入参数 : void返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void I2C_Write(uint8_t reg_address, uint8_t data)
{uint8_t buffer[2];buffer[0] = reg_address; // 寄存器地址buffer[1] = data; // 要写入的数据// 发送数据到从机HAL_I2C_Master_Transmit(&gI2c2, I2C_DEVICE_ADDR << 1, buffer, 2, 1000);
}
/*****************************************************************************函 数 名 : I2C_Read功能描述 : 从从机读取数据输入参数 : void返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
uint8_t I2C_Read(uint8_t reg_address)
{uint8_t data = 0; // 先发送寄存器地址HAL_I2C_Master_Transmit(&gI2c2, I2C_DEVICE_ADDR << 1, ®_address, 1, 1000);// 然后读取数据HAL_I2C_Master_Receive(&gI2c2,(I2C_DEVICE_ADDR << 1)|0x01, &data, 1, 1000);return data;
}
3、I2C.h文件
#ifndef __I2C_H_
#define __I2C_H_
#include "sys/sys.h"#define I2C_DEVICE_ADDR 0x50
#define I2C_WRITE_ADDR (I2C_DEVICE_ADDR<<1)
#define I2C_READ_ADDR (I2C_WRITE_ADDR|0x01)
#define IIC_SCL_GPIO_PORT GPIOB
#define IIC_SCL_GPIO_PIN GPIO_PIN_10
#define IIC_SCL_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */#define IIC_SDA_GPIO_PORT GPIOB
#define IIC_SDA_GPIO_PIN GPIO_PIN_9
#define IIC_SDA_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 *//******************************************************************************************//* IO操作 */
#define IIC_SCL(x) do{ x ? \HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \}while(0) /* SCL */#define IIC_SDA(x) do{ x ? \HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \}while(0) /* SDA */#define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN) /* 读取SDA */extern void MasterI2cInit(void);
extern void I2C_Write(uint8_t reg_address, uint8_t data);
extern uint8_t I2C_Read(uint8_t reg_address);
#endif
(二)、从模式代码
1、从模式初始化
#include "config.h"
I2C_HandleTypeDef gI2c2;//I2C句柄
uint8_t ram[256]; // 模拟I2C从机数据寄存器(主机读写的数据都放在这块内存)
uint8_t offset; // 从机寄存器当前偏移地址
static uint8_t first_byte_state = 1; // 是否收到第1个字节,也就是偏移地址(0:已收到,1:没有收到)
/*****************************************************************************函 数 名 : I2c2SlaveInit功能描述 : IIC初始化,速度400000外部添加2.2K的上拉电阻外部实现上拉输入参数 : void返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void SlaveI2cInit(void)
{gI2c2.Instance=I2C2;gI2c2.Init.ClockSpeed=400000; /*快速模式400,标准模式100KHZ*/gI2c2.Init.DutyCycle = I2C_DUTYCYCLE_16_9; /*占空比:1/2 */ gI2c2.Init.OwnAddress1=I2C_DEVICE_ADDR; /*主机不需要配置地址*/gI2c2.Init.AddressingMode=I2C_ADDRESSINGMODE_7BIT; /*7位地址模式*/gI2c2.Init.DualAddressMode=I2C_DUALADDRESS_DISABLE; /*禁用双地址模式*/gI2c2.Init.OwnAddress2=0; /*从机地址2未使用*/gI2c2.Init.GeneralCallMode=I2C_GENERALCALL_DISABLE; /*禁用广播呼叫*/gI2c2.Init.NoStretchMode=I2C_NOSTRETCH_DISABLE; /*时钟拉伸*/gI2c2.Mode=HAL_I2C_MODE_SLAVE; /*配置为从模式*/HAL_I2C_Init(&gI2c2);HAL_I2C_EnableListen_IT(&gI2c2); // 使能I2C1的侦听中断
}
/*****************************************************************************函 数 名 : HAL_I2C_MspInit功能描述 : IIC初始化,会自动调用输入参数 : void返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{GPIO_InitTypeDef GPIO_InitStruct;if(hi2c->Instance==I2C1){//I2c1}else if(hi2c->Instance == I2C2){__HAL_RCC_GPIOB_CLK_ENABLE();__HAL_RCC_I2C2_CLK_ENABLE();GPIO_InitStruct.Pin = IIC_SCL_GPIO_PIN;GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;//GPIO_MODE_AF_OD;GPIO_InitStruct.Pull = GPIO_NOPULL;//GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;GPIO_InitStruct.Alternate = GPIO_AF4_I2C2;HAL_GPIO_Init(IIC_SCL_GPIO_PORT,&GPIO_InitStruct);//I2C_SDA configGPIO_InitStruct.Pin = IIC_SDA_GPIO_PIN;GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;//GPIO_MODE_AF_OD;GPIO_InitStruct.Pull = GPIO_NOPULL;//GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;GPIO_InitStruct.Alternate = GPIO_AF9_I2C2;HAL_GPIO_Init(IIC_SDA_GPIO_PORT,&GPIO_InitStruct);HAL_NVIC_SetPriority(I2C2_EV_IRQn,1,1); // 事件中断(必须有)HAL_NVIC_EnableIRQ(I2C2_EV_IRQn);}}
/*****************************************************************************函 数 名 : I2C2_EV_IRQHandler功能描述 :I2C 事件中断服务函数输入参数 : void返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void I2C2_EV_IRQHandler(void)
{HAL_I2C_EV_IRQHandler(&gI2c2);
}
/*****************************************************************************函 数 名 : HAL_I2C_ListenCpltCallback功能描述 :从函数回调函数,用于监听主发送的数据输入参数 : i2c句柄返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c)
{if(hi2c->Instance == I2C2){first_byte_state = 1;offset = 0; //偏移位HAL_I2C_EnableListen_IT(hi2c); // slave is ready again// 完成一次通信,清除状态}
}
/*****************************************************************************函 数 名 : HAL_I2C_AddrCallback功能描述 :从设备地址回调函数,地址匹配上以后会进入该函数输入参数 : void返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode)
{if(hi2c->Instance == I2C2){if(TransferDirection == I2C_DIRECTION_TRANSMIT) { // 主机发送,从机接收if(first_byte_state) {// 准备接收第1个字节数据HAL_I2C_Slave_Seq_Receive_IT(hi2c, &offset, 1, I2C_NEXT_FRAME); // 每次第1个数据均为偏移地址} } else { // 主机接收,从机发送HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &ram[offset], 1, I2C_NEXT_FRAME); // 打开中断并把ram[]里面对应的数据发送给主机}}
}
/*****************************************************************************函 数 名 : HAL_I2C_AddrCallback功能描述 :I2C数据接收回调函数(在I2C完成一次接收时会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)输入参数 : hi2c 句柄返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c)
{if(hi2c->Instance == I2C2){if(first_byte_state) {first_byte_state = 0;// 收到的第1个字节数据(偏移地址)} else { // 收到的第N个字节数据offset++; // 每收到一个数据,偏移+1}// 打开I2C中断接收,下一个收到的数据将存放到ram[offset]HAL_I2C_Slave_Seq_Receive_IT(hi2c, &ram[offset], sizeof(ram), I2C_NEXT_FRAME); // 接收数据存到ram[]里面对应的位置}}
/*****************************************************************************函 数 名 : HAL_I2C_AddrCallback功能描述 :I2C数据发送回调函数(在I2C完成一次发送后会关闭中断并调用该函数,因此在处理完成后需要手动重新打开中断)输入参数 : hi2c 句柄返 回 值 : void作 者 : Bright创建日期 : 20240624
*****************************************************************************/
void HAL_I2C_SlaveTxCpltCallback(I2C_HandleTypeDef *hi2c)
{if(hi2c->Instance == I2C2){offset++; // 每发送一个数据,偏移+1HAL_I2C_Slave_Seq_Transmit_IT(hi2c, &ram[offset], sizeof(ram), I2C_NEXT_FRAME); // 打开中断并把ram[]里面对应的数据发送给主机}
}
2、I2c.h文件
#ifndef __I2C_H_
#define __I2C_H_
#include "sys/sys.h"
extern uint8_t ram[256]; #define I2C_DEVICE_ADDR 0xA0#define IIC_SCL_GPIO_PORT GPIOB
#define IIC_SCL_GPIO_PIN GPIO_PIN_10
#define IIC_SCL_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */#define IIC_SDA_GPIO_PORT GPIOB
#define IIC_SDA_GPIO_PIN GPIO_PIN_9
#define IIC_SDA_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 *//******************************************************************************************//* IO操作 */
#define IIC_SCL(x) do{ x ? \HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \}while(0) /* SCL */#define IIC_SDA(x) do{ x ? \HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \}while(0) /* SDA */#define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN) /* 读取SDA */extern void SlaveI2cInit(void);
/*************************************IIC RAM[256]内存说明*****************************************************//******************************************************************************************/
#endif
四、主从模式测试
1、主测试代码:
#include "config.h"
u8 readData=0;
int main(void)
{DemoSystemInit();printf("%d\r\n",HAL_RCC_GetSysClockFreq());printf("Mcu pow On \r\n");uint8_t writeData=0XAA;uint8_t writeData2=0XBB;I2C_Write(0X00,writeData);readData=I2C_Read(0X02);//电池温度ram[2]while(1){if(readData == 0XAA){LED_ON();}else if(readData == 0XBB){LED_ON();DelayMs(1000);LED_OFF();DelayMs(1000);}else{LED_OFF(); }}
}
2、从测试代码
#include "config.h"
extern uint8_t ram[256]; int main(void)
{DemoSystemInit();while(1){if(ram[0] == 0xAA){LED_ON();}else if(ram[0] == 0xBB){LED_ON();DelayMs(500);LED_OFF();DelayMs(500);}else{LED_OFF(); }}}
3、实际效果
五、结尾
总体上硬件i2c操作便是如此,可以作为少量快速相应的通信方式, 之前是想将其与I2S一起实现,但I2S的数据量需要的相应数据过快,I2C不适合。后续还会更新一下软件i2c实现主从机模式,作为学习I2c的对比总结。