嵌入式程序设计思路

项目做的多了,深切地体会到架构的重要性。

俗话说,没有好的架构,移植和复用是件很痛苦的事,只能重复的造轮子。特别是嵌入式的代码,如果应用层中间穿插着驱动层的代码,维护起来是一件相当痛苦的事情。

这篇文章就总结一下自己的代码设计思路。

整体结构框图:

说明:该图是针对于MCU+RTOS框架的应用的。

1、驱动层

驱动层代码的首要原则就是隔离硬件库代码

次要原则就是把“变量”做成宏定义

最好的借鉴就是芯片HAL库或操作系统源码

比如1个STM32或GD32的MCU程序,无论是采用标准库或是HAL,亦或者第三方库(usb协议栈、文件系统管理、UI显示等模块),都应该自己再单独封装出来一层API函数。

然后保证上层只能调用驱动API,不越级调用底层驱动

对于如何封装自己的驱动API,有这么几个思路:

首先,最好的借鉴代码就是芯片原厂驱动库,代码久经考验,完全可以按照他们的规范去写。

1.1代码要紧凑,并且充分考虑扩展性

相似的功能代码,不要搞复制粘贴,做好传参;传参和返回值设计最好在一开始就考虑好扩展性。

1.2驱动层的代码始终由上层调用

驱动层只负责写好接口,驱动初始化或功能开关由上层调用和初始化

1.3把代码移植的时候的需要修改的参数做成宏定义

功能代码固定,移植的时候只需要修改头文件的宏定义;

一般需要修改的如:GPIO引脚号、串口号、打印、延时等等。

当然也需要明白的是,驱动层代码可以通过宏定义隔离RTOS、日志等模块,但无法摆脱硬件库的依赖,比如当你从STM32HAL库换成标准库,或者换成GD32的标准库时就不得不需要大面积修改了。

通过这种良好的隔离化、模块化思想,都能尽量减少移植的工作量。

2、功用模块层

一般来讲,驱动层的模块划分,一是1个接口1个驱动文件,比如can、flash、adc,二是1个模块1个驱动文件,比如usb、bt、4g、wifi,等等。

但是常常会有一个问题,比如bt/wifi/4g等等模块,需要对模块进行复杂的AT指令操作实现某些功能(比如设置模块参数、连接外部设备等等),这样的可以在选择构建一个platform层,这样将驱动一分为二,一块是硬件驱动-只用于基本的数据透传和数据接收发送,另一块是上层功能驱动-只用来完成模块功能(或者添加数据缓冲处理)。

例如,

1)USB需要做初始化、连接状态检测、数据接收(数据队列或者二级缓冲区+信号量通知)处理,数据读取(读队列操作),等等操作;

2)蓝牙/4G同USB,同时又增加了设备名设置、设备地址读取、对外连接等AT功能操作,以及基础透传功能;

3)WiFi同USB,同时又增加了AP/STA网络连接操作和数据传输操作,可以做成状态机处理函数API,供上层调用(做好不要单独做成任务,否则代码缠在一起比较麻烦);

还有其他比较常见的模块,

FLASH管理模块,实现FLASH特定区域的读写接口,实现OTA的驱动API;

文件管理模块,实现如日志存储、文件夹管理等功能API;

UI管理模块,实现是页面初始化,加载刷新、菜单切换等API框架;

……

3、业务层

业务层,可以在RTOS中简单的理解为一个task。

比如一个CAN协议交互功能,就可以设计3个task,

        一个是命令接收处理任务,负责拆包解包;

        一个是命令发送处理任务,负责组包发送;

        最后就是协议处理任务,实现函数指针数组,收到命令后去执行对应的状态机处理函数,并上报执行结果。

从这个角度上,task就可以分为3类:

1)通讯处理任务;

        收到消息通知后,开始处理原始数据包;处理之后再推送到其他task进行进一步处理,或者直接对外发送,或者当场处理掉并发送response。

        触发源是硬件中断,比如USB接收中断和蓝牙接收中断。

2)协议处理任务;

        收到消息通知后,处理加工之后的命令包(或者处理全局变量数据);执行指定的功能操作,并反馈结果到上层。

        触发源由内部软件触发,可以是通讯处理任务的二次传递,也可以是内部软硬件定时器触发。

3)系统监控任务;

        轮询监听系统信息并保存到全局变量中,监听系统状态和参数。比如经典的ADC数据采集(温湿度、压力、电压电流电阻)和GPS信号采集。

如何模块化业务层的任务设计?

1)依赖于RTOS的任务处理代码与数据处理代码隔离开

比如下面这段代码,是一个最上层的接收数据处理task。

整体分为3个部分:

        1.等待信号量通知 OSSemPend;

        2.读取并处理数据包 UpperRxDataBaleProcess;

        3.处理命令 UpperCmdDeal;

经过这样的分层设计之后,将不需要RTOS的代码隔离出去,只调用API接口,减少更换通讯类型或RTOS时的移植工作量,也可以使代码更为简洁。

while(1)
{
	OSSemPend(UpperRxDataSem,0,&byErr);
	if(byErr == OS_ERR_NONE)
	{
		while(1)
		{
			pCmdBuff = UpperRxDataBaleProcess(&unCmdBuffLen);
			if(pCmdBuff != NULL)
			{
				UpperCmdDeal(pCmdBuff,unCmdBuffLen);
				MemoryFree((void**)&pCmdBuff);
			}
			else
			{
				break;
			}
		}
	}
}

2)状态机处理函数

如下代码,是一个典型的总线命令交互式状态机处理框架:

先设计交互流程的状态定义(里面函数可以什么都不用做,只需要先做好状态跳转),在收到1条命令之后执行该状态机处理函数,并做好安全退出机制,允许外部其它命令打断状态机的执行。

void ProtocolProcess(void)
{
	ProtocolInfoInit();
	ProtocolState = STATE_UPPER_NEED_STOP_CHECK;
	
	while(1)
	{
		switch(ProtocolState)
		{
		case STATE_UPPER_NEED_STOP_CHECK:
			UpperNeedStopCheck();
			break;
			
		case STATE_CMD_GET_CMD_INFO:
            ProtocolCmdGetCmdInfo();
            break;
			
		case STATE_CMD_SEND_DATA:
            ProtocolCmdSendData();
            break;
			
        case STATE_CMD_RECEIVE_DATA:
            ProtocolCmdReceiveData();
            break;
			
		case STATE_CMD_RECEIVE_TIMEOUT:
            ProtocolCmdReceiveDataTimeOut();
            break;
			
		case STATE_CMD_RECEIVE_FINISH:
            ProtocolCmdReceiveFinish();
            break;
			
		case STATE_CMD_REPORT_UPPER:
            ProtocolCmdReportUpper();
            return;
		
		case STATE_EXIT:
            ProtocolExit();
            return;
		
		default:
            break;
		}
	}
}

2)合理的设计任务的结构体

一般的,

a.任务运行要设置一个结构体,保存任务的运行参数

        比如,任务当前执行的命令信息/类型,配置信息,运行状态,上报类型和参数

b.一条命令要设置一个结构体,结构化字节序列

        设计通讯协议,一定要注意字节序列与结构体的相互转化

4、通讯协议

典型的几种通讯协议

a. 固定头+长度位+DATA+CRC校验位+固定尾

        安全性最高,有固定的起始和结束,且CRC校验更可靠;但是需要对数据进行替换(有效数据中出现固定头尾时),且校验比较耗时(对于低性能MCU)。

b. 控制位+长度位+DATA(+校验)

       (类似MQTT)不需要进行校验,适合传输JSON数据。

c. 固定头+长度位+DATA+校验和

        复杂度较低,适合空闲中断式的数据传输,校验和计算也非常简单。

d. 起始位+DATA(+校验和)

       (类似CAN-15765协议) 有一个起始位表示报文类型和数据长度,非常适合数据量小的场合。

为什么要有校验位?原因是嵌入式设备和对端如PC、手机等等,都可能因为外部环境或自身故障,导致发送数据时出现脏数据,表象就是一串字节流数据,发出去是对的,但是到了接收方那里,中间某个或若干个字节就变成错的了。

所以除了USB\CAN\SDMMC这种具有严谨的交互机制外,其他的如UART/SPI/I2C/都是有可能发生错误的,尤其是高波特率情况和中断过于频繁时。

  • 3
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猪熊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值