用Micro:bit自制函数信号发生器:从PWM到波形生成全解析

发布时间:2026/8/19 6:24:23
用Micro:bit自制函数信号发生器:从PWM到波形生成全解析 1. 从玩具到工具重新认识Micro:bit的潜力提到Micro:bit很多人的第一反应是“教育板卡”、“学生编程入门玩具”。确实这块由BBC牵头设计的小板子凭借其内置的LED点阵、加速度计、磁力计和两个可编程按钮成为了全球无数中小学生接触编程和电子学的第一站。但如果你也仅仅把它定位成一个玩具那可能就错过了它身上蕴藏的、足以让电子爱好者甚至初级工程师兴奋的潜力。今天我想分享一个具体的项目将这块小小的Micro:bit改造成一台功能完备的函数信号发生器。你可能会问信号发生器不是实验室里那种笨重、昂贵、旋钮密布的仪器吗用Micro:bit来做是不是太儿戏了这正是这个项目的迷人之处——它挑战了我们对“专业工具”的固有认知。通过这个项目我们不仅能亲手制作一个实用的工具更能深刻理解数字信号生成、数模转换DAC、以及嵌入式系统资源调度的核心原理。最终你将得到一台能够输出正弦波、方波、三角波等基本波形频率和幅度在一定范围内可调的便携式信号源。它可能无法替代专业的台式设备进行高精度测量但对于电路调试、传感器测试、音频项目实验或者仅仅是理解波形是如何“无中生有”的它都是一个绝佳的、低成本高收益的实践。这个项目适合所有对嵌入式开发和电子制作感兴趣的朋友无论你是刚接触Micro:bit的学生还是想寻找一个有趣周末项目的资深爱好者。整个过程我们将从Micro:bit的硬件限制出发一步步设计软件算法解决实际遇到的各种问题最终实现一个稳定可用的函数发生器。你会发现限制往往能激发最巧妙的创意。2. 核心硬件剖析Micro:bit的“输出能力”与瓶颈在开始写代码之前我们必须彻底搞清楚手头的“武器”——Micro:bit V2的硬件能力边界。盲目开始只会导致后续频繁碰壁。Micro:bit V2的核心是一颗Nordic nRF52833微控制器它功能强大但用于生成模拟信号时有几个关键限制需要我们仔细应对。首先也是最根本的一点Micro:bit没有真正的硬件数模转换器DAC。这意味着它无法直接输出一个平滑变化的电压值。它所有面向外部的引脚本质上都是数字引脚只能输出高电平约3V或低电平0V。那么我们如何用只能输出“开”和“关”的数字引脚来模拟一个连续变化的模拟信号呢答案是脉冲宽度调制PWM和电阻电容滤波网络。Micro:bit有多个支持PWM的引脚。PWM通过快速切换引脚的高低电平并改变高电平在一个周期内所占的时间比例即占空比来等效一个平均电压值。例如一个3.3V的引脚输出50%占空比的PWM经过滤波后其平均电压就是1.65V。通过程序实时、高速地改变这个占空比我们就能让平均电压随时间变化从而“绘制”出我们想要的波形。这是本项目最核心的技术原理。其次我们需要关注PWM的分辨率和频率。分辨率决定了我们能把电压划分得多细。Micro:bit的PWM分辨率通常是10位0-1023这意味着我们可以用1024个不同的等级来控制占空比理论上可以输出1024个不同的平均电压值这对于生成波形来说精度是足够的。而PWM的基础频率则需要权衡频率太低滤波后纹波大波形不干净频率太高则留给CPU计算和更新下一个占空比值的时间就越短可能无法生成高频的目标波形。Micro:bit的PWM频率通常在几百赫兹到几千赫兹可调我们需要选择一个远高于目标信号频率的PWM载波频率。最后是处理能力与定时器。生成波形是一个实时性要求很高的任务。CPU需要严格按照时间间隔计算并更新PWM的占空比。如果使用delay之类的阻塞函数会严重破坏波形的时序导致频率不准、波形畸变。因此我们必须依赖硬件定时器中断。在中断服务程序中我们根据预先计算好的波形数据表快速更新PWM占空比这样才能保证输出波形的频率精确和稳定。nRF52833的定时器资源丰富这为我们提供了坚实的基础。注意虽然Micro:bit V2的芯片支持更高精度的PWM但在常用的MakeCode或MicroPython环境中可能会受到软件框架的限制。如果追求极致性能可以考虑使用C/C如PlatformIO进行开发直接操作芯片寄存器但这会大大增加复杂度。本项目将以平衡易用性和性能的MicroPython为例。3. 波形生成策略查表法与实时计算法的取舍明确了硬件基础接下来就要设计软件算法我们如何在代码中“描述”一个波形并把它转化为一系列按时间排列的PWM占空比值主流方法有两种查表法和实时计算法。两种方法各有优劣选择哪一种取决于你对波形频率、种类和代码复杂度的要求。查表法是我更推荐给大多数项目的首选方法。它的原理非常简单直接在程序初始化时我们就预先计算好一个完整周期波形所对应的所有PWM占空比值并将这些值存储在一个数组即“波形表”中。例如要生成一个正弦波我们可以用sin()函数计算0到2π区间内等间隔的N个点的正弦值然后将这些值映射到0-1023的PWM占空比范围。生成波形时定时器中断每触发一次我们就从表中取出下一个值更新到PWM输出寄存器然后移动索引。当索引走到表末尾时再回到开头循环往复。查表法的最大优点是速度快对CPU计算压力小。中断服务程序里只需要做简单的数组索引和赋值操作极其高效。这意味着我们可以生成更高频率的波形。此外我们可以为不同波形正弦、三角、方波创建不同的表切换波形就是切换不同的数据表非常灵活。它的缺点是需要占用一定的内存空间来存储波形表。表的大小一个周期的点数决定了波形的“细腻度”。点数越多波形越平滑但最高可输出频率会降低因为需要更频繁地更新数据。这是一个需要权衡的参数。实时计算法则是在每次定时器中断触发时实时计算当前时刻对应的波形幅值。例如维护一个代表相位的变量每次中断时相位增加一个固定步进量然后用sin(相位)计算出当前的幅值再映射为PWM占空比。这种方法的优点是极其节省内存不需要存储任何波形表。同时它理论上可以生成无限多种波形只要你能写出对应的数学表达式。但它的致命缺点是计算开销大。在Micro:bit这样的微控制器上每次中断都计算一次浮点数三角函数如sin会消耗大量的CPU时间导致中断服务程序执行时间过长。这不仅会限制波形的最高频率还可能影响系统其他任务如响应按钮事件的实时性。在MicroPython这种解释型语言中这个开销会更加明显。因此对于Micro:bit函数发生器这个项目我强烈建议使用查表法。它稳定、高效、可靠。我们可以在电脑上用Python或Excel提前生成高精度的波形表然后以数组形式嵌入到MicroPython代码中。这是平衡性能、复杂度和效果的最佳实践。4. 构建基础正弦波发生器从理论到可听的声音让我们从最经典的正弦波开始实现第一个可工作的原型。这一步的目标是验证整个技术路线的可行性并输出一个我们能实际感知例如通过扬声器听到的信号。首先我们需要生成正弦波的波形表。假设我们决定用一个包含256个点的数组来表示一个完整的正弦周期。为什么是256因为它是2的幂次方在计算索引循环时非常高效可以用位与运算index 0xFF来实现回绕。我们在电脑上生成这个表# 此Python脚本在电脑上运行用于生成波形表 import math TABLE_SIZE 256 sine_table [] for i in range(TABLE_SIZE): # 计算正弦值范围[-1, 1] value math.sin(2 * math.pi * i / TABLE_SIZE) # 将正弦值映射到Micro:bit PWM占空比范围 (假设为0-1023) # 先偏移到[0, 2]再缩放到[0, 1023] pwm_value int((value 1) * 1023 / 2) sine_table.append(pwm_value) # 输出为MicroPython数组格式 print(sine_table [) for i in range(0, TABLE_SIZE, 16): # 每行打印16个值 row sine_table[i:i16] print( , .join(str(x) for x in row) ,) print(])接下来我们将这个表和主程序写入Micro:bit。这里使用MicroPython的machine模块来操作硬件定时器和PWM。我们选择pin0作为输出引脚。# MicroPython 代码 (main.py) import machine import utime from machine import Pin, PWM, Timer # 粘贴上面生成的256点正弦表 here sine_table [512, 525, 538, 551, 564, 577, 590, 603, ... , 512] # 此处应为完整的256个值 # 初始化PWM频率设为1kHz这个频率是PWM本身的载波频率要远高于我们想生成的正弦波频率。 pwm_out PWM(Pin(0)) pwm_out.freq(1000) # 设置PWM载波频率为1kHz pwm_out.duty_u16(0) # 初始占空比为0 # 波形生成相关变量 table_index 0 table_size len(sine_table) # 目标正弦波频率例如 10Hz target_freq 10 # 计算定时器中断频率目标频率 * 表大小 # 例如要生成10Hz正弦波每秒需要输出10个周期每个周期256个点则每秒需输出2560个点。 # 因此定时器中断频率应为 10Hz * 256 2560 Hz interrupt_freq target_freq * table_size # 计算定时器周期微秒 period_us int(1000000 / interrupt_freq) # 定时器中断回调函数 def update_wave(timer): global table_index # 从表中取出当前值更新PWM占空比 # MicroPython的PWM.duty_u16()需要0-65535的值我们的表是0-1023需要转换 pwm_value sine_table[table_index] pwm_out.duty_u16(pwm_value * 64) # 1023 * 64 ≈ 65535 # 更新索引循环 table_index (table_index 1) % table_size # 初始化硬件定时器 tim Timer(-1) # 使用虚拟定时器 # 设置定时器模式为周期性周期为上面计算的period_us微秒回调函数为update_wave tim.init(periodperiod_us, modeTimer.PERIODIC, callbackupdate_wave) # 主循环空跑或者可以加入一些控制逻辑 try: while True: utime.sleep(1) except KeyboardInterrupt: tim.deinit() pwm_out.deinit() print(Stopped.)将这段代码刷入Micro:bit后用一根导线连接pin0和GND中间串联一个约100欧姆的电阻和一个压电式蜂鸣器或一个小喇叭注意极性。如果一切正常你应该能听到一个约10Hz的“嗡嗡”声——虽然10Hz不在人耳可听范围20Hz-20kHz但由于PWM载波是1kHz你会听到一个1kHz的“基音”被10Hz调制所产生的声音效果。用示波器探头连接到pin0则能看到一个清晰的正弦波形。实操心得一滤波是关键。直接连接示波器你可能会看到波形上有明显的“台阶”或毛刺这是因为PWM方波没有被完全滤除。为了得到光滑的正弦波必须在输出引脚和地之间连接一个低通滤波器。一个简单的RC滤波器例如一个1kΩ电阻串联在引脚上然后接一个0.1uF电容到地就能大大改善波形质量。电阻和电容的值决定了截止频率需要根据你PWM的载波频率本例是1kHz和目标信号最高频率来选取确保滤掉PWM载波但保留我们生成的低频信号。5. 实现波形切换与频率调节打造交互式界面能输出固定频率的正弦波只是第一步。一个实用的函数发生器必须能够切换波形和调节频率。我们将利用Micro:bit的A、B按钮和加速度计或触摸引脚来实现简单的交互。首先我们扩展波形表。除了正弦波我们再创建方波和三角波的256点查找表。# 方波表前128个点为高电平1023后128个点为低电平0 square_table [1023] * 128 [0] * 128 # 三角波表从0线性上升到1023再线性下降回0 triangle_table [] for i in range(256): if i 128: value int(i * 1023 / 128) else: value int((256 - i) * 1023 / 128) triangle_table.append(value)然后我们修改主程序结构引入状态变量。我们将波形表放在一个字典或列表中用一个索引变量current_wave来标识当前波形。# 波形表字典 wave_tables { 0: sine_table, 1: square_table, 2: triangle_table, } current_wave_index 0 current_table wave_tables[current_wave_index] # 在定时器中断中不再固定使用sine_table而是使用current_table def update_wave(timer): global table_index pwm_value current_table[table_index] # 使用当前波形表 pwm_out.duty_u16(pwm_value * 64) table_index (table_index 1) % table_size接下来我们添加按钮控制逻辑。在主循环中检测A、B按钮的按下事件。button_a Pin(5, Pin.IN, Pin.PULL_UP) # Micro:bit按钮A对应的引脚 button_b Pin(11, Pin.IN, Pin.PULL_UP) # 按钮B def check_buttons(): global current_wave_index, current_table, target_freq, interrupt_freq, period_us if button_a.value() 0: # 按钮被按下是低电平 utime.sleep_ms(20) # 简单消抖 if button_a.value() 0: # A按钮切换波形 current_wave_index (current_wave_index 1) % len(wave_tables) current_table wave_tables[current_wave_index] # 在LED点阵上显示当前波形简图或编号 display_wave_icon(current_wave_index) while button_a.value() 0: # 等待按键释放 pass if button_b.value() 0: utime.sleep_ms(20) if button_b.value() 0: # B按钮增加频率例如每次增加1Hz target_freq 1 if target_freq 100: # 设置一个上限 target_freq 1 # 重新计算定时器参数 interrupt_freq target_freq * table_size period_us int(1000000 / interrupt_freq) # 需要重新初始化定时器这是关键一步。 tim.deinit() tim.init(periodperiod_us, modeTimer.PERIODIC, callbackupdate_wave) # 显示当前频率 display_freq(target_freq) while button_b.value() 0: pass def display_wave_icon(index): # 简单地在LED点阵上显示波形标识 display microbit.display display.clear() icons [microbit.Image.SINE, microbit.Image.SQUARE, microbit.Image.TRIANGLE] # 需要自定义或使用字符 # 这里简化用数字代替 display.show(str(index)) def display_freq(freq): microbit.display.scroll(str(freq) Hz)实操心得二动态改变定时器参数。当频率改变时仅仅更新target_freq和period_us变量是不够的。必须先停止deinit原有的定时器再用新的周期参数重新初始化init定时器。否则定时器会继续以旧的周期运行导致频率调节失效。这是一个常见的坑点。另外频率调节的步进和范围需要仔细设计。步进太小如0.1Hz在高速更新时按钮操作会不灵敏步进太大如10Hz则调节不精细。可以设计长按加速、双击切换粗调/微调等逻辑来改善用户体验但这会大大增加代码复杂度初期实现基础功能即可。6. 幅度调节与输出缓冲让信号更“有用”目前我们的信号幅度是固定的最大接近3.3VPWM占空比1023时。但在实际使用中我们经常需要调节信号的幅度例如驱动一个需要5V输入的电路或者产生一个微弱的mV级信号。Micro:bit的GPIO输出电压是固定的约3.3V我们如何实现幅度调节呢软件调节幅度相对简单。我们可以在输出PWM值之前乘上一个幅度系数0到1之间。例如想要50%的幅度就将波形表中的每一个值都乘以0.5。但需要注意的是我们的PWM占空比是整数乘法后需要取整这会引入微小的量化误差。同时当幅度很小时波形的分辨率会下降例如幅度系数0.1时有效输出范围只有0-102台阶感会更明显。amplitude 0.8 # 幅度系数0.0 ~ 1.0 def update_wave(timer): global table_index raw_value current_table[table_index] # 应用幅度调节 adjusted_value int(raw_value * amplitude) # 确保值在合法范围内 if adjusted_value 1023: adjusted_value 1023 pwm_out.duty_u16(adjusted_value * 64) table_index (table_index 1) % table_size然而软件调节无法突破硬件3.3V的上限。要获得更高的电压或者驱动更低阻抗的负载如扬声器就需要硬件辅助。一个非常实用的设计是增加一个运算放大器缓冲/放大电路。我们可以利用一个常见的单电源运放如LM358搭建一个同相放大器电路。将Micro:bit的PWM输出经过RC滤波后得到的模拟电压连接到运放的同相输入端通过调节反馈电阻的比例来设置增益。例如如果Rf/R1 1那么就是一个电压跟随器增益为1主要起缓冲作用提高带负载能力。如果Rf/R1 0就可以放大电压。运放的电源可以从Micro:bit的3.3V或外部更高的电源如5V获取从而实现电压幅度的提升。注意使用运放时务必注意其输入输出电压范围。LM358在单电源3.3V供电时输出电压最高大约只能到2.5V左右无法达到轨到轨。如果需要0-3.3V的全范围输出应选择轨到轨Rail-to-Rail输出的运放如MCP6002。另外输出端串联一个约100Ω的电阻可以起到限流保护作用防止意外短路损坏运放或Micro:bit。7. 进阶优化与问题排查从“能用”到“好用”一个基础版本完成后我们会发现很多可以优化和可能遇到的问题。这里分享几个关键的进阶优化点和排查思路。问题一波形在高频时畸变或频率不准。这通常是因为CPU不堪重负。检查以下几点中断服务程序ISR是否过于臃肿update_wave函数里只应做最必要的操作查表、计算、写寄存器。避免在ISR内进行浮点运算、复杂判断或调用其他慢速函数。定时器中断频率是否过高最高输出频率受限于中断频率 目标频率 × 表大小。当目标频率升高时中断频率呈线性增长。过高的中断频率会占用几乎所有CPU时间。解决方案减少波形表大小。例如从256点降到64点甚至32点。虽然波形会变得“棱角分明”但在高频下经过RC滤波后效果可能仍在可接受范围内。这是一种典型的用精度换性能的权衡。是否使用了MicroPython如前所述MicroPython的解释执行效率远低于C。如果对频率有较高要求500Hz应考虑使用MakeCode基于C但灵活性稍差或直接使用C/C开发。优化点一使用DMA直接存储器访问。这是终极性能解决方案。nRF52833支持DMA它可以在不占用CPU的情况下自动将波形表数据从内存搬运到PWM比较寄存器。CPU只需要设置好DMA传输的源地址波形表、目标地址PWM寄存器和传输数量DMA控制器就会按照定时器的节奏自动完成数据搬运。这样生成波形几乎不消耗CPU资源。但这需要非常底层的寄存器编程在MicroPython中难以实现通常需要在C语言环境下完成。优化点二增加输出偏移直流偏置。很多应用需要信号在0V上下摆动交流耦合但有时也需要一个可调的直流偏置。我们可以在软件计算中增加一个偏移量常数。例如想要一个以1.65V3.3V/2为中心幅度1V的正弦波。那么计算过程就是最终值 中心值 幅度 * sin(相位)。在代码中中心值对应一个PWM占空比如512幅度系数也需要映射。问题二按钮操作不灵敏或误触发。这是机械按钮的常见问题。除了代码中的延时消抖还可以采用状态机的方式或者使用utime.ticks_ms()来记录按下时间实现长按、短按、双击的区分使交互更友好。最终形态设想一个相对完整的Micro:bit函数发生器可以这样设计A键短按切换波形正弦-方波-三角波-锯齿波...长按进入频率调节模式。B键在频率调节模式下短按步进增减长按快速增减。摇晃Micro:bit切换幅度调节模式通过倾斜角度来连续改变幅度系数利用加速度计。LED点阵实时显示当前波形符号和频率/幅度数值。硬件在Micro:bit扩展板上集成RC滤波电路、运放缓冲/放大电路、以及一个BNC输出接口使其看起来更像一个正经的仪器。通过这个项目你收获的不仅仅是一个自制的函数发生器。你深入实践了嵌入式系统中的实时编程、数模转换原理、中断处理、人机交互设计以及硬件滤波电路的应用。你会发现在有限的资源下完成一个复杂功能所涉及的权衡和创意恰恰是嵌入式开发最精髓、最有趣的部分。