当前位置:首页 > 嵌入式linux
  • 嵌入式Linux要学哪些东西?9点要明确

    嵌入式Linux要学哪些?一些人总在寻思,怕走了弯路,又怕学的东西离企业需求远。那么今天就请华清远见高级讲师曹大神告诉你,9点浅析嵌入式学习步骤。下面是他本人亲笔。1、要学习Linux,首先要会用,如果不会用怎么知道怎么知道怎么去做,所以需要学习Linux系统的安装及使用。2、学会用Linux了,那么我们就要做一些Linux下的开发了,开发什么呢?看到网上有很多很强大很有趣的程序,我们都可以尝试去做,可是如何去做呢,程序是什么呢,怎么写呢?这时我们需要学习一些语言,比如比较基础的C语言,比如面向对象的C++,Java等。3、写完程序了发现不就是一个很普通的文件吗,怎么能像别人写的程序一样运行起来呢? 这时我们需要知道如何编译一个程序,需要知道编译器及一些其他工具的使用,比如GCC,在开发的过程中我们为了提供工作效率还会用到Make,Shell等,为了能够很好的管理我们代码的不同版本,我们还需要知道什么是SVC,CVS,subversion,git等。4、当我们开始写一些比较大的程序,不再是以往的hello world级别的了,这时我们发现我们考虑的更多了,我们考虑到了程序的效率,我们发现需要读写文件了,我们发现需要和另外一台电脑通信了,于是我们有需要学习一些更高级点的东西了,比如数据结构,比如文件IO,比如多进程多线程编程,比如网络通信,这时我们会接触很多新的名词,什么是树,图...,什么是文件io什么是标准IO,什么是进程线程,什么是TCP/IP...。5、当我们再进一步的话,我们会发现我们什么有很多的东西都运行的是Linux系统,可是他们和我的PC运行的Linux有什么区别呢,为什么我电脑要用风扇而手机不用风扇呢,我们打开网页查找手机的详细参数的时候,会看到Cortex-A15 、四核 、ARM等等字眼,什么是ARM,什么是Cortex-A15,我们需要进一步的了解。6、了解完ARM之后,很多人就回去购买一些开发板,去学习它,这时操作系统的移植又成了一个比较重要的内容了,什么是bootloader,什么是kernel,什么是rootfs等等,我们怎么做呢。7、花了很长时间系统做好了,发现板子上很多的什么不能用,怎么办呢? 这时我们需要写一些驱动来驱动这些设备,这时我们需要知道,什么是字符设备驱动、块设备驱动、网络设备驱动,为了更好的写驱动我们需要了解更多的硬件相关的东西,我们需要看懂芯片手册,我们需要看懂原理图,只有我们懂我们的设备,才能更好的驱动它。8、等等,这里就列举更多的内容了,因为还有很多。9、这些都有了我们就可以自行开发我们自己的产品了。嵌入式Linux要学哪些?上述9点基本很明了了。这也算是嵌入式学习的一个基本步骤。本人认为到第九点你已经是一个高级的嵌入式Linux人物了,工资待遇我已经不用说,咋说也得15K往上了,如果做管理30K也是有的。这些东西要靠自学,说实话不太现实,嵌入式入门门槛相对高,要有心里准备,参加培训班是有必要的,除非你有亲戚朋友手把手教你。引用一句老话:“师傅领进门,修行是靠个人!”自己必须得喜欢钻研与学习,只有付出才会有收获!最后说一点,Android系统也是在linux系统基础上开发的,所以学嵌入式linux的朋友,将来不仅可以从事嵌入式开发的工作(嵌入式的应用领域非常广泛,永不过时!)还可以从事android开发方向的工作(轻松迈入),从职业发展来说,嵌入式linux也是程序员最具发展前景的技术首选。

    时间:2018-11-20 关键词: 嵌入式linux 嵌入式学习

  • 基于嵌入式Linux的组态软件实时数据库的设计

    基于嵌入式Linux的组态软件实时数据库的设计

    1 引言实时数据库(real-time database, RTDB)作为组态软件设计与实现的核心内容解决了其所 应对的现代工业生产现场环境中生产数据与控制数据类型复杂多样,数据处理与事件调度时 间约束严格等难题[1]。目前,国内外已经有多种基于Windows 操作系统平台的实时数据库 产品在自动化过程控制领域中得到应用[2],随着Linux 操作系统的出现,这种开发平台单一 的局面有望得到改观。Linux 操作系统具有很多优秀的特性适于组态软件实时数据库系统的 开发,特别是其完善的进程线程管理,进程间通信机制与并发控制,可靠的内存管理系统[3], 更是为时间约束严格的实时数据库的开发提供了有力的支持。因此,本文结合Linux 系统实 时多任务方面的特性,采取能够满足数据实时响应要求的多级存储结构,研究并提出了一种 基于嵌入式Linux 系统平台并可应用于监控组态软件的实时数据库实现方案。2 实时数据库存储结构的分析与设计实时数据库是监控组态软件数据处理,事务调度,各应用程序间通信的中心。图1 即示 出了组态软件实时数据库的数据处理流程。2.1 实时数据库的数据流分析组态软件运行环境分为实时数据库管理系统(RTDBMS)和实时监控界面程序(real-time supervisory control interface, RTSCI)。实时数据库管理系统需要把工业现场中复杂多样的过 程和控制数据抽象为合理高效的数据结构,实时监控界面程序则利用实时过程数据为现场监 控人员提供一个反映实际生产过程的可视化图形界面,在实际运行中二者构成客户端/服务 器计算模式。RTDBMS 作为数据服务的提供者,需要满足RTSCI 种类多样的数据需求。为了形象的描绘工业现场的实际生产过程,RTSCI 由多种图形对象构成,根据不同的数 据类型需求可分为实时显示,实时趋势,历史趋势,实时报警等。而应用于现代工业生产现 场环境的实时数据库还需要满足严格的数据存取与事件响应的定时限制。所以,传统的数据 库管理系统所采用的数据表示方法,存储模式已不能满足工控组态软件所要求的响应速度 [4]。为此,在设计实时数据库时,为了兼顾RTSCI 所要求的数据图形表现多样性与工业生产 环境时间约束的严格性,需要采用多种存储介质合理组合的多层级数据存储结构。在工业生产过程中实时产生的过程量,是需要组态软件在每个采样周期中及时更新的动 态数据,为了保证实时数据库的及时响应,须将其存储在内存中;对于RTSCI 的某些数据 需求,如历史趋势显示,实时数据库应为之提供相比内存更大的存储空间,这类数据需求不 需要很高的响应速度,可将之命名为静态数据,其所服务的图形对象要求可按时间翻页浏览, 这类静态数据适于存储在文件系统中;而需要长期保存的生产过程量数据,即历史数据,它 们是今后进行生产效能分析的依据,这些数据可以保存在通用数据库中。这样,由内存数据 库,外存文件系统以及通用数据库的三级存储结构,便构成了既可满足实时数据定时限制又 兼顾数据需求多样性的可应用于监控组态软件的实时数据库的存储架构。2.2 利用共享内存与命名管道技术实现实时数据库存储结构Linux 提供了一组由AT&T System V.2 版本的UNIX 引入的进程间通信(Inter-Process CommunicatiON, IPC)机制,其中的共享内存技术允许两个不相关的进程访问同一段逻辑内 存,是在两个运行中的进程间传递数据的一种非常高效的数据访问机制[5],可为RTDBMS 与RTSCI 间的动态数据交互提供有力的支持。但共享内存技术本身并未提供任何同步机制, 因此还需要配合IPC 的信号量机制来保证二者间数据访问控制。Linux 提供的另一组在不相 关的进程间进行数据交互的函数是命名管道FIFO。它是将数据存储在文件系统中实现进程 间共享的一种通信方式。命名管道适用于数据存取响应时间要求相对宽松且数据交互总量较 大的应用场合。同时,FIFO 中实现数据读写的read 和write 调用的阻塞机制,还可以提供 进程间的同步控制。由上述对其特点的分析,FIFO 技术是实现RTDBMS 与RTSCI 间静态数据交互较好的 选择。上图即示出了由共享内存,命名管道,ODBC 接口等多种进程间通信机制构建的实时 数据库存储结构。值得注意的是,为了实现实时数据库与通用数据库的双向数据交换,需要编写特定的通用数据库接口(ODBC 接口)例程。Linux 提供了一组丰富的接口函数用来访问 MySQL 数据库。通过对通用数据库MySQL 的数据连接进行组态,实时数据库便可按照预 先指定的采样周期,对规定时间区段内的历史数据与MySQL 数据库建立数据连接。3 实时数据库系统的实现3.1 数据模型的分析与构建传统数据模型包括三个部分:一组数据对象及其结构,一组数据操作,关于数据对象与 操作的完整性约束[6]。而对于工业生产中所产生的实时数据,还必须约束于严格的定时限制。在应用于工业现场控制的组态软件中不仅包括实时产生的过程量数据,还存在着描述系 统运行状况的系统数据,在利用采集到的过程量数据的基础上,经处理后提取出的计算数据, 以及涉及控制测量组态或从工控软件输出到输出装置上的数据等。由此,可将实时数据模型 抽象为:模拟量,开关量,字符串量三种数据类型。3.2 数据类型的实现上述用于构建实时数据过程量的三类数据模型,对应于具体的实现分别可用:浮点型, 布尔型,字符数组来表示。实时数据可由结构类型实现,以其中的实时数据类型字段来区分 不同的过程量类型。实时数据结构类型的实现如下。/*枚举类型标记实时数据过程量类型 */typedef enum {double_t = 1,bool_t} pv_type_set;/* 联合类型实现实时数据过程量值 */typedef union {double dPV;bool swhPV;} pv_data_set;/* 实时数据的数据类型 */#define name_LEN 20#define DESC_LEN 50typedef STruct {char nAME[NAME_LEN + 1];//数据点名称pv_type_set type;//数据点类型char desc[DESC_LEN + 1];//数据点描述信息pv_data_set pv;//数据点过程量值char domain[3];//数据点所在域号char eu[DESC_LEN + 1];//数据点工程单位描述double euLow;//数据点工程单位下限double euHigh;//数据点工程单位上限double pvRaw;//现场测量裸数据bool IsRanCon;//是否进行量程变换double pvRawLow;//裸数据量程下限double pvRawHigh;//裸数据量程上限bool static;//静态数据历史数据存储至文件系统int storecyc;//备份周期bool IsAlarm;//是否报警int AlarmPriority;//报警优先级… …} tag_node;3.3 实时数据在数据库中的组织形式及相关数据结构为了充分地利用 Linux 平台对实时多任务操作的支持,实时数据库的数据采集与处理等任务应以多进程的形式并发执行。而Linux 操作系统IPC 机制中的共享内存技术可以根据需 要离散地分配内存空间,从而可将所有数据点的共享内存地址构成索引并建表。在实际应用 中,经常会将若干在生产工艺上有关联的数据点划分为一个数据域,所以地址索引表为两级 结构:第一级为域表,其中的数据项存储特定数据域的地址;第二级为数据点表,数据项存 储某一数据域中的每个数据点的内存地址。域表与数据点表中存储的数据点所在的域号字段 与数据点号字段组合构成数据点ID。包括所有实时数据点的地址索引由一张域表与多张数 据点表构成。根据存储域表结构的内存地址,便可访问所有数据点的共享内存地址。下面给 出域表与数据点表用到的数据结构。/* 描述域表数据项的数据结构 */typedef struct {char domIndex[3];//域号tbTag_item *tbTag_ptr;//该域的数据点表地址} tbDom_item;/*描述数据点表数据项的数据结构*/typedef struct {char tagIndex[3];//数据点号tag_node *tag_ptr;//指向数据点的指针int shmid;//存储该数据点的共享内存标号char name[NAME_LEN + 1];//数据点名称} tbTag_item;域表与数据点表的数据项内容与关系结构示意见图 3。3.4 一组访问实时数据库的通用编程接口作为投入现场运行的监控组态软件的核心部件,实时数据库需要为现场操作人员提供类 似传统数据库管理系统的实时数据查询与更新等功能。另外出于设备无关性的考虑,也需要 为监控组态软件的其他应用程序提供一组用来直接访问实时数据库的接口函数。这样,对于 其他工控设备与实时数据库进行数据交换的需求,只要利用这样一组接口函数开发不同的驱 动程序便可得到满足,从而增强了实时数据库系统的通用性与开放性。下面列出了一些较为 常用的数据访问接口函数。int CreatTag();//创建数据点char *GetNameByID(char *tagID);//通过数据点ID 取得数据点名char *GetIDByName(char *tagName);//通过数据点名得到数据点IDpv_type_set GetPVType(char *tagName);//通过数据点名得到数据点过程量值类型int GetPVByName(char *tagName, pv_data_set *pv);//根据数据点名获取数据点过程量值int SetPVByName(char *tagName, pv_data_set *pv);//根据数据点名写入数据点过程量值5 结语实时数据库作为监控组态软件的核心部分,其组织结构是否高效直接影响到与底层 I/O 过程设备的数据交换,与实时监控界面程序的数据传递,与组态软件中其它运行程序的实时 通信等多项技术指标。所以,其设计要求结构精简,存储高效,并且具备相当的可靠性与稳 定性。经实际应用证明,由本文提出的利用共享内存,文件系统,通用数据库多层级存储介 质相结合的实时数据库存储结构,能较充分地利用Linux 操作系统实时多任务方面的特性, 较好地满足工业生产现场环境的实时响应要求。另外,实时数据库的开发是一个有着广阔前景的研究领域,其还包括诸如I/O 调度与缓冲管理,恢复与超载管理等多项实现内容[7]。本文作者创新点:本文利用Linux 操作系统对多任务并发处理操作的良好支持,采用二 级地址索引为数据点独立分配共享内存空间,以多进程调度的方式实现了数据采集与处理从 而提高了系统吞吐量和数据存取效率。同时,多层级的实时数据库存储结构能较好地兼顾工 业生产环境的时间约束与数据图形表现多样性的要求。

    时间:2018-06-22 关键词: Linux 实时数据库 组态软件 嵌入式linux

  • 基于Linux的动态电源管理设计

    基于Linux的动态电源管理设计

    为了在产品众多、竞争激烈的市场上使产品与众不同,手持设备的制造商们往往把电池寿命和电源管理作为手机、PDA、多媒体播放器、游戏机、其它便携式消费类设备等产品的关键卖点来考虑。用户是从电池寿命这方面来看待电源管理的成效,其实它是多种因素共同作用的结果,这些因素包括 CPU 功能、系统软件、中间件,以及使用户可以在更长的充电或更换电池的间隔时间内享用各自设备的策略。电源管理范围任何拥有笔记本电脑的人都会感觉到,他们的这种便携式设备依靠电池运行时,与依靠交流电(主电源)运行对比,行为表现不一样,屏幕变暗了,处理器时钟变慢了,并且系统只要有可能,就会转入待机或睡眠状态。另外,PDA 的拥有者们还发现,在设备停用一段时间之后,屏幕会变暗,设备甚至进入睡眠状态,而手机用户会注意到,拨号之后,背光和按键照明光熄灭了。在肉眼能够察觉的这些行为的背后,是若干软硬件技术和策略在起作用。明显的行为如全速运行、待机和睡眠等,充分利用了 CPU 本身的功能来降低工作电压和/或时钟频率,从而省电。大多数设备用户觉察不到的是,实际的电源管理还可以是渐增的,并且可以每秒发生好几百次,而不是整个系统状态大规模变化。任何动态电源管理 (DPM) 战略开始都是调节便携式设备中存在的一个或多个处理器内核的工作电压和频率——高度集成的、基于 PowerPC、ARM 和 x86 的系统通常配备一块 DSP 或智能基带处理器。实际上,Intel XScale 和 TI OMAP 等处理器系列提供了内核电压和频率的动态调节。不过,现代嵌入式处理器的用电效率非常高,以至于 CPU 并不总是主要的耗能器件,其它大能耗器件可能包括高性能存储器、彩色显示器和无线接口。因此,动态电源管理系统如果只关注对处理器内核的电压和频率进行调节的话,那么它的用途也许很有限。真正有用的电源管理方案将支持各种电压和时钟的快速调节,既可以与 CPU 内核的运行协同进行,也可以独立进行。架构两种现有的电源管理方案是来自“白箱”PC 及笔记本电脑领域,第一种是传统的“高级电源管理”(Advanced Power Management,简称 APM)方案,仍用于许多基于 Linux 的便携式设备中,而基于微软操作系统的笔记本电脑和手持设备已停止采用这种方案了,第二种是“高级配置和电源接口”(Advanced Configuration and Power Interface,简称 ACPI),这种现行标准得到了英特尔、东芝等公司的支持。对于 PC、笔记本电脑、服务器、甚至面向通信设备的刀片服务器等“商业成品”(commercial off-the-shelf,简称 COTS)硬件,ACPI 等系统更受青睐,不过它们表现出对目前盛行的 x86/IA-32 BIOS 架构的强烈依赖。嵌入式系统通常没有 BIOS(在 PC/AT 的意义上),并且通常无法奢侈地配备机器抽象,来把操作系统与低层器件和电源管理活动隔离开来。因此,在嵌入式 Linux 中,就像在其它针对电池供电应用的操作系统一样,电源管理活动需要对操作系统内核以及设备驱动程序做特殊干预。不过请注意一件重要事情,虽然动态电源管理的低层实现是驻留在操作系统内核,但电源管理战略及策略可以源自中间件和用户应用软件代码,实际也是如此。接口和 API理想状况下,电源管理系统对于软件堆栈的尽可能多的层次而言,几乎是完全透明的。实际上,这正是 Transmeta 公司在其 Crusoe 架构中遵循的路线,并且已经成为现有的各种基于 BIOS 的电源管理方案的目标。不过,拥有手持设备制造经验的开发人员将证明这一事实:整个系统的各个部分都需要某种程度的直接参与,如下所述:内核接口在针对 Linux 的 DPM 架构中,内核中的 DPM 子系统负责维持系统的电源状态,并把 DPM 系统的各个电源得到管理的元件联系在一起。DPM 子系统通过多个 API 直接与设备驱动程序通信,这些 API 把驱动程序从完全运行状态转为各种电源得到管理的状态。策略管理器(或应用软件自身)通过多个 API 向 DPM 子系统提供指导,这些 API 定义各种策略,并在定义好的运行点之间转移整个系统。驱动程序接口启用了 DPM 的设备驱动程序比默认驱动程序具有更多“状态”:由外部事件通过各种状态来驱动它们,或通过来自内核 DPM 子系统的回调来驱动它们,从而反映并遵循运行策略。驱动程序 API 还允许驱动程序登记它们连接和管理的各个设备的基本运行特征,从而实现更精细的策略决策。用户程序 API用户程序(应用软件)分为三类:·可感知电源管理的应用软件·可感知电源管理的“包装器”中的传统应用软件·不带电源管理的传统应用软件可感知电源管理的应用软件能够充分利用来自策略管理器的 API,从而建立各自的基础约束,并强制电源管理策略发生变化,以便匹配各自的执行要求。不直接带有电源管理功能的传统应用软件可以“包装”到代码或补丁中,从而实现相当的效果,它们还可以按照默认行为来运行,这取决于更宽范围的默认策略管理。嵌入式 Linux DPM 下的实际机制包括各种 API,比如 dpm_set_os()(内核)、assert_constraint()、remove_constraint() 和 set_operating_state()(内核和驱动程序)、set_policy() 和 set_task_state()(经由系统调用的用户级接口),以及 /proc 接口。借助 DPM 实现节能独特的节能机会DPM 的定义性特征是电源管理的迅速、高频率性质。传统的台式机/笔记本电脑范例的运行速度是以数百毫秒或数秒计,与此不同的是,DPM 使各设备的管理速度只受限于改变供电电压 (T芕) 或 CPU 时钟 (T颇) 所需的时间。在流视频的各帧之间实现节能,是对 DPM 的这一性质的最好写照。高质量的流视频的运行频率是 24 帧/秒,在各帧之间留了 41.66 毫秒的可用时间,用于渲染下一帧和进行其它活动。即使是在运行频率为 40-60MHz 的低功率 CPU 内核心上,41.66 毫秒也代表“很长的时间”,并为电源管理带来了充足的机会。当一帧视频呈现给用户之后,活动按以下方式继续进行:·CPU 请求/索取下一帧压缩视频,它来自本地存储系统或流文件缓冲器——CPU 活动量很低;·压缩图像(经由 DMA 或共享内存)被传输至编解码器(DSP 或其它专用硬件),进行解压/渲染——CPU 活动量中等,编解码器活动量高;·当图像准备就绪,即解压完毕时,CPU 调用视频接口驱动程序——CPU 利用率高,最终的显示器利用率高;·在图像处理的整个过程中,显示器背光都要消耗能量。充分利用视觉暂留或针对图像本身的伽玛调节,该参数也可以降低到一个更适度的消耗级别。对视频帧处理的各个阶段期间的能量需求进行总结,得图 2 所示的波形,“线上方的”面积代表潜在的节能。时钟频率调节与电压调节的益处对比CPU 时钟频率调节是嵌入式器件的一种常用省电方式。在给定电压下,与较低的时钟速度相比,较高的时钟速度需要更多的电量才能把逻辑电平推到饱和(克服电容)。而且,时钟频率调节比较容易实现,至少在 CPU内核内部是这样。然而,电压调节带来的益处要大得多——能耗与时钟频率成正比,而几乎是系统电压的立方!DPM 本身并不对时钟频率和电压之间的关联做假设。理论上,两项参数均可以独立而连续地改变。实际上,在给定时钟频率下,存在最低可行电压(最低供电电压)——更低的电压无法在要求的周期时间内把逻辑电平推到饱和,而更高的电压只会消耗更多的电。为了简化电源管理算法,DPM 等方案也不去尝试连续改变时钟和电压,而是由设计人员在时钟/电压连续统计上挑选出一系列合理的运行点,并且 DPM 逐点驱动 CPU 和其它电源得到管理的系统器件。开发和部署电源管理面临的挑战面向嵌入式 Linux 的 DPM 是一种正在发展的技术。由于全球开放源代码领域的开发人员所做的贡献,它的核心技术正在进步,但实际应用仍然必须清除一系列“路障”。在所有器件子系统中协调 DPMCPU 时钟和电压引来了一套非常灵活的电源管理参数,这些参数针对设备中的主要耗电器件之一。其它器件(背光、射频等等)也带来了电源管理机会,但有可能导致非常不同的运行点类型和数量。不过,系统中的各种节点完全独立的情况非常少。CPU 连接到总线、桥路、内存,并直接连接到其它外设,而改变一种器件的时钟和电压可能会限制它与邻近器件的电连接和逻辑连接。解决此类不兼容问题的选择方案包括:· 把 CPU 内核和外设编组成块,这些块共享运行点特性· 选择互为倍数的运行点时钟速率· 运用电压变换器/缓冲器或开路集电极电路来缩小电压差异克服电压及频率调节等待时间为了支持 DPM 等积极的节能范例,系统硬件的响应速度必须能够至少象 DPM 策略指导的状态变化的发生速度一样快。也就是说,如果 DPM 系统需要在给定时间内从一个运行点过渡到另一个运行点,电源电路的时钟设定必须能够与这些变化一同发生。换句话说,改变电压所花的时间必须少于运行点之间的过渡时间( T芕 < T芆P )。为了实现上述的帧间方案,T芕 必须在 5 毫秒范围内 (200 Hz)。一些直流到直流电源内部运行速度约为 200Hz,在有负载情况下只能交付大约 200 毫秒 (5Hz) 内的电压变化,结果降低了 基于 DPM 的系统的可用解析度和效用。实时影响直到最近,CPU 电压和频率调节仍给实时性能带来了严峻挑战。两种参数中任何一种发生变化都造成了不稳定,“重新锁定”锁相环路和其它动态时钟机制需要时间,这些都造成了很长的等待时间(有时是许多毫秒),在此期间 CPU 既不能执行计算操作,又不能响应外部事件(中断)。TI OMAP 和 Intel XScale 等先进嵌入式处理器能够在等待时间以几微秒计的情况下调节频率,并在等待时间以数十微秒计的情况下响应变化的电压,不会中断系统运行,从而实现了更积极、更精细的策略。对实时性能的一个更普遍的挑战是深度睡眠方式期间对中断的响应。大多数片上外设可以设定为在收到中断时“唤醒”系统,不过开发人员必须仔细规定各项策略,来启用(选择性的)基于器件的唤醒,并考虑整个系统的等待时间和存储类别,从而确保及时执行中断处理程序和用户空间对事件的响应(优先等待时间)。参考平台的功能虽然许多 CPU 核心和 SoC(单片系统)的确能够响应频繁的时钟变化和电压变化,但它们所在的参考板通常不能做到这点。事实上,许多 CPU 参考及评估板无法足够快地为 DPM 调节时钟和电压,而且很多板根本不允许对这些参数做任何实时调节!在这些情况下,开发人员必须等待对各自生产硬件的访问,以便衡量各种电源管理方案的益处。嵌入式 Linux 的采用、DPM 和差异化理想情况下,设备用户既不需要了解也不必关心他们购买的手持设备中的底层操作系统。不过,运营商供应的“售后市场”软件正在给予设备制造商的首选操作系统更多的可见度,并正在一个以前不存在品牌的领域创造品牌。尽管品牌创建一直是微软公司的一项重点,但 Windows 系列在手机市场等大批量服务交付市场的普及落在了 Symbian、Brew 的后面,并且现在也落后于多种基于嵌入式 Linux 的新型设计。设备制造商转向 Linux 的原因之一是有机会充分利用基于各种标准的电源管理,而不是目前的专有方案。正在发展的动态电源管理功能,伴随着 ARM 公司的 IEM 等电源剖析技术,正在向手机制造商和其他智能设备 OEM 们提供威力强大的新型工具,来增强产品的差异化,实现更快的产品上市时间,并满足最终用户和运营商的技术要求。

    时间:2018-06-19 关键词: Linux 电源管理 嵌入式linux dpm

  • 基于ARM+DSP的嵌入式Linux数控系统设计

    随着嵌入式技术的发展,ARM、DSP 处理器性能日益强大,而体积、功耗、成本却不断降低; Linux操作系统健壮开源、支持多平台、软件资源丰富,可方便移植到嵌入式系统中。目前ARM-Linux 技术在嵌入式领域得到广泛应用。近年出现很多专用运动控制DSP 芯片如PCL6045、MCX314 等,运动控制功能强大、插补算法成熟、实时性好。在这一技术背景下,作者提出一种基于ARM + DSP 结构的嵌入式Linux 数控系统设计方法,对数控系统小型化、集成化及经济普及化有实际意义。1 嵌入式Linux 数控架构传统数控系统中广泛采用的解决方案为基于PC机和运动控制板卡的结构实现方式: PC 机主要实现用户交互、文件管理以及通信等非实时数控操作; 运动控制板卡负责运动控制和机床I /O 等数控系统中对实时性有严格要求的数控功能。这种结构将数控系统中各功能模块分为实时模块和非实时模块两类,由运动控制板卡来保证实时性要求,充分利用PC 机软件丰富、功能强大的优势,可实现复杂空间插补算法,数控系统软件功能大大增多增强,形成数控即软件的概念。这种方案具有信息处理能力强、运动轨迹控制准确、开放程度高、通用性好等特点。但也存在以下缺点: 运动控制卡需要插入PC 机主板的PCI 或ISA插槽,因此每台数控装置都必须配置一台PC 机作为上位机,无疑对设备的体积、成本和运行环境都有一定限制,难以独立运行和小型化[1]。嵌入式Linux 数控系统借鉴传统PC + 运动控制板卡方式,将数控系统也分为实时模块和非实时模块分别实现。整个系统由硬件层、操作系统层和应用层组成。硬件层以ARM-Linux 为总体控制核心完成数控系统中任务调度、NC 代码编译、人机交互、系统监视等非实时数控功能,以DSP 芯片PCL6045 为运动控制核心实现各种数控中的运动控制要求以保证实时性。硬件层之上是操作系统层,这一层又分为驱动层和内核层。开发过程中根据硬件配置,增加相应驱动程序,例如要添加相关存储设备、通信设备与I /O 设备等驱动程序。立即下载浏览全文:基于ARM_DSP的嵌入式Linux数控系统设计.pdf

    时间:2018-06-18 关键词: DSP ARM Linux 数控 嵌入式linux pcl6045

  • 基于嵌入式Linux的细胞特征提取算法设计

    1 引言至今为止,图像处理已经发展成为相对比较熟悉的研究领域,并且在此领域也取得了巨大的研究成果,但是,绝大部分研究平台都是依赖于传统PC机或者研究成果应用于PC机,PC机体积相对庞大,携带不便,这就凸显了其应用的局限性[1]。同时,对于某些专业领域的应用还会造成大量资源的浪费。为此本文提出了基于嵌入式Linux来实现细胞特征提取的方法。本文算法首先对原始图像进行预处理来减小杂质、噪声等对细胞特征的影响;其次,对预处理过的细胞图像进行区域标号和邻域跟踪;最后,通过自己设计封装嵌入式Linux图像处理接口完成算法的封装实现。实验结果表明,本文提出的方法应用在嵌入式Linux系统下能够准确地提取出细胞的周长及面积特征。2 细胞特征提取原理2.1 细胞图像预处理从显微镜下采集细胞图片中,会出现一些杂质、噪声以及其它各种细微组织的干扰。原始细胞图像如图1(a)所示,为一幅胃粘膜上皮显微细胞图像。为了在后文算法中精确地提取出细胞特征,下面介绍图像几种预处理[2]:(1)对图像进行平滑和滤波处理对在显微镜下采集来的原始肿瘤细胞图像进行平滑和滤波操作,可以改变图像周围像素灰度值相差较大的像素点的值,从而抑制图像中的噪声,处理效果如图1(b)所示,可知噪声点得到了很好的抑制。(2)对图像灰度化操作立即下载浏揽全文:基于嵌入式Linux的细胞特征提取算法设计.pdf

    时间:2018-06-18 关键词: Linux 嵌入式linux 细胞特征提取算法设计

  • Linux快速入门之:嵌入式Linux基础

    1.1嵌入式Linux基础自由开源软件在嵌入式应用上,受到青睐,Linux日益成为主流的嵌入式操作系统之一。随着摩托罗拉手机A760、IBM智能型手表WatchPad、夏普PDAZaurus等一款款高性能“智能数码产品”的出现,以及Motolola、三星、MontaVista、飞利浦、Nokia、IBM、SUN等众多国际顶级巨头的加入,嵌入式Linux的队伍越来越庞大了。目前,国外不少大学、研究机构和知名公司都加入了嵌入式Linux的开发工作,成熟的嵌入式Linux产品不断涌现。2004年全球嵌入式Linux市场规模已达9150万美元,2005年有1.336亿美元,2006年有1.653亿美元,2007年达到2.011亿美元,每年平均增长30%。究竟是什么原因让嵌入式Linux系统发展如此迅速。业界归纳为三大原因︰第一,Linux在嵌入式系统所需的实时性、电源管理等核心技术不断发展;第二,国际标准组织(如OSDL、CELF等)持续建立嵌入式Linux相关标准,有效解决版本分歧与兼容性问题;第三,业界主导组织、开发厂商等不断推出嵌入式Linux相关开发工具、维护系统。嵌入式Linux以年费订阅方式为主,与其他的以产品利润为收入方式的嵌入式系统不同,弹性的捆绑销售策略,助其成功地逐年提高市场占有率,从2004年的46.8%扩大到2007年的56.4%。国际有名的嵌入式Linux操作系统提供商Montavista,收购了PalmSource的爱可信和奇趣科技等,加强了对中国市场的投入,并在整个嵌入式操作系统市场中,占据了重要地位。而嵌入式操作系统的领先厂商,也改变了原来的单一产品路线,开始推出自己的Linux软件产品,实现“两条腿走路”。国内的嵌入式软件厂商也以Linux为突破口,纷纷开发各种基于Linux的操作系统产品。这些嵌入式Linux厂商已经形成了一个不容忽视的群体。以下就从Linux开始,一层层揭开嵌入式Linux的面纱。1.1.1Linux发展概述简单地说,Linux是指一套免费使用和自由传播的类UNIX操作系统。人们通常所说的Linux是LinusTorvalds所写的Linux操作系统内核。当时的Linus还是芬兰赫尔辛基大学的一名学生,他主修的课程中有一门课是操作系统,而且这门课是专门研究程序的设计和执行。最后这门课程提供了一种称为Minix的初期UNIX系统。Minix是一款仅为教学而设计的操作系统,而且功能有限。因此,和Minix的众多使用者一样,Linus也希望能给它添加一些功能。在之后的几个月里,Linus根据实际的需要编写了磁盘驱动程序以便下载访问新闻组的文件,又编写了个文件系统以便能够阅读Minix文件系统中的文件。这样,“当你有了任务切换,有了文件系统和设备驱动程序后,这就是UNIX,或者至少是其内核。”。于是,0.0.1版本的Linux就诞生了。Linus从一开始就决定自由传播Linux,他把源代码发布在网上,于是,众多的爱好者和程序员也都通过互联网加入到Linux的内核开发工作中。这个思想与FSF(FreeSoftwareFoundation)资助发起的GNU(GNU’sNotUNIX)的自由软件精神不谋而合。GNU是为了推广自由软件的精神以实现一个自由的操作系统,然后从应用程序开始,实现其内核。而当时Linux的优良性能备受GNU的赏识,于是GNU就决定采用Linus及其开发者的内核。在他们的共同努力下,Linux这个完整的操作系统诞生了。其中的程序开发共同遵守GeneralPublicLicense(GPL)协议,这是最开放也是最严格的许可协议方式,这个协议规定了源码必须可以无偿的获取并且修改。因此,从严格意义上说,Linux应该叫做GNU/Linux,其中许多重要的工具如gcc、gdb、make、emacs等都是GNU贡献的。这个“婴儿版”的操作系统以平均两星期更新一次的速度迅速成长,如今的Linux已经有超过250种发行版本,且可以支持所有体系结构的处理器,如X86、PowerPC、ARM、Xscale等,也可以支持带MMU或不带MMU的处理器。到目前为止,它的内核版本也已经从原先的0.0.1发展到现在的2.6.xx。小知识自由软件(freesoftware)中的free并不是指免费,而是指自由。它赋予使用者4种自由。·自由之1:有使用软件的自由。·自由之2:有研究该软件如何运作的自由,并且得以改写该软件来满足使用者自身的需求。取得该软件的源码是达成此目的前提。·自由之3:有重新散布该软件的自由,所以每个人都可以藉由散布自由软件来敦亲睦邻。·自由之4:有改善再利用该软件的自由,并且可以发表改写版供公众使用,如此一来,整个社群都可以受惠。取得该软件的源码是达成此目的前提。GPL:GPL协议是GNU组织、维护的一种版权协议,遵守这个协议的软件可以自由地获取、查看、使用其源代码。GPL协议是整个开源世界的精神基础。Linux的内核版本号:Linux内核版本号格式是x.y.zz-www,数字x代表版本类型,数字y为偶数时是稳定版本,为奇数时是开发版本,如2.0.40为稳定版本,2.3.41为开发版本,测试版本为3个数字加上测试号,如2.4.12-rc1。最新的Linux内核版本可从http://www.kernel.org上获得。1.1.2Linux作为嵌入式操作系统的优势从Linux系统的发展过程可以看出,Linux从最开始就是一个开放的系统,并且它始终遵循着源代码开放的原则,它是一个成熟而稳定的网络操作系统,作为嵌入式操作系统有如下优势。1.低成本开发系统Linux的源码开放性允许任何人获取并修改Linux的源码。这样一方面大大降低了开发的成本,另一方面又可以提高开发产品的效率。并且还可以在Linux社区中获得支持,用户只需向邮件列表发一封邮件,即可获得作者的支持。2.可应用于多种硬件平台Linux可支持X86、PowerPC、ARM、Xscale、MIPS、SH、68K、Alpha、Sparc等多种体系结构,并且已经被移植到多种硬件平台。这对于经费、时间受限制的研究与开发项目是很有吸引力的。Linux采用一个统一的框架对硬件进行管理,同时从一个硬件平台到另一个硬件平台的改动与上层应用无关。3.可定制的内核Linux具有独特的内核模块机制,它可以根据用户的需要,实时地将某些模块插入到内核中或者从内核中移走,并能根据嵌入式设备的个性需要量体裁衣。经裁减的Linux内核最小可达到150KB以下,尤其适合嵌入式领域中资源受限的实际情况。当前的2.6内核加入了许多嵌入式友好特性。4.性能优异Linux系统内核精简、高效并且稳定,能够充分发挥硬件的功能,因此它比其他操作系统的运行效率更高。在个人计算机上使用Linux,可以将它作为工作站。它也非常适合在嵌入式领域中应用,对比其他操作系统,它占用的资源更少,运行更稳定,速度更快。5.良好的网络支持Linux是首先实现TCP/IP协议栈的操作系统,它的内核结构在网络方面是非常完整的,并提供了对包括十兆位、百兆位及千兆位的以太网,还有无线网络、Tokenring(令牌环)和光纤甚至卫星的支持,这对现在依赖于网络的嵌入式设备来说无疑是很好的选择。1.1.3Linux发行版本由于Linux属于GNU系统,而这个系统采用GPL协议,并保证了源代码的公开,于是众多组织或公司在Linux内核源代码的基础上进行了一些必要的修改加工,然后再开发一些配套的软件,并把它整合成一个自己的发布版Linux。除去非商业组织Debian开发的DebianGNU/Linux外,美国的RedHat公司发行了RedHatLinux,法国的Mandrake公司发行了MandrakeLinux,德国的SUSE公司发行了SUSELinux,国内众多公司也发行了中文版的Linux,如著名的红旗Linux。Linux目前已经有超过250个发行版本。下面仅对RedHat、Debian、Mandrake等具有代表性的Linux发行版本进行介绍。1.RedHat国内,乃至是全世界的Linux用户最熟悉的发行版想必就是RedHat了。RedHat最早是由BobYoung和MarcEwing在1995年创建的。目前RedHat分为两个系列:由RedHat公司提供收费技术支持和更新的RedHatEnterpriseLinux(RHEL,RedHat的企业版),以及由社区开发的免费的桌面版FedoraCore。RedHat企业版有3个版本——AS、ES和WS。AS是其中功能最为强大和完善的版本。而正统的桌面版RedHat版本更新早已停止,最后一版是RedHat9.0。本书就以稳定性高的RHELAS作为安装实例进行讲解。官方主页:http://www.redhat.com/。2.Debian之所以把Debian单独列出,是因为DebianGNU/Linux是一个非常特殊的版本。在1993年,伊恩·默多克(IanMurdock)发起Debian计划,它的开发模式和Linux及其他开放性源代码操作系统的精神一样,都是由超过800位志愿者通过互联网合作开发而成的。一直以来,DebianGNU/Linux被认为是最正宗的Linux发行版本,而且它是一个完全免费、高质量的且与UNIX兼容的操作系统。Debian系统分为3个版本,分别为稳定版(Stable)、测试版(Testing)和不稳定版(Unstable)。每次发布的版本都是稳定版,而测试版在经过一段时间的测试证明没有问题后会成为新的稳定版。Debian拥有超过8710种不同的软件,每一种软件都是自由的,而且有非常方便的升级安装指令,基本囊括了用户的所有需要。Debian也是最受欢迎的嵌入式Linux之一。官方主页:http://www.debian.org/。3.国内的发行版本及其他目前国内的红旗、新华等都发行了自己的Linux版本。除了前面所提到的这些版本外,业界还存在着诸如gentoo、LFS等适合专业人士使用的版本。在此不做介绍,有兴趣的读者可以自行查找相关的资料做进一步的了解。1.1.4如何学习Linux正如人们常说的“实践出真知”,学习Linux的过程也一样。只有通过大量的动手实践才能真正地领会Linux的精髓,才能迅速掌握在Linux上的应用开发,相信有编程语言经验的读者一定会认同这一点。因此,在本书中笔者安排了大量的实验环节和课后实践环节,希望读者尽可能多参与。另外要指出的是,互联网也是一个很好的学习工具,一定要充分地加以利用。正如编程一样,实践的过程中总会出现多种多样的问题,笔者在写作的过程当中会尽可能地考虑可能出现的问题,但限于篇幅和读者的实际情况,不可能考虑到所有可能出现的问题,所以希望读者能充分利用互联网这一共享的天空,在其中寻找答案。

    时间:2018-06-15 关键词: 操作系统 red 基础教程 hat 嵌入式linux debian 自由软件

  • 嵌入式Linux开发环境的搭建之:嵌入式开发环境的搭建

    嵌入式Linux开发环境的搭建之:嵌入式开发环境的搭建

    5.1嵌入式开发环境的搭建5.1.1嵌入式交叉编译环境的搭建交叉编译的概念在第4章中已经详细讲述过,搭建交叉编译环境是嵌入式开发的第一步,也是必备的一步。搭建交叉编译环境的方法很多,不同的体系结构、不同的操作内容甚至是不同版本的内核,都会用到不同的交叉编译器,而且,有些交叉编译器经常会有部分的bug,这都会导致最后的代码无法正常地运行。因此,选择合适的交叉编译器对于嵌入式开发是非常重要的。交叉编译器完整的安装一般涉及多个软件的安装(读者可以从ftp://gcc.gnu.org/pub/下载),包括binutils、gcc、glibc等软件。其中,binutils主要用于生成一些辅助工具,如objdump、as、ld等;gcc是用来生成交叉编译器的,主要生成arm-linux-gcc交叉编译工具(应该说,生成此工具后已经搭建起了交叉编译环境,可以编译Linux内核了,但由于没有提供标准用户函数库,用户程序还无法编译);glibc主要是提供用户程序所使用的一些基本的函数库。这样,交叉编译环境就完全搭建起来了。上面所述的搭建交叉编译环境比较复杂,很多步骤都涉及对硬件平台的选择。因此,现在嵌入式平台提供厂商一般会提供在该平台上测试通过的交叉编译器,而且很多公司把以上安装步骤全部写入脚本文件或者以发行包的形式提供,这样就大大方便了用户的使用。如优龙的FS2410开发光盘里就附带了2.95.3和3.3.2两个版本的交叉编译器,其中前一个版本是用于编译Linux2.4内核的,而后一个版本是用于编译Linux2.6版本内核的。由于这是厂商测试通过的编译器,因此可靠性会比较高,而且与开发板能够很好地吻合。所以推荐初学者直接使用厂商提供的编译器。当然,由于时间滞后的原因,这个编译器往往不是最新的版本,若需要更新时希望读者另外查找相关资料学习。本书就以优龙自带的cross-3.3.2为例进行讲解(具体的名称不同厂商可能会有区别)。安装交叉编译器的具体步骤在第2章的实验二中已经进行了详细地讲解了,在此仅回忆关键步骤,对于细节请读者参见第2章的实验二。在/usr/local/arm下解压cross-3.3.2.bar.bz2。[root@localhostarm]#tar–jxvfcross-3.3.2.bar.bz2[root@localhostarm]#ls3.3.2cross-3.3.2.tar.bz2[root@localhostarm]#cd./3.3.2[root@localhostarm]#lsarm-linuxbinetcincludeinfoliblibexecmansbinshareVERSIONS[root@localhostbin]#whicharm-linux*/usr/local/arm/3.3.2/bin/arm-linux-addr2line/usr/local/arm/3.3.2/bin/arm-linux-ar/usr/local/arm/3.3.2/bin/arm-linux-as/usr/local/arm/3.3.2/bin/arm-linux-c++/usr/local/arm/3.3.2/bin/arm-linux-c++filt/usr/local/arm/3.3.2/bin/arm-linux-cpp/usr/local/arm/3.3.2/bin/arm-linux-g++/usr/local/arm/3.3.2/bin/arm-linux-gcc/usr/local/arm/3.3.2/bin/arm-linux-gcc-3.3.2/usr/local/arm/3.3.2/bin/arm-linux-gccbug/usr/local/arm/3.3.2/bin/arm-linux-gcov/usr/local/arm/3.3.2/bin/arm-linux-ld/usr/local/arm/3.3.2/bin/arm-linux-nm/usr/local/arm/3.3.2/bin/arm-linux-objcopy/usr/local/arm/3.3.2/bin/arm-linux-objdump/usr/local/arm/3.3.2/bin/arm-linux-ranlib/usr/local/arm/3.3.2/bin/arm-linux-readelf/usr/local/arm/3.3.2/bin/arm-linux-size/usr/local/arm/3.3.2/bin/arm-linux-strings/usr/local/arm/3.3.2/bin/arm-linux-strip可以看到,在/usr/local/arm/3.3.2/bin/下已经安装了很多交叉编译工具。用户可以查看arm文件夹下的VERSIONS文件,显示如下:Versionsgcc-3.3.2glibc-2.3.2binutils-headToolchainbinutilsconfiguration:../binutils-head/configure…Toolchainglibcconfiguration:../glibc-2.3.2/configure…Toolchaingccconfiguration../gcc-3.3.2/configure…可以看到,这个交叉编译工具确实集成了binutils、gcc、glibc这几个软件,而每个软件也都有比较复杂的配置信息,读者可以查看VERSIONS文件了解相关信息。5.1.2超级终端和minicom配置及使用前文已知,嵌入式系统开发的程序只能在对应的嵌入式硬件平台上运行,那么如何把开发板上的信息显示给开发人员呢?最常用的就是通过串口线输出到宿主机的显示器上,这样,开发人员就可以看到系统的运行情况了。在Windows和Linux中都有不少串口通信软件,可以很方便地对串口进行配置,其中最主要的配置参数是波特率、数据位、停止位、奇偶校验位和数据流控制位等,但是它们一定要根据实际情况进行相应配置。下面介绍Windows中典型的串口通信软件“超级终端”和在Linux下的“minicom”。1.超级终端首先,打开Windows下的“开始”→“附件”→“通讯”→“超级终端”,这时会出现如图5.1所示的新建超级终端界面,在“名称”处可随意输入该连接的名称。图5.1新建超级终端界面接下来,将“连接时使用”的方式改为“COM1”,即通过串口1,如图5.2所示。接下来就到了最关键的一步——设置串口连接参数。要注意,每块开发板的连接参数有可能会有差异,其中的具体数据在开发商提供的用户手册中会有说明。如优龙的这款FS2410采用的是波特率为115200,数据位数为8,无奇偶校验位,停止位数为1,无硬件流控,其对应配置如图5.3所示。 图5.2选择连接时使用方式 图5.3配置串口相关参数这样,就基本完成了配置,最后一步单击“确定”按钮就可以了。这时,读者可以把开发板的串口线和PC机相连,若配置正确,在开发板上电后,在超级终端的窗口里应能显示类似于图5.4的串口信息。图5.4在超级终端上显示信息注意要分清开发板上的串口1、串口2,如在优龙的开发板上标有“UART1”、“UATR2”,否则串口无法打印出信息。2.minicomminicom是Linux下串口通信的软件,它的使用完全依靠键盘的操作,虽然没有“超级终端”那么易用,但是使用习惯之后读者将会体会到它的高效与便利。下面主要讲解如何对minicom进行串口参数的配置。首先在命令行中键入“minicom”,这就启动了minicom软件。minicom在启动时默认会进行初始化配置,如图5.5所示。可以通过“minicom-s”命令进行minicom的配置。图5.5minicom启动注意在minicom的使用中,经常会遇到3个键的操作,如“CTRL-AZ”,这表示先同时按下CTRL和“A”,然后松开这两个键再按下“Z”。正如图5.5中的提示,接下来可键入CTRL-AZ,来查看minicom的帮助,如图5.6所示。按照帮助所示,可键入“O”(代表Configureminicom)来配置minicom的串口参数,当然也可以直接键入“CTRL-AO”来进行配置。如图5.7所示。图5.6minicom帮助图5.7minicom配置界面在这个配置框中选择“Serialportsetup”子项,进入如图5.8所示的配置界面。图5.8minicom串口属性配置界面上面列出的配置是minicom启动时的默认配置,用户可以通过键入每一项前的大写字母,分别对每一项进行更改。图5.9所示为在“Changewhichsetting”中键入了“A”,此时光标转移到第A项的对应处。图5.9minicom串口号配置注意在minicom中“ttyS0”对应“COM1”,“ttyS1”对应“COM2”。接下来,要对波特率、数据位和停止位进行配置,键入“E”,进入如图5.10所示的配置界面。图5.10minicom波特率等配置界面在该配置界面中,可以键入相应波特率、停止位等对应的字母,即可实现配置,配置完成后按回车键就退出了该配置界面,在上层界面中显示如图5.11所示配置信息,要注意与图5.8进行对比,确定相应参数是否已被重新配置。图5.11minicom配置完成后界面在确认配置正确后,可键入回车返回上级配置界面,并将其保存为默认配置,如图5.12所示。之后,可重新启动minicom使刚才配置生效,在用串口线将宿主机和开发板连接之后,就可在minicom中打印出正确的串口信息,如图5.13所示。图5.12minicom保存配置信息图5.13minicom显示串口信息到此为止,读者已经能将开发板的系统情况通过串口打印到宿主机上了,这样,就能很好地了解硬件的运行状况。小知识通过串口打印信息是一个很常见的手段,很多其他情况如路由器等也是通过配置串口的波特率这些参数来显示对应信息的。5.1.3下载映像到开发板正如第4章中所述,嵌入式开发的运行环境是目标板,而开发环境是宿主机。因此,需要把宿主机中经过编译之后的可执行文件下载到目标板上。要注意的是,这里所说的下载是下载到目标机中的SDRAM。然后,用户可以选择直接从SDRAM中运行或写入到Flash中再运行。运行常见的下载方式有网络下载(如tftp、ftp等方式)、串口下载、USB下载等,本书主要讲解网络下载中的tftp方式和串口下载方式。1.tftptftp是简单文件传输协议,它可以看作是一个FTP协议的简化版本,与FTP协议相比,它的最大区别在于没有用户管理的功能。它的传输速度快,可以通过防火墙,使用方便快捷,因此在嵌入式的文件传输中广泛使用。同FTP一样,tftp分为客户端和服务器端两种。通常,首先在宿主机上开启tftp服务器端服务,设置好tftp的根目录内容(也就是供客户端访问的根目录),接着,在目标板上开启tftp的客户端程序(现在很多Bootloader几乎都提供该服务)。这样,把目标板和宿主机用直连线相连之后,就可以通过tftp协议传输可执行文件了。下面分别讲述在Linux下和Windows下的配置方法。(1)Linux下tftp服务配置。Linux下tftp的服务器服务是由xinetd所设定的,默认情况下是处于关闭状态。首先,要修改tftp的配置文件,开启tftp服务,如下所示:[root@localhosttftpboot]#vim/etc/xinetd.d/tftp#default:off#description:Thetftpserverservesfilesusingthetrivialfiletransfer#protocol.Thetftpprotocolisoftenusedtobootdiskless#workstations,downloadconfigurationfilestonetwork-awareprinters,#andtostarttheinstallationprocessforsomeoperatingsystems.servicetftp{socket_type=dgram/*使用数据报套接字*/protocol=udp/*使用UDP协议*/wait=yes/*允许等待*/user=root/*用户*/server=/usr/sbin/in.tftpd/*服务程序*/server_args=-s/tftpboot/*服务器端的根目录*/disable=no/*使能*/per_source=11cps=1002flags=IPv4}在这里,主要要将“disable=yes”改为“no”,另外,从“server_args”可以看出,tftp服务器端的默认根目录为“/tftpboot”,用户如果需要则可以更改为其他目录。接下来,重启xinetd服务,使刚才的更改生效,如下所示:[root@localhosttftpboot]#servicexinetdrestart(或者使用/etc/init.d/xinetdrestart,而且因发行版的不同具体路径会有所不同)关闭xinetd:[确定]启动xinetd:[确定]接着,使用命令“netstat-au”以确认tftp服务是否已经开启,如下所示:[root@localhosttftpboot]#netstat–au|greptftpActiveInternetconnections(serversandestablished)ProtoRecv-QSend-QLocalAddressForeignAddressStateudp00*:tftp*:*这时,用户就可以把所需要的传输文件放到“/tftpboot”目录下,这样,主机上的tftp服务就可以建立起来了(注意:需要在服务端关闭防火墙)。接下来,用直连线把目标板和宿主机连起来,并且将其配置成一个网段的地址(例如两个IP都可以设置为192.168.1.XXX格式),再在目标板上启动tftp客户端程序(注意:不同的Bootloader所使用的命令可能会不同,例如:在RedBoot中使用load命令下载文件是基于tftp协议的。读者可以查看帮助来获得确切的命令名及格式),如下所示:=>tftpboot0x30200000zImageTFTPfromserver192.168.1.1;ourIPaddressis192.168.1.100Filename'zImage'.Loadaddress:0x30200000Loading:#############################################################################################################################################################################doneBytestransferred=881988(d7544hex)可以看到,此处目标板使用的IP为“192.168.1.100”,宿主机使用的IP为“192.168.1.1”,下载到目标板的地址为0x30200000,文件名为“zImage”。(2)Windows下tftp服务配置。在Windows下配置tftp服务器端需要下载tftp服务器软件,常见的为tftpd32。首先,单击tftpd32下方的设置按钮,进入设置界面,如图5.14所示,在这里,主要配置tftp服务器端地址,也就是宿主机的地址。接下来,重新启动tftpd32软件使刚才的配置生效,这样服务器端的配置就完成了,这时,就可以用直连线连接目标机和宿主机,且在目标机上开启tftp服务进行文件传输,这时,tftp服务器端如图5.15和图5.16所示。 图5.14tftp文件传输图5.15tftpd32配置界面图5.16tftp服务器端显示情况小知识tftp是一个很好的文件传输协议,它的简单易用吸引了广大用户。但它同时也存在着较大的安全隐患。由于tftp不需要用户的身份认证,因此给了黑客的可乘之机。2003年8月12日爆发的全球冲击波(Worm.Blaster)病毒就是模拟一个tftp服务器,并启动一个攻击传播线程,不断地随机生成攻击地址进行入侵。因此在使用tftp时一定要设置一个单独的目录作为tftp服务的根目录,如上文所述的“/tftpboot”等。2.串口下载使用串口下载需要配合特定的下载软件,如优龙公司提供的DNW软件等,一般在Windows下进行操作。虽然串口下载的速度没有网络下载快,但由于它很方便,不需要额外的连线和设置IP等操作,因此也广受用户的青睐。下面就以DNW软件为例,介绍串口下载的方式。与其他串口通信的软件一样,在DNW中也要设置“波特率”、“端口号”等。打开“Configuration”下的“Options”界面,如图5.17所示。图5.17DNW配置界面在配置完之后,单击“SerialPort”下的“Connect”,再将开发板上电,选择“串口下载”,接着再在“SerialPort”下选择“Transmit”,这时,就可以进行文件传输了,如图5.18和图5.19所示。这里DNW默认串口下载的地址为0x30200000。图5.18DNW串口下载图图5.19DNW串口下载情形图5.1.4编译嵌入式Linux内核在做完了前期的准备工作之后,在这一步,读者就可以编译嵌入式Linux的内核了。在这里,本书主要介绍嵌入式Linux内核的编译过程,在下一节会进一步介绍嵌入式Linux中体系结构相关的内核代码,读者在此之后就可以尝试嵌入式Linux操作系统的移植。编译嵌入式Linux内核都是通过make的不同命令来实现的,它的执行配置文件就是在第3章中讲述的makefile。Linux内核中不同的目录结构里都有相应的makefile,而不同的makefile又通过彼此之间的依赖关系构成统一的整体,共同完成建立依赖关系、建立内核等功能。内核的编译根据不同的情况会有不同的步骤,但其中最主要分别为3个步骤:内核配置、建立依赖关系、创建内核映像,除此之外还有一些辅助功能,如清除文件和依赖关系等。读者在实际编译时若出现错误等情况,可以考虑采用其他辅助功能。下面分别讲述这3步主要的步骤。(1)内核配置。第一步内核配置中的选项主要是用户用来为目标板选择处理器架构的选项,不同的处理器架构会有不同的处理器选项,比如ARM就有其专用的选项如“Multimediacapabilitiesportdrivers”等。因此,在此之前,必须确保在根目录中makefile里“ARCH”的值已设定了目标板的类型,如:ARCH:=arm接下来就可以进行内核配置了,内核支持4种不同的配置方法,这几种方法只是与用户交互的界面不同,其实现的功能是一样的。每种方法都会通过读入一个默认的配置文件—根目录下“.config”隐藏文件(用户也可以手动修改该文件,但不推荐使用)。当然,用户也可以自己加载其他配置文件,也可以将当前的配置保存为其他名字的配置文件。这4种方式如下。n makeconfig:基于文本的最为传统的配置界面,不推荐使用。n makemenuconfig:基于文本选单的配置界面,字符终端下推荐使用。n makexconfig:基于图形窗口模式的配置界面,Xwindow下推荐使用。n makeoldconfig:自动读入“.config”配置文件,并且只要求用户设定前次没有设定过的选项。在这4种模式中,makemenuconfig使用最为广泛,下面就以makemenuconfig为例进行讲解,如图5.20所示。图5.20makemenuconfig配置界面从该图中可以看出,Linux内核允许用户对其各类功能逐项配置,一共有18类配置选项,这里就不对这18类配置选项进行一一讲解了,需要的时候读者可以参见相关选项的help。在menuconfig的配置界面中是纯键盘的操作,用户可使用上下键和“Tab”键移动光标以进入相关子项,图5.21所示为进入了“SystemType”子项的界面,该子项是一个重要的选项,主要用来选择处理器的类型。图5.21SystemType子项可以看到,每个选项前都有个括号,可以通过按空格键或“Y”键表示包含该选项,按“N”表示不包含该选项。另外,读者可以注意到,这里的括号有3种,即中括号、尖括号或圆括号。读者可以用空格键选择相应的选项时可以发现中括号里要么是空,要么是“*”;尖括号里可以是空,“*”和“M”,分别表示包含选项、不包含选项和编译成模块;圆括号的内容是要求用户在所提供的几个选项中选择一项。此外,要注意2.4和2.6内核在串口命名上的一个重要区别,在2.4内核中“COM1”对应的是“ttyS0”,而在2.6内核中“COM1”对应“ttySAC0”,因此在启动参数的子项要格外注意,如图5.22所示,否则串口打印不出信息。图5.22启动参数配置子项一般情况下,使用厂商提供的默认配置文件都能正常运行,所以用户初次使用时可以不用对其进行额外的配置,在以后需要使用其他功能时再另行添加,这样可以大大减少出错的几率,有利于错误定位。在完成配置之后,就可以保存退出,如图5.23所示。图5.23保存退出(2)建立依赖关系。由于内核源码树中的大多数文件都与一些头文件有依赖关系,因此要顺利建立内核,内核源码树中的每个Makefile都必须知道这些依赖关系。建立依赖关系通常在第一次编译内核的时候(或者源码目录树的结构发生变化的时候)进行,它会在内核源码树中每个子目录产生一个“.depend”文件。运行“makedep”即可。在编译2.6版本的内核通常不需要这个过程,直接输入“make”即可。(3)建立内核建立内核可以使用“make”、“makezImage”或“makebzImage”,这里建立的为压缩的内核映像。通常在Linux中,内核映像分为压缩的内核映像和未压缩的内核映像。其中,压缩的内核映像通常名为zImage,位于“arch/$(ARCH)/boot”目录中。而未压缩的内核映像通常名为vmlinux,位于源码树的根目录中。到这一步就完成了内核源代码的编译,之后,读者可以使用上一小节所讲述的方法把内核压缩文件下载到开发板上运行。小知识在嵌入式Linux的源码树中通常有以下几个配置文件,“.config”、“autoconf.h”、“config.h”,其中“.config”文件是makemenuconfig默认的配置文件,位于源码树的根目录中。“autoconf.h”和“config.h”是以宏的形式表示了内核的配置,当用户使用makemenuconfig做了一定的更改之后,系统自动会在“autoconf.h”和“config.h”中做出相应的更改。它们位于源码树的“/include/linux/”下。5.1.5Linux内核源码目录结构Linux内核源码的目录结构如图5.24所示。n /include子目录包含了建立内核代码时所需的大部分包含文件,这个模块利用其他模块重建内核。n /init子目录包含了内核的初始化代码,这里的代码是内核工作的起始入口。n /arch子目录包含了所有处理器体系结构特定的内核代码。如:arm、i386、alpha。n /drivers子目录包含了内核中所有的设备驱动程序,如块设备和SCSI设备。n /fs子目录包含了所有的文件系统的代码,如:ext2、vfat等。n /net子目录包含了内核的网络相关代码。n /mm子目录包含了所有内存管理代码。n /ipc子目录包含了进程间通信代码。n /kernel子目录包含了内核核心代码。5.1.6制作文件系统读者把上一节中所编译的内核压缩映像下载到开发板后会发现,系统在进行了一些初始化的工作之后,并不能正常启动,如图5.25所示。可以看到,系统启动时发生了加载文件系统的错误。要记住,上一节所编译的仅仅是内核,文件系统和内核是完全独立的两个部分。读者可以回忆一下第2章讲解的Linux启动过程的分析(嵌入式Linux是Linux裁减后的版本,其精髓部分是一样的),其中在head.S中就加载了根文件系统。因此,加载根文件系统是Linux启动中不可缺少的一部分。本节将讲解嵌入式Linux中文件系统的制作方法。图5.25系统启动错误制作文件系统的方法有很多,可以从零开始手工制作,也可以在现有的基础上添加部分内容并加载到目标板上去。由于完全手工制作工作量比较大,而且也很容易出错,因此,本节将主要介绍把现有的文件系统加载到目标板上的方法,主要包括制作文件系统映像和用NFS加载文件系统的方法。1.制作文件系统映像读者已经知道,Linux支持多种文件系统,同样,嵌入式Linux也支持多种文件系统。虽然在嵌入式系统中,由于资源受限的原因,它的文件系统和PC机Linux的文件系统有较大的区别,但是,它们的总体架构是一样的,都是采用目录树的结构。在嵌入式系统中常见的文件系统有cramfs、romfs、jffs、yaffs等,这里就以制作cramfs文件系统为例进行讲解。cramfs文件系统是一种经过压缩的、极为简单的只读文件系统,因此非常适合嵌入式系统。要注意的是,不同的文件系统都有相应的制作工具,但是其主要的原理和制作方法是类似的。在嵌入式Linux中,busybox是构造文件系统最常用的软件工具包,它被非常形象地称为嵌入式Linux系统中的“瑞士军刀”,因为它将许多常用的Linux命令和工具结合到了一个单独的可执行程序(busybox)中。虽然与相应的GNU工具比较起来,busybox所提供的功能和参数略少,但在比较小的系统(例如启动盘)或者嵌入式系统中已经足够了。busybox在设计上就充分考虑了硬件资源受限的特殊工作环境。它采用一种很巧妙的办法减少自己的体积:所有的命令都通过“插件”的方式集中到一个可执行文件中,在实际应用过程中通过不同的符号链接来确定到底要执行哪个操作。例如最终生成的可执行文件为busybox,当为它建立一个符号链接ls的时候,就可以通过执行这个新命令实现列出目录的功能。采用单一执行文件的方式最大限度地共享了程序代码,甚至连文件头、内存中的程序控制块等其他系统资源都共享了,对于资源比较紧张的系统来说,真是最合适不过了。在busybox的编译过程中,可以非常方便地加减它的“插件”,最后的符号链接也可以由编译系统自动生成。下面用busybox构建FS2410开发板的cramfs文件系统。首先从busybox网站下载busybox源码(本实例采用的busybox-1.0.0)并解压,接下来,根据实际需要进行busybox的配置。[root@localhostfs2410]#tarjxvfbusybox-1.00.tar.bz2[root@localhostfs2410]#cdbusybox-1.00[root@localhostbusybox-1.00]#makedefconfig/*首先进行默认配置*/[root@localhostbusybox-1.00]#makemenuconfig此时需要设置平台相关的交叉编译选项,操作步骤为:先选中“BuildOptions”项的“DoyouwanttobuildBusyboxwithaCrossComplier?”选项,然后将“CrossCompilerprefix”设置为“/usr/local/arm/3.3.2/bin/arm-linux-”(这是在实验主机中的交叉编译器的安装路径)。图5.26busybox配置画面下一步编译并安装busybox。[root@localhostbusybox-1.00]#make[root@localhostbusybox-1.00]#makeinstallPREFIX=/home/david/fs2410/cramfs其中,PREFIX用于指定安装目录,如果不设置该选项,则默认在当前目录下创建_install目录。创建的安装目录的内容如下所示:[root@localhostcramfs]#lsbinlinuxrcsbinusr从此可知,使用busybox软件包所创建的文件系统还缺少很多东西。下面我们通过创建系统所需要的目录和文件来完善一下文件系统的内容。[root@localhostcramfs]#mkdirmntrootvartmpprocbootetclib[root@localhostcramfs]#mkdir/var/{lock,log,mail,run,spool}如果busybox是动态编译的(即在配置busybox时没选中静态编译),则把所需的交叉编译的动态链接库文件复制到lib目录中。接下来,需要创建一些重要文件。首先要创建/etc/inittab和/etc/fstab文件。inittab是Linux启动之后第一个被访问的脚本文件,而fstab文件是定义了文件系统的各个“挂接点”,需要与实际的系统相配合。接下来要创建用户和用户组文件。以上用busybox构造了文件系统的内容,下面要创建cramfs文件系统映像文件。制作cramfs映像文件需要用到的工具是mkcramfs。此时可以采用两种方法,一种方法是使用我们所构建的文件系统(在目录“/home/david/fs2410/cramfs”中),另一种方法是在已经做好的cramfs映像文件的基础上进行适当的改动。下面的示例使用第二种方法,因为这个方法包含了第一种方法的所有步骤(假设已经做好的映像文件名为“fs2410.cramfs”)。首先用mount命令将映像文件挂载到一个目录下,打开该目录并查看其内容。[root@localhostfs2410]#mkdircramfs[root@localhostfs2410]#mountfs2410.cramgscramfs–oloop[root@localhostfs2410]#lscramfsbindevetchomeliblinuxrcprocQtopiaramdisksbintestshelltmpusrvar因为cramfs文件系统是只读的,所以不能在这个挂载目录下直接进行修改,因此需要将文件系统中的内容复制到另一个目录中,具体操作如下所示:[root@localhostfs2410]#mkdirbackup_cramfs[root@localhostfs2410]#tarcvfbackup.cramfs.tarcramfs/[root@localhostfs2410]#mvbackup.cramfs.tarbackup_cramfs/[root@localhostfs2410]#umountcramfs[root@localhostfs2410]#cdbackup_cramfs[root@localhostbackup_cramfs]#tarzvfbackup.cramfs.tar[root@localhostbackup_cramfs]#rmbackup.cramfs.tar此时我们就像用busybox所构建的文件系统一样,可以在backup_cramfs的cramfs子目录中任意进行修改。例如可以添加用户自己的程序:[root@localhostfs2410]#cp~/hellobackup_cramfs/cramfs/在用户的修改工作结束之后,用下面的命令可以创建cramfs映像文件:[root@localhostfs2410]#mkcramfsbackup_cramfs/cramfs/new.cramfs接下来,就可以将新创建的new.cramfs映像文件烧入到开发板的相应位置了。2.NFS文件系统NFS为NetworkFileSystem的简称,最早是由Sun公司提出发展起来的,其目的就是让不同的机器、不同的操作系统之间通过网络可以彼此共享文件。NFS可以让不同的主机通过网络将远端的NFS服务器共享出来的文件安装到自己的系统中,从客户端看来,使用NFS的远端文件就像是使用本地文件一样。在嵌入式中使用NFS会使应用程序的开发变得十分方便,并且不用反复地烧写映像文件。NFS的使用分为服务端和客户端,其中服务端是提供要共享的文件,而客户端则通过挂载(“mount”)这一动作来实现对共享文件的访问操作。下面主要介绍NFS服务端的使用。在嵌入式开发中,通常NFS服务端在宿主机上运行,而客户端在目标板上运行。NFS服务端是通过读入它的配置文件“/etc/exports”来决定所共享的文件目录的。下面首先讲解这个配置文件的书写规范。在这个配置文件中,每一行都代表一项要共享的文件目录以及所指定的客户端对它的操作权限。客户端可以根据相应的权限,对该目录下的所有目录文件进行访问。配置文件中每一行的格式如下:[共享的目录][客户端主机名称或IP][参数1,参数2…]在这里,主机名或IP是可供共享的客户端主机名或IP,若对所有的IP都可以访问,则可用“*”表示。这里的参数有很多种组合方式,常见的参数如表5.1所示。表5.1 常见参数选项参数含义rw可读写的权限ro只读的权限no_root_squashNFS客户端分享目录使用者的权限,即如果客户端使用的是root用户,那么对于这个共享的目录而言,该客户端就具有root的权限sync资料同步写入到内存与硬盘当中async资料会先暂存于内存当中,而非直接写入硬盘如在本例中,配置文件“/etc/exports”的代码如下:[root@localhostfs]#cat/etc/exports/root/workplace192.168.1.*(rw,no_root_squash)在设定完配置文件之后,需要启动nfs服务和portmap服务,这里的portmap服务是允许NFS客户端查看NFS服务在用的端口,在它被激活之后,就会出现一个端口号为111的sunRPC(远端过程调用)的服务。这是NFS服务中必须实现的一项,因此,也必须把它开启。如下所示:[root@localhostfs]#serviceportmapstart启动portmap:[确定][root@localhostfs]#servicenfsstart启动NFS服务:[确定]关掉NFS配额:[确定]启动NFS守护进程:[确定]启动NFSmountd:[确定]可以看到,在启动NFS服务的时候启动了mountd进程。这是NFS挂载服务,用于处理NFS递交过来的客户端请求。另外还会激活至少两个以上的系统守护进程,然后就开始监听客户端的请求,用“cat/var/log/messages”命令可以查看操作是否成功。这样,就启动了NFS的服务,另外还有两个命令,可以便于使用NFS。其中一个是exportfs,它可以重新扫描“/etc/exports”,使用户在修改了“/etc/exports”配置文件之后不需要每次重启NFS服务。其格式为:exportfs[选项]exportfs的常见选项如表5.2所示。表5.2 常见选项选项参数含义-a全部挂载(或卸载)/etc/exports中的设定文件目录-r重新挂载/etc/exports中的设定文件目录-u卸载某一目录-v在export的时候,将共享的目录显示到屏幕上另外一个是showmount命令,它用于当前的挂载情况。其格式为:showmount[选项]hostnameshowmount的常见选项如表5.3所示。表5.3 常见选项选项参数含义-a在屏幕上显示目前主机与客户端所连上来的使用目录状态-e显示hostname中/etc/exports里设定的共享目录

    时间:2018-06-15 关键词: 基础教程 嵌入式linux 交叉编译 超级终端 minicom

  • 嵌入式Linux开发环境的搭建之:U-Boot移植

    嵌入式Linux开发环境的搭建之:U-Boot移植

    5.2U-Boot移植5.2.1Bootloader介绍1.概念简单地说,Bootloader就是在操作系统内核运行之前运行的一段程序,它类似于PC机中的BIOS程序。通过这段程序,可以完成硬件设备的初始化,并建立内存空间的映射关系,从而将系统的软硬件环境带到一个合适的状态,为最终加载系统内核做好准备。通常,Bootloader比较依赖于硬件平台,特别是在嵌入式系统中,更为如此。因此,在嵌入式世界里建立一个通用的Bootloader是一件比较困难的事情。尽管如此,仍然可以对Bootloader归纳出一些通用的概念来指导面向用户定制的Bootloader设计与实现。(1)Bootloader所支持的CPU和嵌入式开发板。每种不同的CPU体系结构都有不同的Bootloader。有些Bootloader也支持多种体系结构的CPU,如后面要介绍的U-Boot支持ARM、MIPS、PowerPC等众多体系结构。除了依赖于CPU的体系结构外,Bootloader实际上也依赖于具体的嵌入式板级设备的配置。(2)Bootloader的存储位置。系统加电或复位后,所有的CPU通常都从某个由CPU制造商预先安排的地址上取指令。而基于CPU构建的嵌入式系统通常都有某种类型的固态存储设备(比如ROM、EEPROM或Flash等)被映射到这个预先安排的地址上。因此在系统加电后,CPU将首先执行Bootloader程序。(3)Bootloader的启动过程分为单阶段和多阶段两种。通常多阶段的Bootloader能提供更为复杂的功能,以及更好的可移植性。(4)Bootloader的操作模式。大多数Bootloader都包含两种不同的操作模式:“启动加载”模式和“下载”模式,这种区别仅对于开发人员才有意义。n 启动加载模式:这种模式也称为“自主”模式。也就是Bootloader从目标机上的某个固态存储设备上将操作系统加载到RAM中运行,整个过程并没有用户的介入。这种模式是嵌入式产品发布时的通用模式。n 下载模式:在这种模式下,目标机上的Bootloader将通过串口连接或网络连接等通信手段从主机(Host)下载文件,比如:下载内核映像和根文件系统映像等。从主机下载的文件通常首先被Bootloader保存到目标机的RAM中,然后再被Bootloader写入到目标机上的Flash类固态存储设备中。Bootloader的这种模式在系统更新时使用。工作于这种模式下的Bootloader通常都会向它的终端用户提供一个简单的命令行接口。(5)Bootloader与主机之间进行文件传输所用的通信设备及协议,最常见的情况就是,目标机上的Bootloader通过串口与主机之间进行文件传输,传输协议通常是xmodem/ymodem/zmodem等。但是,串口传输的速度是有限的,因此通过以太网连接并借助TFTP等协议来下载文件是个更好的选择。2.Bootloader启动流程Bootloader的启动流程一般分为两个阶段:stage1和stage2,下面分别对这两个阶段进行讲解。(1)Bootloader的stage1。在stage1中Bootloader主要完成以下工作。n 基本的硬件初始化,包括屏蔽所有的中断、设置CPU的速度和时钟频率、RAM初始化、初始化外围设备、关闭CPU内部指令和数据cache等。n 为加载stage2准备RAM空间,通常为了获得更快的执行速度,通常把stage2加载到RAM空间中来执行,因此必须为加载Bootloader的stage2准备好一段可用的RAM空间。n 复制stage2到RAM中,在这里要确定两点:①stage2的可执行映像在固态存储设备的存放起始地址和终止地址;②RAM空间的起始地址。n 设置堆栈指针sp,这是为执行stage2的C语言代码做好准备。(2)Bootloader的stage2。在stage2中Bootloader主要完成以下工作。n 用汇编语言跳转到main入口函数。由于stage2的代码通常用C语言来实现,目的是实现更复杂的功能和取得更好的代码可读性和可移植性。但是与普通C语言应用程序不同的是,在编译和链接Bootloader这样的程序时,不能使用glibc库中的任何支持函数。n 初始化本阶段要使用到的硬件设备,包括初始化串口、初始化计时器等。在初始化这些设备之前可以输出一些打印信息。n 检测系统的内存映射,所谓内存映射就是指在整个4GB物理地址空间中指出哪些地址范围被分配用来寻址系统的内存。n 加载内核映像和根文件系统映像,这里包括规划内存占用的布局和从Flash上复制数据。n 设置内核的启动参数。5.2.2U-Boot概述1.U-Boot简介U-Boot(UniversalBootloader)是遵循GPL条款的开放源码项目。它是从FADSROM、8xxROM、PPCBOOT逐步发展演化而来。其源码目录、编译形式与Linux内核很相似,事实上,不少U-Boot源码就是相应的Linux内核源程序的简化,尤其是一些设备的驱动程序,这从U-Boot源码的注释中能体现这一点。但是U-Boot不仅仅支持嵌入式Linux系统的引导,而且还支持NetBSD、VxWorks、QNX、RTEMS、ARTOS、LynxOS等嵌入式操作系统。其目前要支持的目标操作系统是OpenBSD、NetBSD、FreeBSD、4.4BSD、Linux、SVR4、Esix、Solaris、Irix、SCO、Dell、NCR、VxWorks、LynxOS、pSOS、QNX、RTEMS、ARTOS。这是U-Boot中Universal的一层含义,另外一层含义则是U-Boot除了支持PowerPC系列的处理器外,还能支持MIPS、x86、ARM、NIOS、XScale等诸多常用系列的处理器。这两个特点正是U-Boot项目的开发目标,即支持尽可能多的嵌入式处理器和嵌入式操作系统。就目前为止,U-Boot对PowerPC系列处理器支持最为丰富,对Linux的支持最完善。2.U-Boot特点U-Boot的特点如下。n 开放源码;n 支持多种嵌入式操作系统内核,如Linux、NetBSD、VxWorks、QNX、RTEMS、ARTOS、LynxOS;n 支持多个处理器系列,如PowerPC、ARM、x86、MIPS、XScale;n 较高的可靠性和稳定性;n 高度灵活的功能设置,适合U-Boot调试、操作系统不同引导要求和产品发布等;n 丰富的设备驱动源码,如串口、以太网、SDRAM、Flash、LCD、NVRAM、EEPROM、RTC、键盘等;n 较为丰富的开发调试文档与强大的网络技术支持。3.U-Boot主要功能U-Boot可支持的主要功能列表。n 系统引导:支持NFS挂载、RAMDISK(压缩或非压缩)形式的根文件系统。支持NFS挂载,并从Flash中引导压缩或非压缩系统内核。n 基本辅助功能:强大的操作系统接口功能;可灵活设置、传递多个关键参数给操作系统,适合系统在不同开发阶段的调试要求与产品发布,尤其对Linux支持最为强劲;支持目标板环境参数多种存储方式,如Flash、NVRAM、EEPROM;CRC32校验,可校验Flash中内核、RAMDISK映像文件是否完好。n 设备驱动:串口、SDRAM、Flash、以太网、LCD、NVRAM、EEPROM、键盘、USB、PCMCIA、PCI、RTC等驱动支持。n 上电自检功能:SDRAM、Flash大小自动检测;SDRAM故障检测;CPU型号。n 特殊功能:XIP内核引导。5.2.3U-Boot源码导读1.U-Boot源码结构U-Boot源码结构如图5.27所示。图5.27U-Boot源码结构n board:和一些已有开发板有关的代码,比如makefile和U-Boot.lds等都和具体开发板的硬件和地址分配有关。n common:与体系结构无关的代码,用来实现各种命令的C程序。n cpu:包含CPU相关代码,其中的子目录都是以U-BOOT所支持的CPU为名,比如有子目录arm926ejs、mips、mpc8260和nios等,每个特定的子目录中都包括cpu.c和interrupt.c,start.S等。其中cpu.c初始化CPU、设置指令Cache和数据Cache等;interrupt.c设置系统的各种中断和异常,比如快速中断、开关中断、时钟中断、软件中断、预取中止和未定义指令等;汇编代码文件start.S是U-BOOT启动时执行的第一个文件,它主要是设置系统堆栈和工作方式,为进入C程序奠定基础。n disk:disk驱动的分区相关代码。n doc:文档。n drivers:通用设备驱动程序,比如各种网卡、支持CFI的Flash、串口和USB总线等。n fs:支持文件系统的文件,U-BOOT现在支持cramfs、fat、fdos、jffs2和registerfs等。n include:头文件,还有对各种硬件平台支持的汇编文件,系统的配置文件和对文件系统支持的文件。n net:与网络有关的代码,BOOTP协议、TFTP协议、RARP协议和NFS文件系统的实现。n lib_arm:与ARM体系结构相关的代码。n tools:创建S-Record格式文件和U-BOOTimages的工具。2.U-Boot重要代码(1)cpu/arm920t/start.S这是U-Boot的起始位置。在这个文件中设置了处理器的状态、初始化中断向量和内存时序等,从Flash中跳转到定位好的内存位置执行。.globl_start(起始位置:中断向量设置)_start:bresetldrpc,_undefined_instructionldrpc,_software_interruptldrpc,_prefetch_abortldrpc,_data_abortldrpc,_not_usedldrpc,_irqldrpc,_fiq_undefined_instruction:.wordundefined_instruction_software_interrupt:.wordsoftware_interrupt_prefetch_abort:.wordprefetch_abort_data_abort:.worddata_abort_not_used:.wordnot_used_irq:.wordirq_fiq:.wordfiq_TEXT_BASE:(代码段起始位置).wordTEXT_BASE.globl_armboot_start_armboot_start:.word_start/**Thesearedefinedintheboard-specificlinkerscript.*/.globl_bss_start(BSS段起始位置)_bss_start:.word__bss_start.globl_bss_end_bss_end:.word_endreset:(执行入口)/**setthecputoSVC32mode;使处理器进入特权模式*/mrsr0,cpsrbicr0,r0,#0x1forrr0,r0,#0xd3msrcpsr,r0relocate:(代码的重置)/*relocateU-BoottoRAM*/adrr0,_start/*r0<-currentpositionofcode*/ldrr1,_TEXT_BASE/*testifwerunfromflashorRAM*/cmpr0,r1/*don'trelocduringdebug*/beqstack_setupldrr2,_armboot_startldrr3,_bss_startsubr2,r3,r2/*r2<-sizeofarmboot*/addr2,r0,r2/*r2<-sourceendaddress*/copy_loop:(拷贝过程)ldmiar0!,{r3-r10}/*copyfromsourceaddress[r0]*/stmiar1!,{r3-r10}/*copytotargetaddress[r1]*/cmpr0,r2/*untilsourceendaddreee[r2]*/blecopy_loop/*Setupthestack;设置堆栈*/stack_setup:ldrr0,_TEXT_BASE/*upper128KiB:relocateduboot*/subr0,r0,#CFG_MALLOC_LEN/*mallocarea*/subr0,r0,#CFG_GBL_DATA_SIZE/*bdinfo*/clear_bss:(清空BSS段)ldrr0,_bss_start/*findstartofbsssegment*/ldrr1,_bss_end/*stophere*/movr2,#0x00000000/*clear*/clbss_l:strr2,[r0]/*clearloop...*/addr0,r0,#4cmpr0,r1bneclbss_lldrpc,_start_armboot_start_armboot:.wordstart_armboot(2)interrupts.c这个文件是处理中断的,如打开和关闭中断等。#ifdefCONFIG_USE_IRQ/*enableIRQinterrupts;中断使能函数*/voidenable_interrupts(void){unsignedlongtemp;__asm____volatile__("mrs%0,cpsrn""bic%0,%0,#0x80n""msrcpsr_c,%0":"=r"(temp)::"memory");}/**disableIRQ/FIQinterrupts;中断屏蔽函数*returnstrueifinterruptshadbeenenabledbeforewedisabledthem*/intdisable_interrupts(void){unsignedlongold,temp;__asm____volatile__("mrs%0,cpsrn""orr%1,%0,#0xc0n""msrcpsr_c,%1":"=r"(old),"=r"(temp)::"memory");return(old&0x80)==0;}#endifvoidshow_regs(structpt_regs*regs){unsignedlongflags;constchar*processor_modes[]={"USER_26","FIQ_26","IRQ_26","SVC_26","UK4_26","UK5_26","UK6_26","UK7_26","UK8_26","UK9_26","UK10_26","UK11_26","UK12_26","UK13_26","UK14_26","UK15_26","USER_32","FIQ_32","IRQ_32","SVC_32","UK4_32","UK5_32","UK6_32","ABT_32","UK8_32","UK9_32","UK10_32","UND_32","UK12_32","UK13_32","UK14_32","SYS_32",};…}/*在U-Boot启动模式下,在原则上要禁止中断处理,所以如果发生中断,当作出错处理*/voiddo_fiq(structpt_regs*pt_regs){printf("fastinterruptrequestn");show_regs(pt_regs);bad_mode();}voiddo_irq(structpt_regs*pt_regs){printf("interruptrequestn");show_regs(pt_regs);bad_mode();}(3)cpu.c这个文件是对处理器进行操作,如下所示:intcpu_init(void){/**setupupstacksifnecessary;设置需要的堆栈*/#ifdefCONFIG_USE_IRQDECLARE_GLOBAL_DATA_PTR;IRQ_STACK_START=_armboot_start-CFG_MALLOC_LEN-CFG_GBL_DATA_SIZE-4;FIQ_STACK_START=IRQ_STACK_START-CONFIG_STACKSIZE_IRQ;#endifreturn0;}intcleanup_before_linux(void)/*准备加载linux*/{/**thisfunctioniscalledjustbeforewecalllinux*itpreparestheprocessorforlinux**weturnoffcachesetc...*/unsignedlongi;disable_interrupts();/*turnoffI/D-cache:关闭cache*/asm("mrcp15,0,%0,c1,c0,0":"=r"(i));i&=~(C1_DC|C1_IC);asm("mcrp15,0,%0,c1,c0,0"::"r"(i));/*flushI/D-cache*/i=0;asm("mcrp15,0,%0,c7,c7,0"::"r"(i));return(0);}OUTPUT_ARCH(arm)ENTRY(_start)SECTIONS{.=0x00000000;.=ALIGN(4);.text:{cpu/arm920t/start.o(.text)*(.text)}.=ALIGN(4);.rodata:{*(.rodata)}.=ALIGN(4);.data:{*(.data)}.=ALIGN(4);.got:{*(.got)}__u_boot_cmd_start=.;.u_boot_cmd:{*(.u_boot_cmd)}__u_boot_cmd_end=.;.=ALIGN(4);__bss_start=.;.bss:{*(.bss)}_end=.;}(4)memsetup.S这个文件是用于配置开发板参数的,如下所示:/*memsetup.c*//*memorycontrolconfiguration*//*maker0relativethecurrentlocationsothatit*//*readsSMRDATAoutofFLASHratherthanmemory!*/ldrr0,=SMRDATAldrr1,_TEXT_BASEsubr0,r0,r1ldrr1,=BWSCON /*BusWidthStatusController*/addr2,r0,#520:ldrr3,[r0],#4strr3,[r1],#4cmpr2,r0bne0b/*everythingisfinenow*/movpc,lr.ltorg5.2.4U-Boot移植主要步骤(1)建立自己的开发板类型。阅读makefile文件,在makefile文件中添加两行,如下所示:fs2410_config:unconfig@./mkconfig$(@:_config=)armarm920tfs2410其中“arm”为表示处理器体系结构的种类,“arm920t”表示处理器体系结构的名称,“fs2410”为主板名称。在board目录中建立fs2410目录,并将smdk2410目录中的内容(cp–asmdk2410/*fs2410)复制到该目录中。n 在include/configs/目录下将smdk2410.h复制到(cpsmdk2410.hfs2410.h)。n 修改ARM编译器的目录名及前缀(都要改成以“fs2410”开头)。n 完成之后,可以测试配置。$makefs2410_config;make(2)修改程序链接地址。在board/s3c2410中有一个config.mk文件,它是用于设置程序链接的起始地址,因为会在U-Boot中增加功能,所以留下6MB的空间,修改33F80000为33A00000。为了以后能用U-Boot的“go”命令执行修改过的用loadb或tftp下载的U-Boot,需要在board/s3c2410的memsetup.S中标记符”0:”上加入5句:movr3,pcldrr4,=0x3FFF0000andr3,r3,r4(以上3句得到实际代码启动的内存地址)aadr0,r0,r3(用go命令调试u-boot时,启动地址在RAM)addr2,r2,r3(把初始化内存信息的地址,加上实际启动地址)(3)将中断禁止的部分应该改为如下所示(/cpu/arm920t/start.S):#ifdefined(CONFIG_S3C2410)ldrr1,=0x7ffldrr0,=INTSUBMSKstrr1,[r0]#endif(4)因为在fs2410开发板启动时是直接从NandFlash加载代码,所以启动代码应该改成如下所示(/cpu/arm920t/start.S):#ifdefCONFIG_S3C2410_NAND_BOOT@START@resetNANDmovr1,#NAND_CTL_BASEldrr2,=0xf830@initialvaluestrr2,[r1,#oNFCONF]ldrr2,[r1,#oNFCONF]bicr2,r2,#0x800@enablechipstrr2,[r1,#oNFCONF]movr2,#0xff@RESETcommandstrbr2,[r1,#oNFCMD]movr3,#0@waitnand1:addr3,r3,#0x1cmpr3,#0xabltnand1nand2:ldrr2,[r1,#oNFSTAT]@waitreadytstr2,#0x1beqnand2ldrr2,[r1,#oNFCONF]orrr2,r2,#0x800@disablechipstrr2,[r1,#oNFCONF]@getreadtocallCfunctions(fornand_read())ldrsp,DW_STACK_START@setupstackpointermovfp,#0@nopreviousframe,sofp=0@copyU-BoottoRAMldrr0,=TEXT_BASEmovr1,#0x0movr2,#0x20000blnand_read_lltstr0,#0x0beqok_nand_readbad_nand_read:loop2:bloop2@infiniteloopok_nand_read:@verifymovr0,#0ldrr1,=TEXT_BASEmovr2,#0x400@4bytes*1024=4K-bytesgo_next:ldrr3,[r0],#4ldrr4,[r1],#4teqr3,r4bnenotmatchsubsr2,r2,#4beqstack_setupbnego_nextnotmatch:loop3:bloop3@infiniteloop#endif@CONFIG_S3C2410_NAND_BOOT@END在“_start_armboot:.wordstart_armboot”后加入:.align2DW_STACK_START:.wordSTACK_BASE+STACK_SIZE-4(5)修改内存配置(board/fs2410/lowlevel_init.S)。#defineBWSCON0x48000000#definePLD_BASE0x2C000000#defineSDRAM_REG0x2C000106/*BWSCON*/#defineDW8 (0x0)#defineDW16(0x1)#defineDW32(0x2)#defineWAIT(0x1<<2)#defineUBLB(0x1<<3)/*BANKSIZE*/#defineBURST_EN(0x1<<7)#defineB1_BWSCON(DW16+WAIT)#defineB2_BWSCON(DW32)#defineB3_BWSCON(DW32)#defineB4_BWSCON(DW16+WAIT+UBLB)#defineB5_BWSCON(DW8+UBLB)#defineB6_BWSCON(DW32)#defineB7_BWSCON(DW32)/*BANK0CON*/#defineB0_Tacs0x0/*0clk*/#defineB0_Tcos0x1/*1clk*/#defineB0_Tacc0x7/*14clk*/#defineB0_Tcoh0x0/*0clk*/#defineB0_Tah0x0/*0clk*/#defineB0_Tacp0x0/*pagemodeisnotused*/#defineB0_PMC0x0/*pagemodedisabled*//*BANK1CON*/#defineB1_Tacs0x0/*0clk*/#defineB1_Tcos0x1/*1clk*/#defineB1_Tacc0x7/*14clk*/#defineB1_Tcoh0x0/*0clk*/#defineB1_Tah0x0/*0clk*/#defineB1_Tacp0x0/*pagemodeisnotused*/#defineB1_PMC0x0/*pagemodedisabled*/……/*REFRESHparameter*/#defineREFEN0x1/*Refreshenable*/#defineTREFMD0x0/*CBR(CASbeforeRAS)/Autorefresh*/#defineTrp0x0/*2clk*/#defineTrc0x3/*7clk*/#defineTchr0x2/*3clk*/#defineREFCNT1113/*period=15.6us,HCLK=60Mhz,(2048+1-15.6*60)*/.......word((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN)).word((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN)).word((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT).word0x32.word0x30.word0x30(6)加入NandFlash读函数(创建board/fs2410/nand_read.c文件)。#include<config.h>#define__REGb(x)(*(volatileunsignedchar*)(x))#define__REGi(x)(*(volatileunsignedint*)(x))#defineNF_BASE0x4e000000#defineNFCONF__REGi(NF_BASE+0x0)#defineNFCMD__REGb(NF_BASE+0x4)#defineNFADDR__REGb(NF_BASE+0x8)#defineNFDATA__REGb(NF_BASE+0xc)#defineNFSTAT__REGb(NF_BASE+0x10)#defineBUSY1inlinevoidwait_idle(void){Inti;while(!(NFSTAT&BUSY)){for(i=0;i<10;i++);}}/*lowlevelnandreadfunction*/intnand_read_ll(unsignedchar*buf,unsignedlongstart_addr,intsize){inti,j;if((start_addr&NAND_BLOCK_MASK)||(size&NAND_BLOCK_MASK)){return-1;/*invalidalignment*/}/*chipEnable*/NFCONF&=~0x800;for(i=0;i<10;i++);for(i=start_addr;i<(start_addr+size);){/*READ0*/NFCMD=0;/*WriteAddress*/NFADDR=i&0xff;NFADDR=(i>>9)&0xff;NFADDR=(i>>17)&0xff;NFADDR=(i>>25)&0xff;wait_idle();for(j=0;j<NAND_SECTOR_SIZE;j++,i++){*buf=(NFDATA&0xff);buf++;}}/*chipDisable*/NFCONF|=0x800;/*chipdisable*/return0;}修改board/fs2410/makefile文件,以增加nand_read()函数。OBJS:=fs2410.oflash.onand_read.o(7)加入NandFlash的初始化函数(board/fs2410/fs2410.c)。#if(CONFIG_COMMANDS&CFG_CMD_NAND)typedefenum{NFCE_LOW,NFCE_HIGH}NFCE_STATE;staticinlinevoidNF_Conf(u16conf){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();nand->NFCONF=conf;}staticinlinevoidNF_Cmd(u8cmd){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();nand->NFCMD=cmd;}staticinlinevoidNF_CmdW(u8cmd){NF_Cmd(cmd);udelay(1);}staticinlinevoidNF_Addr(u8addr){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();nand->NFADDR=addr;}staticinlinevoidNF_SetCE(NFCE_STATEs){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();switch(s){caseNFCE_LOW:nand->NFCONF&=~(1<<11);break;caseNFCE_HIGH:nand->NFCONF|=(1<<11);break;}}staticinlinevoidNF_WaitRB(void){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();while(!(nand->NFSTAT&(1<<0)));}staticinlinevoidNF_Write(u8data){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();nand->NFDATA=data;}staticinlineu8NF_Read(void){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();return(nand->NFDATA);}staticinlinevoidNF_Init_ECC(void){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();nand->NFCONF|=(1<<12);}staticinlineu32NF_Read_ECC(void){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();return(nand->NFECC);}#endif/**NANDflashinitialization.*/#if(CONFIG_COMMANDS&CFG_CMD_NAND)externulongnand_probe(ulongphysadr);staticinlinevoidNF_Reset(void){inti;NF_SetCE(NFCE_LOW);NF_Cmd(0xFF);/*resetcommand*/for(i=0;i<10;i++);/*tWB=100ns.*/NF_WaitRB();/*wait200~500us;*/NF_SetCE(NFCE_HIGH);}staticinlinevoidNF_Init(void){#defineTACLS0#defineTWRPH04#defineTWRPH12NF_Conf((1<<15)|(0<<14)|(0<<13)|(1<<12)|(1<<11)|(TACLS<<8)|(TWRPH0<<4)|(TWRPH1<<0));/*1111,1xxx,rxxx,rxxx*//*En512B4stepECCRnFCE=HtACLStWRPH0tWRPH1*/NF_Reset();}voidnand_init(void){S3C2410_NAND*constnand=S3C2410_GetBase_NAND();NF_Init();#ifdefDEBUGprintf("NANDflashprobingat0x%.8lXn",(ulong)nand);#endifprintf("%4luMBn",nand_probe((ulong)nand)>>20);}#endif(8)修改GPIO配置(board/fs2410/fs2410.c)。/*setuptheI/Oports*/gpio->GPACON=0x007FFFFF;gpio->GPBCON=0x002AAAAA;gpio->GPBUP=0x000002BF;gpio->GPCCON=0xAAAAAAAA;gpio->GPCUP=0x0000FFFF;gpio->GPDCON=0xAAAAAAAA;gpio->GPDUP=0x0000FFFF;gpio->GPECON=0xAAAAAAAA;gpio->GPEUP=0x000037F7;gpio->GPFCON=0x00000000;gpio->GPFUP=0x00000000;gpio->GPGCON=0xFFEAFF5A;gpio->GPGUP=0x0000F0DC;gpio->GPHCON=0x0018AAAA;gpio->GPHDAT=0x000001FF;gpio->GPHUP=0x00000656(9)提供nandflash相关宏定义(include/configs/fs2410.h),具体参考源码。(10)加入NandFlash设备(include/linux/mtd/nand_ids.h)staticstructnand_flash_devnand_flash_ids[]={......{"SamsungKM29N16000",NAND_MFR_SAMSUNG,0x64,21,1,2,0x1000,0},{"SamsungK9F1208U0M",NAND_MFR_SAMSUNG,0x76,26,0,3,0x4000,0},{"Samsungunknown4Mb",NAND_MFR_SAMSUNG,0x6b,22,0,2,0x2000,0},......{NULL,}};(11)设置NandFlash环境(common/env_nand.c)intnand_legacy_rw(structnand_chip*nand,intcmd,size_tstart,size_tlen,size_t*retlen,u_char*buf);externstructnand_chipnand_dev_desc[CFG_MAX_NAND_DEVICE];externintnand_legacy_erase(structnand_chip*nand,size_tofs,size_tlen,intclean);/*infoforNANDchips,definedindrivers/nand/nand.c*/externnand_info_tnand_info[CFG_MAX_NAND_DEVICE];......#else/*!CFG_ENV_OFFSET_REDUND*/intsaveenv(void){ulongtotal;intret=0;puts("ErasingNand...");if(nand_legacy_erase(nand_dev_desc+0,CFG_ENV_OFFSET,CFG_ENV_SIZE,0)){return1;}puts("WritingtoNand...");total=CFG_ENV_SIZE;ret=nand_legacy_rw(nand_dev_desc+0,0x00|0x02,CFG_ENV_OFFSET,CFG_ENV_SIZE,&total,(u_char*)env_ptr);if(ret||total!=CFG_ENV_SIZE){return1;}puts("donen");returnret;......#else/*!CFG_ENV_OFFSET_REDUND*/voidenv_relocate_spec(void){#if!defined(ENV_IS_EMBEDDED)ulongtotal;intret;total=CFG_ENV_SIZE;ret=nand_legacy_rw(nand_dev_desc+0,0x01|0x02,CFG_ENV_OFFSET,CFG_ENV_SIZE,&total,(u_char*)env_ptr);

    时间:2018-06-15 关键词: 基础教程 bootloader 嵌入式linux u-boot移植

  • 嵌入式Linux网络编程之:TCP/IP协议概述

    嵌入式Linux网络编程之:TCP/IP协议概述

    10.1TCP/IP协议概述10.1.1OSI参考模型及TCP/IP参考模型读者一定都听说过著名的OSI协议参考模型,它是基于国际标准化组织(ISO)的建议发展起来的,从上到下共分为7层:应用层、表示层、会话层、传输层、网络层、数据链路层及物理层。这个7层的协议模型虽然规定得非常细致和完善,但在实际中却得不到广泛的应用,其重要的原因之一就在于它过于复杂。但它仍是此后很多协议模型的基础,这种分层架构的思想在很多领域都得到了广泛的应用。与此相区别的TCP/IP协议模型从一开始就遵循简单明确的设计思路,它将TCP/IP的7层协议模型简化为4层,从而更有利于实现和使用。TCP/IP的协议参考模型和OSI协议参考模型的对应关系如图10.1所示。图10.1OSI模型和TCP/IP参考模型对应关系下面分别对TCP/IP的4层模型进行简要介绍。n 网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络信息传输单元。n 网络层:负责将数据帧封装成IP数据包,并运行必要的路由算法。n 传输层:负责端对端之间的通信会话连接与建立。传输协议的选择根据数据传输方式而定。n 应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程。10.1.2TCP/IP协议族虽然TCP/IP名称只包含了两个协议,但实际上,TCP/IP是一个庞大的协议族,它包括了各个层次上的众多协议,图10.2列举了各层中一些重要的协议,并给出了各个协议在不同层次中所处的位置,如下所示。n ARP:用于获得同一物理网络中的硬件主机地址。n MPLS:多协议标签协议,是很有发展前景的下一代网络协议。n IP:负责在主机和网络之间寻址和路由数据包。n ICMP:用于发送有关数据包的传送错误的协议。n IGMP:被IP主机用来向本地多路广播路由器报告主机组成员的协议。n TCP:为应用程序提供可靠的通信连接。适合于一次传输大批数据的情况。并适用于要求得到响应的应用程序。n UDP:提供了无连接通信,且不对传送包进行可靠性保证。适合于一次传输少量数据,可靠性则由应用层来负责。10.1.3TCP和UDP在此主要介绍在网络编程中涉及的传输层TCP和UDP协议。1.TCP(1)概述。同其他任何协议栈一样,TCP向相邻的高层提供服务。因为TCP的上一层就是应用层,因此,TCP数据传输实现了从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用TCP并使用TCP服务,提供需要准备发送的数据,用来区分接收数据应用的目的地址和端口号。通常应用程序通过打开一个socket来使用TCP服务,TCP管理到其他socket的数据传递。可以说,通过IP的源/目的可以惟一地区分网络中两个设备的连接,通过socket的源/目的可以惟一地区分网络中两个应用程序的连接。(2)三次握手协议。TCP对话通过三次握手来进行初始化。三次握手的目的是使数据段的发送和接收同步,告诉其他主机其一次可接收的数据量,并建立虚连接。下面描述了这三次握手的简单过程。n 初始化主机通过一个同步标志置位的数据段发出会话请求。n 接收主机通过发回具有以下项目的数据段表示回复:同步标志置位、即将发送的数据段的起始字节的顺序号、应答并带有将收到的下一个数据段的字节顺序号。n 请求主机再回送一个数据段,并带有确认顺序号和确认号。图10.3就是这个流程的简单示意图。图10.3TCP三次握手协议TCP实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报到达目的地后,接收方的TCP实体往回发送一个数据报,其中包含有一个确认序号,它表示希望收到的下一个数据包的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据包。(3)TCP数据包头。图10.4给出了TCP数据包头的格式。TCP数据包头的含义如下所示。n 源端口、目的端口:16位长。标识出远端和本地的端口号。图10.4TCP数据包头的格式n 序号:32位长。标识发送的数据报的顺序。n 确认号:32位长。希望收到的下一个数据包的序列号。n TCP头长:4位长。表明TCP头中包含多少个32位字。n 6位未用。n ACK:ACK位置1表明确认号是合法的。如果ACK为0,那么数据报不包含确认信息,确认字段被省略。n PSH:表示是带有PUSH标志的数据。接收方因此请求数据包一到便将其送往应用程序而不必等到缓冲区装满时才传送。n RST:用于复位由于主机崩溃或其他原因而出现的错误连接。还可以用于拒绝非法的数据包或拒绝连接请求。n SYN:用于建立连接。n FIN:用于释放连接。n 窗口大小:16位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。n 校验和:16位长。是为了确保高可靠性而设置的。它校验头部、数据和伪TCP头部之和。n 可选项:0个或多个32位字。包括最大TCP载荷,滑动窗口比例以及选择重发数据包等选项。2.UDP(1)概述。UDP即用户数据报协议,它是一种无连接协议,因此不需要像TCP那样通过三次握手来建立一个连接。同时,一个UDP应用可同时作为应用的客户或服务器方。由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。UDP协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是在网络质量越来越高的今天,UDP的应用得到了大大的增强。它比TCP协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。(2)UDP数据报头。UDP数据报头如下图10.5所示。n 源地址、目的地址:16位长。标识出远端和本地的端口号。n 数据报的长度是指包括报头和数据部分在内的总的字节数。因为报头的长度是固定的,所以该域主要用来计算可变长度的数据部分(又称为数据负载)。3.协议的选择协议的选择应该考虑到以下3个方面。(1)对数据可靠性的要求。对数据要求高可靠性的应用需选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用可选择UDP传送。(2)应用的实时性。TCP协议在传送过程中要使用三次握手、重传确认等手段来保证数据传输的可靠性。使用TCP协议会有较大的时延,因此不适合对实时性要求较高的应用,如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用。(3)网络的可靠性。由于TCP协议的提出主要是解决网络的可靠性问题,它通过各种机制来减少错误发生的概率。因此,在网络状况不是很好的情况下需选用TCP协议(如在广域网等情况),但是若在网络状况很好的情况下(如局域网等)就不需要再采用TCP协议,而建议选择UDP协议来减少网络负荷。

    时间:2018-06-15 关键词: 操作系统 基础教程 网络编程 tcp/ip协议 嵌入式linux udp协议

  • 嵌入式Linux网络编程之:网络基础编程

    嵌入式Linux网络编程之:网络基础编程

    10.2网络基础编程10.2.1socket概述1.socket定义在Linux中的网络编程是通过socket接口来进行的。人们常说的socket是一种特殊的I/O接口,它也是一种文件描述符。socket是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信。每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。2.socket类型常见的socket有3种类型如下。(1)流式socket(SOCK_STREAM)。流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。(2)数据报socket(SOCK_DGRAM)。数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP。(3)原始socket。原始套接字允许对底层协议如IP或ICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。10.2.2地址及顺序处理1.地址结构相关处理(1)数据结构介绍。下面首先介绍两个重要的数据类型:sockaddr和sockaddr_in,这两个结构类型都是用来保存socket信息的,如下所示:structsockaddr{unsignedshortsa_family;/*地址族*/charsa_data[14];/*14字节的协议地址,包含该socket的IP地址和端口号。*/};structsockaddr_in{shortintsa_family;/*地址族*/unsignedshortintsin_port;/*端口号*/structin_addrsin_addr;/*IP地址*/unsignedcharsin_zero[8];/*填充0以保持与structsockaddr同样大小*/};这两个数据类型是等效的,可以相互转化,通常sockaddr_in数据类型使用更为方便。在建立socketadd或sockaddr_in后,就可以对该socket进行适当的操作了。(2)结构字段。表10.1列出了该结构sa_family字段可选的常见值。表10.1结构定义头文件#include<netinet/in.h>sa_familyAF_INET:IPv4协议AF_INET6:IPv6协议AF_LOCAL:UNIX域协议AF_LINK:链路地址协议AF_KEY:密钥套接字(socket)sockaddr_in其他字段的含义非常清楚,具体的设置涉及其他函数,在后面会有详细的讲解。2.数据存储优先顺序(1)函数说明。计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式,PC机通常采用小端模式)。Internet上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这两个字节存储优先顺序进行相互转化。这里用到了4个函数:htons()、ntohs()、htonl()和ntohl()。这4个地址分别实现网络字节序和主机字节序的转化,这里的h代表host,n代表network,s代表short,l代表long。通常16位的IP端口号用s代表,而IP地址用l来代表。(2)函数格式说明。表10.2列出了这4个函数的语法格式。表10.2 htons等函数语法要点所需头文件#include<netinet/in.h>函数原型uint16_thtons(unit16_thost16bit)uint32_thtonl(unit32_thost32bit)uint16_tntohs(unit16_tnet16bit)uint32_tntohs(unit32_tnet32bit)函数传入值host16bit:主机字节序的16位数据host32bit:主机字节序的32位数据net16bit:网络字节序的16位数据net32bit:网络字节序的32位数据函数返回值成功:返回要转换的字节序出错:-1注意调用该函数只是使其得到相应的字节序,用户不需清楚该系统的主机字节序和网络字节序是否真正相等。如果是相同不需要转换的话,该系统的这些函数会定义成空宏。3.地址格式转化(1)函数说明。通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制IPv6地址),而在通常使用的socket编程中所使用的则是二进制值,这就需要将这两个数值进行转换。这里在IPv4中用到的函数有inet_aton()、inet_addr()和inet_ntoa(),而IPv4和IPv6兼容的函数有inet_pton()和inet_ntop()。由于IPv6是下一代互联网的标准协议,因此,本书讲解的函数都能够同时兼容IPv4和IPv6,但在具体举例时仍以IPv4为例。这里inet_pton()函数是将点分十进制地址映射为二进制地址,而inet_ntop()是将二进制地址映射为点分十进制地址。(2)函数格式。表10.3列出了inet_pton函数的语法要点。表10.3 inet_pton函数语法要点所需头文件#include<arpa/inet.h>函数原型intinet_pton(intfamily,constchar*strptr,void*addrptr)函数传入值familyAF_INET:IPv4协议AF_INET6:IPv6协议strptr:要转化的值addrptr:转化后的地址函数返回值成功:0出错:-1表10.4列出了inet_ntop函数的语法要点。表10.4 inet_ntop函数语法要点所需头文件#include<arpa/inet.h>函数原型intinet_ntop(intfamily,void*addrptr,char*strptr,size_tlen)函数传入值familyAF_INET:IPv4协议AF_INET6:IPv6协议函数传入值addrptr:转化后的地址strptr:要转化的值len:转化后值的大小函数返回值成功:0出错:-14.名字地址转化(1)函数说明。通常,人们在使用过程中都不愿意记忆冗长的IP地址,尤其到IPv6时,地址长度多达128位,那时就更加不可能一次次记忆那么长的IP地址了。因此,使用主机名将会是很好的选择。在Linux中,同样有一些函数可以实现主机名和地址的转化,最为常见的有gethostbyname()、gethostbyaddr()和getaddrinfo()等,它们都可以实现IPv4和IPv6的地址和主机名之间的转化。其中gethostbyname()是将主机名转化为IP地址,gethostbyaddr()则是逆操作,是将IP地址转化为主机名,另外getaddrinfo()还能实现自动识别IPv4地址和IPv6地址。gethostbyname()和gethostbyaddr()都涉及一个hostent的结构体,如下所示:structhostent{char*h_name;/*正式主机名*/char**h_aliases;/*主机别名*/inth_addrtype;/*地址类型*/inth_length;/*地址字节长度*/char**h_addr_list;/*指向IPv4或IPv6的地址指针数组*/}调用gethostbyname()函数或gethostbyaddr()函数后就能返回hostent结构体的相关信息。getaddrinfo()函数涉及一个addrinfo的结构体,如下所示:structaddrinfo{intai_flags;/*AI_PASSIVE,AI_CANONNAME;*/intai_family;/*地址族*/intai_socktype;/*socket类型*/intai_protocol;/*协议类型*/size_tai_addrlen;/*地址字节长度*/char*ai_canonname;/*主机名*/structsockaddr*ai_addr;/*socket结构体*/structaddrinfo*ai_next;/*下一个指针链表*/}hostent结构体而言,addrinfo结构体包含更多的信息。(2)函数格式。表10.5列出了gethostbyname()函数的语法要点。表10.5 gethostbyname函数语法要点所需头文件#include<netdb.h>函数原型structhostent*gethostbyname(constchar*hostname)函数传入值hostname:主机名函数返回值成功:hostent类型指针出错:-1调用该函数时可以首先对hostent结构体中的h_addrtype和h_length进行设置,若为IPv4可设置为AF_INET和4;若为IPv6可设置为AF_INET6和16;若不设置则默认为IPv4地址类型。表10.6列出了getaddrinfo()函数的语法要点。表10.6 getaddrinfo()函数语法要点所需头文件#include<netdb.h>函数原型intgetaddrinfo(constchar*node,constchar*service,conststructaddrinfo*hints,structaddrinfo**result)函数传入值node:网络地址或者网络主机名service:服务名或十进制的端口号字符串hints:服务线索result:返回结果函数返回值成功:0出错:-1在调用之前,首先要对hints服务线索进行设置。它是一个addrinfo结构体,表10.7列举了该结构体常见的选项值。表10.7 addrinfo结构体常见选项值结构体头文件#include<netdb.h>ai_flagsAI_PASSIVE:该套接口是用作被动地打开AI_CANONNAME:通知getaddrinfo函数返回主机的名字ai_familyAF_INET:IPv4协议AF_INET6:IPv6协议AF_UNSPEC:IPv4或IPv6均可ai_socktypeSOCK_STREAM:字节流套接字socket(TCP)SOCK_DGRAM:数据报套接字socket(UDP)ai_protocolIPPROTO_IP:IP协议IPPROTO_IPV4:IPv4协议4IPv4IPPROTO_IPV6:IPv6协议IPPROTO_UDP:UDPIPPROTO_TCP:TCP注意(1)通常服务器端在调用getaddrinfo()之前,ai_flags设置AI_PASSIVE,用于bind()函数(用于端口和地址的绑定,后面会讲到),主机名nodename通常会设置为NULL。(2)客户端调用getaddrinfo()时,ai_flags一般不设置AI_PASSIVE,但是主机名nodename和服务名servname(端口)则应该不为空。(3)即使不设置ai_flags为AI_PASSIVE,取出的地址也可以被绑定,很多程序中ai_flags直接设置为0,即3个标志位都不设置,这种情况下只要hostname和servname设置的没有问题就可以正确绑定。(3)使用实例。下面的实例给出了getaddrinfo函数用法的示例,在后面小节中会给出gethostbyname函数用法的例子。/*getaddrinfo.c*/#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<string.h>#include<netdb.h>#include<sys/types.h>#include<netinet/in.h>#include<sys/socket.h>intmain(){structaddrinfohints,*res=NULL;intrc;memset(&hints,0,sizeof(hints));/*设置addrinfo结构体中各参数*/hints.ai_flags=AI_CANONNAME;hints.ai_family=AF_UNSPEC;hints.ai_socktype=SOCK_DGRAM;hints.ai_protocol=IPPROTO_UDP;/*调用getaddinfo函数*/rc=getaddrinfo("localhost",NULL,&hints,&res);if(rc!=0){perror("getaddrinfo");exit(1);}else{printf("Hostnameis%sn",res->ai_canonname);}exit(0);}10.2.3socket基础编程(1)函数说明。socket编程的基本函数有socket()、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等,其中根据客户端还是服务端,或者根据使用TCP协议还是UDP协议,这些函数的调用流程都有所区别,这里先对每个函数进行说明,再给出各种情况下使用的流程图。n socket():该函数用于建立一个socket连接,可指定socket类型等信息。在建立了socket连接之后,可对sockaddr或sockaddr_in结构进行初始化,以保存所建立的socket地址信息。n bind():该函数是用于将本地IP地址绑定到端口号,若绑定其他IP地址则不能成功。另外,它主要用于TCP的连接,而在UDP的连接中则无必要。n listen():在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新的连接请求。此时调用listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。n accept():服务端程序调用listen()函数创建等待队列之后,调用accept()函数等待并接收客户端的连接请求。它通常从由bind()所创建的等待队列中取出第一个未处理的连接请求。n connect():该函数在TCP中是用于bind()的之后的client端,用于与服务器端建立连接,而在UDP中由于没有了bind()函数,因此用connect()有点类似bind()函数的作用。n send()和recv():这两个函数分别用于发送和接收数据,可以用在TCP中,也可以用在UDP中。当用在UDP时,可以在connect()函数建立连接之后再用。n sendto()和recvfrom():这两个函数的作用与send()和recv()函数类似,也可以用在TCP和UDP中。当用在TCP时,后面的几个与地址有关参数不起作用,函数作用等同于send()和recv();当用在UDP时,可以用在之前没有使用connect()的情况下,这两个函数可以自动寻找指定地址并进行连接。服务器端和客户端使用TCP协议的流程如图10.6所示。服务器端和客户端使用UDP协议的流程如图10.7所示。 图10.6使用TCP协议socket编程流程图图10.7使用UDP协议socket编程流程图(2)函数格式。表10.8列出了socket()函数的语法要点。表10.8 socket()函数语法要点所需头文件#include<sys/socket.h>函数原型intsocket(intfamily,inttype,intprotocol)函数传入值family:协议族AF_INET:IPv4协议AF_INET6:IPv6协议AF_LOCAL:UNIX域协议AF_ROUTE:路由套接字(socket)AF_KEY:密钥套接字(socket)type:套接字类型SOCK_STREAM:字节流套接字socketSOCK_DGRAM:数据报套接字socketSOCK_RAW:原始套接字socketprotoco:0(原始套接字除外)函数返回值成功:非负套接字描述符出错:-1表10.9列出了bind()函数的语法要点。表10.9 bind()函数语法要点所需头文件#include<sys/socket.h>函数原型intbind(intsockfd,structsockaddr*my_addr,intaddrlen)函数传入值socktd:套接字描述符my_addr:本地地址addrlen:地址长度函数返回值成功:0出错:-1端口号和地址在my_addr中给出了,若不指定地址,则内核随意分配一个临时端口给该应用程序。表10.10列出了listen()函数的语法要点。表10.10 listen()函数语法要点所需头文件#include<sys/socket.h>函数原型intlisten(intsockfd,intbacklog)函数传入值socktd:套接字描述符backlog:请求队列中允许的最大请求数,大多数系统缺省值为5函数返回值成功:0出错:-1表10.11列出了accept()函数的语法要点。表10.11 accept()函数语法要点所需头文件#include<sys/socket.h>函数原型intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen)函数传入值socktd:套接字描述符addr:客户端地址addrlen:地址长度函数返回值成功:0出错:-1表10.12列出了connect()函数的语法要点。表10.12 connect()函数语法要点所需头文件#include<sys/socket.h>函数原型intconnect(intsockfd,structsockaddr*serv_addr,intaddrlen)函数传入值socktd:套接字描述符serv_addr:服务器端地址addrlen:地址长度函数返回值成功:0出错:-1表10.13列出了send()函数的语法要点。表10.13 send()函数语法要点所需头文件#include<sys/socket.h>函数原型intsend(intsockfd,constvoid*msg,intlen,intflags)函数传入值socktd:套接字描述符msg:指向要发送数据的指针len:数据长度flags:一般为0函数返回值成功:发送的字节数出错:-1表10.14列出了recv()函数的语法要点。表10.14 recv()函数语法要点所需头文件#include<sys/socket.h>函数原型intrecv(intsockfd,void*buf,intlen,unsignedintflags)函数传入值socktd:套接字描述符buf:存放接收数据的缓冲区len:数据长度flags:一般为0函数返回值成功:接收的字节数出错:-1表10.15列出了sendto()函数的语法要点。表10.15 sendto()函数语法要点所需头文件#include<sys/socket.h>函数原型intsendto(intsockfd,constvoid*msg,intlen,unsignedintflags,conststructsockaddr*to,inttolen)函数传入值socktd:套接字描述符msg:指向要发送数据的指针len:数据长度flags:一般为0to:目地机的IP地址和端口号信息tolen:地址长度函数返回值成功:发送的字节数出错:-1表10.16列出了recvfrom()函数的语法要点。表10.16 recvfrom()函数语法要点所需头文件#include<sys/socket.h>函数原型intrecvfrom(intsockfd,void*buf,intlen,unsignedintflags,structsockaddr*from,int*fromlen)函数传入值socktd:套接字描述符buf:存放接收数据的缓冲区len:数据长度flags:一般为0from:源主机的IP地址和端口号信息tolen:地址长度函数返回值成功:接收的字节数出错:-1(3)使用实例。该实例分为客户端和服务器端两部分,其中服务器端首先建立起socket,然后与本地端口进行绑定,接着就开始接收从客户端的连接请求并建立与它的连接,接下来,接收客户端发送的消息。客户端则在建立socket之后调用connect()函数来建立连接。服务端的代码如下所示:/*server.c*/#include<sys/types.h>#include<sys/socket.h>#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<string.h>#include<unistd.h>#include<netinet/in.h>#definePORT4321#defineBUFFER_SIZE1024#defineMAX_QUE_CONN_NM5intmain(){structsockaddr_inserver_sockaddr,client_sockaddr;intsin_size,recvbytes;intsockfd,client_fd;charbuf[BUFFER_SIZE];/*建立socket连接*/if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){perror("socket");exit(1);}printf("Socketid=%dn",sockfd);/*设置sockaddr_in结构体中相关参数*/server_sockaddr.sin_family=AF_INET;server_sockaddr.sin_port=htons(PORT);server_sockaddr.sin_addr.s_addr=INADDR_ANY;bzero(&(server_sockaddr.sin_zero),8);inti=1;/*允许重复使用本地地址与套接字进行绑定*/setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i));/*绑定函数bind()*/if(bind(sockfd,(structsockaddr*)&server_sockaddr,sizeof(structsockaddr))==-1){perror("bind");exit(1);}printf("Bindsuccess!n");/*调用listen()函数,创建未处理请求的队列*/if(listen(sockfd,MAX_QUE_CONN_NM)==-1){perror("listen");exit(1);}printf("Listening....n");/*调用accept()函数,等待客户端的连接*/if((client_fd=accept(sockfd,(structsockaddr*)&client_sockaddr,&sin_size))==-1){perror("accept");exit(1);}/*调用recv()函数接收客户端的请求*/memset(buf,0,sizeof(buf));if((recvbytes=recv(client_fd,buf,BUFFER_SIZE,0))==-1){perror("recv");exit(1);}printf("Receivedamessage:%sn",buf);close(sockfd);exit(0);}客户端的代码如下所示:/*client.c*/#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<string.h>#include<netdb.h>#include<sys/types.h>#include<netinet/in.h>#include<sys/socket.h>#definePORT4321#defineBUFFER_SIZE1024intmain(intargc,char*argv[]){intsockfd,sendbytes;charbuf[BUFFER_SIZE];structhostent*host;structsockaddr_inserv_addr;if(argc<3){fprintf(stderr,"USAGE:./clientHostname(oripaddress)Textn");exit(1);}/*地址解析函数*/if((host=gethostbyname(argv[1]))==NULL){perror("gethostbyname");exit(1);}memset(buf,0,sizeof(buf));sprintf(buf,"%s",argv[2]);/*创建socket*/if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){perror("socket");exit(1);}/*设置sockaddr_in结构体中相关参数*/serv_addr.sin_family=AF_INET;serv_addr.sin_port=htons(PORT);serv_addr.sin_addr=*((structin_addr*)host->h_addr);bzero(&(serv_addr.sin_zero),8);/*调用connect函数主动发起对服务器端的连接*/if(connect(sockfd,(structsockaddr*)&serv_addr,sizeof(structsockaddr))==-1){perror("connect");exit(1);}/*发送消息给服务器端*/if((sendbytes=send(sockfd,buf,strlen(buf),0))==-1){perror("send");exit(1);}close(sockfd);exit(0);}在运行时需要先启动服务器端,再启动客户端。这里可以把服务器端下载到开发板上,客户端在宿主机上运行,然后配置双方的IP地址,在确保双方可以通信(如使用ping命令验证)的情况下运行该程序即可。$./serverSocketid=3Bindsuccess!Listening....Receivedamessage:Hello,Server!$./clientlocalhost(或者输入IP地址)Hello,Server!

    时间:2018-06-15 关键词: 操作系统 socket 基础教程 网络编程 嵌入式linux

  • 嵌入式Linux网络编程之:网络高级编程

    10.3网络高级编程在实际情况中,人们往往遇到多个客户端连接服务器端的情况。由于之前介绍的如connet()、recv()和send()等都是阻塞性函数,如果资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理I/O多路复用的情况了。本节给出了两种解决I/O多路复用的解决方法,这两个函数都是之前学过的fcntl()和select()(请读者先复习第6章中的相关内容)。可以看到,由于在Linux中把socket也作为一种特殊文件描述符,这给用户的处理带来了很大的方便。1.fcntl()函数fcntl()针对socket编程提供了如下的编程特性。n 非阻塞I/O:可将cmd设置为F_SETFL,将lock设置为O_NONBLOCK。n 异步I/O:可将cmd设置为F_SETFL,将lock设置为O_ASYNC。下面是用fcntl()将套接字设置为非阻塞I/O的实例代码:/*net_fcntl.c*/#include<sys/types.h>#include<sys/socket.h>#include<sys/wait.h>#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<string.h>#include<sys/un.h>#include<sys/time.h>#include<sys/ioctl.h>#include<unistd.h>#include<netinet/in.h>#include<fcntl.h>#definePORT1234#defineMAX_QUE_CONN_NM5#defineBUFFER_SIZE1024intmain(){structsockaddr_inserver_sockaddr,client_sockaddr;intsin_size,recvbytes,flags;intsockfd,client_fd;charbuf[BUFFER_SIZE];if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){perror("socket");exit(1);}server_sockaddr.sin_family=AF_INET;server_sockaddr.sin_port=htons(PORT);server_sockaddr.sin_addr.s_addr=INADDR_ANY;bzero(&(server_sockaddr.sin_zero),8);inti=1;/*允许重复使用本地地址与套接字进行绑定*/setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i));if(bind(sockfd,(structsockaddr*)&server_sockaddr,sizeof(structsockaddr))==-1){perror("bind");exit(1);}if(listen(sockfd,MAX_QUE_CONN_NM)==-1){perror("listen");exit(1);}printf("Listening....\n");/*调用fcntl()函数给套接字设置非阻塞属性*/flags=fcntl(sockfd,F_GETFL);if(flags<0||fcntl(sockfd,F_SETFL,flags|O_NONBLOCK)<0){perror("fcntl");exit(1);}while(1){sin_size=sizeof(structsockaddr_in);if((client_fd=accept(sockfd,(structsockaddr*)&client_sockaddr,&sin_size))<0){perror("accept");exit(1);}if((recvbytes=recv(client_fd,buf,BUFFER_SIZE,0))<0){perror("recv");exit(1);}printf("Receivedamessage:%s\n",buf);}/*while*/close(client_fd);exit(1);}运行该程序,结果如下所示:$./net_fcntlListening....accept:Resourcetemporarilyunavailable可以看到,当accept()的资源不可用(没有任何未处理的等待连接的请求)时,程序就会自动返回。2.select()使用fcntl()函数虽然可以实现非阻塞I/O或信号驱动I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的CPU资源的占用。在这里可以使用select()函数来解决这个问题,同时,使用select()函数还可以设置等待的时间,可以说功能更加强大。下面是使用select()函数的服务器端源代码。客户端程序基本上与10.2.3小节中的例子相同,仅加入一行sleep()函数,使得客户端进程等待几秒钟才结束。/*net_select.c*/#include<sys/types.h>#include<sys/socket.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#include<sys/time.h>#include<sys/ioctl.h>#include<unistd.h>#include<netinet/in.h>#definePORT4321#defineMAX_QUE_CONN_NM5#defineMAX_SOCK_FDFD_SETSIZE#defineBUFFER_SIZE1024intmain(){structsockaddr_inserver_sockaddr,client_sockaddr;intsin_size,count;fd_setinset,tmp_inset;intsockfd,client_fd,fd;charbuf[BUFFER_SIZE];if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){perror("socket");exit(1);}server_sockaddr.sin_family=AF_INET;server_sockaddr.sin_port=htons(PORT);server_sockaddr.sin_addr.s_addr=INADDR_ANY;bzero(&(server_sockaddr.sin_zero),8);inti=1;/*允许重复使用本地地址与套接字进行绑定*/setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i));if(bind(sockfd,(structsockaddr*)&server_sockaddr,sizeof(structsockaddr))==-1){perror("bind");exit(1);}if(listen(sockfd,MAX_QUE_CONN_NM)==-1){perror("listen");exit(1);}printf("listening....\n");/*将调用socket()函数的描述符作为文件描述符*/FD_ZERO(&inset);FD_SET(sockfd,&inset);while(1){tmp_inset=inset;sin_size=sizeof(structsockaddr_in);memset(buf,0,sizeof(buf));/*调用select()函数*/if(!(select(MAX_SOCK_FD,&tmp_inset,NULL,NULL,NULL)>0)){perror("select");}for(fd=0;fd<MAX_SOCK_FD;fd++){if(FD_ISSET(fd,&tmp_inset)>0){if(fd==sockfd){/*服务端接收客户端的连接请求*/if((client_fd=accept(sockfd,(structsockaddr*)&client_sockaddr,&sin_size))==-1){perror("accept");exit(1);}FD_SET(client_fd,&inset);printf("Newconnectionfrom%d(socket)\n",client_fd);}else/*处理从客户端发来的消息*/{if((count=recv(client_fd,buf,BUFFER_SIZE,0))>0){printf("Receivedamessagefrom%d:%s\n",client_fd,buf);}else{close(fd);FD_CLR(fd,&inset);printf("Client%d(socket)hasleft\n",fd);}}}/*endofifFD_ISSET*/}/*endofforfd*/}/*endifwhilewhile*/close(sockfd);exit(0);}运行该程序时,可以先启动服务器端,再反复运行客户端程序(这里启动两个客户端进程)即可,服务器端运行结果如下所示:$./serverlistening....Newconnectionfrom4(socket)/*接受第一个客户端的连接请求*/Receivedamessagefrom4:Hello,First!/*接收第一个客户端发送的数据*/Newconnectionfrom5(socket)/*接受第二个客户端的连接请求*/Receivedamessagefrom5:Hello,Second!/*接收第二个客户端发送的数据*/Client4(socket)hasleft/*检测到第一个客户端离线了*/Client5(socket)hasleft/*检测到第二个客户端离线了*/$./clientlocalhostHello,First!&./clientlocalhostHello,Second

    时间:2018-06-15 关键词: 操作系统 基础教程 嵌入式linux select() 网络高级编程 fcntl()

  • 嵌入式Linux网络编程之:实验内容——NTP协议实现

    嵌入式Linux网络编程之:实验内容——NTP协议实现

    10.4实验内容——NTP协议实现1.实验目的通过实现NTP协议的练习,进一步掌握Linux网络编程,并且提高协议的分析与实现能力,为参与完成综合性项目打下良好的基础。2.实验内容NetworkTimeProtocol(NTP)协议是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS等)做同步化,它可以提供高精确度的时间校正(LAN上与标准时间差小于1毫秒,WAN上几十毫秒),且可用加密确认的方式来防止恶毒的协议攻击。NTP提供准确时间,首先要有准确的时间来源,这一时间应该是国际标准时间UTC。NTP获得UTC的时间来源可以是原子钟、天文台、卫星,也可以从Internet上获取。这样就有了准确而可靠的时间源。时间是按NTP服务器的等级传播。按照距离外部UTC源的远近将所有服务器归入不同的Stratun(层)中。Stratum-1在顶层,有外部UTC接入,而Stratum-2则从Stratum-1获取时间,Stratum-3从Stratum-2获取时间,以此类推,但Stratum层的总数限制在15以内。所有这些服务器在逻辑上形成阶梯式的架构并相互连接,而Stratum-1的时间服务器是整个系统的基础。进行网络协议实现时最重要的是了解协议数据格式。NTP数据包有48个字节,其中NTP包头16字节,时间戳32个字节。其协议格式如图10.9所示。图10.9NTP协议数据格式其协议字段的含义如下所示。n LI:跳跃指示器,警告在当月最后一天的最终时刻插入的迫近闺秒(闺秒)。n VN:版本号。n Mode:工作模式。该字段包括以下值:0-预留;1-对称行为;3-客户机;4-服务器;5-广播;6-NTP控制信息。NTP协议具有3种工作模式,分别为主/被动对称模式、客户/服务器模式、广播模式。在主/被动对称模式中,有一对一的连接,双方均可同步对方或被对方同步,先发出申请建立连接的一方工作在主动模式下,另一方工作在被动模式下;客户/服务器模式与主/被动模式基本相同,惟一区别在于客户方可被服务器同步,但服务器不能被客户同步;在广播模式中,有一对多的连接,服务器不论客户工作在何种模式下,都会主动发出时间信息,客户根据此信息调整自己的时间。n Stratum:对本地时钟级别的整体识别。n Poll:有符号整数表示连续信息间的最大间隔。n Precision:有符号整数表示本地时钟精确度。n RootDelay:表示到达主参考源的一次往复的总延迟,它是有15~16位小数部分的符号定点小数。n RootDispersion:表示一次到达主参考源的标准误差,它是有15~16位小数部分的无符号定点小数。n ReferenceIdentifier:识别特殊参考源。n OriginateTimestamp:这是向服务器请求分离客户机的时间,采用64位时标格式。n ReceiveTimestamp:这是向服务器请求到达客户机的时间,采用64位时标格式。n TransmitTimestamp:这是向客户机答复分离服务器的时间,采用64位时标格式。n Authenticator(Optional):当实现了NTP认证模式时,主要标识符和信息数字域就包括已定义的信息认证代码(MAC)信息。由于NTP协议中涉及比较多的时间相关的操作,为了简化实现过程,在本实验中,仅要求实现NTP协议客户端部分的网络通信模块,也就是构造NTP协议字段进行发送和接收,最后与时间相关的操作不需进行处理。NTP协议是作为OSI参考模型的高层协议比较适合采用UDP传输协议进行数据传输,专用端口号为123。在实验中,以国家授时中心服务器(IP地址为202.72.145.44)作为NTP(网络时间)服务器。3.实验步骤(1)画出流程图。简易NTP客户端的实现流程如图10.10所示。图10.10简易NTP客户端流程图(2)编写程序。具体代码如下:/*ntp.c*/#include<sys/socket.h>#include<sys/wait.h>#include<stdio.h>#include<stdlib.h>#include<errno.h>#include<string.h>#include<sys/un.h>#include<sys/time.h>#include<sys/ioctl.h>#include<unistd.h>#include<netinet/in.h>#include<string.h>#include<netdb.h>#defineNTP_PORT123/*NTP专用端口号字符串*/#defineTIME_PORT37/*TIME/UDP端口号*/#defineNTP_SERVER_IP"210.72.145.44"/*国家授时中心IP*/#defineNTP_PORT_STR"123"/*NTP专用端口号字符串*/#defineNTPV1"NTP/V1"/*协议及其版本号*/#defineNTPV2"NTP/V2"#defineNTPV3"NTP/V3"#defineNTPV4"NTP/V4"#defineTIME"TIME/UDP"#defineNTP_PCK_LEN48#defineLI0#defineVN3#defineMODE3#defineSTRATUM0#definePOLL4#definePREC-6#defineJAN_19700x83aa7e80/*1900年~1970年之间的时间秒数*/#defineNTPFRAC(x)(4294*(x)+((1981*(x))>>11))#defineUSEC(x)(((x)>>12)-759*((((x)>>10)+32768)>>16))typedefstruct_ntp_time{unsignedintcoarse;unsignedintfine;}ntp_time;structntp_packet{unsignedcharleap_ver_mode;unsignedcharstartum;charpoll;charprecision;introot_delay;introot_dispersion;intreference_identifier;ntp_timereference_timestamp;ntp_timeoriginage_timestamp;ntp_timereceive_timestamp;ntp_timetransmit_timestamp;};charprotocol[32];/*构建NTP协议包*/intconstruct_packet(char*packet){charversion=1;longtmp_wrd;intport;time_ttimer;strcpy(protocol,NTPV3);/*判断协议版本*/if(!strcmp(protocol,NTPV1)||!strcmp(protocol,NTPV2)||!strcmp(protocol,NTPV3)||!strcmp(protocol,NTPV4)){memset(packet,0,NTP_PCK_LEN);port=NTP_PORT;/*设置16字节的包头*/version=protocol[6]-0x30;tmp_wrd=htonl((LI<<30)|(version<<27)|(MODE<<24)|(STRATUM<<16)|(POLL<<8)|(PREC&0xff));memcpy(packet,&tmp_wrd,sizeof(tmp_wrd));/*设置RootDelay、RootDispersion和ReferenceIndentifier*/tmp_wrd=htonl(1<<16);memcpy(&packet[4],&tmp_wrd,sizeof(tmp_wrd));memcpy(&packet[8],&tmp_wrd,sizeof(tmp_wrd));/*设置Timestamp部分*/time(&timer);/*设置TransmitTimestampcoarse*/tmp_wrd=htonl(JAN_1970+(long)timer);memcpy(&packet[40],&tmp_wrd,sizeof(tmp_wrd));/*设置TransmitTimestampfine*/tmp_wrd=htonl((long)NTPFRAC(timer));memcpy(&packet[44],&tmp_wrd,sizeof(tmp_wrd));returnNTP_PCK_LEN;}elseif(!strcmp(protocol,TIME))/*"TIME/UDP"*/{port=TIME_PORT;memset(packet,0,4);return4;}return0;}/*获取NTP时间*/intget_ntp_time(intsk,structaddrinfo*addr,structntp_packet*ret_time){fd_setpending_data;structtimevalblock_time;chardata[NTP_PCK_LEN*8];intpacket_len,data_len=addr->ai_addrlen,count=0,result,i,re;if(!(packet_len=construct_packet(data))){return0;}/*客户端给服务器端发送NTP协议数据包*/if((result=sendto(sk,data,packet_len,0,addr->ai_addr,data_len))<0){perror("sendto");return0;}/*调用select()函数,并设定超时时间为1s*/FD_ZERO(&pending_data);FD_SET(sk,&pending_data);block_time.tv_sec=10;block_time.tv_usec=0;if(select(sk+1,&pending_data,NULL,NULL,&block_time)>0){/*接收服务器端的信息*/if((count=recvfrom(sk,data,NTP_PCK_LEN*8,0,addr->ai_addr,&data_len))<0){perror("recvfrom");return0;}if(protocol==TIME){memcpy(&ret_time->transmit_timestamp,data,4);return1;}elseif(count<NTP_PCK_LEN){return0;}/*设置接收NTP包的数据结构*/ret_time->leap_ver_mode=ntohl(data[0]);ret_time->startum=ntohl(data[1]);ret_time->poll=ntohl(data[2]);ret_time->precision=ntohl(data[3]);ret_time->root_delay=ntohl(*(int*)&(data[4]));ret_time->root_dispersion=ntohl(*(int*)&(data[8]));ret_time->reference_identifier=ntohl(*(int*)&(data[12]));ret_time->reference_timestamp.coarse=ntohl*(int*)&(data[16]));ret_time->reference_timestamp.fine=ntohl(*(int*)&(data[20]));ret_time->originage_timestamp.coarse=ntohl(*(int*)&(data[24]));ret_time->originage_timestamp.fine=ntohl(*(int*)&(data[28]));ret_time->receive_timestamp.coarse=ntohl(*(int*)&(data[32]));ret_time->receive_timestamp.fine=ntohl(*(int*)&(data[36]));ret_time->transmit_timestamp.coarse=ntohl(*(int*)&(data[40]));ret_time->transmit_timestamp.fine=ntohl(*(int*)&(data[44]));return1;}/*endofifselect*/return0;}/*修改本地时间*/intset_local_time(structntp_packet*pnew_time_packet){structtimevaltv;tv.tv_sec=pnew_time_packet->transmit_timestamp.coarse-JAN_1970;tv.tv_usec=USEC(pnew_time_packet->transmit_timestamp.fine);returnsettimeofday(&tv,NULL);}intmain(){intsockfd,rc;structaddrinfohints,*res=NULL;structntp_packetnew_time_packet;memset(&hints,0,sizeof(hints));hints.ai_family=AF_UNSPEC;hints.ai_socktype=SOCK_DGRAM;hints.ai_protocol=IPPROTO_UDP;/*调用getaddrinfo()函数,获取地址信息*/rc=getaddrinfo(NTP_SERVER_IP,NTP_PORT_STR,&hints,&res);if(rc!=0){perror("getaddrinfo");return1;}/*创建套接字*/sockfd=socket(res->ai_family,res->ai_socktype,res->ai_protocol);if(sockfd<0){perror("socket");return1;}/*调用取得NTP时间的函数*/if(get_ntp_time(sockfd,res,&new_time_packet)){/*调整本地时间*/if(!set_local_time(&new_time_packet)){printf("NTPclientsuccess!n");}}close(sockfd);return0;}为了更好地观察程序的效果,先用date命令修改一下系统时间,再运行实例程序。运行完了之后再查看系统时间,可以发现已经恢复准确的系统时间了。具体运行结果如下所示。$date-s"2001-01-011:00:00"2001年01月01日星期一01:00:00EST$date2001年01月01日星期一01:00:00EST$./ntpNTPclientsuccess!$date能够显示当前准确的日期和时间了!

    时间:2018-06-15 关键词: 操作系统 基础教程 网络编程 嵌入式linux ntp协议

  • 嵌入式Linux网络编程之:本章小结与思考与练习

    10.5 本章小结本章首先概括地讲解了OSI分层结构以及TCP/IP协议各层的主要功能,介绍了常见的TCP/IP协议族,并且重点讲解了网络编程中需要用到的TCP和UDP协议,为嵌入式Linux的网络编程打下良好的基础。接着本章介绍了socket的定义及其类型,并逐个介绍常见的socket相关的基本函数,包括地址处理函数、数据存储转换函数等,这些函数都是最为常用的函数,要在理解概念的基础上熟练掌握。接下来介绍的是网络编程中的基本函数,这也是最为常见的几个函数,这里要注意TCP和UDP在处理过程中的不同。同时,本章还介绍了较为高级的网络编程,包括调用fcntl()和select()函数,这两个函数在前面的章节中都已经讲解过,但在本章中有特殊的用途。最后,本章以ping程序为例,讲解了常见协议的实现过程,读者可以看到一个成熟的协议是如何实现的。本章的实验安排了实现一个比较简单但完整的NTP客户端程序,主要实现了其中数据收发的主要功能,以及时间同步调整的功能。10.6 思考与练习1.分别用多线程和多路复用实现网络聊天程序。2.实现一个小型模拟的路由器,就是接收从某个IP地址的连接请求,再把该请求转发到另一个IP地址的主机上去。

    时间:2018-06-15 关键词: 操作系统 基础教程 网络编程 tcp/ip协议 嵌入式linux osi分层结构

  • 嵌入式Linux设备驱动开发之:设备驱动概述

    嵌入式Linux设备驱动开发之:设备驱动概述

    11.1设备驱动概述11.1.1设备驱动简介及驱动模块操作系统是通过各种驱动程序来驾驭硬件设备的,它为用户屏蔽了各种各样的设备,驱动硬件是操作系统最基本的功能,并且提供统一的操作方式。设备驱动程序是内核的一部分,硬件驱动程序是操作系统最基本的组成部分,在Linux内核源程序中也占有60%以上。因此,熟悉驱动的编写是很重要的。在第2章中已经提到过,Linux内核中采用可加载的模块化设计(LKMs,LoadableKernelModules),一般情况下编译的Linux内核是支持可插入式模块的,也就是将最基本的核心代码编译在内核中,其他的代码可以编译到内核中,或者编译为内核的模块文件(在需要时动态加载)。常见的驱动程序是作为内核模块动态加载的,比如声卡驱动和网卡驱动等,而Linux最基础的驱动,如CPU、PCI总线、TCP/IP协议、APM(高级电源管理)、VFS等驱动程序则直接编译在内核文件中。有时也把内核模块叫做驱动程序,只不过驱动的内容不一定是硬件罢了,比如ext3文件系统的驱动。因此,加载驱动就是加载内核模块。这里,首先列举一些模块相关的命令。n lsmod列出当前系统中加载的模块,其中左边第一列是模块名,第二列是该模块大小,第三列则是使用该模块的对象数目。如下所示:$lsmodModuleSizeUsedbyAutofs120680(autoclean)(unused)eepro100181281iptable_nat 192520(autoclean)(unused)ip_conntrack185401(autoclean)[iptable_nat]iptable_mangle22720(autoclean)(unused)iptable_filter22720(autoclean)(unused)ip_tables119365[iptable_natiptable_mangleiptable_filter]usb-ohci193280(unused)usbcore545281[usb-ohci]ext3677282jbd444802[ext3]aic7xxx1147043sd_mod115843scsi_mod985122[aic7xxxsd_mod]n rmmod是用于将当前模块卸载。n insmod和modprobe是用于加载当前模块,但insmod不会自动解决依存关系,即如果要加载的模块引用了当前内核符号表中不存在的符号,则无法加载,也不会去查在其他尚未加载的模块中是否定义了该符号;modprobe可以根据模块间依存关系以及/etc/modules.conf文件中的内容自动加载其他有依赖关系的模块。11.1.2设备分类本书在前面也提到过,Linux的一个重要特点就是将所有的设备都当做文件进行处理,这一类特殊文件就是设备文件,它们可以使用前面提到的文件、I/O相关函数进行操作,这样就大大方便了对设备的处理。它通常在/dev下面存在一个对应的逻辑设备节点,这个节点以文件的形式存在。Linux系统的设备分为3类:字符设备、块设备和网络设备。n 字符设备通常指像普通文件或字节流一样,以字节为单位顺序读写的设备,如并口设备、虚拟控制台等。字符设备可以通过设备文件节点访问,它与普通文件之间的区别在于普通文件可以被随机访问(可以前后移动访问指针),而大多数字符设备只能提供顺序访问,因为对它们的访问不会被系统所缓存。但也有例外,例如帧缓存(framebuffer)是一个可以被随机访问的字符设备。n 块设备通常指一些需要以块为单位随机读写的设备,如IDE硬盘、SCSI硬盘、光驱等。块设备也是通过文件节点来访问,它不仅可以提供随机访问,而且可以容纳文件系统(例如硬盘、闪存等)。Linux可以使用户态程序像访问字符设备一样每次进行任意字节的操作,只是在内核态内部中的管理方式和内核提供的驱动接口上不同。通过文件属性可以查看它们是哪种设备文件(字符设备文件或块设备文件)。$ls–l/devcrw-rw----1rootuucp4,6408-3022:58ttyS0/*串口设备,c表示字符设备*/brw-r-----1rootfloppy2,008-3022:58fd0/*软盘设备,b表示块设备*/n 网络设备通常是指通过网络能够与其他主机进行数据通信的设备,如网卡等。内核和网络设备驱动程序之间的通信调用一套数据包处理函数,它们完全不同于内核和字符以及块设备驱动程序之间的通信(read()、write()等函数)。Linux网络设备不是面向流的设备,因此不会将网络设备的名字(例如eth0)映射到文件系统中去。对这3种设备文件编写驱动程序时会有一定的区别,本书在后面会有相关内容的讲解。11.1.3设备号设备号是一个数字,它是设备的标志。就如前面所述,一个设备文件(也就是设备节点)可以通过mknod命令来创建,其中指定了主设备号和次设备号。主设备号表明设备的类型(例如串口设备、SCSI硬盘),与一个确定的驱动程序对应;次设备号通常是用于标明不同的属性,例如不同的使用方法、不同的位置、不同的操作等,它标志着某个具体的物理设备。高字节为主设备号,底字节为次设备号。例如,在系统中的块设备IDE硬盘的主设备号是3,而多个IDE硬盘及其各个分区分别赋予次设备号1、2、3…$ls–l/devcrw-rw----1rootuucp4,6408-3022:58ttyS0/*主设备号4,此设备号64*/11.1.4驱动层次结构Linux下的设备驱动程序是内核的一部分,运行在内核模式下,也就是说设备驱动程序为内核提供了一个I/O接口,用户使用这个接口实现对设备的操作。图11.1显示了典型的Linux输入/输出系统中各层次结构和功能。图11.1Linux输入/输出系统层次结构和功能Linux设备驱动程序包含中断处理程序和设备服务子程序两部分。设备服务子程序包含了所有与设备操作相关的处理代码。它从面向用户进程的设备文件系统中接受用户命令,并对设备控制器执行操作。这样,设备驱动程序屏蔽了设备的特殊性,使用户可以像对待文件一样操作设备。设备控制器获得系统服务有两种方式:查询和中断。因为Linux的设备驱动程序是内核的一部分,在设备查询期间系统不能运行其他代码,查询方式的工作效率比较低,所以只有少数设备如软盘驱动程序采取这种方式,大多设备以中断方式向设备驱动程序发出输入/输出请求。11.1.5设备驱动程序与外界的接口每种类型的驱动程序,不管是字符设备还是块设备都为内核提供相同的调用接口,因此内核能以相同的方式处理不同的设备。Linux为每种不同类型的设备驱动程序维护相应的数据结构,以便定义统一的接口并实现驱动程序的可装载性和动态性。Linux设备驱动程序与外界的接口可以分为如下3个部分。n 驱动程序与操作系统内核的接口:这是通过数据结构file_operations(在本书后面会有详细介绍)来完成的。n 驱动程序与系统引导的接口:这部分利用驱动程序对设备进行初始化。n 驱动程序与设备的接口:这部分描述了驱动程序如何与设备进行交互,这与具体设备密切相关。它们之间的相互关系如图11.2所示。图11.2设备驱动程序与外界的接口11.1.6设备驱动程序的特点综上所述,Linux中的设备驱动程序有如下特点。(1)内核代码:设备驱动程序是内核的一部分,如果驱动程序出错,则可能导致系统崩溃。(2)内核接口:设备驱动程序必须为内核或者其子系统提供一个标准接口。比如,一个终端驱动程序必须为内核提供一个文件I/O接口;一个SCSI设备驱动程序应该为SCSI子系统提供一个SCSI设备接口,同时SCSI子系统也必须为内核提供文件的I/O接口及缓冲区。(3)内核机制和服务:设备驱动程序使用一些标准的内核服务,如内存分配等。(4)可装载:大多数的Linux操作系统设备驱动程序都可以在需要时装载进内核,在不需要时从内核中卸载。(5)可设置:Linux操作系统设备驱动程序可以集成为内核的一部分,并可以根据需要把其中的某一部分集成到内核中,这只需要在系统编译时进行相应的设置即可。(6)动态性:在系统启动且各个设备驱动程序初始化后,驱动程序将维护其控制的设备。如果该设备驱动程序控制的设备不存在也不影响系统的运行,那么此时的设备驱动程序只是多占用了一点系统内存罢了。

    时间:2018-06-15 关键词: 操作系统 基础教程 设备驱动 嵌入式linux

  • 嵌入式Linux设备驱动开发之:字符设备驱动编程

    嵌入式Linux设备驱动开发之:字符设备驱动编程

    11.2字符设备驱动编程1.字符设备驱动编写流程设备驱动程序可以使用模块的方式动态加载到内核中去。加载模块的方式与以往的应用程序开发有很大的不同。以往在开发应用程序时都有一个main()函数作为程序的入口点,而在驱动开发时却没有main()函数,模块在调用insmod命令时被加载,此时的入口点是init_module()函数,通常在该函数中完成设备的注册。同样,模块在调用rmmod命令时被卸载,此时的入口点是cleanup_module()函数,在该函数中完成设备的卸载。在设备完成注册加载之后,用户的应用程序就可以对该设备进行一定的操作,如open()、read()、write()等,而驱动程序就是用于实现这些操作,在用户应用程序调用相应入口函数时执行相关的操作,init_module()入口点函数则不需要完成其他如read()、write()之类功能。上述函数之间的关系如图11.3所示。图11.3设备驱动程序流程图2.重要数据结构用户应用程序调用设备的一些功能是在设备驱动程序中定义的,也就是设备驱动程序的入口点,它是一个在<linux/fs.h>中定义的structfile_operations结构,这是一个内核结构,不会出现在用户空间的程序中,它定义了常见文件I/O函数的入口,如下所示:structfile_operations{loff_t(*llseek)(structfile*,loff_t,int);ssize_t(*read)(structfile*filp,char*buff,size_tcount,loff_t*offp);ssize_t(*write)(structfile*filp,constchar*buff,size_tcount,loff_t*offp);int(*readdir)(structfile*,void*,filldir_t);unsignedint(*poll)(structfile*,structpoll_table_struct*);int(*ioctl)(structinode*,structfile*,unsignedint,unsignedlong);int(*mmap)(structfile*,structvm_area_struct*);int(*open)(structinode*,structfile*);int(*flush)(structfile*);int(*release)(structinode*,structfile*);int(*fsync)(structfile*,structdentry*);int(*fasync)(int,structfile*,int);int(*check_media_change)(kdev_tdev);int(*revalidate)(kdev_tdev);int(*lock)(structfile*,int,structfile_lock*);};这里定义的很多函数是否跟第6章中的文件I/O系统调用类似?其实当时的系统调用函数通过内核,最终调用对应的structfile_operations结构的接口函数(例如,open()文件操作是通过调用对应文件的file_operations结构的open函数接口而被实现)。当然,每个设备的驱动程序不一定要实现其中所有的函数操作,若不需要定义实现时,则只需将其设为NULL即可。structinode结构提供了关于设备文件/dev/driver(假设此设备名为driver)的信息,structfile结构提供关于被打开的文件信息,主要用于与文件系统对应的设备驱动程序使用。structfile结构较为重要,这里列出了它的定义:structfile{mode_tf_mode;/*标识文件是否可读或可写,FMODE_READ或FMODE_WRITE*/dev_tf_rdev;/*用于/dev/tty*/off_tf_pos;/*当前文件位移*/unsignedshortf_flags;/*文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC*/unsignedshortf_count;/*打开的文件数目*/unsignedshortf_reada;structinode*f_inode;/*指向inode的结构指针*/structfile_operations*f_op;/*文件索引指针*/};3.设备驱动程序主要组成(1)早期版本的字符设备注册。早期版本的设备注册使用函数register_chrdev(),调用该函数后就可以向系统申请主设备号,如果register_chrdev()操作成功,设备名就会出现在/proc/devices文件里。在关闭设备时,通常需要解除原先的设备注册,此时可使用函数unregister_chrdev(),此后该设备就会从/proc/devices里消失。其中主设备号和次设备号不能大于255。当前不少的字符设备驱动代码仍然使用这些早期版本的函数接口,但在未来内核的代码中,将不会出现这种编程接口机制。因此应该尽量使用后面讲述的编程机制。register_chrdev()函数格式如表11.1所示。表11.1 register_chrdev()函数语法要点所需头文件#include<linux/fs.h>函数原型intregister_chrdev(unsignedintmajor,constchar*name,structfile_operations*fops)函数传入值major:设备驱动程序向系统申请的主设备号,如果为0则系统为此驱动程序动态地分配一个主设备号name:设备名fops:对各个调用的入口点函数返回值成功:如果是动态分配主设备号,此返回所分配的主设备号。且设备名就会出现在/proc/devices文件里出错:-1unregister_chrdev()函数格式如下表11.2所示:表11.2 unregister_chrdev()函数语法要点所需头文件#include<linux/fs.h>函数原型intunregister_chrdev(unsignedintmajor,constchar*name)函数传入值major:设备的主设备号,必须和注册时的主设备号相同name:设备名函数返回值成功:0,且设备名从/proc/devices文件里消失出错:-1(2)设备号相关函数。在前面已经提到设备号有主设备号和次设备号,其中主设备号表示设备类型,对应于确定的驱动程序,具备相同主设备号的设备之间共用同一个驱动程序,而用次设备号来标识具体物理设备。因此在创建字符设备之前,必须先获得设备的编号(可能需要分配多个设备号)。在Linux2.6的版本中,用dev_t类型来描述设备号(dev_t是32位数值类型,其中高12位表示主设备号,低20位表示次设备号)。用两个宏MAJOR和MINOR分别获得dev_t设备号的主设备号和次设备号,而且用MKDEV宏来实现逆过程,即组合主设备号和次设备号而获得dev_t类型设备号。分配设备号有静态和动态的两种方法。静态分配(register_chrdev_region()函数)是指在事先知道设备主设备号的情况下,通过参数函数指定第一个设备号(它的次设备号通常为0)而向系统申请分配一定数目的设备号。动态分配(alloc_chrdev_region())是指通过参数仅设置第一个次设备号(通常为0,事先不会知道主设备号)和要分配的设备数目而系统动态分配所需的设备号。通过unregister_chrdev_region()函数释放已分配的(无论是静态的还是动态的)设备号。它们的函数格式如表11.3所示。表11.3 设备号分配与释放函数语法要点所需头文件#include<linux/fs.h>函数原型intregister_chrdev_region(dev_tfirst,unsignedintcount,char*name)intalloc_chrdev_region(dev_t*dev,unsignedintfirstminor,unsignedintcount,char*name)voidunregister_chrdev_region(dev_tfirst,unsignedintcount)函数传入值first:要分配的设备号的初始值count:要分配(释放)的设备号数目name:要申请设备号的设备名称(在/proc/devices和sysfs中显示)dev:动态分配的第一个设备号函数返回值成功:0(只限于两种注册函数)出错:-1(只限于两种注册函数)(3)最新版本的字符设备注册。在获得了系统分配的设备号之后,通过注册设备才能实现设备号和驱动程序之间的关联。这里讲解2.6内核中的字符设备的注册和注销过程。在Linux内核中使用structcdev结构来描述字符设备,我们在驱动程序中必须将已分配到的设备号以及设备操作接口(即为structfile_operations结构)赋予structcdev结构变量。首先使用cdev_alloc()函数向系统申请分配structcdev结构,再用cdev_init()函数初始化已分配到的结构并与file_operations结构关联起来。最后调用cdev_add()函数将设备号与structcdev结构进行关联并向内核正式报告新设备的注册,这样新设备可以被用起来了。如果要从系统中删除一个设备,则要调用cdev_del()函数。具体函数格式如表11.4所示。表11.4 最新版本的字符设备注册所需头文件#include<linux/cdev.h>函数原型sturctcdev*cdev_alloc(void)voidcdev_init(structcdev*cdev,structfile_operations*fops)intcdev_add(structcdev*cdev,dev_tnum,unsignedintcount)voidcdev_del(structcdev*dev)函数传入值cdev:需要初始化/注册/删除的structcdev结构fops:该字符设备的file_operations结构num:系统给该设备分配的第一个设备号count:该设备对应的设备号数量函数返回值成功:cdev_alloc:返回分配到的structcdev结构指针cdev_add:返回0出错:cdev_alloc:返回NULLcdev_add:返回-12.6内核仍然保留早期版本的register_chrdev()等字符设备相关函数,其实从内核代码中可以发现,在register_chrdev()函数的实现中用到cdev_alloc()和cdev_add()函数,而在unregister_chrdev()函数的实现中调用cdev_del()函数。因此很多代码仍然使用早期版本接口,但这种机制将来会从内核中消失。前面已经提到字符设备的实际操作在structfile_operations结构的一组函数中定义,并在驱动程序中需要与字符设备结构关联起来。下面讨论structfile_operations结构中最主要的成员函数和它们的用法。(4)打开设备。打开设备的函数接口是open,根据设备的不同,open函数接口完成的功能也有所不同,但通常情况下在open函数接口中要完成如下工作。n 递增计数器,检查错误。n 如果未初始化,则进行初始化。n 识别次设备号,如果必要,更新f_op指针。n 分配并填写被置于filp->private_data的数据结构。其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。因此使用计数器就可以很好地完成这项功能。这里,实现计数器操作的是在2.6内核早期版本的<linux/module.h>中定义的3个宏,它们在最新版本里早就消失了,在下面列出只是为了帮读者理解老版本中的驱动代码。n MOD_INC_USE_COUNT:计数器加1。n MOD_DEC_USE_COUNT:计数器减1。n MOD_IN_USE:计数器非零时返回真。另外,当有多个物理设备时,就需要识别次设备号来对各个不同的设备进行不同的操作,在有些驱动程序中并不需要用到。注意虽然这是对设备文件执行的第一个操作,但却不是驱动程序一定要声明的操作。若这个函数的入口为NULL,那么设备的打开操作将永远成功,但系统不会通知驱动程序。(5)释放设备。释放设备的函数接口是release()。要注意释放设备和关闭设备是完全不同的。当一个进程释放设备时,其他进程还能继续使用该设备,只是该进程暂时停止对该设备的使用;而当一个进程关闭设备时,其他进程必须重新打开此设备才能使用它。释放设备时要完成的工作如下。n 递减计数器MOD_DEC_USE_COUNT(最新版本已经不再使用)。n 释放打开设备时系统所分配的内存空间(包括filp->private_data指向的内存空间)。n 在最后一次释放设备操作时关闭设备。(6)读写设备。读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,也就是将内核空间缓冲区里的数据复制到用户空间的缓冲区中或者相反。这里首先解释一个read()和write()函数的入口函数,如表11.5所示。表11.5 read、write函数接口语法要点所需头文件#include<linux/fs.h>函数原型ssize_t(*read)(structfile*filp,char*buff,size_tcount,loff_t*offp)ssize_t(*write)(structfile*filp,constchar*buff,size_tcount,loff_t*offp)函数传入值filp:文件指针buff:指向用户缓冲区count:传入的数据长度offp:用户在文件中的位置函数返回值成功:写入的数据长度虽然这个过程看起来很简单,但是内核空间地址和应用空间地址是有很大区别的,其中一个区别是用户空间的内存是可以被换出的,因此可能会出现页面失效等情况。所以不能使用诸如memcpy()之类的函数来完成这样的操作。在这里要使用copy_to_user()或copy_from_user()等函数,它们是用来实现用户空间和内核空间的数据交换的。copy_to_user()和copy_from_user()的格式如表11.6所示。表11.6 copy_to_user()/copy_from_user()函数语法要点所需头文件#include<asm/uaccess.h>函数原型unsignedlongcopy_to_user(void*to,constvoid*from,unsignedlongcount)unsignedlongcopy_from_user(void*to,constvoid*from,unsignedlongcount)函数传入值to:数据目的缓冲区from:数据源缓冲区count:数据长度函数返回值成功:写入的数据长度失败:-EFAULT要注意,这两个函数不仅实现了用户空间和内核空间的数据转换,而且还会检查用户空间指针的有效性。如果指针无效,那么就不进行复制。(7)ioctl。大部分设备除了读写操作,还需要硬件配置和控制(例如,设置串口设备的波特率)等很多其他操作。在字符设备驱动中ioctl函数接口给用户提供对设备的非读写操作机制。ioctl函数接口的具体格式如表11.7所示。表11.7 ioctl函数接口语法要点所需头文件#include<linux/fs.h>函数原型int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg)函数传入值inode:文件的内核内部结构指针filp:被打开的文件描述符cmd:命令类型arg:命令相关参数下面列出其他在驱动程序中常用的内核函数。(8)获取内存。在应用程序中获取内存通常使用函数malloc(),但在设备驱动程序中动态开辟内存可以以字节或页面为单位。其中,以字节为单位分配内存的函数有kmalloc(),注意的是,kmalloc()函数返回的是物理地址,而malloc()等返回的是线性虚拟地址,因此在驱动程序中不能使用malloc()函数。与malloc()不同,kmalloc()申请空间有大小限制。长度是2的整次方,并且不会对所获取的内存空间清零。以页为单位分配内存的函数如下所示。n get_zeroed_page():获得一个已清零页面。n get_free_page():获得一个或几个连续页面。n get_dma_pages():获得用于DMA传输的页面。与之相对应的释放内存用也有kfree()或free_page函数族。表11.8给出了kmalloc()函数的语法格式。表11.8 kmalloc()函数语法要点所需头文件#include<linux/malloc.h>函数原型void*kmalloc(unsignedintlen,intflags)函数传入值len:希望申请的字节数flagsGFP_KERNEL:内核内存的通常分配方法,可能引起睡眠GFP_BUFFER:用于管理缓冲区高速缓存GFP_ATOMIC:为中断处理程序或其他运行于进程上下文之外的代码分配内存,且不会引起睡眠GFP_USER:用户分配内存,可能引起睡眠GFP_HIGHUSER:优先高端内存分配__GFP_DMA:DMA数据传输请求内存__GFP_HIGHMEN:请求高端内存函数返回值成功:写入的数据长度失败:-EFAULT表11.9给出了kfree()函数的语法格式。表11.9 kfree()函数语法要点所需头文件#include<linux/malloc.h>函数原型voidkfree(void*obj)函数传入值obj:要释放的内存指针函数返回值成功:写入的数据长度失败:-EFAULT表11.10给出了以页为单位的分配函数get_free_page类函数的语法格式。表11.10 get_free_page类函数语法要点所需头文件#include<linux/malloc.h>函数原型unsignedlongget_zeroed_page(intflags)unsignedlong__get_free_page(intflags)unsignedlong__get_free_page(intflags,unsignedlongorder)unsignedlong__get_dma_page(intflags,unsignedlongorder)函数传入值flags:同kmalloc()order:要请求的页面数,以2为底的对数函数返回值成功:返回指向新分配的页面的指针失败:-EFAULT表11.11给出了基于页的内存释放函数free_page族函数的语法格式。表11.11 free_page类函数语法要点所需头文件#include<linux/malloc.h>函数原型unsignedlongfree_page(unsignedlongaddr)unsignedlongfree_pages(unsignedlongaddr,unsignedlongorder)函数传入值addr:要释放的内存起始地址order:要请求的页面数,以2为底的对数函数返回值成功:写入的数据长度失败:-EFAULT(9)打印信息。就如同在编写用户空间的应用程序,打印信息有时是很好的调试手段,也是在代码中很常用的组成部分。但是与用户空间不同,在内核空间要用函数printk()而不能用平常的函数printf()。printk()和printf()很类似,都可以按照一定的格式打印消息,所不同的是,printk()还可以定义打印消息的优先级。表11.12给出了printk()函数的语法格式。表11.12 printk类函数语法要点所需头文件#include<linux/kernel>函数原型intprintk(constchar*fmt,…)函数传入值fmt:日志级别KERN_EMERG:紧急时间消息KERN_ALERT:需要立即采取动作的情况KERN_CRIT:临界状态,通常涉及严重的硬件或软件操作失败KERN_ERR:错误报告KERN_WARNING:对可能出现的问题提出警告KERN_NOTICE:有必要进行提示的正常情况KERN_INFO:提示性信息KERN_DEBUG:调试信息…:与printf()相同函数返回值成功:0失败:-1这些不同优先级的信息输出到系统日志文件(例如:“/var/log/messages”),有时也可以输出到虚拟控制台上。其中,对输出给控制台的信息有一个特定的优先级console_loglevel。只有打印信息的优先级小于这个整数值,信息才能被输出到虚拟控制台上,否则,信息仅仅被写入到系统日志文件中。若不加任何优先级选项,则消息默认输出到系统日志文件中。注意要开启klogd和syslogd服务,消息才能正常输出。4.proc文件系统/proc文件系统是一个伪文件系统,它是一种内核和内核模块用来向进程发送信息的机制。这个伪文件系统让用户可以和内核内部数据结构进行交互,获取有关系统和进程的有用信息,在运行时通过改变内核参数来改变设置。与其他文件系统不同,/proc存在于内存之中而不是在硬盘上。读者可以通过“ls”查看/proc文件系统的内容。表11.13列出了/proc文件系统的主要目录内容。表11.13 /proc文件系统主要目录内容目录名称目录内容目录名称目录内容apm高级电源管理信息locks内核锁cmdline内核命令行meminfo内存信息cpuinfoCPU相关信息misc杂项devices设备信息(块设备/字符设备)modules加载模块列表dma使用的DMA通道信息mounts加载的文件系统filesystems支持的文件系统信息partitions系统识别的分区表interrupts中断的使用信息rtc实时时钟ioportsI/O端口的使用信息stat全面统计状态表kcore内核映像swaps对换空间的利用情况kmsg内核消息version内核版本ksyms内核符号表uptime系统正常运行时间loadavg负载均衡……除此之外,还有一些是以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的PID号为目录名,它们是读取进程信息的接口。进程目录的结构如表11.14所示。表11.14 /proc中进程目录结构目录名称目录内容目录名称目录内容cmdline命令行参数cwd当前工作目录的链接environ环境变量值exe指向该进程的执行命令文件fd一个包含所有文件描述符的目录maps内存映像mem进程的内存被利用情况statm进程内存状态信息stat进程状态root链接此进程的root目录status进程当前状态,以可读的方式显示出来……用户可以使用cat命令来查看其中的内容。可以看到,/proc文件系统体现了内核及进程运行的内容,在加载模块成功后,读者可以通过查看/proc/device文件获得相关设备的主设备号。

    时间:2018-06-15 关键词: 操作系统 基础教程 设备驱动 嵌入式linux 字符设备驱动

  • 嵌入式Linux设备驱动开发之:GPIO驱动程序实例

    嵌入式Linux设备驱动开发之:GPIO驱动程序实例

    11.3GPIO驱动程序实例11.3.1GPIO工作原理FS2410开发板的S3C2410处理器具有117个多功能通用I/O(GPIO)端口管脚,包括GPIO8个端口组,分别为GPA(23个输出端口)、GPB(11个输入/输出端口)、GPC(16个输入/输出端口)、GPD(16个输入/输出端口)、GPE(16个输入/输出端口)、GPF(8个输入/输出端口)、GPH(11个输入/输出端口)。根据各种系统设计的需求,通过软件方法可以将这些端口配置成具有相应功能(例如:外部中断或数据总线)的端口。为了控制这些端口,S3C2410处理器为每个端口组分别提供几种相应的控制寄存器。其中最常用的有端口配置寄存器(GPACON~GPHCON)和端口数据寄存器(GPADAT~GPHDAT)。因为大部分I/O管脚可以提供多种功能,通过配置寄存器(PnCON)设定每个管脚用于何种目的。数据寄存器的每位将对应于某个管脚上的输入或输出。所以通过对数据寄存器(PnDAT)的位读写,可以进行对每个端口的输入或输出。在此主要以发光二极管(LED)和蜂鸣器为例,讨论GPIO设备的驱动程序。它们的硬件驱动电路的原理图如图11.4所示。 图11.4LED(左)和蜂鸣器(右)的驱动电路原理图在图11.4中,可知使用S3C2410处理器的通用I/O口GPF4、GPF5、GPF6和GPF7分别直接驱动LEDD12、D11、D10以及D9,而使用GPB0端口驱动蜂鸣器。4个LED分别在对应端口(GPF4~GPF7)为低电平时发亮,而蜂鸣器在GPB0为高电平时发声。这5个端口的数据流方向均为输出。在表11.15中,详细描述了GPF的主要控制寄存器。GPB的相关寄存器的描述与此类似,具体可以参考S3C2410处理器数据手册。表11.15 GPF端口(GPF0-GPF7)的主要控制寄存器寄存器地址R/W功能初始值GPFCON0x56000050R/W配置GPF端口组0x0GPFDAT0x56000054R/WGPF端口的数据寄存器未定义GPFUP0x56000058R/WGPF端口的取消上拉寄存器0x0GPFCON位描述GPF7[15:14]00=输入01=输出10=EINT711=保留GPF6[13:12]00=输入01=输出10=EINT611=保留GPF5[11:10]00=输入01=输出10=EINT511=保留GPF4[9:8]00=输入01=输出10=EINT411=保留GPF3[7:6]00=输入01=输出10=EINT311=保留GPF2[5:4]00=输入01=输出10=EINT211=保留GPF1[3:2]00=输入01=输出10=EINT111=保留GPF0[1:0]00=输入01=输出10=EINT011=保留GPFDAT位描述GPF[7:0][7:0]每位对应于相应的端口,若端口用于输入,则可以通过相应的位读取数据;若端口用于输出,则可以通过相应的位输出数据;若端口用于其他功能,则其值无法确定。GPFUP位描述GPF[7:0][7:0]0:向相应端口管脚赋予上拉(pull-up)功能1:取消上拉功能为了驱动LED和蜂鸣器,首先通过端口配置寄存器将5个相应寄存器配置为输出模式。然后通过对端口数据寄存器的写操作,实现对每个GPIO设备的控制(发亮或发声)。在下一个小节中介绍的驱动程序中,s3c2410_gpio_cfgpin()函数和s3c2410_gpio_pullup()函数将进行对某个端口的配置,而s3c2410_gpio_setpin()函数实现向数据寄存器的某个端口的输出。11.3.2GPIO驱动程序GPIO驱动程序代码如下所示:/*gpio_drv.h*/#ifndefFS2410_GPIO_SET_H#defineFS2410_GPIO_SET_H#include<linux/ioctl.h>#defineGPIO_DEVICE_NAME"gpio"#defineGPIO_DEVICE_FILENAME"/dev/gpio"#defineLED_NUM4#defineGPIO_IOCTL_MAGIC'G'#defineLED_D09_SWT_IOW(GPIO_IOCTL_MAGIC,0,unsignedint)#defineLED_D10_SWT_IOW(GPIO_IOCTL_MAGIC,1,unsignedint)#defineLED_D11_SWT_IOW(GPIO_IOCTL_MAGIC,2,unsignedint)#defineLED_D12_SWT_IOW(GPIO_IOCTL_MAGIC,3,unsignedint)#defineBEEP_SWT_IOW(GPIO_IOCTL_MAGIC,4,unsignedint)#defineLED_SWT_ON0#defineLED_SWT_OFF1#defineBEEP_SWT_ON1#defineBEEP_SWT_OFF0#endif/*FS2410_GPIO_SET_H*//*gpio_drv.c*/#include<linux/config.h>#include<linux/module.h>#include<linux/moduleparam.h>#include<linux/init.h>#include<linux/kernel.h>/*printk()*/#include<linux/slab.h>/*kmalloc()*/#include<linux/fs.h>/*everything...*/#include<linux/errno.h>/*errorcodes*/#include<linux/types.h>/*size_t*/#include<linux/mm.h>#include<linux/kdev_t.h>#include<linux/cdev.h>#include<linux/delay.h>#include<linux/device.h>#include<asm/io.h>#include<asm/uaccess.h>#include<asm/arch-s3c2410/regs-gpio.h>#include"gpio_drv.h"staticintmajor=0;/*采用字符设备号的动态分配*/module_param(major,int,0);/*以参数的方式可以指定设备的主设备号*/voids3c2410_gpio_cfgpin(unsignedintpin,unsignedintfunction){/*对某个管脚进行配置(输入/输出/其他功能)*/unsignedlongbase=S3C2410_GPIO_BASE(pin);/*获得端口的组基地址*/unsignedlongshift=1;unsignedlongmask=0x03;/*通常用配置寄存器的两位表示一个端口*/unsignedlongcon;unsignedlongflags;if(pin<S3C2410_GPIO_BANKB){shift=0;mask=0x01;/*在GPA端口中用配置寄存器的一位表示一个端口*/}mask<<=(S3C2410_GPIO_OFFSET(pin)<<shift);local_irq_save(flags);/*保存现场,保证下面一段是原子操作*/con=__raw_readl(base+0x00);con&=~mask;con|=function;__raw_writel(con,base+0x00);/*向配置寄存器写入新配置数据*/local_irq_restore(flags);/*恢复现场*/}voids3c2410_gpio_pullup(unsignedintpin,unsignedintto){/*配置上拉功能*/unsignedlongbase=S3C2410_GPIO_BASE(pin);/*获得端口的组基地址*/unsignedlongoffs=S3C2410_GPIO_OFFSET(pin);/*获得端口的组内偏移地址*/unsignedlongflags;unsignedlongup;if(pin<S3C2410_GPIO_BANKB){return;}local_irq_save(flags);up=__raw_readl(base+0x08);up&=~(1<<offs);up|=to<<offs;__raw_writel(up,base+0x08);/*向上拉功能寄存器写入新配置数据*/local_irq_restore(flags);}voids3c2410_gpio_setpin(unsignedintpin,unsignedintto){/*向某个管脚进行输出*/unsignedlongbase=S3C2410_GPIO_BASE(pin);unsignedlongoffs=S3C2410_GPIO_OFFSET(pin);unsignedlongflags;unsignedlongdat;local_irq_save(flags);dat=__raw_readl(base+0x04);dat&=~(1<<offs);dat|=to<<offs;__raw_writel(dat,base+0x04);/*向数据寄存器写入新数据*/local_irq_restore(flags);}intgpio_open(structinode*inode,structfile*filp){/*open操作函数:进行寄存器配置*/s3c2410_gpio_pullup(S3C2410_GPB0,1);/*BEEP*/s3c2410_gpio_pullup(S3C2410_GPF4,1);/*LEDD12*/s3c2410_gpio_pullup(S3C2410_GPF5,1);/*LEDD11*/s3c2410_gpio_pullup(S3C2410_GPF6,1);/*LEDD10*/s3c2410_gpio_pullup(S3C2410_GPF7,1);/*LEDD9*/s3c2410_gpio_cfgpin(S3C2410_GPB0,S3C2410_GPB0_OUTP);s3c2410_gpio_cfgpin(S3C2410_GPF4,S3C2410_GPF4_OUTP);s3c2410_gpio_cfgpin(S3C2410_GPF4,S3C2410_GPF5_OUTP);s3c2410_gpio_cfgpin(S3C2410_GPF4,S3C2410_GPF6_OUTP);s3c2410_gpio_cfgpin(S3C2410_GPF4,S3C2410_GPF7_OUTP);return0;}ssize_tgpio_read(structfile*file,char__user*buff,size_tcount,loff_t*offp){/*read操作函数:没有实际功能*/return0;}ssize_tgpio_write(structfile*file,constchar__user*buff,size_tcount,loff_t*offp){/*write操作函数:没有实际功能*/return0;}intswitch_gpio(unsignedintpin,unsignedintswt){/*向5个端口中的一个输出ON/OFF值*/if(!((pin<=S3C2410_GPF7)&&(pin>=S3C2410_GPF4))&&(pin!=S3C2410_GPB0)){printk("Unsupportedpin");return1;}s3c2410_gpio_setpin(pin,swt);return0;}staticintgpio_ioctl(structinode*inode,structfile*file,unsignedintcmd,unsignedlongarg){/*ioctl函数接口:主要接口的实现。对5个GPIO设备进行控制(发亮或发声)*/unsignedintswt=(unsignedint)arg;switch(cmd){caseLED_D09_SWT:{switch_gpio(S3C2410_GPF7,swt);}break;caseLED_D10_SWT:{switch_gpio(S3C2410_GPF6,swt);}break;caseLED_D11_SWT:{switch_gpio(S3C2410_GPF5,swt);}break;caseLED_D12_SWT:{switch_gpio(S3C2410_GPF4,swt);}break;caseBEEP_SWT:{switch_gpio(S3C2410_GPB0,swt);break;}default:{printk("Unsupportedcommandn");break;}}return0;}staticintgpio_release(structinode*node,structfile*file){/*release操作函数,熄灭所有灯和关闭蜂鸣器*/switch_gpio(S3C2410_GPB0,BEEP_SWT_OFF);switch_gpio(S3C2410_GPF4,LED_SWT_OFF);switch_gpio(S3C2410_GPF5,LED_SWT_OFF);switch_gpio(S3C2410_GPF6,LED_SWT_OFF);switch_gpio(S3C2410_GPF7,LED_SWT_OFF);return0;}staticvoidgpio_setup_cdev(structcdev*dev,intminor,structfile_operations*fops){/*字符设备的创建和注册*/interr,devno=MKDEV(major,minor);cdev_init(dev,fops);dev->owner=THIS_MODULE;dev->ops=fops;err=cdev_add(dev,devno,1);if(err){printk(KERN_NOTICE"Error%daddinggpio%d",err,minor);}}staticstructfile_operationsgpio_fops={/*gpio设备的file_operations结构定义*/.owner=THIS_MODULE,.open=gpio_open,/*进行初始化配置*/.release=gpio_release,/*关闭设备*/.read=gpio_read,.write=gpio_write,.ioctl=gpio_ioctl,/*实现主要控制功能*/};staticstructcdevgpio_devs;staticintgpio_init(void){intresult;dev_tdev=MKDEV(major,0);if(major){/*设备号的动态分配*/result=register_chrdev_region(dev,1,GPIO_DEVICE_NAME);}else{/*设备号的动态分配*/result=alloc_chrdev_region(&dev,0,1,GPIO_DEVICE_NAME);major=MAJOR(dev);}if(result<0){printk(KERN_WARNING"Gpio:unabletogetmajor%dn",major);returnresult;}gpio_setup_cdev(&gpio_devs,0,&gpio_fops);printk("Themajorofthegpiodeviceis%dn",major);return0;}staticvoidgpio_cleanup(void){cdev_del(&gpio_devs);/*字符设备的注销*/unregister_chrdev_region(MKDEV(major,0),1);/*设备号的注销*/printk("Gpiodeviceuninstalledn");}module_init(gpio_init);module_exit(gpio_cleanup);MODULE_AUTHOR("David");MODULE_LICENSE("DualBSD/GPL");下面列出GPIO驱动程序的测试用例:/*gpio_test.c*/#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<fcntl.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include"gpio_drv.h"intled_timer(intdev_fd,intled_no,unsignedinttime){/*指定LED发亮一段时间之后熄灭它*/led_no%=4;ioctl(dev_fd,LED_D09_SWT+led_no,LED_SWT_ON);/*发亮*/sleep(time);ioctl(dev_fd,LED_D09_SWT+led_no,LED_SWT_OFF);/*熄灭*/}intbeep_timer(intdev_fd,unsignedinttime){/*开蜂鸣器一段时间之后关闭*/ioctl(dev_fd,BEEP_SWT,BEEP_SWT_ON);/*发声*/sleep(time);ioctl(dev_fd,BEEP_SWT,BEEP_SWT_OFF);/*关闭*/}intmain(){inti=0;intdev_fd;/*打开gpio设备*/dev_fd=open(GPIO_DEVICE_FILENAME,O_RDWR|O_NONBLOCK);if(dev_fd==-1){printf("Cann'topengpiodevicefilen");exit(1);}while(1){i=(i+1)%4;led_timer(dev_fd,i,1);beep_timer(dev_fd,1);}close(dev_fd);return0;}具体运行过程如下所示。首先编译并加载驱动程序:$makeclean;make/*驱动程序的编译*/$insmodgpio_drv.ko/*加载gpio驱动*/$cat/proc/devices/*通过这个命令可以查到gpio设备的主设备号*/$mknod/dev/gpioc2520/*假设主设备号为252,创建设备文件节点*/然后编译并运行驱动测试程序:$arm-linux-gcc–ogpio_testgpio_test.c$./gpio_test运行结果为4个LED轮流闪烁,同时蜂鸣器以一定周期发出声响。

    时间:2018-06-15 关键词: 操作系统 基础教程 设备驱动 嵌入式linux gpio驱动程序

  • 嵌入式Linux设备驱动开发之:块设备驱动编程

    嵌入式Linux设备驱动开发之:块设备驱动编程

    11.4块设备驱动编程块设备通常指一些需要以块(如512字节)的方式写入的设备,如IDE硬盘、SCSI硬盘、光驱等。它的驱动程序的编写过程与字符型设备驱动程序的编写有很大的区别。块设备驱动编程接口相对复杂,不如字符设备明晰易用。块设备驱动程序对整个系统的性能影响较大,速度和效率是设计块设备驱动程要重点考虑的问题。系统中使用缓冲区与访问请求的优化管理(合并与重新排序)来提高系统性能。1.编程流程说明块设备驱动程序的编写流程同字符设备驱动程序的编写流程很类似,也包括了注册和使用两部分。但与字符驱动设备所不同的是,块设备驱动程序包括一个request请求队列。它是当内核安排一次数据传输时在列表中的一个请求队列,以最大化系统性能为原则进行排序。在后面的读写操作时会详细讲解这个函数,图11.5为块设备驱动程序的流程图,请读者注意与字符设备驱动程序的区别。图11.5块设备驱动程序流程图2.重要数据结构每个块设备物理实体由一个gendisk结构体来表示(在</linux/genhd.h>中定义),每个gendisk可以支持多个分区。每个gendisk中包含了本物理实体的全部信息以及操作函数接口。整个块设备的注册过程是围绕gendisk来展开的。在驱动程序中需要初始化的gendisk的一些成员如下所示。structgendisk{intmajor;/*主设备号*/intfirst_minor;/*第一个次设备号*/intminors;/*次设备号个数,一个块设备至少需要使用一个次设备号,而且块设备的每个分区都需要一个次设备号,因此这个成员等于1,则表明该块设备是不可被分区的,否则可以包含minors–1个分区。*/chardisk_name[32];/*块设备名称,在/proc/partions中显示*/structhd_struct**part;/*分区表*/structblock_device_operations*fops;/*块设备操作接口,与字符设备的 file_operations结构对应*/structrequest_queue*queue;/*I/O请求队列*/void*private_data;/*指向驱动程序私有数据*/sector_tcapacity;/*块设备可包含的扇区数*/……/*其他省略*/};与字符设备驱动程序一样,块设备驱动程序也包含一个在<linux/fs.h>中定义的block_device_operations结构,其定义如下所示。structblock_device_operations{int(*open)(structinode*,structfile*);int(*release)(structinode*,structfile*);int(*ioctl)(structinode*,structfile*,unsigned,unsignedlong);long(*unlocked_ioctl)(structfile*,unsigned,unsignedlong);long(*compat_ioctl)(structfile*,unsigned,unsignedlong);int(*direct_access)(structblock_device*,sector_t,unsignedlong*);int(*media_changed)(structgendisk*);int(*revalidate_disk)(structgendisk*);int(*getgeo)(structblock_device*,structhd_geometry*);structmodule*owner;};从该结构的定义中,可以看出块设备并不提供read()、write()等函数接口。对块设备的读写请求都是以异步方式发送到设备相关的request队列之中。3.块设备注册和初始化块设备的初始化过程要比字符设备复杂,它既需要像字符设备一样在加载内核时完成一定的工作,还需要在内核编译时增加一些内容。块设备驱动程序初始化时,由驱动程序的init()完成。块设备的初始化过程如图11.6所示。图11.6块设备驱动程序初始化过程(1)向内核注册。使用register_blkdev()函数对设备进行注册。intregister_blkdev(unsignedintmajor,constchar*name);其中参数major为要注册的块设备的主设备号,如果其值等于0,则系统动态分配并返回主设备号。参数name为设备名,在/proc/devices中显示。如果出错,则该函数返回负值。与其对应的块设备的注销函数为unregister_blkdev(),其格式如下所示。intunregister_blkdev(unsignedintmajor,constchar*name);其参数必须与注册函数中的参数相同。如果出错则返回负值。(2)申请并初始化请求队列。这一步要调用blk_init_queue()函数来申请并初始化请求队列,其格式如下所示。structrequest_queue*blk_init_queue(request_fn_proc*rfn,spinlock_t*lock)其中参数rfn是请求队列的处理函数指针,它负责执行块设备的读、写请求。参数lock为自旋锁,用于控制对所分配的队列的访问。(3)初始化并注册gendisk结构。内核提供的gendisk结构相关函数如表11-16所示。表11-16 gendisk结构相关函数函数格式说明structgendisk*alloc_disk(intminors)动态分配gendisk结构,参数为次设备号的个数voidadd_disk(structgendisk*disk)向系统注册gendisk结构voiddel_gendisk(structgendisk*disk)从系统注销gendisk结构首先使用alloc_disk()函数动态分配gendisk结构,接下来,对gendisk结构的主设备号(major)、次设备号相关成员(first_minor和minors)、块设备操作函数(fops)、请求队列(queue)、可包含的扇区数(capacity)以及设备名称(disk_name)等成员进行初始化。在完成对gendisk的分配和初始化之后,调用add_disk()函数向系统注册块设备。在卸载gendisk结构的时候,要调用del_gendisk()函数。4.块设备请求处理块设备驱动中一般要实现一个请求队列处理函数来处理队列中的请求。从块设备的运行流程,可知请求处理是块设备的基本处理单位,也是最核心的部分。对块设备的读写操作被封装到了每一个请求中。已经提过调用blk_init_queue()函数来申请并初始化请求队列。表11-17列出了一些与请求处理相关的函数。表11-17 请求处理相关函数函数格式说明request_queue_t*blk_alloc_queue(intgfp_mask)分配请求队列request_queue_t*blk_init_queue(request_fn_proc*rfn,spinlock_t*lock)分配并初始化请求队列structrequest*blk_get_request(request_queue_t*q,intrw,intgfp_mask)从队列中获取一个请求voidblk_requeue_request(request_queue_t*q,structrequest*rq)将请求再次加入队列voidblk_queue_max_sectors(request_queue_t*q,unsignedshortmax_sectors)设置最大访问扇区数voidblk_queue_max_phys_segments(request_queue_t*q,unsignedshortmax_segments)设置最大物理段数voidend_request(structrequest*req,intuptodate)结束本次请求处理voidblk_queue_hardsect_size(request_queue_t*q,unsignedshortsize)设置物理扇区大小以上简单地介绍了块设备驱动编程的最基本的概念和流程。更深入的内容不是本书的重点,有兴趣的读者可以参考其他书籍。

    时间:2018-06-14 关键词: 基础教程 设备驱动 嵌入式linux 块设备驱动编程

  • 嵌入式Linux设备驱动开发之:中断编程

    嵌入式Linux设备驱动开发之:中断编程

    11.5中断编程前面所讲述的驱动程序中都没有涉及中断处理,而实际上,有很多Linux的驱动都是通过中断的方式来进行内核和硬件的交互。中断机制提供了硬件和软件之间异步传递信息的方式。硬件设备在发生某个事件时通过中断通知软件进行处理。中断实现了硬件设备按需获得处理器关注的机制,与查询方式相比可以大大节省CPU资源的开销。在此将介绍在驱动程序中用于申请中断的request_irq()调用,和用于释放中断的free_irq()调用。request_irq()函数调用的格式如下所示:intrequest_irq(unsignedintirq,void(*handler)(intirq,void*dev_id,structpt_regs*regs),unsignedlongirqflags,constchar*devname,oid*dev_id);其中irq是要申请的硬件中断号。在Intel平台,范围是0~15。参数handler为将要向系统注册的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件中断号、设备id以及寄存器值。设备id就是在调用request_irq()时传递给系统的参数dev_id。参数irqflags是中断处理的一些属性,其中比较重要的有SA_INTERRUPT。这个参数用于标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序只屏蔽正在处理的中断。还有一个SA_SHIRQ属性,设置了以后运行多个设备共享中断,在中断处理程序中根据dev_id区分不同设备产生的中断。参数devname为设备名,会在/dev/interrupts中显示。参数dev_id在中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用irq2dev_map()找到中断对应的设备。释放中断的free_irq()函数调用的格式如下所示。该函数的参数与request_irq()相同。

    时间:2018-06-14 关键词: 操作系统 基础教程 设备驱动 嵌入式linux 中断编程

  • 嵌入式Linux设备驱动开发之:按键驱动程序实例

    嵌入式Linux设备驱动开发之:按键驱动程序实例

    11.6按键驱动程序实例11.6.1按键工作原理LED和蜂鸣器是最简单的GPIO的应用,都不需要任何外部输入或控制。按键同样使用GPIO接口,但按键本身需要外部的输入,即在驱动程序中要处理外部中断。按键硬件驱动原理图如图11-7所示。在图11-7的4×4矩阵按键(K1~K16)电路中,使用4个输入/输出端口(EINT0、EINT2、EINT11和EINT19)和4个输出端口(KSCAN0~KSCAN3)。图11.7按键驱动电路原理图按键驱动电路使用的端口和对应的寄存器如表11-18所示。表11.18 按键电路的主要端口管脚端口输入/输出管脚端口输入/输出KEYSCAN0GPE11输出EINT0EINIT0/GPF0输入/输出KEYSCAN1GPG6输出EINT2EINT2/GPF2输入/输出KEYSCAN2GPE13输出EINT11EINT11/GPG3输入/输出KEYSCAN3GPG2输出EINT19EINT19/GPG11输入/输出因为通常中断端口是比较珍贵且有限的资源,所以在本电路设计中,16个按键复用了4个中断线。那怎么样才能及时而准确地对矩阵按键进行扫描呢?某个中断的产生表示,与它所对应的矩阵行的4个按键中,至少有一个按键被按住了。因此可以通过查看产生了哪个中断,来确定在矩阵的哪一行中发生了按键操作(按住或释放)。例如,如果产生了外部2号线中断(EINT2变为低电平),则表示K7、K8、K9和K15中至少有一个按键被按住了。这时候4个EINT端口应该通过GPIO配置寄存器被设置为外部中断端口,而且4个KSCAN端口的输出必须为低电平。在确定按键操作所在行的位置之后,我们还得查看按键操作所在列的位置。此时要使用KSCAN端口组,同时将4个EINT端口配置为通用输入端口(而不是中断端口)。在4个KSCAN端口中,轮流将其中某一个端口的输出置为低电平,其他3个端口的输出置为高电平。这样逐列进行扫描,直到按键所在列的KSCAN端口输出为低电平,此时按键操作所在行的EINT管脚的输入端口的值会变成低电平。例如,在确认产生了外部2号中断之后,进行逐列扫描。若发现在KSCAN1为低电平时(其他端口输出均为高电平),GPF2(EINT2管脚的输入端口)变为低电平,则可以断定按键K8被按住了。以上的讨论都是在按键的理想状态下进行的,但实际的按键动作会在短时间(几毫秒至几十毫秒)内产生信号抖动。例如,当按键被按下时,其动作就像弹簧的若干次往复运动,将产生几个脉冲信号。一次按键操作将会产生若干次按键中断,从而会产生抖动现象。因此驱动程序中必须要解决去除抖动所产生的毛刺信号的问题。11.6.2按键驱动程序首先按键设备相关的数据结构的定义如下所示:/*butt_drv.h*/……typedefstruct_st_key_info_matrix/*按键数据结构*/{unsignedcharkey_id;/*按键ID*/unsignedintirq_no;/*对应的中断号*/unsignedintirq_gpio_port;/*对应的中断线的输入端口地址*/unsignedintkscan_gpio_port;/*对应的KSCAN端口地址*/}st_key_info_matrix;typedefstruct_st_key_buffer/*按键缓冲数据结构*/{unsignedlongjiffy[MAX_KEY_COUNT];/*按键时间,5s以前的铵键作废*/unsignedcharbuf[MAX_KEY_COUNT];/*按键缓冲区*/unsignedinthead,tail;/*按键缓冲区头和尾*/}st_key_buffer;……下面是矩阵按键数组的定义,数组元素的信息(一个按键信息)按照0行0列,0行1列,…,3行2列,3行3列的顺序逐行排列。staticst_key_info_matrixkey_info_matrix[MAX_COLUMN][MAX_ROW]={{{10,IRQ_EINT0,S3C2410_GPF0,S3C2410_GPE11},/*0行0列*/{11,IRQ_EINT0,S3C2410_GPF0,S3C2410_GPG6},{12,IRQ_EINT0,S3C2410_GPF0,S3C2410_GPE13},{16,IRQ_EINT0,S3C2410_GPF0,S3C2410_GPG2}},{{7,IRQ_EINT2,S3C2410_GPF2,S3C2410_GPE11},/*1行0列*/{8,IRQ_EINT2,S3C2410_GPF2,S3C2410_GPG6},{9,IRQ_EINT2,S3C2410_GPF2,S3C2410_GPE13},{15,IRQ_EINT2,S3C2410_GPF2,S3C2410_GPG2}},{{4,IRQ_EINT11,S3C2410_GPG3,S3C2410_GPE11},/*2行0列*/{5,IRQ_EINT11,S3C2410_GPG3,S3C2410_GPG6},{6,IRQ_EINT11,S3C2410_GPG3,S3C2410_GPE13},{14,IRQ_EINT11,S3C2410_GPG3,S3C2410_GPG2}},{{1,IRQ_EINT19,S3C2410_GPG11,S3C2410_GPE11},/*3行0列*/{2,IRQ_EINT19,S3C2410_GPG11,S3C2410_GPG6},{3,IRQ_EINT19,S3C2410_GPG11,S3C2410_GPE13},{13,IRQ_EINT19,S3C2410_GPG11,S3C2410_GPG2}},};下面是与按键相关的端口的初始化函数。这些函数已经在简单的GPIO字符设备驱动程序里被使用过。此外,set_irq_type()函数用于设定中断线的类型,在本实例中通过该函数将4个中断线的类型配置为下降沿触发式。staticvoidinit_gpio(void){s3c2410_gpio_cfgpin(S3C2410_GPE11,S3C2410_GPE11_OUTP);/*GPE11*/s3c2410_gpio_setpin(S3C2410_GPE11,0);s3c2410_gpio_cfgpin(S3C2410_GPE13,S3C2410_GPE13_OUTP);/*GPE13*/s3c2410_gpio_setpin(S3C2410_GPE13,0);s3c2410_gpio_cfgpin(S3C2410_GPG2,S3C2410_GPG2_OUTP);/*GPG2*/s3c2410_gpio_setpin(S3C2410_GPG2,0);s3c2410_gpio_cfgpin(S3C2410_GPG6,S3C2410_GPG6_OUTP);/*GPG6*/s3c2410_gpio_setpin(S3C2410_GPG6,0);s3c2410_gpio_cfgpin(S3C2410_GPF0,S3C2410_GPF0_EINT0);/*GPF0*/s3c2410_gpio_cfgpin(S3C2410_GPF2,S3C2410_GPF2_EINT2);/*GPF2*/s3c2410_gpio_cfgpin(S3C2410_GPG3,S3C2410_GPG3_EINT11);/*GPG3*/s3c2410_gpio_cfgpin(S3C2410_GPG11,S3C2410_GPG11_EINT19);/*GPG11*/set_irq_type(IRQ_EINT0,IRQT_FALLING);set_irq_type(IRQ_EINT2,IRQT_FALLING);set_irq_type(IRQ_EINT11,IRQT_FALLING);set_irq_type(IRQ_EINT19,IRQT_FALLING);}下面讲解按键驱动的主要接口,以下为驱动模块的入口和卸载函数。/*初始化并添加structcdev结构到系统之中*/staticvoidbutton_setup_cdev(structcdev*dev,intminor,structfile_operations*fops){interr;intdevno=MKDEV(button_major,minor);cdev_init(dev,fops);/*初始化结构体structcdev*/dev->owner=THIS_MODULE;dev->ops=fops;/*关联到设备的file_operations结构*/err=cdev_add(dev,devno,1);/*将structcdev结构添加到系统之中*/if(err){printk(KERN_INFO"Error%daddingbutton%dn",err,minor);}}……/*驱动初始化*/staticintbutton_init(void){intret;/*将主设备号和次设备号定义到一个dev_t数据类型的结构体之中*/dev_tdev=MKDEV(button_major,0);if(button_major){/*静态注册一个设备,设备号先前指定好,并设定设备名,用cat/proc/devices来查看*/ret=register_chrdev_region(dev,1,BUTTONS_DEVICE_NAME);}else{/*由系统动态分配主设备号*/ret=alloc_chrdev_region(&dev,0,1,BUTTONS_DEVICE_NAME);button_major=MAJOR(dev);/*获得主设备号*/}if(ret<0){printk(KERN_WARNING"Button:unabletogetmajor%dn",button_major);returnret;}/*初始化和添加结构体structcdev到系统之中*/button_setup_cdev(&button_dev,0,&button_fops);printk("Buttondriverinitialized.n");return0;}/*驱动卸载*/staticvoid__exitbutton_exit(void){cdev_del(&button_dev);/*删除结构体structcdev*//*卸载设备驱动所占有的资源*/unregister_chrdev_region(MKDEV(button_major,0),1);printk("Buttondriveruninstalledn");}module_init(button_init);/*初始化设备驱动程序的入口*/module_exit(button_exit);/*卸载设备驱动程序的入口*/MODULE_AUTHOR("David");MODULE_LICENSE("DualBSD/GPL");按键字符设备的file_operations结构定义为:staticstructfile_operationsbutton_fops={.owner=THIS_MODULE,.ioctl=button_ioctl,.open=button_open,.read=button_read,.release=button_release,};以下为open和release函数接口的实现。/*打开文件,申请中断*/staticintbutton_open(structinode*inode,structfile*filp){intret=nonseekable_open(inode,filp);if(ret<0){returnret;}init_gpio();/*相关GPIO端口的初始化*/ret=request_irqs();/*申请4个中断*/if(ret<0){returnret;}init_keybuffer();/*初始化按键缓冲数据结构*/returnret;}/*关闭文件,屏蔽中断*/staticintbutton_release(structinode*inode,structfile*filp){free_irqs();/*屏蔽中断*/return0;}在open函数接口中,进行了GPIO端口的初始化、申请硬件中断以及按键缓冲的初始化等工作。在以前的章节中提过,中断端口是比较宝贵而且数量有限的资源。因此需要注意,最好要在第一次打开设备时申请(调用request_irq函数)中断端口,而不是在驱动模块加载的时候申请。如果已加载的设备驱动占用而在一定时间段内不使用某些中断资源,则这些资源不会被其他驱动所使用,只能白白浪费掉。而在打开设备的时候(调用open函数接口)申请中断,则不同的设备驱动可以共享这些宝贵的中断资源。以下为中断申请和释放的部分以及中断处理函数。/*中断处理函数,其中irq为中断号*/staticirqreturn_tbutton_irq(intirq,void*dev_id,structpt_regs*regs){unsignedcharucKey=0;disable_irqs();/*屏蔽中断*//*延迟50ms,屏蔽按键毛刺*/udelay(50000);ucKey=button_scan(irq);/*扫描按键,获得进行操作的按键的ID*/if((ucKey>=1)&&(ucKey<=16)){/*如果缓冲区已满,则不添加*/if(((key_buffer.head+1)&(MAX_KEY_COUNT-1))!=key_buffer.tail){spin_lock_irq(&buffer_lock);key_buffer.buf[key_buffer.tail]=ucKey; key_buffer.jiffy[key_buffer.tail]=get_tick_count();key_buffer.tail++;key_buffer.tail&=(MAX_KEY_COUNT-1);spin_unlock_irq(&buffer_lock);}}init_gpio();/*初始化GPIO端口,主要是为了恢复中断端口配置*/enable_irqs();/*开启中断*/returnIRQ_HANDLED;/*2.6内核返回值一般是这个宏*/}/*申请4个中断*/staticintrequest_irqs(void){intret,i,j;for(i=0;i<MAX_COLUMN;i++){ret=request_irq(key_info_matrix[i][0].irq_no,button_irq,SA_INTERRUPT,BUTTONS_DEVICE_NAME,NULL);if(ret<0){for(j=0;j<i;j++){free_irq(key_info_matrix[j][0].irq_no,NULL);}return-EFAULT;}}return0;}/*释放中断*/static__inlinevoidfree_irqs(void){inti;for(i=0;i<MAX_COLUMN;i++){free_irq(key_info_matrix[i][0].irq_no,NULL);}}中断处理函数在每次中断产生的时候会被调用,因此它的执行时间要尽可能得短。通常中断处理函数只是简单地唤醒等待资源的任务,而复杂且耗时的工作则让这个任务去完成。中断处理函数不能向用户空间发送数据或者接收数据,不能做任何可能发生睡眠的操作,而且不能调用schedule()函数。为了简单起见,而且考虑到按键操作的时间比较长,在本实例中的中断处理函数button_irq()里,通过调用睡眠函数来消除毛刺信号。读者可以根据以上介绍的对中断处理函数的要求改进该部分代码。按键扫描函数如下所示。首先根据中断号确定操作按键所在行的位置,然后采用逐列扫描法最终确定操作按键所在的位置。/***进入中断后,扫描铵键码**返回:按键码(1~16),0xff表示错误*/static__inlineunsignedcharbutton_scan(intirq){unsignedcharkey_id=0xff;unsignedcharcolumn=0xff,row=0xff;s3c2410_gpio_cfgpin(S3C2410_GPF0,S3C2410_GPF0_INP);/*GPF0*/s3c2410_gpio_cfgpin(S3C2410_GPF2,S3C2410_GPF2_INP);/*GPF2*/s3c2410_gpio_cfgpin(S3C2410_GPG3,S3C2410_GPG3_INP);/*GPG3*/s3c2410_gpio_cfgpin(S3C2410_GPG11,S3C2410_GPG11_INP);/*GPG11*/switch(irq){/*根据irq值确定操作按键所在行的位置*/caseIRQ_EINT0:{column=0;}break;caseIRQ_EINT2:{column=1;}break;caseIRQ_EINT11:{column=2;}break;caseIRQ_EINT19:{column=3;}break;}if(column!=0xff){/*开始逐列扫描,扫描第0列*/s3c2410_gpio_setpin(S3C2410_GPE11,0);/*将KSCAN0置为低电平*/s3c2410_gpio_setpin(S3C2410_GPG6,1);s3c2410_gpio_setpin(S3C2410_GPE13,1);s3c2410_gpio_setpin(S3C2410_GPG2,1);if(!s3c2410_gpio_getpin(key_info_matrix[column][0].irq_gpio_port)){/*观察对应的中断线的输入端口值*/key_id=key_info_matrix[column][0].key_id;returnkey_id;}/*扫描第1列*/s3c2410_gpio_setpin(S3C2410_GPE11,1);s3c2410_gpio_setpin(S3C2410_GPG6,0);/*将KSCAN1置为低电平*/s3c2410_gpio_setpin(S3C2410_GPE13,1);s3c2410_gpio_setpin(S3C2410_GPG2,1);if(!s3c2410_gpio_getpin(key_info_matrix[column][1].irq_gpio_port)){key_id=key_info_matrix[column][1].key_id;returnkey_id;}/*扫描第2列*/s3c2410_gpio_setpin(S3C2410_GPE11,1);s3c2410_gpio_setpin(S3C2410_GPG6,1);s3c2410_gpio_setpin(S3C2410_GPE13,0);/*将KSCAN2置为低电平*/s3c2410_gpio_setpin(S3C2410_GPG2,1);if(!s3c2410_gpio_getpin(key_info_matrix[column][2].irq_gpio_port)){key_id=key_info_matrix[column][2].key_id;returnkey_id;}/*扫描第3列*/s3c2410_gpio_setpin(S3C2410_GPE11,1);s3c2410_gpio_setpin(S3C2410_GPG6,1);s3c2410_gpio_setpin(S3C2410_GPE13,1);s3c2410_gpio_setpin(S3C2410_GPG2,0);/*将KSCAN3置为低电平*/if(!s3c2410_gpio_getpin(key_info_matrix[column][3].irq_gpio_port)){key_id=key_info_matrix[column][3].key_id;returnkey_id;}}returnkey_id;}以下是read函数接口的实现。首先在按键缓冲中删除已经过时的按键操作信息,接下来,从按键缓冲中读取一条信息(按键ID)并传递给用户层。/*从缓冲删除过时数据(5s前的按键值)*/staticvoidremove_timeoutkey(void){unsignedlongtick;spin_lock_irq(&buffer_lock);/*获得一个自旋锁*/while(key_buffer.head!=key_buffer.tail){tick=get_tick_count()-key_buffer.jiffy[key_buffer.head];if(tick<5000)/*5s*/break;key_buffer.buf[key_buffer.head]=0;key_buffer.jiffy[key_buffer.head]=0;key_buffer.head++;key_buffer.head&=(MAX_KEY_COUNT-1);}spin_unlock_irq(&buffer_lock);/*释放自旋锁*/}/*读键盘*/staticssize_tbutton_read(structfile*filp,char*buffer,size_tcount,loff_t*f_pos){ssize_tret=0;remove_timeoutkey();/*删除过时的按键操作信息*/spin_lock_irq(&buffer_lock);while((key_buffer.head!=key_buffer.tail)&&(((size_t)ret)<count)){put_user((char)(key_buffer.buf[key_buffer.head]),&buffer[ret]);key_buffer.buf[key_buffer.head]=0;key_buffer.jiffy[key_buffer.head]=0;key_buffer.head++;key_buffer.head&=(MAX_KEY_COUNT-1);ret++;}spin_unlock_irq(&buffer_lock);returnret;}以上介绍了按键驱动程序中的主要内容。11.6.3按键驱动的测试程序按键驱动程序的测试程序所下所示。在测试程序中,首先打开按键设备文件和gpio设备(包括4个LED和蜂鸣器)文件,接下来,根据按键的输入值(按键ID)的二进制形式,LEDD9~D12发亮(例如,按下11号按键,则D9、D10和D12会发亮),而蜂鸣器当每次按键时发出声响。/*butt_test.c*/#include<sys/stat.h>#include<fcntl.h>#include<stdio.h>#include<sys/time.h>#include<sys/types.h>#include<unistd.h>#include<asm/delay.h>#include"butt_drv.h"#include"gpio_drv.h"main(){intbutt_fd,gpios_fd,i;unsignedcharkey=0x0;butt_fd=open(BUTTONS_DEVICE_FILENAME,O_RDWR);/*打开按钮设备*/if(butt_fd==-1){printf("Openbuttondevicebuttonerrr!n");return0;}gpios_fd=open(GPIO_DEVICE_FILENAME,O_RDWR);/*打开GPIO设备*/if(gpios_fd==-1){printf("Openbuttondevicebuttonerrr!n");return0;}ioctl(butt_fd,0);/*清空键盘缓冲区,后面参数没有意义*/printf("PressNo.16keytoexitn");do{if(read(butt_fd,&key,1)<=0)/*读键盘设备,得到相应的键值*/{continue;}printf("KeyValue=%dn",key);for(i=0;i<LED_NUM;i++){if((key&(1<<i))!=0){ioctl(gpios_fd,LED_D09_SWT+i,LED_SWT_ON);/*LED发亮*/}}ioctl(gpios_fd,BEEP_SWT,BEEP_SWT_ON);/*发声*/sleep(1);for(i=0;i<LED_NUM;i++){ioctl(gpios_fd,LED_D09_SWT+i,LED_SWT_OFF);/*LED熄灭*/}ioctl(gpios_fd,BEEP_SWT,BEEP_SWT_OFF);}while(key!=16);/*按16号键则退出*/close(gpios_fd);close(butt_fd);return0;}首先编译和加载按键驱动程序,而且要创建设备文件节点。$makeclean;make/*驱动程序的编译*/$insmodbutt_dev.ko/*加载buttons设备驱动*/$cat/proc/devices/*通过这个命令可以查到buttons设备的主设备号*/$mknod/dev/buttonsc2520/*假设主设备号为252,创建设备文件节点*/接下来,编译和加载GPIO驱动程序,而且要创建设备文件节点。$makeclean;make/*驱动程序的编译*/$insmodgpio_drv.ko/*加载GPIO驱动*/$cat/proc/devices/*通过这个命令可以查到GPIO设备的主设备号*/$mknod/dev/gpioc2510/*假设主设备号为251,创建设备文件节点*/然后编译并运行驱动测试程序。$arm-linux-gcc–obutt_testbutt_test.c$./butt_test

    时间:2018-06-14 关键词: 操作系统 基础教程 设备驱动 嵌入式linux 按键驱动程序

  • 嵌入式Linux设备驱动开发之:实验内容——test驱动

    嵌入式Linux设备驱动开发之:实验内容——test驱动

    11.7实验内容——test驱动1.实验目的该实验是编写最简单的字符驱动程序,这里的设备也就是一段内存,实现简单的读写功能,并列出常用格式的Makefile以及驱动的加载和卸载脚本。读者可以熟悉字符设备驱动的整个编写流程。2.实验内容该实验要求实现对虚拟设备(一段内存)的打开、关闭、读写的操作,并要通过编写测试程序来测试虚拟设备及其驱动运行是否正常。3.实验步骤(1)编写代码。这个简单的驱动程序的源代码如下所示:/*test_drv.c*/#include<linux/module.h>#include<linux/init.h>#include<linux/fs.h>#include<linux/kernel.h>#include<linux/slab.h>#include<linux/types.h>#include<linux/errno.h>#include<linux/cdev.h>#include<asm/uaccess.h>#defineTEST_DEVICE_NAME"test_dev"#defineBUFF_SZ1024/*全局变量*/staticstructcdevtest_dev;unsignedintmajor=0;staticchar*data=NULL;/*读函数*/staticssize_ttest_read(structfile*file,char*buf,size_tcount,loff_t*f_pos){intlen;if(count<0){return-EINVAL;}len=strlen(data);count=(len>count)?count:len;if(copy_to_user(buf,data,count))/*将内核缓冲的数据拷贝到用户空间*/{return-EFAULT;}returncount;}/*写函数*/staticssize_ttest_write(structfile*file,constchar*buffer,size_tcount,loff_t*f_pos){if(count<0){return-EINVAL;}memset(data,0,BUFF_SZ);count=(BUFF_SZ>count)?count:BUFF_SZ;if(copy_from_user(data,buffer,count))/*将用户缓冲的数据复制到内核空间*/{return-EFAULT;}returncount;}/*打开函数*/staticinttest_open(structinode*inode,structfile*file){printk("Thisisopenoperation\n");/*分配并初始化缓冲区*/data=(char*)kmalloc(sizeof(char)*BUFF_SZ,GFP_KERNEL);if(!data){return-ENOMEM;}memset(data,0,BUFF_SZ);return0;}/*关闭函数*/staticinttest_release(structinode*inode,structfile*file){printk("Thisisreleaseoperation\n");if(data){kfree(data);/*释放缓冲区*/data=NULL;/*防止出现野指针*/}return0;}/*创建、初始化字符设备,并且注册到系统*/staticvoidtest_setup_cdev(structcdev*dev,intminor,structfile_operations*fops){interr,devno=MKDEV(major,minor);cdev_init(dev,fops);dev->owner=THIS_MODULE;dev->ops=fops;err=cdev_add(dev,devno,1);if(err){printk(KERN_NOTICE"Error%daddingtest%d",err,minor);}}/*虚拟设备的file_operations结构*/staticstructfile_operationstest_fops={.owner=THIS_MODULE,.read=test_read,.write=test_write,.open=test_open,.release=test_release,};/*模块注册入口*/intinit_module(void){intresult;dev_tdev=MKDEV(major,0);if(major){/*静态注册一个设备,设备号先前指定好,并设定设备名,用cat/proc/devices来查看*/result=register_chrdev_region(dev,1,TEST_DEVICE_NAME);}else{result=alloc_chrdev_region(&dev,0,1,TEST_DEVICE_NAME);}if(result<0){printk(KERN_WARNING"Testdevice:unabletogetmajor%d\n",major);returnresult;}test_setup_cdev(&test_dev,0,&test_fops);printk("Themajorofthetestdeviceis%d\n",major);return0;}/*卸载模块*/voidcleanup_module(void){cdev_del(&test_dev);unregister_chrdev_region(MKDEV(major,0),1);printk("Testdeviceuninstalled\n");}(2)编译代码。虚拟设备的驱动程序的Makefile如下所示:ifeq($(KERNELRELEASE),)KERNELDIR?=/lib/modules/$(shelluname-r)/build/*内核代码编译路径*/PWD:=$(shellpwd)modules:$(MAKE)-C$(KERNELDIR)M=$(PWD)modulesmodules_install:$(MAKE)-C$(KERNELDIR)M=$(PWD)modules_installclean:rm-rf*.o*~core.depend.*.cmd*.ko*.mod.c.tmp_versions.PHONY:modulesmodules_installcleanelseobj-m:=test_drv.o/*将生成的模块为test_drv.ko*/endif(3)加载和卸载模块。通过下面两个脚本代码分别实现驱动模块的加载和卸载。加载脚本test_drv_load如下所示:#!/bin/sh#驱动模块名称module="test_drv"#设备名称。在/proc/devices中出现device="test_dev"#设备文件的属性mode="664"group="david"#删除已存在的设备节点rm-f/dev/${device}#加载驱动模块/sbin/insmod-f./$module.ko$*||exit1#查到创建设备的主设备号major=`cat/proc/devices|awk"\\$2==\"$device\"{print\\$1}"`#创建设备文件节点mknod/dev/${device}c$major0#设置设备文件属性chgrp$group/dev/${device}chmod$mode/dev/${device}卸载脚本test_drv_unload如下所示:#!/bin/shmodule="test_drv"device="test_dev"#卸载驱动模块/sbin/rmmod$module$*||exit1#删除设备文件rm-f/dev/${device}exit0(6)编写测试代码。最后一步是编写测试代码,也就是用户空间的程序,该程序调用设备驱动来测试驱动的运行是否正常。以下实例只实现了简单的读写功能,测试代码如下所示:/*test.c*/#include<stdio.h>#include<stdlib.h>#include<string.h>#include<sys/stat.h>#include<sys/types.h>#include<unistd.h>#include<fcntl.h>#defineTEST_DEVICE_FILENAME"/dev/test_dev"/*设备文件名*/#defineBUFF_SZ1024/*缓冲大小*/intmain(){intfd,nwrite,nread;charbuff[BUFF_SZ];/*缓冲区*//*打开设备文件*/fd=open(TEST_DEVICE_FILENAME,O_RDWR);if(fd<0){perror("open");exit(1);}do{printf("Inputsomewordstokernel(enter'quit'toexit):");memset(buff,0,BUFF_SZ);if(fgets(buff,BUFF_SZ,stdin)==NULL){perror("fgets");break;}buff[strlen(buff)-1]='\0';if(write(fd,buff,strlen(buff))<0)/*向设备写入数据*/{perror("write");break;}if(read(fd,buff,BUFF_SZ)<0)/*从设备读取数据*/{perror("read");break;}else{printf("Thereadstringisfromkernel:%s\n",buff);}}while(strncmp(buff,"quit",4));close(fd);exit(0);}4.实验结果首先在虚拟设备驱动源码目录下编译并加载驱动模块。$makeclean;make$./test_drv_load接下来,编译并运行测试程序$gcc–otesttest.c$./test测试程序运行效果如下:Inputsomewordstokernel(enter'quit'toexit):Hello,everybody!Thereadstringisfromkernel:Hello,everybody!/*从内核读取的数据*/Inputsomewordstokernel(enter'quit'toexit):ThisisasimpledriverThereadstringisfromkernel:ThisisasimpledriverInputsomewordstokernel(enter'quit'toexit):quitThereadstringisfromkernel:quit最后,卸载驱动程序$./test_drv_unload通过dmesg命令可以查看内核打印的信息:$dmesg|tail–n10……Themajorofthetestdeviceis250/*当加载模块时打印*/Thisisopenoperation/*当打开设备时打印*/Thisisreleaseoperation/*关闭设备时打印*/Testdeviceuninstalled/*当卸载设备时打印*/

    时间:2018-06-14 关键词: 操作系统 基础教程 设备驱动 嵌入式linux test驱动

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

技术子站

更多

项目外包