基于RP2350B制作体感小游戏
该项目使用了兼容小脚丫FPGA板上功能及扩展生态的RP2350B核心板,实现了体感小游戏的设计,它的主要功能为:根据加速计判断开发板姿态,通过 ADC 获取按键输入,然后通过 USB 虚拟串口发送数据到 PC 端,实现游戏操作和切换。
标签
嵌入式系统
Rust
加速度计
RP2350B
USB串口
学嵌入式的Momo
更新2025-07-10
19

项目介绍

这次项目使用兼容小脚丫FPGA板上功能及扩展生态的RP2350B核心板,实现体感游戏控制。用户可以通过调整开发板的姿态实现游戏的操作控制,也可以通过按键切换游戏。开发板通过板载 USB 实现了虚拟串口,和直接 PC 连接,不需要其他外设。

实物演示

硬件介绍

开发板使用 STEP RP2350B 核心板,板载 RP2350B,有 150MHz 双核 Cortex-M33 和 RISC-V。出了常见的外设,RP2350 还支持 PIO,可以自定义协议并且独立于主处理器运行。板上还有丰富的外设,这个项目就用到其中的 USB、加速度计、按键

项目设计

项目可以分成两个部分:

  1. 开发板:实现控制输入逻辑
    1. 用 ADC 读取按键的状态
    2. 用加速度计获取开发板的姿态
    3. 通过 USB 虚拟串口把按键、姿态发到 PC 端
  2. PC:实现游戏逻辑
    1. 从 USB 虚拟串口读取开发板的姿态和按键作为游戏的输入控制
    2. 实现两个游戏,根据输入和当前状态更新和渲染

开发板

开发板部分使用大循环模式,在初始化之后进入循环,不断获取加速度和按键输入并通过 USB 串口发送。

按键

开发板上有 4 个按键和4个开关,它们各自控制着一个电阻实现电阻分压,只要通过 ADC 获取电压,就能知道哪一个按键被按下了。

这里使用了 rp235x-hal 库,但是只支持 RP2350A,不支持 RP2350B。RP2350B 比 RP2350A 多了几个 ADC,而且对应的 GPIO 引脚不同:

在上游添加支持之前,只能先自己添加 ADC 7 的支持:https://github.com/embedded-momo/rp-hal/commit/6a6916afa5aa0a75ba6fa8fbe8539d7013d0aa13 。这样就能正确初始化 ADC

    // Use ADC for button input
let mut adc = hal::Adc::new(pac.ADC, &mut pac.RESETS);
let mut adc_pin = hal::adc::AdcPin::new(pins.gpio47).unwrap();

然后读取不同按键按下时的数值就可以识别按键:

        // read adc to detech button input
let pin_adc: u16 = adc.read(&mut adc_pin).unwrap();
let btn: u8 = match pin_adc {
300..350 => 1,
500..550 => 2,
600..650 => 3,
650..700 => 3,
_ => 0,
};

加速度计

加速度计使用 IIC 接口,首先进行初始化:

    let sda_pin: Pin<_, FunctionI2C, _> = pins.gpio22.reconfigure();
let scl_pin: Pin<_, FunctionI2C, _> = pins.gpio23.reconfigure();

let i2c = hal::I2C::i2c1(
pac.I2C1,
sda_pin,
scl_pin,
400.kHz(),
&mut pac.RESETS,
&clocks.system_clock,
);

let mut kxtj3 = Kxtj3::new(i2c, SlaveAddr::Default).unwrap();

这里使用了开源的 Kxtj3 库,可以直接从 IIC 读取加速度:

let raw_accel = kxtj3.accel_raw().unwrap();

有了加速度就可以判断开发板的姿态了,这里使用了一个简单的方法,先设置阈值过来掉噪声,然后检查每一个轴:

如果 X 轴加速度大于零,说明开发板往左倾斜了,如果小于零,说明往右倾斜了。同理,如果Y轴加速度小于0,说明 SWD 接口一边比USB接口一边高,开发板往上翘了。有了开发板的姿态,就可以控制游戏了。

USB 虚拟串口

RP2350 支持 USB 接口,可以实现虚拟串口和 PC 直接通信。首先初始化虚拟串口:

    let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new(
pac.USB,
pac.USB_DPRAM,
clocks.usb_clock,
true,
&mut pac.RESETS,
));

let mut serial = SerialPort::new(&usb_bus);

let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x1234, 0x5678))
.strings(&[StringDescriptors::default()
.manufacturer("Momo")
.product("Serial port")
.serial_number("TEST")])
.unwrap()
.device_class(2) // CDC
.device_sub_class(2) // ACM
.build();

为了把按键和开发板姿态发给 PC,设计一个简单的文本协议:

  • O: <Orientation>
  • G: <btn>

O: 开头表示开发板姿态,G: 开头表示某个按键被按下了。例如用户按下1,然后左转就会发送:

G: 1
O: LandscapeUp

消息内容可以直接通过 USB 发送

// format orientation msg and sent
let mut text: String<64> = String::new();
writeln!(&mut text, "O: {:?}", orientation).unwrap();
let _ = serial.write(text.as_bytes());

// format btn sg and sent
let mut text: String<64> = String::new();
writeln!(&mut text, "G: {:}", btn).unwrap();
let _ = serial.write(text.as_bytes());

PC 端

PC 端实现了两个小游戏,可以从 USB 串口读取开发板的姿态和按钮来进行游玩。整体可以分为两个线程:

  • 消息线程:从 USB 串口读取消息,解析之后发送到一个消息队列
  • 游戏循环:从消息队列读取消息,然后更新游戏状态和渲染


消息线程

消息线程会打开串口文件,读取每一行的内容,解析成游戏的操作再发送到消息队列:

match File::open(Path::new(&filename)) {
Ok(file) => {
let reader = BufReader::new(file);

for line in reader.lines() {
match line {
Ok(line) => {
if let Some(message) = Self::parse_message(&line) { // parse msg
if sender.send(message).is_err() { // write to queue
return;
}
}
}
Err(_) => {break;}
}
}
}
Err(e) => { break; }
}

游戏循环

全部游戏状态保存在一个 Game 结构体里:

struct Game {
breakout: BreakoutGame,
pong: PongGame,
current_game: GameType,
message_receiver: Receiver<Message>,
current_action: Action,
}

其中包含了打砖块和Pong游戏的状态:

struct BreakoutGame {
paddle: Paddle,
ball: Ball,
bricks: Vec<Brick>,
score: i32,
lives: i32,
state: GameState,
game_over_time: Option<Instant>,
}

struct PongGame {
left_paddle: PongPaddle,
right_paddle: PongPaddle,
ball: PongBall,
left_score: i32,
right_score: i32,
state: PongGameState,
winning_score: i32,
game_over_time: Option<Instant>,
}

游戏循环就是不断地从队列里读取消息,更新状态然后渲染:

#[macroquad::main("RP2350B-Game")]
async fn main() {
let serial_port = args().nth(1).unwrap();

let mut game = Game::new(serial_port);

loop {
let dt = get_frame_time();

game.handle_input();
game.update(dt);
game.draw();

next_frame().await;
}
}

以 Pong 游戏为例,每次循环都会执行下面的逻辑来更新状态:

  1. 判断当前游戏是否 Game over,如果是的话,更新游戏状态,然后在 5 秒之后重新开始
  2. 当前输入是不是 Up,如果是的话,更新板子位置往上一点
  3. 当前输入是不是 Down,如果是的话,更新板子位置往下一点
  4. 更新球的位置
  5. 碰撞检测,检查球和板是否碰撞
  6. 如果球和板子碰撞了就要更新球的速度方向
  7. 如果球和边缘碰撞了,就更新速度方向和分数
  8. 如果一方分数等于5,更新成 game over 状态

总结

本次项目实现了根据开发板姿态控制游戏,仍有不足:

  1. 按键切换游戏的时候,板子姿态也会改变,切换的时候容易手忙脚乱
  2. 如果通过加速度计算出位移,把位移直接映射到游戏里板子的位置,操作更加符合直觉

开发中也遇到一些问题:

  1. 文档少,除了原理图和介绍外,就没其他文档了,只能看z
  2. 开源库不完善,因为树莓派没有官方的 rust 支持,rp-hal 是社区主导的项目,还不完善


附件下载
rp2350b-game.tar
代码
团队介绍
Deadline Fighter
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号