引言
JavaScript 通常运行在浏览器或 Node.js 环境中,但在资源受限的嵌入式设备上运行 JavaScript 也是可能的。MJS(Mongoose JavaScript) 是 Cesanta 公司开发的超轻量级 JavaScript 引擎,专为嵌入式系统设计。
- 代码大小:仅 60-100KB ROM,10-30KB RAM
- 性能:基于字节码解释器,执行效率高
- 特性:支持 ES5 核心语法、异步回调、硬件访问
本文从架构原理到实战项目,带你全面掌握嵌入式 JavaScript 开发。
MJS 架构解析
1.1 整体架构
核心组件:
- 词法/语法分析器:解析 JavaScript 源代码
- 字节码生成器:编译为紧凑的字节码格式
- 虚拟机解释器:执行字节码,管理堆栈
- 垃圾回收器:自动内存管理(标记 - 清除算法)
- FFI 接口:调用 C 函数,访问硬件资源
1.2 内存模型
内存管理特点:
- 堆大小可配置:默认 10-50KB,根据 MCU RAM 调整
- 标记 - 清除算法:从根节点遍历,标记可达对象
- 自动回收:当堆内存不足时触发 GC
- 手动触发:
mjs_gc()强制垃圾回收
1.3 与其他引擎对比
| 特性 | MJS | JerryScript | Duktape | Espruino |
|---|---|---|---|---|
| ROM 占用 | 60KB | 80KB | 100KB | 150KB |
| RAM 占用 | 10KB | 15KB | 20KB | 30KB |
| ES 支持 | ES5 | ES5.1 | ES5 | ES5+ |
| FFI 支持 | ✅ | ✅ | ✅ | ✅ |
| 异步支持 | 回调 | Promise | 无 | 事件驱动 |
| 商业许可 | 双授权 | Apache 2.0 | MIT | GPL |
环境搭建
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 引脚 | 说明 |
|---|---|---|
| LED | GPIO 2 | 板载 LED |
| 按键 | GPIO 0 | 启动按钮 |
| UART | GPIO 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 引擎要点:
- 超轻量级:60KB ROM,10KB RAM,适合 MCU
- FFI 强大:轻松调用 C 函数,访问硬件
- 开发高效:JavaScript 语法,快速迭代
- 跨平台:ESP32、STM32、Linux 均支持
- 社区活跃: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