25/2/17 <嵌入式笔记> 桌宠代码解析

这个寒假跟着做了一个开源的桌宠,我们来解析下代码,加深理解。

代码中有开源作者的名字。可以去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();}//站立方式2else if(Action_Mode==13){Action_Hello();}//打招呼}
}

这比较好理解,就是导入,初始化,和if语句。

PWM代码

PWM是脉冲宽度调制,是一张通过调节方波脉冲的宽度,即占空比来控制能量传递的一种方式

PWM 的本质是通过**"开"和"关"的快速切换**(即高电平和低电平切换),控制信号输出的平均电压和传递的能量,达到模拟信号控制的效果。

完整代码

#include "stm32f10x.h"                  // Device headervoid 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;//输出比较模式采用PWM1TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High;TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable;TIM_OCInitStructure.TIM_Pulse=0;//初始化CCR的值为0TIM_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);//使能TIM2TIM_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 工作原理

  1. TIM2 和 TIM3 定时器负责计时,周期由 ARR 和 PSC 决定。
  2. 将定时器的输出复用到 GPIO 引脚,实现 PWM 信号输出。
  3. 通过改变 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));	//等待计数到0SysTick->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_usDelay_msDelay_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;        // 响应优先级
核心概念:
  1. 抢占优先级(Preemption Priority)

    • 决定了当两个中断同时发生时,哪个任务先被 CPU 执行。
    • 数字越小,优先权越高;抢占优先级越高的中断可以打断优先级低的中断。
  2. 响应优先级(SubPriority)

    • 当两个中断抢占优先级相同时,响应优先级决定哪个中断先执行。
  3. USART1 的优先级比 USART3 更高

    • 在代码中,USART1 的抢占优先级为 1USART3 的为 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; // 浮空输入模式
核心概念:
  1. TX(传输引脚)设置成复用推挽输出模式

    • 意义:用于发送数据,要保证信号强度,驱动能力强。
    • 推挽输出模式意味着引脚在高电平和低电平之间切换,这种模式能提供较高的驱动电流,适合通信。
  2. RX(接收引脚)设置成浮空输入模式

    • 意义:用于接收数据,浮空输入使引脚状态完全由外部设备驱动。
  3. 配置 USART 通信的总线、波特率、校验方式等:

    • 串口初始化时需要设置通信参数,如波特率(例如 9600bps)、数据位数(8 位)、校验位(无校验)等。

3. 中断系统

知识点:中断的作用

  • 中断是一种硬件机制,可以暂停 CPU 当前的任务,优先执行紧急任务。
  • 优点:
    1. 节省 CPU 资源:CPU 不需要一直轮询外设是否有新数据,而是等外设发出中断信号时再处理。
    2. 提升实时性:高优先级任务不需要等待,能及时响应,例如接收数据
void USART1_IRQHandler(void)
{if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET){Res = USART_ReceiveData(USART1);// 根据接收到的数据调用对应控制逻辑USART_ClearITPendingBit(USART1, USART_IT_RXNE);}
}
核心概念:
  1. USART_IT_RXNE

    • 指的是 USART 数据寄存器非空标志,表示有新数据被接收。
    • 中断触发条件:串口硬件检测到有数据到达时,触发中断。
  2. USART_ReceiveData() 读取数据

    • 从串口数据寄存器中获取发送给 MCU 的数据字节。
  3. 清除中断标志 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;elseSpeedDelay = 200;break;
}
核心概念:
  1. 指令分类

    • 每种指令对应一个功能,例如 0x29 是宠物机器人趴下,0x38 是增加运动速度。
    • 更改的两个全局变量:
      • Face_Mode:控制机器人表情(例如通过 LED 显示表情或舵机控制动作)。
      • Action_Mode:控制机器人具体动作(例如前进、转弯、摇摆等)。
  2. 状态和速度调整:

    • 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. 总体代码结构与工作流程

模块划分

这段代码涉及两个主要的模块:

  1. PWM 驱动模块(PWM_InitPWM_SetCompareX

    • 负责初始化 PWM 定时器,通过改变占空比输出指定 PWM 信号。
    • 不同的舵机占用 MCU 的不同 PWM 通道,例如 Compare1Compare2Compare3 对应 MCU 内部的独立 PWM 输出。
  2. 舵机控制模块(Servo_InitServo_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 表现不同于普通外设,需要区分 命令 和 数据

    1. OLED_WriteCommand: 向 OLED 发送控制命令,用于初始化或配置显示特性。
    2. 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 条件分支实现,具体流程如下:

  1. 清空 OLED 显示屏:

    • 每次切换表情前,调用 OLED_Clear() 将整个屏幕置空,防止残留的像素影响显示。

     2.根据 Face_Mode 的值选择相应的表情图像:

  1. 将选定表情图像显示到屏幕上:

    • 调用 OLED_ShowImage(0, 0, 128, 64, Face_X) 将图像数据加载到显存。
    • 使用 OLED_Update() 确保显存中的数据同步到 OLED 屏幕。

这就是所有模块的概述分析。

明天在整体讲解一下我的理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/895686.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qt中基于开源库QRencode生成二维码(附工程源码链接)

目录 1.QRencode简介 2.编译qrencode 3.在Qt中直接使用QRencode源码 3.1.添加源码 3.2.用字符串生成二维码 3.3.用二进制数据生成二维码 3.4.界面设计 3.5.效果展示 4.注意事项 5.源码下载 1.QRencode简介 QRencode是一个开源的库&#xff0c;专门用于生成二维码&…

【Android开发】华为手机安装包安装失败“应用是非正式版发布版本,当前设备不支持安装”问题解决

问题描述 我们将Debug版本的安装包发送到手机上安装&#xff0c;会发现华为手机有如下情况 解决办法 在文件gradle.properties中粘贴代码&#xff1a; android.injected.testOnlyfalse 最后点击“Sync now”&#xff0c;等待重新加载gradle资源即可 后面我们重新编译Debug安装…

前端面试手写--虚拟列表

目录 一.问题背景 二.代码讲解 三.代码改装 四.代码发布 今天我们来学习如何手写一个虚拟列表,本文将把虚拟列表进行拆分并讲解,然后发布到npm网站上. 一.问题背景 为什么需要虚拟列表呢?这是因为在面对大量数据的时候,我们的浏览器会将所有数据都渲染到表格上面,但是渲…

vue项目本地svg图标使用

提前准备&#xff1a; 1、一个本地的svg图片 这个直接从网上找一个就行 2、文件整体目录 安装插件 npm i vite-plugin-svg-iconsvite.config.ts中配置插件 完整代码 import { fileURLToPath, URL } from node:url import { resolve } from path import { defineConfig } f…

Go: 使用VS Code配置Go项目支持Windows与Linux双系统调试

在现代软件开发中&#xff0c;越来越多的开发者开始使用VS Code等集成开发环境&#xff08;IDE&#xff09;来提高生产力&#xff0c;特别是在支持远程开发时。VS Code的远程SSH功能&#xff0c;使得开发者可以在本地Windows电脑上&#xff0c;通过远程SSH连接到Linux服务器&am…

萌新学 Python 之集合 set

集合 set&#xff1a;使用一对大括号&#xff0c;元素写在大括号之间&#xff0c;使用逗号分隔 集合中的元素只能是不可变的数据类型&#xff0c;不能是列表、字典和集合 set1 {1, 2, 3} set2 {1, a, (1, 2, 3)} print(type(set1), type(set2)) # <class set> <c…

python中使用数据库sqlite3

Python使用sqlite3数据库 python3.x标准库内置了SQLite3 查看sqlite的版本 import sqlite3 sqlite_version sqlite3.sqlite_version print(f"SQLite version: {sqlite_version}") 显示 导入模块连接sqlitte3 import sqlite3 consqlite3.connect("d:/fi…

maven使用默认settings.xml配置时,Idea基于pom.xml更新依赖时报错,有些组件下载时连接超时

1、问题背景&#xff1a;maven使用默认settings.xml配置时&#xff0c;Idea基于pom.xml更新依赖时报错&#xff0c;有些组件下载时连接超时&#xff0c; 通过日志发下&#xff0c;去连接maven.org网站下载依赖&#xff0c;有时候肯定会超时。 2、解决办法&#xff1a;使用国外…

小狐狸ai3.1.2版本源码无授权版本内 含搭建教程+各种上线教程

内容目录 一、详细介绍小狐狸3.1.2版本源码&#xff0c;新增deepseek接口 文件夹说明&#xff1a; 1、后端&#xff1a;文件夹是后台文件 5、.sql文件是数据库文件后台安装步骤&#xff1a; 1、在宝塔新建个站点&#xff0c;php版本使用7.4&#xff0c;将“后端”文件夹里的文件…

C#之上位机开发---------C#通信库及WPF的简单实践

〇、上位机&#xff0c;分层架构 界面层 要实现的功能&#xff1a; 展示数据 获取数据 发送数据 数据层 要实现的功能&#xff1a; 转换数据 打包数据 存取数据 通信层 要实现的功能&#xff1a; 打开连接 关闭连接 读取数据 写入数据 实体类 作用&#xff1a; 封装数据…

Python Django 入门教程

Django 构建一个完整的博客平台,包含用户认证、评论、权限控制等功能。 环境搭建 安装依赖 先确保已经安装了 Python 环境,并通过 pip 安装 Django 及其它依赖: pip install django pip install djangorestframework创建项目和应用 创建一个 Django 项目并进入该项目目录:…

今日行情明日机会——20250217

2025年02月17日行情 后续投资机会分析 根据最新盘面信息&#xff0c;以下板块和个股具备潜在投资机会&#xff0c;需结合市场动态和基本面进一步验证&#xff1a; 1. 腾讯系AI&#xff08;18家涨停&#xff09; 核心逻辑&#xff1a;涨停家数最多&#xff08;18家&#xff0…

常见的IP地址分配方式有几种:深入剖析与适用场景‌

在数字互联的世界里&#xff0c;IP地址如同网络世界的“门牌号”&#xff0c;是设备间通信的基础。随着网络技术的飞速发展&#xff0c;IP地址的分配方式也日趋多样化&#xff0c;以适应不同规模、不同需求的网络环境。本文将深入探讨当前主流的几种IP地址分配方式&#xff0c;…

快速搭建 OLLAMA + DeepSeek 模型并对接 Cherry Studio

快速搭建 OLLAMA DeepSeek 模型并对接 Cherry Studio 本文将指导您在基于 GPU 的 Ubuntu 服务器上快速搭建 OLLAMA&#xff0c;部署 DeepSeek 模型&#xff0c;并接入 Cherry Studio 进行使用。 环境说明 GPU 服务器: GeForce RTX 2080 Ti, 16 核, 64G 内存系统: Ubuntu 24.…

floodfill算法系列一>扫雷游戏

目录 题目思路&#xff1a;代码设计&#xff1a;代码呈现&#xff1a; 题目思路&#xff1a; 代码设计&#xff1a; 代码呈现&#xff1a; class Solution {int m,n;int[] dx {0,0,-1,1,-1,-1,1,1};int[] dy {-1,1,0,0,-1,1,-1,1};public char[][] updateBoard(char[][] boa…

Java 中操作文件

文章目录 Java 中操作文件File 概述属性构造方法方法代码示例文件内容的读写--数据流InputStream 概述常用方法FileInputStream 概述构造方法常用方法代码示例通过 Scanner 进行字符读取FileReaderOutputStream 概述常用方法FileOutputStream 概述构造方法常用方法代码示例File…

什么是RDD以及它在Spark中的作用

RDD&#xff08;Resilient Distributed Dataset&#xff09;&#xff0c;即弹性分布式数据集&#xff0c;是Spark中最基本的数据抽象。以下是对RDD的详细解释以及它在Spark中的作用&#xff1a; 一、RDD的定义与特性 定义&#xff1a; RDD是一个不可变、可分区、里面的元素可并…

【附带脚本】解决notion加载慢问题

问题原因 notion网站的服务器在国外&#xff0c;因为网络问题&#xff08;国际出口带宽限制&#xff09;导致访问速度较慢和域名解析延迟等问题。 解决方案 通过在 hosts 文件中直接指定一个更快的 IP 地址&#xff08;例如国内镜像服务器&#xff09;&#xff0c;可以显著提…

Banana Pi OpenWRT One 官方路由器的第一印象

OpenWRT One是OpenWRT开源社区推出的首款官方开发板&#xff0c;与Banana Pi社区共同设计&#xff0c;由Banana Pi制造和发行。路由器采用蓝色铝合金外壳&#xff0c;质感极佳&#xff0c;视觉效果远超宣传图。整体设计简洁&#xff0c;呈长方形&#xff0c;虽然不是特别时尚&a…

【信息学奥赛一本通 C++题解】1285:最大上升子序列和

信息学奥赛一本通&#xff08;C版&#xff09;在线评测系统 基础算法 第一节 动态规划的基本模型 1285&#xff1a;最大上升子序列和 “最大上升子序列和”问题课堂讲解 1. 理解题意 同学们&#xff0c;想象我们有一串数字&#xff0c;就像一串彩色的珠子&#xff0c;每个珠子…