树莓派高级教程(五)
协议:CC BY-NC-SA 4.0
十八、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 零点是一个问题,主要有两个原因:
-
ssh 访问在新的 Raspbian 映像上被禁用。
-
只有一个 USB 端口可以连接键盘和鼠标。
为了看到你在做什么,你需要一个迷你 HDMI 转 HDMI 适配器。
ssh 在默认情况下被禁用的原因是 Pi 的映像附带了一个默认的Pi帐户密码。每个人都知道,尤其是黑客。不知情的用户可以启动一个 Pi 并让它连接到网络,从而吸引各种讨厌的业务。所以 Raspbian 映像禁用了 ssh。
你真正需要的是一个由 Raspbian 支持的 USB 集线器。我尝试了一个旧的苹果集线器,运气不好。所以我换了一个最近的。一些键盘提供一两个额外的 USB 端口。如果 Raspbian Linux 支持的话,这可能是有用的。
如果你有耐心的话,你可能根本不需要网络中心。打开 Zero 电源,仅将鼠标插入 USB 适配器电缆。然后将鼠标移至您的系统偏好。当你需要输入东西的时候,你可以拔掉鼠标,插上键盘。对于大部分初始配置,您可能不需要键盘。
需要适配器
以下是你手头应该有的适配器列表,即使你只使用它们一次来设置你的 Zero。图 19-1 举例说明。
图 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 显示了所涉及的控制面板。
图 19-2
Raspberry Pi 配置,接口面板
一旦您的 Zero 重新启动,您应该能够:
-
拔下鼠标(如果不使用集线器)。
-
插入 USB 转以太网适配器。
-
将以太网电缆插入路由器。
-
扫描您的网络以发现分配的 IP 地址。
如果您使用集线器,请使用键盘和鼠标打开终端窗口,并键入ifconfig命令来确定分配的地址。图 19-3 中的例子显示地址为192.168.1.15。
图 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:
使用 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 下载可在以下网址找到:
从网站上找到最新下载的链接。在撰写本文时,以下内容是最新的:
$ 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)支持。这可能会让你陷入无法退格或删除输入的糟糕境地。
要解决该问题,请执行以下操作:
-
CD ~/xrpi/devel/staging/cross tool-ng-1 . 23 . 0/kconfig/LX dialog
-
编辑文件
inputbox.c并转到大约第 128 行,在那里您应该看到一行文字:case KEY_BACKSPACE: -
在它下面加一行简单的文字:
case 8: -
保存文件(
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 键打开路径和杂项选项子菜单。
图 20-1
ct-ng 菜单配置打开对话框
一旦进入路径和杂项选项菜单,如图 20-2 所示,使用光标键向下移动,尝试标记为实验的功能。突出显示该行后,按空格键将星号放在方括号内以选择该选项(再次按空格键将切换设置)。
图 20-2
ct-ng 启用“尝试标记为实验性的功能”(按空格键)
之后,在同一个菜单中,将光标向下移动到标有前缀目录的中间条目,并按回车键选择它(图 20-3 )。不幸的是,最近的默认设置阻止你在菜单选项的最右边看到“前缀目录”文本。
图 20-3
选择 CT_PREFIX 行以设置前缀
对于本章中使用的过程,请将路径修改为以下内容:
/opt/x-tools/${CT_TARGET}
图 20-4 显示了输入对话框。如果您发现无法使用退格键,请应用“修补 inputbox.c”一节中讨论的修复方法。
图 20-4
设置前缀目录
建立路径名后,在显示的“OK”按钮上按 Enter 键。这将使您返回到路径和杂项选项菜单。
然后选择底部显示的退出按钮,并再次按 Enter 键。如果您更改了任何需要保存的内容,则选择“是”(图 20-5 )。
图 20-5
“保存配置”对话框
目标选项
重新启动菜单,用光标选择目标选项,按回车键打开菜单(图 20-6 )。
图 20-6
选择目标选项
然后选择目标架构并按回车键(图 20-7 )。
图 20-7
选择目标架构
在菜单中,选择arm并按空格键(图 20-8 )。然后使用底部的选择按钮。这将使您返回目标选项菜单。
图 20-8
选择 arm 架构
在目标选项菜单中(如下所示),通过查看圆括号中的状态来验证字节序设置。它应该读小端(图 20-9 )。如果没有,进入那个菜单,把它改成小端。Endianness 菜单项下面是 Bitness 选项。它应该已经显示为 32 位。如果没有,那就换一个。
图 20-9
检查字节序和字节序
最后,用 exit 按钮退出这个子菜单。
操作系统
再次进入主菜单(图 20-10 ,选择操作系统,然后选择目标 OS(裸机),如图 20-11 所示。
图 20-12
选择 Linux(不是裸机)
图 20-11
选择目标操作系统(裸机)
图 20-10
选择操作系统
在图 20-12 中选择“linux”后,退出并返回主菜单。
二元实用程序
在主菜单上,打开二进制实用程序菜单(图 20-13 )。
图 20-13
二进制实用程序菜单
光标向下移动到 Binutils 版本并打开它(图 20-14 )。
图 20-14
binutils 版本菜单
一旦进入最终的 binutils 版本选择菜单,选择最新的版本,除非您在之前的构建尝试中遇到了麻烦(图 20-15 )。
图 20-15
选择 binutils 版本(通常是最新的)
退出回到主菜单。
c 编译器
在主菜单上,打开 C 编译器子菜单(图 20-16 )。
图 20-16
主菜单的 C 编译器子菜单
这里建议您启用显示 Linaro 版本选项(图 20-17 )。
图 20-17
启用“显示 Linaro 版本”
启用后,您可以选择 Gcc 版本子菜单(图 20-18 )。
图 20-18
选择菜单“gcc 版本”
图 20-19 显示了选择的 Linaro 编译器的最新版本。
图 20-19
选择最新的 linaro 编译器
然后再次选择退出以返回主菜单。
保存配置
除非您有理由更改其他内容,否则请再次退出菜单,以显示保存提示:
选择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 传感器。
图 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。这可以在命令行上覆盖。
图 22-2
DHT11 电路
当 Pi 监听 GPIO 引脚而 DHT11 不发送数据时,线路会悬空。因此, R 1 用于将线路上拉至 3.3 V 的电平。数据手册建议使用 5kω电阻(可以安全地用更常见的 4.7 千欧电阻代替)。激活时,GPIO 引脚或传感器上的负载小于 1 mA。数据手册还指出,5 千欧电阻应适用于最长 20 米的电缆。
草案
传感器只有在主人(Raspberry Pi)的刺激下才会说话。主机必须首先在总线上发出请求,并等待传感器做出响应。DHT 传感器响应 40 位信息,其中 8 位是校验和。
总体协议
整个信号协议是这样工作的:
-
由于上拉电阻的原因,线路空闲为高电平。
-
主机将线路拉低至少 18 ms,以发出读请求信号,然后释放总线,使线路返回高电平状态。
-
暂停约 20 至 40 μs 后,传感器会将线路拉低 80 μs,然后再让线路返回高电平 80 μs,这表示它打算返回数据。
-
然后,DHT11 将 40 位信息写入总线:每一位以 50 μs 低电平开始,随后是:
-
26 至 28 μs 的高电平表示 0 位
-
70 μs 高电平表示 1 位
-
-
当传感器再次将线路拉低 50 μs 时,传输结束。
-
传感器释放总线,使线路回到高空闲状态。
图 22-3 显示了传感器的整体协议。主控制用粗线表示,而传感器控制用细线表示。最初,总线处于空闲状态,直到主机将线路拉低并释放它(标记为 Request)。传感器抓取总线并发出信号表示总线正在响应(80 μs 低电平,然后 80 μs 高电平)。传感器以 40 位传感器数据结束,再一次转变为低电平(标记为 End)以标志最后一位的结束。
图 22-3
通用 DHT11 协议
数据位
如图 22-4 所示,每个传感器数据位开始转换为低电平,然后转换为高电平。当线路作为下一位的一部分再次变为低电平时,该位结束。最后一位由一个最终的低到高转换来标记。
图 22-4
DHT11 数据位
每个数据位从转换到低电平开始,持续 50 μs。最后一位之后最终转换到低电平也持续 50 μs。在该位从低电平转换到高电平之后,如果高电平仅持续 26 到 28 μs,则该位变为 0 位。1 位则保持高电平 70 μs。
数据格式
图 22-5 显示了 40 位传感器响应,首先传输最高有效位。数据手册规定了 16 位相对湿度、16 位摄氏度温度和 8 位校验和。然而,DHT11 总是为湿度和温度分数字节发送 0。因此,该器件每次测量的精度实际上只有 8 位。据推测,其他模型(或未来的模型)提供了更高精度的分数值。
图 22-5
DHT11 数据格式
校验和是前 4 个字节的简单总和。任何进位溢出都被简单地丢弃。这种校验和使您的应用在面临可能的接收错误时更有信心接收到正确的值。
图 22-6 显示了 DHT11 信号的总体范围轨迹。在该图中,水平轴由 Pi 驱动的 30 ms 低电平信号控制,用于唤醒器件。DHT11 在第一个初始尖峰后发送其 40 位数据,如右图所示。数据手册指出,每秒钟对传感器的查询不应超过一次。
图 22-6
DHT11 信号的范围跟踪概述
DHT11 响应数据的特写如图 22-7 所示。第一个高电平脉冲(左侧)来自 Pi,它释放总线并允许上拉电阻提高总线电压。12 μs 后,DHT11 将总线拉低 80 μs,然后让总线再拉高 80 μs,这标志着随后 40 位数据的开始。
图 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
计算经过时间的一般程序是:
-
捕捉初始时间(称之为 t0)。
-
在事件发生后捕获当前时间(称之为 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.c和libgp.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
变量t0的static值用零建立。当第一次输入代码时,日期/时间在第 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 访问,我们获得了足够快的访问速度来测量信号变化。如果程序因等待丢失事件而停滞不前,应用间隔计时器可以提供恢复安全性。这些是解决棘手问题必须做的一些偷偷摸摸的事情。