【STM32】STM32单片机总目录
在“【STM32】入门(二):跑马灯-GPIO端口输出控制”中,我们是从代码入手,然后分析的手册及原理。本节将会从原理图入手,查询手册,然后分析代码。体验下实际的开发流程。
原理图如下,实际的按键只有KEY0、KEY1、KEY2以及一个复位按键。
实际中没有KEY3,忽略它。这几个普通按键没有去抖电路,因此,需要在代码中用软件去抖。
按键去抖电路(硬件去抖)可以参见下面下复位按键电路。
KEY0、KEY1、KEY2 分别连接在引脚PF6、PF7、PF8上
GPIOF寄存器映射如下:
外部复位引脚NRST为低电平时,产生系统复位。在手册“复位和时钟控制(RCC)”一章可以看到相关说明。
KEY0:按下时执行动作;
KEY1:按下抬起后执行动作;
KEY2:长按1秒后执行动作。
int main(void)
{
delay_init(); //延时函数初始化
LED_Init(); //LED初始化
KEY_Init(); //按键初始化
while(1)
{
key_scan(0);
#a)KEY0:按下时执行动作
if(keydown_data==KEY0_DATA)
{
LED0=0;
LED1=1;
LED2=1;
}
#b)KEY1:按下抬起后执行动作;
if(keyup_data==KEY1_DATA)
{
LED0=1;
LED1=0;
LED2=1;
}
#c)KEY2:长按1秒后执行动作,由于延时5ms扫描一次按键,所以5ms*200=1S
if(key_tem==KEY2_DATA && key_time>200)
{
LED0=1;
LED1=1;
LED2=0;
}
delay_ms(5);
}
}
相关变量、函数定义在另一个文件中key.h中
#define KEY0 PFin(6)
#define KEY1 PFin(7)
#define KEY2 PFin(8)
#define KEY3 PFin(9)
//按键值定义
#define KEY0_DATA 1
#define KEY1_DATA 2
#define KEY2_DATA 3
#define KEY3_DATA 4
//变量声明
extern u8 keydown_data; //按键按下后就返回的值
extern u8 keyup_data; //按键抬起返回值
extern u16 key_time;
extern u8 key_tem;
//函数声明
void KEY_Init(void); //IO初始化
void key_scan(u8 mode); //按键扫描函数
延时函数初始化,在跑马灯程序中也有,但是没有进一步分析。这里详细分析下,延时相关函数。
延时使用Systick定时器也叫滴答定时器,是内核级别的24位倒计数简单定时器,常用做延迟和系统心跳时钟。
定时器的操作步骤:
delay_init初始化主要是设置 SysTick 定时器的时钟源
void delay_init()
{
#a)设置 SysTick 定时器的时钟源:采用AHB总线时钟的八分频
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
#b)计算SYSCLK的8分频时,经过1us的计数次数
fac_us=SYSCLK/8;
fac_ms=(u16)fac_us*1000; //每个ms需要的systick时钟数
}
# c)实际配置函数很简单,最终执行:SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
{
if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
{
SysTick->CTRL |= SysTick_CLKSource_HCLK;
}
else
{
SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
}
}
实际延时函数为delay_xms,但是delay_xms最大只能延时798ms;
在超频到248M的时候,delay_xms最大只能延时541ms。
因此在delay_ms中循环调用delay_ms来实现0~65535ms的延时
void delay_ms(u16 nms)
{
u8 repeat=nms/540; //这里用540,是考虑到某些客户可能超频使用,
//比如超频到248M的时候,delay_xms最大只能延时541ms左右了
u16 remain=nms%540;
while(repeat)
{
delay_xms(540);
repeat--;
}
if(remain)delay_xms(remain);
}
SysTick->LOAD为24位寄存器,所以,最大延时为: nms<=0xffffff81000/SYSCLK,对168M条件下,nms<=798ms
void delay_xms(u16 nms)
{
u32 midtime;
#a)时间加载(SysTick->LOAD为24bit),设置 SysTick 定时器的重装初始值
SysTick->LOAD=(u32)nms*fac_ms;
#b)清零 SysTick 定时器
SysTick->VAL =0x00;
#c)打开 SysTick 定时器
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk;
do
{
midtime=SysTick->CTRL;
}
#d)等待时间到达
while((midtime&0x01)&&!(midtime&(1<<16)));
#e)关闭SysTick 定时器
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;
SysTick->VAL =0X00;
}
相关手册:《STM32F3与F4系列Cortex M4内核编程手册》
按键初始化和跑马灯中的初始化相似,不同的是将引脚模式设置为输入模式,输入引脚不需要设置推挽、开漏
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); //使能GPIOF时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9; //KEY0 KEY1 KEY2 KEY3对应引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; //普通输入模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //100M
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOF, &GPIO_InitStructure); //初始化GPIOF6,7,8,9
}
#define KEY0 PFin(6)
#define KEY1 PFin(7)
#define KEY2 PFin(8)
#define KEY3 PFin(9)
PFin的宏定义:#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入
最终使用的宏定义:#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
在《STM32F3与F4系列Cortex M4内核编程手册》中有关于位带别名计算公式为:
bit_word_addr = bit_band_base + (byte_offset x 32) + (bit_number × 4)
和BITBAND宏对应,其中
“+0x2000000”为原地址到位带别名地址的偏移
“(addr &0xFFFFF)<<5”为“byte_offset x 32”(2的五次方=32);
“bitnum<<2”为bit_number × 4(2的二次方=4)
关于位带别名的详细说明参见下面“5、位带别名”
//IO口操作宏定义
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO口地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+20) //0x40020014
#define GPIOB_ODR_Addr (GPIOB_BASE+20) //0x40020414
#define GPIOC_ODR_Addr (GPIOC_BASE+20) //0x40020814
#define GPIOD_ODR_Addr (GPIOD_BASE+20) //0x40020C14
#define GPIOE_ODR_Addr (GPIOE_BASE+20) //0x40021014
#define GPIOF_ODR_Addr (GPIOF_BASE+20) //0x40021414
#define GPIOG_ODR_Addr (GPIOG_BASE+20) //0x40021814
#define GPIOH_ODR_Addr (GPIOH_BASE+20) //0x40021C14
#define GPIOI_ODR_Addr (GPIOI_BASE+20) //0x40022014
#define GPIOA_IDR_Addr (GPIOA_BASE+16) //0x40020010
#define GPIOB_IDR_Addr (GPIOB_BASE+16) //0x40020410
#define GPIOC_IDR_Addr (GPIOC_BASE+16) //0x40020810
#define GPIOD_IDR_Addr (GPIOD_BASE+16) //0x40020C10
#define GPIOE_IDR_Addr (GPIOE_BASE+16) //0x40021010
#define GPIOF_IDR_Addr (GPIOF_BASE+16) //0x40021410
#define GPIOG_IDR_Addr (GPIOG_BASE+16) //0x40021810
#define GPIOH_IDR_Addr (GPIOH_BASE+16) //0x40021C10
#define GPIOI_IDR_Addr (GPIOI_BASE+16) //0x40022010
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
#define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入
#define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出
#define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入
#define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出
#define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入
#define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出
#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入
#define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出
#define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入
#define PHout(n) BIT_ADDR(GPIOH_ODR_Addr,n) //输出
#define PHin(n) BIT_ADDR(GPIOH_IDR_Addr,n) //输入
#define PIout(n) BIT_ADDR(GPIOI_ODR_Addr,n) //输出
#define PIin(n) BIT_ADDR(GPIOI_IDR_Addr,n) //输入
当 mode 为 0 的时候, key_scan函数将不支持连续按, 扫描某个按键,该按键按下之后必须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多次触发,而坏处就是在需要长按的时候比较不合适。
当 mode 为 1 的时候, KEY_Scan 函数将支持连续按,如果某个按键一直按下,则会一直返回这个按键的键值,这样可以方便的实现长按检测。 该函数有返回值,如果有按键按下,则返回非 0 值,如果没有或者按键不正确,则返回 0。
void key_scan(u8 mode)
{
#a)为了保证键抬起值只有一次有效,再次进入扫描时,将键抬起清零
keyup_data=0;
#b)有键正按下
if(KEY0==0||KEY1==0||KEY2==0||KEY3==0){
if(KEY0==0) key_tem=1;
else if(KEY1==0) key_tem=2;
else if(KEY2==0) key_tem=3;
#c)有键按下后第一次扫描不处理,与else配合第二次扫描有效,这样实现了去抖动
if (key_tem == key_bak){
#d)有键按下后执行一次扫描函数,该变量加1
key_time++;
#e)按键值赋予keydown_data
keydown_data=key_tem;
#f)在单按模式下key_time>1时按键值无效;如果mode为1就为连按
if( (mode==0)&&(key_time>1) )
keydown_data=0;
#g)去抖动
}else{
key_time=0;
key_bak=key_tem;
}
#h)键抬起
}else{
#i)按键抬起后返回一次按键值
if(key_time>2){
#j)键抬起后按键值赋予keydown_data
keyup_data=key_tem;
}
#k)清零,不然下次执行扫描程序时按键的值跟上次按的值一样,抖动功能将会失效
key_bak=0;
key_time=0;
keydown_data=0;
}
}
执行完后,通过变量 keydown_data、keyup_data、key_tem及key_time 来获取按下、抬起、长按的键
位带区:支持位带操作的地址区;
位带别名:对别名地址的访问最终作用到位带区的访问上(这中途有一个地址映射过程);
位带别名操作就是:操作位带别名的一个字相当于操作位带区中一位,映射关系如下图:
设置一个位带别名操作,在《STM32F3与F4系列Cortex M4内核编程手册》中有如下说明:
计算公式为:bit_word_addr = bit_band_base + (byte_offset x 32) + (bit_number × 4)
和代码中 BITBAND 宏对应,其中
“+0x2000000”为原地址到位带别名地址的偏移
“(addr &0xFFFFF)<<5”为“byte_offset x 32”(2的五次方=32);
“bitnum<<2”为bit_number × 4(2的二次方=4)
0x2000_0000‐0x200F_FFFF(SRAM 区中的最低 1MB)
0x4000_0000‐0x400F_FFFF(片上外设区中的最低 1MB)
对 SRAM 位带区的某个比特,记它所在字节地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
AliasAddr=0x22000000+((A-0x20000000)*8+n)*4=0x22000000+(A-0x20000000)*32+n*4
对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则该比特在别名区的地址为:
AliasAddr=0x42000000+((A-0x40000000)*8+n)*4=0x42000000+(A-0x40000000)*32+n*4
上式中,“*4”表示一个字为 4 个字节,“*8”表示一个字节中有 8 个比特。