加载中...

SYSTEM文件夹介绍

SYSTEM文件夹介绍

SYSTEM 文件夹里面的代码由正点原子提供,是 STM32F4xx 系列的底层核心驱动函数,可以用在 STM32F4xx 系列的各个型号上面,方便大家快速构建自己的工程。本章,我们将向大家介绍这些代码的由来及其功能,也希望大家可以灵活使用 SYSTEM 文件夹提供的函数,来快速构建工程,并实际应用到自己的项目中去。

SYSTEM 文件夹下包含了 delay、sys、usart 等三个文件夹。分别包含了 delay.c、sys.c、usart.c 及其头文件。这 3 个 c 文件提供了系统时钟设置、延时和串口 1 调试功能,任何一款 STM32F4都具备这几个基本外设,所以可以快速地将这些设置应用到任意一款 STM32F4 产品上,通过这些驱动文件实现快速移植和辅助开发的效果。

本章将分为如下几个小节:

  • deley 文件夹代码介绍

  • sys 文件夹代码介绍

  • usart 文件夹代码介绍

一、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 都需要用到。

在介绍这些函数之前,我们先了解一下 delay 延时的编程思想:CM4 内核处理器,内部包含了一个 SysTick 定时器,SysTick 是一个 24 位的向下递减的计数定时器,当计数值减到 0 时,将从 RELOAD 寄存器中自动重装载定时初值,开始新一轮计数。只要不把它在 SysTick 控制及状态寄存器中的使能位清除,就永不停息。SysTick 在《STM32F4xx 参考手册_V4(中文版).pdf》里面介绍的很简单,其详细介绍,请参阅《Cortex-M3 权威指南》第 133 页。我们就是利用 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_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

以上代码,仅支持 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 个函数。

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), 168MHz
 * @retval    无
 */  
void delay_init(uint16_t sysclk)
{
#if SYS_SUPPORT_OS                                      /* 如果需要支持OS */
    uint32_t reload;
#endif
    HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);/* SYSTICK使用外部时钟源,频率为HCLK */
    g_fac_us = sysclk;                                  /* 不论是否使用OS,g_fac_us都需要使用 */
#if SYS_SUPPORT_OS                                      /* 如果需要支持OS. */
    reload = sysclk;                                    /* 每秒钟的计数次数 单位为M */
    reload *= 1000000 / delay_ostickspersec;            /* 根据delay_ostickspersec设定溢出时间,reload为24位
                                                         * 寄存器,最大值:16777216,在168M下,约合0.09986s左右
                                                         */
    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_m4.h 里面),里面包含 CTRL、LOAD、VAL、CALIB 等 4 个寄存器。

SysTick->CTRL 的各位定义如下图所示:

SysTick->LOAD 的定义如下图所示:

SysTick->VAL 的定义如下图所示:

SysTick-> CALIB 不常用,在这里我们也用不到,故不介绍了。

HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);这句代码把 SysTick 的时钟选择为内核时钟,这里需要注意的是:SysTick 的时钟源自 HCLK,假设我们外部晶振为

25MHz,然后倍频到 180MHz,那么 SysTick 的时钟即为 180MHz,也就是 SysTick 的计数器 VAL 每减 1,就代表时间过了 1/180us。

在不使用 OS 的时候:fac_us,为 us 延时的基数,也就是延时 1us,Systick 定时器需要走过的时钟周期数。

当使用 OS 的时候,fac_us,还是 us 延时的基数,不过这个值不会被写到 SysTick->LOAD 寄存器来实现延时,而是通过时钟摘取的办法实现的(前面已经介绍了)。而 fac_ms 则代表 ucos 自带的延时函数所能实现的最小延时时间(如 delay_ostickspersec=200,那么 fac_ms 就是 5ms)。

3、delay_us 函数

该函数用来延时指定的 us,其参数 nus 为要延时的微秒数。该函数有使用 OS 和不使用 OS两个版本,这里我们首先介绍不使用 OS 的时候,实现函数如下:

/**
 * @brief       延时nus
 * @param       nus: 要延时的us数.
 * @note        nus取值范围 : 0~190887435(最大值即 2^32 / fac_us @fac_us = 21)
 * @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 ~ 190887435us(最大值即 2^32 / fac_us @fac_us = 21)
 * @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 的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度!

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 / 540;    /*  这里用540,是考虑到可能有超频应用, 比如248M的时候,delay_us最大只能延时541ms左右了 */
    uint32_t remain = nms % 540;

    while (repeat)
    {
        delay_us(540 * 1000);        /* 利用delay_us 实现 540ms 延时 */
        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)
{
    if (delay_osrunning && delay_osintnesting == 0)     /* 如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度) */
    {
        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 可以实现很长的延时(达到 65 秒)而不溢出!,所以放心的使用 delay_us 来实现 delay_ms,不过由于 delay_us 的时候,任务调度被上锁了,所以还是建议不要用 delay_us 来延时很长的时间,否则影响整个系统的性能。

当 OS 运行的时候,我们的 delay_ms 函数将先判断延时时长是否大于等于 1 个 OS 时钟节拍(g_fac_ms),当大于这个值的时候,我们就通过调用 OS 的延时函数来实现(此时任务可以调度),不足 1 个时钟节拍的时候,直接调用 delay_us 函数实现(此时任务无法调度)。

5、HAL 库延时函数 HAL_Delay

前面我们在 7.4.2 章节介绍 stm32f4xx_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库内部函数用到的延时
 * @note        HAL库的延时默认用Systick,如果我们没有开Systick的中断会导致调用这个延时后无法退出
 * @param       Delay : 要延时的毫秒数
 * @retval      None
 */
void HAL_Delay(uint32_t Delay)
{
     delay_ms(Delay);
}

二、sys 文件夹代码介绍

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

void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset);
/* 设置中断偏移量 */
void sys_standby(void); /* 进入待机模式 */
void sys_soft_reset(void); /* 系统软复位 */
uint8_t sys_stm32_clock_init(uint32_t plln, uint32_t pllm, uint32_t pllp,
uint32_t pllq); /* 配置系统时钟 */

/* 以下为汇编函数 */
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_standby 函数用于进入待机模式
  • sys_soft_reset 函数用于系统软复位
  • sys_stm32_clock_init 函数是系统时钟初始化函数

三、usart 文件夹代码介绍

该文件夹下面有 usart.c 和 usart.h 两个文件。在我们的工程使用串口 1 和串口调试助手来实现调试功能,可以把单片机的信息通过串口助手显示到电脑屏幕。串口相关知识,我们将在串口实验的时候进行详细讲解。

标准库下的 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”即可。

在 keil5 中,不管是否使用半主机模式,使用 printf,scanf,fopen,fread 等都需要自己填充底层函数,以 printf 为例,需要补充定义 fputc,启用微库后,在我们初始化和使能串口 1 之后,我们只需要重新实现 fputc 的功能即可将每个传给 fputc 函数的字符 ch 重定向到串口 1,如果这时接上串口调试助手的话,可以看到串口的数据。实现的代码如下:
/* 重定义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;
}

上面说到了微库的一些限制,使用时注意某些函数与标准库的区别就不会影响到我们代码的正常功能。如果不想使用微库,那就要用到我们提到的第二种方法:取消 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

使用的上面的代码,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;
}
posted @ 2023-01-07 15:49  ·LvXudong·  阅读(917)  评论(0)    收藏  举报