C语言嵌入式系统编程修炼之道——软件架构篇

简介:


C语言嵌入式系统编程修炼之道——软件架构篇

作者:宋宝华  e-mail:[email]21cnbao@21cn.com[/email]

1.模块划分

模块划分的“划”是规划的意思,意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。 C 语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能(依功能进行划分在面向对象设计中成为一个错误,牛顿定律遇到了相对论), C 语言模块化程序设计需理解如下概念:
(1)     模块即是一个 .c 文件和一个 .h 文件的结合,头文件 (.h) 中是对于该模块接口的声明;
(2)     某模块提供给其它模块调用的外部函数及数据需在 .h 中文件中冠以 extern 关键字声明;
(3)     模块内的函数和全局变量需在 .c 文件开头冠以 static 关键字声明;
(4)     永远不要在 .h 文件中定义变量!定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如:
/*module1.h*/
int a = 5;               /*  在模块 1 .h 文件中定义 int a  */
 
 
/*module1 .c*/
#include “module1.h”     /*  在模块 1 中包含模块 1 .h 文件  */
 
 
/*module2 .c*/
#include “module1.h”     /*  在模块 2 中包含模块 1 .h 文件  */
 
 
/*module3 .c*/
#include “module1.h”     /*  在模块 3 中包含模块 1 .h 文件  */
以上程序的结果是在模块 1 2 3 中都定义了整型变量 a a 在不同的模块中对应不同的地址单元,这个世界上从来不需要这样的程序。正确的做法是:
/*module1.h*/
extern int a;               /*  在模块 1 .h 文件中 声明 int a  */
 
 
/*module1 .c*/
#include “module1.h”        /*  在模块 1 中包含模块 1 .h 文件  */
int a = 5;                 /*  在模块 1 .c 文件中定义 int a  */
 
 
/*module2 .c*/
#include “module1.h”        /*  在模块 2 中包含模块 1 .h 文件  */
 
 
/*module3 .c*/
#include “module1.h”     /*  在模块 3 中包含模块 1 .h 文件  */
这样如果模块 1 2 3 操作 a 的话,对应的是同一片内存单元。
一个嵌入式系统通常包括两类模块:
1 )硬件驱动模块,一种特定硬件对应一个模块;
2 )软件功能模块,其模块的划分应满足低偶合、高内聚的要求。

2.多任务还是单任务

所谓“单任务系统”是指该系统不能支持多任务并发操作,宏观串行地执行一个任务。而多任务系统则可以宏观并行(微观上可能串行)地“同时”执行多个任务。
多任务的并发执行通常依赖于一个多任务操作系统( OS ),多任务 OS 的核心是系统调度器,它使用任务控制块( TCB )来管理任务调度功能。 TCB 包括任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。调度器在任务被激活时,要用到这些信息。此外, TCB 还被用来存放任务的“上下文”( context) 。任务的上下文就是当一个执行中的任务被停止时,所要保存的所有信息。通常,上下文就是计算机当前的状态,也即各个寄存器的内容。当发生任务切换时,当前运行的任务的上下文被存入 TCB ,并将要被执行的任务的上下文从它的 TCB 中取出,放入各个寄存器中。
嵌入式多任务 OS 的典型例子有 Vxworks ucLinux 等。嵌入式 OS 并非遥不可及的神坛之物,我们可以用不到 1000 行代码实现一个针对 80186 处理器的功能最简单的 OS 内核,作者正准备进行此项工作,希望能将心得贡献给大家。
究竟选择多任务还是单任务方式,依赖于软件的体系是否庞大。例如,绝大多数手机程序都是多任务的,但也有一些小灵通的协议栈是单任务的,没有操作系统,它们的主程序轮流调用各个软件模块的处理程序,模拟多任务环境。

3.单任务程序典型架构

1 )从 CPU 复位时的指定地址开始执行;
2 )跳转至汇编代码 startup 处执行;
3 )跳转至用户主程序 main 执行,在 main 中完成:
a. 初试化各硬件设备;  
b. 初始化各软件模块;
c. 进入死循环(无限循环),调用各模块的处理函数
     用户主程序和各模块的处理函数都以 C 语言完成。用户主程序最后都进入了一个死循环,其首选方案是:
while(1)
{
}
有的程序员这样写:
for(;;)
{
}
这个语法没有确切表达代码的含义,我们从 for(;;) 看不出什么,只有弄明白 for(;;) C 语言中意味着无条件循环才明白其意。
下面是几个“著名”的死循环:
1 )操作系统是死循环;
2 WIN32 程序是死循环;
3 )嵌入式系统软件是死循环;
4 )多线程程序的线程处理函数是死循环。
你可能会辩驳,大声说:“凡事都不是绝对的, 2 3 4 都可以不是死循环”。 Yes you are right ,但是你得不到鲜花和掌声。实际上,这是一个没有太大意义的牛角尖,因为这个世界从来不需要一个处理完几个消息就喊着要 OS 杀死它的 WIN32 程序,不需要一个刚开始 RUN 就自行了断的嵌入式系统,不需要莫名其妙启动一个做一点事就干掉自己的线程。有时候,过于严谨制造的不是便利而是麻烦。君不见,五层的 TCP/IP 协议栈超越严谨的 ISO/OSI 七层协议栈大行其道成为事实上的标准?
经常有网友讨论:
printf(“%d,%d”,++i,i++);    /*  输出是什么? */
c = a+++b ;               /* c=? */
等类似问题。面对这些问题,我们只能发出由衷的感慨:世界上还有很多有意义的事情等着我们去消化摄入的食物。
实际上,嵌入式系统要运行到世界末日。

4.中断服务程序

中断是嵌入式系统中重要的组成部分,但是在标准 C 中不包含中断。许多编译开发商在标准 C 上增加了对中断的支持,提供新的关键字用于标示中断服务程序 (ISR) ,类似于 __interrupt #program interrupt 等。当一个函数被定义为 ISR 的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
中断服务程序需要满足如下要求:
(1) 不能返回值;
(2) 不能向 ISR 传递参数;
(3) ISR 应该尽可能的短小精悍;
(4) printf(char * lpFormatString,…) 函数会带来重入和性能问题,不能在 ISR 中采用。
在某项目的开发中,我们设计了一个队列,在中断服务程序中,只是将中断类型添加入该队列中,在主程序的死循环中不断扫描中断队列是否有中断,有则取出队列中的第一个中断类型,进行相应处理。
/*  存放中断的队列  */
typedef struct tagIntQueue
{
int intType;                /*  中断类型  */
struct tagIntQueue *next;
}IntQueue;
 
 
IntQueue lpIntQueueHead;
 
 
__interrupt ISRexample ()
{
  int  intType;
  intType = GetSystemType();
QueueAddTail(lpIntQueueHead, intType) /*  在队列尾加入新的中断  */
 }
在主程序循环中判断是否有中断:
While(1)
{
If( !IsIntQueueEmpty() )
   {
    intType = GetFirstInt();
    switch(intType)      /*   是不是很象 WIN32 程序的消息解析函数 ?  */
     {                /*   对,我们的中断类型解析很类似于消息驱动  */
     case xxx:          /*  我们称其为“中断驱动”吧?  */
      
     break;
     case xxx:
      
     break;
     
     }
}
    }
按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。

5.硬件驱动模块

一个硬件驱动模块通常应包括如下函数:
1 )中断服务程序 ISR
2 )硬件初始化
a. 修改寄存器,设置硬件参数(如 UART 应设置其波特率, AD/DA 设备应设置其采样速率等);
b. 将中断服务程序入口地址写入中断向量表:
/*  设置中断向量表  */
  m_myPtr = make_far_pointer(0l); /*   返回 void far 型指针 void far *  */    
  m_myPtr += ITYPE_UART;  /*  ITYPE_UART  uart 中断服务程序  */
/*   相对于中断向量表首地址的偏移  */
  *m_myPtr = &UART _Isr;   /* UART _Isr UART 的中断服务程序  */
3 )设置 CPU 针对该硬件的控制线
a. 如果控制线可作 PIO (可编程 I/O )和控制信号用,则设置 CPU 内部对应寄存器使其作为控制信号;
b. 设置 CPU 内部的针对该设备的中断屏蔽位,设置中断方式(电平触发还是边缘触发)。
4 )提供一系列针对该设备的操作接口函数。例如,对于 LCD ,其驱动模块应提供绘制像素、画线、绘制矩阵、显示字符点阵等函数;而对于实时钟,其驱动模块则需提供获取时间、设置时间等函数。

6.C的面向对象化

在面向对象的语言里面,出现了类的概念。类是对特定数据的特定操作的集合体。类包含了两个范畴:数据和操作。而 C 语言中的 struct 仅仅是数据的集合,我们可以利用函数指针将 struct 模拟为一个包含数据和操作的“类”。下面的 C 程序模拟了一个最简单的“类”:
#ifndef  C_Class
       #define C_Class struct
#endif
C_Class A
{
       C_Class A *A_this;             /* this 指针  */
       void (*Foo)(C_Class A *A_this);  /*  行为:函数指针  */
       int a;                        /*  数据  */
       int b;
};
我们可以利用 C 语言模拟出面向对象的三个特性:封装、继承和多态,但是更多的时候,我们只是需要将数据与行为封装以解决软件结构混乱的问题。 C 模拟面向对象思想的目的不在于模拟行为本身,而在于解决某些情况下使用 C 语言编程时程序整体框架结构分散、数据和函数脱节的问题。我们在后续章节会看到这样的例子。

总结

本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。
请记住:软件结构是软件的灵魂!结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。

一个高尚的程序员应该是写出如艺术作品般程序的程序员。




 本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120790,如需转载请自行联系原作者



相关文章
|
2月前
|
机器学习/深度学习 文字识别 监控
安全监控系统:技术架构与应用解析
该系统采用模块化设计,集成了行为识别、视频监控、人脸识别、危险区域检测、异常事件检测、日志追溯及消息推送等功能,并可选配OCR识别模块。基于深度学习与开源技术栈(如TensorFlow、OpenCV),系统具备高精度、低延迟特点,支持实时分析儿童行为、监测危险区域、识别异常事件,并将结果推送给教师或家长。同时兼容主流硬件,支持本地化推理与分布式处理,确保可靠性与扩展性,为幼儿园安全管理提供全面解决方案。
116 3
|
3月前
|
资源调度 监控 调度
基于SCA的软件无线电系统的概念与架构
软件通信体系架构(SCA)是基于软件定义无线电(SDR)思想构建的开放式、标准化和模块化平台,旨在通过软件实现通信功能的灵活配置。SCA起源于美军为解决“信息烟囱”问题而推出的联合战术无线电系统(JTRS),其核心目标是提升多军种联合作战通信能力。 上海介方信息公司的OpenSCA操作环境严格遵循SCA4.1/SRTF标准,支持高集成、嵌入式等场景,适用于军用通信、雷达等领域。 SCA体系包括目标平台资源层(TRL)、环境抽象层(EAL)、SRTF操作环境(OE)及应用层(AL)。其中,SRTF操作环境包含操作系统、运行时环境(RTE)和核心框架(CF),提供波形管理、资源调度等功能。
【YashanDB知识库】如何排查YMP报错:”OCI版本为空或OCI的架构和本地系统的架构不符“
【YashanDB知识库】如何排查YMP报错:”OCI版本为空或OCI的架构和本地系统的架构不符“
【YashanDB知识库】如何排查YMP报错:”OCI版本为空或OCI的架构和本地系统的架构不符“
|
12天前
|
数据采集 存储 算法
人才招聘系统开发全解析:从技术底层到商业逻辑的完整架构优雅草卓伊凡|小无|果果|阿才
人才招聘系统开发全解析:从技术底层到商业逻辑的完整架构优雅草卓伊凡|小无|果果|阿才
54 2
人才招聘系统开发全解析:从技术底层到商业逻辑的完整架构优雅草卓伊凡|小无|果果|阿才
|
2月前
|
人工智能 自然语言处理 API
MCP与A2A协议比较:人工智能系统互联与协作的技术基础架构
本文深入解析了人工智能领域的两项关键基础设施协议:模型上下文协议(MCP)与代理对代理协议(A2A)。MCP由Anthropic开发,专注于标准化AI模型与外部工具和数据源的连接,降低系统集成复杂度;A2A由Google发布,旨在实现不同AI代理间的跨平台协作。两者虽有相似之处,但在设计目标与应用场景上互为补充。文章通过具体示例分析了两种协议的技术差异及适用场景,并探讨了其在企业工作流自动化、医疗信息系统和软件工程中的应用。最后,文章强调了整合MCP与A2A构建协同AI系统架构的重要性,为未来AI技术生态系统的演进提供了方向。
605 62
|
1月前
|
人工智能 Cloud Native 容灾
深圳农商银行三代核心系统全面投产 以云原生架构筑牢数字化转型基石
深圳农商银行完成第三代核心系统全面上云,日均交易超3000万笔,峰值处理效率提升2倍以上。扎根深圳70余年,与阿里云共建“两地三中心”分布式云平台,实现高可用体系及全栈护航。此次云原生转型为行业提供可复制样本,未来将深化云计算与AI合作,推动普惠金融服务升级。
211 17
|
2月前
|
存储 人工智能 开发框架
MCP 实践:基于 MCP 架构实现知识库答疑系统
文章探讨了AI Agent的发展趋势,并通过一个实际案例展示了如何基于MCP(Model Context Protocol)开发一个支持私有知识库的问答系统。
MCP 实践:基于 MCP 架构实现知识库答疑系统
|
2月前
|
存储 人工智能 自然语言处理
Cursor这类编程Agent软件的模型架构与工作流程
编程Agent的核心是一个强大的大语言模型,负责理解用户意图并生成相应的代码和解决方案。这些模型通过海量文本和代码数据的训练,掌握了广泛的编程知识和语言理解能力。
141 1
|
3月前
|
人工智能 运维 Cloud Native
2025年国内工单系统推荐:技术架构、场景适配与行业实践
分析了智能化升级、大数据驱动、云原生架构及全渠道融合四大技术趋势,从功能适配性、易用性、集成能力、安全性和性价比五个维度指导企业选型,并推荐合力亿捷等三家系统的优劣对比,结合电商和制造行业的实际案例,帮助企业提升客户服务水平与竞争力。
223 11
2025年国内工单系统推荐:技术架构、场景适配与行业实践
|
3月前
|
运维 供应链 前端开发
中小医院云HIS系统源码,系统融合HIS与EMR功能,采用B/S架构与SaaS模式,快速交付并简化运维
这是一套专为中小医院和乡镇卫生院设计的云HIS系统源码,基于云端部署,采用B/S架构与SaaS模式,快速交付并简化运维。系统融合HIS与EMR功能,涵盖门诊挂号、预约管理、一体化电子病历、医生护士工作站、收费财务、药品进销存及统计分析等模块。技术栈包括前端Angular+Nginx,后端Java+Spring系列框架,数据库使用MySQL+MyCat。该系统实现患者管理、医嘱处理、费用结算、药品管控等核心业务全流程数字化,助力医疗机构提升效率和服务质量。
204 4