FLM - это проект для Keil, который среда загружает в МК по интерфейсу Jtag или SWD. Файл FLM должен реализовать несколько функций, которые будут вызываться при прошивке и которым будут передаваться данные для записи в память. FLM может быть различное множество - для внутренней Flash, для записи во внешнюю память ОЗУ/ПЗУ по внешней шине или SPI, и т.д. То есть это просто проект, который принимает данные извне и знает что и как с ними делать, предварительно настроив необходимую периферию.
В отличие от официальных версий, FLM из текущего проекта будут поддерживать стирание памяти по секторам. Что позволит использовать их совместно с утилитой J-Flash - "Программирование контроллеров с помощью J-FLASH Lite".
Текущая реализация создает следующие FLM:
Для сборки FLM потребуется лицензионная версия Keil. Демо-версия на 32Кб не умеет собирать проект FLM, эта возможность отключена.
Целиком проект доступен на GitHub, далее шаги по его созданию на примере версии Keil 5.23:
Я завел себе директорию FLM, в которую буду собирать все необходимое для сборки. В этой директории такие папки:
В директорию src из исходников SPL копируем описанные выше файлы. Пак с SPL обычно устанавливается в C:\Keil_v5\ARM\PACK\Keil\MDR1986BExx\1.4\Libraries.
Файлы описания МК (MDR1986VEхх.h, …) потребуются для использования структур и адресов конкретного МК. Заголовочный файл MDR32F9Qx_rst_clk.h необходим для использования определений битов и констант для задания тактирования от HSI. Полагаться на внешние источники нельзя, поскольку неизвестно, чем тактируется МК в изделии (резонатор, генератор и при каких параметрах). Из файлов MDR32F9Qx_eeprom.c и MDR32F9Qx_eeprom.h мы используем штатные, готовые функции работы с внутренней памятью Flash через контроллер EEPROM. В указанных файлах я закомментировал все, что связывало эти файлы с другими источниками.
В директорию src добавлен файл flmClock.c, в него я вытащил реализацию перехода на частоту HSI из библиотечного MDR32F9Qx_rst_clk.с.
Функции, которые необходимо реализовать в FLM файле описаны здесь (про функции подробнее дальше). Для того, чтобы отладить их реализацию, я создаю отдельный тестовый проект для каждого МК, реализую указанные в ссылке варианты вызовов функций, проверяю на МК вживую и отлаживаю. Эти же реализации функций затем используются в проектах, которые собирают сами FLM. При таком подходе, получая FLM, я уверен, что функции с памятью работают корректно.
Для примера рассмотрим создание FLM для МК 1986ВЕ92У. Поэтому в директории test я создаю поддиректорию FLM_Test_VE92 - здесь будет проект для тестирования функций FLM. (Раскладка памяти и периферии в серии 1986ВЕ9х одинакова, поэтому директорию можно было бы назвать FLM_Test_VE9х. Ведь какой-либо привязки конкретно к 1986ВЕ92 проект не имеет. Просто для отладки использовалась плата для 1986ВЕ92У.)
В директории FLM_Build в одном workspace будут собраны все проекты, собирающие FLM. Создание FLM рассмотрим на примере проекта для микроконтроллеров 1986ВЕ9х, поэтому создаем директорию VE9x_FLM_FlashInt. В нее сейчас и будем создавать проект.
Шаблон проекта для создания FLM находится в директории с готовыми алгоритмами - C:\Keil_v5\ARM\Flash. Из этой директории нам понадобятся:
FlashOS.h копируем в src - этот файл общий для всех и необходим Keil при сборке.
Содержимое _Template копируем в FLM_Build\VE9x_FLM_FlashInt. Копируемые проектные файлы имеют название NewDevice, предлагаю обозвать их как-то более осмысленно - меняем названия на VE9x_FLM_FlashInt, расширения файлов сохраняем.
Теперь запускаем переименованный проект VE9x_FLM_FlashInt.uvproj и можно заняться кодированием. При открытии проекта, Keil предложит установить "Legacy Support for ARM7, ARM9 & Cortex-R". Этого не требуется, нажимаем Cancel. У меня уже установлен Legacy support for ARM Cortex-M devices, как рекомендовалось в статье - Установка Keil и SPL Milandr. Если поддержка Cortex-M не установлена, то необходимо ее установить.
В проекте используется всего два исходных файла - FlashDev.c и FlashPrg.c. Дополнительно к этим файлам необходимо подключить исходники из директории scr:
В закладке настроек проекта "С++" указываем путь к папке src (поле Include Paths: ..\..\src). В настройках проекта обязательно необходимо выбрать ядро процессора - закладка Device:
Файл FlashDev.c очень короткий и всего-навсего описывает раскладку Flash памяти и название алгоритма. Изначально файл уже содержит значения по умолчанию, нам необходимо всего-лишь внести правки под наш МК. Раскладка памяти и периферии во всех МК 1986ВЕ9х одинакова, поэтому и FLM им необходим один.
Основные параметры взяты из спецификации. Таймаут для операций я указал в 5 секунд, надеюсь что этого достаточно. Параметры, указанные в этом файле, будут использоваться Keil при стирании и записи страниц памяти.
#include "FlashOS.H" // FlashOS Structures struct FlashDevice const FlashDevice = { FLASH_DRV_VERS, // Driver Version, do not modify! "StartMilandr 1986VE9x FlashInt", // Название, показывается при выборе алгоритма в FlashDownload. ONCHIP, // Device Type - Внутренняя Flash 0x08000000, // Адрес начала Flash памяти 0x00020000, // Количество памяти в байтах (128kB) 0x1000, // Размер порции данных, которыми будет производиться запись // Подойдет размер одной страницы Flash памяти 0, // Reserved, must be 0 0xFF, // Значение стертого байта памяти. 5000, // Таймаут 5 сек на запись страницы 5000, // Таймаут 5 сек на стирание сектора // Размер и относительный адрес начала страниц 0x00001000, 0x00000000, // Sector Size 4kB (32 Pages) SECTOR_END };
Для МК 1986Е1Т и 1986ВЕ3Т раскладка памяти также одинакова, как и само ядро Cortex-M1. Поэтому для этих МК собирается тоже один FLM на двоих. Помимо раскладки памяти следует учитывать вопросы тактирования, но эти МК переводятся на тактирование от HSI одинаково, поэтому проблемы в использовании одного FLM на двоих нет.
#include "FlashOS.H" // FlashOS Structures struct FlashDevice const FlashDevice = { FLASH_DRV_VERS, // Driver Version, do not modify! "StartMilandr 1986VE1/VE3 FlashInt", // Device Name ONCHIP, // Device Type 0x00000000, // Device Start Address 0x00020000, // Device Size in Bytes (128kB) 0x1000, // Programming Page Size 0, // Reserved, must be 0 0xFF, // Initial Content of Erased Memory 5000, // Program Page Timeout 5 Sec 5000, // Erase Sector Timeout 5 Sec // Specify Size and Address of Sectors 0x00001000, 0x00000000, // Sector Size 4kB (32 Pages) SECTOR_END };
Отдельно остановлюсь на последней строке. В начальном варианте проекта я неправильно понял назначение этих параметров. Исправиться помог Владимир из техподдержки Миландр, за что ему большое спасибо!
// Specify Size and Address of Sectors 0x00001000, 0x00000000, // Sector Size 4kB (32 Pages)
Здесь есть небольшая путаница. В терминах FLM, сектор - это часть памяти (адресного пространства от и до), которая может быть стерта за раз. В спецификации на микроконтроллер это соответствует странице памяти. Внутренняя память МК состоит из 32 страниц, каждую из которых можно стереть по отдельности. Если смотреть далее по спецификации, то каждая страница в свою очередь состоит из 4 секторов. Но эти сектора в памяти страницы - совсем не то, что подразумевает FLM. Итого, параметры означают следующее:
Например, если зашиваемая программа занимает порядка 3 секторов, то будет вызвано стирание трех страниц (на примере 1986ВЕ9х):
EraseSector(0x08000000); EraseSector(0x08000000 + 1 * 0x00001000); EraseSector(0x08000000 + 2 * 0x00001000);
Остальные страницы не будут стираться чтобы сэкономить ресурс flash памяти, который составляет порядка 10-20 тысяч циклов перезаписи. Причем Keil, в отличие от J-Flash, поступает еще хитрее. Keil сначала проверяет страницу на стертость, и если страница стерта, то он пропускает стирание этой страницы и переходит к следующей. J-Flash же честно вызывает EraseSector() для каждой страницы, вне зависимости от того пустая она или нет.
Необходимость с указанием размера и относительного адреса связана с тем, что память может быть устроена по-разному. Например, в этом примере от ARM, представлена память, где размер страниц в адресном пространстве разный:
struct FlashDevice const FlashDevice = { ... 0x00000000, // Device Start Address 0x00040000, // Device Size in Bytes (256kB) ... // Specify Size and Address of Sectors 0x002000, 0x000000, // Sector Size 8kB (8 Sectors) 0x010000, 0x010000, // Sector Size 64kB (2 Sectors) 0x002000, 0x030000, // Sector Size 8kB (8 Sectors) SECTOR_END };
В этом примере показана непрерывная область памяти с адресами от 0x00000000 до 0x00040000, где
На самом деле, средние большие страницы по 64КБ могут таковыми и не быть. Важно чтобы функция EraseSector() при обращении к ней с адресами 0х10000 и 0х20000 стирала по 64КБ.
Файл FlashPrg.c должен реализовать функции для работы FLM. Поскольку эти же функции мы будем использовать в тестовом проекте, то файл FlashPrg.c переносим в директорию src. И так как функции работы с EEPROM из файла MDR32F9Qx_eeprom.c работают со всеми МК, то файл FlashPrg.c потребуется всего один на все проекты.
(Удаляем файл из проекта, переносим файл FlashPrg.c в папку src, подключаем файл к проекту из нового пути. В закладке настроек проекта С++ указываем путь к папке src.)
Чтобы не перегружать статью, не буду приводить весь код функций. Остановлюсь только на особенностях.
Если вернуться к описанию вызовов функций FLM при прошивке (link) то видно, что любая работа с памятью начинается с вызова функции Init() и заканчивается вызовом UnInit().
/* * Parameter: adr: Device Base Address * clk: Clock Frequency (Hz) * fnc: Function Code (1 - Erase, 2 - Program, 3 - Verify) * Return Value: 0 - OK, 1 - Failed */ int Init (unsigned long adr, unsigned long clk, unsigned long fnc) { // Запрет всех прерываний __disable_irq(); // Включение HSI ClocksDeinit_HSI(); // Clock from HSI MDR_RST_CLK->CPU_CLOCK = 0; // EEPROM Clock On if ((fnc == 1) || (fnc == 2)) MDR_RST_CLK->PER_CLOCK |= RST_CLK_PCLK_EEPROM; return (0); }
В коде данной функции необходимо учитывать, что в момент начала работы прошивальщика, МК может быть настроен произвольным образом. То есть работать на какой-то неизвестной частоте, с периферией и прерываниями. Следовательно необходимо все это отключить.
(В первых версиях кода я этого не делал, поэтому прошивка завершалась успешно только со второго раза. Как предполагаю, с первого раза портилась текущая программа, а со второго раза не происходил запуск испорченной программы, МК оставался работать в настройках по сбросу, и тогда FLM отрабатывал полностью правильно.)
Из входных параметров я использую только fnc, для того чтобы включить тактирование блока EEPROM, если будут операции стирания и записи. При верификации я читаю память напрямую, без блока EEPROM. Но если FLM будет модифицироваться для работы с информационной памятью или будет выставлена опция в коде READ_BY_EEPROM, то тактирование на RST_CLK_PCLK_EEPROM должно подаваться и в операциях верификации.
Переход на частоту HSI производится функцией ClocksDeinit_HSI(), которая реализована в src/flmClock.c.
/* * De-Initialize Flash Programming Functions * Parameter: fnc: Function Code (1 - Erase, 2 - Program, 3 - Verify) * Return Value: 0 - OK, 1 - Failed */ int UnInit (unsigned long fnc) { volatile uint32_t d; MDR_EEPROM->CMD = 0; MDR_EEPROM->KEY = 0; MDR_RST_CLK->PER_CLOCK = 0x00000010; // Update page cache d =(*((volatile uint32_t *) PAGE1_ANY_ADDR)); // Update page cache d =(*((volatile uint32_t *) PAGE2_ANY_ADDR)); return (0); }
Функция UnInit() выключает тактирование с блока EEPROM, предварительно остановив работу блока через регистры. Исполнение алгоритма может сбиться или зависнуть. Если таймаут операции закончится, то прошивальщик Keil вызовет процедуру UnInit() в процессе работы для того, чтобы остановить исполнение. (В коде других производителей можно увидеть применение сторожевых таймеров, чтобы предотвратить возможные сбои при работе FLM. Например, применение FLM не к месту "убьет" тактирование ядра, тогда Jtag отвалится, и никакие UnInit() уже не помогут - спасет сторожевой таймер.)
В конце кода UnInit() идет обращение в произвольным ячейкам в разных страницах памяти. Дело в том, что, например, в 1986ВЕ1Т, ускоритель Flash памяти после операции записи не обновляет свое содержимое из обновленной памяти и может вернуть предыдущее значение. Доступ к произвольным ячейкам должен заставить ускоритель обновить данные. По крайней мере, такой код был добавлен в официальный FLM для 1986ВЕ1Т - 1986BE1_rev2.FLM.
Используя стандартные функции SPL по работе с EEPROM, реализовать следующие функции FLM не составляет труда.
// Выбор основной или инфо памяти #ifdef SELECT_INFO_MEM #define BANK_SELECTOR EEPROM_Info_Bank_Select #else #define BANK_SELECTOR EEPROM_Main_Bank_Select #endif /* * Parameters: adr: Page Start Address * sz: Page Size * buf: Page Data * Return Value: 0 - OK, 1 - Failed */ // Стирание всей памяти int EraseChip (void) { EEPROM_EraseAllPages(BANK_SELECTOR); return (0); } // Стирание Страницы int EraseSector (unsigned long adr) { EEPROM_ErasePage(adr, BANK_SELECTOR); return (0); } // Программирование слова, байтам требуется Swap. int ProgramPage (unsigned long adr, unsigned long sz, unsigned char *buf) { uint32_t i; for (i = 0; i < sz; i += 4) EEPROM_ProgramWord(adr + i, BANK_SELECTOR, buf[i+3] <<24 | buf[i+2]<<16 | buf[i+1]<<8 | buf[i]); return (0); }
Для реализации следующих двух функций я добавил функцию чтения слова ReadWord(). В этой функции выбирается чтение напрямую или через контроллер EEPROM. В функцию BlankCheck() передается параметр pat, это значение стертого байта из файла FlashDev.c. В текущем коде я им не пользовался, поскольку в микроконтроллерах Миландр стертое слово всегда равно 0xFFFFFFFF.
// Чтение слова напрямую или через контроллер EEPROM uint32_t ReadWord(uint32_t Address) { #ifndef READ_BY_EEPROM return *((volatile uint32_t *)(Address)); #else return EEPROM_ReadWord(Address, EEPROM_Info_Bank_Select); #endif } // Проверка что память очистилась int BlankCheck (unsigned long adr, unsigned long sz, unsigned char pat) { uint32_t i; volatile uint32_t value; for (i = 0; i < sz; i +=4) { value = ReadWord(adr + i); #ifdef FIX_DBL_READ if (0xFFFFFFFF != value) value = ReadWord(adr + i); #endif if (0xFFFFFFFF != value) return 1; } return 0; } // Верификация записанной памяти, по словам unsigned long Verify (unsigned long adr, unsigned long sz, unsigned char *buf) { uint32_t i; //uint32_t addrI; uint32_t* pData32 = (uint32_t*)buf; volatile uint32_t value; // Loop by Int sz = sz >> 2; for (i = 0; i < sz; ++i) { value = ReadWord(adr + (i << 2)); #ifdef FIX_DBL_READ if (pData32[i] != value) value = ReadWord(adr + (i << 2)); #endif if (pData32[i] != value) break; } return adr + (i << 2); }
При отладке функций обнаружилось, что изредка чтение некоторых ячеек дает неправильный результат. Обычно это происходит в 1986ВЕ1Т после стирания всего чипа. Не вполне понял, с чем это связано, виноват ускоритель Flash или что-то иное, но добавление двойного считывания в случае сбоя помогло при прогоне тестов. Сбои прекратились, поэтому опция с двойным чтении осталась в коде. Позднее были собраны отдельные проекты с этой опцией, чтобы сравнить на сколько она необходима. Проекты и конечные FLM отличаются от тех, что с двойным чтением суффиксом _DR (DoubleRead) - например, 1986VE9x_FlashInt_DR.FLM. Мнение о необходимости DoubleRead пока не сложилось.
Стоит отметить, что функции BlankCheck(), EraseChip() и Verify() не являются необходимыми. При отсутствии реализации, EraseChip() заменяется стиранием по секторам, вместо вызова Varify() прошивальщик сравнит CRC исходных и записанных данных. (BlankCheck() видимо совсем может быть пропущено, альтернативы этой функции я не нашел.)
Если все сделано правильно, получившийся проект должен собираться и выдавать файл FLM.
В директории FLM_Build я создал workspace для нескольких проектов. В каждом проекте будет собираться свой FLM. Вариантов проектов может быть много - под каждый МК, с различными опциями или настройками. Все проекты в одном workspace можно собрать разом, достаточно нажать кнопку Batch Build в панели Keil и на выходе получаем готовые FLM файлы.
Создается workspace в меню Keil - Project - New Multy-Project Workspace…, далее необходимо дать название - у меня получился FLM_Build.uvmpw. После задания имени откроется окно менеджера проектов, в котором можно выбрать проекты для добавление в workspace.
Активным может быть только один проект, для него открываются опции проекта и только его можно редактировать. Чтобы поменять активный проект, надо кликнуть на необходимый проект правой клавишей мыши и выбрать Set as Active Project. Для добавления и удаления проектов необходимо правой мышью кликнуть на заголовок workspace и выбрать Manage Multy-Project Workspace….
После добавления всех проектов в workspace нажимаем на панели кнопку Batch Build, при этом откроется окно, в котором необходимо выбрать проекты, которые будут собираться. Варианты с ARM7_ARM9 нам не нужны, остальные выбираем. После сборки проектов Build в директории FLM_Build получаем файлы FLM.
Далее осталось провести их проверку с Keil и c утилитой J-Flash.
Полученные FLM проверены на МК 1986ВЕ1Т, 1986ВЕ3Т и 1986ВЕ9х. Прошивка происходит, но не со 100% результатом. Иногда выдаются сбои верификации. Например, при прошивке через J-Flash выдается статус, что верификация провалена. При этом программа прошивается и работает. Если же запустить прошивку второй раз, то она не происходит, а выдается сообщение, что программа уже зашита и верификация зашитой программы прошла успешно.
Будем разбираться с этим дальше.
(По окончании темы статья будет дополнена)
Обратите внимание, в спецификации на микроконтроллеры указано - "При стирании информационной области автоматически стирается и основная".
Это совсем не очевидно и попалось на глаза случайно. Стоит иметь это ввиду при работе с информационной памятью.
Тестовые проекты описывать не буду, код представлен в директории test. Проекты имитируют последовательность вызовов функций FlashPrg.c прошивальщиком Keil. Это обычные проекты для работы с МК с той лишь особенностью, что они собраны для запуска из ОЗУ и поэтому работают только в режиме отладки. Но, собственно, этого достаточно.