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

56

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

我已经编写了一些基本的字符驱动程序,并且我认为编写SPI设备驱动程序与它类似。字符驱动程序具有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驱动程序。

但是,我没有看到像open、read、write等函数。 不知道程序从哪里开始。


2
只是一个问题:你为什么要重写SPI驱动程序?我之前用过OMAP4 SPI驱动程序,没有遇到任何问题。 - Nils Pipenbrinck
21
编写驱动程序的主要目的是为了学习。 - Sagar Jain
可能是 https://stackoverflow.com/questions/22706570/basic-device-operations-in-spi-driver?rq=1 的重复问题? - tauseef_CuriousGuy
1
您可以看到指针和引用指向设备的不同部分。驱动程序与硬件和内核工具“交互”,因此不需要严格的“打开”或“写入”命令。 - ILMostro_7
s/commands/functions/ - ILMostro_7
4个回答

70

首先,编写一个通用的内核模块。有多个地方可以查找信息,但我发现这个链接非常有用。在您完成那里指定的所有示例之后,可以开始编写自己的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。与字符驱动程序相比,这里的主要函数是*_transfer()函数。在spi.h文件中查找结构描述以获取更多详细信息。还要查看这个替代设备驱动程序API。


1
我在这里找到了“此链接”(_Linux内核模块编程指南_)的更新版本:https://github.com/sysprog21/lkmpg。 - JohanPI

21

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

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

实际上,您不需要关心主驱动程序来编写从设备驱动程序。我如何知道spi-omap2-mcspi.c是主驱动程序?它调用了spi_register_master()

SPI主机,SPI从机?

请参见Documentation/spi/spi_summary文件。该文档提到控制器驱动程序(主机)和协议驱动程序(从机)。从您的描述中,我理解您想要编写协议/设备驱动程序

SPI协议?

要了解这一点,您需要查看从设备的数据手册,它应该告诉您:

  • 您的设备理解的SPI模式
  • 总线上它所期望的协议

与I2C相反,SPI不定义协议或握手协议,SPI芯片制造商必须定义自己的协议。因此请检查数据手册。

SPI模式

来自include/linux/spi/spi.h

* @mode: SPI模式定义了数据如何时钟输出和输入。
* 设备驱动程序可以更改这个模式。
* 片选模式的“低电平”默认值可以被覆盖
* (通过指定SPI_CS_HIGH),每个传输中字的“最高位先”默认值也可以被覆盖
* (通过指定SPI_LSB_FIRST)。

同样,请检查您的SPI设备数据手册。

一个示例SPI设备驱动程序?

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

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

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、位挤压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主设备。

希望这有所帮助。


7

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:
https://dev59.com/C2gu5IYBdhLWcg3wWVyF
*/
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");

用户空间Shell测试程序

#!/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涉及到的内容比较复杂,以下是一些进一步的例子:
- ioctl - poll - mmap 从模拟器中开始软件模型化简化硬件 实际设备硬件开发很“难”,因为:
- 你不能总是轻松地获得所需的硬件。 - 硬件API可能很复杂。 - 很难看到硬件的内部状态。
像QEMU这样的模拟器通过在软件中模拟简化的硬件模拟来克服这些困难。
例如,QEMU有一个内置的教育PCI设备,称为edu,我在这里进一步解释了它:如何在QEMU源代码中添加新设备?,是开始使用设备驱动程序的好方法。我在这里提供了一个简单的驱动程序
然后,你可以像任何其他程序一样在QEMU上放置printf或使用GDB,并精确地看到发生了什么。
对于你特定的用例,还有一个OPAM SPI模型:https://github.com/qemu/qemu/blob/v2.7.0/hw/ssi/omap_spi.c

1
再次感谢Ciro的出色写作和更多资源。太棒了! - J Evans

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接