驱动开发
说明
问题:自己在学习linux的实话,发现很多时候,都是看别人的博客,没有阅读过代码,理解其中的机制,更没有进行实践过,或者通过代码验证过。
作用:希望通过这个文档,建立一套模板,可以快速的实践,学到的东西,从而加深理解。
一、驱动模板
1.1 简单驱动
hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int hello_init(void)
{
printk(KERN_INFO "Hello - init\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_INFO "Hello - exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("WEI_TEST");
MODULE_DESCRIPTION("Hello World Module");
Makefile
PWD := $(shell pwd)
ANDROID_DIR := /home/user/code/xxxx/LINUX/android
KERNEL_OUT := $(ANDROID_DIR)/out/target/product/xxx/obj/KERNEL_OBJ
MODULE_SIGN_FILE := $(KERNEL_OUT)/scripts/sign-file
MODSECKEY := $(KERNEL_OUT)/certs/signing_key.pem
MODPUBKEY := $(KERNEL_OUT)/certs/signing_key.x509
obj-m := hello.o
.PHONY : hello clean
hello:
make -C $(KERNEL_OUT) ARCH=arm64 CROSS_COMPILE=$(ANDROID_DIR)/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-androidkernel- REAL_CC=$(ANDROID_DIR)/vendor/qcom/proprietary/llvm-arm-toolchain-ship/10.0/bin/clang CLANG_TRIPLE=aarch64-linux-gnu- M=$(PWD) modules
cp hello.ko hello.ko.unsigned
$(MODULE_SIGN_FILE) sha512 $(MODSECKEY) $(MODPUBKEY) hello.ko
clean:
@rm -f *.o *.ko *.order *.symvers *.unsigned *.mod.c
安装与卸载:
安装:
insmod hello.ko
[ 1367.237628] Hello - init
[ 1367.237695] Loaded hello: module init layout addresses range: 0xffffffac34959000-0xffffffac3495afff
[ 1367.237709] hello: core layout addresses range: 0xffffffac3494d000-0xffffffac34950fff
查看:
lsmod
Module Size Used by
hello 16384 0
卸载:
rmmod hello
[ 1451.056954] Hello - exit
[ 1451.059449] Unloaded hello: module core layout address range: 0xffffffac3494d000-0xffffffac34950fff
modprobe可以根据依赖关系自动加载依赖的模块,在编译模块时在make … modules后加上modules_install可调用depmod生成模块依赖文件modules.dep
modprobe –d 模块目录 hello
modprobe –d 模块目录 –r hello
ko文件格式:ELF格式,包含代码段(.txt),数据段(.data),符号表(.symtab),模块信息(.modinfo)等内容
动态加载模块流程:
调用insmod命令后,读取ko文件到内存
调用init_module系统调用(syscall),传入模块的内存地址,模块长度,和参数地址。传入的地址都是用户态地址。
init_module根据内核版本的不同实现的细节也有差别,主要的流程如下:
检查运行权限( capabilities )
从用户空间读取模块到内核空间
检查是否是有效的ELF文件
获取模块的基本信息,如名称,section索引等
检查是否在模块黑名单,检查签名
为各section分配内存
将模块加入到模块列表
根据内核符号表设置模块使用的符号对应的逻辑地址,然后刷新指令缓存
从用户空间拷贝参数到内核
链接模块到sysfs
执行模块入口函数
说明:
如果是编译成ko,就按上面的步骤写makefile,如果在源码目录下编译,直接修改makefile就行。
添加模块源代码的object文件到Makefile的obj-m变量中 (obj-m += hello.o)
如果创建了Makefile,需要将目录添加到上级目录Makefile的obj-m变量中(obj-m += hello hello为目录名称)
1.2 字符驱动
上面的驱动,看起来没啥价值,只能说,可以看下驱动的安装和卸载,还有什么事模块。有个概念。
cdev_hello.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>
//#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/slab.h>
struct led_desc{
unsigned int dev_major; //主设备号
struct class* myclass;
struct device* mydev; //创建设备文件的类和设备
void *reg_virt_base; //表示进行虚拟映射后寄存器地址的基准值
}*led_dev; //声明全局的设备对象
#define GPJ0CON 0xE0200240 // GPJ0CON寄存器物理地址
#define GPJ0_SIZE 8
static int kernel_val = 555; //内核空间定义的一个值,可以看成是一段4字节的空间,模拟和用户空间进行数据交互
ssize_t led_dev_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
int ret;
printk("-----%s-----\n",__FUNCTION__);
ret = copy_to_user(buf,&kernel_val, count);
if(ret > 0)
{
printk("copy_to_user error.\n");
return -EFAULT;
}
return 0;
}
ssize_t led_dev_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
int ret;
int value;
printk("-----%s-----\n",__FUNCTION__);
ret = copy_from_user(&value,buf,count);
if(ret > 0)
{
printk("copy_from_user error.\n");
return -EFAULT;
}
if(value) //根据用户空间传过来的value实现LED的亮灭
{
writel(readl(led_dev->reg_virt_base + 4) & ~(1<<3),led_dev->reg_virt_base + 4); //led亮
}
else
{
writel(readl(led_dev->reg_virt_base + 4) | (1<<3),led_dev->reg_virt_base + 4); //led灭
}
return 0;
}
int led_dev_open (struct inode *inode, struct file *filp)
{
printk("-----%s-----\n",__FUNCTION__);
return 0;
}
int led_dev_close (struct inode *inode, struct file *filp)
{
printk("-----%s-----\n",__FUNCTION__);
return 0;
}
const struct file_operations my_fops = {
.open = led_dev_open,
.read = led_dev_read,
.write = led_dev_write,
.release = led_dev_close,
};
static int __init led_dev_init(void) //一般都是申请系统资源
{
int ret;
u32 value;
//0-实例化全局的设备对象----分配空间
led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
if(led_dev == NULL)
{
printk(KERN_ERR "malloc error.\n");
return -ENOMEM;
}
// 1-申请设备号
led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops); //动态分配主设备号,且分配成功返回主设备号
if(led_dev->dev_major < 0)
{
printk(KERN_ERR "register_chrdev error.\n");
ret = -ENODEV;
goto err_0;
}
// 2-创建设备结点
led_dev->myclass = class_create(THIS_MODULE, "dev_class");
if(IS_ERR(led_dev->myclass)) //IS_ERR判断指针是否出错
{
printk(KERN_ERR "class_create error.\n");
ret = PTR_ERR(led_dev->myclass); //PTR_ERR将指针的错误原因转换成错误码
goto err_1;
}
led_dev->mydev = device_create(led_dev->myclass, NULL, MKDEV(led_dev->dev_major,0), NULL, "led%d",0);
if(IS_ERR(led_dev->mydev))
{
printk(KERN_ERR "device_create error.\n");
ret = PTR_ERR(led_dev->mydev);
goto err_2;
}
// 3-硬件初始化
//对地址进行映射
led_dev->reg_virt_base = ioremap(GPJ0CON, GPJ0_SIZE);
if(led_dev->reg_virt_base == NULL)
{
printk(KERN_ERR "ioremap error.\n");
ret = -ENOMEM;
goto err_3;
}
//gpio的输出功能的配置
value = readl(led_dev->reg_virt_base);
value &= ~(0xf<<12); //先清零
value |= (0x1<<12);
writel(value,led_dev->reg_virt_base); //设置GPJ0的[15:12]为输出模式
return 0;
//错误处理
err_3:
device_destroy(led_dev->myclass, MKDEV(led_dev->dev_major,0));
err_2:
class_destroy(led_dev->myclass);
err_1:
unregister_chrdev(led_dev->dev_major,"led_dev_test");
err_0:
kfree(led_dev);
return 0;
}
static void __exit led_dev_exit(void)
{
//一般都是释放资源
iounmap(led_dev->reg_virt_base);
device_destroy(led_dev->myclass, MKDEV(led_dev->dev_major,0));
class_destroy(led_dev->myclass);
unregister_chrdev(led_dev->dev_major,"led_dev_test");
kfree(led_dev);
printk("--------%s-------\n",__FUNCTION__);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL"); // 描述模块的许可证
Makefile
obj-m:=cdev_hello.o
KERNELDIR:=/lib/modules/`uname -r`/build
PWD :=$(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *o *.mod.c *.order *.symvers *.ur-safe
测试结果:
电脑端:编译ok,安装也ok
嵌入式端:编译ok,安装出现下面的错误。
(这个错误原因是,在iomap的时候出错,因为每个板子的的io地址不一样,所以可能导致地址冲突,需要看手册确认io地址,这样才能操作硬件。)
[ 204.175630] ------------[ cut here ]------------
[ 204.175647] WARNING: CPU: 6 PID: 8273 at arch/arm64/mm/ioremap.c:58 __ioremap_caller+0xc4/0xcc
[ 204.175651] Modules linked in: hello(O+) wlan(O) cpe_lsm_dlkm(O) wcd937x_slave_dlkm(O) machine_dlkm(O) wcd937x_dlkm(O) wcd934x_dlkm(O) wcd9335_dlkm(O) mbhc_dlkm(O) wcd9xxx_dlkm(O) wcd_cpe_dlkm(O) tx_macro_dlkm(O) rx_macro_dlkm(O) va_macro_dlkm(O) wsa_macro_dlkm(O) swr_ctrl_dlkm(O) bolero_cdc_dlkm(O) wsa881x_dlkm(O) wcd_core_dlkm(O) stub_dlkm(O) wcd_spi_dlkm(O) hdmi_dlkm(O) swr_dlkm(O) pinctrl_wcd_dlkm(O) usf_dlkm(O) native_dlkm(O) platform_dlkm(O) q6_dlkm(O) adsp_loader_dlkm(O) apr_dlkm(O) snd_event_dlkm(O) q6_notifier_dlkm(O) q6_pdr_dlkm(O) wglink_dlkm(O) msm_11ad_proxy
[ 204.175722] CPU: 6 PID: 8273 Comm: insmod Tainted: G S O 4.14.190+ #1
[ 204.175725] Hardware name: Qualcomm Technologies, Inc. trinket pm6125 + pmi632 Lenovo PVT1 (DT)
[ 204.175730] task: 00000000c84407f8 task.stack: 0000000020fdc8d7
[ 204.175735] pc : __ioremap_caller+0xc4/0xcc
[ 204.175739] lr : __ioremap_caller+0x60/0xcc
[ 204.175742] sp : ffffff80240ebad0 pstate : 00400145
[ 204.175745] x29: ffffff80240ebad0 x28: ffffff80240ebe18
[ 204.175754] x27: 00000000014080c0 x26: 0000000000000002
[ 204.175763] x25: 0000000000000002 x24: 0000000000000240
[ 204.175771] x23: 00000000e0200240 x22: 0000000000001000
[ 204.175779] x21: ffffff9f151c41ac x20: 00000000e0200000
[ 204.175787] x19: 0068000000000f07 x18: 0000000000000006
[ 204.175796] x17: 0000000000aab4cc x16: ffffff8022eaa648
[ 204.175804] x15: d29ced2031b099b6 x14: 88584c0fe2ffffff
[ 204.175812] x13: 0000000080000000 x12: 00000000ffc00000
[ 204.175820] x11: 0000000000000018 x10: 00000000ffffffff
[ 204.175828] x9 : 0000000000000001 x8 : 0000000000000000
[ 204.175836] x7 : bbbbbbbbbbbbbbbb x6 : 0000000000000040
[ 204.175844] x5 : 0000000000000000 x4 : 0000000000000001
[ 204.175853] x3 : ffffff9f151c41ac x2 : 0068000000000f07
[ 204.175861] x1 : 00000000e0200000 x0 : 0000000000000000
[ 204.175869] \x0aPC: 0xffffff8022eaa714:
[ 204.175872] a714 aa1303e3 8b1502c1 aa1503e0 944736ff 34000120 aa1503e0 9406a915 aa1f03e0
[ 204.175900] a734 a9434ff4 a94257f6 a9415ff8 a8c47bfd d65f03c0 8b150300 17fffffa aa1f03e0
[ 204.175928] a754 d4210000 17fffff7 a9be7bfd f9000bf3 910003fd aa0003f3 d503201f 9274ce73
[ 204.175955] a774 aa1303e0 9406a145 34000060 aa1303e0 9406a8ff f9400bf3 a8c27bfd d65f03c0
[ 204.175982] \x0aLR: 0xffffff8022eaa6b0:
[ 204.175985] a6b0 aa0003f7 d503201f 92402ef8 8b180288 9274cef4 913ffd08 9274cd16 8b160288
[ 204.176012] a6d0 d1000508 d370fd09 f100013f fa400ac4 fa541100 54000263 d34cfee0 97fffddd
[ 204.176039] a6f0 35000300 aa1603e0 52800021 aa1503e2 9406a84b b4000180 f9400415 aa1403e2
[ 204.176066] a710 f9001814 aa1303e3 8b1502c1 aa1503e0 944736ff 34000120 aa1503e0 9406a915
[ 204.176094] \x0aSP: 0xffffff80240eba90:
[ 204.176096] ba90 22eaa754 ffffff80 00400145 00000000 e0200000 00000000 000e0200 00000000
[ 204.176124] bab0 ffffffff 0000007f 22eaa6f0 ffffff80 240ebad0 ffffff80 22eaa754 ffffff80
[ 204.176151] bad0 240ebb10 ffffff80 22eaa680 ffffff80 00000002 00000000 00000000 00000000
[ 204.176178] baf0 e0200240 00000000 00000008 00000000 00000f07 00680000 151c41ac ffffff9f
[ 204.176205]
[ 204.176209] Call trace:
[ 204.176214] __ioremap_caller+0xc4/0xcc
[ 204.176218] __ioremap+0x38/0x48
[ 204.176226] init_module+0x1ac/0x1000 [hello]
[ 204.176232] do_one_initcall+0xe0/0x1b8
[ 204.176237] do_init_module+0x60/0x1f8
[ 204.176241] load_module+0x2424/0x27e4
[ 204.176245] SyS_finit_module+0xc0/0x114
[ 204.176248] el0_svc_naked+0x34/0x38
[ 204.176252] ---[ end trace 16b224f349a8d83e ]---
[ 204.176256] ioremap error.
[ 204.177299] Loaded hello: module init layout addresses range: 0xffffff9f151c4000-0xffffff9f151c5fff
[ 204.177305] hello: core layout addresses range: 0xffffff9f151bf000-0xffffff9f151c2fff
二、应用模板
2.1 源代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void print_info()
{
printf("this file is %s,line is %d\n",__FILE__,__LINE__);
printf("this file build time is:%s %s\n",__DATE__,__TIME__);
}
int main(int argc, char const *argv[])
{
int fd;
int value;
printf("this is a test\n");
print_info();
fd = open("/dev/led0",O_RDWR);
//根据 device_create的最后一个参数确定设备结点的名字
if(fd < 0)
{
perror("open");
exit(-1);
}
read(fd,&value,4);
printf("---USER----:%d\n",value);
//应用程序去控制LED的亮灭
while(1)
{
value = 0;
write(fd,&value,4);
sleep(1);
value = 1;
write(fd,&value,4);
sleep(1);
}
close(fd);
return 0;
}
这里面我用了编译器相关的宏__FILE__ __LINE__ __DATE__ __TIME__,可以用于调试,非常方便。
2.2 交叉编译器的安装
这部分,主要看正点原子的教程,我只简单的写一下步骤。
Linaro Releases下载:gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar
sudo mkdir /usr/local/arm
sudo cp gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz /usr/local/arm/ -f
sudo tar -vxf gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz
sudo vi /etc/profile
export PATH=$PATH:/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin
sudo apt-get install lsb-core lib32stdc++6 (在使用交叉编译器之前还需要安装一下其它的库)
arm-linux-gnueabihf-gcc -v 有显示gcc的版本号,就代表交叉编译环境安装搭建成功。
2.3 测试结果
makefile
ARCH ?= x86
ifeq ($(ARCH),x86) #注意看!!!ifeq后边有空格,没有不对
cc=gcc
else
cc=arm-linux-gnueabihf-gcc
endif
TARGET=test
OBJS=test.o
$(TARGET):$(OBJS)
$(cc) $^ -o $@ --static
%.o:%.c
$(cc) -c $< -o $@
.PHONY:clean
clean:
rm $(TARGET) $(OBJS)
注意:这里,我在生成可执行文件的时候,加了参数--static,目前还不知道为什么,因为只有这样,编译出的可执行文件在嵌入式端能使用
2.3.1 电脑端
编译:gcc xxx.c
测试:ok
2.3.2 嵌入式端
编译:arm-linux-gnueabihf-gcc -o helloworld3 test3.c -static (暂且还不知道,这个staic 是什么意思,如果不加它,编译出来的程序无法运行)
测试:ok
待补充:
嵌入式端makefile的编写
嵌入式端如何编译库的呢?如何使用库的呢?
三、hal库的引入
3.1 引入概述
上面的应用程序,就可以操作操作到硬件了,似乎可以万事大吉了。那么如果出现以下情况呢?
如果驱动程序的节点有变化,那用户的程序是不是要跟着变化?
由于linux是开源的,对于数据的一些处理可能需要用到算法,如果写在驱动里也必须开源,但我这些算法不想让别人知道,那怎么办?
遇到这些情况,该怎么办?我们可以将上面的部分抽取出来,编译成库,用户只需要我提供给他的接口就行,所以就有了HAL 库的出现。
3.2 hal 库的几个点
用头文件暴露接口给用户。
内部使用的函数,需要加限制。
3.3 简单的例子
四、什么是分层
五、再讲驱动
5.1 字符设备驱动开发框架
要素:
必须有一个设备号,用于在众多设备驱动中进行区分。
用户必须知道设备驱动对应的设备节点。(设备文件)
对设备操作,就是对文件操作,用户空间的open、read等函数是和驱动中的open、read对应的。
5.2 IO 有哪些接口
驱动填充这些接口,用户程序操作这些接口,从而就操作了硬件。
5.3 用户程序和硬件的交互
5.3.1 用户空间和内核空间的数据交互
copy_to_user(void __user * to, const void * from, unsigned long n)
copy_from_user(void * to, const void __user * from, unsigned long n)
5.3.2 用户程序操作硬件
内核驱动中是通过虚拟地址转换操作寄存器:
void* ioremap(cookie, size); //映射函数
void iounmap(void __iomen*addr); //去映射函数
映射函数参数:
参数1:物理地址
参数2:长度
返回值:虚拟地址
去映射函数参数:映射成功的虚拟地址
头文件包含:#include
参考链接