3.1. 微控制器

OpenMV Cam 运行在一颗微控制器(MCU)上:这是一块单芯片,集成了 CPU、工作内存(RAM)、程序存储(flash),以及一组外设——用于与外部世界交互的硬件模块。

外设才是有意思的部分。每个外设都是一块专门负责某一项工作的硅片:把引脚拉高或拉低、测量模拟电压、通过串行总线逐字节地发送数据。CPU 通过寄存器——硬件持续监视并更新的固定内存地址——来配置和读取每个外设。

MicroPython 把这些寄存器封装到 machine 模块内的各个类中。machine.Pin(...) 返回一个控制通用输入/输出(GPIO)引脚的对象——这是一根芯片可以保持电平(约 3.3 V)或电平(约 0 V)的导线,也可以在外部设备驱动它时读取这两种状态之一。machine.ADC(...) 暴露模数转换器,它测量引脚上的电压并以数字形式报告。machine.UART(...) 运行通用异步收发器(UART)——这是一种外设,通过一对导线 TX(发送)和 RX(接收)一次一位地发送和接收字节。其他类则涵盖其余外设。脚本读写 Python 对象;MicroPython 将每次访问转换为相应的寄存器读写,而这些读写又在物理导线上移动比特。

一个单芯片轮廓,内含若干带标签的模块—— CPU、RAM、flash,以及一排由内部总线连接的外设模块 (GPIO、ADC、Timer/PWM、UART/SPI/I2C、CAN), 每个外设都有箭头从芯片引出,指向物理引脚标签。

一颗 MCU 把 CPU、内存和外设打包进单芯片中。每个外设都由 machine 模块中的一个类暴露给 Python。

3.1.1. 主循环

几乎每个微控制器程序都具有相同的结构:脚本顶部进行一次性设置(导入模块、配置引脚、打开总线),然后在底部是一个无限的 while True: 循环。在循环内部,程序反复地读取输入、做出决策、更新输出。这个循环就是程序本身;当脚本退出时,设备就停止做任何事情。

# setup, runs once
from machine import Pin
led = Pin("P0", Pin.OUT)

# main loop, runs forever
while True:
    led.value(1)
    # ... do work ...
    led.value(0)
    # ... do other work ...

这种结构——设置一次,然后永远循环——就是主循环模式。后面的所有内容都是关于循环里要放什么。

3.1.2. 实时控制

桌面程序与许多其他程序同时运行。操作系统在一个或多个线程——它逐毫秒切换的独立执行流——之间调度它的工作。当某个线程等待 I/O(磁盘、网络、用户移动鼠标)时,操作系统会把 CPU 交给另一个线程。程序基本上是事件驱动的:当有输入到来时,窗口管理器会调用你的代码;当套接字上有字节到达时,HTTP 库会恢复你的代码。是某个更大的东西在调用你。

微控制器程序则恰恰相反。默认情况下没有操作系统、没有调度器、也没有其他线程。刚才展示的主循环就是唯一的循环。外设触发中断或暴露状态标志;循环对它们进行轮询或直接处理中断。如果循环停在某个 time.sleep_ms(1000) 上,那一秒内设备什么都不做;没有其他线程来填补这段空隙。

由此引出两个无处不在的后果:

  • 时间是真实的。在一个紧凑循环中读取引脚两次只需微秒级时间;睡眠十毫秒就意味着这十毫秒里什么别的事都不会发生。非阻塞计时模式就是对此的应对。

  • 硬件是真实的。machine.Pin.value 设为 1 会在一根物理导线上施加约 3.3 V 电压;设为 0 则在那里施加约 0 V。电路的其他部分会立即看到这个电压——包括任何在引脚被错误驱动时可能被损坏的元件。