只想要可执行固件的请直接下载:github.com/nodemcujs/n…html
本文的主要目的是描述如何让 ESP32 芯片运行 JavaScript,而且让 web 前端开发人员也能玩转硬件。 做者以前是 web 前端开发工程师,因此文章会尽可能站在 web 前端开发工程师的角度,抛开底层的硬件知识,去掉一些目前不须要关心的,将重点放在软件上。 尽管这样,咱们接下来所要作的是 硬件+软件 的一个总体,因此一些基础的 C 语言和硬件知识会让你更好的阅读此文章。没有也没关系,由于高深的我也不会阿!前端
文章会分为 2 个篇幅进行讲解。其中基础篇会先介绍基础知识,有如下几个部分:node
实战篇将会介绍如何在 ESP32 芯片上面运行 JerryScript,有如下几个部分:git
首先先介绍一下 ESP32 是个什么东西,简单来讲,它就是一块集成了 WiFi、蓝牙、天线、功率放大器、电源管理模块、CPU、存储器、传感器的单片机微控制器,说人话就是:它能存储并运行咱们的代码,还具备 WiFi 和蓝牙功能。先来看一张图吧:github
左边一块比较大的就是 ESP32 模组,上面提到的全部硬件配置都集成在这片模组上。下面的板子和其它元件是为了方便开发以及芯片启动必要的电路链接,而作成的一个开发板。这个开发板加了电源转换芯片,使得支持 3.3 - 5V 电压,右边小的方块型的是 cp2102 USB转串口芯片,使得咱们可使用 USB 线链接电脑。这个板子把引脚都引出来了,方便使用杜邦线链接各类外围器件。下面咱们说的 ESP32 都表示这整块开发板,而不是 ESP32 模组自己。web
ESP32 采用两个哈佛结构 Xtensa LX6 CPU 构成双核系统,时钟频率在 80MHz - 240MHz 范围内可调。片上集成了 520KB SRAM, 448KB ROM。拥有 41 个外设模块,包含常见的 IIC, SPI, UART, PWM, IR, I2S, SDIO 等。常见的协议基本都有了,这使得咱们能够更方便的和大部分电子模块或外设进行通讯,而不须要像 51 单片机同样,使用软件模拟实现。好比常见的 SSD12864 oled 屏幕,同时具备 IIC 和 SPI 的接口。BLOX-NEO-6M GPS 模块是使用的 UART 接口。直流电机和伺服机可使用 PWM 驱动。电风扇、空调等用的是 IR 红外线传输信号。编程
除此以外,ESP32 还集成了片上传感器和模拟信号处理器。好比电容式触摸传感器,霍尔传感器,ADC,DAC 等。若是咱们设计了有电池的产品,有了 ADC,咱们就能够方便的测量电池的电量,虽然这样测量的值不必定准。api
以上是单片机系统中很常见的外设,ESP32 将它们都集成在一个片上系统中了。但 ESP32 最让人激动的是,它集成了 WIFI 和 蓝牙。有了 WIFI 和 蓝牙,再配合各类外设,GPIO,咱们就能拿它作不少事情,好比温度湿度传感器的值直接上传到服务器。远程下发执行指令开关灯等,尽能够发挥你的想象。缓存
但硬件编程对于软件工程师来讲却实门槛有点高,尤为像咱们 web前端开发工程师,C 语言就是第一道门槛。我一直想将 JavaScript 带到硬件编程中去,这样咱们就可使用熟悉的 JavaScript 发挥咱们的创意。因此才有了本篇文章。bash
Node.js 很强大,但它是创建在 V8 和 libuv 之上的, ESP32 片上 SRAM 只有 520KB,别说 v8 了,libuv 都跑不起来。因此咱们须要一个轻量的,为嵌入式设计的 JavaScript 引擎,幸运的是,咱们有 JerryScript。
JerryScript 是一个轻量级的 JavaScript 引擎,它能够运行在受限制的设备上,例如微控制器,它能在 RAM < 64 KB, ROM < 200 KB 的设备上运行。并且它还提供了完整的 ES5.1 语法支持,以及部分 ES6 语法支持,好比 箭头函数,Symbol, Promise 等。在编程体验上虽然没有 v8 这么爽,但有这些就已经很好了啊(相对于其它的嵌入式 JavaScript 引擎来讲)!
还有一个重要的点是 JerryScript 的 api 更符合咱们的编程习惯,对于已经习惯编写 Node.js addon 的人来讲会更容易接受。因此以上2点,是咱们选择 JerryScript 的理由。为了让你们更直观的理解,下面咱们对比2个目前在嵌入式比较流行的 JavaScript 引擎。
duktape 目前在 github 上面是 3.7K 个 Star,下面是官网的 hello world!
#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;
}
复制代码
#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;
}
复制代码
FreeRTOS 是一个热门的嵌入式设备用即时操做系统核心,它设计小巧且简易,大部分的代码由 C 语言编写。它提供多任务,互斥锁,信号量,和软件定时器等功能,让用户能够快速的进行应用程序设计。
以上是维基百科的介绍,简单来讲主要就是为设计多任务的应用程序提供基本的工具库,让应用开发者能够专一于逻辑的实现,而不必本身实现任务管理和调度。由于在单片机上编程是没有像 Linux 同样的多进程,多线程的概念的,单片机上电启动后就从指定地址加载指令,按照顺序执行完。
单片机通常来讲只有一个处理器,同一时间只能处理一个任务,假如你想让 2 个 LED 交替闪烁,那么你必须在 while(true){...}
循环内手动控制 2 个 LED 逻辑代码的执行时间,假如后续有 3 个,4 个,N 个呢?那么全部的逻辑都得写在里面,会很是庞大。
FreeRTOS 的任务能让各个逻辑跑在单独的 Task 中互不干扰,各 Task 以优先级抢占 CPU 时间。值得注意的是,即便使用了 FreeRTOS,整个应用仍然是单线程的,高优先级任务执行完后,必需要让出 CPU 时间才能让其它低优先级任务执行。记住,单片机只有一个处理器,同一时间只能处理一个任务。
整个 FreeRTOS 的任务是一个链表,从任务列表中取出最高优先级的任务执行,执行完后再取出下一优先级任务,一直循环。不过有几点不一样,FreeRTOS 永远保证高优先级任务先执行,因此低优先级任务有可能没有机会执行。每次执行完任务后,从列表中取出下一个任务时,都会从新计算优先级。执行中的任务只能由任务自身让出 CPU 时间,不然其它任务没有机会执行,固然除了中断。FreeRTOS 是实时操做系统,你能够精确控制各个任务的开始和结束时间。
以上介绍完了基础知识,下面咱们开始让 JerryScript 在 ESP32 上面跑起来,并让串口接收用户输入,将它输入 JerryScript 中执行。
首先须要准备好 ESP-IDF 开发环境,而后新建一个空的工程,我推荐从 idf-hello-world 新建。JerryScript 将做为一个外部依赖放在 deps/JerryScript
目录。JerryScript 源码地址:JerryScript。
最终咱们的工程目录是这样的:
- build
- deps
- jerryscript
- components
- main
- spiffs
- partitions.csv
- CMakeLists.txt
- sdkconfig
复制代码
build
是咱们的构建目录,构建过程当中的全部临时文件都在这里,方便清理。deps
是存放第三方依赖的目录,JerryScript 将做为一个依赖,这样方便管理,能够和官方保持同步。components
是存放用户组件的目录,咱们本身编写的组件都放在这里。main
是一个特殊的组件,做为应用的主程序。spiffs
是存放内置文件系统的目录,里面的全部文件都会被打包成一个二进制文件,方便烧录到芯片上。partitions.csv
是分区表配置文件,每个应用都须要配置一个分区表,这里使用默认的就好了。CMakeLists.txt
是工程的主构建文件,整个工程的构建将从这里开始。sdkconfig
是工程的配置文件,能够配置系统参数和一些用户自定义的参数。以上全部都准备好后,能够开始写代码了。ESP32 的 CPU 型号是 Xtensa 32-bit LX6,因此咱们须要编写 JerryScript 的交叉编译,而后将静态库连接到 main 组件中去,这样 JerryScript 才能运行起来。
下面是主 CMakeLists.txt 文件内容,主要是指定 JerryScript 的源码目录,这样方便在其它组件内使用。而后设置 JERRY_GLOBAL_HEAP_SIZE
为 128KB。 JERRY_GLOBAL_HEAP_SIZE
表示 JerryScript 虚拟机预先申请的内存大小,在虚拟机启动时就会向系统预先申请指定大小的内存。 由于 ESP32 内存总共才 520KB,而 JerryScript 默认的 heap_size 也是 512KB,这样确定是编译不过的,会报溢出错误。
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)
复制代码
主 cmake 编写好后,接下来是编写 main 组件的 cmake 文件。要使用 JerryScript 很是简单,只须要连接 JerryScript 的静态库,而后配置正确的头文件路径。JerryScript 默认会编译为静态库,咱们在 main 组件中将它们连接就行。
下面是 main 组件的 CMakeLists.txt,内容有点多,这里只选择关键的讲解,详情请看 nodemcujs 项目:
set(COMPONENT_PRIV_INCLUDEDIRS
${JerryScript_SOURCE}/jerry-core/include
${JerryScript_SOURCE}/jerry-ext/include
${JerryScript_SOURCE}/jerry-port/default/include)
复制代码
上面是设置 JerryScript 的头文件查找路径。下面将进行 JerryScript 的交叉编译,并把编译后的静态库连接到 main 组件:
# 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}")
复制代码
上面是设置交叉编译的参数,针对 xtensa 处理器,不加这个参数连接通不过。尤为注意 -mlongcalls
参数,此参数虽然被设置为编译参数,但它实际是做用在汇编的。若是你看到 dangerous relocation: call0: call target out of range
这个错误,多半是忘记加这个参数了。详情请看 xtensa-gcc-longcalls 编译器的文档。 注意,这里的都须要写在 register_component()
后面,不然会报错。
编译参数设置好后,下面是使用 externalproject_add
将 JerryScript 做为外部工程单独编译,不能使用 add_subdirectory
,cmake 会报错。
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)
复制代码
上面主要是将 JerryScript 设置为 main 组件的依赖,这样编译 main 组件时会自动编译 JerryScript。而后设置交叉编译工具链。这里须要特别注意,关闭 ENABLE_LTO=OFF
,为何?由于 JerryScript 里面开了此选项后,会判断编译器 ID 是否为 GNU,若是是的话,强制设置编译器为 GCC
,致使咱们的交叉编译工具链设置失效。
最后,咱们将编译后的静态库连接到 main 组件:
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)
复制代码
JerryScript 编译完后,会在编译目录的 main/jerryscript
下面生成最终文件,这个路径是咱们上面本身指定的,咱们这里只须要 jerry-core.a
jerry-ext.a
jerry-default-minimal.a
这三个静态库就好了。 ${COMPONENT_NAME}
就是 main
。
下面编写初始化代码,在系统启动时初始化 JerryScript 虚拟机。
#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();
}
复制代码
初始化 JerryScript 很是简单,只须要调用 jerry_init(JERRY_INIT_EMPTY)
就能够,如今咱们已经让 js 虚拟机跑起来了。vTaskDelay
是 FreeRTOS 提供的函数,做用是让出指定的 cpu 时间去执行其它任务,不至于将整个应用程序阻塞在这里,1000 / portTICK_PERIOD_MS
表示 1000ms,这跟在 Linux 上使用 sleep(1)
是差很少的。portTICK_PERIOD_MS
表示 FreeRTOS 1ms 内执行的节拍,这跟 CPU 的频率有关,详情请参考 FreeRTOS 文档。
如今 JerryScript 的集成就已经完成了,能够编译出可执行的固件了:
$ mkdir build
$ cd build
$ cmake ..
$ make
复制代码
若是没有错误,会在编译目录生成可执行固件,使用 make flash
会自动将固件烧录到 ESP32 芯片上。make flash
不须要额外的配置,能够直接使用,它会调用内置的 esptool.py 进行烧写。
注意
烧录固件时,须要先安装串口驱动,某宝上面卖的板子质量良莠不齐,型号众多,不少卖家不懂技术,本身卖的是什么都不知道。通常来讲,ESP32 都是 CP2102 的驱动,去官网下载驱动就好了。
具体的烧录方法请查看 nodemcujs 烧录固件。
若是编译出错,请从头开始再来一遍。如今 JerryScript 已经跑起来了,可是咱们尚未 js 代码执行,下面咱们将打开串口,让从串口接收到的字符串输入给 JerryScript 执行,而且将结果从串口输出。
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "driver/uart.h"
// ... 省略其它头文件
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);
}
复制代码
代码有点多,将它拆成 2 个函数来看,handle_uart_input
函数负责安装串口驱动,而后启动一个 Task 来处理串口输入。为何要启动一个 task ?由于串口输入是异步的,咱们不能让它阻塞,因此在新的 task 中采用 esp-uart-events 的方式监听事件,等有串口输入的事件到来时再去读取输入并执行。
板子带有一个 USB 转串口芯片,芯片的引脚被链接到 UART_NUM_0
,因此咱们能够默认从这个串口读取输入,printf
默认也会从这里输出,这样插上 USB 就能够当作一台 mini 的 JavaScript 开发板了,方便开发和调试。这正是动态语言在硬件编程上的魅力。
有了输入,咱们还须要一个 native api 用于在 JavaScript 代码中输出数据,这里咱们使用自带的 print
就好了。在 JavaScript 代码中能够直接使用 print(message)
来输出数据到串口。
#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();
}
复制代码
使用 make flash
编译更新后的固件,将它烧录到板子上,如今打开串口,链接上板子,输入 var msg = 'hello nodemcujs'; print(msg)
试试吧。你能够输入任意合法的 JavaScript 语句,使用 print
函数输出数据。
注意:不要使用 minicom,可使用 ESPlorer。由于咱们是将串口的输入直接输入虚拟机执行的,因此只接收可显示字符和换行回车,其它字符好比控制字符会致使执行失败
完整代码请查看 nodemcujs 源码。
上面咱们已经实现了内嵌 JerryScript 虚拟机而且打通了串口交互,但每次重启都会重置数据,这显然不是一块标准的开发板,本章节咱们将会对接文件系统用于存储用户数据。
ESP32 已经集成了一片 4MB 的 SPI 存储芯片,SPI 是一种数据交换协议,咱们这里不用太关心,感兴趣的本身查找资料,下文咱们以 flash
代指这个存储芯片。
ESP-IDF 工程支持 spiffs 组件,咱们只须要拿来用就好了。要使用文件系统,有这些步骤是必需要作的:
咱们基于默认的分区表上进行修改,新增一个 data 分区用于存储用户自定义数据。在项目根目录新建 partitions.csv
文件,内容以下:
# 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
和 phy_init
分区使用默认就行,factory
分区用于存储 App,即编译出来的可执行代码,也能够理解为编译出来的 bin
文件。咱们指定大小为 1M
,目前编译出来的固件大小为 500KB
左右,通常来讲够用了。
storage
分区是咱们新加的分区,用于存储用户自定义数据,offset
咱们这里不填写,会自动对齐上一个分区。大小指定为 0x2F0000
,差很少有 2.7M
可用空间。注意这是最大了,不能再大,ESP32 最多见的 flash 大小是 4MB
,若是你的 flash
大小不同,能够根据状况修改,但不能超过度区大小,能够小于。
ESP32 默认将分区表数据烧写至 0x8000
地址处,长度为 0xC00
,最多能够保存 95
个条目,分区表后面还保存有 MD5
校验和,因此若是你不清楚整个分区表,不要乱改分区数据。详细说明请看 分区表 文档。
注意
要使用用户自定义分区表,须要在 sdkconfig
文件中指定,可使用 make menuconfig
图形界面,具体方法以下:
$ mkdir build
$ cd build
$ cmake ..
$ make menuconfig
复制代码
执行 make menuconfig
后,会出现图形界面,进入:Partition Table
,Partition Table
选择 Custom partition table CSV
。而后 Custom partition CSV file
填写 partitions.csv
,注意这是你的分区表文件名,请根据你本身的状况修改。
分区表制做好后,接下来咱们在启动流程中 mount storage
分区:若是分区没有被初始化,则格式化分区后再次加载,不然直接加载。而且将使用状况打印出来。
#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
咱们设置为 /spiffs
,这至关于根目录前缀,之后访问数据分区时都要使用 /spiffs/file
,固然你能够根据本身状况修改。将 format_if_mount_failed
参数设置为 true
,能够在分区 mount 失败后自动格式化
,这种状况通常是分区未被格式化。注意 spiffs 文件系统是没有目录概念的,/
只是被当作一个文件名,后续咱们能够本身模拟目录的概念。
挂载分区后,咱们就可使用文件系统的 api 去读写文件了。咱们使用 esp_spiffs_info
读取文件系统信息,将总大小和已使用状况打印出来。
最后,在启动流程中调用这个函数:
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();
}
复制代码
从新编译,而后烧写,使用串口链接上板子查看打印出来的分区信息,若是看到成功打印出分区表数据,则说明文件系统挂载成功了,若是失败了,则仔细检查一遍哪里出错了。
上面咱们已经有了文件的概念了,那咱们就能够编写 js 文件模块,而后使用 require 去加载文件模块,而且开机自动加载执行 index.js 文件,这样 JavaScript 开发者来讲就能够脱离 SDK 独立开发了。固然涉及到硬件驱动部分仍是须要 SDK 支持,暴露接口给 JavaScript,这里不进行细说。
先来看一下 Node.js 中的文件模块长什么样:
// a.js
module.exports = function a () {
console.log(`hello, i am ${__filename}`)
}
复制代码
这个模块很简单,只对外提供一个函数,函数里面打印出自身的文件名。那么如何使用这个模块呢:
// index.js
var a = require('./a.js')
a()
复制代码
只须要使用 require
函数加载这个模块,赋值给一个变量,这个变量就引用了模块的全部对外实现。由于咱们对外就暴露一个函数,因此能够直接调用。那么这里的 module.exports
变量是从哪里来的呢?__filename
又为何会等于 a.js
呢?require
的返回值是怎么来的呢?来简单看一下 Node.js 是如何实现的。
当 require 一个文件模块时,Node.js 会读取文件的内容,而后将内容头尾包装一下,最终变成:
(function (exports, require, module, __filename, __dirname) {
// 模块源码
})
复制代码
把参数传递进去执行这个函数,因此咱们能够在文件模块中使用 exports 等未定义的变量,最后 require 函数将 exports 变量返回,就完成了一次模块的加载。固然,Node.js 中的实现是比这个要复杂不少的,这里只是简单的描述一下,详情请查看 Node.js: require 源码。
知道了 require 是如何工做的,如今咱们来实现一个最简单的 require,它只从文件系统中加载文件模块,而且不支持缓存和相对路径的。若是加载成功,则返回模块的 exports 对象,不然返回 undefined。
能够新建一个 用户组件,叫 jerry-module,也能够直接在 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);
}
复制代码
咱们规定每一个 native 模块都有一个 init
方法,以 module
开头,中间的 module 表示模块名。在 init 方法中会给 global
变量注册模块自身须要暴露给 JavaScript 的 api,这样 JavaScript 就可使用了。下面是 require 函数的实现。
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;
}
复制代码
这里咱们的实现很是简单:
name
,表示文件模块的绝对路径。jerry_port_read_source
读取文件的内容,注意使用这个函数须要头文件 jerryscript-port.h
,使用完后记得使用 jerry_port_release_source
释放文件内容。jerry_parse_function
构造一个 JavaScript 函数,咱们这里只实现 exports, module, __filename
这三个参数。jerry_create_object
构造一个 JavaScript object,使用 jerry_set_property
给这个 object 设置 exports
属性。jerry_call_function
将 exports, module, filename
做为参数执行函数,这样文件模块就会执行。module.exports
是 exports
的引用。exports
变量,这就是模块对外暴露的 api,咱们使用 jerry_get_property
将 exports
属性返回,就完成了模块加载。最后,咱们在虚拟机初始化后,调用模块的初始化函数,将模块注册到虚拟机:
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();
}
复制代码
如今,咱们差最后一步:从文件系统中加载执行 index.js
文件,这样开机启动就会自动执行代码了。实现这个也很简单,在全部操做都完成后,使用文件 api 从文件系统读取 index.js
文件,而后使用 jerry_run
执行。
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);
}
复制代码
entry 的入口能够本身修改,咱们指定 /spiffs/index.js
。若是加载失败,则什么也不作。若是加载成功,则使用 jerry_parse
编译 js 代码,最后使用 jerry_run
执行。一样,在启动流程中调用这个函数。
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();
}
复制代码
如今,咱们整理一下启动流程都作了什么:
至此,咱们有了一个 JavaScript 开发板了,但功能有限,驱动部分以及经常使用的功能模块都没有实现。原本还想介绍一下 native 模块和定时器的,篇幅有限,这里就再也不详细介绍了,完整的源码请查看 nodemcujs。
最后再简单介绍一下如何上传 index.js 以及自定义数据到文件系统:
完整的文件镜像制做和烧录方法请查看 nodemcujs 制做文件镜像。