树莓派高级教程-六-

221 阅读36分钟

树莓派高级教程(六)

原文:Advanced Raspberry Pi

协议: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 为作者的克隆双截棍,连接器被切断。代替连接器的是焊接实心线末端,并在焊接点上施加一块热缩材料。实心线端非常适合插入原型试验板。

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 通信呢?