梁山派入门指南2——滴答定时器&位带操作&按键输入
- 1. 滴答定时器
- 1.1 滴答定时器简介
- 1.2 相关寄存器
- 1.3 固件库函数
- 2. 位带操作
- 2.1 位带操作介绍
- 2.2 位带操作的优势
- 2.3 支持位带操作的内存地址
- 2.4 位带别名区地址的计算方式
- 2.5 位带操作使用示例
- 3 按键输入
- 3.1 独立按键原理图
- 3.2 独立按键配置流程
- 3.3 独立按键初始化
- 3.4 按键扫描&bsp_key文件
- 3.5 按键输入demo
1. 滴答定时器
1.1 滴答定时器简介
SysTick 定时器可用作标准的下行计数器,是一个 24 位向下计数器,有自动重新装载能力,可屏蔽系统中断发生器。Cortex-M4 处理器内部包含了一个简单的定时器,所有基于 M4 内核的控制器都带有 SysTick 定时器,这样就方便了程序在不同的器件之间的移植。SysTick 定时器可用于操作系统,提供系统必要的时钟节拍,为操作系统的任务调度提供一个有节奏的“心跳”。正因为所有的 M4内核的芯片都有 Systick 定时器,这在移植的时候不像普通定时器那样难以移植。
GD32中的RCU 通过 AHB 时钟(HCLK)8 分频后作为 Cortex 系统定时器(SysTick)的外部时钟。通过对
SysTick 控制和状态寄存器的设置,可选择上述时钟或 AHB(HCLK)时钟作为 SysTick 时钟。
GD32F4系列的时钟概览:
我们来看一下滴答定时器相关的部分:
SysTick 定时器设定初值并使能之后,每经过 1 个系统时钟周期,计数值就减 1,减到 0 时,SysTick计数器自动重新装载初值并继续计数,同时内部的 COUNTFLAG 标志位被置位,触发中断(前提开启中断)。
- 滴答定时器的时钟频率为30Mhz
1.2 相关寄存器
1.3 固件库函数
SysTick 定 时 器 的 使 用 主 要 有 SysTick_Config() 函 数 和 systick_clksource_set(uint32_t systick_clksource)函数,SysTick_Config()函数主要用来设置定时时间,systick_clksource_set()函数用来选择 SysTick 时钟源。
- systick_config()
/*!
\brief 配置sysTick的时钟和中断周期
\param[in] none
\param[out] none
\retval none
*/
void systick_config(void)
{
/* 设置sysTick中断的频率为1000Hz,即1ms触发1次 */
if(SysTick_Config(SystemCoreClock / 1000U)) {
/* capture error */
while(1) {
}
}
/* 设置sysTick的优先级为0 */
NVIC_SetPriority(SysTick_IRQn, 0x00U);
}
- SysTick_Handler()
/*!
\brief 滴答定时器中断服务函数
\param[in] none
\param[out] none
\retval none
*/
void SysTick_Handler(void)
{
/* delay--,delay是需要延时的时间 */
delay_decrement();
}
/*!
\brief delay decrement
\param[in] none
\param[out] none
\retval none
*/
void delay_decrement(void)
{
if(0U != delay) {
delay--;
}
}
- delay_1ms()
/*!
\brief 延时一段时间,
\param[in] count: count in milliseconds
\param[out] none
\retval none
*/
void delay_1ms(uint32_t count)
{
delay = count;
while(0U != delay) {
}
}
- systick_clksource_set()
以上就是滴答定时器涉及到的几个函数,最主要使用的应该就是,delay_1ms()这个函数,其他的只需要了解即可。。。。
2. 位带操作
2.1 位带操作介绍
为了减少“读-改-写”操作的次数,Cortex ® -M4 处理器提供了一个可以执行单原子比特操作的位带功能。存储器映射包含了两个支持位带操作的区域。其中一个是 SRAM 区的最低 1MB 范围,第二个是片内外设区的最低 1MB 范围。这两个区域中的地址除了普通应用外,还有自己的“位带别名区”。位带别名区把每个比特扩展成一个 32 位的字。当用户访问位带别名区时,就可以达到访问原始比特的目的。总结就是 CPU 不能直接对位带区中的单个数据位位寻址,只能通过对位带别名区的访问(或读/写)实现对位带区单个数据位的访问(或读/写),这种操作被称为位带操作。
2.2 位带操作的优势
- 更高效:避免了 ” 读-改-写 “ 的繁琐步骤
- 读取更简单:只需要直接读取位带别名区的内容,就可以直接对某个bit位寻址
- 访问速度更快:位带操作由于是属于硬件完成的,只需要CPU直接对位带别名区进行读写即可。
- 相对安全:在带有操作系统的开发中,多任务并发运行的时候就有可能在任务切换的过程中发生不可预料的问题,而位带操作由于是属于硬件完成的不可被异常打断的操作(原子操作),相对于读-写-改的操作模式会更安全。
- 提高运行效率和节省代码空间。简单的程序直接使用库函数或者寄存器操作,对于比较复杂的程序建议尽量使用位带操作来实现
2.3 支持位带操作的内存地址
并不是所有的区域都支持位带操作,下面是两个支持位带操作的区域:
- SRAM 区的最低 1MB: 0x2000_0000-0x200F_FFFF(对应位带别名区的地址:0x2200 0000)
- 片上外设区的最低 1MB:0x4000_0000-0x400F_FFFF(对应位带别名区的地址:0x4200 0000)
2.4 位带别名区地址的计算方式
上面介绍了位带操作的地址和优势,那怎么去查找目标比特对应的位带别名区的地址呢?
下面的公式表明了位带别名区中的每个字如何对应位带区的相应比特或目标比特:
bit_word_addr =bit_band_base +(byte_offset×32)+(bit_number×4)
其中:
- bit_word_addr指的是位带区目标比特对应在位带别名区的地址
- bit_band_base指的是位带别名区的起始地址
- byte_offset指的是位带区目标比特所在的字节的字节地址偏移量
- bit_number指的是目标比特在对应字节中的位置(0-7)
2.5 位带操作使用示例
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "bsp_led.h"
#include "sys.h"
#define BIT_ADDR(byte_offset,bit_number) (volatile unsigned long*)(0x42000000 +(byte_offset << 5)+(bit_number << 2)) // 位带别名区的地址
#define GPIOD_OCTL_OFFSET ((GPIOD + 0x14) - 0x40000000) // 偏移量
#define PDout(n) *(BIT_ADDR(GPIOD_OCTL_OFFSET,n)) // PD口输出
/************************************************
函数名称 : main
功 能 : 主函数
参 数 : 无
返 回 值 : 无
作 者 : LC
*************************************************/
int main(void)
{
systick_config(); // 滴答定时器初始化
led_gpio_config(); // led初始化
// gpio_bit_set(GPIOD,GPIO_PIN_7); // PD7输出高电平
// gpio_bit_reset(GPIOD,GPIO_PIN_7); // PD7输出低电平
while(1) {
//gpio_bit_write(PORT_LED2,PIN_LED2,SET); // LED2输出高电平
PDout(7) = 1; // PD7输出高电平
delay_1ms(1000); // 延时1s
//gpio_bit_write(PORT_LED2,PIN_LED2,RESET); // LED2输出低电平
PDout(7) = 0; // PD7输出低电平
delay_1ms(1000); // 延时1s
}
}
3 按键输入
3.1 独立按键原理图
我们打开梁山派提供的原理图,可以看到有三个按键,但是我们能使用的只有唤醒按键:
3.2 独立按键配置流程
打开梁山派开发指南,查看GPIO的输入配置流程如下:
3.3 独立按键初始化
根据配置流程,结合固件库手册中提供的库函数使用说明,开始编写初始化代码:
【注】:GPIO相关的库函数上一章有提到,可以查看:梁山派入门指南1——初窥门径梁山派
/************************************************
函数名称 : key_gpio_config
功 能 : 按键引脚初始化,默认配置为输入模式,下拉
参 数 : 需要初始化的引脚
返 回 值 : 无
作 者 : 不想写代码的我
*************************************************/
void key_gpio_config(void)
{
/* 开启时钟 */
rcu_periph_clock_enable(BSP_KEYUP_RCU);
/* 配置为输入模式,下拉 */
gpio_mode_set(BSP_KEYUP_PORT,GPIO_MODE_INPUT,GPIO_PUPD_PULLDOWN,BSP_KEYUP_PIN);
}
3.4 按键扫描&bsp_key文件
为了节省篇幅,在这里我直接展示我写好的bsp_key文件,不介绍具体原理
- bsp_key.c
#include "bsp_key.h"
/************************************************
函数名称 : key_gpio_config
功 能 : 按键引脚初始化,默认配置为输入模式,下拉
参 数 : 需要初始化的引脚
返 回 值 : 无
作 者 : 不想写代码的我
*************************************************/
void key_gpio_config(void)
{
/* 开启时钟 */
rcu_periph_clock_enable(BSP_KEYUP_RCU);
/* 配置为输入模式,下拉 */
gpio_mode_set(BSP_KEYUP_PORT,GPIO_MODE_INPUT,GPIO_PUPD_PULLDOWN,BSP_KEYUP_PIN);
}
/************************************************
函数名称 : WKUP_Down_Task
功 能 : KEY_UP按键按下时调用的函数
参 数 : 需要初始化的引脚
返 回 值 : 无
作 者 : 不想写代码的我
*************************************************/
void WKUP_Down_Task(void)
{
gpio_bit_toggle(PORT_LED2,PIN_LED2);
}
/************************************************
函数名称 : WWKUP_Up_Task
功 能 : KEY_UP按键弹起时调用的函数
参 数 : 需要初始化的引脚
返 回 值 : 无
作 者 : 不想写代码的我
*************************************************/
void WWKUP_Up_Task(void)
{
}
/************************************************
函数名称 : Key_One_Scan
功 能 : 按键扫描函数,。这里只能扫描KEY_UP按键
参 数 : 需要初始化的引脚
返 回 值 : 无
作 者 : 不想写代码的我
*************************************************/
void Key_One_Scan(uint8_t KeyName ,void(*OnKeyOneUp)(void), void(*OnKeyOneDown)(void))
{
static uint8_t Key_Val[Key_Name_Max]; //按键值的存放位置
static uint8_t Key_Flag[Key_Name_Max]; //KEY0~2为0时表示按下,为1表示松开,WKUP反之
Key_Val[KeyName] = Key_Val[KeyName] <<1; //每次扫描完,将上一次扫描的结果左移保存
switch(KeyName)
{
case Key_Name_WKUP: Key_Val[KeyName] = Key_Val[KeyName] | (gpio_input_bit_get(BSP_KEYUP_PORT,BSP_KEYUP_PIN)); //读取WKUP按键值
break;
default:
break;
}
if(KeyName == Key_Name_WKUP) //WKUP的电路图与其他按键不同,所以需要特殊处理
{
//WKUP特殊情况
//当按键标志为1(松开)是,判断是否按下,WKUP按下时为0xff
if(Key_Val[KeyName] == 0xff && Key_Flag[KeyName] == 1)
{
(*OnKeyOneDown)();
Key_Flag[KeyName] = 0;
}
//当按键标志位为0(按下),判断按键是否松开,WKUP松开时为0x00
if(Key_Val[KeyName] == 0x00 && Key_Flag[KeyName] == 0)
{
(*OnKeyOneUp)();
Key_Flag[KeyName] = 1;
}
}
}
- bsp_key.h
#ifndef _BSP_KEY_H
#define _BSP_KEY_H
#include "gd32f4xx.h"
#include "systick.h"
#include "bsp_led.h"
#define BSP_KEYUP_RCU RCU_GPIOA // 按键端口时钟
#define BSP_KEYUP_PORT GPIOA // 按键端口
#define BSP_KEYUP_PIN GPIO_PIN_0 // 按键引脚
typedef enum
{
Key_Name_WKUP = 0,
Key_Name_Max
}EnumKeyOneName;
/* 按键引脚初始化,默认配置为输入模式,下拉 */
void key_gpio_config(void);
/* KEY_UP按键按下按下和弹起时调用的函数*/
void WKUP_Down_Task(void);
void WWKUP_Up_Task(void);
/* 按键扫描函数 */
void Key_One_Scan(uint8_t KeyName ,void(*OnKeyOneUp)(void), void(*OnKeyOneDown)(void));
#endif /* _BSP_KEY_H */
3.5 按键输入demo
结合上面的bsp_key文件,就可以编写出一个按键扫描的demo,代码如下:
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "bsp_led.h"
#include "bsp_key.h"
/*!
\brief main function
\param[in] none
\param[out] none
\retval none
*/
int main(void)
{
uint16_t i=0;
/* 配置sysTick的时钟和中断周期*/
systick_config();
/* led初始化 */
led_gpio_config(Led_Name_Led1);
led_gpio_config(Led_Name_Led2);
key_gpio_config();
while(1)
{
if(++i >= 50)
{
/* 500ms翻转一次led的状态 */
gpio_bit_toggle(PORT_LED1,PIN_LED1);
i = 0;
}
Key_One_Scan(Key_Name_WKUP,WWKUP_Up_Task,WKUP_Down_Task);
delay_1ms(10);
}
}
【注】:这里的按键输入是采用GPIO输入的方式实现的,后续中断中还会对bsp_key文件进行进一步封装。
关于按键扫描的原理,可以参照:夜深人静学32系列9——GPIO驱动数码管/蜂鸣器/按键/LED 和 STM32框架之按键扫描新思路
以上就是本期的所有内容,创造不易,点个关注再走呗。