在进入本章内容之前,我们先回顾一下前两节提出的一些问题: 1)如何找到固件库自带函数位置? 2)#include 的作用是什么? 3)将函数模块化的优与劣。 如果你已经完成了前两节的例程,那么就带着疑问与笔者一起走进这一节的内容吧。
4.5按键检测点亮LED例程详解
4.5.1工程主体构成
在这里我们可以看到工程目录下有五个文件夹,这里简单介绍一下其中内容与作用,想要更深入了解的同学也不要心急,跟着后续教程学习你的水平必然水涨船高。 我们还需要知道三件事: 第一,是用C语言编写程序时,编写的内容被储存在文本文件中,该文件被称为源代码文件(source code file),大部分C系统都要求文 件名以.c结尾,在文件名中,点号(.)前面 的部分称为基本名(basename),点号后面的部分称为扩展名(extension)。基本名与扩展名的组合就是文件名。 第二,为了方便管理.c文件中定义的各种函数,我们一般会将 .c 文件中定义的函数名在 .h 文件中进行声明,这样在外部需要调用某部分函数时只需使用 #include 将 .h 文件包含进去即可,不需再次声明。 第三,#include这行代码是一条C预处理器指令(preprocessor directive)。通常,C编译器在编译前会对源代码做一些准备工作,即预处理 (preprocessing)。 例,#include "bsp_key.h"的作用相当于把bsp_key.h文件的所有内容都输入该行所在的位置,实际上,这是一种“拷贝-粘贴”的操作(注,此处及上述部分内容引用修改《C Prime Plus》第二章中部分基础知识内容)。
STARTUP:这里放着WB32的启动文件,这是系统上电复位后执行的第一个程序,启动代码充当着程序和操 作系统之间的接口,不需要人为修改。 CMSIS:这里存放着WB32的系统配置文件,主要管理着WB32的时钟频率,除需要修改系统工作频率外不需要修改。 FWLIB:这里存放着WB32的外设固件库函数,我们想要使用WB32,就必须会使用好存放在这里的.c文件。 例,在按键检测点亮LED例程中,我们会使用到核心开发板的GPIO功能,对应的就需要找到 “wb32f10x_gpio.c” 文件。//笔者上面提到过,如果在 .c 文件中定义的函数一般会在 .h 文件中进行声明,所以我们可以直接使用 #include 指令对 .h 文件进行包含即可在外部调用对应的函数了。 USER:此处存放main函数,wb32f10x_it.c 文件以及其他由用户自己编写的函数模块。(wb32f10x_it.c 一般用来存放中断相关函数,后续课程会进行补充) DOC:这里一般存放说明性文档 (readme.txt),可以记录开发过程中的细节问题等。
4.5.2用户代码分析
1.LED驱动程序 以下分别为bsp_led.c与bsp_led.h文件
#include "bsp_led.h"
void LED_GPIO_Config(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BMX1 | LED_GPIO_CLK, ENABLE );
GPIO_Init(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_MODE_OUT |GPIO_OTYPE_PP |GPIO_PUPD_NOPULL |GPIO_SPEED_HIGH);
GPIO_SetBits(LED_GPIO_PORT, LED_GPIO_PIN);
}
#ifndef __BSP_LED_H
#define __BSP_LED_H
#include "wb32f10x.h"
#define LED_GPIO_PIN GPIO_Pin_13
#define LED_GPIO_PORT GPIOC
#define LED_GPIO_CLK RCC_APB1Periph_GPIOC
#define LED_Toggle GPIO_ToggleBits(LED_GPIO_PORT,LED_GPIO_PIN)
#define LED_OFF GPIO_SetBits(LED_GPIO_PORT,LED_GPIO_PIN)
#define LED_ON GPIO_ResetBits(LED_GPIO_PORT,LED_GPIO_PIN)
void LED_GPIO_Config(void);
#endif
—————————————————————————————————————————————————————————————————————————————————————— 我们首先来了解bsp_led.h部分代码: 1)
#ifndef __BSP_LED_H
#define __BSP_LED_H
#include "wb32f10x.h"
#endif
往往在编写头文件时都需要编写这些内容,这里先不做过多讲解,具体如何修改格式请跟随下面的内容一起学习。 —————————————————————————————————————————————————————————————————————————————————————— 2)
#define LED_GPIO_PIN GPIO_Pin_13
#define LED_GPIO_PORT GPIOC
#define LED_GPIO_CLK RCC_APB1Periph_GPIOC
看到注释,大家已经知道了这是对于LED使用到的GPIO的宏定义,为什么要使用宏定义呢?我们在前面的内容大致了解了宏定义是为了提高代码的可维护性。 在这里大家还注意到如“GPIO_Pin_13”、“GPIOC”、“RCC_APB1Periph_GPIOC”这样代码,这些代码底层原理我们暂不深究,初学者只需知道这样的代码是WB32官方在固件库中规定好的,我们要做的只是根据实际使用需求更改成对应的GPIO端口(GPIO_PORT)和引脚(GPIO_PIN)即可。 其中,RCC_APB1Periph_GPIOC,请注意这个函数,RCC代表时钟外设,APB1Periph代表总线位置,GPIOC代表我们要开启时钟的端口,连在一起代表的含义就是:控制LED所使用的GPIO端口的时钟,这个函数我们可以在 wb32f10x_rcc.h 这个头文件中找到。 问题:如果使用GPIOB的引脚12作为LED的控制引脚,请根据下图将此部分代码进行修改。(关于系统结构会在以后的教学中详解)
—————————————————————————————————————————————————————————————————————————————————————— 3)
#define LED_Toggle GPIO_ToggleBits(LED_GPIO_PORT,LED_GPIO_PIN)
#define LED_OFF GPIO_SetBits(LED_GPIO_PORT,LED_GPIO_PIN)
#define LED_ON GPIO_ResetBits(LED_GPIO_PORT,LED_GPIO_PIN)
在这部分代码里,我们根据实际使用需求,定义了控制LED亮、灭和翻转三个状态的宏,三个宏中对应使用的函数GPIO_ToggleBits、GPIO_SetBits、GPIO_ResetBits可以在 wb32f10x_gpio.h 中找到函数声明,那么这些函数该如何使用呢? 例:
首先可以看到,上方英文注释,这里标注着函数功能,形参可以传入的范围(请注意观察自己使用的开发板端口,由于引脚数量限制,不是所有的开发板都有A、B、C、D…等端口)。GPIO_ToggleBits函数需要传入两个形参,分别是 GPIO_TypeDef* GPIOx 和 uint16_t GPIO_Pin ,传入形参进入函数后,通过GPIOx->ODR ^= GPIO_Pin; 进行运算,进而使函数完成对GPIO端口在0与1之间进行状态翻转的过程。 这里涉及到的寄存器操作的知识不需要深究,在目前我们初学时只需要了解库函数中声明的函数的作用,学会调用即可,化繁为简正是库函数开发要做的事。 —————————————————————————————————————————————————————————————————————————————————————— 4)
void LED_GPIO_Config(void);
这个就是LED配置函数的声明,一般会在完成 .c 文件后统一在 .h 文件中统一声明,上面已经讲过声明的作用,这里不过多赘述。 —————————————————————————————————————————————————————————————————————————————————————— 5)
#include "bsp_led.h"
现在开始讲bsp_led.c文件,代码开始首先包含了"bsp_led.h"文件,这是因为在 .c 文件中编写函数时需要使用到我们在 .h 文件中定义好的宏,如果不包含 .h 文件而直接引用,编译器就会报未定义错误。 —————————————————————————————————————————————————————————————————————————————————————— 6)
void LED_GPIO_Config(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BMX1 | LED_GPIO_CLK, ENABLE );
GPIO_Init(LED_GPIO_PORT, LED_GPIO_PIN, GPIO_MODE_OUT |GPIO_OTYPE_PP |GPIO_PUPD_NOPULL |GPIO_SPEED_HIGH);
GPIO_SetBits(LED_GPIO_PORT, LED_GPIO_PIN);
}
在WB32中,初始化一个GPIO引脚只需要两个函数,第一个便是开启时钟。 时钟就是心脏,心脏跳动才能工作。那么如何找到时钟使能函数所在位置,如何确定使用哪一条函数、如何传入参数就是我们必须要理解的事。 对于初学者,请在KEIL中将鼠标放在RCC_APB1PeriphClockCmd上,右击进入定义,可以在wb32f10x_rcc.c中看到 (此处向下翻阅即可看到RCC_APB2PeriphClockCmd) 注释说明此函数的功能及应当传入何种形参,根据我们的需求,我们需要打开C端口的13引脚的时钟,根据前面给出的系统框图可以看到,GPIOC端口在APB1总线上,所以我们应当向此函数中传入RCC_APB1Periph_GPIOC,而我们在头文件中将RCC_APB1Periph_GPIOC宏定义为LED_GPIO_CLK。所以,传入的参数应当为LED_GPIO_CLK,但实际传入的参数为RCC_APB1Periph_GPIOC。(由于WB32特有的保护机制,我们在使能时钟时应同时传入RCC_APB1Periph_BMX1,其中“|”即为“位或”) 问题:若想打开端口A的时钟,传入哪些参数?请你在评论区写一写。
接下来进行GPIO初始化,进入GPIO_Init函数(请猜一猜这个函数在哪个文件里) 请看注释,本函数功能为根据引脚设置对GPIO引脚进行初始化,传入的参数除了GPIO端口、GPIO引脚外还有GPIO设置,但注释里并没有给出此形参建议。 本段配置GPIO输出的代码为(GPIO_MODE_OUT |GPIO_OTYPE_PP |GPIO_PUPD_NOPULL |GPIO_SPEED_HIGH),分别代表的意思为 ( 输出模式| 推挽输出|无上下拉|高速模式 )。 此处定义可在"wb32f10x_gpio.h"中找到 (未来会结合案例详解GPIO配置模式,此处不做过多解释)
在开启端口时钟和初始化GPIO后,为了使开发板上LED处于上电关闭的状态,我们使用GPIO_SetBits函数将对应端口引脚置位1。(不理解为何这样配置可以使LED关闭的小伙伴可以查看WB32开发板原理图的LED部分)
—————————————————————————————————————————————————————————————————————————————————————— 2.按键驱动程序 以下分别为bsp_key.c与bsp_key.h文件
#include "bsp_key.h"
#include "bsp_delay.h"
void KEY_GPIO_Config(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BMX1 | KEY1_GPIO_CLK, ENABLE );
GPIO_Init(KEY1_GPIO_PORT,KEY1_GPIO_PIN, GPIO_MODE_IN | GPIO_PUPD_NOPULL);
}
uint16_t Key_Scan(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
if(GPIO_ReadInputDataBit( GPIOx,GPIO_Pin ) == KEY_ON)
{
SOFT_DELAY;
while( GPIO_ReadInputDataBit( GPIOx,GPIO_Pin ) == KEY_ON);
return KEY_ON;
}
else return KEY_OFF;
}
#ifndef __BSP_KEY_H
#define __BSP_KEY_H
#include "wb32f10x.h"
#define KEY_ON 1
#define KEY_OFF 0
#define KEY1_GPIO_PIN GPIO_Pin_12
#define KEY1_GPIO_PORT GPIOB
#define KEY1_GPIO_CLK RCC_APB1Periph_GPIOB
void KEY_GPIO_Config(void);
uint16_t Key_Scan(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin);
#endif
—————————————————————————————————————————————————————————————————————————————————————— 首先查看"bsp_key.h"部分 1)
#ifndef __BSP_KEY_H
#define __BSP_KEY_H
#include "wb32f10x.h"
硬件宏定义语句
函数声明语句
#endif
根据第一部分LED的代码分析,我们可以观察.h头文件部分的框架,大致是相同的,不同点有“__BSP_KEY_H”、“__BSP_KEY_H”还有硬件宏定义语句和函数声明语句。 关于
#ifndef xx
#define xx
#endif
这个部分的主要含义是防止编译头文件时出错,“xx”其实并无具体的格式要求,开发者亦可按照自己的编写方式去命名,但最好还是遵循这个格式书写,若无特殊需求不必深究此部分代码。 例:有如下头文件key.h,编写此部分内容
#ifndef __KEY_H
#define __KEY_H
#endif
而宏定义语句根据所涉及到的硬件不同和设计的功能不同也会不同,函数声明亦是如此。 —————————————————————————————————————————————————————————————————————————————————————— 2)
#define KEY_ON 1
#define KEY_OFF 0
#define KEY1_GPIO_PIN GPIO_Pin_12
#define KEY1_GPIO_PORT GPIOB
#define KEY1_GPIO_CLK RCC_APB1Periph_GPIOB
关于GPIO端口引脚宏定义大家已经了解了,不再赘述,还不懂的可以参考LED代码分析内容。 关于宏定义输入端口检测到的两种状态,在按键检测电路中,我们往往使用高电平触发的方式,即按键的一脚接在控制器的VCC,另一脚接在控制器引脚处用来检测按键的接通状态,当按键按下时,GPIO引脚与VCC相当于直接导通,即为高电平状态1,当按键松开时,GPIO引脚与VCC断路,即为低电平状态0。所以将这两种状态通过宏定义为方便理解的名称。 —————————————————————————————————————————————————————————————————————————————————————— 接下来看"bsp_key.c"部分 3)
#include "bsp_key.h"
#include "bsp_delay.h"
与分析LED代码时不同,本部分代码多包含了一个"bsp_delay.h"头文件。根据学过的知识我们可以了解到,但凡包含,必定会使用到被包含的头文件里的函数,我们在后续内容分析。 —————————————————————————————————————————————————————————————————————————————————————— 4)
void KEY_GPIO_Config(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BMX1 | KEY1_GPIO_CLK, ENABLE );
GPIO_Init(KEY1_GPIO_PORT,KEY1_GPIO_PIN, GPIO_MODE_IN | GPIO_PUPD_NOPULL);
}
按键GPIO初始化函数,时钟开启部分参考代码分析,主要看GPIO初始化函数里的GPIO配置部分。 在LED代码分析中,需要控制器端口输出来控制LED的亮灭,而在按键检测函数中,则需要检测GPIO端口的状态来判断按键是否按下,因此在初始化函数中的GPIO配置部分有所不同,需要将GPIO配置为无上下拉输入(输入模式 | 无上下拉)。 —————————————————————————————————————————————————————————————————————————————————————— 5)
uint16_t Key_Scan(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
if(GPIO_ReadInputDataBit( GPIOx,GPIO_Pin ) == KEY_ON)
{
SOFT_DELAY;
while( GPIO_ReadInputDataBit( GPIOx,GPIO_Pin ) == KEY_ON);
return KEY_ON;
}
else return KEY_OFF;
}
定义一个按键扫描函数,通过GPIO_ReadInputDataBit函数检测GPIO位的电平,为了防止按键抖动造成的不良影响,编写一个软件防抖函数来保证按键功能正常实现。 首先判断按键是否按下,若第一次判断按键按下则延迟一定时间再次判断是否真的按下,若按下则返回按键打开(KEY_ON),否则返回按键关闭(KEY_OFF)。(在软件消抖中使用到了延时函数,这就是本部分开头包含"bsp_delay.h"的原因,关于延时函数会在下面内容分析) —————————————————————————————————————————————————————————————————————————————————————— 3.延时函数代码分析 以下分别为bsp_delay.c与bsp_delay.h文件
#include "bsp_delay.h"
void Delay( __IO uint32_t nCount)
{
for( ;nCount!=0;nCount--);
}
#ifndef __BSP_DELAY_H
#define __BSP_DELAY_H
#include "wb32f10x.h"
#define SOFT_DELAY Delay(0x0000FF)
void Delay( __IO uint32_t nCount);
#endif
先看bsp_delay.c 1) 就是一个简单延时函数,相信大家都懂。 —————————————————————————————————————————————————————————————————————————————————————— 2)
#define SOFT_DELAY Delay(0x0000FF)
void Delay( __IO uint32_t nCount);
在bsp_delay.h中,对延时函数声明,然后对延时函数赋值并宏定义为一个方便定义的名称,这样外部函数在调用时仅需使用宏定义的名称即可达到延时的目的了,方便漂亮。 —————————————————————————————————————————————————————————————————————————————————————— 3)为什么要这么麻烦?连一个延时函数都要单独封装。这是因为大家现在刚刚接触WB32库开发,编写的还是一些简单的程序,等到后续接触的程序越来越复杂,各个函数之间需要相互调用时,你就会了解这样做的好处了。 —————————————————————————————————————————————————————————————————————————————————————— 4.main函数
#include "wb32f10x.h"
#include "bsp_led.h"
#include "bsp_key.h"
int main(void)
{
LED_GPIO_Config();
KEY_GPIO_Config();
while (1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON)
{
LED_Toggle;
}
}
}
最后来讲一讲main函数,在进入main 函数后,首先要将要使用到的LED配置和按键配置初始化(调用这些函数进行初始化时不必再带上void 和形参)。 为了让按键每次按下后都能翻转LED的状态,需要进入while循环,在循环中判断按键扫描得到的返回值是否等于KEY_ON,若等于,即按键按下,则调用LED_Toggle翻转LED的状态,如果按键没有按下则继续判断,直到按下为止。 ——————————————————————————————————————————————————————————————————————————————————————
4.5.3总结
本章内容到此结束,本节对第二节例程的代码进行了详细的分析,相信用户对WB32库开发已经有了一个初步的认识,在教程中你还遇到过哪些问题呢?欢迎留言讨论。
|