嵌入式Linux驱动开发(二)——字符设备驱动之控制LED

时间:2022-05-16 23:37:44

关于驱动程序框架,请参考:
嵌入式Linux驱动开发(一)——字符设备驱动框架入门

同时,在这篇文章里面留下了两个问题,现在先来解决其中的第一个问题,就是如何用驱动程序来操作需要操作的硬件。

关于开发板点亮LED的问题,在这里简单介绍一下,也可以参考之前我写的文章:
嵌入式Linux开发——裸板程序点亮开发板上的LED灯
首先为了操作硬件,看接线原理图是必不可少的一步,这几个LED的原理图相对来说比较简单,在这里说一下,如果需要看懂稍微复杂一点的原理图,可以参见:
Linux嵌入式开发入门(二)——看懂原理图

以下是LED的接线原理图:


嵌入式Linux驱动开发(二)——字符设备驱动之控制LED
LED的原理图

看到上面的是开发板上面的三盏LED,在图上的左边,他们统一接到了3.3V的一个高电平上,那么如果在LED右侧为地(低电平),那么从左到右就可以形成电压差,因此,也就可以形成电流来点亮LED。

那么在LED右侧,引出三条线,这三条线根据接线原理图可以看出来接入到了下图的芯片中。

嵌入式Linux驱动开发(二)——字符设备驱动之控制LED
LED引脚对应的2440芯片引脚

我们根据上面的两幅接线的原理图可以看出来,三盏LED都接在了这个芯片上,那么接下来就是需要我们来阅读一下这个芯片的手册了,看看如何来操作这些引脚。而我们需要的该引脚的状态应该是:输出低电平点亮LED,输出高电平熄灭LED
查看芯片手册:1. 对应引脚配置为输出引脚 2. 对应引脚输出低电平嵌入式Linux驱动开发(二)——字符设备驱动之控制LED
2440芯片手册对应的GPF引脚信息

根据芯片手册,找到对应引脚的部分,发现操作该引脚实际需要操作三个寄存器:GPFCONGPFDAT
GPFUP
其中配置输出引脚需要配置的是GPFCON寄存器,让寄存器中的数据的对应位保存01,即可设置为输出引脚;而操作GPFDAT寄存器中的对应位的数据为1/0,既可让该引脚输出高/低电平。
所以我们控制对应的LED,实际上就是来控制2440芯片中的GPFCON和GPFDAT两个寄存器中的值。
根据手册可以看到,这个两个寄存器的地址分别是0x5600 00500x5600 0054。我们是不是像单片机程序一样,直接来操作这个两个地址呢?
实际不是的,在驱动程序的开发过程中,并不直接操纵这两个地址,而是操作这两个地址的映射。具体操作稍后详见代码。

PS: 写一个关于点亮LED的驱动,需要遵循一下结果步骤:

  • 搭建驱动的框架
  • 完善硬件操作的部分
    • 看懂原理图,确定引脚
    • 看芯片手册来确定如何操作引脚
  • 写代码(和单片机程序的区别为,驱动程序操作的是ioremap映射的虚拟地址,而非物理地址)

我们现在已经知道了驱动框架如何编写,同时根据硬件的原理图和芯片手册知道了该如何操作我们希望操作的硬件。那么接下来就是编写代码了。

我们驱动等下来完成,先确定我们希望如何来操作硬件,先把测试程序写完,然后再来完善驱动程序。
假设使用驱动程序时,是通过测试程序 第一个led的控制参数 第二个led的控制参数 第三个led的控制参数,也就是./led 1 0 1代表的是第一盏灯和第三盏灯开,第二盏灯关的状态。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char** argv)
{
if(argc != 4) //因为要操作三个LED,如果传入的参数不是四个,程序打印帮助信息
{
printf("The count of arguments is wrong\nUsage: \n");
printf("%s LED1_state LED2_state LED3_state\n", argv[1]);
printf("\t%s 1 0 1\n means: turn on the LED1 and LED3; turn off the LED2", argv[1]);

return -1;
}

int fd = open("/dev/seventh_driver", O_RDWR); //打开驱动文件
if(fd < 0){
printf("open ERROR\n");
return -2;
}

printf("open successfully. the file description is %d\n", fd);


//参数处理
int val[3];
int i = 0;
for(;i < argc - 1; i++)
{
val[i] = strcmp(argv[i + 1], "0") == 0? 1: 0; //直接使用字符串比较,把对应参数转换为int类型

printf("LED %d : %d", i ,val[i]);
}
printf("\n");

//将处理好的参数写入到设备文件(调用驱动程序的write函数)
write(fd, val, 12);

return 0;
}

驱动程序完成了,因为我们只是为了演示功能,所以先定义好了测试函数,以方便确定如何使用,之后在来编写的驱动程序,和正常的开发流程不太相同,一般都是驱动程序已经写好,具体如何使用也已经确定了,这时候根据驱动程序来编写应用程序的代码。

在正式写代码之前,需要先说两个函数
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)

static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
这两个函数定义在%kernel%/include\asm-arm\uaccess.h
以上的函数的目的主要是为了方便内核空间(kernel space)和用户空间(user space)之间的数据交换。
比如在应用程序中,使用的是read和write,其中的第二个参数都会传递一个变量的地址,实际在底层的驱动程序,就是通过copy_to_user和copy_from_user两个函数来获取的。
当应用程序调用了read,在驱动程序中,使用copy_from_user,来从中获取应用程序所传递的数据。
同样当应用程序中调用了write,在驱动程序中,会使用copy_to_user来将驱动程序的数据写给应用程序。

那么接下来看看驱动程序的思路久很清晰了

  • 在入口函数中,初始化设备文件、映射硬件的物理地址等信息
  • 在open函数中,设置GPFCON寄存器,使得LED对应的引脚为输出引脚
  • 在write函数中,读取应用程序发送的数据,并根据数据操作GPFDAT寄存器的数据,让芯片对应引脚输出高电平或者低电平,从而来点亮或者熄灭LED
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm-arm/uaccess.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static int dev_id = 0;
static const char *dev_name = "seventh_driver";
static struct class* seventh_class;
static struct class_device* seventh_class_device;

//定义两个变量,用于存放GPFCON和GPFDAT两个寄存器的地址
static volatile unsigned int* gpfcon = NULL;
static volatile unsigned int* gpfdat = NULL;

static size_t seventh_write (struct file *file, const char __user *buff, size_t size, loff_t *ppos)
{
volatile unsigned int argv[3]; //如果传入的参数数量不对,那么直接返回即可
if(size != 12){
printk("arguments count is wrong.%d\n", size);
return 0;
}

copy_from_user(argv, buff, size);//从用户空间获取传入的数据

//操作GPFDAT对应寄存器的对应位
int i = 0;
for(; i < size / 4; i++){
if(argv[i]) //如果传入的是0,关闭信号,将寄存器对应位改为高电平,熄灭LED
*gpfdat |= (0x1 << (4 + i)); //操作的是4、5、6三个位,所以偏移量从4开始计算
else//打开LED,传入的参数为1
*gpfdat &= ~(0x1 << (4 + i));
}

return 0;
}

static int seventh_open (struct inode *inode, struct file *file)
{
//设置GPFCON寄存器之前,先将对应的位清零
*gpfcon &= ~((0x03 << (4*2)) | (0x03 << (5*2)) | (0x03 << (6*2))); //需要用两位来控制引脚的性质,因此位操作时需要*2
//设置GPFCON寄存器,对应位为01,将对应引脚配置为输出
*gpfcon |= ((0x01 << (4*2)) | (0x01 << (5*2)) | (0x01 << (6*2)));
return 0;
}

static struct file_operations seventh_fops =
{
.owner = THIS_MODULE,
.open = seventh_open,
.write = seventh_write,
};

//入口函数
static int __init seventh_init(void)
{
dev_id = register_chrdev(dev_id, dev_name, &seventh_fops);

seventh_class = class_create(THIS_MODULE, dev_name);
seventh_class_device = class_device_create(seventh_class, NULL, MKDEV(dev_id, 0), NULL, dev_name);

gpfcon = (volatile unsigned int*)ioremap(0x56000050, 8);//将gpfcon映射到0x56000050的物理地址上,需要映射的地址由0x56000050~0x56000057一共八个字节
//其中GPFCON和GPFDAT各占4个字节
gpfdat = gpfcon + 1;

printk("init\n");
return 0;
}

//出口函数
static void __exit seventh_exit(void)
{
unregister_chrdev(dev_id, dev_name);
class_device_unregister(seventh_class_device);
class_destroy(seventh_class);

iounmap(gpfcon); //解映射

printk("exit\n");
}

module_init(seventh_init);
module_exit(seventh_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ethan Lee <4128127@qq.com>");

Makefile

KERN_DIR=/code/LinuxDev/Lab/KernelOfLinux/linux-2.6.22.6    #内核目录

all:
make -C $(KERN_DIR) M=`pwd` modules #M=`pwd`表示,生成的目标放在pwd命令的目录下 # -C代表使用目录中的Makefile来进行编译

clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -f modules.order

obj-m += seventh.o #加载到module的编译链中,内核会编译生成出来ko文件,作为一个模块

完成了以上的 驱动程序和测试程序的编写,接下来就是测试一下了。
直接使用make命令来编译驱动程序,使用arm-linux-gcc test.c -o leds编译测试程序。

编译完成后,将文件拷贝到开发版,在开发版上insmod装在驱动程序,接下来直接使用leds程序来测试是否可以点亮和熄灭LED了。

嵌入式Linux驱动开发(二)——字符设备驱动之控制LED
运行结果
嵌入式Linux驱动开发(二)——字符设备驱动之控制LED
点亮的LED

现在已经完成了LED的操作,实现了驱动程序操作硬件的目的,但是,我们实际在操作这几盏LED的时候,用到的是位操作来控制的。之前提出的关于此设备号还没有用起来,那么接下来就定义次设备号的方式来实现LED的控制。


那么在开始写代码前,先来说说关于此设备号的问题。我们在使用一组设备的时候,时常会使用到主设备号和此设备号一起的方式来操作一组硬件中的每一个,对于设备较多的时候,如果依然采用上面那种控制寄存器,一位一位的控制实际海时很复杂的。那么,通过把不同的字设备设置对应的此设备号的方式来管理,会容易很多。实际上次设备号就是在设备文件,对应字设备号的一种方法,我们在class_device初始化的时候来指定,制定完成后,我们只需要获取子设备号即可。
总结一下思路,我们维护一个/dev/xxx的一个数组,他们的主设备号都是一样的,但是次设备号不同。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static volatile int dev_id = 0;
static const char* dev_name = "eighth_driver";
static const char* dev_name_minor = "eighth_driver_%d";
#define dev_count 4

static struct class* eighth_class;
static struct class_device* eighth_class_device[dev_count];//这块不再是一个指针了,而是指针数组了

static volatile unsigned int *gpfcon = NULL;
static volatile unsigned int *gpfdat = NULL;

//根据传入的次设备号和键值(1,0)来控制LED是否被点亮,其中,如果次设备号传入0,可以统一操作所有的LED
void led_controller(int minor, int key)
{
if(minor){
if(!key)
*gpfdat &= ~(0x1 << (3 + minor));
else
*gpfdat |= (0x1 << (3 + minor));
}
else
{
if(!key)
*gpfdat &= ~((0x1 << 4) | (0x1 << 5) | (0x1 << 6));
else
*gpfdat |= ((0x1 << 4) | (0x1 << 5) | (0x1 << 6));
}
}

//根据次设备号来设置对应的引脚寄存器为输出的状态,如果传入的次设备号为0,则设置所有的引脚为输出
void led_set_output(int minor)
{
if(minor)
{
*gpfcon &= ~(0x03 << ((minor + 3) * 2));
*gpfcon |= (0x01 << ((minor + 3) * 2));
}
else
{
*gpfcon &= ~((0x03 << (4*2)) | (0x03 << (5*2)) | (0x03 << (6*2)));
*gpfcon |= ((0x01 << (4*2)) | (0x01 << (5*2)) | (0x01 << (6*2)));
}
}

static int eighth_open (struct inode *inode, struct file *file)
{
int minor = MINOR(inode->i_rdev);//从inode中获取次设备号
led_set_output(minor);

printk("open minor = %d \n", minor);

return 0;
}

static size_t eighth_write (struct file *file, const char __user *buff, size_t size, loff_t *ppos)
{
//从file中获取次设备号
int minor = MINOR(file->f_dentry->d_inode->i_rdev);
int key = 0;
copy_from_user(&key, buff, size);

printk("\n\nwrite minor = %d\t key = %d\n\n", minor, key);
led_controller(minor, key);

return 0;
}

static struct file_operations eighth_fops =
{
.owner = THIS_MODULE,
.open = eighth_open,
.write = eighth_write,
};

static int __init eighth_init(void)
{
dev_id = register_chrdev(dev_id, dev_name, &eighth_fops);
eighth_class = class_create(THIS_MODULE, dev_name);

//创建设备文件时,创建四个设备文件,他们的主设备号都是major,次设备号为0、1、2、3
int i = 0;
for(;i < dev_count; i++)
{
eighth_class_device[i] = class_device_create(eighth_class, NULL, MKDEV(dev_id, i), NULL, dev_name_minor, i);
}

gpfcon = (volatile unsigned int*)ioremap(0x56000050, 8);
gpfdat = gpfcon + 1;

printk("init%x\n", *gpfcon);
return 0;
}

static void __exit eighth_exit()
{
unregister_chrdev(dev_id, dev_name);

//删除每个自动创建的设备文件
int i = 0;
for(;i < dev_count; i++)
{
class_device_unregister(eighth_class_device[i]);
}

class_destroy(eighth_class);

iounmap(gpfcon);
printk("exit\n");
}


module_init(eighth_init);
module_exit(eighth_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ethan Lee <4128127@qq.com>");

测试程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char** argv)
{
if(argc != 3)
{
printf("The count of arguments is wrong\nUsage:\n");
printf("\t%s minor state\n");
return -1;
}
char dev_name[21];
//将命令行中的参数结合设备名,拼接出字符串,如:/dev/eighth_driver_0
sprintf(dev_name, "/dev/eighth_driver_%s", argv[1]);
printf("%s\n", dev_name);
int fd = open(dev_name, O_RDWR);
if(fd < 0){
printf("open ERROR\n");
return -2;
}

printf("open successfully. the file description is %d\n", fd);

int val;
val = strcmp(argv[2], "1") == 0? 1: 0;
printf("val = %d\n", val);

write(fd, &val, 4);

return 0;
}
嵌入式Linux驱动开发(二)——字符设备驱动之控制LED
执行过程

可以看到,创建了一组设备文件他们的主设备号都是252

嵌入式Linux驱动开发(二)——字符设备驱动之控制LED
使用测试程序

如果在开发板上测试,可以看到,每次使用leds程序都会有相应灯作出反应。
以上就是通过两种方法来操作硬件了,也基本解决了之前文章所遗留的问题。