给自己的学习总结帖~~ 这里仅都是c语言 嵌入式相关代码第2季啊
一、高效解析不定长度的协议帧
通信设计中考虑协议的灵活性,经常把协议设计成“不定长度”。一个实例如下图:锐米LoRa终端的通信协议帧。
如果一个系统接收上述“不定长度”的协议帧,将会有一个挑战--如何高效接收与解析。 为简化系统设计,我们强烈建议您采用“状态机”来解析UART数据帧,并且把解析工作放在ISR(中断服务程序)完成,仅当接收到最后一个字节(0x0D)时,再将整个数据帧提交给进程处理。该解析状态机的原理如下图所示:
那么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;
}
二、按键的短按、长按检测
在电子产品中经常用到按键,尤其是经常需要MCU判断短按和长按这两种动作,本篇我们来专门聊下这个话题。
只谈理论太无聊,我们还是结合着实际应用来说明。例子默认的功能是蓝牙连接后不断的发送数据,从而不断的拍照。而实际中的遥控器通常是按一次按键,控制一次,我们在来实现该功能。
板子上只有两个按键,一个是RESET按键,一个是DOWNLOAD按键,我们使用DOWNLAOD按键,按键的一端接GND,另外一端接CH573的PB22引脚。
原理图中有一个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 );
}
void GPIOB_IRQHandler( void )
{
if(GPIOB_ReadPortPin(GPIO_Pin_22)==0)
{
GPIOB_ClearITFlagBit( GPIO_Pin_22);
tmos_set_event( hidEmuTaskId, START_REPORT_EVT );
}
}
这么写能工作,但是有问题,就是经常会出现按一下误判为多次按下。原因大家应该都清楚,因为按键存在抖动,所以一次按下有可能进入多次进入中断。
理想中的按下-弹起波形是这样的:
但是实际由于按键抖动的存在,实际的波形可能是这样的:
不信的话你可以接上示波器看看,或者软件验证,比如在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);
}
}
这个方法很简单,但是不好的地方是延时占用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);
}
}
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);
}
它的逻辑是每次抖动的下降沿重新开启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);
}
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);
}
实现的逻辑是:当检测到短按时,再开启一个10ms定时器,在定时器到时之中判断电平状态,如果为低电平,就让cnt变量加1,否则cnt=0,当cnt>100,即低电平持续1s认为是长按。我在这里当判断到长按之后或者IO变高之后会停止掉这个定时器,否则周期定时,因为没必要一直开着定时器。
除了上述的中断方式,还可以使用轮询的方式来实现,代码如下:
void Key_Init()
{
GPIOB_ModeCfg( GPIO_Pin_22, GPIO_ModeIN_PU );
}
if(events & START_KEYSCAN_EVT)
{
KeyScan();
tmos_start_task(hidEmuTaskId, START_KEYSCAN_EVT,160);// 100ms执行一次KeyScan()
return (events ^ START_KEYSCAN_EVT);
}
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;
}
}
if(events & START_LONGCHECK_TIMER)
{
key_long_press_flag =true;
PRINT("long press\n");
return (events ^ START_LONGCHECK_TIMER);
}
上面的这段代码初次看着有点绕,但是看明白了之后会觉得这个实现逻辑还是挺好的,注释写了,这里不再详细解释了,我在多个项目里使用的都是它。它兼顾了去抖和短按/长按的检测,并且长按可以判断出长按按下/长按弹起。短按是检测到弹起时认为是短按动作。另外如果想同时支持多个长按,也很方便添加。
轮询和中断各有优缺点,大家可以根据实际情况来选择,你一般常用哪种方式呢?
三、单片机多任务事件驱动
单片机的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
//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
//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;
}
}
}
}
//test_task.h
#ifndef _TEST_THREAD_H
#define _TEST_THREAD_H
void testInit();
void testLoop();
#endif //
//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);
}
四、嵌入式编程模板
输入事件到状态机
#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)条件A:status_index_e
(2)条件B:event_index_e
(3)switch:
EXECUTE_VOID(select_machine[0][1] );
当一个外部事件来的时候(比如按键输入),通过一个全局的结构体变量(C语言中最常用的方法)引入当前的实时状态,由条件导向各种状态机。
这里的实现是通过二维数组即两个下标代表两个条件,两个条件的交点就是具体的状态机。
状态机到面向过程
以上实现的是“输入外部事件>>>>引流到>>>>状态机”
那如何实现“状态机>>>>执行>>>>具体地操作”呢?状态机有一个固定的执行流程(当然也有根据条件执行不同的运行流程的分支),其实这些个流程都是非常确定的执行过程。
在开发过程中的经验体现:就是对所有执行流程的精确完整的分析,然后将其全部罗列出来。“全部罗列出来”这个执行流程在程序中有两种体现方式:
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); // 状态机:步骤二
... // 状态机:步骤....
}
五、STM32中常用的C语言知识点
C语言是单片机开发中的必备基础知识,本文列举了部分STM32学习中会遇见的C语言基础知识点,希望能对大家有所帮助。
位操作
下面,我们先讲解几种位操作符,然后讲解位操作使用技巧。C语言支持如下6中位操作:
六种位操作
下面,我们想着重讲解位操作在单片机开发中的一些实用技巧。
1.在不改变其他位的值的状况下,对某几个位进行设值
这个场景在单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,然后用 | 操作符设值。
比如我要改变GPIOA的状态,可以先对寄存器的值进行&清零操作:
2. 移位操作提高代码的可读性
移位操作在单片机开发中非常重要,下面是delay_init函数的一行代码:
SysTick->CTRL |= 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语言中的预处理命令,它用于宏定义(定义的是常量),可以提高源代码的可读性,为编程提供方便。常见的格式:
定义标识符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;
Typedef为结构体定义了一个别名GPIO_TypeDef,这样我们可以通过GPIO_TypeDef来定义结构体变量:GPIO_TypeDef gpiox;
这里的GPIO_TypeDef就跟struct _GPIO是等同的作用了,但是GPIO_TypeDef使用起来方便很多。