您的当前位置:首页正文

移植嵌入式Linux到ARM2410处理器

2020-08-01 来源:V品旅游网
移植嵌入式Linux到ARM处理器:基本概念

引言

ARM是Advanced RISC Machines(高级精简指令系统处理器)的缩写,是ARM公司提供的一种微处理器知识产权(IP)核。

ARM的应用已遍及工业控制、消费类电子产品、通信系统、网络系统、无线系统等各类产品市场。基于ARM 技术的微处理器应用约占据了32位RISC 微处理器75%以上的市场份额。揭开你的手机、MP3、 PDA,嘿嘿,里面多半藏着一个基于ARM的微处理器!

ARM内核的数个系列(ARM7、ARM9、ARM9E、ARM10E、SecurCore、Xscale、StrongARM),各自满足不同应用领域的需求,无孔不入的渗入嵌入式系统各个角落的应用。这是一个ARM的时代!

下面的图片显示了ARM的随处可见:

有人的地方就有江湖,有嵌入式系统的地方就有ARM。

构建一个复杂的嵌入式系统,仅有硬件是不够的,我们还需要进行操作系统的移植。我们通常在ARM平台上构建Windows CE、Linux、Palm OS等操作系统,其中Linux具有开放源代码的优点。

下图显示了基于ARM嵌入式系统中软件与硬件的关系:

日前,笔者作为某嵌入式ARM(硬件)/Linux(软件)系统的项目负责人,带领项目组成员进行了下述工作:

(1)基于ARM920T内核S3C2410A CPU的电路板设计;

(2)ARM处理下底层软件平台搭建:

a.Bootloader的移植;

b.嵌入式Linux操作系统内核的移植;

c.嵌入式Linux操作系统根文件系统的创建; d.电路板上外设Linux驱动程序的编写。

本文将真实地再现本项目开发过程中作者的心得,以便与广大读者共勉。第一章将简单地介绍本ARM开发板的硬件设计,第二章分析Bootloader的移 植方法,第三章叙述嵌入式mizi Linux的移植及文件系统的构建方法,第四章讲解外设的驱动程序设计,第五章给出一个已构建好的软硬件平台上应用开发的实例。

如果您有嵌入式系统的开发基础,您将非常容易领会本文讲解地内容。即便是您从来没有嵌入式系统的开发经历,本文读起来也不会生涩。您可以通过如下email与作者联系:21cnbao@21cn.com。

2.ARM体系结构

作为一种RISC体系结构的微处理器,ARM微处理器具有RISC体系结构的典型特征。还具有如下增强特点:

(l)在每条数据处理指令当中,都控制算术逻辑单元(ALU)和移位器,以使ALU和移位器获得最大的利用率;

(2)自动递增和自动递减的寻址模式,以优化程序中的循环;

(3)同时Load和Store多条指令,以增加数据吞吐量;

(4)所有指令都条件执行,以增大执行吞吐量。

ARM体系结构的字长为32位,它们都支持Byte(8位)、Halfword(16位)和Word(32位)3种数据类型。

ARM处理器支持7种处理器模式,如下表:

大部分应用程序都在User模式下运行。当处理器处于User模式下时,执行的程序无法访问一些被保护的系统资源,也不能改变模式,否则就会导致一次异常。对系统资源的使用由操作系统来控制。

User模式之外的其它几种模式也称为特权模式,它们可以完全访问系统资源,可以自由地改变模式。其中的FIQ、IRQ、supervisor、 Abort和undefined 5种模式也被称为异常模式。在处理特定的异常时,系统进入这几种模式。这5种异常模式都有各自的额外的寄存器,用于避免在发生异常的时候与用户模式下的程 序发生冲突。

还有一种模式是system模式,任何异常都不会导致进入这一模式,而且它使用的寄存器和User模式下基本相同。它是一种特权模式,用于有访问系统资源请求而又需要避免使用额外的寄存器的操作系统任务。

程序员可见的ARM寄存器共有37个:31个通用寄存器以及6个针对ARM处理器的不同工作模式所设立的专用状态寄存器,如下图:

ARM9采用5级流水线操作:指令预取、译码、执行、数据缓冲、写回。ARM9设置了16个字的数据缓冲和4个字的地址缓冲。这5级流水已被很多的RISC处理器所采用,被看作RISC结构的\"经典\"。

3.硬件设计

3.1 S3C2410A微控制器

电路板上的ARM微控制器 S3C2410A采用了ARM920T核,它由ARM9TDMI、存储管理单元MMU和高速缓存三部分组成。其中,MMU可以管理虚拟内存,高速缓存由独 立的16KB地址和16KB数据高速Cache组成。ARM920T有两个内部协处理器:CP14和CP15。CP14用于调试控制,CP15用于存储系 统控制以及测试控制。

S3C2410A集成了大量的内部电路和外围接口:

·LCD控制器(支持STN和TFT带有触摸屏的液晶显示屏)

·SDRAM控制器

·3个通道的UART

·4个通道的DMA

·4个具有PWM功能的计时器和一个内部时钟

·8通道的10位ADC

·触摸屏接口

·I2C总线接口

·12S总线接口

·两个USB主机接口

·一个USB设备接口

·两个SPI接口

·SD接口

·MMC卡接口

S3C2410A集成了一个具有日历功能的RTC和具有PLL(MPLL和UPLL)的芯片时钟发生器。MPLL产生主时钟,能够使处理器工作频率最高 达到203MHz。这个工作频率能够使处理器轻松运行WinCE、Linux等操作系统以及进行较为复杂的信息处理。UPLL则产生实现USB模块的时 钟。

下图显示了S3C2410A的集成资源和外围接口:

我们需要对上图中的AHB总线和APB总线的概念进行一番解释。ARM核开发的目的,是使其作为复杂片上系统的一个处理单元来应用的,所以还必须提供一 个ARM与其它片上宏单元通信的接口。为了减少不必要的设计资源的浪费,ARM公司定义了AMBA(Advanced Microcontroller Bus Architecture)总线规范,它是一组针对基于ARM核的、片上系统之间通信而设计的、标准的、开放协议。

在AMBA总线规范中,定义了3种总线:

(l)AHB-Advanced High Performace Bus,用于高性能系统模块的连接,支持突发模式数据传输和事务分割;

(2)ASB-Advanced System Bus,也用于高性能系统模块的连接,支持突发模式数据传输,这是较老的系统总线格式,后来由AHB总线替代;

(3)APB-Advanced PeriPheral Bus,用于较低性能外设的简单连接,一般是接在AHB或ASB系统总线上的第二级总线。

典型的AMBA总线系统如下图:

S3C2410A将系统的存储空间分成8个bank,每个bank的大小是128M字节,共1G字节。Bank0到bank5的开始地址是固定的,用于 ROM或SRAM。bank6和bank7可用于ROM、SRAM或SDRAM。所有内存块的访问周期都可编程,外部Wait也能扩展访问周期。下图给出 了S3C2410A的内存组织:

下图给出了S3C2410A的数据总线、地址总线和片选电路:

SDRAM控制信号、集成USB接口电路:

内核与存储单元供电电路(S3C2410A对于片内的各个部件采用了独立的电源供给,内核采用1.8V供电,存储单元采用3.3V独立供电):

JTAG标准通过边界扫描技术提供了对电路板上每一元件的功能、互联及相互间影响进行测试的方法,极大地方便了系统电路的调试。

测试接入端口TAP的管脚定义如下:

·TCK:专用的逻辑测试时钟,时钟上升沿按串行方式对测试指令、数据及控制信号进行移位操作,下降沿用于对输出信号移位操作;

·TMS:测试模式选择,在TCK上升沿有效的逻辑测试控制信号;

·TDI:测试数据输入,用于接收测试数据与测试指令;

·TDO:测试数据输出,用于测试数据的输出。

S3C2410A调试用JTAG接口电路:

3.2 SDRAM存储器

SDRAM被用来存放操作系统(从FLASH解压缩拷入)以及存放各类动态数据,采用SAMSUNG公司 的K4S561632,它是4Mxl6bitx4bank的同步DRAM,容量为32MB。用2片K4S561632实现位扩展,使数据总线宽度达到 32bit,总容量达到64MB,将其地址空间映射在S3C2410A的bank6。

SDRAM 所有的输入和输出都与系统时钟CL K上升沿同步,由输入信号RA S、CA S、WE组合产生SDRAM 控制命令,其基本的控制命令如下:

SDRAM 在具体操作之前首先必须通过MRS命令设置模式寄存器,以便确定

SDRAM 的列地址延迟、突发类型、突发长度等工作模式;再通过ACT命令激活对应地址的组,同时输入行地址;然后通过RD 或WR 命令输入列地址,将相应数据读出或写入对应的地址;操作完成后用PCH 命令或BT 命令中止读或写操作。在没有操作的时候,每隔一段时间必须用ARF命令刷新数据,防止数据丢失。

下图给出了SDRAM的连接电路:

3.3 FLASH存储器

NOR和NAND是现在市场上两种主要的非易失闪存技术。

NOR的特点是芯片内执行(XIP,Execute In Place),即应用程序可直接在Flash闪存内运行,不必把代码读到系统RAM中。NOR的传输效率很高,在1~4MB的小容量时具有很高的成本效益,但是很低的写入和擦除速度大大影响了它的性能。

NAND结构能提供极高的单元密度,可以达到高存储密度,并且写入和擦除的速度也很快。应用NAND的困难在于Flash的管理和需要特殊的系统接口,S3C2410A内嵌了NAND FLASH控制器。

S3C2410A支持从GCS0上的NOR FLASH启动(16位或32位)或从NAND FLASH启动,需要通过OM0和OM1上电时的上下拉来设置:

在系统中分别采用了一片NOR FLASH(28F640)和NAND FLASH(K9S1208),电路如下图:

3.4串口

S3C2410内部集成了UART控制器,实现了并串转换。外部还需提供CMOS/TTL电平与RS232之间的转换:

3.5以太网

以太网控制芯片采用CIRRUS LOGIC公司生产的CS8900A,其突出特点是使用灵活,其物理层接口、数据传输模式和工作模式等都能根据需要而动态调整,通过内部寄存器的设置来适 应不同的应用环境。它符合IEEE803.3以太网标准,带有传送、接收低通滤波的10Base-T连接端口,支持10Base2,10Base5和 10Base-F的AUI接口,并能自动生成报头,自动进行CRC检验,在冲突后自动重发。

CS8900A支持的传输模式有I/O和 Memory模式。当CS8900A有硬件复位或软件复位时,它将默认成为8位工作模式。因此,要使CS8900A工作于16位模式,系统必须在访问之前 提供给总线高位使能管脚(/SBHE)一个由高到低、再由低到高变化的电平。

3.6 USB接口

USB 系统由USB 主机(USB Host)、USB集线器(USB Hub)和USB设备(USB Device)组成。USB 和主机系统的接口称作主机控制器(Host Controller),它是由硬件和软件结合实现的。根集线器是综合于主机系统内部的,用以提供USB的连接点。USB的设备包括集线器(Hub)和功 能器件(Function)。

S3C2410A集成了USB host和USB device,外部连接电路如下图:

3.7电源

LDO(Low Dropout)属于DC/DC变换器中的降压变换器,它具有低成本、低噪声、低功耗等突出优点,另外它所需要的外围器件也很少,通常只有 1~2 个旁路电容。

在电路板上我们分别用两个LDO来实现5V向3.3V(存储接口电平)和1.8V(ARM内核电平)的转换。

up监控电路采用MAX708芯片,提供上电、掉电以及降压情况下的复位输出及低电平有效的人工复位输出:

3.8其它

SN74LVTH62245A提供总线驱动和缓冲能力:

S3C2410A集成LCD液晶显示器控制电路,外部引出接口:

触摸屏有电阻式、电容式等,其本质是一种将手指在屏幕上的触点位置转化为电信号的传感器。手指触到屏幕,引起触点位置电阻或电容的变化,再通过检测这一 电性变化,从而获得手指的坐标位置。通过S3C2410A集成的AD功能,完成电信号向屏幕坐标的转化,触摸屏接口如下:

键盘则直接利用CPU的可编程I/O口,若连接 mxn键盘,则需要m+n个可编程I/O口,由软件实现键盘扫描,识别按键:

3.9整体架构

下图呈现了ARM处理器及外围电路的整体设计框架:

4.小结

本章讲解了基于S3C2410A ARM处理器电路板硬件设计的基本组成,为后续各章提供了总体性的准备工作。

移植嵌入式Linux到ARM处理器:BootLoader

BootLoader指系统启动后,在操作系统内核运行之前运行的一段小程序。通过

BootLoader,我们可以初始化硬件设备、建立内存空间的映 射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。通常,BootLoader是严重地依赖于硬件而实现 的,特别是在嵌入式世界。因此,在嵌入式世界里建立一个通用的 BootLoader 几乎是不可能的。尽管如此,我们仍然可以对

BootLoader归纳出一些通用的概念来,以指导用户特定的BootLoader设计与实现。

BootLoader 的实现依赖于CPU的体系结构,因此大多数 BootLoader 都分为stage1 和stage2 两大部分。依赖于CPU体系结构的代码,比如设备初始化代码等,通常都放在 stage1中,而且通常都用汇编语言来实现,以达到短小精悍的目的。而stage2 则通常用C 语言来实现,这样可以实现更复杂的功能,而且代码会具有更好的可读性和可移植性。

BootLoader 的 stage1 通常包括以下步骤:

·硬件设备初始化;

·为加载Boot Loader的stage2准备 RAM 空间;

·拷贝Boot Loader的stage2 到RAM空间中;

·设置好堆栈;

·跳转到 stage2 的 C 入口点。

Boot Loader的stage2通常包括以下步骤:

·初始化本阶段要使用到的硬件设备;

·检测系统内存映射(memory map);

·将kernel 映像和根文件系统映像从flash上读到 RAM 空间中;

·为内核设置启动参数;

·调用内核。

本系统中的BootLoader参照韩国mizi公司的vivi进行修改。

1.开发环境

我们购买了武汉创维特信息技术有限公司开发的具有自主知识产权的应用于嵌入

式软件开发的集成软、硬件开发平台ADT(ARM Development Tools)它为基于ARM 核的嵌入式应用提供了一整套完备的开发方案,包括程序编辑、工程管理和设置、程序编译、程序调试等。

ADT嵌入式开发环境由ADT Emulator for ARM 和ADT IDE for ARM组成。ADT Emulator for ARM 通过JTAG 实现主机和目标机之间的调试支持功能。它无需目标存储器,不占用目标系统的任何端口资源。目标程序直接在目标板上运行,通过ARM 芯片的JTAG 边界扫描口进行调试,属于完全非插入式调试,其仿真效果接近真实系统。

ADT IDE for ARM 为用户提供高效明晰的图形化嵌入式应用软件开发环境,包括一整套完备的面向嵌入式系统的开发和调试工具:源码编辑器、工程管理器、工程编译器(编译器、汇 编器和连接器)、集成调试环境、ADT Emulator for ARM 调试接口等。其界面同Microsoft Visual Studio 环境相似,用户可以在ADT IDE for ARM 集成开发环境中创建工程、打开工程,建立、打开和编辑文件,编译、连接、设置、运行、调试嵌入式应用程序。

ADT嵌入式软件开发环境 采用主机-目标机交叉开发模型。ADT IDE for ARM 运行于主机端,而ADT Emulator for ARM 实现ADT IDE for ARM 与目标机之间的连接。开发时,首先由ADT IDE for ARM 编译连接生成目标代码,然后建立与ADT Emulator for ARM 之间的调试通道,调试通道建立成功后,就可以在ADT IDE for ARM 中通过ADT Emulator for ARM 控制目标板实现目标程序的调试,包括将目标代码下载到目标机中,控制程序运行,调试信息观察等等。

2.ARM汇编

ARM本身属于RISC指令系统,指令条数就很少,而其编程又以C等高级语言为主,我们仅需要在Bootloader的第一阶段用到少量汇编指令:

(1)+-运算

ADD r0, r1, r2 ―― r0 := r1 + r2 SUB r0, r1, r2 ―― r0 := r1 - r2 其中的第二个操作数可以是一个立即数: ADD r3, r3, #1 ―― r3 := r3 + 1 第二个操作数还可以是位移操作后的结果: ADD r3, r2, r1, LSL #3 ―― r3 := r2 + 8.r1 (2)位运算 AND r0, r1, r2 ―― r0 := r1 and r2 ORR r0, r1, r2 ―― r0 := r1 or r2 EOR r0, r1, r2 ―― r0 := r1 xor r2 BIC r0, r1, r2 ―― r0 := r1 and not r2 (3)寄存器搬移 MOV r0, r2 ―― r0 := r2 MVN r0, r2 ―― r0 := not r2 (4)比较 CMP r1, r2 ―― set cc on r1 - r2 CMN r1, r2 ―― set cc on r1 + r2 TST r1, r2 ―― set cc on r1 and r2 TEQ r1, r2 ―― set cc on r1 or r2 这些指令影响CPSR寄存器中的 (N, Z, C, V) 位 (5)内存操作 LDR r0, [r1] ―― r0 := mem [r1] STR r0, [r1] ―― mem [r1] := r0 LDR r0, [r1, #4] ―― r0 := mem [r1+4] LDR r0, [r1, #4] ! ―― r0 := mem [r1+4] r1 := r1 + 4 LDR r0, [r1], #4 ―― r0 := mem [r1] r1 := r1 +4 LDRB r0 , [r1] ―― r0 := mem8 [r1] LDMIA r1, {r0, r2, r5} ―― r0 := mem [r1] r2 := mem [r1+4] r5 := mem [r1+8] {..} 可以包括r0~r15中的所有寄存器,若包括r15 (PC)将导致程序的跳转。 (6)控制流 例1: MOV r0, #0 ; initialize counter LOOP: ADD r0, r0, #1 ; increment counter CMP r0, #10 ; compare with limit BNE LOOP ; repeat if not equal 例2: CMP r0, #5 ADDNE r1, r1, r0 SUBNE r1, r1, r2 ―― if (r0 != 5) { r1 := r1 + r0 - r2 } 3.BootLoader第一阶段 3.1硬件设备初始化 基本的硬件初始化工作包括: ·屏蔽所有的中断; ·设置CPU的速度和时钟频率; ·RAM初始化; ·初始化LED ARM的中断向量表设置在0地址开始的8个字空间中,如下表:

每当其中的某个异常发生后即将PC值置到相应的中断向量处,每个中断向量处放置一个跳转指令到相应的中断服务程序去进行处理,中断向量表的程序如下:

@ 0x00: Reset b Reset @ 0x04: Undefined instruction exception UndefEntryPoint: b HandleUndef @ 0x08: Software interrupt exception SWIEntryPoint: b HandleSWI @ 0x0c: Prefetch Abort (Instruction Fetch Memory Abort) PrefetchAbortEnteryPoint: b HandlePrefetchAbort @ 0x10: Data Access Memory Abort DataAbortEntryPoint: b HandleDataAbort @ 0x14: Not used NotUsedEntryPoint: b HandleNotUsed @ 0x18: IRQ(Interrupt Request) exception IRQEntryPoint: b HandleIRQ @ 0x1c: FIQ(Fast Interrupt Request) exception FIQEntryPoint: b HandleFIQ 复位时关闭看门狗定时器、屏蔽所有中断: Reset: @ disable watch dog timer mov r1, #0x53000000 mov r2, #0x0 str r2, [r1] @ disable all interrupts mov r1, #INT_CTL_BASE mov r2, #0xffffffff str r2, [r1, #oINTMSK] ldr r2, =0x7ff str r2, [r1, #oINTSUBMSK] 设置系统时钟: @init clk @ 1:2:4 mov r1, #CLK_CTL_BASE mov r2, #0x3 str r2, [r1, #oCLKDIVN] mrc p15, 0, r1, c1, c0, 0 @ read ctrl register orr r1, r1, #0xc0000000 @ Asynchronous mcr p15, 0, r1, c1, c0, 0 @ write ctrl register @ now, CPU clock is 200 Mhz mov r1, #CLK_CTL_BASE ldr r2, mpll_200mhz str r2, [r1, #oMPLLCON]

点亮所有的用户LED:

@ All LED on mov r1, #GPIO_CTL_BASE add r1, r1, #oGPIO_F ldr r2,=0x55aa str r2, [r1, #oGPIO_CON] mov r2, #0xff str r2, [r1, #oGPIO_UP] mov r2, #0x00 str r2, [r1, #oGPIO_DAT] 设置(初始化)内存映射: ENTRY(memsetup) @ initialise the static memory @ set memory control registers mov r1, #MEM_CTL_BASE adrl r2, mem_cfg_val add r3, r1, #52 1: ldr r4, [r2], #4 str r4, [r1], #4 cmp r1, r3 bne 1b mov pc, lr 设置(初始化)UART: @ set GPIO for UART mov r1, #GPIO_CTL_BASE add r1, r1, #oGPIO_H ldr r2, gpio_con_uart str r2, [r1, #oGPIO_CON] ldr r2, gpio_up_uart str r2, [r1, #oGPIO_UP] bl InitUART @ Initialize UART @ @ r0 = number of UART port InitUART: ldr r1, SerBase mov r2, #0x0 str r2, [r1, #oUFCON] str r2, [r1, #oUMCON] mov r2, #0x3 str r2, [r1, #oULCON] ldr r2, =0x245 str r2, [r1, #oUCON] #define UART_BRD ((50000000 / (UART_BAUD_RATE * 16)) - 1) mov r2, #UART_BRD str r2, [r1, #oUBRDIV] mov r3, #100 mov r2, #0x0 1: sub r3, r3, #0x1 tst r2, r3 bne 1b #if 0 mov r2, #'U' str r2, [r1, #oUTXHL] 1: ldr r3, [r1, #oUTRSTAT] and r3, r3, #UTRSTAT_TX_EMPTY tst r3, #UTRSTAT_TX_EMPTY bne 1b mov r2, #'0' str r2, [r1, #oUTXHL] 1: ldr r3, [r1, #oUTRSTAT] and r3, r3, #UTRSTAT_TX_EMPTY tst r3, #UTRSTAT_TX_EMPTY bne 1b #endif mov pc, lr 此外,vivi还提供了几个汇编情况下通过串口打印字符的函数PrintChar、PrintWord和PrintHexWord: @ PrintChar : prints the character in R0 @ r0 contains the character @ r1 contains base of serial port @ writes ro with XXX, modifies r0,r1,r2 @ TODO : write ro with XXX reg to error handling PrintChar: TXBusy: ldr r2, [r1, #oUTRSTAT] and r2, r2, #UTRSTAT_TX_EMPTY tst r2, #UTRSTAT_TX_EMPTY beq TXBusy str r0, [r1, #oUTXHL] mov pc, lr @ PrintWord : prints the 4 characters in R0 @ r0 contains the binary word @ r1 contains the base of the serial port @ writes ro with XXX, modifies r0,r1,r2 @ TODO : write ro with XXX reg to error handling PrintWord: mov r3, r0 mov r4, lr bl PrintChar mov r0, r3, LSR #8 /* shift word right 8 bits */ bl PrintChar mov r0, r3, LSR #16 /* shift word right 16 bits */ bl PrintChar mov r0, r3, LSR #24 /* shift word right 24 bits */ bl PrintChar mov r0, #'\\r' bl PrintChar mov r0, #'\\n' bl PrintChar mov pc, r4 @ PrintHexWord : prints the 4 bytes in R0 as 8 hex ascii characters @ followed by a newline @ r0 contains the binary word @ r1 contains the base of the serial port @ writes ro with XXX, modifies r0,r1,r2 @ TODO : write ro with XXX reg to error handling PrintHexWord: mov r4, lr mov r3, r0 mov r0, r3, LSR #28 bl PrintHexNibble mov r0, r3, LSR #24 bl PrintHexNibble mov r0, r3, LSR #20 bl PrintHexNibble mov r0, r3, LSR #16 bl PrintHexNibble mov r0, r3, LSR #12 bl PrintHexNibble mov r0, r3, LSR #8 bl PrintHexNibble mov r0, r3, LSR #4 bl PrintHexNibble mov r0, r3 bl PrintHexNibble mov r0, #'\\r' bl PrintChar mov r0, #'\\n' bl PrintChar mov pc, r4 3.2Bootloader拷贝 配置为从NAND FLASH启动,需要将NAND FLASH中的vivi代码copy到RAM中: #ifdef CONFIG_S3C2410_NAND_BOOT bl copy_myself @ jump to ram ldr r1, =on_the_ram add pc, r1, #0 nop nop 1: b 1b @ infinite loop #ifdef CONFIG_S3C2410_NAND_BOOT @ @ copy_myself: copy vivi to ram @ copy_myself: mov r10, lr @ reset NAND mov r1, #NAND_CTL_BASE ldr r2, =0xf830 @ initial value str r2, [r1, #oNFCONF] ldr r2, [r1, #oNFCONF] bic r2, r2, #0x800 @ enable chip str r2, [r1, #oNFCONF] mov r2, #0xff @ RESET command strb r2, [r1, #oNFCMD] mov r3, #0 @ wait 1: add r3, r3, #0x1 cmp r3, #0xa blt 1b 2: ldr r2, [r1, #oNFSTAT] @ wait ready tst r2, #0x1 beq 2b ldr r2, [r1, #oNFCONF] orr r2, r2, #0x800 @ disable chip str r2, [r1, #oNFCONF] @ get read to call C functions (for nand_read()) ldr sp, DW_STACK_START @ setup stack pointer mov fp, #0 @ no previous frame, so fp=0 @ copy vivi to RAM ldr r0, =VIVI_RAM_BASE mov r1, #0x0 mov r2, #0x20000 bl nand_read_ll tst r0, #0x0 beq ok_nand_read #ifdef CONFIG_DEBUG_LL bad_nand_read: ldr r0, STR_FAIL ldr r1, SerBase bl PrintWord 1: b 1b @ infinite loop #endif ok_nand_read: #ifdef CONFIG_DEBUG_LL ldr r0, STR_OK ldr r1, SerBase bl PrintWord #endif @ verify mov r0, #0 ldr r1, =0x33f00000 mov r2, #0x400 @ 4 bytes * 1024 = 4K-bytes go_next: ldr r3, [r0], #4 ldr r4, [r1], #4 teq r3, r4 bne notmatch subs r2, r2, #4 beq done_nand_read bne go_next notmatch: #ifdef CONFIG_DEBUG_LL sub r0, r0, #4 ldr r1, SerBase bl PrintHexWord ldr r0, STR_FAIL ldr r1, SerBase bl PrintWord #endif 1: b 1b done_nand_read: #ifdef CONFIG_DEBUG_LL ldr r0, STR_OK ldr r1, SerBase bl PrintWord #endif mov pc, r10 @ clear memory @ r0: start address @ r1: length mem_clear: mov r2, #0 mov r3, r2 mov r4, r2 mov r5, r2 mov r6, r2 mov r7, r2 mov r8, r2 mov r9, r2 clear_loop: stmia r0!, {r2-r9} subs r1, r1, #(8 * 4) bne clear_loop mov pc, lr #endif @ CONFIG_S3C2410_NAND_BOOT 3.3进入C代码 首先要设置堆栈指针sp,堆栈指针的设置是为了执行C语言代码作好准备。设置好堆栈后,调用C语言的main函数: @ get read to call C functions ldr sp, DW_STACK_START @ setup stack pointer mov fp, #0 @ no previous frame, so fp=0 mov a2, #0 @ set argv to NULL bl main @ call main mov pc, #FLASH_BASE @ otherwise, reboot 4. BootLoader第二阶段 vivi Bootloader的第二阶段又分成了八个小阶段,在main函数中分别调用这几个小阶段的相关函数: int main(int argc, char *argv[]) { int ret; /* * Step 1: */ putstr(\"\\r\\n\"); putstr(vivi_banner); reset_handler(); /* * Step 2: */ ret = board_init(); if (ret) { putstr(\"Failed a board_init() procedure\\r\\n\"); error(); } /* * Step 3: */ mem_map_init(); mmu_init(); putstr(\"Succeed memory mapping.\\r\\n\"); /* * Now, vivi is running on the ram. MMU is enabled. */ /* * Step 4: */ /* initialize the heap area*/ ret = heap_init(); if (ret) { putstr(\"Failed initailizing heap region\\r\\n\"); error(); } /* Step 5: */ ret = mtd_dev_init(); /* Step 6: */ init_priv_data(); /* Step 7: */ misc(); init_builtin_cmds(); /* Step 8: */ boot_or_vivi(); return 0; } STEP1的putstr(vivi_banner)语句在串口输出一段字符说明vivi的版本、作者等信息,vivi_banner定义为: const char *vivi_banner = \"VIVI version \" VIVI_RELEASE \" (\" VIVI_COMPILE_BY \"@\" VIVI_COMPILE_HOST \") (\" VIVI_COMPILER \") \" UTS_VERSION \"\\r\\n\"; reset_handler进行相应的复位处理: void reset_handler(void) { int pressed; pressed = is_pressed_pw_btn(); if (pressed == PWBT_PRESS_LEVEL) { DPRINTK(\"HARD RESET\\r\\n\"); hard_reset_handle(); } else { DPRINTK(\"SOFT RESET\\r\\n\"); soft_reset_handle(); } } hard_reset_handle会clear内存,而软件复位处理则什么都不做: static void hard_reset_handle(void) { clear_mem((unsigned long)USER_RAM_BASE, (unsigned long)USER_RAM_SIZE); } STEP2进行板初始化,设置时间和可编程I/O口: int board_init(void) { init_time(); set_gpios(); return 0; } STEP3进行内存映射及MMU初始化: void mem_map_init(void) { #ifdef CONFIG_S3C2410_NAND_BOOT mem_map_nand_boot(); #else mem_map_nor(); #endif cache_clean_invalidate(); tlb_invalidate(); } S3C2410A的MMU初始化只需要调用通用的arm920 MMU初始化函数: static inline void arm920_setup(void) { unsigned long ttb = MMU_TABLE_BASE; __asm__( /* Invalidate caches */ \"mov r0, #0\\n\" \"mcr p15, 0, r0, c7, c7, 0\\n\" /* invalidate I,D caches on v4 */ \"mcr p15, 0, r0, c7, c10, 4\\n\" /* drain write buffer on v4 */ \"mcr p15, 0, r0, c8, c7, 0\\n\" /* invalidate I,D TLBs on v4 */ /* Load page table pointer */ \"mov r4, %0\\n\" \"mcr p15, 0, r4, c2, c0, 0\\n\" /* load page table pointer */ /* Write domain id (cp15_r3) */ \"mvn r0, #0\\n\" /* Domains 0, 1 = client */ \"mcr p15, 0, r0, c3, c0, 0\\n\" /* load domain access register */ /* Set control register v4 */ \"mrc p15, 0, r0, c1, c0, 0\\n\" /* get control register v4 */ /* Clear out 'unwanted' bits (then put them in if we need them) */ /* .RVI ..RS B... .CAM */ \"bic r0, r0, #0x3000\\n\" /* ..11 .... .... .... */ \"bic r0, r0, #0x0300\\n\" /* .... ..11 .... .... */ \"bic r0, r0, #0x0087\\n\" /* .... .... 1... .111 */ /* Turn on what we want */ /* Fault checking enabled */ \"orr r0, r0, #0x0002\\n\" /* .... .... .... ..1. */ #ifdef CONFIG_CPU_D_CACHE_ON \"orr r0, r0, #0x0004\\n\" /* .... .... .... .1.. */ #endif #ifdef CONFIG_CPU_I_CACHE_ON \"orr r0, r0, #0x1000\\n\" /* ...1 .... .... .... */ #endif /* MMU enabled */ \"orr r0, r0, #0x0001\\n\" /* .... .... .... ...1 */ \"mcr p15, 0, r0, c1, c0, 0\\n\" /* write control register */ : /* no outputs */ : \"r\" (ttb) ); } STEP4设置堆栈;STEP5进行mtd设备的初始化,记录MTD分区信息;STEP6设置私有数据;STEP7初始化内建命令。 STEP8启动一个SHELL,等待用户输出命令并进行相应处理。在SHELL退出的情况下,启动操作系统: #define DEFAULT_BOOT_DELAY 0x30000000 void boot_or_vivi(void) { char c; int ret; ulong boot_delay; boot_delay = get_param_value(\"boot_delay\if (ret) boot_delay = DEFAULT_BOOT_DELAY; /* If a value of boot_delay is zero, * unconditionally call vivi shell */ if (boot_delay == 0) vivi_shell(); /* * wait for a keystroke (or a button press if you want.) */ printk(\"Press Return to start the LINUX now, any other key for vivi\\n\"); c = awaitkey(boot_delay, NULL); if (((c != '\\r') && (c != '\\n') && (c != '\\0'))) { printk(\"type \\\"help\\\" for help.\\n\"); vivi_shell(); } run_autoboot(); return; } SHELL中读取用户从串口输出的命令字符串,执行该命令: void vivi_shell(void) { #ifdef CONFIG_SERIAL_TERM serial_term(); #else #error there is no terminal. #endif } void serial_term(void) { char cmd_buf[MAX_CMDBUF_SIZE]; for (;;) { printk(\"%s> \ getcmd(cmd_buf, MAX_CMDBUF_SIZE); /* execute a user command */ if (cmd_buf[0]) exec_string(cmd_buf); } } 5.电路板调试 在电路板的调试过程中,我们首先要在ADT新建的工程中添加第一阶段的汇编代码head.S文件,修改Link脚本,将代码和数据映射到S3C2410A自带的0x40000000开始的4KB内存空间内: SECTIONS { . = 0x40000000; .text : { *(.text) } Image_RO_Limit = .; Image_RW_Base = .; .data : { *(.data) } .rodata : { *(.rodata) } Image_ZI_Base = .; .bss : { *(.bss) } Image_ZI_Limit = .; __bss_start__ = .; __bss_end__ = .; __EH_FRAME_BEGIN__ = .; __EH_FRAME_END__ = .; PROVIDE (__stack = .); end = .; _end = .; .debug_info 0 : { *(.debug_info) } .debug_line 0 : { *(.debug_line) } .debug_abbrev 0 : { *(.debug_abbrev)} .debug_frame 0 : { *(.debug_frame) } }

借助万用表、示波器等仪器仪表,调通SDRAM,并将vivi中自带的串口、NAND FLASH驱动添加到工程中,调试通过板上的串口和FLASH。如果板电路的原理与三星公司DEMO板有差距,则vivi中硬件的操作要进行相应的修改。 全部调试通过后,修改vivi源代码,重新编译vivi,将其烧录入NAND FLASH就可以在复位后启动这个Bootloader了。

调试板上的新增硬件时,宜在ADT中添加相应的代码,在不加载操作系统的情况下,单纯地操作这些硬件。如果电路板设计有误,要进行飞线和割线等处理。

6.小结

本章讲解了ARM汇编、Bootloader的功能,Bootloader的调试环境及ARM电路板的调试方法。

移植嵌入式Linux到ARM处理器:操作系统

在笔者撰写的《C语言嵌入式系统编程修炼之道》一文中,主要陈诉的软件架构是单任务无操作系统平台的,而本文的侧重点则在于讲述操作系统嵌入的软件架构,二者的区别如下图:

嵌入式操作系统并不总是必须的,因为程序完全可以在裸板上运行。尽管如此,但对于复杂的系统,为使其具有任务管理、定时器管理、存储器管理、资源管理、 事件管理、系统管理、消息管理、队列管理和中断处理的能力,提供多任务处理,更好的分配系统资源的功能,很有必要针对特定的硬件平台和实际应用移植操作系 统。鉴于Linux的源代码开放性,它成为嵌入式操作系统领域的很好选择。国内外许多知名大学、公司、研究机构都加入了嵌入式Linux的研究行列,推出 了一些著名的版本:

·RT-Linux提供了一个精巧的实时内核,把标准的Linux核心作为实时核心的一个进程同用户的实时进程一 起调度。RT-Linux已成功地应用于航天飞机的空间数据采集、科学仪器测控和电影特技图像处理等广泛的应用领域。如NASA(美国国家宇航局)将装有 RT-Linux的设备放在飞机上,以测量Georage咫风的风速;

·uCLinux(Micro-Control-Linux,u表示Micro,C表示Control)去掉了MMU(内存管理)功能,应用于没有虚拟内存管理的微处理器/微控制器,它已经被成功地移植到了很多平台上。

本章涉及的mizi-linux由韩国mizi公司根据Linux 2.4内核移植而来,支持S3C2410A处理器。

1.Linux内核要点

和其他操作系统一样,Linux包含进程调度与进程间通信(IPC)、内存管理(MMU)、虚拟文件系统(VFS)、网络接口等,下图给出了Linux的组成及其关系:

Linux内核源代码包括多个目录:

(1)arch:包括硬件特定的内核代码,如arm、mips、i386等;

(2)drivers:包含硬件驱动代码,如char、cdrom、scsi、mtd等;

(3)include:通用头文件及针对不同平台特定的头文件,如asm-i386、asm-arm等;

(4)init:内核初始化代码;

(5)ipc:进程间通信代码;

(6)kernel:内核核心代码;

(7)mm:内存管理代码;

(8)net:与网络协议栈相关的代码,如ipv4、ipv6、ethernet等;

(9)fs:文件系统相关代码,如nfs、vfat等;

(10)lib:库文件,与平台无关的strlen、strcpy等,如在string.c中包含:

char * strcpy(char * dest,const char *src) { char *tmp = dest; while ((*dest++ = *src++) != '\\0') /* nothing */; return tmp; } (11)Documentation:文档 在Linux内核的实现中,有一些数据结构使用非常频繁,对研读内核的人来说至为关键,它们是:

1.task_struct

Linux内核利用task_struct数据结构代表一个进程,用task_struct指针形成一个task数组。当建立新进程的时候,Linux 为新的进程分配一个task_struct结构,然后将指针保存在task数组中。调度程序维护current指针,它指向当前正在运行的进程。

2.mm_struct

每个进程的虚拟内存由mm_struct结构代表。该结构中包含了一组指向vm-area_struct结构的指针,vm-area_struct结构描述了虚拟内存的一个区域。

3.inode

Linux虚拟文件系统中的文件、目录等均由对应的索引节点(inode)代表。

2.Linux移植项目

mizi-linux已经根据Linux 2.4内核针对S3C2410A这一芯片进行了有针对性的移植工作,包括:

(1)修改根目录下的Makefile文件

a.指定目标平台为ARM:

#ARCH := $(shell uname -m | sed -e s/i.86/i386/ -e s/sun4u/sparc64/ -e s/arm.*/arm/ -e s/sa110/arm/) ARCH := arm b.指定交叉编译器: CROSS_COMPILE = arm-linux- (2)修改arch目录中的文件 根据本章第一节可知,Linux的arch目录存放硬件相关的内核代码,因此,在Linux内核中增加对S3C2410的支持,最主要就是要修改arch目录中的文件。 a.在arch/arm/Makefile文件中加入: ifeq ($(CONFIG_ARCH_S3C2410),y) TEXTADDR = 0xC0008000 MACHINE = s3c2410 Endif b.在arch\\arm\\config.in文件中加入: if [ \"$CONFIG_ARCH_S3C2410\" = \"y\" ]; then comment 'S3C2410 Implementation' dep_bool ' SMDK (MERI TECH BOARD)' CONFIG_S3C2410_SMDK $CONFIG_ARCH_S3C2410 dep_bool ' change AIJI' CONFIG_SMDK_AIJI dep_tristate 'S3C2410 USB function support' CONFIG_S3C2410_USB $CONFIG_ARCH_S3C2100 dep_tristate ' Support for S3C2410 USB character device emulation' CONFIG_S3C2410_USB_CHAR $CONFIG_S3C2410_USB fi # /* CONFIG_ARCH_S3C2410 */ arch\\arm\\config.in文件还有几处针对S3C2410的修改。 c.在arch/arm/boot/Makefile文件中加入: ifeq ($(CONFIG_ARCH_S3C2410),y) ZTEXTADDR = 0x30008000 ZRELADDR = 0x30008000 endif d.在linux/arch/arm/boot/compressed/Makefile文件中加入: ifeq ($(CONFIG_ARCH_S3C2410),y) OBJS += head-s3c2410.o endif 加入的结果是head-s3c2410.S文件被编译为head-s3c2410.o。 e.加入arch\\arm\\boot\\compressed\\ head-s3c2410.S文件 #include #include #include .section \".start\ __S3C2410_start: @ Preserve r8/r7 i.e. kernel entry values @ What is it? @ Nandy @ Data cache, Intstruction cache, MMU might be active. @ Be sure to flush kernel binary out of the cache, @ whatever state it is, before it is turned off. @ This is done by fetching through currently executed @ memory to be sure we hit the same cache bic r2, pc, #0x1f add r3, r2, #0x4000 @ 16 kb is quite enough... 1: ldr r0, [r2], #32 teq r2, r3 bne 1b mcr p15, 0, r0, c7, c10, 4 @ drain WB mcr p15, 0, r0, c7, c7, 0 @ flush I & D caches #if 0 @ disabling MMU and caches mrc p15, 0, r0, c1, c0, 0 @ read control register bic r0, r0, #0x05 @ disable D cache and MMU bic r0, r0, #1000 @ disable I cache mcr p15, 0, r0, c1, c0, 0 #endif /* * Pause for a short time so that we give enough time * for the host to start a terminal up. */ mov r0, #0x00200000 1: subs r0, r0, #1 bne 1b

该文件中的汇编代码完成S3C2410特定硬件相关的初始化。 f.在arch\\arm\\def-configs目录中增加配置文件

g.在arch\\arm\\kernel\\Makefile中增加对S3C2410的支持

no-irq-arch := $(CONFIG_ARCH_INTEGRATOR) $(CONFIG_ARCH_CLPS711X) \\ $(CONFIG_FOOTBRIDGE) $(CONFIG_ARCH_EBSA110) \\ $(CONFIG_ARCH_SA1100) $(CONFIG_ARCH_CAMELOT) \\ $(CONFIG_ARCH_S3C2400) $(CONFIG_ARCH_S3C2410) \\ $(CONFIG_ARCH_MX1ADS) $(CONFIG_ARCH_PXA) obj-$(CONFIG_MIZI) += event.o obj-$(CONFIG_APM) += apm2.o h.修改arch/arm/kernel/debug-armv.S文件,在适当的位置增加如下关于S3C2410的代码: #elif defined(CONFIG_ARCH_S3C2410) .macro addruart,rx mrc p15, 0, \\rx, c1, c0 tst \\rx, #1 @ MMU enabled ? moveq \\rx, #0x50000000 @ physical base address movne \\rx, #0xf0000000 @ virtual address .endm .macro senduart,rd,rx str \\rd, [\\rx, #0x20] @ UTXH .endm .macro waituart,rd,rx .endm .macro busyuart,rd,rx 1001: ldr \\rd, [\\rx, #0x10] @ read UTRSTAT tst \\rd, #1 << 2 @ TX_EMPTY ? beq 1001b .endm i.修改arch/arm/kernel/setup.c文件 此文件中的setup_arch非常关键,用来完成与体系结构相关的初始化: void __init setup_arch(char **cmdline_p) { struct tag *tags = NULL; struct machine_desc *mdesc; char *from = default_command_line; ROOT_DEV = MKDEV(0, 255); setup_processor(); mdesc = setup_machine(machine_arch_type); machine_name = mdesc->name; if (mdesc->soft_reboot) reboot_setup(\"s\"); if (mdesc->param_offset) tags = phys_to_virt(mdesc->param_offset); /* * Do the machine-specific fixups before we parse the * parameters or tags. */ if (mdesc->fixup) mdesc->fixup(mdesc, (struct param_struct *)tags, &from, &meminfo); /* * If we have the old style parameters, convert them to * a tag list before. */ if (tags && tags->hdr.tag != ATAG_CORE) convert_to_tag_list((struct param_struct *)tags, meminfo.nr_banks == 0); if (tags && tags->hdr.tag == ATAG_CORE) parse_tags(tags); if (meminfo.nr_banks == 0) { meminfo.nr_banks = 1; meminfo.bank[0].start = PHYS_OFFSET; meminfo.bank[0].size = MEM_SIZE; } init_mm.start_code = (unsigned long) &_text; init_mm.end_code = (unsigned long) &_etext; init_mm.end_data = (unsigned long) &_edata; init_mm.brk = (unsigned long) &_end; memcpy(saved_command_line, from, COMMAND_LINE_SIZE); saved_command_line[COMMAND_LINE_SIZE-1] = '\\0'; parse_cmdline(&meminfo, cmdline_p, from); bootmem_init(&meminfo); paging_init(&meminfo, mdesc); request_standard_resources(&meminfo, mdesc); /* * Set up various architecture-specific pointers */ init_arch_irq = mdesc->init_irq; #ifdef CONFIG_VT #if defined(CONFIG_VGA_CONSOLE) conswitchp = &vga_con; #elif defined(CONFIG_DUMMY_CONSOLE) conswitchp = &dummy_con; #endif #endif } j.修改arch/arm/mm/mm-armv.c文件(arch/arm/mm/目录中的文件完成与ARM相关的MMU处理) 修改 init_maps->bufferable = 0; 为 init_maps->bufferable = 1;

要轻而易举地进 行上述马拉松式的内核移植工作并非一件轻松的事情,需要对Linux内核有很好的掌握,同时掌握硬件特定的知识和相关的汇编。幸而mizi公司的开发者们 已经合力为我们完成了上述工作,这使得小弟们在将mizi-linux移植到自身开发的电路板的过程中只需要关心如下几点:

(1)内核初始化:Linux内核的入口点是start_kernel()函数。它初始化内核的其他部分,包括捕获,IRQ通道,调度,设备驱动,标定延迟循环,最重要的是能够fork\"init\"进程,以启动整个多任务环境。

我们可以在init中加上一些特定的内容。

(2)设备驱动:设备驱动占据了Linux内核很大部分。同其他操作系统一样,设备驱动为它们所控制的硬件设备和操作系统提供接口。

本文第四章将单独讲解驱动程序的编写方法。

(3)文件系统:Linux最重要的特性之一就是对多种文件系统的支持。这种特性使得Linux很容易地同其他操作系统共存。文件系统的概念使得用户能 够查看存储设备上的文件和路径而无须考虑实际物理设备的文件系统类型。Linux透明的支持许多不同

的文件系统,将各种安装的文件和文件系统以一个完整的 虚拟文件系统的形式呈现给用户。

我们可以在K9S1208 NAND FLASH上移植cramfs、jfss2、yaffs等FLASH文件系统。

3. init进程

在init函数中\"加料\",可以使得Linux启动的时候做点什么,例如广州友善之臂公司的demo板在其中加入了公司信息:

static int init(void * unused) { lock_kernel(); do_basic_setup(); prepare_namespace(); /* * Ok, we have completed the initial bootup, and * we're essentially up and running. Get rid of the * initmem segments and start the user-mode stuff.. */ free_initmem(); unlock_kernel(); if (open(\"/dev/console\printk(\"Warning: unable to open an initial console.\\n\"); (void) dup(0); (void) dup(0); /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ printk(\"========================================\\n\"); printk(\"= Friendly-ARM Tech. Ltd. =\\n\"); printk(\"= http://www.arm9.net =\\n\"); printk(\"= http://www.arm9.com.cn =\\n\"); printk(\"========================================\\n\"); if (execute_command) execve(execute_command,argv_init,envp_init); execve(\"/sbin/init\execve(\"/etc/init\execve(\"/bin/init\execve(\"/bin/sh\panic(\"No init found. Try passing init= option to kernel.\"); } 这样在Linux的启动过程中,会额外地输出: ======================================== = Friendly-ARM Tech. Ltd. = = http://www.arm9.net = = http://www.arm9.com.cn = ======================================== 4.文件系统移植

文件系统是基于被划分的存储设备上的逻辑上单位上的一种定义文件的命名、存储、组织及取出的方法。如果一个Linux没有根文件系统,它是不能被正确的启动的。因此,我们需要为Linux创建根文件系统,我们将其创建在K9S1208 NAND FLASH上。

Linux的根文件系统可能包括如下目录(或更多的目录):

(1)/bin (binary):包含着所有的标准命令和应用程序;

(2)/dev (device):包含外设的文件接口,在Linux下,文件和设备采用同种地方法访问的,系统上的每个设备都在/dev里有一个对应的设备文件;

(3)/etc (etcetera):这个目录包含着系统设置文件和其他的系统文件,例如/etc/fstab(file system table)记录了启动时要mount 的filesystem;

(4)/home:存放用户主目录;

(5)/lib(library):存放系统最基本的库文件;

(6)/mnt:用户临时挂载文件系统的地方;

(7)/proc:linux提供的一个虚拟系统,系统启动时在内存中产生,用户可以直接通过访问这些文件来获得系统信息;

(8)/root:超级用户主目录;

(9)/sbin:这个目录存放着系统管理程序,如fsck、mount等;

(10)/tmp(temporary):存放不同的程序执行时产生的临时文件;

(11)/usr(user):存放用户应用程序和文件。

采用BusyBox是缩小根文件系统的好办法,因为其中提供了系统的许多基本指令但是其体积很小。众所周知,瑞士军刀以其小巧轻便、功能众多而闻名世界,成为各国军人的必备工具,并广泛应用于民间,而BusyBox也被称为嵌入式Linux领域的\"瑞士军刀\"。

此地址可以下载BusyBox:http://www.busybox.net,当前最新版本为1.1.3。编译好busybox后,将其放入/bin目录,若要使用其中的命令,只需要建立link,如:

ln -s ./busybox ls ln -s ./busybox mkdir

4.1 cramfs

在根文件系统中,为保护系统的基本设置不被更改,可以采用cramfs格式,它是一种只读的闪存文件系统。制作cramfs文件系统的方法为:建立一个 目录,将需要放到文件系统的文件copy到这个目录,运行\"mkcramfs 目录名 image名\"就可以生成一个cramfs文件系统的image文件。例如如果目录名为rootfs,则正确的命令为:

mkcramfs rootfs rootfs.ramfs

我们使用下面的命令可以mount生成的rootfs.ramfs文件,并查看其中的内容:

mount -o loop -t cramfs rootfs.ramfs /mount/point

此地址可以下载mkcramfs工具:http://sourceforge.net/projects/cramfs/。

4.2 jfss2

对于cramfs闪存文件系统,如果没有ramfs的支持则只能读,而采用jfss2(The Journalling Flash File System version 2)文件系统则可以直接在闪存中读、写数据。jfss2 是一个日志结构(log-structured)的文件系统,包含数据和原数据(meta-data)的节点在闪存上顺序地存储。jfss2记录了每个擦 写块的擦写次数,当闪存上各个擦写块的擦写次数的差距超过某个预定的阀值,开始进行磨损平衡的调整。调整的策略是,在垃圾回收时将擦写次数小的擦写块上的 数据迁移到擦写次数大的擦写块上以达到磨损平衡的目的。

与mkcramfs类似,同样有一个mkfs.jffs2工具可以将一个目录制作为jffs2文件系统。假设把/bin目录制作为jffs2文件系统,需要运行的命令为:

mkfs.jffs2 -d /bin -o jffs2.img

4.3 yaffs

yaffs 是一种专门为嵌入式系统中常用的闪存设备设计的一种可读写的文件系统,它比jffs2 文件系统具有更快的启动速度,对闪存使用寿命有更好的保护机制。为使Linux支持yaffs文件系统,我们需要将其对应的驱动加入到内核中 fs/yaffs/,并修改内核配置文件。使用我们使用mkyaffs工具可以将NAND FLASH中的分区格式化为yaffs格式(如/bin/mkyaffs /dev/mtdblock/0命令可以将第1个MTD块设备分区格式化为yaffs),而使用mkyaffsimage(类似于mkcramfs、 mkfs.jffs2)则可以将某目录生成为yaffs文件系统镜像。

嵌入式Linux还可以使用NFS(网络文件系统)通过以太网 挂接根文件系统,这是一种经常用来作为调试使用的文件系统启动方式。通过网络挂接的根文件系统,可以在主机上生成ARM 交叉编译版本的目标文件或二进制可执行文件,然后就可以直接装载或执行它,而不用频繁地写入flash。

采用不同的文件系统启动时,要注意通过第二章介绍的BootLoader修改启动参数,如广州友善之臂的demo提供如下三种启动方式:

(1)从cramfs挂接根文件系统:root=/dev/bon/2();

(2)从移植的yaffs挂接根文件系统:root=/dev/mtdblock/0;

(3)从以太网挂接根文件系统:root=/dev/nfs。

5.小结

本章介绍了嵌入式Linux的背景、移植项目、init进程修改和文件系统移植,通过这些步骤,我们可以在嵌入式系统上启动一个基本的Linux。

移植嵌入式Linux到ARM处理器:设备驱动

设备驱动程序是操作系统内核和机器硬件之间的接口,它为应用程序屏蔽硬件的细节,一般来说,Linux的设备驱动程序需要完成如下功能:

·设备初始化、释放;

·提供各类设备服务;

·负责内核和设备之间的数据交换;

·检测和处理设备工作过程中出现的错误。

Linux下的设备驱动程序被组织为一组完成不同任务的函数的集合,通过这些函数使得Windows的设备操作犹如文件一般。在应用程序看来,硬件设备 只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作,如open ()、close ()、read ()、write () 等。

Linux主要将设备分为二类:字符设备和块设备。字符设备是指设备发送和接收数据以字符的形式进行;而块设备则以整个数据缓冲区的形式进行。在对字符 设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了;而块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返 回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备主要针对磁盘等慢速设备。

1.内存分配

由于Linux驱动程序在内核中运行,因此在设备驱动程序需要申请/释放内存时,不能使用用户级的malloc/free函数,而需由内核级的函数kmalloc/kfree () 来实现,kmalloc()函数的原型为:

void kmalloc (size_t size ,int priority); 参数size为申请分配内存的字节数,kmalloc最多只能开辟128k的内存;参数priority说明若kmalloc()不能马上分配内存时用 户进程要采用的动作:GFP_KERNEL 表示等待,即等kmalloc()函数将一些内存安排到交换区来满足你的内存需要,GFP_ATOMIC 表示不等待,如不能立即分配到内存则返回0 值;函数的返回值指向已分配内存的起始地址,出错时,返回0。 kmalloc ()分配的内存需用kfree()函数来释放,kfree ()被定义为: # define kfree (n) kfree_s( (n) ,0) 其中kfree_s () 函数原型为: void kfree_s (void * ptr ,int size); 参数ptr为kmalloc()返回的已分配内存的指针,size是要释放内存的字节数,若为0 时,由内核自动确定内存的大小。 2.中断 许多设备涉及到中断操作,因此,在这样的设备的驱动程序中需要对硬件产生的中断请求提供中断服务程序。与注册基本入口点一样,驱动程序也要请求内核将特定的中断请求和中断服务程序联系在一起。在Linux中,用request_irq()函数来实现请求: int request_irq (unsigned int irq ,void( * handler) int ,unsigned long type ,char * name);

参数irq为要中断请求号,参数handler为指向中断服务程序的指针,参数type 用来确定是正常中断还是快速中断(正常中断指中断服务子程序返回后,内核可以执行调度程序来确定将运行哪一个进程;而快速中断是指中断服务子程序返回后, 立即执行被中断程序,正常中断type 取值为0 ,快速中断type 取值为SA_INTERRUPT),参数name是设备驱动程序的名称。 3.字符设备驱动

我们必须为字符设备提供一个初始化函数,该函数用来完成对所控设备的初始化工作,并调用register_chrdev() 函数注册字符设备。假设有一字符设备\"exampledev\",则其init 函数为:

void exampledev_init(void) { if (register_chrdev(MAJOR_NUM, \" exampledev \TRACE_TXT(\"Device exampledev driver registered error\"); else TRACE_TXT(\"Device exampledev driver registered successfully\"); …//设备初始化 } 其中,register_chrdev函数中的参数MAJOR_NUM为主设备号,\"exampledev\"为设备名,exampledev_fops 为包含基本函数入口点的结构体,类型为file_operations。当执行exampledev_init时,它将调用内核函数 register_chrdev,把驱动程序的基本入口点指针存放在内核的字符设备地址表中,在用户进程对该设备执行系统调用时提供入口地址。 较早版本内核的file_operations结构体定义为(代码及图示): struct file_operations { int (*lseek)(); int (*read)(); int (*write)(); int (*readdir)(); int (*select)(); int (*ioctl)(); int (*mmap)(); int (*open)(); void(*release)(); int (*fsync)(); int (*fasync)(); int (*check_media_change)(); void(*revalidate)(); }; 随着内核功能的加强,file_operations结构体也变得更加庞大。但是大多数的驱动程序只是利用了其中的一部分,对于驱动程序中无需提供的功 能,只需要把相应位置的值设为NULL。对于字符设备来说,要提供的主要入口有:open ()、release ()、read ()、write ()、ioctl ()等。 open()函数 对设备特殊文件进行open()系统调用时,将调用驱动程序的open () 函数: int (*open)(struct inode * inode,struct file *filp);

其中参数inode为设备特殊文件的inode (索引结点) 结构的指针,参数filp是指向这一设备的文件结构的指针。open()的主要任务是确定硬件处在就绪状态、验证次设备号的合法性(次设备号可以用 MINOR(inode-> i_rdev) 取得)、控制使用设备的进程数、根据执行情况返回状态码(0表示成功,负数表示存在错误) 等;

release()函数 当最后一个打开设备的用户进程执行close ()系统调用时,内核将调用驱动程序的release () 函数:

void (*release) (struct inode * inode,struct file *filp) ; release 函数的主要任务是清理未结束的输入/输出操作、释放资源、用户自定义排他标志的复位等。 read()函数 当对设备特殊文件进行read() 系统调用时,将调用驱动程序read() 函数: ssize_t (*read) (struct file * filp, char * buf, size_t count, loff_t * offp); 参数buf是指向用户空间缓冲区的指针,由用户进程给出,count 为用户进程要求读取的字节数,也由用户给出。 read() 函数的功能就是从硬设备或内核内存中读取或复制count个字节到buf 指定的缓冲区中。在复制数据时要注意,驱动程序运行在内核中,而buf指定的缓冲区在用户内存区中,是不能直接在内核中访问使用的,因此,必须使用特殊的 复制函数来完成复制工作,这些函数在include/asm/uaccess.h中被声明: unsigned long copy_to_user (void * to, void * from, unsigned long len); 此外,put_user()函数用于内核空间和用户空间的单值交互(如char、int、long)。 write( ) 函数 当设备特殊文件进行write () 系统调用时,将调用驱动程序的write () 函数: ssize_t (*write) (struct file *, const char *, size_t, loff_t *); write ()的功能是将参数buf 指定的缓冲区中的count 个字节内容复制到硬件或内核内存中,和read() 一样,复制工作也需要由特殊函数来完成: unsigned long copy_from_user(void *to, const void *from, unsigned long n); 此外,get_user()函数用于内核空间和用户空间的单值交互(如char、int、long)。 ioctl() 函数 该函数是特殊的控制函数,可以通过它向设备传递控制信息或从设备取得状态信息,函数原型为:

int (*ioctl) (struct inode * inode,struct file * filp,unsigned int cmd,unsigned long arg); 参数cmd为设备驱动程序要执行的命令的代码,由用户自定义,参数arg 为相应的命令提供参数,类型可以是整型、指针等。 同样,在驱动程序中,这些函数的定义也必须符合命名规则,按照本文约定,设备\"exampledev\"的驱动程序的这些函数应分别命名为 exampledev_open、exampledev_ release、exampledev_read、exampledev_write、exampledev_ioctl,因此设备 \"exampledev\"的基本入口点结构变量exampledev_fops 赋值如下(对较早版本的内核): struct file_operations exampledev_fops { NULL , exampledev_read , exampledev_write , NULL , NULL , exampledev_ioctl , NULL , exampledev_open , exampledev_release , NULL , NULL , NULL , NULL } ; 就目前而言,由于file_operations结构体已经很庞大,我们更适合用GNU扩展的C语法来初始化exampledev_fops: struct file_operations exampledev_fops = { read: exampledev _read, write: exampledev _write, ioctl: exampledev_ioctl , open: exampledev_open , release : exampledev_release , }; 看看第一章电路板硬件原理图,板上包含四个用户可编程的发光二极管(LED),这些LED连接在ARM处理器的可编程I/O口(GPIO)上,现在来编写这些LED的驱动: #include #include #include #include #include #include #include #define DEVICE_NAME \"leds\" /*定义led 设备的名字*/ #define LED_MAJOR 231 /*定义led 设备的主设备号*/ static unsigned long led_table[] = { /*I/O 方式led 设备对应的硬件资源*/ GPIO_B10, GPIO_B8, GPIO_B5, GPIO_B6, }; /*使用ioctl 控制led*/ static int leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case 0: case 1: if (arg > 4) { return -EINVAL; } write_gpio_bit(led_table[arg], !cmd); default: return -EINVAL; } } static struct file_operations leds_fops = { owner: THIS_MODULE, ioctl: leds_ioctl, }; static devfs_handle_t devfs_handle; static int __init leds_init(void) { int ret; int i; /*在内核中注册设备*/ ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops); if (ret < 0) { printk(DEVICE_NAME \" can't register major number\\n\"); return ret; } devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT, LED_MAJOR, 0, S_IFCHR | S_IRUSR | S_IWUSR, &leds_fops, NULL); /*使用宏进行端口初始化,set_gpio_ctrl 和write_gpio_bit 均为宏定义*/ for (i = 0; i < 8; i++) { set_gpio_ctrl(led_table[i] | GPIO_PULLUP_EN | GPIO_MODE_OUT); write_gpio_bit(led_table[i], 1); } printk(DEVICE_NAME \" initialized\\n\"); return 0; } static void __exit leds_exit(void) { devfs_unregister(devfs_handle); unregister_chrdev(LED_MAJOR, DEVICE_NAME); } module_init(leds_init); module_exit(leds_exit); 使用命令方式编译led 驱动模块: #arm-linux-gcc -D__KERNEL__ -I/arm/kernel/include -DKBUILD_BASENAME=leds -DMODULE -c -o leds.o leds.c 以上命令将生成leds.o 文件,把该文件复制到板子的/lib目录下,使用以下命令就可以安装leds驱动模块: #insmod /lib/ leds.o 删除该模块的命令是: #rmmod leds 4.块设备驱动 块设备驱动程序的编写是一个浩繁的工程,其难度远超过字符设备,上千行的代码往往只能搞定一个简单 的块设备,而数十行代码就可能搞定一个字符设备。因此,非得有相当的基本功才能完成此项工作。下面先给出一个实例,即mtdblock块设备的驱动。我们 通过分析此实例中的代码来说明块设备驱动程序的写法(由于篇幅的关系,大量的代码被省略,只保留了必要的主干):

#include #include static void mtd_notify_add(struct mtd_info* mtd); static void mtd_notify_remove(struct mtd_info* mtd); static struct mtd_notifier notifier = { mtd_notify_add, mtd_notify_remove, NULL }; static devfs_handle_t devfs_dir_handle = NULL; static devfs_handle_t devfs_rw_handle[MAX_MTD_DEVICES]; static struct mtdblk_dev { struct mtd_info *mtd; /* Locked */ int count; struct semaphore cache_sem; unsigned char *cache_data; unsigned long cache_offset; unsigned int cache_size; enum { STATE_EMPTY, STATE_CLEAN, STATE_DIRTY } cache_state; } *mtdblks[MAX_MTD_DEVICES]; static spinlock_t mtdblks_lock; /* this lock is used just in kernels >= 2.5.x */ static spinlock_t mtdblock_lock; static int mtd_sizes[MAX_MTD_DEVICES]; static int mtd_blksizes[MAX_MTD_DEVICES]; static void erase_callback(struct erase_info *done) { wait_queue_head_t *wait_q = (wait_queue_head_t *)done->priv; wake_up(wait_q); } static int erase_write (struct mtd_info *mtd, unsigned long pos, int len, const char *buf) { struct erase_info erase; DECLARE_WAITQUEUE(wait, current); wait_queue_head_t wait_q; size_t retlen; int ret; /* * First, let's erase the flash block. */ init_waitqueue_head(&wait_q); erase.mtd = mtd; erase.callback = erase_callback; erase.addr = pos; erase.len = len; erase.priv = (u_long)&wait_q; set_current_state(TASK_INTERRUPTIBLE); add_wait_queue(&wait_q, &wait); ret = MTD_ERASE(mtd, &erase); if (ret) { set_current_state(TASK_RUNNING); remove_wait_queue(&wait_q, &wait); printk (KERN_WARNING \"mtdblock: erase of region [0x%lx, 0x%x] \" \"on \\\"%s\\\" failed\\n\pos, len, mtd->name); return ret; } schedule(); /* Wait for erase to finish. */ remove_wait_queue(&wait_q, &wait); /* * Next, writhe data to flash. */ ret = MTD_WRITE (mtd, pos, len, &retlen, buf); if (ret) return ret; if (retlen != len) return -EIO; return 0; } static int write_cached_data (struct mtdblk_dev *mtdblk) { struct mtd_info *mtd = mtdblk->mtd; int ret; if (mtdblk->cache_state != STATE_DIRTY) return 0; DEBUG(MTD_DEBUG_LEVEL2, \"mtdblock: writing cached data for \\\"%s\\\" \" \"at 0x%lx, size 0x%x\\n\ mtdblk->cache_offset, mtdblk->cache_size); ret = erase_write (mtd, mtdblk->cache_offset, mtdblk->cache_size, mtdblk->cache_data); if (ret) return ret; mtdblk->cache_state = STATE_EMPTY; return 0; } static int do_cached_write (struct mtdblk_dev *mtdblk, unsigned long pos, int len, const char *buf) { … } static int do_cached_read (struct mtdblk_dev *mtdblk, unsigned long pos, int len, char *buf) { … } static int mtdblock_open(struct inode *inode, struct file *file) { … } static release_t mtdblock_release(struct inode *inode, struct file *file) { int dev; struct mtdblk_dev *mtdblk; DEBUG(MTD_DEBUG_LEVEL1, \"mtdblock_release\\n\"); if (inode == NULL) release_return(-ENODEV); dev = minor(inode->i_rdev); mtdblk = mtdblks[dev]; down(&mtdblk->cache_sem); write_cached_data(mtdblk); up(&mtdblk->cache_sem); spin_lock(&mtdblks_lock); if (!--mtdblk->count) { /* It was the last usage. Free the device */ mtdblks[dev] = NULL; spin_unlock(&mtdblks_lock); if (mtdblk->mtd->sync) mtdblk->mtd->sync(mtdblk->mtd); put_mtd_device(mtdblk->mtd); vfree(mtdblk->cache_data); kfree(mtdblk); } else { spin_unlock(&mtdblks_lock); } DEBUG(MTD_DEBUG_LEVEL1, \"ok\\n\"); BLK_DEC_USE_COUNT; release_return(0); } /* * This is a special request_fn because it is executed in a process context * to be able to sleep independently of the caller. The * io_request_lock (for <2.5) or queue_lock (for >=2.5) is held upon entry * and exit. The head of our request queue is considered active so there is * no need to dequeue requests before we are done. */ static void handle_mtdblock_request(void) { struct request *req; struct mtdblk_dev *mtdblk; unsigned int res; for (;;) { INIT_REQUEST; req = CURRENT; spin_unlock_irq(QUEUE_LOCK(QUEUE)); mtdblk = mtdblks[minor(req->rq_dev)]; res = 0; if (minor(req->rq_dev) >= MAX_MTD_DEVICES) panic(\"%s : minor out of bound\ if (!IS_REQ_CMD(req)) goto end_req; if ((req->sector + req->current_nr_sectors) > (mtdblk->mtd->size >> 9)) goto end_req; // Handle the request switch (rq_data_dir(req)) { int err; case READ: down(&mtdblk->cache_sem); err = do_cached_read (mtdblk, req->sector << 9, req->current_nr_sectors << 9, req->buffer); up(&mtdblk->cache_sem); if (!err) res = 1; break; case WRITE: // Read only device if ( !(mtdblk->mtd->flags & MTD_WRITEABLE) ) break; // Do the write down(&mtdblk->cache_sem); err = do_cached_write (mtdblk, req->sector << 9,req->current_nr_sectors << 9, req->buffer); up(&mtdblk->cache_sem); if (!err) res = 1; break; } end_req: spin_lock_irq(QUEUE_LOCK(QUEUE)); end_request(res); } } static volatile int leaving = 0; static DECLARE_MUTEX_LOCKED(thread_sem); static DECLARE_WAIT_QUEUE_HEAD(thr_wq); int mtdblock_thread(void *dummy) { … } #define RQFUNC_ARG request_queue_t *q static void mtdblock_request(RQFUNC_ARG) { /* Don't do anything, except wake the thread if necessary */ wake_up(&thr_wq); } static int mtdblock_ioctl(struct inode * inode, struct file * file, unsigned int cmd, unsigned long arg) { struct mtdblk_dev *mtdblk; mtdblk = mtdblks[minor(inode->i_rdev)]; switch (cmd) { case BLKGETSIZE: /* Return device size */ return put_user((mtdblk->mtd->size >> 9), (unsigned long *) arg); case BLKFLSBUF: if(!capable(CAP_SYS_ADMIN)) return -EACCES; fsync_dev(inode->i_rdev); invalidate_buffers(inode->i_rdev); down(&mtdblk->cache_sem); write_cached_data(mtdblk); up(&mtdblk->cache_sem); if (mtdblk->mtd->sync) mtdblk->mtd->sync(mtdblk->mtd); return 0; default: return -EINVAL; } } static struct block_device_operations mtd_fops = { owner: THIS_MODULE, open: mtdblock_open, release: mtdblock_release, ioctl: mtdblock_ioctl }; static void mtd_notify_add(struct mtd_info* mtd) { … } static void mtd_notify_remove(struct mtd_info* mtd) { if (!mtd || mtd->type == MTD_ABSENT) return; devfs_unregister(devfs_rw_handle[mtd->index]); } int __init init_mtdblock(void) { int i; spin_lock_init(&mtdblks_lock); /* this lock is used just in kernels >= 2.5.x */ spin_lock_init(&mtdblock_lock); #ifdef CONFIG_DEVFS_FS if (devfs_register_blkdev(MTD_BLOCK_MAJOR, DEVICE_NAME, &mtd_fops)) { printk(KERN_NOTICE \"Can't allocate major number %d for Memory Technology Devices.\\n\MTD_BLOCK_MAJOR); return -EAGAIN; } devfs_dir_handle = devfs_mk_dir(NULL, DEVICE_NAME, NULL); register_mtd_user(¬ifier); #else if (register_blkdev(MAJOR_NR,DEVICE_NAME,&mtd_fops)) { printk(KERN_NOTICE \"Can't allocate major number %d for Memory Technology Devices.\\n\MTD_BLOCK_MAJOR); return -EAGAIN; } #endif /* We fill it in at open() time. */ for (i=0; i< MAX_MTD_DEVICES; i++) { mtd_sizes[i] = 0; mtd_blksizes[i] = BLOCK_SIZE; } init_waitqueue_head(&thr_wq); /* Allow the block size to default to BLOCK_SIZE. */ blksize_size[MAJOR_NR] = mtd_blksizes; blk_size[MAJOR_NR] = mtd_sizes; BLK_INIT_QUEUE(BLK_DEFAULT_QUEUE(MAJOR_NR), &mtdblock_request, &mtdblock_lock); kernel_thread (mtdblock_thread, NULL, CLONE_FS|CLONE_FILES|CLONE_SIGHAND); return 0; } static void __exit cleanup_mtdblock(void) { leaving = 1; wake_up(&thr_wq); down(&thread_sem); #ifdef CONFIG_DEVFS_FS unregister_mtd_user(¬ifier); devfs_unregister(devfs_dir_handle); devfs_unregister_blkdev(MTD_BLOCK_MAJOR, DEVICE_NAME); #else unregister_blkdev(MAJOR_NR,DEVICE_NAME); #endif blk_cleanup_queue(BLK_DEFAULT_QUEUE(MAJOR_NR)); blksize_size[MAJOR_NR] = NULL; blk_size[MAJOR_NR] = NULL; } module_init(init_mtdblock); module_exit(cleanup_mtdblock); 从上述源代码中我们发现,块设备也以与字符设备register_chrdev、unregister_ chrdev 函数类似的方法进行设备的注册与释放: int register_blkdev(unsigned int major, const char *name, struct block_device_operations *bdops); int unregister_blkdev(unsigned int major, const char *name); 但是,register_chrdev使用一个向 file_operations 结构的指针,而register_blkdev 则使用 block_device_operations 结构的指针,其中定义的open、release 和 ioctl 方法和字符设备的对应方法相同,但未定义 read 或者 write 操作。这是因为,所有涉及到块设备的 I/O 通常由系统进行缓冲处理。 块驱动程序最终必须提供完成实际块 I/O 操作的机制,在 Linux 当中,用于这些 I/O 操作的方法称为\"request(请求)\"。在块设备的注册过程中,需要初始化request队列,这一动作通过blk_init_queue来完成, blk_init_queue函数建立队列,并将该驱动程序的 request 函数关联到队列。在模块的清除阶段,应调用 blk_cleanup_queue 函数。 本例中相关的代码为: BLK_INIT_QUEUE(BLK_DEFAULT_QUEUE(MAJOR_NR), &mtdblock_request, &mtdblock_lock); blk_cleanup_queue(BLK_DEFAULT_QUEUE(MAJOR_NR)); 每个设备有一个默认使用的请求队列,必要时,可使用 BLK_DEFAULT_QUEUE(major) 宏得到该默认队列。这个宏在 blk_dev_struct 结构形成的全局数组(该数组名为 blk_dev)中搜索得到对应的默认队列。blk_dev 数组由内核维护,并可通过主设备号索引。blk_dev_struct 接口定义如下: struct blk_dev_struct { /* * queue_proc has to be atomic */ request_queue_t request_queue; queue_proc *queue; void *data; }; request_queue 成员包含了初始化之后的 I/O 请求队列,data 成员可由驱动程序使用,以便保存一些私有数据。

request_queue定义为:

struct request_queue { /* * the queue request freelist, one for reads and one for writes */ struct request_list rq[2]; /* * Together with queue_head for cacheline sharing */ struct list_head queue_head; elevator_t elevator; request_fn_proc * request_fn; merge_request_fn * back_merge_fn; merge_request_fn * front_merge_fn; merge_requests_fn * merge_requests_fn; make_request_fn * make_request_fn; plug_device_fn * plug_device_fn; /* * The queue owner gets to use this for whatever they like. * ll_rw_blk doesn't touch it. */ void * queuedata; /* * This is used to remove the plug when tq_disk runs. */ struct tq_struct plug_tq; /* * Boolean that indicates whether this queue is plugged or not. */ char plugged; /* * Boolean that indicates whether current_request is active or * not. */ char head_active; /* * Is meant to protect the queue in the future instead of * io_request_lock */ spinlock_t queue_lock; /* * Tasks wait here for free request */ wait_queue_head_t wait_for_request; }; 下图表征了blk_dev、blk_dev_struct和request_queue的关系:

下图则表征了块设备的注册和释放过程:

5.小结

本章讲述了Linux设备驱动程序的入口函数及驱动程序中的内存申请、中断等,并分别以实例讲述了字符设备及块设备的驱动开发方法。

移植嵌入式Linux到ARM处理器:应用实例

应用实例的编写实际上已经不属于Linux操作系统移植的范畴,但是为了保证本系列文章的完整性,这里提供一系列针对嵌入式Linux开发应用程序的实例。

编写Linux应用程序要用到如下工具:

(1)编译器:GCC

GCC是Linux平台下最重要的开发工具,它是GNU的C和C++编译器,其基本用法为:gcc [options] [filenames]。

我们应该使用arm-linux-gcc。

(2)调试器:GDB

gdb是一个用来调试C和C++程序的强力调试器,我们能通过它进行一系列调试工作,包括设置断点、观查变量、单步等。

我们应该使用arm-linux-gdb。

(3)Make

GNU Make的主要工作是读进一个文本文件,称为makefile。这个文件记录了哪些文件由哪些文件产生,用什么命令来产生。Make依靠此 makefile中的信息检查磁盘上的文件,如果目的文件的创建或修改时间比它的一个依靠文件旧的话,make就执行相应的命令,以便更新目的文件。

Makefile中的编译规则要相应地使用arm-linux-版本。

(4)代码编辑

可以使用传统的vi编辑器,但最好采用emacs软件,它具备语法高亮、版本控制等附带功能。

在宿主机上用上述工具完成应用程序的开发后,可以通过如下途径将程序下载到目标板上运行:

(1)通过串口通信协议rz将程序下载到目标板的文件系统中(感谢Linux提供了rz这样的一个命令);

(2)通过ftp通信协议从宿主机上的ftp目录里将程序下载到目标板的文件系统中;

(3)将程序拷入U盘,在目标机上mount U盘,运行U盘中的程序;

(4)如果目标机Linux使用NFS文件系统,则可以直接将程序拷入到宿主机相应的目录内,在目标机Linux中可以直接使用。

1. 文件编程

Linux的文件操作API涉及到创建、打开、读写和关闭文件。

创建

int creat(const char *filename, mode_t mode); 参数mode指定新建文件的存取权限,它同umask一起决定文件的最终权限(mode&umask),其中umask代表了文件在创建时需要去掉的一些存取权限。umask可通过系统调用umask()来改变: int umask(int newmask); 该调用将umask设置为newmask,然后返回旧的umask,它只影响读、写和执行权限。 打开 int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); 读写 在文件打开以后,我们才可对文件进行读写了,Linux中提供文件读写的系统调用是read、write函数: int read(int fd, const void *buf, size_t length); int write(int fd, const void *buf, size_t length); 其中参数buf为指向缓冲区的指针,length为缓冲区的大小(以字节为单位)。函数read()实现从文件描述符fd所指定的文件中读取 length个字节到buf所指向的缓冲区中,返回值为实际读取的字节数。函数write实现将把length个字节从buf指向的缓冲区中写到文件描述 符fd所指向的文件中,返回值为实际写入的字节数。 以O_CREAT为标志的open实际上实现了文件创建的功能,因此,下面的函数等同creat()函数: int open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);

定位

对于随机文件,我们可以随机的指定位置读写,使用如下函数进行定位:

int lseek(int fd, offset_t offset, int whence); lseek()将文件读写指针相对whence移动offset个字节。操作成功时,返回文件指针相对于文件头的位置。参数whence可使用下述值: SEEK_SET:相对文件开头 SEEK_CUR:相对文件读写指针的当前位置 SEEK_END:相对文件末尾 offset可取负值,例如下述调用可将文件指针相对当前位置向前移动5个字节: lseek(fd, -5, SEEK_CUR); 由于lseek函数的返回值为文件指针相对于文件头的位置,因此下列调用的返回值就是文件的长度: lseek(fd, 0, SEEK_END); 关闭 只要调用close就可以了,其中fd是我们要关闭的文件描述符: int close(int fd); 下面我们来编写一个应用程序,在当前目录下创建用户可读写文件\"example.txt\",在其中写入\"Hello World\",关闭文件,再次打开它,读取其中的内容并输出在屏幕上: #include #include #include #include #define LENGTH 100 main() { int fd, len; char str[LENGTH]; fd = open(\"hello.txt\创建并打开文件 */ if (fd) { write(fd, \"Hello, Software Weekly\ /* 写入Hello, software weekly字符串 */ close(fd); } fd = open(\"hello.txt\ len = read(fd, str, LENGTH); /* 读取文件内容 */ str[len] = '\\0'; printf(\"%s\\n\ close(fd); } 2. 进程控制/通信编程 进程控制中主要涉及到进程的创建、睡眠和退出等,在Linux中主要提供了fork、exec、clone的进程创建方法,sleep的进程睡眠和exit的进程退出调用,另外Linux还提供了父进程等待子进程结束的系统调用wait。 fork 对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一,因为它执行一次却返回两个值,以前\"闻所未闻\"。先看下面的程序: int main() { int i; if (fork() == 0) { for (i = 1; i < 3; i++) printf(\"This is child process\\n\"); } else { for (i = 1; i < 3; i++) printf(\"This is parent process\\n\"); } } 执行结果为: This is child process This is child process This is parent process This is parent process fork在英文中是\"分叉\"的意思,一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就\"分叉\"了。当前进程为父进程,通过 fork()会产生一个子进程。对于父进程,fork函数返回子程序的进程号而对于子程序,fork函数则返回零,这就是一个函数返回两次的本质。 exec 在Linux中可使用exec函数族,包含多个函数(execl、execlp、execle、execv、execve和execvp),被用于启动 一个指定路径和文件名的进程。exec函数族的特点体现在:某进程一旦调用了exec类函数,正在执行的程序就被干掉了,系统把代码段替换成新的程序(由 exec类函数执行)的代码,并且原有的数据段和堆栈段也被废弃,新的数据段与堆栈段被分配,但是进程号却被保留。也就是说,exec执行的结果为:系统 认为正在执行的还是原先的进程,但是进程对应的程序被替换了。 fork函数可以创建一个子进程而当前进程不死,如果我们在fork的子进程中调用exec函数族就可以实现既让父进程的代码执行又启动一个新的指定进程,这很好。fork和exec的搭配巧妙地解决了程序启动另一程序的执行但自己仍继续运行的问题,请看下面的例子: char command[MAX_CMD_LEN]; void main() { int rtn; /* 子进程的返回数值 */ while (1) { /* 从终端读取要执行的命令 */ printf(\">\"); fgets(command, MAX_CMD_LEN, stdin); command[strlen(command) - 1] = 0; if (fork() == 0) { /* 子进程执行此命令 */ execlp(command, command); /* 如果exec函数返回,表明没有正常执行命令,打印错误信息*/ perror(command); exit(errorno); } else { /* 父进程,等待子进程结束,并打印子进程的返回值 */ wait(&rtn); printf(\" child process return %d\\n\ } } } 这个函数实现了一个shell的功能,它读取用户输入的进程名和参数,并启动对应的进程。 clone clone是Linux2.0以后才具备的新功能,它较fork更强(可认为fork是clone要实现的一部分),可以使得创建的子进程共享父进程的资源,并且要使用此函数必须在编译内核时设置clone_actually_works_ok选项。 clone函数的原型为: int clone(int (*fn)(void *), void *child_stack, int flags, void *arg); 此函数返回创建进程的PID,函数中的flags标志用于设置创建子进程时的相关选项。 来看下面的例子: int variable, fd; int do_something() { variable = 42; close(fd); _exit(0); } int main(int argc, char *argv[]) { void **child_stack; char tempch; variable = 9; fd = open(\"test.file\ child_stack = (void **) malloc(16384); printf(\"The variable was %d\\n\ clone(do_something, child_stack, CLONE_VM|CLONE_FILES, NULL); sleep(1); /* 延时以便子进程完成关闭文件操作、修改变量 */ printf(\"The variable is now %d\\n\ if (read(fd, &tempch, 1) < 1) { perror(\"File Read Error\"); exit(1); } printf(\"We could read from the file\\n\"); return 0; } 运行输出: The variable is now 42 File Read Error 程序的输出结果告诉我们,子进程将文件关闭并将变量修改(调用clone时用到的CLONE_VM、CLONE_FILES标志将使得变量和文件描述符表被共享),父进程随即就感觉到了,这就是clone的特点。 sleep 函数调用sleep可以用来使进程挂起指定的秒数,该函数的原型为: unsigned int sleep(unsigned int seconds); 该函数调用使得进程挂起一个指定的时间,如果指定挂起的时间到了,该调用返回0;如果该函数调用被信号所打断,则返回剩余挂起的时间数(指定的时间减去已经挂起的时间)。 exit 系统调用exit的功能是终止本进程,其函数原型为: void _exit(int status); _exit会立即终止发出调用的进程,所有属于该进程的文件描述符都关闭。参数status作为退出的状态值返回父进程,在父进程中通过系统调用wait可获得此值。 wait wait系统调用包括: pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);

wait的作用为发出调用的进程只要有子进程,就睡眠到它们中的一个终止为止; waitpid等待由参数pid指定的子进程退出。

Linux的进程间通信(IPC,InterProcess Communication)通信方法有管道、消息队列、共享内存、信号量、套接口等。套接字通信并不为Linux所专有,在所有提供了TCP/IP协议 栈的操作系统中几乎都提供了socket,而所有这样操作系统,对套接字的编程方法几乎是完全一样的。管道分为有名管道和无名管道,无名管道只能用于亲属 进程之间的通信,而有名管道则可用于无亲属关系的进程之间;消息队列用于运行于同一台机器上的进程间通信,与管道相似;共享内存通常由一个进程创建,其余 进程对这块内存区进行读写;信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。

下面是一个使用信号量的例子,该程序创建一个特定的IPC结构的关键字和一个信号量,建立此信号量的索引,修改索引指向的信号量的值,最后清除信号量:

#include #include #include #include void main() { key_t unique_key; /* 定义一个IPC关键字*/ int id; struct sembuf lock_it; union semun options; int i; unique_key = ftok(\".\生成关键字,字符'a'是一个随机种子*/ /* 创建一个新的信号量集合*/ id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666); printf(\"semaphore id=%d\\n\ options.val = 1; /*设置变量值*/ semctl(id, 0, SETVAL, options); /*设置索引0的信号量*/ /*打印出信号量的值*/ i = semctl(id, 0, GETVAL, 0); printf(\"value of semaphore at index 0 is %d\\n\ /*下面重新设置信号量*/ lock_it.sem_num = 0; /*设置哪个信号量*/ lock_it.sem_op = - 1; /*定义操作*/ lock_it.sem_flg = IPC_NOWAIT; /*操作方式*/ if (semop(id, &lock_it, 1) == - 1) { printf(\"can not lock semaphore.\\n\"); exit(1); } i = semctl(id, 0, GETVAL, 0); printf(\"value of semaphore at index 0 is %d\\n\ /*清除信号量*/ semctl(id, 0, IPC_RMID, 0); } 3. 线程控制/通信编程 Linux本身只有进程的概念,而其所谓的\"线程\"本质上在内核里 仍然是进程。大家知道,进程是资源分配的单位,同一进程中的多个线程共享该进程的资源(如作为共享内存的全局变量)。Linux中所谓的\"线程\"只是在被 创建的时候\"克隆\"(clone)了父进程的资源,因此,clone出来的进程表现为\"线程\"。Linux中最流行的线程机制为 LinuxThreads,它实现了一种Posix 1003.1c \"pthread\"标准接口。 线程之间的通信涉及同步和互斥,互斥体的用法为: pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); //按缺省的属性初始化互斥体变量mutex pthread_mutex_lock(&mutex); // 给互斥体变量加锁 … //临界资源 phtread_mutex_unlock(&mutex); // 给互斥体变量解锁 同步就是线程等待某个事件的发生。只有当等待的事件发生线程才继续执行,否则线程挂起并放弃处理器。当多个线程协作时,相互作用的任务必须在一定的条件 下同步。Linux下的C语言编程有多种线程同步机制,最典型的是条件变量(condition variable)。而在头文件semaphore.h 中定义的信号量则完成了互斥体和条件变量的封装,按照多线程程序设计中访问控制机制,控制对资源的同步访问,提供程序设计人员更方便的调用接口。下面的生 产者/消费者问题说明了Linux线程的控制和通信: #include #include #define BUFFER_SIZE 16 struct prodcons { int buffer[BUFFER_SIZE]; pthread_mutex_t lock; int readpos, writepos; pthread_cond_t notempty; pthread_cond_t notfull; }; /* 初始化缓冲区结构 */ void init(struct prodcons *b) { pthread_mutex_init(&b->lock, NULL); pthread_cond_init(&b->notempty, NULL); pthread_cond_init(&b->notfull, NULL); b->readpos = 0; b->writepos = 0; } /* 将产品放入缓冲区,这里是存入一个整数*/ void put(struct prodcons *b, int data) { pthread_mutex_lock(&b->lock); /* 等待缓冲区未满*/ if ((b->writepos + 1) % BUFFER_SIZE == b->readpos) { pthread_cond_wait(&b->notfull, &b->lock); } /* 写数据,并移动指针 */ b->buffer[b->writepos] = data; b->writepos++; if (b->writepos > = BUFFER_SIZE) b->writepos = 0; /* 设置缓冲区非空的条件变量*/ pthread_cond_signal(&b->notempty); pthread_mutex_unlock(&b->lock); } /* 从缓冲区中取出整数*/ int get(struct prodcons *b) { int data; pthread_mutex_lock(&b->lock); /* 等待缓冲区非空*/ if (b->writepos == b->readpos) { pthread_cond_wait(&b->notempty, &b->lock); } /* 读数据,移动读指针*/ data = b->buffer[b->readpos]; b->readpos++; if (b->readpos > = BUFFER_SIZE) b->readpos = 0; /* 设置缓冲区未满的条件变量*/ pthread_cond_signal(&b->notfull); pthread_mutex_unlock(&b->lock); return data; } /* 测试:生产者线程将1 到10000 的整数送入缓冲区,消费者线 程从缓冲区中获取整数,两者都打印信息*/ #define OVER ( - 1) struct prodcons buffer; void *producer(void *data) { int n; for (n = 0; n < 10000; n++) { printf(\"%d --->\\n\ put(&buffer, n); } put(&buffer, OVER); return NULL; } void *consumer(void *data) { int d; while (1) { d = get(&buffer); if (d == OVER) break; printf(\"--->%d \\n\ } return NULL; } int main(void) { pthread_t th_a, th_b; void *retval; init(&buffer); /* 创建生产者和消费者线程*/ pthread_create(&th_a, NULL, producer, 0); pthread_create(&th_b, NULL, consumer, 0); /* 等待两个线程结束*/ pthread_join(th_a, &retval); pthread_join(th_b, &retval); return 0; }

4.小结

本章主要给出了Linux平台下文件、进程控制与通信、线程控制与通信的编程实例。至此,一个完整的,涉及硬件原理、Bootloader、操作系统及文件系统移植、驱动程序开发及应用程序编写的嵌入式Linux系列讲解就全部结束了。

因篇幅问题不能全部显示,请点此查看更多更全内容