项目介绍
这次项目使用兼容小脚丫FPGA板上功能及扩展生态的RP2350B核心板,实现体感游戏控制。用户可以通过调整开发板的姿态实现游戏的操作控制,也可以通过按键切换游戏。开发板通过板载 USB 实现了虚拟串口,和直接 PC 连接,不需要其他外设。
实物演示
硬件介绍
开发板使用 STEP RP2350B 核心板,板载 RP2350B,有 150MHz 双核 Cortex-M33 和 RISC-V。出了常见的外设,RP2350 还支持 PIO,可以自定义协议并且独立于主处理器运行。板上还有丰富的外设,这个项目就用到其中的 USB、加速度计、按键。
项目设计
项目可以分成两个部分:
- 开发板:实现控制输入逻辑
- 用 ADC 读取按键的状态
- 用加速度计获取开发板的姿态
- 通过 USB 虚拟串口把按键、姿态发到 PC 端
- PC:实现游戏逻辑
- 从 USB 虚拟串口读取开发板的姿态和按键作为游戏的输入控制
- 实现两个游戏,根据输入和当前状态更新和渲染
开发板
开发板部分使用大循环模式,在初始化之后进入循环,不断获取加速度和按键输入并通过 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 游戏为例,每次循环都会执行下面的逻辑来更新状态:
- 判断当前游戏是否 Game over,如果是的话,更新游戏状态,然后在 5 秒之后重新开始
- 当前输入是不是 Up,如果是的话,更新板子位置往上一点
- 当前输入是不是 Down,如果是的话,更新板子位置往下一点
- 更新球的位置
- 碰撞检测,检查球和板是否碰撞
- 如果球和板子碰撞了就要更新球的速度方向
- 如果球和边缘碰撞了,就更新速度方向和分数
- 如果一方分数等于5,更新成 game over 状态
总结
本次项目实现了根据开发板姿态控制游戏,仍有不足:
- 按键切换游戏的时候,板子姿态也会改变,切换的时候容易手忙脚乱
- 如果通过加速度计算出位移,把位移直接映射到游戏里板子的位置,操作更加符合直觉
开发中也遇到一些问题:
- 文档少,除了原理图和介绍外,就没其他文档了,只能看z
- 开源库不完善,因为树莓派没有官方的 rust 支持,rp-hal 是社区主导的项目,还不完善