嵌入式Linux开发实用教程

978-7-115-33483-1
作者: 朱兆祺 李强 袁晋蓉
译者:
编辑: 俞彬

图书目录:

详情

本书基于三星公司的S3C6410控制芯片(即ARM11),系统讲解了Linux嵌入式开发全过程。内容包括嵌入式Linux学习的基本技能、U-Boot移植、Linux系统移植、Linux驱动移植、Qt移植、实战项目等。全书采用用最常用和最通俗易懂的案例,帮助初学者循序渐进掌握嵌入式Linux开发。

图书摘要

嵌入式Linux开发实用教程
朱兆祺 李强 袁晋蓉 编著
人民邮电出版社

北京

来自ARM的问候与推荐

ARM Holdings 是全球领先的半导体知识产权(IP)提供商,并因此在数字电子产品的开发中处于核心地位。ARM 的总部位于英国剑桥,2000 多名员工分布在全球多个国家和地区。ARM 公司成立于1990年,目前已有超过250家公司在ARM处理器IP的基础上开发出了数以百计的各类芯片,至今已累计出货超过300亿颗,平均算下来地球上每个人都可以分得4颗ARM“芯”。由于ARM“芯”在各领域的广泛应用及ARM生态中丰富的资源,目前基本上所有的主流操作系统都提供了对ARM架构CPU的支持。目前,ARM技术已在90%的智能手机、80%的数码相机以及28%的电子设备中得到应用。

很高兴看到本书的出版。在嵌入式开发和教学中,软件的比重无疑变得越来越大。不同于PC上的软件开发,嵌入式软件开发者需要对硬件平台和操作系统具有一定的了解。对硬件平台和操作系统的选择经常困扰着很多人特别是初学者。本书作者结合自己的点滴经验为读者们做出了一个很好的范例。三星公司推出的基于 ARM 1176JZF-S 内核的 S3C6410 处理器,直至今日在工程项目和教学实践中仍被广泛采用;软件方面,Linux仍是嵌入式系统中的首选操作系统之一。

本书循序渐进,从Linux基础开始,覆盖了U-Boot移植、Linux移植、驱动开发等方面,并在最后以一个实际的系统设计为例,进行实战演练,对全书的内容进行巩固。

书的目标是帮助初学者快速进入嵌入式Linux学习的大门,听闻已有高校准备采用本书作为实验课教程,相信广大的同学和嵌入式的爱好者们一定能够从本书中获益。也预祝您在嵌入式的学习和开发中获得更多的乐趣并取得成功。

时昕 博士

ARM公司中国区大学计划经理

2013年12月

推荐序

随着平板电脑与手机,乃至网络化电视等智能化电子产品的蓬勃发展,嵌入式系统及其应用获得了众多企业的青睐,以 ARM+Android 的嵌入式系统成为当今 IT 领域最热门的技术之一。Android是基于Linux内核的操作系统,要掌握Android的开发与应用,当然要先学好嵌入式Linux。但是嵌入式Linux是一门非常复杂的软件技术,入门较难,初学者在自学过程常常感到困惑,导致无法掌握,甚至不得不半途而废。

虽然讲授嵌入式Linux的书千千万万,但多数是专家、学者们的专著,或者是培训机构的教材。而这本书则是以一个嵌入式Linux学习者的角度,总结在自学嵌入式Linux过程中的种种体会,也是为众多苦苦跋涉在嵌入式Linux学习途中的自学者,描述成功入门的捷径。

学习嵌入式Linux目的是为了应用,因而作者从U-Boot移植入手,为初学者剖析U-Boot移植的难题,进而学习Linux驱动程序,然后通过Qt图形用户界面应用程序框架的学习,告诉初学者如何建立图形用户界面,以及实现嵌入式Linux在ARM系统中的应用。

特别要指出的是,该书的两位年轻作者,在江西理工大学自动化专业读书期间,专业课程中并没有关于Linux的课程,但是他们却能够独立进行研读Linux并对学习经验进行总结,为这本书今天的成型奠定基础。这不仅凝聚了他们在课余无数个日夜学习的艰辛,也说明了高等工科教育改革的成功。因为从入学起,和许许多多专业学生不一样的是,他们第一个学期已经开始学习“从晶体管到单片机”,第二个学期已经学完了ARM嵌入式系统与μC/OS-II嵌入式操作系统。早期工程教育为后三年的“基于项目的学习”打下了坚实基础。

2006年起,学校与国内著名的嵌入式系统企业——广州周立功单片机科技有限公司通力合作,启动了“3+1”创新教育改革。在“面向工程、项目驱动、能力培养、全面发展”的教育改革理念的指导下,探索实施有效的高等工程教育的新路。每个自动化专业学生在国家级人才培养模式创新试验区(配备全套的计算机、电子仪器及嵌入式系统开发平台),可以日以继夜地学习自动化与嵌入式系统技术,暑期再前往公司强化嵌入式技术能力,大四再到企业进行一年的嵌入式系统工程实训(国家卓越工程师教育培养计划)。所以说,虽然他们当时不过是大三和大四的学生,却拥有在嵌入式技术领域3~4年的实践经验,在这个年轻与日新月异的技术领域,可以说是熟手了。

王祖麟

江西理工大学电气学院副院长

江西理工大学“3+1”创新教育创始人

2013年12月

前言

2012年11月,当我看到论坛中的同龄大学生在学习嵌入式Linux寸步难行,我就计划将我学习嵌入式Linux的点点滴滴记录下来,从一个学生的角度去写,或许更能让初学者接受。2013年1月,当写完初稿再重新审视的时候,总感觉不尽如意。2013年3月,我联系了我的师弟李强,两人打算以一个全新的思维重新完成这本书。

2013年6月,书稿终于定型。

本书一共有6章,从Linux指令基础到Linux常用软件;从U-Boot移植到Linux移植;从Linux驱动程序设计到Qt应用程序设计,全方位解析作为一个初学者该如何快速踏入嵌入式Linux学习的大门。

这本书大体结构如下:

第1章嵌入式Linux基础,为了让还没有接触过或者不太熟悉Linux的读者进一步认识Linux,介绍了两个在嵌入式Linux学习中使用频率很高的软件。有了这一章的知识作铺垫,后续的学习将更加顺畅。

第2章U-Boot-2013.04 分析与移植,本章覆盖 U-Boot 启动分析、SD 卡启动、NAND Flash启动移植、DM9000 网卡移植等内容。笔者从SD卡启动到NAND Flash 启动,解开众多厂家不愿公开的技术点。对于初学者来说,U-Boot的移植无疑是一座大山,笔者将一步步揭开U-Boot的神秘面纱。

第3章Linux-3.8.3 内核移植,本书采用最新内核,涉及Linux 内核分析、NAND Flash 移植、DM9000网卡移植、LCD液晶屏移植、YAFFS2文件系统制作等知识。从OK6410的内核移植,让初学者对Linux有个较为深入的了解和认识。

第4章Linux设备驱动程序设计,笔者截取了较为经典的字符设备驱动和块设备驱动程序对这部分知识进行讲解,给初学者在往后学习Linux设备驱动知识和从事Linux设备驱动工程师奠定扎实的基础。

第5章Qt-4.8.4移植,Qt4.8.4在Qt的发展具有重要地位,本章将带领读者将Qt4.8.4版本移植到OK6410开发板以及学习Qt程序的编写方法。

第6章嵌入式Linux学习拓展,笔者将前5章知识进行进一步拓展,所谓温故而知新、举一反三。

本书根据6章的内容分别录制了视频,联合OK6410-A开发板进行实验,一步一步带领读者深入学习。书中每一节内容都已经标注相对应的视频位置,请读者自行观看。

通过本书的学习,作者不能保证每一位读者都能成为嵌入式高手;但是我相信,一定可以带初学者进入嵌入式的大门。

完成本书的学习其实很简单:将少买一件衣服的钱买一块开发板,将每天玩游戏的1小时用于跟随本书一步步进行学习,我相信,3个月之后,你一定可以成功跨入嵌入式的大门。

在此感谢江西理工大学王祖麟教授大学四年对我的言传身教,并为本书作序;感谢 ARM 公司中国区大学计划经理时昕博士为本书撰写推荐序;感谢我的父母22年来对我含辛茹苦的培养;感谢我女朋友对我一直以来的关心和照顾。参与本书创作的还有谢贤斌、温如春、吴银凤、刘晖、张子明(飞凌嵌入式工程师),为本书做出宣传的电子发烧友陈锋和钱珊珊,在此对他们一并表示感谢。

笔者能力有限,如果有错误之处,还请各位读者指出。笔者邮箱:jxlgzzq@163.com 和jxustlq@163.com。笔者在2013年1月建立了嵌入式Linux学习手册QQ群:284013595、271641475。欢迎各位读者加入群进行学习讨论。有关嵌入式Linux实用教程的相关视频、资料、软件、源代码、程序和C语言学习资料将在以下百度网盘中进行更新。

百度网盘1:

http://pan.baidu.com/share/link?shareid=3412947033&uk=1242133881

百度网盘2:

http://pan.baidu.com/share/link?shareid=135537395&uk=3996269986

百度网盘3:

http://pan.baidu.com/share/link?shareid=170064468&uk=3693641046

百度网盘4:

http://pan.baidu.com/share/link?shareid=216899223&uk=2165231688

百度网盘5:

http://pan.baidu.com/share/link?shareid=302245753&uk=3592819552

这五个网盘资料相同,读者可择优下载。

朱兆祺

2013年12月

第1章 嵌入式Linux基础

1.1 Linux基本命令

在学习嵌入式Linux开发的过程中,将经常使用到Linux的操作命令。实际上,Linux系统中的命令也是为实现特定的功能而编写的,而且绝大数的命令是用C语言编写的。有些实用性强的程序被广泛使用和传播,逐渐地演变成Linux的标准命令。但是Linux的操作命令繁多,本节将在U-Boot、Linux移植过程中常用到的Linux操作命令罗列出来进行讲解,为后续的学习做良好的铺垫。读者不要认为这是Linux简单命令则不屑一顾,嵌入式Linux学习是一个漫长的过程,循序渐进方能有所成就,这个过程是由每一小步累加而成的。天下难事,必作于易;天下大事,必作于细。所以读者务必要对待学习的每一个细节。

1.1.1 文件属性查询与修改

1.文件属性查询

“ls”命令在Linux目录中占据着重要地位,主要用于查看文件属性、查看目录下所包含的文件等。

zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/busybox-1.20.2/_install$ ls

bin   dev home linuxrc proc sbin tmp var

creat_yaffs2.sh etc lib mnt root sys usr

通过“ls”命令可查看_install目录下有哪些东西。如果要进一步查看文件属性,则使用“ll”命令或者“ls -al”命令,这两个命令是等效的。

zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/busybox-1.20.2/_install$ ll

总用量 64

drwxr-xr-x 15 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 .

drwxr-xr-x 35 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 15:34 ..

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 15:34 bin

-rw-r--r-- 1 zhuzhaoqi zhuzhaoqi 393 2013-03-17 16:32 creat_yaffs2.sh

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 dev

drwxr-xr-x 3 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 21:01 etc

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 home

drwxr-xr-x 3 zhuzhaoqi zhuzhaoqi 4096 2013-03-18 09:57 lib

lrwxrwxrwx 1 zhuzhaoqi zhuzhaoqi 11 2013-03-17 15:34 linuxrc -> bin/busybox

drwxr-xr-x 5 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 mnt

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 proc

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 root

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 15:34 sbin

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 sys

drwxrwxrwx 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 tmp

drwxr-xr-x 7 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 usr

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-17 16:33 var

这样每一个文件的属性将一目了然,而属性中的每一个数据都有特定的含义,如表1.1所示。

表1.1 文件属性含义

其中文件权限的10个字符的含义如表1.2所示。

表1.2 文件权限含义

因此/bin目录的文件权限是:文件所有者对/bin目录可读可写可执行,文件所属用户组对/bin目录可读不可写可执行,其他人对/bin目录可读不可写可执行。

当对某个文件进行操作,要特别注意这个文件是否具有将要进行操作的权限。如果我们所在的用户组没有操作权限而又得进行操作,此时就得修改文件的权限。

2.文件权限修改

“chmod”命令的作用是变更一个文件的权限。

zhuzhaoqi@zhuzhaoqi-desktop:~/linux/include$ ll

总用量 8

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-18 22:02 ./

drwxr-xr-x 3 zhuzhaoqi zhuzhaoqi 4096 2013-03-18 22:07 ../

-rw-r--r-- 1 zhuzhaoqi zhuzhaoqi 0 2013-03-18 22:02 s3c6410.h

从上一小节可知,“drwxr-xr-x”除了“d”是文件类型,剩下9个字符划分成3组,表示3个用户组的使用权限。而在Linux系统中,每一个用户组的3个字母分别可用数字进行描述其权限,r:4、w:2、x:1、-:0,将每一组的数字进行相加,即得到这组用户的权限。例如上面s3c6410.h的权限是:rw-r--r--,那么每一用户组权限分别是:6、4、4,那么组合起来即为644。每个文件的最高权限为777。

给予s3c6410.h最高权限,如下:

zhuzhaoqi@zhuzhaoqi-desktop:~/linux/include$ chmod 777 s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux/kernel/include$ ll

总用量 8

drwxr-xr-x 2 zhuzhaoqi zhuzhaoqi 4096 2013-03-18 22:02 ./

drwxr-xr-x 3 zhuzhaoqi zhuzhaoqi 4096 2013-03-18 22:07 ../

-rwxrwxrwx 1 zhuzhaoqi zhuzhaoqi 0 2013-03-18 22:02 s3c6410.h*

通过“chmod”更改权限命令可以看到s3c6410.h的权限是最高权限。

1.1.2 目录与路径处理命令

1.切换目录

“cd”命令的作用是从当前目录切换到另一个目录下。如从用户根目录进入/linux目录下,如下操作:

zhuzhaoqi@zhuzhaoqi-desktop:~$ cd linux/

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$

2.创建新目录

“mkdir”命令的作用是创建一个新的目录,如在/linux目录下再创建一个/linux-3.8.3子目录,如下操作:

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ mkdir linux-3.8.3

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

linux-3.8.3

mkdir 的用法很多,可以通过输入mkdir –help 查看,如下:

zhuzhaoqi@zhuzhaoqi-desktop:~$ mkdir --help

用法:mkdir [选项]... 目录...

若指定目录不存在则创建目录

长选项必须使用的参数对于短选项时也是必需使用的

-m, --mode=模式 设置权限模式(类似chmod),而不是rwxrwxrwx 减umask

-p, --parents 需要时创建目标目录的上层目录,但即使这些目录已存在也不当作错误处理

-v, --verbose 每次创建新目录都显示信息

-Z, --context=CTX 将每个创建的目录的SELinux 安全环境设置为CTX

--help 显示此帮助信息并退出

--version 显示版本并退出

mkdir –p 这个指令在U-Boot和Linux 内核源码中的Makefile 中的使用是相当频繁的。

3.删除目录

如果是删除一个空目录,则使用“rmdir”命令即可;如果该目录下有东西,则不能使用“rmdir”命令删除。

zhuzhaoqi@zhuzhaoqi-desktop:~/linux/linux-3.6.7$ ls

arch

zhuzhaoqi@zhuzhaoqi-desktop:~/linux/linux-3.6.7$ cd ..

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

linux-3.6.7 linux-3.8.3

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ cd linux-3.8.3/

zhuzhaoqi@zhuzhaoqi-desktop:~/linux/linux-3.8.3$ ls

zhuzhaoqi@zhuzhaoqi-desktop:~/linux/linux-3.8.3$ cd ..

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

linux-3.6.7 linux-3.8.3

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ rmdir linux-3.8.3/

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

linux-3.6.7

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ rmdir linux-3.6.7/

rmdir: 删除 "linux-3.6.7/" 失败:目录非空

从上面操作可知,由于/linux-3.8.3 目录为空,则可使用“rmdir”删除;但是/ linux-3.6.7 目录下有一个子目录/arch,则不能使用“rmdir”删除。此时则应该使用“rm -r”命令删除。

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

linux-3.6.7

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls linux-3.6.7/

arch

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ rm -r linux-3.6.7/

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$

通过“ls”命令可知,linux目录下的linux-3.6.7/目录以及被删除。

1.1.3 文件操作

1.新建文件

新建一个文件可以使用“vim”命令,但是使用“vim”命令退出打开的文件时需要保存退出,否则会视为没有创建文件。

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ vim s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

s3c6410.h

2.复制文件

复制文件命令为“cp”。如下:

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

include s3c6410.c s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ cp s3c6410.h include/

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

include s3c6410.c s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls include/

s3c6410.h

如果要复制并且重命名,如下操作:

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

include kernel s3c6410.c s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ cp s3c6410.c include/s3c6400.c

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls include/

s3c6400.c s3c6410.h

当复制目录时,使用“cp -r”命令。如下:

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

include kernel s3c6410.c s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls kernel/

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ cp -r include/ kernel/

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

include kernel s3c6410.c s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls kernel/

include

3.移动文件

移动一个文件则使用“mv”命令,如下:

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

include kernel s3c6410.c s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ mv s3c6410.c kernel/

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls

include kernel s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ ls kernel/

include s3c6410.c

编辑一个文件,建议使用“gedit”命令或者“vim”命令。

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ gedit s3c6410.h

zhuzhaoqi@zhuzhaoqi-desktop:~/linux$ vim s3c6410.c

1.1.4 打包与解包、压缩与解压缩

熟悉打包与解包、压缩与解压缩的操作命令是操作 Linux 文件的必备技能。Linux 下的打包与解包、压缩与解压缩的操作命令种类繁多,本节截取常用的8个格式进行讲解。本节中,FileName是指打包、压缩之后的文件名,DirName是指待打包、压缩的文件名。

(1).tar格式

单纯的tar功能其实仅仅是打包而已,也就是说将很多文件集结成一个文件,并没有进行压缩。

解包:tar xvf FileName.tar

打包:tar cvf FileName.tar DirName

(2).gz格式

GZIP 最早由Jean-loup Gailly和Mark Adler 创建,用于UNIX系统的文件压缩。在Linux 中经常会碰到后缀名为.gz的文件,它们的原型即是GZIP格式。

解压1:gunzip FileName.gz

解压2:gzip -d FileName.gz

压缩:gzip FileName

(3).tar.gz格式和.tgz格式

以.tar.gz和.tgz为后缀名的压缩文件在在Linux和OSX下是非常常见的,Linux和OSX都可以直接解压使用这种压缩文件。

解压:tar zxvf FileName.tar.gz

压缩:tar zcvf FileName.tar.gz DirName

(4).bz2格式

压缩生成后缀名为.bz2 的压缩算法使用的是“Burrows-Wheeler block sorting text”,这类算法压缩比率比较高。

解压1:bzip2 -d FileName.bz2

解压2:bunzip2 FileName.bz2

压缩: bzip2 -z DirName

这里需要注意的是,当执行压缩指令之后,将会生成FileName.bz2压缩文件,同时DirName文件将会自动删除。

(5).tar.bz2格式

bzip2是一个压缩能力非常强的压缩程序,以.bz2和.tar.bz2为后缀名的压缩文件都是bzip2压缩的结果。

解压:tar jxvf FileName.tar.bz2

压缩:tar jcvf FileName.tar.bz2 DirName

(6).Z格式

compress 是一个相当古老的UNIX 压缩指令,压缩后的文件是以.Z 作为后缀名。

解压:uncompress FileName.Z

压缩:compress DirName

(7).tar.Z格式

这个压缩格式可以认为是.tar打包加上.Z压缩。

解压:tar Zxvf FileName.tar.Z

压缩:tar Zcvf FileName.tar.Z DirName

(8).zip格式

因为格式开放而且免费,越来越多的软件支持打开Zip文件。

解压:unzip FileName.zip

压缩:zip FileName.zip DirName

以上8种打包压缩算法都有所区别,最终导致的结果是压缩时间和压缩大小不一样。每一种压缩格式都有其优势和不足,在何种场合应该使用何种压缩格式就得视实际情况而定了。

在程序设计当中,空间换取时间、时间换取空间的现象是非常常见的一种方法。比如在单片机的LED跑马灯中,经常使用在数组中取出想要的花样,这就是空间换取时间。

本节配套视频位于光盘中“嵌入式Linux开发实用教程视频”目录下第一章01课(Linux基本操作指令)。

1.2 Makefile基本知识

Makefile如今能得以广泛应用,这还得归功于它被包含在UNIX系统中。在make诞生之前, UNIX系统的编译系统主要由“make”、“install”shell脚本程序和程序的源代码组成。它可以把不同目标的命令组成一个文件,而且可以抽象化依赖关系的检查和存档。这是向现代编译环境发展的重要一步。1977年,斯图亚特·费尔德曼在贝尔实验室里制作了这个软件。2003年,斯图亚特·费尔德曼因发明了这样一个重要的工具而接受了美国计算机协会(ACM)颁发的软件系统奖。

Makefile 文件可以实现自动化编译,只需要一个“make”命令,整个工程就能完全自动编译,极大地提高了软件开发的效率。目前虽有众多依赖关系检查工具,但是make是应用最广泛的一个。一个程序员会不会写 Makefile,从一个侧面说明了这个程序员是否具备完成大型工程的能力。

1.2.1 Makefile规则

一个简单的Makefile语句由目标、依赖条件、指令组成。

smdk6400_config :unconfig

@mkdir -p $(obj)include $(obj)board/samsung/smdk6400

smdk6400_config:目标;

unconfig:先决条件;

@mkdir -p $(obj)include $(obj)board/samsung/smdk6400:指令。这里特别注意,“@”前面是Tab键,并且必须是Tab键,而不能是空格。

目标和先决条件是依赖关系,目标是依赖于先决条件生成的。

1.2.2 Makefile变量

1.变量的引用方式

使用“$(OBJTREE)”或者“${ OBJTREE }”来引用OBJTREE 这个变量的定义。这个引用方式似乎很像C语言中的指针变量,使用*p来取存放在指针p中的值。

obj := $(OBJTREE)/

OBJTREE := $(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR))

export BUILD_DIR=/tmp/build

$(if $(BUILD_DIR),$(BUILD_DIR),$(CURDIR))的含义:如果“BUILD_DIR”变量值不为空,则将变量“BUILD_DIR”指定的目录作为一个子目录;否则将目录“CURDIR”作为一个子目录。

2.递归展开式变量

这类变量的定义是通过“=”和“define”来定义的。

student = lilei

CLASS = $(student) $(teacher)

teacher = yang

all:

@echo $(CLASS)

其优点是:这种类型递归展开式的变量在定义时,可以引用其他之前没有定义过的变量,这个变量可能在后续部分定义,或者是通过make的命令行选项传递的变量来定义。

其缺点是:其一,使用此风格的变量定义,可能会由于出现变量的递归定义而导致 make 陷入到无限的变量展开过程中,最终使make执行失败。

x = $(y)

y = $(z)

z = $(x)

这样的话会使得Makefile出错,因为到最终引用了自己。

其二,这种风格的变量定义中如果使用了函数,那么包含在变量值中的函数总会在变量被引用的地方执行。

3.直接展开式变量

为了避免递归展开式变量存在的问题和不方便,GNU make 支持另外一种风格的变量,称为直接展开式变量。这种风格的变量使用“:=”定义。在使用“:=”定义变量时,变量值中对其他变量或者函数的引用在定义变量时被展开,也就是对变量进行替换。

X := student

Y := $(X)

X := teacher

all:

@echo $(X) $(Y)

这里的输出是:teacher student。

这个直接展开式变量在定义时就完成了对所引用变量和函数的展开,因此不能实现对其后定义变量的引用。

4.条件赋值

在对变量进行赋值之前,会对其进行判断,只有在这个变量之前没有进行赋值的情况下才会对这个变量进行赋值。

X := student

X ?= teacher

all:

@echo $(X)

由于X在之前被赋值了,所以这里的输出是student。

5.变量的替换引用

对于一个已经定义的变量,可以使用变量的替换引用将变量中的后缀字符使用指定的字符替换。格式为“$(X:a=b)”(或者“${X:a=b}”),即将变量“X”中所有以“a”字符结尾的字替换为以“b”结尾的字。

X := fun.o main.o

Y := $(X: .o=.c)

all:

@echo $(X) $(Y)

特别注意的是,$(X: .o=.c)的“=”两边不能有空格。这里的输出是:fun.o main.o fun.c main.c。

6.追加变量值

追加变量值是指一个通用变量在定义之后的其他一个地方,可以对其值进行追加。也就是说可以在定义时(也可以不定义而直接追加)给它赋一个基本值,后续根据需要可随时对它的值进行追加(增加它的值)。在Makefile中使用“+=”(追加方式)来实现对一个变量值的追加操作。

X = fun.o main.o

X += sub.o

all:

@echo $(x)

这里的输出是:fun.o main.o sub.o。

1.2.3 Makfile常用关键字

1.ifneq关键字

这个关键字是用来判断两个参数是否不相等。格式为:

ifneq “Value1”“Value2”

ifneq (Value1,Value2)

在判断之前先要将Value1和Value2的值进行展开和替换,如在U-Boot-2013.04的顶层目录Makefile中,对U-Boot的版本参数就使用了ifneq关键字进行判断。

VERSION= 2013

PATCHLEVEL = 04

SUBLEVEL=

EXTRAVERSION =

ifneq "$(SUBLEVEL)" ""

U_BOOT_VERSION = $(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)

else

U_BOOT_VERSION = $(VERSION).$(PATCHLEVEL)$(EXTRAVERSION)

endif

先将SUBLEVEL使用$()展开和替换,如果SUBLEVEL的值不是空,则执行:

U_BOOT_VERSION = $(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)

也就是说,如果$(SUBLEVEL) = 1 的话,那么U_BOOT_VERSION = 2013.04.1。

如果SUBLEVEL的值是空,则执行:

U_BOOT_VERSION = $(VERSION).$(PATCHLEVEL)$(EXTRAVERSION)

那么此时U_BOOT_VERSION = 2013.04。

2.ifeq关键字

ifeq关键字和ifneq关键字是相对而言的,用来判断两个参数是否相等。格式为:

ifeq “Value1”“Value2”

ifeq (Value1,Value2)

和ifneq一样,先要将Value1和Value2展开替换之后,再进行比较。

ifeq ($(HOSTARCH),$(ARCH))

CROSS_COMPILE=/usr/local/arm/4.4.1/bin/arm-linux-

endif

如果HOSTARCH展开替换之后和ARCH展开替换之后相等,则:

CROSS_COMPILE=/usr/local/arm/4.4.1/bin/arm-linux-

否则CROSS_COMPILE不等于/usr/local/arm/4.4.1/bin/arm-linux-。

3.ifndef关键字

ifndef关键字用来判断一个变量是否没有进行定义。格式:

ifndef Value

由于在Makefile中,没有定义的变量的值为空。

ifndef CONFIG_SANDBOX

SUBDIRS += $(SUBDIR_EXAMPLES)

endif

如果CONFIG_SANDBOX值为空,条件成立,执行如下语句:

SUBDIRS += $(SUBDIR_EXAMPLES)

否则不执行。

4.ifdef关键字

ifdef关键字用来判断一个变量是否已经进行定义过。格式:

ifdef Value

如:

ifdef CONFIG_SYS_LDSCRIPT

# need to strip off double quotes

LDSCRIPT := $(subst ",,$(CONFIG_SYS_LDSCRIPT))

endif

如果CONFIG_SYS_LDSCRIPT定义过,则执行:

LDSCRIPT := $(subst ",,$(CONFIG_SYS_LDSCRIPT))

否则不执行。

1.2.4 Makefile常用函数

1.Makefile函数语法

在Makefile中,函数的调用和变量的调用类似,都是使用“$”进行标识。语法如下:

$(函数名 函数的参数)

${函数名 函数的参数}

函数名与函数的参数之间使用空格隔开,而函数的参数间使用逗号进行分隔。以上两种写法都是可以的,但是为了风格统一,请不要两者进行混合使用。

2.shell函数

make可以使用shell函数和外部通信。shell函数本身的返回值是其参数的执行结果,没有进行任何处理,对结果的处理是由 make 进行的。当对函数的引用出现在规则的命令行中,命令行在执行时函数才被展开。展开时函数参数(shell命令)的执行是在另外一个shell进程中完成的,因此需要对出现在规则命令行的多级“shell”函数引用需要谨慎处理,否则会影响效率(每一级的“shell”函数的参数都会有各自的shell进程)。

建立一个测试程序,Makefile的内容:

zhu := $(shell cat func)

all:

@ echo $(zhu)

Func文件中的内容:

juxst zhuzhaoqi

执行完成Makefile之后:

zhuzhaoqi@zhuzhaoqi-desktop:~/u-boot/Makefile/shellfunction$ make

juxst zhuzhaoqi

在U-Boot和Linux内核源码中将会大量使用到shell函数。

3.subst函数

subst函数是字符串替换函数,语法为:

$(subst 被替换字串 替换字串 替换操作字符串)

执行subst函数之后,返回的是执行替换操作之后的字符串。如下:

name := zhu zhaoqi

Alphabet_befor := z

Alphabet_after := Z

Name := $(subst $(Alphabet_befor), $(Alphabet_after), $(name))

all:

echo $(Name)

执行上面的Makefile,输出结果为:

echo Zhu Zhaoqi

Zhu Zhaoqi

即将“z”替换成了“Z”。

4.dir函数

dir函数作用为取出该文件的目录,其语法为:

$(dir 文件名称)

执行该函数之后返回文件目录部分。

Makefile中常用函数较多,笔者就不一一例举了,读者可参考相关书籍进行深入了解。

本节配套视频位于光盘中“嵌入式 Linux 开发实用教程视频”目录下第一章 02 课(Makefile)。

1.3 arm-linux交叉编译链

平常我们做的编译叫本地编译,也就是在当前平台编译,编译得到的程序也是在本地执行。相对而言的交叉编译指的是在一个平台上生成另一个平台的可执行代码。

常见的交叉编译有以下3种。

在Windows PC上,利用ADS(ARM 开发环境),使用armcc 编译器,编译出针对ARM CPU的可执行代码。

在Linux PC 上,利用arm-linux-gcc 编译器,编译出针对Linux ARM 平台的可执行代码。

在Windows PC上,利用cygwin 环境,运行arm-elf-gcc 编译器,编译出针对ARM CPU的可执行代码。

1.3.1 arm-linux交叉编译工具链的制作方法

由于一般嵌入式开发系统的存储大小是有限的,通常都要在性能优越的 PC 上建立一个用于目标机的交叉编译工具链,用该交叉编译工具链在PC上编译目标机上要运行的程序,比如在PC平台(X86 CPU)上编译出能运行在以ARM为内核的CPU 平台上的程序。要生成在目标机上运行的程序,必须要用交叉编译工具链完成。交叉编译工具链是一个由编译器、连接器和解释器组成的综合开发环境,交叉编译工具链主要由binutils、gcc 和glibc 3 个部分组成。有时出于减小libc 库大小的考虑,也可以用别的 c 库来代替 glibc,例如 uClibc、dietlibc 和 newlib。建立交叉编译工具链是一个相当复杂的过程,如果不想自己经历复杂繁琐的编译过程,网上有一些编译好的可用的交叉编译工具链可以下载,但就以学习为目的来说,读者有必要学习自己制作一个交叉编译工具链。本节通过具体的实例讲述基于ARM的嵌入式Linux交叉编译工具链的制作过程。

制作arm-linux交叉编译工具链的一般通过crosstool工具或者crosstool_NG,前者使用方便,但是制作会受到一些限制,使用crosstool最多只能编译gcc 4.1.1、glibc 2.x 的版本。crosstool-NG是新的用来建立交叉工具链的工具,它是crosstool的替换者,crosstool_NG有更好的定制性,并且一直保持着更新,对新版本的编译工具链的支持比较好,当然也带来了一些麻烦,它并不是下载下来就可以使用的,必须先配置安装。我们这里选用crosstool_NG来制作我们的编译链。

1.安装crosstool_NG

在crosstool_NG官网上下载最新版本。

zhuzhaoqi@zhuzhaoqi-desktop:~$ mkdir arm-linux-tools

zhuzhaoqi@zhuzhaoqi-desktop:~$ cd arm-linux-tools/

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools$ ls

获取源码操作命令:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools$ wget http://crosstool-ng.org/ download/crosstool-ng/crosstool-ng-1.18.0.tar.bz2

--2013-03-26 21:34:34-- http://crosstool-ng.org/download/crosstool-ng/crosstool-ng-1.18.0.tar.bz2

正在解析主机 crosstool-ng.org... 140.211.15.107

正在连接 crosstool-ng.org|140.211.15.107|:80... 已连接。

已发出 HTTP 请求,正在等待回应... 200 OK

长度: 1884219 (1.8M) [application/x-bzip]

正在保存至: “crosstool-ng-1.18.0.tar.bz2”

100%[======================================>] 1,884,219223K/s 花时 8.8s

下载源码成功之后解压源码:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools$ tar jxvf crosstool-ng-1.18.0.tar.bz2 zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools$ ls

crosstool-ng-1.18.0 crosstool-ng-1.18.0.tar.bz2

考虑到后续将要使用到的各种目录,在这里先建立好后续所需目录。

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools$ mkdir crosstool-build crosstool-install src zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools$ ls

crosstool-buildcrosstool-ng-1.18.0 src

crosstool-install crosstool-ng-1.18.0.tar.bz2

由于Ubuntu操作系统的很多开发软件都没有安装,因此要先安装一些制作交叉编译链必备的软件。在Ubuntu 下安装软件的命令为: sudo apt-get install ***。

笔者建议arm-linux交叉编译工具链的制作最好在CentOS系统中完成,因为CentOS系统自带较为完善的开发软件,对于初学者不会造成不必要的麻烦。

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools$ sudo apt-get install sed bash cut dpkg-dev patch texinfom4 libtool statwebsvn tar gzip bzip2 lzmabison flex texinfo automake libtool patchcvs cvsd gawk–y

配置整个工程并且进行依赖检测:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$./configure--prefix /home/zhuzhaoqi/arm-linux-tools/crosstool-install

在安装过程中,提示如下错误:

……

checking how to run the C preprocessor... gcc -E

checking for ranlib... ranlib

checking for objcopy... objcopy

checking for absolute path to objcopy... /usr/bin/objcopy

checking for objdump... objdump

checking for absolute path to objdump... /usr/bin/objdump

checking for readelf... readelf

checking for absolute path to readelf... /usr/bin/readelf

checking for bison... no

configure: error: missing required tool: bison

输出错误提示缺失bison这个软件,安装:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ sudo apt-get install bison

安装完成之后,再次进行配置:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$./configure--prefix /home/zhuzhaoqi/arm-linux-tools/crosstool-install

又一次输出错误:

……

checking for bison... bison

checking for flex... no

configure: error: missing required tool: flex

提示缺失flex这个软件,进行安装:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ sudo apt-get install flex

安装完成之后,再一次进行配置:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$./configure --prefix /home/zhuzhaoqi/arm-linux-tools/crosstool-install

又一次提示错误:

checking for bison... bison

checking for flex... flex

checking for gperf... no

configure: error: missing required tool: gperf

提示缺失gperf这个软件,进行安装:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ sudo apt-get install gperf

安装完成之后,再一次进行配置:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$./configure --prefix /home/zhuzhaoqi/arm-linux-tools/crosstool-install

再一次提示出错:

……

checking for bison... bison

checking for flex... flex

checking for gperf... gperf

checking for makeinfo... no

configure: error: missing required tool: makeinfo

缺失makeinfo软件,进行安装,如果安装的是makeinfo,则会有如下提示:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ sudo apt-get install makeinfo

正在读取软件包列表... 完成

正在分析软件包的依赖关系树

E: 无法找到软件包makeinfo

此时应该安装texinfo软件:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ sudo apt-get install makeinfo

安装完成之后,再一次进行配置:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$./configure --prefix /home/zhuzhaoqi/arm-linux-tools/crosstool-install

这次的配置成功,如果读者操作还会报错的话,依照上面方法找出其根源进行改正即可。成功配置之后会自动创建我们需要的Makefile文件。

checking for library containing initscr... -lncursesw

configure: creating ./config.status

config.status: creating Makefile

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ ls

bootstrap configure ct-ng.comp LICENSES patches steps.mk

config  configure.ac ct-ng.in licenses.d README TODO

config.log contrib  docs  Makefile samples

config.status COPYING  kconfig Makefile.in scripts

执行Makefile文件:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ make

SED 'ct-ng'

SED 'scripts/crosstool-NG.sh'

SED 'scripts/saveSample.sh'

SED 'scripts/showTuple.sh'

GEN 'config/configure.in'

GEN 'paths.mk'

GEN 'paths.sh'

DEP 'nconf.gui.dep'

DEP 'nconf.dep'

DEP 'lxdialog/yesno.dep'

DEP 'lxdialog/util.dep'

DEP 'lxdialog/textbox.dep'

DEP 'lxdialog/menubox.dep'

DEP 'lxdialog/inputbox.dep'

DEP 'lxdialog/checklist.dep'

DEP 'mconf.dep'

DEP 'conf.dep'

BISON 'zconf.tab.c'

GPERF 'zconf.hash.c'

LEX 'lex.zconf.c'

DEP 'zconf.tab.dep'

CC 'zconf.tab.o'

CC 'conf.o'

LD 'conf'

CC 'lxdialog/checklist.o'

CC 'lxdialog/inputbox.o'

CC 'lxdialog/menubox.o'

CC 'lxdialog/textbox.o'

CC 'lxdialog/util.o'

CC 'lxdialog/yesno.o'

CC 'mconf.o'

LD 'mconf'

CC 'nconf.o'

CC 'nconf.gui.o'

LD 'nconf'

SED 'docs/ct-ng.1'

GZIP 'docs/ct-ng.1.gz'

编译成功之后进行安装:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ make install

成功安装之后,可以看到已经安装到指定的目录下,最后输出这么一句话:

……

For auto-completion, do not forget to install 'ct-ng.comp' into

your bash completion directory (usually /etc/bash_completion.d)

这是在提醒我们不要忘记了配置环境变量,多么人性化的提示。接下来配置环境变量。

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0$ sudo echo"PATH=$PATH:/home/zhuzhaoqi/arm-linux-tools/crosstool-install/bin" >> ~/.bashrc

执行使其生效:

zhuzhaoqi@zhuzhaoqi-desktop:~$ source /home/zhuzhaoqi/.bashrc

使用ct-ng –v 命令查看安装结果:

zhuzhaoqi@zhuzhaoqi-desktop:~$ ct-ng -v

GNU Make 3.81

Copyright (C) 2006 Free Software Foundation, Inc.

This is free software; see the source for copying conditions.

There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A

PARTICULAR PURPOSE.

这个程序创建为 i486-pc-linux-gnu

OK!ct-ng环境变量添加成功,也就意味着整个crosstool-NG安装成功。

2.配置交叉编译链

现在需要去做的就是配置要编译的交叉编译工具链,在crosstool-NG中有很多已经做好的默认配置(位于crosstool-ng- X.Y.Z(crosstool-ng-1.18.0)/samples 目录下),这里只需要进行修改就好了。对于编译器组件部分的版本最好不要修改,因为那个应该是经过测试后的最高版本了,但内核版本可以修改。

可以看到samples目录下的一些默认配置,如下所示:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0/samples$ ls

alphaev56-unknown-linux-gnu  mips64el-n64-linux-uclibc

alphaev67-unknown-linux-gnu  mips-ar2315-linux-gnu

arm-bare_newlib_cortex_m3_nommu-eabi mipsel-sde-elf

arm-cortex_a15-linux-gnueabi  mipsel-unknown-linux-gnu

arm-cortex_a8-linux-gnueabi  mips-malta-linux-gnu

arm-davinci-linux-gnueabi   mips-unknown-elf

armeb-unknown-eabi    mips-unknown-linux-uclibc

armeb-unknown-linux-gnueabi  powerpc-405-linux-gnu

armeb-unknown-linux-uclibcgnueabi powerpc64-unknown-linux-gnu

arm-unknown-eabi     powerpc-860-linux-gnu

arm-unknown-linux-gnueabi   powerpc-e300c3-linux-gnu

arm-unknown-linux-uclibcgnueabi  powerpc-e500v2-linux-gnuspe

armv6-rpi-linux-gnueabi   powerpc-unknown-linux-gnu

avr32-unknown-none    powerpc-unknown-linux-uclibc

bfin-unknown-linux-uclibc powerpc-unknown_nofpu-linux-gnu

i586-geode-linux-uclibc   s390-ibm-linux-gnu

i586-mingw32msvc,i686-none-linux-gnu s390x-ibm-linux-gnu

i686-nptl-linux-gnu    samples.mk

i686-unknown-mingw32    sh4-unknown-linux-gnu

m68k-unknown-elf     x86_64-unknown-linux-gnu

m68k-unknown-uclinux-uclibc  x86_64-unknown-linux-uclibc

mips64el-n32-linux-uclibc   x86_64-unknown-mingw32

里面有很多默认配置,有arm、avr32、mips、powerpc等硬件平台,而arm平台有如下几个:

arm-unknown-eabi是基于裸板,也就是无操作系统。

arm-unknown-linux-gnueabi 是基于Linux。

arm-unknown-linux-uclibcgnueabi 这个应该能看出来了,是为uclinux 用的。

arm-cortex_a15-linux-gnueabi可从名字上看是为cortex-a15用的。

arm-cortex_a8-linux-gnueabi 这个也可从名字上看是为cortex-a8 用的。

arm-xxx$&#*&还有几个,这些暂且不去理会。

因为是制作 arm-linux 交叉编译链,所以选择 arm-unknown-linux-gnueabi 进行配置。将arm-unknown-linux-gnueabi文件夹复制到crosstool-build/目录下:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-ng-1.18.0/samples$ cp -r arm-unknown-linux-gnueabi/ ../../crosstool-build/

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-build$ ls

arm-unknown-linux-gnueabi

将默认配置文件拷贝到工作目录(crosstool-build)下并改名为.config,因为默认的配置文件为.config,完成之后可以加载需要的配置。

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-build$     cparm-unknown-linux-gnueabi/crosstool.config .config

执行ct-ng menuconfig 进入配置界面进行配置:

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-build$ ct-ng menuconfig

LN config

MKDIR config.gen

IN config.gen/arch.in

IN config.gen/kernel.in

IN config.gen/cc.in

IN config.gen/binutils.in

IN config.gen/libc.in

IN config.gen/debug.in

CONF config/config.in

#

# configuration saved

#

进入配置界面,如图1.1所示。

图1.1 ct-ng图形配置界面

下面设置源码目录和安装目录,这需要读者依据自己实际设定的情况来进行配置。

第一步,设定源码包路径和交叉编译器的安装路径。

Paths and misc options --->

(/home/zhuzhaoqi/arm-linux-tools/src) Local tarballs directory 保存源码包路径

(/home/zhuzhaoqi/arm-linux-tools/tools) Prefix directory 交叉编译器的安装路径

配置之后的结构如图1.2所示。

图1.2 添加源码包路径和交叉编译器的安装路径

第二步,修改交叉编译器针对的构架。

因为本次是针对OK6410制作编译链,那就依据s3c6410的硬件特性来制作。

Target options 是重点要修改的地方(以下配置均是基于已拷贝过来的配置)。

Target Architecture(arm) 这个不用管,已经是arm 了。

Default instruction set mode (arm) 这个也不管,也已经是arm 了。

Architecture level()需要进行修改。

通过查找资料,这个应该是指令集的架构,对于 S3C6410 ARM1176JZF-S 核心使用的是armv6zk架构,就选armv6zk。那么,具体都支持哪些架构呢?可以用man gcc来查询,搜索arm,再搜索-march=就可以找到本gcc支持的处理器核心列表了:

-march=name

This specifies the name of the target ARM architecture. GCC uses

this name to determine what kind of instructions it can emit when

generating assembly code. This option can be used in conjunction

with or instead of the -mcpu= option. Permissible names are:

armv2, armv2a, armv3, armv3m, armv4, armv4t, armv5, armv5t, armv5e,

armv5te, armv6, armv6j, armv6t2, armv6z, armv6zk, armv6-m, armv7,

armv7-a, armv7-r, armv7-m, iwmmxt, iwmmxt2, ep9312.

Emit assembly for CPU()需要进行修改。

这个对应的是CPU的核心类型。同样,也和上面的选项一样,对应一个GCC选项。GCC的描述如下。

-mcpu=name

This specifies the name of the target ARM processor. GCC uses this

name to determine what kind of instructions it can emit when

generating assembly code. Permissible names are: arm2, arm250,

arm3, arm6, arm60, arm600, arm610, arm620, arm7, arm7m, arm7d,

arm7dm, arm7di, arm7dmi, arm70, arm700, arm700i, arm710, arm710c,

arm7100, arm720, arm7500, arm7500fe, arm7tdmi,arm7tdmi-s, arm710t,

arm720t, arm740t, strongarm, strongarm110, strongarm1100,

strongarm1110, arm8, arm810, arm9, arm9e, arm920, arm920t, arm922t,

arm946e-s, arm966e-s, arm968e-s, arm926ej-s, arm940t, arm9tdmi,

arm10tdmi, arm1020t, arm1026ej-s, arm10e, arm1020e, arm1022e,

arm1136j-s, arm1136jf-s, mpcore, mpcorenovfp, arm1156t2-s,

arm1176jz-s, arm1176jzf-s, cortex-a8, cortex-a9, cortex-r4,

cortex-r4f, cortex-m3, cortex-m1, xscale, iwmmxt, iwmmxt2, ep9312.

这样看简单一些了,如果是 S3C2410/S3C2440 就选 arm920t ,如果是 s3c6410 就选arm1176jzf-s。

Tune for CPU() ,对应的GCC 描述如下:

-mtune=name

This option is very similar to the -mcpu= option, except that

instead of specifying the actual target processor type, and hence

restricting which instructions can be used, it specifies that GCC

should tune the performance of the code as if the target were of

the type specified in this option, but still choosing the

instructions that it will generate based on the cpu specified by a

-mcpu= option. For some ARM implementations better performance can

be obtained by using this option.

意思是说这个选项和-mcpu 很类似,这里是指真实的CPU 型号。不过有读者是编译2440的工具链,这里选择的是arm9tdmi,如果不是,那就空着。这里的作用是如果arm920t处理不了,就用arm9tdmi的方式来编译。

与Floating point()浮点相关的选项s3c6410 有硬件VFP,所以这里选的是hardware FPU。这个是给有硬浮点的处理器强行选软浮点用的。

Use specific FPU()跟浮点有关,这里不选任何内容。至于怎么组合,读者可以跟据自己的CPU的实际情况进行相应的配置。

C compiler --->

*** Additional supported languages: ***

[ ] Java //不用这个编译器来编译java

当然,如果读者需要用它来编译java那就不用去除。

其他选项采用默认设置存盘然后退出,这样就配置完了。

zhuzhaoqi@zhuzhaoqi-desktop:~/arm-linux-tools/crosstool-build$ ct-ng build

开始编译,此编译过程需要花费大约两个小时,最终编译出arm-linux-gcc-4.4.1编译链。

1.3.2 交叉编译链在宿主机上的安装

这里要安装的交叉编译链版本是arm-linux-gcc 4.4.1(交叉编译链的版本很多,读者可以自行安装版本更高的编译链)。采用的Linux系统环境是Ubuntu10.04.4。具体的操作步骤如下。

1.在/usr/local下面创建一个文件夹:mkdir arm,将arm-linux-gcc 4.4.1 放在arm文件夹里面。然后解压缩,命令根据压缩包的后缀不同而不同。

2.添加环境变量,vim /etc/profile。

3.在最后一行添加:export PATH=$PATH:/usr/local/arm/4.4.1/bin。

4.退出执行命令:source /etc/profile。使其生效。

5.检测安装是否成功:arm-linux-gcc -v ;如果成功,输出最后一行则会提示:gcc version 4.4.1 (Sourcery G++ Lite 2009q3-67)。描述如下:

zhuzhaoqi@zhuzhaoqi-desktop:~/u-boot/Makefile/shellfunction$ arm-linux-gcc -v

Using built-in specs.

Target: arm-none-linux-gnueabi

Configured with: /scratch/julian/2009q3-respin-linux-lite/src/gcc-4.4/configure --build= i686-pc-linux-gnu --host=i686-pc-linux-gnu --target=arm-none-linux-gnueabi --enable-threads --disable-libmudflap --disable-libssp --disable-libstdcxx-pch --enable-extra-sgxxlite-multilibs --with-arch=armv5te --with-gnu-as --with-gnu-ld --with-specs='%{funwind-tables|fno-unwind- tables|mabi=*|ffreestanding|nostdlib:;:-funwind-tables} %{O2:%{!fno-remove-local-statics: -fremove-local-statics}} %{O*:%{O|O0|O1|O2|Os:;:%{!fno-remove-local-statics: -fremove-local- statics}}}' --enable-languages=c,c++ --enable-shared --disable-lto --enable-symvers=gnu --enable-__cxa_atexit --with-pkgversion='Sourcery G++ Lite 2009q3-67' --with-bugurl=https: //support.codesourcery.com/GNUToolchain/ --disable-nls --prefix=/opt/codesourcery --with- sysroot=/opt/codesourcery/arm-none-linux-gnueabi/libc --with-build-sysroot=/scratch/julian/2009q3-respin-linux-lite/install/arm-none-linux-gnueabi/libc--with-gmp=/scratch/julian/2009q3-respin-linux-lite/obj/host-libs-2009q3-67-arm-none-linux-gnueabi-i686-pc-linux-gnu/u sr --with-mpfr=/scratch/julian/2009q3-respin-linux-lite/obj/host-libs-2009q3-67-arm-none- linux-gnueabi-i686-pc-linux-gnu/usr --with-ppl=/scratch/julian/2009q3-respin-linux-lite/ obj/ host-libs-2009q3-67-arm-none-linux-gnueabi-i686-pc-linux-gnu/usr --with-host-libstdcxx='-static- libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm' --with-cloog=/scratch/julian/2009q3-respin-linux-lite/obj/host-libs-2009q3-67-arm-none-linux-gnueabi-i686-pc-linux-gnu/usr

--disable-libgomp --enable-poison-system-directories --with-build-time-tools=/scratch/ julian/2009q3-respin-linux-lite/install/arm-none-linux-gnueabi/bin --with-build-time-tools=/scratch/julian/2009q3-respin-linux-lite/install/arm-none-linux-gnueabi/bin

Thread model: posix

gcc version 4.4.1 (Sourcery G++ Lite 2009q3-67)

笔者建议最好不要在root用户下进行安装,否则使用交叉编译链可能会存在权限限制。

1.4 映像文件的生成和运行

德国罕见的科学大师莱布尼茨,在他的手迹里留下这么一句话:“1与0,一切数字的神奇渊源。这是造物的秘密美妙的典范,因为,一切无非都来自上帝。”二进制0和1两个简单的数字,构造了神奇的计算机世界,对人类的生产活动和社会活动产生了极其重要的影响,并以强大的生命力飞速发展。在嵌入式系统移植过程中,不管文件数量多么庞大,经过编译工具的层层处理后,最终生成一个可以加载到存储器内执行的二进制映像文件(.bin)。本节内容将会探讨映像文件的生成过程,以及它在存储设备的不同位置对程序运行产生的影响,为本书后文嵌入式系统的移植打下坚定的基础。

1.4.1 编译过程

GNU提供的编译工具包括汇编器as、C编译器gcc、C++编译器g++、链接器ld、二进制转换工具objcopy和反汇编的工具objdump等。它们被称作GNU编译器集合,支持多种计算机体系类型。基于ARM平台的工具分别为arm-linux-gcc、arm-linux-g++、arm-linux-ld、arm-linux-objcopy和arm-linux-objdump。arm-linux交叉编译工具链的制作方法已经详细介绍过了,编译程序直接使用前面制作好的工具链。

GNU 编译器的功能非常强大,程序可以用 C 文件、汇编文件编写,甚至是二者的混合。如图 1.3 所示是程序编译的大体流程,源文件经过预处理器、汇编器、编译器、链接器处理后生成可执行文件,再由二进制转换工具转换为可用于烧写到 Flash 的二进制文件,同时为了调试的方便还可以用反汇编工具生成反汇编文件。图中双向箭头的含义是,当gcc增加一些参数时可以相互调用汇编器和链接器进行工作。例如输入命令行“gcc –O main.c”后,直接就得到可执行文件a.out(elf)。

图1.3 程序编译流程

程序编译大体上可以分为编译和链接两个步骤:把源文件处理成中间目标文件.o(linux)、obj (windows)的动作称为编译;把编译形成的中间目标文件以及它们所需要的库函数.a(linux)、lib (windows)链接在一起的动作称为链接。现用一个简单的test工程来分析程序的编译流程。麻雀虽小,五脏俱全,它由启动程序start.S、应用程序main.c、链接脚本test.lds和Makefile四个文件构成。test工程中的程序通过操作单板上的LED灯的状态来判定程序的运行结果,它除了用于理论研究之外,没有其他的实用价值。

1.编译

在编译阶段,编译器会检查程序的语法、函数与变量的声明情况等。如果检查到程序的语法有错误,编译器立即停止编译,并给出错误提示。如果程序调用的函数、变量没有声明原型,编译器只会抛出一个警告,继续编译生成中间目标文件,待到链接阶段进一步确定调用的变量、函数是否存在。

start.S文件的内容如程序清单1.1所示,文件中的_start函数,为程序能够在C语言环境下运行做了最低限度的初始化:将S3C6410处理外设端口的地址范围告知ARM内核,关闭看门狗,清除bss段,初始化栈。初始化工作完毕后,跳转到main()。start.S是用汇编语言编写的代码文件,文件中定义了一个WATCHDOG宏,用于寄存器的赋值。在汇编文件中出现#define宏定义语句,对于初学者可能会有些迷惑。

程序清单1.1 start.S中汇编代码

/*

*This is a part of the test project

*Author: LiQiang Date: 2013/04/01

*Licensed under the GPL-2 or later.

*/

.globl _start

_start:

#define REG32 0x70000000

ldr r0, =REG32

orr r0, r0, #0x13

mcr p15,0,r0,c15,c2,4

/*关闭看门狗*/

#define WATCHDOG 0x7E004000

ldr r0, =WATCHDOG

mov r1, #0

str r1, [r0]

clean_bss:

ldr r0, =bss_start

ldr r1, =bss_end

mov r3, #0

cmp r0, r1

beq clean_done

clean_loop:

str r3, [r0], #4

cmp r0, r1

bne clean_loop

clean_done:

/* 初始化栈 S3C6410 8K 的SRAM 映射到0 地址处*/

ldr sp, =8*1024

bl main

halt:

b halt

事实上,汇编文件有“.S”和“.s”两种后缀,在以“.s”为后缀的汇编文件中,程序完全是由纯粹的汇编代码编写的。所谓的纯粹是相对以“.S”为后缀的汇编文件而言的,由于现代汇编工具引入了预处理的概念,允许在汇编代码(.S)中使用预处理命令。预处理命令以符号“#”开头,包括宏定义、文件包含和条件编译。在U-Boot和Linux内核源码中,这种编程方式运用非常广泛。

main.c文件内容如程序清单1.2所示,main.c中的main函数是运行完_start函数的跳转点。main()中首先定义了一个静态局部变量,初值为12,然后配置S3C6410处理器的GPM端口为输出、下拉模式,并将GPM端口低四位对应的管脚设为高电平(LED驱动管脚的电平为高时,LED熄灭)。最后判断flag是否等于12,如果等于就点亮LED,否则不点亮。从程序上看,这个判断语句好像多此一举、莫名其妙,因为flag期间并没有做任何改变。其实,这个变量是为讲解程序的运行地址和加载地址的概念而定义的,它与程序运行的位置有关。

程序清单1.2 main.c 文件内容

/*

* This is a part of the test project

* Author: LiQiang Date: 2013/04/01

* Licensed under the GPL-2 or later.

*/

#define GPMCON *((volatile unsigned long*)0x7F008820)

#define GPMDAT *((volatile unsigned long*)0x7F008824)

#define GPMPUD *((volatile unsigned long*)0x7F008828)

int main()

{

static int flag = 12;

GPMCON = 0x1111; /* 输出模式 */

GPMPUD = 0x55; /* 使能下拉 */

GPMDAT = 0x0f; /* 关闭LED */

if(12 == flag)

GPMDAT = 0x00;

else

GPMDAT = 0x0f;

while(1);

return 0;

}

将上面两个源码文件处理成中间目标文件,分别输入如下命令行:

arm-linux-gcc -o mian.o main.c –c

arm-linux-gcc -o start.o start.S –c

得到main.o、Start.o两个中间目标文件,供链接器使用。

2.链接

链接是汇编阶段生成的中间目标文件,相互查找自己所需要的函数与变量,重定向数据,完成符号解析的过程。包括对所有目标文件进行重定位、建立符号引用规则,同时为变量、函数等分配运行地址。函数与变量可能来源于其他中间文件或者库文件,如果没有找到所需的实现,链接器立即停止链接,给处错误提示。

利用一个链接脚本(.lds 后缀)来指导链接器工作。控制输出节(Outpat section)在映像文件中的布局。fortest.lds 是一个简单的链接脚本,指示了程序的运行地址(又称链接地址)为0x5000_0000以及text段、data段和bss段在映像文件中的空间排布顺序。fortest.lds文件的内容如下:

ENTRY(_start)

SECTIONS

{

. = 0x50000000;

. = ALIGN(4);

.text : {

start.o (.text)

* (.text)

}

.data : {

* (.data)

}

bss_start = .;

.bss : {

* (.bss)

}

bss_end = .;

}

(1)text段代码段(text segment),通常是用来存放程序执行代码的内存区域。这块区域的大小在程序编译时就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量。

(2)data段数据段(data segment),数据段是存放已经初始化不为0的静态变量的内存区域,静态变量包括全局变量和局部变量,它们与程序有着相同的生存期。

(3)bss 段bss segment,bbs 段与data 段类似,也是存放的静态变量的内存区域。与data 段不同的是,bbs段存放的是没有初始化或者初始化为0 的静态变量,并且bbs段不在生成的可执行二进制文件内。bss_start表示这块内存区域的起始地址,bss_end表示结束地址,它们由编译系统计算得到。未初始化的静态变量默认为 0,因此程序开始执行的时候,在 bss_start 到 bss_end内存中存储的数据都必须是0。

(4)其他段,上面3个段是编译系统预定义的段名,用户还能通过.section伪操作自定义段,在后面的移植过程中会发现,Linux 内核源码中为了合理地排布数据实现特定的功能,定义了各种各样的段。

在宿主机上输入以下命令行,完成中间的目标文件的链接和可执行二进制文件的格式转换。

arm-linux-ld –T test.lds -o test.elf start.o main.o

arm-linux-objcopy -O binary test.elf test.bin

arm-linux-objdump -D test.elf > test.dis

如图1.4所示是使用arm-linux-objcopy格式转换工具得到的二进制文件test.bin的内容,这些内容是处理器能够识别的机器码,我们往往难以直接阅读、理解它们的含义。使用arm-linux-objdump工具生成便于我们阅读的反汇编文件test.dis。

图1.4 二进制镜像文件内容

对比二进制文件test.bin的内容,耐心细致地分析反汇编文件,如程序清单1.3所示,可以提炼出大量的信息。

程序清单1.3 text.dis 文件内容

50000000 <_start>: /* 代码段起始位置程序的运行地址为0x5000_0000*/

50000000: e3a00207 mov r0, #1879048192 ; 0x70000000

50000004: e3800013 orr r0, r0, #19 ; 0x13

50000008: ee0f0f92 mcr 15, 0, r0, cr15, cr2, {4}

5000000c: e59f0030 ldr r0, [pc, #48]; 50000044 <halt+0x4>

50000010: e3a01000 mov r1, #0 ; 0x0

50000014: e5801000 str r1, [r0]

50000018 <clean_bss>: /* 清除bss段 */

50000018: e59f0028 ldr r0, [pc, #40]; 50000048 <halt+0x8>

5000001c: e59f1028 ldr r1, [pc, #40]; 5000004c <halt+0xc>

50000020: e3a03000 mov r3, #0 ; 0x0

50000024: e1500001 cmp r0, r1

50000028: 0a000002 beq 50000038 <clean_done>

5000002c <clean_loop>:

5000002c: e4803004 str r3, [r0], #4

50000030: e1500001 cmp r0, r1

50000034: 1afffffc bne 5000002c <clean_loop>

50000038 <clean_done>:

50000038: e3a0da02 mov sp, #8192 ; 0x2000 /* 初始化sp */

5000003c: eb000003 bl 50000050 <main> /* 跳转至mian() */

50000040 <halt>:

50000040: eafffffe b 50000040 <halt>

50000044: 7e004000 .word 0x7e004000

50000048: 500000e0 .word 0x500000e0

5000004c: 500000e0 .word 0x500000e0

50000050 <main>: /* main()*/

50000050: e52db004 push{fp} ; (str fp, [sp, #-4]!)

50000054: e28db000 add fp, sp, #0 ; 0x0

50000058: e3a0247f mov r2, #2130706432 ; 0x7f000000

5000005c: e2822b22 add r2, r2, #34816 ; 0x8800

50000060: e2822020 add r2, r2, #32 ; 0x20

50000064: e3a03c11 mov r3, #4352 ; 0x1100

50000068: e2833011 add r3, r3, #17 ; 0x11

5000006c: e5823000 str r3, [r2] /* GPMCON = 0x1111 */

50000070: e3a0347f mov r3, #2130706432 ; 0x7f000000

50000074: e2833b22 add r3, r3, #34816 ; 0x8800

50000078: e2833028 add r3, r3, #40 ; 0x28

5000007c: e3a02055 mov r2, #85 ; 0x55

50000080: e5832000 str r2, [r3] /* GPMPUD = 0x55 */

50000084: e3a0347f mov r3, #2130706432 ; 0x7f000000

50000088: e2833b22 add r3, r3, #34816 ; 0x8800

5000008c: e2833024 add r3, r3, #36 ; 0x24

50000090: e3a0200f mov r2, #15 ; 0xf

50000094: e5832000 str r2, [r3] /* GPMDAT = 0x0f */

50000098: e59f3038 ldr r3, [pc, #56]; 500000d8 <main+0x88> /* 读取flag变量存储地址 */

5000009c: e5933000 ldr r3, [r3]/* 读取flag变量的值 */

500000a0: e353000c cmp r3, #12 ; 0xc

500000a4: 1a000005 bne 500000c0 <main+0x70>

500000a8: e3a0347f mov r3, #2130706432 ; 0x7f000000

500000ac: e2833b22 add r3, r3, #34816 ; 0x8800

500000b0: e2833024 add r3, r3, #36 ; 0x24

500000b4: e3a02000 mov r2, #0 ; 0x0

500000b8: e5832000 str r2, [r3]

500000bc: ea000004 b 500000d4 <main+0x84>

500000c0: e3a0347f mov r3, #2130706432 ; 0x7f000000

500000c4: e2833b22 add r3, r3, #34816 ; 0x8800

500000c8: e2833024 add r3, r3, #36 ; 0x24

500000cc: e3a0200f mov r2, #15 ; 0xf

500000d0: e5832000 str r2, [r3]

500000d4: eafffffe b 500000d4 <main+0x84>

500000d8: 500000dc .word 0x500000dc

Disassembly of section .data:

500000dc <flag.1245>: /* flag变量的地址为0x5000_00dc,值为12 */

500000dc: 0000000c .word 0x0000000c

从test.dis反汇编文件中可知,test.bin包含了代码段和数据段,并没有包含bss段。我们知道,bbs 内存区域的数据初始值全部为零,区域的起始位置和结束位置在程序编译的时候预知。很容易想到在程序开始运行时,执行一小段代码将这个区域的数据全部清零即可,没必要在 test.bin包含全为0的bss段。编译器的这种机制有效地减小了镜像文件的大小,节约了磁盘容量。

main()函数的核心功能是验证flag变量是否等于12,现在追踪下这个操作的实现过程。要想读取flag的值,必须知道它的存储位置,首先执行指令“ldrr3, [pc, #56]”得到flag变量的地址(指针)。pc与56相加合成一个地址,它是相对pc偏移56产生的。pc+56地址处存放了flag变量的指针0x5000_00dc,读取出来存放到r3寄存器。然后执行指令“ldrr3, [r3]”,将内存0x5000_00dc地址处的值读出,这个值就是 flag,并覆盖 r3 寄存器。最后,判断 r3 寄存器是否等于 12。flag变量的地址在链接阶段已经被分配好了,固定在0x5000_00dc处,但是从代码中,我们没有找到对flag 变量赋初值的语句,尽管在main 函数已经用C 语句“flag = 12”对它赋初值。

现提供一个验证程序效果的简单方法:将 S3C6410 处理器设置为 SD 卡启动方式,使用SD_Writer软件将test.bin烧写至SD卡中,然后将SD卡插入单板的卡槽,复位启动即可。实际上,启动的时候test.bin被加载到内部SRAM中,SRAM映射到0地址处。这个简单方法可以用来验证一些裸板程序,方法实现的原理和SD_Writer软件用法现在不展开讨论,目前只要会使用即可。复位后,LED并没有点亮。

如果每次编译都要重复输入编译命令,操作起来很麻烦,为此test工程中建立了一个Makefile文件,内容如下:

test.bin: start.o main.o

arm-linux-ld -T fortest.lds -o test.elf start.o main.o

arm-linux-objcopy -O binary test.elf test.bin

arm-linux-objdump -D test.elf > test.dis

start.o : start.S

arm-linux-gcc -o start.o start.S -c

main.o : main.c

arm-linux-gcc -o main.o main.c -c

clean:

rm *.o test.*

当将链接脚本中的运行地址修改为0时,进入test目录,输入“make clean”命令清除旧的文件,再输入“make”重新编译程序,验证新生成的test.bin文件的效果,发现LED全部点亮,产生这个现象的原因在下一个小节讲述。

1.4.2 代码搬运

当程序执行时,必须把代码搬运到链接时所指定的运行地址空间,以保证程序在执行过程中对变量、函数等符号的正确引用。在带有操作系统的嵌入式系统中,这个过程由操作系统负责完成。而在裸机环境下,镜像文件的运行地址由程序员根据具体平台指定,加载地址又与处理器的设计密切相关。通常情况下,启动代码最先执行一段位置无关码,这段代码实现程序从加载地址到运行地址的重定位,或者将程序从外部存储介质直接拷贝至其运行地址。

1.位置无关码

位置无关码必须具有位置无关的跳转、位置无关的常量访问等特点,不能访问静态变量,都是相对pc的偏移量来进行函数的跳转或者常量的访问。在ARM 体系中,使用相对跳转指令b/bl实现程序跳转。指令中所跳转的目标地址用基于当前 PC 的偏移量来表示,与链接时分配给地址标号的绝对地址值无关,因而代码可以在任何位置正确的跳转,实现位置无关性。

使用ldr伪指令将一个常量读取到非pc的其他通用寄存器中,可实现位置无关的常量访问。例如:

ldr r0, =WATCHDOG

如果使用ldr伪指令将一个函数标号读取到pc,这是一条与位置有关的跳转指令,执行的结果是跳转到函数的运行地址处。

2.运行地址与加载地址

试想一下,当系统上电复位的时候,如果test.bin刚好位于0x5000_0000地址(flag的初值12位于0x5000_00dc),PC指向0x5000_0000地址,那么这段代码按照上述flag变量的读取步骤,能够准确无误地得到结果。但是,如果test.bin位于0地址(flag的初值12位于0xdc,LED不亮时的情况),PC指向0地址,程序依然从0x5000_00dc地址读取flag变量,实际上它的初值位于0xdc。这时从C语言的角度看,出现一个flag不等于它的初值的现象(期间没有改变flag)。出现错误的原因是在程序中使用了位置相关的变量,但运行地址与加载地址不一致(加载地址为 0,运行地址为0x5000_0000)。由此,能够容易理解运行地址和加载地址的含义:

加载地址是系统上电启动时,程序被加载到可直接执行的存储器的地址,也就是程序在RAM或者 Flash ROM 中的地址。因为有些存储介质只能用来存储数据不能执行程序,例如 SD 卡和NAND Flash 等,必须把程序从这些存储介质加载到可以执行的地址处。运行地址就是程序在链接时候确定的地址,比如fortest.lds链接脚本指定了程序的运行地址为0x5000_0000,那么链接器在为变量、函数等分配地址的时候就会以0x5000_0000作为参考。当加载地址和运行地址不相等时,必须使用与位置无关码把程序代码从它的加载地址搬运至运行地址,然后使用“ldr pc, =label”指令跳转到运行地址处执行。

1.4.3 混合编程

在嵌入式系统底层编程中,C语言和汇编两种编程语言的使用最广泛。C语言开发的程序具有可读性高,容易修改、移植和开发周期短等特点。但是,C语言在一些场合很难或无法实现特定的功能:底层程序需要直接与CPU内核打交道,一些特殊的指令在C语言中并没有对应的成分,例如关闭看门狗、中断的使能等;被系统频繁调用的代码段,对代码的执行效率要求严格的时候。事实上,CPU 体系结构并不一致,没有对内部寄存器操作的通用指令。汇编语言与 CPU的类型密切相关,提供的助记符指令能够方便直接地访问硬件,但要求开发人员对CPU的体系结构十分熟悉。在早期的微处理器中,由于处理器速度、存储空间等硬件条件的限制,开发人员不得不选用汇编语言开发程序。随着微处理器的发展,这些问题已经得到很好的解决。如果依然完全使用汇编语言编写程序,工作量会非常大,系统很难维护升级。大多数情况下,充分结合两种语言的特点,彼此相互调用,以约定规则传递参数,共享数据。

1.汇编函数与C语言函数相互调用

C 程序函数与汇编函数相互调用时必须严格遵循 ATPCS(ARMThumb Procedure Call Standard)。函数间约定R0、R1和R2为传入参数,函数的返回值放在R0 中。GNU ARM编译环境中,在汇编程序中要使用.global伪操作声明改汇编程序为全局的函数,可被外部函数调用。在C程序中要被汇编程序调用的C函数,同样需要用关键字extern声明。

程序清单1.4是从arch\arm\cpu\arm1176\start.S文件(U-Boot)中截取的代码片段,relocate_code函数用于重定位代码。它在C 程序中,通过relocate_code(addr_sp, id, addr)被调用。变量addr_sp、id和addr分别通过寄存器R0、R1和R3传递给汇编程序,实现了C函数和汇编函数数据的共享。

程序清单1.4 代码重定位函数

.globl relocate_code

relocate_code:

mov r4, r0 /* save addr_sp */

mov r5, r1 /* save addr of gd */

mov r6, r2 /* save addr of destination */

/* Set up the stack */

stack_setup:

mov sp, r4

adr r0, _start

cmp r0, r6

beq clear_bss  /* skip relocation */

mov r1, r6   /* r1 <- scratch for copy_loop */

ldr r3, _bss_start_ofs

add r2, r0, r3  /* r2 <- source end address  */

……

2.C语言内嵌汇编

当需要在C语言程序中内嵌汇编代码时,可以使用gcc提供的asm语句功能。

程序清单1.5是从Linux源码文件arch/arm/include/asm/atomic.h截取的一段代码,本节内容不分析函数的具体实现。对于初学者,这段代码看起来晦涩难懂,因为这不是标准C所定义的形式,而是gcc对C语言扩充的asm功能语句,用以在C语言程序中嵌入汇编代码。

程序清单1.5 整数原子加操作的实现

/*

* ARMv6 UP and SMP safe atomic ops. We use load exclusive and

* store exclusive to ensure that these are atomic. We may loop

* to ensure that the update happens.

*/

static inline void atomic_add(int i, atomic_t *v)

{

unsigned long tmp;

int result;

__asm__ __volatile__("@ atomic_add\n"

"1: ldrex %0, [%3]\n"

" add %0, %0, %4\n"

" strex %1, %0, [%3]\n"

" teq %1, #0\n"

" bne 1b"

: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)

: "r" (&v->counter), "Ir" (i)

: "cc");

}

asm语句最常用的格式为:

__asm __ __volatile__(" inst1 op1, op2, . \n"

" inst2 op1, op2, . \n"  /* 指令部分必选*/

...

" instN op1, op2, . \n"

: output_operands /* 输出操作数可选 */

: input_operands /* 输入操作数可选 */

: clobbered_operands /* 损坏描述部分可选*/

);

它由4个部分组成:指令部分、输出部分、输入部分和损坏描述部分。各部分使用“:”隔开,指令部分必不可少,其他3部分可选。但是如果使用了后面的部分,而前面部分为空,也需要用“:”分隔,相应部分内容为空。__asm__表示汇编语句的起始,__volatile__是一个可选项,加上它可以防止编译器优化时对汇编语句删除、移动。

指令部分,指令之间使用“\n”(也可以使用“;”或者“\n\t”)分隔。嵌入汇编指令的格式与标准汇编指令的格式大体相同,嵌入汇编指令的操作数使用占位符“%”预留位置,用以引用 C语言程序中的变量。操作数占位符的数量取决于 CPU 中通用寄存器的总数量,占位符的格式为%0,%1,……,%n。

输出、输入部分,这两部分用于描述操作数,不同的操作数描述语句之间用逗号分隔,每个操作数描述符由限定字符串和 C 语言表达式组成,当限定字符串中带有“=”时表示该操作数为输出操作数。限定字符串用于指示编译器如何处理C语言表达式与指令操作数之间的关系,限定字符串中的限定字母有很多种,有些是通用的,有些跟特定的体系相关。在程序清单1.5中:result、tmp和v->counter是输出操作数,分别赋给%0、%1和%2;v->counter和i是输入操作数,分别赋给%3和%4。其中,“r”表示把变量放入通用寄存器中;“I”表示0-31之间的常数。

1.5 嵌入式Linux移植常用软件

在进行嵌入式Linux学习与开发的过程中,需要使用到一些常用的开发工具,熟练使用这些软件,能让学习与开发达到事半功倍的效果。

1.5.1 SecureCRT

SecureCRT 是可以在 Window 环境下登录 UNIX 和 Linux 服务器主机的软件,它不仅支持SSH1、SSH2,而且支持TeInet和rlogin协议。

在Ubuntu宿主机上安装SSH。

zhuzhaoqi@zhuzhaoqi-desktop:~/sudo apt-get install openssh-server openssh-client

安装完成之后SSH功能自动打开。从网上下载SecureCRT软件,完成之后安装。

打开软件,弹出如图1.5所示的对话框。在“主机名”中输入Ubuntu虚拟机的ip地址,用户名即为Ubuntu宿主机的用户名。

图1.5 SecureCRT快速链接

连接上之后输入Ubuntu 宿主机密码即可进入SecureCRT连接界面。可以看到界面是单色且不支持中文,那是因为默认的字符编码不支持中文。字符编码是把字符集中的字符编码为指定集合中的某一对象,以便文本在计算机中存储和通过通信网络传递。

单击“选项”菜单,选择“会话选项”命令,打开“会话选项”对话框,如图1.6所示。

单击“字体”可以进行字体的任意设置。在“字符编码”中选择UTF-8,UTF-8是一种针对Unicode 的可变长度字符编码,它逐渐成为电子邮件、网页及其他存储或传送文字的应用优先采用的编码。

单击“终端”下的“仿真”进行颜色设置,如图1.7所示。

图1.6 SecureCRT字符编码配置
图1.7 SecureCRT颜色设置

在仿真终端中选择xterm,并且选择“ANSI颜色”复选框。

在Ubuntu宿主机中编辑/etc/profile,为vim编辑添加颜色显示效果。

zhuzhaoqi@zhuzhaoqi-desktop:~/sudo vim /etc/profile

在/etc/profile文末添加如下内容:

export TERM=xterm-color

添加完毕后执行如下内容,使之生效:

zhuzhaoqi@zhuzhaoqi-desktop:~/source /etc/profile

至此就完成了SecureCRT软件的安装。

1.5.2 Source Insight

由于U-Boot、Linux内核源码等都是相当庞大的工程,文件成千上万,为了方便编写和阅读代码,特此向读者推荐Source Insight 编辑器。

有时候,源码分析的难度不只在于源码本身,而在于如何使用更合适的分析代码的工具和手段。Source Insight非常好用,支持几乎所有的语言,如C、C++、ASM、PAS、ASP、HTML等。Source Insight与其他的编辑器产品相比较,增添了分析源代码,并在编辑的同时立刻提供给您有用的信息和分析等众多人性化功能。

目前能找到的最新版本是Source Insight 3.5.0072。从网上下载该软件,并进行安装。过程很简单,读者可自行完成。

安装完成之后打开Source Insight 软件,如图1.8 所示。

图1.8 打开Source Insight界面

由于Source Insight中C语言文件中所默认支持的只有.c和.h文件,因此需要增添支持其他后缀名的文件。点击“Options”菜单下的“Document Options”子菜单,将弹出如图1.9所示的对话框。

图1.9 添加支持其他后缀名文件

在“File filter”下面添加“*”,表示支持任何后缀名文件。

单击“Project”菜单下的“New Project”子菜单,弹出如图1.10 所示的对话框。

图1.10 新建工程

单击“Browse…”按钮可以更改工程存放路径,在“New project name”文本框下面输入新建工程的名字。完成之后单击“OK”按钮,进入如图1.11所示的对话框。

图1.11 源码存放位置

确定源码存放位置之后,单击“OK”按钮,进入添加源码界面,如图1.12所示。

图1.12 给工程添加源码

将所需要添加的源码——添加入工程。如果需要将所有U-Boot-2012.10工程文件全部添加进入工程,则单击左侧的U-Boot-2012.10目录,在单击“Add All”按钮,则全部添加,进入如图1.13所示的界面。

图1.13 添加文件

完成之后,单击“Project”菜单下的“Synchronize Files…”命令,同步所有工程文件,如图1.14所示。

图1.14 同步文件

单击“OK”按钮开始同步文件,这样工程中的各个变量、函数之间的关系就可以快速查阅了。同步完成之后便可进入阅读和编写工程文件,如图1.15所示。

图1.15 工程文件阅读和编写界面

第4章 Linux设备驱动程序设计

4.1 设备驱动概述

Linux系统将设备分成3种基本类型:字符设备、块设备、网络接口。

(1)字符设备

字符设备是一个能够像字节流一样被访问的设备,字符终端(/dev/console)和串口(/dev/ttys0)就是两个字符设备。字符设备可以通过文件系统节点来访问,比如/dev/tty1和/dev/lp0等。这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道。

(2)块设备

块设备和字符设备相类似,块设备也是通过/dev目录下的文件系统节点来进行访问的。在大多数UNIX系统中,进行I/O操作时块设备每次只能传输一个或多个完整的块;在Linux系统中,应用程序可以像字符设备一样地读写块设备,允许一次传递任意多字节的数据。块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的接口。

(3)网络接口

任何网络事务都是经过一个网络接口形成的,即一个能够和其他主机交换数据的设备。网络接口由内核中的网络子系统驱动,负责发送和接收数据包,但它不需要了解每项事物如何映射到实际传送的数据包。

4.2 字符设备驱动

Linux 操作系统将所有的设备都会看成是文件,因此当我们需要访问设备时,都是通过操作文件的方式进行访问。对字符设备的读写是以字节为单位进行的。

对字符设备驱动程序的学习过程,主要以两个具有代表性且在OK6410开发平台可实践性的字符驱动展开分析,分别为LED驱动程序、ADC驱动程序。

4.2.1 LED驱动程序设计

为了展现LED的裸板程序和基于Linux系统的LED驱动程序的区别与减少难度梯度,在写LED驱动程序之前很有必要先看一下LED的裸板程序是怎样设计的。

1.LED裸板程序

OK6410开发平台中有4个LED灯,原理图如图4.1所示。

图4.1 LED原理图

从图4.1中可知,4个LED采用的是共阳极连接方式,&nbsp;GPM0~GPM3分别控制着 LED1~LED4。而 GPMCON 寄存器地址为:0x7F008820;GPMDAT寄存器地址为:0x7F008824。那么GPM中3个寄存器宏定义为:

/*===============================================================

** 基地址的定义

===============================================================*/

#define AHB_BASE (0x7F000000)

/****************************************************************

** GPX的地址定义

****************************************************************/

#define GPX_BASE (AHB_BASE+0x08000)

……

/****************************************************************

**  GPM寄存器地址定义

****************************************************************/

#define GPMCON  (*(volatile unsigned long *)(GPX_BASE + 0x0820))

#define GPMDAT  (*(volatile unsigned long *)(GPX_BASE + 0x0824))

#define GPMPUD  (*(volatile unsigned long *)(GPX_BASE + 0x0828))

将GPM0~GPM3设置为输出功能:

/* GPM0,1,2,3 设为输出引脚 */

/*

** 每一个GPXCON 的引脚有 4 位二进制进行控制

** 0000-输入 0001-输出

*/

GPMCON = 0x1111;

点亮LED1,则是让GPM3~GPM0输出:1110。

GPMDAT = 0x0e;

点亮LED3,则是让GPM3~GPM0输出:1011。

GPMDAT = 0x0b;

2.LED驱动程序

有了LED裸板程序的基础,那么移植到Linux系统LED驱动设备程序的难度也不会很大了。但是在Linux中,特别注意《s3c6410用户手册》提供的GPM寄存器地址不能直接用于Linux中。

在一般情况下,Linux系统中,进程的4GB(232)内存空间被划分成为两个部分:用户空间(3G)和内核空间(1GB),大小分别为0~3GB和3~4GB,如图4.2所示。

在 3~4GB 之间的内核空间中,从低地址到高地址依次为:系统物理内存映射区、VMALLOC_OFFSET、vmalloc 用来分配物理地址非连续的内存空间、8KB 隔离带、高端内存永久映射区、高端内存固定映射区。

在通常情况下,进程只能访问用户空间的虚拟地址,不能访问内核空间。

每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射的,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,内核的虚拟空间独立于其他程序。

图4.2 Linux内存空间

在内核中,访问I/O内存之前,我们只有I/O内存的物理地址,这样是无法通过软件直接访问的,需要首先用ioremap()函数将设备所处的物理地址映射到内核虚拟地址空间(3GB~4GB)。然后才能根据映射所得到的内核虚拟地址范围,通过访问指令访问这些I/O内存资源。

一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚拟地址空间内(通过页表),然后才能根据映射所得到的核心虚拟地址范围,通过访内指令访问这些I/O内存资源。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚拟地址空间(3GB~4GB)中,如下所示:

void * ioremap(unsigned long phys_addr, unsigned long size,

unsigned long flags);

iounmap函数用于取消ioremap()所做的映射,如下所示:

void iounmap(void * addr);

到这里应该明白,像GPMCON(0x7F00 8820)这个物理地址是不能直接操控的,必须通过映射到内核的虚拟地址中,才能进行操作。

现在开始设计第一个LED驱动程序。

字符驱动程序所要包含的头文件主要位于 include/linux 及/arch/arm/mach-s3c64xx/include/mach目录下,如下LED驱动程序所包含的头文件:

/*

* head file

*/

//moudle.h 包含了大量加载模块需要的函数和符号的定义

#include <linux/module.h>

//kernel.h以便使用printk()等函数

#include <linux/kernel.h>

//fs.h 包含常用的数据结构,如struct file 等

#include <linux/fs.h>

//uaccess.h 包含copy_to_user()、copy_from_user()等函数

#include <linux/uaccess.h>

//io.h 包含inl()、outl()、readl()、writel()等I/O 操作函数

#include <linux/io.h>

#include <linux/miscdevice.h>

#include <linux/pci.h>

//init.h来指定你的初始化和清理函数,例如:module_init(init_function)、module_exit(cleanup_function)

#include <linux/init.h>

#include <linux/delay.h>

#include <linux/device.h>

#include <linux/cdev.h>

#include <linux/gpio.h>

//irq.h中断与并发请求事件

#include <asm/irq.h>

//下面这些头文件是I/O口在内核的虚拟映射地址,涉及I/O口的操作所必须包含的

//#include <mach/gpio.h>

#include <mach/regs-gpio.h>

#include <plat/gpio-cfg.h>

#include <mach/hardware.h>

#include <mach/map.h>

上面所列出的头文件即是本次LED驱动程序所需要包含的头文件。

#define DEVICE_NAME "led"

#define LED_MAJOR  240    /*主设备号*/

这是LED驱动程序的驱动名称和主设备号。

设备节点位于/dev目录下,如下所示,例举出了ubuntu系统/dev/vcs*的设备节点:

zhuzhaoqi@zhuzhaoqi-desktop:~$ ls -l /dev/vcs*

……

crw-rw---- 1 root tty 7, 7 2013-04-09 20:56 /dev/vcs7

crw-rw---- 1 root tty 7, 128 2013-04-09 20:56 /dev/vcsa

……

/dev/vcs7设备节点的主设备号为:7,次设备号为:7;/dev/vcsa设备节点的主设备号为:7,次设备号为:128。

#define LED_ON  0

#define LED_OFF  1

这是LED灯打开或者关闭的宏定义,由于OK6410开发平台的4个LED是共阳连接,所以输出1即为熄灭LED,输出0为点亮LED。

字符驱动程序中实现了open、close、read、write等系统调用。

open函数指针的声明位于fs.h的file_operations结构体中,如下所示:

struct file_operations {

……

int (*open) (struct inode *, struct file *);

……

};

open函数指针的回调函数led_open()完成的任务是设置GPM的输出模式。

static int led_open(struct inode *inode,struct file *file)

{

unsigned int i;

/*设置GPM0~GPM3为输出模式*/

for (i = 0; i < 4; i++)

{

s3c_gpio_cfgpin(S3C64XX_GPM(i),S3C_GPIO_OUTPUT);

printk("The GPMCON %x is %x \n",i,s3c_gpio_getcfg(S3C64XX_GPM(i)) );

}

printk("Led open... \n");

return 0;

}

s3c_gpio_cfgpin()函数原型位于gpio-cfg.h中,如下:

extern int s3c_gpio_cfgpin(unsigned int pin, unsigned int to);

内核对这个函数是这样注释的:s3c_gpio_cfgpin()函数用于改变引脚的 GPIO 功能。参数pin是GPIO的引脚名称,参数to是需要将GPIO这个引脚设置成为的功能。

GPIO的名称在arch/arm/mach-s3c6400/include/mach/gpio.h进行了宏定义:

/* S3C64XX GPIO number definitions. */

#define S3C64XX_GPA(_nr) (S3C64XX_GPIO_A_START + (_nr))

#define S3C64XX_GPB(_nr) (S3C64XX_GPIO_B_START + (_nr))

#define S3C64XX_GPC(_nr) (S3C64XX_GPIO_C_START + (_nr))

#define S3C64XX_GPD(_nr) (S3C64XX_GPIO_D_START + (_nr))

#define S3C64XX_GPE(_nr) (S3C64XX_GPIO_E_START + (_nr))

#define S3C64XX_GPF(_nr) (S3C64XX_GPIO_F_START + (_nr))

#define S3C64XX_GPG(_nr) (S3C64XX_GPIO_G_START + (_nr))

#define S3C64XX_GPH(_nr) (S3C64XX_GPIO_H_START + (_nr))

#define S3C64XX_GPI(_nr) (S3C64XX_GPIO_I_START + (_nr))

#define S3C64XX_GPJ(_nr) (S3C64XX_GPIO_J_START + (_nr))

#define S3C64XX_GPK(_nr) (S3C64XX_GPIO_K_START + (_nr))

#define S3C64XX_GPL(_nr) (S3C64XX_GPIO_L_START + (_nr))

#define S3C64XX_GPM(_nr) (S3C64XX_GPIO_M_START + (_nr))

#define S3C64XX_GPN(_nr) (S3C64XX_GPIO_N_START + (_nr))

#define S3C64XX_GPO(_nr) (S3C64XX_GPIO_O_START + (_nr))

#define S3C64XX_GPP(_nr) (S3C64XX_GPIO_P_START + (_nr))

#define S3C64XX_GPQ(_nr) (S3C64XX_GPIO_Q_START + (_nr))

S3C64XX_GPIO_M_START的定义如下:

enum s3c_gpio_number {

S3C64XX_GPIO_A_START = 0,

S3C64XX_GPIO_B_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_A),

S3C64XX_GPIO_C_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_B),

S3C64XX_GPIO_D_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_C),

S3C64XX_GPIO_E_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_D),

S3C64XX_GPIO_F_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_E),

S3C64XX_GPIO_G_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_F),

S3C64XX_GPIO_H_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_G),

S3C64XX_GPIO_I_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_H),

S3C64XX_GPIO_J_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_I),

S3C64XX_GPIO_K_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_J),

S3C64XX_GPIO_L_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_K),

S3C64XX_GPIO_M_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_L),

S3C64XX_GPIO_N_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_M),

S3C64XX_GPIO_O_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_N),

S3C64XX_GPIO_P_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_O),

S3C64XX_GPIO_Q_START = S3C64XX_GPIO_NEXT(S3C64XX_GPIO_P),

};

S3C64XX_GPIO_NEXT的定义:

#define S3C64XX_GPIO_NEXT(__gpio) \

((__gpio##_START) + (__gpio##_NR) + CONFIG_S3C_GPIO_SPACE + 1)

宏定义一层一层很多,但是通过这个设置,可以很方便地选择想要的任何一个GPIO口进行操作。

GPIO功能设置位于gpio-cfg.h中:

#define S3C_GPIO_SPECIAL_MARK (0xfffffff0)

#define S3C_GPIO_SPECIAL(x) (S3C_GPIO_SPECIAL_MARK | (x))

/* Defines for generic pin configurations */

#define S3C_GPIO_INPUT(S3C_GPIO_SPECIAL(0))

#define S3C_GPIO_OUTPUT (S3C_GPIO_SPECIAL(1))

#define S3C_GPIO_SFN(x) (S3C_GPIO_SPECIAL(x))

通过上面的宏定义可知,GPIO 的引脚功能有输入、输出,和你想要的任何可以实现的功能设置,S3C_GPIO_SFN(x)这个函数即是通过设定x的值,实现任何存在功能的设置。如果要设置GPM0~GPM3为输出功能,则:

for (i = 0; i < 4; i++) {

s3c_gpio_cfgpin(S3C64XX_GPM(i),S3C_GPIO_OUTPUT);

}

通过这样的操作,设置就显得比较简洁实用。

s3c_gpio_getcfg(S3C64XX_GPM(i))

这行代码的作用是获取GMP(argv)的当前值。这个函数的原型在include/linux/gpio.h中:

static inline void gpio_get_value(unsigned int gpio)

{

__gpio_get_value(gpio);

}

完成端口模式设定,接下来的程序是完成LED操作。在fs.h的file_operations结构体中,有unlocked_ioctl函数指针的声明,如下所示:

struct file_operations {

……

long (*unlocked_ioctl) (struct file *,unsigned int,unsigned long);

……

};

unlocked_ioctl函数指针所要回调的led_ioctl()函数即是需要实现应用层对LED1~LED4的控制操作。

static long led_ioctl ( struct file *file, unsigned int cmd, \

unsigned long argv )

{

if (argv > 4) {

return -EINVAL;

}

printk("LED ioctl... \n");

/* 获取应用层的操作 */

switch(cmd) {

/* 如果是点亮LED(argv) */

case LED_ON:

gpio_set_value(S3C64XX_GPM(argv),0);

printk("LED ON \n");

printk( "S3C64XX_GPM(i) = %x\n",gpio_get_value(S3C64XX_GPM(argv)) );

return 0;

/* 如果是熄灭LED(argv) */

case LED_OFF:

gpio_set_value(S3C64XX_GPM(argv),1);

printk("LED OFF \n");

printk( "S3C64XX_GPM(i) = %x \n",gpio_get_value(S3C64XX_GPM(argv)) );

return 0;

default:

return -EINVAL;

}

}

本函数调用了GPIO端口值设定函数。

gpio_set_value(S3C64XX_GPM(argv),1);

这是设定GMP(argv)输出为1。函数的原型位于include/linux/gpio.h中:

static inline void gpio_set_value(unsigned int gpio, int value)

{

__gpio_set_value(gpio, value);

}

release 函数指针所要回调的函数led_release ()函数:

static int led_release(struct inode *inode,struct file *file)

{

printk("zhuzhaoqi >>> s3c6410_led release \n");

return 0;

}

这是驱动程序的核心控制,各个函数指针所对应的回调函数:

struct file_operations led_fops = {

.owner  = THIS_MODULE,

.open  = led_open,

.unlocked_ioctl = led_ioctl,

.release  = led_release,

};

由于 Linux3.8.3 内核中没有 ioctl 函数指针,取而代之的是 unlocked_ioctl 函数指针实现对led_ioctl()函数的回调。

驱动程序的加载分为静态加载和动态加载,将驱动程序编译进内核称为静态加载,将驱动程序编译成模块,使用时再加载称为动态加载。动态加载模块的扩展名为:.ko,使用 insmod 命令进行加载,使用rmmod命令进行卸载。

static int __init led_init(void)

{

int rc;

printk("LEDinit... \n");

rc = register_chrdev(LED_MAJOR,"led",&led_fops);

if (rc < 0)

{

printk("register %s char dev error\n","led");

return -1;

}

printk("OK!\n");

return 0;

}

_init修饰词对内核是一种暗示,表明该初始化函数仅仅在初始化期间使用,在模块装载之后,模块装载器就会将初始化函数释放掉,这样就能将初始化函数所占用的内存释放出来以作他用。

当使用insmod命令加载LED驱动模块时,led_init()初始化函数将被调用,向内核注册LED驱动程序。

static void __exit led_exit(void)

{

unregister_chrdev(LED_MAJOR,"led");

printk("LED exit...\n");

}

_exit这个修饰词告诉内核这个退出函数仅仅用于模块卸载,并且仅仅能在模块卸载或者系统关闭时被调用。

当使用rmmod 命令卸载LED驱动模块时,led_exit ()清除函数将被调用,向内核注册LED驱动程序。

module_init(led_init);

module_exit(led_exit);

module_init 和 module_exit 是强制性使用的,这个宏会在模块的目标代码中增加一个特殊的段,用于说明函数所在位置。如果没有这个宏,则初始化函数和退出函数永远不会被调用。

MODULE_LICENSE("GPL");

如果没有声明LICENSE,模块被加载时,会给出处理内核被污染(kernel taint)的警告。如果在zzq_led.c中没有许可证(LICENSE),则会给出如下提示:

[YJR@zhuzhaoqi 3.8.3]# insmod zzq_led.ko

zzq_led: module license 'unspecified' taints kernel.

Disabling lock debugging due to kernel taint

Linux遵循GNU通用公共许可证(GPL),GPL是由自由软件基金会为GNU项目设计,它允许任何人对其重新发布甚至销售。

当然,也许程序还会有驱动程序的作者和描述信息:

MODULE_AUTHOR("zhuzhaoqi jxlgzzq@163.com");

MODULE_DESCRIPTION("OK6410(S3C6410) LED Driver");

完成驱动程序的设计之后,将 zzq_led.c 驱动程序放置于/drivers/char 目录下,打开 Makefile文件:

zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/linux-3.8.3/drivers/char$ gedit Makefile

在Makefile中添加LED驱动:

obj-m      += zzq_led.o

回到内核的根目录执行make modules 命令生成LED 驱动模块:

zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/linux-3.8.3$ make modules

……

CC [M] drivers/char/zzq_led.o

……

编译完成之后在/drivers/char 目录下会生成 zzq_led.ko 模块,将其拷贝到文件系统下面的/lib/modules/3.8.3(如果没有3.8.3目录,则建立)目录下。

加载LED驱动模块:

[YJR@zhuzhaoqi]\# cd lib/module/3.8.3/

[YJR@zhuzhaoqi]\# ls

zzq_led.ko

[YJR@zhuzhaoqi]\# insmod zzq_led.ko

LED init...

OK!

根据信息输出可知加载zzq_led.ko驱动模块成功。通过lsmod查看加载模块:

[YJR@zhuzhaoqi]\# lsmod

zzq_led 1548 0 - Live 0xbf000000

在/dev目录下建立设备文件,进行如下操作:

[YJR@zhuzhaoqi]\# mknod /dev/led c 240 0

是否建立成功,可以查看/dev下的节点得知:

[YJR@zhuzhaoqi]\# ls /dev/l*

/dev/led  /dev/log  /dev/loop-control

说明LED设备文件已经成功建立。

3.LED应用程序

驱动程序需要应用程序对其操控。程序如下:

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/ioctl.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#define LED_ON 0

#define LED_OFF 1

/*

* LED 操作说明信息输出

*/

void usage(char *exename)

{

printf("How to use: \n");

printf(" %s <LED Number><on/off> \n", exename);

printf(" LED Number = 1, 2, 3 or 4 \n");

}

/*

* 应用程序主函数

*/

int main(int argc, char *argv[])

{

unsigned int led_number;

if (argc != 3) {

goto err;

}

int fd = open("/dev/led",2,0777);

if (fd < 0) {

printf("Can't open /dev/led \n");

return -1;

}

printf("open /dev/led ok ... \n");

led_number = strtoul(argv[1], 0, 0) - 1;

if (led_number > 3) {

goto err;

}

/* LED ON */

if (!strcmp(argv[2], "on")) {

ioctl(fd, LED_ON, led_number);

}

/* LED OFF */

else if (!strcmp(argv[2], "off")) {

ioctl(fd, LED_OFF, led_number);

}

else {

goto err;

}

close(fd);

return 0;

err:

if (fd > 0) {

close(fd);

}

usage(argv[0]);

return -1;

}

在main()函数中,涉及了open()函数,其原型如下:

int open( const char * pathname,int flags, mode_t mode);

当然,很多open函数中的入口参数也只有2个,原型如下:

int open( const char * pathname, int flags);

第一个参数pathname是一个指向将要打开的设备文件途径的字符串。

第二个参数flags是打开文件所能使用的旗标,常用的几种旗标有:

O_RDONLY:以只读方式打开文件

O_WRONLY:以只写方式打开文件

O_RDWR:以可读写方式打开文件

上述3种常用的旗标是互斥使用,但可与其他的旗标进行或运算符组合。

第3个参数mode是使用该文件的权限。比如777、755等。

通过这个应用程序实现对LED驱动程序的控制,为了更加方便快捷地编译这个应用程序,为其写一个Makefile文件,如下所示:

#交叉编译链安装路径

CC = /usr/local/arm/4.4.1/bin/arm-linux-gcc

zzq_led_app:zzq_led_app.o

$(CC) -o zzq_led_appzzq_led_app.o

zzq_led_app.o:zzq_led_app.c

$(CC) -c zzq_led_app.c

clean :

rm zzq_led_app.o zzq_led_app

执行Makefile之后会生成zzq_led_app可执行应用文件,如下:

zhuzhaoqi@zhuzhaoqi-desktop:~/LDD/linux-3.8.3/zzq_led$ make

/usr/local/arm/4.4.1/bin/arm-linux-gcc -c zzq_led_app.c

/usr/local/arm/4.4.1/bin/arm-linux-gcc -o zzq_led_app zzq_led_app.o

zhuzhaoqi@zhuzhaoqi-desktop:~/LDD/linux-3.8.3/zzq_led$ ls

Makefile zzq_led_app zzq_led_app.c zzq_led_app.o zzq_led.c

将生成的zzq_led_app可执行应用文件拷贝到根文件系统的/usr/bin目录下,执行应用文件,如下操作:

[YJR@zhuzhaoqi]\# ./zzq_led_app

How to use:

./zzq_led_app <LED Number><on/off>

LED Number = 1, 2, 3 or 4

根据信息提示可以进行对LED驱动程序的控制,点亮LED1,则如下:

[YJR@zhuzhaoqi]\# ./zzq_led_app 1 on

The GPMCON 0 is fffffff1

The GPMCON 1 is fffffff1

The GPMCON 2 is fffffff1

The GPMCON 3 is fffffff1

zhuzhaoqi >>> LED open...

LED ioctl...

LED ON

S3C64XX_GPM(i) = 0

LED release...

open /dev/led ok ...

此时可以看到LED1点亮。

本节配套视频位于光盘中“嵌入式Linux开发实用教程视频”目录下第四章01课(字符设备驱动之LED)。

4.2.2 ADC驱动程序设计

A/D转换即是将模拟量转换为数字量,在物联网迅速发展的今天,作为物联网的感知前端传感器也随之迅速更新,压力、温度、湿度等众多模拟信号的处理都需要涉及A/D转换,因此A/D驱动程序在学习嵌入式中占据着重要地位。

1.S3C6410的ADC控制寄存器简介

S3C6410控制芯片自带有4路独立专用A/D转换通道,如图4.3所示。

图4.3 A/D转换连接图

通过三星公司提供的《S3C6410 用户手册》可知,ADCCON 为ADC控制寄存器,地址为:0x7E00 B0000。ADCCON 的复位值为:0x3FC4,即为:0011 1111 1100 0100。

#define S3C_ADCREG(x)   (x)

#define S3C_ADCCON   S3C_ADCREG(0x00)

ADCCON控制寄存器具有16位,每一位都能通过赋值来实现其相对应的功能。

ADCCON[0]:ENABLE_START,A/D 转换开始启用。如果READ_START 启用,这个值是无效的。ENABLE_START = 0,无行动;ENABLE_START = 1,A/D 转换开始和该位被清理后开启。ADCCON[0]的复位值为0,即复位之后默认为无行动。

#define S3C_ADCCON_NO_ENABLE_START  (0<<0)

#define S3C_ADCCON_ENABLE_START (1<<0)

ADCCON[1]:READ_START,A/D 转换开始读取。READ_START = 0,禁用开始读操作;READ_START = 1,启动开始读操作。ADCCON[1]的复位值为0,禁用开始读操作。

#define S3C_ADCCON_NO_READ_START  (0<<1)

#define S3C_ADCCON_READ_START  (1<<1)

ADCCON[2]:STDBM,待机模式选择。STDBM = 0,正常运作模式;STDBM = 1,待机模式。ADCCON[2]的复位值为1,待机模式。

#define S3C_ADCCON_RUN (0<<2)

#define S3C_ADCCON_STDBM  (1<<2)

ADCCON[5:3]:SEL_MUX,模拟输入通道选择。SEL_MUX = 000,AIN0;SEL_MUX = 001, AIN1;SEL_MUX = 010,AIN2;SEL_MUX = 011,AIN3;SEL_MUX = 100,YM;SEL_MUX = 101,YP;SEL_MUX = 110,XM;SEL_MUX = 111,XP。ADCCON[5:3]的复位值为000,选用AIN0通道。

#define S3C_ADCCON_RESSEL_10BIT_1 (0x0<<3)

#define S3C_ADCCON_RESSEL_12BIT_1 (0x1<<3)

#define S3C_ADCCON_MUXMASK (0x7<<3)

#define S3C_ADCCON_SELMUX(x)  (((x)&0x7)<<3) //任意通道的选择

ADCCON[13:6]:PRSCVL,ADC 预定标器值0xFF。数据值:5~255。ADCCON[13:6]的复位值为1111 1111,即为0xFF。

#define S3C_ADCCON_PRSCVL(x)  (((x)&0xFF)<<6) // 任意值设定

#define S3C_ADCCON_PRSCVLMASK  (0xFF<<6) //复位值

ADCCON[14]:PRSCEN,ADC预定标器启动。PRSCEN = 0,禁用;PRSCEN = 0,启用。ADCCON[14]的复位值为0,禁用ADC预定标器。

#define S3C_ADCCON_NO_PRSCEN  (0<<14)

#define S3C_ADCCON_PRSCEN  (1<<14)

ADCCON[15]:ECFLG,转换的结束标记(只读)。ECFLG = 0,A/D 转换的过程中;ECFLG = 1,A/D 转换结束。ADCCON[15]的复位值为0,A/D 转换的过程中。

#define S3C_ADCCON_ECFLG_ING  (0<<15)

#define S3C_ADCCON_ECFLG  (1<<15)

ADCDAT0 寄存器为ADC 的数据转换寄存器。地址为:0x7E00B00C。

ADCDAT0[9:0]:XPDATA,X 坐标的数据转换(包括正常的ADC 的转换数据值)。数据值:0x000~0x3FF。

ADCDAT0[11:10]:保留。当启用12位AD时作为转换数据值使用。

#define S3C_ADCDAT0_XPDATA_MASK (0x03FF)

#define S3C_ADCDAT0_XPDATA_MASK_12BIT (0x0FFF)

上面所介绍的是专用A/D转换通道常用寄存器,LCD触摸屏A/D转换有另外的A/D通道。

2.ADC驱动程序

A/D 转化驱动由于也属于字符设备驱动,所以其程序设计流程和 LED 驱动大体一致。在linux-3.8.3/drivers/char 目录下新建 zzqadc.c 驱动文件,当然也可写好之后再拷贝到 linux-3.8.3/drivers/char目录下。

zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/linux-3.8.3/drivers/char$ vim zzqadc.c

头文件是必不可少的,A/D驱动程序所要包含的头文件如下所示:

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/slab.h>

#include <linux/input.h>

#include <linux/init.h>

#include <linux/errno.h>

#include <linux/serio.h>

#include <linux/delay.h>

#include <linux/clk.h>

#include <linux/sched.h>

#include <linux/cdev.h>

#include <linux/miscdevice.h>

#include <asm/io.h>

#include <asm/irq.h>

#include <asm/uaccess.h>

#include <mach/map.h>

#include <mach/regs-clock.h>

#include <mach/regs-gpio.h>

#include <plat/regs-adc.h>

与LED驱动程序所包含的头文件相比较,多了ADC专用的头文件,如regs-adc.h,这个头文件位于linux-3.8.3/arch/arm/plat-samsung/include/plat目录下。

static void __iomem *base_addr;

static struct clk *adc_clock;

#define __ADCREG(name) (*(unsigned long int *)(base_addr + name))

自从linux-2.6.9版本开始便把_iomem加入内核,_iomem是表示指向一个I/O的内存空间。将_iomem加入linux,主要是考虑到驱动程序的通用性。由于不同的CPU体系结构对I/O空间的表示可能不同,但是当使用_iomem时,就会忽略对变量的检查,因为_iomem使用的是void。

#define S3C_ADCREG(x) (x)

#define S3C_ADCCON  S3C_ADCREG(0x00)

#define S3C_ADCDAT0  S3C_ADCREG(0x0C)

/* ADC contrl */

#define ADCCON   _ADCREG(S3C_ADCCON)

/* read the ADdata */

#define ADCDAT0   _ADCREG(S3C_ADCCON)

声明ADC控制寄存器的地址。

/* The set of ADCCON */

#define S3C_ADCCON_ENABLE_START  (1 << 0)

#define S3C_ADCCON_READ_START   (1 << 1)

#define S3C_ADCCON_RUN    (0 << 2)

#define S3C_ADCCON_STDBM    (1 << 2)

#define S3C_ADCCON_SELMUX(x)   ( ((x)&0x7) << 3 )

#define S3C_ADCCON_PRSCVL(x)   ( ((x)&0xFF) << 6 )

#define S3C_ADCCON_PRSCEN    (1 << 14)

#define S3C_ADCCON_ECFLG    (1 << 15)

/* The set of ADCDAT0 */

#define S3C_ADCDAT0_XPDATA_MASK  (0x03FF)

#define S3C_ADCDAT0_XPDATA_MASK_12BIT (0x0FFF)

根据上一小节对ADCCON和ADCDAT0的介绍,可以很容易写出上面的宏定义。

在使用ADC之前,先得对ADC进行初始化设置,由于OK6410开发平台自带的A/D电压采样电路选用的是 AIN0 通道,则这里需要对 AIN0 进行初始化。初始化阶段需要完成的事情为:A/D 转换开始和该位被清理后开启、正常运作模式、模拟输入通道选择AIN0、ADC 预定标器值0xFF、ADC预定标器启动。

/*

* AIN0 init

*/

static int adc_init(void)

{

ADCCON = S3C_ADCCON_PRSCEN | S3C_ADCCON_PRSCVL(0xFF) | \

S3C_ADCCON_SELMUX(0x00) | S3C_ADCCON_RUN;

ADCCON |=S3C_ADCCON_ENABLE_START;

return 0;

}

open函数指针的实现函数adc_open():

/*

* open dev

*/

static int adc_open(struct inode *inode, struct file *filp)

{

adc_init();

return 0;

}

release函数指针的实现函数adc_release():

/*

* release dev

*/

static int adc_release(struct inode *inode,struct file *filp)

{

return 0;

}

read()函数指针的实现函数adc_read(),这个函数的作用是读取ADC采样数据。

/*

* adc_read

*/

static ssize_t adc_read(struct file *filp, char __user *buff,

size_t size, loff_t *ppos)

{

ADCCON |= S3C_ADCCON_READ_START;

/* check the adc Enabled ,The [0] is low*/

while(ADCCON & 0x01);

/* check adc change end */

while(!(ADCCON & 0x8000));

/* return the data of adc */

return (ADCDAT0 & S3C_ADCDAT0_XPDATA_MASK);

}

ADC驱动程序的核心控制部分:

static struct file_operations dev_fops =

{

.owner = THIS_MODULE,

.open= adc_open,

.release = adc_release,

.read= adc_read,

};

static struct miscdevice misc =

{

.minor = MISC_DYNAMIC_MINOR,

.name = “zzqadc“,

.fops = &dev_fops,

};

加载insmod驱动程序,如下所示:

static int __init dev_init()

{

int ret;

/* Address Mapping */

base_addr = ioremap(SAMSUNG_PA_ADC,0X20);

if(base_addr == NULL)

{

printk(KERN_ERR"failed to remap \n");

return -ENOMEM;

}

/* Enabld acd clock */

adc_clock = clk_get(NULL,"adc");

if(!adc_clock)

{

printk(KERN_ERR"failed to get adc clock \n");

return -ENOENT;

}

clk_enable(adc_clock);

ret = misc_register(&misc);

printk("dev_init return ret: %d \n", ret);

return ret;

}

加载 insmod 驱动程序,这里使用到了 ioremap()函数。在内核驱动程序的初始化阶段,通过ioremap()函数将物理地址映射到内核虚拟空间;在驱动程序的 mmap 系统调用中,使用remap_page_range()函数将该块ROM映射到用户虚拟空间。这样内核空间和用户空间都能访问这段被映射后的虚拟地址。

ioremap()宏定义在asm/io.h内:

#define ioremap(cookie,size)   __ioremap(cookie,size,0)

_ioremap函数原型为(arm/mm/ioremap.c):

void _iomem * _ioremap(unsigned long phys_addr, size_t size, unsigned longflags);

phys_addr:要映射的起始的I/O地址;

size:要映射的空间的大小;

flags:要映射的I/O空间和权限有关的标志。

该函数返回映射后的内核虚拟地址(3GB~4GB),接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。

base_addr = ioremap(SAMSUNG_PA_ADC,0X20);

这行代码即是将SAMSUNG_PA_ADC(0x7E00 B000)映射到内核,返回内核的虚拟地址给base_addr。

clk_get(NULL,"adc")可以获得adc时钟,每一个外设都有自己的工作频率,PRSCVL是A/D转换器时钟的预分频功能时A/D 时钟的计算公式,A/D 时钟 = PCLK / (PRSCVL+1)。

注意:AD时钟最大为2.5MHz并且应该小于PCLK的1/5。

adc_clock = clk_get(NULL,"adc");

即为获取adc的工作时钟频率。

ret = misc_register(&misc);

创建杂项设备节点。这里使用到了杂项设备,杂项设备也是在嵌入式系统中用得比较多的一种设备驱动。在 Linux 内核的 include/linux 目录下有 miscdevice.h 文件,要把自己定义的 miscdevice 从设备定义到这里。其实是因为这些字符设备不符合预先确定的字符设备范畴,所有这些设备采用主编号10,一起归于misc device,其实misc_register就是用主标号10调用register_chrdev()的。也就是说,misc设备其实也就是特殊的字符设备,可自动生成设备节点。

卸载rmmod驱动程序:

static void __exit dev_exit()

{

iounmap(base_addr);

/* disable ths adc clock */

if(adc_clock)

{

clk_disable(adc_clock);

clk_put(adc_clock);

adc_clock = NULL;

}

misc_deregister(&misc);

}

许可证声明、作者信息、调用加载和卸载程序:

MODULE_LICENSE("GPL");

MODULE_AUTHOR("zhuzhaoqi jxlgzzq@163.com");

module_init(dev_init);

module_exit(dev_exit);

在/linux-3.8.3/drivers/char目录下的Makefile中添加:

obj-m      += zzqadc.o

回到/linux-3.8.3根目录下:

/home/zhuzhaoqi/Linux/linux-3.8.3# make modules

将/linux-3.8.3/drivers/char目录下生成的zzqadc.ko拷贝到文件系统的/lib/module/3.8.3目录中。

3.ADC应用程序

ADC应用程序也是相对简单,打开设备驱动文件之后进行数据读取即可。

#include <stdio.h>

#include <fcntl.h>

#include <unistd.h>

int main()

{

int fp,adc_data,i;

fp = open("/dev/zzqadc",O_RDWR);

if (fp < 0)

{

printf("open failed! \n");

}

printf("opened ... \n");

for ( ; ; i++)

{

adc_data = read(fp,NULL,0);

printf("Begin the NO. %d test... \n",i);

printf("adc_data = %d \n",adc_data);

printf("The Value = %f V \n" , ( (float)adc_data )* 3.3 / 1024);

printf("End the NO. %d test ...... \n \n",i);

sleep(1);

}

close(fp);

return 0;

}

由于本次使用的A/D转换是10位,则数据转换值即为1024,而OK6410的参考电压是3.3V,则A/D 采集数据和电压之间的转换公式为:(float)adc_data )* 3.3 / 1024。

为ADC应用程序编写Makefile:

CC = /usr/local/arm/4.4.1/bin/arm-linux-gcc

zzqadcapp:zzqadcapp.o

$(CC) -o zzqadcapp zzqadcapp.o

zzqadcapp.o:zzqadcapp.c

$(CC) -c zzqadcapp.c

clean :

rm zzqadcapp.o zzqadcapp

将生成的zzqadcapp应用文件拷贝到文件系统/usr/bin文件夹下。

加载zzqadc.ko设备:

[YJR@zhuzhaoqi 3.8.3]# insmod zzqadc.ko

dev_init return ret: 0

[YJR@zhuzhaoqi]\# ls -l /dev/zzqadc

crw-rw---- 1 root root 10, 60 Jan 1 08:00 /dev/zzqadc

在/dev目录下存在zzqadc设备节点,则说明ADC驱动加载成功。

执行ADC应用程序,电压采样如下所示:

[YJR@zhuzhaoqi]\# ./zzqadcapp

opened ...

……

Begin the NO. 10 test...

adc_data = 962

The Value = 3.100195 V

End the NO. 10 test ......

……

本节配套视频位于光盘中“嵌入式Linux开发实用教程视频”目录下第四章02课(字符设备驱动之ADC)。

4.3 块设备驱动

块设备和字符设备从字面上理解最主要的区别在于读写的基本单元不同,块设备的读写基本单元为数据块,数据的输入输出都是通过一个缓冲区来完成的。而字符设备不带有缓冲,直接与实际的设备相连而进行操作,读写的基本单元为字符。从实现的角度来看,块设备和字符设备是两种不同的机制,字符设备的read、write的API直接到字符设备层,但是块设备相对复杂,是先到文件系统层,然后再由文件系统层发起读写请求。

数据块指的是固定大小的数据,这个值的大小由内核来决定。一般而言,数据块的大小通常是 4096 Bytes,但是大小并不是恒定不变的,而是可以根据体系结构和所使用的文件系统进行改变。与数据块相对应的是扇区,它是由底层硬件决定大小的一个块。内核所处理的设备扇区大小是 512 Bytes,无论何时内核为用户提供一个扇区编号,该扇区的大小都是 512 Bytes。但是如果要使用不同的硬件扇区大小,用户必须对内核的扇区数做相应的修改。

4.3.1 块设备操作

1.file_operations结构体

和字符设备驱动中的 file_operations 结构体类似,块设备驱动中也有一个 block_device_operations结构体,它的声明位于/include/linux目录下的fs.h文件中,它是对块操作的集合。

struct block_device_operations{

int(*open)(struct inode *, struct file*); //打开设备

int(*release)(struct inode *, struct file*); //关闭设备

//实现ioctl系统调用

int(*ioctl)(struct inode *, struct file *, unsigned, unsigned long);

long(*unlocked_ioctl)(struct file *, unsigned, unsigned long);

long(*compat_ioctl)(struct file *, unsigned, unsigned long);

int(*direct_access)(struct block_device *, sector_t, unsigned long*);

//调用该函数用以检查用户是否更换了驱动器的介质

int(*media_changed)(struct gendisk*);

int(*revalidate_disk)(struct gendisk*); //当介质被更换时,调用该函数做出响应

int(*getgeo)(struct block_device *, struct hd_geometry*);//获取驱动器信息

struct module *owner; //指向拥有这个结构体模块的指针,通常被初始化为THIS_MODULE

};

与字符驱动不同的是在这个结构体中缺少了read()和write()函数,那是因为在块设备的I/O子系统中,这些操作都是由request函数进行处理的。

request函数的原型如下:

void request(request_queue_t *queue);

当内核需要驱动程序处理读取、写入以及其他对设备的操作时,便会调用request函数。

2.gendisk结构体

gendisk结构体的定义位于/include/linux目录下的genhd.h文件中,如下所示。

struct gendisk {

/*

*这3个成员的定义依次是:主设备号、第一个次设备号,次设备号。一个驱动中至少有一个次设备号,

*如果驱动器是一个可被分区,那么每一个分区都将分配一个次设号。

*/

int major;

int first_minor;

int minors;

//这个数组用以存储驱动设备的名字

char disk_name[DISK_NAME_LEN];

char *(*devnode)(struct gendisk *gd, umode_t *mode);

unsigned int events;

unsigned int async_events;

struct disk_part_tbl __rcu *part_tbl;

struct hd_struct part0;

//这个结构体用以设置驱动中的各种设备操作

const struct block_device_operations *fops;

//Linux内核使用这个结构体为设备管理I/O请求,具体详解见request_queue结构

struct request_queue *queue;

void *private_data;

int flags;

struct device *driverfs_dev;

struct kobject *slave_dir;

struct timer_rand_state *random;

atomic_t sync_io;

struct disk_events *ev;

#ifdef CONFIG_BLK_DEV_INTEGRITY

struct blk_integrity *integrity;

#endif

int node_id;

};

gendisk结构体是动态分配,但是驱动程序自己不能动态分配该结构,而是通过调用alloc_disk()函数进行动态分配。

struct gendisk *alloc_disk(int minors);

其中minors是该磁盘使用的次设备号。

但是分配了 gendisk 结构并不意味着该磁盘就对系统可用,使用之前的初始化结构体并且调用add_disk()函数。

//初始化结构体

struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

//添加分区

void add_disk(struct gendisk *gd)

如果不再需要这个磁盘,则对其进行卸载。

//删除分区

Void del_gendisk(struct gendisk *gd)

void blk_cleanup_queue(struct request_queue *q)

3.bio结构体

bio结构体的定义位于/include/linux目录下的linux_blk_types.h文件中。

struct bio {

//需要传输的第一个(512 bytes)扇区

sector_t bi_sector;

struct bio  *bi_next;

struct block_device *bi_bdev;

unsigned long bi_flags;

unsigned long bi_rw;

unsigned short  bi_vcnt;

unsigned short  bi_idx;

//BIO中所包含的物理段数目

unsigned int  bi_phys_segments;

//所传输的数据大小(以byte为单位)

unsigned int  bi_size;

unsigned int  bi_seg_front_size;

unsigned int  bi_seg_back_size;

unsigned int  bi_max_vecs;

atomic_t bi_cnt;

struct bio_vec   *bi_io_vec;

bio_end_io_t  *bi_end_io;

void   *bi_private;

#ifdef CONFIG_BLK_CGROUP

struct io_context *bi_ioc;

struct cgroup_subsys_state *bi_css;

#endif

#if defined(CONFIG_BLK_DEV_INTEGRITY)

struct bio_integrity_payload *bi_integrity;

#endif

bio_destructor_t *bi_destructor;

struct bio_vec  bi_inline_vecs[0];

};

bio 结构体包含了驱动程序执行请求的所有信息,既描述了磁盘的位置,又描述了内存的位置,是上层内核与下层驱动的连接纽带。

bio结构体的核心在于:

struct bio_vec   *bi_io_vec;

而bio_vec结构体的声明为:

struct bio_vec {

struct page  *bv_page; /*数据段所在的页*/

unsigned short bv_len; /*数据段的长度*/

unsigned short bv_offset; /*数据段页内偏移*/

};

结构bio_vec代表了内存中的一个数据段,数据段用页、偏移和长度来描述。bio_vec结构体和bio结构体之间的关系如图4.4所示。

从图4.4可知,当I/O请求被转换到bio结构体之后,它将被单独的物理内存页所销毁。

图4.4 bio_vec结构体和bio结构体之间的关系

4.requeset结构体

request结构体代表了挂起的I/O请求,每个请求用一个结构request实例描述,存放在请求队列链表中,由电梯算法进行排序,每个请求包含一个或多个结构bio实例。requeest结构体声明位于/include/linux目录下的blkdev.h文件中。

struct request {

struct list_head queuelist;

struct call_single_data csd;

struct request_queue *q;

unsigned int cmd_flags;

enum rq_cmd_type_bits cmd_type;

unsigned long atomic_flags;

int cpu;

unsigned int __data_len;

sector_t __sector;

struct bio *bio;

struct bio *biotail;

struct hlist_node hash;

union {

struct rb_node rb_node;

void *completion_data;

};

union {

struct {

struct io_cq  *icq;

void   *priv[2];

} elv;

struct {

unsigned int  seq;

struct list_head list;

rq_end_io_fn  *saved_end_io;

} flush;

};

struct gendisk *rq_disk;

struct hd_struct *part;

unsigned long start_time;

#ifdef CONFIG_BLK_CGROUP

struct request_list *rl;

unsigned long long start_time_ns;

unsigned long long io_start_time_ns;

#endif

unsigned short nr_phys_segments;

#if defined(CONFIG_BLK_DEV_INTEGRITY)

unsigned short nr_integrity_segments;

#endif

unsigned short ioprio;

int ref_count;

void *special;

char *buffer;

int tag;

int errors;

unsigned char __cmd[BLK_MAX_CDB];

unsigned char *cmd;

unsigned short cmd_len;

unsigned int extra_len;

unsigned int sense_len;

unsigned int resid_len;

void *sense;

unsigned long deadline;

struct list_head timeout_list;

unsigned int timeout;

int retries;

rq_end_io_fn *end_io;

void *end_io_data;

struct request *next_rq;

};

5.request_queue结构体

每个块设备都有一个请求队列,每个请求队列单独执行I/O调度。请求队列是由请求结构实例链接成的双向链表,链表以及整个队列的信息用request_queue结构体描述,称为请求队列对象结构或请求队列结构。request_queue结构体声明位于/include/linux目录下的blkdev.h文件中。

struct request_queue {

struct list_head queue_head;

struct request  *last_merge;

struct elevator_queue *elevator;

int   nr_rqs[2];

int   nr_rqs_elvpriv;

struct request_list root_rl;

request_fn_proc  *request_fn;

make_request_fn  *make_request_fn;

prep_rq_fn  *prep_rq_fn;

unprep_rq_fn  *unprep_rq_fn;

merge_bvec_fn  *merge_bvec_fn;

softirq_done_fn  *softirq_done_fn;

rq_timed_out_fn  *rq_timed_out_fn;

dma_drain_needed_fn *dma_drain_needed;

lld_busy_fn  *lld_busy_fn;

sector_t end_sector;

struct request  *boundary_rq;

struct delayed_work delay_work;

struct backing_dev_info backing_dev_info;

void   *queuedata;

unsigned long queue_flags;

int   id;

gfp_t   bounce_gfp;

spinlock_t  __queue_lock;

spinlock_t  *queue_lock;

struct kobject kobj;

unsigned long nr_requests; /* Max # of requests */

unsigned int  nr_congestion_on;

unsigned int  nr_congestion_off;

unsigned int  nr_batching;

unsigned int  dma_drain_size;

void   *dma_drain_buffer;

unsigned int  dma_pad_mask;

unsigned int  dma_alignment;

struct blk_queue_tag *queue_tags;

struct list_head tag_busy_list;

unsigned int  nr_sorted;

unsigned int  in_flight[2];

unsigned int  rq_timeout;

struct timer_list timeout;

struct list_head timeout_list;

struct list_head icq_list;

#ifdef CONFIG_BLK_CGROUP

DECLARE_BITMAP  (blkcg_pols, BLKCG_MAX_POLS);

struct blkcg_gq  *root_blkg;

struct list_head blkg_list;

#endif

struct queue_limits limits;

unsigned int  sg_timeout;

unsigned int  sg_reserved_size;

int   node;

#ifdef CONFIG_BLK_DEV_IO_TRACE

struct blk_trace *blk_trace;

#endif

unsigned int  flush_flags;

unsigned int  flush_not_queueable:1;

unsigned int  flush_queue_delayed:1;

unsigned int  flush_pending_idx:1;

unsigned int  flush_running_idx:1;

unsigned long flush_pending_since;

struct list_head flush_queue[2];

struct list_head flush_data_in_flight;

struct request  flush_rq;

struct mutex  sysfs_lock;

int   bypass_depth;

#if defined(CONFIG_BLK_DEV_BSG)

bsg_job_fn  *bsg_job_fn;

int   bsg_job_size;

struct bsg_class_device bsg_dev;

#endif

#ifdef CONFIG_BLK_CGROUP

struct list_head all_q_node;

#endif

#ifdef CONFIG_BLK_DEV_THROTTLING

struct throtl_data *td;

#endif

};

4.3.2 块设备驱动程序

块设备是一种抽象的设备驱动,它看不到、摸不着。由于这本书是针对嵌入式Linux初学者,因此笔者使用最简单、最通俗易懂的一个块设备驱动程序向各位读者展现如何开辟、挂载、使用一个分区。或许这个程序有很多不合理之处,但却是初学者容易接受的。

/*

* 头文件

*/

#include <linux/init.h>

#include <linux/module.h>

#include <linux/blkdev.h>

/* 主设备号,COMPAQ_SMART2_MAJOR = 72 */

#define ZZQ_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR

/* 设备名称 */

#define ZZQ_BLKDEV_DISKNAME  "zzqdisk"

#define ZZQ_BLKDEV_SIZE  (1 * 1024 * 1024)

/* 次设备号 */

#define ZZQ_MIJORS    1

/* 使用数组储块设备数据 */

/*

* 这个数组应该是最忌讳的,1MB的全局变量

*/

unsigned char zzq_blkdev_data[ZZQ_BLKDEV_SIZE];

/* 定义一个指向请求队列的结构体指针 */

static struct request_queue *zzq_blkdev_queue;

/* 定义一个指向独立分区(磁盘)的结构体指针 */

static struct gendisk  *zzq_blkdev_disk;

/*

* 请求队列的操作

*/

static void zzq_blkdev_do_request(struct request_queue *q)

{

struct request *req;

/*

* blk_rq_pos()   : the current sector 当前扇区

* blk_rq_bytes()  : bytes left in the entire request

* blk_rq_cur_bytes() : bytes left in the current segment

* blk_rq_err_bytes() : bytes left till the next error boundary

* blk_rq_sectors() : sectors left in the entire request

* blk_rq_cur_sectors() : sectors left in the current segment

*/

/*

* 从请求队列中取出一个请求(可能是请求中的一段),

* 如果不是为空的话

*/

while((req = blk_fetch_request(q)) != NULL)

{

if(((blk_rq_pos(req) + blk_rq_sectors(req)) << 9) > ZZQ_BLKDEV_SIZE)

{

printk(KERN_ERR ZZQ_BLKDEV_DISKNAME "bad request :block = %llu,count = %u\n", (u64)blk_rq_pos(req), blk_rq_sectors(req));

/*

* 结束一个队列请求,第二个参数表示请求处理结果

* 成功的话设定为1,失败的话设定为0 或者错误号

*/

__blk_end_request_all(req, -EIO);

continue;

}

/*

* rq_data_dir()函数返回该请求的方向:读还是写

*/

switch( rq_data_dir(req))

{

/* 如果是读 */

case READ:

printk(KERN_ALERT "read\n");

/* 把块设备的数据装入队列缓冲区 */

memcpy(req->buffer, zzq_blkdev_data + (blk_rq_pos(req) << 9), blk_rq_sectors(req) << 9);

/* 请求结束 */

__blk_end_request_all(req, 1);

break;

/* 如果是写 */

case WRITE:

printk(KERN_ALERT "write\n");

/* 把缓冲区的数据写入块设备 */

memcpy(zzq_blkdev_data + (blk_rq_pos(req) << 9), req->buffer, blk_rq_sectors(req) << 9);

/* 请求结束 */

__blk_end_request_all(req, 1);

break;

default:

printk(KERN_ALERT "this should not happen");

break;

}

}

}

/*

* 块设备操作的集合

*/

struct block_device_operations zzq_blkdev_fops = {

.owner = THIS_MODULE,

};

/*

* 块设备的初始化

*/

static int __init zzq_blkdev_init(void )

{

int ret;

printk(KERN_ALERT ZZQ_BLKDEV_DISKNAME "init! \n");

/* 初始化请求队列 */

zzq_blkdev_queue = blk_init_queue(zzq_blkdev_do_request, NULL);

if(!zzq_blkdev_queue)

{

ret = -ENOMEM;

goto err_init_queue;

}

/* 为独立分区开辟一个空间 */

zzq_blkdev_disk = alloc_disk(ZZQ_MIJORS);

if(!zzq_blkdev_disk)

{

ret = -ENOMEM;

goto err_alloc_disk;

}

/*

* 以下是初始化分区结构体成员

*/

/* 设备名称 */

strcpy(zzq_blkdev_disk->disk_name, ZZQ_BLKDEV_DISKNAME);

/* 主设备号 */

zzq_blkdev_disk->major = ZZQ_BLKDEV_DEVICEMAJOR;

/* 调用块设备操作集合 */

zzq_blkdev_disk->fops = &zzq_blkdev_fops;

/* 初始化设备的请求队列 */

zzq_blkdev_disk->queue = zzq_blkdev_queue;

/*

* 给分区分配空间

*

* 由于块设备的大小使用扇区作为基本单元,

* 扇区的默认大小是512byte,也就是向右移动9 位

*/

set_capacity(zzq_blkdev_disk, ZZQ_BLKDEV_SIZE >> 9);

/* 注册分区 */

add_disk(zzq_blkdev_disk);

return 0;

err_alloc_disk:

blk_cleanup_queue(zzq_blkdev_queue);

err_init_queue:

return ret;

}

/*

* 块设备卸载

*/

static void zzq_blkdev_exit(void)

{

printk(KERN_ALERT"exit zzqblkdev! \n");

/* 释放删除分区 add_disk() */

del_gendisk(zzq_blkdev_disk);

/* blk_init_queue() */

blk_cleanup_queue(zzq_blkdev_queue);

}

module_init(zzq_blkdev_init);

module_exit(zzq_blkdev_exit);

MODULE_LICENSE("GPL");

这个程序实现的是开辟一个1MB的独立分区(磁盘),我们可以对这个分区进行读写和挂载等操作。

这里面有一个数组,使用了 1MB 的全局变量空间,这是很忌讳的。但是为了程序的通俗易懂性,笔者还是这样做了。要写好一个漂亮的块设备驱动程序,程序员必须要有深厚的C语言功底和数据结构知识。如果要去掉这个 1MB 的全局变量空间,这里内存的申请可以使用基树,或者也可以使用红黑树、哈希表等。

在这里基树是首选,笔者也希望各位读者能使用基树优化这个驱动程序。内核提供了一个基树库,代码在/lib/目录下的 radix-tree.c 文件中。基树是一种空间换时间的数据结构,通过空间的冗余减少时间上的消耗。我们使用如图4.5所示来描述基树算法。

如图4.5所示,元素空间总共为256,但元素个数不固定。那么如果用数组存储,好处是插入查找只用一次操作,但是存储空间需要 256,空间换取时间,但是在嵌入式中,内存是宝贵的。如果用链表存储,存储空间节省了,但是极限情况下查找操作次数等于元素的个数,时间换取空间,但是时间同样是宝贵的。能不能有一种算法,可以兼顾时间和空间呢,有,基树。采用一棵高度为2的基树,第一级最多16个冗余结构,代表元素前四位的索引。第二级代表元素后四位的索引。那么只要两级查找就可以找到特定的元素,而且只有少量的冗余数据。图中假设只有一个元素10001000,那么只有树的第一级有元素,而且树的第二级只有1000个节点有子节点,其他节点都不必分配空间。这样既可以快速定位查找,也减少了冗余数据。基树很适合存储稀疏的数据,内核中文件的页cache就是采用的基树。

图4.5 基树查找算法

将这个块设备驱动程序zzqblkdev.c放入/drivers/block目录下,修改Makefile,将其编译成独立模块。

obj-m     += zzqblkdev.o

编译:

zhuzhaoqi@zhuzhaoqi-desktop:~/Linux/linux-3.8.3$ make modules

……

CC drivers/block/zzqblkdev.mod.o

LD [M] drivers/block/zzqblkdev.ko

……

将生成的zzqblkdev.ko模块放入OK6410的根文件系统中,挂载:

[YJR@zhuzhaoqi]\# insmod zzqblkdev.ko

[YJR@zhuzhaoqi]\# lsmod

zzqblkdev 1049808 0 - Live 0xbf000000

可知挂载zzqblkdev模块成功,并且分配给这个分区的大小即为1MB的空间。再看看/dev下面的设备节点。

[YJR@zhuzhaoqi]\# ls -l /dev/zzqdisk

brw-rw---- 1 root root 72, 0 Jan 1 08:11 /dev/zzqdisk

主设备号为72,次设备号为1。

本节配套视频位于光盘中“嵌入式Linux开发实用教程视频”目录下第四章03课(块驱动)。

图书在版编目(CIP)数据

嵌入式Linux开发实用教程/朱兆祺,李强,袁晋蓉编著.--北京:人民邮电出版社,2014.4

ISBN 978-7-115-33483-1

Ⅰ.①嵌… Ⅱ.①朱…②李…③袁… Ⅲ.①Linux操作系统—程序设计—教材 Ⅳ.①TP316.89

中国版本图书馆CIP数据核字(2013)第254297号

内容提要

嵌入式Linux是将日益流行的Linux操作系统进行裁剪修改,使之能在嵌入式计算机系统上运行的一种操作系统。既继承了Internet上无限的开放源代码资源,又具有嵌入式操作系统的特性,其优势及应用已获得众多企业的青睐。

本书以一个嵌入式 Linux 学习者的角度,由浅入深地总结了从入门到进行项目工程实践的所有学习历程,旨在帮助读者快速入门,以实例为导向扎实掌握嵌入式开放技术。全书共分 6 章,主要内容包括嵌入式Linux基础、U-Boot移植、Linux移植、Linux驱动程序、Qt移植和程序设计以及举一反三的综合拓展学习。由于嵌入式Linux是一门非常复杂的软件技术,入门较难,因此借以此书为自学者提供一条成功入门的捷径。本书光盘包含了作者在本书基础上录制的40集学习视频,涵盖嵌入式Linux基础、U-Boot移植、Linux移植、Linux驱动程序设计、Qt移植等。本书的所有程序以及源码都在光盘中,读者可自行参考。

本书内容详实,结构明确,适合作为初学者的课程教材,也可作为嵌入式系统爱好者的自学参考资料。

◆编著 朱兆祺 李强 袁晋蓉

责任编辑 俞彬

责任印制 程彦红 焦志炜

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京艺辉印刷有限公司印刷

◆开本:787×1092 1/16

印张:16.75

字数:415千字  2014年4月第1版

印数:1–3500册  2014年4月北京第1次印刷

定价:45.00元(附光盘)

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

相关图书

嵌入式Linux与物联网软件开发:ARM处理器开发自学教程
嵌入式Linux与物联网软件开发:ARM处理器开发自学教程
嵌入式Linux软硬件开发详解——基于S5PV210处理器
嵌入式Linux软硬件开发详解——基于S5PV210处理器
嵌入式 Linux C 语言应用程序设计(修订版)
嵌入式 Linux C 语言应用程序设计(修订版)
嵌入式 Linux应用程序开发标准教程(第2版)
嵌入式 Linux应用程序开发标准教程(第2版)
嵌入式Linux应用开发完全手册
嵌入式Linux应用开发完全手册

相关文章

相关课程