diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 32cceba..07f0bdd 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -20,7 +20,6 @@ jobs: name: Linux cache-key: linux cmake-args: '-DPIMORONI_PICO_PATH=$GITHUB_WORKSPACE/pimoroni-pico -DPICO_SDK_PATH=$GITHUB_WORKSPACE/pico-sdk' - apt-packages: clang-tidy gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib runs-on: ${{matrix.os}} @@ -60,11 +59,10 @@ jobs: ref: sdk-2.0.0 submodules: true - # Linux deps - - name: Install deps - if: runner.os == 'Linux' - run: | - sudo apt update && sudo apt install ${{matrix.apt-packages}} + - name: Install Arm GNU Toolchain (arm-none-eabi-gcc) + uses: carlosperate/arm-none-eabi-gcc-action@v1 + with: + release: '12.2.Rel1' - name: Create Build Environment run: cmake -E make_directory ${{runner.workspace}}/build diff --git a/CMakeLists.txt b/CMakeLists.txt index 067f0c1..4d91391 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,9 +25,9 @@ include(drivers/button/button) add_executable(${NAME}) target_sources(${NAME} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c - ${CMAKE_CURRENT_SOURCE_DIR}/src/i2s_audio.c - ${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/i2s_audio.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/board.cpp ) 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/reset-picade-audio.py b/reset-picade-audio.py new file mode 100644 index 0000000..8c74aee --- /dev/null +++ b/reset-picade-audio.py @@ -0,0 +1,8 @@ +import glob +import serial + +picade = glob.glob("/dev/serial/by-id/usb-Pimoroni_Picade_USB_Audio_*")[0] + +device = serial.Serial(picade) + +device.write(b"multiverse:_usb") \ No newline at end of file diff --git a/src/board.cpp b/src/board.cpp index b5b14b9..0c42de7 100644 --- a/src/board.cpp +++ b/src/board.cpp @@ -20,52 +20,51 @@ Button button(BUTTON, pimoroni::ACTIVE_LOW, 0); bool pressed = false; absolute_time_t pressed_time; -extern "C" { - void system_init() { - // Apply a modest overvolt, default is 1.10v. - // this is required for a stable 250MHz on some RP2040s - vreg_set_voltage(VREG_VOLTAGE_1_20); - sleep_ms(10); - set_sys_clock_khz(250000, true); - // DCDC PSM control - // 0: PFM mode (best efficiency) - // 1: PWM mode (improved ripple) - gpio_init(PIN_DCDC_PSM_CTRL); - gpio_set_dir(PIN_DCDC_PSM_CTRL, GPIO_OUT); - gpio_put(PIN_DCDC_PSM_CTRL, 1); // PWM mode for less Audio noise +void system_init() { + // Apply a modest overvolt, default is 1.10v. + // this is required for a stable 250MHz on some RP2040s + vreg_set_voltage(VREG_VOLTAGE_1_20); + sleep_ms(10); + set_sys_clock_khz(250000, true); - volume_control.init(); - } + // DCDC PSM control + // 0: PFM mode (best efficiency) + // 1: PWM mode (improved ripple) + gpio_init(PIN_DCDC_PSM_CTRL); + gpio_set_dir(PIN_DCDC_PSM_CTRL, GPIO_OUT); + gpio_put(PIN_DCDC_PSM_CTRL, 1); // PWM mode for less Audio noise - int32_t get_volume_delta() { - return volume_control.delta(); - } + volume_control.init(); +} - bool get_mute_button_pressed() { - return button.read(); - } +int32_t get_volume_delta() { + return volume_control.delta(); +} + +bool get_mute_button_pressed() { + return button.read(); +} - void handle_mute_button_held() { +void handle_mute_button_held() { #ifdef DEBUG_BOOTLOADER_SHORTCUT - bool current = button.raw(); - if(current && pressed) { - if (absolute_time_diff_us(pressed_time, get_absolute_time()) >= 2000000ul) { - sleep_ms(500); - save_and_disable_interrupts(); - rosc_hw->ctrl = ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB; - reset_usb_boot(0, 0); - } - } else if (current) { - pressed_time = get_absolute_time(); - pressed = true; - } else { - pressed = false; + bool current = button.raw(); + if(current && pressed) { + if (absolute_time_diff_us(pressed_time, get_absolute_time()) >= 2000000ul) { + sleep_ms(500); + save_and_disable_interrupts(); + rosc_hw->ctrl = ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB; + reset_usb_boot(0, 0); } -#endif + } else if (current) { + pressed_time = get_absolute_time(); + pressed = true; + } else { + pressed = false; } +#endif +} - void system_led(uint8_t r, uint8_t g, uint8_t b) { - rgbled.set_rgb(r, g, b); - } +void system_led(uint8_t r, uint8_t g, uint8_t b) { + rgbled.set_rgb(r, g, b); } \ No newline at end of file diff --git a/src/i2s_audio.c b/src/i2s_audio.cpp similarity index 100% rename from src/i2s_audio.c rename to src/i2s_audio.cpp index bd232f5..f1554ea 100644 --- a/src/i2s_audio.c +++ b/src/i2s_audio.cpp @@ -18,8 +18,8 @@ void i2s_audio_init() { // initialize for 48k we allow changing later static audio_format_t audio_format_48k = { - .format = AUDIO_BUFFER_FORMAT_PCM_S16, .sample_freq = 48000, + .format = AUDIO_BUFFER_FORMAT_PCM_S16, .channel_count = 2, }; diff --git a/src/main.c b/src/main.cpp similarity index 78% rename from src/main.c rename to src/main.cpp index 736ce46..85af5ad 100644 --- a/src/main.c +++ b/src/main.cpp @@ -25,6 +25,7 @@ #include #include +#include #include // MIN and MAX #include "bsp/board_api.h" @@ -34,6 +35,13 @@ #include "board_config.h" #include "board.h" +#include "hardware/clocks.h" +#include "hardware/vreg.h" +#include "pico/bootrom.h" +#include "hardware/structs/rosc.h" +#include "hardware/watchdog.h" +#include "pico/timeout_helper.h" + // Approximate exponential volume ramp - (n / 64) ^ 4 // Tested with pure square for perceptual loudness. const uint8_t volume_ramp[] = { @@ -84,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 @@ -99,42 +111,107 @@ const uint8_t resolutions_per_format[CFG_TUD_AUDIO_FUNC_1_N_FORMATS] = {CFG_TUD_ // Current resolution, update on format change uint8_t current_resolution; -void led_blinking_task(void); + +const size_t MAX_UART_PACKET = 64; + +const size_t COMMAND_LEN = 4; +uint8_t command_buffer[COMMAND_LEN]; +std::string_view command((const char *)command_buffer, COMMAND_LEN); + + +void led_task(void); void audio_task(void); void usb_serial_init(void); +uint cdc_task(uint8_t *buf, size_t buf_len); + +uint cdc_task(uint8_t *buf, size_t buf_len) { + + if (tud_cdc_connected()) { + if (tud_cdc_available()) { + return tud_cdc_read(buf, buf_len); + } + } + + return 0; +} + +bool cdc_wait_for(std::string_view data, uint timeout_ms=50) { + timeout_state ts; + absolute_time_t until = delayed_by_ms(get_absolute_time(), timeout_ms); + check_timeout_fn check_timeout = init_single_timeout_until(&ts, until); + + for(auto expected_char : data) { + char got_char; + while(1){ + tud_task(); + if (cdc_task((uint8_t *)&got_char, 1) == 1) break; + if(check_timeout(&ts, false)) return false; + } + if (got_char != expected_char) return false; + } + return true; +} + +size_t cdc_get_bytes(const uint8_t *buffer, const size_t len, const uint timeout_ms=1000) { + memset((void *)buffer, len, 0); + + uint8_t *p = (uint8_t *)buffer; + + timeout_state ts; + absolute_time_t until = delayed_by_ms(get_absolute_time(), timeout_ms); + check_timeout_fn check_timeout = init_single_timeout_until(&ts, until); + + size_t bytes_remaining = len; + while (bytes_remaining && !check_timeout(&ts, false)) { + tud_task(); // tinyusb device task + size_t bytes_read = cdc_task(p, std::min(bytes_remaining, MAX_UART_PACKET)); + bytes_remaining -= bytes_read; + p += bytes_read; + } + return len - bytes_remaining; +} + +void serial_task(void) { + if (tud_cdc_connected()) { + if (tud_cdc_available()) { + if(!cdc_wait_for("multiverse:")) { + return; // Couldn't get 16 bytes of command + } + + if(cdc_get_bytes(command_buffer, COMMAND_LEN) != COMMAND_LEN) { + //display::info("cto"); + return; + } + + if(command == "_rst") { + sleep_ms(500); + save_and_disable_interrupts(); + rosc_hw->ctrl = ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB; + watchdog_reboot(0, 0, 0); + return; + } + + if(command == "_usb") { + sleep_ms(500); + save_and_disable_interrupts(); + rosc_hw->ctrl = ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB; + reset_usb_boot(0, 0); + return; + } + } + } +} /*------------- MAIN -------------*/ 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 @@ -143,14 +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(); - //led_blinking_task(); - //i2s_audio_give_buffer(NULL, 0); + serial_task(); + led_task(); } } @@ -270,10 +347,9 @@ static bool tud_audio_feature_unit_get_request(uint8_t rhport, audio_control_req { 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_0_DB), tu_htole16(VOLUME_CTRL_100_DB), tu_htole16(256) } - }; + audio_control_range_2_n_t(1) range_vol; + range_vol.wNumSubRanges = tu_htole16(1); + range_vol.subrange[0] = { .bMin = tu_htole16(VOLUME_CTRL_0_DB), tu_htole16(VOLUME_CTRL_100_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)); @@ -292,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; @@ -307,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; } @@ -321,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); @@ -437,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]) { @@ -465,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 @@ -488,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) { @@ -510,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 @@ -536,27 +592,29 @@ 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; } } - //--------------------------------------------------------------------+ // 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); } diff --git a/src/tusb_config.h b/src/tusb_config.h index 8cf358f..94b63dd 100644 --- a/src/tusb_config.h +++ b/src/tusb_config.h @@ -94,7 +94,7 @@ extern "C" { #endif //------------- CLASS -------------// -#define CFG_TUD_CDC 0 +#define CFG_TUD_CDC 1 #define CFG_TUD_MSC 0 #define CFG_TUD_HID 0 #define CFG_TUD_MIDI 0 diff --git a/src/usb_descriptors.c b/src/usb_descriptors.cpp similarity index 91% rename from src/usb_descriptors.c rename to src/usb_descriptors.cpp index 0be06e0..d3cb4a6 100644 --- a/src/usb_descriptors.c +++ b/src/usb_descriptors.cpp @@ -73,12 +73,16 @@ void usb_serial_init(void) { //--------------------------------------------------------------------+ // Configuration Descriptor //--------------------------------------------------------------------+ -#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_AUDIO * TUD_AUDIO_HEADSET_STEREO_DESC_LEN) +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_AUDIO * TUD_AUDIO_HEADSET_STEREO_DESC_LEN + CFG_TUD_CDC * TUD_CDC_DESC_LEN) #define EPNUM_AUDIO_IN 0x01 #define EPNUM_AUDIO_OUT 0x01 #define EPNUM_AUDIO_INT 0x02 +#define EPNUM_CDC_NOTIF 0x83 +#define EPNUM_CDC_OUT 0x04 +#define EPNUM_CDC_IN 0x84 + uint8_t const desc_configuration[] = { @@ -86,7 +90,10 @@ uint8_t const desc_configuration[] = TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), // Interface number, string index, EP Out & EP In address, EP size - TUD_AUDIO_HEADSET_STEREO_DESCRIPTOR(2, EPNUM_AUDIO_OUT, EPNUM_AUDIO_IN | 0x80, EPNUM_AUDIO_INT | 0x80) + TUD_AUDIO_HEADSET_STEREO_DESCRIPTOR(2, EPNUM_AUDIO_OUT, EPNUM_AUDIO_IN | 0x80, EPNUM_AUDIO_INT | 0x80), + + // CDC: Interface number, string index, EP notification address and size, EP data address (out, in) and size. + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 5, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64) }; // Invoked when received GET CONFIGURATION DESCRIPTOR @@ -110,6 +117,7 @@ char const* string_desc_arr [] = "Picade USB Audio", // 2: Product usb_serial, // 3: Serials, should use chip ID "Speakers", // 4: Audio Interface + "CDC", // 5: CDC Serial Interface }; static uint16_t _desc_str[32 + 1]; diff --git a/src/usb_descriptors.h b/src/usb_descriptors.h index a9a1957..984a1b0 100644 --- a/src/usb_descriptors.h +++ b/src/usb_descriptors.h @@ -39,9 +39,13 @@ enum { ITF_NUM_AUDIO_CONTROL = 0, ITF_NUM_AUDIO_STREAMING_SPK, + ITF_NUM_CDC, + ITF_NUM_CDC_INT, ITF_NUM_TOTAL }; +#define ITF_NUM_AUDIO_TOTAL (ITF_NUM_TOTAL - 2) + #define TUD_AUDIO_HEADSET_STEREO_DESC_LEN (TUD_AUDIO_DESC_IAD_LEN\ + TUD_AUDIO_DESC_STD_AC_LEN\ + TUD_AUDIO_DESC_CS_AC_LEN\ @@ -68,7 +72,7 @@ enum #define TUD_AUDIO_HEADSET_STEREO_DESCRIPTOR(_stridx, _epout, _epin, _epint) \ /* Standard Interface Association Descriptor (IAD) */\ - TUD_AUDIO_DESC_IAD(/*_firstitfs*/ ITF_NUM_AUDIO_CONTROL, /*_nitfs*/ ITF_NUM_TOTAL, /*_stridx*/ 0x00),\ + TUD_AUDIO_DESC_IAD(/*_firstitfs*/ ITF_NUM_AUDIO_CONTROL, /*_nitfs*/ ITF_NUM_AUDIO_TOTAL, /*_stridx*/ 0x00),\ /* Standard AC Interface Descriptor(4.7.1) */\ TUD_AUDIO_DESC_STD_AC(/*_itfnum*/ ITF_NUM_AUDIO_CONTROL, /*_nEPs*/ 0x01, /*_stridx*/ _stridx),\ /* Class-Specific AC Interface Header Descriptor(4.7.2) */\