BootLoader
BootLoader是嵌入式系统中一种软件程序,它在系统加电或复位后最先执行,负责初始化硬件设备、设置系统环境,并最终引导加载操作系统内核或用户指定的应用程序。连接了硬件启动与高级软件运行之间的环节,确保系统能够从一个裸机的状态过渡到一个完整、可操作的运行环境。
BootLoader 的主要功能和作用:
硬件初始化:开启和配置基本的硬件模块,如CPU、时钟、内存控制器、中断控制器、串口、GPIO等,使它们进入工作状态。设置堆栈、中断向量表等关键数据结构,为后续软件执行做好准备。
内存管理:建立内存空间映射图,识别可用的RAM区域及其大小,为操作系统内核分配合适的运行空间。对于使用MMU的系统,可能还需要设置内存分页和映射规则。
引导加载操作系统:从非易失性存储器(如Flash、EEPROM、NAND/NOR Flash等)中读取并验证操作系统的内核映像。
将内核映像加载到RAM中指定的位置,并按照内核所需的特定格式设置启动参数和环境变量。
U-Boot的启动流程
- 硬件初始化:如CPU、内存、串口等。
-
载入内核:将操作系统内核从存储介质(如Flash、SD卡读取到RAM中。
-
设置启动参数(bootargs):为内核准备启动参数,比如控制台配置、内存布局、根文件系统位置等。
-
调用内核(bootm):在准备好一切后,U-Boot将控制权转交给内核,跳转到内核的入口点开始执行。
Kernel的裁剪、编译
裁剪通常通过配置选项(make menuconfig
)进行,可以选择性地包含或排除特定的模块和功能。编译过程则使用make
命令
Linux驱动程序
驱动程序的分类
- 字符设备驱动:因为软件操作设备是是以字节为单位进行的,是按照字节流进行读写操作的一种设备。典型的如LCD、蜂鸣器、SPI、触摸屏等驱动,都属于字符设备驱动的范畴。大部分的驱动程序都是属于字符设备驱动。
- 块设备驱动:块设备驱动是相对于字符设备驱动而定义的,因为块设备被软件操作时,是以块为单位进行操作的(块指的是多个字节组成一个块)。块设备大多指的都是各种存储类类设备,比如EMMC、SD卡、NANDFlash、U盘等等。
- 网络设备驱动:专门针对网络设备而设计的一种驱动,不管是有线还是无线网络,都属于网络设备驱动。
一个设备可以属于多种设备驱动类型,比如 USB WIFI设备,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。
驱动程序实际上起到承上启下的作用,上承应用程序,对下则实现了具体的硬件操作。应用程序位于用户空间,驱动程序位于内核空间。Linux系统规定,用户空间不可以直接调用内核空间的函数。所以必须经过系统调用,应用程序才可以调用驱动程序的函数。
字符设备驱动框架(基本框架)
1.模块加载和卸载
#include <linux/init.h>
#include <linux/module.h>
//驱动入口函数
static int_init xxx_init(void)
{
具体实现
}
//驱动出口函数
static void_exit xxx_exit(void)
{
具体实现
}
//指定驱动入口和出口函数
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");//GPL模块许可证
2.设备号的管理
通过静态分配:register_chrdev_regionalloc_chrdev_region 或动态分配: alloc_chrdev_region申请唯一的设备号,在Linux内核中,cdev结构体描述一个字符设备,cdev中的dev_t 成员定义了设备号,12位的主设备号和20位的次设备号,主设备号用来区分不同类型的设备,而次设备号用来区分同一类型的多个设备。通过MAJOR和MINOR俩个宏获得主次设备号,再通过宏函数MKDEV(int major, int minor)生成dev_t。
3.文件操作函数
在file_operations(fops)中声明驱动层中的函数(read,write,open,close),以便用户层调用
static const struct file_operations my_chardev_fops = {
.owner = THIS_MODULE,
.open = my_chardev_open,
.read = my_chardev_read,
.write = my_chardev_write,
.release = my_chardev_release,
};
4.中断处理
如果字符设备驱动需要处理中断,则需要实现中断处理函数 irq_handler_callback,并在驱动注册时将中断号和回调函数传递给系统。通过request_irq 函数来申请中断号,如果多个设备共享同一个中断号,则需要实现中断共享。可以使用 share_irq
宏来声明中断共享。
当中断发生时,CPU会自动保存当前程序的上下文(如寄存器值、堆栈指针等),然后跳转到中断处理程序的入口地址开始执行。中断处理程序执行完毕后,通过恢复保存的上下文,CPU可以返回到被中断的程序处继续执行。
中断上下文
为了在中断执行时间应该尽量短和中断处理工作比较耗时之间找到一个平衡点,把中断服务程序分为两部分:中断上文和中断下文。
- 中断上文:完成尽可能少却比较急的任务,中断上文的特点就是响应速度快。
- 中断下文:处理中断剩余的大量比较耗时间的任务,而且可以被中断打断。
处理中断下文的机制
软中断:需要修改内核源码,一般不使用,属于中断
tasklet:
- Tasklet不能睡眠,即它不能执行可能导致进程阻塞的操作,如等待I/O操作完成。
- Tasklet的执行是串行的,即同一时间只能有一个tasklet在给定CPU上运行,但不同CPU可以并行运行tasklet。
处理流程:
- 定义tasklet:使用
tasklet_struct
结构体定义一个tasklet,该结构体包含了tasklet的状态、绑定的处理函数和传递给处理函数的参数等信息。 - 初始化tasklet:通过
tasklet_init
函数初始化tasklet,将其与处理函数和参数关联起来。 - 调度tasklet:在中断处理函数(中断上文)中,通过
tasklet_schedule
函数调度tasklet执行。此时,tasklet并不会立即执行,而是等待软中断处理线程(如ksoftirqd)在适当的时候执行它。
Workqueue
工作队列可以睡眠,即它可以执行可能导致进程阻塞的操作,不属于中断,中断上下文不能阻塞,因为阻塞中断上下文将阻止内核响应其他硬件事件,这可能导致系统不稳定或死锁。
处理流程:
- 定义工作项:使用
work_struct
结构体定义一个工作项,该结构体包含了指向处理函数的指针和传递给处理函数的参数等信息。 - 初始化工作项:通过
INIT_WORK
或DECLARE_WORK
宏初始化工作项,将其与处理函数关联起来。 - 调度工作项:在中断处理函数(中断上文)中,通过
schedule_work
或schedule_delayed_work
函数调度工作项执行。此时,工作项会被添加到工作队列中,等待内核线程(如events/x线程)在适当的时候执行它。
5.内存映射
在Linux中不能直接访问寄存器,想要操作寄存器需要完成物理地址到虚拟空间的映射,通过ioremap进行映射,iounmap进行解除映射
6.应用层和内核层传递数据
应用层和内核层是不能直接进行数据传输的。 要想进行数据传输, 要借助
copy_from_user(void *to, const void __user * from, unsigned long n)
copy_to_user(void __user *to, const void *from, unsigned long n)
7.加载驱动的流程
编写完驱动程序后,在驱动所在路径下配置Kconfig和Makefile文件,在内核顶层目录下配置menuconfig,运行make uImage生成内核镜像,将内核镜像拷贝到tftproot中,运行make modules生成驱动ko文件(内核模块),将ko文件拷贝到nfs/rootfs下,用户层的代码也在这个目录下,最后启动u-boot,通过insmod加载模块(rmmod卸载模块),使用mkdev创建驱动结点,最后运行用户层app,在用户层中,调用open函数打开设备文件,根据设备文件的struct_inode可以知道操作的设备类型和设备号,并分配一个struct_file结构体,每个字符设备中都有一个struct_cdev结构体,该结构体包含了字符设备所有信息,最重要的一项就是字符设备操作函数接口(file——operations),找到struct_cdev后,linux内核会将cdev结构体所在地址记录在inode中,将cdev中的(file_operations)记录在struct_file结构体成员f_ops中,最后VFS层(虚拟文件系统层)会给用户层返回一个文件描述符fd,接下来用户层会通过fd找到struct_file,通过struct_file找到file_operations,完成系统调用。
杂项设备驱动框架
当板子上的某个设备没有办法分类时,就可以用 misc 设备驱动,适用于功能简单的设备,它通常嵌套在 platform 总线驱动中,配合总线驱动达到更复杂,多功能的效果。杂项设备是字符设备的一种,杂项设备可以自动生成设备节点,所有的 misc 设备驱动的主设备号都为 10,不同的设备使用不同的从设备号
1.填充miscdevice结构体
struct miscdevice miscdev = {
.minor = MISC_DYNAMIC_MINOR,//自动获取次设备号
.name = "my_misc", //杂项设备名称
.fops = &misc_fops, //文件操作集
};
2.填充file_operations结构体
struct file_operations misc_fops = {
.owner = THIS_MODULE,
.read .relase .write,
};
3.注册杂项设备并生成结点
static int misc_init(void)
{
int ret = misc_register(&miscdev);
}
4.注销杂项设备
static void misc_exit(void)
{
misc_register(&miscdev);
}
Platform设备驱动框架
一种虚拟的总线, 称为 platform 总线, 相应的设备称为 platform_device, 驱动称platform_driver。
驱动和设备如何匹配
1.OF 风格的匹配:设备树采用的匹配方式,device_driver 结构体中有个名为of_match_table的成员变量,此成员变量保存着驱动的compatible匹配表,设备树中的每个设备节点的 compatible 属性会和 of_match_table 表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe 函数就会执行。
2.ACPI 风格匹配:通过调用acpi_driver_match_device
函数来完成的,该函数会检查设备的ACPI信息是否与驱动程序的ACPI信息匹配。如果匹配成功,返回1。
3.ID 表匹配:驱动程序包含一个ID表,列出了它可以支持的设备ID。通过调用platform_match_id
函数并传入ID表和设备,函数会检查是否有任何ID匹配。如果有匹配项,返回非NULL值(表示匹配成功)。
4.名称匹配:如果以上所有匹配都失败,最后会回退到最直接的名称匹配。这是通过比较设备的名称(pdev->name
)和驱动程序的名称(drv->name
)来完成的,使用strcmp
函数。如果名称相同(即strcmp
返回0),则返回1表示匹配成功。
platform_device配置过程
首先,定义一个platform_device
结构体实例,该结构体包含了设备的各种信息,如设备名称、ID、资源。
struct platform_device led_device = {
.name = "my_led_device", // 设备名称,用于与驱动程序匹配
.id = -1, // 设备ID,通常设置为-1表示不使用ID来区分设备
.num_resources = ARRAY_SIZE(led_res), // 资源数组的大小
.resource = led_res, // 指向资源数组的指针
.dev = {
.release = led_device_release, // 释放函数,当设备被卸载时调用
// 其他device结构体成员...
},
// 其他platform_device结构体成员...
};
接下来,填充资源结构体(struct resource
数组),该数组描述了设备所使用的资源。
#define PHY_BASEADDR_GPIO 0x01C20800
struct resource led_res[] = {
[0] = {
.start = PHY_BASEADDR_GPIO + 0x0108, // 资源的起始地址
.end = PHY_BASEADDR_GPIO + 0x010B, // 资源的结束地址
.flags = IORESOURCE_MEM, // 资源类型,这里是内存资源
.name = "PH_Config_Reg", // 资源名称
},
// 其他资源定义...
};
最后,注册设备到内核,使用platform_device_register
函数将配置好的platform_device
注册到内核中。
platform_driver配置过程
首先,需要定义一个platform_driver
结构体实例,该结构体包含了驱动的各种回调函数和属性。这些回调函数包括probe
、remove
、shutdown
等,它们在驱动与设备匹配、卸载等过程中被调用。
static struct platform_driver xxx_driver = {
.driver = {
.name = "xxx",
.of_match_table = xxx_of_match,
// 其他可能的成员,如suppress_bind_attrs等
},
.probe = xxx_probe,
.remove = xxx_remove,
// 可能还有其他回调函数,如suspend、resume等
};
接下来,需要实现probe
、remove
等回调函数。
在驱动模块加载时,需要调用platform_driver_register
函数来注册platform_driver
。同样地,在驱动模块卸载时,需要调用platform_driver_unregister
函数来注销它。
最后,需要将驱动代码编译成模块(.ko文件),并使用insmod
或modprobe
命令将其加载到内核中。
设备树
设备树是一个描述硬件平台和系统设备的数据结构,以一种可读性强的文本形式,将硬件的层次结构、设备的属性和资源配置等信息整合到一个统一的文档中。在Linux驱动程序中,设备树用来替代Platform_device等结构体用来描述设备的板级信息,这使得不同硬件平台之间可以共享相同的内核代码,提供了更好的可移植性和兼容性。
设备树的组成
DT:设备树本身,特殊的数据结构,用于描述硬件信息
DTS: 设备树源代码文件
DTSI: 更通用的设备树代码,包含于DTS代码中
DTB: 在BootLoader启动时,提供给内核进行解析
DTC: 将DTS文件编译成DTB文件
节点: 描述硬件设备的路径和属性,可以包含父子节点,用于描述设备之间的连接关系。
属性: 用于描述节点的特征和配置信息,包括设备的名称、地址、中断号、寄存器配置等,最重要的compatible属性用于将驱动和设备绑定起来,与驱动程序文件的OF匹配表中值相等,就表示设备可以使用这个驱动。
时钟(CLOCK)
S3C2440A中的时钟控制逻辑可以产生必须的时钟信号,包括 CPU 的 FCLK,AHB(高速)总线的 HCLK 以及 APB(低速)总线外设的 PCLK。S3C2440A 包含两个锁相环(PLL):一个提供给 FCLK、HCLK 和 PCLK,另一个专用于 USB 模块(48MHz)。
ADC
模/数转换器,将模拟信号转换成数字信号,一般物理信号通过传感器转化为电信号,电信号通过ADC转化为数字信号,单片机才能进行处理,可以用作温度监测或者电流监测等方面。
在S3C2440中有10 位的CMOS ADC(模/数转换器,一个 8 通道模拟输入的再循环类型设备。其转换模拟输入信号为 10 位二 进制数字编码,最大转换率为500 KSPS,量程3.3v,精度10位,AD值取值范围(0~1023)
ADC转换结果与实际电压的换算:实际电压 = (AD值 / 1023) * 3.3
ADC配置过程:
- 配置GPIO和ADC的时钟,设置引脚为模拟输入
- 设置ADC的分频因子
- 初始化ADC参数
- 使能ADC并校准
- 触发AD转换,读取AD值
PWM
脉冲宽度调制,利用微处理器的数字输出对模拟电路进行控制,通过改变占空比的方式来改变输出的有效电压,通常用于控制LED的亮度和电机的转速。
pwm的频率:1秒钟内信号从高电平到低电平再回到高电平的次数,表示方式:50Hz
pwm的周期:T=1/f,如果频率为50Hz ,一个周期是20ms
占空比:是一个脉冲周期内,高电平的时间与整个周期时间的比例
脉宽时间: 高电平时间
串行通信协议
UART
简介:
全双工通用异步收发器(串行),使用3根线完成:发送线(TX)、接收线(RX)和地线(GND),通信时必须将双方的TX和RX交叉连接并且GND相连才可正常通信。
特征:
UART 接口不使用时钟信号来同步发送器和接收器设备,而是以异步方式传输数据。发送器根据其时钟信号生成的位流取代了时钟信号,接收器使用其内部时钟信号对输入数据进行采样。同步通过两个设备的相同波特率,如果波特率不同,发送和接收数据的时序可能会受影响,导致数据处理过程出现不一致。允许的波特率差异最大值为10%,超过此值,位的时序就会脱节。
数据传输方式:
- 以数据包形式,数据包由起始位、数据帧、奇偶校验位和停止位组成。
- 当不传输数据时, UART 保持高电平。若要开始数据传输,发送UART 会将传输线从高电平拉到低电平并保持1 个时钟周期,当接收 UART 检测到高到低电压跃迁时,便开始以波特率对应的频率读取数据帧中的位,数据从低位开始发送,起始位是一个逻辑0,停止位则是一个或多个逻辑1。通过奇偶校验位,接收 UART判断传输期间是否有数据发生改变。
IIC
简介:
半双工同步串行总线,利用一根时钟线和一根数据线在连接总线的两个器件之间进行信息的传递,每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。
特征:
- 只需要一根时钟线和一根数据线
- I2C是真正的多主设备总线,可提供仲裁和冲突检测,它能在多个主机同时请求控制总线时利用仲裁机制避免数据冲突并保护数据
- 从机没有主动发起权,只能被主机呼叫
- 每个器件都可作为主机或从机,但一时刻只能有一个主机
- 没有严格的波特率要求
数据传输方式:
主机在时钟线(SCL)上输出串行时钟信号,数据在数据线(SDA)上进行传输,每传输一个字节后面跟随一个应答位,ACK 为 一个低电平信号,当时钟信号为高时, SDA 保持低电平则表明接收端已成功接收到发送端的数据。一个时钟脉冲传输一个数据位,数据从高位开始发送。
- 空闲状态: SCL 和 SDA 都为高电平,此时总线上设备都可以通过发送开始条件启动通信
- 起始信号:SCL 线为高时,SDA 线上出现由高到低的信号,表明总线上产生了起始信号
- 停止信号:SDA 线上出现由低到高的信号,表明总线上产生了停止信号
主机发送起始信号启用总线,随后主机发送一个字节数据(前7位从机地址+1位的读写位),从机发送应答信号进行地址匹配,每发送一个字节都有一个应答信号,循环往复,直至主机发送停止信号释放总线。
SPI
简介:
是一种高速、全双工、同步通信总线。优点是支持全双工通信,通讯方式简单,数据传输速率较快;缺点是没有指定的流控制,没有应答机制,在数据可靠性上有一定缺陷。
至少需要4根线
MISO: 主设备数据输入,从设备数据输出;
MOSI: 主设备数据输出,从设备数据输入;
SCLK : 时钟信号,由主设备产生;
CS: 从设备片选信号,由主设备控制,通常低电平有效,一个从设备一根CS线
SPI与IIC的区别:
都具有主从设备的机制,不同的是 IIC可以具有多个主机,因为其具有仲裁和冲突检测机制
SPI是全双工通信,而IIC是半双工通信
数据传输灵活,不限于8位,且比IIC速度要快,但是进行数据传输时无应答信号,主机在不知情的情况下无处发送
串行通信接口标准
在串行通讯时,要求通讯双方都采用一个标准接口,使不同的设备方便的建立连接。RS232、RS485、TTL和CAN都是常用的串行通信接口标准,它们在电气特性、通信模式、传输距离、传输速率和应用场合等方面有所不同。TTL,RS232,RS485严格来讲都是一种逻辑电平的表示方式。
TTL电平标准
- 电气特性:TTL电平信号通常定义为逻辑“1”为+3.3V左右,逻辑“0”为接近0V,信号幅度较小。
- 通信模式:TTL电平主要用于芯片级的接口,如UART、SPI、I²C等接口的信号传输。
- 传输距离:由于TTL电平信号衰减较快,不适合长距离传输,一般在几厘米到几米的范围内。
- 应用场合:TTL电平在嵌入式系统内部通信、计算机主板上的集成电路之间非常普遍。
RS232电平
- 电气特性:RS232使用非平衡传输,负逻辑,信号电平为±5V~±15V,逻辑“1”对应负电压(通常为-3V至-15V),逻辑“0”对应正电压(通常为+3V至+15V)。
- 通信模式:支持点对点通信,一发送和接收独立
- 传输距离:理论上传输距离较短,一般不超过50米,实际应用中经常使用放大器延长距离。
RS485电平
- 电气特性:采用平衡传输,与RS232不同的是,RS485的工作方式是差分工作方式,所谓差分工作方式,是指在一堆双绞线中,一条定义为A,一条定义为B。+2V~+6V表示“1”,- 6V~- 2V表示“0”。RS485有两线制和四线制两种接线,四线制是全双工通讯方式,两线制是半双工通讯方式
- 通信模式:支持多点互联,最多可以连接32个设备进行网络通信,RS-485采用半双工工作方式,允许在简单的一对屏蔽双绞线上进行多点、双向通信,不过任何时候只能有一点处于发送状态,因此,发送电路须由使能信号加以控制。
- 传输距离:理论上最大传输距离超过1200米,视具体环境和线材质量而定,适合组建大型工业网络。
- 应用场合:广泛应用于工业自动化、楼宇自动化、安防监控等领域,特别适用于需要远距离传输和多设备联网的场合。
总结来说,RS232和RS485更多地是物理层和链路层的通信标准,而TTL电平是集成电路内部通信的一种通用逻辑电平标准,在实际应用中,这些通信方式常常结合使用,比如在设计系统时,可能会在集成电路内部使用TTL电平,然后通过RS232、RS485与其他设备进行通信。
存储器
DMA
直接存储器访问,可以理解为数据的搬运工,可以进行内存与内存,内存与外设,外设与内存的数据传输,不需要CPU的干预,显著提高数据传输效率并减轻CPU负担。
DMA获取数据的流程:
- UART控制器读取外设发送的数据
- DMA自动获取UART中的数据并搬移到内存中
- UART给CPU发送中断信号通知数据读取完毕
- CPU可以访问内存中的数据,这样CPU就不用频繁访问数据寄存器,只关心内存即可
DMA发送数据的流程:
- CPU将数据写入到指定内存中
- DMA读取内存中的数据并搬移到数据寄存器中
- UART控制器将数据发送出去
- DMA给CPU发送中断信号通知数据发送完毕
EEPROM
非易失性存储器,具有掉电不丢失,可擦写的特性,但容量较小(只有几KB),且擦写和写入次数有限
EEPROM操作流程:
- 初始化,设置存储地址,擦除存储器,设置编程单元等
- 通过存储地址获取EEPROM存储器中存储的数据
- 根据存储地址写入数据,写入前可能需要先擦除存储器
FLASH
闪存,也是非易失性存储器,属于EEPROM类型,同时存取速度快、功耗小,
FLASH提供了多种保护机制,以确保数据的安全性和可靠性
-
读写保护:通过选项字节可以设置FLASH的读写保护等级,防止未经授权的访问和修改。
-
一次性可编程保护:OTP区域只能写入一次,写入后无法更改,用于存储重要且不需要更改的数据。
-
上锁机制:FLASH具有上锁功能,复位后默认上锁,防止未经授权的修改,解锁需要向密钥寄存器写入特定的密钥值。
FLASH的操作流程:
-
擦除:在写入新数据之前,需要先对FLASH进行擦除操作。擦除操作可以按扇区进行,也可以对整个FLASH进行批量擦除。擦除过程中,需要检查FLASH状态寄存器中的忙碌标志位(BSY),以确保当前没有正在进行的FLASH操作。
-
编程(写入):擦除完成后,即可进行编程操作。编程前同样需要检查BSY位,并配置FLASH控制寄存器以启动编程操作。写入数据时,需要确保数据宽度与配置相匹配,写入完成后,需要等待BSY位清零,并检查操作是否成功。