使用STM32系统定时器实现精准延时

1.Systick定时器介绍
也叫滴答定时器,是cortex-M4内核的一个外设。24位向下递减的定时器,每计数一次的时间为1/SYSTICK,systick
是系统定时器时钟,可以直接取系统时钟,还可以通过系统时钟8分频。当定时器计数到0时,将从LOAD寄存器中自
动重装定时器初值,重新向下递减计数,如此循环往复。开启Systick中断的话,当定时器计数到0时,将产生一个中
断信号。因此只要知道计数的次数,就可以准确得到延时时间。

2.Systick定时器寄存器
(1)CTRL时Systick定时器的控制及状态寄存器。
第16位名称位COUNTFLAG,类型是R,复位值0,如果上次读本寄存器后,Systick已经数到0,则该位为1,如果读
取该位,自动清零。

第2位是CLKSOURCE,类型是R/W,复位值为0,
当CLKSOURCE=0,时钟源为外部时钟(STCLK)
当CLKSOURCE=1,时钟源为内核时钟(FCLK)168M
通常是让CLKSOURCE取0,通过系统时钟8分频后得到,也就是21MB

第1位,TICKINT,类型R/W,复位值0,
当TICKINT=1,SysTick倒数到0时,产生SysTick异常请求
当TICKINT=0,数到0无动作

第0位,ENABLE,R/W,复位值为0,是SysTick定时器的使能位

(2)LOAD寄存器
位段是(23:0),名称是RELOAD,类型是R/W,复位值为0,
用于存放当倒数到0时,将被重装载的值

(3)VAL寄存器
是SysTick定时器的当前数值寄存器
位段(23:0),名称CURRENT,类型R/Wc,复位值为0
读取时返回当前计数值,写它则使之清零,同时还会清除再Systick控制及状态寄存器(CTRL)中的COUNTFLAG标志

3.Systick定时器配置步骤
SysTick定时器的操作可以分为4步:
(1)设置SysTick定时器的时钟源。
(2)设置SysTick定时器的重装初始值
(3)清零SysTick定时器当前计数器的值
(4)打开SysTick定时器

定时器初始化函数:
void SysTick_Init(u8 SYSCLK)
{

Systick_ClKSourceConfig(SysTick_ClKSource_HCLK_Div8);

/这个库函数存放在misc.c文件中,需要添加到工程中,功能是将系统时钟8分频后传给Systick的时钟,
这个参数是对系统时钟8分频的参数
/
fac_us=SYSCLK/8;
/将SYSCLK8分频后得到的是每1us计数的次数/
fac_ms=(u16)fac_us*1000;
//每ms需要的systick时钟数
}

//微秒的延时函数
void delay_us(u32_nus)//参数不能大于2的24次方/21(大约798915微秒)
{
u32 temp;
SysTick->LOAD=nus*fac_us;
//因为是倒计数定时器,所以要将初始值设置为要定时的时间
SysTick->VAL=0x0;
//清空计数器
SysTick->CTRL|=0x1;
//使能计数器,开始倒数
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16))); //等待时间到达
SysTick->CTRL&=~0x01;//关闭计数器
SysTick->VAL=0x00;//清空计数器
}

void delay_nms(u16_nms)//参数有范围,最大只能到(798.915毫秒)所以不能直接使用这个函数来延时大于1s的
{
u32 temp;
SysTick->LOAD=nms*fac_ms;
SysTick->VAL=0X0;
SysTick->CTRL|=0x01;
do
{
temp=SysTick->CTRL;
}while((temp&0x01)&&!(temp&(1<<16)));
SysTick->CTRL&=~0x01;
SysTick->VAL=0x00;
}
//毫秒延时函数
void delay_ms(u16 nms)
{
u8 repeat=nms/540;
u16 remain=nms%540;
while(repeat)
{
delay_nms(540);
repeat–;
}
if(remain)
delay_nms(remain);
}

stm32f4的位带操作

1.位带介绍
通过访问位带别名区,将每个比特位膨胀成一个32位字,当访问这些字的时候就达到了访问比特的目的。
比方说BSRR寄存器有32个位,那么可以映射到32个地址上,当访问这32个地址就达到访问32个比特的目的
2.位带区和位带别名区的地址转换
支持位带操作的区域是SRAM区的最低1M范围和片内外设区的最低1M范围
一般使用外设区的位带,我们只需要操作位带别名区的地址就可以操作位带区的地址
外设位带区与外设位带别名区的地址转换公式:
AliasAddr = 0x42000000 + (A-0x40000000)84 + n4
A是我们要操作的位所在的寄存器地址,n是位序号
(位带区的一个位在位带别名区会被膨胀成4个字节)
SRAM位带区与SRAM位带别名区的地址转换公式:
AliasAddr = 0x22000000 +(A-0x20000000)
84 + n4
将两个公式合并成为一个
((A & 0xF0000000)+0x02000000+((A & 0X000FFFFF)<<5)+(n<<2))
左移五位相当于乘以32,左移两位相当于乘以4
3.位带操作的优点
(1)控制GPIO输入输出非常简单
(2)操作串行接口芯片非常方便(DS1302、74HC595等)。
(3)代码简洁,阅读方便。

通过位带操作,控制LED进行闪烁(也就是控制外设管脚)
通过上述的公式 我们了解到了 需要通过操作位带别名区来对位带区进行操作,
我们需要再添加两个文件,名为system.c和system.h来对位带进行配置
system.h
首先我们要得到从位带区转移到位带别名区的地址,所以define一个BITBAND(addr,bitnum)
addr是我们要操作的位所在的寄存器地址,bitnum是位序号,代码如下:

#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))
/到这里我们进行的封装是,MEM_ADDR(addr)、BIT_ADDR(addr,bitnum)
MEM_ADDR是将地址转换为指针操作,直接对地址进行操作,然后MEM_ADDR(BITBAND(addr,bitnum))是对
位带别名区的地址进行转换,得到的值是一个指针,然后宏定义为BIT_ADDR(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) //输入

#endif

system.c文件中只需要包含.h文件就行,然后再main.c和led.h中
包含system.h
再把两个文件添加到工程里,不报错就行

接下来是点亮LED,
led1对应PF9,LED2对应PF10所以
再LED_Init中使能F端口时钟,将管脚设置为9和10,代码如下(led.c):

#include “led.h”

/***

  • 函 数 名 : LED_Init

  • 函数功能 : LED初始化函数

  • 输 入 : 无

  • 输 出 : 无

  • **/
    void LED_Init()
    {
    GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE); //使能端口F时钟

    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT; //输出模式
    GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9|GPIO_Pin_10;//管脚设置F9
    GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;//速度为100M
    GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;//推挽输出
    GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;//上拉
    GPIO_Init(GPIOF,&GPIO_InitStructure); //初始化结构体
    GPIO_ResetBits(GPIOF,GPIO_Pin_9|GPIO_Pin_10);
    }

main函数中不需要大量的代码来点亮使能管脚再点亮led,代码如下:
int main()
{
LED_Init();
while(1)
{
led1=!led1; //D1状态取反
delay(6000000);
}
}
这样就是stm32f4的位带操作

STM32F4XX系统时钟配置

stm32f4系统时钟配置
1.时钟树的介绍:
数据手册第六章时钟的介绍里有stm32f4的时钟树的图:
tu16
Stm32有5个时钟源,分别是: LSI(内部的低速时钟),LSE(外部的低速时钟),
HSI(内部的高速时钟),HSE(外部的高速时钟),PLL(分为PLL和PLLI2S)。

LSI给独立看门狗和内部的RTC提供时钟;
LSE对应两个管脚,用来再外部连接两个晶振。它可以指向RTC(一般使用这个时钟来向RTC提供时钟),
还可以指向MCO1;
HSI通过RC振荡器产生,大小为16M,可以给系统时钟提供来源,还可以指向MCO1这个管脚,
还可以流向PLL锁相环。
HSE一般我们设置为8M,可以流向系统时钟,我们的系统时钟就是为系统时钟提供来源(一般不使用HSI内部时钟,当外部时钟HSE损坏时,系统会自动把HSI作为系统时钟来源),
还可以流向PLL,经过分频器。
PLL锁相环,作用是倍频输出,系统时钟就是由HSE提供的8M频率,通过PLL来倍频输出达到168M。

2.系统时钟的来源
tu17
图上画出了HSE作为系统时钟的时钟源,从8M为系统时钟提供168M的过程。
8M经过M分频,取M=8,此时为1M
然后经过N倍频,取N=336,此时为336M
然后经过P分频,取P=2,此时为168M提供给系统时钟。
这是系统时钟的默认配置方法。

3.系统时钟的设置
实验要做的时将系统时钟为84MHZ,来使延时函数的延时时长变为之前的2倍。
我们要再main.c中添加一个void RCC_HSE_Config(u32 pllm,u32 plln,u32 pllp,u32 pllq)
这个函数的功能就是设置M,N,P的值来改变系统时钟。代码如下:
void RCC_HSE_Config(u32 pllm,u32 plln,u32 pllp,u32 pllq)
{
RCC_DeInit();//将外设RCC寄存器重设为缺省值
RCC_HSEConfig(RCC_HSE_ON);//设置外部高速晶振(HSE)
if(RCC_WaitForHSEStartUp()==SUCCESS)
{
RCC_HCLKConfig(RCC_SYSCLK_Div1);//设置AHB时钟,因为AHB的时钟等于系统时钟,所以将系统时钟一分频就是AHB时钟
RCC_PCLK1Config(RCC_HCLK_Div2);//设置低速APB2时钟,设置为84M
RCC_PCLK2Config(RCC_HCLK_Div4);//设置低速APB1时钟,设置为42M
RCC_PLLConfig(RCC_PLLSource_HSE,pllm,plln,pllp,pllq);
RCC_PLLCmd(ENABLE);//控制PLL使能或者失能
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY)==RESET);
//检查PLLRDY的RCC标志位是否设置,PLL就绪
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);//设置系统时钟为PLLCLK
while(RCC_GetSYSCLKSource()!=0x08);//返回用作系统时钟的时钟源
}

}
然后再main函数中调用函数,更改参数就能达到目的,RCC_HSE_Config(8,336,4,7);
这时就能观察到,延时的时长是之前的两倍。
tu18

用寄存器点亮一个LED灯

如何用寄存器点亮一个led灯:
使用寄存器创建模板:
tu9
首先我们在stm32f4xx.h这个文件中来对外设端口(本次我还是用GPIOC3)
进行配置:
第一步:我们在这个文件中宏定义一下C语言的外设基地址,代码如下:

#define PERIPH_BASE ((unsigned int)0x40000000)、
(用到了数据类型的强制转换,避免遇到错误)
第二步:分别定义总线基地址,因为GPIO的总线基地址都是挂接在AHB1上,
所以我们定义的代码如下:

#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
第三步:我们对GPIOC进行配置:
首先,对GPIOC的基地址定义,这个基地址我们需要查找stm32f4xx中文参考手册,
在中文参考手册的第二章的存储器和总线架构的存储器映射里有所有外设对应的地址和总线。
如图:
tu10
我们可以看到GPIO的边界地址是0x4002 0800,由前两行代码可知,AHB1PERIPH_BASE是0x4002 0000
所以对GPIOC的基地址的定义就可以写成:

#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)
然后我们需要对GPIOC的模式寄存器进行定义,在中文参考手册的GPIO寄存器一节中由对模式寄存器的讲解。
如图:
tu11
我们可以看到,它的偏移地址是0x00,也就是说模式寄存器的地址就是GPIOC的基地址,下面我们对它进行定义:

#define GPIOC_MODER (unsigned int)(GPIOC_BASE+0x00)
然后是对置位复位寄存器(GPIOC_BSRR)进行定义,同样看中文参考手册,如图:
tu12
我们看到它的偏移地址是0x18,所以代码如下:

#define GPIOC_BSRR (unsigned int)(GPIOC_BASE+0x18)
(如果不进行强制类型转换,我们的编译器就会认为它是一个立即数,不是地址)
因为STM32要求每次使用外设都必须对相应外设开时钟,所以我们要对时钟外设基地址进行定义,
在存储器映射一章有,RCC的偏移地址是0x4002 3800,所以定义如下:

#define RCC_BASE (AHB1PERIPH_BASE + 0x3800)
然后对AHB1进行开时钟,对查看中文参考手册,在RCC寄存器的外设时钟使能一节,如图:
tu13
我们看到,它的偏移地址是0x30,所以它的定义如下:

#define RCC_AHB1ENR (unsigned int)(RCC_BASE+0x30)
这样我们对stm32f4xx.h就配置完了
第四步是写main函数
main函数代码如下:
int main()
{
RCC_AHB1ENR |= 1<<2;
GPIOC_MODER = (1<<(2*3));
while(1)
{
GPIOC_BSRR=(1<<(16+3));
delay(0xFFFFF);
GPIOC_BSRR=(1<<(3));
delay(0xFFFFF);
}
}
接下来我们对main函数进行分析:
首先查看中文参考手册,RCC AHB1外设时钟使能寄存器,即RCC_AHB1ENR,如图:
tu14
图中寄存器的第2位对应的是GPIOC,所以我们用1左移两位,进行与运算,从而让第2位置1,使能IO端口C时钟。

查看参考手册,如图:
tu11
将模式寄存器的第6位和第七位置为01,为通用输出模式。(写成(1<<(2*3)的好处是能看到是GPIOC的第几个端口)

查看参考手册,如图:
tu15
将置位复位寄存器的第19位置为1,是为了让PC3复位,点亮LED.
然后延时,再熄灭,最后达成闪烁的效果。

用库函数来点亮一个LED(STM32F407ZGT6)

如何点亮一个LED:
根据上一次的之后,在API里创建一个led.c文件和一个led.h文件,然后我们在led.h里添加如下代码:

#ifndef _led_H

#define _led_H

#include “stm32f4xx.h”
//添加stm32f4的官方头文件

#endif
再led.c中,添加#include “led.h”,然后我们需要对LED进行初始化,初始化的函数也在这里:
void LED_Init()
{
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC,ENABLE); //使能端口C时钟

GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT; //设置为输出模式
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_2;//管脚设置C3
          // (因为我用的是核心板,所以我直接用PC3在外部用了一个海绵板,没有使用板子的LED1)
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;//速度为100M
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOC,&GPIO_InitStructure); //初始化结构体
GPIO_ResetBits(GPIOC,GPIO_Pin_3);
            GPIO_SetBits(GPIOC,GPIO_Pin_2);

}
要注意在led.h中对这个函数进行声明。
(当PC3为低电平时LED灯点亮,PC3为高电平时LED灯熄灭)以下是LED原理图:
tu8

在main.c中添加main.h 和led.h两个头文件
然后main函数对管脚进行使能
int main()
{
LED_Init();
while(1)
{int i=1000000;
while(i–);
//GPIO_ResetBits(GPIOC,GPIO_Pin_3);//复位C3 点亮D1
GPIO_ToggleBits(GPIOC,GPIO_Pin_3);
i=1000000;
while(i–);
// GPIO_ResetBits(GPIOC,GPIO_Pin_2);//复位C2
//(set是设置为1,reset就是设置为0.)
GPIO_ToggleBits(GPIOC,GPIO_Pin_2);
// 因为C2设置的是GPIO_SetBits(GPIOC,GPIO_Pin_2)所以会有闪烁效果
}
}

要注意在main.h添加led.h头文件
最后是我完成后的效果:
tu7

以KEIL5为编译环境的stm32工程的建立

如何新建一个以KEIL5为编译环境的STM32的工程:
1.新建一个文件夹,再文件夹里再新建四个文件夹:startup、user、project、STM32F4xx_StdPeriph_Driver。
tu1

2.找到模板,向startup里添加startup_stm32f40_41xxx
tu2
3.在STM32F4xx_StdPeriph_Driver这个驱文件夹里分别创建一个inc、src文件夹分别存放.h(头文件)和.c文件
tu3
4.在user里添加一个api用于存放自己编写的功能代码,然后添加官方给的.h和.c文件
tu4
5.打开keil5,点击project,然后新建一个工程;然后点击品字图标将我们的文件夹全都加去,
tu6

6.最后看到这样的界面,就完成了新建工程的工作,接下来就可以编写代码了。
tu5