我们做了一个能运行在单片机/嵌入式上的JavaScript引擎

感谢吴烜向我推荐了这个论坛,,进来发现好多知乎互相关注的熟人。。。

这个引擎的编译器和虚拟机还有不少小问题,所以暂时还未正式开源上线,不过今年肯定会开源上线

我这里展示一些小Demo,大家都可以探讨一下

1.这个JavaScript引擎目前支持绝大部分ES5和部分ES6语法。
2.支持英文或中文或者混合写代码。
3.编译器和虚拟机均为C语言实现,虚拟机基于栈实现,并未用WASM。

支持绝大部分ES5和ES6语法:
原生支持JSON,数组,字符串,及常用的方法和对象这些基础没什么好说的,不过还是有些不同,比如不支持函数嵌套声明(我个人觉得这没什么卵用),不过嵌套闭包还是可以的:
Video_20230319231151

比如修改了typeof语法,说实话原来JavaScript的typeof有些鸡肋,修改后:

编译器和虚拟机为C语言实现:
既然要移植到各种单片机/嵌入式芯片上运行,至少虚拟机必然要用C语言写,这个虚拟机是基于栈实现一共59条指令,可以通过编译器,编译出字节码文件及相应调试文件,这个字节码调试文件是有中文的,你可以看一下某些程序在栈中的执行情况,这是上面闭包Demo的字节码调试文件:


中文字节码调试文件

这个JavaScript引擎是已经在单片机上扩展封装了大量的库和API:包括常用的单片机硬件对象,语音识别对象,物联网对象(HTTP和MQTT),GUI对象等。

在单片上用JavaScript做一个计算器:


代码在这里:

//导入UI界面必要的库文件
importFile "tim_ui.js"

import tim;

//创建一个字体,单片机硬件资源有限,还不能动态生成字体,只能加载生成好的字库文件
const myFont = createFont({
	file: 'font/lh32_20_7f_b4.bin',
	start: 0x20,
	end: 0x7f,
	bpp: 4,
	lineHeight: 32,
});

//创建另一个字体
const keyFont = createFont({
	file: 'font/lh21_20_7f_b4.bin',
	start: 0x20,
	end: 0x7f,
	bpp: 4,
	lineHeight: 21,
});

const disCont = new timui(curScreen, widgetCont);
timui.style(disCont, {
		'width': '100%',
		'height': '80px',
		'borderBottom': '3px 0x6c757d',
		'color': 0xdc3545, //同CSS指容器内文字颜色
});
const disLabel = new timui(disCont, widgetLabel);
disLabel.setText('0');
timui.style(disLabel, {'font': myFont, 'align': alignRM,});


const stacks = [0, 0, 0, 0, 0];
let stackPointer = 0;
function pushStack(v)
{
	stacks[stackPointer] = v;
	stackPointer++;
	
	if (stackPointer == 3) {
		//当栈里有三个数据时,就可以计算了
		if (typeof stacks[0] === 'string') {
			stackPointer = 1;
			return;
		}

		switch (stacks[1]) {
		case 1:
			stacks[0] += stacks[2];
			break;
		case 2:
			stacks[0] -= stacks[2];
			break;
		case 3:
			stacks[0] *= stacks[2];
			break;
		case 4:
			if (stacks[2] === 0) {
				stacks[0] = '#ff0000 ERR /0!#';
			} else {
				stacks[0] /= stacks[2];
			}
			break;
		}
		stackPointer = 1;
	}
}

let num = 0;
let isFloat = 1, isInt = 10;
let isClear = 0;
function myCalc(evt)
{
	switch (evt.value) {
	case '+':
		pushStack(num);
		pushStack(1);
		break;  
	case '-':
		pushStack(num);
		pushStack(2);
		break;
	case '*':
		pushStack(num);
		pushStack(3);
		break;
	case '/':
		pushStack(num);
		pushStack(4);
		break;
	case '=':
		if (isClear) {
			stackPointer = 0;
			stacks[0] = 0;
			stacks[1] = 0;
			this.children[0].setText('=');
			timui.style(this, {'backgroundColor': 0xffc107});
		} else {
			if (stackPointer == 2) {
				//说明有数据入栈
				pushStack(num);
				stackPointer = 0;
			}
			stacks[1] = 0;
			this.children[0].setText('CE');
			timui.style(this, {'backgroundColor': 0xdc3545});
		}
		isClear = !isClear;
		break;
	case '.':
		isFloat = 0.1;
		isInt = 1;
		disLabel.setText(num + '.');
		return;
	default:
		let n = evt.value.toNumber();
		num = num * isInt + n * isFloat;
		if (typeof isFloat === 'double') {
			isFloat *= 0.1;
		}
		disLabel.setText(num + '');
		return;
	}
	
	num = 0;
	isFloat = 1;
	isInt = 10;
	
	switch (stacks[1]) {
	case 1:
		disLabel.setText(stacks[0] + '+');
		break;
	case 2:
		disLabel.setText(stacks[0] + '-');
		break;
	case 3:
		disLabel.setText(stacks[0] + 'x');
		break;
	case 4:
		disLabel.setText(stacks[0] + '/');
		break;
	default:
		disLabel.setText(stacks[0] + '');
		break;
	}
}



const btnm = new timui(curScreen, widgetCont);
timui.style(btnm, {
		'width': '100%',
		'height': '360px',
		'align': alignBM,
		'top': '-17px',
		
		'display': 'flex',
		flexFlow: flexFlowRowWrap,
		justifyContent: flexStretch,
		alignContent: flexStretch,
});

const btns = ['7', '8', '9', '/',
			'4', '5', '6', '*',
			'1', '2', '3', '-',
			'.', '0', '=', '+'];
const btnSize = (320 - 40) / 4;
for (let i in btns) {
	const cell = new timui(btnm);
	timui.style(cell, {'width': btnSize + 'px', 'height': btnSize + 'px', 'borderRadius': '12px', backgroundColor: 0xe0e0e0});
	timui.style(cell, {'backgroundColor': 0xa1a1a1}, stPressed); //设置按下时的样式
	const label = new timui(cell, widgetLabel);
	timui.style(label, {'font': keyFont});
	label.setText(btns[i]);
	timui.style(label, {'align': alignC,});
	
	timui.bindEvent(cell, evtClicked, myCalc, btns[i]);
}
//单独为14号按钮(也就是‘等于按钮’)设置一个背景色
timui.style(btnm.children[14], {'backgroundColor': 0xffc107});

单片机上行还模拟实现了CSS,这样前端的知识拿过来直接用。。。

这些Demo,包括上面的基于栈的单片机Demo都是可以用全中文实现的,篇幅有限我这里不展开贴代码了。

比如,中文脚本编程在单片机上实现一个温湿度检测仪:https://zhuanlan.zhihu.com/p/594793888

4 个赞

欢迎!
其实开源也可以分步走,比如从用户最可能需要参考源码的部分开始。
中文字节码挺友好,不知有文档介绍各指令吗?
期待开放内测~

哈哈,在知乎上看到过大佬的文章 :wave:

中英文指令说明

#include "share.h"

OpcodeSpecifier tvm_opcode_specifier[] = {
	{"dummy", ""},
	{"push_int_1byte", "入栈1字节整数"},
	{"push_int_2byte", "入栈2字节整数"},
	{"push_int", "入栈整数"},
	{"push_double_0", "入栈浮点数0.0"},
	{"push_double_1", "入栈浮点数1.0"},
	{"push_double", "入栈浮点数"},
	{"push_string", "入栈字符串"},
	{"push_closure", "入栈函数"},
	{"push_null", "入栈空"},
	/**********/
	{"push_stack_let", "入栈局部变量"},
	{"pop_stack_let", "出栈到局部变量"},
	/**********/
	{"push_static_let", "入栈全局变量"},
	{"pop_static_let", "出栈到全局变量"},
	/**********/
	{"push_array", "入栈数组"},
	{"pop_array", "出栈到数组"},
	/**********/
	{"push_object", "入栈对象"},
	{"pop_object", "出栈到对象"},
	/**********/
	{"add_let", "加"},
	{"sub_let", "减"},
	{"mul_let", "乘"},
	{"div_let", "除"},
	{"mod_let", "取余"},
	{"minus_let", "取负"},
	{"increment_let", "递增"},
	{"decrement_let", "递减"},
	{"eq_let", "等于"},
	{"gt_let", "大于"},
	{"ge_let", "大于等于"},
	{"lt_let", "小于"},
	{"le_let", "小于等于"},
	{"ne_let", "不等于"},
	{"logical_and", "且"},
	{"logical_or", "或"},
	{"logical_not", "非"},
	{"pop", "出栈"},
	{"duplicate", "复制栈"},
	{"jump", "跳转"},
	{"jump_if_true", "如果真跳转"},
	{"jump_if_false", "如果假跳转"},
	/**********/
	{"invoke", "调用函数"},
	{"return", "函数返回"},
	/**********/
	{"new_array_literal", "创建常量数组"},
	/**********/
	{"new_json_literal", "创建常量数据对象"},
	/**********/
	{"set_foreach_size", "设置遍历大小"},
	/**********/
	{"duplicate_increment", "复制递增值"},
	{"duplicate_decrement", "复制递减值"},
	/**********/
	{"bit_and_let", "位与"},
	{"bit_xor_let", "位异或"},
	{"bit_or_let", "位或"},
	{"aeq_let", "类型等于"},
	{"ane_let", "类型不等于"},
	{"left_move_let", "左移"},
	{"right_move_let", "右移"},
	/**********/
	{"typeof", "求类型"},
	/**********/
	//闭包相关
	{"push_closure_let", "入栈闭包变量"},
	{"pop_closure_let", "出栈到闭包变量"},
	{"set_closure_env", "设置闭包环境"},
	{"set_child_closure_env", "设置子闭包环境"},
	{"set_parent_function_env", "设置父函数环境"},
};

2 个赞

不是大佬,相互学习哈

哈哈,期待开源 :wave: :wave:

大佬大佬,膜拜大佬

不是大佬。。。你的洛书语言,是认真拜读了你给的链接,我们都是主打同时支持中英文编程,主要扩展方向又都放在单片机/嵌入式上,可以交流一下

1 个赞

你现在的主要支持芯片应该是STM32吧?后面会考虑用洛书+STM32做一些比较有趣的项目吗?比如:智能收音机,智能红外遥控器之类的。

现在的MCU我只做了w806的固件,32也测试过能用,后面可能会做一些小项目,但现在还差不少基础设施没做。还是很有待于完善的

1 个赞

期待大作提莫开发顺利

主要推广方法是什么?通过做这些单片机小项目,录成视频发到平台上吗?

嗯,应该会做一些发在B站上,分享一些demo。
但估计得再过一段时间了,先补上一些技术指标再说

在华为也做过类似的工作,当时是做了一个轻量化的js脚本引擎,支持es5.1,项目名字是MapleJS。匈牙利赛格德大学的tibor教授团队开源了一个类似的项目jerryscript。

jerryScript应该是三星主导开源的项目吧,RTThread也在用他,之前大致了解过

也是算是三星的,三星出的钱,Tibor团队维护和演进JerryScript。

嗯嗯,我们的JavaScript引擎目前,主要语法中除了class不支持(实在不想支持),基本支持的差不多了,把分号自动插入补一下,应该算是一个较为完整的JavaScript引擎了吧。。。

js的class没有必要,其实可以考虑在内存RAM使用上的优化,这样你的js引擎就可以在esp32这样的芯片上运行,那是比较有商业价值的。

ESP32这种“高端”单片机,运行这个JavaScript引擎,太容易了,毕竟有200多KB内存给你用,,,这个引擎是可以在小于40KB内存的ESP8266上运行的,ESP8266上还能用JavaScript写一个不大不小的项目,最开始也是在ESP8266上玩的。。。目前是单片机只运行字节码VM,开发环境编译JavaScript为字节码文件,在将字节码文件复制到单片机里运行,这样还能节省出语法树AST的内存。