Embedded HandbookПриложения
Все разделы › XXVI. От макета к изделию
STM32F103, STM32F407 · уровень средний · 20 мин чтения
Открыть в интерактивном справочнике

201. Тест в производстве и прошивка на линии

Пока плат три, каждую прошивают со стола: воткнул ST-LINK, нажал кнопку, посмотрел глазами. На пятидесятой плате это перестаёт работать, на пятисотой — становится главным узким местом. Производственный тест — не «отладка, только быстрее»: это другой жанр. Никто не ищет ошибку в коде — код проверен до линии (раздел 184). Линия отвечает на один вопрос по каждой плате: собрана ли она правильно — пропаян ли кварц, не перепутан ли номинал, отвечает ли датчик, — и делает это за секунды, скриптом, с вердиктом «годен» или «брак» и строкой в журнале. Этот раздел — про то, как такой пост устроить: стенд, тестовая прошивка, серийный номер, байты конфигурации и журнал выпуска. Инструменты командной строки, на которых всё держится, разобраны в разделе 35б — здесь они собираются в конвейер.

Числа в разделе проверяемы: времена Flash — из таблиц характеристик даташитов STM32F103x8/xB и STM32F405/407 в паках поддержки; синтаксис и частоты STM32_Programmer_CLI 2.23.0 — из его --help и его же базы устройств (сохранены в _verify/ptest201/); адреса OTP и UID — из заголовка stm32f407xx.h и справочных руководств; расчёт времени — calc_time.py; самопроверка и журнал собраны и прогнаны на компьютере (gcc и python в Docker), ядро самопроверки скомпилировано и под Cortex-M3. Против железа ничего не исполнялось и как вывод не цитируется. Сверяет _verify/ptest201_check.py.

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

Оригинал Перевод
production test / end-of-line test тест в производстве, на выходе линии
bed of nails «ложе с иглами»: оснастка с подпружиненными контактами
pogo pin подпружиненный игольчатый контакт
test point / test pad контрольная площадка на плате под иглу или щуп
DUT (device under test) испытуемое устройство
in-circuit test (ICT) внутрисхемный тест: цепи и номиналы до включения
functional test (FCT) функциональный тест: собранное устройство работает
flying probe «летающие щупы»: тест подвижными иглами, без оснастки
golden sample эталонный образец: по нему сверяют сам стенд
go / no-go «годен — брак»: вердикт без промежуточных градаций

201.1 Чем тест на линии отличается от отладки

За столом инженер час крутит одну плату; на линии оператор за минуту должен получить вердикт по очередной. Отсюда всё остальное:

Отладка Тест на линии
вопрос почему не работает правильно ли собрано
кто инженер оператор или автомат
время часы на одну плату секунды—минуты на каждую
инструмент среда, GDB, осциллограф стенд + скрипт, никакого GDB
результат понимание «годен / брак» и строка в журнале

Арифметика времени жёсткая: если линия собирает 200 плат в смену — 8 часов, — на плату приходится 144 секунды такта, и тест обязан укладываться вместе с установкой платы на стенд и снятием. Всё, что не влезает, убирают из теста или распараллеливают на второй стенд.

Классический конвейер контроля двухступенчатый. Внутрисхемный тест (ICT) проверяет плату ещё выключенной: иглами по площадкам меряют сопротивления и ёмкости цепей, ищут короткие замыкания и непропаи — на больших тиражах этим занимается отдельный автомат, на малых первую половину заменяют «летающие щупы» или вовсе визуальный контроль. Функциональный тест (FCT) — то, что разберём здесь: плату включают, прошивают и заставляют саму себя проверить. Стенд при этом тоже надо проверять: раз в смену через него прогоняют эталонный образец — заведомо годную плату; если «эталон» вдруг стал браком, чинят стенд, а не партию.

Шесть шагов выпуска платы: контакт и проверка тока, тестовая прошивка по SWD, самопроверка с вердиктом по UART, серийный номер, боевая прошивка со сверкой, байты конфигурации — и два исхода: годен со строкой в журнале или брак с причиной и числами


201.2 Стенд: ложе с иглами и что вывести на площадки

Разъём на каждую плату ставить дорого, поэтому серийный стенд — это ложе с иглами: оснастка, в которую плата ложится контрольными площадками на подпружиненные контакты (pogo pins). Прижал крышкой — все связи есть; поднял — следующая плата. На малой серии то же самое делают проще: технологический разъём, прижим рукой, даже пайка трёх проводов — ложе окупается на сотнях плат, но набор сигналов от этого не меняется.

Стенд на линии: плата ложится площадками вниз на ложе с иглами, от игл идут ST-LINK по SWDIO, SWCLK и NRST, USB-UART с вердиктом самопроверки и питание стенда с измерением тока, всем управляет компьютер со скриптом и журналом

Какие площадки закладывать ещё при разводке платы (контролепригодность — из тех требований, что вспоминают на этапе испытаний, раздел 79):

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

⚠️ NRST на площадку — обязательно. Камень, у которого прошивка сразу уходит в сон или переназначает выводы SWD, без линии сброса недоступен: нужен приём «подключение под сбросом» (раздел 35б.2). У STM32_Programmer_CLI это режим соединения mode=UR — но держать NRST ему физически нечем, если вывод не разведён на иглу. На линии это не экзотика: после включения защиты и сторожа (201.6) сервисные заходы делаются именно так.


201.3 Прошивка на линии: CLI из скрипта и сколько она стоит секунд

Прошивают на линии те же программы, что в разделе 35б, только зовёт их не человек, а скрипт стенда. Фирменная строка целиком:

STM32_Programmer_CLI -c port=SWD freq=4000 sn=57FF6A06... -q \
    -e all -w app.elf -v -rst -log line.log

Всё по собственной справке программы: freq= — частота SWD в килогерцах (по умолчанию 4000 у ST-LINK/V2 и 8000 у ST-LINK/V3), sn= — серийный номер самого программатора, чтобы стенд с двумя ST-LINK не перепутал платы, -e all — стереть всё, -w — записать, -v — сверить чтением, -rst — сброс, -q убирает индикатор хода, -log пишет подробный протокол в файл. Скрипт смотрит на код возврата команды — как с любой программой (тот же приём, что в конвейере сборки, раздел 110). Открытая альтернатива одной строкой — openocd … -c "program fw.elf verify reset exit" (раздел 35б.3); а когда SWD недоступен вовсе, остаётся заводской загрузчик по UART или USB DFU через BOOT0 (разделы 4.9 и 35б.7).

Теперь арифметика: сколько секунд стоит мегабайт по SWD. Пакет записи одного 32-битного слова в протоколе SWD — 8 бит запроса, разворот шины, 3 бита подтверждения, разворот, 32 бита данных и бит чётности: 46 тактов; чтение — те же 46. На 4 МГц это 11,5 мкс на слово; в мегабайте 262 144 слова — не меньше 3,01 с чистой передачи (на 8 МГц — 1,51 с). Это нижняя граница: холостые такты, ответы «подожди» и накладные расходы USB её только поднимают. Вторая составляющая — сама Flash: у F407 программирование слова занимает типично 16 мкс (максимум 100), у F103 полуслова — 52,5 мкс (40—70). Передача и программирование могут идти внахлёст (данные едут порциями через буфер в ОЗУ), поэтому честная нижняя оценка — большая из этих двух составляющих, а не их сумма. И третья — стирание перед записью:

Составляющая F103CB, 128 КБ F407, 1 МБ
стереть вся Flash: 20—40 мс вся Flash при x32: 8 тип. / 16 макс. с
передать по SWD, 4 МГц не меньше 0,38 с не меньше 3,01 с
записать (сама Flash) 65 536 полуслов × 52,5 мкс = 3,44 с 262 144 слова × 16 мкс = 4,19 с
сверить (чтение назад) ещё ~0,4 с ещё ~3 с
итого, нижняя оценка ≈4 с ≈15 с

Расклад поучительный. У F1 стирание мгновенное, а узкое место — медленная запись полусловами. У F4 наоборот: запись при x32 быстрая, зато стирание всей Flash — 8 секунд типично, и обойти их нельзя: стирание секторами по одному ещё дольше (4 × 0,25 + 0,55 + 7 × 1,0 = 8,55 с по типичным временам), так что -e all — не лень, а оптимизация. Прошивка мегабайта — заметный кусок 144-секундного такта, и это стоит знать до того, как выбрать камень с большой Flash для массового изделия.

🔧 Тестовую прошивку — маленькой. На линии камень прошивается дважды: сначала прошивка-самопроверка (201.4), после её вердикта — боевая. Самопроверка в десятки килобайт заливается за доли секунды и почти ничего не добавляет к такту; тянуть в неё HAL со всем проектом незачем.


201.4 Прошивка-самопроверка: что проверяет и как отдаёт вердикт

Самопроверка — отдельная маленькая прошивка, которая меряет то, что могло быть собрано неправильно, и печатает числа в UART. Ядро — таблица шагов с допусками; из _verify/ptest201/selftest201.c, который собран и прогнан:

struct check {
    const char *name;            /* имя шага — уходит в журнал */
    uint32_t (*fn)(void);        /* измерение */
    uint32_t lo, hi;             /* допуск: годен, если lo <= v <= hi */
};

static const struct check TAB[] = {
    { "VDD_MV", meas_vdd_mv,   3135,    3465    },  /* 3,3 В +-5 % */
    { "HSE_HZ", meas_hse_hz,   7920000, 8080000 },  /* 8 МГц +-1 % */
    { "UART_N", meas_uart_echo, 16,     16      },  /* вернулись все 16 байт */
    { "I2C_ID", meas_i2c_id,    0x68,   0x68    },  /* датчик назвал свой код */
};

Каждый шаг — про сборку, не про код. Питание меряется АЦП по внутреннему опорному источнику VREFINT (раздел 14): просевшие 3,0 В вместо 3,3 — плохая пайка стабилизатора. Кварц проверяется счётом его импульсов таймером от внутреннего RC (раздел 6): непропаянный или треснувший кварц даёт ноль, а не 8 МГц ± 1 %. UART гоняет 16 байт через петлю на стенде — заодно проверены дорожки до площадок. Датчик на шине I2C должен назвать свой код в регистре «кто я» (раздел 17). Сюда же обычно добавляют шаги «глазами»: светодиоды поморгать, оператор нажимает кнопку — но всё, что можно померить числом, лучше мерить числом.

Вердикт печатается построчно, машинно-разборным форматом:

    for (unsigned i = 0; i < sizeof TAB / sizeof TAB[0]; i++) {
        uint32_t v = TAB[i].fn();
        int pass = v >= TAB[i].lo && v <= TAB[i].hi;
        printf("%02u %-6s %lu %s\r\n", i + 1u, TAB[i].name,
               (unsigned long)v, pass ? "OK" : "FAIL");
        if (!pass && !bad)
            bad = (int)(i + 1u);
    }

Логика вердикта — обычный C без регистров, поэтому её можно собрать на компьютере и проверить прогоном (тот же приём, что в разделах 79 и 184). Годная плата в опыте:

SELFTEST v1.2 UID 066DFF3033564E4343125623
01 VDD_MV 3312 OK
02 HSE_HZ 8001234 OK
03 UART_N 16 OK
04 I2C_ID 104 OK
=== PASS

Первой строкой прошивка называет свою версию и 96-битный заводской номер кристалла — он сразу попадает в журнал (201.5). Числа печатаются десятичными (104 — это 0x68), и печатаются всегда, а не только при провале: журнал с числами позволит потом увидеть, что «годные» платы новой партии все у нижней границы допуска. Плата с непропаянным кварцем:

02 HSE_HZ 0 FAIL
=== FAIL 02

Хост-сборка возвращает номер провалившегося шага кодом возврата (в опыте exit=2) — скрипту стенда даже разбирать текст не обязательно. На плате роль кода возврата играют строка === PASS/=== FAIL и светодиод — зелёный или красный, чтобы вердикт видел оператор без экрана. Само ядро крошечное: под Cortex-M3 оно скомпилировалось в 343 байта кода.

⚠️ Самопроверка не должна уехать к заказчику. Это отдельная прошивка, и после её вердикта на плату пишется боевая (201.3) — со сверкой. Если финальный шаг стенда не проверяет, что именно записано, однажды коробка изделий уедет с тестовой прошивкой внутри; страхует графа «версия» в журнале.


201.5 Серийный номер: UID камня или свой в OTP

У каждого кристалла уже есть номер: 96-битный UID, прошитый на заводе (раздел 2а.4). Справочное руководство прямо говорит, что он уникален для любого кристалла в любом контексте и что изменить его нельзя; лежит он по 0x1FFFF7E8 у F1 и по 0x1FFF7A10 у F4, рядом — регистр размера Flash (0x1FFFF7E0 и 0x1FFF7A22). Как ключ прослеживаемости UID идеален: бесплатный, уникальный, неподделываемый. Но как «номер изделия» неудобен: 24 шестнадцатеричных знака, никакой сквозной нумерации — на коробку и в гарантийный талон хочется «№ 000042», а не 066DFF30….

Поэтому на линии делают оба: UID читают и пишут в журнал, а свой короткий номер записывают в кристалл. Куда — зависит от семейства:

🔧 Чтение и запись из CLI — по адресам выше, командами из справки (-r32 <адрес> <размер>, -w32 <адрес> <слова через пробел>, -w8 для байта замка):

STM32_Programmer_CLI -c port=SWD -r32 0x1FFF7A10 12          # UID: три слова
STM32_Programmer_CLI -c port=SWD -w32 0x1FFF7800 0x0000002A  # № 42 в блок 0 OTP
STM32_Programmer_CLI -c port=SWD -w8 0x1FFF7A00 0x00         # замок блока 0

Прошивке номер доступен обычным чтением по тем же адресам — OTP и страница Flash отображены в память (раздел 11.5).

⚠️ Замок OTP необратим — запирать после сверки. Порядок строго: записать номер → прочитать назад и сравнить → только потом 0x00 в замок. Блок, запертый с опечаткой, не исправить никогда — 32 байта потеряны, изделие живёт с «чужим» номером или переезжает в следующий блок (их всего 16).


201.6 Байты конфигурации на выпуске: RDP, сторож, BOR

На отладке байты конфигурации (разделы 11.6 и 35б.7) обычно нетронуты. В изделии — нет: последним шагом линия переводит камень в «боевое» состояние. Имена и значения ниже — из базы устройств самого CubeProgrammer для F405/407 (файл 0x413) и F1 medium-density (0x410):

Байт В бою Зачем
RDP уровень 1: F4 — любое значение, кроме 0xAA и 0xCC (например 0xBB); F1 — любое, кроме 0xA5 отладчик не прочитает прошивку и калибровки; снятие — только с полным стиранием
WDG_SW 0 — аппаратный сторож IWDG (раздел 12.2) стартует сам с включения питания: зависшее изделие перезапустится, даже если прошивка сторож включить забыла
BOR_LEV (только F4) порог по питанию платы; для 3,3 В — 0 (уровень 3: сброс ниже 2,70—3,60 В) просадка питания даёт чистый сброс, а не работу на недопустимом напряжении; у F1 порог фиксированный, настраивать нечего
nRST_STOP, nRST_STDBY 1, если изделие использует сон (раздел 10) иначе первый же вход в Stop/Standby — сброс
STM32_Programmer_CLI -c port=SWD -ob displ      # показать все байты
STM32_Programmer_CLI -c port=SWD -ob WDG_SW=0   # сторож — аппаратный
STM32_Programmer_CLI -c port=SWD -ob BOR_LEV=0  # сброс ниже 2,70—3,60 В
STM32_Programmer_CLI -c port=SWD -ob RDP=0xBB   # защита чтения — последней

RDP ставится последней командой последнего шага: после уровня 1 отладчик Flash больше не читает, так что ни сверить прошивку, ни дочитать OTP уже нельзя. Для сервиса уровень 1 обратим: -rdu переводит защиту в уровень 0, по справке CLI — «shifting the RDP level from level 1 to level 0», и это всегда полное стирание Flash (раздел 11.6).

⚠️ Уровень 2 (RDP=0xCC) — решение без кнопки «назад». Отладка и заводской загрузчик отключаются навсегда; перепрошить возврат из ремонта будет нечем, кроме как через собственный загрузчик изделия (раздел 48), если он есть и жив. На линию уровень 2 ставят осознанно и не в первый месяц выпуска.


201.7 Журнал выпуска: строка на плату — навсегда

Вердикт без журнала забыт через минуту. Журнал выпуска — простой файл (или таблица в базе), куда стенд дописывает по строке на каждую плату — и годную, и брак. Из скрипта стенда journal201.py, прогнанного в опыте:

row = ";".join([f"{serial:06d}", ts, station, uid, "1.4.0", crc] + meas + [verdict])
with open(path, "a", encoding="utf-8") as f:
    f.write(row + "\n")
000042;2026-09-24T10:41:07;ST-01;066DFF3033564E4343125623;1.4.0;96D91CA4;3312;8001234;16;104;PASS

Что в строке: серийный номер, время, номер стенда, UID кристалла, версия боевой прошивки и её CRC32, все измеренные числа и вердикт. Следующий серийник скрипт берёт из самого журнала — последний плюс один: журнал и есть единственный источник нумерации. Хранить это дёшево: строка в опыте — 98 байт, сто тысяч плат — меньше 10 МБ; заводить «ротацию логов» для журнала выпуска не надо, он пишется один раз и хранится всю жизнь изделия.

Зачем числа, а не только «годен»: по журналу видно то, чего не видно на одной плате. Партия, у которой VDD_MV сползла с 3312 к 3200, — это меняющийся стабилизатор у поставщика; рост брака по шагу 02 после смены катушки кварцев — разговор с закупкой, а не с программистом. И когда изделие вернётся из поля через год, строка журнала по его серийнику расскажет, какой прошивкой, каким стендом и с какими числами оно уходило. Возвраты и разбор отказов — часть поддержки (раздел 79.8); журнал — то, что делает их разбираемыми.


201.8 Грабли

На столе плата прошивается, на стенде — нет. Между ST-LINK и камнем теперь иглы, провода ложа и наводки соседнего оборудования: частота SWD, на которой отлаживались за столом, на стенде даёт ошибки подключения. Лечится freq=1000 (или ниже) в строке соединения и проверкой прижима игл; симптом «первые платы шли, потом посыпались ошибки» — стёршиеся или загрязнённые иглы, они расходный материал.

Коробка изделий уехала с тестовой прошивкой. Камень прошивается дважды, и если стенд после самопроверки не сверяет записанную боевую прошивку (-v) и не пишет её версию в журнал, подмена шага незаметна. Финальная проверка — прочитать версию/CRC из камня, а не поверить скрипту на слово.

RDP поставили раньше сверки. После уровня 1 отладчик Flash не читает: -v, чтение OTP и дочитывание журнала невозможны, а снятие защиты стирает камень целиком. Байты защиты — строго последний шаг, после всех проверок.

Замок OTP записали до сверки номера. 0x00 в байте замка необратим: блок с опечаткой остаётся навсегда. Сначала записать, прочитать назад, сравнить — потом запирать (201.5).

Два стенда выдали один серийник. Каждый стенд вёл свой счётчик — номера разъехались при первом же параллельном запуске. Источник номера должен быть один: общий журнал или диапазоны по стендам (ST-01 — с 1, ST-02 — с 500 000), и номер стенда — в строке журнала.

Аппаратный сторож перезапускает самопроверку. Если WDG_SW=0 записан до теста (или пришёл с предыдущей попытки), IWDG стартует с включения, и самопроверка, которая его не гладит, сбрасывается на середине — «плата перезагружается, брак», хотя плата годная. Байты конфигурации — после самопроверки; а сервисному стенду помнить: у выпущенных плат сторож уже аппаратный, подключаться — под сбросом (mode=UR, 201.2).


201.9 Итоги

Полные исходники

selftest201.c — прошивка-самопроверка: шаги, допуски, вердикт.

/* selftest201.c — тестовая прошивка-самопроверка, раздел 201.4.
 *
 * Ядро одно и то же для платы и для компьютера: таблица шагов, допуски,
 * вердикт. На плате meas_* читают ADC, таймер и периферию, а printf
 * уходит в UART; на компьютере (-DHOST) те же шаги получают числа из
 * сценария — логика вердикта проверяется прогоном без железа, как
 * в разделе 184.
 */
#include <stdint.h>
#include <stdio.h>

uint32_t meas_vdd_mv(void);      /* питание: ADC по внутреннему опорному */
uint32_t meas_hse_hz(void);      /* кварц: таймер считает HSE против HSI */
uint32_t meas_uart_echo(void);   /* UART: 16 байт в петлю стенда и обратно */
uint32_t meas_i2c_id(void);      /* датчик платы: регистр «кто я» по I2C */
const uint32_t *uid(void);       /* 96 бит заводского номера (раздел 2а.4) */

struct check {
    const char *name;            /* имя шага — уходит в журнал */
    uint32_t (*fn)(void);        /* измерение */
    uint32_t lo, hi;             /* допуск: годен, если lo <= v <= hi */
};

static const struct check TAB[] = {
    { "VDD_MV", meas_vdd_mv,   3135,    3465    },  /* 3,3 В +-5 % */
    { "HSE_HZ", meas_hse_hz,   7920000, 8080000 },  /* 8 МГц +-1 % */
    { "UART_N", meas_uart_echo, 16,     16      },  /* вернулись все 16 байт */
    { "I2C_ID", meas_i2c_id,    0x68,   0x68    },  /* датчик назвал свой код */
};

int selftest_run(const char *ver)
{
    const uint32_t *u = uid();
    int bad = 0;
    printf("SELFTEST %s UID %08lX%08lX%08lX\r\n", ver,
           (unsigned long)u[2], (unsigned long)u[1], (unsigned long)u[0]);
    for (unsigned i = 0; i < sizeof TAB / sizeof TAB[0]; i++) {
        uint32_t v = TAB[i].fn();
        int pass = v >= TAB[i].lo && v <= TAB[i].hi;
        printf("%02u %-6s %lu %s\r\n", i + 1u, TAB[i].name,
               (unsigned long)v, pass ? "OK" : "FAIL");
        if (!pass && !bad)
            bad = (int)(i + 1u);
    }
    if (bad)
        printf("=== FAIL %02d\r\n", bad);
    else
        printf("=== PASS\r\n");
    return bad;    /* на плате по вердикту зажигается зелёный или красный */
}

#ifdef HOST
/* Сценарии: 0 — все измерения в норме; 1 — кварц молчит, таймер
 * насчитал ноль. Числа «в норме» — обычные для платы с 3,3 В и 8 МГц. */
static const uint32_t S[2][4] = {
    { 3312, 8001234, 16, 0x68 },
    { 3308, 0,       16, 0x68 },
};
static int sc;

uint32_t meas_vdd_mv(void)    { return S[sc][0]; }
uint32_t meas_hse_hz(void)    { return S[sc][1]; }
uint32_t meas_uart_echo(void) { return S[sc][2]; }
uint32_t meas_i2c_id(void)    { return S[sc][3]; }

static const uint32_t UID_HOST[3] = { 0x43125623u, 0x33564E43u, 0x066DFF30u };
const uint32_t *uid(void) { return UID_HOST; }

int main(int argc, char **argv)
{
    sc = argc > 1 && argv[1][0] == '1';
    return selftest_run("v1.2");    /* код возврата читает скрипт стенда */
}
#endif

journal201.py — скрипт стенда: журнал выпуска, строка на плату.

# -*- coding: utf-8 -*-
"""Раздел 201.7: журнал выпуска — одна строка на плату.

Скрипт стенда: запускает самопроверку (в опыте — её HOST-сборку),
разбирает вывод и дописывает строку в journal.csv. Серийный номер —
следующий за последним в журнале; CRC32 считается от исходника
самопроверки (на линии — от прошитого образа)."""
import os
import re
import subprocess
import sys
import zlib

exe, sc, ts, station = sys.argv[1:5]

out = subprocess.run([exe, sc], capture_output=True, text=True).stdout
uid = re.search(r"UID ([0-9A-F]{24})", out).group(1)
meas = re.findall(r"^\d\d \S+\s+(\d+) ", out, re.M)
m = re.search(r"=== FAIL (\d+)", out)
verdict = ("FAIL " + m.group(1)) if m else "PASS"
crc = "%08X" % (zlib.crc32(open("selftest201.c", "rb").read()) & 0xFFFFFFFF)

path = "journal.csv"
serial = 42                      # первый номер партии
if os.path.exists(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            if line.strip():
                serial = int(line.split(";")[0]) + 1

row = ";".join([f"{serial:06d}", ts, station, uid, "1.4.0", crc] + meas + [verdict])
with open(path, "a", encoding="utf-8") as f:
    f.write(row + "\n")
print(row)
print("bytes=%d" % (len(row.encode("utf-8")) + 1))

Дальше: раздел 201а «Выпуск прошивки: версия, метаданные и один файл на производство» — откуда берутся версия и CRC, которые стенд пишет в журнал, и как собрать один файл на линию.

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