1)实验平台:alientek 阿波罗 STM32F767 开发板2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子
第五章 SYSTEM 文件夹介绍
第三章,我们介绍了如何在 MDK5 下建立 STM32F7 工程。在这个新建的工程之中,我们
用到了一个 SYSTEM 文件夹里面的代码,此文件夹里面的代码由 ALIENTEK 提供,是
STM32F7xx 系列的底层核心驱动函数,可以用在 STM32F7xx 系列的各个型号上面,方便大家
快速构建自己的工程。
SYSTEM 文件夹下包含了 delay、sys、usart 等三个文件夹。分别包含了 delay.c、sys.c、usart.c
及其头文件。通过这 3 个 c 文件,可以快速的给任何一款 STM32F7 构建最基本的框架。使用
起来是很方便的。
本章,我们将向大家介绍这些代码,通过这章的学习,大家将了解到这些代码的由来,也
希望大家可以灵活使用 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 在《STM32F7 中文参考手册》里面基本没有介绍,其详细介绍,请参阅
《STM32F7 编程手册》第 211 页,4.4 节。我们就是利用 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*216(假设系统时钟为 216Mhz,因为我们设置 systick 的频率为系统时钟频率,那么 systick
每增加 1,就是 1/216us),然后我们就一直统计 systick 的计数变化,直到这个值变化了 50*216,
一旦检测到变化达到或者超过这个值,就说明延时 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 //使用 UCOSIII 时
OS_ERR err;
OSTimeDly(ticks,OS_OPT_TIME_PERIODIC,&err);//UCOSIII 延时采用周期模式
#else
OSTimeDly(ticks);
//UCOSII 延时
#endif
}
//systick 中断服务函数,使用 ucos 时用到
void SysTick_Handler(void)
{
if(delay_osrunning==1)
//OS 开始跑了,才执行正常的调度处理
{
OSIntEnter();
//进入中断
OSTimeTick();
//调用 ucos 的时钟服务程序
OSIntExit();
//触发任务切换软中断
}
}
以上代码,仅支持 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.2 delay_init 函数
该函数用来初始化 2 个重要参数:fac_us 以及 fac_ms;同时把 SysTick 的时钟源选择为外
部时钟,如果需要支持操作系统(OS),只需要在 sys.h 里面,设置 SYSTEM_SUPPORT_OS 宏
的值为 1 即可,然后,该函数会根据 delay_ostickspersec 宏的设置,来配置 SysTick 的中断时间,
并开启 SysTick 中断。具体代码如下:
//初始化延迟函数
//当使用 OS 的时候,此函数会初始化 OS 的时钟节拍
//SYSTICK 的时钟固定为 HCLK
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 定义了的一个结构体(在 core_m4.h 里面),里面包含 CTRL、LOAD、VAL、
CALIB 等 4 个寄存器,
SysTick->CTRL 的各位定义如图 5.1.2.1 所示:
SysTick-> LOAD 的定义如图 5.1.2.2 所示:
SysTick-> VAL 的定义如图 5.1.2.3 所示:
SysTick-> CALIB 不常用,在这里我们也用不到,故不介绍了。
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK);这句代码把 SysTick 的时钟选择为
内核时钟,这里需要注意的是:SysTick 的时钟源自 HCLK,假设我们外部晶振为 25M,然后
倍频到 216MHZ,那么 SysTick 的时钟即为 216Mhz,也就是 SysTick 的计数器 VAL 每减 1,就
代表时间过了 1/216us。所以 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~204522252(最大值即 2^32/fac_us@fac_us=21)
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
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 的时候打断延时,可能导致的延时不准,所以我们利用这两个函
数来实现免打断,从而保证延时精度。
5.1.4 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 函数实现(此时任务无法调度)。
5.1.5 HAL 库延时函数 HAL_Delay 解析
前面我们讲解了 ALIENTEK 提供的使用 Systick 实现延时相关函数。实际上,HAL 库有提
供延时函数,只不过它只能实现简单的毫秒级别延时,没有实现 us 级别延时。下面我们列出
HAL 库实现延时相关的函数。首先是功能配置函数:
//调用 HAL_SYSTICK_Config 函数配置每隔 1ms 中断一次:文件 stm32f7xx_hal.c 中定义
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
/*配置 1ms 中断一次*/
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority ,0);
return HAL_OK;
}
//HAL 库的 SYSTICK 配置函数:文件 stm32f7xx_hal_context.c 中定义
uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
return SysTick_Config(TicksNumb);
}
//内核的 Systick 配置函数,配置每隔 ticks 个 systick 周期中断一次
//文件 core_cm4.h 中
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
...//此处省略函数定义
}
上面三个函数,实际上开放给 HAL 调用的主要是 HAL_InitTick 函数,该函数在 HAL 库初
始化函数 HAL_Init 中会被调用。该函数通过间接调用 SysTick_Config 函数配置 Systick 定时器
每隔 1ms 中断一次,永不停歇。
接下来我们来看看延时的逻辑控制代码:
//Systick 中断服务函数:文件 stm32f7xx_it.c 中
void SysTick_Handler(void)
{
HAL_IncTick();
}
//下面代码均在文件 stm32f7xx_hal.c 中
static __IO uint32_t uwTick; //定义计数全局变量
__weak void HAL_IncTick(void) //全局变量 uwTick 递增
{
uwTick++;
}
__weak uint32_t HAL_GetTick(void) //获取全局变量 uwTick 的值
{
return uwTick;
}
//开放的 HAL 延时函数,延时 Delay 毫秒
__weak void HAL_Delay(__IO uint32_t Delay)
{
uint32_t tickstart = 0;
tickstart = HAL_GetTick();
while((HAL_GetTick() - tickstart) < Delay)
{
}
}
HAL 库实现延时功能非常简单,首先定义了一个 32 位全局变量 uwTick,在 Systick 中断
服务函数 SysTick_Handler 中通过调用 HAL_IncTick 实现 uwTick 值不断增加,也就是每隔 1ms
增加 1。而 HAL_Delay 函数在进入函数之后先记录当前 uwTick 的值,然后不断在循环中读取
uwTick 当前值,进行减运算,得出的就是延时的毫秒数,整个逻辑非常简单也非常清晰。
但是,HAL库的延时函数有一个局限性,在中断服务函数中使用 HAL_Delay会引起混乱,
因为它是通过中断方式实现,而 Systick 的中断优先级是最低的,所以在中断中运行 HAL_Delay
会导致延时出现严重误差。所以一般情况下,推荐大家使用 ALIENTEK 提供的延时函数库。
5.2 sys 文件夹代码介绍
sys 文件夹内包含了 sys.c 和 sys.h 两个文件。在 sys.h 里面除了函数申明外主要是定义了一
些常用数据类型短关键字。sys.c 里面除了定义时钟系统配置函数 Stm32_Clock_Init 外主要是一
些汇编函数以及 Cache 相关操作函数,对于函数 Stm32_Clock_Init 的讲解请参考本手册 4.3 小
节 STM32F7 时钟系统章节内容。接下来我们看看 STM32F7 的 Cache 使能函数。
5.2.1 Cache 使能函数
STM32F7 自带了指令 Cache(I Cache)和数据 Cache(D Cache),使用 I/D Cache 可以缓存
指令/数据,提高 CPU 访问指令/数据的速度,从而大大提高 MCU 的性能。不过,MCU 在复位
后,I/D Cache 默认都是关闭的,为了提高性能,我们需要开启 I/D Cache,在 sys.c 里面,我们
提供了如下函数:
//使能 STM32F7 的 L1-Cache,同时开启 D cache 的强制透写
void Cache_Enable(void)
{
SCB_EnableICache(); //使能 I-Cache,函数在 core_cm7.h 里面定义
SCB_EnableDCache(); //使能 D-Cache,函数在 core_cm7.h 里面定义
SCB->CACR|=1<<2; //强制 D-Cache 透写,如不开启,实际使用中可能遇到各种问题
}
该函数,通过调用 SCB_EnableICache 和 SCB_EnableDCache 这两个函数来使能 I Cache 和
D Cache。不过,在使能 D Cache 之后,SRAM 里面的数据有可能会被缓存在 Cache 里面,此
时如果有 DMA 之类的外设访问这个 SRAM 里面的数据,就有可能和 Cache 里面数据不同步,
导致数据出错,为了防止这种问题,保证数据的一致性,我们设置了 D Cache 的强制透写功能
(Write Through),这样 CPU 每次操作 Cache 里面的数据,同时也会更新到 SRAM 里面,保证
D Cache 和 SRAM 里面数据一致。关于 Cache 的详细介绍,请参考《STM32F7 Cache Oveview》
和《Level 1 cache on STM32F7 Series》(见光盘:8,STM32 参考资料 文件夹)。
这里 SCB_EnableICache 和 SCB_EnableDCache 这两个函数,是在 core_cm7.h 里面定义的,
我们直接调用即可,另外,core_cm7.h 里面还提供了以下五个常用函数:
1,SCB_DisableICache 函数,用于关闭 I Cache。
2,SCB_DisableDCache 函数,用于关闭 D Cache。
3,SCB_InvalidateDCache 函数,用于丢弃 D Cache 当前数据,重新从 SRAM 获取数据。
4,SCB_CleanDCache 函数,用于将 D Cache 数据回写到 SRAM 里面,同步数据。
5,SCB_CleanInvalidateDCache 函数,用于回写数据到 SRAM,并重新获取 D Cache 数据。
在 Cache_Enable 函数里面,我们直接开启了 D Cache 的透写模式,这样带来的好处就是可
以保证D Cache 和SRAM里面数据的一致性,坏处就是会损失一定的性能(每次都要回写数据),
如果大家想自己控制 D Cache 数据的回写,以获得最佳性能,则可以关闭 D Cache 透写模式,
并在适当的时候,调用 SCB_CleanDCache、SCB_InvalidateDCache 和 SCB_CleanInvalidateDCache
等函数,这对程序员的要求非常高,程序员必须清楚什么时候该回写,什么时候该更新 D Cache!
如果能力不够,还是建议开启 D Cache 的透写,以免引起各种莫名其妙的问题。
5.3 usart 文件夹介绍
该文件夹下面有 usart.c 和 usarts.h 两个文件。串口相关知识,我们将在第九章讲解串
口实验的时候给大家详细讲解。本节我们只给大家讲解比较独立的 printf 函数支持相关的
知识。
5.3.1 printf 函数支持
printf 函数支持的代码在 usart.c 文件的最上方,在我们初始化和使能串口 1 之后,然
后把这段代码加入到工程,便可以通过 printf 函数向串口 1 发送我们需要的内容,方便开
发过程中查看代码执行情况以及一些变量值。这段代码如果要修改一般也只是用来改变
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((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (u8) ch;
return ch;
}
#endif