1)实验平台:探索者 STM32F407 开发板
2)摘自《STM32F4 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子

第五章 SYSTEM 文件夹介绍
第三章,我们介绍了如何在 MDK5 下建立 STM32F4 工程。在这个新建的工程之中,我们
用到了一个 SYSTEM 文件夹里面的代码,此文件夹里面的代码由 ALIENTEK 提供,是
STM32F4xx 系列的底层核心驱动函数,可以用在 STM32F4xx 系列的各个型号上面,方便大家
快速构建自己的工程。
SYSTEM 文件夹下包含了 delay、sys、usart 等三个文件夹。分别包含了 delay.c、sys.c、usart.c
及其头文件。通过这 3 个 c 文件,可以快速的给任何一款 STM32F4 构建最基本的框架。使用
起来是很方便的。
本章,我们将向大家介绍这些代码,通过这章的学习,大家将了解到这些代码的由来,也
希望大家可以灵活使用 SYSTEM 文件夹提供的函数,来快速构建工程,并实际应用到自己的项
目中去。
本章包括如下 3 个小结:
5.1,delay 文件夹代码介绍;
5.2,sys 文件夹代码介绍;
5.3,usart 文件夹代码介绍;
5.1 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);
前面 4 个函数,仅在支持操作系统(OS)的时候,需要用到,而后面 3 个函数,则不论是
否支持 OS 都需要用到。
在介绍这些函数之前,我们先了解一下编程思想:CM4 内核的处理和 CM3 一样,内部都
包含了一个 SysTick 定时器,SysTick 是一个 24 位的倒计数定时器,当计到 0 时,将从 RELOAD
寄存器中自动重装载定时初值。只要不把它在 SysTick 控制及状态寄存器中的使能位清除,就
永不停息。SysTick 在《STM32xx 中文参考手册》里面基本没有介绍,其详细介绍,请参阅
《STM32F3与 F4 系列Cortex M4内核编程手册》第230 页。我们就是利用 STM32的内部 SysTick
来实现延时的,这样既不占用中断,也不占用系统定时器。
这里我们将介绍的是 ALIENTEK 提供的最新版本的延时函数,该版本的延时函数支持在任
意操作系统(OS)下面使用,它可以和操作系统共用 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*21(假设系统时钟为 168Mhz,因为 systick 的频率为系统时钟频率的 1/8,那么 systick
每增加 1,就是 1/21us),然后我们就一直统计 systick 的计数变化,直到这个值变化了 50*21,
一旦检测到变化达到或者超过这个值,就说明延时 50us 时间到了。这样,我们只是抓取 SysTick
计数器的变化,并不需要修改 SysTick 的任何状态,完全不影响 SysTick 作为 UCOS 时钟节拍
的功能,这就是实现 delay 和操作系统共用 SysTick 定时器的原理。
下面我们开始介绍这几个函数。
5.1.1 操作系统支持宏定义及相关函数
当需要 delay_ms 和 delay_us 支持操作系统(OS)的时候,我们需要用到 3 个宏定义和 4
个函数,宏定义及函数代码如下:
//本例程仅作 UCOSII 和 UCOSIII 的支持,其他 OS,请自行参考着移植
//支持 UCOSII
#ifdef OS_CRITICAL_METHOD//OS_CRITICAL_METHOD 定义了,说明要支持 UCOSII
#define delay_osrunning OSRunning //OS 是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec OS_TICKS_PER_SEC //OS 时钟节拍,即每秒调度次数
#define delay_osintnesting OSIntNesting //中断嵌套级别,即中断嵌套次数
#endif
//支持 UCOSIII
#ifdef CPU_CFG_CRITICAL_METHOD
//CPU_CFG_CRITICAL_METHOD 定义了,说明要支持 UCOSIII
#define delay_osrunning OSRunning //OS 是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec OSCfg_TickRate_Hz //OS 时钟节拍,即每秒调度次数
#define delay_osintnesting OSIntNestingCtr //中断嵌套级别,即中断嵌套次数
#endif
//us 级延时时,关闭任务调度(防止打断 us 级延迟)
void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD //使用 UCOSIII
OS_ERR err;
OSSchedLock(&err); //UCOSIII 的方式,禁止调度,防止打断 us 延时
#else //否则 UCOSII
OSSchedLock(); //UCOSII 的方式,禁止调度,防止打断 us 延时
#endif
}
//us 级延时时,恢复任务调度
void delay_osschedunlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD //使用 UCOSIII
OS_ERR err;
OSSchedUnlock(&err); //UCOSIII 的方式,恢复调度#else //否则 UCOSII
OSSchedUnlock(); //UCOSII 的方式,恢复调度
#endif
}
//调用 OS 自带的延时函数延时
//ticks:延时的节拍数
void delay_ostimedly(u32 ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
OS_ERR err;
OSTimeDly(ticks,OS_OPT_TIME_PERIODIC,&err); //UCOSIII 延时采用周期模式
#else
OSTimeDly(ticks); //UCOSII 延时
#endif
}
//systick 中断服务函数,使用 OS 时用到
void SysTick_Handler(void)
{
HAL_IncTick();
if(delay_osrunning==1) //OS 开始跑了,才执行正常的调度处理
{
OSIntEnter(); //进入中断
OSTimeTick(); //调用 ucos 的时钟服务程序
OSIntExit(); //触发任务切换软中断
} }
#endif
以上代码,仅支持 UCOSII 和 UCOSIII,不过,对于其他 OS 的支持,也只需要对以上代
码进行简单修改即可实现。
支持 OS 需要用到的三个宏定义(以 UCOSII 为例)即:
#define delay_osrunning OSRunning //OS 是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec OS_TICKS_PER_SEC //OS 时钟节拍,即每秒调度次数
#define delay_osintnesting OSIntNesting //中断嵌套级别,即中断嵌套次数
宏定义:delay_osrunning,用于标记 OS 是否正在运行,当 OS 已经开始运行时,该宏定义
值为 1,当 OS 还未运行时,该宏定义值为 0。
宏定义:delay_ ostickspersec,用于表示 OS 的时钟节拍,即 OS 每秒钟任务调度次数。
宏定义:delay_ osintnesting,用于表示 OS 中断嵌套级别,即中断嵌套次数,每进入一个
中断,该值加 1,每退出一个中断,该值减 1。
支持 OS 需要用到的 4 个函数,即:
函数:delay_osschedlock,用于 delay_us 延时,作用是禁止 OS 进行调度,以防打断 us 级
延时,导致延时时间不准。
函数:delay_osschedunlock,同样用于 delay_us 延时,作用是在延时结束后恢复 OS 的调度,
继续正常的 OS 任务调度。
函数:delay_ostimedly,则是调用 OS 自带的延时函数,实现延时。该函数的参数为时钟节
拍数。
函数:SysTick_Handler,则是 systick 的中断服务函数,该函数为 OS 提供时钟节拍,同时
可以引起任务调度。
以上就是 delay_ms 和 delay_us 支持操作系统时,需要实现的 3 个宏定义和 4 个函数。
5.1.2delay_init 函数
该函数用来初始化 2 个重要参数:fac_us 以及 fac_ms;同时把 SysTick 的时钟源选择为外部时
钟,如果需要支持操作系统(OS),只需要在 sys.h 里面,设置 SYSTEM_SUPPORT_OS 宏的值
为 1 即可,然后,该函数会根据 delay_ostickspersec 宏的设置,来配置 SysTick 的中断时间,并
开启 SysTick 中断。具体代码如下:
//初始化延迟函数
//当使用 OS 的时候,此函数会初始化 OS 的时钟节拍
//SYSTICK 的时钟固定为 HCLK 时钟的 1/8
void delay_init()
{
#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 和 fac_ms 的值。而如果使用 OS 的时候,
则会进行一些不同的配置,这里的条件编译是根据 SYSTEM_SUPPORT_OS 这个宏来确定的,
该宏在 sys.h 里面定义。
SysTick 是 MDK 定义了的一个结构体(在 core_m4.h 里面),里面包含 CTRL、LOAD、VAL、
CALIB 等 4 个寄存器,
SysTick->CTRL 的各位定义如图 5.1.2.1 所示:

图 5.1.2.1 SysTick->CTRL 寄存器各位定义
SysTick-> LOAD 的定义如图 5.1.2.2 所示:

图 5.1.2.2 SysTick->LOAD 寄存器各位定义
SysTick-> VAL 的定义如图 5.1.2.3 所示:

图 5.1.2.3 SysTick->VAL 寄存器各位定义
SysTick-> CALIB 不常用,在这里我们也用不到,故不介绍了。
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);这句代码把 SysTick 的时钟选择为
内核时钟,这里需要注意的是:SysTick 的时钟源自 HCLK,假设我们外部晶振为 8M,然后倍
频到 168MHZ,那么 SysTick 的时钟即为 168Mhz,也就是 SysTick 的计数器 VAL 每减 1,就代
表时间过了 1/168us。所以 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)。
5.1.3 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; //需要的节拍数
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 调度
}
有了上面对 SysTick 寄存器的描述,这段代码不难理解。其实就是先把要延时的 us 数换算
成 SysTick 的时钟数,然后写入 LOAD 寄存器。然后清空当前寄存器 VAL 的内容,再开启倒数
功能。等到倒数结束,即延时了 nus。最后关闭 SysTick,清空 VAL 的值。实现一次延时 nus
的操作,但是这里要注意 nus 的值,不能太大,必须保证 nus<=(2^24)/fac_us,否则将导致
延时时间不准确。这里特别说明一下:temp&0x01,这一句是用来判断 systick 定时器是否还处
于开启状态,可以防止 systick 被意外关闭导致的死循环。
再来看看使用 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; //需要的节拍数
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 共用一个 SysTick。
上面的 delay_osschedlock 和 delay_osschedunlock 是 OS 提供的两个函数,用于调度上锁和
解锁,这里为了防止 OS 在 delay_us 的时候打断延时,可能导致的延时不准,所以我们利用这
两个函数来实现免打断,从而保证延时精度!同时,此时的 delay_us,,可以实现最长 2^32/fac_us,
在 168M 主频下,最大延时,大概是 204 秒。
5.1.4 delay_ms 函数
该函数是用来延时指定的 ms 的,其参数 nms 为要延时的毫秒数。该函数有使用 OS 和不
使用 OS 两个版本,这里我们分别介绍,首先是不使用 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 函数实现(此时任务无法调度)。
5.2 sys 文件夹代码介绍
sys 文件夹内包含了 sys.c 和 sys.h 两个文件。在 sys.h 里面定义了 STM32F4 的时钟配
置灯函数。sys.c 里面主要是一些汇编函数。
5.3 usart 文件夹介绍
usart 文件夹内包含了 usart.c 和 usart.h 两个文件。这两个文件用于串口的初始化和中
断接收。这里只是针对串口 1,比如你要用串口 2 或者其他的串口,只要对代码稍作修改
就可以了。usart.c 里面包含了 2 个函数一个是 void USART1_IRQHandler(void);另外一个是
void uart_init(u32 bound);里面还有一段对串口 printf 的支持代码,如果去掉,则会导致 printf
无法使用,虽然软件编译不会报错,但是硬件上 STM32 是无法启动的,这段代码不要去
修改。
5.3.1 printf 函数支持
这段引入 printf 函数支持的代码在 usart.c 文件的最上方,这段代码加入之后便可以通
过 printf 函数向串口发送我们需要的内容,方便开发过程中查看代码执行情况以及一些变
量值。这段代码如果要修改一般也只是用来改变 printf 函数针对的串口号,大多情况我们
都不需要修改。
这段代码为:
//加入以下代码,支持 printf 函数,而不需要选择 use MicroLIB
#if 1
#pragma import(__use_no_semihosting)
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式_sys_exit(int x)
{
x = x;
}
//重定义 fputc 函数
int fputc(int ch, FILE *f)
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
USART_SendData(USART1,(uint8_t)ch);
return ch;
}
#endif
5.3.2 uart_init 函数
void uart_init(u32 bound)函数是串口 1 初始化函数。该函数有 1 个参数为波特率,波特
率这个参数对于大家来说应该不陌生,这里就不多说了。uart_init 函数代码如下:
//初始化 IO 串口 1
//bound:波特率
void uart_init(u32 bound){
//UART 初始化设置
UART1_Handler.Instance=USART1; //USART1
UART1_Handler.Init.BaudRate=bound; //波特率
UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为 8 位数据格式
UART1_Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位
UART1_Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位
UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE;//无硬件流控
UART1_Handler.Init.Mode=UART_MODE_TX_RX; //收发模式
HAL_UART_Init(&UART1_Handler); //HAL_UART_Init()会使能 UART1
HAL_UART_Receive_IT(&UART1_Handler, (u8 *)aRxBuffer, RXBUFFERSIZE);
//该函数会开启接收中断:标志位 UART_IT_RXNE,并且设置接收缓冲以及接收
//缓冲接收最大数据量
}
串口相关知识,我们将在第九章讲解串口实验的时候给大家详细讲解。