1.绪论
1.1嵌入式系统的定义、特征
定义:
- IEEE定义:嵌入式系统是“用于控制、监视或者辅助操作机器和设备的装置”,强调从应用的角度,涵盖机电等附属装置。
- 一般定义:以应用为中心、以计算机技术为基础,具有软件和硬件可裁剪,且在功能、可靠性、成本、体积、功耗等方面有严格要求的专用计算机系统。
- 微机学会定义:嵌入式系统是以嵌入式应用为目的的计算机系统,可分为系统级(如工控器)、板级(带CPU主板)、片级(如单片机)。
特征:
- 系统内核小:适用于小型电子装置,内核比传统操作系统小得多(如OSE内核仅5K)。
- 专用性强:软件和硬件结合紧密,针对硬件进行移植,需要不断修改,强调与任务需求紧密结合。
- 系统精简:没有明显的系统软件和应用软件区分,功能设计简化以降低成本和提升安全性。
- 高实时性:嵌入式软件要求实时性高,需固态存储、代码高质量可靠。
- 开发标准化:需选择RTOS开发平台,调度多任务,保障实时性和可靠性,减少开发时间。
- 开发工具和环境依赖:必须有开发工具(如逻辑分析仪、混合信号示波器)和主机-目标机开发环境结合进行开发。
1.2嵌入式系统的组成
嵌入式系统的组成部分
硬件
嵌入式系统的硬件包括处理器(作为运算核心)、存储器(ROM用于存储代码,RAM用于存储中间结果)、输入输出设备(实现与外界的信息交互),以及其他支持系统功能的硬件组件。
软件
软件部分包含应用程序和操作系统,应用程序负责实现具体的系统功能,可以是单任务或多任务的形式;操作系统负责管理应用软件和硬件资源,提供分时执行和实时性的支持。
BSP/HAL
BSP(板级支持包)和HAL(硬件抽象层)是中间层,HAL抽象硬件操作以提高系统移植性,包含硬件初始化和数据输入输出功能;BSP则连接硬件和操作系统,为驱动程序提供访问硬件的接口,同时负责系统启动和硬件初始化。
系统框架
嵌入式系统由硬件子系统和软件子系统组成,结构中包括处理器、存储器、操作系统、协议栈、应用程序、输入输出接口及外围设备等模块,协同完成特定任务。
1.3嵌入式系统的组成
嵌入式系统的设计流程总结
设计流程
嵌入式系统的设计流程以自顶向下的方式展开,首先进行系统需求分析,明确系统目标;接着进行规格说明,详细描述设计的功能需求;然后进行系统结构设计,划分硬件和软件的功能模块;再进入构件设计阶段,包括程序模块开发、硬件芯片选择及电路设计;最后完成系统集成,将所有构件组装成完整的工作系统。
调试方法
调试是嵌入式系统开发的重要部分,通常在RAM中进行,确保程序在运行前已完善。调试方法包括四种:
- 模拟器方式:在主机上运行调试工具,通过软件模拟处理器指令和逻辑。
- 在线仿真器(ICE)方式:使用仿真器实时运行目标CPU的所有动作,支持硬件中断点设置,但成本较高。
- 监控器方式:通过接口连接主机与目标机,监控程序运行状态,无需额外调试硬件,适用范围广。
- 在线调试器(ICD)方式:通过JTAG接口与目标板通信,直接在目标硬件上调试程序,是目前使用最广泛的方法。
1.4嵌入式系统的发展趋势总结
宏观发展趋势
嵌入式系统的发展强调经济性、小型化、可靠性、高速度和智能性,使设备更加便宜、便携、稳定、快速并具备智能交互能力,同时追求更高的用户体验和价值感。
嵌入式应用开发需求
嵌入式开发依赖强大的开发工具和操作系统支持,面对复杂功能需求,需要更高性能的处理器(如32位、64位RISC芯片和DSP),采用实时多任务编程和交叉开发工具以提升开发效率和软件质量。
网络化趋势
随着互联网技术的成熟,嵌入式设备趋于多功能和联网化,要求具备TCP/IP协议支持及多种通信接口(如USB、CAN、蓝牙、IrDA),同时满足Web或无线Web编程的需求,支持浏览器和组网功能。
精简与低功耗
嵌入式设备朝小尺寸、微功耗和低成本方向发展,通过优化算法和编译器,采用Java编程等技术,提高设备性能的同时减少资源占用。
多媒体人机界面
嵌入式设备的人机交互界面趋向友好和多媒体化,采用GUI为中心的设计,支持手写输入、语音控制、彩色图形处理等功能,提升用户体验,但在智能化交互上仍有较大改进空间。
2.嵌入式硬件系统基础
2.1嵌入式硬件系统的组成
嵌入式硬件系统以嵌入式微处理器为核心,主要由嵌入式微处理器、总线、存储器、输入/输出接口和设备组成。
嵌入式微处理器
嵌入式微处理器采用冯·诺依曼结构或哈佛结构:前者指令和数据共享同一存储空间并通过同一总线访问,而后者将程序和数据分开存储,并使用独立的总线以提升数据吞吐率。指令系统包括RISC(精简指令集)和CISC(复杂指令集)。主流微处理器体系有ARM、MIPS、PowerPC、SH、X86等,时钟速度和总线数据宽度因体系不同而异。
嵌入式系统的总线
嵌入式系统的总线一般集成在微处理器中,可分为片外总线(如PCI、ISA)和片内总线(如AMBA、AVALON)。总线种类与微处理器的结构密切相关。
嵌入式系统的存储器
存储器分为主存和外存。主存用于存储可直接访问的代码和数据,常见类型有ROM、EPROM、Nor Flash、SRAM、DRAM等;外存(如NandFlash、SD卡)用于存储其他信息,容量大、价格低,但处理器无法直接访问,通常采用电子盘而非硬盘。
输入/输出接口和设备
嵌入式微处理器集成了大多数输入/输出接口和设备。接口包括中断控制器、DMA、串行/并行接口等,设备包括定时器、计数器、看门狗、RTC、UART、PWM、AD/DA、显示器、键盘和网络等。
2.2嵌入式处理器的特点
基础特点
嵌入式微处理器以通用微处理器为基础,与之相比具备体积小、重量轻、成本低、功耗低、可靠性高等优势,并增强了工作温度范围和抗电磁干扰能力。
集成度
嵌入式处理器不仅集成了CPU核心、缓存、MMU和总线,还集成了多种外设和接口(如中断控制器、DMA、定时器、串口等),通过高集成度实现低成本和低功耗的设计,可采用单芯片或芯片组形式。
性能分类
性能分为低端(价格低、性能≤50MIPS)、中端(150MIPS以上,低功耗)和高端(用于高强度计算,如VLIW架构和多处理器并行执行);通过提升时钟频率、增加缓存及并行度来满足不同需求。
功耗管理
嵌入式系统严格限制功耗,采用降低工作电压、动态调整时钟频率及关闭未使用功能块等策略,同时提供运行、待命、时钟关闭等功耗管理模式,优化总线和存储器规模以降低能耗。
成本控制
处理器成本受功能块数量、存储器大小、封装形式(如PQFP或BGA)、芯片尺寸等影响,通过权衡性能和集成度控制价格;此外,不同架构(如RISC、CISC、VLIW)的代码密度也对成本有重要影响。
2.3嵌入式处理器的类型
嵌入式处理器可以按照位数和用途进行分类。按位数分为4位、8位、16位、32位和64位;按用途分为嵌入式DSP(用于数字信号处理,采用哈佛结构,优化FFT性能)和通用嵌入式微处理器(如SoC芯片,集成通用处理器、总线、接口及设备)。
主流嵌入式处理器系列
当前主流嵌入式微处理器包括ARM系列、MIPS系列、PowerPC系列、SuperH系列和X86系列,这些系列中产品种类繁多,总计超过上千种。
ARM处理器
ARM处理器作为主流32位RISC处理器,具有低功耗、高性价比和高代码密度的特点,广泛应用于手机、游戏机、手持PC和机顶盒等。其分类包括应用处理器(高性能)、实时控制器(实时响应)和微控制器(低功耗、低成本),并支持多种架构版本如ARMv4、v5、v6及最新的ARM Cortex(v7)。
X86系列
X86嵌入式处理器由AMD、Intel等提供,广泛应用于工业控制和通信领域,特别是国内嵌入式PC应用中表现突出。
PowerPC(PPC)系列
MPC系列(Motorola)和PPC系列(IBM)主要用于通信、消费电子、工业控制和军用设备等,具有高度集成性,如支持以太网控制器、显示控制器和低功耗便携式设备。
嵌入式处理器的技术发展
当前技术趋势包括多处理器技术(如ARM MPcore支持1-4个ARM11处理器的集成)、64位处理器及优化SoC片内总线以扩展带宽。华为Mate60所使用的麒麟9000S芯片是中国在芯片领域自主研发的重要突破。
2.3嵌入式处理器的类型
按位数分类
嵌入式处理器按位数划分为4位、8位、16位、32位和64位。其中32位处理器已成为市场主流,占嵌入式市场总量的重要份额。
按用途分类
嵌入式处理器可分为嵌入式DSP和通用嵌入式微处理器。嵌入式DSP专注于数字信号处理,采用哈佛架构,程序与数据分开存储,优化了如FFT的处理速度;通用嵌入式处理器通常为集成处理器核心、总线和外围接口的SoC芯片,并可内嵌DSP协处理器。
主流嵌入式处理器系列
当前市场主流的嵌入式处理器系列包括ARM、MIPS、PowerPC、SuperH和X86系列。其中,ARM和MIPS广泛应用于消费电子,PowerPC多用于工业控制和军用设备,而X86系列因兼容性广泛用于嵌入式PC。
ARM处理器的特点与分类
ARM处理器因功耗低、性价比高和代码密度高成为业界公认的标准。其产品包括ARM7、ARM9、ARM10、ARM11和ARM Cortex系列,并根据应用场景分为应用处理器(性能优先)、实时控制处理器(低功耗实时响应)和微控制器(成本最低)。
ARM架构发展
ARM架构从ARMv4到ARMv7逐步演进:
- ARMv4引入Thumb指令集,提升代码密度和功耗效率;
- ARMv5TE优化Thumb与ARM指令交互,扩展DSP指令集;
- ARMv6新增SIMD扩展、多处理器支持,并优化内存和异常处理;
- ARMv7定义应用(A)、实时(R)和微控制器(M)三种处理器配置,提升多媒体处理能力和浮点性能。
华为自研嵌入式处理器
华为Mate60搭载的麒麟9000S芯片采用自主设计的“泰山”内核,标志中国芯片技术突破。该芯片采用国产制程工艺,实现对美国技术的部分脱离,同时通过12核心设计(包含定制A78AE核心)达到3.1GHz的高主频。
未来发展趋势
嵌入式处理器正在向多核架构和64位技术演进。多核技术通过任务并行分解提升性能,而64位技术在嵌入式SoC中实现片内总线的高效扩展,解决系统性能与带宽瓶颈。ARM的AMBA架构已支持8到1024位总线宽度,推动嵌入式性能持续提升。
系统性能瓶颈及64位需求
系统性能瓶颈可能体现在处理器的计算能力、内存访问带宽以及片上总线的数据吞吐能力上。对于嵌入式系统,是否需要64位技术,主要取决于应用场景中对64位地址(如更大内存寻址能力)还是64位数据(如更高数据精度或处理能力)的需求。例如,高性能计算或多媒体处理可能更需要64位数据,而复杂操作系统和大内存应用更依赖64位地址支持。
2.4嵌入式处理器的体系结构
嵌入式处理器的体系结构包含多层次的设计内容,包括其编程模型、处理器的运行模式、工作状态、寄存器体系、异常处理机制以及内存与 I/O 操作的设计和实现方式。以下为详细解析:
编程模型
嵌入式处理器支持多种数据类型,包括字节型(Byte,8位宽)、半字型(HalfWord,16位宽,要求2字节对齐)和字型(Word,32位宽,要求4字节对齐)。处理器提供灵活的操作模式,包括用户模式(User Mode)和特权模式(Privilege Mode),分别适用于普通任务和系统任务。在用户模式下,程序无法直接访问某些受保护的资源,而必须通过异常机制改变处理器运行模式。在特权模式下,处理器可以完全访问所有系统资源。此外,模式的切换可以通过三种方式实现:软件控制、异常触发或外部中断。
处理器模式
处理器支持 7种模式,分别适用于不同场景:
- 用户模式(User Mode):用于运行普通用户程序,不允许直接访问受保护的资源。
- 系统模式(System Mode):与用户模式类似,但具有访问所有资源的权限,用于运行系统特权任务。
- 快速中断模式(FIQ Mode):用于处理快速数据传输的中断请求。
- 中断请求模式(IRQ Mode):用于处理通用的外部中断。
- 管理模式(Supervisor Mode):系统复位或异常处理时进入的模式,主要用于操作系统管理。
- 终止模式(Abort Mode):处理内存访问异常或非法操作。
- 未定义模式(Undefined Mode):处理未定义指令执行的情况,通常用于扩展指令集(例如通过软件仿真扩展ARM或Thumb指令)。
模式切换时,处理器会自动保存当前状态到影子寄存器中,以便在异常或中断处理完成后能快速恢复。
处理器工作状态
处理器有两种主要工作状态:
- ARM状态:执行32位ARM指令,要求字对齐,提供高性能。
- Thumb状态:执行16位Thumb指令,要求半字对齐,指令长度短,代码密度高。
此外,ARM还支持 Thumb-2扩展,引入了新的32位指令集,可以在Thumb状态下执行,这种混合模式能够更好地平衡性能与代码密度。ARM与Thumb状态之间的切换通过执行 BX指令 实现,同时设置操作数寄存器的状态位[0](0为ARM,1为Thumb)。状态切换不会影响处理器的模式或寄存器内容。
寄存器
ARM处理器的寄存器体系结构由 37个寄存器 组成:
-
31个通用寄存器:
- R0-R7:所有模式下共享的通用寄存器,功能相同,主要用于数据存储和运算。
- R8-R12:根据模式不同有两组物理寄存器,一组用于FIQ模式,另一组用于其他模式。
- R13(SP):栈指针,负责存储不同模式下的堆栈地址,切换模式时自动切换到对应栈指针值。
- R14(LR):链接寄存器,用于保存函数调用的返回地址或异常返回地址。
- R15(PC):程序计数器,保存当前指令地址,ARM状态下低2位为0,Thumb状态下低1位为0。
-
6个状态寄存器:
- CPSR(当前程序状态寄存器):记录当前模式、条件标志、控制标志(如中断使能标志)和处理器状态。
- SPSR(保存的程序状态寄存器):在异常模式下保存CPSR的值,用于异常返回时恢复处理器状态。
影子寄存器是寄存器的物理映射,R8-R14在不同模式下具有不同的影子寄存器,用于快速切换。
异常处理机制
异常是由内部或外部事件触发的特殊情况,处理器在异常发生时会暂停当前指令,并跳转到预定义的向量地址执行异常处理程序。异常的类型和优先级如下:
- Reset(复位异常):系统复位时触发,进入Supervisor模式,禁用FIQ和IRQ中断。
- Undefined Instructions(未定义指令异常):执行未定义指令时触发,可用于扩展指令集。
- SWI(软件中断):执行SWI指令时触发,通常用于操作系统的系统调用。
- Prefetch Abort(预取中止):在指令预取阶段发生内存错误时触发。
- Data Abort(数据中止):在数据读写阶段发生错误时触发。
- IRQ(中断请求异常):外部IRQ信号触发,用于常规外设中断。
- FIQ(快速中断异常):外部FIQ信号触发,用于高优先级数据传输中断。
异常发生时,处理器会保存当前状态到影子寄存器(R14和SPSR),处理完成后通过恢复寄存器值返回正常执行流。
内存与I/O
ARM处理器使用线性地址空间,地址范围为4GB(2^32)。支持 大端(Big-endian) 和 小端(Little-endian) 数据存储模式,通过硬件设置实现。
-
I/O接口
- I/O端口地址采用 内存映射编址,即I/O设备与内存单元共享统一的地址空间。
- ARM处理器访问I/O端口的方式与访问内存相同,但I/O地址空间通常标记为不可缓存(Uncachable)和非缓冲(Unbufferable)。
-
I/O数据与控制
- 数据信息:如键盘输入和显示器输出。
- 状态信息:外设当前工作状态信号(如READY信号和BUSY信号)。
- 控制信息:CPU发送的控制信号(如读写控制信号、中断信号、片选信号等)。
2.5嵌入式系统的总线
嵌入式系统的总线是一种关键的通信机制,用于连接CPU、存储器和外围设备,实现数据、地址和控制信号的传输。根据位置划分,总线分为 片内总线 和 片外总线。片内总线连接CPU内部的功能单元,如ALU(算术逻辑单元)、寄存器和缓存;片外总线则连接CPU与存储器(RAM、ROM)以及I/O接口。按照功能和信号类型划分,总线分为 数据总线(Dbus)、地址总线(Abus) 和 控制总线(Cbus),分别负责数据传输、地址定位和控制信号的传递。
总线的主要参数
嵌入式总线的性能受以下三个关键参数影响:
- 总线宽度:又称总线位宽,表示总线在一次操作中能传送的数据位数,例如16位总线可以传输16位数据。总线宽度越大,数据传输能力越强。
- 总线频率:表示总线的工作速度,单位为MHz。频率越高,总线传输速度越快。
- 总线带宽:即总线的最大数据传输率,用每秒传输的数据量(MB/s)衡量。公式为:总线带宽 =(总线宽度 / 8)× 总线频率。例如,32位总线在66MHz下的带宽为264MB/s。
总线的性能不仅取决于上述参数,还受电路设计和硬件成本影响。例如,高速总线通常使用更宽的数据连接和更昂贵的电路,但通过桥电路(Bus Bridge),可以在高速总线与低速总线之间提供并行性和独立操作。
多总线系统
现代嵌入式系统常采用多总线结构,将高速设备(如存储器、处理器)连接在高速总线中,而将低速设备(如外设)连接在低速总线中,通过桥(Bridge)将高速总线与低速总线进行互联。桥在多总线系统中发挥重要作用,允许不同速率的设备高效协同。例如,在一个典型的多总线系统中,CPU、缓存和存储器控制器通过高速总线(如AMBA AHB)连接,外围设备(如UART、SPI)则通过低速总线(如APB)与系统通信。桥的存在提高了总线之间的并行性,并简化了总线操作。
AMBA总线架构
AMBA(Advanced Microcontroller Bus Architecture) 是ARM公司设计的一种嵌入式总线规范,目前已发展到3.0版本。AMBA总线包括三种主要总线类型:
-
AHB(Advanced High-performance Bus):用于高性能模块的连接,支持突发模式数据传输、事务分割以及流水线操作。AHB可以高效连接处理器、片上和片外存储器,支持一个或多个主单元(如RISC处理器、协处理器和DMA控制器)发起总线操作。从单元(如外存接口和总线桥接口)响应主单元的读写操作,并提供成功、失败或等待的反馈信号。
AHB操作分为两个阶段:- 地址阶段:持续一个时钟周期,在HCLK上升沿有效,所有从单元在此时采样地址信息。
- 数据阶段:持续一个或多个时钟周期,通过HREADY信号控制数据传输的延续或完成。
-
ASB(Advanced System Bus):类似AHB,适用于高性能模块,但目前逐渐被AHB取代。
-
APB(Advanced Peripheral Bus):用于低性能外设的连接,通常作为AHB或ASB的子系统。APB通过APB桥连接到AHB/ASB,并实现以下功能:
- 锁存地址直到数据传输完成。
- 地址译码和外部片选信号生成。
- 写操作时将数据驱动到APB总线,读操作时将数据驱动回AHB/ASB。
- 通过PENABLE信号控制传输的时序。
APB从单元具有简单的接口,依赖特定设计实现外设的控制和数据传输。其操作信号(如PSELx和PADDR)决定目标寄存器的访问。
PCI与CPCI总线
PCI(Peripheral Component Interconnect)总线 是一种高性能32位或64位总线,具有多路复用地址和数据总线,并支持即插即用和中断共享。PCI总线主要参数如下:
- 数据宽度:32位或64位。
- 总线速度:33MHz或66MHz。
- 数据传输方式:由一个主控设备(Master)发起操作,目标设备(Slave)响应请求。同一时刻总线只支持一对设备进行传输,通过仲裁机制分配主控权。
为了适应工业需求,CPCI(Compact PCI)总线将PCI总线规范扩展为工业标准。CPCI结合了PCI总线的高性能和欧洲卡结构的高可靠性,广泛应用于高端嵌入式系统和工业控制领域。CPCI定义了两种板卡尺寸(3U: 100mm×160mm 和 6U: 233mm×160mm),适合在高可靠性要求的环境下使用。
串行总线
嵌入式系统中常用的串行总线包括:
- I2C(Inter-Integrated Circuit):用于短距离、低速的设备通信,具有简单的双线结构(SDA数据线和SCL时钟线)。
- SPI(Serial Peripheral Interface):支持全双工、高速数据传输,通过主从架构实现多设备通信。
- USB(Universal Serial Bus):支持高速通信和多设备连接,是现代嵌入式设备的常用接口。
这些串行总线通过减少引脚数量和简化硬件设计,提升了系统的模块化和扩展能力。
2.6嵌入式系统的存储器
嵌入式系统的存储器是系统核心组成部分,负责存储程序、数据及配置信息。根据存储器的访问方式和功能,可分为 高速缓存(Cache)、主存(片内或片外存储器) 和 外存(如Flash存储和各种存储卡)。不同存储器在速度、容量、可靠性和成本上各有特点,在嵌入式系统中常组合使用,以实现高效运行和数据存储需求。
高速缓存(Cache)
Cache 是嵌入式处理器内部的高速存储器,用于加速主存访问,存放最频繁使用的数据和指令,是主存中部分内容的副本。Cache 以其速度高于主存的特点显著提升了处理器性能,常见于32位嵌入式微处理器中。
Cache 的类型包括数据 Cache、指令 Cache 或两者结合的混合 Cache。处理器访问时,Cache 控制器检查目标地址的数据是否在 Cache 中,若存在则称为 “命中”,否则为“未命中”。未命中时需从主存读取数据并存入 Cache 后供处理器使用。写入数据时的策略有两种:
- 通写(Write Through):写入数据时,Cache 和主存同时更新,保证主存与 Cache 同步。
- 回写(Write Back):仅在 Cache 数据被替换或移出时才更新主存,这种方式能减少主存访问次数,提高性能。
主存(Primary Storage)
主存是处理器直接访问的存储器,主要用于存放操作系统、应用程序以及运行时数据。嵌入式系统中主存可位于 片内存储器(SoC内部) 或 片外存储器(SoC外部)。片内存储器具有存取速度快的特点,但容量较小;片外存储器容量大,但访问速度较慢。
主存类型:
-
SRAM(静态随机存取存储器):
- 特性:SRAM 不需要刷新数据,存储单元由六管电路组成,具有低延迟、高速度的特点。
- 缺点:容量小、成本高,通常用于高速缓存(Cache)或小型片内存储。
- 典型芯片规格:2114(1Kx4)、6116(2Kx8)、62256(32Kx8)等。
-
DRAM(动态随机存取存储器):
- 特性:采用动态存储单元设计(单管或多管电路),需要定期刷新以保持数据完整性。
- 优点:相比 SRAM,DRAM 单位容量成本更低,可用于较大规模存储。
- 工作方式:采用行列地址选通机制,地址被内部分为两路以减少引脚数,需配备 DRAM 控制器处理刷新和多路切换。
-
SDRAM(同步动态随机存取存储器):
- 特性:与处理器共享时钟信号,能够同步工作。其内置双存储阵列,支持交替存储和数据读取,显著提升数据读取效率。
- 优点:是 DRAM 家族中速度最快的一种,广泛应用于高性能嵌入式系统中。
主存中也包括一些 ROM 类存储器(如 Nor Flash、EPROM、E2PROM 等),用于存储不常更改的系统固件或代码。
外存(Secondary Storage)
外存是处理器无法直接访问的存储器,通常通过控制器接口进行操作,用于存放用户数据、配置文件和其他持久性信息。外存容量大,但访问速度较慢。在嵌入式系统中,外存多以 电子盘(使用半导体芯片存储数据) 的形式实现,具有体积小、功耗低和抗震能力强等特点。
外存类型:
-
Nand Flash:
- 特性:作为 Flash Memory 的一种,Nand Flash 具有大容量、低成本、高回写速度等优点,是外存的主流选择。
- 用途:可独立作为外存使用,也可组成各种存储卡(如 USB 盘、SD 卡、CF 卡)。
- 与 Nor Flash 的区别:Nand Flash 注重大容量存储和成本优化;而 Nor Flash 随机访问速度快,功耗低,多用于主存。
-
NOR Flash:
- 特性:具有快速随机访问、电压低、功耗低的优点,稳定性较高,适合固件和操作系统存储。
- 特性:具有快速随机访问、电压低、功耗低的优点,稳定性较高,适合固件和操作系统存储。
-
MMC(多媒体卡):
- 特性:支持高频率(26MHz和52MHz),数据总线宽度灵活(1到8位),传输速率可达52MB/s。
- 应用场景:适用于移动设备和便携式电子设备,提供低功耗和小尺寸解决方案。
-
SD卡:
- 特性:由松下、东芝和 SanDisk 联合推出,是一种标准化的存储卡,提供不同尺寸(标准、mini、micro)和容量。
- 应用场景:被广泛用于移动存储、智能手机和数码相机中。
-
Compact Flash(CF 卡):
- 特性:最早由 SanDisk 提出,体积小,仅为 PCMCIA 卡的四分之一,但提供完整的 PCMCIA-ATA 功能。
- 接口:CF 卡接口为 50 针,遵循 ATA 协议。
- 应用场景:适用于高可靠性要求的嵌入式设备。
-
Disk On Chip(DOC):
- 特性:采用 Nand Flash 芯片作为存储单元,并结合控制芯片和 TrueFFS 技术仿真硬盘。TrueFFS 技术提高了写入次数和数据可靠性,使 DOC 的寿命远高于普通 Flash 存储。
- 应用场景:适用于高写入寿命和高可靠性的嵌入式系统。
3.嵌入式软件系统基础
3.1嵌入式软件系统概述
嵌入式软件是嵌入式系统的核心组成部分,与硬件密切配合以实现特定功能。它包括程序、数据和文档三部分:程序是指按照设计功能执行的指令序列,数据是程序操作的信息,文档是关于开发、维护和使用的软件说明资料。嵌入式软件具有抽象性、复杂性和环境依赖性等特点,与传统软件相比更具定制化需求和硬件约束。
软件的特点与挑战
嵌入式软件具有以下关键特性和挑战:
- 逻辑实体和抽象性:软件是逻辑概念,无形态体现,需通过分析和观察了解其功能与性能。
- 无需制造过程:软件开发完成后可复制,开发质量决定其生命周期的稳定性。
- 修改与退化:软件本身不会老化,但因硬件升级和需求变化需要修改,而修改可能引入错误导致退化。
- 环境依赖性:嵌入式软件依赖具体硬件和运行环境,移植时常需重新设计和优化。
- 复杂性和成本:嵌入式软件涉及复杂的工程设计,需整合多学科知识,开发成本高且开发效率低。
- 社会因素:软件开发涉及管理体制、团队协作和社会认知等因素,这些都会直接影响软件开发的成败。
嵌入式软件的分类
嵌入式软件根据功能和运行场景可以分为以下几类:
-
按功能分类:
- 系统软件:用于控制和管理硬件资源,如嵌入式操作系统和中间件。
- 支撑软件:辅助软件开发的工具,包括系统分析、仿真、测试、配置管理和维护工具。
- 应用软件:针对具体领域设计,如手机操作系统、路由器控制程序、飞控软件等。
-
按运行平台分类:
- 运行在开发平台上的软件:如开发、测试和调试工具。
- 运行在嵌入式系统上的软件:包括操作系统、驱动程序、应用程序和部分调试工具。
嵌入式软件的体系结构
嵌入式软件的体系结构通常分为四层:驱动层、操作系统层、中间件层 和 应用层。
-
驱动层:
- 驱动层直接与硬件交互,为操作系统和应用层提供支持。
- 板级初始化程序:在系统上电后初始化硬件,包括CPU、存储器、中断控制器、DMA和定时器等,确保硬件正常工作。
- 系统软件相关驱动:为操作系统和中间件提供支持,通常由操作系统厂商提供,无需用户修改。
- 应用软件相关驱动:为应用程序提供支持,设计和实现依据应用需求,不一定与操作系统相关联。
-
操作系统层:
- 包括嵌入式内核、TCP/IP网络协议栈、文件系统、GUI系统、电源管理模块等。
- 嵌入式内核是系统的基础,其他模块根据实际需求选择性加入,如嵌入式GUI系统用于图形化界面操作。
-
中间件层:
- 提供通用的软件功能以简化开发过程。
- 包括嵌入式CORBA、Java、DCOM等中间件,可用于特定领域,如软件无线电台的应用中间件SCA(Software Core Architecture)。
-
应用层:
- 由多个独立的应用任务组成,每个任务完成特定功能(如I/O操作、计算或通信任务)。
- 通过操作系统调度任务,保证多任务系统的高效运行。
嵌入式软件运行流程
嵌入式软件运行流程通常包括五个阶段:上电复位、系统引导/升级、系统初始化、应用初始化、多任务应用运行。具体如下:
-
上电复位和板级初始化阶段:
- 系统上电后,进行板级初始化,完成CPU堆栈指针、中断控制器、存储器等硬件模块的初始化。
- 初始化程序通常使用汇编语言编写,不同系统的初始化内容有所不同,但基本工作一致。
-
系统引导/升级阶段:
- 引导阶段将系统软件加载到RAM运行,可通过NOR Flash或外存(如Nand Flash、CF卡)加载,前者适合快速启动,后者成本更低。
- 升级阶段支持远程升级(通过TFTP、FTP、HTTP协议)和本地升级(通过串口和超级终端工具)。
-
系统初始化阶段:
- 初始化操作系统和系统功能模块,包括数据空间、接口设备、内核、网络协议栈、文件系统和中间件。
- 初始化按顺序进行,首先加载内核,然后是外围设备支持模块,最后是高级中间件。
-
应用初始化阶段:
- 创建应用任务及其所需的资源(如信号量、消息队列等),为应用任务的正常运行做好准备。
-
多任务应用运行阶段:
- 各种初始化工作完成后,操作系统进入多任务运行状态,调度任务执行,完成I/O、通信和计算等功能。
3.2嵌入式操作系统
嵌入式操作系统是嵌入式系统中的核心软件,既具备一般操作系统的基本功能,又有针对嵌入式设备的独特特点,如可固化、可配置、可裁剪等。它支持多种微处理器架构,提供独立的板级支持包(BSP),并依赖集成的交叉开发工具链进行开发与调试。随着嵌入式设备领域的拓展,嵌入式操作系统的功能和应用领域不断细分,从基础的内核发展到支持复杂功能模块,如文件系统、网络协议栈和图形界面。
嵌入式操作系统的特点
- 可固化:操作系统可以固化到设备的存储介质中(如Flash),保证系统稳定。
- 可配置、可裁剪:可根据具体嵌入式应用的需求裁剪功能模块,减少内存占用,优化性能。
- 硬件相关性:通过板级支持包(BSP)适配不同硬件平台,实现硬件抽象。
- 开发依赖交叉工具链:在主机平台上设计、开发、编译嵌入式软件,通过交叉编译工具链生成目标代码。
嵌入式操作系统的发展历程
-
无操作系统阶段:
- 早期嵌入式系统基于单片机,采用汇编语言直接控制硬件。
- 设备运行简单单线程程序,如工业监测、设备控制等,处理效率低,几乎无用户接口。
- 特点:系统结构单一,硬件资源受限,存储容量小,适用于简单工业应用。
-
简单操作系统阶段:
- 20世纪80年代,嵌入式微控制器(如PowerPC)集成了RAM、ROM、I/O接口等部件。
- 简单的嵌入式操作系统开始流行,具备初步的内核功能,如任务控制和程序监控。
- 特点:操作系统简单但高效,兼容性和扩展性初步显现,大幅缩短开发周期。
-
实时操作系统阶段(RTOS):
- 90年代,嵌入式系统需求从分布式控制、通信设备扩展到信息家电等领域。
- 出现了实时多任务操作系统(如VxWorks),支持模块化、多任务、文件系统、网络通信和图形界面(GUI)。
- 特点:强实时性、多任务支持、广泛适配多种微处理器架构。
-
面向互联网阶段:
- 21世纪,嵌入式设备逐步与互联网结合,强调网络化、信息化。
- 操作系统支持TCP/IP协议、无线网络、低功耗设计和多媒体人机交互界面。
- 特点:网络互联成为趋势,优化功耗和资源管理,支持复杂应用。
嵌入式操作系统的分类
-
按应用领域分类:
- 信息家电:如Embedded Linux、WinCE,适用于智能家居、电视等设备。
- 智能手机:如Symbian OS、Palm OS,支持移动通信和多媒体功能。
- 汽车电子:如汽车实时操作系统,专注于车载控制、导航等。
- 工业控制:如VxWorks、QNX,适用于高精度、实时性要求高的工业场景。
-
按实时性分类:
- 实时操作系统(RTOS):如VxWorks、QNX,具备严格实时性,常用于军事、通信等领域。
- 非实时操作系统:如嵌入式Linux,具备弱实时性,多用于信息家电等领域。
-
按商业模式分类:
- 商用型:如VxWorks、WinCE,功能稳定,提供全面技术支持,但需支付开发费用和版税。
- 开源型:如Embedded Linux、RTEMS,开放源码,无版税,仅需支付服务费用,便于二次开发。
嵌入式操作系统的体系结构
嵌入式操作系统的体系结构决定了硬件与软件的接口设计、内核与组件的组织关系、系统与应用的接口。常见体系结构包括:
-
单块结构:
- 所有操作系统功能集成在一个大内核中,紧耦合设计,效率高但灵活性较差。
- 常用于小型嵌入式设备。
-
层次结构:
- 分为硬件无关层、硬件抽象层和硬件相关层,各层之间通过模块化设计实现功能分离。
- 特点:便于移植和扩展,硬件适配集中在硬件相关层。
-
微内核结构:
- 仅将最基本的功能(如任务管理、内存管理、中断处理)置于微内核中,其余功能以服务形式运行在用户态。
- 特点:可扩展性强,支持分布式系统,但因消息传递效率问题性能稍逊。
现代嵌入式操作系统大多结合层次化和模块化设计或微内核结构,以实现高效性能和灵活扩展。
嵌入式操作系统的核心功能
-
任务管理:
- 任务调度、创建、删除、挂起、优先级设置等。
- 使用静态优先级抢占式调度算法,优先级高的任务优先执行。
-
内存管理:
- 通常采用静态内存分配和动态内存分配相结合的方式,无虚拟内存管理。
- 支持简单内存保护功能,防止内存越界。
-
通信与同步:
- 提供信号量、消息队列、事件、管道、共享内存等通信方式。
- 解决优先级反转问题,保障多任务并发运行的稳定性。
-
中断管理:
- 支持中断服务程序安装、现场保存与恢复、中断栈切换和中断调度。
-
时间管理:
- 提供高精度系统时钟,支持任务超时、时间片轮转调度等功能。
-
文件系统:
- 相比通用操作系统文件系统功能较简单,主要支持文件存储、检索、更新等。
- 相比通用操作系统文件系统功能较简单,主要支持文件存储、检索、更新等。
嵌入式操作系统实例
-
μC/OS-II:
- 实时多任务操作系统,支持多种处理器架构。
- 具备可移植性、可裁剪性,广泛应用于医疗、工业控制等领域。
-
Embedded Linux:
- 开源系统,支持多任务和多架构,适合从简单到复杂的嵌入式应用。
- 资源丰富,性价比高,常用于信息家电领域。
-
VxWorks:
- 强实时性和可靠性,广泛应用于通信、军事、航空航天等领域。
-
WinCE:
- 适用于多媒体网络设备,提供完整通信协议和多媒体功能。
-
KylinOS(银河麒麟):
- 国产嵌入式操作系统,支持多种国产硬件平台,适用于自主研发领域。
3.3启动代码分析
3.4中断系统
中断系统是嵌入式系统中用于处理硬件事件和异常的重要机制。在CPU运行过程中,当硬件引发特定异常时,系统通过中断响应来处理这些情况。中断可视为异常的一种,由外部硬件触发。异常则是CPU在执行指令时检测到的非法情况,如非法操作码或访问越界等。嵌入式系统中,硬件中断和异常通常以类似方式处理,但其来源和触发方式不同。
ZYNQ的中断系统是嵌入式中断管理的典型案例,提供对多种硬件和软件中断的支持。ZYNQ采用分层中断处理架构,支持多个优先级的中断请求,并结合嵌入式操作系统以实现高效的中断处理流程,从而满足复杂嵌入式应用对实时性和可靠性的要求。
4.Linux内核与驱动程序
4.1设备驱动程序简介
设备驱动程序(Device Driver,简称DD)是Linux内核中一部分,用于连接硬件设备和系统应用程序的标准化接口。其核心作用是将用户通过标准系统调用(如open
、read
、write
等)发起的操作映射到硬件设备的实际操作上,屏蔽硬件工作细节。通过设备驱动程序,用户可以在无需了解设备底层操作的情况下完成硬件功能的使用。
设备驱动程序的角色和作用
设备驱动程序是内核的关键部分,属于设备管理子系统,起到应用程序和硬件设备间的桥梁作用。其核心职责是提供对硬件的基本操作机制,同时将操作策略留给上层应用程序,以实现分层设计的思想。例如,驱动程序处理设备的打开(open
)、读写(read/write
)、关闭(close
)、控制操作(ioctl
)等,而如何具体使用这些操作由应用程序决定。
设备驱动程序可以直接编译进内核(静态编译方式,如zImage),也可以作为动态模块(.ko
文件)加载到正在运行的内核中。动态加载的方式(通过insmod
命令)便于开发调试,而静态编译适用于最终部署。
用户态与内核态
设备驱动程序运行在内核态,而用户程序运行在用户态。用户态权限较低,只能运行受限的应用程序,访问虚拟地址空间,所有资源访问受内核监管;内核态拥有最高权限,可控制内存分配、访问硬件设备、处理器寄存器和中断等。
驱动程序通过内核函数(如get_user
、put_user
、copy_from_user
、copy_to_user
)在用户态和内核态之间传递数据,使用户应用程序能够间接访问底层硬件。
设备文件的抽象
Linux中所有硬件设备都抽象为设备文件,位于/dev
目录下。设备文件提供了统一的接口,用户可以通过标准文件操作(如open
、read
、write
等)访问硬件设备。设备驱动程序负责将这些文件操作映射为硬件的实际操作。
每个设备文件通过主设备号和次设备号标识:
- 主设备号:标识设备类型和对应的驱动程序。
- 次设备号:区分同一主设备号下的多个设备实例,由驱动程序内部使用。
驱动程序与设备文件通过mknod
命令关联,动态加载时可通过insmod
安装。
设备驱动程序的主要类型
-
字符设备
字符设备以字节流方式操作,不支持随机访问,如串口设备。这类设备的驱动通常包括open
、read
、write
、close
等系统调用的实现,逐字节操作硬件,缓存可有可无。 -
块设备
块设备支持随机访问和大块数据传输,通常用于存储设备(如磁盘、闪存)。其驱动通过缓冲区(Buffer)和缓存(Cache)优化性能,可同时支持字节流访问和大块传输操作,适合存储文件系统。 -
网络设备
网络设备处理网络数据包的发送和接收,常用于物理网卡和虚拟网络接口(如回环接口)。这类设备通过内核网络子系统管理,数据传输往往基于数据包而非字节流,因此很难映射为文件系统节点。
设备驱动程序的关键特性
-
核心代码与系统安全
驱动程序是系统核心的一部分,错误的代码可能导致内核崩溃或数据丢失。编写设备驱动需要高度谨慎。 -
标准化接口
驱动程序必须为Linux内核及其子系统提供标准接口,支持标准操作调用(如open
、read
、write
)。 -
动态加载与资源管理
多数Linux设备驱动可以动态加载或卸载,使内核资源利用更高效。闲置时,模块可从内核卸载以释放资源。 -
可配置性
设备驱动可以根据用户需求进行定制,选择合适的设备功能。
驱动程序的功能与机制
设备驱动程序的任务是提供硬件操作的基本机制,而不包含策略。驱动程序专注于解决如何使硬件可用的问题,而如何使用硬件留给上层应用。例如,串口驱动程序仅负责数据的收发,但具体的通信协议则由应用程序实现。
通过分层设计,驱动程序需在多方面平衡,如用户操作灵活性、开发时间和代码复杂度。
动态加载模块与内核扩展
Linux设备驱动程序可作为模块动态加载(insmod
)或卸载(rmmod
)。动态模块可以扩展Linux内核的运行时功能,不仅用于设备驱动,还适用于文件系统、网络协议等其他功能。静态编译入内核的驱动程序适合稳定环境,但动态加载方式更灵活。
内核态与用户态的数据交互
驱动程序通过虚拟地址和物理地址映射访问设备。例如,内核通过/dev/mem
访问设备物理内存地址,通过/dev/port
访问IO端口地址。同时,驱动程序利用虚拟地址映射(如mmap
)使用户程序能够高效访问设备。
设备驱动的重要性
设备驱动程序是嵌入式系统和Linux系统中连接硬件和操作系统的核心桥梁,其设计直接影响系统性能、安全性和功能扩展能力。精心编写的驱动程序不仅能高效利用硬件资源,还能提升应用程序的开发效率和硬件设备的可用性。
4.2 设备驱动程序结构
设备驱动程序是嵌入式系统中连接硬件设备和操作系统内核的关键模块,主要用于管理硬件资源、与用户程序交互、实现硬件抽象。通过标准接口,驱动程序屏蔽了硬件实现的细节,使应用程序能够通过统一的方法访问不同的硬件设备。以下是设备驱动程序的结构和实现细节。
设备驱动程序的主要任务
设备驱动程序的核心任务包括:
- 设备初始化:
- 在驱动加载时完成硬件设备的初始化工作,如分配资源、设置寄存器和初始化缓存。
- 根据设备类型(字符设备、块设备或网络设备)注册驱动程序,分配主设备号和次设备号。
- 硬件操作与管理:
- 提供对硬件设备的直接访问和操作能力,如读写寄存器、控制外设、处理中断等。
- 数据传输:
- 在内核空间和用户空间之间传递数据(如从设备读取数据传递到用户空间,或从用户空间写数据到设备)。
- 资源释放:
- 驱动卸载时清理资源(如释放内存、中断等),确保系统稳定性。
驱动程序的基本概念与核心结构
设备驱动程序的核心在于实现统一的操作接口,使内核和应用程序能够以标准方式与硬件交互。驱动程序通过以下两大核心数据结构实现这一目标:
-
struct file
:- 位于内核头文件
include/fs.h
中,表示一个打开的文件,记录设备的使用状态。 - 常用成员:
f_mode
:文件的读写权限标识。f_pos
:当前读写位置。f_op
:指向文件操作结构file_operations
的指针,关联驱动程序的实现函数。private_data
:指向驱动程序分配的设备私有数据。
- 位于内核头文件
-
struct file_operations
:- 位于
include/linux/fs.h
中,定义设备操作接口,指向实现设备功能的函数指针。 - 常见成员:
open
:打开设备。release
:释放设备。read
:从设备读取数据。write
:向设备写入数据。ioctl
:执行设备控制命令。poll
:实现非阻塞I/O,查询设备状态。llseek
:修改文件读写位置。
- 示例:
static struct file_operations spioc_fops = {.read = spioc_read,.write = spioc_write,.ioctl = spioc_ioctl,.open = spioc_open,.release = spioc_close, };
- 位于
设备驱动程序的实现过程
-
驱动程序初始化:
- 使用
module_init()
宏注册初始化函数。 - 在初始化函数中分配设备号(主设备号和次设备号),注册字符设备,分配必要的内存或硬件资源。
- 示例:
static int __init spioc_init(void) {int result;// 分配缓存区wbuff = kmalloc(64, GFP_KERNEL);rbuff = kmalloc(64, GFP_KERNEL);// 注册字符设备result = register_chrdev(major, "spioc", &spioc_fops);if (result < 0) {printk(KERN_ERR "Cannot register spioc dev.");return -EIO;}return 0; }
- 使用
-
驱动程序卸载:
- 使用
module_exit()
宏注册退出函数。 - 释放设备占用的资源(如内存、寄存器、中断等),注销字符设备。
- 示例:
static void __exit spioc_exit(void) {unregister_chrdev(major, "spioc");kfree(wbuff);kfree(rbuff); }
- 使用
-
设备操作函数的实现:
-
open()
:在设备文件被打开时调用,初始化设备资源(如清空缓存、配置寄存器)。static int spioc_open(struct inode *inode, struct file *filp) {memset(wbuff, 0, 64);memset(rbuff, 0, 64);return 0; }
-
read()
和write()
:- 从设备读取数据或向设备写入数据,通常通过
copy_to_user()
和copy_from_user()
函数实现用户空间和内核空间的数据传输。
static ssize_t spioc_read(struct file *filp, char *buf, size_t cnt, loff_t *off) {if (cnt > count) cnt = count;copy_to_user(buf, rbuff, cnt);return cnt; } static ssize_t spioc_write(struct file *filp, const char *buf, size_t cnt, loff_t *off) {if (cnt > 64) cnt = 64;copy_from_user(wbuff, buf, cnt);return cnt; }
- 从设备读取数据或向设备写入数据,通常通过
-
ioctl()
:处理特殊命令,用于控制设备功能。static int spioc_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg) {switch (cmd) {// 实现不同命令的逻辑}return 0; }
-
release()
:释放设备资源。static int spioc_close(struct inode *inode, struct file *filp) {return 0; }
-
驱动程序的加载与使用
-
动态加载驱动:
- 使用
insmod
命令加载驱动程序模块(如spioc.ko
),调用module_init()
完成初始化。 - 使用
rmmod
卸载驱动程序,调用module_exit()
释放资源。 - 示例命令:
$ insmod spioc.ko $ ls /proc/devices $ mknod /dev/spioc c <主设备号> <次设备号> $ rmmod spioc
- 使用
-
静态编译到内核:
- 将驱动程序代码添加到内核源码中,修改对应
Makefile
和Config.in
文件。 - 编译生成新的内核镜像,驱动随内核启动自动加载。
- 将驱动程序代码添加到内核源码中,修改对应
-
设备文件的创建:
- 使用
mknod
命令创建设备文件,并关联主设备号和次设备号。$ mknod /dev/spioc c 21 0
- 使用
-
用户程序访问设备:
- 用户程序通过文件操作访问设备:
int main() {
- 用户程序通过文件操作访问设备:
int fd = open("/dev/spioc", O_RDWR);write(fd, "data", 4);char buf[4];read(fd, buf, 4);close(fd);return 0;}
驱动程序的代码结构
设备驱动程序的代码通常由以下部分组成:
-
头文件与宏定义:
- 包含内核相关头文件(如
<linux/module.h>
)。 - 定义关键宏,例如
__KERNEL__
表示用于内核环境,MODULE
表示模块驱动。
- 包含内核相关头文件(如
-
初始化与退出函数:
- 使用
module_init()
和module_exit()
宏注册设备初始化和退出函数。
- 使用
-
设备操作函数:
- 实现
open()
、read()
、write()
、ioctl()
、release()
等核心操作。
- 实现
-
注册接口:
- 定义
struct file_operations
,将其注册到内核,内核通过它调用驱动程序的具体实现。
- 定义
-
资源管理:
- 通过内存分配(如
kmalloc
)和资源释放(如kfree
)管理设备相关资源。
- 通过内存分配(如
-
与内核通信:
- 使用内核日志接口(如
printk()
)输出调试信息。
- 使用内核日志接口(如
驱动实例:字符设备驱动程序
以一个简单的字符设备驱动为例,具体实现功能包括:
- 初始化函数:注册字符设备,分配内存缓存区。
- 退出函数:注销字符设备,释放内存。
open()
和release()
:打开和关闭设备,管理资源。read()
和write()
:实现从缓存区读取数据和向缓存区写入数据。ioctl()
:处理特殊命令控制。
总结
4.3字符设备驱动程序
字符设备驱动程序是设备驱动程序的一种,用于操作以字节流方式传输数据的设备,如串口、终端等。字符设备被映射为文件系统中的节点,通常位于 /dev/
目录中,用户通过文件操作(如 open
、read
、write
)与设备交互。
字符设备驱动的特点
字符设备以线性字节流的方式进行数据传输,主要用于非块式存储设备。每个字符设备都通过主设备号和次设备号标识,主设备号关联设备驱动程序,次设备号区分具体设备实例。
设备文件与设备号
字符设备通过设备文件与系统交互,设备文件的创建通常使用 mknod
命令:
$ mknod /dev/my_device c <主设备号> <次设备号>
- 主设备号标识驱动程序,次设备号标识具体设备。
- 静态分配设备号通过
register_chrdev_region()
,动态分配通过alloc_chrdev_region()
,释放设备号使用unregister_chrdev_region()
。
字符设备的初始化与注册
字符设备通过 struct cdev
结构表示,初始化方式包括:
- 动态分配:
struct cdev *my_cdev = cdev_alloc(); my_cdev->ops = &my_fops;
- 静态初始化:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
注册设备使用 cdev_add()
,注销设备使用 cdev_del()
。
设备操作接口
字符设备驱动的核心在于 struct file_operations
,定义了驱动的接口函数。关键操作包括:
open()
:打开设备文件,完成资源分配或初始化操作。int (*open)(struct inode *inode, struct file *filp);
release()
:关闭设备文件,释放资源。int (*release)(struct inode *inode, struct file *filp);
read()
:从设备读取数据到用户空间。ssize_t (*read)(struct file *filp, char __user *buff, size_t count, loff_t *offp);
write()
:从用户空间写数据到设备。ssize_t (*write)(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
ioctl()
:执行设备控制命令(如获取设备状态、设置参数等)。int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
其他方法包括 llseek()
(用于设备定位)和 poll()
(用于非阻塞I/O)。
用户与内核空间的数据传输
在字符设备驱动中,内核通过以下函数实现用户空间和内核空间的数据传输:
copy_to_user()
:将数据从内核空间复制到用户空间。copy_from_user()
:将数据从用户空间复制到内核空间。
确保在操作时检查返回值,以避免未完成的数据传输。
并发与竞态控制
字符设备驱动在多线程环境下可能引发竞态问题,需通过以下方式避免:
- 信号量与互斥:
- 信号量用于控制多进程访问共享资源。
- 互斥量是信号量的一种特殊形式,初始值为1,用于保护临界区。
- 示例:
DECLARE_MUTEX(my_semaphore); down(&my_semaphore); // 获取信号量 up(&my_semaphore); // 释放信号量
- 自旋锁:
- 用于不可休眠的代码中,适合高性能、多CPU系统。
- 示例:
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; spin_lock(&my_lock); // 获取锁 spin_unlock(&my_lock); // 释放锁
中断与数据处理
字符设备驱动常需要处理中断以实现高效的数据传输:
- 中断注册:
- 使用
request_irq()
注册中断服务程序(ISR),使用free_irq()
释放中断。 - 示例:
int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *), unsigned long flags, const char *name, void *dev);
- 使用
- 中断处理:
- 中断程序应尽量短小高效,仅完成关键工作。
- 使用任务延迟机制(如
tasklet
或workqueue
)将复杂处理推迟到安全时机。
字符设备驱动的实例框架
以下是字符设备驱动的基本框架:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>static dev_t dev_num; // 设备号
static struct cdev my_cdev; // cdev 结构// 文件操作函数
static int my_open(struct inode *inode, struct file *file) { return 0; }
static ssize_t my_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { return 0; }
static ssize_t my_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { return count; }
static int my_release(struct inode *inode, struct file *file) { return 0; }// 文件操作结构
static struct file_operations fops = {.owner = THIS_MODULE,.open = my_open,.read = my_read,.write = my_write,.release = my_release,
};// 模块加载与卸载
static int __init my_init(void) {alloc_chrdev_region(&dev_num, 0, 1, "my_device");cdev_init(&my_cdev, &fops);cdev_add(&my_cdev, dev_num, 1);return 0;
}
static void __exit my_exit(void) {cdev_del(&my_cdev);unregister_chrdev_region(dev_num, 1);
}module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
4.4块设备驱动程序
块设备驱动程序是用于管理可以随机访问的固定大小数据块设备的驱动程序,如硬盘、SSD 和 SD 卡。块设备的特点是以块为单位读写数据,而不是以字节流的方式操作,通常具有更高的性能。设备以扇区为基本存储单元,Linux 内核使用的默认扇区大小为 512 字节,而块大小通常为 4KB。
块设备的特性
- 块设备支持随机读写,允许对存储数据的固定大小块进行直接访问。
- 性能优于字符设备,特别是在大文件传输和频繁的随机访问场景中。
- 数据块的大小由文件系统决定,典型值为 4096 字节,但内核操作时基于扇区(通常为 512 字节)。
块设备的注册与注销
块设备需要通过 register_blkdev
和 unregister_blkdev
函数向内核注册或注销:
-
注册块设备:
int register_blkdev(unsigned int major, const char *name);
major
:主设备号,若为 0,则动态分配。name
:设备名,用于标识设备。
-
注销块设备:
int unregister_blkdev(unsigned int major, const char *name);
这些函数确保块设备与内核的关联性,用户可通过设备名访问设备。
块设备的操作接口
块设备的操作接口通过 struct block_device_operations
定义,包括以下方法:
-
open
和release
:- 用于打开和释放块设备,初始化设备状态、分配资源,或在设备关闭时释放资源。
- 它们可能由应用程序调用(如
mount
),也可能由内核直接调用。
-
ioctl
:- 处理特殊的设备控制命令,例如更改设备参数或硬件状态。
- 块设备的大部分
ioctl
命令由内核处理,驱动程序仅实现少量特殊命令。
-
media_changed
和revalidate_disk
:- 用于检测可移动介质设备(如光驱)的介质是否发生更换,并进行相应处理。
块设备与字符设备的主要区别在于块设备没有 read
和 write
方法,而是通过请求队列处理 I/O 请求。
gendisk 结构
struct gendisk
是内核用于表示块设备或其分区的核心结构,包含以下重要成员:
fops
:指向块设备的操作函数集合(block_device_operations
)。queue
:设备的 I/O 请求队列,用于管理和调度设备的读写操作。
块设备初始化时需分配并初始化 gendisk
结构:
- 分配磁盘:
struct gendisk *alloc_disk(int minors);
,用于分配设备实例。 - 添加磁盘:
void add_disk(struct gendisk *gd);
,注册块设备到内核。 - 释放磁盘:
void del_gendisk(struct gendisk *gd);
,删除设备实例并释放资源。
I/O 请求队列的管理
块设备的读写请求由内核通过 I/O 请求队列管理,每个请求可能包含多个生物块(bio
),实现高效的数据传输。I/O 请求的处理流程如下:
-
请求队列初始化:
request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);
request
:请求处理函数,驱动程序定义。lock
:保护队列的自旋锁。
-
从队列中获取请求:
struct request *elv_next_request(request_queue_t *queue);
- 获取下一个请求,但不删除。
-
从队列中删除请求:
void blkdev_dequeue_request(struct request *req);
-
通知内核请求完成:
void end_request(struct request *req, int success);
success
指示请求是否成功完成。
请求的结构与处理
块设备的请求由 request
结构表示,每个请求可能包含多个 bio
,而每个 bio
可能包含多个 bio_vec
(数据段)。请求结构分层组织,用于优化大数据的分块传输和合并操作。
- 遍历请求中的 bio:
rq_for_each_bio(bio, request);
- 遍历 bio 中的数据段:
bio_for_each_segment(bvec, bio, segno);
请求完成函数
请求完成时需调用以下函数通知内核:
-
end_that_request_first
:- 通知内核驱动已完成指定扇区的传送。
int end_that_request_first(struct request *req, int success, int count);
success
:请求是否成功。count
:已完成的扇区数。
-
end_that_request_last
:- 通知等待的进程请求已完成,同时释放请求结构。
void end_that_request_last(struct request *req);
块设备驱动的代码框架
以下是块设备驱动的基本框架:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/blkdev.h>static struct gendisk *my_disk;
static request_queue_t *queue;// 请求处理函数
static void my_request(struct request_queue *q) {struct request *req;while ((req = elv_next_request(q)) != NULL) {// 处理请求end_request(req, 1);}
}// 初始化函数
static int __init my_init(void) {queue = blk_init_queue(my_request, &my_lock);my_disk = alloc_disk(1); // 分配磁盘my_disk->queue = queue;add_disk(my_disk); // 注册磁盘return 0;
}// 卸载函数
static void __exit my_exit(void) {del_gendisk(my_disk);blk_cleanup_queue(queue);
}module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
4.5 #### 网络设备驱动程序
网络设备驱动程序连接操作系统的网络协议栈与底层网络硬件,负责收发网络数据包,同时提供协议无关的接口支持。它与字符设备和块设备不同,没有对应的设备文件,也没有设备号,用户通过网络协议间接操作网络设备。
网络设备驱动的特点
- 网络设备驱动通过注册接口供内核调用,与协议栈相结合。
- 不提供
read
和write
等直接文件操作,而通过异步方式接收数据。 - 驱动与协议独立,支持不同的硬件(如以太网、令牌环)和协议(如 IP、IPX)。
- 必须支持设置网络地址、修改发送参数、维护流量统计和错误统计等功能。
net_device
结构与管理
网络设备通过 struct net_device
表示,其核心字段包括设备的硬件地址、接口名、操作函数和发送队列等。
-
分配
net_device
:- 手动分配:
alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *))
。 - 以太网专用分配:
alloc_etherdev(int sizeof_priv)
,自动设置设备名为eth%d
。 - 示例:
struct net_device *dev = alloc_etherdev(sizeof(struct my_private));
- 手动分配:
-
释放
net_device
:- 使用
free_netdev(struct net_device *dev)
释放分配的设备结构。
- 使用
-
注册和注销设备:
- 注册设备:
register_netdev(struct net_device *dev)
。 - 注销设备:
unregister_netdev(struct net_device *dev)
。
- 注册设备:
网络接口的打开与关闭
网络接口的操作主要由 ifconfig
调用 open
和 close
接口完成:
-
打开接口:
- 申请资源(如中断、I/O 地址空间)。
- 设置硬件地址(如 MAC 地址)。
- 启动发送队列:
void netif_start_queue(struct net_device *dev);
-
关闭接口:
- 释放资源。
- 停止发送队列:
void netif_stop_queue(struct net_device *dev);
数据包的发送与接收
-
数据包的发送:
- 数据包通过
sk_buff
(Socket Buffer)结构表示,包含要发送的数据及其元信息。 - 分配和释放
sk_buff
:struct sk_buff *dev_alloc_skb(unsigned int len); void dev_kfree_skb(struct sk_buff *skb);
- 数据发送函数:
int hard_start_xmit(struct sk_buff *skb, struct net_device *dev);
- 发送队列操作:
- 停止队列:
netif_stop_queue(dev)
。 - 重启队列:
netif_wake_queue(dev)
。 - 完全停止:
netif_tx_disable(dev)
(确保无数据包正在发送)。
- 停止队列:
- 数据包通过
-
数据包的接收:
- 中断触发后,分配缓存区存储数据包(
dev_alloc_skb
)。 - 数据通过
memcpy
拷贝到缓存区。 - 调用
netif_rx
将数据包提交到上层协议栈:int netif_rx(struct sk_buff *skb);
- 中断触发后,分配缓存区存储数据包(
中断处理机制
网络设备通常采用中断方式处理发送和接收事件:
-
中断分类:
- 接收中断:处理接收到的数据包。
- 发送中断:确认发送完成,释放
sk_buff
。 - 错误或状态改变中断:处理错误标志或更新状态。
-
中断处理流程:
- 根据中断状态寄存器判断中断类型(接收、发送或错误)。
- 若为接收中断:
- 分配
sk_buff
保存数据包。 - 更新接收统计计数。
- 提交数据包给协议栈。
- 分配
- 若为发送中断:
- 释放发送缓冲区。
网络驱动的代码框架
以下是一个简单网络设备驱动的基本框架:
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>static struct net_device *net_dev;// 打开设备
static int net_open(struct net_device *dev) {netif_start_queue(dev); // 启动发送队列return 0;
}// 关闭设备
static int net_close(struct net_device *dev) {netif_stop_queue(dev); // 停止发送队列return 0;
}// 发送数据包
static netdev_tx_t net_start_xmit(struct sk_buff *skb, struct net_device *dev) {dev_kfree_skb(skb); // 释放发送的 sk_buffreturn NETDEV_TX_OK;
}// 网络设备操作函数
static const struct net_device_ops netdev_ops = {.ndo_open = net_open,.ndo_stop = net_close,.ndo_start_xmit = net_start_xmit,
};// 模块加载
static int __init net_init(void) {net_dev = alloc_etherdev(0);net_dev->netdev_ops = &netdev_ops;register_netdev(net_dev); // 注册设备return 0;
}// 模块卸载
static void __exit net_exit(void) {unregister_netdev(net_dev);free_netdev(net_dev);
}module_init(net_init);
module_exit(net_exit);
MODULE_LICENSE("GPL");
总结
网络设备驱动程序通过 net_device
和 sk_buff
实现了数据包的发送与接收,并与 Linux 网络协议栈紧密结合,提供了协议无关的硬件支持。其设计强调异步操作、高效的队列管理和资源抽象,使其广泛应用于以太网卡、无线网卡等多种网络设备的驱动开发中。