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

一、高效解析不定长度的协议帧

   通信设计中考虑协议的灵活性,经常把协议设计成“不定长度”。一个实例如下图:锐米LoRa终端的通信协议帧。

c语言-嵌入式专辑2~_#include

 如果一个系统接收上述“不定长度”的协议帧,将会有一个挑战--如何高效接收与解析。    为简化系统设计,我们强烈建议您采用“状态机”来解析UART数据帧,并且把解析工作放在ISR(中断服务程序)完成,仅当接收到最后一个字节(0x0D)时,再将整个数据帧提交给进程处理。该解析状态机的原理如下图所示:

c语言-嵌入式专辑2~_#include_02

 那么ISR处理这个状态机来得及吗?答案是:so easy!因为它只有3个动作,运算量十分小:

    比较接收数据 -> 更新状态变量 -> 存储接收数据,C语言仅3条语句,翻译成机器指令也不超过10条。

    代码清单如下:

/**
* @brief  Status of received communication frame
*/
typedef enum
{
    STATUS_IDLE = (uint8_t)0,
    STATUS_HEAD, /* Rx Head=0x3C */
    STATUS_TYPE, /* Rx Type */
    STATUS_DATA, /* Data filed */
    STATUS_TAIL, /* Tail=0x0D */
    STATUS_END, /* End of this frame */
} COMM_TRM_STATUS_TypeDef;

/**
* @brief  Data object for received communication frame
*/
typedef struct
{
    uint8_t    byCnt; /* Count of 1 field */
    uint8_t    byDataLen; /* Length of data field */
    uint8_t    byFrameLen; /* Length of frame */
    COMM_TRM_STATUS_TypeDef    eRxStatus;
    uint8_t    a_byRxBuf[MAX_LEN_COMM_TRM_DATA]; 
} COMM_TRM_DATA;



/**

* @brief  Data object for received communication frame.
* @note  Prevent race condition that accessed by both ISR and process.
*/
static COMM_TRM_DATA    s_stComm2TrmData;



/**
  * @brief  Put a data that received by UART into buffer.
  * @note  Prevent race condition this called by ISR. 
  * @param  uint8_t byData: the data received by UART.
  * @retval  None
  */
void comm2trm_RxUartData(uint8_t byData)
{
    /* Update status according to the received data */
    switch (s_stComm2TrmData.eRxStatus)
    {
        case STATUS_IDLE:
            if (COMM_TRM_HEAD == byData) /* Is Head */
            {
                s_stComm2TrmData.eRxStatus = STATUS_HEAD;
            }
            else
            {
                goto rx_exception;
            }
            break;
        case STATUS_HEAD:
            if (TYPE_INVALID_MIN < byData && byData < TYPE_INVALID_MAX) /* Valid type */
            {
                s_stComm2TrmData.eRxStatus = STATUS_TYPE;
            }
            else
            {
                goto rx_exception;
            }
            break;
        case STATUS_TYPE:
            if (byData <= MAX_LEN_UART_FRAME_DATA) /* Valid data size */
            {
                s_stComm2TrmData.eRxStatus = STATUS_DATA;
                s_stComm2TrmData.byDataLen = byData;
            }
            else
            {
                goto rx_exception;
            }
            break;
        case STATUS_DATA:
            if (s_stComm2TrmData.byCnt < s_stComm2TrmData.byDataLen)
            {
                ++s_stComm2TrmData.byCnt;
            }
            else
            {
                s_stComm2TrmData.eRxStatus = STATUS_TAIL;
            }
            break;
        case STATUS_TAIL:
            if (COMM_TRM_TAIL == byData)
            {
                /* We received a frame of data, now tell process to deal with it! */
                process_poll(&Comm2TrmProcess);
            }
            else
            {
                goto rx_exception;
            }
            break;
        default:
            ASSERT(!"Error: Bad status of comm2trm_RxUartData().\r\n");
            break;
    }

    /* Save the received data */
    s_stComm2TrmData.a_byRxBuf[s_stComm2TrmData.byFrameLen++] = byData;
    return;

rx_exception:
    ClearCommFrame();
    return; 
}
  • 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.
二、按键的短按、长按检测

在电子产品中经常用到按键,尤其是经常需要MCU判断短按长按这两种动作,本篇我们来专门聊下这个话题。

只谈理论太无聊,我们还是结合着实际应用来说明。例子默认的功能是蓝牙连接后不断的发送数据,从而不断的拍照。而实际中的遥控器通常是按一次按键,控制一次,我们在来实现该功能。

c语言-嵌入式专辑2~_#define_03

板子上只有两个按键,一个是RESET按键,一个是DOWNLOAD按键,我们使用DOWNLAOD按键,按键的一端接GND,另外一端接CH573的PB22引脚。 

c语言-嵌入式专辑2~_#include_04

原理图中有一个NC的C5,但是实际板子上我却没有找到它,可能是版本不一致。

提前说明一下:CH573的代码里跑了TMOS(Task Management Operating System),可以理解为一个简单的操作系统,所以下面的代码一般的裸机代码看着略有不同,不过核心思想都是一样的,用在其他地方也很容易移植,只需要将其中的定时器部分改写即可。

最初我是这么做的,把PB22配置为上拉输入,开启下降沿中断,在中断服务函数里,启动一个事件,执行蓝牙发送。代码如下:

void Key_Init()
{
  GPIOB_ModeCfg( GPIO_Pin_22, GPIO_ModeIN_PU );
  GPIOB_ITModeCfg( GPIO_Pin_22, GPIO_ITMode_FallEdge );
  PFIC_EnableIRQ( GPIO_B_IRQn );
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
void GPIOB_IRQHandler( void )
{
  if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
  {
      GPIOB_ClearITFlagBit( GPIO_Pin_22);
      tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

这么写能工作,但是有问题,就是经常会出现按一下误判为多次按下。原因大家应该都清楚,因为按键存在抖动,所以一次按下有可能进入多次进入中断。

理想中的按下-弹起波形是这样的:

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

但是实际由于按键抖动的存在,实际的波形可能是这样的: 

c语言-嵌入式专辑2~_#include_06

不信的话你可以接上示波器看看,或者软件验证,比如在GPIO中断服务函数里,设置一个全局变量,让它每次进入中断后加1,按按键观察这个变量的值。

那么该如何消除抖动呢?一种方法是硬件消抖,即按键两端并联一个小电容(电容大小由按键的机械特性来决定),另外一种方法是我们今天要重点介绍的软件消抖。

方法一:常用的加延时函数

在中断服务函数中加一个比如10ms的延时函数,延时时间的长短取决于实际所用的按键特性,只要延时时间比抖动时间略大即可。原理很简单,加了延时就避开了抖动的这段时间,在延时之后判断引脚电平,如果为低电平就表示是按下。

void GPIOB_IRQHandler( void )
{
  if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
  {
      mDelaymS(10);
      if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
          tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
      GPIOB_ClearITFlagBit( GPIO_Pin_22);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

这个方法很简单,但是不好的地方是延时占用MCU资源。尤其是这里的BLE应用,在中断服务函数中执行时间长会引起蓝牙连接中断,所以这里不能这么用,我实际测试当按键按快一点就很容易引起蓝牙连接中断。

方法二:加定时器

它的原理和方法一类似,只不过是不在中断服务函数中阻塞等待,而是用一个定时器,代码如下:

void GPIOB_IRQHandler( void )
{
  if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
  {
      GPIOB_ClearITFlagBit( GPIO_Pin_22);

      tmos_stop_task(hidEmuTaskId, START_DEBOUNCE_EVT);
      tmos_start_task(hidEmuTaskId, START_DEBOUNCE_EVT,16);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
if(events & START_DEBOUNCE_EVT)
    {
        if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
        {
            PRINT("short press\n");
            tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
        }

        return (events ^ START_DEBOUNCE_EVT);
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

它的逻辑是每次抖动的下降沿重新开启10ms定时器,在定时器时间到之后判断IO电平状态来判断按键是否按下。

需要注意的是:10ms定时器不是一个周期性的定时器,它是一次性的,即时间到了之后就停止计时了。另外每次进中断后先让定时器重新重头开始计时。如果大家用其他代码实现时要注意这两点。

此方法的好处不像加延时函数那样占用MCU资源。我实际测试这个方法可用,不会引起蓝牙连接中断。

以上介绍了使用中断的方式来判断按键短按,可以看到它判断的依据是按键按下(由高电平变到低电平)这个状态。下面在方法二的基础上我们来实现长按的检测,判断长按的依据是按下后持续的维持一段时间低电平。代码如下:

if(events & START_DEBOUNCE_EVT)
{
    if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
    {
        PRINT("short press\n");
        tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
        tmos_start_task( hidEmuTaskId, START_LONGCHECK_TIMER,16 );
    }

    return (events ^ START_DEBOUNCE_EVT);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
if(events & START_LONGCHECK_TIMER)
    {
        static int cnt=0;
        if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
        {
            cnt++;
            if(cnt>100)
            {
                PRINT("long press\n");
                tmos_stop_task( hidEmuTaskId, START_LONGCHECK_TIMER);
                cnt =0;
            }
            else
                tmos_start_task( hidEmuTaskId, START_LONGCHECK_TIMER,16 );
        }
        else
        {
            cnt=0;
            tmos_stop_task( hidEmuTaskId, START_LONGCHECK_TIMER );
        }

        return (events ^ START_LONGCHECK_TIMER);
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

实现的逻辑是:当检测到短按时,再开启一个10ms定时器,在定时器到时之中判断电平状态,如果为低电平,就让cnt变量加1,否则cnt=0,当cnt>100,即低电平持续1s认为是长按。我在这里当判断到长按之后或者IO变高之后会停止掉这个定时器,否则周期定时,因为没必要一直开着定时器。

除了上述的中断方式,还可以使用轮询的方式来实现,代码如下:

void Key_Init()
{
  GPIOB_ModeCfg( GPIO_Pin_22, GPIO_ModeIN_PU );
}
  • 1.
  • 2.
  • 3.
  • 4.
if(events & START_KEYSCAN_EVT)
{
    KeyScan();
    tmos_start_task(hidEmuTaskId, START_KEYSCAN_EVT,160);// 100ms执行一次KeyScan()
    return (events ^ START_KEYSCAN_EVT);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
bool key_press_flag = false;      // 按下标志
bool key_long_press_flag = false; // 长按标志

void KeyScan()
{
  if(GPIOB_ReadPortPin(GPIO_Pin_22) == 0) // 低电平
  {
    if(key_press_flag == false)
      tmos_start_task( hidEmuTaskId, START_LONGCHECK_TIMER, 1600 ); // 启动1s定时器

    key_press_flag = true;    // 置位按下标志
  }
  else if(key_press_flag == true) // 高电平同时按键被按下过 ,表示是按下后的弹起
  {
      key_press_flag = false; // 清除按下标志

      if(key_long_press_flag == false)// 短按后的弹起
      {
        tmos_stop_task(hidEmuTaskId, START_LONGCHECK_TIMER);
        PRINT("short press\n");
        tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
      }
      else // 长按后的弹起
      {
          key_long_press_flag =false;
      }
  }
  else
  {
    key_press_flag = false;
    key_long_press_flag = false;
  }

}
  • 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.
if(events & START_LONGCHECK_TIMER)
{
    key_long_press_flag =true;
    PRINT("long press\n");
    return (events ^ START_LONGCHECK_TIMER);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

上面的这段代码初次看着有点绕,但是看明白了之后会觉得这个实现逻辑还是挺好的,注释写了,这里不再详细解释了,我在多个项目里使用的都是它。它兼顾了去抖和短按/长按的检测,并且长按可以判断出长按按下/长按弹起。短按是检测到弹起时认为是短按动作。另外如果想同时支持多个长按,也很方便添加。

轮询和中断各有优缺点,大家可以根据实际情况来选择,你一般常用哪种方式呢?

三、单片机多任务事件驱动

单片机的ROM与RAM存贮空间有限,一般没有多线程可用,给复杂的单片机项目带来困扰。

    经过多年的单片机项目实践,借鉴windows消息机制的思想,编写了单片机多任务事件驱动C代码,应用于单片机项目,无论复杂的项目,还是简单的项目,都可以达到优化代码架构的目的。

    经过几轮的精简、优化,现在分享给大家。

    代码分为3个模块:任务列表、事件列表、定时器列表。

    任务列表创建一个全局列表管理任务,通过调用taskCreat()创建事件处理任务,创建成功返回任务ID,任务列表、事件列表与定时器列表通过任务ID关联。

    事件列表创建一个全局循环列表管理事件,调用taskEventIssue()生成一个事件,放到事件循环列表,taskEventLoop()函数放到主线程循环调用,当事件循环列表中有事件时,根据任务ID分发到具体的事件处理任务。

    定时器列表创建一个全局列表管理定时器,taskTimer()建立一个定时器,放到定时器列表执行,当定时时间到,会生成一个定时器事件,放到事件列表,分发到具体的事件处理任务。

//common.h
#ifndef __COMMON_H
#define __COMMON_H

#include "stdio.h"
#include <stdlib.h>
#include <string.h>

typedef short int16_t;
typedef int int32_t;
typedef long long int64_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef unsigned char bool;

#define false 0
#define true 1

#endif // __COMMON_H
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
//task.h
#ifndef _THREAD_H
#define _THREAD_H

#define TASK_MAX 20 // 最多任务数量
#define TASK_EVENT_MAX 100 // 任务队列长度
#define TASK_TIMER_MAX 100 // 定时器最大数量

typedef void (*CBTaskEvent)(int taskID,uint32_t eventID);
typedef struct _TASK_EVENT
{
    int taskID;
    uint32_t eventID;

} TASK_EVENT;

int taskCreat(CBTaskEvent task);
void taskLoop();
void taskEventIssue(int taskID,uint32_t eventID);
void taskEventLoop();

//定时、休眠

typedef struct _TASK_TIMER
{
    bool isValid;
    int taskID;
    uint32_t eventID;
    uint32_t timeMs;
    uint32_t start;

} TASK_TIMER;

void taskTicksInc();
void taskTimer(int taskID,uint32_t eventID,uint32_t time_ms);
void taskTimerLoop();

#endif // _THREAD_H
  • 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.
//task.c
#include "common.h"
#include "task.h"

CBTaskEvent g_taskList[TASK_MAX]={0};

int taskFindEmpty()
{
    static int index = -1;

    for(int i=0; i<TASK_MAX; i++)
    {
        index++;
        index %= TASK_MAX;
        if(g_taskList[index]==NULL)
        {
            return index;
        }
    }

    return -1;
}

int taskCreat(CBTaskEvent task)
{
    int taskID;

    taskID=taskFindEmpty();
    if(taskID == -1)
    {
        printf("error:task list is full!\n");
        return -1;
    }

    g_taskList[taskID] = task;

    printf("creat task<%d>\n",taskID);

    return taskID;
}

void taskDestroy(int taskID)
{
    printf("Destroy task<%d>\n",taskID);

    g_taskList[taskID] = NULL;
}

void taskLoop()
{
    taskEventLoop();
    taskTimerLoop();
}

TASK_EVENT g_taskEventList[TASK_EVENT_MAX];
int g_TKEventWrite=0;
int g_TKEventRead=0;

int tkEventGetSize()
{
    return (g_TKEventWrite + TASK_EVENT_MAX - g_TKEventRead)% TASK_EVENT_MAX;
}

void taskEventIssue(int taskID,uint32_t eventID)
{
    int writePos;

    if(taskID >= TASK_EVENT_MAX || taskID < 0)
    {
        printf("taskEventIssue() error:taskID\n");
        return;
    }

    writePos = (g_TKEventWrite + 1)% TASK_EVENT_MAX;

    if(writePos == g_TKEventRead)
    {
        printf("taskEventIssue() error:task<%d> event list is full!\n",taskID);
        return;
    }

    g_taskEventList[g_TKEventWrite].taskID=taskID;
    g_taskEventList[g_TKEventWrite].eventID=eventID;
    g_TKEventWrite=writePos;

    //printf("add event:%x\n",eventID);
}

void taskEventLoop()
{
    TASK_EVENT event;
    CBTaskEvent task;
    int size;

    size=tkEventGetSize();
    while(size-- >0)
    {
        event=g_taskEventList[g_TKEventRead];
        g_TKEventRead = (g_TKEventRead + 1)% TASK_EVENT_MAX;

        task = g_taskList[event.taskID];
        if(!task)
        {
            printf("taskEventLoop() error:task is NULL\n");
            continue;
        }

        task(event.taskID,event.eventID);
    }
}

// 定时、休眠

uint32_t g_taskTicks=0;

uint32_t getTaskTicks()
{
    return g_taskTicks;
}

void taskTicksInc() // 1ms时间基准
{
    g_taskTicks++;
}

uint32_t taskTickDiff(uint32_t now,uint32_t last)
{
    uint64_t diff;
    diff = now + 0x100000000 - last;

    return (diff & 0xffffffff);
}

TASK_TIMER g_taskTimerList[TASK_TIMER_MAX]={0};

int taskTimerFindEmpty()
{
    for(int i=0; i<TASK_TIMER_MAX; i++)
    {
        if(!g_taskTimerList[i].isValid)
        {
            return i;
        }
    }

    return -1;
}

void taskTimer(int taskID,uint32_t eventID,uint32_t time_ms)
{
    int index;

    index=taskTimerFindEmpty();
    if(index==-1)
    {
        printf("taskTimer() error:timer list is full\n");
        return;
    }

    g_taskTimerList[index].taskID=taskID;
    g_taskTimerList[index].eventID=eventID;
    g_taskTimerList[index].timeMs=time_ms;
    g_taskTimerList[index].start=getTaskTicks();
    g_taskTimerList[index].isValid=true;

    printf("add timer:<%d,%x> %ums\n",taskID,eventID,time_ms);

}

void taskTimerLoop()
{
    static uint32_t start=0;
    if(taskTickDiff(getTaskTicks(),start)<3)
    {
        return;
    }

    start=getTaskTicks();

    for(int i=0; i<TASK_TIMER_MAX; i++)
    {
        if(g_taskTimerList[i].isValid)
        {
            if(taskTickDiff(start,g_taskTimerList[i].start)>=g_taskTimerList[i].timeMs)
            {
                taskEventIssue(g_taskTimerList[i].taskID,g_taskTimerList[i].eventID);
                g_taskTimerList[i].isValid=false;
            }
        }
    }
}
  • 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.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
//test_task.h
#ifndef _TEST_THREAD_H
#define _TEST_THREAD_H

void testInit();
void testLoop();

#endif //
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
//test_task.c
#include "common.h"
#include "task.h"

#define CTRL_EVENT1 0x01
#define CTRL_EVENT2 0x02
#define CTRL_EVENT3 0x04

void eventProcess(int taskID,uint32_t event)
{
    switch(event)
    {
        case CTRL_EVENT1:
            printf("task[%d] CTRL_EVENT1\n",taskID);
            //taskEventIssue(taskID,CTRL_EVENT2);
            taskTimer(taskID,CTRL_EVENT2,1000);
            break;

        case CTRL_EVENT2:
            printf("task[%d] CTRL_EVENT2\n",taskID);
            //taskEventIssue(taskID,CTRL_EVENT3);
            taskTimer(taskID,CTRL_EVENT3,2000);
            break;

        case CTRL_EVENT3:
            printf("task[%d] CTRL_EVENT3\n",taskID);
            taskTimer(taskID,CTRL_EVENT1,4000);
            break;

        default:
            break;
    }
}

void testLoop()
{
    taskLoop();
}

void testInit()
{
    int taskID1,taskID2;

    printf("testInit()\n");

    taskID1 = taskCreat((CBTaskEvent)&eventProcess);

    taskTimer(taskID1,CTRL_EVENT1,5000);

    taskID2 = taskCreat((CBTaskEvent)&eventProcess);
    taskEventIssue(taskID2,CTRL_EVENT2);
    taskDestroy(taskID1);
    taskDestroy(taskID2);
    //taskEventIssue(taskID1,CTRL_EVENT1);
    taskID1 = taskCreat((CBTaskEvent)&eventProcess);
    taskEventIssue(taskID1,CTRL_EVENT1);
}
  • 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.
四、嵌入式编程模板

输入事件到状态机

#include "stdio.h"
#define EXECUTE_VOID(func)  {if((func)!=NULL) (func());}

typedef void (*select_machine_t)(void);

typedef enum _event_index
{
 event_index_1 = 0,
 event_index_2,
 event_index_3,
 event_index_end
} event_index_e; 

typedef enum _status_index
{
 status_index_1 = 0,
 status_index_2,
 status_index_end
} status_index_e;

void machine_1(void);
void machine_2(void);
void machine_3(void);
void machine_4(void);

select_machine_t select_machine[event_index_end][status_index_end] = 
{
 {machine_1, machine_2},
 {NULL,      machine_3},
 {machine_4, NULL}
};

void machine_1(void)
{
 printf("machine_1\r\n");
}

void machine_2(void)
{
 printf("machine_2\r\n");
}

void machine_3(void)
{
 printf("machine_3\r\n");
}

void machine_4(void)
{
 printf("machine_4\r\n");
}

int main(void)
{
 EXECUTE_VOID(select_machine[0][1]);
}
  • 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.

    对应:

(1)条件A:status_index_e

(2)条件B:event_index_e

(3)switch:

EXECUTE_VOID(select_machine[0][1] );
  • 1.

    当一个外部事件来的时候(比如按键输入),通过一个全局的结构体变量(C语言中最常用的方法)引入当前的实时状态,由条件导向各种状态机。

    这里的实现是通过二维数组即两个下标代表两个条件,两个条件的交点就是具体的状态机。

c语言-嵌入式专辑2~_#define_07

状态机到面向过程

    以上实现的是“输入外部事件>>>>引流到>>>>状态机”

    那如何实现“状态机>>>>执行>>>>具体地操作”呢?状态机有一个固定的执行流程(当然也有根据条件执行不同的运行流程的分支),其实这些个流程都是非常确定的执行过程。

    在开发过程中的经验体现:就是对所有执行流程的精确完整的分析,然后将其全部罗列出来。“全部罗列出来”这个执行流程在程序中有两种体现方式:

1、把所有的执行流程以“空函数”的形式罗列出来。

2、把所有的执行流程以“函数指针”的形式罗列出来:

    好处一:

    可以把软件框架写出来,具体逻辑流程已经做好。

    好处二:

    具体的函数的接口可以先空着(NULL),待写好了函数就把函数名赋值给它(sys_api_func* = you_func ;)。

    好处三:

    通用性更高,逻辑性更强。

void (sys_api_func1)(void);
void (sys_api_func2)(void);
void (sys_api_func3)(void);
...

void sys_api_init(void)
{
 sys_api_func1 = NULL;   // 还没有写好实现函数就先赋为NULL
 sys_api_func2 = NULL;
 sys_api_func3 = NULL;
 ...
}

// 状态机1
void machine_1(void)
{
 execute_api_void(sys_api_func1);  // 状态机:步骤一
 execute_api_void(sys_api_func2);  // 状态机:步骤二
 ...                               // 状态机:步骤....
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
五、STM32中常用的C语言知识点

C语言是单片机开发中的必备基础知识,本文列举了部分STM32学习中会遇见的C语言基础知识点,希望能对大家有所帮助。

 位操作 

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

c语言-嵌入式专辑2~_#define_08

六种位操作

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

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

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

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

c语言-嵌入式专辑2~_#include_09

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

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

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

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

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

SysTick->CTRL |= 0X0002;

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

3. 按位取反操作使用技巧

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

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

该代码可以解读为 仅设置CTRL寄存器的第0位(最低位)为0,其他位的值保持不变。
同样我们也不使用按位取反,将代码写成:

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

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

4. 按位异或操作使用技巧

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

GPIOB->ODR ^= 1 << 5;

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

 define宏定义 

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

c语言-嵌入式专辑2~_c语言_10

定义标识符HSE_VALUE的值为8000000,数字后的U表示unsigned的意思。

至于define宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。

 ifdef条件编译 

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

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

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

它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。其中#else部分也可以没有,即:

#ifdef 程序段1 #endif

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

#if !defined (HSE_VALUE) #define HSE_VALUE 24000000U #endif

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

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

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

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

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

extern uint16_t g_usart_rx_sta;

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

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

uint16_t g_usart_rx_sta;

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

 typedef类型别名 

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

struct _GPIO { __IO uint32_t CRL; __IO uint32_t CRH; };

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

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

但这样很繁琐,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;

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