基于OMAPL138的Linux字符驱动GPIO驱动AD9833一之miscdevice和ioctl

37次阅读

共计 6911 个字符,预计需要花费 18 分钟才能阅读完成。

0. 导语

在嵌入式的道路上寻寻觅觅很久,进入嵌入式这个行业也有几年的时间了,从 2011 年后半年开始,我清楚的记得当时拿着 C51 的板子闪烁了 LED 灯,从那时候开始,就进入到了嵌入式的大门里面。嵌入式的学习从来没有停止过,中间也有无数的插曲和机缘巧合学会 C ++ 和 Java,做一些好玩的应用。无论是嵌入式 DSP 也好,还是如今的嵌入式 ARM,7 年之久从来没有停止过。技术最大的好处就是,无论发展到什么境地,那种第一次点亮 LED 灯欣喜永远的可以伴随着你,只要你解决了一个卡了你很久的问题,这就是技术的魅力。我也将开始大肆的从嵌入式 DSP 转入到嵌入式 Linux,在研究生阶段,完成这个转型。

这个 Demo 意义重大,使用 Linux 也有四五年的时间了,Linux 良好的基础和嵌入式基础让我在嵌入式 inux 道路上算的上是顺风顺水。这个 Demo 将过去 STM32,F28xx 的 DSP 或者那些单片机桥接起来,将过去裸机上的程序全部编到内核里面,通过嵌入式的应用进行互联。

本 DEMO 依然使用 AD9833 作为例子,将用 linux 内核级的 gpio 对 AD9833 写时序,完成对于 AD9833 的驱动程序,在嵌入式 Linux 上生成 /dev/ 目录节点,使用 Linux 命令行对 AD9833 产生波形进行控制。(只要有了 /dev 节点,使用 Qt,C++,Python 都可以控制了,这就是物联网最注重的。)

效果视频观看地址:https://v.youku.com/v_show/id…

1. 开发驱动综述

本开发驱动基于 Linux3.3 内核版本 ,且内核必须编译正确,否则不能运行。
这个 Demo 可以归结为三个部分,一个部分为 Linux 字符驱动模板,第二部分为 AD9833 驱动程序,第三部分为通信协议。还附加一个配置文件。

  • Linux 字符驱动模板主要包含 init exit 还有 ioctl,函数;
  • AD9833 驱动程序为 AD9833 的 GPIO 时序(AD9833 为 SPI 协议,这里先用 GPIO 模拟时序,后续升级为 SPI 外设);
  • 通信协议格式方式,用户对于 AD9833 的控制字,比如发送波形命令,频率命令等;
  • 将自己编写的驱动写入内核的代码树,编译成模块或者编译进内核随内核启动;

本 Demo 就围绕这三点进行。

2. Linux 字符驱动模板

* 函数 ioctl

主要负责进行数据交互的。当设备生成字符设备驱动节点(/dev 目录下),使用 shell 级命令 cat 或者编译一段 C 应用程序用 open 打开节点的时候,后面将参数就是通过 ioctl 函数进行传递。(在嵌入式 Linux 端定义一个 ioctl 的函数,在 C 语言的程序也有一个 ioctl 用来和其进行对应,这样就完成了数据参数传递。)

* 结构体 file_operations

static int
ad9833_ioctl(struct file  *file, unsigned int cmd, unsigned long arg)
{printk(DRV_NAME "\tRecv cmd: %u\n", cmd);
    printk(DRV_NAME "\tRecv arg: %lu\n", arg);
    switch(cmd) {
    case CMD_TYPE_SIN:
        ad9833->set_wave_freq(ad9833, 1500);
        ad9833->set_wave_type(ad9833, SIN);
        printk(DRV_NAME "set wave is sine wave! arg = %lu\n" , arg);

        break;

    case CMD_TYPE_TRI:
        ad9833->set_wave_freq(ad9833, 1500);
        ad9833->set_wave_type(ad9833, TRI);
        printk(DRV_NAME "set wave is tri wave! arg = %lu\n" , arg);
        break;

    case CMD_TYPE_SQE:
        ad9833->set_wave_freq(ad9833, 1500);
        ad9833->set_wave_type(ad9833, SQU);
        printk(DRV_NAME "set wave is sw wave! arg = %lu\n" , arg);
        break;

    }
    return    0;
}

ioctl 函数不能独立的存在需要 file_operations 指针进行操作,ioctl 为一个执行命令的清单,file_operations 就是这个清单的执行者。下面就是 file_operations 的指针,里面的成员需要接收到 ad9833_ioctl 的函数地址,在内部运行的时候会调用该地址。

static struct file_operations ad9833_fops = {

        .owner                =    THIS_MODULE,
        .unlocked_ioctl      =      ad9833_ioctl,
};

* 结构体 miscdevice

*miscdevice 结构体为字符驱动的一级,字符驱动如同文献 [3] 所说的一样,非常的凌乱,到底里面使用了 miscdevice 还是 cdev 还是 platform-device or platform-driver,这里暂时不进行理,这里使用 miscdevice 级的字符驱动设备向 Linux 内核进行设备的注册,后续有文章进行区分,类似的文献还有我的《Linux GPIO 键盘驱动开发记录_OMAPL138》,这里使用的室 platform-device 进行。

static struct miscdevice ad9833_miscdev  = {
        // DRV_NAME 在前面进行 define
        // #define    DRV_NAME     "AD9833-ADI"
        .name                =    DRV_NAME,
        .fops                =    &ad9833_fops,
};

可以看见,在 miscdev 里面指定了 file 指针的地址,miscdev 主要的作用就是 向内核注册该驱动

* 函数 init

内核级的嵌入式 Linux 驱动给出的硬性要求进行 init 函数,并标识 init 函数为__init,而且还要在 module_init 中填写 init 函数的地址。

static int __init ad9833_dev_init(void)
{
    int  i,ret;

    /*
     * AD9833 new device
     * */
    printk(DRV_NAME "\tApply memory for AD9833.\n");
    ad9833 = ad9833_dev_new();

    /*
     * AD9833 init gpios.
     * */
    printk(DRV_NAME "\tInititial GPIO\n");

    for (i = 0; i < 3; i ++) {ret    =    gpio_request( ad9833_gpios[i], "AD9833 GPIO" );
        if(ret) {printk("\t%s: request gpio %d for AD9833 failed, ret = %d\n", DRV_NAME,ad9833_gpios[i],ret);
            return ret;
        }else {printk("\t%s: request gpio %d for AD9833 set succussful, ret = %d\n", DRV_NAME,ad9833_gpios[i],ret);
        }
        gpio_direction_output(ad9833_gpios[i],1 );
        gpio_set_value(ad9833_gpios[i],0 );
    }

    ret = misc_register(&ad9833_miscdev);
    printk(DRV_NAME "\tinitialized\n");
    return ret;
}

module_init(ad9833_dev_init);

当我们运行 insmod xxxx.ko 的时候,此时运行的就是这个 init 函数,在这个函数中主要完成对于设备内存的请求和一些初始状态的注册。在本 DEMO 中对对于 AD9833 的结构体进行了注册,并对 gpio 进行申请。ret = misc_register(&ad9833_miscdev); 重点室这句话。

* 函数 exit

除此之外内核也要求了 exit 函数,主要进行对 init 中内存申请的释放。

static void __exit ad9833_dev_exit(void)
{
    int i;
    for(i = 0; i < 3; i++) {gpio_free( ad9833_gpios[i] );
    }
    misc_deregister(&ad9833_miscdev);

}
module_exit(ad9833_dev_exit);

这是一个非常简单的字符驱动的模板,然后就需要我们添加 AD9833 的驱动了。

3. AD9833 芯片级时序驱动

到此,基本上就是裸机嵌入式的知识了,对于芯片功能的描述,对于芯片时序的把握。作为本博客不在赘述,给出函数的列表,如果喜欢,本文将 DEMO 的源码放在后面,自行下载观看。

static void ad9833_set_wave_type(AD9833 *dev, enum ad9833_wavetype_t wave_type);
static void ad9833_set_phase(AD9833 *dev, unsigned int phase_value);
static void ad9833_set_freq(AD9833 *dev, float freq);
static void ad9833_set_para(AD9833 *dev, unsigned long freqs_value, unsigned int phase_value, enum ad9833_wavetype_t wave_type);
static void ad9833_init_device(AD9833 *dev) ;
static void ad9833_write_reg(AD9833 *dev, unsigned int reg_value);
static int     ad9833_ioctl(struct file  *file, unsigned int cmd, unsigned long arg);
AD9833 *ad9833;

AD9833 *ad9833_dev_new()
{AD9833 *dev = (AD9833*)kcalloc(1, sizeof(AD9833), GFP_ATOMIC);

    dev->hw.fsy              =      AD9833_FSY_IO;
    dev->hw.sdi              =   AD9833_DAT_IO;
    dev->hw.clk              =      AD9833_CLK_IO;

    dev->set_wave_para    =   &ad9833_set_para;
    dev->init_device      =   &ad9833_init_device;
    dev->write_reg        =   &ad9833_write_reg;
    dev->set_wave_freq    =   &ad9833_set_freq;
    dev->set_wave_phase      =   &ad9833_set_phase;
    dev->set_wave_type    =   &ad9833_set_wave_type;
    dev->init_device(dev);


    return dev;
}

该设备使用链表进行描述。

3. 与驱动通信的 ioctl 函数

在参考文献 [1] 中,给出了 Linux 字符设备驱动开发重要的 ioctl 函数解析,写的很接地气,很朴实,也写的很明白,包括利用 ioctl 函数应用程序和驱动程序进行交互,ioctl 函数使用 MAGIC_number 幻数对命令进行转换。
在 ioctrl 函数里面通常使用 switch 和 case 进行执行,见上衣章的内容。
这里给出使用 ioctl 的应用程序,它和内核驱动进行通信:


#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define                AD9833_MAGIC                'k'
#define                CMD_TYPE_SIN                _IO(AD9833_MAGIC, 0)
#define                CMD_TYPE_TRI                _IO(AD9833_MAGIC, 1)
#define                CMD_TYPE_SQE                _IO(AD9833_MAGIC, 2)


const char dev_path[]="/dev/AD9833-ADI";

int main(int argc , char *argv[])
{

    int fd = -1, i = 0;
    printf("ad9833 test program run....\n");


    fd = open(dev_path, O_RDWR|O_NDELAY);  // 打开设备
    if (fd < 0) {printf("Can't open /dev/AD9833-ADI\n");
        return -1;
    }

    printf("open device.\n");

    if(strcmp(argv[1],"1") == 0 ) {ioctl(fd, CMD_TYPE_SIN, 5);
        printf("argc = %d,sine wave = %s \n", CMD_TYPE_SIN, argv[1]);
    }else if(strcmp(argv[1],"2") == 0 ) {ioctl(fd, CMD_TYPE_TRI, 1);
        printf("argc = %d,tri wave = %s \n", CMD_TYPE_TRI,argv[1]);
    }else{ioctl(fd, CMD_TYPE_SQE, 1);
        printf("argc = %d,sqe wave = %s \n", CMD_TYPE_SQE, argv[1]);
    }
    
    printf("argc = %d\n", argc);
    close(fd);
    return 0;
}

在 ioctl 函数和嵌入式 Linux 驱动里面的 ioctl 函数就会对应,命令就传递过去了。

另外补充一个知识:

void main(int argc char *argv[] )

  • argc 为传递参数的个数
  • argv[1] 为一个字符串,第一个传递进来的字符串,比如 ./main.o nihao hello 1234

argv[1] 就是 nihao, argv[2] 就是 hello,argv[3] 就是 1234

4. 将驱动程序编入 Linux 内核代码树

驱动开发完毕,就必须要将驱动编入 Linux 内核代码树,假如 Linux 内核代码在./linux-3.3 目录,我们的驱动名字叫做 ad9833.c,那么我们就要将 ad9833.c 文件放入./linux-3.3/drivers/char 目录下,操作两件事情。

修改 Kconfig 文件

修改 Kconfig 文件,在 menuconfig 文件中会出现我们的内核配置选项。

config  AD9833_ADI
        tristate "AD9833 DDS support."
        depends on ARM
        help
          GPIO on OMAPL138 configuration is:
          AD9833_FSY_IO -> GPIO[0,1]
          AD9833_CLK_IO -> GPIO[0,5]
          AD9833_DAT_IO -> GPIO[0,0]
  • tristate: 内核在 linux menuconfig 菜单下显示的名字
  • depends on ARM: 只有在 ARM 架构下才会显示出来该驱动于 menuconfig 中
  • help:帮助文档,做一些提示,我这里给出了 GPIO 的接法。

修改该目录下的 Makefile 文件

在文末追加
obj-$(CONFIG_AD9833_ADI) += ad9833.o
这里 CONFIG_后面接的必须和上面的 Kconfig 中 config 字段一样 ad9833.o 的.o 文件必须和放入该内核代码的 ad9833.c 名字字段一样。

编译内核

  • 配置 menuconfig

make CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm menuconfig
然后,进入到 drivers -> char.. device -> 找到你的驱动
以模块编译或者编译进内核。

  • 编译内核

make CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm -j8

  • 生成 uImage 文件(这个是 omapl 平台要求的)

make CROSS_COMPILE=arm-none-linux-gnueabi- ARCH=arm uImage

  • 将内核和文件都放到目标板子

可以重启运行了

  • 加载内核

insmod ad9833.ko

  • 运行测试程序

可以看到效果了:

源代码下载

链接: https://pan.baidu.com/s/1rfZy… 密码: 4pxx

参考文献

[1] zqixiao_09, [Linux 字符设备驱动开发基础(四)—— ioctl() 函数解析
](https://blog.csdn.net/zqixiao… 2016-03-11
[2] 草根老师, 解决 undefined reference to __aeabi_uidivmod 和 undefined reference to __aeabi_uidiv’ 错误, 2012-07-21 21:59:03
[3] 小 C 爱学习,一步一步写 miscdevice 的驱动模块, 2013-07-24
[4] 宋宝华,Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核

正文完
 0