【STM32】HAL库中的实现(八):I2C通信(以 AT24C02 为例)
本系列文章将HAL库与STM32CubeMX结合在一起讲解,并在此之前提供了基于标准库底层原理讲解,使您基于标准库完全理解各个模块的详细底层实现原理,也可以基于HAL库更快速的学会各个模块的使用方法。
资料参考:
STM32 I2C HAL 官方文档
AT24C02 Datasheet
以及结合本文提供的示例工程:我为你准备好的(可生成) CubeMX + Keil 工程模板
在之前的标准库中,STM32的硬件IIC非常复杂,而且它并不稳定,所以都不推荐使用。
但是在当前的HAL库中,对硬件IIC做了全新的优化,使得之前软件IIC几百行代码,在HAL库中,只需要寥寥几行就可以完成。
通过本篇博客您可以学到:
- IIC的基本原理讲解
- STM32CubeMX创建IIC例程(CubeMX 配置)
- 硬件连接
- HAL库IIC函数库(HAL 代码实现(含读写操作))
- AT24C02 芯片原理
- 调试与注意事项
工具条件:
- 芯片: STM32F103ZET6
- STM32CubeMx软件
- IDE: MDK-Keil软件
- STM32F1xx/STM32F4xxHAL库
- IIC: 使用硬件IIC1
- 原理图 和 芯片手册说明书
I2C的原理之前有写过,这里仅做HAL库的应用,如果需要复习请移步至:【STM32】IIC通信(提供完整实例代码)
IIC例程(由于此前的文章已经完整介绍了IIC的原理,这里借用HAL库实现IIC,我主要讲述的重点是编程代码的实现思路),如果想要更进一步理解IIC的详情,也可以移步至下面提供的博客转载链接之中,很多大佬们都已经写的非常专业了,此处无需重复。正好借此机会,我向大家介绍一下编程中隐性的编程思维。在你未来的工作中可以事半功倍,提升效率。
首先,用别人的代码不要直接拷贝,先看懂他实现了什么,代码的实现逻辑。我们一般在用别人代码的时候,先只是大概的看懂他做了什么,但拷贝代码进入自己的项目中时,我们还需要更进一步确定代码详细的实现逻辑。(如下图所示)
说的极端一点,你根本就不需要知道这个代码的内部具体是怎么实现的(但是必须能看懂),这种东西有很多很多,你只需要去移植别人写好的代码实现功能就可以了。 这篇文章正好提供了I2C的示例代码,你可以移植我的,也可以移植别人的,这些资源在网上到处都是,但你仍然需要看懂代码的内部逻辑和它具体实现了什么功能、是如何实现的。
同理,在实现其他的需求和功能、函数调用的过程,也与这里的IIC实现同理。
这里的HAL库实现IIC很简单,在CubeMX上把配置好底层,然后在网上找一篇写得比较好的代码或者问问AI,然后用别人已经写好的代码例程测试一下,这样IIC(别的知识点也同理)就能跑起来了。如果说你有自己的需求实现和别人的代码逻辑不一样,先把别人的代码跑起来,然后把他的逻辑看明白,再改成自己的逻辑跑一遍就完成了IIC部分对应的功能。重要的是这其中蕴藏的编程思路是最有价值的。
另外,收集资源不可以乱收集,分门别类的框架规整清楚。代码逻辑完善,搭建系统性思维框架。这样,学完了一圈之后,你之后做项目的时候需要什么,要用到什么的时候,把之前学过的瞟一眼拿过来直接用上即可,可以极大的提升工作效率。编程切忌不要陷入技术的死局。
好了,我们继续,这篇文章的IIC实现是从 STM32CubeMX 配置 → I2C外设初始化 → I2C读写 EEPROM → 串口输出验证 的全流程。
一、项目目标
使用 STM32 的 硬件 I2C 接口,通过 HAL 库驱动,读写 EEPROM(AT24C02 / AT24C04),并通过串口打印数据。
二、整体框架结构
STM32 I2C1 (PB6 PB7)↓
AT24C02 EEPROM(0xA0/0xA1)↓
串口 USART1 → 上位机串口助手
三、硬件连接说明看原理图)
首先在原理图上找到芯片的I2C引脚,及其控制的模块(此处为AT24C04 )
硬件连接说明:
AT24C02 引脚 说明 连接
VCC 电源 3.3V
GND 地 GND
SDA 数据线 STM32 → PB7(I2C1_SDA)
SCL 时钟线 STM32 → PB6(I2C1_SCL)
WP 写保护 GND(允许写入)
A0~A2 器件地址设置 接地(默认地址 0xA0/0xA1)注:✅ SDA/SCL 需上拉电阻(一般10kΩ)
☆ 扩展知识
(如果你自己在编程过程中遇到了不懂的内容,需要自己去查阅 补充知识储备 即学即用):
AT24C04 模块是一种常见的 I²C接口的 EEPROM(电可擦除可编程只读存储器),主要用于在嵌入式系统中存储小容量的非易失性数据。它可以用来保存配置参数、设置、用户数据等信息,即使在断电后数据也不会丢失。
24C02是一个2K Bit的串行EEPROM存储器(掉电不丢失),内部含有256个字节。在24C02里面有一个8字节的页写缓冲器。
- A0,A1,A2:硬件地址引脚
- WP:写保护引脚,接高电平只读,接地允许读和写
- SCL和SDA:IIC总线
可以通过存储IC的型号来计算芯片的存储容量是多大,比如24C02后面的02表示的是可存储2Kbit的数据,转换为字节的存储量为21024/8 = 256byte;那么24C04后面的04表示的是可存储4Kbit的数据,转换为字节的储存量为41024/8 = 512byte;以此来类推其它型号的存储空间。
AT24C04模块的主要作用包括:
数据存储:
AT24C04 模块可以用来存储配置参数、设置、用户数据等,这些数据在设备断电后仍然需要保存。
掉电数据保护:
由于EEPROM的特性,AT24C04模块可以在断电的情况下保持数据,这对于需要保存配置信息或状态数据的应用非常重要。
I²C通信:
AT24C04 模块使用I²C 接口与主控制器进行通信,这是一种简单易用的串行通信协议,适合于在嵌入式系统中使用。
容量适中:
AT24C04 模块提供4Kb 的存储容量,对于一些只需要少量数据存储的应用来说足够。
易于使用:
AT24C04 模块通常采用8 脚封装,使用方便,易于在电路板上进行集成。
AT24C04模块在嵌入式系统中扮演着数据存储和掉电保护的重要角色,确保重要数据在设备断电后能够被保留,并且可以通过I²C接口方便地进行访问和管理。
如果您希望了解更多此模块的具体信息,可以查阅资料 (自整理,此处链接皆为转载!尊重原作者,侵删!)
AT24C02 E2PROM芯片详解
利用AT24C02进行数据存储
AT24C02存储器的基本原理与应用
关于E2PROM
⚙️ 四、CubeMX 配置要点(见图)
- RCC 设置(设置高速外部时钟HSE 选择外部时钟源)
- 开启 HSE 外部晶振(8MHz)
- PLL ×9 → SYSCLK = 72MHz
- APB1 = 36MHz(I2C 属于 APB1)
时钟源设置:
我的配置是 外部晶振为8MHz
- 选择外部时钟HSE 8MHz
- PLL锁相环倍频9倍
- 系统时钟来源选择为PLL
- 设置APB1分频器为 / 2
- 使能CSS监视时钟
32的时钟树框图不懂的话可以移步到: 《【STM32】系统时钟RCC详解(超详细,超全面)》 (转载,侵删!)
- IIC设置: I2C1 设置为I2C: 因为我们的硬件IIC 芯片一般都是主设备,也就是一般情况设置主模式即可)
项目设置:
模式:Master features 主模式特性
SDA / SCL:PB7 / PB6
IIC模式设置:I2C Speed Mode:快速模式和标准模式。实际上也就是速率的选择。
时钟频率: I2C Clock Speed:I2C传输速率,默认为100KHz
地址长度:7位( Primary Address Length selection: 从设备地址长度 设置从设备的地址是7bit还是10bit 大部分为7bit)—— Dual Address Acknowledged: 双地址确认
从设备初始地址:Primary slave address: 保持默认
扩展模式:Clock No Stretch Mode: 时钟没有扩展模式
IIC时钟拉伸(Clock stretching):默认启用(推荐)(clock stretching
通过将SCL线拉低来暂停一个传输。直到释放SCL线为高电平,传输才继续进行。clock stretching
是可选的,实际上大多数从设备不包括SCL驱动,所以它们不能stretch
时钟。)
- 串口 USART1 设置( 因为我们需要将AT24C02中存储的数据发送到上位机上,所以需要设置下串口)
项目设置:
模式:异步
波特率:115200
TX / RX:PA9 / PA10
配置好了之后,然后点击GENERATE CODE 创建工程。
五、I2C HAL 库函数说明
在i2c.c
文件中可以看到IIC初始化函数。在stm32f1xx_hal_i2c.h
头文件中可以看到I2C的操作函数。分别对应轮询,中断和DMA三种控制方式。 文件中的函数看起来多,但是只是发送和接收的方式改变了,函数的参数和本质功能并没有改变。比方说,IIC发送函数 还是发送函数,只不过有普通发送,DMA传输,中断 的几种发送模式。
这里我们使用的是普通发送,用其他的方式发送接收数据只是改下函数名即可。
- 写寄存器(带子地址)
HAL_I2C_Mem_Write(&hi2c1, DevAddr, MemAddr, I2C_MEMADD_SIZE_8BIT, data, size, timeout);
功能:IIC写数据
参数 | 含义 |
---|---|
DevAddr | I2C设备地址,如 0xA0 |
MemAddr | EEPROM 内部存储地址 |
data | 写入数据指针 |
size | 写入字节数 |
timeout | 超时时间(单位 ms) |
- 读寄存器(带子地址)
HAL_I2C_Mem_Read(&hi2c1, DevAddr, MemAddr, I2C_MEMADD_SIZE_8BIT, data, size, timeout);
功能:IIC读一个字节
参数 | 含义 |
---|---|
*hi2c | 设置使用的是那个IIC 例:&hi2c2 |
DevAddress | 写入的地址 设置写入数据的地址 例 0xA0 |
*pData | 需要写入的数据 |
Size | 要发送的字节数 |
Timeout | 最大传输时间,超过传输时间将自动退出传输函数 |
Timeout: 最大读取时间,超过时间将自动退出读取函数
📜 六、主程序代码逻辑(main.c
)
✅ 定义区
#define AT24C02_ADDR 0xA0
#define BufferSize 256uint8_t WriteBuffer[BufferSize];
uint8_t ReadBuffer[BufferSize];
✅ 按页写入 EEPROM(每页 8 字节)
for (int i = 0; i < BufferSize; i++) {WriteBuffer[i] = i;
}for (int j = 0; j < 32; j++) {HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR, 8 * j, I2C_MEMADD_SIZE_8BIT, &WriteBuffer[8 * j], 8, 1000);HAL_Delay(10); // 写后必须延时,确保写周期完成
}
- EEPROM 写入后有内部写周期(大约 5~10ms),必须延时。
- 一页写入不能超过 8 字节(AT24C02 结构限制)。
✅ 3. 一次性读取全部数据
HAL_I2C_Mem_Read(&hi2c1, 0xA1, 0, I2C_MEMADD_SIZE_8BIT, ReadBuffer, 256, 1000);
- 读取设备地址为
0xA1
(0xA0 + 读位)
✅ 4. 打印数据到串口
for (int i = 0; i < BufferSize; i++) {printf("0x%02X ", ReadBuffer[i]);if ((i + 1) % 16 == 0)printf("\r\n");
}
📷 下图中串口调试助手成功显示从 EEPROM 读出 0~255 的数据。
理想中的数据打印(效果):
0x00 0x01 0x02 ... 0x0F
0x10 0x11 0x12 ... 0x1F
...
0xF0 0xF1 ... 0xFF数据如此输出打印,说明数据写入 & 读取成功
注意事项:AT24C02的IIC每次写之后要延时一段时间才能继续写 每次写之后要delay 5ms左右 不管硬件IIC采用何种形式(DMA,IT),都要确保两次写入的间隔大于5ms;读写函数最后一个超时调整为1000以上 因为我们一次写8个字节,延时要久一点AT24C02页写入只支持8个byte,所以需要分32次写入。这不是HAL库的bug,而是AT24C02的限制,其他的EEPROM可以支持更多byte的写入。注意读取AT24C02数据的时候延时也要久一点,否则会造成读的数据不完整
当然,你也可以每次写一个字节,分成256次写入,也是可以的↓
// wrinte date to EEPROM 如果要一次写一个字节,写256次,用这里的代码for(i=0;i<BufferSize;i++) { HAL_I2C_Mem_Write(&hi2c1, ADDR_24LCxx_Write, i, I2C_MEMADD_SIZE_8BIT,&WriteBuffer[i],1,0xff);//使用I2C块读,出错。因此采用此种方式,逐个单字节写入 HAL_Delay(5);//此处延时必加,与AT24C02写时序有关 } printf("\r\n EEPROM 24C02 Write Test OK \r\n");
完整代码示例
/* USER CODE BEGIN Header */
/********************************************************************************* @file : main.c* @brief : Main program body******************************************************************************* @attention** Copyright (c) 2025 STMicroelectronics.* All rights reserved.** This software is licensed under terms that can be found in the LICENSE file* in the root directory of this software component.* If no LICENSE file comes with this software, it is provided AS-IS.********************************************************************************/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "string.h"
/* USER CODE END Includes *//* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD *//* USER CODE END PTD *//* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD *//* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM *//* USER CODE END PM *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */
#include <string.h>#define ADDR_24LCxx_Write 0xA0
#define ADDR_24LCxx_Read 0xA1
#define BufferSize 256uint8_t WriteBuffer[BufferSize],ReadBuffer[BufferSize];
uint16_t i;/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP *//* USER CODE END PFP *//* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 *//* USER CODE END 0 *//*** @brief The application entry point.* @retval int*/
int main(void)
{/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* USER CODE BEGIN Init *//* USER CODE END Init *//* Configure the system clock */SystemClock_Config();/* USER CODE BEGIN SysInit *//* USER CODE END SysInit *//* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();MX_I2C1_Init();/* USER CODE BEGIN 2 */HAL_UARTEx_ReceiveToIdle_IT( &huart1 , U1RxData, U1RxDataSize);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){for(i=0; i<256; i++)WriteBuffer[i]=i; /* WriteBuffer init */printf("\r\n***************I2C Example Z小旋测试*******************************\r\n");for (int j=0; j<32; j++){ if(HAL_I2C_Mem_Write(&hi2c1, ADDR_24LCxx_Write, 8*j, I2C_MEMADD_SIZE_8BIT,WriteBuffer+8*j,8, 1000) == HAL_OK){ printf("\r\n EEPROM 24C02 Write Test OK \r\n");HAL_Delay(20);}else{ HAL_Delay(20);printf("\r\n EEPROM 24C02 Write Test False \r\n");}}/* // wrinte date to EEPROM 如果要一次写一个字节,写256次,用这里的代码 for(i=0;i<BufferSize;i++) { HAL_I2C_Mem_Write(&hi2c1, ADDR_24LCxx_Write, i, I2C_MEMADD_SIZE_8BIT,&WriteBuffer[i],1,0xff);//使用I2C块读,出错。因此采用此种方式,逐个单字节写入 HAL_Delay(5);//此处延时必加,与AT24C02写时序有关 } printf("\r\n EEPROM 24C02 Write Test OK \r\n"); */HAL_I2C_Mem_Read(&hi2c1, ADDR_24LCxx_Read, 0, I2C_MEMADD_SIZE_8BIT,ReadBuffer,BufferSize, 0xff);for(i=0; i<256; i++)printf("0x%02X ",ReadBuffer[i]);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}/*** @brief System Clock Configuration* @retval None*/
void SystemClock_Config(void)
{RCC_OscInitTypeDef RCC_OscInitStruct = {0};RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};/** Initializes the RCC Oscillators according to the specified parameters* in the RCC_OscInitTypeDef structure.*/RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;RCC_OscInitStruct.HSEState = RCC_HSE_ON;RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;RCC_OscInitStruct.HSIState = RCC_HSI_ON;RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK){Error_Handler();}/** Initializes the CPU, AHB and APB buses clocks*/RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK){Error_Handler();}
}/* USER CODE BEGIN 4 *//* USER CODE END 4 *//*** @brief This function is executed in case of error occurrence.* @retval None*/
void Error_Handler(void)
{/* USER CODE BEGIN Error_Handler_Debug *//* User can add his own implementation to report the HAL error return state */__disable_irq();while (1){}/* USER CODE END Error_Handler_Debug */
}#ifdef USE_FULL_ASSERT
/*** @brief Reports the name of the source file and the source line number* where the assert_param error has occurred.* @param file: pointer to the source file name* @param line: assert_param error line source number* @retval None*/
void assert_failed(uint8_t *file, uint32_t line)
{/* USER CODE BEGIN 6 *//* User can add his own implementation to report the file name and line number,ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) *//* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
串口打印测试:
七、出现问题调试代码
可能出现的问题 | 解决方法 |
---|---|
写入失败 | 检查延时(HAL_Delay(5~10))是否足够 |
数据错乱 | 写入页大小超过8字节? |
I2C 无响应 | 看是否接好上拉电阻 |
地址不对 | 注意设备地址是否左移过(HAL会自动处理) |
波特率不匹配 | 串口助手与 MCU 设置一致 |
综上所述,HAL I2C 优点显而易见:简洁、稳定、可扩展、代码量小,IIC通信的应用场景也很多,常用于EEPROM、传感器、OLED、RTC 等,EEPROM可存储掉电保存数据,不过要注意写入的时候(HAL_I2C_Mem_Write() / HAL_I2C_Mem_Read()
),每页最多 8 字节,需延时写周期;写完代码后编译烧录代码,使用串口输出 + 示波器分析 SDA/SCL查看代码实现的效果。
IIC的更多应用场景
应用 | 技术点 |
---|---|
OLED 显示 EEPROM 内容 | I2C 驱动 SSD1306 |
多 EEPROM 管理 | 改变 A0~A2 地址 |
EEPROM 写保护 | 控制 WP 引脚 |
EEPROM 数据加密 | 加密写入,验证读取 |
EEPROM + 实时时钟 | 保存掉电时间记录 |
HAL 库下的 I2C 通信,配合 CubeMX 简化了硬件 I2C 的使用,稳定性大幅提升,在工业控制中应用广泛。