精华内容
下载资源
问答
  • 嵌入式linux驱动开发
    2022-08-08 16:40:39

    目的

    驱动开发是嵌入式Linux中工作比重比较大的一部分。这篇文章将记录下最基本的驱动开发过程。

    这篇文章中内容均在下面的开发板上进行测试:
    《新唐NUC980使用记录:自制开发板(基于NUC980DK61YC)》

    这篇文章主要是在下面文章基础上进行的:
    《新唐NUC980使用记录:访问以太网(LAN8720A) & 启用SSH》

    基础说明

    对于驱动程序而言从不同角度来看对它的认知是不同的。从用户应用程序来看—— Linux中一切皆文件 ,系统中各种设备也都是文件(通常/dev/目录下的就是各种设备文件)。对于文件来说用户操作时无非就是 open / close / read / write 等方法。那么这些设备的驱动程序无非就是要实现这些方法提供给用户使用。

    驱动开发主要就是实现上面提到的各种文件操作的方法及其具体对硬件的操作。除了实现这些方法,还需要用某种机制将这些方法和用户应用程序中各种文件操作方法关联起来(通常就是注册下)。

    Linux中驱动程序主要可以分为字符设备驱动、块设备驱动、网络设备驱动、复合设备驱动等,这里主要以字符设备驱动展开说明。

    Linux中驱动程序可以直接编译到内核中,也可以编译成 *.ko 形式的文件,然后在使用时再安装。为了方便测试,这里主要先以这种形式展开说明。

    驱动测试应用程序

    编写完驱动程序后最终需要进行测试,这里先准备个测试程序。根据上一章节的内容,这里的测试程序主要就是对根据驱动程序生成的设备文件进行 open / close / read / write 等操作。

    cd ~/nuc980-sdk/
    mkdir -p drivers/char_dev
    cd drivers/char_dev/
    gedit char_dev_test.c
    

    在文件中写入下面代码:

    #include <string.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    /* 该程序用于读写测试本文要编写的驱动程序对应生成的设备 */
    int main(int argc, char **argv)
    {
    	int fd;
    	char buf[4096];
    	int len;
    
    	/* 判断参数 */
    	if ((argc < 3) || (argc > 4))
    	{
    		printf("Usage: char_dev_test <devpath> -w <string>\n"); // 写数据
    		printf("       char_dev_test <devpath> -r\n");			// 读数据
    		return -1;
    	}
    
    	/* 打开文件 */
    	fd = open(argv[1], O_RDWR);
    	if (fd < 0)
    	{
    		printf("applog: can not open file %s\n", argv[1]);
    		return -1;
    	}
    
    	/* 写数据或读数据 */
    	if ((0 == strcmp(argv[2], "-w")) && (argc == 4))
    	{
    		len = strlen(argv[3]) + 1;
    		write(fd, argv[3], len);
    	}
    	else if ((0 == strcmp(argv[2], "-r")) && (argc == 3))
    	{
    		len = read(fd, buf, 4096);
    		buf[4095] = '\0';
    		printf("applog: read - %s\n", buf);
    	}
    	else
    	{
    		close(fd);
    		return -1;
    	}
    
    	close(fd);
    
    	return 0;
    }
    

    编译生成可执行文件:

    # 配置编译工具链
    export PATH=$PATH:/home/nx/nuc980-sdk/arm_linux_4.8/bin
    
    # 编译生成开发板的可执行文件
    arm-linux-gcc -o char_dev_test char_dev_test.c
    
    # 开发板启用了SSH的话可以使用SCP命令将程序通过网络拷贝到开发板中
    scp char_dev_test root@192.168.31.142:/root/
    

    基础开发与使用

    驱动模块入口与出口

    # cd ~/nuc980-sdk/drivers/char_dev/
    gedit char_dev.c
    

    在文件中写入下面代码:

    #include <linux/module.h>
    
    static int __init char_dev_init(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	return 0;
    }
    
    static void __exit char_dev_exit(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    }
    
    module_init(char_dev_init); // 模块入口
    module_exit(char_dev_exit); // 模块出口
    
    MODULE_LICENSE("GPL"); // 模块许可
    

    上面代码中 printk 用在内核中,用于打印内核日志。MODULE_LICENSE("GPL"); 只是个声明,但不加的话驱动安装时会有个提示。

    创建Makefile文件:

    gedit Makefile
    

    在文件中写入下面代码,编译驱动模块需要用到内核源码,下面需要指定正确的路径:

    # 配置内核源码路径
    KERNEL_DIR := /home/nx/nuc980-sdk/NUC980-linux-4.4.y
    
    MODULE_DIR := $(shell pwd)
    
    obj-m += char_dev.o 
    
    # 以模块方式编译驱动
    all:
    	$(MAKE) -C $(KERNEL_DIR) M=$(MODULE_DIR) modules 
    
    clean:
    	$(MAKE) -C $(KERNEL_DIR) M=$(MODULE_DIR) modules clean 
    
    

    编译生成驱动模块:

    # export PATH=$PATH:/home/nx/nuc980-sdk/arm_linux_4.8/bin
    make
    # 编译后生成的char_dev.ko就是驱动模块,拷贝到开发板上
    scp char_dev.ko root@192.168.31.142:/root/
    

    驱动模块安装与卸载

    在开发板上使用 insmod 来安装驱动模块,使用 rmmod 卸载驱动模块。使用 lsmod 查看已安装的驱动模块:
    在这里插入图片描述

    需要注意的是上面模块安装和卸载时出现的内核日志只有在串口终端中才会出现,如果时通过SSH的终端,那么需要使用 dmesg 命令来查看内核日志:
    在这里插入图片描述
    上面出现了多条日志信息,前面一些是之前测试时产生的。

    字符设备注册与注销

    完成了最基本的模块和使用,接下来开始准备字符设备驱动,将驱动代码改成如下:

    #include <linux/module.h>
    #include <linux/fs.h>
    
    static int major = 0;
    const char *dev_name = "char_dev";
    
    static const struct file_operations char_dev_fops = {
    	.owner = THIS_MODULE,
    };
    
    static int __init char_dev_init(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	major = register_chrdev(0, dev_name, &char_dev_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
    	return 0;
    }
    
    static void __exit char_dev_exit(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	unregister_chrdev(major, dev_name); // 注销字符设备
    }
    
    module_init(char_dev_init); // 模块入口
    module_exit(char_dev_exit); // 模块出口
    
    MODULE_LICENSE("GPL"); // 模块许可
    

    重新编译后在开发板上安装模块就可以在 /proc/devices 中看到字符类型设备驱动了:
    在这里插入图片描述

    设备开关与读写

    接下来继续完善驱动代码,添加相应操作功能:

    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/uaccess.h>
    
    static int major = 0;
    static const char *dev_name = "char_dev";
    
    static char dev_buf[4096];
    
    #define MIN(a, b) ((a) < (b) ? (a) : (b))
    
    static int char_dev_open(struct inode *node, struct file *file)
    {
    	// printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	return 0;
    }
    
    static int char_dev_close(struct inode *node, struct file *file)
    {
    	// printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	return 0;
    }
    
    static ssize_t char_dev_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
    {
    	int ret;
    	ret = copy_to_user(buf, dev_buf, MIN(size, 4096)); // 从内核空间拷贝数据到用户空间
    	return ret;
    }
    
    static ssize_t char_dev_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
    {
    	int ret;
    	ret = copy_from_user(dev_buf, buf, MIN(size, 4096)); // 从用户空间拷贝数据到内核空间
    	return ret;
    }
    
    static const struct file_operations char_dev_fops = {
    	.owner = THIS_MODULE,
    	.open = char_dev_open,
    	.release = char_dev_close,
    	.read = char_dev_read,
    	.write = char_dev_write,
    };
    
    static int __init char_dev_init(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	major = register_chrdev(0, dev_name, &char_dev_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
    	return 0;
    }
    
    static void __exit char_dev_exit(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	unregister_chrdev(major, dev_name); // 注销字符设备
    }
    
    module_init(char_dev_init); // 模块入口
    module_exit(char_dev_exit); // 模块出口
    
    MODULE_LICENSE("GPL"); // 模块许可
    

    重新编译后在开发板上安装模块,接着使用 mknod 来创建设备节点(文件),然后就可以使用前面的应用程序进行读写测试了:
    在这里插入图片描述
    卸载驱动后使用 mknod 创建的设备节点可以使用 rm 进行删除。

    自动创建与销毁设备节点

    上面测试中安装模块后还需要手动创建设备节点,其实也可以在安装时自动生成设备节点,在卸载时自动删除设备节点:

    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/uaccess.h>
    #include <linux/device.h>
    
    static int major = 0;
    static const char *char_dev_name = "char_dev";
    static struct class *char_dev_class;
    static struct device *char_dev_device;
    
    static char dev_buf[4096];
    
    #define MIN(a, b) ((a) < (b) ? (a) : (b))
    
    static int char_dev_open(struct inode *node, struct file *file)
    {
    	// printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	return 0;
    }
    
    static int char_dev_close(struct inode *node, struct file *file)
    {
    	// printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	return 0;
    }
    
    static ssize_t char_dev_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
    {
    	int ret;
    	ret = copy_to_user(buf, dev_buf, MIN(size, 4096)); // 从内核空间拷贝数据到用户空间
    	return ret;
    }
    
    static ssize_t char_dev_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
    {
    	int ret;
    	ret = copy_from_user(dev_buf, buf, MIN(size, 4096)); // 从用户空间拷贝数据到内核空间
    	return ret;
    }
    
    static const struct file_operations char_dev_fops = {
    	.owner = THIS_MODULE,
    	.open = char_dev_open,
    	.release = char_dev_close,
    	.read = char_dev_read,
    	.write = char_dev_write,
    };
    
    static int __init char_dev_init(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	major = register_chrdev(0, char_dev_name, &char_dev_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
    
    	char_dev_class = class_create(THIS_MODULE, "char_dev_class"); // 
    	if (IS_ERR(char_dev_class))
    	{
    		unregister_chrdev(major, char_dev_name);
    		return -1;
    	}
    	char_dev_device = device_create(char_dev_class, NULL, MKDEV(major, 0), NULL, char_dev_name); // 创建设备节点创建设备节点,成功后就会出现/dev/char_dev_name的设备文件
    	if (IS_ERR(char_dev_device))
    	{
    		device_destroy(char_dev_class, MKDEV(major, 0));
    		unregister_chrdev(major, char_dev_name);
    		return -1;
    	}
    
    	return 0;
    }
    
    static void __exit char_dev_exit(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    
    	device_destroy(char_dev_class, MKDEV(major, 0)); // 销毁设备节点,销毁后/dev/下设备节点文件就会删除
    	class_destroy(char_dev_class);
    
    	unregister_chrdev(major, char_dev_name); // 注销字符设备
    }
    
    module_init(char_dev_init); // 模块入口
    module_exit(char_dev_exit); // 模块出口
    
    MODULE_LICENSE("GPL"); // 模块许可
    

    重新编译后在开发板上进行测试:
    在这里插入图片描述

    使用 VS Code 进行开发

    驱动开发和内核中很多源码有交互,使用 VS Code 等工具进行开发可以方便编写代码,方便查阅源码,一定程度上可以提升开发效率。

    Ubuntu中可以使用下面命令来安装VS Code:

    sudo snap install code --classic
    

    通常VS Code中安装了C/C++支持后配置下内核源码位置即可:
    在这里插入图片描述

    .vscodec_cpp_properties.json 文件中内容如下,其中重要的就是内核源码路径:

    {
        "configurations": [
            {
                "name": "Linux",
                "includePath": [
                    "../../NUC980-linux-4.4.y/**"
                ],
                "intelliSenseMode": "linux-gcc-arm",
                "compilerPath": "/home/nx/nuc980-sdk/arm_linux_4.8/bin/arm-linux-gcc",
                "cStandard": "gnu89",
                "cppStandard": "gnu++98"
            }
        ],
        "version": 4
    }
    

    总结

    本文内容只是嵌入式Linux中最基础的驱动开发过程,实际去参考别的驱动的时候可能比上面要复杂些,一方面因为实际功能的实现,另一方面考虑兼容性和可移植性等从而对驱动和设备分层解耦等。

    虽然本文内容比较基础,但是如果只是自己简单用用的话也是可以的,毕竟简单。

    更多相关内容
  • 嵌入式Linux驱动开发

    2021-06-13 19:41:35
    说到Linux驱动,尤其是嵌入式Linux驱动,大家可能会望而却步,因为入门太难!很多书上或课程基本是这样的:一上来给我们...从最简单的入手,一步一步,手把手的编写代码,一步步引领你进入嵌入式Linux驱动开发的大门。
  • 【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.6.7z
  • I.MX6U嵌入式Linux驱动开发指南V1.5,原子出品,很详细,很不错的资料,希望对你的学习工作有所帮助。
  • 嵌入式Linux驱动开发指南V1.3.pdf
  • 百问网出品,嵌入式Linux驱动开发基础知识,非常基础的知识点,值得学习,十分适合嵌入式Linux初学者!!
  • imx6u嵌入式linux驱动开发指南-正点原子.zip
  • I.MX6U 开发指南 v1.5 版本 文字版PDF
  • 为以后的 Linux 驱动开发做准备,通过本篇大家可以掌握在 Ubuntu 下进 行 ARM 开发的方法。 第三篇: Uboot、 Linux 和根文件系统移植 本篇讲解如何将 Uboot、 Linux 和根文件系统移植到我们的开发板上,为后面的...
  • 【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.2.pdf
  • linux驱动学习
  • 重要-【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.pdf
  • 【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0-20191009.rar 1500多页的文档,Linux嵌入式开发非常详细的资料
  • 学习linux的详细教程,学过的都知道,一步一步的知识点,适合出现初学者,感兴趣学习的,一起交流学习,我也是初学者。
  • 【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.pdf.zip,【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.pdf
  • 嵌入式Linux设备驱动程序开发.pdf
  • 嵌入式linux驱动开发教程--源代码.rar 嵌入式linux驱动开发教程--源代码.rar
  • 正点原子IMX6ULL linux驱动开发文档V1.1, 1600页版本的开发手册,包含裸机开发、系统移植、linux设备驱动开发等等
  • 嵌入式Linux驱动开发整体框架

    千次阅读 2022-03-20 12:35:38
    文章目录Linux根目录说明文件类型说明嵌入式Linux驱动开发总结一、环境的搭建交叉编译工具链交叉编译工具链是干嘛的?c文件是如何被编译成可执行文件的Linux中设置环境变量二、裸机开发方式回顾一、STC89C51二、STM...

    Linux根目录说明

    在这里插入图片描述

    文件类型说明

    在这里插入图片描述
    在这里插入图片描述

    嵌入式Linux驱动开发总结

    一、环境的搭建

    一般以Linux(Ubuntu)为开发环境,嵌入式Linux开发的环境主要包括:交叉编译工具链,但由于一般用的都是Windows主机,所以要么双系统,要么配虚拟机。

    配虚拟机的话就需要打通Ubuntu与Windows之间的通道,比如文件的传送,我用的是FillZilla客户端(ftp协议)。

    交叉编译工具链

    交叉编译工具链是干嘛的?

    在学习C语言时,我记得用的是codeblocks,当我们写好c程序后点击编译运行就能看到cmd命令窗口输出的结果。其实在我们点击编译运行这个按钮时,就相当于使用命令gcc来编译c程序使之成为可执行文件(在Windows下是exe格式,在Linux中是elf格式)。但是编译生成的可执行文件只能在此电脑(我的时x86)上运行,不能在arm架构上运行,所以我们可以借助交叉编译工具在x86架构上编译生成可以在arm架构上运行的程序
    那为什么不直接在arm架构上编译c文件呢?
    第一:环境没有搭建好,比如开发板上连u-boot、Linux内核、跟文件系统都没有搭建好;
    第二:就算环境搭建好了,并且也把交叉编译工具链放到了开发板上,但是由于开发办性能不是很好,编译需要相对较长的时间。

    在Ubuntu中用gcc编译的c文件,用file命令查看生成的可执行文件:
    在这里插入图片描述

    在Ubuntu中用交叉编译工具编译的c文件:
    在这里插入图片描述

    c文件是如何被编译成可执行文件的

    参看:一段C语言代码编译、运行全过程解析

    预处理
    编译
    汇编
    预处理
    编译
    汇编
    main.c
    main.i
    mian.s
    main.o
    sub.c
    sub.i
    sub.s
    sub.o
    链接器
    glibc库
    a.out
    $  gcc  -E  hello.c  >  hello.i           //会生成预处理后的C源文件hello.i
    $  gcc  -S  hello.i                       //将hello.i编译成汇编文件hello.s
    $  gcc  -c  hello.s                       //将汇编文件hello.s汇编成hello.o
    $  gcc hello.o  -o hello                  //将目标文件链接成可执行文件hello
    $  ./hello                               // 运行可执行文件hello
    

    我们在Ubuntu中编写一个c文件后,用gcc就能编译链接成可执行文件。但具体过程是怎样的呢?由上面可知,需要预处理,编译,汇编,链接。编译时会去找头文件,去哪找呢?gcc工具链中(一个文件夹);编译时也需要库文件,去哪找呢?还是在gcc工具链中。

    在Ubuntu中用which gcc或whereis gcc可以找到gcc的路径,gcc命令是在/usr/bin下,而编译时所需的库文件在另外的地方,即/usr/lib中,/usr/include下是一些头文件。

    ps. Linux中的usr是指Unix System Resource,即Unix系统资源的缩写。/usr 是系统核心所在,包含了所有的共享文件。它是 unix 系统中最重要的目录之一,涵盖了二进制文件,各种文档,各种头文件,还有各种库文件;还有诸多程序,例如 ftp,telnet 等等。

    在这里插入图片描述
    在这里插入图片描述

    而对于交叉编译来说,Ubuntu是没有的,需要我们自己安装。交叉编译工具链我(正-原-的教程)安装到了/usr/local/arm中去了。如下,
    在这里插入图片描述
    bin目录下是一些可执行文件,就是我们常用的交叉编译工具链,如arm-linux-gnueabihf-gcc, arm-linux-gnueabihf-objdump, arm-linux-gnueabihf-ld等。其它目录就是一些库文件和头文件,也包含一些可执行文件。
    在这里插入图片描述

    Linux中设置环境变量

    如果没有设置环境变量的话,在某个shell中使用命令arm-linux-gnueabihf-gcc会报错,只有在交叉编译工具链的目录下才不会报错,解决方法就是将这一目录加入环境变量PATH中去。Linux下设置环境变量参请参考此

    针对对所有用户都有效的方法:打开/etc/profile文件,在末尾添加如下内容:

    export PATH=$PATH:路径,如export PATH=$PATH:/usr/local/arm/./bin

    二、裸机开发方式回顾

    不管什么开发,最终都是操作底层硬件寄存器。

    一、STC89C51

    这是最能体现直接操作寄存器的,比如初始化及配置串口时会直接操作相关寄存器。

    void SerialInit()
    {
    	TMOD=0x20;
    	TH1=0xf3;	//波特率为4800
    	TL1=0xf3;
    	PCON=0x80;	//SMOD为1,boundrate加倍
    	TR1=1;
    	SCON=0x50;
    	EA=1;
    	ES=1;
    }
    

    二、STM32

    学习stm32时,最开始选择的库函数版本,当时还有寄存器版本,到现在又有了hal库。寄存器版本和c51差不多,直接操作寄存器;而库函数和hal库版本都是间接操作寄存器,即调用相关API对寄存器进行操作。下面是库函数版本:

    u8 DS18B20_Init(void)
    {
     	GPIO_InitTypeDef  GPIO_InitStructure;
     	//使能PORTA口时钟
     	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
     	GPIO_InitStructure.GPIO_Pin = DS18B20IO;			
     	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 	
     	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
     	GPIO_Init(DS18B20PORT, &GPIO_InitStructure);
     	GPIO_SetBits(DS18B20PORT,DS18B20IO);    //输出1
    
    	return DS18B20_Check();
    }  
    

    三、嵌入式Linux下的开发

    其实在嵌入式Linux下开发的方式有很多,可以直接操作寄存器,也可以调用厂家的BSP包(相当于库函数),还可以采用框架(设备树、gpio和pinctrl子系统)等。整体过程如下:

    系统调用/sys_read
    设备树
    gpio/pinctrl
    platform
    APP/read
    Driver/read
    物理寄存器

    一、裸机式开发

    即直接操作寄存器,如下:

    /* main.h文件部分内容
     * 部分相关寄存器地址 
     */
    #define CCM_CCGR0 			*((volatile unsigned int *)0X020C4068)
    #define GPIO1_DR 			*((volatile unsigned int *)0X0209C000)
    /* main.c文件部分内容
     * 将GPIO1_DR的bit3清零	
     */
    void led_on(void)
    {
    	GPIO1_DR &= ~(1<<3); 
    }
    

    二、库函数式开发

    即使用厂商提供的API,如下:

    //bsp_clk.h头文件内容
    #ifndef __BSP_CLK_H
    #define __BSP_CLK_H
    
    #include "imx6ul.h"	//厂商写好的头文件
    /* 函数声明 */
    void clk_enable(void);
    
    #endif
    

    三、驱动框架下的开发

    此开发方式涉及设备树、gpio子系统、pinctrl子系统、platform平台总线等。比如可以直接在驱动程序中定义相关寄存器物理地址,然后编写相应的驱动函数(如read、write等);也可以将寄存器物理地址表示在设备树中,在驱动中使用相关函数获取设备树中的寄存器物理地址。

    什么是设备树

    设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。

    DTC-编译工具
    DTS-设备树源文件
    DTB-设备树二进制文件

    对于设备树,需掌握:

    1. 如何通过设备树描述开发板硬件资源,即需要学习设备树的一些语法。
    2. 如何通过Linux内核解析设备树,即如何获取设备树中相应设备的信息,比如寄存器物理地址,即需要掌握常用的OF操作函数
    3. 明白设备树和驱动程序是怎么联系起来的,即设备结点的compatible属性。

    设备树在Linux系统中的体现

    设备树在Linux中是以文件及文件夹形式显示。
    在这里插入图片描述

    Linux内核解析DTB文件

    Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件。

    四、Linux三巨头

    如果学习过 UCOS/FreeRTOS 应该知道,UCOS/FreeRTOS 移植 就是在官方的 SDK 包里面找一个和自己所使用的芯片一样的工程编译一下,然后下载到开发 板就可以了。那么 Linux 的移植是不是也是这样的,下载 Linux 源码,然后找个和我们所使用 的芯片一样的工程编译一下就可以了?很明显不是的!Linux 的移植要复杂的多,在移植 Linux 之前我们需要先移植一个 bootloader 代码,这个 bootloader 代码用于启动 Linux 内核(相当于Windows下的BIOS),bootloader 有很多,常用的就是 U-Boot。移植好 U-Boot 以后再移植 Linux 内核,移植完 Linux 内核以后 Linux 还不能正常启动,还需要再移植一个根文件系统(rootfs),根文件系统里面包含了一些最常用的命令和文件。所以 U-BootLinux kernelrootfs 这三者一起构成了一个完整的 Linux 系 统,一个可以正常使用、功能完善的 Linux 系统
    在这里插入图片描述

    一、u-boot

    bootloader程序会先初始化DDR等外设,然后将Linux内核从flash(NAND, NOR FLASH,SD,MMC 等)拷贝到 DDR 中,最后启动 Linux 内核。

    u-boot可以看作是一个功能丰富、复杂的裸机程序,在编译生成可执行文件前可以根据自己的需求配置相应的功能,比如USB、网络、SD卡等。配置好后通过以下命令编译。

    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-mx6ull_14x14_ddr512_emmc_defconfig
    make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j12
    

    如何配置呢?

    在uboot源码下的configs文件夹中,有许多配置文件,如下:
    在这里插入图片描述

    1. 复制mx6ull_14x14_evk_emmc_defconfig,然后重命名为mx6ull_alientek_emmc_defconfig,修改其内容为:

      CONFIG_SYS_EXTRA_OPTIONS="IMX_CONFIG=board/freescale/mx6ull_alientek_emmc/imximage.cfg,MX6ULL_EVK_EMMC_REWORK"
      CONFIG_ARM=y
      CONFIG_ARCH_MX6=y
      CONFIG_TARGET_MX6ULL_ALIENTEK_EMMC=y
      CONFIG_CMD_GPIO=y
      
    2. 在目录include/configs 下添加 ALPHA 开 发 板 对 应 的 头 文 件 ,复 制 include/configs/mx6ullevk.h,并重命名为 mx6ull_alientek_emmc.h。注意,头文件的头两行宏定义需要修改,根据头文件名修改,即#ifndef A, #define A改为#ifndef B, #define Bmx6ull_alientek_emmc.h中有很多宏定义,这些宏定义基本用于配置 uboot,也有一些 I.MX6ULL 的配置项目。如果我们自己要想使能或者禁止 uboot 的某些功能,那就在mx6ull_alientek_emmc.h里面做修改即可。

    uboot如何启动Linux的

    这个不用细究,真要用到时再研究。uboot 中有两个非常重要的环境变量 bootcmdbootargs

    bootcmd 保存着 uboot 默认命令,uboot 倒计时结束以后就会执行 bootcmd 中的命令,这些命令一般都是用来启动 Linux 内核的,比如读取 EMMC 或者NAND Flash 中的 Linux 内核镜像文件和设备树文件到 DRAM 中,然后启动 Linux 内核。

    bootargs 保存着 uboot 传递给 Linux 内核的参数,比如bootargs=“console= ttymxc0, 115200 root= /dev/mmcblk1p2 rootwait rw”

    执行以下命令设置两个环境变量的值,然后直接输入boot即可启动Linux内核。

    setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
    setenv bootcmd 'mmc dev 1; fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000;'	//从mmc启动
    setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000' //从网络启动
    saveenv
    

    二、Linux内核

    获取到源码后使用交叉编译工具链编译(因为我们是要移植到arm开发板上去)源码,命令如下:

    #!/bin/sh
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16
    

    编译完成以后就会在 arch/arm/boot 这个目录下生成一个叫做 zImage 的文件,zImage 就是我们要用的 Linux 镜像文件。另外也会在 arch/arm/boo/dts下生成很多.dtb文件,这些.dtb 就是设备树文件。

    进入到目录/sys/bus/cpu/devices/cpu0/cpufreq 中可以看到许多文件,这些文件是关于CPU频率等信息的。

    三、根文件系统

    Linux 中的根文件系统更像是一个文件夹或者叫做目录(在我看来就是一个文件夹,只 不过是特殊的文件夹),在这个目录里面会有很多的子目录。根目录下和子目录中会有很多的文 件,这些文件是 Linux 运行所必须的,比如库、常用的软件和命令、设备文件、配置文件等等。

    根文件系统是 Linux 内核启动以后挂载(mount)的第一个文件系统,然后从根文件系统中读取初始化脚本,比如 rcS,inittab 等。根文件系统和 Linux 内核是分开的,单独的 Linux 内核是没法正常工作的,必须要搭配根文件系统。如果不提供根文件系统,Linux 内核 在启动的时候就会提示内核崩溃(Kernel panic)的提示。

    根文件系统的这个“根”字就说明了这个文件系统的重要性,它是其他文件系统的根,没有这个“根”,其他的文件系统或者软件就别想工作。比如我们常用的 ls、mv、ifconfig 等命令其实就是一个个小软件,只是这些软件没有图形界面,而且需要输入命令来运行。这些小软件 就保存在根文件系统中,这些小软件是怎么来的呢?这个就是我们本章教程的目的,教大家来 构建自己的根文件系统,这个根文件系统是满足 Linux 运行的最小根文件系统,后续我们可以根据自己的实际工作需求不断的去填充这个最小根文件系统,最终使其成为一个相对完善的根文件系统。

    根文件系统的各个子目录

    1. /bin目录:
      看到“bin”大家应该能想到 bin 文件,bin 文件就是可执行文件。所以此目录下存放着系统需要的可执行文件,一般都是一些命令,比如 ls、mv 等命令。此目录下的命令所有的客户都可以使用。
    2. /dev目录:
      dev 是 device 的缩写,所以此目录下的文件都是和设备有关的,此目录下的文件都是设备文件。在 Linux 下一切皆文件,即使是硬件设备,也是以文件的形式存在的,比如/dev/ttymxc0(I.MX6ULL 根目录会有此文件)就表示 I.MX6ULL 的串口 0,我们要想通过串口0发送或者接收数据就要操作文件/dev/ttymxc0,通过对文件/dev/ttymxc0 的读写操作来实现串口0的数据收发。
    3. /etc目录:
      此目录下存放着各种配置文件,大家可以进入 Ubuntu 的 etc 目录看一下,里面的配置文件非常多!但是在嵌入式 Linux 下此目录会很简洁。
    4. /lib目录:
      lib 是 library 的简称,也就是库的意思,因此此目录下存放着 Linux 所必须的库文件。这些库文件是共享库,命令用户编写的应用程序要使用这些库文件。
    5. /mnt目录:
      临时挂载目录,一般是空目录,可以在此目录下创建空的子目录,比如/mnt/sd、/mnt/usb,这样就可以将 SD 卡或者 U 盘挂载到/mnt/sd 或者/mnt/usb 目录中。
    6. /proc目录:
      此目录一般是空的,当 Linux 系统启动以后会将此目录作为 proc 文件系统的挂载点,proc是个虚拟文件系统,没有实际的存储设备。proc 里面的文件都是临时存在的,一般用来存储系统运行信息文件。
    7. /usr目录:
      要注意,usr 不是 user 的缩写,而是 Unix Software[System] Resource 的缩写,也就是 Unix 操作系统软件资源目录。这里有个小知识点,那就是 Linux 一般被认为是类 Unix 操作系统,苹果的 MacOS也是类 Unix 操作系统。既然是软件资源目录,因此/usr 目录下也存放着很多软件,一般系统安装完成以后此目录占用的空间最多。
    8. /var目录:
      此目录存放一些可以改变的数据。
    9. /sbin目录:
      此目录页用户存放一些可执行文件,但是此目录下的文件或者说命令只有管理员才能使用,主要用户系统管理。
    10. /sys目录:
      系统启动以后此目录作为 sysfs 文件系统的挂载点,sysfs 是一个类似于 proc 文件系统的特殊文件系统,sysfs 也是基于 ram 的文件系统,也就是说它也没有实际的存储设备。此目录是系统设备管理的重要目录,此目录通过一定的组织结构向用户提供详细的内核数据结构信息
    11. /opt目录:
      可选的文件、软件存放区,由用户选择将哪些文件或软件放到此目录中。

    BusyBox构建根文件系统

    得到BusyBox源码后,按需要进行配置(也可以通过可视化配置),然后编译(指定编译结果存放目录rootfs)完成后得到binsbinusr三个目录,以及linuxrc 这个文件。前面说过 Linux 内核 init 进程最后会查找用户空间的 init 程序,找到以后就会运行这个用户空间的 init 程序,从而切换到用户态。如果 bootargs 设置 init=/linuxrc,那么 linuxrc 就是可以作为用户空间的 init 程序,所以用户态空间的 init 程序是 busybox 来生成的。

    1、向rootfs的/lib目录添加库文件

    Linux 中的应用程序一般都是需要动态库的,当然你也可以编译成静态的,但是静态的可执行文件会很大。如果编译为动态的话就需要动态库,所以我们需要先根文件系统中添加动态库,在 rootfs 中创建一个名为“lib”的文件夹

    lib 文件夹创建好了,库文件从哪里来呢?lib 库文件从交叉编译器中获取,前面我们搭建交叉编译环境的时候将交叉编译器存放到了“/usr/local/arm/”目录中。交叉编译器里面有很多的 库文件,这些库文件具体是做什么的我们作为初学者肯定不知道,既然我不知道那就简单粗暴的把所有的库文件都放到我们的根文件系统中。

    /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib
    

    此目录下有很多的so(是通配符)和.a 文件,这些就是库文件,将此目录下所有的so*和.a文件都拷贝到 rootfs/lib 目录中。

    /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/lib
    

    此目录下也有很多的的so和.a 库文件,我们将其也拷贝到 rootfs/lib 目录中

    2、向 rootfs 的usr/lib目录添加库文件

    在 rootfs 的 usr 目录下创建一个名为 lib 的目录,将如下目录中的 so 和.a 库文件都拷贝到 rootfs/usr/lib 目录中:

    /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/lib
    
    3、创建/etc/init.d/rcS文件

    rcS 是个 shell 脚本,Linux 内核启动以后需要启动一些服务,而 rcS 就是规定启动哪些文件的脚本文件。在 rootfs 中创建/etc/init.d/rcS 文件,然后在 rcS 中输入如下所示内容(创建好文件/etc/init.d/rcS 以后一定要给其可执行权限!):

    #!/bin/sh
    PATH=/sbin:/bin:/usr/sbin:/usr/bin
    LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
    export PATH LD_LIBRARY_PATH runlevel
     
    mount -a
    mkdir /dev/pts
    mount -t devpts devpts /dev/pts
    
    echo /sbin/mdev > /proc/sys/kernel/hotplug
    mdev -s
    
    4、创建/etc/fstab文件

    在 rootfs 中创建/etc/fstab 文件,fstab 在 Linux 开机以后自动配置哪些需要自动挂载的分区,格式如下:

    <file system> <mount point> <type> <options> <dump> <pass>
    proc 			/proc 		proc 	defaults 	0 	0
    tmpfs 			/tmp 		tmpfs 	defaults 	0 	0
    sysfs 			/sys 		sysfs 	defaults 	0 	0
    
    5、创建/etc/inittab文件

    inittab 的详细内容可以参考 busybox 下的文件 examples/inittab。init 程序会读取/etc/inittab这个文件,inittab 由若干条指令组成。每条指令的结构都是一样的,由以“:”分隔的 4 个段组 成,格式如下:

    格式:<id>:<runlevels>:<action>:<process>
    #etc/inittab
    ::sysinit:/etc/init.d/rcS
    console::askfirst:-/bin/sh
    ::restart:/sbin/init
    ::ctrlaltdel:/sbin/reboot
    ::shutdown:/bin/umount -a -r 7 ::shutdown:/sbin/swapoff -a
    

    至此,根文件系统基本构建完成。

    根文件系统初步测试

    测试方法就是使用 NFS 挂载,uboot 里面的 bootargs 环境变量会设置“root”的值,所以我们将 root 的值改为 NFS 挂载即可。

    setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.250:/home/zuozhongkai/linux/nfs/rootfs ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off' //设置 bootargs
    saveenv //保存环境变量
    
    软件运行测试

    我们使用 Linux 的目的就是运行我们自己的软件,我们编译的应用软件一般都使用动态库,使用动态库的话应用软件体积就很小,但是得提供库文件,库文件我们已经添加到了根文件系统中。我们编写一个小小的测试软件来测试一下库文件是否工作正常,在根文件系统下创建一 个名为“drivers”的文件夹,以后我们学习 Linux 驱动的时候就把所有的实验文件放到这个文件夹里面。

    再Ubuntu下编写hello,world文件,用交叉编译工具编译后拷贝到rootfs/drivers文件夹中去,然后在开发板上运行hello文件。

    五、驱动开发

    就是在Linux提供的框架下进行设备驱动的开发,主要分为:字符设备、块设备、网络设备,相关系统如下:
    在这里插入图片描述

    展开全文
  • 文章目录一、 Linux 内核自带 KEY 驱动使能二、Linux内核自带KEY驱动分析三、设备树节点编写 一、 Linux 内核自带 KEY 驱动使能   进入Linux内核源码根目录下,输入make menuconfig打开图形配置界面,按照以下...
  • 1, linux驱动一般分为3大类: * 字符设备* 块设备* 网络设备 2, 开发环境构建: * 交叉工具链构建* NFS和tftp服务器安装 3, 驱动开发中设计到的硬件: * 数字电路知识* ARM硬件知识* 熟练使用万用表...
  • 【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0.pdf 官方下载完整版,2019.10.26发布完整版
  • 【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.6.pdf
  • 【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0-尝鲜版,完美去除保护密码,可以复制!!!!!!!!!!!!

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 63,827
精华内容 25,530
关键字:

嵌入式linux驱动开发