CRC прошивки средствами IAR и STM32

Который час идет обновление вашей прошивки в устройстве управления реактором. Данные передаются из далека по спаренной телефонной линии, в которую сумасшедшая бабка орет о том, что ядерный реактор плохо влияет на ее кошку. Один неправильно записанный байт и город станет зоной отчуждения. Как предотвратить трагедию? Конечно, контроль целостности прошивки :)

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

схема вычислителя CRC32

К счастью, современные компиляторы имеют встроенные средства генерации контрольных сумм, а контроллеры имеют модули вычисления контрольных сумм, осталось только подружить их друг с другом.

Первое, что нам нужно сделать — отказаться от стандартного скрипта для линкера и подключить свой. Для этого, указываем свое устройство, потом переходим на вкладку Linker и копируем icf файл из того места которое там указано в папку со своим проектом.
$TOOLKIT_DIR$ — это папка установки IAR'а.

дефолтный файл линкера

Естественно, файл можно переименовать, а путь к проекту заменить на $PROJ_DIR$, так проект станет переносимее. Должно получиться вот так:

Новый скрпит линкера

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

icf - файл в проекте

Давайте разберемся, что написано в этом файле:


// Этот кусок редактируется средой автоматически (кнопка edit в настройках линкера)

/*###ICF### Section handled by ICF editor, don't touch! ****/
/*-Editor annotation file-*/
/* IcfEditorFile="$TOOLKIT_DIR$\config\ide\IcfEditor\cortex_v1_0.xml" */
/*-Specials-*/
define symbol __ICFEDIT_intvec_start__ = 0x08000000;                    // где начинаются вектора прерываний
/*-Memory Regions-*/
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000;                // начало флэша
define symbol __ICFEDIT_region_ROM_end__   = 0x0801FFFF;                // конец флэша
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;                // начало оперативки
define symbol __ICFEDIT_region_RAM_end__   = 0x20001FFF;                // конец оперативки
/*-Sizes-*/
define symbol __ICFEDIT_size_cstack__ = 0x800;                          // размер стэка
define symbol __ICFEDIT_size_heap__   = 0x800;                          // размер кучи
/**** End of ICF editor section. ###ICF###*/

define memory mem with size = 4G;                                       // определяем 4Гб адресное пространство
                                                                        // и выделяем в нем два региона - ROM_region
                                                                        // и RAM_region
define region ROM_region   = mem:[from __ICFEDIT_region_ROM_start__   to __ICFEDIT_region_ROM_end__];
define region RAM_region   = mem:[from __ICFEDIT_region_RAM_start__   to __ICFEDIT_region_RAM_end__];

                                                                        // создаем два блока - стек и кучу
define block CSTACK    with alignment = 8, size = __ICFEDIT_size_cstack__   { };
define block HEAP      with alignment = 8, size = __ICFEDIT_size_heap__     { };

initialize by copy { readwrite };                                       // инициализируем все переменные (readwrite)
                                                                        // копированием.
                                                                        // При этом, линкер создает две секции, к примеру,
                                                                        // .data_init и .data, при старте данные копируются
                                                                        // из .data_init в .data.

do not initialize  { section .noinit };                                 // все, что попадает в секцию .noinit не инициализировать

                                                                        // тут мы размещаем наши вектора прерываний
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };

place in ROM_region   { readonly };                                     // размещаем все, что только для чтения (readonly)
                                                                        // в ROM_region

place in RAM_region   { readwrite,                                      // размещаем все, что для чтения и для записи в RAM_region
                        block CSTACK, block HEAP };                     // кроме того, кладем туда два блока - CSTACK и HEAP.



Нам нужно будет добавить к этому всему еще и свою секцию. Добавляем в конце файла:
place at address mem:__ICFEDIT_region_ROM_end__-3 { readonly section .checksum };


Как видно, мы размещаем на четыре (счет идет от нуля) байта от конца флэша секцию (.checksum), куда положим наш CRC.

Теперь переходим в Options->Linker->Checksum и заполняем поля вот так:

настройки CRC IAR

Обратите внимание, что тут нужно написать начало и конец памяти. И если у вас поменяется контроллер, править эти параметры придется уже в двух местах. Конец памяти нужно написать на 4 байта раньше его фактического конца. В эти 4 байта мы и положим наше CRC. Мы ведь не хотим, чтобы CRC считало само себя.

Теперь пишем код. Он совсем простой:

#include "stm32f10x.h"

extern "C" uint32_t __checksum;                                 // импортируем контрольную сумму
#pragma section=".intvec"                                       // говорим, что будем использовать
#pragma section=".checksum"                                     // информацию о секциях

void main()
{
  uint32_t read_crc = __checksum;                               // в __checksum линкер любезно положит
                                                                // контрольную сумму. Его нужно
                                                                // обязательно прочесть, иначе будет ошибка
  
  uint32_t* begin = (uint32_t*)__section_begin(".intvec");      // получаем начало прошивки
  uint32_t* end = (uint32_t*)__section_begin(".checksum");      // получаем начало CRC (конец прошивки)
  uint32_t* ptr = begin;
  
  RCC->AHBENR  |= RCC_AHBENR_CRCEN;                             // включаем такирование модуля CRC

  CRC->CR |= CRC_CR_RESET;                                      // Сбрасываем модуль. После сброса 
                                                                // модуль инициализцирется начальным значением
                                                                // а каждая запись в CRC->DR добавляет записанное 
                                                                // в CRC. Результат можно прочесть из CRC->DR
  do {
    CRC->DR = *ptr;                                             // добавляем всю прошивку в CRC
  } while(++ptr != end);
    
  uint32_t calculated_crc = CRC->DR; 
  
  RCC->AHBENR  &= ~RCC_AHBENR_CRCEN;                            // отключаем тактирование, экономим энергию,
                                                                // делаем планету зеленее
  
  if (calculated_crc != read_crc)
  {
    // плохо :(
  }
  else
  {
    // хорошо!
  }
}


Нажимаем на кнопку «загрузить» и ждем. Ждем долго, потому, что записывается вся флэш (все 128кБайт в данном случае).

Помните опцию «заполнять пустые места»?.. Она нужна, чтобы все байты в диапазоне вычисления контрольной суммы приняли определенные значения, иначе вычисление контрольной суммы теряет смысл. Выходы два:

  1. Отлаживаться без проверки флэша, а включить ее только для финальной прошивки
  2. Уменьшить размер используемой памяти до разумных объемов. Этот вариант подойдет только если у вас прошивка очень маленькая.

Вот и все. Целостной вам прошивки!

18 комментариев

avatar
Что будет, если повредится процедура контроля прошивки?
avatar
Зависит от того, как повредится и в каком месте эта процедура находится :)

Во-первых, при перепрошивке целостность обычно контролирует неперепрошиваемый бутлоадер, вероятность что в нем что-то повредится резко меньше, чем в прошиваемом коде.

Единственное место в процедуре контроля, в котором повреждение критично — инструкция проверки if'a (повреждение во всех остальных местах приведет к правильной ветке if'а). Инструкция сравнения занимает 4 байта (может и 2, лень искать). Пусть прошивка — 64кБайта. Если просто поделить одно на другое, то вероятность пропустить ошибку — 0.006%. Думается мне, что в реальности вероятность будет на порядок больше, но это намного лучше, чем ничего.
avatar
Если говорить об атомных станциях, то обычно постулируется, что система надёжно (ровно 100%) диагностирует один отказ (или два, три…). В данном случае это не так. Так что перед включением сервопривода вывода стержней из активной зоны стоит проверить сумму хотя бы ещё раз =)
avatar
Со станцией это я загнул :) Насколько мне известно. там используют тройное дублирование с мега-надежным аппаратным мажорирующим устройством.
avatar
Мне как-то довелось поработать не с АЭС, но с космосом. Тройное дублирование тоже используют. А мега-надёжное аппаратное мажорирующее устройство состоит из 5 транзисторов. :)
avatar
Кстати, интересно, как оно работает. Если есть опыт, расскажите (можно, отдельной статьей).
avatar
Оно реализует логическую функцию A(B+C)+BC последовательно-параллельным соединением, типа (+) — пять транзисторов — нагрузка — (-). В общем и всё. Вообще, несмотря на огромную теоретическую базу, в основном только тройное дублирование с мажоритированием и используют. Никакой магии. Хотя, раз есть интерес к надёжным системам могу статейку по основным принципам накидать, жаль я оттуда ушёл, не смогу показать на готовых рабочих примерах.
avatar
Было бы интересно :)
avatar
Постараюся объяснить более просто. Мажоритар — это логическое устройство с нечётным числом входов и одним выходом. На выходе присутствует уровень, который имеется на большинстве входов. Обычно используются мажоритары с 3-мя входами, но я где-то слышал, что на корабле «Apollo» в каких-то приборах использовались мажоритары с 5-ю входами (не проверено).
Если имеются 3 одинаковых устройства, которые работают одновременно и одинаково, и их выходы подключены ко входам мажоритара, то на выходе мажоритара будет сигнал, совпадающий с сигналами на выходе всех 3-х устройств. Кстати мажоритаров — тоже 3, их входы подключаются параллельно к выходам каждого из предыдущих устройств, а выходы на входы 3-х следующих устройств — каждый к своему. Если одно из устройств выйдет из строя, а два других продолжают функционировать нормально, то на выходе мажоритара всё равно будет правильный сигнал.
Такая схема отлично работает с устройствами без памяти, а также с устройствами памяти в «чистом» виде (триггеры, ОЗУ). А вот при работе со сложными устройствами есть «ложка дёгтя»:
Предположим, что устройство (к примеру микроконтроллер) опрашивает некий сигнал по таймеру и в ответ на него на одном из своих выходов разворачивает некую циклограмму. Предположим, что одно из устройств обнаружило сигнал и начало выдавать циклограмму, а второе устройство обнаружило его только на следующем такте таймера. Скорее всего обнаружение сигнала на следующем такте — вполне допустимая задержка реакции на сигнал — т.е. оба устройства работают нормально. Но сигналы на входах мажоритара — разные! Здесь возможно два решения:
  1. разбить устройство на более мелкие: «устройство обнаружения сигнала» — «мажоритар» — «устройство реакции на сигнал» — «мажоритар», в этом случае реакция на сигнал последует только после того, как его обнаружат не менее 2-х устройств (а третье может и не обнаружит, но всё равно отреагирует);
  2. отказаться от мажоритирования для микроконтроллеров и использовать обычное переключение на резервный микроконтроллер. Работоспособность микроконтроллера контролировать с помощью сторожевого таймера, но не того, что встроен в микроконтроллер, а внешнего — троированного с мажоритарами.
avatar
Мы ведь не хотим, чтобы CRC считало само себя.
Компилятор умрёт от рекурсии?

И это. Не будет ли равен CRC от ВСЕЙ прошивки (включая CRC от компилятора!) нулю в случае отсутствия ошибок?
avatar
Компилятор умрёт от рекурсии?
Нет, компилятор просто посчитает лишний байт (который там был до вставки CRC), а потом просто CRC не сойдется.

Не будет ли равен CRC от ВСЕЙ прошивки (включая CRC от компилятора!) нулю в случае отсутствия ошибок?
Нет, так было бы, если бы мы XOR прошивки считали, а не CRC32.
Как оказалось, будет :)
avatar
И это. Не будет ли равен CRC от ВСЕЙ прошивки (включая CRC от компилятора!) нулю в случае отсутствия ошибок?
Да, будет. Таково свойство CRC. По сути CRC — это остаток от деления нашей прошивки, как одного много-многобитного числа, на полином (а по сути на число, которое мы видим на вкладке «Options->Linker->Checksum» справа от поля «Algorithm:»). Правда деление используется не обычное. Используется «полиномиальная арифметика по модулю 2», а именно — при обычном делении «в столбик» операция вычитания заменяется на XOR. Дописывая CRC в конец прошивки мы добавляем к числу остаток от деления и в результате получаем число, которое дилится на полином нацело — остаток равен 0.
avatar
Нет, не будет, специально проверил :)
avatar
Значит что-то тут не так… Должно получиться. Но в чём дело я пока не понял. В своей программе для STM32 считал CRC практически так-же, но включая само значение CRC. Правда компилировал GCC, а не IAR. И CRC считал внешней программой в уже откомпилированом .hex файле и только для используемой области Flash памяти. CRC помещал сразу после контролируемой области памяти, порядок байт: младший байт по младшему адресу.
Кстати, в Вашей программе после выхода из цикла переменная ptr как раз указывает на CRC. Не пробовали вместо:
if (calculated_crc != read_crc)
написать:
if (calculated_crc != *ptr)
avatar
Хех, извиняюсь :) Я проверял не считалкой из stm, а программкой, в которой перепутал порядок байт при добавлении crc. Действительно, получится ноль.

if (calculated_crc != *ptr)
Так делать нельзя. Точнее можно, но обратиться к __checksum нужно, иначе компилятор уберет символ, а линкер будет ругаться, что не могу символ найти чтобы CRC Вставить.
avatar
Спасибо, жаль что не появилась статья на пол-годика раньше. Небольшое замечание, эти настройки Checksum для IAR версий 6.40 и новее, а для версий 6.10-6.30 необходимо еще поставить флажок в поле «Reverse byte order within word».
avatar
Ребят, где можно почитать этот пресловутый *.icf файл? Документации что-то не нашел
avatar
IAR->Help->C/C++ Development Guide
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.