上一课:
【小黑嵌入式系统第五课】嵌入式系统开发流程——开发工具、交叉开发环境、开发过程(生成&调试&测试)、发展趋势
文章目录
- 一 单片机的C语言简述
- 1、为什么要用C语言?
- 2、单片机的C语言怎么学?
- 之一:变量定义
- 之二:特殊寄存器操作
- 之三:位操作
- 之四:中断
- 之五:内部函数(intrinsic Function)
- 之六:函数的可重入性
- 讨论与总结:
- 二 程序设计规范
- 1、什么要学习程序设计规范?
- 2、程序规范的基本原则?
- (一)编程风格
- (二)可移植性
- (三)版本管理、可配置
- 三 前后台多任务程序设计
- (一)前后台程序的基本概念
- (二)前后台程序的编写基本原则
- (1)任何一个任务都不能阻塞CPU。
- (2)关注函数重入问题
- (3)临界代码保护(Critical Code Protection)
- (4)临界代码的保护方法
- 讨论与总结
- 四 FSM:状态机建模
- 1、什么是状态机?
- 2、为什么要引入状态机?
- 3、流程图无法描述的软件行为举例
- 4、引入“状态机” 的优势
- 5、状态机的表示方法:状态转移图
- 6、由状态转移图生成代码
- 7、状态机建模举例
- 五 模块化程序设计
- 1、模块化程序的原则
- 2、键盘模块化程序
- 3、带长短键识别的键盘模块化程序
- 4、串口收发模块化程序
- 5、数码管显示模块化程序
- 6、时钟和日历程序
- 六 事件触发多任务程序设计
- 1、什么是事件触发程序?
- 2、事件触发程序的程序架构
- 3、事件触发程序设计范例
- 七 时间触发系统
一 单片机的C语言简述
1、为什么要用C语言?
理由1:大幅减少编程劳动量
理由2:代码的通用性
- C语言可以为世界上任何一款处理器编程 (包括所有单片机)
- 在任何一款单片机上开发的程序,都可以移植到别的处理器上。
- 代码的模块化、可复用性 => 减少重复劳动、加快开发速度。
2、单片机的C语言怎么学?
PSoC Creator集成开发环境所配备的PSoC 5LP编译器为GCC-ARM(GNU Compiler Collection for ARM Embedded Processors),是一款免费的C/C++编译器。其文档位于图中gcc目录内。
C预处理器、链接器、汇编器、调试器、函数库等相关软件工具的文档位于图中的pdf目录及其子目录内。
之一:变量定义
处理器、编译器类型的不同,在变量定义上与VC略有不同。
几个特殊关键词的含义:
const:定义常量。const关键字定义的常量被放在ROM中,常用于定义如系数表、显示段码表等。
static :相当于本地全局变量,在函数内使用,可以避免全局变量使用混乱。
volatile :定义“挥发性”变量。编译器将认为该变量的值会随时改变,对该变量的任何操作都不会被优化掉。
之二:特殊寄存器操作
结论:
(1)特殊寄存器经过定义之后,可以像变量一样操作。
(2)包含单片机头文件,就已经完成了变量定义工作。
之三:位操作
结论:若处理器不支持单个位的操作,则要用左边的方式实现。
之四:中断
结论:(1)C语言将中断作为特殊函数 (2)该函数由中断机制调用
之五:内部函数(intrinsic Function)
结论:
(1)内部函数是将该款单片机的一些特殊指令做成函数形式
(2)在使用前,要包含内部函数头文件。
之六:函数的可重入性
结论:
(1)低端单片机因资源少,函数用静态传递,不可重入。
(2)中高端单片机编译器,函数用栈传递,允许重入。
讨论与总结:
1、即使是初学者,也完全可以在不深入了解汇编指令系统的情况下直接开始C语言开发
2、任何一款处理器的C语言,都是在标准C语言 (ANSI-C) 上增加对相应处理器的特殊操作而构成。
3、C编译器的表现通常非常优秀。如果没有几年的手工汇编经验3很难写出比C编译器更高效的代码。
4、如果在编程过程中将硬件差异集中到某个很小的局部,整个程序通过很简单的修改就能编译成另一CPU的机器码。这就是所谓的“代码移植”,是嵌入式软件设计最重要的思想之一。
5、在一开始就树立可移植性、模块化的概念,养成好的编程习惯,这正是下一部分对减少重复劳动、加快开发速度有很大帮助的主要目的。
二 程序设计规范
1、什么要学习程序设计规范?
1、程序不仅要被计算机执行,还要给程序员阅读,以及被今后重复使用。
2、一个风格清爽而严谨、规范化的程序更容易被读懂,更容易被修改、排错和移植。
3、规范化编程、高度一致的风格和正确的习惯还有助于保持思维清晰,写出正确无误的代码。特别是一个开发团队共同工作时,规范一致化的编程尤其重要
4、每个初学者在项目初期都会因为不良编程习惯浪费大量时间,因此若能在开始写程序时就重视规范化问题,对顺利渡过提高阶段有很大帮助。
2、程序规范的基本原则?
原则:
(1)一致的代码风格,统一的变量名、函数名命名规则
(2)符合英语语法、可阅读的代码
(3)关键代码100%注释
(4)硬件有关、硬件无关代码分离,可移植性强
(5)每个对象都具有完善的封装,接口形式简洁、易用
(6)对所有资源严格要求硬件隔离层,软件中不允许直接访问硬件,不允许跨层调用。
(一)编程风格
(二)可移植性
1)消除CPU差异
若希望自己写的程序在不同处理器上运行,首先需要了解这些处理器的不同之处。其中包括硬件的不同以及特殊语法的差异。例如MSP430单片机没有位操作指令,8051单片机有,若两者之间程序需相互移植,首先需要消灭这个差异。
2)消除硬件差异
更改数码管硬件布线,无需重写段码表
3)封装
封装是指将软/硬件对象的属性与行为绑定在一起,并放置在一个模块单元内。该单元负责将所描述的属性隐藏起来,外界对其内部属性的所有访问只能通过提供的应用程序接口实现。
初学者常见问题:
4)应用程序接口(API)
接口是对软/硬件对象的抽象,对外提供的服务和访问功能,
即:对象封装后对外呈现成什么样?
原则:(1)使用方便(2)功能丰富(3)但不要过多过滥
【例】为液晶显示模块规划应用程序接口:
5)软件层次
例:菜单程序
(1)杜绝跨层调用
(2)功能模块可以任意拼接、组合
(3)要有硬件隔离层(HAL)
(4)更换任意层代码,上下层建筑不变
(三)版本管理、可配置
(1)尽可能减少软件副本
(2)类似的产品/设计尽可能公用同一代码
(3)利用宏定义配置功能
(4)永远在最新的代码上开发,保留最新版
三 前后台多任务程序设计
(一)前后台程序的基本概念
任务(Task) :指完成某一单一功能的程序
后台程序:对时间要求不严格的任务,通常在主循环内执行
前台程序:要求快速响应或者时间严格的任务,通常中断内
队列/缓冲:用于暂存后台来不及处理的前台事件
(二)前后台程序的编写基本原则
(1)任何一个任务都不能阻塞CPU。
每个任务都应主动结束,让出CPU。不能有等待、死循环、长延时等环节(对初学者来说是难点!)
(2)关注函数重入问题
可重入(Reentrancy):函数在自己调用自己的时候,不必担心数据被破坏。
从软件工程角度对函数可重入的作用可以解释为:具有可重入性的函数能够被多个任务同时调用。在后台程序中,任务都是顺序执行的,不存在多个任务同时调用一个函数的情况。但可能出现前台中断服务程序与后台任务同时调用某个函数。
因此对于前后台公用函数,必须是可重入的。
【例】:
(3)临界代码保护(Critical Code Protection)
临界代码:运行时不可分割的代码,这部分代码不允许被中断打断。
1) 依靠软件产生时间严格时序的程序段。
/***************************************************
*名称: Pulse 10us()
*功能: 产生10us脉冲
*入门参数: 无
**************************************************/
void Pulse_10us()
{
_DINT(); // -------以下是临界代码区,不允许被中断
IO=1; // 置高
_delay_cycles(8); // 高电平持续10us (O赋值身耗2us)
IO=0; //置低
_EINTO0; // -------以上是临界代码区-----------
}
2)共享资源互斥性造成的临界代码
程序中任何可被占用的实体都称为“资源”。资源可以是硬件设备,如定时器、串口、打印机、键盘、LCD显示器等,也可以是变量、数组、队列、等数据。
被一个以上任务所占用的资源叫做“共享资源”。共享资源若不能同时被多任务占用,则具有“互斥性”(Mutual Exclusive)。
【例】:前后台同时调用液晶显示函数造成花屏
(4)临界代码的保护方法
- 关中断、开中断
隐患: 如果在A函数的临界代码区调用了另一个函数B,B函数内也有临界代码区,从B函数返回时,中断被打开了,这将造成A函数后续代码失去临界保护。所以,使用该方法时,不能在临界代码区调用任何具其他有临界代码的函数! - 利用硬件栈保存中断使能状态
优点:不影响函数调用
缺点:很多单片机的C语言不支持操作硬件栈 - 利用变量保存中断使能状态
缺点:每一段临界代码要消耗2-4字节内存 - 利用模拟栈保存中断使能状态
缺点:速度慢
典型错误1:在临界代码区调用其他临界代码
A()
{
int_Disable();
B(); // 这期间中断是开的,所执行的代码不被临界保护
int_Enable();
}
B()
{
int_Disable();
// ...
int_Enable();
}
典型错误2:在主函数和中断中都调用LCD操作函数
- LCD操作耗时长,不应放在中断中。
- LCD操作是临界代码,LCD是互斥性共享资源,LCD操作函数都是不可重入的,如果主函数里没有做临界代码保护,结果就是没多久屏幕就花了或呈死机效果。
讨论与总结
- 后台中任务顺序执行。每个后台任务中的内存(局部变量)在任务结束后可以全部释放,让给下一个任务使用。即使在RAM很少的处理器上也能同时执行众多任务。
- 后台中任务顺序执行。天然避免了后台任务资源互斥问题,但仍需考虑前后台之间的资源互斥问题。
- 前后台程序的结构灵活,实现形式与实现手段多样,是使用最广的程序结构,但缺乏架构标准,维护、升级、排错都很困难。
- 必须要程序员自己来判断和处理临界代码的隐患。
- 程序多任务的执行依靠每个任务的非阻塞性来保证,是编程最大的难点,下一节将介绍的FSM将是解决这一问题的利器!
四 FSM:状态机建模
1、什么是状态机?
有限状态自动机(Finite-State Machine, FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
2、为什么要引入状态机?
流程图的缺陷:
1、不能用于描述“任何时候”发生的事件
2、无法描述“由外部事件决定流程”的程序
3、不适合描述带有阻塞的并发过程
4、难以描述大量的独立事件。
需要:能够描述并发结构的软件,能从行为的角度来描述软件,且能够根据模型生成代码,也能够对软件进行完整测试的新手段—状态机模型。
3、流程图无法描述的软件行为举例
(1)不能用于描述“任何时候”发生的事件
(2)不能用于描述由外部事件决定流程的事件
【例】:电子表有两个按键,但按键顺序完全由外部决定
(3)不适合描述带有阻塞的并发过程
【例】:一个单片机同时控制2台电动机完成运转时序
(4)不适合描述大量的独立事件
【例】:投币式自动咖啡机程序
流程图:投币->选咖啡->出咖啡->结束
然 而…
-
如果没有放咖啡杯就选择咖啡,会发生什么情况?
-
如果咖啡杯未满之前,用户取走了咖啡杯,会发生什么情况?
-
如果咖啡灌了一半时,用户取走咖啡杯,再放回,能否把剩下一半继续加满?
-
如果咖啡灌了一半时,用户取走咖啡杯,不再放回,剩下一半是否会加到下一用户杯中?
-
如果咖啡杯未满之前,用户取走了咖啡杯,然后按退币扭,该如何处理?
-
如果还没有完成一次点单之前,用户再次投币,会发生什么情况?
-
如果一杯咖啡尚未加满时,用户又点了另一种咖啡,会发生什么情况?
-
如果用户一次投入两杯咖啡的钱,该怎样处理?
-
如果咖啡存储容器已空,用户点单时能否正确提示?
-
如果咖啡杯未满之前,咖啡机空了,能否给用户退款?
…
4、引入“状态机” 的优势
上面这些例子中,软件下一步要执行的功能不仅与外界入信息有关,还与系统的“当前状态”有关。状态机就是一种基于“状态”与“事件”的软件描述手段!其优势在于:
(1) 能够处理并发。由于系统在每一时刻只能有唯一的状态,在每一个状态下,可能发生的事件也是有限的。因此系统中即使存在有大量的独立事件,软件描述也会简单得多。
(2) 能够消除阻寒。真正需要CPU处理的只有系统状态发生改变的那一刻,在系统等待事件到来的期间,是不需要CPU处理的。如果能够用状态与事件的形式来描述软件,能够将CPU从等待事件发生的过程中解放出来,从而生成无阻塞的代码。
(3) 能够降低系统的复杂度,提高可测性
5、状态机的表示方法:状态转移图
圆圈:状态 (有限个)
有向箭头:状态转移
Event事件:事件发生时将触发状态转移
Action动作:状态转移的同时执行动作
6、由状态转移图生成代码
方法1、在状态中判断事件(事件查询)
switch(State) //根据当前状态决定程序分支
{
case S0: //在s0状态
if(Event0)
{ //如果查询到Event0事件
Action0();
}
else if(Event1)
{ //如果查询到Event1事件
Action1();
State=S1;
}
else if(Event2)
{ //如果查询到Event2事件
Action3();
State=S2;
}
break;
case S1: //在s1状态
if(Event2)
{ //如果查询到Event2事件
Action2();
State=S2;
}
break;
......
方法2、在事件中判断状态(事件触发)
7、状态机建模举例
例1 电子表程序
====A键中断====
switch(Status) /*根据当前状态处理A键所引发的状态跳跳转*/
{
case DISP_TIMME: Status = DISP_DATE;break://时间显示时按A键,显示日期
case DISP_DATE: Status = DISP_SEC; break;
case DISP_SEC: Status= DISP_TIME; break;
case SET_HOUR: if(++Hour>23) Hour=0; break; //小时设置时按A键调整小时case SET_MINUTE: if(++Min>59) Min=0; break;
case SET_MONTH: if(++Month>12) Month=1; break;
case SET_DATE: if(++Date>31) Date=0; break;
}
====B键中断=====
switch (Status) /*/根据当前状态处理B键所引发的状态跳转/*/
{
case DISP_TIME; Status= SET_HOUR; break; //时间显示时按B键,设置小时
case DISP_DATH; Status= SET_HOUR: break; //日期显示时按B键,设置小时
case DISP_SEC; Second 0; break; //秒钟显示时按B键,秒归零
case SET_HOUR; Status= SET_MINUTE; break; //小时设置时按B键,分钟设置
case SET_MINUTE; Status= SET_MONTH; break; //分钟设置时按B键月设置
case SET_MONTH; Status= SET_DATE; break; //月设置时按B键,日设置
case SET_DATE; Status= DISP_TIME; break; //日设置时按B键,显示时间
}
例2 长短按键识别
例3 事件序列匹配
五 模块化程序设计
1、模块化程序的原则
- 非阻寒性 不能长时间占用CPU,更不死等待某个事件的发生,尽快地让出CPU,供后续任务执行。
- 硬件隔离 屏蔽硬件特征,起到硬件隔离层的作用。上层软件将不涉及直接硬件操作。
- 模块独立性 各个程序库之间没有代码关联性(没有互相调用和隶属关系),在使用时,各个模块文件可以任意拼接、组合、拆分。
- 时间独立性 使用缓冲区、事件队列等手段,对时间关联性强的事件进行斩存和缓冲,使之可以被异步处理。
- 可移植性 代码都按照标准C的规范,很容易的移植到其他处理器上,或者其它硬件平台上。
- 100%注释 注释关键代码的功能或设计意图,以及每个函数的功能.入口出口参数、说明及注意事项,使用范例等。
- 开放性 方便日后修改、添加新的功能函数。
2、键盘模块化程序
3、带长短键识别的键盘模块化程序
4、串口收发模块化程序
5、数码管显示模块化程序
6、时钟和日历程序
六 事件触发多任务程序设计
1、什么是事件触发程序?
任务全部在中断内完成,主程序休眠。
可以看做前后台程序中,只有前台任务。
特点:1)实时性较好,事件响应较快;2)低功耗
2、事件触发程序的程序架构
中断:获取原始的事件信息。
事件引擎:判断何种事件发生,并调用相应处理程序。
(一个中断有多个中断源时)
事件处理程序:只负责某个事件的处理,要求非阻塞。
3、事件触发程序设计范例
有3个按键 (S1、S2、S3),按下为低电平。外部输入电压从ADC输入。设计一款超低功耗的电压表,要求用事件触发结构实现下列功能:
- 实现电压测量功能,每秒刷新显示2次电压测量值。
每次测量值由ADC连续采样4次求平均得到。 - 按下S1键时,斩停采样与显示刷新(保持功能)
- 按下S2键时,恢复采样与刷新。
- 按下S3键时,将采样数据从串口发出。
第3步:编写5个事件处理程序,此时并不关心事件如何发生如何被检测到,以及按什么顺序发生
第4步:测试。按照预先设定的顺序,依次调用5个事件处理程序(并不需要硬件上真的发生事件),模拟事件发生,即可进行代码测试。
第5步:编写事件引擎,在中断内检测和判判断事件,并分别调用相应的事件处理程序。
七 时间触发系统
时间触发系统可以理解为一种特殊的事件触发系统,但它另有自己的特点。
例子:一个医生,必须在一些护理人员的帮助下,通宵照顾十个危重病人,方案可以是
● 安排护理人员在某个病人出现严重问题时唤醒他
——事件触发
● 每小时闹铃,闹铃响时起床去探访每个病人
——时间触发
以每小时的间隔探访病人,医生能在严重并发症出现之前探访每个病人并安排合理的治疗。另外,工作量在整个晚上平摊。于是,所有病人都能平安地度过这个夜晚。
而前一种方式可能会出现护理人员在唤醒医生时,有多个病人都出现了严重并发症,需要依次做手术,后面的病人有可能来不及得到手术治疗。
这个医院的例子在事件驱动的系统中,即可能同时发生几个事件。需要处理同时发生的多个事件不但增加了系统复杂性,而且降低了对事件触发系统在所有情况下的行为做出预计的能力。
相比而言,在时间触发的嵌入式系统中,设计人员能够通过仔细安排可控的顺序,保证一次只处理一个事件。
许多嵌入式系统并不需要睡眠,采用时间触发方法有很多好处,有助于改善可靠性和安全性(如广泛应用于航空航天工业和汽车工业),主要原因在于系统的行为可以预计。
时间触发方法还能降低CPU的负荷,并减少存储器的使用量。
嵌入式系统需要执行的任务分为两种:
● **周期性任务**,比如每100ms执行一次
● **单次任务**,如在50ms的延迟后执行一次
考虑方案1:
void main(void)
{
Init_System();
while(1)
{
X(); // 执行任务(耗时10ms)
Delay_90ms(); // 延迟90ms
}
}
需要知道任务X的精确运行时间
这个运行时间永不变化—— 不可行
考虑方案2:
基于定时器中断,在一定的时刻调用函数
如果要支持多个任务(这些任务一般具有不同的运行时间并以不同的时间间隔运行),可以每个任务都使用一个定时器。
但:浪费硬件资源(或者不够用)、硬件的维护增加、会同时产生多个定时器中断。——不合理
解决方案:使用调度器
● 调度器可以看作是一个简单的操作系统,允许以周期性或单次方式来调用任务。
● 从底层的角度来看,调度器可以看作是一个由许多不同任务共享的定时器中断服务程序。
用调度器来调度三个任务的执行:
void main(void)
{
SCH_Init(); // 设置调度器一次
/*---- 增加各任务至任务队列(时间分辨率为1ms)----*/
// Function_A将每隔2ms运行一次
SCH_Add_Task(Function_A, 0, 2);
// Function_B将每隔10ms运行一次
SCH_Add_Task(Function_B, 1, 10);
// Function_C将每隔15ms运行一次
SCH_Add_Task(Function_C, 3, 15);
SCH_Start(); // 启动调度
while(1)
{
// 按调度算法执行任务队列中各任务
SCH_Dispatch_Tasks();
}
}
调度器的分类
● 合作式调度器
● 抢占式调度器
合作式调度器:
● 任务在特定的时刻被调度运行(以周期性或单次方式)
● 当任务需要运行时,被添加到等待队列
● 当CPU空闲时,运行等待任务中的下一个(如果有的话)
● 前一任务运行直到完成,才轮到下一个等待任务
● 简单,用少量代码即可实现
● 一次只为一个任务分配存储器
● 通常完全由高级语言如C实现
● 小心设计可以实现快速响应外部事件
● 简单、可预测、可靠并且安全
抢占式调度器:
● 任务在特定的时刻被调度运行(以周期性或单次方式)
● 当任务需要运行时,被添加到等待队列
● 等待的任务运行一段固定的时间,如果没有完成,将被暂停并放回到等待队列。然后下一个等待任务(如果有的话)将运行一段固定的时间。
● 高优先级的任务可以抢占低优先级任务优先运行
● 复杂,“并行处理的”任务试图访问公用资源时需避免冲突等
● 必须为被抢占任务的所有中间状态分配存储器
● 通常(至少是部分)由汇编语言编写
● 对外部事件响应快
● 与合作式调度比,通常认为其更不可预测、并且可靠性较低
混合式调度器:
● 支持多个合作式调度的任务
● 支持一个抢占式任务(可以“中断”合作式任务)