Linux驱动开发一

一、Linux驱动开发与裸机开发的区别

1、开发思维区别

裸机驱动

(1)底层,跟寄存器打交道,有些MCU提供了库

Linux驱动

(1)Linux下驱动开发直接操作寄存器不现实

(2)根据Linux下的各种驱动框架进行开发。一定要满足框架,也就是Linux下各种驱动框架的掌握。

(3)驱动最终表现就是 /dev/xxx 文件。打开、关闭、读写等的文件操作(Linux下一切皆文件)。

(4)现在新的内核支持设备树。这是一个 .dts 文件,此文件,描述了板子的设备信息。(Linux驱动开发思维,如果有设备树的话,第一件是就是要去修改设备树,添加你板子的信息)

2、Linux驱动开发分类

在 /dev 目录下设备的区分

文件类型:

(1)c:字符设备

(2)b:块设备

(3)d:目录

(4)l:符号链接

        在 /dev 目录主要集中了 字符设备 和 块设备,字符设备以 c 作为开头,块设备以 b 作为开头

Linux驱动分为三大类:

1、字符设备驱动(最多的):

       

设备:

        LED、KEY、BEEP、声卡、显卡、摄像头、鼠标、键盘、触摸屏、手写板、USB、......

使用 ls /dev -l 命令查看字符设备:

特点:

        (1)设备类型是c。应用程序和驱动程序之间交互数据的时候,数据是以字节为单位;不同的设备类型,交互的数据格式是不一样的。

        (2)字符设备驱动数据是实时传递的,按照固定的格式传递。

应用程序访问方法:

        通过Linux系统IO函数访问:open()、read()、write()、ioctl()、close()、mmap()等。

       

        如触摸屏的访问:

        (1)struct input_event ts_ev;

        (2)int fd = open("/dev/input/event0", O_RDONLY);

        (3)read(fd, &ts_ev, sizeof(struct input_event));

        (4)close(fd);

2、块设备驱动(存储相关、大容量的存储设备):

        

设备:

        eMMC(nand flash:8GB)、SD卡、U盘、移动硬盘、......

使用 ls /dev -l 命令查看:

特点:

        (1)块设备是带有缓存的,当缓存满了(刷新缓存)这些数据才会写到设备上去。

        (2)块设备是有文件系统的(如开发板的跟文件系统的格式是ext4)。

        (3)数据是以块(block)为单位。

        (4)块设备文件类型是b。

应用程序访问方法:

        例如:

        U盘中有一个文件test.txt,编写一个程序,读取test.txt文件中的内容,并将该内容通过串口2发送出去。

        如何访问U盘?

        (1)挂载(mount):将块设备以某一种文件系统的格式挂载到根文件系统的某个目录上,再根据该目录访问块设备。有些嵌入式平台可以自动挂载U盘(实际上是做了相关的配置文件)。没有自动挂载,就采用手动挂载。

        (2)挂载好后,则可以访问:fd = open("/mnt/udisk/test.txt", O_RDONLY);

        (3)read函数

        (4)close(fd);

       

        如何访问串口?

        (1)fd = open("/dev/ttySAC2", O_RDWR);

        (2)初始化串口2

        (3)write(fd, buf, sizeof(buf));

        (4)close(fd);

3、网络设备驱动

设备:

        网卡(有线网卡、无线网卡).......

特点:

        网卡  -------------------------  物理层

        网络驱动层 -----------------  数据链路层

如何查看网络设备?

        使用命令 ifconfig -a

应用程序如何访问网络设备?

        socket套接字:

                (1)字节流  -----------------  TCP;

                (2)数据报  -----------------  UDP;

                (3)原始套接字  -----------  开发网络协议;

                (4)地址信息  --------------  IP地址和端口号。

       

        TCP的服务器:

                socket() / bind() / listen() / accept() / read() / recv() / write() / send() / close()

        TCP的客户端:

                socket() / connect() / read() / recv() / write() / send() / close()

驱动程序的特点

1、驱动程序是运行在Linux内核中的,而应用程序是运行在Linux的用户空间的。

2、每个硬件都需要有一个驱动程序,驱动程序是独立的。在一个应用程序中可以访问多个驱动程序。

        如:视频播放器 ------>> 显卡  +  声卡  + 鼠标

3、应用程序是有入口函数的,main 函数是入口。而应用程序是没有出口的。

     而驱动程序是有入口、有出口的:

     (1)入口函数:module_init(watchdog_init);

     (2)出口函数:module_exit(watchdog_exit);

     (3)安装驱动:

                insmod watchdog_drv.ko ---->> 自动调用入口函数module_init ----->> 进入 watchdog_init 函数 ---->> 从内核申请资源、向内核注册驱动、建立驱动模型......

      (4)卸载驱动:

                rmmod watchdog_drv.ko ---->> 自动调用出口函数module_exit----->> 进入 watchdog_exit 函数 ---->> 释放申请的资源、注销驱动......

4、编写应用程序的时候,我们可以使用 库函数 和 系统调用函数。

        那么应用程序使用的头文件是哪里来的(stdio.h、stdlib.h、string.h):

        (1)gcc:/usr/include/stdio.h

        (2)arm-linux-gcc:/usr/local/arm/5.4.0/usr/arm-none-linux-gnueabi/sysroot/usr/include/stdio.h

        编写驱动程序的时候,使用的头文件是哪里来的(linux/kernel.h、/linux/module.h):

        来自于Linux内核源码:/kernel/linux/module.h

5、函数的区别

        应用程序:printf()、malloc()、sleep()

        驱动程序:printk()、kmalloc()、ssleep()

        注意:

        虽然 printk 和 printf 函数非常相似,但是通常 printk 函数不支持浮点数,虽然能够编译成功,但是最终运行却得不到想要的结果

        整个内核空间的调用链上只有 4KB 或 8KB 的栈,相对于应用程序来说是非常小的,如果需要大内存的空间,需要使用专门的函数进行动态分配——kmalloc、zmalloc、vmalloc。

6、__init 和 __exit 关键字

        __init 用来修饰 初始化函数,一般情况下初始化函数只运行一次,运行结束以后,就会将该函数占用的内存释放掉。

7、驱动程序安装好以后,驱动程序不是一直运行的;而是安装到内核中的一个程序,只有应用程序去调用驱动程序,这个驱动程序才开始工作。

注意:驱动是安装在内存中正在运行的内核上

        一个设备不是说一定只属于某一个类型。比如USB WIFI,SDIO WIFI,属于网络设备驱动,因为它们又有USB和SDIO,因此也属于字符设备驱动。

        驱动的本质就是 获取外设,或者传感器数据,控制外设(比如说灯、蜂鸣器等)。驱动就只管获取这些数据,数据会提交给应用程序。

        那么,Linux驱动编译既要编写一个驱动,还要编写一个简单的测试应用程序(APP)。

二、内核态和用户态

        内核态与用户态是操作系统的两种运行级别。CPU提供了 Ring0-Ring3 这四种特权级别。Ring0级别最高,而Ring3级别最低。

        内核态(Kernel Mode),在内核模式下(运行内核和驱动程序),具有Ring0保护级别,代码具有对硬件所有控制权限。可以执行所有CPU指令,可以访问任意地址的内存。

        用户态(User Mode),在用户模式下(执行应用程序),具有Ring3保护级别,带么没有对硬件的直接控制权,也不能直接访问地址的内存。其程序是通过系统调用接口(System Call APIs)来达到访问硬件和内存

三、printk 函数

        在驱动开发当中,我们不能使用 printf 函数,智能使用 printk 函数,使用方法跟 printf 函数类似,但是也有点不同。

        用 dmesg 命令查看日志文件,它能够将开机到现在的所有调试信息打印出来

1、参考内核的一些驱动源码:

#inclde <linux/kernel.h>

使用方法如下所示:

printk(KERN_INFO "%s%s", tpk_tag, tmp);printk(KERN_WARNING "warning: 'lp=0x%x' is deprecated, ignored\n", x);printk(KERN_ERR "dtlk_read times out\n");

2、printk 函数打印优先级的宏定义,在 printk.h 文件中可以找到:

中文版:

#define KERN_EMERG	    "<0>"	/* 致命级:紧急事件消息,系统奔溃之前提示,表示系统不可用 */
#define KERN_ALERT	    "<1>"	/* 警戒级:报告消息,表示必须采取措施 */
#define KERN_CRIT	    "<2>"	/* 临界级:临界条件,通常涉及严重的硬件或软件操作失败 */
#define KERN_ERR	    "<3>"	/* 错误级:错误条件,驱动程序常用 KERN_ERR 来报告硬件错误 */
#define KERN_WARNING	"<4>"	/* 告警级:警告条件,对可能出现问题的情况进行警告  */
#define KERN_NOTICE	    "<5>"	/* 注意级:正常但又重要的条件,用于提醒 */
#define KERN_INFO	    "<6>"	/* 通知级:提示信息,如驱动程序启动时,打印硬件信息 */
#define KERN_DEBUG	    "<7>"	/* 调试级:调试级别的信息 */

        数值越小,优先级越高!!!

3、查看 printk 的打印优先级:

输入命令:

cat /proc/sys/kernel/printk

如下所示:

可见,该 printk 文件总共有 4 个值,这四个数值分别对应:

7:控制台打印级别,默认的消息日志级别优先级高于该值,消息就能够打印到控制台。

4:默认的消息日志级别,例如 printk("Led driver init...\n");

1:写到日志文件当中的最高优先级,能够通过 dmesg 命令查看。

7:写到日志文件当中的最低优先级,能够通过 dmesg 命令查看。

4、修改 printk 的打印优先级

(1)第一种方法:直接修改 printk 文件

输入以下命令:

echo 3 4 1 3 > /proc/sys/kernel/printk^C

如下所示:

(2)第二种方法:在 printk 函数添加优先级:

如:

printk("<3>" "Led driver init...\n");

也可以写为:

printk(KERN_ERR "Led driver init...\n");

四、设备号

        设备号的文档介绍可以去查看对应的内核源码下的 Documentation/devices.txt

        其实设备号等同于设备的身份证号码,方便系统进行管理。

        设备文件跟普通文件的区别在于:

        设备文件比普通文件多出了两个数字,这两个数字分别是 主设备号次设备号

(1)普通设备:

以 hello.c 文件讲解:

        其中,第一个字段(-rw-rw-r--)的第一个字符是 文件类型。这上面的 “-”,表示为 普通文件;如果是 “d”,就表示为 “目录”。然后第一个字段剩下的 9个字符 是 模式,其实就是 权限位(access permission bits)。它是 3个 为一组,每一组用 rwx 表示 “读(read)” “写(write)” “执行(execute)”。如果是字母,就说明有这个权限;如果是横线“-”,就说明没有这个权限。

        而这 3组 分别表示:文件所属的用户权限、文件所属的组权限、其他用户权限。如对于上述hello.c 文件中的 -rw-rw-r-- 就可以翻译为:这是一个普通文件,对于所属用户,可读可写不能执行;对于所属的组,可读可写不能执行;对于其他用户,仅仅可读。

        第二个字段(1)是 硬链接(hard link)数目

        第三个字段(wsm)是 所属用户

        第四个字段(wsm)是 所属组

        第五个字段(365)是 文件的大小

        第六个字段(Nov 6 08:08)是 文件被修改的日期

        最后一个字段(hello.c)是 文件名

(2)设备文件:

观察 fb0 设备文件,多出的数字为 “29,0”,这两数字含义如下:

        29:主设备号

        0:次设备号

主设备号:区分某一类的设备。

                  如:ttySAC 这是串口设备,主设备号为204;

                         mmcblk0 这是电子硬盘属于块设备,主设备号为179。

次设备号:用于区分同一类设备的不同个体或不同分区。

                  串口设备:串口0~串口3,则使用次设备号64~67进行区分。

                  电子硬盘设备:分区1~分区7,则使用次设备号1~7进行区分。

1、设备号的组成

        Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成主设备号表示某一个具体的驱动(它就像是一个品牌,比如华为品牌的手机),次设备号表示使用这个驱动的各个设备(它就像是华为手机的各个系列,如 mate 60、P60等等)。 Linux 提供了一个名为 dev_t 的数据类型表示设备号, dev_t 定义在文件 include/linux/types.h 里面dev_t 其实是unsigned int 类型,是一个 32 位的数据类型其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095
 

设备号相关的操作函数:

MAJOR :从 dev_t 中获取 主设备号,将 dev_t 右移 20 位即可。
MINOR :从 dev_t 中获取 次设备号,取 dev_t 的低 20 位的值即可。
MKDEV :将 给定的主设备号 和 次设备号的值 组合成 dev_t 类型的设备号。

部分代码如下:

static dev_t devno;                        //设备号
static int major =239;                     //主设备号
static int minor =0;                       //次设备号int __init pin4_drv_init(void)  //真实入口 
{devno = MKDEV(major,minor);  //创建设备号return 0;
}

系统中已经被驱动所使用的的主设备号可以在 /proc/devices 文件中查询。

2、设备号的分配

申请设备号,有静态注册和动态注册两种方式。

(1)静态申请设备号:

        在注册字符设备时,需要给设备指定一个设备号,这个设备号可以是驱动开发者静态指定的设备号。但要注意该设备号得是没有被内核开发者分配掉的设备号。

        我们可以使用以下命令进行查看当前系统中所有已经使用了的设备号:

cat /proc/devices

如下所示:

使用 register_chrdev 函数 注册字符设备 时,只需要给定一个主设备号即可:

/* 注册字符设备驱动 */
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);

(2)动态分配设备号:

        使用设备号时,向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。

        如果没有指定设备号的话就使用如下函数来申请设备号:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

        如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

参数讲解:

from :注册的设备号,如果要一次注册多个设备,from就是注册设备号的起始值

count :次设备的数量

name :设备名称,但是该名称不是 /dev 目录下设备文件的名字,而是在 /proc/devices 目录当中的名字。

返回值:

成功,返回0;

失败,返回负数的错误码。

动态分配设备号代码:

/* 注册字符设备驱动 */
/* 1、创建设备号 */
if (newchrled.major) 
{		/*  定义了设备号 */newchrled.devid = MKDEV(newchrled.major, 0);register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
} 
else 
{		/* 没有定义设备号 */alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);	/* 申请设备号 */newchrled.major = MAJOR(newchrled.devid);	/* 获取分配号的主设备号 */newchrled.minor = MINOR(newchrled.devid);	/* 获取分配号的次设备号 */
}
printk("newchrled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor);	

        注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count)

五、字符设备驱动

        字符设备 就是 一个一个字节,按照字节流进行读写操作的设备,读写顺序是分先后的。

Linux下的应用程序调用驱动程序流程如下所示:

        注意:应用程序不会直接调用系统调用,而是通过API函数来间接调用系统调用,比如:C库、POSIX、API等。UNIX操作系统中最常用的编程接口是POSIX。

        应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行于内核空间

        当我们在用户空间想要实现对内核的操作时,由于用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。

        应用程序使用到的函数在具体驱动程序中都有与之对应的函数。比如:应用程序调用open函数,那么在驱动程序中也得有一个open函数。

        每一个系统调用,在驱动中都有与之对应的一个驱动函数,在Linux内核文件 include/linux/fs.h 中有一个叫做 file_operations 的结构体,这个结构体是字符设备驱动的操作函数的集合

        对上述结构体中的比较重要的、常用的函数介绍如下(了解即可):

        (1)owner:拥有该结构体的模块的指针,一般设置为 THIS_MODULE 。

        (2)llseek 函数:用于修改文件当前的读写位置。

        (3)read 函数:用于读取设备文件。

        (4)write 函数:用于向设备文件写入(发送)数据。

        (5)poll 函数:轮询函数,用于查询设备是否可以进行非阻塞的读写。

        (6)unlocked_ioctl 函数:提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。

        (7)compat_ioctl 函数:与 unlocked_ioctl 函数功能一样。

                区别在于:

                1)在64位系统上,运行32位应用程序调用将会使用此函数。

                2)在32位系统上,运行32位应用程序调用将会使用 unlocked_ioctl 函数。

        (8)mmap 函数:将设备的内存映射到进程空间中(也就是用户空间)。一般帧缓冲设备会使用此函数,比如LCD驱动的显存,将帧缓冲(LCD显存)映射到用户空间中,然后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制了。

        (9)open 函数:用于打开设备文件。

        (10)release 函数:用于释放(关闭)设备文件,与应用程序的 close 函数对应。

        (11)fasync 函数:用于刷新等待处理的数据,用于将缓冲区中的数据刷新到磁盘中。

        (12)aio_fsync 函数:与 fasync 函数 的功能类似,只是该函数是异步刷新待处理的数据。

1、字符设备驱动开发流程

(1)在Linux下一切皆文件,驱动在加载成功后,会在 /dev/ 目录下生成一个相应的文件,应用程序通过对 /dev/xxx(xxx是具体的驱动文件名) 文件进行相应的操作即可实现对硬件的操作

(2)比如说 /dev/led 的驱动文件,这文件是led灯的驱动文件。

        对于文件而言,应用程序可以通过open函数打开 /dev/led 文件,使用完设备后,如果要关闭设备,用close函数关闭即可。

        如果要点亮(比如写1)或者关闭(比如写0)led,那么就是用write函数来进行操作;

        如果要获取led灯的状态,就用read函数从驱动中读取相应的状态即可。

(3)编写驱动的时候,也需要编写驱动对应的 open、close、write、read函数。字符设备驱动:file_operations。

        驱动是分驱动框架的,要按照驱动框架来编写,对于字符设备驱动来说,重点编写应用程序对应的open、close、write、read等函数

2、字符设备驱动框架

        字符设备驱动的编写主要就是驱动对应的open、close、read、write等函数的编写。其实就是file_operations 结构体的成员变量的实现。

3、驱动模块的加载与卸载

        Linux驱动程序可以编译到 kernel 里面(也就是 zlmage),当Linux内核启动的时候就会自动运行驱动程序;也可以编译为模块(即生成 .ko 文件)。编译成模块后,测试的时候,只需要加载 .ko 模块即可。

        驱动模块的加载与卸载函数如下所示:

        module_init(xxx_init); //注册模块加载函数
        module_exit(xxx_exit); //注册模块卸载函数

        module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时, xxx_init 这个函数就会被调用。

        module_exit函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

        编写驱动:

#include <linux/module.h>static int __int chrdevbase_init(void)
{//chrdevbase_init 函数名自己可自行更改return 0;
}static void __exit chrdevbase_exit(void)
{}//模块入口
module_init(chrdevbase_init);    //加载模块//模块出口
module_exit(chrdevbase_exit);    //卸载模块

编写驱动的时候需要注意的事项

        编译驱动的时候需要用到 Linux 内核源码!!因此要解压缩Linux内核源码,编译Linux内核源码。编译完Linux内核源码后,会得到 zlmage 和 .dtb 。需要使用使用编译后得到的 zlmage 和 .dtb 去启动系统。

4、编译驱动程序,创建 Makefile 文件

KERNELDIR := /home/wsm/SYSTEM/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

Makefile写好后,输入make命令即可编译驱动模块。 

上述Makefile文件讲解如下:

(1)KERNELDIR :表示开发板所使用的 Linux 内核源码目录,使用绝对路径,大家根据自己的实际情况填写即可。

(2)CURRENT_PATH :表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。

(3)obj-m:表示将 chrdevbase.c 这个文件编译为模块。

(4)$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules:

        modules :表示编译模块。

        -C :表示将当前的工作目录切换到指定目录中,也就是KERNELDIR目录。

        M:表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为 .ko 文件。

5、编译驱动的内核源码

1、内核源码的版本要与目标平台上运行的Linux内核的版本一致:

(1)开发板:用如下命令查看

uname -r

(2)Ubuntu:到所在的内核源码中,进Makefile查看

2、内核源码要正对目标平台正确的配置过:

可以用如下命令,以菜单的形式配置内核源码:

make menuconfig

3、内核源码要正确的编译过,才可以用这个内核源码来去编译驱动。

6、驱动的调试

(1)查看ko文件的信息(modinfo):

(2)查看ko文件的格式(file):

7、加载或者卸载编译好的模块

       (1) 将编译出来的 .ko 文件放到根文件系统里面。加载驱动会用到加载命令:insmod、modprobe。移除驱动使用 rmmod 命令。

        对于一个新的模块使用 modprobe 加载的时候需要先调用一下 depmod 命令。

        (2)驱动模块加载以后可以使用 lsmod 命令查看一下:

       加载安装驱动后,会生成以一个设备文件,而这个设备文件是我们应用程序去访问驱动程序的入口。

        (3)卸载模块使用 rmmod 命令:

8、内核模块参数

        比如我们编写一个串口驱动,想要在串口驱动加载的时候,波特率能够由命令行参数设定,就像运行普通的应用程序的时候,通过命令行来传递信息一样,应用程序示例如下(假设是一个UDP连接的程序):

int main(int argc,char **argv)
{argv[0];argv[1];......
}

在终端通过命令行来传递参数:

./udp 192.168.1.100

        模块参数允许用户在加载模块的时候,通过命令行获得参数值,内核支持的参数:bool、反转bool值、charp(字符串指针)、short、int、long、ushort、uint、ulong类型,这些类型可以对应于整型、数组、字符串。

insmod led_drv.ko 各种参数

(1)module_param 与 module_param_array 宏定义

module_param(name, type, perm)module_param_array(name, type, nump, perm)/* 参数释义 */
name:变量的名字
type:变量或数组元素的类型
nump:数组元素个数的指针,可选。默认写NULL。
perm:在 sysfs 文件系统中对应的文件的权限属性,决定哪些用户能够传递哪些参数,如果该用户权限过低,则无法通过命令行传递参数给该内核模块。

(2)示例源码

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>static int baud = 9600;    //默认波特率
static int port[4] = {0,1,2,3};
static char *name = "vcom";//通过以下宏定义来接收命令行的参数
module_param(baud, int, 0644);                  //0644 对应的权限为:rw- r-- r--
module_param_array(port, int, NULL, 0644);      //0644 对应的权限为:rw- r-- r--
module_param(name, charp, 0644);                //0644 对应的权限为:rw- r-- r--//入口函数
static int __init led_init(void)
{printk("led init...\n");printk("baud = %d\n",baud);printk("port = %d %d %d %d\n",port[0],port[1],port[2],port[3]);printk("name = %s\n",name);return 0;
}//出口函数
static void __exit led_exit(void)
{printk("led exit...\n");
}//驱动程序的入口
module_init(led_init);//驱动程序的出口
module_exit(led_exit);//模块描述
MODULE_AUTHOR("1971363937@qq.com");         //作者信息
MODULE_DESCRIPTION("Led driver");           //模块功能说明
MODULE_LICENSE("GPL v2");                   //许可证,驱动遵循的协议

(3)编译运行

将编译好的驱动下载到板子上:

通过U盘的方式,将 .ko 驱动 复制 到 板子上:

加载驱动:

a、直接按照默认的加载驱动(不使用参数):

insomd leddriver.ko

b、加载时更改默认波特率、端口、名字等内容(动态添加参数方法):

insmod leddriver.ko baud=115200 port=1,2,3,4 name="tcom"

如下所示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/208526.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【MATLAB源码-第97期】基于matlab的能量谷优化算法(EVO)机器人栅格路径规划,输出做短路径图和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 能量谷优化算法&#xff08;Energy Valley Optimization, EVO&#xff09;是一种启发式优化算法&#xff0c;灵感来源于物理学中的“能量谷”概念。它试图模拟能量在不同能量谷中的转移过程&#xff0c;以寻找最优解。 在EVO…

Springboot+FastJson实现解析第三方http接口json数据为实体类(时间格式化转换、字段包含中文)

场景 若依前后端分离版手把手教你本地搭建环境并运行项目&#xff1a; 若依前后端分离版手把手教你本地搭建环境并运行项目_前后端分离项目本地运行-CSDN博客 在上面搭建SpringBoot项目的基础上&#xff0c;并且在项目中引入fastjson、hutool、lombok等所需依赖后。 系统需…

K8S学习指南(1)-docker的安装

文章目录 引言1. Windows 系统中安装 Dockera. 确认系统要求b. 下载 Docker Desktopc. 安装 Docker Desktopd. 配置 Docker Desktope. 验证安装 2. Ubuntu 系统中安装 Dockera. 更新包列表b. 安装依赖包c. 添加 Docker GPG 密钥d. 添加 Docker APT 仓库e. 安装 Dockerf. 添加用…

unity 2d 入门 飞翔小鸟 小鸟跳跃 碰撞停止挥动翅膀动画(十)

1、切换到动画器 点击make transition和exit关联起来 2、设置参数 勾选掉Has Exit Time 3、脚本给动画器传参 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Fly : MonoBehaviour {//获取小鸟&#xff08;刚体&#xff09;p…

linux常用命令-pip命令详解(超详细)

文章目录 前言一、pip命令介绍1. pip命令简介2. pip命令的基本语法3. 常用的pip命令选项4. 常用的pip命令参数 二、pip命令示例用法1. 安装包2. 卸载包3. 列出已安装的包4. 搜索包5. 升级包 总结 前言 pip 是 Python 的包管理器&#xff0c;用于安装和管理 Python 包。它提供了…

JVM常见垃圾回收器

串行垃圾回收器 Serial和Serial Old串行垃圾回收器&#xff0c;是指使用单线程进行垃圾回收&#xff0c;堆内存较小&#xff0c;适合个人电脑 Serial作用于新生代&#xff0c;采用复制算法 Serial Old作用于老年代&#xff0c;采用标记-整理算法 垃圾回收时&#xff0c;只有…

Windows 系统,TortoiseSVN 无法修改 Log 信息解决方法

使用SVN提交版本信息时&#xff0c;注释内容写的不全。通过右键TortoiseSVN的Show log看到提交的的注释&#xff0c;右键看到Edit log message的选项&#xff0c;然而提交后却给出错误提示&#xff1a; Repository has not been enabled to accept revision propchanges; ask …

linux如何删除大文件的第一行(sed)

可以用sed命令实现&#xff1a; 删除文档的第一行 1. sed -i 1d <file>删除文档的最后一行 1. sed -i $d <file>在文档指定行中增加一行 # 示例如下&#xff1a; echo "1"; echo "2"; echo "4"; echo "5"; # 想要在echo…

【PHP】php发送邮箱验证码格式美化,样式美化

效果展示&#xff1a; 格式美化前 格式美化后 代码 大多数框架都自带有封装好的发送email方法&#xff0c;就不多赘述&#xff0c;主要写格式&#xff1a; <? php// 验证码过期时间 $expire 120; // 发件人邮箱 $from_email xx163.com; // 收件人 $to_email to163.com…

硬件产品经理常用的ChatGPT通用提示词模板

产品策略&#xff1a;请帮助我制定一个硬件产品的产品策略。 市场调研&#xff1a;如何进行硬件产品的市场调研&#xff1f; 用户需求&#xff1a;如何确定硬件产品的用户需求&#xff1f; 产品设计&#xff1a;如何设计一个优秀的硬件产品&#xff1f; 用户体验&#xff1…

数据分析基础之《matplotlib(5)—直方图》

一、直方图介绍 1、什么是直方图 直方图&#xff0c;形状类似柱状图却有着与柱状图完全不同的含义。直方图牵涉统计学的概念&#xff0c;首先要对数据进行分组&#xff0c;然后统计每个分组内数据元的数量。在坐标系中&#xff0c;横轴标出每个组的端点&#xff0c;纵轴表示频…

无人机巡山护林,林业无人机智能助力绿色守护

随着全球环保意识的不断提高&#xff0c;无人机巡山护林已经成为解决森林巡检难题的一种独特而高效的方式。在我国&#xff0c;各地正积极探索无人机在森林防火、病虫害监测以及生态调查等领域的创新应用。随着无人机技术的不断演进&#xff0c;其在推动森林保护和可持续发展方…

HTML实现每天单词积累

注册页面 <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>注册</title><style>body {font-family: Arial, sans-serif;background-color: #f5f5f5;}form {max-width: 500px;margin: 50px auto;padding: 40px…

【Docker】进阶之路:(九)Docker网络

【Docker】从零开始&#xff1a;19.Docker网络 Docker网络模式简介bridge网络模式host网络模式none网络模式container网络模式user-defined网络模式1.创建自定义的bridge网络2.使用自定义网络 高级网络配置docker network命令 为什么要了解容器的网络模式? 首先&#xff0c;容…

spark 写入 mysql 报错

报错信息如下&#xff1a; "C:\Program Files\Java\jdk1.8.0_291\bin\java.exe" "-javaagent:D:\Hadoopruanjian\IDEA\IntelliJ IDEA 2021.3.2\lib\idea_rt.jar60971:D:\Hadoopruanjian\IDEA\IntelliJ IDEA 2021.3.2\bin" -Dfile.encodingUTF-8 -classpat…

工业级路由器在风力发电场的远程监控技术

工业级路由器在风力发电场的远程监控技术方面具有重要的应用意义。风力发电场通常由分布在广阔地区的风力发电机组组成&#xff0c;需要进行实时监测、数据采集和远程管理。工业级路由器作为网络通信设备&#xff0c;能够提供稳定可靠的网络连接和多种远程管理功能&#xff0c;…

深入探讨Go语言协程调度:GRM模型解析与优化策略

一、线程调度 1、早期单线程操作系统 一切的软件都是跑在操作系统上&#xff0c;真正用来干活&#xff08;计算&#xff09;的是 CPU早期的操作系统每个程序就是一个进程&#xff0c;直到一个程序运行完&#xff0c;才能进行下一个进程&#xff0c;就是“单进程时代”一切的程…

ES6中新增的基本数据类型----symbol

前言 Symbol 基本数据类型 独一无二得值 Symbol函数创建 接收字符串 对symbol值得描述 let s1 Symbol(描述) /*** symbol 基本数据类型 表示独一无二的值 Symbol函数创建独一无二得值 参数可以是唯一值得描述*/ let sy1 Symbol();//创建好一个独一无二得值 let sy2 Symbo…

EXP-00056: 遇到 ORACLE 错误 12154 ORA-12154: TNS: 无法解析指定的连接标识符

exp oas/oasoas filed:\daochu.dmp owner(s) 导出特定用户 //exp 用户名/密码数据库 filed:\daochu.dmp owner(用户名) 1.重启oracle监听 cmd 中输入 services.msc 找到服务&#xff1a;OracleOraDb10g_home1TNSListener 与 OracleServiceORCL。 把两个服务启动. 若未解决…

Vue 3 + Tailwind CSS:打造现代化项目的完美组合

Vue 3 Tailwind CSS&#xff1a;打造现代化项目的完美组合 本篇教程将向你介绍如何将 Tailwind CSS 与 Vue 3 项目搭配使用&#xff0c;为你的项目提供现代化的 UI 呈现和开发体验。通过本文的逐步演示和示例代码&#xff0c;你将很快掌握在 Vue 3 中集成和使用 Tailwind CSS…