【自学嵌入式: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号