平台介绍
本次使用的开发板为基于恩智浦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的开发让各部分更加独立清晰,更易阅读。