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

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

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

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

Давайте разберемся, что написано в этом файле:
Нам нужно будет добавить к этому всему еще и свою секцию. Добавляем в конце файла:
Как видно, мы размещаем на четыре (счет идет от нуля) байта от конца флэша секцию (.checksum), куда положим наш CRC.
Теперь переходим в Options->Linker->Checksum и заполняем поля вот так:

Обратите внимание, что тут нужно написать начало и конец памяти. И если у вас поменяется контроллер, править эти параметры придется уже в двух местах. Конец памяти нужно написать на 4 байта раньше его фактического конца. В эти 4 байта мы и положим наше CRC. Мы ведь не хотим, чтобы CRC считало само себя.
Теперь пишем код. Он совсем простой:
Нажимаем на кнопку «загрузить» и ждем. Ждем долго, потому, что записывается вся флэш (все 128кБайт в данном случае).
Помните опцию «заполнять пустые места»?.. Она нужна, чтобы все байты в диапазоне вычисления контрольной суммы приняли определенные значения, иначе вычисление контрольной суммы теряет смысл. Выходы два:
Вот и все. Целостной вам прошивки!
Ну, а если серьезно, то контроль целостности прошивки нужно иметь чуть в каждом проекте, не обязательно в тех, которые могут сделать что-то плохое.

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

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

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

Давайте разберемся, что написано в этом файле:
// Этот кусок редактируется средой автоматически (кнопка 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 и заполняем поля вот так:

Обратите внимание, что тут нужно написать начало и конец памяти. И если у вас поменяется контроллер, править эти параметры придется уже в двух местах. Конец памяти нужно написать на 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кБайт в данном случае).
Помните опцию «заполнять пустые места»?.. Она нужна, чтобы все байты в диапазоне вычисления контрольной суммы приняли определенные значения, иначе вычисление контрольной суммы теряет смысл. Выходы два:
- Отлаживаться без проверки флэша, а включить ее только для финальной прошивки
- Уменьшить размер используемой памяти до разумных объемов. Этот вариант подойдет только если у вас прошивка очень маленькая.
Вот и все. Целостной вам прошивки!
18 комментариев
Во-первых, при перепрошивке целостность обычно контролирует неперепрошиваемый бутлоадер, вероятность что в нем что-то повредится резко меньше, чем в прошиваемом коде.
Единственное место в процедуре контроля, в котором повреждение критично — инструкция проверки if'a (повреждение во всех остальных местах приведет к правильной ветке if'а). Инструкция сравнения занимает 4 байта (может и 2, лень искать). Пусть прошивка — 64кБайта. Если просто поделить одно на другое, то вероятность пропустить ошибку — 0.006%. Думается мне, что в реальности вероятность будет на порядок больше, но это намного лучше, чем ничего.
Если имеются 3 одинаковых устройства, которые работают одновременно и одинаково, и их выходы подключены ко входам мажоритара, то на выходе мажоритара будет сигнал, совпадающий с сигналами на выходе всех 3-х устройств. Кстати мажоритаров — тоже 3, их входы подключаются параллельно к выходам каждого из предыдущих устройств, а выходы на входы 3-х следующих устройств — каждый к своему. Если одно из устройств выйдет из строя, а два других продолжают функционировать нормально, то на выходе мажоритара всё равно будет правильный сигнал.
Такая схема отлично работает с устройствами без памяти, а также с устройствами памяти в «чистом» виде (триггеры, ОЗУ). А вот при работе со сложными устройствами есть «ложка дёгтя»:
Предположим, что устройство (к примеру микроконтроллер) опрашивает некий сигнал по таймеру и в ответ на него на одном из своих выходов разворачивает некую циклограмму. Предположим, что одно из устройств обнаружило сигнал и начало выдавать циклограмму, а второе устройство обнаружило его только на следующем такте таймера. Скорее всего обнаружение сигнала на следующем такте — вполне допустимая задержка реакции на сигнал — т.е. оба устройства работают нормально. Но сигналы на входах мажоритара — разные! Здесь возможно два решения:
И это. Не будет ли равен CRC от ВСЕЙ прошивки (включая CRC от компилятора!) нулю в случае отсутствия ошибок?
Нет, так было бы, если бы мы XOR прошивки считали, а не CRC32.Как оказалось, будет :)
Кстати, в Вашей программе после выхода из цикла переменная ptr как раз указывает на CRC. Не пробовали вместо:
if (calculated_crc != read_crc)
написать:
if (calculated_crc != *ptr)
Так делать нельзя. Точнее можно, но обратиться к __checksum нужно, иначе компилятор уберет символ, а линкер будет ругаться, что не могу символ найти чтобы CRC Вставить.