Funpack4-2 - 用SAME51J20A Curiosity Nano 触摸控制呼吸灯
该项目使用了SAME51J20A Curiosity Nano,实现了触摸控制呼吸灯的设计,它的主要功能为:通过电容触摸模块实现左滑、右滑控制 WS2812B LED 呼吸灯的速率,并且在 OLED 上显示当前速度。
标签
Funpack活动
OLED
WS2812B
Rust
SAME51J20A
学嵌入式的Momo
更新2025-07-02
19

本项目使用 SAME51J20A Curiosity Nano 开发板,实现了触摸控制呼吸灯,可以通过电容触摸模块实现左滑、右滑检测,从而控制 WS2812B LED 呼吸灯的速率,并且在 OLED 上显示当前速度。

硬件

SAME51J20A Curiosity Nano

开发板为 SAME51J20A Curiosity Nano,它包含一个 SAME51J20A MCU:

  • Cortex-M4F 内核
  • 120 MHz 运行频率
  • 1024 KB Flash
  • 256 KB SRAM
  • I²C / SPI / UART / … 丰富外设

同时还有板载的调试器,到手即用,不需要买额外的调试器。

触摸模块

触摸模块使用基于TTP224 的四输入电容输入模块。

它有 4 个电容触摸输入,对应到 4 个 GPIO。右滑的时候,1 2 3 4 号引脚会依次变成高电平,左右就是 4 3 2 1。

WS2812B LED

LED 呼吸灯使用 WS2812B LED,比开发板上的 LED 效果更好,能实现更多的颜色。

OLED 屏幕

OLED 屏幕使用 SSD1306 驱动,I2C 接口,分辨率为128*64。

软件

模块

代码分成 5 个模块:

  • Speed 模块:保存当前呼吸灯的速率,供其他模块获取和修改
  • Tick 模块:保存当前的时间,基于 SysTick 实现,延时和呼吸灯都需要根据时间来实现
  • Touch 模块:检测左滑和右滑,调用 Speed 模块更新速率
  • WS2812B 模块:根据当前时间和速率,调节 LED 亮度实现呼吸灯
  • OLED 模块:把当前速率显示在 OLED 上

Speed 模块

Speed 模块保存了一个速度字,其他模块可以调用Speed 模块的方法来获取速度、修改速度。

impl Speed {
fn new() -> Self {
Self {
speed: RefCell::new(1.0),
}
}
// 获取速度
fn get(&self) -> f64 {
*self.speed.borrow()
}
// 加快速度,最大 10
fn increase(&self) {
let new_speed = *self.speed.borrow() + 1.0;
if new_speed <= 10.0 {
self.speed.replace(new_speed);
}
}
// 减慢速度,最小 1
fn decrease(&self) {
let new_speed = *self.speed.borrow() - 1.0;
if new_speed >= 1.0 {
self.speed.replace(new_speed);
}
}
}

Tick 模块

Tick 模块类似 HAL_GetTick,记录了上电后经过的毫秒数。

首先配置 SysTick,让它 1ms 触发一次中断:

pub fn init_tick(mut syst: cortex_m::peripheral::SYST) {
syst.set_reload(120 * 1000); // 1ms, assuming clock is 120MHz
syst.set_clock_source(cortex_m::peripheral::syst::SystClkSource::Core);
syst.enable_counter();
syst.enable_interrupt();
}

在中断处理函数里,对全局变量 TICK 加1:

static TICK: AtomicU32 = AtomicU32::new(0);

#[exception]
fn SysTick() {
TICK.fetch_add(1, Relaxed);
}

其他模块可以通过 get_tick 获取已经经过的毫秒数,作为当前时间:

pub fn get_tick() -> u32 {
TICK.load(Relaxed)
}

任务

Touch 模块,WS2812B 模块,OLED 模块可以对应到 3 个独立的任务,每个任务都是循环:

下面分别介绍这三个任务。

WS2812B 任务

WS2812B 任务在一个循环里执行:

  1. 从 Speed 模块获取当前速率
  2. 从 Tick 模块获取当前时间
  3. 计算 LED 亮度
  4. 发送 WS2812B Reset 信号
  5. 发送 RGB 值
    let mut ws2812b_loop = async {
loop {
// 获取速率
let speed = speed.get();
// 获取时间
let tick = get_tick() as f64 / 1000.0 * speed;
// 计算亮度
let brightness = (sin(tick) * 128.0 + 128.0) as u8;

// 要显示的 LED 颜色
let leds = [
RGB { r: 255, g: 0, b: 0 },
RGB { r: 0, g: 255, b: 0 },
RGB { r: 0, g: 0, b: 255 },
];

// 发送 Reset
ws2812b.reset().await;
// 发送 LED
for led in leds {
let rgb = led.with_brightness(brightness);
ws2812b.write_rgb(rgb);
}
}
};

其中 WS2812B 的 Reset 只需要 80us 时间的低电平,但是这里实际延时了 1ms,主要是顺便做任务切换:

    pub async fn reset(&mut self) {
self.pin.set_low().unwrap();
delay_ms(1).await
}

OLED 任务

OLED 任务和 WS2812B 任务类似:

  1. 获取当前速率
  2. 清空缓冲
  3. 画图
  4. 刷写缓冲
  5. 延时
        loop {
// 获取速率
let speed = speed.get() as u32;
// 清空缓冲
display.clear_buffer();
// 画一个长方形
Rectangle::new(Point::new(0, 0), Size::new(4 + 12 * speed, 16))
.draw_styled(&filled, &mut display)
.unwrap();
// 写数字
let speed_text = get_speed_text(speed);
Text::with_alignment(speed_text, Point::new(64, 32), text_style, Alignment::Right)
.draw(&mut display)
.unwrap();
// 刷写缓冲
if let Err(e) = display.flush() {
rprintln!("flush error: {:?}", e);
}
// 延时
delay::delay_ms(50).await;
}

Touch 任务

Touch 任务最为复杂,它的顶层逻辑是在循环里:

  1. 检测左滑
  2. 如果检测到左滑,就调用 Speed 模块降低速率
  3. 检测右滑
  4. 如果检查到右滑,就调用 Speed 模块加快速率
    let mut touchpad_loop = async {
loop {
if touchpad.is_left_swipe().await { // 左滑
speed.decrease(); // 降低速率
}
if touchpad.is_right_swipe().await { // 右滑
speed.increase(); // 加快速率
}
}
};

其中的左滑和右滑检测是这样的:

右滑检测是需要依此检查 1 2 3 4 号引脚,例如检查 1 号引脚:

  1. 读 1 号引脚
  2. 如果 1 号引脚是高电平,说明手触摸了 1 号输入,循环读取 1 号电平,直到它变成低电平
    pub async fn is_right_swipe(&mut self) -> bool {
let mut swipe = false;

// pin 1
if self.pin1.get().await { // 高电平
swipe = true;
while self.pin1.get().await {} // 循环直到低电平
}

if !swipe {
return swipe;
}

// pin 2
swipe = false;
if self.pin2.get().await { // 高电平
swipe = true;
while self.pin2.get().await {} // 循环直到低电平
}

if !swipe {
return swipe;
}
// pin 3
...
// pin 4
...
swipe
}

这里面的每次读引脚都是做了去抖的,每次去抖都是:读引脚,延时10ms,再读一次引脚。而且在检查到高电平的时候会进入循环,直到用户把手指移开。如果用户一直把手放在上面,就会阻塞其他任务的执行。所以这里不能使用循环来实现延时。

状态机

通常延时的实现方法:

  1. 循环:但是这样会阻塞,浪费了 CPU 时间而且不能执行其他任务
  2. 手写状态机:通过状态机实现延时,但是实现会很复杂,像上面的滑动检测需要非常多的状态,很难设计

有没有办法轻松愉快地写状态机呢?有的,就是让编译器自动生成状态机,在 Rust 里可以通过 async/await 实现。

首先实现延时:

struct Delay {
delay: u32,
prev_tick: u32,
}

impl Future for Delay {
type Output = ();

fn poll(
self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context<'_>,
) -> core::task::Poll<Self::Output> {
cx.waker().wake_by_ref();

let tick = get_tick(); // 获取当前时间
if tick - self.prev_tick >= self.delay { // 当前时间 - 开始时间 >= 延时时间
Poll::Ready(())
} else {
Poll::Pending
}
}
}

pub async fn delay_ms(n: u32) {
let delay = Delay {
delay: n,
prev_tick: get_tick(),
};

delay.await;
}

核心逻辑就是:获取当前时间,如果延时到了,就返回 Ready,如果没到就返回 Pending。可以把它想象成状态机里的一步状态转移,如果延时没到就返回 Pending,如果到了就返回 Ready。编译器会自动把它转换成一个状态机。

实现按键去抖动就可以像阻塞代码一样:

pub struct DebouncedPin<I: PinId> {
state: bool,
pin: Pin<I, Input<PullDown>>,
}

impl<I: PinId> DebouncedPin<I> {
pub fn new(pin: Pin<I, Input<PullDown>>) -> Self {
Self { state: false, pin }
}

pub async fn get(&mut self) -> bool {
let v1 = self.pin.is_high().unwrap(); // 读一次引脚
delay_ms(10).await; // 延时 10ms
let v2 = self.pin.is_high().unwrap(); // 再读一次引脚

if v1 == v2 && self.state != v1 {
self.state = v1;
}

self.state
}
}

核心逻辑都是:读一次引脚,延时 10ms,再读一次引脚,但是编译器会把他编译成一个状态机,而且里面会嵌套一个 delay 的状态机器。

回到最初的三个任务,其实编译器可以把它们转换成 3 个状态机:


图里每个圆圈表示一个状态机,状态机里可以不断嵌套状态机。要运行这些状态机,只要:

  1. 从列表里获取一个状态机
  2. 跳转到它当前的状态并执行代码
    1. 如果返回 Ready 就迁移到下一个状态继续执行,直到 Pending
    2. 如果返回 Pending 就回到第一步,获取下一个状态机去执行

这样就实现了在阻塞的时候,自动切换到一一个任务里。

相比RTOS,这样好处有:

  • 不需要动态内存分配,编译器在编译的时候就计算出状态机的大小,可以把状态机直接放在栈上
  • 共享函数调用栈,不需要为每一个任务单独分配栈空间
  • 更快的任务切换,因为不需要为每个任务保存和恢复寄存器

当然它也有缺点,就是只能做协助式调度,不能抢占。如果一个任务阻塞了,其他任务就没办法被执行,所以写代码的时候需要特别注意,长时间的任务要时不时主动切换一下。

总结

在这次活动中,第一次尝试了非阻塞式的代码,第一次用 WS2812B 和 OLED,体验非常好。SAME51J20A Curiosity Nano 板载资源非常丰富,完全不担心 Flash 和 SRAM 会不够用。最后感谢硬禾、得捷、MicroChip,期待以后可以做更有趣的项目。




软硬件
元器件
WS2812B
WS2812B-2020智能外控集成LED光源
SSD1306
单芯片 CMOS OLED/PLED 驱动器
附件下载
funpack-4-2.tar
代码
团队介绍
Deadline Fighter
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号