引言

JavaScript 通常运行在浏览器或 Node.js 环境中,但在资源受限的嵌入式设备上运行 JavaScript 也是可能的。MJS(Mongoose JavaScript) 是 Cesanta 公司开发的超轻量级 JavaScript 引擎,专为嵌入式系统设计。

  • 代码大小:仅 60-100KB ROM,10-30KB RAM
  • 性能:基于字节码解释器,执行效率高
  • 特性:支持 ES5 核心语法、异步回调、硬件访问

本文从架构原理到实战项目,带你全面掌握嵌入式 JavaScript 开发。

MJS 架构解析

1.1 整体架构

JavaScript 应用代码GPIO、I2C、SPI、网络、定时器MJS 核心引擎词法分析器语法分析器字节码生成器虚拟机解释器垃圾回收FFI(Foreign Function Interface)C 函数绑定、硬件抽象层、系统调用ESP32 HALGPIO、UART、I2CSTM32 HALGPIO、SPI、ADCPOSIX 层Linux、macOS
MJS 引擎架构分层

核心组件

  • 词法/语法分析器:解析 JavaScript 源代码
  • 字节码生成器:编译为紧凑的字节码格式
  • 虚拟机解释器:执行字节码,管理堆栈
  • 垃圾回收器:自动内存管理(标记 - 清除算法)
  • FFI 接口:调用 C 函数,访问硬件资源

1.2 内存模型

MJS Heap(堆内存)Object A{x: 1, y: 2}Array B[1, 2, 3]Function Cfunction(){}Object DunreachableRoot Set⚠️ 不可达对象✓ 存活对象
MJS 垃圾回收机制

内存管理特点

  • 堆大小可配置:默认 10-50KB,根据 MCU RAM 调整
  • 标记 - 清除算法:从根节点遍历,标记可达对象
  • 自动回收:当堆内存不足时触发 GC
  • 手动触发mjs_gc() 强制垃圾回收

1.3 与其他引擎对比

特性MJSJerryScriptDuktapeEspruino
ROM 占用60KB80KB100KB150KB
RAM 占用10KB15KB20KB30KB
ES 支持ES5ES5.1ES5ES5+
FFI 支持
异步支持回调Promise事件驱动
商业许可双授权Apache 2.0MITGPL

环境搭建

2.1 获取源码

# 克隆 MJS 仓库
git clone https://github.com/cesanta/mjs.git
cd mjs

# 切换到稳定版本
git checkout 1.20.0  # 2026 年最新版

# 目录结构
.
├── mjs.c              # 核心引擎实现
├── mjs.h              # 公共 API 头文件
├── ffi.c              # FFI 接口实现
├── gc.c               # 垃圾回收器
├── examples/          # 示例代码
└── ports/             # 平台移植层
    ├── esp32/
    ├── stm32/
    └── posix/

2.2 编译配置

// mjs_config.h - 内存配置
#ifndef MJS_CONFIG_INCLUDED
#define MJS_CONFIG_INCLUDED

// 堆大小(字节)
#define MJS_HEAP_SIZE (32 * 1024)  // 32KB

// 最大字符串长度
#define MJS_MAX_STRING_LEN 256

// 最大嵌套深度
#define MJS_MAX_CALL_DEPTH 32

// 启用调试输出
#define MJS_ENABLE_DEBUG 1

// 启用 FFI
#define MJS_ENABLE_FFI 1

#endif

2.3 最小可运行示例(POSIX)

#include "mjs.h"
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // 创建 MJS 实例
    struct mjs *mjs = mjs_create();
    
    // 执行 JavaScript 代码
    mjs_err_t err = mjs_exec(mjs, "print('Hello from MJS!');", NULL);
    if (err != MJS_OK) {
        printf("错误:%s\n", mjs_strerror(mjs, err));
    }
    
    // 执行数学运算
    mjs_val_t result = mjs_exec(mjs, "2 + 3 * 4", NULL);
    double num = mjs_get_double(mjs, result);
    printf("结果:%.0f\n", num);  // 输出:14
    
    // 清理资源
    mjs_destroy(mjs);
    return 0;
}

// 编译命令
gcc -o mjs_demo mjs.c main.c -lm -ldl

ESP32 实战

3.1 硬件连接

模块ESP32 引脚说明
LEDGPIO 2板载 LED
按键GPIO 0启动按钮
UARTGPIO 1/3串口调试

3.2 Arduino 集成

#include <mjs.h>
#include "mjs_port_esp32.h"

// 全局 MJS 实例
struct mjs *g_mjs;

// FFI 函数:控制 GPIO
mjs_val_t ffi_gpio_write(struct mjs *mjs, mjs_val_t args[], int nargs) {
    if (nargs != 2) return mjs_mk_undefined(mjs);
    
    int pin = mjs_get_int(mjs, args[0]);
    int value = mjs_get_int(mjs, args[1]);
    
    pinMode(pin, OUTPUT);
    digitalWrite(pin, value);
    
    return mjs_mk_undefined(mjs);
}

// FFI 函数:延时
mjs_val_t ffi_delay(struct mjs *mjs, mjs_val_t args[], int nargs) {
    if (nargs != 1) return mjs_mk_undefined(mjs);
    
    int ms = mjs_get_int(mjs, args[0]);
    delay(ms);
    
    return mjs_mk_undefined(mjs);
}

// FFI 函数:读取模拟量
mjs_val_t ffi_analog_read(struct mjs *mjs, mjs_val_t args[], int nargs) {
    if (nargs != 1) return mjs_mk_undefined(mjs);
    
    int pin = mjs_get_int(mjs, args[0]);
    int value = analogRead(pin);
    
    return mjs_mk_number(mjs, value);
}

// 注册 FFI 函数
void register_ffi(struct mjs *mjs) {
    mjs_set(mjs, mjs_get_global(mjs), "gpioWrite", 
            MJS_MK_FN(ffi_gpio_write));
    mjs_set(mjs, mjs_get_global(mjs), "delay", 
            MJS_MK_FN(ffi_delay));
    mjs_set(mjs, mjs_get_global(mjs), "analogRead", 
            MJS_MK_FN(ffi_analog_read));
}

void setup() {
    Serial.begin(115200);
    
    // 初始化 MJS
    g_mjs = mjs_create();
    register_ffi(g_mjs);
    
    // 执行初始化脚本
    const char *init_script = R"(
        print('MJS 已初始化');
        print('ESP32 芯片:' + chipModel());
        
        // LED 闪烁测试
        gpioWrite(2, 1);
        delay(500);
        gpioWrite(2, 0);
    )";
    
    mjs_err_t err = mjs_exec(g_mjs, init_script, NULL);
    if (err != MJS_OK) {
        Serial.printf("初始化失败:%s\n", mjs_strerror(g_mjs, err));
    }
}

void loop() {
    // 从串口读取 JavaScript 代码并执行
    if (Serial.available()) {
        String code = Serial.readStringUntil('\n');
        mjs_err_t err = mjs_exec(g_mjs, code.c_str(), NULL);
        if (err != MJS_OK) {
            Serial.printf("执行错误:%s\n", mjs_strerror(g_mjs, err));
        }
    }
    
    delay(100);
}

3.3 JavaScript 应用示例

// blink.js - LED 闪烁
var ledPin = 2;
var state = 0;

setInterval(function() {
    state = 1 - state;
    gpioWrite(ledPin, state);
    print('LED 状态:' + state);
}, 1000);

// sensor.js - 传感器读取
var sensorPin = 34;
var threshold = 2000;

function readSensor() {
    var value = analogRead(sensorPin);
    print('传感器值:' + value);
    
    if (value > threshold) {
        print('⚠️ 超过阈值!');
        gpioWrite(2, 1);  // 打开 LED 告警
    } else {
        gpioWrite(2, 0);
    }
}

// 每 500ms 读取一次
setInterval(readSensor, 500);

// wifi.js - 网络连接(需要 ESP32 WiFi 库)
var ssid = 'MyWiFi';
var password = 'secret123';

WiFi.begin(ssid, password);
print('正在连接 WiFi...');

while (WiFi.status() !== WL_CONNECTED) {
    delay(500);
    print('.');
}

print('已连接!IP: ' + WiFi.localIP());

// 发送 HTTP 请求
var http = new HTTPClient();
http.begin('http://api.example.com/data');
var code = http.GET();
print('HTTP 状态码:' + code);

STM32 实战

4.1 基于 HAL 的移植

// mjs_port_stm32.c
#include "mjs.h"
#include "stm32f4xx_hal.h"
#include <stdio.h>
#include <stdlib.h>

// 系统时钟获取
mjs_val_t ffi_sys_clock(struct mjs *mjs, mjs_val_t args[], int nargs) {
    uint32_t clock = HAL_RCC_GetHCLKFreq();
    return mjs_mk_number(mjs, clock);
}

// GPIO 初始化
mjs_val_t ffi_gpio_init(struct mjs *mjs, mjs_val_t args[], int nargs) {
    if (nargs != 2) return mjs_mk_undefined(mjs);
    
    GPIO_TypeDef *port = (GPIO_TypeDef *)mjs_get_ptr(mjs, args[0]);
    uint16_t pin = mjs_get_int(mjs, args[1]);
    
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(port, &GPIO_InitStruct);
    
    return mjs_mk_undefined(mjs);
}

// GPIO 写入
mjs_val_t ffi_gpio_write(struct mjs *mjs, mjs_val_t args[], int nargs) {
    if (nargs != 3) return mjs_mk_undefined(mjs);
    
    GPIO_TypeDef *port = (GPIO_TypeDef *)mjs_get_ptr(mjs, args[0]);
    uint16_t pin = mjs_get_int(mjs, args[1]);
    int value = mjs_get_int(mjs, args[2]);
    
    HAL_GPIO_WritePin(port, pin, value ? GPIO_PIN_SET : GPIO_PIN_RESET);
    
    return mjs_mk_undefined(mjs);
}

// ADC 读取
mjs_val_t ffi_adc_read(struct mjs *mjs, mjs_val_t args[], int nargs) {
    if (nargs != 1) return mjs_mk_undefined(mjs);
    
    ADC_HandleTypeDef *hadc = (ADC_HandleTypeDef *)mjs_get_ptr(mjs, args[0]);
    
    HAL_ADC_Start(hadc);
    HAL_ADC_PollForConversion(hadc, 10);
    uint32_t value = HAL_ADC_GetValue(hadc);
    HAL_ADC_Stop(hadc);
    
    return mjs_mk_number(mjs, value);
}

// 注册所有 FFI 函数
void mjs_init_stm32(struct mjs *mjs) {
    // 系统函数
    mjs_set(mjs, mjs_get_global(mjs), "sysClock", 
            MJS_MK_FN(ffi_sys_clock));
    
    // GPIO 函数
    mjs_set(mjs, mjs_get_global(mjs), "gpioInit", 
            MJS_MK_FN(ffi_gpio_init));
    mjs_set(mjs, mjs_get_global(mjs), "gpioWrite", 
            MJS_MK_FN(ffi_gpio_write));
    
    // ADC 函数
    mjs_set(mjs, mjs_get_global(mjs), "adcRead", 
            MJS_MK_FN(ffi_adc_read));
}

4.2 主程序

// main.c
#include "mjs.h"
#include "stm32f4xx_hal.h"

// MJS 堆内存(32KB)
static uint8_t mjs_heap[32 * 1024];

// JavaScript 脚本(存储在 Flash 中)
const char js_script[] = R"(
    print('STM32 MJS 演示');
    print('系统时钟:' + sysClock() + ' Hz');
    
    // 初始化 GPIO
    var GPIOA = 0x40020000;  // GPIOA 基地址
    var PIN5 = 0x0020;       // PA5
    
    gpioInit(GPIOA, PIN5);
    
    // LED 闪烁
    for (var i = 0; i < 10; i++) {
        gpioWrite(GPIOA, PIN5, 1);
        delay(500);
        gpioWrite(GPIOA, PIN5, 0);
        delay(500);
    }
    
    print('演示完成!');
)";

int main(void) {
    HAL_Init();
    SystemClock_Config();
    
    // 初始化 MJS
    struct mjs *mjs = mjs_create();
    mjs_init_stm32(mjs);
    
    // 执行脚本
    mjs_err_t err = mjs_exec(mjs, js_script, NULL);
    if (err != MJS_OK) {
        printf("错误:%s\n", mjs_strerror(mjs, err));
    }
    
    // 进入空闲循环
    while (1) {
        __WFI();
    }
}

高级特性

5.1 异步编程

// 回调函数风格
function readSensor(callback) {
    var value = analogRead(34);
    callback(value);
}

readSensor(function(val) {
    print('传感器值:' + val);
});

// 使用 setTimeout 实现非阻塞
var counter = 0;

function tick() {
    counter++;
    print('计数器:' + counter);
    setTimeout(tick, 1000);  // 1 秒后再次调用
}

tick();  // 启动

5.2 模块系统

// math.js - 数学模块
var MathLib = {
    PI: 3.14159,
    
    add: function(a, b) {
        return a + b;
    },
    
    multiply: function(a, b) {
        return a * b;
    },
    
    circleArea: function(r) {
        return this.PI * r * r;
    }
};

// main.js - 使用模块
load('math.js');

var result = MathLib.add(5, 3);
print('5 + 3 = ' + result);

var area = MathLib.circleArea(10);
print('圆面积:' + area);

5.3 错误处理

// try-catch 风格(MJS 模拟实现)
function safeExec(fn) {
    try {
        fn();
    } catch (e) {
        print('错误:' + e);
    }
}

// 实际 MJS 中使用错误回调
function divide(a, b, errorCallback) {
    if (b === 0) {
        errorCallback('除零错误');
        return undefined;
    }
    return a / b;
}

divide(10, 0, function(err) {
    print('捕获错误:' + err);
});

5.4 与 C 代码互操作

// C 侧:定义结构体
typedef struct {
    int x;
    int y;
} Point;

// JavaScript 侧:访问结构体
var Point = {
    create: ffi(function(x, y) {
        Point *p = malloc(sizeof(Point));
        p->x = x;
        p->y = y;
        return p;  // 返回指针
    }),
    
    getX: ffi(function(ptr) {
        return ((Point *)ptr)->x;
    }),
    
    setX: ffi(function(ptr, val) {
        ((Point *)ptr)->x = val;
    })
};

// 使用
var p = Point.create(10, 20);
print('X = ' + Point.getX(p));
Point.setX(p, 30);

性能优化

6.1 内存优化

// 减小堆大小(如果 RAM 紧张)
#define MJS_HEAP_SIZE (16 * 1024)  // 16KB

// 手动触发 GC
void periodic_gc(void) {
    static uint32_t counter = 0;
    if (++counter % 100 == 0) {
        mjs_gc(g_mjs);
        print('已执行垃圾回收');
    }
}

6.2 代码优化

// ❌ 避免:频繁创建对象
function bad() {
    for (var i = 0; i < 100; i++) {
        var obj = {x: i, y: i * 2};  // 每次循环创建新对象
    }
}

// ✅ 推荐:复用对象
function good() {
    var obj = {x: 0, y: 0};
    for (var i = 0; i < 100; i++) {
        obj.x = i;
        obj.y = i * 2;  // 复用同一对象
    }
}

// ❌ 避免:长字符串拼接
var s = '';
for (var i = 0; i < 100; i++) {
    s += i.toString();
}

// ✅ 推荐:使用数组
var arr = [];
for (var i = 0; i < 100; i++) {
    arr.push(i.toString());
}
var s = arr.join('');

6.3 预编译字节码

# 将 JavaScript 编译为字节码(减小体积)
./mjs_compile.js app.js > app.mbc

# 加载字节码
mjs_exec_file(mjs, "app.mbc", NULL);

实际项目案例

7.1 智能家居控制器

// smart_home.js
var devices = {
    light: {pin: 2, state: false},
    fan: {pin: 4, state: false},
    ac: {pin: 5, state: false}
};

function toggle(device) {
    var dev = devices[device];
    if (!dev) {
        print('未知设备:' + device);
        return;
    }
    
    dev.state = !dev.state;
    gpioWrite(dev.pin, dev.state ? 1 : 0);
    print(device + ' 已' + (dev.state ? '开启' : '关闭'));
}

// MQTT 集成(通过 FFI)
mqtt.subscribe('home/+/control', function(topic, msg) {
    var parts = topic.split('/');
    var room = parts[1];
    var action = msg;
    
    toggle(room);
});

// 定时任务
setInterval(function() {
    // 每 5 分钟上报状态
    var status = JSON.stringify(devices);
    mqtt.publish('home/status', status);
}, 300000);

7.2 数据记录器

// data_logger.js
var fs = require('fs');
var logFile = '/sdcard/sensor_log.csv';

// 初始化 CSV 文件
if (!fs.exists(logFile)) {
    fs.write(logFile, 'timestamp,temperature,humidity\n');
}

function logData() {
    var temp = readTemperature();
    var humid = readHumidity();
    var ts = Date.now();
    
    var line = ts + ',' + temp + ',' + humid + '\n';
    fs.append(logFile, line);
    
    print('已记录:' + temp + '°C, ' + humid + '%');
}

// 每分钟记录一次
setInterval(logData, 60000);

// 命令处理
onCommand('export', function() {
    var data = fs.read(logFile);
    mqtt.publish('sensor/export', data);
});

常见问题

Q1: 内存不足错误

原因:堆大小不足或内存泄漏。

解决

// 增大堆
#define MJS_HEAP_SIZE (64 * 1024)

// 检查内存使用
print('堆使用:' + mjs_get_heap_used() + ' 字节');

// 手动 GC
mjs_gc(mjs);

Q2: FFI 调用崩溃

原因:指针类型不匹配或内存访问越界。

解决

// 确保 C 侧正确注册类型
mjs_set(mjs, global, "myFunc", MJS_MK_FN(ffi_my_func));

// JavaScript 侧检查参数
if (typeof arg !== 'number') {
    print('参数类型错误');
    return;
}

Q3: 执行速度慢

原因:频繁 GC 或复杂计算。

解决

  • 减少对象创建频率
  • 将计算密集型任务移到 C 侧
  • 使用字节码预编译

总结

MJS 嵌入式 JavaScript 引擎要点:

  1. 超轻量级:60KB ROM,10KB RAM,适合 MCU
  2. FFI 强大:轻松调用 C 函数,访问硬件
  3. 开发高效:JavaScript 语法,快速迭代
  4. 跨平台:ESP32、STM32、Linux 均支持
  5. 社区活跃:Cesanta 维护,文档完善

使用 MJS,让嵌入式开发像 Web 开发一样高效!


本文基于 MJS 1.20.0 和 2026 年嵌入式 JavaScript 生态编写。

参考资料

  • MJS 官方仓库:https://github.com/cesanta/mjs
  • Mongoose OS 文档:https://mongoose-os.com/docs/
  • Cesanta 官网:https://cesanta.com/
  • JavaScript 嵌入式应用案例:https://esp32.com/viewtopic.php?t=12345