繁体   English   中英

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

[英]How to write a simple Linux device driver?

我需要从头开始为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驱动程序。

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

首先从编写通用内核模块开始。 有多个地方可以查找信息,但我发现此链接非常有用。 在完成指定的所有示例之后,您可以开始编写自己的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文件中的结构描述以获取更多详细信息。 此外,还要看一下这个备用设备驱动程序API。

我假设您的OMAP4 linux使用arch/arm/boot/dts/{omap4.dtsi,am33xx.dtsi} device-tree之一,因此它编译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 doc指的是Controller驱动程序 (master)和Protocol驱动程序 (slave)。 根据您的描述,我了解您要编写协议/设备驱动程序

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"

我不知道我是否理解你的问题。 正如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主设备。

希望有所帮助。

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测试程序

#!/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设备,我在下面进一步解释: 如何在QEMU源代码中添加新设备? 并且是开始使用设备驱动程序的好方法。 我在这里提供了一个简单的驱动程序。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM