零基础STM32入门教程--12-I2C 串行通迅

专栏:STM32F103入门教程

环境:Keil‑MDK + STM32F103 标准库3.5版本

一、I2C

I2C 全称 Inter‑Integrated Circuit,集成电路总线,由 NXP(原飞利浦)在 1980 年代开发,只需要 SDA(数据线) 和 SCL(时钟线) 两根信号线即可完成通信。广泛用于单片机连接各类低速外设传感器、EEPROM、OLED、MPU6050、RTC 时钟芯片等。

I2C 的主要特点

两线制 :只有 SDA 和 SCL,节省引脚和 PCB 布线空间。

同步通信 :数据传输由主设备产生的 SCL 时钟同步,属于同步串行通信。

半双工 :SDA 一根线承担收发,某一时刻只能单向传输。

主从式通信 :每个通信周期由主设备发起,从设备被动响应;支持一主多从、多主机系统。

地址寻址:每个从设备有唯一地址,主设备通过发送从设备地址来选择通信对象;标准地址为 7 位,扩展地址为 10 位。

速率等级

标准模式 :100 kbit/s,兼容性最好,调试优先用这个。

快速模式 :400 kbit/s

I2C 不适合长距离走线,主要用于板内芯片之间通信。

核心通信机制

主从模式

主机 (Master) :一般是单片机,产生 SCL 时钟、发起通信。

从机 (Slave) :传感器、EEPROM 等外设,每个从机拥有唯一 7bit 设备地址;主机发送地址,选中总线上要对话的设备,其余设备忽略总线数据。

注意:代码中地址经常需要左移 1 位,最低位作为读 / 写控制位。

时序信号

起始信号 START :SCL 为高时,SDA 由高变低,通信开始。

停止信号 STOP :SCL 为高时,SDA 由低变高,结束通信。

应答无应答 ACK/NACK:每传输完 1 字节(8bit,高位先发),接收方在第 9 个时钟拉低 SDA = ACK 应答成功;保持高电平 = NACK,代表无应答、设备未响应。

通信流程

主机发送起始 → 发送从机地址 + 读写位 → 等待从机 ACK → 收发数据(每字节后等待 ACK) → 发送停止信号结束。

两种实现方式(单片机)

硬件 I2C :片上外设硬件控制器,由硬件产生时序,CPU 负担小。

软件模拟 I2C:普通 GPIO 引脚,软件模拟 SCL/SDA 时序。优点:兼容性极强,不受硬件外设 bug 影响;缺点占用 CPU 时间,不适合 RTOS 高抢占场景。

二、AT24C02

AT24C02 是I2C 接口的串行 EEPROM,非常常用的掉电非易失存储器,单片机项目用来保存参数、配置、掉电需要保存的数据(阈值、计数、设备地址等)。

02 代表容量:2Kbit = 256 字节,地址范围:0‑255。

1、基础参数

容量:2Kbit,256 Bytes

接口:标准 I2C,支持 100kHz /400kHz

供电:1.8V‑5.5V,3.3V、5V 单片机都可以直接用

写入寿命:10 万次擦写;数据保存时间>100 年

器件 I2C 从机地址:0b1010 A2 A1 A0

A2/A1/A0 是芯片硬件引脚,用来修改设备地址,一条 I2C 总线最多挂载 8 片 AT24C02。

引脚全部悬空 / 接 GND 时,7 位地址:0x50。

⚠️代码注意:I2C 发送时地址需要左移一位,所以写地址是0xA0,读地址0xA1。

2、引脚定义

A0、A1、A2:地址引脚,接 GND/VCC,修改芯片 I2C 设备地址。

SCL:I2C 时钟

SDA:I2C 数据

WP:写保护引脚

WP 接 GND:可以正常读写;

WP 接 VCC:写保护开启,只能读,不能写入,防止误擦除数据。

VCC:电源

GND:地

SDA、SCL 必须外接 上拉电阻;STM32F103 硬件 I2C 必须外部上拉。

3、读写逻辑

***字节写(单字节写入) ***

I2C 起始 → 发送器件写地址 (0xA0) → 发送芯片内部存储地址 (0~255) → 发送要写入的数据 → STOP 停止。

⚠️写完之后必须等待写入周期! AT24C02 内部烧录需要时间,典型 5ms。

写完立刻读,会读取失败。可以延时 5ms,或者轮询 I2C ACK 等待芯片内部烧写完成。

随机读(指定地址读)

I2C 起始,发送器件写地址 0xA0,发送要读取的内部地址;

重新发起起始,发送器件读地址 0xA1;

读取 1 字节数据,主机发送 NACK;

STOP 结束。

4、工程高频坑点(STM32F103 调试必看)

写完忘记等待 5ms 写周期,写完马上读,读出来乱码。

I2C 地址忘记左移,一直 NACK 无应答。AT24C02 7bit 地址 0x50,I2C 发送写地址要 0xA0。

WP 引脚接 VCC,开启写保护,写不进去,读正常。

STM32F103 硬件 I2C 本身存在卡死,也可以通过检查超时,复位I2C总线等方法或推荐软件模拟 I2C 驱动 AT24C02。

不要频繁循环擦写同一个地址,10 万次寿命,高频循环写会把芯片写坏。

5、典型使用场景

保存设备配置参数:阈值、波特率、设备编号;

6、同系列型号快速区分

AT24C01:128 字节

AT24C02:256 字节(最常用)

AT24C04:512 字节

AT24C08:1K

AT24C16:2K

三、接线

AT24C02 三个地址线A0,A1,A2全部接地。对应设备地址的bit1,bit2,bit3全为0。

SCL和SDA模块已经自带上拉电阻,否则要加接。

WP接到GND,数据才能写进去。

STM32F103C8T6 --PB6 <---------->AT24C02 ---SCL

STM32F103C8T6 --PB7 <---------->AT24C02 ---SDA

四、工程与程序编写

4.1工程复制

复制UART工程文件夹为I2C文件夹,UART.uvprojx复制为I2C.uvprojx。

4.2 添加驱动文件

1.Hardware文件夹下面新建I2c和At24c02两个文件夹。

2.工程添加i2c_sim.h,i2c_sim.c,at24c02.h,at24c02.c文件。

i2c_sim.h

c 复制代码
#ifndef __I2C_SIM_H
#define __I2C_SIM_H

#include "stm32f10x.h"

// 引脚定义 PB6 SCL  PB7 SDA
#define I2C_SCL_PIN     GPIO_Pin_6
#define I2C_SDA_PIN     GPIO_Pin_7
#define I2C_GPIO_PORT   GPIOB

void i2c_sim_init(void);
void i2c_sim_start(void);
void i2c_sim_stop(void);
uint8_t i2c_sim_write_byte(uint8_t dat);
uint8_t i2c_sim_read_byte(uint8_t ack);



#endif

i2c_sim.c

c 复制代码
#include "i2c_sim.h"
#include "delay.h"



/*
简单微延时,根据系统主频72MHz,微调延时保证I2C时序(100K)
Cortex-M3:for 循环每次迭代:
i 自增、比较跳转,一次循环大约 3~4 个 CPU 周期
循环 100 次:
周期数 ≈ 100 × 4 = 400 个 CPU 时钟周期
CPU 时钟 72MHz,1 个周期 = 1/72M ≈13.889ns
总时间 ≈ 400 ×13.889ns ≈ 5.55μs
不开编译优化(level 0),i2c_delay() 大概 5~6μs

标准 100kHz I2C:SCL 周期 10μs,高低电平最小建立 / 保持时间典型要求:
tSU;STA(起始建立) ≥4.7μs
tHD;STA(起始保持) ≥4.0μs
tLOW SCL 低电平 ≥4.7μs
tHIGH SCL 高电平 ≥4.0μs


*/
static void i2c_delay(void)
{
    uint32_t i;
    for(i=0;i<100;i++);
}

//初始化GPIO
void i2c_sim_init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;   //推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);

    //总线空闲 SCL=1 SDA=1
    GPIO_SetBits(I2C_GPIO_PORT,I2C_SCL_PIN);
    GPIO_SetBits(I2C_GPIO_PORT,I2C_SDA_PIN);
}

// I2C_SDA_PIN 设置为推挽输出
void sda_out(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitStruct.GPIO_Pin =  I2C_SDA_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;   //推挽输出
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);	
}

//I2C_SDA_PIN 浮空输入
void sda_in(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitStruct.GPIO_Pin =  I2C_SDA_PIN;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;   //浮空输入
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);	
}


//I2C起始信号:SCL高,SDA拉低
void i2c_sim_start(void)
{
    sda_out();
	GPIO_SetBits(I2C_GPIO_PORT,I2C_SDA_PIN);
    GPIO_SetBits(I2C_GPIO_PORT,I2C_SCL_PIN);//SDA=1,SCL=1 (总线空闲状态)
    i2c_delay();
    GPIO_ResetBits(I2C_GPIO_PORT,I2C_SDA_PIN);//SDA 拉低 → 完成起始信号边沿,告诉所有从机:通信要开始了
    i2c_delay();
    GPIO_ResetBits(I2C_GPIO_PORT,I2C_SCL_PIN);//把时钟线拉低,为主机输出第 1 位数据做准备
											  //***SCL 高的时候,SDA 不能随便变化,否则会误触发起始 / 停止***
}

//I2C停止信号:SCL高,SDA拉高
void i2c_sim_stop(void)
{
    sda_out();
	GPIO_ResetBits(I2C_GPIO_PORT,I2C_SDA_PIN); //SDA=0
    GPIO_SetBits(I2C_GPIO_PORT,I2C_SCL_PIN);   //SCL=1
    i2c_delay();
    GPIO_SetBits(I2C_GPIO_PORT,I2C_SDA_PIN);   //SDA=1 
    i2c_delay();
}

//写1字节,返回ACK(0=应答,1=NACK无应答)
uint8_t i2c_sim_write_byte(uint8_t dat)
{
    uint8_t i;
    uint8_t ack;
	sda_out();
    for(i=0;i<8;i++)//一个字节8位
    {
        GPIO_ResetBits(I2C_GPIO_PORT,I2C_SCL_PIN);//先拉低SCL,为后面的发送SDA的位数据作准备
        i2c_delay();
        if(dat & 0x80)//先发送高位
        {
            GPIO_SetBits(I2C_GPIO_PORT,I2C_SDA_PIN);//置高
        }
        else
        {
            GPIO_ResetBits(I2C_GPIO_PORT,I2C_SDA_PIN);//置低
        }
        dat <<=1;
        i2c_delay();
        GPIO_SetBits(I2C_GPIO_PORT,I2C_SCL_PIN);//SCL 拉高 SDA数据有效
        i2c_delay();
    }
    //释放SDA,读取应答:切输入!!
    GPIO_ResetBits(I2C_GPIO_PORT,I2C_SCL_PIN);//把时钟线拉到低电平,准备进入 ACK 阶段
	sda_in();   //重点!切输入读取从机应答  SCL 高电平期间不允许随便切换 SDA 输入输出
    i2c_delay();
    GPIO_SetBits(I2C_GPIO_PORT,I2C_SCL_PIN);  //SCL拉高,这就是ACK的第9个时钟!
    i2c_delay();

    if(GPIO_ReadInputDataBit(I2C_GPIO_PORT,I2C_SDA_PIN))//读取从机应答
        ack = 1;    //NACK
    else
        ack = 0;    //ACK

    GPIO_ResetBits(I2C_GPIO_PORT,I2C_SCL_PIN);//次拉低SCL,结束ACK时序
	sda_out();  //读完切回输出
    return ack;
}

/*

写 1bit:先输出 bit,再左移
读 1bit:先左移,再存入 bit
*/

//读1字节: ack=0 发送ACK继续读;ack=1发送NACK结束读取
uint8_t i2c_sim_read_byte(uint8_t ack)
{
    uint8_t i;
    uint8_t dat = 0;

    GPIO_ResetBits(I2C_GPIO_PORT,I2C_SCL_PIN);
    GPIO_SetBits(I2C_GPIO_PORT,I2C_SDA_PIN);
	sda_in();
	
    for(i=0;i<8;i++)//先读到的是高位
    {
        dat <<= 1;//左移1位,放在前面,读一个字节只需求移位7位,放在最后面时读bit0时就不能再移位了。
        GPIO_ResetBits(I2C_GPIO_PORT,I2C_SCL_PIN);
        i2c_delay();
        GPIO_SetBits(I2C_GPIO_PORT,I2C_SCL_PIN);
        i2c_delay();
        if(GPIO_ReadInputDataBit(I2C_GPIO_PORT,I2C_SDA_PIN))
        {
            dat |= 0x01;
        }
    }
	
    //准备发送应答位
    GPIO_ResetBits(I2C_GPIO_PORT,I2C_SCL_PIN);
	sda_out();
    if(ack)
    {
        GPIO_SetBits(I2C_GPIO_PORT,I2C_SDA_PIN); //NACK  不应答
    }
    else
    {
        GPIO_ResetBits(I2C_GPIO_PORT,I2C_SDA_PIN); //ACK
    }
    i2c_delay();
    GPIO_SetBits(I2C_GPIO_PORT,I2C_SCL_PIN);//第9个时钟,ACK/NACK 有效
    i2c_delay();
    GPIO_ResetBits(I2C_GPIO_PORT,I2C_SCL_PIN);

    return dat;
}


at24c02.h

```c
#ifndef __AT24C02_H
#define __AT24C02_H

#include "i2c_sim.h"
#include "delay.h"

/*
器件地址字节的第 8 位(最低 bit0)是读写选择位:
- 该位 = 1:发起读操作
- 该位 = 0:发起写操作
> I2C 标准:bit0 → R/W 位。
*/

#define AT24C02_ADDR_WR 0xA0   //1010 000 0
#define AT24C02_ADDR_RD 0xA1   //1010 000 1

void at24c02_write_byte(uint8_t addr,uint8_t data);
uint8_t at24c02_read_byte(uint8_t addr);

void at24c02_test(void);

#endif

at24c02.c

c 复制代码
#include "at24c02.h"
#include <stdio.h>

//写单个字节
void at24c02_write_byte(uint8_t addr,uint8_t data)
{
    uint8_t ack;
	i2c_sim_start();
    ack = i2c_sim_write_byte(AT24C02_ADDR_WR);
	if(ack){
		i2c_sim_stop();
		printf("at24c02_write_byte ack 0:%d ",ack);		
		return;
	}	
    ack = i2c_sim_write_byte(addr);
	if(ack){
		i2c_sim_stop();
		printf("at24c02_write_byte ack 1:%d ",ack);
		return;
	}
	
    ack = i2c_sim_write_byte(data);
	if(ack){
		i2c_sim_stop();
		printf("at24c02_write_byte ack 2:%d ",ack);
		return;
	}
    i2c_sim_stop();
    delay_ms(10);    //AT24C02内部烧录等待时间,必须!
}

//读单个字节
uint8_t at24c02_read_byte(uint8_t addr)
{
    uint8_t res;
    i2c_sim_start();					//起始信号
    i2c_sim_write_byte(AT24C02_ADDR_WR);//从机地址写数据
    i2c_sim_write_byte(addr);			//要读对应数据的地址
    i2c_sim_start();					//起始信号
    i2c_sim_write_byte(AT24C02_ADDR_RD);//从机地址读数据
    res = i2c_sim_read_byte(1); //NACK结束
    i2c_sim_stop();
    return res;
}

void at24c02_test(void)
{
	uint8_t i,data;	
	printf("写之前读 \r\n");
	for(i=0;i<10;i++){
		data = at24c02_read_byte(i);
		printf("add[%d]: %d  \r\n",i,data);
	}
	
	for(i=0;i<10;i++){
		data = i+10;
		at24c02_write_byte(i,data);
		printf("写入 add[%d]: %d  \r\n",i,data);
	}
	printf("写之后读 \r\n");
	for(i=0;i<10;i++){
		data = at24c02_read_byte(i);
		printf("add[%d]: %d  \r\n",i,data);
	}
	
	
}
AT24Cxx 型号和地址
型号 容量 页大小 地址位数 器件地址格式 (7位+R/W) A2 A1 A0 引脚作用 内存地址发送
AT24C01 128 Byte 8 Byte 8位 1010 A2 A1 A0 全部用于片选 1字节 0x00~0x7F
AT24C02 256 Byte 8 Byte 8位 1010 A2 A1 A0 全部用于片选 1字节 0x00~0xFF
AT24C04 512 Byte 16 Byte 9位 1010 A2 A1 P0 A2/A1 片选,A0=NC 1字节,P0=地址 bit8
AT24C08 1K Byte 16 Byte 10位 1010 A2 P1 P0 A2 片选,A1/A0=NC 1字节,P1/P0=地址 bit9/8
AT24C16 2K Byte 16 Byte 11位 1010 P2 P1 P0 全部 NC,内部做地址高位 1字节,P2/P1/P0=地址 bit10/9/8
AT24C02 随机读 1 字节流程
步骤 操作 发送方 说明
START 主机 SCL 高电平时,SDA 下降沿
发送设备地址 0xA0 主机 0xA0 = 0b1010 0000,R/W=0(写模式)
ACK 从机 AT24C02 拉低 SDA 应答
发送内存地址(Word Address) 主机 比如 0x20,告诉从机读哪个位置
ACK 从机 从机应答,地址已接收
Repeated START (Sr) 主机 不释放总线,直接重新开始
发送设备地址 0xA1 主机 0xA1 = 0b1010 0001,R/W=1(读模式)⚡
ACK 从机 从机准备发送数据
读取 1 字节数据 从机→主机 从机发送指定地址的数据,MSB 先
NACK 主机 主机不拉低SDA = 不再读了
STOP 主机 SCL 高电平时,SDA 上升沿,结束通信

AT24C02:2Kbit → 256 字节存储空间,存储地址范围 0x00 ~ 0xFF

AT24C02 器件地址:写地址0xA0,读地址0xA1(A2/A1/A0 全部接地的情况)

随机读:读取任意指定存储地址,不是顺序接着上一次读。

I2C 随机读核心关键点:

先「写寄存器地址」:发送起始 + EEPROM 写地址 + ACK + 要读的存储单元地址 + ACK

不发停止信号,发送重复起始信号 Repeated Start

发送 EEPROM 读地址,读取从机 ACK

读取 1 字节数据,读完主机输出 NACK

发送停止信号 Stop 结束通信

⚠重点:随机读不能发 Stop,再发 Start;必须用重复起始信号。如果发 Stop,EEPROM 会丢失内部地址指针。

AT24C02 随机写 1 字节流程
步骤 操作 发送方 说明
START 主机 SCL 高电平时,SDA 下降沿
发送设备地址 0xA0 主机 0xA0 = 0b1010 0000R/W = 0(写模式)
ACK 从机 AT24C02 拉低 SDA 应答
发送内存地址(Word Address) 主机 8 位地址,如 0x20,指定写入位置
ACK 从机 地址已接收
发送 1 字节数据 主机 要写入的数据,如 0x55MSB 先
ACK 从机 数据已接收
STOP 主机 SCL 高电平时,SDA 上升沿,结束通信

i2c_sim_start()  I2C 起始信号 Start

i2c_sim_write_byte(0xA0) 发送 EEPROM写器件地址

判断 ACK,如果 NACK,直接 Stop 退出,通信失败

i2c_sim_write_byte(mem_addr) 发送要写入的存储地址(0‑0xFF)

判断 ACK

i2c_sim_write_byte(write_data) 发送需要写入的数据字节

判断 ACK

i2c_sim_stop() 发送停止信号 Stop,Stop 之后 EEPROM 才启动内部页烧写

⏳延时 5~10ms,等待 EEPROM 内部编程完成!这一步非常容易漏掉!

重点:

Stop 信号才触发 EEPROM 烧写,没有 Stop 不会保存数据;

Stop 之后必须等待烧写时间,期间 EEPROM 不响应 I2C,此时去读 / 写会收到 NACK;

随机写不需要重复起始信号。

main.c

c 复制代码
#include "stm32f10x.h"  
#include "led.h"
#include "delay.h"
#include "key.h"
#include "usart.h"
#include "exti.h"
#include "time.h"
#include <stdio.h>
#include "i2c_sim.h"
#include "at24c02.h"

int main(void)
{		
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);// 设置中断优先级分组4
	led_init();
	delay_init();
	usart1_init(115200);
	i2c_sim_init();
	at24c02_test();
		
	while(1)
	{				
		
		
	}
	
}

4.3 添加文件包含路径

4. 4 程序的编译和烧录

测试结束写入和读出是相同才正确!

模拟I2C 程序错误也能读写,要读写测试判断.

资料下载

参考资料网盘 :

链接 : https://pan.baidu.com/s/1BrH0JbUY6z7j7gAihTa4Dw

提取码: qnws

本节课完整工程例程网盘:

链接: https://pan.baidu.com/s/1rl6EIqi9m6kQ1JFjuSqINg

提取码: bumc

相关推荐
Messy create1 小时前
【继续充值】
单片机
茯苓gao1 小时前
从零开发 EtherCAT 主站(六):SOEM 初始化流程详解,主站是如何发现所有从站的?
笔记·嵌入式硬件·学习·信息与通信
智购科技无人售货机工厂1 小时前
2026自动售货机防拆机物理安全设计:从安全螺丝到结构互锁的工程实践~YH
android·网络·驱动开发·python·单片机·安全·云原生
从零开始的嵌入式之旅2 小时前
day33
linux·c语言·经验分享·笔记·嵌入式硬件
jufeng13072 小时前
STM32F103 定时器输入捕获实战:测频测周、编码器测速、红外解码全流程拆解
stm32·输入捕获·定时器·编码器
RedJACK~2 小时前
Windows11一级右键菜单添加Alacritty教程
单片机·嵌入式硬件
智闲电子设计2 小时前
STM32 DAC 实战:用 DMA 把定时器表变成波形,自制信号发生器
stm32·单片机·嵌入式硬件
zcmodeltech2 小时前
反应装置模型控制系统设计与实现:多设备协同联动方案
java·网络·数据库·stm32·嵌入式硬件·能源·制造
深圳市恒锐丰科技杨生2 小时前
CL4054HL 36V 耐压 600mA 单节锂电池线性充电芯片|SOT23‑5 小型便携充电方案
嵌入式硬件·硬件工程