Embedded HandbookПриложения
Все разделы › XXIII. Приложение к прибору: Dart и Flutter
STM32F103, STM32F407, STM32G431, STM32L431 · уровень средний · 6 мин чтения
Открыть в интерактивном справочнике

185. Dart для инженера прошивки: байты, числа, разбор кадра

Прибор почти всегда обрастает приложением: посмотреть показания, настроить, обновить прошивку. Часть XXIII — про эту вторую половину работы. Начинаем не с кнопок, а с того, что инженеру прошивки в новом языке нужно в первую очередь: байты и числа.

Пример сквозной: тот самый кадр 0x55 — длина — данные — CRC-8, который разбирала прошивка в разделе 184. Теперь его разбирает приложение, и обе стороны обязаны считать одинаково — это проверено, а не обещано.

Как это называется в документации

Оригинал Перевод
Uint8List, ByteData массив байт, доступ к байтам с указанием порядка
endianness, Endian.little порядок байт, младший байт первым
sealed class закрытая иерархия: все наследники известны
pattern matching, switch expression сопоставление по образцу, switch-выражение
record запись: кортеж с именованными полями
null safety, late, ?. ?? защита от null
package, pubspec пакет и его описание
unit test, dart test модульные тесты

185.1 Числа: где Dart не похож на C

Родной int в Dart — знаковое 64-разрядное целое, и переполняется оно по кругу, как на микроконтроллере:

Что считаем Значение
0x7FFFFFFFFFFFFFFF 9223372036854775807
то же плюс 1 −9223372036854775808

Но у Dart есть вторая цель — браузер, и при сборке в JavaScript int — это double: целые точны только до 2⁵³ (при сборке в WebAssembly int снова 64-разрядный, но протокол рассчитывают на худший случай). Отметка времени прибора в наносекундах это ломает:

Что считаем Значение
2⁵³ + 1 == 2⁵³ (в double) true
отметка времени, нс 1758013245123456789
она же после double 1758013245123456768
потеряно 21 нс

Насколько грубо округляется — видно по шагу между соседними значениями double: на миллисекундах от 1970 года шаг 0,00024 — целые точны; на микросекундах шаг 0,25; на наносекундах уже 256, то есть отметка времени прыгает через четверть микросекунды.

Целое в double: до 2⁵³ точно, дальше — с округлением

Вывод для протокола: не гнать в приложение 64-разрядные счётчики и идентификаторы как числа. Миллисекунды — можно (2⁵³ мс это 285 тысяч лет), наносекунды и серийные номера — только строкой или парой 32-разрядных слов; для точной арифметики есть BigInt.

Беззнаковых типов в Dart нет вовсе, маску ставим руками:

Выражение Значение
-1 & 0xFF 255
(0x80 << 1) & 0xFF 0
-1 >> 56 −1
-1 >>> 56 255

>> тянет знак, >>> — не тянет. Это ровно та разница, которая в C задаётся типом операнда, а здесь — выбором оператора.

Сдвиги из таблицы верны только в машинном коде. На вебе, при сборке в JavaScript, побитовые операции идут над 32 битами, и результат у них беззнаковый: та же программа там печатает -1 >> 56 = 4294967295, -1 >>> 56 = 0, ~0 и -1 | 0 = 4294967295, 1 << 32 = 0, а старший байт 40-разрядного (0x123456789A >> 32) & 0xFF — 0 вместо 18. Маски из двух верхних строк дают то же, что в машинном коде. Поля длиннее 32 бит на вебе собирают из двух половин или считают в BigInt.


185.2 Байты и порядок байт

Массив байт — Uint8List, чтение полей — ByteData. Порядок байт указывают всегда: без второго аргумента ByteData читает старшим байтом вперёд (Endian.big), наоборот по сравнению с ARM, и никакой ошибки не будет:

final ByteData view = ByteData.sublistView(frame);
final int ms = view.getUint32(0, Endian.little);
final double volts = view.getInt16(4, Endian.little) / 1000.0;

Те же четыре байта E8 03 00 00 дают:

Порядок Значение
Endian.little 1000
Endian.big 3892510720

Отсюда правило: порядок байт — часть описания протокола, а не мелочь реализации. В прошивке ARM он little-endian по умолчанию, в приложении его пишут в коде, и на этом стыке ошибаются чаще всего.

Кадр прибора: те же байты в прошивке и в приложении


185.3 Разбор кадра: sealed-классы вместо кодов возврата

В C разборщик возвращал 1 / 0 / −1 (раздел 184.2). В Dart на это есть закрытая иерархия: компилятор сам следит, что разобраны все случаи.

sealed class FrameEvent {}
class FrameNone extends FrameEvent {}
class FrameReady extends FrameEvent { FrameReady(this.data); final Uint8List data; }
class FrameError extends FrameEvent { FrameError(this.reason); final String reason; }

Разбор события — switch-выражение с образцами:

final String what = switch (event) {
  FrameReady(data: final d) => 'кадр ${d.length} байт',
  FrameError(reason: final r) => 'ошибка: $r',
  FrameNone() => 'ждём',
};

Забыть случай нельзя: sealed заставляет перечислить всех наследников, иначе программа не соберётся. Для протокола, где «ошибка» — обычное состояние, это заметно надёжнее, чем if (r == -1).

Само измерение удобно вернуть записью — кортежем с именами:

typedef Measurement = ({int ms, double volts, double amps});

Не нужен ни класс, ни конструктор: поля по имени, сравнение по значению.


185.4 Один алгоритм на двух языках — и доказательство

CRC-8 в приложении и в прошивке обязаны совпадать. Проверяется это не глазами: одна и та же программа считает 256 наборов байт на C (из _verify/test184/frame.c) и на Dart, и результаты сверяются целиком.

Что сверяли Значение
наборов байт 256
совпало значений CRC 256
расхождений 0

Приём общий: любой алгоритм, который есть по обе стороны провода, проверяется перекрёстным прогоном, а не чтением кода. Для CRC, для перевода кодов АЦП в вольты, для формата времени.


185.5 Тесты: то же, что в 184, только короче

dart test работает с пакетом test, который добавляют в описание пакета одной командой dart pub add dev:test; отдельная сборка не нужна:

test('CRC-8 совпадает с прошивкой', () {
  expect(crc8(<int>[0x31, 0x32, 0x33]), 0xC0);
  expect(crc8(<int>[]), 0x00);
});

Число 0xC0 — то самое, независимое, из раздела 184.5: оно ловит испорченный полином, а расчёт «тем же кодом» — нет.

Набор из шести проверок покрывает всё, что мы тут сделали: CRC, целый кадр, испорченную CRC, длину вне предела, порядок байт и сопоставление по образцу. Прогон занимает доли секунды и идёт на любой машине без платы.


185.6 Грабли

64-разрядный идентификатор пришёл числом. На вебе он молча округлится: наносекундная отметка теряет 21 нс уже на первом приведении. Строка или два 32-разрядных слова.

Порядок байт «как получится». Без аргумента ByteData берёт Endian.big: те же четыре байта дают 3 892 510 720 вместо 1000.

Знак вместо нуля при сдвиге. -1 >> 56 это −1, а не 255. Для байт из провода нужен >>> или маска.

int в Dart — не uint8_t. После << маску ставим сами, иначе значение уедет за пределы байта и разойдётся с прошивкой.

Коды возврата вместо типов. sealed + switch не дают забыть случай; if (r == -1) даёт.

Алгоритм переписан «по памяти». Пока нет перекрёстного прогона, совпадение CRC — только надежда.


185.7 Итоги

Дальше: раздел 186 «Асинхронность: поток кадров, таймауты, переподключение, изолят».

Открыть в интерактивном справочнике