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中也没有解析。
好了,先看到这里吧。。。
参考: