当前位置: 首页 > news >正文

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 屏幕。

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

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

相关文章:

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

这个寒假跟着做了一个开源的桌宠&#xff0c;我们来解析下代码&#xff0c;加深理解。 代码中有开源作者的名字。可以去B站搜着跟着做。 首先看下main代码 #include "stm32f10x.h" // Device header #include "Delay.h" #include &quo…...

Kafka偏移量管理全攻略:从基础概念到高级操作实战

#作者&#xff1a;猎人 文章目录 前言&#xff1a;概念剖析kafka的两种位移消费位移消息的位移位移的提交自动提交手动提交 1、使用--to-earliest重置消费组消费指定topic进度2、使用--to-offset重置消费offset3、使用--to-datetime策略指定时间重置offset4、使用--to-current…...

python中使用日期和时间差:datetime模块

datetime模块的表示时间的有 datetime.datetime #时间包含年月日时分秒毫秒 datetime.date #时间只包含年月日 datetime.time #只包含时分秒 获取当前时间 import datetime now datetime.datetime.now() print(now)得到 atetime中的年月日时分秒可以分别取出来 import da…...

申论对策建议类【2022江苏B卷第一题“如何开展网络直播”】

材料&#xff1a; 近年来&#xff0c;公安交管部门通过网络直播&#xff0c;将执法过程和执法细节以视频形式呈现在公众面前&#xff0c;吸引“围观”、组织点评&#xff0c;让执法过程变成一堂生动的法治公开课。 “各位网友&#xff0c;大家好&#xff01;这里是‘全国交通…...

Blazor-父子组件传递任意参数

在我们从父组件传参数给子组件时&#xff0c;可以通过子组件定义的[Parameter]特性的公开属性进行传值&#xff0c;但是当我们需要传递多个值的时候&#xff0c;就需要通过[Parameter]特性定义多个属性&#xff0c;有没有更简便的方式&#xff1f; 我们可以使用定义 IDictionar…...

Python的那些事第二十三篇:Express(Node.js)与 Python:一场跨语言的浪漫邂逅

摘要 在当今的编程世界里,Node.js 和 Python 像是两个性格迥异的超级英雄,一个以速度和灵活性著称,另一个则以强大和优雅闻名。本文将探讨如何通过 Express 框架将 Node.js 和 Python 结合起来,打造出一个高效、有趣的 Web 应用。我们将通过一系列幽默风趣的实例和表格,展…...

win11安装wsl报错:无法解析服务器的名称或地址(启用wsl2)

1. 启用wsl报错如下 # 查看可安装的 wsl --install wsl --list --online此原因是因为没有开启DNS的原因&#xff0c;所以需要我们手动开启DNS。 2. 按照如下配置即可 Google的DNS&#xff08;8.8.8.8和8.8.4.4) 全国通用DNS地址 (114.114.114.114) 3. 运行以下命令来重启 WSL…...

【设计模式】【结构型模式】桥接模式(Bridge)

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f3b5; 当你的天空突…...

1997-2019年各省进出口总额数据

1997-2019年各省进出口总额数据 1、时间&#xff1a;1997-2020年 2、来源&#xff1a;国家统计局、各省年鉴 3、指标&#xff1a;进出口总额 4、范围&#xff1a;31省 5、指标解释&#xff1a;进出口总额‌是指以货币表示的一定时期内一国实际进出口商品的总金额&#xff…...

AI前端开发效率革命:拥抱AI,开启前端开发新纪元

前端开发行业竞争日益激烈&#xff0c;项目交付周期不断缩短&#xff0c;对开发效率的要求也越来越高。在这种高压环境下&#xff0c;开发者们常常面临着巨大的压力。而近年来&#xff0c;人工智能技术的飞速发展&#xff0c;特别是AI写代码工具的出现&#xff0c;为前端开发带…...

Rust编程语言入门教程(一)安装Rust

目录 引言一、为什么要用 Rust&#xff1f;二、与其他语言比较三、Rust 特别擅长的领域四、Rust 与 Firefox五、Rust 的用户和案例六、Rust 的优缺点七、安装 Rust1、访问官网下载 Rust2、下载完成&#xff0c;执行exe文件 八、Rust 安装验证九、开发工具结束语 引言 在当今快…...

Kubernetes控制平面组件:Kubernetes如何使用etcd

云原生学习路线导航页&#xff08;持续更新中&#xff09; kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计&#xff08;一&#xff09;Kubernetes架构原则和对象设计&#xff08;二&#xff09;Kubernetes架构原则和对象设计&#xff08;三&#xff09;Kubernetes控…...

2025年-G4-Lc78--121. 买卖股票的最佳时机--(java版)

1.题目描述 2.思路 思路1: 做两轮排序&#xff0c;第一轮排序找到最小的那个数&#xff0c;然后再判断最小的那个数之后还有其他数吗&#xff0c;如果有在进行排序&#xff0c;选出最大的那个数&#xff0c;然后值相减。 问题要点&#xff1a; &#xff08;1&#xff09;你需要…...

LabVIEW 中的 3dgraph.llb 库

3dgraph.llb 库位于 C:\Program Files (x86)\National Instruments\LabVIEW 2019\vi.lib\Platform 目录下&#xff0c;是 LabVIEW 系统中用于 3D 图形相关操作的重要库。它为 LabVIEW 用户提供了丰富的功能&#xff0c;能在应用程序中创建、显示和交互各种 3D 图形&#xff0c;…...

通过VSCode直接连接使用 GPT的编程助手

GPT的编程助手在VSC上可以直接使用 选择相应的版本都可以正常使用。每个月可以使用40条&#xff0c;超过限制要付费。 如下图对应的4o和claude3.5等模型都可以使用。VSC直接连接即可。 配置步骤如下&#xff1a; 安装VSCODE 直接&#xff0c;官网下载就行 https://code.vis…...

[LeetCode力扣hot100]-C++常用数据结构

0.Vector 1.Set-常用滑动窗口 set<char> ans;//根据类型定义&#xff0c;像vector ans.count()//检查某个元素是否在set里&#xff0c;1在0不在 ans.insert();//插入元素 ans.erase()//删除某个指定元素 2.栈 3.树 树是一种特殊的数据结构&#xff0c;力扣二叉树相…...

2-安装YIUI

YIUI框架&#xff1a;GitHub - LiShengYang-yiyi/YIUI: Unity3D UGUI Framework, 基于UI数据事件绑定为核心 数据驱动的UGUI框架, ETUI框架, ET框架官方推荐UI框架 ET框架&#xff1a;egametang/ET: Unity3D Client And C# Server Framework (github.com) 1 - 安装YIUI框架&a…...

16-使用QtChart创建动态图表:入门指南

QtChart是Qt框架中的一个强大模块&#xff0c;用于创建各种类型的图表&#xff0c;如折线图、柱状图、饼图等。它提供了丰富的API和灵活的配置选项&#xff0c;使得开发者能够轻松地将数据可视化集成到应用程序中。本文将介绍如何使用QtChart创建一个简单的动态折线图&#xff…...

蓝耘智算携手DeepSeek,共创AI未来

&#x1f31f; 各位看官号&#xff0c;我是egoist2023&#xff01; &#x1f30d; 种一棵树最好是十年前&#xff0c;其次是现在&#xff01; &#x1f680; 今天来学习如何通过蓝耘智算使用DeepSeek R1模型 &#x1f44d; 如果觉得这篇文章有帮助&#xff0c;欢迎您一键三连&a…...

具身智能在智能巡检机器人中的应用——以开关柜带电操作机器人为例

随着机器人技术和人工智能的迅速发展&#xff0c;具身智能在各行业的应用日益广泛&#xff0c;尤其是在电力行业中的智能巡检领域。传统的电力巡检和维护工作通常需要人工操作&#xff0c;存在着高温、高压、强电磁场等危险环境&#xff0c;且效率较低。开关柜带电操作机器人作…...

【第4章:循环神经网络(RNN)与长短时记忆网络(LSTM)— 4.6 RNN与LSTM的变体与发展趋势】

引言:时间序列的魔法钥匙 在时间的长河中,信息如同涓涓细流,绵延不绝。而如何在这无尽的数据流中捕捉、理解和预测,正是循环神经网络(RNN)及其变体长短时记忆网络(LSTM)所擅长的。今天,我们就来一场深度探索,揭开RNN与LSTM的神秘面纱,看看它们如何在时间序列的海洋…...

【R语言】回归分析与判别分析

一、线性回归分析 1、lm()函数 lm()函数是用于拟合线性模型&#xff08;Linear Models&#xff09;的主要函数。线性模型是一种统计方法&#xff0c;用于描述一个或多个自变量&#xff08;预测变量、解释变量&#xff09;与因变量&#xff08;响应变量&#xff09;之间的关系…...

git开发流程以及github社区企业版

常规开发流程 1、将仓库 clone 到本地&#xff0c;已经 clone 的要 fetch & pull&#xff0c;保证本地 master 分支已经更新到最新状态 2、在 master 最新分支的基础上 checkout 一个开发分支&#xff0c;分支命名要求规范&#xff0c;如带用户名、日期、bug id 等关键信…...

DeepSeek + Vue实战开发

利用DeepSeek V3模型、siliconflow大模型一站式云服务平台以及vue3.0实现一个在线人工智能客服对话系统。 因为deepseek官网的api密钥使用起来比较缓慢&#xff0c;所以可以使用第三方的&#xff0c;具体操作请自行查阅资料。 siliconflow官网 SiliconFlow, Accelerate AGI …...

从安装软件到flask框架搭建可视化大屏(二)——创建一个flask页面,搭建可视化大屏,零基础也可以学会

附录&#xff1a;所有文件的完整代码 models.py # models/models.py from flask_sqlalchemy import SQLAlchemydb SQLAlchemy()class User(db.Model):__tablename__ user # 显式指定表名为 userid db.Column(db.Integer, primary_keyTrue)username db.Column(db.String(…...

Python编程中,async/await/asyncio分别是干啥的?

在Python异步编程中,async、await和asyncio是三个核心概念。它们共同构成了Python处理高并发I/O密集型任务的解决方案。本文将通过代码实例解析它们的作用和用法。 一、异步编程基础 1.1 同步 vs 异步 同步编程:代码按顺序执行,遇到I/O操作(如网络请求、文件读写)时会阻塞…...

vue非组件的初学笔记

1.创建Vue实例&#xff0c;初始化渲染的核心 准备容器引包创建Vue实例new Vue() el用来指定控制的盒子data提供数据 2.插值表达式 作用利用表达式插值&#xff0c;将数据渲染到页面中 格式{{表达式}} 注意点 表达式的数据要在data中存在表达式是可计算结果的语句插值表达式…...

4.3 学习UVM中的“run_phase“,将其应用到具体案例分为几步?

文章目录 前言1. run_phase 的作用与执行特点2. 关键组件的 run_phase 实现2.1 Driver 的 run_phase&#xff1a;驱动事务2.2 Monitor 的 run_phase&#xff1a;捕获事务2.3 Scoreboard 的 run_phase&#xff1a;数据比对 3. 同步与 Objection 管理3.1 控制仿真结束3.2 多组件协…...

[Python人工智能] 五十.PyTorch入门 (5)快速搭建神经网络及模型保存

从本专栏开始,作者正式研究Python深度学习、神经网络及人工智能相关知识。前文讲解PyTorch构建分类神经网络。这篇文章将介绍如何利用PyTorch快速构建神经网络,之前的代码比较复杂,通过自定义Net类实现,本文通过Torch函数定义神经网络。前面我们的Python人工智能主要以Tens…...

【C语言】有序数组的平方

文章目录 给你一个按非递减顺序排序的整数数组 nums&#xff0c;返回每个数字的平方组成的新数组&#xff0c;要求也按非递减顺序排序。 #include<stdio.h>/*** brief 计算一个整数数组的平方&#xff0c;并按非递减顺序存放结果* * 该函数接受一个整数数组arr和其长度le…...

osgearth视点坐标及鼠标交点坐标的信息显示(七)

核心函数如下: void COSGObject::addViewPointLabel() {//mRoot->addChild(osgEarth::Util::Controls::ControlCanvas::get(mViewer));//放开这句,球就卡住了。 为什么,shitosgEarth::Util::Controls::ControlCanvas* canvas = osgEarth::Util::Controls::ControlCanvas…...

【096】基于51单片机红外线人数统计系统【Proteus仿真+Keil程序+报告+原理图】

☆、设计硬件组成&#xff1a;51单片机最小系统LCD1602液晶显示两路E18-D80NK红外线传感器DS1302时钟芯片AT24C02存储芯片蜂鸣器LED灯按键设置。 1、设计采用STC89C52、AT89C52、AT89S52作为主控芯片&#xff1b; 2、采用DS1302时钟芯片实现对日期和时间的计时&#xff0c;并…...

【ENSP】链路聚合的两种模式

【ENSP】链路聚合的两种模式 1、背景介绍2、链路聚合的使用场景3、配置过程1、手工模式Eth-Trunk配置2、静态LACP模式Eth-Trunk 4、总结 1、背景介绍 随着网络规模的不断扩大&#xff0c;人们对骨干链路的带宽吞吐量和可靠性提出了越来越高的要求。在传统方案中&#xff0c;为…...

机器学习_17 K近邻算法知识点总结

K近邻算法&#xff08;K-Nearest Neighbors&#xff0c;KNN&#xff09;是一种简单而直观的机器学习算法&#xff0c;广泛应用于分类和回归任务。它通过寻找训练集中与新样本最接近的K个样本&#xff08;近邻&#xff09;来进行预测。今天&#xff0c;我们就来深入探讨K近邻算法…...

Web 后端 请求与响应

一 请求响应 1. 请求&#xff08;Request&#xff09; 客户端向服务器发送的HTTP请求&#xff0c;通常包含以下内容&#xff1a; 请求行&#xff1a;HTTP方法&#xff08;GET/POST等&#xff09;、请求的URL、协议版本。 请求头&#xff08;Headers&#xff09;&#xff1a;…...

网络工程师 (44)ARP协议

前言 ARP协议&#xff0c;即地址解析协议&#xff08;Address Resolution Protocol&#xff09;&#xff0c;是一种网络协议&#xff0c;主要用于将网络层的IPv4地址&#xff08;逻辑地址&#xff09;解析为链路层的物理地址&#xff08;通常是MAC地址&#xff09;。 一、基本概…...

使用verilog 实现 cordic 算法 ----- 旋转模式

1-设计流程 ● 了解cordic 算法原理&#xff0c;公式&#xff0c;模式&#xff0c;伸缩因子&#xff0c;旋转方向等&#xff0c;推荐以下链接视频了解 cordic 算法。哔哩哔哩-cordic算法原理讲解 ● 用matlab 或者 c 实现一遍算法 ● 在FPGA中用 verilog 实现&#xff0c;注意…...

搜狗浏览器卸载教程

需求背景 今天发现geek居然无法卸载搜狗浏览器&#xff0c;作为一个老司机&#xff0c;这是不允许的。如果你使用geek或者windows的卸载&#xff0c;或者直接在它的安装包的Uninstall.exe中卸载&#xff0c;他走到100%就一直不动了。那玩意是假的。 卸载教程 结束 -----华丽的…...

ES7 (ES2016) 新特性

目录 Array.prototype.includes()指数运算符与 ES6 的对比实际应用场景最佳实践 Array.includes() 基本语法 array.includes(searchElement[, fromIndex])特点 返回布尔值可以检测 NaN支持可选的 fromIndex 参数比 indexOf() 更语义化 使用示例 const numbers [1, 2, 3…...

设计模式13:职责链模式

系列总链接&#xff1a;《大话设计模式》学习记录_net 大话设计-CSDN博客 1.概述 职责链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为设计模式&#xff0c;它允许将请求沿着处理者链传递&#xff0c;直到有一个处理者能够处理该请求。这种模式通过…...

MongoDB between ... and ... 操作

个人博客地址&#xff1a;​​​​​​​MongoDB between ... and ... 操作 | 一张假钞的真实世界 MongoDB中类似SQL的between and操作可以采用如下语法&#xff1a; db.collection.find( { field: { $gt: value1, $lt: value2 } } );...

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_alloc函数

ngx_alloc 声明在 src\os\unix\ngx_alloc.h 中&#xff1a; void *ngx_alloc(size_t size, ngx_log_t *log); 定义在 src\os\unix\ngx_alloc.c 中&#xff1a; void * ngx_alloc(size_t size, ngx_log_t *log) {void *p;p malloc(size);if (p NULL) {ngx_log_error(NGX_LOG_…...

总结:Helm 命令详解

文章目录 1. Helm 概述2. Helm 的安装与配置2.1 安装 Helm2.2 验证安装 3、Helm 的常用命令3.1 查看帮助3.2 查看 Chart 列表3.3 安装 Chart3.4 卸载 Chart3.5 升级 Chart3.6 回滚 Chart3.7 查看 Chart 详细信息3.8 查看 Chart 的模板3.9 查看 Chart 的值3.10 管理仓库 4. Helm…...

通俗诠释 DeepSeek-V3 模型的 “671B” ,“37B”与 “128K”,用生活比喻帮你理解模型的秘密!

欢迎来到涛涛聊AI。 在DeepSeek-V3模型的参数描述中&#xff0c;你可能会看到类似“671B 37B 128K”这样的标记。这些字母和数字的组合看起来像密码&#xff0c;但其实它们揭示了模型的“大脑容量”和“工作方式”。我们用日常生活的比喻来解释&#xff1a; 一、数字含义&…...

【鸿蒙ArcTS】TypeScript学习记录:函数类型声明与箭头函数

普通函数 function add(x: number, y: number): number {return x y; } 完整未省略版本 const add: (x: number, y: number) > void//函数类型(x: number, y: number): void > {//函数定义console.log("object"); };函数类型&#xff1a; type trigFunc…...

基于 Python 和 Django 的北极星招聘数据可视化系统(附源码,部署)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…...

机器学习_18 K均值聚类知识点总结

K均值聚类&#xff08;K-means Clustering&#xff09;是一种经典的无监督学习算法&#xff0c;广泛应用于数据分组、模式识别和降维等领域。它通过将数据划分为K个簇&#xff0c;使得簇内相似度高而簇间相似度低。今天&#xff0c;我们就来深入探讨K均值聚类的原理、实现和应用…...

LTE参数

RSRP(Reference Singnal Received Power&#xff0c;参考信号接收功率) 是终端接收到的小区公共参考信号(CRS)功率值&#xff0c;数值为测量带宽内单个RE功率的线性平均值&#xff0c;反映的是本小区有用信号的强度。 SINR(SignaltoInterference&Noise Ratio&#xff0c;…...

Word写论文常用操作的参考文章

1.插入多个引用文献&#xff1a;word中交叉引用多篇参考文献格式[1-2]操作以及显示错误问题 更改左域名&#xff0c;输入 \#"[0" 更改右域名&#xff0c;输入 \#"0]" 2.插入题注&#xff1a;word 中添加图片题注、目录、内部链接 3.插入公式编号&#x…...

kubectl exec 实现的原理

kubectl exec 是 Kubernetes 提供的一个命令&#xff0c;它允许你在指定的 Pod 中执行命令&#xff0c;类似于在容器中打开一个终端会话。这个功能对于调试、监控和管理容器化应用非常有用。kubectl exec 的实现涉及到多个 Kubernetes 组件和机制&#xff0c;包括 API Server、…...