如何编写简单的Linux设备驱动程序?

时间:2014-03-25 11:07:52

标签: c linux linux-kernel linux-device-driver embedded-linux

我需要从头开始为omap4编写SPI Linux字符设备驱动程序。 我知道编写设备驱动程序的一些基础知识。但是,我不知道如何从头开始编写特定于平台的设备驱动程序。

我已经编写了一些基本的char驱动程序,我认为编写SPI设备驱动程序与它类似。 Char驱动程序具有结构file_operations,其中包含驱动程序中实现的功能。

struct file_operations Fops = {
    .read = device_read,
    .write = device_write,
    .ioctl = device_ioctl,
    .open = device_open,
    .release = device_release,  /* a.k.a. close */
};

现在,我将通过spi-omap2-mcspi.c代码作为参考,以便从头开始开发SPI驱动程序。

但是,我没有看到打开,读取,写入等功能。 不知道程序从哪里开始。

4 个答案:

答案 0 :(得分:53)

首先编写一个通用内核模块。有多个地方可以查找信息,但我发现this link非常有用。在完成指定的所有示例之后,您可以开始编写自己的Linux驱动程序模块。

请注意,您只需复制粘贴示例代码并希望它可以正常工作,不会。内核API有时可能会更改,但示例不起作用。提供的示例应该被视为如何做某事的指南。根据您使用的内核版本,您必须修改示例才能工作。

尽可能考虑使用TI平台提供的功能,因为这可以为您做很多工作,例如请求和启用所需的时钟,总线和电源。如果我没记错,你可以使用这些函数获取内存映射地址范围,以便直接访问寄存器。我必须提到我对TI提供的功能有不好的经验,因为他们没有正确地释放/清理所有获得的资源,所以对于某些资源,我必须调用其他内核服务来在模块卸载期间释放它们。

修改1:

我并不完全熟悉Linux SPI实现,但我首先要查看drivers / spi / spi-omap2-mcspi.c文件中的omap2_mcspi_probe()函数。正如您所看到的,它使用此API将其方法注册到Linux主SPI驱动程序:Linux / include / linux / spi / spi.h。与char驱动程序相比,这里的主要功能是* _transfer()函数。查看spi.h文件中的结构描述以获取更多详细信息。另外,请查看this备用设备驱动程序API。

答案 1 :(得分:19)

我假设您的OMAP4 linux使用arch/arm/boot/dts/{omap4.dtsi,am33xx.dtsi}设备树之一,因此它编译drivers/spi/spi-omap2-mcspi.c(如果您不了解设备树,read this)。然后:

  • SPI主驱动程序已完成,
  • 它(很可能)注册Linux SPI核心框架drivers/spi/spi.c
  • 它(可能)在您的OMAP4上正常工作。

您实际上并不需要关心主驱动程序来编写从设备驱动程序。我怎么知道spi-omap2-mcspi.c是主驾驶员?它会调用spi_register_master()

SPI主设备,SPI从设备?

请参阅Documentation/spi/spi_summary。 doc指的是 Controller驱动程序(主)和协议驱动程序(从属)。根据您的描述,我了解您要编写协议/设备驱动程序

SPI协议?

要理解这一点,你需要你的从设备数据表,它会告诉你:

  • SPI模式,您的设备可以理解
  • 它在公交车上的协议

与i2c相反,SPI没有定义协议或握手,SPI芯片制造商必须自己定义。请查看数据表。

SPI模式

来自include/linux/spi/spi.h

 * @mode: The spi mode defines how data is clocked out and in.
 *  This may be changed by the device's driver.
 *  The "active low" default for chipselect mode can be overridden
 *  (by specifying SPI_CS_HIGH) as can the "MSB first" default for
 *  each word in a transfer (by specifying SPI_LSB_FIRST).

再次检查您的SPI器件数据表。

示例SPI设备驱动程序?

为了给你一个相关的例子,我需要知道你的SPI设备类型。您会理解 SPI闪存设备驱动程序 SPI FPGA设备驱动程序不同。不幸的是,没有那么多的SPI设备驱动程序。找到它们:

$ cd linux 
$ git grep "spi_new_device\|spi_add_device"

答案 2 :(得分:11)

我不知道我是否正确理解了你的问题。正如m-ric指出的那样,有主驾驶员和奴隶驾驶员。

通常主驱动程序的硬件绑定更多,我的意思是,它们通常操纵IO寄存器或执行一些内存映射IO。

对于Linux内核已经支持的一些架构(如omap3和omap4),已经实现了主驱动程序(McSPI)。

所以我假设您想使用omap4的SPI工具来实现从设备驱动程序(您的协议,通过SPI与外部设备通信)。

我为BeagleBoard-xM(omap3)编写了以下示例。完整代码位于https://github.com/rslemos/itrigue/blob/master/alsadriver/itrigue.c(值得一看,但有更多初始化代码,适用于ALSA,GPIO,模块参数)。我试图分开处理SPI的代码(也许我忘记了一些东西,但无论如何你应该明白这一点):

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/spi/spi.h>

/* MODULE PARAMETERS */
static uint spi_bus = 4;
static uint spi_cs = 0;
static uint spi_speed_hz = 1500000;
static uint spi_bits_per_word = 16;

/* THIS IS WHERE YOUR DEVICE IS CREATED; THROUGH THIS YOU INTERACT WITH YOUR EXTERNAL DEVICE */
static struct spi_device *spi_device;


/* SETUP SPI */

static inline __init int spi_init(void) {
    struct spi_board_info spi_device_info = {
        .modalias = "module name",
        .max_speed_hz = spi_speed_hz,
        .bus_num = spi_bus,
        .chip_select = spi_cs,
        .mode = 0,
    };

    struct spi_master *master;

    int ret;

    // get the master device, given SPI the bus number
    master = spi_busnum_to_master( spi_device_info.bus_num );
    if( !master )
        return -ENODEV;

    // create a new slave device, given the master and device info
    spi_device = spi_new_device( master, &spi_device_info );
    if( !spi_device )
        return -ENODEV;

    spi_device->bits_per_word = spi_bits_per_word;

    ret = spi_setup( spi_device );
    if( ret )
        spi_unregister_device( spi_device );

    return ret;
}

static inline void spi_exit(void) {
    spi_unregister_device( spi_device );
}

要将数据写入您的设备:

spi_write( spi_device, &write_data, sizeof write_data );

上述代码与实现无关,也就是说,它可以使用McSPI,bit-banged GPIO或SPI主设备的任何其他实现。此接口在linux/spi/spi.h

中描述

要使它在BeagleBoard-XM中工作,我必须将以下内容添加到内核命令行:

omap_mux=mcbsp1_clkr.mcspi4_clk=0x0000,mcbsp1_dx.mcspi4_simo=0x0000,mcbsp1_dr.mcspi4_somi=0x0118,mcbsp1_fsx.mcspi4_cs0=0x0000

为omap3 McSPI4硬件设施创建了一个McSPI主设备。

希望有所帮助。

答案 3 :(得分:6)

file_operations最小可运行示例

此示例不与任何硬件交互,但它说明了带有debugfs的更简单的file_operations内核API。

内核模块fops.c

#include <asm/uaccess.h> /* copy_from_user, copy_to_user */
#include <linux/debugfs.h>
#include <linux/errno.h> /* EFAULT */
#include <linux/fs.h> /* file_operations */
#include <linux/kernel.h> /* min */
#include <linux/module.h>
#include <linux/printk.h> /* printk */
#include <uapi/linux/stat.h> /* S_IRUSR */

static struct dentry *debugfs_file;
static char data[] = {'a', 'b', 'c', 'd'};

static int open(struct inode *inode, struct file *filp)
{
    pr_info("open\n");
    return 0;
}

/* @param[in,out] off: gives the initial position into the buffer.
 *      We must increment this by the ammount of bytes read.
 *      Then when userland reads the same file descriptor again,
 *      we start from that point instead.
 * */
static ssize_t read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
    ssize_t ret;

    pr_info("read\n");
    pr_info("len = %zu\n", len);
    pr_info("off = %lld\n", (long long)*off);
    if (sizeof(data) <= *off) {
        ret = 0;
    } else {
        ret = min(len, sizeof(data) - (size_t)*off);
        if (copy_to_user(buf, data + *off, ret)) {
            ret = -EFAULT;
        } else {
            *off += ret;
        }
    }
    pr_info("buf = %.*s\n", (int)len, buf);
    pr_info("ret = %lld\n", (long long)ret);
    return ret;
}

/* Similar to read, but with one notable difference:
 * we must return ENOSPC if the user tries to write more
 * than the size of our buffer. Otherwise, Bash > just
 * keeps trying to write to it infinitely. */
static ssize_t write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
    ssize_t ret;

    pr_info("write\n");
    pr_info("len = %zu\n", len);
    pr_info("off = %lld\n", (long long)*off);
    if (sizeof(data) <= *off) {
        ret = 0;
    } else {
        if (sizeof(data) - (size_t)*off < len) {
            ret = -ENOSPC;
        } else {
            if (copy_from_user(data + *off, buf, len)) {
                ret = -EFAULT;
            } else {
                ret = len;
                pr_info("buf = %.*s\n", (int)len, data + *off);
                *off += ret;
            }
        }
    }
    pr_info("ret = %lld\n", (long long)ret);
    return ret;
}

/*
Called on the last close:
http://stackoverflow.com/questions/11393674/why-is-the-close-function-is-called-release-in-struct-file-operations-in-the-l
*/
static int release(struct inode *inode, struct file *filp)
{
    pr_info("release\n");
    return 0;
}

static loff_t llseek(struct file *filp, loff_t off, int whence)
{
    loff_t newpos;

    pr_info("llseek\n");
    pr_info("off = %lld\n", (long long)off);
    pr_info("whence = %lld\n", (long long)whence);
    switch(whence) {
        case SEEK_SET:
            newpos = off;
            break;
        case SEEK_CUR:
            newpos = filp->f_pos + off;
            break;
        case SEEK_END:
            newpos = sizeof(data) + off;
            break;
        default:
            return -EINVAL;
    }
    if (newpos < 0) return -EINVAL;
    filp->f_pos = newpos;
    pr_info("newpos = %lld\n", (long long)newpos);
    return newpos;
}

static const struct file_operations fops = {
    /* Prevents rmmod while fops are running.
     * Try removing this for poll, which waits a lot. */
    .owner = THIS_MODULE,
    .llseek = llseek,
    .open = open,
    .read = read,
    .release = release,
    .write = write,
};

static int myinit(void)
{
    debugfs_file = debugfs_create_file("lkmc_fops", S_IRUSR | S_IWUSR, NULL, NULL, &fops);
    return 0;
}

static void myexit(void)
{
    debugfs_remove_recursive(debugfs_file);
}

module_init(myinit)
module_exit(myexit)
MODULE_LICENSE("GPL");

Userland shell test program

#!/bin/sh

mount -t debugfs none /sys/kernel/debug

insmod /fops.ko
cd /sys/kernel/debug/lkmc_fops

## Basic read.
cat f
# => abcd
# dmesg => open
# dmesg => read
# dmesg => len = [0-9]+
# dmesg => close

## Basic write

printf '01' >f
# dmesg => open
# dmesg => write
# dmesg => len = 1
# dmesg => buf = a
# dmesg => close

cat f
# => 01cd
# dmesg => open
# dmesg => read
# dmesg => len = [0-9]+
# dmesg => close

## ENOSPC
printf '1234' >f
printf '12345' >f
echo "$?"
# => 8
cat f
# => 1234

## seek
printf '1234' >f
printf 'z' | dd bs=1 of=f seek=2
cat f
# => 12z4

如果您不清楚为每个命令调用哪些系统调用,您还应该编写一个运行这些测试的C程序。 (或者您也可以使用strace并找出: - ))。

其他file_operations涉及更多,下面是一些其他示例:

从仿真器中简化硬件的软件模型开始

实际的设备硬件开发“很难”,因为:

  • 您不能轻易将手放在给定的硬件上
  • 硬件API可能很复杂
  • 很难看出硬件的内部状态是什么

像QEMU这样的仿真器允许我们通过在软件中模拟简化的硬件仿真来克服所有这些困难。

例如,QEMU有一个名为edu的内置教育PCI设备,我在How to add a new device in QEMU source code?进一步解释,这是开始使用设备驱动程序的好方法。我为它制作了一个简单的驱动程序available here

然后您可以像在任何其他程序一样在QEMU上放置printf或使用GDB,并查看到底发生了什么。

还有针对您特定用例的OPAM SPI模型:https://github.com/qemu/qemu/blob/v2.7.0/hw/ssi/omap_spi.c