• 【正点原子STM32连载】第十二章 SYSTEM文件夹介绍 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1


    1)实验平台:正点原子MiniPro H750开发板
    2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560
    3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-336836-1-1.html
    4)对正点原子STM32感兴趣的同学可以加群讨论:879133275

    第十二章 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    前面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计数次数,这里为50480(假设系统时钟为480Mhz,因为systick的频率等于系统时钟频率,那么systick每增加1,就是1/480us),然后我们就一直统计systick的计数变化,直到这个值变化了50480,一旦检测到变化达到或者超过这个值,就说明延时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级延迟)
     * @param     无  
     * @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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    以上代码,仅支持UCOSII和UCOSIII,不过,对于其他OS的支持,也只需要对以上代码进行简单修改即可实现。
    支持OS需要用到的三个宏定义(以UCOSII为例)即:

    #define delay_osrunning   OSRunning             /* OS是否运行标记,0,不运行;1,在运行 */
    #define delay_ostickspersec OS_TICKS_PER_SEC /* OS时钟节拍,即每秒调度次数 */
    #define delay_osintnesting  OSIntNesting      /* 中断嵌套级别,即中断嵌套次数 */
    
    • 1
    • 2
    • 3

    宏定义: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 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    可以看到,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所示:
    在这里插入图片描述

    图12.1.2.1 SysTick->CTRL寄存器各位定义
    SysTick-> LOAD的定义如图12.1.2.2所示:
    在这里插入图片描述

    图12.1.2.2 SysTick->LOAD寄存器各位定义
    SysTick-> VAL的定义如图12.1.2.3所示:
    在这里插入图片描述

    图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数.
     * @note        注意: 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;                /* 时间超过/等于要延迟的时间,则退出 */
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    这里就是利用了我们前面提到的时钟摘取法,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调度 */
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    这里就正是利用了我们前面提到的时钟摘取法,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)给做了 */
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    该函数其实就是多次调用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));      /* 普通方式延时 */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    该函数中,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)
      {
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    12.2 sys文件夹代码介绍

    sys文件夹内包含了sys.c和sys.h两个文件,主要实现下面的几个函数,以及一些汇编函数。

    /* 设置中断向量表偏移量 */
    void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset);
    void sys_cache_enable(void);                     		/* 使能STM32H7的L1-Cahce */
    uint8_t sys_stm32_clock_init(uint32_t plln, uint32_t pllm, uint32_t pllp, 
    uint32_t pllq);      	/* 配置系统时钟 */
    void sys_qspi_enable_memmapmode(uint8_t ftype);    	/* QSPI进入内存映射模式 */
    
    /* 以下为汇编函数 */
    void sys_wfi_set(void);             	/* 执行WFI指令 */
    void sys_intx_disable(void);        	/* 关闭所有中断 */
    void sys_intx_enable(void);         	/* 开启所有中断 */
    void sys_msr_msp(uint32_t addr);   	/* 设置栈顶地址 */
    sys_nvic_set_vector_table函数用于设置中断向量表偏移地址。sys_stm32_clock_ini函数的讲解请参考11.2.1小节STM32H7时钟系统
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    配置章节内容。
    接下来我们重点看一下sys_cache_enable函数和sys_qspi_enable_memmapmode函数。

    12.2.1 Cache使能函数

    STM32H7自带了指令Cache(I Cache)和数据Cache(D Cache),使用I/D Cache可以缓存指令/数据,提高CPU访问指令/数据的速度,从而大大提高MCU的性能。不过,MCU在复位后,I/D Cache默认都是关闭的,为了提高性能,我们需要开启I/D Cache,在sys.c里面,我们提供了如下函数:
    
    • 1
    /**
     * @brief       使能STM32H7的L1-Cache, 同时开启D cache的强制透写
     * @param       无
     * @retval      无
     */
    void sys_cache_enable(void)
    {
        SCB_EnableICache();   /* 使能I-Cache,函数在core_cm7.h里面定义 */
        SCB_EnableDCache();   /* 使能D-Cache,函数在core_cm7.h里面定义 */
        SCB->CACR |= 1 << 2; /* 强制D-Cache透写,如不开启透写,实际使用中可能遇到各种问题 */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    该函数,通过调用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参考资料 文件夹)。注意:F7和H7这部分知识是通用的,所以参考F7的这两份文档即可,H7的这两个资料暂时没出来。
    这里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_CleanInvalidate
    DCache等函数,这对程序员的要求非常高,程序员必须清楚什么时候该回写,什么时候该更新D Cache!如果能力不够,还是建议开启D Cache的透写,以免引起各种莫名其妙的问题。

    12.2.2 QSPI_Enable_Memmapmode函数

    该函数有三个作用:
    1,初始化QSPI接口,并使能内存映射模式;
    2,初始化外部SPI FLASH,使能QPI(QSPI)模式。
    3,设置QSPI FLASH空间的MPU保护。
    其代码如下:

    /**
     * @brief       QSPI进入内存映射模式(执行QSPI代码必备前提)
     *   @note      必须根据所使用QSPI FLASH的容量设置正确的ftype值!
     * @param       ftype: flash类型
     *   @arg           0, 普通FLASH, 容量在128Mbit及以内的
     *   @arg           1, 大容量FLASH, 容量在256Mbit及以上的.
     * @retval      无
     */
    void sys_qspi_enable_memmapmode(uint8_t ftype)
    {
        uint32_t tempreg = 0; 
        GPIO_InitTypeDef qspi_gpio;
    
        __HAL_RCC_GPIOB_CLK_ENABLE();                            /* 使能PORTB时钟 */
        __HAL_RCC_GPIOD_CLK_ENABLE();                            /* 使能PORTD时钟 */
        __HAL_RCC_GPIOE_CLK_ENABLE();                            /* 使能PORTE时钟 */
        __HAL_RCC_QSPI_CLK_ENABLE();                             /* QSPI时钟使能 */
    
        qspi_gpio.Pin = GPIO_PIN_6;                              /* PB6 AF10 */
        qspi_gpio.Mode = GPIO_MODE_AF_PP;
        qspi_gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
        qspi_gpio.Pull = GPIO_PULLUP;
        qspi_gpio.Alternate = GPIO_AF10_QUADSPI;
        HAL_GPIO_Init(GPIOB, &qspi_gpio);
    
        qspi_gpio.Pin = GPIO_PIN_2;                              /* PB2 AF9 */
        qspi_gpio.Alternate = GPIO_AF9_QUADSPI;
        HAL_GPIO_Init(GPIOB, &qspi_gpio);
    
        qspi_gpio.Pin = GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13;/* PD11,12,13 AF9 */
        qspi_gpio.Alternate = GPIO_AF9_QUADSPI;
        HAL_GPIO_Init(GPIOD, &qspi_gpio);
    
        qspi_gpio.Pin = GPIO_PIN_2;                              /* PE2 AF9 */
        qspi_gpio.Alternate = GPIO_AF9_QUADSPI;
        HAL_GPIO_Init(GPIOE, &qspi_gpio);
    
        /* QSPI设置,参考QSPI实验的QSPI_Init函数 */
        RCC->AHB3RSTR |= 1 << 14;        /* 复位QSPI */
        RCC->AHB3RSTR &= ~(1 << 14);    /* 停止复位QSPI */
    
        while (QUADSPI->SR & (1 << 5)); /* 等待BUSY位清零 */
    
    /* QSPI时钟源已经在sys_stm32_clock_init()函数中设置 */
    /* 设置CR寄存器, 这些值怎么来的,请参考QSPI实验/看H750参考手册寄存器描述分析 */
    QUADSPI->CR = 0X01000310;       
    /* 设置DCR寄存器(FLASH容量32M(最大容量设置为32M, 默认用16M的), tSHSL=3个时钟) */
        QUADSPI->DCR = 0X00180201;     
        QUADSPI->CR |= 1 << 0;          /* 使能QSPI */
    
        /*
         *  注意:QSPI QE位的使能,在QSPI烧写算法里面,就已经设置了
         *  所以, 这里可以不用设置QE位,否则需要加入对QE位置1的代码
         *  不过, 代码必须通过仿真器下载, 直接烧录到外部QSPI FLASH, 是不可用的
         *  如果想直接烧录到外部QSPI FLASH也可以用, 则需要在这里添加QE位置1的代码
         *
         *  另外, 对与W25Q256,还需要使能4字节地址模式,或者设置S3的ADP位为1.
         *  我们在QSPI烧写算法里面已经设置了ADP=1(上电即32位地址模式),因此这里也
         *  不需要发送进入4字节地址模式指令/设置ADP=1了, 否则还需要设置ADP=1
         */
    
        /* BY/W25QXX 写使能(0X06指令)*/
        while (QUADSPI->SR & (1 << 5));     	/* 等待BUSY位清零 */
    
        QUADSPI->CCR = 0X00000106;      		/* 发送0X06指令,BY/W25QXX写使能 */
    
        while ((QUADSPI->SR & (1 << 1)) == 0);/* 等待指令发送完成 */
    
        QUADSPI->FCR |= 1 << 1;           	/* 清除发送完成标志位 */
    
        /* MemroyMap 模式设置 */
        while (QUADSPI->SR & (1 << 5)); 	/* 等待BUSY位清零 */
    
        QUADSPI->ABR = 0;  /* 交替字节设置为0,实际上就是25QXX 0XEB指令的, M0~M7 = 0 */
        tempreg = 0XEB; /*INSTRUCTION[7:0]=0XEB, 发送0XEB指令(Fast Read QUAD I/O)*/
        tempreg |= 1 << 8;              	/* IMODE[1:0] = 1, 单线传输指令 */
        tempreg |= 3 << 10;             	/* ADDRESS[1:0] = 3, 四线传输地址 */
    tempreg |= (2 + ftype) << 12; 	/* ADSIZE[1:0] = 2/3, 
    24位(ftype = 0) / 32位(ftype = 1)地址长度 */
        tempreg |= 3 << 14;             	/* ABMODE[1:0] = 3, 四线传输交替字节 */
        tempreg |= 0 << 16;             	/* ABSIZE[1:0] = 0, 8位交替字节(M0~M7) */
        tempreg |= 4 << 18;             	/* DCYC[4:0] = 4, 4个dummy周期 */
        tempreg |= 3 << 24;             	/* DMODE[1:0] = 3, 四线传输数据 */
        tempreg |= 3 << 26;             	/* FMODE[1:0] = 3, 内存映射模式 */
        QUADSPI->CCR = tempreg;        	/* 设置CCR寄存器 */
    
        /* 设置QSPI FLASH空间的MPU保护 */
        SCB->SHCSR &= ~(1 << 16);       	/* 禁止MemManage */
        MPU->CTRL &= ~(1 << 0);         	/* 禁止MPU */
        MPU->RNR = 0;                     	/* 设置保护区域编号为0(1~7可以给其他内存用) */
        MPU->RBAR = 0X90000000;         	/* 基地址为0X9000 000, 即QSPI的起始地址 */
    MPU->RASR = 0X0303002D;         	/* 设置保护参数(禁止共用, 允许cache, 允许缓冲), 
    详见MPU实验解析 */
        MPU->CTRL = (1 << 2) | (1 << 0);  /* 使能PRIVDEFENA, 使能MPU */
        SCB->SHCSR |= 1 << 16;              /* 使能MemManage */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    以上代码,可以分为4个部分解读:
    第一部分,初始化QSPI相关GPIO及QSPI接口。
    第二部分,设置QSPI FLASH进入QSPI模式,并设置相关参数,这里使用的是直接操作寄存器的方式,并没有的调用任何函数,目的就是简化代码。这部分代码的详细理解,读者可以参考我们后续的QSPI实验(实验24 QSPI实验)的相关介绍。这里只需要直接调用就好。
    第三部分,设置STM32H750的QSPI的内存映射模式,从而可以执行外部SPI FLASH的代码。
    第四部分,设置QSPI FLASH空间的内存保护。
    为了简化sys_qspi_enable_memmapmode函数,我们很多操作都做了精简处理,所以理解起来会有一定困难,如果实在看不懂,可以绕过这个,先学习后面的知识点,回头再来学习该函数的具体实现。
    需要注意的是:在sys_qspi_enable_memmapmode函数之前的所有代码,必须不能放到外部QSPI FLASH,因为在该函数之前,STM32H750都是无法访问外部QSPI FLASH的,所以如果遇到一些代码启动不了的情况,请重点怀疑是不是在该函数之前,就调用了QSPI FLASH的程序?如果有,需要将这部分代码放到该函数之后才行!或者将这部分代码放内部FLASH运行(方法见3.2.8节)。
    函数/指令地址,我们可以通过仿真,看反汇编窗口(Disassembly),会有地址显示,从而快速找到问题。

    12.3 usart文件夹代码介绍

    该文件夹下面有usart.c和usarts.h两个文件。在我们的工程使用串口1和串口调试助手来实现调试功能,可以把单片机的信息通过串口助手显示到电脑屏幕。串口相关知识,我们将在第十七章讲解串口实验的时候给大家详细讲解。本节我们只给大家讲解比较独立的printf函数支持相关的知识。
    12.3.1 printf函数支持
    在我们学习C语言时,可以通过printf函数把需要的参数显示到屏幕上,可以做一些简单的调试信息,但对于单片机来说,如果想实现类似的功能来用printf辅助调试的话,是否有办法呢?有,这就是这一节要讲的内容。
    标准库下的printf为调试属性的函数,如果直接使用,会使单片机进入半主机模式(semihosting),这是一种调试模式,直接下载代码后出现程序无法运行,但是在Debug模式下程序可能正常工作的情况。半主机是ARM目标的一种机制,用于将输入/输出请求从应用程序代码通信到运行调试器的主机。例如,此机制可用于允许C库中的函数(如printf()和scanf())使用主机的屏幕和键盘,而不是在目标系统上设置屏幕和键盘。这很有用,因为开发硬件通常不具有最终系统的所有输入和输出设备,如屏幕、键盘等。半主机是通过一组定义好的软件指令(如SVC)SVC指令(以前称为SWI指令)来实现的,这些指令通过程序控制生成异常。应用程序调用相应的半主机调用,然后调试代理处理该异常。调试代理(这里的调试代理是仿真器)提供与主机之间的必需通信。也就是说使用半主机模式必须使用仿真器调试。
    如果想在独立环境下运行调试功能的函数,我们这里是printf ,printf对字符ch处理后写入文件f,最后使用fputc将文件f输出到显示设备。对于PC端的设备,fputc通过复杂的源码,最终把字符显示到屏幕上。那我们需要做的,就是把printf调用的fputc函数重新实现,重定向fputc的输出,同时避免进入半主模式。
    要避免半主机模式,现在主要有两种方式:一是使用MicroLib,即微库;另一种方法是确保ARM应用程序中没有链接MicroLib的半主机相关函数,我们要取消ARM的半主机工作模式,这可以通过代码实现。
    先说微库,ARM的C微库MicroLib是为嵌入式设备开发的一套类似于标准C接口函数的精简代码库,用于替代默认C库,是专门针对专业嵌入式应用开发而设计的,特别适合那些对存储空间有特别要求的嵌入式应用程序,这些程序一般不在操作系统下运行。使用微库编写程序要注意其与默认C库之间存在的一些差异,如main()函数不能声明带参数,也无须返回;不支持stdio,除了无缓冲的stdin、stdout和syderr; 微库不支持操作系统函数;微库不支持可选的单或两区存储模式;微库只提供分离的堆和栈两区存储模式等等,它裁减了很多函数,而且还有很多东西不支持。如果原来用标准库可以跑,选择MicroLib后却突然不行了,是很常见的。与标准的C库不一样,微库重新实现了printf,使用微库的情况下就不会进入半主机模式了。Keil下使用微库的方法很简单,在“Target”下勾选“Use MicroLib”即可。
    在这里插入图片描述

    图12.3.1.1 MDK工程下使用微库的方法
    在keil5中,不管是否使用半主机模式,使用printf,scanf,fopen,fread等都需要自己填充底层函数,以printf为例,需要补充定义fputc,启用微库后,在我们初始化和使能串口1之后,我们只需要重新实现fputc的功能即可将每个传给fputc函数的字符ch重定向到串口1,如果这时接上串口调试助手的话,可以看到串口的数据。实现的代码如下:
    /* 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口

    */
    int fputc(int ch, FILE *f)
    {
        while ((USART_UX->ISR & 0X40) == 0);  	/* 等待上一个字符发送完成 */
    
        USART_UX->TDR = (uint8_t)ch;            	/* 将要发送的字符 ch 写入到DR寄存器 */
        return ch;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面说到了微库的一些限制,使用时注意某些函数与标准库的区别就不会影响到我们代码的正常功能。如果不想使用微库,那就要用到我们提到的第二种方法:取消ARM的半主机工作模式;只需在代码中添加不使用半主机的声明即可,对于AC5和AC6编译器版本,声明半主机的语法不同,为了同时兼容这两种语法,我们在利用编译器自带的宏__ARMCC_VERSION判定编译器版本,并根据版本不同选择不同的语法声明不使用半主机模式,具体代码如下:

    #if (__ARMCC_VERSION >= 6010050)            	/* 使用AC6编译器时 */
    __asm(".global __use_no_semihosting\n\t");	/* 声明不使用半主机模式 */
    __asm(".global __ARM_use_no_argv \n\t");   /* AC6下需要声明main函数为无参数格式,否则部分例程可能出现半主机模式 */
    
    #else
    /* 使用AC5编译器时, 要在这里定义__FILE 和 不使用半主机模式 */
    #pragma import(__use_no_semihosting)
    
    /* 解决HAL库使用时, 某些情况可能报错的bug */
    struct __FILE
    {
        int handle;
        /* Whatever you require here. If the only file you are using is */
        /* standard output using printf() for debugging, no file handling */
        /* is required. */
    };
    
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    使用的上面的代码,Keil的编译器就不会把标准库的这部分函数链接到我们的代码里。如果用到原来半主机模式下的调试函数,需要重新实现它的一些依赖函数接口,对于printf函数需要实现的接口,我们的代码中将它们实现如下:
    /* 不使用半主机模式,至少需要重定义

    _ttywrch\_sys_exit\_sys_command_string函数,以同时兼容AC6和AC5模式 */
    int _ttywrch(int ch)
    {
        ch = ch;
        return ch;
    }
    /* 定义_sys_exit()以避免使用半主机模式 */
    void _sys_exit(int x)
    {
        x = x;
    }
    char *_sys_command_string(char *cmd, int len)
    {
        return NULL;
    }
    /* FILE 在 stdio.h里面定义. */
    FILE __stdout;
    fputc的重定向和之前一样,重定向到串口1即可,如果硬件资源允许,读者有特殊需求,也可以重定向到LCD或者其它串口。
    /* 重定义fputc函数, printf函数最终会通过调用fputc输出字符串到串口 */
    int fputc(int ch, FILE *f)
    {
        while ((USART_UX->SR & 0X40) == 0); /* 等待上一个字符发送完成 */
        USART_UX->DR = (uint8_t)ch;         	/* 将要发送的字符 ch 写入到DR寄存器 */
        return ch;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
  • 相关阅读:
    C语言学习之路(基础篇)—— 数组和字符串 01
    数据结构一
    Redis进阶:主从复制、集群搭建
    LeetCode 61. 旋转链表
    Android 四大组件 -- BroadcastReceiver(广播)
    如何在 PHP 中对密码进行哈希处理
    软考高项-项目评估与决策&招投标
    重学Elasticsearch第9章 : ES集群概念、节点故障恢复问题、路由计算、协调节点、倒排索引
    matlab|计及源荷不确定性的综合能源生产单元运行调度与容量配置随机优化模型
    SpringCloud Gateway--网关服务基本介绍和基本原理
  • 原文地址:https://blog.csdn.net/weixin_55796564/article/details/126465840