prettyprint

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);   
}
	

2022年2月10日 星期四

EPS32 ESP-IDF開發環境儲存設備與檔案系統實驗(二) -- SPI FLASH (A Study on ESP32 Storage Devices and File System--SPI Flash using ESP-IDF)

本實驗是在ESP-IDF環境下,測試ESP32 spi flash storage。使用內建(main flash)與外接flash(external falsh) ,分別測試nvs, spiffs 與fat filesystem。

本實驗使用的外接flash為Winbond W25Q128FV外接模組,


共有VCC, GND, CS, CLK, DI 與DO pins,IO2與IO3未接出。根據W25Q128FV Datasheeet只能操作在standard spi 與dual spi operation。pin No.相對應表如下圖。

(取自Winbond W25Q128FV datasheet)

ESP32 ESP-IDF環境下直接支援Winbond flash,memuconfig預設是開啟支援的

一、加入 SPI Flash device 

  • 內建main flash在memuconfig 中指定mode, speed等,例如QIO, 80MHz


  • 外接SPI Flash依序以下列三個API就能加入系統中
  1. spi_bus_initialize(): 決定使用哪一個SPI HOST,MISO、MOSI、CLK pin和 DMA channel.
  2. spi_bus_add_flash_device():  附加 flash device 在SPI bus上.  指定CS pin, I/O Mode(dio、qio) 與spi bus speed等。
  3. esp_flash_init(): 開始與flash溝通. 偵測chip型號等。
三個API參數詳細說明如下:
esp_err_t spi_bus_initialize(spi_host_device_t host_id, const spi_bus_config_t * bus_config, spi_dma_chan_t dma_chan)
  1. host_id:SPI1_HOST, SPI2_HOST(HSPI_HOST), SPI3_HOST(VSPI_HOST)。例如
    VSPI_HOST。
  2. spi_bus_config_t: 指定miso_io_num, mosi_io_num and sclk_io_num,本實驗例子spi_bus_config_t bus_config = {
            .mosi_io_num = GPIO_NUM_23,
            .miso_io_num = GPIO_NUM_19,
            .sclk_io_num = GPIO_NUM_18 
        };
  3. spi_dma_chan_t: DMA channel,例如
    spi_dma_chan_t spi_dma_chan = SPI_DMA_CH2;
esp_err_t spi_bus_add_flash_device(esp_flash_t ** out_chip, const esp_flash_spi_device_config_t * config)
  1. esp_flash_t: 代表此flash device的pointer,例如
    esp_flash_t * flash_chip;
  2. esp_flash_spi_device_config_t: 指定此flash device的參數,io_mode, speed, cs pin。例如esp_flash_spi_device_config_t spi_device_config = {
            .host_id = VSPI_HOST,
            .cs_id = 0,
            .cs_io_num = 5,
            .io_mode = SPI_FLASH_DIO,
            .speed = ESP_FLASH_40MHZ
        };
esp_err_t IRAM_ATTR esp_flash_init(esp_flash_t * chip);

經過這三個API設定,順利加入spi flash device後,代表此flash device的pointer(上例為:flash_chip)即可以此flash_chip作為後續API的參數。測試系統順利附加spi flash device,如下圖。
(執行結果截圖)

二、分割SPI Flash Device Partition

flash 的partition內容分為幾個部分:
  1. label: 代表此partition名稱。
  2. type: app or data。
  3. subtype: app 或data的subtype,本實驗操作的三個檔案系統為type是data的subtype分別為nvs, spiffs, fat
  4. offset: 位移位址
  5. size:大小
  6. flags: 指定是否encrypted

  • 內建flash(main flash) partition建立方式:

內建flash(main flash) 的partition table可在menuconfig中指定使用Custom partition CSV,如下圖。
製作csv檔案,build project即可為內建flash建立所需的partitions,例如

  • 外接SPI flash partition table建立方式:

使用API esp_partition_register_external來將外接SPI Flash指定每個partition 的label, type, subtype, offset,size等參數,將partition table加入的flash_chip中。
此API詳細參數說明如下:
esp_err_t esp_partition_register_external(esp_flash_t * flash_chip, size_t offset, size_t size, const char * label, esp_partition_type_t type, esp_partition_subtype_t subtype, const esp_partition_t ** out_partition)
  1. esp_flash_t: 上述spi_bus_add_flash_device API, return 的 flshp_chip
  2. offset: 指定 offset
  3. size: 指定大小
  4. esp_partition_type_t: 例如ESP_PARTITION_TYPE_DATA為data type
  5. esp_partition_subtype_t:例如 ESP_PARTITION_SUBTYPE_DATA_FAT為fat subtype,
  6. esp_partiton_t: 為output partition pointer作為後續處理partition用,例如本實驗中fat_part, nvs_part等。
本實驗partition的lable, offset, size分別如下列設定:
const size_t fatOffset = 0, fatSize=8*1024*1024; // 8MB, use for FAT partition
const size_t nvsOffset = 8*1024*1024, nvsSize = 1024*1024; // 1MB, use for nvs partition
const size_t spiffsOffset = 9*1024*1024, spiffsSize = 2*1024*1024; // 2M, user for spiffs partition

const esp_partition_t *fat_part;
const esp_partition_t *nvs_part;
const esp_partition_t *spiffs_part;

// partition label
const char extern_flash_fat[] = "extFATData";
const char extern_flash_nvs[] = "extNVS";
const char extern_flash_spiffs[] = "extSPIFFS";

三、掛載SPI Flash Device Partition

本實驗使用nvs, spiffs, fat 三種 flash partitions,各別partition type說明如下:

NVS partition

    Non-volatile storage (NVS) 常用來儲存 key-value 資料在flash中。partition 的type為data, subtype 為nvs,當partition label 為 nvs 稱為default NVS partition。
    首先需初始化,nvs_flash_init(void)初始化default nvs partiton,使用nvs_flash_init_partiton(const char *partition_label)初始化其他nvs parttiton,例如nvs_flash_init_partiton("extNVS");
其他常用使令:
  1. nvs_flash_erase,nvs_flash_erase_partition:抹除整個partition。
  2. nvs_open,  nvs_open_from_partition: 開啟namespace, readonly or readwrite mode
  3. nvs_set_*: 設定key-value, value可為string, blob , integer or usigned integer。
  4. nvs_get_*: 取得key的value
  5. nvs_commit:寫入到flash中
  6. nvs_close:關閉並釋放資源

SPIFFS partition 

Flash的區塊有erase/write次數的限制,同一個區塊重複erase/write,將會造成讀取速度變慢,甚至損壞。使用Wear Leveling(耗損平均技術)能平均使用快閃記憶體中的每個儲存區塊,來避免某些儲存區塊因過度使用而形成壞區塊。

SPIFFS is a file system 常使用在內鑲系統 SPI NOR flash 設備上. 支援 Wear Leveling, file system 一致性檢查, 但不支援目錄架構。

使用 esp_vfs_spiffs_register掛載partition在vfs後,即可使用標準C file library處理spiffs檔案
esp_err_t esp_vfs_spiffs_register(const esp_vfs_spiffs_conf_t * conf)
參數esp_vfs_spiffs_conf_t指定掛載base path, partition,最大可開啟檔案數,掛載失敗是否要格式化等,例如:
esp_vfs_spiffs_conf_t spiffsConf = {
        .base_path="/spiffs",
        .partition_label = "extSPIFFS", 
        .max_files=5,
        .format_if_mount_failed=true
    };
當掛在extSPIFFS partition的base path為"/spiffs"時,若要開啟"test.txt" 檔案,以指令fopen("/spiffs/test.txt","w");開啟謝入模式。

esp_vfs_spiffs_unregister:從VFS卸載SPIFFS。

FAT partition

使用FAT filesystem在Flash上與使用在SD Card上類似,但增加Wear Levelling設定。
常用指令:
esp_vfs_fat_spiflash_mount, esp_vfs_fat_spiflash_unmount, 

掛載 partition API:
esp_err_t esp_vfs_fat_spiflash_mount(const char *base_path, const char *partition_label, const esp_vfs_fat_mount_config_t *mount_config, wl_handle_t *wl_handle)

ESP-IDF 使用此單一API,完成下堆堆疊動作:把partition table中相對應的partition label做Wear Levelling library初始化,使用FATFS library掛載FAT partition在wear liveiling libarary上,最後註冊VFS的base path,之後即以standard C file libaray存取flash device上的檔案。
API參數說明如下:
  1. base_path: 為掛載partition label的base path,例如:"/spiflash"。
  2. partition label: 欲掛載的 partition label,例如:"extFATData":
  3. esp_vsf_fat_mount_config_t: 說明掛載失敗是否格式化,最大可開啟檔案數等,例如esp_vfs_fat_mount_config_t spiflash_mount_config = {
            .format_if_mount_failed = true,
            .max_files=3,
            .allocation_unit_size=CONFIG_WL_SECTOR_SIZE
        };
  4. wl_handle_t: wear levelling handle,作為後續其他API使用的參數,例如:
    static wl_handle_t wl_handle = WL_INVALID_HANDLE;

卸載partition API:
esp_err_t esp_vfs_fat_spiflash_unmount(const char * base_path, wl_handle_t wl_handle);


四、實驗結果

第一次使用flsah device,尚未分割partition與格式化, spiffs與fat filesystem根據esp_vfs_fat_mount_t指定.format_if_mount_failed = true,自動格式化該partition,如下圖所


分別在內建flash(main flash)與外加flash模組開啟nvs, spiffs 與 FAT partition,個別執行read/write,結果如下圖所示:


五、程式碼

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_log.h"
#include "driver/sdspi_host.h"
#include "esp_partition.h"
#include "esp_spi_flash.h"
#include "esp_spiffs.h"
#include "esp_flash.h"
#include "esp_vfs_fat.h"
#include "esp_flash_spi_init.h"
#include "nvs_flash.h"
#include "esp_spiffs.h"

#define TAG "SPI FLASH TEST"

static esp_flash_t *flash_chip;
static wl_handle_t wl_handle = WL_INVALID_HANDLE;

const char extern_flash_fat[] = "extFATData";
const char extern_flash_nvs[] = "extNVS";
const char extern_flash_spiffs[] = "extSPIFFS";
const size_t fatOffset = 0, fatSize=8*1024*1024; // 8MB
const size_t nvsOffset = 8*1024*1024, nvsSize = 1024*1024; // 1MB
const size_t spiffsOffset = 9*1024*1024, spiffsSize = 2*1024*1024; // 2M


    
const esp_partition_t *fat_part;
const esp_partition_t *nvs_part;
const esp_partition_t *spiffs_part;

bool external_flash_init() {
    esp_err_t ret;
 
    spi_host_device_t vspi_host_id = VSPI_HOST;
    spi_bus_config_t bus_config = {
        .mosi_io_num = GPIO_NUM_23,
        .miso_io_num = GPIO_NUM_19,
        .sclk_io_num = GPIO_NUM_18  
    };
    spi_dma_chan_t spi_dma_chan = SPI_DMA_CH2;
    //step 1 spi_bus_initialize
    ret = spi_bus_initialize(vspi_host_id, &bus_config, spi_dma_chan);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "spi bus initialize failure: %s", esp_err_to_name(ret));
        return false;
    }
    // step 2 spi_bus_add_device
    
    esp_flash_spi_device_config_t spi_device_config = {
        .host_id = VSPI_HOST,
        //.cs_id = 0,
        .cs_io_num = 5,
        //.io_mode = SPI_FLASH_QIO,
        .io_mode = SPI_FLASH_DIO,
        .speed = ESP_FLASH_40MHZ
    };
    ret = spi_bus_add_flash_device(&flash_chip, &spi_device_config);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "spi bus add flash device failure: %s", esp_err_to_name(ret));
        return false;
    }
    //step 3 spi_flash_init
    ret = esp_flash_init(flash_chip);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "spi flash init: %s", esp_err_to_name(ret));
        return false;
    }
    uint32_t fsize;
    ret = esp_flash_get_size(flash_chip, &fsize);
    ESP_LOGI(TAG, "external SPI Flash(W25Q128FV) size:0x%0x, chip size:0x%0x", fsize, flash_chip->size);

    return true;
}

bool register_external_partition() {
    esp_err_t ret;
    
    ret = esp_partition_register_external(flash_chip, fatOffset, fatSize, extern_flash_fat, ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, &fat_part);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "register external fat partitional error:%s", esp_err_to_name(ret));
        return false;
    }
    
    ret = esp_partition_register_external(flash_chip, nvsOffset, nvsSize, extern_flash_nvs, ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS, &nvs_part);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "register external nvs partitional error:%s", esp_err_to_name(ret));
        return false;
    }
    
    ret = esp_partition_register_external(flash_chip, spiffsOffset, spiffsSize, extern_flash_spiffs, ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, &spiffs_part);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "register external spiffs partitional error:%s", esp_err_to_name(ret));
        return false;
    }

    return true;
}

void nvs_read_write_test() {
    esp_err_t ret;

    //test main flash default nvs partition
    ESP_LOGI(TAG, "TEST main flash default nvs partition read/write");
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NEW_VERSION_FOUND || ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        nvs_flash_erase();
        nvs_flash_init();
    }
    nvs_handle_t hNVS;
    ret = nvs_open("defaultnvs", NVS_READWRITE, &hNVS);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "open default nvs error:%s", esp_err_to_name(ret));
        return;
    }
    nvs_set_str(hNVS, "testK1", "default Key1 value");
    nvs_set_str(hNVS, "testK2", "default Key2 value");
    nvs_commit(hNVS);
    nvs_close(hNVS);

    ret = nvs_open("defaultnvs", NVS_READWRITE, &hNVS);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "open default nvs error:%s", esp_err_to_name(ret));
        return;
    }
    char tvalue[100];
    size_t tsize=100;
    nvs_get_str(hNVS, "testK1", tvalue, &tsize);
    printf("Read default nvs testK1, value=%s\n", tvalue);
    tsize=100;
    nvs_get_str(hNVS, "testK2", tvalue, &tsize);
    printf("Read default nvs testK1, value=%s\n\n", tvalue);
    nvs_close(hNVS);


    ESP_LOGI(TAG, "TEST external flash nvs_part partition read/write");
    ret = nvs_flash_init_partition(extern_flash_nvs);
    if (ret == ESP_ERR_NVS_NEW_VERSION_FOUND || ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        nvs_flash_erase_partition(extern_flash_nvs);
        nvs_flash_init_partition(extern_flash_nvs);
    }
   
    ret = nvs_open_from_partition(extern_flash_nvs, "externalnvs", NVS_READWRITE, &hNVS);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "open external flash nvs error:%s", esp_err_to_name(ret));
        return;
    }
    nvs_set_str(hNVS, "extK1", "external flash nvs Key1 value");
    nvs_set_str(hNVS, "extK2", "external flash nvs Key2 value");
    nvs_commit(hNVS);
    nvs_close(hNVS);
    printf("\n");
    ret = nvs_open_from_partition(extern_flash_nvs, "externalnvs", NVS_READWRITE, &hNVS);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "open external flash nvs error:%s", esp_err_to_name(ret));
        return;
    }

    nvs_get_str(hNVS, "extK1", tvalue, &tsize);
    printf("Read external flash nvs extK1, value=%s\n", tvalue);
    tsize=100;
    nvs_get_str(hNVS, "extK2", tvalue, &tsize);
    printf("Read external flash nvs extK1, value=%s\n\n", tvalue);
    nvs_close(hNVS);
    printf("\n");
}

void spiffs_read_write_test() {
    esp_err_t ret;
    size_t t,u;
    FILE *fp;
    char buf[100];
   
    esp_vfs_spiffs_conf_t spiffsConf = {
        .base_path="/spiffs",
        .partition_label = "mainspiffs", 
        .max_files=5,
        .format_if_mount_failed=true
    };
    
    // test main flash spiffs 
    ESP_LOGI(TAG, "main flash spiffs read/write test");
    ret = esp_vfs_spiffs_register(&spiffsConf);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG,"mainflash spiffs register error:%s\n",esp_err_to_name(ret));
        return;
    }
    esp_spiffs_info("mainspiffs", &t, &u);
    ESP_LOGI(TAG, "main flash spiffs size -- Total:%d, used:%d\n", t,u);

    fp = fopen("/spiffs/mtest.txt", "w");
    if (fp) {
        fputs("main flash spiffs test line 1\n", fp);
        fputs("main flash spiffs test line 2\n", fp);
        fputs("main flash spiffs test line 3\n", fp);
        fclose(fp);
    } else {
        ESP_LOGI(TAG, "open main flash spiff error(write)");
        return;
    }

    
    fp = fopen("/spiffs/mtest.txt", "r");
    if (!fp) {
        ESP_LOGI(TAG, "open main flash spiff error(read)");
        return;
    }
    while(fgets(buf, 100, fp)) {
        printf("read from main flash spiffs: %s",buf);
    }
    fclose(fp);
    printf("\n");
    esp_vfs_spiffs_unregister("mainspiffs");

    // test external flash spiffs
    esp_vfs_spiffs_conf_t extspiffsConf = {
        .base_path="/spiffs",
        .partition_label = extern_flash_spiffs, 
        .max_files=5,
        .format_if_mount_failed=true
    };

    ESP_LOGI(TAG, "external flash spiffs read/write test");
    ret = esp_vfs_spiffs_register(&extspiffsConf);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG,"external flash spiffs register error: %s\n",esp_err_to_name(ret));
        return;
    }
    esp_spiffs_info(extern_flash_spiffs, &t, &u);
    ESP_LOGI(TAG, "external flash spiffs size -- Total:%d, used:%d\n", t,u);

    fp = fopen("/spiffs/mtest.txt", "w");
    if (fp) {
        fputs("external flash spiffs test line 1\n", fp);
        fputs("external flash spiffs test line 2\n", fp);
        fputs("external flash spiffs test line 3\n", fp);
        fclose(fp);
    }

    fp = fopen("/spiffs/mtest.txt", "r");
    while(fgets(buf, 100, fp)) {
        printf("read from external flash spiffs ----- %s",buf);
    }
    fclose(fp);
    printf("\n");
   esp_vfs_spiffs_unregister(extern_flash_spiffs);
    
}

void fat_read_write_test() {
    esp_err_t ret;
    FILE *fp;
    char buf[100];

     esp_vfs_fat_mount_config_t spiflash_mount_config = {
        .format_if_mount_failed = true,
        .max_files=3,
        .allocation_unit_size=CONFIG_WL_SECTOR_SIZE
    };
// test main spi flash fat filesysetm
    ESP_LOGI(TAG, "test main spi flash fat filesystem");
    ret = esp_vfs_fat_spiflash_mount("/spiflash", "mainfat", &spiflash_mount_config, &wl_handle);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "mount main flash fat filesystem error: %s", esp_err_to_name(ret));
        return;
    }
    fp = fopen("/spiflash/ftest.txt", "wb");
    if (fp) {
        fputs("main spi flash fat filesystem test, LINE 1\n", fp);
        fputs("main spi flash fat filesystem test, LINE 2\n", fp);
        fputs("main spi flash fat filesystem test, LINE 3\n", fp);
        fclose(fp);
    } else {
        ESP_LOGI(TAG, "open main flash write fat file error(write)\n");
        return;
    }
    ESP_LOGI(TAG, "main spi flash write finished");
    fp = fopen("/spiflash/ftest.txt", "rb");
    if(fp) {
        while(fgets(buf, 100, fp)) {
            printf(buf);
        }
        fclose(fp);
        printf("\n");
    }else  {
         ESP_LOGI(TAG, "open main flash write fat file error(read)\n");
         return;
    }

    esp_vfs_fat_spiflash_unmount("/spiflash", wl_handle);
// test external spi flash fat filesysetm
    ESP_LOGI(TAG, "test external spi flash fat filesystem");
    ret = esp_vfs_fat_spiflash_mount("/spiflash", extern_flash_fat, &spiflash_mount_config, &wl_handle);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG, "mount external flash fat filesystem error: %s", esp_err_to_name(ret));
        return;
    }
    fp = fopen("/spiflash/ftest.txt", "wb");
    if (fp) {
        fputs("external spi flash fat filesystem test, LINE 1\n", fp);
        fputs("external spi flash fat filesystem test, LINE 2\n", fp);
        fputs("external spi flash fat filesystem test, LINE 3\n", fp);
        fclose(fp);
    } else {
        ESP_LOGI(TAG, "open external flash write fat file error(write)\n");
        return;
    }
    ESP_LOGI(TAG, "external spi flash write finished");
    fp = fopen("/spiflash/ftest.txt", "rb");
    if(fp) {
        while(fgets(buf, 100, fp)) {
            printf(buf);
        }
        fclose(fp);
        printf("\n");
    }else  {
         ESP_LOGI(TAG, "open external flash write fat file error(read)\n");
         return;
    }
    
}
void unregister_external_partition() {
    if (nvs_part != NULL)
        esp_partition_deregister_external(nvs_part);
    if (fat_part != NULL)
        esp_partition_deregister_external(fat_part);
    if (spiffs_part != NULL)
        esp_partition_deregister_external(spiffs_part);
}
 
void external_spiffs_read_test() {
    esp_err_t ret;
    size_t t,u;
    FILE *fp;
    char buf[100];
   
    
    // test external flash spiffs
    esp_vfs_spiffs_conf_t extspiffsConf = {
        .base_path="/spiffs",
        .partition_label = extern_flash_spiffs, 
        .max_files=5,
        .format_if_mount_failed=true
    };

    ESP_LOGI(TAG, "external flash spiffs read test");
    ret = esp_vfs_spiffs_register(&extspiffsConf);
    if (ret != ESP_OK) {
        ESP_LOGI(TAG,"external flash spiffs register error: %s\n",esp_err_to_name(ret));
        return;
    }
    esp_spiffs_info(extern_flash_spiffs, &t, &u);
    ESP_LOGI(TAG, "external flash spiffs size -- Total:%d, used:%d\n", t,u);

    fp = fopen("/spiffs/mtest.txt", "r");
    while(fgets(buf, 100, fp)) {
        printf("read from external flash spiffs again ----- %s",buf);
    }
    fclose(fp);
    printf("\n");
   esp_vfs_spiffs_unregister(extern_flash_spiffs);
    
}
void app_main(void)
{
    flash_chip = malloc(sizeof(esp_flash_t));
    if (!external_flash_init()) {
        ESP_LOGI(TAG, "external flash init error");
        return;
    }
//esp_flash_erase_chip(flash_chip);
    if (!register_external_partition()) {
        ESP_LOGI(TAG, "register external flash partition error");
        return;
    }

    nvs_read_write_test();
    spiffs_read_write_test();
    fat_read_write_test();
    external_spiffs_read_test();

    unregister_external_partition();
    

}

2022年2月7日 星期一

EPS32 ESP-IDF開發環境儲存設備與檔案系統實驗(一) -- SDMMC 與SDSPI (A Study on ESP32 Storage Devices and File System--SDMMC and SDSPI using ESP-IDF)

    ESP32 MCU常用的儲存設備為SD(SDMMC & SDSPI) 或SPI Flash,本篇實驗記錄主樣針對SD ,而上層檔案系統使用FatFs,實驗使用ESP32-CAM開發版,因為本開發版已含有SD Card模組。有關SPI Flash探討將於下篇記錄中說明。

一、FatFS

    FatFs是一個為小型系統發展的FAT/exFAT檔案系統,軟體系統架構如下圖所示,

(本圖片取自FatFs官網,http://elm-chan.org/fsw/ff/00index_e.html)

MCU的Application使用統一的FatFs Module,透過中間層使用不同device提供的Low Level device controls去存取各自的devices(例如SD, Flash, RTC等)。

    FatFs Module 與 device controls是完全獨立的,每個設備提供自己的device controls,FatFs與storage device controls則透過Media Access Interface(MAI)中間層去銜接。如下同所示。

(本圖片取自FatFs官網,http://elm-chan.org/fsw/ff/00index_e.html)

FatFs MAI使用下列五個API銜接FatFs Module與Storage device controls:
  1. disk_status - 取得device status。
  2. disk_initialize - 初始化 device
  3. disk_read - Read data
  4. disk_write - Write data
  5. disk_ioctl - Control device dependent functions