🚀 我的STM32学习专栏:[点击直达 | 持续更新中]

⚠️ 时效性提示:本文基于 STM32F103C8T6 + 标准库(STM32F10x_StdPeriph_Lib_V3.5.0)编写,适用于 Keil5 开发环境。若你使用 HAL 库或 STM32CubeMX,配置逻辑相通但 API 接口不同,请注意区分。


一、前言

前面我们学了软件模拟 I2C,用 GPIO 手动翻转电平来模拟时序。当时我就有个疑问:既然软件 I2C 能用,为什么还要搞硬件 I2C?

答案很简单——硬件 I2C 是 STM32 内部专门的硬件外设,它自己帮我们生成起始信号、发送字节、检测应答、处理时钟同步,CPU 只需要配置好参数,然后等着硬件把活干完就行。相比软件 I2C,硬件 I2C 的时序更标准、更稳定,而且不占用 CPU 时间(配合中断或 DMA 效果更佳)。

这节课我们就来把软件 I2C 替换成硬件 I2C,目标还是驱动 MPU6050。整个替换过程可以概括为三步:

  1. 开启 I2C 外设时钟,初始化 GPIO 为复用开漏模式

  2. 用结构体配置 I2C 参数,使能外设

  3. 用硬件 I2C 的 EV 事件标志位,重写读写时序函数

下面一步步来拆解。


二、知识点讲解

2.1 硬件 I2C 的引脚不能随便选!

这是软件 I2C 和硬件 I2C 最大的区别

软件 I2C 的 SCL 和 SDA 可以接任意 GPIO,因为是我们自己用代码控制电平翻转。但硬件 I2C 的引脚是芯片内部固定映射的,必须查数据手册里的复用功能表。

以 STM32F103C8T6 为例:

I2C 外设 SCL 引脚 SDA 引脚 所在总线
I2C1 PB6 PB7 APB1
I2C2 PB10 PB11 APB1

江科大课程里,PB6、PB7 被 OLED 占用了,所以 MPU6050 接的是 PB10(SCL)和 PB11(SDA),对应 I2C2

⚠️这一点一定要记牢!不是你想接哪就接哪。


2.2 GPIO 为什么要配成"复用开漏"?

I2C 协议要求总线是线与逻辑——也就是说,只有当所有设备都输出高电平时,总线才是高电平;只要有一个设备拉低,总线就是低电平。

开漏输出的特点是:输出高电平时实际上是高阻态,电平由外部上拉电阻决定;输出低电平时直接接地。这就天然支持了"线与"。

复用(Alternate Function) 的意思是:把引脚的控制权交给内部的 I2C 硬件外设,而不是普通的 GPIO 寄存器。

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;  // 复用开漏输出,I2C 专用模式

2.3 I2C 初始化结构体六大参数详解

标准库给我们封装好了 I2C_InitTypeDef 结构体,六个成员看起来多,其实一个个拆开很好懂:

typedef struct
{
    uint32_t I2C_ClockSpeed;          // SCL 时钟频率,单位 Hz,最大 400000(400KHz)
    uint16_t I2C_Mode;                // 工作模式:I2C 模式或 SMBus 模式
    uint16_t I2C_DutyCycle;           // 快速模式下 SCL 占空比(Tlow/Thigh)
    uint16_t I2C_OwnAddress1;         // STM32 自身作为从机时的设备地址
    uint16_t I2C_Ack;                 // 应答使能:接收数据后是否自动回复 ACK
    uint16_t I2C_AcknowledgedAddress; // 地址长度:7 位地址或 10 位地址
} I2C_InitTypeDef;

我的理解

  • I2C_ClockSpeed:就是设置 SCL 跳得多快。MPU6050 支持最高 400KHz,但新手建议先配 100KHz(标准模式),等调通了再往上升。

  • I2C_DutyCycle:快速模式(400KHz)下才需要关心,标准模式(100KHz)随便选。选 I2C_DutyCycle_2 就行,意思是低电平时间是高电平的 2 倍。

  • I2C_OwnAddress1:我们 STM32 当主机,这个填 0x00 就行,从机模式才用得上。

  • I2C_Ack必须使能! 否则你读完一个字节,STM32 不会回 ACK,从机以为你出错了,直接罢工。


2.4 硬件 I2C 的"EV 事件"是什么鬼?

这是硬件 I2C 最让新手头疼的地方。软件 I2C 是我们自己 bit-banging,每一步干了什么心里门儿清。但硬件 I2C 是个黑盒,我们只能通过读取状态标志位来判断硬件执行到哪一步了

ST 官方把这些状态标志位封装成了"事件(Event)",常见的事件有:

表格

事件名 含义 什么时候产生
EV5 起始条件已发送(SB=1) 调用 I2C_GenerateSTART()
EV6 地址已发送并收到 ACK(ADDR=1) 发送完从机地址+读写位后
EV8 数据寄存器空,可以发下一个字节(TxE=1) 发送数据写入 DR 寄存器后
EV8_2 最后一个字节发送完毕(BTF=1) 发送停止前检查
EV7 数据寄存器非空,可以读取(RxNE=1) 接收到数据后

我的理解:你可以把 EV 事件想象成"硬件给你发的微信消息"。比如你说"发送起始信号",硬件回你一条"EV5:起始信号已发出",你确认收到后再说"发送地址",硬件再回你"EV6:地址已发送且对方回了 ACK"……就这样一问一答,不能跳步。


三、个人理解 / 心得

  1. 关于时钟配置:STM32 的 I2C 挂在 APB1 总线上,而 APB1 的时钟默认是 36MHz(系统时钟 72MHz 二分频)。这个 36MHz 是 I2C 外设计算 CCR 分频值的基准,不需要手动算 CCR,标准库的 I2C_Init() 函数会根据你填的 I2C_ClockSpeed 自动算好。这一点比寄存器开发省心太多了。

  2. 关于"复用开漏"和"普通开漏"的区别:我一开始以为 GPIO_Mode_Out_OD(普通开漏)也能用,毕竟也是开漏。但实际上,普通开漏是由 GPIO 输出数据寄存器控制的,而 I2C 的时序是由硬件外设自动生成的,必须用 GPIO_Mode_AF_OD 把控制权交给硬件。这是很多人调不通硬件 I2C 的隐藏雷区。

  3. 关于 ACK 时机的把控:读数据的时候,必须在读最后一个字节之前,先关闭 ACK 并发送 STOP。如果顺序搞反了,总线会卡死。这个时序在参考手册里有严格说明,代码里一定要按规矩来。


四、踩坑记录

坑点 现象 解决方案
引脚选错 代码逻辑全对,但 MPU6050 无应答 查数据手册,确认 I2C 复用引脚,不能随意指定
GPIO 模式错配成 Out_OD I2C 时序完全紊乱,示波器看波形不对 必须设为 GPIO_Mode_AF_OD(复用开漏)
忘记使能 I2C 外设时钟 所有寄存器操作无效,程序卡死在等待 EV 事件 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE) 别漏
读数据时 ACK 关闭时机错误 读最后一个字节后总线死锁,后续通信失败 倒数第二个字节读完后关 ACK + 发 STOP,再读最后一个

五、完整代码实现

硬件代码基本和软件的实现差不多,接线图也和我之前写的软件配置的接线图一样。

5.1 MPU6050_Reg.h(头文件)

#ifndef __MPU6050_H
#define __MPU6050_H

#include "stm32f10x.h"                  // 包含 STM32 标准库头文件,提供寄存器定义和库函数声明

//======================== 宏定义区:MPU6050 寄存器地址 ========================
#define MPU6050_SMPLRT_DIV      0x19    // 采样率分频寄存器地址:控制数据输出速率
#define MPU6050_CONFIG          0x1A    // 配置寄存器地址:设置数字低通滤波器(DLPF)
#define MPU6050_GYRO_CONFIG     0x1B    // 陀螺仪配置寄存器地址:设置量程和自检
#define MPU6050_ACCEL_CONFIG    0x1C    // 加速度计配置寄存器地址:设置量程和自检
#define MPU6050_ACCEL_XOUT_H    0x3B    // 加速度计 X 轴高 8 位数据寄存器地址
#define MPU6050_ACCEL_XOUT_L    0x3C    // 加速度计 X 轴低 8 位数据寄存器地址
#define MPU6050_ACCEL_YOUT_H    0x3D    // 加速度计 Y 轴高 8 位数据寄存器地址
#define MPU6050_ACCEL_YOUT_L    0x3E    // 加速度计 Y 轴低 8 位数据寄存器地址
#define MPU6050_ACCEL_ZOUT_H    0x3F    // 加速度计 Z 轴高 8 位数据寄存器地址
#define MPU6050_ACCEL_ZOUT_L    0x40    // 加速度计 Z 轴低 8 位数据寄存器地址
#define MPU6050_TEMP_OUT_H      0x41    // 温度传感器高 8 位数据寄存器地址
#define MPU6050_TEMP_OUT_L      0x42    // 温度传感器低 8 位数据寄存器地址
#define MPU6050_GYRO_XOUT_H     0x43    // 陀螺仪 X 轴高 8 位数据寄存器地址
#define MPU6050_GYRO_XOUT_L     0x44    // 陀螺仪 X 轴低 8 位数据寄存器地址
#define MPU6050_GYRO_YOUT_H     0x45    // 陀螺仪 Y 轴高 8 位数据寄存器地址
#define MPU6050_GYRO_YOUT_L     0x46    // 陀螺仪 Y 轴低 8 位数据寄存器地址
#define MPU6050_GYRO_ZOUT_H     0x47    // 陀螺仪 Z 轴高 8 位数据寄存器地址
#define MPU6050_GYRO_ZOUT_L     0x48    // 陀螺仪 Z 轴低 8 位数据寄存器地址
#define MPU6050_PWR_MGMT_1      0x6B    // 电源管理寄存器 1 地址:控制睡眠模式和时钟源
#define MPU6050_PWR_MGMT_2      0x6C    // 电源管理寄存器 2 地址:控制各轴待机
#define MPU6050_WHO_AM_I        0x75    // 设备 ID 寄存器地址:固定返回 0x68,用于检测器件是否存在

#define MPU6050_ADDRESS         0xD0    // MPU6050 的 I2C 写地址(0x68 << 1 | 0)


#endif

5.2 MPU6050.c(源文件)

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"


// MPU6050器件地址 A0引脚拉低=0xD0
#define MPU6050_ADDRESS 0xD0

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t Timeout;
	Timeout = 10000;									//给定超时计数时间
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)	//循环等待指定事件
	{
		Timeout --;										//等待时,计数值自减
		if (Timeout == 0)								//自减到0后,等待超时
		{
			/*超时的错误处理代码,可以添加到此处*/
			break;										//跳出等待,不等了
		}
	}
}

/**
  * 函    数:MPU6050写寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 参    数:Data 要写入寄存器的数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8
	
	I2C_SendData(I2C2, Data);												//硬件I2C发送数据
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	I2C_GenerateSTOP(I2C2, ENABLE);											//硬件I2C生成终止条件
}

/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成重复起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);		//硬件I2C发送从机地址,方向为接收
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);		//等待EV6
	
	I2C_AcknowledgeConfig(I2C2, DISABLE);									//在接收最后一个字节之前提前将应答失能
	I2C_GenerateSTOP(I2C2, ENABLE);											//在接收最后一个字节之前提前申请停止条件
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);				//等待EV7
	Data = I2C_ReceiveData(I2C2);											//接收数据寄存器
	
	I2C_AcknowledgeConfig(I2C2, ENABLE);									//将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
	
	return Data;
}

/**
  * 函    数:MPU6050初始化
  * 参    数:无
  * 返 回 值:无
  */
void MPU6050_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);		//开启I2C2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);					//将PB10和PB11引脚初始化为复用开漏输出
	
	/*I2C初始化*/
	I2C_InitTypeDef I2C_InitStructure;						//定义结构体变量
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;				//模式,选择为I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;				//时钟速度,选择为50KHz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;		//时钟占空比,选择Tlow/Thigh = 2
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;				//应答,选择使能
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	//应答地址,选择7位,从机模式下才有效
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;				//自身地址,从机模式下才有效
	I2C_Init(I2C2, &I2C_InitStructure);						//将结构体变量交给I2C_Init,配置I2C2
	
	/*I2C使能*/
	I2C_Cmd(I2C2, ENABLE);									//使能I2C2,开始运行
	
	/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);				//电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);				//电源管理寄存器2,保持默认值0,所有轴均不待机
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);				//采样率分频寄存器,配置采样率
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);					//配置寄存器,配置DLPF
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);			//陀螺仪配置寄存器,选择满量程为±2000°/s
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);			//加速度计配置寄存器,选择满量程为±16g
}


/**
  * 函    数:MPU6050获取ID号
  * 参    数:无
  * 返 回 值:MPU6050的ID号
  */
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);		//返回WHO_AM_I寄存器的值
}

/**
  * 函    数:MPU6050获取数据
  * 参    数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
  * 参    数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
  * 返 回 值:无
  */
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;								//定义数据高8位和低8位的变量
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;						//数据拼接,通过输出参数返回
}

5.3 main.c(主函数调用示例)

#include "stm32f10x.h"                  // 包含 STM32 标准库头文件
#include "OLED.h"                       // 包含 OLED 显示驱动头文件(用于数据显示,江科大课程配套)
#include "MPU6050.h"                    // 包含 MPU6050 驱动头文件

int main(void)
{
    OLED_Init();                        // 初始化 OLED 显示屏,准备显示加速度/陀螺仪数据
    MPU6050_Init();                     // 初始化 I2C2 外设和 MPU6050 寄存器,配置完成后传感器开始工作
    
    int16_t AX, AY, AZ, GX, GY, GZ;    // 定义 6 个 16 位有符号整型变量,分别存储三轴加速度和三轴陀螺仪数据
    
    while (1)                           // 进入无限循环,持续读取并显示数据
    {
        MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);  // 调用函数读取 MPU6050 的 6 轴原始数据,通过指针传参回写
        
        OLED_ShowSignedNum(1, 1, AX, 5);   // 在第 1 行第 1 列显示加速度 X 轴数据,占 5 位宽度,带符号
        OLED_ShowSignedNum(2, 1, AY, 5);   // 在第 2 行第 1 列显示加速度 Y 轴数据
        OLED_ShowSignedNum(3, 1, AZ, 5);   // 在第 3 行第 1 列显示加速度 Z 轴数据
        OLED_ShowSignedNum(1, 8, GX, 5);   // 在第 1 行第 8 列显示陀螺仪 X 轴数据
        OLED_ShowSignedNum(2, 8, GY, 5);   // 在第 2 行第 8 列显示陀螺仪 Y 轴数据
        OLED_ShowSignedNum(3, 8, GZ, 5);   // 在第 3 行第 8 列显示陀螺仪 Z 轴数据
    }
}

六、总结表格

对比维度 软件模拟 I2C 硬件 I2C
引脚选择 任意 GPIO,灵活 固定复用引脚,需查表
GPIO 模式 开漏输出 Out_OD 复用开漏 AF_OD
时序控制 代码 bit-banging,CPU 全程参与 硬件自动生成,CPU 等待事件即可
稳定性 受中断和延时影响,时序可能抖动 硬件时序精准,抗干扰强
代码复杂度 需手动实现 Start/Stop/ACK 等 调用库函数,关注 EV 事件标志位
CPU 占用 高,发送 1 字节需多次翻转 GPIO 低,配置后硬件自动完成
适用场景 引脚受限、简单调试 正式项目、高速通信、多设备总线

更多工程代码:STM32_Project: 基于江科大的STM32的教程编写的程序


七、验证方式

  1. 硬件验证:OLED 正常显示 6 轴数据,晃动开发板时 AX/AY/AZ 数值明显变化,静止时 AZ 约为 16384(±2g 量程下对应 1g)。

  2. 软件验证:在 MPU6050_Init() 后读取 WHO_AM_I 寄存器(0x75),应返回固定值 0x68,可单步调试确认。

  3. 示波器验证:SCL 频率约为 50KHz,SDA 波形在 ACK 位有被拉低的低电平脉冲。


写这篇文章的时候,我把江科大的视频又回顾了一遍,发现硬件 I2C 的难点其实不在代码量,而在对 EV 事件和时序的理解。软件 I2C 是我们自己掌控每一步,硬件 I2C 则是"委托"给外设,我们要学会"看标志位办事"。刚开始可能会觉得 EV5、EV6 这些事件很抽象,但对照参考手册里的时序图多看几遍,就会明白硬件其实就是在帮我们做软件 I2C 里那些 MyI2C_W_SDA()MyI2C_W_SCL() 的事情,只是它做得更快、更准。

从软件 I2C 过渡到硬件 I2C,是 STM32 学习中很重要的一步。它让我们开始真正使用芯片内部的硬件外设,而不是把 MCU 当做一个单纯的 GPIO 控制器来用。希望这篇笔记能帮到你,如果有哪里写错了,欢迎评论区指正!


📌 开发环境:Keil5 + STM32F103C8T6 最小系统板 + ST-Link
📌 参考资料:江科大 STM32 入门教程、《STM32F10xxx 参考手册》
📌 前置文章:江科大STM32入门:硬件I2C外设结构与通信时序深度解析

如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐!有任何问题欢迎在评论区交流,一起学习进步!

Logo

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

更多推荐