基于stm32f401的双按键可视化多模式选择模块

目录

0.作品信息(含源码,需要自取)

1.视频演示部分

2.代码复盘部分

2.1 懒鬼的第一步——GPIO初始化封装 

2.1.1 封装GPIO初始化的初衷

2.1.2 头文件(GPIO_Inition.h)

2.1.3 源文件(GPIO_Inition.c)

2.2 按键封装

2.2.1 封装按键的初衷

2.2.2 头文件(KEY.h)

2.2.3 源文件(KEY.c)

2.3 定时器中断

2.3.1 选择使用并封装定时器中断的初衷

2.3.2 头文件(timer.h)

2.3.3 源文件(timer.c)

2.4 主程序

2.4.1 全中断编程!!

2.4.2 源码(main.c)

3.盘后总结(随便唠唠)

3.1 关于为什么要自己一点点封装库函数来实现功能

3.1.1 自己封装库函数的初衷

3.1.2 关于注释

3.1.3 怎么提高效率

3.2 关于遇到的问题

3.2.1 奇怪的幽灵引脚👻

3.2.2 OLED奇怪的刷屏机制😠

3.2.3 按键检测(逻辑严谨太重要啦!!!)

3.2.4 早起摸鱼和饭后摸鱼🐟

3.3未来展望


0.作品信息(含源码,需要自取)

设计及实现者:陈琛

指导老师:周冰航

学校:湖南大学

使用到的外设模块:某宝2元一个的按键(俩)、某宝10块包邮的1.3寸4脚OLED一个(建议买个0.96寸的,因为网上大多是0.96寸OLED的库函数,用1.3寸的会和我一样清屏清不干净☠)

整个项目的工程源码(提取码:cc66):https://pan.baidu.com/s/1cDpzO8iwLbVhdDOsORO_Vw?pwd=cc66

功能介绍:

双按键可视化多模式选择模块,理论上通过两个按键结合OLED翻页显示等库函数的功能即可以选择至多2的32次方种功能(即以一个u32变量来存储被选的功能编号即可)

目前未加上翻页功能,后续若所需选择的模式多余8种可能会考虑添加(因为OLED单页能显示8行)

该模块共使用两个按键,它们的双击和长按功能是一致的——即双击确认选择,此时锁定光标“*”,使其不再闪烁,同时此时可以通过对全局变量spos_y的读取来得知哪一个模式被选中了。长按则解锁被锁定的光标,使其重新闪烁并可以运动。

单击左边绿色接PB7的按键可以在光标自由时令其上移一行(若在第一行会移动到最后一行)

单击左边红色接PB14的按键可以在光标自由时令其下移一行(若在最后一行会移动到第一行)

同时,每次按下按键都会触发一次屏幕刷新,这样可以确保屏幕不会有大面积乱码。

1.视频演示部分

可视化多模块选择模块演示

2.代码复盘部分

2.1 懒鬼的第一步——GPIO初始化封装 

2.1.1 封装GPIO初始化的初衷

众所周知,在对stm32f4xx系列单片机的GPIO进行初始化时大致需要——开启对应时钟、初始化结构体、对结构体所有成员进行配置、将结构体地址回传给初始化函数共四步约如下7行:

// Initialization Structure
GPIO_InitTypeDef GPIO_InitStructure;

// X here can be A-K (这一句注释用英文写得比较顺手,不要喷我)
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOX, ENABLE);

GPIO_InitStructure.GPIO_Mode = XX;

// 这里若是输出模式则是GPIO_InitStructure.GPIO_OType
GPIO_InitStructure.GPIO_PuPd = XX;

// x here can be 0-15
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_x

GPIO_InitStructure.GPIO_Speed = XX;

GPIO_Init(GPIOX, &GPIO_InitStructure);

习惯的做法是在每一个外设的初始化部分重复进行这7行进行相关配置,虽然这么做代码复用率其实已经很高了,但是作为一个自动化专业的准大三资深懒狗(迫真),我决定把这七行压缩到一行。(能1行搞定的事为什么要我做7行🤪)

2.1.2 头文件(GPIO_Inition.h)

俗话说的好,万事开头难,在想要封装一个库函数或者实现某些功能的时候,首先自己要对其中需要的函数、变量和大致的底层逻辑有一个构想。所以先撰写一下头文件,先把模板打好:

#ifndef __GPIOINITION__H 
#define __GPIOINITION__H
#include "stm32f4xx.h"



#endif

在这之后需要来思考函数的构造和变量的使用,首先,我的目标是用一行代码顶替上述的七行代码,那其实在变量方面就相当easy,只需要把原来那七行中所有必需的变量抠出来即可。

仔细康康那七行,不难发现,要对GPIO进行初始化首先要知道是哪一个GPIO,也就是需要一个GPIOx之类的变量,其次是引脚口Pin的值,再来是引脚的使用模式和速度共四个关键变量。

那么这几个变量取什么类型好呢,因为在封装这个库时我正在复习C语言中枚举enum的相关知识,因此我把除Pin之外的变量都进行了enum枚举来定义。(这是一个复习编程知识的好方法)

首先是GPIO使用模式的枚举:(顺便一说,我英语不好,所有英文注释来自百度翻译)

// Enum The Mode Of GPIO
typedef enum{
	CMode_AF   	 = 0x00, /*!< GPIO Alternate function Mode */
	CMode_AN   	 = 0x01, /*!< GPIO Analog Mode */
	CMode_PP   	 = 0x02, /*!< Multiplexed push-pull output,
							  high and low power average can be driven */
	CMode_OD   	 = 0x03, /*!< Open-drain output, low-level drive */
	CMode_NOPULL = 0x04, /*!< Floating input */
	CMode_UP 	 = 0x05, /*!< Drop-up input, which is high by default */
	CMode_DOWN	 = 0x06  /*!< Drop-down input, which is low by default */
}CMode;	

细心如你一定发现了我这里的枚举中竟然没有输入和输出模式,这是为什么呢?因为我是懒鬼,为了缩减所需变量数量,我想到通过具体的输入/输出模式去反推该GPIO是要配输入还是输出。

接下来是IO口的枚举:(这里C是我的姓“陈”的缩写,没有特殊含义哈哈哈)

// IO Selection--There are 11 selections
typedef enum{
	CIO_A	   =	   ((uint32_t)0x00000001),  /* GPIOA selected */
    CIO_B      =  	   ((uint32_t)0x00000002),  /* GPIOB selected */
    CIO_C      =       ((uint32_t)0x00000004),  /* GPIOC selected */
	CIO_D      =       ((uint32_t)0x00000008),  /* GPIOD selected */
	CIO_E      =       ((uint32_t)0x00000010),  /* GPIOE selected */
	CIO_F      =       ((uint32_t)0x00000020),  /* GPIOF selected */
	CIO_G      =       ((uint32_t)0x00000040),  /* GPIOG selected */
	CIO_H      =       ((uint32_t)0x00000080),  /* GPIOH selected */
	CIO_I      =       ((uint32_t)0x00000100),  /* GPIOI selected */
	CIO_J      =       ((uint32_t)0x00000200),  /* GPIOJ selected */
	CIO_K      =       ((uint32_t)0x00000400),  /* GPIOK selected */
	CIO_All    =       ((uint32_t)0x000007FF)   /* All GPIOx selected */
}CIO;	

这一步枚举中的值原本是想用来做复选的,即可以通过或运算对IO口进行复选,但是后来发现要这么搞就不能枚举,应当用宏定义来做,即:

// IO Selection--There are 11 selections
#define	    CIO_A	   	   ((uint32_t)0x00000001),  /* GPIOA selected */
#define     CIO_B          ((uint32_t)0x00000002),  /* GPIOB selected */
#define     CIO_C          ((uint32_t)0x00000004),  /* GPIOC selected */
#define	    CIO_D          ((uint32_t)0x00000008),  /* GPIOD selected */
#define	    CIO_E          ((uint32_t)0x00000010),  /* GPIOE selected */
#define	    CIO_F          ((uint32_t)0x00000020),  /* GPIOF selected */
#define	    CIO_G          ((uint32_t)0x00000040),  /* GPIOG selected */
#define	    CIO_H          ((uint32_t)0x00000080),  /* GPIOH selected */
#define	    CIO_I          ((uint32_t)0x00000100),  /* GPIOI selected */
#define	    CIO_J          ((uint32_t)0x00000200),  /* GPIOJ selected */
#define    	CIO_K          ((uint32_t)0x00000400),  /* GPIOK selected */
#define	    CIO_All        ((uint32_t)0x000007FF)   /* All GPIOx selected */

 此时需要注意在函数中CIO_x就得声明成uint32_t的类型了。当然,最后由于我懒得改,还是保留了枚举的设计,毕竟一般复用都是全选的,可以直接传一个CIO_All的参。

最后是对IO口速度的枚举:

// Speed Selection--There are 4 gears
typedef enum{
	CSpeed_LOW 	     = 0x00, /*!< Low speed */
	CSpeed_MEDIUM	 = 0x01, /*!< Medium speed */
	CSpeed_FAST  	 = 0x02, /*!< Fast speed */
	CSpeed_HIGH      = 0x03, /*!< High speed */
}CSpeed;	

 这里GPIO_Pin这一参数我保留了原来函数中对其的定义,可以反敲“Go to Definition”看看ST官方提供的源码中对于GPIO_Pin这一变量的定义,贴出来给大家看看:

#define GPIO_Pin_0                 ((uint16_t)0x0001)  /* Pin 0 selected */
#define GPIO_Pin_1                 ((uint16_t)0x0002)  /* Pin 1 selected */
#define GPIO_Pin_2                 ((uint16_t)0x0004)  /* Pin 2 selected */
#define GPIO_Pin_3                 ((uint16_t)0x0008)  /* Pin 3 selected */
#define GPIO_Pin_4                 ((uint16_t)0x0010)  /* Pin 4 selected */
#define GPIO_Pin_5                 ((uint16_t)0x0020)  /* Pin 5 selected */
#define GPIO_Pin_6                 ((uint16_t)0x0040)  /* Pin 6 selected */
#define GPIO_Pin_7                 ((uint16_t)0x0080)  /* Pin 7 selected */
#define GPIO_Pin_8                 ((uint16_t)0x0100)  /* Pin 8 selected */
#define GPIO_Pin_9                 ((uint16_t)0x0200)  /* Pin 9 selected */
#define GPIO_Pin_10                ((uint16_t)0x0400)  /* Pin 10 selected */
#define GPIO_Pin_11                ((uint16_t)0x0800)  /* Pin 11 selected */
#define GPIO_Pin_12                ((uint16_t)0x1000)  /* Pin 12 selected */
#define GPIO_Pin_13                ((uint16_t)0x2000)  /* Pin 13 selected */
#define GPIO_Pin_14                ((uint16_t)0x4000)  /* Pin 14 selected */
#define GPIO_Pin_15                ((uint16_t)0x8000)  /* Pin 15 selected */
#define GPIO_Pin_All               ((uint16_t)0xFFFF)  /* All pins selected */

可见这里每一个Pin的编码n恰好对应着十六位二进制数中第n位的1,这个从硬件原理上来说是因为16个Pin引脚口的电平对应着一个三十二位寄存器的低十六位。从这个原理上可以看出这样的定义可以通过或"|"结合Pin脚来同时使能多个引脚。

到这里前期准备工作就结束了,把这些变量塞进一个初始化函数来声明一下头文件就完成了。

// GPIO Initialization
void GPIO_Inition(CMode mode, CIO CIO_x, uint32_t GPIO_Pin, CSpeed speed);

2.1.3 源文件(GPIO_Inition.c)

有了具体思路之后,直接开干!

首先,由于我的keil版本较低,本人较懒不想更新,因此我的结构体声明部分必须在函数最开头,所以先定义一手GPIO初始化结构体:

void GPIO_Inition(CMode mode, CIO CIO_x, uint32_t GPIO_Pin, CSpeed speed)
{
	// Initialization Structure
	GPIO_InitTypeDef GPIO_InitStructure;
}

接下来就根据传入的参数来完成GPIO的初始化设置就好了。

首先是模式的设置:

这里通过switch-case代码块对模式进行设置,可以看到这里我的懒狗精神在default处淋漓尽致,由于输入模式有上拉(UP),下拉(DOWN)以及浮空(NOPULL)三种,而输出只有开漏(OD,Open Drain),推挽(Push-Pull)两种模式。因此,根据包多不包少的原则,我将默认模式设置为输入,以此优化switch结构。

此后根据对GPIO_InitStructure.GPIO_Mode的读取来相应的配置输入/输出模式即可。

    // Set GPIO Mode
	switch(mode)
	{
		case CMode_AF:
			GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
			break;
		case CMode_AN:
			GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
			break;
		case CMode_PP:
			GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
			break;
		case CMode_OD:
			GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
			break;
		default:
			// Input by default
			GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
			break;
	}
	
	// Input Mode Config
	if (GPIO_InitStructure.GPIO_Mode == GPIO_Mode_IN)
	{
		switch(mode)
		{
			case CMode_NOPULL:
				GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
				break;
			case CMode_UP:
				GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
				break;
			default:
				// Drop-down input by default
				GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;
				break;
		}
	}
	// Output Mode Config
	else if (GPIO_InitStructure.GPIO_Mode == GPIO_Mode_OUT)
	{
		switch(mode)
		{
			case CMode_PP:
				GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
				break;
			default:
				GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
				break;
		}
	}

 紧接着,进行最轻松的一步,引脚配置,因为GPIO_Pin延用了官方的定义,所以可以直接配置:

    // Pin Config
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin;

 然后是速度配置:

这里依旧是平平无奇的switch-case代码块

    // Set Speed
	switch(speed)
	{
		case CSpeed_LOW:
			GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
			break;
		case CSpeed_MEDIUM:
			GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz;
			break;
		case CSpeed_FAST:
			GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
			break;
		default:
			// High speed by default
			GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
			break;
	}

接下来是IO口的选择和时钟的开启(这俩扔一块是因为这俩都只需要GPIOx,哦不,CIO_x这一个变量即可完成)

    // Enable Clock & Bind Init Function with Pointer of Structure
	switch(CIO_x)
	{
		case CIO_A:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOA, &GPIO_InitStructure);
			break;
		case CIO_B:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOB, &GPIO_InitStructure);
			break;
		case CIO_C:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOC, &GPIO_InitStructure);
			break;
		case CIO_D:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOD, &GPIO_InitStructure);
			break;
		case CIO_E:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOE, &GPIO_InitStructure);
			break;
		case CIO_F:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOF, &GPIO_InitStructure);
			break;
		case CIO_G:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOG, &GPIO_InitStructure);
			break;
		case CIO_H:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOH, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOH, &GPIO_InitStructure);
			break;
		case CIO_I:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOI, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOI, &GPIO_InitStructure);
			break;
		case CIO_J:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOJ, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOJ, &GPIO_InitStructure);
			break;
		case CIO_K:
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOK, ENABLE);
			// Bind Init Function with Pointer of Structure
			GPIO_Init(GPIOK, &GPIO_InitStructure);
			break;
		default:
			// Enable All The Clock by Default
			RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|
								   RCC_AHB1Periph_GPIOB|
								   RCC_AHB1Periph_GPIOC|
								   RCC_AHB1Periph_GPIOD|
								   RCC_AHB1Periph_GPIOE|
								   RCC_AHB1Periph_GPIOF|
								   RCC_AHB1Periph_GPIOG|
								   RCC_AHB1Periph_GPIOH|
								   RCC_AHB1Periph_GPIOI|
								   RCC_AHB1Periph_GPIOJ|
								   RCC_AHB1Periph_GPIOK, ENABLE);
			break;
	}

 至此,GPIO初始化封装便完成了。

2.2 按键封装

2.2.1 封装按键的初衷

由于本人不幸学过一点C++,受其面向对象编程、模块化编程思想的“荼毒”较深,因此一想到要用多个按键来实现相关功能,就立马想到对它进行结构体封装(C语言没有类难受死我了)。

2.2.2 头文件(KEY.h)

既然要进行结构体封装,那就要确定好需要哪些成员变量。

首先,我们只有两个按键,却想要实现多功能的选择,因此我们需要对按键进行多种交互模式的监测(我这里用的是单击、双击、长按三种模式的检测),所以需要一个成员来存储该按键的状态。

其次,在监测按键按下事件时,需要有一个变量来区分是哪个按键,所以再来一个成员存储该按键的编号即可。

由上述的分析,我们就轻轻松松得到了下面的结构体:(这里我采用了官方的注释风格,可以方便该库的使用和延拓)

/**

* @addtogroup KEY Structure 

* @brief 	  Key Struct

* @arg 	 	  key_state:Describe this key is which state at this moment


*/
typedef struct{
	
	KEYSTATE key_state;				/** State of the key, can be 1...4
									Meaning of it can be find @ref KEYSTATE */
	
	uint8_t key_id;					/* Id of the key, can be 0...255 */
	
}KEY;

这里的KEYSTATE是对按键状态的枚举:

/**

* @brief Enum 4 states of keys

*/
typedef enum
{

	KEYSTATE_None 			= 	((uint8_t)0x00),			/* None State:Nothing has happened */
	KEYSTATE_Click 			= 	((uint8_t)0x01),			/* Click State:Key has been clicked once */
	KEYSTATE_Click_Double 	= 	((uint8_t)0x02),			/* Click_Double State:Key has been clicked twice */
	KEYSTATE_Click_Long 	= 	((uint8_t)0x03)			/* Click_Long State:Key has been pressed for a long time */

}KEYSTATE;

 有了按键的结构体之后就是初始化函数的声明,由于初始化希望能将每一个按键对应一个结构体,所以首先需要知道结构体的地址和他的成员变量初始值。此外,用到IO口的外设的初始化肯定逃不掉GPIO初始化,所以还需要知道GPIO口和对应Pin脚。

P.S.:由于我使用的按键外设统统是高电平触发,所以我默认封装的按键初始化中是不需要模式和速度参数的,直接默认下拉输入(DOWN)和高速(HIGH)。顺便一说,像板子自带的用户按键ukey一类的低电平触发的按键则需要对相应的IO口配置上拉输入(UP),这么做是为了确保按键没按下时IO口有稳定的值。(不明白的都给我去看江科大自化协的视频!!!😡)

明确了所必需的变量后随便给孩子函数取个名吧:

/* Initialize the Key Structure */
void KEY_Init(KEY *key, KEYSTATE state, uint8_t id, 
			  CIO IOx, uint16_t GPIO_Pin);

这样就完成了初始化函数的声明。接下来,我们遵循C艹的类中构造-析构的有始有终的思想,进行一个将结构体“复位”的函数的声明(也即将其状态复位回未按下)

/* Clear the Key State */
void KEY_Clear(KEY *key);

 紧接着,重量级函数登场——按键检测函数:

这里先大致说一下思路,因为我准备基于定时器中断来进行按键检测(即配置一个10ms触发一次的定时器中断来对按键进行监听)。那么在监听到首次按下后先浅浅计两个数(相当于延时20ms),这是为了消抖(De-Shake),此后若仍然监听到按下事件就证明真的按下了,此时再对按下时间计数来区分短按、长按,若是短按,则再多听一会看看有没有第二次按下事件,有的话标记一个双击即可。具体逻辑如下图:

 从该逻辑可以看出我们需要监听按键是否按下,这一步显然是通过读取IO口引脚电平实现的,所以我们还需要得知该按键对应的GPIO口和引脚编号才行。

综上,我们便得到了如下声明:(说一下这里的*GPIOx类型怎么确定的,因为读取电平的标准库函数是ReadInputDataBit(),所以反敲去看看它的定义,把它的参数copy过来就好了嘿嘿。到达代码最高层——CV工程师!!)

/* Detect The State Of The Key */
void KEY_DETECTION(KEY* key, GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

接下来,我们来构思一下这俩按键拿来干啥,并实现一下相应功能的函数:

首先,我们既然是要通过可视化来实现多功能选择,那么两个按键想选多个模式,我想到的主意便是通过移动屏幕上的某一个光标字符来选择模式。因此,一左一右两个按钮单击就分别被我定义为光标向上和光标向下的操作。同时两者双击均代表选中当前模式并锁定光标(让他不再闪烁且不响应移动指令),长按即可取消选中。所以得到了以下四个函数的声明(光标上移、下移、选中、取消):

/* Cursor Move Up One Unit */
void Cursor_Up(void);

/* Cursor Move Down One Unit */
void Cursor_Down(void);

/* Confirm Selection */
void Selection_Confirm(void);

/* Cancel Current Selection */
void Selection_Cancel(void);

 至此,按键库的头文件封装就搞定了。

2.2.3 源文件(KEY.c)

下面来实现上述功能。

按键初始化“首当其冲”:

/**

* @breif 	Initialize the Key Structure

* @arg 		key			:The pointer to the aim key structure

* @arg		state		:State of the key, can be 1...4
						 Meaning of it can be find @ref KEYSTATE 

* @arg		id			:Id of the key, can be 0...255

* @arg 		IOx			:Describe Which GPIO Will be Activated

* @arg 		GPIO_Pin	:The pin(s) that the key output channel used 

* @return 	None

*/
void KEY_Init(KEY *key, KEYSTATE state, uint8_t id, 
			  CIO IOx, uint16_t GPIO_Pin)
{

	key->key_state = state;
	key->key_id    = id;
	
	// Initialize the GPIO
	GPIO_Inition(CMode_DOWN, IOx, GPIO_Pin, CSpeed_HIGH);
	
}

很精简吧,通过GPIO初始化的封装,这一个Init函数只有三行,嘿嘿。 

紧接着是按键复位,这个同初始化一样,原理简单,不再赘述:

/**

* @breif 	Clear the Key Structure's State

* @arg 		key:The pointer to the aim key structure

* @return 	None

*/
void KEY_Clear(KEY *key)
{

	key->key_state = KEYSTATE_None;

}

这两个函数中唯一值得注意的就是由于按键地址的传参是一个Pointer(放个洋屁哈哈,其实就是指针的意思) ,所以在函数中操作对应结构体的成员变量需要使用"->"不能使用"."来调用。

接下来,有请重量级嘉宾——按键检测函数登场,老规矩,先贴代码再复盘思路:

/**

* @breif 	Detect Which State Of The Key Is

* @arg 		key	 	:The pointer to the aim key structure

* @arg 		GPIOx	:Describe Which GPIO Will be Activated4

* @arg 		GPIO_Pin:The pin(s) that the key output channel used 

* @return 	None

*/
void KEY_DETECTION(KEY* key, GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin) 
{
	
	static uint16_t 	press_cnt[5] = {0, 0, 0, 0, 0};
	static uint8_t 		press_bit[5] = {0, 0, 0, 0, 0};
	
	if(key->key_state==KEYSTATE_None)
	{
		
		if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin) == 1 && press_bit[key->key_id]==0)
		{	
			// First Press
			press_bit[key->key_id]=1;
			press_cnt[key->key_id]=0;
		}
		else if(press_bit[key->key_id]==1)
		{	
			// De-shake
			press_cnt[key->key_id]++;
			// wait 10ms for stable signal
			if(press_cnt[key->key_id]==1)
			{
				press_cnt[key->key_id]=0;
				press_bit[key->key_id]=2;
			}
		}
		else if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin)==1 && press_bit[key->key_id]==2)
		{	
			// Detect the keeping time of the First Press
			press_cnt[key->key_id]++;
			if(press_cnt[key->key_id] > 65)
			{		
				//If Current Press Event Has Keep More Than 650ms
				press_cnt[key->key_id]=0;
				press_bit[key->key_id]=5;		
			}
		}
		else if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin)==0 && press_bit[key->key_id]==5)
		{	
				// Reset Count and Bit After Long Press
				press_cnt[key->key_id]=0;
				press_bit[key->key_id]=0;	
				// Long Press
				key->key_state=KEYSTATE_Click_Long;		
		}
		else if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin)==0 && press_bit[key->key_id]==2 && press_cnt[key->key_id] <= 50)
		{
			// Click during a short time between 0ms-50ms
			press_cnt[key->key_id]=0;
			press_bit[key->key_id]=3;
		}
		else if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin)==0 && press_bit[key->key_id]==3)
		{	
			// Detect whether the key has been pressed in 250ms after first press 
			press_cnt[key->key_id]++;
			if(press_cnt[key->key_id] > 30)
			{	
				/** Key has @Not been pressed in 250ms after first press */ 
				press_bit[key->key_id]=0;		
				press_cnt[key->key_id]=0;
				// Single Click
				key->key_state=KEYSTATE_Click;	
			}
		}
		else if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin)==1 && press_bit[key->key_id]==3 && press_cnt[key->key_id] < 30)
		{	
			// Key has been pressed again in 250ms after first press 
			press_bit[key->key_id]=4;			
			press_cnt[key->key_id]=0;
		}
		else if(GPIO_ReadInputDataBit(GPIOx, GPIO_Pin)==0 && press_bit[key->key_id]==4)
		{	
			// Wait for the release of Double-click
			press_bit[key->key_id]=0;			
			press_cnt[key->key_id]=0;
			// Double-Click
			key->key_state=KEYSTATE_Click_Double;		
		}
	}
	
}

 实现这一函数时,首先我是没有用数组存储按下状态标志press_bit和按下时长计数press_cnt的,我直接在中断函数里扔了两句Detection,很快我发现由于为了方便计数而定义的静态变量会由第一个检测函数影响到第二个,所以常出现两个按键的状态同时改变的bug(这是通过在屏幕上显示两个按键对应IO口的电平调试时发现的,所以说,年轻人有OLED是真的好!OLED调试是真***好用!)

为了解决上述问题,我一不小心想到了用数组存储来将不同按键的按下标志、计时分开(因为前面正好结构体中有个叫id的东东,正好可以用来当数组独一无二的下标嘿嘿嘿)

剩下的其实就是对于上面那个流程图的简单实现,把图中没确定的一些阈值反复调教调试到比较理想的情况就好,至于咋样算理想就见仁见智了(学小金广发)

最后就是对四个响应函数的编写啦!先贴代码:

/**

* @breif Let The Cursor "*" Move Up One Unit 

*/
void Cursor_Up(void)
{
	
	// Make Sure Current State Allow Us Move The Cursor
	if (!s_ok )
	{
	
		if (spos_y == 1)
			spos_y = 4;
		else
			spos_y--;
		
		OLED_Refresh();
	
	}

}

/**

* @breif Let The Cursor "*" Move Down One Unit 

*/
void Cursor_Down(void)
{

	// Make Sure Current State Allow Us Move The Cursor
	if (!s_ok )
	{
	
		spos_y = spos_y%4 + 1;
		OLED_Refresh();
	
	};

}

/**

* @breif Confirm Current Selection

*/
void Selection_Confirm(void)
{
	if ( !s_ok )
	{
	
		s_ok = 1;
		OLED_Refresh();
	
	}
		

}

/**

* @breif Cancel Current Selection

*/
void Selection_Cancel(void)
{
	
	if ( s_ok )
	{
	
		s_ok = 0;
		OLED_Refresh();
	
	}
}

因为是前期做着备着的,所以暂时只显示一页与四个模式,理论上可以选择2的32次方种模式,这是为什么我就不说了,懂得都懂,不懂得提示一下一个叫uint32_t的东东。

同时,关于光标我选择了符号"*",并通过main.c中定义一个spos_y和一个s_ok分表示这一标志(symbol)的y坐标(即在哪一行,也同时是用来看是哪个模式的,s_ok则用来分辨光标此时是锁定的还是自由的),那么通过在KEY.c中extern一下这俩变量即可。

extern uint16_t spos_y;
extern uint16_t s_ok;

这个最好写在KEY.c的最开始的地方嗷。

 那么,上面四个函数就很好理解了叭,首先是光标上移,也就是spos_y--,这里只需要注意一下如果当前是第一行就把spos_y定为4,也即最后一行。

而下移那里取模+1的操作相信学过计组或者接触过数据结构的你一定DNA狂动吧嘿嘿(没错这里不用if判断特殊条件,直接取模+1,这样的思路同样可以用于数据结构中环状队列(circular queues)中的front、rear的移动)

s_ok的标志变动就不多说了,0技术含量。

2.3 定时器中断

2.3.1 选择使用并封装定时器中断的初衷

在和老师交流的时候,老师就有说过按键检测用定时器中断来实现比较奈斯,但是那时候我刚学到外部中断,并且为之震撼,于是我的叛逆劲就上来了——嘿,我还偏要用外部中断来实现一下按键检测。结果一不小心真给弄出来了,但是反复调试效果都不甚理想,因为外部中断逗留的时间短到ms对它来说都太久了。于是我想到用delay_us来制造微秒级的技术来进行消抖和按键检测,经过两天的努力(mō yú),它虽然能大概进行响应,但是漏检、误检是常有的事,而delay最小的单位就是us了,在一番思想挣扎之后我放弃了外部中断,果断往下学定时器中断。

学了半天之后发现定时器中断太香了,外部中断就是弟中之弟,除了检测外部事件次数之外的功能一律无脑定时器中断绝对没错!!

2.3.2 头文件(timer.h)

首先,声明一个定时器初始化的“模板”函数(没错这里是我的懒鬼心态在作祟,想封装一个库函数一行实现定时器的初始化)

P.S.:这里的参数确定流程详见2.1.1嗷

// TIMx Initialization
void TIM_Initialize(TIM_TypeDef* TIMx, uint16_t DIVx, uint16_t CountMode,
					uint16_t Per, uint16_t Pre, uint16_t Rep, uint16_t TIM_IT_X);

然后来初始化一个定时器3用来完成我们的模块吧!

// TIM3 Initialization
void TIM3_Init(void);

 既然是定时器,那不显示一下时间是不是有点对不起它的名字了哈哈哈,所以我定义了一个TIM3_ShowTime(void)函数用来在 OLED 上显示过去了几秒。

然后select_y和select_n是用来控制光标"*"在屏幕上的闪烁的,如果当前锁定了光标(或者说选中了某一模式),那么就显示一个"*",不然的话通过定时器计时来交替显示空格" "和光标"*"来实现待选择时的闪烁效果。

最后的Main_Show是用来显示屏幕上的信息的,比如每一行的模式名称和光标"*"之类的。

// Show The Time that TIM3 Has Counted At This Moment
void TIM3_ShowTime(void);
// Do Not Shining When Current Mode Has been Selected
void select_y(void);
// Shining When Current Mode Has @Not been Selected
void select_n(void);
// Show Main Information
void Main_Show(void);

2.3.3 源文件(timer.c)

先定义一下用到的全局变量

extern uint16_t TIM_count;
extern KEY key3,key4;
extern uint16_t spos_y;
extern uint16_t s_ok;

 然后先来实现最没技术含量的初始化封装和TIM3初始化:

值得说明的一点是该函数中不包括NVIC的配置,需要在具体定时器的初始化函数中对NVIC进行配置。

/**

* @breif TIMx Initialization

* @arg	 TIMx 		: Represent Which Timer Will be Used
					  x can be any number during 1...14

* @arg 	 DIVx 		: This parameter describes how many parts the main frequency will be divided into
					  Value of it Can be finded @ref TIM_Clock_Division_CKD
				
* @arg 	 CountMode 	: This parameter describes which mode will be used to count
					  Value of it Can be finded @ref TIM_Counter_Mode
					  
* @arg	 Per		: Specifies the period value to be loaded into the active
                      Auto-Reload Register at the next update event.
                      This parameter must be a number between 0x0000 and 0xFFFF.
					  
* @arg	 Pre		: Specifies the prescaler value used to divide the TIM clock.
                      This parameter can be a number between 0x0000 and 0xFFFF.
					  
* @arg	 Rep		: Specifies the repetition counter value. Each time the RCR downcounter
                      reaches zero, an update event is generated and counting restarts
                      from the RCR value (N).
                      This means in PWM mode that (N+1) corresponds to:
                      - the number of PWM periods in edge-aligned mode
                      - the number of half PWM period in center-aligned mode
                      This parameter must be a number between 0x00 and 0xFF. 
                      @note This parameter is valid only for TIM1 and TIM8.

* @arg	 TIM_IT_X	: This parameter describes which interrupt trigger mode will be used
                      Value of it Can be finded @ref TIM_interrupt_sources.

*/
void TIM_Initialize(TIM_TypeDef* TIMx, uint16_t DIVx, uint16_t CountMode,
					uint16_t Per, uint16_t Pre, uint16_t Rep, uint16_t TIM_IT_X)
{

	// Initialize the TBU(ref. TimeBaseUnit) Initial Structure
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	
	// Turn on the corresponding clock
	if (TIMx == TIM1)
	{
	
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
	
	}
	else if (TIMx == TIM8)
	{
	
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE);
	
	}
	else if (TIMx == TIM9)
	{
	
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM9, ENABLE);
	
	}
	else if (TIMx == TIM10)
	{
	
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM10, ENABLE);
	
	}
	else if (TIMx == TIM11)
	{
	
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM11, ENABLE);
	
	}
	else if (TIMx == TIM2)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	
	}
	else if (TIMx == TIM3)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
	
	}
	else if (TIMx == TIM4)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
	
	}
	else if (TIMx == TIM5)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);
	
	}
	else if (TIMx == TIM6)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
	
	}
	else if (TIMx == TIM7)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);
	
	}
	else if (TIMx == TIM12)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM12, ENABLE);
	
	}
	else if (TIMx == TIM13)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM13, ENABLE);
	
	}
	else if (TIMx == TIM14)
	{
	
		RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14, ENABLE);
	
	}
	
	// Choose which clock source will be used
	TIM_InternalClockConfig(TIMx);
	
	// TBU InitStructure Config
	TIM_TimeBaseInitStructure.TIM_ClockDivision = DIVx;			/* 1 division */
	TIM_TimeBaseInitStructure.TIM_CounterMode = CountMode; 	/* Count Up */
	TIM_TimeBaseInitStructure.TIM_Period = Per - 1;
	TIM_TimeBaseInitStructure.TIM_Prescaler = Pre - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = Rep;
	
	// Combine the TBU InitStructure to the Init Function
	TIM_TimeBaseInit(TIMx, &TIM_TimeBaseInitStructure);
	
	// Clear The Flag Bit
	TIM_ClearITPendingBit(TIMx, TIM_IT_X);
	
	// Enable The Update Channel
	TIM_ITConfig(TIMx, TIM_IT_X, ENABLE);
	
	// Start The Timer
	TIM_Cmd(TIMx, ENABLE);
	
}

下面TIM3的初始化中配置NVIC时用到了我封装的NVIC初始化函数,一行搞定嘿嘿嘿,这里因为我们需要不断对按键进行检测,因此不想错过每一次按键按下的可能,所以将定时器设置成10ms触发一次。这里实现10ms的方法是设置84M/8400 = 10kHz的计数频率与100的自动重装载值实现的。(84M是因为我用的stm32f401最大频率是84M的,这个参数也需要见仁见智)

/**

* @breif TIM3 Initialization

*/
void TIM3_Init(void)
{
	
	// Initialize the TIM3
	TIM_Initialize(TIM3, TIM_CKD_DIV1, TIM_CounterMode_Up,
				   100, 8400, 0, TIM_IT_Update);
	
	// Initialize the NVIC
	NVIC_Initialize(TIM3_IRQn, 0, 0);
	
}

 接下来是时间的显示,我希望它1s计时一次,不然刷屏频率就太快了,所以需要对每次计数的值做一个判断,满了100次中断才给显示的数字加1。

/**

* @breif  Show The Time that TIM3 Has Counted At This Moment
* @arg	  None
* @return None

*/
void TIM3_ShowTime(void)
{
	
	static uint8_t temp = 0;
	
	// 定时结束就记一次数
	OLED_ShowString(1, 5, "Time:", 8);	
	OLED_ShowNum(80, 5,TIM_count ,5, 12);
	// 1S
	// TIM_count++;
	// 10ms
	if (temp <= 100)
	{
	
		temp ++;
	
	}
	else
	{
	
		temp = 0; 
		TIM_count++;
		
	}

}

接着是中断函数的实现:

只需要每次中断里一直调用按键响应函数即可

/**

* @breif  Interruption Function of Timer 3
* @arg	  None
* @return None

*/
void TIM3_IRQHandler(void)
{
	
	// Ensure The Program Already Trigger The Interruption
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
	{
		
		Key_Inter_Respond();
	
	}
	
	// Clear The Flag Bit
	TIM_ClearITPendingBit(TIM3, TIM_IT_Update);

}

光标显示控制函数:

 可以看到,通过定时器中断来计时我们在待选状态下实现了光标"*"0.5S的交替闪烁功能。

/**

* @breif Do Not Shining When Current Mode Has been Selected

*/
void select_y(void)
{
	
	OLED_ShowChar(60, spos_y,'*',8);

}

/**

* @breif Shining When Current Mode Has @Not been Selected

*/
void select_n(void)
{
	
	static uint16_t shining_count = 0;
	shining_count++;
	if ( shining_count <= 50 )
		OLED_ShowChar(60,spos_y,' ',8);
	else if ( shining_count <= 100)
	{
		
		OLED_ShowChar(60,spos_y,'*',8);
	
	}
	else
	{
		
		// Reset the Counter
		shining_count = 0;
	
	}

}

然后是主屏显示函数:

先通过判断光标状态来显示光标,然后写上模式信息的显示代码

/**

* @breif Show Main Information

*/ 
void Main_Show(void)
{

		if (!s_ok)
			select_n();
		else
			select_y();
	
		OLED_ShowString(1, 1, "Mode 1:", 8);
		OLED_ShowString(1, 2, "Mode 2:", 8);
		OLED_ShowString(1, 3, "Mode 3:", 8);
		OLED_ShowString(1, 4, "Mode 4:", 8);

}

 在这些显示函数完成之后,写一个按键的中断响应函数即可:

这个函数逻辑也很简单,首先无论如何,信息都得显示,所以先将Main_Show()和TIM3_ShowTime()写上。此后便是对两个按键进行按键检测,检测函数之后是根据按键当前状态执行对应操作(移动光标与锁定/解锁光标)

/**

* @breif Let The Key Respond The Interrupt Event 

*/ 
void Key_Inter_Respond(void)
{

		Main_Show();
		
		TIM3_ShowTime();
		
		// Detect the Key
		KEY_DETECTION(&key3, GPIOB, GPIO_Pin_7);
		KEY_DETECTION(&key4, GPIOB, GPIO_Pin_14);
		
		// Respond The Key3Press Event
		if ( key3.key_state != KEYSTATE_None )
		{
			
			// key3 Single Click
			if ( key3.key_state == KEYSTATE_Click )
			{
				
				// Key3 Single Click : Symbol "*" move up one unit
				Cursor_Up();
				// Clear The State For Next Detect
				KEY_Clear(&key3);
			
			}
			// key3 Long Click
			else if ( key3.key_state == KEYSTATE_Click_Long )
			{
				
				// Key3 Long Click : Cancel Current Selection
				Selection_Cancel();
				// Clear The State For Next Detect
				KEY_Clear(&key3);
			}
				
			else if ( key3.key_state == KEYSTATE_Click_Double )
			{
			
				// Key3 Double Click : Confirm Current Selection
				Selection_Confirm();
				// Clear The State For Next Detect
				KEY_Clear(&key3);
			
			}
			
		}
		
		// Respond The Key4Press Event
		else if ( key4.key_state != KEYSTATE_None )
		{
			
			// key4 Single Click
			if ( key4.key_state == KEYSTATE_Click )
			{
				
				// Key3 Single Click : Symbol "*" move down one unit
				Cursor_Down();
				// Clear The State For Next Detect
				KEY_Clear(&key4);
			
			}
			// key4 Long Click
			else if ( key4.key_state == KEYSTATE_Click_Long )
			{
				
				// Key4 Long Click : Cancel Current Selection
				Selection_Cancel();
				// Clear The State For Next Detect
				KEY_Clear(&key4);

			}
			// Key4 Double Click
			else if ( key4.key_state == KEYSTATE_Click_Double )
			{
			
				// Key4 Double Click : Confirm Current Selection
				Selection_Confirm();
				// Clear The State For Next Detect
				KEY_Clear(&key4);
			
			}
			
		}
	
}

2.4 主程序

2.4.1 全中断编程!!

在周一组会上老师强调了要用全中断编程,当时我并没有太在意,但是后来了解了之后发现全中断编程不仅更好debug更有逻辑性,还可以降低功耗。因此我将所有功能实现都写进了上述的定时器中断中。主函数的while(1)死循环只留了一句__WFI();用来进入低功耗模式。

2.4.2 源码(main.c)

#include "main.h"
#include "stm32f4xx.h"                  // Device header
#include "GPIOInition.h"				// GPIO Initialization
#include "delay.h"						// delay
#include "LED.h"
#include "UKEY.h"
#include "Key_Inter.h"
#include "oled.h"
#include "timer.h"
#include "KEY.h"

KEY key3,key4;

uint16_t spos_y 	= 1; /* The y position of selection symbol "*" */
uint16_t s_ok 		= 0; /* The symbol whether current mode has been selected */
uint16_t TIM_count 	= 0;

int main(void)
{
	
	// Enable the four keys
	KEY_Init( &key3, KEYSTATE_None, 3, CIO_B, GPIO_Pin_7);
	KEY_Init( &key4, KEYSTATE_None, 4, CIO_B, GPIO_Pin_14);
	
	// Enable PC13
	// LED_init();
	
	// TIMER
	TIM3_Init(); 
	
	// Key_Inter_Init();
	
	// ukey_init();
	
	delay_init(10);
	OLED_Init();
	OLED_Clear();
	delay_init(84);

	LED_OFF(LED1); 
	
	while(1)
	{
		
		/**
		
		* @breif  		Low Power Mode
		
		* @wakeup		Can be waked up by Timer Interrupt
		
		*/
		__WFI();
		
	}
	
}

不难发现我这个应用了全中断编程的主函数相当简短。

3.盘后总结(随便唠唠)

3.1 关于为什么要自己一点点封装库函数来实现功能

3.1.1 自己封装库函数的初衷

由于我本人对于敲代码、模块化编程和实现自动化办公提高效率比较有经验和兴趣,同时对于单片机由于一年多没接触已经相当于小白(一年半前学的太浅了,没有什么遗留的经验),因此我选择在学习每一个模块时都封装和延拓一下形成一个由我创造的可以供他人利用并方便他人利用的库函数,这也会给我很大的满足感和成就感。

此外,我们组这次留校参加电赛的三人都没什么良好的单片机基础,所以这也算是前期积累,自己写一些库函数也可以让我学得更透一些。这样越到后期我们的优势就越明显,这一次即使无法拿奖也能积累很多宝贵的自己写的库函数和完成的模块之类的东西。

3.1.2 关于注释

在注释方面,我虽然有着两年的python、C++编程经验,却在这一周封装库函数时才注意到了官方代码的统一注释格式,这令我很感兴趣,我也想写出这样标准好读的注释,这对我的库函数走出湖南大学、面向全球是有重要意义的(/doge)

采用标准的注释格式是一个好习惯,这样不起眼的小点可能是加分项,至少我的代码不是屎山,看起来很清秀。

3.1.3 怎么提高效率

在这一周的stm32f401学习和编程方面学习到了许多提高效率的方法。首先,学会查看官方代码的注释和手册很重要。其次多逛逛CSDN,虽然401相关的代码例程很少,但是可以copy别人407的相关代码来修改,因为同是f4系列相差不会太大太大。然后就是学会用debug和OLED来调试太太太太重要了!这对于我这种小白来说常常是能救命的。

3.2 关于遇到的问题

3.2.1 奇怪的幽灵引脚👻

先描述一下问题:在进行按键检测部分的硬件搭建、软件调试部分我一直构思的是四个按钮(两个用于调整上下,两个用于确认和取消),但是出现了很奇怪的问题——即有许多个GPIO的Pin引脚可以正常输出,但是用作按键输入就无法实现目的,我们前前后后检查硬件电路和软件代码浪费掉了好几天的时间。(这几个IO口查了手册明明都是用作I/O的,也没有用作SW和JTAG)

出现这个问题的引脚有:PB5、PB6、PB8、PB12、PB13、PA5-PA8、PA12

最后试出来PB14可以实现按键输入,所以该模块最后采用双按键模式。

该问题亟待解决,需要在下一次组会时和老师进行交流探讨找到解决方案并实施。

3.2.2 OLED奇怪的刷屏机制😠

不知道是不是由于下载的OLED例程的问题,它的刷屏速度与delay函数的初始化配置有关。起初配置delay时我一翻数据手册,嗷,f401是84M的啊,那直接delay_init(84);不就得了,结果如此配置之后,刷屏速度堪称逆天,需要10S左右才能完成一次OLED_Clear()的操作,差点没把我吓死。

最后不断地更改参数我发现在每次需要刷屏的时候来一句delay_intit(10);就可以将刷屏速度整到几乎不会有视觉逗留效应。但显然这是会影响其他用到delay的地方的,所以此后若需要用到 delay还得先整个delay_init(84);或者我正在考虑写一个自己的delay函数供以后使用,这一个就永远配置10用来刷屏得了。

3.2.3 按键检测(逻辑严谨太重要啦!!!)

按键检测部分的代码经过了我两三天的拷打(因为有两天在整外部中断哈哈哈),不断的修改让我明白了逻辑严谨闭环太重要了,之前编写代码时常常因为if条件中漏了点必要条件造成奇奇怪怪的误检而让自己怀疑人生,所以以后实现模块功能时我决定要先画流程图并且细抠每一个逻辑判据。

3.2.4 早起摸鱼和饭后摸鱼🐟

上面都是些软硬件相关问题,这些都好办,但是还有一个最难顶的问题——摸鱼。由于期末考完没多久就进了工训进行学习,而那时候的我心思全在想“我考完试咯”、“可以摆烂咯”这样的事,所以一直到7.1之前我都是不在状态中的,每天早起到工位,吃过早饭之后就边看看江科大自化协的视频边和队友扯扯皮,再要不然就是逛逛优信电子看看要不要买点什么模块、杜邦线、跳线之类的。饭后的摸鱼就更顶级了,午饭过后的午觉常常能睡到四五点,晚饭过后和队友刘某飞能扯皮到七八点然后啥也不想学了就溜了。

仔细复盘一下,我的良心受到了不小的拷打,不过好在我已经进入了学习、研究的状态当中,之后应该会少摸几条鱼(认真)。

3.3未来展望

经过对这一周完成的小模块的复盘和对这一周学习生活的总结,我们发现了自身太多的不足,但还是有一些可圈可点的部分的(确信)。由于目前stm32f401的基础知识我已经差不多学完了,并且前期通用函数的封装也很到位,所以在接下来的一周我在代码部分计划完成对摄像头模块以及电机模块的代码部分编写,然后基于这两个模块和这一个功能选择模块完成一点小功能。硬件和电路设计部分与已完成的模块的word文档撰写部分就交给其他两位队友啦!

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码小狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值