【STM32零基础嵌入式入门实操】基于keil5、STM32CubeMX HAL库的——IIC(I2C)协议采集温湿度与OLED显示
本次实践围绕STM32F103C8T6与AHT20温湿度传感器的I2C通信展开,从协议理论到实战开发形成完整闭环,成功实现了每隔1秒采集温湿度数据并通过串口输出的核心需求。在理论层面,通过分层思想清晰拆解了I2C协议的物理层与协议层:物理层的“两线制+上拉电阻”设计简化了硬件连接,协议层的起始/停止信号、寻址机制、应答逻辑则保障了数据传输的准确性;同时明确了软件I2C与硬件I2C的差异,为实际开发
文章目录
前言
在嵌入式系统开发中,传感器数据采集是连接硬件与实际应用的核心环节,而I2C总线协议凭借其接口简单、占用引脚少的优势,成为短距离外设通信的主流选择,广泛应用于温湿度传感器、EEPROM、OLED显示屏等器件的互联。
本次实践以STM32F103C8T6最小系统板为核心,聚焦I2C协议的实战应用,目标是完成AHT20温湿度传感器的数据采集与串口输出。
AHT20作为一款高精度、低功耗的数字温湿度传感器,采用I2C接口进行数据交互,非常适合嵌入式场景下的环境监测需求。本次作业将围绕两大核心展开:一是深入理解“软件I2C”与“硬件I2C”的本质区别及实现逻辑,夯实协议理论基础;二是基于HAL库与STM32CubeMX,完成传感器初始化、每隔2秒采集温湿度数据,并通过串口将数据上传至Win10上位机的全流程开发。
通过本次实践,不仅能掌握I2C协议的通信时序、传感器数据解析方法,还能深化STM32外设配置与多模块(I2C+串口)协同开发的能力,为后续复杂嵌入式系统(如环境监测终端、智能家居节点)的设计积累关键经验。
以下I2C协议的解释参考了野火资料,想要进一步了解,可以跳转到链接:
一、了解I2C总线协议
1. 协议简介
I2C 通讯协议(Inter-Integrated Circuit)是由Phiilps公司开发的,由于它引脚少,硬件实现简单,可扩展性强, 不需要USART、CAN等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。
在计算机科学与嵌入式开发领域,多数复杂问题都能通过 “分层思想” 拆解简化。比如芯片设计中,会明确划分内核层与片上外设层,前者负责核心运算,后者承担外设控制,职责清晰;STM32 的标准库则更具代表性,它作为寄存器操作与用户应用代码之间的中间软件层,无需开发者直接操控寄存器,大幅降低了开发门槛。
对于通讯协议,分层思想同样是核心理解逻辑,最基础的划分便是物理层与协议层。物理层负责定义通讯系统的机械结构(如引脚布局)、电子特性(如电平标准、传输速率),核心作用是搭建 “物理通道”,保障原始数据能在导线等物理介质上稳定传输。协议层则聚焦 “逻辑规则”,统一收发双方的数据打包格式、解包规则,确保数据传递后能被正确解读。
简单类比:物理层好比约定 “用嘴巴说话” 还是 “用手势比划” 的交流方式,协议层则是约定 “说中文” 还是 “说英文” 的沟通语言,二者结合才能实现高效、准确的通讯。
2. 物理层
I2C 物理层主要定义了硬件连接方式和电气特性,核心是 “两根线实现多设备通信”:
- 线路组成:仅需两根双向信号线 ——SDA(数据线,传输数据)和 SCL(时钟线,同步数据传输节奏)。
- 连接方式:总线上所有设备(主设备和从设备)的 SDA、SCL 分别并联,支持 “一主多从” 或 “多主多从”(实际常用一主多从)。
- 电气特性:设备接口多为开漏输出,需通过外部上拉电阻将 SDA、SCL 拉到高电平(通常接 3.3V 或 5V),实现 “线与逻辑”(任意设备拉低总线,总线即低;所有设备释放,总线由上拉电阻拉高)。
I2C 通讯设备常用连接方式(引用野火资料中的图)
3. 协议层
I2C 协议层主要规定了数据传输的 “逻辑规则”,解决 “怎么传、传什么、如何确认” 的问题,核心包括以下关键机制:
- 通信发起与结束:主设备通过发送 “起始信号”(SCL 高电平时,SDA 从高拉低)启动通信;通过 “停止信号”(SCL 高电平时,SDA 从低拉高)结束通信。

- 数据有效性:
I2C使用SDA信号线来传输数据,使用SCL信号线进行数据同步。见图 数据有效性。 SDA数据线在SCL的每个时钟周期传输一位数据。传输时,SCL为高电平的时候SDA表示的数据有效,即此时的SDA为高电平时表示数据“1”, 为低电平时表示数据“0”。当SCL为低电平时,SDA的数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。
每次数据传输都以字节为单位,每次传输的字节数不受限制。 - 寻址机制:主设备先发送 “从设备地址”(7 位或 10 位,常用 7 位),后跟 1 位 “读写位”(0 表示写,1 表示读),指定要通信的从设备及操作方向。
- 数据传输格式:

每次传输 1 字节(8 位),字节后紧跟 1 位 “应答位(ACK)”—— 接收方需拉低 SDA 表示已收到,无应答则视为传输结束。
- 读写流程:写操作时,主设备连续发送数据;读操作时,从设备向主设备返回数据,主设备通过应答位控制是否继续读取。
4. 软件I2C与硬件I2C
在嵌入式开发中,“软件 I2C” 和 “硬件 I2C” 是实现 I2C 通信的两种方式,核心区别在于时序控制的实现主体
- 软件 I2C(bit-banging I2C)
通过通用 GPIO 引脚(任意可配置为输入输出的引脚),用软件代码模拟 I2C 协议的时序(起始信号、停止信号、数据收发、应答位等)。
- 实现方式:开发者手动编写代码,通过控制 GPIO 的高低电平变化,严格遵循 I2C 协议的时序要求(如 SCL 时钟线的高低电平持续时间、SDA 数据线在时钟信号下的跳变时机等)。例如:用HAL_GPIO_WritePin()函数控制 SCL 和 SDA 的电平,用HAL_Delay()或延时函数控制时序间隔。
- 特点:
- 灵活性高:可使用任意 GPIO 引脚,不受硬件 I2C 外设引脚限制;
- 可控性强:时序细节(如速率)可通过代码精确调整;
- 缺点:占用 CPU 资源(需持续执行软件延时和电平控制),通信速率较低(通常不超过 100kHz),且代码复杂度稍高(需严格模拟时序)。
- 硬件 I2C(peripheral I2C)
利用单片机内部集成的 I2C 硬件外设(如 STM32 的 I2C1、I2C2),通过配置外设寄存器(或 HAL 库函数),由硬件自动生成 I2C 协议时序。
- 实现方式:只需配置 I2C 外设的时钟频率、地址模式等参数(如通过 STM32CubeMX 配置 I2C 外设),然后调用库函数(如HAL_I2C_Master_Transmit()、HAL_I2C_Master_Receive())即可完成数据收发,时序(如起始 / 停止信号、应答位)由硬件自动处理。
- 特点:
效率高:硬件自动生成时序,不占用 CPU 资源,可同时处理其他任务;
速率快:支持更高的通信速率(如标准模式 100kHz、快速模式 400kHz);
缺点:引脚固定(受硬件 I2C 外设引脚限制),灵活性较低;若时序异常,调试难度稍高(需排查硬件配置)。
总结
简单说,软件 I2C 是 “用代码模拟电线的通断节奏”,硬件 I2C 是 “让专用电路自动控制节奏”。实际开发中,若引脚紧张或需灵活调整时序,可选软件 I2C;若追求效率和高速通信,优先用硬件 I2C。
二、I2C协议的温湿度传感器的数据采集(通过串口)
以下是基于 STM32F103C8T6 和 HAL 库实现 AHT20 温湿度采集的详细配置与代码实现,包含 STM32CubeMX 配置、硬件连接、核心代码及测试流程:
1. 硬件准备与连接
- 硬件清单
- STM32F103C8T6 最小系统板
- AHT20 (或UHT20) 温湿度传感器模块(自带 I2C 接口,通常已内置上拉电阻)
- USB-TTL模块(用于串口通信)
- 杜邦线若干
- 接线说明(硬件 I2C 为例,使用 I2C1)
| STM32 引脚 | AHT20 | 说明 |
|---|---|---|
| PB6 | SCL | I2C 时钟线(硬件 I2C1_SCL) |
| PB7 | SDA | I2C 数据线(硬件 I2C1_SDA) |
| 3.3V | VCC | 传感器供电(需 3.3V,不可接 5V) |
| GND | GND | 共地 |
| PA9 | TX(USB-TTL) | 串口发送(USART1_TX) |
| PA10 | RX(USB-TTL) | 串口接收(USART1_RX) |
2. STM32CubeMX 配置步骤
- 基础配置(RCC、SYS)
- RCC 配置:勾选HSE(外部高速时钟),配置时钟树为 72MHz(APB1 分频设为 2,I2C 时钟频率由 APB1 提供,最高 400kHz)。


- SYS 配置:Debug 选择Serial Wire(方便调试)。

- I2C 配置(硬件 I2C1)
- 外设选择:在 Pinout 视图中,找到 I2C1,将 PB6 配置为I2C1_SCL,PB7 配置为I2C1_SDA。
- 参数配置:
- 模式:I2C(默认)
- 时钟频率:100kHz(AHT20 支持标准模式,100kHz 足够)
- 地址模式:7-bit(AHT20 的 I2C 地址为 0x38,7 位)
- 其余保持默认(无需使能中断,采用轮询方式)。



- 串口配置(USART1)
- 模式:
Asynchronous(异步通信) - 参数:波特率
115200,无奇偶校验,1 停止位,无硬件流控。 - 引脚自动分配为 PA9(TX)、PA10(RX)。


- 生成工程
选择 IDE(如 MDK-ARM V5),勾选 “Generate peripheral initialization as a pair of .c/.h files per peripheral”,生成代码。


3. 配置keil5的代码
以下是基于 HAL 库的 AHT20 驱动整合代码:
- 宏定义与全局变量(main.c 中)
/* 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 <stdio.h>
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
// AHT20设备地址(7位地址左移1位,HAL库要求)
#define AHT20_ADDR 0x70 // 0x38 << 1
// 命令定义
#define AHT20_CMD_INIT 0xBE // 初始化命令
#define AHT20_CMD_MEASURE 0xAC // 测量命令
#define AHT20_CMD_RESET 0xBA // 软复位命令
// 状态位定义
#define AHT20_STATUS_BUSY 0x80 // 忙状态位
#define AHT20_STATUS_CAL 0x08 // 校准状态位
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
float humidity = 0.0f; // 湿度值(%)
float temperature = 0.0f; // 温度值(℃)
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
uint8_t AHT20_Init(void);
uint8_t AHT20_ReadData(void);
uint8_t AHT20_Reset(void);
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/**
* @brief AHT20软复位
* @retval 0:成功 其他:失败
*/
uint8_t AHT20_Reset(void) {
uint8_t cmd = AHT20_CMD_RESET;
HAL_StatusTypeDef status;
status = HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDR, &cmd, 1, 100);
HAL_Delay(20); // 复位后等待20ms
return (status == HAL_OK) ? 0 : 1;
}
/**
* @brief 初始化AHT20传感器
* @retval 0:成功 1:复位失败 2:初始化命令失败 3:未校准
*/
uint8_t AHT20_Init(void) {
uint8_t status;
uint8_t init_buf[3] = {AHT20_CMD_INIT, 0x08, 0x00}; // 初始化命令+参数
// 软复位
if (AHT20_Reset() != 0) {
return 1;
}
// 发送初始化命令
status = HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDR, init_buf, 3, 100);
if (status != HAL_OK) {
return 2;
}
HAL_Delay(10); // 等待初始化完成
// 检查校准状态
uint8_t status_reg;
if (HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDR, &status_reg, 1, 100) != HAL_OK) {
return 3;
}
if ((status_reg & AHT20_STATUS_CAL) == 0) {
return 3; // 未校准
}
return 0;
}
/**
* @brief 读取AHT20温湿度数据
* @retval 0:成功 1:测量命令失败 2:读取数据失败 3:数据无效
*/
uint8_t AHT20_ReadData(void) {
uint8_t measure_buf[3] = {AHT20_CMD_MEASURE, 0x33, 0x00}; // 测量命令+参数
uint8_t data[6]; // 接收缓冲区
uint32_t hum_raw, temp_raw;
uint8_t i;
// 发送测量命令
if (HAL_I2C_Master_Transmit(&hi2c1, AHT20_ADDR, measure_buf, 3, 100) != HAL_OK) {
return 1;
}
// 等待测量完成(最多等待100ms)
for (i = 0; i < 100; i++) {
HAL_Delay(1);
if (HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDR, data, 1, 10) == HAL_OK) {
if ((data[0] & AHT20_STATUS_BUSY) == 0) {
break; // 测量完成
}
}
}
if (i >= 100) {
return 3; // 测量超时
}
// 读取完整数据(6字节)
if (HAL_I2C_Master_Receive(&hi2c1, AHT20_ADDR, data, 6, 100) != HAL_OK) {
return 2;
}
// 解析湿度(20位数据)
hum_raw = ((uint32_t)data[1] << 12) | ((uint32_t)data[2] << 4) | (data[3] >> 4);
humidity = (hum_raw / (float)(1 << 20)) * 100.0f;
// 解析温度(20位数据)
temp_raw = ((uint32_t)(data[3] & 0x0F) << 16) | ((uint32_t)data[4] << 8) | data[5];
temperature = (temp_raw / (float)(1 << 20)) * 200.0f - 50.0f;
return 0;
}
// 重定向printf到USART1
int fputc(int ch, FILE *f) {
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
uint8_t init_status;
/* 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_I2C1_Init();
MX_USART1_UART_Init();
MX_I2C2_Init();
/* USER CODE BEGIN 2 */
printf("AHT20 Test Start...\r\n");
// 初始化AHT20
init_status = AHT20_Init();
if (init_status != 0) {
printf("AHT20 Init Failed! Error: %d\r\n", init_status);
} else {
printf("AHT20 Init Success!\r\n");
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if (init_status == 0) {
uint8_t read_status = AHT20_ReadData();
if (read_status == 0) {
printf("Humidity: %.2f %% Temperature: %.2f ℃\r\n", humidity, temperature);
} else {
printf("Read Data Failed! Error: %d\r\n", read_status);
}
}
HAL_Delay(1000); // 1秒读取一次
}
/* 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 */
注意:这部分代码需检查 printf 重定向是否生效(HAL 库关键)
代码中虽有 fputc 重定向,但 Keil 需开启「微库」才能支持 printf,否则函数无法调用:
打开 Keil 工程,点击菜单栏「Options for Target」(魔法棒图标)。
切换到「Target」标签,在「Code Generation」下勾选 Use MicroLIB(微库)。
点击「OK」,重新编译烧录。
原理:标准 C 库的 printf 依赖文件系统,嵌入式环境需用微库简化实现,否则重定向无效。
- 测试验证
- 编译烧录:将代码编译后,通过 ST-Link 或串口下载到 STM32F103C8T6。
- 上位机配置:打开串口助手(如 SSCOM),选择 USB-TTL 对应的 COM 口,波特率
115200,无奇偶校验。 - 现象观察:串口助手每隔 2 秒收到一行数据,格式为:
Humidity: 45.32 %,Temperature: 22.94 C
用手触摸 AHT20,温度和湿度值应随环境变化。
实验结果如下:

温湿度
总结
本次实践围绕STM32F103C8T6与AHT20温湿度传感器的I2C通信展开,从协议理论到实战开发形成完整闭环,成功实现了每隔1秒采集温湿度数据并通过串口输出的核心需求。
在理论层面,通过分层思想清晰拆解了I2C协议的物理层与协议层:物理层的“两线制+上拉电阻”设计简化了硬件连接,协议层的起始/停止信号、寻址机制、应答逻辑则保障了数据传输的准确性;同时明确了软件I2C与硬件I2C的差异,为实际开发中选择合适的通信方式提供了依据。
实战环节中,基于STM32CubeMX完成了高效配置——HSE时钟72MHz的精准设定、I2C1(100kHz标准模式)与USART1(115200波特率)的参数配置,大幅降低了底层开发复杂度。代码实现上,通过AHT20的软复位、初始化校准、测量命令发送与20位原始数据解析,精准计算出温湿度物理量;串口重定向功能的实现(需开启Keil微库)则确保了数据能实时上传至上位机。
测试结果表明,串口助手可稳定接收格式规范的温湿度数据,且数据随环境变化(如触摸传感器导致温度上升),验证了方案的可行性。本次实践不仅夯实了I2C协议与STM32外设配置的基础,更积累了多模块协同开发的经验,为后续拓展OLED显示、多传感器组网等复杂功能提供了可靠的技术支撑。
更多推荐



所有评论(0)