Last active
May 12, 2024 14:38
-
-
Save HiFiPhile/9ed6f4f65bf1dc5bc0c7626fb7700d8d to your computer and use it in GitHub Desktop.
TinyUSB uac2_speaker_fb minimal example on STM32F723E-DISCO
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* The MIT License (MIT) | |
* | |
* Copyright (c) 2020 Jerzy Kasenberg | |
* Copyright (c) 2023 HiFiPhile | |
* | |
* 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 <stdio.h> | |
#include <string.h> | |
#include "bsp/board_api.h" | |
#include "tusb.h" | |
#include "usb_descriptors.h" | |
#include "common_types.h" | |
#include "stm32f723e_discovery.h" | |
#include "stm32f723e_discovery_audio.h" | |
//--------------------------------------------------------------------+ | |
// MACRO CONSTANT TYPEDEF PROTOTYPES | |
//--------------------------------------------------------------------+ | |
// List of supported sample rates | |
#if defined(__RX__) | |
const uint32_t sample_rates[] = {44100, 48000}; | |
#else | |
const uint32_t sample_rates[] = {44100, 48000, 88200, 96000}; | |
#endif | |
uint32_t current_sample_rate = 44100; | |
#define N_SAMPLE_RATES TU_ARRAY_SIZE(sample_rates) | |
#define FRAME_SIZE (32 * CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX * CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_RX) | |
/* Blink pattern | |
* - 25 ms : streaming data | |
* - 250 ms : device not mounted | |
* - 1000 ms : device mounted | |
* - 2500 ms : device is suspended | |
*/ | |
enum | |
{ | |
BLINK_STREAMING = 25, | |
BLINK_NOT_MOUNTED = 250, | |
BLINK_MOUNTED = 1000, | |
BLINK_SUSPENDED = 2500, | |
}; | |
enum | |
{ | |
VOLUME_CTRL_0_DB = 0, | |
VOLUME_CTRL_10_DB = 2560, | |
VOLUME_CTRL_20_DB = 5120, | |
VOLUME_CTRL_30_DB = 7680, | |
VOLUME_CTRL_40_DB = 10240, | |
VOLUME_CTRL_50_DB = 12800, | |
VOLUME_CTRL_60_DB = 15360, | |
VOLUME_CTRL_70_DB = 17920, | |
VOLUME_CTRL_80_DB = 20480, | |
VOLUME_CTRL_90_DB = 23040, | |
VOLUME_CTRL_100_DB = 25600, | |
VOLUME_CTRL_SILENCE = 0x8000, | |
}; | |
static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED; | |
// Audio controls | |
// Current states | |
int8_t mute[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX + 1]; // +1 for master channel 0 | |
int16_t volume[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX + 1]; // +1 for master channel 0 | |
// Buffer for speaker data | |
uint16_t i2s_buffer[FRAME_SIZE/2]; | |
void led_blinking_task(void); | |
void audio_task(void); | |
#if CFG_AUDIO_DEBUG | |
void audio_debug_task(void); | |
uint8_t current_alt_settings; | |
#endif | |
/*------------- MAIN -------------*/ | |
int main(void) | |
{ | |
board_init(); | |
// 70 volume is very loud ! | |
BSP_AUDIO_OUT_Init(OUTPUT_DEVICE_HEADPHONE, 70, I2S_AUDIOFREQ_48K); | |
BSP_AUDIO_OUT_SetAudioFrameSlot(CODEC_AUDIOFRAME_SLOT_02); | |
// init device stack on configured roothub port | |
tud_init(BOARD_TUD_RHPORT); | |
if (board_init_after_tusb) { | |
board_init_after_tusb(); | |
} | |
TU_LOG1("Speaker running\r\n"); | |
while (1) | |
{ | |
tud_task(); // TinyUSB device task | |
led_blinking_task(); | |
#if CFG_AUDIO_DEBUG | |
audio_debug_task(); | |
#endif | |
} | |
} | |
//--------------------------------------------------------------------+ | |
// 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; | |
BSP_AUDIO_OUT_Stop(CODEC_PDWN_SW); | |
} | |
// 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; | |
BSP_AUDIO_OUT_Stop(CODEC_PDWN_SW); | |
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; | |
} | |
//--------------------------------------------------------------------+ | |
// Application Callback API Implementations | |
//--------------------------------------------------------------------+ | |
/** | |
* @brief Calculates the remaining file size and new position of the pointer. | |
* @param None | |
* @retval None | |
*/ | |
void BSP_AUDIO_OUT_TransferComplete_CallBack(void) | |
{ | |
tud_audio_read((uint8_t*)i2s_buffer + FRAME_SIZE / 2, FRAME_SIZE/2); | |
} | |
/** | |
* @brief Manages the DMA Half Transfer complete interrupt. | |
* @param None | |
* @retval None | |
*/ | |
void BSP_AUDIO_OUT_HalfTransfer_CallBack(void) | |
{ | |
tud_audio_read(i2s_buffer, FRAME_SIZE/2); | |
} | |
// Helper for clock get requests | |
static bool tud_audio_clock_get_request(uint8_t rhport, audio_control_request_t const *request) | |
{ | |
TU_ASSERT(request->bEntityID == UAC2_ENTITY_CLOCK); | |
if (request->bControlSelector == AUDIO_CS_CTRL_SAM_FREQ) | |
{ | |
if (request->bRequest == AUDIO_CS_REQ_CUR) | |
{ | |
TU_LOG1("Clock get current freq %lu\r\n", current_sample_rate); | |
audio_control_cur_4_t curf = { (int32_t) tu_htole32(current_sample_rate) }; | |
return tud_audio_buffer_and_schedule_control_xfer(rhport, (tusb_control_request_t const *)request, &curf, sizeof(curf)); | |
} | |
else if (request->bRequest == AUDIO_CS_REQ_RANGE) | |
{ | |
audio_control_range_4_n_t(N_SAMPLE_RATES) rangef = | |
{ | |
.wNumSubRanges = tu_htole16(N_SAMPLE_RATES) | |
}; | |
TU_LOG1("Clock get %d freq ranges\r\n", N_SAMPLE_RATES); | |
for(uint8_t i = 0; i < N_SAMPLE_RATES; i++) | |
{ | |
rangef.subrange[i].bMin = (int32_t) sample_rates[i]; | |
rangef.subrange[i].bMax = (int32_t) sample_rates[i]; | |
rangef.subrange[i].bRes = 0; | |
TU_LOG1("Range %d (%d, %d, %d)\r\n", i, (int)rangef.subrange[i].bMin, (int)rangef.subrange[i].bMax, (int)rangef.subrange[i].bRes); | |
} | |
return tud_audio_buffer_and_schedule_control_xfer(rhport, (tusb_control_request_t const *)request, &rangef, sizeof(rangef)); | |
} | |
} | |
else if (request->bControlSelector == AUDIO_CS_CTRL_CLK_VALID && | |
request->bRequest == AUDIO_CS_REQ_CUR) | |
{ | |
audio_control_cur_1_t cur_valid = { .bCur = 1 }; | |
TU_LOG1("Clock get is valid %u\r\n", cur_valid.bCur); | |
return tud_audio_buffer_and_schedule_control_xfer(rhport, (tusb_control_request_t const *)request, &cur_valid, sizeof(cur_valid)); | |
} | |
TU_LOG1("Clock get request not supported, entity = %u, selector = %u, request = %u\r\n", | |
request->bEntityID, request->bControlSelector, request->bRequest); | |
return false; | |
} | |
// Helper for clock set requests | |
static bool tud_audio_clock_set_request(uint8_t rhport, audio_control_request_t const *request, uint8_t const *buf) | |
{ | |
(void)rhport; | |
TU_ASSERT(request->bEntityID == UAC2_ENTITY_CLOCK); | |
TU_VERIFY(request->bRequest == AUDIO_CS_REQ_CUR); | |
if (request->bControlSelector == AUDIO_CS_CTRL_SAM_FREQ) | |
{ | |
TU_VERIFY(request->wLength == sizeof(audio_control_cur_4_t)); | |
current_sample_rate = (uint32_t) ((audio_control_cur_4_t const *)buf)->bCur; | |
TU_LOG1("Clock set current freq: %ld\r\n", current_sample_rate); | |
return true; | |
} | |
else | |
{ | |
TU_LOG1("Clock set request not supported, entity = %u, selector = %u, request = %u\r\n", | |
request->bEntityID, request->bControlSelector, request->bRequest); | |
return false; | |
} | |
} | |
// Helper for feature unit get requests | |
static bool tud_audio_feature_unit_get_request(uint8_t rhport, audio_control_request_t const *request) | |
{ | |
TU_ASSERT(request->bEntityID == UAC2_ENTITY_FEATURE_UNIT); | |
if (request->bControlSelector == AUDIO_FU_CTRL_MUTE && request->bRequest == AUDIO_CS_REQ_CUR) | |
{ | |
audio_control_cur_1_t mute1 = { .bCur = mute[request->bChannelNumber] }; | |
TU_LOG1("Get channel %u mute %d\r\n", request->bChannelNumber, mute1.bCur); | |
return tud_audio_buffer_and_schedule_control_xfer(rhport, (tusb_control_request_t const *)request, &mute1, sizeof(mute1)); | |
} | |
else if (request->bControlSelector == AUDIO_FU_CTRL_VOLUME) | |
{ | |
if (request->bRequest == AUDIO_CS_REQ_RANGE) | |
{ | |
audio_control_range_2_n_t(1) range_vol = { | |
.wNumSubRanges = tu_htole16(1), | |
.subrange[0] = { .bMin = tu_htole16(-VOLUME_CTRL_50_DB), tu_htole16(VOLUME_CTRL_0_DB), tu_htole16(256) } | |
}; | |
TU_LOG1("Get channel %u volume range (%d, %d, %u) dB\r\n", request->bChannelNumber, | |
range_vol.subrange[0].bMin / 256, range_vol.subrange[0].bMax / 256, range_vol.subrange[0].bRes / 256); | |
return tud_audio_buffer_and_schedule_control_xfer(rhport, (tusb_control_request_t const *)request, &range_vol, sizeof(range_vol)); | |
} | |
else if (request->bRequest == AUDIO_CS_REQ_CUR) | |
{ | |
audio_control_cur_2_t cur_vol = { .bCur = tu_htole16(volume[request->bChannelNumber]) }; | |
TU_LOG1("Get channel %u volume %d dB\r\n", request->bChannelNumber, cur_vol.bCur / 256); | |
return tud_audio_buffer_and_schedule_control_xfer(rhport, (tusb_control_request_t const *)request, &cur_vol, sizeof(cur_vol)); | |
} | |
} | |
TU_LOG1("Feature unit get request not supported, entity = %u, selector = %u, request = %u\r\n", | |
request->bEntityID, request->bControlSelector, request->bRequest); | |
return false; | |
} | |
// Helper for feature unit set requests | |
static bool tud_audio_feature_unit_set_request(uint8_t rhport, audio_control_request_t const *request, uint8_t const *buf) | |
{ | |
(void)rhport; | |
TU_ASSERT(request->bEntityID == UAC2_ENTITY_FEATURE_UNIT); | |
TU_VERIFY(request->bRequest == AUDIO_CS_REQ_CUR); | |
if (request->bControlSelector == AUDIO_FU_CTRL_MUTE) | |
{ | |
TU_VERIFY(request->wLength == sizeof(audio_control_cur_1_t)); | |
mute[request->bChannelNumber] = ((audio_control_cur_1_t const *)buf)->bCur; | |
TU_LOG1("Set channel %d Mute: %d\r\n", request->bChannelNumber, mute[request->bChannelNumber]); | |
return true; | |
} | |
else if (request->bControlSelector == AUDIO_FU_CTRL_VOLUME) | |
{ | |
TU_VERIFY(request->wLength == sizeof(audio_control_cur_2_t)); | |
volume[request->bChannelNumber] = ((audio_control_cur_2_t const *)buf)->bCur; | |
TU_LOG1("Set channel %d volume: %d dB\r\n", request->bChannelNumber, volume[request->bChannelNumber] / 256); | |
return true; | |
} | |
else | |
{ | |
TU_LOG1("Feature unit set request not supported, entity = %u, selector = %u, request = %u\r\n", | |
request->bEntityID, request->bControlSelector, request->bRequest); | |
return false; | |
} | |
} | |
// Invoked when audio class specific get request received for an entity | |
bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request) | |
{ | |
audio_control_request_t const *request = (audio_control_request_t const *)p_request; | |
if (request->bEntityID == UAC2_ENTITY_CLOCK) | |
return tud_audio_clock_get_request(rhport, request); | |
if (request->bEntityID == UAC2_ENTITY_FEATURE_UNIT) | |
return tud_audio_feature_unit_get_request(rhport, request); | |
else | |
{ | |
TU_LOG1("Get request not handled, entity = %d, selector = %d, request = %d\r\n", | |
request->bEntityID, request->bControlSelector, request->bRequest); | |
} | |
return false; | |
} | |
// Invoked when audio class specific set request received for an entity | |
bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const *p_request, uint8_t *buf) | |
{ | |
audio_control_request_t const *request = (audio_control_request_t const *)p_request; | |
if (request->bEntityID == UAC2_ENTITY_FEATURE_UNIT) | |
return tud_audio_feature_unit_set_request(rhport, request, buf); | |
if (request->bEntityID == UAC2_ENTITY_CLOCK) | |
return tud_audio_clock_set_request(rhport, request, buf); | |
TU_LOG1("Set request not handled, entity = %d, selector = %d, request = %d\r\n", | |
request->bEntityID, request->bControlSelector, request->bRequest); | |
return false; | |
} | |
bool tud_audio_set_itf_close_EP_cb(uint8_t rhport, tusb_control_request_t const * p_request) | |
{ | |
(void)rhport; | |
uint8_t const itf = tu_u16_low(tu_le16toh(p_request->wIndex)); | |
uint8_t const alt = tu_u16_low(tu_le16toh(p_request->wValue)); | |
if (ITF_NUM_AUDIO_STREAMING == itf && alt == 0) | |
{ | |
BSP_AUDIO_OUT_Stop(CODEC_PDWN_SW); | |
memset(i2s_buffer, 0, sizeof(i2s_buffer)); | |
blink_interval_ms = BLINK_MOUNTED; | |
} | |
return true; | |
} | |
bool tud_audio_set_itf_cb(uint8_t rhport, tusb_control_request_t const * p_request) | |
{ | |
(void)rhport; | |
uint8_t const itf = tu_u16_low(tu_le16toh(p_request->wIndex)); | |
uint8_t const alt = tu_u16_low(tu_le16toh(p_request->wValue)); | |
TU_LOG2("Set interface %d alt %d\r\n", itf, alt); | |
if (ITF_NUM_AUDIO_STREAMING == itf && alt != 0) | |
blink_interval_ms = BLINK_STREAMING; | |
BSP_AUDIO_OUT_SetFrequency(current_sample_rate); | |
BSP_AUDIO_OUT_Play(i2s_buffer, FRAME_SIZE); | |
BSP_AUDIO_OUT_Resume(); | |
#if CFG_AUDIO_DEBUG | |
current_alt_settings = alt; | |
#endif | |
return true; | |
} | |
void tud_audio_feedback_params_cb(uint8_t func_id, uint8_t alt_itf, audio_feedback_params_t* feedback_param) | |
{ | |
(void)func_id; | |
(void)alt_itf; | |
// Set feedback method to fifo counting | |
feedback_param->method = AUDIO_FEEDBACK_METHOD_FIFO_COUNT; | |
feedback_param->sample_freq = current_sample_rate; | |
} | |
//--------------------------------------------------------------------+ | |
// BLINKING TASK | |
//--------------------------------------------------------------------+ | |
void led_blinking_task(void) | |
{ | |
static uint32_t start_ms = 0; | |
static bool led_state = false; | |
// Blink every interval ms | |
if (board_millis() - start_ms < blink_interval_ms) return; | |
start_ms += blink_interval_ms; | |
board_led_write(led_state); | |
led_state = 1 - led_state; | |
} | |
#if CFG_AUDIO_DEBUG | |
//--------------------------------------------------------------------+ | |
// HID interface for audio debug | |
//--------------------------------------------------------------------+ | |
static uint32_t fifo_count_avg; | |
// Every 1ms, we will sent 1 debug information report | |
void audio_debug_task(void) | |
{ | |
static uint32_t start_ms = 0; | |
uint32_t curr_ms = board_millis(); | |
if ( start_ms == curr_ms ) return; // not enough time | |
start_ms = curr_ms; | |
uint16_t fifo_count = tud_audio_available(); | |
// Same averaging method used in UAC2 class | |
fifo_count_avg = (uint32_t)(((uint64_t)fifo_count_avg * 63 + ((uint32_t)fifo_count << 16)) >> 6); | |
audio_debug_info_t debug_info; | |
debug_info.sample_rate = current_sample_rate; | |
debug_info.alt_settings = current_alt_settings; | |
debug_info.fifo_size = CFG_TUD_AUDIO_FUNC_1_EP_OUT_SW_BUF_SZ; | |
debug_info.fifo_count = fifo_count; | |
debug_info.fifo_count_avg = fifo_count_avg >> 16; | |
for (int i = 0; i < CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX + 1; i++) | |
{ | |
debug_info.mute[i] = mute[i]; | |
debug_info.volume[i] = volume[i]; | |
} | |
if(tud_hid_ready()) | |
tud_hid_report(0, &debug_info, sizeof(debug_info)); | |
} | |
// Invoked when received GET_REPORT control request | |
// Unused here | |
uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) | |
{ | |
// TODO not Implemented | |
(void) itf; | |
(void) report_id; | |
(void) report_type; | |
(void) buffer; | |
(void) reqlen; | |
return 0; | |
} | |
// Invoked when received SET_REPORT control request or | |
// Unused here | |
void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) | |
{ | |
// This example doesn't use multiple report and report ID | |
(void) itf; | |
(void) report_id; | |
(void) report_type; | |
(void) buffer; | |
(void) bufsize; | |
} | |
#endif | |
extern SAI_HandleTypeDef haudio_out_sai; | |
/** | |
* @brief This function handles DMA2 Stream 4 interrupt request. | |
* @param None | |
* @retval None | |
*/ | |
void AUDIO_OUT_SAIx_DMAx_IRQHandler(void) | |
{ | |
HAL_DMA_IRQHandler(haudio_out_sai.hdmatx); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
External dependencies:
-wm8994.c/h