1 IIC 协议概述

① 起始位和结束位

空闲状态 SCL 和 SDA 都是高电平!

起始位: SCL高电平时候,检测到 SDA 下降沿
结束位: SCL高电平时候,检测到 SDA 上升沿
② 物理层

总线上的设备(主设备还是从设备)使用开漏模式,靠总线上的上拉电阻输出高电平

③ 协议层
1. 在SCL还是低电平的时候,修改SDA的状态
2. SDA 高低电平要持续稳定一段时间
④ --模式--
标准模式: 100Kbit/s
快速模式: 400kbit/s
高速模式: 3.4Mbit/s

####

2 EEPROM 概述

① 设备地址

固定1010 + E1、E2、E3三个引脚配置

② 写多个字节数据(一页内)

EEPROM 检测到Stop信号开始写(M24C02的写周期 5ms)

③ 读多个字节数据(先伪写再读)

EEPROM 收到主设备发的 NACK 后会停止发送数据。

3 使用IIC协议读写EEPROM 软件方式实现 (寄存器)

需求描述

我们向E2PROM写入一段数据,再读取出来,最后发送到串口,核对是否读写正确。

硬件电路设计

硬件原理图

① IIC驱动代码
1. 初始化
2. 发送起始信号
3. 发送结束信号
4. 发送一个字节的数据
5. 接收应答信号
6. 接收一个字节的数据
7. 发送应答信号
Driver_I2C2.h
#ifndef __DRIVER_I2C2_H__
#define __DRIVER_I2C2_H__

#include <stdint.h>
#include "stm32f10x.h"
#include "Common_Delay.h"

#define SCL_HIGH GPIOB->ODR |= GPIO_ODR_ODR10 // SCL引脚输出高电平
#define SCL_LOW GPIOB->ODR &= ~GPIO_ODR_ODR10 // SCL引脚输出低电平

#define SDA_HIGH GPIOB->ODR |= GPIO_ODR_ODR11 // SDA引脚输出高电平
#define SDA_LOW GPIOB->ODR &= ~GPIO_ODR_ODR11 // SDA引脚输出低电平

#define I2C2_DELAY Delay_us(10)      // 每次改变引脚状态之后的延时

#define SDA_READ (GPIOB->IDR & GPIO_IDR_IDR11) ? 1 : 0// 读取SDA引脚的电平

/**
 * @brief I2C2初始化
 * 
 */
void Driver_I2C2_Init(void);

/**
 * @brief 发送起始信号
 * 
 */
void Driver_I2C2_Start(void);

/**
 * @brief 发送停止信号
 * 
 */
void Driver_I2C2_Stop(void);

/**
 * @brief 写入一个字节
 * 
 * @param byte 要写入的字节
 */
void Driver_I2C2_SendByte(uint8_t byte);

/**
 * @brief 接收应答信号
 * 
 * @return uint8_t 0表示ACK,1表示NACK
 */
uint8_t Driver_I2C2_ReceiveACK(void);

/**
 * @brief 读取一个字节
 *
 * @return uint8_t 读取到的字节
 */
uint8_t Driver_I2C2_ReceiveByte(void);


/**
 * @brief 发送应答信号
 *
 * @param ack 0表示ACK,1表示NACK
 */
void Driver_I2C2_SendACK(uint8_t ack);



#endif /* __DRIVER_I2C2_H__ */
Driver_I2C2.c
#include "Driver_I2C2.h"

/**
 * @brief I2C2初始化
 * 
 */
void Driver_I2C2_Init(void)
{
    // 使能GPIOB时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // 设置GPIOB的PB10和PB11为通用开漏输出模式,最大输出速度50MHZ,CNF11为01,MODE11为11;CNF10为01,MODE10为11
    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);  // 设置最大输出速度为50MHz
    GPIOB->CRH &= ~(GPIO_CRH_CNF10_1 | GPIO_CRH_CNF11_1);    // 设置为通用开漏输出模式
    GPIOB->CRH |= (GPIO_CRH_CNF10_0 | GPIO_CRH_CNF11_0);    
}

/**
 * @brief 发送起始信号
 * 
 */
void Driver_I2C2_Start(void)
{
    // 拉高SCL线
    SCL_HIGH;
    I2C2_DELAY;

    // 拉高SDA线 SDA产生下降沿作为起始信号
    SDA_HIGH;
    I2C2_DELAY;
    // 拉低SDA线
    SDA_LOW;
    I2C2_DELAY;

    // 拉低SCL线 准备数据
    SCL_LOW;
    I2C2_DELAY;
}

/**
 * @brief 发送停止信号
 * 
 */
void Driver_I2C2_Stop(void)
{
    // 拉低SDA线
    SDA_LOW;
    I2C2_DELAY;

    // 拉高SCL线
    SCL_HIGH;
    I2C2_DELAY;

    // 拉高SDA线 SDA产生上升沿作为停止信号
    SDA_HIGH;
    I2C2_DELAY;
}

/**
 * @brief 写入一个字节
 * 
 * @param byte 要写入的字节
 */
void Driver_I2C2_SendByte(uint8_t byte)
{
    // 发送8位数据
    for (uint8_t i = 0; i < 8; i++)
    {
        // 准备发送数据, 先判断最高位是否为1, SCL低电平时, SDA上的数据才有效
        if (byte & 0x80)
        {
            SDA_HIGH;
        }
        else
        {
            SDA_LOW;
        }
        I2C2_DELAY;
        
        // 拉高SCL线
        SCL_HIGH;
        I2C2_DELAY;
        
        // 拉低SCL线
        SCL_LOW;
        I2C2_DELAY;

        // 左移一位
        byte <<= 1;
    }
}

/**
 * @brief 接收应答信号
 * 
 * @return uint8_t 0表示ACK,1表示NACK
 */
uint8_t Driver_I2C2_ReceiveACK(void)
{
    // 准备接收数据, 释放数据总线
    SDA_HIGH;
    I2C2_DELAY;

    // 拉高SCL线
    SCL_HIGH;
    I2C2_DELAY;

    // 读取SDA线状态
    uint8_t ack = SDA_READ;

    // 拉低SCL线
    SCL_LOW;
    I2C2_DELAY;

    return ack;
}

/**
 * @brief 读取一个字节
 *
 * @return uint8_t 读取到的字节
 */
uint8_t Driver_I2C2_ReceiveByte(void)
{
    uint8_t byte = 0;

    // 接收8位数据
    for (uint8_t i = 0; i < 8; i++)
    {
        // 准备接收数据, 释放数据总线
        SDA_HIGH;
        I2C2_DELAY;

        // 拉高SCL线
        SCL_HIGH;
        I2C2_DELAY;

        // 读取SDA线状态
        byte <<= 1;
        if (SDA_READ)
        {
            byte |= 0x01;
        }
    
        // 拉低SCL线
        SCL_LOW;
        I2C2_DELAY;
    }

    return byte;
}


/**
 * @brief 发送应答信号
 *
 * @param ack 0表示ACK,1表示NACK
 */
void Driver_I2C2_SendACK(uint8_t ack)
{
    // 发送应答信号
    if (ack)  // NACK
    {
        SDA_HIGH;
    }
    else  // ACK
    {
        SDA_LOW;
    }
    I2C2_DELAY;

    // 拉高SCL线
    SCL_HIGH;
    I2C2_DELAY;

    // 拉低SCL线
    SCL_LOW;
    I2C2_DELAY;
}
② EEPROM 部分代码
1. 向EEPROM的指定位置写入指定长度的数据
2. 从EEPROM的指定位置读取指定长度的数据
Hardware_EEPROM.h
#ifndef __HARDWARE_EEPROM_H__
#define __HARDWARE_EEPROM_H__

#include <stdint.h>
#include "Driver_I2C2.h"

#define DEV_ADDR_W 0xA0
#define DEV_ADDR_R 0xA1
#define PAGE_SIZE 16    // EEPROM每页大小

/**
 * @brief EEPROM写入数据
 * 
 * @param addr 写入地址
 * @param buf 数据
 * @param len 数据长度(字节个数)
 */
void Hardware_EEPROM_Write(uint8_t addr, uint8_t *bufs, uint16_t len);

/**
 * @brief EEPROM读取数据
 *
 * @param addr 读取地址
 * @param bufs 写读取到的数据写入该地址处
 * @param len 数据长度(字节个数)
 */
void Hardware_EEPROM_Read(uint8_t addr, uint8_t *bufs, uint16_t len);



#endif /* __HARDWARE_EEPROM_H__ */
Hardware_EEPROM.c
#include "Hardware_EEPROM.h"

// 向一个页内写入数据
static void Hardware_EEPROM_Write_Page(uint8_t addr, uint8_t *bufs, uint8_t len)
{
    // 发送起始信号
    Driver_I2C2_Start();

    // 发送从设备地址和写标识,并接收应答信号
    Driver_I2C2_SendByte(DEV_ADDR_W); // 0xA0为从设备地址,0表示写操作
    Driver_I2C2_ReceiveACK();

    // 发送EEPROM内部字节地址和接收应答信号
    Driver_I2C2_SendByte(addr); 
    Driver_I2C2_ReceiveACK();

    // 发送数据和接收应答信号
    for (uint8_t i = 0; i < len; i++) {
        Driver_I2C2_SendByte(bufs[i]);
        Driver_I2C2_ReceiveACK();
    }

    // 发送停止信号
    Driver_I2C2_Stop();

    // 延时,等待写周期
    Delay_ms(5);
}

/**
 * @brief EEPROM写入数据
 * 
 * @param addr 写入地址
 * @param buf 数据
 * @param len 数据长度(字节个数)
 */
void Hardware_EEPROM_Write(uint8_t addr, uint8_t *bufs, uint16_t len)
{
    uint8_t page_remain = 0;

    while (1)
    {
        // 根据 addr 计算当前页的剩余长度
        page_remain = PAGE_SIZE - (addr % PAGE_SIZE);

        // 如果剩余长度大于等于要写入的数据长度,则直接写入
        if (page_remain >= len) {
            Hardware_EEPROM_Write_Page(addr, bufs, len);
            return;
        }   
        else {
            Hardware_EEPROM_Write_Page(addr, bufs, page_remain);  // 写入当前页剩余的数据
            len -= page_remain;     // 剩余数据长度减少
            bufs += page_remain;    // 数据指针移到下一页
            addr += page_remain;    // 地址指针移到下一页
        }
    }
}

/**
 * @brief EEPROM读取数据
 *
 * @param addr 读取地址
 * @param bufs 写读取到的数据写入该地址处
 * @param len 指定读取的数据长度(字节个数)
 */
void Hardware_EEPROM_Read(uint8_t addr, uint8_t *bufs, uint16_t len)
{
    // 第一大步 伪写 ---------------------------------------------------------
    // 发送起始信号
    Driver_I2C2_Start();

    // 发送从设备地址和写标识,并接收应答信号
    Driver_I2C2_SendByte(DEV_ADDR_W); // 0xA0为从设备地址,0表示写操作
    Driver_I2C2_ReceiveACK();

    // 发送EEPROM内部字节地址和接收应答信号
    Driver_I2C2_SendByte(addr);
    Driver_I2C2_ReceiveACK();


    // 第二大步 正式读 -------------------------------------------------------
    // 发送起始信号
    Driver_I2C2_Start();

    // 发送从设备地址和读标识,并接收应答信号
    Driver_I2C2_SendByte(DEV_ADDR_R); // 0xA0为从设备地址,1表示读操作
    Driver_I2C2_ReceiveACK();

    // 读取多个字节数据
    for (uint8_t i = 0; i < len; i++) {
        // 接收一个字节
        bufs[i] = Driver_I2C2_ReceiveByte();
        
        // 发送应答信号
        if (i == len - 1) {
            Driver_I2C2_SendACK(1);       // 最后一个字节,发送NACK
        } else {
            Driver_I2C2_SendACK(0);       // 非最后一个字节,发送ACK
        }
    }

    // 发送停止信号
    Driver_I2C2_Stop();
}
main.c
#include <stdio.h>
#include "stm32f10x.h"
#include "Driver_USART1.h"
#include "Driver_I2C2.h"
#include "Hardware_EEPROM.h"

// 主函数
int main()
{
    // 初始化USART1
    Driver_USART1_Init();

    // 初始化I2C2
    Driver_I2C2_Init();

    // 定义一个数组,用于存储要写入的数据
    // uint8_t bufs[15] = "Happy New Year!";
    // 向EEPROM写入数据
    // Hardware_EEPROM_Write(0x00, bufs, 15);

    // 定义一个数组,用于存储读取的数据
    uint8_t bufr[15];
    // 从EEPROM读取数据
    Hardware_EEPROM_Read(0x00, bufr, 15);

    // 打印读取的数据
    printf("%s\r\n", bufr);


    /* // 初始化I2C2
    Driver_I2C2_Init();

    // 发送起始信号
    Driver_I2C2_Start();

    // 发送地址和读写位,并接收应答信号
    Driver_I2C2_SendByte(0xA0); // 0xA0为从设备地址,0表示写操作
    Driver_I2C2_ReceiveACK();

    // 发送数据和接收应答信号
    Driver_I2C2_SendByte(0x12); // 0x12为要发送的数据
    Driver_I2C2_ReceiveACK();

    // 发送停止信号
    Driver_I2C2_Stop(); */

    // 死循环
    while (1)
        ;
}

4 使用IIC协议读写EEPROM 硬件方式实现 (寄存器)

STM32的 I2C 外设可用作通讯的主机及从机,支持100Kbit/s和400Kbit/s的速率,支持7位、10位设备地址,支持DMA数据传输,并具有数据校验功能。

它的I2C外设还支持 SMBus2.0协议,SMBus协议与I2C类似。

STM32的I2C外设的功能框图

I2C的所有硬件架构都是根据图中左侧SCL线和SDA线展开的(其中的SMBA线用于SMBUS的警告信号,I2C通讯没有使用)。STM32芯片有多个I2C外设,咱们现在用的这款有2个I2C外设,它们的I2C通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚。

4.1 IIC驱动代码
① I2C相关寄存器

寄存器名称

说明

相关控制位/标志位

CR1

控制寄存器1

ACK: 应答使能STOP: 停止信号产生START: 开始信号产生SMBus: 选择I2C模式(0)还是SMBus模式(1)PE: I2C模块使能

CR2

控制寄存器2

FREQ:I2C时钟频率,置范围2~36MHZ

DR

数据寄存器

高8位保留,低8位有效

SR1

状态寄存器1

TxE (Data register empty (transmitters: 数据寄存器为空(发送时) 0:数据寄存器非空; 1:数据寄存器空。RxNE (Data register not empty (receivers)) : 数据寄存器非空(接收时) 0:数据寄存器为空; 1:数据寄存器非空。BTF (Byte transfer finished) : 字节发送结束ADDR: 地址已发送SB (Start bit (Master mode)) : 起始位已发送

SR2

状态寄存器2

BUSY: 1表示总线忙,0表示总线无数据通信

CCR

时钟控制寄存器

F/S:I2C主模式选项 (I2 位15 C master mode selection) 0:标准模式的I2C; 1:快速模式的I2C。CCR[11:0]:快速/标准模式下的时钟控制分频系数(主模式) (Clock control register in Fast/Standard mode (Master mode))该分频系数用于设置主模式下的SCL时钟。在I2C标准模式或SMBus模式下:Thigh = CCR ×TPCLK1Tlow = CCR ×TPCLK1

TRISE

升沿寄存器

TRISE:的最大上升时间 ( 主模式 )

② 初始化流程
1. 时钟使能
    1.1 GPIOB 模块时钟使能
    1.2 I2C2 模块时钟使能
    
2. PB10、PB11 都设置为复用开漏输出

3. I2C 配置
    3.1 选择I2C模式或SMBus模式(CR1寄存器中的SMBUS控制位)
    3.2 选择为标准模式(0:标准模式; 1:快速模式)(CCR寄存器中的FS控制位)
    3.3 配置标准模式下的时钟控制分频系数(CCR寄存器中的n个CCR控制位)
    3.4 设置I2C的时钟频率, 最大36MHz(CR2寄存器中的FREQ控制位)
    3.5 设置允许的SCL的最大上升时间(TRISE寄存器,需要在PE=0的时候进行配置)
    3.6 I2C2模块使能(CR1寄存器中的PE控制位)
// 1. 时钟使能 ----------------------------------
    // 1.1 对 I2C2 模块的时钟使能
    RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
    // 1.2 对 GPIOB 时钟使能
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // 2. 配置 GPIO 引脚模式 ------------------------------
    // 配置PB10和PB11为复用开漏输出模式,MODE10、CNF10 都为 11,MODE11、CNF11 都为 11
    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_CNF10 | GPIO_CRH_MODE11 | GPIO_CRH_CNF11);

    // 3. 配置 I2C2 模块 ----------------------------------
    // 3.1 选择 SMBus 或者 I2C,设置成 I2C 模式
    I2C2->CR1 &= ~I2C_CR1_SMBUS;
    // 3.2 配置成I2C的标准模式
    I2C2->CCR &= ~I2C_CCR_FS;
    // 3.3 配置标准模式下的时钟控制分频系数
    I2C2->CCR &= ~I2C_CCR_CCR; // 将CCR对应的位全部置0
    I2C2->CCR |= 180;          // 将CCR对应的位设置成180(CCR是低12位)
    // 3.4 配置时钟频率,配置给I2C设备提供的时钟的频率 36MHz
    I2C2->CR2 &= ~I2C_CR2_FREQ; // 将FREQ对应的位全部置0
    I2C2->CR2 |= 36;            // 将FREQ对应的位设置成36(FREQ是低8位)
    // 3.5 配置标准模式下的最大上升时间 ( 主模式 )
    I2C2->TRISE = 37;
    // 3.6 I2C2使能
    I2C2->CR1 |= I2C_CR1_PE;
3.1 选择 SMBus 或者 I2C,设置成 I2C 模式(CR1)

I2C2->CR1 &= ~I2C_CR1_SMBUS;

3.2 配置成I2C的标准模式(CCR)

模式

标准模式: 100Kbit/s快速模式: 400kbit/s高速模式: 3.4Mbit/s

I2C2->CCR &= ~I2C_CCR_FS;

3.3 配置标准模式下的时钟控制分频系数(CCR)

I2C2->CCR &= ~I2C_CCR_CCR; // 将CCR对应的位全部置0I2C2->CCR |= 180; // 将CCR对应的位设置成180(CCR是低12位)

模式
标准模式: 100Kbit/s
快速模式: 400kbit/s
高速模式: 3.4Mbit/s
    
设置I2C的通讯速率为100KHz 通过计算为
        100KHz=100 000 次/s
        周期=1/频率
        一个周期时间=1/100 000 s=1000 000/100 000us=10us
        一个周期时间T=Tlow+Thigh
        Tlow为低电平的时间
        Tlow为高电平的时间
        Tlow=Thigh=1/T=5us
    
        Tlow = CCR ×TPCLK1
        Thigh = CCR ×TPCLK1
        Thight = 5us
        Tpclk1 = 1/36us
        CCR = 5 * 36 = 180
        
标准模式下的时钟控制分频系数:
标准模式速度: 100Kbit/s 
1 秒=1,000,000 μs
一个SCL周期:   1 / 100000 s
               =1,000,000 /100,000 us
               = 10us
                           
一个SCL周期内:T_high 和 T_low 各 5 us

T_TPCLK1 表示APB1每个时钟周期时间:
时钟频率=36MHZ
TPCLK1=1/时钟频率=1/36000000 s
=1,000,000/36,000,000 us
=1/36 us
                           
Thigh = CCR ×TPCLK1
Tlow = CCR ×TPCLK1                         
5 = CCR * 1/36
CCR=5*36=180
3.4 设置 I2C的时钟频率 : 36MHz (范围2-36)(CR2)

I2C2->CR2 &= ~I2C_CR2_FREQ; // 将FREQ对应的位全部置0 I2C2->CR2 |= 36; // 将FREQ对应的位设置成36(FREQ是低8位)

3.5 配置标准模式下的最大上升时间 ( 主模式 )(TRISE)

I2C2->TRISE = 37;

允许的 SCL 最大上升沿时间
            100KHz的时候要求最大上升沿不超过1us(手册)。
            时钟频率是36MHz则 写入:1 /(1/36) + 1 = 37
           其实就是计算的 最大上升沿时间/时钟周期 + 1
           
例如:标准模式中最大允许SCL上升时间为1000ns。如果在I2C_CR2寄存器中FREQ[5:0]中的
值等于0x08且TPCLK1=125ns,故TRISE[5:0]中必须写入09h(1000ns/125 ns = 8+1)。
1.已知标准模式中最大允许SCL上升时间为1000ns
1.计算T_TPCLK1:
APB1域的最大允许频率是36MHz
T_TPCLK1:APB1以ms为单位的时钟间隔(时钟周期)
时钟周期=1/时钟频率
T_TPCLK1    1/36000000 s
            = 1,000,000/36,000,000 us
            = 1/36 us
            = 1000 / 36 ns
2.计算TRISE的值:
公式:(标准模式中最大允许SCL上升时间/T_TPCLK1)+1
=1000ns / (1000 / 36)ns + 1    = 37
3.6 I2C2使能(CR1)

I2C2->CR1 |= I2C_CR1_PE;

③ 起始信号和停止信号

发送起始信号:

1. 将 CR1 寄存器中的 START 控制位置1 发送起始信号
2. 等待起始信号发送完成(SR1 寄存器中的 SB 标志位会置1)才结束函数
/**
 * @brief 发送起始信号
 *
 */
uint8_t Driver_I2C2_Start(void)
{
    // 1. 发送起始信号
    I2C2->CR1 |= I2C_CR1_START;

    // 2. 等待起始信号发送完成并引入超时机制
    uint16_t timeout = 0xFFFF;
    while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout)
    {
        timeout--;
    }

    // 返回结果
    return timeout ? I2C_OK : I2C_FAIL;
}

发送结束信号:

将 CR1 寄存器中的 STOP 控制位置1,发送结束信号

/**
 * @brief 发送停止信号
 *
 */
void Driver_I2C2_Stop(void)
{
    // 发送停止信号
    I2C2->CR1 |= I2C_CR1_STOP;
}
④ 发送一个数据
1 发从设备地址和读写标识
1. 将从设备地址和读写标识写入 DR 寄存器
2. 等待发送完成(SR1寄存器中ADDR标志位置1)并清除标志位
/**
 * @brief 发送从设备地址和读写标识
 *
 * @param addr 从设备地址和读写标识
 */
uint8_t Driver_I2C2_SendAddr(uint8_t addr)
{
    // 1. 发送从设备地址和读写标识
    I2C2->DR = addr;

    // 2. 等待地址发送完成并引入超时机制
    uint16_t timeout = 0xFFFF;
    while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout)
    {
        timeout--;
    }

    // 3. 如果正常发送成功就清除标志位
    if (timeout)
    {
        I2C2->SR2;
    }

    // 4. 返回结果
    return timeout ? I2C_OK : I2C_FAIL;
}
2 发一个字节数据
1. 等待DR寄存器为空(SR1寄存器中的TXE标志位置1)
2. 将数据写入DR寄存器,自动发送
3. 等待发送完成(SR1寄存器中BTF标志位置1)并清除标志位
/**
 * @brief 写入一个字节
 *
 * @param byte 要写入的字节
 */
uint8_t Driver_I2C2_SendByte(uint8_t byte)
{
    // 1. 等待数据寄存器空(标志着上一个字节已经放入移位数据寄存器)
    while ((I2C2->SR1 & I2C_SR1_TXE) == 0)
        ;

    // 2. 写入数据
    I2C2->DR = byte;

    // 3. 等待发送完成并添加超时机制
    uint16_t timeout = 0xFFFF;
    //BTF:字节发送结束 (Byte transfer finished) 
    while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout)
    {
        timeout--;
    }

    // 4. 如果正常发送完成,清除标志位
    if (timeout)
    {
        I2C2->DR;
    }

    // 5. 返回结果
    return timeout ? I2C_OK : I2C_FAIL;
}
⑤ 接收一个字节数据
接收一个字节
1. 等待字节数据接收完成(SR1寄存器中的 RXNE 标志位置1)
2. 读 DR 寄存器
/**
 * @brief 读取一个字节
 *
 * @return uint8_t 读取到的字节
 */
uint8_t Driver_I2C2_ReceiveByte(void)
{
    // 1. 等待接收数据寄存器非空(标志字节接收完成)
    while ((I2C2->SR1 & I2C_SR1_RXNE) == 0)
        ;

    // 2. 读取数据并返回
    return I2C2->DR;
}
发送应答信号

CR1 寄存器中的 ACK 控制位,置1表示发送ACK信号,置0表示不应答(NACK信号)

/**
 * @brief 发送应答信号
 *
 * @param ack 0表示ACK,1表示NACK
 */
void Driver_I2C2_SendACK(uint8_t ack)
{
    if (ack)
    {
        // 发送NACK信号
        I2C2->CR1 &= ~I2C_CR1_ACK;
    }
    else
    {
        // 发送ACK信号
        I2C2->CR1 |= I2C_CR1_ACK;
    }
}
⑥ 接收数据注意事项
1. 为了在收到最后一个字节后产生一个NACK脉冲,在读倒数第二个数据字节之后(在倒数第二个RxNE事件之后)必须清除ACK位。
2. 为了产生一个停止/重起始条件,软件必须在读倒数第二个数据字节之后(在倒数第二个
RxNE事件之后)设置STOP/START位。
⑦代码
与软件方式实现的区别

比软件方式实现时少了一个方法

uint8_t Driver_I2C2_ReceiveACK(void);

比软件方式实现时多了一个方法

uint8_t Driver_I2C2_SendAddr(uint8_t addr);

STM32的IIC外设会自动处理应答信号的接收,并通过中断或状态标志通知用户

Driver_I2C2.h
#ifndef __DRIVER_I2C2_H__
#define __DRIVER_I2C2_H__

#include <stdint.h>
#include "stm32f10x.h"

#define I2C_OK  0
#define I2C_FAIL  1

/**
 * @brief I2C2初始化
 * 
 */
void Driver_I2C2_Init(void);

/**
 * @brief 发送起始信号
 * 
 * @return 0 表示发送成功;1 表示发送失败
 */
uint8_t Driver_I2C2_Start(void);

/**
 * @brief 发送停止信号
 * 
 */
void Driver_I2C2_Stop(void);

/**
 * @brief 发送从设备地址和读写标识
 * 
 * @param addr 从设备地址和读写标识
 */
uint8_t Driver_I2C2_SendAddr(uint8_t addr);


/**
 * @brief 发送一个字节
 * 
 * @param byte 要写入的字节
 */
uint8_t Driver_I2C2_SendByte(uint8_t byte);

/**
 * @brief 读取一个字节
 *
 * @return uint8_t 读取到的字节
 */
uint8_t Driver_I2C2_ReceiveByte(void);


/**
 * @brief 发送应答信号
 *
 * @param ack 0表示ACK,1表示NACK
 */
void Driver_I2C2_SendACK(uint8_t ack);



#endif /* __DRIVER_I2C2_H__ */
Driver_I2C2.c
#include "Driver_I2C2.h"

/**
 * @brief I2C2初始化
 *
 */
void Driver_I2C2_Init(void)
{
    // 1. 时钟使能 ----------------------------------
    // 1.1 对 I2C2 模块的时钟使能
    RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
    // 1.2 对 GPIOB 时钟使能
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;

    // 2. 配置 GPIO 引脚模式 ------------------------------
    // 配置PB10和PB11为复用开漏输出模式,MODE10、CNF10 都为 11,MODE11、CNF11 都为 11
    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_CNF10 | GPIO_CRH_MODE11 | GPIO_CRH_CNF11);

    // 3. 配置 I2C2 模块 ----------------------------------
    // 3.1 选择 SMBus 或者 I2C,设置成 I2C 模式
    I2C2->CR1 &= ~I2C_CR1_SMBUS;
    // 3.2 配置成I2C的标准模式
    I2C2->CCR &= ~I2C_CCR_FS;
    // 3.3 配置标准模式下的时钟控制分频系数
    I2C2->CCR &= ~I2C_CCR_CCR; // 将CCR对应的位全部置0
    I2C2->CCR |= 180;          // 将CCR对应的位设置成180(CCR是低12位)
    // 3.4 配置时钟频率,设置为 36MHZ
    I2C2->CR2 &= ~I2C_CR2_FREQ; // 将FREQ对应的位全部置0
    I2C2->CR2 |= 36;            // 将FREQ对应的位设置成36(FREQ是低8位)
    // 3.5 配置标准模式下的最大上升时间 ( 主模式 )
    I2C2->TRISE = 37;
    // 3.6 I2C2使能
    I2C2->CR1 |= I2C_CR1_PE;
}

/**
 * @brief 发送起始信号
 *
 */
uint8_t Driver_I2C2_Start(void)
{
    // 1. 发送起始信号
    I2C2->CR1 |= I2C_CR1_START;

    // 2. 等待起始信号发送完成并引入超时机制
    uint16_t timeout = 0xFFFF;
    //SB:起始位(主模式) (Start bit (Master mode)) 
    //0:未发送起始条件;
    //1:起始条件已发送。
    while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout)
    {
        timeout--;
    }

    // 返回结果
    return timeout ? I2C_OK : I2C_FAIL;
}

/**
 * @brief 发送停止信号
 *
 */
void Driver_I2C2_Stop(void)
{
    // 发送停止信号
    I2C2->CR1 |= I2C_CR1_STOP;
}

/**
 * @brief 发送从设备地址和读写标识
 *
 * @param addr 从设备地址和读写标识
 */
uint8_t Driver_I2C2_SendAddr(uint8_t addr)
{
    // 1. 发送从设备地址和读写标识
    I2C2->DR = addr;

    // 2. 等待地址发送完成并引入超时机制
    uint16_t timeout = 0xFFFF;
    while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout)
    {
        timeout--;
    }

    // 3. 如果正常发送成功就清除标志位
    if (timeout)
    {
        I2C2->SR2;
    }

    // 4. 返回结果
    return timeout ? I2C_OK : I2C_FAIL;
}

/**
 * @brief 写入一个字节
 *
 * @param byte 要写入的字节
 */
uint8_t Driver_I2C2_SendByte(uint8_t byte)
{
    // 1. 等待数据寄存器空(标志着上一个字节已经放入移位数据寄存器)
    while ((I2C2->SR1 & I2C_SR1_TXE) == 0)
        ;

    // 2. 写入数据
    I2C2->DR = byte;

    // 3. 等待发送完成并添加超时机制
    uint16_t timeout = 0xFFFF;
    while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout)
    {
        timeout--;
    }

    // 4. 如果正常发送完成,清除标志位
    if (timeout)
    {
        I2C2->DR;
    }

    // 5. 返回结果
    return timeout ? I2C_OK : I2C_FAIL;
}

/**
 * @brief 读取一个字节
 *
 * @return uint8_t 读取到的字节
 */
uint8_t Driver_I2C2_ReceiveByte(void)
{
    // 1. 等待接收数据寄存器非空(标志字节接收完成)
    while ((I2C2->SR1 & I2C_SR1_RXNE) == 0)
        ;

    // 2. 读取数据并返回
    return I2C2->DR;
}

/**
 * @brief 发送应答信号
 *
 * @param ack 0表示ACK,1表示NACK
 */
void Driver_I2C2_SendACK(uint8_t ack)
{
    if (ack)
    {
        // 发送NACK信号
        I2C2->CR1 &= ~I2C_CR1_ACK;
    }
    else
    {
        // 发送ACK信号
        I2C2->CR1 |= I2C_CR1_ACK;
    }
}
4.2 EEPROM 部分代码
与软件实现时的区别

发送设备地址调用Driver_I2C2_SendAddr方法,发送内部地址和发送字节还是调用Driver_I2C2_SendByte方法

不需要接收应答信号

注意点:

读取EEPROM 数据时Hardware_EEPROM_Read

如果先接收数据再发送应答信号或停止信号如:

for (uint8_t i = 0; i < len; i++)
    {
        //接收数据
        data[i] = Driver_IIC2_Recive_Byte();
        if (i == len - 1)
        {
            //发送应答信号
            Driver_IIC2_Send_ACK(1);
            // 发送停止信号
            Driver_IIC2_Stop();
        }
        else
        {
            //发送应答信号
            Driver_IIC2_Send_ACK(0);
        }
    }

会出现以下异常情况

接收完第一个字符之后就会接收到一个NACK信号,导致后面的字符无法接收

原因:

发送应答信号代码

// 发送NACK信号 I2C2->CR1 &= ~I2C_CR1_ACK;

CR1寄存器的ACK只是一个使能位,并不是调用了立马就发送数据,而是表示

在接收到一个字节后返回一个应答(匹配的地址或数据)。

此操作必须在当前传输字节的ACK脉冲之前完成。

问题解决:接收数据之前发送应答使能和停止使能

for (uint8_t i = 0; i < len; i++)
    {
        if (i == len - 1)
        {
            //发送应答信号
            Driver_IIC2_Send_ACK(1);
            // 发送停止信号
            Driver_IIC2_Stop();
        }
        else
        {
            //发送应答信号
            Driver_IIC2_Send_ACK(0);
        }
        data[i] = Driver_IIC2_Recive_Byte();
    }

正确

Hardware_EEPROM.h
#ifndef __HARDWARE_EEPROM_H__
#define __HARDWARE_EEPROM_H__

#include <stdint.h>
#include "Common_Delay.h"
#include "Driver_I2C2.h"


#define DEV_ADDR_W 0xA0
#define DEV_ADDR_R 0xA1
#define PAGE_SIZE 16    // EEPROM每页大小

/**
 * @brief EEPROM写入数据
 * 
 * @param addr 写入地址
 * @param buf 数据
 * @param len 数据长度(字节个数)
 */
void Hardware_EEPROM_Write(uint8_t addr, uint8_t *bufs, uint16_t len);

/**
 * @brief EEPROM读取数据
 *
 * @param addr 读取地址
 * @param bufs 写读取到的数据写入该地址处
 * @param len 数据长度(字节个数)
 */
void Hardware_EEPROM_Read(uint8_t addr, uint8_t *bufs, uint16_t len);



#endif /* __HARDWARE_EEPROM_H__ */
Hardware_EEPROM.c
#include "Hardware_EEPROM.h"

// 向一个页内写入数据
static void Hardware_EEPROM_Write_Page(uint8_t addr, uint8_t *bufs, uint8_t len)
{
    // 发送起始信号
    Driver_I2C2_Start();

    // 发送从设备地址和写标识,并接收应答信号
    Driver_I2C2_SendAddr(DEV_ADDR_W); // 0xA0为从设备地址,0表示写操作

    // 发送EEPROM内部字节地址和接收应答信号
    Driver_I2C2_SendByte(addr);

    // 发送数据
    for (uint8_t i = 0; i < len; i++)
    {
        Driver_I2C2_SendByte(bufs[i]);
    }

    // 发送停止信号
    Driver_I2C2_Stop();

    // 延时,等待写周期
    Delay_ms(5);
}

/**
 * @brief EEPROM写入数据
 *
 * @param addr 写入地址
 * @param buf 数据
 * @param len 数据长度(字节个数)
 */
void Hardware_EEPROM_Write(uint8_t addr, uint8_t *bufs, uint16_t len)
{
    uint8_t page_remain = 0;

    while (1)
    {
        // 根据 addr 计算当前页的剩余长度
        page_remain = PAGE_SIZE - (addr % PAGE_SIZE);

        // 如果剩余长度大于等于要写入的数据长度,则直接写入
        if (page_remain >= len)
        {
            Hardware_EEPROM_Write_Page(addr, bufs, len);
            return;
        }
        else
        {
            Hardware_EEPROM_Write_Page(addr, bufs, page_remain); // 写入当前页剩余的数据
            len -= page_remain;                                  // 剩余数据长度减少
            bufs += page_remain;                                 // 数据指针移到下一页
            addr += page_remain;                                 // 地址指针移到下一页
        }
    }
}

/**
 * @brief EEPROM读取数据
 *
 * @param addr 读取地址
 * @param bufs 写读取到的数据写入该地址处
 * @param len 指定读取的数据长度(字节个数)
 */
void Hardware_EEPROM_Read(uint8_t addr, uint8_t *bufs, uint16_t len)
{
    // 第一大步 伪写 ---------------------------------------------------------
    // 发送起始信号
    Driver_I2C2_Start();

    // 发送从设备地址和写标识,并接收应答信号
    Driver_I2C2_SendAddr(DEV_ADDR_W); // 0xA0为从设备地址,0表示写操作

    // 发送EEPROM内部字节地址和接收应答信号
    Driver_I2C2_SendByte(addr);

    // 第二大步 正式读 -------------------------------------------------------
    // 发送起始信号
    Driver_I2C2_Start();

    // 发送从设备地址和读标识,并接收应答信号
    Driver_I2C2_SendAddr(DEV_ADDR_R); // 0xA0为从设备地址,1表示读操作

    // 读取多个字节数据
    for (uint8_t i = 0; i < len; i++)
    {

        // 发送应答信号
        if (i == len - 1)
        {
            Driver_I2C2_SendACK(1); // 最后一个字节,发送NACK      
            Driver_I2C2_Stop();     // 发送停止信号
        }
        else
        {
            Driver_I2C2_SendACK(0); // 非最后一个字节,发送ACK
        }

        // 接收一个字节
        bufs[i] = Driver_I2C2_ReceiveByte();
    }
}
main.c
#include <stdio.h>
#include "stm32f10x.h"
#include "Driver_USART1.h"
#include "Driver_I2C2.h"
#include "Hardware_EEPROM.h"

// 定义一个数组,用于存储读取的数据
uint8_t bufr[100];

// 主函数
int main()
{
    // 初始化USART1
    Driver_USART1_Init();

    // 初始化I2C2
    Driver_I2C2_Init();

    printf("硬件方式I2C通信(寄存器实现)功能演示:\n");

    // 写入数据 ---------------------------------------------
    Hardware_EEPROM_Write(0x10, "Happy New Year!", 15);

    // 读取数据 ---------------------------------------------
    // 从EEPROM读取数据
    Hardware_EEPROM_Read(0x10, bufr, 15);
    // 打印读取的数据
    printf("读取到的结果:%s\n", bufr);

    // 死循环
    while (1)
        ;
}

5 使用IIC协议读写EEPROM 硬件方式实现 (HAL库)

STM32CubeMx中配置

HAL_I2C_Mem_Write() :

/**
  * @brief  以阻塞模式向指定的内存地址写入数据
  * @param  hi2c 指向 I2C_HandleTypeDef 结构体的指针,包含指定 I2C 的配置信息
  * @param  DevAddress 目标设备地址:设备在数据手册中的 7 位地址值
  *         在调用该接口之前必须向左移一位
  * @param  MemAddress 内部存储器地址
  * @param  MemAddSize 内部存储器地址的大小
  * @param  pData 指向数据缓冲区的指针
  * @param  Size 要发送的数据量
  * @param  Timeout 超时时间
  * @retval HAL 状态
  */
HAL_StatusTypeDef HAL_I2C_Mem_Write(
    I2C_HandleTypeDef *hi2c, 
    uint16_t DevAddress, 
    uint16_t MemAddress, 
    uint16_t MemAddSize, 
    uint8_t *pData, 
    uint16_t Size, 
    uint32_t Timeout
)

HAL_I2C_Mem_Read() :

/**
  * @brief  以阻塞模式从指定的内存地址读取数据
  * @param  hi2c 指向 I2C_HandleTypeDef 结构体的指针,包含指定 I2C 的配置信息
  * @param  DevAddress 目标设备地址:设备在数据手册中的 7 位地址值
  *         在调用该接口之前必须向左移一位
  * @param  MemAddress 内部存储器地址
  * @param  MemAddSize 内部存储器地址的大小
  * @param  pData 指向数据缓冲区的指针
  * @param  Size 要读取的数据量
  * @param  Timeout 超时时间
  * @retval HAL 状态
  */
HAL_StatusTypeDef HAL_I2C_Mem_Read(
    I2C_HandleTypeDef *hi2c, 
    uint16_t DevAddress, 
    uint16_t MemAddress, 
    uint16_t MemAddSize, 
    uint8_t *pData, 
    uint16_t Size, 
    uint32_t Timeout
);
代码
eeprom.h
#ifndef __EEPROM_H__
#define __EEPROM_H__

#include <stdint.h>
#include "stm32f1xx.h"
#include "i2c.h"

#define EEPROM_ADDRESS 0xA0
#define EEPROM_PAGE_SIZE 16

/**
 * @brief EEPROM写入数据
 * 
 * @param addr 写入地址
 * @param buf 数据
 * @param len 数据长度(字节个数)
 */
void EEPROM_Write(uint8_t addr, uint8_t *bufs, uint16_t len);

/**
 * @brief EEPROM读取数据
 *
 * @param addr 读取地址
 * @param bufs 写读取到的数据写入该地址处
 * @param len 数据长度(字节个数)
 */
void EEPROM_Read(uint8_t addr, uint8_t *bufs, uint16_t len);


#endif /* __EEPROM_H__ */
eeprom.c
#include "eeprom.h"


// 向一个页内写入数据
static void EEPROM_Write_Page(uint8_t addr, uint8_t *bufs, uint8_t len)
{
   HAL_I2C_Mem_Write(&hi2c2, EEPROM_ADDRESS, addr, I2C_MEMADD_SIZE_8BIT, bufs, len, HAL_MAX_DELAY);
   HAL_Delay(5);   // 写周期
}


/**
 * @brief EEPROM写入数据
 * 
 * @param addr 写入地址
 * @param buf 数据
 * @param len 数据长度(字节个数)
 */
void EEPROM_Write(uint8_t addr, uint8_t *bufs, uint16_t len)
{
     uint8_t page_remain = 0;

    while (1)
    {
        // 根据 addr 计算当前页的剩余长度
        page_remain = EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE);

        // 如果剩余长度大于等于要写入的数据长度,则直接写入
        if (page_remain >= len)
        {
            EEPROM_Write_Page(addr, bufs, len);
            return;
        }
        else
        {
            EEPROM_Write_Page(addr, bufs, page_remain); // 写入当前页剩余的数据
            len -= page_remain;                                  // 剩余数据长度减少
            bufs += page_remain;                                 // 数据指针移到下一页
            addr += page_remain;                                 // 地址指针移到下一页
        }
    }
}

/**
 * @brief EEPROM读取数据
 *
 * @param addr 读取地址
 * @param bufs 写读取到的数据写入该地址处
 * @param len 数据长度(字节个数)
 */
void EEPROM_Read(uint8_t addr, uint8_t *bufs, uint16_t len)
{
    HAL_I2C_Mem_Read(&hi2c2, EEPROM_ADDRESS, addr, I2C_MEMADD_SIZE_8BIT, bufs, len, HAL_MAX_DELAY);
}
main.c
// 用于接收数据
uint8_t buf[100];

/**
 * @brief  The application entry point.
 * @retval int
 */
int main(void)
{

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
  /* Configure the system clock */
  SystemClock_Config();
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_I2C2_Init();

  printf("Hareware Mode I2C HAL: \n");

  // 向EEPROM写入数据
  EEPROM_Write(0x00, "Hello World and You!", 20);

  // 从EEPROM读取数据
  EEPROM_Read(0x00, buf, 20);
  printf("EEPROM Data: %s\n", buf);

  /* USER CODE BEGIN WHILE */
  while (1)
  {
  }
  /* USER CODE END 3 */
}

附录

寄存器名称

RCC: 复位和时钟寄存器,Reset and Clock Control

APB2 peripheral clock enable register (RCC_APB2ENR) APB2外围时钟使能寄存器

GPIOx: 通用输入输出x寄存器,General-Purpose Input/Output

Port configuration register high (GPIOx_CRH) (x=A..G) 端口配置寄存器高

AFIO: 复用功能输入输出寄存器,Alternate Function Input/Output

External interrupt configuration register 3 (AFIO_EXTICR3) 外部中断配置寄存器3

EXTI: 外部中断寄存器,External Interrupt

Interrupt mask register (EXTI_IMR)  中断屏蔽寄存器
Rising trigger selection register (EXTI_RTSR)  上升触发选择寄存器
Falling trigger selection register (EXTI_FTSR)  下降触发选择寄存器

STM32F10xxx Cortex-M3编程手册-英文版-PM0056-Rev6手册中寄存器

NVIC: 嵌套向量中断控制器, Nested Vectored Interrupt Controller

Interrupt set-enable registers (NVIC_ISERx)  中断使能寄存器 
Interrupt priority registers (NVIC_IPRx)    中断优先级寄存器

SCB:系统控制块 System control block

Application interrupt and reset control register (SCB_AIRCR) 中断及复位控制寄存器

名词缩写

AHB:    Advanced High-performance Bus (高性能总线)
APB:    Advanced Peripheral Bus Bus (高级外设总线)
FSMC:   Flexible Static Memory Controller (灵活静态存储器控制器)
DMA:    Direct Memory Access (直接存储访问)
HSI:    High Speed Internal oscillator (高速内部时钟)
HSE:    High Speed External Oscillator/Clock (高速外部时钟 )
PLL:    Phase Locked Loop (锁相环/倍频器)
LSI:    Low Speed Internal (低速内部时钟)
LSE:    Low Speed External oscillator (低速外部时钟)
HAL:    Hardware Abstraction Layer (硬件抽象层)
GPIO:   General-Purpose Input/Output (通用输入输出)
CMSIS:  Cortex Microcontroller Software Interface Standard (Cortex 微控制器软件接口标准 )
AFIO:   Alternate Function I/O (替代功能输入输出)
IrDA:   Infrared Data Association, 红外线数据协会
LIN:    Local Interconnect Network,本地互联网络 
SMBus   System Management Bus(系统管理总线)

单词

assistant   助理、助手
optimize    优化
tick        发出滴答声
mask        掩饰、伪装、屏蔽
rising      上升
falling     下降
external    外部的
nest        嵌套
vector      向量
interrupt   中断
priority    优先级

universal       普遍的、通用的
Synchronous     同步的
Asynchronous    异步的
parity          奇偶校验
Infrared        红外线
idle            闲置的

I²C 和 SMBus 对比

特性

SMBus

I²C

通信速率

10 kbps - 100 kbps

标准模式 100 kbps,快模式 400 kbps,高速模式最高可达 3.4 Mbps

超时机制

从设备响应时间不能超过 35 毫秒

无强制超时要求

电源故障保护

支持低功耗和断电情况下的系统状态保护

无电源管理机制

报警信号线

支持 SMBALERT#,从设备可以主动报警

无报警信号线,通信必须由主设备发起

数据包格式和校验

更严格的格式和 CRC 校验

格式灵活,无强制的 CRC 校验

电压要求

3.0V - 5V

1.8V - 5.5V,适应更多电压范围

应用场景

系统管理,如电源、温度、电池状态监控

通用设备间通信,如传感器、存储器

SMBus 是 I²C 协议的一种变种, 专注于可靠性和状态监控,适用于电源管理和设备健康监控;而 I²C 具有较大的灵活性,适合通用的数据传输需求。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐