您尚未登录。

楼主 # 2021-06-20 15:07:50

SdtElectronics
会员
注册时间: 2020-07-27
已发帖子: 101
积分: 379.5
个人网站

主线内核上的下一代GPIO API简介及其C++包装库的一点使用经验

Linux上利用sysfs接口操作GPIO大家都已不陌生,但在Linux 4.8以后这个API就已经deprecated了,其后继者是GPIO character device API,暴露到用户空间的设备文件位置由/sys/class/gpio变动到/dev/gpiochipN(虽然只要编译内核时没有取消sysfs GPIO接口的支持,sysfs API仍然可以用)。新接口的设备文件没法像之前的sysfs接口一样,通过直接对data和direction等文件的读写来简便地操作IO,但相比sysfs接口增加了数个重大的功能支持:

  • 为设置IO的上下拉提供了统一的界面。此前虽然部分厂家的BSP内核通过私有的界面实现了sysfs中对IO上下拉的配置,但主线内核里是没有支持的

  • 提供了用户空间中断的原生支持。此前如果想利用sysfs实现类似中断的效果,只能利用select/poll来模拟

  • 可以在一个系统调用的时间内同时读取/设置多个IO的值。此前操作多个IO必须分别对每个IO对应的设备文件进行read/write,相比之下新的接口不但节省了开销,更可以对多个IO电平变化的时机进行精确的同步

新接口对于IO的操作主要是通过作用在设备文件上的一系列ioctl实现的,比较底层而不方便使用,一个建立在其上的封装库libgpiod应运而生。这个库本身是纯C的,不过提供了对于python和C++两种语言的封装,官方称其为python/C++ binding。由于纯C的界面我并没有直接使用过,下文主要介绍一下C++ binding的使用方法。libgpiod项目本身还在开发中,文档并不完善,而且文档采用的是和kernel一致的嵌入在代码注释里的风格,有不少地方的描述欠详,一些要点只能依靠上下文和源码猜测,如果有错误之处敬请指出。

离线

楼主 #1 2021-06-20 15:55:09

SdtElectronics
会员
注册时间: 2020-07-27
已发帖子: 101
积分: 379.5
个人网站

Re: 主线内核上的下一代GPIO API简介及其C++包装库的一点使用经验

libgpiod C++ binding 概述

libgpiod C++ binding,以下简称libgpiod,主要提供了三个对象来实现对IO的操作,分别是

  • gpiod::chip代表一个GPIO芯片,对应/dev下的一个gpiochipN文件

  • gpiod::line代表一个GPIO引脚

  • gpiod::line_bulk代表多个GPIO引脚,开头说的同时操作多个IO就是依此实现的

操作一个IO的步骤是:
1,使用gpiod::chip构造函数实例化一个chip对象,参数是character device的文件名,比如gpiochip0:

gpiod::chip chip("gpiochip0");

2,使用gpiod::chip::get_line或者get_lines来获得一个line或者line_bulk对象
对于gpiod::chip::get_line,参数是那个引脚对应的偏移量,比如主线内核里一个port一般是32个脚,PH00就是224:

gpiod::line PH00 = chip.get_line(224);

对于gpiod::chip::get_lines,参数是包含那一组引脚对应的偏移量的一个std::vector<unsigned int>,利用C++11列表初始化可以很方便地写在一行:

gpiod::line_bulk PH00_01 = get_lines(std::vector<unsigned int>{224, 225});

需要注意的是到这一步line和line_bulk还不能用,如果直接调用其上的方法会抛出一个std::system_error异常:
what():  error setting GPIO line values: Operation not permitted
必须首先通过gpiod::line::request或者gpiod::line_bulk::request来配置这个引脚的方向和偏置(上下拉或开漏/开源),参数是一个gpiod::line_request结构体。这个结构体有三个成员,分别是consumer名字的string,io的用途(输入或输出或中断),以及一个标志位的bitset,用于配置极性,输入上下拉或者输出开漏或开源,利用C++11列表初始化也可以很方便地写在一行:

PH00.request({"", gpiod::line_request::DIRECTION_OUTPUT, 0})

其中io的用途是一个匿名枚举,可以是以下的值:

		DIRECTION_AS_IS = 1,
		/**< Request for values, don't change the direction. */
		DIRECTION_INPUT,
		/**< Request for reading line values. */
		DIRECTION_OUTPUT,
		/**< Request for driving the GPIO lines. */
		EVENT_FALLING_EDGE,
		/**< Listen for falling edge events. */
		EVENT_RISING_EDGE,
		/**< Listen for rising edge events. */
		EVENT_BOTH_EDGES,
		/**< Listen for all types of events. */

每个值的含义配合名字和注释应该很容易理解,不赘述了,多提一句,需要使用这个匿名枚举的类型名的时候怎么办?可以利用C++11的decltype给他指派一个名字:

using reqType = decltype(glReq::EVENT_BOTH_EDGES);

标志位可以是以下几个预定义的值,配合名字和注释也很容易理解,不赘述:

FLAG_ACTIVE_LOW;
/**< Set the active state to 'low' (high is the default). */
FLAG_OPEN_SOURCE;
/**< The line is an open-source port. */
FLAG_OPEN_DRAIN;
/**< The line is an open-drain port. */
FLAG_BIAS_DISABLED;
/**< The line has neither pull-up nor pull-down resistor enabled. */
FLAG_BIAS_PULL_DOWN;
/**< The line has a configurable pull-down resistor enabled. */
FLAG_BIAS_PULL_UP;
/**< The line has a configurable pull-up resistor enabled. */

操作line的函数主要就是get_value()读取和set_value(int val)设置,line_bulk对应的的get_values()和set_values(const ::std::vector<int>& values)就是把返回值和参数换成vector,更多的成员函数可以在libgpiod头文件的源码里查阅,不多说。
已经request过的line/lines要作他用时必须先调用release(),否则会抛出std::system_error异常:
what():  error requesting GPIO lines: Device or resource busy
最后需要注意的是,编译的时候需带上选项 -lgpiodcxx 以链接对应的库。当然,编译平台上还必须安装了libgpiod的包。

最近编辑记录 SdtElectronics (2021-06-20 16:24:35)

离线

楼主 #2 2021-06-20 16:17:24

SdtElectronics
会员
注册时间: 2020-07-27
已发帖子: 101
积分: 379.5
个人网站

Re: 主线内核上的下一代GPIO API简介及其C++包装库的一点使用经验

关于中断
libgpiod的一个特色是可以简便地在用户空间使用外部中断,具体的实现机制我并不清楚,但应该和sysfs的polling有很大区别,因为申请用作中断的引脚必须是有硬件中断支持的。尝试为没有连接到中断控制器的引脚请求中断的话会抛出异常std::system_error:
what():  error requesting GPIO lines: Unknown error 517
和多数中断注册API一样,libgpiod可以监视三种中断:上升沿,下降沿和双边沿。以监视PH00上的下降沿为例:

PH00.request({"", gpiod::line_request::EVENT_FALLING_EDGE, 0});

之后就可以通过gpiod::line::event_wait()来等待中断了,这个调用会一直阻塞到中断发生为止或超时才返回,参数是一个const std::chrono::nanoseconds&,正是设置超时时间的,例如等待PH00上的中断,1小时后超时:

PH00.event_wait(std::chrono::hours(1));

假设一个下降沿在PH00上产生,上一个调用返回了。这时再调用一次line::event_wait(),这个调用不会等到下一个下降沿来临,而是会直接返回,就像单片机上的中断标志位没被清除一样。这是因为上一次产生中断的事件没有被读取,调用一次line::event_read就可以清除了:

PH00.event_read();

另外line::event_read返回上次中断的事件类型,这样监视双边沿的时候可以利用它来获知到底产生中断的是什么边沿。
line::event_read只能清除一个中断事件,如果此次event_wait之前产生了多个中断,调用还是会立即返回。line::event_read_multiple则可以一次读取、清除所有此前的中断事件。注意这两个read调用前没有产生中断的话都会被阻塞到中断产生。
最后一点就是get_value不止可以用在被配置为DIRECTION_INPUT的line上,对于配置为输出和中断的line也是能用的。这个可以用来判断中断之后io的电平,来实现消抖之类的功能。

离线

页脚

工信部备案:粤ICP备20025096号 Powered by FluxBB

感谢为中文互联网持续输出优质内容的各位老铁们。 QQ: 516333132, 微信(wechat): whycan_cn (哇酷网/挖坑网/填坑网) service@whycan.cn