# rv1103_study **Repository Path**: L__Can/rv1103_study ## Basic Information - **Project Name**: rv1103_study - **Description**: 这是研究pico_plus开发板,板载瑞芯微的rv1103的研究笔记 - **Primary Language**: C++ - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 1 - **Created**: 2024-01-23 - **Last Updated**: 2025-12-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RV1103学习笔记 ## 一、rv1103介绍 rv1103是瑞芯微推出的一个 ~~经济型的~~ soc芯片,基于linux开发。板载a78内核以及一块mcu协处理器,用于音视频开发。 ## 二、rv1103与开发板lockFox pico_plus - 开发板外形 ![开发板图片](./开发板图片.png) ![引脚图](./pico_plus芯片引脚图.png) - [开发资料](https://spotpear.cn/index/study/detail/id/1159.html) - 从镜像开始
下载了usb驱动(DriverAssitant_v5.12.7z)后,解压缩,不断打开子文件夹会发现一个叫做DriverInstall.exe的程序。管理员打开后点击按钮“安装驱动”就会把驱动打上。
然后买一张sd卡(我把镜像烧录在了sd卡里,当然也可以使用pico_plus板载的64md flash,不过流程不一样),下载芯片烧录工具(SocToolKit.7z),解压后打开子文件夹发现一个叫做SocToolKit.exe的程序打开。会让我们选择芯片,pico_plus或者pico开发板使用rv1103芯片。选择了芯片后点自己"sd卡工具" ![烧录](./烧录.png),即可制作完成。插入到开发板中,并连接到电脑。当然如果想要使用板载的flash,那么选择到“固件下载”->“usb设备”->“固件”->“升级”,即可。镜像选择"update.img" - 使用串口连接开发板,usb转串口工具连接开发板引出来的UART_TX_M1/UART_TX_M2口。我们使用xshell来当串口收发器。 ![会话配置](./xshell会话选项.png) ![测试连接](./xshell测试连接.png) 当然了,我电脑会话配置的端口是com11是因为我把usb转串口接到了电脑的com11。具体自己的电脑连到了哪里可以用设备管理器查看。 - 使用adb连接开发板,就不需要一个额外的usb转串口工具。下载adb工具(adb_fastboot.zip),解压后就是一个免安装程序,把这个文件夹的存放地址放到系统变量里 ![把adb工具放到环境变量](./环境变量设置.png)
之后打开“win + R”控制台,使用下面的命令去连接到开发板。 ```c adb connect #设备连接网口后的分配的ip,如果是usb的话不需要理会这条 adb devices #显示连接到pc的设备 adb -s shell #连接到设备 ``` ![示例图](./控制台示例图.png) ## 三、使用windows10/11来运行编译固件 - 在这里演示使用windows来编译固件,具体原理是使用wsl2来给windows增加一个linux子系统。(我觉得使用vmware虚拟机更友好) - 在这个[>网址<](https://learn.microsoft.com/zh-cn/windows/wsl/install)按照步骤安装好wsl、linux发行版,我这里安装了ubuntu-22.04,没有使用图形界面 ```c++ 1、dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart 2、dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart 3、双击安装progarm文件夹中的wsl_update_x64.msi程序安装wsl内核 4、wsl --set-default-version 2 5、打开 Microsoft Store,并搜索ubuntu,下载后会提示安装<可以自己选择一个带图形界面的os> 6、设置密码 7、关闭终端后可以直接键入 wsl 来打开运行linux ``` - 安装好了linux后就可以开始愉快的使用windows开发linux,虽说这样很方便但是自我感觉还是不如直接做双系统或者直接买一个小主机安装linux。 - 打开linus,在根目录的/mnt目录就可以看到基本把windows里的盘符都装载了。这样就可以满足需求:在windows编辑在linux编译而后使用windows的烧录工具做开发。虽说这样子跨文件系统编译性能会打折扣,但是我电脑性能还ok,所以完美!!!一般电脑不行的不建议直接把工程放在windows目录下,而应该使用linux目录下,linux目录可以使用windows文件管理器管理,打开文件管理器就会看到多出一个linux图标,当他是一个目录使用即可。![图标](./文件管理器多出的linux图标.png) - 双系统之间互相访问运行对方的程序也是可以做到的,但是我目前没有这个需求,所以暂时就不探索这个玩法了。 - 微软官方也有使用vscode来进行开发wsl的工程的实例,使用linux目录的开发方式是要探索这个玩法的,工程存放在windows目录下需要担心vscode路径问题。 - 一切就绪后,就可以尝试着编译一次rv1103的工程,暂时使用不做改动先做一次编译。 - 先给linux安装环境,可能会出现“[build.sh:error] Running parse_partition_env failed![build.sh:error] exit code 127 from line 1214:”的提示,复制下面的命令执行。 ```c++ 1、 sudo apt-get install repo git ssh make gcc gcc-multilib g++-multilib module-assistant expect g++ gawk texinfo libssl-dev bison flex fakeroot cmake unzip gperf autoconf device-tree-compiler libncurses5-dev pkg-config # 安装依赖软件 2、 cd {SDK_PATH}/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf #SDK_PATH就是你的工程路径,cd到该处 3、 source env_install_toolchain.sh # 添加环境变量 ``` - 运行下下面的代码来clone仓库到本地<内地github连接一般不稳定,请配合些许有益身心的工具使用> ```c++ 1、cd ~ 2、git clone https://github.com/LuckfoxTECH/luckfox-pico.git 或者也可以使用gitee的:git clone https://gitee.com/LuckfoxTECH/luckfox-pico.git 3、cd luckfox-pico/ 4、git submodule update --init --recursive ``` - 我这里用了ubuntu22.4的版本,直接执行上面的命令遇到了很多的问题,先是需要搭梯子连外网的源否则提示“Unable to locate package ***”,紧急买了个梯子上房揭瓦。然后“cd到工程目录的根目录并输入'./bulid.sh lunch'来选择开发板,我用了pico plus开发板并且准备使用板载的nand flash来存储程序所以输入了一个7并回车”。然后'sudo ./build.sh'编译结果报错“./build.sh: line 1214: bc: command not found”,那就'sudo apt-get install bc'装一个bc计算器。然后再编译再报错,艹,没找到python,ubuntu有python3应该不会报错才对,一番查找发现是因为没有python,控制台键入“python”找不到指令,破案。看样子“python”命令链接不到python3上去,我这里输入命令“sudo ln -s /usr/bin/python3 /usr/bin/python ”给python3起个别名,“/usr/bin/python3”是我计算机中python3的存放位置。“./build.sh”再编译一次,报错,艹!一看是没有添加sudo运行。键入“sudo ./bulid.sh”后选择自己的开发板后等了个半个小时运行ok。编译成功,至此这块开发板第一步解决。(我发现直接使用linux电脑编译速度只要10分钟,难顶,而且linux电脑的性能是很垃圾的) - 编译出来的文件放到了"./output/image/"文件夹下面
![地址](./编译出的固件地址.png) - 可以使用```./build.sh lunch```重新选择编译哪个开发板,具体可以参考下面列表 ```shell Usage: build.sh [OPTIONS] Available options: lunch -Select Board Configure env -build env meta -build meta (optional) uboot -build uboot kernel -build kernel rootfs -build rootfs driver -build kernel's drivers sysdrv -build uboot, kernel, rootfs media -build rockchip media libraries app -build app recovery -build recovery tool -build tool updateimg -build update image unpackimg -unpack update image factory -build factory image all -build uboot, kernel, rootfs, recovery image allsave -build all & firmware & save clean -clean all clean uboot -clean uboot clean kernel -clean kernel clean driver -clean driver clean rootfs -clean rootfs clean sysdrv -clean uboot/kernel/rootfs clean media -clean rockchip media libraries clean app -clean app clean recovery -clean recovery firmware -pack all the image we need to boot up system ota -pack update_ota.tar save -save images, patches, commands used to debug check -check the environment of building info -see the current board building information ``` - 加入之前已经编译一次,再rebuild,需要运行```sudo ./buish.sh clean```清除一下编译文件,否则会因为文件存在无法清除导致编译失败 - 更多编译rv1106/rv1103编译指令使用可以参考这个文档[->this-<](./rv1106编译.md) ## 四、编辑代码 - 使用wsl内打开windows的vscode来编辑工程。只要cd到工程根目录上一级,然后"code luckfox-pico/"使用windows的vscode来打开工程,期间wsl会下载一些依赖,等一会即可。然后开始愉快的编程。 ## 五、使用linux发行版来编辑与编译 - 上面讨论了使用windows官方的wsl来开发linux程序。但是直接使用linus发行版系统开发也是可以的,比如我就给自己的一台n5100主机装了一个deepin系统。(现在n100小主机也很便宜了,用来给一些简单的小型项目开发还是没问题的,但是无论是n5100还是n100都是最大支持内存16g是个硬伤,一般建议编译使用的内存是32g,但是编译rv1103实测还ok) - 我把使用linux开发rv1103的文档放在了这里[>linux环境下的rv1103开发方法<](./linux环境.md) ## 六、rv1103介绍 - 虽然rv1103这颗芯片已经在数据手册详细的介绍了,但在这个文档里简要的简介还是有必要的,毕竟也没有多少人因为只是要找一些常用的资料来翻找数据手册。当然数据手册可以在这里找到[>数据手册与文档<](./pico_plus/doc/Rockchip%20RV1103%20Datasheet%20V1.0-20220406.pdf),你也可以看看幸狐开发板的开发资料[>应用指南<](.//Rockchip_Developer_Guide_Linux_RV1106_ACodec.pdf) > 1、资源 > > - 处理器:Cortex A7 @1.2GHz + RISC-V MCU > - 神经网络处理器 (NPU):0.5TOPS,支持 int4、int8、int16 > - 图像处理器 (ISP):输入 4M @30fps (Max) > - 内存:64MB > - USB:USB 2.0 Host/Device > - 摄像头接口:MIPI CSI 2-lane > - GPIO:24 个 GPIO 引脚 > - 网口:10/100M Ethernet controller and embedded PHY > - 默认存储介质 > - Luckfox-Pico:Micro SD 卡座 > - Luckfox-Pico-Plus:SPI NAND FLASH (1Gb) > ![alt text](image-1.png) > > 2、我关注的资源 > > - rv1103带两个adc输入通道,可以用来做左右麦克风收音,数据手册实际标注的指标是-3db下THD+N可以做到-82db,一般来说这个值还可以,用来做做简单的非专业用途的设计没问题。同时带dac功能,但是只有一个通道,指标标注600欧姆LINE output THD+N可以做到-3db下-84db,这个指标也是不算好不算差。跟专业设备的SNR(A)能做到-110db以下的设备比起来差了很多。失真估计是0.003%,还行吧。还是只能做做小东西。但是带了iis接口,可以接专业的dac使用。 > - 带MIPI摄像头接口,好评。可以实现2k 30帧的录制。 > - 带2个spi,可以实现带一个小的显示屏。因为是单核的原因,过高的负载很容易卡顿,系统优化是一个大坑。 > - 带一些基本的比如说uart、iic、pwm、dac接口,可以方便的连接外设。而且还带了一个risc-v的mcu可以用来简化linux程序的复杂性把一些需要高度实时行的偏向控制的操作丢给mcu。 > - 还带了npu,可以用来跑一个很简洁的模型,来做摄像头或者语音识别。 > > 3、感想 > > - rv1103这个小芯片非常适合用来做自动机器人,带基本的语音、摄像,还有方便的mcu来解耦控制程序,越看越适合 ## 七、makefile ### 1、makefile的书写规则 - 在GNU官网上就有make的详细文档,也可以看这里[>makefile.html<](./make/GNU%20make.html)、[>makefile.pdf<](./make/make.pdf) - 介绍makefile的基本规则: > - 书写基本结构:target ... : perequisites ... > - 伪目标:.PHONY : ... > - 放弃默认目标指定一个目标:.DEFAULT_GOAL = ... > - 宏定义:... = ... > - 注释:# ... > - 多行书写: > >> - 有空格:\ >> - 无空格:&\ >> > - makefile的三种文件命名:GNUmakefile 、makefile 、Makefile > - 包含其他makefile文件:include ./path (其他子makefile文件经常被命名为makefile.build或者makefile.mk) > - 一个代表默认头文件路径的宏:.INCLUDE_DIRS > - gcc指令中指定编译的头文件路径:-I ... > - 定义编译用的临时环境变量:export name = value_or_string > - make中用于给终端打印log的指令:$(info txt) > - 通配符:*.o (obj =*.o的变量定义,在命令行中可能会被误解为指令) > - 指定文件查找路径的指令:vpath fileName path1:path2(文件名可以联动通配符) > - shell中make命令的一些常用[option]: > >> - make -n : 不真正执行目标只打印命令 >> - make -p :打印变量 >> - make -I :指定头文件路径 ### 2、从一个makefile文件理解使用与书写 ```shell ifneq ($(KERNELRELEASE),) obj-m:=gpioDev.o else KERNELDIR := ~/linux_dev_project/doc/luckfox-pico/sysdrv/source/kernel CURRENT_PATH := $(shell pwd) bulid: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean endif ``` ## 八、从编写一个打印hello world驱动开始学习linux ### 1、基础知识 - linux里设备可以分为是:字符设备、块设备、网络设备(当然一个实际的设备也可能同时是字符设备跟块设备甚至是网络设备) - 字符设备的开发直接套模板去实现```struct file_operations{};```的结构体内的一些函数,用到那些就实现哪些。当然,最常用的就是```open()、write()、read()、close()```以及模块入口跟出口并通过这两个函数注册```module_init()、module_exit()``` ```c++ struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); ANDROID_KABI_RESERVE(1); ANDROID_KABI_RESERVE(2); ANDROID_KABI_RESERVE(3); ANDROID_KABI_RESERVE(4); } __randomize_layout; ``` - 驱动模块的加载可以直接编译进内核镜像也可以编译成模块(下面会演示编译成模块) ### 2、一个最简驱动编写 - 在自己git clone的linux内核的根目录下运行下面的命令 ```shell mkdir myDrives cd myDrives mkdir oneCharDev touch chardevBase.c vscode . ``` - 然后在vscode里把工作区另存到当前的文件夹,创建出oneCharDev.code-workspace文件,再然后用vscode的指令创建出c_cpp_properties.json文件,并把下面的内容复制到文件中(当然里面的头文件路径需要自己改改) ```json { "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**", "~/linux_dev_project/doc/luckfox-pico/sysdrv/source/kernel/include/linux", "~/linux_dev_project/doc/luckfox-pico/sysdrv/source/kernel/arch/arm/include", "~/linux_dev_project/doc/luckfox-pico/sysdrv/source/kernel/arch/arm/include/generated", "~/linux_dev_project/doc/luckfox-pico/sysdrv/source/kernel/include", "~/linux_dev_project/doc/luckfox-pico/sysdrv/source/kernel/arch/arm/include/uapi", ], "defines": [], "compilerPath": "", "cStandard": "c17", "cppStandard": "gnu++14", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 } ``` - 再然后开始编写驱动文件,下面是一个char设备驱动写法,可以在下次还写char设备时copy进新项目 ```c #include "linux/module.h" #include "linux/fs.h" static int __init chrdevbase_init(void) { printk("init module\n"); return 0; } static void __exit chrdevbase_exit(void) { printk("exit module\n"); } MODULE_LICENSE("GPL"); module_init(chrdevbase_init); module_exit(chrdevbase_exit); ``` - 这是我vscode工程的文件夹目录,看看自己是不是这个样子的 ![alt text](image-2.png) - 然后用```touch Makefile```命令创建编译文件,把下面的代码copy进去 ```make KERNELDIR := ~/linux CURRENT_PATH := $(shell pwd) obj-m = chardevBase.o bulid: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean ``` - 看到该编译代码中并不是编译的是luckfox使用的内核,而是我下载的一个linux最新内核用于做实例。基于luckfox做开发会在下章节讲,这个章节是用于演示驱动的基本流程。值得注意的是,我下载的linux 第一次make时提示缺少文件```/elf.h:10:10:fatal error:gelf.h:没有那个文件或目录 #include "gelf.h~"```,可能是下载时有些包没有下全,如果遇到同样的问题使用下面的指令下载即可```sudo apt-get install libelf-dev```。 - 然后cd进自己写的驱动源码目录,```make```开启编译。ok后就会看到自己的文件夹下多了*.ko的文件,使用命令```insmod youDrivername.ko```即可加载驱动进自己的系统,当然因为我当前使用的pc系统内核跟我编译使用的系统内核不一致所以是无法在我这台pc上加载该驱动的,所以使用哪个kernel version要注意跟自己linux发行版系统的一致才可以。 ### 3、在luckfox开发板上insmod自己编写的gpio驱动程序 - 在上一个章节演示了一个驱动的最简编写方式,这一节演示如何使用luckfox开发板编写gpio驱动程序,并点亮一个led灯。 - 在上一章节的驱动源码基础上,要想编译成功需要增加一些东西 ```c #include "linux/module.h" #include "linux/fs.h" static int __init chrdevbase_init(void) { printk("init module\n"); return 0; } static void __exit chrdevbase_exit(void) { printk("exit module\n"); } module_init(chrdevbase_init); module_exit(chrdevbase_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Luckfox"); MODULE_VERSION("V1.0"); ``` - 驱动要记得修改成luckfox的linux内核 ```make ifneq ($(KERNELRELEASE),) obj-m:=chardevBase.o else KERNELDIR := ~/linux_dev_project/doc/luckfox-pico/sysdrv/source/kernel CURRENT_PATH := $(shell pwd) bulid: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean endif ``` - 还需要配置一下环境变量,CROSS_COMPILE变量要改成自己的文件夹路径 ```shell export ARCH=arm export CROSS_COMPILE=~/linux_dev_project/doc/luckfox-pico/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf- make ``` - 可以把上面的脚本封装成一个run.sh文件,该脚本增加更多的指令来获取编译过程信息,如下:
![run.sh](./run.sh文件.png)
值得注意的是touch创建的这个run.sh文件,没有执行的权限,需要通过指令```chmod +x run.sh``` 来增加执行权限。还有一个细节,我发现我使用sudo运行的脚本会无法编译通过,这里很奇怪,如果你电脑也有这个问题可以不增加sudo指令,直接```./run.sh``` 即可。 - 做完这些后,就可看到文件夹了多了个```chardevBase.ko```模块,使用```adb push ./chardevBase.ko ~```推送到luckfox设备里,用```adb shell```进入设备的shell,找到自己推送的那个模块,使用```insmod chardevBase.ko```加载模块,使用```dmesg```查看内核日志可以看到自己打印的信息。然后就可以使用```mknod path/dev_name 主设备号 次设备号```创建节点,```ls dev/```查看设备。 - 基本解决luckfox驱动编译加载问题后,正式开始gpio驱动编写 - 在驱动编写时,当然首先实现模块的加载以及退出。这样就会在内核加载驱动时会自动调用那两个init、exit函数。当然只实现这两个函数是不够的,字符驱动的编写主要是在于实现```write、read、open、release```这四个函数,而这是个函数定义在```linux/fs.h```文件下的```struct file_operations {} __randomize_layout;```结构体里。所以也可以说驱动编写是主要去实现这个结构体的函数。下面是一个字符设备的编写模板 ```c #include "linux/module.h" #include "linux/fs.h" /* 打开设备 */ static int chrtest_open(struct inode *inode, struct file *filp){ /* 用户实现具体功能 */ printk("chrtest_open module\n"); return 0; } /* 从设备读取 */ static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){ /* 用户实现具体功能 */ printk("chrtest_read module\n"); return 0; } /* 向设备写数据 */ static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){ /* 用户实现具体功能 */ printk("chrtest_write module\n"); return 0; } /* 关闭/释放设备 */ static int chrtest_release(struct inode *inode, struct file *filp){ /* 用户实现具体功能 */ printk("chrtest_release module\n"); return 0; } //定义了一个file_operations结构体变量test_fops,就是设备的操作函数集合 static struct file_operations test_fops = { .owner = THIS_MODULE, .open = chrtest_open, .read = chrtest_read, .write = chrtest_write, .release = chrtest_release, }; /* 驱动入口函数 */ static int __init chrdevbase_init(void) { /* 入口函数具体内容 */ int retvalue = 0; /* 注册字符设备驱动 */ retvalue = register_chrdev(200, "chrtest", &test_fops); if(retvalue < 0){ /* 字符设备注册失败,自行处理 */ } printk("init module\n"); return 0; } /* 驱动出口函数 */ static void __exit chrdevbase_exit(void) { /* 注销字符设备驱动 */ unregister_chrdev(200, "chrtest"); printk("exit module\n"); } /* 将上面两个函数指定为驱动的入口和出口函数 */ module_init(chrdevbase_init); module_exit(chrdevbase_exit); MODULE_LICENSE("GPL"); //添加模块 LICENSE 信息 MODULE_AUTHOR("Luckfox"); //添加模块作者信息 MODULE_VERSION("V1.0"); ``` - 这样,每次在编写字符设备驱动时跟着模板创建就简单很多。需要讲解的是其中的设备号,设备号分为主设备号以及次设备号,在内核目录```include/linux/kdev_t.h```路径下可以看到关于设备号的定义,如下 ```c++ #define MINORBITS 20//表示次设备号位数,一共是 20 位 #define MINORMASK ((1U << MINORBITS) - 1)//表示次设备号掩码 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))//用于从 dev_t 中获取主设备号,将dev_t右移20位即可 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))// 用于从 dev_t 中获取次设备号,取dev_t的低20位的值即可 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))//用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号 ``` - 注意设备号的申请分静态跟动态申请方法,一些主设备号被linux内核开发者分配掉了。动态设备号使用下面的方法申请。当然```register_chrdev```函数里设置的设备号写0也是系统自动分配。如果想指定主设备号那应该先在开发板上shell用```cat /proc/devices```查看当前使用了的设备号,然后再选一个没用到的设备号,值得注意:主设备号占用一个32位类型的高12位,所以主设备号可选:0-4095 ```c++ // dev:保存申请到的设备号 // baseminor:次设备号起始地址 // count:要申请的设备号数量 // name:设备名字 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);//设备号申请函数 ``` - 那么什么是主设备号跟次设备号?主设备号唯一对应加载进内核里的一个驱动模块,在创建设备节点(或者叫设备文件)时会要求输入设备名、主设备号以及设置次设备号,这样做就能够把设备文件跟驱动模块对应,上层应用使用文件操作时就能够访问到驱动模块。而一个驱动模块可以有多个设备节点对应着,所以为了区分会有不同的次设备号。也就是说主设备号标识不同种类的驱动模块,次设备号标识同一类型驱动模块的不同驱动节点。 - 如果已经编译了一个模块,那么如何在应用程序里跨层调度驱动程序?linux系统里一切皆文件,加载了驱动后,驱动的调用也是可以用打开关闭文件的方法来访问到底层驱动。下面的这段代码是我在网上找到的一个演示历程,可以用于学习,当然编译应用程序不需要内核参与,使用```arm-linux-gnueabihf-gcc you_filename.c -o you_filename```编译即可(这个指令只适合arm内核的芯片,x86用别的)。 ```c #include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" static char usrdata[] = {"usr data!"}; //argc:参数个数 //argv:各个下标存储以空格隔开的参数 int main(int argc, char *argv[]){ int fd, retvalue; char *filename; char readbuf[100], writebuf[100]; if(argc != 3){ printf("Error Usage!\r\n"); return -1; } filename = argv[1]; /* 打开驱动文件 */ fd = open(filename, O_RDWR); if(fd < 0){ printf("Can't open file %s\r\n", filename); return -1; } if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */ retvalue = read(fd, readbuf, 50); if(retvalue < 0){ printf("read file %s failed!\r\n", filename); }else{ /* 读取成功,打印出读取成功的数据 */ printf("read data:%s\r\n",readbuf); } } if(atoi(argv[2]) == 2){ /* 向设备驱动写数据 */ memcpy(writebuf, usrdata, sizeof(usrdata)); retvalue = write(fd, writebuf, 50); if(retvalue < 0){ printf("write file %s failed!\r\n", filename); } } /* 关闭设备 */ retvalue = close(fd); if(retvalue < 0){ printf("Can't close file %s\r\n", filename); return -1; } return 0; } ``` - 在linux里,编译驱动模块、编译应用程序、编译kernel是不同的办法,但他们之间有其联系。熟读kernel makefile能找到那种感觉,能自己做一个设计就可以说自己入门了。 - 在linux的驱动,还有设备树的概念。 - linux里的设备树就是为了应对arm众多的芯片但是使用的驱动代码大同小异而引出的一种驱动管理方法。在linux4.0内核之后开始支持。 - .dts是驱动源码、.dtsi是多个板型共同使用的头文件。dts源文件的头文件、.dtc是编译器一般是gcc、.dtb是编译得到的bin文件 - 我们一般可以在kernel/arch/arm/boot/dts目录下找到各个支持的板子的dts文件 - 设备树的语法很简单,而且也很少需要自己从0写设备树,只需要从原厂设备树里增加自己的外围设备就好 ```c 1、设备树结构: /{//根节点 #address-cells = <1>;//属性 #size-cells = <1>;//属性 compatible = "rockchip,rv1106";//属性 chosen {//特殊的一级子节点 ...//属性 }; aliasens {//特殊的一级子节点 gpio0 = &gpio0;//属性 ... }; cpus {//一级子节点 #address-cells = <1>;//属性 #size-cells = <0>;//属性 cpu0: cpu@0 {//lable:name@0212 device_type = "cpu";//属性 compatible = "arm,cortex-a7";//属性 reg = <0x0>;//属性 clocks = <&cru ARMCLK>;//属性 operating-points-v2 = <&cpu0_opp_table>;//属性 }; ...//更多的子节点 }; ...//更多的子节点 }; 2、我们可以看到在根节点‘/’下的是几个一级子节点,一级子节点还可以有二级子节点,不过一般不会嵌套太多。而节点可以用‘lableName:name@01212’的方式起一个别名,当然alisens节点里的也是别名不过驱动编写时一般不使用。设备树允许include设备树,所以在自己写的设备树可以用‘&lable{};’追加到整个设备树的子节点里。 3、设备树的语法就上面这些,其中有两个特殊的子节点‘chosen、alisens’。先说‘chosen’节点,这个节点除了节点里面写的属性值,uboot在启动时还会给这个节点添加一个bootargs的环境变量,从而传递到内核里面。 ``` - chosen的bootargs传递 ![chosen的bootargs传递](image-3.png) - 特殊的设备树属性 - linux设备树里有一些约定熟成或者必须存在的一些属性 ```c 1、compatible = "..."//兼容性,根节点的compatible属性指示内核是否可兼容,与内核内部的一个兼容性列表进行比对得出是否支持 2、model = "..."//模块名 3、status = "okay" = "disabled" = "fail" = "fail-sss" 4、#address-cells #size-cells 5、reg = //reg里的值关联第四点的两个属性值 6、ranges = ``` - OF函数 - of函数是内核编写非常重要的api,可以在“path = include/linux/of.h”中找到声明的一系列of函数名开头api - 下面是列出的常用of函数的api ```c of_find_node_by_name();//根据名称查找节点,注意不是lable名 of_find_node_by_path();//根据路径查找节点 of_find_property();//查找属性 of_property_count_elems_of_size();//查找获取的属性元素的长度,跟上面的函数配合使用 of_property_read_u32_index();//查找属性中指定元素的数值 of_property_read_u8_array();//获取属性的所有数值类型元素 of_property_read_u16_array(); of_property_read_u32_array(); of_property_read_u64_array(); of_find_node_by_name();//读取值是字符类型的属性 of_n_addr_cells();//获取#address-cells 属性值 of_n_size_cells();//获取#size-cells 属性值 of_device_is_compatible();//读取兼容性属性值用于检查兼用性 of_iomap();//将io地址从物理地址转换成虚拟地址,在无设备树前用的是ioremap(); ``` - 在编写驱动之前需要使用一个新的申请设备号方法来替换旧方法里需要手动写入设备号的方法 ```c // 没有指定设备号,申请设备号 // dev : 设备号 // baseminor : 次设备号 , 通常为0 // count : 设备申请数量 // name : 设备名 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) // 指定设备号,申请设备号 // from : 给定的设备号 // count : 申请设备数量 // name : 设备名称 int register_chrdev_region(dev_t from, unsigned count, const char *name) // 注销设备 void unregister_chrdev_region(dev_t from, unsigned count) // 字符设备结构体 struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; }; // 初始化字符设备 void cdev_init(struct cdev *, const struct file_operations *); // 添加字符设备 int cdev_add(struct cdev *, dev_t, unsigned); // 删除字符设备 void cdev_del(struct cdev *); ``` - 下面是我写的基于rv1103 soc芯片的一个gpio驱动,当然官方是提供了gpio驱动的,但是为了学习我会重新编写。下面的gpio驱动可以用于参考。 ```c ```