蓝桥杯嵌入式
蓝桥杯嵌入式
一点
流程:1、配置时钟 2、配置CubeMX生成MDK工程 3、配置工程分开生成c和h文件 4、移植LCD屏幕 5、配置MDK为DAP烧录,并且烧录后自动重启运行 6、CubeMX配置锁存器,使led初始化为全灭
MX添加芯片包



开启代码补全
ctrl + alt +空格
考点


1、配置时钟RCC__HSE,很重要,配置错误灯都点不亮

输入24MHz的HSE通过PLL锁相环输出80MHz
输入24MHz(Input frequency)
输出80MHz(HCLK)

如果忘了外部晶振频率,可以通过赛场资源包原理图得知

LED:
LD1~8 ---PC8~15,低电平亮
锁存器LE:PD2
1选通,0锁存
先将锁存器LE引脚初始化为低电平,锁存LED状态,需要操作LED时再开启,操作完即关闭,避免LED与LCD的互相影响


key:
从上到小,按键B14,PB13,PA0
PB0~2,PA0,上拉,检测低电平
按键扫描函数(延时):
类似检测下降沿,单次按键单次触发(仍有小概率多次触发)
uint8_t B1_state,B1_last_state;
uint8_t B2_state,B2_last_state;
uint8_t B3_state,B3_last_state;
uint8_t B4_state,B4_last_state;
uint16_t keynum=0;
uint16_t num=0;
void key_process()
{
B1_last_state=B1_state;
B2_last_state=B2_state;
B3_last_state=B3_state;
B4_last_state=B4_state;
B1_state=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);//获取按键实时状态
B2_state=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
B3_state=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
B4_state=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
if(B1_state==0&&B1_last_state==1) //B1,上次按键状态未按下,此刻按键按下则触发,(状态变化触发)
{
HAL_Delay(20);keynum=1;num++;
}
if(B2_state==0&&B2_last_state==1) //B2
{
HAL_Delay(20);keynum=2;num++;
}
if(B3_state==0&&B3_last_state==1) //B3
{
HAL_Delay(20);keynum=3;num++;
}
if(B4_state==0&&B4_last_state==1) //B3
{
HAL_Delay(20); keynum=4;num++;
}
}
代码来自b站up主:【蓝桥杯嵌入式省赛快速入门教程(诚心分享+全程干货+免费无套路)】https://www.bilibili.com/video/BV1uH4y1E7G4?p=5&vd_source=cdeca744628183bf6ce46fc0a3b3e75c
struct key
{
uint8_t key_val;//当前电平
uint8_t key_state;//按脚循环状态
uint8_t key_long_flag;//长按标志位
uint8_t key_double_flag;
uint8_t key_single;
uint8_t long_time;
uint8_t double_time;
uint8_t key_double;
};
struct key keys[4]={0,0,0,0,0,0,0,0};
uint8_t keynum;
void key_process(void)//放在10ms定时器回调函数中
{
keys[0].key_val=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);//������ƽ��ȡ
keys[1].key_val=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
keys[2].key_val=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
keys[3].key_val=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
for(int i=0;i<4;i++)
{
switch(keys[i].key_state)
{
case 0://状态0:初始检测
{
if(keys[i].key_val==0)keys[i].key_state=1;
}break;
case 1://状态1:消抖确认
{
if(keys[i].key_val==0)keys[i].key_state=2;
else keys[i].key_state=0;
}break;
case 2://状态2:长按检测
{
if(keys[i].key_val==1)
{
if(keys[i].long_time>100)
{
keys[i].key_long_flag=1;//长按
keys[i].key_state=0;
}
else keys[i].key_state=3;//检测跳转短按或双击检测
keys[i].long_time=0;
}
else keys[i].long_time++;
}break;
case 3://状态3:双击(短按)检测
{
if(keys[i].key_val==0)
{
if(keys[i].double_time<=35)
{
keys[i].key_double_flag=1;//
}
}
else
{
keys[i].double_time++;
if(keys[i].double_time>35)
{
keys[i].key_single=1;//短按
keys[i].double_time=0;
keys[i].key_state=0;
}
}
if(keys[i].key_double_flag==1&&keys[i].key_val==1)
{
keys[i].key_double=1;//双击
keys[i].double_time=0;
keys[i].key_state=0;
keys[i].key_double_flag=0;
}
}break;
// default:break;
}
}
}
按键检测(放while循环中)
static struct key
{
uint8_t key_state;//按键循环状态
uint8_t key_val;//按键当前电平
uint8_t key_one_flag;//短按
uint8_t key_long_flag;//长按
uint8_t long_time; //长按时长
};
struct key keys[4]={0};
void key_process(void)
{
// 读取按键电平(假设按下为低电平)
keys[0].key_val = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
keys[1].key_val = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);
keys[2].key_val = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2);
keys[3].key_val = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
for (uint8_t i = 0; i < 4; i++)
{
switch (keys[i].key_state)
{
case 0: // 初始状态:检测按下
{
if (keys[i].key_val == 0) //按下
{
keys[i].key_state = 1;//跳转消抖
keys[i].long_time = 0; // 重置长按计时
}
}break;
case 1: // 消抖确认
{
if (keys[i].key_val == 0) //按下
{
keys[i].key_state = 2;//跳转长按检测
keys[i].long_time = 0; // 重置计时,用于长按检测
}
else //抖动
{
keys[i].key_state = 0;
keys[i].long_time = 0;
}
}break;
case 2: // 长按检测:按住期间计时/短按判断
{
if (keys[i].key_val == 0)
keys[i].long_time++; // 每10ms递增
else
{
// 松开按键时判断长按/短按
if (keys[i].long_time >= 50) // 长按0.5秒
keys[i].key_long_flag = 1; // 标记长按(仅触发一次)
else
keys[i].key_one_flag = 1; // 标记短按
// 重置状态
keys[i].key_state = 0;
keys[i].long_time = 0;
}
}break;
}
}
}
按键长按加短按
static struct key
{
uint8_t key_state;//按键循环状态
uint8_t key_val;//按键当前电平
uint8_t key_one_flag;//短按
uint8_t key_long_flag;//长按
uint8_t long_time; //长按时长
};
struct key keys[4]={0};
void key_process(void)
{
// 读取按键电平(假设按下为低电平)
keys[0].key_val = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
keys[1].key_val = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1);
keys[2].key_val = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2);
keys[3].key_val = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
for (uint8_t i = 0; i < 4; i++)
{
switch (keys[i].key_state)
{
case 0: // 初始状态:检测按下
{
if (keys[i].key_val == 0)
{
keys[i].key_state = 1;
keys[i].long_time = 0; // 重置长按计时
}
}break;
case 1: // 消抖确认:持续检测50ms
{
if (keys[i].key_val == 0)
{
keys[i].key_state = 2;
keys[i].long_time = 0; // 重置计时,用于长按检测
}
else
{
// 抖动或短按释放,返回初始
keys[i].key_state = 0;
keys[i].long_time = 0;
}
}break;
case 2: // 长按检测:按住期间计时
{
if (keys[i].key_val == 0)
keys[i].long_time++; // 每10ms递增
else
{
// 松开按键时判断长按/短按
if (keys[i].long_time >= 50) // 长按0.5秒
keys[i].key_long_flag = 1; // 标记长按(仅触发一次)
else
keys[i].key_one_flag = 1; // 标记短按
// 重置状态
keys[i].key_state = 0;
keys[i].long_time = 0;
}
}break;
}
}
}
uint8_t keynum;
void key_w_process(void)
{
if(keys[0].key_one_flag==1)
{
keynum++;
keys[0].key_one_flag=0;
}
if(keys[0].key_long_flag==1)
{
keynum--;
keys[0].key_long_flag=0;
}
}
移植LCD:
不需要再CubeMX中进行引脚配置
include <stdio.h>
将下类文件添加进工程文件夹

main中只用声明lcd.h头文件就可以使用LCD(不用声明fonts.h否则报错,嵌套引用)
UART串口通信
UART1(9600)
PA9,10(需手动配置,默认为PC端口)
异步模式:
开启串口中断
添加的头文件
stdio.h(标准输入输出头文件)
string.h(字符串处理头文件)
#include <stdio.h> //
#include <string.h> //
- 输入输出函数:
printf:格式化输出函数,用于将数据输出到标准输出(通常是屏幕)。scanf:格式化输入函数,用于从标准输入(通常是键盘)读取数据。fopen:打开文件。fclose:关闭文件。fread:从文件中读取数据。fwrite:向文件中写入数据。fgets:从文件中读取一行数据。fputs:向文件中写入一行数据。
- 文件操作函数:
remove:删除文件。rename:重命名文件。tmpfile:创建临时文件。tmpnam:生成临时文件名。
- 控制函数:
setbuf:设置缓冲区。setvbuf:设置缓冲区。getchar:从标准输入读取一个字符。putchar:向标准输出写入一个字符。
string.h头文件中包含了以下几类函数:
- 字符串操作函数:
strlen:计算字符串的长度。strcpy:复制字符串。strncpy:复制字符串的一部分。strcat:连接两个字符串。strncat:连接两个字符串的一部分。strcmp:比较两个字符串。strncmp:比较两个字符串的一部分。strchr:查找字符串中首次出现的字符。strrchr:查找字符串中最后一次出现的字符。strstr:查找子字符串。strtok:分割字符串。
- 内存操作函数:
memcpy:复制内存块。memmove:移动内存块。memcmp:比较内存块。memset:设置内存块。
- 字符操作函数:
isalpha:检查字符是否为字母。isdigit:检查字符是否为数字。islower:检查字符是否为小写字母。isupper:检查字符是否为大写字母。isspace:检查字符是否为空白字符。toupper:将字符转换为大写。tolower:将字符转换为小写。
声明串口句柄
UART_HandleTypeDef huart1;
因为CubeMX中自动生成了串口初始化,并包含在MX_GPIO_Init()中,故不用再次初始化
设置串口
串口发送
unsigned char str[100]; sprintf(str, "%04d:Hello,world.\r\n", counter);//**格式化字符串** HAL_UART_Transmit(&huart1,(unsigned char *)str, strlen(str), 50);//**通过UART发送字符串**strlen():计算字符串的长度

串口接收
main():
HAL_UART_Receive_IT(&huart1, rx, 1);//启动接收中断。uint8_t rx[100]; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//串口接收回调函数(弱函数) { GPIOC->ODR = ((rx[0] << 8) | 0x00FF);//将接收到的二进制数据写入C端口,控制LED HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);//1选通锁存器 HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET); //0 锁存 HAL_UART_Receive_IT(&huart1, rx, 1); }
函数弱定义,可以ctrl+f搜索_weak,快速找到该函数定义
ADC电压测量
PB15__J11 电压采集1 ADC2_IN15
PB12__J12 电压采集2 ADC1_IN11



不用设置NVIC(默认)

获取ADC数值函数:
uint16_t getADC2_PB15_VL1(void)//PB15--ADC2_IN15
{
uint16_t adc = 0;
HAL_ADC_Start(&hadc2);//开启ADC2
adc = HAL_ADC_GetValue(&hadc2);//获取adc数值
return adc;
}
uint16_t getADC1_PB12_VL2(void)//PB12--ADC1_IN11
{
uint16_t adc = 0;
HAL_ADC_Start(&hadc1);
adc = HAL_ADC_GetValue(&hadc1);
return adc;
}

显示电压函数:
#include <stdio.h> // 添加此头文件
unsigned char buf[20];//存储电压值
sprintf(buf, " VAL:%.2fV", getADC2_PB15_VL1()*3.3/4096);//转化成电压值,保存在buf
LCD_DisplayStringLine(Line8, (uint8_t *)buf);//将cahr型变量转化成unsigned char 型变量,并在lcd中显示
sprintf(buf, " VAL:%.2fV", getADC1_PB12_VL2()*3.3/4096);
LCD_DisplayStringLine(Line9, (uint8_t *)buf);
解决LCD与LED引脚冲突
先将锁存器LE引脚初始化为低电平,锁存LED状态,需要操作LED时再开启,操作完即关闭,避免LED与LCD的互相影响

//LED off 熄灭所有led
HAL_GPIO_WritePin(GPIOC, LD6_Pin|LD7_Pin|LD8_Pin|LD1_Pin
|LD2_Pin|LD3_Pin|LD4_Pin|LD5_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LE_GPIO_Port, LE_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LE_GPIO_Port, LE_Pin, GPIO_PIN_RESET);
TIME1定时1s
选择定时器时钟输入源:
Internal Clock:内部时钟

NVIC开启定时器1更新中断

定时周期1s,(8000PSCX1000ARR)/8*10^6=0.1s

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)//定时器回调函数(中断服务函数)处理定时器周期溢出事件
{
t++;//定时溢出时执行程序
}
main(){
HAL_TIM_Base_Start_IT(&htim1);//开启定时器中断
......
}
while(1)中
sprintf(buf, " t=%d", t);//,保存在char型变量buf【20】中
LCD_DisplayStringLine(Line8, (uint8_t *)buf);//将cahr型变量转化成unsigned char 型变量,并在lcd中显示
基础定时器TIM6
activate:激活
pluse:脉冲
系统时钟80MHz
定时周期T=(8000X1000)/8X10^6=0.1s
PSC:8000-1,ARR=1000-1
自动重装预寄存器:auto-reload preload,无影响
开启更新中断

定时器TIM17定时10ms


初始化:
HAL_TIM_Base_Start_IT(&htim17);//启动基础定时器 TIM17 的基础计数功能并开启中断
定时器中断回调函数
unsigned char ms_10;
unsigned char s;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance ==TIM17)//判断是否是定时器3触发的中断
{
ms_10++;
if(ms_10>=100)
{
s++;
ms_10=0;
}
// HAL_TIM_Base_Start_IT(&htim17);//重新启动定时器的输入捕获功能,并使能相应的中断。
}
}
频率测量,(定时器输入捕获,当计数器)

R39__PB4 TIM3_CH1,TIM16_CH1
R40__PA15 TIM2_CH1,TIM8_CH1
原理:将预分频80后,定时器时钟频率为1MHz
配置定时器为输入捕获模式

因为时钟树输出时钟80MHz,预分频80,则计数n为1/1MHz=1*10的-6次方=n us
配置PSC为80-1,重装值为0xFFFF(65536),最大计时值为65536us,65.5ms
频率为计时值倒数,1/n(us)=
开启定时器中断

测周法:两个上升沿内,以标准频率fc计次,得到N,则频率
f(捕获)=1MHz(分频后)/计数值

fc=80MHz/80=1MHz
$$
fc=1Mhz/N=1000000/N
$$
定时器回调函数:
uint32_t cc1_value_1,cc1_value_2 = 0; uint32_t f39,f40 = 0;
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)//定时器回调函数
{
if(htim->Instance ==TIM3)//判断是否是定时器3触发的中断
{
cc1_value_1 = __HAL_TIM_GET_COUNTER(&htim3);//获取计数值
__HAL_TIM_SetCounter(&htim3,0);//清除计数值
f39 = 1000000/cc1_value_1;//计算频率(10……6)
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);//重新启动定时器的输入捕获功能,并使能相应的中断。
}
if(htim->Instance ==TIM2)
{
cc1_value_2 = __HAL_TIM_GET_COUNTER(&htim2);
__HAL_TIM_SetCounter(&htim2,0);
f40 = 1000000/cc1_value_2;//(10^6)
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
}
}
相关初始化
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);//启动定时器的输入捕获功能,并使能相应的中断。
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
可通过搜索关键词输入捕获开启
_IC_st
查找,因为IC是Input capture输入捕获的缩写,st是start开启的缩写
读取频率
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
sprintf(buf, " FRQ(R39):%dHz ",f39);
LCD_DisplayStringLine(Line8, (uint8_t *)buf);
HAL_Delay(200);
sprintf(buf, " FRQ(R40):%dHz ",f40);
LCD_DisplayStringLine(Line9, (uint8_t *)buf);
HAL_Delay(200);
}
PWM输出(占空比调节)
PA2__TIM2_CH2
选择定时器2通道2功能为

设置为1Khz频率
PSC:800
ARR:100
f输出=80MHz/(PSC*ARR)=80x10^6/(800x100)=1000hz
开启输出比较(默认)

不开定时器中断(默认)
初始化PWM输出
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);//开启time2,通道2pwm输出
uint16_t pwmduty=50;//占空比
TIM2->CCR2 =pwmduty;
//TIM2->CCR2 =50;
//uint16_t pwmduty=50;//占空比
占空比=输出比较寄存器值/自动重装载值,pwmduty=CCR/ARR=CCR2/100,即在ARR=100时,CCR的值即为占空比(<=100)
更改占空比(自己定义)
TIM2->CCR2 =pwmduty;
pwmduty++;
HAL_Delay(10);
if(pwmduty>95)pwmduty=0;
EEPROM(移植库文件,无需CubeMX配置)
A2=A1=A0=0
地址0xa0写
0xa1读
将下面文件拷贝进工程

引用头文件
在i2c_hal.c文件中编写iic读取EEPROM和写入函数
写入函数(器件内地址,要写入的数据)
void eeprom_write(uint8_t addr, uint8_t dat)
{
I2CStart();
I2CSendByte(0xa0);//器件地址
I2CWaitAck();//等待应答(只有写操作后需要)
I2CSendByte(addr);//期间内要操作的地址0~255???
I2CWaitAck();
I2CSendByte(dat);//写入的数据
I2CWaitAck();
I2CStop();
HAL_Delay(20);//一定要加
}
读取函数(器件内地址)
uint8_t eeprom_read(uint8_t addr)
{
I2CStart();
I2CSendByte(0xa0);//器件地址
I2CWaitAck();//等待应答
I2CSendByte(addr);//期间内要操作的地址0~255???
I2CWaitAck();
I2CStop(); //一定要关闭IIC总线后重新开启
I2CStart();
I2CSendByte(0xa1);//读取命令
I2CWaitAck();
uint8_t dat = I2CReceiveByte();//接收数据
I2CSendNotAck(); //主机不应答 从机不再发数据---------------------
I2CStop();
return dat;
}
初始化,写入,读取
I2CInit();
eeprom_write(0, 15);//在0地址写入15
uint8_t dat = eeprom_read(0);//读取0地址
char text[20];
显示到Lcd
sprintf(text, " %d ", dat);//将数据dat转化成字符串保存在text数组变量中
LCD_DisplayStringLine(Line0, (uint8_t *)text);
RTC时钟+闹钟
在RTC中勾选
开启时钟
开启闹钟
选择内部闹钟1

选择24h制式

选择数据格式转化为二进制

设置初始化时间和日期
闹钟A设置时间

选择是否屏蔽(屏蔽日期则闹钟不考虑日期)

开启闹钟中断(闹钟时间时进入中断)

设置保存时间的结构体
char text[20];
RTC_TimeTypeDef sTime = {0};//save time
RTC_DateTypeDef sDate = {0};//save day
获取时间和日期保存到结构体
HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
读取时间并显示到LCD上
sprintf(text, " test ");
LCD_DisplayStringLine(Line0, (uint8_t *)text);
sprintf(text, " %2d:%2d:%2d ", sTime.Hours, sTime.Minutes, sTime.Seconds);
LCD_DisplayStringLine(Line1, (uint8_t *)text);
sprintf(text, " %d-%d-%d-%d ", sDate.Year, sDate.Month, sDate.Date, sDate.WeekDay);
LCD_DisplayStringLine(Line2, (uint8_t *)text);
需要自己添加的闹钟回调函数(rtc.c)闹钟时间到达时执行
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_TogglePin(GPIOC,LD8_Pin);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
}
真题训练要点记录
1、首先设置时钟树,24MHz输入,80MHz输出
led全灭初始化:
HAL_GPIO_WritePin(GPIOC, LD6_Pin|LD7_Pin|LD8_Pin|LD1_Pin
|LD2_Pin|LD3_Pin|LD4_Pin|LD5_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LE_GPIO_Port, LE_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(LE_GPIO_Port, LE_Pin, GPIO_PIN_RESET);
定时器捕获频率:
CubeMX:
选择定时器输入通道并开启,配置预分频为80-1,开启触发中断。
MDK:
main中初始化
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);//启动定时器的输入捕获功能,并使能相应的中断。
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
完成定时器中断回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance ==TIM2)
{
cc1_value_2 = __HAL_TIM_GET_COUNTER(&htim2);
__HAL_TIM_SetCounter(&htim2,0);
f40 = 1000000/cc1_value_2;
T40=1000000/f40;
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
}
}
LCD显示浮点数据(两位小数)
if (f39 < 1000){
sprintf(buf, " B=%dHz ", f39);
LCD_DisplayStringLine(Line4, (uint8_t *)buf);
}
else {
// 将f39转换为浮点数,并除以1000,保留两位小数
sprintf(buf, " B=%.2fKHz ", (float)f39/1000);
LCD_DisplayStringLine(Line4, (uint8_t *)buf);
}
HAL_Delay(200);
添加头文件
#include "stdio.h"//转换字符串
各种需记住函数:
定时器:
计时器:
//基础定时器中断初始化
HAL_TIM_Base_Start_IT(&htim17);
//定时器基础回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
PWM:


浙公网安备 33010602011771号