The front end can also play with hardware: run JavaScript on ESP32

Posted by Wozzr on Mon, 03 Jan 2022 22:45:40 +0100

0. Write in front

The main purpose of this article is to describe how to make the ESP32 chip run JavaScript and let the web front-end developers play with the hardware. The author was a web front-end development engineer before, so the article will try to stand in the perspective of Web front-end development engineer, put aside the underlying hardware knowledge, remove some things that don't need to be concerned at present, and focus on software. Nevertheless, what we need to do next is a whole of hardware + software, so some basic C language and hardware knowledge will make you better read this article. It doesn't matter if I don't, because I won't be a senior!

The article will be divided into two parts. The basic part will first introduce the basic knowledge, including the following parts:

  1. Introduction to ESP32 hardware
  2. A brief introduction to Jerry script
  3. FreeRTOS brief introduction

The practical part will introduce how to run JerryScript on ESP32 chip, including the following parts:

  1. Let Jerry script run and accept serial input
  2. Using on-chip memory chip: flash
  3. Implement JS file module

1. Introduction to esp32 hardware

First of all, let's introduce what ESP32 is. In short, it is a single-chip microcomputer microcontroller integrating WiFi, Bluetooth, antenna, power amplifier, power management module, CPU, memory and sensor. In human words, it can store and run our code, and also has WiFi and Bluetooth functions. Let's look at a picture first:

ESP32 development board

The larger one on the left is the ESP32 module. All the hardware configurations mentioned above are integrated into this module. The following board and other components are a development board made for the convenience of development and the necessary circuit connection for chip startup. This development board adds a power conversion chip to support 3.3 - 5V voltage. The small block on the right is cp2102 USB to serial port chip, so that we can connect the computer with USB cable. This board leads out all the pins for easy use DuPont line Connect various peripheral devices. The ESP32 mentioned below represents the whole development board, not the ESP32 module itself.

ESP32 adopts two Harvard structure xtensa lx6 CPUs to form a dual core system, clock frequency Adjustable in the range of 80MHz - 240MHz. 520KB SRAM and 448KB ROM are integrated on the chip. Has 41 Peripheral modules , including common IIC, SPI, UART, PWM, IR, I2S, SDIO, etc. Common protocols are basically available, which makes it more convenient for us to communicate with most electronic modules or peripherals without using software simulation like 51 single chip microcomputer. For example, the common SSD12864 oled screen has both IIC and SPI interfaces. The BLOX-NEO-6M GPS module is the UART interface used. DC motor and servo machine can be driven by PWM. Electric fans, air conditioners, etc. use IR infrared to transmit signals.

In addition, ESP32 also integrates on-chip sensors and Analog signal processor . Such as capacitive touch sensors, hall sensor , ADC, DAC, etc. If we design a product with battery and ADC, we can easily measure the power of the battery, although the measured value may not be accurate.

The above are very common peripherals in single chip microcomputer system. ESP32 integrates them into a system on chip. But the most exciting thing about ESP32 is that it integrates WIFI and Bluetooth. With WIFI and Bluetooth, together with various peripherals and GPIO, we can do many things with it, such as uploading the values of temperature and humidity sensors directly to the server. Remotely issue execution instructions, switch lights, etc., and give full play to your imagination.

However, the threshold of hardware programming is a little high for software engineers, especially for our web front-end development engineers, C language is the first threshold. I've always wanted to bring JavaScript to hardware programming so that we can use familiar JavaScript to give full play to our creativity. That's why we have this article.

2. Brief introduction to Jerry script

Node.js is very powerful, but it is built on V8 and libuv. SRAM on ESP32 is only 520KB, not to mention V8. Libuv can't run. So we need a lightweight JavaScript engine designed for embedded. Fortunately, we have JerryScript.

Jerry script is a lightweight JavaScript engine that can run on restricted devices, such as microcontrollers, and can run on devices with RAM < 64 kb and ROM < 200 KB. And it also provides a complete Es5 1 syntax support and some ES6 syntax support, such as arrow function, Symbol, Promise, etc. Although the programming experience is not as good as v8, it is good to have these (compared with other embedded JavaScript engines)!

Another important point is that the api of Jerry script is more in line with our programming habits. For those who are used to writing node JS addon will be easier to accept. So the above two points are the reasons why we choose Jerry script. For a more intuitive understanding, let's compare two popular JavaScript engines in embedded system.

duktape

duktape At present, there are 3.7K stars on github and hello world on the official website below!

#include <stdio.h>
#include "duktape.h"

/* Adder: add argument values. */
static duk_ret_t native_adder(duk_context *ctx) {
  int i;
  int n = duk_get_top(ctx);  /* #args */
  double res = 0.0;

  for (i = 0; i < n; i++) {
    res += duk_to_number(ctx, i);
  }

  duk_push_number(ctx, res);
  return 1;  /* one return value */
}

int main(int argc, char *argv[]) {
  duk_context *ctx = duk_create_heap_default();

  duk_push_c_function(ctx, native_adder, DUK_VARARGS);
  duk_put_global_string(ctx, "adder");

  duk_eval_string(ctx, "adder(1+2);");
  printf("1+2=%d\n", (int) duk_get_int(ctx, -1));

  duk_destroy_heap(ctx);
  return 0;
}

JerryScript

#include "jerryscript.h"
#include "jerryscript-ext/handler.h"

static jerry_value_t adder_handler(const jerry_value_t func_value, /**< function object */
                                   const jerry_value_t this_value, /**< this arg */
                                   const jerry_value_t args[],    /**< function arguments */
                                   const jerry_length_t args_cnt)  /**< number of function arguments */
{
  double total = 0;
  uint32_t argIndex = 0;

  while (argIndex < args_cnt)
  {
    double = double + jerry_get_number_value(args[argIndex]);
    argIndex++;
  }
  return jerry_create_number(total);
}

int main (void)
{
  const jerry_char_t script[] = "print(adder(1, 2));";

  /* Initialize engine */
  jerry_init (JERRY_INIT_EMPTY);

  /* Register 'print' function from the extensions */
  jerryx_handler_register_global ((const jerry_char_t *) "print", jerryx_handler_print);

  /* Register 'adder' function from the extensions */
  jerryx_handler_register_global ((const jerry_char_t *) "adder", adder_handler);

  /* Setup Global scope code */
  jerry_value_t parsed_code = jerry_parse (NULL, 0, script, sizeof (script) - 1, JERRY_PARSE_NO_OPTS);

  if (!jerry_value_is_error (parsed_code))
  {
    /* Execute the parsed source code in the Global scope */
    jerry_value_t ret_value = jerry_run (parsed_code);

    /* Returned value must be freed */
    jerry_release_value (ret_value);
  }

  /* Parsed source code must be freed */
  jerry_release_value (parsed_code);

  /* Cleanup engine */
  jerry_cleanup ();

  return 0;
}

3. Brief introduction to FreeRTOS

FreeRTOS It is a popular real-time operating system core for embedded devices. Its design is small and simple. Most of the code is written in C language. It provides multitasking, mutex lock, semaphore, software timer and other functions, so that users can quickly design applications.

The above is the introduction of Wikipedia. In short, it mainly provides a basic tool library for designing multitasking applications, so that application developers can focus on the implementation of logic without having to implement task management and scheduling themselves. Because there is no multi process and multi thread concept like Linux when programming on the single chip microcomputer, after the single chip microcomputer is powered on and started, the instructions are loaded from the specified address and executed in sequence.

Generally speaking, a single chip microcomputer has only one processor and can only process one task at a time. If you want two LEDs to blink alternately, you must be in while(true) {...} Manually control the execution time of 2 LED Logic Codes in the cycle. What if there are 3, 4 and N in the follow-up? Then all the logic has to be written in it. It will be very huge.

FreeRTOS tasks allow each logic to run in a separate Task without interfering with each other, and each Task preempts CPU time with priority. It is worth noting that even if FreeRTOS is used, the whole application is still single threaded. After high priority tasks are executed, CPU time must be allowed to allow other low priority tasks to execute. Remember, a single chip microcomputer has only one processor and can only process one Task at a time.

The task of the whole FreeRTOS is a linked list. The task with the highest priority is taken out from the task list for execution. After execution, the next priority task is taken out, and the cycle continues. However, there are several differences. FreeRTOS always ensures that high priority tasks are executed first, so low priority tasks may not have the opportunity to execute. Each time a task is executed, the priority will be recalculated when the next task is taken from the list. The CPU time of the executing task can only be given by the task itself, otherwise other tasks have no chance to execute, except for interruption of course. FreeRTOS is a real-time operating system. You can accurately control the start and end time of each task.

Actual combat chapter

1. Let Jerry script run and accept serial input

After introducing the basic knowledge above, let's start to let Jerry script run on ESP32, and let the serial port receive user input and input it into Jerry script for execution.

First, you need to be ready ESP-IDF Development environment, and then create an empty project. I recommend starting from idf-hello-world newly build. JerryScript will be placed as an external dependency in the deps/JerryScript directory. JerryScript source address: JerryScript.

Finally, our project catalogue is as follows:

- build
- deps
  - jerryscript
- components
- main
- spiffs
- partitions.csv
- CMakeLists.txt
- sdkconfig
  • Build is our build directory. All temporary files during the construction process are here for easy cleaning.
  • deps is a directory for storing third-party dependencies. Jerry script will be used as a dependency, which is convenient for management and can be synchronized with the official.
  • components are stored User component All the components we write are placed here.
  • Main is a special component as the main program of the application.
  • Spiffs is a directory for storing the built-in file system. All files in spiffs will be packaged into a binary file for easy burning on the chip.
  • partitions.csv is a partition table configuration file. Each application needs to configure a partition table. Here, just use the default.
  • CMakeLists.txt is the main build file of the project. The construction of the whole project will start from here.
  • sdkconfig is the configuration file of the project. You can configure system parameters and some user-defined parameters.

When all the above are ready, you can start writing code. The CPU model of ESP32 is Xtensa 32-bit LX6, so we need to write the cross compilation of JerryScript, and then link the static library to the main component, so that JerryScript can run.

Here is the main cmakelists Txt file content, mainly specifies the source directory of Jerry script, which is convenient for use in other components. Then set JERRY_GLOBAL_HEAP_SIZE is 128KB. JERRY_GLOBAL_HEAP_SIZE indicates the memory size requested in advance by the JerryScript virtual machine. When the virtual machine is started, it will request the specified size of memory from the system in advance. Because the total memory of ESP32 is only 520KB, and Jerry script defaults to heap_ The size is also 512KB, so it must be compiled, and overflow errors will be reported.

cmake_minimum_required(VERSION 3.5)

set(JERRYSCRIPT_SOURCE "${CMAKE_SOURCE_DIR}/deps/jerryscript")

# JerryScript setting here
set(JERRY_GLOBAL_HEAP_SIZE "(128)")

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(nodemcujs)

After the main cmake is written, the next step is to write the cmake file of the main component. To use JerryScript is very simple, just link to JerryScript's static library and configure the correct header file path. JerryScript will be compiled into static libraries by default. We just link them in the main component.

The following is cmakelists for the main component Txt, there are a lot of contents. Only the key explanations are selected here. Please see the details nodemcujs Project:

set(COMPONENT_PRIV_INCLUDEDIRS
    ${JerryScript_SOURCE}/jerry-core/include
    ${JerryScript_SOURCE}/jerry-ext/include
    ${JerryScript_SOURCE}/jerry-port/default/include)

The above is to set the header file search path of Jerry script. The following will cross compile Jerry script and link the compiled static library to the main component:

# Xtensa processor architecture optimization
set(EXTERNAL_COMPILE_FLAGS -ffunction-sections -fdata-sections -fstrict-volatile-bitfields -mlongcalls -nostdlib -w)
string(REPLACE ";" "|" EXTERNAL_COMPILE_FLAGS_ALT_SEP "${EXTERNAL_COMPILE_FLAGS}")

The above is the parameter to set cross compilation. For xtensa processor, it can't be linked without this parameter. Pay special attention to the - mlongcalls parameter. Although this parameter is set as a compilation parameter, it actually acts on assembly. If you see the error of dangerous relocation: call0: call target out of range, you probably forget to add this parameter. For details, please see xtensa-gcc-longcalls Compiler documentation. Note that everything here needs to be written in register_component () later, otherwise an error will be reported.

Xtensa Options: longcalls​embarc.org/man-pages/gcc/Xtensa-Options.html

After the compilation parameters are set, the following is to use external project_ Add compiles JerryScript as an external project separately. Add cannot be used_ Subdirectory, cmake will report an error.

externalproject_add(jerryscript_build
  PREFIX ${COMPONENT_DIR}
  SOURCE_DIR ${JERRYSCRIPT_SOURCE}
  BUILD_IN_SOURCE 0
  BINARY_DIR jerryscript
  INSTALL_COMMAND "" # Do not install to host
  LIST_SEPARATOR | # Use the alternate list separator
  CMAKE_ARGS
    -DJERRY_GLOBAL_HEAP_SIZE=${JERRY_GLOBAL_HEAP_SIZE}
    -DJERRY_CMDLINE=OFF
    -DENABLE_LTO=OFF # FIXME: This option must be turned off or the cross-compiler settings will be overwritten
    -DCMAKE_C_COMPILER_WORKS=true # cross-compiler
    -DCMAKE_SYSTEM_NAME=Generic
    -DCMAKE_SYSTEM_PROCESSOR=xtensa
    -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
    -DEXTERNAL_COMPILE_FLAGS=${EXTERNAL_COMPILE_FLAGS_ALT_SEP}
    -DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS}
    -DCMAKE_LINKER=${CMAKE_LINKER}
    -DCMAKE_AR=${CMAKE_AR}
    -DCMAKE_NM=${CMAKE_NM}
    -DCMAKE_RANLIB=${CMAKE_RANLIB}
    -DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER
)
add_dependencies(${COMPONENT_NAME} jerryscript_build)

The above mainly sets JerryScript as the dependency of the main component, so that JerryScript will be compiled automatically when compiling the main component. Then set up the cross compilation tool chain. Special attention should be paid here. Turn off ENABLE_LTO=OFF, why? If this option is enabled in JerryScript, it will determine whether the compiler ID is GNU. If so, the compiler will be forcibly set to GCC, resulting in the invalidation of our cross compilation tool chain settings.

Finally, we link the compiled static library to the main component:

set(COMPONENT_BUILD_PATH ${CMAKE_BINARY_DIR}/${COMPONENT_NAME}/jerryscript)

target_link_libraries(${COMPONENT_NAME}
                      ${COMPONENT_BUILD_PATH}/lib/libjerry-core.a
                      ${COMPONENT_BUILD_PATH}/lib/libjerry-ext.a
                      ${COMPONENT_BUILD_PATH}/lib/libjerry-port-default-minimal.a)

After Jerry script is compiled, the final file will be generated under main / Jerry script in the compilation directory. This path is specified above. We only need Jerry core a jerry-ext.a jerry-default-minimal. A these three static libraries are OK$ {COMPONENT_NAME} is main.

Write the initialization code below to initialize the JerryScript virtual machine at system startup.

#include <stdio.h>
#include <string.h>

#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"

#include "jerryscript.h"
#include "jerryscript-ext/handler.h"
#include "jerryscript-port.h"

static void start_jerryscript()
{
  /* Initialize engine */
  jerry_init(JERRY_INIT_EMPTY);
}

void app_main()
{
  // init jerryscript
  start_jerryscript();
  while (true)
  {
    // alive check here. but nothing to do now!
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
  /* Cleanup engine */
  jerry_cleanup();
}

Initializing Jerry script is very simple. You only need to call jerry_init(JERRY_INIT_EMPTY) is OK. Now we have made the js virtual machine run. Vtask delay is a function provided by FreeRTOS. It is used to allow the specified CPU time to perform other tasks without blocking the whole application. 1000 / portTICK_PERIOD_MS means 1000ms, which is similar to using sleep(1) on Linux. portTICK_PERIOD_MS indicates the beat executed by FreeRTOS within 1ms, which is related to the CPU frequency. For details, please refer to FreeRTOS file.

Now the integration of Jerry script has been completed, and the executable firmware can be compiled:

$ mkdir build
$ cd build
$ cmake ..
$ make

If there is no error, the executable firmware will be generated in the compilation directory, and the firmware will be automatically burned to the ESP32 chip using make flash. Make flash does not require additional configuration and can be used directly. It will call the built-in esptool.py Burn.

be careful

When burning firmware, you need to install a serial driver first. The boards sold on a treasure have uneven quality and many models. Many sellers don't understand technology and don't know what they sell. Generally speaking, ESP32 is the driver of CP2102. Just go to the official website to download the driver.

See nodemcujs for specific burning methods Burn firmware.

Nodemcujs burning firmware GitHub COM / nodemcujs / nodemcujs firmware #4-% E7% 83% A7% E5% BD% 95% E5% 9b% Ba% E4% BB% B6 uploading... Re uploading cancelled

If the compilation fails, please start again from the beginning. Now JerryScript has run, but we have no js code execution. Next, we will open the serial port, input the string received from the serial port to JerryScript for execution, and output the result from the serial port.

#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "driver/uart.h"
// ...  Omit other header files
static QueueHandle_t uart_queue;
static void uart_event_task(void *pvParameters)
{
  uart_event_t event;
  uint8_t *dtmp = (uint8_t *)malloc(1024 * 2);
  for (;;) {
    // Waiting for UART event.
    if (xQueueReceive(uart_queue, (void *)&event, (portTickType)portMAX_DELAY)) {
      bzero(dtmp, 1024 * 2);
      switch (event.type) {
      /** 
       * We'd better handler data event fast, there would be much more data events than
       * other types of events. If we take too much time on data event, the queue might
       * be full.
       */
      case UART_DATA:
        uart_read_bytes(UART_NUM_0, dtmp, event.size, portMAX_DELAY);
        /* Setup Global scope code */
        jerry_value_t parsed_code = jerry_parse(NULL, 0, dtmp, event.size, JERRY_PARSE_NO_OPTS);

        if (!jerry_value_is_error(parsed_code)) {
          /* Execute the parsed source code in the Global scope */
          jerry_value_t ret_value = jerry_run(parsed_code);

          /* Returned value must be freed */
          jerry_release_value(ret_value);
        } else {
          const char *ohno = "something was wrong!";
          uart_write_bytes(UART_NUM_0, ohno, strlen(ohno));
        }

        /* Parsed source code must be freed */
        jerry_release_value(parsed_code);
        // free(dtmp);
        break;
      //Event of UART ring buffer full
      case UART_BUFFER_FULL:
        // If buffer full happened, you should consider encreasing your buffer size
        // As an example, we directly flush the rx buffer here in order to read more data.
        uart_flush_input(UART_NUM_0);
        xQueueReset(uart_queue);
        break;
      //Others
      default:
        break;
      }
    }
  }
  free(dtmp);
  dtmp = NULL;
  vTaskDelete(NULL);
}

/**
 * Configure parameters of an UART driver, communication pins and install the driver
 * 
 * - Port: UART0
 * - Baudrate: 115200
 * - Receive (Rx) buffer: on
 * - Transmit (Tx) buffer: off
 * - Flow control: off
 * - Event queue: on
 * - Pin assignment: TxD (default), RxD (default)
 */
static void handle_uart_input()
{
  uart_config_t uart_config = {
      .baud_rate = 115200,
      .data_bits = UART_DATA_8_BITS,
      .parity = UART_PARITY_DISABLE,
      .stop_bits = UART_STOP_BITS_1,
      .flow_ctrl = UART_HW_FLOWCTRL_DISABLE};
  uart_param_config(UART_NUM_0, &uart_config);

  //Set UART pins (using UART0 default pins ie no changes.)
  uart_set_pin(UART_NUM_0, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
  //Install UART driver, and get the queue.
  uart_driver_install(UART_NUM_0, 1024 * 2, 1024 * 2, 20, &uart_queue, 0);

  //Create a task to handler UART event from ISR
  xTaskCreate(uart_event_task, "uart_event_task", 1024 * 2, NULL, 12, NULL);
}

There is a lot of code. Split it into two functions to see handle_ uart_ The input function is responsible for installing the serial port driver, and then starting a Task To handle serial input. Why start a task? Because the serial input is asynchronous, we can't let it block, so we use it in the new task esp-uart-events Listen for events in a way. When the event with serial port input arrives, read the input and execute it.

The board is equipped with a USB to serial port chip, and the pin of the chip is connected to UART_NUM_0, so we can read the input from this serial port by default, and printf will also output from here by default. In this way, plug in USB and use it as a mini JavaScript development board to facilitate development and debugging. This is the charm of dynamic language in hardware programming.

With input, we also need a native api to output data in JavaScript code. Here, we can use the built-in print. In JavaScript code, you can directly use print(message) to output data to the serial port.

#include "jerryscript.h"
#include "jerryscript-ext/handler.h"

static void handler_print()
{
  /* Register 'print' function from the extensions */
  jerryx_handler_register_global ((const jerry_char_t *) "print",
                                  jerryx_handler_print);
}

void app_main()
{
  // init jerryscript
  start_jerryscript();
  handler_print();
  // handle uart input
  handle_uart_input();
  while (true)
  {
    // alive check here. but nothing to do now!
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
  /* Cleanup engine */
  jerry_cleanup();
}

Use make flash to compile the updated firmware and burn it to the board. Now open the serial port, connect to the board, and enter var msg = 'hello nodemcujs'; print(msg) try it. You can enter any valid JavaScript statement and use the print function to output data.

Note: do not use minicom, you can use ESPlorer . Because we directly input the input of the serial port into the virtual machine for execution, we only receive displayable characters and line feed and carriage return. Other characters, such as control characters, will lead to execution failure

Please check the complete code nodemcujs Source code.

nodemcujs/nodemcujs-firmware​github. COM / nodemcujs / nodemcujs firmware uploading... Re upload cancel

2. Use on-chip memory chip: flash

Above, we have implemented the embedded Jerry script virtual machine and opened the serial port interaction, but each restart will reset the data. Obviously, this is not a standard development board. In this chapter, we will connect the file system to store user data.

ESP32 has integrated a 4MB SPI memory chip. SPI is a data exchange protocol. We don't care too much here. We are interested in finding information ourselves. Below, we refer to this memory chip with flash.

ESP-IDF Engineering support spiffs Components, we just need to use them. To use the file system, you must do these steps:

  1. Partition table - partition the purpose of the disk. It tells the system how many partitions there are and what the size of each partition is. The flash of each ESP32 can contain multiple applications and many different types of data (such as calibration data, file system data Parameter memory Data, etc.). Therefore, we need to introduce the concept of partitioned table.
  2. mount - read the partition table configuration and format the disk if it has not been initialized

We modify it based on the default partition table and add a data partition to store user-defined data. New at project root partitions.csv Document, which reads as follows:

# Name,   Type, SubType, Offset,  Size, Flags
# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 1M,
storage,  data, spiffs,  ,        0x2F0000,
 

nvs and Phy_ The init partition uses the default. The factory partition is used to store the App, that is, the compiled executable code, or the compiled bin file. We specify the size of 1M. At present, the size of the compiled firmware is about 500KB, which is generally enough.

The storage partition is a newly added partition, which is used to store user-defined data. offset is not filled in here, and the previous partition will be automatically aligned. The size is specified as 0x2F0000, and there is almost 2.7M free space. Note that this is the largest and cannot be larger. The most common flash size of ESP32 is 4MB. If your flash size is different, it can be modified according to the situation, but it can not exceed the partition size, and can be smaller than.

ESP32 writes the partition table data to the address 0x8000 by default, with a length of 0xC00. It can save up to 95 entries, and MD5 checksum is also saved behind the partition table. Therefore, if you don't know the whole partition table, don't change the partition data. For details, please see Partition table file.

be careful

To use the user-defined partition table, you need to specify it in the sdkconfig file. You can use the make menuconfig graphical interface. The specific methods are as follows:

$ mkdir build
$ cd build
$ cmake ..
$ make menuconfig

After executing make menuconfig, a graphical interface will appear. Enter: Partition Table, Partition Table, and select Custom partition table CSV. Then fill in the Custom partition CSV file CSV, please note that this is the file name of your Partition Table. Please modify it according to your own situation.

After the partition table is made, we will start the mount storage partition in the startup process: if the partition is not initialized, format the partition and load it again, otherwise it will be loaded directly. And print out the usage.

#include "esp_system.h"
#include "esp_spi_flash.h"
#include "esp_heap_caps.h"
#include "esp_err.h"

#include "driver/uart.h"
#include "esp_spiffs.h"

static void mount_spiffs()
{
  esp_vfs_spiffs_conf_t conf = {
    .base_path = "/spiffs",
    .partition_label = NULL,
    .max_files = 5,
    .format_if_mount_failed = true
  };

  esp_err_t ret = esp_vfs_spiffs_register(&conf);

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

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

bash_path is set to / spiffs, which is equivalent to the root directory prefix. In the future, when accessing the data partition, you should use / spiffs/file. Of course, you can modify it according to your own situation. Set format_ if_ mount_ If the failed parameter is set to true, it can be automatically formatted after the partition mount fails. In this case, the partition is generally unformatted. be careful spiffs The file system does not have the concept of directory, / is just used as a file name. Later, we can simulate the concept of directory ourselves.

After the partition is mounted, we can use the api of the file system to read and write files. We use esp_spiffs_info reads the file system information and prints the total size and usage.

Finally, call this function in the startup process.

void app_main()
{
  // mount spiffs
  mount_spiffs();
  // init jerryscript
  start_jerryscript();
  handler_print();
  // handle uart input
  handle_uart_input();
  while (true)
  {
    // alive check here. but nothing to do now!
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
  /* Cleanup engine */
  jerry_cleanup();
}

Recompile and then write. Use the serial port to connect to the board to view the printed partition information. If you see that the partition table data is successfully printed, it indicates that the file system is mounted successfully. If it fails, carefully check what is wrong.

3. Implement JS file module

We already have the concept of file, so we can write js file module, then use require to load the file module, and automatically load and execute index js file, so that JavaScript developers can develop independently from SDK. Of course, when it comes to the hardware driver, you still need SDK support to expose the interface to JavaScript. I won't elaborate here.

Let's take a look at node What does the file module look like in JS

// a.js
module.exports = function a () {
  console.log(`hello, i am ${__filename}`)
}

This module is very simple. It only provides a function, which prints its own file name. How to use this module:

// index.js
var a = require('./a.js')

a()

Just use the require function to load the module and assign it to a variable, which references all the external implementations of the module. Because we expose a function externally, we can call it directly. So here's the module Where does the exports variable come from__ Why is filename equal to a.js? How does the return value of require come from? Let's take a brief look at node How JS is implemented.

When a file module is require d, node JS will read the contents of the file, then wrap the contents at the beginning and end, and finally become:

(function (exports, require, module, __filename, __dirname) {
  // Module source code
})

Pass in the parameters to execute this function, so we can use undefined variables such as exports in the file module. Finally, the require function returns the exports variable to complete the loading of the module. Of course, node The implementation in JS is much more complex than this. Here is just a brief description. Please check the details Node.js: require Source code.

Now that we know how require works, let's implement the simplest require, which only loads file modules from the file system and does not support caching and relative path. If the loading is successful, the exports object of the module is returned; otherwise, undefined is returned.

You can create a new one User component , called Jerry module, can also be written directly in main.

void module_module_init()
{
  jerry_value_t global = jerry_get_global_object();

  jerry_value_t prop_name = jerry_create_string((const jerry_char_t *)"require");
  jerry_value_t value = jerry_create_external_function(require_handler);
  jerry_release_value(jerry_set_property(global, prop_name, value));
  jerry_release_value(prop_name);
  jerry_release_value(value);

  jerry_release_value(global);
}

We specify that each native module has an init method, starting with module, and the middle module represents the module name. In the init method, the global variable will be registered. The module itself needs to expose the api to JavaScript, so that JavaScript can be used. The following is the implementation of the require function.

static jerry_value_t require_handler(const jerry_value_t func_value, /**< function object */
                                     const jerry_value_t this_value, /**< this arg */
                                     const jerry_value_t args[],     /**< function arguments */
                                     const jerry_length_t args_cnt)  /**< number of function arguments */
{
  jerry_size_t strLen = jerry_get_string_size(args[0]);
  jerry_char_t name[strLen + 1];
  jerry_string_to_char_buffer(args[0], name, strLen);
  name[strLen] = '\0';

  size_t size = 0;
  jerry_char_t *script = jerry_port_read_source((char *)name, &size);

  if (script == NULL)
  {
    printf("No such file: %s\n", name);
    return jerry_create_undefined();
  }
  if (size == 0)
  {
    return jerry_create_undefined();
  }

  jerryx_handle_scope scope;
  jerryx_open_handle_scope(&scope);

  static const char *jargs = "exports, module, __filename";
  jerry_value_t res = jerryx_create_handle(jerry_parse_function((jerry_char_t *)name, strLen,
                                          (jerry_char_t *)jargs, strlen(jargs),
                                          (jerry_char_t *)script, size, JERRY_PARSE_NO_OPTS));
  jerry_port_release_source(script);
  jerry_value_t module = jerryx_create_handle(jerry_create_object());
  jerry_value_t exports = jerryx_create_handle(jerry_create_object());
  jerry_value_t prop_name = jerryx_create_handle(jerry_create_string((jerry_char_t *)"exports"));
  jerryx_create_handle(jerry_set_property(module, prop_name, exports));
  jerry_value_t filename = jerryx_create_handle(jerry_create_string((jerry_char_t *)name));
  jerry_value_t jargs_p[] = { exports, module, filename };
  jerry_value_t jres = jerryx_create_handle(jerry_call_function(res, NULL, jargs_p, 3));

  jerry_value_t escaped_exports = jerry_get_property(module, prop_name);
  jerryx_close_handle_scope(scope);

  return escaped_exports;
}

Our implementation here is very simple:

  1. require only receives a parameter called name, which indicates the absolute path of the file module.
  2. Then use jerry_port_read_source reads the contents of the file. Note that using this function requires the header file Jerry script port h. Remember to use Jerry after use_ port_ release_ Source releases the contents of the file.
  3. Then judge whether the file exists. If it does not exist or the file content is empty, it returns undefined, indicating that loading the module failed.
  4. Using jerry_parse_function constructs a JavaScript function. We only implement exports and module here__ Filename these three parameters.
  5. Using jerry_create_object construct a JavaScript object using jerry_set_property sets the exports property for this object.
  6. Using jerry_call_function executes the function with exports, module and filename as parameters, so that the file module will execute. module.exports Is a reference to exports.
  7. Finally, the exports variable will be assigned inside the file module, which is the api exposed by the module. We use jerry_get_property returns the exports property to complete the module loading.

Finally, after initializing the virtual machine, we call the module's initialization function to register the module to the virtual machine.

void app_main()
{
  // mount spiffs
  mount_spiffs();
  // init jerryscript
  start_jerryscript();
  handler_print();
  // handle uart input
  handle_uart_input();
  // init node core api
  module_module_init();

  while (true)
  {
    // alive check here. but nothing to do now!
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
  /* Cleanup engine */
  jerry_cleanup();
}

Now, we are close to the last step: load and execute index. From the file system JS file, so that the code will be executed automatically after startup. This is also very simple to implement. After all operations are completed, use the file api to read the index from the file system JS file, and then use jerry_run execution.

static void load_js_entry()
{
  char *entry = "/spiffs/index.js";
  size_t size = 0;
  jerry_char_t *script = jerry_port_read_source(entry, &size);
  if (script == NULL) {
    printf("No such file: /spiffs/index.js\n");
    return;
  }
  jerry_value_t parse_code = jerry_parse((jerry_char_t *)entry, strlen(entry), script, size, JERRY_PARSE_NO_OPTS);
  if (jerry_value_is_error(parse_code)) {
    printf("Unexpected error\n");
  } else {
    jerry_value_t ret_value = jerry_run(parse_code);
    jerry_release_value(ret_value);
  }
  jerry_release_value(parse_code);
  jerry_port_release_source(script);
}

The entry can be modified by ourselves. We specify / spiffs / index js. If the load fails, nothing is done. If the load is successful, Jerry is used_ Parse compiles JS code, and finally uses jerry_run execution. Again, call this function in the startup process.

void app_main()
{
  // mount spiffs
  mount_spiffs();
  // init jerryscript
  start_jerryscript();
  handler_print();
  // handle uart input
  handle_uart_input();
  // init node core api
  module_module_init();

  // load /spiffs/index.js
  load_js_entry();

  while (true)
  {
    // alive check here. but nothing to do now!
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
  /* Cleanup engine */
  jerry_cleanup();
}

Now let's sort out what the startup process has done:

  1. mount spiffs file system
  2. initialization JerryScript virtual machine
  3. Register the global print function for serial output
  4. Install the serial driver and pass the input to the virtual machine for execution
  5. Register module module
  6. Load index. From the file system JS file and execute
  7. A very important step: use vTaskDelay Allow CPU time for other tasks

So far, we have a JavaScript development board, but the functions are limited. The driver and common function modules have not been implemented. Originally, I wanted to introduce the native module and timer. The space is limited, so I won't introduce it in detail here. Please check the complete source code nodemcujs.

Finally, let's briefly introduce how to upload index JS and custom data to the file system:

  1. use mkspiffs Make file image
  2. use esptool.py The burning tool burns the file image to the board

See nodemcujs for complete file image creation and burning methods Make file image.

Topics: Single-Chip Microcomputer ESP32