嵌入式Linux系列第20篇:驱动编写入门

简介: 嵌入式Linux系列第20篇:驱动编写入门

1.引言

   很早之前就有网友建议写一篇关于Linux驱动的文章。之所以拖到现在才写,原因之一是我之前没有在工作中遇到需要自己手动去写驱动的需求,主要是现在Linux内核驱动的支持已经比较完善了,另外一个原因是自己水平实在有限,不敢写驱动这个话题,Linux驱动里涉及到的东西太多了,很多年前专门买过驱动相关的书籍,厚厚的,看的云里雾里。借此机会,在这里给大家做个非常非常入门级的介绍,希望对大家有所帮助。                            

2.环境介绍

2.1.硬件

   网上的一个第三方做的NUC972开发板,这里会用到板子上的MPU6050传感器芯片,相关部分原理图如下:

2.2.软件

   1) Uboot不需要改动    2) Kernel不需要改动    3) Rootfs不需要重新编译

3.最简单的驱动例子

   第1步:编写hello.c

#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void) {
    printk(KERN_INFO  "module init success");
    return 0;
}
static void __exit hello_exit(void) {
    printk(KERN_INFO  "module exit success");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wuya");
MODULE_DESCRIPTION("driver example");

  这是一个简单的内核模块程序,可以动态加载和卸载。模块加载的时候系统会打印module init success,模块卸载的时候系统会打印module exit success。

   开头的两个头文件,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放到板子上,然后登录板子输入:    insmod hello.ko

   如果模块加载成功的话,可以查看模块加载情况,使用lsmod命令

   并且可以查看内核打印的消息,使用dmesg命令,

   rmmod hello.ko,用来卸载模块,使用dmesg命令可以看到相关输出信息

4.MPU6050驱动

   本章以板子上的MPU6050 传感器为例,来介绍驱动的编写。由于板子上使用的是PE10和PE11,它们不是真正的I2C引脚,所以这里我们使用GPIO来模拟I2C时序。编写驱动前,首先需要下载被控制器件的datasheet,在官网https://www.invensense.com/products/motion-tracking/6-axis/mpu-6050/ 可以下载。

   第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:%d\n",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 %d\n",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 %d\n", 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 Registered\n");
    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 up\n");
}
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文件放到板子,insmod mpu6050d.ko 。模块如果加载成功,在/dev目录下可以看到mpu6050的设备名出现。

第3步:写个应用程序mpu6050app.c,

#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
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.", -1);
        return -1;    
    }    
    nread = read(in, buffer, 12);
    close(in);    
    if (nread < 0) {
        printf("ERROR: %d, A read error has occurred", 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 ", x_accel);
    printf("y accel is: %d ", y_accel);
    printf("z accel is: %d ", z_accel);    
    printf("x gyro is: %d ", x_gyro);
    printf("y gyro is: %d ", y_gyro);
    printf("z gyro is: %d ", z_gyro);
    printf("temperature is: %d ", temp);
    exit(0);
}

 编译arm-linux-gcc mpu6050app.c -o mpu6050app    第4步:将板子水平摆放朝上,运行例子结果如下,

   我们来计算下z轴加速度和温度的实际数值。

   因为驱动里AFS_SEL寄存器设置的值是2,所以对应量程8g。数字-32767对应-8g,32767对应8g。把32767除以8,就可以得到4096,即1g对应的数值。把从加速度计读出的数字除以4096,就可以换算成加速度的数值。上面我们从加速度计z轴读到的数字是3723,那么对应的加速度数据是3723/4096≈0.91g。g为加速度的单位,重力加速度定义为1g, 等于9.8米每平方秒。由于桌上不是很平,加上传感器自身误差,所以这个值是合理的。

   再看看温度计算,从手册中可以看到如下的计算公式:

   上述的-2352计算后得到温度为29.6℃,注意这个温度不是环境温度,是芯片内部的温度,环境温度会比这个值略低。

   由于我是在北京,冬天屋里有暖气,所以这个值也是合理的。

5.结束语

   本期给大家介绍关于Linux驱动最简单的使用,可以看到驱动开发和应用开发还是有很大的差异,驱动需要关注底层,需要深入的阅读芯片的数据手册,同时也得具备内核的相关知识。市场上Linux应用开发人员相对更多,真正懂驱动的人相对较少,大部分集中在芯片原厂公司。推荐大家在实际做产品时尽量选择官方推荐的元器件,或者选择可以提供Linux驱动的元器件,以降低开发难度。

6.参考资料

    https://blog.csdn.net/u010632165/article/details/86541941

   https://blog.csdn.net/u010632165/article/month/2019/05

   https://www.cnblogs.com/wulei0630/p/9498195.html

   https://www.cnblogs.com/amanlikethis/p/4914510.html

   https://segmentfault.com/a/1190000020905713

   https://blog.csdn.net/m0_37777700/article/details/84305584

   https://www.cnblogs.com/sawyer22/p/9652294.html  

相关文章
|
2天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
35 13
|
1月前
|
Unix Linux Shell
linux入门!
本文档介绍了Linux系统入门的基础知识,包括操作系统概述、CentOS系统的安装与远程连接、文件操作、目录结构、用户和用户组管理、权限管理、Shell基础、输入输出、压缩打包、文件传输、软件安装、文件查找、进程管理、定时任务和服务管理等内容。重点讲解了常见的命令和操作技巧,帮助初学者快速掌握Linux系统的基本使用方法。
72 3
|
3月前
|
机器学习/深度学习 安全 网络协议
Linux防火墙iptables命令管理入门
本文介绍了关于Linux防火墙iptables命令管理入门的教程,涵盖了iptables的基本概念、语法格式、常用参数、基础查询操作以及链和规则管理等内容。
240 73
|
2月前
|
机器学习/深度学习 Linux 编译器
Linux入门3——vim的简单使用
Linux入门3——vim的简单使用
60 1
|
2月前
|
Linux Shell Windows
Linux入门1——初识Linux指令
Linux入门1——初识Linux指令
34 0
Linux入门1——初识Linux指令
|
2月前
|
存储 数据可视化 Linux
Linux 基础入门
Linux 基础入门
|
2月前
|
Linux Go 数据安全/隐私保护
Linux入门2——初识Linux权限
Linux入门2——初识Linux权限
30 0
|
4月前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
54 6
|
4月前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
58 5
|
4月前
|
NoSQL Linux C语言
嵌入式GDB调试Linux C程序或交叉编译(开发板)
【8月更文挑战第24天】本文档介绍了如何在嵌入式环境下使用GDB调试Linux C程序及进行交叉编译。调试步骤包括:编译程序时加入`-g`选项以生成调试信息;启动GDB并加载程序;设置断点;运行程序至断点;单步执行代码;查看变量值;继续执行或退出GDB。对于交叉编译,需安装对应架构的交叉编译工具链,配置编译环境,使用工具链编译程序,并将程序传输到开发板进行调试。过程中可能遇到工具链不匹配等问题,需针对性解决。
128 3