嵌入式系统C语言编程——背景
嵌入式系统C语言编程修炼之道——软件架构                                                                     1.模块划分
2.多任务还是单任务
3.单任务程序典型架构
4.中断服务程序
5.硬件驱动模块
6.C的面向对象化...
总结... 10
嵌入式系统C语言编程道——内存操作                                                                                1.数据指针                                                                                                                                 2.函数指针                                                                                                                                3.数组vs.动态申请                                                                                                                     4.关键字const
5.关键字volatile.
6.CPU字长与存储器位宽不一致处理
总结
嵌入式系统C语言编程——屏幕操作
1.汉字处理                                                                                                                                2.系统时间显示                                                                                                                         3.动画显示
4.菜单操作                                                                                                                                 5.模拟MessageBox函数总结
嵌入式系统C语言编程——键盘操作篇                                                                                1.处理功能键                                                                                                                            2.处理数字键                                                                                                                            3.整理用户输入
总结
嵌入式系统C语言编程修炼之道——性能优化
1.使用宏定义
2.使用寄存器变量
3.内嵌汇编
4.利用硬件特性
5.活用位操作
总结
 
 
嵌入式系统C语言编程——背景篇
不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势必要求其编程语言具备较强的硬件直接操作能力。无疑,汇编语言具备这样的特质。但是,归因于汇编语言开发过程的复杂性,它并不是嵌入式系统开发的一般选择。而与之相比,C语言——一种“高级的低级”语言,则成为嵌入式系统开发的最佳选择。笔者在嵌入式系统项目的开发过程中,一次又一次感受到C语言的精妙,沉醉于C语言给嵌入式开发带来的便利。本文的目的在于进行“C语言嵌入式系统开发的内功心法”秀,一共包括25招。
图1给出了本文的讨论所基于的硬件平台,实际上,这也是大多数嵌入式系统的硬件平台。它包括两部分:
(1)     以通用处理器为中心的协议处理模块,用于网络控制协议的处理;
(2)     以数字信号处理器(DSP)为中心的信号处理模块,用于调制、解调和数/模信号转换。
本文的讨论主要围绕以通用处理器为中心的协议处理模块进行,因为它更多地牵涉到具体的C语言编程技巧。而DSP编程则重点关注具体的数字信号处理算法,主要涉及通信领域的知识,不是本文的讨论重点。
着眼于讨论普遍的嵌入式系统C编程技巧,系统的协议处理模块没有选择特别的CPU,而是选择了众所周知的CPU芯片——80186,每一位学习过《微机原理》的读者都应该对此芯片有一个基本的认识,且对其指令集比较熟悉。80186的字长是16位,可以寻址到的内存空间为1MB,只有实地址模式。C语言编译生成的指针为32位(双字),高16位为段地址,低16位为段内编译,一段最多64KB。
图1  系统硬件架构
协议处理模块中的FLASH和RAM几乎是每个嵌入式系统的必备设备,前者用于存储程序,后者则是程序运行时指令及数据的存放位置。系统所选择的FLASH和RAM的位宽都为16位,与CPU一致。
实时钟芯片可以为系统定时,给出当前的年、月、日及具体时间(小时、分、秒及毫秒),可以设定其经过一段时间即向CPU提出中断或设定报警时间到来时向CPU提出中断(类似闹钟功能)。
NVRAM(非易失去性RAM)具有掉电不丢失数据的特性,可以用于保存系统的设置信息,譬如网络协议参数等。在系统掉电或重新启动后,仍然可以读取先前的设置信息。其位宽为8位,比CPU字长小。文章特意选择一个与CPU字长不一致的存储芯片,为后文中一节的讨论创造条件。
UART则完成CPU并行数据传输与RS-232串行数据传输的转换,它可以在接收到[1~MAX_BUFFER]字节后向CPU提出中断,MAX_BUFFER为UART芯片存储接收到字节的最大缓冲区。
键盘控制器和显示控制器则完成系统人机界面的控制。
以上提供的是一个较完备的嵌入式系统硬件架构,实际的系统可能包含更少的外设。之所以选择一个完备的系统,是为了后文更全面的讨论嵌入式系统C语言编程技巧的方方面面,所有设备都会成为后文的分析目标。
嵌入式系统需要良好的软件开发环境的支持,由于嵌入式系统的目标机资源受限,不可能在其上建立庞大、复杂的开发环境,因而其开发环境和目标运行环境相互分离。因此,嵌入式应用软件的开发方式一般是,在宿主机(Host)上建立开发环境,进行应用程序编码和交叉编译,然后宿主机同目标机(Target)建立连接,将应用程序下载到目标机上进行交叉调试,经过调试和优化,最后将应用程序固化到目标机中实际运行。
CAD-UL是适用于x86处理器的嵌入式应用软件开发环境,它运行在Windows操作系统之上,可生成x86处理器的目标代码并通过PC机的COM口(RS-232串口)或以太网口下载到目标机上运行,如图2。其驻留于目标机FLASH存储器中的monitor程序可以监控宿主机Windows调试平台上的用户调试指令,获取CPU寄存器的值及目标机存储空间、I/O空间的内容。
图2  交叉开发环境
后续章节将从软件架构、内存操作、屏幕操作、键盘操作、性能优化等多方面阐述C语言嵌入式系统的编程技巧。软件架构是一个宏观概念,与具体硬件的联系不大;内存操作主要涉及系统中的FLASH、RAM和NVRAM芯片;屏幕操作则涉及显示控制器和实时钟;键盘操作主要涉及键盘控制器;性能优化则给出一些具体的减小程序时间、空间消耗的技巧。
本文即将讲述的25个主题可分为两类,一类是编程技巧,有很强的适用性;一类则介绍嵌入式系统编程的一般常识,具有一定的理论意义。
So, let’s go.
 
嵌入式系统C语言编程——软件架构篇
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语言编程时程序整体框架结构分散、数据和函数脱节的问题。我们在后续章节会看到这样的例子。
总结
本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。
请记住:软件结构是软件的灵魂!结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。
一个高尚的程序员应该是写出如艺术作品般程序的程序员。
 
C语言嵌入式系统编程修炼之道——内存操作篇
1.数据指针
在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV指令,而除C/C++以外的其它编程语言基本没有直接访问绝对地址的能力。在嵌入式系统的实际调试中,多借助C语言指针所具有的对绝对地址单元内容的读写能力。以指针直接操作内存多发生在如下几种情况:
(1)   某I/O芯片被定位在CPU的存储空间而非I/O空间,而且寄存器对应于某特定地址;
(2)   两个CPU之间以双端口RAM通信,CPU需要在双端口RAM的特定单元(称为mail box)书写内容以在对方CPU产生中断;
(3)   读取在ROM或FLASH的特定单元所烧录的汉字和英文字模。
譬如:
unsigned char *p = (unsigned char *)0xF000FF00;
*p=11;
以上程序的意义为在绝对地址0xF0000+0xFF00(80186使用16位段地址和16位偏移地址)写入11。
在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的数据类别。上例中p++后的结果是p= 0xF000FF01,若p指向int,即:
int *p = (int *)0xF000FF00;
p++(或++p)的结果等同于:p = p+sizeof(int),而p—(或—p)的结果是p = p-sizeof(int)。
同理,若执行:
long int *p = (long int *)0xF000FF00;
则p++(或++p)的结果等同于:p = p+sizeof(long int) ,而p—(或—p)的结果是p = p-sizeof(long int)。
记住:CPU以字节为单位编址,而C语言指针以指向的数据类型长度作自增和自减。理解这一点对于以指针直接操作内存是相当重要的。
2.函数指针
首先要理解以下三个问题:
(1)C语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;
(2)调用函数实际上等同于“调转指令+参数传递处理+回归位置入栈”,本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的PC寄存器;
(3)因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可以“调用”一个根本就不存在的函数实体,晕?请往下看:
请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到,186 CPU启动后跳转至绝对地址0xFFFF0(对应C语言指针是0xF000FFF0,0xF000为段地址,0xFFF0为段内偏移)执行,请看下面的代码:
typedef  void  (*lpFunction) ( );    /* 定义一个无参数、无返回类型的 */
/* 函数指针类型 */
lpFunction lpReset =(lpFunction)0xF000FFF0;    /* 定义一个函数指针,指向*/
/* CPU启动后所执行第一条指令的位置 */
lpReset();                           /* 调用函数 */
在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用:lpReset(),它实际上起到了“软重启”的作用,跳转到CPU启动后第一条要执行的指令的位置。
记住:函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令!
3.数组vs.动态申请
在嵌入式系统中动态内存申请存在比一般系统编程时更严格的要求,这是因为嵌入式系统的内存空间往往是十分有限的,不经意的内存泄露会很快导致系统的崩溃。
所以一定要保证你的malloc和free成对出现,如果你写出这样的一段程序:
char * function(void)
{
  char *p;
  p = (char *)malloc(…);
  if(p==NULL)
…;
  …                         /* 一系列针对p的操作 */
return p;
}
在某处调用function(),用完function中动态申请的内存后将其free,如下:
char *q = function();

free(q);
上述代码明显是不合理的,因为违反了malloc和free成对出现的原则,即“谁申请,就由谁释放”原则。不满足这个原则,会导致代码的耦合度增大,因为用户在调用function函数时需要知道其内部细节!

转载于:https://blog.51cto.com/2636976/474988

嵌入式C语言基础教程一相关推荐

  1. 嵌入式C语言基础(一)

    嵌入式C语言基础: system函数:功能是运行windows命令 #include <stdio.h> #include <stdlib.h> int main() {//m ...

  2. 嵌入式C语言基础知识梳理

    该图是关于C语言基础知识的树状结构图,也许里面有些地方看起来不够主流,但是可能更合理.以后将会对该图的所有细节知识点以通俗易懂的方式逐个梳理,敬请关注!

  3. web前端开发基础教程一

    网页: 网站是指因特网上根据一定的规则,使用HTML等制作的用于展示特定内容相关的网页集合. 网页是网站中的一页,通常是HTML格式的文件,他要通过浏览器来阅读. 网页是构成网站的基本元素,它通常由图 ...

  4. 【wordpress基础教程一】:wordpress简介和安装

    一.简介        WordPress是一款免费开源的个人博客系统,但随着版本的不断更新和更多开发者的介入,wordpress已逐渐演变成一款小型的CMS系统,使用它可以制作越来越多类型的网站.目 ...

  5. C语言基础教程之enum

    枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读. 枚举语法定义格式为: enum 枚举名 {枚举元素1,枚举元素2,--}; 接下来我们举个例子,比如:一星期有 7 天,如果不用枚举 ...

  6. C语言简单教程一:编程的第一课

    今天将带大家走进C语言的世界一个神秘又充满乐趣的世界,接下来将给大家一些指导关于获取编译器,建立第一个自己的程序. 首先是准备C语言编程的编译器 建立第一个关于自己的程序 学习C语言的方法与习惯 C语 ...

  7. Ogre3D基础教程一

    文档:教程:基础教程:基础教程一 出自Ogre3D开放资源地带 跳转到: 导航, 搜索 目录 [隐藏] 1 简介 2 从这里开始 2.1 错误排除 2.1.1 编译错误 2.1.2 运行失败 3 OG ...

  8. Directx11基础教程一之Directx11框架

    首先,我已经有一定的D3D11的基础,<Introduction to 3D Game Programming with Direct3D 11>,我已经具备D3D11入门水平了,我决定用 ...

  9. IDAPython基础教程一

    给出的文件名为rabbithole 首先使用file命令查看一下 可以看到是64位的可执行文件 接下来我们切换到win,使用IDApro载入,以此文件为样例,学习IDAPython的用法. 首先介绍下 ...

最新文章

  1. php 缓存模块,PHP缓存之模块缓存(APC)_PHP教程
  2. HTML !DOCTYPE 标签
  3. CentOS环境下tomcat启动超级慢的解决方案
  4. 《面向对象程序设计课程学习进度条》
  5. android 环形时间显示_使用Arduino构建OLED显示屏与Android手机接口的智能手表
  6. linux mysql数据库日志关闭,linux 怎样恢復mysql数据库日志
  7. 差分隐私学习路线【定期更新】
  8. Windows8在激烈竞争的平板电脑市场的优势与特色---移动3G时代失落的windows开发人员的福音...
  9. 安装Ubuntu系统后的配置工作
  10. .net 集合分成几个等数量集合_一课研究之集合图的应用教学设计
  11. jdbc连接带密码的access数据库
  12. 射频微波天线知识点整理
  13. android系统黑点bug,老外实测“小黑点”死机短信:iPhone安卓都中招 解决方法很简单...
  14. C语言ctype.h
  15. windows10操作系统开启以及关闭测试模式
  16. keyshot怎么贴logo_KeyShot实例渲染技巧教程,教你如何给产品添加有织纹的Logo
  17. 2017年英语六级翻译
  18. js任意进制转换(二进制,八进制,十进制...三十六进制)
  19. 抖音电商直播间SOP主播工作计划脚本话术模板方案
  20. 软件质量与测试--第四周作业 wcPro

热门文章

  1. java代码段替换,java-片段添加或替换不起作用
  2. 中gcd函数_欧拉函数φ(n)的计算及欧拉定理
  3. 三级网络技术刷题笔记
  4. 高速串行总线系列(2)高速串行总线技术总览
  5. 【SRIO】4、Xilinx RapidIO核详解
  6. 基带信号传输之信道均衡
  7. PyCharm安装mysqlclient一直提示MS Visual C++ 14 required
  8. 10-GLBP Weighting //2.1.5(GNS3版本,后面都是如此注明)
  9. PM2管理node.js
  10. elasticsearch 的filter cache (search 技术的冰山一角)