Cortex-M3-STM32F1 开发:(五十)软件模拟 IIC 和硬件 IIC 的区别,以及软件 IIC 配置步骤及相关函数,以及相关问题

上一篇 下一篇
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)配置步骤

  1. 使能 SCL 和 SDA 对应时钟
    • __HAL_RCC_GPIOB_CLK_ENABLE()
  2. 设置 GPIO 工作模式
    • SDA 开漏 / SCL 推挽输出模式,使用 HAL_GPIO_Init 初始化
  3. 编写基本信号
    • 起始信号、停止信号、应答信号【接收器:send_ack()send_nack();发送器:wait_ack()
  4. 编写读和写函数
    • 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 引脚没有外接上拉电阻,那么波形的上升沿部分就会不利落、上不去,具体如下所示:

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


相关推荐
清风6666662 小时前
基于单片机的电流电压可调数控电源
单片机·毕业设计·课程设计·期末大作业
泡泡糖的中文规格书2 小时前
【无标题】
单片机·嵌入式硬件·规格说明书·datasheet
LongRunning3 小时前
【BLE】STM32WB55+CubeMAX_BLE配置
stm32
风雨中的蜜蜂3 小时前
SKY13330-397LF国产替代ATR5330 SPDT开关芯片
单片机·嵌入式硬件
殷忆枫3 小时前
基于STM32的ESP8266连接Onenet(HAL库)
stm32·单片机·嵌入式硬件
Sophia么么3 小时前
嵌入式知识---如何配置定时器的时基单元,如何配置输出通道
单片机·嵌入式硬件
Katecat996633 小时前
尿液样本中细胞与非细胞成分检测分类系统实现
单片机·分类·数据挖掘
2401_863318633 小时前
基于单片机的恒温箱设计
单片机·嵌入式硬件
ベadvance courageouslyミ4 小时前
嵌入式硬件基础
嵌入式硬件·51单片机·嵌入式·数码管·二极管