欢迎来到我的博客。今天我想向大家介绍一下STM32软件I2C功能。
首先,让我们来了解一下I2C(Inter-Integrated Circuit)总线。I2C是一种串行通信总线,最初由Philips公司开发。它允许多个设备使用同一条总线进行通信,并且每个设备都有唯一的地址。I2C通常用于连接微控制器、传感器和其他外设。
在STM32中,I2C总线被实现为硬件和软件两种方式。硬件I2C功能可以直接使用STM32芯片上的I2C外设,而软件I2C需要通过编程实现。由于某些应用场景不适宜使用硬件I2C功能,所以软件I2C在STM32中也变得非常重要。
STM32软件I2C功能与硬件I2C功能类似,它们之间的主要区别在于数据传输的过程。软件I2C需要使用GPIO口模拟I2C通信过程,因此实现起来相对复杂。但是软件I2C具有很高的灵活性,可以根据需要进行修改和扩展。
在STM32中,软件I2C驱动程序通常由以下几个部分组成:
初始化:这一步包括配置GPIO口、设置时序等操作,以确保I2C通信正常进行。
启动:启动信号是I2C总线上的一个信号,用于指示传输开始。为了在软件I2C中实现“启动”信号,我们需要将SDA(数据线)从高电平拉到低电平,然后将SCL(时钟线)从高电平拉到低电平。
停止:停止信号用于指示传输结束。在软件I2C中,我们需要将SCL从低电平拉到高电平,然后将SDA从低电平拉到高电平。
数据传输:数据传输通过向SDA写入位来完成。在传输数据之前,我们需要向SCL写入一个脉冲来获取ACK(应答)信号,以确保数据已被正确接收。
虽然软件I2C比硬件I2C更加复杂,但它具有很高的灵活性和可扩展性。此外,在某些情况下,软件I2C可以提供更好的性能和功耗优化。
下面上代码。根据野火例程修改而来,已验证。
bsp_i2c_gpio.c
/**
******************************************************************************
* @file bsp_i2c_ee.c
* @version V1.0
* @date 2023-4-12
* @brief 用gpio模拟i2c总线, 适用于STM32系列CPU。该模块不包括应用层命令帧,仅包括I2C总线基本操作函数。
******************************************************************************
#include "bsp_i2c_gpio.h"
#include "stm32f4xx.h"
#include <stdio.h>
/*
*********************************************************************************************************
* 函 数 名: i2c_Delay
* 功能说明: I2C总线位延迟,最快400KHz
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
static void i2c_Delay(void)
{
uint8_t i;
/*
下面的时间是通过逻辑分析仪测试得到的。
工作条件:CPU主频72MHz ,MDK编译环境,1级优化
循环次数为10时,SCL频率 = 205KHz
循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us
循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us
*/
for (i = 0; i < 10; i++)
;
}
/*
*********************************************************************************************************
* 函 数 名: i2c_Start
* 功能说明: CPU发起I2C总线启动信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_Start(void)
{
/* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
BSP_I2C_SDA_1();
BSP_I2C_SCL_1();
i2c_Delay();
BSP_I2C_SDA_0();
i2c_Delay();
BSP_I2C_SCL_0();
i2c_Delay();
}
/*
*********************************************************************************************************
* 函 数 名: i2c_Stop
* 功能说明: CPU发起I2C总线停止信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_Stop(void)
{
/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
BSP_I2C_SDA_0();
BSP_I2C_SCL_1();
i2c_Delay();
BSP_I2C_SDA_1();
}
/*
*********************************************************************************************************
* 函 数 名: i2c_SendByte
* 功能说明: CPU向I2C总线设备发送8bit数据
* 形 参:_ucByte : 等待发送的字节
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_SendByte(uint8_t _ucByte)
{
uint8_t i;
/* 先发送字节的高位bit7 */
for (i = 0; i < 8; i++)
{
if (_ucByte & 0x80)
{
BSP_I2C_SDA_1();
}
else
{
BSP_I2C_SDA_0();
}
i2c_Delay();
BSP_I2C_SCL_1();
i2c_Delay();
BSP_I2C_SCL_0();
if (i == 7)
{
BSP_I2C_SDA_1(); // 释放总线
}
_ucByte <<= 1; /* 左移一个bit */
i2c_Delay();
}
}
/*
*********************************************************************************************************
* 函 数 名: i2c_ReadByte
* 功能说明: CPU从I2C总线设备读取8bit数据
* 形 参:无
* 返 回 值: 读到的数据
*********************************************************************************************************
*/
uint8_t i2c_ReadByte(void)
{
uint8_t i;
uint8_t value;
/* 读到第1个bit为数据的bit7 */
value = 0;
for (i = 0; i < 8; i++)
{
value <<= 1;
BSP_I2C_SCL_1();
i2c_Delay();
if (BSP_I2C_SDA_READ())
{
value++;
}
BSP_I2C_SCL_0();
i2c_Delay();
}
return value;
}
/*
*********************************************************************************************************
* 函 数 名: i2c_WaitAck
* 功能说明: CPU产生一个时钟,并读取器件的ACK应答信号
* 形 参:无
* 返 回 值: 返回0表示正确应答,1表示无器件响应
*********************************************************************************************************
*/
uint8_t i2c_WaitAck(void)
{
uint8_t re;
BSP_I2C_SDA_1(); /* CPU释放SDA总线 */
i2c_Delay();
BSP_I2C_SCL_1(); /* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
i2c_Delay();
if (BSP_I2C_SDA_READ()) /* CPU读取SDA口线状态 */
{
re = 1;
}
else
{
re = 0;
}
BSP_I2C_SCL_0();
i2c_Delay();
return re;
}
/*
*********************************************************************************************************
* 函 数 名: i2c_Ack
* 功能说明: CPU产生一个ACK信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_Ack(void)
{
BSP_I2C_SDA_0(); /* CPU驱动SDA = 0 */
i2c_Delay();
BSP_I2C_SCL_1(); /* CPU产生1个时钟 */
i2c_Delay();
BSP_I2C_SCL_0();
i2c_Delay();
BSP_I2C_SDA_1(); /* CPU释放SDA总线 */
}
/*
*********************************************************************************************************
* 函 数 名: i2c_NAck
* 功能说明: CPU产生1个NACK信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_NAck(void)
{
BSP_I2C_SDA_1(); /* CPU驱动SDA = 1 */
i2c_Delay();
BSP_I2C_SCL_1(); /* CPU产生1个时钟 */
i2c_Delay();
BSP_I2C_SCL_0();
i2c_Delay();
}
/*
*********************************************************************************************************
* 函 数 名: i2c_CfgGpio
* 功能说明: 配置I2C总线的GPIO,采用模拟IO的方式实现
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_CfgGpio(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
I2Cx_SCL_GPIO_CLK_ENABLE();
I2Cx_SDA_GPIO_CLK_ENABLE();
/**I2C2 GPIO Configuration
PB10 ------> I2C2_SCL
PB9 ------> I2C2_SDA
*/
GPIO_InitStruct.Pin = BSP_I2C_SCL_PIN | BSP_I2C_SDA_PIN;
;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(BSP_GPIO_PORT_I2C, &GPIO_InitStruct);
/* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
i2c_Stop();
}
bsp_i2c_gpio.h
#ifndef _BSP_I2C_GPIO_H
#define _BSP_I2C_GPIO_H
#include <inttypes.h>
#define BSP_I2C_WR 0 /* 写控制bit */
#define BSP_I2C_RD 1 /* 读控制bit */
/* 定义I2C总线连接的GPIO端口时钟控制 */
#define I2Cx_SDA_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2Cx_SCL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
/* 定义I2C总线连接的GPIO端口, 用户只需要修改下面3行代码即可任意改变SCL和SDA的引脚 */
#define BSP_GPIO_PORT_I2C GPIOB /* GPIO端口 */
#define BSP_I2C_SCL_PIN GPIO_PIN_8 /* 连接到SCL时钟线的GPIO */
#define BSP_I2C_SDA_PIN GPIO_PIN_9 /* 连接到SDA数据线的GPIO */
/* 定义读写SCL和SDA的宏,已增加代码的可移植性和可阅读性 */
#if 0 /* 条件编译: 1 选择GPIO的库函数实现IO读写 */
#define BSP_I2C_SCL_1() digitalH(BSP_GPIO_PORT_I2C, BSP_I2C_SCL_PIN) /* SCL = 1 */
#define BSP_I2C_SCL_0() digitalL(BSP_GPIO_PORT_I2C, BSP_I2C_SCL_PIN) /* SCL = 0 */
#define BSP_I2C_SDA_1() digitalH(BSP_GPIO_PORT_I2C, BSP_I2C_SDA_PIN) /* SDA = 1 */
#define BSP_I2C_SDA_0() digitalL(BSP_GPIO_PORT_I2C, BSP_I2C_SDA_PIN) /* SDA = 0 */
//#define BSP_I2C_SDA_READ() GPIO_ReadInputDataBit(BSP_GPIO_PORT_I2C, BSP_I2C_SDA_PIN) /* 读SDA口线状态 */
#define BSP_I2C_SDA_READ() ((BSP_GPIO_PORT_I2C->IDR & BSP_I2C_SDA_PIN) != 0) /* 读SDA口线状态 */
#else /* 这个分支选择直接寄存器操作实现IO读写 */
/* 注意:如下写法,在IAR最高级别优化时,会被编译器错误优化 */
#define BSP_I2C_SCL_1() BSP_GPIO_PORT_I2C->BSRR = (uint32_t)BSP_I2C_SCL_PIN /* SCL = 1 */
#define BSP_I2C_SCL_0() BSP_GPIO_PORT_I2C->BSRR = (uint32_t)BSP_I2C_SCL_PIN << 16U /* SCL = 0 */
#define BSP_I2C_SDA_1() BSP_GPIO_PORT_I2C->BSRR = (uint32_t)BSP_I2C_SDA_PIN /* SDA = 1 */
#define BSP_I2C_SDA_0() BSP_GPIO_PORT_I2C->BSRR = (uint32_t)BSP_I2C_SDA_PIN << 16U /* SDA = 0 */
#define BSP_I2C_SDA_READ() ((BSP_GPIO_PORT_I2C->IDR & BSP_I2C_SDA_PIN) != 0) /* 读SDA口线状态 */
#endif
/* 直接操作寄存器的方法控制IO */
#define digitalH(p, i) \
{ \
p->BSRR = i; \
} // 设置为高电平
#define digitalL(p, i) \
{ \
p->BSRR = (uint32_t)i << 16; \
} // 输出低电平
void i2c_CfgGpio(void);
void i2c_Start(void);
void i2c_Stop(void);
void i2c_SendByte(uint8_t _ucByte);
uint8_t i2c_ReadByte(void);
uint8_t i2c_WaitAck(void);
void i2c_Ack(void);
void i2c_NAck(void);
#endif
最后,不要忘记在主程序中调用 i2c_CfgGpio();
完成用于模拟I2C的GPIO初始化。
应用实例
这里我使用的是lis2dw12加速度传感器,在数据手册中给出了I2C通信时序如下。
Master:主机
Slave:从机
ST:起始信号 START signal
SAD:从机地址 Slave Address
SAK:从机应答 slave acknowledge
DATA :8位的数据内容
SP:停止信号 STOP signal
NMAK :非主机应答 No Master Acknowledge
SUB:8位的子地址 8-bit sub-address
W :读操作
R:写操作
软件模拟这个流程就能实现通讯,对照时序图和程序的每一个步骤阅读,方便理解。
使用前记得包含头文件
#include "bsp_i2c_gpio.h"
软件模拟读操作
/*
* @brief Read generic device register (platform dependent)
*
* @param handle customizable argument. In this examples is used in
* order to select the correct sensor bus handler.
* @param reg register to read
* @param bufp pointer to buffer that store the data read
* @param len number of consecutive register to read
*
*/
static int32_t platform_read(void *handle, uint8_t reg, uint8_t *bufp,
uint16_t len)
{
uint16_t i;
/* 第1步:发起I2C总线启动信号 */
i2c_Start();
/* 第2步:发送控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
i2c_SendByte(BSP_I2C_ADD | BSP_I2C_WR); /* 写指令 */
/* 第3步:等待ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail;
}
/* 第4步: 发送SUB */
i2c_SendByte(reg);
/* 第5步: 等待ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail;
}
/* 第6步: 发送SR (repeated START) */
i2c_Start();
/* 第7步: 发送控制字节 */
i2c_SendByte(BSP_I2C_ADD | BSP_I2C_RD); /* 读指令 */
/* 第8步: 发送ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail;
}
/* 第9步: 循环读取数据 */
for (i = 0; i < len; i++)
{
bufp[i] = i2c_ReadByte(); /* 读1个字节 */
/* 每读完1个字节后,需要主机发送ACK,最后一个字节发送NACK */
if (i != len - 1)
{
i2c_Ack(); /* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
}
else
{
i2c_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
}
}
/* 第10步:发送停止信号 */
i2c_Stop();
return 0;
cmd_fail:
i2c_Stop();
return 1;
}
软件模拟写操作
/*
* @brief Write generic device register (platform dependent)
*
* @param handle customizable argument. In this examples is used in
* order to select the correct sensor bus handler.
* @param reg register to write
* @param bufp pointer to buffer that store the data read
* @param len number of consecutive register to read
*
*/
static int32_t platform_write(void *handle, uint8_t reg, const uint8_t *bufp,
uint16_t len)
{
uint16_t i;
/* 第0步: 发送停止信号 */
i2c_Stop();
/* 第1步: 发起I2C总线启动信号 */
i2c_Start();
/* 第2步: 发送控制字节 */
i2c_SendByte(BSP_I2C_ADD | BSP_I2C_WR);
/* 第3步: 等待ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail;
}
/* 第4步: 发送SUB */
i2c_SendByte(reg);
/* 第5步: 等待ACK */
if (i2c_WaitAck() != 0)
{
goto cmd_fail;
}
/* 第6步: 循环发送DATA */
for (i = 0; i < len; i++)
{
i2c_SendByte(bufp[i]); /* 发一个数据 */
if (i2c_WaitAck() != 0)
{
goto cmd_fail; /* 无应答 */
}
}
/* 第7步: 发送停止信号 */
i2c_Stop();
return 0;
cmd_fail:
i2c_Stop();
return 1;
}
总而言之,STM32软件I2C是一种非常重要的通信方式,尤其适用于那些不适合使用硬件I2C的应用场景。希望本文对你了解STM32软件I2C功能有所帮助,如果有不理解的地方欢迎私信留言。