九个-Pico-PIO-Wat-与-Rust-第一部分-

九个 Pico PIO Wat 与 Rust(第一部分)

原文:towardsdatascience.com/nine-pico-pio-wats-with-rust-part-1-9d062067dc25/

Pico PIO 惊喜 - 来源:https://openai.com/dall-e-2/。所有其他图表均来自作者。

Pico PIO 惊喜 – 来源:openai.com/dall-e-2/。所有其他图表均来自作者。

同样可用:本文的 MicroPython 版本

在 JavaScript 和其他语言中,我们称令人惊讶或不一致的行为为“Wat!” [即“What!?”]。例如,在 JavaScript 中,一个空数组加一个空数组产生一个空字符串,[] + [] === ""。Wat!

与之相比,Rust 语言一致且可预测。然而,在 Raspberry Pi Pico 微控制器上的 Rust 语言有一个类似的惊喜。具体来说,Pico 的可编程输入/输出(PIO)子系统,虽然功能强大且用途广泛,但也存在一些特殊性。

PIO 编程很重要,因为它为精确、低级硬件控制挑战提供了一个巧妙的解决方案。它非常快且灵活:而不是依赖于特殊用途硬件来控制您可能想要控制的无数外围设备,PIO 允许您在软件中定义自定义行为,无缝适应您的需求,而不会增加硬件复杂性。

考虑这个简单的例子:一种类似$15 theremin 的音乐仪器。通过在空中挥动手臂,音乐家可以改变(诚然有些恼人的)音调。使用 PIO 提供了一种简单的方式来编程这个设备,确保它能即时响应动作。

因此,一切都很美好,除了——用蜘蛛侠的话来说:

权力越大……九个 Wat!?

我们将通过创建这个 theremin 来探索和展示这九个 PIO Wat。

本文面向谁?

  • 所有程序员:像 Pico 这样的微控制器价格低于$7,并支持 Python、Rust 和 C/C++等高级语言。本文将展示微控制器如何让您的程序与物理世界交互,并介绍如何编程 Pico 的低级、高性能 PIO 硬件。

  • Rust Pico 程序员:对 Pico 的潜在能力感兴趣吗?除了其两个主要核心外,它还有八个专门用于 PIO 编程的微小“状态机”。这些状态机接管时间关键任务,为主处理器腾出空间进行其他工作,并实现令人惊讶的并行性。

  • C/C++ Pico 程序员:虽然本文使用 Rust,但 PIO 编程——无论是好是坏——在所有语言中几乎都是相同的。如果您在这里理解了它,您将能够很好地将其应用于 C/C++。

  • MicroPython Pico 程序员:你可能想阅读这篇文章的 MicroPython 版本

  • PIO 程序员:通过九个 Wat 的旅程可能不会像 JavaScript 的怪癖那样有趣(幸运的是),但它将揭示 PIO 编程的奇特之处。如果你曾经觉得 PIO 编程令人困惑,这篇文章应该会让你放心,问题(不一定)是你——部分原因是 PIO 本身。最重要的是,理解这些 Wat 将使编写 PIO 代码变得更加简单和有效。

最后,这篇文章并不是关于“修复”PIO 编程的。PIO 在它的主要目的上表现出色:高效且灵活地处理自定义外围接口。其设计是有目的的,非常适合其目标。相反,这篇文章的重点是理解 PIO 编程及其怪癖——从奖励 Wat 开始。

奖励 Wat 0:“状态机”不是状态机——除非它们是

尽管名字叫作“PIO 状态机”,但树莓派 Pico 中的八个“PIO 状态机”在计算机科学的严格、正式意义上并不是状态机。一个经典的有限状态机 (FSM)由有限个状态和转换组成,完全由输入符号驱动。相比之下,Pico 的 PIO 状态机是带有自己指令集的小型可编程处理器。与正式的 FSM 不同,它们有一个程序计数器、寄存器以及通过移位和递减来修改这些寄存器的能力,这使得它们能够存储中间值并影响执行流程,而不仅仅是固定的状态转换。

话虽如此,称它们为可编程状态机是有道理的:它们的行为取决于它们执行的程序,而不是一个固定的预定义状态集。在大多数情况下,这个程序将定义一个状态机或类似的东西——特别是由于 PIO 经常用于实现精确、状态驱动的 I/O 控制。

每个 PIO 在每个时钟周期处理一条指令。$4 Pico 1 以每秒 1.25 亿个周期的速度运行,而$5 Pico 2 提供更快的每秒 1.5 亿个周期。每条指令执行一个简单的操作,例如“移动一个值”或“跳转到标签”。

随着奖励 Wat 的解决,让我们转向我们的第一个主要 Wat。

Wat 1:寄存器饥饿游戏

在 PIO 编程中,一个 寄存器 是一个小的、快速的存储位置,它像状态机的变量一样工作。你可能梦想拥有大量的变量来存储你的计数器、延迟和临时值,但现实是残酷的:你只能得到两个通用寄存器,xy。这就像《饥饿游戏》,无论有多少贡品进入竞技场,只有凯特尼斯和皮塔能成为胜利者。你必须无情地削减你的需求,以适应这两个寄存器,决定什么要优先考虑,什么要牺牲。同样,就像《饥饿游戏》一样,我们有时可以弯曲规则。

让我们从一项挑战开始:创建一个备用蜂鸣器 – 1000 Hz ½ 秒,静音 ½ 秒,重复。结果?“哔哔哔哔……”

我们希望有五个变量:

  • half_period:保持电压高然后低以创建 1000 Hz 音调所需的时钟周期数。这是 125,000,000 / 1000 / 2 = 62,500 个高周期和 62,500 个低周期。

生成 1000 Hz 方波音调所需的电压和时序(毫秒和时钟周期)

生成 1000 Hz 方波音调所需的电压和时序(毫秒和时钟周期)。

  • y:循环计数器从 0 到 half_period 以创建延迟。

  • period_count:填充 ½ 秒时间所需的重复周期数。125,000,000 × 0.5 / (62,500 × 2) = 500。

  • x:循环计数器从 0 到 period_count 以填充 ½ 秒时间。

  • silence_cycles:½ 秒静音所需的时钟周期数。125,000,000 × 0.5 = 62,500,000。

备用蜂鸣器示意图

我们想要五个寄存器,但只能有两个,所以游戏开始了!愿好运永远在您这边。

首先,我们可以消除 silence_cycles,因为它可以表示为 half_period × period_count × 2。虽然 PIO 不支持乘法,但它支持循环。通过嵌套两个循环——其中内部循环延迟 2 个时钟周期——我们可以创建 62,500,000 个时钟周期的延迟。

减少了一个变量,但我们如何消除另外两个呢?幸运的是,我们不必这样做。虽然 PIO 只提供了两个通用寄存器,xy,但它还包括两个专用寄存器:osr(输出移位寄存器)和 isr(输入移位寄存器)。

我们一会儿将看到的 PIO 代码实现了备用蜂鸣器。这是它的工作原理:

初始化

  • pull block 指令从缓冲区读取音调的半周期(62,500 个时钟周期)并将其值放入 osr

  • 然后将该值复制到 isr 以供以后使用。

  • 第二个 pull block 从缓冲区读取周期计数(500 次重复)并将其值放入 osr,我们将其保留。

蜂鸣循环

  • mov x, osr 指令将周期计数复制到 x 寄存器中,它作为外部循环计数器。

  • 对于内部循环,mov y, isr 重复将半周期复制到 y 中,以创建音调的高低状态延迟。

静音循环

  • 静音循环与蜂鸣循环的结构相似,但不会设置任何引脚,因此它们仅作为延迟使用。

包装和连续执行

  • .wrap_target.wrap 指令定义了状态机的主循环。

  • 在完成蜂鸣和静音循环后,状态机跳回到程序的开始附近,无限重复序列。

在这个大纲的基础上,以下是生成备用蜂鸣器信号的 PIO 汇编代码。

.program backup

; Read initial configuration
    pull block         ; Read the half period of the beep sound
    mov isr, osr      ; Store the half period in ISR
    pull block        ; Read the period_count

.wrap_target          ; Start of the main loop

; Generate the beep sound
    mov x, osr        ; Load period_count into X
beep_loop:
    set pins, 1       ; Set the buzzer to high voltage (start the tone)
    mov y, isr        ; Load the half period into Y
beep_high_delay:
    jmp y--, beep_high_delay    ; Delay for the half period

    set pins, 0       ; Set the buzzer to low voltage (end the tone)
    mov y, isr        ; Load the half period into Y
beep_low_delay:
    jmp y--, beep_low_delay     ; Delay for the low duration

    jmp x--, beep_loop          ; Repeat the beep loop

; Silence between beeps
    mov x, osr        ; Load the period count into X for outer loop
silence_loop:
    mov y, isr        ; Load the half period into Y for inner loop
silence_delay:
    jmp y--, silence_delay [1]  ; Delay for two clock cycles (jmp + 1 extra)

    jmp x--, silence_loop       ; Repeat the silence loop

.wrap                 ; End of the main loop, jumps back to wrap_target

这是配置和运行备用蜂鸣器 PIO 程序的核心 Rust 代码。它使用 Embassy 嵌入式应用程序框架。该函数初始化状态机,计算计时值(half_periodperiod_count),并将它们发送到 PIO。然后它播放蜂鸣序列 5 秒,然后进入一个无限循环。完整的源 文件项目 在 GitHub 上可用。

async fn inner_main(_spawner: Spawner) -> Result<Never> {
    info!("Hello, back_up!");
    let hardware: Hardware<'_> = Hardware::default();
    let mut pio0 = hardware.pio0;
    let state_machine_frequency = embassy_rp::clocks::clk_sys_freq();
    let mut back_up_state_machine = pio0.sm0;
    let buzzer_pio = pio0.common.make_pio_pin(hardware.buzzer);
    back_up_state_machine.set_pin_dirs(Direction::Out, &amp;[&amp;buzzer_pio]);
    back_up_state_machine.set_config(&amp;{
        let mut config = Config::default();
        config.set_set_pins(&amp;[&amp;buzzer_pio]); // For set instruction
        let program_with_defines = pio_file!("examples/backup.pio");
        let program = pio0.common.load_program(&amp;program_with_defines.program);
        config.use_program(&amp;program, &amp;[]);
        config
    });
    back_up_state_machine.set_enable(true);
    let half_period = state_machine_frequency / 1000 / 2;
    let period_count = state_machine_frequency / (half_period * 2) / 2;
    info!(
        "Half period: {}, Period count: {}",
        half_period, period_count
    );
    back_up_state_machine.tx().wait_push(half_period).await;
    back_up_state_machine.tx().wait_push(period_count).await;
    Timer::after(Duration::from_millis(5000)).await;
    info!("Disabling back_up_state_machine");
    back_up_state_machine.set_enable(false);
    // run forever
    loop {
        Timer::after(Duration::from_secs(3_153_600_000)).await; // 100 years
    }
}

当您运行程序时,会发生以下情况:

旁注 1:自行运行在 Pico 上运行 Rust 代码最简单但往往令人沮丧的方法是在您的桌面上交叉编译它,并手动复制生成的文件。一个更好的方法是投资 $12 的 Raspberry Pi 调试探针,并在您的桌面上设置 [probe-rs](https://probe.rs/)。使用这种设置,您可以使用 cargo run 在桌面上自动编译,复制到您的 Pico,然后开始运行代码。更好的是,您的 Pico 代码可以使用 info! 语句将消息发送回您的桌面,您可以进行交互式断点调试。有关设置说明,请访问 probe-rs 网站。

要听到声音,我连接了一个无源蜂鸣器、一个电阻和一个晶体管到 Pico。有关详细的布线图和零件清单,请查看 SunFounder 的 Kepler Kit 中的 无源蜂鸣器说明

旁注 2:如果您唯一的目标是用 Pico 生成音调,则不需要 PIO。MicroPython 足够快,可以直接 切换引脚,或者您可以使用 Pico 内置的 脉冲宽度调制 (PWM) 功能

注册饥饿游戏的替代结局

我们使用了四个寄存器——两个通用和两个特殊——来解决这个挑战。如果这个解决方案感觉不够令人满意,这里有一些可以考虑的替代方法:

使用常量:为什么要把 half_periodperiod_countsilence_cycles 变量都设置为变量?将常量“62,500”、“500”和“62,500,000”直接硬编码可以简化设计。然而,PIO 常量有一些限制,我们将在 Wat 5 中探讨。

Pack Bits: 寄存器可以存储 32 位。我们真的需要两个寄存器(2×32=64 位)来存储half_periodperiod_count吗?不。存储 62,500 只需要 16 位,而 500 只需要 9 位。我们可以将这些值打包到一个寄存器中,并使用out指令将值移位到xy。这种方法可以释放osrisr供其他任务使用,但一次只能释放一个——另一个寄存器必须持有打包的值。

慢动作:在 Rust 的 Embassy 框架中,你可以通过设置其clock_divider来配置一个运行在较低频率的 PIO 状态机。这允许状态机以高达~1907 Hz 的速度运行。以较慢的速度运行状态机意味着像half_period这样的值可以更小,可能小到2。较小的值更容易作为常量硬编码,并且可以更紧凑地打包到寄存器中。

寄存器饥饿游戏的快乐结局

寄存器饥饿游戏要求战略性的牺牲和创造性的解决方案,但通过利用 PIO 的特殊寄存器和巧妙的循环结构,我们最终取得了胜利。如果风险更高,其他技术可能有助于我们适应和生存。

但在一个领域的胜利并不意味着挑战就此结束。在下一个 Wat 中,我们面临新的考验:PIO 严格的 32 条指令限制。

Wat 2:32 指令的便携式行李箱

恭喜!你只需花费$4 就购买了一次环球旅行的机会。但是,有一个条件?你必须把所有物品都塞进一个小型的便携式行李箱。同样,PIO 程序允许你创建令人难以置信的功能,但每个 PIO 程序都限制在仅 32 条指令。

Wat! 只有 32 条指令?这空间太小,无法装下所有你需要的东西!但通过巧妙的规划,你通常可以使其工作。

规则

  • 没有 PIO 程序可以超过 32 条指令。

  • wrap_targetwrap指令不计入。

  • 标签不计入。

  • Pico 1 包括八个状态机,分为两个四块。Pico 2 包括十二个状态机,分为三个四块。每个块共享 32 条指令槽。因此,由于一个块中的四个状态机都从相同的 32 条指令池中获取,如果一个机器的程序使用了所有 32 个槽位,那么其他三个就没有空间了。

当你的行李箱打不开时

如果你的想法不适合 PIO 指令槽,这些打包技巧可能有所帮助。**(免责声明:我并没有亲自尝试过所有这些。)*

let half_period = state_machine_frequency / 1000 / 2;
back_up_state_machine.tx().push(half_period); // Using non-blocking push since FIFO is empty
let pull_block = pio_asm!("pull block").program.code[0];
unsafe {
    back_up_state_machine.exec_instr(pull_block);
}
  • 使用 PIO 的exec命令:在你的状态机中,你可以使用 PIO 的exec机制动态执行指令。例如,你可以使用out exec执行存储在osr中的指令值。或者,你可以使用mov exec, xmov exec, y直接从这些寄存器中执行指令。

现在行李打包好了,让我们加入多利特博士寻找传说中的生物的队伍。

奖励 Wat 2.5:多利特博士的 PIO Pushmi-Pullyu

两位读者指出我遗漏了一个重要的 PIO Wat——所以这里有一个额外的内容! 当编程 PIO 时,你会注意到一些奇怪的事情:

  • PIO pull 指令从 TX FIFO(发送缓冲区)接收值并将它们输入到输出移位寄存器(osr)。因此,它从输出输入并从接收发送。

  • 同样,PIO push 指令从输入移位寄存器(isr)输出值并将它们发送到 RX FIFO(接收缓冲区)。因此,它从输入输出并从发送接收。

Wat!? 就像来自《多利特医生》故事的两位头Pushmi-Pullyu一样,似乎有些颠倒。但当你意识到 PIO 大多数事物都是从主机视角(MicroPython、Rust、C/C++)命名,而不是从 PIO 程序的角度看时,它开始变得有道理。

此表总结了指令、寄存器和缓冲区名称。(“FIFO”代表先进先出。)

拿着 Pushmi-Pullyu,我们接下来转向一个神秘场景。

Wat 3: 拉取无阻塞“神秘”

Wat 1中,我们将音频硬件编程为备用蜂鸣器。但这并不是我们乐器所需要的。相反,我们想要一个无限播放给定音调的 PIO 程序——直到它被告知播放新的音调。程序还应等待在给定特殊的“休息”音调时保持沉默。

在提供新的音调之前保持休息状态,使用pull block编程很容易——我们将在下面探讨细节。在特定频率播放音调也是直截了当的,建立在我们在Wat 1中完成的工作之上。

但我们如何检查新的音调,同时继续播放当前的音调呢?答案在于在pull noblock中使用“noblock”而不是“block”。现在,如果有新值,它将被加载到osr中,使程序能够无缝更新。

这里神秘开始的地方:如果调用pull noblock而没有新值,osr会发生什么?

我假设它会保持其之前的值。错了!也许它被重置为 0?再次错了!令人惊讶的真相:它得到x的值。为什么?(不,不是y——是x。)因为Pico SDK这么说。具体来说,3.4.9.2 节解释道:

在空 FIFO 上非阻塞的 PULL 与 MOV OSR, X 的效果相同。

了解pull noblock的工作原理很重要,但这里有一个更大的教训。将Pico SDK 文档像神秘小说的背面一样对待。不要试图独自解决所有问题——作弊!跳到“谁干的”部分,在 3.4 节中,仔细阅读你使用的每个命令的详细信息。阅读几段内容可以节省你数小时的困惑。

旁白:当即使 SDK 文档感觉不清楚时,转向 RP2040(Pico 1)和 RP2350(Pico 2)数据手册。这些百科全书——分别有 600 页和 1,300 页——就像全能的叙述者:它们提供了事实真相。

考虑到这一点,让我们看看一个实际例子。下面是播放持续音调和休止符的 PIO 程序。它使用 pull block 在休止期间等待输入,并使用 pull noblock 在播放音调时检查更新。

.program sound

;  Rest until a new tone is received.
resting:
    pull block           ; Wait for a new delay value
    mov x, osr           ; Copy delay into X
    jmp !x resting       ; If delay is zero, keep resting

; Play the tone until a new delay is received.
.wrap_target             ; Start of the main loop
    set pins, 1          ; Set the buzzer high voltage.
high_voltage_loop:
    jmp x-- high_voltage_loop   ; Delay

    set pins, 0          ; Set the buzzer low voltage.
    mov x, osr           ; Load the half period into X.
low_voltage_loop:
    jmp x-- low_voltage_loop    ; Delay

; Read any new delay value. If none, keep the current delay.
    mov x, osr          ; set x, the default value for "pull(noblock)"
    pull noblock        ; Read a new delay value or use the default.

; If the new delay is zero, rest. Otherwise, continue playing the tone.    
    mov x, osr          ; Copy the delay into X.
    jmp !x resting      ; If X is zero, rest.
.wrap ; Continue playing the sound.

我们最终会在我们的类似 theremin 的乐器中使用这个 PIO 程序。现在,让我们通过播放熟悉的旋律来查看 PIO 程序的实际效果。这个演示使用“Twinkle, Twinkle, Little Star”来展示如何通过向状态机提供频率和持续时间来控制旋律。只需这段代码 (完整文件项目),你就可以让 Pico 唱歌!

const TWINKLE_TWINKLE: [(u32, u64, &amp;str); 16] = [
    // Bar 1
    (262, 400, "Twin-"), // C
    (262, 400, "-kle"),  // C
    (392, 400, "twin-"), // G
    (392, 400, "-kle"),  // G
    (440, 400, "lit-"),  // A
    (440, 400, "-tle"),  // A
    (392, 800, "star"),  // G
    (0, 400, ""),        // rest
    // Bar 2
    (349, 400, "How"),  // F
    (349, 400, "I"),    // F
    (330, 400, "won-"), // E
    (330, 400, "-der"), // E
    (294, 400, "what"), // D
    (294, 400, "you"),  // D
    (262, 800, "are"),  // C
    (0, 400, ""),       // rest
];
async fn inner_main(_spawner: Spawner) -> Result<Never> {
    info!("Hello, sound!");
    let hardware: Hardware<'_> = Hardware::default();
    let mut pio0 = hardware.pio0;
    let state_machine_frequency = embassy_rp::clocks::clk_sys_freq();
    let mut sound_state_machine = pio0.sm0;
    let buzzer_pio = pio0.common.make_pio_pin(hardware.buzzer);
    sound_state_machine.set_pin_dirs(Direction::Out, &amp;[&amp;buzzer_pio]);
    sound_state_machine.set_config(&amp;{
        let mut config = Config::default();
        config.set_set_pins(&amp;[&amp;buzzer_pio]); // For set instruction
        let program_with_defines = pio_file!("examples/sound.pio");
        let program = pio0.common.load_program(&amp;program_with_defines.program);
        config.use_program(&amp;program, &amp;[]);
        config
    });
    sound_state_machine.set_enable(true);
    for (frequency, ms, lyrics) in TWINKLE_TWINKLE.iter() {
        if *frequency > 0 {
            let half_period = state_machine_frequency / frequency / 2;
            info!("{} -- Frequency: {}", lyrics, frequency);
            // Send the half period to the PIO state machine
            sound_state_machine.tx().wait_push(half_period).await;
            Timer::after(Duration::from_millis(*ms)).await; // Wait as the tone plays
            sound_state_machine.tx().wait_push(0).await; // Stop the tone
            Timer::after(Duration::from_millis(50)).await; // Give a short pause between notes
        } else {
            sound_state_machine.tx().wait_push(0).await; // Play a silent rust
            Timer::after(Duration::from_millis(*ms + 50)).await; // Wait for the rest duration + a short pause
        }
    }
    info!("Disabling sound_state_machine");
    sound_state_machine.set_enable(false);
    // run forever
    loop {
        Timer::after(Duration::from_secs(3_153_600_000)).await; // 100 years
    }
}

当你运行程序时,会发生以下情况:

我们解决了一个谜团,但总会有另一个挑战潜伏在角落里。在 Wat 4 中,我们将探讨当你的智能硬件带有陷阱——它也非常便宜时会发生什么。

Wat 4:智能、便宜硬件:情感过山车

在声音工作后,我们转向使用 HC-SR04+ 超声波测距仪测量音乐家手的距离。这个小巧但强大的设备售价不到两美元。

HC-SR04+ 测距仪(笔用于标尺。)

这个小外围设备让我经历了“Wats!?!”的情感过山车:

  • 向上:令人惊讶的是,这个 2 美元的测距仪包含自己的微控制器,使其更智能且易于使用。

  • 向下:令人沮丧的是,那种“智能”的行为并不直观。

  • 向上:方便的是,Pico 可以用 3.3V 或 5V 的电源为外围设备供电。

  • 向下:不可预测的是,许多测距仪在 3.3V 下不可靠——或者直接失败——,并且在 5V 下可能会损坏你的 Pico。

  • 向上:幸运的是,损坏的测距仪和 Pico 都很便宜,并且测距仪的双电压版本解决了我的问题。

详细信息

我最初认为当回声返回时,测距仪会将 Echo 引脚置高。我错了。

相反,测距仪会发出 8 个 40 kHz 的超声波脉冲模式(把它想象成狗的备用蜂鸣器)。紧接着,它将 Echo 置高。然后 Pico 应该开始测量 Echo 降至低的时间,这表示传感器检测到了模式——或者它超时了。

至于电压,文档指定测距仪在 5V 下运行。它似乎在 3.3V 下也能工作——直到它不能。大约在同一时间,当我的 Pico 继续使用 Rust(通过调试探针probe-rs)工作时,它停止了与任何 MicroPython IDE 的工作,这些 IDE 依赖于特殊的 USB 协议。

因此,此时 Pico 和测距仪都损坏了。

在尝试了各种电缆、USB 驱动程序、编程语言,甚至一个旧的仅 5V 范围的测距仪后,我终于通过以下方式解决了问题:

  • 继续使用 Rust 的 Pico,但切换到另一个 Pico 用于 MicroPython。

  • 购买一个新的双电压 3.3/5V 测距仪,仍然每件只需 2 美元。

Wat 4: 经验教训

当过山车回到站台时,我学到了两个关键教训。首先,多亏了微控制器,即使是简单的硬件也可能以非直观的方式表现,这需要仔细阅读文档。其次,虽然这款硬件很聪明,但它也很便宜——这意味着它容易出故障。当它出故障时,深呼吸,记住它只值几美元,然后更换它。

然而,硬件的怪癖只是故事的一部分。在Wat 5的第二部分中,我们将把注意力转回软件:PIO 编程语言本身。我们将揭示一种如此出乎意料的行为,可能会让你质疑你所知道的一切关于常量的知识。


这些就是用 MicroPython 编程 Pico PIO 的前四个 Wat。你可以在 GitHub 上找到项目的代码

第二部分中,我们将探索 Wat 5 到 9。这些将涵盖不稳定的常量、透过玻璃的条件、跳过跳跃、过多的引脚和笨拙的调试。我们还将揭示完成乐器的代码。

关注我在 Medium 上的账号,获取关于这篇文章和未来文章的更新通知。我主要撰写关于 Rust 和 Python 的科学编程、机器学习和统计学方面的文章。我通常每月发布一篇文章。

posted @ 2026-03-27 10:41  布客飞龙IV  阅读(1)  评论(0)    收藏  举报