【C语言】如何优雅地进行嵌入式C开发?(万字总结)

相信很多做嵌入式的朋友,做过一些项目后,会有一个瓶颈期,就是明明熟悉了C语言的所有语法,但拿到一个项目不知道如何下手,或者明明知道一个项目该怎么做,去设计代码时,却总会遇到一些逻辑上自相矛盾的地方。这时想要提升自己的代码能力,却又不知道学什么,心想C语言不就那么点东西吗?

如题:什么是好的嵌入式C代码? 其实语言只是一种工具,重要的是思想!在本文中,我将从代码结构和代码逻辑上来阐述。代码结构上将阐述分层设计的要点;逻辑上将从状态机、查表法、信号槽设计模式(23种)面向对象(封装、继承、多态)防御性编程等思想,来解决我们经常遇到的逻辑冲突、减少重复代码、增加代码的观赏性和可读性。

如果当你听过很多大道理,依然写不好C代码,就来读读这篇文章吧!!!

一、照妖镜:分层设计

分层设计是代码的照妖镜,我们通常可以通过它一眼看出一份代码的好坏。

嵌入式程序主要分为:物理层、功能接口层、应用层。
先看一个典型的分层设计模型:

  • PHY(物理层)
    • UART
    • ADC
    • GPIO
    • ……
  • USER(功能接口层)
    • MODBUS
    • TEMP
    • LED
    • ……
  • APP(应用层)
    • CTRL
    • ……

通常一个项目涉及多人开发,每人负责不同层级。层级之间面临着前期接口的不透明和频繁更改,合并代码时难免存在冲突;代码合并后,在底层更改了接口名,必须人为地通知上层去调用,随着接口增多,合并代码的压力也越大。那么我们如何无视这些未知的冲突,进行快乐的编程呢?也就是说我们可以根本不关心对方接口的名字和实现方式,我们只需要在前期约定格式(对于一个函数接口,所谓的格式即:参数和返回值),各自去实现和调用即可。

1、使用 Static 关键字

来将函数变量限制在本文件内,避免命名冲突

2、使用回调函数

①、回调函数的作用
  • 在.c文件之间优雅的传递数据,而不用全局变量共享数据
    拿STM32为例,像外部中断信号、串口、ADC、按键等输入信号,他们的输入时间节点是未知的,我们不想轮询,芯片 模组厂商一般会采用中断来通知应用层,我们只需要在应用层编写回调函数(底层调用了一个没有实现的和回调函数同类 型的处理函数A),然后调用底层的注册函数把回调函数注册到底层(底层的注册函数就是把回调函数指针作为入参,A就可以指向我们在应用层编写的回调函数了),比如我们熟知的串口中断回调函数
  • 封装代码的利器
    在上一条中可以看出:我们不需要告诉底层处理函数的名字,然后让底层调用我们去处理数据;也不需要知道底层函数的名字,然后调用它获取数据。我们只要在应用层编写数据处理回调函数,然后把函数指针注册到底层(因为使用了指针,回调函数的名字可以随便取,但入参和返回值类型的格式要符合要求,否则没法和底层关联),底层数据到来时就会自动处理,这之间没有对底层做任何操作,成功解耦。
②、回调函数的应用

我们在编写驱动时,可以模仿这些芯片厂商,比如编写一个ADC驱动:

ADC.h
#ifndef _APP_ADC_H_
#define _APP_ADC_H_

typedef enum{
   
  ADC1,
  ADC2,
}ADC_ID_TYPEDEF;

typedef void (*adcGatherCallBack)(ADC_ID_TYPEDEF adc, uint32_t value);
void adc_callback_register(adcGatherCallBack adcCB);

#endif

注意:函数指针和注册函数都是在底层编写

ADC.c
static adcGatherCallBack adcGatherCB; 

void adc_callback_register(adcGatherCallBack adcCB)
{
   
    adcGatherCB = adcCB;
}

void adc_task(void)
{
   
    while (1) {
   
        uint32_t adc_reading = 0;
        for (int i = 0; i < NO_OF_SAMPLES; i++) {
   
            adc_reading += adc1_get_raw((adc1_channel_t)channel);
        }
        adc_reading /= NO_OF_SAMPLES;
        uint32_t voltage = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars);

        adc_channel = ADC1;
        adcGatherCB(adc_channel,voltage);//底层当成已有函数使用,在应用层可灵活定义回调函数实现具体操作

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

注意:adcGatherCB(adc_channel,voltage);是一个adcGatherCallBack 类型的指针,不需要去实现,只需要将ADC通道和值传入,待应用层将回调函数注册进来,这个指针就指向回调函数,实现回调函数的实现。

APP.c(应用层)
/*ADC回调函数*/
static void adcCallBack(ADC_ID_TYPEDEF adc_channel,uint32_t value)
{
   
    //电压值处理
    switch(adc_channel)
    {
   
        case ADC1:
            //显示电池电压
            printf("Channel: ADC%d\tVoltage: %dmV\n", adc_channel, value);
        break;

        case ADC2:
            //显示VBUS电压
        break;

        default:
        break;
    }
}

/*注册所有回调*/
static void app_callback_register()
{
   
    adc_callback_register(adcCallBack);
}

/*主逻辑初始化*/
void app_init(void)
{
   
    app_callback_register();
}

二、三板斧:常用机制

学会这三板斧,已经能解决很多常见问题,当大佬看到你代码时,会想:嗯,这小伙子还有点东西,但不多,是个可塑之才。

1、查表法

typedef struct
{
   
    char *help_list;
    uint8 permission;                       //操作权限。0:支持读写    1:只读
    gpio_port gpio_port_idx;
    uint8 pin_index;
    Ifx_P *port;
    uint8 default_state;
}GPIO_LIST;

GPIO_LIST gpio_listor[] =
{
   //   help_list             permission   port_idx   pin     Ifx_P *   default_state
    {
   "MCU_LED_EN(RW)\t",        GPIO_RW,    port13,   0,   &MODULE_P13     ,0       },    //MCU_RUN_LED
    {
   "MCU_CAN0_STB(RW)",        GPIO_RW,    port01,   4,   &MODULE_P01     ,0       },    //CAN0_STB
    {
   "MCU_CAN1_STB(RW)",        GPIO_RW,    port01,   6,   &MODULE_P01     ,0       },    //CAN1_STB
    {
   "MCU_CAN2_STB(RW)",        GPIO_RW,    port01,   7,   &MODULE_P01     ,0       },    //CAN2_STB
    {
   "MCU_CAN3_STB(RW)",        GPIO_RW,    port00,   9,   &MODULE_P00     ,0       },    //CAN3_STB
    {
   "MCU_CAN4_STB(RW)",        GPIO_RW,    port34,   3,   &MODULE_P34     ,0       },    //CAN4_STB
    {
   "MCU_ETH_EN(RW)\t",        GPIO_RW,    port12,   0,   &MODULE_P12     ,0       },    //ETH_PHY_EN
    {
   "MCU_PHY_RST(RW)\t",       GPIO_RW,    port11,   14,  &MODULE_P11     ,0       },    //ETH_PHY_RST
    {
   "J2A_9296_EN1(RW)",        GPIO_RW,    port22,   7,   &MODULE_P22     ,1       },    //CAM_EN1
    {
   "J2A_9296_EN2(RW)",        GPIO_RW,    port22,   8,   &MODULE_P22     ,1       },    //CAM_EN2
    {
   "J2A_9296_EN3(RW)",        GPIO_RW,    port22,   9,   &MODULE_P22     ,1       },    //CAM_EN3
    {
   "CAME_OFF(RW)\t",          GPIO_RW,    port33,   2,   &MODULE_P33     ,0       },    //CAM_POW
};
#define GPIO_MAX_NUM    sizeof(gpio_listor)/sizeof(GPIO_LIST)

void init_gpio(void)
{
   
    for(uint8 i = 0; i < GPIO_MAX_NUM; i++)
    {
   
        if(gpio_listor[i].permission == GPIO_RW)
        {
   
            IfxPort_setPinMode(gpio_listor[i].port, gpio_listor[i].pin_index, IfxPort_Mode_outputPushPullGeneral);
            IfxPort_setPinState(gpio_listor[i].port, gpio_listor[i].pin_index, gpio_listor[i].default_state);
        }
    }
}

2、状态机

不是一看到switch/case就是状态机,要有状态的切换。
假如有如下状态关系:
在这里插入图片描述

代码参考网上:

//定义枚举类型STATE_t表示状态机状态:
typedef enum{
   
	STATE1 = 0,
	STATE2,
	STATE3,
	STATE4,
}STATE_t; 
//定义ACTION_MAP_t结构体类型,表示状态机状态属性:
typedef void (*STATE_ACTION)(void);	
typedef struct ACTION_MAP
{
   
	STATE_t 		stStateID;
	STATE_ACTION 	EnterAct;	
	STATE_ACTION 	RunningAct;	
	STATE_ACTION 	ExitAct;
}ACTION_MAP_t;
//建立“动作”表:
void state1_entry(void)
{
   
	printf("state1_entry\n");
}
void state1_do(void)
{
   
	printf("state1_do\n");
}
void state1_exit(void)
{
   
	printf("state1_exit\n");
}

void state2_entry(void)
{
   
	printf("state2_entry\n");
}
void state2_do(void)
{
   
	printf("state2_do\n");
}
void state2_exit(void)
{
   
	printf("state1_exit\n");
}

void state3_entry(void)
{
   
	printf("state3_entry\n");
}
void state3_do(void)
{
   
	printf("state3_do\n");
}
void state3_exit(void)
{
   
	printf("state3_exit\n");
}

void state4_entry(void)
{
   
	printf("state4_entry\n");
}
void state4_do(void)
{
   
	printf("state4_do\n");
}
void state4_exit(void)
{
   
	printf("state4_exit\n");
}
ACTION_MAP_t actionMap[] = 
{
   
	{
   STATE1,	state1_entry,	state1_do,	state1_exit},
	{
   STATE2,	state2_entry,	state2_do,	state2_exit},
	{
   STATE3,	state3_entry,	state3_do,	state3_exit},
	{
   STATE4,	state4_entry,	state4_do,	state4_exit},
};
//定义枚举类型EVENT_t表示事件:
typedef enum
{
   
	EVENT1 = 0,
	EVENT2,
	EVENT3,
	EVENT4,
	EVENT5,
	
	EVENT_MAP_END
}EVENT_t;
//定义EVENT_MAP_t结构体类型,表示事件表属性:
typedef struct EVENT_MAP
{
   
	EVENT_t	stEventID;
	STATE_t stCurState;
	STATE_t stNextState;
}EVENT_MAP_t;
//根据状态机流程图建立事件表:
EVENT_MAP_t eventMap[] = 
{
   
	{
   EVENT1,	STATE1,	STATE2},
	{
   EVENT2,	STATE2,	STATE3},	
	{
   EVENT3,	STATE3,	STATE4},
	{
   EVENT4,	STATE4,	STATE1},
	{
   EVENT5,	STATE1,	STATE4},

	{
   EVENT_MAP_END,	0,	0},		
};
//定义状态机结构体类型:
typedef struct FSM
{
   
	STATE_t stCurState;
	STATE_t stNextState;
	ACTION_MAP_t *pActionMap;
	EVENT_MAP_t *pEventMap;
}FSM_t;
//定义状态机注册函数:
void fsm_init(FSM_t* pFsm,EVENT_MAP_t* pEventMap,ACTION_MAP_t *pActionMap)
{
   
	pFsm->stCurState = 0;
	pFsm->stNextState = EVENT_MAP_END;
	pFsm->pEventMap = pEventMap;
	pFsm->pActionMap = pActionMap;
}
//定义状态机转换函数:
void fsm_state_transfer(FSM_t* pFsm, EVENT_t stEventID)
{
   
	uint8_t i = 0;
	
	for(i=0; pFsm->pEventMap[i].stEventID<EVENT_MAP_END; i++)
	{
   
		if((stEventID == pFsm->pEventMap[i].stEventID) 
			&& (pFsm->stCurState == pFsm->pEventMap[i].stCurState))
		{
   
			pFsm->stNextState = pFsm->pEventMap[i].stNextState;
			
			return;
		}
	}	
}
//定义动作执行函数:
void action_perfrom(FSM_t* pFsm)
{
   
	if(EVENT_MAP_END != pFsm->stNextState)
	{
   
		pFsm->pActionMap[pFsm->stCurState].ExitAct();
		pFsm->pActionMap[pFsm->stNextState].EnterAct();
		
		pFsm->stCurState = pFsm->stNextState;
		pFsm->stNextState = EVENT_MAP_END;
	}
	else
	{
   
		pFsm->pActionMap[pFsm->stCurState].RunningAct();
	}
}
//测试
int main(void)
{
   
	int i = 0;		
	FSM_t stFsmWeather;	//定义状态机
	
	fsm_init(&stFsmWeather,eventMap,actionMap);	//注册状态机
	
	while(1)
	{
   
		usleep(10);
		printf("i = %d\n",i++);
		
		action_perfrom(&stFsmWeather);
		
		//利用i产生EVENT1~EVENT5
		if(0 == (i%11))
		{
   
			fsm_state_transfer(&stFsmWeather,EVENT1);
		}
		
		if(0 == (i%31))
		{
   
			fsm_state_transfer(&stFsmWeather,EVENT2);
		}
		
		if(0 == (i%74))
		{
   
			fsm_state_transfer(&stFsmWeather,EVENT3);
		}
		
		if(0 == (i%13))
		{
   
			fsm_state_transfer(&stFsmWeather,EVENT4);
		}	
		
		if(0 == (i%19))
		{
   
			fsm_state_transfer(&stFsmWeather,EVENT5);
		}
	}
	
	return 0;
}

3、信号槽

信号槽的概念来自QT,我们可以用C语言模拟信号槽的机制。

代码参考网上:

#ifndef _SIMPLE_SIGNAL_SOLTS_H_
#define _SIMPLE_SIGNAL_SOLTS_H_
#include "string.h"

typedef void (*SIMPLE_SIGNAL)(void *signal, void *pArg);
typedef void (*SIMPLE_SOLTS) (void *pArg);

#define SIMPLE_SOLTS_T(FuncName)   void(FuncName)(void *pArg)

#define SIMPLE_EMIT(signal, arg)  if (signal != NULL)signal(&signal, arg)

#define SIMPLE_SIGNAL_SOLTS_MAX_SOLTS       10      //一个信号最多连接槽的数量
#define SIMPLE_SIGNAL_SOLTS_MAX_SIGNAL      10      //信号最多数量


ErrorStatus SimpleSignalSolts_Connect(SIMPLE_SIGNAL *signal, SIMPLE_SOLTS solts);

#endif
#include "SimpleSignalSolts.h"
#include <string.h>

//信号结构
typedef struct
{
   
    void            *signleID;    //信号的指针的指针,保存信号的指针,根据指针的地址确定是否是唯一
    SIMPLE_SOLTS    soltsTable[SIMPLE_SIGNAL_SOLTS_MAX_SOLTS];
    uint8_t         soltsCount;
}SIMPLE_SIGNAL_T;
//信号表结构
typedef struct
{
   
    SIMPLE_SIGNAL_T signalTable[SIMPLE_SIGNAL_SOLTS_MAX_SIGNAL];
    uint8_t         signalCount;
}SIMPLE_SIGNAL_SOLTS_HANDLE_T;

SIMPLE_SIGNAL_SOLTS_HANDLE_T handle = 
{
   
    .signalCount = 0,
};

static void SimpleSignalSolts_Signal(void *signal, void *pArg)
{
   
    uint8_t i, j;
    SIMPLE_SOLTS solts;
    for (i = 0; i < handle.signalCount; i++) //查找是否是同一个信号
    {
   
        if (handle.signalTable[i].signleID == signal)  //这里注意,signleID保存的是指针的地址,
        {
   
            for (j = 0; j < handle.signalTable[i].soltsCount; j++)
            {
   
                 solts = handle.signalTable[i].soltsTable[j];
                 if (solts != NULL)
                 {
   
                     solts(pArg);
                 }
            }
        }
    }
}

/******************************************************************
  * @函数说明:   连接信号与槽
  * @输入参数:   SIMPLE_SIGNAL *singnal 信号的指针(指针的指针) 
                  SIMPLE_SOLTS solts     槽
  * @输出参数:   无
  * @返回参数:   ErrorStatus 
******************************************************************/
ErrorStatus SimpleSignalSolts_Connect(SIMPLE_SIGNAL *signal, SIMPLE_SOLTS solts)
{
   
    if (signal == NULL || solts == NULL)    //查空
    {
   
        return ERROR;
    }

    uint8_t i;

    if (handle.signalCount > SIMPLE_SIGNAL_SOLTS_MAX_SIGNAL)    //错误
    {
   
        handle.signalCount = 0
  • 13
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

外来务工人员徐某

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值