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

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

我写了一些基本的字符驱动程序,我认为写SPI设备驱动程序将是类似于它。 字符驱动程序有一个结构file_operations ,其中包含在驱动程序中实现的function。

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

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

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

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

请注意,只要复制粘贴示例代码,并希望它可以工作,就不会脱身。 内核API有时可能会改变,例子不起作用。 在这里提供的例子应该被看作是一个指导如何做一些事情。 根据您使用的内核版本,您必须修改示例才能正常工作。

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

编辑1:

我并不完全熟悉Linux SPI的实现,但是我将首先查看drivers / spi / spi-omap2-mcspi.c文件中的omap2_mcspi_probe()函数。 正如你在那里可以看到的那样,它使用这个API将它的方法注册到Linux主SPI驱动程序:Linux / include / linux / spi / spi.h。 与char驱动程序相比,这里的主要function是* _transfer()函数。 查看spi.h文件中的结构描述以获取进一步的细节。 另外,也可以看看这个替代设备驱动程序API。

我假设你的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模式定义数据如何输出和input。
  *这可能会由设备的驱动程序更改。
  * chipselect模式的“低电平有效”默认值可以被覆盖
  *(通过指定SPI_CS_HIGH)和“MSB优先”缺省值一样
  *传输中的每个字(通过指定SPI_LSB_FIRST)。

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

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

为了给你一个相关的例子,我需要知道你的SPI设备types。 您将会理解, SPI闪存设备驱动程序SPI FPGA设备驱动程序不同 。 不幸的是,在那里没有太多的SPI设备驱动程序。 要find它们:

 $ 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硬件设备创build了一个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 shelltesting程序 :

 #!/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程序来运行这些testing,如果你不清楚哪些系统调用是为每个命令调用的。 (或者你也可以使用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