STM32F405: прошити 400кб за 10 секунд або швидкий UART-завантажувач заточений під USB-UART, розміром менше 4 кілобайт

C утилітою для ПК і платою — програматором,
з використанням SPL,
з повноцінною системою команд і перевіркою CRC32,
з гарантією доставки й перевідправки збійної або втраченої команди,
з перевірками помилок, налагоджувальні повідомленнями і урізаним printf'ом.
Оптимізовано під сучасні USB-UART перетворювачі та потокову передачу.


Зміст
Історія
Аналіз причин низької швидкості протоколу AN3155
Вимоги до мого завантажувачу
Опис протоколу завантажувача
Утиліта для завантаження ПК
UART Програматор на базі CP2103
Реалізація прошивки завантажувача
Оптимізація розміру
Підсумок та результати

Передісторія
Ми майже у всіх своїх пристроях використовуємо STM32. Наприклад в наших шлюзах.
У дуже багатьох пристроях варто STM32F405/407 і є USB <--> UART міст на базі CP2103, але іноді FTDI.
У всіх STM32 згідно AN3155 є вбудований завантажувач UART і ми його використовуємо на всіх стадіях: від розробки і виробництва до техпідтримки наших користувачів.
Фірма STM також пропонує ПК утиліти для того щоб скористатися цим протоколом.
У CP2103 є штатні GPIO, якими можна пристрій скинути в основну програму або в штатний завантажувач.
Ще добре те, що ці GPIO вінда не чіпає, коли шукає Plug&Play пристрою на RS232, і тому пристрій не скидається при підключенні до ПК і не «вилітає» в незрозумілий для користувача режим.
Здавалося б все добре, але у цього штатного завантажувача є відчутна проблема: надто вже довго він прошиває: близько двох хвилин, особливо коли отлаживаешь схемотехніку, а не свій код.
При цьому зазвичай вносиш мінімальні правки і довше чекаєш коли прошьется, встигаєш відволіктися і тд.
Та й справа не втрачати за день більше чотирьох годин на прошивку вироби, притому що деколи розробка триває не один місяць.
Ця проблема довгої прошивки змусила шукати причину проблеми і почати її вирішувати.
І рішення представлене в цій статті — це друга спроба.
Перша спроба полягала в тому, що я написав свою утиліту для ПК використовує AN3155 — "ARMkaProg".
Вона вийшла на порядок зручніше і розумніші штатної, змогла використати GPIO у CP2103 і інші апаратні зручності.
Але…
Штатна утиліта і AN3155 не рекомендували шити на швидкості вище 115200 бод,
Моя ж програма могла шити навіть 1000к бодах, але це давало збільшення швидкості всього в 2 рази (порівняно з 115200).
А не в 9, і в підсумку питання швидкості прошивки залишився не вирішеним.

Аналіз причин низької швидкості протоколу AN3155
Низька швидкість прошивки випливає із специфіки і простоти цього протоколу:
  1. Рекомендована максимальна швидкість — 115200, але багато USB мости підтримують як мінімум мегабод, але на швидкостях вище 115200 протокол активується з десятої — сотої спроби, а кожна спроба це мінімум 300 — 500мс.
  2. Прати треба всю флеш, навіть якщо розмір прошивки 10к з доступних 1024к байт — в деяких кристалах були баги з частковим стиранням.
  3. Штатний бутлоадер (bootloader) — універсальний і, тому, дуже повільний, він не використовує зовнішній кварец і не використовує PLL і високу частоту, а так само помічено, що флеш повільніше стирається на низькій тактовій — це зроблено для батарейного застосування.
  4. Маленький розмір команди. Максимум за 1 раз можна прошити 256 байт, а на деяких версіях чіпів не більше 128.
  5. Купа зайвих рухів: треба активувати завантажувач, отримувати списки команд, для виробництва треба дізнатися унікальний ChipID, не завжди вдається прочитати однією командою і не завжди це можливо навіть з бутлоадера.
  6. З міркувань безпеки у нас завжди будь прошивки залочены, навіть тимчасові або налагоджувальні. Хоч разлочка за фактом — стирання, але штатну процедуру стирання робити обов'язково треба, т. к. ніде не сказано, що разлочка гарантовано пере, а не забиває наприклад сміттям / нулями і тд. Тому виходить, що треба прати двічі, а це вже майже хвилина!
  7. Треба прошивку верифікувати, тобто ганяти туди-сюди прошивку двічі: перший раз для запису, а другий раз — зчитувати і звіряти.
  8. В контролері є дуже багато оперативна пам'ять: 192к, але завантажувач цим не користується, він єдиний для всіх чіпів включаючи ті, що мають дуже мало ОЗУ.
  9. На кожну команду припадає три підтвердження (АСК): код команди, на адресу і дані. Але USB працює інтервалами по 1мс, в результаті на кожну команду витрачається 3мс навіть якщо треба вважати або записати 1 байт і ми можемо послати не більше 333 команд і без того мізерних блоків даних.
Розглянемо докладніше команду записи з даташита на AN3155:

де червоним відмічено: (1) — підтвердження коду команди, (2) — підтвердження адреси, (3) — блоку даних
Дивимося на команду записи осцилографом (швидкість 1000к бод)

і реально бачимо ці стани АСК: (1), (2) і (3), і їх помітну затримку з пункту 9,
а то ж бачимо, що час запису величезна і більше часу витрачається на прошивку, ніж на передачу даних (див. пункт 3).
І якщо глянути на десяток команд запису

то видно що пауз дуже багато і вони займають 2/3 часу.
Хмм, а що якщо не чекати на кожну команду по три штуки ACK і просто слати дані потоком?
Але нічого доброго з цього не виходить. Завантажувач простий настільки, що він втрачає дані якщо їх не чекає, прийому за DMA або перериванням в ньому немає, та й ROM пам'яті теж немає щоб на кожен інтерфейс зробити все ідеально.
З усіх цих причин логічним чином випливають:

Вимоги до мого завантажувачу
  1. Фіксована швидкість UART в 921600 BOD: без стадії автовизначення швидкості, яка часто дає збої.
  2. Часткове стирання вибраних сторінок.
  3. Робота на максимальній тактовій частоті: використовувати PLL і прошивати флеш швидше ніж приходять нові дані.
  4. Підтримувати команди з великими блоками даних, мінімум 2к байти.
  5. Не повинно бути ніяких стадій типу активації, слід одразу видавати однією командою все необхідне: включаючи ChipID і всі опції і версії.
  6. Можливість роботи в залоченном чіпі, без його розблокування.
  7. Перевірка цілісності без зчитування хост: наприклад, відправленням і звіркою CRC всій прошивки.
  8. Використання всього ОЗУ: поки йде стирання вже можна передати як мінімум цілих 100 кбайт вмісту прошивки.
  9. Не вимагати очікування підтвердження на кожну команду. Вхідний потік від Хоста не повинен залежати або чекати виходить від завантажувача. Хост може видавати команди на максимальній швидкості. А завантажувач повинен лише повідомити, що останній вдало прошитий адреса такий — то. І, у випадку збою, хост міг би продовжити з цієї адреси. Наприклад, подібним чином працює протокол TCP.
Ці пункти усувають відповідні причини низької швидкості.
Але на практиці потрібні ще дрібниці:
  • Точка початку прошивки і точка входу повинні розрізнятися: перші сторінки зручні для організації псевдо-EEPROM і тому зазвичай туди код прошивки і її вектора переривань не кладуть. А починаються вони з зсуву 0x08010000 або далі, тобто в тих сторінках, які великі і довго стираються.
  • Потрібен монітор станів і текстові повідомлення про помилки: Не люблю коли немає налагоджувальних і статусних повідомлень, т. к. пекла важко налагоджувати «чорний ящик».
  • Завантажувач повинен бути як можна менше: пристрої у нас різні і з різною периферією, але з загальною платою і загальної прошивкою, включаючи всі завантажувачі, клієнтський код і код самодіагностики — все в одному. А так само моїх завантажувачів в одному образі теж безліч, мій варіант AN3155 з шифруванням, GPRS, повноцінний FSK-модем, і всі їх треба зуміти вмістити в одну-дві перші сторінки розміром 16-32к байта. У підсумку, кожен байт на рахунку.
  • Завантажувач повинен бути надійним, в AN3155 команд захист від збоїв і сміття слабка — просто XOR всіх байт. Було кілька випадків, коли вона давала збої.

Опис протоколу завантажувача
Вирішив зробити більш докладний опис свого протоколу на випадок, якщо хто-небудь захоче перенести програму для завантаження ПК під інші ОС ніж Windows.
Розгорнути опис протоколуЗагальний принцип
  • Два учасника: Хост і Пристрій.
  • Хост прошиває Пристрій.
  • Хост ведучий та ініціює обмін.
  • ведене Пристрій.
  • Обмін здійснюється командами.
  • Команди оформлені пакети.
  • Порядок байт в пакетах: Little endian
  • Один пакет — одна команда.
  • Одна команда — одна дія або подія.
  • Пристрій відповідає на команди Хоста з тим же кодом команди що й хост.
  • Пристрій генерує без запиту лише аварійні команди про перезавантаження і таймауте.
  • Кожен пакет починається з 32-х бітної сигнатури початку пакета.
  • Сигнатури різні для напрямків у Хост і Пристрій.
  • Решті формат однаковий для обох напрямків.
  • Кожен пакет захищений CRC32 в кінці пакета.
  • Розмір пакета в байтах повинен бути кратний чотирьом.
  • Таймаут на загальний скидання Пристрою в 500мс, Хост зобов'язаний гарантувати що пауз більше 500мс не буде.
  • Крім команд можна видавати з Пристрою налагоджувальні текстові повідомлення в голому ASCII, вони не повинні починатися або містити сигнатуру початку пакета і повинні виводитися поза тіла пакетів.
Структура пакету:









адреса розмір вміст 0 4 Сигнатура початку команди, для передачі в пристрій 0x817EA345, для прийому з пристрою 0x45A37E81 4 1 код команди, задає тип дії або події 5 1 побитово-інверсний код команди (для перевірки) 6 2 N — розмір додаткової інформації в байтахзобов'язаний бути кратним 4 8 N додаткова інформація — залежить від коду команди 8+N 4 вбудований апаратний в STM32 CRC32 пакета з адреси 4 N (виключаючи сигнатуру)
Сигнатура початку команди для різних напрямків обрана різна для того, щоб помилково не сприйняти свої дані, які потрапили собі ж на прийом. Наприклад, коли КЗ на ніжках RX і TX контролера або збої програматора / кабелю.
Обробка помилок на рівні пакетів і таймаутів
  • Якщо код команди не відповідає перевірочного инверсному кодом, то початок пакета не зараховується, і пошук нової команди починається з таких байт.
  • Якщо прийнятий пакет з розміром додаткової інформації більше ніж внутрішній буфер, то пакет ігнорується, і пошук нової команди починається з наступних після розміру байт.
  • Якщо прийнята в тілі пакета CRC32 не дорівнює фактично розрахункової, то вміст пакета ігнорується і пошук нової команди починається з наступних після CRC32 байт.
  • Якщо приходять байти, але сигнатура початку пакета не задетектирована. Ці байти вважати налагоджувальні текстовими повідомленнями і накопичувати до коду 13 (переведення рядка), а після цього коду виводити налагоджувальну консоль.
  • Якщо з моменту останнього прийому пакета пройшло більше 500мс, то завантажувач скидається в початковий стан. Пакет, який не встиг взятися до кінця, ігнорується і так-же скидається. Про таймауте Пристрій повідомляє пакетом з спеціальним кодом команди "таймаут".
  • При запуску завантажувача генерується інший пакет зі спеціальним кодом "перезавантаження".
Порядок роботи з боку пристрою
  1. Відповідаємо на команду інформації, а при скиданні або таймауте оповіщаємо хост про це.
  2. Прошивка записується послідовно з початку вільного діапазону до кінця прошивки або вільного місця.
  3. У пристрої міститься адреса поточного блоку, який повинен бути записаний.
  4. За замовчуванням адресу поточного блоку встановлений в 0, що означає, що стирання не було виконано і запис неприпустима.
  5. Команда стирання: стираємо скільки запитано округливши за розміром сторінки і звітуючи про кожній стертою сторінці.
  6. По закінченню стирання адреса поточного блоку встановлюється на початок вільного діапазону — можна почати запис.
  7. Команда запису: якщо адреса прийнятого блоку дорівнює адресою поточного блоку, то записуємо його і оновлюємо поточний адресу.
  8. Команда запису: якщо адреса прийнятого блоку НЕ дорівнює адресою поточного блоку, то ігноруємо запис.
  9. В обох випадках 6 і 7 повідомляємо хост адреса для поточного запису блоку.
  10. Після закінчення запису Хост повинен видати команду "Старт" з CRC32 всій прошивки, і якщо вона вірна, то прошивка запускається.
  11. Якщо протягом 5 секунд команд не надходило, то запустити основну прошивку якщо вона є.
  12. При таймауте 500мс або апаратного скидання адреса для поточного запису блоку скидається в 0.
Порядок роботи з боку хоста
  1. Отримуємо інформацію з пристрою: розмір флеша, ChipID, розмір буфера прийому, адреса старту, версія завантажувача і чіпа.
  2. Даємо команду стирання або всієї прошивки, або частини вказавши розмір стираної області.
  3. По завершенню стирання починаємо безперервно відправляти команди запису блоками один за одним, перевіряючи що поточна записана позиція в пристрої збільшується.
  4. Якщо позиція перестала збільшуватися (взяли дві відповіді на команду "запис" з однаковою адресою), то скорегувати адресу на Хості, скинути буфери відправки і почати передавати з нового скоригованого адреси.
  5. По завершенню запису подати команду "Старт" передавши CRC32 всій прошивки, у відповідь Пристрій повідомить фактичне CRC32.
  6. Якщо фактичне CRC32 одно розрахункового, то завантаження успішно закінчена і прошивка запущена.
Опис команд
В описі команд розпишу тільки додаткові параметри команди, які йдуть після коду і розміру параметрів.
Код команди подано в круглих дужках, далі константа в исходниках і назва російською
(0x97) SFU_CMD_INFO Команда: Запит інформаціїДодаткові параметри команди з Хоста:
відсутні.
Відповідь пристрою:










розмір в байтах Опис 12 Унікальний ChipID 4 Модель і ревізія чіпа, взятий з DBGMCU->IDCODE 2 Розмір доступної для запису завантажувачем флеш-пам'яті в KiB (*1024 байт) 2 Версія завантажувача 0x0100 4 Розмір буфера прийому пристрою (для опції -PreWrite) 4 Адреса початку доступною для запису пам'яті 4 Адреса таблиці векторів переривань і місце з якого береться контекст запуску (стек + точка входу)
приклад з логів:

(0xC5) SFU_CMD_ERASE Команда: Стирання флеш пам'ятіДодаткові параметри команди з Хоста:
4 байта — розмір стираної області в байтах
Відповідь пристрою: видається по закінченню стирання всіх сторінок,
4 байта: Якщо успішно, то розмір стертою області рівний тому, що передав Хост. Якщо стався збій, то 0.
Під час стирання пристрій:
  1. Пере сторінки починаючи з №1 і до необхідної відповідно до розміру округленому в більшу сторону.
  2. Після стирання кожної сторінки відповідає командою SFU_CMD_ERASE_PART («сторінка стерта»).
  3. Накопичує надходять команди в буфері прийому, але на них не відповідає і обробляє тільки після завершення стирання.
  4. Час стирання перших трьох сторінок близько 300 мс, наступних близько 2 секунд.
приклад з логів:

(0xB3) SFU_CMD_ERASE_PART Повідомлення: сторінка стерта (для опції -PreWrite)Хост не повинен видавати команду з таким кодом — вона буде проігнорована.
Хост під час стирання може заздалегідь передавати дані для запису командами SFU_CMD_WRITE («запис») — для прискорення запису.
Але передавати команд можна не більше ніж розмір буфера прийому пристрою, інакше він переповниться і перші пакети заміняться новими, а наступні будуть проігноровані.
Параметри команди з Пристрою:
4 байта: номер стертою сторінки починаючи з №1 до №11.
(0x38) SFU_CMD_WRITE Команда: ЗаписДодаткові параметри команди з Хоста:
4 байта — адрес, починаючи з якого необхідно записувати вміст.
X*4 байт — вміст прошивки, де Х — кількість 32 бітних слова і повинно бути: 1… 1023.
Запис ігнорується якщо адреса зазначений Хост не дорівнює поточному адресою запису в пристрої.
Пристрій відповідає завжди незалежно від того була проведена запис або проігнорована.
Відповідь пристрою:
4 байта: адреса для наступного запису блоку збільшується якщо запис відбулася успішно, не змінюється якщо команда проігнорована
4 байта: кількість необроблених даних у буфері прийому пристрою (для налагодження та моніторингу).
Приклад відповідей кількох успішних команд "Запис" з логів:

(0x26) SFU_CMD_START Команда: СтартДодаткові параметри команди з Хоста:
4 байта: CRC32 всій записаної прошивки, початок прошивки зазначено в команді "Інформація", кінець — останнє підтверджене пристроєм записане слово командою "запис" — адресу для наступного запису блоку (не включно).
Відповідь пристрою:
4 байта: Адреса початку прошивки.
4 байта: Кількість записаних байтів (Увага, не 32-х бітних слів!), кратне чотирьом.
4 байта: CRC32 для звірки Хостом, обчислюється починаючи з "Адреса початку прошивки", розміром з "Кількість записаних байтів".
Після цієї команди Пристрій перевіряє CRC32 і якщо воно збігається з тим, що дав Хост, то запускає прошивку, виконавши повну деинициализацию обладнання.
Приклад з логів:

(0xAA) SFU_CMD_TIMEOUT Повідомлення: Минув таймаут в 500мсМинуло більше 500 мс з моменту отримання останньої команди і таймаут минув, пристрій скинулися в початковий стан
Без параметрів.
Хост нічого не має на неї відповідати.
Це аварійне повідомлення — команда, яку видає тільки пристрій.
(0x55) SFU_CMD_WRERROR Повідомлення: Помилка запису флеш пам'ятіЗапис у флеш пам'ять завершилася з помилкою. Таке зустрічається якщо харчування недостатньо або підроблені китайські чіпи типу GD32F4xx.
Без параметрів.
Хост нічого не має на неї відповідати.
Це аварійне повідомлення — команда, яку видає тільки пристрій.
(0x11) SFU_CMD_HWRESET Повідомлення: Апаратний скидання завантажувачаАпаратний скидання пристрою. Пристрій АПАРАТНО скинулися в початковий стан — завантажувач перезапустился.
Без параметрів.
Хост нічого не має на неї відповідати.
Це аварійне повідомлення — команда, яку видає тільки пристрій.

Утиліта для завантаження ПК
вигляд:

Написана для Windows на Delphi 6 (2001 року, той що має 8 бітний тип char і не уникоде). Скомпілював на Delphi XE5 і перевірив працездатність. Такий старий делфі обраний тому що мені так було простіше: ще з початку 2000-их були великі напрацювання по роботі з CP210x, COM-портами і тд.
Робота з пристроєм на рівні байт виділена в окремий потік tCOMclient не залежить від затримок візуального інтерфейсу. Зв'язок з цим окремим потоком зроблена за допомогою черг на зчитування і запис розміром в 65536 байт.
Рівень парсинга з оформленням команд і рівень логіки роботи з командами розділений на два окремих класу tSFUcmd і tSFUboot.
Оновлення прошивки проводиться на швидкості 921600 бод, без парності, 8 біт, один стоп-біт.
Пристрою можна вказувати:
По імені COM порту, наприклад COM123.
За серійним номером записаному в CP210x
По системному шляху WinNT, наприклад \??\USB#VID_10C4&PID_EA60#GM18_E_0010#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
У будь-якому разі, якщо відкрите пристрій CP2103, то утиліта може спробувати його скинути через GPIO1 (18 пін) виставивши 0-1-0.
Хустки-програматор з її схемою, також прикладена та описано далі.
Якщо запущена без параметрів командного рядка, відновлює при запуску і зберігає при завершення параметри з текстового файла: FastTest.exe.config
Якщо параметри командного рядка присутні, то налаштування з цього файлу ігноруються і він не змінюється. Замість цього налаштування візуальні компоненти беруться з командного рядка і прошивка запускається якщо це зазначено.
Можна використовувати такі параметри командного рядка:
  • Прошивка вказується без перфиксов
  • -DEV:<device_name> Ім'я пристрою або COM порту вказується в <device_name>
  • reset або RST скинути пристрій якщо COM порт йде через CP2103.
  • -fast Швидке стирання, прати тільки ті сторінки, в яких буде розміщуватися прошивка
  • exit Закрити програму по завершенню якщо прошивка пройшла успішно чи вказана опція -no-Errors-Keep
  • -no-Errors-Keep Закрити програму після завершення у будь-якому випадку
  • -no-Prewrite Не посилати дані для запису під час стирання
  • go або run або -start Почати прошивку відразу після запуску програми, інакше налаштування застосовуються, але запуск не проводиться.
Можна завантажити звідси:
https://github.com/Mirn/Boot_F4_fast_uart/tree/master/delphi/Release

UART Програматор на базі CP2103
Викладаю наш маленький і простенький програматор який:
  • Використовує для прошивки у CP2103 UART і GPIO, всього п'ять проводів: GND, TX, RX, BOOT, RST.
  • Їм можна шити всі види STM32 за уарту, і використовувати мій завантажувач з цієї статті.
  • У ньому є захист від гарячого підключення, і за весь час він ні разу не згорів (поки, сподіваюся...).
  • Контакти підписані шовкографією, що дуже зручно (не забудьте замовити шовкографію в шарі топ)
  • Маленький, трохи більше USB роз'єму.
  • В комплекті є утиліта ConfigProg\CP2102_Enum.exe для налаштування GPIO в CP2103: вибрати пристрій і натиснути "USB write".
  • УВАГА: він не гальванічно розв'язаний.
установки повинні бути такі:
IO.Mode = 1100001101010100
IO.Reset = 0000110011111111
IO.Suspend = 0000111111111111
IO.EnhFxn = 10

Утиліта FastTest з попередньої глави якраз на цьому програматорі розроблялася і відлагоджувати.
Схема
Плата
Вихідні файли на програматор качати звідси:
https://github.com/Mirn/ProgCP2103

Реалізація прошивки завантажувача
Засоби розробки і сторонні бібліотеки:
  • Середа: Eclipse Kepler Service Release 1
  • Компілятор: GCC v5.4 2016q2, з налаштуваннями:
    mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Os -fmessage-length=0 -ffunction-sections -fdata-sections -ffreestanding -fno-builtin -Wunused -Wuninitialized -Wall -Wextra -Wpointer-arith -Wshadow -Wlogical-op -Waggregate-return -Wfloat-equal -Wno-sign-compare 
  • Лінкування і компонування за допомогою файлу ld скрипта з секціями.
  • Проект збирається за допомогою утиліти Makefile можна збирати без середовища командою make all
  • Код запуску і ініціалізації взято з CooIDE версії 1.6, тому що він мінімальний і відомий мені.
  • Бібліотека роботи з периферією від STM: standard peripheral library (SPL V1.3.0)
Профіль використання пам'яті:
  • Стек я розташував в окремій CCM пам'яті 65к.
  • Основний кільцевої буфер прийому з UART в 100 кілобайт розташований в окремій секції на початку RAM
  • Всі інші буфера і змінні розташував у .bss в останніх 28к RAM пам'яті.
Бібліотека usart_mini.c
Зроблена максимально просто: DMA не використовується, передача зроблена прямий відправкою в периферію без переривань за допомогою функцій SPL. Але раз основна мета прискорення протоколу — безперервний потік команд з вмістом прошивки, то прийом даних UART зроблений по перериванню USART1_IRQHandler.
Так само в UART я реалізував контроль і облік помилок і перевірку і корекцію при переповненні буфера даних, якщо їх записано більше, ніж його розмір.
При реалізації прийому UART в перериваннях виникла проблема:
код знаходиться під флеш і під час прошивки флеш пам'яті, шина флеша блокується і виконання зупиняється повністю включаючи переривання. І на швидкостях вище 500к БОД це призводить до втрати прийнятих з UART даних, т. к. час призупинення стає більше часу прийому байта. Тому функція обробки переривання була винесена в ОЗП ось таким от чином:
__attribute__ ((long_call, section(".data")))
void USART1_IRQHandler(void)

при цьому є важлива тонкість, що якщо функція, що лежить в RAM, викликає інші функції флеш, то отримаємо помилку виду:
usart_mini.c: relocation truncated to fit: R_ARM_THM_CALL against symbol `demo' defined in .text.demo section in ./src/main.o

Це викликано обмеженням архітектури ARM інструкцій Thumb2 на максимальне адресний відстань між викликами. А в цьому випадку воно більше допустимого. Я виправив це додавши до всім викликаним з RAM функцій атрибут-модифікатор long_call.
Бібліотека packet_receiver.c
Приймає пакети за протоколом описаного в цій статті і перевіряє їх цілісність. При цьому, на всіх стадіях розгляду пакета перевіряє на наявність помилок і підраховує їх кількість, якщо вони зустрінуться. Але помилки не замовчуються, а виводяться текстові повідомлення і раз в секунду виводиться рядок з усіма помилками як UART так і рівня пакетів і випадків таймауту у 500 мс. Цей таймаут в 500 мс контролюється і виробляється цієї бібліотекою.
Бібліотека sfu_commands.з
Обробляє логіку команд SFU_CMD_XXX як описано вище. Пере і прошиває флеш, при цьому функція прошивки слова в флеш пам'ять також винесена в RAM, щоб дані по прийому UART не губилися. У неї ж реалізований запуск основної прошивку, при цьому перевіряючи що її контекст вказує на реальну флеш пам'ять RAM пам'ять. Перед запуском основний прошивки уся периферія і тактові повноцінно деинициализируются і скидаються.
Працездатність прошивки перевірена на моделях: STM32F405RG, STM32F405VG,
і на швидкостях від 115200 до 921600 бод.
всі вихідні коди прошивки доступні на моєму гітхабі за посиланням:
https://github.com/Mirn/Boot_F4_fast_uart

Оптимізація розміру
В першу чергу і, найголовніше, що впливає на розмір — це загальна архітектура алгоритму і використаних даних. Я намагався робити все максимально просто і, навіть місцями, примітивно. При цьому я намагався найскладніші речі в логіці перекладати на Хост. Одним словом, порядок і стислість в коді починається з порядку в голові розробника.
Але потрібно знати міру і не забувати про тих вигодах, які допомагають краще зрозуміти що твориться у завантажувачі, і тому залишилися налагоджувальні повідомлення, контроль і підрахунок помилок і інші дрібниці і зручності. А так само я не став ліпити в одну функцію і розклав все по поличках, і розбив на модулі. Хоч це і призводить до збільшення розміру прошивки на пару сотень байт, мені її ще багато років доведеться підтримувати, розвивати і на базі неї створювати нові. Ще невеликий внесок у збільшення розміру дала необхідність помістити частину функцій в RAM.
Також не варто забувати, як працює компілятор і його оптимізатор. Компилировал я природно -Os, але якихось інших особливих ключів не використовував і навіть не заморочувався з цим. Якщо дати побільше конкретики, то компілятор зможе краще оптимізувати параметри підписувати const де це можна, локальні в межах одного файлу функції як static і т. д.
Так само не варто шаманити з дрібницями типу перестановки рядків, вилизування ифов з булевої оптимізацією умов у них — це всі компілятори давним давно вміють. Довіртеся їм. В разі чого, можна глянути map файл, де написано яка функція, скільки займає або просто в лістингу порахувати кількість рядків. Навіть не знаючи асма при цьому відразу буде видно яка функція несподівано монстроидально розгорнулася.
Оптимізація SPL
Standard peripheral library від STM має дуже великий потенціал зменшення в розмірі. Вона написано дуже просто — багато функції перекладають дані з переданих їм заповнених структур, у відповідні регістри периферії. Ці функції не містять внутрішніх статичних змінних, не звертаються до глобальних змінних і зазвичай не вимагають покажчики на якісь сховища станів. Вони дуже рідко звертаються до інших своїм або чужим функцій. Але у них є недолік: вони містять дуже багато повторювану коду, наприклад GPIO_DeInit перевіряє рівність переданого GPIO до кожного порту GPIOA, GPIOB… GPIOI, і скидає кожен порт окремо окремим кодом. Тобто там всередині дійсно пачка з десяти if і двадцяти RCC_AHB1PeriphResetCmd. І тому SPL споживає дуже багато флеша. На в'язку UART і GPIO з RCC зазвичай припадає близько 8 кілобайт.
Тому я скопіював код використаних функцій SPL в окремий заголовочник, оголосив їх як static inline і додав до кожної такої функції суфікс _inline, наприклад GPIO_DeInit_inline. Так само заинлайнил всі викликані ними функції. Це відразу скоротило код в рази.
Оптимізація секції .data і .ro_data
В секції .data зберігаються стартові значення змінних, які задані на стадії компілювання. Вони розміщуються в флеша, і в коді є цикл, який їх копіюються при старті на RAM.
Я написав код так, щоб таких змінних не було взагалі, і не довелося б писати код, який вручну виставляє їм потрібні параметри.
В секції .ro_data зберігаються всі константи, в тому числі і текстові. Тут потрібно просто знати міру, і поеми термінал не виводити, обмежившись мінімально інформативним логом з одного-двох слів. А ще у GCC є такий баг, коли функція не використовується, але її константные змінні .ro_data і прошивку все-таки потрапляють. Такі випадки я теж закомментировал або видалив.
Оптимізація printf і його тіні impure_data і impure_ptr
Я взяв з CoIDE готову реалізацію урізаного printf, в ній багато спрощено, а підтримки плаваючою комою взагалі немає. Але вона неявно використовують структуру impure_data і покажчик impure_ptr. Вони займають сотні байт і тягнуть за собою ще багато чого. Компілятор gcc приховано від програміста поміщає stderr і stdin саме в цій структурі їх необхідно не використовувати у коді.
Спочатку приклад printf як раз містив stderr і stdout, їх згадування я і прибрав, попутно замінивши на більш прямі виклики і закомментировал непотрібні опції printf. І прибрав не використовувані варіанти виводу наприклад рядків, знакових цілих, шестнадцетиричных і тд.
Оптимізація запуску коду і видалення повторного коду
З CoIDE я взяв самий мінімальний, який знайшов, код запуску і первинної ініціалізації. Він копіює .data з флеша в RAM, запускає кварец і налаштовує частоти, обнуляє .bss і налаштовує процесор: стек, плаваючі коми, CCM пам'ять і тд.
Але частина цих завдань вже реалізовані в SPL і використані мною. Я їх замінив прямим викликом відповідної не інлайн функції SPLа.
Також було багато повторів коду, коли, наприклад, плаваючі коми включаються аж у трьох місцях.
Прибив цвяхами SystemCoreClock до дефайну і викинув функцію SystemCoreClockUpdate.
Код запуску використовував таблиці констант для розрахунків які зберігалися в RAM як volatile (цікаво чому?). Переніс в флеш, а при оптимізації компілятор частина з них замінив на прямий розрахунок (там де були ступеня двійки, в тридцять два слова).
Зменшення таблиці переривань
Таблиця переривань містить у перших двох 32-бітних і осередках контекст виконання: адреса, код та адресу стека. А в наступних містить покажчики на всі можливі переривання. А це майже 500 байт. Так як "Остапа понесло" і я вже не міг змиритися, що коду більше 4к (привіт 4к демо сцені!). То я позбавився і від таблиці тим, що стиснув її до перших двох осередків. А в стартап-коді переніс вектор на таблицю в RAM, куди додав тільки один обробник UARTа ручками таким кодом:
__attribute__ ((section(".isr_vector_minimal"))) void (* const StartVectors_minimal[])(void) =
{
(void *)&_estack,
Reset_Handler,
};

__attribute__ ((section(".isr_vector_RAM"))) void (* StartVectors_RAM_actual[128])(void) = {0};

void Default_Reset_Handler(void)
{
...
StartVectors_RAM_actual[0xD4 / 4] = USART1_IRQHandler;
SCB->VTOR = (uint32_t)StartVectors_RAM_actual;
main();
}

і поправив ld файл прописавши так, щоб секція для таблиці переривань в RAM була вирівняна як і належить по 512 байтам
.text : { 
KEEP(*(.isr_vector_minimal*))
*(.text .text.* .gnu.linkonce.t.*) 
*(.rodata .rodata* .gnu.linkonce.r.*)
} > rom

.bss (NOLOAD) : { 
_sbss = . ;

. = ALIGN(512); 
*(.isr_vector_RAM*)

*(.bss .bss.*) 
*(COMMON) 
. = ALIGN(4); 
_ebss = . ; 
} > ram

Економія на таблиці векторів вийшла майже в 400 байт.

Підсумок та результати
Час прошивки розміром 400 кілобайт.
вбудованим завантажувачем за AN3155 з швидкістю 256000 БОД: 95 секунд
вбудованим завантажувачем за AN3155 з швидкістю 500000 БОД: 78 секунд
вбудованим завантажувачем за AN3155 з швидкістю 921600 БОД: 70 секунд
у всіх випадках з разлочкой і залочкой, з повним стиранням
моїм завантажувачем зі швидкістю 921600 БОД: 9 секунд,
що швидше у 8 разів.
Відео роботи нового завантажувача (на початку), і старого за AN3155, запускається після нового.
Перевіряємо осцилографом на предмет безперервності потоку даних і відсутності пауз по UART

або більш розгорнуто один пакет:

пауз немає, безперервний потік, прискорення в 8 разів отримано.
Вийшло все що було заплановано і до чого прагнули.
Ще раз посилання на гитхаб:
https://github.com/Mirn/Boot_F4_fast_uart
Це мій перший проект на гітхабі і опублікований з метою його вивчення і входження в співтовариство.
Я вирішив зробити не "хеллоу ворлд", а щось реально корисне. Гитхаб для спільноти і нерозумно починати з марного всім проекту. Я згадав про те, до чого руки свербіли, але за ліні багато років не доходили. І раптом видався привід: з-за кризи в мене скоро виникне просто сила-силенна вільного часу, а щось робити треба зараз. У підсумку так і народився цей турбо-завантажувач.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.