前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >嵌入式应用软件架构设计

嵌入式应用软件架构设计

作者头像
杨永贞
发布2020-08-04 21:46:08
1.8K0
发布2020-08-04 21:46:08
举报
文章被收录于专栏:独行猫a的沉淀积累总结

要做到嵌入式应用的代码逻辑清晰,且避免重复的造轮子,没有好的应用架构怎么行。 如果没有好的架构,移植将会是一件很痛苦的事情。

如果没有好的架构,复用是最大的难题,没法更大限度的复用原有的代码。

如果没有好的架构,一旦驱动改了,所有的地方都要改,费时费力且很容易出错。

如果没有好的架构,应用层中穿插着硬件驱动层的代码,看着会是一片混乱,逻辑不清,代码维护起来会很困难。

这里总结下我的嵌入式程序设计思路,分享出来与大家共同探讨,同时也欢迎提出不同意见。 现在的小朋友都爱玩搭积木的游戏,一个模块一个模块的拼装起来,快速组成各种不同的模型。现在的产品设计也很少从零开始。大都复用现有成熟的模块,专注于某个擅长领域。

我的嵌入式应用架构思路来源与此,即功能模块设计与分层。

把API分为驱动层和应用层API,而不是所有程序都调用驱动层API。(整个应用中都调用驱动层API会导致应用中驱动调用随处可见,无法移植和最大限度的复用)

先把一个应用进行功能模块划分,并对整体结构进行分层,然后设计出功能独立的各个模块(如算法模块,文件库模块,通信库模块),在模块之上开放公共接口。

驱动层提供出公共接口供上层调用。各个功能模块可以独立编译(如算法模块纯ANSI C,可在任意平台复用),或者调用驱动层接口(文件库模块调用了驱动读写Flash),总而言之,言而总之,封装出各个功能独立的可复用的功能模块。

总体分 硬件驱动层-->功能模块层-->应用接口层-->业务逻辑层-->应用层

总体结构示意框图:

应用层,为程序的总体的运行框架,组织调用业务逻辑。可以用某种嵌入式操作系统实现几种任务 。如定时任务,卡处理任务,菜单任务,通信任务。 业务逻辑层,如CPU卡处理,交通部卡处理,银联卡处理,M1卡处理,通信记录上传,黑名单下载,票价参数下载等。 应用接口层,提供公共的api接口供应用接口供上层调用。这些接口也可由下层的功能模块开放出来,应用接口层负责汇总。 功能模块层,可以封装不同的功能模块。如算法库,文件库,通信库,银联库,向上提供应用接口层的接口,向下调用驱动接口。 硬件驱动层,由各个驱动模块组成,向上提供统一的接口。

遵循一些约定, 1.每个模块提供出的接口要统一,后续只能增,不能改原来的接口。 2.模块与模块之间相互独立,互不影响,不能相互调用,只能调用它下层的接口。 3.由模块构成层,层与层之间不能跨级调用。如在应用层中不能看到直接调用驱动层的代码。

4.模块中又可以继续分层,如接口层,驱动层,硬件层。

如果驱动变动了,或者换不同平台,只需更改驱动层,应用层不受影响。 如果功能模块变动了,只需升级功能功能模块,其他的模块不受影响,应用层也不受影响。

按照这种逻辑设计好之后,主要的工作就是在业务逻辑层。应用层则为程序的总体流程和框架,主要调用业务逻辑层实现不同的功能。

我们现在的代码结构,基本是按这个思路来的。

硬件驱动层-->功能模块层-->应用接口层-->业务逻辑层-->应用层。

看看以下两种风格的代码,你更喜欢哪个。

另一种风格:

同样是保存参数,非要拆成 AlgCRC16 ,WritePraFlash( (unsigned char *)&NetPra , NETPRA_ADDR , sizeof(_NetPra) )两步吗?

还有AH_Para_Verify这个,在应用层中真是多余啊,检测失败又从Flash读取。关于参数,一开机就应该检测合法性了。

既然都是要保存参数,就应该做个封装,如上图所示,把系统用到的不同参数做个规划。应用层调用APP_Open_UseFile 或者APP_Read_UseFile,

而不是直接的去读写Flash。

来看看赫赫有名的谷歌的android架构,虽然很复杂,但从框图上看,也像是搭积木,各个功能模块独立,层次分明。最低层建立在linux Kernel基础上,然后是各个组件库libraries,再往上是应用框架和应用。

以NC_FileLib,文件库模块为例,如果要用在其他平台,如EH0918手持机设备,只需要移植几个硬件层接口即可。

NC_FileSys文件库,跟硬件相关的接口在Hook文件夹, 重新实现以下几个函数即可: void HW_FRAM_Init( void ) unsigned int HW_FRAM_Read( unsigned int addr,unsigned int size,unsigned char *buffer) unsigned int HW_FRAM_Write( unsigned int addr, unsigned int size,unsigned char *buffer ) //擦除FLASH一页 (FLASH擦除的最小单元) unsigned int HW_Flash_PageErase( unsigned int page ) unsigned int HW_Flash_Read( unsigned int addr, unsigned int size, unsigned char *buffer ) unsigned int HW_Flash_NotEraseWrite( unsigned int addr, unsigned int size, unsigned char *buffer ) //擦除FLASH一页 (FLASH擦除的最小单元) unsigned int HW_Flash_PageErase( unsigned int page )

按照以上模块化设计思想,很容易实现一模拟pos机。

以开发一个智能pos应用为例: 一个智能pos涉及到的功能模块有: 读写卡功能,保存与读取消费记录,查找保存黑名单,界面显示,菜单显示,通信下载参数上传记录等。

以下为移植功能模块到电脑上,自己做的一个模拟Pos工具: 在电脑上实现一模拟pos(只是功能上的实现,完成刷卡消费,记录存储,记录上传,黑名单,票价下载等功能。界面为Dos窗口。后续如果用QT把界面也做出来,就是一功能齐全的模拟POS机,不过得把荒废多年的C++重新拾起来了。可以继续完善做一个上位机模拟pos,改变编译器在上位机仿真调试并交叉编译后运行在真实POS上)。 用到的功能模块有 文件存储模块,卡处理模块,算法模块,银联库模块。我把这些模块移植到电脑上。 关于卡处理模块的实现,由于电脑上没读卡头,于是用外接读卡器。把读卡器串口接电脑上。电脑上做一读写卡服务,提供TCP接口的读写卡接口。 移植文件库,嵌入式程序中是操作的flash,在电脑上把文件库中用到的接口用读写文件的形式替换。 移植算法库,算法库都是c写的,直接用gcc在windows平台重新编译即可。 实现效果:

第7项,模拟POS与银联通信:

POSP模拟器为模拟银联后台的一个工具。运行bus,exe进行签到,下载IC卡参数与公钥并保存至电脑上,

第5项,银联卡消费:

读卡器通过串口接到电脑上,电脑上运行读写卡服务tcpserver.exe,,提供读写卡APDU指令接口。利用之前做的小工具远程读卡器。

有时候为了测试卡片消费,不能每次都重新编译进来吧,这时候可以利用LUA脚本。选择第11项。把需要测试卡片的指令写进LUA脚本,然后执行,

如图所示:

执行结果:

封装的功能模块,即各种库如下: liblua.a 执行lua脚本使用 libblkfile.a 黑名单查找与存储 libmaycalc.a 算法库,如SHA,DES算法 libmycard.a 卡库,提供操作卡片的APDU libmyfile.a 文件库,提供文件存储与读取 libmycom.a 通信库,提供socket通信 libmyup.a 银联库,银联卡处理逻辑与银联后台通信业务

其中,libmyblkfile库的makefile文件如下: ######################################## #makefile ######################################## BINARY= libmyblkfile CC= gcc LD= ld CFLAGS= -std=c99 -g LDSCRIPT= LDFLAGS= -Llib OBJS= AH_BlackList.o AH_BlkDirFileLib.o APP_Blacklist.o #CFLAGS=-std=c99 .PHONY: clean all:images images: $(BINARY).a $(OBJS):%.o:%.c $(CC) -c $(CFLAGS) $< -o $@ %.a: $(OBJS) ar crv $(*).a $(OBJS) cp libmyblkfile.a ../ clean: rm -f *.o

主程序的makefile文件如下: ######################################## #makefile ######################################## #编译指定子目录 SUBDIRS := .\\lib\\NC_Com\ .\\lib\\NC_FileSys\ .\\lib\\NC_BlkFile\ .\\lib\\NC_Card\\NC_Card_Lib\ .\\lib\\NC_UPCash\\NC_UPCash_Lib define make_subdir @ for subdir in $(SUBDIRS) ; do \ ( cd $$subdir && make $1) \ done; endef #编译主程序 BINARY := ./bin/bus OBJ_DIR := ./obj/ CC= gcc LD= ld CFLAGS= -std=c99 -Wall -g LDSCRIPT= -lmycom -lws2_32 -liconv -lmyfile -lmycard -lmyup -lmycalc -lmyblkfile -llua LDFLAGS= -Llib SRC = $(wildcard *.c) DIR = $(notdir $(SRC)) OBJS = $(patsubst %.c,$(OBJ_DIR)%.o,$(DIR)) #OBJS= main.o myutils.o inirw.o cmdpboc.o cputest.o bustcp.o ansrec.o m1cmd.o m1api.o m1test.o upcash.o myother.o getsys.o #CFLAGS=-std=c99 #@echo Building lib... #$(call make_subdir) .PHONY: clean lib all: prebuild $(BINARY).exe prebuild: @echo Building app... $(BINARY).exe : $(OBJS) @echo Generating ... $(CC) -o $(BINARY).exe $(OBJS) $(LDFLAGS) $(LDSCRIPT) @echo OK! $(OBJ_DIR)%.o : %.c $(CC) -c $(CFLAGS) $< -o $@ lib: @echo Building lib... $(call make_subdir) clean: rm -f $(OBJ_DIR)*.o @echo Removed!

各个功能模块,又可以进一步细分为子模块。 拿通信库举例: 嵌入式设备都需要支持各种不同的通讯模块。比如硬件设备有A701、A801、B502等,通讯模块有GL868、MG323、MC8630、N710、ZIGBEE等,这些设备分别支持全部或部分通讯模块。 整体架构分为如下:

驱动大致分为三层: 1、接口层:为用户提供统一的接口,比如:Connect、TxData、RxData、Disconnect等。 2、驱动层:向接口层暴露统一的接口,这些接口用于完成实际的连接断开和数据收发等,比如:DevConnect、DevTxData、RxData、Disconnect等。该层只会和支持的通讯模块相关,不会直接访问任何硬件功能,包括串口通讯、GPIO控制全部通过底层的设备层实现。 3、设备层:向驱动层提供统一的接口,这些接口通过访问物理硬件来实现和模块的通讯,比如:XXXPowerOn、SerialSend、SerialReceive等,并定义该设备支持哪些模块。 三层之间通过标准的接口进行互相访问。 1、接口层对外接口(用户操作通讯模块使用的API): uint32_t Com_Dev_Start(void); //设备重新上电,并和网络建立连接 uint32_t Com_Dev_Restart(void); //设备断网断电 uint32_t Com_Dev_Stop(void); //连接远端服务器 uint32_t Com_Dev_Connect(uint8_t *ip, uint16_t port, uint32_t timeout, uint32_t channel); //断开连接 uint32_t Com_Dev_Disconnect(uint32_t channel); //发送数据 uint32_t Com_Dev_TxData(uint8_t *buf, uint32_t len, uint32_t timeout, uint32_t channel); //接收数据 uint32_t Com_Dev_RxData(uint8_t *buf, uint32_t *rxlen, uint32_t len, uint32_t timeout, uint32_t channel); //获取信号质量 uint32_t Com_Dev_GetSQ(uint8_t *csq); 2、驱动层对接口层的接口(对接口层屏蔽各通讯模块的差异): struct ComDevFunc{ uint32_t (*start)(ComDevDesc *dev); uint32_t (*restart)(ComDevDesc *dev); uint32_t (*stop)(ComDevDesc *dev); uint32_t (*connect)(ComDevDesc *dev, uint8_t *ip, uint16_t port, uint32_t timeout, uint32_t channel); uint32_t (*disconnect)(ComDevDesc *dev, uint32_t channel); uint32_t (*txData)(ComDevDesc *dev, uint8_t *buf, uint32_t len, uint32_t timeout, uint32_t channel); uint32_t (*rxData)(ComDevDesc *dev, uint8_t *buf, uint32_t *rxlen, uint32_t len, uint32_t timeout, uint32_t channel); uint32_t (*getVer)(ComDevDesc *dev, uint8_t *buf, uint32_t versize); uint32_t (*getCSQ)(ComDevDesc *dev, uint8_t *csq); }; 3、设备层对驱动层提供的接口(屏蔽串口号、GPIO引脚等平台相关的内容): int Com_PortOpen(uint32_t baud); int Com_PortClose(void); int Com_PortSend(uint8_t *buf, int len); int Com_PortRecv(uint8_t *buf, int len, int timeout); int Com_PortGetLen(void); int Com_PortFlush(void); //具体硬件相关函数,在Model_XXXXX.h中实现 //仅需实现平台支持的模块即可 void COM_GL868Power(int state); void COM_MC8332Power(int state); void COM_MG323Power(int state); void COM_EMV3081Power(int state); void COM_CC2530Power(int state); void COM_N710Power(int state); 2. 目录结构 │ ComAPI.c -----通讯库API实现 │ ComAPI.h -----通讯库API声明(接口层API) │ ComDevs.c -----供驱动使用的公共定义和工具函数 │ ComDevs.h -----供驱动使用的公共定义和工具函数(驱动层API) │ Readme.txt │ ├─Devices -----存放各种模块的驱动程序 │ Dev_CDMA_MC8332.c │ Dev_CDMA_MC8332.h │ Dev_GPRS_GL868.C │ Dev_GPRS_GL868.h │ Dev_WIFI_EMV3081.c │ Dev_WIFI_EMV3081.h │ └─Models -----存放适配各种设备型号的目录 Model.h -----设备层API Model_A701.c Model_B502.c 3. 支持新模块的方法 比如A701设备要新增支持SUPER123模块 1、在Devices目录中增加一个文件Dev_6G_SUPER123.c并实现ComDevFunc结构中定义的各函数 2、在设备文件Model_A701.c中增加电源控制函数COM_SUPER123Power。 3、在设备文件Model_A701.c中添加驱动函数到gComFuncs数组中。 4. 适配新设备的方法 1、在Models目录新增加一个文件Model_XXXX.c 2、实现串口通讯函数(Com_PortXXXX系列,参考Model.h中的定义) 3、实现支持的通讯模块的上下电函数COM_XXXXPower(int state); 4、实现gComFuncs数组,其中引用所有支持模块的驱动函数。 5. 对部分系统函数的引用 对于部分系统相关函数,比如延时、调试信息的打印等,全部提供默认实现,并将默认实现声明为弱函数。这样当用户需要进行自定义的时候可以进行自定义,如果不需要可以直接忽略不会造成编译错误。 这样的函数有如下三个: void Com_Hook_Printf(char* fmt, ...); void Com_Hook_PrintHex(uint8_t* buf, int len); //重定义延时功能,如果使用操作系统的话可以充分利用操作系统的调度特性,避免死等造成的浪费 void Com_Hook_DelayMs(uint32_t ms); 6. 驱动中普遍使用的功能 对于部分编写通讯模块驱动常用的功能,模块中提供了一系列的辅助函数,避免重复劳动

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017/08/09 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档