嵌入式软件开发学习过程记录,本部分结合本人的学习经验撰写,系统描述各类基础例程的程序撰写逻辑。构建裸机开发的思维,为RTOS做铺垫(本部分基于库函数版实现),如有不足之处,敬请批评指正。
(2)中还是IO的一些基本操作和扩展,包括系统定时器、蜂鸣器、数码管、按键
一 SysTick系统定时器
SysTick系统定时器是ARM Cortex-M微控制器中的一个定时器,可以用于实现一些基本的计时和调度功能。它可以作为系统时钟的一个计数器,以固定的时间间隔来触发中断,从而实现一些定时任务的调度,例如周期性的数据采集、任务轮询等。作为一个 24 位向下递减的定时器,每计数一次所需时间为 1/SYSTICK,SYSTICK为系统定时器时钟
SysTick系统定时器的主要功能包括:
- 提供了一个硬件定时器,可以以固定的时间间隔触发中断,从而实现一些定时操作;
- 可以作为系统时钟的计数器,方便进行一些时间相关的操作和计算;
- 可以与其他外设如串口、定时器等配合使用,实现更复杂的系统控制功能;
- 可以用于软件延时或者忙等待的实现,避免在一些特定场景下使用while循环等方式造成CPU浪费等问题。
总之,SysTick系统定时器可以作为一个非常方便的系统时钟计数器和定时器,为嵌入式系统的各种应用提供了基础的时间和定时相关功能。
注意:STM32F1的库函数中,没有提供SysTick定时器配置函数,因此需要我们根据芯片寄存器进行撰写。SysTick 定时器寄存器分别是 CTRL、LOAD、VAL、CALIB。此处需要查看Cortex M3手册(以下摘自文档内容)
CTRL寄存器注:CLKSOUTCE 位是用于选择 SysTick 定时器时钟来源,如果该位为 1,表示其时钟是由系统时钟直接提供即 72M。如果为 0,表示其时钟是由系统时钟八分频后提供即 72/8=9M
LOAD寄存器
因为 STM32F1 的 SysTick 定时器是一个 24 位递减计数器,因此重装载寄存器中只使用到了低 24 位,即 bit0-bit23。当系统复位时,其值为 0。
VAL寄存器
同样只有 bit0-bit23 有效,复位时值为 0。
CALIB 寄存器
CALIB寄存器是SysTick系统定时器中的一个寄存器,用于存储SysTick定时器的校准值。该寄存器是一个只读寄存器,长度为32位,包含了3个字段:
- TENMS:表示每个时钟滴答所对应的时间,单位为微秒;
- SKEW:标志位,表示当前SysTick定时器的实现是否存在失调(skew);
- NOREF:标志位,表示SysTick定时器是否支持外部参考时钟。
TENMS字段是CALIB寄存器中最重要的一个字段,它可以告诉我们,当前系统时钟频率下,一次SysTick定时器中断所需要的时间间隔。这个信息在实际应用中非常有用,例如在进行延时操作时,可以使用该值作为延时参数,在不同的系统时钟频率下保证延时的准确性。
SKEW和NOREF字段则分别表示SysTick定时器的实现是否存在时钟失调和是否支持外部参考时钟。如果SKEW字段为1,则表示当前的SysTick定时器实现可能存在时钟失调,需要特别处理;如果NOREF字段为1,则表示SysTick定时器不支持外部参考时钟输入,只能使用内部的系统时钟进行计数。
需要注意的是,CALIB寄存器只能在SysTick定时器使能之前进行读取,并且在操作SysTick定时器之前需要根据实际所使用的时钟频率进行校准。
程序设计
(1)设置 SysTick 定时器的时钟源。(2)设置 SysTick 定时器的重装初始值(如果要使用中断的话,就将中断使能打开)。(3)清零 SysTick 定时器当前计数器的值。(4)打开 SysTick 定时器。
同样的,作为一个单片机程序中的公用部分,我们在Public文件夹中创建SysTick.c和SysTick.h文件(想了解为什么这么做可以去看STM32裸机开发(1))
首先对SysTick定时器初始化,SysTick_Init 函数形参 SYSCLK 表示的系统时钟大小,默认配置使用的系统时钟是 72M,所以调用这个函数时,形参值即为 72。函数内部调用了一个库函数 SysTick_CLKSourceConfig,此函数用来对 SysTick 定时器时钟的选择,使 用 的 SysTick 定 时 器 时 钟 是 系 统 时 钟 的 8 分 频 , 所 以 参 数 是 SysTick_CLKSource_HCLK_Div8。如果使用源系统时钟作为 SysTick 定时器时钟,那么参数即为 SysTick_CLKSource_HCLK。
void SysTick_Init(u8 SYSCLK)
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
fac_us=SYSCLK/8; //SYSCLK的 8分频 保存 1us所需的计数次数
fac_ms=(u16)fac_us*1000; //每个 ms 需要的 systick 时钟数
}
设计基于SysTick定时器的1us级函数
①将需要延时多少 us 的计数值加载到 SysTick 的 LOAD 寄存器中,fac_us 值是延时 1us 所需的计数值。
②清空当前计数值寄存器 VAL。
③打开 SysTick 定时器,定时器开始向下递减计数。
④CTRL 寄存器的第 16 位是 SysTick 递减到 0 的标志位,如果递减到 0,此为置 1,通过读取该位来判断延时是否完成,从而退出 while 循环。
⑤关闭 SysTick 定时器。
⑥清空当前计数值寄存器 VAL。
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL|=0x01 ; //开始倒数
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达
SysTick->CTRL&=~0x01; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
设计基于SysTick定时器的1ms级函数,SYSCLK 是系统时钟为 72M,所以最大延时为1864ms。如果需要延时大于 1.864S,可以调用多个 delay_ms 函数即可。(通过引入.h,即可在main.c中直接调用delay_ms(500))
void delay_ms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms; //时间加载(SysTick->LOAD 为 24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL|=0x01 ; //开始倒数
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达
SysTick->CTRL&=~0x01; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
二 蜂鸣器实验及数码管实验
蜂鸣器实验
作为常用的蜂鸣器部件,可以给设备出现故障时进行报警工作。
无源蜂鸣器需提供一定频率的脉冲信号才能发声,频率大小通常在 1.5-5KHz 之间。
有源蜂鸣器加一个 1.5-5KHz 的脉冲信号,同样也会发声,而且改变这个频率,就可以调节蜂鸣器音调,产生各种不同音色、音调的声音。如果改变输出电平的高低电平占空比,则可以改变蜂鸣器的声音大小。
基于前面led点灯和位带实验的基础,此处不多进行解释,直接给出deep.c和deep.h的代码(创建在APP文件夹中)
#include "beep.h"
void BEEP_Init() //端口初始化
{
GPIO_InitTypeDef GPIO_InitStructure; //声明一个结构体变量,用来初始化 GPIO
RCC_APB2PeriphClockCmd(BEEP_PORT_RCC,ENABLE); /* 开启 GPIO 时钟 */
/* 配置 GPIO 的模式和 IO 口 */
GPIO_InitStructure.GPIO_Pin=BEEP_PIN; //选择你要设置的 IO口
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP; // 设置推挽输出模式
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //设置传输速率
GPIO_Init(BEEP_PORT,&GPIO_InitStructure); /* 初 始 化 GPIO*/
}
#ifndef _beep_H
#define _beep_H
#include "system.h"
/* 蜂鸣器时钟端口、引脚定义 */
#define BEEP_PORT GPIOB
#define BEEP_PIN GPIO_Pin_5
#define BEEP_PORT_RCC RCC_APB2Periph_GPIOB
#define beep PBout(5)
void BEEP_Init(void);
#endif
数码管
数码管可以看做是多个led灯管组成,可分为共阴数码管,共阳数码管,比如数码管的 8 个段选口(A-DP)是连接在STM32 的 PC 0-7 管脚上。通过输出GPIO口的高低电平,即可控制输出不同的数字,
具体实现则可以视为创建一个数组,以共阳数码管为例:
u8 smgduan[16]={0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};//0~F
三 按键控制实验
按键作为一个后续常用的一个硬件外设,通过判断输入电平的高低来触发相应的函数任务,通过与中断配合,完成各项功能,此处仅简单介绍不含中断的按键任务。
通常按键所用开关为机械弹性开关,但会出现抖动,一般为5ms至10ms,因此往往需要采用硬件消抖或软件消抖。
以图为例:K_UP 按键另一端是接在 3.3V 上,按下时输入到芯片管脚即为高电平;K_LEFT、K_DOWN、K_RIGHT 按键另一端是全部接在 GND 上,按下时输入到芯片管脚即为低电平。在APP文件夹内创建key文件夹,创建key.c和key.h。
因为硬件电路中K_UP与K_LEFT、K_DOWN、K_RIGHT不同,因此需要设置两种GPIO结构体的参数。其中K_UP是下拉输入模式,K_LEFT、K_DOWN、K_RIGHT是上拉输入模式。
void KEY_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);
GPIO_InitStructure.GPIO_Pin=KEY_UP_Pin; //选择你要设置的IO 口
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPD;//下拉输入
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //设置传输速率
GPIO_Init(KEY_UP_Port,&GPIO_InitStructure); /* 初始化GPIO */
GPIO_InitStructure.GPIO_Pin=KEY_DOWN_Pin|KEY_LEFT_Pin|KEY_RIGHT_Pin;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(KEY_Port,&GPIO_InitStructure);
}
要知道哪个按键被按下,就需要编写按键检测函数,KEY_Scan 函数带一个形参 mode,该参数用来设定是否连续扫描按键,如果 mode 为 0,只能操作一次按键,只有当按键松开后才能触发下次的扫描,这样做的好处是可以防止按下一次出现多次触发的情况。如果 mode 为 1,函数是支持连续扫描的,即使按键未松开,在函数内部有 if(mode==1)这条判断语句,因此 key 始终是等于 1 的,所以可以连续扫描按键,当按下某个按键,会一直返回这个按键的键值,这样做的好处是可以很方便实现连按操作。(注意:此函数中各个按键的扫描优先级是不相同的,如需优先级一致,则需全部设置if语句)
u8 KEY_Scan(u8 mode)
{
static u8 key=1;
if(key==1&&(K_UP==1||K_DOWN==0||K_LEFT==0||K_RIGHT==0)) //任意一个按键按下
{
delay_ms(10); //消抖
key=0;
if(K_UP==1)
{
return KEY_UP;
}
else if(K_DOWN==0)
{
return KEY_DOWN;
}
else if(K_LEFT==0)
{
return KEY_LEFT;
}
else
{
return KEY_RIGHT;
}
}
else if(K_UP==0&&K_DOWN==1&&K_LEFT==1&&K_RIGHT==1) //无按键按下
{
key=1;
}
if(mode==1) //连续按键按下
{
key=1;
}
return 0;
}
key.h文件中则依旧借用位带操作命令。
#ifndef _key_H
#define _key_H
#include "system.h"
#define KEY_LEFT_Pin GPIO_Pin_2 //定义 K_LEFT 管脚
#define KEY_DOWN_Pin GPIO_Pin_3 //定义 K_DOWN 管脚
#define KEY_RIGHT_Pin GPIO_Pin_4 //定义 K_RIGHT 管脚
#define KEY_UP_Pin GPIO_Pin_0 //定义 KEY_UP 管脚
#define KEY_Port (GPIOE) //定义端口
#define KEY_UP_Port (GPIOA) //定义端口
//使用位操作定义
#define K_UP PAin(0)
#define K_DOWN PEin(3)
#define K_LEFT PEin(4)
#define K_RIGHT PEin(2)
//定义各个按键值
#define KEY_UP 1
#define KEY_DOWN 2
#define KEY_LEFT 3
#define KEY_RIGHT 4
void KEY_Init(void);
u8 KEY_Scan(u8 mode);
#endif
main.c
int main()
{
u8 key,i;
SysTick_Init(72);
LED_Init();
KEY_Init();
while(1)
{
key=KEY_Scan(0); //扫描按键
switch(key)
{
case KEY_UP:
led2=0;
break; //按下 K_UP 按键 点亮D2 指示灯
case KEY_DOWN:
led2=1;
break; //按下 K_DOWN 按键 熄灭 D2 指示灯
case KEY_LEFT:
led3=1;
break; //按下 K_LEFT 按键 点亮 D3 指示灯
case KEY_RIGHT:
led3=0;
break; //按下 K_RIGHT 按键 熄灭 D3 指示灯
}
i++;
if(i%20==0)
{
led1=!led1; //LED1 状态取反
}
delay_ms(10);
}
}