树莓派高级教程(六)
协议:CC BY-NC-SA 4.0
二十三、双节棍鼠标
你可能没有实际使用任天堂 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 为作者的克隆双截棍,连接器被切断。代替连接器的是焊接实心线末端,并在焊接点上施加一块热缩材料。实心线端非常适合插入原型试验板。
图 23-1
双截棍克隆,电线末端焊接在
启用 I2C
您需要启用您的 I2C 支持。进入 Raspberry Pi 配置面板,打开 I2C(图 23-2 )。然后重启使其生效。
图 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 控制器的奇特之处在于,写入寄存器地址和读取数据之间必须有短暂的延迟。先执行写操作,然后立即执行读操作不起作用。然而,在寄存器地址之后立即写入数据确实会成功。
加密
双截棍被设计成提供一个加密的链接。但是,可以通过某种方式初始化来禁用它。失败程序如下:
-
将 0x55 写入 Nunchuk 寄存器位置 0xF0。
-
暂停一下。
-
将 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_SYN、EV_KEY和EV_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节点。这将由接收应用使用,以便读取注入的事件。这涉及两个编程步骤:
-
用
write(2)将结构uinput_user_dev信息写入文件描述符。 -
执行
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_KEY,code被设置为按键代码,并且通过将成员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_X或REL_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_X和REL_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_X和REL_Y,然后注入一个SYN_REPORT事件,让应用将它们视为一组。
关闭输入
这涉及到两个步骤:
-
/dev/input/event%d节点的破坏 -
文件描述符的关闭
以下示例显示了这两种情况:
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 展示了工具包中的内容。
图 24-1
5 英寸 HDMI 显示器套件,配有触控笔、DVD 和 LCD 单元(仍带有塑料 Shell)。不含 USB 电源线。
LCD 的背面显示了一个 HDMI 连接器(图 24-2 ,底部中间)、一个电源 USB 连接器(HDMI 连接器的右侧)、一个背光开关(右上)、一个 13x2 连接器(中上)和一个 LVDS 连接器(左下)。
图 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。
驱动程序安装
安装脚本尝试确定:
-
这是一个树莓派(第 16-17 行),以及
-
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 侧看的,没有完全就位,因此您可以看到正在插入的针脚。
图 24-3
5 英寸 LCD 13x2 连接器连接到 GPIO 条,位于 USB 连接器(Pi 3 B+)的另一端。连接器尚未完全就位。
这很容易出错,所以要反复检查。这里的一个错误可能会毁了你的一天。如果匹配正确,HDMI 连接器应该在相反的一侧排成一行,如图 24-4 所示。
图 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 所示。该模块装配有直角引脚,可以很好地插入试验板。我的也是从易贝运来的,装有电池,但不要指望这个。通常有电池运输限制。您可能需要单独购买电池。
图 25-1
插入试验板的 DS3231 模块的前视图
从连接标签列表来看,很明显这是一个 I 2 C 设备。除了电源和 I 2 C 连接,还有一个标为 SQW 的输出,可以配置为产生 1 赫兹的脉冲。在本章中,我建议您将其连接到 GPIO 22 进行演示。图 25-2 显示了 pcb 的背面。
图 25-2
安装了电池的 DS3231 的背面视图
小费
建议提前购买新电池,因为电池通常在到达时已经耗尽,或者根据运输规定不包括在内。
连接
DS3231 模块还包括一个可以使用的 AT24C32 4kx8 I 2 C EEPROM,但这将留给读者作为练习。影响 RTC 芯片的接线图如图 25-3 所示。SQW 输出的连接是可选的。它可以用来以精确的时间间隔获得 1 赫兹的脉冲。
图 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_1s和secs_10s代表 BCD(二进制编码十进制)数字,表示以秒为单位的时间。因此,值 0x23(在 uint8_t 中)字节表示十进制值 23。当 DS3231 IC 计时时,RTC(实时时钟)会自动增加这些时间值和其他时间值。
寄存器 0x01(分钟)
分钟读数在字节偏移量 0x01 处提供,格式类似于秒部分。同样,成员mins_10s和mins_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_10s和hours_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_10s和day_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_10s和months_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_10s和year_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;
成员alrms01和alrms10构成了报警 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;
成员alrmm10和alrmm01根据掩码位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)。对day10s和day1s是指定日期的 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;
成员位A1IE和A2IE设置为 1 位时启用报警中断。INTCN决定芯片发出中断信号(低电平有效)还是方波输出(INTCN =1)。选项BBSQW也必须设为 1 位,以启用方波输出。当位RS1和RS2都设置为零时,为方波输出选择 1 Hz 的速率。CONV用于启用芯片温度的读取。最后,位NEOSC ( 不是 EOSC)在为 0 位时使能振荡器,否则在为真时停止振荡器。
读数温度
DS3231 能够保持精确的时间,部分原因是它能够监控自身的温度并进行补偿。可以通过执行以下操作读取温度:
-
检查
BSY标志和CONV标志是否未设置。 -
设置
CONV标志开始转换。 -
当
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;
标志A1F和A2F指示相应的警报何时被触发。成员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.bsy或s0F.CONV标志为真时,该函数首先循环,表示器件正忙。在第 232 行设置s0E.CONV标志,然后在第 234 行写到设备。此后,DS3231 被轮询以查看s0E.CONV标志何时归零。一次一个。CONV 复位后,我们可以安全地从s11和s12寄存器中读取温度(第 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 模块支持,请执行以下操作:
-
sudo -i -
编辑
/boot/config.txt并在文件末尾添加行dtoverlay=i2c-rtc,ds3231。 -
对同一文件取消注释或添加“
dtparam=i2c_arm=on”。 -
编辑文件
/lib/udev/hwclock-set并注释掉下面三行,在前面加一个散列(#):#if [ -e /run/systemd/system ] ; then # exit 0 #fi -
重新启动
重新启动后,尝试以下操作:
# 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 通信呢?