stm32主从机硬件IIC实现
前言:
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 config
GPIO_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 config
GPIO_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++; // 每发送一个数据,偏移+1
HAL_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的对比总结。