| 上一篇 | 下一篇 |
|---|---|
| IIC 总线结构和协议 |
2)软件模拟 IIC 和硬件 IIC 的区别
结合上面的时钟信号的说明
-
硬件 I²C:
单片机内部有专门的 I²C 控制器模块(比如 STM32、某些增强型 51、AVR、ESP 等),你只需要配置寄存器,它就能自动处理起始信号、停止信号、时钟、数据收发、ACK/NACK 等时序。优点是省 CPU 资源、速度快、稳定可靠。
-
软件模拟 I²C(又称"位带操作"或"GPIO 模拟"):
此类单片机没有专用 I²C 硬件模块 (比如传统 8051 单片机),就用普通 GPIO 引脚,通过程序逐位控制高低电平 来模拟 I²C 的时序(起始、停止、SCL 时钟、SDA 数据等)。优点是灵活、任何 GPIO 都能用 ,缺点是占用 CPU 时间、速度慢、容易受中断干扰。
所以,像经典的 8051 单片机 (如 AT89C51)确实没有硬件 I²C ,必须用软件模拟。而一些增强型 51(如 STC12/15 系列部分型号)可能集成了硬件 I²C 模块。
对比总结表如下:

一般来说:硬件 IIC 要留给高级外设或者电源管理通讯,触摸屏等简单外设要用软件模拟 IIC 。并且由于版权的原因,STM32 把 IIC 的 HAL 库写的非常复杂,如果需要使用硬件 IIC 的话,会调用函数就行。
3)软件模拟 IIC 配置步骤及相关函数
不要误以为发送器只能是主机,发送器和接收器都可以是主机或从机。
主机通常是 MCU,从机通常是外设。
3.1)配置步骤
- 使能 SCL 和 SDA 对应时钟
__HAL_RCC_GPIOB_CLK_ENABLE()
- 设置 GPIO 工作模式
- SDA 开漏 / SCL 推挽输出模式,使用 HAL_GPIO_Init 初始化
- 编写基本信号
- 起始信号、停止信号、应答信号【接收器:
send_ack()、send_nack();发送器:wait_ack()】
- 起始信号、停止信号、应答信号【接收器:
- 编写读和写函数
iic_read_byte()iic_send_byte(),发送完成,发送器要释放 SDA
说明:为什么 IIC 总线中的 SDA 线引脚建议用开漏模式?⭐️
IIC 的 SDA 脚即要作为输出,又要作为输入,用开漏输出模式,可以很好实现输出输入共用,避免 IO 模式频繁切换带来的麻烦。
- 主机输出时:主机(MCU)输出 0,可以拉低信号,来实现低电平发送,主机输出 1(实际不起作用),由外部上拉电阻上拉,实现高电平发送。
- 主机输入时:主机(MCU)设置输出 1 状态,此时因为 MCU 无法输出 1,相当于释放了 SDA 脚;此时外部器件可以主动拉低 SDA脚 / 释放 SDA 脚(同样由上拉电阻提供"输出1的功能"),实现SDA脚的高低电平变化。
由于开漏输出模式下,MCU 还是可以读取 IDR(输入数据寄存器),来获取引脚高低电平,因此 MCU 读取 IDR ,即可获得 SDA 脚的高低电平状态,从而实现输入检测。
3.2)相关函数
只需要更改 .h 文件中的引脚定义部分即可应用到别的板子上
software_iic.h 文件代码如下:
c
#ifndef __SOFTWARE_IIC_H
#define __SOFTWARE_IIC_H
#include "sys.h"
/******************************************************************************************/
/* 引脚 定义 */
#define IIC_SCL_GPIO_PORT GPIOB
#define IIC_SCL_GPIO_PIN GPIO_PIN_6
#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_7
#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 */
void iic_init(void);
void iic_start(void);
void iic_stop(void);
uint8_t iic_wait_ack(void);
void iic_ack(void);
void iic_nack(void);
void iic_send_byte(uint8_t data);
uint8_t iic_read_byte (uint8_t ack);
#endif
software_iic.c 文件代码如下:
c
#include "software_iic.h"
#include "delay.h"
/**
* @brief 模拟IIC初始化函数
* @param none
* @retval none
* @note 主要是 GPIO 引脚的配置以及时钟使能
*/
void iic_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
IIC_SCL_GPIO_CLK_ENABLE(); /* SCL引脚时钟使能 */
IIC_SDA_GPIO_CLK_ENABLE(); /* SDA引脚时钟使能 */
gpio_init_struct.Pin = IIC_SCL_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(IIC_SCL_GPIO_PORT, &gpio_init_struct); /* SCL */
gpio_init_struct.Pin = IIC_SDA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD; /* 开漏输出 */
HAL_GPIO_Init(IIC_SDA_GPIO_PORT, &gpio_init_struct); /* SDA */
/* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */
}
/**
* @brief 延时函数
* @note 固定2us,用于等待信号线电平响应,可加长时间
*/
static void iic_delay(void)
{
delay_us(2);
}
/**
* @brief 起始信号
* @note SCL为高电平期间, SDA从高电平往低电平跳变
*/
void iic_start(void)
{
IIC_SDA (1);
IIC_SCL (1);
iic_delay( );
IIC_SDA (0);
iic_delay( );
IIC_SCL (0);
iic_delay( ); /* 钳住总线, 准备发送/接收数据 */
}
/**
* @brief 停止信号
* @note SCL为高电平期间, SDA从低电平往高电平跳变
*/
void iic_stop(void)
{
IIC_SDA (0);
iic_delay( );
IIC_SCL (1);
iic_delay( );
IIC_SDA (1); /* 发送总线停止信号*/
iic_delay( );
}
/**
* @brief 等待应答信号
* @retval 1:NACK、0:ACK
*/
uint8_t iic_wait_ack (void)
{
IIC_SDA (1); /* 主机释放SDA线 */
iic_delay( );
IIC_SCL (1); /* 拉高SCL线,等待从机返回ACK */
iic_delay( );
if(IIC_READ_SDA)
{
iic_stop(); /* SDA高电平表示从机NACK */
return 1;
}
IIC_SCL(0); /* 拉低SCL,结束ACK检查 */
iic_delay( );
return 0; /* SDA低电平表示从机ACK */
}
/**
* @brief 发送应答信号
* @note SCL为高电平期间, SDA为低电平
*/
void iic_ack(void)
{
IIC_SCL (1);
iic_delay( );
IIC_SDA (0); /* 数据线为低电平,表示应答 */
iic_delay( );
IIC_SCL (0);
iic_delay( );
}
/**
* @brief 发送非应答信号
* @note SCL为高电平期间, SDA为高电平
*/
void iic_nack(void)
{
IIC_SCL (1);
iic_delay( );
IIC_SDA (1); /* 数据线为高电平,表示非应答 */
iic_delay( );
IIC_SCL (0);
iic_delay( );
}
/**
* @brief 发送 1 个字节数据
* @param data:1字节数据
* @note 高位先发
*/
void iic_send_byte(uint8_t data)
{
for (uint8_t t = 0; t < 8; t++)
{
/* 高位先发 */
IIC_SDA((data & 0x80) >> 7);
iic_delay( );
IIC_SCL (1);
iic_delay( );
IIC_SCL (0);
data <<= 1; /* 左移1位, 用于下一次发送 */
}
IIC_SDA (1); /* 发送完成,主机释放SDA线 */
}
/**
* @brief 读取 1 个字节数据
* @param ack:是否应答(1:ACK、0:NACK)
* @retval receive:1字节数据
* @note 高位先收
*/
uint8_t iic_read_byte (uint8_t ack)
{
uint8_t receive = 0 ;
for (uint8_t t = 0; t < 8; t++)
{
/* 高位先输出,先收到的数据位要左移 */
receive <<= 1;
IIC_SCL ( 1 );
iic_delay( );
if ( IIC_READ_SDA ) receive++;
IIC_SCL ( 0 );
iic_delay( );
}
if (ack) iic_ack(); // 发送 ACK
else iic_nack(); // 发送 NACK
return receive;
}
4)问题
如果 SCL 和 SDA 引脚没有外接上拉电阻,那么波形的上升沿部分就会不利落、上不去,具体如下所示:

虽然接了上拉电阻也会出现类似的情况😄。