В последние 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, рассказать в деталях, как работает эта диагностика, я не могу. Но в целом, как и любая другая во всём мире, общая картина примерно такая:

  1. Процедура теста делает слепок системы до теста и после. в конце теста сравнивает — они должны совпадать. Это так называемый критерий целостности теста. Если до и после не совпадают, то либо тест что-то поломал, либо кто-то во время тесиа влез и поменял данные, которые использует тест. Понятно, что на время теста все прерывания запрещаются, и как будто бы никто ничего поменять не может — кроме модулей, которые сами что-то творят без участия ядра, например DMA.

  2. Выполняются все команды 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 инструкций «простого» параллельного семейства:

Знаковые

Беззнаковые

С обменом половин

SADD16

UADD16

SASX

SSUB16

USUB16

SSAX

SADD8

UADD8

UASX

SSUB8

USUB8

USAX

Всё остальное — обычные 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)


  1. amphasis
    27.08.2026 20:11

    и ST тут ни при чём, но в документации на тест DSP про это ни слова

    Но, почему ни при чём? Eсли это их библиотека для тестирования, и она предполагает, что флаги в регистре будут в состоянии 0000, но сама их в это состояние не переводит, то я бы написал баг-репорт.

    Кроме того непонятна необходимость сброса этих флагов в основном коде, а не в коде, который крутится в диагностическом потоке.


    1. lamerok Автор
      27.08.2026 20:11

      Да, согласен, надо было ST указать в документации, что эталон был высчитан на GE==0000.

      Дело в том, что это библиотека от ST, с конретным хешем, она с таким хешем и сертификат имеет от TUV. Поэтому менять изменять её код нельзя, иначе автоматически считается, что у неё нет сертификата. Ну и самое главное по договору, мы и не могли никак её менять вообще.

      Вся такая диагностика крутится у нас в самой низкоприоритетной задаче, поэтому перед тестом в этой задаче нельзя никак, так как есть риск, что высокоприоритеная с расчтетом вклинится в момент после сброса флага GE и запуском функции диагностики.


  1. r_o_m_k_o_l_a
    27.08.2026 20:11

    Спасибо за разбор, особенно за кусок про то, как ошибка пропадала под отладчиком.

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

    Общее у наших историй в том, что тест проверял свой участок и радовался, а сломан был контракт со слоем, в который никто не заглядывал.

    Вопрос по вашему выходу из ситуации: вы в итоге остановились на сбросе GE перед диагностикой или совсем отказались от SIMD в этом месте? Интересно, насколько дорого обошёлся отказ по времени выполнения.


    1. lamerok Автор
      27.08.2026 20:11

      Да, мы совсем отказались от SIMD инструкций в данном случае условно расчет по времени увеличился с 16мс до примерно 40мс, но весь цикл измерения примерно 300мс, поэтому для нас не критично. Зато код стал читабельнее и понятнее. Можно было конечно поменять на Q команды, но к моменту, когда полный разбор произошел уже на С++ функции были переписаны и решили время не тратить.

      Под отладчиком она пропадала чисто по случайности, когда включался отладчик, сигнал на плате сенсора чуть чуть становился мьше букавально на какие-то несколько милливольт, но этого хватало, чтбы операция вычитания не высталяла флаг GE.


  1. SIISII
    27.08.2026 20:11

    На обычном коде без DSP-инструкций это были бы CMP + ветвление на каждый отсчёт

    Строго говоря, и обычными средствами можно обойтись без переходов, используя команду IT, но, понятно дело, в специализированных случаях специализированные средства эффективнее -- команды DSP не просто так ввели.


  1. 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.

    Не понимаю зачем так мусолить повествование о примитивном баге про который знает любая заштатная модель. Или на подписке экономите?


    1. lamerok Автор
      27.08.2026 20:11

      Да, описаны примерно те же симптомы, но тест совсем другой, и workaround нам бы не помог, и воозможно эта еррата была выпущена, в том числе по нашему репорту в ST. Проблема в том, что нам ST не ответила