在上一篇笔记中,详细的讲解了字符设备驱动开发步骤,并且用一个虚拟的chrdevbase设备为例完成了第一个字符设备驱动的开发。本章就开始编写第一个真正的Linux字符设备驱动。在正点原子STM32MP157开发板上有一个LED灯,本章就学习一下如何编写Linux下的LED灯驱动。
Linux下LED灯驱动原理
Linux下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以LED灯驱动最终也是对STM32MP157 的IO口进行配置,与裸机实验不同的是,在Linux下编写驱动要符合Linux的驱动框架。开发板上的LED0连接到STM32MP157的PI0这个引脚上,因此重点就是编写Linux下STM32MP157引脚控制驱动。
地址映射
先简单了解一下MMU,MMU全称叫做Memory
Manage Unit,也就是内存管理单元。在老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。MMU 主要完成的功能如下:
- 完成虚拟空间到物理空间的映射。
- 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于32位的处理器来说,虚拟地址范围2^32=4GB,开发板上有1GB的DDR3,这1GB的内存就是物理内存,经过MMU可以将其映射到整个4GB的虚拟空间,如下图所示:
Linux内核启动的时候会初始化MMU,设置好内存映射,设置好以后CPU访问的都是虚拟地址。比如STM32MP157的PI0引脚的端口模式寄存器GPIOI_MODER物理地址为0x5000A000。开启了 MMU,并且设置了内存映射,因此不能直接向0x5000A000这个地址写入数据,必须得到0x5000A000这个物理地址在Linux系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap和iounmap。
ioremap函数
ioremap函数用于获取指定物理地址空间对应的虚拟地址空间,定义在arch/arm/include/asm/io.h文 件中。函数定义如下:
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
一共有两个参数,而函数内部实际是调用了arch_ioremap_caller,并且有一个返回值:
- res_cookie:要映射的物理起始地址。
- size:映射的内存空间大小。
- 返回值:__iomem类型指针,指向映射后虚拟空间首地址。
iounmap函数
卸载驱动的时候需要使用iounmap函数释放掉 ioremap函数所做的映射, iounmap函数原型如下:
void iounmap (volatile void __iomem *addr);
只有一个参数,就是要取消映射的虚拟地址空间首地址。
I/O内存访问函数
I/O是输入/输出。这里涉及到两个概念:I/O端口和I/O内存。当外部寄存器或内存映射到IO空间时,称为I/O端口。当外部寄存器或内存映射到内存空间时,称为I/O内存。对于ARM体系下只有I/O内存 (可以直接理解为内存)。使用ioremap函数将寄存器的物理地址映射到虚拟地址以后,就可以直接通过指针访问这些地址,但是Linux内核不建议
这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
读操作函数
readb、readw和readl这三个函数分别对应8bit、16bit和32bit读操作,参数addr就是要读取写内存地址,返回值就是读取到的数据。
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
写操作函数
writeb、writew和writel这三个函数分别对应8bit、16bit和32bit写操作,参数value是要写入的数值,addr是要写入的地址。
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
硬件原理图分析
正点原子的STM32MP157开发班上有一个LED0,如下图所示:
可以看出, LED0接到了PI0上,当PI0输出低电平LED0就会导通点亮,当PI0输出高电平LED0不会导通,因此LED0也就不会点亮。
实验程序编写
LED灯驱动
首先定义一些方便操作的宏,比如设备号、设备名字以及LED开/官状态宏;之后需要定义寄存器宏定义;定义映射后的寄存器地址指针,指针是__iomem*类型;
之后定义led_switch函数,通过readl和writel函数来操作LED的状态;之后定义led_unmap函数,其中就是iounmap来取消各个寄存器的映射;
设备的read和open以及release都直接return 0就可以了,没什么操作;write函数就需要把数据拷贝过来之后,接收到的消息来判断具体的操作,在其中调用led_switch改变LED状态;然后file_operations把这几个设备操作函数封装一下;
最后是init函数,在这其中需要通过配置寄存器的方式来配置LED:首先通过ioremap读取物理寄存器映射后虚拟地址,然后就是寄存器的方法配置,最后需要register_chrdev注册led这个字符设备;注册失败需要回收,通过led_unmap来搞定;
最后还有exit函数,就是直接led_unmap取消映射,然后注销unregister_chrdev;
最后module_init和module_exit以及添加license和author就可以了,最后在表示这个驱动是intree模块驱动。
编写测试APP
就是最基础的打开、关闭以及写操作。
先判断argc是否为3,然后把设备名字argv[1]传入自己的char* filename,之后int fd通过open接住打开的设备,fd<0说明没有打开就直接return -1;之后把读取的数据传入databuf[0],这也是自己定义的unsigned char数组,大小为1,通过atoi(argv[2])传进去;最后调用write写进int retvalue;关闭文件同样调用close传给retvalue。
运行测试
编译驱动模块和测试APP
驱动程序就直接写一个Makefile,把obj-m设置为led.o就好了,然后make就能得到“led.ko”驱动模块文件。
测试APP则通过如下命令编译生成:
arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp |
运行测试
把编译生成的两个文件拷贝到rootfs/lib/modules/5.4.31中,然后重启开发板,进入lib/modules/5.4.31然后加载驱动:
depmod //第一次加载驱动的时候需要运行此命令 modprobe led //加载驱动 |
成功后就创建“/dev/led”设备节点:
mknod /dev/led.c 200 0 |
然后通过ledAPP测试驱动:
./ledApp /dev/led 1 //打开 LED灯 ./ledApp /dev/led 0 //关闭 LED灯 |
如果成功,之后可卸载驱动:
rmmod led.ko |
总结
本篇主要就是通过Linux的地址映射,把物理寄存器地址映射到虚拟内存中,然后通过字符设备的操作方式,操作LED的亮灭。这里面主要还要学习一下寄存器配置的方式,翻翻手册记一下就好了。