zoukankan      html  css  js  c++  java
  • 什么是GPIO?

    ”通用输入/输出口”(GPIO)是一个灵活的由软件控制的数字信号。他们可由多种芯片提供,且对于从事嵌入式和定制硬件的Linux开发者来说是比较熟 悉。每个GPIO都代表一个连接到特定引脚或球栅阵列(BGA)封装中“球珠”的一个位。电路板原理图显示了GPIO与外部硬件的连接关系。驱动可以编写 成通用代码,以使板级启动代码可传递引脚配置数据给驱动。


    片 上系统 (SOC) 处理器对GPIO有很大的依赖。在某些情况下,每个非专用引脚都可配置为GPIO,且大多数芯片都最少有一些GPIO。可编程逻辑器件(类似 FPGA) 可以方便地提供GPIO。像电源管理和音频编解码器这样的多功能芯片经常留有一些这样的引脚来帮助那些引脚匮乏的SOC。同时还有通过I2C或SPI串行 总线连接的”GPIO扩展器”芯片。大多数PC的南桥有一些拥有GPIO能力的引脚 (只有BIOS固件才知道如何使用他们)。


    GPIO的实际功能因系统而异。通常的用法有:


    - 输出值可写 (高电平=1, 低电平=0)。一些芯片也有如何驱动这些值的选项,例如只允许输出一个值、支持“线与”及其他取值类似的模式(值得注意的是“开漏”信号)。


    - 输入值可读(1、0)。一些芯片支持引脚在配置为“输出”时回读,这对于类似“线与”的情况(以支持双向信号)是非常有用的。GPIO 控制器可能有输入去毛刺/消抖逻辑,这有时需要软件控制。


    - 输入通常可作为IRQ信号,一般是沿触发,但有时是电平触发。这样的 IRQ 可能配置为系统唤醒事件,以将系统从低功耗状态下唤醒。


    - 通常一个GPIO根据不同产品电路板的需求,可以配置为输入或输出,也有仅支持单向的。


    - 大部分 GPIO 可以在持有自旋锁时访问,但是通常由串行总线扩展的GPIO不允许持有自旋锁。但某些系统也支持这种类型。


    对于给定的电路板,每个GPIO都用于某个特定的目的,如监控MMC/SD卡的插入/移除、检测卡的写保护状态、驱动LED、配置收发器、模拟串行总线、复位硬件看门狗、感知开关状态等等。


    GPIO公约


    注 意,这个叫做“公约”,因为这不是强制性的,不遵循这个公约是无伤大雅的,因为此时可移植性并不重要。GPIO常用于板级特定的电路逻辑,甚至可能随着电 路板的版本而改变,且不可能在不同走线的板子上使用。仅有在很少的功能上才具有可移植性,其他功能是平台特定。这也是由于“胶合”的逻辑造成的。


    此 外,这不需要任何的执行框架,只是一个接口。某个平台可能通过一个简单的访问芯片寄存器的内联函数来实现它,其他平台可能通过委托一系列不同的GPIO控 制器的抽象函数来实现它。(有一些可选的代码能支持这种策略的实现,本文档后面会介绍,但作为GPIO接口的客户端驱动程序必须与它的实现无关。)


    也 就是说,如果在他们的平台上支持这个公约,驱动应该尽可能的使用它。平台必须在Kconfig中声明对 GENERIC_GPIO 的支持 (布尔型 true),并提供一个 <asm/gpio.h> 文件。那些调用标准GPIO函数的驱动应该在 Kconfig 入口中声明依赖GENERIC_GPIO。当驱动包含文件:




      1. #include <linux/gpio.h>



    GPIO函数是可用,无论是“真实代码”还是经优化过的语句。


    如果你遵守这个公约,当你的代码完成后,对其他的开发者来说会更容易看懂和维护。


    注意,这些操作包含所用平台的 I/O 屏障代码,驱动无须显式地调用他们。


    标识GPIO


    GPIO是通过无符号整型来标识的,范围是0到MAX_INT。保留“负”数用于其他目的,例如标识信号“在这个板子上不可用”或指示错误。未接触底层硬件的代码会忽略这些整数。


    平 台会定义这些整数的用法,且通常使用 #define 来定义 GPIO ,这样板级特定的启动代码可以直接关联相应的原理图。相对来说,驱动应该仅使用启动代码传递过来的 GPIO 编号,使用 platform_data 保存板级特定引脚配置数据 (同时还有其他须要的板级特定数据),避免可能出现的问题。


    例 如一个平台使用编号 32-159 来标识 GPIO,而在另一个平台使用编号 0-63 标识一组 GPIO 控制器,64-79 标识另一类 GPIO 控制器, 且在一个含有FPGA的特定板子上使用 80-95 。编号不一定要连续,那些平台中,也可以使用编号 2000-2063 来标识一个I2C接口的GPIO扩展器中的GPIO。


    如果你要初始化一个带有无效GPIO编号的结构体,可以使用一些负编码(比如”-EINVAL”),那将永远不会是有效。来测试这样一个结构体中的编号是否关联一个GPIO,你可使用以下断言:




      1. int gpio_is_valid(int number);



    如果编号不存在,则请求和释放GPIO的函数将拒绝执行相关操作(见下文)。其他编号也可能被拒绝,比如一个编号可能存在,但暂时在给定的板子上不可用。


    一个平台是否支持多个GPIO控制器是平台特定的实现问题,就像是否可以在GPIO编号空间中有“空洞”和是否可以在运行时添加新的控制器一样。这些问题会影响其他事情,包括相邻的GPIO编号是否存在等。


    使用 GPIO


    对于一个GPIO,系统应该做的第一件事情就是通过 gpio_request() 函数分配他,见下文。


    而接下来要做的是标识它的方向,这通常是在板级启动代码中为GPIO设置一个platform_device时做的。




      1. / 设置为输入或输出, 返回 0 或负的错误代码 /


      1. int gpio_direction_input(unsigned gpio);


      1. int gpio_direction_output(unsigned gpio, int value);



    返 回值为零代表成功,否则返回一个负的错误代码。这个返回值需要检查,因为get/set(获取/设置)函数调用没法返回错误,且有可能是配置错误。通常, 你应该在进程上下文中调用这些函数。然而,对于自旋锁安全的GPIO,在板子启动的早期、进程启动前使用他们也是可以的。


    对于作为输出的GPIO,为其提供初始输出值,对于避免在系统启动期间出现信号毛刺是很有帮助的。


    为了与传统的GPIO接口兼容, 在设置一个GPIO方向时,如果它还未被申请,则隐含了申请那个GPIO的操作(见下文)。这种兼容性正在从可选的gpiolib框架中移除。


    如 果这个GPIO编码不存在或特定的GPIO不能用于那种模式,则方向设置可能失败。依赖启动固件来正确地设置方向通常是一个坏主意,因为它可能除了启动 Linux,并没有做更多的验证工作。(类似地, 板子的启动代码可能需要将这个复用的引脚设置为GPIO,并正确地配置上拉/下拉电阻。) 


    访问自旋锁安全的GPIO


    大多数GPIO控制器可以通过内存读/写指令来访问。这些指令不会休眠,可以安全地在硬(非线程)中断例程和类似的上下文中完成。


    对于那些用gpio_cansleep()测试总是返回失败的GPIO(见下文),使用以下的函数访问:




      1. / GPIO 输入:返回零或非零 /


      1. int gpio_get_value(unsigned gpio);




      1. / GPIO 输入 /


      1. void gpio_set_value(unsigned gpio, int value);



    返回值是布尔值,零表示低电平,非零表示高电平。当读取一个输出引脚的值时,返回值应该是引脚上的值。这个值不总是和输出值相符,因为存在开漏输出信号和输出潜伏期的问题。


    以 上的get/set函数不会对早期已经通过gpio_direction_*()报告“无效的GPIO”返回错误。此外,还需要注意的是并不是所有平台都 可以从输出引脚中读取数据的,那些引脚也不总是返回零。且对那些无法安全访问(可能会休眠)的GPIO(见下文)使用这些函数是错误的。


    在 GPIO编号(还有输出、值)为常数的情况下,鼓励通过平台特定的实现来优化这两个函数来访问GPIO值。这种情况(读写一个硬件寄存器)下只需要几条指 令是很正常的,且无须自旋锁。这种优化函数比起那些在子程序上花费许多指令的函数可以使得模拟接口(译者注:例如GPIO模拟I2C、1-wire或 SPI)的应用(在空间和时间上都)更具效率。


    访问可能休眠的GPIO


    某些GPIO控制器必须通过基于总线(如I2C或SPI)的消息访问。读或写这些GPIO值的命令需要等待其信息排到队首才发送命令,再获得其反馈。期间需要休眠,这不能在IRQ例程(中断上下文)中执行。


    支持此类GPIO的平台通过以下函数返回非零值来区分出这种GPIO。(此函数需要一个之前通过gpio_request分配到的有效的GPIO编号):




      1. int gpio_cansleep(unsigned gpio);


      1. 为了访问这种GPIO,内核定义了一套不同的函数:


      1. / GPIO 输入:返回零或非零 ,可能会休眠 /


      1. int gpio_get_value_cansleep(unsigned gpio);




      1. / GPIO 输入,可能会休眠 /


      1. void gpio_set_value_cansleep(unsigned gpio, int value);



    访问这样的GPIO需要一个允许休眠的上下文,例如线程IRQ处理例程,并用以上的访问函数替换那些没有cansleep()后缀的自旋锁安全访问函数。


    除了这些访问函数可能休眠,且它们操作的GPIO不能在硬件IRQ处理例程中访问的事实,这些处理例程实际上和自旋锁安全的函数是一样的。


    * 除此之外 * 调用设置和配置此类GPIO的函数也必须在允许休眠的上下文中,因为它们可能也需要访问GPIO控制器芯片: (这些设置函数通常在板级启动代码或者驱动探测/断开代码中,所以这是一个容易满足的约束条件。)




      1. gpio_direction_input()


      1. gpio_direction_output()


      1. gpio_request()




      1. ## gpio_request_one()


      1. ## gpio_request_array()


      1. ## gpio_free_array()




      1. gpio_free()


      1. gpio_set_debounce()



    声明和释放GPIO


    为了有助于捕获系统配置错误,定义了两个函数。




      1. /* 申请 GPIO, 返回0或负的错误代码.


      1. * 非空标签可能有助于诊断.


      1. */


      1. int gpio_request(unsigned gpio, const char *label);




      1. / 释放之前声明的GPIO /


      1. void gpio_free(unsigned gpio);



    将 无效的GPIO编码传递给gpio_request()会导致失败,申请一个已使用这个函数声明过的GPIO也会失败。gpio_request()的返 回值必须检查。你应该在进程上下文中调用这些函数。然而,对于自旋锁安全的GPIO,在板子启动的早期、进入进程之前是可以申请的。


    这 个函数完成两个基本的目标。一是标识那些实际上已作为GPIO使用的信号线,这样便于更好地诊断;系统可能需要服务几百个潜在的GPIO,但是对于任何一 个给定的电路板通常只有一些被使用。另一个目的是捕获冲突,查明错误:如两个或更多驱动错误地认为他们已经独占了某个信号线,或是错误地认为移除一个管理 着某个已激活信号的驱动是安全的。也就是说,申请GPIO的作用类似一种锁机制。


    某些平台可能也使用GPIO作为电源管理(例如通过关闭未使用芯片区和简单地关闭未使用时钟)激活信号。


    注意:申请一个GPIO并没有以任何方式配置它,只不过标识那个GPIO处于使用状态。必须有另外的代码来处理引脚配置(如控制GPIO使用的引脚、上拉/下拉)。


    并且注意在释放GPIO前,你必须停止使用它。


    考虑到大多数情况下声明GPIO之后就会立即配置它们,所以定义了以下三个辅助函数:




      1. /* 申请一个GPIO信号, 同时通过特定的’flags’初始化配置,


      1. * 其他和gpio_request()的参数和返回值相同




      1. */


      1. int gpio_request_one(unsigned gpio, unsigned long flags, const char *label);




      1. /* 在单个函数中申请多个GPIO


      1. */


      1. int gpio_request_array(struct gpio *array, size_t num);




      1. /* 在单个函数中释放多个GPIO


      1. */


      1. void gpio_free_array(struct gpio *array, size_t num);



    这里 ‘flags’ 当前定义可指定以下属性:




      1. * GPIOF_DIR_IN - 配置方向为输入


      1. * GPIOF_DIR_OUT - 配置方向为输出




      1. * GPIOF_INIT_LOW - 在作为输出时,初始值为低电平


      1. * GPIOF_INIT_HIGH - 在作为输出时,初始值为高电平



    因为 GPIOF_INIT_* 仅有在配置为输出的时候才存在,所以有效的组合为:




      1. * GPIOF_IN - 配置为输入


      1. * GPIOF_OUT_INIT_LOW - 配置为输出,并初始化为低电平


      1. * GPIOF_OUT_INIT_HIGH - 配置为输出,并初始化为高电平



    将来这些标志可能扩展到支持更多的属性,比如开漏状态。


    更进一步,为了更简单地声明/释放多个GPIO,’struct gpio’被引进来封装所有这三个领域:




      1. struct gpio {


      1. unsigned gpio;


      1. unsigned long flags;


      1. const char *label;


      1. };



    一个典型的使用案例:







          1. static struct gpio leds_gpios[] = {

          1. { 32, GPIOF_OUT_INIT_HIGH, “Power LED” },     / default to ON /

          1. { 33, GPIOF_OUT_INIT_LOW, “Green LED” },     / default to OFF /

          1. { 34, GPIOF_OUT_INIT_LOW, “Red LED” }, / default to OFF /

          1. { 35, GPIOF_OUT_INIT_LOW, “Blue LED” },     / default to OFF /

          1. { ... },

          1. };

          1. err = gpio_request_one(31, GPIOF_IN, “Reset Button”);

          1. if (err)

          1. ...

          1. err = gpio_request_array(leds_gpios, ARRAY_SIZE(leds_gpios));

          1. if (err)

          1. ...

          1. gpio_free_array(leds_gpios, ARRAY_SIZE(leds_gpios));





    GPIO映射到IRQ


    GPIO编号是无符号整数;IRQ编号也是。这些构成了两个逻辑上不同的命名空间(GPIO 0不一定使用IRQ 0)。你可以通过以下函数在它们之间实现映射:







          1. / 映射GPIO编号到IRQ编号 /

          1. int gpio_to_irq(unsigned gpio);

          1. / 映射IRQ编号到GPIO编号 (尽量避免使用) /

          1. int irq_to_gpio(unsigned irq);





    它 们的返回值为对应命名空间的相关编号,或是负的错误代码(如果无法映射)。(例如,某些GPIO无法做为IRQ使用。)以下的编号错误是未经检测的:使用 一个未通过gpio_direction_input()配置为输入的GPIO编号,或者使用一个并非来源于gpio_to_irq()的IRQ编号。


    这两个映射函数可能会在信号编号的加减计算过程上花些时间。它们不可休眠。


    gpio_to_irq() 返回的非错误值可以传递给request_irq()或者free_irq()。它们通常通过板级特定的初始化代码存放到平台设备的IRQ资源中。注 意:IRQ触发选项是IRQ接口的一部分,比如IRQF_TRIGGER_FALLING,系统唤醒能力也是如此。


    irq_to_gpio()返回的非错误值大多数通常可以被gpio_get_value()所使用,比如在IRQ是沿触发时初始化或更新驱动状态。注意某些平台不支持反映射,所以你应该尽量避免使用它。


    模拟开漏信号


    有 时在只有低电平信号作为实际驱动结果(译者注:多个输出连接于一点,逻辑电平结果为所有输出的逻辑与)的时候,共享的信号线需要使用“开漏”信号。(该术 语适用于CMOS管;而TTL用“集电极开路”。)一个上拉电阻使信号为高电平。这有时被称为“线与”。实际上,从负逻辑(低电平为真)的角度来看,这是 一个“线或”。


    一个开漏信号的常见例子是共享的低电平使能IRQ信号线。此外,有时双向数据总线信号也使用漏极开路信号。


    某些 GPIO 控制器直接支持开漏输出,还有许多不支持。当你需要开漏信号但你的硬件又不直接支持的时候,一个常用的方法是用任何即可作输入也可作输出的GPIO引脚来模拟:


    LOW: gpio_direction_output(gpio, 0) … 这代码驱动信号并覆盖上拉配置


    HIGH: gpio_direction_input(gpio) … 这代码关闭输出,所以上拉电阻(或其他的一些器件)控制了信号。


    如 果你将信号线“驱动”为高电平,但是gpio_get_value(gpio)报告了一个低电平(在适当的上升时间后),你就可以知道是其他的一些组件将 共享信号线拉低了。这不一定是错误的。一个常见的例子就是I2C时钟的延长:一个需要较慢时钟的从设备延迟SCK的上升沿,而I2C主设备相应地调整其信 号传输速率。


    这些公约忽略了什么?


    这 些公约忽略的最大一件事就是引脚复用,因为这属于高度芯片特定的属性且没有可移植性。某个平台可能不需要明确的复用信息;有的对于任意给定的引脚可能只有 两个功能选项;有的可能每个引脚有八个功能选项;有的可能可以将几个引脚中的任何一个作为给定的GPIO。(是的,这些例子都来自于当前运行Linux的 系统。)


    在 某些系统中,与引脚复用相关的是配置和使能集成的上、下拉模式。并不是所有平台都支持这种模式,或者不会以相同的方式来支持这种模式;且任何给定的电路板 可能使用外置的上拉(或下拉)电阻,这时芯片上的就不应该使用。(当一个电路需要5kOhm的拉动电阻,芯片上的100 kOhm电阻就不能做到。)同样的,驱动能力(2 mA vs 20 mA)和电压(1.8V vs 3.3V)是平台特定问题,就像模型一样在可配置引脚和GPIO之间(没)有一一对应的关系。


    还 有其他一些系统特定的机制没有在这里指出,例如上述的输入去毛刺和线与输出选项。硬件可能支持批量读或写GPIO,但是那一般是配置相关的:对于处于同一 块区(bank)的GPIO。(GPIO通常以16或32个组成一个区块,一个给定的片上系统一般有几个这样的区块。)某些系统可以通过输出GPIO触发 IRQ,或者从并非以GPIO管理的引脚取值。这些机制的相关代码没有必要具有可移植性。


    当前,动态定义GPIO并不是标准的,例如作为配置一个带有某些GPIO扩展器的附加电路板的副作用。


    GPIO实现者的框架 (可选)


    前面提到了, 有一个可选的实现框架,让平台使用相同的编程接口,更加简单地支持不同种类的GPIO控制器。这个框架称为”gpiolib”。


    作为一个辅助调试功能,如果debugfs可用,就会有一个 /sys/kernel/debug/gpio 文件。通过这个框架,它可以列出所有注册的控制器,以及当前正在使用中的GPIO的状态。


    控制器驱动: gpio_chip


    在框架中每个 GPIO 控制器都包装为一个 “struct gpio_chip” ,他包含了该类型的每个控制器的常用信息:




      1. - 设置GPIO方向的方法


      1. - 用于访问GPIO值的方法


      1. - 告知调用其方法是否可能休眠的标志


      1. - 可选的debugfs信息导出方法 (显示类似上拉配置一样的额外状态)


      1. - 诊断标签



    也包含了来自device.platform_data的每个实例的数据:它第一个GPIO的编号和它可用的GPIO的数量。


    实现 gpio_chip 的代码应该支持多控制器实例,可能使用驱动模型。那些代码要配置每个 gpio_chip,并发起 gpiochip_add()。卸载一个 GPIO 控制器很少见,但在必要的时候可以使用gpiochip_remove()。


    大部分 gpio_chip 是一个实例特定结构体的一部分,而并不将GPIO接口单独暴露出来,比如编址、电源管理等。类似编解码器这样的芯片会有复杂的非GPIO状态。


    任何一个debugfs信息导出方法通常应该忽略还未申请作为GPIO的信号线。他们可以使用 gpiochip_is_requested()测试,当这个GPIO已经申请过了就返回相关的标签,否则返回NULL。


    平台支持


    为 了支持这个框架,一个平台的 Kconfig 文件将会 “select”(选择) ARCH_REQUIRE_GPIOLIB 或 ARCH_WANT_OPTIONAL_GPIOLIB ,并让它的 <asm/gpio.h> 包含 <asm-generic/gpio.h> ,并定义三个方法:gpio_get_value()、gpio_set_value()和gpio_cansleep()。


    它也应该提供一个ARCH_NR_GPIOS的定义值,这样可以更好地反映该平台GPIO的实际数量,节省静态表的空间。(这个定义值应该包含片上系统内建GPIO和GPIO扩展器中的数据。)




      1. ARCH_REQUIRE_GPIOLIB 意味着gpiolib核心在这个构架中将总是编译进内核。


      1. ARCH_WANT_OPTIONAL_GPIOLIB 意味着gpiolib核心默认关闭,且用户可以使能它,并将其编译进内核(可选)。



    如果这些选项都没被选择,该平台就不通过GPIO-lib支持GPIO,且代码不可以被用户使能。


    以下这些方法的实现可以直接使用框架代码,并总是通过gpio_chip调度:




      1. #define gpio_get_value __gpio_get_value


      1. #define gpio_set_value __gpio_set_value


      1. #define gpio_cansleep __gpio_cansleep



    这 些定义可以用更理想的实现方法替代,那就是使用经过逻辑优化的内联函数来访问基于特定片上系统的GPIO。例如,若引用GPIO的(寄存器地址)是常量 “12”,读取或设置它可能只需少则两或三个指令,且不会休眠。当这样的优化无法实现时,那些函数必须使用框架提供的代码,那就至少要几十条指令才可以实 现。对于用GPIO模拟的I/O接口, 如此精简指令是很有意义的。


    对 于片上系统,平台特定代码为片上GPIO每个区(bank)定义并注册gpio_chip实例。那些GPIO应该根据芯片厂商的文档进行编码/标签,并直 接和电路板原理图对应。他们应该开始于零并终止于平台特定的限制。这些GPIO(代码)通常从arch_initcall()或者更早的地方集成进平台初 始化代码,使这些GPIO总是可用,且他们通常可以作为IRQ使用。


    板级支持


    对 于外部 GPIO 控制器(例如I2C或SPI扩展器、专用芯片、多功能器件、FPGA或CPLD),大多数常用板级特定代码都可以注册控制器设备,并保证他们的驱动知道 gpiochip_add()所使用的GPIO编号。他们的起始编号通常跟在平台特定的GPIO编号之后。


    例如板级启动代码应该创建结构体指明芯片公开的GPIO范围,并使用platform_data将其传递给每个GPIO扩展器芯片。然后芯片驱动中的probe()例程可以将这个数据传递给gpiochip_add()。


    初 始化顺序很重要。例如,如果一个设备依赖基于I2C的(扩展)GPIO,那么它的probe()例程就应该在那个GPIO有效以后才可以被调用。这意味着 设备应该在GPIO可以工作之后才可被注册。解决这类依赖的的一种方法是让这种gpio_chip控制器向板级特定代码提供setup()和 teardown()回调函数。一旦所有必须的资源可用之后,这些板级特定的回调函数将会注册设备,并可以在这些GPIO控制器设备变成无效时移除它们。


    用户空间的Sysfs接口(可选)


    使用”gpiolib”实现框架的平台可以选择配置一个GPIO的sysfs用户接口。这不同于debugfs接口,因为它提供的是对GPIO方向和值的控制,而不只显示一个GPIO的状态摘要。此外,它可以出现在没有调试支持的产品级系统中。


    例 如,通过适当的系统硬件文档,用户空间可以知道GIOP #23控制Flash存储器的写保护(用于保护其中Bootloader分区)。产品的系统升级可能需要临时解除这个保护:首先导入一个GPIO,改变其 输出状态,然后在重新使能写保护前升级代码。通常情况下,GPIO #23是不会被触及的,并且内核也不需要知道他。


    根据适当的硬件文档,某些系统的用户空间GPIO可以用于确定系统配置数据,这些数据是标准内核不知道的。在某些任务中,简单的用户空间GPIO驱动可能是系统真正需要的。


    注意:标准内核驱动中已经存在通用的“LED和按键”GPIO任务,分别是:”leds-gpio” 和 “gpio_keys”。请使用这些来替代直接访问GPIO,因为集成在内核框架中的这类驱动比你在用户空间的代码更好。


    Sysfs中的路径


    在/sys/class/gpio中有3类入口:




      1. - 用于在用户空间控制GPIO的控制接口;


      1. - GPIOs 本身;以及


      1. - GPIO 控制器 (“gpio_chip” 实例)。



    除了这些标准的文件,还包含“device”符号链接。


    控制接口是只写的:




      1. /sys/class/gpio/


      1. “export” … 用户空间可以通过写其编号到这个文件,要求内核导出一个GPIO的控制到用户空间。


      1. 例如: 如果内核代码没有申请GPIO #19,”echo 19 > export” 将会为GPIO #19创建一个 “gpio19” 节点。


      1. “unexport” … 导出到用户空间的逆操作。


      1. 例如: “echo 19 > unexport” 将会移除使用”export”文件导出的 “gpio19” 节点。



    GPIO信号的路径类似 /sys/class/gpio/gpio42/ (对于 GPIO #42来说),并有如下的读/写属性:




      1. /sys/class/gpio/gpioN/


      1. “direction” … 读取得到 “in” 或 “out”。这个值通常运行写入。写入”out” 时,其引脚的默认输出为低电平。为了确保无故障运行,”low” 或 “high” 的电平值应该写入GPIO的配置,作为初始输出值。


      1. 注意:如果内核不支持改变GPIO的方向,或者在导出时内核代码没有明确允许用户空间可以重新配置GPIO方向,那么这个熟悉将不存在。


      1. “value” … 读取得到 0 (低电平) 或 1 (高电平)。如果GPIO配置为输出,这个值允许写操作。任何非零值都以高电平看待。


      1. 如果引脚可以配置为中断信号,且如果已经配置了产生中断的模式(见”edge”的描述),你可以对这个文件使用轮询操作(poll(2)),且轮询操作会 在任何中断触发时返回。如果你使用轮询操作(poll(2)),请在events中设置POLLPRI 和 POLLERR。如果你使用轮询操作(select(2)),请在exceptfds设置你期望的文件描述符。在轮询操作(poll(2))返回之后,既 可以通过lseek(2)操作读取sysfs文件的开始部分,也可以关闭这个文件并重新打开它来读取数据。


      1. “edge” … 读取得到 “none”、”rising”、”falling”或者”both”。将这些字符串写入这个文件可以选择沿触发模式,会使得轮询操作(select(2))在”value”文件中返回。


      1. 这个文件仅有在这个引脚可以配置为可产生中断输入引脚时,才存在。


      1. “active_low” … 读取得到 0 (假) 或 1 (真)。 写入任何非零值可以翻转这个属性的(读写)值。已存在或之后通过”edge”属性设置了”rising” 和 “falling” 沿触发模式的轮询操作(poll(2))将会遵循这个设置。



    GPIO 控制器的路径类似 /sys/class/gpio/gpiochip42/ (对于从#42 GPIO开始实现控制的控制器),并有着以下只读属性:




      1. /sys/class/gpio/gpiochipN/


      1. “base” … 与以上的N相同,代表此芯片管理的第一个GPIO的编号


      1. “label” … 用于诊断 (并不总是只有唯一值)


      1. “ngpio” … 此控制器所管理的GPIO数量(而GPIO编号从 N 到 N + ngpio - 1)



    大 多数情况下,电路板的文档应当标明每个GPIO的使用目的。但是那些编号并不总是固定的,例如在扩展卡上的GPIO会根据所使用的主板或所在堆叠架构中其 他的板子而有所不同。在这种情况下,你可能需要使用gpiochip节点(尽可能地结合电路图)来确定给定信号所用的GPIO编号。


    从内核代码中导出


    内核代码可以明确地管理那些已通过gpio_request()申请的GPIO的导出:




      1. / 导出GPIO到用户空间 /


      1. int gpio_export(unsigned gpio, bool direction_may_change);




      1. / gpio_export()的逆操作 /


      1. void gpio_unexport();




      1. / 创建一个sysfs连接到已导出的GPIO节点 /


      1. int gpio_export_link(struct device dev, const char name,


      1. unsigned gpio)




      1. / 改变sysfs中的一个GPIO节点的极性 /


      1. int gpio_sysfs_set_active_low(unsigned gpio, int value);



    在一个内核驱动申请一个GPIO之后, 它可以通过gpio_export()使其在sysfs接口中可见。该驱动可以控制信号方向是否可修改。这有助于防止用户空间代码无意间破坏重要的系统状态。


    这个明确的导出有助于(通过使某些实验更容易来)调试,也可以提供一个始终存在的接口,与文档配合作为一个板级支持包的一部分。


    在 GPIO被导出之后,gpio_export_link()允许在sysfs文件系统的任何地方创建一个到这个GPIO sysfs节点的符号链接。这样驱动就可以通过一个描述性的名字,在sysfs中他们所拥有的设备下提供一个(到这个GPIO sysfs节点的)接口。


    驱 动可以使用 gpio_sysfs_set_active_low() 来在用户空间隐藏电路板之间 GPIO 线的极性差异。这个仅对sysfs接口起作用。极性的改变可以在gpio_export()前后进行,且之前使能的轮询操作(poll(2))支持(上升 或下降沿)将会被重新配置来遵循这个设置。



    ============================================================================================ 

    标识端口 

    ————- 

    gpio使用0~MAX_INT之间的整数标识,不能使用负数。 

    使用以下函数检查一个端口号的合法性: 

    int gpio_is_valid(int number);

    使用gpio 

    ————- 


    使用io的第一步是分配端口,使用 gpio_request()。 


    接下来要做的是标记它的方向。 


    /设为输入或者输出,成功返回零或者失败返回负的错误值


     int gpio_direction_input(unsigned gpio); 


     int gpio_direction_output(unsigned gpio, int value); 


    通常应该检查它们的返回值。通常应该假定这两个接口会在线程上下文中调用。但是 


    对于自旋锁安全的gpio也可以在线程出现之前的早期初始化之间使用。 


    对于输出的gpio,提供的值作为输出的初始值。



    使用自旋锁安全的gpio 

    ———————————- 


    大部分gpio控制器可以使用内存读写指令来访问,不需要睡眠,可以在中断上下文访问。 


    对于gpio_cansleep返回假的gpio可以使用下面的接口访问: 


    /读取输入,返回零或非零


    int gpio_get_value(unsigned gpio); 


    /输出


    void gpio_set_value(unsigned gpio, int value); 


    返回的值是布尔值,零代表低,非零代表高电平。当读取输出引脚的值,返回值应该是引脚上的实际状态, 


    这个值不一定等于配置的输出值,因为从设定信号到信号稳定需要一定时间。 


    不是所有的平台可以读取输出的引脚值,这些平台不能总是返回零。用这两这两个函数访问 


    不能非在睡眠的上下文中安全访问的gpio将是一个错误。

    可被休眠上下文中访问的GPIO 

    ———————————————- 


    一些gpio控制器必须使用基于信息的总线,比如i2c和spi.读写gpio值的命令需要在队列中等待。 


    操作这些gpio可能会睡眠,不能在中断上下文中调用。 


    支持这种gpio的平台为了通过在这个函数中返回非零来区分其它 


    类型的gpio(需要一个已经被gpio_request申请的gpio号): 


    int gpio_cansleep(unsigned gpio); 


    为了访问这些端口,定义了另一组函数接口: 


    /输入端口:返回零或非零,可能睡眠


    int gpio_get_value_cansleep(unsigned gpio); 


    /输出端口:可能睡眠


    void gpio_set_value_cansleep(unsigned gpio, int value); 


    只能在允许睡眠的上下文中访问这些端口,比如线程化的中断中, 


    必须使用这些接口而不是没有cansleep前缀的自旋锁安全接口。 






    除了这些接口可能睡眠这个事实之外,它们操作那些不能在中断处理函数中访问的端口,这些调用的表现和 


    自旋锁安全的调用表现一致。 


    另外:调用安装和配置这样的gpio必须是在可睡眠的上下文中,因为他们可能需要访问gpio控制器。 


            gpio_direction_input() 


            gpio_direction_output() 


            gpio_request() 



    ##      gpio_request_one() 


    ##      gpio_request_array() 


    ##      gpio_free_array() 






              gpio_free() 


             gpio_set_debounce()
    申请和释放gpio 

    ———————— 


    为了获取系统配置错误,定义了两个调用: 


    /*申请gpio,返回0或负的错误值 


    * 非空的lables指针有助于诊断*/ 


    int gpio_request(unsigned gpio, const char *label); 


    /释放之前申请的gpio


    void gpio_free(unsigned gpio); 


    应该假设这两个函数实在进程上下文中调用的,但对于自旋锁安全的gpio也可从 


    在进程建立之前的早期启动过程中调用。 






    考虑到大多数情况下gpio会在申请过后立即需要被配置,下面三个接口负责这些工作: 


    /申请一个单独的gpio,使用“flag”作为初始的配置参数


    int gpio_request_one(unsigned gpio, unsigned long flags, const char *label); 


    /在一个调用中申请多个gpio


    int gpio_request_array(struct gpio *array, size_t num); 


    /释放多个端口


    void gpio_free_array(struct gpio *array, size_t num); 






    “falg”用来配置下面的特性: 


      


             * GPIOF_DIR_IN          - 配置方向为输入 


             * GPIOF_DIR_OUT         -配置方向为输出 


      


             * GPIOF_INIT_LOW        - 做输出引脚,输出低电平 


             * GPIOF_INIT_HIGH       - 做输出引脚,输出高电平 






    为了同时处理多个gpio,定义了一个专门结构体: 


             struct gpio { 


                     unsigned        gpio; 


                     unsigned long   flags; 


                     const char      *label; 


             }; 






    典型的使用如下: 


     327        static struct gpio leds_gpios[] = { 


     328                { 32, GPIOF_OUT_INIT_HIGH, “Power LED” }, /* default to ON */ 


     329                { 33, GPIOF_OUT_INIT_LOW,  “Green LED” }, /* default to OFF */ 


     330                { 34, GPIOF_OUT_INIT_LOW,  “Red LED”   }, /* default to OFF */ 


     331                { 35, GPIOF_OUT_INIT_LOW,  “Blue LED”  }, /* default to OFF */ 


     332                { … }, 


     333        }; 


     334 


     335        err = gpio_request_one(31, GPIOF_IN, “Reset Button”); 


     336        if (err) 


     337                … 


     338 


     339        err = gpio_request_array(leds_gpios, ARRAY_SIZE(leds_gpios)); 


     340        if (err) 


     341                … 


     342 


     343        gpio_free_array(leds_gpios, ARRAY_SIZE(leds_gpios)); 


       




      • GPIO映射到IRQ 



    —————————- 


     352        /* map GPIO numbers to IRQ numbers */ 


     353        int gpio_to_irq(unsigned gpio); 


     354 


     355        /* map IRQ numbers to GPIO numbers (avoid using this) */ 


     356        int irq_to_gpio(unsigned irq); 


    返回值是对应的编号或者是负的错误码。使用未用gpio_direction_input()安装的gpio编号或者不是从 


    gpio_to_irq()得到的中断号是不会被gpio检查的错误。 






    gpio_to_irq()返回的中断编号可以传给request_irq()和free_irq()。 


    irq_to_gpio()返回的gpio编号通常用来调用gpio_get_value(),比如在沿触发的中断中获取引脚的状态。 


    有些平台不支持这种映射,应该避免调用映射函数。 






    模拟漏极开路 


    —————— 


    gpio可能支持也可能不支持漏极开路输出。如果硬件不支持,可以模拟漏极开路输出的输入或者输出: 


    LOW:    gpio_direction_output(gpio, 0)  忽略上拉电阻。 


    HIGH:    gpio_direction_input(gpio) 关闭输出,上拉电阻控制信号。 


    如果驱动输出高电平但是gpio_get_value(gpio)报告的电平是低电平(经过一定延迟之后),可以断定 


    其它部分正在信号线上输出低电平。 






    GPIO 的框架(可选) 


    =============== 


    GPIO的框架在gpiolib中实现。 


    如果使能了 debugfs,系统下会出现 /sys/kernel/debug/gpio这个文件。这个文件内 


    将会列出所有注册在框架内的控制器和当前使用的gpio的状态。 


    在gpio框架中使用”struct gpio_chip”表示每个控制器的所有信息: 


    –设置gpio方向的方法 


    –访问gpio状态的方法 


    –表示是否会睡眠的标志 


    –可选的debugfs方法 


    –诊断用的标签lable(char指针) 


    当然还有每个实例的独有数据(可能来自dev.platform_data);第一个gpio的编号和管理的gpio数量。 






    gpiolib支持多个gpio控制器的实例,使用gpiochip_add()来注册,使用gpiochip_remove()注销。 


    gpio_chip的debugfs方法将会忽略未被申请的gpio。使用gpiochip_is_requested()将会返回NULL(未被申请)或者 


    lable(已被申请) 






    平台支持 


    ———— 


    为了支持gpio框架,必须选择Kconfig中的选项:ARCH_REQUIRE_GPIOLIB 或者 ARCH_WANT_OPTIONAL_GPIOLIB。并且 


    在 <asm/gpio.h>包含<asm-generic/gpio.h>,还要定义三个函数: gpio_get_value(), gpio_set_value(), and gpio_cansleep(). 


    同时也可以提供ARCH_NR_GPIOS的定义,以反映平台支持的gpio数量。 






    ARCH_REQUIRE_GPIOLIB表示gpiolib代码总是会编译进内核中。 


    ARCH_WANT_OPTIONAL_GPIOLIB表示gpiolib默认是关闭的,但是用户可以使能它并把它编译进内核。 


    如果这两个选项一个都未被选中,gpiolib则不能被用户使能。 






    通常那三个函数接口可以直接使用框架内的代码(通过gpio_chip来调用): 


                 #define gpio_get_value        __gpio_get_value 


                 #define gpio_set_value        __gpio_set_value 


                 #define gpio_cansleep         __gpio_cansleep 










    用户空间的sysfs接口(可选) 


    ================== 






    在sysfs下的路径 


    ——————— 


    在/sys/class/gpio下有三种类型的入口: 


                –用户空间控制GPIO的接口(第一类) 


                –对应具体GPIO 


                –GPIO控制器(”gpio_chip”的实例)(第三类) 


    以上接口不包括标准“device”文件和它们的链接。 






    控制接口(第一类)是只写的: 


                /sys/class/gpio/ 


                            “export” 用户空间通过写入gpio的编号来向内核申请将某个gpio的控制权导出到用户空间 


                                        比如“echo 19 > export ”将会为19号gpio创建一个节点“gpio19”,前提是没有内和代码申请了这个端口。 


                            “unexport” 和导出的效果相反。 


                                        比如“echo 19 > unexport”将会移除“gpio19”这个节点。 


    GPIO管脚的路径类似于/sys/class/gpio/gpio42/ (for GPIO #42)的形式,并且有以下可读写的属性: 


                /sys/class/gpio/gpioN/   (第二类) 


                            “direction” 读取结果是“in”或者“out”。也可以往其中写入。写入“out”默认将引脚值初始化为低。 


                                        写入“low”或“high”可以初始化作为输出时的初始值。 


                                        如果内核不支持或者内核代码不愿意,将不会存在这个属性。 


                            “value” 读取结果是0(低电平)或1(高电平)。如果GPIO被配置为输出,这个值是可写的, 


                                        任何非零的值都将输出高电平。 


                                        如果某个引脚能并且已经被配置为产生中断,可以在它的节点上调用poll(2)并且poll(2)将在中断触发后返回。 


                                        如果使用poll(2),设置事件类型为POLLPRI和POLLERR。如果使用sellect(2),将文件描述符加入exceptfds。 


                                        在poll(2)返回后,可以使用lseek(2)移动到文件开头读取新的值或者关闭它再重新打开读取新值。 


                            “edge”   读取改节点将会得到以下值: “none”, “rising”, “falling”,或者  “both”。写如这些字符串到这个节点中将会选择 


                                        唤醒在“value”上的poll(2)的信号沿。 


                                        这个文件节点只有在引脚能被配置为输入中断引脚的时候才存在。 


                            “active_low” 读取值是0(假)或者1(真)。写入任何非零的值都将反转“value”中读取和写入的值。 


                                        已经在使用和之后使用poll(2)的“edge”节点的“rasing”和“falling”值将会受”active_low” 的影响。 


                                        (自己的理解:这个值实际是将高变为0,低变为1。也就是实现了逻辑反转。) 






    GPIO控制器的路径类似于/sys/class/gpio/gpiochip42/(这个控制器的最小端口是42)并且有以下的读写属性: 


                /sys/class/gpio/gpiochipN/ 


                            “base” 和N相同,也就是控制器管理的最小的端口编号。 


                            “lable”  诊断使用的标志(并不总是唯一的) 


                            “ngpio” 控制器管理的端口数量(端口范围是:N ~ N+ngpio-1)




    从内核空间中导出 


    ———————— 


    内核可以对已经被 
    gpio_request()申请的gpio的导出进行明确的管理。 


              /* 导出GPIO到用户空间的sysfs,当允许用户空间修改gpio的方向,第二个参数是真 */ 


               
    int gpio_export(unsigned gpio, bool direction_may_change); 


              /* 撤销GPIO的导出 */ 


               
    void gpio_unexport(); 


              /* 创建到导出GPIO的 
    sysfs link 
    ,第一个参数是在哪个dev下创建,第二个参数名字,第三个是gpio编号 */ 


               
    int gpio_export_link(struct device *dev, const char *name, unsigned gpio) 


              /* 改变sysfs中GPIO节点的极性,也就是将高低电平的逻辑反转。可以在export之前和之后进行。之前设置的 
    enabled poll(2)也会相应改变   
    */ 


               
    int gpio_sysfs_set_active_low(unsigned gpio, int value); 







    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 


    以上内容是内核文档的内容,下面分析gpio_chip的内容,这是一个控制器。 



      48/** 


      49 * struct gpio_chip - 一个 GPIO 控制器的抽象 


      50 * @label: 诊断用的标签(名字) 


      51 * @dev: 可选的设备结构体 


      52 * @owner: 拥有者模块指针 


      53 * @request: 可选的激活回调函数,就像电源管理的接口一样。可以睡眠 


      55 * @free: 可选的释放回调函数,可以睡眠。 


      57 * @direction_input: 配置某个偏移对应的端口为输入引脚,可能返回错误。 


      58 * @get: 返回相应偏移引脚的值; 如果是输出引脚则返回输出引脚的状态,或者返回零。 


      60 * @direction_output: 配置输出引脚的输出值,或者返回错误 


      61 * @set: 设置指定引脚的输出值。 


      62 * @to_irq: 可选的回调函数,支持非静态的 gpio_to_irq()映射, 不可以睡眠 ; 


      64 * @dbg_show: 可选的 debugfs接口; 如果不设置,内核会使用默认函数赋给这个指针。 


      67 * @base: gpiogpio_chip所管理的第一个gpio的编号;如果注册期间是负值,将会动态申请一个值。 


      69 * @ngpio: GPIO控制器管理的IO数目,最后一个IO的号码是  (base + ngpio - 1). 


      71 * @can_sleep: 如果get()/set() 方法会睡眠这个标志必须被设置 


      73 * @names:如果设置了这个成员,它必须是大小为 ngpio的字符指针数组,每个成员可以指向相应引脚的特殊的名字,也可以为空。 


                             name影响的是exportGPIO到sysfs的名字,如果不设置的话名字就是“gpioN”(N是IO对应的编号)。 


                             当然这个成员可以不初始化。 






    gpio_chip使用offset(0~(ngpio - 1))来分辨不同的引脚。 


      */ 







    struct gpio_chip { 


      91        const char              *label; 


      92        struct device           *dev; 


      93        struct module           *owner; 


      94 


      95        int                     (*request)(struct gpio_chip *chip, 


      96                                                unsigned offset); 


      97        void                    (*free)(struct gpio_chip *chip, 


      98                                                unsigned offset); 


      99 


     100        int                     (*direction_input)(struct gpio_chip *chip, 


     101                                                unsigned offset); 


     102        int                     (*get)(struct gpio_chip *chip, 


     103                                                unsigned offset); 


     104        int                     (*direction_output)(struct gpio_chip *chip, 


     105                                                unsigned offset, int value); 


     106        int                     (*set_debounce)(struct gpio_chip *chip, 


     107                                                unsigned offset, unsigned debounce); 


     108 


     109        void                    (*set)(struct gpio_chip *chip, 


     110                                                unsigned offset, int value); 


     111 


     112        int                     (*to_irq)(struct gpio_chip *chip, 


     113                                                unsigned offset); 


     114 


     115        void                    (*dbg_show)(struct seq_file *s, 


     116                                                struct gpio_chip *chip); 


     117        int                     base; 


     118        u16                     ngpio; 


     119        const char              *const *names; 


     120        unsigned                can_sleep:1; 


     121        unsigned                exported:1; 


     122 


     123#if defined(CONFIG_OF_GPIO) 


     124        /* 


     125         * If CONFIG_OF is enabled, then all GPIO controllers described in the 


     126         * device tree automatically may have an OF translation 


     127         */ 


     128        struct device_node *of_node; 


     129        int of_gpio_n_cells; 


     130        int (*of_xlate)(struct gpio_chip *gc, struct device_node *np, 


     131                        const void *gpio_spec, u32 *flags); 


     132#endif 


     133}; 






    gpio_chip的接口: 


    extern const char *gpiochip_is_requested(struct gpio_chip *chip, unsigned offset); 如果offet对应的gpio已经被申请了则返回NULL,否则返回lable指针或者“??” 







    extern int __must_check gpiochip_reserve(int start, int ngpio); 


    将start开始的ngpio个编号保留 


    这个函数是在全局范围内预留一段编号,将来供某个gpio_chip使用。 

    gpio_request


    一般gpio_request封装了mem_request(),起保护作用,最后要调用mem_free之类的。主要是告诉内核这地址被占用了。当其它地方调用同一地址的gpio_request就会报告错误,该地址已被申请。在/proc/mem应该会有地址占用表描述。
    这种用法的保护作用前提是大家都遵守先申请再访问,有一个地方没遵守这个规则,这功能就失效了。好比进程互斥,必需大家在访问临界资源的时候都得先获取锁一样,其中一个没遵守约定,代码就废了。



     


    其原型为 int gpio_request(unsigned gpio, const char *label)先说说其参数,gpio则为你要申请的哪一个管脚,label则是为其取一个名字。


    其具体实现如下:


    int gpio_request(unsigned gpio, const char *label)
    {
     struct gpio_desc *desc;//这个自己看源码
     struct gpio_chip *chip;//这个自己看源码
     int   status = -EINVAL;
     unsigned long  flags;


     spin_lock_irqsave(&gpio_lock, flags);//屏蔽中断


     if (!gpio_is_valid(gpio))//判断是否有效,也就是参数的取值范围判断
      goto done;
     desc = &gpio_desc[gpio];//这个是关键gpio_desc为定义的一个全局的数组变量,这个函数的实值也就是,用gpio_desc里面的一个变量来表示数组中的这个元素


                                                  已经被申请了,而这个变量就是下面会看到的desc->flags。
     chip = desc->chip;按理说这个这个全局的gpio_desc如果没有初始化的话,这个chip就为空了,随后就直接返回-EINVAL了。
     if (chip == NULL)如果不为空继续往下走
      goto done;


     if (!try_module_get(chip->owner))
      goto done;


     /* NOTE:  gpio_request() can be called in early boot,
      * before IRQs are enabled, for non-sleeping (SOC) GPIOs.
      */


     if (test_and_set_bit(FLAG_REQUESTED, &desc->flags) == 0) {这里测试并设置flags的第FLAG_REQUESTED位,如果没有被申请就返回该位的原值0,分析到这儿,


                                                                                                                     也差不多满足了我的个人要求。
      desc_set_label(desc, label ? : “?”);
      status = 0;
     } else {
      status = -EBUSY;
      module_put(chip->owner);
      goto done;
     }


     if (chip->request) {
      /* chip->request may sleep */
      spin_unlock_irqrestore(&gpio_lock, flags);
      status = chip->request(chip, gpio - chip->base);
      spin_lock_irqsave(&gpio_lock, flags);


      if (status < 0) {
       desc_set_label(desc, NULL);
       module_put(chip->owner);
       clear_bit(FLAG_REQUESTED, &desc->flags);
      }
     }

    done: 

     if (status) 

      pr_debug(“gpio_request: gpio-%d (%s) status %d ”, 

       gpio, label ? : “?”, status); 

     spin_unlock_irqrestore(&gpio_lock, flags); 

     return status; 
  • 相关阅读:
    【Nginx】ngx_event_core_module模块
    ELMAH--Using HTTP Modules and Handlers to Create Pluggable ASP.NET Components 77 out of 90 rated th
    nyist oj 214 单调递增子序列(二) (动态规划经典)
    java 入门书籍(java7)
    ARCGIS将WGS84坐标投影到高斯平面
    【linux】linux下对java程序生成dump文件,并使用IBM Heap Analyzer进行分析,查找定位内存泄漏的问题代码
    【springboot】【socket】spring boot整合socket,实现服务器端两种消息推送
    【linux】linux修改open file 大小
    【docker】docker限制日志文件大小的方法+查看日志文件的方法
    【docker】docker部署spring boot服务,但是docker logs查看容器输出控制台日志,没有日志打印,日志未打印,docker logs不打印容器日志
  • 原文地址:https://www.cnblogs.com/isAndyWu/p/10388581.html
Copyright © 2011-2022 走看看