学了基本的字符设备框架之后可以对树莓派的底层写个驱动,简单的写个驱动IO的控制。
编写驱动可以根据上层的应用需求进行编写。
示例:实现上层逻辑代码向底层写入想要的字符,再编写底层驱动接收到字符后实现IO口的高低电平转换
一、查阅手册
想控制IO口的高低电平转换,需要查阅芯片手册,这里树莓派3使用的是 BCM2835
查阅芯片手册,我们要看的是IO口相关寄存器,按下面的目录去找
可以看到这里有一堆寄存器,和寄存器的功能描述,直接看需要的寄存器,这里我们要知道一点,每个寄存器很多组,你想操控的IO口是属于该寄存器的第几组,对应的地址是不一样的,后面映射地址需要知道。
1)先来看功能选择寄存器GPFSEL
下图的register 0 代表上面的IO口被分为第0组,其他IO口被分为其他组
以pin4引脚为例,我们要把引脚4设输出模式,则需要把此寄存器 14-12bit 位置设置为 001
2)输出1(高电平)寄存器GPSET
下图SETn n表示引脚号,箭头所指就是设为1即高电平
上图可看到引脚4依旧被分为第0组寄存器
3)输出0(低电平)清零寄存器GPCLR
下图箭头所指即是清零
上图可看到引脚4依旧被分为第0组寄存器
所以实际上只需要用到3个寄存器
二、上层代码
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <stdio.h>
int main()
{
int fd;
int cmd;
fd = open("/dev/pin4",O_RDWR);
if(fd < 0){
printf("open failed\n");
perror("reson");
}else{
printf("open success\n");
}
printf("input 0 / 1 0:set pin4 low 1:set pin4 high\n");
scanf("%d",&cmd);
if(cmd == 0){
printf("pin4 set 0\n");
}else if(cmd == 1){
printf("pin4 set 1\n");
}
fd = write(fd,&cmd,1);
return 0;
}
三、驱动代码
编写驱动之前注意一个问题,IO口的起始地址是0x3f000000,加上GPIO的偏移量0x20000000,所以GPIO的物理地址应该是从0x3f200000开始的,
然后在此基础上进行linux系统的MMU内存虚拟化管理,映射到虚拟地址上。
寄存器代码操作处理:
之前已经查阅了寄存器,知道引脚4在功能选择寄存器GPFSEL上14-12的位置上,并且我们要设置为输出模式,就是001
31 30 ...........14 13 12 11 10 .........2 1
0 0 1 0000000000操作原则是不能影响其他bit位置上原来的数值,我们只能影响14-12bit
先把14-13bit位置上变为数值00,我们需要把数值11左移12位,然后取反再与上原数值之后即可。
之后12位bit位置上变成数值1,我们需要把数值1左移12位,然后或上原数值即可。
代码操作就是 *GPFSEL0 &= ~(0x6 << 12); *GPFSEL0 |= (0x1 << 12);
下面代码是映射地址,我们知道引脚4属于这些寄存器的第0组,我们开头查看的第一列就是地址
void __iomem * __ioremap(resource_size_t res_cookie, size_t size);
resource_size_t 是要映射的起始的IO地址
size_t 是要映射的空间的大小,我们树莓派寄存器是32位即4个字节,所以size设为4
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
针对read,我们用copy_to_user ,把用户空间的数据读到内核空间的数据中
copy_to_user(void * to, const void * from,unsigned long n);
第一参数用户空间的数据地址,
第二参数是内核空间数据的地址,
第三参数是复制的字节数
返回值为实际copy的字节数
针对write,我们用copy_from_user ,把内核空间的数据写到用户空间里
copy_from_user(void * to, const void __user *from,unsigned long n);
第一个参数是内核空间数据的地址,kernel_buf
第二个参数是用户空间的数据地址,即__user
第三个参数是复制的字节数
返回值为真实读到的字节数量
#include <linux/fs.h> //file_operations声明
#include <linux/module.h> //module_init module_exit声明
#include <linux/init.h> //__init __exit 宏定义声明
#include <linux/device.h> //class devise声明
#include <linux/uaccess.h> //copy_from_user 的头文件
#include <linux/types.h> //设备号 dev_t 类型声明
#include <asm/io.h> //ioremap iounmap的头文件
static struct class *pin4_class;
static struct device *pin4_class_dev;
static dev_t devno; //设备号
static int major =231; //主设备号
static int minor =0; //次设备号
static char *module_name="pin4"; //模块名
volatile unsigned int* GPFSEL0 = NULL;
volatile unsigned int* GPSET0 = NULL;
volatile unsigned int* GPCLR0 = NULL;
//pin4_open函数
static int pin4_open(struct inode *inode,struct file *file)
{
printk("pin4_open\n"); //内核的打印函数,和printf类似
*GPFSEL0 &= ~(0x6 << 12);
*GPFSEL0 |= (0x1 << 12);
return 0;
}
//pin4_write函数
static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos)
{
int userCmd;//与上层写下来的数据类型对应,即int类型
printk("pin4_write\n");
copy_from_user(&userCmd,buf,count);//用户空间向内核空间传输数据
if(userCmd == 1){
printk("set 1\n");
*GPSET0 |= 0x1 << 4;
}else if(userCmd == 0){
printk("set 0\n");
*GPCLR0 |= 0x1 << 4;
}else{
printk("undo\n");
}
return 0;
}
static struct file_operations pin4_fops = {
.owner = THIS_MODULE,
.open = pin4_open,
.write = pin4_write,
};
int __init pin4_drv_init(void) //驱动的真正入口
{
int ret;
printk("insmod driver pin4 success\n");
devno = MKDEV(major,minor); //创建设备号
ret = register_chrdev(major, module_name,&pin4_fops); //注册驱动 告诉内核,把这个驱动加入到内核驱动的链表中
pin4_class=class_create(THIS_MODULE,"myfirstdemo"); //由代码在/dev下自动生成设备
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name); //创建设备文件
GPFSEL0 = (volatile unsigned int *)ioremap(0x3f200000,4);
GPSET0 = (volatile unsigned int *)ioremap(0x3f20001C,4);
GPCLR0 = (volatile unsigned int *)ioremap(0x3f200028,4);
return 0;
}
void __exit pin4_drv_exit(void)
{
iounmap(GPFSEL0);
iounmap(GPSET0);
iounmap(GPCLR0);
device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major, module_name); //卸载驱动
}
module_init(pin4_drv_init); //入口:内核加载驱动的时候,这个宏会被调用,而真正的驱动入口是它调用的函数
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPL v2");
四、交叉编译驱动代码和上层代码
1)进入到/home/jdr/SYSTEM/linux-rpi-4.14.y/drivers/char 目录,并把写好的驱动代码 pin4drive.c 放进来
2)编译之前需要进入Makefile (在哪个文件夹下面就修改哪个文件夹下的Makefile ),添加信息为了告诉编译器,要编译该驱动文件
vi Makefile
在文件中添加下面这句话,
obj-m +=pin4driver2.o
3)编译
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
上层代码pin4drivertest.c也要交叉编译,然后上传到树莓派,这里就不再赘述。
4)将.ko文件用scp上传到树莓派后
scp pin4drive.ko pi@192.168.1.6:/home/pi
5)装载设备驱动
sudo insmod pin4driver2.ko
其他命令:
命令 ls -l /dev/pin4 可以看到生成的设备的详细信息
命令 lsmod 查看挂载的驱动
命令 sudo rmmod pin4driver2 可以删除驱动
6)装载完成后,给设备文件加权限,之后开始运行上层程序
sudo chmod 666 /dev/pin4
./pin4text
命令 dmesg 可以查看内核打印信息