1.序言
很早之前就有网友建议写一篇关于Linux驱动的文章。之所以拖到现今才写,缘由之一是我之前没有在工作中遇见须要自己自动去写驱动的需求,主要是现今Linux内核驱动的支持早已比较健全了,另外一个诱因是自己水平实在有限,不敢写驱动这个话题,Linux驱动里涉及到的东西太多了,好多年前专门买过驱动相关的书籍,厚厚的,看的云里雾里。以此机会,在这儿给你们做个十分特别入门级的介绍,希望对你们有所帮助。
2.环境介绍2.1.硬件
网上的一个第三方做的NUC972开发板,这儿会用到板子上的MPU6050传感芯片,相关部份原理图如下:
2.2.软件
1)Uboot不须要改动
2)Kernel不须要改动
3)Rootfs不须要重新编译
3.最简单的驱动事例
第1步:编撰hello.c
#include
#include
static int __init hello_init(void) {
printk(KERN_INFO "module init successn");
return 0;
}
static void __exit hello_exit(void) {
printk(KERN_INFO "module exit successn");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wuya");
MODULE_DESCRIPTION("driver example");
这是一个简单的内核模块程序,可以动态加载和卸载。模块加载的时侯系统会复印moduleinitsuccess嵌入式linux基础教程 第2版 pdf,模块卸载的时侯系统会复印moduleexitsuccess。
开头的两个头文件,init.h定义了驱动的初始化和退出相关的函数,module.h定义了内核模块相关的函数、变量及宏。之后module_init和module_exit是模组加载和卸载相关的两个函数,
第2步:编撰Makefile
obj-m := hello.o
PWD := $(shell pwd)
KDIR :=/home/topsemic/nuc972/kernel/NUC970_Linux_Kernel-master/
all:
$(MAKE) -C $(KDIR) M=$(PWD)
clean:
rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a
注意:KDIR取决于你自己Linux内核安装的位置,一定要设置正确,否则编译会报错。
第3步:编译
将hello.c和Makefile置于同一路径下进行编译,输入make即可。编译成功后,会在当前路径下生成hello.ko,这就是我们即将加载到内核的模块。
第4步:将生成的hello.ko放在板子上,之后登陆板子输入:
insmodhello.ko
假如模块加载成功的话,可以查看模块加载情况嵌入式linux基础教程 第2版 pdf,使用lsmod命令
而且可以查看内核复印的消息,使用dmesg命令,
rmmodhello.ko,拿来卸载模块,使用dmesg命令可以见到相关输出信息
4.MPU6050驱动
本章以板子上的MPU6050传感为例,来介绍驱动的编撰。因为板子上使用的是PE10和PE11,它们不是真正的I2C引脚,所以这儿我们使用GPIO来模拟I2C时序。编撰驱动前,首先须要下载被控制元件的datasheet,在官网可以下载。
第1步:写驱动文件,我们这儿在驱动文件里放了三个文件,分别为mpu6050.c、mpu6050bsp.c和mpu6050bsp.h
其中mpu6050.c代码如下:
#include"mpu6050bsp.h"
int MPU6050_MAJOR = 0;
int MPU6050_MINOR = 0;
int NUMBER_OF_DEVICES = 2;
struct class *my_class;
struct cdev cdev;
dev_t devno;
/*************************************************************************************/
#define DRIVER_NAME "mpu6050"
int mpu6050_open(struct inode *inode,struct file *filp)
{
u8 reg;
reg=InitMPU6050();
printk("mpu6050:%dn",reg);
return nonseekable_open(inode,filp);
}
long mpu6050_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
switch(cmd)
{
default:
return -2;
}
return 0;
}
int mpu6050_read(struct file *filp, char *buffer,size_t count, loff_t *ppos)
{
mpu_get_data();
return copy_to_user(buffer, mpu_data, 14);
}
int mpu6050_write(struct file *filp, char *buffer, size_t count, loff_t *ppos)
{
return 0;
}
struct file_operations mpu6050_fops = {
.owner = THIS_MODULE,
.read = mpu6050_read,
.write = mpu6050_write,
.open = mpu6050_open,
.unlocked_ioctl = mpu6050_ioctl,
};
/**************************************************************************************/
static int __init mpu6050_init(void)
{
int result;
devno = MKDEV(MPU6050_MAJOR, MPU6050_MINOR);
if (MPU6050_MAJOR)
result = register_chrdev_region(devno, 2, "mpu6050");
else
{
result = alloc_chrdev_region(&devno, 0, 2, "mpu6050");
MPU6050_MAJOR = MAJOR(devno);
}
printk("MAJOR IS %dn",MPU6050_MAJOR);
my_class = class_create(THIS_MODULE,"mpu6050_class"); //类名为
if(IS_ERR(my_class))
{
printk("Err: failed in creating class.n");
return -1;
}
device_create(my_class,NULL,devno,NULL,"mpu6050"); //设备名为mpu6050
if (result<0)
{
printk (KERN_WARNING "hello: can't get major number %dn", MPU6050_MAJOR);
return result;
}
cdev_init(&cdev, &mpu6050_fops);
cdev.owner = THIS_MODULE;
cdev_add(&cdev, devno, NUMBER_OF_DEVICES);
printk (KERN_INFO "mpu6050 driver Registeredn");
return 0;
}
static void __exit mpu6050_exit (void)
{
cdev_del (&cdev);
device_destroy(my_class, devno); //delete device node under /dev//必须先删除设备,再删除class类
class_destroy(my_class); //delete class created by us
unregister_chrdev_region (devno,NUMBER_OF_DEVICES);
printk (KERN_INFO "char driver cleaned upn");
}
module_init (mpu6050_init );
module_exit (mpu6050_exit );
MODULE_LICENSE ("GPL");
上述代码整体结构和第3章介绍的hello.c类似,不过为了支持对字符设备的操作,多了open/write/read的几个函数实现。
mpu6050bsp.c因为内容较多,不把代码贴到这儿了,你们一看就明白了,它就是用gpio来模拟i2c功能,实现寄存器操作功能。mpu6050bsp.h主要是相关寄存器定义。
第2步:编译,之后把ko文件放在板子,insmodmpu6050d.ko。模块假如加载成功,在/dev目录下可以看见mpu6050的设备名出现。
第3步:写个应用程序mpu6050app.c,
#include
#include
#include
#include
short x_accel, y_accel, z_accel;
short x_gyro, y_gyro, z_gyro;
short temp;
int main()
{
char buffer[128];
short *value;
int in, out;
int nread;
in = open("/dev/mpu6050", O_RDONLY);
if (!in) {
printf("ERROR: %d, Open /dev/mpu6050 failed.n", -1);
return -1;
}
nread = read(in, buffer, 12);
close(in);
if (nread < 0) {
printf("ERROR: %d, A read error has occurredn", nread);
return -1;
}
value = (short*)buffer;
x_accel = *(value);
y_accel = *(value + 1);
z_accel = *(value + 2);
temp = *(value + 3);
x_gyro = *(value + 4);
y_gyro = *(value + 5);
z_gyro = *(value + 6);
printf("x accel is: %d n", x_accel);
printf("y accel is: %d n", y_accel);
printf("z accel is: %d n", z_accel);
printf("x gyro is: %d n", x_gyro);
printf("y gyro is: %d n", y_gyro);
printf("z gyro is: %d n", z_gyro);
printf("temperature is: %d n", temp);
exit(0);
}
编译arm-linux-gccmpu6050app.c-ompu6050app
第4步:将板子水平摆放朝上,运行事例结果如下,
我们来估算下z轴加速度和湿度的实际数值。
由于驱动里AFS_SEL寄存器设置的值是2,所以对应阻值8g。数字-32767对应-8g,32767对应8g。把32767乘以8,就可以得到4096,即1g对应的数值。把从加速度计读出的数字乘以4096,就可以换算成加速度的数值。前面我们从加速度计z轴读到的数字是3723,这么对应的加速度数据是3723/4096≈0.91g。g为加速度的单位linux学习,重力加速度定义为1g,等于9.8米每平方秒。因为桌上不是很平,加上传感自身偏差,所以这个值是合理的。
再瞧瞧气温估算,从指南中可以见到如下的估算公式
上述的-2352估算后得到气温为29.6℃,注意这个气温不是环境湿度,是芯片内部的气温,环境湿度会比这个值略低。
因为我是在上海,冬天屋内有供暖,所以这个值也是合理的。
5.结束语
本期给你们介绍关于Linux驱动最简单的使用,可以看见驱动开发和应用开发还是有很大的差别,驱动须要关注底层,须要深入的阅读芯片的数据指南小型linux系统,同时也得具备内核的相关知识。市场上Linux应用开发人员相对更多,真正懂驱动的人相对较少,大部分集中在芯片原厂公司。推荐你们在实际做产品时尽量选择官方推荐的元元件,或则选择可以提供Linux驱动的元元件,以减少开发难度。