我们之前的驱动程序已经把框架写好,只是打印一些语句,没有做其他的事,现在我们让他点亮led灯
驱动的硬件操作分为3步:
-
看原理图
-
看芯片手册(这里是S3C2440)
-
写硬件相关代码
linux驱动的硬件相关代码和单片机的有什么不同呢
单片机的是直接操作物理地址
linux的是操作虚拟地址(由ioremap函数映射,物理地址——>虚拟地址)
1.查看原理图
(1)通过查看原理图,我们知道要led的引脚为GPF4,5,6
2.查看芯片手册
搜索GPFCON,知道要操作GPFCON和GPFDAT寄存器,他们的物理地址如下:
点亮led需要配置GPFCON寄存器,输出模式
设置GPFDATE寄存器,来输出高低电平
我们把配置GPFCON放在open函数,把设置GPFDATE放在write函数
(1)配置GPF4,5,6为输出,操作GPFCON寄存器
(2)设置GPFDATE寄存器,来输出高低电平
3.设置GPFCON寄存器
(1)由芯片手册知道GPFCON寄存器的物理地址为0x56000050,linux操作是使用虚拟地址,所以我们要使用ioremap把物理地址映射为虚拟地址,在入口函数里面配置,每次打开就自动配置好
搜索ioremap的使用方法,参数为起始地址和长度(映射空间为从起始地址+长度这么一段空间,这里由于有4个寄存器,用16字节),由于32位的指针都是4字节,所以gpfdat= gpfcon + 1,完善:
在出口地址设置取消映射:
以后操作寄存器的时候就用指针gpfcon和gpfdat操作引脚
(2)操作
A &= ~B为位清零
A |= B 为位置1
先为清零在置1设置输出模式,由于是2位,用0x03(两个1)清零,01为输出,用0x01置1.
4.设置GPFDAT寄存器
在write函数里操作
我们要点亮关闭,需要在应用层输入参数on/off,在应用层的参数怎么传到内核层的驱动函数,用copy_from_user()函数
然后设置val是1或者0开灯关灯
5.编写测试程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
/*
*myled_test on
*myled_test off
*/
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/myled", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}
if (argc != 2)
{
printf("Usage :\n");
printf("%s <on|off>\n", argv[0]);
return 0;
}
if (strcmp(argv[1], "on") == 0)
{
val = 1;
}
else
{
val = 0;
}
write(fd, &val, 4);
return 0;
}
驱动代码为:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
static struct class *firstdrv_led_class;
static struct class_device *firstdrv_led_class_dev;
static int firstdrv_led_open(struct inode *inode, struct file *file)
{
/*配置GPF 4 5 6 为输出*/
*gpfcon &= ~((0x03)<<(4*2)|(0x03)<<(5*2)|(0x03)<<(6*2));
*gpfcon |= ((0x01)<<(4*2)|(0x01)<<(5*2)|(0x01)<<(6*2));
return 0;
}
static ssize_t firstdrv_led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
copy_from_user(&val,buf,count); //由应用层的write(fd, &val, 4)函数的val参数传到内核层,让我们可以使用应用层的val值
//把buf=&val(write(fd, &val, 4)函数中的&val)传给copy_from_user(&val,buf,count)中的val
//对应的内核层到应用层是用copy_to_user()
if(val == 1)
{
//点灯,输出低电平
*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
}
else
{
//灭灯,高电平
*gpfdat |=(1<<4)|(1<<5)|(1<<6);
}
return 0;
}
/* 这个结构是字符设备驱动程序的核心
* 当应用程序操作设备文件时所调用的open、read、write等函数,
* 最终会调用这个结构中指定的对应函数
*/
static struct file_operations firstdrv_led_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = firstdrv_led_open,
.write = firstdrv_led_write,
};
int major;
int firstdrv_led_init(void)
{
major =register_chrdev(0,"myled",&firstdrv_led_fops);//注册firstdrv,告诉内核,注冊设备时给设备号写0,则内核会自己主动分配一个主设备号返回。
firstdrv_led_class = class_create(THIS_MODULE,"firstdrv_led");
firstdrv_led_class_dev = class_device_create(firstdrv_led_class,NULL,MKDEV(major,0),NULL,"myled");
gpfcon = (volatile unsigned long *)ioremap(0x56000050,16);
gpfdat= gpfcon + 1;
return 0;
}
void firstdrv_led_exit(void)
{
unregister_chrdev(major,"firstdrv_led");//卸载
class_device_unregister(firstdrv_led_class_dev);
class_destroy(firstdrv_led_class);
iounmap(gpfcon);
}
module_init(firstdrv_led_init);
module_exit(firstdrv_led_exit);
MODULE_LICENSE("GPL");
6.测试
结果灯成功亮灭
7.总结
(1)写驱动框架(open,write等,file_operations结构体,注册,自动创建设备节点)
(2)硬件操作(看原理图,手册,ioremap映射)
8.拓展(次设备号的应用)
我们的实验结果是传进1,就点亮三盏灯,0就灭灯,那有没有方法单独点亮某盏灯,可以使用不同的val,但是太麻烦,这里我们使用次设备号对应不同的灯来操作,之前我们生成的是: