簡體   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