使用 JavaScript 对 Arduino 编程

一、向 Arduino 刷入 Firmata 固件

Firmata 是一种开放的类似于 MIDI 的协议。它可以将微控制器(microcontroller)变成“客户端”,通过串口通信直接被“主机”电脑访问和控制。
通过 Firmata 协议,可以在主机上使用 Ruby、Python、JavaScript 等多种语言与刷入了 Firmata 固件的 arduino 板“交流”。

首先安装 firmata-party 工具:
$ npm install -g firmata-party
安装成功后通过 USB 线将 Arduino 板连接至电脑,运行以下命令:

1
2
3
4
5
6
$  firmata-party uno --debug
found uno on port /dev/tty.usbserial-14110
connected
reset complete.
flashing, please wait...
flash complete.

刷写完毕后,可以通过 Firmata Test Program 进行简单的测试。
Firmata Test

二、使用 JavaScript 编程

可以使用如下命令开始一个新的项目:

1
2
3
$ mkdir test && cd test
$ npm init -y
$ npm install --save firmata

编辑源代码文件(blink_led.js)用来控制 LED 闪烁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 加载依赖库
var Board = require('firmata');

// 连接初始化
Board.requestPort(function(error, port) {
if (error) {
console.log(error);
return;
}

var board = new Board(port.comName);

// 等待连接
board.on("ready",function() {
console.log("Ready!");
var ledOn = true;

// 设置 pin 13 为输出引脚
board.pinMode(13, board.MODES.OUTPUT);

// 使 LED 闪烁
setInterval(function() {
if (ledOn) {
console.log('ON');
board.digitalWrite(13, board.HIGH);
} else {
console.log('OFF');
board.digitalWrite(13, board.LOW);
}
ledOn = !ledOn;
}, 1000);
});
});

运行效果如下:

1
2
3
4
5
6
$ node blink_led2.js
Ready!
ON
OFF
ON
...

代码执行后 arduino 板子上 13 号引脚上接的 LED 开始以 1 秒的间隔闪烁,同时电脑的终端界面不停地输出 ON 和 OFF 表示当前 LED 的开关状态。

Analog Input

Arduino 板子上的 A0 - A5 引脚可以读取模拟输入信号。
示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 加载依赖库
var Board = require('firmata');

// 引脚定义
const LED = 5;
const POT = 0;

// 初始化变量
var ledOn = 0;
var delay = 0;

// map 函数。将初始的数值范围转变成需要的数值范围
function map(x, in_min, in_max, out_min, out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

// 连接初始化
Board.requestPort(function(error, port) {
if (error) {
console.log(error);
return;
}

var board = new Board(port.comName);

// 等待连接
board.on("ready", function() {

// 控制 LED 闪烁的 blink() 函数
function blink() {
board.digitalWrite(LED, ledOn);
ledOn = !ledOn;
setTimeout(blink, delay);
}

// 读取并更新可变电阻提供的模拟输入
board.analogRead(0, function(d) {
delay = map(d, 0, 1023, 400, 1600);
});

blink();
});
});

线路连接图如下:
线路连接图

即通过 Arduino 板 A0 引脚可以读取模拟输入的功能(连接可变电阻的中间引脚),获取可变电阻的取值(0-1023)。
再通过程序中的 map 函数将 0-1023 的取值转变成 400-1600,作为接在 5 号引脚上 LED 的闪烁频率。

程序运行后,调节可变电阻的旋钮,LED 即以不同的时间间隔(0.4s - 1.6s)闪烁。

PWM (Pulse-Width Modulation)

PWM 即脉宽调制,其机制为:通过 MCU 内部的计时器设置引脚的状态,使该引脚在一个周期内的某段时间保持 HIGH 状态,在该周期内的其余时间保持 LOW 状态。
平均下来看,该引脚的输出电压即介于最低和最高输出电压之间(即 0~5 V 之间)。所以一般可以近似地作为模拟电压输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 加载依赖库
var Board = require('firmata');

// 变量定义
const LED = 5;
var brightness = 0;
var fadeAmount = 5;

// 连接初始化
Board.requestPort(function(error, port) {
if (error) {
console.log(error);
return;
}

var board = new Board(port.comName);

// 等待连接
board.on("ready", function() {

// 设置 LED 引脚为 PWM 模式
board.pinMode(LED, board.MODES.PWM);

// 每隔 30 ms 提高或降低 LED 亮度,循环执行
function fadeLed() {
brightness += fadeAmount;
if (brightness ==0 || brightness == 255) {
fadeAmount = -fadeAmount;
}
board.analogWrite(LED, brightness);
setTimeout(fadeLed, 30);
}
fadeLed();
});
});

该程序执行后,5 号引脚上的 LED 灯先是缓慢变亮,达到最亮以后再缓慢变暗。依照此规则循环。

附录:JavaScript 的串口通信

即使 Arduino 板子未刷入 Firmata 固件,运行的是普通的程序。
也可以利用 Node.js 的 serialport 库和串口通信完成 Arduino 与主机的对话。

首先初始化项目并安装依赖库:

1
2
3
$ mkdir test2 && cd test2
$ npm init -y
$ npm install --save-dev serialport

扫描串口设备
1
2
3
4
5
6
7
8
9
10
$ cat list_ports.js
var serialPort = require("serialport");
serialPort.list(function (error, ports) {
ports.forEach(function(port) {
console.log(port.comName);
});
});
$ node list_ports.js
/dev/tty.Bluetooth-Incoming-Port
/dev/tty.usbserial-14110
从 Arduino 收取数据

首先通过 Arduino IDE 刷入以下 counter.ino 文件,作为发送端程序:

1
2
3
4
5
6
7
8
9
void setup() {
Serial.begin(9600);
}

int i=0;
void loop() {
Serial.println(i++);
delay(1000);
}

上述代码以 1000 ms 的间隔生成从 0 开始不断递增的数字并发送至串口。

电脑端的接收程序 read_port.js 代码如下:

1
2
3
4
5
6
const SerialPort = require('serialport')
const Readline = require('@serialport/parser-readline')
const port = new SerialPort('/dev/cu.usbserial-14110')

const parser = port.pipe(new Readline({ delimiter: '\n' }))
parser.on('data', console.log)

运行效果如下:read_port
发送端不断生成新的数字并发送至串口,接收端通过串口接收该数字后将其打印到终端输出。

向 Arduino 发送数据

首先通过 Arduino IDE 刷入以下 receiver.ino 文件,作为接收端程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void setup() {
Serial.begin(9600);
}

void loop() {
int incoming = 0;
if (Serial.available() > 0) {
incoming = Serial.parseInt();

if (Serial.read() == '\n') {
Serial.println(incoming);
}
}
}

发送端的 write_port.js 程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var SerialPort = require('serialport');
var Stream = require('stream');
var modem = 'cu.usbserial-14110';
var ws = new Stream();
ws.writable = true;

ws.write = function(data) {
serialPort.write(data);
};

ws.end = function(buf) {
console.log('bye');
}

var serialPort = new SerialPort('/dev/' + modem);

process.stdin.pipe(ws);

运行效果如下:write_port

电脑端发送程序运行后($ node write_port.js),打开 Arduino IDE 的 Serial Monitor
在命令行输入任何一个数字并按回车进行确认,Arduino 板子接收到该数字后,又将其打印到串口输出(即 Serial Monitor 中显示的内容)

参考资料

Node.js for Embedded Systems: Using Web Technologies to Build Connected Devices 1st Edition
Node SerialPort