【自学嵌入式:stm32单片机】硬件I2C读写MPU6050
目录
硬件I2C读写MPU6050
接线图

这是江科大版本的接线图,我的版本是把I2C拓展模块接在PB10和PB11
我接的I2C2的引脚,然后OLED屏幕的驱动代码我也进行了重写,OLED屏幕接在了拓展模块上面,HardI2C模块基于MYI2C魔改而来,MPU6050模块的函数声明头文件以及寄存器声明和上一篇文章一致,不提供了,MYOLED模块的函数声明头文件也和之前的一致,也不提供了,详见对应开源仓库,因为我用串口做了调试,所以一些串口相关的代码注释了
代码实现
已开源到:https://gitee.com/qin-ruiqian/jiangkeda-stm32
标准库实现
HardI2C.h
#ifndef __HARDI2C_H
#define __HARDI2C_H
void HardI2C_Init(void);
void HardI2C_Start(void);
void HardI2C_SendAddress_Write(uint8_t Address);
void HardI2C_SendAddress_Read(uint8_t Address);
void HardI2C_WriteByte(uint8_t Byte);
void HardI2C_WriteBytes(uint8_t* Bytes, uint16_t Length);
void HardI2C_Close(void);
uint8_t HardI2C_ReadByte_WithClose(void);
void HardI2C_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
#endif
HardI2C.c
#include "stm32f10x.h"                  // Device header
//#include "Serial.h"
//延时等待事件
void HardI2C_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后,等待超时
		{
			//Serial_Printf("出现超时! \r\n");
			/*超时的错误处理代码,可以添加到此处*/
			break;										//跳出等待,不等了
		}
	}
}
//初始化硬件I2C2
void HardI2C_Init(void)
{
    //I2C1和I2C2都是APB1的外设
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
    //打开GPIO时钟,PB口
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    //PB10和PB11都开启为复用开漏模式
    GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //复用开漏模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; //PB10,11打开
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	//初始化I2C2外设
    I2C_InitTypeDef I2C_InitStructure;
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //选I2C模式
    I2C_InitStructure.I2C_ClockSpeed = 400000; //最快400Khz
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //1:1占空比在高速传输下限制总线最大传输速度,波形变成三角形,SCL低电平期间,SDA数据变化也不是完全贴到下降沿的,这也会有一些延时,所以有必要在低电平多分配一些时间,所以先设置2:1试试
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //用于确定在接收一个字节后是否给从机应答
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //指定STM32作为从机可以相应几位的地址
    I2C_InitStructure.I2C_OwnAddress1 = 0x00; //给STM32作为从机时候的地址,因为这里STM32做主机,所以,随便给一个,只要不和总线上其他设备地址冲突就行
    I2C_Init(I2C2, &I2C_InitStructure);
    //使能I2C2
    I2C_Cmd(I2C2, ENABLE);
}
//开始I2C通讯
void HardI2C_Start(void)
{
    I2C_GenerateSTART(I2C2, ENABLE);
    //等待EV5事件到来,确认起始帧已发送
    //死循环多了容易卡死,需设计超时退出机制
    HardI2C_WaitEvent(I2C2, 
    I2C_EVENT_MASTER_MODE_SELECT); //EV5事件也可以叫做主机模式已选择的事件
}
//发送从机地址,写模式
void HardI2C_SendAddress_Write(uint8_t Address)
{
    I2C_Send7bitAddress(I2C2, Address, I2C_Direction_Transmitter); //发送模式,I2C_Direction_Transmitter让地址最低为置0
    //硬件自带应答,发送地址之后,应答位就不需要处理了
    //等待EV6事件
    HardI2C_WaitEvent(I2C2, 
    I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
}
//发送从机地址,读模式
void HardI2C_SendAddress_Read(uint8_t Address)
{
    I2C_Send7bitAddress(I2C2, Address, I2C_Direction_Receiver); //最低位置1
    //等待EV6事件,EV6接收模式
    HardI2C_WaitEvent(I2C2, 
    I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
}
//发送一个字节
void HardI2C_WriteByte(uint8_t Byte)
{
    I2C_SendData(I2C2, Byte);
    //等待EV8_2事件
    HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
}
//发送连续多个字节
void HardI2C_WriteBytes(uint8_t* Bytes, uint16_t Length)
{
    uint16_t i = 0;
    for(i = 0; i<Length; i++)
    {
        I2C_SendData(I2C2, Bytes[i]);
        //到最后一个才检查EV8_2,其他都检查EV8事件
        if(i != Length - 1)
        {
            //EV8事件非常快,基本不用等,第一个数据写进DR了,会立刻跑到移位寄存器,这时不用等第一个数据发完,第二个数据就可以写进去等着了
            //等待EV8事件
            HardI2C_WaitEvent(I2C2, 
            I2C_EVENT_MASTER_BYTE_TRANSMITTING);
        }
        else
        {
            //要等待硬件把两级缓存,所有数据都清空,才能产生终止条件
            //等待EV8_2事件
            HardI2C_WaitEvent(I2C2, 
            I2C_EVENT_MASTER_BYTE_TRANSMITTED);
        }
    }
}
//终止I2C通信
void HardI2C_Close(void)
{
    I2C_GenerateSTOP(I2C2, ENABLE);
}
//读指定地址从机发来的一个字节,包含终止I2C通信的功能
uint8_t HardI2C_ReadByte_WithClose(void)
{
    //指定地址读一个字节,所以在EV6_1时
    //也就是EV6事件之后,要清除响应和停止条件的产生
    //也就是把应答位ACK置0,同时把停止条件生成位STOP置1
    //规定就是,在接收到数据之前,需要提前把ACK置0,同时设置停止位STOP
    uint8_t Byte = 0;
    I2C_AcknowledgeConfig(I2C2, DISABLE);
    I2C_GenerateSTOP(I2C2, ENABLE);
    HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
    Byte = I2C_ReceiveData(I2C2);
    //最后别忘了默认给ACK置回1
    //我们的想法是默认状态下ACK就是1
    //在收到最后一个字节之前,临时把ACK置0,给非应答
    //在收最后一个字节之前,临时把ACK置0,给非应答
    I2C_AcknowledgeConfig(I2C2, ENABLE);
    return Byte;
}
MPU6050.c
#include "stm32f10x.h" // Device header
#include "MPU6050_Reg.h"
#include "MPU6050.h"
#include "HardI2C.h"
#define MPU6050_ADDRESS 0xD0
// 指定地址写寄存器
// RegAddress - 8位寄存器地址
// Data - 8位数据
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
    // I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	// I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	// I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8
	
	// I2C_SendData(I2C2, Data);												//硬件I2C发送数据
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	// I2C_GenerateSTOP(I2C2, ENABLE);											//硬件I2C生成终止条件
    HardI2C_Start();
    HardI2C_SendAddress_Write(MPU6050_ADDRESS);
    uint8_t Bytes[2] = {RegAddress, Data};
    HardI2C_WriteBytes(Bytes, 2); //一次性发送从机寄存器地址和数据
    HardI2C_Close();
    // MYI2C_WriteByte(MPU6050_ADDRESS); // 从机地址+读写位
    // MYI2C_ReadAck(); //接收应答,不进行处理,测试用
    // MYI2C_WriteByte(RegAddress); //指定寄存器地址
    // MYI2C_ReadAck(); //接收应答,不进行处理,测试用
    // MYI2C_WriteByte(Data); //指定寄存器的数据
    // MYI2C_ReadAck(); //接收应答,不进行处理,测试用
    // MYI2C_Close();
}
//指定地址读数据
//RegAddress - 指定读数据的8位寄存器地址
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
    uint8_t Data = 0; //接收的数据
    // I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
    HardI2C_Start();
	
	// I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
    HardI2C_SendAddress_Write(MPU6050_ADDRESS);
	
	// I2C_SendData(I2C2, RegAddress);											//硬件I2C发送寄存器地址
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
    HardI2C_WriteByte(RegAddress);
	
	// I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成重复起始条件
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
    HardI2C_Start();
	
	// I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);		//硬件I2C发送从机地址,方向为接收
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);		//等待EV6
    HardI2C_SendAddress_Read(MPU6050_ADDRESS); //发送从机地址,接收数据模式
	
	// I2C_AcknowledgeConfig(I2C2, DISABLE);									//在接收最后一个字节之前提前将应答失能
	// I2C_GenerateSTOP(I2C2, ENABLE);											//在接收最后一个字节之前提前申请停止条件
	
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);				//等待EV7
	// Data = I2C_ReceiveData(I2C2);											//接收数据寄存器
	
	// I2C_AcknowledgeConfig(I2C2, ENABLE);									//将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
    //接收一个字节并关闭I2C通信
    Data = HardI2C_ReadByte_WithClose();
    // MYI2C_Start();
    // MYI2C_WriteByte(MPU6050_ADDRESS); // 从机地址+读写位
    // MYI2C_ReadAck(); //接收应答,不进行处理,测试用
    // MYI2C_WriteByte(RegAddress); //指定寄存器地址
    // MYI2C_ReadAck(); //接收应答,不进行处理,测试用
    // //重新指定读写位,就要重新开始
    // MYI2C_Start();
    // MYI2C_WriteByte(MPU6050_ADDRESS | 0x01); // 从机地址+读写位,读写位为1
    // MYI2C_ReadAck(); //接收应答,不进行处理,测试用
    // //接收应答之后,总线控制权正式交给从机
    // Data = MYI2C_ReadByte();
    // //发送ACK
    // MYI2C_WriteAck(1); //不想继续读,就写1,想连续读多个字节,就写0
    return Data;
}
void MPU6050_Init(void)
{
    HardI2C_Init();
    //设置电源管理寄存器1
    //0不复位0解除睡眠模式0不需要循环0无关位0温度传感器不失能001选择x轴陀螺仪时钟
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
    //设置电源管理寄存器2
    //00不需要循环模式唤醒频率,000000,每个轴待机位,不需要待机,都给0
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
    //设置采样率分频寄存器
    //10分频
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
    //设置配置寄存器
    //bit7-bit3全都给0,不需要外部同步
    //bit2-bit0,数字低通滤波器,给110,最平滑的滤波
    MPU6050_WriteReg(MPU6050_CONFIG, 0X06);
    //设置陀螺仪配置寄存器
    //bit7-bit5是自测使能,不自测,都给0
    //bit4-bit3满量程选择,给11,选择最大量程
    //bit2-bit0无关位,都给0
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0X18);
    //设置加速度计配置寄存器
    //自测000
    //满量程,最大给11
    //高通滤波器,用不到,给000
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}
//获取数据寄存器
void MPU6050_GetData(MPU6050Data* pMPU6050Data)
{
    //因为这个16位数据是补码表示的有符号数
    //直接赋值给int16_t也没问题
    pMPU6050Data->AccX = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //先读高8位
    pMPU6050Data->AccX = (pMPU6050Data->AccX)<<8; //左移8位,准备接收低8位
    pMPU6050Data->AccX = (pMPU6050Data->AccX) | MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读低8位
    pMPU6050Data->AccY = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //先读高8位
    pMPU6050Data->AccY = (pMPU6050Data->AccY)<<8; //左移8位,准备接收低8位
    pMPU6050Data->AccY = (pMPU6050Data->AccY) | MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); //读低8位
    pMPU6050Data->AccZ = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); //先读高8位
    pMPU6050Data->AccZ = (pMPU6050Data->AccZ)<<8; //左移8位,准备接收低8位
    pMPU6050Data->AccZ = (pMPU6050Data->AccZ) | MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); //读低8位
    pMPU6050Data->GyroX = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); //先读高8位
    pMPU6050Data->GyroX = (pMPU6050Data->GyroX)<<8; //左移8位,准备接收低8位
    pMPU6050Data->GyroX = (pMPU6050Data->GyroX) | MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); //读低8位
    pMPU6050Data->GyroY = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); //先读高8位
    pMPU6050Data->GyroY = (pMPU6050Data->GyroY)<<8; //左移8位,准备接收低8位
    pMPU6050Data->GyroY = (pMPU6050Data->GyroY) | MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); //读低8位
    pMPU6050Data->GyroZ = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); //先读高8位
    pMPU6050Data->GyroZ = (pMPU6050Data->GyroZ)<<8; //左移8位,准备接收低8位
    pMPU6050Data->GyroZ = (pMPU6050Data->GyroZ) | MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); //读低8位
}
//获取MPU6050的ID
uint8_t MPU6050_GetID(void)
{
    return MPU6050_ReadReg(0X75);
}
MYOLED.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "HardI2C.h"
#include "OLED_Font.h"
#include "OLED_ZhCN_Font.h"
#define MYOLED_ADDRESS 0x78
//以下所有x是列,y是行
//OLED屏幕写指令
void MYOLED_WriteCommand(uint8_t Command)
{
	// I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	// I2C_Send7bitAddress(I2C2, MYOLED_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	// I2C_SendData(I2C2, 0x00);											//硬件I2C发送寄存器地址
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8
	
	// I2C_SendData(I2C2, Command);												//硬件I2C发送数据
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	// I2C_GenerateSTOP(I2C2, ENABLE);	
	HardI2C_Start();
	HardI2C_SendAddress_Write(MYOLED_ADDRESS);
	uint8_t Bytes[2] = {0x00, Command};
	HardI2C_WriteBytes(Bytes, 2); //一次性发送0x00写指令命令和对应要写的命令
	HardI2C_Close();
	// //启动信号
	// MYI2C_Start();
	// MYI2C_WriteByte(0x78); //从机地址0111 100,最后一位置0代表写
	// //接收应答位(不做处理,默认收到)
	// MYI2C_ReadAck();
	// MYI2C_WriteByte(0x00); //紧接着发送0x00告诉OLED屏幕进入写指令模式
	// //接收应答位
	// MYI2C_ReadAck();
	// MYI2C_WriteByte(Command); //发送指令
	// //接收应答位
	// MYI2C_ReadAck();
	// MYI2C_Close();
}
//OLED屏幕写数据(一个字节)
void MYOLED_WriteData(uint8_t Data)
{
	// I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始条件
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5
	
	// I2C_Send7bitAddress(I2C2, MYOLED_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C发送从机地址,方向为发送
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6
	
	// I2C_SendData(I2C2, 0x40);											//硬件I2C发送寄存器地址
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8
	
	// I2C_SendData(I2C2, Data);												//硬件I2C发送数据
	// HardI2C_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2
	
	// I2C_GenerateSTOP(I2C2, ENABLE);	
	HardI2C_Start();
	HardI2C_SendAddress_Write(MYOLED_ADDRESS);
	uint8_t Bytes[2] = {0x40, Data};
	HardI2C_WriteBytes(Bytes, 2); //一次性发送0x40告诉OLED屏幕进入写数据模式以及对应要写入的数据
	HardI2C_Close();
	// MYI2C_Start();
	// MYI2C_WriteByte(0x78); //从机地址0111 100,最后一位置0代表写
	// MYI2C_ReadAck(); //接收应答位(不做处理,默认收到)
	// MYI2C_WriteByte(0x40); //紧接着发送0x40告诉OLED屏幕进入写数据模式
	// MYI2C_ReadAck(); //接收应答位(不做处理,默认收到)
	// MYI2C_WriteByte(Data); //发送指令
	// MYI2C_ReadAck(); //接收应答位(不做处理,默认收到)
	// MYI2C_Close();
}
//设置OLED屏幕光标,按从0开始的下标设置坐标
//y:每8行像素算作一页,也就是8*128是一页,设置y其实设置的是下标为y的页
//x:设置在某页的第几列开始显示
//x和y确定后就是一列竖着的8个像素点,正好是一个字节
//通过命令(B0h-B7h)设置目标显示位置页起始地址
//通过命令(00h-0Fh)设置列起始地址低位
//通过命令(10h-1Fh)设置列起始地址高位
void MYOLED_SetCursor(uint8_t x, uint8_t y)
{
	//0xB0是 OLED 的 “翻页命令”
	//比如0xB0对应第 0 页,0xB1对应第 1 页,以此类推
	MYOLED_WriteCommand(0xB0 | y);					//设置Y位置,也就是设置第几页
	MYOLED_WriteCommand(0x10 | ((x & 0xF0) >> 4));	//设置X位置的高4位
	MYOLED_WriteCommand(0x00 | (x & 0x0F));			//设置X位置的低4位
}
//OLED屏幕清屏
void MYOLED_Clear(void)
{
	//每一页的列字节都写0
	uint8_t x = 0;
	uint8_t y = 0;
	for(y = 0; y < 8; y++)
	{
		MYOLED_SetCursor(0, y); //列地址指针回到0
		for(x = 0; x < 128; x++)
		{
			//设置光标不写在这里是因为访问对应页的列数据后
			//列地址指针在OLED屏幕中自动加1,不需要手动操作
			MYOLED_WriteData(0x00);
		}
	}
}
//指定页的对应列的8个像素
void MYOLED_SetPixel(uint8_t x, uint8_t y, uint8_t data)
{
	MYOLED_SetCursor(x,y);
	MYOLED_WriteData(data);
}
//直接读取二值化图像到OLED显示器
void MYOLED_ReadBinaryImage(uint8_t arr[8][128])
{
	uint8_t x = 0;
	uint8_t y = 0;
	for(y = 0; y < 8; y++)
	{
		MYOLED_SetCursor(0, y); //列地址指针回到0
		for(x = 0; x < 128; x++)
		{
			MYOLED_WriteData(arr[y][x]);
		}
	}
}
//在指定的页和列显示一个字符
void MYOLED_ShowChar(uint8_t x, uint8_t y, char Char)
{
	//借用江科大的字库头文件
	uint8_t i = 0;
	//找到y页,然后设置8个像素宽,这样形成8x8区域(ASCII字符上半8X8+下半8X8总共8X16组成一个字符)
	//一个ASCII字符占两页,所以y*2
	//y = 0, y=2 ,y=4...
	MYOLED_SetCursor(x * 8, y * 2);
	for(i = 0; i < 8; i++)
	{
		MYOLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容
	}
	//接下来显示下半部分
	// y = 1, 3, 5...
	MYOLED_SetCursor(x * 8, y * 2 + 1);
	for(i = 0; i < 8; i++)
	{
		MYOLED_WriteData(OLED_F8x16[Char - ' '][i+8]); //显示上半部分内容
	}
	
}
//在指定位置显示一个字库里面自带的中文字符
//中文字符是方正的,16x16
//只能按索引获取,0是测,1是试
void MYOLED_ShowZhCNChar(uint8_t x, uint8_t y, uint8_t index)
{
	//用自己的中文字库头文件
	uint8_t i = 0;
	MYOLED_SetCursor(x * 16, y * 2);
	for(i = 0; i < 16; i++)
	{
		MYOLED_WriteData(cn_font_arr[index][i]); //显示上半部分内容
	}
	//接下来显示下半部分
	// y = 1, 3, 5...
	MYOLED_SetCursor(x * 16, y * 2 + 1);
	for(i = 0; i < 16; i++)
	{
		MYOLED_WriteData(cn_font_arr[index][i+16]); //显示上半部分内容
	}
	
}
//在指定位置输出字符串(ASCII非中文编码)
//x是列,y是行
void MYOLED_ShowString(uint8_t x, uint8_t y, char* String)
{
	uint8_t i = 0;
	for (i = 0; String[i] != '\0'; i++)
	{
		MYOLED_ShowChar(x + i, y, String[i]);
	}
}
//有了这些基本操作,后面就和之前的LCD1602操作差不多
//计算X的Y次方
uint32_t MYOLED_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}
//输出数字,指定长度
void MYOLED_ShowNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length)
{
	uint8_t i = 0;
	//从最高位开始输出,和LCD1602不同的地方在于
	//这个确定光标位置后,要自己写代码移动光标位置
	//也就是下面x+ Length - i这个参数,Length - i是指Length和当前要显示数字的位数的差
	//比如一共显示3位长度,3位10进制,从右往左,第三位是百位,3-3 = 0,这样就能把高位十进制写在最左边
	//让其按人眼逻辑显示
	for (i = Length; i > 0; i--)								
	{
		MYOLED_ShowChar(x + Length - i, y, Number / MYOLED_Pow(10, i - 1) % 10 + '0');
	}
}
//显示带符号数
void MYOLED_ShowSignedNum(uint8_t x, uint8_t y, int32_t Number, uint8_t Length)
{
	uint8_t i = 0;
	uint32_t Number1; //一会取反用的
	//大于0显示正号,0不显示符号,负数显示负号
	if (Number > 0)
	{
		MYOLED_ShowChar(x, y, '+');
		Number1 = Number;
	}
	else if(Number ==0)
	{
		Number1 = Number;
	}
	else
	{
		MYOLED_ShowChar(x, y, '-');
		Number1 = -Number;
	}
	//给符号位腾出一个空间
	if(Number != 0)
	{
		x++;
	}
	//从最高位开始输出,和LCD1602不同的地方在于
	//这个确定光标位置后,要自己写代码移动光标位置
	//也就是下面x+ Length - i这个参数,Length - i是指Length和当前要显示数字的位数的差
	//比如一共显示3位长度,3位10进制,从右往左,第三位是百位,3-3 = 0,这样就能把高位十进制写在最左边
	//让其按人眼逻辑显示
	for (i = Length; i > 0; i--)							
	{
		MYOLED_ShowChar(x + Length - i, y, Number1 / MYOLED_Pow(10, i - 1) % 10 + '0');
	}
}
//显示16进制数字
void MYOLED_ShowHexNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length)
{
	uint8_t i, SingleNumber;
	for (i = Length; i > 0; i--)							
	{
		SingleNumber = Number / MYOLED_Pow(16, i - 1) % 16;
		if (SingleNumber < 10)
		{
			MYOLED_ShowChar(x + Length - i, y, SingleNumber + '0');
		}
		else
		{
			MYOLED_ShowChar(x + Length - i, y, SingleNumber - 10 + 'A');
		}
	}
}
//显示二进制数
void MYOLED_ShowBinNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = Length; i > 0; i--)							
	{
		MYOLED_ShowChar(x + Length - i, y, Number / MYOLED_Pow(2, i - 1) % 2 + '0');
	}
}
//初始化OLED屏幕
void MYOLED_Init(void)
{
	Delay_s(1); //上电延时等1s,让电压稳定
	HardI2C_Init();
	MYOLED_WriteCommand(0xAE);	//关闭显示,清除上一次显示内容
	//设置时钟分频比和振荡器频率
	//时钟分频比:维持屏幕刷新,协调通信节奏
	//振荡器频率:直接影响屏幕刷新和数据处理速度。
	//让内部时钟 “跑赢” 通信速度
	//100K波特率的通信速度
	/*
	高 4 位(0000):控制时钟分频值。
	规则:实际分频值 = 高 4 位数值 + 1 → 0 + 1 = 1(分频值为 1)。
	含义:内部振荡器的原始频率直接使用(不分频或除以 1),保证处理速度最快。
	低 4 位(0000):控制振荡器频率。
	这是默认的 “中等频率”(约 4MHz,不同型号略有差异),配合分频值 1,最终内部处理时钟约为 4MHz,远高于 100k 波特率,完全能 “跑赢” 通信速度。
	SSD1306 内部的振荡器是RC 振荡器(靠电阻和电容的充放电产生频率),这种振荡器的频率没法像 “拧旋钮” 一样精确设定(比如精确到 4.000MHz),只能通过低 4 位 “选档位”
	厂商在设计时,会通过调整内部 RC 元件的参数,让 “0000” 这个档位的典型频率落在4MHz 左右(不同厂商的芯片可能略有差异,比如 3.5~4.5MHz)。
	*/
	MYOLED_WriteCommand(0xD5);
	/*
	假设你是一个 “消息处理员”,每天要接收别人发来的消息,然后整理好记下来。
	外部设备(比如单片机)给 OLED 发数据,就像 “别人给你发消息”,速度是100 条 / 秒(对应 100k 波特率)。
	OLED 内部的 “时钟处理模块” 就像你的 “记笔记速度”—— 你必须比对方发消息的速度快,才能不遗漏任何一条。
	现在,0xD5命令里的 “高四位” 其实是在调你的 “记笔记速度缩放比例”:
	高四位设为0000,相当于 “不减速”—— 你用自己最快的速度记,每秒能记4000 条(对应 4MHz)。
	这时候对方每秒只发 100 条,你记完一条还能歇会儿,完全从容,绝不会漏记。
	如果高四位设大了(比如1000),相当于 “放慢 9 倍速度记”—— 你每秒只能记444 条。
	虽然比 100 条 / 秒快,但余量太少了:对方稍微发快一点(比如偶尔 150 条 / 秒),你就可能手忙脚乱,记混或漏记。
	*/
	/*
	先明确三个 “角色”:
	低四位:决定 “快递仓库的基础处理能力”(振荡器频率)。
	比如低四位设为0000时,仓库的基础能力是 “每小时处理 4000 件快递”(对应振荡器频率 4MHz)。
	高四位:决定 “给仓库装一个减速阀”(时钟分频比)。
	高四位的数值 + 1 = 实际减速倍数(分频值)。比如高四位0000(数值 0),减速倍数 = 0+1=1(不减速);高四位1000(数值 8),减速倍数 = 8+1=9(处理能力降到 1/9)。
	100k 波特率:相当于 “外部送来的快递速度”—— 每小时送 100 件(100k = 每秒 100,000 位数据,类比每小时 100 件)。
	核心公式:内部实际处理速度 = 振荡器频率 ÷ 分频值
	(这里的 “处理速度” 就是内部时钟能支持的最大通信处理能力)
	三者的联系:必须让 “内部处理速度” 远大于 “外部送来的速度”
	就像仓库处理能力必须比外部送快递的速度快,否则会堆积、出错。
	举例(对应 100k 波特率):
	低四位0000 → 振荡器频率 = 4MHz(每小时 4000 件)。
	高四位0000 → 分频值 = 1(不减速)。
	内部实际处理速度 = 4MHz ÷ 1=4MHz(每小时 4000 件)。
	而外部送来的速度是 100k 波特率(每小时 100 件),4MHz(4000 件 / 小时)远大于 100k(100 件 / 小时),完全能处理,不会堆积。
	*/
	MYOLED_WriteCommand(0x00);
	//设置显示区域,多路复用率,多路复用相当于一个64层的楼,是一层一层单独供电,还是同时给多层供电,答案是同时给多层供电
	MYOLED_WriteCommand(0xA8);	//设置多路复用率,告诉屏幕 “我有多少行像素需要控制”
	MYOLED_WriteCommand(0x3F);	//常见的 SSD1306 是 128x64 的屏幕(128 列、64 行)。0xA8 是 “设置行数” 的命令,0x3F 对应的是 64 行(十进制63,下标从0开始)
	//设置显示偏移,默认从顶端开始
	MYOLED_WriteCommand(0xD3);	//设置显示偏移
	MYOLED_WriteCommand(0x00);  //偏移值,默认不偏移,这一个字节的数据都记录向上偏移多少位
	MYOLED_WriteCommand(0x40);  //01 000000,后6位表示从哪一行开始显示,此处为0,表示从下标为0的行开始显示
	//设置显示方向是从左到右,从上到下
	MYOLED_WriteCommand(0xA1);	//设置左右方向,0xA1正常 0xA0左右反置
	MYOLED_WriteCommand(0xC8);	//设置上下方向,0xC8正常 0xC0上下反置
	//设置COM引脚硬件
	/*
	OLED 屏幕的像素是由 “列导线”(SEG 引脚)和 “行导线”(COM 引脚)交叉控制的 
	—— 就像一张方格纸,列导线是竖线,行导线是横线,交叉点就是一个像素。
	要让某个像素亮起来,需要给对应的列导线和行导线通特定的电。
	OLED 屏幕的像素是由 “列导线”(SEG 引脚)和 “行导线”(COM 引脚)交叉控制的 —— 就像一张方格纸,列导线是竖线,行导线是横线,交叉点就是一个像素。
	要让某个像素亮起来,需要给对应的列导线和行导线通特定的电。
	*/
	MYOLED_WriteCommand(0xDA);	//设置COM引脚硬件配置指令
	MYOLED_WriteCommand(0x12);	//不同厂家生产的 OLED 屏幕,内部行导线的焊接顺序可能不一样 —— 有的是从屏幕顶部到底部依次接 1~64 行,有的可能反过来(顶部是 64 行,底部是 1 行)。0x12里的特定位会告诉屏幕 “不用反转,按默认顺序接”。
	//设置屏幕亮度
	MYOLED_WriteCommand(0x81);	//设置对比度控制指令
	MYOLED_WriteCommand(0xCF);	//亮度值:范围 0~255,0xCF 是中等偏亮,值越大越亮,但太亮可能耗电快,这里是平衡后的常用值。
	//设置像素充电周期
	MYOLED_WriteCommand(0xD9);	//设置预充电周期指令
	MYOLED_WriteCommand(0xF1);	//OLED 的像素需要 “充电” 才能发光,0xD9 是 “设置充电时间” 的命令,0xF1 是经过测试的合适时间(兼顾亮度和稳定性)。时间太短,像素亮度不足;太长,可能反应变慢。
	//设定“熄灭电压”
	/*
	作用:让像素熄灭时更彻底,不留残影。
	为什么:像素熄灭时需要一个 “截止电压”,
	0xDB 是 “设置这个电压” 的命令,0x30 是常用值。
	电压不合适会导致像素 “关不严”,留下淡淡的影子。
	*/
	MYOLED_WriteCommand(0xDB);	//设置VCOMH取消选择级别
	MYOLED_WriteCommand(0x30);
	//刚才关了屏幕,现在打开屏幕
	/*
	0xA4 是 “正常模式”(数据是 0 就黑,1 就亮);如果用 0xA5,
	屏幕会强制全亮(不管数据是什么),这一步是确保显示内容由数据控制。
	*/
	MYOLED_WriteCommand(0xA4);	//设置整个显示打开/关闭
	//设置正常显示模式
	/*
	作用:确保 “0 是黑、1 是亮”(正常显示)。
	为什么:0xA6 是 “正常模式”;0xA7 是 “反色模式”(0 亮 1 黑)。
	这里用正常模式,符合我们看屏幕的习惯。
	*/
	MYOLED_WriteCommand(0xA6);	//设置正常/倒转显示指令
	//开启 “升压泵”
	/*
	作用:给屏幕提供 “点亮像素的高压”。
	为什么:OLED 的像素需要比供电电压(3.3V)更高的电压才能点亮,0x8D 是 “控制升压泵” 的命令,
	0x14 是 “开启升压泵”(产生高压)。不开启的话,屏幕可能很暗或不亮。
	*/
	MYOLED_WriteCommand(0x8D);	//设置充电泵指令
	MYOLED_WriteCommand(0x14);
	//开启显示指令
	MYOLED_WriteCommand(0xAF);	//开启显示,之前用 0xAE 关了屏幕,现在设置完了,就可以打开了
	MYOLED_Clear();
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "MYOLED.h"
#include "MPU6050.h"
#include "HardI2C.h"
//#include "Serial.h"
MPU6050Data mpu6050data;
uint8_t ID; //设备ID
int main(void)
{
	//Serial_Init();
	//Serial_Printf("串口初始化完成 \r\n");
    HardI2C_Init();
	//Serial_Printf("硬件I2C通信初始化完成 \r\n");
    MYOLED_Init();
	//Serial_Printf("OLED屏幕初始化完成 \r\n");
    MPU6050_Init();
	//Serial_Printf("MPU6050初始化完成 \r\n");
	MYOLED_ShowString(0,0,"ID:");
    ID = MPU6050_GetID();
    MYOLED_ShowHexNum(3,0,ID,2);
    //scanI2CBusDevices();
    while (1) {
        MPU6050_GetData(&mpu6050data);
        MYOLED_ShowSignedNum(0,1,mpu6050data.AccX, 5);
        MYOLED_ShowSignedNum(0,2,mpu6050data.AccY, 5);
        MYOLED_ShowSignedNum(0,3,mpu6050data.AccZ, 5);
        MYOLED_ShowSignedNum(7,1,mpu6050data.GyroX, 5);
        MYOLED_ShowSignedNum(7,2,mpu6050data.GyroY, 5);
        MYOLED_ShowSignedNum(7,3,mpu6050data.GyroZ, 5);
        //MYOLED_ShowString(0,0, "Testing1");
    }
}
HAL库实现
已开源到:https://gitee.com/qin-ruiqian/jiangkeda-stm32-hal
HAL库实现非常的简单,集成度相当高,我都没做HardI2C模块就实现了功能,下图是关于硬件I2C的IDE设置

分别给OLED屏幕和MPU6050增加了设置句柄函数
MPU6050.h
/*
 * MPU6050.h
 *
 *  Created on: Aug 21, 2025
 *      Author: Administrator
 */
#ifndef HARDWARE_MPU6050_H_
#define HARDWARE_MPU6050_H_
#include "stm32f1xx_hal.h"
//结构体存储MPU6050的数据
typedef struct MPU6050Data
{
    int16_t AccX;
    int16_t AccY;
    int16_t AccZ;
    int16_t GyroX;
    int16_t GyroY;
    int16_t GyroZ;
}MPU6050Data;
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
void MPU6050_Init(void);
void MPU6050_GetData(MPU6050Data* pMPU6050Data);
uint8_t MPU6050_GetID(void);
void MPU6050_SetI2CHandleBeforeInit(I2C_HandleTypeDef *hi2c);
#endif /* HARDWARE_MPU6050_H_ */
MPU6050.c
/*
 * MPU6050.c
 *
 *  Created on: Aug 21, 2025
 *      Author: Administrator
 */
#include "MPU6050.h"
#include "MPU6050_Reg.h"
#include "stm32f1xx_hal.h"
#define MPU6050_ADDRESS 0xD0
I2C_HandleTypeDef *__MPU6050_hi2c; //全局变量I2C硬件句柄
//设置句柄,先于初始化
void MPU6050_SetI2CHandleBeforeInit(I2C_HandleTypeDef *hi2c)
{
	__MPU6050_hi2c = hi2c;
}
// 指定地址写寄存器
// RegAddress - 8位寄存器地址
// Data - 8位数据
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	//HAL库一行代码就实现了
	HAL_I2C_Mem_Write(__MPU6050_hi2c, MPU6050_ADDRESS, RegAddress, 1, &Data, 1, HAL_MAX_DELAY);
}
//指定地址读数据
//RegAddress - 指定读数据的8位寄存器地址
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
    uint8_t Data = 0; //接收的数据
    HAL_I2C_Mem_Read(__MPU6050_hi2c, MPU6050_ADDRESS, RegAddress, 1, &Data, 1, HAL_MAX_DELAY);
    return Data;
}
void MPU6050_Init(void)
{
    //MYI2C_Init();
    //设置电源管理寄存器1
    //0不复位0解除睡眠模式0不需要循环0无关位0温度传感器不失能001选择x轴陀螺仪时钟
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
    //设置电源管理寄存器2
    //00不需要循环模式唤醒频率,000000,每个轴待机位,不需要待机,都给0
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
    //设置采样率分频寄存器
    //10分频
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
    //设置配置寄存器
    //bit7-bit3全都给0,不需要外部同步
    //bit2-bit0,数字低通滤波器,给110,最平滑的滤波
    MPU6050_WriteReg(MPU6050_CONFIG, 0X06);
    //设置陀螺仪配置寄存器
    //bit7-bit5是自测使能,不自测,都给0
    //bit4-bit3满量程选择,给11,选择最大量程
    //bit2-bit0无关位,都给0
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0X18);
    //设置加速度计配置寄存器
    //自测000
    //满量程,最大给11
    //高通滤波器,用不到,给000
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}
//获取数据寄存器
void MPU6050_GetData(MPU6050Data* pMPU6050Data)
{
    //因为这个16位数据是补码表示的有符号数
    //直接赋值给int16_t也没问题
    pMPU6050Data->AccX = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //先读高8位
    pMPU6050Data->AccX = (pMPU6050Data->AccX)<<8; //左移8位,准备接收低8位
    pMPU6050Data->AccX = (pMPU6050Data->AccX) | MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读低8位
    pMPU6050Data->AccY = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //先读高8位
    pMPU6050Data->AccY = (pMPU6050Data->AccY)<<8; //左移8位,准备接收低8位
    pMPU6050Data->AccY = (pMPU6050Data->AccY) | MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); //读低8位
    pMPU6050Data->AccZ = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); //先读高8位
    pMPU6050Data->AccZ = (pMPU6050Data->AccZ)<<8; //左移8位,准备接收低8位
    pMPU6050Data->AccZ = (pMPU6050Data->AccZ) | MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); //读低8位
    pMPU6050Data->GyroX = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); //先读高8位
    pMPU6050Data->GyroX = (pMPU6050Data->GyroX)<<8; //左移8位,准备接收低8位
    pMPU6050Data->GyroX = (pMPU6050Data->GyroX) | MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); //读低8位
    pMPU6050Data->GyroY = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); //先读高8位
    pMPU6050Data->GyroY = (pMPU6050Data->GyroY)<<8; //左移8位,准备接收低8位
    pMPU6050Data->GyroY = (pMPU6050Data->GyroY) | MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); //读低8位
    pMPU6050Data->GyroZ = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); //先读高8位
    pMPU6050Data->GyroZ = (pMPU6050Data->GyroZ)<<8; //左移8位,准备接收低8位
    pMPU6050Data->GyroZ = (pMPU6050Data->GyroZ) | MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); //读低8位
}
//获取MPU6050的ID
uint8_t MPU6050_GetID(void)
{
    return MPU6050_ReadReg(0X75);
}
MYOLED.h
/*
 * MYOLED.h
 *
 *  Created on: Aug 11, 2025
 *      Author: Administrator
 */
#ifndef HARDWARE_MYOLED_H_
#define HARDWARE_MYOLED_H_
void MYOLED_Init(void); //初始化OLED屏幕
void MYOLED_WriteCommand(uint8_t Command); //OLED屏幕写指令
void MYOLED_WriteData(uint8_t Data); //OLED屏幕写数据(一个字节)
void MYOLED_SetCursor(uint8_t x, uint8_t y); //设置OLED屏幕光标,按从0开始的下标设置坐标
void MYOLED_Clear(void); //OELD清屏
void MYOLED_SetPixel(uint8_t x, uint8_t y, uint8_t data); //指定页的列8个像素点设置
void MYOLED_ReadBinaryImage(uint8_t arr[8][128]); //直接读取二值化图像到OLED显示器
void MYOLED_ShowChar(uint8_t x, uint8_t y, char Char); //在指定的页和列显示一个字符
void MYOLED_ShowZhCNChar(uint8_t x, uint8_t y, uint8_t index); //在指定为位置显示中文字符
void MYOLED_ShowString(uint8_t x, uint8_t y, char* String); //在指定位置显示字符串
void MYOLED_ShowNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length); //输出数字,指定长度
void MYOLED_ShowSignedNum(uint8_t x, uint8_t y, int32_t Number, uint8_t Length); //输出带符号数
void MYOLED_ShowHexNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length); //显示十六进制数
void MYOLED_ShowBinNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length); //显示二进制数
void MYOLED_SetI2CHandleBeforeInit(I2C_HandleTypeDef *hi2c);
#endif /* HARDWARE_MYOLED_H_ */
MYOLED.c
/*
 * MYOLED.c
 *
 *  Created on: Aug 11, 2025
 *      Author: Administrator
 */
#include "stm32f1xx_hal.h"
#include "OLED_Font.h"
#include "OLED_ZhCN_Font.h"
#define MYOLED_ADDRESS 0x78
I2C_HandleTypeDef *__MYOLED_hi2c; //设置一个全局变量,懒得全部面向对象
//设置OLED屏幕的I2C硬件句柄,必须先设置,才能初始化
void MYOLED_SetI2CHandleBeforeInit(I2C_HandleTypeDef *hi2c)
{
	__MYOLED_hi2c = hi2c;
}
//以下所有x是列,y是行
//OLED屏幕写指令
void MYOLED_WriteCommand(uint8_t Command)
{
	HAL_I2C_Mem_Write(__MYOLED_hi2c, MYOLED_ADDRESS , 0x00, 1, &Command, 1, HAL_MAX_DELAY);
}
//OLED屏幕写数据(一个字节)
void MYOLED_WriteData(uint8_t Data)
{
	HAL_I2C_Mem_Write(__MYOLED_hi2c, MYOLED_ADDRESS , 0x40, 1, &Data, 1, HAL_MAX_DELAY);
}
//设置OLED屏幕光标,按从0开始的下标设置坐标
//y:每8行像素算作一页,也就是8*128是一页,设置y其实设置的是下标为y的页
//x:设置在某页的第几列开始显示
//x和y确定后就是一列竖着的8个像素点,正好是一个字节
//通过命令(B0h-B7h)设置目标显示位置页起始地址
//通过命令(00h-0Fh)设置列起始地址低位
//通过命令(10h-1Fh)设置列起始地址高位
void MYOLED_SetCursor(uint8_t x, uint8_t y)
{
	//0xB0是 OLED 的 “翻页命令”
	//比如0xB0对应第 0 页,0xB1对应第 1 页,以此类推
	MYOLED_WriteCommand(0xB0 | y);					//设置Y位置,也就是设置第几页
	MYOLED_WriteCommand(0x10 | ((x & 0xF0) >> 4));	//设置X位置的高4位
	MYOLED_WriteCommand(0x00 | (x & 0x0F));			//设置X位置的低4位
}
//OLED屏幕清屏
void MYOLED_Clear(void)
{
	//每一页的列字节都写0
	uint8_t x = 0;
	uint8_t y = 0;
	for(y = 0; y < 8; y++)
	{
		MYOLED_SetCursor(0, y); //列地址指针回到0
		for(x = 0; x < 128; x++)
		{
			//设置光标不写在这里是因为访问对应页的列数据后
			//列地址指针在OLED屏幕中自动加1,不需要手动操作
			MYOLED_WriteData(0x00);
		}
	}
}
//指定页的对应列的8个像素
void MYOLED_SetPixel(uint8_t x, uint8_t y, uint8_t data)
{
	MYOLED_SetCursor(x,y);
	MYOLED_WriteData(data);
}
//直接读取二值化图像到OLED显示器
void MYOLED_ReadBinaryImage(uint8_t arr[8][128])
{
	uint8_t x = 0;
	uint8_t y = 0;
	for(y = 0; y < 8; y++)
	{
		MYOLED_SetCursor(0, y); //列地址指针回到0
		for(x = 0; x < 128; x++)
		{
			MYOLED_WriteData(arr[y][x]);
		}
	}
}
//在指定的页和列显示一个字符
void MYOLED_ShowChar(uint8_t x, uint8_t y, char Char)
{
	//借用江科大的字库头文件
	uint8_t i = 0;
	//找到y页,然后设置8个像素宽,这样形成8x8区域(ASCII字符上半8X8+下半8X8总共8X16组成一个字符)
	//一个ASCII字符占两页,所以y*2
	//y = 0, y=2 ,y=4...
	MYOLED_SetCursor(x * 8, y * 2);
	for(i = 0; i < 8; i++)
	{
		MYOLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容
	}
	//接下来显示下半部分
	// y = 1, 3, 5...
	MYOLED_SetCursor(x * 8, y * 2 + 1);
	for(i = 0; i < 8; i++)
	{
		MYOLED_WriteData(OLED_F8x16[Char - ' '][i+8]); //显示上半部分内容
	}
}
//在指定位置显示一个字库里面自带的中文字符
//中文字符是方正的,16x16
//只能按索引获取,0是测,1是试
void MYOLED_ShowZhCNChar(uint8_t x, uint8_t y, uint8_t index)
{
	//用自己的中文字库头文件
	uint8_t i = 0;
	MYOLED_SetCursor(x * 16, y * 2);
	for(i = 0; i < 16; i++)
	{
		MYOLED_WriteData(cn_font_arr[index][i]); //显示上半部分内容
	}
	//接下来显示下半部分
	// y = 1, 3, 5...
	MYOLED_SetCursor(x * 16, y * 2 + 1);
	for(i = 0; i < 16; i++)
	{
		MYOLED_WriteData(cn_font_arr[index][i+16]); //显示上半部分内容
	}
}
//在指定位置输出字符串(ASCII非中文编码)
//x是列,y是行
void MYOLED_ShowString(uint8_t x, uint8_t y, char* String)
{
	uint8_t i = 0;
	for (i = 0; String[i] != '\0'; i++)
	{
		MYOLED_ShowChar(x + i, y, String[i]);
	}
}
//有了这些基本操作,后面就和之前的LCD1602操作差不多
//计算X的Y次方
uint32_t MYOLED_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}
//输出数字,指定长度
void MYOLED_ShowNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length)
{
	uint8_t i = 0;
	//从最高位开始输出,和LCD1602不同的地方在于
	//这个确定光标位置后,要自己写代码移动光标位置
	//也就是下面x+ Length - i这个参数,Length - i是指Length和当前要显示数字的位数的差
	//比如一共显示3位长度,3位10进制,从右往左,第三位是百位,3-3 = 0,这样就能把高位十进制写在最左边
	//让其按人眼逻辑显示
	for (i = Length; i > 0; i--)
	{
		MYOLED_ShowChar(x + Length - i, y, Number / MYOLED_Pow(10, i - 1) % 10 + '0');
	}
}
//显示带符号数
void MYOLED_ShowSignedNum(uint8_t x, uint8_t y, int32_t Number, uint8_t Length)
{
	uint8_t i = 0;
	uint32_t Number1; //一会取反用的
	//大于0显示正号,0不显示符号,负数显示负号
	if (Number > 0)
	{
		MYOLED_ShowChar(x, y, '+');
		Number1 = Number;
	}
	else if(Number ==0)
	{
		Number1 = Number;
	}
	else
	{
		MYOLED_ShowChar(x, y, '-');
		Number1 = -Number;
	}
	//给符号位腾出一个空间
	if(Number != 0)
	{
		x++;
	}
	//从最高位开始输出,和LCD1602不同的地方在于
	//这个确定光标位置后,要自己写代码移动光标位置
	//也就是下面x+ Length - i这个参数,Length - i是指Length和当前要显示数字的位数的差
	//比如一共显示3位长度,3位10进制,从右往左,第三位是百位,3-3 = 0,这样就能把高位十进制写在最左边
	//让其按人眼逻辑显示
	for (i = Length; i > 0; i--)
	{
		MYOLED_ShowChar(x + Length - i, y, Number1 / MYOLED_Pow(10, i - 1) % 10 + '0');
	}
}
//显示16进制数字
void MYOLED_ShowHexNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length)
{
	uint8_t i, SingleNumber;
	for (i = Length; i > 0; i--)
	{
		SingleNumber = Number / MYOLED_Pow(16, i - 1) % 16;
		if (SingleNumber < 10)
		{
			MYOLED_ShowChar(x + Length - i, y, SingleNumber + '0');
		}
		else
		{
			MYOLED_ShowChar(x + Length - i, y, SingleNumber - 10 + 'A');
		}
	}
}
//显示二进制数
void MYOLED_ShowBinNum(uint8_t x, uint8_t y, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = Length; i > 0; i--)
	{
		MYOLED_ShowChar(x + Length - i, y, Number / MYOLED_Pow(2, i - 1) % 2 + '0');
	}
}
//初始化OLED屏幕
void MYOLED_Init(void)
{
	HAL_Delay(1000); //上电延时等1s,让电压稳定
	MYOLED_WriteCommand(0xAE);	//关闭显示,清除上一次显示内容
	//设置时钟分频比和振荡器频率
	//时钟分频比:维持屏幕刷新,协调通信节奏
	//振荡器频率:直接影响屏幕刷新和数据处理速度。
	//让内部时钟 “跑赢” 通信速度
	//100K波特率的通信速度
	/*
	高 4 位(0000):控制时钟分频值。
	规则:实际分频值 = 高 4 位数值 + 1 → 0 + 1 = 1(分频值为 1)。
	含义:内部振荡器的原始频率直接使用(不分频或除以 1),保证处理速度最快。
	低 4 位(0000):控制振荡器频率。
	这是默认的 “中等频率”(约 4MHz,不同型号略有差异),配合分频值 1,最终内部处理时钟约为 4MHz,远高于 100k 波特率,完全能 “跑赢” 通信速度。
	SSD1306 内部的振荡器是RC 振荡器(靠电阻和电容的充放电产生频率),这种振荡器的频率没法像 “拧旋钮” 一样精确设定(比如精确到 4.000MHz),只能通过低 4 位 “选档位”
	厂商在设计时,会通过调整内部 RC 元件的参数,让 “0000” 这个档位的典型频率落在4MHz 左右(不同厂商的芯片可能略有差异,比如 3.5~4.5MHz)。
	*/
	MYOLED_WriteCommand(0xD5);
	/*
	假设你是一个 “消息处理员”,每天要接收别人发来的消息,然后整理好记下来。
	外部设备(比如单片机)给 OLED 发数据,就像 “别人给你发消息”,速度是100 条 / 秒(对应 100k 波特率)。
	OLED 内部的 “时钟处理模块” 就像你的 “记笔记速度”—— 你必须比对方发消息的速度快,才能不遗漏任何一条。
	现在,0xD5命令里的 “高四位” 其实是在调你的 “记笔记速度缩放比例”:
	高四位设为0000,相当于 “不减速”—— 你用自己最快的速度记,每秒能记4000 条(对应 4MHz)。
	这时候对方每秒只发 100 条,你记完一条还能歇会儿,完全从容,绝不会漏记。
	如果高四位设大了(比如1000),相当于 “放慢 9 倍速度记”—— 你每秒只能记444 条。
	虽然比 100 条 / 秒快,但余量太少了:对方稍微发快一点(比如偶尔 150 条 / 秒),你就可能手忙脚乱,记混或漏记。
	*/
	/*
	先明确三个 “角色”:
	低四位:决定 “快递仓库的基础处理能力”(振荡器频率)。
	比如低四位设为0000时,仓库的基础能力是 “每小时处理 4000 件快递”(对应振荡器频率 4MHz)。
	高四位:决定 “给仓库装一个减速阀”(时钟分频比)。
	高四位的数值 + 1 = 实际减速倍数(分频值)。比如高四位0000(数值 0),减速倍数 = 0+1=1(不减速);高四位1000(数值 8),减速倍数 = 8+1=9(处理能力降到 1/9)。
	100k 波特率:相当于 “外部送来的快递速度”—— 每小时送 100 件(100k = 每秒 100,000 位数据,类比每小时 100 件)。
	核心公式:内部实际处理速度 = 振荡器频率 ÷ 分频值
	(这里的 “处理速度” 就是内部时钟能支持的最大通信处理能力)
	三者的联系:必须让 “内部处理速度” 远大于 “外部送来的速度”
	就像仓库处理能力必须比外部送快递的速度快,否则会堆积、出错。
	举例(对应 100k 波特率):
	低四位0000 → 振荡器频率 = 4MHz(每小时 4000 件)。
	高四位0000 → 分频值 = 1(不减速)。
	内部实际处理速度 = 4MHz ÷ 1=4MHz(每小时 4000 件)。
	而外部送来的速度是 100k 波特率(每小时 100 件),4MHz(4000 件 / 小时)远大于 100k(100 件 / 小时),完全能处理,不会堆积。
	*/
	MYOLED_WriteCommand(0x00);
	//设置显示区域,多路复用率,多路复用相当于一个64层的楼,是一层一层单独供电,还是同时给多层供电,答案是同时给多层供电
	MYOLED_WriteCommand(0xA8);	//设置多路复用率,告诉屏幕 “我有多少行像素需要控制”
	MYOLED_WriteCommand(0x3F);	//常见的 SSD1306 是 128x64 的屏幕(128 列、64 行)。0xA8 是 “设置行数” 的命令,0x3F 对应的是 64 行(十进制63,下标从0开始)
	//设置显示偏移,默认从顶端开始
	MYOLED_WriteCommand(0xD3);	//设置显示偏移
	MYOLED_WriteCommand(0x00);  //偏移值,默认不偏移,这一个字节的数据都记录向上偏移多少位
	MYOLED_WriteCommand(0x40);  //01 000000,后6位表示从哪一行开始显示,此处为0,表示从下标为0的行开始显示
	//设置显示方向是从左到右,从上到下
	MYOLED_WriteCommand(0xA1);	//设置左右方向,0xA1正常 0xA0左右反置
	MYOLED_WriteCommand(0xC8);	//设置上下方向,0xC8正常 0xC0上下反置
	//设置COM引脚硬件
	/*
	OLED 屏幕的像素是由 “列导线”(SEG 引脚)和 “行导线”(COM 引脚)交叉控制的
	—— 就像一张方格纸,列导线是竖线,行导线是横线,交叉点就是一个像素。
	要让某个像素亮起来,需要给对应的列导线和行导线通特定的电。
	OLED 屏幕的像素是由 “列导线”(SEG 引脚)和 “行导线”(COM 引脚)交叉控制的 —— 就像一张方格纸,列导线是竖线,行导线是横线,交叉点就是一个像素。
	要让某个像素亮起来,需要给对应的列导线和行导线通特定的电。
	*/
	MYOLED_WriteCommand(0xDA);	//设置COM引脚硬件配置指令
	MYOLED_WriteCommand(0x12);	//不同厂家生产的 OLED 屏幕,内部行导线的焊接顺序可能不一样 —— 有的是从屏幕顶部到底部依次接 1~64 行,有的可能反过来(顶部是 64 行,底部是 1 行)。0x12里的特定位会告诉屏幕 “不用反转,按默认顺序接”。
	//设置屏幕亮度
	MYOLED_WriteCommand(0x81);	//设置对比度控制指令
	MYOLED_WriteCommand(0xCF);	//亮度值:范围 0~255,0xCF 是中等偏亮,值越大越亮,但太亮可能耗电快,这里是平衡后的常用值。
	//设置像素充电周期
	MYOLED_WriteCommand(0xD9);	//设置预充电周期指令
	MYOLED_WriteCommand(0xF1);	//OLED 的像素需要 “充电” 才能发光,0xD9 是 “设置充电时间” 的命令,0xF1 是经过测试的合适时间(兼顾亮度和稳定性)。时间太短,像素亮度不足;太长,可能反应变慢。
	//设定“熄灭电压”
	/*
	作用:让像素熄灭时更彻底,不留残影。
	为什么:像素熄灭时需要一个 “截止电压”,
	0xDB 是 “设置这个电压” 的命令,0x30 是常用值。
	电压不合适会导致像素 “关不严”,留下淡淡的影子。
	*/
	MYOLED_WriteCommand(0xDB);	//设置VCOMH取消选择级别
	MYOLED_WriteCommand(0x30);
	//刚才关了屏幕,现在打开屏幕
	/*
	0xA4 是 “正常模式”(数据是 0 就黑,1 就亮);如果用 0xA5,
	屏幕会强制全亮(不管数据是什么),这一步是确保显示内容由数据控制。
	*/
	MYOLED_WriteCommand(0xA4);	//设置整个显示打开/关闭
	//设置正常显示模式
	/*
	作用:确保 “0 是黑、1 是亮”(正常显示)。
	为什么:0xA6 是 “正常模式”;0xA7 是 “反色模式”(0 亮 1 黑)。
	这里用正常模式,符合我们看屏幕的习惯。
	*/
	MYOLED_WriteCommand(0xA6);	//设置正常/倒转显示指令
	//开启 “升压泵”
	/*
	作用:给屏幕提供 “点亮像素的高压”。
	为什么:OLED 的像素需要比供电电压(3.3V)更高的电压才能点亮,0x8D 是 “控制升压泵” 的命令,
	0x14 是 “开启升压泵”(产生高压)。不开启的话,屏幕可能很暗或不亮。
	*/
	MYOLED_WriteCommand(0x8D);	//设置充电泵指令
	MYOLED_WriteCommand(0x14);
	//开启显示指令
	MYOLED_WriteCommand(0xAF);	//开启显示,之前用 0xAE 关了屏幕,现在设置完了,就可以打开了
	MYOLED_Clear();
}
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 "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "MPU6050.h"
#include "MYOLED.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 */
MPU6050Data mpu6050data;
uint8_t ID; //设备ID
/* 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_I2C2_Init();
  /* USER CODE BEGIN 2 */
  MYOLED_SetI2CHandleBeforeInit(&hi2c2);
  MYOLED_Init();
  MPU6050_SetI2CHandleBeforeInit(&hi2c2);
  MPU6050_Init();
  MYOLED_ShowString(0,0,"ID:");
  ID = MPU6050_GetID();
  MYOLED_ShowHexNum(3,0,ID,2);
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	MPU6050_GetData(&mpu6050data);
	MYOLED_ShowSignedNum(0,1,mpu6050data.AccX, 5);
	MYOLED_ShowSignedNum(0,2,mpu6050data.AccY, 5);
	MYOLED_ShowSignedNum(0,3,mpu6050data.AccZ, 5);
	MYOLED_ShowSignedNum(7,1,mpu6050data.GyroX, 5);
	MYOLED_ShowSignedNum(7,2,mpu6050data.GyroY, 5);
	MYOLED_ShowSignedNum(7,3,mpu6050data.GyroZ, 5);
    /* 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 */
实现效果

 
                    
                 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号