一、项目介绍
本项目基于rp2350B核心板平台,基于板载的IMU进行姿态解算,实现体感鼠标的功能。并实现以下 功能:
- 进行IMU姿态解算并通过USB HID输出鼠标移动和点击
- 3个拨码开关进行灵敏度调整
- 4个按键分别映射左右键,中键以及让鼠标指针回到左上角,数码管显示当前灵敏度档位
- 空闲自动进入低功耗模式,摇动唤醒
二、硬件介绍
STEP_RP2350B核心板基于Raspberry Pi RP2350B芯片设计,充分利用其双核Arm Cortex-M33 + 双核RISC-V处理器(最高150MHz)、丰富PIO可编程IO以及48个GPIO的优势,兼容“小脚丫”FPGA核心板的功能与扩展生态,适合电赛、技能训练和嵌入式学习。该核心板上搭载了多种外设:
- 8颗单色LED,用于各种LED相关的编程练习,比如心跳灯、流水灯、呼吸灯灯
- 2个三色LED,用于使用RGB三种颜色的编程练习
- 2个7段数码管,用于计数、显示数字信息,可以显示0-99之间的数字
- 4个拨码开关,用于设置、切换一些状态,其状态是使用模拟信号的方式送到RP2350B的ADC进行判断
- 4个轻触按键,用于控制信息的输入,其状态是使用模拟信号的方式送到RP2350B的ADC进行判断
- 一颗单色LED
- 一个姿态传感器(KXTJ3-1057)


三、程序框图与项目设计思路
1、设计思路
项目开发的核心思路是:先逐一编写各个基础外设的独立驱动程序(包括加速度计 I2C 接口、数码管移位寄存器、WS2812 RGB、ADC 按键/拨码开关、GPIO、USB HID 等),每个驱动都包含初始化、读写控制、错误处理,并经过单元测试确保可靠;待所有底层模块稳定后,再逐步将它们整合到主程序框架中,依次实现姿态解算、HID 输出、灵敏度调节、数码管显示,以及低功耗休眠/唤醒等功能,最终完成完整、稳定、高响应的体感鼠标固件系统。
- 按键与拨码开关
这块核心板上是使用一个adc去读取4个按键加上4个拨码开关的方式,rp2350的上搭载的是12位的adc,理论分析常态下应该是在3.3v左右,也就是adc读取值会接近4096。但是经过实测,常态下adc测量出来的值大概在3704,且在30次读值取平均后仍有不小的抖动,因此当sw1闭合时,理论值会减少3704/256=14,且实际上会更低,这就很难和抖动区分开,于是这里只能是放弃掉sw4。其余的经过实测后大致是以下数值,并就可根据数值编写对应代码:
状态 | 参考值 | KEY1 值 | KEY2 值 | KEY3 值 | KEY4 值 |
|---|---|---|---|---|---|
所有按键/开关断开 | 3704 | 1895 | 2810 | 3250 | 3485 |
sw2 按下 | 3583 | 1785 | 2698 | 3144 | 3368 |
sw3 按下 | 3644 | 1840 | 2750 | 3195 | 3423 |
sw4 按下 | 3670 | 1875 | 2780 | 3232 | 3458 |

- 数码管
数码管比较简单,由2个74HC595来控制2个8为数码管,网上参考代码很多

- 加速度计
这块核心板上面搭载的是KXTJ3-1057,这里参考开源的驱动:GitHub - ldab/KXTJ3-1057: KXTJ3-1057 Tri-axis Digital Accelerometer - Arduino Library,并移植了c版本(当然是阉割过后的)。然后通过阅读数据手册和原理图就能知道这块imu的i2c地址是0X0E,这样就能通信并读取数值了。


- TinyUSB
这部分参考的是派pico官方的例程:pico-examples\usb\device\dev_hid_composite,这个例程演示了一个复合hid设备,我这里直接剪切其中的鼠标部分。
- 低功耗
经查阅手册,KXTJ3-1057这块加速度计是支持晃动唤醒并产生中断信号的,但是从原理图来看,INT引脚是没有接到gpio的。于是乎这里的低功耗加唤醒采用的方案是,平常保持每5ms检测一次移动并向电脑发送hid报文,当连续5s一直保持位移均为0时,将检测频率降低到1s一次,并且将主时钟频率从150MHz降低到48MHz,并关闭掉其他外设,当检测到有位移时再调整回来

四 、代码介绍
本次项目基于ico-sdk的开发,并搭建了wsl+vscode环境。
1.程序框图

2.核心任务代码
#define LEISURE_THRESHOLD_MS 5000
uint16_t leisure_time = 0;
uint8_t leisure_mode = 0;
uint32_t interval_ms = 5;
void app_task(void)
{
uint8_t key_mode, key, last_key = 0;
int8_t hid_x, hid_y;
static uint8_t last_leisure_mode = -1;
// 默认10ms更新一次
static uint32_t start_ms = 0;
if (board_millis() - start_ms < interval_ms) return;
get_key(&key_mode, &key);
kxtj3_get_hid_displacement(&hid_x, &hid_y);
// 如果一段时间内没有输入,就进入休闲模式
if (hid_x == 0 && hid_y == 0 && key == 0)
{
leisure_time += interval_ms;
if (leisure_time > LEISURE_THRESHOLD_MS)
{
leisure_mode = 1;
}
}
else
{
leisure_time = 0;
leisure_mode = 0;
if (key < 4)
hid_task(hid_x, hid_y, 0x01 << (key - 1 )); // 根据按键状态设置鼠标按钮
}
if (leisure_mode == 1 && last_leisure_mode == 0)
{
interval_ms = 1000;
_seg_scan_clear();
set_sys_clock_hz(48 * MHZ, false);
}
else if (leisure_mode == 0 && last_leisure_mode == 1)
{
interval_ms = 10;
set_sys_clock_hz(150 * MHZ, false);
}
if (leisure_mode == 0)
{
if (key_mode == 0)
TILT_GAIN_X = TILT_GAIN_Y = 50.0f;
else if (key_mode == 1)
TILT_GAIN_X = TILT_GAIN_Y = 100.0f;
else if (key_mode == 2)
TILT_GAIN_X = TILT_GAIN_Y = 200.0f;
else if (key_mode == 3)
TILT_GAIN_X = TILT_GAIN_Y = 300.0f;
_seg_scan_clear();
_seg_display(key_mode, 0, false);
if (key == 4 && last_key != 4) {
return_center();
}
}
last_leisure_mode = leisure_mode;
start_ms += interval_ms;
last_key = key;
}
三、功能演示
鼠标功能图片展示不出来,这里就展示一下低功耗的模式,这里插一个电流计判断,常态下电流基本稳定到44mA,当保持5s不动后电流降低至16mA。


4、遇到的难题与不足
在本次项目中遇到了不少难题
1、ws2812一直无法点亮
使用的是官方的ws2812示例程序:pico-examples\pio\ws2812,一直检查不出原因,但是目前手上没有逻辑分析仪可以查看波形是否正常,因此这次只能遗憾放弃。等之后再仔细研究一下。
后续:经过仔细排查后,在使用set(PICO_BOARD pico2 CACHE STRING "Board type")时,需要将pico-sdk\src\boards\include\boards\pico2.h中PICO_RP2350A设置为0才是使用的RP2350B,原先因为使用的是RP2350A导致pio无法使用30号以后的引脚,修改后成功点亮
#define PICO_RP2350A 0
2、按键
之前也有想过用一个gpio来读取多个按键的这种方案,但一直没有尝试过,一开始也是被卡了挺久,后续也是无奈阉割掉一个开关来解决问题。不过感觉这种方案应该可以通过调整电阻来变得更加合理后续再试试;
3、数码管
一开始编写驱动的时候一直点不亮数码管,检查多次后并没有发现错误。后续猜猜会不会是频率太高,因此在发送每一位时都加上1us延时,成功点亮。
uint8_t seg_bits[] = {0xFC, 0x60, 0xDA, 0xF2, 0x66, 0xB6, 0xBE, 0xE0, 0xFE, 0xE6, 0x01, 0x00};
void _seg_display(uint8_t index, uint8_t id, bool point)
{
uint8_t seg = id ? 0x02 : 0X01;
uint8_t dig = point? seg_bits[index] | seg_bits[10]: seg_bits[index];
for (int i = 0; i < 2; i++)
{
SER(seg & 0x01);
seg >>= 1;
SCK(1); SCK(0);
sleep_us(1);
}
for (int i = 0; i < 8; i++)
{
SER(dig & 0x01);
dig >>= 1;
SCK(1); SCK(0);
sleep_us(1);
}
RCK(1); RCK(0);
sleep_us(REFRESH_US);
}
4、心得体会
本次实现的还算是比较简单的一个综合性项目,也是有不小的收获。也感谢电子森林的本次活动,让我有动力去做更多有趣的东西。