嵌入式应用软件架构设计

article/2025/9/14 14:52:17

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

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

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

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

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

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

把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读取。关于参数,一开机就应该检测合法性了。

不要在应用层看到驱动的影子和具体实现细节。要让谁不了解底层细节的情况下按照模块接口文档都能容易的使用。

比如上面的风格1,目的就是往flash里保存参数,但是非要让搞应用的人了解你底层的那么多细节,这样好吗?不怕给你搞崩了?

不管你是往falsh还是往文件存,还是做了个crc16校验,让做应用的人看到这些细节作甚?徒增加烦恼和出错的概率。

脑海中一定要有一种模块化,组件化和分层的概念,先谋而后动,做好规划和分层。这样模块化达到一定积累后,效率会倍增,稳定性和拓展性,维护性才能有提高。

既然都是要保存参数,就应该做个封装,如上图所示,把系统用到的不同参数做个规划。应用层调用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.   驱动中普遍使用的功能
对于部分编写通讯模块驱动常用的功能,模块中提供了一系列的辅助函数,避免重复劳动

 

-------------------------------------------------------------------------------------------

成功不是追求别人眼中的最好,而是把自己能做的事情做得最好。

每个人都应该有梦想,这才是生命的意义。

做事情贵在坚持,只有这份坚持,才实践了意义。

处处留心皆学问,爱学习,爱思考。

在这里分享学习,分享感悟,共同进步。

凝聚学习的圈子,思考的圈子。

扫码关注个人微信公众号:aazhen1987

凝聚学习和思考的圈子。

--------------------------------------------------------------------------------------------


http://chatgpt.dhexx.cn/article/WxuOAMTf.shtml

相关文章

嵌入式软件设计(1)--概述

嵌入式软件的定义及特点 嵌入式系统的定义 嵌入式系统是以应用为中心&#xff0c;以计算机技术为基础&#xff0c;软硬件可裁剪、功能、可靠性、功耗严格要求的专用计算机系统。 其中要说明的几个点是 1. 嵌入式系统是专用系统&#xff0c;而不是通用系统&#xff0c;其往往…

嵌入式软件设计必看书籍

提高C语言编程能力 以上4本书籍可以提高C语言编程能力&#xff0c;深入理解C语言指针用法&#xff0c;深入理解C语言标准。 提高软件架构设计能力 以上2本书籍掌握以下知识&#xff1a; 1、软件设计原则。 2、软件设计模式。 3、软件设计构架。 4、软件设计思维。 提升对RTO…

嵌入式软件架构的设计

嵌入式软件架构的设计 大多数嵌入式程序员学习编程&#xff0c;都是从开发板的附带例程开始。之后工作也会继续参考那些例程&#xff0c;很多编程习惯、方式也会受之影响。 其实开发板式的编程方式与工作中实际需求的并不完全一致。 开发板的通常卖给初学者&#xff0c;注重…

嵌入式软件设计(stm32快速上手)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 有很多的培训机构在培训嵌入式的时候,一上来会讲很多的理论知识。其实这个时候要是能通过实操帮助学员快速入门、快速上手、快速看到效果,或许这才是最重要的。 1、安装mdk软件 类…

举例说明嵌入式框架该如何分层

关注、星标公众号&#xff0c;直达精彩内容 素材来源&#xff1a;https://blog.csdn.net/weixin_46185705/article/details/122536374 整理&#xff1a;技术让梦想更伟大 | 李肖遥 前言 为了能够使得产品得到更好的开发速度与以后更好的迭代和移植&#xff0c;框架分层是很有必…

嵌入式软件架构设计

如何设计一个好的软件架构&#xff0c;如何提高软件的扩展性&#xff0c;移植性&#xff0c;复用性和可读性&#xff1f; 很多做嵌入式开发的朋友经常会遇到这种情况&#xff1a;一个项目软件设计完成了&#xff0c;客户提出了一些新的功能需求。这时侯如果客户新需求不多&…

《嵌入式软件设计方法》--设计原则

设计原则&#xff08;SOLID&#xff09; 使用各个原则的首字母组成了一个单词:SOLID。 SRP:Single Responsibility Principle&#xff0c;单一职责原则&#xff1b;OCP:Open Closed Principle&#xff0c;开闭原则&#xff1b;LSP:Liskov Substitution Principle&#xff0c;…

嵌入式软件设计之设计模式

文章目录 前言1.设计模式之适配器模式2.设计模式之单例模式3.设计模式之命令模式4.设计模式之门面模式 前言 在嵌入式软件设计过程中&#xff0c;也会用到一些设计模式&#xff0c;所以说设计模式并不是面向对象语言的专利&#xff0c;下面我通过查资料以及自己的思考总结的几…

2、【已解决】Oracle错误问题(ORA-03113)

在执行一条sql时出现了如下问题&#xff1a; 处理方案网上找了一下日志&#xff0c;说是备份日志满了的原因&#xff1b; 通过以下方法看了下错误日志信息&#xff1a; 1、export TIME_STYLE%Y-%m-%d %H:%M:%S #格式化文件日期&#xff0c;便于查找错误文件 2、sqlplus &qu…

搭建ADG过程中复制报错 RMAN-03009 ORA-03113

搭建ADG过程中复制报错 RMAN-03009 ORA-03113 猜测主备之间网络路由过多导致。。。 开启mrp进程报错 发现数据文件是主库ASM的路径&#xff0c;备库是单机的 switch database to copy; 报错RMAN-6571 report schema;switch database to copy; select name from v$datafile;s…

oracle 启动报错03113,oracle数据库无法启动,总报ora-03113错误

虚机上装的oracle&#xff0c;由于系统突然宕机&#xff0c;导致了以下问题&#xff1a; 问题&#xff1a; 1、监听无法启动&#xff1a; $ lsnrctl start LSNRCTL for Linux: Version 11.2.0.4.0 - Production on 22-NOV-2018 15:27:11 Copyright (c) 1991, 2013, Oracle. Al…

oracle 启动报错03113,Oracle 入门之Oracle启动报错“ORA-03113”

Oracle 入门之Oracle启动报错“ORA-03113” [日期&#xff1a;2010-09-25] 来源&#xff1a;Linux社区 作者&#xff1a;naruto6006 [字体&#xff1a;大 中 小] 早上连接Oracle,发现oracle无法正常工作&#xff0c;无法shutdown immediate方式关闭&#xff0c;shutdown abort方…

linux oracle 03113,Oracle数据库关闭时出现ORA-03113错误

Oracle数据库关闭时&#xff0c;出现ORA-03113错误&#xff1a; SQL> shutdown immediate ORA-03113: end-of-file on communication channel Process ID: 3437 Session ID: 125 Serial number: 5 SQL> startup ORA-24324: service handle not initialized ORA-01041: in…

oracle startup open ora 03113,oracle宕机,startup报错ora03113

一、故障情况 应用无法连接数据库&#xff0c;检查oracle发现已经宕机。 startup 后报错 ORA-03113: end-of-file on communication channel 二、查找原因 查看alter日志 tail -500 /oracle/database/oracle/diag/rdbms/udb/udb1/trace/alter_UDB1.log Unable to create archiv…

ora03113通信通道的文件结尾 会话id 149 序列号 3

ORA-19815: 警告: db_recovery_file_dest_size 字节 已使用100.00%, 尚有 0 字节可用。” 是db_recovery_file_dest_size也叫归档日志空间不足导致 解决方法 第一步&#xff0c;将空间设置大点&#xff0c;另一个就是将多余的文件删除掉即可&#xff0c;那么我们就将这两个办法…

ORA-03113:通信通道的文件结尾

转自--------------http://blog.csdn.net/zwk626542417/article/details/39667999 由来 今天跟往常一样&#xff0c;登陆PL/SQL&#xff0c;确登陆失败&#xff0c;出现一个错误“ORA-01034”和“ORA-27101”如图&#xff1a; 然后就就通过命令提示符去登陆Oracle&#xff0c;去…

ORA-03113:通信通道的文件结尾-完美解决方案

今天发现系统登录和查询数据特别慢&#xff0c;orcl进程占用内存也特别高。打开程序调试用&#xff0c;存储过程调用时报错【ORA-03113&#xff1a;通信通道的文件结尾】 解决方案&#xff1a; oracle 文档中对这个错误这样解释&#xff1a; ORA-03113 错误就是说连接到数据…

ORA-03113: 通信通道的文件结尾

一、报错及错误原因 启动oracle数据库事报错&#xff1a;ORA-03113: 通信通道的文件结尾&#xff0c;报错内容如下图 出现问题后&#xff0c;去查看告警日志文件&#xff08;D:\app\diag\rdbms\cjyorcl\cjyorcl\trace\alert_cjyorcl.log&#xff09;&#xff0c;日志报错内容如…

TinyPng:在线PNG图片压缩工具

本资源由 伯乐在线 - 卢伟 整理 TinyPng:在线PNG图片压缩工具是一款可以帮助网页设计师们优化图片的工具&#xff0c;只需要简单的两步就可以完成对PNG图片的高压缩而且还不会影响PNG图片的质量&#xff0c;这样就可以解决网页设计师因图片太大而影响网站加载网页速度的难题。 …