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

news2025/3/14 4:40:11

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

代码中有开源作者的名字。可以去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 工作原理

  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));	//等待计数到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_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;
        else
            SpeedDelay = 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.coloradmin.cn/o/2300275.html

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

相关文章

油田安全系统:守护能源生命线的坚固壁垒

油田安全系统&#xff1a;不可或缺的能源护盾 在能源领域&#xff0c;油田作为国家重要的能源供应基地&#xff0c;其安全生产的重要性不言而喻。油田安全系统犹如一道坚固的护盾&#xff0c;全方位守护着人员生命、企业财产以及生态环境&#xff0c;是油田平稳运行与可持续发展…

【故障处理】- 执行命令crsctl query crs xxx一直hang

【故障处理】- 执行命令crsctl query crs xxx一直hang 一、概述二、故障处理三、解决方法 一、概述 Oracle RAC环境中&#xff0c;遇到执行crsctl query crs xxx等相关命令不返回任何结果&#xff0c;一直hang在那里。系统下执行命令ps -ef |grep crsctl query crs softwarever…

JMeter工具介绍、元件和组件的介绍

Jmeter功能概要 JDK常用文件目录介绍 Bin目录&#xff1a;存放可执行文件和配置文件 Docs目录&#xff1a;是Jmeter的API文档&#xff0c;用于开发扩展组件 printable_docs目录&#xff1a;用户帮助手册 lib目录&#xff1a;存放JMeter依赖的jar包和用户扩展所依赖的Jar包…

DeepSeek 引领AI 大模型时代,服务器产业如何破局进化?

2025 年 1 月&#xff0c;DeepSeek - R1 以逼近 OpenAI o1 的性能表现&#xff0c;在业界引起轰动。其采用的混合专家架构&#xff08;MoE&#xff09;与 FP8 低精度训练技术&#xff0c;将单次训练成本大幅压缩至 557 万美元&#xff0c;比行业平均水平降低 80%。这一成果不仅…

安卓burp抓包,bypass ssl pinning

好久好久没有发东西了。主要是懒。。。 这几天在搞apk渗透&#xff0c;遇到了burp无法抓包问题&#xff0c;觉得可以写下来。 问题描述 1. 一台安卓手机&#xff0c;装了面具&#xff0c;可以拿到root 2. 电脑上有burp&#xff0c;设置代理 3.手机和电脑连同一个网段&…

服务器中部署大模型DeepSeek-R1 | 本地部署DeepSeek-R1大模型 | deepseek-r1部署详细教程

0. 部署前的准备 首先我们需要足够算力的机器&#xff0c;这里我在vultr中租了有一张A16显卡一共16GB显存的服务器作为演示。部署的模型参数为14b的。如果需要部署满血版本671b的&#xff0c;需要更大的算力支持&#xff0c;这里由于是个人资金有限&#xff0c;就演示14b的部署…

rust学习笔记2-rust的包管理工具Cargo使用

首先先解决一个配置文件&#xff0c;目前rust版本升级后&#xff0c;config已经改成 config.toml 内容也做了如下调整 [source.crates-io] replace-with tuna[source.tuna] registry "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" 1.Rust 编程…

DeepSeek + Vue实战开发

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

【数据结构】(8) 二叉树

一、树形结构 1、什么是树形结构 根节点没有前驱&#xff0c;其它节点只有一个前驱&#xff08;双亲/父结点&#xff09;。所有节点可以有 0 ~ 多个后继&#xff0c;即分支&#xff08;孩子结点&#xff09;。每个结点作为子树的根节点&#xff0c;这些子树互不相交。 2、关于…

Web 后端 请求与响应

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

CEF132 编译指南 Linux 篇 - CEF 编译实战:构建 CEF(六)

1. 引言 经过前几篇的精心准备&#xff0c;我们已经完成了所有必要的环境配置和源码下载。现在&#xff0c;我们将进入激动人心的 CEF 编译阶段。本篇将详细指导你在 Linux 系统上编译 CEF 6834 分支&#xff08;对应 Chromium 132 版本&#xff09;&#xff0c;包括创建项目文…

【Spring+MyBatis】_图书管理系统(上篇)

目录 1. MyBatis与MySQL配置 1.1 创建数据库及数据表 1.2 配置MyBatis与数据库 1.2.1 增加MyBatis与MySQL相关依赖 1.2.2 配置application.yml文件 1.3 增加数据表对应实体类 2. 功能1&#xff1a;用户登录 2.1 约定前后端交互接口 2.2 后端接口 2.3 前端页面 2.4 单…

【苍穹外卖】学习

软件开发整体介绍 作为一名软件开发工程师,我们需要了解在软件开发过程中的开发流程&#xff0c; 以及软件开发过程中涉及到的岗位角色&#xff0c;角色的分工、职责&#xff0c; 并了解软件开发中涉及到的三种软件环境。那么这一小节&#xff0c;我们将从 软件开发流程、角色…

DeepSeek-V2-技术文档

DeekSeek-v2-简述 1. DeepSeek-V2是什么? DeepSeek-V2是一个基于混合专家(Mixture-of-Experts,简称MoE)架构的语言模型。它是一种新型的人工智能模型,专门用于处理自然语言处理(NLP)任务,比如文本生成、翻译、问答等。与传统的语言模型相比,DeepSeek-V2在训练成本和…

Linux中线程创建,线程退出,线程接合

线程的简单了解 之前我们了解过 task_struct 是用于描述进程的核心数据结构。它包含了一个进程的所有重要信息&#xff0c;并且在进程的生命周期内保持更新。我们想要获取进程相关信息往往从这里得到。 在Linux中&#xff0c;线程的实现方式与进程类似&#xff0c;每个线程都…

什么是蒸馏技术

蒸馏技术&#xff08;Knowledge Distillation, KD&#xff09;是一种模型压缩和知识迁移的方法&#xff0c;旨在将一个复杂模型&#xff08;通常称为“教师模型”&#xff09;的知识转移到一个小型模型&#xff08;通常称为“学生模型”&#xff09;中。蒸馏技术的核心思想是通…

Python——寻找矩阵的【鞍点】(教师:恒风)

在矩阵中&#xff0c;一个数在所在行中是最大值&#xff0c;在所在列中是最小值&#xff0c;则被称为鞍点 恒风的编程 思路&#xff1a; 使用while循环找到行中最大值&#xff0c;此时列的坐标已知&#xff0c;利用列表推导式生成列不变的纵列&#xff0c;利用min()函数得到纵…

处理项目中存在多个版本的jsqlparser依赖

异常提示 Correct the classpath of your application so that it contains a single, compatible version of net.sf.jsqlparser.statement.select.SelectExpressionIte实际问题 原因&#xff1a;项目中同时使用了 mybatis-plus 和 pagehelper&#xff0c;两者都用到了 jsqlpa…

【iOS】包大小和性能稳定性优化

包大小优化 图片 LSUnusedResources 扫描重复的图片 ImageOptim,压缩图片 压缩文件 优化音视频资源 &#xff0c;使用MP3 代替 WAV ffmpeg -i input.mp3 -b:a 128k output.mp3 视频 H.265&#xff08;HEVC&#xff09; 代替 H.264 ffmpeg ffmpeg -i input.mp4 -vcodec lib…

Jenkinsdebug:遇到ERROR: unable to select packages:怎么处理

报错信息&#xff1a; 报错信息解释&#xff1a; musl-1.2.5-r0 和 musl-dev-1.2.5-r1: 这里说明 musl-dev 需要一个特定版本的 musl&#xff0c;即 musl1.2.5-r1&#xff0c;但是当前版本的 musl&#xff08;1.2.5-r0&#xff09;并不满足这个条件。版本冲突: 当尝试安装新…