DE10-Nano: HPS USB 2.0 HID vendor examples
HID vendor example
An example that enumerates as a HID vendor device with custom 64 byte input report (report from device to host) and 64 byte output report (report from host to device). This is useful for embedded MCU when you want to transfer small amount of data and does not require additional driver installation on Windows or Linux.
Host applications are provided which uses libusb (all OS) and Windows WDK HID API (Windows only) to communicate with the device.
Control Panel\Hardware and Sound\Devices and PrintersThe enumerated device is TruHIDCus.
prebuilt_hostapps\c++\hid_cus_libusb\test_64_64.batThe HID can only use control and interrupt flow type so the transfer rate isn't very good (Unit: 1MB = 1000 bytes).
HID endpoints and reports
The HID class can use dedicated endpoints for both the input and output reports, or a single dedicated one only for the input report. This is because the default endpoint 0 can be used to transfer the output report using setup request.
Both methods are demonstrated by separate examples. The hid_custom_c5soc example uses two dedicated endpoints (IN EP1 and OUT EP1), and the hid_custom_ctl_c5soc use one dedicated endpoint (IN EP1).
Note the dedicated endpoints are intended for the input and output reports only, the bidirectional feature report which both the device and host may transmit and receive can only do so using the SET_REPORT/GET_REPORT setup requests, meaning transfer is started via host request only. There is a callback for the SET_REPORT request. Note the library automatically updates the report buffer when it receives a SET_REPORT request, the callback is for letting the application know there was a report received. A callback for the GET_REPORT is not needed because this library automatically sends the requested report to the host. Your application should update the feature report whenever it is necessary.
According to USB spec if there is only a single report of a type the first byte isn't the report ID, it is the start of the data payload. If there are multiple HID reports of the same type then the first byte of each report is the report ID.
1. Initialisation
The first HID device initialisation is the hid_cus_init_app() function below.
c5soc/usb_examples/device/hid_custom_c5soc.c
void hid_cus_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
};
hid_cus_init(
&pc,
&hid,
TRU_USB_U1_BASE,
phy,
TRU_DWC2_DCFG_DEVSPD_USBHS20,
true,
false,
HID_CUS_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.
usb_examples/device/hid_custom.c
void hid_cus_init(
tru_usb_pc_t *pc,
tru_usb_hid_t *hid,
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_hid_cus_pd_info()->pd,
usb_base_addr,
phy,
speed_sel,
otg_id_pin_supported,
enable_sof_trigger,
dma_mode,
1
);
tru_usb_hid_init(hid, pc, get_hid_cus_pd_info());
hid_cus_cb_setup_buffers();
// Register callback functions
tru_usb_hid_register_cb_reset(hid, cb_reset);
tru_usb_hid_register_cb_enumdone(hid, cb_enumdone);
tru_usb_hid_register_cb_setup(hid, cb_setup);
tru_usb_hid_register_cb_cfg_act(hid, cb_config);
tru_usb_hid_register_cb_epo_act(hid, HID_CUS_OUT_REPORT_EPNUM, cb_output_report_act);
tru_usb_hid_register_cb_epi_act(hid, HID_CUS_IN_REPORT_EPNUM, cb_input_report_act);
tru_usb_hid_register_cb_epo_compl(hid, HID_CUS_OUT_REPORT_EPNUM, cb_output_report_compl);
tru_usb_hid_register_cb_epi_compl(hid, HID_CUS_IN_REPORT_EPNUM, cb_input_report_compl);
}
This function initialises the HID 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 hid_cus_cb_setup_buffers() function is called here for the upper layer to create them.
4. Endpoint transfer buffers
c5soc/usb_examples/device/hid_custom_c5soc.c
// Transfer buffers for chunk transfers
#if defined(HID_CUS_DMA_MODE_CHOICE) && HID_CUS_DMA_MODE_CHOICE == _TRU_DWC2_DMA_MODE_SG
NONCACHEABLE_SECTION static uint8_t output_report_buffer[HID_CUS_SG_NUM_OUTREPORTBUF][HID_CUS_OUTPUT_REPORT_VARCOUNT];
NONCACHEABLE_SECTION static tru_dwc2_dev_sgdma_desc_reg_t output_report_dma_descs[HID_CUS_SG_NUM_OUTREPORTBUF];
#else
NONCACHEABLE_SECTION static uint8_t output_report_buffer[HID_CUS_OUTPUT_REPORT_VARCOUNT];
#endif
// Override weak function
void hid_cus_cb_setup_buffers(void){
tru_usb_pc_epo_t *outrep__ep = &pc.eps.epos[HID_CUS_OUT_REPORT_EPNUM];
#if defined(HID_CUS_DMA_MODE_CHOICE) && HID_CUS_DMA_MODE_CHOICE == _TRU_DWC2_DMA_MODE_SG
// Register transfer buffers
for(uint32_t i = 0; i < HID_CUS_SG_NUM_OUTREPORTBUF; i++){
tru_usb_lxfer_register_buffer(&outrep__ep->lxfer, output_report_buffer[i], sizeof(output_report_buffer[i]));
}
// Register SG DMA descriptor lists
tru_usb_lxfer_register_sgdma_descs(&outrep__ep->lxfer, output_report_dma_descs, sizeof(output_report_dma_descs) / sizeof(tru_dwc2_dev_sgdma_desc_reg_t));
#else
tru_usb_lxfer_register_buffer(&outrep__ep->lxfer, output_report_buffer, sizeof(output_report_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. For HID device class we only need to create buffer for the output report, the input reports are used directly as the endpoint transfer buffer.
5. Registration of activated endpoint callback
An important callback is when the endpoint (EP) is activated, these are registered in the hid_cus_init() function.
usb_examples/device/hid_custom.c
tru_usb_hid_register_cb_epo_act(hid, HID_CUS_OUT_REPORT_EPNUM, cb_output_report_act);
tru_usb_hid_register_cb_epi_act(hid, HID_CUS_IN_REPORT_EPNUM, cb_input_report_act);
The function cb_output_report_act() is registered as the out EP (output report EP) activated callback, and the cb_input_report_act() as the in EP (input report EP) 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/hid_custom.c
void cb_output_report_act(tru_usb_hid_t *hid, tru_usb_pc_epo_t *ep, bool is_activated){
if(is_activated){
#if defined(TRU_CFG_USB_LOG_CALLBACK) && TRU_CFG_USB_LOG_CALLBACK == 1
LOG("CB: EP1 RX ACTIVATED\n");
#endif
// ========================
// Receive an output report
// ========================
tru_usb_pc_receive(hid->pc, ep->num, HID_CUS_OUTPUT_REPORT_VARCOUNT, HID_CUS_OUTPUT_REPORT_VARCOUNT);
}
}
In this example, as soon as the endpoint is activated we want to receive an output report.
7. Transfer complete endpoint callback
Whenever a reception completes the registered transfer complete callback is automatically executed. Here, we want to transmit the next input report and receive another output report.
usb_examples/device/hid_custom.c
void cb_output_report_compl(tru_usb_hid_t *hid, tru_usb_pc_epo_t *ep){
// ========================
// Transmit an input report
// ========================
// Variables
tru_usb_pc_epi_t *epin = tru_hid_get_report_epi_from_epo(hid, ep);
tru_usb_hid_report_info_t *input_report_info = tru_hid_get_next_input_report_info(epin, hid->pc->dma_mode); // Get the next input report. Ensure this is executed only once per transmit
// Put message into the input report buffer
memset(input_report_info->report, 0, input_report_info->size);
TRU_MEMCPY(input_report_info->report, "Hello HID report", 16);
tru_usb_pc_transmit(hid->pc, epin->num, input_report_info->size, input_report_info->size);
#if defined(TRU_CFG_USB_LOG_CALLBACK) && TRU_CFG_USB_LOG_CALLBACK == 1
LOG("CB: EP1 RX COMPL\n");
LOG("CB: TX INPUT REPORT LEN %lu: ", epin->lxfer.whole.total_xfer_size);
for(uint32_t i = 0; i < epin->lxfer.whole.total_xfer_size; i++){
LOG("%.2x", rd8_unaligned((uint8_t *)epin->lxfer.chunk.buffers[0].buf + i));
}
LOG("\n");
#endif
// ========================================
// Prepare to receive another output report
// ========================================
tru_usb_pc_receive(hid->pc, ep->num, HID_CUS_OUTPUT_REPORT_VARCOUNT, HID_CUS_OUTPUT_REPORT_VARCOUNT);
}
Document date: Rev 1: 24 Jul 2026