当前位置:首页 > 编写
  • 编写可移植 C/C  程序的一些要点

    ↓推荐关注↓ 以前做过两年C 程序移植工作,从Win32平台移植到Linux平台。大约有上百万行C/C 代码,历时一年多。 在开发Win32版本时,已经强调了程序的可植性,无奈Win32团队里对Linux精通的人比较少,很多问题没有想到,直到后来移植工作开始时,才发现移植并非像想的那样简单。 后来,我发现大家对移植工程师都比较轻视,不管是从工资待遇还是管理层的态度来看都是这样。他们往往认为,你们不过是把别人实现好的东西移植过去罢了,你老老实实,按步就班去做就行了,根本不需要丝毫创意。事实并非如此,特别是对于大项目,其中遇到的问题和困难可谓一言难尽。比如前面提到的那个项目,虽然过去好几年了,很多问题我仍然记忆犹新。 这里总结一些经验吧,这些经验,无一不是经过大量汗水换来的,有的引起的BUG甚至耗费数周时间才查出来。写出来,供类似的项目参考,不用再走这些弯路。 1、分层设计,隔离平台相关的代码。 就像可测试性一样,可移植性也要从设计抓起。一般来说,最上层和最下层都不具有良好的可移植性。最上层是GUI,大多数GUI都不是跨平台的,如Win32 SDK和MFC。最下层是操作系统API,大多部分操作系统API都是专用的。如果这两层的代码散布在整个软件中,那么这个软件的可植性将非常的差,这是不言自明的。 那么如何避免这种情况呢?当然是分层设计了:最底层采用Adapter模式,把不同操作系统的API封装成一套统一的接口。至于封装成类还是封装成函数,要看你采用的C还是C 写的程序了。这看起来很简单,其实不尽然(看完整篇文章后你会明白的),它将耗去你大量的时间去编写代码,去测试它们。 采用现存的程序库,是明智的做法,有很多这样的库,比如,C库有glib(GNOME的基础类),C 库有ACE(ADAPTIVE Communication Environment)等等,在开发第一个平台时就采用这些库,可以大大减少移植的工作量。最上层采用MVC模型,分离界面表现与内部逻辑代码。把大部分代码放到内部逻辑里面,界面仅仅是显示和接收输入,即使要换一套GUI,工作量也不大。这同时也是提高可测试性的手段之一,当然还有其它一些附加好处。所以即使你采用QT或者GTK 等跨平台的GUI设计软件界面,分离界面表现与内部逻辑也是非常有用的。若做到了以上两点,程序的可移植性基本上有保障了,其它的只是技术细节问题。 2、事先熟悉各目标平台,合理抽象底层功能。 这一点是建立在分层设计之上的,大多数底层函数,像线程、同步机制和IPC机制等等,不同平台提供的函数,几乎是一一对应的,封装这些函数很简单,实现Adapter的工作几乎只是体力活。然而,对于一些比较特殊的应用,如图形组件本身,就拿GTK 来说吧,基于X Window的功能和基于Win32的功能,两者差巨大,除了窗口、事件等基本概念外,几乎没有什么相同的,如果不事先了解各个平台的特性,在设计时就精心考虑的话,抽象出来的抽口在另外一个平台几乎无法实现。 3、尽量使用标准C/C 函数。 大多数平台都会实现POSIX(Portable Operating System Interface)规定的函数,但这些函数较原生(Native) 函数来说,性能上的表现可能较次一些,用起来也不如原生函数方便。但是,最好不要贪图这种便宜而使用原生函数函数,否则搬起的石头最终会轧到自己的脚。比如,文件操作就用fopen之类的函数,而不要用CreateFile之类的函数等。 4、尽量不要使用C/C 新标准里出现的特性。 并不是所有的编译器都支持这些特性,像VC就不支持C99里面要求的可变参数的宏,VC对一些模板特性的支持也不全面。为了安全起见,这方面不要太激进了。 5、尽量不要使用C/C 标准里没有明确规定的特性。 比如你有多个动态库,每个动态库都有全局对象,而且这些全局对象的构造还有依赖关系,那你迟早会遇到麻烦的,这些全局对象构造的先后顺序在标准里是没有规定的。在一个平台上运行正确,在另外一个平台上可能莫明其妙的死机,最终还是要对程序作大量修改。 6、尽量不要使用准标准函数。 有些函数大多数平台上都有,它们使用得太广泛了,以至于大家都把它们当成标准了,比如atoi(把字符串转换成整数)、strdup(克隆字符串)、alloca(在栈分配自动内存)等等。不怕一万,就怕万一,除非明白你在做什么,否则还是别碰它们为好。 7、注意标准函数的细节。 也许你不相信,即使是标准函数,抛开内部实现不论,就其外在表现的差异也有时令人惊讶。这里略举几个例子: (1) int accept(int s, struct sockaddr *addr, socklen_t *addrlen);addr/ addrlen本来是输出参数,如果是C 程序员,不管怎么样,你已经习惯于初始化所有的变量,不会有问题。如果是C程序员,就难说了,若没有初始化它们,程序可能莫名其妙的crash,而你做梦也怀疑不到它头它。这在Win32下没问题,在Linux下才会出现。 (2)int snprintf(char *str, size_t size, const char *format, ...);第二个参数size,在Win32下不包括空字符在内,在Linux下包括空字符,这一个字符的差异,也可能让你耗上几个小时。 (3) int stat(const char *file_name, struct stat *buf);这个函数本身没有问题,问题出在结构stat上,st_ctime在Win32下代表创建(create)时间,在Linux下代表最后修改(change)时间。(4)FILE *fopen(const char *path, const char *mode);在读取二进制文件,没有什么问题。在读取文本文件可要小心,Win32下自动预处理,读出来的内容与文件实际都长度不一样,在Linux则没有问题。 8、小心数据标准数据类型。 不少人已经吃过int类型由16位转变成32位带来的苦头,这已经是陈年往事了,这里且不谈。你可知道char在有的系统上是有符号的,在有的系统是无符号的吗?你可知道wchar_t在Win32下是16位的,在Linux 下是32位的吗?你可知道有符号的1bit的位域,取值是0和-1而不是0和1吗?这些貌合神离的东东,端的是神出鬼没,一不小心着了它的道。 9、最好不要使用平台独有的特性。 比如Win32下DLL可以提供一个DllMain函数,在特定的时间,操作系统的Loader会自动调用这个函数。这类功能很好用,但最好不要用,目标平台可不能保证有这种功能。 10、最好不要使用编译器特有的特性。 现代的编译器都做很人性化,考虑得很周到,一些功能用起非常方便。像在VC里,你要实现线程局部存储,你都不调用TlsGetValue /Tls TlsSetValue之类的函数,在变量前加一个__declspec( thread )就行了,然而尽管在pthread里有类似的功能,却不能按这种方式实现,所以无法移植到Linux下。同样gcc也有很多扩展,是在VC或者其它编译器里所没有的。 11、注意平台的特性。 比如:在Win32下的DLL里面,除非明确指明为export的函数外,其它函数对外都是不可见的。而在Linux下,所有的非static的全局变量和函数,对外全部是可见的。这要特别小心,同名函数引起的问题,让你查上两天也不为过。 (1)目录分隔符,在Win32下用’//’,在Linux下用’/’。 (2)文本文件换行符,在Win32下用’/r/n’,在Linux下用’/n’,在MacOS下用’/r’。 (3)字节顺序(大端/小端),不同硬件平台的字节顺序可能不一样。 (4)字节对齐,在有的平台(如x86)上,字节不对齐,无非速度慢一点,而有的平台(如arm)上,它完全用错误的方式去读取数据,而且不会给你一点提示。若出问题,可能让你一点头绪都没有。12、最好清楚不同平台的资源限制。 想必你还记得DOS下同时打开的文件个数限制在几十个的情形吧,如今操作系统的功能已经强大多了,但是并非没有限制。比如Linux下的共享内存默认的最大值是4M。若你对目标平台常见的资源限制了然于胸,可能有很大的帮助,一些问题很容易定位。 可移植性的问题决不限于以上几种,一方面,即使以前遇到过的问题,部份已经忘记了。另外一方面,还有很多未知的问题,根本没有遇到过。这里算是抛砖引玉吧,请大家补充。

    时间:2021-09-23 关键词: 移植 编写

  • 干货|如何编写优质的嵌入式C程序

    ▼点击下方名片,关注公众号▼摘要:本文旨在向年轻的嵌入式软件工程师们介绍如何在裸机环境下编写优质嵌入式C程序。首先分析了C语言的陷阱和缺陷,对容易犯错的地方进行归纳整理;分析了编译器语义检查的不足之处并给出防范措施,以Keil MDK编译器为例,介绍了该编译器的特性、对未定义行为的处理以及一些高级应用。本文思维导图(已上传至gitee)1. 简介市面上介绍C语言以及编程方法的书数目繁多,但对如何编写优质嵌入式C程序却鲜有介绍,特别是对应用于单片机、ARM7、Cortex-M3这类微控制器上的优质C程序编写方法几乎是个空白。本文面向的,正是使用单片机、ARM7、Cortex-M3这类微控制器的底层编程人员。编写优质嵌入式C程序绝非易事,它跟设计者的思维和经验积累关系密切。嵌入式C程序员不仅需要熟知硬件的特性、硬件的缺陷等,更要深入一门语言编程,不浮于表面。为了更方便的操作硬件,还需要对编译器进行深入的了解。2. C语言特性语言是编程的基石,C语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步。虽然有众多书籍、杂志、专题讨论过C语言的陷阱和缺陷,但这并不影响本节再次讨论它。总是有大批的初学者,前仆后继的倒在这些陷阱和缺陷上,民用设备、工业设备甚至是航天设备都不例外。本节将结合具体例子再次审视它们,希望引起足够重视。深入理解C语言特性,是编写优质嵌入式C程序的基础。2.1处处都是陷阱2.1.1 无心之过1、 =和==将比较运算符”==”误写成赋值运算符”=”,可能是绝大多数人都遇到过的,比如下面代码:if(x=5)   {       //其它代码   }代码的本意是比较变量x是否等于常量5,但是误将==写成了=,if语句恒为真。如果在逻辑判断表达式中出现赋值运算符,现在的大多数编译器会给出警告信息。比如keil MDK会给出警告提示:warning: #187-D: use of "=" where"==" may have been intended,但并非所有程序员都会注意到这类警告,因此有经验的程序员使用下面的代码来避免此类错误:if(5=x)   {       //其它代码   }将常量放在变量x的左边,即使程序员误将==写成了=,编译器会产生一个任谁也不能无视的语法错误信息:不可给常量赋值!2、复合赋值运算符复合赋值运算符( =、*=等等)虽然可以使表达式更加简洁并有可能产生更高效的机器代码,但某些复合赋值运算符也会给程序带来隐含Bug,比如 =容易误写成= ,代码如下:tmp= 1;代码本意是想表达tmp=tmp 1,但是将复合赋值运算符 =误写成= :将正整数常量1赋值给变量tmp。编译器会欣然接受这类代码,连警告都不会产生。如果你能在调试阶段就发现这个Bug,真应该庆祝一下,否则这很可能会成为一个重大隐含Bug,且不易被察觉。复合赋值运算符-=也有类似问题存在。3、  其它容易误写使用了中文标点头文件声明语句最后忘记结束分号逻辑与

    时间:2021-09-13 关键词: 嵌入式C 编写

  • 最简单的程序怎么编写?(看三种单片机的闪烁灯程序套路

    了解一下小小的编程序怎么回事。

    时间:2018-12-28 关键词: 程序 闪烁灯 编写

  • MSP430的C语言编写注意事项

    微处理器一般用于特定环境和特定用途,出于成本、功耗和体积的考虑,一般都要求尽量节省使用资源,并且,由于微处理器硬件一般都不支持有符号数、浮点数的运算,且运算位有限,因此,分配变量时必须仔细。另外要说明的是,速度和存储器的消耗经常是2个不可兼顾的目标,在多数情况下,编程者必须根据实际情况作出权衡和取舍。 需要注意的事项如下: 1) 通常在满足运算需求的前提下,尽量选择为变量定义字节少的数据类型。比如最常用的 int和 char, int是 16 位的, char 是8位的,如果没有必要,不要使用 int,而且使用 char 也最好使用 unsigned char。运行时,可以在变量窗口看到,使用类型为unsigned char 的变量是 16 进制的格式,而使用 int 的是十进制格式,如果 char 没有定义为 unsigned,会出现负号,如果没有必要的话,在 430 中是不需要负数的。 2) 尽量不用过长的数据类型,如 long、long long和 double 3) MSP430的 C编译器不支持位寻址,所以运算中尽量减少位操作,对于只有“是”和“否”的变量,如果RAM 容量允许,则可分配为 unsigned char类型,可提高运算速度。如果分配为某字节的某个位,可以减少存储器的消耗,但是会降低运算速度 4) 避免使用浮点数,尽量使用定点数进行小数运算。如果必须使用浮点数,则尽量用 32 位的 float,而不是 64位的 double 5) 尽量将变量分配为无符号数据类型 6) 对于指针变量,如果声明后其值不再改变,则声明为 const 类型,这样编译器编 译时能更好的优化生成的代码 7) 尽可能的使用局部变量而非全局变量或者静态变量(static) 。这样有利于编译器编译时更好的优化生成的代码 8) 避免对局部变量使用 &取地址符。因为这样会使编译器无法把此变量放在 CPU的寄存器中,而是放在RAM中,从而失去了优化的机会 9) 仅在模块内使用的变量声明为 static,有利于优化代码 10) 如果堆栈空间有限,尽量减少函数调用的层次和递归调用

    时间:2013-10-17 关键词: MSP430 注意事项 语言 编写

  • 计算机喜剧演员:编写笑话有性别歧视 还不好笑

    爱丁堡大学科学家创造的这台电脑,最初的设计目的是为了让它遵循一些简单的规则创作它自己的幽默小笑话,电脑系统也被设定成利用最成功和最流行的喜剧系列之一进行编写。然而,与它的一些人类前辈一样,这台计算机喜剧演员已经形成了一种立场相当错误的幽默方法,它编写的笑话具有性别歧视。 这台计算机编写的幽默笑话具有性别歧视,而且并不好笑。 计算机软件被设定寻找不太可能的词语组合并且在它们之间进行联系。它编写的最常见的笑话就是将男人或者女人与其它的物体进行比较。虽然一些笑话能够使一位人类喜剧演员产生苦笑,但是其它的笑话并不好笑。爱丁堡大学信息学院的一位计算机科学家David Matthews帮助开发了这个计算机喜剧演员。他说道,当他们将这些笑话讲给志愿者听时,尽管这些笑话不如人类编写的幽默,但是他们笑了。 Matthews声称,想要改善这台计算机编写的笑话,它的计算机软件需要设计形成文化认知。他说道:“计算机比人类拥有更大的优势,它们能够处理大量的信息,因此我们提供给计算机大量的材料,它们能够根据我们的幽默模板从中提取创造性和独特性的词语组合。”

    时间:2013-08-08 关键词: 歧视 好笑 喜剧 编写

  • 日本电子:信奉真正的武士不会去编写软件

          真正的武士永远不会去编写软件。”这是在日本企业界流传甚广的一句话,因为硬件的斗争更像武士之间实打实的搏斗。 如果你想知道日本的电子企业现在的情况怎样,那就去搭一趟东京地铁吧。 东京地铁内是不允许打电话的,不过那里有3G信号,所以你会看见乘客都低着头忙着玩他们的3G设备。 他们用的是什么设备?粗略观察,一个车厢里八成的人都在用苹果的iPhone。 虽然这不是一个严谨的统计,但这个数据也足以说明某些问题。曾几何时,人人都在听索尼Walkman,但现在,苹果和三星才是主流,即便在索尼的老家也如此。 这种状况也在日本电子企业的财务报表上得到体现——这些巨头的财务数据都是血淋淋的:索尼今年或许会小赚一点,这将是2008年以来第一次盈利;松下预计今年亏损90亿美元;规模比起前面这些公司小许多的夏普,也在巨亏,如果没有一笔大量的现金注入,恐怕它很难再活多一年。 这些一度统治世界的日本消费电子巨头为何沦落至此,到底又是哪里出了问题? “数字革命”惹的祸 “日本电子巨头是被数字革命击倒的。”常住东京的经济学家格哈德·法索尔(Gerhard Fasol)说。 在格哈德看来,昔日这些巨头是在复杂的电器原件上建立起自己的王国的,比如彩电、收音机、录音机、冰箱、洗衣机,以及足以代表一个时代的CD、DVD播放机,但数字时代到来,世界变了。索尼Walkman就是个典型例子,它里面并没有软件,全是机械的。“现在的世道是你一定要有软件业务,和以前完全不一样了。” 数字革命不仅改变了电子设备的运作方式,也改变了它们的制作过程。随着生产转移到成本较低的国家,整个制造模式也发生了变化。这让日本制造商承受了巨大的压力。 “你看苹果公司,他们制造的iPod和iPhone的利润率至少有50%。人们都说苹果产品是在中国制造,但一台iPhone中国制造方可能只分得3%的利润。所以,像松下这种规模的公司,还是依赖制造,自然很难赚大钱。他们其实应该做得更多。”格哈德说。 与索尼和夏普不同,今年66岁的日立总裁中西宏明(Hiroaki Nakanishi)在2010年看见公司财务状况和国内同行一样惨烈时,做出了一个非常“不日本”的决定——关闭或出售亏损部门,其中多数都是消费电子产品。 “数字技术把一切都改变了。比如电视制造业,现在一个芯片就能够做到高清的显像质量,而且谁都能做到。这也意味着,在成本和价格上,韩国和中国的对手现在更有优势。”中西宏明说。 过去,日立曾凭借优秀的工艺在业内拥有着良好的口碑。但现在,角逐的战场已从制造工艺前移到谁拥有最佳的销售和市场策略,谁又有最高额的广告预算。中西宏明说,如今日本的企业都有些跟不上形势。 “整个行业的结构都变了,我们无法适应现在的环境,所以日立放弃了那些部门。”中西宏明说。与日立类似,东芝公司也在2012年5月宣布不再制造LCD电视,价格、全球竞争、汇率等都是原因之一。   “专守”文化成阻碍 《华尔街日报》认为,这些日本企业的问题,都出在他们早年的优点上:专守“monozukuri”(即制造的艺术),只追求硬件卓越。正所谓“成也萧何,败也萧何”。 日本的造物文化历来突出,社交游戏公司Gree创始人田中良和曾对媒体这样说过:“日本社会的整个身份认知是与制造业联系在一起的,如果你不生产实物产品,人们对你的态度,就好像你在做什么靠不住的事。” “真正的武士永远不会去编写软件。”这是在日本企业界流传甚广的一句话,因为硬件的斗争更像武士之间实打实的搏斗。这个理念,也让日本企业在介绍自己的电子产品时更喜欢强调最薄最小,却总是忽略一些对用户而言真正重要的因素,比如易操作性。 2004年,技术分析师麦克·盖丁伯格在日本出差时曾被索尼新推出的电子阅读器Librie吸引了好一阵,以为这款产品很快也会风靡美国,但结果并没有。这款产品的软件是日本做的,一定需要用电脑下载书籍,而且选择很有限。所以,更加开放、轻巧的亚马逊电子阅读器kindle后起而胜之,如今还有几个人记得Librie呢? 优秀的硬件和糟糕的用户体验,几乎可以概括日本电子企业近年来的产品特性。尽管十年前他们制造的手机就能上网、收发邮件,先进的制造技术也的确让他们在手机、平板电视等领域独领风骚多年,但他们太过于执着硬件,如今他们的竞争对手却找到了另一条路径——通过软件降低满足用户需求的成本。 竞争对手还快速进步了,通过简单易用的软件和在线服务来整合产品,也使用了更聪明的市场策略,比如索尼的电子阅读器Librie,目标是在卖设备,而亚马逊的kindle,实际上是在卖书。 “日本企业对自己的技术和制造能力过分自信了。我们丢掉了用户视角。”松下总裁津贺一宏(Kazuhiro Tsuga)说。他在2012年6月上任,当时的松下,年亏损是该企业94年历史中最严重的,约97亿美元。 “榜样”索尼 在所有深陷巨亏泥潭的日企中,索尼无疑是其中最具代表性的一个。 在苹果崛起之前,索尼一直都是消费电子的时尚标杆,就连乔布斯也自称是索尼的粉丝。“我还记得盛田昭夫送给我和史蒂夫每人一部Walkman。之前我们从未见过这样的产品。史蒂夫为之痴迷,他所做的第一件事就是拆卸Walkman,认真查看每一个零件,研究它是如何安装、制作和打磨的。”曾任苹果CEO的约翰·斯库里这样回忆起1980年代他们对索尼创始人盛田昭夫的那次拜访。 “当时,他(乔布斯)感觉是‘进了天堂’。他不想学IBM,他想成为索尼。” 约翰·斯库里说。 在其后的二十年里,除了学习索尼的精益制造的理念外(时至今日这仍是苹果产品最具代表性的标签之一),乔布斯显然也对盛田昭夫要打造完整视听产业链的构想进行了认真的剖析,我们在后来的由iPod、iPhone、iPad加iTune音乐商店组成的苹果完整商业链条上,多少都能看到盛田昭夫当年蓝图的影子。 为什么索尼自己没有完成这种进化?那是因为盛田昭夫的继承者只是在简单地复制以往的成功模式,用CD取代磁带就是这种现象的典型体现——索尼只想在介质上完成升级,而没有考虑到一个崭新的数字时代已经到来。后来的事情,大家都已熟悉,苹果完成了对榜样索尼的超越,从iPod到iPhone再到iPad,苹果的每一种产品都是一种革命——软硬结合,完整的生态链条,而索尼只能被动跟着苹果的节奏推出相应的产品,试图仍沿用传统的硬件思维——精美的外形和细腻做工——来与对手较量,全然忽视了苹果全产业链的威力。 所以,在过去的这十年中,索尼一直深陷创新力及执行力放缓的困境,技术和产品的滞后也让索尼在电视机、游戏机、手机等传统优势业务上的市场份额,逐步被竞争对手侵蚀。2011年至2012年,索尼更是遭遇了16年来的最大亏损,股价及市值都跌至历史最低。 公开财报显示,整个2011财年(2011年4月至2012年3月31日),索尼净亏4567亿日元(约合59亿美元),创下历史新高;2012年虽然情况有所好转,但仍未彻底走出亏损困境——截至2012年12月底之前的9个月时间里,索尼仍亏损509亿日元(约5.4亿美元)。为了实现2012财年扭亏的目标,今年2月索尼还将东京的一座办公大楼以1110亿日元(约12亿美元)出售,据称该交易将为2012财年计入410亿日元的运营利润。 面对持续的亏损,去年2月,英国人霍华德·斯金格将坐了近7年的索尼总裁兼CEO的位置让给了在PlayStation业务和网络娱乐业务中有出色表现的执行副总裁平井一夫(Kazuo Hirai)。 平井一夫曾长期负责索尼娱乐部门,在音乐和游戏行业有着经验丰富。为了重振索尼,平井一夫上任没多久就做出了裁掉1万名员工的决定,同时公布了“一个索尼(One Sony)”政策,宣布集中聚焦在三大“支柱”业务:移动、数码影像和游戏。 但在今年2月,平井一夫在接受《华尔街日报》采访时坦承,让索尼重整旗鼓绝非易事,“我过去以为重振PlayStation业务将是我职业生涯中的最大挑战,但如今看来并非如此,一个问题接着一个问题。” 就在2012财年财报即将公布之际,索尼又下调了多项业务的销量预期:游戏机从3个月前的1000万台下调至700万台;电视从去年5月份的1750万台下调到1350万台……智能手机?索尼早已远远落后于苹果和三星,其在美国市场的占有率几乎为零。 “Brain country” 面对困境,日立总裁中西宏明已经决定让日立回到它的核心业务上:重型机械工程。燃气轮机、蒸汽轮机、核能工厂、高速列车,特别是在发展中国家,这些仍是他眼里能够保持日立举世无双的领域。 “城际快车”是英国高铁更新换代计划中的一部分,在该项目的竞争中,日立的列车就一路领先,拿下45亿英镑的订单。 “在发展中国家,他们对一些大的基础工程建设并没有很专门的规划和方法上的知识,我们有。这也不是简单的卖机器而已,还包括了工程、规划甚至一些项目的财务服务。整个过程一条龙,它们都是我们的优势。” 中西宏明的策略显然奏效了,日立重新盈利。但对于其他企业来说,日子仍不好过。BBC认为,全球最大的市场——中国的消费者对日本电子产品的抵制也让这些企业雪上加霜。 索尼、松下、夏普三者之中索尼总体实力最强,讽刺的是,索尼在卖人寿保险上赚的钱都要比它制造电子机器赚的多得多。松下和夏普则已经没有那么多可以转而依靠的业务了,投资失误又让事情更加糟糕。 2007年,夏普决定投资44亿美金在大阪附近建立大型工厂生产平板电视,本以为这样可以做到用比竞争对手低的成本生产大屏幕电视,然而他们错了,对手们都扩充了产能并且降低了电视的售价。2012年3月,夏普不得不把这个工厂46%的股份卖给了台湾的对手鸿海。2011财年,夏普亏损约47亿美元。 格哈德说,就像上世纪五六十年代那样,现在,这些日本企业恐怕又得向美国取经。 战后,日本几家电子企业抱团布局,齐心打造产业链,在当时资本高度集中的经济环境下,中小企业很难找到发展空间,但在世界范围内,这几年的创新动力正是源自中小企业,比如美国硅谷。相比起大企业大财团,他们不教条,容易有火花。在特殊的经济周期中,财团曾经贡献巨大,而如今在急剧变化的环境里,却显得无所适从。 “你有很高品质的教育体系,又有很聪明的国民,你就要利用好这点。有时候这些价值并不只在制造业中体现,在软件业也如此。而软件在日本显然是被忽视了。许多最成功的企业集中在硅谷不是偶然,像思科、甲骨文这样的公司就不会被韩国的竞争者影响。日本必须成为一个‘Brain country’,就像瑞士或者英国那样。”格哈德说。

    时间:2013-04-22 关键词: 软件 日本电子 编写

  • 编写PIC单片机源程序应注意的问题

    编写PIC单片机的源程序,除了源程序的开始处要求严格的列表指令外,还需注意源程序中字母符号大小写的有关规则,否则在PC机上汇编源程序时不会成功。笔者用下列的PIC16F84单片机对B口送数的源程序(源程序各自定义)为实例,说明其注意的问题。 LIST    P=PIC16F84      #INCLUDE P16F84?INC    ORG 0 START CLRW       ;起始地址     BSF STATUS,5 ;选体1     MOVWF  TRISB ;置B口为输出 BCF STATUS,5;STATUS,5复位     MOVW   0xAA ;可使B口的              LED间亮     MOVWF  PORTB ;B口输出10              101010 LOOP GOTO   LOOP     END 上述源程序中因用了伪指令INCLUDE,在这里是指把列表指定的PIC16F84文件(在MPLAB中)读入源程序作为上述源程序的一部分,所以凡是 MPLAB中有关PIC16F84单片机已有的寄存器在上述源程序中无需再用赋值指令(EQU)赋值,这就使所建立的源程序大为简化。 此外,由于有了伪指令INCLUDE,所以根据MPLAB软件中的格式,在源程序中的操作数凡是涉及MPLAB已规定的寄存器名称,其字母一律只能大写,不能小写,其余操作码、标号字母可任意大小写,但0x中的x应小写,否则汇编不会成功。鉴于上述原因,为了书写方便,所以在使用MPLAB软件时,PIC 单片机的源程序均用大写字母为宜(0x例外)。 扩展阅读:PIC与EMC指令对照表【图】

    时间:2013-04-16 关键词: pic 单片机源程序 编写

  • 手把手教您编写第一个单片机程序

    51单片机的开发环境是Keil 软件。Keil 软件虽然是一个收费软件,但从uVision2到目前的uVison4版本都有破解版,在网上都可以找到下载。笔者推荐大家使用uVisong4破解版本,好处不用多说。Keil uVision4软件的压缩包里附有安装和破解说明,本文不再赘述。 开发一个单片机程序,一般都要经过这几个步骤:建立工程->建立C文件->添加C文件到工程->编写C代码->设置目标工程的选项->编译工程产生HEX文件->将HEX文件下载到单片机。本文将一步一步手把手教您开发一个LED闪烁的简单且实用的C51程序。让您从0基础起步学习开发51单片机。 安装Keil uVison4之后,第一次运行出现如图1的界面,从上往下数,依次是菜单栏、第一条工具栏、第二条工具栏,接下来左边白色部分为工程文件区(显示文件、函数、语言模板和相关书籍),右边灰色部分为文本区(编写源文件),最下边为编译信息栏(显示编译时产生的相关信息)。     图1 一.建立工程 点击“Project”菜单项,选“New uVision Project…”,跳出创建新工程对话框,选择工程放置位置,在这里笔者选择F盘,并在根目录创建LED这个文件夹,用来放置工程文件,如图2:     图2 打开LED文件夹,然后给新工程取个名字(可以任意取),在这里笔者取工程名字为“LED”,如图3:     图3 点击“保存”按钮,跳出器件选择对话框,如图4:     图4 找到Atmel单片机,选择AT89S52,同时右边的描述栏里显示了该器件的基本信息,如图5:     图5 点击“OK按钮,”跳出提示对话框,如图6:     图6 提示对话框问您“是否将8051标准启动代码复制到工程文件夹并添加到工程?”,根据您的需要选择,一般不需要,在这里笔者选择“否”。此时,可看到keil uVision4界面左边的工程窗口里多了一个目标文件夹“Target 1”,其下有一个源文件组文件夹“Suorce Group 1”,如图7。此时,新工程已经建立,但还只是一个空的工程。     图7 二.建立C文件和保存C文件 从图7可看到,“Suorce Group 1”这个源文件夹下还没有任何文件。接下来就是创建一个C文件。点击“File”菜单,选择“New”,文档区便出现了一个默认文件名为“Text 1”的空白文档,如图8:     图8 在该文档上随便输入一个空格,然后点击“File”菜单,选择“Save”,跳出保存对话框,如图9,输入文件名为“LED.c”,点击“保存”按钮。这一步需要注意的是这个C文件必须保存在刚才创建的工程文件夹下,否则在后面编译时会出错。C文件的文件名一般和工程名一致。扩展名必须为“.c”。这个C文件就是您后面要编写C语言源代码的源文件,现在已经在你的工程文件夹里了。     图9 三.添加C文件到工程 虽然“LED.c”这个文件已经在您的工程文件夹里,但还不属于你的工程文件,如果您不把它添加到工程里去,它的存在对您这个工程没有任何意义。接下来就是把C文件添加到您的工程里,让它变成您的工程文件。右键点击左边工程窗口里的“Suorce Group 1”这个源文件夹,选“Add File to Group ‘Suorce Group 1’…”,跳出添加源文件对话框,找到刚才建立的“LED.c”文件,点击“Add”按钮后,再点击“Close”按钮退出,不要重复点击,如图10:     图10 这时可以看到“Suorce Group 1”这个源文件下多了一个源文件“LED.c”,同时,右边的文档区的文件名也改变了,如图11。     图11 四.编写C代码 在空白的文档区输入如下C代码: /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #i nclude//包含头文件 sbit led=P2^0;//定义位变量led,使其关联单片机管脚P2.0 void Delayms(unsigned int t);//声明延时函数 ///////////////////////////////////////////////////////////////////////////////// int main(void)//主函数(C语言程序入口函数) { while(1) { led=0;//P2.0拉低,点亮LED Delayms(500);//调用延时函数,延时500毫秒 led=1;//P2.0拉高,熄灭LED Delayms(500);//调用延时函数,延时500毫秒 } return 0; } ////////////////////////////////////////////////////////////////////////////////// void Delayms(unsigned int t)//定义延时函数 { unsigned int i,j; for(i=0;i for(j=0;j<120;j++);//大约延时1毫秒 } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 这是一个常用的指示灯闪烁的程序,输入完成以后,先不要马上编译,还需要作一些设置。 五.工程选项设置 这一步主要是设置时钟频率和产生HEX文件这两项。进入工程选项有两个途径:从“Project”菜单进入和直接点击工具栏快捷按钮进入。这里使用工具栏快捷按钮进入,点击第二条工具栏的第7个按钮(Target Opsions…),跳出选项对话框,默认选项卡为“Target”,如图12,将晶振频率设为你目标板所使用的晶振频率,这里设为12.0;再点击“Output”选项卡,将“Create HEX File ”复选框打勾,这个项设置绝不能忽略,否则您的工程就没有 HEX文件产生。其他选项保持默认。最后点击“OK”按钮,完成选项设置。     图12 六.编译工程产生HEX文件 点击第二条工具栏的第三个按钮(Rebuild),工程便进入编译链接状态,“Build Output”信息栏就会出现相关的编译信息,如图13。从该信息栏可以知道程序的大小,使用了的多少内部RAM和外部RAM、生成多少个代码、是否生成HEX文件、有多少个错误和警告等信息。如果有错误,目标文件将不会被创建,只要双击错误信息,光标就会跳到C文档错误代码的行号或错误代码的附近,方便您排查错误。     图13 七.将HEX文件下载到单片机 编译通过的C代码,在工程文件夹下就会生成很多文件,其中有两个文件是最重要的,一个是扩展名为“.c”文件“LED.c”,这是移植程序用的C源文件,是程序的核心,拥有这个文件就相当于拥有整个程序;另一个是扩展名为“.hex”的文件“LED.hex”,这是下载程序用的文件。这两个文件都可以用记事本打开。“LED.hex”文件是采用Intel hex文件格式存储程序代码的。下载程序的时用下载软件打开这个hex文件,将文件里的程序代码提取出来写入单片机的程序存储区里。 在下载单片机程序之前,您需要构建一个单片机最小系统,请参阅笔者的《学单片机从构建最小系统开始》一文。下载软件使用WSFISP软件,也可以用AtmelISP软件。下载线可根据笔者提供的原理图自己DIY,也可以购买。51单片机程序下载软件操作都很简单,本文不再叙述。 扩展阅读:单片机程序架构详解

    时间:2013-02-20 关键词: 单片机程序 手把手 编写

  • PIC8位单片机源程序编写方法

    PIC系列单片机的源程序是指PIC的助记符指令编写的程序(汇编语言程序)。这里将对PIC源程序的格式要求和其源程序的建立或书写作简要的介绍。 一般来说对PIC单片机的源程序格式并没有要求统一的编写形式,用户可以根据习惯来编写,当然编写时应遵守汇编的一些规范。下面以PIC16F84芯片点亮一支发光二极管的汇编程序实例,介绍一种源程序编写的方法(格式),以供实际应用时参考。 源程序清单 1?File TURNON.ASM 2?Assembly code for PIC16F84 microcontroller 3?Turns on an LED connected to B0 4?Uses RC oscillator,about 100kHz 5?CPU configuration 6?(it’s a 16F84,RC oscillator? 7?watchdog timer off,power-up timer on) 8?processor 16F84 9?include 10?_config_RC_OSC&_WDT_OFF&_PWRTE_ON 11?Program 12?org 0;start at address 0 13?At startup,all ports are inputs? 14?Set Port B to all outputs 15?movlw B‘00000000’;w=binary 00000000 16?tris Portb     ;copy w to port B control reg 17?Put a 1 in the lowest bit of port B? 18?movlw B‘00000001’;w=binary 00000001 19?movwf portb    ;copy w to port B itself 20?Stop by going into an endless loop 21?fin:goto fin 22?end ;program ends here 现对源程序清单逐行加以说明。 第1行为文件(File)名,这里取名TURNON?ASM。TURNON意指一个接通(即接通LED)程序。ASM是源程序的扩展名。 第2行说明是由适用于PIC16F84微控制器的汇编码编写的源程序。 第3行说明程序用途是接通PIC16F84 PORTB的B0位LED。 第4行说明由RC振荡器供给时钟,频率约为100kHz。 第5、6、7行说明对16F84 CPU的结构设置(用RC振荡器,看门狗定时器即watchdog timer关,上电power-up定时器开,关于16F84的结构设置,后面将详谈)。 第8、9、10行为伪指令。这是用来向汇编程序提供有关如何完成汇编的控制命令信息,它既是汇编过程的控制指令,也是汇编程序的操作指令。在这里8、9、10行还向汇编程序说明,单片机用的是16F84,用RC振荡器、看门狗定时器关断不用,但上电定时器接通使用。对照5、6行,可见二者内容相同,但一个是注释,专门为用户说明,另一个是伪指令,是为PC机的汇编程序提供命令信息。 第11行又是注释,说明程序主体由此开始。 第12行说明程序存放在由地址0开始的16F84单片机中的程序存贮器中,也是一条伪指令。 第13、14两行为注释,说明后续的一段指令(两句)的作用,即在启动时,所有端口为输入,随即将B口设定为全输出(即各位均为输出)。 第15行是汇编语言编写的第一条指令,即对二进制(B)数“00000000”存入工作寄存器W。 第16行将W寄存器的内容(即00000000)传送到B口控制寄存器,使B口成为输出口。15、16行均带有注释。  第17行为注释,说明下面的二条指令是使B口的最低位为1。 第18行的指令将二进制数00000001送入工作寄存器W,第19行指令则将W寄存器的内容复制到B口,则B口的最低位B0为高电平,其余各位则为低电平,使接到B0位的LED发光。 第20行又是注释。说明下面的指令将使B0位为高的状态继续下去。 第21行的指令为无限循环语句,维持B1口输出为高不变。 第22行是伪指令END,表明程序到此结束。 将清单所列程序在PC机上用一种名为MPASM的汇编软件进行汇编后,如无错误,即可获得扩展名为?HEX的相应机器码文件,借助编程器将该文件的相应内容烧入PIC16F84单片机的程序存储器内,并将编程好的芯片插入前述的电路板中,再加上电源,即可点亮16F84 B0位的LED。 【更多资源】

    时间:2012-12-25 关键词: 方法 pic8 位单片机 编写

  • 用Excel中的VBA编写卡方测算相关程序

     在生命科学及社会科学研究中,卡方(χ2)检验是最常用的统计分析方法之一[1-8]。卡方(χ2)检验是次数资料的显著性检验方法,包括适合性检验和独立性检验两类[9]。适合性检验用于检验某性状观察次数与该性状的理论比率是否符合,如在遗传分析中研究两对性状杂种后代的分离现象是否符合某一特定比率;而独立性检验是用于判断两类因子是彼此相关还是相互独立的,如:采用某种措施与预防某种灾害的关系。 卡平方的计算很复杂,但随着计算机与统计软件的普及,应用计算机计算卡平方成为最准确可靠的方法,各种大型统计软件如SAS、SPSS、DPS等均应用于卡平方的测算[10-12],一些研究者也研发了在Excel上编制运算表来实现卡平方测算的方法[13]。  使用如SAS、SPSS、DPS等大型统计软件需要有较高花费;采用R统计软件来编程进行卡平方测算要求研究者有较强学术底蕴和计算机编程能力。这在客观上限制了SAS、SPSS、DPS、R等统计软件的普及应用。Excel作为 Microsoft Office 家族成员,由于多数学生和科研人员都具有Excel基础知识,采用Excel软件计算卡平方无需考虑运行环境而受到欢迎。然而,采用编制运算表的方法还是让人感到有些繁锁,不易学习使用。鉴于此,本文采用一种基于Excel的VBA编程方法[14-15],编写了“卡平方测算”相关程序,可在所有配备Office 2000以上版本的计算机上使用。“卡平方测算”在VBA程序运算时只需输入最原始的数据,应用步骤实行最直观的人机对话,任何初学者都可以即学即用轻松地掌握程序的使用方法;编写完的程序成为工作模板后,可以任意复制或通过e-mail邮寄等方式进行传播、拷贝,因此这种方法受到使用者的一致好评。 本文介绍了该程序的源代码及使用方法,让所有不方便使用大型统计软件的同行都可分享这些程序带来的所有便利。 1 VBA程序源代码 1.1 用于适合性检验的卡平方计算程序 Private Sub CommandButton1_Click() Dim n As Integer n=InputBox("请输入数据组数n=?") Cells(1,2).Value=("数据组数n") Cells(2,2).Value=n Dim a0(0 To 99)As Single Dim al(0 To 99)As Single Dim x2 As Integer Cells(1,3).Value="实测值a0" Cells(1,4).Value="理论值al" Cells(1,5).Value="卡平方值x2" For i=1 To n a0(i)=InputBox("请输入实测值的第"& i &"个样本值") Cells(1+i,3).Value=a0(i) Next i For i=1 To n al(i)=InputBox("请输入理论值的第"& i &"个样本值") Cells(1+i,4).Value=al(i) Next i x=0 For i=1 To n x=x+((a0(i)-al(i))^2)/al(i) Next i Cells(2,5).Value=x End Sub 1.2 用于独立性检验的卡平方计算程序 1.2.1 2×2表的独立性测验 Private Sub CommandButton1_Click() Dim a  As Integer :Dim b  As Integer:Dim a0  As             Integer:Dim b0  As Integer Dim n As Integer Dim a1 As Single:Dim b1 As Single:Dim a01 As             Single:Dim b01 As Single Dim E11 As Single:Dim E12 As Single:Dim E21 As             Single:Dim E22 As Single Dim c1  As Single:Dim c2 As Single:Dim c3  As             Single:Dim c4 As Single Dim x As Single a=InputBox("请输入A事件效果1数字a=?") Cells(1,1).Value="A事件效果1数a" Cells(2,1).Value=a b=InputBox("请输入B事件效果1数字b=?") Cells(1,2).Value="B事件效果1数字b" Cells(2,2).Value=b a0=InputBox("请输入A事件效果2数字a0=?") Cells(1,3).Value="A事件效果2数a0" Cells(2,3).Value=a0 b0=InputBox("请输入B事件效果2数字b0=?") Cells(1,4).Value="B事件效果2数字b0" Cells(2,4).Value=b0 n=a0+b0+a+b aa0=a+a0:bb0=b+b0:ab=a+b:a0b0=a0+b0 E11=aa0*ab/n:E12=aa0*a0b0/n E21=bb0*ab/n:E22=bb0*a0b0/n c1=Abs(a-E11):c2=Abs(a0-E12):c3=Abs(b-E21):c4=Abs(b0-E22) x=((c1-0.5)^2)/E11+((c2-0.5)^2)/E12+((c3-0.5) ^2)/E21+((c4-0.5)^2)/E22 Cells(1,5).Value="卡平方值x2" Cells(2,5).Value=x End Sub 1.2.2 2×c表的独立性测验 Private Sub CommandButton1_Click() Dim C As Integer :Dim R As Single :Dim d As             Single:Dim h As Single Dim x As Single Dim a0(0 To 99) As Single :Dim b0(0 To 99) As         Single:Dim g(0 To 99) As Single C=InputBox("请输入数据组数C=?") Cells(1,2).Value=("数据组数C") Cells(2,2).Value=C Cells(1,3).Value="A事件数值a0" Cells(1,4).Value="B事件数值b0" Cells(1,5).Value="a(i)+b(i)" R1=0]:R2=0 For i=1 To C a0(i)=InputBox("请输入A事件数值的第("& i &")            个样本a0("& i &")=?") Cells(1+i,3).Value=a0(i) b0(i)=InputBox("请输入B事件数值的第("& i &")            个样本b0(" & i & ")=?") Cells(1+i,4).Value=b0(i) g(i)=a0(i)+b0(i) Cells(1+i,5).Value=g(i) R1=R1+a0(i):R2=R2+b0(i) Next i R=R1+R2 Cells(1,6).Value="A事件数值之和,R1" Cells(1,7).Value="B事件数值之和,R2" Cells(1,8).Value="AB事件所有数值之和,R" Cells(2,6).Value=R1:Cells(2,7).Value=R2:Cells                (2,8).Value=R h=0 For i=1 To C h=h+a0(i)^2/g(i) Next i x=(h - R1 ^ 2 / R) * R ^ 2 / R1 / R2 Cells(1,9).Value=" 卡平方值x2" Cells(2,9).Value=x End Sub 1.2.3 r×c表的独立性测验 Private Sub CommandButton1_Click() Dim C As Integer:Dim R As Integer :Dim n As             Single:Dim h As Single Dim x As Single Dim a(0 To 99,0 To 99) As Single Dim g(0 To 99) As Single Dim k(0 To 99) As Single C=InputBox("请输入数据组数C=?") Cells(1,2).Value=("数据组数C") Cells(2,2).Value=C R=InputBox("请输入数据组数R=?") Cells(1,3).Value=("数据组数R") Cells(2,3).Value=R Cells(1,4).Value=" Gi数值" Cells(1,5).Value=" Kj数值" Cells(1,6).Value=" 所有数字之和,n" For i=1 To C For j=1 To R a(i,j)=InputBox("请输入第(" & i & ")行,第("& j         & ")列的样本数值a(i,j)=?") Next j Next i For i=1 To C For j=1 To R g(i)=g(i)+a(i,j) Cells(1+i,4).Value=g(i) Next j Next i For j=1 To R For i=1 To C k(j)=k(j)+a(i,j) Cells(1+j,5).Value=k(j) Next i Next j For i=1 To C n=n+g(i) Next i Cells(2,6).Value=n h=0 For i=1 To C For j=1 To R h=h+a(i,j)^2/g(i)/k(j) Next j Next i x=n * (h-1) Cells(1,9).Value=" 卡平方值x2" Cells(2,9).Value=x End Sub 2 “卡平方测算”VBA程序的应用步骤:  (1)运行环境:Win2003、WinXP、Win2000等。  (2)VBA程序的应用步骤:打开Excel“EC50、EC90测算”程序工作簿(在此过程中,若计算机屏幕显示对话框,则应选择并点击“启用宏”),单击(程序运行命令)按钮(本程序中该按钮上面写着“计算”),则程序开始运行计算机,屏幕依次出现对话框,按对话框所提问题逐个输入数据,输完后瞬间即在程序工作簿页面上显示输出结果(包括卡平方值)。  (3)“卡平方测算”VBA程序的下载及拷贝方法:①下载源程序的方法参见文献[6-7];②“卡平方测算” 文件的再拷贝:上述工作完成后,此Excel文件即成为一个工作模板,可以任意复制、粘贴或通过e-mail邮寄等方式进行传播和拷贝。  本Excel文件所占内存约为60 KB,与其他有类似功能的程序相比要小得多。上述4个程序也可以组合成一个较大的程序,但从应用角度考虑,这样做会浪费计算机的内部资源(因为事先声明了较多在当次运算中并不使用的变量),故本文未这样处理。 参考文献 [1] 武晓玲,周斌,孙石,等.大豆对大豆疫霉菌株Pm14抗性的遗传分析及基因定位[J].中国农业科学,2011,44(3):456-460. [2] 王保通,李强,胡茂林,等.小麦品种Libellula和N. strampelli抗条锈病主效、微效基因遗传分析[J].植物病理学报,2010,40(3):300-306. [3] 张宏,任志龙,胡银岗,等.陕麦139抗条锈病基因遗传分析[J].作物学报,2010,36(1):109-114. [4] 何丽华,牛宝龙,齐晓朋,等.棉铃虫成虫体色突变体的发现及其遗传分析[J].核农学报,2007(4):397-400. [5] 杨振宇,王晓丽,张晓波,等.部分抗SMV大豆品种成株抗性基因对数分析[J].吉林农业大学学报,2011,33(6):591-594. [6] 龚瑞,杨炬,黎唏,等.2007-2010年度宁夏流感监测结果分析[J].宁夏医学杂志,2011,33(3):222-224. [7] 钱峰.基于卡方检验的国内外知识管理研究热点比较[J].情报杂志,2008(9):56-58. [8] 徐向阳.卡方检验在学生成绩差异性分析中的应用[J].常州技术师范学院学报,2001,7(4):13-16. [9] 盖钧镒.试验统计方法[M].北京,中国农业出版社,2000. [10] 詹秋文.Excel和SAS在生物统计学的应用比较[J].生物学杂志,2009,26(1):74-75,83. [11] 向穷,施树良,李钰.常用统计软件在生物统计中的应用比较[J].现代生物医学进展,2009,9(9):1775-1777,1789. [12] 唐启义,冯明光.实用统计分析及其DPS数据处理系统[M].北京:科学出版社,2002:188-95. [13] 谭永强,余华强,陈桥生,等.利用Excel软件建立卡方检验分析模板在农业统计中的应用[J].湖北农业科学,2010,49(12):3192-3195. [14] 龚沛曾,陆慰民.Visual Basic程序设计教程(6.0版)[M].北京:高等教育出版社,2001. [15] 李晓玫,杨小平.Excel中的VBA程序设计[J].四川师范大学学报(自然科学版),2004(4):423-426. [16] 马海霞,刘 影,王艳红,等.用EXCEL中的VBA编写“多项式的三角函数拟合单峰曲线”程序[J].菌物研究,2009,7(3-4):195-200. [17] 段显德,王艳红,杨信东.用EXCEL中的VBA编写“试卷分析”程序[J].通化师范学院学报,2010,31(8):52-53.

    时间:2012-11-22 关键词: excel vba 测算 编写

  • 用单片机编写几种跑马灯

    任务: 1、在电路板上实现跑马灯,一次1匹 2、在电路板上实现跑马灯,一次2匹 3、在电路板上实现4个二极管的同时闪烁 源程序1: /***********************************信息**************************************** **作者:刘海涛 **版本:初始版V1.0 **描叙:用电路板实现跑马灯。 **日期:2010年7月25日 *******************************************************************************/ /**********************************头文件*************************************** **头文件"reg52.h" *******************************************************************************/ /**********************************函数名*************************************** **函数名:延时函数delay() **输  入:无 **输  入:无 **宏定义:无 *******************************************************************************/ /**********************************宏定义*************************************** 宏定义:#define XBYTE ((unsigned char *)0x20000L) *******************************************************************************/ #include"reg52.h" delay(unsigned int dat)      // 延时函数定义 {  unsigned int i,j;  for(i=0;i<dat;i++)  {   for(j=0;j<10000;j++);  } }   #define XBYTE ((unsigned char *)0x20000L) //宏定义   void main(void)        //主函数 {  unsigned char i;  while(1)     {   for(i=0;i<4;i++)   {    XBYTE[0xd000]=(0x01<<i)^0xFF;   //参考电路图,点亮第一个灯,并循环左移    delay(10);      //延时函数调用   }   } }     源程序2: /***********************************信息**************************************** **作者:刘海涛 **版本:初始版V1.0 **描叙:用电路板实现跑马灯,一次移动2灯。 **日期:2010年7月25日 *******************************************************************************/ /**********************************头文件*************************************** **头文件"reg52.h" *******************************************************************************/ /**********************************函数名*************************************** **函数名:延时函数delay() **输  入:无 **输  入:无 **宏定义:无 *******************************************************************************/ /**********************************宏定义*************************************** **宏定义:#define XBYTE ((unsigned char *)0x20000L) *******************************************************************************/ #include"reg52.h"       // 头文件 #define XBYTE ((unsigned char *)0x20000L) //宏定义 delay(unsigned int dat)      //延时函数 {  unsigned int i,j;  for(i=0;i<dat;i++)  {   for(j=0;j<10000;j++);  } } void main(void)         //主函数 {  unsigned int i;  while(1)     {   for(i=0;i<3;i++)   {      XBYTE[0xd000]=(0x03<<i)^0xff;  //点亮1、2两个灯    delay(10);   }    XBYTE[0xd000]=(0x09<<0)^0xff;  //点亮第一、第四个灯    delay(10);     } } 源程序3: /***********************************信息**************************************** **作者:刘海涛 **版本:初始版V1.0 **描叙:用电路板实现跑马灯,四个二极管同时闪烁。 **日期:2010年7月25日 *******************************************************************************/ /**********************************头文件*************************************** **头文件"reg52.h" *******************************************************************************/ /**********************************函数名*************************************** **函数名:延时函数delay() **输  入:无 **输  入:无 *******************************************************************************/ /**********************************宏定义*************************************** **宏定义:#define XBYTE ((unsigned char *)0x20000L) *******************************************************************************/ #include"reg52.h"        //主函数 #define XBYTE ((unsigned char *)0x20000L)  //宏定义 delay(unsigned int dat)       //延时函数 {  unsigned int i,j;  for(i=0;i<dat;i++)  {   for(j=0;j<10000;j++);  } } void main(void)         //主函数 {    while(1)     {        XBYTE[0xd000]=(0x0f<<0)^0xff;  //    delay(10);    XBYTE[0xd000]=(0x0f<<4)^0xff;    delay(10);     } }  

    时间:2012-09-19 关键词: 用单片机 跑马灯 编写

  • arl模板编写方法简介

    其实arl模板很容易写,只要明白d表的内容就可以,也可以照350自带的arl修改,复制粘贴很快就可以搞定.主要是几个关键位置 FMT_SKIPUNTIL ????  $skipe    和FMT_ROUND    $skip   D$dcode ....   其他关键字 见350的帮助 FILE_EXTENSION  (文件扩展名,若不确定可以为空) UNIT mm       (单位,若不确定可以为空,导入时手工定义公制还是英制(mil))  IGNORE_CHAR |  (这个是要忽略的 字符 ) #IGNORE_CHAR =  (这个也是要忽略的 字符 ,#以区分上边的) # Shape definitions S_ROUND       circle   (350的形状与 d 表内形状的 匹配) # Line formats: FMT_SKIP ===    (跳过次内容,不换行) FMT_SKIPUNTIL +4   photo data table  $skipe   (这个是关键,见到photo data table 这行+4行处开始读取d码数据,此前的内容忽略, 这里也是350 识别此模板arl 格式(名字)的关键,因此这个内容要选择d表内有代表性,独一无二的内容,最好是  整行) FMT_ROUND    $skip   D$dcode    $shape   $xsize     $skipe  (这里是d表的实际内容的格式,形状,大小,D#在哪一列,中间有无用数据列加$skip 跳过, 后边加$skipe 换行) FMT_SKIPEOF    *******  $skipe  这里的内容表示见到*****(此为可以代表d表结束的内容如 end of table)表示此表结束,后边的内容忽略。

    时间:2012-07-29 关键词: 方法 模板 arl 编写

  • WINCE6.0 简单LED驱动程序的编写

    把它写成了实验报告的样子! 在此BLOG中的有些函数是针对特定的BSP, 比如:地址的映射函数, 在每一个BSP中它的地址映射函数都不同, 但是都是地址映射(废话当然是地址映射了)。其实里面有很多的东西都还可以添加:比如添加读取LED信息状态, 优化地址映射(不必把全部的GPIO都映射, 可以自定义一个小的结构体来实现)。 一. 实验目的 a) 掌握流驱动的结构 b) 掌握一般流驱动的编写方法 二. 实验设备 a) S3C6410 开发板一台 b) PC 机一台, VS2005, CE6.0 环境 三. 实验内容 a) 进行简单 LED 驱动的编写。 四. 实验原理 a) 硬件原理图 i. ii. iii. 从原理图可以知道如果我们要点亮 LEDx , 只需要 GPMx 输出高电平, 如果要熄灭 LEDx , 只需要 GPMx 输出低电平即可。 b) 软件原理设计 i. 由于在 WINCE 中使用都是虚拟的地址, 所以需要将实际的物理地址转换为虚拟地址来使用。 在系统提供的 BSP 中提供了一个函数来实现从物理地址到虚拟地址的映射。 1. void *DrvLib_MapIoSpace (UINT32 PhysicalAddress , UINT32 NumberOfBytes , BOOL CacheEnable ) 2. 把物理地址转换为虚拟地址。(其实它就是在地址映射表中查找相应的物理地址,然后返回对应的虚拟地址加上它的偏移。) 3. UINT32 PhysicalAddress :要实现映射的物理地址的起始地址。 4. UINT32 NumberOfBytes : 要映射物理地址的长度。 5. BOOL CacheEnable :该物理地址是否使用了CACHE.( 具体参照地址映射表) 。 ii. LED 上下文结构体的定义 1. 定义了一个 LED 驱动的上下文, 用来保存 LED 驱动的信息。 但是只是简单的 LED 驱动, 没有包含多的数据。 typedef struct { volatile S3C6410_GPIO_REG *pGPIOReg ; }LED_PUBLIC_CONTEXT , *PLED_PUBLIC_CONTEXT ; 2 . S3C6410_GPIO_REG 是BSP 预先定义的一个GPIO 使用的数据结构。 iii. GPIO 寄存器的使用。 GPIO 寄存器的地址都映射到了虚拟的地址上。 BSP 提供了一个结构体方便 GPIO 寄存器的使用。 1. typedef struct { …………….. UINT32 GPMCON; // 820 UINT32 GPMDAT; // 824 UINT32 GPMPUD; // 828 ……………….. } S3C6410_GPIO_REG, *PS3C6410_GPIO_REG; 2. 在使用这个结构全的时候把 GPIO 的虚拟地址的基地址映射到此结构的开始即可以操作此结构体的数据来操作实际的寄存器。 3. 进行GPIO 物理地址到虚拟地址的映射: pLedContext ->pGPIOReg = (volatile S3C6410_GPIO_REG *)DrvLib_MapIoSpace (S3C6410_BASE_REG_PA_GPIO , sizeof (S3C6410_GPIO_REG ), FALSE ) ; iv. 实现 LED 灯状态的操作。 1. 获得了 GPIO 的虚拟地址就可以像实际的物理地址那样实现寄存器的操作。 2. a) // 使能上拉 b) pLedContext ->pGPIOReg ->GPMPUD |= 0x0ff; c) // 设置为输出 d) pLedContext ->pGPIOReg ->GPMCON = 0x111111; e) // 关闭所有的LED f) LED_ALL_OFF (pLedContext ->pGPIOReg ->GPMDAT ); 3. 为了方便进行操作,定义了一组宏。 // 打开或关闭LED0 #define LED0_ON (x ) (x |= 0x00000001) #define LED0_OFF (x ) (x &= 0xfffffffe) // 打开或关闭LED1 #define LED1_ON (x ) (x |= 0x00000002) #define LED1_OFF (x ) (x &= 0xfffffffd) // 打开或关闭LED2 #define LED2_ON (x ) (x |= 0x00000004) #define LED2_OFF (x ) (x &= 0xfffffffb) // 打开或关闭LED3 #define LED3_ON (x ) (x |= 0x00000008) #define LED3_OFF (x ) (x &= 0xfffffff7) // 打开或关闭所有的LED #define LED_ALL_ON (x ) (x |= 0x0000000f) #define LED_ALL_OFF (x ) (x &= 0xfffffff0) 五. 实验步骤 a) Xxx_Init 函数的原型: i. DWORD XXX_Init( LPCTSTR pContext , DWORD dwBusContext ); ii. pContext: Pointer to a string containing the registry path to the active key for the stream interface driver. iii. lpvBusContext: Potentially process-mapped pointer passed as the fourth parameter to ActivateDeviceEx . If this driver was loaded through legacy mechanisms, then dwBusContext is zero. This pointer, if used, has only been mapped again as it passes through the protected server library (PSL). The XXX _Init function is responsible for performing all protection checking iv. 返回值 : Returns a handle to the device context created if successful. Returns zero if not successful. This handle is passed to the XXX_Open (Device Manager) , XXX_PowerDown (Device Manager) , XXX_PowerUp (Device Manager) , and XXX_Deinit (Device Manager) functions v. 注意:当调用 设备管理程序当调用 ActivateDeviceEx 函数的时候会间接调用到此函数, ActivateDeviceEx 的作用就是加载设备 vi. LED_Init 函数的编写。 LED_Init 最主要的功能就是进行硬件的初使化。 vii. LED_Init 函数的实现: DWORD Led_Init (LPCTSTR pContext ) { volatile PLED_PUBLIC_CONTEXT pLedContext ; RETAILMSG (DEBUG_LED ,(TEXT ("Led_Init Function!/n" ))); // 申请LED 的CONTEXT pLedContext = (PLED_PUBLIC_CONTEXT )LocalAlloc (LPTR , sizeof (pContext )); if (!pLedContext ) { RETAILMSG (DEBUG_LED , (TEXT ("Can't alloc memory for led context!/n" ))); return NULL ; } // 得到GPIO 寄存器的地址 pLedContext ->pGPIOReg = (volatile S3C6410_GPIO_REG *)DrvLib_MapIoSpace (S3C6410_BASE_REG_PA_GPIO , sizeof (S3C6410_GPIO_REG ), FALSE ); if (pLedContext ->pGPIOReg == NULL ) { RETAILMSG (DEBUG_LED , (TEXT ("LED for pGPIORges: DrvLib_MapIoSpace failed!/n" ))); LocalFree (pLedContext ); return NULL ; } // 使能上拉 pLedContext ->pGPIOReg ->GPMPUD |= 0x0ff; // 设置为输出 pLedContext ->pGPIOReg ->GPMCON = 0x111111; // 关闭所有的LED LED_ALL_OFF (pLedContext ->pGPIOReg ->GPMDAT ); RETAILMSG (DEBUG_LED , (TEXT ("Led0 on! %d!/n" ), (pLedContext ->pGPIOReg ->GPMDAT ))); return pLedContext ; ` } a) Xxx_Write 函数原型: DWORD XXX_Write(DWORD hOpenContext , LPCVOID pBuffer , DWORD Count ); i. hOpenContext : Handle to the open context of the device. The call to the XXX_Open (Device Manager) function returns this identifier 。 ii. pBuffer : Pointer to the buffer that contains the data to write. iii. Count: Number of bytes to write from the pBuffer buffer into the device. iv. 返回值: The number of bytes written indicates success. A value of –1 indicates failure. v. LED_Write 函数的作用只是进行对 LED 状态的写入。 自定义结构体: typedef struct { unsigned char cLedNum ; unsigned char fLedStatue ; }LED_DATA , *PLED_DATA ; cLedNum :LED 的标号。0 对应第一个LED, 1 对应第2 个LED … 4 表示全部熄灭(因为它只有4 个LED ) fLegStatue :将要实现的操作。 0 熄灭, 1 点亮。 vi. LED_Write 函数的实现。 DWORD Led_Write (DWORD hOpenContext , DWORD pBuffer , DWORD Count ) { PLED_DATA pLedData = (PLED_DATA )pBuffer ; RETAILMSG (DEBUG_LED , (TEXT ("Current hOpenContext %d /n" )), hOpenContext ); RETAILMSG (DEBUG_LED , (TEXT ("Corrent GPMDATA %d!/n" ), (((PLED_PUBLIC_CONTEXT )hOpenContext )->pGPIOReg ->GPMDAT ))); switch (pLedData ->cLedNum ) { case 0: if (pLedData ->fLedStatue ) { LED0_ON (((PLED_PUBLIC_CONTEXT )hOpenContext )->pGPIOReg ->GPMDAT ); RETAILMSG (DEBUG_LED , (TEXT ("Led0 on! %d/n!" ), ((PLED_PUBLIC_CONTEXT )hOpenContext )->pGPIOReg ->GPMDAT )); } else { LED0_OFF (((PLED_PUBLIC_CONTEXT )hOpenContext )->pGPIOReg ->GPMDAT ); RETAILMSG (DEBUG_LED , (TEXT ("Led0 off!/n!" ))); } break ; case 1: 。。。。。。 break ; case 2: 。。。。。。 break ; case 3: 。。。。。。 case 4: if (pLedData ->fLedStatue ) { LED_ALL_ON (((PLED_PUBLIC_CONTEXT )hOpenContext )->pGPIOReg ->GPMDAT ); } else { LED_ALL_OFF (((PLED_PUBLIC_CONTEXT )hOpenContext )->pGPIOReg ->GPMDAT ); } default : break ; } return (Count ); } b) 其它流接口函数的编写:其它的流接口函数并没有实现函数过程, 它们一般就直接返回。在Led_Deinit 函数中进行了申请内存的释放,也调用了一个放弃映射的函数DrvLib_UnmapIoSpace , 其实这个函数在6.0 之中没有作用, 它是直接返回结果的函数。 c) LED 的配置: 设备管理器要使用LED 流驱动的接口,就必须将LED 提供的接口函数导出。在Led.def 文件中添加如下代码: LIBRARY LED EXPORTS Led_Init Led_Deinit Led_Open Led_Close Led_Read Led_Write Led_Seek Led_PowerDown Led_PowerUp Led_IOControl 导出了LED 的接口函数,要把Led.dll 添加到镜像文件中去还要修改platform.bib 文件。 在 MODULES 节中添加代码如图: 最后修改注册表 platform.reg : HKEY_LOCAL_MACHINE/Drives/BuildIn 下添加注册表项 LED( 任意名字都可 ) 注册表项中的内容: 六. 实验结果及分析 a) 按照上述步骤进行 LED 流驱动的编写, 再编写一个简单的应用程序即可进行 LED 灯的控制。  

    时间:2012-06-18 关键词: LED 简单 电源技术解析 驱动程序 wince6.0 编写

  • 手把手教您编写第一个单片机程序 

    51单片机的开发环境是Keil 软件。Keil 软件虽然是一个收费软件,但从uVision2到目前的uVison4版本都有破解版,在网上都可以找到下载。笔者推荐大家使用uVisong4破解版本,好处不用多说。Keil uVision4软件的压缩包里附有安装和破解说明,本文不再赘述。 开发一个单片机程序,一般都要经过这几个步骤:建立工程->建立C文件->添加C文件到工程->编写C代码->设置目标工程的选项->编译工程产生HEX文件->将HEX文件下载到单片机。本文将一步一步手把手教您开发一个LED闪烁的简单且实用的C51程序。让您从0基础起步学习开发51单片机。 安装Keil uVison4之后,第一次运行出现如图1的界面,从上往下数,依次是菜单栏、第一条工具栏、第二条工具栏,接下来左边白色部分为工程文件区(显示文件、函数、语言模板和相关书籍),右边灰色部分为文本区(编写源文件),最下边为编译信息栏(显示编译时产生的相关信息)。 一.建立工程 点击“Project”菜单项,选“New uVision Project…”,跳出创建新工程对话框,选择工程放置位置,在这里笔者选择F盘,并在根目录创建LED这个文件夹,用来放置工程文件,如图2: 打开LED文件夹,然后给新工程取个名字(可以任意取),在这里笔者取工程名字为“LED”,如图3: 点击“保存”按钮,跳出器件选择对话框,如图4: 找到Atmel单片机,选择AT89S52,同时右边的描述栏里显示了该器件的基本信息,如图5: 点击“OK按钮,”跳出提示对话框,如图6: 提示对话框问您“是否将8051标准启动代码复制到工程文件夹并添加到工程?”,根据您的需要选择,一般不需要,在这里笔者选择“否”。此时,可看到keil uVision4界面左边的工程窗口里多了一个目标文件夹“Target 1”,其下有一个源文件组文件夹“Suorce Group 1”,如图7。此时,新工程已经建立,但还只是一个空的工程。 二.建立C文件和保存C文件 从图7可看到,“Suorce Group 1”这个源文件夹下还没有任何文件。接下来就是创建一个C文件。点击“File”菜单,选择“New”,文档区便出现了一个默认文件名为“Text 1”的空白文档,如图8: 在该文档上随便输入一个空格,然后点击“File”菜单,选择“Save”,跳出保存对话框,如图9,输入文件名为“LED.c”,点击“保存”按钮。这一步需要注意的是这个C文件必须保存在刚才创建的工程文件夹下,否则在后面编译时会出错。C文件的文件名一般和工程名一致。扩展名必须为“.c”。这个C文件就是您后面要编写C语言源代码的源文件,现在已经在你的工程文件夹里了。 三.添加C文件到工程 虽然“LED.c”这个文件已经在您的工程文件夹里,但还不属于你的工程文件,如果您不把它添加到工程里去,它的存在对您这个工程没有任何意义。接下来就是把C文件添加到您的工程里,让它变成您的工程文件。右键点击左边工程窗口里的“Suorce Group 1”这个源文件夹,选“Add File to Group ‘Suorce Group 1’…”,跳出添加源文件对话框,找到刚才建立的“LED.c”文件,点击“Add”按钮后,再点击“Close”按钮退出,不要重复点击,如图10: 这时可以看到“Suorce Group 1”这个源文件下多了一个源文件“LED.c”,同时,右边的文档区的文件名也改变了,如图11。 四.编写C代码 在空白的文档区输入如下C代码: /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #i nclude<reg52.h>//包含头文件 sbit led=P2^0;//定义位变量led,使其关联单片机管脚P2.0 void Delayms(unsigned int t);//声明延时函数 ///////////////////////////////////////////////////////////////////////////////// int main(void)//主函数(C语言程序入口函数) { while(1) { led=0;//P2.0拉低,点亮LED Delayms(500);//调用延时函数,延时500毫秒 led=1;//P2.0拉高,熄灭LED Delayms(500);//调用延时函数,延时500毫秒 } return 0; } ////////////////////////////////////////////////////////////////////////////////// void Delayms(unsigned int t)//定义延时函数 { unsigned int i,j; for(i=0;i<t;i++) for(j=0;j<120;j++);//大约延时1毫秒 } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 这是一个常用的指示灯闪烁的程序,输入完成以后,先不要马上编译,还需要作一些设置。 五.工程选项设置 这一步主要是设置时钟频率和产生HEX文件这两项。进入工程选项有两个途径:从“Project”菜单进入和直接点击工具栏快捷按钮进入。这里使用工具栏快捷按钮进入,点击第二条工具栏的第7个按钮(Target Opsions…),跳出选项对话框,默认选项卡为“Target”,如图12,将晶振频率设为你目标板所使用的晶振频率,这里设为12.0;再点击“Output”选项卡,将“Create HEX File ”复选框打勾,这个项设置绝不能忽略,否则您的工程就没有 HEX文件产生。其他选项保持默认。最后点击“OK”按钮,完成选项设置。 六.编译工程产生HEX文件 点击第二条工具栏的第三个按钮(Rebuild),工程便进入编译链接状态,“Build Output”信息栏就会出现相关的编译信息,如图13。从该信息栏可以知道程序的大小,使用了的多少内部RAM和外部RAM、生成多少个代码、是否生成HEX文件、有多少个错误和警告等信息。如果有错误,目标文件将不会被创建,只要双击错误信息,光标就会跳到C文档错误代码的行号或错误代码的附近,方便您排查错误。 七.将HEX文件下载到单片机 编译通过的C代码,在工程文件夹下就会生成很多文件,其中有两个文件是最重要的,一个是扩展名为“.c”文件“LED.c”,这是移植程序用的C源文件,是程序的核心,拥有这个文件就相当于拥有整个程序;另一个是扩展名为“.hex”的文件“LED.hex”,这是下载程序用的文件。这两个文件都可以用记事本打开。“LED.hex”文件是采用Intel hex文件格式存储程序代码的。下载程序的时用下载软件打开这个hex文件,将文件里的程序代码提取出来写入单片机的程序存储区里。 在下载单片机程序之前,您需要构建一个单片机最小系统,请参阅笔者的《学单片机从构建最小系统开始》一文。下载软件使用WSFISP软件,也可以用AtmelISP软件。下载线可根据笔者提供的原理图自己DIY,也可以购买。51单片机程序下载软件操作都很简单,本文不再叙述。

    时间:2012-05-30 关键词: 单片机程序 手把手 编写

  • 手把手教您编写第一个单片机程序 

    51单片机的开发环境是Keil 软件。Keil 软件虽然是一个收费软件,但从uVision2到目前的uVison4版本都有破解版,在网上都可以找到下载。笔者推荐大家使用uVisong4破解版本,好处不用多说。Keil uVision4软件的压缩包里附有安装和破解说明,本文不再赘述。 开发一个单片机程序,一般都要经过这几个步骤:建立工程->建立C文件->添加C文件到工程->编写C代码->设置目标工程的选项->编译工程产生HEX文件->将HEX文件下载到单片机。本文将一步一步手把手教您开发一个LED闪烁的简单且实用的C51程序。让您从0基础起步学习开发51单片机。 安装Keil uVison4之后,第一次运行出现如图1的界面,从上往下数,依次是菜单栏、第一条工具栏、第二条工具栏,接下来左边白色部分为工程文件区(显示文件、函数、语言模板和相关书籍),右边灰色部分为文本区(编写源文件),最下边为编译信息栏(显示编译时产生的相关信息)。 一.建立工程 点击“Project”菜单项,选“New uVision Project…”,跳出创建新工程对话框,选择工程放置位置,在这里笔者选择F盘,并在根目录创建LED这个文件夹,用来放置工程文件,如图2: 打开LED文件夹,然后给新工程取个名字(可以任意取),在这里笔者取工程名字为“LED”,如图3: 点击“保存”按钮,跳出器件选择对话框,如图4: 找到Atmel单片机,选择AT89S52,同时右边的描述栏里显示了该器件的基本信息,如图5: [!--empirenews.page--] 点击“OK按钮,”跳出提示对话框,如图6: 提示对话框问您“是否将8051标准启动代码复制到工程文件夹并添加到工程?”,根据您的需要选择,一般不需要,在这里笔者选择“否”。此时,可看到keil uVision4界面左边的工程窗口里多了一个目标文件夹“Target 1”,其下有一个源文件组文件夹“Suorce Group 1”,如图7。此时,新工程已经建立,但还只是一个空的工程。 二.建立C文件和保存C文件 从图7可看到,“Suorce Group 1”这个源文件夹下还没有任何文件。接下来就是创建一个C文件。点击“File”菜单,选择“New”,文档区便出现了一个默认文件名为“Text 1”的空白文档,如图8: 在该文档上随便输入一个空格,然后点击“File”菜单,选择“Save”,跳出保存对话框,如图9,输入文件名为“LED.c”,点击“保存”按钮。这一步需要注意的是这个C文件必须保存在刚才创建的工程文件夹下,否则在后面编译时会出错。C文件的文件名一般和工程名一致。扩展名必须为“.c”。这个C文件就是您后面要编写C语言源代码的源文件,现在已经在你的工程文件夹里了。 [!--empirenews.page--] 三.添加C文件到工程 虽然“LED.c”这个文件已经在您的工程文件夹里,但还不属于你的工程文件,如果您不把它添加到工程里去,它的存在对您这个工程没有任何意义。接下来就是把C文件添加到您的工程里,让它变成您的工程文件。右键点击左边工程窗口里的“Suorce Group 1”这个源文件夹,选“Add File to Group ‘Suorce Group 1’…”,跳出添加源文件对话框,找到刚才建立的“LED.c”文件,点击“Add”按钮后,再点击“Close”按钮退出,不要重复点击,如图10: 这时可以看到“Suorce Group 1”这个源文件下多了一个源文件“LED.c”,同时,右边的文档区的文件名也改变了,如图11。 四.编写C代码 在空白的文档区输入如下C代码: /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #i nclude<reg52.h>//包含头文件 sbit led=P2^0;//定义位变量led,使其关联单片机管脚P2.0 void Delayms(unsigned int t);//声明延时函数 ///////////////////////////////////////////////////////////////////////////////// int main(void)//主函数(C语言程序入口函数) { while(1) { led=0;//P2.0拉低,点亮LED Delayms(500);//调用延时函数,延时500毫秒 led=1;//P2.0拉高,熄灭LED Delayms(500);//调用延时函数,延时500毫秒 } return 0; } ////////////////////////////////////////////////////////////////////////////////// void Delayms(unsigned int t)//定义延时函数 { unsigned int i,j; for(i=0;i<t;i++) for(j=0;j<120;j++);//大约延时1毫秒 } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 这是一个常用的指示灯闪烁的程序,输入完成以后,先不要马上编译,还需要作一些设置。 [!--empirenews.page--] 五.工程选项设置 这一步主要是设置时钟频率和产生HEX文件这两项。进入工程选项有两个途径:从“Project”菜单进入和直接点击工具栏快捷按钮进入。这里使用工具栏快捷按钮进入,点击第二条工具栏的第7个按钮(Target Opsions…),跳出选项对话框,默认选项卡为“Target”,如图12,将晶振频率设为你目标板所使用的晶振频率,这里设为12.0;再点击“Output”选项卡,将“Create HEX File ”复选框打勾,这个项设置绝不能忽略,否则您的工程就没有 HEX文件产生。其他选项保持默认。最后点击“OK”按钮,完成选项设置。 六.编译工程产生HEX文件 点击第二条工具栏的第三个按钮(Rebuild),工程便进入编译链接状态,“Build Output”信息栏就会出现相关的编译信息,如图13。从该信息栏可以知道程序的大小,使用了的多少内部RAM和外部RAM、生成多少个代码、是否生成HEX文件、有多少个错误和警告等信息。如果有错误,目标文件将不会被创建,只要双击错误信息,光标就会跳到C文档错误代码的行号或错误代码的附近,方便您排查错误。 七.将HEX文件下载到单片机 编译通过的C代码,在工程文件夹下就会生成很多文件,其中有两个文件是最重要的,一个是扩展名为“.c”文件“LED.c”,这是移植程序用的C源文件,是程序的核心,拥有这个文件就相当于拥有整个程序;另一个是扩展名为“.hex”的文件“LED.hex”,这是下载程序用的文件。这两个文件都可以用记事本打开。“LED.hex”文件是采用Intel hex文件格式存储程序代码的。下载程序的时用下载软件打开这个hex文件,将文件里的程序代码提取出来写入单片机的程序存储区里。 在下载单片机程序之前,您需要构建一个单片机最小系统,请参阅笔者的《学单片机从构建最小系统开始》一文。下载软件使用WSFISP软件,也可以用AtmelISP软件。下载线可根据笔者提供的原理图自己DIY,也可以购买。51单片机程序下载软件操作都很简单,本文不再叙述。

    时间:2012-05-28 关键词: 单片机 程序 电源技术解析 手把手 一个 编写

  • Linux网卡驱动程序编写

    工作需要写了我们公司一块网卡的Linux驱动程序。经历一个从无到有的过程,深感技术交流的重要。Linux作为挑战微软垄断的强有力武器,日益受到大家的喜爱。真希望她能在中国迅速成长。把程序文档贴出来,希望和大家探讨Linux技术和应用,促进Linux在中国的普及。 Linux操作系统网络驱动程序编写 一.Linux系统设备驱动程序概述 1.1Linux设备驱动程序分类 1.2编写驱动程序的一些基本概念 二.Linux系统网络设备驱动程序 2.1网络驱动程序的结构 2.2网络驱动程序的基本方法 2.3网络驱动程序中用到的数据结构 2.4常用的系统支持 三.编写Linux网络驱动程序中可能遇到的问题 3.1中断共享 3.2硬件发送忙时的处理 3.3流量控制(flowcontrol) 3.4调试 四.进一步的阅读 五.杂项 一.Linux系统设备驱动程序概述 1.1Linux设备驱动程序分类 Linux设备驱动程序在Linux的内核源代码中占有很大的比例,源代码的长度日益增加,主要是驱动程序的增加。在Linux内核的不断升级过程中,驱动程序的结构还是相对稳定。在2.0.xx到2.2.xx的变动里,驱动程序的编写做了一些改变,但是从2.0.xx的驱动到2.2.xx的移植只需做少量的工作。 Linux系统的设备分为字符设备(chardevice),块设备(blockdevice)和网络设备(networkdevice)三种。字符设备是指存取时没有缓存的设备。块设备的读写都有缓存来支持,并且块设备必须能够随机存取(randomaccess),字符设备则没有这个要求。典型的字符设备包括鼠标,键盘,串行口等。块设备主要包括硬盘软盘设备,CD-ROM等。一个文件系统要安装进入操作系统必须在块设备上。 网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSDunix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。 1.2编写驱动程序的一些基本概念 无论是什么操作系统的驱动程序,都有一些通用的概念。操作系统提供给驱动程序的支持也大致相同。下面简单介绍一下网络设备驱动程序的一些基本要求。 1.2.1发送和接收 这是一个网络设备最基本的功能。一块网卡所做的无非就是收发工作。所以驱动程序里要告诉系统你的发送函数在哪里,系统在有数据要发送时就会调用你的发送程序。还有驱动程序由于是直接操纵硬件的,所以网络硬件有数据收到最先能得到这个数据的也就是驱动程序,它负责把这些原始数据进行必要的处理然后送给系统。这里,操作系统必须要提供两个机制,一个是找到驱动程序的发送函数,一个是驱动程序把收到的数据送给系统。 1.2.2中断 中断在现代计算机结构中有重要的地位。操作系统必须提供驱动程序响应中断的能力。一般是把一个中断处理程序注册到系统中去。操作系统在硬件中断发生后调用驱动程序的处理程序。Linux支持中断的共享,即多个设备共享一个中断。 1.2.3时钟 在实现驱动程序时,很多地方会用到时钟。如某些协议里的超时处理,没有中断机制的硬件的轮询等。操作系统应为驱动程序提供定时机制。一般是在预定的时间过了以后回调注册的时钟函数。在网络驱动程序中,如果硬件没有中断功能,定时器可以提供轮询(poll)方式对硬件进行存取。或者是实现某些协议时需要的超时重传等。 二.Linux系统网络设备驱动程序 2.1网络驱动程序的结构 所有的Linux网络驱动程序遵循通用的接口。设计时采用的是面向对象的方法。一个设备就是一个对象(device结构),它内部有自己的数据和方法。每一个设备的方法被调用时的第一个参数都是这个设备对象本身。这样这个方法就可以存取自身的数据(类似面向对象程序设计时的this引用)。 一个网络设备最基本的方法有初始化、发送和接收。 ---------------------------------------- |deliverpackets||receivepacketsqueue| |(dev_queue_xmit())||them(netif_rx())| ---------------------------------------- ||/ /|| ------------------------------------------------------- |methodsandvariables(initialize,open,close,hard_xmit,| |interrupthandler,config,resources,status...)| ------------------------------------------------------- ||/ /|| --------------------------------------- |sendtohardware||receivcefromhardware| --------------------------------------- ||/ /|| ----------------------------------------------------- |hardwaremedia| ----------------------------------------------------- 初始化程序完成硬件的初始化、device中变量的初始化和系统资源的申请。发送程序是在驱动程序的上层协议层有数据要发送时自动调用的。一般驱动程序中不对发送数据进行缓存,而是直接使用硬件的发送功能把数据发送出去。接收数据一般是通过硬件中断来通知的。在中断处理程序里,把硬件帧信息填入一个skbuff结构中,然后调用netif_rx()传递给上层处理。 2.2网络驱动程序的基本方法 网络设备做为一个对象,提供一些方法供系统访问。正是这些有统一接口的方法,掩蔽了硬件的具体细节,让系统对各种网络设备的访问都采用统一的形式,做到硬件无关性。 下面解释最基本的方法。 2.2.1初始化(initialize) 驱动程序必须有一个初始化方法。在把驱动程序载入系统的时候会调用这个初始化程序。它做以下几方面的工作。检测设备。在初始化程序里你可以根据硬件的特征检查硬件是否存在,然后决定是否启动这个驱动程序。配置和初始化硬件。在初始化程序里你可以完成对硬件资源的配置,比如即插即用的硬件就可以在这个时候进行配置(Linux内核对PnP功能没有很好的支持,可以在驱动程序里完成这个功能)。配置或协商好硬件占用的资源以后,就可以向系统申请这些资源。有些资源是可以和别的设备共享的,如中断。有些是不能共享的,如IO、DMA。接下来你要初始化device结构中的变量。最后,你可以让硬件正式开始工作。[!--empirenews.page--] 2.2.2打开(open) open这个方法在网络设备驱动程序里是网络设备被激活的时候被调用(即设备状态由down-->up)。所以实际上很多在initialize中的工作可以放到这里来做。比如资源的申请,硬件的激活。如果dev->open返回非0(error),则硬件的状态还是down。 open方法另一个作用是如果驱动程序做为一个模块被装入,则要防止模块卸载时设备处于打开状态。在open方法里要调用MOD_INC_USE_COUNT宏。 2.2.3关闭(stop) close方法做和open相反的工作。可以释放某些资源以减少系统负担。close是在设备状态由up转为down时被调用的。另外如果是做为模块装入的驱动程序,close里应该调用MOD_DEC_USE_COUNT,减少设备被引用的次数,以使驱动程序可以被卸载。 另外close方法必须返回成功(0==success)。 2.2.4发送(hard_start_xmit) 所有的网络设备驱动程序都必须有这个发送方法。在系统调用驱动程序的xmit时,发送的数据放在一个sk_buff结构中。一般的驱动程序把数据传给硬件发出去。也有一些特殊的设备比如loopback把数据组成一个接收数据再回送给系统,或者dummy设备直接丢弃数据。 如果发送成功,hard_start_xmit方法里释放sk_buff,返回0(发送成功)。如果设备暂时无法处理,比如硬件忙,则返回1。这时如果dev->tbusy置为非0,则系统认为硬件忙,要等到dev->tbusy置0以后才会再次发送。tbusy的置0任务一般由中断完成。硬件在发送结束后产生中断,这时可以把tbusy置0,然后用mark_bh()调用通知系统可以再次发送。在发送不成功的情况下,也可以不置dev->tbusy为非0,这样系统会不断尝试重发。如果hard_start_xmit发送不成功,则不要释放sk_buff。传送下来的sk_buff中的数据已经包含硬件需要的帧头。所以在发送方法里不需要再填充硬件帧头,数据可以直接提交给硬件发送。sk_buff是被锁住的(locked),确保其他程序不会存取它。 2.2.5接收(reception) 驱动程序并不存在一个接收方法。有数据收到应该是驱动程序来通知系统的。一般设备收到数据后都会产生一个中断,在中断处理程序中驱动程序申请一块sk_buff(skb),从硬件读出数据放置到申请好的缓冲区里。接下来填充sk_buff中的一些信息。skb->dev=dev,判断收到帧的协议类型,填入skb->protocol(多协议的支持)。把指针skb->mac.raw指向硬件数据然后丢弃硬件帧头(skb_pull)。还要设置skb->pkt_type,标明第二层(链路层)数据类型。可以是以下类型: PACKET_BROADCAST:链路层广播 PACKET_MULTICAST:链路层组播 PACKET_SELF:发给自己的帧 PACKET_OTHERHOST:发给别人的帧(监听模式时会有这种帧) 最后调用netif_rx()把数据传送给协议层。netif_rx()里数据放入处理队列然后返回,真正的处理是在中断返回以后,这样可以减少中断时间。调用netif_rx()以后, 驱动程序就不能再存取数据缓冲区skb。 2.2.6硬件帧头(hard_header) 硬件一般都会在上层数据发送之前加上自己的硬件帧头,比如以太网(Ethernet)就有14字节的帧头。这个帧头是加在上层ip、ipx等数据包的前面的。驱动程序提供一个hard_header方法,协议层(ip、ipx、arp等)在发送数据之前会调用这段程序。 硬件帧头的长度必须填在dev->hard_header_len,这样协议层回在数据之前保留好硬件帧头的空间。这样hard_header程序只要调用skb_push然后正确填入硬件帧头就可以了。 在协议层调用hard_header时,传送的参数包括(2.0.xx):数据的sk_buff,device指针,protocol,目的地址(daddr),源地址(saddr),数据长度(len)。数据长度不要使用sk_buff中的参数,因为调用hard_header时数据可能还没完全组织好。saddr是NULL的话是使用缺省地址(default)。daddr是NULL表明协议层不知道硬件目的地址。如果hard_header完全填好了硬件帧头,则返回添加的字节数。如果硬件帧头中的信息还不完全(比如daddr为NULL,但是帧头中需要目的硬件地址。典型的情况是以太网需要地址解析(arp)),则返回负字节数。hard_header返回负数的情况下,协议层会做进一步的buildheader的工作。目前Linux系统里就是做arp(如果hard_header返回正,dev->arp=1,表明不需要做arp,返回负,dev->arp=0,做arp)。 对hard_header的调用在每个协议层的处理程序里。如ip_output。 2.2.7地址解析(xarp) 有些网络有硬件地址(比如Ethernet),并且在发送硬件帧时需要知道目的硬件地址。这样就需要上层协议地址(ip、ipx)和硬件地址的对应。这个对应是通过地址解析完成的。需要做arp的的设备在发送之前会调用驱动程序的rebuild_header方法。调用的主要参数包括指向硬件帧头的指针,协议层地址。如果驱动程序能够解析硬件地址,就返回1,如果不能,返回0。 对rebuild_header的调用在net/core/dev.c的do_dev_queue_xmit()里。 2.2.8参数设置和统计数据 在驱动程序里还提供一些方法供系统对设备的参数进行设置和读取信息。一般只有超级用户(root)权限才能对设备参数进行设置。设置方法有: dev->set_mac_address() 当用户调用ioctl类型为SIOCSIFHWADDR时是要设置这个设备的mac地址。一般对mac地址的设置没有太大意义的。 dev->set_config() 当用户调用ioctl时类型为SIOCSIFMAP时,系统会调用驱动程序的set_config方法。用户会传递一个ifmap结构包含需要的I/O、中断等参数。 dev->do_ioctl() 如果用户调用ioctl时类型在SIOCDEVPRIVATE和SIOCDEVPRIVATE+15之间,系统会调用驱动程序的这个方法。一般是设置设备的专用数据。 读取信息也是通过ioctl调用进行。除次之外驱动程序还可以提供一个 dev->get_stats方法,返回一个enet_statistics结构,包含发送接收的统计信息。ioctl的处理在net/core/dev.c的dev_ioctl()和dev_ifsioc()里。 linuxman@263.net .3网络驱动程序中用到的数据结构 最重要的是网络设备的数据结构。定义在include/linux/netdevice.h里。它的注释已经足够详尽。 structdevice[!--empirenews.page--] { /* *Thisisthefirstfieldofthe"visible"partofthisstructure *(i.e.asseenbyusersinthe"Space.c"file).Itisthename *theinterface. */ char*name; /*I/Ospecificfields-FIXME:Mergetheseandstructifmapintoone*/ unsignedlongrmem_end;/*shmem"recv"end*/ unsignedlongrmem_start;/*shmem"recv"start*/ unsignedlongmem_end;/*sharedmemend*/ unsignedlongmem_start;/*sharedmemstart*/ unsignedlongbase_addr;/*deviceI/Oaddress*/ unsignedcharirq;/*deviceIRQnumber*/ /*Low-levelstatusflags.*/ volatileunsignedcharstart,/*startanoperation*/ interrupt;/*interruptarrived*/ /*在处理中断时interrupt设为1,处理完清0。*/ unsignedlongtbusy;/*transmitterbusymustbelongfor bitops*/ structdevice*next; /*Thedeviceinitializationfunction.Calledonlyonce.*/ /*指向驱动程序的初始化方法。*/ int(*init)(structdevice*dev); /*Somehardwarealsoneedsthesefields,buttheyarenotpartofthe usualsetspecifiedinSpace.c.*/ /*一些硬件可以在一块板上支持多个接口,可能用到if_port。*/ unsignedcharif_port;/*SelectableAUI,TP,..*/ unsignedchardma;/*DMAchannel*/ structenet_statistics*(*get_stats)(structdevice*dev); /* *Thismarkstheendofthe"visible"partofthestructure.All *fieldshereafterareinternaltothesystem,andmaychangeat *will(read:maybecleanedupatwill). */ /*Thesemaybeneededforfuturenetwork-power-downcode.*/ /*trans_start记录最后一次成功发送的时间。可以用来确定硬件是否工作正常。*/ unsignedlongtrans_start;/*Time(injiffies)oflastTx*/ unsignedlonglast_rx;/*TimeoflastRx*/ /*flags里面有很多内容,定义在include/linux/if.h里。*/ unsignedshortflags;/*interfaceflags(alaBSD)*/ unsignedshortfamily;/*addressfamilyID(AF_INET)*/ unsignedshortmetric;/*routingmetric(notused)*/ unsignedshortmtu;/*interfaceMTUvalue*/ /*type标明物理硬件的类型。主要说明硬件是否需要arp。定义在 include/linux/if_arp.h里。*/ unsignedshorttype;/*interfacehardwaretype*/ /*上层协议层根据hard_header_len在发送数据缓冲区前面预留硬件帧头空间。*/ unsignedshorthard_header_len;/*hardwarehdrlength*/ /*priv指向驱动程序自己定义的一些参数。*/ void*priv;/*pointertoprivatedata*/ /*Interfaceaddressinfo.*/ unsignedcharbroadcast[MAX_ADDR_LEN];/*hwbcastadd*/ unsignedcharpad;/*makedev_addralignedto8 bytes*/ unsignedchardev_addr[MAX_ADDR_LEN];/*hwaddress*/ unsignedcharaddr_len;/*hardwareaddresslength*/ unsignedlongpa_addr;/*protocoladdress*/ unsignedlongpa_brdaddr;/*protocolbroadcastaddr*/ unsignedlongpa_dstaddr;/*protocolP-Pothersideaddr*/ unsignedlongpa_mask;/*protocolnetmask*/ unsignedshortpa_alen;/*protocoladdresslength*/ structdev_mc_list*mc_list;/*Multicastmacaddresses*/ intmc_count;/*Numberofinstalledmcasts*/ structip_mc_list*ip_mc_list;/*IPmulticastfilterchain*/ __u32tx_queue_len;/*Maxframesperqueueallowed*/ /*Forloadbalancingdriverpairsupport*/ unsignedlongpkt_queue;/*Packetsqueued*/ structdevice*slave;/*Slavedevice*/ structnet_alias_info*alias_info;/*maindevaliasinfo*/ structnet_alias*my_alias;/*aliasdevs*/ /*Pointertotheinterfacebuffers.*/ structsk_buff_headbuffs[DEV_NUMBUFFS]; /*Pointerstointerfaceserviceroutines.*/ int(*open)(structdevice*dev); int(*stop)(structdevice*dev); int(*hard_start_xmit)(structsk_buff*skb, structdevice*dev); int(*hard_header)(structsk_buff*skb, structdevice*dev, unsignedshorttype, void*daddr, void*saddr, unsignedlen); int(*rebuild_header)(void*eth,structdevice*dev, unsignedlongraddr,structsk_buff*skb); #defineHAVE_MULTICAST void(*set_multicast_list)(structdevice*dev); #defineHAVE_SET_MAC_ADDR int(*set_mac_address)(structdevice*dev,void*addr); #defineHAVE_PRIVATE_IOCTL int(*do_ioctl)(structdevice*dev,structifreq*ifr,intcmd); #defineHAVE_SET_CONFIG int(*set_config)(structdevice*dev,structifmap*map); #defineHAVE_HEADER_CACHE void(*header_cache_bind)(structhh_cache**hhp,structdevice *dev,unsignedshorthtype,__u32daddr); void(*header_cache_update)(structhh_cache*hh,structdevice *dev,unsignedchar*haddr); #defineHAVE_CHANGE_MTU int(*change_mtu)(structdevice*dev,intnew_mtu); structiw_statistics*(*get_wireless_stats)(structdevice*dev); }; 2.4常用的系统支持 2.4.1内存申请和释放 include/linux/kernel.h里声明了kmalloc()和kfree()。用于在内核模式下申请和释放内存。 void*kmalloc(unsignedintlen,intpriority); voidkfree(void*__ptr); [!--empirenews.page--]与用户模式下的malloc()不同,kmalloc()申请空间有大小限制。长度是2的整次方。可以申请的最大长度也有限制。另外kmalloc()有priority参数,通常使用时可以为GFP_KERNEL,如果在中断里调用用GFP_ATOMIC参数,因为使用GFP_KERNEL则调用者可能进入sleep状态,在处理中断时是不允许的。 kfree()释放的内存必须是kmalloc()申请的。如果知道内存的大小,也可以用kfree_s()释放。 2.4.2request_irq()、free_irq() 这是驱动程序申请中断和释放中断的调用。在include/linux/sched.h里声明。 request_irq()调用的定义: intrequest_irq(unsignedintirq, void(*handler)(intirq,void*dev_id,structpt_regs*regs), unsignedlongirqflags, constchar*devname, void*dev_id); irq是要申请的硬件中断号。在Intel平台,范围0--15。handler是向系统登记的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件中断号,deviceid,寄存器值。dev_id就是下面的request_irq时传递给系统的参数dev_id。irqflags是中断处理的一些属性。比较重要的有SA_INTERRUPT, 标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有一个SA_SHIRQ属性,设置了以后运行多个设备共享中断。dev_id在中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用irq2dev_map找到中断对应的设备。 voidfree_irq(unsignedintirq,void*dev_id); 2.4.3时钟 时钟的处理类似中断,也是登记一个时间处理函数,在预定的时间过后,系统会调用这个函数。在include/linux/timer.h里声明。 structtimer_list{ structtimer_list*next; structtimer_list*prev; unsignedlongexpires; unsignedlongdata; void(*function)(unsignedlong); }; voidadd_timer(structtimer_list*timer); intdel_timer(structtimer_list*timer); voidinit_timer(structtimer_list*timer); 使用时钟,先声明一个timer_list结构,调用init_timer对它进行初始化。 time_list结构里expires是标明这个时钟的周期,单位采用jiffies的单位。 jiffies是Linux一个全局变量,代表时间。它的单位随硬件平台的不同而不同。 系统里定义了一个常数HZ,代表每秒种最小时间间隔的数目。这样jiffies的单位就是1/HZ。Intel平台jiffies的单位是1/100秒,这就是系统所能分辨的最小时间间隔了。所以expires/HZ就是以秒为单位的这个时钟的周期。 function就是时间到了以后的回调函数,它的参数就是timer_list中的data。data这个参数在初始化时钟的时候赋值,一般赋给它设备的device结构指针。 在预置时间到系统调用function,同时系统把这个time_list从定时队列里清除。所以如果需要一直使用定时函数,要在function里再次调用add_timer()把这个timer_list加进定时队列。 2.4.4I/O I/O端口的存取使用: inlineunsignedintinb(unsignedshortport); inlineunsignedintinb_p(unsignedshortport); inlinevoidoutb(charvalue,unsignedshortport); inlinevoidoutb_p(charvalue,unsignedshortport); 在include/adm/io.h里定义。 inb_p()、outb_p()与inb()、outb_p()的不同在于前者在存取I/O时有等待(pause)一适应慢速的I/O设备。 为了防止存取I/O时发生冲突,Linux提供对端口使用情况的控制。在使用端口之前,可以检查需要的I/O是否正在被使用,如果没有,则把端口标记为正在使用,使用完后再释放。系统提供以下几个函数做这些工作。 intcheck_region(unsignedintfrom,unsignedintextent); voidrequest_region(unsignedintfrom,unsignedintextent,constchar*name); voidrelease_region(unsignedintfrom,unsignedintextent); 其中的参数from表示用到的I/O端口的起始地址,extent标明从from开始的端口数目。name为设备名称。 2.4.5中断打开关闭 系统提供给驱动程序开放和关闭响应中断的能力。是在include/asm/system.h中的两个定义。 #definecli()__asm____volatile__("cli"::) #definesti()__asm____volatile__("sti"::) 2.4.6打印信息 类似普通程序里的printf(),驱动程序要输出信息使用printk()。在include/linux/kernel.h里声明。 intprintk(constchar*fmt,...); 其中fmt是格式化字符串。...是参数。都是和printf()格式一样的。 2.4.7注册驱动程序 如果使用模块(module)方式加载驱动程序,需要在模块初始化时把设备注册到系统设备表里去。不再使用时,把设备从系统中卸除。定义在drivers/net/net_init.h里的两个函数完成这个工作。 intregister_netdev(structdevice*dev); voidunregister_netdev(structdevice*dev); dev就是要注册进系统的设备结构指针。在register_netdev()时,dev结构一般填写前面11项,即到init,后面的暂时可以不用初始化。最重要的是name指针和init方法。name指针空(NULL)或者内容为或者name[0]为空格(space),则系统把你的设备做为以太网设备处理。以太网设备有统一的命名格式,ethX。对以太网这么特别对待大概和Linux的历史有关。 init方法一定要提供,register_netdev()会调用这个方法让你对硬件检测和设置。 register_netdev()返回0表示成功,非0不成功。 2.4.8sk_buff Linux网络各层之间的数据传送都是通过sk_buff。sk_buff提供一套管理缓冲区的方法,是Linux系统网络高效运行的关键。每个sk_buff包括一些控制方法和一块数据缓冲区。控制方法按功能分为两种类型。一种是控制整个buffer链的方法, 另一种是控制数据缓冲区的方法。sk_buff组织成双向链表的形式,根据网络应用的特点,对链表的操作主要是删除链表头的元素和添加到链表尾。sk_buff的控制[!--empirenews.page--] 方法都很短小以尽量减少系统负荷。(translatedfromarticlewrittenbyAlanCox) 常用的方法包括: .alloc_skb()申请一个sk_buff并对它初始化。返回就是申请到的sk_buff。 .dev_alloc_skb()类似alloc_skb,在申请好缓冲区后,保留16字节的帧头空间。主要用在Ethernet驱动程序。 .kfree_skb()释放一个sk_buff。 .skb_clone()复制一个sk_buff,但不复制数据部分。 .skb_copy()完全复制一个sk_buff。 .skb_dequeue()从一个sk_buff链表里取出第一个元素。返回取出的sk_buff,如果链表空则返回NULL。这是常用的一个操作。 .skb_queue_head()在一个sk_buff链表头放入一个元素。 .skb_queue_tail()在一个sk_buff链表尾放入一个元素。这也是常用的一个操作。网络数据的处理主要是对一个先进先出队列的管理,skb_queue_tail() 和skb_dequeue()完成这个工作。 .skb_insert()在链表的某个元素前插入一个元素。 .skb_append()在链表的某个元素后插入一个元素。一些协议(如TCP)对没按顺序到达的数据进行重组时用到skb_insert()和skb_append()。 .skb_reserve()在一个申请好的sk_buff的缓冲区里保留一块空间。这个空间一般是用做下一层协议的头空间的。 .skb_put()在一个申请好的sk_buff的缓冲区里为数据保留一块空间。在 alloc_skb以后,申请到的sk_buff的缓冲区都是处于空(free)状态,有一个tail指针指向free空间,实际上开始时tail就指向缓冲区头。skb_reserve() 在free空间里申请协议头空间,skb_put()申请数据空间。见下面的图。 .skb_push()把sk_buff缓冲区里数据空间往前移。即把Headroom中的空间移一部分到Dataarea。 .skb_pull()把sk_buff缓冲区里Dataarea中的空间移一部分到Headroom中。 -------------------------------------------------- |Tailroom(free)| -------------------------------------------------- Afteralloc_skb() -------------------------------------------------- |Headroom|Tailroom(free)| -------------------------------------------------- Afterskb_reserve() -------------------------------------------------- |Headroom|Dataarea|Tailroom(free)| -------------------------------------------------- Afterskb_put() -------------------------------------------------- |Head|skb_|Data|Tailroom(free)| |room|push||| ||Dataarea|| -------------------------------------------------- Afterskb_push() -------------------------------------------------- |Head|skb_|Dataarea|Tailroom(free)| ||pull||| |Headroom||| -------------------------------------------------- Afterskb_pull() 三.编写Linux网络驱动程序中需要注意的问题 3.1中断共享 Linux系统运行几个设备共享同一个中断。需要共享的话,在申请的时候指明共享方式。系统提供的request_irq()调用的定义: intrequest_irq(unsignedintirq, void(*handler)(intirq,void*dev_id,structpt_regs*regs), unsignedlongirqflags, constchar*devname, void*dev_id); 如果共享中断,irqflags设置SA_SHIRQ属性,这样就允许别的设备申请同一个中断。需要注意所有用到这个中断的设备在调用request_irq()都必须设置这个属性。系统在回调每个中断处理程序时,可以用dev_id这个参数找到相应的设备。一般dev_id就设为device结构本身。系统处理共享中断是用各自的dev_id参数依次调用每一个中断处理程序。 3.2硬件发送忙时的处理 主CPU的处理能力一般比网络发送要快,所以经常会遇到系统有数据要发,但上一包数据网络设备还没发送完。因为在Linux里网络设备驱动程序一般不做数据缓存,不能发送的数据都是通知系统发送不成功,所以必须要有一个机制在硬件不忙时及时通知系统接着发送下面的数据。 一般对发送忙的处理在前面设备的发送方法(hard_start_xmit)里已经描述过,即如果发送忙,置tbusy为1。处理完发送数据后,在发送结束中断里清tbusy,同时用mark_bh()调用通知系统继续发送。 但在具体实现我的驱动程序时发现,这样的处理系统好象并不能及时地知道硬件已经空闲了,即在mark_bh()以后,系统要等一段时间才会接着发送。造成发送效率很低。2M线路只有10%不到的使用率。内核版本为2.0.35。 我最后的实现是不把tbusy置1,让系统始终认为硬件空闲,但是报告发送不成功。系统会一直尝试重发。这样处理就运行正常了。但是遍循内核源码中的网络驱动程序,似乎没有这样处理的。不知道症结在哪里。 3.3流量控制(flowcontrol) 网络数据的发送和接收都需要流量控制。这些控制是在系统里实现的,不需要驱动程序做工作。每个设备数据结构里都有一个参数dev->tx_queue_len,这个参数标明发送时最多缓存的数据包。在Linux系统里以太网设备(10/100Mbps)tx_queue_len一般设置为100,串行线路(异步串口)为10。实际上如果看源码可以知道,设置了dev->tx_queue_len并不是为缓存这些数据申请了空间。这个参数只是在收到协议层的数据包时判断发送队列里的数据是不是到了tx_queue_len的限度,以决定这一包数据加不加进发送队列。发送时另一个方面的流控是更高层协议的发送窗口(TCP协议里就有发送窗口)。达到了窗口大小,高层协议就不会再发送数据。 接收流控也分两个层次。netif_rx()缓存的数据包有限制。另外高层协议也会有一个最大的等待处理的数据量。 发送和接收流控处理在net/core/dev.c的do_dev_queue_xmit()和netif_rx()中。 3.4调试 很多Linux的驱动程序都是编译进内核的,形成一个大的内核文件。但对调试来说,这是相当麻烦的。调试驱动程序可以用module方式加载。支持模块方式的驱动程序必须提供两个函数:intinit_module(void)和voidcleanup_module(void)。init_module()在加载此模块时调用,在这个函数里可以register_netdev()注册设备。init_module()返回0表示成功,返回负表示失败。cleanup_module()在驱动程序被卸载时调用,清除占用的资源,调用unregister_netdev()。[!--empirenews.page--] 模块可以动态地加载、卸载。在2.0.xx版本里,还有kerneld自动加载模块,但是2.2.xx中已经取消了kerneld。手工加载使用insmod命令,卸载用rmmod命令,看内核中的模块用lsmod命令。 编译驱动程序用gcc,主要命令行参数-DKERNEL-DMODULE。并且作为模块加载的驱动程序,只编译成obj形式(加-c参数)。编译好的目标文件放在/lib/modules/2.x.xx/misc下,在启动文件里用insmod加载。 四.进一步的阅读 Linux程序设计资料可以从网上获得。这就是开放源代码的好处。并且没有什么“未公开的秘密”。我编写驱动程序时参阅的主要资料包括: Linux内核源代码 < >byMichaelK.Johnson < >byOriPomerantz byollyinBBS水木清华站   可以选择一个模板作为开始,内核源代码里有一个网络驱动程序的模板, drivers/net/skeleton.c。里面包含了驱动程序的基本内容。但这个模板是以以太网设备为对象的,以太网的处理在Linux系统里有特殊“待遇”,所以如果不是以太网设备,有些细节上要注意,主要在初始化程序里。 最后,多参照别人写的程序,听听其他开发者的经验之谈大概是最有效的帮助了。

    时间:2012-05-07 关键词: Linux 电源技术解析 网卡 驱动程序 编写

  • Linux网卡驱动程序编写

    工作需要写了我们公司一块网卡的Linux驱动程序。经历一个从无到有的过程,深感技术交流的重要。Linux作为挑战微软垄断的强有力武器,日益受到大家的喜爱。真希望她能在中国迅速成长。把程序文档贴出来,希望和大家探讨Linux技术和应用,促进Linux在中国的普及。 Linux操作系统网络驱动程序编写 一.Linux系统设备驱动程序概述 1.1Linux设备驱动程序分类 1.2编写驱动程序的一些基本概念 二.Linux系统网络设备驱动程序 2.1网络驱动程序的结构 2.2网络驱动程序的基本方法 2.3网络驱动程序中用到的数据结构 2.4常用的系统支持 三.编写Linux网络驱动程序中可能遇到的问题 3.1中断共享 3.2硬件发送忙时的处理 3.3流量控制(flowcontrol) 3.4调试 四.进一步的阅读 五.杂项 一.Linux系统设备驱动程序概述 1.1Linux设备驱动程序分类 Linux设备驱动程序在Linux的内核源代码中占有很大的比例,源代码的长度日益增加,主要是驱动程序的增加。在Linux内核的不断升级过程中,驱动程序的结构还是相对稳定。在2.0.xx到2.2.xx的变动里,驱动程序的编写做了一些改变,但是从2.0.xx的驱动到2.2.xx的移植只需做少量的工作。 Linux系统的设备分为字符设备(chardevice),块设备(blockdevice)和网络设备(networkdevice)三种。字符设备是指存取时没有缓存的设备。块设备的读写都有缓存来支持,并且块设备必须能够随机存取(randomaccess),字符设备则没有这个要求。典型的字符设备包括鼠标,键盘,串行口等。块设备主要包括硬盘软盘设备,CD-ROM等。一个文件系统要安装进入操作系统必须在块设备上。 网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSDunix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。 1.2编写驱动程序的一些基本概念 无论是什么操作系统的驱动程序,都有一些通用的概念。操作系统提供给驱动程序的支持也大致相同。下面简单介绍一下网络设备驱动程序的一些基本要求。 1.2.1发送和接收 这是一个网络设备最基本的功能。一块网卡所做的无非就是收发工作。所以驱动程序里要告诉系统你的发送函数在哪里,系统在有数据要发送时就会调用你的发送程序。还有驱动程序由于是直接操纵硬件的,所以网络硬件有数据收到最先能得到这个数据的也就是驱动程序,它负责把这些原始数据进行必要的处理然后送给系统。这里,操作系统必须要提供两个机制,一个是找到驱动程序的发送函数,一个是驱动程序把收到的数据送给系统。 1.2.2中断 中断在现代计算机结构中有重要的地位。操作系统必须提供驱动程序响应中断的能力。一般是把一个中断处理程序注册到系统中去。操作系统在硬件中断发生后调用驱动程序的处理程序。Linux支持中断的共享,即多个设备共享一个中断。 1.2.3时钟 在实现驱动程序时,很多地方会用到时钟。如某些协议里的超时处理,没有中断机制的硬件的轮询等。操作系统应为驱动程序提供定时机制。一般是在预定的时间过了以后回调注册的时钟函数。在网络驱动程序中,如果硬件没有中断功能,定时器可以提供轮询(poll)方式对硬件进行存取。或者是实现某些协议时需要的超时重传等。 二.Linux系统网络设备驱动程序 2.1网络驱动程序的结构 所有的Linux网络驱动程序遵循通用的接口。设计时采用的是面向对象的方法。一个设备就是一个对象(device结构),它内部有自己的数据和方法。每一个设备的方法被调用时的第一个参数都是这个设备对象本身。这样这个方法就可以存取自身的数据(类似面向对象程序设计时的this引用)。 一个网络设备最基本的方法有初始化、发送和接收。 ---------------------------------------- |deliverpackets||receivepacketsqueue| |(dev_queue_xmit())||them(netif_rx())| ---------------------------------------- ||/ /|| ------------------------------------------------------- |methodsandvariables(initialize,open,close,hard_xmit,| |interrupthandler,config,resources,status...)| ------------------------------------------------------- ||/ /|| --------------------------------------- |sendtohardware||receivcefromhardware| --------------------------------------- ||/ /|| ----------------------------------------------------- |hardwaremedia| ----------------------------------------------------- 初始化程序完成硬件的初始化、device中变量的初始化和系统资源的申请。发送程序是在驱动程序的上层协议层有数据要发送时自动调用的。一般驱动程序中不对发送数据进行缓存,而是直接使用硬件的发送功能把数据发送出去。接收数据一般是通过硬件中断来通知的。在中断处理程序里,把硬件帧信息填入一个skbuff结构中,然后调用netif_rx()传递给上层处理。 2.2网络驱动程序的基本方法 网络设备做为一个对象,提供一些方法供系统访问。正是这些有统一接口的方法,掩蔽了硬件的具体细节,让系统对各种网络设备的访问都采用统一的形式,做到硬件无关性。 下面解释最基本的方法。 2.2.1初始化(initialize) 驱动程序必须有一个初始化方法。在把驱动程序载入系统的时候会调用这个初始化程序。它做以下几方面的工作。检测设备。在初始化程序里你可以根据硬件的特征检查硬件是否存在,然后决定是否启动这个驱动程序。配置和初始化硬件。在初始化程序里你可以完成对硬件资源的配置,比如即插即用的硬件就可以在这个时候进行配置(Linux内核对PnP功能没有很好的支持,可以在驱动程序里完成这个功能)。配置或协商好硬件占用的资源以后,就可以向系统申请这些资源。有些资源是可以和别的设备共享的,如中断。有些是不能共享的,如IO、DMA。接下来你要初始化device结构中的变量。最后,你可以让硬件正式开始工作。[!--empirenews.page--] 2.2.2打开(open) open这个方法在网络设备驱动程序里是网络设备被激活的时候被调用(即设备状态由down-->up)。所以实际上很多在initialize中的工作可以放到这里来做。比如资源的申请,硬件的激活。如果dev->open返回非0(error),则硬件的状态还是down。 open方法另一个作用是如果驱动程序做为一个模块被装入,则要防止模块卸载时设备处于打开状态。在open方法里要调用MOD_INC_USE_COUNT宏。 2.2.3关闭(stop) close方法做和open相反的工作。可以释放某些资源以减少系统负担。close是在设备状态由up转为down时被调用的。另外如果是做为模块装入的驱动程序,close里应该调用MOD_DEC_USE_COUNT,减少设备被引用的次数,以使驱动程序可以被卸载。 另外close方法必须返回成功(0==success)。 2.2.4发送(hard_start_xmit) 所有的网络设备驱动程序都必须有这个发送方法。在系统调用驱动程序的xmit时,发送的数据放在一个sk_buff结构中。一般的驱动程序把数据传给硬件发出去。也有一些特殊的设备比如loopback把数据组成一个接收数据再回送给系统,或者dummy设备直接丢弃数据。 如果发送成功,hard_start_xmit方法里释放sk_buff,返回0(发送成功)。如果设备暂时无法处理,比如硬件忙,则返回1。这时如果dev->tbusy置为非0,则系统认为硬件忙,要等到dev->tbusy置0以后才会再次发送。tbusy的置0任务一般由中断完成。硬件在发送结束后产生中断,这时可以把tbusy置0,然后用mark_bh()调用通知系统可以再次发送。在发送不成功的情况下,也可以不置dev->tbusy为非0,这样系统会不断尝试重发。如果hard_start_xmit发送不成功,则不要释放sk_buff。传送下来的sk_buff中的数据已经包含硬件需要的帧头。所以在发送方法里不需要再填充硬件帧头,数据可以直接提交给硬件发送。sk_buff是被锁住的(locked),确保其他程序不会存取它。 2.2.5接收(reception) 驱动程序并不存在一个接收方法。有数据收到应该是驱动程序来通知系统的。一般设备收到数据后都会产生一个中断,在中断处理程序中驱动程序申请一块sk_buff(skb),从硬件读出数据放置到申请好的缓冲区里。接下来填充sk_buff中的一些信息。skb->dev=dev,判断收到帧的协议类型,填入skb->protocol(多协议的支持)。把指针skb->mac.raw指向硬件数据然后丢弃硬件帧头(skb_pull)。还要设置skb->pkt_type,标明第二层(链路层)数据类型。可以是以下类型: PACKET_BROADCAST:链路层广播 PACKET_MULTICAST:链路层组播 PACKET_SELF:发给自己的帧 PACKET_OTHERHOST:发给别人的帧(监听模式时会有这种帧) 最后调用netif_rx()把数据传送给协议层。netif_rx()里数据放入处理队列然后返回,真正的处理是在中断返回以后,这样可以减少中断时间。调用netif_rx()以后, 驱动程序就不能再存取数据缓冲区skb。 2.2.6硬件帧头(hard_header) 硬件一般都会在上层数据发送之前加上自己的硬件帧头,比如以太网(Ethernet)就有14字节的帧头。这个帧头是加在上层ip、ipx等数据包的前面的。驱动程序提供一个hard_header方法,协议层(ip、ipx、arp等)在发送数据之前会调用这段程序。 硬件帧头的长度必须填在dev->hard_header_len,这样协议层回在数据之前保留好硬件帧头的空间。这样hard_header程序只要调用skb_push然后正确填入硬件帧头就可以了。 在协议层调用hard_header时,传送的参数包括(2.0.xx):数据的sk_buff,device指针,protocol,目的地址(daddr),源地址(saddr),数据长度(len)。数据长度不要使用sk_buff中的参数,因为调用hard_header时数据可能还没完全组织好。saddr是NULL的话是使用缺省地址(default)。daddr是NULL表明协议层不知道硬件目的地址。如果hard_header完全填好了硬件帧头,则返回添加的字节数。如果硬件帧头中的信息还不完全(比如daddr为NULL,但是帧头中需要目的硬件地址。典型的情况是以太网需要地址解析(arp)),则返回负字节数。hard_header返回负数的情况下,协议层会做进一步的buildheader的工作。目前Linux系统里就是做arp(如果hard_header返回正,dev->arp=1,表明不需要做arp,返回负,dev->arp=0,做arp)。 对hard_header的调用在每个协议层的处理程序里。如ip_output。 2.2.7地址解析(xarp) 有些网络有硬件地址(比如Ethernet),并且在发送硬件帧时需要知道目的硬件地址。这样就需要上层协议地址(ip、ipx)和硬件地址的对应。这个对应是通过地址解析完成的。需要做arp的的设备在发送之前会调用驱动程序的rebuild_header方法。调用的主要参数包括指向硬件帧头的指针,协议层地址。如果驱动程序能够解析硬件地址,就返回1,如果不能,返回0。 对rebuild_header的调用在net/core/dev.c的do_dev_queue_xmit()里。 2.2.8参数设置和统计数据 在驱动程序里还提供一些方法供系统对设备的参数进行设置和读取信息。一般只有超级用户(root)权限才能对设备参数进行设置。设置方法有: dev->set_mac_address() 当用户调用ioctl类型为SIOCSIFHWADDR时是要设置这个设备的mac地址。一般对mac地址的设置没有太大意义的。 dev->set_config() 当用户调用ioctl时类型为SIOCSIFMAP时,系统会调用驱动程序的set_config方法。用户会传递一个ifmap结构包含需要的I/O、中断等参数。 dev->do_ioctl() 如果用户调用ioctl时类型在SIOCDEVPRIVATE和SIOCDEVPRIVATE+15之间,系统会调用驱动程序的这个方法。一般是设置设备的专用数据。 读取信息也是通过ioctl调用进行。除次之外驱动程序还可以提供一个 dev->get_stats方法,返回一个enet_statistics结构,包含发送接收的统计信息。ioctl的处理在net/core/dev.c的dev_ioctl()和dev_ifsioc()里。 linuxman@263.net .3网络驱动程序中用到的数据结构 最重要的是网络设备的数据结构。定义在include/linux/netdevice.h里。它的注释已经足够详尽。 structdevice[!--empirenews.page--] { /* *Thisisthefirstfieldofthe"visible"partofthisstructure *(i.e.asseenbyusersinthe"Space.c"file).Itisthename *theinterface. */ char*name; /*I/Ospecificfields-FIXME:Mergetheseandstructifmapintoone*/ unsignedlongrmem_end;/*shmem"recv"end*/ unsignedlongrmem_start;/*shmem"recv"start*/ unsignedlongmem_end;/*sharedmemend*/ unsignedlongmem_start;/*sharedmemstart*/ unsignedlongbase_addr;/*deviceI/Oaddress*/ unsignedcharirq;/*deviceIRQnumber*/ /*Low-levelstatusflags.*/ volatileunsignedcharstart,/*startanoperation*/ interrupt;/*interruptarrived*/ /*在处理中断时interrupt设为1,处理完清0。*/ unsignedlongtbusy;/*transmitterbusymustbelongfor bitops*/ structdevice*next; /*Thedeviceinitializationfunction.Calledonlyonce.*/ /*指向驱动程序的初始化方法。*/ int(*init)(structdevice*dev); /*Somehardwarealsoneedsthesefields,buttheyarenotpartofthe usualsetspecifiedinSpace.c.*/ /*一些硬件可以在一块板上支持多个接口,可能用到if_port。*/ unsignedcharif_port;/*SelectableAUI,TP,..*/ unsignedchardma;/*DMAchannel*/ structenet_statistics*(*get_stats)(structdevice*dev); /* *Thismarkstheendofthe"visible"partofthestructure.All *fieldshereafterareinternaltothesystem,andmaychangeat *will(read:maybecleanedupatwill). */ /*Thesemaybeneededforfuturenetwork-power-downcode.*/ /*trans_start记录最后一次成功发送的时间。可以用来确定硬件是否工作正常。*/ unsignedlongtrans_start;/*Time(injiffies)oflastTx*/ unsignedlonglast_rx;/*TimeoflastRx*/ /*flags里面有很多内容,定义在include/linux/if.h里。*/ unsignedshortflags;/*interfaceflags(alaBSD)*/ unsignedshortfamily;/*addressfamilyID(AF_INET)*/ unsignedshortmetric;/*routingmetric(notused)*/ unsignedshortmtu;/*interfaceMTUvalue*/ /*type标明物理硬件的类型。主要说明硬件是否需要arp。定义在 include/linux/if_arp.h里。*/ unsignedshorttype;/*interfacehardwaretype*/ /*上层协议层根据hard_header_len在发送数据缓冲区前面预留硬件帧头空间。*/ unsignedshorthard_header_len;/*hardwarehdrlength*/ /*priv指向驱动程序自己定义的一些参数。*/ void*priv;/*pointertoprivatedata*/ /*Interfaceaddressinfo.*/ unsignedcharbroadcast[MAX_ADDR_LEN];/*hwbcastadd*/ unsignedcharpad;/*makedev_addralignedto8 bytes*/ unsignedchardev_addr[MAX_ADDR_LEN];/*hwaddress*/ unsignedcharaddr_len;/*hardwareaddresslength*/ unsignedlongpa_addr;/*protocoladdress*/ unsignedlongpa_brdaddr;/*protocolbroadcastaddr*/ unsignedlongpa_dstaddr;/*protocolP-Pothersideaddr*/ unsignedlongpa_mask;/*protocolnetmask*/ unsignedshortpa_alen;/*protocoladdresslength*/ structdev_mc_list*mc_list;/*Multicastmacaddresses*/ intmc_count;/*Numberofinstalledmcasts*/ structip_mc_list*ip_mc_list;/*IPmulticastfilterchain*/ __u32tx_queue_len;/*Maxframesperqueueallowed*/ /*Forloadbalancingdriverpairsupport*/ unsignedlongpkt_queue;/*Packetsqueued*/ structdevice*slave;/*Slavedevice*/ structnet_alias_info*alias_info;/*maindevaliasinfo*/ structnet_alias*my_alias;/*aliasdevs*/ /*Pointertotheinterfacebuffers.*/ structsk_buff_headbuffs[DEV_NUMBUFFS]; /*Pointerstointerfaceserviceroutines.*/ int(*open)(structdevice*dev); int(*stop)(structdevice*dev); int(*hard_start_xmit)(structsk_buff*skb, structdevice*dev); int(*hard_header)(structsk_buff*skb, structdevice*dev, unsignedshorttype, void*daddr, void*saddr, unsignedlen); int(*rebuild_header)(void*eth,structdevice*dev, unsignedlongraddr,structsk_buff*skb); #defineHAVE_MULTICAST void(*set_multicast_list)(structdevice*dev); #defineHAVE_SET_MAC_ADDR int(*set_mac_address)(structdevice*dev,void*addr); #defineHAVE_PRIVATE_IOCTL int(*do_ioctl)(structdevice*dev,structifreq*ifr,intcmd); #defineHAVE_SET_CONFIG int(*set_config)(structdevice*dev,structifmap*map); #defineHAVE_HEADER_CACHE void(*header_cache_bind)(structhh_cache**hhp,structdevice *dev,unsignedshorthtype,__u32daddr); void(*header_cache_update)(structhh_cache*hh,structdevice *dev,unsignedchar*haddr); #defineHAVE_CHANGE_MTU int(*change_mtu)(structdevice*dev,intnew_mtu); structiw_statistics*(*get_wireless_stats)(structdevice*dev); }; 2.4常用的系统支持 2.4.1内存申请和释放 include/linux/kernel.h里声明了kmalloc()和kfree()。用于在内核模式下申请和释放内存。 void*kmalloc(unsignedintlen,intpriority); voidkfree(void*__ptr); [!--empirenews.page--]与用户模式下的malloc()不同,kmalloc()申请空间有大小限制。长度是2的整次方。可以申请的最大长度也有限制。另外kmalloc()有priority参数,通常使用时可以为GFP_KERNEL,如果在中断里调用用GFP_ATOMIC参数,因为使用GFP_KERNEL则调用者可能进入sleep状态,在处理中断时是不允许的。 kfree()释放的内存必须是kmalloc()申请的。如果知道内存的大小,也可以用kfree_s()释放。 2.4.2request_irq()、free_irq() 这是驱动程序申请中断和释放中断的调用。在include/linux/sched.h里声明。 request_irq()调用的定义: intrequest_irq(unsignedintirq, void(*handler)(intirq,void*dev_id,structpt_regs*regs), unsignedlongirqflags, constchar*devname, void*dev_id); irq是要申请的硬件中断号。在Intel平台,范围0--15。handler是向系统登记的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件中断号,deviceid,寄存器值。dev_id就是下面的request_irq时传递给系统的参数dev_id。irqflags是中断处理的一些属性。比较重要的有SA_INTERRUPT, 标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有一个SA_SHIRQ属性,设置了以后运行多个设备共享中断。dev_id在中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用irq2dev_map找到中断对应的设备。 voidfree_irq(unsignedintirq,void*dev_id); 2.4.3时钟 时钟的处理类似中断,也是登记一个时间处理函数,在预定的时间过后,系统会调用这个函数。在include/linux/timer.h里声明。 structtimer_list{ structtimer_list*next; structtimer_list*prev; unsignedlongexpires; unsignedlongdata; void(*function)(unsignedlong); }; voidadd_timer(structtimer_list*timer); intdel_timer(structtimer_list*timer); voidinit_timer(structtimer_list*timer); 使用时钟,先声明一个timer_list结构,调用init_timer对它进行初始化。 time_list结构里expires是标明这个时钟的周期,单位采用jiffies的单位。 jiffies是Linux一个全局变量,代表时间。它的单位随硬件平台的不同而不同。 系统里定义了一个常数HZ,代表每秒种最小时间间隔的数目。这样jiffies的单位就是1/HZ。Intel平台jiffies的单位是1/100秒,这就是系统所能分辨的最小时间间隔了。所以expires/HZ就是以秒为单位的这个时钟的周期。 function就是时间到了以后的回调函数,它的参数就是timer_list中的data。data这个参数在初始化时钟的时候赋值,一般赋给它设备的device结构指针。 在预置时间到系统调用function,同时系统把这个time_list从定时队列里清除。所以如果需要一直使用定时函数,要在function里再次调用add_timer()把这个timer_list加进定时队列。 2.4.4I/O I/O端口的存取使用: inlineunsignedintinb(unsignedshortport); inlineunsignedintinb_p(unsignedshortport); inlinevoidoutb(charvalue,unsignedshortport); inlinevoidoutb_p(charvalue,unsignedshortport); 在include/adm/io.h里定义。 inb_p()、outb_p()与inb()、outb_p()的不同在于前者在存取I/O时有等待(pause)一适应慢速的I/O设备。 为了防止存取I/O时发生冲突,Linux提供对端口使用情况的控制。在使用端口之前,可以检查需要的I/O是否正在被使用,如果没有,则把端口标记为正在使用,使用完后再释放。系统提供以下几个函数做这些工作。 intcheck_region(unsignedintfrom,unsignedintextent); voidrequest_region(unsignedintfrom,unsignedintextent,constchar*name); voidrelease_region(unsignedintfrom,unsignedintextent); 其中的参数from表示用到的I/O端口的起始地址,extent标明从from开始的端口数目。name为设备名称。 2.4.5中断打开关闭 系统提供给驱动程序开放和关闭响应中断的能力。是在include/asm/system.h中的两个定义。 #definecli()__asm____volatile__("cli"::) #definesti()__asm____volatile__("sti"::) 2.4.6打印信息 类似普通程序里的printf(),驱动程序要输出信息使用printk()。在include/linux/kernel.h里声明。 intprintk(constchar*fmt,...); 其中fmt是格式化字符串。...是参数。都是和printf()格式一样的。 2.4.7注册驱动程序 如果使用模块(module)方式加载驱动程序,需要在模块初始化时把设备注册到系统设备表里去。不再使用时,把设备从系统中卸除。定义在drivers/net/net_init.h里的两个函数完成这个工作。 intregister_netdev(structdevice*dev); voidunregister_netdev(structdevice*dev); dev就是要注册进系统的设备结构指针。在register_netdev()时,dev结构一般填写前面11项,即到init,后面的暂时可以不用初始化。最重要的是name指针和init方法。name指针空(NULL)或者内容为或者name[0]为空格(space),则系统把你的设备做为以太网设备处理。以太网设备有统一的命名格式,ethX。对以太网这么特别对待大概和Linux的历史有关。 init方法一定要提供,register_netdev()会调用这个方法让你对硬件检测和设置。 register_netdev()返回0表示成功,非0不成功。 2.4.8sk_buff Linux网络各层之间的数据传送都是通过sk_buff。sk_buff提供一套管理缓冲区的方法,是Linux系统网络高效运行的关键。每个sk_buff包括一些控制方法和一块数据缓冲区。控制方法按功能分为两种类型。一种是控制整个buffer链的方法, 另一种是控制数据缓冲区的方法。sk_buff组织成双向链表的形式,根据网络应用的特点,对链表的操作主要是删除链表头的元素和添加到链表尾。sk_buff的控制[!--empirenews.page--] 方法都很短小以尽量减少系统负荷。(translatedfromarticlewrittenbyAlanCox) 常用的方法包括: .alloc_skb()申请一个sk_buff并对它初始化。返回就是申请到的sk_buff。 .dev_alloc_skb()类似alloc_skb,在申请好缓冲区后,保留16字节的帧头空间。主要用在Ethernet驱动程序。 .kfree_skb()释放一个sk_buff。 .skb_clone()复制一个sk_buff,但不复制数据部分。 .skb_copy()完全复制一个sk_buff。 .skb_dequeue()从一个sk_buff链表里取出第一个元素。返回取出的sk_buff,如果链表空则返回NULL。这是常用的一个操作。 .skb_queue_head()在一个sk_buff链表头放入一个元素。 .skb_queue_tail()在一个sk_buff链表尾放入一个元素。这也是常用的一个操作。网络数据的处理主要是对一个先进先出队列的管理,skb_queue_tail() 和skb_dequeue()完成这个工作。 .skb_insert()在链表的某个元素前插入一个元素。 .skb_append()在链表的某个元素后插入一个元素。一些协议(如TCP)对没按顺序到达的数据进行重组时用到skb_insert()和skb_append()。 .skb_reserve()在一个申请好的sk_buff的缓冲区里保留一块空间。这个空间一般是用做下一层协议的头空间的。 .skb_put()在一个申请好的sk_buff的缓冲区里为数据保留一块空间。在 alloc_skb以后,申请到的sk_buff的缓冲区都是处于空(free)状态,有一个tail指针指向free空间,实际上开始时tail就指向缓冲区头。skb_reserve() 在free空间里申请协议头空间,skb_put()申请数据空间。见下面的图。 .skb_push()把sk_buff缓冲区里数据空间往前移。即把Headroom中的空间移一部分到Dataarea。 .skb_pull()把sk_buff缓冲区里Dataarea中的空间移一部分到Headroom中。 -------------------------------------------------- |Tailroom(free)| -------------------------------------------------- Afteralloc_skb() -------------------------------------------------- |Headroom|Tailroom(free)| -------------------------------------------------- Afterskb_reserve() -------------------------------------------------- |Headroom|Dataarea|Tailroom(free)| -------------------------------------------------- Afterskb_put() -------------------------------------------------- |Head|skb_|Data|Tailroom(free)| |room|push||| ||Dataarea|| -------------------------------------------------- Afterskb_push() -------------------------------------------------- |Head|skb_|Dataarea|Tailroom(free)| ||pull||| |Headroom||| -------------------------------------------------- Afterskb_pull() 三.编写Linux网络驱动程序中需要注意的问题 3.1中断共享 Linux系统运行几个设备共享同一个中断。需要共享的话,在申请的时候指明共享方式。系统提供的request_irq()调用的定义: intrequest_irq(unsignedintirq, void(*handler)(intirq,void*dev_id,structpt_regs*regs), unsignedlongirqflags, constchar*devname, void*dev_id); 如果共享中断,irqflags设置SA_SHIRQ属性,这样就允许别的设备申请同一个中断。需要注意所有用到这个中断的设备在调用request_irq()都必须设置这个属性。系统在回调每个中断处理程序时,可以用dev_id这个参数找到相应的设备。一般dev_id就设为device结构本身。系统处理共享中断是用各自的dev_id参数依次调用每一个中断处理程序。 3.2硬件发送忙时的处理 主CPU的处理能力一般比网络发送要快,所以经常会遇到系统有数据要发,但上一包数据网络设备还没发送完。因为在Linux里网络设备驱动程序一般不做数据缓存,不能发送的数据都是通知系统发送不成功,所以必须要有一个机制在硬件不忙时及时通知系统接着发送下面的数据。 一般对发送忙的处理在前面设备的发送方法(hard_start_xmit)里已经描述过,即如果发送忙,置tbusy为1。处理完发送数据后,在发送结束中断里清tbusy,同时用mark_bh()调用通知系统继续发送。 但在具体实现我的驱动程序时发现,这样的处理系统好象并不能及时地知道硬件已经空闲了,即在mark_bh()以后,系统要等一段时间才会接着发送。造成发送效率很低。2M线路只有10%不到的使用率。内核版本为2.0.35。 我最后的实现是不把tbusy置1,让系统始终认为硬件空闲,但是报告发送不成功。系统会一直尝试重发。这样处理就运行正常了。但是遍循内核源码中的网络驱动程序,似乎没有这样处理的。不知道症结在哪里。 3.3流量控制(flowcontrol) 网络数据的发送和接收都需要流量控制。这些控制是在系统里实现的,不需要驱动程序做工作。每个设备数据结构里都有一个参数dev->tx_queue_len,这个参数标明发送时最多缓存的数据包。在Linux系统里以太网设备(10/100Mbps)tx_queue_len一般设置为100,串行线路(异步串口)为10。实际上如果看源码可以知道,设置了dev->tx_queue_len并不是为缓存这些数据申请了空间。这个参数只是在收到协议层的数据包时判断发送队列里的数据是不是到了tx_queue_len的限度,以决定这一包数据加不加进发送队列。发送时另一个方面的流控是更高层协议的发送窗口(TCP协议里就有发送窗口)。达到了窗口大小,高层协议就不会再发送数据。 接收流控也分两个层次。netif_rx()缓存的数据包有限制。另外高层协议也会有一个最大的等待处理的数据量。 发送和接收流控处理在net/core/dev.c的do_dev_queue_xmit()和netif_rx()中。 3.4调试 很多Linux的驱动程序都是编译进内核的,形成一个大的内核文件。但对调试来说,这是相当麻烦的。调试驱动程序可以用module方式加载。支持模块方式的驱动程序必须提供两个函数:intinit_module(void)和voidcleanup_module(void)。init_module()在加载此模块时调用,在这个函数里可以register_netdev()注册设备。init_module()返回0表示成功,返回负表示失败。cleanup_module()在驱动程序被卸载时调用,清除占用的资源,调用unregister_netdev()。[!--empirenews.page--] 模块可以动态地加载、卸载。在2.0.xx版本里,还有kerneld自动加载模块,但是2.2.xx中已经取消了kerneld。手工加载使用insmod命令,卸载用rmmod命令,看内核中的模块用lsmod命令。 编译驱动程序用gcc,主要命令行参数-DKERNEL-DMODULE。并且作为模块加载的驱动程序,只编译成obj形式(加-c参数)。编译好的目标文件放在/lib/modules/2.x.xx/misc下,在启动文件里用insmod加载。 四.进一步的阅读 Linux程序设计资料可以从网上获得。这就是开放源代码的好处。并且没有什么“未公开的秘密”。我编写驱动程序时参阅的主要资料包括: Linux内核源代码 < >byMichaelK.Johnson < >byOriPomerantz byollyinBBS水木清华站   可以选择一个模板作为开始,内核源代码里有一个网络驱动程序的模板, drivers/net/skeleton.c。里面包含了驱动程序的基本内容。但这个模板是以以太网设备为对象的,以太网的处理在Linux系统里有特殊“待遇”,所以如果不是以太网设备,有些细节上要注意,主要在初始化程序里。 最后,多参照别人写的程序,听听其他开发者的经验之谈大概是最有效的帮助了。

    时间:2012-05-07 关键词: Linux 电源技术解析 网卡 驱动程序 编写

  • 如何为嵌入式应用编写优秀的C++代码

    在嵌入式软件技术中,C++语言具有较高的编程效率。但是,要实现高效率,还有许多问题需要特别注意。首先,应该正确理解C++的工作原理,逐步利用它的各种强大功能,把专业经验集成到对象中,并使用针对嵌入式应用做过优化的开发工具。不建议使用纯粹的自顶向下的设计策略和深层嵌套继承,并避免为适应工具而放弃语言功能,而使编程的效率降低。 嵌入式软件技术似乎落后于新的发展形势,这主要是因为嵌入式开发人员根据多年来应付有限资源的经验而养成了谨慎保守的态度。这里所指的有限资源包括:存储器容量非常有限,CPU的功能只是刚好够用。 对编程语言的选择完全反映了这种实用保守主义态度。数年前,嵌入式开发人员慢慢开始熟悉高级语言,但最终被接受的是C语言。为了促使业界接受, C语言具有很高质量的代码生成和透明调试功能,另一方面,也正是在巨大的压力下C语言才变得更有效率。 即使相对于C,C++取得了明显的改进,但它在嵌入式系统中的应用仍远未普及。 造成这种犹豫不决的原因倒不是因为人们固执地拒绝改变,而是很简单,他们还是担心资源问题。存储器可能是大了一些,但你不可能随心所欲地额外增加 500兆字节容量;处理器速度也确实快了很多,但成本和功耗限制决定了它们的功能远远比不上即使是最普通的台式机中的处理器那么强大。 按这种思路对C++用于嵌入式软件的适当性产生顾虑又是否合理呢?答案是不一定。就像任何工具那样,只有正确合理地使用语言,才能产生良好的结果。 建议 开始逐步使用C++。如果设计师今天还在使用C,那么让他明天就使用C++肯定是一个极大的跨越。然而,由于C++并不是一种面向对象的语言(实际上它是具有某些面向对象特点的一种过程语言),并且基本上是C的一个超集,因此设计师可以逐步发掘C++新增的强大功能。 理解C++的实际工作原理。查看生成的代码,并试着理解为什么要按它那样的方式做。 在对象中集成专业经验。嵌入式软件开发小组成员会有各种编程技巧,将这些专业经验集成到一些类(class)里面,从而能让其他团队成员安全地共享这些专业经验。 使用针对嵌入式应用做过优化的设计工具。虽然许多公司提供嵌入式编程工具,但相对而言,一些工具更能满足嵌入式开发者的实际需求。 适当应用语言功能。C++并不是专门针对嵌入式应用而开发的语言。某些语言特性,比如过载功能,绝不会消耗任何资源。而其它特性,如异常处理系统 (EHS),则可能需要很大的开销。该功能可以帮助编程人员构建极具鲁棒性的代码。但缺点是为了适应这种功能,工具会在后台悄悄地产生大量的代码。如果这种情况不可避免,那么至少以简单的方式使用EHS将能减少意外发生。 不建议 将嵌入式系统当作PC。如果设计师在对PC进行编程,那么可以认为存储器是无限和随便使用的,并且总是有足够的CPU处理能力。但在编写嵌入式软件时需要更加谨慎。先编一些代码,看看它的长度和执行性能。然后,只有当测试结果位于合理范围内而使设计师满意时,才能继续做下去。 图1:在嵌入式系统中,存储器一直是珍贵的资源。一个支持智能链接的链接器能利用编译器产生输出来消除重复的存储器区域。 使用纯粹的至顶向下的实现方法。从高层开始,创建只包括低层功能stub程序的软件应用程序创建是非常吸引人的。然而,这种策略的缺点在于会出现很多令人讨厌的意外问题。设计师可能在编写完代码后发现整个程序太大或太慢,甚至又大又慢。 使用深层嵌套的继承(inheritance)。面向对象编程方法的优点之一是能够让设计师自己或其它开发人员根据以前创建的其它基础类定义新的类,并且不用完全理解那些基础类的内部工作机理。这就是继承。但这样做的缺点是根据这种方式生成的类的实例化对象可能需要一定的开销。 编写“聪明的”代码。开发人员可以用C++写出非常聪明简洁的代码。但C++也能让人写出相当晦涩难懂的代码。千万不要是后者。 仅仅因为工具不能很好地处理而放弃一些语言功能。例如,如果模板实现很差,它们就会产生严重的代码膨胀(code bloat)。如果工具不能很好地处理这种情况,那就改用别的能够帮助提高编程效率的工具。

    时间:2012-05-02 关键词: 代码 嵌入式应用 编写

  • 如何编写Windows CE.net的usb驱动程序(2)

    上述讲了堆理论,可能读者脑袋都已经大了,为此,我们举个简单的例子来详细说明一下驱动程序的开发过程。 例如我们有个USBMouse设备,设备信息描述如下: DeviceDescriptor: bcdUSB:0x0100 bDeviceClass:0x00 bDeviceSubClass:0x00 bDeviceProtocol:0x00 bMaxPacketSize0:0x08(8) idVendor:0x05E3(GenesysLogicInc.) idProduct:0x0001 bcdDevice:0x0101 iManufacturer:0x00 iProduct:0x01 iSerialNumber:0x00 bNumConfigurations:0x01 ConnectionStatus:DeviceConnected CurrentConfigValue:0x01 DeviceBusSpeed:Low DeviceAddress:0x02 OpenPipes:1 EndpointDescriptor: bEndpointAddress:0x81 TransferType:Interrupt wMaxPacketSize:0x0003(3) bInterval:0x0A 可以看出上述设备有一个中断PIPE,包的最大值为3。可能有人问上述的值怎么得到的,win2k的DDK中有个usbview的例程,编译一下,将你的USB设备插到PC机的USB口中,运行usbview.exe即可看得相应的设备信息。 有了这些基本信息,就可以编写USB设备了,首先声明一下,下面的代码取自微软的USB鼠标样本程序,版权归微软所有,此处仅仅借用来描述一下USB鼠标驱动的开发过程,读者如需要引用此代码,需要得到微软的同意。 首先,必须输出USBD要求调用的三个函数,首先到设备插入到USB端口时,USBD会调用USBDeviceAttach()函数,相应的代码如下: extern"C"BOOL USBDeviceAttach( USB_HANDLEhDevice,//USB设备句柄 LPCUSB_FUNCSlpUsbFuncs,//USBDI的函数集合 LPCUSB_INTERFACElpInterface,//设备接口描述信息 LPCWSTRszUniqueDriverId,//设备ID描述字符串。 LPBOOLfAcceptControl,//返回TRUE,标识我们可以控制此设备,反之表示不能控制 DWORDdwUnused) { *fAcceptControl=FALSE; //我们的鼠标设备有特定的描述信息,要检测是否是我们的设备。 if(lpInterface==NULL) returnFALSE; //打印相关的USB设备接口描述信息。 DEBUGMSG(ZONE_INIT,(TEXT("USBMouse:DeviceAttach,IF%u,#EP:%u,Class:%u,Sub:%u,Prot:%urn"),lpInterface->Descriptor.bInterfaceNumber,lpInterface->Descriptor.bNumEndpoints,lpInterface->Descriptor.bInterfaceClass,lpInterface->Descriptor.bInterfaceSubClass,lpInterface->Descriptor.bInterfaceProtocol)); //初试数据USB鼠标类,产生一个接受USB鼠标数据的线程 CMouse*pMouse=newCMouse(hDevice,lpUsbFuncs,lpInterface); if(pMouse==NULL) returnFALSE; if(!pMouse->Initialize()) { deletepMouse; returnFALSE; } //注册一个监控USB设备事件的回调函数,用于监控USB设备是否已经拔掉。 (*lpUsbFuncs->lpRegisterNotificationRoutine)(hDevice, USBDeviceNotifications,pMouse); *fAcceptControl=TRUE; returnTRUE; } 第二个函数是USBInstallDriver()函数, 一些基本定义如下: constWCHARgcszRegisterClientDriverId[]=L"RegisterClientDriverID"; constWCHARgcszRegisterClientSettings[]=L"RegisterClientSettings"; constWCHARgcszUnRegisterClientDriverId[]=L"UnRegisterClientDriverID"; constWCHARgcszUnRegisterClientSettings[]=L"UnRegisterClientSettings"; constWCHARgcszMouseDriverId[]=L"Generic_Sample_Mouse_Driver"; 函数接口如下: extern"C"BOOL USBInstallDriver( LPCWSTRszDriverLibFile)//@parm[IN]-ContainsclientdriverDLLname { BOOLfRet=FALSE; HINSTANCEhInst=LoadLibrary(L"USBD.DLL"); //注册USB设备信息 if(hInst) { LPREGISTER_CLIENT_DRIVER_IDpRegisterId=(LPREGISTER_CLIENT_DRIVER_ID) GetProcAddress(hInst,gcszRegisterClientDriverId); LPREGISTER_CLIENT_SETTINGSpRegisterSettings= (LPREGISTER_CLIENT_SETTINGS)GetProcAddress(hInst, gcszRegisterClientSettings); if(pRegisterId&&pRegisterSettings) { USB_DRIVER_SETTINGSDriverSettings; DriverSettings.dwCount=sizeof(DriverSettings); //设置我们的特定的信息。 DriverSettings.dwVendorId=USB_NO_INFO; DriverSettings.dwProductId=USB_NO_INFO; DriverSettings.dwReleaseNumber=USB_NO_INFO; DriverSettings.dwDeviceClass=USB_NO_INFO; DriverSettings.dwDeviceSubClass=USB_NO_INFO; DriverSettings.dwDeviceProtocol=USB_NO_INFO; DriverSettings.dwInterfaceClass=0x03;//HID DriverSettings.dwInterfaceSubClass=0x01;//bootdevice DriverSettings.dwInterfaceProtocol=0x02;//mouse fRet=(*pRegisterId)(gcszMouseDriverId); if(fRet) { fRet=(*pRegisterSettings)(szDriverLibFile, gcszMouseDriverId,NULL,&DriverSettings); if(!fRet) { //BUGBUGunregistertheClientDriver’sID } } } else { RETAILMSG(1,(TEXT("!USBMouse:ErrorgettingUSBDfunctionpointersrn"))); } FreeLibrary(hInst); } returnfRet; } 上述代码主要用于产生USB设备驱动程序需要的注册表信息,需要注意的是:USB设备驱动程序不使用标准的注册表函数,而是使用RegisterClientDriverID()和RegisterClientSettings来注册相应的设备信息。[!--empirenews.page--] 另外一个函数是USBUninstallDriver()函数,具体代码如下: extern"C"BOOL USBUnInstallDriver() { BOOLfRet=FALSE; HINSTANCEhInst=LoadLibrary(L"USBD.DLL"); if(hInst) { LPUN_REGISTER_CLIENT_DRIVER_IDpUnRegisterId= (LPUN_REGISTER_CLIENT_DRIVER_ID) GetProcAddress(hInst,gcszUnRegisterClientDriverId); LPUN_REGISTER_CLIENT_SETTINGSpUnRegisterSettings= (LPUN_REGISTER_CLIENT_SETTINGS)GetProcAddress(hInst, gcszUnRegisterClientSettings); if(pUnRegisterSettings) { USB_DRIVER_SETTINGSDriverSettings; DriverSettings.dwCount=sizeof(DriverSettings); //必须填入与注册时相同的信息。 DriverSettings.dwVendorId=USB_NO_INFO; DriverSettings.dwProductId=USB_NO_INFO; DriverSettings.dwReleaseNumber=USB_NO_INFO; DriverSettings.dwDeviceClass=USB_NO_INFO; DriverSettings.dwDeviceSubClass=USB_NO_INFO; DriverSettings.dwDeviceProtocol=USB_NO_INFO; DriverSettings.dwInterfaceClass=0x03;//HID DriverSettings.dwInterfaceSubClass=0x01;//bootdevice DriverSettings.dwInterfaceProtocol=0x02;//mouse fRet=(*pUnRegisterSettings)(gcszMouseDriverId,NULL, &DriverSettings); } if(pUnRegisterId) { BOOLfRetTemp=(*pUnRegisterId)(gcszMouseDriverId); fRet=fRet?fRetTemp:fRet; } FreeLibrary(hInst); } returnfRet; } 此函数主要用于删除USBInstallDriver()时创建的注册表信息,同样的它使用自己的函数接口UnRegisterClientDriverID()和UnRegisterClientSettings()来做相应的处理。 另外一个需要处理的注册的监控通知函数USBDeviceNotifications(): extern"C"BOOLUSBDeviceNotifications(LPVOIDlpvNotifyParameter,DWORDdwCode, LPDWORD*dwInfo1,LPDWORD*dwInfo2,LPDWORD*dwInfo3, LPDWORD*dwInfo4) { CMouse*pMouse=(CMouse*)lpvNotifyParameter; switch(dwCode) { caseUSB_CLOSE_DEVICE: //删除相关的资源。 deletepMouse; returnTRUE; } returnFALSE; } USB鼠标的类的定义如下: classCMouse { public: CMouse::CMouse(USB_HANDLEhDevice,LPCUSB_FUNCSlpUsbFuncs, LPCUSB_INTERFACElpInterface); ~CMouse(); BOOLInitialize(); private: //传输完毕调用的回调函数 staticDWORDCALLBACKMouseTransferCompleteStub(LPVOIDlpvNotifyParameter); //中断处理函数 staticULONGCALLBACKCMouse::MouseThreadStub(PVOIDcontext); DWORDMouseTransferComplete(); DWORDMouseThread(); BOOLSubmitInterrupt(); BOOLHandleInterrupt(); BOOLm_fClosing; BOOLm_fReadyForMouseEvents; HANDLEm_hEvent; HANDLEm_hThread; USB_HANDLEm_hDevice; USB_PIPEm_hInterruptPipe; USB_TRANSFERm_hInterruptTransfer; LPCUSB_FUNCSm_lpUsbFuncs; LPCUSB_INTERFACEm_pInterface; BOOLm_fPrevButton1; BOOLm_fPrevButton2; BOOLm_fPrevButton3; //数据接受缓冲区。 BYTEm_pbDataBuffer[8]; }; 具体实现如下: //构造函数,初始化时调用 CMouse::CMouse(USB_HANDLEhDevice,LPCUSB_FUNCSlpUsbFuncs, LPCUSB_INTERFACElpInterface) { m_fClosing=FALSE; m_fReadyForMouseEvents=FALSE; m_hEvent=NULL; m_hThread=NULL; m_hDevice=hDevice; m_hInterruptPipe=NULL; m_hInterruptTransfer=NULL; m_lpUsbFuncs=lpUsbFuncs; m_pInterface=lpInterface; m_fPrevButton1=FALSE; m_fPrevButton2=FALSE; m_fPrevButton3=FALSE; memset(m_pbDataBuffer,0,sizeof(m_pbDataBuffer)); } //析构函数,用于清除申请的资源。 CMouse::~CMouse() { //通知系统去关闭相关的函数接口。 m_fClosing=TRUE; //Wakeuptheconnectionthreadagainandgiveittimetodie. if(m_hEvent!=NULL) { //通知关闭数据接受线程。 SetEvent(m_hEvent); if(m_hThread!=NULL) { DWORDdwWaitReturn; dwWaitReturn=WaitForSingleObject(m_hThread,1000); if(dwWaitReturn!=WAIT_OBJECT_0) { TerminateThread(m_hThread,DWORD(-1)); } CloseHandle(m_hThread); m_hThread=NULL; } CloseHandle(m_hEvent); m_hEvent=NULL; } if(m_hInterruptTransfer) (*m_lpUsbFuncs->lpCloseTransfer)(m_hInterruptTransfer); if(m_hInterruptPipe) (*m_lpUsbFuncs->lpClosePipe)(m_hInterruptPipe); } //初始化USB鼠标驱动程序 BOOLCMouse::Initialize() { LPCUSB_DEVICElpDeviceInfo=(*m_lpUsbFuncs->lpGetDeviceInfo)(m_hDevice); //检测配置:USB鼠标应该只有一个中断管道 if((m_pInterface->lpEndpoints[0].Descriptor.bmAttributes&USB_ENDPOINT_TYPE_MASK)!=USB_ENDPOINT_TYPE_INTERRUPT) { RETAILMSG(1,(TEXT("!USBMouse:EP0wrongtype(%u)!rn"),[!--empirenews.page--] m_pInterface->lpEndpoints[0].Descriptor.bmAttributes)); returnFALSE; } DEBUGMSG(ZONE_INIT,(TEXT("USBMouse:EP0:MaxPacket:%u,Interval:%urn"), m_pInterface->lpEndpoints[0].Descriptor.wMaxPacketSize, m_pInterface->lpEndpoints[0].Descriptor.bInterval)); m_hInterruptPipe=(*m_lpUsbFuncs->lpOpenPipe)(m_hDevice, &m_pInterface->lpEndpoints[0].Descriptor); if(m_hInterruptPipe==NULL){ RETAILMSG(1,(TEXT("Mouse:Erroropeninginterruptpipern"))); return(FALSE); } m_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL); if(m_hEvent==NULL) { RETAILMSG(1,(TEXT("USBMouse:ErroronCreateEventforconnecteventrn"))); return(FALSE); } //创建数据接受线程 m_hThread=CreateThread(0,0,MouseThreadStub,this,0,NULL); if(m_hThread==NULL) { RETAILMSG(1,(TEXT("USBMouse:ErroronCreateThreadrn"))); return(FALSE); } return(TRUE); } //从USB鼠标设备中读出数据,产生相应的鼠标事件。 BOOLCMouse::SubmitInterrupt() { if(m_hInterruptTransfer) (*m_lpUsbFuncs->lpCloseTransfer)(m_hInterruptTransfer); //从USB鼠标PIPE中读数据 m_hInterruptTransfer=(*m_lpUsbFuncs->lpIssueInterruptTransfer) (m_hInterruptPipe,MouseTransferCompleteStub,this, USB_IN_TRANSFER|USB_SHORT_TRANSFER_OK,//表示读数据 min(m_pInterface->lpEndpoints[0].Descriptor.wMaxPacketSize, sizeof(m_pbDataBuffer)), m_pbDataBuffer, NULL); if(m_hInterruptTransfer==NULL) { DEBUGMSG(ZONE_ERROR,(L"!USBMouse:ErrorinIssueInterruptTransferrn")); returnFALSE; } else { DEBUGMSG(ZONE_TRANSFER,(L"USBMouse::SubmitInterrupt,Transfer:0x%Xrn", m_hInterruptTransfer)); } returnTRUE; } //处理鼠标中断传输的数据 BOOLCMouse::HandleInterrupt() { DWORDdwError; DWORDdwBytes; DWORDdwFlags=0; INTdx=(signedchar)m_pbDataBuffer[1]; INTdy=(signedchar)m_pbDataBuffer[2]; BOOLfButton1=m_pbDataBuffer[0]&0x01?TRUE:FALSE; BOOLfButton2=m_pbDataBuffer[0]&0x02?TRUE:FALSE; BOOLfButton3=m_pbDataBuffer[0]&0x04?TRUE:FALSE; if(!(*m_lpUsbFuncs->lpGetTransferStatus)(m_hInterruptTransfer,&dwBytes,&dwError)) { DEBUGMSG(ZONE_ERROR,(TEXT("!USBMouse:ErrorinGetTransferStatus(0x%X)rn"), m_hInterruptTransfer)); returnFALSE; } else { DEBUGMSG(ZONE_TRANSFER,(TEXT("USBMouse::HandleInterrupt,hTransfer0x%Xcomplete(%ubytes,Error:%X)rn"), m_hInterruptTransfer,dwBytes,dwError)); } if(!SubmitInterrupt()) returnFALSE; if(dwError!=USB_NO_ERROR) { DEBUGMSG(ZONE_ERROR,(TEXT("!USBMouse:Error0x%Xininterrupttransferrn"),dwError)); returnTRUE; } if(dwBytes<3) { DEBUGMSG(ZONE_ERROR,(TEXT("!USBMouse:Invalidbytecnt%ufrominterrupttransferrn"),dwBytes)); returnTRUE; } if(dx||dy) dwFlags|=MOUSEEVENTF_MOVE; if(fButton1!=m_fPrevButton1) { if(fButton1) dwFlags|=MOUSEEVENTF_LEFTDOWN; else dwFlags|=MOUSEEVENTF_LEFTUP; } if(fButton2!=m_fPrevButton2) { if(fButton2) dwFlags|=MOUSEEVENTF_RIGHTDOWN; else dwFlags|=MOUSEEVENTF_RIGHTUP; } if(fButton3!=m_fPrevButton3) { if(fButton3) dwFlags|=MOUSEEVENTF_MIDDLEDOWN; else dwFlags|=MOUSEEVENTF_MIDDLEUP; } m_fPrevButton1=fButton1; m_fPrevButton2=fButton2; m_fPrevButton3=fButton3; DEBUGMSG(ZONE_EVENTS, (TEXT("USBMouseevent:dx:%d,dy:%d,dwFlags:0x%X(B1:%u,B2:%u,B3:%u)rn"), dx,dy,dwFlags,fButton1,fButton2,fButton3)); //通知系统产生鼠标事件 if(m_fReadyForMouseEvents) mouse_event(dwFlags,dx,dy,0,0); else m_fReadyForMouseEvents=IsAPIReady(SH_WMGR); returnTRUE; } DWORDCALLBACKCMouse::MouseTransferCompleteStub(LPVOIDlpvNotifyParameter) { CMouse*pMouse=(CMouse*)lpvNotifyParameter; return(pMouse->MouseTransferComplete()); } //数据传输完毕回调函数 DWORDCMouse::MouseTransferComplete() { if(m_hEvent) SetEvent(m_hEvent); return0; } ULONGCALLBACKCMouse::MouseThreadStub(PVOIDcontext) { CMouse*pMouse=(CMouse*)context; return(pMouse->MouseThread()); } //USB鼠标线程 DWORDCMouse::MouseThread() { DEBUGMSG(ZONE_INIT,(TEXT("USBMouse:Workerthreadstartedrn"))); SetThreadPriority(GetCurrentThread(),THREAD_PRIORITY_HIGHEST);[!--empirenews.page--] if(SubmitInterrupt()) { while(!m_fClosing) { WaitForSingleObject(m_hEvent,INFINITE); if(m_fClosing) break; if((*m_lpUsbFuncs->lpIsTransferComplete)(m_hInterruptTransfer)) { if(!HandleInterrupt()) break; } else { RETAILMSG(1,(TEXT("!USBMouse:Eventsignalled,buttransfernotcompletern"))); //Theonlytimethisshouldhappenisifwegetanerroronthetransfer ASSERT(m_fClosing||(m_hInterruptTransfer==NULL)); break; } } } RETAILMSG(1,(TEXT("USBMouse:Workerthreadexitingrn"))); return(0); } 看到了没有,其实USB的驱动程序编写就这么简单,类似的其他设备,例如打印机设备,就有BulkOUTPIPE,需要Bulk传输,那就需要了解一下IssueBulkTransfer()的应用。当然如果是开发USBMassStorageDisk的驱动,那就需要了解更多的协议,例如Bulk-OnlyTransport协议等。 微软的WindowsCE.NET的PlatformBuild中已经带有USBPrinter和USBMassStorageDisk的驱动的源代码了,好好研究一下,你一定回受益非浅的。

    时间:2012-04-30 关键词: USB Windows 如何 电源技术解析 驱动程序 ce.net 编写

  • 编写STC单片机的ISP协议的方法介绍

    STC单片机包含两个Flash块,在一块Flash中运行的程序可对另一块Flash进行擦除和重新编程.一般都将ISP程序存放在容量较小的一块Flash中(Block1),而将用户代码存放在容量较大的一块Flash中(Block0).Block1中的ISP程序对Block0中的用户代码进行擦除和重新编程. 在做STC单片机解密或是使用的朋友都知道,在STC单片机的程序存储区后,还有一段大约3K的代码,这段代码就是STC单片机的ISP程序,在STC单片机的数据手册里有关于对于ISP的生机资料,既然这段代码是可以升级的,那么我们就可以肯定这个区域是可以改写的,呵呵,这些涉及到了对STC单片机软解密的技术,我不多提了,大家可以动用自己的智慧。      下面为大家提供一些自己设计STC ISP协议的资料。许多应用系统中都需要进行程序代码升级,如果程序代码在外部Flash存储器中,实现程序代码升级可以对外部Flash直接操作.但对于在单片机内部的一些系统程序代码,就要求此单片机支持IAP(In-Application-Programming)功能.本文即介绍此情况下的ISP(In-System-Programming)程序设计方法,以及在SST和STC单片机上的具体实现.       1  ISP实现基本结构         ISP的实现方式有很多种,但大致都遵循图1所示流程.                  图1         其中,判断用户ISP选择,一般有以下几种方式.       (1)  连接计算机系统,由系统的命令选择         进入用户ISP选择判断时,先由单片机发送特定特征数据,然后等待命令数据,如果在一定的时间内,接收到计算机系统发出的选择命令则进入用户代码升级,否则直接跳转到用户代码执行.       (2)  由用户板上的跳线选择         一般利用单片机空余的端口,设计一个代码升级选择跳线.进入用户ISP选择判断时,单片机可以直接根据此端口的状态判断进入用户代码升级还是直接跳转到用户代码执行.       (3)  由用户板操作功能选择         在用户板的功能菜单或功能组合中,允许用户选择代码升级功能,同时,在外部存储器中存放相应的标志.当选择代码升级功能时,在外部存储器中写入特定数据,然后程序复位.进入用户ISP选择判断时,判断外部存储器中的数据,如果符合条件则进入用户代码升级,否则直接跳转到用户代码执行.       2  IAP程序设计         目前,许多单片机都支持IAP功能,一般这些单片机内部都包含两个Flash块,在一块Flash中运行的程序可对另一块Flash进行擦除和重新编程.一般都将ISP程序存放在容量较小的一块Flash中(Block1),而将用户代码存放在容量较大的一块Flash中(Block0).Block1中的ISP程序对Block0中的用户代码进行擦除和重新编程.          下面以两种51系列兼容的Flash单片机为例,介绍IAP的程序设计.一种是SST89C54,另一种是STC89C516RD.       (1)  SST89C54相关特性         SST89C54内部有20KB(16KB+4KB)程序存储器,统一编址.Block0为0000H~3FFFH;Block1为F000H~FFFFH.Block1可以选择映射到0000H地址开始的1KB/2KB/4KB程序区.       (2)  STC89C516RD相关特性         SST89C516RD内部有72KB(64KB+8KB)程序存储器.Block0为0000H~FFFFH,Block1可以选择映射到0000H地址开始的8KB程序区(上电复位缺省为地址映射).         SST与STC单片机的IAP操作几乎完全相同,本刊网站(www.dpj.com.cn)中给出了IAP函数的C程序源代码IAP.C.要特别注意的是,Block0_erase函数中对于block0的选择,两种单片机是不同的(正好相反).       3  ISP程序到用户代码的切换         在设计中一般都将ISP程序设计为上电复位后运行的程序,如果不需要用户代码升级或升级完成后,就要将程序切换到用户代码执行.ISP程序到用户代码的切换,不同的单片机各不相同.       (1)  SST89C54程序区Block1到Block0的切换实现         SST89C54单片机在烧录时,将ISP程序写到Block1,并且烧录映射选择位RB0/RB1(RE-MAP[1:0]).这样程序上电复位时,自动将Block1映射到0000H地址开始的4KB程序区,进入ISP程序执行.由于Block1同时还分配在地址F000H~FFFFH,因此,编译生成ISP程序代码时,设定所有的地址范围都在F000H~FFFFH.需要切换到用户代码(Block0)运行时,修改SFCF[7]控制位VIS,将Block1的0000H地址映射取消,然后程序跳转到地址0000H执行,则开始运行Block0中的用户代码程序.         本刊网站中给出了ISP的C程序源代码ISP.C.需要注意的是,此程序在Keil-C中要建立工程文件,包含       IAP.C函数以及STARTUP.A51,并且在IAP.C和ISP.C中都要去掉STC的定义.为了将地址范围设定到F000H~FFFFH,要将STARTUP.A51中程序入口地址由0改为0F000H,如下:       CSEG AT 0F000H       ?C_STARTUP: LJMP STARTUP1          还要修改编译选项设置Target选项卡中Off-chip Code memory:Start       =0xF000;Size=0x1000;还要设置C51选项卡中Interrupt Vectors at address:0xF000.       (2)  STC89C516RD程序区Block1到Block0的切换实现         STC89C516RD单片机在烧录时,将ISP程序写到Block1.(注意:并不烧录SC0/SC1位).单片机上电复位时,缺省的Block1映射到0000H地址开始的8KB程序区,进入ISP程序执行.需要切换到用户代码(Block0)运行时,ISP修改SFCF[1]控制位SWR,产生一个软复位(Software       Reset).由于SC0和SC1都未烧录,程序软复位后,Block1将不再映射到0000H地址映,则开始运行Block0中的用户代码程序.         本刊网站中给出了ISP的C程序源代码ISP.C.要注意的是:此程序在Keil-C中要建立工程文件,包含IAP.C函数,并且在IAP.C和ISP.C中都要保留STC的定义.  4  与计算机的通信协议         升级用户代码时,需要与计算机进行通信,一般采用RS232串行通信,数据协议采用简单协议.此协议参考了ADuC812单片机的ISP数据协议.(ADuC812单片机硬件内置ISP程序).       (1)  复位命令(计算机→单片机)         计算机发送4字节复位命令:21H,5AH,00H,A6H,单片机返回复位信息.       (2)  复位信息(单片机→计算机)         复位信息为25字节,前3字节为单片机公司特征字符(如:“ADI”“SST”“STC”),最后1字节为校验和.       (3)  数据包格式(计算机→单片机)         计算机发送数据包格式:07H、0EH、长度、数据、校验和(长度与数据的校验和).       (4)  擦除命令         计算机发送数据包,其中数据只有1字节,内容为:字符“A”或“C”.单片机擦除用户程序区后返回1字节06H表示成功;05H表示失败.       (5)  编程命令         计算机发送数据包,其中数据内容:“W”,00H,地址高字节,地址低字节,程序数据.单片机返回1字节06H表示成功;05H表示失败.       (6)  运行用户程序         计算机发送数据包,其中数据只有1字节,内容为:字符“U”.单片机返回1字节06H表示成功,然后跳转到用户程序运行.         本刊网站提供了计算机下载软件的C程序源代码Download.C.       结语         根据以上ISP程序设计思路和实例,大家可以修改ISP初始代码,或者丰富它的ISP功能(读功能、口令控制等),尝试设计自己的ISP程序,定能为自己的系统增色不少.

    时间:2012-04-04 关键词: isp 单片机 stc 编写

首页  上一页  1 2 下一页 尾页
发布文章

技术子站