蓝桥杯

435 阅读23分钟

1.学会看手册

1.手册的了解

关于后三个手册,我们可以简单理解为

  • 编程手册——内核,不涉及外设
  • 参考手册——外设和寄存器
  • 数据手册——引脚功能描述

2.CT117E-M4产品手册

  • 开发板的一个使用说明书了。在里面列举了板子上所有的硬件连接情况。

4.数据手册

  • P16,时钟树
  • P33,定时器

5.每一个手册都要有一定理解

  • 注意,比如,13届考题翻转LCD显示,需要查看LCD手册,搜方向,找寄存器,看手册(手册上有

2.GPIO

image.png

1.输出

  • 从左到右
  • 主要由两个MOS管控制,可分为推挽,开漏,关闭三种模式
  • 需要注意蓝桥杯比赛中,只有I2C需要配置为开漏输出,其他全部为推挽输出。
    • 开漏输出:P-MOS不工作,具有“线与"功能->外部上拉电阻的大小决定了逻辑电平转换的速度,阻值越小,延时越小,功耗越高,反之,亦然。当延时有要求时,使用下降沿输出
      • 优点:可硬件,实现“线与"功能(“线与”允许多个设备通过开漏输出共同驱动一条线,且只有在所有设备都处于低电平状态时,信号线才会被拉低)(可实现5v供电)
      • 缺点:在开漏配置下,GPIO 引脚可以拉低(输出低电平),但不能主动拉高(输出高电平)
    • 推挽输出:两个MOS管均有效,可输出高低电平
      • 优点:负载能力强,速度快
      • 缺点:存在短路风险

2.输入

  • 从右往左

  • 分为4种模式

    • 上拉输入: 如果上面导通,下面断开,就是上拉输入模式 ,默认为高电平的输入模式,
    • 下拉输入:如果下面导通,上面断开,就是下拉输入模式 ,默认为低电平的输入模式

    • 浮空输入:如果两个都断开,就是 浮空输入(极易受外界干扰) (//使用浮空输入的时间,当外部输入信号功率很小,内部上拉电阻可能会影响到这个输入信号)

    • 高阻态模拟输入模式:最上面的一个,未通过施密特触发器(本质上,实现的是规范输入的作用),冲用于模拟输入

3.LED模块

image.png

  • 通过原理图,我们可以发现,LED灯的左侧通过一个电阻接到了VDD,右侧连接到了SN74HC573,然后连接到了PC8-15引脚。那么,如果我们想让LED灯亮,就需要在右侧输出一个低电平,这样电路才会导通。

  • 同时需要注意,SN74HC573为锁存器,需要将PD2置低电平,才能实现通路

  • 点灯时,注意将数据左移8位(寄存器思想)。

  • & 全1为1 | 有1为1 ^ 相同为0不同为1

  • 最好的led模块代码(推荐记忆)

void LED_Disp(uint8_t x)
{
	
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);
	HAL_GPIO_WritePin(GPIOC,x<<8,GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);

}
void LED_Proc(uint8_t num,uint8_t sta)
{
	uint8_t postion=0x01 <<(num-1);
	uint8_t led_sta =0x10;
	led_sta=(led_sta&(~postion))|(postion*sta);
            LED_Disp(led_sta);	
}

4.按键

  • 注意要消抖

  • 按键最好一开始配置为上拉输入,并且使用定时器来配置

  • 最基本按键处理函数

if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==0)
	{
		HAL_Delay(10);
		if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)==0)
		{
			//按键按下执行的操作
		}
		
	}
  • 外部中断法(不推荐PA0和PB0占用了同一个外部中断通道,因此只能用3个按键
    • cubemx配置为Falling edge trigger mode
    • Pull_up 默认拉高
    • 注意在,nvic中勾选中断
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
	if(GPIO_Pin==GPIO_PIN_0)
	{
		//HAL_Delay(10);中断函数中不能有延时函数,如果一定要使用最好在cubemx中将system tick优先级升高,超过中断的优先级
		if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==0)
		{
			//按键按下执行的操作
		}
	}
}
 

  • 定时器法,长按,短按实例代码:
struct keys
{
	uchar judge_sta;
	bool key_sta;
	bool single_sta;//bool short_flag;
	bool long_flag;
	

	uint key_time;
};
#include "interrupt.h"

struct keys key[4]={0,0,0};
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM3)//判断
	{
		key[0].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
		key[1].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
		key[2].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
		key[3].key_sta=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
		
		for(int i=0;i<4;i++)
		{
			switch(key[i].judge_sta)
			{	
				case 0: 
				{
					if(key[i].key_sta==0)//判断是否被按下
					{
						key[i].judge_sta=1;
						key[i].key_time=0;
					}
				}
				break;
				case 1://实现了消抖
				{
					if(key[i].key_sta==0)
					{
						key[i].judge_sta=2;
						//key[i].single_sta=1;//确实按下了
						
					}
					else
						key[i].judge_sta=0;
				}
				break;
				case 2:
				{
					if(key[i].key_sta==1)//当按键松开
					{
						key[i].judge_sta=0;
						if(key[i].key_time<=100)//10ms定时器对按键进行扫描1次
						{
							key[i].single_sta=1;//确定位短按
						}
					}
					else//当按键没有松开
					{
						key[i].key_time++;//记录按下时间
						if(key[i].key_time>100)//10ms定时器对按键进行扫描1次
						{
							key[i].long_flag=1;//确定位长按
						}
					}	
				}
				break;
			}	
		}
	}
}
//注意在主函数中加上
extern struct keys key[];

//开启定时器中断
HAL_TIM_Base_Start_IT(&htim6);//button tim

  • 定时器第二种(推荐)
struct keys
{
	uint8_t age;
	uint8_t flag;
	uint8_t Long_flag;
	uint8_t press;
	
};
uint8_t key_read()
{
	if(HAL_GPIO_ReadPin(GPIOB ,GPIO_PIN_0)==0) return 1;
	else if(HAL_GPIO_ReadPin(GPIOB ,GPIO_PIN_1)==0) return 2;
	else if(HAL_GPIO_ReadPin(GPIOB ,GPIO_PIN_2)==0) return 3;
	else if(HAL_GPIO_ReadPin(GPIOA ,GPIO_PIN_0)==0) return 4;
	
	else return 0;
	
}
//常用长按函数
void key_serv_long()
{
	uint8_t key_value=key_read();
	if(key_value!=0)// 当按下
	{
		M_key[key_value].age++;
		if(M_key[key_value].age>5 ) //实现了消抖
			M_key[key_value].press=1;
		
		
	}
	else //当松开
	{
		 
		for(int i=0;i<5;i++) 
		{
			if(key[i].age>100&&key[i].press==1 ) M_key[i].Long_flag=1;//需要知
                        道当长短按都需要判断时,有必要将长按判断置前
			if(M_key[i].press==1&&M_key[i].Long_flag==0)M_key[i].flag=1;//短按
		
			//长按

			M_key[i].age=0;
			M_key[i].press=0;
			//M_key[i].Long_flag=0;//注意题目要求,看放在哪合适
		}
	}
	 
}
//双击+长按(长按快速增加)+短按
void key_serv_double()
{
	uint8_t key_value=key_read();
	if(key_value!=0)// 当按下
	{
		M_key[key_value].age++;
		if(M_key[key_value].age>5 ) //实现了消抖
			M_key[key_value].press=1;
		
		
	}
	else //当松开
	{
		 
		for(int i=0;i<5;i++) 
		{
                        //`顺序不能变`
			if(M_key[i].double_ageEn==1&&M_key[i].press==1)//当双击,必须写在前面
			{
				M_key[i].double_flag=1;
				M_key[i].double_ageEn=0;
				M_key[i].double_age=0;
				M_key[i].press=0;
			}
			if(M_key[i].press==1&&M_key[i].Long_flag==0) M_key[i].double_ageEn=1;
			if(M_key[i].double_ageEn==1) M_key[i].double_age++;
			if(M_key[i].double_ageEn==1&&M_key[i].double_age>20)//当间隔时间过长
			{
				M_key[i].flag=1;
				M_key[i].double_ageEn=0;
				M_key[i].double_age=0;
			}
			
				 
			M_key[i].age=0;
			M_key[i].press=0;
			M_key[i].Long_flag=0;
		}
	}
	//在按下的同时判断
	if(M_key[key_value].age>100) M_key[key_value].Long_flag=1;//长按
}

//注意在主函数中加上
extern struct keys key[];

//开启定时器中断
HAL_TIM_Base_Start_IT(&htim6);//button tim

5.LCD

  • LCD移植库函数,将LCD提到的GPIO引脚设置为推挽输出,然后调用函数即可

  • 常见lcd和按键组合例程(注意将,这两个函数写入循环中)

void key_proc(void)
{
	if(key[0].single_sta==1)
	  {
		  LCD_Clear(Black);//最好将清屏放入按键判断中,放在外面会导致在循环中不断清屏
		  view=!view;
 		  key[0].single_sta=0;
	  }
	 else if(key[1].single_sta==1)
	  {
		  PA6_Duty+=10;
		  if(PA6_Duty>90)
		  {
			  PA6_Duty=10;
		  }
		  //__HAL_TIM_SET_COMPARE(&htim16, TIM_CHANNEL_1,PA6_Duty);//设置占空比
		  TIM16->CCR1=PA6_Duty;
		  key[1].single_sta=0;
	  }
	  else if(key[2].single_sta==1)
	  {
		  PA7_Duty+=10;
		  if(PA7_Duty>90)
		  {
			  PA7_Duty=10;
		  }
		  //__HAL_TIM_SET_COMPARE(&htim17, TIM_CHANNEL_1,PA7_Duty);//设置占空比
		  TIM17->CCR1=PA7_Duty;
		  key[2].single_sta=0;
	  }
	  
}

void lcd_proc(void)
{
	if(view==0)
	{
	  
	 
 	char str[21];
 	 sprintf(str,"       DATA");
	LCD_DisplayStringLine(Line0, ( unsigned char *)str); 
			
	}
	if(view==1)
	{
	 
 	char str[21];
 	 sprintf(str,"       Para");
	LCD_DisplayStringLine(Line0, ( unsigned char *)str);
		
 	 sprintf(str,"    PA6 :%d",PA6_Duty);
	LCD_DisplayStringLine(Line2, ( unsigned char *)str); 
	 sprintf(str,"    PA7 :%d ",PA7_Duty);
	LCD_DisplayStringLine(Line4, ( unsigned char *)str); 
	}
}


  • 需要注意LED和LCD共用PC8-PC15,因此控制信号不能同时有效
    • 本文是解决 同时在 定时器中点灯 与 LCD屏幕显示 冲突异常的问题
    • 我们大家都知道,G431RBT6开发板上led与lcd是冲突的,所以在lcd.c文件中的这三个函数中

void LCD_WriteReg(u8 LCD_Reg, u16 LCD_RegValue)
void LCD_WriteRAM_Prepare(void)
void LCD_WriteRAM(u16 RGB_Code)

的头跟尾分别加这两句话

uint16_t temp = GPIOC->ODR;//头
GPIOC->ODR=temp;//尾

就可以避免冲突。

但是,如果有定时器的参与点灯的话是否也正常呢? 答案是不行!

2进制显示
  • 想要二进制显示只能使用void LCD_DisplayChar(u8 Line, u16 Column, u8 Ascii) 函数
  • 第一个参数写Linex(0~10),第二个参数写第x列(360-(16*x )),第三个写ascii码

3.TIM

1.输入捕获

image.png

  • 从模式设置为RESET,这样测出来的频率值才不会跳
//回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
        {
            c_value1a = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1) + 1; // 总时间
            frq1 = (80000000 / 80) / c_value1a;
            HAL_TIM_IC_Start(htim, TIM_CHANNEL_1); // 重新启动通道 1 捕获
        }
        else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
        {
            c_value1b = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2) + 1; // 高电平时间
            A_Duty = (float)c_value1b / (float)c_value1a; // 计算占空比
            HAL_TIM_IC_Start(htim, TIM_CHANNEL_2); // 重新启动通道 2 捕获
        }
        __HAL_TIM_SetCounter(htim, 0); // 清零计数器
    }
    if (htim->Instance == TIM3)
    {
        if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
        {
            c_value2a = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1) + 1; // 总时间
            frq2 = (80000000 / 80) / c_value2a;
            HAL_TIM_IC_Start(htim, TIM_CHANNEL_1); // 重新启动通道 1 捕获
            __HAL_TIM_SetCounter(htim, 0); // 清零计数器
        }
		 else if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
        {
            c_value2b = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2) + 1; // 高电平时间
            Duty2 = (float)c_value2b / (float)c_value2a; // 计算占空比
            HAL_TIM_IC_Start(htim, TIM_CHANNEL_2); // 重新启动通道 2 捕获
        }
		 __HAL_TIM_SetCounter(htim, 0);  // 清零计数器
    }
}


2.定时中断

  • 中断频率=输入频率/PSC+1/ARR+1
  • 中断轮询时间=1/中断频率
  • 配置定时中断要点:
    • 配置时基单元
    • 在NVIC中勾选中断使能
    • 在主函数中有没有开启中断 HAL_TIM_Base_Start_IT(&htim4);//开启按键中断
    • 看看中断函数有没有写错,或者看函数里是否有延时函数(如果有修改system tick timer 优先级,调高)

3.输出比较(PWM产生)

  • PWM产生,除了配置最基本的定时器参数,需要注意的是还要配置最重要的参数CCR(给CCR配置一个非零的参数,如果为零那么ARR就会和0做比较,最终会导致无法产生PWM波形)

  • 代码部分只需要在初始化中,开启pwm即可,不需要其他参数

    HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_2);

4.ADC

每步操作的意义
  • HAL_ADC_PollForConversion(&hadc2,HAL_MAX_DELAY);这个代码意义是不断轮询adc转换结束标志位(adc_sr)是否置1,当调用HAL_ADC_GetValue后就会将该标志位置0

  • stm32中存在一个1.2v的参考电压

  • 如果想要使用DMA连续获得adc的参数最好调用adc中断回调函数

    • 如光找不到中断回调函数,那么可以给DMA开启循环模式,adc配置为持续扫描模式(特别注意一点,对于蓝桥杯板子,还需要开启DMA continous requests
  • 同步(Synchronous):

    • 同步I/O操作是指在执行I/O操作时,程序必须等待操作完成才能继续执行。在同步操作中,程序提交一个I/O请求后,操作系统会阻塞该程序,直到请求操作完成。此时,程序才能继续执行后续的代码。因此,同步操作会导致程序执行流程暂停,直至I/O操作完成。(总结同步操作会阻塞程序执行
  • 异步(Asynchronous):

    • 异步I/O操作是指程序在发起I/O请求后,无需等待操作完成,可以继续执行其他任务。当异步I/O操作完成时,程序会通过某种方式(如回调函数、事件通知、信号等)得到通知。因此,异步操作使程序执行流程得以继续,而不必等待I/O操作完成。(总结异步操作不会阻塞程序执行

ADC单通道

image.png

  • differential表示差分输入 和single-ended表示单端输入
  • 直接在cubemx中配置好adc,然后生成代码就行
//adc函数
float ADC_Read(ADC_HandleTypeDef *hadc)
{
	uint16_t M_adc;//adc输出量为12位
	HAL_ADC_Start(hadc);
	M_adc=HAL_ADC_GetValue(hadc);
	return M_adc*3.3f/4095;
	
}
* 注意要在 ,初始化函数中加上   校准函数
HAL_ADCEx_Calibration_Start(&hadc2,ADC_SINGLE_ENDED);//adc2 单端模式


ADC双通道

  • cubemx配置,不需要开连续扫描模式(转换完一个通道后立即自动执行下一个通道的转换) image.png

  • 代码

                  
                  HAL_ADC_Start(&hadc2);
			HAL_ADC_PollForConversion(&hadc2,50);
			adc2_PA4=HAL_ADC_GetValue(&hadc2)*3.30f/4096;
			HAL_ADC_Start(&hadc2);
			HAL_ADC_PollForConversion(&hadc2,50);
			adc2_PA5=HAL_ADC_GetValue(&hadc2)*3.30f/4096;
			HAL_ADC_Stop(&hadc2);

ADC新方法(提高精度)(不常用)

  • 对于比赛板子可采用,硬件配置来提高adc精度(ADC过采样模式)
    • 函数也要做出修改
float ADC_Read_OS(ADC_HandleTypeDef *hadc)
{
	uint16_t M_adc;//adc输出量为12位
	HAL_ADC_Start(hadc);
	M_adc=HAL_ADC_GetValue(hadc);
	return M_adc*3.3f/65535;//相当于测了128次平均位1次
	

屏幕截图 2025-01-15 195942.png

5.IIC读写EEPROM

  • 值得注意的是,EEPROM存储的数据不能是小数,eeprom不能连续读取

  • IIC时序基本单元

    • 起始条件:SCL高电平期间,SDA由低电平到高电平
    • 终止条件:SCL高电平期间,SDA由高电平到低电平
  • 总结

    • 高位先行
    • 数据以字节传输
    • 数据在SCL高电平稳定
    • 数据发送完,还要有应答信号
  • 移植官方库函数,并在官方库函数上加(EEPROM读写),并不需要在CUBEmx上配置(不能硬件iic配置,只能软件IIC)要在初始化函数上加上I2CInit(void),然后写这两个函数即可

//eeprom读写
uchar eeprom_read(uchar address)
{
	uchar m_data;
	
	I2CStart();//开启iic
	I2CSendByte(0xa0);//联系芯片
	I2CWaitAck();//等待完成
	I2CSendByte(address);//写入地址
	I2CWaitAck();//等待完成
	I2CStop();//停止
	
	
	I2CStart();
	I2CSendByte(0xa1);//发送从机地址
	I2CWaitAck();//等待完成
	m_data=I2CReceiveByte();//读出
	I2CSendNotAck( );//发送应答信号
	I2CStop();//停止
	
	return m_data;
}

 void eeprom_write(uchar address,uchar mdata)
{
	 
	
	I2CStart();//开启iic
	I2CSendByte(0xa0);//联系芯片
	I2CWaitAck();//等待完成
	I2CSendByte(address);//写入
	I2CWaitAck();//等待完成
	I2CSendByte(mdata);
	I2CWaitAck();//等待完成

	I2CStop();//停止
	
	 
	
}

  • 指定写 image.png
  • 指定读(随机读+指定写) image.png
  • 硬件 image.png
  • AT24C02是2kb串行EEPROm,内部组织为256Byte,支持8bit页写,写周期内部定时(小于5ms),实现8个器件共用一个接口
  • A1 A2 A3对应了E1 E2 E3

IIC和ADC的结合mcp4017t

  • 由上图可得,mcp4017t也是IIC通信上的从机,因此可以通过上位机来调整它的阻值,通过IIC读写来调整,他的从机地址为(0x5e),然后就可以通过ADC来读他的模拟值。
//写mcp 
void mcp_write(uint8_t mdata)
{
	 
	//指定地址写
	I2CStart();//开启iic
	I2CSendByte(0x5e);//联系芯片
	I2CWaitAck();//等待完成
	I2CSendByte(mdata);
	I2CWaitAck();//等待完成
	
	I2CStop();//停止
	
}

第一次不读取eeprom值

  • 设备第一次读取时使用代码中的初始化,后续启动使用EEPROM中存储值作为商品信息的初值,那么如何判断设备是否第一次启动就成了商品信息初始化的关键点。这里采用的方法是:使用EEPROM中的两个地址存储特定的值,每次启动时都读取这两个地址,一旦读取出的数值不是设定的特定值就判断是第一次启动设备,再将特定值写入;否则,就不是第一次启动,那么就使用EEPROM中的值作为初值。
//读eeprom(放在初始化函数中)
	if(EEPROM_read(111)!=111)//第一次上电
	{
		X_stock=10;//库存
		Y_stock=10;
		
		X_prise=1.0f;//价格1.0-2.0
		Y_prise=1.0f;

		EEPROM_write(111,111);//第一次上电后置标志位
	}
	else
	{
		
		X_stock=EEPROM_read(0);//eeprom不能连续读取
		HAL_Delay(2);
		Y_stock=EEPROM_read(1);
		HAL_Delay(2);
		X_prise=EEPROM_read(2)/10.0;
		HAL_Delay(2);
		Y_prise=EEPROM_read(3)/10.0;
		HAL_Delay(2);
		
	}

eeprom存储小数的方式

  • eeprom不能存储小数,可以采用先存

6.串口(uART)

image.png

  • 比赛考的很有限,不考DMA等等;
  • 步骤:在cubemx中开启串口,如果要接受代码,记得开启中断,在初始化中也要开启中断HAL_UARTEx_ReceiveToIdle_IT然后生成代码,
  • 发送数据
//uart
//不开中断,在循环中写
	sprintf(str,"   ADC_value:%.4f \r  ", ADC_Read_OS(&hadc2));
	HAL_UART_Transmit(&huart1,(const uint8_t*)str,strlen(str),HAL_MAX_DELAY);
//开中断,
    HAL_UART_Transmit_IT(&huart1,(const uint8_t*)str,strlen(str));//在初始化函数中开启,
   // 在中断函数中,需要在最后将上面函数加上
   
//中断接收函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);

memset(arr,'\0',sizeof(arr));//将arr清零

  • 接收数据

//开中断,
    HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//在初始
    //化函数中开启,在中断函数中,需要在最后将上面函数加上
//中断函数(最好的),可以接受不定长
    HAL_UARTEx_RxEventCallback() //是一个在 STM32 HAL库中定义的回调函数,它是用于处理 UART 接收
    
    //事件的扩展回调函数。这个函数通常在 UART 接收到数据并且数据接收完成时被调用,特别是在使用 DMA
    (直接内存访问)或其他非阻塞接收方法时。它是 HAL 库中用于处理 UART 通信的一部分,允许用户在接
    收到特定数量的数据后执行自定义操作。
串口应用(密码代换)
  • 特别注意将数组长度改成4,不然有编码格式问题,无法比较(因为字符串最后一位需要存储'\0')
  • char old_pswd[3]; char new_pswd[3];需要注意,要将这个放到中断函数中,否则会导致password的第一位置0;
char password[3]="123";
static uint16_t count;
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	count++;//记录进入中断的次数
    if(huart->Instance==USART1)
    {
		char old_pswd[3];
		char new_pswd[3];

        // 确保数据格式为 old_pswd-new_pswd
        if (sscanf(rx_data, "%3s-%3s", old_pswd, new_pswd)==2 )  // 确保成功读取两个密码
        {
            // 判断旧密码是否正确
            if (strcmp(password,old_pswd) == 0)
            {
                // 密码正确,更新密码
                for (int i = 0; i < 3; i++)
                {
                    password[i] = new_pswd[i];  // 更新为新密码
                }
                

                // 返回成功信息
                sprintf(tx_data, "   success   ");
                HAL_UART_Transmit(&huart1, (const uint8_t*)tx_data, strlen(tx_data), HAL_MAX_DELAY);
            }
            else
            {
                // 旧密码不匹配
                sprintf(tx_data, "   fail   ");
                HAL_UART_Transmit(&huart1, (const uint8_t*)tx_data, strlen(tx_data), HAL_MAX_DELAY);
            }
        }
        else
        {
            // 数据格式不正确
            sprintf(tx_data, "   invalid format   ");
            HAL_UART_Transmit(&huart1, (const uint8_t*)tx_data, strlen(tx_data), HAL_MAX_DELAY);
        }
		
        // 重新启动接收中断
        HAL_UARTEx_ReceiveToIdle_IT(&huart1,(  uint8_t*)rx_data,50);
		
		
    }
}
  • HAL_UARTEx_ReceiveToIdle_IT();//- 该函数用于在中断模式下接收数据,直到 UART 接收缓冲区空闲(即没有数据接收一段时间)。推荐使用
    • 不过需要注意,与之对应的 中断回调函数是
    • void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)该函数还有一个参数size可以用来 检测每次传输进来的数据的格式

7.systick

  • HAL_GetTick();获取当前时间,向上计时

  • 如果将HAL_Delay()函数放进中断函数中便会卡死,这是因为 HAL_Delay()函数  调用的滴答定时器的中断优先级太低,导致无法打断定时器中断而卡死,只需在nvic中将滴答定时器的优先级提高即可

image.png

8. 客观题解

stm32

image.png

    • 1LSB为最小有效位,

      1. 计算公式: LSB=Vref2n\text{LSB} = \frac{V_{\text{ref}}}{2^n}
      1. 将这些值代入公式中: LSB=2.4V28=2.4V2560.009375V\text{LSB} = \frac{2.4 \, \text{V}}{2^8} = \frac{2.4 \, \text{V}}{256} \approx 0.009375 \, \text{V}
  1. stm32中,程序可以在RAM和ROM区域上运行

  2. systick属于内核级外设,24位递减计数器,可以通过软件来控制它的开始与停止

  3. 嵌入式系统不能跨平台移植

  4. eeprom,ROM和nor flash属于非易失性存储器,RAM属于易失性存储器

  5. 在I²C总线协议中,从器件的地址由7位或10位组成。对于7位地址模式:

    1. 地址位数:7位地址模式使用7位二进制数表示从器件地址。
    2. 地址范围:7位地址的范围是 0000000 到 1111111,即 0 到 127(十进制)。
    3. 可挂载数量:理论上,7位地址可以支持 128 个从器件
数电
  1. 门电路中输出端可以直接相连实现线与功能的是OD电路和OC电路
模电
  1. 有源滤波器和无源的区别,是否需要电源,是否有增益
  2. 放大电路零点漂移的原因
      1. 温度变化
      1. 元件的不匹配
      1. 电源电压波动
      1. 老化效应
    • 输入偏置电流
    • 漏电流
    • 电磁干扰

9. 常见技巧

标志位,实现持续处于状态而不累加

static uint8_t LF=0,LA=0,LT=0;//作为标志位,实现持续处于报警状态不累加
	if(frq1>FH)
	{
		if(LF==0)
		{
			FN++;
			LF=1;
		}
	}
	else LF=0;
 //-------------------------------------------------
 //本来只用
 if(frq1>FH)
 {
     FN++;
 }
 
 
 

长按锁定,短按切换状态

  • 本质上是置一个标志位,通过标志位判断
//长按2s就置标志位
if(Mkey[4].flag==1)//短按4
	{
		 
		if(view==0&&tim_2s_flag==1)
		{
			ctrl_dutyf=0;//占空比调整解锁
			
		}
		
		Mkey[4].flag=0;
	}
	
	
	
if(Mkey[4].Long_flag==1)//长按4
	{
		 if(view==0)
		 {
			ctrl_dutyf=1;//锁定占空比调整功能,占空比不变
			
		 }		
		
		Mkey[4].Long_flag=0;
	}
	

LED闪烁(使用滴答定时器)

  • 详情见 N14
if(uwTick-time100ms>100)//0.1s
	{
		static uint8_t blink;
		blink=!blink;
		
		if(rec_en==1)LED_O(1,blink);
		else LED_O(1,0);
		
		if(replay_pwm_en==1)LED_O(2,blink);
		else LED_O(2,0);
		
		if(replay_v_en==1)LED_O(3,blink);
		else LED_O(3,0);
		
	}

取值取平均值,最大值,最小值方法

  • 详见 N13

			HAL_ADC_Start(&hadc2);
			HAL_ADC_PollForConversion(&hadc2,50);
			adc2_PA4=HAL_ADC_GetValue(&hadc2)*3.30f/4096;
			HAL_ADC_Start(&hadc2);
			HAL_ADC_PollForConversion(&hadc2,50);
			adc2_PA5=HAL_ADC_GetValue(&hadc2)*3.30f/4096;
			HAL_ADC_Stop(&hadc2);
			
			adc4[N4]=adc2_PA4;
			adc5[N5]=adc2_PA5;
			
			
			if(adc4[N4]>adc4_max)adc4_max=adc4[N4];
			
			if(N4==0)
			{
				adc4_min=adc4[N4];
			}
			 
			else if(adc4[N4]<adc4_min)adc4_min=adc4[N4];
			
			
				adc4_sum+=adc4[N4];
			
			
			adc4_avrge=adc4_sum/(N4+1);
			
			
			if(adc5[N5]>adc5_max)adc5_max=adc5[N5];
			
			if(N4==0)
			{
				adc5_min=adc5[N5];
			}
			 
			else if(adc5[N5]<adc4_min)adc5_min=adc5[N5];
			
			 
				adc5_sum+=adc5[N5];
			 
			
			adc5_avrge=adc5_sum/(N5+1);
			N4++;
			N5++;
			

记录最值

  • 常见记录页面,会记录最大值,最小值。最大值好计算,但是最小值如果初始化为0的话,会导致最小值一直为0,出现不存在的数
  • 这个代码本质上便是置了一个标志位,如果标志位为0,是第一次就将该次的值赋给最小值,如果为1,就正常计算
uint16_t rate_max;
uint16_t rate_min;
bool rate_min_initialized=0;


    if(rate>rate_max) rate_max=rate;
		  
	 if (!rate_min_initialized || rate<rate_min) {
            rate_min = rate;
            rate_min_initialized = true; // 标记rate_min已被初始化
        }

串口输入值

  • sscanf(rx_data,"%4s:%4s:%12s",car_type,car_id,car_time) 用来处理串口接收的数据
  • 同时HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)还有第二个参数Size,可以用来判断接收到数据的字符数
  • 这两个函数可以搭配起来使用(P12停车收费系统

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
	if(huart==&huart1)
	{
		
	if(sscanf(rx_data,"%4s:%4s:%12s",car_type,car_id,car_time)==3&&Size==22)//判断格式信息
	{
		
		int i=0;//车库的索引
		
		for(;i<car_cmnnum;i++)//先判断车库里有没有这个车的信息
		{
			if(cars[i].flag==1&&strcmp(car_id,cars[i].id)==0)//如果有
			{
				
				carisin_flag=1;//置标志位,证明该车在车库
				
				break;
			}
		}
		
		
		if(strcmp("CNBR",car_type)==0)//当类型为1
		{
			car1_flag=1;
			
		}
		else if(strcmp("VNBR",car_type)==0)
		{
			car2_flag=1;
		}
		
		
		
		
		if(carisin_flag==0)//当是进车库
		{
			//找空车位
			
			int t=0;
			for(;t<car_cmnnum;t++)
			{
				if(cars[t].flag==0)//如果有空车位
				{
				
					pksptempty_flag=1;//置标志位,证明该车库空
				
					break;
				}
			}
			
			
			if(pksptempty_flag==1)
			{
				
				//将信息存入车库信息中
				strcpy(cars[t].type,car_type);
				strcpy(cars[t].id,car_id);
				strcpy(cars[t].time,car_time);
				cars[t].flag=1;
			
				idle_paringspt--;//剩余数量--
			
				if(strcmp(car_type,"CNBR")==0)
				{
					car1++;
				 
				}
				if(strcmp(car_type,"VNBR")==0)
				{
					car2++;
				 
				}
				
				pksptempty_flag=0;//空车库标志位清除
				
			}
			
			
			
		}
		else //当出车库	
		{
			char car_outyear[3],car_outmonth[3],car_outday[3],car_outhour[3],car_outmin[3];
			sscanf(car_time,"%2s%2s%2s%2s%2s",car_outyear,car_outmonth,car_outday,car_outhour,car_outmin);//存入数据
			
			
			//处理信息
			char car_inyear[3],car_inmonth[3],car_inday[3],car_inhour[3],car_inmin[3];
			sscanf(cars[i].time,"%2s%2s%2s%2s%2s",car_inyear,car_inmonth,car_inday,car_inhour,car_inmin);//存入数据
			
			
			in_time=atoi(car_inyear)*3153600+atoi(car_inmonth)*720+atoi(car_inday)*24+atoi(car_inhour);//时间转化
			

			out_time=atoi(car_outyear)*3153600+atoi(car_outmonth)*720+atoi(car_outday)*24+atoi(car_outhour);//时间转化
			
			
			sum_time=out_time-in_time;//总的时间
			
			if((atoi(car_outmin)-atoi(car_inmin))>0) 
				sum_time+=1;
			
			
			if(strcmp(car_type,"CNBR")==0)
			{
				car1--;
				sum_price=car1_price*sum_time;
				
			}
			if(strcmp(car_type,"VNBR")==0)
			{
				car2--;
				sum_price=car2_price*sum_time;
				
			}
			
			//输出信息
			printf("%s:%s:%s\r\n",cars[i].type,cars[i].id,cars[i].time);//进场信息
			
			printf("%s:%s:%s\r\n",car_type,car_id,car_time);//出场信息
			
			printf("%s:%s:%d:%.2f\r\n",car_type,car_id,sum_time,sum_price);

			
			idle_paringspt++;//剩余数量++
			
			
			//将对应车库信息删除
			strcpy(cars[i].type,"0");
			strcpy(cars[i].id,"0");
			strcpy(cars[i].time,"0");
			cars[i].flag=0;
			
			
			
			carisin_flag=0;//车库无车
		}
			
			
	}
			
	else
	{
		printf("   ERROR  \r\n");
		
		
	}
		
		
		memset(rx_data,0,sizeof(rx_data)/sizeof(char));
		
		
		
	
		 
		
	}
	HAL_UARTEx_ReceiveToIdle_IT(&huart1,(uint8_t *)rx_data,sizeof(rx_data)/sizeof(char));//开启中断
	
}


串口重定向

int fputc(int ch, FILE*F)
{
    /*发送一个字节数据到串口DEBUG_USART*/
    HAL_UART_Transmit(&huart1 ,(uint8_t * ) &ch,1,1000);
    return (ch);
}