diff --git a/README.md b/README.md index fbbb7e7..2a1021b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,23 @@ -# Pico TinyUSB i2s Speaker +# Picade Max: Audio And Volume Control Board -A USB speaker firmware for the RP2040/Pico using TinyUSB and i2s. +Firmware for the Picade Max audio board. Includes encoder volume control with +push-button mute. + +## Status LED + +* Red - Lights up when board is muted +* Green - Should idle blink at 1s intervals, and flash quickly when audio is streaming +* Blue - Brightness indicates volume ## Updating the firmware for the board Push the volume button in for 2 seconds and hold. +Alternatively run `echo "multiverse:_usb" > /dev/serial/by-id/usb-Pimoroni_Picade_USB_Audio_*-if02` via SSH or a terminal. + This will put the audio board into bootloader mode. -If you're using Recalbox, you may see a pop up that states a new USB device named RPI-RP2 has been discovered and asks you if you wish to initialise. You can ignore this screen. +If you're using Recalbox, you may see a pop up that states a new USB device named RPI-RP2 has been discovered and asks you if you wish to initialise. You can ignore this screen. If you don't have any other external USB devices plugged in, you should be able to access the bootloader at: diff --git a/src/main.cpp b/src/main.cpp index 50d30b6..85af5ad 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -92,6 +92,10 @@ static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED; int system_volume = 255; int volume_speed = 10; +uint8_t led_red = 0; +uint8_t led_green = 0; +uint8_t led_blue = 0; + // Audio controls // Current states int8_t mute[CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_RX + 1]; // +1 for master channel 0 @@ -115,7 +119,7 @@ uint8_t command_buffer[COMMAND_LEN]; std::string_view command((const char *)command_buffer, COMMAND_LEN); -void led_blinking_task(void); +void led_task(void); void audio_task(void); void usb_serial_init(void); uint cdc_task(uint8_t *buf, size_t buf_len); @@ -202,34 +206,12 @@ void serial_task(void) { int main(void) { -/* - gpio_init(LED_R); - gpio_set_function(LED_R, GPIO_FUNC_SIO); - gpio_set_dir(LED_R, GPIO_OUT); - gpio_put(LED_R, 1); - - gpio_init(LED_G); - gpio_set_function(LED_G, GPIO_FUNC_SIO); - gpio_set_dir(LED_G, GPIO_OUT); - gpio_put(LED_G, 1); - - gpio_init(LED_B); - gpio_set_function(LED_B, GPIO_FUNC_SIO); - gpio_set_dir(LED_B, GPIO_OUT); - gpio_put(LED_B, 1); -*/ - - /* HACK: To allow testing on a basic Pico with a button on Pin 12 - gpio_init(PICO_UNICORN_A); - gpio_set_function(PICO_UNICORN_A, GPIO_FUNC_SIO); - gpio_set_dir(PICO_UNICORN_A, GPIO_IN); - gpio_pull_up(PICO_UNICORN_A); - */ - system_init(); board_init(); + // Fetch the Pico serial (actually the flash chip ID) into `usb_serial` + // This has nothing to do with CDC serial! usb_serial_init(); // init device stack on configured roothub port @@ -238,15 +220,14 @@ int main(void) i2s_audio_init(); i2s_audio_start(); - TU_LOG1("Headset running\r\n"); + TU_LOG1("Picade Max Audio Running\r\n"); while (1) { - tud_task(); // TinyUSB device task + tud_task(); audio_task(); serial_task(); - //led_blinking_task(); - //i2s_audio_give_buffer(NULL, 0); + led_task(); } } @@ -387,6 +368,7 @@ static bool tud_audio_feature_unit_get_request(uint8_t rhport, audio_control_req } // Helper for feature unit set requests +// This handles volume control and mute requests coming from the USB host to Picade Max Audio static bool tud_audio_feature_unit_set_request(uint8_t rhport, audio_control_request_t const *request, uint8_t const *buf) { (void)rhport; @@ -402,11 +384,8 @@ static bool tud_audio_feature_unit_set_request(uint8_t rhport, audio_control_req TU_LOG1("Set channel %d Mute: %d\r\n", request->bChannelNumber, mute[request->bChannelNumber]); - if(mute[request->bChannelNumber]) { - system_led(255, 0, 0); - } else { - system_led(0, 0, 0); - } + // Set the red LED channel to indicate mute + led_red = mute[request->bChannelNumber] ? 255 : 0; return true; } @@ -416,7 +395,8 @@ static bool tud_audio_feature_unit_set_request(uint8_t rhport, audio_control_req volume[request->bChannelNumber] = tu_le16toh(((audio_control_cur_2_t const *)buf)->bCur); - system_led(0, 0, MIN(255, volume[request->bChannelNumber] / 100)); + // Set the blue LED channel to indicate volume + led_blue = MIN(255, volume[request->bChannelNumber] / 100); system_volume = MIN(255u, volume[request->bChannelNumber] / 100); @@ -532,24 +512,9 @@ void audio_task(void) static uint32_t start_ms = 0; uint32_t volume_interval_ms = 50; - // When new data arrived, copy data from speaker buffer, to microphone buffer - // and send it over - // Only support speaker & headphone both have the same resolution - // If one is 16bit another is 24bit be care of LOUD noise ! if (spk_data_size) { - //led_b_state = !led_b_state; - //gpio_put(LED_B, led_b_state); - // "Hardware" volume is 0 - 100 in steps of 256, with a maximum value of 25600 - - //unsigned int current_volume = (unsigned int)volume[0] * volume_ramp[(uint8_t)system_volume] / 25600; - - /*int current_volume = volume[0] / 100; - if (current_volume > 256) current_volume = 256; - if (current_volume < 0) current_volume = 0; - current_volume = volume_ramp[current_volume];*/ - int current_volume = volume_ramp[system_volume]; if (mute[0]) { @@ -560,18 +525,29 @@ void audio_task(void) spk_data_size = 0; } + // Only handle volume control changes every volume_interval_ms + // The encoder driver should - I believe - asynchronously gather a delta to be handled here if (board_millis() - start_ms >= volume_interval_ms) { + // This is just the raw delta from the encoder int32_t volume_delta = get_volume_delta(); + // Adjust the speed of volume control (number of volume steps per encoder turn) + volume_delta *= volume_speed; + // Long press triggers reset to bootloader handle_mute_button_held(); if(get_mute_button_pressed()) { + // Toggle one channel and copy the mute value to the other + // We don't want to end up with one muted and one unmuted somehow... mute[0] = !mute[0]; - mute[1] = !mute[1]; + mute[1] = mute[0]; + + // Illuminate the LED red if muted + led_red = mute[0] ? 255 : 0; - // Mute was changed + // Mute was changed - notify the host with an interrupt // 6.1 Interrupt Data Message const audio_interrupt_data_t data = { .bInfo = 0, // Class-specific interrupt, originated from an interface @@ -583,20 +559,13 @@ void audio_task(void) }; tud_audio_int_write(&data); + // Call tud_task to handle the interrupt to host tud_task(); } - /*if(volume_delta > 0) { - system_led(0, 255, 0); - } else if (volume_delta < 0) { - system_led(0, 0, 255); - } else { - system_led(0, 0, 0); - }*/ - - volume_delta *= volume_speed; int old_system_volume = system_volume; + if(volume_delta + system_volume > 255) { system_volume = 255; } else if (volume_delta + system_volume < 0) { @@ -605,21 +574,13 @@ void audio_task(void) system_volume += volume_delta; } - /* HACK: To allow testing on a basic Pico with a button on Pin 12 - if(!gpio_get(PICO_UNICORN_A)) { - system_volume ++; - system_volume %= 255; - volume_delta = 1; - } - */ - if(system_volume != old_system_volume) { - system_led(0, system_volume, 0); + led_blue = system_volume; volume[0] = system_volume * 100; volume[1] = system_volume * 100; - // Volume has changed + // Volume has changed - notify the host with an interrupt // 6.1 Interrupt Data Message const audio_interrupt_data_t data = { .bInfo = 0, // Class-specific interrupt, originated from an interface @@ -631,6 +592,8 @@ void audio_task(void) }; tud_audio_int_write(&data); + // Call tud_task to handle the interrupt to host + tud_task(); } start_ms += volume_interval_ms; @@ -640,17 +603,18 @@ void audio_task(void) //--------------------------------------------------------------------+ // BLINKING TASK //--------------------------------------------------------------------+ -void led_blinking_task(void) +void led_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; + if (board_millis() - start_ms >= blink_interval_ms) { + start_ms += blink_interval_ms; + + led_green = led_state ? 64 : 0; + led_state = !led_state; + } - //board_led_write(led_state); - //gpio_put(LED_G, led_state); - system_led(255 * led_state, 0, 0); - led_state = 1 - led_state; + system_led(led_red, led_green, led_blue); }