上一篇说完了STM32库开发的引脚输出控制,这一篇对其引脚输入控制方法进行说明,引脚设置为输入功能时能够感知引脚上的电平高低,具有模拟输入复用功能的引脚还可以结合芯片内部的A/D准确测量其电平值,后续在ADC章节再进行讨论。
这里通过按键改变引脚的输入电平高低,当引脚上接入高电平时,其输入数据寄存器对应的位将置1,而当引脚上接入低电平时,将清0;程序对引脚的输入数据进行判断,并控制LED的亮灭。
由于笔者蓝色的最小系统板没有设置按键,因此用黑色的最小系统板进行按键程序的演示。
按键电路原理图
从图中可以看出,黑色系统板的LED由GPIOC13引脚控制,低电平点亮;按键接在GPIOA0引脚,按键按下则引脚与地连接,即引脚接入低电平,在上一篇的基础上,可以开始写程序了。
程序思路是,初始化GPIOA0为上拉输入模式,即在STM32芯片内部使能上拉电阻,如图所示:
在STM32引脚内部,有输出驱动与输入驱动电路,通过Port Configuration Register(CRL、CRH)寄存器设置引脚输入还是输出,并设置相应的输入输出模式。
在这里将GPIOA0引脚设置为上拉输入模式,即上拉电阻的“开关”闭合,引脚在默认状态下,通过内部的上拉电阻接到VDD,也就是默认为高电平;这样,当按键按下时,GPIOA0通过按键接地,引脚切换为低电平。
检测到按键按下时,读取GPIOC13引脚的输出数据(0或1),并对其取反,重新写入GPIOC13引脚数据输出寄存器,改变引脚输出状态,控制LED状态反转。
同理复制模板工程文件到新工程文件夹Key下,打开Keil uVision5工程文件。这里直接给出核心程序:
/**
* @brief Main program.
* @param None
* @retval None
*/
int main(void)
{
// BitAction是库文件提供的enum枚举类型
BitAction status;
// 配置引脚
GPIOConfig();
while(1)
{
// 读取GPIOA0输入状态
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)==0)
{
// 延时去抖
delay_u(1000);
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)==0)
{
// 等待按键释放,即松手检测
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)==0);
// 读取LED状态并取反
status = (BitAction)(1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));
// 设置LED反转
GPIO_WriteBit(GPIOC, GPIO_Pin_13, status);
}
}
}
}
/**
* @brief Configure GPIO
* @param None
* @retval None
*/
void GPIOConfig(void)
{
GPIO_InitTypeDef GPIOInitStruct;
// GPIOA与GPIOC均挂载在APB2总线上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
// 设置GPIOA为上拉输入模式,输入模式下无需设置引脚输出速率
GPIOInitStruct.GPIO_Pin = GPIO_Pin_0;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIOInitStruct);
// 设置GPIOC13为推挽输出模式
GPIOInitStruct.GPIO_Pin = GPIO_Pin_13;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIOInitStruct);
GPIO_SetBits(GPIOC, GPIOInitStruct.GPIO_Pin);
}
其中BitAction是库文件中提供的枚举类型,表示引脚的位清0与置1:
/**
* @brief Bit_SET and Bit_RESET enumeration
*/
typedef enum
{ Bit_RESET = 0,
Bit_SET
}BitAction;
uint8_t GPIO_ReadOutputDataBit( GPIO_TypeDef * GPIOx, uint16_t GPIO_Pin ) 为引脚输入数据读取函数,可以在库文件帮助文档中找到其用法。
在利用官方库进行STM32程序开发时,需要时刻结合帮助文档,查找相关函数。
运行结果编译程序并下载运行,可以看到按键改变LED亮灭状态:
这里用的矩阵键盘规格为4x4,即4行4列共16个按键,通过8个端子就可以进行控制和读取,电路原理图及实物如下:
从电路原理图可以看出,按键的每一行、每一列均分别相连并接入P1的一个端子,其控制方式为扫描读取(行扫描列读取或者列扫描行读取),这里以行扫描为例:
P1的1-4端子为列,用于读取数据,5-8端子为行,进行扫描;假设K4被按下:
控制STM32的引脚,首先使P1的5端子输出低电平,6-8端子为高电平,并读取1-4端子的电平数据,可以知道1端子为低电平,2-4端子为高电平;由此可以推知K4按下;
依次使P1的5-8端子分别输出低电平,并读取列数据,可以获取按下的按键位置;
但进行行扫描时矩阵键盘无法识别同一列同时按下的两个按键,举例若K4、K8同时按下:
第一行扫描时,P1的5端子输出低电平,6端子输出高电平 此时6端子的高电平通过K8、K4进入5端子,形成回路; 而由上面的引脚内部结构可以看出,引脚配置成推挽输出时,输出高电平时P-MOS导通,而输出低电平时N-MOS导通,且这两个MOS管采用的是对称设计,即参数近似,故此时VDD通过两个MOS管接到VSS,引脚输出的电压为12VDD\frac{1}{2}V_{DD}21VDD,近似于高电平; 从而1端子的输入引脚的电平被干扰,导致无法正确读取数据 程序矩阵键盘列接到PA0-PA3,行接到PA4-PA7,而上一篇中的流水灯接到PB8-PB15
矩阵键盘扫描程序:
/**
* @brief Key Scan
* @param None
* @retval sum of key values
*/
uint8_t MatrixKeyScan(void)
{
uint8_t i, temp, pin = 0;
// 进行行扫描
for(i=0; i<4; i++)
{
// 首先将所有行都置高电平,然后按照行扫描顺序将第i行输出低电平
GPIO_SetBits(GPIOA, 0xF0);
// 由于PA4-PA7接行,故0x10左移i位为当前行控制引脚编号
GPIO_ResetBits(GPIOA, 0x10<<i);
// 读取列数据,由于GPIO_ReadInputData返回uint16_t,因此只取低4位
temp = (uint8_t)(GPIO_ReadInputData(GPIOA) & 0x000F);
if(temp != 0x0F)
{
// 延时去抖
delay_u(1000);
temp = (uint8_t)(GPIO_ReadInputData(GPIOA) & 0x000F);
// 这里不进行松手检测,直接将该行中所有按下的键值相加
if(temp !=0x0F)
{
pin += ~(temp|0xF0)+i*4;
}
}
}
// 返回计算的键值
return pin;
}
这里通过8个LED以二进制的方式显示按下的键的值的和,如按下K1、K3,则LED1、LED3亮,二进制值为0b00000101,即4
完整程序(不含上一篇中写好的延时程序)如下:
/* Includes ------------------------------------------------------------------*/
#include "stm32f10x.h"
/* Private functions Declaration ---------------------------------------------*/
void GPIOConfig(void);
uint8_t MatrixKeyScan(void);
void delay_m(uint32_t t);
void delay_u(uint32_t t);
/**
* @brief Main program.
* @param None
* @retval None
*/
int main(void)
{
uint8_t keys;
GPIOConfig();
while(1)
{
keys = MatrixKeyScan();
GPIO_SetBits(GPIOB, 0xFF00);
GPIO_ResetBits(GPIOB, keys<<8);
}
}
/**
* @brief Key Scan
* @param None
* @retval sum of key values
*/
uint8_t MatrixKeyScan(void)
{
uint8_t i, temp, pin = 0;
for(i=0; i<4; i++)
{
GPIO_SetBits(GPIOA, 0xF0);
GPIO_ResetBits(GPIOA, 0x10<<i);
temp = (uint8_t)(GPIO_ReadInputData(GPIOA) & 0x000F);
if(temp != 0x0F)
{
delay_u(1000);
temp = (uint8_t)(GPIO_ReadInputData(GPIOA) & 0x000F);
if(temp !=0x0F)
{
pin += ~(temp|0xF0)+i*4;
}
}
}
return pin;
}
/**
* @brief Configure GPIO
* @param None
* @retval None
*/
void GPIOConfig(void)
{
GPIO_InitTypeDef GPIOInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//GPIOInitStruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
GPIOInitStruct.GPIO_Pin = 0x00F0;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIOInitStruct);
//GPIOInitStruct.GPIO_Pin = |GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;
GPIOInitStruct.GPIO_Pin = 0x0F;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIOInitStruct);
//GPIOInitStruct.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15;
GPIOInitStruct.GPIO_Pin = 0xFF00;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIOInitStruct);
GPIO_ResetBits(GPIOC, GPIOInitStruct.GPIO_Pin);
delay_m(500);
GPIO_SetBits(GPIOC, GPIOInitStruct.GPIO_Pin);
}
注意在GPIO初始化和控制GPIO输出时,并没有用GPIO_Pin_x的形式(被注释的部分),查看GPIO_Pin_x的定义(如在GPIO_Pin_0上右键Go To Definition…)可知,是用二进制位表示的GPIO引脚编号,因此可以直接用二进制数来表示引脚,更为方便(虽然没有那么直观)。
运行结果编译程序并下载,现象如下(一根手指按下了K1-K4,难道可以用这个练大横按~啊哈哈哈哈哈):