The serial port, usually referred to as UART, is one of the simplest communication protocols used in electronics systems, employed from old 8-bit microcontrollers to complex 32/64-bit microprocessors and a wide variety of peripherals, such as GPS or mobile modems, among others. Despite being an old technology, its simplicity and widespread use cases require that developers know how to use it in everyday situations.
In the embedded systems scenario, high-level of abstraction operating systems such as Linux, enabled by small-sized yet powerful microprocessors, allow developers to employ high-level of abstraction programming languages in their embedded applications, traditionally dominated by the C programming language.
Node.js is a server-side interpreter of the Javascript language, and it has become somehow popular in the last years. Nevertheless, sometimes developers have some doubts regarding how to access hardware features using such a language, and being so, this article is aimed at people who want to build Node.js applications in Embedded Linux systems, making use of the serial ports available to their solution.
The use-case scenarioThe approach chosen to illustrate the use of the serial port in Node.js employs two computer on modules (CoM) and off-the-shelf carrier boards, running Embedded Linux, in order to exchange data between them. The hardware configuration is: Toradex Apalis I.MX6Q + Apalis Evaluation Board and Colibri I.MX6S + Iris Carrier Board.
The use-case chosen is a draft of a simple command-line game, in which Colibri sends input data to Apalis (keys W, A, S, D and space), and Apalis prints and continuously updates a rectangle (area or matrix) of characters, in which there is an element that can move around the positions of the matrix. It is a very simple idea, described in the picture below.
In order to install Node.js and the Node Serialport, the package manager for the Linux Angstrom distribution – OPKG – and the Node Package Manager – NPM – will be employed. The procedures described in this chapter will be applied to both the Apalis and the Colibri CoMs, and they must be connected to the internet. If you don't have a Linux distribution running yet, please refer to this article in order to flash a pre-built image to your module.
* As of 2016/01/17, the OPKG repositories for the latest beta image from Toradex (v2.7Beta1) were not available yet. You may want to use a stable image while following this article (non-beta).
Once you have access to the Linux terminal (user:root
and password is empty), we can start setting up the system. To install Node.js and NPM:
opkg update
opkg install nodejs nodejs-npm
Since Node Serialport does not have pre-compiled binaries for ARM, NPM will build it from source, so we need some additional packages that are not usually required in a production image:
opkg install packagegroup-core-buildessential python-compiler python-misc python-multiprocessing nodejs nodejs-npm
If your module does not have the latest pre-built image, the installation of some packages may fail. You can either update your image to the latest version, or just try to use the "--force-downgrade
" flag with opkg:
opkg install packagegroup-core-buildessential python-compiler python-misc python-multiprocessing nodejs nodejs-npm --force-downgrade
Next you should be able to install the Node Serialport module. Notice that OPKG installs Node.js v0.12, which is only supported by Node Serialport v4 – this is important when consulting the module documentation:
npm install serialport
Note: when building a definitive image for your project, it is recommended to include Node.js and Node Serialport in your image prior to deploying it to the embedded system. Instead of using package managers, which can be achieved by building an OpenEmbedded/Yocto distribution – you may use the meta-nodejs layer, for instance. Steps to build an image can be found in this article.
Understanding the hardwareThe Apalis i.MX6 has 5 UART interfaces, 4 of them available by default (section 5.9 of the datasheet) and Colibri i.MX6 CoM also has 5 UARTs, but only 3 of them are available without changing the system configuration (section 5.11 of the datasheet). Both of the modules are standard 3.3V TTL. *If you need all of the UARTs, please have a look at this article.
Both the Apalis Evaluation Board (section 3.13.1.2 of the datasheet) and the Iris Carrier Board (section 3.9.1 and 3.9.2 of the datasheet) have converters for the RS-232 standard, but since the Evaluation Board signal is provided through a DB-9 connector and the Iris signal is provided through a pin header, it was chosen not to use the RS-232 standard and instead use directly the 3.3V TTL pins of the modules, thus an adapter cable is not needed.
For the Apalis Evaluation Board, consulting the section 3.16 of the datasheet, the pins 38 (UART3-Tx) and 39 (UART3-Rx) of connector x3 were chosen, while for the Iris Carrier Board, consulting the section 3.9.3 of the datasheet, the pins 31 (UART_C-Rx) and 32 (UART_C-Tx) were chosen.
Pins connection between Apalis Evaluation Board and Iris Carrier Board:
|Apalis <----> Colibri|
|UART3 <----> UART_C|
|38(Tx) <----> 31(Rx)|
|39(Rx) <----> 32(Tx)|
The figures below illustrate the jumpers connection to the pin headers.
Regarding the access of the UART interfaces from Linux, Apalis UART_3 corresponds to /dev/ttymxc3
and Colibri UART_C corresponds to /dev/ttymxc2
. This information and more regarding UART on Linux can be found in this article from the Toradex Developer Center.
In this section first the code from the Colibri module will be described, and then the code from the Apalis module.
Colibri i.MX6
The first step required in order to make the modules talk to each other is to use the Node Serialport module. You can find usage information and reference regarding the Node Serialport here. In order to configure the UART, we need to require the module and create a new port, using the path described in the last section, an arbitrary baud rate of 57600bps and a parser by byte length of 1 byte – meaning our messages will be 1 byte long. Alternatively, an end-of-message separator should be used as parser, such as newline, if we need variable size message.
var SerialPort = require('serialport');
//Using the UART_C from the Colibri i.MX6 module
var port = new SerialPort('/dev/ttymxc2', {
baudRate: 57600,
parser: SerialPort.parsers.byteLength(1)
});
After that we must handle error events and create a simple function that sends data through the serial connection:
// open errors will be emitted as an error event
port.on('error', function(err) {
console.log('Error: ', err.message);
});
function sendCommand(key){
port.write(key, function writeHello(err) {
if (err) console.log('Error on write: ', err.message);
});
}
Then, we must handle the receiving of data. In our project, the only data received that matters is the first connection between modules. Let's use the '0' character for handshake – the Colibri module will wait until it receives a '0' and answer with a '0' also.
port.on('data', function(data){
if(data.toString() == '0'){
console.log('Connection estabilished');
port.write('0', function (err){
if (err) console.log('Error on write: ', err.message);
enableStdin();
});
}
});
After this happens, the user input will be enabled by calling the enableStdin() function. This function initiates the receiving of data from stdin
(terminal) and at every keystroke it sends the data to the Apalis module:
var input = process.stdin;
function enableStdin(){
input.setRawMode(true);
input.resume();
input.setEncoding('UTF-8');
input.on('data', function(key){
//quit on ctrl+c
if(key === '\u0003') process.exit(0);
sendCommand(key);
});
}
You can find the code here.
Apalis i.MX6
The serial port configuration, error handling and send data function will be omitted from this section, since they are the same as the functions for the Colibri. When the UART port opens, it will first try to connect, and also a timer will be set to try to connect every 15 seconds, in case that the first try fails, by sending the '0' character:
port.on('open', function() {
port.write('0', function writeHello(err) {//first try to connect
if (err) {
console.log('Error on write: ', err.message);
}
console.log('First connection try...');
});
});
//If fail to connect in the first try, do it again periodically
var tryHelloHandler = setInterval(function tryHelloUntilAnswered(){
port.write('0', function writeHello(err) {//write hello message
if (err) {
console.log('Error on write: ', err.message);
}
console.log('Retry to connect...');
});
}, 15000);
When the Apalis receives data, it handles it as follows: if '0' the periodic connection try is disabled and the first "rectangle" is printed on screen; if ' ' (space) the "rectangle" is cleared and if other then a function is called to handle the movement of the entity on screen:
port.on('data', function(data){
dataHandler(data);
});
function dataHandler(data, callback){
//if(data == '0') console.log('zero!');
//console.log(typeof data);
switch(data.toString()){
case '0'://connection estabilished
console.log('Connection estabilished!');
clearInterval(tryHelloHandler);
//print first lines
process.stdout.write('--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
break;
case ' '://print clear screen
process.stdout.moveCursor(0,-6)
process.stdout.cursorTo(0);
process.stdout.write('--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
break;
default://move commands
printLine(data, currentPosition, currentChar);
}
}
The movement handling function does the positioning based on receiving 'w', 'a', 's' or 'd' and then updates the screen. I have left some bugs and not implemented functionality for you to improve, but the basics are there:
var currentChar = 'A';
var currentPosition = 10;
function printLine(command, lastPosition, lastChar){
var line = '';
for(var i = 0; i < 20; i++){
if(command == 's' && lastPosition == i){
line += 'V';
currentChar = 'V';
}
else if(command == 'w' && lastPosition == i){
line += 'A';
currentChar = 'A';
}
else if(command == 'a' && (lastPosition - 1) == i){
line += lastChar;
if(currentPosition > 1)
currentPosition--;
}
else if(command == 'd' && (lastPosition + 1) == i){
line += lastChar;
if(currentPosition < 19)
currentPosition++;
}
else line += '-';
}
process.stdout.moveCursor(0,-6)
process.stdout.cursorTo(0);
process.stdout.write('--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n--------------------');
process.stdout.write('\n' + line);
process.stdout.write('\n--------------------');
}
You can find the code here.
Running the applicationIn order to run the application in both Apalis and Colibri:
node main_apalis.js
node main_colibri.js
Notice that if you start the Apalis code before, it will send the connection try and lose it, doing a retry only 15 seconds later. If you start the Colibri code first, there will be no connection delay. Of course you can also change or remove the handshake.
This article went through the process of installing Node.js and the Node Serialport modules in a Toradex pre-built image; identifying the UARTs available for the chosen modules and their correspondence in the Linux environment; and using the Node Serialport in order to connect two CoMs from different families running Linux. The process of installing all the software was the trickiest part, since it was needed to install libraries and compile source-code in the embedded system – although it was also provided information about how to do it in a host system.
In the end, using the serial port in Node.js was proven an easy software solution for those who are interested in using this language in embedded systems, where interfacing with low-level hardware is more often than not a requirement. I hope this article was helpful and see you soon!
Comments