楼主在网上找了很多相关LINUX驱动开发的相关例程。发现基本的驱动开发都是有框架或者多种开发手段的,我们可以使用不同的开发方式来降低开发难度。有文件系统的开发和LINUX系统自带的内核函数来开发。这两者有什么区别呢?就像是单片机的库函数开发和寄存器开发一样。

先让我们来看一下GPIO子系统在linux内核中的结构吧。该文件目录在/sys/class/gpio/下。

struct gpio_chip {
  const char *label;
  struct device *dev;
  struct module *owner;
  int (*request)(struct gpio_chip *chip, unsigned offset);
  void (*free)(struct gpio_chip *chip, unsigned offset);
  int (*get_direction)(struct gpio_chip *chip, unsigned offset);
  int (*direction_input)(struct gpio_chip *chip, unsigned offset);
  int (*direction_output)(struct gpio_chip *chip, unsigned offset,
  int value);
  int (*get)(struct gpio_chip *chip,unsigned offset);
  void (*set)(struct gpio_chip *chip, unsigned offset, int value);
  void (*set_multiple)(struct gpio_chip *chip, unsigned long *mask,
  unsigned long *bits);
  int (*set_debounce)(struct gpio_chip *chip, unsigned offset,
  unsigned debounce);
  int (*to_irq)(struct gpio_chip *chip, unsigned offset);
  int base;
  u16 ngpio;
  const char *const *names;
  bool can_sleep;
  bool irq_not_threaded;
  bool exported;
#ifdef CONFIG_GPIOLIB_IRQCHIP
  /*
   * With CONFIG_GPIOLIB_IRQCHIP we get an irqchip
   * inside the gpiolib to handle IRQs for most practical cases.
   */
  struct irq_chip *irqchip;
  struct irq_domain *irqdomain;
  unsigned int irq_base;
  irq_flow_handler_t irq_handler;
  unsigned int irq_default_type;
#endif
#if defined(CONFIG_OF_GPIO)
  /*
   * If CONFIG_OF is enabled, then all GPIO controllers described in the
    * device tree automatically may have an OF translation
   */
  struct device_node *of_node;
  int of_gpio_n_cells;
  int (*of_xlate)(struct gpio_chip *gc,
  const struct of_phandle_args *gpiospec, u32 *flags);
};

以下几个是主要的注意事项:

  • request 是特定芯片激活的可选回调函数。如果提供了,在调用gpio_request()或gpiod_get()时,它会在分配GPIO之前执行。

  • free 是一个可选的回调函数,用于特定芯片的释放。如果提供了,那么在调用gpiod_put()或gpio_free()时,它会在GPIO被释放之前执行。

  • get_direction 在您需要知道方向的时候执行GPIO偏移量。返回值应为0表示out, 1表示in(与GPIOF_DIR_XXX相同),或负错误。

  • direction_input 将信号偏移量offset配置为输入,否则返回错误。

  • get 返回GPIO offset 的值;对于输出信号,这将返回实际感知到的值或0。

  • set 指定一个输出值给GPIO offset。


有些的芯片运行的Linux系统里面自带了引脚的编号,如果要获取编号可能要在该芯片文档中用公式计算。以Orange-pi为例它的计算公式如图所示:

利用SYSFS设置GPIO

当然,我们也能查看已经被占用的GPIO端口。

cat /sys/kernel/debug/gpio

例如第一行的意思是编号为8的GPIO的名字是goodix-int,现在是输入模式并且是高电平。已经被占用的GPIO无法使用,除非修改其寄存器。


让我们来尝试的用命令行初始化一个GPIO吧。

echo 10 > export #添加一个GPIO并且设置编号

然后我们进入GPIO10的direction 查看其电平状态。

echo out > direction #改变为输出模式

同理可用次办法尝试设置LED的参数,现在如下图我的电位是低电平所以我的LED灯已经亮起。

现在再次查看设备,GPIO10已被添加并且被设置。


当然我们也能用代码来实现上面的操作,这里举个例子,在我更后面的博客里面我会详细说明。Linux一切皆文件!! 这使得一切都变得非常好理解。

int gpio_set_dir(unsigned int gpio, unsigned int out_flag)
{
    int fd, len;
    char buf[MAX_BUF];
 
    len = snprintf(buf, sizeof(buf), SYSFS_GPIO_DIR  "/gpio%d/direction", gpio);
 
    fd = open(buf, O_WRONLY);
    if (fd < 0) {
        perror("gpio/direction");
        return fd;
    }
 
    if (out_flag)
        write(fd, "out", 4);
    else
        write(fd, "in", 3);
 
    close(fd);
    return 0;
}

例如上面就是设置一个IO输入输出的函数。也是打开文件系统然后往里面写参数就可以了。相信大家学过linux文件系统的都会很简单的理解。


方法2:用linux内核函数来初始化GPIO

话不多说,上框架。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
static int led_init(void)//驱动函数入口
{
}
static void led_exit(void)//驱动函数出口
{
}
module_init(led_init);//注册入口
module_exit(led_exit);//注册出口
MODULE_LICENSE("GPL");//模块的许可证声明这个一般都要有这句话,具体可以去百度

然后丰富函数内容。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <mach/platform.h> //PAD_GPIO_C

static int led_init(void)
{
    gpio_request(10,"myled");
    gpio_direction_output(10, 0);
    printk("led init...\n");
    return 0;
}
static void led_exit(void)
{
    gpio_set_value(10, 1);
    gpio_free(10);
    printk("led exit...\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

这样,一个GPIO的LED驱动就完成啦。下面我将再次说明以下与GPIO相关的简单驱动内核函数。

  • fint gpio_request(unsigned gpio, const char *label)

函数功能:CPU的任何一个GPIO引脚硬件资源对于Linux内核来说都是一种宝贵的资源,如果某个内核程序要想访问这个GPIO引脚资源,首先必须想Linux内核申请资源(类似malloc)

  • void gpio_free(unsigned gpio)

函数功能:内核程序如果不再使用访问GPIO硬件资源记得要将硬件资源归还给linux内核,类似free

  • int gpio_direction_output(unsigned gpio, int value)

函数功能:配置GPIO引脚为输出功能,并且输出一个value值(1高电平/0低电平)

  • int gpio_direction_input(unsigned gpio)

函数功能:配置GPIO为输入功能

  • int gpio_set_value(unsigned gpio, int value)

函数功能:设置GPIO引脚的输出值为value(1:高/0:低),前提是必须首先将GPIO配置为输出功能

  • int gpio_get_value(unsigned gpio)

函数功能:获取GPIO引脚的电平状态,返回值就是引脚的电平状态(返回1:高电平;返回0:低电平),此引脚到底是输入还是输出没关系!


设备树

Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。设备树源文件扩展名为.dts(device tree source),一般放置/arch/arm/boot/dts/目录内。设备树信息在根文件系统中/proc/device-tree目录下,包括根节点’/’的所有属性和子节点。

设备树的格式如下(例子):

    gpios {
        compatible = "gpio-user";                                                                                                                                                                                   
        status = "okay";
        /*input*/
        gpio0 {
            label = "in0";
            gpios = <&gpio1 1 0>;
            default-direction = "in";
         };
        ... ...
         /*output*/
         gpio17 {
            label = "out1";
            gpios = <&gpio3 3 0>;
            default-direction = "out";
        };
    };

为啥有时候要修改设备树呢?因为有时候你不想重新改动驱动代码,或者其他不需要的设备占用了你的IO,你想把它ban掉。有时候开发会涉及到设备树的修改,这里我引用一个大佬写的比较详细的博客:设备树的使用和说明 - 知乎 (zhihu.com)。(如有侵权亲联系我我会立刻删掉)


我们在编译这些文件的时候一般的会用makefile编译。不会写makefile的小伙伴可以去学CMake,足以应付一些不太复杂的文件编译。在这里我也推荐一篇大佬写的不错的博客:https://blog.csdn.net/weixin_44498318/article/details/106219135。(如有侵权亲联系我我会立刻删掉)

Logo

更多推荐