1)实验平台:正点原子MiniPRO STM32H750 开发板
2) 章节摘自【正点原子】《MiniPRO H750开发指南》
3)购买链接:https://detail.tmall.com/item.htm?id=677017430560
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boar ... 32h750_minipro.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子STM32技术交流群:756580169 


第十二章 SYSTEM文件夹介绍
SYSTEM文件夹里面的代码由正点原子提供,是STM32H7xx系列的底层核心驱动函数,可以用在STM32H7xx系列的各个型号上面,方便大家快速构建自己的工程。 SYSTEM文件夹下包含了delay、sys、usart等三个文件夹。分别包含了delay.c、sys.c、usart.c及其头文件。通过这3个c文件,可以快速的给任何一款STM32H7构建最基本的框架,使用起来是很方便的。 本章,我们将向大家介绍这些代码,通过这章的学习,大家将了解到这些代码的由来,也希望大家可以灵活使用SYSTEM文件夹提供的函数,来快速构建工程,并实际应用到自己的项目中去。 本章将分为如下几个小节: 12.1 deley文件夹代码介绍 12.2 sys文件夹代码介绍 12.3 usart文件夹代码介绍
12.1 deley文件夹代码介绍delay文件夹内包含了delay.c和delay.h两个文件,这两个文件用来实现系统的延时功能,其中包含7个函数: - void delay_osschedlock(void);
- void delay_osschedunlock(void);
- void delay_ostimedly(uint32_t ticks);
- void SysTick_Handler(void);
- void delay_init(uint16_t sysclk);
- void delay_us(uint32_t nus);
- void delay_ms(uint16_t nms);
复制代码前面4个函数,仅在支持操作系统(OS)的时候,需要用到,而后面3个函数,则不论是否支持OS都需要用到。 在介绍这些函数之前,我们先了解一下编程思想:CM7内核和CM3/CM4内核一样,内部都包含了一个SysTick定时器,SysTick 是一个24 位的向下递减的计数定时器,当计数值减到0 时,将从RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除,就永不停息。SysTick在《STM32H7xx参考手册_V7(英文版).pdf》里面基本没有介绍,其详细介绍,请参阅《STM32H7编程手册.pdf》第212页,4.4节。我们就是利用STM32的内部SysTick来实现延时的,这样既不占用中断,也不占用系统定时器。 这里我们将介绍的是正点原子提供的最新版本的延时函数,该版本的延时函数支持在任意操作系统(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*480(假设系统时钟为480Mhz,因为systick的频率等于系统时钟频率,那么systick每增加1,就是1/480us),然后我们就一直统计systick的计数变化,直到这个值变化了50*480,一旦检测到变化达到或者超过这个值,就说明延时50us时间到了。这样,我们只是抓取SysTick计数器的变化,并不需要修改SysTick的任何状态,完全不影响SysTick作为UCOS时钟节拍的功能,这就是实现delay和操作系统共用SysTick定时器的原理。 下面我们开始介绍这几个函数。 12.1.1 操作系统支持宏定义及相关函数当需要delay_ms和delay_us支持操作系统(OS)的时候,我们需要用到3个宏定义和4个函数,宏定义及函数代码如下: - /*
- * 当delay_us/delay_ms需要支持OS的时候需要三个与OS相关的宏定义和函数来支持
- * 首先是3个宏定义:
- * delay_osrunning :用于表示OS当前是否正在运行,以决定是否可以使用相关函数
- * delay_ostickspersec:用于表示OS设定的时钟节拍,delay_init
- * 将根据这个参数来初始化systick
- * delay_osintnesting :用于表示OS中断嵌套级别,因为中断里面不可以调度,
- * delay_ms使用该参数来决定如何运行
- * 然后是3个函数:
- * delay_osschedlock :用于锁定OS任务调度,禁止调度
- * delay_osschedunlock:用于解锁OS任务调度,重新开启调度
- * delay_ostimedly :用于OS延时,可以引起任务调度.
- *
- * 本例程仅作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
- /**
- * @brief us级延时时,关闭任务调度(防止打断us级延迟)
- * [url=home.php?mod=space&uid=271674]@param[/url] 无
- * @retval 无
- */
- void delay_osschedlock(void)
- {
- #ifdef CPU_CFG_CRITICAL_METHOD /* 使用UCOSIII */
- OS_ERR err;
- OSSchedLock(&err); /* UCOSIII的方式,禁止调度,防止打断us延时 */
- #else /* 否则UCOSII */
- OSSchedLock(); /* UCOSII的方式,禁止调度,防止打断us延时 */
- #endif
- }
- /**
- * @brief us级延时时,恢复任务调度
- * @param 无
- * @retval 无
- */
- void delay_osschedunlock(void)
- {
- #ifdef CPU_CFG_CRITICAL_METHOD /* 使用UCOSIII */
- OS_ERR err;
- OSSchedUnlock(&err); /* UCOSIII的方式,恢复调度 */
- #else /* 否则UCOSII */
- OSSchedUnlock(); /* UCOSII的方式,恢复调度 */
- #endif
- }
- /**
- * @brief us级延时时,恢复任务调度
- * @param ticks: 延时的节拍数
- * @retval 无
- */
- void delay_ostimedly(uint32_t ticks)
- {
- #ifdef CPU_CFG_CRITICAL_METHOD
- OS_ERR err;
- OSTimeDly(ticks, OS_OPT_TIME_PERIODIC, &err); /* UCOSIII延时采用周期模式 */
- #else
- OSTimeDly(ticks); /* UCOSII延时 */
- #endif
- }
- /**
- * @brief systick中断服务函数,使用OS时用到
- * @param ticks: 延时的节拍数
- * @retval 无
- */
- 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个函数。 12.1.2 delay_init函数该函数用来初始化2个重要参数:fac_us以及fac_ms;同时把SysTick的时钟源选择为外部时钟,如果需要支持操作系统(OS),只需要在sys.h里面,设置SYS_SUPPORT_OS宏的值为1即可,然后,该函数会根据delay_ostickspersec宏的设置,来配置SysTick的中断时间,并开启SysTick中断。具体代码如下: - /**
- * @brief 初始化延迟函数
- * @param sysclk: 系统时钟频率, 即CPU频率(rcc_c_ck), 480Mhz
- * @retval 无
- */
- void delay_init(uint16_t sysclk)
- {
- #if SYS_SUPPORT_OS /* 如果需要支持OS */
- uint32_t reload;
- #endif
- /* SYSTICK使用内核时钟源,同CPU同频率 */
- HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
- g_fac_us = sysclk; /* 不论是否使用OS,g_fac_us都需要使用 */
- #if SYS_SUPPORT_OS /* 如果需要支持OS. */
- reload = sysclk; /* 每秒钟的计数次数 单位为M */
- /* 根据delay_ostickspersec设定溢出时间,reload为24位
- 寄存器,最大值:16777216,在480M下,约合0.035s左右 */
- reload *= 1000000 / delay_ostickspersec;
- g_fac_ms = 1000 / delay_ostickspersec; /* 代表OS可以延时的最少单位 */
- SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; /* 开启SYSTICK中断 */
- SysTick->LOAD = reload; /* 每1/delay_ostickspersec秒中断一次 */
- SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* 开启SYSTICK */
- #endif
- }
复制代码可以看到,delay_init函数使用了条件编译,来选择不同的初始化过程,如果不使用OS的时候,只是设置一下SysTick的时钟源以及确定fac_us值。而如果使用OS的时候,则会进行一些不同的配置,这里的条件编译是根据SYS_SUPPORT_OS这个宏来确定的,该宏在sys.h里面定义。 SysTick是MDK定义了的一个结构体(在core_m7.h里面),里面包含CTRL、LOAD、VAL、CALIB等4个寄存器。 SysTick->CTRL的各位定义如图12.1.2.1所示: file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image002.jpg 图12.1.2.1 SysTick->CTRL寄存器各位定义 SysTick-> LOAD的定义如图12.1.2.2所示: file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image004.jpg 图12.1.2.2 SysTick->LOAD寄存器各位定义 SysTick-> VAL的定义如图12.1.2.3所示: file:///C:/Users/ALIENTEK/AppData/Local/Temp/msohtmlclip1/01/clip_image006.jpg 图12.1.2.3 SysTick->VAL寄存器各位定义 SysTick-> CALIB不常用,在这里我们也用不到,故不介绍了。 HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);这句代码把SysTick的时钟选择为内核时钟,这里需要注意的是:SysTick的时钟源自HCLK,假设我们外部晶振为8M,然后倍频到480MHZ,那么SysTick的时钟即为480Mhz,也就是SysTick的计数器VAL每减1,就代表时间过了1/480us。 在不使用OS的时候:fac_us,为us延时的基数,也就是延时1us,Systick定时器需要走过的时钟周期数。 当使用OS的时候,fac_us,还是us延时的基数,不过这个值不会被写到SysTick->LOAD寄存器来实现延时,而是通过时钟摘取的办法实现的(前面已经介绍了)。而fac_ms则代表ucos自带的延时函数所能实现的最小延时时间(如delay_ostickspersec=200,那么fac_ms就是5ms)。 12.1.3 delay_us函数该函数用来延时指定的us,其参数nus为要延时的微秒数。该函数有使用OS和不使用OS两个版本,这里我们首先介绍不使用OS的时候,实现函数如下: - /**
- * @brief 延时nus
- * @param nus: 要延时的us数.
- * [url=home.php?mod=space&uid=60778]@note[/url] 注意: nus的值,不要大于34952us(最大值即2^24/g_fac_us @g_fac_us = 480)
- * @retval 无
- */
- void delay_us(uint32_t nus)
- {
- uint32_t ticks;
- uint32_t told, tnow, tcnt = 0;
- uint32_t reload = SysTick->LOAD; /* LOAD的值 */
- ticks = nus * g_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的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度。 再来看看使用OS的时候,delay_us的实现函数如下: - /**
- * @brief 延时nus
- * @param nus: 要延时的us数
- * @note nus取值范围: 0~8947848(最大值即2^32 / g_fac_us @g_fac_us = 480)
- * @retval 无
- */
- void delay_us(uint32_t nus)
- {
- uint32_t ticks;
- uint32_t told, tnow, tcnt = 0;
- uint32_t reload = SysTick->LOAD; /* LOAD的值 */
- ticks = nus * g_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调度 */
- }
复制代码这里就正是利用了我们前面提到的时钟摘取法,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,在480M主频下,最大延时,大概是8.9秒。 12.1.4 delay_ms函数该函数是用来延时指定的ms的,其参数nms为要延时的毫秒数。该函数有使用OS和不使用OS两个版本,这里我们分别介绍,首先是不使用OS的时候,实现函数如下: - /**
- * @brief 延时nms
- * @param nms: 要延时的ms数 (0< nms <= 65535)
- * @retval 无
- */
- void delay_ms(uint16_t nms)
- {
- uint32_t repeat = nms / 30; /* 这里用30,是考虑到可能有超频应用, 比如500Mhz
- 的时候, delay_us最大只能延时33554us左右了 */
- uint32_t remain = nms % 30;
- while (repeat)
- {
- delay_us(30 * 1000); /* 利用delay_us 实现 1000ms 延时 */
- repeat--;
- }
- if (remain)
- {
- delay_us(remain * 1000); /* 利用delay_us, 把尾数延时(remain ms)给做了 */
- }
- }
复制代码该函数其实就是多次调用delay_us函数,来实现毫秒级延时的。我们做了一些处理,使得调用delay_us函数的次数减少,这样时间会更加精准。再来看看使用OS的时候,delay_ms的实现函数如下: - /**
- * @brief 延时nms
- * @param nms: 要延时的ms数 (0< nms <= 65535)
- * @retval 无
- */
- void delay_ms(uint16_t nms)
- {
- /* 如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度) */
- if (delay_osrunning && delay_osintnesting == 0)
- {
- if (nms >= g_fac_ms) /* 延时的时间大于OS的最少时间周期 */
- {
- delay_ostimedly(nms / g_fac_ms);/* OS延时 */
- }
- nms %= g_fac_ms; /* OS已经无法提供这么小的延时了,采用普通方式延时 */
- }
- delay_us((uint32_t)(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函数实现(此时任务无法调度)。 12.1.5 HAL库延时函数HAL_Delay 前面我们在7.4.2章节介绍stm32h7xx_hal.c 文件时,已经讲解过Systick实现延时相关函数。实际上,HAL库提供的延时函数,只能实现简单的毫秒级别延时,没有实现us级别延时。我看看HAL库的HAL_Delay函数原定义: - /* HAL库的延时函数,默认延时单位ms */
- __weak void HAL_Delay(uint32_t Delay)
- {
- uint32_t tickstart = HAL_GetTick();
- uint32_t wait = Delay;
- /* Add a freq to guarantee minimum wait */
- if (wait < HAL_MAX_DELAY)
- {
- wait += (uint32_t)(uwTickFreq);
- }
- while ((HAL_GetTick() - tickstart) < wait)
- {
- }
- }
复制代码HAL库实现延时功能非常简单,首先定义了一个32位全局变量uwTick,在Systick中断服务函数SysTick_Handler中通过调用HAL_IncTick实现uwTick值不断增加,也就是每隔1ms增加uwTickFreq,而uwTickFreq默认是1。而HAL_Delay函数在进入函数之后先记录当前uwTick的值,然后不断在循环中读取uwTick当前值,进行减运算,得出的就是延时的毫秒数,整个逻辑非常简单也非常清晰。 但是,HAL库的延时函数有一个局限性,在中断服务函数中使用HAL_Delay会引起混乱(虽然一般禁止在中断中使用延时函数),因为它是通过中断方式实现,而Systick的中断优先级是最低的,所以在中断中运行HAL_Delay会导致延时出现严重误差。所以一般情况下,推荐大家使用ALIENTEK提供的延时函数库。 HAL库的ms级别的延时函数__weak void HAL_Delay(uint32_t Delay);它是弱定义函数,所以用户可以自己重新定义该函数。例如:我们在deley.c文件可以这样重新定义该函数: - /**
- * @brief HAL库延时函数重定义
- * @param Delay 要延时的毫秒数
- * @retval None
- */
- void HAL_Delay(uint32_t Delay)
- {
- delay_ms(Delay);
- }
复制代码
|