В последние 5 лет мы разрабатываем устройства с уровнем функциональной безопасности SIL3 (в части встроенного ПО). Это накладывает кучу ограничений: например, абсолютный запрет на использование несертифицированных по функциональной безопасности средств — типа Cube, компиляторов GNU и Clang. Только сертифицированные средства, например IAR Workbench Functional Safety.
Соответственно, для того чтобы получить как можно больше баллов по функциональной безопасности, необходимо иметь как можно больше диагностик, в том числе и диагностики CPU. Самим писать диагностику CPU — дело довольно трудоёмкое, поэтому мы используем сертифицированные библиотеки от производителя. Например, ST библиотека для тестирования. Тестов много, и они довольно детальные: тестирование регистров, ALU, FPU, конвейер, стека, ОЗУ, DSP и так далее. Эта диагностика выполняется в фоне в самой низкоприоритетной задаче, и в случае, если во время диагностики обнаружен отказ микроконтроллера, ПО переводит датчик в безопасный режим (например, ток в уровень Аварии 3.6 мА, статус датчика — Malfunction, а детальный статус показывает отказ микроконтроллера). За всю жизнь я наблюдал отказ микроконтроллера ровно один раз — это была ATmega, в ней накрылось ALU, и он реально выдавал откровенную дичь.
Так вот, в нашем новом, довольно сложном проекте, на стадии разработки после каждого спринта мы запускаем системные тесты, которые длятся по несколько дней. И вот после очередного спринта на тестах обнаруживаем: датчик показывает отказ микроконтроллера. Проявлялось это только на одном экземпляре.
Первая мысль — что мы, возможно, что-то сделали не так. Тем более что за год до этого был выпущен другой датчик, где использовался тот же самый микроконтроллер с той же самой библиотекой. Но в дальнейшем такие отказы начали появляться и на других платах.
Мы немного озадачились, но для того чтобы тесты всё-таки можно было провести, занесли баг в систему и отключили диагностику. Тесты не выявили никаких отклонений. Все расчёты выполнялись верно, функционально встроенный софт работал без сюрпризов. Поэтому на время, пока мы добавляли функциональность от спринта к спринту, мы забыли о проблеме.
На одном из заключительных спринтов мы снова включили диагностику и стали прогонять тесты — и… ничего. Всё хорошо, диагностика работает, датчик фурычит, никаких отказов.
Принципиально весь датчик состоит из двух плат: назовём их «центральная плата» и «плата сенсора». Пока мы добавляли функциональность, плата сенсора постоянно изменялась, и к моменту, когда мы обратно врубили диагностику, у нас была уже совершенно другая — в части разводки и электроники — плата сенсора. Поэтому мы просто решили взять старую плату сенсора и снова попробовать прогнать тесты. И вот буквально через час со старой платой сенсора у нас опять появилась ошибка микроконтроллера.
Стоит отметить, что датчики у нас работают от токовой петли 4–20 мА и должны устанавливать ток аварии 3.6 мА. То есть условно, ток потребления всего датчика не должен превышать 3.5 мА на 16 В. Но это среднее потребление: в пике датчик кратковременно мог потреблять и 20, и 30 мА — это необходимо для генерации коротких импульсов для платы сенсора.
И первая мысль, которая возникла: пиковое потребление как-то влияет на диагностику, и происходит что-то типа глитча. Опять подключаем новую плату сенсора, запускаем тесты — всё проходит без проблем. Консультируемся с электронщиками — действительно, они уменьшали потребление платы сенсора, делали специальные изменения. Поэтому наезжаем на электронщиков, мол идите разбирайтесь с потреблением.
И опять откладываем вопрос, так как дальше должны были добавлять функциональность.
Вопрос лежал до тестов на вибрацию. На вибрацию датчик шел с последней версией платы сенсора и не прохошел тест по причине… отказа микроконтроллера.
Ещё больше начинаем верить, что это что-то электронное: мол, на вибрации контакты дребезжат, что-то не контачит, повышается потребление, и происходит глитч. И снова вешаем всех собак на электронщиков.
Но все равно назначаем специально обученного специалиста, который пытается отловить отказ под отладчиком. Сколько ни ждём — и с новой платой, и со старой — отказов нет. Снова делаем вывод, что отладчик даёт дополнительное питание, плюс дополнительный ток течёт через линии SWD, и это может как-то влиять. Идея, что это всё-таки глитч по питанию, не покидает нас.
И тут другая группа, которая испытывает датчик, сообщает тоже об отказе микроконтроллера. Вот в этот момент решаем: нужно разбираться до конца.
Первым делом решаем залогировать, какой из 9 тестов STL-библиотеки вообще падает. Библиотека имеет с десяток тестов, поэтому первоочередно нужно было понять, какой же блок диагностика считает битым.
Сделали лог — просто записывали в NV-память номер теста, который упал. Оказалось, падает блок DSP, другие диагностики работали стабильно. Всегда падал только один и то же тест на DSP.
Настраиваем библиотеку так, чтобы выполнялся только один тест DSP, и решаем разобраться, что же происходит в нём. Поскольку мы подписывали NDA, рассказать в деталях, как работает эта диагностика, я не могу. Но в целом, как и любая другая во всём мире, общая картина примерно такая:
Процедура теста делает слепок системы до теста и после. в конце теста сравнивает — они должны совпадать. Это так называемый критерий целостности теста. Если до и после не совпадают, то либо тест что-то поломал, либо кто-то во время тесиа влез и поменял данные, которые использует тест. Понятно, что на время теста все прерывания запрещаются, и как будто бы никто ничего поменять не может — кроме модулей, которые сами что-то творят без участия ядра, например DMA.
Выполняются все команды DSP-модуля с заведомо определёнными операндами, и в конце сравниваются с эталоном, который посчитал производитель на заведомо исправном модуле. Не совпало — где-то проблема.
Было предположение, что DMA нечаянно мог, например, залезть на стек и попортить данные, которые использует диагностика. Поэтому сразу сделали две вещи:
отключили DMA;
увеличили стек. Поставили на тесты — падает, особенно часто со старой платой сенсора.
Делаем лог на тип ошибки, чтобы понять, какая точно ошибка приключилась. Лог всё время выдаёт, что ошибка именно в несовпадении результатов вычислений. Значит, реально вычисления делаются некорректно.
Дальше подключаем ИИ. ИИ (разные, доступные на тот момент) выдаёт нам ту же версию с глитчем, сбоем тактовой частоты и прямо настаивает на ней. Электронщики исследуют линию питания под микроскопом с развёрткой в наносекунды — ничего.
Убираем ИИ, решаем ещё раз посмотреть код. Комментируем все места, где используются DSP-инструкции — и, о чудо, ошибка пропадает. Заменяем код с DSP-инструкциями на обычный — ошибка не появляется. Правда, расчёты начинают работать в 2.5 раза медленнее, но нам сейчас не важна скорость, важно понять, что происходит.
То есть проблема точно в том, что когда мы используем DSP-инструкции — тест падает, когда не используем — тест не падает. Локализуем все такие места и по порядку начинаем убирать инструкции. Путём нескольких итераций приходим к выводу, что на результаты теста влияют всегда две инструкции: SADD16 и SSUB16. Заменяя их — ошибка пропадает.
И тут возникает мысль: а не влияет ли как-то флаги результатов вычислений на результат теста?
Делаем такой эксперимент: просто после каждого вызова наших DSP-функций очищаем флаг GE — и, о чудо, всё работает, ошибок нет.
А теперь нужно пояснение, что же было.
Что такое GE-бит
GE[3:0] — 4 бита в APSR (биты 19:16), рядом с привычными N/Z/C/V/Q. Но в отличие от них это не флаги ошибки и не флаги переполнения — это «вектор» результатов сравнения, единственный потребитель которого — инструкция SEL. Устанавливают его только 12 инструкций «простого» параллельного семейства:
Знаковые |
Беззнаковые |
С обменом половин |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Всё остальное — обычные ADD/SUB, умножения, Q-семейство, SH/UH-семейство, расширения — GE не трогают. GE «липкий»: раз установлен, живёт сколь угодно долго, переживает прерывания (xPSR стекируется/восстанавливается) и весь не-DSP код.
Как работают инструкции SADD16 / SSUB16
На входе инструкции 32-битный регистр:
- `SADD16` / `SSUB16` — два lane(слова, но оставлю везде в оригинале) по 16 бит: - биты `[31:16]` — High lane (lane 0) - биты `[15:0]` — Low lane (lane 1)
Одна инструкция SADD16 r0, r1 выполняет два сложения одновременно, и ключевое слово — независимо: перенос из Low lane в High lane не переходит, каждая живёт как отдельный int16:
r1 = 0x0003_0005 ; High lane = 3, Low lane = 5 r0 = 0xFFFE_0002 ; High lane = -2, Low lane = 2 │ SADD16 r0, r1: High lane: (-2) + 3 = +1 ─┐ два независимых Low lane: 2 + 5 = +7 ─┘ сложения за 1 такт r0 = 0x0001_0007
GE — это табличка «какой знак получился на каждой lane». Обычные флаги N/Z/C/V — один набор на всю инструкцию. Но когда lane две (или четыре), одного флага N не хватит: у каждой lane свой результат со своим знаком. Поэтому для параллельных инструкций завели GE — по одному «биту знака» на lane:
- `SADD16` / `SSUB16` (две lane): - `GE[3:2] = 0b11`, если результат High lane >= 0 - `GE[1:0] = 0b11`, если результат Low lane >= 0 - (пара всегда либо `11`, либо `00` — биты дублируются) - `SADD8` / `SSUB8` (четыре lane): - `GE[3]`, `GE[2]`, `GE[1]`, `GE[0]` — по одному биту на каждую байтовую lane
Пример с SSUB16
SSUB16 r3, %[Rsub] Отсчёты: r3 = 0x012C_0064 ; отсчёт[n+1] = 300, отсчёт[n] = 100 Вычитаемое: Rsub = 0x00C8_00C8 ; вычитаемое значение = 200 в обеих lane High lane: 300 - 200 = +100 >= 0 → GE[3:2] = 11 Low lane: 100 - 200 = -100 < 0 → GE[1:0] = 00 Итог: GE = 0b1100
Отсчёт [n+1] больше вычитаемого значения, отсчёт [n] — меньше.
Биты GE нужны не для диагностики, а для безветвного условного выбора.
Инструкция SEL Rd, Rn, Rm — единственная, кто читает GE: она собирает результат по lane из двух источников — где GE=1, берёт значение из Rn, где GE=0 — из Rm:
SSUB16 r0, r3, %[Rlimit] ; GE = «lane >= limit?» SEL r0, %[Rlimit], r0 ; где GE=1 → взять limit, где GE=0 → оставить r0
На обычном коде без DSP-инструкций это были бы CMP + ветвление на каждый отсчёт, а здесь — две инструкции без переходов на два отсчёта сразу.
Поскольку с платы сенсора приходит большой массив отсчётов, то при вычислении с использованием SADD16 / SSUB16 флаг GE мог сбрасываться и устанавливаться в зависимости от уровня сигнала сенсора. И после расчёта он ставился в значение, зависящее от последних двух отсчётов. Если хотя бы одно из этих значений было больше, чем вычитаемое, то флаг GE взводился и оставался там, пока не запустится диагностика DSP-блока.
И поскольку в диагностике DSP инструкция SEL читает флаг GE, то мы (условно) берём значение из другой ветки (с другого регистра), и результирующее значение не совпадает с тем эталоном, что STL-библиотека посчитала для GE = 0000.
Вот и вся разгадка. Библиотека STL расчитывала эталонное значение исходя из факта, что GE == 0000, а у нас этот флаг зависел от уровня сигнала платы сенсора
Выход из этой ситуации простой:
либо использовать команды
QADD16,QSUB16, которые не ставят флаг GE;либо сбрасывать флаг после каждой функции, использующей DSP;
либо уже вообще не использовать блок DSP.
Вот так оказалось: и вроде как мы всё делали правильно, и ST тут ни при чём, но в документации на тест DSP про это ни слова, а наш запрос ST не ответила. Как говорится, век живи — век учись.
PS. Электронщики извините, что наезжали на вас. Вы не причём.
Комментарии (7)

r_o_m_k_o_l_a
27.08.2026 20:11Спасибо за разбор, особенно за кусок про то, как ошибка пропадала под отладчиком.
Ваш GE — частный случай очень противной категории: самопроверка молча считает, что стартует с чистого состояния, а состояние ей досталось от предыдущей работы. У меня недавно был тот же по духу случай на несколько этажей выше: обёртка над базой подставляла запрос через JS-замену, где `$$` означает один `$`. Долларовые кавычки схлопывались, запрос не выполнялся, а обёртка отвечала HTTP 200 с телом-ошибкой. Проверка «уже делали это?» читала пустоту и всегда отвечала «нет».
Общее у наших историй в том, что тест проверял свой участок и радовался, а сломан был контракт со слоем, в который никто не заглядывал.
Вопрос по вашему выходу из ситуации: вы в итоге остановились на сбросе GE перед диагностикой или совсем отказались от SIMD в этом месте? Интересно, насколько дорого обошёлся отказ по времени выполнения.

lamerok Автор
27.08.2026 20:11Да, мы совсем отказались от SIMD инструкций в данном случае условно расчет по времени увеличился с 16мс до примерно 40мс, но весь цикл измерения примерно 300мс, поэтому для нас не критично. Зато код стал читабельнее и понятнее. Можно было конечно поменять на Q команды, но к моменту, когда полный разбор произошел уже на С++ функции были переписаны и решили время не тратить.
Под отладчиком она пропадала чисто по случайности, когда включался отладчик, сигнал на плате сенсора чуть чуть становился мьше букавально на какие-то несколько милливольт, но этого хватало, чтбы операция вычитания не высталяла флаг GE.

SIISII
27.08.2026 20:11На обычном коде без DSP-инструкций это были бы
CMP+ ветвление на каждый отсчётСтрого говоря, и обычными средствами можно обойтись без переходов, используя команду IT, но, понятно дело, в специализированных случаях специализированные средства эффективнее -- команды DSP не просто так ввели.

Brazil
27.08.2026 20:11У ST существует официальный ES0647 — X-CUBE-CLASSB self-test library software errata, где описана буквально эта самая ошибка:
APSR register content is incorrectly assumed to have all GE bits cleared during the test.
И ST прямо пишет, что при
GE != 0функция CPU TMCB способна вернутьSTL_FAILEDна полностью исправном процессоре. Это классифицировано как false positive.
Не понимаю зачем так мусолить повествование о примитивном баге про который знает любая заштатная модель. Или на подписке экономите?
lamerok Автор
27.08.2026 20:11Да, описаны примерно те же симптомы, но тест совсем другой, и workaround нам бы не помог, и воозможно эта еррата была выпущена, в том числе по нашему репорту в ST. Проблема в том, что нам ST не ответила
amphasis
Но, почему ни при чём? Eсли это их библиотека для тестирования, и она предполагает, что флаги в регистре будут в состоянии 0000, но сама их в это состояние не переводит, то я бы написал баг-репорт.
Кроме того непонятна необходимость сброса этих флагов в основном коде, а не в коде, который крутится в диагностическом потоке.
lamerok Автор
Да, согласен, надо было ST указать в документации, что эталон был высчитан на GE==0000.
Дело в том, что это библиотека от ST, с конретным хешем, она с таким хешем и сертификат имеет от TUV. Поэтому менять изменять её код нельзя, иначе автоматически считается, что у неё нет сертификата. Ну и самое главное по договору, мы и не могли никак её менять вообще.
Вся такая диагностика крутится у нас в самой низкоприоритетной задаче, поэтому перед тестом в этой задаче нельзя никак, так как есть риск, что высокоприоритеная с расчтетом вклинится в момент после сброса флага GE и запуском функции диагностики.