prettyprint

2022年4月15日 星期五

ESP-IDF: Using ESP-IDF Classic BT SPP API to implement ESP32 Bluetooth SPP Server and Client devices

 本實驗使用ESP-IDF Classic BT API來製作含有5個藍芽設備形成的piconet,一個ESP32-WROOM-32作為Master,兩個ESP32(ESP32-S, ESP32-CAM)為Slave,一台Android phone 為Slave執行Bluetooth Serial Terminal當作控制台,即時接受piconet中元件連線狀態,另外一台筆電為Slave執行Bluetooth Serial Terminal。Slave可向Master發送控制命令,或藉由Master向其他Slave發送控制命令或文字訊息。

本實驗共訂出5個指令類型:

  1. AT+ST<device_name>:向Master註冊自已的名稱<device_name>,例如:AT+STP0
  2. AT+LS: 列出已連線的Slaves,,例如:AT+LS。
  3. AT+CMD<target_device_name:command>:向其他device送出命令,例如:AT+CMDC0:ON
  4. AT+TXT<target_device_name:TEXT>:向其他device送出Text,例如:AT+TXTT0:TEST
  5. AT+DIS: 向Master送出離線訊息,,例如:AT+DIS。
本實驗Master名稱固定為M0, 擔任控制台的Slave指定為P0。如下圖所示:


一、成果展示影片:



二、使用軟體環境:

  1. VSCode
  2. ESP-IDF extension
  3. Bluetooth serial terminal for Android and for Windows

三、ESP-IDF Classic BT API - GAP & SPP:

SPP Bluetooth Stack:
BT GAP API提供設備間配對連線等,BT SPP API提供Serial Port Service(Server or client)等。
ESP32 Master與Slave端程式碼大同小異,一端負責提供SPP Service Server,另一端為SPP Client。
有關ESP藍芽架構主要參考文件為下列官方網址:

GAP與SPP以register callback function 來處理相對應的Event,API相關文件說明如下連結。

詳細程式碼如下所示,完整程式說明請觀看成果展示影片。

四、程式碼:

1. Server端(Run at Master side)
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_bt_defs.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_spp_api.h"
#include "esp_err.h"
#include "esp_log.h"
#include "string.h"
#include "driver/gpio.h"


#define TAG "MY_SPP_MASTER"
#define BT_DEVICE_NAME  "MY_SPP_MASTER"
#define SPP_SERVER_1 "MY_SPP_SERVER"
#define CLIENT_NAME_MAX 10

QueueHandle_t spp_queue_handle;
char rmt_bd_name[ESP_BT_GAP_MAX_BDNAME_LEN+1];
// slave device link list
typedef struct _tbd{
    uint32_t handle;
    uint8_t bd_client_name[CLIENT_NAME_MAX+1];
    uint8_t len;
    struct _tbd *next;
} bd_client_t;
bd_client_t *bd_link_header=NULL;

//input data item
typedef struct {
    uint32_t handle;
    uint16_t len;
    uint8_t in_data[ESP_SPP_MAX_MTU];
} spp_queue_item_t;
spp_queue_item_t spp_queue_item;

bool pannel_connected=false;
uint32_t pannel_handle = 0;

void my_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) {
    switch(event) {
        case ESP_BT_GAP_DISC_RES_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_DISC_RES_EVT");
        break;
        case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_DISC_STATE_CHANGED_EVT");
        break;
        case ESP_BT_GAP_RMT_SRVCS_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVCS_EVT");
        break; 
        case ESP_BT_GAP_RMT_SRVC_REC_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVC_REC_EVT");
        break;
        case ESP_BT_GAP_AUTH_CMPL_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_AUTH_CMPL_EVT");
            if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
                ESP_LOGI(TAG, "bda:%x%x%x%x%x%x, device_name:%s--", 
                param->auth_cmpl.bda[0],param->auth_cmpl.bda[1],param->auth_cmpl.bda[2],param->auth_cmpl.bda[3],param->auth_cmpl.bda[4],param->auth_cmpl.bda[5],
                (char*)param->auth_cmpl.device_name);
            }
        break;
        case ESP_BT_GAP_PIN_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_PIN_REQ_EVT");
        break;
        case ESP_BT_GAP_CFM_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_CFM_REQ_EVT");
            esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
        break;
        case ESP_BT_GAP_KEY_NOTIF_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_NOTIF_EVT");
        break;
        case ESP_BT_GAP_KEY_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_REQ_EVT");
        break;
        case ESP_BT_GAP_READ_RSSI_DELTA_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_READ_RSSI_DELTA_EVT");
        break;
        case ESP_BT_GAP_CONFIG_EIR_DATA_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_CONFIG_EIR_DATA_EVT");
        break;
        case ESP_BT_GAP_SET_AFH_CHANNELS_EVT:
            ESP_LOGI(TAG, "");
        break;
        case ESP_BT_GAP_READ_REMOTE_NAME_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_READ_REMOTE_NAME_EVT");
        break;
        case ESP_BT_GAP_MODE_CHG_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_MODE_CHG_EVT");
        break;
        case ESP_BT_GAP_REMOVE_BOND_DEV_COMPLETE_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_REMOVE_BOND_DEV_COMPLETE_EVT");
        break;
        case ESP_BT_GAP_QOS_CMPL_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_QOS_CMPL_EVT");
        break;
        default:
        break;
    }
}
char msg[100];
char temp_name[30];
void my_bt_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
    switch(event) {
        case ESP_SPP_INIT_EVT:
            ESP_LOGI(TAG, "ESP_SPP_INIT_EVT");
        break;
        case ESP_SPP_UNINIT_EVT:
            ESP_LOGI(TAG, "ESP_SPP_UNINIT_EVT");
        break;
        case ESP_SPP_DISCOVERY_COMP_EVT:
            ESP_LOGI(TAG, "ESP_SPP_DISCOVERY_COMP_EVT");
        break;
        case ESP_SPP_OPEN_EVT:
            ESP_LOGI(TAG,"ESP_SPP_OPEN_EVT");
        break;
        case ESP_SPP_CLOSE_EVT:
            ESP_LOGI(TAG, "close handle:%d, status:%d", param->close.handle, param->close.status);
            spp_queue_item.handle = param->close.handle;
            spp_queue_item.len = 6;
            memcpy(spp_queue_item.in_data, (uint8_t*)"AT+DIS", 6);
            xQueueSend(spp_queue_handle, &spp_queue_item, portMAX_DELAY);
        break;
        case ESP_SPP_START_EVT:
            ESP_LOGI(TAG, "SPP_START_EVENT:status:%d, scn:%d", param->start.status, param->start.scn);
        break;
        case ESP_SPP_CL_INIT_EVT:
            ESP_LOGI(TAG, "ESP_SPP_CL_INIT_EVT");
        break;
        case ESP_SPP_DATA_IND_EVT:
            spp_queue_item.handle = param->data_ind.handle;
            spp_queue_item.len = param->data_ind.len;
            memcpy(spp_queue_item.in_data, param->data_ind.data, param->data_ind.len);
            if (spp_queue_item.in_data[spp_queue_item.len-2] == (uint8_t)'\r' && spp_queue_item.in_data[spp_queue_item.len-1] == (uint8_t)'\n') {
                spp_queue_item.len -= 2;
            }
            xQueueSend(spp_queue_handle, &spp_queue_item, portMAX_DELAY);
        break;
        case ESP_SPP_CONG_EVT:
            ESP_LOGI(TAG, "cong handle:%d, cong:%d, status:%d", param->cong.handle, param->cong.cong, param->cong.status);
        break;
        case ESP_SPP_WRITE_EVT:
            ESP_LOGI(TAG, "ESP_SPP_WRITE_EVT");
        break;
        case ESP_SPP_SRV_OPEN_EVT:
            ESP_LOGI(TAG, "ESP_SPP_SRV_OPEN_EVT:rem_bda:%x:%x:%x:%x:%x:%x",
            param->srv_open.rem_bda[0],param->srv_open.rem_bda[1],param->srv_open.rem_bda[2],param->srv_open.rem_bda[3],param->srv_open.rem_bda[4],param->srv_open.rem_bda[5]);
            ESP_LOGI(TAG, "handle:%d, new handle:%d", param->srv_open.handle, param->srv_open.new_listen_handle);
        break;
        case ESP_SPP_SRV_STOP_EVT:
            ESP_LOGI(TAG, "ESP_SPP_SRV_STOP_EVT");
        break;
        default:
        break;
    }
}

void set_bd_client(spp_queue_item_t item) {
    bd_client_t *client;
    bd_client_t *end = bd_link_header;

    client = malloc(sizeof(bd_client_t));
    client->handle = item.handle;
    memcpy(client->bd_client_name, item.in_data+5, item.len-5);
    if (memcmp(client->bd_client_name, (uint8_t*)"P0", item.len-5)==0) {
        pannel_connected=true;
        pannel_handle = item.handle;
    }
    client->len = item.len-5;
    client->next=NULL;
    if(end == NULL) {
        bd_link_header = client;
    } else {
        while(end) {
            if (memcmp(end->bd_client_name, item.in_data+5, item.len-5) == 0) {
                end->handle=client->handle;
                free(client);
                break;
            } else if (end->next == NULL) {
                end->next = client;
                if (pannel_handle) {
                    memcpy((uint8_t*)msg, client->bd_client_name, client->len);
                    sprintf(msg+client->len, ":Connected");
                    esp_spp_write(pannel_handle, client->len+10, (uint8_t*)msg);
                }
                break;
            }
            end = end->next;
        }
    }
}

void show_bd_connected_client(spp_queue_item_t item) {
    bd_client_t *end = bd_link_header;
    while (end) {
        esp_spp_write(item.handle, end->len, end->bd_client_name);
        vTaskDelay(20/portTICK_PERIOD_MS);
        esp_spp_write(item.handle, 2, (uint8_t*)"\r\n");
        vTaskDelay(20/portTICK_PERIOD_MS);
        end = end->next;
    }
}

void get_bd_client_name(uint32_t handle, char *name) {
    bd_client_t *end = bd_link_header;
    
    while (end) {
        if ((end->handle) == handle) {
            memcpy((uint8_t*)name, end->bd_client_name, end->len);
            name[end->len] = '\0';
            break;
        }
        end=end->next;
    }
    name=NULL;
}
void bd_send_cmd(spp_queue_item_t item) {
    bd_client_t *end = bd_link_header;
    int colon=0;
    for (int i=0; i < item.len; i++) {
        if (item.in_data[i] == (uint8_t)':') {
            colon = i;
            break;
        }
    }
    if (colon == 0) return;
    if (memcmp(item.in_data+6, (uint8_t*)"M0",colon-6)==0) { // M0 is default master device name
        if (memcmp(item.in_data+colon+1, (uint8_t*)"ON", item.len-(colon+1)) == 0) {
            gpio_set_level(GPIO_NUM_4, 1);
        }
        if (memcmp(item.in_data+colon+1, (uint8_t*)"OFF", item.len-(colon+1)) == 0) {
            gpio_set_level(GPIO_NUM_4, 0);
        }
    } else {
        while(end) {
            if (memcmp(end->bd_client_name, item.in_data+6, end->len)==0) {
                esp_spp_write(end->handle, item.len-(colon+1), item.in_data+colon+1);
                break;
            }
            end=end->next;
        }
    }
}

void bd_send_txt(spp_queue_item_t item) {
    bd_client_t *end = bd_link_header;
    int colon=0;
    for (int i=0; i < item.len; i++) {
        if (item.in_data[i] == (uint8_t)':') {
            colon = i;
            break;
        }
    }
    if (colon == 0) return;
    char name[10];
    while(end) {
        memcpy((uint8_t*)name, end->bd_client_name, end->len);
        name[end->len]='\0';
        printf("%s:%d\n", name, end->handle);
        if (memcmp(end->bd_client_name, item.in_data+6, end->len)==0) {
            esp_spp_write(end->handle, item.len-(colon+1), item.in_data+colon+1);
            break;
        }
        end=end->next;
    }
    
}

void bd_disconnect(spp_queue_item_t item) {
    bd_client_t *pre;
    bd_client_t *end = bd_link_header;
    
    get_bd_client_name(item.handle, temp_name);

    if (strcmp(temp_name, "P0")==0) {
        pannel_connected = false;
        pannel_handle=0;
    }
    pre = bd_link_header;
    while (end) {
        if (item.handle == end->handle) {
            pre->next = end->next;
            if (pannel_connected && pannel_handle) {
                memcpy((uint8_t*)msg, end->bd_client_name, end->len);
                sprintf(msg+end->len, ":Disconnected");
                esp_spp_write(pannel_handle, end->len+13, (uint8_t*)msg);
            }
            if (end == bd_link_header) {
                bd_link_header = end->next;
            } else {
                pre->next = end->next;
            }
            free(end);
            break;
        }
        pre = end;
        end = end->next;
    }
}
void QueueReceiveTask(void* param) {
    spp_queue_item_t item;
    while(1) {
        if (xQueueReceive(spp_queue_handle, &item, portMAX_DELAY) == pdTRUE) {
            item.in_data[item.len]='\0';
            if (memcmp(item.in_data, (uint8_t*)"AT+ST", 5) == 0) {
                set_bd_client(item);
            } else if (memcmp(item.in_data, (uint8_t*)"AT+LS", 5) == 0) {
                show_bd_connected_client(item);
            } else if (memcmp(item.in_data, (uint8_t*)"AT+CMD", 6) == 0) {
                bd_send_cmd(item);
            } else if (memcmp(item.in_data, (uint8_t*)"AT+TXT", 6) == 0) {
                bd_send_txt(item);
            } else if (memcmp(item.in_data, (uint8_t*)"AT+DIS", 6) == 0) {
                bd_disconnect(item);
            }
        }   
        vTaskDelay(10);
    } 
}

void app_main(void)
{
    esp_err_t err;
    err = nvs_flash_init();
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "nvs flash init error");
        return;
    }
 
    gpio_set_direction(GPIO_NUM_4, GPIO_MODE_OUTPUT);
    gpio_set_level(GPIO_NUM_4, 0);
    spp_queue_handle =  xQueueCreate(10, sizeof(spp_queue_item_t));
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    err = esp_bt_controller_init(&bt_cfg);
    err = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);

    err = esp_bluedroid_init();
    err = esp_bluedroid_enable();
    
    err = esp_spp_register_callback(my_bt_spp_cb);
    err = esp_spp_init(ESP_SPP_MODE_CB);

    err = esp_spp_start_srv(ESP_SPP_SEC_AUTHENTICATE, ESP_SPP_ROLE_MASTER, 1, SPP_SERVER_1);

    err = esp_bt_gap_register_callback(my_bt_gap_cb);
    
    esp_bt_cod_t cod;
    cod.major = 0x1;
    cod.minor=0x0;
    err = esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_MAJOR_MINOR);

    esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_NONE;
    err = esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap, sizeof(esp_bt_io_cap_t));
    err = esp_bt_dev_set_device_name(BT_DEVICE_NAME);
    err = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);

    esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_VARIABLE;
    esp_bt_pin_code_t pin_code;
    esp_bt_gap_set_pin(pin_type, 0, pin_code);

    xTaskCreatePinnedToCore(QueueReceiveTask, "TASK", 5120, 0, 10,0,1);
 }

2. Client端(Run at Slave side)
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_spp_api.h"
#include "esp_err.h"
#include "esp_log.h"
#include "string.h"
#include "driver/gpio.h"


#define TAG "MY_SPP_C1"
#define BT_DEVICE_NAME  "MY_SPP_C1"
uint8_t client_name[7]="AT+STC1";
#define SPP_SERVER_NAME "MY_SPP_MASTER"
uint8_t *spp_peer_name;

esp_bd_addr_t spp_server_addr = {0};
uint32_t hServer=0;


QueueHandle_t spp_queue_handle;

typedef struct {
    uint32_t handle;
    uint16_t len;
    uint8_t in_data[ESP_SPP_MAX_MTU];
} spp_queue_item_t;

spp_queue_item_t spp_queue_item;

uint8_t bd_name_len;

void my_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) {
    switch(event) {
        case ESP_BT_GAP_DISC_RES_EVT:
            for (int i = 0 ; i < param->disc_res.num_prop; i++) {
                if (param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_EIR) {
                    spp_peer_name = esp_bt_gap_resolve_eir_data(param->disc_res.prop[i].val, ESP_BT_EIR_TYPE_CMPL_LOCAL_NAME, &bd_name_len);
                    if (!spp_peer_name) {
                        spp_peer_name = esp_bt_gap_resolve_eir_data(param->disc_res.prop[i].val, ESP_BT_EIR_TYPE_SHORT_LOCAL_NAME, &bd_name_len);
                    }
                    if (spp_peer_name) {
                        if (bd_name_len > ESP_BT_GAP_MAX_BDNAME_LEN) bd_name_len = ESP_BT_GAP_MAX_BDNAME_LEN;
                        spp_peer_name[bd_name_len]='\0';
                        // connect to default MASTER: MY_SPP_MASTER
                        if (strncmp(SPP_SERVER_NAME, (char*)spp_peer_name, bd_name_len) == 0) {
                            esp_bt_gap_cancel_discovery();
                            memcpy(spp_server_addr, param->disc_res.bda, ESP_BD_ADDR_LEN);
                            esp_spp_start_discovery(spp_server_addr);
                        }
                    } else {
                        ESP_LOGI(TAG, "GAP_DISC_RES, no peer_name, len:%d", bd_name_len);
                    }
                }
            }
        break;
        case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
            ESP_LOGI(TAG, "GAP_DISC_STATE_CHANGE");
        break;
        case ESP_BT_GAP_RMT_SRVCS_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVCS_EVT");
        break; 
        case ESP_BT_GAP_RMT_SRVC_REC_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVC_REC_EVT");
        break;
        case ESP_BT_GAP_AUTH_CMPL_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_AUTH_CMPL_EVT");
        break;
        case ESP_BT_GAP_PIN_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_PIN_REQ_EVT");
        break;
        case ESP_BT_GAP_CFM_REQ_EVT:
            esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
        break;
        case ESP_BT_GAP_KEY_NOTIF_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_NOTIF_EVT");
        break;
        case ESP_BT_GAP_KEY_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_REQ_EVT");
        break;
        case ESP_BT_GAP_READ_RSSI_DELTA_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_READ_RSSI_DELTA_EVT");
        break;
        case ESP_BT_GAP_CONFIG_EIR_DATA_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_CONFIG_EIR_DATA_EVT");
        break;
        case ESP_BT_GAP_SET_AFH_CHANNELS_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_SET_AFH_CHANNELS_EVT");
        break;
        case ESP_BT_GAP_READ_REMOTE_NAME_EVT:
            ESP_LOGI(TAG, "remote Name:%s",(char*)param->read_rmt_name.rmt_name);
        break;
        case ESP_BT_GAP_MODE_CHG_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_MODE_CHG_EVT");
        break;
        case ESP_BT_GAP_REMOVE_BOND_DEV_COMPLETE_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_REMOVE_BOND_DEV_COMPLETE_EVT");
        break;
        case ESP_BT_GAP_QOS_CMPL_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_QOS_CMPL_EVT");
        break;
        default:
        break;
    }
}

void my_bt_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
    switch(event) {
        case ESP_SPP_INIT_EVT:
            if (param->init.status == ESP_SPP_SUCCESS) {
                esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
                esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 0x10, 0);
            } else {
                ESP_LOGI(TAG, "ESP_SPP_INIT_ERROR");
            }
        break;
        case ESP_SPP_UNINIT_EVT:
        break;
        case ESP_SPP_DISCOVERY_COMP_EVT:
            // connect to spp server if found
            if (param->disc_comp.status == ESP_SPP_SUCCESS) {
                if (esp_spp_connect(ESP_SPP_SEC_AUTHORIZE, ESP_SPP_ROLE_SLAVE, 1, spp_server_addr)!=ESP_OK) {
                    ESP_LOGI(TAG, "esp_spp_connect error");
                }
            } else {
                ESP_LOGI(TAG, "ESP_SPP_DISCOVERY_COMP_EVT: not found retry...");
                esp_spp_start_discovery(spp_server_addr);
            }
        break;
        case ESP_SPP_OPEN_EVT:
             if (param->open.status == ESP_SPP_SUCCESS) {
                hServer=param->open.handle;
                esp_spp_write(param->open.handle, 7, (uint8_t*)client_name);
             } else {
                 ESP_LOGI(TAG, "CLINET OPEN- failure");
             }
        break;
        case ESP_SPP_CLOSE_EVT:
            ESP_LOGI(TAG, "close handle:%d, status:%d", param->close.handle, param->close.status);
        break;
        case ESP_SPP_START_EVT:
            ESP_LOGI(TAG, "SPP_START_EVENT:status:%d, scn:%d", param->start.status, param->start.scn);
        break;
        case ESP_SPP_CL_INIT_EVT:
            ESP_LOGI(TAG, "Clinet init");
        break;
        case ESP_SPP_DATA_IND_EVT:
            spp_queue_item.handle = param->data_ind.handle;
            spp_queue_item.len = param->data_ind.len;
            memcpy(spp_queue_item.in_data, param->data_ind.data, param->data_ind.len);
            xQueueSend(spp_queue_handle, &spp_queue_item, portMAX_DELAY);
        break;
        case ESP_SPP_CONG_EVT:
            ESP_LOGI(TAG, "cong handle:%d, cong:%d, status:%d", param->cong.handle, param->cong.cong, param->cong.status);
        break;
        case ESP_SPP_WRITE_EVT:
            ESP_LOGI(TAG, "ESP_SPP_WRITE_EVT");
        break;
        case ESP_SPP_SRV_OPEN_EVT:
        ESP_LOGI(TAG, "ESP_SPP_SRV_OPEN_EVT");
        break;
        case ESP_SPP_SRV_STOP_EVT:
        ESP_LOGI(TAG, "ESP_SPP_SRV_STOP_EVT");
        break;
        default:
        break;
    }
}

void QueueReceiveTask(void* param) {
    spp_queue_item_t item;
    while(1) {
        if (xQueueReceive(spp_queue_handle, &item, portMAX_DELAY) == pdTRUE) {
            if (memcmp(item.in_data, (uint8_t*)"ON", 2) == 0) {
                gpio_set_level(GPIO_NUM_4, 1);
            }
            if (memcmp(item.in_data, (uint8_t*)"OFF", 3) == 0) {
                gpio_set_level(GPIO_NUM_4, 0);
            }
        }   
        vTaskDelay(10);
    } 
}

void app_main(void)
{
    esp_err_t err;
    err = nvs_flash_init();
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "nvs flash init error");
        return;
    }
    gpio_set_direction(GPIO_NUM_4, GPIO_MODE_OUTPUT);
    spp_queue_handle =  xQueueCreate(10, sizeof(spp_queue_item_t));
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    err = esp_bt_controller_init(&bt_cfg);
    err = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);

    err = esp_bluedroid_init();
    err = esp_bluedroid_enable();
    
    err = esp_bt_gap_register_callback(my_bt_gap_cb);
    esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_NONE;
    err = esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap, sizeof(esp_bt_io_cap_t));
    err = esp_bt_dev_set_device_name(BT_DEVICE_NAME);
   
    err = esp_spp_register_callback(my_bt_spp_cb);
    err = esp_spp_init(ESP_SPP_MODE_CB);

    xTaskCreatePinnedToCore(QueueReceiveTask, "TASK", 4096, 0, 1,0,1);
 }

2022年4月6日 星期三

ESP-IDF: ESP32 Bluetooth stereo Speaker with Backward/Play(Pause)/Forward button

本實驗製作一個藍芽喇叭,並附有前一首/下一首,播放/暫停控制鍵。

一、使用元件:

  1. ESP32-S(38 pins)開發版
  2. MAX98357A
  3. 3W喇叭
  4. 按鈕

二、成果展示:




三、軟體開發環境:

  1. VS Code
  2. ESP-IDF extension

四、Bluetooth classic Audio:

ESP32 Bluetooth 架構主要參考文件為下列官方網址:
https://www.espressif.com/sites/default/files/documentation/esp32_bluetooth_architecture_en.pdf
bluetooth audio 使用bluetooth classic A2DP and AVRCP因此只能使用Bluedroid,架構如下
 
memuconfig的bluetooth選項如下:

bluetooth audio 使用A2DP與AVRCP remote control,其profile相關如下圖所示:
依此結構,軟體初始化順序為
controller: init/enable --> bluedroid init/enable --> AVRCP register callback/init --> A2DP register cb --> A2DP sink register cb and data cb/init --> GAP register cb, security param, set device name and scan mode:詳細程式碼如文末所附程式碼(或成果展示影片的詳細步驟)。相關API參考網址:
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/classic_bt.html

五、ESP32 使用的GPIO Pins:

  1. Buttons and LEDs
  2. I2S pins:

六、程式碼:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_bt.h"
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_a2dp_api.h"
#include "esp_avrc_api.h"
#include "esp_gap_bt_api.h"
#include "string.h"
#include "nvs_flash.h"
#include "driver/i2s.h"
#include "driver/gpio.h"
#include "esp_log.h"

#define BUTTON_FORWARD_PIN           5
#define BUTTON_PLAY_PAUSE_PIN        23
#define BUTTON_BACKWARD_PIN          19
#define LED_RED_PIN                  17
#define LED_GREEN_PIN                16
#define TAG "MY_BT_SPEAKER"

uint8_t my_bt_speaker_state=ESP_AVRC_PLAYBACK_STOPPED;
const char bt_device_name[]="MY_BT_SPEAKER";
bool avrc_conn=false;

static bool ct_button_play_pause_press=false;
static bool ct_button_backward_press=false;
static bool ct_button_forward_press=false;

void my_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) {
    switch (event) {
        case ESP_BT_GAP_DISC_RES_EVT:
        break;
        case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
        break;
        case ESP_BT_GAP_RMT_SRVCS_EVT:
        break;
        case ESP_BT_GAP_RMT_SRVC_REC_EVT:
        break;
        case ESP_BT_GAP_AUTH_CMPL_EVT:
            ESP_LOGI(TAG,"auth: state:%d, remote_name:%s", param->auth_cmpl.stat, param->auth_cmpl.device_name);
        break;
        case ESP_BT_GAP_PIN_REQ_EVT:
        break;
        case ESP_BT_GAP_CFM_REQ_EVT:
            esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
        break;
        case ESP_BT_GAP_KEY_NOTIF_EVT:
        break;
        case ESP_BT_GAP_MODE_CHG_EVT:
        break;
        default: 
            ESP_LOGI(TAG, "GAP EVENT ID:%d",event);
        break;
    }
}


void my_bt_a2d_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t *param) {
    switch(event) {
        case ESP_A2D_CONNECTION_STATE_EVT:
        if (param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
            esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
            gpio_set_level(LED_GREEN_PIN, 1);
            gpio_set_level(LED_RED_PIN, 0);
        }
        if (param->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
            esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
            gpio_set_level(LED_GREEN_PIN, 0);
            gpio_set_level(LED_RED_PIN, 1);
        }
        break;
        case ESP_A2D_AUDIO_STATE_EVT:
        break;
        case ESP_A2D_AUDIO_CFG_EVT:
        if (param->audio_cfg.mcc.type == ESP_A2D_MCT_SBC) {
            int sample_rate = 16000;
            char oct0 = param->audio_cfg.mcc.cie.sbc[0];
            if (oct0 & (0x01 << 6)) {
                sample_rate = 32000;
            } else if (oct0 & (0x01 << 5)) {
                sample_rate = 44100;
            } else if (oct0 & (0x01 << 4)) {
                sample_rate = 48000;
            }
            i2s_set_clk(0, sample_rate, 16, 2);
        }
            
        break;
        case ESP_A2D_MEDIA_CTRL_ACK_EVT:
        break;
        case ESP_A2D_PROF_STATE_EVT:
        break;
        default:
        break;
    }
}

void my_bt_a2d_sink_data_cb(const uint8_t *buf, uint32_t len) {
    size_t wb;
    i2s_write(0, buf, len, &wb, portMAX_DELAY);
}

esp_avrc_rn_evt_cap_mask_t rn_cap_mask={0};
void my_bt_avrc_cb(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t *param) {
    char *text;
    switch(event) {
        case ESP_AVRC_CT_CONNECTION_STATE_EVT:
        if (param->conn_stat.connected) {
            esp_avrc_ct_send_get_rn_capabilities_cmd(1);
            avrc_conn=1;
            esp_avrc_ct_send_register_notification_cmd(1, ESP_AVRC_RN_PLAY_STATUS_CHANGE, 0);
        } else {
            rn_cap_mask.bits=0;
             avrc_conn=0;
        }
        break;
        case ESP_AVRC_CT_PASSTHROUGH_RSP_EVT:
        break;
        case ESP_AVRC_CT_CHANGE_NOTIFY_EVT:
            switch(param->change_ntf.event_id) {
                case ESP_AVRC_RN_VOLUME_CHANGE:
                break;
                case ESP_AVRC_RN_PLAY_STATUS_CHANGE:
                    my_bt_speaker_state = param->change_ntf.event_parameter.playback;
                break;
                default:
                break;
            }
        break;
        case ESP_AVRC_CT_GET_RN_CAPABILITIES_RSP_EVT:
            rn_cap_mask.bits = param->get_rn_caps_rsp.evt_set.bits;
            break;
        case ESP_AVRC_CT_SET_ABSOLUTE_VOLUME_RSP_EVT:
        break;
        case ESP_AVRC_CT_METADATA_RSP_EVT:
            text = (char*)param->meta_rsp.attr_text;
            text[param->meta_rsp.attr_length]='\0';
            ESP_LOGI(TAG,"metadata_rsp:%d, %s, %d", param->meta_rsp.attr_id, text, param->meta_rsp.attr_length);
        break;
        default:
        break;
    }
}
static void ct_play_forward(void *param) {
    if (!ct_button_forward_press) {
        ct_button_forward_press=true;
    }
}
static void ct_play_play_pause(void *param) {
    if (!ct_button_play_pause_press) {
        ct_button_play_pause_press=true;
    }
}
static void ct_play_backward(void *param) {
    if (!ct_button_backward_press) {
        ct_button_backward_press=true;
    }
}
void my_bt_set_gpio_pins() {
    gpio_set_direction(BUTTON_BACKWARD_PIN, GPIO_MODE_INPUT);
    gpio_set_direction(BUTTON_FORWARD_PIN, GPIO_MODE_INPUT);
    gpio_set_direction(BUTTON_PLAY_PAUSE_PIN, GPIO_MODE_INPUT);
    gpio_set_direction(LED_GREEN_PIN, GPIO_MODE_OUTPUT);
    gpio_set_direction(LED_RED_PIN, GPIO_MODE_OUTPUT);

    gpio_set_intr_type(BUTTON_BACKWARD_PIN, GPIO_INTR_POSEDGE);
    gpio_set_intr_type(BUTTON_FORWARD_PIN, GPIO_INTR_POSEDGE);
    gpio_set_intr_type(BUTTON_PLAY_PAUSE_PIN, GPIO_INTR_POSEDGE);
    

    gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
    gpio_isr_handler_add(BUTTON_FORWARD_PIN, ct_play_forward, 0);
    gpio_isr_handler_add(BUTTON_PLAY_PAUSE_PIN, ct_play_play_pause, 0);
    gpio_isr_handler_add(BUTTON_BACKWARD_PIN, ct_play_backward, 0);

    gpio_set_level(LED_GREEN_PIN,0);
    gpio_set_level(LED_RED_PIN,1);
}
void app_main(void)
{
    esp_err_t err;
    err = nvs_flash_init();
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "nvs init error");
        return;
    }

    my_bt_set_gpio_pins();

    i2s_config_t i2s_config = {
        .mode = I2S_MODE_MASTER | I2S_MODE_TX,                          // Only TX
        .sample_rate = 44100,
        .bits_per_sample = 16,
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,                   //2-channels
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,              // MAX98357A I2S Philips
        .dma_buf_count = 6,
        .dma_buf_len = 60,
        .intr_alloc_flags = 0,                                          //Default interrupt priority
        .tx_desc_auto_clear = true                                      //Auto clear tx descriptor on underflow
    };
    i2s_driver_install(0, &i2s_config, 0, NULL);

    i2s_pin_config_t pin_config = {
        .bck_io_num = 26,
        .ws_io_num = 22,
        .data_out_num = 25,
        .data_in_num = -1                                                       //Not used
    };

    i2s_set_pin(0, &pin_config);

    esp_bt_controller_config_t bt_controller  = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    err = esp_bt_controller_init(&bt_controller);
    err = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);

    err= esp_bluedroid_init();
    err = esp_bluedroid_enable();

    err = esp_avrc_ct_register_callback(my_bt_avrc_cb);
    err = esp_avrc_ct_init();
   
    err = esp_a2d_register_callback(my_bt_a2d_cb);
    err = esp_a2d_sink_register_data_callback(my_bt_a2d_sink_data_cb);
    err = esp_a2d_sink_init();

    err = esp_bt_gap_register_callback(my_bt_gap_cb);
    err = esp_bt_dev_set_device_name(bt_device_name);
    esp_bt_io_cap_t io_cap=ESP_BT_IO_CAP_IO;
    err = esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &io_cap, sizeof(io_cap));
    err = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
   
   while(1) {
        if (avrc_conn) {
            vTaskDelay(5);
            if (ct_button_backward_press) {
                esp_avrc_ct_send_passthrough_cmd(1, ESP_AVRC_PT_CMD_BACKWARD, ESP_AVRC_PT_CMD_STATE_PRESSED);
                vTaskDelay(300/portTICK_PERIOD_MS);
                //esp_avrc_ct_send_metadata_cmd(1, ESP_AVRC_MD_ATTR_TITLE);
                ct_button_backward_press=false;
            } 
            if (ct_button_forward_press) {
                esp_avrc_ct_send_passthrough_cmd(1, ESP_AVRC_PT_CMD_FORWARD, ESP_AVRC_PT_CMD_STATE_PRESSED);
                vTaskDelay(300/portTICK_PERIOD_MS);
                //esp_avrc_ct_send_metadata_cmd(1, ESP_AVRC_MD_ATTR_TITLE);
                ct_button_forward_press=false;
            } 
            if (ct_button_play_pause_press) {
                if (my_bt_speaker_state == ESP_AVRC_PLAYBACK_STOPPED || my_bt_speaker_state == ESP_AVRC_PLAYBACK_PAUSED) {
                    esp_avrc_ct_send_passthrough_cmd(1, ESP_AVRC_PT_CMD_PLAY, ESP_AVRC_PT_CMD_STATE_PRESSED);
                    my_bt_speaker_state = ESP_AVRC_PLAYBACK_PLAYING;
                } else  if (my_bt_speaker_state == ESP_AVRC_PLAYBACK_PLAYING) {
                    esp_avrc_ct_send_passthrough_cmd(1, ESP_AVRC_PT_CMD_PAUSE, ESP_AVRC_PT_CMD_STATE_PRESSED);
                    my_bt_speaker_state = ESP_AVRC_PLAYBACK_PAUSED;
                }
                //esp_avrc_ct_send_metadata_cmd(1, ESP_AVRC_MD_ATTR_TITLE);
                vTaskDelay(300/portTICK_PERIOD_MS);
                ct_button_play_pause_press=false;
            } 
        } else {
            vTaskDelay(5);
        }
    }
}


2022年3月23日 星期三

STM32 使用HAL程式庫實作I2S介面語音錄音儲存與播放(STM32 I2S Audio record/store/play using HAL library)

 本實驗使用STM32F407VE I2S語音介面,以INMP441 MEMS microphone與MAX98357A DAC & Amplifier來實做一組錄音/儲存/播放語音設備。

一、使用元件

  1. STM32F407VE開發版
  2. INMP441
  3. MAX98357A
  4. 3W4Ω Speaker
  5. Micro SD card
  6. LM386

二、INMP441基本說明(摘錄自INMP441 datasheet)

  1. Digital I²S Interface with High-Precision 24-Bit Data
  2. 6 pins:
    SCK: Serial-Data Clock for I²S Interface
    SD: Serial-Data Output for I²S Interface
    WS: Serial Data-Word Select for I²S Interface
    L/R: Left/Right Channel Select. 本實驗只使用一個INMP441 Microphone, default is left channel
    VDD: Power, 1.8 V to 3.3 V.
    GND: Ground
  3. MSB-first
  4. Data Word Length: The output data word length is 24 bits per channel. The INMP441 must always have 64 clock cycles for every stereo data-word 。
  5. Data-Word Format:
    The default data format is I²S (two’s complement), MSB-first. In this format, the MSB of each word is delayed by one SCK cycle from the start of each half-frame.

  6. 根據以上INMP441規格,本實驗ST32F407VE I2S2設為 Master Receive Mode, I2S Philips。雖然Data Word Length只有24Bits,但是補滿32bits傳送,所以STM32F407VE data and frame format設定為32 Bits Data in 32 Bit Frame。

三、MAX98357A基本說明(摘錄自MAX98357A datasheet)

  1. 16 or 32 bits Data length, MSB First
  2. No MCLK Required
  3. supporting 8kHz–96kHz sampling rates with 16/24/32-bit resolution for I2S/left justified data
  4. The MAX98357A accepts standard I2S data through DIN, BCLK, and LRCLK while the MAX98357B accepts left-justified data through the same inputs
  5. Pins:
    Vin: 3.3 or 5V
    GND: ground
    SD: 選左右聲道,內定為(left/2+right/2)
    Gain: 設定增益(9dB)
    DIN: Serial Data(即為STM32F407VE I2S的SD pin)
    BCLK: Serial-Data Clock for I²S Interface(同SCK)
    LRC: Serial Data-Word Select for I²S Interface(同WS)
  6. Input data format (Standard I2S 32 bit)


    STM32F407VE I2S3 Transmission Mode: Master Transmission Mode, Communication Standard: I2S Philips。

四、STM32F4xx I2S (摘錄RM0090 Reference manual)

  1. The I2S shares three common pins with the SPI
  2. I2S Philips standard: For this standard, the WS signal is used to indicate which channel is being transmitted. It is activated one CK clock cycle before the first bit (MSB) is available.
  3. MSB justified standard:  For this standard, the WS signal is generated at the same time as the first data bit, which is the MSB first.
  4.  LSB justified standard: This standard is similar to the MSB justified standard (no difference for the 16-bit and 32-bit full-accuracy frame formats).
  5. For all data formats and communication standards, the most significant bit is always sent first (MSB first).
  6. data width is 16bits
選擇I2S2連接INMP441, Master Receive Mode; Communication Standard: I2S Philips; data and frame format: 32 Bits Data in 32 Bit Frame。
選擇I2S3連接MAX93857A, Master Transmission Mode; Communication Standard: I2S Philips; data and frame format: 32 Bits Data in 32 Bit Frame。

五、MAX98357A輸出聲音過小問題:

    以上列方式實驗,錄音能取得正常音量,但由MAX98357A輸出時音量太小(如成果影片展示),將SD接HIGH(只輸出LEFT Channel),Gain 100KΩ接地(15dB)以獲得最大輸出。但音量仍不理想。在上篇實驗[ESP-IDF: ESP32 I2S介面語音錄音機(ESP32 I2S Audio Recorder using ESP-ADF)]使用相同的MAX98357A但能輸出較大的音量。
  1. 使用硬體放大:再將MAX98357A輸出接入LM386音頻放大器,再一次放大,可以獲得較大的音量(如成果影片展示)。
  2. 使用簡易軟體放大:((out_word)&0x80000000) |( ((out_word)<< 3)&0x7FFFFFFF );out_word為欲輸出的digital data先運算放大再輸出,可以獲得較大的音量(如成果影片展示)。

六、啟動與停止錄音按鈕

    實驗流程,按一次EXTI GPIO按鈕啟動錄音,儲存WAVE檔案至SD Card,再按一次停止錄音,接者播放儲存的WAVE檔案。按鈕採用硬體RC Debounce 線路。

七、WAVE Format

        實驗以WAVE format儲存在SD card上(有關STM32F4xx storage 可參閱另一篇文章[STM32微控制器(STM32F407VET6) SD-4bits、SD-SPI,FLASH等儲存設備管理]。
wave 檔案的header如下所示:
(圖片來源: Win38383838 - 自己的作品, CC BY-SA 4.0, 
https://commons.wikimedia.org/w/index.php?curid=46442654)

先寫入44bytes header,資依序寫入由INMP441 microphone擷取資料,最後修改總長度與data長度。INMP441為MSB格式(Big Endian),由於MAX98357A播放的資料亦為MSB,因此本實驗儲存在SD card上的檔案暫不轉換成Wave檔案little Endian格式,下列程式另提供轉換函式。
亦可存成AIFF檔案,AIFF為未壓縮PCM big endian格式,換成AIFF header直接輸出big endian資料,本實驗暫時未實作。

八、STM32CubeMX設定

  1. SDIO: 4 bits, SDIO_RX and SDIO_TX DMA enable


  2. EXTI GPIO button & and Red LED, Green LED: all pull down, enable EXTI line1 interrupt
  3. FatFS: 
  4. I2S2 for INMP441 MEMS Microphone:


    因為I2S 介面為16bits,  所以DMA data width只能選half word
  5. I2S3 for MAX98357A:

  6. RCC clock: High Speed Clock(HSE)-Crystal/Ceramic Resonate

九、成果展示



十、程式碼

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "fatfs.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "string.h"
#include "stdio.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define MAX_DMA_ACCESS_COUNT	(40960)

#define DMA_READ_SIZE	(128)

#define BUFFER_COUNT	(4)

#define STATE_STOP				1
#define STATE_RECORDING			2
#define STATE_START_RECORDING	3
#define STATE_PLAYING			4
typedef union {
	uint32_t w;
	char b[4];
} _WORD;
typedef union  {
	uint16_t hw;
	char b[2];
} _HALF_WORD;

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
 I2S_HandleTypeDef hi2s2;
I2S_HandleTypeDef hi2s3;
DMA_HandleTypeDef hdma_spi2_rx;
DMA_HandleTypeDef hdma_spi3_tx;

RTC_HandleTypeDef hrtc;

SD_HandleTypeDef hsd;
DMA_HandleTypeDef hdma_sdio_rx;
DMA_HandleTypeDef hdma_sdio_tx;

/* USER CODE BEGIN PV */
uint16_t DMA_TxRx_SIZE = DMA_READ_SIZE*2;
static uint16_t rcvBuf[DMA_READ_SIZE*2*BUFFER_COUNT];
static uint32_t rCount=0, wCount=0;
static uint8_t audio_state = STATE_STOP;
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void PeriphCommonClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_SDIO_SD_Init(void);
static void MX_DMA_Init(void);
static void MX_I2S2_Init(void);
static void MX_RTC_Init(void);
static void MX_I2S3_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
typedef struct _WaveHeader{
	char riff[4];
	uint32_t size;
	char wave[4];
	char fmt[4];
	uint32_t fmt_size;
	uint16_t format; //1:PCM
	uint16_t channels; // channels
	uint32_t sampleRate;  // sample rate
	uint32_t rbc;//sampleRate*bitsPerSample*channels/8
	uint16_t bc; //bitsPerSample*channels/8
	uint16_t bitsPerSample; //bitsPerSample
	char data[4];
	uint32_t data_size;
} WAVE_HEADER;

void convertEndian(char* sd_path, char *file_in, char *file_out) {
	WAVE_HEADER wave_header;
	FRESULT res;
	FIL fin, fout;
	char fn[256];
	UINT bw, br;
	uint16_t bitsSample;
	uint8_t readBytes;
	_WORD *w_data;
	_HALF_WORD *h_data;

	  //res = f_mount(&SDFatFS, SDPath, 0);
	sprintf(fn, "%s%s", sd_path, file_in);
	res = f_open(&fin, fn, FA_OPEN_EXISTING|FA_READ);
	sprintf(fn, "%s%s", sd_path, file_out);
	res = f_open(&fout, fn, FA_CREATE_ALWAYS|FA_WRITE);
	f_read(&fin, (uint8_t*)&wave_header, sizeof(wave_header), &br);

	  bitsSample= wave_header.bitsPerSample;
	  if (bitsSample == 32) {
		  w_data = (_WORD*)malloc(512);
	  } else if (bitsSample == 16){
		  h_data = (_HALF_WORD*)malloc(512);
	  } else {
		  return;
	  }


	  f_write(&fout, (uint8_t*)&wave_header, sizeof(wave_header), &bw);
	  for (int i=0; i < wave_header.data_size; i+=512) {
		  if (bitsSample == 32) {
			  f_read(&fin, (uint8_t*)w_data, 512, &br);
			  for (int i = 0; i < br/4; i++) {
				  w_data[i].w = w_data[i].b[0] << 24 | w_data[i].b[1] << 16 | w_data[i].b[2] << 8 | w_data[i].b[3];
			  }
			  f_write(&fout, (uint8_t*)(w_data), br, &bw);
		  }
		  else {
			  f_read(&fin, (uint8_t*)h_data, 512, &br);
			  for (int i = 0; i < br/2; i++) {
				  h_data[i].hw = h_data[i].b[0] << 8 | h_data[i].b[1];
			  }
			  f_write(&fout, (uint8_t*)(h_data), br, &bw);
		  }
	  }
	  f_close(&fout);
	  f_close(&fin);
}


uint8_t txCplt = 0;

void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef *hi2s) {
	txCplt=1;
}
uint8_t rcvCplt = 0;
uint16_t* rpt, *wpt, *temppt;
void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s){
	rCount++;
	rpt = (rcvBuf)+(rCount%BUFFER_COUNT)*DMA_TxRx_SIZE;
	HAL_I2S_Receive_DMA(hi2s, rpt, DMA_READ_SIZE);
}

FRESULT fwrite_wav_header(FIL* file, uint16_t sampleRate, uint8_t bitsPerSample, uint8_t channels) {
	UINT bw;
	WAVE_HEADER wave_header;
	wave_header.riff[0] = 'R';wave_header.riff[1] = 'I';
	wave_header.riff[2] = 'F';wave_header.riff[3] = 'F';
	wave_header.size = (uint32_t)0;
	wave_header.wave[0] = 'W';wave_header.wave[1] = 'A';
	wave_header.wave[2] = 'V';wave_header.wave[3] = 'E';
	wave_header.fmt[0] = 'f';wave_header.fmt[1] = 'm';
	wave_header.fmt[2] = 't';wave_header.fmt[3] = ' ';
	wave_header.fmt_size = 16;
	wave_header.format = 1; // PCM
	wave_header.channels = channels; // channels
	wave_header.sampleRate=sampleRate;  // sample rate
	wave_header.rbc = sampleRate*bitsPerSample*2/8;
	wave_header.bc =  bitsPerSample*2/8;
	wave_header.bitsPerSample = bitsPerSample; //bitsPerSample
	wave_header.data[0] = 'd'; wave_header.data[1] = 'a';
	wave_header.data[2] = 't'; wave_header.data[3] = 'a';
	wave_header.data_size = 0;
	return f_write(file, (uint8_t*)&wave_header, sizeof(wave_header), &bw);
}

void startRecord(char *filename) {
	UINT bw;
	UINT writeBytes;
	UINT skipCount=125;// skip 0.5 second
	FIL fp;
	FRESULT res;

	writeBytes = DMA_TxRx_SIZE*2;
	res = f_open(&fp, filename, FA_CREATE_ALWAYS|FA_WRITE);
	res = fwrite_wav_header(&fp, 16000, 32, 2);

	HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_SET);
	audio_state = STATE_RECORDING;
	rpt = rcvBuf;
	wpt = rpt;
	rCount=0; wCount=0;
	HAL_I2S_Receive_DMA(&hi2s2, rpt, DMA_READ_SIZE);

	while (1)
	{
		if (wCount < rCount ) {
			if (rCount > skipCount)
			{
				res = f_write(&fp, wpt, writeBytes, &bw);
			}
			 wCount++;
			 wpt = (rcvBuf)+(wCount%BUFFER_COUNT)*DMA_TxRx_SIZE;
		  }

		  if (audio_state == STATE_STOP || rCount > MAX_DMA_ACCESS_COUNT)
		  {
			  HAL_I2S_DMAStop(&hi2s2);
			  break;
		  }
	}
	uint32_t data_len = (wCount-1) * writeBytes;
	uint32_t total_len = data_len+36;
	f_lseek(&fp, 4);
	f_write(&fp, (uint8_t*)&total_len, 4, &bw);
	f_lseek(&fp, 40);
	f_write(&fp, (uint8_t*)&data_len, 4, &bw);
	f_close(&fp);
	HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_RESET);
	audio_state = STATE_STOP;
}
void startPlay(char *filename) {

	FIL fp;
	WAVE_HEADER wave_header;
	UINT br;

	uint16_t *readpt, *writept, *temppt;
	FRESULT res;

	res = f_open(&fp, filename, FA_OPEN_EXISTING|FA_READ);

	res = f_read(&fp, (uint8_t*)&wave_header, sizeof(wave_header), &br);
	if (br != sizeof(wave_header)) {f_close(&fp);return; }

	audio_state = STATE_PLAYING;
	HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_SET);

	readpt = rcvBuf;
	writept = (rcvBuf)+DMA_TxRx_SIZE;
	  txCplt=0;
	  f_read(&fp, (uint8_t*)readpt, DMA_TxRx_SIZE*2, &br);
	  temppt=writept;
	  writept=readpt;
	  readpt=temppt;
	  HAL_I2S_Transmit_DMA(&hi2s3, (uint16_t*)writept, DMA_READ_SIZE);
	  uint32_t *tu;
	  while (1) {
		  res = f_read(&fp, (uint8_t*)readpt, DMA_TxRx_SIZE*2, &br);
		  for (int i = 0; i < DMA_TxRx_SIZE; i+=2) {
			tu = (uint32_t*)&readpt[i];
			*tu = ((*tu)&0x80000000) | (((*tu) << 3)&0x7FFFFFFF);
		}

		  while(!txCplt) ;
		  txCplt=0;
		  temppt=writept;
		  writept=readpt;
		  readpt=temppt;

		  HAL_I2S_Transmit_DMA(&hi2s3, (uint16_t*)writept, DMA_READ_SIZE);
		  if (br < DMA_TxRx_SIZE*2) break;
	  }


	  HAL_I2S_DMAStop(&hi2s3);

	  f_close(&fp);
	  HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_RESET);
	  audio_state = STATE_STOP;
}


void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
	if (GPIO_Pin == BUTTON_Pin) {
		switch (audio_state) {
		case STATE_STOP:
			audio_state = STATE_START_RECORDING;
			break;
		case STATE_RECORDING:
			audio_state = STATE_STOP;
			break;
		case STATE_PLAYING:
			break;
		case STATE_START_RECORDING:
			break;
		default:
			audio_state = STATE_STOP;
			break;
		}

	}
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

/* Configure the peripherals common clocks */
  PeriphCommonClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_SDIO_SD_Init();
  MX_DMA_Init();
  MX_FATFS_Init();
  MX_I2S2_Init();
  MX_RTC_Init();
  MX_I2S3_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  FRESULT res;
  char filename[256];
  res = f_mount(&SDFatFS, SDPath, 0);

  uint16_t count;
   while (1)
  {
	    if (audio_state == STATE_START_RECORDING) {
	  		  HAL_Delay(1);
	  		  sprintf(filename, "%sr_%05d.wav", SDPath, count++);
	  		  startRecord(filename);

	  		  HAL_Delay(1000);
	  		  startPlay(filename);

	  	  }

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }

  f_mount(&SDFatFS, "", 0);

  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Configure the main internal regulator output voltage
  */
  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.LSIState = RCC_LSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 4;
  RCC_OscInitStruct.PLL.PLLN = 168;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = 7;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief Peripherals Common Clock Configuration
  * @retval None
  */
void PeriphCommonClock_Config(void)
{
  RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0};

  /** Initializes the peripherals clock
  */
  PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_I2S;
  PeriphClkInitStruct.PLLI2S.PLLI2SN = 50;
  PeriphClkInitStruct.PLLI2S.PLLI2SR = 2;
  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief I2S2 Initialization Function
  * @param None
  * @retval None
  */
static void MX_I2S2_Init(void)
{

  /* USER CODE BEGIN I2S2_Init 0 */

  /* USER CODE END I2S2_Init 0 */

  /* USER CODE BEGIN I2S2_Init 1 */

  /* USER CODE END I2S2_Init 1 */
  hi2s2.Instance = SPI2;
  hi2s2.Init.Mode = I2S_MODE_MASTER_RX;
  hi2s2.Init.Standard = I2S_STANDARD_PHILIPS;
  hi2s2.Init.DataFormat = I2S_DATAFORMAT_32B;
  hi2s2.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE;
  hi2s2.Init.AudioFreq = I2S_AUDIOFREQ_16K;
  hi2s2.Init.CPOL = I2S_CPOL_LOW;
  hi2s2.Init.ClockSource = I2S_CLOCK_PLL;
  hi2s2.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE;
  if (HAL_I2S_Init(&hi2s2) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN I2S2_Init 2 */
  //hi2s2.Instance->CR1 = hi2s2.Instance->CR1 | SPI_CR1_LSBFIRST;
  /* USER CODE END I2S2_Init 2 */

}

/**
  * @brief I2S3 Initialization Function
  * @param None
  * @retval None
  */
static void MX_I2S3_Init(void)
{

  /* USER CODE BEGIN I2S3_Init 0 */

  /* USER CODE END I2S3_Init 0 */

  /* USER CODE BEGIN I2S3_Init 1 */

  /* USER CODE END I2S3_Init 1 */
  hi2s3.Instance = SPI3;
  hi2s3.Init.Mode = I2S_MODE_MASTER_TX;
  hi2s3.Init.Standard = I2S_STANDARD_PHILIPS;
  hi2s3.Init.DataFormat = I2S_DATAFORMAT_32B;
  hi2s3.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE;
  hi2s3.Init.AudioFreq = I2S_AUDIOFREQ_16K;
  hi2s3.Init.CPOL = I2S_CPOL_LOW;
  hi2s3.Init.ClockSource = I2S_CLOCK_PLL;
  hi2s3.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE;
  if (HAL_I2S_Init(&hi2s3) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN I2S3_Init 2 */
  //hi2s3.Instance->CR1 = hi2s3.Instance->CR1 | SPI_CR1_LSBFIRST;
  /* USER CODE END I2S3_Init 2 */

}

/**
  * @brief RTC Initialization Function
  * @param None
  * @retval None
  */
static void MX_RTC_Init(void)
{

  /* USER CODE BEGIN RTC_Init 0 */

  /* USER CODE END RTC_Init 0 */

  RTC_TimeTypeDef sTime = {0};
  RTC_DateTypeDef sDate = {0};

  /* USER CODE BEGIN RTC_Init 1 */

  /* USER CODE END RTC_Init 1 */

  /** Initialize RTC Only
  */
  hrtc.Instance = RTC;
  hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
  hrtc.Init.AsynchPrediv = 127;
  hrtc.Init.SynchPrediv = 255;
  hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
  hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
  hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
  if (HAL_RTC_Init(&hrtc) != HAL_OK)
  {
    Error_Handler();
  }

  /* USER CODE BEGIN Check_RTC_BKUP */
if (0) {
  /* USER CODE END Check_RTC_BKUP */

  /** Initialize RTC and set the Time and Date
  */
  sTime.Hours = 0;
  sTime.Minutes = 0;
  sTime.Seconds = 0;
  sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
  sTime.StoreOperation = RTC_STOREOPERATION_RESET;
  if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
  {
    Error_Handler();
  }
  sDate.WeekDay = RTC_WEEKDAY_MONDAY;
  sDate.Month = RTC_MONTH_MARCH;
  sDate.Date = 6;
  sDate.Year = 22;

  if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN RTC_Init 2 */
}
  /* USER CODE END RTC_Init 2 */

}

/**
  * @brief SDIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_SDIO_SD_Init(void)
{

  /* USER CODE BEGIN SDIO_Init 0 */

  /* USER CODE END SDIO_Init 0 */

  /* USER CODE BEGIN SDIO_Init 1 */

  /* USER CODE END SDIO_Init 1 */
  hsd.Instance = SDIO;
  hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
  hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
  hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
  hsd.Init.BusWide = SDIO_BUS_WIDE_1B;
  hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
  hsd.Init.ClockDiv = 0;
  /* USER CODE BEGIN SDIO_Init 2 */

  /* USER CODE END SDIO_Init 2 */

}

/**
  * Enable DMA controller clock
  */
static void MX_DMA_Init(void)
{

  /* DMA controller clock enable */
  __HAL_RCC_DMA2_CLK_ENABLE();
  __HAL_RCC_DMA1_CLK_ENABLE();

  /* DMA interrupt init */
  /* DMA1_Stream3_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Stream3_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Stream3_IRQn);
  /* DMA1_Stream5_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
  /* DMA2_Stream3_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
  /* DMA2_Stream6_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream6_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream6_IRQn);

}

/**
  * @brief GPIO Initialization Function
  * @param None
  * @retval None
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOH_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOA, LED_RED_Pin|LED_GREEN_Pin, GPIO_PIN_RESET);

  /*Configure GPIO pin : PC13 */
  GPIO_InitStruct.Pin = GPIO_PIN_13;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /*Configure GPIO pin : BUTTON_Pin */
  GPIO_InitStruct.Pin = BUTTON_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  HAL_GPIO_Init(BUTTON_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : LED_RED_Pin LED_GREEN_Pin */
  GPIO_InitStruct.Pin = LED_RED_Pin|LED_GREEN_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* EXTI interrupt init*/
  HAL_NVIC_SetPriority(EXTI1_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(EXTI1_IRQn);

}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

2022年2月26日 星期六

ESP-IDF: ESP32 I2S介面語音錄音機(ESP32 I2S Audio Recorder using ESP-ADF)

 本實驗製作一個I2S介面錄音設備,使用元件如下:

  1. ESP32-WROOM-32(30pins) development board
  2. INMP441 microphone(I2S)
  3. MAX98357A 
  4. 3W4Ω Speaker
  5. 2.4" TFT (8-bit parallel port with touch)
  6. SPI SD Card
實驗內容:
  1. 使用touch screen控制錄放音樂存放在SD上。
  2. 錄音時動畫顯示
  3. 可調整錄音或撥放音量
  4. 可捲動選擇撥放的錄製清單
軟體開發環境: VSCode 使用ESP-IDF & ESP-ADF extension; 不使用ESP32 audio development board。
本實驗幾乎用完所有GPIO pins,在不使用ESP32 audio development board下,利用ESP-ADF環境開發audio application,實驗內容不另外define custom board,在menuconfig Audio board選用ESP32-Lyart-Mini,再自行重新另訂GPIO pins。



一、GPIO pins:

I2S:


SPI SD Card
TFT Display:

二、Record audio pipeline



INMP441使用left channel,接線如下:
相對應的參數設定,
I2S 參數:由I2S_STREAM_CFG_DEFAULT()修改:
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_cfg.task_core = APP_CPU_NUM;
i2s_cfg.type = AUDIO_STREAM_READER;
i2s_cfg.i2s_config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT;
i2s_cfg.use_alc = true;
i2s_cfg.volume=-63;
i2s_stream_reader = i2s_stream_init(&i2s_cfg);

I2S pins:
i2s_pin_config_t rp = {
        .bck_io_num = 5,
        .ws_io_num = 25,
        .data_out_num = -1,
        .data_in_num = 39
    };
    i2s_set_pin(I2S_NUM_0, &rp);


三、Play audio pipeline

I2S參數使用default即可,因為audio_element_setinfo 函數填入

i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
    i2s_cfg.type = AUDIO_STREAM_WRITER;
    i2s_cfg.use_alc = true;
    i2s_cfg.volume=0;
i2s_stream_writer = i2s_stream_init(&i2s_cfg);

audio_element_getinfo(audio_decoder, &music_info);
audio_element_setinfo(i2s_stream_writer, &music_info);

I2S pins:
i2s_pin_config_t wp = {
        .bck_io_num = 22,
        .ws_io_num = 25,
        .data_out_num = 26,
        .data_in_num = -1
    };
 i2s_set_pin(I2S_NUM_0, &wp);

SPI SD Card:
設定VFS base_path:
#define SDSPI_MOUNT "/sdcard"
SPI pin 設定
sdmmc_host_t sdspi_host = SDSPI_HOST_DEFAULT();
sdspi_device_config_t sdspi_device = SDSPI_DEVICE_CONFIG_DEFAULT();
spi_bus_config_t bus_config = {
    .miso_io_num = GPIO_NUM_19,
    .mosi_io_num = GPIO_NUM_23,
    .sclk_io_num = GPIO_NUM_18
};
sdspi_device.host_id = VSPI_HOST;
spi_dma_chan_t dma_chan = SPI_DMA_CH1;
esp_vfs_fat_mount_config_t mount_config = {
     .format_if_mount_failed=true,
     .max_files = 5,
     .allocation_unit_size=16*512   //8192
 };
 ret = spi_bus_initialize(sdspi_device.host_id, &bus_config, dma_chan);
 if (ret != ESP_OK) {
           return;
 }
 ret = esp_vfs_fat_sdspi_mount(SDSPI_MOUNT, &sdspi_host, &sdspi_device, &mount_config, &sdspi_card);
 if(ret != ESP_OK) {
      return;
}

相關SD FAT filesystem操作可參閱千一篇文章(

EPS32 ESP-IDF開發環境儲存設備與檔案系統實驗(一) -- SDMMC 與SDSPI )



四、8-bit parallel TFT(with touch)  
ILI3941 ESP32 Driver使用https://github.com/nopnop2002/esp-idf-parallel-tft, 相關的檔案為ili3941, lcd_com, lcd_lib, fontx,與font檔案,TFT程式碼放在components/tft_library下,font資料夾下放font檔案與相關的UI button JPEG檔案。font資料夾會先預載到flash 的spiffs partition下。Kconfig.projbuild是相關tft menuconfig設定,放在`main資料夾下。相關位置如下圖
Flash partition tables:

五、成果展示:


六、main程式碼:

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include "audio_element.h"
#include "audio_pipeline.h"
#include "audio_event_iface.h"
#include "audio_common.h"
//#include "board.h"
#include "esp_peripherals.h"
#include "periph_sdcard.h"
#include "fatfs_stream.h"
#include "i2s_stream.h"
#include "wav_encoder.h"
#include "wav_decoder.h"
#include "lcd_com.h"
#include "lcd_lib.h"
#include "fontx.h"
#include "decode_jpeg.h"
#include "string.h"
#include "driver/i2s.h"

#if CONFIG_INTERFACE_I2S
#define INTERFACE INTERFACE_I2S
#elif CONFIG_INTERFACE_GPIO
#define INTERFACE INTERFACE_GPIO
#elif CONFIG_INTERFACE_REG
#define INTERFACE INTERFACE_REG
#endif

#include "ili9341.h"
#define DRIVER "ILI9340"
#define INIT_FUNCTION(a, b, c, d, e) ili9341_lcdInit(a, b, c, d, e)

#define RECORD_STATE	(1)
#define PLAY_STATE		(2)
#define PAUSE_STATE		(3)
#define STOP_STATE		(0)
#define VOLUME_MIN		(-50)
#define VOLUME_MAX		(50)
#define MAX_INDEX 		(1024)
int  volume=0;
static char **fltable=NULL;
static TFT_t dev;

static int16_t tindex=0, bindex=0, totalindex=0, lastnumber=0;
static int16_t selectindex=-1;
static char selectFile[11];

uint8_t gStatus=STOP_STATE; 
static TaskHandle_t hPlayWave, hRecordingTask;
static TaskHandle_t hPlayTask;
audio_element_handle_t fatfs_stream_reader, i2s_stream_writer, audio_decoder;
audio_element_handle_t fatfs_stream_writer, i2s_stream_reader, audio_encoder;

FontxFile fx16G[2];
FontxFile fx24G[2];
FontxFile fx32G[2];
FontxFile fx16M[2];
FontxFile fx24M[2];
FontxFile fx32M[2];


#define RECORD_TIME_SECONDS (20)

//static const char *TAG = "Audio Recoder";

#include "esp_vfs_fat.h"
#include "esp_vfs.h"
#include "driver/sdspi_host.h"
#include "esp_spiffs.h"

static sdmmc_card_t *sdspi_card;

#define SDSPI_MOUNT "/sdcard"

static audio_pipeline_handle_t rec_pipeline;
static audio_pipeline_handle_t play_pipeline;


void drawJPEG(TFT_t * dev, char * file, int px, int py, int width, int height) {
	lcdSetFontDirection(dev, 0);
	int _width = width;
	if (width > 240) _width = 240;
	int _height = height;
	if (height > 320) _height = 320;

	pixel_jpeg **pixels;
	uint16_t imageWidth;
	uint16_t imageHeight;
	esp_err_t err = decode_jpeg(&pixels, file, _width, _height, &imageWidth, &imageHeight);
	if (err == ESP_OK) {
		uint16_t jpegWidth = width;
		uint16_t offsetX = px;
		if (width > imageWidth) {
			jpegWidth = imageWidth;
		}
		uint16_t jpegHeight = height;
		uint16_t offsetY = py;
		if (height > imageHeight) {
			jpegHeight = imageHeight;
		}
		uint16_t *colors = (uint16_t*)malloc(sizeof(uint16_t) * jpegWidth);


		for(int y = 0; y < jpegHeight; y++){
			for(int x = 0;x < jpegWidth; x++){
				colors[x] = pixels[y][x];
				}
			lcdDrawMultiPixels(dev, offsetX, y+offsetY, jpegWidth, colors);
		}

		free(colors);
		release_image(&pixels, _width, _height);

	}
}


int button[7][2] = {{15,275},{60,275},{105,275},{150,275},{195,275},{202,10},{202,220}};

bool getTouchPos(TFT_t * dev, int *posx, int *posy) {
	
	float _xd = dev->_max_xp - dev->_min_xp;
	float _yd = dev->_max_yp - dev->_min_yp;
	
	float _xs = dev->_max_xc - dev->_min_xc;
	float _ys = dev->_max_yc - dev->_min_yc;
	//ESP_LOGD(TAG, "_xs=%f _ys=%f", _xs, _ys);

	int _xpos = 0;
	int _ypos = 0;

	int _xp;
	int _yp;
	if (touch_getxy(dev, &_xp, &_yp)) { 
		if (dev->_max_xp > dev->_min_xp) {
			if (_xp < dev->_min_xp && _xp > dev->_max_xp) return false;
		} else {
			if (_xp < dev->_max_xp && _xp > dev->_min_xp) return false;
		}
		if (dev->_max_yp > dev->_min_yp) {
			if (_yp < dev->_min_yp && _yp > dev->_max_yp) return false;
		} else {
			if (_yp < dev->_max_yp && _yp > dev->_min_yp) return false;
		}
		// Convert from position to coordinate
		_xpos = ( (float)(_xp - dev->_min_xp) / _xd * _xs ) + dev->_min_xc;
		_ypos = ( (float)(_yp - dev->_min_yp) / _yd * _ys ) + dev->_min_yc;
		*posx = _xpos;
		*posy = _ypos;
		return true;
	} else {
		return false;
	}
}

void playWave(void* param) {
	char *file[5] = {"/spiffs/w0.jpg", "/spiffs/w1.jpg", "/spiffs/w2.jpg", "/spiffs/w3.jpg", "/spiffs/w4.jpg"};
	int i = 0;
	lcdDrawFillRect(&dev,6,6,199,259,BLACK);
	while (1) {
		drawJPEG(&dev, file[i], 15, 80, 180, 100);
		vTaskDelay(500/portTICK_PERIOD_MS);
		i = (i+1) % 5;
	}
}

void drawBackground(TFT_t *dev) {
	lcdFillScreen(dev, BLACK);
	lcdDrawRect(dev, 5,5,200, 260,  CYAN);
	lcdDrawRect(dev, 201,5,239, 260,  CYAN);
	drawJPEG(dev, "/spiffs/mic.jpg", 15, 275, 36,36);
	drawJPEG(dev, "/spiffs/play.jpg", 60, 275, 36,36);
	drawJPEG(dev, "/spiffs/stop.jpg", 105, 275, 36,36);
	drawJPEG(dev, "/spiffs/volume_up.jpg", 150, 275, 36,36);
	drawJPEG(dev, "/spiffs/volume_down.jpg", 195, 275, 36,36);
	drawJPEG(dev, "/spiffs/up.jpg", 202, 10, 36,36);
	drawJPEG(dev, "/spiffs/down.jpg", 202, 220, 36,36);

	fltable = malloc(MAX_INDEX*sizeof(char*));
	char tf[12];
	lastnumber = 0;
	totalindex=-1;
	DIR* dir = opendir("/sdcard/");
	assert(dir != NULL);
	while (true) {
		struct dirent*pe = readdir(dir);
		if (!pe) break;
		if ((pe->d_name)[0] == 'v' && strlen(pe->d_name) >=6){
			totalindex++;
			fltable[totalindex] = malloc(strlen(pe->d_name)*sizeof(char));
			strcpy(fltable[totalindex], pe->d_name);
			strncpy(tf, pe->d_name+1,5);
			int i = atoi(tf);
			if (lastnumber < i) lastnumber = i;
		}
	}
	closedir(dir);	

	int items;
	if (totalindex < 9) items = totalindex+1; else items = 10;
	tindex=0;
	for (int i = 0; i < items; i++) {
		bindex=i;
		lcdDrawString(dev, fx24G, 15, 10+(i+1)*24, (uint8_t*)fltable[i], WHITE);
	}
}
void TFT_Touch_init(bool touchEnable) {
	esp_vfs_spiffs_conf_t conf = {
		.base_path = "/spiffs",
		.partition_label = NULL,
		.max_files = 10,
		.format_if_mount_failed =true
	};
	esp_err_t ret = esp_vfs_spiffs_register(&conf);

	if (ret != ESP_OK) {
		if (ret == ESP_FAIL) {
			//ESP_LOGE(TAG, "Failed to mount or format filesystem");
		} else if (ret == ESP_ERR_NOT_FOUND) {
			//ESP_LOGE(TAG, "Failed to find SPIFFS partition");
		} else {
			//ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)",esp_err_to_name(ret));
		}
		return;
	}

	size_t total = 0, used = 0;
	ret = esp_spiffs_info(NULL, &total,&used);
	if (ret != ESP_OK) {
		//ESP_LOGE(TAG,"Failed to get SPIFFS partition information (%s)",esp_err_to_name(ret));
	} else {
		//ESP_LOGI(TAG,"Partition size: total: %d, used: %d", total, used);
	}

	// set font file
	InitFontx(fx16G,"/spiffs/ILGH16XB.FNT",""); // 8x16Dot Gothic
	InitFontx(fx24G,"/spiffs/ILGH24XB.FNT",""); // 12x24Dot Gothic
	InitFontx(fx32G,"/spiffs/ILGH32XB.FNT",""); // 16x32Dot Gothic
	
	InitFontx(fx16M,"/spiffs/ILMH16XB.FNT",""); // 8x16Dot Mincyo
	InitFontx(fx24M,"/spiffs/ILMH24XB.FNT",""); // 12x24Dot Mincyo
	InitFontx(fx32M,"/spiffs/ILMH32XB.FNT",""); // 16x32Dot Mincyo
	//TFT_t dev;
	lcd_interface_cfg(&dev, INTERFACE);

	INIT_FUNCTION(&dev, CONFIG_WIDTH, CONFIG_HEIGHT, CONFIG_OFFSETX, CONFIG_OFFSETY);

	if (touchEnable) {
		int gpio_xp = dev._d6;
		int gpio_xm = dev._rs;
		int gpio_yp = dev._wr;
		int gpio_ym = dev._d7;
		touch_interface_cfg(&dev, CONFIG_ADC_CHANNEL_YP, CONFIG_ADC_CHANNEL_XM, gpio_xp, gpio_xm, gpio_yp, gpio_ym);
	}
}
void TouchCalibration(TFT_t * dev, int width, int height) {
	if (dev->_calibration == false) return;
	lcdFillScreen(dev, BLACK);
	// get font width & height
	uint8_t buffer[FontxGlyphBufSize];
	uint8_t fontWidth;
	uint8_t fontHeight;
	GetFontx(fx24G, 0, buffer, &fontWidth, &fontHeight);
	//ESP_LOGD(__FUNCTION__,"fontWidth=%d fontHeight=%d",fontWidth,fontHeight);

	uint8_t ascii[24];
	int xpos = 0;
	int ypos = 0;

	// Calibration
	lcdFillScreen(dev, BLACK);
	dev->_min_xc = 15;
	dev->_min_yc = 15;
	lcdDrawFillCircle(dev, dev->_min_xc, dev->_min_yc, 10, CYAN);
	strcpy((char *)ascii, "Calibration");
	ypos = ((height - fontHeight) / 2) - 1;
	xpos = (width - (strlen((char *)ascii) * fontWidth)) / 2;
	lcdSetFontDirection(dev, DIRECTION0);
	lcdDrawString(dev, fx24G, xpos, ypos, ascii, WHITE);
	ypos = ypos + fontHeight;
	int _xpos = xpos;
	
	for(int i=0;i<10;i++) {
		lcdDrawFillCircle(dev, _xpos, ypos, fontWidth/2, RED);
		_xpos = _xpos + fontWidth + 5;
	}

	int32_t xp = 0;
	int32_t yp = 0;
	int counter = 0;
	while(1) {
		vTaskDelay(1);
		int _xp;
		int _yp;
		if (touch_getxy(dev, &_xp, &_yp) == false) continue;
		xp += _xp;
		yp += _yp;
		//ESP_LOGI(TAG, "counter=%d _xp=%d _yp=%d xp=%d yp=%d", counter, _xp, _yp, xp, yp);
		counter++;
		if (counter == 100) break;
		if ((counter % 10) == 0) {
			lcdDrawFillCircle(dev, xpos, ypos, fontWidth/2, GREEN);
			xpos = xpos + fontWidth + 5;
		}
	} // end while
	dev->_min_xp = xp/100;
	dev->_min_yp = yp/100;
	//ESP_LOGI(TAG, "_min_xp=%d _min_yp=%d", dev->_min_xp, dev->_min_yp);

	lcdFillScreen(dev, BLACK);
	dev->_max_xc = width-10;
	dev->_max_yc = height-10;
	lcdDrawFillCircle(dev, dev->_max_xc, dev->_max_yc, 10, CYAN);
	ypos = ((height - fontHeight) / 2) - 1;
	xpos = (width - (strlen((char *)ascii) * fontWidth)) / 2;
	lcdDrawString(dev, fx24G, xpos, ypos, ascii, WHITE);
	ypos = ypos + fontHeight;
	_xpos = xpos;
	for(int i=0;i<10;i++) {
		lcdDrawFillCircle(dev, _xpos, ypos, fontWidth/2, RED);
		_xpos = _xpos + fontWidth + 5;
	}

	xp = 0;
	yp = 0;
	counter=0;
	while(1) {
		vTaskDelay(1);
		int _xp;
		int _yp;
		if (touch_getxy(dev, &_xp, &_yp) == false) continue;
		if (_xp < dev->_min_xp+100 || _yp < dev->_min_yp+100) continue;
		xp += _xp;
		yp += _yp;
		counter++;
		if (counter == 100) break;
		if ((counter % 10) == 0) {
			lcdDrawFillCircle(dev, xpos, ypos, fontWidth/2, GREEN);
			xpos = xpos + fontWidth + 5;
		}
	} // end while
	
	dev->_max_xp = xp/100;
	dev->_max_yp = yp/100;
	dev->_calibration = false;
}


void vTaskRecord(void *param) {
    esp_err_t ret;
    audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
    rec_pipeline = audio_pipeline_init(&pipeline_cfg);
    mem_assert(rec_pipeline);

    fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT();
	fatfs_cfg.task_core=APP_CPU_NUM;
    fatfs_cfg.type = AUDIO_STREAM_WRITER;
    fatfs_stream_writer = fatfs_stream_init(&fatfs_cfg);

    i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
	i2s_cfg.task_core = APP_CPU_NUM;
    i2s_cfg.type = AUDIO_STREAM_READER;
    i2s_cfg.i2s_config.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT;
    i2s_cfg.use_alc = true;
    i2s_cfg.volume=-63;
	i2s_stream_reader = i2s_stream_init(&i2s_cfg);	

    i2s_pin_config_t rp = {
        .bck_io_num = 5, 
        .ws_io_num = 25,
        .data_out_num = -1,
        .data_in_num = 39 //35
    };
    i2s_set_pin(I2S_NUM_0, &rp);

    wav_encoder_cfg_t wav_cfg = DEFAULT_WAV_ENCODER_CONFIG();
	wav_cfg.task_core = APP_CPU_NUM;
    audio_encoder = wav_encoder_init(&wav_cfg);
   
    audio_pipeline_register(rec_pipeline, i2s_stream_reader, "i2s");
    audio_pipeline_register(rec_pipeline, audio_encoder, "wav");
    audio_pipeline_register(rec_pipeline, fatfs_stream_writer, "file");

    const char *link_tag[3] = {"i2s", "wav", "file"};
    audio_pipeline_link(rec_pipeline, &link_tag[0], 3);

	audio_element_info_t music_info = {0};
    audio_element_getinfo(i2s_stream_reader, &music_info);
    audio_element_setinfo(fatfs_stream_writer, &music_info);

    char filename[100];
    sprintf(filename, "%s/%s", SDSPI_MOUNT, (char*) param);
    audio_element_set_uri(fatfs_stream_writer, filename);
    
    audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
    audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg);

    audio_pipeline_set_listener(rec_pipeline, evt);

    i2s_alc_volume_set(i2s_stream_reader, -63);
	
    audio_pipeline_run(rec_pipeline);
    
	vTaskDelay(310/portTICK_PERIOD_MS);
	i2s_alc_volume_set(i2s_stream_reader, 20);
	int second_recorded = 0;
    while (1) {
        audio_event_iface_msg_t msg;
		ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY);
		if (ret != ESP_OK) {
            continue;
        }
		/*
        if (audio_event_iface_listen(evt, &msg, 1000/portTICK_PERIOD_MS) != ESP_OK) {
            second_recorded ++;
            if (second_recorded >= RECORD_TIME_SECONDS) {
               audio_element_set_ringbuf_done(i2s_stream_reader);
            }
			continue;
        }
		*/

        /* Stop when the last pipeline element (fatfs_stream_writer in this case) receives stop event */
        if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && msg.source == (void *) fatfs_stream_writer
            && msg.cmd == AEL_MSG_CMD_REPORT_STATUS
            && (((int)msg.data == AEL_STATUS_STATE_STOPPED) || ((int)msg.data == AEL_STATUS_STATE_FINISHED)
                || ((int)msg.data == AEL_STATUS_ERROR_OPEN))) {
            break;
        }
    }
    
	audio_pipeline_stop(rec_pipeline);
    audio_pipeline_wait_for_stop(rec_pipeline);
    audio_pipeline_terminate(rec_pipeline);

    audio_pipeline_unregister(rec_pipeline, audio_encoder);
    audio_pipeline_unregister(rec_pipeline, i2s_stream_reader);
    audio_pipeline_unregister(rec_pipeline, fatfs_stream_writer);


    /* Terminal the pipeline before removing the listener */
    audio_pipeline_remove_listener(rec_pipeline);

    /* Make sure audio_pipeline_remove_listener & audio_event_iface_remove_listener are called before destroying event_iface */
    audio_event_iface_destroy(evt);

    /* Release all resources */
    audio_pipeline_deinit(rec_pipeline);
    audio_element_deinit(fatfs_stream_writer);
    audio_element_deinit(i2s_stream_reader);
    audio_element_deinit(audio_encoder);

    vTaskDelete(NULL);

}

void vTaskPlay(void *param) {
    
	audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
    play_pipeline = audio_pipeline_init(&pipeline_cfg);
    mem_assert(play_pipeline);

    //ESP_LOGI(TAG, "[Playing] Create fatfs stream to read data from sdcard");
    fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT();
    fatfs_cfg.type = AUDIO_STREAM_READER;
    fatfs_stream_reader = fatfs_stream_init(&fatfs_cfg);

    //ESP_LOGI(TAG, "[Playing]  Create i2s stream to write audio data to MAX98357A");
    i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
    i2s_cfg.type = AUDIO_STREAM_WRITER;
    i2s_cfg.use_alc = true;
    i2s_cfg.volume=0;
	i2s_stream_writer = i2s_stream_init(&i2s_cfg);

    i2s_pin_config_t wp = {
        .bck_io_num = 22,
        .ws_io_num = 25,
        .data_out_num = 26, 
        .data_in_num = -1
    };
    i2s_set_pin(I2S_NUM_0, &wp);

    wav_decoder_cfg_t wav_cfg = DEFAULT_WAV_DECODER_CONFIG();
    audio_decoder = wav_decoder_init(&wav_cfg);
    
	audio_pipeline_register(play_pipeline, fatfs_stream_reader, "file");
    audio_pipeline_register(play_pipeline, audio_decoder, "wav");
    audio_pipeline_register(play_pipeline, i2s_stream_writer, "i2s");

    const char *link_tag[3] = {"file", "wav", "i2s"};
    audio_pipeline_link(play_pipeline, &link_tag[0], 3);
    
	char filename[100];
    sprintf(filename, "%s/%s", SDSPI_MOUNT, (char*) param);
    printf("%s\n", filename);
    audio_element_set_uri(fatfs_stream_reader, filename);
    
	audio_event_iface_cfg_t evt_cfg = AUDIO_EVENT_IFACE_DEFAULT_CFG();
    audio_event_iface_handle_t evt = audio_event_iface_init(&evt_cfg);

	audio_pipeline_set_listener(play_pipeline, evt);

    audio_pipeline_run(play_pipeline);

   
    while (1) {
        audio_event_iface_msg_t msg;
         esp_err_t ret = audio_event_iface_listen(evt, &msg, portMAX_DELAY);
        if (ret != ESP_OK) {
            continue;
        }
		
        if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT
            && msg.source == (void *) audio_decoder
            && msg.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) {
                audio_element_info_t music_info = {0};
            audio_element_getinfo(audio_decoder, &music_info);
            audio_element_setinfo(i2s_stream_writer, &music_info);
            i2s_stream_set_clk(i2s_stream_writer, music_info.sample_rates, music_info.bits, music_info.channels);
            continue;
        }
	
        if (msg.source_type == AUDIO_ELEMENT_TYPE_ELEMENT
            && (msg.source == (void *) i2s_stream_writer || msg.source == (void *) fatfs_stream_reader)
            && msg.cmd == AEL_MSG_CMD_REPORT_STATUS && 
			(((int)msg.data == AEL_STATUS_STATE_STOPPED) || ((int)msg.data == AEL_STATUS_STATE_FINISHED))) {            
            break;
        }
    }
    ////ESP_LOGI(TAG, "[Playing]  Stop audio_pipeline");
    audio_pipeline_stop(play_pipeline);
    audio_pipeline_wait_for_stop(play_pipeline);
    audio_pipeline_terminate(play_pipeline);

	audio_pipeline_unregister(play_pipeline, fatfs_stream_reader);
    audio_pipeline_unregister(play_pipeline, audio_decoder);
    audio_pipeline_unregister(play_pipeline, i2s_stream_writer);
   
    /* Terminal the pipeline before removing the listener */
    audio_pipeline_remove_listener(play_pipeline);

    /* Make sure audio_pipeline_remove_listener & audio_event_iface_remove_listener are called before destroying event_iface */
    audio_event_iface_destroy(evt);

    /* Release all resources */
    audio_pipeline_deinit(play_pipeline);
    audio_element_deinit(fatfs_stream_reader);
    audio_element_deinit(i2s_stream_writer);
    audio_element_deinit(audio_decoder);
	gStatus = PAUSE_STATE;
	drawJPEG(&dev, "/spiffs/play.jpg", button[1][0], button[1][1], 36,36);

	vTaskDelete(NULL);

}

void app_main(void)
{
    esp_err_t ret;
	audio_element_state_t m_el_state;
	TFT_Touch_init(true);
	TouchCalibration(&dev, CONFIG_WIDTH, CONFIG_HEIGHT);
	lcdDrawFillRect(&dev, 1,1,239,319, BLACK);

    
// mount sdspi sd card
    sdmmc_host_t sdspi_host = SDSPI_HOST_DEFAULT();
    sdspi_device_config_t sdspi_device = SDSPI_DEVICE_CONFIG_DEFAULT();
    spi_bus_config_t bus_config = {
        .miso_io_num = GPIO_NUM_19,
        .mosi_io_num = GPIO_NUM_23,
        .sclk_io_num = GPIO_NUM_18
    };
    sdspi_device.host_id = VSPI_HOST;
    spi_dma_chan_t dma_chan = SPI_DMA_CH1;
    esp_vfs_fat_mount_config_t mount_config = {
        .format_if_mount_failed=true,
        .max_files = 5,
        .allocation_unit_size=16*512   //8192 
    };
    ret = spi_bus_initialize(sdspi_device.host_id, &bus_config, dma_chan);
    if (ret != ESP_OK) {
		lcdDrawString(&dev, fx16M, 10,50, (uint8_t*)"SPI BUS initialize error", WHITE);
		vTaskDelay(2000/portTICK_PERIOD_MS);
        return;
    }
    ret = esp_vfs_fat_sdspi_mount(SDSPI_MOUNT, &sdspi_host, &sdspi_device, &mount_config, &sdspi_card);
    if(ret != ESP_OK) {
		lcdDrawString(&dev, fx16M, 10,50, (uint8_t*)"SDSPI sdcard mount error", WHITE);
		vTaskDelay(2000/portTICK_PERIOD_MS);
        return;
    }
	
	lcdDrawString(&dev, fx16M, 10,50, (uint8_t*)"SDSPI sdcard mounted", WHITE);
	vTaskDelay(1000/portTICK_PERIOD_MS);
	drawBackground(&dev);

	int posx, posy;
	int selItem=-1;
	char fileName[12];

	while(1) {
		vTaskDelay(100/portTICK_PERIOD_MS);
		if (getTouchPos(&dev, &posx, &posy)) {
			selItem = -1;
			for (int i = 0 ; i < 7; i++) {
				if (posx >= button[i][0]-5 && posx <= button[i][0]+41 && 
					posy >= button[i][1]-5 && posy <= button[i][1]+41) { //+=5
					selItem=i;
					break;
				}
			}
			switch(selItem) {
				case 0:
				if (gStatus != RECORD_STATE) {
					if (gStatus != STOP_STATE) {
						// stop play
						m_el_state = audio_element_get_state(i2s_stream_writer);
						if (m_el_state == AEL_STATE_RUNNING || m_el_state == AEL_STATE_PAUSED){
							audio_pipeline_stop(play_pipeline);
							audio_pipeline_wait_for_stop(play_pipeline);
							drawJPEG(&dev, "/spiffs/play.jpg", button[1][0], button[1][1], 36,36);
						}
					
					}
					gStatus = RECORD_STATE;
					lastnumber++;
					sprintf(fileName, "v%05d.wav", lastnumber);
					//lcdDrawFillRect(&dev, 6,6,199,259, BLACK);
					//lcdDrawString(&dev, fx32G, 15, 100, (uint8_t*)"Recording...",WHITE);
					xTaskCreatePinnedToCore(playWave, "play Wave", 1024*3, NULL, 3, &hPlayWave, PRO_CPU_NUM);
					xTaskCreatePinnedToCore(vTaskRecord, "record task", 1024*4, fileName, 3, &hRecordingTask, APP_CPU_NUM);
					selectindex=-1;
				}
				break;
				case 1:
				if (selectindex != -1 && gStatus == STOP_STATE) {
							xTaskCreatePinnedToCore(vTaskPlay, "play task", 1024*4, selectFile, 3, &hPlayTask, APP_CPU_NUM);
							drawJPEG(&dev, "/spiffs/pause.jpg", button[1][0], button[1][1], 36,36);
							gStatus = PLAY_STATE;
				} else {
					
					m_el_state = audio_element_get_state(i2s_stream_writer);
					if (m_el_state == AEL_STATE_PAUSED){
						audio_pipeline_resume(play_pipeline);
						gStatus = PLAY_STATE;
						drawJPEG(&dev, "/spiffs/pause.jpg", button[1][0], button[1][1], 36,36);
					} else if (m_el_state == AEL_STATE_RUNNING){
						audio_pipeline_pause(play_pipeline);
						gStatus = PAUSE_STATE;
						drawJPEG(&dev, "/spiffs/play.jpg", button[1][0], button[1][1], 36,36);
					} 
					
				}
				break;
				case 2:
					if (gStatus == PAUSE_STATE || gStatus == PLAY_STATE) {
						drawJPEG(&dev, "/spiffs/play.jpg", button[1][0], button[1][1], 36,36);
						//m_el_handle = audio_pipeline_get_el_by_tag(hPlayTask, "is2");
						//audio_element_set_ringbuf_done(fatfs_stream_reader);
						audio_pipeline_stop(play_pipeline);
						audio_pipeline_wait_for_stop(play_pipeline);
					} 
					if (gStatus == RECORD_STATE && totalindex < MAX_INDEX-1) {
						totalindex++;
						audio_pipeline_stop(rec_pipeline);
						audio_pipeline_wait_for_stop(rec_pipeline);
						vTaskDelete(hPlayWave);
						fltable[totalindex] = malloc(11*sizeof(char));
						sprintf(fileName,"v%05d.wav", lastnumber);
						strcpy(fltable[totalindex], fileName);
						bindex=totalindex;
						if (bindex > 9) tindex = bindex-9; else tindex=0;
						selectindex = -1;
						lcdDrawFillRect(&dev, 6,6,199,259, BLACK);
						for (int i = tindex; i <= bindex; i++) {
							lcdDrawString(&dev, fx24G, 15, 10+(i-tindex+1)*24, (uint8_t*)fltable[i], WHITE);
						}
					}
					gStatus = STOP_STATE;
				break;
				case 3:
					if (gStatus == PLAY_STATE) {
						ret = i2s_alc_volume_get(i2s_stream_writer, &volume);
						if (ret == ESP_OK) {
							if (volume < VOLUME_MAX) {
								volume = volume+2;
								i2s_alc_volume_set(i2s_stream_writer, volume);
							}
						}
					}
					if (gStatus == RECORD_STATE) {
						ret = i2s_alc_volume_get(i2s_stream_reader, &volume);
						if (ret == ESP_OK) {
							if (volume < VOLUME_MAX) {
								volume = volume+2;
								i2s_alc_volume_set(i2s_stream_reader, volume);
							}
						}
					}
				break;
				case 4:
				if (gStatus == PLAY_STATE) {
						ret = i2s_alc_volume_get(i2s_stream_writer, &volume);
						if (ret == ESP_OK) {
							if (volume > VOLUME_MIN) {
								volume = volume-2;
								i2s_alc_volume_set(i2s_stream_writer, volume);
							}
						}
					}
					if (gStatus == RECORD_STATE) {
						ret = i2s_alc_volume_get(i2s_stream_reader, &volume);
						if (ret == ESP_OK) {
							if (volume > VOLUME_MIN) {
								volume = volume-2;
								i2s_alc_volume_set(i2s_stream_reader, volume);
							}
						}
					}
				break;
				case 5:
					if (tindex == 0 || bindex - tindex < 9) break;
					tindex--;
					selectindex=-1;
					lcdDrawFillRect(&dev, 6,6,199,259, BLACK);
					for (int i = tindex; i < tindex+10; i++) {
						bindex=i;
						lcdDrawString(&dev, fx24G, 15, 10+(i-tindex+1)*24, (uint8_t*)fltable[i], WHITE);
					}
				break;
				case 6:
					if (bindex == totalindex || bindex - tindex < 9) break;
					tindex++;
					selectindex=-1;
					lcdDrawFillRect(&dev, 6,6,199,259, BLACK);
					for (int i = tindex; i < tindex+10; i++) {
						bindex=i;
						lcdDrawString(&dev, fx24G, 15, 10+(i-tindex+1)*24, (uint8_t*)fltable[i], WHITE);
					}
				break;
				default:
					if (posx > 5 && posx < 200 && posy > 5 && posy < 260) {
						if (selectindex != -1) {
							lcdDrawFillRect(&dev, 15, 10+(selectindex-tindex)*24, 199,10+(selectindex-tindex+1)*24, BLACK);
							lcdDrawString(&dev, fx24G, 15, 10+(selectindex-tindex+1)*24, (uint8_t*)fltable[selectindex], WHITE);
						}
						selectindex = (posy-5)/24+tindex;
						if (selectindex <= totalindex && selectindex-tindex < 10) {
							strcpy(selectFile, fltable[selectindex]);
							lcdDrawFillRect(&dev, 15, 10+(selectindex-tindex)*24, 199,10+(selectindex-tindex+1)*24, BLUE);
							lcdDrawString(&dev, fx24G, 15, 10+(selectindex-tindex+1)*24, (uint8_t*)fltable[selectindex], WHITE);
						} else {
							selectindex = -1;
						}
						
					}
				break;
			}
		}

	}
    esp_vfs_fat_sdcard_unmount(SDSPI_MOUNT, sdspi_card);   
}