微控制器在日常所有的电子产品中用到,包括交通灯。交通灯控制器是一种特殊的系统,它可以定时改变信号灯,观察行人是否要过马路,以及根据交通流量调整信号灯的时间,与附近的交通灯系统通话,以确保整个交通网络保持顺畅运行。虽然构建一个大规模的交通管理系统是一个相当先进的项目,但构建一个由树莓派Pico驱动的微型模拟器就比较简单。在这个项目中,你将看到如何控制多个LED,设置不同的时间,以及当程序的其余部分使用一种称为“线程”的技术继续运行时如何监测一个按键的输入。

交通灯的控制

在我们设计的树莓派Pico的学习板上,有4个LED,分别为R(红色)、G(绿色)、B(蓝色)、Y(黄色),我们可以使用其中的三个来仿真现实中的交通灯的工作状态:

  • 红色LED亮表示禁止通行
  • 黄色LED亮表示交通状态就要改变
  • 绿色LED亮表示可以再次通行

要给我们的交通灯编程,先把我们的Pico连接到树莓派或其它电脑,并加载Thonny。创建一个新程序,并从导入machine库开始,这样我们就可以控制PicoGPIO引脚:

import machine

为了添加灯之间的延迟,并能够控制灯的关闭时间,还需要导入utime库:

import utime

与使用Pico的GPIO引脚的任何程序一样,需要先配置每个管脚的属性:

led_red = machine.Pin(19, machine.Pin.OUT) 
led_yellow = machine.Pin(16, machine.Pin.OUT) 
led_green = machine.Pin(18, machine.Pin.OUT)

这些代码设置引脚GP19、GP16、GP18作为输出,每个管脚都给了一个描述性的名称,以'led'开始,这样可读性更好,一眼就知道哪个管脚控制一个led,以及该led的颜色。

实际的交通灯不会一闪而过就停下来,即便没有交通堵塞,没有人通行,它们也会一直运行着。为了让你的程序做同样的事情,你需要建立一个无限循环:

while True:

下面的每一行都需要缩进四个空格,这样MicroPython就知道它们是循环的一部分, 当你按下回车键时,Thonny会自动为你缩进。

    led_red.value(1) 
    utime.sleep(5) 
    led_yellow.value(1) 
    utime.sleep(2) 
    led_red.value(0) 
    led_yellow.value(0) 
    led_green.value(1)
    utime.sleep(5) 
    led_green.value(0) 
    led_yellow.value(1) 
    utime.sleep(5) 
    led_yellow.value(0)

单击Run图标并将程序以TrafficLights.py的形式保存到Pico中。

LED的状态变化:

  1. 首先红色的LED会亮起来 - 禁止通行;
  2. 接下来,黄色的LED灯会亮起来,意味着交通灯即将改变;
  3. 然后两个LED都关闭,绿色LED亮起来,意味着可以通行了;
  4. 然后绿色的LED灯熄灭,黄色的灯亮起来,意味着交通灯又要变了;
  5. 最后,黄色的LED熄灭,循环重新开始,红色的LED亮起。

该模式将一直无限循环下去,直到你按下停止按钮。它是基于英国和爱尔兰现实世界交通控制系统中使用的交通灯模式,在这个模拟试验中,我们只是将状态转换做了加速 - 最长的通行时间只有5秒钟。

然而,真正的红绿灯并不仅仅是用于道路车辆的,也要考虑到行人的通行,让他们也有机会安全地通过繁忙的道路,交通灯控制系统可以根据行人的按键来决定交通灯的状态变化。

那我们需要在交通灯控制系统中增加一个行人可以控制的按键开关,这样行人可以要求交通灯让他们过马路;再增加一个蜂鸣器,告知行人什么时候该过马路了。

我们可以将学习板上的其中一个按键KEY1用作行人可以操作的按钮,用学习板上的蜂鸣器给出提示音,在Thonny IDE中,回到设置led的行,并在下面添加以下两行:

button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) 
buzzer = machine.Pin(22, machine.Pin.OUT)

此设置引脚GP12上的按钮用作行人的按键输入,引脚GP22上的蜂鸣器作为声音输出。因为树莓派Pico有内置的可编程输入电阻,我们将其设置为上拉模式。这意味着引脚的电压被拉到3.3V(它的逻辑电平是1),除非它连接到GND(在这种情况下,它的逻辑电平将是0)。

接下来,您需要一种方法让您的程序不断监视按键的值。以前,你所有的程序都是通过一系列的指令一步一步地工作的,一次只做一件事。红绿灯程序也没有什么不同:当它运行时,MicroPython会一步一步地完成指令,打开和关闭led。

对于一套基本的交通灯来说,这就足够了;然而,对于我们目前设定的交通灯控制模式,我们的程序需要能够记录按钮是否按下时不会干扰交通灯。要实现此功能,我们需要一个新的库: thread。回到程序中导入machine和utime库的部分,并导入thread库:

import _thread

一个或多个执行线程实际上是一个小的、部分独立的程序。您可以将前面编写的控制灯光的循环视为程序的主线程,并且使用_thread库可以创建一个同时运行的附加线程。

想象这些线程的一个简单方法是把它们想象成厨房里一个独立的工人:当厨师在准备主菜时,另一个人在做酱料。目前,我们的程序只有一个线程 - 控制交通灯的线程。然而,驱动Pico的RP2040微控制器有两个处理核心, 这意味着,就像厨房里的厨师和副厨师一样,我们可以同时运行两个线程来完成更多的工作。

在创建另一个线程之前,我们需要一种方法让新线程将信息传递回主线程,我们可以使用全局变量来实现这一点。在此之前使用的变量称为局部变量,只在程序的一个部分中有效;全局变量在任何地方都可以工作,这意味着一个线程可以更改值,另一个线程可以检查它是否被更改。

首先,我们需要创建一个全局变量。在buzzer = 行下面,添加以下内容:

global button_pressed
button_pressed = False

这将button_pressed设置为一个全局变量,并给它一个默认值False, 意思是当程序开始时,按钮还没有被按下。下一步是定义你的线程,直接在下面添加以下几行, 如果你想,添加一个空行,让你的程序更具可读性:

def button_reader_thread(): 
     global button_pressed 
     while True:
        if button.value() == 0: 
            button_pressed = True
        utime.sleep(0.01)

你添加的第一行定义了你的线程并给它起了一个描述性的名字, 它是一个读取按钮输入的线程。与编写循环时一样,MicroPython需要线程中包含的所有内容缩进4个空格, 这样它就知道线程的开始和结束位置。

下一行让MicroPython知道我们将更改全局button_pressed变量的值。如果我们只是想检查值,就不需要这一行, 但没有它,我们是不能对变量做任何改变的。

接下来,我们设置一个新的循环, 同时也遵循一个新的4格缩进,总共8格,所以MicroPython知道循环是线程的一部分,下面的代码也是循环的一部分。这个多级缩进的嵌套代码在MicroPython中是很常见的, Thonny也会尽最大的努力在每次需要的时候来帮助你自动添加一个新的层级, 但我们要记得完成一个特定的层级以后删除多余的空格。

下一行是一个条件语句,用于检查按键的值是否为1, 我们的Pico使用一个内部的上拉电阻,当按键没有被按下时,读取的值是1,这意味着在此条件下代码永远不会运行, 只有当按键被按下时,读取的值才为0,线程的最后一行才会运行,这一行将button_pressed变量设置为True,让程序的其余部分知道按键已经被按下。最后,我们添加了一个非常短(0.01秒)的延迟,以防止while循环运行过快。

我们注意到,当按键被按下后再释放时,线程中没有任何东西可以将button_pressed变量重置为False。这是有原因的,虽然我们可以在红绿灯周期的任何时候按路口的按键,但它只在红灯亮起、我们可以安全过马路时才生效。新线程需要做的就是在按键被按下时更改变量,当行人安全地过马路时,主线程会将其重置为False。

定义一个线程并不会设置它运行,在我们的程序中可以在任何时候启动一个线程,我们需要明确地告诉_thread库想要启动线程的时间。与运行正常的代码行不同,运行线程不会停止程序的其余部分,当线程启动时,MicroPython将继续运行程序的下一行,即使它运行新线程的第一行。

在我们的线程下面创建一个新行,删除所有Thonny自动为我们添加的缩进,如下所示:

_thread.start_new_thread(button_reader_thread, ())

这告诉_thread库启动前面定义的线程。在这一点上,线程将开始运行并快速进入它的循环, 每秒检查按键数千次,看看它是否被按下。与此同时,主线程将继续执行程序的主要部分。

现在单击Run按钮,我们会看到交通灯和以前一模一样,没有任何延误或停顿,这时候如果我们按下按钮,什么也不会发生,因为我们还没有添加对按钮作出实际反应的代码。

到主循环的开始部分,就在(while True:)这一行的后面添加以下代码,记住要注意嵌套缩进,并在不再需要时删除Thonny添加的缩进:

    if button_pressed == True: 
        led_red.value(1)
        for i in range(10):
            buzzer.value(1) 
            utime.sleep(0.2) 
            buzzer.value(0) 
            utime.sleep(0.2)
        global button_pressed 
        button_pressed = False

这段代码检查button_pressed全局变量,以查看自循环最后一次运行以来,按键是否在任何时候被按下。如果有,就像我们之前做的按键阅读线程报告的那样,它开始运行一段代码,首先打开红色的LED灯来停止交通,然后按下蜂鸣器十次,让行人知道时间到了可以过马路了。

最后两行将button_pressed变量重置为False, 所以下一次环路运行它不会触发行人通行规则,除非再次按下按钮。

我们会发现不需要global button_pressed行来检查条件变量的状态, 只有当我们想要更改变量并使该更改影响程序的其它部分时,才需要使用它。

最终我们的程序应该是这样的:

import machine 
import utime 
import _thread
led_red = machine.Pin(19, machine.Pin.OUT) 
led_yellow = machine.Pin(16, machine.Pin.OUT)
led_green = machine.Pin(18, machine.Pin.OUT)
button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) 
buzzer = machine.Pin(22, machine.Pin.OUT)
 
global button_pressed 
button_pressed = False
 
def button_reader_thread(): 
    global button_pressed 
    while True:
        if button.value() == 0: 
            button_pressed = True
        utime.sleep(0.01) 
 
_thread.start_new_thread(button_reader_thread, ())
 
while True:
    if button_pressed == True:
        led_red.value(0) 
        for i in range(10): 
            buzzer.value(1)
            utime.sleep(0.2) 
            buzzer.value(0) 
            utime.sleep(0.2)
        global button_pressed
        button_pressed = False 
    led_red.value(0) 
    utime.sleep(5) 
    led_yellow.value(0) 
    utime.sleep(2) 
    led_red.value(1) 
    led_yellow.value(1) 
    led_green.value(0) 
    utime.sleep(5) 
    led_green.value(1) 
    led_yellow.value(0) 
    utime.sleep(5) 
    led_yellow.value(1)

单击Run图标。

一开始的时候,程序按照正常模式运行 - 交通灯将以通常的模式开或关。

按下按钮开关,如果程序目前处于循环的中间,什么也不会发生,直到它到达终点并再次循环, 这时红灯会变红,蜂鸣器会发出哔哔声,让你知道可以安全通过马路了。

检测过马路的条件部分的代码,是在我们刚才编写的在循环模式中打开/关闭灯的代码之前运行,它完成之后, 模式将恢复到像往常一样 - 红色LED在原来点亮的基础上再亮五秒的时间,蜂鸣器也一直在响。

这是在模拟真正的过马路的方式, 即使蜂鸣器停止鸣叫,红灯仍然亮着,所以在蜂鸣器响着的时候开始过马路的人有时间在车辆允许通行之前到达另一边。

让交通灯再循环几次,然后再次按下按键触发另一个路口。

祝贺你:你已经实现了你自己的十字路口交通控制系统!

挑战:你能改进它吗?

你能改变程序,延长行人过马路的时间吗? 你能找到其他国家交通灯模式的信息,并重新编程我们的交通灯以匹配吗? 你能不能再加一个按钮,让对面的行人也能发出想要过马路的信号?