🌐Networking

This week I want to create a board that combines ESP32 with an SD card and a microphone.

  1. Install the Arduino IDE

    brew cask install arduino

  2. Setup board manager for ESP 32

    • Go to Preferences

    • Add an additional Board Manager URL

      https://dl.espressif.com/dl/package_esp32_index.json

🕵🏻‍♂️Researching past ESP32 projects

http://www.lapis-semi.com/en/semicon/speech/sample.html

https://github.com/maspetsberger/esp32-i2s-mems

https://jeelabs.org/2017/03/sd-cards-interrupts-and-dma/

https://www.esp32.com/viewtopic.php?t=1756&start=20

https://hackaday.io/project/28233/logs?sort=oldest

https://github.com/espressif/esp-idf/tree/master/examples/peripherals/i2s_adc_dac

Digital microphone (i2s or PDM)

http://fab.cba.mit.edu/classes/863.15/section.Harvard/people/Garber/wk10.html

Double Buffers

Hi Guys, I've had success connecting this SPH0641LU4H-1 MEMS PDM microphone to I2S interface and it works - sound quality is great.

 

Difference between PDM and i2S microphones? [https://ez.analog.com/audio/f/q-a/4259/difference-between-pdm-and-i2s-digital-microphones]

SPH0645LM4H i2S MEMS Microphone https://github.com/leonyuhanov/ESP32_MEMSMicrophone/blob/master/ESP32_MEMSMic.ino

INMP441 I2S Mic is BETTER (https://forum.micropython.org/viewtopic.php?f=18&t=5900&start=10)

Example Code for ESP32 i2s without Pipelines. https://www.esp32.com/viewtopic.php?f=13&t=1756

https://hackaday.io/project/162059-street-sense/log/160705-new-i2s-microphone

SPH0645LM4H

Understanding DMA and i2S https://github.com/miketeachman/micropython-esp32-i2s-examples

https://github.com/miketeachman/micropython-esp32-i2s-examples/blob/master/examples/read-mic-write-external-sdcard.py

Soft AccessPoint Example: https://github.com/espressif/esp-idf/tree/master/examples/wifi/getting_started/softAP

https://stackoverflow.com/questions/36303123/ios-how-to-programmatically-connect-to-a-wifi-network-given-the-ssid-and-passw

xcrun simctl io booted recordVideo filename.mov

https://techtutorialsx.com/2018/01/07/esp32-arduino-http-server-over-soft-ap/

 

Multiple ways to write to SD cards, SPI, 1-bit SD, 4-bit SD MMC (faster: https://electronics.stackexchange.com/questions/124234/difference-between-1-bit-4-bit-and-8-bit-sdio)

https://og.reddit.com/r/esp32/comments/d71es9/a_breakdown_of_my_experience_trying_to_talk_to_an/

https://community.st.com/s/question/0D50X00009XkWw8/best-practice-using-dma-to-read-from-uart-and-write-to-sd

📦Reading and Writing to SD from ESP32

If you use SD interface in 4-line mode, instead of SPI, you can get read speed around 3MB/s, from a FAT partition, and around 12MB/s if you read from card as a raw block device

(Source: https://github.com/espressif/esp-idf/issues/3249)

https://esp32.com/viewtopic.php?t=1942

Code example: https://github.com/espressif/esp-idf/tree/d57890cdf/examples/storage/sd_card

 

Schematics for SD to ESP32 Connection.

Code: https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/sdspi_host.html

Which ESP32 Pins can be used for SPI? GPIO vs IOMUX https://esp32.com/viewtopic.php?t=11224

I used the recomended pullups GPIO14, GPIO15, GPIO2, GPIO4, GPIO12, GPIO13, each 10k.

SD card pinSPI pinNotes 
GPIO14 (MTMS)CLKSCK10k pullup in SD mode
GPIO15 (MTDO)CMDMOSI10k pullup, both in SD and SPI modes
GPIO2D0MISO10k pullup in SD mode, pull low to go into download mode (see Note about GPIO2 below!)
GPIO4D1N/Cnot used in 1-line SD mode; 10k pullup in 4-line SD mode
GPIO12 (MTDI)D2N/Cnot used in 1-line SD mode; 10k pullup in 4-line SD mode (see Note about GPIO12 below!)
GPIO13 (MTCK)D3CSnot used in 1-line SD mode, but card's D3 pin must have a 10k pullup
N/CCD optional, not used in the example
N/CWP optional, not used in the example

With the given pinout for SPI mode, same connections between the SD card and ESP32 can be used to test both SD and SPI modes, provided that the appropriate pullups are in place. See the document about pullup requirements for more details about pullup support and compatibility of modules and development boards.

In SPI mode, pins can be customized. See the initialization of sdspi_slot_config_t structure in the example code.

Source: https://github.com/espressif/esp-idf/tree/master/examples/storage/sd_card

Really helpful tutorial on ESP32 Pins:https://randomnerdtutorials.com/esp32-pinout-reference-gpios/

Ring Buffer? Dual Buffer?

Ring Buffer Documentation: https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/freertos_additions.html

 

The classic way to deal with this problem is a ring buffer or alternatively a two phase buffer. You can then read data continuously while perfroming periodic time consuming block writes.

A ring buffer has separate read and write pointers (or indexes). You read ADC values on a timer interrupt. The interrupt handler writes the data through the write pointer, increments the pointer, and checks if it's gone past the end of the buffer. If it has, it sets it back to the start. The main program loop checks the amount of unwritten data in the buffer: if it's above a threshold it writes out a block of size equal to the threshold value. The check is done by comparing the read and write indexes to see how far ahead the write index has got. You subtract the read index from the write index. If the result is negative, the write index has wrapped round and you need to add the buffer length to the result (modulo arithmetic).

Handling wrap-round for the read pointer is simple if you choose a threshold of half of the buffer size: it will only ever have two values after completion of a read (0 or len(buf)//2) and won't wrap until the last element has been read. In other words you only need to perform the check at the end and the read activity always handles contiguous elements. This works for thresholds of other integer fractions of the size.

A two phase buffer uses two separate buffers with read and write alternating between the two. There's little to choose between them and I generally use ring buffers. They are actually much simpler to implement than my explanation suggests, once you've got your head round the idea.

In either case the size of the buffer is determined by the amount of time it takes to write out the amount of data you've chosen. This should be a worst case including provision for garbage collection - there's no penalty in too large a buffer aside from RAM use. In terms of implementation it's likely that an array will provide better performance than a Python list.

Source: https://forum.micropython.org/viewtopic.php?t=601

16k buffer?

Create big DMA buffer and use interrupts to flush it to SD card.

https://calsol.berkeley.edu/optimizing-file-writes/

// LOOP RECORDING
do
{
	//read data from I2S bus
	esp_err_t res = i2s_read(I2S_NUM, (void*)i2s_read_buff, I2S_DMA_BUFF_LEN_BYTES, &bytes_read, portMAX_DELAY);
	if(res != ESP_OK)
        {
		ESP_LOGE(TAG, "i2s_read error (ESP_FAIL) -> STOP RECORDING");
		break;
	}

	if(bytes_read != I2S_DMA_BUFF_LEN_BYTES)
	{
		ESP_LOGE(TAG, "i2s_read error (bytes_read != I2S_DMA_BUFF_LEN_BYTES)");
	} 

        // write to SD Card
        size_t bytes_written_sd = fwrite(i2s_read_buff, bytes_read, 1, fileRec);
	if(bytes_written_sd != 1)
	{
		ESP_LOGE(TAG, "fwrite error -> STOP RECORDING");
		break;
	}
} while((xEventGroupGetBits(s_status_event_group) & STOP_RECORDING_BIT) == 0)

https://esp32.com/viewtopic.php?t=9583

Calloc vs Malloc

https://stackoverflow.com/questions/1538420/difference-between-malloc-and-calloc

 

Source: https://community.pixelmatix.com/t/will-i-benefit-by-having-an-esp-with-psram/447/5

ESP32 has a few hundred kilobytes of internal RAM, residing on the same die as the rest of the chip components. It can be insufficient for some purposes, so ESP32 has the ability to also use up to 4 MB of external SPI RAM memory.

 

👂Use ESP Audio Pipeline APIs or not?

This should be the backup option, if the ESP32 WROOM doesn't have enough power. Upgrade to the WROOVER

[codec_chip]-->i2s_stream-->wav_encoder-->fatfs_stream-->[sdcard]

https://github.com/espressif/esp-adf/blob/master/examples/recorder/pipeline_wav_sdcard/main/record_wav_sdcard_example.c

https://docs.espressif.com/projects/esp-adf/en/latest/design-guide/design-considerations.html

"The initial spare internal RAM is 290kB." Wifi 50k+, Flash Drive 12k+

  1. According to the Wi-Fi menuconfig each Tx and Rx buffer occupies 1.6kB internal RAM. The value of 50kB RAM is assuming use of 5 Rx static buffers and 6 Tx static buffers. If PSRAM is not in use, then the “Type of WiFi Tx Buffer” option should be set as DYNAMIC in order to save RAM, in this case, the RAM usage will be far less than 50kB, but programmer should keep at least 50kB available for the Wi-Fi to be able to transmit the data. [Internal RAM only]
  2. Depending on value of SD_CARD_OPEN_FILE_NUM_MAX in audio_hal/board/board.h, that is then used in sd_card_mount() function, the RAM needed will increase with a greater number of maximum open files. 12kB is the RAM needed with 5 max files and 512 bytes CONFIG_WL_SECTOR_SIZE. [Internal RAM only]
  3. Depending on configuration settings of the I2S stream, refer to audio_stream/include/i2s_stream.h and audio_stream/i2s_stream.c. [Internal RAM only]
  4. Depending on configuration setting of the Ringbuffer, refer to DEFAULT_PIPELINE_RINGBUF_SIZE in audio_pipeline/include/audio_pipeline.h or user setting, if the buffer is created with e.g. rb_create().

 

 

You can output the I2S0 clock using CLK_OUT0 like this:

 PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1);
    REG_SET_FIELD(PIN_CTRL, CLK_OUT1, 0);

Source: https://esp32.com/viewtopic.php?t=1521

Low Level i2s Code https://github.com/cnlohr/esp32-cnlohr-demo/tree/master/main

https://github.com/espressif/esp-adf/issues/164

https://og.reddit.com/r/esp32/comments/bid08m/finally_got_audio_sampling_via_dma_with_no_cpu/

Open source schematics of a ESP32 Adf Board: https://github.com/OLIMEX/ESP32-ADF/tree/master/SOFTWARE/Internet_Radio_demo

I2s pins seem like they can be any GPIO pin. https://www.esp32.com/viewtopic.php?t=1756

</reasearch>


🏄‍♂️Making the Board

I made two boards. One was a direct copy of Neil's example board. The other included a microSD card as well.

Screen Shot 2019-11-19 at 2.28.34 PM

Screen Shot 2019-11-20 at 2.05.28 PM

Screen Shot 2019-11-20 at 1.59.41 PM

Here are the KiCad Schematics for my version.

IMG_1540

I milled both boards with minimal errors. I decided to get everything working with Neil's ESP32, because I wanted to minimize unknown sources of error while getting started with the ESP IDF.

IMG_1545

☕️ Getting Started with the Espressif Integrated Development Framework

The first thing I did was looking up the documentation. I am going to use the ESP Integrated Development Framework (IDF) rather than the Arduino. This may present some difficulties.

Something I record doing first is installing esptools.py which lets you debug if your board is working much more quickly. Just run pip install esptool.

  1. Download the toolchain here and save it into your ~/Downloads directory. (This is the URL to the current version as of Nov 19th 2019, it may have been updated since then.)

  2. Run the following commands

    mkdir -p ~/esp
    cd ~/esp
    tar -xzf ~/Downloads/xtensa-esp32-elf-osx-1.22.0-80-g6c4433a-5.2.0.tar.gz
    
  3. Add the toolchain to your PATH.

  4. Go to the ~/esp directory and git clone -b v3.3 --recursive https://github.com/espressif/esp-idf.git

  5. Add IDF to your PATH.

  6. Install python dependencies

Now the development environment has been set up, we need to create our first project.

  1. Find the serial port your device is connected to.

  2. Copy the example project in a new folder.

    cd ~/esp
    cp -r $IDF_PATH/examples/get-started/hello_world .
    
  3. Set up the configurations and check if everything has worked so far.

    cd ~/esp/hello_world
    make menuconfig
    

    Screen Shot 2019-11-19 at 8.27.06 PMScreen Shot 2019-11-19 at 8.27.06 PM

    You should see a screen that looks like this. Navigate to Serial flasher config > Default serial port and update the value.

  4. Run make flash with the ESP32 connected via an FTDI cable.

    Screen Shot 2019-11-19 at 8.28.57 PM

🐜Debugging

IMG_1549

Just had a moment of panic because I thought my board wasn't working. I tried a lot of random stuff. Ended up downloading esptools.py. Tried different serial ports. Turns out the FabFTDI that I made can't program the board. I am not sure why, but swapping out out for one of the normal cables fixed the issue. 

Use esptool to check the connection,esptool.py --port /dev/cu.usbserial-FTAV0OPC chip_id .

Make sure to disconnet any CoolTerm (or other serial connection) instances or you will get errors when trying to connect. Stuff like "could not open port" or "port is busy."

💻Server over Soft Access Point

For my final project, I will need to transfer audio files from the ESP32 to my iOS device. I looked into file transfer over Bluetooth, but Apple doesn't permit it.

So the next best way to do this is to create a soft access point — essentially just a local WiFi hotspot that is not connected to the internet — and send the files over HTTP.

I used the code below which is a mix of the IDF SoftAP example and code to start a server that I found here.

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event_loop.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "lwip/err.h"
#include "lwip/sys.h"

#include "esp_vfs.h"
#include "esp_spiffs.h"
#include "esp_http_server.h"

#include <stdio.h>
#include <sys/param.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include <dirent.h>

#include "tcpip_adapter.h"
static const char *TAG = "wifi softAP";

static esp_err_t servePage_get_handler(httpd_req_t *req)
{
    httpd_resp_sendstr_chunk(req, "<!DOCTYPE html><html>");

    httpd_resp_sendstr_chunk(req, "<head><meta charset = \"utf-8\">");
    httpd_resp_sendstr_chunk(req, "<title>mschrage's audio recordings</title>");

    httpd_resp_sendstr_chunk(req, "</head>");

    httpd_resp_sendstr_chunk(req, "<body>");

    httpd_resp_sendstr_chunk(req, "<center><h1>Welcome to my webserver.</h1><h2>Eventually you will connect to this via an iOS app.</h2></center>");

    httpd_resp_sendstr_chunk(req, "</body></html>");

    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}

static const httpd_uri_t servePage = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = servePage_get_handler,
    .user_ctx = NULL};

static httpd_handle_t start_webserver(void)
{
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();

    // Start the httpd server
    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server, &config) == ESP_OK)
    {
        // Set URI handlers
        ESP_LOGI(TAG, "Registering URI handlers");
        httpd_register_uri_handler(server, &servePage);
        return server;
    }

    ESP_LOGI(TAG, "Error starting server!");
    return NULL;
}


/* The examples use WiFi configuration that you can set via 'make menuconfig'.

   If you'd rather not, just change the below entries to strings with
   the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_ESP_WIFI_SSID      "mschrage"
#define EXAMPLE_ESP_WIFI_PASS      ""
#define EXAMPLE_MAX_STA_CONN       CONFIG_MAX_STA_CONN


/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;


static esp_err_t event_handler(void *ctx, system_event_t *event)
{
    switch(event->event_id) {
    case SYSTEM_EVENT_AP_STACONNECTED:
        ESP_LOGI(TAG, "station:"MACSTR" join, AID=%d",
                 MAC2STR(event->event_info.sta_connected.mac),
                 event->event_info.sta_connected.aid);
        break;
    case SYSTEM_EVENT_AP_STADISCONNECTED:
        ESP_LOGI(TAG, "station:"MACSTR"leave, AID=%d",
                 MAC2STR(event->event_info.sta_disconnected.mac),
                 event->event_info.sta_disconnected.aid);
        break;
    default:
        break;
    }
    return ESP_OK;
}

void wifi_init_softap()
{
    s_wifi_event_group = xEventGroupCreate();

    tcpip_adapter_init();
    ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    wifi_config_t wifi_config = {
        .ap = {
            .ssid = EXAMPLE_ESP_WIFI_SSID,
            .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
            .password = EXAMPLE_ESP_WIFI_PASS,
            .max_connection = EXAMPLE_MAX_STA_CONN,
            .authmode = WIFI_AUTH_WPA_WPA2_PSK
        },
    };
    if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    }

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

            tcpip_adapter_ip_info_t ip_info;
        IP4_ADDR(&ip_info.ip, 192, 168, 1, 1);
        IP4_ADDR(&ip_info.gw, 192, 168, 1, 1);
        IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);

        tcpip_adapter_ap_start((uint8_t *)"9C:B6:D0:E7:30:0F", &ip_info);

    ESP_LOGI(TAG, "wifi_init_softap finished.SSID:%s password:%s",
             EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);

    start_webserver();

}


void app_main()
{
    //Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
    
    ESP_LOGI(TAG, "ESP_WIFI_MODE_AP");
    wifi_init_softap();
}

Screen Shot 2019-11-19 at 10.53.55 PM

The default IP address of the ESP32 in SoftAccess Mode is 192.168.4.1. So if you connect to the ESP32's WiFi network and go to 192.168.4.1:80/, you will see the following webpage.

Screen Shot 2019-11-20 at 1.19.49 AM

IMG_1551

📱Connecting via iOS App

It was getting late by the time I managed to get this to work, but I wanted to throw together a simple iOS app to see if I could get the device to programmatically connect to the ESP32's wifi network.

import NetworkExtension

let wifiConfig = NEHotspotConfiguration(ssid: ssid)
        wiFiConfig.joinOnce = false
        NEHotspotConfigurationManager.shared.apply(wifiConfig) { error in
       //Do something                                                       }

You need to add Network Extensions & Hotspot Capability to your app — this requires registering as an Apple Developer and paying $99 per year.

Screen Shot 2019-11-20 at 1.08.33 AM

Screen Shot 2019-11-20 at 1.08.26 AM

You will need to update these settings in your App Id as well as in Xcode.

Screen Shot 2019-11-20 at 1.11.45 AM

I got a rudimentary version of the app to work. When you shake the phone, it attempts to connect you to the ESP32's wifi network.

IMG_1552