系统移植 & 字符设备驱动开发

0、

1、框架

1.1 系统移植(uboot引导、linux内核、根文件系统)

  • bootloader( uboot ) 引导系统
    • uboot 是 bootloader的一种实现
    • 开发阶段:tftp方式
    • 产品阶段:SD卡、emmc等(flash)
  • linux 内核移植
    • 开发阶段:tftp方式
    • 产品阶段:emmc等(flash)
  • linux 根文件系统
    • 开发阶段:nfs方式
    • 产品阶段:磁盘(flash)

1.2 字符设备驱动开发

本篇内容:

- 内核模块

- 字符设备驱动

- 中断

  1. 驱动设备 代码框架
  2. 内存映射、中断、定时器等,进行初始化操作
  3. 注册驱动:
    1. 查看:cat /proc/devices
    2. 创建设备节点: class_create
    3. 创建字符设备文件:device_create
  4. 数据交互(应用层、底层)
  5. 注销驱动:
    1. 注销中断、去初始化、去定时器、取消映射等
    2. 删除设备文件:device_destroy
    3. 删除设备节点:class_destroy
    4. 注销字符设备驱动: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内核打印函数

  1. 使用vi -t KERN_ERR命令查看内核打印级别。
  2. include/linux/printk.h文件中查找相关定义。
  3. 根据需要修改系统默认的级别,例如:
    • 终端级别: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、 字符设备驱动 注册步骤 

  1. 首先,在应用层创建一个设备文件(设备节点),例如:/dev/led
  2. 然后,在内核层编写一个字符设备驱动,例如:led_driver.c。在这个驱动中,需要定义一个file_operations结构体,用于描述字符设备的操作方法,如打开、读取、写入和关闭等。
  3. 接下来,在硬件层初始化LED灯。这通常涉及到设置GPIO引脚、配置PWM等操作。
  4. 在用户空间,通过open()系统调用打开设备文件,然后通过read()write()系统调用与设备进行交互。
  5. 最后,在应用层使用close()系统调用关闭设备文件。

register_chrdev函数用于注册一个字符设备驱动,它接受三个参数:

  1. unsigned int major:主设备号。如果传入的值大于0,表示使用传入的主设备号;如果传入的值为0,表示让操作系统自动分配一个主设备号。
  2. const char *name:设备名称,用于在/proc/devices文件中显示设备信息。
  3. const struct file_operations *fops:指向一个file_operations结构体的指针,该结构体描述了设备的操作方法,如打开、关闭、读取和写入等。

unregister_chrdev函数用于注销一个已注册的字符设备驱动,它接受两个参数:

  1. unsigned int major:主设备号。
  2. 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文件传递信息

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值