嵌入式16届省赛复盘记录

蓝桥杯嵌入式第十六届省赛复盘记录,这个复盘会围绕一下几点来进行。

  1. 赛题分析
  2. 模板建立
  3. 赛题逻辑分析
  4. 可优化之处

赛题分析

首先清楚赛题需要使用到那些硬件资源,LCD,LED,按键是不必多说的,接下来的就是两路模拟信号,PWM和输入捕获了
请添加图片描述


接下来大概过一遍题目

请添加图片描述

请添加图片描述

  • LCD屏幕的三个界面,监控界面,统计界面和参数界面。
  • 接下来就是按键的功能,切换界面,切换参数,按键加和按键减。
  • 最后就是LED的指示功能。

模板建立

模板使用的西风的模板,感兴趣的可以b站搜索“Alice_西风”进行了解。模板不会从新建工程开始。只会进行部分的说明。

引脚的分配就如下图所示

请添加图片描述

其中有一点需要注意的是:由于LED和LCD有8个引脚是共用的,所以千万不要完了配置PD2,PD2是锁存器的引脚,如果不配置的话,LED会全亮

  • ADC的采集使用的DMA循环采集然后求平均值的形式

请添加图片描述

这里需要把连续采集和DMA连续采集给使能。

  • 输入捕获同样是使用的DMA采集的形式去做,这里有一个小插曲,由于比赛的时候我的预分频值没有改,最后捕获的频率只有几十Hz,debug打断点浪费了很多的时间,所以配置完之后一定要检查一下,不然到时候写上逻辑的时候,不知道是配置的问题还是代码有问题,这样整个人的心态就会崩了。

请添加图片描述

模式选择的是复位模式,由于我使用的是定时器2的通道1,所以触发源选的是TI1FP1,时钟源选的是内部时钟,通道1选的是输入捕获直接模式,补充:这里的其它通道可以选择输入捕获间接模式,触发边沿可以改为与直接模式相反,这样就可以测得PWM的周期和频率,这个在某套国赛题亦有体现。

  • PWM的配置基本是一样的,这里就不多说。

请添加图片描述

这里有一点需要提醒一下,像图中这样配置,在PWM的频率等于1kHz的时候,这里的arr值会溢出,因为这里使用的定时器是16位定时器,最大记录值为65535,频率的计算公式如下
实际输出频率=时钟频率(预分频器值+1)×(ARR+1) 实际输出频率= \frac{\text{时钟频率}}{(\text{预分频器值} + 1) \times (\text{ARR} + 1)} 实际输出频率=(预分频器值+1)×(ARR+1)时钟频率
在不进行分频的情况下,arr+1的值是80000>65535,所以这里需要进行分频处理。

以下是模板中的代码

void pwm_freq_set(uint32_t freq)
{
	
	uint32_t TIM_CLK = 80000000;
	
	uint8_t Prescaler = 1;
	
	uint32_t arr_value = TIM_CLK / (freq * Prescaler) - 1;
	
	float duty = 0;
	
	if(arr_value > 65535)
	{
		Prescaler = (arr_value + 65535) / 65535;
		arr_value = TIM_CLK / (freq * Prescaler) - 1;
	}
	
	Prescaler_MY = Prescaler;
	
	duty = (float)TIM3->CCR2 / (float)(TIM3->ARR + 1);
	
	TIM3->ARR = arr_value;
	
	TIM3->CCR2 = duty * (arr_value + 1);
	
	__HAL_TIM_PRESCALER(&htim3,Prescaler - 1);
	
	TIM3->EGR = TIM_EGR_UG;
	
}

以上就是模板建立需要注意的一些点。

赛题逻辑分析

这里赛题逻辑分析,不会去仔细去研究按键的功能怎么,LCD屏幕怎么显示,这里只会对其中的一部分进行分析。

  • 将电压映射到分散的点上,实现固定步长阶梯式调节
image-20250512160923083

观察图片,可以看到0 ~ 3.3V有未知数量的点,这些点都对应着不同的占空比,需要注意的是,这些点之间不是连续的,而是离散的点。接下来将对这个图进行详细的分析。

  1. 确定 PWM 参数的总调节范围和总步数:
    • 占空比从 10% 调节到 DR,步长为 DS
    • 首先需要计算初始值到最大步长的差值,然后使用这个差值除以步长DS,得出这个N是多少(代码中需要进行加一处理,因为图中是从0开始算起的)
  2. 将电压范围映射到总步数:
    • 接下来要计算每一步电压的变化量,使用3.3V/N,就可以得到每一步的电压是多少。
    • 这意味着,电压每变化 3.3V/N,对应的 PWM 参数就应该“走”一个步长 (DSFS)。
  3. 根据当前电压计算目标步数:
    • 通过将当前电压除以 voltage_per_step 来计算当前电压对应的“目标步数”。
  4. 确定目标 PWM 值:
    • 一旦确定了目标步数,就可以计算出当前电压对应的最终目标 PWM 值:
      • 目标占空比值 = 初始占空比值 + (DS * duty_target_step)
      • 目标频率值 = 初始频率值 + (FS * freq_target_step)

上面这几步就是这个图的逻辑,以下是代码实现。

//步进处理函数

/*
	1.计算n
	2.计算需要步进的次数N
*/

uint16_t duty_step_count;
uint16_t freq_step_count;

float voltage_duty_step;
float voltage_freq_step;

uint16_t number_duty;
uint16_t number_freq;

uint16_t duty_set_count;
uint16_t freq_set_count;

uint16_t duty_set_value;//初始值
uint16_t freq_set_value;

uint16_t duty_flag;
uint16_t freq_flag;

void step_proc_func(void)
{
static uint16_t duty_old = 0;
static uint16_t freq_old = 0;
static uint8_t number_duty_old = 0;
static uint8_t number_freq_old = 0;

duty_step_count = (duty_freq_set[1] - 10) / duty_freq_set[0];
freq_step_count = (duty_freq_set[3] - 1000) / duty_freq_set[2];

//使用n计算每次需要步进的电压
voltage_duty_step = 3.3f / (float)duty_step_count;
voltage_freq_step = 3.3f / (float)freq_step_count;

//使用每次步进的电压计算需要步进的次数N
number_duty = (uint16_t)(voltage[0] / voltage_duty_step);
number_freq = (uint16_t)(voltage[1] / voltage_freq_step);

if(sys_state_index) //解锁状态
{
    if(number_duty != number_duty_old)
    {
        number_duty_old = number_duty;
        pwm_duty_set(10);
        duty_flag = 1;
    }
    if(number_freq != number_freq_old)
    {
        number_freq_old = number_freq;
        pwm_freq_set(1000);
        freq_flag = 1;
    }
    if(((duty_set_count++) < number_duty + 1) && duty_flag)
    {
        duty_set_value += duty_freq_set[0];
        if(duty_set_value >= duty_freq_set[1])
        {
            duty_set_value = duty_freq_set[1];
        }
        if(duty_old != duty_set_value)
        {
            pwm_duty_set((float)(duty_set_value + 10));
            duty_old = duty_set_value;
        }
    }
    else
    {
        duty_set_count = 0;
        duty_set_value = 0;
        duty_flag = 0;
    }
    if(((freq_set_count++) < number_freq + 1) && freq_flag)
    {
        freq_set_value += duty_freq_set[2];
        if(freq_set_value >= duty_freq_set[3])
        {
            freq_set_value = duty_freq_set[3];
        }

        if(freq_old != freq_set_value)
        {
            pwm_freq_set(freq_set_value + 1000);
            freq_old = freq_set_value;
        }
    }
    else
    {
        freq_set_value = 0;
        freq_set_count = 0;
        freq_flag = 0;
    }
}
}
  • 其实赛后重新做一遍看,感觉这届省赛不是很难,可能就是一些细节上的问题,比如判断“异常”状态的时候,要注意负数的处理,如果这个频率差值函数使用的是无符号变量定义,这时候就会出现溢出。
  • 还有系统运行时间是可以使用RTC的。

可优化之处

这个优化主要是对步进函数进行的优化,简单的if...else判断使用状态机的形式去进行判断。

但是这里是有一个问题的,我电压值由a到b,中间会计算出其它的步数,而这些步数也会去触发这个步进状态,这就导致电压值从a到b的这个过程中,会触发两次这个步进状态。

//状态机思想步进函数
void step_func_pro(void)
{
	// 添加用于去抖动的变量
	static uint16_t stable_number_duty = 0;
	static uint16_t stable_number_freq = 0;
	static uint32_t last_voltage_change_time = 0;
	const uint32_t VOLTAGE_DEBOUNCE_TIME_MS = 500; // 去抖动时间,可以根据需要调整

	
	static uint16_t duty_old = 0;
	static uint16_t freq_old = 0;
	static uint16_t number_duty_old = 0;
	static uint16_t number_freq_old = 0;
	
	switch(pwm_setp_state)
	{
		case STEPPING_STATE_LOCKED:
			if(sys_state_index)
			{
				// 重置稳定值,准备进入INIT
				stable_number_duty = (uint16_t)(voltage[0] / voltage_duty_step); // 使用当前值初始化稳定值
				stable_number_freq = (uint16_t)(voltage[1] / voltage_freq_step);
				last_voltage_change_time = 0; // 重置去抖动计时器
				pwm_setp_state = STEPPING_STATE_INIT;
			}
		break;
		
		case STEPPING_STATE_IDLE:
			//判定是否上锁
			if(sys_state_index != 1) //上锁
			{
				pwm_setp_state = STEPPING_STATE_LOCKED;
				break;
			}
			// 根据稳定的电压计算值准备进行步进
			if(stable_number_duty != number_duty_old)
			{
					number_duty_old = stable_number_duty;
					pwm_duty_set(10); // 在步进前设置一个基础值
					duty_flag = 1;
					duty_set_count = 0; // 重置占空比步进计数
					duty_set_value = 0; // 重置占空比设置值
			} else {
					 duty_flag = 0; // 本次不需要占空比步进
			}
			
			if(stable_number_freq != number_freq_old)
			{
					number_freq_old = stable_number_freq;
					pwm_freq_set(1000); // 在步进前设置一个基础值
					freq_flag = 1;
					freq_set_count = 0; // 重置频率步进计数
					freq_set_value = 0; // 重置频率设置值
			} else {
					freq_flag = 0; // 本次不需要频率步进
			}
			
			// 如果需要占空比或频率步进,则切换到STEPPING状态
			if(duty_flag || freq_flag)
			{
					pwm_setp_state = STEPPING_STATE_STEPPING;
			} else {
					// 没有需要步进的,回到INIT状态继续监控电压变化
					pwm_setp_state = STEPPING_STATE_INIT;
			}
			
		break;
		
		case STEPPING_STATE_STEPPING:
			if(((duty_set_count++) < number_duty + 1) && duty_flag)
			{
				duty_set_value += duty_freq_set[0];
				if(duty_set_value >= duty_freq_set[1])
				{
					duty_set_value = duty_freq_set[1];
				}
				if(duty_old != duty_set_value)
				{
					pwm_duty_set((float)(duty_set_value + 10));
					duty_old = duty_set_value;
				}
			}
			else
			{
				duty_set_count = 0;
				duty_set_value = 0;
				duty_flag = 0;
			}
			
			if(((freq_set_count++) < number_freq + 1) && freq_flag)
			{
				freq_set_value += duty_freq_set[2];
				if(freq_set_value >= duty_freq_set[3])
				{
					freq_set_value = duty_freq_set[3];
				}
				
				if(freq_old != freq_set_value)
				{
					pwm_freq_set(freq_set_value + 1000);
					freq_old = freq_set_value;
				}
			}
			else
			{
				freq_set_value = 0;
				freq_set_count = 0;
				freq_flag = 0;
			}
			
			//当频率和占空比步进完成的时候,切换状态
			if((duty_flag != 1) && (freq_flag != 1))
			{
				pwm_setp_state = STEPPING_STATE_INIT;
			}
		break;
		
		case STEPPING_STATE_INIT:
			duty_step_count = (duty_freq_set[1] - 10) / duty_freq_set[0];
			freq_step_count = (duty_freq_set[3] - 1000) / duty_freq_set[2];
			
			//使用n计算每次需要步进的电压
			voltage_duty_step = 3.3f / (float)duty_step_count;
			voltage_freq_step = 3.3f / (float)freq_step_count;
		
			//使用每次步进的电压计算需要步进的次数N
			number_duty = (uint16_t)(voltage[0] / voltage_duty_step);
			number_freq = (uint16_t)(voltage[1] / voltage_freq_step);
			
			// 检查当前的计算值是否与稳定的值不同
			if(number_duty != stable_number_duty || number_freq != stable_number_freq)
			{
					// 如果检测到变化,启动或更新去抖动计时器
					if(last_voltage_change_time == 0) // 第一次检测到变化时记录时间
					{
							last_voltage_change_time = uwTick;
					}

					// 检查变化是否已经持续了一定的时间 (去抖动时间)
					if(uwTick - last_voltage_change_time >= VOLTAGE_DEBOUNCE_TIME_MS)
					{
							// 电压值稳定且与之前不同,更新稳定值并准备进入IDLE进行步进
							stable_number_duty = number_duty;
							stable_number_freq = number_freq;
							last_voltage_change_time = 0; // 重置去抖动计时器

							// 检查是否上锁,如果未上锁则进入IDLE准备步进
							if (sys_state_index == 1)
							{
									 pwm_setp_state = STEPPING_STATE_IDLE;
							}
							// 如果上锁,则停留在INIT,等待解锁后进入IDLE
					}
					// 否则:电压仍在波动或去抖动时间未到,停留在INIT
			}
			else
			{
					// 电压值与稳定的值相同,重置去抖动计时器
					last_voltage_change_time = 0;
			}
		break;
	}
}
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐