SwiftyGPIO

用于 Linux/ARM 板卡上硬件项目的 Swift 库,支持 GPIO/SPI/I2C/PWM/UART/1-Wire。

概述

该库提供了一种在 Linux 系统上使用 Swift 语言,轻松与外部传感器和设备交互的方式,利用了 Raspberry Pi 等开发板提供的数字 GPIO、SPI/I2C 接口、1-Wire 总线、PWM 信号和串行端口。

就像 Android Things 或 Python 中类似的库一样,SwiftyGPIO 提供了控制各种设备所需的基本功能:传感器、显示器、输入设备(如游戏手柄)、RGB LED 灯条和矩阵。

您将能够配置端口属性并读取/写入当前 GPIO 值,使用 SPI 接口(如果您的开发板提供硬件 SPI,则通过硬件方式,否则使用软件位 bang SPI),通过总线与 I2C 通信,生成 PWM 以驱动外部显示器、伺服电机、LED 和更复杂的传感器,使用 AT 命令或自定义协议与暴露 UART 串行连接的设备交互,最后连接到 1-Wire 设备。

虽然您仍然可以使用 Xcode 或其他 IDE 开发项目,但该库专门构建为在 Linux ARM 开发板(Raspberry Pi、BeagleBone、ODROID、Orange Pi 等)上运行。

下面列出了使用 SwiftyGPIO 构建的 设备库完整项目 的示例,您可以将其作为自己 DIY 硬件项目的灵感来源,祝您玩得开心!

内容

支持的开发板

以下开发板经过测试,支持最新版本的 Swift

但基本上所有具有 ARMv7/8+Ubuntu/Debian/Raspbian 或 ARMv6+Raspbian/Debian 的设备,如果您可以在其上运行 Swift,都应该可以工作。

请记住,ARM 上的 Swift 完全是由社区驱动的,并且存在多种可能的开发板+操作系统配置,即使大多数时候都能正常工作,也不要期望在每种配置上都能立即工作,特别是如果您是第一个尝试新配置或开发板的人。

安装

要使用此库,您需要一个运行 Swift 3.x/4.x/5.x 的 Linux ARM (ARMv7/8 或 ARMv6) 开发板。

如果您有运行 Ubuntu 或 Raspbian 的 Raspberry Pi (A,B,A+,B+,Zero,ZeroW,2,3,4),请从此处获取 Swift 5.x,或按照 buildSwiftOnARM 中的说明在几个小时内自行构建。

我始终建议先尝试最新的可用二进制文件(Ubuntu 或 Raspbian),然后再花时间自己编译,这些二进制文件可能(并且大多数时候确实)也适用于其他基于 Debian 的发行版和不同的开发板。

在 Raspberry Pi 上获取这些 Swift 二进制文件的另一种方法是通过 Swift on Balena 项目,该项目提供了组织良好的、面向 IoT 的 Docker 镜像。

您还可以设置交叉编译工具链并从 Mac 构建 ARM 二进制文件 (Ubuntu/Raspbian),再次感谢 Helge Heß(以及 Johannes Weiß 在 SPM 中实现它)的工作,请此处阅读更多相关信息。

要开始您的项目,请将 SwiftyGPIO 添加为 Package.swift 中的依赖项

// swift-tools-version:4.0
import PackageDescription

let package = Package(
    name: "light",
    dependencies: [
         .package(url: "https://github.com/uraimo/SwiftyGPIO.git", from: "1.0.0")
    ]
)

然后使用 swift build 进行构建。

编译器将在 .build/debug/MyProject 下创建一个可执行文件。

重要提示: 与每个使用 GPIO/SPI/I2C/etc... 的库一样,如果您的操作系统没有预定义的用户组来访问这些功能,您需要使用 root 权限运行您的应用程序,即使用 sudo。如果您使用的是带有 Raspbian 或最新 Ubuntu(16.04 Xenial 及更高版本)的 Raspberry Pi,并且实现了 /dev/gpiomem,则使用基本 GPIO 不需要 sudo,只需启动编译器构建的可执行文件即可。

在配置错误的系统上,监听器等功能也可能需要 root 权限,而 PWM 等高级功能始终需要 root 权限。

或者,可以手动配置用于 gpio 访问的特定用户组,如此处或此 stackoverflow 上的答案所示。按照这些说明操作后,请记住使用 sudo usermod -aG gpio pi 将您的用户(例如 pi)添加到 gpio 组,并重新启动,以便应用您所做的更改。

您的第一个项目:闪烁的 LED 和传感器

如果您更喜欢从实际项目开始,而不是仅仅阅读文档,您将在 Examples/ 下找到一些随时可运行的示例,以及一些教程、完整项目和在线视频

用法

目前,SwiftyGPIO 公开了 GPIO、SPI(如果不可用,可以创建位 bang VirtualSPI)、I2C、PWM、1-Wire 和 UART 端口,让我们看看如何使用它们。

GPIO

假设我们正在使用 Raspberry Pi 3 开发板,并且在 GPIO 引脚 P2(可能在两者之间串联一个 1K 欧姆左右的电阻)和 GND 之间连接了一个 LED,我们想将其打开。

请注意,SwiftyGPIO 使用原始博通编号方案此处描述)为每个引脚分配一个编号。

首先,我们需要检索开发板上可用的 GPIO 列表,并获取我们要修改的 GPIO 的引用

import SwiftyGPIO

let gpios = SwiftyGPIO.GPIOs(for:.RaspberryPi3)
var gp = gpios[.P2]!

以下是预定义开发板的可能值

GPIOs(for:) 返回的映射包含特定开发板的所有 GPIO,如 这些图表所述。

或者,如果我们的开发板不受支持,则可以手动实例化每个单独的 GPIO 对象,使用其 SysFS GPIO ID

var gp = GPIO(name: "P2",id: 2)  // User defined name and GPIO Id

下一步是配置端口方向,可以是 GPIODirection.INGPIODirection.OUT,在本例中,我们将选择 .OUT

gp.direction = .OUT

然后我们将引脚值更改为高电平值 "1"

gp.value = 1

就是这样,LED 将会亮起。

现在,假设我们有一个开关或按钮连接到 P2,要读取进入 P2 端口的值,方向必须配置为 .IN,并且可以从 value 属性读取值

gp.direction = .IN
let current = gp.value

某些开发板(如 Raspberry Pi)允许在某些 GPIO 引脚上启用上拉/下拉电阻,以便在外部设备断开连接时默认将引脚连接到 3.3V (.up)、0V (.down) 或使其浮空 (.neither),要启用它,只需设置 pull 属性

gp.direction = .IN
gp.pull = .up

拉状态只能设置,不能读回。

GPIO 对象上的其他可用属性(edge、active low)指的是可以配置的 GPIO 的附加属性,但在大多数情况下您不需要它们。有关详细说明,请参阅 内核 sysfs 文档

GPIO 对象还支持在引脚值更改时执行闭包。可以使用方法 onRaising(引脚值从 0 变为 1)、onFalling(值从 1 变为 0)和 onChange(值只是从前一个值更改)添加闭包

let gpios = SwiftyGPIO.GPIOs(for:.RaspberryPi3)
var gp = gpios[.P2]!


gp.onRaising{
    gpio in
    print("Transition to 1, current value:" + String(gpio.value))
}
gp.onFalling{
    gpio in
    print("Transition to 0, current value:" + String(gpio.value))
}
gp.onChange{
    gpio in
    gpio.clearListeners()
    print("The value changed, current value:" + String(gpio.value))
}  

闭包接收对其唯一参数的引用,即已更新的 GPIO 对象,因此您无需使用外部变量。调用 clearListeners() 会删除所有监听更改的闭包并禁用更改处理程序。在检查 GPIO 更新时,无法更改引脚的 direction(并配置为 .IN),但是一旦在闭包内部或其他位置清除了监听器,您就可以自由修改它。

设置 bounceTime 属性将启用软件防抖,这将限制通知给闭包的转换次数,在指定的秒时间间隔内只允许一个事件。

以下示例仅允许每 500 毫秒进行一次转换

let gpios = SwiftyGPIO.GPIOs(for:.RaspberryPi3)
var gp = gpios[.P2]!

gp.bounceTime = 0.5
gp.onRaising{
    gpio in
    print("Transition to 1, current value:" + String(gpio.value))
} 

当使用开关时,此功能非常有用,因为开关的机械特性,在按下开关时往往会产生多个值尖峰。

SPI

如果您的开发板具有 SPI 连接,并且 SwiftyGPIO 在其预设中包含它,则可以通过使用预定义的开发板之一调用 hardwareSPIs(for:) 来获得可用 SPI 通道的列表。

在 Raspberry Pi 和其他开发板上,硬件 SPI SysFS 接口默认情况下未启用,请查看 wiki 上的设置指南,以了解如何使用 raspi-config 在需要时启用它。

让我们看一些使用 Raspberry Pi 3 的示例,它具有两个双向 SPI,由 SwiftyGPIO 管理为两个 SPIObject

let spis = SwiftyGPIO.hardwareSPIs(for:.RaspberryPi3)!
var spi = spis[0]

该接口由 3 根线组成:时钟线 (SCLK)、输入线 (MISO) 和输出线 (MOSI)。一个或多个 CS 引脚(具有反向逻辑)可用于启用或禁用从设备。

或者,我们可以使用四个 GPIO 创建一个软件 SPI,其中一个将用作时钟引脚 (SCLK),一个用作片选 (CS 或 CE),另外两个将用于发送和接收实际数据(MOSI 和 MISO)。这种位 bang SPI 比硬件 SPI 慢,因此,推荐的方法是在可用时使用硬件 SPI。

要创建软件 SPI,只需检索两个引脚并创建一个 VirtualSPI 对象

let gpios = SwiftyGPIO.GPIOs(for:.RaspberryPi3)
var cs = gpios[.P27]!
var mosi = gpios[.P22]!
var miso = gpios[.P4]!
var clk = gpios[.P17]!

var spi = VirtualSPI(mosiGPIO: mosi, misoGPIO: miso, clockGPIO: clk, csGPIO: cs)

这两个对象都实现了相同的 SPIObject 协议,因此提供了相同的方法。要区分硬件和软件 SPIObject,请使用 isHardware 属性。

要通过 SPI 发送一个或多个字节,请使用 sendData 方法。在其最简单的形式中,它只需要一个 UInt8 数组作为参数

spi?.sendData([UInt(42)], frequencyHz: 500_000)

如果需要,可以指定发送数据的频率(或者将使用默认值,硬件 SPI 为 500khz,虚拟 SPI 为最佳可用速度)。

由于接口仅执行全双工传输,要从 SPI 读取一些数据,您需要写入相同数量的位。对于大多数设备,您将使用这意味着您需要发送一些虚拟数据,具体取决于您的设备使用的协议。有关更多信息,请查看设备参考。

让我们看一个简单的示例,它从设备读取 32 个字节,仅发送 32 个空字节

let data = [ UInt8 ](repeating: 0, count: 32)
let res  = spi?.sendDataAndRead(data)

res 数组将包含从设备接收的原始数据。同样,要发送什么以及应如何解释接收到的数据取决于您正在使用的设备或 IC,请始终阅读参考手册。

I2C

I2C 接口可用于在 I2C 总线上使用 SMBus 协议进行通信,读取或写入由数字地址标识的设备上的寄存器。此接口只需要两根线(时钟和数据),并且与 SPI 不同,它不需要专用的片选/使能线来选择哪个设备将接收发送的信号,因为协议消息的目的地地址包含在消息本身中,这是一个很大的改进。

要获取 I2CInterface 对象的引用,请调用 SwiftyGPIO 类的 hardwareI2Cs(for:) 实用程序方法

let i2cs = SwiftyGPIO.hardwareI2Cs(for:.RaspberryPi3)!
let i2c = i2cs[1]

在 Raspberry Pi 和其他开发板上,此接口可能默认情况下未启用,请始终验证其状态,查看 wiki 上的设置指南,以了解如何使用 raspi-config 在需要时启用它。

此对象提供了读取和写入不同大小的寄存器的方法,并验证是否可以访问特定地址的设备或在协议消息上启用 CRC

func isReachable(_ address: Int) -> Bool
func setPEC(_ address: Int, enabled: Bool)

您应该根据您的设备是否支持多个寄存器(SMBus 术语中的 command)以及您要从中读取的寄存器的大小来选择要使用的读取方法

func readByte(_ address: Int) -> UInt8
func readByte(_ address: Int, command: UInt8) -> UInt8
func readWord(_ address: Int, command: UInt8) -> UInt16
func readData(_ address: Int, command: UInt8) -> [UInt8]
func readI2CData(_ address: Int, command: UInt8) -> [UInt8]

读取和写入数据块支持两种模式,一种标准 SMBus 模式(readDatawriteData),它在实际数据之前添加块的长度,另一种旧式 I2C 模式(readI2CDatawriteI2CData),它只发送数据,没有额外的元数据。根据设备的不同,可能只支持两种模式中的一种。

假设我们要从 DS1307 RTC 时钟读取秒寄存器(ID 为 0),其 I2C 地址为 0x68

print(i2c.readByte(0x68, command: 0)) //Prints the value of the 8bit register

您应该以相同的方式选择可用的写入函数之一,只需注意 writeQuick 用于执行快速命令,并且不执行正常的写入。SMBus 的快速命令通常用于打开/关闭设备或执行不需要额外参数的类似任务。

func writeQuick(_ address: Int)

func writeByte(_ address: Int, value: UInt8)
func writeByte(_ address: Int, command: UInt8, value: UInt8)
func writeWord(_ address: Int, command: UInt8, value: UInt16)
func writeData(_ address: Int, command: UInt8, values: [UInt8])
func writeI2CData(_ address: Int, command: UInt8, values: [UInt8])

虽然使用 I2C 功能不需要额外的软件即可运行,但 i2c-tools 中包含的工具可用于手动执行 I2C 事务,以验证一切是否正常工作。

例如,我建议始终检查您的设备是否已正确连接,运行 i2cdetect -y 1。有关 I2C 的更多信息以及 Raspberry Pi 的配置说明,请访问 Sparkfun

Example/ 目录包含 i2cdetect 的 Swift 实现,可能是开始实验的好地方。

docs/ 目录包含一个简单的指南,用于 调试 I2C 设备通信问题

PWM

PWM 输出信号可用于驱动伺服电机、RGB LED 和其他设备,或者更一般地,在只有数字 GPIO 端口时,近似模拟输出值(例如,生成的值就像在 0V 和 3.3V 之间 一样)。

如果您的开发板具有 PWM 端口并且受支持(目前仅支持 Raspberry Pi 开发板),请使用 hardwarePWMs 工厂方法检索可用的 PWMOutput 对象

let pwms = SwiftyGPIO.hardwarePWMs(for:.RaspberryPi3)!
let pwm = (pwms[0]?[.P18])!

此方法返回所有支持 PWM 功能的端口,按控制它们的 PWM 通道分组。

每个通道您只能使用一个端口,考虑到 Raspberry Pi 有两个通道,您将能够同时使用两个 PWM 输出,例如 GPIO12 和 GPIO13 或 GPIO18 和 GPIO19。

检索到您计划使用的端口的 PWMOutput 后,您需要初始化它以选择 PWM 功能。在这种开发板上,每个端口可以有多个功能(简单 GPIO、SPI、PWM 等),您可以选择您想要的功能,配置专用的寄存器。

pwm.initPWM()

要启动 PWM 信号,请调用 startPWM,提供周期(以纳秒为单位)(如果您有频率,请使用 1/频率进行转换)和占空比(以百分比表示)

print("PWM from GPIO18 with 500ns period and 50% duty cycle")
pwm.startPWM(period: 500, duty: 50)

一旦您调用此方法,ARM SoC 的 PWM 子系统将开始生成信号,您无需执行任何其他操作,您的程序将继续执行,如果您只想等待,可以在此处插入 sleep(seconds)

当您想停止 PWM 信号时,调用 stopPWM() 方法

pwm.stopPWM()

如果您想更改正在生成的信号,则无需停止之前的信号,只需使用不同的参数调用 startPWM 即可。

此功能使用 M/S 算法,并已在周期范围为 300ns 到 200ms 的信号上进行了测试,生成超出此范围的信号可能会导致过度的抖动,这对于某些应用来说可能是不可接受的。如果您需要生成接近该范围极端的信号,并且手头有示波器,请始终验证结果信号是否足够满足您的需求。

基于模式的信号发生器(通过 PWM)

此功能利用 PWM 根据两个模式生成数字信号,这两个模式通过占空比的变化来表示 0 或 1 值。让我们看一个实际示例,以更好地理解用例以及如何使用此信号发生器

例如,让我们考虑 WS2812/NeoPixel(请参阅专用 ),这是一种带有集成驱动器的 LED,用于许多 LED 灯条。

此 LED 通过 400Khz 到 800Khz 之间的信号激活,该信号包含一系列编码的 3 字节值,分别代表绿色、色和色分量,每个 LED 一个。颜色分量字节的每个位都必须以这种方式编码

一旦发送了 LED 灯条的整个颜色序列,您需要在电压保持在 0 50us 后,才能传输新的序列。发送的字节将配置灯条的 LED,从最后一个 LED 开始,向后到第一个 LED。

来自官方文档的这张图表让您更好地了解这些信号的外观,基于先前定义的 T0H、T0L、T1H、T1L

ws2812 timings

您可能会想到仅根据那些 0 和 1 模式更改 GPIO 的值来发送此信号,但实际上 ARM 开发板不可能跟上 WS2812 LED 等设备所需的速度,并且尝试在软件中生成这些信号也会引入明显的抖动。

一旦模式周期低于 100us 左右,您就需要另一种发送这些信号的方式。

这就是基于模式的信号发生器解决的问题,它利用了支持 PWM 的输出引脚。

您将在 Examples/PWMPattern 下找到一个完整的示例,但让我们描述使用此功能所需的每个步骤。

在本简短指南中,我使用的是带有 64 个 WS2812 LED 的 8x8 LED 矩阵(这些矩阵通常以 NeoPixel 矩阵、Nulsom Rainbow 矩阵等名称销售,您可以在一些 Pimoroni 产品(如 UnicornHat)中找到其中一种)。

首先,让我们检索一个 PWMOutput 对象,然后初始化它

let pwms = SwiftyGPIO.hardwarePWMs(for:.RaspberryPi3)!
let pwm = (pwms[0]?[.P18])!

// Initialize PWM
pwm.initPWM()

然后,我们将配置信号发生器,指定我们需要的频率(1250ns 模式周期为 800KHz)、序列中 LED 的数量(我在这里使用 8x8 LED 矩阵)以及复位时间(55us)的持续时间。我们将调用 initPWMPattern 来配置这些参数。我们指定 0 和 1 值的占空比(模式应具有高电平值的周期百分比)。

let NUM_ELEMENTS = 64
let WS2812_FREQ = 800000 // 800Khz
let WS2812_RESETDELAY = 55  // 55us reset

pwm.initPWMPattern(bytes: NUM_ELEMENTS*3, 
                   at: WS2812_FREQ, 
                   with: WS2812_RESETDELAY, 
                   dutyzero: 33, dutyone: 66) 

完成此操作后,我们可以开始发送数据,这次我们使用一个设置颜色的函数和另一个将它们转换为 GBR 格式的 UInt8 序列的函数

func toByteStream(_ values: [UInt32]) -> [UInt8]{
    var byteStream = [UInt8]()
    for led in values {
        // Add as GRB, converted from RGB+0x00
        byteStream.append(UInt8((led >> UInt32(16))  & 0xff))
        byteStream.append(UInt8((led >> UInt32(24)) & 0xff))
        byteStream.append(UInt8((led >> UInt32(8))  & 0xff))
    }
    return byteStream
}

var initial = [UInt32](repeating:0x50000000, count:NUM_ELEMENTS)
var byteStream: [UInt8] = toByteStream(initial)

pwm.sendDataWithPattern(values: byteStream)

方法 sendDataWithPatter 将使用 UInt8 序列来生成由上述模式组成的信号。

然后我们可以等待信号完全发送,然后执行必要的最终清理

// Wait for the transmission to end
pwm.waitOnSendData()

// Clean up once you are done with the generator
pwm.cleanupPattern()

此时,如果您想配置不同的信号,可以再次调用 initPWMPattern

UART

如果您的开发板支持 UART 串行端口功能(对于 Raspberry Pi 开发板,请使用 raspi-config 禁用串行登录),您可以使用 SwiftyGPIO.UARTs(for:) 检索可用 UARTInterface 的列表

let uarts = SwiftyGPIO.UARTs(for:.RaspberryPi3)!
var uart = uarts[0]

在 Raspberry Pi 和其他开发板上,此接口可能默认情况下未启用,请始终验证其状态,查看 wiki 上的设置指南,以了解如何使用 raspi-config 在需要时启用它。

在我们可以开始传输数据之前,您需要配置串行端口,指定:速度(从 9600bps 到 115200bps)、字符大小(每个字符 6、7 或 8 位)、停止位数量(1 或 2)和信号的奇偶校验(无奇偶校验、奇校验或偶校验)。使用此库时,软件和硬件流控制均已禁用。

uart.configureInterface(speed: .S9600, bitsPerChar: .Eight, stopBits: .One, parity: .None)

配置端口后,您可以使用 UARTInterface 的特定方法之一开始读取或写入字符串或 UInt8 序列

func readString() -> String
func readData() -> [CChar]
func writeString(_ value: String)
func writeData(_ values: [CChar])

func hasAvailableData() throws -> Bool
func readLine() -> String

还提供了一种方法来了解 UART 串行端口上是否有可用数据,以及一种读取文本行的特定方法(\n 用作行终止符,串行读取仍然是非规范的)。

1-Wire

如果您的开发板提供 1-Wire 端口(目前仅限 Raspberry Pi 开发板),您可以使用 SwiftyGPIO.hardware1Wires(for:) 检索可用 OneWireInterface 的列表

let onewires = SwiftyGPIO.hardware1Wires(for:.RaspberryPi3)!
var onewire = onewires[0]

要检索与连接到 1-Wire 总线的设备关联的字符串标识符,请调用 getSlaves()

然后可以使用在上一步中获得的标识符之一,通过 readData(slaveId:) 检索这些设备提供的数据。

来自设备的数据由 Linux 驱动程序作为一系列行返回,其中大多数行只是协议数据,您应该忽略它们。查看您的传感器参考,了解如何解释格式化信息。

示例

Examples 目录中提供了针对不同开发板和功能的示例,您可以直接从那里开始,修改其中一个。

以下示例旨在在 C.H.I.P. 开发板上运行,显示单个 GPIO 端口的所有属性的当前值,更改方向和值,然后再次显示属性的摘要

let gpios = SwiftyGPIO.GPIOs(for:.CHIP)
var gp0 = gpios[.P0]!
print("Current Status")
print("Direction: "+gp0.direction.rawValue)
print("Edge: "+gp0.edge.rawValue)
print("Active Low: "+String(gp0.activeLow))
print("Value: "+String(gp0.value))

gp0.direction = .OUT
gp0.value = 1

print("New Status")
print("Direction: "+gp0.direction.rawValue)
print("Edge: "+gp0.edge.rawValue)
print("Active Low: "+String(gp0.activeLow))
print("Value: "+String(gp0.value))

第二个示例使 LED 以 150 毫秒的频率闪烁

import Glibc

let gpios = SwiftyGPIO.GPIOs(for:.CHIP)
var gp0 = gpios[.P0]!
gp0.direction = .OUT

repeat{
	gp0.value = (gp0.value == 0) ? 1 : 0
	usleep(150*1000)
}while(true) 

我们无法使用 CHIP 测试硬件 SPI,但 SwiftyGPIO 也提供了 SPI 接口的位 bang 软件实现,您只需要两个 GPIO 即可初始化它

let gpios = SwiftyGPIO.GPIOs(for:.CHIP)
var sclk = gpios[.P0]!
var dnmosi = gpios[.P1]!

var spi = VirtualSPI(dataGPIO:dnmosi,clockGPIO:sclk) 

pi.sendData([UInt8(truncatingBitPattern:0x9F)]) 

请注意,我们正在使用构造函数 UInt8(truncatingBitPattern:) 转换 0x9F Int,在这种情况下,实际上不需要这样做,但建议每个用户提供的或计算的整数都这样做,因为 Swift 不支持隐式截断以转换为较小的整数类型,如果您尝试转换的 Int 不适合 UInt8,它只会崩溃。

使用 SwiftyGPIO 构建

一些使用 SwiftyGPIO 构建的项目和库。您是否构建了想要分享的东西?请告诉我!

特定设备的库。

优秀项目

完整的 IoT 项目。

支持库

可能对您的 IoT 项目有用的附加库。

附加文档

其他文档,主要是实现细节,可以在 docs 目录中找到。