树莓派高级教程(三)

原文:Advanced Raspberry Pi

协议:CC BY-NC-SA 4.0

十一、GPIO 硬件

通用 I/O 是 Raspberry Pi 用户最关心的话题,因为这是与外界的接口。Pi 设计灵活,允许在软件控制下重新配置 I/O 引脚。例如,GPIO 14 可以是输入、输出,或者作为串行端口 TX 数据线工作。

与 Pi 的 GPIO 接口相关的挑战之一是它使用弱 CMOS 3.3 V 接口。GPIO 引脚也容易受到 ESD(静电放电)损坏,并且是弱驱动(2 至 16 mA)。最后,有限的 GPIO 功率必须从总备用电流容量(原始 Pi 型号 B 上为 50 mA)中进行预算。使用适配器板克服了这些问题,但会大大增加成本。因此,这为提出廉价有效的自行解决方案提供了肥沃的土壤。

引脚和名称

表 11-1 和 11-2 展示了现代 Raspberry Pi 模型的 GPIO 连接。表 11-1 列出了奇数引脚,而表 11-2 提供了 20x2 头带上的偶数引脚。

表 11-2

现代 Raspberry Pi 的偶数 GPIO 引脚

|

别针

|

通用输入输出接口

|

名字

|

描述

|
| — | — | — | — |
| P1-02 |   |   | +5 V 电源 |
| P1-04 |   |   | +5 V 电源 |
| P1-06 |   |   | 地面 |
| P1-08 | GPIO-14 | TXD0 | UART 发送 |
| P1-10 | GPIO-15 | RXD0 | UART 接收 |
| P1-12 | GPIO-18 | GPIO_GEN1 |   |
| P1-14 |   |   | 地面 |
| P1-16 | GPIO-23 | GPIO_GEN4 |   |
| P1-18 | GPIO-24 | GPIO-GEN5 |   |
| P1-20 |   |   | 地面 |
| P1-22 | GPIO-25 | GPIO-GEN6 |   |
| P1-24 | GPIO-8 | SPI_CE0_N |   |
| P1-26 | GPIO-7 | SPI_CE1_N |   |
| P1-28 |   | S7-1200 可编程控制器 | I2C ID EEPROM 时钟 |
| P1-30 |   |   | 地面 |
| P1-32 | GPIO-12 |   |   |
| P1-34 |   |   | 地面 |
| P1-36 | GPIO-16 |   |   |
| P1-38 | GPIO-20 |   |   |
| P1-40 | GPIO-21 |   |   |

表 11-1

现代 Raspberry Pi 的奇数 GPIO 引脚

|

别针

|

通用输入输出接口

|

名字

|

描述

|
| — | — | — | — |
| P1-01 |   |   | 来自稳压器的+3.3 V 电源 |
| P1-03 | GPIO-2 |   | I2C SDA1(带 1 个*)。*8kω上拉电阻) |
| P1-05 | GPIO-3 |   | I2C SCL1(带 1 *)。*8kω上拉电阻) |
| P1-07 | GPIO-4 | GPIO_GCLK(通用串行总线) | 通用时钟输出或单线 |
| P1-09 |   |   | 地面 |
| P1-11 | GPIO-17 | GPIO_GEN0 |   |
| P1-13 | GPIO-27 | GPIO_GEN2 |   |
| P1-15 | GPIO-22 | GPIO_GEN3 |   |
| P1-17 |   |   | 来自稳压器的+3.3 V 电源 |
| P1-19 | GPIO-10 | SPI_MOSI |   |
| P1-21 | GPIO-9 | SPI_MISO |   |
| P1-23 | GPIO-11 | SPI_CLK 函数 |   |
| P1-25 |   |   | 地面 |
| P1-27 |   | ID_SD | I2C ID EEPROM 数据 |
| P1-29 | GPIO-5 |   |   |
| P1-31 | GPIO-6 |   |   |
| P1-33 | GPIO-13 |   |   |
| P1-35 | GPIO-19 |   |   |
| P1-37 | GPIO-26 |   |   |
| P1-39 |   |   | 地面 |

GPIO 交叉引用

通常你可能知道你想要的 GPIO,但是找到管脚号需要一点搜索。表 11-3 是一个方便的交叉引用,按 GPIO 号排序,并列出了相应的管脚号。

表 11-3

GPIO 交叉引用

|

通用输入输出接口

|

别针

|

通用输入输出接口

|

别针

|

通用输入输出接口

|

别针

|

通用输入输出接口

|

别针

|
| — | — | — | — | — | — | — | — |
| GPIO-2 | P1-03 | GPIO-9 | P1-21 | GPIO-16 | P1-36 | GPIO-23 | P1-16 |
| GPIO-3 | P1-05 | GPIO-10 | P1-19 | GPIO-17 | P1-11 | GPIO-24 | P1-18 |
| GPIO-4 | P1-07 | GPIO-11 | P1-23 | GPIO-18 | P1-12 | GPIO-25 | P1-22 |
| GPIO-5 | P1-29 | GPIO-12 | P1-32 | GPIO-19 | P1-35 | GPIO-26 | P1-37 |
| GPIO-6 | P1-31 | GPIO-13 | P1-33 | GPIO-20 | P1-38 | GPIO-27 | P1-13 |
| GPIO-7 | P1-26 | GPIO-14 | P1-08 | GPIO-21 | P1-40 |   |   |
| GPIO-8 | P1-24 | GPIO-15 | P1-10 | GPIO-22 | P1-15 | |   |

复位后的配置

复位时,大多数 GPIO 引脚被配置为通用输入,但有一些例外。然而,随着 Raspbian Linux 的变化和新的 Pi 模型的引入,可能不存在可以安全假设的引导 GPIO 状态。如果您使用 GPIO,那么应该在使用前进行配置。

上拉电阻

如前所述,GPIO 2 和 3 (I2C 引脚)有一个连接到+3.3 V 供电轨的外部电阻,以满足 I2C 要求。其余 GPIO 引脚由 SoC 中的内部 50kω电阻拉高或拉低。内部上拉电阻很弱,只能有效地为未连接的 GPIO 输入提供定义的状态。CMOS(互补金属氧化物半导体)输入不应在其逻辑高电平和低电平之间浮动。当外部电路需要上拉电阻时,最好提供外部上拉电阻,而不是依靠内部弱电阻。

配置上拉电阻

GPIO 引脚的上拉配置可以使用 SoC 寄存器GPPUPGPPUDCLK0/1在 C 程序中进行配置。Pi GPPUP 寄存器的布局如表 11-4 所示。

表 11-4

Raspberry Pi GPPUP 寄存器

|

|

|

描述

|

类型

|

重置

|
| — | — | — | — | — |
| 31-2 | - | 未使用的 GPIO 引脚上拉/下拉 | 稀有 | Zero |
| 1-0 | PUD | 00 关—禁用上拉/下拉 01 下拉使能 10 上拉使能 11 保留 | 拆装 | Zero |

GPPUDCLK0寄存器布局的布局如表 11-5 所示。

表 11-5

GPPUDCLK0 寄存器布局

|

|

|

描述

|

类型

|

重置

|
| — | — | — | — | — |
| 31-0 | PUDCLKn | n = 0…31 | 拆装 | Zero |
| Zero | 没有影响 |   |   |
| one | 断言时钟 |

最后,GPPUDCLK1寄存器布局如表 11-6 所示。

表 11-6

GPPUDCLK1 寄存器布局

|

|

|

描述

|

类型

|

重置

|
| — | — | — | — | — |
| 31-22 | - | 内向的; 寡言少语的; 矜持的 | 稀有 | Zero |
| 21-0 | PUDCLKn | n = 32…53 | 拆装 | Zero |
| Zero | 没有影响 |
| one | 断言时钟 |

Broadcom 文档描述了上拉电阻编程的一般程序,如下所示:

  1. 将所需的上拉配置写入 32 位GPPUP寄存器最右边的 2 位。配置选项如下:

    00:禁用上拉控制。

    01:启用下拉控制。

    10:使能上拉控制。

  2. 等待 150 个周期,以便记录之前的写操作。

  3. 向正在配置的 32 个 GPIO 引脚组中的每个 GPIO 位置写入 1 位。

    gpio 0–31 由寄存器GPPUDCLK0配置。

  4. 再等待 150 个周期,让步骤 3 注册。

  5. 00写入GPPUP以移除控制信号。

  6. 再等待 150 个周期,让步骤 5 注册。

  7. 最后,写入GPPUDCLK0/1删除时钟。

由于单词时钟,Broadcom 的程序可能看起来很混乱。使用前述程序写入GPPUPGPPUDCLK0/1寄存器旨在向内部上拉电阻触发器(其数据时钟输入)提供一个脉冲。首先在步骤 1 中建立状态,然后在步骤 3 中将配置的 1 位变为高电平(针对选定的 GPIO 引脚)。第 5 步建立零状态,然后在第 7 步发送到触发器时钟输入。

文档还指出,无法读取上拉驱动器的当前设置(没有寄存器访问权限可用于读取这些触发器)。当您考虑到状态由这些被过程改变的内部触发器保持时,这是有意义的。幸运的是,在配置特定 GPIO 引脚的状态时,您只需更改由GPPUDCLK0/1寄存器选择的引脚。其他保持不变。第十六章将演示如何在 C 程序中改变上拉电阻。

驱动力

就电流而言,一个 GPIO 引脚可以提供多大的驱动力?SoC(片上系统)的设计使得每个 GPIO 引脚可以安全地吸收或提供高达 16 mA 的电流而不会造成损害。驱动强度可通过软件配置,范围为 2 至 16 mA。

表 11-7 列出了用于配置 GPIO 驱动强度的 SoC 寄存器。共有三个寄存器,影响三组 28 个 GPIO 引脚(两组影响用户可访问的 GPIO)。压摆率、滞后和驱动强度设置都适用于组级别。驱动强度通过 2 mA 至 16 mA 范围内的 3 位值进行配置,增量为 2 mA。当写入这些寄存器时,域 PASSWRD 必须包含十六进制值0x5A,以防意外更改。

表 11-7

GPIO 焊盘控制

|

|

|

描述

|

输入-输出

|

重置

|
| — | — | — | — | — |
| 31:24 | 密码§ | 0x5A | 写入时必须是 0x5A | W | 0x00 |
| five past eleven p.m. | 内向的; 寡言少语的; 矜持的 | 0x00 | 写为零,读为不在乎 | 拆装 |   |
| 04:04 | 许多 | 转换速度 |   |   |
| Zero | 转换速率受限 | 拆装 | one |
| one | 转换速率不受限制 |
| 03:03 | HYST | 使能输入迟滞 |   |   |
| Zero | 有缺陷的 | 拆装 | one |
| one | 使能够 |
| two o’clock | 驱动器 | 驱动力 | 拆装 | three |
| Zero | 2 毫安 |
| one | 4 毫安 |
| Two | 6 毫安 |
| three | 8 毫安(默认,28 至 45 除外) |
| four | 10 毫安 |
| five | 12 毫安 |
| six | 14 毫安 |
| seven | 16 毫安(GPIO 28 至 45) |

要直观了解 Raspberry Pi 如何控制驱动强度,请参见图 11-1 。控制线 Drive0 至 Drive2 由 Drive 寄存器中的位使能。这三条控制线禁用(零)时,只有底部的 2 mA 放大器有效(该放大器始终使能输出)。这代表最弱的驱动强度设置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-1

驱动强度控制

当 Drive 0 设为 1 时,顶部放大器使能,增加另一个 2 mA 驱动,总电流为 4 mA。使能驱动器 1 会再增加 4 mA 的驱动器,总计 8 mA。使能驱动 2 可使总驱动能力达到 16 mA。

应该提到的是,这些驱动能力是而不是限流器。他们所做的是应用或多或少的放大器驱动。如果 GPIO 输出连接到轻负载,如 CMOS 芯片或 MOSFET 晶体管,消耗的电流很少,则最低 2 mA 的驱动就足够了。当 GPIO 输出加载较高的电流负载时,单个 2 mA 缓冲器可能不足以将逻辑电平保持在规格范围内。通过施加更大的驱动力,输出电压电平被控制在正确的工作范围内。

逻辑电平

Raspberry Pi GPIO 引脚使用 3.3 V 逻辑电平。原始 BCM2835 SoC 的精确逻辑级规格如下(新型号可能略有不同)。

|

参数

|

伏特

|

描述

|
| — | — | — |
| V | ≤0。 8 | 低输入电压 |
| VIH | ≥1。 3 | 电压,输入高 |

V IL 和 V IH 之间的电压电平分别对于逻辑值 0 和 1 被认为是模糊的或未定义的,必须避免。当驱动 LED 等电流负载时,这一点就不那么重要了。

输入引脚

GPIO 输入引脚只能承受 0 至 3.3 V(最大值)之间的电压。与使用更高电压的其它电路(如 TTL 逻辑,使用 5 V)接口时要小心。SoC 不能耐受过压,可能会被损坏。

虽然芯片上有保护二极管来防止负输入摆幅和过压,但这些二极管很弱,只能释放静电荷。Broadcom 没有记录这些保护二极管的电流容量。

输出引脚

作为输出 GPIO 引脚,用户对电流限制负全部责任。没有没有限流。当输出引脚处于高电平状态时,作为电压源,它试图提供 3.3 V 电压(在晶体管和电源电压调节器的限制范围内)。

如果该输出对地短路,则尽可能多的电流会流过。这可能会导致永久性损伤。

输出也能在前面列出的电压规格下工作。但是附加的负载会扭曲工作电压范围。一个输出引脚可以提供吸收电流。所需的电流量和配置的输出驱动的量会改变工作电压曲线。只要您保持在所配置的驱动能力的电流限制内,您的 Pi 就应该满足电压规格。

源电流

图 11-2 显示了 GPIO 端口如何向其负载(显示为电阻)提供电流。电流从+3.3 V 电源流出,通过晶体管M1,流出 GPIO 引脚,进入负载,然后接地。因此,需要高电平(逻辑 1)才能将电流送入负载。这是一个高电平有效配置的例子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-2

GPIO 通过负载从晶体管 M1 获得电流

下沉电流

图 11-3 说明了 GPIO 输出如何使用晶体管 M2 通过负载将电流吸收到地。由于负载连接到+3.3 V 电源,电流从电源流入负载,然后通过 M 2 流入 GPIO 输出引脚接地。为了通过负载发送电流,将逻辑 0 写入输出端口,使其成为低电平有效配置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-3

通过 M2 从负载流出的 GPIO 吸电流

驱动 led

当 LED 接到 GPIO 输出端口时,负载变成 LED 和限流电阻。图 11-4 说明了从 GPIO 驱动 LED 的两种方式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-4

Raspberry Pi GPIO 通过一个限流电阻驱动两个 led。(A)左侧的高电平有效配置。(B)右侧的低电平有效配置。

GPIO 输出驱动器中使用的 MOSFETs 是互补的。请注意代表 M1 的箭头与代表 M2 的箭头有何不同。这些晶体管充当开关。驱动信号从写入的 GPIO 输出位反转。向输出写入 1 位时,M1 和 M2 栅极的驱动信号为低电平。低电平打开 M1,同时关闭 M2。以这种方式,对于给定的驱动信号,只有上部或下部晶体管导通。

当 GPIO 写入 1 位时,LED1 点亮,因为 GPIO 晶体管 M1 通过 LED1 提供电流(查看图 11-2 )。因为 1 位打开 LED,这被称为有效配置。

当一个 0 位被写到 GPIO 时,LED2 被点亮,因为晶体管 M2 导通,将电流吸收到地(查看图 11-3 )。因为 0 位打开 LED,这被称为有效配置。

为了限制流经 LED 的电流并保护输出晶体管,应使用限流电阻®。使用欧姆定律计算电阻:

)

与所有二极管一样,LED 具有正向压降(V F ),这使得数学计算稍微复杂了一些。在计算电阻时,应从电源电压(V CC )中减去该正向压降。对于红色 led,电压降通常在 1.63 至 2.03 V 之间。

已知 LED 所需的电流消耗,所需电阻可通过下式计算:

)

其中:

VCC 是电源电压(+3.3 V)。

V LED 为 LED 的正向压降。

I LED 为 LED 所需的电流消耗。

对于 V LED 来说,最好假设最坏的情况,假设最低电压降为 1.63 V。对于 5 mm LED 的亮度来说,大约 8 mA 是合理的,这样我们就可以计算限制电阻的电阻:

)

由于电阻采用标准值,因此我们四舍五入至最接近的标准 10%元件值 220ω。

注意

向下舍入电阻会导致更高的电流。宁可错在电流小。

LED 和 220ω限流电阻可根据图 11-4 接线,无论是高电平有效(A)还是低电平有效(B)配置。

其他 LED 颜色

一些纯绿色、蓝色、白色和 UV(紫外线)led 的 V F 约为 3.3 V。这些 led 会让您计算出零欧姆或接近零欧姆的电阻。这种情况下,不需要限流电阻。

另一方面,黄色 led 的 V F 约为 1.8 V。使用 led 时,通常不会有数据手册。尤其是当你把它们从垃圾箱里拿出来的时候。最好使用图 11-5 的试验板电路测量 V F 。在这种测量中,使用 5 V 或更高的电源。这样,如果你的 V F 测量值接近或高于 3.3 V,你可以得到一个好的读数。将您的 DMM(数字万用表)探针连接到指定点并测量电压。假设电阻约为 220 至 330 欧姆(使用 3 mm 或更小的 led 时更高)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-5

测量未知 LED 的正向电压(V F

尽管有测量正向电压的所有预防措施,您仍然可以计算 220 或 330 欧姆的 10%电阻值。但这让您放心,GPIO 不会受到任何伤害。对于更高电压的 led,可以安全地消除限流电阻。如果有任何疑问,请测量 LED 开启时消耗的电流。它不应超过 16 mA,以保持在 Pi 的驱动极限内。

驱动逻辑接口

对于 led 来说,接口的要求相当简单。如果当输出端口处于一种状态时 LED 亮起,而在另一种状态时 LED 熄灭,则接口成功。如果遵守最大电流限制,这两种状态下 GPIO 输出引脚上出现的精确电压就无关紧要了。

与逻辑接口时,输出电压至关重要。对于接收逻辑,输出电平必须至少为VIH才能可靠地记录 1 位(对于 BCM2835,这是 1.3 V)。同样,输出应小于 V IL ,以便在接收器中可靠地记录 0(对于 BCM2835,这是 0.8V)。这些限值之间的任何电压电平都是不明确的,可能导致接收器随机看到 0 或 1。

不同逻辑系列之间的接口有多种方法。文档“Microchip 3V Tips’n Tricks”提供了一个很好的信息来源 12 另一篇题为“3V 和 5V 应用接口,AN240”的文档描述了系统间接口的问题和挑战。 13 举例来说,它描述了如果不采取预防措施,一个 5 V 系统最终会提高 3.3 V 的电源电压。

接口方法包括直接连接(安全时)、分压电阻、二极管电阻网络和更复杂的运算放大器比较器。“自定义 Raspberry Pi 接口”中有整整一章专门讨论这个主题 14 选择方法时,记得考虑接口必要的切换速度。

驱动双色 led

这是一个提到驱动双色 led 的好地方。其中一些被配置成使得一个 LED 被正向偏置,而另一个被反向偏置,但是使用两个引线。或者你可以只使用一对连接在一起的发光二极管,如图 11-6 所示。这具有只需要两个 GPIO 输出的优势。要改变颜色,只需改变一对 GPIO 输出的极性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 11-6

驱动双色 LED 或 LED 对

表 11-8 总结了 GPIO 对可能状态的真值表。当两个 GPIO 输出具有不同的状态时,因为电流可以流动,所以一个 LED 或另一个 LED 被点亮。当两个 GPIOs 具有相同的状态时,没有电流可以关闭两个 led。当 LED1 和 LED2 提供不同的颜色时,您可以通过选择哪个 LED 正向偏置来选择颜色输出。

表 11-8

图 11-6 中双色 led 驱动真值表

|

GPIO-1

|

GPIO-2

|

结果

|
| — | — | — |
| 低的 | 低的 | 两个指示灯都不亮 |
| 低的 | 高的 | LED2 正向偏置(导通),LED1 关断 |
| 高的 | 低的 | LED1 正向偏置(导通),LED2 关断 |
| 高的 | 高的 | 两个指示灯都不亮 |

需要注意的一个复杂情况是,不同颜色的 led 的 V F 可能会有很大不同。你需要在限流电阻上妥协。确保 GPIO 输出永远不需要超过 16 mA 的源电流或吸电流。

交错函数

配置 GPIO 引脚时,您必须选择它是输入、输出还是替代功能(如 UART)。完整的选择列表如表 11-9 所示。替代功能 x 的确切含义取决于所配置的引脚。

表 11-9

替代功能选择

|

密码

|

选择的功能

|

中高音

|
| — | — | — |
| 000 | GPIO 引脚是一个输入。 |   |
| 001 | GPIO 引脚是一个输出。 |   |
| One hundred | GPIO 引脚是备用功能 0。 | Zero |
| One hundred and one | GPIO 引脚是备用功能 1。 | one |
| One hundred and ten | GPIO 引脚是备用功能 2。 | Two |
| One hundred and eleven | GPIO 引脚是备用功能 3。 | three |
| 011 | GPIO 引脚是备用功能 4。 | four |
| 010 | GPIO 引脚是备用功能 5。 | five |

表中 Code 列显示的值用于配置寄存器本身。替代功能编号列在 ALT 栏中。在编程时,保持这两者的一致性可能会令人困惑。选择功能后,将根据外设类型对配置进行微调。

输出引脚

当引脚配置为输出时,配置的其余元素包括:

  • 逻辑感

  • 输出状态

GPIO 引脚的输出状态可以设置为一个 32 位字,一次影响 32 个 GPIO,也可以单独设置或清零。通过单独的置位/清零操作,主机可以改变单个位,而不会干扰其它位的状态,也不必知道它们的状态。

输入引脚

由于提供了额外的硬件功能,输入引脚更加复杂。这要求输入 GPIO 引脚配置如下:

  • 检测上升输入信号(同步/异步)

  • 检测下降输入信号(同步/异步)

  • 检测高电平信号

  • 检测低电平信号

  • 逻辑感

  • 中断处理(由驱动程序处理)

  • 选择不上拉;使用上拉或下拉电阻

一旦做出这些选择,就可以接收与输入信号变化相关的数据,或者简单地查询引脚的当前状态。

浮动电位

如果没有提供或配置上拉或下拉电阻,未连接的 GPIO 输入可能会“浮动”。当输入端连接到驱动电路时,该电路将提供非浮动电压电平。GPIO 输入使用 MOSFET 晶体管。本质上,它只对电压敏感(不像双极晶体管那样对电流敏感)。因此,当输入未连接时,GPIO 输入可以感应电压,包括附近的静电(像猫一样)。

输出 GPIO 引脚箝位在输出电平,使内部输入晶体管处于安全状态。当 GPIO 配置为输入时,通常最好配置一个上拉或下拉电阻。这将把信号拉高或接地。当保持悬空时,静电将是随机的,需要 ESD(静电放电二极管)保护二极管来释放电荷。

摘要

本章介绍了 Raspberry Pi GPIO 的一些硬件特性。这为您提供了它的能力和局限性的坚实基础。Pi 的设计为 GPIO 提供了相当大的灵活性,包括驱动电平、上拉电阻和替代功能。

接下来的章节将介绍使用 GPIO 端口的不同方法,包括从 C 程序直接访问。

十二、Sysfs GPIO

本章使用 Raspbian Linux sysfs 伪文件系统研究 GPIO 驱动程序访问。使用 Raspbian 驱动程序甚至允许 shell 脚本配置、读取或写入 GPIO 引脚。

C/C++程序员可能会很快认为这种方法太慢而不予考虑。但驱动器确实提供了合理的边沿检测,这是直接寄存器访问方法所无法实现的。该驱动程序具有接收 GPIO 状态变化中断的优势。这些信息可以通过系统调用传递给程序,比如poll(2)

/sys/class/gpio

通过将顶级目录更改为根目录来浏览该目录:

$ sudo -i
# cd /sys/class/gpio

此时,您应该能够看到两个感兴趣的主要伪文件:

  • 出口

  • unexport(导出)

这些是只写的伪文件,无法读取,甚至 root 用户也无法读取:

# cat export
cat: export: Input/output error
# cat unexport
cat: unexport: Input/output error

通常,内核管理 GPIO 引脚的使用,尤其是像 UART 这样需要它们的外设。export伪文件的目的是允许用户保留它以供使用,就像打开文件一样。unexport伪文件用于将资源返回给 Raspbian 内核。

导出 GPIO

为了获得 GPIO17 的独占使用,export伪文件写入如下:

# echo 17 >/sys/class/gpio/export
# echo $?
0

注意,当查询$?时,返回代码是 0。这表示没有发生错误。如果我们提供了一个无效的 GPIO 号,或者一个没有被放弃的 GPIO 号,我们将返回一个错误:

# echo 170 >/sys/class/gpio/export
-bash: echo: write error: Invalid argument
# echo $?
1

成功保留 gpio17 后,应该会出现一个新的伪子目录,名为gpio17

# ls /sys/class/gpio/gpio17
active_low  device  direction  edge  power  subsystem  uevent  value

配置 GPIO

一旦您可以从导出中访问 GPIO,您会对主要的伪文件感兴趣:

  • direction:设置输入输出方向

  • value:读取或写入 GPIO 值

  • 改变逻辑感

  • edge:检测中断驱动的变化

gpiox/方向:

表 12-1 中描述了可读取或写入方向伪文件的值。

表 12-1

gpiox/direction 文件的值

|

|

意为

|
| — | — |
| 在 | GPIO 端口是一个输入。 |
| 在外 | GPIO 端口是一个输出。 |
| 高的 | 配置为输出,并向端口输出高电平。 |
| 低的 | 配置为输出,并向端口输出低电平。 |

要将我们的gpio17配置为输出引脚,请执行以下操作:

# echo out > /sys/class/gpio/gpio17/direction
# cat /sys/class/gpio/gpio17/direction
out

后面的cat命令不是必需的,但它验证了我们已经将gpio17配置为输出。

也可以使用方向伪文件将 GPIO 配置为输出一步设置其值:

# echo high > /sys/class/gpio/gpio17/direction
# echo low > /sys/class/gpio/gpio17/direction

gpioX/值

value伪文件允许您为已配置的 GPIO 设置值。当 GPIO 设置为输出模式时,我们现在可以将高电平写入引脚:

# echo 1 > /sys/class/gpio/gpio17/value

合法的值仅仅是 1 或 0。读取输入时,返回值 1 或 0。

如果您有一个连接到 GPIO17 的 LED,它现在应该是亮的。使用图 11-4 (A)进行 LED 和电阻器的接线。我们写入 GPIO 的任何内容也可以被读回:

# cat /sys/class/gpio/gpio17/value
1

将零写入伪文件,将输出值设置为低电平,关闭 LED。

# echo 0 > /sys/class/gpio/gpio17/value

图 12-1 展示了作者的“iRasp”设置——Raspberry Pi 3 B+用螺丝固定在一台老式 Viewsonic 显示器的背面,使用 Pi 鹅卵石电缆和适配器将 GPIO 信号引入试验板。连接到 GPIO17 的是一个红色 LED,与 330 欧姆限流电阻串联。鉴于 Pi 3 B+有 WIFI,这使得它成为一个方便的类似 iMac 的工作站,可以四处移动。在图中,它是无头操作,但四个 USB 端口使添加键盘和鼠标成为一件简单的事情。观察力敏锐的人可能会注意到,这款显示器的显示器支架是由另一款显示器改装而成的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 12-1

Raspberry Pi 3 B+使用 Pi 鞋匠连接到试验板,红色 LED 连接到高电平有效配置中的 GPIO17

gpioX/低电平有效

有时信号的极性不方便。当信号为低电平而不是正常的高电平时,低电平有效配置识别信号为有效的事实。如果这证明不方便,您可以使用active_low伪文件改变信号的含义:

# cat /sys/class/gpio/gpio17/active_low
0
# echo 1 > /sys/class/gpio/gpio17/active_low
# cat /sys/class/gpio/gpio17/active_low
1

第一个命令(cat)只是读取当前设置。零表示正常高电平有效逻辑有效。第二个命令(echo)将高电平有效配置更改为低电平有效。第三个命令确认设置完成。现在发送一个 1 到gpio17/value伪文件:

# echo 1 > /sys/class/gpio/gpio17/value

随着低电平有效配置的建立,这将导致 LED 熄灭。如果我们接着向伪文件写入零,LED 指示灯将会亮起:

# echo 0 > /sys/class/gpio/gpio17/value

逻辑的意义被颠倒了。如果改变 LED 的接线,使其与图 11-4 (B)相对应,写入零将打开 LED。在这个场景中,逻辑的意义与连线的意义相匹配。

gpioX/edge 和 gpioX/uevent

有些应用需要检测 GPIO 的变化。由于用户模式程序不接收中断,它唯一的选择就是不断地轮询 GPIO 以了解状态的变化。这浪费了 CPU 资源,就像坐在汽车后座的孩子问:“我们到了吗?我们到了吗?”驱动程序为程序接收更改通知提供了一种间接的方式。

表 12-2 中列出了写入该伪文件的可接受值。

表 12-2

伪文件边缘的可接受值

|

|

意为

|
| — | — |
| 没有人 | 没有边缘检测。 |
| 上升的 | 检测上升信号变化。 |
| 下降 | 检测下降信号变化。 |
| 两者 | 检测上升或下降信号变化。 |

这些值只能在输入 GPIO 上设置,并且必须使用准确的大小写。

# echo in > /sys/class/gpio/gpio17/direction
# echo both > /sys/class/gpio/gpio17/edge
# cat /sys/class/gpio/gpio17/edge
both

配置完成后,可以使用 uevent 伪文件来检查更改。这必须使用 C/C++程序来完成,该程序可以使用poll(2)selectl(2)来获得通知。当使用poll(2),请求事件POLLPRIPOLLERR时。使用select(2)时,文件描述符应该放入异常集。不幸的是,uevent文件对 shell 程序员没有帮助。

GPIO 测试脚本

目录~/RPi/scripts/gp 中提供了一个简单的测试脚本,并在清单 12-1 中列出。要在 GPIO17 上运行它,请按如下方式调用它(从 root):

$ sudo -i
# ~pi/RPi/scripts/gp 17
GPIO 17: on
GPIO 17: off  Mon Jul  2 02:48:49 +04 2018
GPIO 17: on
GPIO 17: off  Mon Jul  2 02:48:51 +04 2018
GPIO 17: on
GPIO 17: off  Mon Jul  2 02:48:53 +04 2018
GPIO 17: on
GPIO 17: off  Mon Jul  2 02:48:55 +04 2018

如果您有一个连接到 GPIO17 的 LED,您应该会看到它缓慢闪烁。

0001: #!/bin/bash
0002:
0003: GPIO="$1"
0004: SYS=/sys/class/gpio
0005: DEV=/sys/class/gpio/gpio$GPIO
0006:
0007: if [ ! -d $DEV ] ; then
0008:     # Make pin visible
0009:     echo $GPIO >$SYS/export
0010: fi
0011:
0012: # Set pin to output
0013: echo out >$DEV/direction
0014:
0015: function put() {
0016:     # Set value of pin (1 or 0)
0017:     echo $1 >$DEV/value
0018: }
0019:
0020: # Main loop:
0021: while true ; do
0022:     put 1
0023:     echo "GPIO $GPIO: on"
0024:     sleep 1
0025:     put 0
0026:     echo "GPIO $GPIO: off  $(date)"
0027:     sleep 1
0028: done
0029:
0030: # End

Listing 12-1The ~/RPi/scripts/gp test script

GPIO 输入测试

另一个简单的脚本如清单 12-2 所示,它将在输入 GPIO 改变时报告其状态。它需要三个参数:

  1. 输入 GPIO 号(默认为 25)

  2. 输出 GPIO 号(默认为 24)

  3. 有效检测:0 =高电平有效,1 =低电平有效(默认为 0)

以下调用假设输入 GPIO 为 25,LED 输出为 17,配置为高电平有效。按下 Control-C 以退出。

0001: #!/bin/bash
0002:
0003: INP="${1:-25}"  # Read from GPIO 25 (GEN6)
0004: OUT="${2:-24}"  # Write to GPIO 24 (GEN5)
0005: ALO="${3:-0}"   # 1=active low, else 0
0006:
0007: set -eu
0008: trap "close_all" 0
0009:
0010: function close_all() {
0011:   close $INP
0012:   close $OUT
0013: }
0014: function open() { # pin direction
0015:   dev=$SYS/gpio$1
0016:   if [ ! -d $dev ] ; then
0017:     echo $1 >$SYS/export
0018:   fi
0019:   echo $2 >$dev/direction
0020:   echo none >$dev/edge
0021:   echo $ALO >$dev/active_low
0022: }
0023: function close() { # pin
0024:   echo $1 >$SYS/unexport
0025: }
0026: function put() { # pin value
0027:   echo $2 >$SYS/gpio$1/value
0028: }
0029: function get() { # pin
0030:   read BIT <$SYS/gpio$1/value
0031:   echo $BIT
0032: }
0033:
0034: count=0
0035: SYS=/sys/class/gpio
0036:
0037: open $INP in
0038: open $OUT out
0039: put $OUT 1
0040: LBIT=2
0041:
0042: while true ; do
0043:   RBIT=$(get $INP)
0044:   if [ $RBIT -ne $LBIT ] ; then
0045:     put $OUT $RBIT
0046:     printf "%04d Status: %d\n" $count $RBIT
0047:     LBIT=$RBIT
0048:     let count=count+1
0049:   else
0050:     sleep 1
0051:   fi
0052: done
0053:
0054: # End

Listing 12-2The ~/RPi/scripts/input script

# ~pi/RPi/scripts/input 25 17 0
0000 Status: 0
0001 Status: 1
0002 Status: 0
0003 Status: 1
0004 Status: 0
^C

摘要

本章介绍了如何将 sysfs 驱动程序接口应用于 GPIO 端口。虽然看起来这个接口主要用于 shell 脚本,但是uevent伪文件需要 C/C++程序来利用它。这些伪文件另外提供了一个命令行接口,允许不同的 GPIO 操作。

下一章将研究程序对uevent文件的访问,并探索对 GPIO 寄存器本身的直接访问。

十三、C 程序 GPIO

无论您的应用需要 GPIO 的快速访问还是专门访问,C/C++程序都是最方便的方法。Python 程序同样可以在模块的帮助下直接访问。

本章着眼于如何从程序内部直接访问 GPIO 端口,从使用uevent文件的未竟事业开始,在后台中断的帮助下检测输入 GPIO 的变化。

边缘事件

前一章介绍了 GPIO 驱动程序提供的uevents伪文件。那里解释说你需要使用一个系统调用poll(2)select(2)来利用这个通知。这里我将说明poll(2)的用法,因为它是两者中首选的系统调用。

poll(2)背后的思想是提供一个开放文件描述符的结构化数组,并指出您感兴趣的事件。poll(2)使用的结构被定义为:

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

打开的文件描述符被放入成员fd中,而感兴趣的事件被保存到成员events中。结构成员revents由系统调用填充,返回后可用。

在目录~/RPi/evinput中你会找到 C 程序源文件evinput.c。清单 13-1 展示了执行poll(2)调用的部分。

0126: static int
0127: gpio_poll(int fd) {
0128:   struct pollfd polls[1];
0129:   char buf[32];
0130:   int rc, n;
0131:
0132:   polls[0].fd = fd;           /* /sys/class/gpio17/value */
0133:   polls[0].events = POLLPRI;  /* Events */
0134:
0135:   do  {
0136:       rc = poll(polls,1,-1);  /* Block */
0137:       if ( is_signaled )
0138:           return -1;          /* Exit if ^C received */
0139:   } while ( rc < 0 && errno == EINTR );
0140:
0141:   assert(rc > 0);
0142:
0143:   lseek(fd,0,SEEK_SET);
0144:   n = read(fd,buf,sizeof buf); /* Read value */
0145:   assert(n>0);
0146:   buf[n] = 0;
0147:
0148:   rc = sscanf(buf,"%d",&n);
0149:   assert(rc==1);
0150:   return n;                    /* Return value */
0151: }

Listing 13-1The gpio_poll() function, invoking the poll(2) system call

在这个程序中,我们只对一个 GPIO 感兴趣,所以数组用一个元素声明:

0128:   struct pollfd polls[1];

在调用 poll(2)之前,结构 polls[0]被初始化:

0132:   polls[0].fd = fd;          /* /sys/class/gpio17/value */
0133:   polls[0].events = POLLPRI; /* Events */

如果有第二个条目,那么 polls[1]也将被初始化。此后,可以调用系统调用:

0136:       rc = poll(polls,1,-1);  /* Block */

第一个参数提供第一个结构条目的地址(相当于&polls[0])。第二个参数表明有多少条目适用于这个调用(只有一个)。最后一个参数是以毫秒为单位的超时参数,负值表示永远阻塞。

如果系统调用返回一个正值(rc),这表明有多少结构条目返回了一个事件(在成员revents中)。当这种情况发生时,调用者必须扫描数组(polls)寻找任何返回的事件。理论上,程序应该测试:

if ( polls[0].revents & POLLPRI )

查看该文件描述符是否有活动。在这个程序中,我们不测试它,因为只提供了一个文件描述符(它是唯一可以返回活动的文件描述符)。但是如果你测试两个或者更多的 GPIOs,这个测试是必须的。

当 poll(2)的返回值为零时,这仅仅意味着超时已经发生。在这个程序中,没有使用超时,所以这不会发生。

如果返回值为-1,则系统调用因出错而返回。有一个特殊的错误代码,EINTR,稍后将会解释。

对于正常读取数据,要使用的事件宏名称是POLLIN。对于uevent伪文件,事件宏名为POLLPRI,表示有紧急数据需要读取。要读取的数据确实很紧急,因为当您读取value伪文件时,GPIO 端口的状态可能会改变。因此,如果你希望抓住上升的事件,不要惊讶你有时读回零。当这种情况发生时,在读取 GPIO 状态之前,上升事件已经来了又去。

输入错误

Unix 老手很快就掌握了 EINTR 错误代码。我们在这个循环中看到对它的引用:

0135:   do  {
0136:       rc = poll(polls,1,-1);  /* Block */
0137:       if ( is_signaled )
0138:           return -1;          /* Exit if ^C received */
0139:   } while ( rc < 0 && errno == EINTR );

poll(2)的问题是,当不可能超时时,没有办法响应信号(像终端 Control-C)。信号处理程序的功能有限,因为它是一个异步调用,例如,它可能会中断 malloc(3)调用。出于这个原因,evinput.c程序为 Control-C 指定了一个安全的中断处理程序。它只是将变量is_signaled设置为1

0018: static int is_signaled = 0;   /* Exit program when signaled */
...
0156: static void
0157: sigint_handler(int signo) {
0158:   is_signaled = 1;            /* Signal to exit program */
0159: }

为了让程序注意到变量已经变为非零,内核返回rc=-1来指示错误,并设置errno=EINTR。代码 EINTR 仅仅意味着系统调用被中断,应该重试。在给出的代码中,第 137 行测试该变量是否被设置为非零。如果是,函数会立即返回。否则,第 139 行中的while循环保持第 136 行中重试的系统调用。

阅读事件

一旦确定有紧急数据需要读取,接下来需要执行两步操作。这是不是poll(2)要求,而是伪文件uevent的驱动程序要求:

0143:   lseek(fd,0,SEEK_SET);
0144:   n = read(fd,buf,sizeof buf); /* Read value */
0145:   assert(n>0);
0146:   buf[n] = 0;
0147:
0148:   rc = sscanf(buf,"%d",&n);
0149:   assert(rc==1);
0150:   return n;                    /* Return value */

第 143 行在读取第 144 行中的文件描述符之前,有效地执行了一次倒带。这通知驱动程序使其事件数据可用于即将到来的读取。第 146 行只是将一个空字节放在读取数据的末尾,以便sscanf(3)可以使用它。因为我们期待文本形式的01,所以在第 148 行将其转换为整数值n,然后返回。

示范

要构建一个演示程序,请执行以下操作(如果需要强制重建,请执行“make clobber”):

$ make
gcc -c -Wall -O0 -g evinput.c -o evinput.o
gcc evinput.o -o evinput
sudo chown root ./evinput
sudo chmod u+s ./evinput

这个程序不需要你 sudo,因为它将evinput可执行文件设置为setuid root。在安全系统上,您可能想回顾一下。

要显示使用信息,使用-h选项:

$ ./evinput -h
Usage: ./evinput -g gpio [-f] [-r] [-b]
where:
       -f    detect rising edges
       -r    detect falling edges
       -b    detect both edges

Defaults are: -g17 -b

-g选项指定想要输入的 GPIO(默认为 17)。默认情况下,程序采用-b选项来报告上升沿和下降沿。现在让我们试试这个:

$ ./evinput -g 17 -b
Monitoring for GPIO input changes:

GPIO 17 changed: 0
GPIO 17 changed: 1
GPIO 17 changed: 0
GPIO 17 changed: 1
GPIO 17 changed: 0
^C

示例会话显示了从零到一以及从零到一的一些变化。由于接触反弹和这些变化发生的速度,这不会总是如此干净。现在就用上升的变化试试:

$ ./evinput -g 17 -r
Monitoring for GPIO input changes:

GPIO 17 changed: 0
GPIO 17 changed: 1
GPIO 17 changed: 1
GPIO 17 changed: 1
GPIO 17 changed: 0
GPIO 17 changed: 1
^C

预期读取值是上升沿后的一个1。然而,请注意,一个零溜了进来,这提醒我们接触反弹和时间起了作用。这个程序显示的第一个值总是 GPIO 的初始状态。

多 GPIO

为了便于说明,evinput.c程序保持简单。但是边沿检测的有用性可能会让您一次将它应用于多个 GPIO 端口。poll(2)方法的美妙之处在于您的应用不会浪费 CPU 周期等待事件发生。相反,GPIO 中断会在变化发生时通知内核,从而通知uevent驱动程序。当在伪文件上执行时,这将依次通知poll(2)

为了将演示代码扩展到多个 GPIO,在将 GPIO 放入正确的配置中之后,首先需要打开多个uevent伪文件。然后您需要扩展数组polls[]来包含感兴趣的 GPIOs 的数量(第 128 行)。然后初始化每个条目,如第 132 和 133 行所示。

第 136 行中调用poll(2)的第二个参数需要匹配初始化数组元素的数量。如果您正在监控五个 GPIOs,那么参数二需要是值5

在第 139 行结束的do while循环之后,您将需要扫描数组 polls【】,以确定哪些 GPIO 文件描述符报告了一个类似如下的事件:

for ( x=0; x<rc; ++x ) {
    if ( polls[x].revents & EPOLLPRI ) {
        // read polls[x].fd for GPIO value
    }
}

通过这种方式,您的应用可以非常有效地监控几个 GPIO 输入的变化。然而,你的代码必须能够处理接触反弹。一些 IC,如 PCF8574 I2C GPIO 扩展器,带有一个 INT 引脚,可以使用这种方法进行监控。

直接寄存器访问

出于性能或其他原因,用户模式程序有时需要直接访问 GPIO 寄存器。这需要 root 访问权限来控制用户访问,因为如果操作不当,可能会导致系统崩溃。崩溃是非常不可取的,因为它会导致文件丢失。

新的 Raspberry Pi 模型的引入增加了处理不同硬件平台的挑战。在最初的 Raspberry Pi 型和后来的 A 型中,外设寄存器有一个固定的硬件偏移。然而,这种情况已经改变,我们现在需要根据所涉及的硬件型号来计算正确的寄存器地址。

外围设备基址

为了访问 GPIO 外设寄存器,我们需要完成两件事:

  1. 确定我们寄存器组的基址

  2. 需要将物理内存映射到虚拟地址空间

鉴于 Raspberry Pis 现在在寄存器的物理位置上有所不同,我们需要确定外设基址。清单 13-2 显示了如何打开和读取伪文件,以确定实际的基地址。

0315: uint32_t
0316: peripheral_base() {
0317:   static uint32_t pbase = 0;
0318:   int fd, rc;
0319:   unsigned char buf[8];
0320:
0321:   fd = open("/proc/device-tree/soc/ranges",O_RDONLY);
0322:   if ( fd >= 0 ) {
0323:       rc = read(fd,buf,sizeof buf);
0324:       assert(rc==sizeof buf);
0325:       close(fd);
0326:       pbase = buf[4] << 24 | buf[5] << 16 | buf[6] << 8 | buf[7] << 0;
0327:   } else  {
0328:       // Punt: Assume RPi2
0329:       pbase = BCM2708_PERI_BASE;
0330:   }
0331:
0332:   return pbase;
0333: }

Listing 13-2Determining the peripheral base address

基本步骤是:

  1. 打开伪文件(第 321 行)。

  2. 将前 8 个字节读入字符数组 buf(第 323 行)。

  3. 一旦被读取,文件描述符可以被关闭(第 325 行)。

  4. 把地址拼凑在 326 行。

  5. 如果步骤 1 失败,采用宏BCM2708_PERI_BASE (which is 0x3F00000).的值

映射存储器

直接访问 GPIO 寄存器的下一步涉及到将物理内存映射到 C/C++程序的虚拟内存。清单 13-3 展示了物理内存是如何映射的。

0274: void *
0275: mailbox_map(off_t offset,size_t bytes) {
0276:   int fd;
0277:
0278:   fd = open("/dev/mem",O_RDWR|O_SYNC);
0279:   if ( fd < 0 )
0280:       return 0;       // Failed (see errno)
0281:
0282:   void *map = (char *) mmap(
0283:       NULL,                   // Any address
0284:       bytes,                  // # of bytes
0285:       PROT_READ|PROT_WRITE,
0286:       MAP_SHARED,             // Shared
0287:       fd,                     // /dev/mem
0288:       offset
0289:   );
0290:
0291:   if ( (long)map == -1L ) {
0292:       int er = errno;     // Save errno
0293:       close(fd);
0294:       errno = er;         // Restore errno
0295:       return 0;
0296:   }
0297:
0298:   close(fd);
0299:   return map;
0300: }

Listing 13-3
Mapping physical memory

执行的基本步骤如下:

  1. 通过打开/dev/mem访问第一个存储器,用于读取和写入(第 278 行)。此步骤需要 root 访问权限来保护系统的完整性。

  2. 一旦文件被打开,mmap(2)系统调用被用来将它映射到调用者的虚拟内存中(第 282 到 289 行)。

    1. 调用的第一个参数为 NULL,指定任何虚拟内存地址都是可接受的。可以指定这个地址,但是如果内核认为它不可接受,调用就会失败。

    2. 第二个参数是该区域要映射的字节数。在我们的演示程序中,这被设置为内核的页面大小。它需要是页面大小的倍数。

    3. 第三个参数表示我们想要读取和写入映射内存。如果你只想查询寄存器,宏PROT_WRITE可以被删除。

    4. 第四个参数是MAP_SHARED允许我们的调用程序与系统中任何其他可能访问相同区域的进程共享。

    5. 第五个参数是我们打开的文件描述符。

    6. 最后一个参数是我们希望访问的物理内存的起始偏移量。

  3. 如果mmap(2)调用由于任何原因失败,返回值将是一个长的负值。值errno将反映原因(第 291 到 296 行)。

  4. 否则,该文件可以被关闭(行 298 ),因为存储器访问已经被授权。在 299 中返回虚拟存储器地址。

寄存器访问

一旦所需的存储器被映射,就可以直接访问外设寄存器。要计算给定寄存器的正确虚拟内存地址,可以像这样使用宏:

0040: #define GPIO_BASE_OFFSET  0x200000    // 0x7E20_0000

附加宏引用相对于基址偏移量的特定寄存器。例如,该宏向寄存器提供一个偏移量,允许设置 GPIO 位。

0052: #define GPIO_GPSET0   0x7E20001C

这些寄存器访问相当混乱。在示例gp.c程序中,下面的gpio_read()函数使用 set_gpio32()辅助函数来确定:

  1. 寄存器地址(保存到gpiolev,第 232 行)。

  2. 所需的位shift(保存到变量移位,第 227 行)。

  3. 从需要访问的寄存器(GPIO_GPLEV0,第 232 行)。

该程序在gpiolev中提供计算出的字地址,并提供一个shift值用于参考特定位。清单 13-4 展示了该程序的代码。

0225: int
0226: gpio_read(int gpio) {
0227:     int shift;
0228:
0229:     if ( gpio < 0 || gpio > 31 )
0230:         return EINVAL;
0231:
0232:     uint32_v *gpiolev = set_gpio32(gpio,&shift,GPIO_GPLEV0);
0233:
0234:     return !!(*gpiolev & (1<<shift));
0235: }

Listing 13-4C function, gpio_read() to read a GPIO input bit

然后,线 234 访问包含感兴趣的 GPIO 位的寄存器,并将其返回给调用者。

写访问是类似的,除了寄存器写入值(列表 13-5 )。

0241: int
0242: gpio_write(int gpio,int bit) {
0243:   int shift;
0244:
0245:   if ( gpio < 0 || gpio > 31 )
0246:       return EINVAL;
0247:
0248:   if ( bit ) {
0249:       uint32_v *gpiop = set_gpio32(gpio,&shift,GPIO_GPSET0);
0250:           *gpiop = 1u << shift;
0251:   } else  {
0252:       uint32_v *gpiop = set_gpio32(gpio,&shift,GPIO_GPCLR0);
0253:       *gpiop = 1u << shift;
0254:   }
0255:   return 0;
0256: }

Listing 13-5Writing GPIO registers

by writing to the register address

读和写的唯一区别。Pi 有不同的寄存器来设置 GPIO 位(第 249 行)和另一个寄存器来清除它们(第 252 行)。

演示程序

在~/RPi/gpio 中构建源代码(如果您希望强制完全重建,请执行“make clobber”):

$ make
gcc -c -Wall -O0 -g gp.c -o gp.o
gcc gp.o -o gp
sudo chown root ./gp
sudo chmod u+s ./gp

同样,这个程序使用 setuid root,这样您就不会被迫使用 sudo。应用-h选项,程序具有使用信息:

$ ./gp -h | expand -t 8
Usage: ./gp -g gpio { input_opts | output_opts | -a | drive_opts} [-v]
where:
        -g gpio GPIO number to operate on
        -A n    Set alternate function n
        -a      Query alt function
        -q      Query drive, slew and hysteresis
        -v      Verbose messages

Input options:
        -i n    Selects input mode, reading for n seconds
        -I      Input mode, but performing one read only
        -u      Selects pull-up resistor
        -d      Selects pull-down resistor
        -n      Selects no pull-up/down resistor

Output options:
        -o n    Write 0 or 1 to gpio output
        -b n    Blink for n seconds

Drive Options:
        -D n    Set drive level to 0-7
        -S      Enable slew rate limiting
        -H      Enable hysteresis

所有调用都需要指定-g选项来提供 GPIO 号进行操作。可以添加选项-v以提供额外的输出。

GPIO 输入

以下示例会话将 GPIO 端口 17 配置为输入,并选择 60 秒的上拉高读数:

$ ./gp -g17 -i60
GPIO = 1
GPIO = 0
GPIO = 1
GPIO = 0
GPIO = 1
GPIO = 0
GPIO = 1
GPIO = 0

您的会话输出可能会显示一些接触反弹,所以不要期望所有的转换都是一个零一个地交替。

对于输入的一次性读取,使用-I代替:

$ ./gp -g17 -I
GPIO = 1

GPIO 输出

要将 GPIO 配置为输出并向其写入值,请使用以下命令:

$ ./gp -g17 -o1 -v
gpio_peri_base = 3F000000
Wrote 1 to gpio 17
$ ./gp -g17 -o0 -v
gpio_peri_base = 3F000000
Wrote 0 to gpio 17

在此会话中,添加了 verbose 选项用于直观确认。

测试时有闪烁的输出是很有用的。使用-b 选项来完成此操作。该参数指定闪烁的秒数:

$ ./gp -g17 -b4 -v
gpio_peri_base = 3F000000
GPIO 17 -> 1
GPIO 17 -> 0
GPIO 17 -> 1
GPIO 17 -> 0

驱动、迟滞和压摆率

驱动、压摆率限制和迟滞选项均可通过-D-q选项进行设置和查询。-D设定值和-q查询:

$ ./gp -g17 -D7, -S1 -H0 -q -v
gpio_peri_base = 3F000000
  Set Drive=7, slew=true, hysteresis=false
  Got Drive=7, slew=true, hysteresis=false

不使用 verbose 选项时,Set Drive 行被取消。-q 选项在 set 操作后执行,并报告更改后的配置。它只能用于查询:

$ ./gp -g17 -q
  Got Drive=7, slew=true, hysteresis=false

交替方式

也可以查询和设置 GPIO 的备用模式:

$ ./gp -g17 -a
GPIO 17 is in Output mode.

使用-A 选项设置备用模式:

$ ./gp -g17 -A5
$ ./gp -g17 -a
GPIO 17 is in ALT5 mode.

晶体管驱动器

在我们结束 GPIO 的话题之前,让我们回顾一下一个简单的晶体管驱动器,它可以用在一个完整的 ic 解决方案可能有些过头的情况下。Raspberry Pi 的 GPIO 引脚驱动电流的能力有限。即使配置为全驱动,它们也被限制为 16 mA。

您可能会发现,您只需要缓冲一个信号,而不是使用缓冲 IC。像 2N2222A 这样的廉价实用晶体管可能就是你所需要的。图 13-1 说明了该电路。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-1

一种简单的双极晶体管驱动器

输入信号从电路左侧的 GPIO 输出端到达,流经电阻 R1,通过基极发射极结接地。R1 将该电流限制在安全值。电阻 R2 连接在 Q1 的集电极和电源之间,电源电压可能略高于+3.3 V,这是安全的,因为集电极基极结反向偏置。但是,注意不要超过集电极基极电压。

Q1 在 25°c 时可以处理的最大功率为 0.5 W。当晶体管导通(饱和)时,Q1 两端的电压(V CE )在 0.3 到 1 V 之间。电压的剩余部分在负载上产生。如果我们假设 V CE 的最坏情况为 1 V,我们可以计算出 Q1 的最大电流:

)

该计算电流超过数据手册中 I C =600 mA 的限值,因此我们现在改用 600 mA。假设我们只需要 100 mA,而不是绝对极限。

接下来,我们想知道在所选集电极电流下所用器件的最低适用 H FE 。基于 STMicroelectronic 数据表,估计最低的 H FE 在 100 mA 附近约为 50。这个值很重要,因为它影响需要多少基极电流驱动。

)

现在知道了驱动晶体管的最小基极电流,我们可以计算基极电阻 R1:

)

最接近 10%的电阻值为 1.2 千欧。

感性负载

当涉及更大的电流或电压时,驱动继电器线圈并不罕见。然而,感性负载的问题是,当磁场崩溃时,驱动电路中会感应出反向电压。这发生在线圈电流被移除时。必须特别注意抑制这种情况。图 13-2 显示了驱动继电器线圈的晶体管。继电器断开和闭合负载触点 K1。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-2

由 Q1 驱动的感性负载

继电器线圈需要一个反向偏置二极管(D1 ),以泄放当电流从线圈(图中的引脚 5 和 2)移除时发生的任何反向冲击。这将具有减缓触点释放的效果。但是这优于导致系统崩溃的感应尖峰。

摘要

gp.c中提供的源代码完全是用 C 语言编写的,并保留了最基本的内容。它不仅演示了所涉及的直接寄存器访问步骤,还为您提供了可以在自己的 C/C++程序中重用的代码。

本章最后简要介绍了在需要驱动器时如何使用驱动晶体管。当更便宜的单晶体管解决方案可能就足够了时,人们往往会寻求 IC。

十四、单线驱动器

单线协议最初是由达拉斯半导体公司为 iButton 开发的。这种通信协议很有吸引力,可以应用到其他设备上,并很快被其他制造商采用。本章概述了 1-Wire 协议及其在 Raspberry Pi 中的支持方式。

单线线路和电源

1-Wire 协议实际上使用了两条导线,但不包括接地线:

  • 数据:用于数据通信的单线

  • 地线:地线或“回线”

单线协议设计用于与温度传感器等低数据量设备通信。它通过在用于数据通信的同一根电线上供电来提供低成本的遥感。当数据线处于高状态(也是线路的空闲状态)时,每个传感器可以从数据线接受电力。被吸走的少量功率为芯片的内部电容充电(通常约为 800 pF)。 15

当数据线有效(变为低电平)时,传感器芯片继续使用内部电容(寄生模式)。数据通信导致数据线在低电平和高电平之间波动。每当线路电平再次回到高电平时,即使是短暂的瞬间,电容也会重新充电。

该器件还提供一个可选的 V DD 引脚,允许直接向其供电。这有时用在寄生模式不够好的时候。这当然需要额外的电线,增加了成本。本章将重点讨论寄生模式,其中 V DD 接地。

线路驱动

数据线由主设备和从设备中的开漏晶体管驱动。当晶体管都处于关断状态时,该线由上拉电阻保持高电平。为了发出信号,一个晶体管导通,将线路拉低到地电位。

图 14-1 显示了连接到总线的主机(GPIO)的简化示意图。一些电压 V(通常为+5 V)通过上拉电阻R上拉 施加到 1 线总线上。当开漏晶体管M2 处于 Off 状态时,由于上拉电阻的作用,总线上的电压保持高电平。当主设备激活晶体管M2 时,电流将从总线流向地,类似于信号短路。连接到总线的从设备将看到接近零的电压。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-1

单线驱动电路

同样,当从机收到响应信号时,主机监听总线,同时从机激活其驱动晶体管。每当所有驱动晶体管关闭时,总线返回高空闲状态。

主机可以请求所有从机复位。主机发出请求后,它会放弃总线,并允许总线返回高电平。连接到总线的所有从机在短暂停顿后都会将线路拉低,以此作为响应。多个从机会同时将线路拉低,但这是允许的。这通知主设备至少有一个从设备连接到总线。此外,该程序将所有从机置于已知的复位状态。

主人和奴隶

主机始终控制着单线总线。奴隶只有在被要求的时候才会和主人说话。从不存在从设备到从设备的通信。

如果主机发现由于某种原因通信变得困难,它可能会强制总线复位。这纠正了可能在线路上叽叽喳喳的错误从设备。

草案

本节介绍单线通信协议。了解一些信号如何工作不仅有趣,而且可能有助于故障排除。更多信息可在互联网上获得。 16

重置

图 14-2 提供了单线协议复位程序的简化时序图。当主驱动器开始时,它复位 1 线总线,将所有从器件置于已知状态。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-2

单线复位协议

对于复位,总线被拉低并保持约 480 μs,然后总线被释放,上拉电阻再次将其拉高。短时间后,连接到总线的从设备开始响应,将线路拉低并保持一段时间。几个奴隶可以同时参与其中。主机在释放总线后约 70 μs 对总线进行采样。如果它发现线路为低电平,它知道至少有一个从机连接并响应。

在主采样点之后不久,所有从机再次释放总线并进入监听状态。它们不会再次响应,直到主机明确寻址从机。为了简单起见,我们将省略所使用的发现协议。

注意

每个从机都有一个保证唯一的地址。

数据输入输出

数据协议如图 14-3 所示。无论是写入 0 还是 1 位,发送设备都会将总线拉低。这宣告了数据位的开始。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-3

0 数据位的单线读/写

发送 0 时,线路保持低电平约 60 μs,然后总线被释放,并允许返回高电平。当发送 1 位时,在释放总线之前,线路仅保持低电平约 6 μs。另一个数据位直到前一位开始后 70 μs 才开始。这使得位之间有 10 μs 的保护时间。这样,接收机就有充足的时间来处理该位,并获得一定的信号抗噪能力。

当线路变为低电平时,接收器会注意到数据位的到来。然后,它启动一个定时器,以大约 15 μs 的时间对总线进行采样,如果总线仍处于低电平状态,则会记录一个 0 数据位。否则,数据位被解释为 1。注册数据位后,接收器继续等待,直到线路返回高电平(在 0 位的情况下)。

接收器保持空闲,直到它注意到线路再次变低,宣布下一位开始。

发送方可以是主方,也可以是从方,但是主方总是控制谁可以接着发言。除非主设备请求,否则从设备不会写入总线。

从属支持

表 14-1 列出了 Raspbian Linux 支持的从设备。列出的模块名可以在内核源代码目录arch/arm/machbcm2708/slave中找到。

表 14-1

单线从驱动器支持

|

设备

|

组件

|

描述

|
| — | — | — |
| DS18S20 | w1 _ 热温度 | 精密数字温度计 |
| DS18B20 突击步枪 | 可编程分辨率温度计 |
| DS1822 | Econo 数字温度计 |
| ds28 和 00 | 带 PIO 的 9 至 12 位数字温度计 |
| bq27000 | w1_bq27000.c | 高精度电池监控器 |
| DS2408 | w1_ds2408.c | 八通道可寻址开关 |
| DS2423 | w1 _ ds2423.c 型电脑 | 带计数器的 4 KB RAM |
| DS2431 | w1 _ ds2431.c | 1 KB EEPROM |
| DS2433 | w1 _ ds2433.c | 4 KB EEPROM |
| DS2760 | w1 _ ds2760.c | 精密锂离子电池监控器 |
| DS2780 | w1 _ ds2780.c | 独立燃油表 |

配置

随着 Linux 设备树的出现,现在有必要为单线驱动程序配置访问。编辑文件/boot/config.txt并添加以下行:

dtoverlay=w1-gpio,gpiopin=4,pullup=on

参数gpiopin=4指定 1 线总线在 GPIO4 上。这在过去是硬编码在驱动程序中的,但是现在允许你做不同的选择。如果未指定参数,它仍然是默认值。

参数pullup=on通常是成功操作所必需的。即使我将一个 4.7 千欧的电阻连接到+3.3 V 总线,我也无法让我的器件在寄生模式下工作。我建议您提供这个参数。编辑完文件后,重新启动以使其生效。

/boot目录中有一些有趣的文档:

$ less /boot/overlays/README
...
Name:   w1-gpio
Info:   Configures the w1-gpio Onewire interface module.
        Use this overlay if you *don't* need a GPIO to drive an external
        pullup.
Load:   dtoverlay=w1-gpio,<param>=<val>
Params: gpiopin        GPIO for I/O (default "4")

        pullup         Non-zero, "on", or "y" to enable the parasitic
                       power (2-wire, power-on-data) feature

引用的README文件还包含一个名为w1-gpio-pullup的条目,除非您知道为什么要使用它,否则您应该避免使用它。它需要一个额外的 GPIO 来上拉总线(默认为 GPIO5)。

读数温度

对常用温度传感器的支持可以在内核模块w1_therm中找到。当您第一次启动 Raspbian Linux 时,该模块可能不会被加载。您可以使用lsmod命令来检查它(清单中不需要 root):

$ lsmod
Module             Size   Used by
snd_bcm2835       12808   1
snd_pcm           74834   1 snd_bcm2835
snd_seq           52536   0
...

模块w1_therm依赖于另一个名为wire的驱动模块。要验证驱动程序模块是否已加载,请检查伪文件系统:

$ ls –l /sys/bus/w1
ls: cannot access /sys/bus/w1 : No such file or directory

没有找到路径名/sys/bus/w1,我们确认设备驱动程序没有被加载。

加载模块w1_therm将会带来它的大部分依赖模块:

$ sudo modprobe w1_therm
$ lsmod
Module                 Size   Used by
w1_therm               2705   0
wire                  23530   1 w1_therm
cn                     4649   1 wire
snd_bcm2835           12808   1
snd_pcm               74834   1 snd_bcm2835
...

加载完wire模块后,您会看到/sys/bus/w1/devices目录。还需要一个模块:

$ sudo modprobe w1_gpio
$ lsmod
Module                   Size   Used by
w1_gpio                  1283   0
w1_therm                 2705   0
wire                    23530   2 w1_therm,w1_gpio
cn                       4649   1 wire
snd_bcm2835             12808   1
...
$ cd /sys/bus/w1/devices
$ ls
w1_bus_master1

一旦模块w1_gpio被加载,配置的 GPIO 端口就会有一个总线主驱动程序。总线主控器通过创建符号链接devices/w1_bus_master1来表明其存在。转到/sys/bus/w1 目录并列出它,以查看其中关联的伪文件。长长的行被缩短了:

# pwd
/sys/bus/w1
# ls -lR .
.:
total 0
drwxr-xr-x 2 root root    0 Jul  6 06:47 devices
drwxr-xr-x 4 root root    0 Jul  6 06:47 drivers
-rw-r--r-- 1 root root 4096 Jul  6 06:47 drivers_autoprobe
--w------- 1 root root 4096 Jul  6 06:47 drivers_probe
--w------- 1 root root 4096 Jul  6 06:47 uevent

./devices:
total 0
lrwxrwxrwx 1 root root 0 Jul  6 06:47 28-00000478d75e -> ...
lrwxrwxrwx 1 root root 0 Jul  6 06:47 28-0000047931b5 -> ...
lrwxrwxrwx 1 root root 0 Jul  6 06:47 w1_bus_master1 -> ...

./drivers:
total 0
drwxr-xr-x 2 root root 0 Jul  6 06:47 w1_master_driver
drwxr-xr-x 2 root root 0 Jul  6 06:47 w1_slave_driver

./drivers/w1_master_driver:
total 0
--w------- 1 root root 4096 Jul  6 06:47 bind
--w------- 1 root root 4096 Jul  6 06:47 uevent
--w------- 1 root root 4096 Jul  6 06:47 unbind
lrwxrwxrwx 1 root root    0 Jul  6 06:47 w1_bus_master1 -> ...

./drivers/w1_slave_driver:
total 0
lrwxrwxrwx 1 root root    0 Jul  6 06:47 28-00000478d75e -> ...
lrwxrwxrwx 1 root root    0 Jul  6 06:47 28-0000047931b5 -> ...
--w------- 1 root root 4096 Jul  6 06:47 bind
--w------- 1 root root 4096 Jul  6 06:47 uevent
--w------- 1 root root 4096 Jul  6 06:47 unbind

伪文件名28-00000478d75e28-0000047931b5是作者的两个 DS18B20 设备的设备条目。如果您没有立即看到您的条目,请不要担心,因为发现协议需要时间来找到它们。

从属设备

图 14-4 显示了 Dallas DS18B20 从设备的引脚排列。该温度传感器是许多单线从机的典型器件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-4

DS18B20 引脚排列

从设备由代表产品系列的一对数字标识,后面是连字符和十六进制序列号。ID 28-00000478d75e 就是一个例子。您可能还想尝试不同的设备,如类似的 DS18S20。图 14-5 显示了连接到 Raspberry Pi GPIO 的 DS18B20。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-5

1 线,带 DS18B20 从电路,使用 V CC =3.3 V 和 4.7 k 上拉电阻

当一切正常时,总线主设备会自动检测从设备,作为其定期扫描的一部分。当您的设备被发现时,它们会以类似28-0000028f6667.的名称出现在设备子目录中

以下示例显示了两个 DS18B20 温度传感器如何出现在 1 线总线上:

$ cd /sys/bus/w1/devices
$ ls
28−00000478d75e 28−0000047931b5 w1_bus_master1
$

图 14-6 展示了作者使用的试验板设置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-6

带有两个 DS18B20 温度传感器的试验板连接到 Raspberry Pi

读取温度

从器件的温度可以通过读取其w1_slave伪文件来读取。在本例中,我们读取了两个 DS18B20 温度传感器,它们的精度应该达到 0.5°c,将这两个传感器一起读取应该显示出相当好的一致性(它们彼此非常接近):

# cd /sys/bus/w1/devices
# cat 28-00000478d75e/w1_slave
a6 01 4b 46 7f ff 0a 10 f6 : crc=f6 YES
a6 01 4b 46 7f ff 0a 10 f6 t=26375

以 t=26375 结尾的第二行表示读数为 26.375 摄氏度。

如果驱动程序在读取设备时遇到问题,DS18B20 的响应可能如下所示:

# cd /sys/bus/w1/devices
# cat 28-00000478d75e/w1_slave
50 05 4b 46 7f ff 0c 10 1c : crc=1c YES
50 05 4b 46 7f ff 0c 10 1c t=85000

值 t=85000 是绝对的泄露。如果您看到这种情况,请检查您的布线,尤其是上拉电阻。该电路需要一个 4.7 千欧的上拉电阻至+3.3 V。

摘要

在本章中,使用 1 线 Linux 支持来读取 Dallas Semiconductor DS18B20 温度传感器。表 14-1 列出了您可能会用到的几种其他类型的单线传感器。有了司机的支持,使用这样的传感器变得轻而易举。

十五、I2C 总线

I 2 C 总线,也称为双线接口(TWI ),由 Philips 于 1982 年左右开发,用于与低速外设进行通信。 17 它也很经济,因为它只需要两根电线(不包括地线和电源)。从那以后,在这个框架的基础上,又设计出了其他标准,比如 SMBus。然而,最初的 I 2 C 总线作为一种简单、经济的外设连接方式仍然很受欢迎。

I 2 C 概述

图 15-1 显示了树莓 Pi 环境中的 I 2 C 总线。Raspberry Pi 使用 BCM2835 器件作为总线主机来提供总线。注意,Pi 还提供外部上拉电阻R1 和R2,如虚线所示。表 15-1 列出了在顶条上提供的两条 I2C 总线。

表 15-1

I2C 公交线路

|

关系

|

通用输入输出接口

|

描述

|
| — | — | — |
| P1-03 | GPIO-2 | SDA1(串行总线数据) |
| P1-05 | GPIO-3 | SCL1(串行总线时钟) |

I2C 总线的设计允许多个外设连接到 SDA 和 SCL 线。每个从外设都有自己唯一的 7 位地址。例如,MCP23017 GPIO 扩展器外设可能配置为地址 0x20。主机使用该地址引用每个外设。非寻址外设应该保持安静。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-1

树莓派上的 I 2 C 总线

民主行动党和 SCL

主人和奴隶在不同的时间轮流“抢公共汽车”。主机和从机使用开漏晶体管来驱动总线。因为所有参与者都使用开漏驱动器,所以必须使用上拉电阻(由 Pi 提供)。否则,数据和时钟线会在切换之间浮动。

开漏驱动器设计允许所有参与者驱动总线线路,只是不能同时驱动。例如,从设备关闭其线路驱动器,让主设备驱动信号线。奴隶们只是听着,直到主人按地址叫他们。当从机需要应答时,从机将断言它的驱动程序,抢占线路。从机认为此时主机已经释放了总线。当从机完成自身的传输后,它会释放总线,让主机继续工作。

两条线路的空闲状态都为高。Raspberry Pi 的高电平状态为+3.3 V,其它系统可能使用+5 V 信号。购买 I 2 C 外设时,选择工作在 3.3 V 电平的外设。有时,通过仔细的信号规划或使用适配器,可以使用 5 V 外设。

总线信号

开始和停止位在 I 2 C 协议中是特殊的。起始位如图 15-2 所示。注意 SDA 线从高电平变为低电平,而时钟保持在高电平(空闲)状态。SDA 转换后 1/2 位时间后,时钟将变为低电平。这种特殊的信号组合通知所有连接的设备“监听”,因为下一条传输的信息将是设备地址。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-2

I2C 启动/停止信号

停止位的特殊之处还在于,它允许从设备知道是否有更多信息到来。当 SDA 线在一个位单元的中途从低电平变为高电平时,它被解释为一个停止位。停止位表示消息结束。

还有一个重复开始的概念,在图中通常标记为 SR 。该信号在电学上与起始位相同,除了它出现在消息中代替停止位。这向外设发出信号,表明正在发送或需要更多数据作为另一个消息的一部分。

数据位

数据位时序大致如图 15-3 所示。在 SCL 线变为高电平之前,SDA 线应根据发送的数据位稳定在高电平或低电平。接收器在 SCL 的下降沿输入数据,并对下一个数据位重复该过程。请注意,最高有效位首先传输(网络顺序,或大端)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-3

I2C 数据位传输

消息格式

图 15-4 显示了可用于 MCP23017 芯片的两个 I2C 消息示例。最简单的消息是写寄存器请求。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-4

I2C 消息示例

该图显示了以 S(开始)位开始并以 P(停止)位结束的每条消息。在起始位之后,每条消息以一个包含 7 位外设地址和一个读/写位的字节开始。每个外设都必须读取该字节,以确定该报文是否发给它。

地址发送后,被寻址的外设会返回一个 ACK/NAK 位。如果外设因任何原因未能响应,线路将因上拉电阻而变高,表示 NAK。主机在看到 NAK 后,将发送停止位并终止传输。

当被寻址的外设确认地址字节时,当请求是写操作时,主机继续写操作。第一个示例显示了接下来要写入的 MCP23017 8 位寄存器编号。这表示要写入外设的哪个寄存器。然后,外设将确认寄存器编号,允许主机将数据字节写入选定的寄存器。这也必须得到承认。如果主机没有更多的数据要发送,则发送 P(停止)位以结束传输。

图 15-4 中的第二个例子显示了一条消息如何由写消息和读消息组成。初始序列看起来像写操作,但这只是将一个寄存器号写入外设。一旦寄存器编号被确认,主机就会发送一个 SR(开始,重复)位。这告诉外设没有更多的写数据到达,并期待一个外设地址跟随其后。由于发送的地址指定了相同的外围设备,相同的外围设备以 ACK 响应。该请求是一个读请求,因此外设继续以所请求的 8 位读数据响应,主机应答。主机用 P (stop)终止消息,表示不再读取数据。

许多外设将支持自动递增寄存器模式。然而,这是外围设备的一个特征。并非所有设备都支持此功能。一旦通过写操作建立了外设的寄存器,就可以在自动递增模式下进行连续的读或写操作,每传输一个字节,寄存器就递增一次。这导致了高效的传输。

I 2 C 总线速度

与 SPI 总线不同,I 2 C 总线在 Raspbian Linux 中以固定速度运行。SoC 文档声称 I 2 C 工作频率高达 400 kHz,但默认为 100 kHz。

要使用 I 2 C,您必须在您的/boot/config.txt文件中通过取消注释来启用它。您还可以通过指定i2c_arm_baudrate参数来指定时钟频率。以下使能 I 2 C 并将时钟设置为 400 kHz:

dtparam=i2c_arm=on,i2c_arm_baudrate=400000

默认时钟速率相当于:

dtparam=i2c_arm=on,i2c_arm_baudrate=100000

保存 config.txt 文件并重新启动。您可以按如下方式确认时钟速率被接受:

  • xxd命令将一组 4 字节(-g4)报告为 00061a80。

  • gdb命令用于以十进制打印(p命令)该值(不要忘记在报告的数字前加上前缀0x以表示该值是十六进制)。

# xxd -g4 /sys/class/i2c-adapter/i2c-1/of_node/clock-frequency
00000000: 00061a80                             ....
# gdb
GNU gdb (Raspbian 7.12-6) 7.12.0.20161007-git
Copyright (C) 2016 Free Software Foundation, Inc.
...
(gdb) p 0x00061a80
$1 = 400000
(gdb) quit
#

I 2 C 工具

一些实用程序使得使用 I 2 C 外设变得更加容易。这些可能预装在您的 Raspbian Linux 中,但如果需要,也可以轻松安装。

$ sudo apt−get install i2c−tools

i2c-tools包包括以下实用程序:

  • i2cdetect:检测 I2C 线上的外围设备

  • i2cdump:从 I2C 外设转储值

  • i2cset:设置 I2C 寄存器和值

  • i2cget:获取 I2C 寄存器和值

这些实用程序中的每一个都有一个手册页来提供更多信息。

MCP23017

MCP23S17 是 I2C 芯片,提供 16 个扩展 GPIO 端口。启动时,这些引脚默认为输入,但也可以像本地 Pi GPIO 端口一样配置为输出。MCP23S17 是 SPI 总线的配套芯片。

该芯片允许有线配置八个不同的 I2C 地址。与本机 Pi GPIOs 一样,端口可以配置为高电平有效或低电平有效。该芯片采用 1.8 至 5.5 V 电源供电,非常适合 Pi 3.3 V 工作电压。

输出模式 GPIO 可以吸电流高达 8 mA,源电流为 3 mA。驱动负载(甚至是 led)时,应考虑到这一点。

对于输入 GPIOs,它具有中断功能,可通过 INTA (GPIOA0 至 GPIOA7)或 INTB 引脚(GPIOB0 至 GPIOB7)发出输入变化信号。该芯片可以配置为报告 INTA 上的所有更改,这就是它在这里的使用方式。这对于输入非常重要,因为否则您将需要持续轮询设备。

也许最好的部分是它有一个内核驱动程序。这使得使用起来非常方便。

驱动程序设置

必须配置的第一件事是必须启用 I 2 C,如果你还没有这么做的话。/boot/config.txt文件必须有以下未注释的行:

dtparam=i2c_arm=on,i2c_arm_baudrate=100000

接下来,您必须在config.txt中启用驱动程序:

  • 可选参数gpiopin=4指定 GPIO4 将用于检测芯片中的中断。GPIO4 是默认值。

  • 可选参数addr=0x20指定 MCP23017 芯片的 I 2 C 地址。默认值为 0x20。

dtoverlay=mcp23017,gpiopin=4,addr=0x20

编辑这些更改后,重新启动:

# sync
# /sbin/shutdown -r now

Pi 重新启动后,使用dmesg命令登录并检查可疑的错误消息。如果你觉得幸运,你可以跳过这一步。

如果一切顺利,您应该在/sys/class/gpio目录中看到如下内容:

# ls /sys/class/gpio
export  gpiochip0  gpiochip128  gpiochip496  unexport

如果您使用 I2C 地址 0x20,并且您将 MCP23017 连接到总线,您应该会看到子目录名称gpiochip496(其他地址更高)。如果您没有看到列出的芯片,则:

  • 仔细检查dmesg日志中的错误。

  • 检查配置和接线。

  • 确保 MCP23017 芯片的)引脚连接到+3.3 V。

接线

图 15-5 显示了本例中使用的接线。关于该电路,有几点值得注意:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 15-5

MCP23017 至 Raspberry Pi 的接线

  • 从+3.3 V(不是 5 V)为 MCP23017 芯片供电。

  • 总线不需要电阻,因为树莓 Pi 已经提供了 R 1 和 R 2

  • 重要!将)线连接到+3.3 V。否则会发生随机或完全故障。

  • 如果您仅计划使用输出模式 GPIOs,则不需要布线 INTA 线。但是,无论是否使用,驱动程序都会消耗配置的 GPIO。

)线没有连接到+3.3 V 时,芯片的输入会浮动。有时 CMOS 输入会浮动在高电平,有时浮动在低电平(导致芯片复位)。我最初连接电路时碰到了这个。最糟糕的是,驱动程序和芯片工作了一段时间,但后来出现了问题。

INTA 线(以及图 15-5 中的 GPIO4)的目的是通知 Pi 输入 GPIO 端口已经改变状态。这通知 mcp23017 驱动器发送 I 2 C 请求来读取输入。如果没有这个通知,驱动程序将不得不忙于 I 2 C 总线,重复读取请求以查看是否有新的输入。

测试 GPIO 输出

连接好电路,设置好配置,重新启动系统后,如果您使用 0x20 的 I 2 C 地址,您应该会看到驱动程序在/sys/class/gpio中以gpiochip496的形式报告其存在。

按照第十二章中访问本地 GPIO 的相同方式,我们可以导出该 GPIO。但是首先我们需要确定每个 MCP23017 GPIO 端口对应哪个 GPIO 号。为此有两个伪文件:

  1. gpiochip496/base 列出了该设备的起始 GPIO 号(496)。

  2. gpiochip496/ngpio 列出了支持的 gpio 数量(16)。

下面显示了一个发现会话示例:

# cd /sys/class/gpio
# ls
export    gpio503  gpiochip0  gpiochip128  gpiochip496  unexport
# ls gpiochip496
base  device  label  ngpio  power  subsystem  uevent
# cat gpiochip496/base
496
# cat gpiochip496/ngpio
16
#

该信息允许在表 15-2 中创建图表。

表 15-2

Gpiochip496 (I 2 C 地址 0x20)的 GPIO 关联

|

通用输入输出接口

|

别针

|

MCP23017

|

通用输入输出接口

|

别针

|

MCP23017

|
| — | — | — | — | — | — |
| GPIO496 | Twenty-one | A0 | GPIO504 | one | B0 |
| GPIO497 | Twenty-two | 一流的 | GPIO505 | Two | B1 |
| GPIO498 | Twenty-three | 主动脉第二声 | GPIO506 | three | B2 |
| GPIO499 | Twenty-four | A3 号 | GPIO507 | four | B3 |
| GPIO500 | Twenty-five | A4 号 | GPIO508 | five | B4 |
| GPIO501 | Twenty-six | A5 号 | GPIO509 | six | B5 |
| GPIO502 | Twenty-seven | A6 | GPIO510 | seven | B6 |
| GPIO503 | Twenty-eight | A7 | GPIO511 | eight | B7 |

要将 MCP23017 GPIO A7 用作输出,我们需要:

# pwd
/sys/class/gpio
# echo out >gpio503/direction
# cat gpio503/direction
out
# echo 1 >gpio503/value
# cat gpio503/value
1

如果在高电平有效配置中有一个 LED 连接到 A7,它应该会亮起。否则,用数字式万用表测量,你应该在针脚 28 上看到+3.3 V。

# echo 0 >gpio503/value
# cat gpio503/value
0

完成上述操作后,GPIO A7 现在应该会变低。

测试 GPIO 输入

在 GPIO A7 仍配置为输出的情况下,将 MCP23017 GPIO A6 配置为输入:

# ls
export    gpio503  gpiochip0  gpiochip128  gpiochip496  unexport
# echo 502 >export
# ls
export    gpio502  gpio503  gpiochip0  gpiochip128  gpiochip496  unexport
# echo in >gpio502/direction

在 A7(针脚 28)和 A6(针脚 27)之间连接一根跨接导线。现在让我们看看输入 A6 是否与输出 A7 一致:

# cat gpio502/value
0
# cat gpio503/value
0
# cat gpio502/value
0
# echo 1 >gpio503/value
# cat gpio502/value
1

不出所料,当我们更改 A7 时,输入 A6 随之而来。

测试 GPIO 输入中断

仅仅能够读取一个 GPIO 通常是不够的。我们需要知道何时发生了变化,以便可以在那个时间点读取。在图 15-5 中,MCP23017 芯片的 INTA 引脚连接到 Pi 的 GPIO4。MCP23017 将在输入发生未读变化时激活该线路,在 Pi 中提醒驾驶员。只有这样,驱动程序才需要读取芯片的当前输入状态。

为了测试这是否可行,我们将重用 evinput 程序来监控 gpio502 (GPIO 输入 A6):

$ cd ~/RPi/evinput
$ ./evinput -g502 -b

转到根终端会话,让我们切换 A7 几次:

# pwd
/sys/class/gpio
# ls
export    gpio502  gpio503  gpiochip0  gpiochip128  gpiochip496  unexport
# echo 1 >gpio503/value
# echo 0 >gpio503/value
# echo 1 >gpio503/value
# echo 0 >gpio503/value

切换回 evinput 会话,查看是否有边沿(- b选项监控上升沿和下降沿):

$ ./evinput -g502 -b
Monitoring for GPIO input changes:

GPIO 502 changed: 1
GPIO 502 changed: 0
GPIO 502 changed: 1
GPIO 502 changed: 0
^C

事实上,这证实了中断设施的工作。注意,我们监控的是 GPIO502 (A6)而不是 GPIO4。只有驱动程序需要监控 GPIO4。

限制

MCP23017 的驱动程序支持为您的 Raspberry Pi 添加 16 个 GPIOs 提供了一种非常方便的方式。尽管这很好,但仍有几点需要考虑:

  • 扩展 gpio 没有原生 Pi GPIOs 快。

  • 添加多个 MCP23017 芯片可能需要做一些功课。虽然总线支持多达八个唯一寻址的 MCP23017 芯片,但设备驱动程序可能不支持。通过向设备树添加节点,这是可能的。

  • I/O 性能与 I 2 C 时钟速率直接相关。

  • GPIOs 是通过 sysfs 伪文件系统访问的,这进一步影响了性能。

需要记住的主要一点是,所有 GPIO 交互都以时钟速率(100 kHz 或 400 kHz)通过 I 2 C 总线进行。每个 I/O 可能需要几个字节的传输,因为 MCP23017 有一大组寄存器。传输每个字节都需要时间。默认为 100 kHz 时,一个字节的传输需要:

)

读取一个 GPIO 输入寄存器需要一个起始位、三个字节的数据和一个停止位。这导致最小处理时间为 260 μs。这将 GPIO 读取次数限制在大约 3,800 次读取/秒。这还不包括与其它器件共享总线的情况。

最后,适用性取决于应用。通过将速率最高的 GPIO 事务转移到 Pi 的本机 GPIO,并将较慢的 I/o 转移到扩展 GPIO,您可能会发现这种安排非常有效。

I2c API

本节将介绍用于 I 2 C 总线事务的裸机 C 语言 API。使用该 API,您可以使用另一个 GPIO 扩展器(如 PCF8574)编写自己的接口。该芯片提供了 8 个额外的 GPIOs,但经济实惠且+3.3 V 友好。它只有一个配置寄存器,易于直接使用。

内核模块支持

通过使用内核模块来提供对 I2C 总线的访问。如果您已经在 config.txt 中启用了 I2C,您应该能够列出总线控制器:

# i2cdetect -l
i2c-1 i2c        bcm2835 I2C adapter             I2C adapter

对驱动程序的访问由以下节点提供:

# ls -l /dev/i2c*
crw-rw---- 1 root i2c 89, 1 Jul  7 16:23 /dev/i2c-1

头文件

以下头文件应包含在 I 2 C 程序中:

#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c−dev.h>

打开(2)

使用 I 2 C 设备很像使用文件。你打开一个文件描述符,用它做一些 I/O 操作,然后关闭它。一个区别是使用了ioctl(2)调用,而不是通常的read(2)write(2)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname,int flags,mode_t mode);

在哪里

|

错误

|

描述

|
| — | — |
| 电子会议 | 不允许访问该文件。 |
| 这是一次很好的尝试。 | 路径名指向可访问的地址空间之外。 |
| 电磁场 | 进程已经打开了最大数量的文件。 |
| 最后一个 | 已达到系统对打开文件总数的限制。 |
| 伊诺梅 | 可用的内核内存不足。 |

  • pathname是您需要打开/创建的文件/目录/驱动程序的名称。

  • flags是可选标志的列表(使用O_RDWR进行读写)。

  • mode是创建文件的权限位(省略参数,或在不创建时提供零)。

  • 返回-1(错误代码在errno中)或打开文件描述符> =0。

要使用 I 2 C 总线控制器,您的应用必须打开设备节点上的驱动程序:

int fd;

fd = open("/dev/i2c−1",O_RDWR);
if ( fd < 0 ) {
    perror("Opening /dev/i2c−1");

注意,设备节点(/dev/i2c-1由 root 所有,所以您需要提升权限来打开它或者让您的程序使用setuid(2)

ioctl(2,FUNC I2C)

在 I 2 C 代码中,通常会执行一个检查来确保驱动程序有正确的支持。I2C_FUNC ioctl(2)调用允许调用程序查询 I 2 C 能力。返回的能力标志记录在表 15-3 中。

表 15-3

I2C_FUNC 位

|

位屏蔽

|

描述

|
| — | — |
| I2C FUNC I2C | 支持普通 I2C(非 SMBus) |
| FUNC I2C ADDR 10 比特 | 支持 10 位地址 |
| FUNC I2C 协议芒林 | 支持: |
|   | I2C _ M _ 忽略 _NAK |
|   | ADDR I2C 机场 |
|   | -= ytet-伊甸园字幕组=-翻译 |
|   | I2C_M_NO_RD_ACK |

long funcs;
int rc;

rc = ioctl(fd,I2C_FUNCS,&funcs);
if ( rc < 0 ) {
    perror("ioctl(2,I2C_FUNCS)");
    abort();
}

/* Check that we have plain I2C support */
assert(funcs & I2C_FUNC_I2C);

用于检查至少普通 I2C 支持存在的宏。否则,程序中止。

ioctl(2,I2C_RDWR)

虽然可以使用ioctl(2,I2C_SLAVE)然后使用read(2)write(2)调用,但这不太实际。因此,系统调用ioctl(2,I2C_RDWR)的使用将被提升。这个系统调用允许在执行复杂的 I/O 事务时有相当大的灵活性。

任何ioctl(2)调用的通用 API 如下:

#include <sys/ioctl.h>

int ioctl(int fd,int request,argp);

在哪里

|

错误

|

描述

|
| — | — |
| ebadf(消歧义) | fd 不是有效的描述符。 |
| 这是一次很好的尝试。 | argp 引用了不可访问的内存区域。 |
| 埃因瓦尔 | 请求或 argp 无效。 |

  • fd是打开的文件描述符。

  • request是要执行的 I/O 命令。

  • argp是与命令相关的参数(类型根据request而变化)。

  • 返回-1(errno中的错误代码),完成的消息数(当request = I2C_RDWR时)。

request参数作为I2C_RDWR提供时,argp参数是指向struct i2c_rdwr_ioctl_data的指针。这个结构指向一个消息列表,并指出涉及到多少条消息。

struct i2c_rdwr_ioctl_data {
    struct i2c_msg   *msgs;   /* ptr to array of simple messages */
    int              nmsgs;   /* number of messages to exchange */
};

前述结构引用的单个 I/O 消息由struct i2c_msg描述:

struct i2c_msg {
    __u16     addr;   /* 7/10 bit slave address */
    __u16     flags;  /* Read/Write & options */
    __u16     len;    /* No. of bytes in buf */
    __u8     *buf;    /* Data buffer */
};

该结构的成员如下:

表 15-4

I2C 能力标志

|

|

描述

|
| — | — |
| I2C _ 十点 | 使用 10 位从机地址 |
| I2C 月日 | 读入缓冲区 |
| -= ytet-伊甸园字幕组=-翻译 | 抑制(重新)开始位 |
| ADDR I2C 机场 | 反相读/写位 |
| I2C _ M _ 忽略 _NAK | 将 NAK 视为 ACK |
| I2C_M_NO_RD_ACK | 读取不会有 ACK |
| I2C 大学 | 缓冲区可以容纳 32 个额外的字节 |

  • addr:通常这是 7 位从地址,除非标志I2C_M_TEN和功能

  • I2C_FUNC_10BIT_ADDR都用上了。必须为每条消息提供。

  • flags:有效标志列在表 15-4 中。标志I2C_M_RD表示该操作是读操作。否则,当该标志不存在时,假设写操作。

  • buf:用于读/写该报文组件的 I/O 缓冲区。

  • len:该报文组件中要读/写的字节数。

实际的ioctl(2,I2C_RDWR)调用将编码如下。本例中,MCP23017 寄存器地址 0x15 被写入外设地址 0x20,随后读取 1 个字节:

int fd;
struct i2c_rdwr_ioctl_data msgset;
struct i2c_msg iomsgs[2];
static unsigned char reg_addr[] = {0x15};
unsigned char rbuf[1];
int rc;

iomsgs[0].addr   = 0x20;            /* MCP23017−A */
iomsgs[0].flags  = 0;               /* Write operation. */
iomsgs[0].buf    = reg_addr;
iomsgs[0].len    = 1;

iomsgs[1].addr   = iomsgs[0].addr;  /* Same MCP23017-A */
iomsgs[1].flags  = I2C_M_RD;        /* Read operation */
iomsgs[1].buf    = rbuf;
iomsgs[1].len    = 1;

msgset.msgs      = iomsgs;
msgset.nmsgs     = 2;

rc = ioctl(fd,I2C_RDWR,&msgset);
if ( rc < 0 ) {
    perror("ioctl (2, I2C_RDWR)");

所示示例将iomsgs[0]定义为 1 个字节的写入,包含一个寄存器号。条目iomsgs[1]描述了从外设读取 1 个字节。这两条消息在一个ioctl(2)事务中执行。iomsgs[x]中的 flags 成员决定操作是读(I2C_M_RD)还是写(0)。

注意

不要混淆外围设备的内部寄存器号和外围设备的 I2C 地址。

每个iomsgs[x].addr成员必须包含一个有效的 I 2 C 外设地址。每个消息可能寻址不同的外设。第一条消息失败时,ioctl(2)将返回一个错误。因此,您可能不希望在一次ioctl(2)通话中包含多条信息,尤其是在涉及不同设备的时候。

成功时,返回值是成功执行的struct i2c_msg消息的数量。

摘要

从这一章中,你看到了在你的 Pi 中增加 16 个 GPIOs 可以通过增加一个芯片和一点连线来实现。考虑到附加板的成本,这可以大大节省您的项目。有了 MCP23017 的驱动程序支持,使用这些扩展 GPIOs 就像使用本地端口一样简单。

对于希望通过 I 2 C 直接与他的设备交互的开发人员来说,C API 就是这样做的。不管是通过驱动程序还是直接通过 C API,没有一个 PI 开发者想要访问 GPIO 端口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值