给自己的学习总结帖~~ 这里仅都是c语言 嵌入式相关代码第4季啊

一、状态机框架

Zorb Framework是一个基于面向对象的思想来搭建一个轻量级的嵌入式框架。

本次分享的是Zorb Framework的状态机的实现。

中小型嵌入式程序说白了就是由各种状态机组成,因此掌握了如何构建状态机,开发嵌入式应用程序可以说是手到拈来。

简单的状态机可以用Switch-Case实现,但复杂一点的状态机再继续使用Switch-Case的话,层次会变得比较乱,不方便维护。因此我们为Zorb Framework提供了函数式状态机。

状态机的功能

我们先来看看要实现的状态机提供什么功能:

初步要提供的功能如下:

1、可以设置初始状态

2、可以进行状态转换

3、可以进行信号调度

4、最好可以在进入和离开状态的时候可以做一些自定义的事情

5、最好可以有子状态机

因此,初步设计的数据结构如下:

/* 状态机结构 */
struct _Fsm
{
    uint8_t Level;                  /* 嵌套层数,根状态机层数为1,子状态机层数自增 */
                                    /* 注:严禁递归嵌套和环形嵌套 */
    List *ChildList;                /* 子状态机列表 */
    Fsm *Owner;                     /* 父状态机 */
    IFsmState OwnerTriggerState;    /* 当父状态机为设定状态时,才触发当前状态机 */
                                    /* 若不设定,则当执行完父状态机,立即运行子状态机 */
    IFsmState CurrentState;         /* 当前状态 */
    bool IsRunning;                 /* 是否正在运行(默认关) */

    /* 设置初始状态 */
    void (*SetInitialState)(Fsm * const pFsm, IFsmState initialState);

    /* 运行当前状态机 */
    bool (*Run)(Fsm * const pFsm);

    /* 运行当前状态机和子状态机 */
    bool (*RunAll)(Fsm * const pFsm);

    /* 停止当前状态机 */
    bool (*Stop)(Fsm * const pFsm);

    /* 停止当前状态机和子状态机 */
    bool (*StopAll)(Fsm * const pFsm);

    /* 释放当前状态机 */
    bool (*Dispose)(Fsm * const pFsm);

    /* 释放当前状态机和子状态机 */
    bool (*DisposeAll)(Fsm * const pFsm);

    /* 添加子状态机 */
    bool (*AddChild)(Fsm * const pFsm, Fsm * const pChildFsm);

    /* 移除子状态机(不释放空间) */
    bool (*RemoveChild)(Fsm * const pFsm, Fsm * const pChildFsm);

    /* 调度状态机 */
    bool (*Dispatch)(Fsm * const pFsm, FsmSignal const signal);

    /* 状态转移 */
    void (*Transfer)(Fsm * const pFsm, IFsmState nextState);

    /* 状态转移(触发转出和转入事件) */
    void (*TransferWithEvent)(Fsm * const pFsm, IFsmState nextState);
};
  • 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.

关于信号,Zorb Framework做了以下定义:

/* 状态机信号0-31保留,用户信号在32以后定义 */
enum {
    FSM_NULL_SIG = 0,
    FSM_ENTER_SIG,
    FSM_EXIT_SIG,
    FSM_USER_SIG_START = 32
    /* 用户信号请在用户文件定义,不允许在此定义 */
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

创建状态机:

bool Fsm_create(Fsm ** ppFsm)
{
    Fsm *pFsm;
    
    ZF_ASSERT(ppFsm != (Fsm **)0)
    
    /* 分配空间 */
    pFsm = ZF_MALLOC(sizeof(Fsm));
    if (pFsm == NULL)
    {
        ZF_DEBUG(LOG_E, "malloc fsm space error\r\n");
        return false;
    }
    
    /* 初始化成员 */
    pFsm->Level = 1;
    pFsm->ChildList = NULL;
    pFsm->Owner = NULL;
    pFsm->OwnerTriggerState = NULL;
    pFsm->CurrentState = NULL;
    pFsm->IsRunning = false;
    
    /* 初始化方法 */
    pFsm->SetInitialState = Fsm_setInitialState;
    pFsm->Run = Fsm_run;
    pFsm->RunAll = Fsm_runAll;
    pFsm->Stop = Fsm_stop;
    pFsm->StopAll = Fsm_stopAll;
    pFsm->Dispose = Fsm_dispose;
    pFsm->DisposeAll = Fsm_disposeAll;
    pFsm->AddChild = Fsm_addChild;
    pFsm->RemoveChild = Fsm_removeChild;
    pFsm->Dispatch = Fsm_dispatch;
    pFsm->Transfer = Fsm_transfer;
    pFsm->TransferWithEvent = Fsm_transferWithEvent;
    
    /* 输出 */
    *ppFsm = pFsm;
    
    return true;
}
  • 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.

调度状态机:

/******************************************************************************
 * 描述  :调度状态机
 * 参数  :(in)-pFsm           状态机指针
 *         (in)-signal         调度信号
 * 返回  :-true               成功
 *         -false              失败
******************************************************************************/
bool Fsm_dispatch(Fsm * const pFsm, FsmSignal const signal)
{
    /* 返回结果 */
    bool res = false;
    
    ZF_ASSERT(pFsm != (Fsm *)0)
    
    if (pFsm->IsRunning)
    {
        if (pFsm->ChildList != NULL && pFsm->ChildList->Count > 0)
        {
            uint32_t i;
            Fsm * pChildFsm;
            
            for (i = 0; i < pFsm->ChildList->Count; i++)
            {
                pChildFsm = (Fsm *)pFsm->ChildList
                    ->GetElementDataAt(pFsm->ChildList, i);
                
                if (pChildFsm != NULL)
                {
                    Fsm_dispatch(pChildFsm, signal);
                }
            }
        }
        
        if (pFsm->CurrentState != NULL)
        {
            /* 1:根状态机时调度
               2:没设置触发状态时调度
               3:正在触发状态时调度
             */
            if (pFsm->Owner == NULL || pFsm->OwnerTriggerState == NULL
                || pFsm->OwnerTriggerState == pFsm->Owner->CurrentState)
            {
                pFsm->CurrentState(pFsm, signal);
                
                res = true;
            }
        }
    }
    
    return res;
}
  • 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.

篇幅有限,其它接口实现可阅读:

 https://github.com/54zorb/Zorb-Framework

状态机测试

/**
  *****************************************************************************
  * @file    app_fsm.c
  * @author  Zorb
  * @version V1.0.0
  * @date    2018-06-28
  * @brief   状态机测试的实现
  *****************************************************************************
  * @history
  *
  * 1. Date:2018-06-28
  *    Author:Zorb
  *    Modification:建立文件
  *
  *****************************************************************************
  */

#include "app_fsm.h"
#include "zf_includes.h"

/* 定义用户信号 */
enum Signal
{
    SAY_HELLO = FSM_USER_SIG_START
};

Fsm *pFsm;        /* 父状态机 */
Fsm *pFsmSon;     /* 子状态机 */

/* 父状态机状态1 */
static void State1(Fsm * const pFsm, FsmSignal const fsmSignal);
/* 父状态机状态2 */
static void State2(Fsm * const pFsm, FsmSignal const fsmSignal);

/******************************************************************************
 * 描述  :父状态机状态1
 * 参数  :-pFsm       当前状态机
 *         -fsmSignal  当前调度信号
 * 返回  :无
******************************************************************************/
static void State1(Fsm * const pFsm, FsmSignal const fsmSignal)
{
    switch(fsmSignal)
    {
        case FSM_ENTER_SIG:
            ZF_DEBUG(LOG_D, "enter state1\r\n");
            break;

        case FSM_EXIT_SIG:
            ZF_DEBUG(LOG_D, "exit state1\r\n\r\n");
            break;

        case SAY_HELLO:
            ZF_DEBUG(LOG_D, "state1 say hello, and want to be state2\r\n");
            /* 切换到状态2 */
            pFsm->TransferWithEvent(pFsm, State2);
            break;
    }
}

/******************************************************************************
 * 描述  :父状态机状态2
 * 参数  :-pFsm       当前状态机
 *         -fsmSignal  当前调度信号
 * 返回  :无
******************************************************************************/
static void State2(Fsm * const pFsm, FsmSignal const fsmSignal)
{
    switch(fsmSignal)
    {
        case FSM_ENTER_SIG:
            ZF_DEBUG(LOG_D, "enter state2\r\n");
            break;

        case FSM_EXIT_SIG:
            ZF_DEBUG(LOG_D, "exit state2\r\n\r\n");
            break;

        case SAY_HELLO:
            ZF_DEBUG(LOG_D, "state2 say hello, and want to be state1\r\n");
            /* 切换到状态1 */
            pFsm->TransferWithEvent(pFsm, State1);
            break;
    }
}

/******************************************************************************
 * 描述  :子状态机状态
 * 参数  :-pFsm       当前状态机
 *         -fsmSignal  当前调度信号
 * 返回  :无
******************************************************************************/
static void SonState(Fsm * const pFsm, FsmSignal const fsmSignal)
{
    switch(fsmSignal)
    {
        case SAY_HELLO:
            ZF_DEBUG(LOG_D, "son say hello only in state2\r\n");
            break;
    }
}

/******************************************************************************
 * 描述  :任务初始化
 * 参数  :无
 * 返回  :无
******************************************************************************/
void App_Fsm_init(void)
{
    /* 创建父状态机,并设初始状态 */
    Fsm_create(&pFsm);
    pFsm->SetInitialState(pFsm, State1);

    /* 创建子状态机,并设初始状态 */
    Fsm_create(&pFsmSon);
    pFsmSon->SetInitialState(pFsmSon, SonState);

    /* 设置子状态机仅在父状态State2触发 */
    pFsmSon->OwnerTriggerState = State2;

    /* 把子状态机添加到父状态机 */
    pFsm->AddChild(pFsm, pFsmSon);

    /* 运行状态机 */
    pFsm->RunAll(pFsm);
}

/******************************************************************************
 * 描述  :任务程序
 * 参数  :无
 * 返回  :无
******************************************************************************/
void App_Fsm_process(void)
{
    ZF_DELAY_MS(1000);
    /* 每1000ms调度状态机,发送SAY_HELLO信号 */
    pFsm->Dispatch(pFsm, SAY_HELLO);
}

/******************************** END OF FILE ********************************/
  • 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.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.

结果:

c语言-嵌入式专辑4~_堆栈

二、STM32单片机的堆栈

   学习STM32单片机的时候,总是能遇到“堆栈”这个概念。分享本文,希望对你理解堆栈有帮助。

    对于了解一点汇编编程的人,就可以知道,堆栈是内存中一段连续的存储区域,用来保存一些临时数据。堆栈操作由PUSH、POP两条指令来完成。而程序内存可以分为几个区:

  • 栈区(stack)
  • 堆区(Heap)
  • 全局区(static)
  • 文字常亮区程序代码区

    程序编译之后,全局变量,静态变量已经分配好内存空间,在函数运行时,程序需要为局部变量分配栈空间,当中断来时,也需要将函数指针入栈,保护现场,以便于中断处理完之后再回到之前执行的函数。

    栈是从高到低分配,堆是从低到高分配。

普通单片机与STM32单片机中堆栈的区别
    普通单片机启动时,不需要用bootloader将代码从ROM搬移到RAM。

    但是STM32单片机需要,这里我们可以先看看单片机程序执行的过程,单片机执行分三个步骤:

  • 取指令
  • 分析指令
  • 执行指令

    根据PC的值从程序存储器读出指令,送到指令寄存器。然后分析执行执行。这样单片机就从内部程序存储器去代码指令,从RAM存取相关数据。

    RAM取数的速度是远高于ROM的,但是普通单片机因为本身运行频率不高,所以从ROM取指令慢并不影响。

    而STM32的CPU运行的频率高,远大于从ROM读写的速度。所以需要用bootloader将代码从ROM搬移到RAM

    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

    其实堆栈就是单片机中的一些存储单元,这些存储单元被指定保存一些特殊信息,比如地址(保护断点)和数据(保护现场)。

    如果非要给他加几个特点的话那就是:

  • 这些存储单元中的内容都是程序执行过程中被中断打断时,事故现场的一些相关参数。如果不保存这些参数,单片机执行完中断函数后就无法回到主程序继续执行了。
  • 这些存储单元的地址被记在了一个叫做堆栈指针(SP)的地方。

结合STM32的开发讲述堆栈

    从上面的描述可以看得出来,在代码中是如何占用堆和栈的。可能很多人还是无法理解,这里再结合STM32的开发过程中与堆栈相关的内容来进行讲述。

    如何设置STM32的堆栈大小?

    在基于MDK的启动文件开始,有一段汇编代码是分配堆栈大小的。

c语言-嵌入式专辑4~_单片机_02

    这里重点知道堆栈数值大小就行。还有一段AREA(区域),表示分配一段堆栈数据段。数值大小可以自己修改,也可以使用STM32CubeMX数值大小配置,如下图所示。

c语言-嵌入式专辑4~_状态机_03

STM32F1默认设置值0x400,也就是1K大小。

Stack_Size EQU 0x400
  • 1.

    函数体内局部变量:

void Fun(void){ char i; int Tmp[256]; //...}
  • 1.

    局部变量总共占用了256*4 + 1字节的栈空间。所以,在函数内有较多局部变量时,就需要注意是否超过我们配置的堆栈大小。

    函数参数:

void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
  • 1.

    这里要强调一点:传递指针只占4字节,如果传递的是结构体,就会占用结构大小空间。提示:在函数嵌套,递归时,系统仍会占用栈空间。

    堆(Heap)的默认设置0x200(512)字节。

Heap_Size EQU 0x200
  • 1.

    大部分人应该很少使用malloc来分配堆空间。虽然堆上的数据只要程序员不释放空间就可以一直访问,但是,如果忘记了释放堆内存,那么将会造成内存泄漏,甚至致命的潜在错误。

MDK中RAM占用大小分析

    经常在线调试的人,可能会分析一些底层的内容。这里结合MDK-ARM来分析一下RAM占用大小的问题。在MDK编译之后,会有一段RAM大小信息:

c语言-嵌入式专辑4~_状态机_04

    这里4+6=1640,转换成16进制就是0x668,在进行在调试时,会出现:

c语言-嵌入式专辑4~_c语言_05

    这个MSP就是主堆栈指针,一般我们复位之后指向的位置,复位指向的其实是栈顶:

c语言-嵌入式专辑4~_状态机_06

    而MSP指向地址0x20000668是0x20000000偏移0x668而得来。具体哪些地方占用了RAM,可以参看map文件中【Image Symbol Table】处的内容:

c语言-嵌入式专辑4~_c语言_07

三、C语言中的枚举类型enum

举例说明C语言中enum枚举关键字的用法。

用来同时定义多个常量

利用enum定义月份的例子如下。

#include<stdio.h>
enum week {Mon=1,Tue,Wed,Thu,Fri,Sat,Sun};
int main()
{
    printf("%d",Tue);
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这样定义Mon的值为1之后,Tue的值就被默认定义为2,Wed的值为3,依此类推。如果没写Mon=1的话,Mon的默认值就为0。例如:

enum color {red,blue,green,yellow}; //red的值默认为0
  • 1.

从中间开始赋值的情况,见如下例子:

enum color {red,blue,green=5,yellow}; 
//red、bule、green、yellow的值依次为0、1、5、6
  • 1.
  • 2.

用来限定变量的取值范围

有时为了保证程序的健壮性而使用enum。

#include<stdio.h>
enum Month {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec};
int main()
{
    enum Month a =  Feb;
    printf("%d",a);
    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

比如上面例子,枚举类型a的取值被限定在那12个变量中。

enum类型的定义方法

在定义enum的同时声明变量:

enum Month {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec} a,b;
//这样就声明了两个枚举类型a和b
  • 1.
  • 2.

定义完enum之后再声明变量:

enum Month {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec};
enum Month a =  Feb;
  • 1.
  • 2.

定义匿名的枚举变量:

enum  {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec} a;
//这样就只能使用a这一个枚举类型的变量,不能再定义其他枚举类型了
  • 1.
  • 2.
四、volatile用法

许多程序员都无法正确理解C语言关键字volatile,这并不奇怪。因为大多数C语言书籍通常都是一两句一带而过,本文将告诉你如何正确使用它。

    在C/C++嵌入式代码中,你是否经历过以下情况:

  • 代码执行正常–直到你打开了编译器优化
  • 代码执行正常–直到打开了中断
  • 古怪的硬件驱动
  • RTOS的任务独立运行正常–直到生成了其他任务

    如果你的回答是“yes”,很有可能你没有使用C语言关键字volatile。你并不是唯一的,很多程序员都不能正确使用volatile。不幸的是,大多数c语言书籍对volatile的藐视,只是简单地一带而过。

    volatile用于声明变量时的使用的限定符。它告诉编译器该变量值可能随时发生变化,且这种变化并不是代码引起的。给编译器这个暗示是很重要的。在开始前,我们向来看一看volatile的语法。

C语言关键字volatile语法

    声明一个变量为volatile,可以在数据类型之前或之后加上关键字volatile。下面的语句,把foo声明一个volatile的整型。

volatile int foo;
int volatile foo;
  • 1.
  • 2.

    把指针指向的变量声明为volatile很常见,尤其是I/O寄存器的地址映射。下面的语句,把pReg声明为一个指向8-bit无符号指针,指针指向的内容为volatile。

volatile uint8_t * pReg;
uint8_t volatile * pReg;
  • 1.
  • 2.

    volatile的指针指向非volatile的变量很少见(我只使用过一次),但我还是给出相应的语法。

int * volatile p;
  • 1.

    顺便提一下,关于为什么要在数据类型前使用volatile关键字,请自行百度搜素。

    最后,如果你再struct或者union前使用volatile关键字,表明struct或者union的所有内容都是volatile。如果这不是你的本意,可以在struct或者union成员上使用volatile关键字。

正确使用C语言关键字volatile

    只要变量可能被意外的修改,就需要把该变量声明为volatile。在实际应用中,只有三种类型数据可能被修改:

  • 外设寄存器地址映射
  • 在中断服务程序中修改全局变量
  • 在多线程、多任务应用中,全局变量被多个任务读写

    接下来,我们将分别讨论上述三种情况。

外设寄存器

    嵌入式系统包含真正的硬件,通常会有复杂的外设。这些外设寄存器的值可能被异步的修改。举个简单的例子,我们要把一个8-bit状态寄存器的地址映射到0x1234。在程序中循环查看该状态寄存器的值是否变为非0。C语言操作寄存器的手法

    下面是最容易想到,但错误的实现方法:

c语言-嵌入式专辑4~_状态机_08

当你打开编译器优化时,程序总是执行失败。因为编译器会生成下面的汇编代码:

c语言-嵌入式专辑4~_堆栈_09

 程序被优化的原因很简单,既然已经把变量的值读入累加器,就没有必要重新一遍,编译器认为值是不会变化的。就这样,在第三行,程序进入了无限死循环。为了告诉编译器我们的真正意图,我们需要修改函数的声明:

c语言-嵌入式专辑4~_单片机_10

编译器生成的汇编代码:

c语言-嵌入式专辑4~_状态机_11

    像这样,我们得到了正确的动作。

中断服务程序

    在中断服务程序中,经常会修改一些全局变量值,来作为主程序中的判断条件。例如,在串口中断服务程序中,可能会检测是否接收到了ETX(假如是消息的结束标识符)字符。如果接收到了ETX,ISR设置一个全局标志位。

    错误的做法: 

c语言-嵌入式专辑4~_堆栈_12

在关闭编译器优化的情况下,程序可能执行正常。然而,任何像样点而优化都会“break”这段程序。问题是编译器并不知道etx_rcvd可能被ISR中被修改。编译器只知道,表达式!ext_rcvd始终为真,你讲用于无法退出循环。结果,循环后面的代码可能被编译器优化掉。

    幸运的话,你的编译器可能会发出警告;不幸的话,(或者你不认真的查看编译器警告),你的程序无法正常执行。当然,你可以责怪编译器执行了“糟糕的优化”。

    解决方式是,将变量etx_rcvd声明为volatile,所有问题(当然,也可能是部分问题)就消失了。

多线程应用

    在实时系统中,尽管有想queues,pipes等这些同步机制,使用全局变量实现两个任务共享信息的做法依然很常见。即使在你的程序中加入了抢占式调度器,你的编译器依然无法知道什么是上下文切换,或何时发生上下文切换。因此从概念上讲,多任务修改全局变量的的做法与中断服务程序中修改全局变量的做法是相同的。

    因此,所有这类全局变量都应该声明为volatile。

    例如下面的程序:

c语言-嵌入式专辑4~_状态机_13

当打开编译器优化时,这段程序可能执行失败。解决方法是将cntr声明为volatile。

总结 

    一些编译器允许你把所有的变量隐式的声明为volatile。请抵制这种诱惑,因为它会令你不再思考,当然也会导致生成低效的代码。

    另外,也不要责怪优化器或直接把它关掉。现代的优化器已经足够优秀,我已经记不清上次遇到优化bug是什么时候了。相反,我常常看到程序员们错误的使用volatile。

    如果你被要求去修改一个很古怪的代码,请在程序中查找一下volatile关键字;如果你什么也没有找到,上面讨论的例子可以向你提供一些解决问题的思路。

五、STM32单片机的C语言基础

C语言是单片机开发中的必备基础知识,本文列举了部分STM32学习中比较常见的一些C语言基础知识。

1 位操作

    下面我们先讲解几种位操作符,然后讲解位操作使用技巧。C语言支持以下六种位操作:

c语言-嵌入式专辑4~_单片机_14

 下面,重点讲解一下位操作在单片机开发中的一些实用技巧。

1.1 在不改变其他位的值的状况下,对某几个位进行设值

    这个场景在单片机开发中经常使用,方法就是我们先对需要设置的位用&操作符进行清零操作,然后用 | 操作符设值。

    比如,我要改变GPIOA的状态,可以先对寄存器的值进行&清零操作:

c语言-嵌入式专辑4~_堆栈_15

1.2 移位操作提高代码的可读性

    移位操作在单片机开发中非常重要,下面是delay_init函数的一行代码:

SysTick->CTRL |= 1 << 1;
  • 1.

    这个操作就是将CTRL寄存器的第1位(从0开始算起)设置为1,为什么要通过左移而不是直接设置一个固定的值呢?

    其实这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第1位设置为1。如果写成:

SysTick->CTRL |= 0X0002;
  • 1.

    这个虽然也能实现同样的效果,但是可读性稍差,而且修改也比较麻烦。

1.3 ~按位取反操作使用技巧

    按位取反在设置寄存器的时候经常被使用,常用于清除某一个/某几个位。下面是delay_us函数的一行代码:

SysTick->CTRL &= ~(1 << 0) ;    /* 关闭SYSTICK */
  • 1.

    该代码可以解读为:仅设置CTRL寄存器的第0位(最低位)为0,其他位的值保持不变。

    同样我们也不使用按位取反,将代码写成:

SysTick->CTRL &= 0XFFFFFFFE;        /* 关闭SYSTICK */
  • 1.

    可见,前者的可读性及可维护性都要比后者好很多。

1.4 ^按位异或操作使用技巧

    该功能非常适合用于控制某个位翻转,常见的应用场景就是控制LED闪烁,如下:

GPIOB->ODR ^= 1 << 5;
  • 1.

    执行一次该代码,就会使PB5的输出状态翻转一次,如果我们的LED接在PB5上,就可以看到LED闪烁了。

2 define宏定义

    define是C语言中的预处理命令,它用于宏定义(定义的是常量),可以提高源代码的可读性,为编程提供方便。常见的格式:

c语言-嵌入式专辑4~_单片机_16

   定义标识符HSE_VALUE的值为8000000,数字后的U表示unsigned的意思。至于define宏定义的其他一些知识,比如宏定义带参数,这里就不多讲解了。

3 ifdef条件编译

    单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

    条件编译命令最常见的形式为:

#ifdef 标识符    程序段1#else    程序段2#endif
  • 1.

    它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。

    其中#else部分也可以没有,即:

#ifdef    程序段1    #endif
  • 1.

    条件编译在HAL库里面是用得很多,在stm32mp1xx_hal_conf.h这个头文件中经常会看到这样的语句:

#if !defined  (HSE_VALUE)      #define HSE_VALUE       24000000U    #endif
  • 1.

    如果没有定义HSE_VALUE这个宏,则定义HSE_VALUE宏,并且HSE_VALUE的值为24000000U。条件编译也是C语言的基础知识吧。

    这里提一下,24000000U中的U表示无符号整型,常见的,UL表示无符号长整型,F表示浮点型。

    这里加了U以后,系统编译时就不进行类型检查,直接以U的形式把值赋给某个对应的内存,如果超出定义变量的范围,则截取。

4 extern变量申明

    C语言中extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

    这里面要注意,对于extern申明变量可以多次,但定义只有一次。在我们的代码中你会看到看到这样的语句:

extern uint16_t g_usart_rx_sta;
  • 1.

    这个语句是申明g_usart_rx_sta变量在其他文件中已经定义了,在这里要使用到。

    所以,你肯定可以找到在某个地方有变量定义的语句:

uint16_t g_usart_rx_sta;
  • 1.

    extern的使用比较简单,但是也会经常用到,需要掌握。

5 typedef类型别名

    typedef用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。typedef在HAL库用得最多的就是定义结构体的类型别名和枚举类型了。

struct _GPIO    {        __IO uint32_t CRL;        __IO uint32_t CRH;        …    };
  • 1.

    定义了一个结构体GPIO,这样我们定义结构体变量的方式为:

struct  _GPIO  gpiox;       /* 定义结构体变量gpiox */
  • 1.

    但这样很繁琐,HAL库中有很多这样的结构体变量需要定义。

    这里我们可以为结体定义一个别名GPIO_TypeDef,这样我们就可以在其他地方通过别名GPIO_TypeDef来定义结构体变量了,方法如下:

typedef struct    {            __IO uint32_t CRL;            __IO uint32_t CRH;            …    } GPIO_TypeDef;
  • 1.

    Typedef为结构体定义一个别名GPIO_TypeDef,这样我们可以通过GPIO_TypeDef来定义结构体变量:

GPIO_TypeDef gpiox;
  • 1.

    这里的GPIO_TypeDef就跟struct _GPIO是等同的作用了,但是GPIO_TypeDef使用起来方便很多。

六、C语言数制与大小端转换

  编程时,经常用到进制转换、字符转换。比如软件界面输入的数字字符串,如何将字符串处理成数字呢?今天就和大家分享一下。

u8、u32转换

    举个例子:ASCII码里: 

c语言-嵌入式专辑4~_堆栈_17

    这里写图片描述字符‘A’ , 一个字节8bit 。

    即u8 十六进制为:

0x41
  • 1.

    二进制为:

0100 0001
  • 1.

    而对应的十进制为65整型65,4个字节32bit。

    即u32 十六进制为:

0x41
  • 1.

    二进制为:

0000 0000 0000 0000 0000 0000 0100 0001
  • 1.

    将u32数转换成u8数组。

    注意:这里是字符数组,不是字符串。

    字符串是以空字符(\0)结尾的char数组

void U32ToU8Array(uint8_t *buf, uint32_t u32Value)
{
    buf[0] = ((u32Value >> 24) & 0xFF);
    buf[1] = ((u32Value >> 16) & 0xFF);
    buf[2] = ((u32Value >> 8) & 0xFF);
    buf[3] = (u32Value & 0xFF);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

    效果:整型 50 转字符数组 {‘\0’,’\0’,’\0’,’2’}

    u8数组转u32:

void U8ArrayToU32(uint8_t *buf, uint32_t *u32Value)
{
    *u32Value = (buf[0] <<24) + (buf[1] <<16) + (buf[2] <<8) + (buf[3] <<0);
}
  • 1.
  • 2.
  • 3.
  • 4.

    效果:字符数组 {‘\0’,’\0’,’\0’,’2’}转为整型 50

大小端(高低位)转换

    STM32 默认是小端模式的,那么该如何转为大端?

//转为大端:
pPack[0] = (u8)((len >> 8) & 0xFF);
pPack[1] = (u8)(len & 0xFF);
  • 1.
  • 2.
  • 3.
//转为小端:
pPack[0] = (u8)(len & 0xFF);
pPack[1] =  (u8)((len >> 8) & 0xFF);
  • 1.
  • 2.
  • 3.

    效果:len为数据类型为 u16(short),

    比如 0x11 0x22,转为u8(usigned char)数组。

大端为:

pPack[0] (0x11 ) 
pPack[1] (0x22)
  • 1.
  • 2.

小端为:

pPack[0] (0x22) 
pPack[1] (0x11)
  • 1.
  • 2.