Email: BuddyZhang1 buddy.zhang@aliyun.com

目录


PCF8574AT 简介

PCF8574 是一个 8bit 的 GPIO 拓展芯片,提供了标准的 I2C 接口。PCF8574 具有 低电流消耗以及具有高电流驱动能力的锁存输出,可用于驱动 LED 等。PCF8574 还拥有一条中断线 (INT) 用于连接到主控的中断逻辑上。PCF8574AT 具有 8 个可 用的 GPIO,每当 GPIO 的状态改变时,PCF8574 的 INT 引脚将会产生一个中断 信号,从而知道 GPIO 的状态,而不必通过轮寻的方式检查 GPIO 的状态。主控 可以通过 I2C 总线向 8 个 GPIO 写值,也可以读取 8 个 GPIO 的值。PCF8574 一般用于 GPIO 紧缺的情况,可以在 I2C 总线上挂载多个 PCF8574 来增加 GPIO 的数量。

PCF8574AT Datasheet

市面上也提供了很过针对 PCF8574AT 的模块设计,例如下图:


实践准备

BiscuitOS 已经支持 PCF8574AT 的硬件实践,但在实践前需要作相应的准备, 必须要的准备有:


硬件准备

在开发 PCF8574AT 驱动前,开发者应该准备下列硬件.


硬件平台

开发者需要准备一块 RaspberryPi 4B 开发板,并配有 SD 卡,SD 读卡器, TTL 转 USB 串口一个。


PCF8574AT

开发者可以从网上购买 PCF8574AT 模块。


逻辑分析仪

逻辑分析仪能够帮助开发者快速分析数据,测试 PCF8574AT 功能,稳定性, 大量数据采样等。逻辑分析仪不是必须的,这里推荐使用 DreamSourceLab 开发的 DSLogic:

DSLogic 逻辑分析仪数据工具:


示波器

示波器能够帮助开发者对 I2C 总线进行最透彻的分析,示波器测量 的数据具有可靠性高,精度高的特定,是分析 I2C 问题不可或缺的 工具。示波器建议准备,这里推荐使用 DreamSourceLab 开发的 DsCope:

DSCope 示波器采用样图:


硬件连接

在准备好所有的硬件之后,接下来将 PCF8574AT 与 RaspberryPi 4B 连接, 连接如下图:

红线为 VCC (3.3V) 接 PCF8574AT 的 VCC 引脚; 黑线为 GND 接 PCF8574AT 的 GND 引脚; 黄线为 SDA 接 PCF8574AT 的 SDA 引脚; 绿线为 SCL 接 PCF8574AT 的 SCL 引脚, 白线接到 PCF8574AT 的 INT 引脚上。 RaspberryPi 4B 的引脚定义如下图:

连接过程中要注意杜邦线虚连,连接完毕之后请使用万用表确保每条线 都已经连接上。


软件准备

在进行 PCF8574AT 软件开发前,开发这应该准备并部署所需的 软件工具及环境。如下表列出的内容:


RaspberryPi 4B 开发环境部署

在驱动开发前,开发者需要准备一个能在 RaspberryPi 4B 上运行的 BiscuitOS,具体制作方法可以参考下列文档:


内核启用 I2C 功能

为了在 RaspberryPi 上使用 PCF8574AT,开发者应该基于 BiscuitOS RaspberryPi 的开发环境对内核进行配置,开发者可以参考如下命令:

cd BiscuitOS/output/RaspberryPi_4B/linux/linux
make menuconfig ARCH=arm

选择并进入 “Device Driver”

选择并进入 “I2C support —>”

以模块的方式选择 “I2C device interface”

选择并进入 “I2C Hardware Bus support —>”

以模块的形式选择 “Broadcom BCM2835 I2C controller”, 最后保存并退出。 接着是编译内核和模块,并安装模块和内核到新的 SD 卡上,可以 参数如下文档:

准备好以上步骤之后,在系统启动之后,使用串口登录到 RaspberryPi, 使用如下命令安装 I2C 总线模块,如下:

cd /lib/modules/5.0.21/kernel/driver/i2c/bus
insmod busses/i2c-bcm2835.ko
insmod i2c-dev.ko

RaspberryPi 4B 启用 I2C 功能

制作完 BiscuitOS 镜像之后,将 BiscuitOS 镜像烧录到 SD 卡, 烧录完毕之后,重启插拔 SD 读卡器,此时可以获得 SD 卡出现 两个分区,其中一个为 BOOT 分区,如下图:

此时修改 BOOT 分区了的 “config.txt” 文件,RaspberryPi 默认是 关闭 I2C 功能的,此时将下面关于 I2C 的配置打开,如下:

# Uncomment some or all of these to enable the optional hardware interfaces
dtparam=i2c_arm=on
#dtparam=i2s=on
#dtparam=spi=on

确保 “dtparam=i2c_arm=on” 没有被注释掉。修改完毕之后,保存退出,最后 移除 SD 卡,将其重新插入到 RaspberryPi 4B 的 SD 卡槽里。


开发相关文档

PCF8574AT 驱动

BiscuitOS 为 PCF8574AT 提供了完整的内核驱动支持,开发者可以参考本节 内容,对 PCF8574AT 进行实践部署。


PCF8574AT 驱动分析

BiscuitOS 已经支持最新的 PCF8574AT 芯片,开发者可以使用 BiscuitOS 提供 的驱动对 PCF8574AT 进行使用,驱动源码 GithuB 地址如下:

BiscuitOS 提供的完整驱动如下:

驱动分作三个部分,第一个部分是 PCF8574AT 设备注册到对应总线上;第二部分是 PCF8574AT 提供 I/O Expander 的读写接口;第三部分是驱动对 I/O Expander 的使用。 接下来的内容将详细分析每一部分代码的构成。


PCF8574AT 设备注册

新版本内核中,I2C 子系统提供了一套简单的接口就可以将一个 I2C 设备 注册到 I2C 子系统。本例程通过向 DTS 中添加 PCF8574AT,并将 PCF8574AT 注册到 I2C 子系统。首先如下图:

函数首先定义一个 struct i2c_driver 结构体,然后填充该结构体。结构体中提供了 driver 成员,在 PCF8574AT 中需要提供 name, owner, 以及 of_match_table 三个 成员,其中 of_match_table 是一个 struct of_device_id 结构,该结构用于与 DTS 中的节点 compatible 属性进行对比,如果相同,即从 DTS 中找到驱动对应的节点, 例如在本例程中,compatible 属性的属性值是 “BiscuitOS,pcf8574”; name 成员必须 与 struct i2c_device_id 结构的 name 成员相同,即总线上的驱动和设备通过名字进行 匹配,在上面的代码中即 pcf8574_id 结果的 name 设置的与 pcf8574_driver 结构的 driver 成员的 name 一致;至于 owner 成员,由于驱动以模块的形式添加到内核,那么 owner 设置为 THIS_MODULE 就行。最后提供 I2C 设备需要的 probe 和 remove 接口。

以上就是简单的驱动端程序设置,设置完毕之后调用 module_i2c_driver() 函数 将 pcf8574_driver 对应的驱动注册到 I2C 子系统上。提供驱动代码还不能让驱动 正常的工作,还需 DTS 中添加 pcf8574 对应的设备节点。开发者可以参考 BiscuitOS 提供的 RaspberryPi 4B 项目的方法进行添加。在 RaspberryPi 4B 中,DTS 位于 内核源码 arch/arm/boot/dts/bcm2711-rpi-4-b.dts, 添加如下:

在上面的 DTS 文件中,节点名字为 pcf8574,与驱动中的 struct i2c_device_id name 成员相同。特别值得注意的是节点 reg 属性值必须与节点名字 @ 符号后面的数字 相同,这符合 DTS 语法。reg 属性即 PCF8574AT I2C 的从设备地址。节点的 compatible 属性值必须与驱动的 struct of_device_id 一致。通过上面的代码,系统启动后可以 向系统注册一个简单的 PCF8574AT 模块。 DB-gpio 属性指向中断所在的 GPIO.


PCF8574AT I2C 接口

通过 PCF8574AT 的数据手册可以知道,PCF8574AT 提供只提供了一种方式的 读和一种方式的写操作,与大多数的 I2C 从设备不同,PCF8574 从设备不能通过 通用的 i2c 工具或函数进行读写,需要对 I2C 的读写过程进行封装。 接下里分别介绍每种操作如何编写代码。


PCF8574AT 读操作

上图定义了 PCF8574AT 的 I2C 总线读操作时序,从上图的定义可以看出, 在主控产生一个 START 信号之后,只需发送向 I2C 总线上发送 PCF8574AT 的从设备地址,并且 R/W bit 设置为 1 即可,此时如果 PCF8574AT 收到 主控发来的请求之后,会通过拉低 SDA 总线产生一个 ACK 应答信号,此时 主控收到应答信号之后,就等待 PCF8574AT 发送两个 Byte 的数据。PCF8574AT 产生第一个 ACK 应答之后,会向 SDA 总线上发送一个 BYTE 的数据,该数据 就是当前 8-bit I/O 的状态,接着采用同样的方式,拉低 ACK 再发送一次 8-bit I/O 的状态。发送完毕两 Byte 的数据之后,主控产生一个 STOP 信息 结束本次读操作。值得注意的是,PCF8574AT 在发送完第一个 BYTE 数据之后, 会在 INT 引脚上产生一个时长为 Tir 的中断。

上图即是根据信号协议编写的 Read 操作函数。在内核中,I2C 子系统定义了 struct i2c_msg 结构为一个 I2C 帧,其定义如下:

struct i2c_msg {
        __u16 addr;     /* slave address                        */
        __u16 flags;
#define I2C_M_RD                0x0001  /* read data, from slave to master */
                                        /* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN               0x0010  /* this is a ten bit chip address */
#define I2C_M_DMA_SAFE          0x0200  /* the buffer of this message is DMA safe */
                                        /* makes only sense in kernelspace */
                                        /* userspace buffers are copied anyway */
#define I2C_M_RECV_LEN          0x0400  /* length will be first received byte */
#define I2C_M_NO_RD_ACK         0x0800  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK        0x1000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR      0x2000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART           0x4000  /* if I2C_FUNC_NOSTART */
#define I2C_M_STOP              0x8000  /* if I2C_FUNC_PROTOCOL_MANGLING */
        __u16 len;              /* msg length                           */
        __u8 *buf;              /* pointer to msg data                  */
};

根据 PCF8574AT 读时序,主控只需发送一个 i2c_msg 即可,因此定义了 一个 i2c_msg 结构,由于读时序的时候,R/W bit 置位,因此给 i2c_msg 结构的 flags 添加 I2C_M_RD 标志,以此表示该 i2c_msg 用于读操作。 接着根据主控发送 i2c_msg 之后,PCF8574AT 会返回两个 byte 的字节, 因此将 i2c_msg 的 len 设置为 2, 并且 buf 只想一个可以存储 2 个字节 的内存地址。

以上准备好 i2c_msg 之后,继续调用 i2c_transfer() 函数,将 i2c_msg 传递给 I2C 核心层,最终到达 I2C 控制器的收发器上发送。接下来使用 示波器和逻辑分析仪实际的抓起波形分析,具体如下:

从上图的波形图可以看出,抓取的波形符合 “PCF8574AT Read” 的规范 要求。

借助 DSlogic 分析仪,可以快速获得数字波形以及协议解析信息, 上图的波形符合 “PCF8574AT Read” 的要求。


PCF8574AT 写操作

上图为 PCF8574AT 的写操作时序,其时序比较简单,主控首先产生一个 START 信号,然后发送从设备地址到 I2C 总线上,并且 R/W bit 设置为 0. 发送 完毕上面数据后,主控等待从设备的应答,如果从设备将 SDA 拉低,那么表示 从设备应答,那么主控继续将第一个 byte 写到 I2C 总线上,该 byte 用于 设置 PCF8574AT 8-bit 的状态,写完第一个 byte 之后,主控等待 PCF8574AT 应答,如果从设备应答,那么接着发送第二部 byte。发送完毕之后,主控不应答 并产生一个 STOP 信号。

上图即是根据信号协议编写的 PCF8574AT Write。在内核中,I2C 子系统定义了 struct i2c_msg 结构为一个 I2C 帧,其定义如下:

struct i2c_msg {
        __u16 addr;     /* slave address                        */
        __u16 flags;
#define I2C_M_RD                0x0001  /* read data, from slave to master */
                                        /* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN               0x0010  /* this is a ten bit chip address */
#define I2C_M_DMA_SAFE          0x0200  /* the buffer of this message is DMA safe */
                                        /* makes only sense in kernelspace */
                                        /* userspace buffers are copied anyway */
#define I2C_M_RECV_LEN          0x0400  /* length will be first received byte */
#define I2C_M_NO_RD_ACK         0x0800  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK        0x1000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR      0x2000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART           0x4000  /* if I2C_FUNC_NOSTART */
#define I2C_M_STOP              0x8000  /* if I2C_FUNC_PROTOCOL_MANGLING */
        __u16 len;              /* msg length                           */
        __u8 *buf;              /* pointer to msg data                  */
};

在上面的函数中,由于 PCF8574AT 只包含了一次写操作,因此只需要一个 i2c_msg 就可以完成任务,因此定义一个 i2c_msg 结构,由于写操作的 时候,R/W bit 为 0,因此给 i2c_msg 结构的 flags 添加 I2C_M_WR 标志, 由于需要写两个 byte 的数据,因此 len 设置为 2,并且 buf 只想存储 两个字节的地址.

以上准备好 i2c_msg 之后,继续调用 i2c_transfer() 函数,将 i2c_msg 传递给 I2C 核心层,最终到达 I2C 控制器的收发器上发送。接下来使用 示波器和逻辑分析仪实际的抓起波形分析,具体如下:

上图为按字节写的波形,波形符合 PCF8574AT 协议规范。

上图可以看出逻辑分析仪对按字节写的波形的解读,符合规范要求。


PCF8574AT 使用

开发者可以参考上面代码在内核驱动中使用 PCF8574AT, 例如在上面的代码中, 通过设置 GPIO 的状态和读取 GPIO 状态。


PCF8574AT BiscuitOS-RaspberryPi 实践部署

BiscuitOS 以及完整支持 PCF8574AT,并基于 Kbuild 编译系统,制作了一套 便捷的 PCF8574AT 开发环境,开发者可以参考如下步骤进行快速开发。


PCF8574AT 源码获取

首先开发者应该准备基于 RaspberryPi 4B BiscuitOS 开发环境,并 准备好一个在 RaspberryPi 上运行的 BiscuitOS,然后使用如下命令 获得 PCF8574AT 所需的开发环境:

cd BiscuitOS
make RaspberryPi_4B_defconfig
make menuconfig

选择 “Package —>” 并进入下一级菜单

选择 “I2C: Inter-Integrated Circuit —>” 并进入下一级菜单

设置 “PCF8574 IO Expander Device Driver —>” 为 “Y”。设置完毕之后, 保存并退出,


PCF8574AT 源码编译

PCF8574AT 的编译很简单,只需执行如下命令就可以快速编译:

make
cd BiscuitOS/output/RaspberryPi_4B/package/pcf8574_module-0.0.1/
make prepare
make download
make
make install
make pack


PCF8574AT 模块安装

接下来是将模块更新到 RaspberryPi 4B 上,更新的方式多种方式,可以参考下面 方式:


PCF8574AT DTB 安装

由于 PCF8574AT 驱动的加载需要使用 DTB,因此在编写完驱动之后,应该 修改 DTS 并生成 DTB 更新到 RaspberryPi 4B 上。如果 DTB 已经更新, 不必重复更新 DTB。DTS 的修改可以参考 “pcf8574_module-0.0.1” 目录下的 default.dts 文件,使用命令 如下:

cd BiscuitOS/output/RaspberryPi_4B/linux/linux/arch/arm/boot/dts
vi bcm2711-rpi-4-b.dts

将 PCF8574AT DTS 节点信息加入到 i2c1 节点下,如下:

&i2c1 {
        pinctrl-names = "default";
        pinctrl-0 = <&i2c1_pins>;
        clock-frequency = <100000>;

        pcf8574@38 {
                compatible = "BiscuitOS,pcf8574";
                reg = <0x38>;
                BD-gpio = <&gpio 25 GPIO_ACTIVE_HIGH>;
        };
};

修改完 DTS 之后,开发者使用如下命令编译 DTS:

cd BiscuitOS/output/RaspberryPi_4B/linux/linux/
make ARCH=arm CROSS_COMPILE=BiscuitOS/output/RaspberryPi_4B/arm-linux-gnueabi/arm-linux-gnueabi/bin/arm-linux-gnueabi- dtbs -j4

最后就是将生成的 DTB 更新到 RaspberryPi 4B 上,更新的方式 如下:

如果采用 NFS 方式更新,在 RaspberryPi 4B 上使用如下命令:

mkdir -p /nfs
mount -t nfs 192.168.x.x:BiscuitOS/output/RaspberryPi_4B/ /nfs -o nolock
cp /nfs/linux/RPI_linux_github/arch/arm/boot/dts/bcm2711-rpi-4-b.dtb /boot
sync
reboot

在上面的命令中,192.168.x.x 为主机的 IP 地址。如果采用 SD 拷贝方式, 主机端请参考如下命令:

cp BiscuitOS/output/RaspberryPi_4B/linux/RPI_linux_github/arch/arm/boot/dts/bcm2711-rpi-4-b.dtb /media/XXXX/BOOT/
sync

在上面的命令中,XXXX 表示主机的用户名。至此 DTB 更新成功.


PCF8574AT 模块使用

最后就是在 BiscuitOS RaspberryPi 4B 平台上加载模块,可以参考如下命令:

cd /lib/modules/5.0.21-v7l\+/extra
ls
insmod i2c-bcm2835.ko
insmod pcf8574.ko


PCF8574AT 工程实践部署

开发者也可以在自己的工程中部署 PCF8574AT 驱动程序,可以参考如下步骤:


PCF8574AT 源码获取

首先获得 PCF8574AT 驱动源码,使用如下命令:

wget https://gitee.com/BiscuitOS_team/HardStack/raw/Gitee/Device-Driver/i2c/Device/PCF8574/Base/kernel/Kconfig
wget https://gitee.com/BiscuitOS_team/HardStack/raw/Gitee/Device-Driver/i2c/Device/PCF8574/Base/kernel/Makefile
wget https://gitee.com/BiscuitOS_team/HardStack/raw/Gitee/Device-Driver/i2c/Device/PCF8574/Base/kernel/default.dts
wget https://gitee.com/BiscuitOS_team/HardStack/raw/Gitee/Device-Driver/i2c/Device/PCF8574/Base/kernel/pcf8574.c

PCF8574AT 源码编译

获得上面的源码之后,可以采用外部模块独立编译,或者加入内核源码树进行编译, 开发者可以参考如下步骤进行编译。

外部模块独立编译

此时对 Makefile 文件进行修改,其中 KERNELDIR 修改为开发者项目 Linux 源码树所在的目录。CROSS_COMPILE 变量指向交叉编译工具链所在的位置。

设置完毕之后,使用如下命令:

make
make install
内核源码树编译

开发者也可以将驱动源码直接加入内核源码树,当加入到指定目录后,请参考 Kconfig 文件的内容,将 PCF8574AT 加入到指定目录,修改该目录下的 Kconfig 和 Makefile, 然后配置内核将 PCF8574AT 启动加入编译,使用如下命令:

make menuconfig ARCH=arm
make modules ARCH=arm CROSS_COMPILE=BiscuitOS/output/RaspberryPi_4B/arm-linux-gnueabi/arm-linux-gnueabi/bin/arm-linux-gnueabi-
make modules_install INSTALL_MOD_PATH=BiscuitOS/output/RaspberryPi_4B/rootfs/rootfs

PCF8574AT 模块安装

接下来是将模块更新到项目平台上,更新的方式多种方式,可以参考下面 方式:


PCF8574AT DTB 安装

由于 PCF8574AT 驱动的加载需要使用 DTB,因此在编写完驱动之后,应该 修改 DTS 并生成 DTB 更新到 RaspberryPi 4B 上。如果 DTB 已经更新, 不必重复更新 DTB。DTS 的修改可以参考 “pcf8574_module-0.0.1” 目录下的 default.dts 文件,使用命令 如下:

cd BiscuitOS/output/RaspberryPi_4B/linux/linux/arch/arm/boot/dts
vi bcm2711-rpi-4-b.dts

将 PCF8574AT DTS 节点信息加入到 i2c1 节点下,如下:

&i2c1 {
        pinctrl-names = "default";
        pinctrl-0 = <&i2c1_pins>;
        clock-frequency = <100000>;

        pcf8574@38 {
                compatible = "BiscuitOS,pcf8574";
                reg = <0x38>;
		BD-gpio = <&gpio 25 GPIO_ACTIVE_HIGH>;
        };
};

修改完 DTS 之后,开发者使用如下命令编译 DTS:

cd BiscuitOS/output/RaspberryPi_4B/linux/linux/
make ARCH=arm CROSS_COMPILE=BiscuitOS/output/RaspberryPi_4B/arm-linux-gnueabi/arm-linux-gnueabi/bin/arm-linux-gnueabi- dtbs -j4

最后就是将生成的 DTB 更新到 RaspberryPi 4B 上,更新的方式 如下:

如果采用 NFS 方式更新,在 RaspberryPi 4B 上使用如下命令:

mkdir -p /nfs
mount -t nfs 192.168.x.x:BiscuitOS/output/RaspberryPi_4B/ /nfs -o nolock
cp /nfs/linux/RPI_linux_github/arch/arm/boot/dts/bcm2711-rpi-4-b.dtb /boot
sync
reboot

在上面的命令中,192.168.x.x 为主机的 IP 地址。如果采用 SD 拷贝方式, 主机端请参考如下命令:

cp BiscuitOS/output/RaspberryPi_4B/linux/RPI_linux_github/arch/arm/boot/dts/bcm2711-rpi-4-b.dtb /media/XXXX/BOOT/
sync

在上面的命令中,XXXX 表示主机的用户名。至此 DTB 更新成功.


PCF8574AT 模块使用

最后就是在开发平台上加载模块,可以参考如下命令:

cd /lib/modules/5.0.21-v7l\+/extra
ls
insmod i2c-bcm2835.ko
insmod pcf8574.ko


i2cdetect

i2cdetect 工具用于探测指定 I2C 总线上所有 I2C 从设备,如果 指定 I2C 地址存在 I2C 设备,那么会将 I2C 从设备地址标记在结果 里。i2cdetect 使用如下图:

如上图,i2cdetect 找到了 PCF8574AT 的多个从设备地址。i2cdetect 工具的 参数中,”-y” 用于指定 I2C 总线的编号.


i2cdump

i2cdump 工具用于打印指定 I2C 总线上指定从设备上的所有数据,该 工具用于 I/O Expander 的数据查看,i2cdump 使用如下图:

如上图,使用 i2cdump 工具查看 PCF8574AT 上的所有数据. 该工具对快速读取 I2C 设备上数据有帮助。


i2cget

i2cget 工具用于从指定 I2C 总线的从设备上读取指定位置的值, 在开发过程中,需要对从设备的某个寄存器进行读取操作,这个 工具再适合过不,其使用如下图:

在使用 i2cget 工具的时候,”-y” 参数后面匹配 I2C 总线编号,如上图 中查找的是 I2C1 总线; 接下来的参数是 I2C 从设备地址和内部偏移地址。


i2cset

与 i2cget 工具相对应的工具就是 i2cset, 该工具用于在指定 I2C 总线 上,给特定的 I2C 从设备的内部地址进行写操作, 其使用如下图:

在使用 i2cset 工具的时候,”-y” 参数后面匹配 I2C 总线编号,如上图 查找的是 I2C1 总线;接下来的参数是 I2C 从设备地址,以及内部要写 的地址,最后是要写的内容。


PCF8574AT 应用程序

BiscuitOS 为 PCF8574AT 提供了完整的应用程序支持,开发者可以参考本节 内容,对 PCF8574AT 应用程序进行实践部署。


PCF8574AT 应用程序分析

BiscuitOS 已经支持最新的 PCF8574AT 芯片,开发者可以使用 BiscuitOS 提供 的应用程序 PCF8574AT 进行使用,驱动源码 GithuB 地址如下:

BiscuitOS 提供的完整应用程序如下:

应用程序分作两个部分,第一部分是 PCF8574AT 提供 I/O Expander 的读写接口; 第二部分是驱动对 I/O Expander 的使用。接下来的内容将详细分析每一部分代码的构成。


PCF8574AT I2C 接口

通过 PCF8574AT 的数据手册可以知道,PCF8574AT 提供了一种方式的读,也 只提供了一种方式的写。接下里分别介绍每种操作如何编写代码。


PCF8574AT 随机地址读操作

上图定义了 PCF8574AT 的 I2C 总线读操作时序,从上图的定义可以看出, 在主控产生一个 START 信号之后,只需发送向 I2C 总线上发送 PCF8574AT 的从设备地址,并且 R/W bit 设置为 1 即可,此时如果 PCF8574AT 收到 主控发来的请求之后,会通过拉低 SDA 总线产生一个 ACK 应答信号,此时 主控收到应答信号之后,就等待 PCF8574AT 发送两个 Byte 的数据。PCF8574AT 产生第一个 ACK 应答之后,会向 SDA 总线上发送一个 BYTE 的数据,该数据 就是当前 8-bit I/O 的状态,接着采用同样的方式,拉低 ACK 再发送一次 8-bit I/O 的状态。发送完毕两 Byte 的数据之后,主控产生一个 STOP 信息 结束本次读操作。值得注意的是,PCF8574AT 在发送完第一个 BYTE 数据之后, 会在 INT 引脚上产生一个时长为 Tir 的中断。

上图即是根据信号协议编写的 Read 操作函数。在内核中,I2C 子系统定义了 struct i2c_msg 结构为一个 I2C 帧,其定义如下:

struct i2c_msg {
        __u16 addr;     /* slave address                        */
        __u16 flags;
#define I2C_M_RD                0x0001  /* read data, from slave to master */
                                        /* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN               0x0010  /* this is a ten bit chip address */
#define I2C_M_DMA_SAFE          0x0200  /* the buffer of this message is DMA safe */
                                        /* makes only sense in kernelspace */
                                        /* userspace buffers are copied anyway */
#define I2C_M_RECV_LEN          0x0400  /* length will be first received byte */
#define I2C_M_NO_RD_ACK         0x0800  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK        0x1000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR      0x2000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART           0x4000  /* if I2C_FUNC_NOSTART */
#define I2C_M_STOP              0x8000  /* if I2C_FUNC_PROTOCOL_MANGLING */
        __u16 len;              /* msg length                           */
        __u8 *buf;              /* pointer to msg data                  */
};

根据 PCF8574AT 读时序,主控只需发送一个 i2c_msg 即可,因此定义了 一个 i2c_msg 结构,由于读时序的时候,R/W bit 置位,因此给 i2c_msg 结构的 flags 添加 I2C_M_RD 标志,以此表示该 i2c_msg 用于读操作。 接着根据主控发送 i2c_msg 之后,PCF8574AT 会返回两个 byte 的字节, 因此将 i2c_msg 的 len 设置为 2, 并且 buf 只想一个可以存储 2 个字节 的内存地址。

以上准备好 i2c_msg 之后,继续调用 ioctl() 函数,将 i2c_msg 传递给 I2C 核心层,最终到达 I2C 控制器的收发器上发送。接下来使用 示波器和逻辑分析仪实际的抓起波形分析,具体如下

从上图的波形图可以看出,抓取的波形符合 “PCF8574AT Read” 的规范 要求。

借助 DSlogic 分析仪,可以快速获得数字波形以及协议解析信息, 上图的波形符合 “PCF8574AT Read” 的要求。


PCF8574AT 写操作

上图为 PCF8574AT 的写操作时序,其时序比较简单,主控首先产生一个 START 信号,然后发送从设备地址到 I2C 总线上,并且 R/W bit 设置为 0. 发送 完毕上面数据后,主控等待从设备的应答,如果从设备将 SDA 拉低,那么表示 从设备应答,那么主控继续将第一个 byte 写到 I2C 总线上,该 byte 用于 设置 PCF8574AT 8-bit 的状态,写完第一个 byte 之后,主控等待 PCF8574AT 应答,如果从设备应答,那么接着发送第二部 byte。发送完毕之后,主控不应答 并产生一个 STOP 信号。

上图即是根据信号协议编写的 PCF8574AT Write。在内核中,I2C 子系统定义了 struct i2c_msg 结构为一个 I2C 帧,其定义如下:

struct i2c_msg {
        __u16 addr;     /* slave address                        */
        __u16 flags;
#define I2C_M_RD                0x0001  /* read data, from slave to master */
                                        /* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN               0x0010  /* this is a ten bit chip address */
#define I2C_M_DMA_SAFE          0x0200  /* the buffer of this message is DMA safe */
                                        /* makes only sense in kernelspace */
                                        /* userspace buffers are copied anyway */
#define I2C_M_RECV_LEN          0x0400  /* length will be first received byte */
#define I2C_M_NO_RD_ACK         0x0800  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK        0x1000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR      0x2000  /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART           0x4000  /* if I2C_FUNC_NOSTART */
#define I2C_M_STOP              0x8000  /* if I2C_FUNC_PROTOCOL_MANGLING */
        __u16 len;              /* msg length                           */
        __u8 *buf;              /* pointer to msg data                  */
};

在上面的函数中,由于 PCF8574AT 只包含了一次写操作,因此只需要一个 i2c_msg 就可以完成任务,因此定义一个 i2c_msg 结构,由于写操作的 时候,R/W bit 为 0,因此给 i2c_msg 结构的 flags 添加 I2C_M_WR 标志, 由于需要写两个 byte 的数据,因此 len 设置为 2,并且 buf 只想存储 两个字节的地址.

以上准备好 i2c_msg 之后,继续调用 ioctl() 函数,将 i2c_msg 传递给 I2C 核心层,最终到达 I2C 控制器的收发器上发送。接下来使用 示波器和逻辑分析仪实际的抓起波形分析,具体如下:

上图为按字节写的波形,波形符合 PCF8574AT 协议规范。

上图可以看出逻辑分析仪对按字节写的波形的解读,符合规范要求。


PCF8574AT 使用

有了上面的接口之后,开发者可以在用户空间参考下面代码对 PCF8574AT 使用:

在上面的代码中,可以在 PCF8574AT 的 GPIO1 上接一个 LED 灯, 以此直观的感受 GPIO 的状态变化,如下图:


PCF8574AT BiscuitOS-RaspberryPi 中使用

BiscuitOS 以及完整支持 PCF8574AT,并基于 Kbuild 编译系统,制作了一套 便捷的 PCF8574AT 开发环境,开发者可以参考如下步骤进行快速开发。


PCF8574AT 源码获取

首先开发者应该准备基于 RaspberryPi 4B BiscuitOS 开发环境,并 准备好一个在 RaspberryPi 上运行的 BiscuitOS,然后使用如下命令 获得 PCF8574AT 所需的开发环境:

cd BiscuitOS
make RaspberryPi_4B_defconfig
make menuconfig

选择 “Package —>” 并进入下一级菜单

选择 “I2C: Inter-Integrated Circuit —>” 并进入下一级菜单

设置 “PCF8574 IO Expander Application —>” 为 “Y”。设置完毕之后, 保存并退出,


PCF8574AT 源码编译

PCF8574AT 的编译很简单,只需执行如下命令就可以快速编译:

make
cd BiscuitOS/output/RaspberryPi_4B/package/pcf8574_app-0.0.1/
make prepare
make download
make
make install
make pack


PCF8574AT 模块安装

接下来是将模块更新到 RaspberryPi 4B 上,更新的方式多种方式,可以参考下面 方式:


PCF8574AT 应用程序的使用

最后就是在 BiscuitOS RaspberryPi 4B 平台上加载模块,可以参考如下命令:

cd /lib/modules/5.0.21-v7l\+/extra
insmod i2c-bcm2835.ko
insmod i2c-dev.ko
pcf8574_app-0.0.1


PCF8574AT 工程实践部署

开发者也可以在自己的工程中部署 PCF8574AT 应用程序,可以参考如下步骤:


PCF8574AT 源码获取

首先获得 PCF8574AT 应用源码,使用如下命令:

wget https://gitee.com/BiscuitOS_team/HardStack/raw/Gitee/Device-Driver/i2c/Device/PCF8574/Base/userland/pcf8574.sh
wget https://gitee.com/BiscuitOS_team/HardStack/raw/Gitee/Device-Driver/i2c/Device/PCF8574/Base/userland/pcf8574.c
wget https://gitee.com/BiscuitOS_team/HardStack/raw/Gitee/Device-Driver/i2c/Device/PCF8574/Base/userland/Makefile

PCF8574AT 源码编译

获得上面的源码之后,开发者只需提供编译工具,就可以直接编译源码, 此时对 Makefile 文件进行修改,其中 CROSS_COMPILE 变量指向交叉编 译工具链所在的位置。

设置完毕之后,使用如下命令:

make
make install

PCF8574AT 应用安装

接下来是将应用更新到项目平台上,更新的方式多种方式,可以参考下面 方式:


PCF8574AT 应用使用

最后就是在开发平台上加载模块,可以参考如下命令:

cd /lib/modules/5.0.21-v7l\+/extra
ls
insmod i2c-bcm2835.ko
insmod i2c-dev.ko
pcf8574_app-0.0.1


PCF8574AT 问题合集


PCF8574AT 中断处理函数读操作引起的 Panic

在使用 PCF8574 的中断实践中,编写中断处理函数,在中断处理中添加了 对 PCF8574 的 8-bit GPIO 读操作。其实践代码以及实践过程参考下面内容:

基于上面的文档,使用一个 Button 连接中断,具体连接如下图:

BiscuitOS 已经集成了问题源码,开发者可以参考下列内容进行获得:

中断处理函数源码如下图:

从上面的中断处理函数可以得知,在处理的中断的时候,运行了低速的 外部 I/O 访问。基于上面的代码,在树莓派上运行上面的代码,当产生 一个中断的时候,系统打印的信息如下:

完整错误 log 如下:


问题分析

上面的程序存在一个致命的错误,中断处理函数中访问了低速的外围设备, 这在程序设计中是不应该的,中断处理函数秉持能多块返回就多块返回的 原则,而此处中断处理函数在规定时间内没有返回,那么系统直接报错。 为了解决刚刚上面发生的问题,可以考虑将耗时的处理放到中断下半部进行 处理,而中断上半部则为了更快速的返回。内核提供了几种机制满足 目前问题的需求,这里采用 workqueue 机制,其逻辑就是在中断发生 之后,中断只处理快速的操作,耗时的操作都放到 workqueue 里处理, 代码修改如下:

再次再树莓派上运行驱动,这次驱动正常的处理中断,读取了 GPIO 的值。


问题总结

外围设备经常包含中断机制,但在编写中断处理函数的时候,一定不要 将低速的操作放在中断处理函数中。


PCF8574AT 进阶研究


PCF8574AT 中断使用问题

课题简介

在 PCF8574AT 8-bit I/O 状态改变的情况下,会触发一个中断,因此在 有需要使用 GPIO 中断的应用中,可以参照本节内容。为了更好的演示中 断的使用,这里使用一个按钮,并将其接到 PCF8574AT 任意一个 GPIO 上, 如下图:

按钮不按下的时候输出高电平,按下的时候输出低电平,输出低电平。


课题分析

为了能介绍 PCF8574AT 8-bit IO Expander 中断的使用方法,这里 使用一个完整的驱动进行讲解,其原理就是讲 Button 连接到 PCF8574 任意一个 GPIO 上,然后讲 PCF8574AT 的 INT 引脚连接 到树莓派的 GPIO25 上,并设置了上升沿触发中断。每当中断产生, 驱动就会读取 PCF8574AT 8-bit GPIO 状态。


课题实践

本例中使用一个完整的驱动介绍中断的使用,如下:

驱动的基础流程是注册了为一个 platform 驱动,并从 DTS 获得中断所连接 GPIO 对应的中断,然后讲这个中断注册到内核中。在中断处理函数中,由于 通过 I2C 读取 PCF8574 的速度特别慢,因此如果该操作放到中断中处理会引起 错误,因此这里使用一个 WorkQueue,讲 PCF8574 读操作放到 WorkQueue 的处理 函数中,即中断处理下半部处理 PCF8574 读操作。具体处理如下图:

在 PCF8574AT 的中断处理函数中,只是简单的调用 schedule_work() 函数, 接着就中断返回,然后把慢速的 PCF8574 读操作放到了 wq_isr() 处理函数 里面了。

该驱动同样也集成到 BiscuitOS 里,开发者可以参考下面章节进行驱动 的使用:

基于上面的章节,驱动程序的选择如下图:

将上面的驱动编译并安装到树莓派之后,每当按下按钮之后, 串口打印的数据如下:


PCF8574AT 工程应用

工程实践中经常使用 PCF8574AT 用于特定的任务,本节用于介绍 PCF8574AT I/O Expander的工程应用:


交通灯

开发者经常在马路上看到交通灯,交通灯按交通规则点亮制定的灯。本节用于 介绍使用 PCF8574AT 模块制作一个简易的交通灯。


实现原理

交通灯由三个灯组成,分别为红、绿、黄组成,每个灯根据交通规则 亮不同的时长。其工作原理所有灯由二极管组成,所有二极管的 GND 引脚都接到一起共地,然后正极分别接到控制引脚上,如果需要点亮, 只需讲该灯的控制引脚输出高电平即可,如果想让灯更亮,那么需要 给灯的正极接上拉。本节将三个颜色的灯接到 PCF8574AT 的三个 GPIO 上,并按照一个规则给不同的颜色灯上点并点亮。


实践部署

BiscuitOS 支持一键部署 “交通灯”, 开发者请参考如下命令:

cd BiscuitOS
make RaspberryPi_4B_defconfig
make menuconfig

选择并进入 “Package —>”

选择 “I2C: Inter-Integrated Circuit —>” 并进入下一级菜单

选择并进入 “PCF8574 Traffic Light Application —>” 最后保存并退出,执行下列命令:

cd BiscuitOS
make
cd BiscuitOS/output/RaspberryPi_4B/package/pcf8574_traffic-0.0.1/
make prepare
make download
make
make install
make pack

至此,项目编译已经完成,接下来的将 “交通灯” 安装到指定硬件平台, 本文例子运行在 RaspberryPi 4B 上,具体应用程序安装请参考如下内容:


使用说明

将应用程序安装到硬件平台后,使用如下命令进行使用:

pcf8574_traffic-0.0.1


附录

BiscuitOS Home

BiscuitOS Driver

BiscuitOS Kernel Build

Linux Kernel

Bootlin: Elixir Cross Referencer

搭建高效的 Linux 开发环境

捐赠支持一下吧 🙂

MMU