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.
SD card image: tru_usb_msos_winusb.sd.imgHost app: prebuilt_hostapps\c++\msos_winusb\test_1048576_1048576.bat
The bulk flow type is very fast with 1MB transfer length, we get peak 47MB/s (Unit: 1MB/s = 1000000 bytes per second).
Source code guide
1. Initialisation
The first MSOS device initialisation is the msos_winusb_init_app() function below.
tru_usb_device/c5soc/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 contains 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 registered), and those are the functions that you would normally work with.
usb/synopsys/tru_dwc2_otg1_isr.c
tru_dwc2_otg1_isr()
This interrupt handler function is used because the DE10-Nano's OTG usb connector is wired to USB controller 1.
3. Common callbacks
All common callbacks are placed in the generic (library layer) file below.
tru_usb_device/common/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];
req.code = MSOS_WINUSB_REQ_NONE;
req.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 created for the upper layer to override and create them.
4. Endpoint transfer buffers
tru_usb_device/c5soc/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 EP0 buffers are predefined by the library so those are excluded. The buffer code is a little bit more complicated than it should, using 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.
tru_usb_device/common/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 activated out EP callback (device to host), and the cb_txd_act() as the activated in EP callback (host to device). Both of these are called when the endpoint is activated or deactivated.
6. Activated endpoint callback
When an endpoint becomes active or inactive via the SET_CONFIG or SET_INTERFACE setup requests from the host, the registered callback is executed. Typically you would put the receive or transmit code into these callbacks
tru_usb_device/common/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 this example, as soon as the endpoint is activated we want to receive data.
Please note, transfers are started by the host, the transmit/receive only primes the USB hardware controller to get ready to transfer - it now waits for the host for an IN or OUT token packet before transfer actually starts.
7. Chunk endpoint callback
Whenever a reception completes the registered chunk callback is automatically executed for each chunk that is transferred.
tru_usb_device/common/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;
req.code = data[0];
req.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 callback. When a transfer is larger than the buffer you will get multiple chunk callbacks. In contrast you will get only one transfer complete (non-chunk) callback, which happens when all data is transferred.
tru_usb_device/common/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 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;
req.code = data[0];
req.write_len = buf_le_to_u32(data + 1);
}
switch(req.code){
case MSOS_WINUSB_REQ_RATE_TEST:
tru_usb_pc_transmit(msos->pc, txd_ep->num, req.write_len, req.write_len);
break;
case MSOS_WINUSB_REQ_VERIFY_TEST:
tru_usb_pc_transmit(msos->pc, txd_ep->num, req.write_len, req.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 complete callback. Here we process a cmd request, if valid enable transmission and finally we enable another receive. We want to scan only the first chunk for parameters, the rxd_ep->lxfer.whole.offset represents the linear offset of all chunks combined so if it is at 0 then we are in the first chunk.
tru_usb_device/common/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 command code is valid enable transmission and finally we enable another receive.
There is nothing to be processed for the transmit chunk and transfer complete callbacks so these are empty.
Document date: Rev 1: 24 Jul 2026