2024年寒假练 - 基于带调试器的I.MX RT1021开发板实现USB虚拟鼠标
该项目使用了I.MX RT1021,实现了USB虚拟鼠标的设计,它的主要功能为:通过旋钮放大缩小,游戏手柄移动鼠标,旋钮按钮或电阻屏模拟鼠标左键。
标签
RT1021
USB虚拟鼠标
oxlm
更新2024-03-29
167

背景介绍

在做完英飞凌多媒体键盘功能后,发现rt1021的板卡外设挺有意思,能做不少好玩的东东,因此顺势下单买了这块板子,前期继续搞usb类任务,完成后把有意思的部分搞完。

项目功能介绍

利用核心板的USB接口与输入功能实现一个USB虚拟鼠标,具体实现方案为 1. 使用板卡上的触摸屏板卡上的触摸屏实现左键点击操作 2. 使用双轴电位器实现鼠标移动 3. 旋钮旋转控制缩放,点击模拟鼠标左键点击事件

设计思路

由于是练习目的,因此优先考虑使用现有demo魔改出效果,在这里,优先选择usb hid mouse类设备,因为这个demo已经把最难的usb设备这块给打通了。之后便是修改demo,把示例中不适合本项目需求的部分移除,增加应用层逻辑。再后面一个模块一个模块的添加,以实现所需功能。

硬件框图和软件流程图

简单的硬件介绍

触摸框部分

0


这个图比较直观,电阻触摸的原理,本质上是按压位置处上下层导通,因此:1. 只需对X+ X- 加压,读取 Y+或Y-的值,便可换算出X轴位置2. 只需对Y+ Y-加压,读取X+ 或X-的值,便可换算出Y轴的位置而1021板卡中,选择的芯片XPT2046所实现的便是这么个功能

旋钮部分


旋钮的原理其实挺简单的,判断旋钮旋转有两个管脚,在旋钮未旋转时(正常位),这两个管脚会保持高电平,而在旋转过程中,正旋和反旋时,两个脚电平会先后掉下,之后再先后升高,因此仅需判断谁先掉下,或者谁先升高,便可判断旋转方向。

双轴电位器部分

双轴电位器的硬件原理,和滑动变阻器很类似,区别是滑动变阻器是一维的,而双轴电位器是二维的。因此,仅需要采集电位器两个输出端的电压便可换算出位置。而1021板卡中,采用了一个有意思的电路:

乍一看,觉得太复杂,感觉直接测量FJ08K-N的第2,5脚的电平就行了,加一颗LMV324感觉有些多此一举。而在仿真后,发现这个电路有些奇妙,其实U5A是多余的,真正有用的是U5B的第7脚,这个脚的波形,占空比对应的是Y轴的位置信息,PWM基础频率对应的是X轴的位置。这样,两路ADC采样变成了一路PWM波形采集,MCU硬件资源占用立马减少一个管脚。

主要代码片段

核心代码

typedef enum _msg_type {
ROTARY_RIGHT = 0x00,
ROTARY_LEFT = 0x01,
JOYSTICK_EVENT = 0x02,
MOUSE_EVENT = 0x03,
} msg_type;

typedef struct _msg_buffer {
msg_type type;
int8_t data[4];
} msg_buffer;

#define MSG_BUFFER_SIZE 50
msg_buffer msg_buff[MSG_BUFFER_SIZE];
lwrb_t msg;

void APP_task(void *handle)
{
BaseType_t ret;
msg_buffer ret_data;
uint8_t isMousePressed = pdFALSE;

USB_DeviceApplicationInit();
#if USB_DEVICE_CONFIG_USE_TASK
if (g_UsbDeviceComposite.deviceHandle) {
if (xTaskCreate(USB_DeviceTask, /* pointer to the task */
"usb device task", /* task name for kernel awareness debugging */
5000L / sizeof(portSTACK_TYPE), /* task stack size */
g_UsbDeviceComposite.deviceHandle, /* optional task startup argument */
5U, /* initial priority */
&g_UsbDeviceComposite.deviceTaskHandle /* optional task handle to create */
) != pdPASS) {
usb_echo("usb device task create failed!\r\n");
return;
}
}
#endif

while (1U) {
if (lwrb_get_linear_block_read_length(&msg) > 0) {
lwrb_read(&msg, &ret_data, sizeof(msg_buffer));
// usb_echo("ret_data %x\r\n", ret_data.type);
switch (ret_data.type) {
case ROTARY_LEFT:
send_key_scale_up();
vTaskDelay(10);
send_key_null();
break;

case ROTARY_RIGHT:
send_key_scale_down();
vTaskDelay(10);
send_key_null();
break;

case JOYSTICK_EVENT:
// usb_echo("px: %d py: %d\r\n", ret_data.data[0], ret_data.data[1]);
send_mouse_event(ret_data.data[0], ret_data.data[1], isMousePressed);
break;

case MOUSE_EVENT:
// usb_echo("Mouse Key: %d\r\n", ret_data.data[0]);
isMousePressed = ret_data.data[0];
send_mouse_event(0, 0, isMousePressed);
break;

default:
break;
}
} else {
vTaskDelay(20);
}
}
}

旋钮

void GPIO2_GPIO_COMB_0_15_IRQHANDLER(void) {
BaseType_t pxHigherPriorityTaskWoken = pdFALSE;
msg_buffer send_data;
static uint8_t firstKeyDetect;

if (GPIO_GetPinsInterruptFlags(ENCODE_PORT) & (1U << ENCODE_A_PIN)) {
/* clear the interrupt status */
GPIO_PortClearInterruptFlags(ENCODE_PORT, 1U << ENCODE_A_PIN);
/* Change state of switch. */

if (GPIO_PinRead(ENCODE_PORT, ENCODE_A_PIN)) { // Rising edge
if (GPIO_PinRead(ENCODE_PORT, ENCODE_B_PIN) && (firstKeyDetect == ROTARY_LEFT)) {
send_data.type = ROTARY_LEFT;
lwrb_write(&msg, &send_data, sizeof(msg_buffer));
} else if (!GPIO_PinRead(ENCODE_PORT, ENCODE_B_PIN) && (firstKeyDetect == ROTARY_RIGHT)) {
send_data.type = ROTARY_RIGHT;
lwrb_write(&msg, &send_data, sizeof(msg_buffer));
}
} else { // Falling edge
if (GPIO_PinRead(ENCODE_PORT, ENCODE_B_PIN)) {
firstKeyDetect = ROTARY_RIGHT;
} else {
firstKeyDetect = ROTARY_LEFT;
}
}

} else if (GPIO_GetPinsInterruptFlags(ENCODE_PORT) & (1U << ENCODE_D_PIN)) {
/* clear the interrupt status */
GPIO_PortClearInterruptFlags(ENCODE_PORT, 1U << ENCODE_D_PIN);
/* Change state of switch. */

send_data.type = MOUSE_EVENT;
send_data.data[0] = (GPIO_PinRead(ENCODE_PORT, ENCODE_D_PIN) == 0) ? 1 : 0;
lwrb_write(&msg, &send_data, sizeof(msg_buffer));
}

SDK_ISR_EXIT_BARRIER;
}

双轴电位器

#define JOYSTICK_PORT GPIO2
#define JOYSTICK_PIN 28U

#define DUTY_CYCLE_FULL_TIME 1000000

TaskHandle_t joystickTaskHandle;
void Joystick_Task(void *handle) {
int32_t StartCount, HighLevelCount = 0, LowLevelCount = 0;
msg_buffer send_data;
int8_t px, py;

while (1) {
HighLevelCount = 0;
LowLevelCount = 0;
StartCount = 0;

vTaskDelay(10);

// usb_echo("tick_start %d\r\n", xTaskGetTickCount());
if (GPIO_PinRead(JOYSTICK_PORT, JOYSTICK_PIN)) {
// Get start signal
while (GPIO_PinRead(JOYSTICK_PORT, JOYSTICK_PIN)) {
if (StartCount++ > DUTY_CYCLE_FULL_TIME) {
break;
}
}
if (StartCount > DUTY_CYCLE_FULL_TIME) {
continue;
}

while (!GPIO_PinRead(JOYSTICK_PORT, JOYSTICK_PIN)) {
LowLevelCount++;
// Delay_us(1);
}

while (GPIO_PinRead(JOYSTICK_PORT, JOYSTICK_PIN)) {
HighLevelCount++;
// Delay_us(1);
}

} else {
// Get start signal
while (!GPIO_PinRead(JOYSTICK_PORT, JOYSTICK_PIN)) {
if (StartCount++ > DUTY_CYCLE_FULL_TIME) {
break;
}
}
if (StartCount > DUTY_CYCLE_FULL_TIME) {
continue;
}

while (GPIO_PinRead(JOYSTICK_PORT, JOYSTICK_PIN)) {
HighLevelCount++;
// Delay_us(1);
}

while (!GPIO_PinRead(JOYSTICK_PORT, JOYSTICK_PIN)) {
LowLevelCount++;
// Delay_us(1);
}
}
// usb_echo("tick_end %d\r\n", xTaskGetTickCount());

// usb_echo("HighLevelCount: %d LowLevelCount: %d StartCount: %d\r\n", HighLevelCount, LowLevelCount, StartCount);
px = ((HighLevelCount * 100) / (HighLevelCount + LowLevelCount) - 49) / 6; // set to -5~5
py = ((((HighLevelCount + LowLevelCount) * 100) / 40000) - 60) / 3; // set to -5~5;

// StartCount = usb_echo("StartCount: %d\r\n", StartCount);
if ((px != 0) || (py != 0)) {
// usb_echo("px: %d py: %d\r\n", px, py);
send_data.type = JOYSTICK_EVENT;
send_data.data[0] = px;
send_data.data[1] = py;
lwrb_write(&msg, &send_data, sizeof(msg_buffer));
}
}
return;
}

电阻触摸

#define XPT2046_CMD_S 0x80

#define XPT2046_CMD_MODE_12_BIT 0x80
#define XPT2046_CMD_MODE_8_BIT 0x00

#define XPT2046_CMD_TEST_Y 0x10
#define XPT2046_CMD_TEST_X 0x50

#define XPT2046_CMD_SER 0x04
#define XPT2046_CMD_DFR 0x00

#define XPT2046_CMD_ADC_EN 0x02
#define XPT2046_CMD_ADC_DISABLE 0x00

#define XPT2046_PENIRQ_EN 0x01
#define XPT2046_PENIRQ_DISABLE 0x00

#define XPT2046_CS_PORT BOARD_INITPINS_TOUCH_CS_GPIO
#define XPT2046_CS_PIN BOARD_INITPINS_TOUCH_CS_GPIO_PIN

int16_t getTouchPos(int8_t direction) {
lpspi_transfer_t masterXfer;
uint8_t masterRxData[2] = {0U};
uint8_t masterTxData[1] = {0U};
int16_t touchValue;

GPIO_PinWrite(XPT2046_CS_PORT, XPT2046_CS_PIN, 0U);

if (direction) {
masterTxData[0] = XPT2046_CMD_S | XPT2046_CMD_MODE_12_BIT | XPT2046_CMD_TEST_Y | XPT2046_CMD_DFR |
XPT2046_CMD_ADC_EN | XPT2046_PENIRQ_DISABLE;
} else {
masterTxData[0] = XPT2046_CMD_S | XPT2046_CMD_MODE_12_BIT | XPT2046_CMD_TEST_X | XPT2046_CMD_DFR |
XPT2046_CMD_ADC_EN | XPT2046_PENIRQ_DISABLE;
}
masterXfer.txData = masterTxData;
masterXfer.rxData = NULL;
masterXfer.dataSize = 1;
masterXfer.configFlags = kLPSPI_MasterPcsContinuous | kLPSPI_MasterByteSwap;
LPSPI_MasterTransferBlocking(LPSPI4_PERIPHERAL, &masterXfer);

vTaskDelay(10);
masterXfer.txData = NULL;
masterXfer.rxData = masterRxData;
masterXfer.dataSize = 2;
masterXfer.configFlags = kLPSPI_MasterPcsContinuous | kLPSPI_MasterByteSwap;
LPSPI_MasterTransferBlocking(LPSPI4_PERIPHERAL, &masterXfer);
vTaskDelay(1);

GPIO_PinWrite(XPT2046_CS_PORT, XPT2046_CS_PIN, 1U);

touchValue = (int16_t)masterRxData[0] * 256 + masterRxData[1];
return touchValue;
}

int8_t getTouchStatus(void) {
int8_t isTouchPressed = pdFALSE;
int16_t touchX, touchY;

touchX = getTouchPos(0);
touchY = getTouchPos(1);

// usb_echo("read data %x %x\r\n", touchX, touchY);

if ((touchX > 0x0100 && touchX < 0x7C00) || (touchY > 0x0100 && touchY < 0x7C00)) {
isTouchPressed = pdTRUE;
}
return isTouchPressed;
}

TaskHandle_t touchTaskHandle;
void Touch_Task(void *handle) {
int8_t isTouchPressed = pdFALSE;
msg_buffer send_data;

while (1) {
if (getTouchStatus() != isTouchPressed) {
isTouchPressed = !isTouchPressed;

send_data.type = MOUSE_EVENT;
send_data.data[0] = isTouchPressed;
lwrb_write(&msg, &send_data, sizeof(msg_buffer));
}
vTaskDelay(100);
}
return;
}

整体流程图

图一

图二


图三

遇到的主要难题及解决方法

中断中无法发送quene

这个问题估计是rt1021的freertos的适配问题,因为涉及到底层的修改,改动之后影响面也暂时未知。因此这部分采用循环队列规避,我所使用的循环队列为开源的lwRB。为啥使用循环队列,恰巧是因为循环队列刚好实现了中断修改的变量和线程修改的变量不是同时修改的。由于项目复杂度不高,因此循环队列未作发送端的加锁功能,可能存在消息丢失的情况。

旋钮丢波形

硬件本身问题,软件能做的只能是过滤掉异常波形

各种更换烧录器都无法烧录

最后发现是1021小板上的座子CLK脚虚焊,时灵时不灵的,补焊后问题消失

PWM无法通过中断采集

  • 采用TMR实现: 发现此定时器只能做波形个数统计
  • 采用GPT实现: 发现GPT无法判断是上升沿触发还是下降沿触发
  • 采用eflexpwm实现: 貌似是最佳实现方式,但是这方面的资料,除了RM没有更多的了,未成功调试出
  • 最终采用方案: 直接开启一个线程,轮询GPIO状态并计时,确保收到一个完整波形后,计算频率和占空比

未来的计划或建议

  • 电阻触摸框改为usb触摸板(只实现鼠标移动功能,点击功能不实现,原因为由于点击力度不确定,个人认为电阻屏实现点击功能容易导致屏损坏)
  • 继续实现eflexpwm方式的pwm采集
  • 打通显示,六轴传感器和地磁传感器,玩手柄类算法

 参考资料

Changelog — LwRB documentation (majerle.eu) --- 开源循环队列

IMXRT1020RM.pdf

https://www.eetree.cn/activity/15#platform ---- 任务链接

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