用Microbit模拟环形振荡器:从逻辑门到时钟信号的数字电路实践
1. 项目概述:用Microbit玩转数字电路的心脏
如果你对数字电路或者嵌入式开发感兴趣,一定听说过“时钟信号”这个概念。它是整个数字系统协调工作的节拍器,从你手机里的处理器到路由器里的芯片,都离不开一个稳定可靠的时钟源。今天,我们不谈那些复杂的晶振或PLL芯片,就来聊聊一个最基础、最纯粹,却也最能揭示数字电路本质的时钟生成电路——环形振荡器。
环形振荡器的原理简单得令人着迷:它只需要奇数个“非门”(NOT Gate),把它们首尾相连成一个环。由于非门的特性是“输入为高,输出就为低;输入为低,输出就为高”,当这个环形成后,信号就会在这个环里不断地自我翻转、循环奔跑,从而产生一个持续的方波信号。这个方波的频率,直接取决于信号跑完一圈所需要的时间,也就是所有非门延迟时间的总和。所以,从本质上讲,环形振荡器是把数字逻辑门固有的、微小的“反应时间”(传播延迟)巧妙地利用起来,转化成了我们需要的周期性信号。
那么,为什么用Microbit来做这件事呢?对于初学者或教育场景,直接搭建物理的74系列逻辑门电路虽然直观,但需要面包板、连线、电源,调试起来也麻烦。而Microbit这类微控制器,本质上就是一个可编程的数字系统,它的GPIO引脚可以模拟数字输入输出,其程序执行时间可以精确控制,从而完美地“模拟”出逻辑门及其延迟。这就像是在软件世界里搭建了一个虚拟的数字电路实验室,安全、灵活且成本极低。通过这个项目,你不仅能亲手实现一个振荡器,更能深刻理解“软件如何定义硬件行为”以及“时序”在数字系统中的核心地位。无论你是电子爱好者、学生,还是想巩固底层知识的嵌入式开发者,这个从逻辑门到频率控制的实践之旅,都会让你对数字世界的脉搏有更直接的感触。
2. 核心原理与设计思路拆解
2.1 环形振荡器的工作原理:为什么必须是奇数个门?
要理解环形振荡器,必须从非门(反相器)的基本特性说起。非门是一个具有单一输入和单一输出的逻辑器件,它的功能是逻辑取反:输入为逻辑0(低电平),则输出为逻辑1(高电平);输入为逻辑1,则输出为逻辑0。理想情况下,这个翻转是瞬间完成的。但在现实中,任何物理器件都存在“传播延迟”——信号从输入发生变化到输出产生相应变化所需要的时间,记作 Td。
当我们把奇数个(N个)非门串联起来,并将最后一个门的输出反馈回第一个门的输入,就构成了一个闭环,如下图所示(概念示意):
现在,让我们追踪一个逻辑跳变在这个环中的旅行。假设在某个初始时刻,第一个非门的输入为0。经过一个Td的延迟后,它的输出变为1。这个1作为第二个非门的输入,再经过一个Td,第二个非门的输出变为0……如此传递下去。经过 N*Td 的总时间后,这个逻辑跳变(从0到1的变化)会传递回起点,即第一个非门的输入。关键点来了:因为经历了奇数(N)次取反,回到起点的信号状态与最初的状态相反(最初是0,回来的是1)。这个新的输入状态(1)又会触发新一轮的翻转,使输出变为0,这个0再经过N个门后,又会以0的状态回来(因为0经过奇数次取反变1,再经过奇数次取反又变回0?这里需要仔细推演)。
实际上,更严谨的分析是考虑环路的稳态。由于环路增益大于1且相移为180度(奇数个反相器提供180度相移,加上环路本身的180度相移,满足振荡的巴克豪森准则),电路无法稳定在任何一个静态逻辑电平上,只能进入持续振荡状态。信号在环中每循环一圈,其电平就翻转一次。一圈的行程时间是 N*Td,而一次完整的翻转(从高到低再到高,或从低到高再到低)需要信号在环中跑两圈(因为跑一圈后电平相反,需要再跑一圈才能回到初始电平)。因此,输出方波的周期 T = 2 * N * Td。
由此,我们得到振荡频率的公式: 频率 F = 1 / T = 1 / (2 * N * Td)
这就是为什么必须是奇数个门。如果是偶数个门,信号绕一圈回来经历了偶数次取反,状态与初始状态相同,电路会迅速稳定在某个逻辑电平上(可能是亚稳态,但最终会趋于一个固定值),而无法形成持续振荡。
2.2 用Microbit模拟非门与延迟:软件定义硬件
在物理电路中,Td是由半导体工艺、温度、电压等因素决定的固有属性,通常为纳秒级别,且难以在成品芯片中大幅调整。而用Microbit模拟,则给我们带来了极大的灵活性。
1. 非门功能的软件实现: Microbit的非门模拟非常简单。我们使用一个GPIO引脚作为“输入”,另一个GPIO引脚作为“输出”。在程序的主循环中,不断执行以下操作:
- 读取输入引脚的电平(
digital read)。 - 对这个电平值进行逻辑“非”运算(如果读到的值是1,则准备输出0;如果是0,则准备输出1)。
- 将运算结果写入输出引脚(
digital write)。
这个过程本身就引入了一个延迟,即执行一次“读取-计算-写入”循环所需的时间。这个时间就是我们在软件中可控的 Td。
2. 延迟时间的精确控制: Microbit的微控制器(通常是ARM Cortex-M系列)运行速度很快,一次简单的读取-写入循环可能只需要几微秒,这会产生很高的振荡频率(几百kHz甚至MHz),远超我们肉眼或简单仪器能观察的范围。因此,我们需要在循环中主动加入延迟,来获得我们想要的、更低的频率(如1Hz到几十Hz)。
加入延迟的方法通常是调用类似 sleep_ms() 或 delay() 的函数,或者执行一个空循环来“浪费”时间。通过调整这个延迟的时长,我们就可以直接控制公式中的 Td,从而精确设定输出频率。例如,想要一个1Hz的振荡器(周期1秒),假设我们使用1个非门(N=1),根据公式 T = 2 * 1 * Td = 1秒,那么我们需要设置的 Td = 0.5秒。这意味着我们的代码循环一次(完成一次非操作)需要耗时500毫秒。
3. 单门振荡与多级振荡的考量: 原项目提到了使用单个非门。根据公式,当N=1时,F = 1/(2Td)。这意味着频率只由软件延迟Td决定。这是最简单的实现方式。 理论上,我们也可以在代码中模拟多个串联的非门(例如,用多个“读取-取反-写入”的代码段顺序执行,每个段之间可以有不同的延迟),这样N就大于1。增加N会降低频率,或者在相同频率下允许使用更短的软件延迟(因为Td = 1/(2N*F))。但在Microbit上,模拟多级门在代码上会更复杂,且由于所有“门”都在同一个处理器上串行执行,其总延迟与使用一个门但延迟时间设为N倍Td是等价的。因此,从教学和简洁性角度,使用单个非门并调整延迟时间是最直观有效的方法。
2.3 硬件连接与信号观测方案
尽管核心逻辑在软件中,但我们仍需要简单的硬件连接来完成闭环并观测结果。
1. 闭环连接: 这是最关键的一步。你需要一根杜邦线(或任何导线),将Microbit上被定义为“输出”的那个引脚,直接连接到被定义为“输入”的那个引脚。这就构成了物理上的反馈环,将软件非门的输出送回了它的输入。没有这根线,代码就只是一个不断读取某个固定输入(可能是浮空状态,不确定)并输出其反相值的程序,不会自激振荡。
2. 频率计算与测量点:
- 计算:根据你代码中设置的延迟时间(Td),利用公式 F = 1/(2Td) 即可计算出理论频率。例如,若
delay(500)表示延迟500毫秒,则 Td = 0.5秒,理论频率 F = 1/(20.5) = 1 Hz。 - 测量点:你可以从“输出”引脚连接信号到示波器或逻辑分析仪,直接观察生成的方波波形,测量其周期和频率,与理论值对比。这是最准确的验证方式。
3. 可视化反馈(用于无仪器情况): 为了方便在没有专业仪器的情况下观察振荡,我们可以利用Microbit的LED点阵或串口输出。
- LED指示:在代码中,让每次输出电平变化时,切换一个LED(如中央的LED)的亮灭。这样,LED的闪烁频率就是方波频率的一半(因为亮和灭各对应半个周期)。观察LED的闪烁,可以直观感受频率的变化。
- 串口打印:可以在每次循环中通过串口打印当前的输出电平值或时间戳,然后在电脑的串口监视器上观察文本输出的变化节奏,这也是一种定性的观测方法。
注意:GPIO引脚配置 在连接导线前,务必在代码初始化部分正确设置引脚的输入/输出模式。输出引脚应设置为
OUTPUT,输入引脚应设置为INPUT。对于这种自反馈连接,由于输出直接驱动输入,输入引脚不需要上拉或下拉电阻。
3. 代码实现与关键参数解析
我们将使用MicroPython来为Microbit V2编写代码(V1同样适用)。整个项目的代码结构非常清晰,核心就是一个包含了延迟的循环。
3.1 基础单非门振荡器代码实现
代码逐行解析:
- 引脚定义:选择了pin0和pin1。你可以根据手头连接线的方便程度选择任何一对数字IO引脚(如pin0-pin1, pin2-pin3等),只要确保在物理上用导线将输出引脚连回输入引脚。
- 循环体:
while True构成了一个无限循环,模拟了数字电路持续工作的特性。 - 读取与取反:
INPUT_PIN.read_digital()读取输入引脚当前的数字电平(0或1)。随后用三元表达式进行逻辑取反。 - 输出与可视化:
OUTPUT_PIN.write_digital()将取反后的值输出。同时,通过display.set_pixel将输出状态映射到LED点阵上,高电平时LED亮起,低电平时熄灭,提供了直观的视觉反馈。 - 核心延迟:
sleep(delay_ms)是控制频率的关键。它暂停程序的执行,从而人为地增加了“非门”的传播延迟 Td。
3.2 频率计算与参数调整实践
根据公式 F = 1 / (2 * Td),其中 Td 是循环一次的总时间(包括读取、计算、写入和sleep的时间)。由于读取、计算、写入的操作在微控制器上执行极快(微秒级),当sleep时间在几十毫秒以上时,这些操作时间可以忽略不计。因此,Td ≈ delay_ms / 1000 秒。
-
目标频率为1Hz:
- 周期 T = 1/F = 1秒。
- 由于 T = 2 * Td,所以 Td = T / 2 = 0.5秒 = 500毫秒。
- 将代码中的
delay_ms设置为 500。 - 此时,中央LED会以1秒为周期(亮0.5秒,灭0.5秒)闪烁。
-
目标频率为25Hz(接近Microbit V2默认示例频率):
- T = 1/25 = 0.04秒 = 40毫秒。
- Td = T / 2 = 20毫秒。
- 将
delay_ms设置为 20。 - LED的闪烁会非常快,人眼几乎分辨不出闪烁,感觉像是持续亮着但略有暗淡。
-
参数调整实验: 你可以创建一个变量列表来尝试不同的延迟值,观察现象:
PYTHONdelays_to_try = [1000, 500, 200, 100, 50, 20, 10] # 单位毫秒for d in delays_to_try:display.scroll(str(d)) # 显示当前尝试的延迟值sleep(1000)# 然后运行一段时间的振荡器start_time = utime.ticks_ms()while utime.ticks_diff(utime.ticks_ms(), start_time) < 5000: # 运行5秒# ... 振荡器循环代码,使用变量 d 作为延迟 ...input_state = INPUT_PIN.read_digital()output_state = 0 if (input_state == 1) else 1OUTPUT_PIN.write_digital(output_state)display.set_pixel(2, 2, output_state * 9)sleep(d) # 使用变量ddisplay.clear()通过这个实验,你可以清晰地看到延迟时间(Td)与振荡频率(LED闪烁快慢)之间的反比关系,直观验证理论公式。
3.3 代码优化与稳定性探讨
基础代码虽然能工作,但在实际实验中可能会遇到一些不稳定现象,比如LED闪烁不均匀,或者测量到的频率与理论值有偏差。这通常由以下原因造成:
1. 循环时间的不精确性:
sleep() 函数提供的延迟是近似值,它会让程序暂停至少指定的毫秒数,但精确度受系统调度和其他后台任务(如Microbit的蓝牙栈、系统计时器)的影响。对于要求不高的低频振荡(如1Hz),这通常可以接受。但对于更高频率或更精确的需求,就需要更精细的时间控制。
优化方案:使用 utime.ticks_us() 进行忙等待
这种方法通过高精度计时器进行“忙等待”,能提供比 sleep() 更精确的延迟,尤其适用于较短的延迟时间。但缺点是CPU占用率100%。
2. 输入信号稳定与防抖动: 由于输出直接通过导线连接回输入,理论上信号是干净的。但在实际中,导线可能引入噪声,或者在电平切换的瞬间,输入引脚可能会读取到一个不稳定的中间值(虽然概率很低)。为了代码健壮性,可以考虑在读取输入后进行一次简单的验证或多次采样取平均值(对于数字信号,通常取连续两次读取相同值)。
3. 使用外部中断的进阶思路(可选): 一个更接近硬件非门工作方式的模拟是使用外部中断。可以将输入引脚配置为中断触发模式,当输入电平变化时,自动触发中断服务程序,在中断中立即取反并输出。这样,延迟Td就主要由中断响应时间和代码执行时间决定,更加“即时”。但这部分代码更复杂,且中断处理不当可能引入其他时序问题,适合进阶探索。
实操心得: 在最初测试时,我建议先使用最简单的
sleep()方案,并选择较长的延迟(如500毫秒),确保电路和代码基本工作。在成功观察到稳定的1Hz闪烁后,再逐步减少延迟,尝试更高的频率,并切换到更精确的定时方法。同时,用手机慢动作摄影功能拍摄LED闪烁,然后逐帧查看,是测量低频周期的一个非常实用的土办法。
4. 硬件搭建、测试与问题排查
4.1 分步搭建指南与安全注意事项
所需材料清单:
- Microbit开发板(V1或V2均可)一块。
- 微型USB数据线一条(用于供电和编程)。
- 杜邦线(母对母)至少一根。
- (可选)万用表或示波器/逻辑分析仪,用于精确测量。
- (可选)电脑一台,用于编写和上传代码。
搭建步骤:
-
软件准备:访问Microbit官方编程网站(如Python Editor),将上一章节的代码复制进去,或者使用Mu Editor等离线工具。根据你想要的频率,修改
delay_ms变量的值(例如,先设置为500对应1Hz)。将程序下载为.hex文件,并通过USB线拖入MICROBIT盘符,完成烧录。 -
物理连接:
- 找到你代码中定义的
OUTPUT_PIN和INPUT_PIN对应的物理引脚。例如,如果代码用的是pin1输出,pin0输入,那么找到Microbit边缘连接器上标有“1”和“0”的焊盘或引脚。 - 用一根杜邦线,将输出引脚(如pin1)和输入引脚(如pin0)直接短接起来。 这是形成振荡环路的唯一必要连接。
- 确保连接牢固,避免虚接。
- 找到你代码中定义的
-
上电与观察:
- 通过USB线给Microbit上电。
- 观察Microbit的5x5 LED点阵。你应该能看到位于(2,2)的中央LED开始以设定的频率闪烁。
- 如果LED常亮、常灭或不规则闪烁,请进入排查环节。
安全与注意事项:
- 静电防护:虽然Microbit相对耐用,但处理时尽量避免直接触摸芯片和裸露的电路,尤其是在干燥环境下。
- 短路风险:确保杜邦线只连接了指定的两个引脚,没有触碰到其他引脚(尤其是3V和GND),防止短路。
- 电流驱动:Microbit的GPIO引脚驱动能力有限(约5mA)。本项目是输出连接回输入,输入阻抗很高,几乎不消耗电流,所以完全在安全范围内。切勿用这个输出引脚直接驱动电机、大功率LED等负载。
- 代码修改安全:在修改延迟参数时,避免设置极短的延迟(如小于1毫秒)。这可能导致循环频率过高,消耗大量CPU资源,虽然不会损坏硬件,但可能导致系统响应变慢,LED显示异常。
4.2 测试方法与频率验证
1. 定性测试(目测LED): 这是最直接的方法。设定一个较长的延迟(如500ms),观察中央LED是否规律地亮灭。用手机的秒表功能,计时LED闪烁10个周期所需的时间,然后除以10得到平均周期,再取倒数得到频率。与理论值(1/(2*0.5)=1Hz)对比。
2. 定量测试(使用万用表): 将万用表打到直流电压档,红表笔接触输出引脚(如pin1),黑表笔接触Microbit的GND引脚。当振荡器工作时,你会看到电压值在0V和~3V之间周期性跳动。对于低频(如1Hz),万用表读数会缓慢地在0和3左右切换。对于较高频率,万用表可能显示一个中间值(平均电压),例如50%占空比的方波,平均电压约为1.5V。这可以证明方波的存在,但无法精确测频。
3. 精确测试(使用示波器或逻辑分析仪): 这是最佳方法。
- 将示波器探头的信号钩连接到输出引脚,地线夹到Microbit的GND。
- 调整示波器的时基和电压刻度。
- 你应该能看到一个清晰的方波波形。
- 使用示波器的测量功能,直接读取波形的频率和周期。
- 对比理论值与实测值:记录下你代码中设置的
delay_ms值,计算理论频率 F_theory = 1000 / (2 * delay_ms) Hz。在示波器上读取实测频率 F_measured。两者通常会有些许差异。
4.3 常见问题排查速查表
下表列出了搭建和测试过程中可能遇到的问题、原因及解决方法:
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| LED完全不亮或完全常亮 | 1. 反馈环路未闭合。 2. 代码未成功烧录。 3. 引脚定义错误。 4. 杜邦线损坏或接触不良。 |
1. 检查连线:确保输出引脚确实用导线连接到了输入引脚。这是最常见错误。 2. 验证程序:重新烧录程序,尝试让Microbit显示一个简单的图案(如 display.show(Image.HAPPY))以确认程序运行正常。3. 核对引脚:检查代码中的 INPUT_PIN和OUTPUT_PIN变量值与实际连接的物理引脚是否一致。Microbit的pin0, pin1, pin2等是通用的。4. 更换导线:换一根杜邦线试试。 |
| LED闪烁频率明显不对(如比理论值慢很多或快很多) | 1. 延迟时间计算或设置错误。 2. 代码中除了 sleep还有额外耗时操作。3. sleep函数精度问题。 |
1. 复查公式:确认Td是sleep的时间(秒)。频率F=1/(2*Td)。2. 简化代码:移除LED显示等非核心代码,只保留读、反、写、延迟四步,再测试频率。 3. 使用更精确延时:如4.3节所述,尝试使用 utime.ticks_us进行忙等待延迟,对比结果。 |
| LED闪烁不稳定(周期时长时短) | 1. 电源不稳定。 2. 微控制器被其他任务中断(如系统后台任务)。 3. 导线接触不良,引入噪声。 |
1. 检查电源:使用电脑USB口或可靠的手机充电器供电,避免使用电量不足的移动电源。 2. 关闭无线功能:如果用的是Microbit V2,且不需要,可以尝试在代码开头禁用蓝牙 radio.off(),减少系统干扰。3. 确保连接可靠:按压一下杜邦线接头,或更换连接点。 |
| 示波器上看不到干净方波(波形畸变、毛刺) | 1. 探头接地不良。 2. 示波器带宽或设置不当。 3. Microbit GPIO输出上升/下降时间本身有限(非理想方波)。 |
1. 检查探头接地:确保示波器地线夹紧在Microbit的GND上,且接地回路尽量短。 2. 调整示波器:尝试调整触发电平,使用带宽限制功能滤除高频噪声。 3. 理解现实器件:微控制器引脚输出不是理想的瞬时跳变,会有纳秒级的上升/下降时间,在低频下观察不到,在很高频率下才会显现。本项目频率很低,这通常不是问题。 |
| 改变延迟参数,频率无变化 | 1. 代码修改后未重新烧录。 2. 变量名拼写错误,实际延迟未改变。 3. 延迟值太小,被循环其他部分的时间主导。 |
1. 重新烧录:任何代码修改后,必须重新生成.hex文件并拖入MICROBIT盘符。 2. 检查代码:仔细查看 sleep语句中的变量或数值是否已更新。3. 增加延迟:尝试一个非常大的延迟值(如2000毫秒),看LED闪烁是否明显变慢,以确认延迟是否生效。 |
一个关键的排查技巧: 如果一切连接正确但电路不振荡,可以尝试一个简单的“开环测试”。暂时断开输出到输入的反馈线。在代码中,将INPUT_PIN.read_digital()改为一个固定值(如input_state = 0)。这样,输出应该会持续输出高电平(1),LED常亮。然后手动将输入引脚(通过杜邦线)瞬间接地(GND),再断开,模拟一个低电平脉冲。你应该能看到LED随之熄灭再亮起。这个测试可以验证“读->取反->写”这个核心功能是否正常。如果正常,再接回反馈线,就应该能振荡了。
5. 项目延伸与高级应用思考
完成了基础的环形振荡器,你已经掌握了其核心原理。但这个简单的项目可以作为一个跳板,探索更多数字电路和嵌入式系统的有趣概念。
5.1 从软件延迟到“真实”门延迟的探索
我们通过sleep函数模拟的延迟,是宏观的、软件可控的延迟。而真实数字集成电路(如74HC04非门)的传播延迟是物理特性,通常在纳秒级,且受温度、电压、负载影响。
进阶实验:测量“最小”振荡频率
尝试将代码中的延迟时间delay_ms设置为0,或者一个非常小的值(如1毫秒)。此时,循环的延迟主要由代码执行read_digital, 计算, write_digital 以及MicroPython解释器本身的开销决定。这个时间可以近似看作是你用软件模拟的这个“非门”的最小传播延迟 Td_min。
- 测量此时的输出频率 F_max。
- 根据公式 Td_min ≈ 1/(2 * F_max)。
- 你可以将这个Td_min与数据手册中一个典型74HC04门电路在特定条件下的传播延迟(如10ns)进行对比,直观感受软件模拟与硬件速度的巨大差异。这个实验能让你深刻理解为什么高速数字电路必须用专用硬件实现。
5.2 构建多级环形振荡器与占空比调整
软件模拟多级振荡器: 虽然如前所述,在单处理器上模拟多级串联门在效果上等同于增加单级延迟,但我们可以从代码结构上模拟,以加深理解。例如,模拟一个3级环形振荡器:
在这个结构中,总延迟 Td_total = 3 * delay_per_gate,振荡频率 F = 1/(2 * 3 * delay_per_gate)。你可以调整 delay_per_gate 来观察频率变化。
占空比调整: 标准的环形振荡器产生的是占空比50%的方波(高电平和低电平时间相等)。能否产生非50%的方波呢?可以,但需要修改“门”的行为。我们不再简单地执行“取反-延迟”,而是可以定义“输出高电平持续T_high,输出低电平持续T_low”。这实际上变成了一个可编程的脉冲发生器。
此时,周期 T = T_high + T_low,频率 F = 1/T,占空比 = T_high / T * 100%。这已经超越了经典环形振荡器的范畴,展示了用微控制器生成任意波形的能力。
5.3 在真实项目中的应用场景启示
虽然我们这个Microbit振荡器在精度和稳定性上无法与晶振媲美,但其揭示的原理和灵活性在某些场景下很有价值:
- 教学与原型验证:快速验证一个数字电路模块(如分频器、时序逻辑)是否需要时钟信号,以及大致频率范围是否合适。
- 可编程低频时钟源:为那些对时钟精度要求不高,但需要灵活改变频率的低速外设(如LED呼吸灯、蜂鸣器提示音、周期性传感器采样)提供时钟。你可以通过按钮、光敏电阻或其他传感器来动态调整
delay_ms,从而实现频率调制。 - 理解时钟抖动与稳定性:通过这个项目,你可以实际测量软件生成时钟的周期抖动(每个周期时间的微小变化)。对比使用硬件定时器(如Microbit的
Timer模块)生成的PWM信号,能深刻理解软件循环定时与硬件定时在精度上的本质区别。 - 故障注入与测试:在测试其他电路时,可以故意使用这个不太稳定的振荡器作为输入,来测试被测电路对时钟抖动的容忍度。
最后一点个人体会:玩转这个项目后,再看任何微控制器或CPU的数据手册,里面关于“内部RC振荡器”、“时钟树”、“PLL倍频”的描述,你会感到格外亲切。你会明白,所有复杂的时钟系统,其最原始的雏形,或许就是一个追求自我维持翻转的环形结构。从用一根导线短接两个引脚让代码“跑起来”,到理解现代处理器中GHz时钟的由来,这中间是一条充满奇思妙想的工程之路。而这个小小的Microbit环形振荡器,正是踏上这条路的一块有趣的垫脚石。