这个开发板已经很久没有动了,这一次辞职后想来想去还是选择去做驱动吧。以前写的那些驱动代码早就不知道哪里去了,当然更不记得了。所以现在从头开始学习,也顺便记录下笔记;
首先看看LED的电路图:
不难看出,LED1==GPB5 LED2==GPB6 LED3==GPB7 LED4==GPB8
然后就去看看IO端口图:
要设置的非常简单,就是把GPBCON设置为输出,GPBDAT设置为0时,则灯亮;设置为1时,则灯灭;
其实上一篇中s3c2440系统自带的管脚宏和函数已经分析过那些引脚宏;
S3C2410_GPB(5):表示GPB5的虚拟地址;
s3c2410_gpio_setpin(S3C2410_GPB(5), 0):表示给GPB5数据位的虚拟地址上设置为0;其实就是GPBDAT中对应位设置为0;
s3c2410_gpio_cfgpin(S3C2410_GPB(5), S3C2410_GPIO_OUTPUT):表示给GPB5控制位的虚拟地址上设置为输出;其实就是GPBCON中对应位设置为输出状态;
下面涉及的结构体和函数都在 include/cdev.h中可以找到
字符设备结构体cdev:
struct cdev {
struct kobject kobj;
struct module *owner;// 模块属于谁
const struct file_operations *ops;// 对应的操作函数
struct list_head list;
dev_t dev;// 设备号
unsigned int count;
};
struct cdev *cdev_alloc(void);为字符设备结构体申请内存;
int cdev_add(struct cdev *, dev_t, unsigned);把设备号和字符设备结构体绑定起来,其实这里就相当于把我们自己实现的操作函数和设备号对接起来。上层应用打开的文件是根据设备号,也就是说上层操作的仅仅是和设备号有关系,而要上层应用去使用我们自己设计的函数,就必须把实现的函数和设备号绑定起来。这样他们之间就有关系了。(语言表达有限,自己可以体会下)
void cdev_del(struct cdev *);把字符设备卸载下来;
void cdev_put(struct cdev *p);对字符设备结构体内存的释放;(我程序中使用到这个却是说未定义,但在源代码cdev.h中却能找到这个函数,不知道为什么会这样)
还有些函数等使用到的时候再去分析;
驱动实现代码
#include
#include
#include
#include
#include
#include
#include
#define MAJOR_NUM 250 // 主设备号
#define MINOR_NUM 0 // 次设备号
#define DEV_NAME "yzh_led_test" //驱动名称,加载完后用命令 cat /proc/devices可以查看到这个
#define led_on 1
#define led_off 2
#define all_led_on 3
#define all_led_off 4
static unsigned long led_IO_port[]=
{
S3C2410_GPB(5),
S3C2410_GPB(6),
S3C2410_GPB(7),
S3C2410_GPB(8),
};
static struct cdev *cdevp = NULL; // 字符设备结构体cdev
static int devNum;
int myOpen(struct inode *inode, struct file *file)
{
printk(KERN_INFO"in myOpen() will open led!\n");
if (file->f_mode & FMODE_READ)
printk(KERN_INFO"file is readonly!\n");
else if (file->f_mode & FMODE_WRITE)
printk(KERN_WARNING"file is writeonly!\n");
else
printk(KERN_DEBUG"file is read and write!\n");
return 0;
}
static int led_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
int i;
if (0 > arg || 4 < arg)
return -EINVAL;
printk(KERN_INFO"cmd:%u, ledNum:%ld\n", cmd, arg);
switch(cmd){
case led_on:
s3c2410_gpio_setpin(led_IO_port[arg], 0);
break;
//case led_off:
case 5:
s3c2410_gpio_setpin(led_IO_port[arg], 1);
break;
case all_led_on:
for (i = 0; i < 4; i++)
s3c2410_gpio_setpin(led_IO_port[i], 0);
break;
case all_led_off:
for (i = 0; i < 4; i++)
s3c2410_gpio_setpin(led_IO_port[i], 1);
break;
default:
return -EINVAL;
}
return 0;
}
struct file_operations fops =
{
.owner = THIS_MODULE,
.open = myOpen,
.ioctl = led_ioctl,
};
static int __init myDrive_init(void)
{
int ret, i;
dev_t allocDevNum;
printk(KERN_INFO"led init!\n");
devNum = MKDEV(MAJOR_NUM, MINOR_NUM);//根据主设备号和次设备号得到设备号
ret = register_chrdev_region(devNum, 1, DEV_NAME);// 静态申请设备号,设备号不变
if (ret < 0){
printk(KERN_INFO"register_chrdev_region error!\n");
ret = alloc_chrdev_region(&allocDevNum, 0, 1, DEV_NAME);// 动态申请设备号,设备号是系统分配的
if (ret < 0){
printk(KERN_INFO"alloc_chrdev_region error!\n");
return -EINVAL;
}
devNum = allocDevNum;
}
printk(KERN_INFO"devNum:%d, major:%d, minor:%d\n", devNum, MAJOR(devNum), MINOR(devNum));
cdevp = cdev_alloc();
cdev_init(cdevp, &fops);
cdevp->owner = THIS_MODULE;
ret = cdev_add(cdevp, devNum, 1);
if (ret){
printk(KERN_NOTICE"Error %d adding cdev", ret);
return -EINVAL;
}
for (i = 0; i < 4; i++){// 初始化LED GPBCON和熄灭所有灯
s3c2410_gpio_cfgpin(led_IO_port[i], S3C2410_GPIO_OUTPUT);
s3c2410_gpio_setpin(led_IO_port[i], 1);
}
return 0;
}
static void __exit myDrive_exit(void)
{
printk(KERN_INFO"myDrive exit!\n");
cdev_del(cdevp);
unregister_chrdev_region(devNum, 1);
}
module_init(myDrive_init);
module_exit(myDrive_exit);
MODULE_LICENSE("Dual BSD/GPL");
上面的代码都通过测试的,我用ioctl(fd, 2, n)总是报错误地址,所以在代码中我用 5 来替换 led_off(其实这是2)。其他的就没问题,当然这是这基本的一个代码,至于流水灯,跑马灯啊,有兴趣就自己去实现,这里就给个点灯的代码思路;
编译驱动有两种方法,可以参考上一篇blog:驱动的两种编译方法;我两种方法都实验过,我们可以先用动态加载,然后测试,最后确定好了,再把该驱动放到内核中;
首先把 .ko文件用nfs共享给开发板,开发板中insmod xx.ko,把驱动加载进内核,用命令 cat /proc/devices可以查看到加载的驱动,用lsmod也可以看到。当然我们要的是主设备号。
接下来就在 /dev/下创建一个设备文件,用来对应你的驱动。 mknod /dev/myLed c 250 0;根据主设备号在/dev下创建个驱动对应的设备文件。我们测试的时候就是操作这个文件的;
下面看看测试代码:
#include
#include
#include
#include
int main(int argc, char* argv[])
{
int flag, ledNum, ret;
int fd = open("/dev/charDevice", O_RDWR);
if (fd < 0){
printf("open fild!\n");
printf("errno:%d\n", errno);
return -1;
}
sscanf(argv[1], "%d", &flag);
sscanf(argv[2], "%d", &ledNum);
printf("fd:%d, argv[1]--flag:%d, argv[2]--ledNum:%d\n\n", fd, flag, ledNum);
ret = ioctl(fd, flag, ledNum);
if (ret < 0){
printf("error in ioctl, errno:%d\n", errno);
}
return 0;
}
可能有些多余的语句,那是我开始用来调试驱动的。这里有个errno比较好用,当出错时,可以查看下errno的错误码,方便调试;
struct class { ...
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev); };
不过我查了下资料,有一篇blog提到找到了这个结构体: http://blog.csdn.net/bingqingsuimeng/article/details/7929283;我不知道是不是我内核版本问题。
myClass = class_create(THIS_MODULE, "yzh_led_test");// 定义了个本地全局变量,static struct class *myClass = NULL;
cdevp = cdev_alloc();
cdev_init(cdevp, &fops);
cdevp->owner = THIS_MODULE;
ret = cdev_add(cdevp, devNum, 2); // 记住这里是创建两个设备,个人理解:这个count是该驱动将管理的设备个数
if (ret){
printk(KERN_NOTICE"Error %d adding cdev", ret);
return -EINVAL;
}
for (i = 0; i < 2; i++){// 创建两个设备文件,通过次设备号来区分
device_create(myClass, NULL, MKDEV(MAJOR(devNum), i), NULL, "yzhLedTest%d", i);
}
销毁部分(注意顺序):
cdev_del(cdevp);
device_destroy(myClass, MKDEV(MAJOR(devNum), 0));// 要销毁两个设备文件,根据次设备号不同来释放
device_destroy(myClass, MKDEV(MAJOR(devNum), 1));
class_destroy(myClass); // 销毁设备类
unregister_chrdev_region(devNum, 1);// 销毁设备号,因为上面设备文件中关联了设备号,所以要最后释放设备号
这时候当驱动加载完后在/dev目录下就会自动创建两个文件:yzhLedTest0 和 yzhLedTest1;两个文件都关联到了驱动;
当然也可以用write()函数来实现点灯操作,或者其他方法,总之都是大同小异。