DE10-Nano: HPS USB 2.0 MSOS WinUSB example
MSOS WinUSB example
An example that enumerates as a vendor class MSOS (Microsoft OS) with WinUSB signature. The device associates with the WinUSB driver, which means no manual driver installation required, Windows will automatically load the WinUSB driver and the device can be opened with libusb library.
This example implements IN (device to host) and an OUT (host to device) bulk endpoints. Any data transferred is discarded so we can test out the maximum throughput. The host application for the PC is provided in c++ source and python script.
Control Panel\Hardware and Sound\Devices and PrintersThe enumerated device is TruMSOSWinUSB.
prebuilt_hostapps\c++\msos_winusb\test_1048576_1048576.batThe bulk flow type is very fast with 1MB transfer length, we get peak 47MByte/s (Unit: 1MB = 1000 bytes).
Source code guide
1. Initialisation
The first MSOS device initialisation is the msos_winusb_init_app() function below.
c5soc/usb_examples/device/msos_winusb_c5soc.c
void msos_winusb_init_app(void){
tru_dwc2_phy_t phy = {
.speed = TRU_DWC2_PHYSPEED_HS,
.iftype = TRU_DWC2_PHYIFTYPE_INTERNAL,
.rate = TRU_DWC2_PHYRATE_SDR,
.width = TRU_DWC2_PHYWIDTH_8BIT
};
msos_winusb_init(
&pc,
&msos,
TRU_USB_U1_BASE,
phy,
TRU_DWC2_DCFG_DEVSPD_USBHS20,
true,
false,
MSOS_WINUSB_DMA_MODE_CHOICE
);
tru_usb_pc_start(&pc);
}
This file is for hardware (application layer) specific initialisation code. The macro TRU_USB_U1_BASE selects USB controller 1, for USB controller 0 you would change that to TRU_USB_U0_BASE. The macro TRU_DWC2_DCFG_DEVSPD_USBHS20 selects enumeration to highest speed, if you want to limit to full-speed then change this to TRU_DWC2_DCFG_DEVSPD_USBFS20.
2. USB interrupt handler
When you connect the DE10-Nano's USB OTG to the host computer, USB controller 1 will generate interrupts which is processed by the ISR handler below. You do not need to do anything in this file because callbacks are automatically called if they are registered, and those are the functions that you would normally work with.
usb/synopsys/tru_dwc2_otg1_isr.c
tru_dwc2_otg1_isr()
This is interrupt handler function is used because the DE10-Nano's OTG usb connector wired to USB controller 1.
3. Common callbacks
All common callbacks are placed in the generic (library layer) file below.
usb_examples/device/msos_winusb.c
void msos_winusb_init(
tru_usb_pc_t *pc,
tru_usb_msos_t *msos,
uintptr_t usb_base_addr,
tru_dwc2_phy_t phy,
uint8_t speed_sel,
bool otg_id_pin_supported,
bool enable_sof_trigger,
tru_dwc2_dma_mode_t dma_mode
){
tru_usb_pc_init(
pc,
get_msos_winusb_pd_info()->pd,
usb_base_addr,
phy,
speed_sel,
otg_id_pin_supported,
enable_sof_trigger,
dma_mode,
1
);
tru_usb_msos_init(msos, pc, get_msos_winusb_pd_info());
msos_winusb_cb_setup_buffers();
rxd_ep = &msos->pc->eps.epos[MSOS_WINUSB_RXD_EPNUM];
txd_ep = &msos->pc->eps.epis[MSOS_WINUSB_TXD_EPNUM];
cmd = MSOS_WINUSB_REQ_NONE;
cmd_param_write_len = 0;
fill_sample_data();
// Register callback functions
tru_usb_msos_register_cb_reset(msos, cb_reset);
tru_usb_msos_register_cb_enumdone(msos, cb_enumdone);
tru_usb_msos_register_cb_epo_act(msos, MSOS_WINUSB_RXD_EPNUM, cb_rxd_act);
tru_usb_msos_register_cb_epi_act(msos, MSOS_WINUSB_TXD_EPNUM, cb_txd_act);
tru_usb_msos_register_cb_epo_chunk(msos, MSOS_WINUSB_RXD_EPNUM, cb_rxd_chunk);
tru_usb_msos_register_cb_epo_compl(msos, MSOS_WINUSB_RXD_EPNUM, cb_rxd_compl);
tru_usb_msos_register_cb_epi_chunk(msos, MSOS_WINUSB_TXD_EPNUM, cb_txd_chunk);
tru_usb_msos_register_cb_epi_compl(msos, MSOS_WINUSB_TXD_EPNUM, cb_txd_compl);
}
This function initialises the MSOS class and also registers some callbacks. In this demo we must create endpoint transfer buffers, however the size is determined by the hardware's available memory which is at the application layer, so a weak callback msos_winusb_cb_setup_buffers() function is called here for the upper layer to create them.
4. Endpoint transfer buffers
c5soc/usb_examples/device/msos_winusb_c5soc.c
// Transfer buffers for chunk transfers
#if defined(MSOS_WINUSB_DMA_MODE_CHOICE) && MSOS_WINUSB_DMA_MODE_CHOICE == _TRU_DWC2_DMA_MODE_SG
NONCACHEABLE_SECTION static uint8_t txd_buffer[MSOS_WINUSB_SG_NUM_TXBUF][32768];
NONCACHEABLE_SECTION static uint8_t rxd_buffer[MSOS_WINUSB_SG_NUM_RXBUF][32768];
NONCACHEABLE_SECTION static tru_dwc2_dev_sgdma_desc_reg_t txd_dma_descs[MSOS_WINUSB_SG_NUM_TXBUF];
NONCACHEABLE_SECTION static tru_dwc2_dev_sgdma_desc_reg_t rxd_dma_descs[MSOS_WINUSB_SG_NUM_RXBUF];
#else
NONCACHEABLE_SECTION static uint8_t txd_buffer[65536];
NONCACHEABLE_SECTION static uint8_t rxd_buffer[65536];
#endif
// Override weak function
void msos_winusb_cb_setup_buffers(void){
tru_usb_pc_epi_t *txd_ep = &pc.eps.epis[MSOS_WINUSB_TXD_EPNUM];
tru_usb_pc_epo_t *rxd_ep = &pc.eps.epos[MSOS_WINUSB_RXD_EPNUM];
#if defined(MSOS_WINUSB_DMA_MODE_CHOICE) && MSOS_WINUSB_DMA_MODE_CHOICE == _TRU_DWC2_DMA_MODE_SG
// Register transfer buffers
for(uint32_t i = 0; i < MSOS_WINUSB_SG_NUM_TXBUF; i++){
tru_usb_lxfer_register_buffer(&txd_ep->lxfer, txd_buffer[i], sizeof(txd_buffer[i]));
}
for(uint32_t i = 0; i < MSOS_WINUSB_SG_NUM_RXBUF; i++){
tru_usb_lxfer_register_buffer(&rxd_ep->lxfer, rxd_buffer[i], sizeof(rxd_buffer[i]));
}
// Register SG DMA descriptor lists
tru_usb_lxfer_register_sgdma_descs(&txd_ep->lxfer, txd_dma_descs, sizeof(txd_dma_descs) / sizeof(tru_dwc2_dev_sgdma_desc_reg_t));
tru_usb_lxfer_register_sgdma_descs(&rxd_ep->lxfer, rxd_dma_descs, sizeof(rxd_dma_descs) / sizeof(tru_dwc2_dev_sgdma_desc_reg_t));
#else
tru_usb_lxfer_register_buffer(&txd_ep->lxfer, txd_buffer, sizeof(txd_buffer));
tru_usb_lxfer_register_buffer(&rxd_ep->lxfer, rxd_buffer, sizeof(rxd_buffer));
#endif
}
Buffers are created here. The control setup endpoints in EP0 and out EP1 buffers are predefined by the library so those are excluded. The buffer code is a little bit more complicated with conditional macros to support the Scatter Gather DMA mode buffers.
5. Registration of activated endpoint callback
An important callback is when the endpoint (EP) is activated, these are registered in the hid_gamepad1_init() below.
usb_examples/device/msos_winusb.c
tru_usb_msos_register_cb_epo_act(msos, MSOS_WINUSB_RXD_EPNUM, cb_rxd_act);
tru_usb_msos_register_cb_epi_act(msos, MSOS_WINUSB_TXD_EPNUM, cb_txd_act);
The function cb_rxd_act() is registered as the out EP (device to host) activated callback, and the cb_txd_act() as the in EP (host to device) activated callback. Both of these are called when the endpoint is activated or deactivated.
6. Activated endpoint callback
When an endpoint becomes active or inactive by the SET_CONFIG or SET_INTERFACE setup requests, the registered callback is executed. Typically you would put the receive or transmit code into these callbacks
usb_examples/device/msos_winusb.c
void cb_rxd_act(tru_usb_msos_t *msos, tru_usb_pc_epo_t *ep, bool is_activated){
if(is_activated){
tru_usb_pc_receive(msos->pc, rxd_ep->num, MSOS_WINUSB_RXD_MAX_EXP_LEN, MSOS_WINUSB_RXD_MAX_EXP_LEN);
}
}
In the example, as soon as the endpoint is activated we want to receive data.
7. Chunk transfer complete endpoint callback
Whenever a reception completes the registered chunk transfer complete callback is automatically executed for each chunk.
usb_examples/device/msos_winusb.c
void cb_rxd_chunk(tru_usb_msos_t *msos, tru_usb_pc_epo_t *ep){
#if defined(TRU_CFG_USB_LOG_CALLBACK) && TRU_CFG_USB_LOG_CALLBACK == 1
LOG("CB: EP1 RX CHUNK, XFERRED=%lu\n", ep->lxfer.chunk.total_xferred_size);
#endif
if(msos->pc->eps.epos[MSOS_WINUSB_RXD_EPNUM].lxfer.whole.total_xferred_size > 0){ // Not a ZLP?
if(rxd_ep->lxfer.whole.offset == 0){ // Beginning of data?
// Extract parameters from the received data
uint8_t *data = rxd_ep->lxfer.chunk.buffers[0].buf;
cmd = data[0];
cmd_param_write_len = buf_le_to_u32(data + 1);
}
}
}
Normally, we process and extract cmd request code and parameters from the received data in the chunk transfer complete callback. When a transfer is larger than the buffer you will get multiple chunk transfer complete callbacks. In contrast you will get only one transfer complete (non-chunk) callback for the last chunk and the buffer naturally contain only the last chunk.
usb_examples/device/msos_winusb.c
void cb_rxd_compl(tru_usb_msos_t *msos, tru_usb_pc_epo_t *ep){
#if defined(TRU_CFG_USB_LOG_CALLBACK) && TRU_CFG_USB_LOG_CALLBACK == 1
LOG("CB: EP1 RX LONG COMPL, XFERRED=%lu\n", ep->lxfer.whole.total_xferred_size);
#endif
if(ep->lxfer.whole.total_xferred_size > 0){ // Not a ZLP?
if(rxd_ep->lxfer.whole.offset == 0){ // Beginning of data?
// Extract parameters from the received data
uint8_t *data = rxd_ep->lxfer.chunk.buffers[0].buf;
cmd = data[0];
cmd_param_write_len = buf_le_to_u32(data + 1);
}
switch(cmd){
case MSOS_WINUSB_REQ_RATE_TEST:
tru_usb_pc_transmit(msos->pc, txd_ep->num, cmd_param_write_len, cmd_param_write_len);
break;
case MSOS_WINUSB_REQ_VERIFY_TEST:
tru_usb_pc_transmit(msos->pc, txd_ep->num, cmd_param_write_len, cmd_param_write_len);
break;
}
}
tru_usb_pc_receive(msos->pc, rxd_ep->num, MSOS_WINUSB_RXD_MAX_EXP_LEN, MSOS_WINUSB_RXD_MAX_EXP_LEN);
}
Above is the receive transfer complete callback. Here we process a cmd request, if valid enable transmission and finally we enable another receive.
There is nothing to be processed for the transmit chunk and non-chunk transfer complete callbacks so these are empty.
Document date: Rev 1: 24 Jul 2026