树莓派高级教程-全-

树莓派高级教程(全)

原文:Advanced Raspberry Pi

协议:CC BY-NC-SA 4.0

一、树莓派

Raspberry Pi 在两个层面上令人惊叹——信用卡大小的 SBC(单板计算机)的高级功能和它的价格。即使有今天的圆周率的竞争对手,树莓圆周率仍然是至高无上的,因为很少有人能击败它的价格。此外,它还享有强大的软件和社区支持。

价格是 Pi 的一个重要优势,而竞争对手并不总是欣赏这一点。爱好者和制造者正在以新的、有时是冒险的方式应用圆周率。一些刚开始的人不想因为一个新手的失误而失去他们的 SBC。在低 Pi 价位,亏损可以不灰心的吸收。想象一下,一名学生以 349 美元的价格购买了一台英特尔焦耳 1 (当它被提供时),并意外地举杯庆祝。这足以让大多数人放弃了!价格让每个人在学习中无所畏惧。

SBC 库存

在考虑关于 Raspberry Pi 中资源的细节之前,做一个高级清单是有用的。在这一章中,让我们列出当你购买 Pi 时你会得到什么。

在本书中,您将从两个角度研究每种资源:

  • 硬件本身——它是什么以及它是如何工作的

  • 背后的驱动软件和 API

在某些情况下,硬件背后会有一个或多个内核模块,形成设备驱动程序层。它们公开了应用和硬件设备之间的接口软件 API。例如,应用通过使用ioctl(2)调用与驱动程序通信,而驱动程序与总线上的 I2C 设备通信。/sys/class文件系统是设备驱动程序向应用公开自己的另一种方式。在第十二章研究 GPIO(通用输入/输出)时,你会看到这一点。

有些情况下,Raspbian Linux 中不存在驱动程序,这需要您使用“裸机”方法。例如,使用软件创建 PWM 信号。通过将 GPIO 寄存器映射到应用存储空间,可以直接从应用中获得所需的结果。直接访问和驱动程序访问各有利弊。

因此,虽然汇总清单仅列出了硬件设备,但您将在前面的章节中更详细地检查每个资源。

模型

硬件清单直接受到被检查设备型号的影响。这些年来,已经生产了几种型号,从型号 B 开始,然后是型号 a。从那时起,其他几种装置也开始出现,这些总结在表 1-1 中。更多细节可以在网上看到。 2

表 1-1

树莓 Pi 模型综述

|

模型

|

介绍

|

价格

|

中央处理器

|

社会学

|

混杂的

|
| --- | --- | --- | --- | --- | --- |
| 模型 A | 2013 年 2 月 | $25 | ARMv6Z 战斗机 | BCM2835 | 32 位 |
| 模型 A | 2014 年 11 月 | $20 | ARMv6Z 战斗机 | BCM2835 | 32 位 |
| B 型 | 2012 年 4 月 | $35 | ARMv6Z 战斗机 | BCM2835 | 32 位 |
|   | 2014 年 7 月 | $25 | ARMv6Z 战斗机 | BCM2835 | 32 位 |
| B 2 型 | 2015 年 2 月 | $35 | ARMv7-A 战斗机 | BCM2836 | 四路 32 位 |
| 型号 B 2 (1.2) | 2016 年 10 月 | $35 | ARMv8-A 突击步枪 | BCM2837 | 四路 32/64 位 |
| 模型 B 3 | 2016 年 2 月 | $35 | ARMv8-A 突击步枪 | BCM2837 | 四路 32/64 位 |
| 型号 B 3+ | 2018 年 3 月 | $35 | ARMv8-A 突击步枪 | BCM2837B0 | 四路 32/64 位 |
| 计算模块 1 | 2016 年 1 月 | $30 | ARMv6Z 战斗机 | BCM2835 | 32 位 |
| 计算模块 3 | 2017 年 1 月 | $30 | ARMv8-A 突击步枪 | BCM2837 | 四路 64 位 |
| 计算模块 3 Lite | 2017 年 1 月 | $25 | ARMv8-A 突击步枪 | BCM2837 | 四路 64 位 |
| 零(1.2) | 2015 年 11 月 | $5 | ARMv6Z 战斗机 | BCM2834 | 32 位 |
| 零(1.3) | 2016 年 5 月 | $5 | ARMv6Z 战斗机 | BCM2834 | 32 位 |
| 零瓦特 | 2017 年 2 月 | $10 | ARMv6Z 战斗机 | BCM2834 | 无线 32 位 |

树莓 Pi 型号 B

图 1-1 展示了树莓 Pi 模型 B,第 1 代。这款主板于 2012 年 4 月发布,售价 35 美元。请注意,它使用了大 SDHC(安全数字高容量)卡,显示在照片的左边。下面的插座如图 1-2 所示。GPIO 当时是一个 26 针的插头,和后来的 A 型一样。还有一个标记为 P5 的 4x2 接头,它有电源、接地和四个 GPIO 引脚。

img/326071_2_En_1_Fig1_HTML.jpg

图 1-1

树莓 Pi 型号 B(顶部),第 1 代

采用的 ARM 架构是 ARMv6Z。单个 32 位内核运行速度为 700 MHz,使用 256 MB SDRAM。2016 年 5 月,这一数字增加到了 512 MB。该板包括 2 个 USB 端口、15 针 MIPI 摄像头接口、LCD MIPI 接口、HDMI 和 RCA 复合视频输出、3.5 毫米音频插孔和 GPIOs。网络接口由一个 10/100 Mbit/s 的以太网适配器组成。

img/326071_2_En_1_Fig2_HTML.jpg

图 1-2

树莓 Pi 型号 B(下图),第 1 代

额定功率约为 700 毫安(3.5 瓦),取自微型 USB 连接器或接头带。

树莓 Pi 2 型号 B

树莓 Pi 2 型号 B 于 2015 年 2 月上市,售价 35 美元。该型号采用 ARMv7A 32 位架构。主要改进是支持四个 CPU(中央处理器)内核,运行频率为 900 MHz。另一个改进是 1 GB 的 SDRAM,允许更大的应用混合。图 1-3 示出了 pcb 的顶部,而图 1-4 示出了底部。

其他值得注意的变化包括用于 GPIO 的 Raspberry Pi 标准化 40 引脚排线。提供了四个 USB 端口,并将安装孔移到了 pcb(印刷电路板)上。

img/326071_2_En_1_Fig3_HTML.jpg

图 1-3

树莓 Pi 2 型号 B 的顶部

主板还使用微 SDHC 插槽来存储文件。图 1-3 显示其从 pcb 下方的左中部伸出。空闲时功耗降至 220 毫安(1.1 瓦),但在压力下会跃升至 820 毫安(4.1 瓦)。这就需要一个更大的电源适配器来为设备供电。

img/326071_2_En_1_Fig4_HTML.jpg

图 1-4

树莓 Pi 2 型号 B 的底部

树莓 Pi 3 型号 B

2016 年 2 月,树莓 Pi 3 Model B 上市,售价也是 35 美元。这提供了 arm V8-64/32 位架构。四核以 1.2 GHz 的速度运行,并配有 1 GB 的 SDRAM。另一个礼物是增加了 IEEE 802.11n-2009 无线支持和蓝牙 4.1。图 1-5 示出了 pcb 的顶侧,而图 1-6 示出了底部。

空闲时功耗为 300 mA (1.5 W),但在压力下会增加到 1.34 A (6.7 W)。图中显示了添加到 CPU 的散热器,但不包括在内。添加散热器可以防止内核降低时钟速度来调节温度。

img/326071_2_En_1_Fig6_HTML.jpg

图 1-6

树莓 Pi 3 型号 B 的底部

img/326071_2_En_1_Fig5_HTML.jpg

图 1-5

树莓 Pi 3 型号 B 的顶部

树莓 Pi 3 型号 B+

这款机型于 2018 年 3 月上市,价格也是 35 美元。是一款 64 位,1.4 GHz 四核,1gb SDRAM。网络端口支持 10/100/1000 Mbit/s 以太网,尽管由于其内部使用 USB 集线器,最高速度被限制在 300 Mbit/s 左右。无线支持现在包括用于双频 2.4/5 GHz 操作的 802.11ac。蓝牙升级到蓝牙 4.2 LS BLE。

空闲时功耗为 459 毫安(2.295 瓦),满负荷时增加到 1.13 安(5.661 瓦)。注意图 1-7 中 CPU 芯片上的金属帽。这有助于在不需要散热器的情况下散热(尽管使用散热器可能仍然是有益的)。pcb 的底面如图 1-8 所示。

img/326071_2_En_1_Fig8_HTML.jpg

图 1-8

树莓 Pi 3 型号 B+的底部

img/326071_2_En_1_Fig7_HTML.jpg

图 1-7

树莓 Pi 3 型号 B+的顶部

树莓派零度

并非每个 maker 项目都需要 64 位四核和 1gb SDRAM 的全部资源。首款树莓 Pi Zero 于 2015 年 11 月问世,后来于 2016 年 5 月升级。单价为 5 美元,是许多小型项目的理想 SBC。

Zero 是一款 ARMv6Z 架构(32 位)器件,以 1 GHz 运行单核。SDRAM 限制在 512 MB,对于大多数项目来说还是很够用的。第一个零缺少 MIPI 相机接口,该接口是在 2016 年修订版中添加的。

为了节省成本,没有焊接头带或连接器。如果最终用户需要,pcb 上还有复合视频的标记点。HDMI 输出通过迷你 HDMI 连接器提供,立体声音频通过 PWM(脉宽调制)GPIO 提供。零上也没有有线以太网端口。它可以通过使用一个微型 USB 端口和一个以太网适配器来提供。

电源通过另一个 Micro-USB 连接器提供,空闲时的功耗为 100 mA (0.5 W),压力下为 350 mA (1.75 W)。图 1-9 和 1-10 显示了树莓派零和树莓派零 w

img/326071_2_En_1_Fig10_HTML.jpg

图 1-10

树莓 Pi Zero(底部)和树莓 Pi Zero W(顶部)的底部

img/326071_2_En_1_Fig9_HTML.jpg

图 1-9

树莓派 Zero(底部)和树莓派 Zero W(顶部)的顶部

树莓派零度 W

Raspberry Pi Zero W 名称中的“W”是一个赠品,它通过 Zero 上的无线功能得到增强。它的定价是 10 美元。支持的无线标准有 802.11n 和蓝牙 4.1。与 Zero 一样,Zero W 没有有线以太网连接器,只有一个 Micro-USB 端口(另一个仅用于电源)。拥有 WIFI (WIFI 是 Wi-Fi 联盟的商标)接入大大增加了设备的通信多功能性。

哪个型号?

自然产生的问题是“买哪个型号?”答案很像买车——视情况而定。如果你正在寻找一台可以连接键盘、鼠标和显示器的廉价电脑,那么就买一台功能最强大的设备,比如 Raspberry Pi 3 型号 B+另一类涉及 AI(人工智能)或视频识别的项目是强大硬件的另一个例子。

为了建造一些必须在室外经受天气考验并拍摄鸟巢中鸟类的照片,那么带有 WIFI 连接的 Raspberry Pi Zero W 似乎是合适的。也许有其他项目根本不需要网络接入,在这些项目中,像零这样的最低价格是适用的。最好的消息是,你有很多低价的选择。

二、准备

虽然假设您已经开始使用 Raspberry Pi,但是在阅读本书的其余部分之前,您可能还需要做一些事情。例如,如果您通常使用笔记本电脑或台式计算机,您可能更喜欢从那里访问您的 Pi。

如果你计划完成本书中的大部分或全部项目,我强烈推荐你使用 Adafruit Pi T-Cobbler(本章后面会讲到)。该硬件以一种你可以在试验板上访问的方式断开 GPIO 线。

静态 IP 地址

标准的 Raspbian 映像提供了一个有能力的 Linux 系统,当插入网络时,它使用 DHCP(动态主机配置协议)自动为它分配一个 IP 地址。如果您想从台式机或笔记本电脑远程连接到它,那么 DHCP 分配的动态 IP 地址是有问题的。

有可下载的扫描网络的 Windows 程序。如果你用的是 Linux 或者 Mac 主机,可以用nmap扫描一下。下面是一个来自 Devuan Linux 的会话示例,使用了nmap命令。这里扫描的 IP 地址范围从 1 到 250:

root@devuan:~# nmap -sP 192.168.1.1-250

Starting Nmap 6.47 ( http://nmap.org ) at 2018-06-01 19:59 EDT
Nmap scan report for 192.168.1.1
Host is up (0.00026s latency).
MAC Address: C0:FF:D4:95:80:04 (Unknown)
Nmap scan report for 192.168.1.2
Host is up (0.044s latency).
MAC Address: 00:1B:A9:BD:79:02 (Brother Industries)
Nmap scan report for 192.168.1.77
Host is up (0.15s latency).
MAC Address: B8:27:EB:ED:48:B1 (Raspberry Pi Foundation)
Nmap scan report for 192.168.1.121
Host is up (0.00027s latency).
MAC Address: 40:6C:8F:11:B8:AE (Apple)
Nmap scan report for 192.168.1.80
Host is up.
Nmap done: 250 IP addresses (4 hosts up) scanned in 7.54 seconds
root@devuan:~#

在本例中,Raspberry Pi 被标识在 192.168.1.77 上,并带有完整的 MAC 地址(这些地址出现在报告“Raspberry Pi Foundation”的行上方)。虽然这种发现方法是可行的,但是它需要时间并且不方便。

如果您更喜欢将您的 Raspberry Pi 更改为使用静态 IP 地址,请参见第八章中的“有线以太网”部分以获取说明。

使用 SSH

如果您知道您的 Raspberry Pi 的 IP 地址,使用 nmap 发现了它,或者在您的 hosts 文件中注册了名称,您可以使用 SSH 登录到它。在本例中,我们以用户pi的身份从 Devuan Linux 机器登录到主机 192.168.1.77:

$ ssh pi@192.168.1.77
pi@192.168.1.77's password:
Linux raspberrypi 4.14.34-v7+ #1110 SMP Mon Apr 16 15:18:51 BST 2018 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law.
Last login: Fri Jun  1 20:07:24 2018 from 192.168.1.80
$

还可以使用scp命令将文件复制到 Raspberry Pi 或从 Raspberry Pi 复制文件。在 Raspberry Pi 上执行man scp操作,了解使用信息。

如果有 X-Window 服务器在运行,也可以在您的笔记本电脑/台式机上显示 X-Window 系统(X-Window)图形。(Windows 用户可以为此使用 Cygwin,从www.cygwin.com开始提供。)以 Linux 为例,首先配置 X-Window 服务器的安全性以允许请求。在这里,我将采用懒惰的方法,通过使用xhost命令允许所有主机(在不是 Pi 或者是另一个 Pi 的 Linux 机器上):

$ xhost +
access control disabled, clients can connect from any host
$

现在使用带-Y 选项的 ssh 登录到远程 Pi:

$ ssh pi@192.168.1.77 -Y
pi@192.168.1.77's password:
Warning: No xauth data; using fake authentication data for X11 forwarding.
Linux raspberrypi 4.14.34-v7+ #1110 SMP Mon Apr 16 15:18:51 BST 2018 armv7l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law.
Last login: Fri Jun  1 20:14:40 2018 from 192.168.1.80
$

从 Raspberry Pi 会话中,我们可以启动 xpdf,以便它在本地 Linux 机器上打开一个窗口:

$ xpdf &

如果失败,尝试在遥控器(pi)上导出一个显示变量,以通知软件 X-Window 服务器和屏幕的位置:

$ export DISPLAY=192.168.1.80:0

这里,我已经指定了 Devuan Linux 地址(或者,可以使用一个/etc/hosts名称),并指示 Raspberry Pi 使用 Linux 的显示号:0。我们在后台运行xpdf命令,这样我们就可以在当前的 SSH 会话中继续发出命令。同时,xpdf 窗口将在 Linux 屏幕上打开,而 xpdf 程序在 Raspberry Pi 上运行。

这并不能让您以图形方式访问 Pi 的桌面,但是对于开发人员来说,SSH 通常就足够了。如果你想远程图形访问树莓的桌面,一个选择是使用 VNC。

远程桌面

如果你已经在使用笔记本电脑或你最喜欢的台式电脑,你可以通过网络方便地访问你的 Raspberry Pi 的图形桌面。一旦 Raspberry Pi 的 VNC 服务器配置完毕,您所需要的就是在您的访问计算机上安装一个 VNC 客户端。这消除了连接到 Raspberry Pi 的键盘、鼠标和 HDMI 显示设备的需要。换句话说,您可以“无头”地运行 Pi。

让 VNC 工作需要一点设置。Raspbian Linux 已经采取措施让它变得简单。如果设置不正确,当您尝试登录时,VNC 查看器只会显示黑屏。

要使用 VNC,您必须安装桌面软件(GUI)。这也将使您更容易配置它。如果你安装了一个 Raspbian Lite 发行版,它将包括必要的桌面服务器软件。

启动图形桌面,然后从树莓图标(左上角),下拉菜单,选择“首选项”,并选择“树莓 Pi 配置。”这将弹出一个如图 2-1 所示的对话框。

img/326071_2_En_2_Fig1_HTML.jpg

图 2-1

树莓 Pi 配置对话框。请注意 Boot:选项是如何选中单选按钮“To Desktop”的。

您可能已经将“引导”选项设置为“到桌面”,但是现在单击它。这将导致桌面软件在启动后启动,以便您可以通过 VNC 连接到它。

将桌面配置为重启后启动后,您还需要如图 2-2 所示,通过点击“接口”选项卡来启用 VNC 服务器。

img/326071_2_En_2_Fig2_HTML.jpg

图 2-2

VNC 在对话框的接口选项卡中启用

在“接口”对话框中,单击标有“启用”的 VNC 单选按钮单击右下角的确定保存您的设置。然后重启你的 Pi。请等待重新启动和图形桌面启动的时间。

vnc 查看器

为了访问 VNC 服务器,在客户端需要一个相应的 VNC 浏览器。一种解决方案是使用来自以下网站的免费 realvnc 查看器:

https://www.realvnc.com/en/connect/download/viewer/

从该网站上,您可以找到您最喜爱的台式机的下载链接,如图 2-3 所示。忽略网站对“VNC 连接”的引用

img/326071_2_En_2_Fig3_HTML.jpg

图 2-3

VNC 浏览器的各种下载选项。忽略本页的“VNC 连接”信息。

根据您的桌面平台下载并安装。

当您启动查看器时,您会看到一个类似于图 2-4 所示的小对话框。一旦您成功登录,图标就会出现(如图所示)。

img/326071_2_En_2_Fig4_HTML.jpg

图 2-4

初始 VNC 浏览器对话框和一个以前使用的登录图标

成功连接并登录后,您应该会看到您的 Pi 桌面。图 2-5 举例说明了一个桌面窗口(在 Mac 上)。

img/326071_2_En_2_Fig5_HTML.jpg

图 2-5

在 Mac 上连接的树莓 Pi VNC 会话

如果您的 VNC 浏览器连接并似乎挂起,请耐心等待。VNC 屏幕更新的速度将取决于您的网络传输图形数据的能力。我发现我可以通过 WIFI 连接在 Pi 3 B+上使用 VNC,没有太多延迟。

虽然 VNC 的设施非常适合提供远程图形桌面访问,但不言而喻,其性能不适合观看视频或快速动作游戏。

黑色 VNC 屏幕

如果您将 Pi 的配置更改为引导至命令行模式(参见图 2-1 按钮“至 CLI”),而不是桌面模式,当您使用 VNC 查看器时,您将会遇到黑屏(参见图 2-6 )。选择命令行模式会导致桌面软件不运行,即使您可能启用了 VNC(图 2-2 “启用”)。

img/326071_2_En_2_Fig6_HTML.jpg

图 2-6

黑色的 VNC 浏览器屏幕表示桌面软件没有运行

要重新获得 VNC 桌面访问,您必须将配置更改为“至桌面”(图 2-1 ),然后重新启动。另一种方法是转到命令行(控制台或 ssh)并手动启动 X 服务器。从控制台登录,只需执行以下操作:

$ startx

从 ssh 会话中,您需要成为 root 用户(使用 sudo):

$ sudo -i
root@pi3bplus:~# startx

让服务器软件有时间启动后,VNC 查看器应该能够连接到您的桌面。当然,如果您注销 ssh 会话,服务器也会退出并终止桌面访问。

试验板设置

没有试验板和 Pi T-Cobbler 适配器也可以工作,但是你会发现有了高质量的试验板和适配器,实验会有趣得多。推荐的 T-Cobbler 可以从 Adafruit 购买,图 2-7 中有一个便宜的副本。

https://www.adafruit.com/product/2028

img/326071_2_En_2_Fig7_HTML.jpg

图 2-7

两个 Pi T-Cobbler 试验板适配器。请注意,来自中国(左)的单位需要一个电缆扭曲。推荐 Adafruit 单位(右)。

摘要

抛开这些细节不谈,本书的其余部分可以专注于 Raspberry Pi 必须提供的各种资源。Pi 提供了很多内容,让我们开始吧。

三、电源

系统中最常被忽视的部分之一往往是电源——至少在一切正常的情况下是如此。只有当事情出错时,电源才开始受到一些审查。

树莓派的主人需要给电源额外的尊重。与许多 AVR 级电路板不同,原始输入电压后接一个板载 5 V 调节器,Pi 期望其功率在输入端得到调节。Pi 确实包含板载调节器,但这些调节器可调节较低的电压(3.3 V 及更低)。

图 3-1 显示了相当脆弱的微型 USB 电源输入连接器。最初的型号 B 在连接器后面有一个大的圆形电容器,经常被抓住以发挥杠杆作用。避免这样做,因为许多人报告说是不小心“突然关掉”的。

img/326071_2_En_3_Fig1_HTML.jpg

图 3-1

微型 USB 电源输入

自最初的型号 B 以来的这些年里,已经生产了没有大电容器的其他型号。但它们都使用脆弱的微型 USB 电源输入,如图 3-1 所示。插入电源连接器时,请小心轻放。

计算能力

有时,电源是根据电压和功率处理能力(瓦特)来指定的。Pi 的 5 V 输入电压必须支持根据所用型号而变化的输入电流。表 3-1 总结了模型功率需求最小值。

表 3-1

Raspberry Pi 最低功耗要求汇总

|

模型

|

低值电流

|

电源

|
| --- | --- | --- |
| Pi 模型 B | 700 毫安 | 3.5 瓦 |
| Pi 2 模型 B | 820 毫安 | 4.1 瓦 |
| Pi 3 模型 B | 1.34 A | 6.7 瓦 |
| Pi 3 型号 B+ | 1.13 A | 5.65 瓦 |
| 圆周率零和 W | 350 毫安 | 1.75 瓦 |

让我们以瓦特为单位验证 Raspberry Pi 3 型号的电源数字(这不包括任何添加的外围设备):

$$ {\displaystyle \begin{array}{l}P=V\times I\ {}\kern1.5em =\kern0.5em 5\times 1.13\ {}\kern1.5em =\kern0.5em 5.65W\end{array}} $$

5.65 W 代表最低要求,因此我们应该额外超额配置 50%:

$$ {\displaystyle \begin{array}{l}P=5.65\times 1.50\ {}\kern1.5em =8.475W\end{array}} $$

额外的 50%产生大约 8.5 W 的功率需求

小费

为您的电源预留 50%的额外容量。电源变坏可能会导致损坏或其他一些问题。Pi 的一个常见电源相关问题是 SD 卡上的数据丢失。

当前需求

由于正在寻找的电源产生一个输出电压(5 V),您可能会看到具有广告的电流额定值而不是功率的电源适配器。在这种情况下,您可以简单地考虑 50%的额外电流(对于 Pi 3):

$$ {\displaystyle \begin{array}{l}{I}_{supply}={I}_{pi}\times 1.50\ {}\kern4.25em =\kern0.5em 1.13\times 1.50\ {}\kern4.25em =\kern0.5em 1.695A\end{array}} $$

为了仔细检查我们的工作,让我们看看这是否与我们之前计算的额定功率一致:

$$ {\displaystyle \begin{array}{l}P=V\times I\ {}\kern1.5em =\kern0.5em 5\times 1.695\ {}\kern1.5em =\kern0.5em 8.475W\end{array}} $$

结果一致。根据这一评估,您可以得出这样的结论:您至少需要一个 5 V 电源来产生以下结果之一:

  • 8.475 瓦或以上

  • 1.695 A 或更高(忽略外设)

能够满足任一要求的电源应该足够了。然而,你应该意识到,并不是所有的广告收视率是他们所看到的。廉价的供应品往往不能满足他们自己的要求,因此必须考虑额外的利润。

外围电源

功耗预算中必须考虑每个额外的耗电电路,尤其是 USB 外设。根据其类型,插入 USB 2 端口的给定 USB 外设可以期望高达 500 mA 的电流,假设它可以获得它。

众所周知,无线适配器非常耗电。使用时不要忘记键盘和鼠标,因为它们也会增加功耗。如果您已经连接了一个 RS-232 电平转换器电路(可能使用 MAX232CPE),您也应该在 3.3 V 电源预算中为这一小部分做预算。这将间接增加你的+5 V 预算,因为 3.3 V 稳压器是由它供电。(USB 端口使用+5 V 电源。)任何从你的树莓派汲取能量的东西都应该被记录下来。

3.3 伏电源

Raspberry Pi 的输入是调节后的 5 伏电压。但是 Pi 本身依赖于 3.3 V 电源,该电源由板载调节器提供。板载稳压器可为其他支持 IC 提供额外的较低电压,具体取决于型号。由于 3.3 V 电源是从 5 V 输入电源间接获得的,因此原始型号 B 的最大过量电流为 50mA;Pi 耗尽了调节器的剩余容量。

规划设计时,需要仔细预算 3.3 V 电源。每个 GPIO 输出引脚从该电源获得额外的 3 至 16 mA 电流,具体取决于其使用方式。电流预算较高的项目可能需要包括自己的 3.3 V 稳压器,从输入 5 V 输入供电。

供电 USB 集线器

如果您的电源预算因 USB 外设而捉襟见肘,您可能会考虑使用由供电的 USB 集线器。这样,集线器而不是你的树莓派为下游外设提供必要的电力。

并非所有的 USB 集线器都能与(Raspbian) Linux 兼容。内核需要与连接的 USB 集线器协作,因此软件支持至关重要。以下网页列出了已知工作正常的 USB 集线器:

http://elinux.org/RPi_Powered_USB_Hubs

电源适配器

对于树莓派的高电流四核型号,您只需购买一个合适的适配器。不要在你的高性能装备上浪费廉价或劣质的供应品。

对于低功耗 Pi,如旧型号 B、Zero 或 Zero W,您可能会尝试一些更便宜的解决方案。让我们检查一些选项。

不合适的供应

图 3-2 所示的例子是在易贝花 1.18 美元购买的,免运费(见即将发布的假货警告)。因此,使用它是很有诱惑力的。

img/326071_2_En_3_Fig2_HTML.jpg

图 3-2

型号 A1265 苹果适配器

这是一个具有以下额定值的适配器/充电器:

  • 型号 : A1265

  • 输入:100–240 伏交流电

  • 输出 : 5 V,1 A

当插上电源时,Raspberry Pi 的电源 LED 立即亮起,这是适配器(相对于充电器)的一个好迹象。电源的快速上升时间导致成功的上电复位。测量电压时,在+5 V 电源上的读数为+4.88 V。虽然不理想,但在可接受的电压范围内。(电压应在+5V-4.75 和 5.25 V 的 10%以内。)

当没有使用 HDMI 显卡时(使用串行控制台、SSH 或 VNC),苹果设备似乎工作得相当好。然而,我发现当使用 HDMI 并且 GPU 有工作要做时(例如,在桌面上移动一个窗口),系统往往会卡住。这清楚地表明适配器没有完全输送或调节得足够好。

警告

非常小心假冒的苹果充电器/适配器。Raspberry Pi 基金会已经看到被这些损坏的返回单元。有关视频和更多信息,请参见 www.raspberrypi.org/archives/2151

电子书适配器

一些人报告说使用电子书电源适配器是成功的。我也成功使用过 2 A 的 Kobo 充电器。

电源质量

虽然低价购买 USB 电源适配器是可能的,但更明智的做法是花更多的钱购买高质量的设备。为了节省几美元而扔掉你的树莓派或者经历随机的失败是不值得的。

如果你没有示波器,你将无法检查你的电源电流是干净还是肮脏。一个好的电源适配器比示波器便宜。不稳定/有噪音的电源会导致各种不明显的间歇性问题。

一个很好的开始是简单地谷歌“推荐电源树莓派。”做你的研究,并把你的 USB 外设包括在电力预算中。请记住,无线 USB 适配器消耗大量电流,高达 500 mA。

注意

一项随机互联网调查显示,无线 USB 适配器的功耗范围为 330 mA 至 480 mA。

电压测试

如果您有一个 DMM(数字万用表),在 Pi 上电后进行测试是值得的。如果您遇到问题,这可能是您应该做的第一件事。

按照以下步骤执行电压测试(假设现在标准化的 40 引脚排线用于引脚编号):

  1. 将 Raspberry Pi 的微型 USB 端口插入电源适配器的 USB 端口。

  2. 插入电源适配器。

  3. 测量 P1-02 (+5 V)和 P1-39 或 P1-06(接地)间的电压:预期+4.75 至+5.25 V

  4. 测量 P1-01 (+3.3 V)和 P1-39 或 P1-06(接地)间的电压:预期+3.135 至+3.465 V

警告

小心你的万用表表笔绕过 P1 的针脚。特别注意不要将+5 V 短接到+3.3 V 引脚,哪怕是几分之一秒。这样做会使你的 Pi 崩溃!为了安全起见,请使用杜邦线。

图 3-3 显示了在 Raspberry Pi 3 型号 B+上测试+5 V 电源。请注意+5V 电源(P1-02 引脚)的接头带上使用了红色杜邦导线。一根蓝色的杜邦线连接到地线。

img/326071_2_En_3_Fig3_HTML.jpg

图 3-3

测试 Raspberry Pi 3 型号 B+的+5V 电源

图 3-4 同样显示了从 Pi 的板载调节器测量调节后的 3.3 V 电源。图中红色的杜邦线连接到出现调节输出的 P1-01。

img/326071_2_En_3_Fig4_HTML.jpg

图 3-4

测试 Raspberry Pi 3 B+的+3.3 V 电源

附录 B 列出了 ATX 电源标准电压等级,包括+5±0.25V 和+3.3±0.165V,作为比较指南。

电池电量

由于 Raspberry Pi 的尺寸较小,因此可能需要使用电池电源来运行它(尤其是对于零 W 或零 W)。这样做需要一个监管机构和一些仔细的规划。为了满足 Raspberry Pi 的要求,您必须制定功耗预算。一旦你知道你的最大电流需求,你就可以充实其余的。以下示例假设需要 1 A。

要求

为了清楚起见,让我们列出电池电源必须满足的功率要求:

  • 电压 5 V,在 0.25 V 范围内

  • 电流 1 A

净空高度

最简单的方法是使用线性 LM7805 作为 5 V 调节器。但是这种方法也有缺点:

  • 输入电压之上必须有一定的裕量(约 2 V)。

  • 允许过多的净空会增加调节器的功耗,导致浪费电池电量。

  • 也可能导致较低的最大输出电流。

您的电池应提供最低 5+2 V (7 V)的输入。调节器的任何较低输入电压都会导致调节器“下降”并降至 5 V 以下,显然,6 V 电池输入是不行的。

LM7805 法规

图 3-5 显示了一个使用 LM7805 线性稳压器的非常简单的电池电路。电池电源进入中的 V ,从右边的引脚 1 输出+5 V 的调节输出。

img/326071_2_En_3_Fig5_HTML.jpg

图 3-5

使用 LM7805 稳压芯片的电池稳压电路

8.4 V 电池电源可以由 7 个 NiCad 电池串联而成,每个电池产生 1.2 V 电压,8.4 V 输入允许电池电压降至 7 V,不会超过 2 V 的最小裕量。

根据所选的确切 7805 调节器器件,典型的散热参数设置可能如下:

  • 输入电压:7–25v

  • 输出电压 : 1.5 A(散热)

  • 工作温度:125 摄氏度

务必在调节器上使用散热器,以便将热能散发到周围空气中。没有一个,调节器可以进入热关断状态,减少电流以防止损坏。发生这种情况时,输出电压将降至+5 V 以下。

请记住,电池消耗的功率大于负载接收的功率。如果我们假设 Raspberry Pi Zero 的功耗为 350 mA,则通过调节器从电池中汲取的电流最少也有 350 mA(这可能略高)。要知道,由于输入电压较高,稳压器正在消耗额外的能量。调节器(P R )和负载(P L )消耗的总功率如下:

$$ {\displaystyle \begin{array}{l}{P}_d={P}_L\times {P}_R\ {}\kern2em =\kern0.5em 5V\times 0.350A+\left(8.4V-5V\right)\times 0.350A\ {}\kern1.75em =\kern0.5em 1.75W+1.19W\ {}\kern1.75em =\kern0.5em 1.94W\end{array}} $$

调节器必须消除输入和输出电压之间的差异(1.19 W)。这些额外的能量加热调节器,并通过散热器散发出去。由于这个问题,设计者避免在线性调节器电路上使用高输入电压。

如果调节器在 7 V(输入)时的最大额定电流为 1.5 A,则调节器的最大功率约为 10.5 W,如果我们施加 8.4 V 而不是 7V 的输入电压,则我们可以得出 5 V 最大电流:

$$ {\displaystyle \begin{array}{l}{I}_{\mathrm{max}}=\frac{P_{\mathrm{max}}}{V_{in}}\ {}\kern3.25em =\kern0.5em \frac{10.5W}{8.4V}\ {}\kern3.25em =\kern0.5em 1.25A\end{array}} $$

由此我们发现,8.4 V 电池稳压器电路可以在输出端提供最大 1.25 A 的电流,而不会超过稳压器的额定功率。用 8.4 V 乘以 1.25 A 来说服自己这等于 10.5 W。

DC-DC 降压转换器

例如,如果应用是为数据采集而设计的,则希望它在给定的一组电池或充电周期内运行尽可能长的时间。开关调节器可能比线性调节器更合适,因为其效率更高。

图 3-6 显示了一个非常小的 pcb,长度约为 1.5 SD 卡。这个单位是从易贝购买的 1.40 美元,免费送货。以这样的价格,你为什么要建一个呢?

img/326071_2_En_3_Fig6_HTML.jpg

图 3-6

DC-DC 降压转换器

它们使用起来也很简单。转换器提供+和–输入连接以及+和–输出连接。在一个电压下输入功率,在另一个电压下输出功率。

但是不要马上把它接到你的树莓 Pi 上,直到你校准了输出电压。虽然它可能来预校准为 5 V,最好不要指望它。如果该装置产生更高的电压,您可能会烧坏 Pi。

通过 pcb 上的多匝调整电位计,可以轻松调整调节后的输出电压。当你读你的数字万用表的时候调整锅。

表 3-2 提供了我购买的设备的规格,供您参考。请注意宽范围的输入电压,以及它可以在低至–40°c 的温度下工作的事实,宽范围的输入电压和高达 3 A 的电流显然使这款器件非常适合连接到电压可能变化很大的太阳能电池板。

表 3-2

DC-DC 降压转换器规格

|

参数

|

福建话

|

最大

|

单位

|

参数

|

福建话

|

最大

|

单位

|
| --- | --- | --- | --- | --- | --- | --- | --- |
| 输入电压 | Four | Thirty-five | 伏特 | 输出脉动 |   | Thirty point nine | 妈 |
| 输入电流 |   |  3.0 | 安培数 | 负载调节 | ±0.5 | % |   |
| 输出电压 | One point two three | Thirty | 伏特 | 电压调整 | ±2.5 | % |   |
| 换能效率 |   | Ninety-two | % | 工作温度 | –40 | +85 | C |
| 开关频率 |   | One hundred and fifty | 千赫 | PCB 尺寸 |   | 45×20×12 | 毫米 |
|   |   |   |   | 净重 |   | Ten | g |

该规范声称高达 92%的转换效率。在输入端使用 15 V 电压,我进行了自己的测量实验。将装置调整到在输出端产生 5.1 V,读取表 3-3 中所示的读数。

表 3-3

从实验中获得的读数

|

参数

|

投入

|

输出

|

单位

|
| --- | --- | --- | --- |
| 电压 | Fifteen point one three | Five point one | 伏特 |
| 目前的 | Zero point one nine | Zero point four one | 安培数 |
| 电源 | Two point eight seven | Two point zero nine | 瓦茨 |

从表中我们可以看到输入端使用了更多的功率(2.87 W)。输出端使用的功率为 2.09 W。效率就变成了除法问题:

$$ \frac{2.09}{2.87}=0.728 $$

由此我们可以得出结论,测得的转换效率约为 72.8%。

如果使用 LM7805 调节器,我们能做得多好?以下是最好的情况估计,因为我没有这种情况下的实际电流读数。但我们知道,从调节器流出的电流至少有同样多的电流必须流入调节器(可能更多)。那么,LM7805 调节器理论上能做到的最好水平是什么呢?让我们在 5.10 V 下对 Raspberry Pi 应用同样的 410 mA 电流消耗,如表 3-4 所示。(这是在没有使用 HDMI 输出的情况下进行的。)

表 3-4

假设的 LM7805 功耗

|

参数

|

投入

|

输出

|

单位

|
| --- | --- | --- | --- |
| 电压 | Seven point one | Five point one | 伏特 |
| 目前的 | Zero point four one | Zero point four one | 安培数 |
| 电源 | Two point nine one | Two point zero nine | 瓦茨 |

这种最佳情况下的功效相当于:

$$ \frac{2.09}{2.91}=0.718 $$

LM7805 调节器的绝对最佳效率为 71.8%。但这是在其最佳输入电压下实现的。将输入电压提高至 12 V 会导致功耗大幅上升,从而产生 42.5%的效率(这一计算留给读者作为练习)。试图在 15.13 V 下操作 LM7805 调节器,就像我们对降压转换器所做的那样,会导致效率下降到 33.7%以下。显然,降压转换器在转换来自更高电压源的功率时效率要高得多。

电力不足的迹象

在论坛中,有报道称 ping 有时在桌面上无法工作(使用 HDMI),但在控制台模式下可以正常工作。此外,我发现如果移动桌面窗口,它们会冻结(HDMI)。例如,当您开始移动终端窗口时,移动会在中途停止,就好像鼠标停止工作一样。

这些都是树莓派电力不足的迹象。GPU 在活动时消耗更多的功率,执行加速图形。桌面冻结(GPU 饥饿)或网络接口失败(ping)。可能还有其他与 HDMI 活动相关的症状。

另一个被报道的问题是在启动后不久,树莓 Pi 被重置。随着内核启动,主板开始消耗更多的功率,这可能导致 Pi 被耗尽。 3

如果您在插入 USB 设备时失去了以太网连接,这也可能是电量不足的迹象。 4

虽然看起来 1 A 电源应该足以提供 700 mA 的 Raspberry Pi,但使用 2 A 电源会更好。许多电源根本达不到其宣传的额定功率。

微型 USB 电缆是另一个值得怀疑的东西。有些是用细导体制造的,这可能导致显著的电压降。如前面“电压测试”一节所示测量电压可能有助于诊断。尝试更高质量的电缆,看看是否有所改善。

摘要

本章简要介绍了您在使用 Raspberry Pi 时可能会遇到的一些电源问题。现在,您应该准备好做出关于电源适配器或电池供电选项的明智选择。

四、LED 和标题条

本章介绍 Raspberry Pi LEDs 和标题条。这些是从 Pi 到外部世界的重要接口。您可能希望为表 4-5 使用书签,该表概述了现代 P1 上的 GPIO(通用输入/输出)引脚。

状态指示灯

根据型号的不同,Raspberry Pi 有不同组的 LED 指示灯。表 4-1 提供了总体参考图表。 5 树莓 Pi Zero 和 Zero W 已经被混为一谈。

表 4-1

树莓 Pi LED 指示灯表

|

发光二极管

|

颜色

|

A

|

A+

|

B

|

B+

|

π2

|

圆周率 3

|

零/W

|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| okcat | 格林(姓氏);绿色的 | 是 | 是 | 是 | 是 | 是 | 是 | 是 |
| 压水反应堆 | 红色 | 是 | 是 | 是 | 是 | 是 | 是 | 不 |
| 联邦快递 | 格林(姓氏);绿色的 | 不 | 不 | 是 | 是 | 插口 | 插口 | 不 |
| 环 | 格林(姓氏);绿色的 | 不 | 不 | 是 | 插口 | 插口 | 插口 | 不 |
| 10M/100 | 黄色 | 不 | 不 | 是 | 不 | 不 | 不 | 不 |

正常/动作指示灯

这是 SD(安全数字)卡活动指示器。

电源 LED 灯

在最初的型号 B 和随后的型号 A 上,该 LED 连接到 3.3 V 调节器,并指示调节器有电。在以后的型号中,当输入功率低于 4.63 V 时,该 LED 将闪烁。

FDX 领导

这表明网络是全双工连接的。

链接 LED

链路指示灯指示 LAN(局域网)上是否有网络活动。

10M/100 LED

当网络以 100 Mbit/s 的速度运行时,这个黄色的 LED 会显示出来。

原始标题 P1

最初的 Raspberry Pi 型号 A 和 B 使用标识为 P1 的 13 x 2 排线,露出 GPIO 引脚。这包括 I2C、SPI 和 UART 外设以及电源连接。表 4-2 列出了这种老式收割台的连接。

表 4-2

原装 Pi GPIO 接头连接器 P1(俯视图)

|

左下角

|

|

左上角

|
| --- | --- | --- |
| 3.3 V 电源 | P1-01 | P1-02 | 5 V 电源 |
| GPIO 0 (I2C0 SDA)+ R1=1.8k | P1-03 | P1-04 | 5 V 电源 |
| GPIO 1 (I2C0 SCL)+ R2=1.8k | P1-05 | P1-06 | 地面 |
| GPIO 4 (GPCLK 0/1 线) | P1-07 | P1-08 | GPIO 14 (TXD) |
| 地面 | P1-09 | P1-10 | GPIO 15 (RXD) |
| GPIO 17 | P1-11 | P1-12 | GPIO 18 (PCM_CLK) |
| GPIO 21 (PCM_DOUT) | P1-13 | P1-14 | 地面 |
| GPIO 22 | P1-15 | P1-16 | GPIO 23 |
| 3.3 V 电源 | P1-17 | P1-18 | GPIO 24 |
| GPIO 10 (MOSI) | P1-19 | P1-20 | 地面 |
| GPIO 9(美索不达米亚) | P1-21 | P1-22 | GPIO 25 |
| GPIO 11 (SCKL) | P1-23 | P1-24 | GPIO 8 (CE0) |
| 地面 | P1-25 | P1-26 | GPIO 7 (CE1) |

修订的 GPIO 标题 P1

表 4-3 显示了对原 a 13 x 2 顶梁 P1 的修改。请注意,I2C 外设从 I2C0 变为 I2C1。

表 4-3

修改后的原始 Pi GPIO 接头连接器 P1(俯视图)

|

左下角

|

|

左上角

|
| --- | --- | --- |
| 3.3 V 电源,最大 50 mA | P1-01 | P1-02 | 5 V 电源 |
| GPIO 2 (I2C1 SDA1)+R1=1.8k | P1-03 | P1-04 | 5 V 电源 |
| GPIO 3 (I2C1 SCL1)+R2=1.8k | P1-05 | P1-06 | 地面 |
| GPIO 4 (GPCLK 0/1 线) | P1-07 | P1-08 | GPIO 14 (TXD0) |
| 接地 | P1-09 | P1-10 | GPIO 15 (RXD0) |
| GPIO 17(第 0 代) | P1-11 | P1-12 | GPIO 18(PCM _ CLK/EN1) |
| GPIO 27(EN2) | P1-13 | P1-14 | 接地 |
| GPIO 22(第三代) | P1-15 | P1-16 | GPIO 23(第四代) |
| 3.3 伏电源,最大 50 毫安 | P1-17 | P1-18 | GPIO 24(第五代) |
| GPIO 10 (SPI_MOSI) | P1-19 | P1-20 | 接地 |
| GPIO 9 (SPI_MISO) | P1-21 | P1-22 | GPIO 25(第六代)) |
| GPIO 11 (SPI_SCKL) | P1-23 | P1-24 | GPIO 8 (CE0_N) |
| 地面 | P1-25 | P1-26 | GPIO 7 (CE1_N) |

在表 4-3 中有 P1 连接器的旧型号也有显示在 vin 表 4-4 中的 P5 接头。

表 4-4

版本 2.0 P5 集管(俯视图)

|

左下角

|

|

左上角

|
| --- | --- | --- |
| (方形)5 V | P5-01 | P5-02 | 3.3 伏,50 毫安 |
| GPIO 28 | P5-03 | P5-04 | GPIO 29 |
| GPIO 30 | P5-05 | P5-06 | GPIO 31 |
| 地面 | P5-07 | P5-08 | 地面 |

修订版 2.0 GPIO 头 P1

从型号 B+和 A+开始,GPIO 接头被标准化为使用 40 针(20 x 2)接头。GPIO 引脚 ID_SD (GPIO0,P1-27)和 ID_SC (GPIO1,P1-28)保留用于板(HAT)检测/识别。这些引脚用于读取 HAT 中的 24Cxx 型 3.3 V 16 位可寻址 I2C EEPROM。

表 4-5

标准化 40 引脚 Raspberry Pi GPIO 接头(所有现代设备)

|

左下角

|

|

左上角

|
| --- | --- | --- |
| 3.3 V 电源,最大 50 mA | P1-01 | P1-02 | 5 V 电源 |
| GPIO 2 (I2C1 SDA1)+R1=1.8k | P1-03 | P1-04 | 5 V 电源 |
| GPIO 3 (I2C1 SCL1)+R2=1.8k | P1-05 | P1-06 | 地面 |
| GPIO 4 (GPCLK 0/1 线) | P1-07 | P1-08 | GPIO 14 (TXD0) |
| 接地 | P1-09 | P1-10 | GPIO 15 (RXD0) |
| GPIO 17(第 0 代) | P1-11 | P1-12 | GPIO 18(PCM _ CLK/EN1) |
| GPIO 27(EN2) | P1-13 | P1-14 | 地面 |
| GPIO 22(第三代) | P1-15 | P1-16 | GPIO 23(第四代) |
| 3.3 V 电源 | P1-17 | P1-18 | GPIO 24(第五代) |
| GPIO 10 (SPI_MOSI) | P1-19 | P1-20 | 接地 |
| GPIO 9 (SPI_MISO) | P1-21 | P1-22 | GPIO 25(第六代)) |
| GPIO 11 (SPI_SCKL) | P1-23 | P1-24 | GPIO 8 (CE0_N) |
| 接地 | P1-25 | P1-26 | GPIO 7 (CE1_N) |
| GPIO0 (ID_SD) | P1-27 | P1-28 | GPIO1 (ID_SC) |
| GPIO5 | P1-29 | P1-30 | 地面 |
| GPIO6 | P1-31 | P1-32 | GPIO12 (PWM0) |
| GPIO13 (PWM1) | P1-33 | P1-34 | 地面 |
| GPIO19(味噌) | P1-35 | P1-36 | GPIO16 |
| GPIO26 | P1-37 | P1-38 | GPIO20 (MOSI) |
| 接地 | P1-39 | P1-40 | GPIO21 (SCLK) |

安全模式

Raspbian Linux 曾经支持一个安全模式,但是在 2014 年 3 月已经被移除。然而,NOOBS 可能仍然支持它。 6

引脚 P1-05,GPIO3 专用于引导序列。将此引脚接地或将其跳线至 P1-06(地)会导致引导序列使用安全模式引导程序。如果引脚用于其他目的,您可以通过配置参数来防止这种情况:

avoid_safe_mode=1

当你使用电源插头(如 P1-01 或 P1-02)时,请小心不要意外接地。如果您的 Pi 无法响应安全模式,可能是由于制造错误或缺少固件支持。看到这条消息:

www.raspberrypi.org/phpBB3/viewtopic.php?f=29&t=12007

当跳线调用安全模式时,config.txt文件被忽略,除了avoid_safe_mode参数。此外,这种模式会覆盖内核命令行,并加载kernel_emergency.img。如果该文件不可用,则使用kernel.img代替。

此功能的目的是允许用户克服配置问题,而不必在另一台机器上编辑 SD 卡来进行纠正。被引导的紧急内核是一个 BusyBox 映像,挂载了/boot以便进行调整。此外,如果有必要,可以修复或挂载/dev/mmcblk0p2根文件系统分区。

逻辑电平

用于 GPIO 引脚的逻辑电平为 3.3 V,并且不能容忍 5 V TTL 逻辑电平。Raspberry Pi pcb 设计用于插入 pcb 扩展卡(HATs ),或与 3.3 V 逻辑精心接口。输入电压参数 V IL 和 V IH 在第十一章中有更详细的描述。

复位时的 GPIO 配置

Raspberry Pi GPIO 引脚可以通过软件控制配置为输入或输出,具有上拉或下拉电阻,或者承担一些特殊的外设功能。复位后,只有 GPIO14 和 15 被分配了特殊功能(UART)。然而,启动后,软件甚至可以根据需要重新配置 UART 引脚。

当 GPIO 引脚配置为输出时,它可以驱动的电流量有限(源电流或吸电流)。默认情况下,当引脚配置为输出时,每个 P1 GPIO 配置为使用 8 mA 驱动器。第十一章有更多关于这个的信息。

注意

Raspbian 默认单线总线是 GPIO 4 (GPCLK0)引脚 P1-07。

单线驱动器

单线驱动器的默认 GPIO 引脚过去被硬编码为使用 GPIO4。现在可以进行配置了。

摘要

本章介绍了树莓 Pi 上的 LED 指示灯和标题条。虽然所有新的 Pi 现在都使用 20 x 2 GPIO 头条,但最初的头是为了参考目的而描述的。一个 Pi 永远不会老到不能工作!

五、同步动态随机访问内存

最初的 Model B Rev 2.0 Raspberry Pi 有 512 MB 的 SDRAM(同步动态随机访问存储器),而旧版本和 Model A 有 256 MB。现代的 Pi 现在配备了 1 GB 的内存,除了 Raspberry Pi Zero,它有 512 MB。相比之下,AVR 类 ATmega328P 有 2 KB 的静态 RAM!

一般 Pi 开发人员并不太关心内存硬件,因为操作系统为管理程序使用提供了丰富的工具。然而,让我们研究一些有用的 Raspbian Linux 接口,它们告诉我们内存是如何被利用的。您还将研究如何直接从您的 Linux 应用访问内存映射的 ARM 外设。

/proc/meminfo

伪文件/proc/meminfo为我们提供了关于内存使用的信息。这些信息因架构和内核使用的编译选项而有所不同。让我们研究一个由运行 Raspbian Linux 的 Raspberry Pi 3 Model B+生成的示例:

$ cat /proc/meminfo
MemTotal:         949452 kB
MemFree:          631676 kB
MemAvailable:     756320 kB
Buffers:           20532 kB
Cached:           158004 kB
SwapCached:            0 kB
Active:           170108 kB
Inactive:         107020 kB
Active(anon):      99576 kB

Inactive(anon):    13172 kB
Active(file):      70532 kB
Inactive(file):    93848 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:        102396 kB
SwapFree:         102396 kB
Dirty:                40 kB
Writeback:             0 kB
AnonPages:         98596 kB
Mapped:            79016 kB
Shmem:             14152 kB
Slab:              22444 kB
SReclaimable:      10640 kB
SUnreclaim:        11804 kB
KernelStack:        1768 kB
PageTables:         3376 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:      577120 kB
Committed_AS:     809528 kB
VmallocTotal:    1114112 kB
VmallocUsed:           0 kB
VmallocChunk:          0 kB
CmaTotal:           8192 kB
CmaFree:            6796 kB

所有显示的内存值右边都有单位 KB ,表示千(1,024)字节。这些报告值在网上有详细描述。 7 但是让我们总结一些有趣的价值。

记忆总数

MemTotal行表示可用内存的总量,减去一些保留的二进制区域。注意,分配给 GPU 的内存不计入MemTotal。一些所有者可能会选择分配最少 16 MB 给 GPU,以便在不使用图形桌面时获得更多可用内存。默认分配 64 MB。

如果进一步细分,考虑分配给 GPU 的内存,我们会发现大约有 32 MB 的内存未计算在内,如表 5-1 所示。由于 GPU 寻址内存的方式不同,这将因 Pi 型号和分配的 GPU 内存而异。

表 5-1

GPU 和主内存故障

|

记忆

|

B 型

|

评论

|
| --- | --- | --- |
| 同步动态随机访问内存(Synchronous Dynamic random access memory) | 1,048,576 KB | 配备硬件的存储器 |
| 记忆总数 | 949,452 KB | /proc/meminfo |
| 差异 | 99,124 KB | 剩余内存 |
| gpu_mem | 65,536 KB | /boot/config.txt(默认情况下 gpu_mem=64) |
| 差异 | 33,588 KB | 下落不明 |

记忆自由

MemFree通常表示英特尔 x86 平台上以千字节为单位的LowFree + HighFree内存的总和。对于 ARM,这只是表示用户空间程序可用的内存量。

缓冲

该值表示内核中用于原始磁盘块等的临时缓冲区。这个值不应该比大约 20 MB 大太多。 8

藏起

该值表示已缓存的读取文件内容(页面缓存)。这不包括为SwapCached报告的内容。

交换的

显示的SwapCached值代表换出的内存,现在又换回。为了提高效率,这些内存页面仍然由交换磁盘空间表示,以备再次需要。该值被报告为零的事实是一个可喜的迹象,表明没有发生交换或者交换不再相关。

活跃的

Active内存值表示最近使用的不回收的内存,除非绝对必要。

不活动的

该值表示不活动的内存,并且可能在需要内存时被回收。

活动(匿名)

该值表示不是由文件备份且处于活动状态的内存。除非绝对必要,否则不会回收活动内存。

不活动(匿名)

该值表示没有文件备份且不活动的内存。如果需要内存,非活动内存可以回收。

活动(文件)

该值表示活动的文件支持内存。只有绝对需要时,才会回收活动内存。

不活动(文件)

该值表示由文件支持的非活动内存。当需要内存时,非活动内存可以回收。

不可战胜的

此数量反映了无法回收的内存总量。例如,被锁定的内存不能被回收。

锁定的

该值报告锁定的内存量。

交换总量

该值以千字节为单位报告可用的交换空间总量。

交换自由

该值以千字节为单位报告剩余的可用交换空间量。

肮脏的

该值表示已修改并等待写入磁盘的内存的千字节数。

回复

该值报告写回磁盘的内存量(以千字节为单位)。

匿名页面

这表示映射到用户空间的非文件支持的内存页面。

计划

该值报告已经映射到内存中的文件。这可能包括库代码。

施姆

这个参数似乎没有被很好地记录。但是,它表示以千字节为单位的共享内存量。

平板

这个参数被称为“内核数据结构缓存”

可索赔的

这个参数被描述为“可能被回收的Slab的一部分,例如缓存。”

阳光回收

这个参数被描述为“在内存压力下不能被回收的部分”

内核堆栈

该值报告内核堆栈使用的内存。

页面表格

该值报告内核中使用的页表所需的内存量。显然,随着需要管理的内存越来越多,就有更多的内存专用于页表。

NFS _ 不稳定

该值表示“发送到服务器,但尚未提交到稳定存储的 NFS 页面”该示例数据表明没有使用 NFS。

活力

这将报告用于“块设备反弹缓冲区”的内存

写回 Tmp

此参数报告 FUSE 用于“临时写回缓冲区”的内存

承诺限制

文件指出:

根据过量使用比率(vm.overcommit_ratio),这是系统上当前可分配的内存总量。只有在启用了严格过量使用会计时,才遵守该限制(模式 2vm.overcommit_memory)。CommitLimit通过以下公式计算:

$$ CommitLimit=\left( vm. overcommit_ ratio\times PhysicalRAM\right)+ swap $$

例如,一个具有 1 GB 物理 RAM 和 7 GB 交换空间且vm.overcommit_ratio为 30 的系统将产生 7.3 GB 的CommitLimit。有关更多细节,请参见vm/overcommitaccounting中的内存过量使用文档。

该公式可以写成如下形式:

$$ C=\left(R\times r\right)+S $$

此公式的元素描述如下:

  • C 是超量承诺限制。

  • R 是可用的物理 RAM(MemTotal)。

  • S 是可用的交换空间(SwapTotal)。

  • r 是超量承诺比率百分比(以分数表示)。

/proc/meminfo数据中未报告过量使用比率 r。为了获得这个比率,我们查阅另一个伪文件。这个例子取自 2.0 版的模型 B,但它似乎是所有 Pi 的通用值:

$ cat /proc/sys/vm/overcommit_ratio
50

值 50 应解释为 r = 0.50 (50%)。

使用超量使用公式,可以计算出可用交换空间的 S 值:

$$ {\displaystyle \begin{array}{l}S=C-\left(R\times r\right)\ {}\kern1em =577120-\left(949452\times 0.50\right)\ {}\kern1em =82394\kern0.125em KB\end{array}} $$

用户可以通过在伪文件中写入一个值来配置超量使用比率。本例将比率更改为 35%:

$ sudo -i
# echo 35 >/proc/sys/vm/overcommit_ratio
# cat /proc/sys/vm/overcommit_ratio
35

已提交 _ 作为

该参数描述如下:

系统上当前分配的内存量。提交的内存是由进程分配的所有内存的总和,即使它还没有被它们“使用”。malloc()的 1 GB 内存,但只使用了 300 MB 的进程只会显示为使用了 300 MB 的内存,即使它有为整个 1 GB 分配的地址空间。这 1 GB 是由虚拟机“提交”的内存,分配应用可以随时使用。在系统上启用严格过量使用时(vm.overcommit_memory中的模式 2),将不允许超过CommitLimit(详见上文)的分配。如果需要保证在成功分配内存后,进程不会因为缺少内存而失败,这是非常有用的。

VmallocTotal

这表示分配的虚拟内存地址空间的总量。

虚拟专用

这是正在使用的虚拟内存量,以千字节为单位。

VmallocChunk

该值报告了vmalloc区域的最大大小,单位为千字节。

物理内存

通常,物理内存不是应用程序员关心的问题,因为操作系统及其驱动程序提供了一种抽象的、通常是可移植的访问方式。然而,如果没有这种支持,则需要直接访问 PWM 控制器等外设。

图 5-1 展示了原始 Pi 模型 B 在 Raspberry Pi 上使用的物理寻址(为简单起见)。SDRAM 从物理地址零开始,直到 ARM/GPU 分割点(第十七章定义了分割点)。在 Pi Model B 上,ARM 外设从地址 0x20000000 开始映射到物理存储器,这个起始地址是程序员非常感兴趣的。

img/326071_2_En_5_Fig1_HTML.jpg

图 5-1

物理内存布局

对于现代 Pi 模型,我们不能假定起始外设地址为常数。在本书的后面,将展示一种在运行时确定该值的方法。

在标有外设的区域,表 5-2 中所示的偏移量和地址是我们感兴趣的。

表 5-2

Raspberry Pi 型的外设失调

|

外围的

|

抵消

|

地址

|

描述

|

c 偏移宏

|
| --- | --- | --- | --- | --- |
| 基地 | 0x00000000 | 0x20000000 | 起始地址 | bcm 2708 _ 基本周长 |
| PADS_GPIO | 0x00100000 | 0x20100000 | 衬垫底座 | PADS_GPIO_BASE |
| GPIO 00..27 | 0x0010002C | 0x2010002C | GPIO 00..27 个垫子 | PADS_GPIO_00_27 |
| GPIO 28..45 | 0x00100030 | 0x20100030 | GPIO 28..45 个垫子 | PADS_GPIO_28_45 |
| GPIO 46..53 | 0x00100034 | 0x20100034 | GPIO 46..53 个垫子 | PADS_GPIO_46_53 |
| 时钟 | 0x00101000 | 0x20101000 | 时钟寄存器 | CLK_BASE |
| GPIO | 0x00200000 | 0x20200000 | GPIO 寄存器 | GPIO_BASE |
| gppu | 0x00200025 | 0x20200025 | 上拉使能 |   |
| gppuclk 0 | 0x00200026 | 0x20200026 | 上拉时钟 0 |   |
| gppuclk 1 | 0x00200027 | 0x20200027 | 上拉时钟 1 |   |
| 脉宽调制 | 0x0020C000 | 0x2020C000 | PWM 寄存器 | PWM_BASE |

存储器交换

要在 Linux 下获得对物理内存的访问,您需要使用/dev/mem字符设备和mmap(2)系统调用。这里显示的是/dev/mem节点:

$ ls -l /dev/mem
crw-r----- 1 root kmem 1, 1 Jun  5 20:24 /dev/mem

从显示的所有权信息来看,很明显您需要 root 权限来访问它。这是明智的,因为一个进程可能会导致对物理内存的直接访问。在应用如何处理这种内存访问时要小心。

这里显示了mmap(2)系统调用 API:

#include <sys/mman.h>

void ∗mmap(
  void     ∗addr,    /∗Address to use ∗/
  size_t    length,   /∗Number of bytes to access ∗/
  int       prot,     /∗Memory protection ∗/
  int       flags,    /∗Option flags ∗/
  int       fd,       /∗Opened file descriptor ∗/
  off_t     offset    /∗Starting off set ∗/
) ;

与其查看这个有点复杂的系统调用可用的所有选项和标志,不如让我们看看我们在下面的代码中使用的选项和标志:

static char ∗map = 0;

static void
gpio_init() {
    int fd;
    char ∗map;

    fd = open("/dev/mem",O_RDWR|O_SYNC) ;   /∗Needs root access ∗/
    if ( fd < 0 ) {
        perror("Opening /dev/mem") ;
        exit(1) ;
    }

    map = (char ∗) mmap(
        NULL,                  /∗ Any address ∗/
        BLOCK_SIZE,            /∗ # of bytes ∗/
        PROT_READ|PROT_WRITE,
        MAP_SHARED,            /∗Shared ∗/
        fd,                    /∗ /dev/mem ∗/
        GPIO_BASE              /∗ Offset to GPIO ∗/
    ) ;

    if ( (long)map == −1L ) {
        perror("mmap(/dev/mem)");
        exit(1) ;
    }

    close(fd);
    ugpio = (volatile unsigned ∗)map;
}

这段代码执行的第一件事是打开设备驱动程序节点/dev/mem。它被打开用于读和写(O_RDWR),选项标志O_SYNC要求对这个文件描述符的任何write(2)调用都会导致阻塞调用程序的执行,直到它完成。

地址

接下来,调用mmap(2)调用。address 参数提供了NULL(零),这样内核就可以选择将它映射到调用者地址空间的何处。如果应用要指定一个要使用的起始地址,而内核不能使用它,那么系统调用将会失败。返回起始地址,并将其分配给前面清单中的字符指针map

长度

在本例中,参数 2 由宏BLOCK_SIZE提供。这是您希望映射到地址空间的字节数。这在前面的程序中定义为 4 KB:

#define BLOCK_SIZE (4∗1024)

虽然应用可能不需要映射完整的 4 KB 物理内存,但mmap(2)可能坚持使用页面大小的倍数。这可以通过命令行进行验证,如下所示:

$ getconf PAGE_SIZE
4096

程序可以通过使用sysconf(2)系统调用直接确定这一点:

#include <unistd.h>

    ...
    long sz = sysconf(_SC_PAGESIZE);

保护

第三个mmap(2)参数由标志PROT_READPROT_WRITE提供。这表明应用希望对内存映射区域进行读写访问。

旗帜

flags参数提供值MAP_SHARED。这允许对底层映射的非独占访问。

文件描述符

此参数提供了要映射到内存中的基础打开文件。在这种情况下,我们通过使用打开的设备驱动程序节点/dev/mem将一个物理 ARM 内存区域映射到我们的应用中。

抵消

最后一个参数指定了物理内存中开始访问的位置。对于 GPIO 寄存器,它是原始 Pi 模型 b 上的地址 0x20200000。

返回值

如果成功,返回值将是一个应用地址,指向我们请求的物理内存区域。应用员不需要关心这个地址是什么,除了保存和使用它进行访问。

返回值也用于指示失败,因此应对此进行检查和处理:

if ( (long) map == –1L )  {
    perror("mmap(/dev/mem)");
    exit(1);
}

返回的地址(指针)map被转换成一个长整数并与-1L进行比较。这是指示发生错误的神奇值。错误代码在errno中找到。

不稳定的

GPIO 初始化代码的最后一部分将地址map分配给另一个变量ugpio,如下所示:

ugpio = (volatile unsigned ∗)map;

ugpio已在程序中提前定义:

static volatile unsigned ∗ugpio = 0;

关于这一点有两件事值得注意:

  • 数据类型是无符号的int(Pi 上的 32 位)。

  • 指向的数据被标记为 volatile。

由于 Pi 寄存器的大小为 32 位,因此将其作为 32 位字来访问通常更方便。无符号数据类型非常适合这种情况。但是在使用这个指针时要小心偏移量,因为它们是而不是字节偏移量。

关键字volatile告诉编译器不要通过指针变量优化对内存的访问。想象这样一段代码,它读取一个外设寄存器,然后再次读取同一个寄存器,看看是否有事件发生。一个优化编译器可能会对自己说,“我已经在 CPU 寄存器 R 中有了这个值,所以我就用它,因为它更快。”但这段代码的效果是,它永远不会看到外设寄存器中的位发生变化,因为数据没有被取回到寄存器中。volatile关键字强制编译器检索该值,即使使用仍然在寄存器中找到的值会更快。

虚拟内存

在上一节中,您了解了如何在应用中访问物理内存,前提是您拥有访问权限(root 或setuid)。Broadcom Corporation PDF 手册“BCM2835 ARM 外设”第 5 页也在右侧显示了一个虚拟内存布局。这不应与之前检查的物理内存布局相混淆。使用mmap(2).可以通过/dev/kmem驱动节点访问虚拟内存

摘要

一些参数如Buffers会影响 Raspbian Linux 在 Pi 上的性能。例如,为从磁盘读取的文件数据分配了多少内存?其他配置参数决定了有多少 SDRAM 专用于 GPU。

可能内存分配最重要的方面是有多少内存可供开发者应用使用。因此,MemFree的值是一个有用的度量。当超过物理内存限制时,交换参数就会变成感兴趣的度量。

最后,介绍了使用mmap(2)直接访问 Raspberry Pi 外设。在 Raspbian Linux 获得 PWM 等外设的设备驱动程序之前,直接访问技术将是必要的。即使有驱动程序支持,有时也有充分的理由直接访问外设寄存器。

六、中央处理器

自从第一个 B 型和 A 型后继者以来,已经出现了几个树莓派的型号。在本章中,将介绍 ARM 架构以及您的 Pi 所支持的 CPU 特性。然后将介绍用于管理应用中 CPU 的 Linux API(应用编程接口)(线程)。

/proc/cpuinfo

Raspbian Linux 在/proc/cpuinfo提供了一个不错的字符设备,用来列出关于你的 CPU 的信息。清单 6-1 中提供了一个取自树莓 Pi 3 型号 B+的样本。您不需要 root 访问权限来读取这些信息。

$ cat /proc/cpuinfo
processor        : 0
model name       : ARMv7 Processor rev 4 (v7l)
BogoMIPS         : 38.40
Features         : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 \ idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer  : 0x41
CPU architecture : 7
CPU variant      : 0x0
CPU part         : 0xd03
CPU revision     : 4
...
Hardware         : BCM2835
Revision         : a020d3
Serial           : 00000000d4b81de4

Listing 6-1Session output listing /proc/cpuinfo on a Raspberry Pi 3 model B+

有四组处理器标识为 0 到 3(图中只显示了第一组)。文件底部列出了硬件、版本和序列号。

处理器组中有一行标记为“型号名称”在本例中,我们看到列出了“ARMv7 处理器版本 4 (v7l)”。同样在底部,列出了值为“BCM2835”的“硬件”。让我们花点时间来讨论一下架构名称的含义。

ARM 架构

架构是一种设计。在这种情况下,它定义了 ARM 程序员的模型,包括寄存器、寻址、存储器、异常和操作的所有方面。在 Raspberry Pi 环境中,使用了不同的 ARM 架构,如表 6-1 所列。

表 6-1

Raspberry Pi ARM 架构和实现

|

架构名称

|

总线尺寸

|

指令集

|

社会学

|
| --- | --- | --- | --- |
| ARMv6Z 战斗机 | 32 位 | 手臂和拇指(16 位) | BCM2835 |
| ARMv7-A 战斗机 | 32 位 | 手臂和拇指(16 位) | BCM2836 |
| ARMv8-A 突击步枪 | 32/64 位 | AArch32(与 ARMv7-A 兼容)和 AArch64 执行状态。 | BCM2837 |

总线尺寸和指令集列中总结了设计和一般功能。每一种新的架构都为指令集和其他处理器特性增加了新的功能。

SoC(片上系统)列标识了 Broadcom 架构的实现

新的 ARMv8-A 架构有两种可能的运行状态:

  • AArch32,与 ARMv7-A 架构兼容。

  • AArch64,采用全新 ARM 64 位指令集。

执行状态必须在系统启动时选择,这就是为什么 Raspbian Linux 在 Raspberry Pi 3 模型 B+上报告以下内容:

$ uname -m
armv7l

它运行 AArch32 执行状态,以便与 32 位 Raspbian Linux 代码兼容。希望有一天,我们能看到真正的 64 位 Raspbian Linux。

建筑后缀

较新的架构有一个后缀来标识 Cortex 系列:

  • “A”代表 Cortex-一系列应用处理器。

  • “R”代表 Cortex-R 系列的实时处理器。

  • “M”代表 Cortex-M 系列低功耗、微控制器处理器。

在架构名称 ARMv7-A 或 ARMv8-A 中,我们看到它们属于应用处理器系列。这些是完全有能力的成员,而 Cortex-R 和 Cortex-M 家族通常是子集或专门从事少数领域。

特征

再次查看/proc/cpuinfo 输出,注意标有“Features”的那一行。它有一个识别特征的名称列表,这些特征对于 CPU(中央处理器)是唯一的。表 6-2 列出了一些你可能会看到的手臂特征。

表 6-2

可能在/proc/cpuinfo 中列出的 ARM 特性

|

功能名称

|

描述

|
| --- | --- |
| 一半 | 半字加载和存储 |
| 拇指 | 16 位 Thumb 指令集支持 |
| 法斯特穆特 | 32x32 产生 64 位乘法支持 |
| 心室充盈压 | 早期 SIMD 向量浮点指令 |
| 处理器 | DSP 扩展 |
| 氖 | 高级 SIMD/氖支持 |
| vfpv3 | VFP 版本 3 支持 |
| 坦克激光瞄准镜(Tank Laser-Sight 的缩写) | TLS 寄存器 |
| vfpv4 | 具有快速上下文切换的 VFP 版本 4 |
| 伊迪瓦 | ARM 模式下的 SDIV 和 UDIV 硬件部门 |
| idivt | Thumb 模式下的 SDIV 和 UDIV 硬件部门 |
| vfpd32 | 具有 32 个 D 寄存器的 VFP |
| LPA(巴勒) | 大型物理地址扩展(32 位架构上的 4 GB 以上物理内存) |
| evtstrm | 使用通用架构定时器的内核事件流 |
| crc32 | CRC-32 硬件加速支持 |

执行环境

与 CPU 的概念相联系的是程序执行本身。在查看程序执行之前,先从高层次上了解一下执行上下文。图 6-1 显示了正在执行的程序的运行环境。

img/326071_2_En_6_Fig1_HTML.jpg

图 6-1

程序执行上下文

在地址空间的最低端是包含程序代码的“文本”区域。虚拟内存的这个区域是只读的,除了可执行代码之外,它还保存只读的程序常量。

下一个区域(地址递增)包含未初始化数组、缓冲区、静态 C 变量和extern存储的块。

在内存的高端是程序的环境变量,比如PATH。通过使用getenv("PATH")并打印返回的地址,您可以很容易地检查这一点。它的地址可能是您的 Raspberry Pi 应用中最高的地址,除非是另一个环境变量。

在此之下,主程序的堆栈开始向下增长。每个函数调用都会导致在当前堆栈框架下创建一个新的堆栈框架。

如果你现在给程序添加一个线程,就必须为它分配一个新的堆栈。在 Pi 上的实验表明,第一个线程堆栈是在主堆栈的起点以下大约 123 MB 处创建的。第二个线程的堆栈分配比第一个线程低大约 8 MB。每个新线程的堆栈(默认情况下)被分配 8 MB 的堆栈空间。

动态分配的内存是从位于static / extern区域和堆栈底端之间的中分配的。

线

每个程序都有一个主执行线程。但是有时需要多线程的性能优势,尤其是在具有四个内核的 Pi 上。

pthread 标题

所有pthread函数都需要以下头文件:

#include <pthread.h>

当使用pthreads链接程序时,添加链接器选项:

  • -lpthread

  • to链接 pthread 库。

pthread 错误处理

pthread 例程成功时返回零,失败时返回错误代码。值errno是用于这些调用的而不是

这背后的原因可能是人们认为传统的 Unix errno方法将在不久的将来被淘汰(那时 POSIX 线程正在被标准化)。errno的最初用途如下:

extern int errno;

然而,这种方法不适用于线程程序。想象两个线程同时用open(2)打开文件,失败时设置errno值。两个线程不能共享同一个errnoint值。

与其以这种方式改变大量已经使用errno的代码,不如实现其他方法,为每个线程提供自己的私有errno副本。这就是今天使用errno的程序必须包含头文件errno.h的原因之一。头文件负责定义对errno的线程特定引用。

因为 pthread 标准是在errno解决方案普遍出现之前开发的,所以 pthread 库直接返回错误代码,成功时返回零。如果今天要重写 Unix,所有的系统调用可能都是这样的。

pthread_create(3)

函数pthread_create(3)用于创建一个新的执行线程。函数调用看起来比实际情况更令人畏惧:

int pthread_create(
  pthread_t ∗thread,
  const pthread_attr_t ∗attr,
  void ∗(∗start_routine)(void ∗),
  void ∗arg
);

pthread_create(3)的调用创建了一个新的堆栈,设置了寄存器,并执行其他内务处理。让我们来描述一下论点:

|

错误

|

描述

|
| --- | --- |
| 再一次 | 资源不足,无法创建另一个线程,或者遇到了系统对线程数量的限制。 |
| 埃因瓦尔 | 属性中的设置无效。 |
| 草莓!草莓 | 没有权限设置在属性中指定的调度策略和参数。 |

  • thread:第一个参数只是一个指向pthread_t变量的指针,用来接收创建的线程的 ID 值。ID 值允许您查询和控制创建的线程。如果调用成功,线程 ID 将返回给调用程序。

  • attr:这是一个指向提供各种选项和参数的pthread_attr_t属性对象的指针。如果您可以接受默认值,只需提供零或NULL

  • start_routine:如下面的代码所示,这只是一个接受空指针并返回空指针的开始例程的名称。

  • arg:这个通用指针被传递给start_routine。它可能指向线程函数感兴趣的任何东西(start_routine)。通常这是一个包含值的结构,或者在 C++程序中,它可以是一个对象的指针。如果不需要参数值,则提供零(或NULL)。

  • returns:功能成功返回零;否则,返回错误号(不在errno中)。

参数 3 的 C 语言语法对于初学 C 的程序员来说有点讨厌。让我们展示参数 3 的函数是什么样子的:

void ∗
start_routine(void ∗arg) {
    ...
    return some_ptr;
}

下面可能是最简单的线程创建示例:

static void ∗
my_thread(void ∗arg) {
    ...                                    // thread execution
    return 0;
}

int
main(int argc, char ∗∗argv) {
    pthread_t tid;                         // Thread   ID
    int rc;

    rc = pthread_create(&tid,0,my_thread,0);
    assert(!rc);

此示例不使用线程属性(参数 2 为零)。我们也不关心传入my_thread()的值,所以参数 4 被提供了一个零。参数 3 只需要告诉系统调用执行什么函数。如果线程成功创建(由assert(3)宏测试),则rc的值将为零。

此时,主线程和函数my_thread()并行执行。因为 Raspberry Pi 上只有一个 CPU,所以在任何时刻都只有一个 CPU 执行。但是它们同时执行,以抢先的方式交换执行时间块。当然,每个都使用自己的堆栈运行。

线程my_thread()通过返回。

pthread_attr_t

有几个线程属性可以获取和设置。为了使这个速成课程简短,您将只看可能是最重要的属性(堆栈大小)。有关属性和函数的完整列表,可以查看手册页:

$ man pthread_attr_init

要初始化一个新的属性,或者释放一个先前初始化的 pthread 属性,使用这一对例程:

|

错误

|

描述

|
| --- | --- |
| 伊诺梅 | 资源(内存)不足 |

  • attr:初始化/销毁的pthread_attr_t变量的地址

  • returns:成功时为零,失败时为错误代码(不在errno中)

int pthread_attr_init(pthread_attr_t ∗attr);
int pthread_attr_destroy(pthread_attr_t ∗attr);

pthread_attr_init(3)的 Linux 实现可能永远不会返回ENOMEM错误,但是其他 Unix 平台可能会。

以下是创建和销毁属性对象的简单示例:

pthread_attr_t attr;

pthread_attr_init(&attr);    // Initialize attr
...
pthread_attr_destroy(&attr); // Destroy attr

线程最重要的属性之一可能是堆栈大小属性:

  • attr:指向要从中获取值或在其中建立属性的属性的指针。

  • stacksize:设置属性时的栈大小值,取栈大小时指向接收size_t变量的指针。

  • returns:如果调用成功,返回零;否则,返回错误号(不在errno中)。

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

pthread_attr_setstacksize(3)可能出现以下错误:

|

错误

|

描述

|
| --- | --- |
| 埃因瓦尔 | 堆栈大小小于 PTHREAD_STACK_MIN (16,384)字节。 |

Linux 手册页进一步指出:

在某些系统上,如果堆栈大小不是系统页面大小的倍数,pthread _ attr _ setstacksize()可能会失败,并出现错误 EINVAL

以下简单示例获取系统默认堆栈大小,并将其增加 8 MB:

pthread_attr_t attr;
size_t         stksiz;

pthread_attr_init(&attr);                     // Initialize attr
pthread_attr_getstacksize (&attr,&stksiz);    // Get stack size
stksiz  += 8 ∗ 1024 ∗ 1024;                   // Add 8 MB
pthread_attr_setstacksize(&attr,stksiz);      // Set stack size

系统默认值由attr的初始化提供。然后就是从attr对象中“获取”一个值,然后在对pthread_attr_setstacksize()的调用中放入一个新的堆栈大小。

注意,这组操作只是简单地准备了属性对象attr用于pthread_create()调用。实际创建线程时,该属性在新线程中生效:

pthread_attr_t attr;

...
rc = pthread_create(&tid,&attr,my_thread,0);

pthread_join(3)

在前面的pthread_create()示例中,主程序创建了my_thread()并开始执行它。在某个时刻,主程序将要结束,想要退出(或返回)。如果主程序在my_thread()完成前退出,整个进程和其中的线程都会被破坏,即使它们还没有完成。

为了使主程序等待直到线程完成,使用函数pthread_join(3):

  • thread:要连接的螺纹的螺纹 ID。

  • retval:指向void *变量的指针,用于接收返回值。如果您对返回值不感兴趣,可以给该参数提供零(或NULL)。

  • returns:函数成功返回零;否则,返回错误号(不在errno中)。

int pthread_join(pthread_t thread, void **retval);

下面的例子增加了pthread_join(3),这样主程序直到my_thread()退出后才退出。

int
main(int argc,char ∗∗argv) {
      pthread_t tid;                            // Thread ID
      void ∗retval = 0;                         // Returned value pointer
      int rc;

      rc = pthread_create(&tid,0,my_thread,0);
      assert(!rc);
      rc = pthread_join(tid,&retval);           // Wait for my_thread()
      assert(!rc);
      return 0;
}

pthread_detach(3)

函数pthread_join(3)使调用者等待,直到指示的线程返回。然而,有时一个线程被创建后就不再被检查了。当该线程退出时,它的一些资源被保留,以允许对它进行连接操作。如果永远不会有连接,那么最好在线程退出时将其遗忘,并立即释放其资源。

pthread_detach(3)函数用于指示在指定的线程上不会执行任何连接。这样,命名线程就被配置为在退出时自动释放自己。

int pthread_detach(pthread_t thread);

参数和返回值如下:

|

错误

|

描述

|
| --- | --- |
| 埃因瓦尔 | 线程不是可连接的线程。 |
| esrsch(欧洲核子研究中心) | 找不到 ID 为 thread 的线程。 |

  • thread:要改变的线程的线程 ID,以便它在完成时不会等待加入。它的资源将在指定线程终止时立即释放。

  • returns:如果呼叫成功,则为零;否则返回错误代码(不在errno中)。

pthread_detach函数只需要线程 ID 值作为它的参数:

pthread_t tid;             // Thread ID
int rc;

rc = pthread_create(&tid,0,my_thread,0);
assert(!rc);
pthread_detach(tid);      // No joining with this thread

pthread_self(3)

有时候,在一段代码中找出当前的线程 ID 是什么是很方便的。pthread_self(3)功能是这项工作的合适工具:

pthread_t pthread_self(void);

此处显示了其使用示例:

pthread_t tid;

tid = pthread_self();

pthread_kill(3)

pthread_kill(3)函数允许调用者向另一个线程发送信号。线程信号的处理超出了本文的范围。但是这个函数有一个非常有用的应用,我们很快就会看到:

#include <signal.h>

int pthread_kill(pthread_t thread, int sig);

注意,函数原型和信号定义需要signal.h的头文件。

|

错误

|

描述

|
| --- | --- |
| 埃因瓦尔 | 指定了无效信号。 |
| esrsch(欧洲核子研究中心) | 找不到 ID 为 thread 的线程。 |

  • thread:这是你要发信号(或测试)的线程 ID。

  • 这是你想发出的信号。或者,提供零来测试线程是否存在。

  • returns:如果调用成功,返回零,或者返回一个错误代码(不在errno)。

函数的一个有用的应用是测试另一个线程是否存在。如果sig参数被设置为 0,则不会传递实际信号,但仍会执行错误检查。如果函数返回零,你知道线程仍然存在

但是当线程存在的时候意味着什么呢?是不是说现在还是在执行?还是意味着它没有作为pthread_join(3)的一部分被回收,或者作为pthread_detach(3)清理的结果?

原来,当线程存在时,意味着它还在执行。换句话说,它还没有从启动的线程函数中返回。如果线程已经返回,则认为它不能接收信号。

基于此,您知道当线程仍在执行时,您将得到一个零返回。当返回错误代码ESRCH时,您知道线程已经完成。

互斥体

虽然严格来说互斥锁不是 CPU 的主题,但它与线程的讨论是分不开的。一个互斥体是一个锁定装置,它允许软件设计者停止一个或多个线程,而另一个线程正在处理一个共享资源。换句话说,一个线程接收独占访问。这是促进线程间通信所必需的。我在这里将简单地描述互斥 API,而不是互斥应用背后的理论。

pthread_mutex_create(3)

互斥体通过对pthread_mutex_init(3)的系统调用进行初始化:

|

错误

|

描述

|
| --- | --- |
| 再一次 | 系统缺少必要的资源(内存除外)来初始化另一个互斥体。 |
| 伊诺梅 | 内存不足,无法初始化互斥体。 |
| 草莓!草莓 | 调用方没有执行该操作的权限。 |
| 电子布西 | 该实现检测到有人试图重新初始化 mutex 引用的对象,mutex 是以前初始化过但尚未销毁的 mutex。 |
| 埃因瓦尔 | attr 指定的值无效。 |

  • mutex:指向要初始化的pthread_mutex_t object的指针。

  • attr:指向pthread_mutexattr_t对象的指针,描述互斥选项。如果您可以接受默认值,请提供零(或NULL)。

  • returns:如果调用成功,返回零;否则,返回错误代码(不在errno中)。

int pthread_mutex_init(
    pthread_mutex_t ∗mutex,
    const pthread_mutexattr_t ∗attr
);

这里提供了一个互斥体初始化的示例:

pthread_mutex_t mutex;
int rc;

rc = pthread_mutex_init(&mutex,0);
assert (!rc);

pthread_mutex_destroy(3)

当应用不再需要互斥体时,它应该使用pthread_mutex_destroy(3)来释放它的资源:

|

错误

|

描述

|
| --- | --- |
| 电子布西 | 互斥体被锁定或与pthread_cond_wait(3)pthread_cond_timedwait(3)一起使用。 |
| 埃因瓦尔 | 互斥体指定的值无效。 |

  • mutex:要释放资源的互斥体的地址。

  • returns:成功返回零,失败返回错误码(不在errno)。

pthread_mutex_t mutex ;
int rc;

...
rc = pthread_mutex_destroy(&mutex);
assert(!rc);

pthread_mutex_lock(3)

当一个线程需要独占访问一个资源时,它必须锁定该资源的互斥体。只要协作线程遵循相同的先锁定过程,它们就不能同时访问共享对象。

|

错误

|

描述

|
| --- | --- |
| 埃因瓦尔 | 互斥体是用值为 PTHREAD_PRIO_PROTECT 的协议属性创建的,调用线程的优先级高于互斥体的当前优先级上限。或者互斥体指定的值没有引用初始化的互斥体对象。 |
| 再一次 | 已超过互斥体的最大递归锁数。 |
| EDEADLK | 当前线程已经拥有互斥体。 |

  • mutex:指向要锁定的互斥体的指针。

  • returns:如果互斥锁成功,返回零;否则返回错误代码(不在errno中)。

int pthread_mutex_lock(pthread_mutex_t ∗mutex);

下面显示了正在调用的函数:

pthread_mutex_t mutex;
int rc;

...
rc = pthread_mutex_lock(&mutex);

pthread_mutex_unlock(3)

当不再需要对资源的独占访问时,互斥体被解锁:

|

错误

|

描述

|
| --- | --- |
| 埃因瓦尔 | mutex 指定的值没有引用初始化的 mutex 对象。 |
| 草莓!草莓 | 当前线程不拥有互斥体。 |

  • mutex:指向要解锁的互斥体的指针。

  • returns:如果互斥锁解锁成功,返回零;否则返回错误代码(不在errno中)。

int pthread_mutex_unlock(pthread_mutex_t ∗mutex);

这里提供了一个解锁互斥体的简单示例:

pthread_mutex_t mutex;
int rc;

...
rc = pthread_mutex_unlock(&mutex);

条件变量

有时,互斥体本身不足以在不同线程之间高效地调度 CPU。互斥体和条件变量经常一起使用来促进线程间的通信。起初,新来者可能会对这个概念感到困惑。

当我们有互斥时,为什么我们需要条件变量?

考虑在构建一个最多可以容纳八个项目的软件队列时需要做些什么。在我们对某些东西进行排队之前,我们需要首先查看队列是否已满。但是在我们锁定队列之前,我们无法测试它——否则,另一个线程可能会在我们眼皮底下改变事情。

所以我们锁定了队列,但是发现它已经满了。我们现在要干嘛?我们只是解锁并再试一次吗?这是可行的,但是会浪费 CPU 时间。如果我们有某种方法在队列不再满的时候得到提醒,那不是更好吗?

条件变量与互斥体和“信号”(各种类型)协同工作。用伪代码的术语来说,试图将一个项目放入队列的程序将执行以下步骤:

  1. 锁定互斥锁。在锁定队列之前,我们无法检查队列中的任何内容。

  2. 检查队列的容量。我们能在里面放一个新项目吗?如果是这样:

    1. 将新项目放入队列中。

    2. 解锁并退出。

  3. 如果队列已满,则执行以下步骤:

    1. 使用一个条件变量,用相关的互斥体“等待”它。

    2. 当控制从等待中返回时,返回到步骤 2。

条件变量对我们有什么帮助?考虑以下步骤:

  1. 互斥体被锁定(1)。

  2. 执行等待(3a)。这将导致内核执行以下操作:

    1. 将调用线程置于睡眠状态(置于内核等待队列中)。

    2. 解锁在步骤 1 中锁定的互斥锁。

在步骤 2b 中解锁互斥体是必要的,以便另一个线程可以对队列做一些事情(希望从队列中取出一个条目,这样它就不再满了)。如果互斥锁保持锁定,那么没有线程能够移动。

在未来的某个时间点,另一个线程将执行以下操作:

  1. 锁定互斥体。

  2. 在队列中查找条目(当前队列已满),并从中取出一个条目。

  3. 解锁互斥体。

  4. 向“服务员”正在使用的条件变量发出信号,以便它能够被唤醒。

等待线程随后醒来:

  1. 内核让“等待”线程准备就绪。

  2. 互斥体被成功重新锁定。

一旦该线程在互斥锁被锁定的情况下醒来,它就可以重新检查队列,看看是否有空间对某个项目进行排队。注意,只有当线程已经重新获得互斥锁时,它才会被唤醒。这就是为什么条件变量在使用时与互斥体成对出现的原因。

pthread_cond_init(3)

像任何其他对象一样,条件变量需要初始化:

|

错误

|

描述

|
| --- | --- |
| 再一次 | 该系统缺乏必要的资源。 |
| 伊诺梅 | 内存不足,无法初始化条件变量。 |
| 电子布西 | 实现检测到有人试图重新初始化 cond 引用的对象,cond 是一个先前已初始化但尚未销毁的条件变量。 |
| 埃因瓦尔 | attr 指定的值无效。 |

  • cond:指向要初始化的pthread_cond_t结构的指针。

  • attr:指向cond变量属性的指针,如果提供了一个变量属性,则提供零(或NULL)。

  • returns:如果调用成功,返回零;否则返回错误代码(不在errno中)。

int pthread_cond_init(
  pthread_cond_t             ∗cond,
  const pthread_condattr_t   ∗attr
);

pthread_cond_destroy(3)

当不再需要条件(cond)变量时,应通过以下调用释放其资源:

|

错误

|

描述

|
| --- | --- |
| 电子布西 | 检测到试图销毁 cond 引用的对象,而该对象正被另一个线程中的 pthread_cond_wait()或 pthread_cond_timedwait()引用。 |
| 埃因瓦尔 | cond 指定的值无效。 |

  • cond:待释放的条件变量。

  • returns:如果呼叫成功,则为零;否则,返回错误代码(不在errno中)。

int pthread_cond_destroy(pthread_cond_t ∗cond);

pthread_cond_wait(3)

这个函数是队列解决方案的一半。调用pthread_cond_wait(3)函数时互斥体已经被锁定。然后内核会让调用线程休眠(在等待队列上)来释放 CPU,同时解锁互斥体。调用线程保持阻塞状态,直到条件变量cond以某种方式发出信号(稍后将详细介绍)。

当线程被内核唤醒时,系统调用返回互斥锁。此时,线程可以检查应用的条件(比如队列长度),如果情况良好就继续执行,或者再次调用pthread_cond_wait(3)继续等待。

|

错误

|

描述

|
| --- | --- |
| 埃因瓦尔 | cond,mutex 指定的值无效。或者为同一条件变量上的并发 pthread_cond_timedwait()或 pthread_cond_wait()操作提供了不同的互斥体。 |
| 草莓!草莓 | 在调用时,互斥体不属于当前线程。 |

  • cond:指向用于唤醒调用的条件变量的指针。

  • mutex:指向与条件变量相关联的互斥体的指针。

  • returns:成功返回零;否则返回错误代码(不在errno中)。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

下面的代码片段显示了排队函数如何使用它。(假设初始化mutexcond。)

pthread_mutex_t mutex;
pthread_cond_t cond;

...
pthread_mutex_lock(&mutex);

while ( queue.length >=max_length )
    pthread_cond_wait(&cond,&mutex);

// queue the item
...
pthread_mutex_unlock(&mutex);

while循环重试测试以查看队列是否“未满”当多个线程插入到队列中时,while循环是必要的。根据时间的不同,另一个线程可能会比当前线程先排队一个项目,使队列再次变满。

pthread_cond_signal(3)

当从队列中取出一个条目时,需要一种机制来唤醒试图将一个条目放入整个队列的线程。一个唤醒选项是pthread_cond_signal(3)系统调用:

|

错误

|

描述

|
| --- | --- |
| 埃因瓦尔 | cond 值不是指初始化的条件变量。 |

  • cond:指向用于向一个线程发送信号的条件变量的指针。

  • returns:如果函数调用成功,返回零;否则,返回错误号(不在errno中)。

int pthread_cond_signal(pthread_cond_t ∗cond);

如果没有其他线程在等待,那么这就不是错误。但是,如果一个或多个线程正在等待指定的条件变量,此函数会唤醒一个等待线程。

如果发出一个线程将“工作”的信号,那么出于性能的原因,这个调用是首选的当存在一些特殊情况,一些线程可能成功,而另一些可能失败时,您需要一个广播调用来代替。当它可以使用时,唤醒一个线程可以节省 CPU 周期。

pthread_cond_broadcast(3)

这是pthread_cond_signal(3)的广播变体。如果多个服务员有不同的测试,应该使用广播让所有服务员醒来并考虑发现的情况。

|

错误

|

描述

|
| --- | --- |
| 埃因瓦尔 | cond 值不是指初始化的条件变量。 |

  • cond:指向条件变量的指针,条件变量发信号,唤醒所有等待线程。

  • returns:调用成功返回零;否则,返回错误号(不在errno中)。

int pthread_cond_broadcast(pthread_cond_t ∗cond);

没有服务员的时候广播是不是错误。

摘要

本章介绍了 CPU 是一种有待开发的资源。描述了/proc/cpuinfo 驱动程序,它提供了您的 CPU 能力(和处理器数量)的快速摘要。

还介绍了 ARM 架构,让您了解架构与实现的不同,例如,BCM2837 是 Broadcom 对 ARMv8-A 架构的实现。对于 C 程序员来说,本章以对 Linux 支持的 pthread API 的快速浏览结束。

七、通用串行总线

USB 端口在数字世界中已经变得无处不在,允许使用大量外围设备。Raspberry Pi 型号支持一到四个 USB 端口,具体取决于型号。

本章简要分析了与 USB 支持和电源集线器相关的一些电源注意事项。本章的其余部分将通过对 EZ-USB FX2LP 开发板的编程访问,研究 Raspbian Linux 开发人员可用的设备驱动程序接口。

电源

非常早期的 Raspberry Pi 型号将每个 USB 端口限制在 100 mA,因为板上有多保险丝。修订版 2.0 模型及以后的版本取消了这些,使您从可能发生的各种故障中解脱出来。USB 2 功率限制为单个端口 500 mA。在设计你的 IoT(物联网)的时候要记住这一点。

注意

无线 USB 适配器的功耗介于 350 毫安和 500 毫安之间。

电动集线器

有些应用需要一个带电源的 USB 集线器来连接大电流外设。对于无线网络适配器来说尤其如此,因为它们需要高达 500 mA 的电流。但是 USB 集线器需要与 Linux 内核协调,因此需要软件支持。据报道,许多集线器不起作用。下面的网页是一个很好的资源,可以列出已知可以与 Raspbian Linux 一起工作的集线器:

http://elinux.org/RPi_Powered_USB_Hubs

插入通电的 USB 集线器后,您可以使用lsusb命令列出已经注册到内核的 USB 设备:

$ lsusb
Bus 001 Device 008: ID 1a40:0101 Terminus Technology Inc. Hub
Bus 001 Device 007: ID 1a40:0101 Terminus Technology Inc. Hub
Bus 001 Device 004: ID 045e:00d1 Microsoft Corp. Optical Mouse with Tilt Wheel
Bus 001 Device 005: ID 04f2:0841 Chicony Electronics Co., Ltd HP Multimedia Keyboard
Bus 001 Device 006: ID 0424:7800 Standard Microsystems Corp.
Bus 001 Device 003: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 002: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

示例会话中的前两个显示了 my Terminus Technology Inc .提供的 hub,它是在插入 Pi 后注册的。鼠标(微软)和键盘(惠普)是插入 Pi 的两个外围设备。剩下的是支持 USB 端口的 Pi 集线器的驱动程序。本次会议使用的轮毂如图 7-1 所示。

img/326071_2_En_7_Fig1_HTML.jpg

图 7-1

通电的 USB 集线器

EZ-USB FX2LP

在这一章中,我们不仅仅要讨论 USB。相反,我们将把你的 Pi 接口到一个叫做 EZ-USB FX2LP 的经济型主板上,这款主板在易贝可以买到,价格大约为 4 美元。板上芯片为 CY7C68013A,由赛普拉斯公司制造。如果你在易贝搜索“EZ-USB FX2LP 板”,你应该能找到几个来源。

有一个 FX3LP 芯片可用,但它不是爱好定价。此外,安装驱动程序支持需要特殊说明。如果您继续使用 FX2LP,Raspbian Linux 内核驱动程序应该会自动支持它。

图 7-2 显示了作者正在使用的设备,其中插入了 USB Mini-b (5 针)电缆。如果你还没有,你需要订购电缆。通过使用 USB 开发板,您可以控制 USB 连接的两端。然而 EZ-USB 使用起来非常简单,让我们可以避开火箭科学。

img/326071_2_En_7_Fig2_HTML.jpg

图 7-2

FX2LP EZ-USB 开发板

当你第一次得到这个设备时,你应该能够简单地通过把它插入 Pi USB 端口来测试它。然后使用lsusb命令查看 Linux 内核是否看到它(下面显示的行被换行以适应页面宽度)。

$ lsusb
Bus 001 Device 011: ID 04b4:8613 Cypress Semiconductor Corp. CY7C68013 EZ-USB FX2 \
        USB 2.0 Development Kit
...

设备介绍

Anchor Chips Inc .于 1999 年被 Cypress Semiconductor corp .8Anchor 设计了一种 8051 芯片,允许软件通过 USB 上传到 SRAM 中,以支持各种外设功能。这种方法允许通过软件来配置一个硬件设备,以获得最大的灵活性。此后,Cypress 在 FX2LP (USB 2.0)等新设计中改进并扩展了其功能。这款设备的最大特点之一是硬件内置了大量 USB 支持。

完整的 PDF 手册可从以下网址下载:

http://www.cypress.com/file/126446/download

在本文档中,您将找到大量关于设备和 USB 的信息。关于这款设备,可以写一整本书,但让我们简单地列出一些突出的特点:

  • 带有 Cypress 扩展的 8051 微控制器架构

  • 16 KB SRAM,用于微控制器代码和数据

  • 硬件 FIFO 支持快速无软件传输(高达 96 MB/s)

  • 用于快速状态机传输的 GPIF(通用编程接口)

  • 2 个 UART 串行通信

  • 带闪存的 I/O I2C 主外设

  • 硬件 USB 2.0 串行引擎

我选择这个产品的原因之一是,你可以在圆周率上编程所有的软件,并尝试你的改变,而不必刷新任何东西。并且不需要特殊的微控制器程序员。

USB API 支持

在 Linux 方面,我们显然也需要软件支持。USB 设备通常由设备驱动程序支持,并作为通用外设出现,如键盘、鼠标或存储器。关于 EZ-USB 设备有趣的是,我们在 Linux 内核中有足够的支持将 FX2LP 固件上传到设备。一旦上传到 FX2LP 的 SRAM,设备将重置

USB 枚举

当一个 USB 设备第一次插入 USB 网络时(或者在引导时第一次看到),它必须通过枚举的工作来发现总线上存在什么设备,并知道它们的要求。

总线的主人是主机(PC/laptop/Pi)。所有插入总线的设备都是从设备,必须等待主机请求应答。除了极少数例外,奴隶只有在主人叫他们说话的时候才说话。

发现过程要求主机使用地址零查询设备(所有设备都必须对此做出响应)。该请求是一个 Get-Descriptor-Device 请求,允许设备描述它的一些属性。接下来,主机将通过设置地址请求分配特定的设备地址。主机发出额外的 Get-Descriptor 请求以获得更多信息。从这些信息传输中,主机了解到端点的数量、电源要求、所需的总线带宽以及要加载什么驱动程序等。

ReNumeration

Cypress 使用这个术语来描述一个活动的 EZ-USB 设备如何从 USB 总线断开,并再次枚举,可能是作为一个不同的 USB 设备。当在 EZ-USB SRAM 中执行下载的固件时,这是可能的。或者,EZ-USB 可以配置为使用其 I2C 总线将其固件从板载闪存下载到 SRAM 中。

Raspbian Linux 安装

要在 Pi 上演示 USB,我们必须首先能够在 FX2LP 板上编译、上传和运行软件。为此,我们需要安装一些软件工具。所有这些安装都必须从 root 帐户执行。请使用 sudo:

$ sudo -i
#

安装 sdcc

sdcc包包括 8051 交叉编译器和库。谢天谢地,只需要一个命令:

# apt-get install sdcc
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  gputils gputils-common gputils-doc sdcc-doc sdcc-libraries
Suggested packages:
  sdcc-ucsim
The following NEW packages will be installed:
  gputils gputils-common gputils-doc sdcc sdcc-doc sdcc-libraries
0 upgraded, 6 newly installed, 0 to remove and 2 not upgraded.
Need to get 0 B/4,343 kB of archives.
After this operation, 53.6 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Selecting previously unselected package sdcc-libraries.
(Reading database ... 128619 files and directories currently installed.)
Preparing to unpack .../0-sdcc-libraries_3.5.0+dfsg-2_all.deb ...
Unpacking sdcc-libraries (3.5.0+dfsg-2) ...
Selecting previously unselected package sdcc.
Preparing to unpack .../1-sdcc_3.5.0+dfsg-2_armhf.deb ...
Unpacking sdcc (3.5.0+dfsg-2) ...
Selecting previously unselected package sdcc-doc.
Preparing to unpack .../2-sdcc-doc_3.5.0+dfsg-2_all.deb ...
Unpacking sdcc-doc (3.5.0+dfsg-2) ...
Selecting previously unselected package gputils-common.
Preparing to unpack .../3-gputils-common_1.4.0-0.1_all.deb ...
Unpacking gputils-common (1.4.0-0.1) ...
Selecting previously unselected package gputils.
Preparing to unpack .../4-gputils_1.4.0-0.1_armhf.deb ...
Unpacking gputils (1.4.0-0.1) ...
Selecting previously unselected package gputils-doc.
Preparing to unpack .../5-gputils-doc_1.4.0-0.1_all.deb ...
Unpacking gputils-doc (1.4.0-0.1) ...
Setting up sdcc-libraries (3.5.0+dfsg-2) ...
Setting up gputils-common (1.4.0-0.1) ...
Setting up gputils-doc (1.4.0-0.1) ...
Setting up sdcc-doc (3.5.0+dfsg-2) ...
Setting up sdcc (3.5.0+dfsg-2) ...
Processing triggers for man-db (2.7.6.1-2) ...
Setting up gputils (1.4.0-0.1) ...
#

下一个包是可选的,但你可能有一天会用到它。它允许您在 Pi 上模拟 8051 代码:

# apt-get install sdcc-ucsim
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following NEW packages will be installed:
  sdcc-ucsim
0 upgraded, 1 newly installed, 0 to remove and 2 not upgraded.
Need to get 705 kB of archives.
After this operation, 1,952 kB of additional disk space will be used.
Get:1 http://raspbian.mirror.colo-serv.net/raspbian stretch/main armhf sdcc-ucsim armhf 3.5.0+dfsg-2 [705 kB]
Fetched 705 kB in 2s (268 kB/s)
Selecting previously unselected package sdcc-ucsim.
(Reading database ... 131104 files and directories currently installed.)
Preparing to unpack .../sdcc-ucsim_3.5.0+dfsg-2_armhf.deb ...
Unpacking sdcc-ucsim (3.5.0+dfsg-2) ...
Processing triggers for man-db (2.7.6.1-2) ...
Setting up sdcc-ucsim (3.5.0+dfsg-2) ...
# sync

在做出重大更改后,Pi 上的sync命令(在最后)是一个好主意。它会导致内核将磁盘缓存刷新到闪存文件系统。这样,如果您的 Pi 由于任何原因崩溃,您至少可以确保这些更改现在保存在闪存中。如果你有猫在你的 Pi 周围嗅来嗅去,这是一个救命稻草。

安装 cycfx2prog

接下来安装 cycfx2prog 包。我们将使用 cycfx2prog 命令将我们的固件上传到 FX2LP。

# apt-get install cycfx2prog
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following NEW packages will be installed:
  cycfx2prog
0 upgraded, 1 newly installed, 0 to remove and 2 not upgraded.
Need to get 12.6 kB of archives.
After this operation, 52.2 kB of additional disk space will be used.
Get:1 http://muug.ca/mirror/raspbian/raspbian stretch/main armhf cycfx2prog armhf 0.47-1 [12.6 kB]
Fetched 12.6 kB in 1s (8,007 B/s)
Selecting previously unselected package cycfx2prog.
(Reading database ... 131163 files and directories currently installed.)
Preparing to unpack .../cycfx2prog_0.47-1_armhf.deb ...
Unpacking cycfx2prog (0.47-1) ...
Setting up cycfx2prog (0.47-1) ...
# sync

安装 libusb-1.0-0-dev

此时你应该做的第一件事是更新你的系统,如果你最近还没有这样做的话。最初安装 dev 包时出现了一个问题,因此以 root 用户身份执行以下操作来纠正这个问题:

# apt-get update
# apt-get upgrade

完成后,安装 libusb:

# apt-get install libusb-1.0-0-dev

如果还没有安装 libusb-1.0-0(没有“dev”),安装这个包也会安装它。检查头文件是否存在,这将非常重要:

# ls -l /usr/include/libusb-1.0/libusb.h
-rw-r--r-- 1 root root 71395 Oct 26  2016 /usr/include/libusb-1.0/libusb.h

黑名单 usbtest

这一步很可能是必要的,除非以前做过。它禁用 Linux 内核模块usbtest,,该模块将连接到无人认领的设备。除非禁用此功能,否则我们的代码将无法附加到 FX2LP 设备。在 root 中,执行以下操作以使更改永久生效:

# sudo -i
# echo 'blacklist usbtest' >> /etc/modprobe.d/blacklist.conf

如果您不想进行这种更改,可以在需要时手动删除已加载的模块(以 root 用户身份):

# rmmod usbtest

从 github.com 获得软件

现在让我们从 github.com 下载这本书的源代码。从顶级(主)目录执行:

$ git clone https://github.com/ve3wwg/Advanced_Raspberry_Pi.git
Cloning into './Advanced_Raspberry_Pi'...

如果您不喜欢使用的子目录名称,您可以简单地将其重命名:

$ mv ./Advanced_Raspberry_Pi ./RPi

或者,您可以将其直接克隆到您选择的子目录名称中(注意添加的参数):

$ git clone https://github.com/ve3wwg/Advanced_Raspberry_Pi.git ./RPi
Cloning into './RPi'...

测试 EZ-USB FX2LP 设备

在我们进入实际的 USB 项目之前,让我们确保我们的工具和 EZ-USB 设备工作正常。转到以下子目录:

$ cd ~/RPi/libusb/blink

列出那里的文件,您应该会看到:

$ ls
blink.c  Makefile
$

那里的 Makefile 还引用了以下文件:

../ezusb/Makefile.incl

如果您是高级用户,需要进行更改,请务必检查该文件。这用于定义如何上传到 FX2LP 设备等。那里也有一些定制的 FX2LP 包含文件。

编译闪烁

使用 sdcc 交叉编译器,我们可以如下编译blink.c模块(长行用反斜杠断开):

$ make
sdcc --std-sdcc99 -mmcs51 --stack-size 64 --model-small --xram-loc 0x0000 \
  --xram-size 0x5000 --iram-size 0x0100 --code-loc 0x0000 -I../ezusb blink.c

生成的感兴趣的文件命名为 blink.ihx(英特尔十六进制):

$ cat blink.ihx
:03000000020006F5
:03005F0002000399
:0300030002009068
:20006200AE82AF837C007D00C3EC9EED9F501E7AC87B00000000EA24FFF8EB34FFF9880279
:200082008903E84970ED0CBC00DE0D80DB2275B203D280C2819003E8120062C280D2819041
:0700A20003E812006280EA8E
:06003500E478FFF6D8FD9F
:200013007900E94400601B7A009000AD780075A000E493F2A308B8000205A0D9F4DAF275E7
:02003300A0FF2C
:20003B007800E84400600A790075A000E4F309D8FC7800E84400600C7900900000E4F0A3C5
:04005B00D8FCD9FAFA
:0D0006007581071200A9E582600302000366
:0400A900758200223A
:00000001FF

这是已编译的 blink 固件的英特尔十六进制格式文件,将上传到 FX2LP 设备上执行。

EZ-USB 程序执行

这部分需要特别注意,因为 Makefile 不知道 FX2LP 如何枚举总线和设备号。首先列出 USB 总线上的设备:

$ lsusb
Bus 001 Device 010: ID 045e:00d1 Microsoft Corp. Optical Mouse with Tilt Wheel
Bus 001 Device 009: ID 04f2:0841 Chicony Electronics Co., Ltd HP Multimedia Keyboard
Bus 001 Device 011: ID 04b4:8613 Cypress Semiconductor Corp. CY7C68013 EZ-USB \
        FX2 USB 2.0 Development Kit
Bus 001 Device 006: ID 0424:7800 Standard Microsystems Corp.
Bus 001 Device 003: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 002: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

在此会话中,找到 EZ-USB 设备。这里总线号是001,设备号是011。使用此信息,键入以下内容(修改以匹配您自己的总线和设备号):

$ make BUS=001 DEV=011 prog
sudo cycfx2prog -d=001.011 reset prg:blink.ihx run
Using ID 04b4:8613 on 001.011.
Putting 8051 into reset.
Putting 8051 into reset.
Programming 8051 using "blink.ihx".
Putting 8051 out of reset.
$

如果一切顺利,您现在应该会看到 FX2LP 板上的两个内置 led 在交替闪烁。源代码如清单 7-1 所示。

0006: #include <fx2regs.h>
0007: #include <fx2sdly.h>
0008:
0009: static void
0010: delay(unsigned times) {
0011:   unsigned int x, y;
0012:
0013:   for ( x=0; x<times; x++ ) {
0014:       for ( y=0; y<200; y++ ) {
0015:           SYNCDELAY;
0016:       }
0017:   }
0018: }
0019:
0020: void
0021: main(void) {
0022:
0023:   OEA = 0x03;     // PA0 & PA1 is output
0024:
0025:   for (;;) {
0026:       PA0 = 1;
0027:       PA1 = 0;
0028:       delay(1000);
0029:       PA0 = 0;
0030:       PA1 = 1;
0031:       delay(1000);
0032:   }
0033: }

Listing 7-1The EZ-USB FX2LP blink.c source code

如果您使用不同的制造板,您可能需要跟踪 LED 引脚并对代码进行小的更改。据我所知,所有可用的板都使用这些相同的 led。我正在使用的板有连接到 GPIO 端口 A 引脚 0 ( PA0)和PA1的 led。如果你的不同,替换代码中的PA0PA1

此外,您需要更改以下行:

0023:   OEA = 0x03;     // PA0 & PA1 is output

OEA是端口 A 输出使能的寄存器名称。该寄存器中的位设置为 1,并将相应的端口 A 引脚配置为输出引脚。例如,如果您的电路板使用PA2PA3来代替,您需要将该行改为:

0023:   OEA = 0x0C;     // PA2 & PA3 is output (bits 2 & 3)

如果 led 完全位于不同的端口,则更改OEA中的“A”以匹配正在使用的端口。

USB 演示

现在我们终于可以使用 libusb 与 usb 设备(FX2LP)进行通信来演示 Raspberry Pi 了。为了简单起见,我们的任务相当简单,除了在两端之间使用 USB。目标是能够从 Raspberry Pi 端打开/关闭 FX2LP 设备上的 led。同时,Pi 将从 USB 设备读取有关 led 当前状态的确认信息。实际上,该演示练习了向 FX2LP 发送命令信息,同时还从 FX2LP 接收有关 LED 状态的更新。

FX2LP 源代码

我们的主要焦点是 Raspberry Pi 方面,但是让我们检查 FX2LP 源代码的重要方面,以便您可以看到远程设备中正在发生什么。首先转到以下子目录:

$ cd ~/RPi/libusb/controlusb

感兴趣的 FX2LP 源文件被命名为ezusb.c,主程序如清单 7-2 所示。

0091: void
0092: main(void) {
0093:
0094:   OEA = 0x03;     // Enable PA0 and PA1 outputs
0095:   initialize();   // Initialize USB
0096:
0097:   PA0 = 1;        // Turn off LEDs..
0098:   PA1 = 1;
0099:
0100:   for (;;) {
0101:       if ( !(EP2CS & bmEPEMPTY) )
0102:           accept_cmd();   // Have data in EP2
0103:
0104:       if ( !(EP6CS & bmEPFULL) )
0105:           send_state();   // EP6 is not full
0106:   }
0107: }

Listing 7-2FX2LP main program in ezusb.c

第 94 至 98 行将 FX2LP 的 GPIO 引脚配置为输出。然后它永远运行第 100 行到第 106 行的循环。第 101 行的 if 语句测试 USB 端点 2 中是否有接收到的数据,当不为空时,调用函数 accept_cmd()。

行 104 检查端点 6 是否未满。如果未满,则调用函数 send_state()发送状态信息。现在让我们更详细地检查这两个函数。

函数接受 _ 命令

该功能显示在清单 7-3 中。

0047: static void
0048: accept_cmd(void) {
0049:   __xdata const unsigned char *src = EP2FIFOBUF;
0050:   unsigned len = ((unsigned)EP2BCH)<<8 | EP2BCL;
0051:
0052:   if ( len < 1 )
0053:       return;         // Nothing to process
0054:   PA0 = *src & 1;     // Set PA0 LED
0055:   PA1 = *src & 2;     // Set PA1 LED
0056:   OUTPKTEND = 0x82;   // Release buffer
0057: }

Listing 7-3The FX2LP function accept_cmd() in ezusb.c

FX2LP 的神奇之处在于,大部分 USB 设备都是用硅片处理的。线 50 访问硅中的寄存器,该寄存器指示有多少数据被传送到端点 2。如果没有数据,函数只在第 53 行返回。

否则,通过第 49 行获得的特殊指针访问数据。第 54 行根据接收到的第一个字节的位 0 设置 LED 输出引脚PA0(Raspberry Pi 程序将只发送一个字节)。PA1同样由同一命令字节的位 1 设置。

最后,第 56 行告诉芯片端点 2 中的数据可以被释放。如果不这样做,就不会收到更多的数据。

函数发送状态

send_state()函数读取 GPIO 端口PA0PA1的当前状态,并形成 ASCII 报文发送回 Raspberry Pi(清单 7-4 )。选择详细消息格式作为发送/接收几个字节信息的示例。

0063: static void
0064: send_state(void) {
0065:   __xdata unsigned char *dest = EP6FIFOBUF;
0066:   const char *msg1 = PA0 ? "PA0=1" : "PA0=0";
0067:   const char *msg2 = PA1 ? "PA1=1" : "PA1=0";
0068:   unsigned char len=0;
0069:
0070:   while ( *msg1 ) {
0071:       *dest++ = *msg1++;
0072:       ++len;
0073:   }
0074:   *dest++ = ',';
0075:   ++len;
0076:   while ( *msg2 ) {
0077:       *dest++ = *msg2++;
0078:       ++len;
0079:   }
0080:
0081:   SYNCDELAY;
0082:   EP6BCH=0;
0083:   SYNCDELAY;
0084:   EP6BCL=len; // Arms the endpoint for transmission
0085: }

Listing 7-4The FX2LP function send_state() in ezusb.c

第 65 行访问端点 6 FIFO 缓冲区,用于将消息放入。第 66 和 67 行只是根据 GPIO 端口是 1 位还是 0 位来选择消息。然后,第 70 到 73 行将这条消息从第 65 行复制到端点缓冲区。第 74 行和第 75 行添加了一个逗号,然后第 76 行到第 79 行的循环将第二条消息复制到端点缓冲区。

当以最高时钟速度运行时,SYNCDELAY 宏是 FX2LP 特有的时序问题。第 82 行将 FIFO 长度的高位字节设置为零(我们的消息小于 256 字节)。第 84 行将 FIFO 长度的低位字节设置为我们在变量len中累积的长度。一旦设置了 FIFO 长度的低位字节,芯片就会运行缓冲器,并将其发送到 USB 总线上的 Pi。

除了 FX2LP 端点的初始化和设置之外,这就是 EZ-USB 实现的全部内容。初始化源代码也在 ezusb.c 中,供那些想更仔细研究它的人使用。

EZ-USB 初始化

为了初始化 FX2LP 设备,需要将一些值填充到一些寄存器中,以便对其进行配置。忽略 SYNCDELAY 宏调用——这些只是为了让 FX2LP 有足够的时间在器件以最高时钟速率工作时接受配置更改。清单 7-5 说明了相关的配置步骤。

0010: static void
0011: initialize(void) {
0012:
0013:   CPUCS = 0x10;       // 48 MHz, CLKOUT disabled.
0014:   SYNCDELAY;
0015:   IFCONFIG = 0xc0;    // Internal IFCLK @ 48MHz
0016:   SYNCDELAY;
0017:   REVCTL = 0x03;      // Disable auto-arm + Enhanced packet handling
0018:   SYNCDELAY;
0019:   EP6CFG = 0xE2;      // bulk IN, 512 bytes, double-buffered
0020:   SYNCDELAY;
0021:   EP2CFG = 0xA2;      // bulk OUT, 512 bytes, double-buffered
0022:   SYNCDELAY;
0023:   FIFORESET = 0x80;   // NAK all requests from host.
0024:   SYNCDELAY;
0025:   FIFORESET = 0x82;   // Reset EP 2
0026:   SYNCDELAY;
0027:   FIFORESET = 0x84;   // Reset EP 4..
0028:   SYNCDELAY;
0029:   FIFORESET = 0x86;
0030:   SYNCDELAY;
0031:   FIFORESET = 0x88;
0032:   SYNCDELAY;
0033:   FIFORESET = 0x00;   // Back to normal..
0034:   SYNCDELAY;
0035:   EP2FIFOCFG = 0x00;  // Disable AUTOOUT
0036:   SYNCDELAY;
0037:   OUTPKTEND = 0x82;   // Clear the 1st buffer
0038:   SYNCDELAY;
0039:   OUTPKTEND = 0x82;   // ..both of them
0040:   SYNCDELAY;
0041: }

Listing 7-5The EZ-USB initialization code from ezusb.c

第 13 行将 CPU 时钟配置为 48 MHz,而第 15 行也将接口时钟配置为 48 MHz。第 19 行配置端点 6 用于批量输入(从主机的角度来看),而第 21 行配置端点 2 用于批量输出。第 23 到 31 行执行 FIFO 复位。第 37 和 39 行清除双缓冲 FIFO,然后 FX2LP 芯片准备好处理 USB 请求。

树莓 Pi 源代码

现在让我们把注意力转向 Raspberry Pi 代码,使用 libusb。清单 7-6 展示了在 controlusb.cpp 中找到的主程序源代码。我们仍然在这个目录中:

0164: int
0165: main(int argc,char **argv) {
0166:   Tty tty;
0167:   int rc, ch;
0168:   char buf[513];
0169:   unsigned id_vendor = 0x04b4,
0170:       id_product = 0x8613;
0171:   libusb_device_handle *hdev;
0172:   unsigned state = 0b0011;
0173:
0174:   hdev = find_usb_device(id_vendor,id_product);
0175:   if ( !hdev ) {
0176:       fprintf(stderr,
0177:           "Device not found. "
0178:           "Vendor=0x%04X Product=0x%04X\n",
0179:           id_vendor,id_product);
0180:       return 1;
0181:   }
0182:
0183:   rc = libusb_claim_interface(hdev,0);
0184:   if ( rc != 0 ) {
0185:       fprintf(stderr,
0186:           "%s: Claiming interface 0.\n",
0187:           libusb_strerror(libusb_error(rc)));
0188:       libusb_close(hdev);
0189:       return 2;
0190:   }
0191:
0192:   printf("Interface claimed:\n");
0193:
0194:   if ( (rc = libusb_set_interface_alt_setting(hdev,0,1)) != 0 ) {
0195:       fprintf(stderr,"%s: libusb_set_interface_alt_setting(h,0,1)\n",
0196:           libusb_strerror(libusb_error(rc)));
0197:       return 3;
0198:   }
0199:
0200:   tty.raw_mode();
0201:
0202:   // Main loop:
0203:
0204:   for (;;) {
0205:       if ( (ch = tty.getc(500)) == -1 ) {
0206:           // Timed out: Try to read from EP6
0207:           rc = bulk_read(hdev,0x86,buf,512,10/*ms*/);
0208:           if ( rc < 0 ) {
0209:               fprintf(stderr,
0210:                   "%s: bulk_read()\n\r",
0211:                   libusb_strerror(libusb_error(-rc)));
0212:               break;
0213:           }
0214:
0215:           assert(rc < int(sizeof buf));
0216:           buf[rc] = 0;
0217:           printf("Read %d bytes: %s\n\r",rc,buf);
0218:           if ( !isatty(0) )
0219:               break;
0220:       } else  {
0221:           if ( ch == 'q' || ch == 'Q' || ch == 0x04 /*CTRL-D*/ )
0222:               break;
0223:           if ( ch == '0' || ch == '1' ) {
0224:               unsigned mask = 1 << (ch & 1);
0225:
0226:               state ^= mask;
0227:               buf[0] = state;
0228:               rc = bulk_write(hdev,0x02,buf,1,10/*ms*/);
0229:               if ( rc < 0 ) {
0230:                   fprintf(stderr,
0231:                       "%s: write bulk to EP 2\n",
0232:                       libusb_strerror(libusb_error(-rc)));
0233:                   break;
0234:               }
0235:               printf("Wrote %d bytes: 0x%02X  (state 0x%02X)\n",
0236:                   rc,unsigned(buf[0]),state);
0237:           } else  {
0238:               printf("Press q to quit, else 0 or 1 to "
                           "toggle LED.\n");
0239:           }
0240:       }
0241:   }
0242:
0243:   rc = libusb_release_interface(hdev,0);
0244:   assert(!rc);
0245:   libusb_close(hdev);
0246:
0247:   close_usb();
0248:   return 0;
0249: }

Listing 7-6The main program in controlusb.cpp for the Raspberry Pi

$ cd ~/RPi/libusb/controlusb

Raspberry Pi 代码使用 C++来简化一些事情。非 C++程序员不必担心。很多 Arduino 同学都在使用 C++,而没有意识到。Arduino 的人可能会因为我这么说而退缩,因为他们不想吓到任何人。在这个项目中,我们将关注看起来和工作起来都像 c 的东西。

第 166 行定义了名为 tty 的类实例。不要担心它的细节,因为我们只是用它来做一些终端 I/O 的事情,这些事情对我们来说并不重要。

第 169 和 170 行定义了我们将在某个 USB 总线上寻找的供应商和产品 ID。第 174 行基于这两个 ID 号调用 libusb 函数find_usb_device。如果没有找到设备,它返回一个空指针,在第 175 行测试。

当设备被找到时,控制传递到第 183 行,这里我们声明接口 0。如果这个失败,很可能是因为它被另一个驱动程序声明了(如usbtest)。

在线 194 中选择备用接口 1。这是导致成功的 USB 设备访问的序列中的最后一步,用于随后的循环,从第 204 行开始。一旦循环退出(我们马上会看到如何退出),接口在第 243 行被释放,然后在第 245 行被关闭。第 247 行关闭 libusb 库。

USB I/O 环路

第 200 行使用 tty 对象为终端启用“raw 模式”。这允许这个程序一次接收一个字符。通常在程序看到任何输入之前必须按下一个RETURN键,这对于这个演示来说是不方便的。

在循环中,第 205 行试图读取一个终端字符,等待 500 毫秒。如果在此时间内没有收到任何消息,调用返回-1 以指示超时。发生这种情况时,从第 207 行开始的代码尝试从 USB 端点 6 读取(从主机的角度来看,0x86 中的高位表示这是一个 OUT 端口)。这是我们的 FX2LP 将向我们发送状态更新(作为字符串消息)的端点。

当接收到来自 Raspberry Pi 键盘的字符时,执行第 150 和 152 行。如果字符是“q”,程序在第 151 行退出循环。这允许一个干净的程序退出。

如果键入‘0’或‘1’,则执行第 224 到 236 行。第 224 行将变量mask中的字符转换为 0 位或 1 位。换句话说,掩码分配为 0x01 或 0x02,具体取决于输入字符分别为“0”或“1”。行 226 跟踪名为state的变量中 LED 位的状态。然后,mask的值根据其先前的状态切换相应位的开或关。

第 227 行将状态字节放入第一个缓冲区字节。然后,这个 1 字节的缓冲区被写入端点 2(参数0x02),如果需要,在行 228 中的 10 ms 之后超时。如果超时发生,rc的返回值将为负。否则,写入的字节将从第 235 行显示在终端上。

如果程序不理解在终端输入的字符,就执行第 238 行。

清单 7-7 展示了函数 find_usb_device 的源代码。

0080: static libusb_device_handle *
0081: find_usb_device(unsigned id_vendor,unsigned id_product) {
0082:
0083:   if ( !usb_devs ) {
0084:       libusb_init(nullptr);       // Initialize
0085:       // Fetch list of devices
0086:       n_devices = libusb_get_device_list(nullptr,&usb_devs);
0087:       if ( n_devices < 0 )
0088:           return nullptr;     // Failed
0089:   }
0090:   return libusb_open_device_with_vid_pid(
0091:       nullptr,id_vendor,id_product);
0092: }

Listing 7-7The function find_usb_device in file controlusb.cpp

第一次调用 libusb 时,必须调用函数 lib usb_init。如果变量usb_devs是一个空指针(注意,变量usb_devs是一个static变量,并被初始化为空(C++中的nullptr),那么这在第 84 行完成)。之后,第 86 行获取一个 USB 设备列表,并将一个指针存储到usb_devs中以备将来使用。

一旦手续办完,我们就叫libusb_open_device_with_vid_pid找到并打开我们的设备。

函数 bulk_read

在清单 7-6 所示的主循环中,从第 207 行调用了函数 bulk_read。清单 7-8 展示了该功能的代码。

0111: static int
0112: bulk_read(
0113:   libusb_device_handle *hdev,
0114:   unsigned char endpoint,
0115:   void *buffer,
0116:   int buflen,
0117:   unsigned timeout_ms
0118: ) {
0119:   unsigned char *bufp = (unsigned char*)buffer;
0120:   int rc, xlen = 0;
0121:
0122:   assert(endpoint & 0x80);
0123:   rc = libusb_bulk_transfer(hdev,endpoint,
            bufp,buflen,&xlen,timeout_ms);
0124:   if ( rc == 0 || rc == LIBUSB_ERROR_TIMEOUT )
0125:       return xlen;
0126:   return -int(rc);
0127: }

Listing 7-8The bulk_read function in controlusb.cpp

本质上,这个函数是第 123 行中库函数libusb_bulk_transfer的一个简单插曲。在这个调用中,实际读取的字节数被返回到变量intxlen。对于较大的数据包,可以将其分成数据段。这里我们使用一个简单的假设,我们将在一次传输中接收所有的数据。

注意,如果传输超时,我们仍然可以传输一些数据(第 124 行对此进行了测试)。第 125 行返回读取的字节数。否则我们返回错误代码的负整数。

函数批量写入

bulk_write 函数更加复杂,因为它必须确保传输完整的消息,即使是以小块的形式发送。清单 7-9 说明了。

0133: static int
0134: bulk_write(
0135:   libusb_device_handle *hdev,
0136:   unsigned char endpoint,
0137:   void *buffer,
0138:   int buflen,
0139:   unsigned timeout_ms
0140: ) {
0141:   unsigned char *bufp = (unsigned char*)buffer;
0142:   int rc, xlen = 0, total = 0;
0143:
0144:   assert(!(endpoint & 0x80));
0145:
0146:   for (;;) {
0147:       rc = libusb_bulk_transfer(hdev,endpoint,
                bufp,buflen,&xlen,timeout_ms);
0148:       if ( rc == 0 || rc == LIBUSB_ERROR_TIMEOUT ) {
0149:           total += xlen;
0150:           bufp += xlen;
0151:           buflen -= xlen;
0152:           if ( buflen <= 0 )
0153:               return total;
0154:       } else  {
0155:           return -int(rc); // Failed
0156:       }
0157:   }
0158: }

Listing 7-9The bulk_write function in controlusb.cpp

消息传输再次使用 libusb_bulk_transfer,但根据端点号知道这是发送给主机的(第 144 行的断言检查)。调用实际发送的字节数在 xlen 变量中返回(参数五)。如果传输成功或超时,总字节数将作为正数返回(第 153 行)。否则,返回负的错误代码。

注意,该例程跟踪第 149 行中传输的总字节数。在行 150 中增加缓冲器开始指针,在行 151 中减少要发送的计数。该例程仅在所有字节都已发送或请求超时时返回。理想情况下,应该为超时情况提供更好的处理。

示威游行

现在让我们来表演插图。将 FX2LP 设备插入 USB 端口后,找出其总线和设备号,以便向其上传固件:

$ lsusb
Bus 001 Device 007: ID 04b4:8613 Cypress Semiconductor Corp. CY7C68013 \ EZ-USB FX2 USB 2.0 Development Kit
Bus 001 Device 006: ID 0424:7800 Standard Microsystems Corp.
Bus 001 Device 003: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 002: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
$

现在知道它在总线001和设备007上,上传固件。您应该会看到如下所示的会话输出:

$ sudo make BUS=001 DEV=007 prog
sudo cycfx2prog -d=001.007 prg:ezusb.ihx run delay:10 dbulk:6,-512,5
Using ID 04b4:8613 on 001.007.
Putting 8051 into reset.
Programming 8051 using "ezusb.ihx".
Putting 8051 out of reset.
Delay: 10 msec
Reading <=512 bytes from EP adr 0x86  ...etc.

一旦cycfx2prog使 FX2LP 脱离复位,我们的固件代码就开始执行,这就是“Reading <=512 bytes消息的意义所在。现在,如果您还没有编写树莓 Pi 程序,请编写它:

$ make -f Makefile.posix

g++ -c -std=c++11 -Wall -Wno-deprecated  -I. -g -O0 controlusb.cpp \ -o controlusb.o
g++  controlusb.o -o controlusb -lusb

现在启动它:

$ sudo ./controlusb
Interface claimed:
Read 11 bytes: PA0=1,PA1=1
Read 11 bytes: PA0=1,PA1=1
Read 11 bytes: PA0=1,PA1=1
Wrote 1 bytes: 0x01  (state 0x01)
Read 11 bytes: PA0=1,PA1=1
Read 11 bytes: PA0=1,PA1=1
Read 11 bytes: PA0=1,PA1=0
Read 11 bytes: PA0=1,PA1=0
Read 11 bytes: PA0=1,PA1=0
Read 11 bytes: PA0=1,PA1=0
Wrote 1 bytes: 0x00  (state 0x00)
Read 11 bytes: PA0=1,PA1=0
Read 11 bytes: PA0=1,PA1=0
Wrote 1 bytes: 0x02  (state 0x02)
Read 11 bytes: PA0=0,PA1=0
Read 11 bytes: PA0=0,PA1=0
Wrote 1 bytes: 0x00  (state 0x00)
Read 11 bytes: PA0=0,PA1=1

该程序需要 root 用户,所以用sudo启动它。否则,它将找到设备,但不能声明接口。第一行:

Wrote 1 bytes: 0x01  (state 0x01)

是我打了一个1的时候写的。不久之后,PA1上的 LED 亮起(LED 为低电平有效,因此 0 位打开 LED)。两行之后,FX2LP 能够向我们发送一条消息,报告PA1=0 (LED 亮起)。这并不是 FX2LP 的一部分出现了延迟,而是因为直到 Pi 读取了之前的 USB 消息,它才能够发送关于它的消息。

用“0”和“1”进行一些其他涂鸦,直到“q”键结束演示。

摘要

这一章涵盖了很多内容。对 FX2LP EZ-USB 设备进行了旋风式的介绍。好奇的人应该看看 EZ-USB 设备的 PDF 文档,并为它寻找书籍和在线资源。这一章仅仅触及了硅能做什么的表面。

本章的主要焦点是如何在 Raspberry Pi 上直接从用户模式程序处理 USB I/O。一旦您了解了基础知识,libusb 库会使这变得相当容易。这些都包含在 controlusb.cpp 源代码中。现在你有了武器和危险,你可以通过使用 USB 设计新的应用来将你的 USB 知识提高到一个新的水平。

八、以太网

无论是有线还是无线,网络已经成为日常生活的重要组成部分。在您的 Raspberry Pi 上安装一个网络适配器可以让您连接到它,并在您的台式机或笔记本电脑上舒适地工作。它还允许 Pi 上的应用与外界通信。即使当 Raspberry Pi 作为嵌入式项目的一部分部署时,网络接口仍然会继续发光。远程日志和控制只是几个例子。

有线以太网

标准 Raspbian SD 卡映像提供有线网络连接,使用 DHCP(动态主机配置协议)自动为其分配 IP 地址。如果您使用 HDMI 输出和键盘设备在 Pi 上工作,动态分配的 IP 地址不是问题。但是,如果您想去掉附加的显示器和键盘,并进行“无头”操作,那么通过网络连接到 Pi 是很有吸引力的。唯一的问题是 DHCP 分配的 IP 地址可能会改变。

DHCP 不会总是使用不同的 IP 地址,因为它会暂时向 ?? 出租该地址。但是动态分配的地址使得当它改变时很难从另一台计算机连接到您的 Raspberry Pi。正如在第二章中所讨论的,您可以使用nmap命令来扫描它,但是这并不方便(这个例子来自 Devuan Linux):

root@devuan:~# nmap -sP 192.168.1.1-250

Starting Nmap 6.47 ( http://nmap.org ) at 2018-06-01 19:59 EDT
Nmap scan report for 192.168.1.1
Host is up (0.00026s latency).
MAC Address: C0:FF:D4:95:80:04 (Unknown)
Nmap scan report for 192.168.1.12
Host is up (0.044s latency).
MAC Address: 00:1B:A9:BD:79:12 (Brother Industries)
Nmap scan report for 192.168.1.77
Host is up (0.15s latency).
MAC Address: B8:27:EB:ED:48:B1 (Raspberry Pi Foundation)
Nmap scan report for 192.168.1.121
Host is up (0.00027s latency).
MAC Address: 40:6C:8F:11:8B:AE (Apple)
Nmap scan report for 192.168.1.89
Host is up.
Nmap done: 250 IP addresses (4 hosts up) scanned in 7.54 seconds
root@devuan:~#

如果您在学校或离家在外使用您的 Pi,使用 DHCP 可能仍然是您的最佳选择。如果你在旅行时将它接入不同的网络,DHCP 会正确设置你的 IP 地址,会负责域名服务器配置。但是,如果您在家里使用您的设备,或者您的学校可以为您分配一个有效的 IP 地址,静态 IP 地址可以简化访问。

注意

确保获得批准并分配 IP 地址,以防止网络冲突。

静态有线地址

设置静态有线以太网地址的最简单方法是使用图形桌面并打开“无线和有线网络设置”对话框这可以通过右键单击扬声器图标左侧的 WIFI 图标在屏幕右上角找到。图 8-1 所示。

img/326071_2_En_8_Fig1_HTML.jpg

图 8-1

包含“无线和有线网络设置”对话框的弹出式菜单

选择并单击“无线和有线网络设置”应该会出现如图 8-2 所示的对话框。

img/326071_2_En_8_Fig2_HTML.jpg

图 8-2

“网络首选项”对话框

要配置有线接口,请选择“接口”,然后选择“eth0”。如果你愿意,让“自动配置空选项”保持选中状态,就像我在图中所做的那样。填写您的地址、路由器和 DNS 服务器信息,点击“应用”,然后点击“关闭”此操作更新了什么?以下行将被附加到文件/etc/dhcpcd.conf 中:

interface eth0
inform 192.168.1.177
static routers=192.168.1.1
static domain_name_servers=192.168.1.1

建立这些设置后,有线以太网端口应自动分配静态 IP 地址192.168.1.177,在本例中。

无线配置

无线网络接口的配置方式与有线适配器类似。调出您在图 8-2 中看到的对话框,除了您将选择“SSID”而不是“接口”图 8-3 显示无线配置对话框。

img/326071_2_En_8_Fig3_HTML.jpg

图 8-3

无线配置对话框

在“SSID”的右侧,选取您想要加入的无线网络。单击应用,然后单击关闭。完成此操作后,文件/etc/dhcpcd.conf 将更新为:

SSID BaldEaglesLair
inform 192.168.1.77
static routers=192.168.1.1
static domain_name_servers=192.168.1.1

疯狂的

图形对话框工作很好,除了如果您重复无线网络的配置,您将在文件的底部得到添加的条目。如果同一个 SSID 的多个设置发生冲突,您的无线网络可能永远无法工作。如果是这种情况,您需要编辑文件以删除冲突的重复项:

# sudo -i
# nano /etc/dhcpcd.conf

WIFI 国家

如果您还没有这样做,请确保您的 WIFI 国家已正确配置。从桌面左上方的树莓下拉菜单中选择“首选项”,然后选择“树莓 Pi 配置”点击图 8-4 中所示的“本地化”标签。

img/326071_2_En_8_Fig4_HTML.jpg

图 8-4

带有 WIFI 国家设置的“本地化”选项卡

一旦有,这是一个简单的事情,点击“设置 WIFI 国家”选择您的国家。这是一个重要的配置项目,因为它决定了您所在国家的无线 LAN(局域网)适配器的合法工作频率。

测试静态 IP 地址

一旦你配置好了,最简单的事情就是重启你的 Raspberry Pi 让新的设置生效。通常,图形对话框会让您的更改几乎立即生效。

一个好的手动检查是使用ifconfig命令:

$ ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.177  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 fe80::8cc8:d1d2:61ba:d377  prefixlen 64  scopeid 0x20<link>
        ether b8:27:eb:b8:1d:e4  txqueuelen 1000  (Ethernet)
        RX packets 10505  bytes 900810 (879.6 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 26412  bytes 14866204 (14.1 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

由此您可以看到有线网络适配器的 IP 地址配置如下:

inet 192.168.1.177  netmask 255.255.255.0  broadcast 192.168.1.255

同样,您可以检查无线适配器:

$ ifconfig wlan0
wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.77  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 fe80::dd0c:a1af:9a22:a0c0  prefixlen 64  scopeid 0x20<link>
        ether b8:27:eb:ed:48:b1  txqueuelen 1000  (Ethernet)
        RX packets 24977  bytes 1777643 (1.6 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 6924  bytes 7627770 (7.2 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

现在让我们检查一下名字是解析。通常,我会推荐使用nslookupdig命令,但是 Raspbian 上没有预装这两个命令。所以让我们使用ping命令:

$ ping -c 1 google.com
PING google.com (172.217.0.238) 56(84) bytes of data.
64 bytes from yyz10s03-in-f14.1e100.net (172.217.0.238): icmp_seq=1 ttl=55 time=17.9 ms

--- google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 17.931/17.931/17.931/0.000 ms

在本例中,我们看到google.com被查找并转换为 IP 地址172.217.0.238(您的尝试可能有所不同)。由此,我们得出结论,名称服务正在工作。ping命令行上的-c1选项导致只执行一次 ping。否则,ping将继续尝试,您可能需要^C来中断它的执行。

如果名称google.com无法解析,您需要对/etc/resolv.conf进行故障诊断。它应该看起来像这样(注意名称服务器行):

$ cat /etc/resolv.conf
# Generated by resolvconf
search subversive.cats.ca
nameserver 192.168.1.1

有关此文件的更多信息,请参考:

$ man 5 resolv.conf

USB 适配器

如果您有有线 USB 以太网适配器,您也可以设置网络。图 8-5 显示了一个廉价装置的例子。当你已经有一个像 Raspberry Pi 3 B+这样的内置有线适配器时,它应该在你的对话框中显示为“interface”“et h1”。用于树莓 Pi Zero(非 Zero W)时,会显示为“eth0”。这是一个与你的零暂时交流的好方法。

如果您的 Raspbian Linux 支持的话,也可以使用无线 USB 适配器。通常需要加载特定于设备的固件来提供支持。还要记住,无线适配器可能需要来自 USB 端口的 350 到 500 mA 的电流。

Radicom 宣传他们的非无线型号 LUHM200 型号最大需要 165 mA(未经证实是否支持 Raspbian Linux)。Pi 兼容性列表可在以下网站找到:

https://elinux.org/RPi_USB_Ethernet_adapters

该网站列出了一些其他的电流消耗数据,包括一个需要 250 毫安的苹果适配器。一般来说,有线适配器应该比无线适配器消耗少得多,并且在支持时,应该不需要特殊的驱动程序。我的单位如图 8-5 所示使用了大约 45 mA。

img/326071_2_En_8_Fig5_HTML.jpg

图 8-5

插入 USB 延长线的有线 USB 以太网适配器

/etc/hosts 文件

如果你的 Raspberry Pi 有一个静态的 IP 地址,为什么不用一个主机名来更新你的 Linux 或者 OS X 文件呢?例如,您的 hosts 文件可以添加以下行:

$ cat /etc/hosts
. . .
192.168.1.177 rasp raspi rpi pi # My Raspberry Pi

现在,您可以使用主机名raspraspirpipi在网络上访问您的 Raspberry Pi。

Pi Direct

鉴于 Raspberry Pi SBCs(单板计算机)的低成本,您可以在您的主 Raspberry Pi 3 B+之外运行另一个 Pi 作为卫星。如果你是一名游戏开发人员,你可能想利用卫星 Pi 进行 VR(虚拟现实)显示。如果两者之间的网络连接足够快,这将为您提供 2 倍的 HDMI 输出。对于较轻的图形负载,您甚至可以使用 Raspberry Pi Zero。

为了探索这种可能性,让我们概述一下通过有线以太网连接直接链接两个 Raspberry Pi 的步骤。我将使用通过 WIFI(接口 wlan0)访问的 Raspberry Pi 3 B+,但通过两者上的有线 eithernet 端口(两者上的接口 eth0)链接卫星 Pi (Raspberry Pi 3 B, not plus )。远程(卫星)Pi 将通过 Pi 3 B+完全访问互联网。

过去有必要使用以太网交叉电缆,但现在没有必要了。以太网固件现在自动配置带有直电缆的端口。图 8-6 展示了我将用来简化讨论的示例网络。现在花一点时间沉浸其中。

img/326071_2_En_8_Fig6_HTML.jpg

图 8-6

Pi 3 B+和点对点连接 Pi 3 B 的示例网络

在这个例子中,我们将重点讨论 Pi 3 B+和 Pi 3 B。但是请注意,B+节点的互联网接入通过 WIFI 路由器(192.168.1.1)经由 WIFI(接口 wlan0)到达。这是通过位于 192.168.0.1 的 ISP 路由器传送的。B+节点的桌面访问也是通过 WIFI,使用 IP 地址 192.168.1.77。

要解决的问题如下:

  • 在 B+和 B 之间建立点对点连接,在两个 Pi 上使用从 eth0 到 eth0 的电缆。

  • 启用 IP 路由,以便所有连接请求都通过 B+(来自 B)。

  • 启用从 B 到 internet 的转发(这包括设置名称服务器访问)。

启用除互联网转发之外的所有功能非常简单。让最后一步工作,以便您可以从互联网上升级您的远程 Pi (B)有点棘手。我们开始吧。

点对点

第一步是让 Pi 3 B(远程 Pi)与 Pi 3 B+ (B+)对话。这需要两个 Pi 主机的合作。让我们从远程 Pi 开始(使用 Pi 3 B 键盘、鼠标和显示器进行初始设置)。

远程 Pi 配置

要让远程 Pi 设置其 eth0 有线连接,请编辑以下文件(以 root 用户身份):

# nano /etc/dhcpcd.conf

添加/编辑行,使您最终得到:

interface eth0
inform 192.168.2.86
static ip_address=192.168.2.86/24
static domain_name_servers=192.168.1.1
static routers=192.168.2.87

如果您以前来过这里,请确保注释掉或者禁用对接口 eth0 的旧引用。

  • inform选项在启动时告诉 DHCP 以太网接口 eth0 将使用 IP 地址 192.168.2.86 启动。

  • 除了/24指示网络和主机地址之间的边界之外,static ip_address行指定了相同的内容。

  • static domain_name_servers行配置名称服务器请求应该去哪里。在这里,我们将名称服务器请求转发到位于 192.168.1.1 的 WIFI 路由器。

  • 这条线在这里至关重要。它会将所有发往未知主机的流量“FQ”发送给 Pi 3 B+主机。

192.168.2.87 是那个直连有线链路的 B+端(审图 8-6 )。重要的是,IP 地址是该链路的 B+端。如果您错误地提供了 192.168.2.86,它最终会尝试转发给自己(B)。

保存这些更改并重新启动。在它出现后,您应该能够验证它的 IP 地址和路由表,如下所示(通过键盘和显示器):

# ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.2.86  netmask 255.255.255.0  broadcast 192.168.2.255
        inet6 fe80::595f:6363:5a8:d68  prefixlen 64  scopeid 0x20<link>
        ether b8:27:eb:4d:56:6f  txqueuelen 1000  (Ethernet)
        RX packets 163  bytes 13423 (13.1 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 143  bytes 22865 (22.3 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

如果链接没有启动(运行),先不要担心。我们在连接的 B+端还有工作要做。检查路由:

  • 目的地 0.0.0.0 代表默认目的地。我们从第一行看到,它被配置为发送到 192.168.2.87(点对点的 B+端)链路。

  • 第二行表示默认情况下,192.168.2.0 的任何网络请求也将被路由。

# route -n
Kernel IP routing table
Destination  Gateway      Genmask         Flags Metric Ref Use Iface
0.0.0.0        192.168.2.87 0.0.0.0       UG    202    0   0   eth0
192.168.2.0  0.0.0.0      255.255.255.0 U     202    0   0   eth0

这将完成远程 Pi (B)配置。

WIFI Pi (B+)

现在,我们必须配置 Pi 3 B+,以便它也能提供点对点接口 eth0。编辑其文件/etc/dhcpcd.conf,以便为 eth0 保留以下行:

  • 当界面可用时,行auto eth0调出界面。

  • interface eth0导致所有后续行应用于该接口。

  • inform行告诉 DHCP 服务器接口 eth0 的 IP 地址是 192.168.2.87。

  • static ip_address行指定 IP 地址,并用/24间接指定网络任务。

  • nogateway选项表示没有要配置的网关(在 192.168.2.0 网络上找不到其他主机)。

auto eth0
interface eth0
inform 192.168.2.87
static ip_address=192.168.2.87/24
nogateway

重新启动 Pi 3 B+(和 Pi 3 B)后,您现在应该有了两者之间的直接链接。检查 Pi 3 B+:

# ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.2.87  netmask 255.255.255.0  broadcast 192.168.2.255
        inet6 fe80::8cc8:d1d2:61ba:d377  prefixlen 64  scopeid 0x20<link>
        ether b8:27:eb:b8:1d:e4  txqueuelen 1000  (Ethernet)
        RX packets 26  bytes 3670 (3.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 31  bytes 4206 (4.1 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

希望您的链接既有地址 192.168.2.87,也有 up(“运行”)。还要检查常规:

# route -n
Kernel IP routing table
Destination  Gateway      Genmask       Flags Metric Ref Use Iface
0.0.0.0      192.168.1.1  0.0.0.0       UG   303       0     0   wlan0
192.168.1.0  0.0.0.0      255.255.255.0 U    303       0     0    wlan0
192.168.2.0  0.0.0.0      255.255.255.0 U    202       0     0    eth0

在此显示中,请注意以下内容:

  • 默认网关是 192.168.1.1(来自早期的 WIFI 设置)。所以任何主机不知道怎么路由的东西都会FQ扔给 WIFI 路由器处理。

  • 任何与 WIFI 路由器网络(192.168.1.0)相关的内容也会被路由到 WIFI 路由器。

  • 任何被定向到网络 192.168.2.0 的内容都被发送到接口 eth0,这是我们到远程 Pi 的点对点链路。

从这个 B+端,让我们使用远程端的 IP 号测试登录到远程 Pi (B ):

$ ssh pi@192.168.2.86
pi@192.168.2.86's password:
Linux raspberrypi3 4.14.34-v7+ #1110 SMP ...

目前为止一切顺利。

iptables

我希望我能告诉你我们结束了。不幸的是,还剩下两个步骤。在 B+上,我们必须:

  • 启用 IP 转发

  • 配置 iptables,以便它知道如何转发哪些数据包。

默认情况下禁用 IP 转发功能,因为它可能会导致安全漏洞。当你需要的时候,把这个单独的设置设为关可以让你安心。

启用 IP 转发

要打开 IP 转发,请执行以下操作:

# sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

-w选项会更新系统文件以保存您的设置。如果没有此选项,该设置将不会在下次重新启动时恢复。IP 转发尚未投入使用——这只是允许转发数据包。

配置 IP 转发

现在我们将注意力转向 Linux 中的iptables工具。如果你像我一样,你可能会抱怨“我真的需要学习所有这些关于防火墙的东西吗?”或者也许是“唉,我只是想让它工作!”请原谅,只需要多一点点。

如果你还没有弄乱iptables的话,那么你可以跳过清除这一步。如果您已经安装了想要保留的防火墙规则,您也会想要避免清除。否则,我们检查一下iptables,然后清除规则。首先列出过滤器:

# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

如果一切都很清楚,这就是你应该在显示屏上看到的。当您不使用-t选项时,您隐式地引用了一个名为filters的表。但是,我们还必须检查名为nat的表:

# iptables -L -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination

显示屏上没有任何添加的规则。如果您看到想要删除的规则,请使用-F 和-X 选项将其全部清除:

# iptables -F
# iptables -F -t nat
# iptables -X
# iptables -X -t nat

运行这些命令后,您应该能够获得如前所示的空列表。-F选项删除所有的规则链。X 删除任何特殊的用户定义的链。同样,当没有-t选项时,就好像您指定了-t filters。选项-t nat适用于网络地址转换(NAT)表。

现在我们可以告诉 iptables 转发我们的数据包,并在必要时应用 NAT。

# iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE

该命令附加到nat表中,以引导任何转发到接口wlan0的数据包进行网络地址转换(NATted)。这些数据包被“伪装”成来自 192.18.1.77(b+),而不是远程 Pi (B)。

准备就绪后,让我们在远程 Pi (B)上重新测试。

第二次远程 Pi 测试

用键盘登录远程 Pi (B ),测试链路路由:

$ ping 192.168.1.1
PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
64 bytes from 192.168.1.1: icmp_seq=1 ttl=63 time=3.22 ms
64 bytes from 192.168.1.1: icmp_seq=2 ttl=63 time=4.12 ms
^C
--- 192.168.1.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 3.229/3.675/4.121/0.446 ms

如果事情是正确的,这个 ping 您的 WIFI 网关应该是成功的,如这个例子所示。目前为止,一切顺利。

另一个很好的测试是 ping google:

$ ping 8.8.8.8

如果失败了,这可能表明你的网络(或谷歌)的其他部分有问题。

假设这是成功的,尝试测试名称解析器:

# ping google.com
PING google.com (172.217.0.110) 56(84) bytes of data.
...

事实上,它显示你 192.217.0.110(在这个例子中)一个 IP 号码的名称 google.com 意味着名称服务器正在工作。

作为最后的测试,您应该能够进行apt-get更新:

# apt-get update
Hit:1 http://archive.raspberrypi.org/debian stretch InRelease
Hit:2 http://raspbian.raspberrypi.org/raspbian stretch InRelease
Reading package lists... Done

此时,您应该能够像往常一样理解这一点:

# apt-get upgrade

持久性 iptables

为了避免每次都必须设置iptables,我们需要让规则在引导时可恢复。但是在我们保存规则之前,让我们确保我们没有破坏现有的文件。/etc/iptables目录应该还不存在:

# ls -d /etc/iptables
ls: cannot access '/etc/iptables': No such file or directory

如果没有,现在就创建它:

# mkdir /etc/iptables

如果该目录确实存在,检查名为rules的文件是否已经存在或者没有重要内容:

# cat /etc/iptables/rules

假设规则文件尚不存在或者没有实质性内容,我们可以将我们的iptables规则保存到这个文件中:

# iptables-save >/etc/iptables/rules

要在引导时自动恢复 iptables 规则,请创建/编辑以下文件:

# nano /etc/dhcpcd.enter-hook

向其中添加以下行:

iptables-restore </etc/iptables/rules

保存编辑。现在,当您启动时,DHCP 服务器应该从该文件中恢复您的iptables规则。

布图规则检测

即使事情进展顺利,也很高兴能验证这种iptables东西正在工作。通过添加-v选项,用计数列出规则,iptables命令可以显示正在执行的规则:

# iptables -L -v
Chain INPUT (policy ACCEPT 1277 packets, 122K bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain FORWARD (policy ACCEPT 258 packets, 83716 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 1181 packets, 120K bytes)
 pkts bytes target     prot opt in     out     source               destination

从这个显示中,我们可以确认除了关于输入和输出规则的信息之外,还转发了 258 个数据包。通过添加-t nat选项,相同的报告可用于nat表。

接近

刚才概述的过程允许您以间接的方式登录到您的远程 Pi。使用本章的例子,您将首先 ssh 到 192.168.1.77,然后从那里 ssh 到 192.168.2.86。如果没有额外的配置,您将无法直接从台式计算机 ssh 到 192.168.2.86。这是可以做到的,但是这个话题超出了我们的范围。

然而,目前的安排确实允许两个 Pi 相互直接通信。游戏应用可以利用共享的 CPU 资源,并利用两个 HDMI 显示器。作为一个额外的奖励,远程 Pi 还可以访问互联网,包括用apt-get更新的能力。

安全

本演示是一个入门示例。有更多的选项和规则可以用于更好的保护。但是,如果您已经在防火墙后运行,那么就没有必要增加管理的复杂性。

如果你的 Pi 没有在防火墙后面,并且你计划直接在互联网上暴露你的设备,那么你必须花时间去学习更多关于iptables和防火墙原理的知识。

摘要

本章介绍了如何使用图形桌面配置无线和有线以太网适配器。这非常适合正常和常规的情况。

然而,对于特殊的配置,事情会变得更加复杂。本章的其余部分讲述了文件的编辑和iptables设置,以建立点对点连接。这允许远程 Pi 通过第一个 Pi 访问互联网。了解如何保存iptables配置并在引导时哄 DHCP 服务器恢复这些规则,就完成了这幅画面。以此为起点,您可以扩展您的网络配置知识,以满足特殊需求。

九、SD 卡存储

文件系统是 Linux 借鉴的 Unix 系统设计的核心。传统上,大容量存储需求是通过硬盘子系统来满足的。然而,随着主机变得像信用卡一样小,闪存技术已经取代了笨重的机械驱动器。

SD 卡介质

第一个私家侦探用的是标准尺寸的 SD 卡。然而,所有较新的型号现在都使用如图 9-1 所示的 MicroSD 卡,以及标准 SD 适配器。

img/326071_2_En_9_Fig1_HTML.jpg

图 9-1

SD MicroSD 适配器(左侧)和 8 GB MicroSD 卡(右侧)

MicroSD 底面的 8 个引脚如图 9-2 所示。

img/326071_2_En_9_Fig2_HTML.jpg

图 9-2

MicroSD 卡的底部,有 8 个引脚暴露在外

SD 卡基础知识

SD 卡包括一个内部控制器,也称为闪存处理器(FSP)。在这种配置中,主机仅提供命令并等待响应。FSP 负责完成命令所需的所有擦除、编程和读取操作。以这种方式,随着新的性能和存储密度的实现,闪存卡设计的复杂性被允许增加。

SD 卡管理扇区大小为 512 字节的数据。为了与现有操作系统兼容,特意将它设计为与 IDE 磁盘驱动器相同。主机发出的命令包括扇区地址,以允许一个或多个扇区的读/写。

注意

操作系统可能会使用多个 512 字节的扇区。

命令和数据受 FSP 生成的 CRC(循环冗余校验)码保护。FSP 还会在写入后自动执行读取,以验证数据是否正确写入。 9 如果写入有缺陷,FSP 会自动纠正,必要时用另一个物理扇区替换。

SD 卡的软错误率远低于磁盘驱动器。在极少数情况下,当发现错误时,最后一道防线是纠正 ECC(纠错码),它允许数据恢复。这些错误在介质中被纠正,以防止将来出现不可恢复的错误。所有这些活动对主机都是透明的。

拉斯比安块大小

操作系统使用的块大小可能是介质扇区大小的倍数。为了确定在 Raspbian 下使用的物理块大小,我们首先发现根文件系统是如何挂载的(下面的清单已经被修改):

$ mount
/dev/mmcblk0p2 on / type ext4 (rw,noatime,data=ordered)
...
/dev/mmcblk0p1 on /boot type vfat (rw,relatime,fmask=0022,dmask=0022, \
      codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)

由此我们推断用于根文件系统的设备是/dev/mmcblk0p2。使用的命名约定告诉我们以下内容:

|

成分

|

名字

|

数字

|

类型

|
| --- | --- | --- | --- |
| 前缀 | /dev/mmcblk |   | MMC 块 |
| 设备号 | Zero | Zero |   |
| 分区号码 | p2 | Two |   |

从前面的mount命令输出中,注意到/boot文件系统被挂载在/dev/mmcblk0p1上。这表明/boot文件系统来自同一个 SD 卡设备的分区 1。

使用设备信息,我们参考/sys伪文件系统来找出物理和逻辑扇区大小。这里我们提供mmcblk0作为第三级路径名限定符来查询设备:

$ cat /sys/block/mmcblk0/queue/physical_block_size
  512
$ cat /sys/block/mmcblk0/queue/logical_block_size
  512
$

显示的结果告诉我们,本例中使用的 Raspbian Linux 使用 512 字节的块(扇区)大小,无论是物理上还是逻辑上。这与 SD 卡的扇区大小完全匹配。

磁盘高速缓存

在我们检查已安装的 SD 卡文件系统时,我们还要检查所使用的设备节点的类型:

$ ls -l /dev/mmcblk0p?
brw-rw---- 1 root disk 179, 1 Jun 19 07:42 /dev/mmcblk0p1
brw-rw---- 1 root disk 179, 2 Jun 19 07:42 /dev/mmcblk0p2

示例输出在 brw-rw 字段的开头显示一个b。这告诉我们磁盘设备是一个设备,而不是一个字符设备。(相关的字符设备将显示一个c。)块设备对于文件系统非常重要,因为它们提供了磁盘缓存功能,可以极大地提高文件系统的性能。输出显示root(分区 2)和/boot(分区 1)文件系统都是使用块设备挂载的。

容量和性能

SD 卡允许在介质限制范围内配置数据总线宽度。所有 SD 卡都从一条数据位线开始,直到存储卡的容量已知。在知道介质的能力之后,数据总线可以在软件控制下扩展,如支持的那样。表 9-1 总结了 SD 卡的功能。 10

表 9-1

SD 卡功能

|

标准

|

描述

|

大于

|

直到

|

数据总线

|
| --- | --- | --- | --- | --- |
| 级计算中心 | 标准容量 | Zero | 2 GB | 1 位 |
| SDHC(消歧义) | 大容量 | 2 GB | 32 GB | 4 位 |
| SDXC(消歧义) | 扩展容量 | 32 GB | 2 TB | 4 位 |

转移模式

SD 卡使用三种基本的数据传输模式:

  • SPI 总线模式

  • 1 位标清模式

  • 4 位标清模式

SPI 总线模式

SPI 总线模式主要由使用支持 SPI 总线的小型微控制器的消费电子产品使用。检查表 9-2 显示,在此模式下,数据一次传输 1 位(引脚 2 或 7)。

表 9-2

spi 总线模式微秒

|

别针

|

名字

|

输入-输出

|

逻辑

|

描述

|

精力

|
| --- | --- | --- | --- | --- | --- |
| one | 网络计算机 |   |   |   |   |
| Two | /CS | 我 | 包裹邮递(Parcel Post) | 卡选择(低电平有效) | 特许测量员 |
| three | 国防情报部 | 我 | 包裹邮递(Parcel Post) | 数据输入 | 工业博物馆 |
| four | 电源电压 | S | S | 电源 |   |
| five | 时钟信号 | 我 | 包裹邮递(Parcel Post) | 时钟 | sclk-sclk-sclk-sclk-sclk-sclk-sclk-sclk-sclk-sclk-sclk-sclk |
| six | 虚存系统 | S | S | 地面 |   |
| seven | 防御命令(Defense Order) | O | 包裹邮递(Parcel Post) | 数据输出 | 军事情报部门组织(Military Intelligence Service Organization) |
| eight |   |   |   | 内向的; 寡言少语的; 矜持的 |   |

各种 SD 卡连接的使用方式各不相同,如表 9-2I/O 和逻辑列中的助记符所示。表 9-3 是这些的图例,也适用于表 9-4 。

表 9-3

I/O 和逻辑图例

|

注释

|

意义

|

笔记

|
| --- | --- | --- |
| 我 | 投入 | 相对于卡片 |
| O | 输出 |
| 输入-输出 | 输入或输出 |
| 包裹邮递(Parcel Post) | 推/拉逻辑 |
| 平均海面 | 明渠 |
| S | 电源 |
| 网络计算机 | 未连接 | 或逻辑高 |

标清模式

SD 模式允许改变数据总线宽度,以增加 SDHC 和 SDXC 卡支持的 I/O 速率。更高的数据时钟速率也能提高传输速率。表 9-4 列出了引脚分配。

表 9-4

微型标清模式引脚

|

别针

|

名字

|

输入-输出

|

逻辑

|

描述

|
| --- | --- | --- | --- | --- |
| one | DAT2 | 输入-输出 | 包裹邮递(Parcel Post) | dota2 |
| Two | CD/日期 3 | 输入-输出 | 包裹邮递(Parcel Post) | 卡检测/数据 3 |
| three | 煤矿管理局 | 输入-输出 | 聚丙烯/外径 | 命令/响应 |
| four | 电源电压 | S | S | 电源 |
| five | 时钟信号 | 我 | 包裹邮递(Parcel Post) | 时钟 |
| six | 虚存系统 | S | S | 地面 |
| seven | 日期 0 | 输入-输出 | 包裹邮递(Parcel Post) | 数据 0 |
| eight | DAT1 | 输入-输出 | 包裹邮递(Parcel Post) | 数据 1 |

磨损均衡

不幸的是,对于执行的每个操作,闪存遭受磨损(因为每个写操作需要擦除和编程数据块)。闪存的设计要求擦除和重写大块存储器,即使单个扇区的值已经改变。因此,损耗均衡被用作延长介质寿命的技术。损耗均衡通过将数据移动到不同的物理块,同时保留相同的逻辑地址来延长寿命。

在没有支持文档的情况下,给定的存储卡是否支持损耗均衡是一个公开的问题。一些制造商可能根本不实施损耗均衡,或者使用较低水平的过度配置。SD 卡标准中没有规定损耗均衡,因此没有制造商被迫遵循 SanDisk 的领导。

直接文件系统挂载

有时候,在 Pi 离线的情况下,使用 Linux 对 SD 卡文件系统进行更改是很方便的。如果您有 USB 卡适配器,这也可以通过不同的 Pi 来完成。使用连接到 Linux 系统的 SD 卡插槽或 SD 卡读卡器,您可以直接挂载文件系统。

但是,您如何知道要安装什么呢?您至少可以应用两个有用的命令:

  • lsblk(消歧义)

  • 布瑞克

lsblk命令非常适合向您展示块设备和分区安排:

# lsblk
NAME     MAJ:MIN   RM     SIZE   RO   TYPE MOUNTPOINT
sda        8:0      0   149.1G    0   disk
├─sda1    8:1      0   147.3G    0   part /
├─sda2    8:2      0       1K    0   part
└─sda5    8:5      0     1.8G    0   part [SWAP]
sdb        8:16     1    14.5G    0   disk
├─sdb1    8:17     1    41.8M    0   part
└─sdb2    8:18     1    14.5G    0   part
sr0       11:0      1    1024M    0   rom
#

从这个显示中,您可以看到 Linux 根文件系统是从/dev/sda1开始挂载的。我们的 SD 卡出现在/dev/sdb上,有分区sdb1sdb2blkid命令给了我们更多的信息,包括分区标签:

# blkid
/dev/sda1: UUID="51d355c1-2fe1-4f0e-aaae-01d526bb27b5" TYPE="ext4" PARTUUID="61c63d91-01"
/dev/sda5: UUID="83a322e3-11fe-4a25-bd6c-b877ab0321f9" TYPE="swap" PARTUUID="61c63d91-05"
/dev/sdb1: LABEL="boot" UUID="A75B-DC79" TYPE="vfat" PARTUUID="2e37b5e0-01"
/dev/sdb2: LABEL="rootfs" UUID="485ec5bf-9c78-45a6-9314-32be1d0dea38" TYPE="ext4" \
           PARTUUID="2e37b5e0-02"

这个显示表明我们的 Pi /boot分区在/dev/sdb1上,而它的根分区在/dev/sdb2上可用。这些可以直接安装。首先确保您有目录条目来挂载它们(如果它们还不存在的话):

# mkdir /mnt/1
# mkdir /mnt/2

现在它们可以安装在:

# mount /dev/sdb1 /mnt/1
# mount /dev/sdb2 /mnt/2

这样挂载之后,您可以随意列出或更改/mnt/1 或/mnt/2 目录中的文件。

只读问题

当 Linux 抱怨你的 SD 卡是只读的,你会怎么做?

# mount /dev/sdb1 -o rw /mnt/1
mount: /dev/sdb1 is write-protected, mounting read-only

这个问题至少有三个可能的原因:

  • MicroSD 适配器上的开关已经滑到“保护”(或锁定)位置。

  • Linux 在设备上有一个软件锁。

  • 或者 MicroSD 适配器有故障(连接不良)。

MicroSD 适配器开关

用于 MicroSD 适配器的滑动开关可能是一个真正的麻烦,因为它会意外地滑入“锁定”位置。解决办法是把它拉回来,并调整开关设置。

软件保护

另一种可能是 Linux 在设备上有一个软件锁。-r1选项打开该功能:

# hdparm -r1 /dev/sdb1

/dev/sdb1:
 setting readonly to 1 (on)
 readonly      =  1 (on)
# mount /dev/sdb1 /mnt/1
mount: /dev/sdb1 is write-protected, mounting read-only
#

使用-r1命令的hdparm命令可以为设备设置软件锁。尝试在启用此锁的情况下装载文件系统会导致只读装载。解决方案是使用选项-r0禁用该保护:

# hdparm -r0 /dev/sdb1

/dev/sdb1:
 setting readonly to 0 (off)
 readonly      =  0 (off)
# mount /dev/sdb1 /mnt/1

MicroSD 适配器质量

当这个只读问题第一次发生在我身上时,我以为问题出在我使用的 Linux 计算机的硬件上。通过研究,我发现有些人报告说他们的 MicroSD 适配器有问题。在尝试了三种不同的 MicroSD 适配器后,我最终获得了成功。前两个适配器连接不良,导致设备只读。

如果您的 MicroSD 卡配有适配器,那可能就是您想要使用的适配器。然而,这并不能保证成功。

图像文件

如果您无法直接安装 SD 卡,那么您仍然可以操作图像文件。这可能是下载的 Raspbian 图像,也可能是一个朋友以某种方式提供给你的。也许他们是从电脑的 SD 卡上创建的图像:

# dd if=/dev/sdb of=/tmp/sdcard.img bs=1024k

这个dd命令使用 1 MB (1024k)的块大小将输入磁盘(/dev/sdb)复制到输出文件(/tmp/sdcard.img)。大块大小用于提高效率。

镜像文件的问题是它包含两个分区。如果它是一个单独的分区,那么它可以直接挂载。分区需要我们做更多的工作。安装kpartx将使这项任务变得更容易:

# apt-get install kpartx

现在,当我们有一个镜像文件要挂载时,我们可以如下使用它:

# kpartx -v -a /tmp/sdcard.img
add map loop0p1 (254:0): 0 85611 linear /dev/loop0 8192
add map loop0p2 (254:1): 0 30351360 linear /dev/loop0 98304

注意名字loop0p1loop0p2?使用它们在映像文件中挂载文件系统分区:

# mount /dev/mapper/loop0p1 /mnt/1
# mount /dev/mapper/loop0p2 /mnt/2

现在您将在/mnt/1中找到您的/boot文件,并且在/mnt/2中挂载 Pi 根分区。完成更改后,卸载分区:

# umount /mnt/1
# umount /mnt/2

卸载文件系统后,您可以将映像文件复制回 SD 卡。

摘要

本章简要介绍了 SD 卡及其操作。然后研究了在 Raspberry Pi 之外使用 SD 卡文件系统的两种不同方式——直接在 Linux 上挂载 SD 卡和挂载图像文件。两者都有其用途,尤其是在救援行动中。

十、通用非同步收发传输器

Raspberry Pi 有一个 UART 接口,允许它执行串行数据通信。使用的数据线是 3.3 V 逻辑电平信号,应该而不是连接到 TTL 逻辑(+5 V)(它们也是不兼容 RS-232)。要使用 RS-232 与设备通信,您需要一个转换器模块。

RS-232 转换器

虽然勤劳的人可以自己构建 RS-232 转换器,但如果有 pcb 上的廉价转换器,就没什么必要这么做了。

图 10-1 是我用过的一个 MAX232CSE 芯片 pcb。这个特殊的单元只支持 RX 和 TX 线路,没有硬件流量控制。当寻找一个单位,得到一个工作与 3.3 伏逻辑电平。有些单元只能用 TTL (+5 V)逻辑工作,这对你的 Pi 是有害的。当 MAX232CSE 芯片的 VCC 电源引脚连接到+3.3 V 时,该芯片支持 3.3 V 工作电压

img/326071_2_En_10_Fig1_HTML.jpg

图 10-1

MAX232CSE 接口

我还建议你选择一个支持硬件流控制信号的单元。寻找 CTS 和 DTR 信号。完整的 RS-232 转换器还包括 DTR、DSR 和 CD 信号。

注意

在本文中,我们将参考 3 V ,知道它更准确地说是 3.3 V。

TTL 适配器

您也可以使用 TTL 适配器,而不是将信号转换为 RS-232 所需的+/-电压。Pi 要求信号端(TTL)应能在+3.3 V 而不是通常的+5 V 下工作,使用+5 V 适配器可能会损坏 Pi。可以连接+3.3 V 的单元可能会有一个跳线来选择电压。

DTE 或 DCE

选择 RS-232 转换器时,请记住有两种串行连接:

  • DCE :数据通信设备(母接头)

  • DTE :数据终端设备(公接头)

一个普通的 USB 串行适配器(例如,用于笔记本电脑)将提供一个 DTE(公)连接器。这种电缆的布线是这样的,它预计插入到 DCE(阴)连接。当这适用于你的 Raspberry Pi 适配器时,笔记本电脑的串行适配器可以直接插入 DCE(母)连接器,消除了对交叉电缆或零调制解调器的需要。

因此,对于您的 Pi,请选择提供母(DCE)连接器的 RS-232 转换器。同样,确保您为笔记本电脑/台式机购买了一个具有公(DTE)连接的电缆或 USB 设备。连接 DTE 到 DTE 或 DCE 到 DCE 需要一根交叉电缆,根据电缆的不同,还需要一个“性别修理工”。最好从一开始就把事情“搞清楚”。

假设您对 Pi 使用了 DCE 转换器,将 RS-232 转换器的 3 V 逻辑 TX 连接到 Pi 的 TXD0,将 RX 连接到 Pi 的 RXD0 数据线。

所有这些关于 DCE 和 DTR 的事情总是让人很困惑。如果你也觉得这令人困惑,有另一种实用的方法来看待它。从您计划使用的连接器和电缆开始。确保它们在两端匹配,并且串行电缆是已知的直电缆(相对于交叉电缆)。一旦解决了这些物理问题,您就可以正确布线了。将 TX 连接到 RX,并将 RX 连接到 TX。换句话说,在 RS-232 适配器和 Raspberry Pi 之间自己布线交叉。需要记住的重要一点是,发射端需要向接收端双向发送信号。

注意

在 DB9 或 DB25 电缆上,直串行电缆将引脚 2 连接到引脚 2,引脚 3 连接到引脚 3。一根交叉电缆将穿过这两条信号线以及其他信号线。

RS-232

RS-232 是与串行通信相关的一系列标准的传统名称。它是在 1962 年由 EIA 的无线电部门首次引入的。第一个数据终端是与调制解调器(DCE)通信的电传打字机(DTE)。早期的串行通信受到不兼容的困扰,直到后来的标准发展。

串行链路包括两条数据线,数据从一个终端发送,由同一个终端接收。除了这些数据线,还有几个握手信号(比如 RTS 和 CTS)。默认情况下,Raspberry Pi 不提供这些功能。

图 10-2 显示了一个串行信号传输,时间从左到右。RS-232 设备要求信号在–15V 至+15 V 范围内变化。

img/326071_2_En_10_Fig2_HTML.jpg

图 10-2

串行信号

标准规定,当电压在–3 和–15v 之间时,信号被视为处于标记状态。如果电压在+3 和+15 V 之间,信号被视为处于空间状态。当线路空闲时,RS-232 数据线处于标记状态。

开始位

当要发送数据的异步字符时,该行首先在 1 比特的持续时间内转换到一个空间电平。这就是所谓的起始位 (0)。数据位紧随其后。

异步线路不像同步链路那样使用时钟信号。异步接收器的时钟必须与发送器的波特率匹配。接收器在位单元时间内对线路采样 16 次,以确定其值。采样有助于避免噪声脉冲触发错误的数据读取。

数据位

数据位紧跟在起始位之后,最低有效位在前。空格是 0 数据位,而标记代表 1 位。早期的电传设备使用 5 个数据位发送 5 位博多码中的字符。 11 因此,串行端口可以配置为 5、6、7 或 8 个数据位。在 ASCII 字符集扩展到 8 位之前,通常使用 7 位串行数据。

奇偶校验位

可选的奇偶校验位可以在发送时生成,也可以在接收端检测到。奇偶性可以是奇数、偶数或固定的(标记或空格)。目前最常用的设置是无奇偶校验,这样可以节省 1 位时间,从而加快通信速度。旧设备通常使用奇偶校验来防止来自噪声串行线路的错误。奇数奇偶校验优于偶数奇偶校验,因为它强制字节传输中至少有一次信号转换。这有助于提高数据的可靠性。

传号或空号奇偶校验并不常见,而且用处有限。标记奇偶校验可以与 2 个停止位一起使用,以便为非常慢的电传打字机设备有效地提供 3 个停止位。标记或空间奇偶校验降低了数据的有效吞吐量,但除了可能用于诊断目的之外,没有提供任何好处。表 10-1 总结了各种奇偶校验配置。

表 10-1

RS-232 奇偶校验设置

|

平价

|

X

|

笔记

|
| --- | --- | --- |
| 没有人 | 普通 | 无奇偶校验位 |
| 平的 | E | 1 如果偶数个数据 1 位 |
| 奇数的 | O | 1 如果奇数个数据 1 位 |
| 标记 | M | 始终处于标记水平(1) |
| 空间 | S | 总是在空间级别(0) |

停止位

异步通信要求接收机与发射机同步。因此,存在一个或多个停止位,以便接收器可以与下一个起始位的前沿同步。实际上,每个停止位后跟一个开始位,提供了内置的同步。

许多 UARTs 支持 1、1.5 或 2 个停止位。Broadcom SoC 仅支持 1 或 2 个停止位。使用两个停止位在电传打字机设备中很常见,现在可能很少使用了。使用 1 个停止位可提高整体数据吞吐量。表 10-2 总结了停止位配置。

表 10-2

停止位配置

|

停止位

|

描述

|
| --- | --- |
| one | 1 个停止位 |
| One point five | 1.5 个停止位() |
| Two | 2 个停止位 |

?不受树莓派的支持

波特率

波特率是根据每秒位数计算的,包括起始位、数据位、奇偶位和停止位。使用 115200 波特、无奇偶校验和 1 个停止位的链路提供以下数据字节速率:

$$ {\displaystyle \begin{array}{l}{D}_{rate}=\frac{B}{s+d+p+S}\ {}\kern3.75em =\frac{115200}{1+8+0+1}\ {}\kern3.75em =11,520\kern0.125em bytes/s\end{array}} $$

在哪里

B 是波特率。

s 是起始位(总是 1 位)。

d 是数据位数(5、6、7 或 8)。

p 是校验位(0 或 1)。

S 是停止位(1、1.5 或 2)。

115200 波特链路允许每秒 11250 字节。如果添加一个奇偶校验位,吞吐量会降低:

$$ {\displaystyle \begin{array}{l}{D}_{rate}=\frac{115200}{1+8+1+1}\ {}\kern3.75em =10,472.7\kern0.125em bytes/s\end{array}} $$

添加一个奇偶校验位会将传输速率降低到每秒 10,472.7 字节。

表 10-3 列出了在 Raspberry Pi 上串行链路可以配置的标准波特率。

表 10-3

标准波特率

|

速度

|

笔记

|
| --- | --- |
| Seventy-five | 电传打字机 |
| One hundred and ten | 电传打字机 |
| Three hundred | 低速(声学)调制解调器 |
| One thousand two hundred |   |
| Two thousand four hundred |   |
| Four thousand eight hundred |   |
| Nine thousand six hundred |   |
| Nineteen thousand two hundred |   |
| Thirty-eight thousand four hundred |   |
| Fifty-seven thousand six hundred |   |
| One hundred and fifteen thousand two hundred | Raspberry Pi 控制台 |

破裂

通过异步通信,还可以发送和接收中断信号。这是通过将起始位扩展到数据位和停止位之外,并最终将行返回到标记状态来实现的。当接收器看到一个空格而不是停止位的标记时,它会看到一个帧错误

一些 UARTs 通过记录线路保持在空白状态的时间来区分成帧错误和中断。一个简单的成帧错误可能作为噪声串行线路通信的一部分发生(特别是当使用调制解调器时),通常归因于接收字符错误。在没有中断检测的情况下,当序列中出现几个成帧错误时,可以假定已经接收到中断。然而,短序列的成帧错误也可能只是表明两个端点之间的波特率不匹配。

流控制

任何从一端传输到另一端接收器的链路都存在流量控制问题。想象一条工厂装配线,待装配的零件到达工人工作站的速度比他/她装配它们的速度快。在某些时候,传送带必须暂时停止,否则一些零件将无法组装。或者,如果传送带速度降低,装配工人将能够跟上,但速度低于最佳速度。

除非串行链路接收器能够像数据到达一样快地处理数据的每个字符,否则它将需要流量控制。最简单的方法是简单地降低波特率,这样接收器就能一直跟上。但是这并不总是令人满意的。日志记录应用可能能够快速写入信息,例如,当写入 SD 卡时除外。

一个更好的方法是当接收者陷入困境时,向发送者发出停止发送的信号。一旦接收器赶上,它就可以告诉发送器恢复传输。请注意,串行链路的两端都存在此问题:

  • 传输到终端的数据(DTE)

  • 数据传输到数据通信设备(DCE)

使用两种形式的流量控制:

  • 硬件流控制

  • 软件流程控制

硬件流控制

硬件流量控制使用额外的信号线来调节数据流。RS-232 标准定义了一组相当复杂的信号,但流量控制所需的主要信号如表 10-4 所示。与数据线不同,这些信号在空间状态下是无效的,而在标记状态下是有效的。

表 10-4

硬件流控制

|

数据终端设备(Data Terminal Equipment)

|

方向

|

数据通信设备(Data Communications Equipment)

|

描述

|

活跃的

|
| --- | --- | --- | --- | --- |
| 即时战略游戏 | →中 | 即时战略游戏 | 请求发送 | 低的 |
| 同CARPAL TUNNEL SYNDROME | ← | 同CARPAL TUNNEL SYNDROME | 允许发送() |
| 每日业务报告(Daily Service Report) | ← | 每日业务报告(Daily Service Report) | 数据集就绪 | 低的 |
| 负荷型定额(Duty Type Rating) | →中 | 负荷型定额(Duty Type Rating) | 数据终端就绪 |

主要流量控制信号

最重要的信号是表 10-4 中用匕首标记的信号。例如,当 CTS 激活(标记)时,DCE (Pi)表示可以发送数据。如果 DCE 因数据量过大而不堪重负,CTS 信号将变为非活动(空白)状态。看到这个,DTE(桌面)被要求停止发送数据。否则,可能会丢失数据。

类似地,作为 DTE 运行的桌面从 DCE (Pi)接收数据。如果笔记本电脑被输入的大量数据淹没,RTS 信号将变为非活动状态(space)。然后,远端(DCE)将停止传输。当桌面赶上时,它会重新声明,允许 DCE 恢复。

DTR 和 DSR 信号旨在传达两端设备的就绪状态。如果终端被认为没有准备好(DTR),DSR 不会被 DCE 激活。类似地,终端不会断言 DTR,除非它准备好了。在现代串行链路中,DTR 和 DSR 通常被认为是正确的,只留下 CTS 和 RTS 来处理流量控制。

在需要流量控制的地方,硬件流量控制被认为比软件流量控制更可靠。

软件流程控制

为了简化串行通信的布线和支持硬件,可以省略/忽略硬件流控制。取而代之的是使用数据协议。

最初,链路的每一端都假定准备好接收数据。发送数据,直到收到一个XOFF字符,表示传输应该停止。当接收器准备好再次恢复接收时,发送XON字符。这些软件流程控制字符如表 10-5 所示。

表 10-5

软件流控制字符

|

密码

|

意义

|

美国信息交换标准代码

|

十六进制

|

键盘

|
| --- | --- | --- | --- | --- |
| 唇 | 暂停传输 | DC3 | Thirteen | 控制-S |
| 发送朴通 | 恢复传输 | DC1 | Eleven | 控制 |

在终端会话中,键盘命令可用于控制串行连接。例如,如果信息显示得太快,用户可以键入 Ctrl-S 来停止传输。按 Ctrl-Q 允许它继续。

软件流控制的缺点包括以下几点:

  1. 线路噪声会阻止接收器看到XOFF字符,并导致数据丢失(由于数据溢出)。

  2. 线路噪声会使远端看不到XON字符,并可能无法恢复传输(导致链路“锁定”)。

  3. 线路噪声会导致接收到错误的XON / XOFF字符(数据丢失或链路锁定)。

  4. 如果接收缓冲区已满,远程终端看到发送的XOFF字符的延迟会导致数据丢失。

  5. XONXOFF字符不能用于传输链路中的数据。

问题 1 至 3 可能会导致链路锁定或数据丢失。如果缓冲区足够早地通知另一端以防止溢出,问题 4 是可以避免的。问题 5 是二进制数据传输的问题。

树莓馅饼

Raspberry Pi 支持两种 UARTs:

|

通用非同步收发传输器(Universal Asynchronous Receiver/Transmitter)

|

五金器具

|

结节

|

通用输入输出接口

|

中高音

|
| --- | --- | --- | --- | --- |
| UART0 | PL011 | /dev/ttyAMA0 | 14 & 15 | Zero |
| UART1 | 迷你 UART | /dev/ttys 0 | 14 & 15 | five |

使用 PL011 还是迷你 UART 取决于 Raspberry Pi 的型号。本来这个问题回答起来很简单。B 型和 A 型 Pi 只是使用 PL011 ( /dev/ttyAMA0)设备作为控制台。迷你 UART ( /dev/ttyS0)是一个不同的硬件模块,也是可用的,尽管功能有限。

随着 Pi 3 和 Pi Zero W 上无线和蓝牙功能的增加,PL011 UART 被用于 BT(蓝牙)和 WIFI 支持,而 mini UART 则取代了串行控制台。所有其他型号都使用首选的 PL011 设备作为控制台。

但是,由于使用了设备树覆盖,分配内容的规则变得更加复杂。更多细节可从 raspberrypi 获得。org 。下面的在线文件,不包括日期戳,有更多的血淋淋的细节:

https://www.raspberrypi.org/documentation/configuration/uart.md

哪个在用?

您可以验证正在使用哪个串行设备,如下所示:

$ cat /boot/cmdline.txt
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 ...

console=选项可以在内核命令行中出现多次。在这个 Raspberry Pi 3 Model B 示例中,我们看到定义了两个控制台,但只有一个是串口(serial0)。列出串行设备显示:

$ ls -l /dev/serial0
lrwxrwxrwx 1 root root 5 Jun 19 22:04 /dev/serial0 -> ttyS0

名称/dev/serial0是实际设备/dev/ttyS0的符号链接。

$ ls -l /dev/ttyS0
crw--w---- 1 root tty 4, 64 Jun 19 22:04 /dev/ttyS0

所以这个 Pi 被配置为使用迷你 UART ( /dev/ttyS0)。

在我的 Raspberry Pi 3 B+上列出引导命令行产生了:

$ cat /boot/cmdline.txt
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 ...

它号称用的是/dev/serial0,但是这个配置有没有 serial0 设备:

$ ls /dev/serial0
ls: cannot access '/dev/serial0': No such file or directory

raspberrypi.org 页面还指出:

如果 cmdline.txt 使用别名 serial0 来指代用户可访问的端口,无论是否使用该覆盖,固件都会将其替换为适当的端口。

禁用串行控制台

如果你想将串行设备用于非控制台的目的,那么显然我们必须禁用控制台。最简单的方法是成为 root 用户并使用raspi-config:

# raspi-config

光标向下选择“界面选项”并按回车键(图 10-3 ))。

img/326071_2_En_10_Fig3_HTML.jpg

图 10-3

raspi-config 的打开对话框,选择了“接口选项”

然后光标向下(图 10-4 )选择“串行”并按Return

img/326071_2_En_10_Fig4_HTML.jpg

图 10-4

在 raspi-config 中选择“串行”,然后按回车键

然后选择<No>禁用控制台,按Return(图 10-5 )。

img/326071_2_En_10_Fig5_HTML.jpg

图 10-5

选择,禁用 raspi-config 中的控制台

通过提示您是否要重启,对话框结束(图 10-6 )。这对于内核中的控制台更改是必要的。如果您已经有了选择的设置,可以跳过此提示。

img/326071_2_En_10_Fig6_HTML.jpg

图 10-6

raspi-config 对话框最后会询问您是否要重新启动

Raspberry Pi 3 串行控制台

在连接串口并启用控制台的情况下启动 Raspberry Pi 3时,启动后会显示以下登录提示:

Raspbian GNU/Linux 9 rpi3bplus ttyS0
rpi3bplus login: pi
Password:

注意提示上方的“ttyS0”。尽管事实上/dev/serial0并不存在(在 B+上),拉斯边安排/dev/ttyS0(迷你 UART)用作控制台。

Raspberry Zero 串行控制台

在串行控制台启用的情况下启动 Raspberry Pi Zero(非 W)确认其使用 PL011 (/dev/ttyAMA0设备作为控制台:

Raspbian GNU/Linux 8 raspberrypi ttyAMA0

raspberrypi login: pi
Password:

图 10-7 所示的 Raspberry Pi Zero 有一个焊接到电路板上的母接头带。串行端口适配器是使用 MAX232CSE IC 的真正 RS232C 转换器。这些连接是:

  • P1-01 (+3.3 伏)至 VCC

  • P1-06 呼叫地面

  • P1-08 (GPIO14)至 TX

  • P1-10 (GPIO15)至 RX

img/326071_2_En_10_Fig7_HTML.jpg

图 10-7

Raspberry Pi Zero,串行控制台连接到 RS232C 适配器。该适配器插入了真正的 RS232C 转 USB 接口。

在这种安排下,没有流量控制。考虑到波特率很高,如果需要提高数据完整性,可以将波特率降低到 9600 波特。

PL011 和微型 UART 的区别

迷你 UART 的 FIFOs 较小,不支持硬件流控制。如果没有流量控制,它将很容易在高数据速率下丢失数据。mini 的波特率也与 VPU 时钟相关,这导致了其他问题。

微型 UART 在以下方面也有缺陷:

  • 无中断检测

  • 无成帧错误检测

  • 不支持奇偶校验位

  • 无接收超时中断

  • 没有 DCD,DSR,DTR,或里的信号

VPU 时钟给 UART 带来了一个问题,因为 VPU 频率调节器通常会改变内核频率。这将导致 UART 波特率也发生变化。raspberrypi.org 国家:

通过将 enable_uart=1 添加到 config.txt,可以重新启用 Linux 控制台。这也将 core_freq 固定为 250Mhz(除非设置了 force_turbo,否则它将固定为 400Mhz),这意味着 uart 波特率保持一致。

尽可能避免使用微型 UART。

PL011 UART 特性

Broadcom BCM2835 ARM 外设手册声明以下功能不支持:

  • 红外数据协会(IrDA)支持

  • 串行红外(SIR)协议编码器/解码器(endec)

  • 直接内存访问(DMA)

  • 不支持DCD、DSR、DTR 和里的信号

然而,支持以下功能:

  • 独立的 16×8 发送和 16×12 接收 FIFO 缓冲器

  • 可编程波特率发生器

  • 错误起始位检测

  • 断线生成和检测

  • 支持控制功能 CTS 和 RTS

  • 可编程硬件流量控制

  • 完全可编程串行接口特性:

    • 数据可以是 5、6、7 或 8 位。

    • 偶数、奇数、标记、空白或非奇偶校验位的生成和检测。

    • 1 或 2 个停止位生成。

    • 波特率生成,DC 高达 UARTCLK/16。

Broadcom 还表示,其 UART 和 16C650 UART 的实现存在一些差异。但是这些主要是设备驱动程序的细节:

  • 接收 FIFO 触发电平为 1/8、1/4、1/2、3/4 和 7/8。

  • 发送 FIFO 触发电平为 1/8、1/4、1/2、3/4 和 7/8。

  • 内部寄存器映射地址空间和每个寄存器的位功能不同。

  • 1.5 停止位不支持

  • 独立接收时钟。

应用开发人员唯一真正关心的是 1.5 停止位配置选项是而不是可用的,这在最近很少使用。

如果需要 RS-232 DCD、DSR、DTR 和 RI 信号,可以使用 GPIO 输入和输出引脚(以及适当的 RS-232 线路电平转换器)来实现。这些是变化相对较慢的信号,可以在用户空间中轻松处理。然而,这种方法的一个限制是缺少设备驱动程序提供的挂起 TTY 控制。要改变这种情况,可以修改设备驱动程序源代码,使用 GPIO 来支持这些信号。对此感兴趣的 Raspbian Linux 模块如下:

drivers/tty/serial/amba-pl011.c

UART GPIO 引脚

默认情况下,发送和接收引脚是 GPIO 14 (TX)和 15 (RX),分别是 GPIO 头上的 P1-08 和 P1-10 引脚。当 PL011 器件可用时,当选择备用功能 5 时,硬件流控制信号也可以出现在 GPIO 头上。表 10-6 列出了这些连接。

表 10-6

UART 引脚

|

功能

|

通用输入输出接口

|

P1/P5

|

中高音

|

方向

|

描述

|
| --- | --- | --- | --- | --- | --- |
| 数据发送 | Fourteen | P1-08 | Zero | 在外 | DTE 传输数据 |
| 接收数据 | Fifteen | P1-10 | Zero | 在…里 | DTE 接收数据 |
| 即时战略游戏 | Seventeen | P1-11 | five | 在外 | 请求发送 |
| 同CARPAL TUNNEL SYNDROME | Thirty | P1-36 | five | 在…里 | 清除发送 |

RTS/CTS 访问

配置后,硬件流控制 CTS 和 RTS 分别在 GPIO 30 (P1-26)和 17 (P1-11)上可用。默认情况下,这些是 GPIO 输入,但可以重新配置。要访问 UART 的 CTS 和 RTS 信号,将 GPIO 30 和 17 配置为备用功能 5。

摘要

随着 Raspberry Pi 成熟成为新的型号,与串行设备相关的功能变得更加复杂。然而 Raspberry Pi Foundation 已经为您提供了raspi-config工具来简化串行控制台或专用串行线的配置。

有了提供的信息,您将能够使用串行适配器登录到您的无头 Raspberry Pi Zero。这些信息让你尽可能地利用这一宝贵的资源。

十一、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 | 密码(p) | 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 放大器有效(该放大器始终使能输出)。这代表最弱的驱动强度设置。

img/326071_2_En_11_Fig1_HTML.jpg

图 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)才能将电流送入负载。这是一个高电平有效配置的例子。

img/326071_2_En_11_Fig2_HTML.jpg

图 11-2

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

下沉电流

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

img/326071_2_En_11_Fig3_HTML.jpg

图 11-3

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

驱动 led

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

img/326071_2_En_11_Fig4_HTML.jpg

图 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 的电流并保护输出晶体管,应使用限流电阻(R)。使用欧姆定律计算电阻:

$$ R=\frac{V}{I} $$

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

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

$$ R=\frac{V_{CC}-{V}_{LED}}{I_{LED}} $$

其中:

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

V LED 为 LED 的正向压降。

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

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

$$ {\displaystyle \begin{array}{l}R=\frac{3.3-1.63}{0.008}\ {}\kern1.75em =208.75\varOmega \end{array}} $$

由于电阻采用标准值,因此我们四舍五入至最接近的标准 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 时更高)。

img/326071_2_En_11_Fig5_HTML.jpg

图 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 输出的极性。

img/326071_2_En_11_Fig6_HTML.jpg

图 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 端口使添加键盘和鼠标成为一件简单的事情。观察力敏锐的人可能会注意到,这款显示器的显示器支架是由另一款显示器改装而成的。

img/326071_2_En_12_Fig1_HTML.jpg

图 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 说明了该电路。

img/326071_2_En_13_Fig1_HTML.jpg

图 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 的最大电流:

$$ {\displaystyle \begin{array}{l}{I}_C=\frac{P_{Q1}}{V_{CE}}\ {}\kern2em =\frac{1}{0.3}\ {}\kern2em =3.3A\end{array}} $$

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

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

$$ {\displaystyle \begin{array}{l}{I}_B=\frac{I_C}{H_{FE}}\ {}\kern1.875em =\frac{100\kern0.125em mA}{50}\ {}\kern1.875em =2\kern0.125em mA\end{array}} $$

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

$$ {\displaystyle \begin{array}{l}{R}_1=\frac{GPI{O}_{HIGH}-{V}_{BE}}{I_B}\ {}\kern1.875em =\frac{3-0.7}{0.002}\ {}\kern1.875em =1150\kern0.125em ohms\end{array}} $$

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

感性负载

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

img/326071_2_En_13_Fig2_HTML.jpg

图 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 时,电流将从总线流向地,类似于信号短路。连接到总线的从设备将看到接近零的电压。

img/326071_2_En_14_Fig1_HTML.jpg

图 14-1

单线驱动电路

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

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

主人和奴隶

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

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

草案

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

重置

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

img/326071_2_En_14_Fig2_HTML.jpg

图 14-2

单线复位协议

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

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

注意

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

数据输入输出

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

img/326071_2_En_14_Fig3_HTML.jpg

图 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 从设备的引脚排列。该温度传感器是许多单线从机的典型器件。

img/326071_2_En_14_Fig4_HTML.jpg

图 14-4

DS18B20 引脚排列

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

img/326071_2_En_14_Fig5_HTML.jpg

图 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 展示了作者使用的试验板设置。

img/326071_2_En_14_Fig6_HTML.jpg

图 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。主机使用该地址引用每个外设。非寻址外设应该保持安静。

img/326071_2_En_15_Fig1_HTML.jpg

图 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 位时间后,时钟将变为低电平。这种特殊的信号组合通知所有连接的设备“监听”,因为下一条传输的信息将是设备地址。

img/326071_2_En_15_Fig2_HTML.jpg

图 15-2

I2C 启动/停止信号

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

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

数据位

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

img/326071_2_En_15_Fig3_HTML.jpg

图 15-3

I2C 数据位传输

消息格式

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

img/326071_2_En_15_Fig4_HTML.jpg

图 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 芯片的$$ \overline{RESET} $$引脚连接到+3.3 V。

接线

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

img/326071_2_En_15_Fig5_HTML.jpg

图 15-5

MCP23017 至 Raspberry Pi 的接线

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

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

  • 重要!将$$ \overline{RESET} $$线连接到+3.3 V。否则会发生随机或完全故障。

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

$$ \overline{RESET} $$线没有连接到+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 时,一个字节的传输需要:

$$ {\displaystyle \begin{array}{l}t=\frac{1}{100 kHz}\times 8 bits\ {}\kern0.75em =80\mu s\end{array}} $$

读取一个 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 端口。

十六、SPI 总线

串行外设接口总线,被亲切地称为 spy ,是由摩托罗拉命名的同步串行接口。18SPI 协议以全双工模式工作,允许它同时发送和接收数据。一般来说,SPI 比 I 2 C 协议有速度优势,但是需要更多的连接。

SPI 基础知识

SPI 总线上的器件以主机/从机方式通信。多个从机共存于一条给定的 SPI 总线上,每个从机由一个从机选择信号(也称为片选)选择通信。图 16-1 显示 Raspberry Pi 作为主机与一个从机通信。除了将使用不同的从机选择信号之外,将如图所示连接额外的从机。

img/326071_2_En_16_Fig1_HTML.jpg

图 16-1

SPI 接口

数据通过 MOSI 线从主机传输到从机(主机输出,从机输入)。当主机发送每个位时,从机同时在 MISO 线上发送数据(主机输入,从机输出)。位从主机移入从机,位从从机移入主机。两次传输都在系统时钟的节拍下进行(CLK)。

许多 SPI 器件仅支持 8 位传输,而其他器件则更加灵活。SPI 总线是事实上的标准,这意味着数据传输宽度和 SPI 模式没有标准。18SPI 控制器也可以配置为首先传输最高有效位或最低有效位。所有这些灵活性都会导致混乱。

SPI 模式

SPI 以四种可能的时钟信号模式之一工作,基于两个参数:

|

参数

|

描述

|
| --- | --- |
| 断续器 | 时钟极性 |
| CPHA | 时钟相位 |

每个参数有两种可能性,因此 SPI 有四种可能的工作模式。表 16-1 列出了所有四种可用模式。注意,给定的模式通常用一对数字来表示,如 1,0 或简单地称为模式 2 (对于同一模式,如表中所示)。两种引用类型都显示在模式列中。

表 16-1

SPI 模式

|

断续器

|

CPHA

|

方式

|

描述

|
| --- | --- | --- | --- |
| Zero | Zero | 0,0 | Zero | 同相时钟,在上升沿采样 |
| Zero | one | 0,1 | one | 同相时钟,在下降沿采样 |
| one | Zero | 1,0 | Two | 反相时钟,在上升沿采样 |
| one | one | 1,1 | three | 反相时钟,在下降沿采样 |
|   | 时钟感应 | 描述 |
| 非反转 | 信号为空闲低电平,有效高电平 |
| 反向的 | 信号为高电平空闲,低电平有效 |

外围设备制造商在开始时没有定义标准的信号惯例。因此,SPI 控制器通常允许配置四种模式中的任何一种,而其余的只允许两种模式。然而,一旦选择了一种模式,同一总线上的所有从机都必须同意。

发信号

时钟极性决定空闲时钟电平,而相位决定数据线是在时钟信号上升还是下降时进行采样。图 16-2 显示了模式 0,0,这可能是 SPI 信号的首选形式。在图 16-2 中,通过激活$$ \overline{SS} $$(从机选择)首先选择从机。一次只能选择一个从机,因为必须有一个从机驱动 MISO 线。选择从机后不久,主机驱动 MOSI 线,从机用第一个数据位同时驱动 MISO 线。这可能是最高位,也可能是最低位,具体取决于控制器的配置。该图首先显示最低有效位。

img/326071_2_En_16_Fig2_HTML.jpg

图 16-2

SPI 信号,模式 0 和 2

在模式 0,0 下,当时钟线从高电平变为低电平时,第一位被读入主机和从机。该时钟跳变位于数据位单元的中间。随着时钟从高电平变为低电平,其余位依次同时输入主机和从机。当主机停用从机选择线时,传输结束。当时钟极性反转时(CPOL = 1,CPHA = 0),图 16-2 所示的时钟信号简单反转。数据在数据单元中同时计时,但在时钟的上升沿计时。

图 16-3 显示了相位设置为 1 (CPHA = 1)的时钟信号。当时钟不反转时(CPOL = 0),数据在上升沿输入。时钟必须比相位为 0 (CPHA = 0)时提前半个时钟周期转换到非理想状态。当 SPI 模式为 1,1 时,数据在时钟的下降沿读入。

img/326071_2_En_16_Fig3_HTML.jpg

图 16-3

SPI 信号模式 1 和 3

虽然这四种不同的模式可能会造成混淆,但必须认识到,数据是在数据位单元内同时采样的。数据位总是在数据单元的中点进行采样。当时钟相位为 0 (CPHA = 0)时,根据 CPOL,数据在时钟的下降沿或上升沿进行采样。当时钟相位为 1 (CPHA = 1)时,根据 CPOL,数据在时钟的上升沿或下降沿进行采样。

从属选择

与 I 2 C 不同,在 I2C 中,从机通过发送地址寻址,SPI 总线为每个从机使用一条专用选择线。Raspberry Pi 将表 16-2 中列出的 GPIO 引脚用作从机选择线(也称为芯片使能线)。

表 16-2

Raspberry Pi 内置芯片使能引脚

|

通用输入输出接口

|

芯片使能

|

第一亲代

|
| --- | --- | --- |
| eight | $$ \overline{CE0} $$ | P1-24 |
| seven | $$ \overline{CE1} $$ | P1-26 |

Raspbian Linux 内核驱动程序只支持使用这两条芯片使能线。但是,驱动程序的设计使得您不必使用它们,或者只使用这些。例如,在用户软件控制下,可以使用不同的 GPIO 引脚作为选择。应用只负责在数据 I/O 之前激活从机选择 GPIO 线,并在之后将其停用。当驱动器控制从属选择时,这是自动完成的。

驱动程序支持

要启用 SPI 驱动程序,请编辑/boot/config.txt文件,取消对该行的注释,如下所示:

dtparam=spi=on

然后重新启动:

# sync
# /sbin/shutdown -r now

重新启动后,使用lsmod命令,您应该看到驱动程序spi_bcm2835列在其他驱动程序中。

$ lsmod
Module                  Size  Used by
fuse                  106496  3
rfcomm                 49152  6
...
spi_bcm2835            16384  0
...

一旦出现内核模块支持,设备驱动程序节点应该会出现:

$ ls /dev/spi*
/dev/spidev0.0  /dev/spidev0.1
$

这两个设备节点是根据应该激活哪个从机选择来命名的,如表 16-3 所示。

表 16-3

SPI 器件节点

|

路径名

|

公共汽车

|

设备

|

通用输入输出接口

|

$$ \overline{SS} $$

|
| --- | --- | --- | --- | --- |
| /dev/spidev0.0 | Zero | Zero | eight | $$ \overline{CE0} $$ |
| /dev/spidev0.1 | Zero | one | seven | $$ \overline{CE1} $$ |

如果您用 C 宏SPI_NO_CS打开这些设备节点中的任何一个,所选择的节点没有任何区别。宏SPI_NO_CS指示从机选择将由应用而不是驱动程序执行,如果使用任何选择的话。当只连接一个从设备时,是否可以使用永久硬连线选择。

SPI API

像 Linux 下的 I 2 C 一样,SPI 的裸机 API 包括对ioctl(2)的调用,以配置接口和同步读/写。通常的read(2)write(2)系统调用可以用于单向传输。

头文件

SPI 编程所需的头文件如下:

#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>

spidev.h include文件定义了几个宏和struct spi_ioc_transfer。表 16-4 列出了声明的主要宏。宏SPI_CPOLSPI_CPHA用于值SPI_MODE_x的定义。如果您愿意,可以使用SPI_CPOLSPI_CPHA来代替模式宏。

表 16-4

SPI 宏定义

|

|

支持

|

描述

|
| --- | --- | --- |
| SPI_CPOL | 是 | 时钟极性反转(CPOL = 1) |
| SPI_CPHA | 是 | 时钟相位为 1 (CPHA = 1) |
| SPI_MODE_0 | 是 | SPI 模式 0.0(cpol = 0,CPHA = 0) |
| SPI_MODE_1 | 是 | SPI 模式 0.1(cpol = 0,CPHA = 1) |
| SPI_MODE_2 | 是 | SPI 模式 1.0(cpol = 1,CPHA = 0) |
| SPI_MODE_3 | 是 | SPI 模式 1.1(cpol = 1,CPHA = 1) |
| SPI _ CS _ 高电平 | 是 | 片选高电平有效 |
| SPI_LSB_FIRST | 不 | LSB 首先传输 |
| SPI _ 线 | 不 | 使用 3 线数据 I/O 模式 |
| SPI_LOOP | 不 | MOSI/MISO 数据线环路 |
| SPI_NO_CS | 是 | 不应用芯片选择 |
| SPI _ 就绪 | 不 | 启用额外就绪信号 |

与 SPI 器件通信包括以下系统调用:

  • open(2):打开 SPI 设备驱动节点

  • read(2):读取但无传输

  • write(2):写入数据,同时丢弃接收的数据

  • ioctl(2):用于配置和双向输入输出

  • close(2):关闭 SPI 设备驱动节点

在 SPI 通信中,read(2)write(2)的使用一般不常见。通常,ioctl(2)用于促进同步读/写传输。

开放式设备

为了通过内核驱动程序执行 SPI 通信,您需要使用open(2)打开其中一个设备节点。设备路径名的一般格式是

/dev/spidev<bus>.<device>

以下是打开总线 0、设备 0 的代码片段。

int fd;

fd = open("/dev/spidev0.0",O_RDWR);
if ( fd < 0 ) {
    perror("Unable to open SPI driver");
    exit(1);
}

因为 SPI 通常涉及读写,所以驱动器通常为读写(O_RDWR)打开。

SPI 宏模式

在执行 SPI 通信之前,需要选择通信模式。表 16-5 列出了可用于配置 SPI 模式的 C 语言宏。

表 16-5

SPI 宏模式

|

|

影响

|

评论

|
| --- | --- | --- |
| SPI_CPOL | CPOL = 1 | 或者使用 SPI_MODE_x |
| SPI_CPHA | CPHA = 1 | 或者使用 SPI_MODE_x |
| SPI _ CS _ 高电平 | SS 高电平有效 | 与众不同的 |
| SPI_NO_CS | 不要断言选择 | 未使用/应用控制 |

这些位值简单地进行“或”运算,以指定所需的选项。使用SPI_CPOL意味着 CPOL = 1。它的不存在意味着 CPOL = 0。类似地,使用SPI_CPHA意味着 CPHA = 1,否则 CPHA = 0。宏SPI_MODE_x使用SPI_CPOLSPI_CPHA来定义它们,所以不要在你的代码中同时使用它们。模式定义如下所示:

#define SPI_MODE_0 (0|0)
#define SPI_MODE_1 (0|SPI_CPHA)
#define SPI_MODE_2 (SPI_CPOL|0)
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)

不支持的选项不会显示,尽管将来可能会支持其中的一个或多个选项。

下面是一个定义SPI_MODE_0的例子:

uint8_t mode = SPI_MODE_0;
int rc;

rc = ioctl(fd,SPI_IOC_WR_MODE,&mode);
if ( rc < 0 ) {
    perror("Can't set SPI write mode.");

如果您想了解 SPI 驱动器当前的配置,您可以使用ioctl(2)读取 SPI 模式,如下所示:

uint8_t mode;
int rc;

rc = ioctl(fd,SPI_IOC_RD_MODE,&mode);
if ( rc < 0 ) {
     perror("Can't get SPI read mode.");

每字位数

SPI 驱动器需要知道每个 I/O 字要传输多少位。虽然驱动程序可能会默认为 8 位,但最好不要依赖它。请注意,在 LoSSI 模式(低速串行接口)下,Pi 仅支持 8 位或 9 位。这通过以下ioctl(2)调用进行配置:

uint8_t bits = 8;
int rc;

rc = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD,&bits);
if ( rc < 0 ) {
    perror ("Can't set bits per SPI word.");

可以用ioctl(2)获取当前配置的值,如下所示:

uint8_t bits;
int rc;

rc = ioctl(fd,SPI_IOC_RD_BITS_PER_WORD,&bits);
if ( rc == −1 ) {
    perror("Can't get bits per SPI word.");

当位数不是 8 的偶数倍时,假设这些位是右对齐的。例如,如果字长设置为 4 位,则传输最低有效的 4 位。高阶位被忽略。

同样,当接收数据时,最低有效位包含数据。然而,所有这些在 Pi 上都是理论上的,因为驱动程序只支持字节范围的传输。

时钟频率

要配置数据传输速率,您可以使用ioctl(2)设置时钟速率,如下所示:

uint32_t speed = 500000; /* Hz */
int rc;

rc = ioctl(fd,SPI_IOC_WR_MAX_SPEED_HZ,&speed);
if ( rc < 0 ) {
    perror("Can't configure SPI clock rate.");

speed 中提供的时钟速率应该是 2 的倍数(它会自动向下舍入)。可以使用下面的ioctl(2)调用获取当前配置的时钟速率:

uint32_t speed; /* Hz */
int rc;

rc = ioctl(fd,SPI_IOC_RD_MAX_SPEED_HZ,&speed);
if ( rc < 0 ) {
    perror("Can't get SPI clock rate.");

数据输入输出

SPI 通信通常会在发送数据的同时接收数据。因此,不能使用read(2)write(2)系统调用。然而,ioctl(2)调用将同时执行读和写。

ioctl(2)调用的SPI_IOC_MESSAGE(n)形式使用以下结构作为其参数:

struct spi_ioc_transfer {
    __u64   tx_buf;        /* Ptr to tx buffer */
    __u64   rx_buf;        /* Ptr to rx buffer */
    __u32   len;           /* # of bytes */
    __u32   speed_hz;      /* Clock rate in Hz */
    __u16   delay_usecs;   /* Delay in microseconds */
    __u8    bits_per_word; /* Bits per "word" */
    __u8    cs_change;     /* Apply chip select */
    __u32   pad;           /* Reserved */
};

tx_bufrx_buf结构成员被定义为 64 位无符号整数(__u64)。因此,在对缓冲区指针进行赋值时,必须对其进行强制转换:

uint8_t tx[32], rx[32];
struct spi_ioc_transfer tr;

tr.tx_buf = (unsigned long) tx;
tr.rx_buf = (unsigned long ) rx;

在 Raspberry Pi 上,您将看到简单地将指针指向unsigned long的示例代码。编译器会自动将这些 32 位值提升为 64 位值。这在 Pi 上是安全的,因为指针值的大小是 32 位。

如果您不想接收数据(可能因为它是“无关紧要”的数据),您可以将接收缓冲区清空:

uint8_t tx[32];
struct spi_ioc_transfer tr;

tr.tx_buf = (unsigned long) tx;
tr.rx_buf = 0;                  /* ignore received data */

请注意,要接收数据,主机必须始终发送数据,以便将数据移出从机外设。如果发送任何字节都可以,可以省略发送缓冲器。然后,驱动器将自动发送零字节,以将从机数据移出。

也允许从您正在接收的缓冲区传输到:

uint8_t io[32];
struct spi_ioc_transfer tr;

tr.tx_buf = (unsigned long) io;         /* Transmit buffer */
tr.rx_buf = (unsigned long) io;         /* is also recv buffer */

len结构成员表示 I/O 传输的字节数。接收和发送缓冲区(当两者都使用时)应该传输相同数量的字节。

成员speed_hz定义了您希望用于该 I/O 的时钟频率,单位为 Hz。在 I/O 期间,这将覆盖模式设置中配置的任何值。必要时,该值将自动向下舍入到支持的时钟速率。

当值speed_hz为 0 时,使用之前配置的时钟速率(SPI_IOC_WR_MAX_SPEED_HZ)。

delay_usecs成员不为零时,它指定传输之间延迟的微秒数。它在传输结束时应用,而不是在传输开始时应用。当单个ioctl(2)请求中有多个 I/O 传输时,这允许中间有时间,以便外设可以处理数据。

bits_per_word成员定义了一个“字”单元中有多少位。通常单位是 1 字节(8 位),但也不需要(但注意 Raspbian Linux 驱动程序在 LoSSI 模式下只支持 8 位或 9 位)。

bits_per_word值为 0 时,使用之前从SPI_IOC_WR_BITS_PER_WORD配置的值。

cs_change成员被视为布尔值。为 0 时,驱动器不执行片选。应用需要通知外设它被选中(通常 GPIO 引脚被拉低)。I/O 完成后,应用必须取消选择从外设。

cs_change成员为true(非零)时,选择的从机将依赖于打开的设备路径名。总线和从机地址嵌入在设备名称中:

/dev/spidev<bus>.<device>

cs_changetrue时,驱动器在 I/O 之前断言spidev0.0$$ \overline{GPIO8} $$,断言spidev0.1$$ \overline{GPIO7} $$,并在完成后将其去激活。当然,使用这两个节点需要两个不同的open(2)调用。

SPI_IOC_MESSAGE(n)宏在ioctl(2)调用中用于执行一个或多个 SPI I/O 操作。这个宏不同寻常,因为它需要一个参数 n 。(这与 I2C 的方法有很大不同。)这指定了您想要执行的 I/O 传输次数。为每个所需的传输声明并配置一个spi_ioc_transfer结构数组,如下例所示:

struct spi_ioc_transfer io[3];    /* Define 3 transfers */
int rc;

io[0].tx_buf = ...;               /* Configure I/O */
...
io[2].bits_per_word = 8;

rc = ioctl(fd,SPI_IOC_MESSAGE(3),& io[0]);

前面的示例将执行三次 I/O 传输。由于应用从未在这些 I/o 之间执行任何 GPIO 操作,这适用于与一个特定的从设备通信。

以下示例代码将所有概念结合在一起,以演示一个 I/o。spi_ioc_transfer结构被初始化,以便发送 32 个字节,同时接收 32 个字节。

uint8_t tx[32], rx[32];
struct spi_ioc_transfer tr;
int rc;

tr.tx_buf        = (unsigned long) tx;
tr.rx_buf        = (unsigned long) rx;
tr.len           = 32;
tr.delay_usecs   = delay;
tr.speed_hz      = speed;
tr.bits_per_word = bits;

rc = ioctl(fd,SPI_IOC_MESSAGE(1),&tr);
if ( rc < 1 ) {
    perror("Can't send spi message");

这里发生单个 I/O 传输,数据从阵列tx发送,并被阵列rx接收。ioctl(2)调用的返回值返回传输的字节数(本例中为 32)。否则,返回-1以指示发生了错误。

关闭

像所有 Unix I/O 操作一样,当不再需要打开的文件描述符时,设备必须关闭(否则将在进程终止时关闭):

close(fd);

如果接收的数据不重要,可以使用write(2)系统调用。但是,请注意,这个调用不可能有延迟。

阅读

read(2)系统调用实际上不适合 SPI,因为主机必须在 MOSI 上传输数据,以便从机在 MISO 线路上发回位。然而,当使用read(2)时,驱动器将自动发送必要的零位以完成读取。(请注意,您的外设将接受零字节,而不会产生意外后果。)就像write(2)呼叫一样,没有延迟是可能的。

SPI 测试

开发 SPI 通信软件时,您可以执行简单的环回测试来测试您的框架。一旦框架通过检查,您就可以将注意力转移到与实际设备的通信上。

虽然 Pi 硬件不支持SPI_LOOP模式位,但您仍然可以通过从 MOSI 输出连接回 MISO 输入引脚的导线(连接 GPIO 10 至 GPIO 9)来实现 SPI 总线的物理环路。

接下来显示的一个简单程序演示了这种类型的环回测试。它会将 4 个字节(0x12、0x23、0x45 和 0x67)写入 SPI 驱动器。因为您已经将 MOSI 引脚连接到 MISO 输入端,所以任何发送的信号都会被接收到。

当程序执行时,它将报告收到的字节数和四个十六进制值:

$ sudo ./spiloop
rc=4 12 23 45 67
$

如果移除 MOSI 与 MISO 之间的导线,并将 MISO 连接到高电平(+3.3 V),则应该能够读取所有接收字节的 0xFF。如果随后将 MISO 接地,则每个字节将接收 0x00。确保使用正确的销以避免损坏(清单 16-1 )。

/********************************************
 * spiloop.c − Example loop test
 * Connect MOSI (GPIO 10) to MISO (GPIO 9)
 ********************************************/
0005: #include <stdio.h>
0006: #include <errno.h>
0007: #include <stdlib.h>
0008: #include <stdint.h>
0009: #include <fcntl.h>
0010: #include <unistd.h>
0011: #include <sys/ioctl.h>
0012: #include <linux/types.h>
0013: #include <linux/spi/spidev.h>
0014:
0015: static void
0016: errxit(const char *msg) {
0017:   perror(msg);
0018:   exit(1);
0019: }
0020:
0021: int
0022: main(int argc, char ** argv) {
0023:   static uint8_t tx[] = {0x12, 0x23, 0x45, 0x67};
0024:   static uint8_t rx[] = {0xFF, 0xFF, 0xFF, 0xFF};
0025:   struct spi_ioc_transfer ioc = {
0026:       .tx_buf = (unsigned long) tx,
0027:       .rx_buf = (unsigned long) rx,
0028:       .len = 4,
0029:       .speed_hz = 100000,
0030:       .delay_usecs = 10,
0031:       .bits_per_word = 8,
0032:       .cs_change = 1
0033:   };
0034:   uint8_t mode = SPI_MODE_0;
0035:   int rc, fd=-1;
0036:
0037:   fd = open("/dev/spidev0.0",O_RDWR);
0038:   if ( fd < 0 )
0039:       errxit("Opening SPI device.");
0040:
0041:   rc = ioctl(fd,SPI_IOC_WR_MODE,&mode);
0042:   if ( rc < 0 )

0043:       errxit("ioctl (2) setting SPI mode.");
0044:
0045:   rc = ioctl(fd,SPI_IOC_WR_BITS_PER_WORD,&ioc.bits_per_word);
0046:   if ( rc < 0 )
0047:       errxit("ioctl (2) setting SPI bits perword.");
0048:
0049:   rc = ioctl(fd,SPI_IOC_MESSAGE(1),&ioc);
0050:   if ( rc < 0 )
0051:       errxit("ioctl (2) for SPI I/O");
0052:   close(fd);
0053:
0054:   printf("rc=%d %02X %02X %02X %02X\n",
0055:       rc, rx[0], rx[1], rx[2], rx[3]);
0056:   return 0;
0057: }

Listing 16-1The spiloop.c SPI loopback program

摘要

本文介绍了 SPI 总线及其操作,以及 C 编程 API。本章以一个简单的 SPI 环路测试程序结束。运行这个不需要额外的硬件。

该循环测试很好地覆盖了所应用的 API。读取器可以更进一步,通过 SPI 总线访问实际的从机。最后一步是将从机选择添加到整体画面中,以及设备所需的任何命令/响应处理。您现在是 SPI 大师了!

十七、启动

当 Raspberry Pi 第一次通电时,或者已经复位时,启动引导序列。其实是 Pi 的 GPU 造就了 ARM 的 CPU。最初,树莓派的设计方式是,它必须从 SD 卡上的固件启动。GPU 的 RISC 代码由 Raspberry Pi 基金会在文件bootcode.bin中提供。在执行第二阶段引导加载程序之后,可以加载其他操作系统或引导加载程序,比如 U-Boot。

由于公众的兴趣,在这个过程中已经引入了一些变化,允许从 USB 直接引导。本章涵盖了各种配置下的引导过程。

引导 ARM Linux

现代引导过程由以下事件序列组成:

  1. 通电(或复位)时,ARM CPU 离线,但 GPU 通电。

  2. GPU 中的一个小 RISC 内核开始执行 OTP(一次性可编程)ROM 代码(第一阶段引导加载程序)。

  3. 默认情况下,它从以下优先级列表中确定引导:

    1. SD 卡启动

    2. USB 设备引导

    3. GPIO 引导模式

GPIO 引导模式描述如下:

www . raspberrypi . org/documentation/hardware/raspberrypi/boot modes/boot flow . MD

这里描述了几个警告,包括:

  • OTP 设置无法撤消。

  • 除非您的固件日期为 2017 年 10 月 20 日或更晚,否则不要尝试使用program_gpio_bootmode

与常规 Pi 相比,program_gpio_bootmode可能更适用于计算模块。

根据介质类型(SD 卡、USB 或 GPIO),引导过程继续进行:

  1. GPU 初始化 SD 卡/USB/GPIO 硬件。

  2. GPU 查看 SD 卡或 USB 介质中的 FAT(文件分配表)分区。搜索继续 FAT 分区,直到找到文件bootcode.bin。如果修改了 OTP 设置,序列也会改变。通常情况下:

    1. 首先检查 SD 卡,最多需要 5 秒钟才会失败(如果不存在)。

    2. 然后,当由 OTP 启用时,将尝试 USB 模式引导(这至少需要 3 秒钟来允许驱动器启动和枚举)。

  3. 名为bootcode.bin的第二阶段引导加载程序固件被加载到本地 128k 缓存中。

  4. GPU 控制传递给加载的bootcode.bin固件,并启用 SDRAM。

  5. 文件start.elf由 GPU 从同一个分区加载到 RAM 中,GPU 给予它控制权。

  6. 检查文件config.txt中需要处理的配置参数。

  7. cmdline.txt中找到的信息也由start.elf处理。

  8. GPU 允许 ARM CPU 执行程序start.elf

  9. 内核镜像由运行 start.elf 的 GPU 加载到 RAM 中。

  10. 最后,GPU 启动在 ARM CPU 上执行的内核。

引导文件

当 Raspbian Linux 启动时,包含引导文件的 FAT 分区通常挂载为/boot。表 17-1 列出了适用于引导过程的文件。可以编辑文本文件以影响新的配置。二进制文件也可以被相同的新版本替换。

表 17-1

/引导文件

|

文件名

|

目的

|

格式

|
| --- | --- | --- |
| 启动代码,bin | 第二阶段引导加载程序 | 二进制的 |
| 配置文件 | 配置参数 | 文本 |
| 厘米线.txt | 内核的命令行参数 | 文本 |
| 修复它*。 | 在 ARM CPU 和 GPU 之间划分 SDRAM | 二进制的 |
| start .精灵 | ARM CPU 代码即将推出 | 二进制的 |
| 内核. img | 要加载的内核 | 二进制的 |
|   | 名称可以用 config.txt 中的kernel=参数覆盖 |   |

配置文件

与许多包含 BIOS 系统的 PC 不同,Raspberry Pi 使用的是config.txt文件。该文件是可选的,因此当它丢失时,将应用默认值。当 Raspbian Linux 启动时,这个文件位于/boot/config.txt 中。

该文件中vcgencmd支持的内容可以显示为:

# vcgencmd get_config int
aphy_params_current=819
arm_freq=1400
audio_pwm_mode=514
config_hdmi_boost=5
core_freq=250
desired_osc_freq=0x33e140
desired_osc_freq_boost=0x3c45b0
disable_commandline_tags=2
disable_l2cache=1
display_hdmi_rotate=-1
display_lcd_rotate=-1
dphy_params_current=547
enable_uart=1
force_eeprom_read=1

force_pwm_open=1
framebuffer_ignore_alpha=1
framebuffer_swap=1
gpu_freq=300
hdmi_force_cec_address=65535
init_uart_clock=0x2dc6c00
lcd_framerate=60
over_voltage_avs=31250
over_voltage_avs_boost=0x200b2
pause_burst_frames=1
program_serial_random=1
sdram_freq=450

对于字符串选项,使用:

# vcgencmd get_config str
device_tree=-

此外,还有其他选项,包括设备树设置:

dtparam=spi=on

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

因为对原有和新选项的支持不断变化,所以随着每个新型号的发布,您可以查看以下资源:

https://elinux.org/RPiconfig

复合长宽比

sdtv_aspect参数配置复合视频宽高比。

|

sdtv_aspect

|

描述

|
| --- | --- |
| one | 4:3(默认) |
| Two | 14:9 |
| three | 16:9 |

彩色脉冲

默认情况下,启用颜色突发。这允许从复合视频插孔中产生颜色。在某些情况下,为了获得更清晰的显示,可能需要将视频设置为单色。

|

sdtv_disable_colourburst

|

描述

|
| --- | --- |
| Zero | 颜色突发使能(默认) |
| one | 彩色突发禁用(单色) |

高清视频

本节介绍影响 HDMI 操作的config.txt设置。

HDMI 安全模式

hdmi_safe参数支持自动 HDMI 配置,以实现最佳兼容性。

|

hdmi _ 安全

|

描述

|
| --- | --- |
| Zero | 禁用(默认) |
| one | 使能够 |

hdmi_safe=1(启用)时,隐含以下设置:

  • hdmi_force_hotplug=1

  • config_hdmi_boost=4

  • hdmi_group=1

  • hdmi_mode=1

  • disable_overscan=0

HDMI 强制热插拔

此配置设置允许您强制 HDMI 显示器的热插拔信号,无论显示器是否连接。默认情况下,NOOBS 发行版启用此设置。

|

hdmi_force_hotplug

|

描述

|
| --- | --- |
| Zero | 禁用(非 NOOBS 默认) |
| one | 即使没有检测到 HDMI 显示器,也使用 HDMI 模式(NOOBS 默认) |

HDMI 忽略热插拔

启用hdmi_ignore_hotplug设置会使系统看起来没有连接 HDMI 显示器,即使有。这有助于在插入 HDMI 显示器时强制合成视频输出。

|

hdmi_ignore_hotplug

|

描述

|
| --- | --- |
| Zero | 禁用(默认) |
| one | 即使检测到 HDMI 显示器,也使用复合视频 |

HDMI 驱动器

此模式允许您在 DVI(无声音)和 HDMI 模式(有声音,如果支持)之间进行选择。

|

hdmi_drive

|

描述

|
| --- | --- |
| one | 正常 DVI 模式(无声音) |
| Two | 正常 HDMI 模式(如果支持并启用,将发送声音) |

HDMI 忽略 EDID

启用此选项会导致来自显示屏的 EDID 信息被忽略。通常,这些信息是有用的,并且被使用。

|

HDMI _ ignore _edid(HDMI _ 忽略 _ edid)

|

描述

|
| --- | --- |
| 未指明的 | 阅读 EDID 信息 |
| 0xa5000080 | 忽略 EDID 信息 |

HDMI EDID 文件

hdmi_edid_file被启用时,EDID 信息从名为edid.txt的文件中获取。否则,当可用时,从显示器中取出。

|

hdmi _ edid _ 档案

|

描述

|
| --- | --- |
| Zero | 从设备读取 EDID 数据(默认) |
| one | 从 edid.txt 文件中读取 EDID 数据 |

HDMI Force EDID 音讯

启用此选项会强制支持所有音频格式,即使显示器不支持它们。当报告为不受支持时,这允许 DTS/AC3 通过。

|

hdmi _ force _ edid _ 音讯

|

描述

|
| --- | --- |
| Zero | 使用 EDID 提供的值(默认) |
| one | 假设支持所有音频格式 |

避免 EDID 模糊匹配

避免 EDID 中描述的模式的模糊匹配。

|

避免 _ edid _ 模糊 _ 匹配

|

描述

|
| --- | --- |
| Zero | 使用模糊匹配(默认) |
| one | 避免模糊匹配 |

HDMI 组

hdmi_group选项定义了 HDMI 类型。

|

hdmi_group

|

描述

|
| --- | --- |
| Zero | 使用 EDID 报告的首选组(默认) |
| one | 成本效益分析(Cost Effectiveness Analysis) |
| Two | 深水活动目标(Deep Mobile Target) |

HDMI 模式

该选项定义了 CEA 或 DMT 格式中使用的屏幕分辨率(参见前面“HDMI 组”小节中的参数hdmi_group)。在表 17-2 中,所示的修饰语具有以下含义:

表 17-2

HDMI 模式设置

|

|

成本效益分析(Cost Effectiveness Analysis)

|

深水活动目标(Deep Mobile Target)

|
| --- | --- | --- |
|

方式

|

解决

|

恢复精神

|

修饰语

|

解决

|

恢复精神

|

笔记

|
| --- | --- | --- | --- | --- | --- | --- |
| one | 视频图形阵列 |   |   | 640×350 | 85 赫兹 |   |
| Two | 480 便士 | 60 赫兹 |   | 640×400 | 85 赫兹 |   |
| three | 480 便士 | 60 赫兹 | H | 720×400 | 85 赫兹 |   |
| four | 720 便士 | 60 赫兹 |   | 640×480 | 60 赫兹 |   |
| five | 1080 i | 60 赫兹 |   | 640×480 | 72 赫兹 |   |
| six | 480 i | 60 赫兹 |   | 640×480 | 75 赫兹 |   |
| seven | 480 i | 60 赫兹 | H | 640×480 | 85 赫兹 |   |
| eight | 240 便士 | 60 赫兹 |   | 800×600 | 56 赫兹 |   |
| nine | 240 便士 | 60 赫兹 | H | 800×600 | 60 赫兹 |   |
| Ten | 480 i | 60 赫兹 | 4x | 800×600 | 72 赫兹 |   |
| Eleven | 480 i | 60 赫兹 | 4x H | 800×600 | 75 赫兹 |   |
| Twelve | 240 便士 | 60 赫兹 | 4x | 800×600 | 85 赫兹 |   |
| Thirteen | 240 便士 | 60 赫兹 | 4x H | 800×600 | 120 赫兹 |   |
| Fourteen | 480 便士 | 60 赫兹 | 2x | 848×480 | 60 赫兹 |   |
| Fifteen | 480 便士 | 60 赫兹 | 2x 高 | 1024×768 | 43 赫兹 | 不要使用 |
| Sixteen | 1080 便士 | 60 赫兹 |   | 1024×768 | 60 赫兹 |   |
| Seventeen | 576 便士 | 50 赫兹 |   | 1024×768 | 70 赫兹 |   |
| Eighteen | 576 便士 | 50 赫兹 | H | 1024×768 | 75 赫兹 |   |
| Nineteen | 720 便士 | 50 赫兹 |   | 1024×768 | 85 赫兹 |   |
| Twenty | 1080 i | 50 赫兹 |   | 1024×768 | 120 赫兹 |   |
| Twenty-one | 576 i | 50 赫兹 |   | 1152×864 | 75 赫兹 |   |
| Twenty-two | 576 i | 50 赫兹 | H | 1280×768 |   | 稀有 |
| Twenty-three | 288 便士 | 50 赫兹 |   | 1280×768 | 60 赫兹 |   |
| Twenty-four | 288 便士 | 50 赫兹 | H | 1280×768 | 75 赫兹 |   |
| Twenty-five | 576 i | 50 赫兹 | 4x | 1280×768 | 85 赫兹 |   |
| Twenty-six | 576 i | 50 赫兹 | 4x H | 1280×768 | 120 赫兹 | 稀有 |
| Twenty-seven | 288 便士 | 50 赫兹 | 4x | 1280×800 |   | 稀有 |
| Twenty-eight | 288 便士 | 50 赫兹 | 4x H | 1280×800 | 60 赫兹 |   |
| Twenty-nine | 576 便士 | 50 赫兹 | 2x | 1280×800 | 75 赫兹 |   |
| Thirty | 576 便士 | 50 赫兹 | 2x 高 | 1280×800 | 85 赫兹 |   |
| Thirty-one | 1080 便士 | 50 赫兹 |   | 1280×800 | 120 赫兹 | 稀有 |
| Thirty-two | 1080 便士 | 24 赫兹 |   | 1280×960 | 60 赫兹 |   |
| Thirty-three | 1080 便士 | 25 赫兹 |   | 1280×960 | 85 赫兹 |   |
| Thirty-four | 1080 便士 | 30 赫兹 |   | 1280×960 | 120 赫兹 | 稀有 |
| Thirty-five | 480 便士 | 60 赫兹 | 4x | 1280×1024 | 60 赫兹 |   |
| Thirty-six | 480 便士 | 60 赫兹 | 4x H | 1280×1024 | 75 赫兹 |   |
| Thirty-seven | 576 便士 | 50 赫兹 | 4x | 1280×1024 | 85 赫兹 |   |
| Thirty-eight | 576 便士 | 50 赫兹 | 4x H | 1280×1024 | 120 赫兹 | 稀有 |
| Thirty-nine | 1080 i | 50 赫兹 | 稀有 | 1360×768 | 60 赫兹 |   |
| Forty | 1080 i | 100 赫兹 |   | 1360×768 | 120 赫兹 | 稀有 |
| Forty-one | 720 便士 | 100 赫兹 |   | 1400×1050 |   | 稀有 |
| forty-two | 576 便士 | 100 赫兹 |   | 1400×1050 | 60 赫兹 |   |
| Forty-three | 576 便士 | 100 赫兹 | H | 1400×1050 | 75 赫兹 |   |
| forty-four | 576 i | 100 赫兹 |   | 1400×1050 | 85 赫兹 |   |
| Forty-five | 576 i | 100 赫兹 | H | 1400×1050 | 120 赫兹 | 稀有 |
| Forty-six | 1080 i | 120 赫兹 |   | 1440×900 |   | 稀有 |
| Forty-seven | 720 便士 | 120 赫兹 |   | 1440×900 | 60 赫兹 |   |
| Forty-eight | 480 便士 | 120 赫兹 |   | 1440×900 | 75 赫兹 |   |
| forty-nine | 480 便士 | 120 赫兹 | H | 1440×900 | 85 赫兹 |   |
| Fifty | 480 i | 120 赫兹 |   | 1440×900 | 120 赫兹 | 稀有 |
| Fifty-one | 480 i | 120 赫兹 | H | 1600×1200 | 60 赫兹 |   |
| fifty-two | 576 便士 | 200 赫兹 |   | 1600×1200 | 65 赫兹 |   |
| Fifty-three | 576 便士 | 200 赫兹 | H | 1600×1200 | 70 赫兹 |   |
| Fifty-four | 576 i | 200 赫兹 |   | 1600×1200 | 75 赫兹 |   |
| Fifty-five | 576 i | 200 赫兹 | H | 1600×1200 | 85 赫兹 |   |
| fifty-six | 480 便士 | 240 赫兹 |   | 1600×1200 | 120 赫兹 | 稀有 |
| Fifty-seven | 480 便士 | 240 赫兹 | H | 1680×1050 |   | 稀有 |
| Fifty-eight | 480 i | 240 赫兹 |   | 1680×1050 | 60 赫兹 |   |
| Fifty-nine | 480 i | 240 赫兹 | H | 1680×1050 | 75 赫兹 |   |
| Sixty |   |   |   | 1680×1050 | 85 赫兹 |   |
| Sixty-one |   |   |   | 1680×1050 | 120 赫兹 | 稀有 |
| Sixty-two |   |   |   | 1792×1344 | 60 赫兹 |   |
| Sixty-three |   |   |   | 1792×1344 | 75 赫兹 |   |
| Sixty-four |   |   |   | 1792×1344 | 120 赫兹 | 稀有 |
| Sixty-five |   | |   | 1856×1392 | 60 赫兹 |   |
| Sixty-six |   |   |   | 1856×1392 | 75 赫兹 |   |
| Sixty-seven |   |   |   | 1856×1392 | 120 赫兹 | 稀有 |
| sixty-eight |   |   |   | 1920×1200 |   | 稀有 |
| sixty-nine |   |   |   | 1920×1200 | 60 赫兹 |   |
| Seventy |   |   |   | 1920×1200 | 75 赫兹 |   |
| Seventy-one |   |   |   | 1920×1200 | 85 赫兹 |   |
| seventy-two |   |   |   | 1920×1200 | 120 赫兹 | 稀有 |
| Seventy-three |   |   |   | 1920×1440 | 60 赫兹 |   |
| Seventy-four |   |   |   | 1920×1440 | 75 赫兹 |   |
| Seventy-five |   |   |   | 1920×1440 | 120 赫兹 | 稀有 |
| Seventy-six | |   |   | 2560×1600 |   | 稀有 |
| Seventy-seven |   |   |   | 2560×1600 | 60 赫兹 |   |
| seventy-eight |   |   |   | 2560×1600 | 75 赫兹 |   |
| Seventy-nine |   |   |   | 2560×1600 | 85 赫兹 |   |
| Eighty |   |   |   | 2560×1600 | 120 赫兹 | 稀有 |
| Eighty-one |   |   |   | 1366×768 | 60 赫兹 |   |
| Eighty-two | 1080 便士 | 60 赫兹 |   |   |   |   |
| Eighty-three | |   |   | 1600×900 |   | 稀有 |
| Eighty-four |   |   |   | 2048×1152 |   | 稀有 |
| eighty-five | 720 便士 | 60 赫兹 |   |   |   |   |
| Eighty-six |   |   |   | 1366×768 |   | 稀有 |

  • H表示正常 4:3 模式的 16:9 变体。

  • 2x表示像素加倍(时钟频率更高)。

  • 4x表示像素翻两番(时钟速率更高)。

  • R表示减少消隐(数据流中用于消隐的字节减少,从而降低时钟速率)。

HDMI 升压

config_hdmi_boost参数允许您调整 HDMI 信号强度。

|

配置 _hdmi_boost

|

描述

|
| --- | --- |
| Zero | 非 non 预设 |
| one |   |
| Two |   |
| three |   |
| four | 如果有干扰问题,请使用(NOOBS 默认设置) |
| five |   |
| six |   |
| seven | 最大强度 |

HDMI 忽略 CEC 初始化

启用此选项时,CEC 初始化不会发送到设备。这可以避免重启时电视脱离待机状态和频道切换。

|

hdmi_ignore_cec_init

|

描述

|
| --- | --- |
| Zero | 正常(默认) |
| one | 不发送初始活动源消息 |

HDMI 忽略此选项

使能此选项时,假设 HDMI 设备根本不支持 CEC,即使该设备支持 CEC。因此,不支持任何 CEC 功能。

|

hdmi_ignore_cec

|

描述

|
| --- | --- |
| Zero | 正常(默认) |
| one | 禁用 CEC 支持 |

过扫描视频

一些选项控制复合视频输出的过扫描支持。当启用过扫描时,会根据配置跳过屏幕两侧的一定数量的像素。

禁用过扫描

disable_overscan选项可以禁用过扫描功能。默认情况下,它处于启用状态:

|

禁用 _ 过扫描

|

描述

|
| --- | --- |
| Zero | 过扫描使能(默认) |
| one | 禁用过扫描 |

左、右、上、下过扫描

这些参数控制在屏幕左侧、右侧、顶部和底部跳过的像素数。

|

参数

|

要跳过的像素

|
| --- | --- |
| 过扫描 _ 左=0 | 在左边 |
| 过扫描 _ 右=0 | 在右边 |
| overscan_top=0 | 在顶端 |
| overscan_bottom=0 | 实际上 |

帧缓冲设置

Linux 帧缓冲支持由本节中描述的几个配置选项来配置。

帧缓冲宽度

默认情况下,将帧缓冲器的宽度定义为显示器的宽度减去过扫描像素。

|

帧缓冲区 _ 宽度

|

描述

|
| --- | --- |
| 系统默认值 | 显示宽度过扫描 |
| 帧缓冲区宽度=n | 将宽度设置为 n 像素 |

帧缓冲高度

默认情况下,将帧缓冲区的高度定义为显示器的高度减去过扫描像素。

|

帧缓冲区 _ 高度

|

描述

|
| --- | --- |
| 系统默认值 | 显示高度过扫描 |
| 帧缓冲区高度=n | 将高度设置为 n 像素 |

帧缓冲深度

该参数定义了每个像素的位数。

|

帧缓冲区 _ 深度

|

描述

|
| --- | --- |
| eight | 有效,但默认的 RGB 调色板使屏幕不可读 |
| Sixteen | 默认 |
| Twenty-four | 截至 2012 年 6 月 15 日,外观有所改善,但存在腐败问题 |
| Thirty-two | 无损坏,但要求 framebuffer_ignore_alpha=1,并且自 2012 年 6 月 15 日起显示错误的颜色 |

帧缓冲区忽略 Alpha

使用此选项可以禁用 alpha 通道。截至本文撰写之时,当使用 32 位帧缓冲深度时,必须使用此选项。

|

帧缓冲区 _ 忽略 _ 阿尔法

|

描述

|
| --- | --- |
| Zero | Alpha 通道使能(默认) |
| one | 阿尔法通道禁用 |

常规视频选项

根据display_rotate选项,显示屏可以不同方式翻转或旋转。通过将翻转值与旋转值相加,您应该能够进行翻转和旋转。

90°和 270°旋转需要 GPU 上的额外内存,因此这些选项不适用于 16 MB GPU 分割。

|

显示 _ 旋转

|

描述

|
| --- | --- |
| Zero | 0(默认) |
| one | 90° |
| Two | 180° |
| three | 270° |
| 0x1000 | 水平翻转 |
| 0x2000 | 垂直翻转 |

虽然记录了翻转选项,但我无法让它们工作。然而,轮换被证实是可行的。

许可的编解码器

以下选项允许您为受影响的编解码器配置购买的许可证密钥代码。

|

[计]选项

|

笔记

|
| --- | --- |
| decode_MPG2=0x12345678 | 硬件 MPEG-2 解码的许可证密钥 |
| decode_WVC1=0x12345678 | 硬件 VC-1 解码的许可证密钥 |

测试

以下测试选项在启动期间启用图像/声音测试。这是用于制造商测试的。

|

测试模式

|

描述

|
| --- | --- |
| Zero | 禁用测试模式(默认) |
| one | 启用测试模式 |

记忆

本节总结了与内存相关的配置设置。

禁用 GPU L2 缓存

disable_l2cache选项允许禁用 ARM CPU 对 GPU L2 缓存的访问。这需要在内核中禁用相应的 L2。

|

禁用 _ 缓存

|

描述

|
| --- | --- |
| Zero | 启用 GPU L2 缓存访问 |
| one | 禁用 GPU L2 缓存访问 |

引导选项

本节中的几个选项会影响引导过程。许多选项与正在启动的内核有关,而其他选项则影响文件系统和设备。

命令行

cmdline选项允许您在config.txt文件中配置内核命令行参数,而不是在cmdline.txt文件中。

|

厘米线

|

描述

|
| --- | --- |
| 未指明的 | 命令行取自cmdline.txt |
| cmdline="command " | 命令行取自参数 |

核心

默认情况下,start.elf 根据 Raspberry Pi 模型从名为kernel.imgkernel7.img的文件中加载内核。这可以被覆盖以指定特定的图像。

内核地址

这个参数决定了内核映像加载到的内存地址。

|

内核地址

|

描述

|
| --- | --- |
| 0x00000000 | 默认 |

RAM 文件系统文件

ramfsfile参数为 RAM FS 文件命名文件,用于内核。

|

ramfsfile

|

描述

|
| --- | --- |
| 未指明的 | 没有使用 RAM FS 文件 |
| ramfsfile="ramfs.file " | 使用文件 ramfs.file |

RAM 文件系统地址

ramfsaddr参数指定 RAM 文件系统映像加载到内存的位置。

|

拉姆夫萨德

|

描述

|
| --- | --- |
| 0x00000000 | 默认地址 |

初始化 RAM 文件系统

该选项是一个方便的选项,它结合了选项ramfsfileramfsaddr

|

林特克

|

参数 1

|

参数 2

|

描述

|
| --- | --- | --- | --- |
| 林特克 | initram.gz 文件 | 0x00800000 | 例子 |

设备树地址

device_tree_address选项定义了设备树地址的加载位置。

|

设备树地址

|

描述

|
| --- | --- |
| 0x00000000 | 默认 |

初始化 UART 波特率

init_uart_baud选项允许用户重新配置串行控制台,使用不同于默认的波特率。

|

初始化 uart 波特

|

描述

|
| --- | --- |
| One hundred and fifteen thousand two hundred | 默认波特率 |

初始化 UART 时钟

init_uart_clock参数允许用户重新配置 UART 以使用不同的时钟速率。

|

初始化 uart 时钟

|

描述

|
| --- | --- |
| Three million | 默认 |

初始化 EMMC 时钟

init_emmc_clock参数允许用户调整 EMMC 时钟,这可以提高 SD 卡的性能。

|

初始化 _ emmc _ 时钟

|

描述

|
| --- | --- |
| One hundred million | 默认 |

启动延迟

boot_delayboot_delay_ms选项允许用户在加载内核之前重新配置start.elf使用的延迟。所用的实际延迟时间由下式计算得出:

$$ D=1000\times b+m $$

在哪里

|

boot_delay (b)

|

描述

|
| --- | --- |
| one | 默认 |

  • D 是以毫秒为单位计算的延迟。

  • bboot_delay值。

  • mboot_delay_ms值。

boot_delay_ms增加了boot_delay参数。

|

启动延迟毫秒(毫秒)

|

描述

|
| --- | --- |
| Zero | 默认 |

避免安全模式

可以在管脚 P1-05 (GPIO 1)和 P1-06(地)之间放置一个跳线或开关,使start.elf启动安全模式引导。如果 GPIO 1 正用于其他 I/O 功能,则应禁用安全模式检查。

|

避免 _ 安全 _ 模式

|

描述

|
| --- | --- |
| Zero | 默认(检查 P1-05 的安全模式) |
| one | 禁用安全模式检查 |

厘米线.txt

cmdline.txt文件用于向内核提供命令行参数。标准图像中提供的 Raspbian 值在这里被分成多行,以便于阅读(这个示例是针对 Raspberry Pi 3 B+):

# cat cmdline.txt
dwc_otg.lpm_enable=0 \
console=tty1 \
console=serial0,115200 \
root=PARTUUID=2383b4bd-02 \
rootfstype=ext4 \
elevator=deadline \
fsck.repair=yes \
rootwait \
quiet \
splash \
plymouth.ignore-serial-consoles

提供该文件是为了方便起见,因为可以使用cmdline="text"选项在config.txt文件中配置参数。当提供了config.txt选项时,它会取代cmdline.txt文件。

一旦 Raspbian Linux 内核启动,您可以查看如下所用的命令行选项(这是同一个 Raspberry Pi 3 B+):

$ cat /proc/cmdline
8250.nr_uarts=1 \
bcm2708_fb.fbwidth=1680 \
bcm2708_fb.fbheight=1050 \
bcm2708_fb.fbswap=1 \
vc_mem.mem_base=0x3ec00000 \
vc_mem.mem_size=0x40000000 \
dwc_otg.lpm_enable=0 \
console=tty1 \
console=ttyS0,115200 \
root=PARTUUID=2383b4bd-02 \
rootfstype=ext4 \
elevator=deadline \
fsck.repair=yes \
rootwait \
quiet \
splash \
plymouth.ignore-serial-consoles

这个输出很有趣,因为它显示了cmdline.txt文件中没有的细节。格式name.option=values的选项是特定于内核可加载模块的。

串行控制台=

Linux console 参数指定 Linux 使用什么设备作为控制台。在本例中,它是:

console=ttyS0,115200

这指的是启动后作为/dev/ttyS0可用的串行设备。设备名称后面的参数是波特率。

串行控制台选项的一般形式如下:

console=ttyDevice,bbbbpnf

第二个参数是选项字段:

|

地区

|

描述

|

价值

|

拉斯比安笔记

|
| --- | --- | --- | --- |
| 双侧束支传导阻滞 | 波特率 | One hundred and fifteen thousand two hundred | 可以超过四位数 |
| p | 平价 | n | 无奇偶校验 |
|   |   | o | 奇宇称 |
|   |   | e | 偶数奇偶性 |
| n | 位数 | seven | 7 个数据位 |
|   |   | eight | 8 个数据位 |
| f | 流控制 | r | 即时战略游戏 |
|   |   | 省略 | RTS 时 |

虚拟控制台=

Linux 支持虚拟控制台,也可以通过console=参数进行配置。Raspbian Linux 指定了以下内容:

console=tty1

内核启动后,该设备从/dev/tty1开始可用。可列出用于该虚拟控制台的tty参数(为便于阅读,在此编辑):

$ sudo −i
# stty −a </dev/tty1
speed 38400 baud ; rows 26; columns 82; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; \
eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>;
start = ^Q; stop = ^S ; susp = ^Z; rprnt = ^R; werase = ^W; \
lnext = ^V; flush = ^O; min = 1; time = 0;
−parenb −parodd cs8 hupcl −cstopb cread −clocal −crtscts
−ignbrk brkint −ignpar −parmrk −inpck −istrip −inlcr \
−igncr icrnl ixon −ixoff −iuclc −ixany imaxbel iutf8
opost −o lcuc −ocrnl onlcr −onocr −onlret −ofill −ofdel \
nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok −echonl −noflsh \
−xcase −tostop −echoprt −echoctl echoke
#

根=

Linux 内核需要知道哪个设备持有根文件系统。标准 Raspbian 图像提供以下内容:

root=PARTUUID=2383b4bd-02

该信息可以通过blkid命令获得:

# blkid
/dev/mmcblk0p1: LABEL="boot" UUID="A75B-DC79" TYPE="vfat" PARTUUID="2383b4bd-01"
/dev/mmcblk0p2: LABEL="rootfs" UUID="485ec5bf-9c78-45a6-9314-32be1d0dea38" \
                TYPE="ext4" PARTUUID="2383b4bd-02"
/dev/mmcblk0: PTUUID="2383b4bd" PTTYPE="dos"

查看该输出可以确认名为“rootfs”的分区正被用作根文件系统。本例中的分区由分区 UUID(UUID 的缩写,是一个通用的唯一标识符)指定。

root=参数的一般形式支持以下形式:

  • root=MMmm:从主要设备MM,次要设备mm(十六进制)引导。

  • root=/dev/nfs:引导一个由nfsroot指定的 NFS 磁盘(参见nfs-root=ip=)。

  • root=/dev/name:从名为/dev/name的设备启动。

  • root=PARTUUID=:从由标识为缩短 UUID 的本地唯一分区引导。

rootfstype=

除了指定保存根文件系统的设备之外,Linux 内核有时需要知道文件系统的类型。这是通过rootfstype参数配置的。标准 Raspbian 图像提供以下内容:

rootfstype=ext4

这个例子表明根文件系统是ext4类型。

Linux 内核可以检查 root 参数中给定的设备,以确定文件系统类型。但是也有内核无法解析类型或者混淆的情况。否则,您可能需要强制某种文件系统类型。

电梯=

该选项选择要在内核中使用的 I/O 调度程序方案。标准 Raspbian 图像指定了以下内容:

elevator=deadline

要找出正在使用的 I/O 调度程序选项和其他可用选项(在您的内核中),我们可以参考/sys伪文件系统:

$ cat /sys/block/mmcblk0/queue/scheduler
noop [deadline] cfq
$

名称mmcblk0是根文件系统所在设备的名称(查看前面的示例blkid输出)。方括号中的输出显示正在使用deadline I/O 调度程序。其他的选择是noopcfq。这些 I/O 调度程序如下:

|

名字

|

描述

|

笔记

|
| --- | --- | --- |
| 无操作 | 没有特殊的请求顺序 |   |
| 完全公平队列 | 完全公平调度程序 | 年长的 |
| 最后期限 | 循环调度程序,但请求有截止日期 | 最新的 |

I/O 调度程序是最新的实现,旨在提高效率和公平性。deadline调度器使用循环电梯,除了它额外记录请求的截止日期。循环电梯是根据扇区号和磁头移动(向前和向后)对请求进行排序的电梯。deadline调度器将使用循环电梯行为,但是如果请求看起来即将过期,它将被给予立即的优先权。

rootwait=

当用于根文件系统的设备是与其他内核引导功能异步启动的设备时,使用此选项。这通常是 USB 和 MMC 设备所需要的,这可能需要额外的时间来初始化。rootwait选项强制内核等待,直到根设备准备就绪。

假设根文件系统在 SD 卡(一个 MMC 设备)上,Raspbian 映像使用以下内容:

rootwait

nfsroot=

nfsroot选项允许您定义一个从 NFS 挂载启动的内核(假设内核中编译了 NFS 支持)。方括号显示可选值的位置:

|

|

描述

|
| --- | --- |
| 服务器 ip | NFS 服务器 IP 号(默认使用 ip=) |
| 根目录 | NFS 服务器上的根目录。如果存在%s,IP 地址将被插入其中。 |
| nfs 选项 | NFS 选项像 ro 一样,用逗号分隔 |

nfsroot=[server−ip:]root−dir[,nfs−options]

未指定时,将使用默认值/tftpboot/client_ip_address。这需要指定root=/dev/nfs,并且可以选择添加ip=

为了测试您的内核是否支持 NFS,您可以在系统启动时查询/proc文件系统:

# cat /proc/filesystems
nodev  sysfs
nodev  rootfs
nodev  ramfs
nodev  bdev
nodev  proc
nodev  cpuset
nodev  cgroup
nodev  cgroup2
nodev  tmpfs
nodev  devtmpfs
nodev  configfs
nodev  debugfs
nodev  tracefs
nodev  sockfs
nodev  pipefs
nodev  rpc_pipefs
nodev  devpts
       ext3
       ext2
       ext4
       vfat
       msdos
nodev  nfs
nodev  nfs4
nodev  autofs
       f2fs
nodev  mqueue
       fuseblk
nodev  fuse
nodev  fusectl

从这个例子中,我们看到旧的 NFS ( nfs)和新的 NFS4 文件系统都受支持。

ip=

此选项允许用户配置网络设备的 IP 地址,或者指定如何分配 IP 号码。参见root=nfsroot=选项。

ip=client−ip:server−ip:gw−ip:netmask:hostname:device:autoconf

表 17-3 描述了该选项中的字段。如果需要,autoconf 可以单独出现,中间没有冒号。当给出ip=o ff 或ip=none时,不进行自动配置。自动配置协议在表 17-4 中列出。

表 17-4

自动配置协议

|

草案

|

描述

|
| --- | --- |
| 关闭或无 | 不要自动配置 |
| on 或任何 | 使用任何可用的协议(默认) |
| 动态主机配置协议 | 使用 DHCP |
| 引导程序协议 | 使用 BOOTP |
| 怪怪的 | 使用 RARP |
| 两者 | 使用 BOOTP 或 RARP,但不要使用 DHCP |

表 17-3

ip=内核参数

|

|

描述

|

默认

|
| --- | --- | --- |
| ip 客户端 | 客户端的 IP 地址 | 自动配置 |
| ip 服务器 | NFS 服务器的 IP 地址,仅 NFS 根目录需要 | 自动配置 |
| gw-ip | 服务器的 IP 地址(如果在单独的子网上) | 自动配置 |
| 网络掩码 | 本地 IP 地址的网络掩码 | 自动配置 |
| 主机名 | 要提供给 DHCP 的主机名 | 客户端 IP 地址 |
| 设备 | 要使用的接口的名称 | 当有多个可用时,autoconf |
| 自动配置 | 自动配置方法 | 任何的 |

随着内核的交换和配置恢复到安全选项,现在应该可以启动紧急内核了。登陆救援。

要将系统恢复到正常状态,您需要遵循以下步骤:

  1. kernel.img改名为kernel_emergency.img(为了以后的救援)。

  2. kernel.bak重命名为kernel.img(恢复您的正常内核)。

  3. 如有必要,恢复/更改您的config.txt配置。

  4. 如有必要,恢复/更改您的cmdline.txt配置。

此时,您可以使用原来的内核和配置重新启动。

十八、vcgencmd

除了显示状态的常见 Linux 命令之外,Raspberry Pi 还包括一个名为vcgencmd的定制命令,它可以报告电压和温度以及其他 Pi 特定的属性。本章记录了该命令的已知功能。该命令的可执行文件是/usr/bin/vcgencmd

vcgencmd 命令

该命令没有手册页,但是所有支持选项的列表可以通过commands选项显示。为了便于阅读,所示的示例命令输出被分成几行:

# vcgencmd commands
commands="vcos, ap_output_control, ap_output_post_processing, \
vchi_test_init, vchi_test_exit, vctest_memmap, vctest_start, \
vctest_stop, vctest_set, vctest_get, pm_set_policy, \
pm_get_status, pm_show_stats, pm_start_logging, pm_stop_logging, \
version, commands, set_vll_dir, set_backlight, set_logging, \
get_lcd_info, arbiter, cache_flush, otp_dump, test_result, \
codec_enabled, get_camera, get_mem, measure_clock, measure_volts, \
scaling_kernel, scaling_sharpness, get_hvs_asserts, get_throttled, \
measure_temp, get_config, hdmi_ntsc_freqs, hdmi_adjust_clock, \
hdmi_status_show, hvs_update_fields, pwm_speedup, force_audio, \
hdmi_stream_channels, hdmi_channel_map, display_power, \
read_ring_osc, memtest, dispmanx_list, get_rsts, schmoo, \
render_bar, disk_notify, inuse_notify, sus_suspend, sus_status, \
sus_is_enabled, sus_stop_test_thread, egl_platform_switch, \
mem_validate, mem_oom, mem_reloc_stats, hdmi_cvt, \
hdmi_timings, file"
#

表 18-1 对这些进行了分类和列出,总结了对它们的了解。

表 18-1

vcgencmd 命令行选项摘要

|

命令

|

争论

|

描述

|
| --- | --- | --- |
| ap 输出控制 |   |   |
| ap _ 输出 _ 后处理 |   |   |
| 仲裁人 |   |   |
| 缓存刷新 |   | 刷新 GPU 的 L1 缓存 |
| 编解码器 _ 已启用 | 多媒体数字信号编解码器 | 报告编解码器的状态:H264 MPG2 WVC1 之一 |
| 命令 |   | 列出支持的命令 |
| 磁盘 _ 通知 |   |   |
| 显示 _ 电源 | 0 或 1 | 关闭或打开显示器 |
| dispmanx _ list(消歧义) |   |   |
| egl _ 平台 _ 开关 |   |   |
| 文件 |   |   |
| 强制 _ 音频 |   |   |
| 获取 _ 相机 |   |   |
| 获取配置 | 参数 | 查询配置参数 |
| get_hvs_asserts |   |   |
| get_lcd_info |   | LCD/监视器宽度、高度和显示帧缓冲区的像素深度 |
| 获取 _ 记忆 | arm 还是 gpu | 获取 CPU (ARM)或 GPU 之间的内存分配 |
| get_rsts |   |   |
| 获得 _ 节流 |   |   |
| hdmi_adjust_clock |   |   |
| hdmi _ 通道 _ 映射 |   |   |
| hdmi_cvt 连接埠 |   |   |
| hdmi_ntsc_freqs |   |   |
| hdmi _ 状态 _ 显示 |   |   |
| hdmi _ 流 _ 通道 |   |   |
| hdmi _ 计时 |   |   |
| hvs _ 更新 _ 字段 |   |   |
| 使用 _ 通知 |   |   |
| 测量 _ 时钟 | 时钟名称 | 测量各种时钟的频率 |
| 测量温度 |   | 测量 SoC 的温度 |
| 测量 _ 伏特 | 设备名称 | 测量各种设备的电压 |
| mem_oom |   | 内存不足事件的统计信息 |
| 记忆 _ 重定位 _ 统计 |   | 可重定位内存统计 |
| memtest(模因测试) |   |   |
| 内存 _ 验证 |   |   |
| otp_dump |   | 转储 OTP 设置 |
| 下午 _ 获取 _ 状态 |   |   |
| pm _ 设置 _ 策略 |   |   |
| 下午 _ 显示 _ 统计 |   |   |
| pm _ 开始 _ 日志记录 |   |   |
| pm _ 停止 _ 日志记录 |   |   |
| pwm _ 加速 |   |   |
| 读 _ 环 _ 振荡 |   |   |
| 渲染栏 |   | 调试功能 |
| 缩放 _ 内核 |   |   |
| 缩放 _ 清晰度 |   |   |
| 学校 |   |   |
| 设置背光 |   | 保留供将来使用 |
| 设置日志记录 | 级别=n | 更改 VideoCore 记录器的级别 |
| S7-1200 可编程控制器 |   |   |
| sus_is_enabled |   |   |
| sus_status |   |   |
| sus _ 停止 _ 测试 _ 线程 |   |   |
| sus _ 挂起 |   |   |
| 测试结果 |   |   |
| vchi _ 测试 _ 退出 |   |   |
| vchi _ 测试 _ 初始化 |   |   |
| vcos(vcos) | 命令 | 可能的命令有日志、帮助和版本 |
| vctest_get |   |   |
| vctest_memmap |   |   |
| vctest_set |   |   |
| 虚拟测试 _ 开始 |   |   |
| vctest_stop |   |   |
| 版本 |   | 显示 VideoCore 固件的当前内部版本 |

选项测量 _ 时钟

根据measure_clock后的参数,该固件访问选项为用户提供时钟速率信息。表 18-2 中列出了 <时钟> 的有效值。

表 18-2

measure_clock选项的有效参数

|

时钟

|

描述

|
| --- | --- |
| 手臂 | 处理器 |
| 核心 | 核心 |
| (灰)点/英寸 (扫描仪的清晰度参数) | 显示像素接口 |
| 艾美奖 | 外部 MMC 设备 |
| h264 | h.264 编码器 |
| 高清晰度多媒体接口 | HDMI 时钟 |
| 临时系统程序 | 图像传感器流水线 |
| 像素 | 像素时钟 |
| 脉宽调制(pulse-width modulating 的缩写) | 脉宽灯 |
| 通用异步收发报机 | UART 时钟 |
| v3d | 视频 3D |
| 向量误差修正 |   |

vcgencmd  measure_clock  <clock>

以下 shell 脚本片段可用于列出所有可用的时钟:

for src in arm core h264 isp v3d uart pwm emmc pixel vec hdmi dpi ; do
     echo −e "$src : $(vcgencmd measure_clock $src)" ;
done

树莓 Pi 3 B+的一个例子如下所示:

arm : frequency(45)=600000000
core : frequency(1)=250000000
h264 : frequency(28)=250000000
isp : frequency(42)=250000000
v3d : frequency(43)=250000000
uart : frequency(22)=47999000
pwm : frequency(25)=0
emmc : frequency(47)=200000000
pixel : frequency(29)=146250000
vec : frequency(10)=0
hdmi : frequency(9)=163683000
dpi : frequency(4)=0

选项测量 _ 伏特

measure_volts选项允许报告各种子系统电压:

# for id in core sdram_c sdram_i sdram_p ; do \
    echo -e "$id: $(vcgencmd measure_volts $id)" ; \
done
core: volt=1.2000V
sdram_c: volt=1.2500V
sdram_i: volt=1.2500V
sdram_p: volt=1.2250V
#

表 18-3 提供了输出报告行的图例。

表 18-3

measure_volts的有效设备名称

|

设备

|

描述

|
| --- | --- |
| 核心 | 核心 |
| sdram_c | SDRAM 控制器 |
| sdram_i | SDRAM I/O |
| sdram_p | SDRAM 物理 |

选项测量 _ 温度

measure_temp选项允许用户检索 SoC 温度,单位为摄氏度。

$ vcgencmd measure_temp
temp=43.5 °C

在本例中,相对空闲的内核温度为 43.5°c。

选项编解码器 _ 已启用

codec_enabled选项报告 Raspberry Pi 支持的编解码器的运行状态。表 18-4 中列出了有效的编解码器名称。编解码器支持可以用以下命令总结:

表 18-4

vcgencmd 编解码器名称

|

名字

|

描述

|
| --- | --- |
| H264 | h.264 编解码器 |
| MPG2 | MPEG-2 编解码器 |
| WVC1 | VC1 编解码器 |

# for id in H264 MPG2 WCV1 ; do
   echo -e "$id: $(vcgencmd codec_enabled $id)";
done
H264: H264=enabled
MPG2: MPG2=disabled
WCV1: WCV1=disabled

选项版本

version选项报告 GPU 固件版本:

# vcgencmd version
Apr 16 2018 18:16:56
Copyright (c) 2012 Broadcom
version af8084725947aa2c7314172068f79dad9be1c8b4 (clean) (release)

选项 get_lcd_info

get_lcd_info命令提供 LCD/监视器的宽度和高度,以及帧缓冲器的像素深度:

# vcgencmd get_lcd_info
1680 1050 24

选项获取配置

get_config选项在需要查询 Raspberry Pi 配置的脚本中很有用,如/boot/config.txt中所示。例如,一个脚本可以查询the uart是否为is enabled:

# vcgencmd get_config enable_uart
enable_uart=1

otp_dump

otp_dump命令将列出您在 Pi 中找到的 OTP(一次性可编程)设置。本节摘自《树莓 Pi 3 B+ 》:

# vcgencmd otp_dump
08:00000000
09:00000000
10:00000000
11:00000000
12:00000000
13:00000000
14:00000000
15:00000000
16:00280000
17:3020000a
18:3020000a
19:ffffffff
20:ffffffff
21:ffffffff
22:ffffffff
23:ffffffff
24:ffffffff
25:ffffffff
26:ffffffff
27:00001f1f
28:d4b81de4
29:2b47e21b
30:00a020d3
31:00000000
32:00000000
33:00000000
34:00000000
35:00000000
36:00000000
37:00000000
38:00000000
39:00000000
40:00000000
41:00000000
42:00000000
43:00000000
44:00000000
45:00000000
46:00000000
47:00000000
48:00000000
49:00000000
50:00000000
51:00000000
52:00000000

53:00000000
54:00000000
55:00000000
56:00000000
57:00000000
58:00000000
59:00000000
60:00000000
61:00000000
62:00000000
63:00000000
64:00000000
65:00000000
66:02009eaa

十九、Linux 控制台和 Pi Zero

Raspbian Linux 控制台是由内核命令行在引导时配置的。不过,在我们研究串行控制台访问之前,让我们先了解一下设置 Pi Zero 或 Zero W 的挑战,主要挑战是使用单个 USB 端口和正确的适配器。然后,我们将研究串行设备控制台的选项。

Pi 零/零 W

启动 Pi 零点是一个问题,主要有两个原因:

  1. ssh 访问在新的 Raspbian 映像上被禁用。

  2. 只有一个 USB 端口可以连接键盘和鼠标。

为了看到你在做什么,你需要一个迷你 HDMI 转 HDMI 适配器。

ssh 在默认情况下被禁用的原因是 Pi 的映像附带了一个默认的Pi帐户密码。每个人都知道,尤其是黑客。不知情的用户可以启动一个 Pi 并让它连接到网络,从而吸引各种讨厌的业务。所以 Raspbian 映像禁用了 ssh。

你真正需要的是一个由 Raspbian 支持的 USB 集线器。我尝试了一个旧的苹果集线器,运气不好。所以我换了一个最近的。一些键盘提供一两个额外的 USB 端口。如果 Raspbian Linux 支持的话,这可能是有用的。

如果你有耐心的话,你可能根本不需要网络中心。打开 Zero 电源,仅将鼠标插入 USB 适配器电缆。然后将鼠标移至您的系统偏好。当你需要输入东西的时候,你可以拔掉鼠标,插上键盘。对于大部分初始配置,您可能不需要键盘。

需要适配器

以下是你手头应该有的适配器列表,即使你只使用它们一次来设置你的 Zero。图 19-1 举例说明。

img/326071_2_En_19_Fig1_HTML.jpg

图 19-1

配有迷你 HDMI 适配器、USB 适配器电缆和可选 USB 以太网适配器的 Pi Zero 照片

  • 电源适配器(黑色插头插入右边的零点)。

  • USB 2.0 Micro B 5 针至母 USB 2.0 A 型适配器电缆(中间为白色电缆)。

  • HDMI 迷你适配器(Pi 左侧的白色适配器)。

  • 可选的 USB 转以太网适配器(最右侧,白色)。

如果您在本章后面使用串行端口控制台,您可以不用 USB 和 mini HDMI 适配器。

启用 ssh 和 VNC 访问

如果你有一个支持的 USB 到以太网适配器,然后连接你的 HDMI,键盘,和/或鼠标,启动 Raspbian Linux。我的 USB 以太网适配器只消耗大约 42 毫安。如果在没有集线器的情况下操作,请单独插入鼠标。一旦你有了图形化的桌面,用鼠标打开 Raspberry Pi 配置。单击接口选项卡,然后单击以下条目以启用它们:

  • 启用 SSH

  • 启用 VNC

  • 可选地启用串行端口(默认情况下启用)

  • 可选地启用串行控制台(默认情况下启用)

不要忘记点击右下角的 OK。然后仍然使用你的鼠标,点击重启来应用这些新的设置。图 19-2 显示了所涉及的控制面板。

img/326071_2_En_19_Fig2_HTML.jpg

图 19-2

Raspberry Pi 配置,接口面板

一旦您的 Zero 重新启动,您应该能够:

  1. 拔下鼠标(如果不使用集线器)。

  2. 插入 USB 转以太网适配器。

  3. 将以太网电缆插入路由器。

  4. 扫描您的网络以发现分配的 IP 地址。

如果您使用集线器,请使用键盘和鼠标打开终端窗口,并键入ifconfig命令来确定分配的地址。图 19-3 中的例子显示地址为192.168.1.15

img/326071_2_En_19_Fig3_HTML.jpg

图 19-3

使用 ifconfig 命令在终端窗口中显示您的 USB 转以太网地址

否则,从桌面扫描您的 Pi Zero。以下示例在 Mac/Linux 上扫描 192.168.1.2 到 192.168.1.254 之间的地址:

$ nmap -sP 192.168.1.2-254

Starting Nmap 7.40 ( https://nmap.org ) at 2018-07-12 20:04 EDT
Nmap scan report for 192.168.1.3
Host is up (0.077s latency).
...
Nmap scan report for 192.168.1.15
Host is up (0.0017s latency).
...
Nmap done: 253 IP addresses (6 hosts up) scanned in 2.86 seconds
$ ssh pi@192.168.1.15
pi@192.168.1.15's password:
Linux pizero 4.14.52+ #1123 Wed Jun 27 17:05:32 BST 2018 armv6l
...

在这个例子中,我知道 Pi 将被分配一个 192.168.1。*地址,因为以太网电缆插入了路由器。我通过实验发现圆周率零点的地址是 192.168.1.15。

串行控制台

如果鼠标和 HDMI 方法不合适,可能是因为您缺少适配器,您可以尝试串行控制台方法。Raspbian Linux 映像默认启用串行控制台。它使用 115200 波特的波特率,没有硬件流量控制。

然而,使用工作在 3.3 V 电平的串行适配器是至关重要的。不要使用 5 V 串行适配器,因为它们会损坏 Pi。通过跳线配置,一些适配器可以在任一级别上运行。

连接适配器,以便 Pi +3.3 V (P1-01)为串行适配器供电(一些 USB 适配器自己供电)。这些联系总结如下:

  • Pi +3.3 V (P1-01)电源至适配器+3.3 V(可标为 V CC )。

  • Pi 接地(P1-06)到适配器接地(通常标记为 Gnd)。

  • Pi TX (P1-08)至适配器 TX。

  • Pi RX (P1-10)到适配器 RX。

警告

不要连接 5 伏 TTL 串行适配器。这可能会造成损坏。一些适配器有一个跳线来选择 3.3 或 5 伏操作。

如果你发现这样不行,试着把 TX 接到 RX,RX 接到 TX。一些适配器可能是根据 DCE(数据通信设备)的观点来标记的,而另一些适配器将使用 DTE(数据终端设备)的惯例。

连接后,将您的 USB 适配器插入您的桌面,启动minicom(或其他喜爱的终端程序),并将您的串行参数设置为:

  • 155200 波特

  • 8 位,无奇偶校验,1 个停止位(8-N-1)

  • 硬件流量控制关闭

启动你的圆周率零点,让它出现。留出额外的时间,尤其是当桌面即将打开时。一旦它出现,请按 Enter 键以显示登录提示。然后,您可以像往常一样登录:

Welcome to minicom 2.7

OPTIONS:
Compiled on Sep 17 2016, 05:53:15.
Port /dev/cu.usbserial-A100MX3L, 20:52:57

Press Meta-Z for help on special keys

Raspbian GNU/Linux 9 pizero ttyAMA0
pizero login: pi
Password:
Last login

: ...

摘要

当您缺少所有其他设备:键盘、鼠标和屏幕时,串行端口控制台非常有用。这通常是为您的特殊项目初始化 Pi Zero 或 Pi Zero W 所需的全部内容。

患者用户只需一根 USB 适配器电缆和一个迷你 HDMI 适配器进行初始设置即可。键盘和鼠标可以根据需要更换。一旦 ssh 或 VNC 启用,您就可以在您最喜欢的桌面上舒适地操作了。

二十、交叉编译

嵌入式计算机经常缺乏开发和编译软件的必要资源。Raspberry Pi 在这方面比较特别,因为它已经包含了gcc编译器和所需的链接工具(在 Raspbian Linux 下)。但是,虽然可以在 Raspberry Pi 上开发和构建代码,但它可能并不总是最适合软件开发的地方。一个原因是 SD 卡的性能较低,而 Pi Zero 或 Zero W 在这方面可能表现不佳。

要为 Raspberry Pi 编译本机代码,您需要一个知道如何生成 ARM 二进制可执行文件的编译器和链接器。然而,它必须运行在不同架构的主机上(例如,Mac OS X)。因此它被称为交叉编译器。交叉编译器将获取本地(编译)平台上的源代码,并生成 ARM 二进制可执行文件,安装在目标 Pi 上。

在这一章中,你将逐步了解如何构建你自己的交叉编译器。这将允许您使用现有的 Linux 平台完成工作。

术语

让我们先来看看本章中用到的一些术语:

  • build :也叫本地平台,这是你执行编译的平台(比如 Mac OS X)。

  • target :目的平台,本章为 Raspberry Pi (ARM)。

在你冒险之前,让我们先考虑一些交叉编译的问题。交叉编译有两个主要的问题领域:

  • Raspberry Pi (ARM)的所有 C/C++包含文件和库必须在您的构建平台上可用(例如,在构建内核时)。

  • 交叉编译器和相关工具必须生成适合目标平台的代码。

在您决定要构建一个交叉编译器环境之前,您准备好了吗

  • 提供 ARM 平台所有匹配的 C/C++头文件?

  • 提供所有需要的 ARM 库,包括您打算链接的 sqlite3 等第三方产品的库?

  • 为交叉编译器和工具提供足够的磁盘空间?

crosstool-NG 软件将缓解其中一些问题。例如,通过本章后面显示的配置步骤选择正确的 Linux 头文件。

磁盘空间通过在构建平台上保存 Raspberry Pi 根文件系统的副本来解决许多问题。简单的程序不需要这样做(例如,Hello World 程序)。但是链接到库的软件可能需要这样做。即使磁盘空间有限,您也可以在构建平台上安装 Raspbian SD 卡,从而访问 Raspberry Pi 的根文件系统。

操作系统

用于构建交叉编译器环境的过程有些复杂和脆弱。使用 crosstool-NG 软件大大简化了事情。尽管有这个优势,最好还是坚持使用经过验证的交叉编译器平台和配置。

你可能会说,“源代码是开放的,所以它应该可以在任何操作系统上工作。”(你甚至可以说,“我会自己解决问题。”)现实并非如此简单。除非你愿意花时间在互联网论坛上等待答案,否则我建议你采取一种更务实的方法——在最近稳定的 Ubuntu 或 Debian/Devuan 环境上构建你的交叉编译环境。

本章使用了最近安装的 Devuan,它是基于捐赠给实验室的旧 32 位计算机的 Debian。你可以在 Mac OS X MacBook Pro 上使用 VirtualBox 4.3.12 ( www.virtualbox.org ),如果你愿意,也可以运行英特尔 i7 处理器。推荐使用当前版本的 Debian 或 Devuan Linux。

宿主、来宾、构建和目标

在这一点上,一个简短的说明是合适的,因为这些术语可能会变得混乱,尤其是第一次。让我们列出环境术语,这些术语将在本章的剩余部分提到:

  • 主机环境

  • 客人环境

  • 构建/本地环境

  • 目标环境

这么多环境!当你使用像 VirtualBox 这样的虚拟机时,术语主机客户环境就会出现。VirtualBox 被用来在你正在使用的操作系统之上托管另一个操作系统。例如,您可能在笔记本电脑上运行 Mac OS X。在这个例子中,OS X 环境在 VirtualBox 中托管了 Linux 实例。该 Linux 操作系统因此被称为客户操作系统。

术语构建(或本地)环境指的是执行交叉编译器和工具的 Linux 环境。这些 Linux 工具为目标环境(Raspberry Pi 的 ARM CPU)产生或操作代码。

平台限制

今天,许多人都在使用 64 位平台,如配备英特尔 i7 处理器或类似处理器的 MacBook Pro。如果您想为 Raspberry Pi(32 位平台)构建一个交叉编译器,这可能会带来一个问题。32 位交叉编译器必须建立在 32 位处理器上。

另一个可能让一些人感到困惑的项目是 Raspberry Pi 3 型号 B+,它运行 64 位处理器。虽然它是一个 64 位处理器,但 Raspbian Linux 的当前版本是以 32 位模式运行的。在 64 位 Linux 可用之前,您需要 32 位工具。其他 Linux 发行版,如 SUSE SLES,将支持原生 64 位 Linux,但你可能会遇到固件 blobs 等其他挑战。此问题在论坛中提出,并添加了以下回复:

回复:我们能得到 64 位操作系统吗?

2017 年 12 月 23 日星期六下午 3:30

不,没有。他们不想分割操作系统,所以他们必须支持两种不同的操作系统,一种仅用于 PI3,另一种用于所有旧的 PI,这让许多困惑的用户抱怨 64 位操作系统无法在他们的旧 PI 上工作。

此外,转换到 64 位 CPU 几乎没有带来任何东西,比如说速度没有提高多少。

如果您使用的是 64 位平台,那么您可能希望选择 VirtualBox 解决方案或使用较旧的 Linux 32 位服务器。这为您提供了一个 32 位操作系统来托管交叉编译器。另一方面,如果您已经在运行 32 位操作系统,那么创建原生交叉编译器应该是轻而易举的事情。

注意

您需要在 32 位平台上运行交叉编译器。交叉编译器不能在 64 位平台上构建。

不带 VirtualBox(本机)

如果您已经在使用 Debian、Devuan 或 Ubuntu 等 Linux 开发环境,术语 host 相当于构建(或本地)环境。主机和客户环境同样是等价的,尽管在这个场景中说没有客户操作系统可能更正确。这个更简单的场景只留给我们两种环境:

  • 主机/客户/构建:运行交叉编译工具的本地环境

  • 目标:目标执行环境(Raspberry Pi)

使用 VirtualBox (Debian/Linux)

如果您没有合适的 Linux 环境,可以在您现有的平台上托管一个。您可以使用从以下网站下载的 VirtualBox 从 Windows、Mac OS X、Solaris 或其他 Linux 发行版托管 Linux:

www.virtualbox.org

使用 VirtualBox 时,主机环境是运行 VirtualBox 的环境(例如,Mac OS X)。客户操作系统将会像 Debian 一样有点 Linux 的味道。这样我们总共有三种环境:

  • 主机:或者本机,运行 VirtualBox(例如 Windows)

  • Guest/build:VirtualBox 内的 Debian/Ubuntu 开发环境

  • 目标:目标执行环境(你的 Raspberry Pi)

规划您的交叉开发环境

此时主要考虑的通常是磁盘空间。如果您使用的是 VirtualBox,有限的内存可能是另一个因素。如果您使用的是 Linux 或 Mac OS X,请检查您装载的磁盘的可用空间(或 Windows 工具,视情况而定):

$ df -k
Filesystem     1K-blocks      Used  Available Use% Mounted on
/dev/sda1      151903380  15768740  128395300  11% /
udev               10240         0      10240   0% /dev
tmpfs             181080       388     180692   1% /run
tmpfs               5120         4       5116   1% /run/lock
tmpfs             727920         0     727920   0% /run/shm

在前面的输出示例中,我们看到根文件系统有足够的空间。但是您的文件系统可能会有所不同。必要时可以使用符号链接将更大的磁盘区域移植到您的主目录中。

如果您正在使用 VirtualBox,请为 Linux 操作系统和交叉编译器环境创建具有足够空间的虚拟磁盘。您可能希望将您的 Linux 软件放在一个最小大小约为 10 GB 的虚拟磁盘上(允许它变得更大)。

为您的交叉编译器环境留出至少 10 GB 的空间(并允许其增长)。您还必须考虑为 Raspberry Linux 内核、其包含文件和所有其他可能需要构建的第三方库(更好的是,Raspberry Pi 根文件系统的副本)提供额外的空间。

在您的开发 Linux 构建环境中,确保您的交叉编译器和构建区域正在使用具有可用空间的磁盘区域。很容易在某个方便的地方创建一个目录,然后发现您认为要使用的空间不可用。

构建交叉编译器

此时,我将假设您已经在 VirtualBox 中设置并安装了 Linux,如果有必要的话,或者使用 32 位 Linux 的一个实例。我将使用基于 Debian 的 Devuan Linux。

下载 crosstool-NG

已发布的 crosstool-NG 下载可在以下网址找到:

http://crosstool-ng.org

从网站上找到最新下载的链接。在撰写本文时,以下内容是最新的:

$ wget http://crosstool-ng.org/download/crosstool-ng/crosstool-ng-1.23.0.tar.bz2

暂存目录

我将假设您已经用符号链接到您的具有足够可用磁盘空间的磁盘区域。本章将使用~/xrpi 作为交叉编译器的平台。

$ mkdir ~/xrpi
$ cd ~/xrpi

接下来,我将假设您已经创建了一个指向您的磁盘空间区域的符号链接,或者如果当前目录已经有了空间,则只是创建了一个子目录:

$ symlink /some/big/area/of/disk ~/xrpi/devel

但是如果~/xrpi 已经有足够的空间,那么只需执行以下操作:

$ mkdir ~/xrpi/devel

为了方便起见,我们现在转到该目录:

$ cd ~/xrpi/devel

在目录~/xrpi/devel中,创建一个名为staging ( ~/devel/staging)的子目录,并将其更改为:

$ mkdir ./staging
$ cd ./staging          # Dir is ~/xrpi/devel/staging

$ pwd
/home/myuserid/xrpi/devel/staging
$

打开包装

假设 tarball crosstool-ng-1.23.0.tar.bz2被下载到您的主目录,您将执行以下操作(如果后缀不是.bz2,则更改选项j):

$ tar xjvf ~/crosstool-ng-1.23.0.tar.bz2
. . .
$

解包完成后,在您的staging目录中应该有一个名为crosstoolng-1.23.0的子目录。

创建/选择/x 工具

如果愿意,您可以选择不同的位置,但是为了便于参考,我将假设 crosstool-NG 软件将安装到/opt/x-tools中。我们还假设您的用户 ID 是fred(替换您自己的 ID)。

$ sudo mkdir -p /opt/x−tools
$ sudo chown fred /opt/x−tools

或者,安装完成后,您可以将所有权改回root进行保护。

安装软件包依赖项

crosstool-NG 构建依赖于您的 Linux 发行版作为可选安装软件提供的几个包。现在至少要安装以下软件:

# apt-get install bison
# apt-get install flex
# apt-get install libtool
# apt-get install texinfo
# apt-get install gawk
# apt-get install gperf
# apt-get install automake
# apt-get install subversion
# apt-get install help2man

如果在配置 crosstools-ng 的过程中,您发现您需要其他软件包,那么可以安装它们,然后重新配置。

配置交叉工具-NG

安装了软件包依赖项后,您现在就可以制作 crosstool-NG 软件了(根据本章剩余部分的需要替换 crosstool-ng 版本):

$ cd ~/xrpi/devel/staging/crosstool-ng-1.23.0
$ ./configure --prefix=/opt/x-tools

如果完成后没有出现错误,您就可以构建和安装软件了。如果它报告您缺少软件包依赖项,请立即安装并重复。

补丁 inputbox.c

我对一些现代 Linux 和工具非常恼火的一点是,它们不能很好地支持 backspac e 字符(Control-H)。这是一个标准的 ASCII 字符,专门用于此目的。为什么被放逐了?我会尽量克制自己,不再对此大吼大叫。

crosstool-ng所使用的菜单程序遭遇了与 linux 内核menuconfig相同的问题:没有退格字符(Control-H)支持。这可能会让你陷入无法退格或删除输入的糟糕境地。

要解决该问题,请执行以下操作:

  1. CD ~/xrpi/devel/staging/cross tool-ng-1 . 23 . 0/kconfig/LX dialog

  2. 编辑文件inputbox.c并转到大约第 128 行,在那里您应该看到一行文字:

    case KEY_BACKSPACE:
    
    
  3. 在它下面加一行简单的文字:

    case 8:
    
    
  4. 保存文件(inputbox.c)。

如果您已经知道退格键发送的是转义序列而不是 Control-H,那么您可以安全地跳过这一更改。否则,这种事情会让你发疯。随着文件的保存,一些理智的表象将随之而来。

制作十字工具

此时,构建 crosstool-NG 应该没有问题(包括上面的修复)。执行以下make命令:

$ cd ~/devel/staging/crosstool-ng-1.23.0
$ make
  SED    'ct-ng'
  SED    'scripts/scripts.mk'
  SED    'scripts/crosstool-NG.sh'
  SED    'scripts/saveSample.sh'
  SED    'scripts/showConfig.sh'
  GEN    'config/configure.in'
  GEN    'paths.mk'
  GEN    'paths.sh'
  DEP    'nconf.gui.dep'
  DEP    'nconf.dep'
  DEP    'lxdialog/yesno.dep'
  DEP    'lxdialog/util.dep'
  DEP    'lxdialog/textbox.dep'
  DEP    'lxdialog/menubox.dep'
  DEP    'lxdialog/inputbox.dep'
  DEP    'lxdialog/checklist.dep'
  DEP    'mconf.dep'
  DEP    'conf.dep'
  BISON  'zconf.tab.c'
  GPERF  'zconf.hash.c'
  LEX    'zconf.lex.c'
  DEP    'zconf.tab.dep'
  CC     'zconf.tab.o'
  CC     'conf.o'
  LD     'conf'
  CC     'lxdialog/checklist.o'
  CC     'lxdialog/inputbox.o'
  CC     'lxdialog/menubox.o'
  CC     'lxdialog/textbox.o'
  CC     'lxdialog/util.o'
  CC     'lxdialog/yesno.o'
  CC     'mconf.o'
  LD     'mconf'
  CC     'nconf.o'
  CC     'nconf.gui.o'
  LD     'nconf'
  SED    'docs/ct-ng.1'
  GZIP   'docs/ct-ng.1.gz'
$

这需要很少的时间,似乎没有麻烦。

制作安装

crosstool-NG 包编译完成后,就可以安装到/opt/x-tools中了。来自同一目录:

$ sudo make install
  GEN    'config/configure.in'
  GEN    'paths.mk'
  GEN    'paths.sh'
  MKDIR   '/opt/x-tools/bin/'
  INST    'ct-ng'
  MKDIR   '/opt/x-tools/lib/crosstool-ng-1.23.0/'
  INSTDIR 'config/'
  INSTDIR 'contrib/'
  INSTDIR 'patches/'
  INSTDIR 'scripts/'
  INST    'steps.mk'
  INST    'paths'
  INSTDIR 'samples/'
  INST    'kconfig/'
  MKDIR   '/opt/x-tools/share/doc/crosstool-ng/crosstool-ng-1.23.0/'
  INST    'docs/manual/*.md'
  MKDIR   '/opt/x-tools/share/man/man1/'
  INST    'ct-ng.1.gz'

For auto-completion, do not forget to install 'ct-ng.comp' into
your bash completion directory (usually /etc/bash_completion.d)

如果您仍然拥有前面的目录/opt/x-tools(回想一下sudo chown fred /opt/x - tools),您就不需要在前面的步骤中使用sudo。执行完make install后,您将在目录/opt/x-tools/bin中安装 crosstool-NG 命令ct-ng

小路

要使用新安装的ct-ng命令,您需要调整您的PATH环境变量(以及每次登录时):

$ PATH="/opt/x-tools/bin:$PATH"

该网站还指出,如果您的平台已经定义了环境变量LD_LIBRARY_PATH,您可能必须取消设置该变量。如果是这样,则按如下方式取消设置,以避免任何不必要的麻烦:

$ unset LD_LIBRARY_PATH

现在,您应该能够运行ct-ng来获取版本信息(注意,在下面的命令中,version前面没有连字符)。查看版本输出可以确认您的ct-ng命令已经安装并且运行正常:

$ ct-ng version
This is crosstool-NG version crosstool-ng-1.23.0

Copyright (C) 2008  Yann E. MORIN <yann.morin.1998@free.fr>
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A \
  PARTICULAR PURPOSE

.

交叉编译器配置

命令ct-ng简化了配置和构建交叉编译器工具链的必要工作。从这里开始,我们关心的是构建交叉编译器工具本身。当这个过程完成时,您已经将交叉编译器工具填充到了目录/opt/x-tools/arm-unknown-linux-gnueabi中。

ct-ng可以构建您的交叉编译器之前,必须首先配置它:

$ cd ~/xrpi/devel/staging
$ ct-ng menuconfig

如果收到“未找到命令”的错误信息,请检查PATH变量是否设置正确。

路径和杂项选项

当 ct-ng 命令启动时,出现如图 20-1 所示的菜单配置画面。按 Enter 键打开路径和杂项选项子菜单。

img/326071_2_En_20_Fig1_HTML.jpg

图 20-1

ct-ng 菜单配置打开对话框

一旦进入路径和杂项选项菜单,如图 20-2 所示,使用光标键向下移动,尝试标记为实验的功能。突出显示该行后,按空格键将星号放在方括号内以选择该选项(再次按空格键将切换设置)。

img/326071_2_En_20_Fig2_HTML.jpg

图 20-2

ct-ng 启用“尝试标记为实验性的功能”(按空格键)

之后,在同一个菜单中,将光标向下移动到标有前缀目录的中间条目,并按回车键选择它(图 20-3 )。不幸的是,最近的默认设置阻止你在菜单选项的最右边看到“前缀目录”文本。

img/326071_2_En_20_Fig3_HTML.jpg

图 20-3

选择 CT_PREFIX 行以设置前缀

对于本章中使用的过程,请将路径修改为以下内容:

/opt/x-tools/${CT_TARGET}

图 20-4 显示了输入对话框。如果您发现无法使用退格键,请应用“修补 inputbox.c”一节中讨论的修复方法。

img/326071_2_En_20_Fig4_HTML.jpg

图 20-4

设置前缀目录

建立路径名后,在显示的“OK”按钮上按 Enter 键。这将使您返回到路径和杂项选项菜单。

然后选择底部显示的退出按钮,并再次按 Enter 键。如果您更改了任何需要保存的内容,则选择“是”(图 20-5 )。

img/326071_2_En_20_Fig5_HTML.jpg

图 20-5

“保存配置”对话框

目标选项

重新启动菜单,用光标选择目标选项,按回车键打开菜单(图 20-6 )。

img/326071_2_En_20_Fig6_HTML.jpg

图 20-6

选择目标选项

然后选择目标架构并按回车键(图 20-7 )。

img/326071_2_En_20_Fig7_HTML.jpg

图 20-7

选择目标架构

在菜单中,选择arm并按空格键(图 20-8 )。然后使用底部的选择按钮。这将使您返回目标选项菜单。

img/326071_2_En_20_Fig8_HTML.jpg

图 20-8

选择 arm 架构

在目标选项菜单中(如下所示),通过查看圆括号中的状态来验证字节序设置。它应该读小端(图 20-9 )。如果没有,进入那个菜单,把它改成小端。Endianness 菜单项下面是 Bitness 选项。它应该已经显示为 32 位。如果没有,那就换一个。

img/326071_2_En_20_Fig9_HTML.jpg

图 20-9

检查字节序和字节序

最后,用 exit 按钮退出这个子菜单。

操作系统

再次进入主菜单(图 20-10 ,选择操作系统,然后选择目标 OS(裸机),如图 20-11 所示。

img/326071_2_En_20_Fig12_HTML.jpg

图 20-12

选择 Linux(不是裸机)

img/326071_2_En_20_Fig11_HTML.jpg

图 20-11

选择目标操作系统(裸机)

img/326071_2_En_20_Fig10_HTML.jpg

图 20-10

选择操作系统

在图 20-12 中选择“linux”后,退出并返回主菜单。

二元实用程序

在主菜单上,打开二进制实用程序菜单(图 20-13 )。

img/326071_2_En_20_Fig13_HTML.jpg

图 20-13

二进制实用程序菜单

光标向下移动到 Binutils 版本并打开它(图 20-14 )。

img/326071_2_En_20_Fig14_HTML.jpg

图 20-14

binutils 版本菜单

一旦进入最终的 binutils 版本选择菜单,选择最新的版本,除非您在之前的构建尝试中遇到了麻烦(图 20-15 )。

img/326071_2_En_20_Fig15_HTML.jpg

图 20-15

选择 binutils 版本(通常是最新的)

退出回到主菜单。

c 编译器

在主菜单上,打开 C 编译器子菜单(图 20-16 )。

img/326071_2_En_20_Fig16_HTML.jpg

图 20-16

主菜单的 C 编译器子菜单

这里建议您启用显示 Linaro 版本选项(图 20-17 )。

img/326071_2_En_20_Fig17_HTML.jpg

图 20-17

启用“显示 Linaro 版本”

启用后,您可以选择 Gcc 版本子菜单(图 20-18 )。

img/326071_2_En_20_Fig18_HTML.jpg

图 20-18

选择菜单“gcc 版本”

图 20-19 显示了选择的 Linaro 编译器的最新版本。

img/326071_2_En_20_Fig19_HTML.jpg

图 20-19

选择最新的 linaro 编译器

然后再次选择退出以返回主菜单。

保存配置

除非您有理由更改其他内容,否则请再次退出菜单,以显示保存提示:

img/326071_2_En_20_Figa_HTML.jpg

选择Yes后,命令在保存配置后退出。此时,值得一提的是,您可能希望将您的配置保存在当前目录之外的某个位置。配置保存在名为.config的文件中,可以复制到其他地方进行备份:

$ cp .config ~/ct-ng.config.bak

如果使用ct-ng distclean,将文件保存在当前目录之外将防止意外丢失。

构建交叉编译器

检查/opt/x-tools的所有权。如果您不拥有此目录,现在更改所有权(用您的用户 id 代替fred):

$ sudo chown -R fred /opt/x-tools

这将使您不必使用 root 权限执行构建过程。现在您可以开始构建交叉编译器了。请注意,您的系统需要连接到互联网才能下载,除非之前已经尝试过下载:

$ cd ~/xrpi/devel/staging
$ ct-ng build

为这项工作留出四个小时的时间,如果是重复的话就更少了(第一次下载文件)。理想情况下,您可以让命令运行,并在早上检查是否成功完成。在这一阶段出现不同的软件问题并不罕见,但是如果确实出现了,请阅读下一节,了解一些故障排除技巧。如果一切顺利,ct-ng将工具编译并安装到/opt/x-tools中,无需任何进一步的交互。

$ ct-ng build
[INFO ]  Performing some trivial sanity checks
[INFO ]  Build started 20180713.233346
[INFO ]  Building environment variables
[WARN ]  Directory '/home/wwg/src' does not exist.
[WARN ]  Will not save downloaded tarballs to local storage.
[EXTRA]  Preparing working directories
[EXTRA]  Installing user-supplied crosstool-NG configuration
[EXTRA]  ======================================================
[EXTRA]  Dumping internal crosstool-NG configuration
[EXTRA]    Building a toolchain for:
[EXTRA]      build  = i686-pc-linux-gnu
[EXTRA]      host   = i686-pc-linux-gnu
[EXTRA]      target = arm-unknown-linux-gnueabi
[EXTRA]  Dumping internal crosstool-NG configuration

: done in 0.20s (at 00:03)
[INFO ]  ======================================================
[INFO ]  Retrieving needed toolchain components' tarballs
[EXTRA]    Retrieving 'automake-1.15'
[EXTRA]    Retrieving 'libtool-2.4.6'
[EXTRA]    Retrieving 'linux-4.10.8'
...
[INFO ]  Installing C library: done in 1950.45s (at 165:01)
[INFO ]  ======================================================
[INFO ]  Installing final gcc compiler
[EXTRA]    Configuring final gcc compiler
[EXTRA]    Building final gcc compiler
[EXTRA]    Installing final gcc compiler
[EXTRA]    Housekeeping for final gcc compiler
[EXTRA]       " --> lib (gcc)   lib (os)
[INFO ]  Installing final gcc compiler: done in 3446.75s (at 222:28)
[INFO ]  ======================================================
[INFO ]  Finalizing the toolchain's directory
[INFO ]    Stripping all toolchain executables
[EXTRA]    Installing the populate helper
[EXTRA]    Installing a cross-ldd helper
[EXTRA]    Creating toolchain aliases
[EXTRA]    Removing installed documentation
[INFO ]  Finalizing the toolchain's directory: done in 4.55s (at 222:33)
[INFO ]  Build completed at 20180714.031618
[INFO ]  (elapsed: 222:32.13)
[INFO ]  Finishing installation (may take a few seconds)...
[222:33] /
$

根据所报告的 222:33 的数字,完成这个下载和构建大约需要 4 小时 15 分钟。这个构建是在一个旧的单处理器 Devuan Linux 实例上执行的。

解决纷争

从这个构建过程中获得的会话输出非常简洁。因此,你并不总是清楚地知道真正的失败是什么。出于这个原因,您将经常需要检查build.log文件:

$ less build.log

使用less,您可以通过键入一个大写的 g 来导航到build.log文件的末尾。

一开始经常发生的一个故障是下载失败。虽然构建过程会重试下载并尝试不同的下载方法,但仍然可能会失败。您需要做的就是重试构建。它将只下载其余需要的文件。有时,它会在第二次或第三次重试时成功。

有时组件会在其配置阶段失败。首先检查build.log文件,以准确确定涉及的组件。接下来,您将需要检查这个特定组件的config.log文件。例如,假设isl组件失败了。深入到.build子目录,直到找到它的config.log文件:

$ cd .build/arm-unknown-linux-gnueabi/build/build-isl-host-i686-build_pc-linux-gnu
$ less config.log

导航到config.log的结尾并向后工作几页。最后,您将看到描述所尝试的命令和所产生的错误消息的文本。在一个实例中,我能够确定我添加的定制编译器选项(-fpermissive导致了失败。当时的解决方案是取消该选项,然后再试一次。

某些错误只会在特定的版本选择中出现。有一段时间,我收到与 PPL 相关的错误,需要一个补丁来纠正它。

在解决这些问题时,您可以简单地进行更正,然后重新运行ct-ng build命令。一旦问题得到解决,建议您计划稍后重新构建一切(在clean之后)。这将确保你有一个没有依赖问题的好的构建。

如果在纠正之后,您遇到了同样的问题,您可能需要先执行clean步骤,然后重新开始。根据您认为问题的严重程度,选择以下选项之一:

  • ct-ng clean

  • ct-ng distclean(小心;见下文。)

命令通常就足够了,迫使下一个构建重新开始。任何下载的文件和配置都将保留并被重用。

ct-ng distclean命令更加激烈,因为它删除了所有下载的内容和您的配置文件。我已经把.config文件复制到了.config.bak,惊恐地发现.config.bak也被删除了!因此,如果您备份了.config文件,为了安全起见,请将它复制到当前目录之外的位置。**

最重要的是,保持头脑清醒。如果您感到时间压力或对投入的时间感到愤怒,就很难解决这些问题。当有时间压力时,把它留到另一天,当你能深思熟虑地处理它时。每次重做都需要相当长的时间。尽可能消除猜测。

每个问题,深呼吸,耐心寻找线索,注意错误信息中的细节。还记得电影《阿波罗 13 号》中的那句台词吗:“解决问题,伙计们!”

摘要

在本章中,你已经看到了如何为你的 Raspberry Pi 安装一个交叉编译器,无论是在一个旧的 32 位 Linux 平台上还是在一个 VirtualBox 实例中。这样做将为您提供编译内核或应用所需的编译器工具,用于功能较弱的 Raspberry Pis,如 Zero 或 Zero W。

二十一、交叉编译内核

虽然在嵌入式平台上通常是不可能的,但是用它豪华的根文件系统在你的 Raspberry Pi 上构建内核是可能的。尽管如此,在桌面系统上进行交叉编译通常会获得更快的编译速度。本章研究了在 Pi 之外构建 Raspbian 内核的过程。

假设您已经准备好了交叉编译器工具和环境。第二十章中内置的工具组或已安装的预建工具链均可。在本章中,我假设交叉编译器前缀如下(以连字符结尾):

/opt/x−tools/arm–unknown−linux–gnueabi/bin/*

如果您的工具安装方式不同,请适当替换。

内核可以在 Pi 上构建,在编写本文时,Raspberry Pi 3 B+可能是最好的选择。本章还提供了本地构建的步骤,因为该过程与交叉构建非常相似。

主机环境工具

如果这些工具和库尚未安装,请立即安装:

$ sudo apt-get install git bc
$ sudo apt-get install libncurses5-dev

内核源代码

使用 git 获取内核源代码,除非您选择了另一种方法:

$ cd ~/xrpi/devel/staging
$ git clone $depth=1 https://github.com/raspberrypi/linux
$ cd ./linux

一定要在 git 命令中添加$depth=1选项,以避免下载时间过长。这可以避免下载你可能不关心的历史。

注意

如果您在使用 VirtualBox 的git时遇到问题,可能会涉及到网络问题(重新配置可能会纠正这个问题)。最简单的解决方法是简单地使用 VirtualBox 外部的git,用scp上传master.tar.gz文件。

修复 inputbox.c

我们再次遇到退格字符支持问题。如果您关心这个问题,请应用以下简单的修复方法:

$ nano scripts/kconfig/lxdialog/inputbox.c

在第 128 行周围,找到该行:

    case KEY_BACKSPACE:

并在其下方添加一条case语句:

    case 8:

然后从编辑器中保存它。

使 mrproper

理论上,这一步不应该是必要的。但是内核开发人员希望您无论如何都要这样做,以防某些东西被意外地遗忘。注意,这一步还会删除.config文件(如果需要,将其复制到备份文件中)。

$ cd ~/xrpi/devel/staging/linux
$ make mrproper

警告

命令make mrproper清理一切,包括您的内核.config文件。保存一份.config~/.config.bak或者其他安全的地方,在当前目录之外。

Pi 1/零/零 W 的 Makefile

交叉编译时,编辑 Makefile:

$ cd ~/xrpi/devel/staging/linux
$ nano Makefile

然后将下面两行添加到文件的顶部:

ARCH=arm
CROSS_COMPILE=arm-unknown-linux-gnueabi-

根据/opt/x-tools 目录的内容,显示的CROSS_COMPILE值可能与您的不同。列出它以验证:

$ ls /opt/x-tools
arm-unknown-linux-gnueabi  bin  lib  share

宏的值应该与列出的名称完全一致,并在其末尾添加一个尾随连字符。

Pi 1/零/零 W 的配置

编辑 Makefile 后,对 PATH 变量应用以下更改(如果您已注销并再次登录,则再次应用):

$ PATH="/opt/x-tools/arm-unknown-linux-gnueabi/bin:$PATH"

在构建内核之前,您需要一个配置。下载的内核源代码不包括您的 Pi 的内核设置。要生成合适的默认配置,执行以下操作创建一个名为.config的文件:

$ make bcmrpi_defconfig

生成默认配置文件后,您可以使用以下命令对其进行进一步定制:

$ make menuconfig

Pi 2/3/3+/计算模块 3 的 Makefile

如果您从非 Raspberry Pi 平台进行交叉编译,请改为在 Makefile 中添加以下两行:

ARCH=arm
CROSS_COMPILE=arm-unknown-linux-gnueabi-

Pi 2/3/3+/计算模块 3 的配置

调整路径变量:

$ PATH="/opt/x-tools/arm-unknown-linux-gnueabi/bin:$PATH"

然后生成一个默认配置,然后进行定制:

$ make bcm2709_defconfig
$ make menuconfig

zimage dtbs 模块

现在已经建立了配置,开始构建过程。如果您没有计划进行配置更改,您可能仍然会被提示一些配置问题。要继续操作而不更改配置,只需按 Enter 键接受参数的现有值。

您可以单独构建这些组件,也可以使用以下命令一次性构建所有组件:

$ make zImage modules dtbs

构建过程需要相当长的时间。在一个旧的 32 位单核 Devuan Linux 实例上,这一步花了 2 小时 15 分钟。

小费

如果您的/tmp文件系统对于构建来说不够大,您可以将临时文件定向到另一个目录。例如,在您的工作区域使用./tmp:

$ mkdir ./tmp

$ export TMPDIR="$PWD/tmp"

本机安装内核映像

当在 Raspberry Pi 3 B+或类似的平台上构建内核时,可以通过以下步骤将新内核安装到/boot 分区中:

$ sudo make modules_install
$ sudo cp arch/arm/boot/dts/*.dtb /boot/
$ sudo cp arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/
$ sudo cp arch/arm/boot/dts/overlays/README /boot/overlays/

最后一步取决于所涉及的 Pi 类型。使用以下建筑作为 Pi 1/Zero/Zero W:

$ sudo cp arch/arm/boot/zImage /boot/kernel.img

对于 Pi 2/3/3+/计算模块 3,请使用以下代码:

$ sudo cp arch/arm/boot/zImage /boot/kernel7.img

区别只是内核的名字— kernel.img或者kernel7.img

交叉安装

安装交叉编译的内核时,需要将内核和相关文件放入 SD 卡。在接下来的过程中,我假设您已经在交叉编译的主机文件系统上安装了 SD 卡。要找出您的 SD 卡在 Linux 下的位置,这是使用lsblk的一种方法:

$ lsblk
NAME     MAJ:MIN   RM     SIZE   RO   TYPE MOUNTPOINT
sda        8:0      0   149.1G    0   disk
|─sda1      8:1      0   147.3G    0   part /
|─sda2      8:2      0       1K    0   part
|─sda5      8:5      0     1.8G    0   part [SWAP]
sdb        8:16     1     7.2G    0   disk
|─sdb1      8:17     1    43.2M    0   part
|─sdb2      8:18     1     7.2G    0   part
sr0       11:0      1    1024M    0   rom

另一个命令blkid提供了更多信息,但必须以 root 用户身份运行:

$ sudo blkid
/dev/sda1: UUID="51d355c1-2fe1-4f0e-aaae-01d526bb27b5" \
           TYPE="ext4" PARTUUID="61c63d91-01"
/dev/sda5: UUID="83a322e3-11fe-4a25-bd6c-b877ab0321f9"
           TYPE="swap" PARTUUID="61c63d91-05"

/dev/sdb1: LABEL="boot" UUID="6228-7918" \
           TYPE="vfat" PARTUUID="f8dea240-01"
/dev/sdb2: LABEL="rootfs" UUID="6bfc8851-cf63-4362-abf1-045dda421aad" \
           TYPE="ext4" PARTUUID="f8dea240-02"

从上面可以明显看出/dev/SDb1 保存了插入的 sd 卡的/boot 分区。将该文件和“rootfs”文件挂载到某个地方,例如:

# mkdir /mnt/boot
# mkdir /mnt/root
# mount /dev/sdb1 /mnt/boot
# mount /dev/sdb2 /mnt/root

安装了 SD 卡后,您就可以更换内核了。建议您重命名原始的kernel.img文件,以备以后恢复。

# cd /mnt/boot
# mv kernel.img kernel.was

交叉模块安装

一旦原始内核在 SD 卡上被安全地重命名,您就可以将新内核复制到 SD 卡的/boot分区。回到您的普通用户 id,执行:

$ cd ~/xrpi/devel/staging/linux
$ sudo make INSTALL_MOD_PATH=/mnt/root modules_install

这将把编译后的模块安装到挂载的根文件系统中。注意参数INSTALL_MOD_PATH是如何指定文件系统的位置的。

交叉内核文件安装

对于较小的 Pi,使用以下公式:

$ sudo cp arch/arm/boot/zImage /mnt/boot/kernel.img

对于较大的 pi,请使用 kernel7.img 作为目标文件名:

$ sudo cp arch/arm/boot/zImage /mnt/boot/kernel7.img

随后是以下副本:

$ sudo cp arch/arm/boot/dts/*.dtb /mnt/boot/
$ sudo cp arch/arm/boot/dts/overlays/*.dtb* /mnt/boot/overlays/
$ sudo cp arch/arm/boot/dts/overlays/README /mnt/boot/overlays/

现在,您可以安全地卸载 SD 卡文件系统:

$ sudo unmount /mnt/boot
$ sudo unmount /mnt/root

烟雾测试

完成所有艰苦的工作后,我们现在可以将 SD 卡插入目标 Pi 并启动了!将卡插入 pizero(主机名为 Pi Zero)后,就启动了,我登录运行dmesg。第二行确认我们已经使用了新的交叉编译内核:

[    0.000000] Linux version 4.14.56+ (wwg@devuan) \
    (gcc version 6.3.1 20170109 (crosstool-NG crosstool-ng-1.23.0)) \
    #1 Tue Jul 17 23:09:49 EDT 2018

启动失败

如果您在控制台上看到初始彩色闪烁屏幕,这表明kernel.img文件加载/启动失败。

摘要

本章有几个步骤,但很多都与 Pi 模型的差异有关。一旦提取了目标平台所需的步骤,过程就简单了。能够为您的 Pi 构建新的内核意味着您可以启用和禁用您选择的组件和子系统。更令人兴奋的是可以编写新的内核模块来充分利用您的系统。

二十二、DHT11 传感器

DHT11 湿度和温度传感器是由 D-Robotics UK ( www.droboticsonline.com )制造的经济型外设。它能够在 0 至 50°C 的工作温度范围内测量 20%至 90% RH 之间的相对湿度,精确度为±5% RH。温度测量范围为 0 至 50°C,精度为±2°C,两个值均以 8 位分辨率返回。

这种分配对于 Linux 应用来说是一种挑战,因为要适应信号时序限制。例如,在 Pi 启动传感器后,要测量的第一个事件发生在大约 12 μs 内。这需要一些特殊的处理。这个项目使用直接 GPIO 访问,因为 sysfs 驱动程序根本无法处理相关事件的速率。

特征

DHT 传感器使用的信号与 1 线协议相似,但响应时间不同。此外,没有设备序列号支持。这些因素使得该设备与 Linux 内核中的单线驱动程序不兼容。图 22-1 显示了位于试验板上的 DHT11 传感器。

img/326071_2_En_22_Fig1_HTML.jpg

图 22-1

DHT11 传感器前视图(左),后视图(右)。引脚 1 是面向封装正面的最左侧引脚(左图)。

与许多单线外设不同,DHT11 传感器需要电源。数据手册指出,DHT11 可以在 3.3 至 5.5 V 的范围内供电(这也可以在图 22-1 中的器件背面看到)。从 Raspberry Pi 的 3.3 V 电源供电,可将信号电平保持在 GPIO 的安全范围内。该器件的功耗介于 0.5 和 2.5 mA 之间。对于那些担心电池寿命的人来说,其待机电流据说为 100 至 150 μA。

电路

图 22-2 显示了 Raspberry Pi 和 DHT11 传感器之间的一般电路连接。引脚 4 连接到公共地,而引脚 1 连接到 3.3 V 电源。引脚 2 是信号引脚,与选定的 GPIO 引脚通信。dht11.c的程序列表被配置为使用 GPIO 22。这可以在命令行上覆盖。

img/326071_2_En_22_Fig2_HTML.jpg

图 22-2

DHT11 电路

当 Pi 监听 GPIO 引脚而 DHT11 不发送数据时,线路会悬空。因此, R 1 用于将线路上拉至 3.3 V 的电平。数据手册建议使用 5kω电阻(可以安全地用更常见的 4.7 千欧电阻代替)。激活时,GPIO 引脚或传感器上的负载小于 1 mA。数据手册还指出,5 千欧电阻应适用于最长 20 米的电缆。

草案

传感器只有在主人(Raspberry Pi)的刺激下才会说话。主机必须首先在总线上发出请求,并等待传感器做出响应。DHT 传感器响应 40 位信息,其中 8 位是校验和。

总体协议

整个信号协议是这样工作的:

  1. 由于上拉电阻的原因,线路空闲为高电平。

  2. 主机将线路拉低至少 18 ms,以发出读请求信号,然后释放总线,使线路返回高电平状态。

  3. 暂停约 20 至 40 μs 后,传感器会将线路拉低 80 μs,然后再让线路返回高电平 80 μs,这表示它打算返回数据。

  4. 然后,DHT11 将 40 位信息写入总线:每一位以 50 μs 低电平开始,随后是:

    1. 26 至 28 μs 的高电平表示 0 位

    2. 70 μs 高电平表示 1 位

  5. 当传感器再次将线路拉低 50 μs 时,传输结束。

  6. 传感器释放总线,使线路回到高空闲状态。

图 22-3 显示了传感器的整体协议。主控制用粗线表示,而传感器控制用细线表示。最初,总线处于空闲状态,直到主机将线路拉低并释放它(标记为 Request)。传感器抓取总线并发出信号表示总线正在响应(80 μs 低电平,然后 80 μs 高电平)。传感器以 40 位传感器数据结束,再一次转变为低电平(标记为 End)以标志最后一位的结束。

img/326071_2_En_22_Fig3_HTML.jpg

图 22-3

通用 DHT11 协议

数据位

如图 22-4 所示,每个传感器数据位开始转换为低电平,然后转换为高电平。当线路作为下一位的一部分再次变为低电平时,该位结束。最后一位由一个最终的低到高转换来标记。

img/326071_2_En_22_Fig4_HTML.jpg

图 22-4

DHT11 数据位

每个数据位从转换到低电平开始,持续 50 μs。最后一位之后最终转换到低电平也持续 50 μs。在该位从低电平转换到高电平之后,如果高电平仅持续 26 到 28 μs,则该位变为 0 位。1 位则保持高电平 70 μs。

数据格式

图 22-5 显示了 40 位传感器响应,首先传输最高有效位。数据手册规定了 16 位相对湿度、16 位摄氏度温度和 8 位校验和。然而,DHT11 总是为湿度和温度分数字节发送 0。因此,该器件每次测量的精度实际上只有 8 位。据推测,其他模型(或未来的模型)提供了更高精度的分数值。

img/326071_2_En_22_Fig5_HTML.jpg

图 22-5

DHT11 数据格式

校验和是前 4 个字节的简单总和。任何进位溢出都被简单地丢弃。这种校验和使您的应用在面临可能的接收错误时更有信心接收到正确的值。

图 22-6 显示了 DHT11 信号的总体范围轨迹。在该图中,水平轴由 Pi 驱动的 30 ms 低电平信号控制,用于唤醒器件。DHT11 在第一个初始尖峰后发送其 40 位数据,如右图所示。数据手册指出,每秒钟对传感器的查询不应超过一次。

img/326071_2_En_22_Fig6_HTML.png

图 22-6

DHT11 信号的范围跟踪概述

DHT11 响应数据的特写如图 22-7 所示。第一个高电平脉冲(左侧)来自 Pi,它释放总线并允许上拉电阻提高总线电压。12 μs 后,DHT11 将总线拉低 80 μs,然后让总线再拉高 80 μs,这标志着随后 40 位数据的开始。

img/326071_2_En_22_Fig7_HTML.png

图 22-7

DHT11 响应数据位开始的范围跟踪

软件

为读取 Raspberry Pi 上的 DHT11 传感器而编写的用户空间软件使用 GPIO 引脚的直接寄存器访问。这种方法带来的挑战包括:

  • 短时序:26 至 70 μs

  • Linux 内核中的抢占式调度延迟

一种方法是计算在到达位的末端之前(当线路变低时)程序可以读取高电平信号的次数。然后决定 0 位用于较短时间,1 位用于较长时间。经过一些实验后,可以画出一条分界线,其中较短的信号表示 0,而其他信号表示 1。

源代码

这个项目的源代码可以在以下目录中找到:

$ cd ~/RPi/dht11

要从头开始重建应用,请执行以下操作:

$ make clobber
$ make

使用-h 选项可以获得帮助:

$ ./dht11 -h
Usage: ./dht11 [-g gpio] [-h]
where:
      -g gpio   Specify GPIO pin (22 is default)
      -h        This help

时机

这种应用的主要挑战之一是执行快速准确的计时。但是,我们无法通过 NTP 守护程序(网络时间协议)更新系统时钟来获得准确的时间测量。因此,我们不依赖挂钟的时间感,而是使用 Linux 单调时钟。这也可以由系统稍微调整,但我们保证这个时钟只在时间上向前递增。

清单 22-1 展示了用于从 Raspbian Linux 获取单调时间的短内联函数。Linux 将 struct timespec 声明为:

struct timespec {
    time_t  tv_sec;     /* seconds */
    long    tv_nsec;    /* and nanoseconds */
}

所以我们的timeofday()函数将返回秒和纳秒。

0042: static inline void
0043: timeofday(struct timespec *t) {
0044:     clock_gettime(CLOCK_MONOTONIC,t);
0045: }

Listing 22-1The dht11.c, timeofday() function

计算经过时间的一般程序是:

  1. 捕捉初始时间(称之为 t0)。

  2. 在事件发生后捕获当前时间(称之为 t1)。

清单 22-2 中ns_diff()形式的 a 函数用于计算经过的时间。

0057: static inline long
0058: ns_diff(struct timespec *t0,struct timespec *t1) {
0059:     int dsec = (int)(t1->tv_sec - t0->tv_sec);
0060:     long dns = t1->tv_nsec - t0->tv_nsec;
0061:
0062:     assert(dsec >= 0);
0063:     dns += dsec * 1000000000L;
0064:     return dns;
0065: }

Listing 22-2The dht11.c, ns_diff() function to calculate elapsed time in nanoseconds

主循环

主循环位于 main()函数中。处理完命令行选项后,循环的顶部如清单 22-3 所示。

0192: gpio_open();
0193:
0194: gpio_configure_io(gpio_pin,Output);
0195: gpio_write(gpio_pin,1);
0196:
0197: for (;; ++reading) {
0198:        wait_ready();
0199:
0200:        gpio_write(gpio_pin,1);
0201:        gpio_configure_io(gpio_pin,Output);
0202:        wait_ms(3);

Listing 22-3The top of the main loop in dht11.c

第 192 行初始化用于直接 GPIO 访问的库(见源文件libgp.clibgp.h)。随后,GPIO 引脚被配置为线 194 中的输出,并且最初在线 195 中被驱动为高电平。

主循环从第 197 行开始。计数器变量reading仅用于在输出报告中提供递增的读数计数器。第 198 行启动功能wait_ready(),稍后将对其进行描述。其目的是防止程序查询 DHT11 设备的频率超过每秒一次。如果查询太频繁,设备就无法响应。

在确定可以查询 DHT11 设备之后,行 200 将 GPIO 输出的电平设置为高。除了第一次进入环路,GPIO 被配置为一个输入引脚。在将其配置为线 201 中的输出之前将其设置为高意味着当 GPIO 再次成为输出时,从其当前输入状态转换为高时没有毛刺。线路 202 简单地等待 3 ms 以允许线路稳定。

等待毫秒( )

为了提供相当精确的毫秒级等待功能,使用了poll(2)系统调用(清单 22-4 )。这通常用于监控打开的文件描述符。但是poll(2)可以不带描述符使用,利用它的超时参数(第 92 行的参数三)。注意参数二是如何表示零个文件描述符条目的。

0087: static void
0088: wait_ms(int ms) {
0089: struct pollfd p[1];
0090: int rc;
0091:
0092: rc = poll(&p[0],0,ms

);
0093: assert(!rc);
0094: }

Listing 22-4The wait_ms() function in dht11.c

等待就绪( )

该功能用于防止过于频繁地查询设备。清单 22-5 展示了所使用的代码。

0067: static void
0068: wait_ready(void) {
0069: static struct timespec t0 = {0L,0L};
0070: struct timespec t1;
0071:
0072: if ( !t0.tv_sec ) {
0073:        timeofday(&t0);
0074:        --t0.tv_sec;
0075: }
0076:
0077: for (;;) {
0078:        timeofday(&t1);
0079:        if ( ms_diff(&t0,&t1) >= 1000 ) {
0080:              t0 = t1;
0081:              return;
0082:        }
0083:        usleep(100);
0084:  }
0085: }

Listing 22-5The wait_ready() function in dht11.c

变量t0static值用零建立。当第一次输入代码时,日期/时间在第 73 行被初始化,然后减去一秒(第 74 行)。减去 1 允许第一次立即通过。

第 77 到 84 行中的循环每 100 微秒采样一次时间,如果从最后一次设备请求开始至少过去了一秒钟,则在第 81 行返回。

阅读 DHT11

现在有趣的部分来了——读取设备响应。清单 22-6 展示了所使用的逻辑。回想一下,线路 206 驱动总线线路为低,以唤醒设备。那么 30 ms 是在 GPIO 变成输入引脚之前使用的延迟时间(第 207 和 208 行)。通过将 GPIO 配置为输入引脚,我们现在让上拉电阻接管并将总线电压上拉至+3.3 V。

此时,DHT11 将最终抢占总线并做出响应。我们使用函数wait_change()等待换行(第 210 行)。它返回总线线路的当前(最终)状态,并用过去的纳秒数填充变量nsec

第一次转换有时发生得如此之快(在 Raspberry Pi 3 B+上),以至于它看到自己的 GPIO 线在上拉电阻完成其工作之前从低到高。第 219 行测试了这一点,如果这确实是真的,我们等待另一个信号转换——我们关心的信号转换,即从高到低的转换(第 220 行)。如果最终状态仍然是 1 位,或者nsec中的时间太高,我们拒绝响应并重新开始(第 221 到 223 行)。

0206:        gpio_write(gpio_pin,0);
0207:        wait_ms(30);
0208:        gpio_configure_io(gpio_pin,Input);
0209:
0210:        b = wait_change(&nsec);
0211:
0212:        /*
0213:         * If the returned value is 1, it is likely
0214:         * that we were fast enough to catch the
0215:         * pullup resistor action. When that happens
0216:         * look for the next transition

(expecting
0217:         * b == 0).
0218:         */
0219:        if ( b == 1 )
0220:              b = wait_change(&nsec);
0221:        if ( b || nsec > 20000 ) { // Expecting about 12 us
0222:              printf("%04d: Fail, b0=%d, %ld nsec\n",reading,b,nsec);
0223:              continue;
0224:        }
0225:
0226:        /*
0227:         * This is the 80 us transition from 0 to 1:
0228:         */
0229:        b = wait_change(&nsec);
0230:        if ( !b || nsec < 40000 || nsec > 90000 ) {
0231:              printf("%04d: Fail, b1=%d, %ld nsec\n",reading,b,nsec);
0232:              continue;
0233:        }
0234:
0235:        /*
0236:         * Wait for the 80 us transition from 1 to 0:
0237:         */
0238:        b = wait_change(&nsec);
0239:
0240:        if ( b != 0 || nsec < 40000 || nsec > 90000 ) {
0241:              printf("%04d: Fail, b2=%d, %ld nsec\n",reading,b,nsec);
0242:              continue;
0243:        }
0244:
0245:        /*
0246:         * Read the 40-bit value from the DHT11\. The
0247:         * returned value is distilled into 16-bits:
0248:         */
0249:        unsigned resp = read_40bits();

Listing 22-6Reading the DHT11 response in dht11.c

在进入线 229 时,该线为低。等待下一个转换应该报告总线已经变高,时间接近 80 μs。如果最终状态不是高,或者时间太长,则在第 230 到 232 行拒绝响应。

如果通过,则在第 240 到 243 行测量从高到低的下一次转换。同样,如果测量的信号不正确,响应被拒绝,程序在循环的顶部再次尝试。

最后,在第 259 行,DHT11 的 40 位响应准备好被读取。

等待 _ 更改( )

wait_change()函数用于监控 GPIO 的信号变化。如果信号最初为低电平,它会一直等待,直到信号变为高电平并重新开启。如果信号最初为高电平,它会一直等到信号变为低电平并返回 0。除了等待状态更改之外,还会返回经过的纳秒数。该功能如清单 22-7 所示。

0024: static volatile bool timeout = false;
...
0096: static inline int
0097: wait_change(long *nsec) {
0098:  int b1;
0099:  struct timespec t0, t1;
0100:  int b0 = gpio_read(gpio_pin);
0101:
0102:  timeofday(&t0);
0103:
0104:  while ( (b1 = gpio_read(gpio_pin)) == b0 && !timeout )
0105:       ;
0106:  timeofday(&t1);
0107:
0108:  if ( !timeout ) {
0109:        *nsec = ns_diff(&t0,&t1);
0110:        return b1;
0111:  }
0112:  *nsec = 0;
0113:  return 0;
0114: }

Listing 22-7The wait_change() function from dht11.c

程序读取 GPIO 的当前读数,并将其保存在变量b0(第 100 行)。初始时间t0在线 102 中被捕获。第 104 行和第 105 行的 null 语句形成了一个紧密的循环。当前的 GPIO 读数被读取并存入变量b1。只要b1的值等于初始值b0,循环就继续。变量timeout也被测试。只要volatile bool timeout保持false,循环就会继续。稍后,我们将看到timeout值是如何设置的。

一旦 GPIO 从初始值改变,循环通常退出。停止时间被捕获到第 106 行的t1中。只要没有超时,第 109 行计算并返回经过的纳秒数。第 110 行返回 GPIO 的当前状态。

当超时发生时,我们简单地返回零表示经过的时间,返回零表示当前的 GPIO 值。此时的目的是跳出 while 循环。错过的事件可能会发生,尤其是因为 Linux 操作系统可以抢占程序的执行。如果它试图读取 40 位数据,但错过了一个或两个信号变化的读数,循环可能会永远挂起。

超时处理

考虑到程序挂起的可能性,使用了间隔计时器。在一个关键部分的开始,通过调用清单 22-8 中的 set_timer()来启动定时器。这在 Linux 内核中启动了一个定时器,除了启动之外,我们不需要管理它。

间隔定时器的配置在结构定时器中建立(第 28 到 31 行)。我们不希望计时器重启,所以将timer.it_interval成员初始化为零(第 29 行)。第 34 行和第 35 行确定了我们希望在定时器到期之前经过的时间。一旦定时器到期,它将不会自动更新。

当定时器到期时,Linux 内核将调用我们在第 145 到 147 行声明的名为sigalrm_handler()的超时处理程序。它所做的只是将布尔变量timeout设置为true。信号处理程序被异步调用。因此,它们决不能调用不可重入的例程,如printf()malloc()等。因为电话随时可能打来。当信号在执行过程中中断malloc()时,你不会想要调用malloc()

还因为信号处理器是异步,它的处理就像另一个线程。如果变量timeout没有被声明为volatile,循环代码可能永远不会注意到它被更改为 true,因为编译器将该值缓存在寄存器中。

0026: static inline void
0027: set_timer(long usec) {
0028:  static struct itimerval timer = {
0029:        { 0, 0 },    // Interval
0030:        { 0, 0 }     // it_value
0031:  };
0032:  int rc;
0033:
0034:  timer.it_value.tv_sec = 0;
0035:  timer.it_value.tv_usec = usec;
0036:
0037:  rc = setitimer(ITIMER_REAL,&timer,NULL);
0038:  assert(!rc);
0039:  timeout = false;
0040: }
...
0144: static void
0145: sigalrm_handler(int signo) {
0146:  timeout = true;
0147: }

Listing 22-8The timer and handler in dht11.c

信号 SIGALRM 的定时器处理程序的初始设置在主程序中执行,如清单 22-9 所示。一旦建立了超时处理程序,它只需要调用set_timer()来启动它。这是在读取总线上信号的较大循环开始时执行的。如果定时器在 DHT11 的整个响应被读取之前被触发,则循环从清单 22-7 的第 104 行退出。

0187:  new_action.sa_handler = sigalrm_handler;
0188:  sigemptyset(&new_action.sa_mask);
0189:  new_action.sa_flags = 0;
0190:  sigaction(SIGALRM,&new_action,NULL);

Listing 22-9The timer setup in dht11.c

示范

一旦你接通电源,演示程序就可以开始了。该示例说明了将 GPIO 指定为 22,但这是默认值。

$ ./dht11 -g22
0000: RH 32% Temperature 25 C
0001: RH 32% Temperature 26 C
0002: RH 32% Temperature 26 C
0003: RH 32% Temperature 26 C
0004: RH 32% Temperature 26 C
0005: RH 32% Temperature 26 C
0006: RH 32% Temperature 26 C
0007: RH 32% Temperature 26 C
0008: RH 32% Temperature 26 C

在快速 Pi 上,比如 Pi 3 B+,您应该看到这样的输出。如果您没有看到任何成功的读取,那么检查您的布线。不要忘记上拉电阻。

但是,由于错过了事件,可能会看到一些错误:

0040: RH 32% Temperature 26 C
0041: Fail, Checksum error.
0101: RH 32% Temperature 26 C
0102: RH 32% Temperature 26 C
0103: RH 32% Temperature 26 C
0104: RH 32% Temperature 26 C
0105: Fail, Checksum error.
0106: RH 32% Temperature 26 C

不要对此感到惊讶,因为我们是在非实时操作系统上执行信号的实时测量。由于性能较低,Raspberry Pi Zero 和 Zero W 可能会出现更多错误。零点仍然会经常返回好的读数,足以使项目有价值。在一个已完成的项目中,您只需修改代码来隐藏错误报告。

摘要

本章解决了在不提供实时调度的系统上读取 DHT11 实时信号的困难。通过使用直接 GPIO 访问,我们获得了足够快的访问速度来测量信号变化。如果程序因等待丢失事件而停滞不前,应用间隔计时器可以提供恢复安全性。这些是解决棘手问题必须做的一些偷偷摸摸的事情。

二十三、双节棍鼠标

你可能没有实际使用任天堂 Wii 双节棍作为树莓派鼠标,但它是一个很好的例子,说明液晶触摸屏如何提供输入事件。

双截棍有两个按钮:一个 X-Y 操纵杆;以及 X、Y 和 Z 加速度计。传感器数据通过 I 2 C 总线进行通信。这也将给我们一个使用 I 2 C C API 的练习。让我们来为 X Window 系统桌面实现一个双节棍定点设备。

项目概述

我们面临的挑战分为两大类:

  • 双截棍设备的 I 2 C 数据通信

  • 将检测到的数据插入 X 窗口系统桌面事件队列

让我们首先从 Linux API 的角度检查 I 2 C,然后完成将接收到的事件插入 X Window 系统这一章。

双截棍特色

双截棍的基本物理和数据特征列于表 23-1 中。

表 23-1

双截棍控件和数据特征

|

用户界面特征

|

|

数据

|

硬件/芯片

|
| --- | --- | --- | --- |
| c 按钮 | one | 布尔代数学体系的 | 薄膜开关 |
| z 按钮 | one | 布尔代数学体系的 | 薄膜开关 |
| X-Y 操纵杆 | 8x2 | 整数 | 30 千欧姆电位器 |
| x、Y 和 Z 加速度计 | 10x3 | 整数 | ST LIS3L02 系列 |

对于作为鼠标的应用,C 和 Z 按钮代替了鼠标左键和右键。操纵杆用于定位鼠标光标。虽然双截棍通常以 400 kHz 的时钟频率运行,但它在 100 kHz I 2 C 频率下也能正常工作。

连接器引脚排列

有四根线:其中两根是电源和地线(一些单元可能有两根额外的线,一根连接到屏蔽层,另一根连接到未使用的中心引脚)。其余两根线用于 I 2 C 通信(SDA 和 SCL)。表 23-2 显示了电缆端连接器内的连接。

表 23-2

双节棍电缆连接

|

圣地亚哥

|

等级

|

接地

|
| --- | --- | --- |
| +3.3 伏 | 不适用 | 国家药品监督管理局 |

Nunchuk 连接器非常不标准。有些人已经推出了自己的适配器,使用双面 PCB 来匹配内部连接。其他人从易贝购买了适配器。廉价的克隆双节棍也可以在易贝找到。随着越来越多的克隆适配器以更具竞争力的价格上市,没有理由切断克隆适配器的连接器。

小费

当心双节棍赝品。

如果你真的切断了连接器,你会很快发现没有标准的电线配色方案。你唯一能指望的是引脚的布局如表 23-2 所示。如果你有一个真正的 Wii 双截棍,表 23-3 中列出的电线颜色可能是有效的。标有“克隆线”的列列出了我自己的克隆线的线颜色。你的可能会不同

表 23-3

双节棍连接器布线

|

别针

|

Wii Wire

|

克隆线

|

描述

|

第一亲代

|
| --- | --- | --- | --- | --- |
| 接地 | 白色的 | 白色的 | 地面 | P1-25 |
| 国家药品监督管理局 | 格林(姓氏);绿色的 | 蓝色 | 数据 | P1-03 |
| +3.3 伏 | 红色 | 红色 | 电源 | P1-01 |
| 圣地亚哥 | 黄色 | 格林(姓氏);绿色的 | 时钟 | P1-05 |

克隆电线颜色各异!

在您从克隆体上切下连接器之前,考虑您将需要追踪连接器到一个电线颜色。切断电缆,为连接器留下大约 3 英寸的电线。然后,您可以切断绝缘层,用欧姆表(或查看电缆端连接器内部)追踪引脚到导线。

图 23-1 为作者的克隆双截棍,连接器被切断。代替连接器的是焊接实心线末端,并在焊接点上施加一块热缩材料。实心线端非常适合插入原型试验板。

img/326071_2_En_23_Fig1_HTML.png

图 23-1

双截棍克隆,电线末端焊接在

启用 I2C

您需要启用您的 I2C 支持。进入 Raspberry Pi 配置面板,打开 I2C(图 23-2 )。然后重启使其生效。

img/326071_2_En_23_Fig2_HTML.jpg

图 23-2

在 Raspberry Pi 配置面板中启用 I2C 支持

测试连接

将 I 2 C 连接插入 Pi,并用i2cdetect命令探测。

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- 52 -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
$

如果双截棍正在工作,它将显示在十六进制地址 52 处。验证完硬件之后,是时候继续软件了。

双截棍 I 2 C 协议

双截棍包含一个古怪的小控制器,通过 I 2 C 总线进行通信。为了知道写入的字节存储在哪里,写入的第一个字节必须是 8 位寄存器地址。换句话说,每次写入双节棍都需要满足以下要求:

  • 一个寄存器地址字节,随后是

  • 要写入连续位置的零个或多个数据字节

因此,对于写操作,发送到双节棍的第一个字节告诉它从哪里开始。接收到的任何后续写入字节写入时,寄存器地址都会递增。

小费

不要将寄存器地址与双节棍的 I 2 C 地址 0x52 混淆。

也可以写入寄存器地址,然后读取字节。该程序指定了要读取的数据字节的起始位置。

Nunchuk 控制器的奇特之处在于,写入寄存器地址和读取数据之间必须有短暂的延迟。先执行写操作,然后立即执行读操作不起作用。然而,在寄存器地址之后立即写入数据确实会成功。

加密

双截棍被设计成提供一个加密的链接。但是,可以通过某种方式初始化来禁用它。失败程序如下:

  1. 将 0x55 写入 Nunchuk 寄存器位置 0xF0。

  2. 暂停一下。

  3. 将 0x00 写入 Nunchuk 寄存器位置 0xFB。

下面说明了所涉及的消息序列。注意,这是作为两个分开的 I 2 C 写操作来执行的:

|

|

中止

|

|
| --- | --- | --- |
| 寡霉素敏感比较因子 | Fifty-five | - | 运货单(freight bill) | 00 |

一旦成功执行,所有将来的数据都将以未加密的形式返回。

读取传感器数据

双节棍的全部意义在于读取它的传感器数据。当被请求时,它返回如表 23-4 所示格式的 6 字节数据。

表 23-4

Nunchuk Data

|

字节

|

|

描述

|
| --- | --- | --- |
| one |   | 模拟杆 x 轴值 |
| Two |   | 模拟棒 y 轴值 |
| three |   | x 加速度位 9:2 |
| four |   | y 加速度位 9:2 |
| five |   | z 加速度位 9:2 |
| six | Zero | 按下 z 按钮(低电平有效) |
| one | 按下 c 按钮(低电平有效) |
| 3:2 | x 加速度位 1:0 |
| 5:4 | y 加速度位 1:0 |
| 7:6 | z 加速度位 1:0 |

一些数据被分割成多个字节。例如,X 加速度位 9:2 从字节 3 获得。最低 2 位位于字节 6 的第 3 位和第 2 位。这些共同构成 9 位 X 加速度值。

为了检索这些数据,我们总是需要告诉双节棍从哪里开始。因此,该序列总是从写入偏移量 0x00 开始,然后暂停:

|

|

中止

|

读取 6 个字节

|
| --- | --- | --- |
| 00 | - | 01 | 02 | 03 | 04 | 05 | 06 |

双节棍不允许我们在一个ioctl(2)调用中,作为两个 I/O 消息来做这件事。写入零后必须有一个停顿。那么这六个数据字节可以作为一个单独的 I 2 C 读操作来读取。但是,如果暂停时间过长,Nunchuk 控制器就会超时,导致返回不正确的数据。所以我们必须用双截棍的方式做事。

Linux 用户界面

虽然阅读双节棍很有趣,但我们需要将它作为鼠标应用到桌面上。我们需要根据从中读取的内容插入鼠标事件。

Linux uinput驱动程序允许程序员开发非标准的输入驱动程序,以便可以将事件注入到输入流中。这种方法允许在不改变应用代码的情况下添加新的输入流(比如触摸屏输入)。

可以在以下站点找到 uinput API 的文档:

另一个信息来源是设备驱动程序源代码本身:

drivers/input/misc/uinput.c

本章提供的示例程序有助于将所有必要的细节整合在一起。

使用头文件

uinput API 所需的头文件包括以下内容:

#include <sys/ioctl.h>
#include <linux/input.h>
#include <linux/uinput.h>

为了利用 I 2 C 编译代码,您还需要安装libi2c开发库,如果您还没有这样做的话:

$ sudo apt-get install libi2c-dev

打开设备节点

打开设备节点,连接到uinput设备驱动程序:

/dev/uinput

以下是必需的open(2)调用的示例:

int fd;

fd = open("/dev/uinput",O_WRONLY|O_NONBLOCK);
if ( fd < 0 ) {
    perror("Opening /dev/uinput");
    ...

配置事件

为了插入事件,驱动程序必须配置为接受它们。下面代码中对ioctl(2)的每次调用都会基于参数事件启用一类事件。以下是一个概括的例子:

int rc;
unsigned long event = EV_KEY;

rc = ioctl(fd,UI_SET_EVBIT,event);
assert(!rc);

表 23-5 中提供了UI_SET_EVBIT事件类型的列表。最常用的事件类型是EV_SYNEV_KEYEV_REL(或EV_ABS)。

表 23-5

uinput 事件类型列表

|

从头文件输入. h

|
| --- |
| 巨 | 描述 |
| 我的儿子 | 事件同步/分离 |
| EV_KEY | 按键/按钮状态改变 |
| 家庭暴力 | 相对轴鼠标状变化 |
| 电动汽车 _ABS | 绝对轴鼠标状变化 |
| ev _ MSC(MSC)工作表 | 杂项事件 |
| 电动自行车 _ 软件 | 二进制(开关)状态变化 |
| 电动汽车 _LED | LED 开/关变化 |
| S7-1200 可编程控制器 | 输出到声音设备 |
| 电动汽车代表 | 用于自动重复设备 |
| 电动汽车 | 向输入设备强制反馈命令 |
| EV_PWR | 电源按钮/开关事件 |
| 电动汽车 _ FF _ 状态 | 接收力反馈设备状态 |

警告

不要将事件类型放在一起。设备驱动程序期望每个事件类型被分别注册

*#### 配置 EV_KEY

一旦您注册了提供EV_KEY事件的意图,您需要注册所有可能使用的关键代码。虽然这看起来很麻烦,但它确实可以防止错误程序注入垃圾。下面的代码注册了它注入转义键代码的意图:

int rc;

rc = ioctl(fd,UI_SET_KEYBIT,KEY_ESC);
assert(!rc);

要配置所有可能的密钥,可以使用循环。但不要注册密钥代码 0 ( KEY_RESERVED)或 255;包含文件表明代码 255 是为 at 键盘驱动程序的特殊需要而保留的。

int rc;
unsigned long key;

for ( key=1; key<255; ++key ) {
    rc = ioctl(fd,UI_SET_KEYBIT,key);
    assert(!rc);
}

鼠标按钮

除了键码,同样的ioctl(2,UI_SET_KEYBIT)调用用于注册鼠标、操纵杆和其他按钮事件。这包括来自触控板、平板电脑和触摸屏的触摸事件。头文件linux/input.h中定义了按钮代码的长列表。常见的嫌疑人如表 23-6 所示。

表 23-6

关键事件宏

|

|

同义词

|

描述

|
| --- | --- | --- |
| BTN _ 左 | btn _ 鼠标 | 鼠标左键 |
| BTN 对吗 |   | 鼠标右键 |
| BTN 中部 |   | 鼠标中键 |
| BTN_SIDE |   | 鼠标侧键 |

以下示例显示了应用注入鼠标左键和右键事件的意图:

int rc;

rc=ioctl(fd,UI_SET_KEYBIT,BTN_LEFT);
assert(!rc);
rc = ioctl(fd,UI_SET_KEYBIT,BTN_RIGHT);
assert(!rc);

配置电动汽车 _REL

为了注入EV_REL事件,必须预先登记相对运动的类型。有效参数代码的完整列表如表 23-7 所示。以下示例表明了注入 x 轴和 y 轴相对运动的意图:

表 23-7

UI_SET_RELBIT 选项

|

|

目的

|
| --- | --- |
| REL_X | 发送相对 X 变化 |
| 继电器 _Y | 发送相对 Y 变化 |
| REL_Z | 发送相对 Z 值变化 |
| REL_RX | x 轴倾斜 |
| 继电器 _RY | y 轴倾斜 |
| rel _ 罗马 | z 轴倾斜 |
| REL_HWHEEL | 水平车轮更换 |
| REL_DIAL | 转盘改变 |
| REL 车轮 | 换车轮 |
| REL_MISC | 多方面的 |

rc = ioctl(fd,UI_SET_RELBIT,REL_X);
assert(!rc);
rc = ioctl(fd,UI_SET_RELBIT,REL_Y);
assert(!rc);

配置 EV_ABS

虽然这个项目不使用EV_ABS选项,但是了解这个特性可能是有用的。这个事件代表绝对的光标移动,它也需要意图的注册。EV_ABS代码的完整列表在linux/input.h中定义。表 23-8 中定义了通常的嫌疑人。

表 23-8

绝对光标移动事件宏

|

|

描述

|
| --- | --- |
| ABS_X | 将 X 移动到这个绝对 X 坐标 |
| ABS_Y | 将 Y 移动到这个绝对 Y 坐标 |

以下是注册绝对 x 轴和 y 轴事件意图的示例:

int rc;

rc = ioctl(fd,UI_SET_ABSBIT,ABS_X);
assert(!rc);
rc = ioctl(fd,UI_SET_ABSBIT,ABS_X);
assert(!rc);

除了注册注入这些事件的意图之外,还需要定义一些坐标参数。下面是一个例子:

struct uinput_user_dev uinp;

uinp.absmin[ABS_X] = 0;
uinp.absmax[ABS_X] = 1023;

uinp.absfuzz[ABS_X] = 0;
uinp.absflat[ABS_X] = 0;

uinp.absmin[ABS_Y] = 0;
uinp.absmax[ABS_Y] = 767;

uinp.absfuzz[ABS_Y] = 0;
uinp.absflat[ABS_Y] = 0;

这些值必须作为ioctl(2,UI_DEV_CREATE)操作的一部分来建立,这将在下面描述。

创建节点

在所有向uinput设备驱动程序的注册完成后,最后一步是创建uinput节点。这将由接收应用使用,以便读取注入的事件。这涉及两个编程步骤:

  1. write(2)将结构uinput_user_dev信息写入文件描述符。

  2. 执行ioctl(2,UI_DEV_CREATE)以创建uinput节点。

第一步涉及填充以下结构:

struct input_id {
    __u16       bustype;
    __u16       vendor;
    __u16       product;
    __u16       version;
};

struct uinput_user_dev {
    char        name[UINPUT_MAX_NAME_SIZE];
    struct input_id id;
    int         ff_effects_max;
    int         absmax[ABS_CNT];
    int         absmin[ABS_CNT];
    int         absfuzz[ABS_CNT];
    int         absflat[ABS_CNT];
};

下面提供了一个填充这些结构的示例。如果您计划注入EV_ABS事件,您还必须填充abs成员,在“配置EV_ABS一节中提到过。

       struct uinput_user_dev uinp;
       int rc;

       memset(&uinp,0,sizeof uinp);

       strncpy(uinp.name,"nunchuk",UINPUT_MAX_NAME_SIZE);

       uinp.id.bustype = BUS_USB;
       uinp.id.vendor = 0x1;
       uinp.id.product = 0x1;
       uinp.id.version = 1;

//       uinp.absmax[ABS_X] = 1023; /∗EV_ABS only ∗/
//       ...

       rc = write(fd,&uinp,sizeof(uinp));
       assert(rc == sizeof(uinp));

write(2)的调用将所有这些重要信息传递给uinput驱动程序。现在剩下的就是请求创建一个设备节点供应用使用:

int rc;

rc = ioctl(fd,UI_DEV_CREATE);
assert(!rc);

该步骤使uinput驱动程序使一个设备节点出现在伪目录/dev/input中。这里显示了一个示例:

$ ls -l /dev/input
total 0
crw-rw---- 1 root input 13, 64 Jul 26 06:11 event0
crw-rw---- 1 root input 13, 63 Jul 26 04:50 mice
crw-rw---- 1 root input 13, 32 Jul 26 06:11 mouse0

当程序运行时,设备/dev/input/event0是双截棍创建的uinput节点。

发布 EV_KEY 事件

下面的代码片段显示了如何发布一个按键按下事件,然后是一个按键按下事件:

 1 static void
 2 uinput_postkey(int fd,unsigned key) {
 3     struct input_event ev;
 4     int rc;
 5
 6     memset(&ev,0,sizeof(ev));
 7     ev.type = EV_KEY;
 8     ev.code = key;
 9     ev.value = 1;
10
11     rc = write(fd,&ev,sizeof(ev));
12     assert(rc == sizeof(ev));
13
14     ev.value = 0;
15     rc = write(fd,&ev,sizeof(ev));
16     assert(rc == sizeof(ev));
17 }

从这个例子中,您可以看到每个事件都是通过编写适当初始化的input_event结构来提交的。该示例说明了名为type的成员被设置为EV_KEYcode被设置为按键代码,并且通过将成员value设置为 1 来指示按键(第 9 行)。

为了注入一个键向上事件,value被重置为 0(第 14 行)并且该结构被再次写入。

鼠标按钮事件以同样的方式工作,除了您为code成员提供鼠标按钮代码。例如:

memset(&ev,0,sizeof(ev));
ev.type = EV_KEY;
ev.code = BTN_RIGHT;        /∗Right click ∗/
ev.value = 1;

发布 EV_REL 活动

为了发布相对的鼠标移动,我们将input_event填充为类型EV_REL。成员code被设置为事件类型(本例中为REL_XREL_Y),相对运动的值在成员value中建立:

static void
uinput_movement(int fd,int x,inty) {
     struct input_event ev;
     int rc;

     memset(&ev,0,sizeof(ev));
     ev.type = EV_REL;
     ev.code = REL_X;
     ev.value = x;

     rc = write(fd,&ev,sizeof(ev));
     assert(rc == sizeof(ev));

     ev.code = REL_Y;
     ev.value = y;
     rc = write(fd,&ev,sizeof(ev));
     assert (rc == sizeof(ev));
}

请注意,REL_XREL_Y事件是分别创建的。如果您希望接收应用避免单独处理这些,该怎么办呢?EV_SYN事件在这方面有所帮助(下)。

发布 EV_SYN 事件

uinput驱动程序推迟事件的传递,直到EV_SYN事件被注入。EV_SYN事件的SYN_REPORT类型导致排队的事件被清除并报告给感兴趣的应用。下面是一个例子:

static void
uinput_syn(int fd) {
    struct input_event ev;
    int rc;

    memset(&ev,0,sizeof(ev));
    ev.type = EV_SYN;
    ev.code = SYN_REPORT;
    ev.value = 0;
    rc = write(fd,&ev,sizeof(ev));
    assert(rc == sizeof(ev));
}

例如,对于鼠标相对移动事件,您可以注入一个REL_XREL_Y,然后注入一个SYN_REPORT事件,让应用将它们视为一组。

关闭输入

这涉及到两个步骤:

  1. /dev/input/event%d节点的破坏

  2. 文件描述符的关闭

以下示例显示了这两种情况:

int rc;

rc = ioctl(fd,UI_DEV_DESTROY);
assert(!rc);
close(fd);

关闭文件描述符意味着ioctl(2,UI_DEV_DESTROY)操作。应用可以选择销毁设备节点,同时保持文件描述符打开。

x 窗口

只有当我们的桌面系统在监听时,我们新的uinput设备节点的创建才是有用的。Raspbian Linux 的 X-Window 系统需要一点配置帮助来注意我们的弗兰肯斯坦创造。下面的定义可以添加到/usr/share/X11/xorg.conf.d目录中。将文件命名为20-nunchuk.conf:

# Nunchuck event queue

Section "InputClass"
        Identifier "Raspberry Pi Nunchuk"
        Option "Mode" "Relative"
        MatchDevicePath "/dev/input/event0"
        Driver "evdev"
EndSection

# End 20−nunchuk.conf

只有当你的双截棍uinput设备显示为/dev/input/event0时,这个配置更改才会生效。如果你的树莓 Pi 上有其他专门的输入设备,它可以被命名为event1或其他数字。请参阅下一节“测试双节棍”获取故障排除信息。

重启 X-Window 服务器,让配置文件被注意到。

小费

通常,你的双截棍程序应该已经在运行了。但是 X-Window 服务器会在双节棍启动时注意到它。

输入实用程序

当编写基于事件的代码时,你会发现包input-utils非常有用。可以从命令行安装该软件包,如下所示:

$ sudo apt-get install input-utils

将安装以下命令:

  • lsinput(8):列出uinput个设备

  • input-events(8):转储选中的uinput事件

  • input-kbd(8):键盘地图显示

本章使用前两个实用程序:lsinput(8)input-events(8)

测试双截棍

现在硬件、驱动和软件都准备好了,是时候练习双截棍了。不幸的是,应用无法直接识别您创建的uinput节点。当 Nunchuk 程序运行时,节点可能显示为/dev/input/event0或其他一些已存在的编号节点。如果您想在 Linux 引导过程中启动一个 Nunchuk 驱动程序,您需要创建一个脚本来编辑注册了实际设备名的文件。受影响的 X-Windows 配置文件如下:

/usr/share/X11/xord.conf.d/20-nunchuk.conf

脚本(如下所示)决定了 Nunchuk 程序创建了哪个节点。以下是运行 Nunchuk 程序时的运行示例:

$ ./findchuk
/dev/input/event0

当没有找到节点时,findchuk脚本用一个非零代码退出,并向stderr打印一条消息:

$ ./findchuk
Nunchuk uinput device not found.
$ echo $?
1

清单 23-1 中显示了findchuk脚本。

#!/bin/bash
###############################################################
# Find the Nunchuck
###############################################################
#
# This script locates the Nunchuk uinput device by searching the
# /sys/devices/virtual/input pseudo directory for names of the form:
# input[0_9]∗. For all subdirectories found, check the ./name pseudo
# file, which will contain "nunchuk". Then we derive the /dev path
# from a sibling entry named event[0_9]∗. That will tell use the
# /dev/input/event%d pathname, for the Nunchuk.

DIR=/sys/devices/virtual/input  # Top level directory

set_eu

cd "$DIR"
find . −type d −name 'input[0−9]∗' | (
      set −eu
      while read dirname ; do
              cd "$DIR/$dirname"
              if [−f "name"] ; then
                     set +e
                     name=$(cat name)
                     set −e
                     if [ $(cat name) = nunchuk ] ; then
                            event="/dev/input/$ (ls−devent[0−9]∗)"
                            echo $event
                            exit 0             # Found it
                     fi
              fi
      done

      echo "Nunchuk uinput device not found." >&2
      exit 1
)

# End findchuk

Listing 23-1The findchuk shell script 

测试。/双节棍

当您想要查看正在接收的双节棍数据时,您可以添加-d命令行选项:

$ ./nunchuk −d
Raw nunchuk data: [83] [83] [5C] [89] [A2] [63]
.stick_x = 0083 (131)
.stick_y = 0083 (131)
.accel_x = 0170 (368)
.accel_y = 0226 (550)
.accel_z = 0289 (649)
.z_button= 0
.c_button= 0

第一行报告接收到的数据的原始字节。其余的行以解码后的形式报告数据。当原始数据报告按钮按下为低电平有效时,Z 和 C 按钮在解码数据中报告为 1。左列中的值是十六进制格式,而括号中的值是十进制格式。

公用事业输入

当双截棍程序运行时,您应该能够在列表中看到双截棍uinput设备:

$ lsinput
/dev/input/event0
   bustype : BUS_USB
   vendor  : 0x1
   product : 0x1
   version : 1
   name    : "nunchuk"
   bits ev : EV_SYN EV_KEY EV_REL

在这个例子中,双截棍显示为event0

公用事业输入-事件

在开发与uinput相关的代码时,input-events实用程序是一个很大的帮助。这里我们为event0运行它(命令行上的参数 0),其中双节棍鼠标设备是:

$ input-events 0
/dev/input/event0
   bustype   : BUS_USB
   vendor    : 0x1
   product   : 0x1
   version   : 1
   name      : "nunchuk"
   bits ev   : EV_SYN EV_KEY EV_REL

waiting for events
23:35:15.345105: EV_KEY BTN_LEFT (0x110) pressed
23:35:15.345190: EV_SYN code=0 value=0
23:35:15.517611: EV_KEY BTN_LEFT (0x110) released
23:35:15.517713: EV_SYN code=0 value=0
23:35:15.833640: EV_KEY BTN_RIGHT (0x111) pressed
23:35:15.833727: EV_SYN code=0 value=0
23:35:16.019363: EV_KEY BTN_RIGHT (0x111) released
23:35:16.019383: EV_SYN code=0 value=0
23:35:16.564129: EV_REL REL_X −1
23:35:16.564213: EV_REL REL_Y 1
23:35:16.564261: EV_SYN code=0 value=0
...

该计划

I 2 C 编程的一些有趣的方面作为 I 2 C 设备编程的例子在 C 中给出。项目目录是:

$ cd ~/RPi/nunchuk

将要呈现的源模块被命名为nunchuck.c

要访问 I2C 巴士,我们必须首先打开它,如清单 23-2 所示。第 44 行打开了总线,由/dev/i2s-1 标识。如果 I 2 C 总线没有在 Raspberry Pi 控制面板中启用,这将会失败。

0039: static void
0040: i2c_init(const char *node) {
0041:  unsigned long i2c_funcs = 0;    /* Support flags */
0042:  int rc;
0043:
0044:  i2c_fd = open(node,O_RDWR);   /* Open driver /dev/i2s-1 */
0045:  if ( i2c_fd < 0 ) {
0046:        perror("Opening /dev/i2s-1");
0047:        puts("Check that I2C has been enabled in the "
                  "control panel\n");
0048:        abort();
0049:  }
0050:
0051:  /*
0052:   * Make sure the driver supports plain I2C I/O:
0053:   */
0054:  rc = ioctl(i2c_fd,I2C_FUNCS,&i2c_funcs);
0055:  assert(rc >= 0);
0056:  assert(i2c_funcs & I2C_FUNC_I2C);
0057: }

Listing 23-2Opening the I2C bus in nunchuk.c

一旦总线被成功打开,线路 54 中的ioctl(2)调用返回可用的功能支持。第 56 行中的断言宏测试正常的 I 2 C 函数使用宏I2C_FUNC_I2C是可用的。如果在应用宏后没有位保持为真,断言将中止程序。

函数 nunchuk_init()用于初始化 nunchuk 并破解它的加密(清单 23-3 )。

0062: static void
0063: nunchuk_init(void) {
0064:  static char init_msg1[] = { 0xF0, 0x55 };
0065:  static char init_msg2[] = { 0xFB, 0x00 };
0066:  struct i2c_rdwr_ioctl_data msgset;
0067:  struct i2c_msg iomsgs[1];
0068:  int rc;
0069:
0070:  iomsgs[0].addr = 0x52;          /* Address of Nunchuk */
0071:  iomsgs[0].flags = 0;            /* Write */
0072:  iomsgs[0].buf = init_msg1;      /* Nunchuk 2 byte sequence */
0073:  iomsgs[0].len = 2;              /* 2 bytes */
0074:
0075:  msgset.msgs = iomsgs;
0076:  msgset.nmsgs = 1;
0077:
0078:  rc = ioctl(i2c_fd,I2C_RDWR,&msgset);
0079:  assert(rc == 1);
0080:
0081:  timed_wait(0,200,0);            /* Nunchuk needs time */
0082:
0083:  iomsgs[0].addr = 0x52;          /* Address of Nunchuk */
0084:  iomsgs[0].flags = 0;            /* Write */
0085:  iomsgs[0].buf = init_msg2;      /* Nunchuk 2 byte sequence */
0086:  iomsgs[0].len = 2;              /* 2 bytes */
0087:
0088:  msgset.msgs = iomsgs;
0089:  msgset.nmsgs = 1;
0090:
0091:  rc = ioctl(i2c_fd,I2C_RDWR,&msgset);
0092:  assert(rc == 1);
0093: }

Listing 23-3Initializing the Nunchuk in nunchuk.c

第 70 到 76 行初始化这些结构,将初始消息发送给 nunchuk。线 78 和 79 将该信息传递给设备。第 81 行为双节棍控制器提供了必要的暂停。第 83 到 92 行发送后续消息来破解加密。

最后给出的代码清单是用于读取双截棍的代码(清单 23-4 )。行 106 是一个延迟,以防止多次读取使 I 2 C 设备不堪重负。

0098: static int
0099: nunchuk_read(nunchuk_t *data) {
0100:   struct i2c_rdwr_ioctl_data msgset;
0101:   struct i2c_msg iomsgs[1];
0102:   char zero[1] = { 0x00 };        /* Written byte */
0103:   unsigned t;
0104:   int rc;
0105:
0106:   timed_wait(0,15000,0);
0107:
0108:   /*
0109:    * Write the nunchuk register address of 0x00 :
0110:    */
0111:   iomsgs[0].addr = 0x52;          /* Nunchuk address */
0112:   iomsgs[0].flags = 0;            /* Write */
0113:   iomsgs[0].buf = zero;           /* Sending buf */
0114:   iomsgs[0].len = 1;              /* 6 bytes */
0115:
0116:   msgset.msgs = iomsgs;
0117:   msgset.nmsgs = 1;
0118:
0119:   rc = ioctl(i2c_fd,I2C_RDWR,&msgset);
0120:   if ( rc < 0 )
0121:           return -1;              /* I/O error */
0122:
0123:   timed_wait(0,200,0);            /* Zzzz, nunchuk needs time */
0124:
0125:   /*
0126:    * Read 6 bytes starting at 0x00 :
0127:    */
0128:   iomsgs[0].addr = 0x52;                  /* Nunchuk address */
0129:   iomsgs[0].flags = I2C_M_RD;             /* Read */
0130:   iomsgs[0].buf = (char *)data->raw;      /* Receive raw bytes here */
0131:   iomsgs[0].len = 6;                      /* 6 bytes */
0132:
0133:   msgset.msgs = iomsgs; 

0134:   msgset.nmsgs = 1;
0135:
0136:   rc = ioctl(i2c_fd,I2C_RDWR,&msgset);
0137:   if ( rc < 0 )
0138:           return -1;                      /* Failed */
0139:
0140:   data->stick_x = data->raw[0];
0141:   data->stick_y = data->raw[1];
0142:   data->accel_x = data->raw[2] << 2;
0143:   data->accel_y = data->raw[3] << 2;
0144:   data->accel_z = data->raw[4] << 2;
0145:
0146:   t = data->raw[5];
0147:   data->z_button = t & 1 ? 0 : 1;
0148:   data->c_button = t & 2 ? 0 : 1;
0149:   t >>= 2;
0150:   data->accel_x |= t & 3;
0151:   t >>= 2;
0152:   data->accel_y |= t & 3;
0153:   t >>= 2;
0154:   data->accel_z |= t & 3;
0155:   return 0;
0156: }

Listing 23-4The nunchuk_read() function in nunchuk.c

第 111 到 117 行准备了一条消息,告诉 nunchuk 我们想从寄存器 0 开始读取 6 个字节。第 199 行的ioctl(2)调用启动它。第 123 行再次将时间给了古怪的双节棍控制器。之后,发出六个字节的读命令是安全的(第 128 到 136 行)。

剩余的 140 到 155 行从返回的 nunchuk 寄存器信息中提取相关信息。

摘要

本章介绍了向 Raspberry Pi 的图形桌面添加设备的 uinput 机制。此外,它还提供了一个 I2C C C 程序的工作示例。有了提供的编程和输入实用程序,您就可以构建自己创建的定制界面了。*

二十四、LCD HDMI 显示器

一些 Pi 项目最好使用简单的 LCD 显示器。相关的触摸控制将您从键盘和鼠标中解放出来。本章将研究一个 5 英寸 800x480 像素 LCD 触摸屏的例子,并描述如何设置它。

该显示单元

特色显示单元被宣传为“ 5 英寸 800 x 480 高清 TFT LCD 触摸屏,用于 Raspberry PI 2 型号 B / B+ / A+ / B ”,售价约为 42 美元。它具有一个 4 线电阻式 XPT2046 触摸控制器。图 24-1 展示了工具包中的内容。

img/326071_2_En_24_Fig1_HTML.jpg

图 24-1

5 英寸 HDMI 显示器套件,配有触控笔、DVD 和 LCD 单元(仍带有塑料 Shell)。不含 USB 电源线。

LCD 的背面显示了一个 HDMI 连接器(图 24-2 ,底部中间)、一个电源 USB 连接器(HDMI 连接器的右侧)、一个背光开关(右上)、一个 13x2 连接器(中上)和一个 LVDS 连接器(左下)。

img/326071_2_En_24_Fig2_HTML.jpg

图 24-2

拧入四个支架的 5 英寸 HDMI 显示器背面

如果您阅读了附带的 DVD 说明,您应该运行名为LCD5-show的脚本,但不要运行它——它与 Raspbian Linux 的当前版本不一致。事实上,该脚本在运行后可能会使您的 Pi 无法启动。本章将使用该脚本作为指南,并对其进行修改。

装置

首先要做的是从 DVD 上复制软件或者用 git 获取它。下面的git命令获取软件并将其放入~pi/LCD 目录:

$ git clone https://github.com/goodtft/LCD-show.git ./LCD
$ cd ./LCD

安装脚本

清单 24-1 显示了提供的安装脚本,它将作为我们的指南,但不能直接使用。这可能会导致您的 Pi 无法启动。清单中带下划线的行突出了一些问题区域。让我们在接下来的几节中手动完成更正的步骤。

0001: #!/bin/bash
0002:  sudo rm -rf /etc/X11/xorg.conf.d/40-libinput.conf
0003:  sudo cp -rf ./boot/config-5.txt /boot/config.txt
0004: if [ -b /dev/mmcblk0p7 ]; then
0005: sudo cp ./usr/cmdline.txt-noobs /boot/cmdline.txt
0006: else
0007: sudo cp ./usr/cmdline.txt /boot/
0008: fi
0009: sudo cp ./usr/inittab /etc/
0010: sudo cp -rf ./usr/99-fbturbo.conf-HDMI /usr/share/X11/xorg.conf.d/99-fbturbo.conf
0011: sudo mkdir /etc/X11/xorg.conf.d
0012: sudo cp -rf ./usr/99-calibration.conf-5 /etc/X11/xorg.conf.d/99-calibration.conf
0013: nodeplatform=`uname -n`
0014: kernel=`uname -r`
0015: version=`uname -v`
0016: if test "$nodeplatform" = "raspberrypi";then
0017: echo "this is raspberrypi kernel"
0018: version=${version%% *}
0019: version=${version#*#}
0020: echo $version
0021: if test $version -lt 970;then
0022: echo "reboot"
0023: else
0024: echo "need to update touch configuration"
0025: if test $version -ge 1023;then
0026: echo "install xserver-xorg-input-evdev_2.10.5-1"
0027: sudo dpkg -i -B xserver-xorg-input-evdev_2.10.5-1_armhf.deb
0028: else
0029: echo "install xserver-xorg-input-evdev_1%3a2.10.3-1"
0030: sudo dpkg -i -B xserver-xorg-input-evdev_1%3a2.10.3-1_armhf.deb
0031: fi
0032: sudo cp -rf /usr/share/X11/xorg.conf.d/10-evdev.conf \
                  /usr/share/X11/xorg.conf.d/45-evdev.conf
0033: echo "reboot"
0034: fi
0035: else
0036: echo "this is not raspberrypi kernel, no need to update touch configure, reboot"
0037: fi
0038: sudo reboot

Listing 24-1Provided LCD5-show install script (do not run!)

支持

在对系统进行任何更改之前,请备份几个关键文件,以防以后需要恢复配置。将以下两个文件复制到您的主目录或您自己选择的位置:

# cp /boot/config.txt ~pi/config.txt.bak
# cp /boot/cmdline.txt ~pi/cmdline.txt.bak

要恢复您的原始配置,您只需要复制这些并重新启动。

文件 40-libinput.conf

清单 24-1 第 2 行中的安装脚本试图删除一个不存在的文件(Raspbian Linux 没有提供/etc/X11/xorg.conf.d目录)。但是/usr/share/X11/xord.conf.d/40-libinput.conf里有一个文件。你可能会发现你可以把它留在那里,但我建议你重命名它,以避免任何可能的冲突。如果您选择将系统恢复到原始状态,您需要稍后撤销此操作。

要禁用它而不删除它,只需用一个不同的后缀重命名它(如.was)。

# mv /usr/share/X11/xorg.conf.d/40-libinput.conf \
     /usr/share/X11/xorg.conf.d/40-libinput.conf.was

如果您还没有进入 LCD 软件目录,现在是转到该目录的好时机:

# cd ~pi/LCD

编辑/引导/配置.txt

如果您运行清单 24-1 中的安装脚本,第 3 行将会清除您之前对/boot/config.txt文件所做的任何更改。更糟糕的是,旧的安装文件可能不完全适用于当前版本的 Raspbian。您最好编辑文件,做出您实际需要的更改。

# nano /boot/config.txt

如果你认为你搞砸了,你可以通过复制备份文件来恢复,然后重新开始。

进行更改时,有时可以取消对某行的注释。在其他情况下,您需要添加行(最好在文件的末尾)。如果您有一个指令冲突,您可以在第一列中用一个散列字符(#)将其注释掉,并简单地在文件末尾添加您的更改。

进行以下更改:

dtparam=spi=on
max_usb_current=1
hdmi_force_hotplug=1
config_hdmi_boost=7
hdmi_group=2
hdmi_mode=1
hdmi_mode=87
hdmi_drive=1
hdmi_cvt 800 480 60 6 0 0 0
dtoverlay=ads7846,cs=1,penirq=25,penirq_pull=2,speed=50000,ke

保存您的更改。这一步替换了安装脚本的第 3 行。

编辑/boot/cmdline.txt

接下来执行的非 noobs 步骤是清单 24-1 的第 7 行。像上一步一样,它用潜在的不兼容选项破坏文件(它可能影响到根设备的路径)。更好的方法是简单地编辑文件,进行必要的修改。在行尾添加以下文本,用空格分隔任何以前的文本(对于非 noob 和非 noob):

fbcon=map:10 fbcon=font:ProFont6x11

保存更改。

99-fbturbo.conf 文件

清单 24-1 安装脚本的第 9 行不再适用。不管是好是坏,Raspbian Linux 和 Debian 一样使用systemd。不再有/etc/inittab文件,因此可以跳过这一步。

另一方面,第 10 行指示我们复制以下内容:

# cp -rf ./usr/99-fbturbo.conf-HDMI /usr/share/X11/xorg.conf.d/99-fbturbo.conf

文件 99-calibration.conf-5

脚本第 11 行不适用(不搜索目录/etc/X11/xorg.conf.d),应该跳过。

执行以下拷贝,注意第二个路径名中的更改(带下划线):

# cp -rf ./usr/99-calibration.conf-5 /usr/share/X11/xorg.conf.d/99-calibration.conf

脚本使用了目标目录/etc/X11,但在上面的命令中,正确的目录名是/usr/share/X11

驱动程序安装

安装脚本尝试确定:

  1. 这是一个树莓派(第 16-17 行),以及

  2. Raspbian Linux 的版本(第 18–19 行)。

测试#1 依赖于您的 Pi 的主机名“raspberrypi”如果您已经定制了主机名并运行了那个脚本,它就会安装错误的驱动程序,认为它是而不是Pi。哎呀!

在脚本的第 21 行和第 25 行中,对 Raspbian Linux 版本进行了一些测试。这些是基于以下结果:

# uname -v
#1110 SMP Mon Apr 16 15:18:51 BST 2018

本例中的版本号是 1110。第 21 行表示如果您的 Raspbian 版本低于 970 ,则不需要安装驱动程序。在这种情况下,是时候简单地重启了(改为执行关机和断电)。另一方面,Raspbian 的最新版本版本 1023 或更高版本要求安装驱动程序。**

在您的~pi/LCD目录中,应该存在您想要安装的 debian 驱动程序包。要查看您正在安装的内容,请使用-c 查询软件包文件,文件名如下所示:

# dpkg -c xserver-xorg-input-evdev_2.10.5-1_armhf.deb
drwxr-xr-x root/root         0 2017-01-18 18:26 ./
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/lib/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/lib/xorg/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/lib/xorg/modules/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/lib/xorg/modules/input/
-rw-r--r-- root/root     39292 2017-01-18 18:26 ./usr/lib/xorg/modules/input/evdev_drv.so
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/X11/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/X11/xorg.conf.d/
-rw-r--r-- root/root      1099 2017-01-18 18:26 ./usr/share/X11/xorg.conf.d/10-evdev.conf
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/bug/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/bug/xserver-xorg-input-evdev/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/doc/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/doc/xserver-xorg-input-evdev/
-rw-r--r-- root/root      6293 2017-01-18 18:26 ./usr/share/doc/\
                               xserver-xorg-input-evdev/changelog.Debian.gz
-rw-r--r-- root/root     83217 2017-01-18 07:15 ./usr/share/doc/\
                               xserver-xorg-input-evdev/changelog.gz
-rw-r--r-- root/root      4988 2017-01-18 18:26 ./usr/share/doc/\
                               xserver-xorg-input-evdev/copyright
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/man/
drwxr-xr-x root/root         0 2017-01-18 18:26 ./usr/share/man/man4/
-rw-r--r-- root/root      4306 2017-01-18 18:26 ./usr/share/man/man4/evdev.4.gz
lrwxrwxrwx root/root         0 2017-01-18 18:26 ./usr/share/bug/xserver-xorg-\
                               input-evdev/script -> ../xserver-xorg-core/script

使用以下命令安装 debian 包:

# dpkg -i -B xserver-xorg-input-evdev_2.10.5-1_armhf.deb

安装完成后,从脚本的第 32 行执行最后一个复制命令:

# cp -rf /usr/share/X11/xorg.conf.d/10-evdev.conf /usr/share/X11/xorg.conf.d/45-evdev.conf

关机

现在是时候用关闭的 Pi 连接硬件了。执行系统关机:

# sync
# /sbin/shutdown -h now

关机过程通常会为您执行sync命令,但我喜欢知道它已经完成的舒适感觉(它会刷新您未写入的磁盘缓存)。

插入电源

此时,软件和配置已经准备就绪。关闭电源,将 LCD 单元插入 Pi,13x2 连接器与 Pi 的 GPIO 条的一端连接,如图 24-3 所示。该图是从 GPIO 侧看的,没有完全就位,因此您可以看到正在插入的针脚。

img/326071_2_En_24_Fig3_HTML.jpg

图 24-3

5 英寸 LCD 13x2 连接器连接到 GPIO 条,位于 USB 连接器(Pi 3 B+)的另一端。连接器尚未完全就位。

这很容易出错,所以要反复检查。这里的一个错误可能会毁了你的一天。如果匹配正确,HDMI 连接器应该在相反的一侧排成一行,如图 24-4 所示。

img/326071_2_En_24_Fig4_HTML.jpg

图 24-4

使用随附的 HDMI 转 HDMI 侧适配器连接到 Pi 的 LCD

如果 HDMI 适配器似乎没有对齐,请重新检查 13x2 连接器是否正确插入 GPIO 条。

插入电源线后,打开设备电源。背光启动时,液晶屏应显示短暂的闪烁。如果您看不到任何活动,最好立即关闭电源并重新检查连接。

启动

Pi 启动后,观察桌面是否出现。一旦你的桌面出现并显示一个小的鼠标箭头,试着点击屏幕来移动箭头。如果触摸控制不起作用,请检查以下内容:

  • 重新检查连接的配合情况。连接不良或连接错位都会影响触控。

  • 再次检查/boot/config.txt/boot/cmdline.txt文件。

  • 检查用电情况(下一节)。

  • 检查 LCD 面板的版本。较旧的设备对 penirq 使用 GPIO 22。这在下面带下划线的/boot/config.txt文件中指定:

    dtoverlay=ads7846,cs=1,penirq=25,penirq_pull=2,speed=50000,ke
    
    

电源

我拥有的这个单元在背光开启时消耗大约 242 毫安,关闭时消耗 168 毫安。确保您的 Pi 和 LCD 面板供应充足。Pi 3 B+建议使用 2.5 A 电源,但当 LCD 插入 USB 端口时,您可能会在高峰使用期间耗尽当前容量。

连接

表 24-1 中描述了 13x2 连接器上的连接。手动连接触摸控制时,注意不要混淆连接“MO”和“MI”从从机的角度来看,这些奇怪的连接分别表示 MISO (MO)和 MOSI (MI)。

表 24-1

13x2 LCD 连接器的连接

|

描述

|

别针

|

|

|

别针

|

描述

|
| --- | --- | --- | --- | --- | --- |
| 电源(+5V) | +5V | Two | one | +3.3V |   |
| 电源(+5V) | +5V | four | three |   |   |
| 地面 | 接地 | six | five |   |   |
| 网络计算机 |   | eight | seven |   |   |
| 网络计算机 |   | Ten | nine | 接地 | 地面 |
| 网络计算机 |   | Twelve | Eleven |   |   |
| 接地 | 接地 | Fourteen | Thirteen |   |   |
| 网络计算机 |   | Sixteen | Fifteen |   |   |
| 网络计算机 |   | Eighteen | Seventeen | +3.3V |   |
| 接地 | 接地 | Twenty | Nineteen | 大调音阶的第三音 | 工业博物馆 |
| 笔 IRQ | GPIO 25 | Twenty-two | Twenty-one | 军医 | 军事情报部门组织(Military Intelligence Service Organization) |
| 网络计算机 |   | Twenty-four | Twenty-three | 血清肌酸激酶 | 血清肌酸激酶 |
| 笔芯片选择 | GPIO-7 | Twenty-six | Twenty-five | 接地 | 地面 |

摘要

经常出现的一个问题是,当软件变得陈旧时,硬件会被定价出售。当操作系统更新而软件保持静态时,就会发生这种情况。如果你能像本章所做的那样,手动完成安装的细节,这会对你有利。

所展示的 LCD 屏幕让您对类似产品有所了解。需要注意的一点是,在购买之前,要确保您的 Linux 版本有任何必要的驱动程序支持。

触摸感应液晶显示屏为您的想象力开启了许多新的可能性。

二十五、实时时钟

为 Arduino 出售的 DS3231 模块非常适合 Raspberry Pi,因为该 IC 的工作电压范围为+2.3 至 5.5 V。这允许 Pi 用户从 Pi 的+3.3 V 电源供电,并将其连接到 I 2 C 总线。该模块带有备用电池,允许它在 Pi 断电时保持准确的时间。DS3231 包括温度测量和调节,以保持计时精度。

本章将使用 C 程序来设置和读取日期/时间。此外,如果有应用,可以从 GPIO 端口检测到 1 Hz 输出。DS3231 RTC(实时时钟)还提供相当精确的温度读数。

DS3231 概述

模块的正面照片如图 25-1 所示。该模块装配有直角引脚,可以很好地插入试验板。我的也是从易贝运来的,装有电池,但不要指望这个。通常有电池运输限制。您可能需要单独购买电池。

img/326071_2_En_25_Fig1_HTML.jpg

图 25-1

插入试验板的 DS3231 模块的前视图

从连接标签列表来看,很明显这是一个 I 2 C 设备。除了电源和 I 2 C 连接,还有一个标为 SQW 的输出,可以配置为产生 1 赫兹的脉冲。在本章中,我建议您将其连接到 GPIO 22 进行演示。图 25-2 显示了 pcb 的背面。

img/326071_2_En_25_Fig2_HTML.jpg

图 25-2

安装了电池的 DS3231 的背面视图

小费

建议提前购买新电池,因为电池通常在到达时已经耗尽,或者根据运输规定不包括在内。

连接

DS3231 模块还包括一个可以使用的 AT24C32 4kx8 I 2 C EEPROM,但这将留给读者作为练习。影响 RTC 芯片的接线图如图 25-3 所示。SQW 输出的连接是可选的。它可以用来以精确的时间间隔获得 1 赫兹的脉冲。

img/326071_2_En_25_Fig3_HTML.jpg

图 25-3

将 DS3231 连接到树莓派 I2C 总线

模块在+3.3 V 下运行,因此很容易接线,因为 Pi 和模块共享同一电源。只需将 SDA 和 SCL 连接和电源连接到设备。不要忘记接地。

当模块连接到 Pi 的 I 2 C 总线时,您应该能够检测到它。

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- 57 -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

显示的数字是发现的设备的十六进制地址。0x68 器件是 DS3231,而 0x57 是 AT24C32 EEPROM 器件。如果您的设备未被发现,请关闭您的 Pi 并重新检查您的线路。

注意

DS3231 RTC 使用 I2C 地址 0x68。

寄存器映射

DS3231 与以前的 5 V DS1307 芯片兼容,但在其他特性中增加了两个报警。清单 25-1 中说明了 DS3231 寄存器在 C 语言中的声明。寄存器布局的每个部分都被描述为一个子结构。例如,struct s_00是寄存器在字节偏移量 0x00 处的布局。

0023: struct s_ds3231_regs {
0024:   struct s_00 {                  /* Seconds */
0025:           uint8_t secs_1s  : 4;   /* Ones digit: seconds */
0026:           uint8_t secs_10s : 3;   /* Tens digit: seconds */
0027:           uint8_t mbz_0    : 1;
0028:   } s00;
0029:   struct s_01 {                  /* Minutes */
0030:           uint8_t mins_1s  : 4;   /* Ones digit: minutes */
0031:           uint8_t mins_10s : 3;   /* Tens digit: minutes */
0032:           uint8_t mbz_1    : 1;
0033:   } s01;
0034:   union u_02 {                   /* Hours */
0035:           struct  {
0036:                   uint8_t hour_1s  : 4;   /* Ones digit: hours */
0037:                   uint8_t hour_10s : 1;   /* Tens digit: hours (24hr mode) */
0038:                   uint8_t ampm     : 1;   /* AM=0/PM=1 */
0039:                   uint8_t mode_1224: 1;   /* Mode bit: 12=1/24=0 hour format */
0040:           } hr12;
0041:           struct  {
0042:                   uint8_t hour_1s  : 4;   /* Ones digit: hours */
0043:                   uint8_t hour_10s : 3;   /* Tens digit: hours (24hr mode) */
0044:                   uint8_t mode_1224: 1;   /* Mode bit: 12=1/24=0 hour format */
0045:           } hr24;
0046:   } u02;
0047:   struct s_03 {                   /* Weekday */
0048:           uint8_t wkday    : 3;   /* Day of week (1-7) */
0049:           uint8_t mbz_2    : 5;
0050:   } s03;
0051:   struct s_04 {                   /* Day of month */

0052:           uint8_t day_1s   : 4;   /* Ones digit: day of month (1-31) */
0053:           uint8_t day_10s  : 2;   /* Tens digit: day of month */
0054:           uint8_t mbz_3    : 2;
0055:   } s04;
0056:   struct s_05 {                   /* Month */
0057:           uint8_t month_1s : 4;   /* Ones digit: month (1-12) */
0058:           uint8_t month_10s: 1;   /* Tens digit: month */
0059:           uint8_t mbz_4    : 2;
0060:           uint8_t century  : 1;   /* Century */
0061:   } s05;
0062:   struct s_06 {                   /* Year */
0063:           uint8_t year_1s  : 4;   /* Ones digit: BCD year */
0064:           uint8_t year_10s : 4;   /* Tens digit: BCD year */
0065:   } s06;
0066:   struct s_07 {                   /* Alarm Seconds */
0067:           uint8_t alrms01  : 4;   /* Alarm BCD 1s seconds */
0068:           uint8_t alrms10  : 3;   /* Alarm BCD 10s Seconds */
0069:           uint8_t AxM1     : 1;   /* Alarm Mask 1 */
0070:   } s07;                          /* Alarm Seconds */
0071:   struct s_08 {                   /* Alarm Minutes */
0072:           uint8_t alrmm01  : 4;   /* Alarm BCD 1s Minutes */
0073:           uint8_t alrmm10  : 3;   /* Alarm BCD 10s Minutes */
0074:           uint8_t AxM2     : 1;   /* Alarm Mask 2 */
0075:   } s08;                          /* Alarm Minutes */
0076:   union u_09 {                    /* Alarm Hours */
0077:           struct  {
0078:                   uint8_t alr_hr10 : 1;   /* Alarm 10s Hours */
0079:                   uint8_t alr_ampm : 1;   /* Alarm am=0/pm=1 */
0080:                   uint8_t alr_1224 : 1;   /* Alarm 12=1 */
0081:                   uint8_t AxM3     : 1;   /* Alarm Mask 3 */
0082:           } ampm;

0083:           struct  {
0084:                   uint8_t alr_hr10 : 2;   /* Alarm 10s Hours */
0085:                   uint8_t alr_1224 : 1;   /* Alarm 24=0 */
0086:                   uint8_t AxM3     : 1;   /* Alarm Mask 3 */
0087:           } hrs24;
0088:   } u09;                          /* Alarm 1 Hours */
0089:   union u_0A {                    /* Alarm Date */
0090:           struct  {
0091:                   uint8_t day1s    : 4;   /* Alarm 1s date */
0092:                   uint8_t day10s   : 2;   /* 10s date */
0093:                   uint8_t dydt     : 1;   /* Alarm dy=1 */
0094:                   uint8_t AxM4     : 1;   /* Alarm Mask 4 */
0095:           } dy;
0096:           struct  {
0097:                   uint8_t day1s    : 4;   /* Alarm 1s date */
0098:                   uint8_t day10    : 2;   /* Alarm 10s date */
0099:                   uint8_t dydt     : 1;   /* Alarm dt=0 */
0100:                   uint8_t AxM4     : 1;   /* Alarm Mask 4 */
0101:           } dt;
0102:   } u0A;
0103:   struct s_08 s0B;                /* Alarm 2 Minutes */
0104:   union u_09 u0C;                 /* Alarm 2 Hours */
0105:   union u_0A u0D;                 /* Alarm 2 Date */
0106:   struct s_0E {                   /* Control */
0107:           uint8_t A1IE     : 1;   /* Alarm 1 Int enable */
0108:           uint8_t A2IE     : 1;   /* Alarm 2 Int enable */
0109:           uint8_t INTCN    : 1;   /* SQW signal when 1 */
0110:           uint8_t RS1      : 1;   /* Rate select 1 */
0111:           uint8_t RS2      : 1;   /* Rate select 2 */
0112:           uint8_t CONV     : 1;   /* Temp conversion */
0113:           uint8_t BBSQW    : 1;   /* Enable square wave */
0114:           uint8_t NEOSC    : 1;   /* /EOSC: Enable */
0115:   } s0E;

0116:   struct s_0F {                   /* Control/status */
0117:           uint8_t A1F      : 1;   /* Alarm 1 Flag */
0118:           uint8_t A2F      : 1;   /* Alarm 2 Flag */
0119:           uint8_t bsy      : 1;   /* Busy flag */
0120:           uint8_t en32khz  : 1;   /* Enable 32kHz out */
0121:           uint8_t zeros    : 3;
0122:           uint8_t OSF      : 1;   /* Stop Osc when 1 */
0123:   } s0F;
0124:   struct s_10 {                   /* Aging offset */
0125:           int8_t data      : 8;   /* Data */
0126:   } s10;
0127:   struct s_11 {
0128:           int8_t temp      : 8;   /* Signed int temp */
0129:   } s11;
0130:   struct s_12 {
0131:           uint8_t mbz      : 6;
0132:           uint8_t frac     : 2;   /* Fractional temp bits */
0133:   } s12;
0134: } __attribute__((packed));

Listing 25-1The full C language register map for the DS3231

寄存器 0x00(秒)

0x00 处的寄存器由两个位域组成:s00.secs_1s 和 s00.secs_10s,重复如下:

0024:   struct s_00 {                   /* Seconds */
0025:           uint8_t secs_1s  : 4;   /* Ones digit: seconds */
0026:           uint8_t secs_10s : 3;   /* Tens digit: seconds */
0027:           uint8_t mbz_0    : 1;
0028:   } s00;

对于阅读本文的学生来说,有必要解释一下 C 语言的位域。冒号和后面的数字指定了字段的位宽。被分割的字段由类型决定,在这种情况下是一个无符号字节(uint8_t)。首先列出的字段指定最低有效位(在 Pi 上),随后的位字段代表较高编号的位。例如,字段s00.secs_1s定义了位 3-0(最右边),而s00.secs_10s定义了位 6-4,并且s00.mbz_0声明了位 7-6(最左边的两位)。指定位字段使我们不必使用按位和移位操作来移入和移出值。

成员secs_1ssecs_10s代表 BCD(二进制编码十进制)数字,表示以秒为单位的时间。因此,值 0x23(在 uint8_t 中)字节表示十进制值 23。当 DS3231 IC 计时时,RTC(实时时钟)会自动增加这些时间值和其他时间值。

寄存器 0x01(分钟)

分钟读数在字节偏移量 0x01 处提供,格式类似于秒部分。同样,成员mins_10smins_1s是分钟时间部分的 BCD 数字。

0029:   struct s_01 {                   /* Minutes */
0030:           uint8_t mins_1s  : 4;   /* Ones digit: minutes */
0031:           uint8_t mins_10s : 3;   /* Tens digit: minutes */
0032:           uint8_t mbz_1    : 1;
0033:   } s01;

名称类似于mbz_1的字段是“必须为零”字段,否则可以忽略。

寄存器 0x02(小时)

字节偏移量 0x02 处的小时部分更有趣一些,因为它可以存在于两个视图中。组件u03.hr12选择 12 小时制,而工会成员u02.hr24选择 24 小时制。使用的视图由成员mode_1224决定。当成员mode_1224是 1 位时,那么要使用的正确视图是u02.hr12,否则应该使用u02.hr24

0034:   union u_02 {                    /* Hours */
0035:           struct  {
0036:                   uint8_t hour_1s  : 4;   /* Ones digit: hours */
0037:                   uint8_t hour_10s : 1;   /* Tens digit: hours (24hr mode) */
0038:                   uint8_t ampm     : 1;   /* AM=0/PM=1 */
0039:                   uint8_t mode_1224: 1;   /* Mode bit: 12=1/24=0 hour format */
0040:           } hr12;
0041:           struct  {
0042:                   uint8_t hour_1s  : 4;   /* Ones digit: hours */
0043:                   uint8_t hour_10s : 3;   /* Tens digit: hours (24hr mode) */
0044:                   uint8_t mode_1224: 1;   /* Mode bit: 12=1/24=0 hour format */
0045:           } hr24;
0046:   } u02;

成员值hours_10shours_1s也是以十进制数字表示每小时时间的 BCD 值。在 24 小时制中,有一个额外的位来描述较大的每小时 10 位数字。

在 12 小时制中,当该位是 0 位时,u02.hr12.ampm的值代表 AM,否则代表 PM。

寄存器 0x03(工作日)

在字段s03.wkday中的偏移量 0x03 处找到工作日值。

0047:   struct s_03 {                   /* Weekday */
0048:           uint8_t wkday    : 3;   /* Day of week (1-7) */
0049:           uint8_t mbz_2    : 5;
0050:   } s03;

请注意,有效值的范围是从 1 到 7。Unix/Linux 使用 0–6 的值范围来表示工作日。

寄存器 0x04(一月中的某一天)

寄存器偏移量 0x04 包含一个月中的某一天。

0051:   struct s_04 {                   /* Day of month */
0052:           uint8_t day_1s   : 4;   /* Ones digit: day of month (1-31) */
0053:           uint8_t day_10s  : 2;   /* Tens digit: day of month */
0054:           uint8_t mbz_3    : 2;
0055:   } s04;

成员day_10sday_1s是一个月中某一天的 BCD 值,范围为 1 到 31。

寄存器 0x05(月)

寄存器偏移量 0x05 保存一年中的月份。

0056:   struct s_05 {                   /* Month */
0057:           uint8_t month_1s : 4;   /* Ones digit: month */
0058:           uint8_t month_10s: 1;   /* Tens digit: month */
0059:           uint8_t mbz_4    : 2;
0060:           uint8_t century  : 1;   /* Century */
0061:   } s05;

month_10smonths_1s是月份的 BCD 数字,范围是 1 到 12。提供成员century是为了表示从 1999 年到 2000 年的世纪交替。

寄存器 0x06(年)

年份由寄存器偏移量 0x06 提供。

0062:   struct s_06 {                   /* Year */
0063:           uint8_t year_1s  : 4;   /* Ones digit: BCD year */
0064:           uint8_t year_10s : 4;   /* Tens digit: BCD year */
0065:   } s06;

year_10syear_1s成员对提供年份的 BCD 数字。

寄存器 0x07(报警 1 秒)

DS3231 芯片支持两种报警。该寄存器提供报警 1 秒的值。

0066:   struct s_07 {                   /* Alarm Seconds */
0067:           uint8_t alrms01  : 4;   /* Alarm BCD 1s seconds */
0068:           uint8_t alrms10  : 3;   /* Alarm BCD 10s Seconds */
0069:           uint8_t AxM1     : 1;   /* Alarm Mask 1 */
0070:   } s07;

成员alrms01alrms10构成了报警 1 的 BCD 形式的一对秒数字。位域 AxM1 是一个位,决定报警的秒数必须匹配(AxM1=0),或者每秒触发一次报警(AxM1=1)。

寄存器 0x08(报警 1 分钟)

报警 1 的分钟数由偏移量 0x08 的寄存器指定。

0071:   struct s_08 {                   /* Alarm Minutes */
0072:           uint8_t alrmm01  : 4;   /* Alarm BCD 1s Minutes */
0073:           uint8_t alrmm10  : 3;   /* Alarm BCD 10s Minutes */
0074:           uint8_t AxM2     : 1;   /* Alarm Mask 2 */
0075:   } s08;

成员alrmm10alrmm01根据掩码位AxM2指定定义报警分钟的 BCD 对。

寄存器 0x09(报警 1 小时)

寄存器偏移量 0x09 保存报警的小时时间。

0076:   union u_09 {                    /* Alarm Hours */
0077:           struct  {
0078:                   uint8_t alr_hr10 : 1;   /* Alarm 10s Hours */
0079:                   uint8_t alr_ampm : 1;   /* Alarm am=0/pm=1 */
0080:                   uint8_t alr_1224 : 1;   /* Alarm 12=1 */
0081:                   uint8_t AxM3     : 1;   /* Alarm Mask 3 */
0082:           } ampm;
0083:           struct  {
0084:                   uint8_t alr_hr10 : 2;  /* Alarm 10s Hours */
0085:                   uint8_t alr_1224 : 1;   /* Alarm 24=0 */
0086:                   uint8_t AxM3     : 1;   /* Alarm Mask 3 */
0087:           } hrs24;

像前面描述的 union u_02一样,根据使用的是 12 小时制还是 24 小时制,有两种视图。根据屏蔽位AxM3应用小时。

寄存器 0x0A(报警 1 日期)

要应用的报警日期由 DS1332 寄存器文件中的偏移量 0x0A 给出。

0089:   union u_0A {                    /* Alarm Date */
0090:           struct  {
0091:                   uint8_t day1s    : 4;   /* Alarm 1s date */
0092:                   uint8_t day10s   : 2;   /* 10s date */
0093:                   uint8_t dydt     : 1;   /* Alarm dy=1 */
0094:                   uint8_t AxM4     : 1;   /* Alarm Mask 4 */
0095:           } dy;
0096:           struct  {
0097:                   uint8_t day1s    : 4;   /* Alarm 1s date */
0098:                   uint8_t day10    : 3;   /* Alarm 10s date */
0099:                   uint8_t dydt     : 1;   /* Alarm dt=0 */
0100:                   uint8_t AxM4     : 1;   /* Alarm Mask 4 */
0101:           } dt;
0102:   } u0A;

这是两个视图的联合,根据使用的是工作日(dydt=1)还是一个月中的某一天(dydt=0)。对day10sday1s是指定日期的 BCD 对。掩码 AxM4 决定了日期如何影响报警。

警报 2

报警 2 类似于报警 1,除了它缺少秒说明符。

0103:   struct s_08 s0B;                /* Alarm 2 Minutes */
0104:   union u_09 u0C;                 /* Alarm 2 Hours */
0105:   union u_0A u0D;                 /* Alarm 2 Date */

它在其他方面的用法是一样的。

寄存器 0x0E(控制)

寄存器偏移量 0x0E 提供了一些控制选项。

0106:   struct s_0E {                   /* Control */
0107:           uint8_t A1IE     : 1;   /* Alarm 1 Int enable */
0108:           uint8_t A2IE     : 1;   /* Alarm 2 Int enable */
0109:           uint8_t INTCN    : 1;   /* SQW signal when 1 */
0110:           uint8_t RS1      : 1;   /* Rate select 1 */
0111:           uint8_t RS2      : 1;   /* Rate select 2 */
0112:           uint8_t CONV     : 1;   /* Temp conversion */
0113:           uint8_t BBSQW    : 1;   /* Enable square wave */
0114:           uint8_t NEOSC    : 1;   /* /EOSC: Enable */
0115:   } s0E;

成员位A1IEA2IE设置为 1 位时启用报警中断。INTCN决定芯片发出中断信号(低电平有效)还是方波输出(INTCN =1)。选项BBSQW也必须设为 1 位,以启用方波输出。当位RS1RS2都设置为零时,为方波输出选择 1 Hz 的速率。CONV用于启用芯片温度的读取。最后,位NEOSC ( 不是 EOSC)在为 0 位时使能振荡器,否则在为真时停止振荡器。

读数温度

DS3231 能够保持精确的时间,部分原因是它能够监控自身的温度并进行补偿。可以通过执行以下操作读取温度:

  1. 检查BSY标志和CONV标志是否未设置。

  2. 设置CONV标志开始转换。

  3. CONV标志复位至零时,读取偏移量 0x11 和 0x12 处的寄存器值。

寄存器 0x0F(控制/状态)

寄存器偏移量 0x0F 提供更多控制和状态位。

0116:   struct s_0F {                   /* Control/status */
0117:           uint8_t A1F      : 1;   /* Alarm 1 Flag */
0118:           uint8_t A2F      : 1;   /* Alarm 2 Flag */
0119:           uint8_t bsy      : 1;   /* Busy flag */
0120:           uint8_t en32khz  : 1;   /* Enable 32kHz out */
0121:           uint8_t zeros    : 3;
0122:           uint8_t OSF      : 1;   /* Stop Osc when 1 */
0123:   } s0F;

标志A1FA2F指示相应的警报何时被触发。成员bsy是设备的忙碌标志。当与其他选项结合使用时,成员en32khz启用 32 kHz 信号输出。当标志OSF设置为 1 位时,振荡器停止工作。

寄存器 0x10(老化)

DS3231 内部使用的老化值,用于根据温度调整计时,可以从该寄存器中读取。它是一个有符号的 8 位数字。

0124:   struct s_10 {                   /* Aging offset */
0125:           int8_t data      : 8;   /* Data */
0126:   } s10;

寄存器 0x11 和 0x12(温度)

这对寄存器用于读取 DS3231 的内部温度,精确到四分之一摄氏度。

0127:   struct s_11 {
0128:           int8_t temp      : 8;   /* Signed int temp */
0129:   } s11;
0130:   struct s_12 {
0131:           uint8_t mbz      : 6;
0132:           uint8_t frac     : 2;   /* Fractional temp bits */
0133:   } s12;

s11.temp的值包含以摄氏度为单位的整数部分。s12.frac包含一对指定值为 0 到 3 的位。温度的形成可以由下式确定:

s11.temp + (float) s12.frac * 0.25;

从 DS3231 读取

目录中提供了程序ds3231.c的完整源代码:

$ cd ~/RPi/ds3231

构建或强制重新构建它,如下所示:

$ make clobber
rm -f *.o core errs.t
rm -f ds3231
$ make
gcc -c -Wall -O0 -g ds3231.c -o ds3231.o
gcc ds3231.o -o ds3231
sudo chown root ./ds3231
sudo chmod u+s ./ds3231

清单 25-2 中显示了从 DS3231 设备中执行读取的功能。

0136: static const char *node = "/dev/i2c-1";
0137: static int i2c_fd = -1;                  /* Device node: /dev/i2c-1 */
...
0175: static bool
0176: i2c_rd_rtc(ds3231_regs_t *rtc) {
0177:   struct i2c_rdwr_ioctl_data msgset;
0178:   struct i2c_msg iomsgs[2];
0179:   char zero = 0x00;               /* Register 0x00 */
0180:
0181:   iomsgs[0].addr = 0x68;          /* DS3231 */
0182:   iomsgs[0].flags = 0;            /* Write */
0183:   iomsgs[0].buf = &zero;          /* Register 0x00 */
0184:   iomsgs[0].len = 1;
0185:
0186:   iomsgs[1].addr = 0x68;          /* DS3231 */
0187:   iomsgs[1].flags = I2C_M_RD;     /* Read */
0188:   iomsgs[1].buf = (char *)rtc;
0189:   iomsgs[1].len = sizeof *rtc;
0190:
0191:   msgset.msgs = iomsgs;
0192:   msgset.nmsgs = 2;
0193:
0194:   return ioctl(i2c_fd,I2C_RDWR,&msgset) == 2;
0195: }

Listing 25-2The i2c_rd_rtc() function for the DS3231

该代码假设 I 2 C 总线已经打开,文件描述符保存在i2c_fd(第 137 行)。两个消息被组装到第 178 行的数组iomsgs中。第 181 行到第 184 行准备写入一个字节,表示我们从 DS3231 中的寄存器偏移量0x00开始寻址。第 186 行到第 189 行准备一条消息,将设备的所有寄存器读入缓冲结构rtc。当ioctl(2)返回2时,读取成功,表示两个消息被成功执行。

写入 DS3231

清单 25-3 列出了用于写入 DS3231 器件的函数。

0198: static bool
0199: i2c_wr_rtc(ds3231_regs_t *rtc) {
0200:   struct i2c_rdwr_ioctl_data msgset;
0201:   struct i2c_msg iomsgs[1];
0202:   char buf[sizeof *rtc + 1];      /* Work buffer */
0203:
0204:   buf[0] = 0x00;                  /* Register 0x00 */
0205:   memcpy(buf+1,rtc,sizeof *rtc);  /* Copy RTC info */
0206:
0207:   iomsgs[0].addr = 0x68;          /* DS3231 Address */
0208:   iomsgs[0].flags = 0;            /* Write */
0209:   iomsgs[0].buf = buf;            /* Register + data */
0210:   iomsgs[0].len = sizeof *rtc + 1; /* Total msg len */
0211:
0212:   msgset.msgs = &iomsgs[0];
0213:   msgset.nmsgs = 1;
0214:
0215:   return ioctl(i2c_fd,I2C_RDWR,&msgset) == 1;
0216: }

Listing 25-3The function i2c_wr_rtc() for writing to the DS3231

该功能类似于读取功能,只是只需要一条ioctl(2)消息。要写入的 RTC 值被复制到行 205 中的buf。第一个字节被设置为0x00,以指示我们想要开始写入的寄存器编号。在第 207 到 210 行中准备消息,并且在第 215 行中执行实际的写入。当ioctl(2)返回值 1 时,操作成功,表示执行了一条成功消息。

读数温度

对于那些想知道温度的人,清单 25-4 中提供了读取温度的函数。

0221: static float
0222: read_temp(void) {
0223:   ds3231_regs_t rtc;
0224:
0225:   do      {
0226:           if ( !i2c_rd_rtc(&rtc) ) {
0227:                   perror("Reading RTC for temp.");
0228:                   exit(2);
0229:           }
0230:   } while ( rtc.s0F.bsy || rtc.s0F.CONV ); /* Until not busy */
0231:
0232:   rtc.s0E.CONV = 1;               /* Start conversion */
0233:
0234:   if ( !i2c_wr_rtc(&rtc) ) {
0235:           perror("Writing RTC to read temp.");
0236:           exit(2);
0237:   }
0238:
0239:   do      {
0240:           if ( !i2c_rd_rtc(&rtc) ) {
0241:                   perror("Reading RTC for conversion.");
0242:                   exit(2);
0243:           }
0244:   } while ( rtc.s0E.CONV );       /* Until converted */
0245:
0246:   return rtc.s11.temp + (float)rtc.s12.frac * 0.25;
0247: }

Listing 25-4Reading the DS3231 temperature

s0F.bsys0F.CONV标志为真时,该函数首先循环,表示器件正忙。在第 232 行设置s0E.CONV标志,然后在第 234 行写到设备。此后,DS3231 被轮询以查看s0E.CONV标志何时归零。一次一个。CONV 复位后,我们可以安全地从s11s12寄存器中读取温度(第 246 行)。

演示时间

一旦根据图 25-3 编译并连接了演示,我们就可以练习 C 程序了。-h 选项报告使用信息:

$ ./ds3231 -h
Usage:  /ds3231 [-S time] [-f format] [-d] [-e] [-v] [-h]
where:
        -s      Set RTC clock based upon system date
        -f fmt  Set date format
        -e      Enable 1 Hz output on SQW
        -d      Disable 1 Hz output on SQW
        -t      Display temperature
        -S time Set DS3231 time from given
        -v      Verbose, show SQW register settings
        -h      This help

读取日期/时间

要读取仪器的当前日期/时间,只需运行程序:

$ ./ds3231
RTC time is 2018-08-05 00:54:55 (Sunday)

如果出现错误,再次运行i2c-detect并确保设备接线正确。

读取温度

要读取温度,请使用-t 选项:

$ ./ds3231 -t
RTC time is 2018-08-05 01:23:06 (Sunday)
Temperature is 26.75 C

温度读数的准确性相当不错,这一事实有助于其计时的稳定性。

设置 RTC

您可以使用-S 选项设置 RTC 时间:

$ ./ds3231 -S '2018-08-04 12:00:00'
Set RTC to 2018-08-04 12:00:00 (Saturday)
RTC time is 2018-08-04 12:00:00 (Saturday)

第二个打印行是从 RTC 设备读回的值。如果需要使用不同的日期/时间格式,请提供-f 选项。

1 赫兹方波

要测试 1 Hz SQW 输出,请使用-e (enable)命令。-v (verbose)选项只是通过显示一些值来确认:

$ ./ds3231 -ev
RTC time is 2018-08-05 01:24:34 (Sunday)
 BBSQW=1 INTCN=0 RS2=0 RS1=0

现在运行本书前面使用的 evinput 程序来监控 GPIO 22 引脚(假设您按照图 25-3 进行了接线)。如果您选择了其他 GPIO,请替换您使用的 GPIO:

$ ../evinput/evinput -g22
Monitoring for GPIO input changes:

GPIO 22 changed: 0
GPIO 22 changed: 1
GPIO 22 changed: 0
GPIO 22 changed: 1
GPIO 22 changed: 0
GPIO 22 changed: 1
GPIO 22 changed: 0

从输出中,您可以看到输入以大约半秒的速度变化(一个完整的周期需要一秒)。

内核支持

到目前为止,我们已经使用 C 程序对 DS3231 器件进行了测试。但是您可能已经知道 Raspbian Linux 支持 DS3231 的内核模块。在设置它之前,您需要禁用 ntp(至少暂时禁用):

# systemctl disable systemd-timesyncd.service

要在内核中配置 ds3231 模块支持,请执行以下操作:

  1. sudo -i

  2. 编辑/boot/config.txt并在文件末尾添加行dtoverlay=i2c-rtc,ds3231

  3. 对同一文件取消注释或添加“dtparam=i2c_arm=on”。

  4. 编辑文件/lib/udev/hwclock-set并注释掉下面三行,在前面加一个散列(#):

    #if [ -e /run/systemd/system ] ; then
    #    exit 0
    #fi
    
    
  5. 重新启动

重新启动后,尝试以下操作:

# hwclock -r
2018-08-04 08:13:18.719447-0400

您可以对照ds3231程序进行检查:

# ~pi/RPi/ds3231/ds3231
RTC time is 2018-08-04 12:14:23 (Saturday)

这里有四个小时的时差,但这是时区和夏令时的差异。当我的 Pi 被配置时,它保持 UTC 时间。要将 DS3231、内核模块和系统日期都放在同一个页面上,可以执行以下操作:

# ~pi/RPi/ds3231/ds3231 -S '2018-08-05 02:22:00'
Set RTC to 2018-08-05 02:22:00 (Sunday)
RTC time is 2018-08-05 02:22:00 (Sunday)
root@rpi3bplus:~# hwclock -s
root@rpi3bplus:~# date
Sat Aug  4 22:22:26 EDT 2018
root@rpi3bplus:~#

当涉及时区偏移时,这可能会令人困惑。在ds3231 -S命令中,我将日期/时间设置为所需的 UTC 日期/时间。然后,命令hwclock -s使其复位 DS3231 芯片的系统时间感。接下来,date命令用我的时区偏移量报告当地时间。

摘要

ds3231 是 Raspberry Pi 的完美伴侣,尤其是当它们没有联网时。凭借备用电池,ds3231 每月将精确时间保持在几秒之内。

所展示的程序让您深入了解芯片的操作,并练习从 C 与芯片通信。在 C 程序中,应用了位域语言特性的演示。这通常只在系统或设备编程中出现。

最后,演示了 ds3231 的 Raspbian 内核支持,以便您可以在未来的 Pi 项目中使用它。如果你正在寻找一个 I 2 C 项目,为什么不尝试与板载 EEPROM 通信呢?

二十六、Pi 照相机

有些应用需要摄像机,例如鸟瞰摄像机。给你的私家侦探一只眼睛既便宜又有趣。本章介绍 Pi 摄像机及其安装和使用。

连接

你通常会得到一个扁平带状电缆包括相机。大多数说明只会说“插入相机”作为设置的一部分。但是如何插入这种类型的电缆呢?它往哪边走?我的放不进去,怎么回事?

关闭电源,找到 Pi 上标有“CAMERA”的连接器。图 26-1 展示了连接器的外观。您 Pi 可能在连接器的顶部还有一个保护性的塑料胶带(我的是红色的)。这大概是为了防止灰尘进入。剥开它,然后你应该会看到一个如图所示的连接器。如果不剥掉保护条,你可能会为如何将电缆放入其中而绞尽脑汁。

img/326071_2_En_26_Fig1_HTML.jpg

图 26-1

Pi 上的摄像头连接器,去掉了保护胶带(Pi 3 B+)

在图中,您可以看到连接器的黑色 C 形部分,可以通过轻轻拉起侧面将其提起(图 26-2 )。这就露出了一个用于电缆插入的插槽。

img/326071_2_En_26_Fig2_HTML.jpg

图 26-2

摄像头连接器,黑色锁定部件已抬起,可容纳带状电缆(Pi 3 B+)

关闭电源,轻轻插入带状电缆,使触点远离连接器的黑色部分。换句话说,使触点朝向图 26-2 所示的标签。

电缆插入后,您可能仍能看到或看不到一些接触表面(图 26-3 露出连接器上方的一些接触区域)。不要用太大的力,但要用足够的力完全插入。握住电缆,向下推连接器的肩部,将 C 形黑色部分固定在插座中。

img/326071_2_En_26_Fig3_HTML.jpg

图 26-3

插入摄像头电缆,使触点背离连接器的黑色连接部分

图 26-4 显示了电缆背面在连接器中的样子。电缆的蓝色条带将朝向插座的黑色连接部分。将捆绑组件按下插入的电缆后,轻轻拉动电缆,查看其是否安装牢固。如果出来了,那就再试一次。

img/326071_2_En_26_Fig4_HTML.jpg

图 26-4

电缆的背面(蓝色)应朝向插座的黑色 C 形装订部分

照相机

相机可能带有保护套,应该去掉。图 26-5 所示。

img/326071_2_En_26_Fig5_HTML.jpg

图 26-5

移除了保护镜头盖(左)和盖子(右)的摄像机

摄像机硬件准备就绪后,可以为摄像机启动和配置 Pi。

配置

启动进入 Pi 桌面,打开 Raspberry Pi 配置。选择接口选项卡,然后启用摄像机(图 26-6 )。启用相机后,不要忘记点击确定,然后重新启动。

img/326071_2_En_26_Fig6_HTML.jpg

图 26-6

在 Raspberry Pi 配置中启用摄像机,接口选项卡。单击确定,然后重新启动。

命令

Raspbian Linux 发行版很方便,因为除了配置方便之外,它还提供了使用相机的工具。命令raspistill可用于在五秒钟的预览期后捕捉图像。

$ raspistill -o itworks.jpg

图 26-7 是使用raspistill命令从 Pi 相机捕捉的。相机预览仅显示在必须运行的图形桌面上。预览也不会在 VNC 会话中显示,但会出现在您的显示器上。

img/326071_2_En_26_Fig7_HTML.jpg

图 26-7

作者运行 raspistill 命令,大声说“成功了!”

目前有三个命令支持相机:

  • raspistill 用于静态捕捉。

  • 录制视频的 raspivid。

  • raspistillyuv 用于仍不进行编码的捕获(将 YUV/RGB 信息记录到文件中)。

这些命令的文档可以在这里找到: www。拉斯贝里皮。org/app/uploads/2013/07/RaspiCam-Documentation。pdf

计算机编程语言

如果你对 Python 感兴趣,可以尝试使用提供的 Python 包 picamera。例如,创建一个包含以下语句的 Python 文件:

from picamera import PiCamera
from time import sleep

camera = PiCamera()

camera.start_preview()
sleep(10)
camera.stop_preview()

保存为pieye.py(不要用而不是命名为picamera.py)。然后运行程序:

$ py pieye.py

摄像机预览将显示在图形桌面上,该桌面必须正在运行。

摘要

本章让你开始使用你的 Pi 相机。Raspbian Linux 通过 bult-in 驱动程序和类似raspistill的 Raspbian Linux 命令很好地支持它。有了 Raspbian 提供的工具,您就可以很好地完成与相机相关的项目。高级开发人员可以使用像 OpenCV ( opencv)这样的项目。org

这一章也为这本书画上了句号。我希望这些内容能激发你用你的 Pi 做更多的事情。谢谢你允许我做你的向导。

posted @ 2024-08-02 19:35  绝不原创的飞龙  阅读(12)  评论(0编辑  收藏  举报