本篇文章介紹 使用Edge Impulse 語音機器學習,在Raspberry Pi Pico(RP2040) MCU上執行 Voice recognition,用來控制一輛遙控小車。
使用元件:
一. 控制端:
- Raspberry Pi Pico
- INMP441 I2S microphone
- SD card module
- nRF24L01
二、受控端:
- 向前進(forward)
- 往後退(backward)
- 停在原地(stop)
- 方向左轉(turn left)
- 方向右轉(turn right)
- 速度加快(speed up)
- 速度減慢(slow down)
Edge Impulse 本專案使用參數:
- Create Impulse
- Performance calibration
機器學習訓練過程請參閱成果影片的動態說明。
成果影片:
程式碼:
控制端:
有關nRF24L01, pico_audio_recorder, storage_driver等library使用起參閱前一篇文章:MCU(RP2040) voice recognition/voice commands using TinyML(Edge Impulse)
- pico_tinyML_voice.cpp
#include <stdlib.h> #include <stdio.h> #include <sttring.h> #include "pico/stdlib.h" #include "bsp/board.h" #include "tusb.h" #include "pico_storage.h" #include "hardware/rtc.h" #include "pico_audio_recorder.h" #include "hardware/dma.h" #include "nRF24L01.h" #include "edge-impulse-sdk/classifier/ei_run_classifier.h" #include "edge-impulse-sdk/classifier/ei_classifier_smooth.h" extern bool new_read_data; extern int16_t *fw_ptr; #if DATA_ACQUISITION #else int raw_feature_get_data(size_t offset, size_t length, float *out_ptr) { numpy::int16_to_float(fw_ptr+offset, out_ptr, length); return 0; } #endif //=== nRF24L01 irq char const *addr[5] = {"0node", "1node", "2node","3node","4node"}; #define TX_PIN 17 void irq_callback(uint8_t event_type, uint8_t datapipe, uint8_t* data, uint8_t width) { static uint32_t ack_payload=0; uint8_t ack_buff[15]; switch(event_type) { case EVENT_RX_DR: data[width]='\0'; printf("RECV== pipe:%d, width:%d, %s\n", datapipe, width, data); break; case EVENT_TX_DS: gpio_put(TX_PIN, !gpio_get(TX_PIN)); //printf("event_data_sent:%d\n", datapipe); break; case EVENT_MAX_RT: printf("event_max_rt:%d, \n", datapipe); break; } } /*------------- MAIN -------------*/ int main(void) { stdio_init_all(); #if DATA_ACQUISITION rtc_init(); datetime_t t = { .year=2024, .month=02, .day=12, .dotw=6, .hour=10, .min=32, .sec=50 }; if (!rtc_set_datetime(&t)) printf("set rtc error\n"); storage_driver_init(); board_init(); // init device stack on configured roothub port tud_init(BOARD_TUD_RHPORT); #else gpio_init(TX_PIN); gpio_set_dir(TX_PIN,true); // == nRF24L01 init nRF24_spi_default_init(8, 9, irq_callback); nRF24_config_mode(TRANSMITTER); nRF24_enable_feature(FEATURE_EN_DPL, true); nRF24_set_TX_addr((uint8_t*)addr[0], 5); nRF24_set_RX_addr(0, (uint8_t*)addr[0], 5); nRF24_enable_data_pipe_dynamic_payload_length(0, true); //== edge impulse run_classifier_init(); EI_IMPULSE_ERROR res; ei_impulse_result_t result = {nullptr}; signal_t features_signal; features_signal.total_length = EI_CLASSIFIER_SLICE_SIZE; features_signal.get_data = &raw_feature_get_data; ei_classifier_smooth_t smooth; ei_classifier_smooth_init(&smooth, 4, 3, 0.9, 0.3); #endif inmp441_pio_init(INMP441_pio, INMP441_SM, INMP441_SD, INMP441_SCK, INMP441_SAMPLE_RATE); uint8_t f_idx=0; while (1) { #if DATA_ACQUISITION if (get_recorder_state() == STATE_START_RECORDING) { inmp441_starting_recording_to_file_wav(); } tud_task(); // tinyusb device task #else if (new_read_data) { new_read_data = false; //absolute_time_t t1 = get_absolute_time(); res = run_classifier_continuous(&features_signal, &result, false,true ); if (res != EI_IMPULSE_OK) { ei_printf("ERR: Failed to run classifier (%d)\n", res); } //display_results(&result); printf("\n"); // only for debug ei_classifier_smooth_update(&smooth, &result); for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) { //result.classification[i].value = run_moving_average_filter(&classifier_maf[ix], result.classification[i].value); if (result.classification[i].value > 0.9f) { // for debug //ei_printf(" %s: ", ei_classifier_inferencing_categories[i]); //ei_printf("%.5f\r\n", result.classification[i].value); // for running nRF24_write_payload((uint8_t*)ei_classifier_inferencing_categories[i], strlen(ei_classifier_inferencing_categories[i])); if (strcmp(ei_classifier_inferencing_categories[i], "noise")!=0) run_classifier_init(); break; } } //absolute_time_t t2 = get_absolute_time(); //printf("time:%lld\n", absolute_time_diff_us(t1,t2)); //printf("\n"); } #endif tight_loop_contents(); } return 0; }
受控端(小車)
- pico_tinyML_voice_RC_CAR.c
#include <stdio.h> #include "pico/stdlib.h" #include "nRF24L01.h" #include "string.h" #include "hardware/pwm.h" #include "ws2812.h" #define CAR_PIN1A 16 #define CAR_PIN1B 17 #define CAR_PIN2A 18 #define CAR_PIN2B 19 #define SPEED_MAX 5 uint slice1a, slice1b, slice2a, slice2b; uint chan_1a,chan_1b,chan_2a,chan_2b; uint8_t addr[5][5] = {"0node", "1node", "2node","3node","4node"}; enum { NOISE=0, FORWARD, BACKWARD, STOP, TURN_LEFT, TURN_RIGHT, SPEED_UP, SLOW_DOWN, ACTION_MAX, }; int8_t speed=0; uint16_t pwm_top_count[SPEED_MAX+1] = {0, 4000,8000,12000,16000,20000}; uint32_t ws2812_color[SPEED_MAX+1] = {0x0, 0xFF0000, 0xFF0000, 0x8FFF00, 0x8FFF00, 0xFF00}; uint8_t voice_state = 0; uint8_t current_direction=STOP; #define NUM_PIXELS 5 void play_ws2812() { for (int j=0; j < speed; j++) { put_grb_pixel(ws2812_color[j+1]); } for (int j=speed; j < SPEED_MAX; j++) { put_grb_pixel(ws2812_color[0]); } //sleep_ms(100); //printf("speed:%d\n", speed); } //nRF24L01 irq callback void irq_callback(uint8_t event_type, uint8_t datapipe, uint8_t* data, uint8_t width) { static uint32_t ack_payload=0; uint8_t ack_buff[15]; switch(event_type) { case EVENT_RX_DR: data[width]='\0'; if (strcmp(data, "noise")==0) voice_state=NOISE; if (strcmp(data, "forward")==0) voice_state=FORWARD; if (strcmp(data, "backward")==0) voice_state=BACKWARD; if (strcmp(data, "stop")==0) voice_state=STOP; if (strcmp(data, "turn_left")==0) voice_state=TURN_LEFT; if (strcmp(data, "turn_right")==0) voice_state=TURN_RIGHT; if (strcmp(data, "speed_up")==0) voice_state=SPEED_UP; if (strcmp(data, "slow_down")==0) voice_state=SLOW_DOWN; printf("RECV== stat:%d, width:%d, %s\n", voice_state, width, data); break; case EVENT_TX_DS: printf("event_data_sent:%d\n", datapipe); break; case EVENT_MAX_RT: printf("EVENT_MAX_RT:%d\n", datapipe); break; } } void voice_action(uint8_t stat) { switch(stat) { case FORWARD: pwm_set_chan_level(slice1a, chan_1a, pwm_top_count[speed]); pwm_set_chan_level(slice2a, chan_2a, pwm_top_count[speed]); pwm_set_chan_level(slice1b, chan_1b, 0); pwm_set_chan_level(slice2b, chan_2b, 0); current_direction = FORWARD; break; case BACKWARD: pwm_set_chan_level(slice1a, chan_1a, 0); pwm_set_chan_level(slice2a, chan_2a, 0); pwm_set_chan_level(slice1b, chan_1b, pwm_top_count[speed]); pwm_set_chan_level(slice2b, chan_2b, pwm_top_count[speed]); current_direction = BACKWARD; break; case STOP: pwm_set_chan_level(slice1a, chan_1a, 0); pwm_set_chan_level(slice2a, chan_2a, 0); pwm_set_chan_level(slice1b, chan_1b, 0); pwm_set_chan_level(slice2b, chan_2b, 0); current_direction=STOP; break; case TURN_LEFT: pwm_set_chan_level(slice1a, chan_1a, 0); pwm_set_chan_level(slice2a, chan_2a, 0); pwm_set_chan_level(slice1b, chan_1b, 0); pwm_set_chan_level(slice2b, chan_2b, 0); pwm_set_chan_level(slice1a, chan_1a, pwm_top_count[speed]); sleep_ms(500); break; case TURN_RIGHT: pwm_set_chan_level(slice1a, chan_1a, 0); pwm_set_chan_level(slice2a, chan_2a, 0); pwm_set_chan_level(slice1b, chan_1b, 0); pwm_set_chan_level(slice2b, chan_2b, 0); pwm_set_chan_level(slice2a, chan_2a, pwm_top_count[speed]); sleep_ms(500); break; case SPEED_UP: speed++; if (speed > SPEED_MAX) speed=SPEED_MAX; play_ws2812(); break; case SLOW_DOWN: speed--; if (speed < 0) speed=0; play_ws2812(); break; default: break; } } int main() { stdio_init_all(); ws2812_init(pio0, 0, 20); gpio_set_function(CAR_PIN1A, GPIO_FUNC_PWM); gpio_set_function(CAR_PIN1B, GPIO_FUNC_PWM); gpio_set_function(CAR_PIN2A, GPIO_FUNC_PWM); gpio_set_function(CAR_PIN2B, GPIO_FUNC_PWM); slice1a = pwm_gpio_to_slice_num(CAR_PIN1A); slice1b = pwm_gpio_to_slice_num(CAR_PIN1B); slice2a = pwm_gpio_to_slice_num(CAR_PIN2A); slice2b = pwm_gpio_to_slice_num(CAR_PIN2B); chan_1a = pwm_gpio_to_channel(CAR_PIN1A); chan_1b = pwm_gpio_to_channel(CAR_PIN1B); chan_2a = pwm_gpio_to_channel(CAR_PIN2A); chan_2b = pwm_gpio_to_channel(CAR_PIN2B); pwm_config c = pwm_get_default_config(); pwm_config_set_clkdiv(&c, 125); // 20ms period, steps 20000, clkdiv = 125 pwm_config_set_wrap(&c, 20000); pwm_config_set_phase_correct(&c, false); pwm_init(slice1a, &c, true); pwm_init(slice1b, &c, true); pwm_init(slice2a, &c, true); pwm_init(slice2b, &c, true); uint32_t count=0; uint8_t status; nRF24_spi_default_init(8, 9, irq_callback); nRF24_config_mode(RECEIVER); nRF24_enable_feature(FEATURE_EN_DPL, true); nRF24_set_RX_addr(0, addr[0], 5); nRF24_enable_data_pipe_dynamic_payload_length(0, true); while(1) { if (voice_state > 0 && voice_state < ACTION_MAX) { voice_action(voice_state); voice_action(current_direction); voice_state = NOISE; } tight_loop_contents(); } return 0; }
- CMakeLists.txt
# Generated Cmake Pico project file cmake_minimum_required(VERSION 3.13) set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) # Initialise pico_sdk from installed location # (note this can come from environment, CMake cache etc) set(PICO_SDK_PATH "/home/duser/pico/pico-sdk") set(PICO_BOARD pico CACHE STRING "Board type") # Pull in Raspberry Pi Pico SDK (must be before project) include(pico_sdk_import.cmake) if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0") message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.4.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") endif() project(pico_tinyML_voice_RC_CAR C CXX ASM) # Initialise the Raspberry Pi Pico SDK pico_sdk_init() # Add executable. Default name is the project name, version 0.1 add_executable(pico_tinyML_voice_RC_CAR pico_tinyML_voice_RC_CAR.c ) pico_set_program_name(pico_tinyML_voice_RC_CAR "pico_guesture_ws2812") pico_set_program_version(pico_tinyML_voice_RC_CAR "0.1") pico_enable_stdio_uart(pico_tinyML_voice_RC_CAR 0) pico_enable_stdio_usb(pico_tinyML_voice_RC_CAR 1) # Add the standard library to the build target_link_libraries(pico_tinyML_voice_RC_CAR pico_stdlib hardware_pwm) # Add the standard include files to the build target_include_directories(pico_tinyML_voice_RC_CAR PRIVATE ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required ) # Add any user requested libraries add_subdirectory(nRF24L01) target_link_libraries(pico_tinyML_voice_RC_CAR nRF24L01_drv ) add_subdirectory(ws2812) target_link_libraries(pico_tinyML_voice_RC_CAR ws2812 ) pico_add_extra_outputs(pico_tinyML_voice_RC_CAR)
ws2812:
- ws2812.c
#include "ws2812.h" #include "stdlib.h" #include "string.h" #define NUM_PIXELS 12 #define green 0x1f0000 #define red 0x001f00 #define blue 0x00001f static inline void ws2812_program_init(PIO pio, uint sm, uint pin, float freq) { pio_gpio_init(pio, pin); pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); uint offset = pio_add_program(pio, &ws2812_program); pio_sm_config c = ws2812_program_get_default_config(offset); sm_config_set_sideset_pins(&c, pin); sm_config_set_out_shift(&c, false, true, 24); sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); int cycles_per_bit = ws2812_T1 + ws2812_T2 *2; float div = clock_get_hz(clk_sys) / (freq * cycles_per_bit); sm_config_set_clkdiv(&c, div); pio_sm_init(pio, sm, offset, &c); pio_sm_set_enabled(pio, sm, true); } void put_grb_pixel(uint32_t pixel_grb) { pio_sm_put_blocking(pio0, 0, pixel_grb << 8u); } static inline uint32_t urgb_u32(uint8_t r, uint8_t g, uint8_t b) { return ((uint32_t) (r) << 8) | ((uint32_t) (g) << 16) | (uint32_t) (b); } void clear_pixel() { for (int i = 0; i < NUM_PIXELS; i++) put_grb_pixel(0x000000); } void ws2812_init(PIO pio, uint sm, uint ws2812_pin) { ws2812_program_init(pio, sm, ws2812_pin, 800000); }
- ws2812.h
#ifndef _WS2812_H_ #define _WS2812_H_ #include <stdio.h> #include "pico/stdlib.h" #include "hardware/pio.h" #include "ws2812.pio.h" #include "hardware/clocks.h" void ws2812_init(PIO pio, uint sm, uint ws2812_pin); void put_grb_pixel(uint32_t pixel_grb); #endif
- ws2812.pio
; ; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. ; ; SPDX-License-Identifier: BSD-3-Clause ; .program ws2812 .side_set 1 .define public T1 2 .define public T2 2 .lang_opt python sideset_init = pico.PIO.OUT_HIGH .lang_opt python out_init = pico.PIO.OUT_HIGH .lang_opt python out_shiftdir = 1 .wrap_target bitloop: out x, 1 side 0 [T1 - 1] ; Side-set still takes place when instruction stalls jmp !x do_zero side 1 [T2 - 1] ; Branch on the bit we shifted out. Positive pulse do_one: jmp bitloop side 1 [T2 - 1] ; Continue driving high, for a long pulse do_zero: nop side 0 [T2 - 1] ; Or drive low, for a short pulse .wrap % c-sdk { %}
- CMakeLists.txt(ws2812)
add_library(ws2812 INTERFACE) pico_generate_pio_header(ws2812 ${CMAKE_CURRENT_LIST_DIR}/ws2812.pio) target_sources(ws2812 INTERFACE ${CMAKE_CURRENT_LIST_DIR}/ws2812.c ) target_include_directories(ws2812 INTERFACE ${CMAKE_CURRENT_LIST_DIR} ) target_link_libraries(ws2812 INTERFACE hardware_pio )