なんか右手首が腱鞘炎気味で少し痛い。打鍵よりも、Xを読むためのタップスクロールが原因じゃないかと思ったりしている。
というわけで、手持ちの部材でスクロールキーを作ろう。
ATOM S3 LITEとDual Button Unitを出してくる。

前にもArduinoベースで作ったことがあるのだけど、普通のキーにすると、長押しでスクロールを連続すると、キーリピートが早すぎるので、ヌルヌルくらいの速度で画面が読めるようにスクロールしたい。
今回は、HIDはマウスにして、ホイールイベントを使うのがいいだろうか。すると、ESP-IDF + TinyUSBになるかな。
ATOMS3 LITEは、USBポートは1つしかないので、とりあえず、TinyUSBに割り当てることにして、FWの書き込みはボタンでGPIO0をLowに落として書き込むことにする。というわけで、シリアルコンソールまわりは無効にしておく。
% idf.py create-project smoothwheel
% cd smoothwheel
% idf.py set-target esp32s3
% idf.py menuconfig
Component config --->
ESP System Settings --->
Channel for console output (None) --->
Channel for console secondary output (No secondary console) --->
あれ? TinyUSBの設定がないな... デフォルトでは含まれていないのかな?
別コンポーネントなのかな... あったあった。
espressif/esp_tinyusb • v2.2.0 • ESP Component Registry
https://components.espressif.com/components/espressif/esp_tinyusb/versions/2.2.0/readme
コンポーネントを追加する。
% idf.py add-dependency "espressif/esp_tinyusb^2.2.0"
% cat main/idf_component.yml
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: '>=4.1.0'
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies public flag can be set.
# # public flag doesn't have an effect dependencies of the main component.
# # All dependencies of main are public by default.
# public: true
espressif/esp_tinyusb: ^2.2.0
もっかいmenuconfigする。esp_tinyusbが起動ログに認識されている。
NOTICE: Processing 3 dependencies:
NOTICE: [1/3] espressif/esp_tinyusb (2.2.0)
NOTICE: [2/3] espressif/tinyusb (0.19.0~3)
NOTICE: [3/3] idf (5.5.2)
TinyUSB Stackが表示されている。
TinyUSB Stack --->


HIDを一個使うので、1にする。
Human Interface Device Class (HID) --->


TinyUSB HID interfaces count = 1
デバイス情報はこんな感じになる。HIDなので、あんまり気にしない。

保村して終了する。
コードはChatGPTに書いてもらう。こんな感じ。しかし、案の定一回では動かないw 修正してもらった。
% vi main/smoothwheel.c
#include <stdint.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "tinyusb.h"
#include "tinyusb_default_config.h"
#include "class/hid/hid_device.h"
// ATOMS3 Lite Grove想定
#define BTN_UP_GPIO GPIO_NUM_1
#define BTN_DOWN_GPIO GPIO_NUM_2
#define BUTTON_ACTIVE_LEVEL 0
#define DEBOUNCE_MS 25
#define LONG_PRESS_MS 350
#define REPEAT_INTERVAL_MS 120
#define WHEEL_STEP 1
static const char *TAG = "smoothwheel";
enum {
REPORT_ID_MOUSE = 1,
};
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_HID * TUD_HID_DESC_LEN)
static bool suspended = false;
static bool wakeup_host = false;
/*
HID report descriptor
MouseだけのHIDとして定義します。
*/
const uint8_t hid_report_descriptor[] = {
TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(REPORT_ID_MOUSE))
};
/*
String descriptor
*/
const char *hid_string_descriptor[5] = {
(char[]){0x09, 0x04}, // 0: English
"M5Stack", // 1: Manufacturer
"smoothwheel", // 2: Product
"0001", // 3: Serial
"Mouse Wheel HID", // 4: HID interface
};
/*
Configuration descriptor
HID interface 1個だけ。
*/
static const uint8_t hid_configuration_descriptor[] = {
TUD_CONFIG_DESCRIPTOR(
1, // configuration number
1, // interface count
0, // string index
TUSB_DESC_TOTAL_LEN, // total length
TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP,
100 // power mA
),
TUD_HID_DESCRIPTOR(
0, // interface number
4, // string index
false, // boot protocol
sizeof(hid_report_descriptor),
0x81, // EP IN address
16, // EP size
10 // polling interval ms
),
};
typedef enum {
SCROLL_UP = 0,
SCROLL_DOWN,
} scroll_dir_t;
typedef struct {
gpio_num_t gpio;
scroll_dir_t dir;
bool stable_pressed;
bool last_raw_pressed;
int64_t last_change_ms;
bool long_mode;
int64_t press_start_ms;
int64_t last_repeat_ms;
} button_state_t;
static int64_t now_ms(void)
{
return esp_timer_get_time() / 1000;
}
static bool button_raw_pressed(gpio_num_t gpio)
{
return gpio_get_level(gpio) == BUTTON_ACTIVE_LEVEL;
}
static void send_mouse_wheel(scroll_dir_t dir)
{
if (!tud_mounted()) {
return;
}
if (!tud_hid_ready()) {
return;
}
if (suspended) {
if (wakeup_host) {
tud_remote_wakeup();
wakeup_host = false;
}
return;
}
int8_t wheel = (dir == SCROLL_UP) ? WHEEL_STEP : -WHEEL_STEP;
tud_hid_mouse_report(
REPORT_ID_MOUSE,
0, // buttons
0, // x
0, // y
wheel, // vertical wheel
0 // horizontal wheel
);
vTaskDelay(pdMS_TO_TICKS(5));
if (tud_mounted() && tud_hid_ready()) {
tud_hid_mouse_report(
REPORT_ID_MOUSE,
0,
0,
0,
0,
0
);
}
}
static void update_button(button_state_t *b)
{
const int64_t t = now_ms();
bool raw_pressed = button_raw_pressed(b->gpio);
if (raw_pressed != b->last_raw_pressed) {
b->last_raw_pressed = raw_pressed;
b->last_change_ms = t;
return;
}
if ((t - b->last_change_ms) < DEBOUNCE_MS) {
return;
}
if (raw_pressed != b->stable_pressed) {
b->stable_pressed = raw_pressed;
if (b->stable_pressed) {
b->press_start_ms = t;
b->last_repeat_ms = t;
b->long_mode = false;
send_mouse_wheel(b->dir);
} else {
b->long_mode = false;
}
}
if (!b->stable_pressed) {
return;
}
if (!b->long_mode) {
if ((t - b->press_start_ms) >= LONG_PRESS_MS) {
b->long_mode = true;
b->last_repeat_ms = t;
send_mouse_wheel(b->dir);
}
return;
}
if ((t - b->last_repeat_ms) >= REPEAT_INTERVAL_MS) {
b->last_repeat_ms = t;
send_mouse_wheel(b->dir);
}
}
static esp_err_t button_gpio_init(void)
{
gpio_config_t io_conf = {
.pin_bit_mask =
(1ULL << BTN_UP_GPIO) |
(1ULL << BTN_DOWN_GPIO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
return gpio_config(&io_conf);
}
uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance)
{
(void) instance;
return hid_report_descriptor;
}
uint16_t tud_hid_get_report_cb(
uint8_t instance,
uint8_t report_id,
hid_report_type_t report_type,
uint8_t *buffer,
uint16_t reqlen
) {
(void) instance;
(void) report_id;
(void) report_type;
(void) buffer;
(void) reqlen;
return 0;
}
void tud_hid_set_report_cb(
uint8_t instance,
uint8_t report_id,
hid_report_type_t report_type,
uint8_t const *buffer,
uint16_t bufsize
) {
(void) instance;
(void) report_id;
(void) report_type;
(void) buffer;
(void) bufsize;
}
void tud_suspend_cb(bool remote_wakeup_en)
{
suspended = true;
wakeup_host = remote_wakeup_en;
ESP_LOGI(TAG, "USB suspended, remote_wakeup=%d", remote_wakeup_en);
}
void tud_resume_cb(void)
{
suspended = false;
wakeup_host = false;
ESP_LOGI(TAG, "USB resumed");
}
static esp_err_t tinyusb_hid_init(void)
{
tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG();
tusb_cfg.descriptor.device = NULL;
tusb_cfg.descriptor.full_speed_config = hid_configuration_descriptor;
tusb_cfg.descriptor.string = hid_string_descriptor;
tusb_cfg.descriptor.string_count =
sizeof(hid_string_descriptor) / sizeof(hid_string_descriptor[0]);
#if (TUD_OPT_HIGH_SPEED)
tusb_cfg.descriptor.high_speed_config = hid_configuration_descriptor;
#endif
return tinyusb_driver_install(&tusb_cfg);
}
void app_main(void)
{
ESP_LOGI(TAG, "smoothwheel start");
ESP_ERROR_CHECK(button_gpio_init());
button_state_t btn_up = {
.gpio = BTN_UP_GPIO,
.dir = SCROLL_UP,
.stable_pressed = false,
.last_raw_pressed = false,
.last_change_ms = now_ms(),
.long_mode = false,
.press_start_ms = 0,
.last_repeat_ms = 0,
};
button_state_t btn_down = {
.gpio = BTN_DOWN_GPIO,
.dir = SCROLL_DOWN,
.stable_pressed = false,
.last_raw_pressed = false,
.last_change_ms = now_ms(),
.long_mode = false,
.press_start_ms = 0,
.last_repeat_ms = 0,
};
ESP_ERROR_CHECK(tinyusb_hid_init());
ESP_LOGI(TAG, "USB HID mouse wheel ready");
ESP_LOGI(TAG, "UP gpio=%d, DOWN gpio=%d", BTN_UP_GPIO, BTN_DOWN_GPIO);
while (1) {
update_button(&btn_up);
update_button(&btn_down);
vTaskDelay(pdMS_TO_TICKS(2));
}
}
修正したコードをビルドしてみる。
% idf.py build
こんどはエラーは出ないね。
書き込む。リセットボタンを長押しして書き込みモードに入れる。うまくいったかな。
% idf.py flash monitor
USBデバイスとして認識していないな...
コード側を修正。HID用のconfiguration descriptorをtusb_cfg.descriptor.full_speed_configに渡していなかった模様。
ビルドして書き込む。
認識した。


赤が上、青が下。ちょっとヌルヌルとはいえ遅いかな。速度調整しているのはここ。
#define REPEAT_INTERVAL_MS 120
1回あたりのスクロール量はここ。
#define WHEEL_STEP 1
長押し開始までの待ち時間はここ。
#define LONG_PRESS_MS 350
REPEAT_INTERVAL_MSを少し小さくしてみるか。
80にすると微妙に早い感じ。100あたりがいいかな。
読みながらスクロールできる速度になった。
しばらく使ってみるかな。

使っていると、100だと微妙に遅い。90とか95かな。90だと微妙に読み流しできないな。

オリジナル投稿:
USB上下スクロールボタンをつくる|kinneko|pixivFANBOX
https://kinneko.fanbox.cc/posts/11886264



