0、
1、框架
1.1 系统移植(uboot引导、linux内核、根文件系统)
- bootloader( uboot ) 引导系统
- uboot 是 bootloader的一种实现
- 开发阶段:tftp方式
- 产品阶段:SD卡、emmc等(flash)
- linux 内核移植
- 开发阶段:tftp方式
- 产品阶段:emmc等(flash)
- linux 根文件系统
- 开发阶段:nfs方式
- 产品阶段:磁盘(flash)
1.2 字符设备驱动开发
本篇内容:
- 内核模块
- 字符设备驱动
- 中断
- 驱动设备 代码框架
- 内存映射、中断、定时器等,进行初始化操作
- 注册驱动:
- 查看:cat /proc/devices
-
创建设备节点: class_create
-
创建字符设备文件:device_create
-
数据交互(应用层、底层)
-
注销驱动:
-
注销中断、去初始化、去定时器、取消映射等
-
删除设备文件:device_destroy
-
删除设备节点:class_destroy
-
注销字符设备驱动:unregister_chrdev
-
1.3 练习项目:设计一个社区饮水机控制系统
功能概述:
模拟饮水机按键控制开始停止功能,并根据时间控制出水量(红灯表示出水状态,绿灯表示停止状态)。应用层程序模拟刷卡或者扫码计费,如应用层(向驱动)传递数据3那么就表示付费3元。
详情:
应用层检测终端输入数据(最好有包头包尾的数据处理功能),输入格式(包头ID金额包尾 eg(0X550X010X020XFF)表示包头0X55 ID 0x01 金额2元包尾0XFF),应用层本地保存用户数据,保存个文本就行(谁消费多少),之后传递金额数据给驱动。
驱动层上点红灯闪烁,当检测到应用层安装以后绿灯常亮,当应用层传来消费金额以后根据金额(注意识别包头包尾),倒计时并亮红灯表示放水,倒计时结束亮绿灯并且蜂鸣器响一段时间表示水停。
步骤:
配合linux内核定时器,ARM外设外部中断、地址映射等操作实现。
2、uboot移植
概念:
在嵌入式开发中,bootloader是用来引导和加载内核,并且启动内核,然后给内核传递参数的工具。常见的bootloader有u-boot、Bios、vivi、redboot等等。其中,使用最广泛的bootloader是u-boot。u-boot是一个开源软件。
特点:
1. U-boot是一个开源软件。
2. U-boot支持多种架构的平台,包括ARM、PowerPC、MIPS和x86等。
3. U-boot的源码短小精悍。
4. U-boot就是一个裸机代码。
5. U-boot用于引导加载内核,启动内核,并给内核传递参数。
6. U-boot可以完成部分硬件的初始化,如UART、内存、eMMC和网卡等。
7. U-boot的生命周期相对较短,一旦启动完内核并传递完参数(告诉内核从何处挂载根文件系统),U-boot的任务就完成了。
步骤:
1. 阅读README文件。
2. 配置交叉编译工具链,打开u-boot源码顶层目录的Makefile,修改为:
ifeq (arm,arm)(里面的两个相等即可,写ARM的原因是我们用的是arm)
CROSS_COMPILE ?= arm-none-linux-gnueabi-
endif
3. 删除u-boot源码的中间文件,执行命令:`make distclean/clean`。
4. 配置u-boot源码支持fs6818开发板,执行命令:`make <board_name>_config`和`make fs6818_config`。
如果打印成功信息,表示成功;如果打印错误信息,表示u-boot不支持此开发板。
5. 编译u-boot源码,生成ubootpak.bin,执行命令:`make / make all(编译的时间比较长)`。
6. 将生成的ubootpak.bin文件下载到开发板,测试是否可以使用。具体操作步骤如下:
- 通过SD卡的方式启动uboot,进入到FS6818#界面。
- 拷贝ubootpak.bin到tftpboot目录下。
- 将ubootpak.bin使用tftp命令烧写到内存中。
- 更新EMMC中的uboot。
- 设置拨码开关,切换到EMMC启动。
- 开发板重新上电。
3、linux内核移植
【情景不同,自行百度】
【面试题】
Makefile、.config和Kconfig文件之间的关系?:
1. Makefile:用于指导内核进行编译的文件。
2. .config:存放内核的配置信息和硬件信息的文件。
3. Kconfig:存放菜单选项(menuconfig的源代码)的文件。
执行make fs6818_defconfig命令时,会根据fs6818_config文件和Kconfig文件中的配置信息生成.config文件。
执行make menuconfig命令时,会根据Kconfig生成菜单的图形化界面。如果根据菜单图形化界面进行配置后,会更新.config文件。
Makefile文件根据.config文件中的信息,决定将哪些文件编译到uImage中,哪些不编译到uImage中。
4、根文件系统
概念:
根文件系统:指系统运行所必须依赖的一些文件,包括脚本、库、配置文件等,本质上就是目录和文件的集合。
根文件系统镜像:将根文件系统按照某种格式进行打包压缩后生成的单个文件,通常命名为rootfs或ramdisk.img。
文件系统:是一种管理和访问磁盘的软件机制,用于组织和管理存储在磁盘上的数据。不同的文件系统具有不同的管理和访问磁盘的机制。
目录介绍
注释:各个文件的功能解析
- bin:命令文件(通过busybox工具制作)
- dev:设备文件(被操作系统识别的设备才有对应的文件,即设备运行时)
- etc:配置文件(配置内核相关的一些信息)
- lib:库文件,比如C的标准库(从交叉编译工具链中复制的)
- linuxrc:根文件系统被挂载后运行的第一个程序(通过busybox工具制作)
- mnt:共享目录(非必要),比如挂载SD卡等时将SD卡挂载在该目录
- proc:与进程相关的文件(当有进程运行时才会有文件)
- root:用户权限(板子本身就是以root用户运行)
- sbin:超级用户命令,一般用户不可用(板子本身是超级用户,通过busybox工具制作)
- sys:系统文件(系统运行时,系统加载后才会有文件)
- tmp:临时文件(比如插入新的设备时会产生临时文件)
- usr:用户文件(通过busybox工具制作)
- var:存放下载的文件和软件(可有可无)
【busybox使用】
自行搜索
5、测试移植的系统
1. 先进行本地组网
win主机:提供linux虚拟机
linux虚拟机:提供tftp nfs服务
开发板:通过网线与win主机连接,因为网线下载快
2. 开发阶段
从tftp服务器下载 名字为 uImage 的linux内核镜像到内存。
设置nfs 的启动参数
保存环境变量
设置启动命令:
```
setenv bootcmd tftp 0x48000000 uImage;bootm 0x48000000
```
设置启动参数:
```
setenv bootargs root=/dev/nfs nfsroot=192.168.1.66:/home/hq/nfs/rootfs rw console=ttySAC0,115200 init=/linuxrc ip=192.168.1.88
```
保存环境变量:
```
saveenv
```
3. 产品阶段
从mmc读取 内核镜像、文件系统,设置启动参数和地址,保存环境变量
setenv bootcmd mmc read 0x48000000 0x800 0x4000;mmc read 0x49000000 0x20800 0x20800;bootm 0x48000000 0x49000000
setenv bootargs root=/dev/ram rw initrd=0x49000040,0x1000000 rootfstype=ext4 init=/linuxrc console=ttySAC0,115200
saveenv
6、ARM裸机代码和驱动之间的区别?
- 共同点:两者都可以操作硬件。
- 不同点:
- 裸机使用C语言编写,直接操作寄存器;而驱动通常是用C语言编写的,但需要遵循一定的内核编程规范,依赖内核编译和执行。
- ARM裸机是单独编译和执行的,而驱动依赖于内核编译和执行。
- ARM裸机只能执行一份代码,而驱动可以同时执行多份代码。当需要操作串口时,驱动程序可以利用内核提供的API,而不需要程序员自己编写相应的代码。
- ARM裸机的main函数通常只包含逻辑代码,而驱动模块需要依赖内核框架和操作硬件的过程。
7、宏内核和微内核区别
宏内核和微内核是操作系统内核的两种主要结构类型。
宏内核:
- 定义与特点:将操作系统内核的所有模块运行在内核态,具备直接操作硬件的能力。用户服务和内核服务在同一空间中实现,执行速度快。
- 优点:性能极佳,所有功能集成在一个程序中,紧密集成,提供很好的性能。例如Linux就是传统的宏内核结构,性能极高。
- 缺点:耦合度高,一个模块问题可能导致整个内核崩溃。扩展性较弱,添加新功能可能需要修改各个模块。
- 示例:Ubuntu、Android等操作系统使用宏内核结构。
微内核:
- 定义与特点:只将进程和内存机制集成到内核中,文件、设备和驱动在操作系统之外。设计目标是将大部分操作系统运行在内核之外。
- 优点:稳定性强,即使某个服务崩溃了,整个内核也不会崩溃。
- 缺点:由于频繁的数据传递,效率相对较低。
- 示例:鸿蒙操作系统采用微内核结构。
8、驱动模块三要素及makefile
- 入口:资源的申请,通常使用
__init
函数实现。 - 出口:资源的释放,通常使用
__exit
函数实现。 - 许可证:GPL(GNU通用公共许可证),表示该驱动模块需要开源,因为Linux系统是开源的,所以需要写许可协议。
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
return 0;
}
static void __exit hello_exit(void)
{
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
KERNEL_PATH=/home/hq/kernel/kernel-3.4.39 # 开发板
# KERNEL_PATH=/lib/modules/$(shell uname -r)/build # 虚拟机路径
PWD=$(shell pwd)
# 将shell命令pwd执行结果赋值给PWD变量
all:
make -C $(KERNEL_PATH) M=$(PWD) modules
# 到内核顶层目录执行make modules,将本地驱动编译并生成驱动模块(本地生成)
.PHONY:clean
clean:
make -C $(KERNEL_PATH) M=$(PWD) clean
9、printk内核打印函数
- 使用
vi -t KERN_ERR
命令查看内核打印级别。 - 在
include/linux/printk.h
文件中查找相关定义。 - 根据需要修改系统默认的级别,例如:
- 终端级别:
echo 4 3 1 7 > /proc/sys/kernel/printk
- 终端级别:
10、驱动模块传递参数
用于终端 安装模块时,给其传递参数
1.传递方式:
sudo insmod demo.ko hello world
2. 注意问题
传递字符时使用ASCII码值;传递字符串时,不能有空格
传字符,需要用十进制数字表示,而不能用ascii字符
3. 函数接口
module_param(name, type, perm)
sudo insmod hello.ko a=20 b=30 c=65 p="hello_world"
module_param_array(name, type, nump, perm)
sudo insmod hello.ko a=121 b=10 c=65 p="hello" ww=1,2,3,4,5
- @name: 数组名
- @type: 变量的类型 / 数组的类型
- @nump: 参数的个数,即数组的长度
- @perm: 权限
11、 字符设备驱动 注册步骤
- 首先,在应用层创建一个设备文件(设备节点),例如:
/dev/led
。- 然后,在内核层编写一个字符设备驱动,例如:
led_driver.c
。在这个驱动中,需要定义一个file_operations
结构体,用于描述字符设备的操作方法,如打开、读取、写入和关闭等。- 接下来,在硬件层初始化LED灯。这通常涉及到设置GPIO引脚、配置PWM等操作。
- 在用户空间,通过
open()
系统调用打开设备文件,然后通过read()
和write()
系统调用与设备进行交互。- 最后,在应用层使用
close()
系统调用关闭设备文件。
register_chrdev
函数用于注册一个字符设备驱动,它接受三个参数:
unsigned int major
:主设备号。如果传入的值大于0,表示使用传入的主设备号;如果传入的值为0,表示让操作系统自动分配一个主设备号。const char *name
:设备名称,用于在/proc/devices
文件中显示设备信息。const struct file_operations *fops
:指向一个file_operations
结构体的指针,该结构体描述了设备的操作方法,如打开、关闭、读取和写入等。
unregister_chrdev
函数用于注销一个已注册的字符设备驱动,它接受两个参数:
unsigned int major
:主设备号。const char *name
:设备名称。
cat /proc/devices
是一个Linux命令,用于查看系统中已注册的设备信息。
12、手动创建设备文件
手动创建设备文件
sudo mknod led (路径是任意) c/b 主设备号 次设备号
sudo –rf led 删除的时候记得加-rf
13、内核与应用层的数据交互
应用程序如何将数据传递给驱动(读写的方向是站在用户的角度来说的)
#include <linux/uaccess.h>
✧int copy_from_user(void *to, const void __user *from, int n)
功能:从用户空间拷贝数据到内核空间(用户需要写数据的时候)
参数:
@to :内核中内存的首地址
@from:用户空间的首地址
@n :拷贝数据的长度(字节)
返回值:成功返回0,失败返回未拷贝的字节的个数
✧int copy_to_user(void __user *to, const void *from, int n)
功能:从内核空间拷贝数据到用户空间(用户开始读数据)
参数:
@to :用户空间内存的首地址
@from:内核空间的首地址
@n :拷贝数据的长度(字节)
14、物理内存映射到虚拟内存空间
rgb_led灯的寄存器是物理地址,在linux内核启动之后,在使用地址的时候,操作的全是虚拟地址。需要将物理地址转化为虚拟地址。
在驱动代码中操作的虚拟地址就相当于操作实际的物理地址。
物理地址<------>虚拟地址
✧void * ioremap(phys_addr_t offset, unsigned long size)
(当__iomen告诉编译器,取的时候是一个字节大小)
功能:将物理地址映射成虚拟地址
参数:
@offset :要映射的物理的首地址
@size :大小(字节)(映射是以业为单位,一页为4K,就是当你小于4k的时候映射的区域都为4k)
返回值:成功返回虚拟地址,失败返回NULL((void *)0);
✧void iounmap(void *addr)
功能:取消映射
参数:
@addr :虚拟地址
返回值:无
15、设备节点创建问题(udev/mdev)
#include <linux/device.h>
自动创建设备节点:
struct class *cls;
✧cls = class_create(owner, name) /void class_destroy(struct class *cls)//销毁
功能:向用户空间提交目录信息(内核目录的创建)
参数:
@owner :THIS_MODULE(看到owner就添THIS_MODULE)
@name :目录名字
返回值:成功返回struct class *指针
失败返回错误码指针 int (-5)
IS_ERR() :返回值为0,不在错误码地址范围,非0,在错误码地址范围
内核从0xffffffff 地址开始往地址减少的方向,预留了4K空间用来作为错误码的地址。
if(IS_ERR(cls)){
return PTR_ERR(cls);(PTR_ERR:把错误码指针转换成错误码)
}
✧struct device *device_create(struct class *class,
struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
(内核文件的创建),每个文件对应一个外设(硬件设备)
功能:向用户空间提交文件信息
参数:
@class :目录名字
@parent:NULL
@devt :设备号 (major<<12 |0 < = > MKDEV(major,0))
@drvdata :NULL
@fmt :文件的名字
返回值:成功返回struct device *指针
失败返回错误码指针 int (-5)
void device_destroy(struct class *class, dev_t devt)//销毁
16、IO控制函数
用户程序所作的只是通过命令码告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道 // GREE_ON BLUE_ON
app应用层
(功能:input output 的控制)
#include <sys/ioctl.h>
✧int ioctl(int fd, int request, ...);(RED_ON)
(让点灯的代码变得简洁)
参数:
@fd : 打开文件产生的文件描述符
@request: 请求码(读写|第三个参数传递的字节的个数),
:在sys/ioctl.h中有这个请求码的定义方式。
@... :可写、可不写,如果要写,写一个内存的地址
Kernel:
(在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情;)
fops:
- long (*unlocked_ioctl) (struct file *file,
unsigned int request, unsigned long args);
对于使用ioctl函数时,主要的就是请求码的设计,请求码主要在sys/ioctl.h文件里面进行了设计。
#define _IO(type,nr)
_IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)
_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define RDE_LED _IO(type,nr)
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
dir << 30 | size<<16 | type << 8 | nr << 0
2 14 8 8
方向 大小 类型 序号
(方向:00 01 10 11读写相关,)
(大小:sizeof(变量名))
(类型:组合成一个唯一的不重合的整数,一般传一个字符)
(序号:表示同类型中的第几个,当开灯的时候写0,那关的时候就不写0)。
#define RLED_ON _IOWR('a',0,int)//亮灯
#define RLED_OFF _IOWR('a',1,int) //灭灯
内核中已经使用的命令码的域在如下文档中已经声明了。
vi kernel-3.4.39/Documentation/ioctl$ vi ioctl-number.txt
(2^32次方 = 4G的数字,所以可以使用,
内核的想法是:每一个数字代表一个,功能和数字一一对应,但是不一样的驱动使用的时候相同也是可以的)
17、Linux内核中断
ARM处理器在按下按键时,首先会执行汇编文件start.s中的异常向量表里的irq。
在irq中进行一些操作,然后跳转到C语言的do_irq()函数。接下来,进行以下操作:1)判断中断序号;2)处理中断;3)清除中断。
Linux内核实现和ARM逻辑实现中断的原理是相同的。
当按键按下后,依然会进入异常向量表,然后调用handler_irq函数(写死的)。
在handler_irq函数中,定义了一个数组,数组中的每个成员都包含一个结构体。
结构体中有一个函数指针,该函数指针指向了我们自己提交的函数名。数组的下标是Linux内核的软中断号,它与硬件中断号之间存在映射关系。在内核实现中断时,handler_irq函数会初始化中断寄存器,我们只需要获取软中断号,并将中断处理函数绑定即可。
✧int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
功能:注册中断 //0-159 -> 160 GPIOB15 - >1*32+15 GPIOC7 - 2*32+7
参数:
@irq : 软中断号
gpio的软中断号
软中断号 = gpio_to_irq(gpino号);//160--》 0-159
gpiono = m*32+n(n:组内的序号)
m:那一组 A B C D E(5组)
0 1 2 3 4
gpioa28 = 0*32+28
gpiob8 =1*32+8 gpiob16 = 1*32+16
#define IRQF_DISABLED 0x00000020 //快速中断(在处理函数里面写了他,就先处理这个中断)
#define IRQF_SHARED 0x00000080 //共享中断(中断的接口较少,但是器件都想要中断,那管脚需要外接两个,寄存器里面有中断状态标志位,看中断状态标志位有没有置位。一个口不可以链接两个按键,按键没办法区分)
#define IRQF_TRIGGER_RISING 0x00000001(上升沿触发)
#define IRQF_TRIGGER_FALLING 0x00000002(下降沿出发)
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
@name :名字 cat /proc/interrupts
@dev :向中断处理函数中传递参数 ,不想传就写为NULL
返回值:成功0,失败返回错误码
✧void free_irq(unsigned int irq, void *dev_id)
功能:注销中断
参数:
@irq :软中断号
@dev_id:向中断处理函数中传递的参数,不想传就写为NULL
18、linux 内核定时器
●定时器的当前时间如何获取?
jiffies:内核时钟节拍数
jiffies是在板子上电这一刻开始计数,只要
板子不断电,这个值一直在增加(64位)。在
驱动代码中直接使用即可。
●定时器加1代表走了多长时间?
在内核顶层目录下有.config
CONFIG_HZ=1000
周期 = 1/CONFIG_HZ
周期是1ms;
➢分配的对象
struct timer_list mytimer;
➢对象的初始化
➢
struct timer_list {
unsigned long expires; //定时的时间
void (*function)(unsigned long); //定时器的处理函数
unsigned long data; //向定时器处理函数中填写的值
};
void timer_function(unsigned long data) //定时器的处理函数
{
}
init_timer(&mytimer); //内核帮你填充你未填充的对象
mytimer.expries = jiffies + 1000; //1s
mytimer.function = timer_function;
mytimer.data = 0;
➢对象的添加定时器
void add_timer(struct timer_list *timer);
//同一个定时器只能被添加一次,
//在你添加定时器的时候定时器就启动了,只会执行一次
int mod_timer(struct timer_list *timer, unsigned long expires)
//再次启动定时器 jiffies+1000
➢4.对象的删除
int del_timer(struct timer_list *timer)
//删除定时器
Int gpio_get_value(int gpiono);//通过gpiono获取当权gpio的所处状态
返回0,低电平 非0:高电平
19、模块导出符号表
思考1:应用层两个app程序,app1中拥有一个add函数,app1运行时app2是否可以调用app1中的add函数? 不行,因为应用层app运行的空间是私有的(0-3G)没有共享。
思考2:两个驱动模块,module1中的函数,module2是否可以调用?可以,他们公用(3-4G)内核空间,只是需要找到函数的地址就可以。好处:减少代码冗余性,代码不会再内存上被重复加载。代码更精简,一些代码可以不用写,直接调用别人写好的函数就可以。
编写驱动代码找到其他驱动中的函数,需要用模块导出符号表将函数导出,被人才可以使用这个函数。他是一个宏函数。
在驱动的一个模块中,想使用另外一个模块中的函数/变量,只需要使用EXPORT_SYMBOL_GPL将变量或者函数的地址给导出。使用者就可以用这个地址来调用它了。
EXPORT_SYMBOL_GPL(sym)
sym:变量名或函数名
代码举例1:两个独立的代码驱动模块
代码举例2:提供者为内核已经安装使用的驱动
总结:
编译:
1.先编译提供者,编译完成之后会产生一个Module.symvers
2.将Module.symvers拷贝到调用者的目录下
3.编译调用者即可
安装:
先安装提供者
再安装调用者
卸载:
先卸载调用者
再卸载提供者
如果调用者和提供者时两个独立(xx.ko)驱动模块,他们间传递地址的时候,是通过Module.symvers传递的。
如果提供者是内核的模块(uImage),此时调用者和提供者间就不需要Module.symvers文件传递信息。