设计指南

本指南涵盖了 CircuitPython 核心和库 API 的各种开发实践。这些 API 既 内置于 CircuitPython 中,也 分布在 GitHub 以及Adafruit社区包中。与这些实践的一致性确保初学者可以学习一次模式并将其应用于整个 CircuitPython 生态系统。

使用 cookiecutter 启动库

Cookiecutter 是一种工具,可让您根据另一个存储库引导新存储库。我们在这里为 CircuitPython 库制作了一个,其中包括 Travis CI 和 ReadTheDocs 的配置以及 setup.py、许可证、行为准则、自述文件等文件。

Cookiecutter 将提供一系列与库相关的提示,然后创建一个包含所有文件的新目录。有关更多详细信息,请参阅the CircuitPython cookiecutter README

模块命名

Adafruit 资助的库应该在 adafruit 组织下, 并具有格式 Adafruit_CircuitPython_<name> 和相应的adafruit_<name>目录(又名包)或adafruit_<name>.py文件(又名模块)。

如果名称通常有一个空格,例如“Thermal Printer”,请使用下划线代替(“Thermal_Printer”)。即使“adafruit”和“circuitpython”之间的分隔是用 -. 在 cookiecutter 提示中使用下划线。

社区创建的库应该具有 repo 格式 CircuitPython_<name>并且没有adafruit_ 模块或包前缀。

两者都应该在 GitHub 上有 CircuitPython 存储库主题。

术语

正如我们的行为准则所述,我们努力使用“欢迎和包容的语言”。无论是在文档中还是在代码中,我们使用的词语都很重要。这意味着我们不喜欢由于历史和社会背景而使社区成员和潜在社区成员感到不受欢迎的语言。

除非技术限制需要,否则有一些特定的术语需要避免。虽然特定情况可能需要其他术语,但请先考虑使用这些建议的术语:

首选

已弃用

主要(设备)

掌握

外设

奴隶

传感器

次要(设备)

拒绝名单

黑名单

许可名单

白名单

请注意,“技术限制”是指例如上游库或 URL 必须包含这些子字符串才能工作的情况。但是,当涉及到 CircuitPython 中的文档以及参数和属性的名称时,我们将使用替代术语,即使这打破了过去实践的传统。

Lifetime 和 ContextManagers

驱动程序应该在构建后初始化并准备好使用。如果设备需要取消初始化,则通过提供它deinit() 并提供__enter____exit__创建可与 with.

例如,用户可以使用 deinit()`:

import digitalio
import board
import time

led = digitalio.DigitalInOut(board.D13)
led.direction = digitalio.Direction.OUTPUT

for i in range(10):
    led.value = True
    time.sleep(0.5)

    led.value = False
    time.sleep(0.5)
led.deinit()

只要没有异常发生,这将在程序结束时取消初始化底层硬件。

或者,使用with 语句确保硬件被取消初始化:

import digitalio
import board
import time

with digitalio.DigitalInOut(board.D13) as led:
    led.direction = digitalio.Direction.OUTPUT

    for i in range(10):
        led.value = True
        time.sleep(0.5)

        led.value = False
        time.sleep(0.5)

Python 的with 语句确保 deinit 代码运行,而不管 with 语句中的代码是否无异常执行。

对于像示例这样的小程序,这不是一个主要问题,因为在程序运行或 REPL 运行后,所有用户可用的硬件都会重置。但是,对于可能间歇性地使用硬件并且也可能自己处理异常的更复杂的程序,使用 with 语句取消初始化硬件将确保硬件的启用时间不会超过所需时间。

验证您的设备

只要有可能,请确保您正在通话的设备是您期望的设备。如果不是,则引发 RuntimeError。请注意,不同设备上的 I2C 地址可能相同,因此请读取您知道的寄存器以确保它们符合您的期望。预先验证这一点将有助于发现错误。

吸气剂/吸气剂

在为设备设计驱动程序时,使用设备状态的属性并使用设备执行的抽象操作序列的方法。状态是设备作为一个整体存在的属性,无论代码在做什么。这包括温度、时间、声音、光线和开关状态等内容。有关更完整的列表,请参阅下面的传感器属性项目符号。

另一种将状态与动作分开的方法是,状态通常是用户可以通过视觉或感觉来感知自己的东西。动作是用户可以观看的东西。设备执行此操作,然后执行此操作。

让用户清楚地了解这种分离将有助于初学者了解何时使用什么。

这里有更多关于 Python属性的信息 。

异常和断言

每当关键测试或其他条件失败时,引发适当的Exception以及有用的消息。

例子:

if not 0 <= pin <= 7:
    raise ValueError("Pin number must be 0-7.")

如果内存受限并且需要更紧凑的方法,请 改用。The assert statement instead.

例子:

assert 0 <= pin <= 7, "Pin number must be 0-7."

与 CPython 兼容的设计

CircuitPython 旨在成为人们第一次使用代码的体验。这将是进入硬件和软件世界的第一步。为了简化第一步的探索,请确保与 CPython 共享的功能共享相同的 API。它不需要是完整的 API,它可以是一个子集。但是,不要将非 CPython API 添加到相同的模块中。相反,使用单独的非 CPython 模块来添加额外的功能。通过区分模块中的 API 边界,您可以增加在导入时发现错误预期的可能性,而不是在运行时随机发现。

当为与 CPython 模块相关的附加功能添加新模块时,不要简单地在它前面加上 u。这与 CPython 的区别还不够大。这是 MicroPython 约定,它们使用 u* 模块与 CPython 名称互换。这令人困惑。相反,想出一个与您添加的额外功能相关的新名称。

例如,存储挂载和卸载相关功能已从 uosstorage 模块中移出 。终端相关的功能被移到 multiterminal. 这些名称更好地匹配它们的功能并且不与 CPython 名称冲突。确保检查您是否也与 CPython 库不冲突。这样我们将来可以将 API 移植到 CPython。

例子

当向 CircuitPython 添加额外功能以模拟正常操作系统会做什么时,要么复制现有的 CPython API(例如文件写入),要么创建一个单独的模块来实现你想要的。例如,安装和卸载驱动器不是 CPython 的一部分,因此它应该在模块中完成,例如新storage模块,该模块仅在 CircuitPython 中可用。这样,当有人将代码移动到 CPython 时,他们就知道需要调整哪些部分。

文件内联

只要有可能,就在实现它的代码旁边记录您的代码。这使得它更有可能与实现本身保持同步。使用 Sphinx 的自动模块在 ReadTheDocs 中很好地格式化这些。cookiecutter 帮助设置这些。

使用 Sphinx 风格 rST进行标记。

大量的文档是一件好事,但它可能会占用大量空间。为了最大限度地减少磁盘和负载使用的空间,将库分发为 .py 和 .mpy,MicroPython 和 CircuitPython 的省略注释的字节码格式。

模块说明

在许可评论之后:

"""
`<module name>`
=================================================

<Longer description>

* Author(s):

Implementation Notes
--------------------


**Hardware:**

* `Adafruit Device Description
  <hyperlink>`_ (Product ID: <Product Number>)

**Software and Dependencies:**

* CircuitPython中文网 firmware for the supported boards:
  https://circuitpython.org/downloads

* Adafruit's Bus Device library:
  https://github.com/adafruit/Adafruit_CircuitPython_BusDevice

* Adafruit's Register library:
  https://github.com/adafruit/Adafruit_CircuitPython_Register

"""

班级说明

在类级别记录类的作用以及如何初始化它:

class DS3231:
    """DS3231 real-time clock.

       :param ~busio.I2C i2c_bus: The I2C bus the DS3231 is connected to.
       :param int address: The I2C address of the device. Defaults to :const:`0x40`
    """

    def __init__(self, i2c_bus, address=0x40):
        self._i2c = i2c_bus

呈现为:

class DS3231(i2c_bus, address=64)

DS3231 实时时钟。

参数
  • i2c_bus (I2C) – DS3231 所连接的 I2C 总线。

  • address (int) – 设备的 I2C 地址。默认为0x40

记录参数

尽管在 Python 中记录类和函数定义的方法有多种,但以下是记录 CircuitPython 库参数的常用方法。在记录类参数时,您应该使用以下结构:

:param param_type param_name: Parameter_description

参数类型

参数的类型。这可能是除其他int, float, 等。要记录在CircuitPython域中的对象,则需要包括如在下面的例子中示出的定义之前:str bool~

:param ~busio.I2C i2c_bus: The I2C bus the DS3231 is connected to.

为了包含对 CircuitPython 模块的引用,cookiecutter 在conf.py位于 docs 目录中的文件的 intersphinx_mapping 部分中创建了一个条目。要在 CircuitPython 之外添加不同类型,您需要将它们包含在 intersphinx_mapping 中:

intersphinx_mapping = {
    "python": ("https://docs.python.org/3.4", None),
    "BusDevice":("https://circuitpython.readthedocs.io/projects/busdevice/en/latest/", None,),
    "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None),
}

上面的 intersphinx_mapping 包括对 Python、BusDevice 和 CircuitPython 文档的引用

当参数有两种不同的类型时,应按如下方式引用它们:

class Character_LCD:
    """Base class for character LCD

       :param ~digitalio.DigitalInOut rs: The reset data line
       :param ~pwmio.PWMOut,~digitalio.DigitalInOut blue: Blue RGB Anode

    """

    def __init__(self, rs, blue):
        self._rc = rs
        self.blue = blue

呈现为:

class Character_LCD(rs, blue)

字符 LCD 的基类

Parameters

参数名称

类或方法定义中使用的参数名称

参数说明

参数说明。当参数默认为特定值时,最好包含默认值:

:param int pitch: Pitch value for the servo. Defaults to :const:`4500`

属性

属性是对象的状态。(有关何时使用它们的更多讨论,请参阅上面的Getters/Setters 。)它们可以通过多种不同的方式在内部定义。下面列举了每种方法,并解释了注释的去向。

不管属性是如何实现的,它应该有一个简短的描述它代表什么状态,包括类型、可能的值和/或单位。对于不可读和不可写的属性,它应该标记为(read-only)(write-only) 在第一行的末尾。

实例属性

评论来自作业后:

def __init__(self, drive_mode):
    self.drive_mode = drive_mode
    """
    The pin drive mode. One of:

    - `digitalio.DriveMode.PUSH_PULL`
    - `digitalio.DriveMode.OPEN_DRAIN`
    """

呈现为:

drive_mode

引脚驱动模式。之一:

属性描述

评论来自吸气剂:

@property
def datetime(self):
    """The current date and time as a `time.struct_time`."""
    return self.datetime_register

@datetime.setter
def datetime(self, value):
    pass

呈现为:

datetime

当前日期和时间作为time.struct_time.

只读示例:

@property
def temperature(self):
    """
    The current temperature in degrees Celsius. (read-only)

    The device may require calibration to get accurate readings.
    """
    return self._read(TEMPERATURE)

呈现为:

temperature

当前温度以摄氏度为单位。(只读)

设备可能需要校准才能获得准确的读数。

数据描述符描述

注释在定义之后:

lost_power = i2c_bit.RWBit(0x0f, 7)
"""True if the device has lost power since the time was set."""

呈现为:

lost_power

如果自设置时间以来设备断电,则为真。

方法说明

方法定义后的第一行:

def turn_right(self, degrees):
    """Turns the bot ``degrees`` right.

       :param float degrees: Degrees to turn right
    """

呈现为:

turn_right(degrees)

将机器人degrees向右转。

参数

degrees (float) – 向右转的度数

对其他库的文档参考

当您需要引用其他库中的文档时,您应该使用单个反引号引用该类:class:`~adafruit_motor.servo.Servo`。您还必须通过添加新条目在conf.py文件中 添加引用:intersphinx_mapping section

"adafruit_motor": ("https://circuitpython.readthedocs.io/projects/motor/en/latest/", None,),

使用总线设备

BusDevice 是一个很棒的基础库,可以为您管理共享的 I2C 或 SPI 设备。这些设备管理锁定,确保传输作为单个单元完成,尽管 CircuitPython 内部以及将来的其他 Python 线程。对于 I2C,设备还管理设备地址。SPI 设备管理波特率设置、片选线和额外的交易后时钟周期。

I2C 示例

from adafruit_bus_device import i2c_device

DEVICE_DEFAULT_I2C_ADDR = 0x42

class Widget:
    """A generic widget."""

    def __init__(self, i2c, address=DEVICE_DEFAULT_I2C_ADDR):
        self.i2c_device = i2c_device.I2CDevice(i2c, address)
        self.buf = bytearray(1)

    @property
    def register(self):
        """Widget's one register."""
        with self.i2c_device as i2c:
            i2c.writeto(b'0x00')
            i2c.readfrom_into(self.buf)
        return self.buf[0]

SPI 示例

from adafruit_bus_device import spi_device

class SPIWidget:
    """A generic widget with a weird baudrate."""

    def __init__(self, spi, chip_select):
        # chip_select is a pin reference such as board.D10.
        self.spi_device = spi_device.SPIDevice(spi, chip_select, baudrate=12345)
        self.buf = bytearray(1)

    @property
    def register(self):
        """Widget's one register."""
        with self.spi_device as spi:
            spi.write(b'0x00')
            spi.readinto(self.buf)
        return self.buf[0]

类文档示例模板

在记录类时,您应该使用以下模板来说明基本用法。它与 simpletest 示例类似,但是这将显示 Read The Docs 文档中的信息。使用此模板的优势在于它使库中的文档保持一致。

这是 AHT20 温度传感器的示例。在 class 参数后包含以下内容:

"""

**Quickstart: Importing and using the AHT10/AHT20 temperature sensor**

    Here is an example of using the :class:`AHTx0` class.
    First you will need to import the libraries to use the sensor

    .. code-block:: python

        import board
        import adafruit_ahtx0

    Once this is done you can define your `board.I2C` object and define your sensor object

    .. code-block:: python

        i2c = board.I2C()  # uses board.SCL and board.SDA
        aht = adafruit_ahtx0.AHTx0(i2c)

    Now you have access to the temperature and humidity using
    the :attr:`temperature` and :attr:`relative_humidity` attributes

    .. code-block:: python

        temperature = aht.temperature
        relative_humidity = aht.relative_humidity

"""

使用组合

在编写驱动程序时,接受提供您需要的功能的对象,而不是接受它们的参数并自己构造它们或将具有功能的父类子类化。这种技术被称为组合,并导致比传统继承更灵活和可测试的代码。

也可以看看

维基百科 有更多关于“依赖倒置”的信息。

例如,如果您正在为 I2C 设备编写驱动程序,则接收 I2C 对象而不是引脚本身。这允许调用代码为任何对象提供适当的方法,例如 I2C 扩展板。

另一个例子是期望一个DigitalInOut引脚切换而不是一个 Pin 来自 boardPin单独接受对象会将驱动程序限制为实际微控制器上的引脚,而不是其他驱动程序(例如 IO 扩展器)提供的引脚。

很多小模块

CircuitPython 板往往有少量的内部闪存和少量的 ram,但文件系统有大量的外部闪存。因此,创建许多可以根据需要加载的小型库,而不是一个可以完成所有任务的大文件。

速度秒

速度不如 API 清晰度和代码大小重要。所以,更喜欢简单的 API,比如状态属性,即使它牺牲了一点速度。

避免在驱动程序中分配

尽管 Python 不需要管理内存,但对于库编写者来说,考虑内存分配仍然是一个很好的做法。如果可以,请避免在驱动程序中使用它们,因为您永远不知道会调用多少东西。更少的分配意味着更少的清理时间。因此,在可能的情况下,更喜欢__init__ 在对象中创建并在整个对象中使用的字节数组缓冲区,并使用读取或写入缓冲区的方法,而不是创建新对象。统一的硬件 API 类,例如busio.SPI 设计用于读取和写入缓冲区的子部分。

分配一个对象返回给用户就可以了。请注意,由于内部逻辑,每次调用会导致多次分配。

然而, 这是一个内存权衡,所以不要对大的或很少使用的缓冲区这样做。

例子

结构包

使用 struct.pack_into代替 struct.pack

MicroPython 的使用const()

The MicroPython const()功能,如 本论坛帖子和本 本问题线程 中所讨论,提供了一些优化,可用于较小的、内存受限的设备。但是,在使用 时const(),请记住以下一般准则:

  • 始终通过导入使用,例如: from micropython import const

  • 仅限使用全局(模块级)变量。

  • 如果用户不需要访问变量,前缀名称带有前导下划线,例如:_SOME_CONST.

库示例

添加示例时,cookiecutter 会 <name>_simpletest.py在示例目录中为您添加一个文件。确保包含具有库最小功能的代码以在设备上工作。如果需要,您可以使用其他示例来展示库的不同功能。如果您添加其他示例,请确保将它们包含在examples.rst. 示例文件的命名应该使用库的名称后跟描述,使用下划线分隔它们。使用打印语句时,您应该使用格式,因为有些特定的板不能使用 f 字符串。 " ".format()

text_to_display = "World!"

print("Hello {}".format(text_to_display))

传感器属性和单位

AAdafruit的统一传感器驱动的Arduino库有一个 伟大的名单的测量及其单位。使用相同的名称,包括属性名称本身,以便驱动程序在具有相同属性时可以互换使用。

物业名称

蟒蛇型

单位

acceleration

(浮动,浮动,浮动)

x, y, z 米每秒每秒

magnetic

(浮动,浮动,浮动)

x, y, z 微型特斯拉 (uT)

orientation

(浮动,浮动,浮动)

x, y, z 度

gyro

(浮动,浮动,浮动)

x, y, z 弧度每秒

temperature

浮动

摄氏度

CO2

浮动

以 ppm 为单位测量的 CO2

eCO2

浮动

以 ppm 为单位的等效/估计 CO2(根据其他一些测量值估计)

TVOC

浮动

以 ppb 为单位的总挥发性有机化合物

distance

浮动

厘米 (cm)

proximity

整数

非单位特定的接近度值(单调但不是实际距离)

light

浮动

非单位特定的光照水平(应该是单调的,但不是勒克斯)

lux

浮动

SI勒克斯

pressure

浮动

百帕 (hPa)

relative_humidity

浮动

百分

current

浮动

毫安 (mA)

voltage

浮动

伏特 (V)

color

整数

RGB,每通道八位(0xff0000 为红色)

alarm

(时间结构,str)

采样警报时间和字符串以表征频率,例如“每小时”

datetime

时间结构

日期和时间

duty_cycle

整数

16 位 PWM 占空比(与输出分辨率无关)

frequency

整数

赫兹 (Hz)

value

布尔值

数字逻辑

value

整数

16 位模拟值,无单位

weight

浮动

克 (g)

sound_level

浮动

非单位特定的声级(单调但不是实际分贝)

添加原生模块

新模块的 Python API 应该定义并记录在 shared-bindings一个底层 C API 中。如果实现与端口无关或依赖于另一个模块的底层 API,则代码应位于shared-module. 如果它是特定 common-hal 于端口的,那么它应该位于端口的文件夹中。在任何一种情况下,文件和文件夹结构都应该模仿shared-bindings.

要测试您的本机模块或核心增强功能,请按照这些 Adafruit 学习指南构建本地固件以闪存到您的设备上:

构建电路Python

MicroPython 兼容性

保持与 MicroPython 的兼容性并不是一个高优先级。当它不与上述任何目标相冲突时,就应该这样做。