搭建开发环境

AVR

搭建开发环境

在Arduino上使用Rust需要一些额外的步骤,因为Arduino通常使用C/C++编程语言。在阅读本书之前,需要一些预备知识:

  • 熟悉Rust语言。本书假设你已完整阅读过The Rust Programming Language,能够熟练使用Rust语言开发应用程序。
  • 需要了解Rust Embedded开发的基本概念。本书假设你已完整阅读过The Embedded Rust Book的Introduction部分,了解Rust进行嵌入式开发的基本架构。

工具

搭建完整的开发环境,你需要预先准备一些软件和硬件

软件

  • 一台可以编写、编译和写入程序到板卡的开发电脑
  • 安装了Cargo,并用Cargo安装了cargo-generate
  • 安装了nightly版本的Rust编译器

硬件

  • Arduino Uno

    本书编写时,使用的VS Code作为IDE,本书的示例演示一般使用终端或者串行监视器。

安装和设置

您需要一个nightly版本的Rust编译器来为AVR编译Rust代码。Rust版本可以在稍后查看模板生成的项目脚手架中的rust-toolchain.toml文件,编译时将将自动安装正确的版本。 安装依赖:

  • Ubuntu

    sudo apt install avr-libc gcc-avr pkg-config avrdude libudev-dev build-essential
    
  • Macos

    xcode-select --install # if you haven't already done so
    brew tap osx-cross/avr
    brew install avr-gcc avrdude
    
  • Windows

    在Windows 10和11上使用Winget

    winget install AVRDudes.AVRDUDE ZakKemble.avr-gcc
    

    在更早的系统上,你需要先使用Powershell安装Scoop

    Set-ExecutionPolicy RemoteSigned -Scope CurrentUser # Needed to run a remote script the first time
    irm get.scoop.sh | iex
    

    之后安装avr-gcc和avrdude

    scoop install avr-gcc
    scoop install avrdude
    

然后,安装ravedude,一个用来集成到cargo工作流中无缝刷写您的板卡的工具:

cargo +stable install ravedude

如果您使用VS Code的Arduino插件,插件会自动给你安装avr-gcc和avrdude。请根据IDE在.vscode下自动生成的c_cpp_properties.jsonincludePath找到avr-gcc和avrdude的目录。

创建项目

您可以使用github模板来创建您的项目:

cargo generate --git https://github.com/Rahix/avr-hal-template.git

运行

将您的Arduino连接到开发电脑上,修改项目配置文件,以在您的板和计算机之间以每秒 57600 位数据的速度进行串行通信,修改.cargo/cargo.toml中[targe.'cfg(target_arch = "avr")']下的runner配置

[target.'cfg(target_arch = "avr")']
runner = "ravedude uno -b 57600 -P /dev/tty.usbmodem14101"

打开生成的项目中的src/main.rs文件,输入下列代码:

/*!
 * Toggle an LED on and off every second.
 *
 * This example shows you how toggle LED on and off every second.
 */
#![no_std]
#![no_main]

use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut led = pins.d13.into_output();

    loop {
        led.toggle();
        arduino_hal::delay_ms(1000);
    }
}

在项目根目录的终端中执行

cargo build
cargo run

如果您看到板卡上的绿色LED闪烁,证明您的开发环境已经搭建成功。修改delay_ms中的值,可以使LED闪烁的频率发生变化。

参考内容

基础

模拟读取串行

读取电位计,将其状态打印到终端或者VS Code的串行监视器。

此示例向您展示如何使用电位计从物理世界读取模拟输入。 电位计是一种简单的机械装置,当其轴转动时,它会提供不同大小的阻力。 通过将电压通过电位计传递到板上的模拟输入,可以将电位计(或简称电位器)产生的电阻值测量为模拟值。 在此示例中,您将在 Arduino 和VS Code的计算机之间建立串行通信后监视电位计的状态。

硬件要求

  • Arduino板卡
  • 10k欧电位器

电路

将电位计的三根线连接到电路板上。 第一个从电位计的一个外部引脚接地。 第二个从电位计的另一个外部引脚施加到 5 伏电压。 第三个从电位计的中间引脚到模拟引脚 A0。

通过转动电位计的轴,可以改变连接到电位计中心销的游标两侧的电阻值。 这会改变中心引脚的电压。 当中心与连接5伏的一侧之间的电阻接近于零(而另一侧的电阻接近10k欧姆)时,中心引脚的电压接近5伏。 当电阻反向时,中心引脚的电压接近 0 伏或接地。 该电压是您作为输入读取的模拟电压。

Arduino 板内部有一个称为模数转换器或 ADC 的电路,它读取此变化的电压并将其转换为 0 到 1023 之间的数字。当轴沿一个方向转动到底时,会有 0 伏电压 到引脚,输入值为 0。当轴沿相反方向转动到底时,有 5 伏电压流向引脚,输入值为 1023。在这期间,analog_read() 返回 0 之间的数字 1023 与施加到引脚的电压量成正比。

电路图

模拟读取串行

代码

创建串口连接

let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

创建ADC连接

let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());

获取A0引脚并设置为数据输入

let a0 = pins.a0.into_analog_input(&mut adc);

接下来,在代码的主循环中,您需要建立一个变量来存储来自电位计的电阻值(介于 0 到 1023 之间,该变量为u16类型):

let sensor_value = a0.analog_read(&mut adc);

最后,您需要将此信息打印到串行监视器:

ufmt::uwriteln!(&mut serial, "{}",sensor_value).unwrap_infallible();

编译并运行示例

cargo build
cargo run

通过VS Code的串行监视器连接到板卡串口,您应该会看到稳定的 范围从 0 到 1023 的数字流,与旋钮的位置相关。 当您转动电位器时,这些数字几乎会立即响应。

完整代码如下: src/main.js

/*!
 * Show readouts of analog input on analog pin 0.
 *
 * This example shows you how to read an analog input on analog pin 0,
 * convert the values from analogRead() into voltage, and print it out to the serial monitor.
 */
#![no_std]
#![no_main]

use arduino_hal::prelude::*;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut serial = arduino_hal::default_serial!(dp,pins,57600);
    let mut adc = arduino_hal::Adc::new(dp.ADC,Default::default());
    let a0 = pins.a0.into_analog_input(&mut adc);

    loop {
        let sensor_value = a0.analog_read(&mut adc);

        ufmt::uwriteln!(&mut serial, "{}", sensor_value).unwrap_infallible();

        arduino_hal::delay_ms(1000);
    }
}

闪烁

每秒开关LED一次

此示例展示了使用 Arduino 可以执行的最简单的操作来查看物理输出:它使板载 LED 闪烁。

硬件要求

  • Arduino板卡

可选

  • LED
  • 220欧姆电阻

电路

此示例使用大多数 Arduino 板都具有的内置 LED。 该 LED 连接到数字引脚,其数量可能因板类型而异。 以下是板卡和数字引脚之间的对应关系。

  • D13 - 101
  • D13 - Due
  • D1 - Gemma
  • D13 - Intel Edison
  • D13 - Intel Galileo Gen2
  • D13 - Leonardo and Micro
  • D13 - LilyPad
  • D13 - LilyPad USB
  • D13 - MEGA2560
  • D13 - Mini
  • D6 - MKR1000
  • D13 - Nano
  • D13 - Pro
  • D13 - Pro Mini
  • D13 - UNO
  • D13 - Yún
  • D13 - Zero 如果您想用此草图点亮外部 LED,则需要构建此电路,将电阻器的一端连接到非内置LED对应的数字引脚。 将 LED 的长腿(正极腿,称为阳极)连接到电阻器的另一端。 将 LED 的短脚(负脚,称为阴极)连接到 GND。 与 LED 串联的电阻值可以是与 220 欧姆不同的值; 当电阻值高达 1K 欧姆时,LED 也会亮起。

代码

获取led引脚并设置为数据输出

let mut led = pins.d13.into_output();

切换led状态:

led.toggle();

编译并运行示例

cargo build
cargo run

您希望有足够的时间让人们看到更改,因此,delay_ms() 命令告诉主板在 1000 毫秒(即一秒)内不执行任何操作。 当您使用delay_ms()命令时,在这段时间内不会发生任何其他事情。 了解基本示例后,请查看 无延迟闪烁 示例以了解如何在执行其他操作时创建延迟。

了解此示例后,请查看 读取串口数字信号示例以了解如何读取连接到电路板的开关。

完整代码如下: src/main.js

/*!
 * Toggle an LED on and off every second.
 *
 * This example shows you how toggle LED on and off every second.
 */
#![no_std]
#![no_main]

use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut led = pins.d13.into_output();

    loop {
        led.toggle();
        arduino_hal::delay_ms(1000);
    }
}

数字读取串行

读取开关,将状态打印到串行监视器。

此示例向您展示如何通过 USB 在 Arduino 和计算机之间建立串行通信来监控开关的状态。

硬件要求

  • Arduino板卡
  • 瞬时开关、按钮或拨动开关
  • 10k欧电阻
  • 连接线
  • 面包板

电路

将三根电线连接到板上。 前两个(红色和黑色)连接到面包板侧面的两个长垂直行,以提供对 5 伏电源和接地的访问。 第三根电线从数字引脚 2 连接到按钮的一条腿。 按钮的同一条腿通过下拉电阻(此处为 10k 欧姆)连接到地。 按钮的另一条腿连接到 5 伏电源。

当您按下按钮或开关时,它们会连接电路中的两点。当按钮打开(未按下)时,按钮的两个引脚之间没有连接,因此该引脚接地(通过下拉电阻)并读取为低电平或 0。当按钮关闭(按下)时),它在两个引脚之间建立连接,将引脚连接到 5 伏,以便引脚读取为高电平或 1。

如果您断开数字 I/O 引脚与所有设备的连接,其读数可能会发生不稳定变化。这是因为输入是“浮动”的 - 也就是说,它没有与电压或接地牢固连接,并且它会随机返回高电平或低电平。这就是电路中需要下拉电阻的原因。

电路图

数字读取串行

代码

创建串口连接

let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

获取d2引脚并设置为数据输入

let pin = pins.d2.into_floating_input().downgrade();

判断引脚输入是否为高电平,并转换为十进制数

let sensor_value = pin.is_high() as u8;

最后,您需要将此信息打印到串行监视器:

ufmt::uwriteln!(&mut serial, "{}",sensor_value).unwrap_infallible();

编译并运行示例

cargo build
cargo run

通过VS Code的串行监视器连接到板卡串口,如果开关打开,您将看到一串“0”;如果开关关闭,您将看到“1”。

完整代码如下: src/main.rs

/*!
 * Read a switch, print the state out to the Serial Monitor.
 *
 * This example shows you how to read a switch state input on digital pin 2.
 */
#![no_std]
#![no_main]

use arduino_hal::prelude::*;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

    let pin = pins.d2.into_floating_input().downgrade();

    loop {
        let sensor_value = pin.is_high() as u8;

        ufmt::uwriteln!(&mut serial, "{}", sensor_value).unwrap_infallible();

        arduino_hal::delay_ms(1000);
    }
}

渐显/渐隐LED

演示如何使用模拟输出来淡化 LED。

此示例演示了如何使用 AnalogWrite() 函数使 LED 淡入淡出。 AnalogWrite 使用脉冲宽度调制 (PWM),以不同的开和关比率快速打开和关闭数字引脚,以产生淡入淡出效果。

硬件要求

  • Arduino板卡
  • LED
  • 220欧电阻
  • 连接线
  • 面包板

电路

通过 220 欧姆电阻将 LED 的阳极(较长的正极腿)连接到板上的数字输出引脚 9。将阴极(较短的负极腿)直接接地。

在 Arduino Uno 上,可以在数字I/O引脚 3、5、6、9、10 和 11 上进行 PWM 输出。

电路图

渐显/渐隐LED

代码

我们使用simple_pwm模块中的timer来创建脉冲。simple_pwd为我们提供了3个脉冲时钟对应不同的PWM时钟以对应不同的数字I/O引脚。

  • Timer0Pwm:为PWM使用TC0(对应PD5、PD6,即引脚5和引脚6)
  • Timer1Pwm:为PWM使用TC1(对应PB1、PB2,即引脚9和引脚10)
  • Timer2Pwm:为PWM使用TC2(对应PB3、PD3,即引脚3和引脚11)

我们的LED连接在引脚9上,对应PB1,因此使用Timer1Pwm。

另外,我们还需要通过预分频器设置脉冲产生的频率,频率和时钟的对应关系如下:

Prescaler16 MHz Clock8 MHz Clock
Direct62.5 kHz31.3 kHz
Prescale87.81 kHz3.91 kHz
Prescale64977 Hz488 Hz
Prescale256244 Hz122 Hz
Prescale102461.0 Hz30.5 Hz

我们使用Prescale64来设置脉冲时钟,为977Hz,每个脉冲的时间大概为1ms。 我们将引脚9转换为一个PWD输出的LED。

let mut pwm_led = pins.d9.into_output().into_pwm(&Timer1Pwm::new(dp.TC1, Prescaler::Prescale64));

此处有两种方式控制LED的变化:

  • 使用set_duty() 在程序主循环中,我们通过设置PWD的duty来控制每轮脉冲的占空比,以达到控制LED亮度的目的。每次循环等待10ms,也就是大概10个脉冲

    for x in (0..=255).chain((0..=254).rev()) {
      pwm_led.set_duty(x%5);
      arduino_hal::delay_ms(10);
    }

    编译并运行示例

    cargo build
    cargo run
    

    完整代码如下:

    src/main.rs

    /*!
    * Demonstrates the use of analog output to fade an LED.
    *
    * This example demonstrates the use of arduino_hal::simple_pwm::IntoPwmPin trait to convert pin to a PWM pin and
    * use set_duty() function in fading an LED off and on.
    * simple_pwd module uses pulse width modulation (PWM), turning a digital pin on and off very quickly with different 
    * ratio between on and off, to create a fading effect.
    */
    #![no_std]
    #![no_main]
    
    use arduino_hal::simple_pwm::*;
    use panic_halt as _;
    
    #[arduino_hal::entry]
    fn main() -> ! {
        let dp = arduino_hal::Peripherals::take().unwrap();
        let pins = arduino_hal::pins!(dp);
    
        // Digital pin 9 is connected to a LED and a resistor in series
        let mut pwm_led = pins
            .d9
            .into_output()
            .into_pwm(&Timer1Pwm::new(dp.TC1, Prescaler::Prescale64));
        pwm_led.enable();
    
        loop {
            for x in (0..=255).chain((0..=254).rev()) {
                pwm_led.set_duty(x);
                arduino_hal::delay_ms(10);
            }
        }
    }
  • 使用embedded_hal的trait

    注意:此处需要修改模版项目依赖中的embedded_hal为最新版本

    创建一个embedded_hal兼容的arduino::Delay变量

    let mut delay = arduino_hal::Delay::new();

    设置LED的duty循环的百分比,并使用delay来控制延迟

    for pct in (0..=100).chain((0..100).rev()) {
    	led.set_duty_cycle_percent(pct).unwrap();
    	delay.delay_ms(10);
    }

    编译并运行示例

    cargo build
    cargo run
    

    完整代码如下:

    src/main.rs

    /*!
     * Demonstrates the use of analog output to fade an LED.
     *
     * This example demonstrates the use of arduino_hal::simple_pwm::IntoPwmPin trait to convert pin to a PWM pin and
     * use set_duty() function in fading an LED off and on.
     * simple_pwd module uses pulse width modulation (PWM), turning a digital pin on and off very quickly with different
     * ratio between on and off, to create a fading effect.
     * this app is not aware of `avr-hal` and only uses `embedded-hal` traits.
     */
    #![no_std]
    #![no_main]
    
    use arduino_hal::simple_pwm::*;
    use embedded_hal::delay::DelayNs;
    use embedded_hal::pwm::SetDutyCycle;
    use panic_halt as _;
    
    #[arduino_hal::entry]
    fn main() -> ! {
        let dp = arduino_hal::Peripherals::take().unwrap();
        let pins = arduino_hal::pins!(dp);
    
        // Digital pin 9 is connected to a LED and a resistor in series
        let mut pwm_led = pins
            .d9
            .into_output()
            .into_pwm(&Timer1Pwm::new(dp.TC1, Prescaler::Prescale64));
        pwm_led.enable();
    
        let mut delay = arduino_hal::Delay::new();
    
        loop {
            for pct in (0..=100).chain((0..100).rev()) {
                pwm_led.set_duty_cycle_percent(pct).unwrap();
                delay.delay_ms(10);
            }
        }
    }

此时,你可以看到LED逐渐变亮,然后变暗。

读取模拟电压

读取模拟输入并将电压打印到串行监视器

此示例向您展示如何读取模拟引脚 0 上的模拟输入,将值转换为电压,并将其打印到终端或者VS Code的串行监视器。

硬件要求

  • Arduino板卡
  • 10k欧姆电位器

电路

将电位计的三根线连接到电路板上。 第一个从电位计的一个外部引脚接地。 第二个从电位计的另一个外部引脚电压变为 5 伏。 第三个从电位计的中间引脚连接到模拟输入 0。

通过转动电位计的轴,可以改变连接到电位计中心销的游标两侧的电阻值。 这会改变中心引脚的电压。 当中心与连接5伏的一侧之间的电阻接近于零(而另一侧的电阻接近10k欧)时,中心引脚的电压接近5伏。 当电阻反向时,中心引脚的电压接近 0 伏或接地。 该电压是您作为输入读取的模拟电压。

该板的微控制器内部有一个称为模数转换器或 ADC 的电路,它读取此变化的电压并将其转换为 0 到 1023 之间的数字。当轴沿一个方向转动到底时,有 0 引脚上有 5 伏电压,输入值为 0。当轴沿相反方向旋转到底时,引脚上有 5 伏电压,输入值为 1023。在这期间,analog_read() 返回一个数字 0 到 1023 之间,与施加到引脚的电压量成正比。

电路图

读取模拟电压

代码

创建串口连接

let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

创建ADC连接

let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());

获取A0引脚并设置为数据输入

let a0 = pins.a0.into_analog_input(&mut adc);

接下来,在代码的主循环中,您需要建立一个变量来存储来自电位计的电阻值(介于 0 到 1023 之间,该变量为u16类型):

let sensor_value = a0.analog_read(&mut adc);

要将值从 0-1023 更改为与引脚正在读取的电压相对应的范围,您需要创建另一个变量(浮点数),并进行一些数学运算。 要缩放 0.0 到 5.0 之间的数字,请将 5.0 除以 1023.0,然后乘以sensor_value:

let voltage = (sensor_value as f32) * 5.0 / 1023.0;

最后,您需要将此信息打印到串行监视器:

ufmt::uwriteln!(&mut serial, "{}",uFmt_f32::Two(voltage)).unwrap_infallible();

注意:此处需要在Cargo.toml中所有profile的配置中加入overflow-check=false。否则,编译链接avr-gcc时会报错。这个问题可以参见github上的这个issue的相关讨论:-Zbuild-std + lto="fat" = undefined reference to core::panicking::panic

编译并运行示例

cargo build
cargo run

完整代码如下:

src/main.rs

/*!
 * Read readouts of A0 ADC channels and convert it to voltage.
 *
 * This example shows you how to read an analog input on analog pin 0, 
 * convert the values from analogRead() into voltage, and print it out to the serial monitor.
 */
#![no_std]
#![no_main]

use arduino_hal::prelude::*;
use panic_halt as _;
use ufmt_float::uFmt_f32;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

    let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());

    let a0 = pins.a0.into_analog_input(&mut adc);

    loop {
        let sensor_value = a0.analog_read(&mut adc);

        let voltage = (sensor_value as f32) * 5.0 / 1023.0;

        ufmt::uwriteln!(&mut serial, "{}",uFmt_f32::Two(voltage)).unwrap_infallible();

        arduino_hal::delay_ms(1000);
    }
}

编写millis()函数

代码

在C++代码中,因为Arduino上没有任何类型的时钟,所以,我们只能通过millis()来获得程序开始后运行的时间。而std::time::SystemTime::now()位于标准库中,我们在嵌入式环境下无法使用,所以,我们需要自己编写一个简单函数来实现此功能。关于此函数实现的详细分析,可以参看Write your own Arduino millis() in Rust

因为ATMega328P的时钟频率是16MHz,如果我们需要得到一个精度在1ms的计时器,因此,计时器的工作频率在1/0.001=1kHz。

这里,我们就需要用预分频从CPU时钟得到一个频率更低的时钟。预分频器有Direct,8,64,256,1024几种。预分频后的频率为CPU频率/预分频值,假设预分频值选择64,即得到的时钟频率为16M/64=250k。因此,我们经过250次时钟tick,就是1ms。

const PRESCALER: u32 = 64;
const TIMER_COUNTS: u32 = 250;

const MILLIS_INCREMENT: u32 = PRESCALER * TIMER_COUNTS / 16000;

我们需要开辟一个内存空间(Cell),记录下这个毫秒值。因为这个时钟值是全局共享的,考虑到并发访问时,对这个变量的访问必须是线程安全的,因此,我们使用一个Mutex类型来保证每次只有一个线程会读写这个值。

static MILLIS_COUNTER: avr_device::interrupt::Mutex<cell::Cell<u32>> =
    avr_device::interrupt::Mutex::new(cell::Cell::new(0));

接下来,我们就可以使用中断请求来触发,每经过TIMER_COUNTER次定时器的Tick,就对MILLIS_COUNTER触发一次+1操作,这样就在计时器上增加了1ms的计数。也就是说,定时器每数250个数,就恢复到0,那么我们这里就需要使用定时器的CTC模式。CTC模式可以设定一个TOP值,当计数器达到TOP值时,就会触发一次中断。因为我们的TOP值<255,即他可以是一个u8类型,所以,我们可以使用TC0,也是一个8位定时器来作为计时器。ATMega328P一共有三个不同类型的定时器,Timer0、Timer1、Timer2。Timer0和Timer2都是8位定时器,即他们tick可以count的最大数值为255,Timer2比Timer0多了异步请求的特性;而Timer1是16位定时器。因此,我们这里只需要使用Timer0作为millis()函数的定时器即可。为了实现这一点,我们需要设置TC0的控制寄存器CR(Control Register),设置使用分频值为64使用CTC模式的波形生成器,并且设置输出寄存器ocr0a的值为249(减1原则),并且在屏蔽寄存器上设置ocie0a位为1来启用输出比较中断,这样就可以得到我们需要的计时器锯齿波。

pub fn millis_init(tc0: arduino_hal::pac::TC0) {
    // Configure the timer for the above interval (in CTC mode)
    // and enable its interrupt.
    tc0.tccr0a.write(|w| w.wgm0().ctc());
    tc0.ocr0a.write(|w| w.bits(TIMER_COUNTS as u8));
    tc0.tccr0b.write(|w| w.cs0().variant(PRESCALER));
    tc0.timsk0.write(|w| w.ocie0a().set_bit());
}

我们可以在系统标记为暂停接受中断请求后,进行中断处理操作。即用到avr_device::interrupt::free()方法。这个方法可以接受一个执行一次(FnOnce)的函数,即每次接收到中断请求时执行一次中断处理操作。这个函数接受一个类型为CriticalSection的参数,Critical Section是一个保护内存区域,当我们需要对某个共享内存值进行修改的时候,我们可以将当前值的指针Borrow到区域上,这样外部程序就无法访问变量指针,因为这个变量所指向的内存地址已经没有了。当对保护区的数据完成操作后,随着中断处理流程完毕,保护区域的指针被释放,原始变量重新获得了数据的内存地址,那么,修改后的数据就可以访问了。

#[avr_device::interrupt(atmega328p)]
fn TIMER0_COMPA() {
    avr_device::interrupt::free(|cs| {
        let counter_cell = MILLIS_COUNTER.borrow(cs);
        let counter = counter_cell.get();
        counter_cell.set(counter.wrapping_add(1));
    })
}

之后,我们如果需要读取当前的毫秒数,我们只需要在中断处于free上下文的时候,读取MILLIS_COUNTER值即可。

fn millis() -> u32 {
    avr_device::interrupt::free(|cs| MILLIS_COUNTER.borrow(cs).get())
}

我们可以将其放在::util::millis模块里,以供后续项目使用 完整代码如下:

src/utils/millis.rs

use arduino_hal::pac::tc0::tccr0b::CS0_A;
use avr_device::interrupt::Mutex;
use core::cell::Cell;
use panic_halt as _;

/*
 * Possible Values:
 *
 * ╔═══════════╦══════════════╦═══════════════════╗
 * ║ PRESCALER ║ TIMER_COUNTS ║ Overflow Interval ║
 * ╠═══════════╬══════════════╬═══════════════════╣
 * ║        64 ║          250 ║              1 ms ║
 * ║       256 ║          125 ║              2 ms ║
 * ║       256 ║          250 ║              4 ms ║
 * ║      1024 ║          125 ║              8 ms ║
 * ║      1024 ║          250 ║             16 ms ║
 * ╚═══════════╩══════════════╩═══════════════════╝
 */
const PRESCALER: CS0_A = CS0_A::PRESCALE_64;
const TIMER_COUNTS: u32 = 249;

static MILLIS_COUNTER: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));

pub fn millis_init(tc0: arduino_hal::pac::TC0) {
    // Configure the timer for the above interval (in CTC mode)
    // and enable its interrupt.
    tc0.tccr0a.write(|w| w.wgm0().ctc());
    tc0.ocr0a.write(|w| w.bits(TIMER_COUNTS as u8));
    tc0.tccr0b.write(|w| w.cs0().variant(PRESCALER));
    tc0.timsk0.write(|w| w.ocie0a().set_bit());
}

#[avr_device::interrupt(atmega328p)]
fn TIMER0_COMPA() {
    avr_device::interrupt::free(|cs| {
        let counter_cell = MILLIS_COUNTER.borrow(cs);
        let counter = counter_cell.get();
        counter_cell.set(counter.wrapping_add(1));
    })
}

pub fn millis() -> u32 {
    avr_device::interrupt::free(|cs| MILLIS_COUNTER.borrow(cs).get())
}

之后,我们可以编写一个主程序里面测试这个库。

编译并运行示例

cargo build
cargo run

打开Vs Code的串行监视器,连接到板卡,开启监控终端的时间戳,我们可以看到输出的毫秒数到板卡启动的时间差与时间戳的时间差基本上是一致的,可能会有几个毫秒的差距,这是因为ufmt也需要占用串行输出,而他的损耗又比较高,在时间输出上有一定的延迟。

完整代码如下:

src/main.rs

/*!
 * Test millis function
 */
#![no_std]
#![no_main]

use arduino_hal::{default_serial, delay_ms, pins, Peripherals};
use arduino_uno_example::utils::millis::{millis, millis_init};
use avr_device::entry;
use panic_halt as _;

#[entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);
    let mut serial = default_serial!(dp, pins, 57600);

    millis_init(dp.TC0);

    unsafe { avr_device::interrupt::enable() };

    loop {
        let now = millis();
        ufmt::uwriteln!(&mut serial, "now:{}", now).unwrap();
        delay_ms(1000);
    }
}

LED灯带

如何使用外部中断

数字引脚3-6 上的 4 个 LED 依次闪烁。当 D2/INT0 上的外部中断到来时顺序颠倒。

硬件要求

  • Arduino板卡
  • 4个LED
  • 4个220欧姆电阻
  • 按钮
  • 连接线
  • 面包板

电路

将4个LED的正极分别接到数字引脚3-6上,负极通过220欧姆电阻接地。将一个开关一脚接到5v电源,另一角接到数字引脚2上。

板卡可中断引脚表

BOARDDIGITAL PINS USABLE FOR INTERRUPTSNOTES
Uno Rev3, Nano, Mini, other 328-based2, 3
UNO R4 Minima, UNO R4 WiFi2, 3
Uno WiFi Rev2, Nano EveryAll digital pins
Mega, Mega2560, MegaADK2, 3, 18, 19, 20, 21(pins 20 & 21 are not available to use for interrupts while they are used for I2C communication; they also have external pull-ups that cannot be disabled)
Micro, Leonardo0, 1, 2, 3, 7
Zero0-3, 5-13, A0-A5Pin 4 cannot be used as an interrupt.
MKR Family boards0, 1, 4, 5, 6, 7, 8, 9, A1, A2
Nano 33 IoT2, 3, 9, 10, 11, 13, A1, A5, A7
Nano 33 BLE, Nano 33 BLE Sense (rev 1 & 2)all pins
Nano RP2040 Connect0-13, A0-A5
Nano ESP32all pins
GIGA R1 WiFiall pins
Dueall digital pins
101all digital pins(Only pins 2, 5, 7, 8, 10, 11, 12, 13 work with CHANGE)

中断号和引脚对应表

在C/C++中,Arduino API提供了digitalPinToInterrupt(pin)函数获得引脚中断号,而无需将中断号直接放入草图中。具有中断的特定引脚及其与中断号的映射因每种类型的板而异。直接使用中断号可能看起来很简单,但当您的程序在不同的板上运行时,可能会导致兼容性问题。 然而,较旧的草图通常有直接中断号。通常使用数字 0(对于数字引脚 2)或数字 1(对于数字引脚 3)。下表显示了各种板上可用的中断引脚。 请注意,在下表中,中断编号是指传递给attachInterrupt() 的编号。由于历史原因,该编号并不总是直接对应于 ATmega 芯片上的中断编号(例如 int.0 对应于 ATmega2560 芯片上的 INT4)。

BOARDINT.0INT.1INT.2INT.3INT.4INT.5
Uno, Ethernet23
Mega25602321201918
32u4 based (e.g Leonardo, Micro)32017

对于 Uno WiFi Rev2、Due、Zero、MKR 系列和 101 板,中断号 = 引脚号

因此,引脚2对应的中断号为0,在Rust中,我们可以使用中断过程宏来触发中断处理,对应的处理函数名称为INT0

电路图

LED灯带

代码

该程序背后的主要思想是 LED 依次闪烁,首先降低速度,然后增加。

loop {
    blink_for_range(0..10, &mut leds);
    blink_for_range(10..0, &mut leds);
}

循环一组类似的元素(例如引脚集合),很容易将这些引脚直接放入数组中(请记住:像 Vec 这样的更高级的数据结构将需要alloc crate)。然而,每个引脚都有自己的类型,因此不能简单地将它们放入数组中。通常,引脚用于完全不同的原因,因此这个安全网是精心设计的。幸运的是 avr-hal 确实提供了一种实现阵列引脚的方法,称为降级(downgrading)

let mut leds: [Pin<mode::Output>; 4] = [
    pins.d3.into_output().downgrade(),
    pins.d4.into_output().downgrade(),
    pins.d5.into_output().downgrade(),
    pins.d6.into_output().downgrade(),
];

反转序列的一种简单方法是使用 iter_mut() 或 iter_mut().rev()。这里的问题是向前和向后迭代器具有不同的类型。幸运的是,还有另一个crate可以救援。有使用 Haskell 或 Scala 等函数式语言经验的人都会认识到,这两个crate。它提供了便利,如果 Left 和 Right 都是迭代器类型,则 Either 也将表现为迭代器类型。具体应用:

et iter = if is_reversed() {
	Left(leds.iter_mut().rev())
} else {
	Right(leds.iter_mut())
};
iter.for_each(|led| {
    led.toggle();
    arduino_hal::delay_ms(ms as u16);
})

要使任一crate正常工作,重要的是禁用 std 编译,因为这是一个no_std环境。这是通过将以下内容添加到 Cargo.toml 来实现的

[dependencies.either]
version = "1.6.1"
default-features = false

中断处理程序本身可以相当简单。约定是处理程序与其应处理的中断类型具有相同的名称,在本例中为 INT0:

#[avr_device::interrupt(atmega328p)]
fn INT0() {
    let current = REVERSED.load(Ordering::SeqCst);
    REVERSED.store(!current, Ordering::SeqCst);
}

由于中断服务例程 (ISR) 和其余代码之间存在同步问题,因此使用AtomicBool,如 Rahix 的博客文章中详细介绍的。当 ISR 执行时,不会有任何其他中断,因此不需要临界区。读取值时,需要一个关键部分,但这似乎与地址空间有关,因为 AtomicBool 应该已经提供了正确的同步:

fn is_reversed() -> bool {
    return avr_device::interrupt::free(|_| {
    	REVERSED.load(Ordering::SeqCst) 
    });
}

最后要做的是将 D2 引脚切换到 EXT0 并全局打开中断:

// thanks to tsemczyszyn and Rahix: https://github.com/Rahix/avr-hal/issues/240
// Configure INT0 for falling edge. 0x03 would be rising edge.
dp.EXINT.eicra.modify(|_, w| w.isc0().bits(0x02));
// Enable the INT0 interrupt source.
dp.EXINT.eimsk.modify(|_, w| w.int0().set_bit());

unsafe { avr_device::interrupt::enable() };

让代码运行并按下按钮会显示正在运行的中断。

编译并运行示例

cargo build
cargo run

完整代码如下:

src/main.rs

/*!
 * Blinks a 4 leds in sequence on pins D3 - D6. When an external interrupt on D2/INT0 comes in
 * the sequence is reversed.
 * 
 * Note: The use of the either crate requires the deactivation of std to use it in core. See the Cargo.toml 
 * in this directory for details.
 */
#![no_std]
#![no_main]
#![feature(abi_avr_interrupt)]

use panic_halt as _;
use core::sync::atomic::{AtomicBool, Ordering};
use arduino_hal::port::{mode, Pin};
use either::*;

static REVERSED: AtomicBool = AtomicBool::new(false);

fn is_reversed() -> bool {
    REVERSED.load(Ordering::SeqCst)
}

#[avr_device::interrupt(atmega328p)]
fn INT0() {
    let current = REVERSED.load(Ordering::SeqCst);
    REVERSED.store(!current, Ordering::SeqCst);
}

fn blink_for_range(range: impl Iterator<Item = u16>, leds: &mut [Pin<mode::Output>]) {
    range.map(|i| i * 100).for_each(|ms| {
        let iter = if is_reversed() {
            Left(leds.iter_mut().rev())
        } else {
            Right(leds.iter_mut())
        };
        iter.for_each(|led| {
            led.toggle();
            arduino_hal::delay_ms(ms as u16);
        })
    });
}

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    // thanks to tsemczyszyn and Rahix: https://github.com/Rahix/avr-hal/issues/240
    // Configure INT0 for falling edge. 0x03 would be rising edge.
    dp.EXINT.eicra.modify(|_, w| w.isc0().bits(0x02));
    // Enable the INT0 interrupt source.
    dp.EXINT.eimsk.modify(|_, w| w.int0().set_bit());

    let mut leds: [Pin<mode::Output>; 4] = [
        pins.d3.into_output().downgrade(),
        pins.d4.into_output().downgrade(),
        pins.d5.into_output().downgrade(),
        pins.d6.into_output().downgrade(),
    ];

    unsafe { avr_device::interrupt::enable() };

    loop {
        blink_for_range(0..10, &mut leds);
        blink_for_range((0..10).rev(), &mut leds);
    }
}

控制舵机

控制舵机旋转

舵机是一种电机,它使用一个反馈系统来控制电机的位置。可以很好掌握电机角度。大多数舵机是可以最大旋转180°的。也有一些能转更大角度,甚至360°。舵机比较多的用于对角度有要求的场合,比如摄像头,智能小车前置探测器,需要在某个范围内进行监测的移动平台。又或者把舵机放到玩具,让玩具动起来。还可以用多个舵机,做个小型机器人,舵机就可以作为机器人的关节部分。所以,舵机的用处很多。

由于avr-hal没有像C那样提供9G舵机的类库,所以,在本示例中,我们需要手动编写一个控制它。本例使用的9G舵机可以在旋转180度。根据舵机数据表,该舵机使用PWM控制,位置“0”(1.5 ms 脉冲)位于中间,“90”(~2ms 脉冲)位于一直向右的位置,“-90”(~1ms 脉冲)一直向左,信号频率为50Hz。

而因为,UNO缺乏数模转换(DAC)输出模块,所以,我们需要自定义的PWM,通过使用定时器Timer1调用中断来自定义波形生成模式WGM来生成控制舵机的波形进行实现。

硬件要求

  • Arduino板卡
  • Micro Servo 9G舵机
  • 连接线

电路

按照舵机的数据表,舵机有棕红橙三根不同颜色的线,分别对应GND、+5V和PWM控制信号,控制信号我们选引脚9。

按照舵机数据表的说明,我们需要在引脚9上输出一个50Hz的变动占空比,即PWM信号来控制不同时间点舵机的旋转角度。

关于Arduino Uno的PWM的详细教程,可以参见Secrets of Arduino PWM

关于计时器说明可以参考ATMega328P数据表

定时器是一种中断。 它就像一个简单的时钟,可以测量事件的时间间隔。 每个微控制器都有一个时钟(振荡器),比如在 Arduino Uno 中它是 16Mhz。 这对速度负责。 时钟频率越高,处理速度越高。 定时器使用计数器,该计数器根据时钟频率以一定的速度进行计数。 Arduino UNO时钟以16MHz运行。计数器的一个刻度值表示1 / 16,000,000秒(~63ns),跑完1s需要计数值16,000,000。Arduino UNO有3种功能不同的定时器:

  1. Timer0:8位定时器。用来执行delay(), millis()这些函数。
  2. Timer1:16位定时器。用来运行本例的舵机控制。
  3. Timer2:8位定时器。比如用来运行C类库中的tone()函数。

定时器寄存器

定时器寄存器用来修改寄存器的配置。

  1. 定时器/计数器控制寄存器(TCCRnA/B):

该寄存器保存定时器的主要控制位,用于控制定时器的预分频器。它还允许使用 WGM 位控制定时器的模式。

帧格式:

TCCR1A76543210
COM1A1COM1A0COM1B1COM1B0COM1C1COM1C0WGM11WGM10
TCCR1B76543210
--------------
ICNC1ICES1-WGM13WGM12CS12CS11CS10

预分频器:

TCCR1B 中的 CS12、CS11、CS10 位设置预分频器值。预分频器用于设置定时器的时钟速度。 Arduino Uno 的预分频器为 1、8、64、256、1024。

CS12CS11CS10描述
000没有时钟源(定时器/计数器停止)
001未分频
0108预分频
01164预分频
100256预分频
1011024预分频
110T1 引脚上的外部时钟源。时钟在下降沿。
111T1 引脚上的外部时钟源。时钟在上升沿。
  1. 定时器/计数器寄存器(TCNTn):

该寄存器用于控制计数器值并设置预加载器值。

所需时间(以秒为单位)的预加载器值的公式: \[ TCNTn = 65535 –(16 \times 10^{10} \times 秒数/预分频器值)\] 要计算 2 秒时间内定时器 1 的预加载器值: \[TCNT1 = 65535 – (16 \times 10^{10} \times 2/1024) = 34285 \]

定时器中断

输出比较寄存器(OCRnA/B):

当输出比较匹配中断发生时,中断服务ISR (TIMERx_COMPy_vect)被调用,并且TIFRx寄存器中的OCFxy标志位将被设置。 该ISR通过设置TIMSKx寄存器中 OCIExy中的启用位来启用。 其中TIMSKx是定时器中断屏蔽寄存器。

当定时器达到比较寄存器值时,相应的输出被切换。

定时器中断捕获:

接下来,当定时器输入捕捉中断发生时,将调用中断服务 ISR (TIMERx_CAPT_vect),并且 TIFRx(定时器中断标志寄存器)中的 ICFx 标志位将被置位。 通过设置 TIMSKx 寄存器中 ICIEx 中的使能位来启用此 ISR。

定时器可以在溢出和/或与任一输出比较寄存器匹配时生成中断。

电路图

控制舵机

代码

这里我们需要利用定时器来触发定时中断以向舵机发送转角数据。

按照舵机数据表,我们首先需要得到一个50Hz的中断脉冲,通过改变每个中断期间的占空比,方波的宽度来控制舵机的角度。我们可以在每次方波发出后等待1/50s,即20ms来接受下一次的脉冲信号。我们可以通过定时器中断寄存器来配置64预分频,波形生成模式为0b11,表示此时使用Fast PWM模式,生成的是锯齿波,每个duty cycle可以有0-255种值,即占用255个tick,因此,波形频率为16M/64/256=976.56Hz。分频后的时钟频率为250kHz,每个tick为4µs。那么,按照舵机的数据表,我们需要设置输出的tick值在100-600之间,即0.4-2.4ms之间。0-180度的对应的具体的测试值,最终为120-600。如果我们选择的分频数高于这个值,比如256分频,那么每个tick就会上升到16µs,输出的tick值就在50-150之间了。而我们需要180个角度值,缺乏足够的分辨率。

tc1.tccr1b
    .write(|w| w.wgm1().bits(0b11).cs1().prescale_64());

而因为simple pwm的set_duty只能接受u8类型的值,即0-255,而我们需要输入的值为u16类型,按照手册,我们只能选用可以输出16位值的寄存器,也就是Timer1(Timer0是8位寄存器,Timer2也是8位寄存器但是带有异步模式)。而因为Timer1的输出比较匹配A输出到数字引脚9上,而Timer1对应的另一个引脚是引脚10是外部源的输入捕获输入引脚。因此,我们只能将舵机连接到引脚9上。

pins.d9.into_output();

上述代码中,wgm1().bits(0b11)代表,设置了tccr1b中的WGM的模式配置位3,2,为1,WGM在AVR中有15种工作模式,因此用4个位表示,另外两个位在tccr1a中,为模式配置位1和0,在下面这段代码中,我们设置这两位的值为1和0,那么,WGM工作模式就设置为了1110,即14。14表示Fast PWM,输出的是锯齿波。而wgm和com1a的配置,共同决定了波形生成器的工作模式,所以我们还需要设置com1a的值。match_clear()设置了com1a的值为2,这个时候在匹配前波形为低电平,匹配后,波形为高电平,正好符合我们驱动舵机的需要。

tc1.tccr1a
    .write(|w| w.wgm1().bits(0b10).com1a().match_clear());

我们需要把触发中断输出的频率变为50Hz,即需要976.56/50=19.53,差不多每20个波触发一次比较输出中断。因此,根据"减一原则"我们需要设置输入捕获寄存器(Input Capture Register)的值为20*256-1=5119。这个值设置了在波形生成器在14的工作模式下设置了定时器计数器的TOP值,这是OC1A的一个特性,这个值决定了波形生成的周期,这样我们就得到了一个50Hz的Fast PWM锯齿波。

tc1.icr1.write(|w| w.bits(5119));

按照上面的说明,我们需要设置一个宽度为在1ms-2ms之间并且被等分为180份的duty序列,来控制舵机的运动角度,序列的每个值对应1度。而输出序列值,我们要通过设置定时器输出比较寄存器OCR1A来写入当前输出的duty值。

for degree in (0..=180).chain((0..179).rev()) {
    let duty = degree as f32 * ((600.0 - 120.0) / 180.0) + 120.0;
    ufmt::uwriteln!(
        &mut serial,
        "degree:{},duty:{}",
        degree,
        ufmt_float::uFmt_f32::Two(duty.clone())
    )
    .unwrap_infallible();
    tc1.ocr1a.write(|w| w.bits(duty as u16));
    delay_ms(20);
}

编译并运行示例

cargo build
cargo run

此时,我们可以看到,舵机开始移动到0度,之后每次移动∼1度 完整代码如下:

src/main.rs

/*!
 * Servo Control
 * 
 * Sweep a standard SG90 compatible servo from its left limit all the way to its right limit and back.
 *
 * Because avr-hal does not have a dedicated servo driver library yet, we do this manually using
 * timer TC1.  The servo should be connected to D9 (AND D9 ONLY!  THIS DOES NOT WORK ON OTHER PINS
 * AS IT IS).
 *
 * As the limits are not precisely defined, we undershoot the datasheets 1ms left limit and
 * overshoot the 2ms right limit by a bit - you can figure out where exactly the limits are for
 * your model by experimentation.
 *
 */
#![no_std]
#![no_main]

use arduino_hal::{
    default_serial, delay_ms, pins, prelude::_unwrap_infallible_UnwrapInfallible, Peripherals,
};
use avr_device::entry;
use panic_halt as _;

#[entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);
    let mut serial = default_serial!(dp, pins, 57600);

    // Important because this sets the bit in the DDR register!
    pins.d9.into_output();

    // - TC1 runs off a 250kHz clock, with 5000 counts per overflow => 50 Hz signal.
    // - Each count increases the duty-cycle by 4us.
    // - Use OC1A which is connected to D9 of the Arduino Uno.
    let tc1 = dp.TC1;
    tc1.icr1.write(|w| w.bits(5119));
    tc1.tccr1a
        .write(|w| w.wgm1().bits(0b10).com1a().match_clear());
    tc1.tccr1b
        .write(|w| w.wgm1().bits(0b11).cs1().prescale_64());

    loop {
        for degree in (0..=180).chain((0..179).rev()) {
            let duty = degree as f32 * ((600.0 - 120.0) / 180.0) + 120.0;
            ufmt::uwriteln!(
                &mut serial,
                "degree:{},duty:{}",
                degree,
                ufmt_float::uFmt_f32::Two(duty.clone())
            )
            .unwrap_infallible();
            tc1.ocr1a.write(|w| w.bits(duty as u16));
            delay_ms(20);
        }
    }
}

使用C/C++类库控制舵机

如何调用C/C++类库

控制舵机中,我们展示了如何使用定时器控制波形生成器生成控制舵机的波形。

由于C/C++生态在嵌入式开发场景下,已经比较成熟了,有大量的类库可以使用,那么,本示例中,我们将讲解如何在Rust中调用C/C++类库的类型和方法。

我们需要编写一个build.rs脚本,放在项目根目录下,扩展cargo的编译过程。

硬件要求

  • Arduino板卡
  • Micro Servo 9G舵机
  • 连接线

电路

按照舵机的数据表,舵机有棕红橙三根不同颜色的线,分别对应GND、+5V和PWM控制信号,控制信号我们选引脚9。

电路图

使用C/C++类库控制舵机

代码

调用C/C++类库,我们需要经过以下几个步骤:

  1. 加载类库的头文件'*.h'
  2. 指定需要加载的类库文件所在的路径
  3. 编译类库文件为.a并存放到指定输出目录下
  4. 生成需要调用的函数的绑定文件
  5. 在编译时链接库文件并生成可执行文件 为了实现以上目标,我们需要使用两个类库:
  • crate::cc用于编译
  • crate::bindgen用于生成绑定文件 首先,我们在项目根目录下编写一个配置文件arduino.yaml,以配置编译过程和绑定代码的生成过程

指定Arduino的安装目录,不同的系统安装目录各有不同,请参考对应平台的安装文档或者IDE中的类库加载路径配置。

Arduino主目录和外部库的安装目录

# Home path for aruduino and external libraries
arduino_home: $HOME/Library/Arduino15
external_libraries_home: $HOME/Documents/Arduino/libraries

SDK版本信息、板卡类型信息和avr-gcc版本信息

# version and board info
core_version: 1.8.6
variant: eightanaloginputs
avr_gcc_version: 7.3.0-atmel3.6.1-arduino7

需要加载的Arduino类库和外部类库

Home path for aruduino and external libraries

# libraries to load
arduino_libraries:
  - Wire
external_libraries:
  - Servo

为了解析配置文件,我们需要增加一个包在编译时进行处理,修改Cargo.toml

[build-dependencies]
...
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
...

按照配置的格式,我们增加一个Config类型来承载配置文件解析的结果

struct Config {
    pub arduino_home: String,
    pub external_libraries_home: String,
    pub core_version: String,
    pub variant: String,
    pub avr_gcc_version: String,
    pub arduino_libraries: Vec<String>,
    pub external_libraries: Vec<String>,
	...
}

为配置文件实现一系列在编译和绑定时需要使用的路径方法

impl Config {
    fn arduino_package_path(&self) -> PathBuf {
        let expanded = envmnt::expand(&self.arduino_home, None);
        let arduino_home_path = PathBuf::from(expanded);
        arduino_home_path.join("packages").join("arduino")
    }

    fn core_path(&self) -> PathBuf {
        self.arduino_package_path()
            .join("hardware")
            .join("avr")
            .join(&self.core_version)
    }

    fn avr_gcc_home(&self) -> PathBuf {
        self.arduino_package_path()
            .join("tools")
            .join("avr-gcc")
            .join(&self.avr_gcc_version)
    }

    fn avg_gcc(&self) -> PathBuf {
        self.avr_gcc_home().join("bin").join("avr-gcc")
    }

    fn arduino_core_path(&self) -> PathBuf {
        self.core_path().join("cores").join("arduino")
    }

    fn arduino_include_dirs(&self) -> Vec<PathBuf> {
        let variant_path = self.core_path().join("variants").join(&self.variant);
        let avr_gcc_include_path = self.avr_gcc_home().join("avr").join("include");
        vec![self.arduino_core_path(), variant_path, avr_gcc_include_path]
    }

    fn arduino_libraries_path(&self) -> Vec<PathBuf> {
        let library_root = self.core_path().join("libraries");
        let mut result = vec![];

        for library in &self.arduino_libraries {
            result.push(library_root.join(library).join("src"));
        }
        result
    }

    fn external_libraries_path(&self) -> Vec<PathBuf> {
        let expanded = envmnt::expand(&self.external_libraries_home, None);
        let external_library_root = PathBuf::from(expanded);
        let mut result = vec![];

        for library in &self.external_libraries {
            result.push(external_library_root.join(library).join("src"));
        }
        result
    }

    fn include_dirs(&self) -> Vec<PathBuf> {
        let mut result = self.arduino_include_dirs();
        result.extend(self.arduino_libraries_path());
        result.extend(self.external_libraries_path());
        result
    }
	...
}

计算需要监控的项目文件,以在变更后重新编译

impl Config {
	...
	fn project_files(&self, patten: &str) -> Vec<PathBuf> {
        let mut result =
            files_in_folder(self.arduino_core_path().to_string_lossy().as_ref(), patten);
        let mut libraries = self.arduino_libraries_path();
        libraries.extend(self.external_libraries_path());

        let pattern = format!("**/{}", patten);
        for library in libraries {
            let lib_sources = files_in_folder(library.to_string_lossy().as_ref(), &pattern);
            result.extend(lib_sources);
        }

        result
    }

	fn cpp_files(&self) -> Vec<PathBuf> {
        self.project_files("*.cpp")
    }

    fn c_files(&self) -> Vec<PathBuf> {
        self.project_files("*.c")
    }

    ...
}

遍历目录中的文件

fn files_in_folder(folder: &str, pattern: &str) -> Vec<PathBuf> {
    let cpp_pattern = format!("{}/{}", folder, pattern);
    let mut results = vec![];
    for cpp_file in glob(&cpp_pattern).unwrap() {
        let file = cpp_file.unwrap();
        if !file.ends_with("main.cpp") {
            results.push(file);
        }
    }
    results
}

为此,我们需要新增一个crate blob

[build-dependencies]
...
glob = "0.3"
...

让Cargo监控当前文件变化,在发生改变时重新执行

// Rebuild if config file changed
println!("cargo:rerun-if-changed={}", CONFIG_FILE); 

读取并解析配置文件

let config_string = std::fs::read_to_string(CONFIG_FILE)
    .unwrap_or_else(|e| panic!("Unable to read {} file: {}", CONFIG_FILE, e));
let config: Config = serde_yaml::from_str(&config_string)
    .unwrap_or_else(|e| panic!("Unable to parse {} file: {}", CONFIG_FILE, e));

接下来,我们加入编译依赖cc,以编译C/C++类库文件到target文件夹,为链接生成二进制文件做准备。

我们在进行编译时,需要向编译器传递下列参数并指定编译标志,我们也加入配置文件arduino.yaml中,其中-mmcu指定了微控制器的型号,这样avr-gcc才能为合适的target输出编译后的机器码。

...
# Compile parameters and flags
definitions:
  ARDUINO: "10807"
  F_CPU: 16000000L
  ARDUINO_AVR_UNO: "1"
  ARDUINO_ARCH_AVR: "1"
flags:
  - "-mmcu=atmega328p"

先要配置编译器

fn configure_arduino(config: &Config) -> Build {
    let mut builder = Build::new();
    for (k, v) in &config.definitions {
        builder.define(k, v.as_str());
    }
    for flag in &config.flags {
        builder.flag(flag);
    }
    builder
        .compiler(config.avg_gcc())
        .flag("-Os")
        .cpp_set_stdlib(None)
        .flag("-fno-exceptions")
        .flag("-ffunction-sections")
        .flag("-fdata-sections");

    for include_dir in config.include_dirs() {
        builder.include(include_dir);
    }
    builder

添加类库源文件到Cargo的监视目标,在更新后会使类库重新编译

pub fn add_source_file(builder: &mut Build, files: Vec<PathBuf>) {
    for file in files {
        println!("cargo:rerun-if-changed={}", file.to_string_lossy());
        builder.file(file);
    }
}

编译C/C++类库,按照命名规范,类库名称用lib开头

fn compile_arduino(config: &Config) {
    let mut builder = configure_arduino(&config);
    builder
        .cpp(true)
        .flag("-std=gnu++11")
        .flag("-fpermissive")
        .flag("-fno-threadsafe-statics");
    add_source_file(&mut builder, config.cpp_files());
    builder.compile("libarduino_c++.a");

    let mut builder = configure_arduino(&config);
    builder.flag("-std=gnu11");
    add_source_file(&mut builder, config.c_files());
    builder.compile("libarduino_c.a");

    println!("cargo:rustc-link-lib=static=arduino_c++");
    println!("cargo:rustc-link-lib=static=arduino_c");
}

在main函数里加载编译过程

compile_arduino(&config);

我们还需要为类库里的类型和方法生成绑定,才能够在Rust代码里直接调用。绑定可以手动编写,也可以通过工具bindgen生成。这里需要注意的是,bindgen只能很好的根据C类库的.h头文件生成绑定,而对于C++代码,会存在一定的无法解析的情况,对于无法解析的代码,他会自动略过,而且无法有效解析C++的声明和实现混合的.hpp头文件。 我们在项目根目录下编写一个wrapper.h文件,这样可以有效的控制我们需要编译和绑定哪些头文件,对于头文件中包含的代码指向的其它源文件和头文件,将会递归检索。

#include <Arduino.h>
#include <Servo.h>

配置bindgen,传入板卡参数和微控制器的信号,指定文件包含路径,以及指定需要生成和屏蔽的函数和方法的绑定

fn configure_bindgen_for_arduino(config: &Config) -> Builder {
    let mut builder = Builder::default();
    for (k, v) in &config.definitions {
        builder = builder.clang_arg(&format!("-D{}={}", k, v));
    }
    for flag in &config.flags {
        builder = builder.clang_arg(flag);
    }
    builder = builder
        .clang_args(&["-x", "c++", "-std=gnu++11"])
        .use_core()
        .header("wrapper.h")
        .layout_tests(false)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));

    for include_dir in config.include_dirs() {
        builder = builder.clang_arg(&format!("-I{}", include_dir.to_string_lossy()));
    }

    for item in &config.bindgen_lists.allowlist_function {
        builder = builder.allowlist_function(item);
    }

    for item in &config.bindgen_lists.allowlist_type {
        builder = builder.allowlist_type(item);
    }

    for item in &config.bindgen_lists.blocklist_function {
        builder = builder.blocklist_function(item);
    }

    for item in &config.bindgen_lists.blocklist_type {
        builder = builder.blocklist_type(item);
    }

    builder
}

因为这里我们只需要使用Servo类型及其方法,所以,我们可以在arduino.yaml配置文件中增加如下配置

...
# binding filter
bindgen_lists:
  allowlist_function:
    # - Arduino.*
    - Servo.*
  allowlist_type:
    - Servo.*
  blocklist_function:
    - Print.*
    - String.*
  blocklist_type:
    - Print.*
    - String.*

生成绑定并输出到项目target的OUT_DIR

fn generate_bindings(config: &Config) {
    let bindings: Bindings = configure_bindgen_for_arduino(&config)
        .generate()
        .expect("Unable to generate bindings");
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

在main函数里加载编译过程

compile_arduino(&config);

为项目增加编译依赖项

[build-dependencies]
...
cc = "1.0.96"
bindgen = "0.69.4"

以上,我们就编写好了build.rs来扩展cargo的编译过程了。

完整代码如下:

arduino.yaml

# Home path for aruduino and external libraries
arduino_home: $HOME/Library/Arduino15
external_libraries_home: $HOME/Documents/Arduino/libraries
# Version and board info
core_version: 1.8.6
variant: eightanaloginputs
avr_gcc_version: 7.3.0-atmel3.6.1-arduino7
# Libraries to load
arduino_libraries:
  - Wire
external_libraries:
  - Servo
# Compile parameters and flags
definitions:
  ARDUINO: "10807"
  F_CPU: 16000000L
  ARDUINO_AVR_UNO: "1"
  ARDUINO_ARCH_AVR: "1"
flags:
  - "-mmcu=atmega328p"
# binding filter
bindgen_lists:
  allowlist_function:
    # - Arduino.*
    - Servo.*
  allowlist_type:
    - Servo.*
  blocklist_function:
    - Print.*
    - String.*
  blocklist_type:
    - Print.*
    - String.*

build.rs

use std::{collections::HashMap, env, path::PathBuf};

use bindgen::{Bindings, Builder};
use cc::Build;
use glob::glob;
use serde::Deserialize;

const CONFIG_FILE: &str = "arduino.yaml";

fn main() {
    // Rebuild if config file changed
    println!("cargo:rerun-if-changed={}", CONFIG_FILE);

    let config_string = std::fs::read_to_string(CONFIG_FILE)
        .unwrap_or_else(|e| panic!("Unable to read {} file: {}", CONFIG_FILE, e));
    let config: Config = serde_yaml::from_str(&config_string)
        .unwrap_or_else(|e| panic!("Unable to parse {} file: {}", CONFIG_FILE, e));

    println!("Arduino configuration: {:#?}", config);
    println!(
        "arduino_library_path:{:#?}\nexternal_library_path:{:#?}",
        config.arduino_libraries_path(),
        config.external_libraries_path()
    );

    compile_arduino(&config);
    generate_bindings(&config);
}

#[derive(Debug, Deserialize)]
struct BindgenLists {
    pub allowlist_function: Vec<String>,
    pub allowlist_type: Vec<String>,
    pub blocklist_function: Vec<String>,
    pub blocklist_type: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct Config {
    pub arduino_home: String,
    pub external_libraries_home: String,
    pub core_version: String,
    pub variant: String,
    pub avr_gcc_version: String,
    pub arduino_libraries: Vec<String>,
    pub external_libraries: Vec<String>,
    pub definitions: HashMap<String, String>,
    pub flags: Vec<String>,
    pub bindgen_lists: BindgenLists,
}

impl Config {
    fn arduino_package_path(&self) -> PathBuf {
        let expanded = envmnt::expand(&self.arduino_home, None);
        let arduino_home_path = PathBuf::from(expanded);
        arduino_home_path.join("packages").join("arduino")
    }

    fn core_path(&self) -> PathBuf {
        self.arduino_package_path()
            .join("hardware")
            .join("avr")
            .join(&self.core_version)
    }

    fn avr_gcc_home(&self) -> PathBuf {
        self.arduino_package_path()
            .join("tools")
            .join("avr-gcc")
            .join(&self.avr_gcc_version)
    }

    fn avg_gcc(&self) -> PathBuf {
        self.avr_gcc_home().join("bin").join("avr-gcc")
    }

    fn arduino_core_path(&self) -> PathBuf {
        self.core_path().join("cores").join("arduino")
    }

    fn arduino_include_dirs(&self) -> Vec<PathBuf> {
        let variant_path = self.core_path().join("variants").join(&self.variant);
        let avr_gcc_include_path = self.avr_gcc_home().join("avr").join("include");
        vec![self.arduino_core_path(), variant_path, avr_gcc_include_path]
    }

    fn arduino_libraries_path(&self) -> Vec<PathBuf> {
        let library_root = self.core_path().join("libraries");
        let mut result = vec![];

        for library in &self.arduino_libraries {
            result.push(library_root.join(library).join("src"));
        }
        result
    }

    fn external_libraries_path(&self) -> Vec<PathBuf> {
        let expanded = envmnt::expand(&self.external_libraries_home, None);
        let external_library_root = PathBuf::from(expanded);
        let mut result = vec![];

        for library in &self.external_libraries {
            result.push(external_library_root.join(library).join("src"));
        }
        result
    }

    fn include_dirs(&self) -> Vec<PathBuf> {
        let mut result = self.arduino_include_dirs();
        result.extend(self.arduino_libraries_path());
        result.extend(self.external_libraries_path());
        result
    }

    fn project_files(&self, patten: &str) -> Vec<PathBuf> {
        let mut result =
            files_in_folder(self.arduino_core_path().to_string_lossy().as_ref(), patten);
        let mut libraries = self.arduino_libraries_path();
        libraries.extend(self.external_libraries_path());

        let pattern = format!("**/{}", patten);
        for library in libraries {
            let lib_sources = files_in_folder(library.to_string_lossy().as_ref(), &pattern);
            result.extend(lib_sources);
        }

        result
    }

    fn cpp_files(&self) -> Vec<PathBuf> {
        self.project_files("*.cpp")
    }

    fn c_files(&self) -> Vec<PathBuf> {
        self.project_files("*.c")
    }
}

fn files_in_folder(folder: &str, pattern: &str) -> Vec<PathBuf> {
    let cpp_pattern = format!("{}/{}", folder, pattern);
    let mut results = vec![];
    for cpp_file in glob(&cpp_pattern).unwrap() {
        let file = cpp_file.unwrap();
        if !file.ends_with("main.cpp") {
            results.push(file);
        }
    }
    results
}

fn configure_arduino(config: &Config) -> Build {
    let mut builder = Build::new();
    for (k, v) in &config.definitions {
        builder.define(k, v.as_str());
    }
    for flag in &config.flags {
        builder.flag(flag);
    }
    builder
        .compiler(config.avg_gcc())
        .flag("-Os")
        .cpp_set_stdlib(None)
        .flag("-fno-exceptions")
        .flag("-ffunction-sections")
        .flag("-fdata-sections");

    for include_dir in config.include_dirs() {
        builder.include(include_dir);
    }
    builder
}

pub fn add_source_file(builder: &mut Build, files: Vec<PathBuf>) {
    for file in files {
        println!("cargo:rerun-if-changed={}", file.to_string_lossy());
        builder.file(file);
    }
}

fn compile_arduino(config: &Config) {
    let mut builder = configure_arduino(&config);
    builder
        .cpp(true)
        .flag("-std=gnu++11")
        .flag("-fpermissive")
        .flag("-fno-threadsafe-statics");
    add_source_file(&mut builder, config.cpp_files());
    builder.compile("libarduino_c++.a");

    let mut builder = configure_arduino(&config);
    builder.flag("-std=gnu11");
    add_source_file(&mut builder, config.c_files());
    builder.compile("libarduino_c.a");

    println!("cargo:rustc-link-lib=static=arduino_c++");
    println!("cargo:rustc-link-lib=static=arduino_c");
}

fn configure_bindgen_for_arduino(config: &Config) -> Builder {
    let mut builder = Builder::default();
    for (k, v) in &config.definitions {
        builder = builder.clang_arg(&format!("-D{}={}", k, v));
    }
    for flag in &config.flags {
        builder = builder.clang_arg(flag);
    }
    builder = builder
        .clang_args(&["-x", "c++", "-std=gnu++11"])
        .use_core()
        .header("wrapper.h")
        .layout_tests(false)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));

    for include_dir in config.include_dirs() {
        builder = builder.clang_arg(&format!("-I{}", include_dir.to_string_lossy()));
    }

    for item in &config.bindgen_lists.allowlist_function {
        builder = builder.allowlist_function(item);
    }

    for item in &config.bindgen_lists.allowlist_type {
        builder = builder.allowlist_type(item);
    }

    for item in &config.bindgen_lists.blocklist_function {
        builder = builder.blocklist_function(item);
    }

    for item in &config.bindgen_lists.blocklist_type {
        builder = builder.blocklist_type(item);
    }

    builder
}

fn generate_bindings(config: &Config) {
    let bindings: Bindings = configure_bindgen_for_arduino(&config)
        .generate()
        .expect("Unable to generate bindings");
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

在使用时,我们需要修改libs.rs,因为一个rust项目只允许有一个libs文件,所以我们需要通过include宏将绑定文件的内容合并到libs中。由于C/C++类库和Rust类库的命名规范不一致,所以,我们这里要使用属性宏允许绑定文件中出现不符合命名规范的函数和类型。

#![allow(non_snake_case)]

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

之后,我们需要在Rust源代码里面初始化Arduino类库的初始化过程,这需要用到Arduino.h中包含的init()方法。因为我们已经编译了Arduino类库,所以,我们只需要在引入这个方法。

extern "C" {
    fn init();
}

到这里,我们便可以在Rust里直接使用Arduino的类库类型和函数了。

完整代码如下: src/main.rs

/*!
 * Servo Control
 *
 * Sweep a standard SG90 compatible servo from its left limit all the way to its right limit and back.
 */
#![no_std]
#![no_main]

use arduino_hal::delay_ms;
use arduino_uno_example::Servo;
use avr_device::entry;
use panic_halt as _;

extern "C" {
    fn init();
}

#[entry]
fn main() -> ! {
    unsafe {
        init();
    }

    unsafe {
        let mut myservo = Servo::new();
        myservo.attach(9);

        loop {
            for degree in (0..=180).chain((0..179).rev()) {
                myservo.write(degree);
                delay_ms(15)
            }
        }
    }
}

编译并运行示例

cargo build
cargo run

此时,我们可以看到,舵机开始移动到0度,之后每次移动∼1度

参考内容

数字

无延迟闪烁

不使用delay_ms() 函数使LED 闪烁。

有时您需要同时做两件事。例如,您可能希望在读取按钮按下情况时使 LED 闪烁。在这种情况下,您不能使用delay_ms(),因为Arduino会在delay_ms()期间暂停您的程序。如果在 Arduino 暂停等待delay_ms()通过时按下按钮,您的程序将错过按钮按下。

该草图演示了如何在不使用delay_ms()的情况下使LED闪烁。它会打开 LED,然后记下时间。然后,每次通过loop(),它都会检查是否已经过了所需的闪烁时间。如果有,它会打开或关闭 LED 并记下新时间。通过这种方式,LED 会连续闪烁,而草图执行不会滞后于单个指令。

打个比方,就像用微波炉加热披萨,同时等待一些重要的电子邮件。您将披萨放入微波炉中,加热 10 分钟。使用delay_ms() 的类比是坐在微波炉前看着计时器从10 分钟倒计时直到计时器达到零。如果重要的电子邮件在此期间到达,您将会错过它。

在现实生活中,你会做的就是打开披萨,然后检查你的电子邮件,然后可能会做其他事情(这不会花太长时间!),每隔一段时间你就会回到微波炉看看如果计时器已为零,则表明您的披萨已完成。 在本教程中,您将学习如何设置类似的计时器。

硬件要求

  • Arduino板卡
  • 220欧电阻
  • LED

电路

要构建电路,请将电阻器的一端连接到电路板的引脚 13。将 LED 的长腿(正极腿,称为阳极)连接到电阻器的另一端。将 LED 的短脚(负极脚,称为阴极)连接到电路板 GND,

大多数 Arduino 板已在板本身的引脚 13 上连接了一个 LED。如果您在没有连接硬件的情况下运行此示例,您应该会看到 LED 闪烁。

代码

我们可以通过检查当前时间点和上次时间点的差值,来开启或者关闭LED,以实现闪烁效果。

编译并运行示例

cargo build
cargo run

完整代码如下:

src/main.rs

/*!
 * Blink an LED without using the delay_ms() function.
 */
#![no_std]
#![no_main]

use panic_halt as _;
use arduino_uno_example::utils::{millis_init,millis};

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    let mut led = pins.d13.into_output();
    const INTERVAL: u32 = 1000;
    let mut previous_time = 0;

    millis_init(dp.TC0);

    // Enable interrupts globally
    unsafe { avr_device::interrupt::enable() };

    loop {
        let current_time = millis();

        if current_time - previous_time >= INTERVAL {
            previous_time = current_time;
            led.toggle();
        }
    }
}

如何对进行按钮连线和编程

了解如何对按钮进行接线和编程来控制 LED。

当您按下按钮或开关时,它们会连接电路中的两点。当您按下按钮时,本示例将打开引脚 13 上的内置 LED。

硬件要求

  • Arduino板卡
  • 瞬时按钮或开关
  • 10k欧姆电阻
  • 连接线
  • 面包板

电路

将三根电线连接到板上。前两个(红色和黑色)连接到面包板侧面的两个长垂直行,以提供对 5 伏电源和接地的访问。第三根电线从数字引脚 2 连接到按钮的一条腿。按钮的同一条腿通过下拉电阻(此处为 10K 欧姆)连接到地。按钮的另一条腿连接到 5 伏电源。

当按钮打开(未按下)时,按钮的两条腿之间没有连接,因此该引脚接地(通过下拉电阻),我们读取低电平。当按钮关闭(按下)时,它会在两个引脚之间建立连接,将引脚连接到 5 伏,以便我们读取高电平。

您还可以以相反的方式连接该电路,使用上拉电阻将输入保持为高电平,并在按下按钮时变为低电平。如果是这样,草图的行为将相反,LED 常亮,按下按钮时熄灭。

如果断开数字 I/O 引脚与所有设备的连接,LED 可能会不规律地闪烁。这是因为输入是“浮动”的——也就是说,它将随机返回高电平或低电平。这就是电路中需要上拉或下拉电阻的原因。

电路图

如何对按钮进行连线和编程来控制LED

代码

编译并运行示例

cargo build
cargo run

完整代码如下:

src/main.rs

/*!
 * Button
 *
 * Turns on and off a light emitting diode(LED) connected to digital pin 13,
  when pressing a pushbutton attached to pin 2.
 */
#![no_std]
#![no_main]

use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    let mut led = pins.d13.into_output();
    let d2 = pins.d2.into_floating_input().downgrade();

    loop {
        if d2.is_high() {
            led.set_high();
        } else {
            led.set_low();
        }
    }
}

按钮去抖

读取按钮,过滤噪音

由于机械和物理问题,按钮在按下时通常会产生虚假的打开/关闭转换:这些转换可能会被解读为在很短的时间内多次按下,从而欺骗了程序。此示例演示了如何对输入进行反跳操作,这意味着在短时间内检查两次以确保确实按下了按钮。在没有去抖的情况下,按下按钮一次可能会导致不可预测的结果。该草图使用 millis() 函数来跟踪自按下按钮以来经过的时间。

硬件要求

  • Arduino板卡
  • 瞬时按钮或开关
  • 10k欧姆电阻
  • 连接线
  • 面包板

电路

电路图

按钮去抖

代码

下面的草图基于 Limor Fried 的去抖版本,但逻辑与她的示例相反。在她的示例中,开关在闭合时返回低电平,在打开时返回高电平。此处,开关在按下时返回高电平,在未按下时返回低电平。

编译并运行示例

cargo build
cargo run

完整代码如下:

src/main.rs

/*!
 * Debounce
 *
 * Each time the input pin goes from LOW to HIGH (e.g. because of a push-button
 * press), the output pin is toggled from LOW to HIGH or HIGH to LOW. There's a
 * minimum delay between toggles to debounce the circuit (i.e. to ignore noise).
 */
#![no_std]
#![no_main]
#![feature(abi_avr_interrupt)]

use arduino_hal::prelude::_unwrap_infallible_UnwrapInfallible;
use arduino_uno_example::utils::{millis, millis_init};
use embedded_hal::digital::OutputPin;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);

    let mut led_pin = pins.d13.into_output();
    let button_pin = pins.d2.into_floating_input().downgrade();

    let mut last_button_is_high = false;
    let mut button_is_high: bool = false;
    let mut led_is_high: bool = true;

    let mut last_debounce_time: u32 = 0;

    const DEBOUNCE_DELAY: u32 = 50;

    millis_init(dp.TC0);

    // Enable interrupts globally
    unsafe { avr_device::interrupt::enable() };

    loop {
        let current_pin_is_high = button_pin.is_high();

        if current_pin_is_high != last_button_is_high {
            last_debounce_time = millis();
        }

        if millis() - last_debounce_time > DEBOUNCE_DELAY {
            if current_pin_is_high != button_is_high {
                button_is_high = current_pin_is_high;
            }

            if button_is_high {
                led_is_high = !led_is_high;
            }
        }

        led_pin.set_state(led_is_high.into()).unwrap_infallible();

        last_button_is_high = current_pin_is_high;
    }
}

输入串行上拉

此示例演示了 INPUT_PULLUP 与 pinMode() 的使用。它通过 USB 在 Arduino 和计算机之间建立串行通信来监视开关的状态。

此外,当输入为高电平时,连接到引脚 13 的板载 LED 将点亮;当为低电平时,LED 将关闭。

硬件要求

  • Arduino板卡
  • 瞬时按钮或开关
  • 连接线
  • 面包板

电路

将两根电线连接到 Arduino 板。黑线将地线连接至按钮的一根腿。第二根电线从数字引脚 2 连接到按钮的另一条腿。

当您按下按钮或开关时,它们会连接电路中的两点。当按钮打开(未按下)时,按钮的两条腿之间没有连接。由于引脚 2 上的内部上拉处于活动状态并连接到 5V,因此当按钮打开时我们读取为高电平。当按钮关闭时,Arduino 读数为低电平,因为接地连接已完成。

电路图

输入串行上拉

代码

创建串口连接

let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

接下来,将数字引脚2初始化为输入并启用内部上拉电阻:

let button_pin = pins.d2.into_pull_up_input();

以下行使引脚 13(带有板载 LED)成为输出:

let mut led_pin= pins.d13.into_output();

现在您的设置已完成,请进入代码的主循环。当您的按钮未被按下时,内部上拉电阻连接至 5 伏。这会导致 Arduino 报告高电平。当按下按钮时,Arduino 引脚被拉至地,导致 Arduino 报告低电平。

在程序的主循环中需要做的第一件事是建立一个变量来保存来自开关的信息。由于来自开关的信息要么是高,要么是低,因此您可以使用 bool 数据类型。将此变量命名为is_high,并将其设置为等于数字引脚2上读取的值。您只需一行代码即可完成所有这些:

let is_high = button_pin.is_high();

Arduino 读取输入后,将其以布尔值的形式打印回计算机。您可以使用最后一行代码中的宏ufmt::uwrintln!()来执行此操作:

ufmt::uwriteln!(&mut serial, "{}", is_high).unwrap_infallible();

编译并运行示例

cargo build
cargo run

通过VS Code的串行监视器连接到板卡串口,如果按钮按下,您将看到一串false,如果按钮松开,您将看到true流。 当开关处于高电平时,引脚 13 上的 LED 将亮起,而当开关处于低电平时,引脚 13 上的 LED 将熄灭。

完整代码如下:

src/main.rs

/*!
 * Input Pull-up Serial
 *
 * This example demonstrates the use of into_pull_up_input. It reads a digital
 * input on pin 2 and prints the results to the Serial Monitor.
 * 
 * Unlike into_floating_input(), there is no pull-down resistor necessary. An internal
 * 20K-ohm resistor is pulled to 5V. This configuration causes the input to read
 * HIGH when the switch is open, and LOW when it is closed.
 */
#![no_std]
#![no_main]

use arduino_hal::prelude::*;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

	let button_pin = pins.d2.into_pull_up_input();
	let mut led_pin= pins.d13.into_output();
	
	loop {
		let is_high = button_pin.is_high();
		ufmt::uwriteln!(&mut serial, "{}", is_high).unwrap_infallible();

		if is_high {
			led_pin.set_low();
		} else {
			led_pin.set_high();
		}
	}
}

按钮的状态变化检测(边缘检测)

计算按下按钮的次数。

一旦按钮开始工作,您通常希望根据按钮被按下的次数来执行一些操作。为此,您需要知道按钮何时将状态从关闭更改为打开,并计算这种状态更改发生的次数。这称为状态变化检测或边缘检测。在本教程中,我们学习如何检查状态更改,向串行监视器发送包含相关信息的消息,并计算四个状态更改以打开和关闭 LED。

硬件要求

  • Arduino板卡
  • 瞬时按钮或开关
  • 10k欧姆电阻
  • 连接线
  • 面包板

电路

将三根电线连接到板上。第一个从按钮的一条腿通过下拉电阻(此处为 10k 欧姆)接地。第二个从按钮的相应支路连接到 5 伏电源。第三个连接到数字 I/O 引脚(此处为引脚 2),用于读取按钮的状态。

当按钮打开(未按下)时,按钮的两条腿之间没有连接,因此该引脚接地(通过下拉电阻),我们读取低电平。当按钮关闭(按下)时,它会在两个引脚之间建立连接,将引脚连接到电压,以便我们读取高电平。 (该引脚仍然接地,但电阻器阻止电流流动,因此电阻最小的路径是+5V。)

如果断开数字 I/O 引脚与所有设备的连接,LED 可能会不规律地闪烁。这是因为输入是“浮动”的,即未连接到电压或接地。它或多或少会随机返回高电平或低电平。这就是电路中需要下拉电阻的原因。

电路图

按钮状态变化检测(边缘检测)

代码

下面的草图不断读取按钮的状态。然后,它将按钮的状态与上次通过主循环的状态进行比较。如果当前按钮状态与上一个按钮状态不同并且当前按钮状态为高,则按钮从关闭变为打开。然后,该草图会递增按钮按下计数器。

该草图还检查按钮按下计数器的值,如果它是四的整数倍,它将打开引脚 13 上的 LED。否则,它会将其关闭。

编译并运行示例

cargo build
cargo run

完整代码如下:

src/main.rs

/*!
 * State change detection (edge detection)

 * Often, you don't need to know the state of a digital input all the time, but
 * you just need to know when the input changes from one state to another.
 * For example, you want to know when a button goes from OFF to ON. This is called
 * state change detection, or edge detection.

 * This example shows how to detect when a button or button changes from off to on
 * and on to off.
*/
#![no_std]
#![no_main]

use arduino_hal::prelude::*;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);

    let mut led_pin = pins.d13.into_output();
    let button_pin = pins.d2.into_floating_input();

    let mut button_push_counter: u32 = 0;
    let mut last_button_is_high = false;

    loop {
        let button_is_high = button_pin.is_high();

        if button_is_high != last_button_is_high {
            if button_is_high {
                button_push_counter += 1;
                ufmt::uwriteln!(&mut serial, "on").unwrap_infallible();
                ufmt::uwriteln!(
                    &mut serial,
                    "number of button pushes: {}",
                    button_push_counter
                )
                .unwrap_infallible();
            } else {
                ufmt::uwriteln!(&mut serial, "off").unwrap_infallible();
            }
        }
		
		last_button_is_high = button_is_high;

		if button_push_counter % 4 == 0 {
			led_pin.set_high();
		} else {
			led_pin.set_low();
		}
    }
}

模拟

模拟输入,串行输出

读取模拟输入引脚,映射结果,然后使用该数据来调暗或调亮 LED。

此示例向您展示如何读取模拟输入引脚,将结果映射到 0 到 255 的范围,使用该结果设置输出引脚的脉宽调制 (PWM) 以调暗或调亮 LED,并将值打印在VS Code的串行监视器或者终端。

硬件要求

  • Arduino 板卡
  • 电位器
  • 红色LED
  • 220欧姆电阻

电路

将电位计的一个引脚连接到 5V,中心引脚连接到模拟引脚 0,其余引脚接地。接下来,将 220 欧姆限流电阻连接到数字引脚 9,并串联一个 LED。 LED 的长正引脚(阳极)应连接到电阻器的输出,而较短的负引脚(阴极)应接地。

电路图

模拟输入,串行输出

代码

在下面的草图中,声明两个引脚分配(我们的电位器的模拟 0 和 LED 的数字 9)和两个变量(sensor_value 和 output_value)后,创建一个串口连接。

接下来,在主循环中,sensor_value 被分配来存储从电位计读取的原始模拟值。 Arduino 的 analog_read范围为0到1023,而 set_duty_cycle_percent 范围仅为 0 到 100,因此,在使用电位计调暗 LED 之前,需要将电位计的数据转换为更小的范围。

转换这个值:

let output_value = (sensor_value * 100/1023) as u8;

输出值被指定为等于电位计的缩放值。 map() 接受五个参数:要映射的值、输入数据的低范围和高值以及要重新映射到的数据的低值和高值。在这种情况下,传感器数据从其原始范围 0 到 1023 向下映射到 0 到 255。

然后,新映射的传感器数据会输出到analogOutPin,随着电位计的转动,LED 会变暗或变亮。最后,原始传感器值和缩放传感器值均以稳定的数据流发送到VS Code串行监视器窗口。

编译并运行示例

cargo build
cargo run

完整代码如下:

src/main.rs

/*!
 * Analog input, analog output, serial output
 *
 * Reads an analog input pin, maps the result to a range from 0 to 100 and uses
 * the result to set the pulse width modulation (PWM) of an output pin.
 * 
 * Also prints the results to the Serial Monitor.
 *
 * We are using an embedded_hal compatible version.
 */
#![no_std]
#![no_main]

use arduino_hal::prelude::_unwrap_infallible_UnwrapInfallible;
use arduino_hal::simple_pwm::*;
use embedded_hal::delay::DelayNs;
use embedded_hal::pwm::SetDutyCycle;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut serial = arduino_hal::default_serial!(dp, pins, 57600);
    let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());

    let a0 = pins.a0.into_analog_input(&mut adc);
    let mut pwm_led = pins
        .d9
        .into_output()
        .into_pwm(&Timer1Pwm::new(dp.TC1, Prescaler::Prescale64));
    pwm_led.enable();

    let mut delay = arduino_hal::Delay::new();

    loop {
        let sensor_value = a0.analog_read(&mut adc) as u32;

        let output_value = (sensor_value * 100 / 1023) as u8;

        pwm_led.set_duty_cycle_percent(output_value).unwrap();

        ufmt::uwriteln!(
            &mut serial,
            "sensor: {}, output: {}",
            sensor_value,
            output_value
        )
        .unwrap_infallible();
        delay.delay_ms(2);
    }
}

模拟输入

使用电位器来控制 LED 的闪烁。

在此示例中,我们使用可变电阻器(电位计或光敏电阻器),使用 Arduino 板的一个模拟输入读取其值,并相应地更改内置 LED 的闪烁率。电阻器的模拟值被读取为电压,因为这就是模拟输入的工作原理。

硬件要求

  • Arduino板卡
  • 电位器或10k欧姆光明电阻和10k欧姆电阻
  • 引脚13上的内置LED或220欧电阻和红色LED

电路

将三根线连接到 Arduino 板。第一个从电位计的一个外部引脚接地。第二个电压从 5 伏到电位计的另一个外部引脚。第三个从模拟输入 0 到电位器的中间引脚。

对于此示例,可以使用连接到引脚 13 的电路板内置 LED。要使用附加 LED,请将其较长的引脚(正极引脚或阳极)连接到与 220 欧姆电阻串联的数字引脚 13,它是连接引脚 13 旁边的接地 (GND) 引脚的较短引脚(负引脚或阴极)。

基于光敏电阻的电路使用电阻分压器来允许高阻抗模拟输入测量电压。这些输入几乎不消耗任何电流,因此根据欧姆定律,无论电阻器的值如何,在连接到 5V 的电阻器另一端测得的电压始终为 5V。为了获得与光敏电阻值成比例的电压,需要一个电阻分压器。该电路使用一个可变电阻、一个固定电阻,测量点位于电阻中间。测量的电压 (Vout) 遵循以下公式:

Vout=Vin*(R2/(R1+R2))

其中 Vin 为 5V,R2 为 10k 欧姆,R1 为光敏电阻值,范围从黑暗中的 1M 欧姆到日光下的 10k 欧姆(10 流明),在亮光或阳光下小于 1k 欧姆(>100 流明)。

电路图

电位器

模拟输入(电位器)

光敏电阻器

模拟输入(光敏电阻)

代码

在此草图的开头,变量sensor_pin设置为连接电位计的模拟引脚0,led_pin设置为数字引脚9。您还将创建另一个变量sensor_value来存储从传感器读取的值。

analog_read() 将输入​​电压范围(0 至 5 伏)转换为 0 至 1023 之间的数字值。这是由微控制器内部称为模数转换器或 ADC 的电路完成的。

通过转动电位计的轴,可以改变电位计中心销(或游标)两侧的电阻值。这会改变中心引脚和两个外部引脚之间的相对电阻,从而在模拟输入处提供不同的电压。当轴沿一个方向转动到底时,中心销和接地销之间没有电阻。此时中心引脚的电压为 0 伏,analog_read() 返回 0。当轴沿另一个方向旋转到底时,中心引脚和连接到 +5 伏的引脚之间没有电阻。中心引脚的电压为 5 伏,analog_read() 返回 1023。在这之间,analog_read() 返回 0 到 1023 之间的数字,该数字与施加到引脚的电压量成正比。

该值存储在sensor_value中,用于为眨眼周期设置delay_ms()。值越大,周期越长,值越小,周期越短。该值在周期开始时读取,因此开/关时间始终相等。

编译并运行示例

cargo build
cargo run

完整代码如下:

src/main.rs

/*!
 * Analog Input
 * 
 * Demonstrates analog input by reading an analog sensor on analog pin 0 and
 * turning on and off a light emitting diode(LED) connected to digital pin 9.
 * The amount of time the LED will be on and off depends on the value obtained
 * by analogRead().

 */
#![no_std]
#![no_main]

use embedded_hal::delay::DelayNs;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut adc = arduino_hal::Adc::new(dp.ADC, Default::default());

    let a0 = pins.a0.into_analog_input(&mut adc);
    let mut led_pin = pins
        .d9
        .into_output();

    let mut delay = arduino_hal::Delay::new();
	loop{
        let sensor_value = a0.analog_read(&mut adc) as u32;
		led_pin.set_high();
		delay.delay_ms(sensor_value);
		led_pin.set_low();
		delay.delay_ms(sensor_value)
	}
}

通信

创建LED调光器

通过串口发送数据来调节LED的亮度

此示例演示如何将数据从个人计算机发送到 Arduino 板以控制 LED 的亮度。数据以单独的字节形式发送,每个字节的值范围为 0 到 255。该程序读取这些字节并使用它们来设置 LED 的亮度。

硬件要求

  • Arduino板卡
  • LED
  • 220欧电阻

电路

将220欧姆限流电阻连接到数字引脚9,并串联一个LED。 LED 的长正引脚(阳极)应连接到电阻器的输出,而较短的负引脚(阴极)应接地。

电路图

创建LED调光器

代码

编译并运行示例

cargo build
cargo run

使用VS Code的串行监视器,并使用十六进制或者二进制格式发送一个字节的数据。

完整代码如下:

src/main.rs

/*!
 * Dimmer
 *
 * Demonstrates sending data from the computer to the Arduino board, in this case
 * to control the brightness of an LED. The data is sent in individual bytes,
 *
 * each of which ranges from 0 to 255. Arduino reads these bytes and uses them to
 * set the brightness of the LED.
 */
#![no_std]
#![no_main]

use arduino_hal::{
    default_serial,
    simple_pwm::{IntoPwmPin, Prescaler, Timer1Pwm},
};
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = arduino_hal::Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let mut serial = default_serial!(dp, pins, 57600);

    let mut pwm_led = pins
        .d9
        .into_output()
        .into_pwm(&Timer1Pwm::new(dp.TC1, Prescaler::Prescale64));
    pwm_led.enable();

    loop {
        pwm_led.set_duty(serial.read_byte());
    }
}

读取ASCII字符串

解析以逗号分隔的整数字符串以淡出 LED。

此草图使用read_byte()函数从串口缓冲区里读取输入的字节,并使用heapless::Vec来创建buffer来存储输入的ASCII字符串的字节,输入字节以LF即'\n'换行作为终止符。之后用split()函数按逗号分割字符串,并将其转换为u8整型。字符串由非字母数字字符分隔的值。人们通常使用逗号来表示不同的信息(这种格式通常称为逗号分隔值或 CSV),但其他字符(例如空格或句点)也可以使用。这些值被解析为整数并用于确定 RGB LED 的颜色。您将使用VS Code串行监视器将“5,220,70”等字符串发送到开发板以更改灯光颜色。

硬件要求

  • Arduino板卡
  • 共阴极RGB LED
  • 3个220欧姆电阻
  • 连接线
  • 面包板

电路

您需要四根电线来构成上面的电路。一根电线将电路板的地线GND(如果是共阳极则需连接到5V电压)连接到RGB LED的最长引脚。您应该转动 LED,使最长的引脚位于右侧第二个引脚上。 将RGB LED放在面包板上,最长的引脚作为从顶部数第三个引脚。检查特定LED的数据表以验证引脚,但它们应该是 G,B,V+和R。因此,地线GND的电线应连接顶部的第二个引脚。 使用剩余的电线,将红色阴极连接到引脚9,将绿色阳极连接到引脚10,将蓝色阳极连接到引脚11,与电阻串联。 具有共阴极的RGB LED共用一个公共地线引脚(共阳极的共用一个公共电源引脚)。共阴极LED你需要将PWM引脚置于高电平点亮LED,共阳极则相反,以在LED两端产生电压差。因此,通过 set_duty() 发送0(共阳极则是255)会关闭 LED,而值255(共阳极则是0)会以全亮度打开 LED。在下面的代码中,您将在草图方面使用一些数学,因此您可以发送与预期亮度相对应的值。

电路图

读取ASCII字符串

代码

创建3个PWD_LED连接,需要注意的是,如果这里我们不先创建timer1,那么直接在into_pwm内部创建timer,会因为都要使用dp.TC1而报错。

let timer1 = Timer1Pwm::new(dp.TC1, Prescaler::Prescale64);
let mut red_pin = pins.d9.into_output().into_pwm(&timer1);
red_pin.enable();
let mut green_pin = pins.d10.into_output().into_pwm(&timer1);
green_pin.enable();
let timer2 = Timer2Pwm::new(dp.TC2, Prescaler::Prescale64);
let mut blue_pin = pins.d11.into_output().into_pwm(&timer2);
blue_pin.enable();

创建字符串字节缓冲区,缓冲区设置为512个字节,按照UNO的技术规范,RX缓冲区的大小为64个字节,但是如果字符串很长,超过这个大小,我们需要先将缓冲区内的字节及时读出,以避免缓冲区满了后丢失字节。关于丢失字节的问题,可以参见这个讨论Arduino Uno Serial read seems to be dropping bytes #248

let mut buffer: Vec<u8, 512> = Vec::new();

我们在读取到b'\n'时,对应的ascii为10u8,开始处理,并在处理结束后,清空缓冲区,以接收新的输入内容。我们这里假设,用户的输入间隔远高于处理速度,所以,在下一次RX缓冲区被填满之前,不需要清空buffer,现实情况一般如此。但是,如果存在高速率的M2M的场景,不能做这样的假设,需要更复杂的处理。

通过用","对buffer内的字节分割分组获得一个迭代器,依序使用迭代器内的元素得到RGB的三个字节切片。

let mut iter = buffer.split(|num| *num == b',');

let red_byte = iter.next().unwrap_or_default();
let green_byte = iter.next().unwrap_or_default();
let blue_byte = iter.next().unwrap_or_default();

之后,解析切片内的字节得到RGB的duty值(整形)

let red = u8::from_str_radix(str::from_utf8(red_byte).unwrap_or_default(), 10)
    .unwrap_or_default();
let blue = u8::from_str_radix(str::from_utf8(blue_byte).unwrap_or_default(), 10)
    .unwrap_or_default();
let green = u8::from_str_radix(str::from_utf8(green_byte).unwrap_or_default(), 10)
    .unwrap_or_default();

当然,我们这里可以采用更有效和更省空间的方法,就是直接使用一个指针和一个u8切片来来遍历一次buffer,并且在每次碰到b','时解析对应的字节(u8类型)并赋给对应的变量。当我们需要处理很长的ASCII字符串的时候,可以考虑使用这种方式来编写一个宏,相关内容参照Rust语言的文档,可以自行尝试一下。

最后,设置LED的duty值,我们就可以点亮它了。

red_pin.set_duty(red);
blue_pin.set_duty(blue);
green_pin.set_duty(green);

我们还可以将处理后的结果打印到串口以进行调试。

ufmt::uwriteln!(&mut serial, "RGB({},{},{})", red, green, blue).unwrap_infallible();

编译并运行示例

cargo build
cargo run

打开VS Code的串行监视器,输入类似于127,32,64并将换行符设置成LF,你可以看到,LED灯随着输入值的不同而显示不同的颜色。

完整代码如下:

src/main.rs

/*!
 * Reading a serial ASCII-encoded string.
 *
 * This sketch demonstrates the heapless::Vec type as buffer, heapless::Vec::split(), u8::from_str_radix()function.
 *
 * It looks for an ASCII string of comma-separated values.
 *
 * It parses them into ints, and uses those to fade an RGB LED.
 */
#![no_std]
#![no_main]

use core::{str, u8};

use arduino_hal::{
    default_serial, pins,
    prelude::*,
    simple_pwm::{IntoPwmPin, Prescaler, Timer1Pwm, Timer2Pwm},
    Peripherals,
};
use heapless::Vec;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);
    let mut serial = default_serial!(dp, pins, 57600);

    let timer1 = Timer1Pwm::new(dp.TC1, Prescaler::Prescale64);
    let mut red_pin = pins.d9.into_output().into_pwm(&timer1);
    red_pin.enable();
    let mut green_pin = pins.d10.into_output().into_pwm(&timer1);
    green_pin.enable();
    let timer2 = Timer2Pwm::new(dp.TC2, Prescaler::Prescale64);
    let mut blue_pin = pins.d11.into_output().into_pwm(&timer2);
    blue_pin.enable();

    let mut buffer: Vec<u8, 512> = Vec::new();
    loop {
        let b = serial.read_byte();

        if b == b'\n' {
            let mut iter = buffer.split(|num| *num == b',');

            let red_byte = iter.next().unwrap_or_default();
            let green_byte = iter.next().unwrap_or_default();
            let blue_byte = iter.next().unwrap_or_default();

            let red = u8::from_str_radix(str::from_utf8(red_byte).unwrap_or_default(), 10)
                .unwrap_or_default();
            let blue = u8::from_str_radix(str::from_utf8(blue_byte).unwrap_or_default(), 10)
                .unwrap_or_default();
            let green = u8::from_str_radix(str::from_utf8(green_byte).unwrap_or_default(), 10)
                .unwrap_or_default();
            buffer.clear();
            red_pin.set_duty(red);
            blue_pin.set_duty(blue);
            green_pin.set_duty(green);
            ufmt::uwriteln!(&mut serial, "RGB({},{},{})", red, green, blue).unwrap_infallible();
        } else {
            buffer.push(b).unwrap();
        }
    }
}

具有ASCII编码输出的串行调用和响应(握手)

使用调用和响应(握手)方法发送多个变量,并在发送前对值进行 ASCII 编码。

此示例演示了使用调用和响应(握手)方法从 Arduino 板到计算机的基于字符串的通信。 该草图在启动时发送一个 ASCII 字符串并重复该操作,直到收到计算机的串行响应。然后,它以 ASCII 编码的数字形式发送三个传感器值,以逗号分隔并以换行符和回车符终止,并等待计算机的另一个响应。 您可以使用VS Code串行监视器查看发送的数据。下面的示例将传入字符串上的逗号并再次将字符串转换为数字。 你也可以修改此示例使用二进制值发送。虽然作为 ASCII 编码字符串发送需要更多字节,但这意味着您可以轻松地为每个传感器读数发送大于 255 的值。在串行终端程序中读取也更容易。

硬件要求

  • Arduino板卡
  • 2个模拟传感器(电位器、光电管、FSR等)
  • 按钮
  • 3个10k欧姆电阻
  • 连接线
  • 面包板

电路

使用用作分压器的 10K 欧姆电阻将模拟传感器连接到模拟输入引脚0和1。将按钮或开关连接到数字I/O引脚 2,并使用10K欧姆电阻作为接地参考。

电路图

具有ASCII编码输出的串行调用和响应(握手)

代码

前面我们使用过serail.read_byte()或者nb::block!(serial.read())来阻塞主进程运行,等待串口信息输入。而在本例中,因为我们需要在未接受到信息的时候向串口发送数据或者做其它处理,因此,不能采用阻塞的方式来运行程序。我们需要用非阻塞方式接受信息,非阻塞方式在读取信息时,会返回一个nb::Result,如果读取到字节,他返回该字节,如果未读取到字节,他返回一个Err。这与C/C++ API通过available()方法检查RX的缓冲区未读取的字节数。因此,我们可以用match来判断Result,如果是Ok,则运行响应的处理过程,如果是Err,则向串口发出一个字节"a"

编译并运行示例

cargo build
cargo run

之后打开VS Code串行监视器,我们会看到终端上显示字母a,而如果我们输入任意字符串后,终端上会显示类似于"0,518,324"这样的信息。

完整代码如下:

src/main.rs

/*
 * Serial Call and Response in ASCII
 *
 * This program sends an ASCII A (byte of value 65) on startup and repeats that
 * until it gets some data in. Then it waits for ASCII string in the serial port, and
 * sends three ASCII-encoded, comma-separated sensor values, truncated by a
 * linefeed and carriage return, whenever it gets bytes in.
 */
#![no_std]
#![no_main]

use arduino_hal::{default_serial, delay_ms, pins, prelude::*, Adc, Peripherals};
use heapless::Vec;
use panic_halt as _;

#[arduino_hal::entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);
    let mut serial = default_serial!(dp, pins, 57600);
    let mut adc = Adc::new(dp.ADC, Default::default());

    let push_btn = pins.d2.into_floating_input();
    let a0 = pins.a0.into_analog_input(&mut adc);
    let a1 = pins.a1.into_analog_input(&mut adc);
    let mut buffer: Vec<u8, 512> = Vec::new();
    loop {
        let b = serial.read();
        match b {
            Ok(b) => {
                if b != b'\n' {
                    buffer.push(b).unwrap_or_default();
                } else {
                    let mut state = 0u8;
                    if push_btn.is_high() {
                        state = 1u8;
                    }
                    ufmt::uwriteln!(
                        &mut serial,
                        "{},{},{}",
                        state,
                        a0.analog_read(&mut adc),
                        a1.analog_read(&mut adc)
                    )
                    .unwrap_infallible();

                    delay_ms(1000);
                }
            }

            Err(_) => {
                ufmt::uwriteln!(&mut serial, "a").unwrap_infallible();
            }
        }
    }
}

传感器

温度报警器

当温度超过阈值的时候蜂鸣器会报警

当温度到达我们设定的限定值时,报警器就会响。我们可以用于厨房温度检测报警等等,各种需要检测温度的场合。这个项目中,除了要用到蜂鸣器外,还需要一个LM35温度传感器。

硬件要求

  • Arduino板卡
  • LM35温度传感器
  • 蜂鸣器
  • 连接线
  • 面包板

电路

将蜂鸣器的一端接在数字引脚8上,另一端直接接地。

在接LM35温度传感器时,注意三个引脚的位置,有LM35字样的一面面向自己,从左至右依次接5V、Analog 0、GND。注意不要将正负极接反。

电路图

温度报警器

代码

设置温度报警阈值

const TEMP_THRESHOLD: f32 = 30.0;

依公式将传感器数值转换为温度

let temp = sensor_value * 5.0 / 10.24;

编译并运行示例

cargo build
cargo run

之后打开VS Code串行监视器,我们会在终端上看到当前测量到的温度。

完整代码如下:

src/main.rs

/*!
 * Temperature Alarm
 *
 * This example shows how to read value from a LM35 temperature sensor.
 * When the temperature is above the TMEP_THRESHOLD, the buzzer will ring.
 */
#![no_std]
#![no_main]

use arduino_hal::{
    default_serial, entry, pins, prelude::_unwrap_infallible_UnwrapInfallible, Adc, Peripherals,
};
use panic_halt as _;
use ufmt_float::uFmt_f32;

#[entry]
fn main() -> ! {
    const TEMP_THRESHOLD: f32 = 30.0;

    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);
    let mut serial = default_serial!(dp, pins, 57600);
    let mut adc = Adc::new(dp.ADC, Default::default());

    let temp_input = pins.a0.into_analog_input(&mut adc);
    let mut buzzer_output = pins.d8.into_output();
    loop {
        let sensor_value = temp_input.analog_read(&mut adc) as f32;
        let temp = sensor_value * 5.0 / 10.24;
        ufmt::uwriteln!(&mut serial, "temp:{}", uFmt_f32::Two(temp)).unwrap_infallible();
        if temp > TEMP_THRESHOLD {
            buzzer_output.toggle();
        }
    }
}

震动探测

使用震动传感器触发中断,并处理中断信号

此示例向您展示了如何使用倾斜开关作为震动传感器来检测设备是否处于震动状态。震动传感器,我们从名字中应该就可以判断,传感器能够检测震动中的物体。我们用什么来做震动传感器呢?那就是倾斜开关。倾斜开关,其内部含有导电珠子,器件一旦震动,珠子随之滚动,就能使两端的导针导通。

通过这个原理,通过倾斜开关可以做个简单的震动传感器,并把震动传感器和LED的结合,当传感器检测到物体震动时,LED亮起,停止震动时,LED关闭。

硬件要求

  • Arduino板卡
  • 倾斜开关
  • 220欧姆电阻
  • 连接线
  • 面包板

电路

用三根线连接倾斜开关,其中红线连接到5V电源,黑色线通过220欧姆电阻连接到地线GND,且接地脚接到数字引脚3。

电路图

震动检测

代码

在没有任何打扰的情况下,程序正常运行,让LED一直处于关闭。如果板子被摇晃,就触发中断。按照LED灯带中的关于中断的说明,我们知道,连接在引脚3上的开关触发的中断类型为INT1。因此,我们需要对这个终端进行配置。

首先,我们配置中断信号的检测为上沿(RISING)。在arduino_hal中,需要写入外部中断控制寄存器(External Interrupt Control Register),并设置外部中断检测寄存器的值为RISING即0x03。这一段代码可以理解为,我们要求外部中断控制寄存器为我们对外部中断检测寄存器执行一段操作,对应的引脚3,INT1的中断检测为isc1()函数返回的对象,并且设置值为0x03。这样,INT1中断请求就会在引脚电平,由低变高时对MCU发出。

dp.EXINT.eicra.write(|w| w.isc1().bits(0x03));

然后,我们配置中断服务路由启用INT1中断。在arduino_hal中,需要写入一段运行指令到外部中断屏蔽寄存器(External Interrupt Mask Register),要求将int1()返回的对象对应的屏蔽位设置为1,int1()返回的是INT1中断的请求对象。

dp.EXINT.eimsk.write(|w| w.int1().set_bit());

之后,我们全局启用中断调度。

unsafe {
    interrupt::enable();
}

此时,中断服务路由会根据这个配置来接受中断请求,并且执行对应的任务。因此,我们只需震动开关,而不需要在代码里主动读取任何引脚数据,就可以自动找到对应的执行步骤了。将状态值设为true。

#[interrupt(atmega328p)]
fn INT1() {
    unsafe {
        STATE.store(true, Ordering::SeqCst);
    }
}

而当主程序检测到状态值为true的时候,就会将LED点亮并重新将STATE设为false。如果之后震动持续,则后续会继续产生中断,将状态值设为true,继续触发点亮LED的动作。而震动停止后,因为STATE为false,所以LED熄灭。这里,我们因为需要在INITmain之间共享STATE变量,所以我们将STATE设置为全局变量,我们选了使用原子布尔类型来作为这个全局变量的类型。

static STATE: AtomicBool = AtomicBool::new(false);

编译并运行示例

cargo build
cargo run

当你晃动面包板的时候,LED灯会被点亮;停止晃动后,LED灯熄灭。

完整代码如下:

src/main.rs

/*!
 * Detecting vibration
 * 
 * If you shake the breadboard with tilt switch sensor on, the LED light on, otherwise the LED will be off.
 * You don't have to keep the breadboard level.
 */
#![no_std]
#![no_main]
#![feature(abi_avr_interrupt)]

use core::sync::atomic::{AtomicBool, Ordering};

use arduino_hal::{delay_ms, entry, pins, Peripherals};
use avr_device::interrupt;
use panic_halt as _;

static STATE: AtomicBool = AtomicBool::new(false);

#[interrupt(atmega328p)]
fn INT1() {
    let current = STATE.load(Ordering::SeqCst);
    if !current {
        STATE.store(true, Ordering::SeqCst);
    }
}

#[entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);

    let mut led = pins.d13.into_output();

    dp.EXINT.eicra.write(|w| w.isc1().bits(0x03));
    dp.EXINT.eimsk.write(|w| w.int1().set_bit());

    unsafe {
        interrupt::enable();
    }

    loop {
        if STATE.load(Ordering::SeqCst) {
            STATE.store(false, Ordering::SeqCst);
            led.set_high();
            delay_ms(500);
        } else {
            led.set_low();
        }
    }
}

感光灯

使用光敏电阻控制LED明灭

这个示例中将介绍光敏电阻。在黑暗的环境中,光敏电阻具有非常高阻值的电阻。光线越强,电阻值反而越低。通过读取这个电阻值,就可以检查光线的亮暗了。我们这里选用的是光敏二极管,光敏二极管其实就是光敏电阻中的一种,只是它还具有正负极性。

当环境黑暗的时候,光敏电阻的阻值提高,LED变亮;当环境明亮的时候,LED变暗。

硬件要求

  • Arduino板卡
  • 光敏电阻
  • 10k电阻

可选

  • 220欧电阻
  • LED灯
  • 手电筒

电路

用三根线连接倾斜开关,其中红线通过10k欧姆电阻连接到5V电源,黑色连接到地线GND,且接电源的引脚也接到模拟引脚3。这里我们使用内置LED。如果需要使用外接LED,可以用220欧姆电阻和LED串联接通。注意,光敏二极管一般是反向使用的。长脚为正极,短脚为负极。

当光照变强的时候,光照电流变大,电压降低,输出的数值变小,当低于一个阈值的时候,灯就熄灭了。反之,光电流变小,电压提升,输出的数值变大,当高于一个阈值时,灯被点亮。

电路图

感光灯

代码

编译并运行示例

cargo build
cargo run

使用VS Code的串行监视器,你会在终端看到一个读数,当你用手电筒等强光光源照射光敏原件时,读数会变低。当读数高于阈值,本例是1000时,LED灯会被点亮;而当读数低于1000时,LED灯熄灭。

在本示例中,连接10k电阻后,在黑暗环境里,读数为1023,在普通日光灯环境下,读数为850-870,在手机手电筒近距离照射下,读数为10-30。如果数值变化不敏感,请检查下光敏二极管是否正确连接。

完整代码如下:

src/main.rs

/*!
 * Photosensive Light
 *
 * When the environment goes dark, the resistance of the photoresistor increases and the LED becomes brighter;
 * when the environment goes bright, the LED becomes darker.
 */
#![no_std]
#![no_main]

use arduino_hal::{
    default_serial, delay_ms, entry, pins, prelude::_unwrap_infallible_UnwrapInfallible, Adc,
    Peripherals,
};
use panic_halt as _;

#[entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);
    let mut serial = default_serial!(dp, pins, 57600);
    let mut adc = Adc::new(dp.ADC, Default::default());

    let mut led = pins.d13.into_output();
    let sensor = pins.a0.into_analog_input(&mut adc);

    loop {
        let sensor_value = sensor.analog_read(&mut adc);
        ufmt::uwriteln!(&mut serial, "sensor value:{}", sensor_value).unwrap_infallible();

        if sensor_value >1000 {
            led.set_high();
        } else {
            led.set_low();
        }
        delay_ms(500);
    }
}

红外接收管

使用Crate::Infrared库来接受红外控制器信号

此示例向您展示了,如何使用红外接收管接受红外控制器发出的信号,并将接受到的信号代码输出到串行监视器的终端上。

红外接收管有多种不同的规格,一般有三个引脚,VCC、GND和Y,分别连接电源、地线和数字/模拟信号引脚。具体的连接,请参考您购买的器件。

红外传输有多种不同的协议,其编解码方式因此而有所不同。本示例使用了常见的NEC协议。如果不知道控制器的协议,可以使用Arduino的IRremote库或者IRMP库编写一个接受程序,来获取协议信息。

硬件要求

  • Arduino板卡
  • 红外接收管
  • 红外控制器
  • 红色和绿色LED
  • 2个220欧姆电阻
  • 连接线
  • 面包板

电路

使用3根线连接红外接收器,其中红线连接5v电源,黑线连接GND,以给红外接收管供电;另外,将绿线连接到数字引脚2上作为信号输入。

此外分别将连接红色和绿色LED连接到板卡的数字引脚6和7上。

电路图

红外接收管

代码

Pin Change Interrupt(PCI)会在对应的引脚电平发生变化的时候产生中断请求,ATMega328P的0-23引脚都可以产生PCI请求,而PCI请求的配置分为BCD三个组。我们需要设置启用PCI引脚所在的组和屏蔽码来从引脚2接受红外信号。相应的内容可以查询ATmega328P的数据表。这里我们需要设置PCI的控制寄存器PCICR和屏蔽寄存器PCMSK。

PCICR是一个8位寄存器,具体描述如下:

  • 位7..3 - Res:保留位

    这些位是Atmel® ATmega328P 中未使用的位,并且始终读为零。

  • 位2 - PCIE2:引脚电平变化中断启用2

    当PCIE2 位被置位(1)并且状态寄存器(SREG)中的I 位被置位(1)时,引脚电平变化中断2 被启用。 任何已启用的 PCINT23..16 引脚上的任何更改都会导致中断。 引脚改变中断请求对应的中断从PCI2中断向量执行。 PCINT23..16 引脚由 PCMSK2 寄存器单独启用。

  • 位1 - PCIE1:引脚电平变化中断启用1

    当PCIE1 位被置位(1)并且状态寄存器(SREG)中的I 位被置位(1)时,引脚电平变化中断1 被使能。 任何已启用的 PCINT14..8 引脚上的任何更改都会导致中断。 引脚改变中断请求对应的中断从PCI1中断向量执行。 PCINT14..8 引脚由 PCMSK1 寄存器单独启用。

  • 位0 - PCIE0:引脚电平变化中断启用0

    当PCIE0 位被置位(1)并且状态寄存器(SREG)中的I 位被置位(1)时,引脚电平变化中断0 被使能。 任何已启用的 PCINT7..0 引脚上的任何更改都会导致中断。 引脚改变中断请求对应的中断从PCI0中断向量执行。 PCINT7..0 引脚由 PCMSK0 寄存器单独启用。

查询数据表,我们可知,数字引脚2对应的是控制器的PD2,PCINT18。因此我们需要设置PCICR中的PCIE2位为1。

dp.EXINT.pcicr.write(|w| unsafe { w.bits(0b100) });

为了启用对应的引脚,我们还需要设置PCMSK2中对应PCINT18位

dp.EXINT.pcmsk2.write(|w| w.bits(0b100));

我们可以使用类库的Receiver类型的with_pin()方法,创建一个接收端实例,设置Receiver的接收引脚以及工作频率。

let ir = Receiver::with_pin(Clock::FREQ, pins.d2);

PortD上的PCI会产生一个PCINT2的ISR,因此,我们需要编写对应的中断处理函数,函数名为PCINT2。如果在中断请求处理时,我们得到解析到一个正确的命令,那么,我们会在中断free的阶段,将命令数据写入一个数据共享区,这样主循环就可以读取到解析后的命令对象,并打印到终端上。

#[avr_device::interrupt(atmega328p)]
fn PCINT2() {
    let recv = unsafe { RECEIVER.as_mut().unwrap() };
    let signal_led = unsafe { SIGNAL_LED.as_mut().unwrap() };
    let error_led = unsafe { ERROR_LED.as_mut().unwrap() };

    let now = CLOCK.now();

    match recv.event_instant(now) {
        Ok(Some(cmd)) => {
            avr_device::interrupt::free(|cs| {
                let cell = CMD.borrow(cs);
                cell.set(Some(cmd));
            });
            error_led.set_low();
        }
        Ok(None) => (),
        Err(_) => error_led.set_high(),
    }

    signal_led.toggle();

此外,我们还需要为了接收红外信号创建一个工作时钟,这个时钟定义了红外接收管的信号解析,大部分红外发射器的发射频率在38kHz,而红外接收器的工作频率应该保持可以完整解析的接收发射器的信号,可以理解为一连串的PWM信号。具体的设置可以参考对应接收管的手册。

我们这里将接收器的时钟频率设置在20kHz,使用8分频器,这样,每个时钟周期的宽度为16000/8/20=100,那么,每个周期可以分辨0.5µs的电平,NEC协议在Repeat标志中,会使用562.5µs的脉冲,因此信号分辨率足够了。关于工作时钟的编码,我们可以参见编写millis函数这个部分。我们设计一个Clock的结构体,他包含一个计数器属性用来存储当前的时钟周期数

struct Clock {
    cntr: Mutex<Cell<u32>>,
}

因为我们后续需要在中断处理函数中传递当前的时钟周期,以找到发射过来的信号电平并进行解析

let now = CLOCK.now();

match recv.event_instant(now) {
	...
}

同时,我们需要通过start()方法来设置和启用工作时钟及其中断

impl Clock {
    const FREQ: u32 = 20_000;
    const PRESCALER: CS0_A = CS0_A::PRESCALE_8;
    const TOP: u8 = 99;

	...

    pub fn start(&self, tc0: arduino_hal::pac::TC0) {
        // Configure the timer for the above interval (in PWM Phase mode)
        tc0.tccr0a.write(|w| w.wgm0().ctc());
        tc0.ocr0a.write(|w| w.bits(Self::TOP));
        tc0.tccr0b.write(|w| w.cs0().variant(Self::PRESCALER));

        // Enable interrupt
        tc0.timsk0.write(|w| w.ocie0a().set_bit());
    }

	...
}

并使用tick()方法更新工作时钟计数

#[avr_device::interrupt(atmega328p)]
fn TIMER0_COMPA() {
    CLOCK.tick();
}

impl Clock {
	...

    pub fn tick(&self) {
        avr_device::interrupt::free(|cs| {
            let c = self.cntr.borrow(cs);

            let v = c.get();
            c.set(v.wrapping_add(1));
        });
    }
}

以及now()方法获取当前的时钟周期

impl Clock {

    ...

    pub fn now(&self) -> u32 {
        avr_device::interrupt::free(|cs| self.cntr.borrow(cs).get())
    }

    ...
}

至此,我们就可以编写出完整的红外接收信号的处理代码了。

编译并运行示例

cargo build
cargo run

之后打开VS Code串行监视器,当终端上显示Hello from Arduino and Rust!字样的时候,说明程序已经准备好接收并解析红外信号。

我们按红外遥控器的任意键,终端上会显示类似于"Cmd: Address: 0, Command: 12, repeat: true"这样的信息。这表示,信号已经成功接收,绿色LED应该在接收信号时闪烁。其中Address表示当前设备的设备地址,Command表示按键的数字代码,repeat表示键是否被连续按下。

完整代码如下:

src/main.rs

/*!
 * Infrared
 *
 * This example shows how to use crate infrared to receive infrared signal from IR Remote controller
 * with Nec protocol by Pin Change Interrupt.
 *
 * The code for this example refers to the crate example from
 * https://github.com/jkristell/infrared/blob/master/examples/arduino_uno/src/bin/external-interrupt.rs
 *
 * Pins and functions:
 * d7: Signal led
 * d6: Error led
 *
 * d2: Infrared rx
 */
#![no_std]
#![no_main]
#![feature(abi_avr_interrupt)]

use core::cell::Cell;

use arduino_hal::{
    default_serial,
    hal::port::{PD2, PD6, PD7},
    pac::tc0::tccr0b::CS0_A,
    pins,
    port::{
        mode::{Floating, Input, Output},
        Pin,
    },
    prelude::*,
    Peripherals,
};
use avr_device::interrupt::Mutex;
use infrared::{
    protocol::{nec::NecCommand, *},
    Receiver,
};
use panic_halt as _;

type IrPin = Pin<Input<Floating>, PD2>;
type IrProto = Nec;
type IrCmd = NecCommand;

static CLOCK: Clock = Clock::new();
static mut RECEIVER: Option<Receiver<IrProto, IrPin>> = None;
static mut SIGNAL_LED: Option<Pin<Output, PD7>> = None;
static mut ERROR_LED: Option<Pin<Output, PD6>> = None;

static CMD: Mutex<Cell<Option<IrCmd>>> = Mutex::new(Cell::new(None));

#[arduino_hal::entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);
    let mut serial = default_serial!(dp, pins, 57600);

    // Monotonic clock to keep track of the time
    CLOCK.start(dp.TC0);

    let mut uno_led = pins.d13.into_output();
    let mut signal_led = pins.d7.into_output();
    let mut error_led = pins.d6.into_output();

    uno_led.set_low();
    signal_led.set_low();
    error_led.set_low();

    // Enable group 2 (PORTD)
    dp.EXINT.pcicr.write(|w| unsafe { w.bits(0b100) });

    // Enable pin change interrupts on PCINT18 which is pin PD2 (= d2)
    dp.EXINT.pcmsk2.write(|w| w.bits(0b100));

    let ir = Receiver::with_pin(Clock::FREQ, pins.d2);

    unsafe {
        RECEIVER.replace(ir);
        SIGNAL_LED.replace(signal_led);
        ERROR_LED.replace(error_led);
    }

    // Enable interrupts globally
    unsafe { avr_device::interrupt::enable() };

    ufmt::uwriteln!(&mut serial, "Hello from Arduino and Rust!\r").unwrap_infallible();

    loop {
        if let Some(cmd) = avr_device::interrupt::free(|cs| CMD.borrow(cs).take()) {
            ufmt::uwriteln!(
                &mut serial,
                "Cmd: Address: {}, Command: {}, repeat: {}\r",
                cmd.addr,
                cmd.cmd,
                cmd.repeat
            )
            .unwrap_infallible();
        }
    }
}

#[avr_device::interrupt(atmega328p)]
fn PCINT2() {
    let recv = unsafe { RECEIVER.as_mut().unwrap() };
    let signal_led = unsafe { SIGNAL_LED.as_mut().unwrap() };
    let error_led = unsafe { ERROR_LED.as_mut().unwrap() };

    let now = CLOCK.now();

    match recv.event_instant(now) {
        Ok(Some(cmd)) => {
            avr_device::interrupt::free(|cs| {
                let cell = CMD.borrow(cs);
                cell.set(Some(cmd));
            });
            error_led.set_low();
        }
        Ok(None) => (),
        Err(_) => error_led.set_high(),
    }

    signal_led.toggle();
}

#[avr_device::interrupt(atmega328p)]
fn TIMER0_COMPA() {
    CLOCK.tick();
}

struct Clock {
    cntr: Mutex<Cell<u32>>,
}

impl Clock {
    const FREQ: u32 = 20_000;
    const PRESCALER: CS0_A = CS0_A::PRESCALE_8;
    const TOP: u8 = 99;

    pub const fn new() -> Clock {
        Clock {
            cntr: Mutex::new(Cell::new(0)),
        }
    }

    pub fn start(&self, tc0: arduino_hal::pac::TC0) {
        // Configure the timer for the above interval (in PWM Phase mode)
        tc0.tccr0a.write(|w| w.wgm0().ctc());
        tc0.ocr0a.write(|w| w.bits(Self::TOP));
        tc0.tccr0b.write(|w| w.cs0().variant(Self::PRESCALER));

        // Enable interrupt
        tc0.timsk0.write(|w| w.ocie0a().set_bit());
    }

    pub fn now(&self) -> u32 {
        avr_device::interrupt::free(|cs| self.cntr.borrow(cs).get())
    }

    pub fn tick(&self) {
        avr_device::interrupt::free(|cs| {
            let c = self.cntr.borrow(cs);

            let v = c.get();
            c.set(v.wrapping_add(1));
        });
    }
}

红外发射器

(由于Infrared库需要依赖旧版本的embedded-hal,而avr-hal的pwm并没有为实现PwmPin Trait,因为他已经在embedded-hal 1.0版本中被移除了,所以,目前没有找到更好的办法实现红外发射)

应用

交通信号灯

本示例展示了如何设置一个行人过马路的路灯

开始时,汽车灯为绿灯,行人灯为红灯,代表车行人停。一旦行人,也就是你,按下按钮,请求过马路,那么汽车灯开始由绿变黄,等待3秒,然后,行人灯由红变绿,汽车灯变红。在行人通行的过程中,设置了一个过马路的时间 cross_time,一旦到点,行人绿灯开始闪烁3秒,提醒行人快速过马路。 闪烁完毕,最终,又回到了开始的状态,汽车灯为绿灯,行人灯为红灯。

硬件要求

  • Arduino板卡
  • 2个红色LED,2个绿色LED和1个黄色LED
  • 5个220欧姆电阻
  • 1个按钮
  • 1个10k欧姆电阻
  • 连接线
  • 面包板

电路

用1个绿色LED和1个红色LED表示行人交通灯,通过220欧电阻将正极连接到数字信号引脚7和8上。用1个绿色LED、一个黄色LED和1个红色LED表示汽车交通灯,通过220欧姆电阻将正极连接到数字信号引脚10、11和12上。

将按钮同侧的两个引脚分别连接到5V电压和通过10k欧姆电阻接地,将接地引脚同时连接到数字引脚上接收按钮发出的信号。

电路图

交通信号灯

代码

编译并运行示例

cargo build
cargo run

之后打开VS Code串行监视器,我们会看到终端上显示字母a,而如果我们输入任意字符串后,终端上会显示类似于"0,518,324"这样的信息。

完整代码如下:

src/main.rs

/*!
 * Traffic light for vehicles and passagers
 *
 * At the beginning, the car light is green and the pedestrian light is red, indicating that cars and pedestrians
 * have stopped.
 * Once the pedestrian, that is, you, presses the button and requests to cross the road, the car lights start to
 * change from green to yellow, wait for 3 seconds, then the pedestrian lights change from red to green, and the
 * car lights turn red. During the pedestrian passage, a cross_time is set for crossing the road. Once the cross_time
 * is reached, the pedestrian green light starts to flash for 3 seconds to remind pedestrians to cross the road quickly.
 * The flashing is completed, and finally, it returns to the starting state, with the car light being green and the
 * pedestrian light being red.
 */

#![no_std]
#![no_main]

use arduino_hal::{delay_ms, entry, pins, Peripherals};
use arduino_uno_example::utils::millis::{millis, millis_init};
use panic_halt as _;

#[entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = pins!(dp);

    const PREPARE_TIME: u32 = 3000;
    const CROSS_TIME: u32 = 5000;
    const WHOLE_TIME: u32 = 2 * PREPARE_TIME + CROSS_TIME;
    const CROSS_ALARM_TIME: u32 = PREPARE_TIME + CROSS_TIME;
    const BLINK_INTERVAL: u32 = 200;

    let passenger_btn = pins.d9.into_floating_input();
    let mut last_btn_is_high = false;
    let mut padestrian_red = pins.d8.into_output();
    let mut pedestrian_green = pins.d7.into_output();

    padestrian_red.set_high();
    pedestrian_green.set_high();

    let mut vehicle_red = pins.d12.into_output();
    let mut vehicle_yellow = pins.d11.into_output();
    let mut vehicle_green = pins.d10.into_output();

    vehicle_red.set_high();
    vehicle_yellow.set_high();
    vehicle_green.set_high();

    delay_ms(1000);

    pedestrian_green.set_low();
    vehicle_red.set_low();
    vehicle_yellow.set_low();

    let mut pedestrian_start_time = 0u32;
    let mut prev_time = 0u32;

    millis_init(dp.TC0);
    unsafe {
        avr_device::interrupt::enable();
    }

    loop {
        let elapsed_time = millis() - pedestrian_start_time;
        let btn_is_high = passenger_btn.is_high();

        if pedestrian_start_time == 0 || elapsed_time > CROSS_TIME + 2 * PREPARE_TIME {
            if btn_is_high != last_btn_is_high {
                if btn_is_high {
                } else {
                    pedestrian_start_time = millis();
                    vehicle_green.set_low();
                    vehicle_yellow.set_high();
                }

                last_btn_is_high = btn_is_high;
            }
        } else {
            if elapsed_time < PREPARE_TIME {
                let current_time = millis() - pedestrian_start_time;
                if current_time - prev_time > BLINK_INTERVAL {
                    vehicle_yellow.toggle();
                    prev_time = current_time;
                }
            } else if elapsed_time < CROSS_ALARM_TIME && elapsed_time >= PREPARE_TIME {
                prev_time = 0;
                vehicle_red.set_high();
                pedestrian_green.set_high();
                padestrian_red.set_low();
                vehicle_yellow.set_low();
            } else if elapsed_time >= CROSS_ALARM_TIME && elapsed_time < WHOLE_TIME {
                let current_time = elapsed_time - CROSS_ALARM_TIME;
                if current_time - prev_time > BLINK_INTERVAL {
                    pedestrian_green.toggle();
                    prev_time = current_time;
                }
            } else if elapsed_time == WHOLE_TIME {
                vehicle_green.set_high();
                vehicle_red.set_low();
                pedestrian_green.set_low();
                padestrian_red.set_high();
                prev_time = 0;
            }
        }
    }
}