usbx 基本紹介
usbx は ThreadX OS の USB スタックで、複数のホストドライバ、デバイス列挙、および OTG に対応しています。
オープンソースのリポジトリはこちら
解説はこちら
ThreadX RTOS を使用する場合、この USB スタックをホスト単体またはデバイス単体として使うのは非常に簡単です。しかし、このライブラリのホスト/デバイス機能(OTG)を同時に使おうとすると、少し難しくなります。公式ドキュメントには断片的な記述しかなく、各フォーラムにも完全で詳細な例がないためです。
usbx の OTG はハードウェア OTG を指すのではなく、プロトコル層の OTG です。つまり、usbx プロトコルスタックのホスト機能とデバイス機能を同時に使用でき、host オブジェクトと device オブジェクトを同時に正常に動作させられます。ハードウェア層が1つのインターフェースであろうと複数であろうと、usbx は気にしません。
まず、自分に以下の能力があるか確認してください。
- C 言語ライブラリで、マクロを使ってコードの動作を制御する方法について基礎的な理解があること。
- 公式サンプルを読み解く能力があること。公式サンプルの main のメインフローで、各行が何をしているのか理解できることを指します。
- 手動またはツールでチップのペリフェラルを設定できる能力があること。以降、ペリフェラルドライバの部分は扱いません。
ライブラリの基本的な使い方
以下は host プロトコルスタックを有効にする基本の流れです。要するに、メモリ領域を定義してメモリプールとし、そのプールからメモリを確保して usbx システムに渡し、使いたいホストクラスを登録し、さらにメモリを確保してホストアプリケーションスレッドを起動します。ホストアプリケーションスレッド内で何をするかは、公式サンプルを参考にしてください。
#define UX_HOST_APP_MEM_POOL_SIZE 1024 * 44
__ALIGN_BEGIN static UCHAR ux_host_byte_pool_buffer[UX_HOST_APP_MEM_POOL_SIZE] __ALIGN_END;
static TX_BYTE_POOL ux_host_app_byte_pool;
void txAppUSBXHostInit(void)
{
UINT status = TX_SUCCESS;
VOID *memory_ptr;
if(tx_byte_pool_create(&ux_host_app_byte_pool, "Ux App memory pool", ux_host_byte_pool_buffer, UX_HOST_APP_MEM_POOL_SIZE) != TX_SUCCESS)
{
}
else
{
memory_ptr = (VOID *)&ux_host_app_byte_pool;
status = MX_USBX_Host_Init(memory_ptr);
if(status != UX_SUCCESS)
{
while(1)
{
}
}
}
}
UINT MX_USBX_Host_Init(VOID *memory_ptr)
{
UINT ret = UX_SUCCESS;
UCHAR *pointer;
TX_BYTE_POOL *byte_pool = (TX_BYTE_POOL *)memory_ptr;
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, USBX_HOST_MEMORY_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(ux_system_initialize(pointer, USBX_HOST_MEMORY_STACK_SIZE, UX_NULL, 0) != UX_SUCCESS)
{
return UX_ERROR;
}
if(ux_host_stack_initialize(ux_host_event_callback) != UX_SUCCESS)
{
return UX_ERROR;
}
ux_utility_error_callback_register(&ux_host_error_callback);
if(ux_host_stack_class_register(_ux_system_host_class_storage_name, ux_host_class_storage_entry) != UX_SUCCESS)
{
return UX_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, UX_HOST_APP_THREAD_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_host_app_thread, UX_HOST_APP_THREAD_NAME, app_ux_host_thread_entry, 0, pointer, UX_HOST_APP_THREAD_STACK_SIZE, UX_HOST_APP_THREAD_PRIO, UX_HOST_APP_THREAD_PREEMPTION_THRESHOLD, UX_HOST_APP_THREAD_TIME_SLICE, UX_HOST_APP_THREAD_START_OPTION) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
return ret;
}
以下は device プロトコルスタックを有効にする基本の流れです。要するにホスト側と同様で、メモリプールを作成し、メモリを確保して初期化と登録を行い、デバイスアプリケーションスレッドを起動します。
#define UX_DEVICE_APP_MEM_POOL_SIZE 1024 * 24
__ALIGN_BEGIN static UCHAR ux_device_byte_pool_buffer[UX_DEVICE_APP_MEM_POOL_SIZE] __ALIGN_END;
static TX_BYTE_POOL ux_device_app_byte_pool;
void txAppUSBXDrviceInit(void)
{
UINT status = TX_SUCCESS;
VOID *memory_ptr;
if(tx_byte_pool_create(&ux_device_app_byte_pool, "Ux App memory pool", ux_device_byte_pool_buffer, UX_DEVICE_APP_MEM_POOL_SIZE) != TX_SUCCESS)
{
}
else
{
memory_ptr = (VOID *)&ux_device_app_byte_pool;
status = MX_USBX_Device_Init(memory_ptr);
if(status != UX_SUCCESS)
{
while(1)
{
}
}
}
}
UINT MX_USBX_Device_Init(VOID *memory_ptr)
{
UINT ret = UX_SUCCESS;
UCHAR *device_framework_high_speed;
UCHAR *device_framework_full_speed;
ULONG device_framework_hs_length;
ULONG device_framework_fs_length;
ULONG string_framework_length;
ULONG language_id_framework_length;
UCHAR *string_framework;
UCHAR *language_id_framework;
UCHAR *pointer;
TX_BYTE_POOL *byte_pool = (TX_BYTE_POOL *)memory_ptr;
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, USBX_DEVICE_MEMORY_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(ux_system_initialize(pointer, USBX_DEVICE_MEMORY_STACK_SIZE, UX_NULL, 0) != UX_SUCCESS)
{
return UX_ERROR;
}
device_framework_high_speed = USBD_Get_Device_Framework_Speed(USBD_HIGH_SPEED, &device_framework_hs_length);
device_framework_full_speed = USBD_Get_Device_Framework_Speed(USBD_FULL_SPEED, &device_framework_fs_length);
string_framework = USBD_Get_String_Framework(&string_framework_length);
language_id_framework = USBD_Get_Language_Id_Framework(&language_id_framework_length);
if(ux_device_stack_initialize(device_framework_high_speed, device_framework_hs_length, device_framework_full_speed, device_framework_fs_length, string_framework, string_framework_length, language_id_framework, language_id_framework_length, UX_NULL) != UX_SUCCESS)
{
return UX_ERROR;
}
cdc_acm_parameter.ux_slave_class_cdc_acm_instance_activate = USBD_CDC_ACM_Activate;
cdc_acm_parameter.ux_slave_class_cdc_acm_instance_deactivate = USBD_CDC_ACM_Deactivate;
cdc_acm_parameter.ux_slave_class_cdc_acm_parameter_change = USBD_CDC_ACM_ParameterChange;
cdc_acm_configuration_number = USBD_Get_Configuration_Number(CLASS_TYPE_CDC_ACM, 0);
cdc_acm_interface_number = USBD_Get_Interface_Number(CLASS_TYPE_CDC_ACM, 0);
if(ux_device_stack_class_register(_ux_system_slave_class_cdc_acm_name, ux_device_class_cdc_acm_entry, cdc_acm_configuration_number, cdc_acm_interface_number, &cdc_acm_parameter) != UX_SUCCESS)
{
return UX_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, UX_DEVICE_APP_THREAD_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_device_app_thread, UX_DEVICE_APP_THREAD_NAME, app_ux_device_thread_entry, 0, pointer, UX_DEVICE_APP_THREAD_STACK_SIZE, UX_DEVICE_APP_THREAD_PRIO, UX_DEVICE_APP_THREAD_PREEMPTION_THRESHOLD, UX_DEVICE_APP_THREAD_TIME_SLICE, UX_DEVICE_APP_THREAD_START_OPTION) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, 1024, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_cdc_read_thread, "cdc_acm_read_usbx_app_thread_entry", usbx_cdc_acm_read_thread_entry, 1, pointer, 1024, 20, 20, TX_NO_TIME_SLICE, TX_AUTO_START) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
return ret;
}
usbx OTG を有効にする際の特別な操作
まず usbx を使うなら、コンパイルのグローバル define として UX_INCLUDE_USER_DEFINE_FILE を追加しておくのがよいでしょう。ux_user.h で usbx の各機能を取捨選択してカスタマイズできます。ライブラリのソースコードには ux_user_sample.h という名前のファイルがあるはずなので、それをコピーして ux_user.h にリネームし、プロジェクトに追加してください。
その中には次のような部分があるはずです。ここが混乱の主な原因です。元のサンプルでは UX_OTG_SUPPORT の定義がコメントアウトされています。自分の ux_user.h では、UX_OTG_SUPPORT をコメントアウトしないようにし、あわせて ux_user.h 内の UX_HOST_SIDE_ONLY と UX_DEVICE_SIDE_ONLY の define をコメントアウトしてください。これでライブラリの OTG サポートが有効になります。また、UX_DEVICE_BIDIRECTIONAL_ENDPOINT_SUPPORT と UX_DEVICE_CLASS_CDC_ACM_WRITE_AUTO _ZLP のマクロも有効にしておくことをおすすめします。
#ifndef UX_HOST_SIDE_ONLY
#ifndef UX_DEVICE_SIDE_ONLY
/* #define UX_OTG_SUPPORT */
#endif
#endif
基本の流れの中で、ホストとデバイスの init 処理がどちらも ux_system_initialize を呼び出していることに注目してください。これが、サンプルをそのままコピーしたり、CubeMX でホストとデバイスの両方を含むプロジェクトを生成したりしても動かない原因です。ux_system_initialize を2回呼び出してはいけません。どちらか一方の機能が異常になり、内部の処理スレッドでハングしてしまいます。正しい手順は、3つのメモリ領域をあらかじめ定義し、3つのメモリプールを使うことです。1つ目のプールから先にメモリを確保して ux_system_initialize を実行し、2つ目と3つ目のプールでホストとデバイスの init を行います。その際、このフローの中で ux_system_initialize を再び呼び出さないようにしてください。これで以降は正常に使用できます。
その他の細かい注意点
Windows の CDC_ACM はドライバ不要のシリアルポートとして、エンドポイント 0x01 と 0x81 を IN/OUT に固定使用します。このエンドポイント番号は変更しないでください。
一部のパケットは長さを4バイトアラインにする必要があります。以下は優れたゼロ埋めの方法です。
uint8_t *data; // 原有报文buffer
uint32_t len = OriginalLen; // 原有长度
for(uint32_t i = len; i < ((len + 0x3) & ~0x3U); i++)
{
data[i] = 0x00;
}
_write(data, ((len + 0x3) & ~0x3U));
コンポジットデバイスとして列挙する場合、ディスクリプタは CubeMX が生成した ux_device_descriptors.c をベースに修正するのがよいでしょう。実は、このディスクリプタ生成のソースコードは本当に優秀です。
注意点として、エンドポイントを使用する場合は、対応する USB TxFIFO を正しく有効にしておく必要があります。以下に例を示します。
/* Set Rx FIFO */
HAL_PCDEx_SetRxFiFo(&hpcd_USB_OTG_FS, 0x200);
/* Set Tx FIFO 0 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 0, 0x80); // 通用的 setup 端点
/* Set Tx FIFO 1 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 1, 0x100); // 开启了 0x01 和 0x81 端点
/* Set Tx FIFO 3 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 3, 0x100); // 开启了 0x03 和 0x83 端点