{"id":"709430","timePublished":"2023-02-07T07:41:32+00:00","isCorporative":false,"lang":"ru","titleHtml":"Регистры vs библиотеки на примере сердечек","leadData":{"textHtml":"<p>Впереди 14 февраля. Можно спорить об уместности этого праздника в наших краях, а можно направить энергию в мирное русло. Например, откопать Arduino, щедро обсыпать светодиодами и сформовать их во что-то сердечкоподобное. Неубедительно? Согласен. Давайте так: откопаем в дальней коробке макетку на STM32, забудем, что у нас есть готовые библиотеки и подёргаем регистры, выгрызая каждый байт ROM у злобного компилятора. Потом сделаем всё тоже самое, но без фанатизма, с привлечением CMSIS библиотек и сравним результаты. Возможно даже сделаем выводы. Будет код, надругательство над таблицей векторов. Ардуинка тоже будет, куда ж без неё.</p><p></p>","imageUrl":"https://habrastorage.org/getpro/habr/upload_files/210/d1f/f86/210d1ff862faf0398a99abf7cc4026ab.jpg","buttonTextHtml":"Build Target","image":{"url":"https://habrastorage.org/getpro/habr/upload_files/210/d1f/f86/210d1ff862faf0398a99abf7cc4026ab.jpg","fit":"cover","positionY":17,"positionX":18}},"editorVersion":"2.0","postType":"article","postLabels":[],"author":{"id":"419792","alias":"Kudesnick33","fullname":"Tikhon","avatarUrl":"//habrastorage.org/getpro/habr/avatars/7aa/d40/2ae/7aad402ae71dd5100e834a6b08db603a.jpg","speciality":"Разработчик встраиваемых систем","scoreStats":{"score":20,"votesCount":46},"rating":0,"relatedData":null,"contacts":[],"authorContacts":[],"paymentDetails":{"paymentYandexMoney":null,"paymentPayPalMe":null,"paymentWebmoney":null},"donationsMethod":null,"isInBlacklist":null,"careerProfile":null,"isShowScores":true,"reach":null},"statistics":{"commentsCount":33,"favoritesCount":57,"readingCount":8138,"score":35,"votesCount":28,"votesCountPlus":27,"votesCountMinus":1,"reach":8611,"readers":8611},"hubs":[{"id":"17717","alias":"c","type":"collective","title":"C","titleHtml":"C","isProfiled":true,"relatedData":null},{"id":"19737","alias":"controllers","type":"collective","title":"Программирование микроконтроллеров","titleHtml":"Программирование микроконтроллеров","isProfiled":true,"relatedData":null},{"id":"21976","alias":"DIY","type":"collective","title":"DIY или Сделай сам","titleHtml":"DIY или Сделай сам","isProfiled":false,"relatedData":null}],"flows":[{"id":"1","alias":"develop","title":"Разработка","titleHtml":"Разработка"},{"id":"7","alias":"popsci","title":"Научпоп","titleHtml":"Научпоп"}],"relatedData":{"vote":null,"unreadCommentsCount":0,"bookmarked":false,"canComment":false,"canEdit":false,"canViewVotes":false,"votePlus":{"canVote":false,"isChargeEnough":false,"isKarmaEnough":false,"isVotingOver":true,"isPublicationLimitEnough":false},"voteMinus":{"canVote":false,"isChargeEnough":false,"isKarmaEnough":false,"isVotingOver":true,"isPublicationLimitEnough":false},"canModerateComments":false,"trackerSubscribed":false,"emailSubscribed":false},"textHtml":"<div xmlns=\"http://www.w3.org/1999/xhtml\"><p>Впереди 14 февраля. Можно спорить об уместности этого праздника в наших краях, а можно направить энергию в мирное русло. Например, откопать Arduino, щедро обсыпать светодиодами и сформовать их во что-то сердечкоподобное. Неубедительно? Согласен. Давайте так: откопаем в дальней коробке макетку на STM32, забудем, что у нас есть готовые библиотеки и подёргаем регистры, выгрызая каждый байт ROM у злобного компилятора. Потом сделаем всё тоже самое, но без фанатизма, с привлечением CMSIS библиотек и сравним результаты. Возможно даже сделаем выводы. Будет код, надругательство над таблицей векторов. Ардуинка тоже будет, куда ж без неё.</p><h2>Преамбула</h2><p>Статья ориентирована на тех, кто интересуется разработкой под голое железо либо сделал это своей профессией. Любители найдут три способа написать \"Hello world\" на светодиодной панели а профессионалы, возможно, найдут полезными выводы из тех цифр, которые мы получим в итоге сравнения этих трёх подходов. В основном цифры будут касаться объема получившегося кода и могут стать дополнительным аргументом в холиварах \"Регистры vs HAL\".</p><p>Изначально я планировал просто набросать за вечерок что-то интересное для ребёнка. Он сейчас в такой стадии, когда привлекает всё, что мигает и светится. Идея была абсолютно спонтанной и поэтому провалилась. В итоге ребёнок получил светофор на базе конструктора \"Вольтик\", с бонусом от папы в виде автоматического переключения под управлением платой Digispark, а папа получил Nucleo-F103RB с прикрученной матрицей 8×32 красных светодиода и готовым кодом управления этой матрицей по SPI. Кстати, если у кого-то есть идея, какую игрушку можно таки из этой матрицы сделать, прошу поделиться в комментариях. Тетрис не предлагать.</p><p>В итоге родилась демка с сердечками. Хочу поделиться ею с общественностью. Вдруг, кому-то именно такая нужна на предстоящий Валентинов день? Если поторопиться, то ещё есть время сбегать в магазин.</p><h2>Окружение</h2><p>Проект написан в среде Keil MDK-ARM. Свежую версию можно скачать с официального сайта. Отсутствие лицензии накладывает ограничение на выходной файл программы в 32 кБ, но для нашего случая это несущественно. Эту среду часто ругают за убогий редактор кода, отсутствие тем оформления, глючность навигации по коду и пр. Всё это есть, но есть и кое-что ещё – очень удобный менеджер пакетов. Настройка окружения будет состоять из пяти простых шагов:</p><ol><li><p>Скачать Keil MDK-ARM версии 5.37 или выше;</p></li><li><p>Установить с настройками по умолчанию;</p></li><li><p>Скачать проект / клонировать репозиторий с проектом;</p></li><li><p>Открыть в папке проекта файл <code>keilprj/led_string.uvprojx</code>;</p></li><li><p>Подождать, пока среда скачает все необходимые для проекта зависимости.</p></li></ol><p>Всё, окружение готово. Да, чуть не забыл. Это всё справедливо для ОС Windows. Для обладателей linux-машин все не намного сложнее – Keil прекрасно работает в Wine.</p><h2>Железо</h2><p><strong>Нам понадобится:</strong></p><p>Светодиодная матрица на базе MAX7219, состоящая из четырёх последовательно соединённых сегментов, каждый из которых представляет собой микросхему MAX7219 и матрицу 8×8 светодиодов.</p><p>Плата разработчика Nucleo-F103RB. Можно взять что попроще: Blue Pill, например. А то и вовсе голый чип STM32F103 с любыми буквами. Обвязка чипа должна обеспечивать питание и доступ к пинам PA5, PA6, PA7.</p><p><strong>Опционально:</strong></p><p>Пять проводов для arduino-монтажа с разъёмами мама-мама. Рекомендую использовать провода, склеенные в шлейф. На работоспособности это никак не скажется, но смотреться будет аккуратней. Если вы решите использовать blue-pill, например, приклеенный с обратной стороны LED-матрицы, то с проводами думаю разберётесь сами.</p><p>Кабель USB-mini. Нужен на этапе программирования платы Nucleo. Пишу на всякий случай, т.к. кабель из моды вышел и может отсутствовать на вашем рабочем месте. Опять же, если используете внешний программатор – действуйте по обстоятельствам.</p><p>Программатор. ST-Link, J-Link и пр. Это если вы используете вместо nucleo что-то такое, на чём набортный программатор отсутствует.</p><p>Arduino UNO. Если душа к STM не лежит, но хочется посмотреть результат. Подойдёт любая arduino-совместимая плата.</p><p><strong>Сборка</strong></p><p>Принципиальную схему приводить не буду, ограничусь таблицей соединений проводов.</p><div><div class=\"table\"><table><tbody><tr><td data-colwidth=\"217\" width=\"217\"><p align=\"left\">Контакт на LED матрице</p></td><td data-colwidth=\"244\" width=\"244\"><p align=\"left\">CN8, CN9 (CN10) на nucleo</p></td><td><p align=\"left\">Ножки МК</p></td><td><p align=\"left\">Arduino</p></td></tr><tr><td data-colwidth=\"217\" width=\"217\"><p align=\"left\">VCC</p></td><td data-colwidth=\"244\" width=\"244\"><p align=\"left\">5V (8)</p></td><td><p align=\"left\">-</p></td><td><p align=\"left\">5V</p></td></tr><tr><td data-colwidth=\"217\" width=\"217\"><p align=\"left\">GND</p></td><td data-colwidth=\"244\" width=\"244\"><p align=\"left\">GND (9)</p></td><td><p align=\"left\">-</p></td><td><p align=\"left\">GND</p></td></tr><tr><td data-colwidth=\"217\" width=\"217\"><p align=\"left\">DIN (MOSI)</p></td><td data-colwidth=\"244\" width=\"244\"><p align=\"left\">PWM/MOSI/D11 (15)</p></td><td><p align=\"left\">PA7</p></td><td><p align=\"left\">11</p></td></tr><tr><td data-colwidth=\"217\" width=\"217\"><p align=\"left\">CS (SS)</p></td><td data-colwidth=\"244\" width=\"244\"><p align=\"left\">MISO/D12 (13)</p></td><td><p align=\"left\">PA6</p></td><td><p align=\"left\">10</p></td></tr><tr><td data-colwidth=\"217\" width=\"217\"><p align=\"left\">CLK (SCK)</p></td><td data-colwidth=\"244\" width=\"244\"><p align=\"left\">SCK/D13 (11)</p></td><td><p align=\"left\">PA5</p></td><td><p align=\"left\">13</p></td></tr></tbody></table></div></div><h2>Структура проекта</h2><p>Итак, железка лежит на столе, проект открыт. Давайте посмотрим, что там есть интересного. Проект имеет три различных цели сборки (таргета). Итоговый результат – один, но под капотом всё немного по-разному. Начнём разбор с таргета CMSIS_drv. В этом таргете самописными являются только три исполняемых файла: <code>main.c</code> – основная логика приложения, генерация и анимация картинок; <code>max7219.c/h</code> – это драйвер микросхемы MAX7219; <code>spi.c/csp.h</code> – инициализация периферии МК. Из периферии мы настраиваем только модуль SPI. Этот файл используется в связке с заголовочным файлом <code>csp.h</code>. Почему это так, объясню несколькими абзацами ниже.</p><p>По сути – эти три файла олицетворяют три слоя нашего приложения. Main – прикладной уровень. Здесь мы реализуем пользовательские фишки и преобразуем их в команды MAX7219. Никто не мешает нам использовать другие микросхемы для реализации матрицы светодиодов. Мы можем использовать другую микросхему. Можем, например, вообще взять адресные светодиоды на WS2812b. Код прикладного уровня от этого поменяться не должен. Здесь я немного лукавлю. Код из <code>main.c</code> напрямую обращается к функциям драйвера <code>max7219.c</code>. Любой проект можно довести до совершенства. Правда, на это нужно бесконечное время, а до 14 февраля осталось совсем чуть-чуть.</p><details class=\"spoiler\"><summary>Взглянем на код main.c</summary><div class=\"spoiler__content\"><pre><code class=\"cpp\">#include \"stm32f10x.h\"\n#include \"csp.h\"\n#include \"max7219.h\"\n\n#if(1)\n/// @brief Побитное зеркалирование 4-байтного слова\n/// @example 0x80000010 -&gt; 0x08000001\n#define RBIT(dw) __RBIT(dw)\n#else\nuint32_t RBIT(uint32_t in)\n{\n    uint32_t result = 0;\n    \n    for (uint32_t i = 32; i; i--)\n    {\n        result &lt;&lt;= 1;\n        result |= (in &amp; 1);\n        in &gt;&gt;= 1;\n    }\n\n    return result;\n}\n#endif\n\n/// Структура строки изображения\ntypedef union\n{\n    uint32_t dw;\n    uint16_t w[2];\n    uint8_t  b[4];\n} buf_str_t;\n\n/// Половинка \"сердца\"\nconst uint8_t heart[STR_CNT] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x08, 0x88, 0x70};\n\n/// Буфер изображения\nbuf_str_t img_buf[STR_CNT];\n\n/// Загрузить изображение\nstatic void load_img()\n{\n    for (uint32_t i = 0; ++i &lt;= STR_CNT;)\n    {\n        const uint32_t tmp = img_buf[i-1].dw;\n        max7219_send_data(i, tmp | RBIT(tmp));\n    }\n    csp_delay(16);\n}\n\n/// @brief Эффект пульсации (изменение яркости)\n/// @param cnt Количество пульсаций\nstatic void pulse(const uint32_t cnt)\n{\n    for (uint32_t i = cnt; i-- &gt; 0;)\n    {\n        for (uint32_t i = 0; ++i &lt; 0x20;)\n        {\n            max7219_send_cmd(MAX7219_BRIGHTNESS, i ^ (0xF * (i &gt;&gt; 4)));\n            csp_delay(2 &lt;&lt; (i &gt;&gt; 4));\n        }\n        csp_delay(200);\n    }\n}\n\n/** @defgroup Animation Функции анимации\n *  @{ ********************************************************************************************/\n\n/// Сигнатура функции преобразования строки изображения\ntypedef void(*step_t)(const uint32_t, const uint32_t);\n\n/// @brief Применение функции преобразования к изображению\n/// @param j Количество кадров анимации\n/// @param fn_p Функция преобразования строки изображения\nvoid step(int8_t j, step_t fn_p)\n{\n    for (; j &gt;= 0; j--)\n    {\n        for (int32_t i = STR_CNT; --i &gt;= 0;)\n        {\n            fn_p(i, j);\n        }\n\n        load_img();\n    }\n}\n\n/// @brief Функция преобразования \"одно сердце\"\n/// @param i номер строки\n/// @param j номер кадра\nvoid one_heart(const uint32_t i, const uint32_t j)\n{\n    img_buf[i].dw = (uint16_t)heart[i] &lt;&lt; 8 &gt;&gt; j;\n}\n\n/// @brief Функция преобразования \"два сердца\"\n/// @param i номер строки\n/// @param j номер кадра\nvoid dbl_heart(const uint32_t i, const uint32_t j)\n{\n    img_buf[i].dw = ((uint32_t)heart[i] &gt;&gt; j) | (uint32_t)heart[i] &lt;&lt; (16 - j);\n}\n\n/// @brief Функция преобразования \"слияние сердец\"\n/// @param i номер строки\n/// @param j номер кадра\nvoid uni_heart(const uint32_t i, const uint32_t j)\n{\n    (void)j;\n\n    img_buf[i].w[0] &lt;&lt;= 1;\n    img_buf[i].w[1] &gt;&gt;= 1;\n}\n\n/** @} ********************************************************************************************/\n\n/// Инициализация\n__STATIC_FORCEINLINE void init()\n{\n    csp_spi_init();\n\n    csp_spi_nss_inactive();\n\n    // Base initialisation of MAX7219\n    max7219_send_cmd(MAX7219_TEST, 0x00); // 1 - on, 0 - off\n    max7219_send_cmd(MAX7219_SCAN_LIMIT, STR_CNT - 1);\n    max7219_send_cmd(MAX7219_BRIGHTNESS, 0x00); // 0x00..0x0F\n    max7219_send_cmd(MAX7219_DECODE_MODE, 0x00); // 0 - raw data\n    max7219_send_cmd(MAX7219_SHUTDOWN, 0x01); // 1 - active mode, 0 - inactive mode\n}\n\n/// @brief Основная функция\n/// @return Нафиг не нужен, но против стандарта не попрешь\nint main()\n{\n    init();\n\n    step(15, one_heart);\n    pulse(1);\n\n    step(7, dbl_heart);\n    pulse(2);\n\n    step(7, uni_heart);\n\n    // Заливка контурного \"сердца\"\n    for (uint32_t j = 0; ++j &lt; 7;)\n    {\n        img_buf[j].dw |= img_buf[j - 1].dw;\n\n        load_img();\n    }\n\n    // Бесконечно\n    for (;; pulse(1));\n\n    return 0;\n}\n</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p></p></div></details><p>Пробежимся по основным моментам. Для изображения \"сердечка\" нам нужна исходная картинка. Она задаётся массивом <code>heart</code>. Каждый байт этого массива содержит битовую маску строки светодиодов для отображения половины контура \"сердечка\". Т.к. фигура симметрична, вторая половина достраивается программно, путём зеркалирования первой. Для генерации изображения мы используем \"видеобуфер\" <code>img_buf</code> – массив из восьми тридцатидвухбитных значений. Каждый бит соответствует светодиоду в матрице. Для отправки буфера в матрицу используется функция <code>load_img</code>. Я нарочно сделал буфер глобальным, и во всех функциях работаю с ним в явном виде. Мне хотелось посмотреть, насколько компактным может получиться код, в т. ч. и на нулевой оптимизации. Передача указателя в функцию увеличит каждый вызов на 4 байта. В маленьком проекте такое вполне допустимо, мы же экспериментируем. Но никогда не делайте так в коде, за который вам платят деньги.</p><p>Генерация изображений представляет собой некоторые математические действия с данными в <code>img_buf</code>. Анимация может состоять из нескольких кадров, в каждом кадре мы последовательно производим преобразования каждой строки. Таким образом, для отображения одной сцены нам нужно иметь два вложенных цикла. Внешний цикл определяет номер кадра в сцене, а внутренний – номер преобразуемой строки. В конце каждой итерации внешнего цикла вызывается функция <code>load_img</code> для обновления изображения на матрице.</p><p>После нескольких вариантов реализации самым компактным, но всё ещё не потерявшим читабельность вариантом был выбран такой: циклы и механизм обновления изображения завёрнуты в функцию, которая принимает на вход два параметра: количество кадров и указатель на функцию математического преобразования строки. Такой механизм позволил избавиться от многократного копирования вложенных циклов для каждой сцены и на дистанции в три сцены уже дал выигрыш в десяток байт. При этом каждая функция сцены схлопнулась до двух-трёх строчек кода, отображающих суть. Выглядит это примерно так, на примере сцены соединения двух половинок сердечка, появляющихся с двух сторон матрицы:</p><pre><code>/// Сигнатура функции преобразования строки изображения\ntypedef void(*step_t)(const uint32_t, const uint32_t);\n\n/// @brief Применение функции преобразования к изображению\n/// @param j Количество кадров анимации\n/// @param fn_p Функция преобразования строки изображения\nvoid step(int8_t j, step_t fn_p)\n{\n    for (; j &gt;= 0; j--)\n    {\n        for (int32_t i = STR_CNT; --i &gt;= 0;)\n        {\n            fn_p(i, j);\n        }\n\n        load_img();\n    }\n}\n\n/// @brief Функция преобразования \"одно сердце\"\n/// @param i номер строки\n/// @param j номер кадра\nvoid one_heart(const uint32_t i, const uint32_t j)\n{\n    img_buf[i].dw = (uint16_t)heart[i] &lt;&lt; 8 &gt;&gt; j;\n}</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Функция <code>main</code> занимается тем, что инициализирует драйвер, а потом по очереди вызывает сцены с параметрами. В конце она проваливается в бесконечный цикл, в котором бесконечно крутится эффект \"бьющегося сердца\".</p><p>В этом коде ещё есть куда ужаться. Например, можно освободить 4 байта ROM, удалив код возврата функции <code>int main()</code>. Но это уже пахнет подавлением предупреждений компилятора, и я решил, что код от этого потеряет больше, чем приобретёт прошивка. Не могу сказать, что выжал из этого кода каждый байт, но таки постарался сделать так, чтобы оптимизация объёма прошивки стала не слишком тривиальной задачей. На ОЗУ тоже сэкономил, но цифры – в конце.</p><details class=\"spoiler\"><summary>max7219.c – самый неинтересный файл</summary><div class=\"spoiler__content\"><pre><code>#include \"max7219.h\"\n#include \"csp.h\"\n\nvoid max7219_send_cmd(const uint32_t cmd, const uint32_t data)\n{\n    csp_spi_nss_active();\n\n    for (uint32_t i = MATRX_CNT; i-- &gt; 0;)\n    {\n        csp_spi_send((cmd &lt;&lt; 8) | data);\n    }\n\n    csp_spi_nss_inactive();\n}\n\nvoid max7219_send_data(const uint32_t str, const uint32_t data)\n{\n    csp_spi_nss_active();\n\n    for (int8_t i = MATRX_CNT; --i &gt;= 0;)\n    {\n        csp_spi_send((str &lt;&lt; 8) | ((uint8_t *)&amp;data)[i]);\n    }\n\n    csp_spi_nss_inactive();\n}\n</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p></p></div></details><p>В нем только самые необходимые для проекта функции – послать команду на все четыре микросхемы MAX7219 и отобразить строку согласно 32-битной маске, передаваемой в аргументе.</p><p>Дальше нас ждёт настройка железа. А в таргете CMSIS_drv мы настраиваем его, используя все готовые библиотеки, которые предоставляет нам среда разработки. Для того, чтобы увидеть, что же мы там наподключали, заходим в менеджер окружения (Project-&gt;manage-&gt;run-time environment...) и видим такую картину:</p><figure class=\"full-width \"><img src=\"https://habrastorage.org/r/w1560/getpro/habr/upload_files/b10/de5/1b8/b10de51b8d9d08eb4ea5d5711dd47fcd.png\" alt=\"Подключение компонентов CMSIS\" title=\"Подключение компонентов CMSIS\" width=\"646\" height=\"786\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https://habrastorage.org/r/w780/getpro/habr/upload_files/b10/de5/1b8/b10de51b8d9d08eb4ea5d5711dd47fcd.png 780w,&#10;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/b10/de5/1b8/b10de51b8d9d08eb4ea5d5711dd47fcd.png 781w\" loading=\"lazy\" decode=\"async\"/><figcaption>Подключение компонентов CMSIS</figcaption></figure><p>Это всё, что нужно, чтобы завести SPI интерфейс. Ну, почти всё. В дереве проектов находим файл RTE_Device.h и открываем его в редакторе Keil. Этот файл Keil IDE любезно нам сгенерировала. Надо его слегка поправить. Для этого внизу находим вкладку \"Configuration Wizard\" и переключаемся на неё. Вот это да! Файл превратился в визуальное меню конфигураци. Настраиваем выводы SPI, как на картинке:</p><figure class=\"full-width \"><img src=\"https://habrastorage.org/r/w1560/getpro/habr/upload_files/c7f/f7e/21b/c7ff7e21b6b75375e7166e6612b3186b.png\" alt=\"Настройка SPI интерфейса\" title=\"Настройка SPI интерфейса\" width=\"729\" height=\"515\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https://habrastorage.org/r/w780/getpro/habr/upload_files/c7f/f7e/21b/c7ff7e21b6b75375e7166e6612b3186b.png 780w,&#10;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/c7f/f7e/21b/c7ff7e21b6b75375e7166e6612b3186b.png 781w\" loading=\"lazy\" decode=\"async\"/><figcaption>Настройка SPI интерфейса</figcaption></figure><p>Как думаете, сколько кода нам придётся написать ручками, чтобы завести SPI? Вот столько:</p><pre><code>#include \"RTE_Components.h\"\n#include CMSIS_device_header\n#include \"Driver_SPI.h\"\n\nextern ARM_DRIVER_SPI Driver_SPI1; ///&lt; SPI Driver external\n\nARM_DRIVER_SPI *SPIdrv = &amp;Driver_SPI1; ///&lt; SPI Driver pointer\n\nvoid csp_spi_init()\n{\n    /* Initialize the SPI driver */\n    SPIdrv-&gt;Initialize(NULL);\n    /* Power up the SPI peripheral */\n    SPIdrv-&gt;PowerControl(ARM_POWER_FULL);\n    /* Configure the SPI to Master */\n    SPIdrv-&gt;Control(0\n                    | ARM_SPI_MODE_MASTER\n                    | ARM_SPI_CPOL0_CPHA0\n                    | ARM_SPI_MSB_LSB\n                    | ARM_SPI_SS_MASTER_SW\n                    | ARM_SPI_DATA_BITS(16),\n                    1000000);\n\n    SPIdrv-&gt;Control(ARM_SPI_CONTROL_SS, ARM_SPI_SS_INACTIVE);\n}</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Настройка ножек, тактирование портов и пр. CMSIS возьмет на себя.</p><details class=\"spoiler\"><summary>Весь файл spi.c выглядит так.</summary><div class=\"spoiler__content\"><pre><code>#include \"RTE_Components.h\"\n#include CMSIS_device_header\n\n#include \"Driver_SPI.h\"\n\n/// SPI Driver external\nextern ARM_DRIVER_SPI Driver_SPI1;\n\n/// SPI Driver pointer\nARM_DRIVER_SPI *SPIdrv = &amp;Driver_SPI1;\n\nvoid csp_delay(const uint8_t del)\n{\n    for (volatile uint32_t i = 0xFFF * del; i != 0; i--);\n}\n\nvoid csp_spi_init()\n{\n    /* Initialize the SPI driver */\n    SPIdrv-&gt;Initialize(NULL);\n    /* Power up the SPI peripheral */\n    SPIdrv-&gt;PowerControl(ARM_POWER_FULL);\n    /* Configure the SPI to Master */\n    SPIdrv-&gt;Control(0\n                    | ARM_SPI_MODE_MASTER\n                    | ARM_SPI_CPOL0_CPHA0\n                    | ARM_SPI_MSB_LSB\n                    | ARM_SPI_SS_MASTER_SW\n                    | ARM_SPI_DATA_BITS(16),\n                    1000000);\n\n    SPIdrv-&gt;Control(ARM_SPI_CONTROL_SS, ARM_SPI_SS_INACTIVE);\n}\n\nvoid csp_spi_nss_active()\n{\n    SPIdrv-&gt;Control(ARM_SPI_CONTROL_SS, ARM_SPI_SS_ACTIVE);\n}\n\nvoid csp_spi_nss_inactive()\n{\n    while (SPIdrv-&gt;GetStatus().busy);\n    SPIdrv-&gt;Control(ARM_SPI_CONTROL_SS, ARM_SPI_SS_INACTIVE);\n}\n\nvoid csp_spi_send(const uint32_t data)\n{\n    SPIdrv-&gt;Send(&amp;data, 1);\n    while (SPIdrv-&gt;GetStatus().busy);\n}\n</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p></p></div></details><p>Ещё один файл, который любезно сгенерировала нам Keil IDE и на который стоит обратить внимание, это startup_stm32f10x_md.s, ассемблерный файл, в котором происходит первичная инициализация, размечается таблица векторов прерываний и настраивается размер стека и кучи. Кучу мы не используем, а стека нам достаточно 208 байт (0xD0). Видите, какие мы экономные.</p><h2>Прошивка</h2><p>Ну, что же, пора уже заводить этот балаган. Жмём F7, и через пару секунд таргет собран. Подтыкаем в свободный USB разъем нашу Nucleo-F103RB. Жмём Alt+F7 и в открывшемся диалоге выбираем вкладку Debug. Выбираем для нашей отладочной платы дебаггер ST-Link и если всё подключено правильно, можем заливать прошивку. Можно просто нажать F8, прошивка улетит в nucleo и порадует вас анимацией. Если, конечно, вы всё правильно подключили. Если хочется поковыряться во внутренностях МК, тогда вам прямая дорога в отладчик: Ctrl+F5, а там уж как-нибудь сами.</p><div class=\"tm-iframe_temp\" data-src=\"https://embedd.srv.habr.com/iframe/63e141ba396453e034463ca1\" data-style=\"\" id=\"63e141ba396453e034463ca1\" width=\"\" data-habr-games=\"\"></div><h2>Срезаем жирок</h2><p>Если мы посмотрим на вывод линкера, то увидим примерно следующее:</p><pre><code class=\"cpp\">Program Size: Code=9032 RO-data=392 RW-data=20 ZI-data=292</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Меньше десяти килобайт! В условиях, когда приложение фонарика на телефоне может не моргнув LED'ом схавать мегабайт 10 – звучит неплохо. Или плохо? Всё познаётся в сравнении. Поменяем правила игры – откажемся от модного SPI драйвера. Снова заходим в менеджер окружения и отключаем всё лишнее:</p><figure class=\"full-width \"><img src=\"https://habrastorage.org/r/w1560/getpro/habr/upload_files/753/72c/55e/75372c55e5803870a5fa653cc8a60820.png\" alt=\"Всё лишнее обведено красным\" title=\"Всё лишнее обведено красным\" width=\"646\" height=\"786\" sizes=\"(max-width: 780px) 100vw, 50vw\" srcset=\"https://habrastorage.org/r/w780/getpro/habr/upload_files/753/72c/55e/75372c55e5803870a5fa653cc8a60820.png 780w,&#10;       https://habrastorage.org/r/w1560/getpro/habr/upload_files/753/72c/55e/75372c55e5803870a5fa653cc8a60820.png 781w\" loading=\"lazy\" decode=\"async\"/><figcaption>Всё лишнее обведено красным</figcaption></figure><p>Мы всё ещё используем CMSIS, но только нижний уровень – файлы начальной инициализации <code>startup_stm32f10x_md.s</code> и <code>system_stm32f10x.c</code>. Работу с SPI придётся реализовывать самостоятельно. У нас это файл <code>spi.c</code>, помните? Но чтобы всё окончательно запутать, мы оставим <code>spi.c</code> для истории а к его интерфейсу напишем новую реализацию – на регистрах. Назовем это Chip Support Package (помните, что заголовочный файл у нас называется <code>csp.h</code>?). Она получится чуть длиннее, чем при использовании драйвера.</p><details class=\"spoiler\"><summary>Настройка и функции работы с SPI выглядят так:</summary><div class=\"spoiler__content\"><pre><code class=\"cpp\">#include \"stm32f10x.h\"\n\n#define NSS_PORT  GPIOA ///&lt; Адрес порта SS (CS)\n#define NSS_PIN   (6)   ///&lt; Номер пина SS (CS)\n#define SCK_PORT  GPIOA ///&lt; Адрес порта SCK (CLK)\n#define SCK_PIN   (5)   ///&lt; Номер пина SCK (CLK)\n#define MOSI_PORT GPIOA ///&lt; Адрес порта MOSI (DIN)\n#define MOSI_PIN  (7)   ///&lt; Номер пина MOSI (DIN)\n\n/// Port Mode\ntypedef enum\n{\n    GPIO_MODE_INPUT     = 0x00, ///&lt; GPIO is input\n    GPIO_MODE_OUT10MHZ  = 0x01, ///&lt; Max output Speed 10MHz\n    GPIO_MODE_OUT2MHZ   = 0x02, ///&lt; Max output Speed  2MHz\n    GPIO_MODE_OUT50MHZ  = 0x03  ///&lt; Max output Speed 50MHz\n} GPIO_MODE;\n\n/// Port Conf\ntypedef enum\n{\n    GPIO_OUT_PUSH_PULL  = 0x00, ///&lt; general purpose output push-pull\n    GPIO_OUT_OPENDRAIN  = 0x01, ///&lt; general purpose output open-drain\n    GPIO_AF_PUSHPULL    = 0x02, ///&lt; alternate function push-pull\n    GPIO_AF_OPENDRAIN   = 0x03, ///&lt; alternate function open-drain\n    GPIO_IN_ANALOG      = 0x00, ///&lt; input analog\n    GPIO_IN_FLOATING    = 0x01, ///&lt; input floating\n    GPIO_IN_PULL_DOWN   = 0x02, ///&lt; alternate function push-pull\n    GPIO_IN_PULL_UP     = 0x03  ///&lt; alternate function pull up\n} GPIO_CONF;\n\n#define CONF_CLR(pin) (0xF &lt;&lt; ((pin) &lt;&lt; 2))\n#define CONF_SET(pin, conf) ((((conf) &lt;&lt; 2) | GPIO_MODE_OUT50MHZ) &lt;&lt; ((pin) &lt;&lt; 2))\n\n#define BITBANDING_ADDR_CALC(ADDR, BYTE)       \\\n    ((uint32_t)(ADDR) &amp; 0xF0000000UL) +        \\\n    (((uint32_t)(ADDR) &amp; 0x00FFFFFFUL) &lt;&lt; 5) + \\\n    0x02000000UL + ((BYTE) &lt;&lt; 2)\n#define BB_REG(ADDR, BYTE) (*(volatile uint32_t *)(BITBANDING_ADDR_CALC(ADDR, BYTE)))\n\nvoid csp_delay(const uint32_t del)\n{\n    for (volatile uint32_t i = del &lt;&lt; 12; --i;);\n}\n\nvoid csp_spi_init()\n{\n    // GPIO init\n    RCC-&gt;APB2ENR |= RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN;\n    GPIOA-&gt;CRL &amp;= ~(CONF_CLR(NSS_PIN) | CONF_CLR(SCK_PIN) | CONF_CLR(MOSI_PIN));\n    GPIOA-&gt;CRL |= CONF_SET(NSS_PIN, GPIO_OUT_PUSH_PULL) | CONF_SET(SCK_PIN, GPIO_AF_PUSHPULL) | CONF_SET(MOSI_PIN, GPIO_AF_PUSHPULL);\n\n    // SPI1 init MODE_MASTER, CPOL0, CPHA0, MSB_LSB, DATA_16_BITS, Max speed\n    SPI1-&gt;CR1 = SPI_CR1_MSTR  | SPI_CR1_SSI | SPI_CR1_SSM | SPI_CR1_DFF | SPI_CR1_SPE;\n}\n\nvoid csp_spi_nss_active()\n{\n    NSS_PORT-&gt;BRR = (1UL &lt;&lt; NSS_PIN);\n}\n\nvoid csp_spi_nss_inactive()\n{\n    while (BB_REG(&amp;(SPI1-&gt;SR), 7)); // SPI_SR_BSY\n    NSS_PORT-&gt;BSRR = (1UL &lt;&lt; NSS_PIN);\n}\n\nvoid csp_spi_send(const uint32_t data)\n{\n    while (!BB_REG(&amp;(SPI1-&gt;SR), 1)); // SPI_SR_TXE\n    SPI1-&gt;DR = data;\n}</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p></p></div></details><p>Здесь интерес представляет макрос</p><pre><code class=\"cpp\">#define BITBANDING_ADDR_CALC(ADDR, BYTE)       \\\n    ((uint32_t)(ADDR) &amp; 0xF0000000UL) +        \\\n    (((uint32_t)(ADDR) &amp; 0x00FFFFFFUL) &lt;&lt; 5) + \\\n    0x02000000UL + ((BYTE) &lt;&lt; 2)\n#define BB_REG(ADDR, BYTE) (*(volatile uint32_t *)(BITBANDING_ADDR_CALC(ADDR, BYTE)))</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Не то, чтобы он необходим. В данном конкретном случае его использование позволяет сэкономить 4–8 байт. Но он реализует очень интересный механизм, называемый <a href=\"https://developer.arm.com/documentation/ddi0337/h/programmers-model/bit-banding\" rel=\"noopener noreferrer nofollow\">bit banding</a>. Что это за зверь такой? Это такой механизм отображения физической памяти ОЗУ или периферийных регистров на другой диапазон адресов. Но это не простое зеркалирование. В этой новой области адресов каждому физическому биту ОЗУ выделен целый самостоятельный адрес в памяти. Это позволяет обращаться к биту, как к 32-разрядному регистру, используя те же ассемблерные инструкции. При этом в старших разрядах всегда будут нули, а младший разряд – как раз и есть наш бит. Это дико упрощает работу с полями и флагами на уровне ассемблера, что положительно сказывается как на быстродействии, так и на объёме исполняемого кода.</p><p>Ну и зачем так мучаться, спросите вы? А вот зачем:</p><pre><code class=\"cpp\">Before\nProgram Size: Code=9032 RO-data=392 RW-data=20 ZI-data=292\nAfter\nProgram Size: Code=1296 RO-data=260 RW-data=0  ZI-data=240  </code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Ого, ужались в шесть раз. Неплохо, для начала. А что мы ещё можем отрезать? У нас остались ZI данные. Это такие данные, которые инициализируются нулями перед тем как компоновщик передаст управление главной функции – main. Но нам эта инициализация не нужна. А ещё мы не используем прерывания, а в файле <code>startup_stm32f10x_md.s</code> целая здоровенная таблица векторов. Давайте-ка и от этого всего избавимся.</p><h2>Обгладываем кости</h2><p>Отключаем в менеджере окружения всё, кроме CMSIS Core. Мы же не хотим писать хардкорные адреса вместо имён регистров? Заветный <code>startup_stm32f10x_md.s</code> исчезает, а значит, стек, и таблицу векторов нам придётся рисовать самостоятельно. Поехали!</p><p>Структура нашей таблицы векторов будет выглядеть так:</p><pre><code class=\"cpp\">typedef volatile const struct\n{\n    const void *const sp; ///&lt; Указатель на вершину стека\n    int (*main)(void); ///&lt; Вектор сброса\n} vectors_t;</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Первое поле – это указатель на вершину стека, а второе – вектор сброса – единственный, который нам нужен. Без него никак. Стек – это просто массив байтов в ОЗУ, а про указатели на функции вы и так всё знаете. Весь файл <code>startup.c</code> получился таким:</p><pre><code>#include &lt;stdint.h&gt;\n\n/// Размер стека. Должен быть кратен 8 байтам.\n#define STACK_SIZE (0x68)\n\n/// @brief Структура таблицы векторов\ntypedef volatile const struct\n{\n    const void *const sp; ///&lt; Указатель на вершину стека\n    int (*main)(void); ///&lt; Вектор сброса\n} vectors_t;\n\n/// Стек. Должен быть кратен 8 байтам.\nvolatile uint64_t stack[STACK_SIZE / sizeof(uint64_t)];\n\n/// @brief Указатель на вершину стека.\n/// @details Стек растет сверху вниз.\nconst void *const __initial_sp = (void *)((uint32_t)stack + sizeof(stack));\n\n/// @brief Главная функция\nextern int main();\n\n/// @brief Таблица векторов\nvectors_t vectors __attribute__((section(\"reset\"))) =\n{\n    .sp = __initial_sp,\n    .main = main,\n};</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Директива <code>__attribute__((section(\"reset\")))</code> говорит линкеру, что переменная должна располагаться не абы где, а в секции памяти, имеющей имя \"reset\". Это мы так назвали секцию, со стартовым адресом <code>0x08000000</code>. Адрес начала таблицы векторов прерываний в нашем МК. А как линкер сопоставит имя и адрес? А для этого у нас заготовлен кастомный scatter-файл. Который выглядит примерно так:</p><pre><code class=\"cpp\">LR_IROM1 0x08000000 0x00020000  {\n  ER_IROM1 0x08000000 0x00020000  {\n   *.o (reset, +First)\n   .ANY (+RO)\n   .ANY (+XO)\n  }\n  RW_IRAM1 0x20000000 UNINIT 0x5000 {\n   .ANY (+RW)\n   .ANY (+ZI)\n  }\n}</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Подробно разбирать мы его не будем, есть <a href=\"https://developer.arm.com/documentation/dui0474/m/scatter-file-syntax/syntax-of-a-scatter-file\" rel=\"noopener noreferrer nofollow\">всякие мануалы</a> на эту тему. Видите имя секции \"reset\"? Вот эта именованная секция и будет содержать нашу таблицу векторов. Ключевое слово <code>+First</code> говорит о том, что секция будет располагаться первой в регионе <code>ER_IROM1</code>, берущим своё начало по адресу <code>0x08000000</code>. То, что доктор прописал. Да, регион ОЗУ – <code>RW_IRAM1</code> – пометим как <code>UNINIT</code>. Расположенные в нем переменные, а в нашем случае это стек и \"видеобуфер\" – не будут инициализированы нулями или чем либо ещё. Конечно, при старте они могут содержать бог знает что, но мы об этом помним и ручками инициализируем всё что нужно. В нашем случае – ничего.</p><p>F7, погнали. Что мы получили в итоге наших издевательств? Примерно следующее:</p><pre><code class=\"cpp\">Program Size: Code=900 RO-data=20 RW-data=0 ZI-data=136</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Вот это уже хорошо! Не потому, что лучше нельзя. Компилятор говорит, что можно. Выставим оптимизацию по объёму кода и не меняя ни строчки получим:</p><pre><code class=\"cpp\">Program Size: Code=616 RO-data=20 RW-data=0 ZI-data=136</code><div class=\"code-explainer\"><a href=\"https://sourcecraft.dev/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"/></a></div></pre><p>Просто праздник уже близко, а ведь надо ещё за подарками успеть.</p><h2>Выводы</h2><p>Пользовать библиотеки – легко и приятно. Глюков в том же HAL'е я не находил не потому, что их там нет, а потому, что их за меня уже нашли другие. Велосипеды, вроде финального варианта программы из этой статьи – лютое зло, если они попадают на прод. Да, есть исключения, есть супермаленькие камни, древние загрузчики, которые надо впихнуть \"туда, где был, но добавить USB\", есть прерывание, которое должно обрабатывать условия раз в полгода, но делать это за доли микросекунды. В таких ситуациях и ассемблером не грех воспользоваться. Именно для таких редких случаев полезны подобные упражнения. А в остальное время помните, что ваш код будут читать гораздо чаще, чем вы его править. Любите ближних, коллег и проекты, над которыми работаете.</p><p><a href=\"https://github.com/Kudesnick/led_string\" rel=\"noopener noreferrer nofollow\">Ссылка на репозиторий с проектом на github</a>. Там же скетч для Arduino, реализующий то же самое. Объем результирующего кода – 5k5.</p><p></p></div>","tags":[{"titleHtml":"STM32"},{"titleHtml":"CMSIS"},{"titleHtml":"MAX7219"},{"titleHtml":"Arduino"},{"titleHtml":"Keil"},{"titleHtml":"День святого Валентина"},{"titleHtml":"hello world"}],"metadata":{"stylesUrls":[],"scriptUrls":[],"shareImageUrl":"https://habrastorage.org/getpro/habr/upload_files/210/d1f/f86/210d1ff862faf0398a99abf7cc4026ab.jpg","shareImageWidth":1200,"shareImageHeight":630,"vkShareImageUrl":"https://habrastorage.org/getpro/habr/upload_files/210/d1f/f86/210d1ff862faf0398a99abf7cc4026ab.jpg","schemaJsonLd":"{\"@context\":\"http:\\/\\/schema.org\",\"@type\":\"Article\",\"mainEntityOfPage\":{\"@type\":\"WebPage\",\"@id\":\"https:\\/\\/habr.com\\/ru\\/articles\\/709430\\/\"},\"headline\":\"Регистры vs библиотеки на примере сердечек\",\"datePublished\":\"2023-02-07T10:41:32+03:00\",\"dateModified\":\"2023-02-10T03:15:08+03:00\",\"author\":{\"@type\":\"Person\",\"name\":\"Tikhon\"},\"publisher\":{\"@type\":\"Organization\",\"name\":\"Habr\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\/\\/habrastorage.org\\/webt\\/a_\\/lk\\/9m\\/a_lk9mjkccjox-zccjrpfolmkmq.png\"}},\"description\":\"Впереди 14 февраля. Можно спорить об уместности этого праздника в наших краях, а можно направить энергию в мирное русло. Например, откопать Arduino, щедро обсыпа...\",\"url\":\"https:\\/\\/habr.com\\/ru\\/articles\\/709430\\/#post-content-body\",\"about\":[\"h_c\",\"h_controllers\",\"h_DIY\",\"f_develop\",\"f_popsci\"],\"image\":[\"https:\\/\\/habr.com\\/share\\/publication\\/709430\\/81f203376e7f88e06282b1058279c04b\\/\",\"https:\\/\\/habrastorage.org\\/getpro\\/habr\\/upload_files\\/b10\\/de5\\/1b8\\/b10de51b8d9d08eb4ea5d5711dd47fcd.png\",\"https:\\/\\/habrastorage.org\\/getpro\\/habr\\/upload_files\\/c7f\\/f7e\\/21b\\/c7ff7e21b6b75375e7166e6612b3186b.png\",\"https:\\/\\/habrastorage.org\\/getpro\\/habr\\/upload_files\\/753\\/72c\\/55e\\/75372c55e5803870a5fa653cc8a60820.png\"]}","metaDescription":"Впереди 14 февраля. Можно спорить об уместности этого праздника в наших краях, а можно направить энергию в мирное русло. Например, откопать Arduino, щедро обсыпать светодиодами и сформовать их во...","mainImageUrl":null,"amp":true,"customTrackerLinks":[]},"polls":[],"commentsEnabled":{"status":true,"reason":null},"rulesRemindEnabled":false,"votesEnabled":true,"status":"published","plannedPublishTime":null,"checked":null,"hasPinnedComments":false,"format":"case","banner":null,"multiwidget":null,"multiwidgetUuid":null,"readingTime":15,"complexity":null,"isEditorial":false,"flowNew":null,"linkedPostTranslation":null,"hasRegionalRestrictions":false}