LDD3学习9--USB驱动2(Device HID实操)

1 前言

正好之前写过一个USB的基础:LDD3学习8--USB驱动1(理论)_brcmf set channel-CSDN博客

最近又在看RTOS,刚好两个东西可以一起看了。。。那就是在树莓派PICO上弄一下基于RTOS的USB device。本来LDD3的系列应该是基于Linux来弄,不过这次只有先不用了。

这次的例子是HID。其实想做的还是树莓派PICO的UAC device,不过先看看HID也可以。。。

2 TinyUSB

看tinyusb是因为貌似这个就是pico的官方指定usb库,很多example,都是用的tinyusb。首先就是一个问题有点奇怪,pico上很多硬件接口,i2c,spi也没说需要一个上层库来支持啊,为什么usb就需要一个tinyusb呢?

看一下主页:https://docs.tinyusb.org/en/latest/ 

上面也大概说了下,大意就是HCD,DCD这几个controller是硬件实现的,但是上层的TUH(TinyUSB Host),TUD(TinyUSB Device),USBH(USB Host),USBD(USB Device)这几个还是软件实现的。 

大致看了下USB的划分,好像也确实如此(The USB 3.0 functional layer),物理层,链路层,协议层划到硬件那边的。

这个设计和TCP/IP就非常类似了,物理层和数据链路层是用硬件实现,其余的则基本是软件实现。好吧,这样一下就能理解了。

再看一下PICO的芯片RP2040。https://www.raspberrypi.com/products/rp2040/specifications/

是只集成了USB1.1的controller和phy。 

https://www.raspberrypi.com/products/rp2040/

按照这个理解,那么tinyusb首先提供了两个基础的usb,就是USBH(USB Host),USBD(USB Device)。基于这两个基础,又提供了一些应用。如下:

Device Stack

Supports multiple device configurations by dynamically changing USB descriptors, low power functions such like suspend, resume, and remote wakeup. The following device classes are supported:

  • Audio Class 2.0 (UAC2)

  • Bluetooth Host Controller Interface (BTH HCI)

  • Communication Device Class (CDC)

  • Device Firmware Update (DFU): DFU mode (WIP) and Runtime

  • Human Interface Device (HID): Generic (In & Out), Keyboard, Mouse, Gamepad etc …

  • Mass Storage Class (MSC): with multiple LUNs

  • Musical Instrument Digital Interface (MIDI)

  • Network with RNDIS, Ethernet Control Model (ECM), Network Control Model (NCM)

  • Test and Measurement Class (USBTMC)

  • Video class 1.5 (UVC): work in progress

  • Vendor-specific class support with generic In & Out endpoints. Can be used with MS OS 2.0 compatible descriptor to load winUSB driver without INF file.

  • WebUSB with vendor-specific class

If you have a special requirement, usbd_app_driver_get_cb() can be used to write your own class driver without modifying the stack. Here is how the RPi team added their reset interface raspberrypi/pico-sdk#197

Host Stack

  • Human Interface Device (HID): Keyboard, Mouse, Generic

  • Mass Storage Class (MSC)

  • Communication Device Class: CDC-ACM

  • Vendor serial over USB: FTDI, CP210x, CH34x

  • Hub with multiple-level support

Similar to the Device Stack, if you have a special requirement, usbh_app_driver_get_cb() can be used to write your own class driver without modifying the stack.

好吧,东西看来也不少。。。

3 编译和运行

最早是想把tinyusb加到自己的pwm那个工程,不过遇到不少的编译问题。

 试了好几个方法都还是不行。cmake也不是很熟悉,后面还要再看看。

重新看了一下pico的sdk,sdk里面的tingusb自己就带了很多example,这个应该可以正常运行了吧。

cmake的时候带一个平台参数,直接就可以了。

cmake .. -DFAMILY=rp2040

编译的过程很长,这里就不细说了。

tom@PC-20241221RKUQ:/mnt/e/test/pico/pico-sdk/lib/tinyusb/examples/device/hid_composite/build$ make
BOARD not specified, defaulting to pico_sdk
PICO_SDK_PATH is /mnt/e/test/pico/pico-sdk
Target board (PICO_BOARD) is 'pico'.
Using board configuration from /mnt/e/test/pico/pico-sdk/src/boards/include/boards/pico.h
Pico Platform (PICO_PLATFORM) is 'rp2040'.
Build type is Release
TinyUSB available at /mnt/e/test/pico/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040; enabling build support for USB.
BTstack available at /mnt/e/test/pico/pico-sdk/lib/btstack
cyw43-driver available at /mnt/e/test/pico/pico-sdk/lib/cyw43-driver
lwIP available at /mnt/e/test/pico/pico-sdk/lib/lwip
mbedtls available at /mnt/e/test/pico/pico-sdk/lib/mbedtls
-- Configuring done (3.6s)
-- Generating done (6.3s)
-- Build files have been written to: /mnt/e/test/pico/pico-sdk/lib/tinyusb/examples/device/hid_composite/build
make[1]: Warning: File 'CMakeFiles/Makefile2' has modification time 0.25 s in the future
make[2]: Warning: File 'pico-sdk/src/rp2040/boot_stage2/CMakeFiles/bs2_default.dir/depend.make' has modification time 0.25 s in the future
make[2]: warning:  Clock skew detected.  Your build may be incomplete.
[  2%] Built target bs2_default
make[2]: Warning: File 'pico-sdk/src/rp2040/boot_stage2/CMakeFiles/bs2_default_library.dir/depend.make' has modification time 0.23 s in the future
make[2]: warning:  Clock skew detected.  Your build may be incomplete.
[  5%] Built target bs2_default_library
make[2]: Warning: File 'CMakeFiles/hid_composite.dir/compiler_depend.make' has modification time 1.8 s in the future
make[2]: warning:  Clock skew detected.  Your build may be incomplete.
[100%] Built target hid_composite
make[1]: warning:  Clock skew detected.  Your build may be incomplete.
tom@PC-20241221RKUQ:/mnt/e/test/pico/pico-sdk/lib/tinyusb/examples/device/hid_composite/build$ make
[  2%] Built target bs2_default
[  5%] Built target bs2_default_library
[100%] Built target hid_composite

直接将编译好的uf2文件拷贝到pico中,就可以看到led在闪,同时也有新的HID设备。

4 代码

详细看一下代码,其实真多。翻来覆去就是5个文件。除了CMakeLists.txt,两个头文件,两个源文件。真的算挺简单的。

4.1 整体流程

main.c

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Ha Thach (tinyusb.org)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "bsp/board_api.h"
#include "tusb.h"

#include "usb_descriptors.h"

//--------------------------------------------------------------------+
// MACRO CONSTANT TYPEDEF PROTYPES
//--------------------------------------------------------------------+

/* Blink pattern
 * - 250 ms  : device not mounted
 * - 1000 ms : device mounted
 * - 2500 ms : device is suspended
 */
enum  {
  BLINK_NOT_MOUNTED = 250,
  BLINK_MOUNTED = 1000,
  BLINK_SUSPENDED = 2500,
};

static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED;

void led_blinking_task(void);
void hid_task(void);

/*------------- MAIN -------------*/
int main(void)
{
  board_init();

  // init device stack on configured roothub port
  tud_init(BOARD_TUD_RHPORT);

  if (board_init_after_tusb) {
    board_init_after_tusb();
  }

  while (1)
  {
    tud_task(); // tinyusb device task
    led_blinking_task();

    hid_task();
  }
}

//--------------------------------------------------------------------+
// Device callbacks
//--------------------------------------------------------------------+

// Invoked when device is mounted
void tud_mount_cb(void)
{
  blink_interval_ms = BLINK_MOUNTED;
}

// Invoked when device is unmounted
void tud_umount_cb(void)
{
  blink_interval_ms = BLINK_NOT_MOUNTED;
}

// Invoked when usb bus is suspended
// remote_wakeup_en : if host allow us  to perform remote wakeup
// Within 7ms, device must draw an average of current less than 2.5 mA from bus
void tud_suspend_cb(bool remote_wakeup_en)
{
  (void) remote_wakeup_en;
  blink_interval_ms = BLINK_SUSPENDED;
}

// Invoked when usb bus is resumed
void tud_resume_cb(void)
{
  blink_interval_ms = tud_mounted() ? BLINK_MOUNTED : BLINK_NOT_MOUNTED;
}

//--------------------------------------------------------------------+
// USB HID
//--------------------------------------------------------------------+

static void send_hid_report(uint8_t report_id, uint32_t btn)
{
  // skip if hid is not ready yet
  if ( !tud_hid_ready() ) return;

  switch(report_id)
  {
    case REPORT_ID_KEYBOARD:
    {
      // use to avoid send multiple consecutive zero report for keyboard
      static bool has_keyboard_key = false;

      if ( btn )
      {
        uint8_t keycode[6] = { 0 };
        keycode[0] = HID_KEY_A;

        tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, keycode);
        has_keyboard_key = true;
      }else
      {
        // send empty key report if previously has key pressed
        if (has_keyboard_key) tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, NULL);
        has_keyboard_key = false;
      }
    }
    break;

    case REPORT_ID_MOUSE:
    {
      int8_t const delta = 5;

      // no button, right + down, no scroll, no pan
      tud_hid_mouse_report(REPORT_ID_MOUSE, 0x00, delta, delta, 0, 0);
    }
    break;

    case REPORT_ID_CONSUMER_CONTROL:
    {
      // use to avoid send multiple consecutive zero report
      static bool has_consumer_key = false;

      if ( btn )
      {
        // volume down
        uint16_t volume_down = HID_USAGE_CONSUMER_VOLUME_DECREMENT;
        tud_hid_report(REPORT_ID_CONSUMER_CONTROL, &volume_down, 2);
        has_consumer_key = true;
      }else
      {
        // send empty key report (release key) if previously has key pressed
        uint16_t empty_key = 0;
        if (has_consumer_key) tud_hid_report(REPORT_ID_CONSUMER_CONTROL, &empty_key, 2);
        has_consumer_key = false;
      }
    }
    break;

    case REPORT_ID_GAMEPAD:
    {
      // use to avoid send multiple consecutive zero report for keyboard
      static bool has_gamepad_key = false;

      hid_gamepad_report_t report =
      {
        .x   = 0, .y = 0, .z = 0, .rz = 0, .rx = 0, .ry = 0,
        .hat = 0, .buttons = 0
      };

      if ( btn )
      {
        report.hat = GAMEPAD_HAT_UP;
        report.buttons = GAMEPAD_BUTTON_A;
        tud_hid_report(REPORT_ID_GAMEPAD, &report, sizeof(report));

        has_gamepad_key = true;
      }else
      {
        report.hat = GAMEPAD_HAT_CENTERED;
        report.buttons = 0;
        if (has_gamepad_key) tud_hid_report(REPORT_ID_GAMEPAD, &report, sizeof(report));
        has_gamepad_key = false;
      }
    }
    break;

    default: break;
  }
}

// Every 10ms, we will sent 1 report for each HID profile (keyboard, mouse etc ..)
// tud_hid_report_complete_cb() is used to send the next report after previous one is complete
void hid_task(void)
{
  // Poll every 10ms
  const uint32_t interval_ms = 10;
  static uint32_t start_ms = 0;

  if ( board_millis() - start_ms < interval_ms) return; // not enough time
  start_ms += interval_ms;

  uint32_t const btn = board_button_read();

  // Remote wakeup
  if ( tud_suspended() && btn )
  {
    // Wake up host if we are in suspend mode
    // and REMOTE_WAKEUP feature is enabled by host
    tud_remote_wakeup();
  }else
  {
    // Send the 1st of report chain, the rest will be sent by tud_hid_report_complete_cb()
    send_hid_report(REPORT_ID_KEYBOARD, btn);
  }
}

// Invoked when sent REPORT successfully to host
// Application can use this to send the next report
// Note: For composite reports, report[0] is report ID
void tud_hid_report_complete_cb(uint8_t instance, uint8_t const* report, uint16_t len)
{
  (void) instance;
  (void) len;

  uint8_t next_report_id = report[0] + 1u;

  if (next_report_id < REPORT_ID_COUNT)
  {
    send_hid_report(next_report_id, board_button_read());
  }
}

// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
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)
{
  // TODO not Implemented
  (void) instance;
  (void) report_id;
  (void) report_type;
  (void) buffer;
  (void) reqlen;

  return 0;
}

// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 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;

  if (report_type == HID_REPORT_TYPE_OUTPUT)
  {
    // Set keyboard LED e.g Capslock, Numlock etc...
    if (report_id == REPORT_ID_KEYBOARD)
    {
      // bufsize should be (at least) 1
      if ( bufsize < 1 ) return;

      uint8_t const kbd_leds = buffer[0];

      if (kbd_leds & KEYBOARD_LED_CAPSLOCK)
      {
        // Capslock On: disable blink, turn led on
        blink_interval_ms = 0;
        board_led_write(true);
      }else
      {
        // Caplocks Off: back to normal blink
        board_led_write(false);
        blink_interval_ms = BLINK_MOUNTED;
      }
    }
  }
}

//--------------------------------------------------------------------+
// BLINKING TASK
//--------------------------------------------------------------------+
void led_blinking_task(void)
{
  static uint32_t start_ms = 0;
  static bool led_state = false;

  // blink is disabled
  if (!blink_interval_ms) return;

  // Blink every interval ms
  if ( board_millis() - start_ms < blink_interval_ms) return; // not enough time
  start_ms += blink_interval_ms;

  board_led_write(led_state);
  led_state = 1 - led_state; // toggle
}

里面很多个回调函数cb,这部分应该是在框架中,main函数如下:

int main(void)
{
  board_init();

  // init device stack on configured roothub port
  tud_init(BOARD_TUD_RHPORT);

  if (board_init_after_tusb) {
    board_init_after_tusb();
  }

  while (1)
  {
    tud_task(); // tinyusb device task
    led_blinking_task();

    hid_task();
  }
}

首先是板子的初始化,然后usb device的初始化,最后是板子的再初始化(如果有的话)。

这个是没有操作系统的,所以直接就是while1。

tud_task()应该是框架提供的,感觉是支持了下面的几个回调,tud_mount_cb,tud_umount_cb,tud_suspend_cb,tud_resume_cb。这里主要是usb拔插的事件处理。

void led_blinking_task(void)是控制led的,主要是闪烁的间隔时间,这个时间根据上面的usb拔插时间决定。

4.2 描述符

描述符是在usb_descriptors.h和usb_descriptors.c。这部分基本上只要把内容填好,在框架中就会处理发送这些内容。就是tud_init(BOARD_TUD_RHPORT)。

从交互来看,就是PC发送GET_DESCRIPTOR,然后PICO返回。这里tinyusb依然是做了封装。

基本上就是三个。

tud_descriptor_device_cb这个要求返回描述符。

返回的就是这个:

#define USB_PID           (0x4000 | _PID_MAP(CDC, 0) | _PID_MAP(MSC, 1) | _PID_MAP(HID, 2) | \
                           _PID_MAP(MIDI, 3) | _PID_MAP(VENDOR, 4) )

#define USB_VID   0xCafe
#define USB_BCD   0x0200

//--------------------------------------------------------------------+
// Device Descriptors
//--------------------------------------------------------------------+
tusb_desc_device_t const desc_device =
{
    .bLength            = sizeof(tusb_desc_device_t),
    .bDescriptorType    = TUSB_DESC_DEVICE,
    .bcdUSB             = USB_BCD,
    .bDeviceClass       = 0x00,
    .bDeviceSubClass    = 0x00,
    .bDeviceProtocol    = 0x00,
    .bMaxPacketSize0    = CFG_TUD_ENDPOINT0_SIZE,

    .idVendor           = USB_VID,
    .idProduct          = USB_PID,
    .bcdDevice          = 0x0100,

    .iManufacturer      = 0x01,
    .iProduct           = 0x02,
    .iSerialNumber      = 0x03,

    .bNumConfigurations = 0x01
};

之后是tud_hid_descriptor_report_cb,这个应该当PC发现是HID设备之后,要求发送HID描述符。此时发送的是这个。

uint8_t const desc_hid_report[] =
{
  TUD_HID_REPORT_DESC_KEYBOARD( HID_REPORT_ID(REPORT_ID_KEYBOARD         )),
  TUD_HID_REPORT_DESC_MOUSE   ( HID_REPORT_ID(REPORT_ID_MOUSE            )),
  TUD_HID_REPORT_DESC_CONSUMER( HID_REPORT_ID(REPORT_ID_CONSUMER_CONTROL )),
  TUD_HID_REPORT_DESC_GAMEPAD ( HID_REPORT_ID(REPORT_ID_GAMEPAD          ))
};

最后是tud_descriptor_string_cb,这个说的是收到GET STRING DESCRIPTOR请求时返回的数据。返回的内容如下:

uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
  (void) langid;
  size_t chr_count;

  switch ( index ) {
    case STRID_LANGID:
      memcpy(&_desc_str[1], string_desc_arr[0], 2);
      chr_count = 1;
      break;

    case STRID_SERIAL:
      chr_count = board_usb_get_serial(_desc_str + 1, 32);
      break;

    default:
      // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors.
      // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors

      if ( !(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0])) ) return NULL;

      const char *str = string_desc_arr[index];

      // Cap at max char
      chr_count = strlen(str);
      size_t const max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1; // -1 for string type
      if ( chr_count > max_count ) chr_count = max_count;

      // Convert ASCII string into UTF-16
      for ( size_t i = 0; i < chr_count; i++ ) {
        _desc_str[1 + i] = str[i];
      }
      break;
  }

  // first byte is length (including header), second byte is string type
  _desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));

  return _desc_str;
}

根据之前的理论学习,一个USB设备是这样的:

在这个例子中,Device就是厂商,里面的信息都是我们在回调里面定义的。Config描述的就是供电这些(感觉现在Device和config可以合并。。。),在这里的Interface就是HID,Endpoint和后面的传输有关,这里是0x81,表示端点1的IN,从设备到主机。

最后看看实际的抓包(Windows10主机):

这里的2.12.0就是设备地址,2是URB总线编号,也就是主板上的U口编号,12是设备编号,这个下来可以再看看。

可以看到,一共是有4次来回,分别是GET_DESCRIPTOR DEVICE,GET_DESCRIPTOR CONFIGURATION,SET CONFIGURATION和GET_DESCRIPTOR HID Report。

第一个GET_DESCRIPTOR DEVICE,就是tud_descriptor_device_cb里面填的值。

GET_DESCRIPTOR CONFIGURATION,是从tud_descriptor_configuration_cb回调返回。

SET CONFIGURATION这个没看到。

最后的GET_DESCRIPTOR HID Report,则是从HID的处理中发送的。

4.3 HID处理

重点的还是下面的hid_task();

// Every 10ms, we will sent 1 report for each HID profile (keyboard, mouse etc ..)
// tud_hid_report_complete_cb() is used to send the next report after previous one is complete
void hid_task(void)
{
  // Poll every 10ms
  const uint32_t interval_ms = 10;
  static uint32_t start_ms = 0;

  if ( board_millis() - start_ms < interval_ms) return; // not enough time
  start_ms += interval_ms;

  uint32_t const btn = board_button_read();

  // Remote wakeup
  if ( tud_suspended() && btn )
  {
    // Wake up host if we are in suspend mode
    // and REMOTE_WAKEUP feature is enabled by host
    tud_remote_wakeup();
  }else
  {
    // Send the 1st of report chain, the rest will be sent by tud_hid_report_complete_cb()
    send_hid_report(REPORT_ID_KEYBOARD, btn);
  }
}

可以看到,在mcu中,估计是因为没有操作系统的原因,所以等待,全部用的是时间检查来做的,即读取最新时间,来判断是否达到了需要等待的事件。

btn就是pico板子上唯一的那个按钮,写了BOOTSEL的那个。按下之后,发送REPORT_ID_KEYBOARD键盘事件。

当发送成功之后,会有一个回调tud_hid_report_complete_cb,在这里,发送了鼠标和游戏手柄事件,还有一个音量控制的事件。

这里就不多看了,主要看看鼠标吧。

    case REPORT_ID_MOUSE:
    {
      int8_t const delta = 5;

      // no button, right + down, no scroll, no pan
      tud_hid_mouse_report(REPORT_ID_MOUSE, 0x00, delta, delta, 0, 0);
    }
    break;

可以看到,应该还是tinyusb对mouse进行了封装,封装是在hid_device.c里面。最后调用的还是tud_hid_n_report。

看了一下实际发送的抓包:

是以URB中断的形式发上去的。

Endpoint这里是0x81,表示端点1的IN,从设备到主机。 URB传输类型是中断。

关于HID的数据在最下面,也不长,不过wireshark中也没有解析。

好了,先看到这里吧。。。

参考:

How does a USB keyboard work?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值