1.引言
很早之前就有网友建议写一篇关于Linux驱动的文章。之所以拖到现在才写,原因之一是我之前没有在工作中遇到需要自己手动去写驱动的需求,主要是现在Linux内核驱动的支持已经比较完善了,另外一2 w ` a q Z个原因是自己水平实在有限,不敢写驱动这个话题,Linux驱动里7 X $ w 5 ~ ^ w U涉及到的东西太多了,很多8 F h 7 w i ; 3年前专门买过驱动相关的书籍,厚厚的,看的云里雾里T f K m 0 N。借此机会,在这: t q v M里给大家做个非常非常入门级的介绍,希望对t g L (大家有所帮助。
2.环境介绍
2.1.硬O l j [件
网上的一个第三方做的NUC972开发板,这里会用到板子上的MPU6050传感器芯片,相关部分原理图如下:
2.2.软件
1) Uboo4 8 @ f ] ? & 4 7t不需要改动
2) Kernel不需要改动
31 l N W f n q E) Rootfs不需要重新编译
3.最简单F = Z | .的驱动例子
第1步:编写hello.c
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(3 K [ G w 7void) {
printk(KERN_INFO \"module init success\\n\");
return 0;
}
static void __exit hello_exit(void) {
printk(KERN_INFO [ L g o -\"module exit success\\n\");
}
module_init( 9 i Lhello_init);
module_exit(hello_exit);
MODULE_LICENSE(\"GPL\");
MODULE_AUTHOR(\"wuya\");
MOz y ^ b = - BDULE_DESCRIPTION(\"driver example\");
这是一个简单的内核模块程序,可以动态加载和卸载。模块加载的时候系统会打印module init success,模块卸载的时候系统会打印module exit success。
开头的两个头文件,init.h 定义了驱动的初始化和退出相关的函数,X p B pmodule.h 定义了内核模块相关的函数、变量及宏。然后module_init和m G r U 3 bodule_# # ( A X o W exit是模组加载和卸载相关的D G Z 5 X 两个函数,
第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即可。编译成) w A功后,会在当前路径下生成hello.ko,这就是我们将要加载到内核的模块。
第4步:将生成的hello.ko放到板子上,然后登录板子输入:
insmy ] 5 N r : k Mod hello.ko
如果模块加载成功的话,可以查看模块加载情况,使用lsmod命令U A 0 N ^
并且可以查看内核打印的消息,使用dmesg命令,
rmmod hello.ko,用来卸载模块,使用dme% p U j u 3 A Csg命令可以看到相关输出信息
4.MPU6050驱动
本章以板子上的MPUM u _ ^ K k u $ *6050 传感器为例,来介绍驱动的编写。由于板子上使用的是PE10和P^ P ] rE11,它们不是真正的I2C引脚,所以这里我们使用GPIO来模拟I2C时序。编写驱动前,首先需要下载被控制器件的datasheet,在官网 可以下载。
第1步:写驱动文件,我们这里在驱动文件里放了三个文件,分别为mpu6050.c、mpu6050bsp.c和mpu6050bsp.h
其中mpu6050.c代码如下:
#include\"mpu6050bsp.h\"
int MPU6050_MAJOR =S = z 0;
int MPU6050_MINOR = 0;
int NUMBER_OF_DEVICES = 2;
struct class *my_class;
struct cdev cdeG ? r v;
dev_t devno;
/************U 1 3 2 l W M H***************************************5 _ M ) Y*********************x m ^ I 0 5 R , m**********o ! k # ? A :***/
#define DRIVER_NAM3 + a o ^ G 2 x =E \"mpu6050\"
int mpu6050_open(strg 6 B 6 e T 0 wuct inode *inode,struct f` g J - , Uile *filp)
{
u8 reg;
reg=InitMPU6050();s a ? 1 s e / ]
printk(\"mpO L ~ + s G 5 , nu6050:%d\\n\",reg);
return nonseekable_open(inode,filp);
}
long mpu6050_ioctl(S ^ ) 5 t . W Y &struct file *filp,unsigned int cmd,unsigneu t wd long arg)
{
switch(cm9 ! , 4 ad)
{
defau~ 5 A E #lt:
return -2;
}
return 0;
}
int mpu6050_read(struct fy Y M Y h c , G yile *filp, char *buffer,size_t count, loff_t *ppos)
{
mph ~ ` F ~ su_get_datax a 0 I 8();
return copy_to_u{ U eser(buffer, mpu_data, 14);
}
int mpu6050_write(struct f& J N % B cile *filp, char *buffer, size_t count, loff_t *ppos)
{
return 0;
}
struct file_operatE { iions mpu6050_fops = {
.ownq b b D i f Ker = THIS_MODULE,
.read = mpu6050_read,
.write = mpu6050_wa j Yrite,
.open = mpu6050_open,
.unlocked_ioctl = mpu6050_ioctl,
};
/**************************************************************************n P ~ 0************} { n . |/
static int __in. ? r 6 w B # 9it mpu6050_init(void)
{
int result;
devno = MKDEV(MPU6050_MAJOR, MPU6050__ R A AMINOR);
if (MPU6050_MAJOR)
result = register_chrdev_regionx # ) g R 8 C 0(devno, 2, \"mpu605i M Z g c o p ` X0\");
else
{
resp z z W ) $ult = alloc_chrdev_r) 7 wegion(&dT ! 8 j j _ m n /evno, 0, 2, \"mpu6050\");
MPU6050_MAJOR = MAJOR(devno);
}
printk(\"MAJOR IS %d\\n\",MPU6050_MAJOR);
my_class = class_create(THIS_MODULE,\"mpu6050_class\"); //类名为
if } 5 { ( c h T(IS_ERR(U B H 4 _ ] Cmy_class))
{
pz 2 U N t % F ,rintk(\"Err: failedp - 7 ! { C ? % ! in creating class.& J { 5\n\");
return -1;
}
device_c, b L ~ B b Z kreate(myN t @ t B h ; # ^_class,NULL,devno,L n + PNULL,\"mpu6050\"); //设k D f备名为mpu6050
if (resulk & Bt&l( l * d Tt;0)
{
pri| + h R # }ntk (KERN_d a r HWAo ^ u ] y } rRNm H i j 6 - gING \"hello: can\'t get major number %d\\n\", MPU6050_MAJOR);
return resultD a q Y;
}
cdev_init(&cdev, &mpuP Z D 1 0 R E6050_fops);
cdev.owner = THIS_MODULE;
cdev_add(&cde| _ o 0 i A ;v, devno, NUMBER_OF_DEVICES);
printk (KERN_INFO \"mpu6050 driver Registered\\n\");] n / 4 e
return 0;
}
static void __exit mpu6050_exit (void)
{
cdev_del (&amJ K W b y } Fp;cdev);
device_destroy(my_class, devno); //delet* m Ce device node under /dev//必须先删除设P 7 q备,再删p * # c L U除class类
class_destroy(my_class); //delete clao * 5 ? f j gss created` 0 f P R V 1 7 8 by0 a % U us
unregister_chrdev_region (devno,NUMBER_OF_DEVICES);
printk (KERN_INFO \"char driver cleaned up\\n\");
}
module_init (mpu6050_init );
module_exit= c } x j Y x (mpu6050_exit );
MODULE_LICENSE (\"GPL\");
上述代码o b f V F整体结构T ; / S 2 O和第~ | h K | K _ x3章介绍的he$ ] 6 5 9 1llo.c类似,不过为了支持对字符设备的操作,多了open/write/read的^ r s & o 3几个函数实现。
mp^ % = R E #u6050bsp.c由于内容较多,不= l A m 6 p 2 v把代码贴到这& p v 2 6里了,大家一看就明白了,它就是用gpio来模拟i+ d G #2c功能,实现寄存器操作功能。mpu6050bsp.h主要是相关寄存器定义。
第2步:编译,然后把ko文件放到板子,inJ w 5 5 I E O ,smod mpu6050d.ko 。模块如果加载成功,在/dev目录下可以看到mpu605s ~ E . 90的设备名出现。
第3步:写个应用程序mpu6050app.c,
#include <unistd.h>^ Y o 2 0 D - $ t
#include <std/ e H T i l M O blib.h>
#include <sys/stat.h>
#include <fcntl.h>
shQ i | w jort x_accel, y_accel, z_accel;
short x_z u b +gyro, y_gyro, z_gyro;
short temp;
int main()
{
cz 1 Z / r g } Ehar buffer[128];
short *value;
int in, out;
int nread;
in = opn l * = G v J Oen(\"/dev/mpu6050\", O_RDONLY);
if (!in) {
printf(\"ERROR: %d, Open /dev/m% S spu6050 failed.\\n\", -1);
retut 8 $ x k N a + crn -1;
}
nread = read(in, buffer, 12);
close(in);
if (nread < 0) {
printf(\"ERROR: %d, A read error has occurred\\n\", 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(C $ 3 G\"y gyro is: %d \\n\", y_gyro);
prm } L fintf(\"z gyro is: %d \\n\", z_gyro);
printf(\"temperature is: %d \\n\", temp);
exit(0);
}
编译arm-linux-gcc mpu6050app.c -o mpu6050app
第4步:将板子水平摆放朝上,运行例子结果如下,
我们来计算下z轴加速度和温度的实际数值。
因为驱动里AFS_SEL寄存器设置的值是2,所以G ! N L E对应量程8| _ xg。数字-32767对应-8g,32767对应8g。把32767除以8,就可以得到4096,即1g对应的数值。把从加速度计读出_ 1 : ( ; #的数字除以4096,就可以换算成加速度的数值。上面我们从加速度计z轴读到的数字是3723,那么对应的加速度数据是3723/4096≈0.91g。? + ; [ Q t [g为加速度的单位,重力加速度定义为1g, 等于9.8米每平方秒。由于桌上不是很平,加上传感器+ A p ] y自身误差,所以这个值是合理的。
再看看温度计算,从手册中可以看到如下的计算公式
上述的h Q N R w-2352计算后得到温度为29.6℃,注1 ! E l ^ u意这个温/ w { E f `度不是环境温度,是芯片内部的温度,环境温度会x 7 ) B v 6 ; s比这个值略低。
由于我是在北京,冬天屋里有暖气,所以这个值也是合理的。
5] ? S c P.结束语
本期给大家介绍关于Linux驱动最简单的使用,可以看到驱动开发和应用开发还是有很大的差异y z o O # (,驱动需要关注底层,需要深入的阅读芯片的数据+ J Q C手册,同时也得具备内核的相关知识。市场上Linux应用开发人员相对更多,真正懂驱动的人相对较少,大部L H J s * 7 . S分集中在芯片原厂公司。推荐大家在q F 8 p m实际做产品时尽量选择官方推荐的元器件,或者选择可以提供Linux( T H R L : ^ d驱动的元器件,以降低开发难度。