2024年寒假练 - 基于RT1021的USB虚拟鼠标
该项目使用了RT1021,实现了USB虚拟鼠标的设计,它的主要功能为:触摸屏可以实现触摸板的效果,双轴电位器可以模拟出鼠标移动,旋钮可以控制缩放,电脑音量或者控制电脑屏幕亮度等参数。
标签
嵌入式系统
yekai
更新2024-03-29
148

平台介绍

本次使用的开发板为基于恩智浦iMX RT1021处理器的核心板和扩展版。RT1021是一颗性能强劲同时又极具性价比的处理器,基于Cortex-M7内核,可运行在500MHz频率,同时包含256K的SRAM,无内置Flash设计,程序存储在外挂的W25Q64上,可用空间高达8M。扩展板包含屏幕、摇杆等一系列外设,便于开发。

任务需求

本次我选择的任务是实现一个USB虚拟鼠标,主要实现了以下内容。

  • USB HID上报
  • 触摸屏的读取
  • 摇杆、编码器的使用

设计思路

触摸板、摇杆、编码器分别运行在各自线程中采集,有上报请求后通过消息队列向USB线程发送请求。

USB HID在单独的线程中运行,接收其他线程发送的上报请求。

具体实现

触摸屏的采集

触摸屏使用的是XPT2046,需要使用一个较低的SPI频率来读取。外设层我使用了RT-Thread的SPI设备模型。

    // 初始化
auto ret = rt_hw_spi_device_attach("spi4", "spi40", GET_PIN(3, 5));
   if (ret != RT_EOK) {
       log_e("spi device attach failed");
       return;
  }

   auto spi_dev = (struct rt_spi_device *) rt_device_find("spi40");
   if (spi_dev == RT_NULL) {
       log_e("spi device not found");
       return;
  }

   struct rt_spi_configuration cfg;
   cfg.data_width = 8;
   cfg.mode = RT_SPI_MASTER | RT_SPI_MODE_0 | RT_SPI_MSB;
   cfg.max_hz = 1 * 1000 * 1000;                           /* 1M */

   rt_spi_configure(spi_dev, &cfg);
// 读取
rt_spi_send_then_recv(spi_dev, send_buf, 2, recv_buf, 2);

电阻屏的抖动还是蛮大的,时常总有一些偏差较大的意外的值,所以我们要进行一个滤波,取32个数据排序后仅取中间16个的平均

    uint8_t send_buf[2] = {0, 0};
   uint8_t recv_buf[2] = {0, 0};
   int32_t sample_x, sample_y, sample_x_sum, sample_y_sum;
   int32_t sample_x_buf[32], sample_y_buf[32];
   int32_t sample_x_cache{0}, sample_y_cache{0};
   uint8_t detect_log = 0;
   bool detected = false;
   bool isRelease = true;
   rt_tick_t press_time = 0;
   const auto click_threshold = 400;
   while (1) {
       detect_log <<= 1;
       detect_log |= detected;
       if (detect_log == 0) {
           if (!isRelease) {
               isRelease = true;
               log_d("release detected");
               if (rt_tick_get() - press_time < click_threshold) {
                   SendClick();
                   press_time = 0;
              }
          }
      } else if (detect_log & 0b111) {
           if (isRelease) {
               isRelease = false;
               log_d("press detected");
               press_time = rt_tick_get();
          }
      }
       // start to detect
       detected = false;
       for (int i = 0; i < 32; i++) {
           // x
           send_buf[0] = 0;
           send_buf[1] = 0xd0;
           recv_buf[0] = 0;
           recv_buf[1] = 0;
           rt_spi_send_then_recv(spi_dev, send_buf, 2, recv_buf, 2);
           sample_x = ((recv_buf[0] & 0x7F) << 8) | recv_buf[1];
           sample_x_buf[i] = sample_x;
           // y
           send_buf[0] = 0;
           send_buf[1] = 0x90;
           recv_buf[0] = 0;
           recv_buf[1] = 0;
           rt_spi_send_then_recv(spi_dev, send_buf, 2, recv_buf, 2);
           sample_y = ((recv_buf[0] & 0x7F) << 8) | recv_buf[1];
           sample_y_buf[i] = sample_y;
//           log_d("x: %d, y: %d", sample_x, sample_y);
           rt_thread_mdelay(1);
      }
       // sort sample buf and get the median 16 samples average
       const auto cmp_func = [](const void *a, const void *b) {
           return int(*(int32_t *) a - *(int32_t *) b);
      };
       std::qsort(sample_x_buf, 32, sizeof(int32_t), cmp_func);
       std::qsort(sample_y_buf, 32, sizeof(int32_t), cmp_func);
       sample_x_sum = 0;
       sample_y_sum = 0;
       for (int i = 8; i < 24; i++) {
           sample_x_sum += sample_x_buf[i];
           sample_y_sum += sample_y_buf[i];
      }
       sample_x = sample_x_sum / 16;
       sample_y = sample_y_sum / 16;
       if ((sample_x < 1000) || ((0x7FFF - 1000) < sample_x) ||
          (sample_y < 1000) || ((0x7FFF - 1000) < sample_y)) {
           continue;            // 边界外,丢弃
      }
       auto x_move = sample_x - sample_x_cache;
       auto y_move = sample_y - sample_y_cache;
       sample_x_cache = int32_t(sample_x);
       sample_y_cache = int32_t(sample_y);
       const auto move_max = 6000;
       if ((x_move < -move_max) || (x_move > move_max) || (y_move < -move_max) || (y_move > move_max)) {
           // 意外的大跳变,可能为抬起
//           log_i("release detected, x: %d, y: %d, dx: %d, dy: %d", sample_x, sample_y, x_move, y_move);
           continue;
      }
       // 抖动
       static const auto AvoidShake = [](int32_t &delta, int32_t threshold) {
           if (delta < threshold && delta > -threshold) {
               delta = 0;
          }
      };
       AvoidShake(x_move, 100);
       AvoidShake(y_move, 100);
       if (x_move == 0 && y_move == 0) {
           detected = true;
           continue;
      }
       detected = true;
       if ((rt_tick_get() - press_time > click_threshold) && (press_time != 0) && !isRelease) {
           SendMove(int8_t(x_move / 20), int8_t(-y_move / 20));
      }
//       log_i("x: %d, y: %d, dx: %d, dy: %d", sample_x, sample_y, x_move, y_move);
  }

编码器的采集

编码器使用ENC外设,直接拷贝了MCUXpresso Config Tools的初始化代码

    const auto ENC1_config = enc_config_t{
          .enableReverseDirection = false,
          .decoderWorkMode = kENC_DecoderWorkAsNormalMode,
          .HOMETriggerMode = kENC_HOMETriggerDisabled,
          .INDEXTriggerMode = kENC_INDEXTriggerDisabled,
          .enableTRIGGERClearPositionCounter = false,
          .enableTRIGGERClearHoldPositionCounter = false,
          .enableWatchdog = false,
          .watchdogTimeoutValue = 0,
           // .filterPrescaler = kENC_FilterPrescalerDiv1,
          .filterCount = 5,
          .filterSamplePeriod = 1,
          .positionMatchMode = kENC_POSMATCHOnPositionCounterEqualToComapreValue,
          .positionCompareValue = 4294967295,
          .revolutionCountCondition = kENC_RevolutionCountOnINDEXPulse,
          .enableModuloCountMode = false,
          .positionModulusValue = 0,
          .positionInitialValue = 0,
  };

   ENC_Init(ENC1, &ENC1_config);

实际测试经常有非4的倍数的波,所以应用层还是要滤一下

    static const auto SetEncCnt = [](int32_t cnt) {
       ENC_SetInitialPositionValue(ENC1, cnt);
       ENC_DoSoftwareLoadInitialPositionValue(ENC1);
  };    

   while (1) {
       auto enc_cnt = static_cast<int32_t>(ENC_GetPositionValue(ENC1)) - init_value;

       static const auto signal_func = [](uint32_t cnt) {
           if (cnt == 0) {
               return;
          }
           switch (enc_mode) {
               case ZOOM:
                   SendZoom(int8_t(-cnt));
                   break;
               case VOLUME:
                   SendVolume(int8_t(-cnt));
                   break;
               case BRIGHTNESS:
                   SendBrightness(int8_t(-cnt));
                   break;
               default:
                   break;
          }
//           log_i("cnt: %d", cnt);
      };

       if (enc_cnt != 0) {
           if (enc_cnt == enc_cnt_cache) {
               // 稳定了
               enc_cnt_cache = INT32_MAX;
               signal_func(enc_cnt / 4);
               SetEncCnt(enc_cnt % 4 + init_value);
          } else {
               enc_cnt_cache = enc_cnt;
          }
      }

       button_ticks();
       rt_thread_mdelay(10);
  }

由于编码器需要支持3种模式,所以使用编码器的按键来切换,我简单的使用MultiButton

// 由于multi-button使用C风格的函数指针不能接收带有捕获的C++匿名函数,所以得用全局变量来传递
using enc_mode_t = enum {
   ZOOM,
   VOLUME,
   BRIGHTNESS
};
volatile enc_mode_t enc_mode = ZOOM;

#define enc_btn_pin GET_PIN(2,5)
   rt_pin_mode(enc_btn_pin, PIN_MODE_INPUT_PULLUP);
   auto m_btn = new button;
   button_init(m_btn, []() -> uint8_t {
       return rt_pin_read(enc_btn_pin);
  }, 0);
   button_attach(m_btn, PRESS_DOWN, [](void *args) {
       enc_mode = static_cast<enc_mode_t>((enc_mode + 1) % 3);
       log_i("press down, encoder mode change to %s",
             enc_mode == ZOOM ? "zoom" : (enc_mode == VOLUME ? "volume" : "brightness"));
  });
   button_start(m_btn);

while (1) {
      ...
       button_ticks();
       rt_thread_mdelay(10);
  }

摇杆的采集

摇杆的接法蛮神奇,一路通过电阻分压接到ADC,同时两路一起接入运放震荡后接入IO。使用示波器抓波可知,一个方向调节占空比,一个方向调节频率。

中值

上下

左右

ADC初始化基本拷贝了MCUXpresso Config Tools的初始化代码

    // init adc for y
   const auto ADC1_config = adc_config_t{
          .enableOverWrite = false,
          .enableContinuousConversion = false,
          .enableHighSpeed = false,
          .enableLowPower = false,
          .enableLongSample = true,
          .enableAsynchronousClockOutput = true,
          .referenceVoltageSource = kADC_ReferenceVoltageSourceAlt0,
          .samplePeriodMode = kADC_SamplePeriodLong24Clcoks,
          .clockSource = kADC_ClockSourceAD,
          .clockDriver = kADC_ClockDriver8,
          .resolution = kADC_Resolution12Bit
  };
   ADC_Init(ADC1, &ADC1_config);
   ADC_EnableHardwareTrigger(ADC1, false);
   if (kStatus_Success != ADC_DoAutoCalibration(ADC1)) {
       log_w("adc auto calibration failed");
  }
   const auto adcChannelConfigStruct = adc_channel_config_t{
          .channelNumber = 0,
          .enableInterruptOnConversionCompleted = false
  };

由于一路可由ADC采集所得,所以我们只需要读出频率即可得知另一路。使用QTMR读取,基本拷贝了MCUXpresso Config Tools的初始化代码

    // init qtmr for y
   const qtmr_config_t TMR1_Channel_0_config = {
          .primarySource = kQTMR_ClockCounter0InputPin,
          .secondarySource = kQTMR_Counter0InputPin,
          .enableMasterMode = false,
          .enableExternalForce = false,
          .faultFilterCount = 0,
          .faultFilterPeriod = 10,
          .debugMode = kQTMR_RunNormalInDebug
  };
   /* Quad timer channel Channel_0 peripheral initialization */
   QTMR_Init(TMR1, kQTMR_Channel_0, &TMR1_Channel_0_config);
   /* Setup the Input capture mode of the timer channel */
   QTMR_SetupInputCapture(TMR1, kQTMR_Channel_0, kQTMR_Counter0InputPin,
                          false, false, kQTMR_NoCapture);
   /* Start the timer - select the timer counting mode */
   QTMR_StartTimer(TMR1, kQTMR_Channel_0, kQTMR_PriSrcRiseEdge);

上电后采集值作为中值

    int32_t x_mid_value = 0;
   int32_t y_mid_value = 0;

   rt_thread_mdelay(200);

   for (int i = 0; i < 8; i++) {
       QTMR_SetCurrentTimerCount(TMR1, kQTMR_Channel_0, 0);
       rt_thread_mdelay(100);
       y_mid_value += QTMR_GetCurrentTimerCount(TMR1, kQTMR_Channel_0);
       QTMR_SetCurrentTimerCount(TMR1, kQTMR_Channel_0, 0);

       ADC_SetChannelConfig(ADC1, 0, &adcChannelConfigStruct);
       while (0U == ADC_GetChannelStatusFlags(ADC1, 0)) {}
       x_mid_value += static_cast<int32_t>(ADC_GetChannelConversionValue(ADC1, 0));
  }
   x_mid_value /= 8;
   y_mid_value /= 8;
   log_i("x_mid_value: %d, y_mid_value: %d", x_mid_value, y_mid_value);

主循环中循环读取

    rt_thread_mdelay(100);
while (1) {
ADC_SetChannelConfig(ADC1, 0, &adcChannelConfigStruct);
while (0U == ADC_GetChannelStatusFlags(ADC1, 0)) {}
auto x = static_cast<int32_t>(ADC_GetChannelConversionValue(ADC1, 0));
// log_i("ADC Value: %d", x);
auto y = QTMR_GetCurrentTimerCount(TMR1, kQTMR_Channel_0);
QTMR_SetCurrentTimerCount(TMR1, kQTMR_Channel_0, 0);
// log_i("pulse cnt: %d", y);

bool x_should_move{false}, y_should_move{false};
int32_t dx = x - x_mid_value;
int32_t dy = y - y_mid_value;
if ((dx < -400) || (400 < dx)) {
x_should_move = true;
}
if ((dy < -6) || (6 < dy)) {
y_should_move = true;
}
if (x_should_move || y_should_move) {
SendMove(x_should_move ? int8_t(dx / 100) : 0, y_should_move ? int8_t(-dy) : 0);
}
rt_thread_mdelay(100);
}

USB上报

USB使用RT-Thread适配的TinyUSB,同时自己再适配一下RT1021

#include "fsl_clock.h"
#include "rtthread.h"
#include "device/usbd.h"

int tusb_board_init(void)
{
// Clock
CLOCK_EnableUsbhs0PhyPllClock(kCLOCK_Usbphy480M, 480000000U);
CLOCK_EnableUsbhs0Clock(kCLOCK_Usb480M, 480000000U);

USBPHY_Type* usb_phy;

// RT105x RT106x have dual USB controller.
#ifdef USBPHY1
usb_phy = USBPHY1;
#else
usb_phy = USBPHY;
#endif

// Enable PHY support for Low speed device + LS via FS Hub
usb_phy->CTRL |= USBPHY_CTRL_SET_ENUTMILEVEL2_MASK | USBPHY_CTRL_SET_ENUTMILEVEL3_MASK;

// Enable all power for normal operation
usb_phy->PWD = 0;

// TX Timing
uint32_t phytx = usb_phy->TX;
phytx &= ~(USBPHY_TX_D_CAL_MASK | USBPHY_TX_TXCAL45DM_MASK | USBPHY_TX_TXCAL45DP_MASK);
phytx |= USBPHY_TX_D_CAL(0x0C) | USBPHY_TX_TXCAL45DP(0x06) | USBPHY_TX_TXCAL45DM(0x06);
usb_phy->TX = phytx;

NVIC_SetPriority(USB_OTG1_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 4, 0));
NVIC_EnableIRQ(USB_OTG1_IRQn);

return 0;
}

void USB_OTG1_IRQHandler(void)
{
/* enter interrupt */
rt_interrupt_enter();
dcd_int_handler(0);
/* leave interrupt */
rt_interrupt_leave();
}

同时解决一下RT-Thread 5.0.0更出来的坑,use rt-thread 5+ mq recv api by kaidegit · Pull Request #2473 · hathach/tinyusb (github.com)

等待接收消息队列后上报即可

using hid_msg_t = struct {
hid_msg_type_t type;
union {
struct {
int8_t x;
int8_t y;
} move;
struct {
int8_t zoom;
} zoom;
struct {
int8_t volume;
} volume;
struct {
int8_t brightness;
} brightness;
};
};

static struct rt_messagequeue hid_mq;
rt_align(4)
static hid_msg_t hid_mq_pool[20];
static struct rt_semaphore hid_sem;

void tud_hid_report_complete_cb(uint8_t instance, uint8_t const *report, uint8_t len) {
(void) instance;
(void) report;
(void) len;

rt_sem_release(&hid_sem);
}

extern "C" void hid_thread_entry(void *para) {
// Init semaphore
rt_sem_init(&hid_sem, "hid", 0, RT_IPC_FLAG_PRIO);

auto ret = rt_mq_init(
&hid_mq, "hid_mq",
hid_mq_pool, sizeof(hid_msg_t),
sizeof(hid_mq_pool), RT_IPC_FLAG_PRIO);
if (ret != RT_EOK) {
log_e("hid mq init failed %d", ret);
}

hid_msg_t hid_msg;
while (1) {
if (!tud_connect()) {
rt_thread_mdelay(100);
continue;
}
if (rt_mq_recv(&hid_mq, &hid_msg, sizeof(hid_msg), 100) > 0) {
switch (hid_msg.type) {
case HID_MSG_CLICK: {
log_i("click");
tud_hid_mouse_report(
REPORT_ID_MOUSE, MOUSE_BUTTON_LEFT,
0, 0, 0, 0);
rt_sem_take(&hid_sem, 100);
tud_hid_mouse_report(
REPORT_ID_MOUSE, 0,
0, 0, 0, 0);
rt_sem_take(&hid_sem, 100);
}
break;
case HID_MSG_MOVE: {
log_i("move: x: %d, y: %d", hid_msg.move.x, hid_msg.move.y);
tud_hid_mouse_report(
REPORT_ID_MOUSE, 0,
(int8_t) hid_msg.move.x, (int8_t) hid_msg.move.y, 0, 0);
rt_sem_take(&hid_sem, 100);
}
break;
case HID_MSG_ZOOM: {
log_i("zoom: %d", hid_msg.zoom.zoom);
tud_hid_keyboard_report(REPORT_ID_KEYBOARD, KEYBOARD_MODIFIER_LEFTCTRL, nullptr);
rt_sem_take(&hid_sem, 100);
tud_hid_mouse_report(REPORT_ID_MOUSE, 0, 0, 0, hid_msg.zoom.zoom, 0);
rt_sem_take(&hid_sem, 100);
tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, nullptr);
rt_sem_take(&hid_sem, 100);
}
break;
case HID_MSG_VOLUME: {
log_i("volume: %d", hid_msg.volume.volume);
uint16_t volume_report = HID_USAGE_CONSUMER_VOLUME_INCREMENT;
uint16_t dummy = 0;
if (hid_msg.volume.volume < 0) {
volume_report = HID_USAGE_CONSUMER_VOLUME_DECREMENT;
hid_msg.volume.volume = -hid_msg.volume.volume;
}
for (int i = 0; i < hid_msg.volume.volume; i++) {
tud_hid_report(REPORT_ID_CONSUMER_CONTROL, &volume_report, 2);
rt_sem_take(&hid_sem, 100);
tud_hid_report(REPORT_ID_CONSUMER_CONTROL, &dummy, 2);
rt_sem_take(&hid_sem, 100);
}
}
break;
case HID_MSG_BRIGHTNESS: {
log_i("brightness: %d", hid_msg.brightness.brightness);
uint16_t brightness_report = HID_USAGE_CONSUMER_BRIGHTNESS_INCREMENT;
uint16_t dummy = 0;
if (hid_msg.brightness.brightness < 0) {
brightness_report = HID_USAGE_CONSUMER_BRIGHTNESS_DECREMENT;
hid_msg.brightness.brightness = -hid_msg.brightness.brightness;
}
for (int i = 0; i < hid_msg.brightness.brightness; i++) {
tud_hid_report(REPORT_ID_CONSUMER_CONTROL, &brightness_report, 2);
rt_sem_take(&hid_sem, 100);
tud_hid_report(REPORT_ID_CONSUMER_CONTROL, &dummy, 2);
rt_sem_take(&hid_sem, 100);
}
}
break;
default:
log_w("unknown hid msg type");
break;
}
}
}
rt_sem_detach(&hid_sem);
}

消息内容均从其他线程调用此类函数上报

void SendClick() {
auto *msg = new hid_msg_t;
msg->type = HID_MSG_CLICK;
rt_mq_send(&hid_mq, reinterpret_cast<const void *>(msg), sizeof(hid_msg_t));
delete msg;
}

实现的效果

触摸板移动

缩放

音量

亮度

摇杆移动

具体可见视频

遇到的问题及解决方案

  • TinyUSB的OSAL支持由于RT Thread在5.0.0改了接口返回值导致了一些问题,已经给TinyUSB提交了PR
  • RT-Thread的bsp也是一堆问题,改了改ld,拿MCUXpresso Config Tools重新生成了下配置。
  • 触摸屏采样数据不太稳定,滤波后还算可以

总结

RT-Thread的愿景很美好,但bsp的审核不太严格,导致了bsp的体验好多都存在问题。在较为稳定后再更改api的操作也比较降低兼容性。毕竟是开源项目,有坑也不担责是吧,修一修还是能用的。加入RTOS的开发让各部分更加独立清晰,更易阅读。

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