Funpack第二季第一期 Syntiant TinyML 使用机器学习实现语音指令控制LED
开发板为Syntiant TinyML Board,通过板载麦克风收集音频数据上传到EDGE IMPULSE,进行机器学习,实现了语音指令控制板载LED,
标签
机器学习
神经网络
语音识别
Funpack第二季第一期 Syntiant TinyML Board
四衍九
更新2022-07-05
成都信息工程大学
559

板卡介绍

Frw1ZJLSapGwAexZ76zlVQXdO9bf

本次活动使用到的板卡是Syntiant的TinyML Board,它配合EDGE IMPULSE可以轻松建立机器学习模型,而无需任何专用硬件。

板子搭载的主要硬件如下:

· 超低功耗 Syntiant ® NDP101神经决策处理器™

· 主控为Atmel的SAMD21,Cortex-M0+ 32位低功耗48MHz ARM MCU,内置256KB FLASH和32KB SRAM

· 2MB串行闪存

· 一个用户定义的RGB LED

· 一个uSD卡槽

· BMI160 6轴运动传感器

· SPH0641LM4H 麦克风

除此之外,TinyML Board兼容Arduino MKR系列板卡,有5个数字IO,包括一路UART和一路I2C接口。

 

开发环境

arduino-cli 18.0版本

Arduino IDE和EDGE IMPULSE

 

任务介绍

本次活动我选择了任务一:

使用板载麦克风收集音频数据,然后上传到EDGE IMPULSE建立机器学习模型并训练,实现中文语音指令控制板载LED灯。一共实现了三个指令控制,分别是:“打开灯”、“关闭灯”、“灯闪烁”。

因为本次活动使用了Arduino来开发,提供了许多库可供使用,再加上EDGE IMPULSE提供了一份示例工程,而且MCU端的任务是控制LED,相对简单,所以其代码相对简单。而本次任务的重点难点则是在EDGE上建立识别效果良好的机器学习模型上。

 

任务实现

1.数据收集

数据集分为训练集和测试集,EDGE IMPULSE推荐分别占80%和20%。因为实时分类无法使用,所以测试集还是有必要建立,可以初步检测模型效果,避免了模型部署到板子上才发现效果不佳而浪费时间。

在我的模型中,我设置了四个类,除三个语音指令之外还有一个否定类标签(z_openset),用于存放所有噪音数据。四个类分别是:openlight(对应指令“打开灯”)、closelight(对应指令“关闭灯”)、lightflash(对应指令“灯闪烁”)、z_openset。

FhDvFS_T4N7gbB1Uuw33jU7XLLHQ

每个类都有一千条以上的数据,所有数据一共有4小时6分10秒。

三个语音指令的数据主要来源于我自己,剩下的来自于身边的同学朋友。openlight比另外两个指令多是因为,这是我第一个录的指令,没有仔细规划数据量。

z_openset类的数据主要来自于官方的噪音集和我自己录制各种噪音。

官方的噪音集是来自于“Go”和“Stop”的示例项目。在该项目的教程指引中提到,它基于Google Speech Commands数据集的子集而构建,同时还添加了来自Microsoft Scalable Noisy Speech数据集的噪声。包含四个类,每个类有25分钟的数据:“Yes”、“No”、“Unknown”(其他单词)、“Noise”(背景或静态噪声)。

而我自己录制的噪声主要包括生活中的各种噪声(犬吠声、歌曲声、孩童玩闹声、乐器声等等)和中文语音噪声(一段语音广播音频和一段小说讲述音频)。

数据分布图:

FqwOibNBozOEFPwcopXAou3OPAKD

2.模型建立

FiBZXcWTSU9Dgnruo1sRuIZgUz9g

时间序列(Time series data)板块中,窗口大小是968ms,这是因为NDP101输入的每个张量有1600个特征,纵轴有40个频率,横轴有40个FFT窗口。每个FFT窗口采样512次,而麦克风的采样率是16KHz,每个FFT窗口时间就是32ms。每次采样32ms,第一次不重复,而后面的每一次采样,都和前一次重复8ms,所以每个张量的时间窗口就是32+39*24=968ms。因为张量的特征数必须为1600,所以968ms是固定的,无法调整。

而窗口移动(Window increase)指窗口往后移动的距离。这个值小一点更好,值越小,相同时间内执行的推理次数越多,相应的,理论上识别率应该会更高,我将这里设置为了100ms。

3.模型训练

Impulse创建好后,在Syntiant信号处理块中生成特征,此处的参数不需要调整。然后在NN Classifier中开始训练,训练前的参数调整参考官方文档。生成特征和模型训练都需要一定的时间,数据越多花费时间越长。

4.模型测试

 进行模型参数,初步检测模型效果,如果分类效果太差,就要进行检查,查看数据,然后重新训练。而通过模型检测后,就可以部署模型到开发板上了。

 

代码实现

1. 初始化

整个程序的初始化由syntiant_setup()完成。

void setup(void)
{
    syntiant_setup();
}
// Arduino System Setup routine
// Initialises devices. Reads flash device to see if valid uilib has
// been programmed.
// Loads uilib if present
void syntiant_setup(void)
{
    timer4.enable(false); //disable timer4

    // uilib variables
    int s;
    uint32_t v;
    byte i;
    char name[9];
    char *namep = name;

    // Initialize Serial Port
    Serial.begin(115200);
    Serial2.begin(115200);
    //delay(3000); // Enable serial ports to print to console

    // Show sign of life
    pinMode(LED_BUILTIN, OUTPUT); // RED LED

    digitalWrite(LED_BUILTIN, HIGH); // Light RED LED
    //digitalWrite(LED_BLUE, HIGH); // Light BLUE LED
    //digitalWrite(LED_GREEN, HIGH); // Light GREEN LED

    SerialFlash.begin(FLASH_CS);
    SerialFlash.readID(FlashType);
    if(FlashType[0] == SST25VF016B) { // Set up clock for Bluebank board
        analogWrite(3, 0x10); //TinyML Final Board
        REG_TCC1_PER = 1464;
        while (TCC1->SYNCBUSY.bit.PER)
            ;
        REG_TCC1_CC1 = 732;
        while (TCC1->SYNCBUSY.bit.CC1)
            ;
        PORSTB = 24;
    } else if(FlashType[0] == MX25R6435FSN) { // Set up clock for Tessolve board
        // Set up 32KHz NDP clock
        /********************* Timer #3, 16 bit, toggles pin PA18 */
        zt3.configure(TC_CLOCK_PRESCALER_DIV1,      // prescaler
                      TC_COUNTER_SIZE_16BIT,        // bit width of timer/counter
                      TC_WAVE_GENERATION_MATCH_FREQ // frequency or PWM mode
        );
        zt3.PWMout(true, 0, 10);                       // Actually toggles pin PA18
        zt3.setCompare(0, (48000000 / 32000 / 2) - 1); // 32KHz output
        zt3.enable(true);
        PORSTB = 3;
    } else {
        Serial2.print("Unknown board: ");
        Serial2.println(FlashType[0], HEX);
        Serial.print("Board not recognized. Press your board's reset button to exit");
        while(true)
            ;
    }

    // reset NDP
    pinMode(PORSTB, OUTPUT);
    digitalWrite(PORSTB, LOW);
    delay(100);
    digitalWrite(PORSTB, HIGH);

    // Set up SPI (NDP) & SPI1 (SD card)
    SPI.begin();
    SPI.beginTransaction(SPISettings(spiSpeedGeneral, MSBFIRST, SPI_MODE0));

    // See which board we are by trying to read NDP101 Registion Register
    pinMode(NDP9101_CS, OUTPUT);
    digitalWrite(NDP9101_CS, HIGH);
    pinMode(TINYML_CS, OUTPUT);
    digitalWrite(TINYML_CS, HIGH);

    SPI_CS = NDP9101_CS;
    NDP.spiTransfer(NULL, 0, 0x0, NULL, spiData, 1);

    if (spiData[0] == 0x20)
    {
        SPI_CS = NDP9101_CS;
        idle = NDP9101_USB_IDLE;
    }

    else
    {
        SPI_CS = TINYML_CS;
        NDP.spiTransfer(NULL, 0, 0x0, NULL, spiData, 1);

        if (spiData[0] == 0x20)
        {
            SPI_CS = TINYML_CS;
            pinMode(NDP9101_CS, INPUT); // make NDP9101_CS input to save power

            Serial2.begin(115200);

            // Serial2 will be available on TinyML connector pin 6 (RX) & pin 7 (TX)
            // PA20 Arduino pin 6 is RX.
            // PA21 Arduino pin 7 is TX.
            // Assign pins PA20 & PA21 to SERCOM functionality.
            pinPeripheral(6, PIO_SERCOM_ALT);
            pinPeripheral(7, PIO_SERCOM_ALT);
            delay(100);
            Serial2.println("Hello Serial2 World!");

            pinMode(LED_BLUE, OUTPUT);
            pinMode(LED_GREEN, OUTPUT);
            pinMode(USER_SWITCH, INPUT_PULLUP);

            // find which Serial Flash device is connected
            SerialFlash.begin(FLASH_CS);
            SerialFlash.readID(FlashType);
            // Set up pin to drive 5v out to Arduino companion
            if (FlashType[0] == SST25VF016B)
            {
                digitalWrite(ENABLE_5V, LOW); // disable 5v output Bluebank Board
                Serial2.println("Syntiant TinyML Board B");
            }
            if (FlashType[0] == MX25R6435FSN)
            {
                digitalWrite(ENABLE_5V, HIGH); // disable 5v output Tesolve Boards
                Serial2.println("Syntiant TinyML Board T");
            }

            pinMode(PMU_OTG, OUTPUT);   // set up OTG/PMU current pin
            pinMode(ENABLE_5V, OUTPUT); // Set up 5v gate

            // Initialise SGM41512
            if (!PMIC.begin()) {
                Serial2.println("Failed to initialize PMIC!");
            }
            pmuCharge(); // Enable PMU Charge mode

            pinMode(0, OUTPUT);
            pinMode(1, OUTPUT);
            idle = TINYML_USB_IDLE;
        }

        else
        {
            Serial.println("No NDP device found");
            while (true)
                ;
        }
    }

    // Initialize SD & Serial Flash. Try & load NDP BIN file which contains NDP firmware & Neural Network
    // If not able to load bin file, use Bridging Mode to access NDP
    NDP.setInterrupt(NDP_INT, ndpInt);
    switch (loadModel(model))
    {
    case BIN_LOAD_OK:
        ei_printf("BIN file loaded correctly from SD Card");
        runningFromFlash = 1;
        digitalWrite(LED_BLUE, HIGH); // Light BLUE LED as uilib load successful
        loadedFromSD = 1;
        loadedFromSerialFlash = 0;
        break;
    case NO_SD:
        ei_printf("No SD Card inserted, please insert card");
        ei_printf("Running in Bridge Mode");
        break;
    case SD_NOT_INITIALIZED:
        ei_printf("SD Card initialization failed!");
        ei_printf("Running in Bridge Mode");
        break;
    case BIN_NOT_OPENED:
        // ei_printf(model + " NOT opened. Make sure you're using the correct BIN file name.");
        ei_printf("Running in Bridge Mode");
        break;
    case ERROR_LOADING_FLASH:
        ei_printf("Error loading bin from Flash!");
        ei_printf("Running in Bridge Mode");
        break;
    case ERROR_LOADING_SD:
        ei_printf("Error loading bin from SD!");
        ei_printf("Running in Bridge Mode");
        break;
    case LOADED_FROM_SERIAL_FLASH:
        ei_printf("BIN File Loaded correctly from Serial Flash");
        runningFromFlash = 1;
        //digitalWrite(LED_GREEN, HIGH); // Light GREEN LED as uilib load successful
        loadedFromSerialFlash = 1;
        loadedFromSD = 0;
        break;
    default:
        ei_printf("Running in Bridge Mode");
        break;
    }

    // Allow some peripherals to be active in Standby mode.
    // Standby is used when battery powered for lowest power
    TC3->COUNT16.CTRLA.bit.RUNSTDBY = 1; // enable timer3 in sleep mode. This generates the NDP clock
    SYSCTRL->XOSC32K.bit.RUNSTDBY = 1;   // Run the 32KHz Oscillator in sleep
    SYSCTRL->DFLLCTRL.bit.RUNSTDBY = 1;  // Run the Oscillator DFLL in sleep
    SYSCTRL->VREG.bit.RUNSTDBY = 1;      // Run the voltage regulator in sleep. This keeps NDP CLK at 32KHz

    // turn LED off
    digitalWrite(LED_BUILTIN, LOW);

    if (!runningFromFlash) {
        // Reset the NDP if the log load failed
        pinMode(PORSTB, OUTPUT);
        digitalWrite(PORSTB, LOW);
        delay(100);
        digitalWrite(PORSTB, HIGH);

        // Light RED LED as uilib NOT loaded successfully
        digitalWrite(LED_RED, HIGH);
    }

    // Set up timer to turn LEDs off after 1 second
    // ledTimerCount = 1000; // set LED timer for 1 second
    ledTimerCount = 1 * (1000000 / timer_in_uS); // set LED timer for 1 second

    // Set interrupt priority.
    // The priority specifies the interrupt priority value, whereby
    // lower values indicate a higher priority.
    // The default priority is 0 for every interrupt. This is the highest
    // possible priority.
    NVIC_SetPriority(TC4_IRQn, 3); // Make timer 4 the lowest priority

    tankSize = indirectRead(DSP_CONFIG_TANK) >> 4;
    tankAddress = indirectRead(DSP_CONFIG_TANKADDR);

#if defined(WITH_AUDIO)
    // Load Audio Buffer with test pattern
    for (i = 0; i < sizeof(audioBuf) / 2; i++) {
        audioBuf[i] = 4000 * (i - (sizeof(audioBuf) / 4));
    }
#endif


#if defined(WITH_AUDIO)
    AudioUSB.getShortName(namep); // needed for platformio to load AudioUSB
#endif


    //ENABLE_PDM = 25 is  declared in NDP_GPIO.h is the pin for controlling buffer SGM7SZ125. This buffer
    // will be activated (low) for voice command spotting and deactvated (high) for sensor appliactions

    pinMode(ENABLE_PDM, OUTPUT);
#if defined(WITH_IMU)
    digitalWrite(ENABLE_PDM, HIGH); // Disable PDM clock
    Serial.println("setup for IMU done");
#else
    digitalWrite(ENABLE_PDM, LOW); // Enable PDM clock
    Serial.println("setup for audio done");
#endif

    timer4.enable(true); // enable 1mS timer interrupt

    startingFWAddress = indirectRead(0x1fffc0c0);

    ei_setup();
}

2. 主循环

主循环实际上在syntiant_loop()里完成。

void loop(void)
{
    syntiant_loop();
}
void syntiant_loop(void)
{
    // Loop to stay in Standby Mode unless we get a ": " from USB
    // OR interrupt from NDP
    while (1)
    {

        if (flashflag == 1)
        {
            if (++flashcnt >= 500)
            {
                flashcnt = 0;
                digitalWrite(LED_GREEN, !digitalRead(LED_GREEN));
            }
        }
        else
        {
            flashcnt = 0;
        }

        if (match)
        {
            processMatch();
        }
        if (timer4TimedOut)
        {
            timer4TimedOut = 0;
            // Turn Off Arduino LED
            digitalWrite(LED_RED, LOW);
            /*digitalWrite(LED_BLUE, LOW);
            digitalWrite(LED_GREEN, LOW);*/
        }
        // check USB serial port for ": " command from host
        // if (Serial.read() == ':')
        // {
        //     break;
        // }
        if(ei_command_line_handle() == true) {
            break;
        }

        // Deep sleep only if USB disconnected.
        SCB->SCR &= !SCB_SCR_SLEEPDEEP_Msk; // remove deep sleep bit

        if ((USB->DEVICE.STATUS.reg & 0xc0) != idle)
        {
            // we are connected to USB
            USBConnected = 0;
            timer4.enableInterrupt(true); // enable interrupt for Audio
            if (detached == 1)
            {
                detached = 0; // assume attached to host USB
                Serial2.println("Recommected to USB");
            }
        }
        else
            USBConnected += 1;

        if (USBConnected > 0xfff00)
        {
            USBConnected = 0;

            // Only deep sleep (Standby) if LED timer has expired.
            // See if LED timer = 0, & timed out flag not set
            if ((!ledTimerCount) && (!timer4TimedOut))
            {
                SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; // enable Deep Sleep

                // This saves 0.63mA when battery powered. But then needs
                // keyword interrupt to wake CPU up
                USBDevice.detach();
                detached = 1;

                // stop timer as we are going to deep sleep
                timer4.enableInterrupt(false);

                // Put Flash into Deep Power Down
                digitalWrite(FLASH_CS, LOW);
                SPI1.transfer(FLASH_DP); // enable RESET
                digitalWrite(FLASH_CS, HIGH);
                delayMicroseconds(1);
                delay(1);

                delay(1000);
                Serial2.println("Deep Sleep");
                delay(2000);
            }
            else
            {
                timer4.enableInterrupt(true);
            }
        }

        digitalWrite(1, HIGH);

        __DSB(); // Data sync to ensure outgoing memory accesses complete
        __WFI();
        if (REG_SYSCTRL_DFLLMUL != SAVE_REG_SYSCTRL_DFLLMUL)
        {
            REG_SYSCTRL_DFLLMUL = SYSCTRL_DFLLMUL_MUL(0xBB80) | SYSCTRL_DFLLMUL_FSTEP(1) | SYSCTRL_DFLLMUL_CSTEP(1);
        }

        digitalWrite(1, LOW);

        // Woken up. See if attached to USB host
        if (detached)
        {
            USBDevice.attach();
            Serial.begin(115200);
            detached = 0;
        }
    }
    // Stop timer4 as we will access NDP from main()
    timer4.enableInterrupt(false); // disable 1mS timer interrupt

    // ':' received -- perform a management command
    runManagementCommand();

    timer4.enableInterrupt(true); // enable 1mS timer interrupt
}

3. 模型输出

在定时器4中断里处理NDP101的所有反馈。

NDP.poll()返回匹配的分类match,然后传给模型输出函数ei_classification_output(),因为类标签用字符串数组储存,所以传给的是match-1。

// Timer 4 interrupt. Handles ALL touches of NDP. Also services USB Audio
void isrTimer4(struct tc_module *const module_inst)
{
    digitalWrite(0, LOW);
    int s;
    unsigned int len;

    SCB->SCR &= !SCB_SCR_SLEEPDEEP_Msk; // Don't Allow Deep Sleep
    if ((ledTimerCount < 0xffff) && (ledTimerCount > 0))
    {
        ledTimerCount--;
        if (ledTimerCount == 0)
        {
            timer4TimedOut = 1;
        }
    }


#ifdef WITH_AUDIO
    if (runningFromFlash) {

        uint32_t tankRead;
        int i;

        for (i = 0; i < 32; i += 4)
        {
            tankRead =
                indirectRead(tankAddress + ((currentPointer - 32 + i) % tankSize));
            audioBuf[i / 2] = tankRead & 0xffff;
            audioBuf[(i / 2) + 1] = (tankRead >> 16) & 0xffff;
        }
        currentPointer += 32;
        currentPointer %= tankSize;
    }
    AudioUSB.write(audioBuf, 32); // write samples to AudioUSB
#else

    if(runningFromFlash) {
        currentPointer = indirectRead(startingFWAddress);

        int32_t diffPointer = ((int32_t)currentPointer - prevPointer);

        if(diffPointer < 0) {
            diffPointer = (64000 - prevPointer) + currentPointer;
        }
        if(diffPointer >= dataLengthToBeSaved) {
            len = sizeof(dataBuf);
            int ret = NDP.extractData((uint8_t *)dataBuf, &len);

            if(ret != SYNTIANT_NDP_ERROR_NONE) {
                ei_printf("Extracting data failed with error : %d\r\n", ret);
            }
            else {
                if(imu_active == false) {
                    imu_active = true;
                    for(int i = 0; i < (dataLengthToBeSaved / 2); i++) {
                        imu[i] = dataBuf[i];
                    }

                    imu_active = false;
                }
            }

            prevPointer = currentPointer;
        }
    }
#endif

    if (doInt)
    {
        doInt = 0;
        // Poll NDP for cause of interrupt (if running from flash)
        if (runningFromFlash)
        {
            match = NDP.poll();

            if (match)
            {
                // Light Arduino LED
                //digitalWrite(LED_BUILTIN, HIGH);

                ei_classification_output(match -1);

                printBattery(); // Print current battery level

            }
        }
        else
        {
            match = 1;
        }
    }
    digitalWrite(0, HIGH);
}

ei_classification_output()函数用于输出模型的识别结果,以字符串形式返回标签名给on_classification_changed()函数。标签名存储在model_variables.h中。

void ei_classification_output(int matched_feature)
{
    if (ei_run_impulse_active()) {

        ei_printf("\nPredictions:\r\n");

        for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
            ei_printf("    %s: \t%d\r\n", ei_classifier_inferencing_categories[ix],
                (matched_feature == ix) ? 1 : 0);
        }

        on_classification_changed(ei_classifier_inferencing_categories[matched_feature], 0, 0);
    }
}

4. 功能实现

void on_classification_changed(const char *event, float confidence, float anomaly_score) {

    // here you can write application code, e.g. to toggle LEDs based on keywords
    if (strcmp(event, "openlight") == 0) {
        // Toggle LED
        digitalWrite(LED_GREEN, HIGH);
    }

    if (strcmp(event, "closelight") == 0) {
        // Toggle LED
        digitalWrite(LED_GREEN, LOW);
        flashflag = 0;
    }

    if (strcmp(event, "lightflash") == 0) {
        flashflag = 1;
    }
}
if (flashflag == 1)
        {
            if (++flashcnt >= 500)
            {
                flashcnt = 0;
                digitalWrite(LED_GREEN, !digitalRead(LED_GREEN));
            }
        }
        else
        {
            flashcnt = 0;
        }

这里控制的是RGB灯的绿灯。

在定时器4的中断函数中对模型的输出进行了处理,模型输出的函数ei_classification_output()在中断里被调用,然后将识别结果传给on_classification_changed()函数。

然后在on_classification_changed中检测传来的标签名,若识别到标签openlight(对应指令“打开灯”)就打开灯;若识别到标签closelight(对应指令“关闭灯”)就关灯;若识别到标签lightflash(对应指令“灯闪烁”)就让标志flashflag置1。在主循环中,若标志位flashflag为1,则执行灯闪烁的代码,否则清空闪烁计数器flashcnt。

 

效果演示

“打开灯”

li33hQkmQ6ElJ8jnNX0Q6t7W_1ug

“关闭灯”

lq-2I7l6TXHNpiOPdLY1DmZRetqM

“灯闪烁”

lkQgUwm6pv0-HXCDuD2Ic9d0MYuU

 

 

心得体会

本次是第一次参加Funpack活动,通过活动了解到了机器学习的相关知识,有不少收获。

也是第一次接触Arduino相关的东西,Arduino早就听说过,虽然这块板子并非Arduino官方硬件,但是这次Adruino IDE的使用体验并不是太好,有时候总会出现一些奇怪的错误。

不过总的来说,还是完成了这次活动,也感谢硬禾学堂和得捷提供这次机会,希望以后有更多的活动可以参与。

附件下载
Funpack 2-1.zip
Arduino工程和完整固件
团队介绍
宋文锴 成都信息工程大学
团队成员
四衍九
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号