目录
一、何谓有使用价值的单片机程序
二、 硬件板及设计目的
三、建立工程
1.配置GPIO
2.配置时钟源和Debug
3.配置系统时钟
四、代码编写与修改
1.创建应用程序文件夹
2.编写应用文件keyed.h和 keyled. c
3.修改main.c
一、何谓有使用价值的单片机程序
前面我写了很多MCU程序开发相关的文章,这些文章都是以学习知识为目的的简单应用程序开发,不是以产品开发为目的进行的软件开发。这些简单的软件都集中写在main.c和main.h里,其特征是文件没有目录层次,源代码(函数)直接写在main.c里,main.c程序结构复杂,易读性差,可移植性差。
- 有使用价值单片机程序首要特征是main.c程序结构扁平、简单,可读性好。
- 有使用价值单片机程序建立了与应用相关的文件目录层次,在项目浏览器里,会建立与产品应用相关的文件夹,可以不止一个,文件夹里包括与产品应用有关的.c,.h文件。
- 在与产品应用有关的.h文件定义了大量的与应用相关的宏;在与产品应用有关的.c文件定义了大量的与产品应用相关的宏函数、方法;
- main.c程序中调用自定义的宏函数(方法),所以代码很短,可读性好。
- 有使用价值单片机程序可移植性好。配置参数(MCU管脚资源)大量标识资源的别名,不直接使用管脚编程。这样编写的程序可移植性好,当更换硬件MCU,对目标MCU的管脚标识相同的名称,源程序几乎可以不修改就使用(新旧平台管脚的高低电平不同时需要简单修改)。
二、 硬件板及设计目的
本文使用的硬件板是ST的开发板NUCLEO-G474RE,板上MCU型号为STM32G474RET6。并按照资源提示设计制造了扩展IO板,有需要此扩展板的留言联系我。
设计一个示例Demo6_1KeyLED,其功能和操作流程如下。
- 按下KeyLeft键时,使LED2的输出翻转。
- 按下KeyRight键时,使LED1的输出翻转。
- 按下KeyUp键时,使LED1和LED2的输出都翻转。
- 按下KeyDown键时,蜂鸣器输出翻转。
根据按键、LED和蜂鸣器的电路,整理出MCU连接的GPIO引脚的输入/输出配置:
用户标签 | 引脚名称 | 引脚功能 | GPIO模式 | 默认电平 | 上拉或下拉 | |
LED1 | PB11 | GPIO_Output | 推挽输出 | High | 上拉 | |
LED2 | PB12 | GPIO_Output | 推挽输出 | High | 上拉 | |
KeyRight | K1 | PA0 | GPIO_Input | 输入 | 上拉 | |
KeyDown | K2 | PA1 | GPIO_Input | 输入 | 上拉 | |
KeyLeft | K3 | PA6 | GPIO_Input | 输入 | 上拉 | |
KeyUp | K5 | PA7 | GPIO Input | 输入 | 上拉 | |
Buzzer | PA4 | GPIO_Output | 推挽输出 | LOW | 上拉 |
三、建立工程
1.配置GPIO
配置PB11、PB12,GPIO OUTPUT,默认High Level,PP,PullUp,High Speed,标识为LED1,LED2;
配置PA4,GPIO OUTPUT,默认Low Level,PP,PullUp,High Speed,标识为Buzzer;
配置PA0,Input mode,PP,标识为:KeyRight;
配置PA1,Input mode,PP,标识为:KeyDown;
配置PA6,Input mode,PP,标识为:KeyLeft;
配置PA7,Input mode,PP,标识为:KeyUp;
2.配置时钟源和Debug
打开System Core中的RCC,高速时钟(HSE)选择Crystal/ eramic Resonator,使用片外时钟晶体作为HSE的时钟源。在SYS中将Debug设置Serial Wire。
3.配置系统时钟
将系统时钟(SYSCLK)频率配置为170 MHz。
四、代码编写与修改
1.创建应用程序文件夹
在项目根目录下,我们创建一个文件夹 KEYLED,然后创建文件 keyled.h和 keyled. c,保
存到这个文件夹下。创建方法:
把鼠标光标放置在项目名称上,右键→属性(Properties),点击→C/C++ General→Paths and Symbols→选择Includes菜单,点击ADD,添加新的文件目录KEY_LED→一定要点击应用按钮。
再选择Source Location菜单 → 点击ADD,在提示窗口中,选择刚刚新添加的文件夹KEY_LED → 最后,一定要点击应用按钮。
2.编写应用文件keyled.h和 keyled. c
/* keyled.h */
#include "main.h" //在main.h中定义了Keys、LEDs和Buzzer引脚的宏
//表示4个按键的枚举类型
typedef enum {
KEY_NONE = 0, //没有按键被按下
KEY_LEFT, //KeyLeft键
KEY_RIGHT, //KeyRight键
KEY_UP, //KeyUp键
KEY_DOWN, //KeyDown键
}KEYS;
#define KEY_WAIT_ALWAYS 0 //作为函数ScanKeys()的一种参数,表示一直等待按键输入
//轮询方式扫描按键,timeout=KEY_WAIT_ALWAYS时一直扫描,否则等待时间timeout,单位ms
KEYS ScanPressedKey(uint32_t timeout);
#ifdef LED1_Pin //LED1的控制
#define LED1_Toggle() HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin) //输出翻转
#define LED1_ON() HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_RESET) //输出0,亮
#define LED1_OFF() HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_SET) //输出1,灭
#endif
#ifdef LED2_Pin //LED2的控制
#define LED2_Toggle() HAL_GPIO_TogglePin(LED2_GPIO_Port,LED2_Pin) //输出翻转
#define LED2_ON() HAL_GPIO_WritePin(LED2_GPIO_Port,LED2_Pin,GPIO_PIN_RESET) //输出0,亮
#define LED2_OFF() HAL_GPIO_WritePin(LED2_GPIO_Port,LED2_Pin,GPIO_PIN_SET) //输出1,灭
#endif
#ifdef Buzzer_Pin //蜂鸣器的控制
#define Buzzer_Toggle() HAL_GPIO_TogglePin(Buzzer_GPIO_Port,Buzzer_Pin) //输出翻转
#define Buzzer_ON() HAL_GPIO_WritePin(Buzzer_GPIO_Port,Buzzer_Pin,GPIO_PIN RESET)//输出0,蜂鸣器不响
#define Buzzer_OFF() HAL_GPIO_WritePin(Buzzer_GPIO_Port,Buzzer_Pin,GPIO _PIN_SET) //输出1,蜂鸣器响
#endif
这个文件包含了头文件main.h,因为要用到main.h中GPIO引脚标签的宏。
LED和蜂鸣器的操作定义为宏函数,例如,LED1_Toggle()使LED1输出翻转,LED1_ON(),点亮LED1,LED1_OFF()熄灭LED1。只有定义了GPIO引脚的宏之后,才会编译这些宏函数, 所以一个项目里如果用不到蜂鸣器,CubeMX中不定义蜂鸣器的GPIO引脚即可。
文件还定义了表示按键的枚举类型KEYS,函数ScanPressedKey(uint32_t timeout)用于检测按 建输入,参数timeout是等待时间,如果timeout为KEY_WAIT_ALWAYS,就表示无限等待时 间。函数的返回值是按下的按键的枚举值。
/* keyled.c */
#include "keyled.h"
//轮询方式扫描4个按键,返回按键值
//timeout单位ms,若timeout=0表示一直扫描,直到有键按下
KEYS ScanPressedKey(uint32_t timeout)
{
KEYS key=KEY_NONE;
uint32_t tickstart = HAL_GetTick(); //当前计数值
const uint32_t btnDelay=10; //按键按下阶段的抖动,延时再采样时间
GPIO_PinState keyState;
while(1)
{
#ifdef KeyLeft_Pin //如果定义了KeyLeft,就可以检测KeyLeft
keyState=HAL_GPIO_ReadPin(KeyLeft_GPIO_Port, KeyLeft_Pin); //PA6=KeyLeft,低输入有效,LED1
if (keyState==GPIO_PIN_RESET)
{
HAL_Delay(btnDelay); //前抖动期
keyState=HAL_GPIO_ReadPin(KeyLeft_GPIO_Port, KeyLeft_Pin); //再采样
if (keyState ==GPIO_PIN_RESET)
return KEY_LEFT;
}
#endif
#ifdef KeyRight_Pin //如果定义了KeyRight,就可以检测KeyRight
keyState=HAL_GPIO_ReadPin(KeyRight_GPIO_Port, KeyRight_Pin); //PA0=KeyRight,低输入有效,LED0
if (keyState==GPIO_PIN_RESET)
{
HAL_Delay(btnDelay); //前抖动期
keyState=HAL_GPIO_ReadPin(KeyRight_GPIO_Port, KeyRight_Pin);//再采样
if (keyState ==GPIO_PIN_RESET)
return KEY_RIGHT;
}
#endif
#ifdef KeyDown_Pin //如果定义了KeyDown,就可以检测KeyDown
keyState=HAL_GPIO_ReadPin(KeyDown_GPIO_Port, KeyDown_Pin); //PA1=KeyDown,输入低电平时蜂鸣
if (keyState==GPIO_PIN_RESET)
{
HAL_Delay(btnDelay); //前抖动期
keyState=HAL_GPIO_ReadPin(KeyDown_GPIO_Port, KeyDown_Pin);//再采样
if (keyState ==GPIO_PIN_RESET)
return KEY_DOWN;
}
#endif
#ifdef KeyUp_Pin //如果定义了KeyUp,就可以检测KeyUp
keyState=HAL_GPIO_ReadPin(KeyUp_GPIO_Port, KeyUp_Pin); //PA7=KeyUp,输出低电平时双闪
if (keyState== GPIO_PIN_RESET)
{
HAL_Delay(btnDelay); //10ms 抖动期
keyState=HAL_GPIO_ReadPin(KeyUp_GPIO_Port, KeyUp_Pin);//再采样
if (keyState == GPIO_PIN_RESET)
return KEY_UP;
}
#endif
if (timeout != KEY_WAIT_ALWAYS) //没有按键按下时,会计算超时,timeout时退出
{
if ((HAL_GetTick() - tickstart) > timeout)
break;
}
}
return key;
}
函数ScanPressedKey()用轮询方式检测按键输入,也就是用函数HAL_GPIO_ReadPin()不断地读取4个按键引脚的输入,如果某个按键引脚的输入信号有效,就表示检测到按键输入了, 将这个按键的枚举值作为函数返回值。代码使用了条件编译,只有一个按键的引脚宏被定义后,才编译相应代码段。
函数参数timeout定义了一个超时,单位是ms。在程序中,首先用函数HAL_GetTick()获取系统嘀嗒信号当前计数值,赋值给tickstart,系统嘀嗒信号计数值增大1,就表示过了1ms, 所以用HAL_GetTick()减去tickstart可以得到程序运行的时间。如果timeout设置为0,就表示一直等待,直到有按键按下。
3.修改main.c
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "keyled.h"
/* USER CODE END Includes */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
KEYS curKey = ScanPressedKey(KEY_WAIT_ALWAYS); //�?测按键输入,�?直等�?
switch(curKey)
{
case KEY_LEFT: //keyLeft
LED1_Toggle();
break;
case KEY_RIGHT: //KeyRight
LED2_Toggle();
break;
case KEY_UP: //KeyUp
LED1_Toggle();
LED2_Toggle();
break;
case KEY_DOWN: //KeyDown
Buzzer_Toggle();
default:
break;
}
HAL_Delay(200); //按键弹起阶段的消抖动延时
}
/* USER CODE END 3 */
}
main()函数中添加的用户代码比较简单,就是在while()循环里用函数ScanPressedKey()一直 等待按键输入,检测到某个有效按键后,函数返回按键值,程序根据按键值做出相应的处理。 在while()循环的最后执行HAL_Delay(200)延时200ms,这是为了消除按键弹起阶段的抖动影响。这个延时时间可以调整,要既能消除按键抖动,又不至于使程序响应迟钝。
构建项目无误后,我们将其下载到开发板并加以测试,连续运行时分别按4个按键,就会看到开发板上的LED和蜂鸣器的变化符合设计预期。