这个寒假跟着做了一个开源的桌宠,我们来解析下代码,加深理解。
代码中有开源作者的名字。可以去B站搜着跟着做。
首先看下main代码
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "BlueTooth.h"
#include "Servo.h"
#include "PetAction.h"
#include "Face_Config.h"
//作者是Sngels_wyh只在抖音与B站
int main(void)
{
Servo_Init();
OLED_Init();//OLED初始化
BlueTooth_Init();//蓝牙初始化
OLED_ShowImage(0,0,128,64,Face_sleep);
OLED_Update();
while(1)
{
if(Action_Mode==0){Action_relaxed_getdowm();WServo_Angle(90);}//放松趴下
else if(Action_Mode==1){Action_sit();}//坐下
else if(Action_Mode==2){Action_upright();}//站立
else if(Action_Mode==3){Action_getdowm();}//趴下
else if(Action_Mode==4){Action_advance();}//前进
else if(Action_Mode==5){Action_back();}//后退
else if(Action_Mode==6){Action_Lrotation();}//左转
else if(Action_Mode==7){Action_Rrotation();}//右转
else if(Action_Mode==8){Action_Swing();}//摇摆
else if(Action_Mode==9){Action_SwingTail();}//摇尾巴
else if(Action_Mode==10){Action_JumpU();}//前跳
else if(Action_Mode==11){Action_JumpD();}//后跳
else if(Action_Mode==12){Action_upright2();}//站立方式2
else if(Action_Mode==13){Action_Hello();}//打招呼
}
}
这比较好理解,就是导入,初始化,和if语句。
PWM代码
PWM是脉冲宽度调制,是一张通过调节方波脉冲的宽度,即占空比来控制能量传递的一种方式
PWM 的本质是通过**"开"和"关"的快速切换**(即高电平和低电平切换),控制信号输出的平均电压和传递的能量,达到模拟信号控制的效果。
完整代码
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//开启TIM2时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//开启TIM3时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启GPIOA时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出模式
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_6;//默认PA0是TIM2通道1的复用,PA1是TIM2通道2的复用所以开启这俩IO口...
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);//TIM2切换为内部定时器
TIM_InternalClockConfig(TIM3);//TIM3切换为内部定时器
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;//不分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStructure.TIM_Period=20000-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=72-1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1;//输出比较模式采用PWM1
TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse=0;//初始化CCR的值为0
TIM_OC1Init(TIM2,&TIM_OCInitStructure);//TIM2复用通道1开启
TIM_OC2Init(TIM2,&TIM_OCInitStructure);//TIM2复用通道2开启
TIM_OC3Init(TIM2,&TIM_OCInitStructure);//TIM2复用通道3开启
TIM_OC4Init(TIM2,&TIM_OCInitStructure);//TIM2复用通道4开启
TIM_OC1Init(TIM3,&TIM_OCInitStructure);//TIM2复用通道1开启
TIM_Cmd(TIM2,ENABLE);//使能TIM2
TIM_Cmd(TIM3,ENABLE);//使能TIM3
}
//作者是Sngels_wyh只在抖音与B站
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);//设置CCR1的值
}
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare);//设置CCR2的值
}
void PWM_SetCompare3(uint16_t Compare)
{
TIM_SetCompare3(TIM2, Compare);//设置CCR3的值
}
void PWM_SetCompare4(uint16_t Compare)
{
TIM_SetCompare4(TIM2, Compare);//设置CCR4的值
}
void PWM_WSetCompare(uint16_t Compare)
{
TIM_SetCompare1(TIM3, Compare);//设置尾巴CCR1的值
}
1. 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
知识点:STM32 的时钟树和时钟管理
STM32 的外设,比如定时器 TIM2 和 GPIO,只能在时钟信号可用时工作。你需要 手动启用相应外设的时钟,否则代码运行时会出错。
- 每个外设的时钟来源于 RCC 模块内的时钟树:
- TIM2 和 TIM3 是挂在 APB1(低速外设总线)上的外设。
- GPIOA 则挂在 APB2(高速外设总线)上。
- 本质上,调用
RCC_APB1PeriphClockCmd
或RCC_APB2PeriphClockCmd
函数,就是打开对应模块的开关。
结合到代码:为什么需要时钟?
- GPIO 时钟用于引脚初始化,配置它们为输入、输出或者复用模式。
- TIM2 和 TIM3 时钟决定定时器模块的时序计数。如果没有打开时钟,TIM2、TIM3 这些模块将无法产生计数,PWM 也就无法工作。
2. 配置 GPIO 引脚
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
知识点:GPIO 模式和定时器输出
- GPIO 引脚可以有多种模式,比如输入、输出、复用功能等。
- 在这里,
GPIO_Mode_AF_PP
表示配置为 复用功能推挽输出:- 复用功能(AF):GPIO 不处理普通输入输出,而是作为定时器或其他模块的专用引脚(TIM2 的 PWM 信号通过这些引脚输出)。
- 推挽输出(Push Pull):信号切换速度快,适合 PWM 这样高速信号的需求。
如何结合到代码?
-
代码中配置了 PA0 ~ PA3 和 PA6 为 TIM2 和 TIM3 的 PWM 信号输出引脚。为什么这么分配?
- PA0 用于 TIM2_CH1(定时器 2 的通道 1),PA1、PA2、PA3 类似。
- 你可以查 STM32F103 的管脚功能表,找到 GPIO 引脚和定时器通道之间的复用关系。
- PA6 是 TIM3 的通道 1,一个单独的尾巴 PWM。
-
注意:GPIO_Speed 设置为 50MHz 是为了提升引脚响应速度。在 PWM 应用中,推挽输出的响应速度直接会影响高频信号的输出质量。
3. 配置定时器时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); // 配置 TIM2
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); // 配置 TIM3
知识点:PWM 的频率由 TIM_Period 和 TIM_Prescaler 决定
- PWM 的周期(也就是信号从高到低变化的时间)取决于两个参数:
- TIM_Period(ARR):定时器的自动重装载值。计数器从 0 数到这个值时,完成一个周期。
- TIM_Prescaler(PSC):用于对时钟进行预分频,减慢计数速度。
- 定时器工作频率的计算公式:
结合到代码:如何配置 PWM 的周期?
- 代码中
TIM_Prescaler = 72 - 1
,假设系统时钟 f系统=72 MHz,则计数器的工作频率为:
- (每秒计数 1,000,000 次)。
TIM_Period = 20000 - 1
,所以 PWM 信号的频率为
这在舵机和电机控制中是一种标准频率。
4. 配置定时器 PWM 输出通道
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 设置 PWM 模式 1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 高电平有效
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; // 初始 CCR 值设为 0
TIM_OC1Init(TIM2, &TIM_OCInitStructure); // TIM2 通道 1
TIM_OC2Init(TIM2, &TIM_OCInitStructure); // TIM2 通道 2
...
知识点:PWM 模式和比较寄存器
- PWM 模式 1(TIM_OCMode_PWM1):
- 当计数值 CNT<CCRCNT<CCR 时,输出 High(高电平)。
- 当计数值 CNT≥CCRCNT≥CCR 时,输出 Low(低电平)。
CCR
是 比较寄存器 的值,控制占空比。
结合到代码:比较值如何影响占空比?
- 假设当前
ARR = 20000
,并将CCR1 = 1500
:- 占空比 = CCR1/ARR=1500/20000=7.5%
- PWM 信号会保持 7.5% 的时间为高电平,92.5% 的时间为低电平。
- TIM_OCPolarity 设置为 TIM_OCPolarity_High,表示高电平为有效信号。
5. 调整占空比
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
知识点:实时改变占空比
- 通过修改
TIM2->CCR1
的值,随时调整 TIM2_CH1 输出的占空比。
总结:STM32 的 PWM 工作原理
- TIM2 和 TIM3 定时器负责计时,周期由
ARR
和PSC
决定。 - 将定时器的输出复用到 GPIO 引脚,实现 PWM 信号输出。
- 通过改变
CCR
值实时调整占空比,从而控制设备的速度、亮度或者角度等。
延时函数
#include "stm32f10x.h"
/**
* @brief 微秒级延时
* @param xus 延时时长,范围:0~233015
* @retval 无
*/
void Delay_us(uint32_t xus)
{
SysTick->LOAD = 72 * xus; //设置定时器重装值
SysTick->VAL = 0x00; //清空当前计数值
SysTick->CTRL = 0x00000005; //设置时钟源为HCLK,启动定时器
while(!(SysTick->CTRL & 0x00010000)); //等待计数到0
SysTick->CTRL = 0x00000004; //关闭定时器
}
/**
* @brief 毫秒级延时
* @param xms 延时时长,范围:0~4294967295
* @retval 无
*/
void Delay_ms(uint32_t xms)
{
while(xms--)
{
Delay_us(1000);
}
}
/**
* @brief 秒级延时
* @param xs 延时时长,范围:0~4294967295
* @retval 无
*/
void Delay_s(uint32_t xs)
{
while(xs--)
{
Delay_ms(1000);
}
}
代码中包含了三个函数:Delay_us
、Delay_ms
和Delay_s
,分别用于实现微秒级、毫秒级和秒级的延时。
蓝牙模块
代码通过两个串口(USART1 和 USART3)分别接收语音模块和蓝牙模块发来的指令,并根据接收到的命令,切换 "面部表情" 和执行不同的 "动作模式"。
1. NVIC 中断优先级配置
知识点:NVIC
- NVIC 全称是 Nested Vectored Interrupt Controller(嵌套向量中断控制器)。
- 它是 ARM Cortex-M 核心中的一个模块,用来管理嵌套中断系统。
- 它决定了不同中断的执行顺序(由优先级决定),并支持多级中断嵌套的实现。
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 响应优先级
核心概念:
-
抢占优先级(Preemption Priority):
- 决定了当两个中断同时发生时,哪个任务先被 CPU 执行。
- 数字越小,优先权越高;抢占优先级越高的中断可以打断优先级低的中断。
-
响应优先级(SubPriority):
- 当两个中断抢占优先级相同时,响应优先级决定哪个中断先执行。
-
USART1 的优先级比 USART3 更高:
- 在代码中,
USART1
的抢占优先级为1
,USART3
的为2
。 - 如果语音模块(
USART1
)和蓝牙模块(USART3
)同时接收到数据,语音指令会被先处理。
- 在代码中,
2. GPIO 初始化与串口通信
知识点:串口通信(UART/USART)
- 串口通信是一种常见的数据传输方式,允许两个设备之间传输字节流数据。
- 关键引脚:
- TX(Transmit):发送数据。
- RX(Receive):接收数据。
- 在 MCU 中,串口通常用于与外部传感器、PC 或无线模块(如蓝牙)通信。
GPIO 配置代码:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入模式
核心概念:
-
TX(传输引脚)设置成复用推挽输出模式:
- 意义:用于发送数据,要保证信号强度,驱动能力强。
- 推挽输出模式意味着引脚在高电平和低电平之间切换,这种模式能提供较高的驱动电流,适合通信。
-
RX(接收引脚)设置成浮空输入模式:
- 意义:用于接收数据,浮空输入使引脚状态完全由外部设备驱动。
-
配置 USART 通信的总线、波特率、校验方式等:
- 串口初始化时需要设置通信参数,如波特率(例如 9600bps)、数据位数(8 位)、校验位(无校验)等。
3. 中断系统
知识点:中断的作用
- 中断是一种硬件机制,可以暂停 CPU 当前的任务,优先执行紧急任务。
- 优点:
- 节省 CPU 资源:CPU 不需要一直轮询外设是否有新数据,而是等外设发出中断信号时再处理。
- 提升实时性:高优先级任务不需要等待,能及时响应,例如接收数据
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
Res = USART_ReceiveData(USART1);
// 根据接收到的数据调用对应控制逻辑
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
核心概念:
-
USART_IT_RXNE:
- 指的是 USART 数据寄存器非空标志,表示有新数据被接收。
- 中断触发条件:串口硬件检测到有数据到达时,触发中断。
-
USART_ReceiveData()
读取数据:- 从串口数据寄存器中获取发送给 MCU 的数据字节。
-
清除中断标志
USART_ClearITPendingBit()
:- 每次中断处理后,必须手动清除标志位,否则中断会一直触发,导致程序无法执行其他任务。
4. switch-case 和命令解析
知识点:指令解析
- 串口接收到的每个数据帧(字节)都是一个指令,通过
switch-case
或其他方式进行解析,执行对应的功能。
switch (Res)
{
case 0x29:
Face_Mode = 0;
Action_Mode = 0;
break;
case 0x38:
if (SpeedDelay > 120)
Face_Mode = 3;
Face_Config();
if (SpeedDelay > 100)
SpeedDelay -= 20;
else
SpeedDelay = 200;
break;
}
核心概念:
-
指令分类:
- 每种指令对应一个功能,例如
0x29
是宠物机器人趴下,0x38
是增加运动速度。 - 更改的两个全局变量:
Face_Mode
:控制机器人表情(例如通过 LED 显示表情或舵机控制动作)。Action_Mode
:控制机器人具体动作(例如前进、转弯、摇摆等)。
- 每种指令对应一个功能,例如
-
状态和速度调整:
SpeedDelay
是控制运动速度的延时时间,延时越短,运动越快。- 每次接收到对应指令后,减少延时,逐步加速。
- 当速度过快时,逻辑会重置为初始值(如 200),避免速度过快失控。
舵机模块
基于 PWM 驱动,控制多个舵机(左上、右上、左下、右下、尾巴)的转动角度。代码中核心是通过改变 PWM 的占空比来设置舵机的目标角度。
1. 定义基础函数:Servo_Init
void Servo_Init()
{
PWM_Init();
}
- 这个函数是舵机初始化函数,调用了
PWM_Init()
来初始化 PWM(脉冲宽度调制)模块。 - PWM 的作用: PWM 模块通过生成周期性信号,将特定占空比的脉冲信号发送给舵机。舵机会根据接收到的脉冲信号调节自己的目标角度。
2. 核心函数:Servo_AngleX
void Servo_Angle1(float Angle)//左上
{
PWM_SetCompare1(Angle / 180 * 2000 + 500);
}
(1) 舵机角度驱动的原理
- 市面上常见的舵机(如 SG90)通常使用 PWM 信号控制角度。
- 舵机角度范围通常是 0°-180°,而它响应的 PWM 脉冲宽度范围是 500μs 到 2500μs。
- 500μs:舵机移动到最小角度(0°)。
- 2500μs:舵机移动到最大角度(180°)。
(2) 公式解释
Angle / 180 * 2000 + 500
这个公式的作用是将舵机目标角度 Angle
转换为对应的 PWM 脉冲宽度:
Angle / 180
:将角度归一化到 0 到 1 的比例值。* 2000
:将归一化的比例值映射为脉冲宽度(从 0 到 2000 μs)。+ 500
:加上最小的基础脉宽(500 μs),以覆盖舵机的实际工作区间(500-2500 μs)。
也就是说:
- 当
Angle = 0°
:PWM宽度=0/180×2000+500=500 μs - 当
Angle = 180°
:PWM宽度=180/180×2000+500=2500 μs
通过改变输入角度值,就能调整 PWM 输出信号,从而控制舵机的转动。
3. 舵机单个控制的函数:
(1) 左上舵机(Servo_Angle1
):
void Servo_Angle1(float Angle)//左上
{
PWM_SetCompare1(Angle / 180 * 2000 + 500);
}
作用:
- 调用
PWM_SetCompare1
,将期望角度转换为相应的 PWM 输出,使左上舵机转动到指定角度。
4. 总体代码结构与工作流程
模块划分
这段代码涉及两个主要的模块:
-
PWM 驱动模块(
PWM_Init
,PWM_SetCompareX
):- 负责初始化 PWM 定时器,通过改变占空比输出指定 PWM 信号。
- 不同的舵机占用 MCU 的不同 PWM 通道,例如
Compare1
、Compare2
、Compare3
对应 MCU 内部的独立 PWM 输出。
-
舵机控制模块(
Servo_Init
,Servo_Angle1
等):- 调用 PWM 模块的 API,将实际需要的角度转换为 PWM 信号宽度,从而直接控制舵机。
舵机控制运动模块
这段代码定义了一个使用舵机控制的「四足机器人」的各类动作,包括站立、趴下、移动、转向、摇摆、跳跃等。它通过调整舵机角度(调用 Servo_AngleX
函数),实现机器人腿部(舵机1到4)和尾巴(舵机尾巴控制)的复杂运动。
这些函数定义了机器人最基本的姿态,包括站立、趴下、坐下等。它们为更复杂的动作奠定了基础。
举个例子:趴下 - Action_relaxed_getdowm()
void Action_relaxed_getdowm(void)
{
Servo_Angle1(20);
Servo_Angle2(20);
Delay_ms(150);
Servo_Angle3(160);
Servo_Angle4(160);
}
- 前腿(舵机1、舵机2)向前伸到 20°。
- 后腿(舵机3、舵机4)向后扳到 160°,使机器人呈趴下的休息姿势。
前进 - Action_advance()
void Action_advance(void)//前进
{
while(Action_Mode==4)
{
// 前腿右甩+后腿左甩
Servo_Angle2(45);
Servo_Angle3(45);
Delay_ms(SpeedDelay);
if(Action_Mode!=4)break;
// 前腿左甩+后腿右甩
Servo_Angle1(135);
Servo_Angle4(135);
Delay_ms(SpeedDelay);
if(Action_Mode!=4)break;
// 回归站立位置
Servo_Angle2(90);
Servo_Angle3(90);
Delay_ms(SpeedDelay);
if(Action_Mode!=4)break;
Servo_Angle1(90);
Servo_Angle4(90);
Delay_ms(SpeedDelay);
if(Action_Mode!=4)break;
// 另一侧腿交替动起来
Servo_Angle1(45);
Servo_Angle4(45);
Delay_ms(SpeedDelay);
if(Action_Mode!=4)break;
Servo_Angle2(135);
Servo_Angle3(135);
Delay_ms(SpeedDelay);
if(Action_Mode!=4)break;
Servo_Angle1(90);
Servo_Angle4(90);
Delay_ms(SpeedDelay);
if(Action_Mode!=4)break;
Servo_Angle2(90);
Servo_Angle3(90);
Delay_ms(SpeedDelay);
if(Action_Mode!=4)break;
}
}
- 两对腿交替做前后摆动,用「对角线方式」前进。
- 右前腿(舵机2)+左后腿(舵机3)先向前摆,左前腿(舵机1)+右后腿(舵机4)后摆。
- 动作完成后回到站立位置。
- 另一对对角线腿重复动作,以继续前进。
OLED 显示屏模块
代码可以分为以下几个模块:
1. 底层通信控制
-
I²C通信实现:
OLED 屏幕通过 I²C 协议进行通信,OLED_GPIO_Init
函数初始化了 I²C 接口引脚 (GPIOB_Pin8
和GPIOB_Pin9
),并通过以下三大函数完成 I²C 信号的发送:OLED_I2C_Start
: 发送起始条件。OLED_I2C_Stop
: 发送停止条件。OLED_I2C_SendByte
: 按位发送一个字节数据。
-
命令与数据写入:
OLED 表现不同于普通外设,需要区分 命令 和 数据:OLED_WriteCommand
: 向 OLED 发送控制命令,用于初始化或配置显示特性。OLED_WriteData
: 向 OLED 写入要显示的内容。
总结: 这一部分定义了OLED硬件级功能抽象,间接操作OLED的显存和功能单元。
2. 内存显存管理
操作 OLED 显示的核心是显存 OLED_DisplayBuf
,所有绘制操作仅作用于此虚拟显存,只有调用 OLED_Update
函数时,才会将显存内容同步到 OLED 屏。
-
显存更新:
- 全屏更新:
OLED_Update
遍历显存中所有数据,并将其发送到 OLED。 - 区域更新:
OLED_UpdateArea
对显存的任意矩形区域进行更新,以提高效率。
- 全屏更新:
-
显存操作:
OLED_Clear
:清空整个屏幕。OLED_ClearArea
:对指定区域清零。
优势: 在显存中先行处理数据,可以最大限度减少 I²C 的传输负担。
3. 基础绘图函数
所有复杂图形的绘制均基于像素操作,OLED_DrawPoint
是最基本的单位,结合以下功能可满足基本绘图需求:
-
点操作:
OLED_DrawPoint
:在屏幕指定位置点亮一个像素。OLED_GetPoint
:读取显存中某个像素是否被点亮。
-
直线绘制:
OLED_DrawLine
使用 Bresenham 算法 绘制高效直线,并支持水平线、垂直线和斜线的生成。
4. 复杂图形绘制
这一部分提供丰富的几何图形绘制方法,适用于各种场景。
-
矩形绘制:
函数OLED_DrawRectangle
同时支持填充 (OLED_FILLED
) 和空心 (OLED_UNFILLED
) 两种样式。 -
圆形绘制:
使用 Bresenham 圆形算法 实现高效的圆形绘制。OLED_DrawCircle
和OLED_DrawEllipse
进一步扩展到椭圆绘制,并支持填充模式。 -
三角形绘制:
可以通过OLED_DrawTriangle
为三角形指定三个顶点,并支持三角形填充。 -
角弧绘制:
提供OLED_DrawArc
绘制扇形或部分环形。通过起始角和终止角参数(-180°到180°),可以实现精确的角度绘制。
5. 字符与图像显示
-
字符显示:
字符通过字模库 (OLED_F8x16
和OLED_F6x8
) 显示两种不同大小字体。OLED_ShowChar
负责单字符显示,OLED_ShowString
可处理字符串。 -
数字显示:
提供多种格式的数字显示:- 普通整数:
OLED_ShowNum
- 有符号整数:
OLED_ShowSignedNum
- 浮点数:
OLED_ShowFloatNum
- 十六进制:
OLED_ShowHexNum
- 二进制:
OLED_ShowBinNum
- 普通整数:
-
图像显示:
使用OLED_ShowImage
绘制任意图像,支持不规则形状和任意大小(图像定义通过外部数组传入)。
6. 其他功能
-
区域取反:
OLED_Reverse
和OLED_ReverseArea
提供显存区域内像素的取反功能,用于特效处理。 -
多边形内点判断:
函数OLED_pnpoly
判断某点是否位于多边形内部,常用于复杂形状的填充。
1. 初始化
在调用任何显示功能之前,必须执行 OLED_Init
初始化 OLED 硬件。
OLED_Init();
2. 基础绘图
以下代码显示了如何在屏幕上画一个点、线和矩形:
OLED_DrawPoint(10, 10); // 点亮(10,10)位置的像素
OLED_DrawLine(0, 0, 127, 63); // 画一条从左上到右下的对角线
OLED_DrawRectangle(20, 20, 40, 30, OLED_FILLED); // 绘制一个填充的矩形
OLED_Update(); // 显示绘图结果
3. 显示文本
以下代码绘制字符串和数字:
OLED_ShowString(0, 0, "Hello, OLED!", OLED_8X16); // 显示字符串
OLED_ShowNum(0, 16, 12345, 5, OLED_8X16); // 显示整数
OLED_Update(); // 显示更新
4. 绘制图形
以下代码显示了如何绘制圆形和椭圆:
OLED_DrawCircle(64, 32, 15, OLED_FILLED); // 绘制一个填充的圆形
OLED_DrawEllipse(64, 32, 20, 10, OLED_UNFILLED); // 绘制一个未填充的椭圆
OLED_Update();
表情模块
通过 OLED 显示屏实现了表情的变化显示,根据不同的 Face_Mode
值,切换预定义的表情图像(从 Face_sleep
到 Face_hello
等)。
代码逻辑流程
代码采用 if-else 条件分支实现,具体流程如下:
-
清空 OLED 显示屏:
- 每次切换表情前,调用
OLED_Clear()
将整个屏幕置空,防止残留的像素影响显示。
- 每次切换表情前,调用
2.根据 Face_Mode
的值选择相应的表情图像:
-
将选定表情图像显示到屏幕上:
- 调用
OLED_ShowImage(0, 0, 128, 64, Face_X)
将图像数据加载到显存。 - 使用
OLED_Update()
确保显存中的数据同步到 OLED 屏幕。
- 调用
这就是所有模块的概述分析。
明天在整体讲解一下我的理解。