September 9, 2019  New functions and disassembler in the Bridge

Better understanding of your code allows development of real-time and complex CAN processing applications using a built-in programming language.

Disassembler of the NMEA 2000 Bridge

The NMEA 2000 Bridge is the only product with a built-in programming language to process CAN messages. You can load the program's text from a MicroSD card, and get a log of the program execution or compilation errors in the output files. This allows editing and tuning of your programs while on board; you only need a device with a MicroSD card reader and text editor.

With the Bridge, it is possible to "remove" wrong data or filter out unnecessary messages with 2-3 lines of code, or change CAN messages on the fly (see example 1), or run much more complicated programs (trigonometry example, digital switching example).

The Bridge also protects your software from unauthorized access or coping. Even though it has special support for NMEA 2000 (like pre-assembling of fast messages), it can be used with different CAN networks (for example, J1939) running on speeds from 50 kbps to 1 Mbps. Both 11-bit and 29-bit CAN identifiers are supported.

The user's code is compiled to byte code and then executed in a virtual machine. Usually you do not need to know "assembler", but many users like to know what is under the hood. Just add DECOMPILER=1 to your program (the program below does nothing, it's just an example):

DECOMPILER=1

init()
{
	A = 1
	B = 2
	if (A > 0)
	{
		B = B+A*3
	}
	else
	{
		B = 0
	}
	while (B > 0)
	{
		call()
	}
}

func()
{
	B = B-1
}

And you will get YDNBSAVE.CFG where every line of your code is accompanied by disassembled byte code:

# Current configuration of Yacht Devices NMEA 2000 Bridge
# Firmware: 1.32 27/08/2019                 
# Serial: 00000000, Password level: 0

PGNS_TO_ASSEMBLY=
FW_CAN1_TO_CAN2=ON
FW_CAN2_TO_CAN1=ON
CAN1_HARDWARE_FILTER_1=0x00000000,0x00000000
CAN2_HARDWARE_FILTER_1=0x00000000,0x00000000
CAN2_SPEED=250

init()
{
    # 0000:  PUSH 04 01 00 00 00 
    a=1
    # 0006:  POPV 00 
    # 0008:  PUSH 04 02 00 00 00 
    b=2
    # 000E:  POPV 01 
    # 0010: PUSHV 00 
    # 0012:  PUSH 04 00 00 00 00 
    # 0018: GREAT 
    if (a > 0) {
        # 0019:  JMPF 12 
        # 001B: PUSHV 01 
        # 001D: PUSHV 00 
        # 001F:  PUSH 04 03 00 00 00 
        # 0025:   MUL 
        # 0026:   ADD 
        b=(b + (a * 3))
        # 0027:  POPV 01 
        # 0029:   JMP 0A 
    }
    else {
        # 002B:  PUSH 04 00 00 00 00 
        b=0
        # 0031:  POPV 01 
    }
    # 0033:   NOP 
    # 0034: PUSHV 01 
    # 0036:  PUSH 04 00 00 00 00 
    # 003C: GREAT 
    while (b > 0) {
        # 003D:  JMPF 06 
        call()
        # 003F:  CALL 
        # 0040:  JMPL F4 FF 
    }

}
func()
{
    # 0000: PUSHV 01 
    # 0002:  PUSH 04 01 00 00 00 
    # 0008:   SUB 
    b=(b - 1)
    # 0009:  POPV 01 

}

The PUSH command puts the number to the top of the stack. The first byte after the PUSH is the type of number (int8 — 0, unsigned int8 — 1, int16 — 2, unsigned int16 — 3, int32 — 4, unsigned int32 — 5, float — 6). The POPV (yes, with "V" suffix) takes the number from the top of the stack and saves it to a specified variable (A — 0, B — 1, and so on). The PUSHV command saves the variable to the stack.

The GREAT command compares two numbers on the top of the stack and sets the flag in the "flag register" of virtual machine. JMPF (jump false) makes forward short jump (a specified number of bytes) if the flag is not set. JMP performs an unconditional forward short jump (JMP 0 is the dead loop).

The similar commands JMPFL and JMPL (long jumps) take a two-byte signed argument, and can perform forward and backward jumps (used in the while() statement).

Our virtual machine is very simple, and we are sure you will be able to read other "assembler" text easily. It has no other registers except flags, and all operations are performed with data on the top of the stack.

The size of one subprogram (inside init(), heartbeat(), match() or func()) is limited to 16 KB. The whole program size is also limited to 16 KB. In other words, you can have one big match() which consumes all available ROM. With disassembler, you find out the size of your bytecode, the 4-digit hexadecimal number before the command in disassembled text is the address of the command inside the subprogram.

If you already familiar with Bridge programming, you may be surprised by call() and func() keywords. The func() is a new keyword where you can place some subprogram and call it with the call().

The recursion ( call() inside func() ) is also allowed, but the depth is limited to 10. If logging is turned on (see "max-recursion" folder), you may get the following line in the file:

    02:57.345 !! FUNC Maximum recursion depth (10) is reached

The while() can also make a dead loop, and we limited the time of subprogram execution to 3 seconds (see "max-execution" folder). Most of the Bridge's programs have only few lines of code and are executed in less than half of a millisecond. And usually you do not have to worry about execution time.

The Bridge's MCU runs at 20 MHz. We can calculate that the performance of the virtual machine (the heartbeat() in this example will be called by the Bridge every 1000 ms):

# Current configuration of Yacht Devices NMEA 2000 Bridge
# Firmware: 1.32 27/08/2019                 
# Serial: 00000000, Password level: 0

PGNS_TO_ASSEMBLY=
FW_CAN1_TO_CAN2=ON
FW_CAN2_TO_CAN1=ON
CAN1_HARDWARE_FILTER_1=0x00000000,0x00000000
CAN2_HARDWARE_FILTER_1=0x00000000,0x00000000
CAN2_SPEED=250

heartbeat(1000)
{
    # 0000: TIMER 
    t=timer()
    # 0001:  POPV 13 
    # 0003:  PUSH 04 00 00 00 00 
    b=0
    # 0009:  POPV 01 
    # 000B: PUSHV 01 
    # 000D:  PUSH 04 E8 03 00 00 
    # 0013:  LESS 
    while (b < 1000) {
        # 0014:  JMPF 10 
        # 0016: PUSHV 01 
        # 0018:  PUSH 04 01 00 00 00 
        # 001E:   ADD 
        b=(b + 1)
        # 001F:  POPV 01 
        # 0021:  JMPL EA FF 
    }
    # 0024: PUSHV 13 
    # 0026: TDIFF 
    log(timediff(t))
    # 0027:   LOG 

}

In the log we find that execution of 9006 commands by the virtual machine (9*1000+6) takes 102 ms (0x66) or about 10 microseconds per command:

    00:02.112 LG HEARTBEAT, UINT32 0x00000066
    00:03.222 LG HEARTBEAT, UINT32 0x00000066
    00:04.325 LG HEARTBEAT, UINT32 0x00000066
    00:05.428 LG HEARTBEAT, UINT32 0x00000066

The smallest interval of NMEA 2000 messages in the Standard (for example, wind sensor messages) is 100 milliseconds and even a big handler with 100 commands of virtual machine or 30 lines of source code will have a processing time of about 1 ms.

Of course, trigonometry functions may have a bigger execution time. We calculated wind message handler execution time from this example. It has 85 lines of code (or 55 lines of clear code without comments and empty lines) which are compiled to 492 bytes or 205 commands of virtual machine.

The initial result was 8ms, but most of time was consumed by logging. When the send() function is called (two times in this handler) and the text log is on, the Bridge saves a copy of the sent message to the log on MicroSD card. And, of course, this time counts! When we commented the send() calls, the execution time shrank to only 3 milliseconds. This is about 15 microseconds per command on average, but we are using sqrt(), acos() and floating point math.

To learn more about performance of the Bridge and optimization of your code, see Chapter IX of the Manual.

The firmware update 1.32 for the NMEA 2000 Bridge is available on the Downloads page.

 

Next articles:

See also: recent news, all news...