什么是嵌入式开发?

前言

       这是一篇查阅大量资料之后总结摘录出的文章,写这篇文章有两个目的:一是具备与嵌入式软件开发从业者们沟通的能力,不至于在与别人沟通甚至是面试时不知所云、一问三不知,所以本文对嵌入式系统中的一些常见概念进行了阐述;二是对嵌入式开发以及Linux开发有一个全面的了解,这无疑会对今后无论是学习或者工作方向的选择都大有裨益,正所谓“先观其广,再究其深”,私以为不系统的了解嵌入式的情况下,任何学习和工作行为都是盲目的。

一、嵌入式软件开发方向   

       嵌入式系统是用于控制、监视或者辅助管理其他设备的装置,是一种专用的计算机系统(IEEE定义);国内定义是以应用为中心,以计算机技术为基础,能够根据用户需求(功能、可靠性、成本、体积、功耗、环境等)灵活裁剪软硬件模块的专用计算机系统。简单的来说,除了通用计算机(电脑)和部分服务器外的一切计算机系统,都是嵌入式系统。嵌入式开发的发展方向有很多,主要分为硬件和软件开发两大类,其中软件又可细分为多个方向,如下图所示:

​​
图1.1 嵌入式开发方向脑图

       任何一个计算机系统都是系统中软硬件协作的结果,没有硬件的软件是空中楼阁,没有软件的硬件是一堆废铁,硬件是软件运行的基础。软件上所有操作最终都会被硬件以硬件工作的时序进行工作,硬件建造出来是固定的,而软件则很灵活,可以根据场景适应多种应用,两者相辅相成,缺一不可。

       嵌入式电子产品硬件部分大部分都是相同的,核心的都是由CPU、RAM和FLASH几大部分组成,而软件就千差万别了。产品的具体功能都是由软件来实现的,一般来讲一个产品的实现,软件设计的工作量是硬件设计的4~5倍。

二、嵌入式系统中的一些概念

2.1 操作系统概念

       操作系统(Operatint System,OS)是指控制和管理整个计算机系统的硬件和软件资源,并合理地组织调度计算机的工作和资源的分配,以提供给用户和其他软件方便的接口和环境;它是计算机系统中最基本的、最接近系统硬件的系统软件。如windows操作系统的“任务管理器”中进程应用界面就是对软件的管理,性能窗口中CPU使用率和内存使用率就是对硬件的管理。

       早期嵌入式开发没有嵌入式操作系统的概念 ,直接操作裸机,在裸机上写程序,比如用51单片机基本就没有操作系统的概念。通常程序包括一个死循环和若干个中断服务程序:应用程序是一个无限循环,循环中调用API函数完成所需的操作。中断服务程序用于处理系统的异步事件,中断是处理器提供的一种异步机制。

异步是指:在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底的, 而是走走停停,以不可预知的速度向前推进,这就是进程的异步性。如果失去了并发性,即系统只能串行地运行各个程序,那么每个程序的执行会 一贯到底。只有系统拥有并发性,才有可能导致异步性

       并不是任何一个计算机系统都一定要运行操作系统,在许多情况下操作系统是不要的。对于功能比较单一、控制并不复杂的系统,如公交车刷卡机、电冰箱、微波、简单的手机和小灵通等,并不需要多任务调度、文件系统、内存管理等复杂功能,单任务架构完全可以很好地支持它们的工作。一个无限循环中夹杂对设备中断的检测或者对设备的轮询是这种系统中软件的典型架构。裸机的实现就有点类似单片机(MCU)了,尽管单片机的寄存器没有那么的多,如果会裸机驱动,单片机开发工作应该也能得心应手了,基础的单片机编程通常都是指裸机编程,即不加入任何操作系统如RTOS(Real Time Operating System 实时操作系统)。 

2.1.1 并发、并行和串行

       串行运行指的是一种顺序执行,比如先完成task1,接着做task2,直到完成task2,然后做task3……依次按照顺序完成每一件事情,必须要完成每一件事情才能去做下一件事,只有一个执行单元。

​​
图2.1 串行运行示意图

       并行运行指的是可以并排/并列执行多个任务,这样的系统,它通常有多个执行单元,所以可以实现并行运行,比如并行运行task1、task2、task3,如下图所示。并行运行并不一定要同时开始运行、同时结束运行。

​​
图2.2 并行运行示意图

       并发运行强调的是一种时分复用,与串行的区别在于它不必等待上一个任务完成之后再做下一个任务,可以打断当前执行的任务切换下一个任务,这就是时分复用。在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉/交替执行),这就是并发运行。如下图所示:

​​
图2.3 并发运行示意图

       有一个很生动的比喻用来说明串行、并行、并发三个概念的区别:

       串行:你吃饭吃到一半,电话来了,你一直到吃完了以后才去接电话,这就说明你不支持并发也不支持并行,仅仅只是串行。

       并行:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

       并发:你吃饭吃到一半,电话来了,你停下吃饭去接了电话,电话接完后继续吃饭,这说明你支持并发

多核处理器和单核处理器:

       单核处理器只有一个执行单元,同时只能执行一条指令;多核处理有多个执行单元,可以并行执行多条指令,比如8核处理器可以并行执行8条不同的指令。计算机操作系统中,通常同时运行着几十上百个不同的线程,在单核或多核处理系统中都是如此。

       对于单核处理器系统来说,它只有一个执行单元,只能采用并发运行系统中的线程,而肯定不可能是串行,事实上也确实如此。内核实现了调度算法,用于控制系统中所有线程的调度,简单来说,系统中所有参与调度的线程会加入到系统的调度队列中,它们由内核控制,每一个线程执行一段时间后,由系统调度切换执行调度队列中下一个线程,依次进行。

       对于多核处理器系统来说,它拥有多个执行单元,在操作系统中,多个执行单元以并行方式运行多个线程,同时每一个执行单元以并发方式运行系统中的多个线程。

总结:

①并行运行情况下的多个执行单元,每一个执行单元同样也可以以并发方式运行。

②单核CPU同一时刻只能执行一个程序,各个程序只能并发执行;

③多核CPU同一时刻可以同时执行多个程序,多个程序可以并行地执行,比如Intel的第八代i3处理器就是4核CPU,意味着可以并行地执行4个程序,即使是对于4核CPU来说,只要有4个以上的程序需要“同时”运行,那么并发性依然是必不可少的,因此并发性是操作系统最基本的特性。

关于同时运行:

       计算机处理器运行速度是非常快的,在单个处理核心虽然以并发方式运行着系统中的线程(微观上交替 /交叉方式运行不同的线程),但在宏观上所表现出来的效果是同时运行着系统中的所有线程,因为处理器的运算速度太快了,交替轮训一次所花费的时间在宏观上几乎是可以忽略不计的,所以表示出来的效果就是同时运行着所有线程。

2.1.2 裸机系统—轮询系统

       为什么需要操作系统?首先得知道裸机系统,裸机系统通常分成前后台系统轮询系统。轮询系统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情,经典的单片机代码如下:

int main()
{
	HardWareInit();				//硬件相关初始化

	for (;;)
	{
		DoSomething1();			//处理任务1
		DoSomething2();			//处理任务2
		DoSomething3();			//处理任务3
	}
	return 0;
}

       轮询系统是一种非常简单的软件结构,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情。在代码清单中,如果只是实现LED翻转,串口输出,液晶显示等这些操作,那么使用轮询系统将会非常完美。

       但是,如果加入了按键操作等需要检测外部信号的事件,用来模拟紧急报警,那么整个系统的实时响应能力就不会那么好了。假设DoSomething3是按键扫描,当外部按键被按下,相当于一个警报,这个时候,需要立马响应 , 并做紧急处理 , 而这个时候程序刚好执行到DoSomething1,要命的是DoSomething1需要执行的时间比较久,久到按键释放之后都没有执行完毕,那么当执行到DoSomething3的时候就会丢失掉一次事件。足见,轮询系统只适合顺序执行的功能代码,当有外部事件驱动时,实时性就会降低

2.1.3 裸机系统—前后台系统

       但是裸机要实现实时响应也可以,用中断。在裸机系统中,所有的操作都是在一个无限的大循环里面实现,支持中断检测。前后台系统:外部中断紧急事件在中断里面标记或者响应,中断服务称为前台,main 函数里面的while(1)无限循环称为后台,按顺序处理业务功能,以及中断标记的可执行的事件。在顺序执行后台程序的时候,如果有中断来临,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序里面标记事件,如果事件要处理的事情很简短,则可在中断服务程序里面处理,如果事件要处理的事情比较多,则返回到后台程序里面处理。一般来说:如果项目里面没有使用RTOS等操作系统,则一般使用的都是这种前后台系统。前后台系统程序结构通常如下:

int main()
{
	HardWareInit();				//硬件相关初始化

	for (;;)
	{
		if (flag1)
		{
			DoSomething1();			//处理任务1
		}
		if (flag2)
		{
			DoSomething2();			//处理任务2
		}
		if (flag3)
		{
			DoSomething3();			//处理任务3
		}
	}
	return 0;
}

void ISR1()
{
	flag1 = 1;
	DoSomething1();			//如果事件处理时间很短,则在中断里面处理如果事件处理时间比较长,则回到后台处理处理
}

void ISR2()
{
	flag2 = 1;
	DoSomething2();			//如果事件处理时间很短,则在中断里面处理如果事件处理时间比较长,则回到后台处理处理
}

void ISR3()
{
	flag3 = 1;
	DoSomething3();			//如果事件处理时间很短,则在中断里面处理如果事件处理时间比较长,则回到后台处理处理
}

       小型的电子产品用的都是裸机系统,而且也能够满足需求。可是这样会让程序结构变得复杂,当程序复杂之后,这样的裸机程序难以阅读和维护。虽然事件的响应和处理是分开了,但事件的处理还是在后台里面顺序执行的,但相比轮询系统,前后台系统确保了事件不会丢失,再加上中断具有可嵌套的功能,这可以大大的提高程序的实时响应能力。在大多数的中小型项目中,前后台系统运用的好,堪称有操作系统的效果。

2.1.4 裸机系统局限性

       综上对裸机系统的阐述,显而易见裸奔(裸机)是不太好的,有很多局限性,具体体现在:

1)程序并发工作效率低

       在写裸机软件时,不可避免的在主程序中会有一个超级大的 while(1) 循环,这里面几乎包含整个项目的所有业务逻辑。因为每个业务逻辑里面都会有 delay 这样的循环等待函数,这样导致了所有的业务逻辑几乎都是串行起来工作的。这个时候 CPU 就会有很多时间都浪费在了延时函数里,一直在空转,导致软件的并发效率非常差。

2)违背“高内聚、低耦合”原则

       而裸机的模块化开发难度非常大,模块间的耦合较重,这也导致了无法在大型项目使用裸机来开发。还是刚才main函数中大 while(1) 的例子,可以想象到那么多功能都紧紧的挤在一个函数里,不可拆分,模块化开发的困难重重。有的工程师把单任务系统设计成设备驱动和具体的应用软件模块处于同一层次(即应用程序也在比如serial.c中实现),这显然是不合理的不符合软件设计中高内聚低耦合的要求。

3)很多高级软件,必须依赖于操作系统来实现。

4)功能复杂的情况下实时性无法保证。

5)裸机的代码会过多的依赖于底层硬件,重复造轮子的过程不可避免,软件可复用性差。

2.1.5 操作系统功能

       裸机运行的程序代码没有多任务、线程的概念,引入操作系统后,程序执行时可以把一个应用程序分割为多个任务,每个任务完成一部分工作,并且每个任务都可以写成死循环。操作系统根据任务的优先级,通过调度器使CPU分时执行各个任务,保证每个任务都能够得到运行。若调度方法优良,则可使各个任务看起来是并行执行的,减少了CPU的空闲时间,提高了CPU的利用率。由操作系统的任务管理衍生出相应的CPU管理、内存管理,它们分别负责分配任务对CPU的占有权和管理任务所占有的内存空间。在linux操作系统中,还具有文件管理、I/O设备管理的功能。

​​
图2.4 操作系统功能示意图

        上图箭头含义分别是:

       ①操作系统作为系统资源的管理者,提供的功能有:处理器管理、存储器管理、文件管理、设备管理。

       ②操作系统作为用户与计算机硬件之间的接口,要为其上层的用户、应用程序提供简单易用的服务。提供的功能有:命令接口、程序接口、GUI。

       ③操作系统作为最接近硬件的层次:实现对硬件机器的拓展,用户无需关心这些硬件在底层是怎么组织起来工作的,只需直接使用操作系统提供的接口即可。

​​
图2.5 操作系统功能脑图

       毫无疑问操作系统具有很大的优越性,使用了操作系统后,多线程可以提高程序模块化程度,并发运行使CPU利用率提高;通过设计线程的优先级可以保证整个软件的实时性;另外操作系统无疑是智慧的结晶,设计者们站在应用软件、底层驱动的开发角度,对很多常见的软件功能进行了封装、抽象,比如:信号量、事件通知等,这些功能拿来即用,对开发者来说十分友好;还有一些操作系统比如Linux,封装了一套标准的硬件操作接口,一般称为设备驱动框架,对应用工程师来说不用担心更换硬件重复造轮子了,提高开发效率。

       虽然操作系统本身也会占用一部分计算机资源,但是总体上它让计算机运行更为稳定,同时也减少了软件开发者的工作量,因为程序猿只需要考虑操作系统的标准接口,而不需要考虑硬件系统的底层差异。

       总而言之,操作系统,是个庞然大物,但是大家都很喜欢,因为他能帮你做很多你不愿意面对的事。他能提供很好的底部的支持。以QQ发消息举例:QQ就是一款软件,往对话框里写字,然后通过网络传到另一台用户端。如果有操作系统,要对话框就给对话框,要联网改一改IP就能上网,中间的网络协议也不要管。要是没有操作系统,需要写代码生成一个对话框,对话框写进去的字要变成文件,自己打包使之符合网络协议,自己通信。类似要生火,一个人说(操作系统),我有打火机,你自己用。另一个说(没有操作系统),我这有铁矿,还有天然气矿,都给你了,你想做什么打火机都可以。

       如果是一个巨大的项目,操作系统是必备的,他能提供很多支持,做很多基层的工作,方便以后的升级。但是他的维护和他所消耗的资源(空间),也成为了它在单片机领域推广的致命伤。所以相反的如果你只要某个功能,不装操作系统明显方便,而且易于维护。

       单片机可以运行操作系统,但是操作系统不是刚需,上操作系统比较占用单片机的资源,比如占用比较多的FLASH和RAM,间接增加了硬件成本,不过现如今单片机的 FLASH和RAM是越来越大,完全足以抵挡RTOS那点开销。

操作系统四大特征:并发、共享、虚拟、异步。

       并发和共享是最为基本的特征,二者互为存在条件。并发性指计算机系统中同时存在着多个运行着的程序。共享性是指系统中的资源可供内存中多个并发执行的进程共同使用。通过一个例子来看并发与共享的关系:使用QQ发送文件A,同时使用微信发送文件B,两个进程正在并发执行(并发性);需要共享地访问硬盘资源(共享性)。如果失去并发性,则系统中只有一个程序正在运行,则共享性失去存在的意义;如果失去共享性,则QQ和微信不能同时访问硬盘资源,就无法实现同时发送文件,也就无法并发,所以并发性与共享性互为存在的条件!

2.1.6 分时操作系统与实时操作系统

       目前用的比较多的就是实时操作系统(Real Time Operating System,RTOS),常用的有国外的FreeRTOS、μC/OS、RTX 和国内的 RT-thread、Huawei LiteOS 和 AliOS-Things 等,其中开源且免费的 FreeRTOS 的市场占有率极高,在工作中用的最多的就是FreeRTOS。

       μC/OS-II嵌入式实时操作系统因其方便移植、代码量小、实时性强、可靠性高、内核可剪裁等优点,成为我国计算机嵌入式应用领域最受喜爱的实时操作系统之一。

      实时操作系统强调的是:实时性,只有“实时性”才是RTOS的最大特征,其它的都不算是;RTOS操作系统的核心内容在于:实时内核。实时操作系统中都要包含一个实时任务调度器,这样在有操作系统的任务调度之后,就会让系统响应更具有实时性。这个任务调度器与其它操作系统的最大不同是:严格按照优先级来分配CPU时间,并且时间片轮转不是实时调度器的一个必选项。在实时操作系统的控制下,计算机系统接收到外部信号后及时进行处理,并且要在严格的时限内处理完事件。实时操作系统的主要特点是及时性和可靠性。主要优点就是能够优先响应一些紧急任务,某些紧急任务不需时间片排队。

       提出实时操作系统的概念,可以至少解决两个问题:一个是早期的CPU任务切换的开销太大,实时调度器可以避免任务频繁切换导致CPU时间的浪费;另一个是在一些特殊的应用场景中,必须要保证重要的任务优先被执行。

       经常跟实时操作系统一起讲的,还有嵌入式操作系统这个概念,但实际上这是完全不同的两种东西,虽然大多数实时操作系统都是嵌入式操作系统,但嵌入式操作系统并不全都是实时的,还有分时操作系统(Time-sharing Operating System,TSOS),Linux就属于分时操作系统。

分时操作系统:计算机以时间片为单位轮流为各个用户/作业服务,各个用户可通过终端与计算机进行交互。
主要优点:用户请求可以被即时响应,解决了人机交互问题。允许多个用户同时使用一台计算机,并且用户对计算机的操作相互独立,感受不到别人的存在。
主要缺点:不能优先处理一些紧急任务。操作系统对各个用户/作业都是完全公平的,循环地为每个用户/ 作业服务一个时间片,不区分任务的紧急性。

2.2 驱动

2.2.1 嵌入式驱动的作用

       随着芯片的集成度越来越高,相当多的处理器如ARM处理器集成了UART、I2C控制器、SPI控制器、USB控制器、ADC控制器等外设,而如何使这些模块正常工作,并能够控制外部的芯片满足最后功能的需求,驱动的概念应运而生,它是应用或系统访问实际硬件的桥梁。驱动针对的对象是存储器和外设(包括 CPU 内部集成的存储器和外设),而不是针对CPU 内核。

       对设备驱动最通俗的解释就是“驱使硬件设备行动” 。设备驱动与底层硬件直接打交道按照硬件datasheet要求的方式上下电、读写寄存器、中断处理、通信、DMA搬运,进行物理内存向虚拟内存的映射等,最终使通信设备能够收发数据,使显示设备能够显示文字和画面,使存储设备能够记录文件和数据。

       由此可见,设备驱动充当了硬件和应用软件之间的纽带,它使得应用软件只需要调用系统软件的应用编程接口(API)就可让硬件去完成要求的工作。在系统中没有操作系统的情况下,工程师可以根据硬件设备的特点自行定义接口,如对串口定义SerialSend()、SerialRecv();对 LED 定义LightOn()、LightOff();以及对 Flash 定义FlashWrite()、FlashRead()等。而在有操作系统的情况下,设备驱动的架构则由相应的操作系统定义,驱动工程师必须按照相应的架构设计设备驱动,这样,设备驱动才能良好地整合到操作系统的内核中。

       驱动程序沟通着硬件和应用软件,它为应用程序屏蔽了硬件的细节,软件和硬件不应该互相渗透到对方的领地,为了尽可能快速地完成设计,应用软件工程师不想也不必关心硬件,而硬件工程师也难有足够的闲暇和能力来顾及软件,也就是说,应用软件工程师需要看到一个没有硬件的纯粹的软件世界。例如,应用软件工程师在调用套接字发送和接收数据包的时候,他不必关心网卡上的中断、寄存器、存储空间、I/O端口、片选以及其他任何硬件词汇;在使用printf()函数输出信息的时候,他不用知道底层究竟是怎样把相应的信息输出到屏幕或串口。随着通信、电子行业的迅速发展,全世界每天都会有大量的新芯片被生产,大量的新电路板被设计,因此,也会有大量设备驱动需要开发。这些设备驱动,或运行在简单的单任务环境中,或运行在VxWorks、Linux、Windows等多任务操作系统环境中,发挥着不可替代的作用。

       讲到嵌入式驱动开发,很多时候都会理解为嵌入式linux平台下的驱动开发,这种理解当然有其原因,对于大部分单片机项目,因为需求和设计比较简单,开发中往往并没有驱动的概念,更加不会有嵌入式linux开发中严格分层的概念,但是对寄存器的配置和读写实现对外部硬件的控制,它们事实上也属于驱动的范畴,不过驱动的工作当然远不止如此。

2.2.2 无操作系统的设备驱动

       对于功能比较单一、控制并不复杂的系统,并不需要多任务调度、文件系统、内存管理等复杂功能,单任务架构完全可以很好地支持它们的工作,可能一个无限循环加上按键、中断的处理就能完成功能设计。在这种情况下,应用和驱动分割的不是那么清楚,一般可能就是一个人完成了应用和驱动。

       裸机系统虽然不存在操作系统,但是设备驱动是必须存在的。一般情况下,对每一种设备驱动都会定义为一个软件模块,包含.h文件和.c文件,前者定义该设备驱动的数据结构并声明外部函数,后者进行设备驱动的具体实现。例如一个串口驱动serial.c serial.h,主要是配置GPIO,串口控制寄存器,以及串口的收发(读写)寄存器,而这几个配置都是自定义函数实现的,比如串口的写(发)SerialSend 函数等。

       其他模块需要使用这个设备的时候,只需要包含设备驱动的头文件 serial.h,然后调用其中的外部接口函数即可。如我们要从串口上发送字符串“Hello World”,使用函数SerialSend( " Hello World ",11)即可。

       有的工程师把单任务系统设计成设备驱动和具体的应用软件模块处于同一层次(即应用程序也在比如serial.c中实现),这显然是不合理的,不符合软件设计中高内聚低耦合的要求。另一种不合理的设计是直接在应用中操作硬件的寄存器(单独一个main.c,所有功能都在一个函数中实现,不采用其他任何接口/函数),而不单独设计驱动模块,这种设计意味着系统中不存在或未能充分利用可被重用的驱动代码。

总结:由此可见,在没有操作系统的情况下,设备驱动的接口被直接提交给了应用软件工程师, 应用软件没有跨越任何层次就直接访问了设备驱动的接口,设备驱动包含的接口函数也与硬件的功能直接吻合, 没有任何附加功能,如下图所示:

图2.6 没有操作系统设备驱动

2.2.3 有操作系统的设备驱动

       不管有无操作系统,设备驱动都是必须的,设备驱动的硬件操作工作仍然是必不可少的。有了操作系统后,驱动程序需要融入到内核,为了实现这种融合,必须在所有的设备驱动中设计面向操作系统内核的接口这样的接口由操作系统规定,对一类设备而言结构一致,独立于具体的设备。

       由此可见,当系统中存在操作系统的时候,设备驱动变成了连接硬件和内核的桥梁。操作系统的存在势必要求设备驱动附加更多的代码和功能(以我看,主要是提供了很多结构),把单一的“驱使硬件设备行动”变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的API,不再给应用软件工程师直接提供接口。有了操作系统之后,设备驱动反而变得复杂,操作系统就是通过给设备驱动制造麻烦来达到给上层应用提供便利的目的。如果设备驱动都按照操作系统给出的独立于设备的接口而设计,应用程序将可使用统一的系统调用接口来访问各种设备。对于类UNIX的VxWorks、Linux等操作系统而言,应用程序通过write()、read()等函数(不用针对串口定义个SerialSend函数,针对I2C定义一个I2CSend函数)读写文件就可以访问各种字符设备和块设备,而不用管设备的具体类型和工作方式,是非常方便的。

       应用和驱动之间的纽带是固定的,驱动工程师需要按照操作系统规定的接口进行设计,所以存在操作系统时,驱动变成了硬件和内核直接的桥梁,它对外呈现的是统一的接口,例如:write()、read()、驱动程序有Aread()、Bread(),操作系统会根据实际使用的设备调用相应的驱动,不用每次都重新匹配,应用工程师也完全不必关心硬件变化。

总结:有了操作系统后,驱动会变得稍显复杂,所有设备驱动设计必须符合操作系统内核的接口规定,驱动变成连接硬件和内核的桥梁,对外呈现为操作系统的API,不再给应用软件工程师直接提供接口。应用软件工程师需要通过系统调用或其它方式间接操作驱动设备接口,如下图所示:

图2.7 有操作系统设备驱动

2.2.4 系统调用

       上面提到驱动按照操作系统规定的接口设计时,应用程序可以使用统一的系统调用接口来访问各种设备,什么是系统调用?

设备驱动程序是操作系统内核机器硬件之间的接口

系统调用是操作系统内核应用程序之间的接口。

       以Linux为例进行说明,系统调用(system call)其实是 Linux 内核提供给应用层的应用编程接口(API),是Linux应用层进入内核的入口。不止Linux系统,所有的操作系统都会向应用层提供系统调用,应用程序通过系统调用来使用操作系统提供的各种服务。

       通过系统调用,Linux 应用程序可以请求内核以自己的名义执行某些事情,譬如打开磁盘中的文件、读 写文件、关闭文件以及控制其它硬件外设。通过系统调用 API,应用层可以实现与内核的交互,其关系可通过下图简单描述:

​​
图2.8 内核、系统调用与应用程序

       内核提供了一系列的服务、资源,支持一系列功能,应用程序通过调用系统调用 API 函数来使用内核提供的服务、资源以及各种各样的功能,比如Windows 应用编程,操作系统内核一般都会向应用程序提供应用编程接口API,否则我们将无法使用操作系统。

系统调用API与库函数API区别(文件I/O与标准I/O):

      系统调用是内核直接向应用层提供的应用编程接口,例如open、write、 read、close 等,标准C语言函数库是操作系统实现的,构建于系统调用之上的,例如fopen就利用open系统调用执行打开文件的操作(有些库函数并不调用任何系统调用如字符串处理函数等)。他们之间的区别如下:

       ①库函数属于应用层,而系统调用是内核提供给应用层的编程接口,属于系统内核的一部分;

       ②库函数运行在用户空间,调用系统调用会由用户空间(用户态)陷入到内核空间(内核态);

       ③库函数通常是有缓存的,而系统调用是无缓存的,所以在性能、效率上,库函数通常要优于系统调用

       ④可移植性:库函数相比于系统调用具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同的,而对于C语言库函数来说,由于很多操作系统都实现了C语言库,C语言库在不同的操作系统之间其接口定义几乎是一样的。

Tips:

       在Linux系统下,可以通过man命令(也叫 man 手册)来查看某一个Linux系统调用的帮助信息,man命令可以将该系统调用的详细信息显示出来,比如函数功能介绍、函数原型、参数、返回值以及使用该函数所需包含的头文件等信息。

       man命令后面跟着两个参数,数字2表示系统调用,man命令除了可以查看系统调用的帮助信息外,还可以查看Linux命令(对应数字 1以及标准C库函数(对应数字 3)所对应的帮助信息;最后一个 参数表示需要查看的系统调用函数名,例如man 2 open。

2.3 内核

       内核是计算机上配置的底层软件,是操作系统最基本、最核心的部分,实现操作系统内核功能的程序就是内核程序。内核是操作系统执行的第一道程序,被率先加载到内存中开始系统行为。内核始终保持在主内存中直到系统被关闭。内核将用户输入的命令转换成计算机硬件能理解的机器语言。

注意:不同的操作系统在对内核的划分上是不同的。有的系统会将时钟管理、中断处理、原语和对系统资源进行管理的功能(进程管理、存储器管理、设备管理)都算作操作系统的内核;但是,有的操作系统不会将对系统资源进行管理的功能算在内核之中,这一类操作系统内核就只会包含时钟管理、中断处理、原语。
       由此我们引入大内核和微内核的概念,大内核是将上图中的进程管理、存储器管理、时钟管理、中断处理等操作均包含在内;微内核是只有最接近底层软件的时钟管理、中断处理、原语等。

       内核是系统应用软件和硬件的桥梁。内核直接与硬件联系,并告之它由应用软件发起的请求。操作系统不能脱离内核工作,内核是系统正常运行最重要的程序。内核的主要职责是:进程管理、磁盘管理、任务调度、内存管理等。

内核与操作系统的区别:内核用于管理系统资源,例如提供对软件层面的抽象(例如对进程、文件系统、同步、内存、网络协议等对象的操作和权限控制),和对硬件访问的抽象(例如磁盘,显示,网络接口卡(NIC));操作系统,在内核的基础上有延伸,包括了提供基础服务的系统组件。

操作系统与内核区别
序号内核操作系统
1系统级软件,操作系统的一部分系统级软件
2应用软件和硬件的接口用户和硬件的接口
3是操作系统执行的第一道程序是计算机系统执行的第一道程序
4主要负责进程管理、磁盘管理、任务调度、内存管理等核心任务主要是负责安全性与隐私、中断与挂起等其他任务

2.4 嵌入式处理器

2.4.1 嵌入式处理器分类

       嵌入式系统的核心,就是嵌入式处理器。嵌入式处理器一般分为以下几种典型类型: 

图2.9 嵌入式处理器分类

​       总结一下,MPU和MCU的区别本质上是因为应用定位不同,为了满足不同的应用场景而按不同方式优化出来的两类器件。MPU注重通过较为强大的运算/处理能力,执行复杂多样的大型程序,通常需要外挂大容量的存储器。而MCU通常运行较为单一的任务,执行对于硬件设备的管理/控制功能。通常不需要很强的运算/处理能力,因此也不需要有大容量的存储器来支撑运行大程序。通常以单片集成的方式在单个芯片内部集成小容量的存储器实现系统的“单片化”。

图2.10 MCU和MPU

​​2.4.2 ARM公司及其ARM架构

       当我们谈及嵌入式处理器的体系架构时,一般都是想到Intel的X86架构和ARM公司的ARM架构。X86架构和ARM架构最大的不同点就是使用的指令集不同,前者使用的CISC指令集,后者使用的是RISC指令集,还有一点就是X86架构使用的是冯诺依曼结构,ARM架构既使用冯诺依曼结构,也使用哈佛结构(已经成了一种趋势)。

       ARM包含两个意思:一是指ARM公司;二是指ARM公司设计的低功耗CPU及其架构(关于ARM架构的具体工作原理不需要深入了解,除非是在intel或者ARM这些设计内核的大厂上班),包括ARM1~ARM11与Cortex,ARM架构已经由V1版本发展之至V9(2021.3),V6版本ARM公司内核命名开始加上Cortex(之前命名都是ARM+数字), 到V7版本分三路发展,A系列,R系列,M系列,例如Arm Cortex-A7内核。

     ARM是全球领先的32位嵌入式RISC芯片内核设计公司。RISC的英文全称是Reduced Instruction Set Computer,即精简指令集计算机。特点是所有的指令格式都是一致的,所有指令的指令周期也是相同的,并且采用流水线技术。

       ARM公司本身并不生产和销售芯片,只做内核设计,以出售ARM内核的知识产权(IP授权)为主要模式。全球顶尖的半导体公司,例如Actel、TI、ST、Fujitsu、NXP等均通过购买ARM的内核,结合各自的技术优势进行生产和销售,例如时钟、总线、FLASH、SRAM、外设等的设计;然后由代工厂如台积电、中芯国际、三星、长电科技等厂家生产芯片(处理器),内核与MCU的关系如下图所示:

​​
图2.11 内核与MCU

       Cortex是ARM的全新一代处理器内核,按照3类典型的嵌入式系统应用,即高性能、微控制器、实时类分成3个系列,即Cortex-A(Application)、Cortex-M(Microcontroller),Cortex-R(Real-time)。

Cortex内核分类及特征
Cortex-ACortex-RCortex-M
特点高时钟频率,长流水线,高性能较高时钟频率,较长的流水线,实时性强时钟频率较低,通常较短的流水线,超低功耗
应用场景移动计算、智能手机、平板电脑、数字电视军工、汽车电子、无线基带、硬盘控制器工控、传感器、消费电子、家用电器、医疗器械

三、嵌入式Linux开发

3.1 嵌入式Linux系统组成

       首先我们需要知道Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。Linux的发行版(也就是将Linux内核与应用软件做一个打包)目前市面上比较知名的有:Ubuntu、RedHat、CentOS、Debian等。

注意(Linux内核与操作系统区别):

       Linux本质上是内核,不是操作系统,Ubuntu属于操作系统,它是在Linux内核的基础上添加了各种服务(TFTP服务、NFS服务等)、桌面环境(原生的Linux内核是没有界面的,只能敲命令操作,Ubuntu有桌面让用户可以像使用windows那样方便操作)、库(原生的Linux内核也是没有诸如stdio.h和math.h这些库的,是操作系统提供的)、应用程序等形成的。

        所有的电子产品,所有技术都可以认为要么是单片机,要么是Linux;GUI(图形用户界面)方面主要是QT/Android,它们都是运行于Linux之上的。现在Android无处不在,所以很多时候Linux+Android成了标配。安卓(Android)是一种基于 Linux 内核(不包含 GNU 组件)的自由及开放源代码的移动操作系统。主要应用于移动设备 ,如智能手机和平板电脑 ,由美国Google公司和开放手机联盟领导及开发,可以理解为Android是Linux的一个发行版。     

      在操作系统领域,μCOS太简单,VxWorks太贵太专业,Windows不玩嵌入式了,IOS不开源,所以对于操作系统领域我们也只能玩Linux了,下图是软件开发平台使用的操作系统占比,在嵌入式领域Linux一家独大!

图3.1 嵌入式操作系统开发占比

Linux层次结构:

图3.2 Linux层次结构

       Shell:英文本意是外壳,Linux Shell 就是 Linux 操作系统的外壳(不能直接操作内核,对内核起保护作用),为用户提供使用操作系统的接口,是Linux系统与用户交互的重要接口,ls、cd、cp等命令就是shell命令。登录 Linux 系统或者打开 Linux 的终端,都将会启动 Linux 所使用的 Shell。 

        Lib(库):操作系统实现的标准C语言函数库。

        Config Files(配置文件):如IP配置信息等。

3.1.1 嵌入式Linux系统启动过程

       嵌入式Linux系统,就相当于一套完整的PC软件系统。了解嵌入式Linux系统可以类比我们比较熟悉的Windows系统,二者的软件系统是类似的,如下图所示(摘录自韦东山嵌入式Linux文章):

图3.3 Linux与Windows类比

       Windows电脑一开机的界面就是BIOS在自检,然后从硬盘上读入Windows,并启动它;对应于嵌入式Linux里的bootloader,bootloader去Flash上读入Linux内核,并启动。 启动Windows的目的是什么?当然是上网使用各种APP,这些APP在C盘、D盘上,所以Windows要先识别出C盘、D盘;在Linux下我们称为根文件系统,根文件系统里面含有APP。嵌入式Linux系统启动共分为四步:

       第一步:bootloader初始化,首先完成内存初始化、微处理器配置、时钟初始化等基本工作,然后搬运Linux 内核到特定内存,并调用Linux 内核初始化函数(一般是head_armv.s 中的第一条指令),启动内核,把CPU 的控制权交给内核代码。

       第二步:内核代码开始执行,初始化硬件,构建内存管理系统、进程管理系统、模块管理系统、中断管理系统等 Linux 各种重要功能系统,最后创建init 进程,并加载根文件系统,将控制权递交到根文件系统。

        第三步:内核挂载根文件系统(相当于将根文件系统内容解压缩)。

        第四步:执行文件系统中init程序(Linux初始化及shell登录等工作)。

        综上不难理解嵌入式Linux系统的组成:

        嵌入式Linux系统 = bootloader + linux内核 + 根文件系统(里面含有APP)。

3.1.2 bootloader

       bootloader(启动引导系统)是一个可执行文件如uboot.bin,是操作系统内核运行前运行的一段小程序。它的目的是启动内核,即自检设备、初始化硬件,从Flash加载操作系统内核到内存,并执行内核代码。所以需要有Flash外设的驱动能力,为了调试方便还会有网络功能。

       所以,可以认为 bootloader = 裸机集合,它就是一个复杂的单片机程序。bootloader有很多种,vivi、u-boot等,最常用的是u-boot。u-boot功能强大、源码比较多,对于编程经验不丰富、阅读代码经验不丰富的人,一开始可能会觉得难以掌握。

       u-boot的主要功能就是:启动内核。它涉及:读取内核到内存、设置启动参数、启动内核。在实际工作中,对于u-boot基本上是修修改改,甚至不改。但是u-boot本身是很复杂的,比如为了便于调试,它支持网络功能;有些内核是保存在FAT32分区里,于是它要能解析FAT32分区,读FAT32分区的文件。

UBOOT工作模式:
①启动加载模式是Bootloader的正常工作模式,嵌入式产品发布时,Bootloader必须工作在这种模式下,Bootloader 将嵌入式操作系统从FLASH中加载到SDRAM中运行,整个过程是自动的。
②下载模式就是Bootloader通过某些通信手段将内核映像或根文件系统映像等从PC机中下载到目标板的 FLASH 中。

        U-Boot 是一个主要用于嵌入式系统的引导加载程序,可以支持多种不同的计算机系统结构,u-boot在工作中基本用不到,理解u-boot的作用、会使用u-boot的命令就可以了。可参考博客【嵌入式】构建嵌入式Linux系统(uboot、内核、文件系统)_嵌入式linux设备-CSDN博客

3.1.3 Linux内核与内核源码

       内核是嵌入式Linux 系统的核心,负责管理系统的进程、内存、文件系统、网络和设备驱动等。如果把一个国家比作计算机系统,内核就是管理计算机资源的“政府”,内核中的每个功能模块相当于政府中的每个部门。内核也是一个可执行文件,编译内核生成可执行文件Image。Linux内核的最主要目的是去启动APP,APP保存在哪里?保存在“根文件系统”里。“根文件系统”又保存在哪里?在Flash、SD卡等设备里,甚至可能在网络上。所以Linux内核要有这些Flash、SD卡设备的驱动能力。不仅如此,Linux内核还有进程调度能力、内存管理等功能。内核就是一系列功能模块构成,包括系统调度、进程管理、内存管理、虚拟文件系统、网络协议栈、设备驱动、中断管理系统、系统调用接口

设备驱动:

       Linux内核中有大量代码(约60%)都在设备驱动程序中,它们以一种特定的模式管理底层硬件设备并以统一的接口向上层进程提供底层硬件的使用。

系统调用接口:

       系统调用层为用户空间提供了一套标准的系统调用函数来访问 Linux 内核,搭起了用户空间到内核空间的桥梁。用户空间和内核空间是程序执行的两种不同状态,我们可以通过“系统调用”和“硬件中断“来完成用户空间到内核空间的转移。

       根据内核的核心功能, Linux内核划分为5个子系统,其下包括各个子模块,如下图所示,详细描述可参考博客linux内核整体架构分析笔记_夏之七的博客-CSDN博客

图3.4 Linux内核子系统与子模块

        所以:Linux内核 = 驱动集合 + 进程调度 + 内存管理等。

       Linux内核源代码采用树形结构进行组织,非常合理地把功能相关的文件都放在同一个子目录下方法,使得程序更具可读性。Linux内核源代码包括三个主要部分:

       1)内核核心代码:包括图3.3linux内核各子系统和子模块,以及其他支撑子系统,如:电源管理、linux初始化等。

       2)非核心代码:例如库文件(因为 Linux 内核是一个自包含的内核,即内核不依赖其它的任何软件,自己就可以编译通过)、固件集合、 KVM(虚拟机技术)等。
       3)编译脚本、配置文件、帮助文档、版权说明等辅助性文件。 

       下图是4.1.15版本Linux内核源码目录结构,Linux 内核代码中广泛使用了数据结构和算法,其中最常用的两个是链表和红黑树:Linux内核实现了一套纯链表的封装,链表节点数据结构只有指针区而没有数据区,另外还封装了各种操作函数,如创建节点函数、删除节点函数、遍历节点函数等;红黑树( Red Black Tree)被广泛应用在内核的内存管理和进程调度中,用于将排序的元素组织到树中。具体可参考博客linux内核源码分析笔记_linux源代码分析_夏之七的博客-CSDN博客

图3.5 内核源码目录结构

include/ :内核头文件,需要提供给外部模块(例如用户空间代码)使用。
kernel/ : Linux 内核的核心代码,包含了 进程调度子系统,以及和进程调度相关的模块。
mm/ :内存管理子系统
fs/ ---- VFS 子系统。
net/ ---- 不包括网络设备驱动的网络子系统。
ipc/ ---- IPC(进程间通信)子系统。
arch// ---- 体系结构相关的代码,例如 arm, x86 等等。
arch//mach- ---- 具体的 machine/board 相关的代码。
arch//include/asm ---- 体系结构相关的头文件。
arch//boot/dts ---- 设备树( Device Tree)文件。
init/ ---- Linux 系统启动初始化相关的代码。
block/ ---- 提供块设备的层次。
sound/ ---- 音频相关的驱动及子系统,可以看作“音频子系统”。
drivers/ ---- 设备驱动
lib/ ---- 实现需要在内核中使用的库函数,例如 CRC、 FIFO、 list、 MD5 等。
crypto/ ----- 加密、解密相关的库函数。
security/ ---- 提供安全特性( SELinux)。
virt/ ---- 提供虚拟机技术( KVM 等)的支持。
usr/ ---- 用于生成 initramfs 的代码。
firmware/ ---- 保存用于驱动第三方设备的固件。
samples/ ---- 一些示例代码。
tools/ ---- 一些常用工具,如性能剖析、自测试等。
Kconfig, Kbuild, Makefile, scripts/ ---- 用于内核编译的配置文件、脚本等。
COPYING ---- 版权声明。
MAINTAINERS ----维护者名单。
CREDITS ---- Linux 主要的贡献者名单。
REPORTING-BUGS ---- Bug 上报的指南。
Documentation, README ---- 帮助、说明文档。

       arch目录下包含各体系结构特定的代码,用于屏蔽不同体系、平台之间的差异,如arm、X86等,在每个体系结构目录下通常都有:

图3.6 arch目录文件

       Linux内核源码树drivers目录很复杂,包含了各种外设的驱动,对嵌入式Linux开发而言,通常需要关注的目录如下图:

图3.7 drivers目录

3.1.4 根文件系统(rootfs)

了解根文件系统之前先了解init进程:
       由3.1.1小节可知Linux系统启动分为四步,第二步创建linux内核的0号进程,是所有进程的“祖宗”,其他进程都是直接或间接从它fork出的;随后挂载根文件系统,寻找文件系统下的相应init程序并执行,从内核态进程转变到用户态1号进程,也就是第四步根据inittab配置文件,加载应用程序,启动shell程序。

       根文件系统是内核启动所挂载的第一个文件系统,其本质是内核启动和运行所必须的使用的一些文件(如内核配置文件、库文件、Shell脚本等);Linux内核在根文件系统挂载之后会从根文件系统中把一些基本的初始化脚本和服务等加载到内存中去运行,即前面图3.2中描述的内核外层那些东西,要明白这些东西都不属于内核。

       根文件系统是一种特殊的文件系统,包含系统启动时所必须的目录和关键性的文件,以及使其他文件系统得以挂载(mount)所必要的文件,也就是加载其它文件系统的”根“,如果没有这个根,其它的文件系统也就没有办法进行加载。

      根文件系统被挂载到根目录下“/”上后,在根目录下就有根文件系统的各个目录,文件:/bin /sbin /mnt等,再将其他分区挂接到/mnt目录上,/mnt目录下就有这个分区的各个目录和文件。

正常来说,根文件系统至少包括以下目录:

/etc/:存储重要的配置文件。

/bin/:存储常用且开机时必须用到的执行文件。

/sbin/:存储着开机过程中所需的系统执行文件。

/lib/:存储/bin/及/sbin/的执行文件所需的链接库,以及Linux的内核模块。

/dev/:存储设备文件。

五大目录必须存储在根文件系统上,缺一不可。

       Linux文件系统(以后提到文件系统如不特别指明都表示根文件系统)一般有如下脑图所示几个目录:

图3.8 根文件系统目录

3.2 嵌入式Linux软件开发

        总体来说,嵌入式Linux软件分为应用层开发和底层开发。

       Linux应用编程指的是基于Linux操作系统的应用编程,在应用程序中通过调用系统调用API完成应用程序的功能和逻辑,应用程序运行于操作系统之上。应用编程门槛低,熟悉系统平台上的编程方法和流程,掌握好C语言、数据结构等基础知识就能开发APP。

       底层开发的很大一部分是驱动相关工作,Linux内核中大部分代码都是设备驱动程序,可以认为Linux内核由各类驱动构成。在实际工作中,我们从事的是“操作系统”周边的开发,并不会太深入学习、修改操作系统本身。操作系统具有进程管理、存储管理、文件管理和设备管理等功能,这些核心功能非常稳定可靠,基本上不需要我们修改代码。我们只需要针对自己的硬件完善驱动程序,驱动开发时必定会涉及其他知识,比如存储管理、进程调度。当深入理解了驱动程序后,也会加深对操作系统其他部分的理解。

       设备驱动与底层硬件直接打交道,编写Linux设备驱动要求工程师具有非常好的硬件基础,懂得SRAM、Flash、SDRAM、磁盘的读写方式,UART、I2C、USB等设备的接口以及轮询、中断、DMA 的原理,PCI总线的工作方式以及CPU的内存管理单元( MMU)等。无论做多复杂的系统最终都会落实到最底层的硬件控制,前面已经提及bootloader就是一个稍微复杂的裸板程序(不过工作中一般不涉及u-boot等的开发工作),裸机编程有助于理解硬件的构架、控制原理,掌握硬件操作之后就可以更好地入手驱动开发了。基于ARM和Linux的裸机实际上就是Linux下的单片机,掌握裸机开发后就会发现windows下的单片机开发是如此简单。

      Linux驱动开发非常庞大、繁琐,要想进行Linux驱动开发,必须要先移植Uboot、然后移植 Linux内核和根文件系统到开发平台上,这也就涉及到了完整的Linux系统移植,也就是要了解如何构建嵌入式Linux系统:这三者都能在网上下载到相应的源代码,但是源代码不可能下载编译后就能在你的系统上运行,需要修改,根据需求裁剪选配使之能精简的运行在开发板上。

       当我们把Uboot,Linux内核和根文件系统都在开发板上移植好了以后就可以按照操作系统内核的规定接口开始Linux驱动开发了。驱动变成连接硬件和内核的桥梁,对外呈现操作系统的API,不再给应用软件工程师直接提供接口,应用软件工程师需要通过系统调用或其它方式间接操作驱动设备接口。

      综上,就嵌入式Linux硬件平台下的软件开发来说,我们大可将编程分为三种:裸机编程、Linux驱动编程以及 Linux应用编程。一般把没有操作系统支持的编程环境称为裸机编程环境,例如单片机上的编程开发,编写直接在硬件上运行的程序,没有操作系统支持;狭义上Linux驱动编程指的是基于内核驱动框架开发驱动程序,驱动开发工程师通过调用Linux内核提供的接口完成设备驱动的注册,驱动程序负责底层硬件操作相关逻辑;而Linux应用编程(系统编程)则指的是基于Linux操作系统的应用编程,在应用程序中通过调用系统调用API完成应用程序的功能和逻辑,应用程序运行于操作系统之上。

       通常在操作系统下有两种不同的状态:内核态用户态,应用程序运行在用户态、而内核则运行在内核态。最基本的,当我们编写程序时,首先要明确嵌入式Linux分为用户空间内核空间。用户空间是应用程序运行的空间,内核空间就是操作系统和驱动程序运行的空间。这是从软件的角度来说的,对应于ARM芯片来说,就是芯片的不同“工作模式”。这两个空间是通过“地理隔离”实现互相完全独立的,它们各自的程序使用不同的内存地址区间,各自使用自己的头文件(有些头文件在两个空间内甚至是重名的,要注意区分)、各自调用属于自己空间的函数(哪怕实现的功能相同,比如printf()和printk()),而且不能互相直接访问(用指针也不行)。这是两套独立的知识体系,内核空间相关的东西有:Linux内核源码、内核编译和配置、内核移植、文件系统、设备驱动程序编写、中断编程等。用户空间相关的东西有:Shell、应用程序编译和调试、进程、线程、文件IO编程、网络通信相关、Qt图形界面编程等。

       做底层还是做应用,之间并没有一个界线,有底层经验,再去做应用,你会感觉很踏实。做驱动,也可以称为“做底层系统”,做好了这是通杀各行业。

3.2.1 裸机开发

       裸机开发是了解所使用的CPU最直接、最简单的方法,跟STM32单片机一样,裸机开发是直接操作CPU的寄存器。Linux驱动开发最终也是操作的寄存器,但是在操作寄存器之前要先编写一个符合Linux驱动的框架。同样一个点灯驱动,裸机可能只需要十几行代码,但是Linux下的驱动就需要几十行代码。

       裸机开发是连接Cortex-M(如STM32)单片机和Cortex-A(如 I.MX6ULL)处理器的桥梁,掌握裸机编程有助于STM32开发到Linux驱动开发的转换。而且通过裸机的学习可以掌握外设的底层原理,在以后进行Linux驱动开发的时候就只需要将精力放到Linux驱动框架上。

     ARM+Linux下的裸机开发学习有助于了解硬件操作原理,并且前面3.1.2小节已述及:bootloader = 裸机集合,它就是一个复杂的单片机程序,因此裸机学习可以为bootloader开发打基础。不过工作中bootloader开发基本涉及不到,再加上裸机系统有很大的局限性,所以裸机编程可能更多体现的是其基础学习价值。

3.2.2 系统移植

       系统移植就是在我们自己的开发板上构建一个Linux系统,前已述及Linux系统由bootloader、Linux内核、根文件系统三部分组成,这三者都能在网上下载到相应的源代码,但是这个源代码不可能下载编译后就能在你的系统上运行,需要很多的修改;而且硬件发生变化时,软件要进行裁剪,包括uboot裁剪、内核裁剪、根文件系统裁剪以适配硬件,裁剪做出最精简的系统。这个修改、裁剪的过程就叫移植,通俗来讲:移植就是让一个平台上的代码能够在其他平台上运行。Linux驱动开发的前提是开发板上需要运行Linux系统,所以掌握系统移植的流程至关重要。

       嵌入式Linux系统移植由四部分组成:搭建交叉开发环境、bootloader的选择和移植、linux内核的配置、编译和移植、根文件系统移植

3.2.2.1 搭建交叉开发环境

什么是交叉环境?

       在嵌入式开发中,第一个环节就是搭建环境:嵌入式比较特殊的是不能在目标机上开发程序,交叉环境是指在开发主机上(通常是开发人员的PC机)开发出能够在目标机(开发板)上运行的程序。对于一个原始开发板,为了能让它跑起来,我们必须要借助PC机进行烧录程序等相关工作。

为什么需要交叉环境?

       有两个原因:一是由于嵌入式系统资源匮乏,一般不能像PC一样安装本地编译器和调试器,不能在本地编写、编译和调试自身运行的程序,而需借助其它系统如PC来完成这些工作,这样的系统通常被称为宿主机。二是嵌入式系统MCU体系结构和指令集不同,因此需要安装交叉编译工具进行编译,这样编译的目标程序才能够在相应的平台上比如ARM上正常运行。

       宿主机通常是Linux系统,并安装交叉编译器、调试器等工具;宿主机也可以是Windows系统,安装嵌入式Linux集成开发环境。在宿主机上编写和编译代码,通过串口、网口或者硬件调试器将程序下载到目标系统里面运行,系统示意图如图1所示。

图3.9 嵌入式Linux开发环境

        开发主机与目标板(开发板)的三种链接介质对应的硬件介质,还需有相应的软件“介质”支持:

①对于串口:通常用的有串口调试助手,putty工具等;

②对于USB线:当然必须要有USB的驱动,一般芯片公司会提供;

③对于网线:则必须要有网络协议支持才可以,常用的服务主要有两个,tftp服务和nfs服务;还有samba服务(Windows主机和Linux虚拟机之间的文件共享)。

交叉编译器

       交叉开发环境必然会用到交叉编译工具,通俗地讲交叉编译就是在一种平台上编译出能运行在体系结构不同的另一种平台上的程序,开发主机PC平台(X86CPU)上编译出能运行在以ARM为内核的CPU平台上的程序,编译得到的程序在X86CPU平台上是不能运行的,必须放到ARM-CPU平台上才能运行,虽然两个平台用的都是Linux系统。

       相对于交叉编译,平常做的编译叫本地编译,也就是在当前平台编译,编译得到的程序也是在本地执行。用来编译这种跨平台程序的编译器就叫交叉编译器,相对来说,用来做本地编译的工具就叫本地编译器。所以要生成在目标机上运行的程序,必须要用交叉编译工具链来完成。
为什么叫交叉工具链?

       因为程序不能光编译一下就可以运行,还得进行汇编和链接等过程,同时还需要进行调试,对于一个很大工程,还需要进行工程管理等等,所以,这里 说的交叉编译工具是一个由编译器、连接器和解释器组成的综合开发环境,交叉编译工具链主要由binutils(主要包括汇编程序as和链接程序ld)、gcc(为GNU系统提供C编译器)和glibc(一些基本的C函数和其他函数的定义) 3个部分组成。

       构建安装交叉编译器和系统移植有很多相似的地方,交叉开发工具是一个支持很多平台的工具集的集合(类似于Linux源码),然后我们只需从这些工具集中找出跟我们平台相关的工具就行了,如何制作交叉工具链此处不做详述。

3.2.2.2 bootloader的选择和移植

       3.1.2小节介绍了什么是bootloader及其作用,bootloader有很多,常用的就是U-boot,U-boot是一个裸机代码,可以看作是一个裸机综合例程。

       U-boot代码有三种:uboot官方的uboot代码、半导体厂商的uboot代码、开发板厂商的uboot代码。但是我们一般不会直接用uboot官方的U-Boot源码的,因为支持太弱了。uboot官方的uboot源码是给半导体厂商准备的,半导体厂商会下载uboot官方的uboot源码,然后将自家相应的芯片移植进去。也就是说半导体厂商会自己维护一个版本的uboot,这个版本的uboot相当于是他们定制的。既然是定制的,那么肯定对自家的芯片支持会很全,虽然uboot官网的源码中一般也会支持他们的芯片,但是绝对是没有半导体厂商自己维护的uboot全面。所以最常用的就是半导体厂商或者开发板厂商的uboot,如果你用的半导体厂商的评估板,那么就使用半导体厂商的uboot,如果你是购买的第三方开发板,比如正点原子的I.MX6ULL开发板,那么就使用正点原子提供的uboot源码(也是在半导体厂商的uboot上修改的)。当然了,也可以在购买了第三方开发板以后使用半导体厂商提供的uboot,只不过有些外设驱动可能不支持,需要自己移植,这个就是我们常说的uboot移植。

       半导体厂商会将uboot移植到他们自己的原厂开发板上,测试好以后就会将这个uboot发布出去,这就是大家常说的原厂BSP包。我们一般做产品的时候就会参考原厂的开发板做硬件,然后在原厂提供的BSP包上做修改,将uboot或者linux kernel移植到我们的硬件上。这个就是uboot移植的一般流程,即:

       ①在uboot中找到参考的开发平台,一般是原厂的开发板。

       ②参考原厂开发板移植uboot到我们所使用的开发板上。

      以将NXP官方的uboot移植到正点原子的I.MX6ULL开发板上为例,如果直接编译NXP官方uboot源码并在开发板上启动,虽然可能uboot启动正常,通过命令检查SD和EMMC驱动也是正常的,但是uboot启动的时候可能提示网络异常(ping不通主机,因为硬件网络芯片复位引脚和NXP官方开发板不一 样,需要修改驱动),LCD显示也异常(LCD默认驱动支持分辨率不符,也需要修改驱动)。

       NXP官方uboot中默认都是NXP自己的开发板,虽说也可以直接在官方的开发板上直接修改,但最好还是在uboot中添加自己的的开发板或者开发平台。涉及到的相关工作有:添加开发板默认配置文件、添加开发板对应头文件(文件中基本都是“CONFIG_”开头 的宏定义,这也说明此头文件的主要功能就是配置或者裁剪uboot,如果需要某个功能的话就在里面添加这个功能对应的 CONFIG_XXX宏即可,如果不需要某个功能的话就删除掉对应的宏即可。其中以CONFIG_CMD开头的宏都是用于使能相应命令的,其他的以CONFIG开头的宏都是完成一些配置功能的)、添加开发板对应的板级文件、修改U-boot图形界面配置文件,然后就是新建shell脚本编译编译uboot(生成u-boot.bin、u-boot.imx等文件)。

       最后就是修改驱动,重点注意硬件原理图管脚配置及参数配置,如LCD驱动就注意LCD的IO管脚及LCD分辨率、像素格式等参数;网络驱动就注意复位引脚、使能PHY驱动、PHY的ID、PHY地址等。

3.2.2.3 Linux内核移植

       NXP官方提供的Linux内核源码肯定是可以在其生产的MCU处理器(例如I.MX6ULL)为主板的开发板上运行的,只不过开发板外设配置也就是设备树和内核配置不同需要修改,以适应我们的开发板硬件设计和功能需求。与uboot一样,内核移植就是在Linux内核中添加自己的开发板,Linux内核是以模块的方式来组织这个操作系统的,添加和删除相应的模块即裁剪内核就是内核移植的主要工作,个人理解这应该就是所谓的Linux内核移植吧。

       根文件系统缺失或者根文件系统路径设置错误,内核启动时会提示内核崩溃(Kernel panic),因为VFS(虚拟文件系统)不能挂载根文件系统,根文件系统目录不存在。即使根文件系统目录存在,如果根文件系统目录里面是空的依旧会提示内核崩溃。 这个就是根文件系统缺失导致的内核崩溃,但是内核是启动了的,只是根文件系统不存在而已。    

      Linux内核的源码树目录下一般都会有两个文件:Kconfig和Makefile。分布在各目录下的Kconfig构成了一个分布式的内核配置数据库,每个Kconfig分别描述了所属目录源文件相关的内核配置菜单。每个目录都会存放功能相对独立的信息,在每个目录中会存放各个不同的模块信息,比如在/dev/char/目录下就存放了所有字符设备的驱动程序,而这些程序代码在内核中是以模块的形式存在的,也就是说当系统需要这个驱动的时候,会把这个驱动以模块的方式编译到系统的内核中,编译分为静态编译和动态编译,静态编译内核体积比动态编译的体积要大。每个目录的Kconfig文件描述了所属目录源文件相关的内核配置菜单,有其特殊的语法格式,图形化界面的文字正是从这个文件中读取出来的,如果把这个文件中的相应目录文件的信息全部删除,那么在图形化界面中将看不到该模块的信息,因此也不能进行模块的配置。在内核配置make menuconfig时,系统会自动从Kconfig中读出配置菜单,用户配置完后保存到.config(在顶层目录下生成)中。在内核编译时,主Makefile调用这个.config,(.config的重要性就体现在,它保存了我们的所有的配置信息,是我们选取源代码并且进行编译源代码的最终依据!!!)就知道了用户对内核的配置情况。

       上面的内容说明:Kconfig就是对应着内核的配置菜单。假如要想添加新的驱动到内核的源码中,可以通过修改Kconfig来增加对我们驱动的配置菜单,这样就有途径选择我们的驱动,假如想使这个驱动被编译,还要修改该驱动所在目录下的Makefile。因此,一般添加新的驱动时需要修改的文件有两种,即:Kconfig 和相应目录的Makefile(注意不只是两个),系统移植的重要内容就是给内核添加和删除相应的模块,因此主要修改的内核文件就是Kconfig 和相应目录的Makefile这两个文件。

       每个板子都有其对应的默认配置文件,这些默认配置文件保存在arch/arm/configs目录下,我们需要拷贝标准版配置文件得到跟开发板相关的配置信息,进行"make menuconfig"时系统会根据我们选取的平台信息自动选取相关的代码和模块,因此只需要进入图形化配置界面保存配置信息即可,系统会把跟移植平台相关的所有配置信息全部保存在顶层目录的.config文件中。输入命令"make menuconfig"打开图形化配置界面选择使能或者禁能某个驱动或者模块,使能以后config文件对应的宏(类似为宏)就会赋值为y,例如使能LAN8720A驱动之后,config中:CONFIG_SMSC_PHY = y(Y:将该功能编译进内核;N:不将该功能编译进内核;M:将该功能编译成模块,可以再需要时动态插入到内核中),内核编译时就会编译smsc.c文件,注意图形化配置后一定要保存配置文件。然后就是添加适合开发板设备树文件,进入目录arch/arm/boot/dts中,复制一份合适的.dts文件重命名为开发板的设备树文件,修改设备树源码以适配开发板硬件,例如修改网络驱动,因为后续做驱动开发的时候要用到网络调试驱动,所以必须在内核移植时就要把网络驱动调好:如果PHY芯片的复位引脚及时钟引脚与复制的设备树源码中引脚不一致就要修改设备树对应网络PHY节点引脚配置,还有PHY地址,最后配置Linux内核时使能开发板所选用的PHY芯片驱动,若内核源码驱动目录没有该驱动则需要把厂家提供或者自己开发的驱动编译进内核。

       内核移植修改相应的驱动:像NAND Flash、EMMC、SD卡等驱动官方的Linux内核都是已经提供好了,基本不会出问题。重点是网络驱动,因为 Linux 驱动开发一般都要通过网络调试代码,所以一定要确保网络驱动工作正常。如果是处理器内部MAC+外部PHY这种网络方案的话,一般网络驱动都很好处理,因为在Linux内核中是有外部PHY通用驱动的。只要设置好复位引脚、PHY地址信息基本上都可以驱动起来。

       在Linux内核中添加完开发板的配置文件及设备树文件后就可以开始编译内核了,Linux内核的顶层Makefile文件控制内核的编译流程,还有子Makefile,一定要了解之。修改顶层目录下的Makefile主要是修改平台的体系架构和交叉编译器,这两个变量值会直接影响顶层Makefile的编译行为,即选择编译哪些代码,用什么编译器进行编译。Linux内核编译完成以后会在 arch/arm/boot目录下生成Linux内核镜像文件zImage,如果使用设备树的话还会在arch/arm/boot/dts目录下生成开发板对应的.dtb(设备树)文件。Linux 内核启动以后需要根文件系统,否则会崩溃,所以确定Linux内核移植成功以后就要开始根文件系统的构建。

       分布在Linux内核源代码中的Makefile,定义Linux内核的编译规则。顶层目录的Makefile管理整个Linux内核的配置编译。顶层Makefile递归的进入到内核的各个子目录中,分别调用位于这些子目录中的 Makefile,至于到底进入哪些子目录,取决于内核的配置。在顶层 Makefile中有一句语句包含了特定CPU体系结构下的Makefile,这个Makefile中包含了平台相关的信息。位于各个子目录下的Makefile同样也根据.config给出的配置信息,构造出当前配置下需要的源文件列表。Makefile 的作用是根据配置的情况,构造出需要编译的源文件列表,然后分别编译,并把目标代码链接到一起,最终形成 Linux 内核二进制文件。

3.2.2.4 根文件系统移植

      文件系统的制作和移植是系统移植的最后一道工序,了解分析Linux内核启动流程就会知道Linux内核最终是需要和根文件系统打交道的,需要挂载根文件系统,并且执行根文件系统中的init程序,以此来进去用户态。

       类似于Windows下的C、D、E等各个盘,Linux系统也可以将磁盘、Flash等存储设备划分为若干个分区,在不同分区存放不同类别的文件。与Windows的C盘类似,Linux一样要在一个分区上存放系统启动所必需的文件,比如内核映象文件(在嵌入式系统中,内核一般单独存放在一个分区中)、内核启动后运行的第一个程序(init)、给用户提供操作界面的shell程序、应用程序所依赖的库等。这些必需的、基本的文件,合称为根文件系统,它们存放在一个分区中。Linux系统启动后首先挂接这个分区——称为挂接(mount)根文件系统。其他分区上所有目录、文件的集合,也称为文件系统,比如我们常说:“挂接硬盘第二个分区”、“挂接硬盘第二个分区上的文件系统”。

       Linux中并没有C、D、E等盘符的概念,它以树状结构管理所有目录、文件,其他分区挂接在某个目录上——这个目录被称为挂接点或安装点(mount point),然后就可以通过这个目录来访问这个分区上的文件了。比如根文件系统被挂接在根目录“/”上后,在根目录下就有根文件系统的各个目录、文件:/bin、/sbin、/mnt等;再将其他分区挂接到/mnt目录上, /mnt目录下就有这个分区的各个目录、文件。

      在一个分区上存储文件时,需要遵循一定的格式,这种格式称为文件系统类型,比如fat16、fat32、ntfs、ext2、ext3、jffs2、yaffs等。除这些拥有实实在在的存储分区的文件系统类型外,Linux还有几种虚拟的文件系统类型,比如proc、sysfs等,它们的文件并不存储在实际的设备上,而是在访问它们时由内核临时生成。比如proc文件系统下的uptime文件,读取它时可以得到两个时间值(用来表示系统启动后运行的时间秒数、空闲的时间秒数),每次读取时都由内核即刻生成,每次读取结果都不一样。

       Linux根文件系统目录结构描述见3.1.4小节,所谓制作根文件系统,就是创建3.1.4小节提到的各种目录,并且在里面创建各种文件。比如在/bin、/sbin目录下存放各种可执行程序,在/etc目录下存放配置文件,在/lib目录下存放库文件。

       根文件系统里面就是一堆的可执行文件和其他文件组成的,BusyBox就是负责“收集”这些文件的,它是一个集成了大量的Linux命令和工具的软件,像ls、mv、ifconfig等命令BusyBox都会提供。一般下载BusyBox的源码,然后配置BusyBox,Busybox集合了几百个命令,在一般系统中并不需要全部使用。可以通过配置Busybox来选择这些命令、定制某些命令的功能(选项)、指定Busybox的连接方法(动态连接还是静态连接)、指定Busybox的安装路径。跟uboot、内核移植一样,Busybox同样具有图形化配置界面,编译之前要修改根目录的Makefile。

       BusyBox编译完成后在根文件系统目录下就会有bin、sbin、usr三个目录,此时的根文件系统还不能使用,还需要完善:

       ①向根文件系统添加lib库,Linux中的应用程序一般都是需要动态库的,当然你也可以编译成静态的,但是静态的可执行文件会很大。如果编译为动态的话就需要动态库,所以我们需要向根文件系统中添加动态库。在rootfs中创建一个名为“lib”的文件夹,可以将安装的交叉编译器里面的库文件拷贝至lib目录下。还有usr/lib目录下的库文件也需要添加。

       ②构建etc目录,init进程根据/etc/inittab文件来创建其他子进程,比如调用脚本文件配置IP地址、挂接其他文件系统,最后启动shell等。etc目录下的内容取决于要运行的程序,一般只需要创建3个文件:etc/inittab、etc/init.d/rcS、etc/fstab。

       ③构建dev目录。

       ④构建其他目录,如proc、mnt、tmp、sys、root等,可以是空目录。

        一般我们在Linux驱动开发的时候都是通过nfs挂载根文件系统的,当产品最终上市开卖的时候才会将根文件系统烧写到EMMC或者NAND中。所以开发阶段可以在nfs服务器目录中创建一个子目录(名字随意)。

        以上内容参考:构建Linux根文件系统-腾讯云开发者社区-腾讯云 (tencent.com) 。里面有init进程启动的详细描述。

3.2.3 嵌入式Linux驱动开发

       移植系统并不是最终的目的,最终的目的是开发产品,做项目,这些都要进行驱动程序的开发,移植系统是驱动开发的前提。Linux驱动程序= 驱动框架 + 硬件操作,有单片机基础的人,对硬件操作比较熟悉了,把重点放在驱动框架上就可以。高能预警:驱动框架可不简单,对于LED来说是简单,但是还有更复杂的驱动程序,它要考虑“通用”,这很要命。本人曾经在面试的时候就被提问驱动开发的关键是什么?答案就是驱动框架Linux驱动开发重点是学习其驱动框架,在工作中基本上只是移植、修改驱动,一定是参考同类驱动程序(开卷有益)。

      大部分Linux驱动初学者都是从STM32转过来的,Linux驱动开发和STM32开发区别很大,比如Linux没有MDK、IAR这样的集成开发环境,需要我们自己在Ubuntu下搭建交叉编译环境。直接上手Linux驱动开发可能会因为和STM32巨大的开发差异而打击学习信心。

       会Linux底层的人肯定会单片机,会单片机的人不一定会Linux。

       Linux中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。其中字符设备驱动是最大的一类驱动,因为字符设备最多,从最简单的点灯到I2C、SPI、音频等都属于字符设备驱动的类型。块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都给我们编写好了,大多数情况下都是直接可以使用的。所谓的块设备驱动就是存储器设备的驱动,比如 EMMC、NAND、SD卡和U盘等存储设备,因为这些存储设备的特点是以存储块为基础,因此叫做块设备。网络设备驱动就更好理解了,就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。一个设备可以属于多种设备驱动类型,比如USB WIFI,其使用USB接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。

        编写Linux设备驱动要求工程师有一定的Linux内核基础,虽然并不要求工程师对内核各个部分有深入的研究,但至少要明白驱动与内核的接口。尤其是对于块设备、网络设备、 Flash 设备、串口设备等复杂设备,内核定义的驱动体系结构本身就非常复杂。学习驱动时必定会涉及其他知识,比如存储管理、进程调度。当你深入理解了驱动程序后,也会加深对操作系统其他部分的理解,要成为该领域的高手,一定要深入理解Linux操作系统本身,要去研读它的源代码。做“驱动”也就是做“底层系统”,做好了通杀各行业。

3.2.3.1 Linux下的应用程序是如何调用驱动程序的?
图3.10 Linux应用程序对驱动程序的调用流程


       在Linux中一切皆文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫做/dev/led 的驱动文件,此文件是led灯的驱动文件。应用程序使用open函数来打开文件/dev/led,使用完成以后使用close函数关闭/dev/led 这个文件。open和close就是打开和关闭 led驱动的函数,如果要点亮或关闭led,那么就使用write函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开led的控制参数。如果要获取led灯的状态,就用read函数从驱动中读取相应的状态。

       应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write和read等这些函数是由C库提供的,在Linux系统中,系统调用作为C库的一部分。当我们调用open函数的时候流程如下图所示:

图3.11 open函数调用流程
3.2.3.2 什么是驱动程序的框架?

       裸机或者STM32关于驱动的开发就是初始化相应的外设寄存器,在Linux驱动开发中肯定也是要初始化相应的外设寄存器,这个是毫无疑问的。只是在Linux驱动开发中我们需要按照其规定的框架来编写驱动,所以说Linux驱动开发重点是学习其驱动框架。下面以字符设备驱动开发为例:

      ①应用程序使用到的函数在具体驱动程序中都有与之对应的函数, 比如应用程序中调用了open这个函数,那么在驱动程序中也得有一个名为open的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在Linux内核文件include/linux/fs.h中有个叫做file_operations的结构体,此结构体就是Linux内核驱动操作函数集合。(驱动程序的核心)

       ②构造好file_operations结构体后通过register_chardev函数注册字符设备(告诉内核),参数为主设备号,可传入0就会自动分配主设备号(返回主设备号,相当于有一个数组存放各个驱动的file_operations结构体,编号就是主设备号,调用此函数传入0会遍历数组返回空闲的号)。输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号,可以选择未被使用的作为主设备号。

      major:主设备号,Linux下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号详解自行在网上查阅。

       ③一般字符设备的注册在驱动模块的入口函数xxx_init 中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行。

       ④为了让系统自动创建设备结点,还可以创建类。

       驱动程序不可以直接使用寄存器的物理地址,先把物理地址使用ioremap函数把物理地址映射得到虚拟地址,然后访问虚拟地址操作硬件。

       Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个Linux代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux内核中,当然也可以不编译进Linux内核中,具体看自己的需求。

3.2.3.3 驱动进化之路(三种写法)

       传统的设备驱动编写比较简单,都是对IO进行最简单的读写操作。像I2C、SPI、LCD等这些复杂外设的驱动就不能这么去写了,Linux系统要考虑到驱动的可重用性,因此提出了驱动的分离与分层这样的软件思路,在这个思路下诞生了驱动开发者最常打交道的platform设备驱动,也叫做平台设备驱动。基于总线、设备和驱动这样的驱动框架,Linux内核提出来platform这个虚拟总线,相应 的也有platform设备和platform驱动。其中总线不需要驱动程序员去管理,这个是Linux内核提供的,我们在编写驱动的时候只要关注于设备和驱动的具体实现即可。在没有设备树的Linux内核下,我们需要分别编写并注册 platform_device 和 platform_driver,分别代表设备和驱动。在使用设备树的时候,设备的描述被放到了设备树中,因此 platform_device 就不需要我们去编写了,我们只需要实现platform_driver即可。最新的Linux内核已经支持了设备树,因此在设备树下如何编写 platform驱动就显得尤为重要。

       传统的设备驱动写法使用哪个引脚,怎么操作引脚,都写死在代码中,最简单,不考虑扩展性,可以快速实现功能,修改引脚时,需要重新编译。

       总线设备驱动模型引入platform_device/platform_driver,将“资源”与“驱动”分离开来。 代码稍微复杂,但是易于扩展。冗余代码太多,修改引脚时设备端的代码需要重新编译。更换引脚时xxx_drv.c 基本不用改,但是需要修改 xxx_dev.c。

       设备树驱动模型通过配置文件-设备树来定义“资源”,代码稍微复杂,但是易于扩展。无冗余代码,修改引脚时只需要修改dts文件并编译得到dtb文件,把它传给内核,无需重新编译内核/驱动。

3.2.3.4 设备树

       新版本的Linux中,ARM相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出的CPU其驱动开发也基本都是基于设备树的,所以了解设备树尤为重要。

       众所周知操作系统一直在不断的更新和发展,而在Linux驱动的架构上面也是不断的进步和完善。在早期的Linux内核和ARM架构中并没有采用设备树。在没有设备树的时候Linux是通过大量的arch/arm/mach-xxx 和arch/arm/plat-xxx文件夹来描述对应平台的板机信息。而随着智能终端设备,智能手机的发展,每年新出的ARM架构芯片都有数百款,从而导致Linux内核中的板级信息文件过多,使得Linux内核虚胖。当Linux之父linus看到ARM社区向Linux内核添加了大量“无用”、冗余的板级信息文件,不禁发出了一句“This whole ARM thing is a f*cking pain in the ass”。从此以后 ARM社区就引入了PowerPC等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从Linux中分离出来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树。这个通用的文件就是.dtsi文件,类似于C语言中的头文件。一般用.dts描述板机信息(也就是开发板上有多少个IIC设备、SPI设备等),dtsi描述SOC级信息(也就是SOC有几个CPU、主频是多少、多少个外设控制寄存器信息等)。
       DTS文件的主要功能就是按照树状结构来描述板子上的设备信息,DTS文件是有相应的语法规则要求的,每一个设备都是一个节点,叫做设备节点,每个节点都是通过一些属性信息来描述节点信息,属性就是键值对,设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,其他详细的设备树节点属性含义可网上查阅相关资料。和C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi,例如一个.dts文件头部有以下内容:

#include <dt-bindings/input/input.h> //引用了“input.h”这个.h头文件
#include "imx6ull.dtsi" //引用.dtsi头文件

       设备树在编译内核时会被编译为.dtb文件被Linux内核解析,也可单独编译设备树文件。可以了解一下使用设备树之前和使用设备树之后的设备匹配方法,还有Pinctrl子系统、I2C子系统、GPIO子系统相关知识,此处不做介绍。       

3.2.4 嵌入式Linux应用开发

       应用程序入门门槛低,基本上练习好基本功,熟练使用C语言即可胜任相关工作,数据结构、算法是必备,然后凭兴趣选择数据库、网络编程等等进行深入钻研。如果想学习驱动开发,具备应用开发的基础也会让你得心应手。应用开发与驱动开发是两个不同的方向,将来在工作当中也会负责不同的任务、解决不同的问题,应用程序负责处理应用层用户需求、逻辑,而驱动程序负责内核层硬件底层操作,Linux操作系统向应用层提供了封装好的 API 函数(也称为系统调用),应用层通过系统调用可以完成对系统硬件设备的操作、控制;所以应用开发并不要求会驱动开发,不过如果熟悉驱动开发必然对应用开发是有一定帮助的。

       应用开发以上位机,界面化开发为主,现在的趋势是图形应用程序的开发,而图形应用程序中用得最多的还是qt/e函数库。应用程序分为C、C++、Android,嵌入式LinuxC应用开发需要掌握linux下标准I/O的用法,学会linux下文件I/O的用法,Linux系统调用概念(参照2.2.4描述),熟悉库的制作和使用;掌握进程和线程编程,掌握进程间的通信机制,熟悉多线程编程、多线程间的同步和互斥等。高阶需掌握网络数据通信过程,TCP/IP协议,Socket编程、TCP网络编程、UDP网络编程,QT编程。网络编程最为重要,同样也是最难的。Linux 系统是依靠互联网平台迅速发展起来的,所以它具有强大的网络功能支持,也是Linux系统的一大特点,Linux系统向应用层提供了丰富的API函数,例如socket接口及其相关函数,需了解如何使用socket接口进行网络编程开发。

       在嵌入式Linux系统中,我们编写的应用程序通常需要与硬件设备进行交互、操控硬件,比如点亮开发板上的一颗LED灯、获取按键输入数据、在LCD屏上显示摄像头采集的图像、应用程序向串口发送数据或采集串口数据、网络编程等,应用开发就是编写应用程序控制开发板上的各种硬件外设;Linux系统下,一切皆文件,也包括各种硬件设备,所以在Linux系统下,各种硬件设备是以文件的形式呈现给用户层,应用程序通过对文件的I/O操作来控制硬件设备。

       再次强调,嵌入式LinuxC应用开发的基础就是C语言, C语言是嵌入式的重中之重,Linux操作系统就是用C实现的,包括安卓系统的底层也是C语言实现的,基础不牢,地动山摇,一定要打好基础。C语言核心知识点:三大语法结构、数据类型、函数、结构体、指针、文件操作;重点是指针,结构体,文件的处理,数组等。

嵌入式C语言基础脑图 :

图3.12 C语言

嵌入式Linux应用开发脑图:

图3.13嵌入式Linux应用层开发

嵌入式Linux底层开发脑图:

图3.14 嵌入式Linux底层开发

       另外说一下数据结构,数据结构是程序员必修课之一,掌握数据结构中的线性表、栈和队列的用法及编程实现。掌握二叉树的递归遍历、层次遍历、及递归如何转非递归。掌握各种查找算法及编程实现,掌握各种排序算法及实现。对于嵌入式来说,数据结构的要求比传统互联网要低,但是数据结构中的各种链表、二叉树在操作系统,在Linux内核和驱动中会经常出现,掌握数据结构的知识,对于我们理解操作系统复杂代码比较有帮助。数据结构核心知识点:数组、队列、链表、堆栈、树、图、散列等。对于数据结构学习,前5个是必备学习的,可能在刚开始学习时感觉不到作用在哪里,但是随着接触到嵌入式底层设计及算法设计的时候,才会恍然大悟。

3.2.5 单片机和Linux编程区别

       对于从单片机转行到嵌入式Linux的开发者,套着单片机的开发经验,往往会比较迷惑,对于LED的操作这类简单的应用,单片机的操作可能只要几行代码就能实现功能,但在嵌入式Linux中不允许应用开发人员直接去操作寄存器,要想点灯必须通过驱动程序来访问寄存器。以点灯为例,无论是单片机还是Linux,要做的事情都一样:看原理图确定引脚是哪一个输出什么电平,看芯片手册确定要怎么操作寄存器;但是怎么编写程序单片机和linux有很大不同。在单片机程序里,没有应用程序、驱动程序的概念,很可能一个人包揽了硬件设计、模块调试(或称之为驱动)、功能开发(或称之为应用)的全部活,在Linux中应用程序和驱动程序是分开的而。很多单片机项目严重依赖于硬件,换一个芯片后就要重写一套代码。

四、前端、后端

前端概念:

       前端即网站前台部分,运行在PC端,移动端等浏览器上展现给用户浏览的网页。随着互联网技术的发展,HTML5,CSS3,前端框架的应用,跨平台响应式网页设计能够适应各种屏幕分辨率,完美的动效设计,给用户带来极高的用户体验。

       前端开发主要涉及网站和App,用户能够从App屏幕或浏览器上看到东西。通俗讲: 前端是做展示的,制作网页的,你看到的页面都是前端做的。

后端概念:

       后端是指用户看不见的东西,通常是与前端工程师进行数据交互及网站数据的保存和读取,相对来说后端涉及到的逻辑代码比前端要多的多,后端考虑的是底层业务逻辑的实现,平台的稳定性与性能等。通俗讲:后端是运行业务逻辑的,产生数据的,前端展示的数据都是从后端拿过来的。

       后端开发即“服务器端”开发,主要涉及软件系统“后端”的东西。比如,用于托管网站和App数据的服务器、放置在后端服务器与浏览器及App之间的中间件,它们都属于后端。简单地说,那些你在屏幕上看不到但又被用来为前端提供支持的东西就是后端。

前端、后端工作方向:

       从应用范围来看,前端开发不仅被常人所知、且应用场景也要比后端广泛的太多太多。目前电脑端仍是前端一个主要的领域,主要分为面向大众的各类网站,如新闻媒体、社交、电商、论坛等和面向管理员的各种 CMS (内容管理系统)和其它的后台管理系统。还有WEB开发、Game(游戏)开发、桌面应用软件等都是前端应用领域。

       前端工程师主要的工作职责分为三大部分,分别是传统的Web前端开发,移动端开发和大数据呈现端开发。Web前端开发主要针对的是PC端开发任务;移动端开发则包括Android开发、iOS开发和各种小程序开发,在移动互联网迅速发展的带动下,移动端的开发任务量是比较大的,随着5G标准的落地,未来移动端的开发任务将得到进一步的拓展;大数据呈现则主要是基于已有的平台完成最终分析结果的呈现,呈现方式通常也有多种选择,比如大屏展示等。
后端工程师的主要职责也集中在三大部分,分别是平台设计、接口设计和功能实现。平台设计主要是搭建后端的支撑服务容器;接口设计主要针对于不同行业进行相应的功能接口设计,通常一个平台有多套接口,就像卫星导航平台设有民用和军用两套接口一样;功能实现则是完成具体的业务逻辑实现。

前端后端比较
前端后端
技能前端Web开发者需要掌握HTML、CSS和JavaScript。后端开发者需要懂数据库、服务器、API等。
职责前端开发者负责设计网站的外观。后端开发者负责构建数据库架构,为前端提供支持。
独立性前端不能作为单独的服务提供,除非它是一个静态的网站。后端可以作为BaaS(后端即服务,Backend as a Service)提供。
目标前端开发者的目标是确保所有用户都可以访问网站或APP。后端开发者的目标是围绕前端开发应用程序,并为前端提供支持,并确保整个网站或App正常运行。

       综上大概可以知道嵌入式Linux与前后端开发应该是隶属于不同的领域。看到网上说前端比较简单,但是相应的发展不如后端、嵌入式。

  • 34
    点赞
  • 144
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值