【正点原子STM32连载】 第十二章 SYSTEM文件夹介绍 摘自【正点原子】STM32F103 战舰开发指南V1.2

news2025/1/10 14:10:04

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html

第十二章 SYSTEM文件夹介绍

SYSTEM文件夹里面的代码由正点原子提供,是STM32F1xx系列的底层核心驱动函数,可以用在STM32F1xx系列的各个型号上面,方便大家快速构建自己的工程。本章,我们将向大家介绍这些代码的由来及其功能,也希望大家可以灵活使用SYSTEM文件夹提供的函数,来快速构建工程,并实际应用到自己的项目中去。
SYSTEM文件夹下包含了delay、sys、usart等三个文件夹。分别包含了delay.c、sys.c、usart.c及其头文件。这3个c文件提供了系统时钟设置、延时和串口1调试功能,任何一款STM32F1都具备这几个基本外设,所以可以快速地将这些设置应用到任意一款STM32F1产品上,通过这些驱动文件实现快速移植和辅助开发的效果。
本章将分为如下几个小节:
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都需要用到。
在介绍这些函数之前,我们先了解一下delay延时的编程思想:CM3内核处理器,内部包含了一个SysTick定时器,SysTick是一个24位的向下递减的计数定时器,当计数值减到0时,将从RELOAD寄存器中自动重装载定时初值,开始新一轮计数。只要不把它在SysTick控制及状态寄存器中的使能位清除,就永不停息。SysTick在《STM32F10xxx参考手册_V10(中文版).pdf》里面介绍的很简单,其详细介绍,请参阅《Cortex-M3权威指南》第133页。我们就是利用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计数次数,这里为5072(假设系统时钟为72Mhz,在经过8分频之后,systick的频率等于1/8系统时钟频率,那么systick每增加1,就是1/9us),然后我们就一直统计systick的计数变化,直到这个值变化了509,一旦检测到变化达到或者超过这个值,就说明延时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      无
 */
static 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      无
 */
static 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      无
 */
static 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)
{
    if (delay_osrunning == 1) 	/* OS开始跑了,才执行正常的调度处理 */
    {
        OSIntEnter();           	/* 进入中断 */
        OSTimeTick();           	/* 调用ucos的时钟服务程序 */
        OSIntExit();            	/* 触发任务切换软中断 */
    }
    HAL_IncTick();
}

#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个重要参数:g_fac_us以及g_fac_ms;同时把SysTick的时钟源选择为外部时钟,如果需要支持操作系统(OS),只需要在sys.h里面,设置SYS_SUPPORT_OS宏的值为1即可,然后,该函数会根据delay_ostickspersec宏的设置,来配置SysTick的中断时间,并开启SysTick中断。具体代码如下:

/**
 * @brief       初始化延迟函数
 * @param       sysclk: 系统时钟频率, 即CPU频率(HCLK)
 * @retval      无
 */
void delay_init(uint16_t sysclk)
{
#if SYS_SUPPORT_OS		/* 如果需要支持OS. */
    uint32_t reload;
#endif
SysTick->CTRL = 0;	/*清Systick状态,以便下一步重设,如果这里开了中断会关闭其中断*/

/* SYSTICK使用内核时钟源8分频,因systick的计数器最大值只有2^24 /
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);
g_fac_us = sysclk / 8; /
不论是否使用OS,g_fac_us都需要使用,作为1us的基础时基 */

#if SYS_SUPPORT_OS                            /* 如果需要支持OS. */
    reload = sysclk / 8;                      /* 每秒钟的计数次数 单位为M */
    reload *= 1000000/delay_ostickspersec;/* 根据delay_ostickspersec设定溢出时间*/
    g_fac_ms = 1000 / delay_ostickspersec;/* 代表OS可以延时的最少单位 */
    SysTick->CTRL |= 1 << 1;                 /* 开启SYSTICK中断 */
    SysTick->LOAD = reload;                  /* 每1/delay_ostickspersec秒中断一次 */
    SysTick->CTRL |= 1 << 0;                 /* 开启SYSTICK */
#endif
}

可以看到,delay_init函数使用了条件编译,来选择不同的初始化过程,如果不使用OS的时候,只是设置一下SysTick的时钟源以及确定g_fac_us值。而如果使用OS的时候,则会进行一些不同的配置,这里的条件编译是根据SYS_SUPPORT_OS这个宏来确定的,该宏在sys.h里面定义。
SysTick是MDK定义了的一个结构体(在core_m3.h里面),里面包含CTRL、LOAD、VAL、CALIB等4个寄存器,
SysTick->CTRL(地址:0xE000_E010)的各位定义如图12.1.2.1所示:
在这里插入图片描述

图12.1.2.1 SysTick->CTRL寄存器各位定义
SysTick->LOAD(地址:0xE000_E014)的定义如图12.1.2.2所示,这里要注意这个的最大可重装值只有24位:
在这里插入图片描述

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

图12.1.2.3 SysTick->VAL寄存器各位定义
SysTick->CALIB(地址:0xE000_E01C)不常用,在这里我们也用不到,故不介绍了。
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);这句代码把SysTick的时钟设置为内核时钟的1/8,这里需要注意的是:SysTick的时钟源自HCLK,假设配置系统时钟为72MHZ,经过分频器8分频后,那么SysTick的时钟即为9Mhz,也就是SysTick的计数器VAL每减1,就代表时间过了1/9us。
在不使用OS的时候:fac_us,为us延时的基数,也就是延时1us,Systick定时器需要走过的时钟周期数。
当使用OS的时候,fac_us还是us延时的基数,不过这个值不会被写到SysTick->LOAD寄存器来实现延时,而是通过时钟摘取的办法实现的(前面已经介绍了)。而g_fac_ms则代表ucos自带的延时函数所能实现的最小延时时间(如delay_ostickspersec=200,那么g_fac_ms就是5ms)。
12.1.3 delay_us函数
该函数用来延时指定的us,其参数nus为要延时的微秒数。该函数有使用OS和不使用OS两个版本,这里我们首先介绍不使用OS的时候,实现函数如下:

/**
 * @brief      延时nus
 * @param      nus: 要延时的us数.
 * @note       注意: nus的值,不要大于1864135us(最大值即2^24/g_fac_us @g_fac_us = 9)
 * @retval     无
 */
void delay_us(uint32_t nus)
{
    uint32_t temp;
    SysTick->LOAD = nus * g_fac_us;	/* 时间加载 */
    SysTick->VAL = 0x00;              /* 清空计数器 */
    SysTick->CTRL |= 1 << 0 ;		/* 开始倒数 */
    do
    {
        temp = SysTick->CTRL;
} while ((temp & 0x01) && 
!(temp & (1 << 16)));	/* CTRL.ENABLE位必须为1, 并等待时间到达 */
    SysTick->CTRL &= ~(1 << 0) ;	/* 关闭SYSTICK */
    SysTick->VAL = 0X00;              /* 清空计数器 */
}

不使用OS的delay_us函数,首先结合需要延时的时间nus与us延时的基数,得到SysTick计数次数,赋值给SysTick重装载数值寄存器。对计数器进行清空操作SysTick->VAL=0x00。然后对SysTick->CTRL寄存器的位0赋1操作,即使能SysTick定时器。当重装载寄存器的值递减到0的时候,清除SysTick->CTRL寄存器COUNTFLAG标志这个特性作为判断的条件,等待计数值变为零,即计时时间到了。
对于使用OS的时候,delay_us的实现函数是使用的时钟摘取法,只不过使用delay_osschedlock和delay_osschedunlock两个函数,用于调度上锁和解锁,这是为了防止OS在delay_us的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度。
再来看看使用OS的时候,delay_us的实现函数如下:

/**
 * @brief       延时nus
 * @param       nus: 要延时的us数.
 * @note        nus取值范围: 0 ~ 477218588(最大值即2^32 / g_fac_us @g_fac_us = 9)
 * @retval      无
 */
void delay_us(uint32_t nus)
{
    uint32_t ticks;
    uint32_t told, tnow, tcnt = 0;
    uint32_t reload;
    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的时候打断延时,可能导致的延时不准,所以我们利用这两个函数来实现免打断,从而保证延时精度!
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)
{
/*这里用1000,是考虑到可能有超频应用,如128Mhz时,delay_us最大只能延时1048576us左右*/
    uint32_t repeat = nms / 1000;
    uint32_t remain = nms % 1000;
    while (repeat)
    {
        delay_us(1000 * 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可以实现很长的延时(达到53秒)而不溢出!所以放心的使用delay_us来实现delay_ms,不过由于delay_us的时候,任务调度被上锁了,所以还是建议不要用delay_us来延时很长的时间,否则影响整个系统的性能。
当OS运行的时候,我们的delay_ms函数将先判断延时时长是否大于等于1个OS时钟节拍(g_fac_ms),当大于这个值的时候,我们就通过调用OS的延时函数来实现(此时任务可以调度),不足1个时钟节拍的时候,直接调用delay_us函数实现(此时任务无法调度)。
12.1.5 HAL库延时函数HAL_Delay
前面我们在7.4.2章节介绍STM32F1xx_hal.c文件时,已经讲解过Systick实现延时相关函数。实际上,HAL库提供的延时函数,只能实现简单的毫秒级别延时,没有实现us级别延时。我们看看HAL库的HAL_Delay函数原定义:

__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会导致延时出现严重误差。所以教程中推荐大家使用正点原子提供的延时函数库,但我们在第七章介绍HAL库时,也提到过,不使用操作系统(OS)的情况下,我们禁用了Systick中断,会导致部分HAL库函数无法超时退出,读者需要特别留意。
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);
}

12.2 sys文件夹代码介绍
sys文件夹内包含了sys.c和sys.h两个文件,在sys.c主要实现下面的几个函数,以及一些汇编函数。
/* 函数声明 */

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);

/* 汇编函数 */
void sys_wfi_set(void);
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函数是系统时钟初始化函数,在11.2.1小节STM32F1时钟系统配置章节已经有说明了,大家可以复习这部分知识点。
在sys.h文件中只是对于sys.c的函数进行声明。
12.3 usart文件夹代码介绍
该文件夹下面有usart.c和usart.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 LED工程下使用微库的方法
在Keil5中,不管是否使用半主机模式,使用printf,scanf,fopen,fread等都需要自己填充底层函数,以printf为例,需要补充定义fputc,启用微库后,在我们初始化和使能串口1之后,我们只需要重新实现fputc的功能即可将每个传给fputc函数的字符ch重定向到串口1,如果这时接上串口调试助手的话,可以看到串口的数据。实现的代码如下:

#define USART_UX             USART1
/* 重定义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;
};
#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;
}

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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/548039.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

文件夹路径保存不同,什么批量修改名称

在日常工作中不知道大家有没有遇到过&#xff0c;需要批量修改文件夹名称&#xff0c;并且文件夹保存路径不同呢&#xff0c;像这种情况到底不能批量修改呢。我也问了很多身边的朋友&#xff0c;他们有的说&#xff0c;他一般都修改保存路径是同一个&#xff0c;还很少遇到像我…

【C++ STL】 趣学stackqueuepriority_queue【对话情景版】

文章目录 &#x1f4cd;前言C STL 之 stack&queue基础知识及其模拟实现&#x1f4cd;容器适配器&#x1f388;什么是适配器&#xff1f;&#x1f388;STL标准库中stack和queue的底层结构&#x1f388;deque的简单介绍(了解)&#x1f4cc;deque的原理介绍&#x1f4cc;deque…

强化学习:基本概念

以 grid-world 为例&#xff0c;进行强化学习基础概念的介绍。如图&#xff0c;机械人处于一个网格世界中&#xff0c;不同的网格代表不同的功能&#xff0c;白色代表机械人可以自由的进入&#xff0c;黄色代表陷阱&#xff08;机械人一旦进入就会被强制返回起点&#xff09;&a…

《Reinforcement Learning: An Introduction》第1章笔记

文章目录 1.1 强化学习1.2 强化学习的例子1.3 强化学习的要素1.4 局限和范围1.5 拓展例子&#xff1a;井字游戏1.6 总结1.7 强化学习的早期历史参考资料 1.1 强化学习 强化学习是学习做什么—如何将情景映射到动作—以便最大化数字奖励信号。学习者不会被告知该采取什么动作&a…

MySQL基础(三十九)MySQL常用命令

1 MySQL常用命令 1.1 mysql 该mysql不是指mysql服务&#xff0c;而是指mysql的客户端工具。 语法 &#xff1a; mysql [options] [database]1.1. 连接选项 #参数 &#xff1a; -u, --username 指定用户名 -p, --password[name] 指定密码 -h, --hostname 指定服务器IP或域名…

计算机组成原理实验报告二-认识汇编语言

实验资料&#xff1a; https://wwpv.lanzoue.com/b05drqjef 密码:d19t 使用txt文档编写下面C源码&#xff0c;文档命名为【学号_hello.c】并使用Mingw工具&#xff08;是 Minimalist GNU for Windows的缩写&#xff09;的bin文件夹下gcc.exe带选项编译&#xff08;&#xff09…

JUC之线程池的标准创建方式

文章目录 JUC之线程池的标准创建方式核心和最大线程数量空闲时长(keepAliveTime)线程工厂(ThreadFactory)任务阻塞队列线程池的拒绝策略线程池的任务调度流程 JUC之线程池的标准创建方式 ​ 因为使用Executors快捷创建线程池在使用时会有严重的潜在问题&#xff0c;因此在实战…

数据结构——队列的实现(细就完事了)

1.队列 1.1队列的概念和结构 今天我们要是实现的队列是完全相反的&#xff0c;队列是数据先进先出。而在栈中我们使用的顺序表(数组)来实现的。而队列却用单链表来实现是更加合适的。 队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行数据操作的特殊线…

【王道·计算机网络】第六章 应用层【未完】

一、基本概念 1.1 应用层概述 应用层对应用程序的通信提供服务应用层协议定义&#xff1a; 应用进程交换的报文类型&#xff0c;请求还是响应?各种报文类型的语法&#xff0c;如报文中的各个字段及其详细描述字段的语义&#xff0c;即包含在字段中的信息的含义进程何时、如何…

这是关于“树先生“的故事

《数据结构专栏》 文章目录 《数据结构专栏》一、认识树结构如何遍历树如何创建一个树&#xff1f;如何判断一颗树是否是完全二叉树&#xff1f; 二、树的简单算法——递归1.相同树2.镜像树3.单值二叉树 总结 一、认识树结构 树的定义&#xff1a;树是指由N&#xff08;N>0…

高效研发团队都在看!一套方法论带你找到适合自己的效能提升路径

近日&#xff0c;ONES 受邀参加 2023 QECon 全球软件质量&效能大会&#xff08;深圳站&#xff09;。在会上&#xff0c;ONES 研发效能改进咨询顾问陈仪&#xff0c;发表了主题为《如何为研发团队打造专属的效能提升路径》的演讲。 陈仪有着丰富的咨询经验&#xff0c;曾带…

Netty核心技术二--BIO编程

1. I/O模型 I/O 模型简单的理解&#xff1a;就是用什么样的通道进行数据的发送和接收&#xff0c;很大程度上决定了程序通信的性能 Java共支持3种网络编程模型/IO模式&#xff1a;BIO、NIO、AIO Java BIO &#xff1a;同步并阻塞(传统阻塞型)&#xff0c;服务器实现模式为一个…

C++每日一练:饿龙咆哮-逃离城堡(避坑指南)非负整数求和

文章目录 前言一、题目二、解题代码及思路1、思路2、代码 三、非负整数求和总结 前言 饿龙这一题要说难度嘛&#xff0c;还真是挺简单的&#xff0c;但要满分也是有坑的&#xff01;本文就记录了笔者解题过程&#xff0c;希望能对读者使用C编程有所启发。至于非负整数求和代码…

RocketMQ集群环境部署

文章目录 1. 准备环境2. 修改主机名3. 免密登录配置4. 配置RocketMQ集群5. 搭建RocketMQ集群6. 启动集群 1. 准备环境 准备好三台虚拟机&#xff0c;下面是我的虚拟机的一些基本信息 名称ip地址worker010.117.33.135worker110.117.39.202worker210.117.9.52 三台虚拟机都已经…

Windows下nginx的配置与启动

一&#xff0c;下载 http://nginx.org/&#xff0c;打开官网&#xff0c;点击download 选择下载稳定版 二&#xff0c;解压 1&#xff0c;解压到硬盘某个目录 2&#xff0c;由于80端口被占用&#xff0c;于是我要修改conf目录下的nginx.conf文件 查看端口是否被占用 net…

路径规划算法:基于蝴蝶算法的路径规划算法- 附代码

路径规划算法&#xff1a;基于蝴蝶优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于蝴蝶优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法蝴蝶…

【C++】类和对象(上):带你速度了解什么是类,如何定义类!!

前言&#xff1a; 前面我们学习C一些基础的内容&#xff0c;也可以说C针对C语言的缺陷进行改进。而今天我们要学的是C的内容也就是类和对象。 一、初识类&#xff1a; 我们先来看看C语言解决一个问题的过程&#xff1a; 假设有以下这个场景&#xff1a;你需要手洗一件衣服&am…

算法Day09 | KMP,28. 实现 strStr() ,459.重复的子字符串

Day09 KMP28. 实现 strStr()459.重复的子字符串 KMP KMP是三个人人名缩写&#xff0c;用于在文本字符串text中搜索pattern字符串&#xff0c;返回在text中第一出现的位置。 算法做法就是在暴力匹配的基础上加速匹配。通过对pattern字符串求next数组(该数组也成为前缀表)&#…

element ui 表格内嵌图片预览展示样式问题 (element plus)

❤️砥砺前行&#xff0c;不负余光&#xff0c;永远在路上❤️ 目录 前言一、问题二、解决 前言 一、问题 二、解决 添加上preview-teleported 属性即可。

ChatGPT ✖️ 前端 = 有点er意思

HOT! HOT! HOT! &#x1f525; &#x1f525; &#x1f525; ChatGPT登上了国内各大平台的热搜榜&#xff0c;应该在去年11月末的时候就有不少同学了解并使用过&#xff0c;那个时候它刚刚问世&#xff0c;在互联网圈子里有了很大的热度&#xff0c;但是对于大众来说&#xff…