文章目录
- 一、delay 文件夹代码介绍
- 1.delay_init 函数
- 2.delay_us 函数
- 3.delay_ms函数
- 二、sys 文件夹代码介绍
- 1.IO 口的位操作实现
- 三、usart 文件夹代码介绍
- 1.printf 函数支持
在 新建工程模板——库函数版本中,我们用到了一个
SYSTEM
文件夹里面的代码,此文件夹里面的代码由
ALIENTEK
提供,是
STM32F4xx
系列的底层核心驱动函数,可以用在
STM32F4xx
系列的各个型号上面,方便大家快速构建自己的工程。
SYSTEM
文件夹下包含了
delay
、
sys
、
usart
等三个文件夹。分别包含了
delay.c
、
sys.c
、
usart.c
及其头文件。
一、delay 文件夹代码介绍
delay
文件夹内包含了 delay.c
和 delay.h
两个文件,这两个文件用来实现系统的延时功能,其中包含 7 个函数:
void delay_osschedlock(void);
void delay_osschedunlock(void);
void delay_ostimedly(u32 ticks);
void SysTick_Handler(void);
void delay_init(u8 SYSCLK);
void delay_ms(u16 nms);
void delay_us(u32 nus);
在介绍这些函数之前,我们先了解一下编程思想:CM4
内核的处理和 CM3
一样,内部都包含了一个 SysTick
定时器,SysTick
是一个 24 位的倒计数定时器,当计到 0 时,将从 RELOAD
寄存器中自动重装载定时初值。只要不把它在 SysTick
控制及状态寄存器中的使能位清除,就永不停息。我们就是利用 STM32
的内部 SysTick
来实现延时的,这样既不占用中断,也不占用系统定时器。
这里,我们以 UCOSII
为例,介绍如何实现操作系统和我们的 delay
函数共用 SysTick
定时器。首先,我们简单介绍下 UCOSII
的时钟:ucos
运行需要一个系统时钟节拍(类似 “心跳”),而这个节拍是固定的(由 OS_TICKS_PER_SEC
宏定义设置),比如要求 5ms
一次(即可设置:OS_TICKS_PER_SEC=200
),在 STM32
上面,一般是由 SysTick
来提供这个节拍,也就是 SysTick
要设置为 5ms
中断一次,为 ucos
提供时钟节拍,而且这个时钟一般是不能被打断的(否则就不准了)。
因为在 ucos
下 systick
不能再被随意更改,如果我们还想利用 systick
来做 delay_us
或者delay_ms
的延时,就必须想点办法了,这里我们利用的是时钟摘取法。以 delay_us
为例,比如:delay_us(50)
,在刚进入 delay_us
的时候先计算好这段延时需要等待的 systick
计数次数,这里为 50*180
(假设系统时钟为 180Mhz
,因为我们设置 systick
的频率为系统时钟频率,那么 systick
每增加 1,就是 1/180us
),然后我们就一直统计 systick
的计数变化,直到这个值变化了 50*180
,一旦检测到变化达到或者超过这个值,就说明延时 50us
时间到了。这样,我们只是抓取 SysTick
计数器的变化,并不需要修改 SysTick
的任何状态,完全不影响 SysTick
作为 UCOS
时钟节拍的功能,这就是实现 delay
和操作系统共用 SysTick
定时器的原理。
1.delay_init 函数
该函数用来初始化 2 个重要参数:fac_us
以及 fac_ms
;同时把 SysTick
的时钟源选择为外部时钟,如果需要支持操作系统(OS
),只需要在 sys.h
里面,设置 SYSTEM_SUPPORT_OS
宏的值为 1 即可,然后,该函数会根据delay_ostickspersec
宏的设置,来配置 SysTick
的中断时间,并开启 SysTick
中断。具体代码如下:
//初始化延迟函数
//当使用ucos的时候,此函数会初始化ucos的时钟节拍
//SYSTICK的时钟固定为AHB时钟
//SYSCLK:系统时钟频率
void delay_init(u8 SYSCLK)
{
#if SYSTEM_SUPPORT_OS //如果需要支持OS.
u32 reload;
#endif
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);//SysTick频率为HCLK
fac_us=SYSCLK; //不论是否使用OS,fac_us都需要使用
#if SYSTEM_SUPPORT_OS //如果需要支持OS.
reload=SYSCLK; //每秒钟的计数次数 单位为K
reload*=1000000/delay_ostickspersec; //根据delay_ostickspersec设定溢出时间
//reload为24位寄存器,最大值:16777216,在180M下,约合0.745s左右
fac_ms=1000/delay_ostickspersec; //代表OS可以延时的最少单位
SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;//开启SYSTICK中断
SysTick->LOAD=reload; //每1/OS_TICKS_PER_SEC秒中断一次
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启SYSTICK
#else
#endif
}
可以看到,delay_init
函数使用了条件编译,来选择不同的初始化过程,如果不使用 OS
的时候,只是设置一下 SysTick
的时钟源以及确定 fac_us
值。而如果使用 OS
的时候,则会进行一些不同的配置,这里的条件编译是根据SYSTEM_SUPPORT_OS
这个宏来确定的,该宏在sys.h
里面定义。SysTick
是 MDK
定义了的一个结构体,里面包含 CTRL
、LOAD
、VAL
、CALIB
等 4 个寄存器,
/**
\brief Structure type to access the System Timer (SysTick).
*/
typedef struct
{
__IOM uint32_t CTRL; /*!< Offset: 0x000 (R/W) SysTick Control and Status Register */
__IOM uint32_t LOAD; /*!< Offset: 0x004 (R/W) SysTick Reload Value Register */
__IOM uint32_t VAL; /*!< Offset: 0x008 (R/W) SysTick Current Value Register */
__IM uint32_t CALIB; /*!< Offset: 0x00C (R/ ) SysTick Calibration Register */
} SysTick_Type;
CTRL
是SysTick
控制和状态寄存器。SysTick->CTRL
的各位定义如图所示:
LOAD
是SysTick
自动重装载除值寄存器。SysTick-> LOAD
的定义如图所示:
VAL
是SysTick
当前值寄存器。SysTick-> VAL
的定义如图所示:
CALIB
是SysTick
校准值寄存器。SysTick-> CALIB
不常用,在这里我们也用不到,故不介绍了。
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);
这句代码把 SysTick
的时钟选择为内核时钟,这里需要注意的是:SysTick
的时钟源自 HCLK
,假设我们外部晶振为 25M
,然后倍频到 180MHZ
,那么 SysTick
的时钟即为 180Mhz
,也就是 SysTick
的计数器 VAL
每减 1,就代表时间过了 1/180us
。所以fac_us=SYSCLK;
这句话就是计算在 SYSCLK
时钟频率下延时 1us
需要多少个 SysTick
时钟周期。
在不使用 OS
的时候:fac_us
为 us
延时的基数,也就是延时 1us
,Systick
定时器需要走过的时钟周期数。 当使用 OS
的时候,fac_us
,还是 us
延时的基数,不过这个值不会被写到SysTick->LOAD
寄存器来实现延时,而是通过时钟摘取的办法实现的。而fac_ms
则代表 ucos
自带的延时函数所能实现的最小延时时间(如 delay_ostickspersec=200
,那么 fac_ms
就是 5ms
)。
2.delay_us 函数
该函数用来延时指定的 us
,其参数 nus
为要延时的微秒数。该函数有使用 OS
和不使用 OS
两个版本,这里我们首先介绍不使用 OS
的时候,实现函数如下:
//延时nus
//nus为要延时的us数.
//nus:0~190887435(最大值即2^32/fac_us@fac_us=22.5)
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
}
}
这里就正是利用了我们前面提到的时钟摘取法,ticks
是延时 nus
需要等待的 SysTick
计数次数(也就是延时时间),told
用于记录最近一次的 SysTick->VAL
值,然后 tnow
则是当前的SysTick->VAL
值,通过他们的对比累加,实现 SysTick
计数次数的统计,统计值存放在 tcnt
里面,然后通过对比 tcnt
和 ticks
,来判断延时是否到达,从而达到不修改 SysTick
实现 nus
的延时。
对于使用 OS
的时候,delay_us
的实现函数和不使用 OS
的时候方法类似,都是使用的时钟摘取法,只不过使用 delay_osschedlock
和delay_osschedunlock
两个函数,用于调度上锁和解锁,这是为了防止 OS
在 delay_us
的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度。
//延时nus
//nus:要延时的us数.
//nus:0~190887435(最大值即2^32/fac_us@fac_us=22.5)
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
delay_osschedlock(); //阻止OS调度,防止打断us延时
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
delay_osschedunlock(); //恢复OS调度
}
3.delay_ms函数
该函数是用来延时指定的 ms
的,其参数 nms
为要延时的毫秒数。该函数有使用 OS
和不
使用 OS
两个版本,这里我们分别介绍,首先是不使用 OS
的时候,实现函数如下:
//延时nms
//nms:要延时的ms数
void delay_ms(u16 nms)
{
u32 i;
for(i=0;i<nms;i++) delay_us(1000);
}
该函数其实就是多次调用前面所讲的 delay_us
函数,来实现毫秒级延时的。
再来看看使用 OS
的时候,delay_ms
的实现函数如下:
//延时nms
//nms:要延时的ms数
//nms:0~65535
void delay_ms(u16 nms)
{
if(delay_osrunning&&delay_osintnesting==0)//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)
{
if(nms>=fac_ms) //延时的时间大于OS的最少时间周期
{
delay_ostimedly(nms/fac_ms); //OS延时
}
nms%=fac_ms; //OS已经无法提供这么小的延时了,采用普通方式延时
}
delay_us((u32)(nms*1000)); //普通方式延时
}
该函数中,delay_osrunning
是 OS
正在运行的标志,delay_osintnesting
则是 OS
中断嵌套次数,必须 delay_osrunning
为真,且 delay_osintnesting
为 0 的时候,才可以调用 OS
自带的延时函数进行延时(可以进行任务调度),delay_ostimedly
函数就是利用 OS
自带的延时函数,实现任务级延时的,其参数代表延时的时钟节拍数(假设 delay_ostickspersec=200
,那么delay_ostimedly (1)
,就代表延时 5ms
)。
当 OS
还未运行的时候,我们的 delay_ms
就是直接由 delay_us
实现的,OS
下的 delay_us
可以实现很长的延时(达到 204 秒)而不溢出!,所以放心的使用 delay_us
来实现 delay_ms
,不过由于 delay_us
的时候,任务调度被上锁了,所以还是建议不要用 delay_us
来延时很长的时间,否则影响整个系统的性能。
当 OS
运行的时候,我们的 delay_ms
函数将先判断延时时长是否大于等于 1 个 OS
时钟节拍fac_ms
,当大于这个值的时候,我们就通过调用 OS
的延时函数来实现(此时任务可以调度),不足 1 个时钟节拍的时候,直接调用 delay_us
函数实现(此时任务无法调度)。
二、sys 文件夹代码介绍
sys
文件夹内包含了 sys.c
和 sys.h
两个文件。在 sys.h
里面定义了 STM32F4
的 IO
口位
操作输入读取宏定义和输出宏定义以及类型别名。sys.c
里面除了定义时钟系统配置函数Stm32_Clock_Init
外主要是一些汇编函数。本小节我们主要向大家介绍 sys.h
头文件里面的 IO
口位操作。
1.IO 口的位操作实现
该部分代码在 sys.h
文件中,实现对STM32F4
各个 IO
口的位操作,包括读入和输出。
当然在这些函数调用之前,必须先进行 IO
口时钟的使能和 IO
口功能定义。此部分仅仅对IO
口进行输入输出读取和控制。
位带操作简单的说,就是把每个比特膨胀为一个 32 位的字,当访问这些字的时候就达到了访问比特的目的,比如说 GPIO
的 ODR
寄存器有 32 个位,那么可以映射到 32 个地址上,我们去访问这 32 个地址就达到访问 32 个比特的目的。这样我们往某个地址写 1 就达到往对应比特位写 1 的目的,同样往某个地址写 0 就达到往对应的比特位写 0 的目的。
对于上图,我们往 Address0
地址写入 1,那么就可以达到往寄存器的第 0 位 Bit0
赋值1 的目的。下面我们看看 sys.h 中位带操作的定义,代码如下:
//位带操作,实现51类似的GPIO控制功能
//具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).M4同M3类似,只是寄存器地址变了.
//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 GPIOJ_ODR_ADDr (GPIOJ_BASE+20) //0x40022414
#define GPIOK_ODR_ADDr (GPIOK_BASE+20) //0x40022814
#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
#define GPIOJ_IDR_Addr (GPIOJ_BASE+16) //0x40022410
#define GPIOK_IDR_Addr (GPIOK_BASE+16) //0x40022810
//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) //输入
#define PJout(n) BIT_ADDR(GPIOJ_ODR_Addr,n) //输出
#define PJin(n) BIT_ADDR(GPIOJ_IDR_Addr,n) //输入
#define PKout(n) BIT_ADDR(GPIOK_ODR_Addr,n) //输出
#define PKin(n) BIT_ADDR(GPIOK_IDR_Addr,n) //输入
以上代码的便是 GPIO
位带操作的具体实现。比如说,我们调用 PAout(1)=1
是设置了 GPIOA
的第一个管脚 GPIOA.1
为 1,实际是设置了寄存器的某个位,但是我们的定义中可以跟踪过去看到却是通过计算访问了一个地址。上面一系列公式也就是计算GPIO
的某个 IO
口对应的位带区的地址了。
有了上面的代码,我们就可以像51一样操作STM32
的IO
口了。比如,我要PORTA
的第七个 IO
口输出 1,则可以使用 PAout(6)=1;
即可实现。我要判断 PORTA
的第 15个位是否等于 1,则可以使用 if(PAin(14)==1)…;
就可以了。
这里顺便说一下,在 sys.h
中的还有个全局宏定义:
//0,不支持os
//1,支持os
#define SYSTEM_SUPPORT_OS 0 //定义系统文件夹是否支持OS
SYSTEM_SUPPORT_OS
,这个宏定义用来定义 SYSTEM
文件夹是否支持 ucos
,如果在 ucos
下面使用 SYSTEM
文件夹,那么设置这个值为 1 即可,否则设置为 0(默认)。
三、usart 文件夹代码介绍
该文件夹下面有 usart.c
和 usarts.h
两个文件。串口相关知识将在串口实验中讲解。
1.printf 函数支持
printf
函数支持的代码在 usart.c
文件的最上方,在我们初始化和使能串口 1 之后,然后把这段代码加入到工程,便可以通过 printf
函数向串口 1 发送我们需要的内容,方便开发过程中查看代码执行情况以及一些变量值。这段代码如果要修改一般也只是用来改变printf
函数针对的串口号,大多情况我们都不需要修改。代码如下:
//加入以下代码,支持printf函数,而不需要选择use MicroLIB
//#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif