目录
2.5 main.c主要代码(不包含系统时钟配置等,这些Cube自动生成)
题解思路:
- 使用STM32 CubeMX配置时钟80MHz、GPIO(包括PC8~PC15(LED)、PD2(锁存器), PA1(PWM), PA0、PB0~2(按键), PA9、A10(UART)
- 分析:按键响应时间t<0.1秒,同时要避免一次按下多次触发,则使用三行按键法短按即可;由于LED和LCD有共用引脚,要用锁存器即时先将LED引脚电平锁住。
- 考虑系统实时性问题,无需同步执行某些代码时可使用HAL_Delay(),若需要类同步执行则可使用系统滴答计时器(uwTick)或者启动定时器资源。
详细内容:
一、系统初始化配置
1.1 配置时钟:选择HSE外部晶振,配置主频80MHz
![时钟配置图](https://img-blog.csdnimg.cn/direct/3b618d672a034b77a7c5373f4172cd9f.png)
图 1-1 时钟树配置
1.2 芯片引脚配置:
根据下图自行配置,但实际工程中可以多复制几份工程文件,再进入不同的CubeMX的配置文 件(.ioc)配置生成相应的gpio.c,这里建议分别改为led.c\key.c\pwm.c等等方便随时移植的文件,文 件比较多时也可以新建文件夹bsp(板载)来区分外设和芯片内部的代码文件。
![](https://img-blog.csdnimg.cn/direct/df764aa67fce41c7a02fb0dd876f7fe6.png)
图 1-2 芯片引脚配置
1.3 引脚输入、输出模式配置
通过查阅竞赛板外设资料可知按键自带上拉模式,(PA0,PB0~2)配置为浮空输入即可,配 置后的生成的gpio.c/h改为key.c/h文件方便移植。
![](https://img-blog.csdnimg.cn/direct/c72f976747ce486fbef19396c618ef78.png)
图 1-3-1 按键引脚配置
LED为共阳极连接,芯片输出高电平时不导通,默认上电GPIO output level选择High;锁存器对应PD2,低电平锁存,高电平解锁,默认上电GPIO output level选择Low;输出只有高低电平之分,选择推挽输出即可,配置后生成的gpio.c/h改为led.c/h文件方便移植。
![](https://img-blog.csdnimg.cn/direct/95907894afe74d33b3ae9ada314503c1.png)
图 1-3-2 LED引脚配置
1.4 TIM和USART配置
根据赛题要求,引脚A1为PWM输出端 ,点击引脚可知对应为TIM2的通道2,时钟源选择内部时钟源(系统时钟)即可,模式配置参考下图。
![](https://img-blog.csdnimg.cn/direct/4ae173b8d6b542e8a28b1fa671cdb7ea.png)
图 1-4-1 TIM模式配置
上电默认PWM频率为1000Hz,方波即占空比为50%,预分频系数选择80-1,得到的时钟频率为80M/80=1MHz,自动重装载数为1000-1,即计1000个数,频率变为1M/1K=1KHz,根据占空比(50%)配置计数个数CCR=500,模式1为计数n<CCR为有效电平,n>CCR为无效电平,之后修改占空比也是修改CCR的值。配置完成会生成tim.c/h文件。
![](https://img-blog.csdnimg.cn/direct/260442baf4994820bf7a1b7b59e92007.png)
图 1-4-2 TIM生成PWM配置参数
串口通信USART配置为异步模式Asynchronous,波特率配置为9600Bits/s,NVIC Settings中Enabled注意勾选,其余不用修改。配置完成会生成usart.c/h文件。
![](https://img-blog.csdnimg.cn/direct/d31d6bdc37af481e9b8e101e9cdad87b.png)
图 1-4-3 USART参数配置
二、 代码编写
2.1 按键扫描
采用三行按键法扫描,即将各个按键端口的状态组成一个字节数据,这里只有四个按键,采 用或运算将四个引脚状态放到字节数据低4位,详情见代码。
key.h/key.c
#ifndef __KEY_H__
#define __KEY_H__
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* USER CODE BEGIN Includes */
#define KEY1 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0)
#define KEY2 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1)
#define KEY3 HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2)
#define KEY4 HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)
/** 将每个按键的状态放到低四位,组合成新的字节数据 **/
#define KEYPORT KEY1|(KEY2 << 1)|(KEY3 << 2)|(KEY4 <<3)|0xf0
/*** 参数初始化 ***/
void KEY_Init(void);
void KEY_Input();
void key_scan();
/* USER CODE BEGIN Prototypes */
/* USER CODE END Prototypes */
#endif
#include "key.h"
/* USER CODE BEGIN 0 */
unsigned char ucTrg1=0;
unsigned char ucCont=0;
unsigned char ucRead=0;
/* USER CODE END 0 */
/* USER CODE BEGIN 1 */
uint8_t key[3]={0,0,0};//初始化输入值
uint8_t keyword[3];//确认密码值
uint8_t errorTime=0,OkKey=0;
/* USER CODE END 1 */
/** CubeMX生成gpio.c中的初始化函数 进行重命名 **/
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin : PA0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pins : PB0 PB1 PB2 */
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
/** 按下按键时输入密码值变化 **/
uint16_t keyClick=0;
void KEY_Input()
{
uint8_t flag=0;
key_scan();
//延时20毫秒
if(uwTick-keyClick<20)return;
/** 判断 **/
if(ucTrg1==0x01)
{
key[0]++;
if(key[0]==10)key[0]=0;
}
else if(ucTrg1==0x02)
{
key[1]++;
if(key[1]==10)key[1]=0;
}
else if(ucTrg1==0x04)
{
key[2]++;
if(key[2]==10)key[2]=0;
}
else if(ucTrg1==0x08)
{
for(int i=0;i<3;i++)
{
keyword[i]=key[i];
flag++;
}
//如果密码不正确,错误次数增加
if(flag!=3)
errorTime++;
OkKey=1;
}
keyClick=uwTick;
}
/** 按键扫描 **/
void key_scan()
{
/**如果按键A0按下,KEYPORT就会变为ffff 0111,即0xf7异或后
ucRead=0x08,在第一次按下时ucTrg1=ucRead=0x08,但持续按下时
ucTrg1又会变为0x00,所以可以避免一次按下多次触发
**/
ucRead=(KEYPORT)^0xff;
ucTrg1=ucRead&(ucRead^ucCont);
ucCont=ucRead;
}
2.2 Led初始化和修改
由于LED和LCD界面在密码正确时同步变化,写在同个函数也行。需要注意LED得在循环内随时设置为灭和锁存状态,保证LCD修改时不会干扰LED。
led.h/led.c
#ifndef __LED_H__
#define __LED_H__
#ifdef __cplusplus
extern "C" {
#endif
/* Includes ------------------------------------------------------------------*/
#include "main.h"
void LED_Init(void);
void LED_LCD_Control();
/* USER CODE BEGIN Prototypes */
/* USER CODE END Prototypes */
#ifdef __cplusplus
}
#endif
#endif /*__ GPIO_H__ */
#include "led.h"
#include "key.h"
#include "lcd.h"
#define LED1 GPIOC,GPIO_PIN_8
#define LED2 GPIOC,GPIO_PIN_9
#define LOCK GPIOD,GPIO_PIN_2
/* USER CODE BEGIN 0 */
extern int past_password[3];
extern uint8_t keyword[3];
extern uint8_t errorTime,OkKey;
uint32_t led1Tick=0,led2Tick1=0,led2Tick2=0;
extern void PWM_Change(uint32_t freq,float D);
extern void PWM_Display();
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_8
|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12, GPIO_PIN_SET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
/*Configure GPIO pins : PC13 PC14 PC15 PC8
PC9 PC10 PC11 PC12 */
GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_8
|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
/*Configure GPIO pin : PD2 */
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}
/*** 密码输入和确认后LED、LCD、PWM输出改变 ***/
uint8_t tempFlag2=1;
void LED_LCD_Control()
{
uint8_t passFlag=0;
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15|GPIO_PIN_8
|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12, GPIO_PIN_SET);
HAL_GPIO_WritePin(LOCK,GPIO_PIN_SET);//解锁
HAL_GPIO_WritePin(LOCK,GPIO_PIN_RESET);
for(int i=0;i<3;i++)
if(keyword[i]==past_password[i])
passFlag++;
/*** 5秒亮灯 ***/
if(passFlag==3 && OkKey==1)
{
HAL_GPIO_WritePin(LED1,GPIO_PIN_RESET);
HAL_GPIO_WritePin(LOCK,GPIO_PIN_SET);//解锁
HAL_GPIO_WritePin(LOCK,GPIO_PIN_RESET);
//PWM、LCD界面切换
PWM_Change(2000,0.1);
PWM_Display();
//这里的延时能让当前线程停止,按键无响应,保持输出2KHzPWM.
HAL_Delay(5000);
OkKey=0;//清空确认位
PWM_Change(1000,0.5);
LCD_Clear(Black);
}
/*** 密码三次以上错误 ***/
if(errorTime>=3 && OkKey==1)
{
HAL_GPIO_WritePin(LOCK,GPIO_PIN_SET);//解锁
HAL_GPIO_WritePin(LED2,GPIO_PIN_RESET); //亮
HAL_GPIO_WritePin(LOCK,GPIO_PIN_RESET);
HAL_Delay(100);
HAL_GPIO_WritePin(LOCK,GPIO_PIN_SET);//解锁
HAL_GPIO_WritePin(LED2,GPIO_PIN_SET); //灭
HAL_GPIO_WritePin(LOCK,GPIO_PIN_RESET);
//led2Tick1=uwTick;
//确保第一次延时
if(tempFlag2==1)
{
led2Tick2=uwTick;
tempFlag2=0;
}
if(uwTick-led2Tick2<=5000)return;
led2Tick2=uwTick;
errorTime=0;
OkKey=0;
tempFlag2=1;
}
}
2.3 PWM频率和占空比修改
初始化在生成后的tim.c中,修改占空比函数如下;
/**** 方波频率(HZ)和占空比修改 **/
void PWM_Change(u32 freq,float D)
{
//设置重装载数值改变频率
__HAL_TIM_SetAutoreload(&htim2,(int)(1000000.0/freq)-1);
//设置CCR值修改占空比
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,(int)(D*(1000000.0/freq)));
//更新定时器
HAL_TIM_GenerateEvent(&htim2,TIM_EVENTSOURCE_UPDATE);
}
2.4 串口修改密码
由于只需要接收密码设定和判断是否输入了正确的密码,可使用C库中的int sscanf()函数将接收到的字符串转为整数数组元素,再来判定每个值是否相等,同时返回值等于6(xxx-xxx)则证明解析每个值正确,当然也可以直接利用字符串相等的函数int strcmp()(相等返回0),密码长度很长时建议使用后者,这里使用前者即可。接收串口输入函数可用HAL_UART_Receive(),想调试是否修改密码情况可以在LCD上显示,也可用串口发送返回到上位机。
/*** 串口设定密码 ***/
int inputBool=0;
void setPassword()
{
passFlag=0;
HAL_UART_Receive(&huart1,(u8*)result,sizeof(result)-1,50);
//now_password作为上位机输入的密码(修改密码的许可),setword为要设定的密码值
inputBool=sscanf(result,"%1d%1d%1d-%1d%1d%1d",
&now_password[0],&(now_password[1]),&(now_password[2]),&(setword[0]),&(setword[1]),&(setword[2]));
//判断是否解析成功(格式正确)
if(inputBool==6)
for(int i=0;i<3;i++)
if(now_password[i]==past_password[i])passFlag++;
//如果格式正确输入密码也正确,则显示成功修改1秒,这里只是作为调试
if(passFlag==3)
{
LCD_DisplayStringLine(Line8,(u8*)"Success");
for(int i=0;i<3;i++)
past_password[i]=setword[i];
HAL_Delay(1000);
LCD_ClearLine(Line8);
}
}
发送数据(只作为调试),将past_password密码值传入data发送到上位机即可知道原有的密码past_password是否改为setword。
u32 usartTick=0;
void Usart_Send(u8* data)
{
if(uwTick-usartTick<500)return;
HAL_UART_Transmit(&huart1,data,strlen(data),50);
usartTick=uwTick;
}
2.5 main.c主要代码(不包含系统时钟配置等,这些Cube自动生成)
这里笔者忘记还有密码要初始值为“@”的要求,直接用LCD_DisplayChar即可,lcd.c/h和font.h竞赛时资料会发放。
main.c
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "tim.h"
#include "usart.h"
#include "lcd.h"
#include "led.h"
#include "key.h"
#include "stdio.h"
#include "string.h"
/*** 初始参数 ***/
unsigned char PSDString[3][20]={{0}},PWM_String[2][20]={{0}};
extern uint8_t key[3];
extern uint8_t keyword[3];
extern uint8_t errorTime;
char result[8];
/*** 串口输入设定密码 、原密码、当前输入密码、是否匹配标志 ***/
int setword[3]={0,0,0},past_password[3]={1,2,3},now_password[3]={0,0,0};
u8 passFlag=0;
/*** ***/
void SystemClock_Config(void);
/*** 自定义函数 ***/
void System_Init();
void setPassword();
void PSD_Display();
void PWM_Display();
void PWM_Change(u32 freq,float D);
/***/
int main(void)
{
HAL_Init();
SystemClock_Config();
/* USER CODE BEGIN SysInit */
System_Init();
/* USER CODE END SysInit */
while (1)
{
/* USER CODE END WHILE */
PSD_Display();
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/*** 系统初始化 **/
void System_Init()
{
LCD_Init();
KEY_Init();
USART1_UART_Init();
TIM2_Init();
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
LED_Init();
}
/*** 串口设定密码 ***/
int inputBool=0;
void setPassword()
{
passFlag=0;
HAL_UART_Receive(&huart1,(u8*)result,sizeof(result)-1,50);
inputBool=sscanf(result,"%1d%1d%1d-%1d%1d%1d",
&now_password[0],&(now_password[1]),&(now_password[2]),&(setword[0]),&(setword[1]),&(setword[2]));
//判断是否解析成功(格式正确)
if(inputBool==6)
for(int i=0;i<3;i++)
if(now_password[i]==past_password[i])passFlag++;
//如果格式正确输入密码也正确,则显示成功修改1秒
if(passFlag==3)
{
LCD_DisplayStringLine(Line8,(u8*)"Success");
for(int i=0;i<3;i++)
past_password[i]=setword[i];
HAL_Delay(1000);
LCD_ClearLine(Line8);
}
}
/***** 密码输入界面 ***/
void PSD_Display()
{
LCD_DisplayStringLine(Line1,(u8*)" PSD");
setPassword();
KEY_Input();
for(int i=0;i<3;i++)
sprintf((char*)PSDString[i]," B%d:%d",i,key[i]);
LCD_DisplayStringLine(Line3,PSDString[0]);
LCD_DisplayStringLine(Line4,PSDString[1]);
LCD_DisplayStringLine(Line5,PSDString[2]);
LED_LCD_Control();
}
/***** */
/**** 方波参数显示界面 **/
void PWM_Display()
{
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
LCD_DisplayStringLine(Line1,(u8*)" STA");
LCD_DisplayStringLine(Line3,(u8*)" F:2000HZ");
LCD_DisplayStringLine(Line4,(u8*)" D:10%");
}
/*****/
/**** 方波频率(HZ)和占空比修改 **/
void PWM_Change(u32 freq,float D)
{
__HAL_TIM_SetAutoreload(&htim2,(int)(1000000.0/freq)-1);
__HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,(int)(D*(1000000.0/freq)));
HAL_TIM_GenerateEvent(&htim2,TIM_EVENTSOURCE_UPDATE);
}
三、 调试结果
![](https://img-blog.csdnimg.cn/direct/8464f09f8bcb487b95d707efa08764c8.jpeg)
图 3-1 未确认开发板状态
![](https://img-blog.csdnimg.cn/direct/50357b7aa8694b9d85349028c940bf16.jpeg)
图 3-2 密码正确后开发板状态
![](https://img-blog.csdnimg.cn/direct/8a665fa1aa5a46149ea8495fc1e1767e.jpeg)
图 3-3 密码三次输入错误开发板状态
四、结语和注意事项
笔者第一次写博客,内容和排版较为粗糙,旨在分享内容和记录学习过程,希望文章对现在和未来的小伙伴能够有所帮助。注意:在只有一颗MCU的单片机上没办法多线程并行(虽然可以进行并发操作),所以在使用HAL_Delay()的时候会影响系统实时性,这里就可以借用滴答计时器的uwTick作为毫秒精确计数的方法,if语句执行也就几十纳秒,使用多的寄存器暂时存储当前的数对系统实时性影响几乎不计,尽量使用这种方式可以节省TIM资源,同时也不会像Delay函数一样中断线程。实际建立工程中,要注意代码规范、工程文件的分类性、可移植性,数据类型也需要注意,像8位还是32位数据对空间分配还是有影响的,单片机的flash空间也是有限的,写入数据时要注意量,像LCD写入图片数据量也不能太大。