[Из песочницы] Начинаем изучать микроконтроллеры на примере STM32F030f4p6
Данная статья преследует следующие цели:
- показать, как работать конкретно с этой платой;
- показать подход, с помощью которого можно написать программу мигания светодиодом, опираясь исключительно на документацию и логику;
- изложить материал языком, понятным человеку, слабо знакомому с микроконтроллерами.
Код получится минималистичным с точки зрения использования дополнительных файлов — мы не будем подключать ни один файл, кроме тех, что нужны для сборки пустой, но валидной, прошивки. Т.е. на базе кода прошивки, которая работает, но ничего полезного не делает.
Нам понадобится следующая документация:
- Datasheet STM32F030×4 (я пользуюсь документом от января 2017 DocID024849 Rev 3);
- RM0360 Reference manual STM32F030×4/x6/x8/xC (я пользуюсь документом от апреля 2017 DocID025023 Rev 4);
- схема платы.
Скачать именно эти документы можно с облака.
Таймер в статье не будет рассмотрен и не будет задействован в коде.
Программатор ST-LINK не использовался. Для работы с платой использовался переходник USB-COM (RS232 на базе PL2303HX), который эмулирует COM-порт.
Всё собиралось на виртуальной машине Windows XP Professional 2002 SP3, запущенной через VirtualBox версии 5.2.22r126460 на хосте Windows X.
Windows не в помощь, качаем с оф.сайта Prolific (первая ссылка на запрос «prolific driver» в Гугле) драйвер USB to UART/Serial/Printer PL2303 Windows Driver (нужен тот, который Standard Driver). Либо можете скачать с моего облака.
Устанавливаем драйвер, перезагружаемся и видим новый COM-порт.
Настройки порта оставил стандартные. Можете изменить номер COM-порта на своё усмотрение. На своём опыте только один раз в жизни видел, чтобы программа видела только первые 4 COM-порта, если не ошибаюсь это был какой-то Bluetooth-терминал под Windows.
2.0 Скачивание утилиты для работы с платой
Качаем с оф.сайта STM утилитку FLASHER-STM32 (в описании зовётся STM32 Flash loader demonstrator (UM0462)), для этого придётся зарегистрироваться, но это не страшно — в итоге нам упадёт zip-архив с установочником; Next→Next→Next… и всё установлено. Я для удобства в рабочей папке создаю ярлык на это приложение.
По умолчанию путь к утилите C: Program Files\STMicroelectronics\Software\Flash Loader Demo\STMFlashLoader Demo.exe.
2.1 BOOT-нюанс
На плате имеется перемычка (джампер) BOOT.
- Когда перемычка замкнута, микроконтроллер будет загружать инструкции из своей памяти (т.е. программу, написанную программистом).
- Когда перемычка разомкнута, микроконтроллер будет принимать информацию по линиям RX и TX, т.е. будет прошиваться от COM-порта (от переходника в моём случае).
2.2 Настройка утилиты
Запустим это приложение, оно на самом деле наипростейшее (содержит минимум настроек). На первом окне выбираем:
- интерфейс (у меня это COM-3);
- скорость, с которой будет общаться компьютер и микроконтроллер (имхо, 9600 нормальное значение);
- количество бит данных (у меня это окно почему-то недоступно, но пока это не важно);
- чётность (у меня без чётности, т.е. None);
- echo (у меня ВЫКЛ);
- время ожидания (у меня 10 секунд).
Жмём Next, и если всё в порядке, то увидим зелёный свет и «Target is readable»; если видим красный свет, то компьютер не смог подключиться.
Порядок шагов, которые помогают всегда:
- Во-первых, нужно проверить не замкнута ли перемычка BOOT на плате.
- Во-вторых, в любом случае, отключить питание микроконтроллера и желательно линии TX и RX, идущие от переходника на плату (землю можно не отключать).
- В-третьих, в программе прожать Back до конца, т.е. до первой странички, или же вообще закрыть её и запустить заново (вообще говоря, она иногда подвисает). Важно перед каждым подключением к плате через эту программу всегда начинать с первой страницы.
- В-четвёртых, подцепить обратно провода от переходника к плате и снова попытаться подключиться в программе (обязательно с первой странички!).
Если ничего не помогло, можно попробовать отключить всё, перезагрузить комп, и попробовать заново подключиться к плате.
Т.к. я работаю через виртуальную машину, приходится по несколько раз переподключать переходник USB-COM, чтобы он был обнаружен виртуальной машиной, а хост-машина не успела установить нерабочие драйвера.
Другой вариант, который я обнаружил рабочим во время написания этой статьи — это нажатие кнопки на плате вместо постоянного дёргания проводов. Однако замыкать и размыкать перемычку BOOT придётся в любом случае. Работает этот вариант, потому что кнопка подведена к ноге внешнего сброса NRST.
На следующем окне нужно выбрать целевое устройство Target. Кстати говоря, иногда здесь можно увидеть (возможно, это баг) вообще левое устройство, например вместо STM32 увидеть STM8 — где-то произошёл какой-то сбой, порядок лечения описан выше. Поэтому на этом шаге нельзя спешить жать Next, а всегда обращать внимание на то, что в Target выбрано нужное устройство.
Как определить, какое у нас устройство? — смотрим на чип и переписываем себе всё, что на нём написано. Открываем Datasheet на наш чип, в разделе Ordering information описано, какая буква за что отвечает. В моём случае это:
Предлагается на выбор 4 действия с чипом:
- стереть память (всю или выбрать конкретную область);
- записать прошивку на устройство;
- считать прошивку с устройства;
- включить/отключить защиту от записи или чтения.
2.3 Считывание прошивки с платы
При самом первом моём подключении платы я решил сохранить исходную прошивку, этакий бэкап — сделаем это и сейчас. Нужно будет указать, куда сохранять эту прошивку и какие страницы памяти сохранять, также на выбор предлагается использовать hex, bin или s19 формат файла.
Если только заливать на плату или считывать с платы прошивку, то разницы между этими форматами файлов никакой нет. Далее следует страница прогресса, на которой у меня иногда надолго подвисает процесс на 99% (не обязательно именно 99), но спустя несколько секунд якобы успешно завершается — на самом деле после этого плата не выдавала то поведение, которое соответствовало бы загружаемой прошивке. Проще говоря, надо всё переподключить и заново залить прошивку, ничего критичного в этом нету.
Файл прошивки сохранился, и в будущем его можно заливать на плату.
Однако, если установлена защита от чтения, считать прошивку не получится.
2.4 Прошивание платы
Сейчас зальём файл прошивки, написание исходного кода которой приведено ниже. Забегая вперёд скажу, что заливать будем bin и hex файлы, т.к. именно их будет выдавать среда разработки. Дополнительные настройки для s19 и hex файлов идентичны; в отличие от них у bin файла можно выбрать адрес, с которого будет записываться прошивка, по умолчанию в утилите он равен 8000000 (нам подходит).
Перед записью можно очистить Flash память микроконтроллера, выбрав один из трёх вариантов:
- Erase necessary pages (очистить необходимые участки памяти);
- No Erase (без очистки);
- Global Erase (полная очистка).
На самом деле очистка — это процесс записи нулей в память.
Ещё есть опциональные байты, но пока их можно не трогать. Жмём Next, ждём завершения процесса и всё готово.
На случай, если хотите записать мою прошивку, найти её можно в облаке, файл blink.bin. При использовании данной прошивки должен мигать встроенный светодиод, запитанный от ножки PA4.
3.0 Установка среды разработки CooCox CoIDE
Скачать IDE можно с сайта SoftPedia.com, раньше можно было скачать с сайта STM и с сайта самой IDE, но с тех пор, как IDE перестали поддерживать, это стало невозможным. Ничего критичного в том, что IDE перестали поддерживать, нету, т.к. для написания кода главное — это компилятор. Я скачал обе версии, но пользуюсь версией 1.7.8.
Первый запуск среды хорошо описан здесь, Next→Next→Next… и ничего сложного. Добавлю только, что сначала лучше создавать проект, а потом всё остальное.
И ещё, если потеряли вкладку Repository, найти её можно в меню View → Repository.
Скачать инструменты (компилятор) для среды можно тут или спросить у Гугла «gnu tools for arm»; я скачивал вариант, у которого на конце sha1.exe.
3.1 Каркас исходников
Итак, проект создан, чип выбран, теперь добавим в проект минимальный набор исходников, без которых он вообще не сможет жить.
Выделим CMSIS BOOT и среда автоматически выделит M0 Cmsis Core, т.к. зависимости требуют этого.
Соберём проект (значок Build, или клавиша F7). По непонятным мне причинам hex файл не собрался (в консоли есть предупреждение); я несколько раз переустанавливал IDE и компилятор, заново создавал проект, но на виртуальной машине почему-то такой результат; на другом компьютере (не виртуальном, а реальном) всё один-в-один и на выходе рабочий hex. К счастью, есть bin.
Хоть код и ничего не делает, я заливаю его на плату, дабы убедиться, что залить можно (что, например, утилита его не отбраковывает). Советую сделать это и читателю. Если не получается — попробовать ещё и ещё раз, а также писать комментарии.
3.2 Алгоритм на пальцах
Для начала набросаем алгоритм, как с точки зрения человека микроконтроллер будет мигать светодиодом. А для этого немного рассуждений.
Каждая техника работает за счёт запасённой энергии, например некоторые двигатели могут работать на разных видах топлива, но для этого двигатель нужно скорректировать под вид топлива, которым мы собираемся его кормить. Аналогично и микроконтроллер требуется скорректировать (настроить) под источник энергии — это будет первый блок алгоритма.
Рассуждаем далее. У настольного компьютера есть монитор, колонки, клавиатура, мышь… и можно заметить, что некоторые устройства предоставляют нам информацию, а с помощью других мы предоставляем информацию компьютеру, но все они подключены к общей для всех них коробке (системному блоку). Можно догадаться, что и микроконтроллер может получать и отдавать информацию, а это значит, что его ножки могут принимать сигнал или выдавать сигнал — это будет следующий блок алгоритма.
Далее микроконтроллер должен включать светодиод, ждать какое-то время, выключать светодиод, ждать какое-то время и заново включать-ждать-выключать…
В итоге алгоритм будет выглядеть примерно так
Назначение данной блок-схемы — наглядно показать, что делает алгоритм; в первую очередь схема пишется для себя, поэтому каждый волен писать\рисовать её как хочет (для себя). Я считаю, что схема должна преследовать цель быть максимально простой, удобочитаемой и наглядной, иметь высокий уровень абстракции.
В соответствии с этим алгоритмом будем писать код.
3.3 Работа с документацией
Данную часть статьи рекомендую читать с открытым файлом stm32f0xx.h, который лежит в папке cmsis_boot нашего проекта, и открытой документацией.
3.3.1 Выбор источника тактирования
Во-первых, нужно обеспечить питание микроконтроллера. Микроконтроллер получает от переходника 5 Вольт (замерял мультиметром), однако возникает вопрос «на какой частоте работает микроконтроллер», ведь известно, что электроника работает на разных частотах. Сперва откроем datasheet, в содержании можно увидеть два раздела, подходящие по смыслу: Power management, Clocks and startup. В первом идёт речь о вольтаже и о режимах низкого энергопотребления. Во втором разделе прячется то, что нам интересно в данный момент. Уже в самом первом предложении сказано «the internal RC 8 MHz oscillator is selected as default CPU clock on reset», что значит, что по умолчанию после сброса МК в качестве основного источника тактирования выбирается внутренняя 8-ми МГц RC-цепочка.
Далее идёт какая-то непонятная схема Clock tree, которую мы рассмотрим чуть позже.
Строго говоря, можно положиться на фразу «по умолчанию после сброса МК…» и прочитать по диагонали данную часть статьи.
Сейчас нужно отвлечься на плату и поискать внутренний светодиод. Мне известно, что диоды на схемах обозначаются D1, D2…, т.е. D == diode, на моей плате недалеко от резистора R7 находится диод D1.
Возможно, внимательно рассмотрев плату вы сможете проследить, к какой ножке подцеплен диод, я же обращусь к схеме платы. К сожалению, элементы платы не в точности соответствуют элементам на схеме по своему расположению;, но я рад, что нашёл такую схему в интернете (а то вообще долго не мог найти ничего).
На схеме видим, что катод диода через перемычку J2 подцеплен к земле, а анод через резистор подцеплен к выводу PA4. PA4 означает 4-ый вывод порта A, а это значит, что для зажигания и отключения светодиода надо будет подавать напряжение на вывод PA4.
Далее нужно определить, каким образом подать напряжение на этот вывод. Для меня это было вовсе не интуитивно, и я долго бороздил документацию вдоль и поперёк, пока не наткнулся в самом начале даташита на схему Block diagram в разделе Description. И в ней я увидел заветную дорожку PA[15:0] <=> GPIO port A <=> AHB decoder <=> Bus matrix <=> Cortex-M0, т.е. порт А является портом ввода-вывода общего назначения и подключен к шине AHB.
Отмечу, что в электронике принято разбивать выводы микроконтроллера на порты, и обычно у порта имеется 16 выводов. На диаграмме видно, что у портов A, B и C их как раз 16, а вот у портов D и F их меньше (меньше 16-ти выводов может быть, больше — нет).
Вернёмся к схеме Clock tree и найдём вывод, подписанный AHB. Разберёмся, на какой частоте работает данный вывод. К AHB идёт сигнал HCLK, который выходит из делителя HPRE. На этот делитель поступает сигнал SYSCLK с переключателя SW. Программно задаётся то, какой из сигналов на входе SW будет использоваться в качестве SYSCLK — далее мы это зададим в коде. На выбор предлагается:
- HSI — сигнал с внутреннего высокочастотного генератора, его выдаёт 8 МГц кварцевый резонатор, который я впаивал перед работой с этой платой;
- PLLCLK — сигнал с множителя частоты PLLMUL;
- HSE — сигнал с внешнего высокочастотного генератора.
Для нашей задачи подойдёт любой вариант, я предлагаю выбрать самый простой и доступный из них — HSI.
Перейдём в Reference manual и откроем раздел 7 Reset and clock control (RCC), а конкретно 7.2.6 System clock selection, где ещё раз натыкаемся на похожую формулировку, встретившуюся в даташите: «after a system reset, the HSI oscillator is selected as system clock» — т.е. нам даже ничего делать не надо, МК сам заведётся на HSI.
Чтобы убедиться, что МК действительно будет работать от этого источника, я пропишу это явно в программе; пролистываем до регистров, которые отвечают за сброс и тактирование (раздел 7.4 RCC registers). Первый регистр, описываемый в документации, это Clock control register (RCC_CR); ниже идёт описание битов, какой за что отвечает.
Нас интересует нулевой бит HSION, который отвечает за включение резонатора (0 — выключен, 1 — включен).
Таким образом, необходимо будет записать единицу в регистр RCC_CR. (нулевой бит это единица, или 20 = 1).
Теперь найдём в файле stm32f0xx.h определение RCC (#define RCC).
Как видим, это структура, расположенная по адресу RCC_BASE; адрес 0×40021000, если развернуть все define, тот же самый адрес можно увидеть в Reference manual в разделе 2.2.2 Memory map and register boundary adresses и в даташите в разделе 5 Memory mapping (область AHB).
Чтобы записать в регистр CR блока RCC единицу для включения HSI, понадобится строка кода
RCC->CR |= 0x1;
3.3.2 Настройка ножек
Подача сигнала на ножку микроконтроллера для зажигания светодиода и прекращение подачи сигнала для того, чтобы светодиод потух, являются простыми действиями, а потому это относится к функциям GPIO (порты ввода-вывода общего назначения).
По умолчанию ножки МК не подключены, т.е. на выходе неопределённость. Необходимо подключить порт, ножка которого будет питать светодиод. Ранее мы определили, что порты GPIO подключены к шине AHB — нужно затактировать эту шину. Продолжая листать раздел 7.4 RCC registers (регистры управления сброса и контроля), встречаем раздел 7.4.6 AHB peripheral clock enable register (RCC_AHBENR, регистр включения тактирования шины AHB). Ранее я определил, что мой светодиод подключен к ножке PA4 — соответственно мне нужно записать единицу в 17-ый бит регистра, чтобы затактировать порт A.
Соответственно код должен быть
RCC->AHBENR |= (1 << 17);
или, что то же самое,
RCC->AHBENR |= 0x20000;
либо используя #define файла stm32f0xx.h написать
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
Порт A мы запитали, теперь надо сообщить МК, что PA4 будет работать на выход — будем читать раздел 8 General-purpose I/Os (GPIO); во введении раздела уже сказано «Each general-purpose I/O port has four 32-bit configuration registers (GPIOx_MODER, GPIOx_OTYPER, GPIOx_OSPEEDR and GPIOx_PUPDR), two 32-bit data registers (GPIOx_IDR and GPIOx_ODR)…» — у каждого GPIO порта есть 4 настроечных регистра и 2 регистра данных — это то, что нам надо (сконфигурировать порт A, а точнее вывод PA4, и периодически отправлять на него 0 и 1). Для лучшего понимания (теории) происходящего можно почитать данный раздел, я же пролистаю до раздела 8.4 GPIO registers и сконфигурирую порт в соответствии с описаниями.
- режим порта — выход. В соответствии с документацией необходимо записать 01 в соответствующую область (MODER4) соответствующего регистра (GPIOA_MODER), т.е. биты 9 и 8: в 9-ом бите должен оказаться ноль, в 8-ом единица:
GPIOA->MODER |= (1 << 8); // или
GPIOA->MODER |= 0x100; // или
GPIOA->MODER |= GPIO_MODER_MODER4_0;
GPIO port mode registerGPIOA→MODER - тип выхода. Честно говоря, я до сих пор до конца не разобрался в схемотехнике этого дела (буду разбираться, ещё раз перечитывать форумы и проч.), но изучение других ресурсов по теме конфигурации выхода МК, а также логика и интуиция подсказывают, что здесь должен быть push-pull и далее должен быть pull-up. Во всяком случае код написан, всё работает и ничего не сгорело. Есть реальный риск сжечь, если выбрать тип open-drain и закоротить этот вывод с другим устройством, т.к. это открытый выход и он ничем не защищён. К тому же у нас имеется токоограничительный резистор перед диодом — тут уж точно не сгорит.
Следуя документации, необходимо записать ноль в 4-ый бит; также в документации указано, что после сброса здесь будет ноль.
GPIOA->OTYPER &= ~(1 << 4); // или
GPIOA->OTYPER &= ~0x10; // или
GPIOA->OTYPER &= ~GPIO_OTYPER_OT_4;
GPIO port output type register - скорость вывода. В нашем случае не играет значения, но для верности запишу сюда ноль.
GPIOA->OSPEEDR &= ~(1 << 8); // или
GPIOA->OSPEEDR &= ~0x100; // или
GPIOA->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR4_0;
GPIO port output speed register - подтяжка. Т.к. вывод будет питать светодиод, необходимо подтянуть к питанию, т.е. pull-up.
Нужно подтянуть 4-ый вывод порта A; документация говорит, что для этого необходимо записать в 9 и 8 биты ноль и единицу соответственно.
GPIOA->PUPDR |= (1 << 8); // или
GPIOA->PUPDR |= 0x100; // или
GPIOA->PUPDR |= GPIO_PUPDR_PUPDR4_0;
GPIO port pull-up/pull-down registerGPIOA→PUPDR
3.3.3 Включение-выключение светодиода и задержка
Ранее мы прочитали, что у каждого порта есть регистры, в том числе регистры данных IDR и ODR — регистры входных и выходных данных соответственно. Логические нули и единицы на ножке МК — это данные? — да, данные. Данные могут приходить в микроконтроллер извне (быть входными) и выходить из микроконтроллера и идти на другое устройство (быть выходными). Единица на ножке МК — это наличие высокого уровня напряжения, т.е. если вывести единицу на выход, то будет напряжение, и этим напряжением можно питать наш светодиод. Вывод единицы на ножку микроконтроллера — нечто иное, как запись этой единицы в регистр выходных данных ODR.
По документации видим, что для каждого порта (A, B, C, D, F) есть 32-битный регистр, однако т.к. у порта не может быть более 16 выводов, то используются только первые 16 бит регистра. Каждый бит соответствует номеру порта (выводу). Для вывода единицы на ножку PA4 нужно записать единицу в 4-ый бит, для вывода нолика — записать ноль в 4-ый бит, т.е. убрать напряжение с вывода.
Код для включения светодиода будет выглядеть так
GPIOA->ODR |= (1 << 4); // или
GPIOA->ODR |= 0x10; // или
GPIOA->ODR |= GPIO_ODR_4;
Код для выключения светодиода
GPIOA->ODR &= ~(0 << 4); // или
GPIOA->ODR &= ~0x10; // или
GPIOA->ODR &= ~GPIO_ODR_4;
Но если написать строку выключения светодиода вслед за строкой включения, то мигать светодиод не будет (если интересно, что произойдёт — можете попробовать; ничего не сгорит, это уже обговаривали выше) — значит нужно сделать задержку. Для задержки используются таймеры, но таймеры достойны отдельной статьи (ввиду сложности), поэтому мы сделаем костыльную задержку: будем гонять холостой цикл. Есть один момент: если включена оптимизация компилятора, то компилятор вырежет наш холостой цикл, и задержки не будет. Убедимся, что оптимизация не включена. Для этого зайдём в конфигурацию проекта (правой кнопкой на имя проекта в дереве проекта) и во вкладке Compile проверим строку Compile Control String: в ней должен быть аргумент -O0 («о ноль» значит, что оптимизация отключена). Если вы собирали всё по моей инструкции, то скорее всего у вас тоже будет -O0, т.к. так было по умолчанию и я здесь ничего не трогал. Аргументы -O1 -O2 -O3 означают что включена оптимизация соответствующего уровня.
Холостой цикл можно написать так:
int t = 4000000;
while(t > 0) t--;
Значение t я задал таковым неслучайно, рассуждал так: если микроконтроллер будет работать на 8MHz, то за секунду он выполнит предположительно 8000000 инструкций, если глубоко утрировать, то для полсекундной задержки понадобится прогнать цикл 4000000 раз.
Холостой цикл нужно будет прогонять и после включения светодиода, и после выключения, и всё вместе это зациклить.
3.4 Написание кода и запуск
Соберём вместе все строки кода, что мы до этого писали. Также надо подключить заголовочный файл stm32f0xx.h, т.к. мы опирались на него и брали из него определения структур, адресов и значений. В итоге должно получиться:
#include "stm32f0xx.h"
int main(void)
{
int t; // для 'таймера'
RCC->CR |= 0x1; // тактировать от HSI
RCC->AHBENR |= RCC_AHBENR_GPIOAEN; // затактировать порт A
GPIOA->MODER |= GPIO_MODER_MODER4_0; // PA4 как выход
GPIOA->OTYPER &= ~GPIO_OTYPER_OT_4; // тип push-pull для PA4
GPIOA->OSPEEDR &= ~GPIO_OSPEEDER_OSPEEDR4_0; // низкая скорость для PA4
GPIOA->PUPDR |= GPIO_PUPDR_PUPDR4_0; // режим pull-up для PA4
while(1)
{
GPIOA->ODR |= GPIO_ODR_4; // включить светодиод на PA4
t = 4000000;
while(t > 0) t--; // ждать
GPIOA->ODR &= ~GPIO_ODR_4; // выключить светодиод на PA4
t = 4000000;
while(t > 0) t--; // ждать
}
}
Жмём Rebuild и заливаем код на плату через утилиту.
Чтобы плата запустила новую прошивку, не забудьте замкнуть перемычку BOOT и сделать сброс (RESET).
Код написан, всё работает. Сил потрачено немерено. Радует, что опираясь на документацию, получилось написать рабочий код, во многом благодаря тому, что у STM качественная документация.
В планах написать статью, как всё собрать руками, без IDE, через консоль, true oldschool, в идеале так, чтобы всё это делать из-под Linux. Сейчас работаю над ШИМ и АЦП (тоже на данной плате) — по ним тоже напишу статью.