Yacht Devices Новости: NMEA 2000 Bridge: новые функции и дизассемблер

09.09.2019

Лучшее понимание вашего кода позволяет разрабатывать сложные приложения обработки CAN в реальном времени с использованием встроенного языка программирования.

Дизассемблер NMEA 2000 Bridge

NMEA 2000 Bridge — единственный продукт со встроенным языком программирования для обработки сообщений CAN. Вы можете загрузить текст программы с карты MicroSD и получить журнал ошибок выполнения программы или компиляции в выходных файлах. Это позволяет редактировать и настраивать ваши программы прямо на борту; вам нужно только устройство с кард-ридером MicroSD и текстовым редактором.

С помощью Bridge можно «удалить» неправильные данные или отфильтровать ненужные сообщения с помощью 2-3 строк кода, или изменить сообщения CAN на лету (см. пример 1) или запускать гораздо более сложные программы (пример тригонометрии, пример цифровой коммутации).

Bridge также защищает ваше программное обеспечение от несанкционированного доступа или преодоления. Несмотря на то, что он имеет специальную поддержку NMEA 2000 (например, предварительную сборку быстрых сообщений), его можно использовать с различными сетями CAN (например, J1939), работающими на скоростях от 50 Кбит/с до 1 Мбит/с. Поддерживаются как 11-битные, так и 29-битные идентификаторы CAN.

Код пользователя компилируется в байт-код, а затем выполняется на виртуальной машине. Обычно вам не нужно знать «ассемблер», но многим пользователям нравится знать, что скрывается под капотом. Просто добавьте DECOMPILER=1 в свою программу (программа ниже ничего не делает, это просто пример):

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
}

И вы получите YDNBSAVE.CFG, где каждая строка вашего кода сопровождается дизассемблированным байт-кодом:

# 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 

}

Команда PUSH помещает число на вершину стека. Первый байт после PUSH — это тип числа (int8 — 0, unsigned int8 — 1, int16 — 2, unsigned int16 — 3, int32 — 4, unsigned int32 — 5, плавающее — 6). POPV (да, с суффиксом «V») берет число из вершины стека и сохраняет его в указанную переменную (A — 0, B — 1 и т. д.). Команда PUSHV сохраняет переменную в стек.

GREAT сравнивает два верхних числа стека и устанавливает флаг виртуальной машины. JMPF выполняет короткий переход вперёд на заданное число байтов, если флаг не установлен. JMP выполняет безусловный короткий переход вперёд; JMP 0 создаёт бесконечный цикл.

Аналогичные команды JMPFL и JMPL (длинные переходы) принимают двухбайтовый аргумент со знаком и могут выполнять переходы вперед и назад (используются в операторе while()).

Наша виртуальная машина очень проста, и мы уверены, что вы сможете легко читать другой «ассемблерный» текст. Никаких других регистров, кроме флагов, у него нет, а все операции выполняются с данными на вершине стека.

Размер одной подпрограммы (внутри init(), heartbeat(), match() или func()) ограничен 16 КБ. Размер всей программы также ограничен 16 КБ. Другими словами, у вас может быть один большой match(), который использует весь доступный ROM. С помощью дизассемблера вы узнаете размер вашего байт-кода, 4-значное шестнадцатеричное число перед командой в дизассемблированном тексте является адресом команды внутри подпрограммы.

Если вы уже знакомы с программированием Bridge, вас могут удивить ключевые слова call() и func(). func() — это новое ключевое слово, в которое вы можете поместить некоторую подпрограмму и вызывать ее с помощью call().

Рекурсия (call() внутри func()) также разрешена, но глубина ограничена 10. Если ведение журнала включено (см. папку «max-recursion»), вы можете получить следующую строку в файле:

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

Функция while() также может создавать мертвый цикл, и мы ограничили время выполнения подпрограммы 3 секундами (см. папку «max-execution»). Большинство программ Bridge содержат всего несколько строк кода и выполняются менее чем за полмиллисекунды. И обычно вам не нужно беспокоиться о времени выполнения.

MCU Bridge работает на частоте 20 МГц. Мы можем подсчитать, что производительность виртуальной машины (heartbeat() в этом примере будет вызываться Bridge каждые 1000 мс):

# 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 

}

В журнале мы видим, что выполнение виртуальной машиной 9006 команд (9*1000+6) занимает 102 мс (0x66) или около 10 микросекунд на команду:

    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

Наименьший интервал сообщений NMEA 2000 в Стандарте (например, сообщения датчика ветра) составляет 100 миллисекунд и даже большой обработчик со 100 командами виртуальной машины или 30 строками исходного кода будет иметь время обработки около 1 мс.

Конечно, тригонометрические функции могут иметь большее время выполнения. Мы рассчитали время выполнения обработчика сообщения ветра из этого примера. Он имеет 85 строк кода (или 55 строк чистого кода без комментариев и пустых строк), которые скомпилированы в 492 байта или 205 команд виртуальной машины.

Первоначальный результат составил 8 мс, но большую часть времени ушло на логирование. Когда вызывается функция send() (два раза в этом обработчике) и включен текстовый журнал, Bridge сохраняет копию отправленного сообщения в журнал на карте MicroSD. И, конечно же, это время имеет значение! Когда мы прокомментировали вызовы send(), время выполнения сократилось всего до 3 миллисекунд. В среднем это около 15 микросекунд на команду, но мы используем sqrt(), acos() и вычисления с плавающей запятой.

Чтобы узнать больше о производительности Bridge и оптимизации вашего кода, см. Глава IX Руководства.

обновление прошивки 1.32 для NMEA 2000 Bridge доступен на загрузок .