AST2500片内ADC驱动详解

AST2500片内ADC驱动详解

PS. 由于此dirver是linux内核程序遵守GPL许可证,所以在此公布一些细节不算侵权。
PS. 这篇博客假定读者没有驱动开发的基础,所以刚开始会对一些基础知识进行一些描述,后面的博客不会再对一些符号或者常见的东西再做说明。
PS. linux驱动公认的经典书籍有《linux设备驱动程序》,虽然是以linux2.6内核讲解的,但是从原理上和用法上来阐述已然足够。书本的知识是必然落后于现实技术的发展的,学会举一反三才是真本事。。。本文虽然是在AST2500平台上开发的驱动基础上阐述的,大致原理类似。

此文代码可以如下链接获得:
https://github.com/WangKaiwh/linux_CPP_C_codes/tree/master/linux_driver_ast2500/ADC

1.片内ADC概述

overview:

这里写图片描述

以上是官方datasheet的overview。意思是片内ADC模块有16个ADC的通道,每个通道都有高低的阈值,超过这个阈值会触发中断。芯片还有第二套边界值用于滞后的判断(这句话,不好理解,先简单认为是防错处理吧。如有错误,方便告知在下~)。

features:

AST2500片内ADC驱动详解_第1张图片
有几个重点内容:
1. 每个ADC的通道精度都是10-bit。精度的概念类似于有2^10个刻度,最小的测量粒度:Vf / (2^10)。
2. ADC的采样时钟可以配置。
3. 每个通道的高低阈值可以配置。

2. ADC driver的文件组成

ADC驱动是一个很简单的驱动。涉及到的文件仅仅只有四个,如下:
adc_drv.c
adc_define.h
ioaccess.h
Makefile

简单描述一下每个文件的作用:
adc_drv.c是此驱动的主体,里面实现了一个字符设备驱动,给应用程序提供了ioctl的接口访问底层硬件。
adc_define.h定义了芯片相关的寄存器的宏。
ioaccess.h定义了ioctl的命令。
此驱动可以简单的看成提供了一套ioctl接口给应用程序调用,原理很简单。关于ioctl的细节不在此讨论,如果有不熟悉的同学可以查看《UNIX环境高级编程》。

3. adc_drv.c详解

3.1 license,author, description声明

MODULE_LICENSE ("GPL");
MODULE_AUTHOR("Insyde Software Corp.");
MODULE_DESCRIPTION("ADC driver");  

这几句代码“望文生义”,不需要额外的解释。各位童鞋也没有必要对此疑虑,以后自己写driver的时候,可以把这几句当模板来用,把里面一些声明改下。

3.2 模块变量以及入口

除了一些基础级的驱动,大家习惯于直接编译到内核外,很多驱动是以模块(.ko)的形式存在的。这些驱动模块在应用需要用的时候才会由应用程序(包括脚本)insmod(linux命令加载内核ko的作用)到内核。
驱动模块的最大的好处是,可以加载性,在平台模块很多的时候,有选择的加载某些模块可以极大的减少启动时间,减少资源利用率,以及可以方便内核开发人员调试代码。频繁的烧写uImage估计是每个驱动开发者最烦扰的问题了吧。。。此ADC驱动就是以ko形式存在的。具体怎么编译为ko,后面讲Makefile的时候会介绍。
模块变量(module param):

// 一个用于做调试开关的全局变量
int gAdcDebugFlag = 0;
module_param_named(debug, gAdcDebugFlag, int, S_IRUGO | S_IWUSR);

“模块变量”,这个词您听起来也许会觉得奇怪。一个模块的变量难道是模块随便定义的一个变量吗?当然不是,这里特指一个模块会用到全局变量,且可以通过insmod加载的时候,给它赋值的变量。
总结一下一个“模块变量”具有一下特征:
1,全局变量。
2,可以insmod加载ko时候带参数给它赋值。
3,由module_param_named或者module_param来声明成”模块变量”。

module_param_named语句,第一个参数debug是此模块变量的在内核中的标识,gAdcDebugFlag声明的模块变量名字,类型是int,访问权限是S_IRUGO和S_IWUSR(“用户” “组” “其他” 都可以读,只有”用户”可以写)。至于权限,大家仍然可以通过任何一本介绍linux的书籍中查到。
模块入口(module_init):

module_init (adc_mod_init);

module_init是linux的一个系统api,用于定义一个模块的入口。当此模块被加载到内核的时候,调用adc_mod_init函数。
模块出口(module_exit):

module_exit (adc_mod_cleanup);

module_init是linux的一个系统api,用于定义一个模块的出口。当此模块被卸载(rmmod)的时候,调用adc_mod_cleanup函数。

3.3 模块初始化函数adc_mod_init

int adc_mod_init (void) {
    int result;
    DKLOG_ALERT ("ADC Module Insert, Build Time %s\n", __TIME__);

    result = register_chrdev (ADC_MAJOR, ADC_MODULE_NAME, &ioaccess_fops);
    if (result < 0) {
        DKLOG_ERR("Can't get major number.\n");
        return (result);
    }
    if (ioaccess_major == 0) {
        ioaccess_major = result;
        DKLOG(KERN_ALERT, "Auto assigned ioaccess_major = %d \n", ioaccess_major);
    }
    adc_init ();
    DKLOG_ALERT("ADC Module Insert done\n");
    return (0);
}

DKLOG_ALERT是一个调试宏,如果gAdcDebugFlag调试开关打开,则调用printk函数输出msg到控制台串口。
printk的用法与printf类似,除了自带有KERN_EMERG等日志级别定义外,和printf用法一致。不在此过多解释。

register_chrdev (ADC_MAJOR, ADC_MODULE_NAME, &ioaccess_fops),这个函数调用是此驱动的重点。register_chrdev注册一个字符设备,参数1,ADC_MAJOR是一个宏,静态定义了字符设备的主设备号。静态定义一个主设备号,这点非常不好,而且register_chrdev这个接口也是比较老的内核api。当这个参数设置为0的时候,内核可以自动分配主设备号。这里不做过多的发散,只需要知道主设备号是linux里面用于区分不同驱动的一个关键数字,其类型是unsigned char,最大支持255。
参数2,ADC_MODULE_NAME定义了模块的名字。
参数3,是一个struct file_operations结构体,它是字符设备中操作底层硬件IO的方法的集合。
完整定义如下:

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
    int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

常用的回调函数有:
open,应用层调用open函数打开一个任意字符设备时候,都会尝试调用底层的struct file_operations::open函数。但是如果驱动不现实open函数,则不调用。
llseek,设置设备读写时候的位置标记。
read/write,读写字符设备。
poll,应用层调用select或者epoll等函数时候,会调用到底层的poll。select和epoll是linux应用层在大并发轮询io时候常用的函数调用,强烈建议看《UNIX环境高级编程》,来了解和掌握。
unlocked_ioctl,应用层调用ioctl时候,会调用到它,此函数是一个多功能箱,比read/write能更丰富的控制底层硬件。
此驱动仅仅只实现了unlocked_ioctl,对于此驱动的业务需求来讲,足矣!

3.4 unlocked_ioctl实现

static int ioaccess_ioctl (struct file *filp,
                            unsigned int cmd,
                            unsigned long arg) {
  int ret = 0;
  long readdata = 0;

  IO_ACCESS_DATA Kernel_IO_Data;

  memset (&Kernel_IO_Data, 0, sizeof(IO_ACCESS_DATA));

  Kernel_IO_Data = *(IO_ACCESS_DATA *) arg;

  switch (cmd) {
  case IOCTL_IO_READ:
    Kernel_IO_Data.Data = *(unsigned int *) (IO_ADDRESS (Kernel_IO_Data.Address));
    DKLOG_INFO("IOCTL_IO_READ (value = %ld)\n", Kernel_IO_Data.Data);
    *(IO_ACCESS_DATA *) arg = Kernel_IO_Data;
    ret = 0;
    break;

  case IOCTL_IO_WRITE:
    DKLOG_INFO("IOCTL_IO_WRITE (value = %ld)\n", Kernel_IO_Data.Data);
    *(unsigned int *) (IO_ADDRESS (Kernel_IO_Data.Address)) = Kernel_IO_Data.Data;
    ret = 0;
    break;

  case IOCTL_SET_ADC_CLOCK:
    ret = set_adc_clock ((unsigned char) Kernel_IO_Data.Address, (unsigned char) Kernel_IO_Data.Data);
    break;

  case IOCTL_ADC_MEASURE:
    readdata = adc_measure ((unsigned char) Kernel_IO_Data.Address);
    readdata = readdata + CV_Data;
    if( (readdata < 0) || (readdata == CV_Data) ) {
        Kernel_IO_Data.Data = 0;
    } else {
        Kernel_IO_Data.Data = (unsigned long)readdata;
    }
    *(IO_ACCESS_DATA *) arg = Kernel_IO_Data;
    ret = (int) Kernel_IO_Data.Data;
    break;

  case IOCTL_ENABLE_ADC:
    ret = enable_adc_channel ((unsigned char) Kernel_IO_Data.Address);
    break;

  default:
    ret = 3;
  }
  return (ret);
}

以上实现了几个ioctl的命令,其中比较重要的有如下:

IOCTL_SET_ADC_CLOCK

AST2500片内ADC驱动详解_第2张图片
设置ADC的时钟,设置ADC的时钟是通过写寄存器ADC0C: ADC Clock Control,设置时钟分频因子。如上图所示。

IOCTL_ADC_MEASURE

AST2500片内ADC驱动详解_第3张图片
ADC测量,其实现原理是读取寄存器ADC_DATA_REGISTER。如上图所示。

IOCTL_ENABLE_ADC

AST2500片内ADC驱动详解_第4张图片
开启ADC,根据用户传入的值,打开相应的ADC通道。其实现原理是设置ADC00: Engine Control寄存器相应位。

4.驱动卸载adc_mod_cleanup

void adc_mod_cleanup (void) {
    unregister_chrdev (ADC_MAJOR, ADC_MODULE_NAME);
    DKLOG_ALERT ("ADC Module cleanup\n");
    return;
}

unregister_chrdev与register_chrdev相反,清除内核为此字符设备驱动分配的资源。

5.驱动的测试与测试驱动开发(TDD)

在软件工程的思想里,有很多方法论,其中在大型软件开发的过程中,有了敏捷开发等等,其中测试驱动开发(TDD),显得尤为重要。在嵌入式领域里,TDD有着天然的困难,如硬件依赖,驱动加载慢,编译慢等的,“不好测试”的问题十分常见。我将在后续篇幅,用TDD的思路重构这个ADC驱动。
用测试驱动开发的思路重构ADC LINUX驱动(一)

你可能感兴趣的:(linux内核)