В интернете куча статей о том, как мигать светодиодом на esp8266. Предлагаем рассмотреть ту же задачу, но на альтернативном микроконтроллере - stm32.
Перед вами небольшое руководство, в котором описано, как зажечь светодиод с помощью микроконтроллера STM32, настроив контакты GPIO. В посте разберём основы регистров микроконтроллера и как ими манипулировать напрямую. Также здесь вы найдёте пошаговое руководство по написанию кода на ассемблер и на C для управления светодиодом.
Я работал над драйвером для массива из 110 светодиодов Чарлиплексинг с таймером обратного отсчета. Использовался микроконтроллер STM32L010K4. Это прочный чип с ультранизким энергопотреблением 32 МГц, с 16 КБ флэш-памяти и колоссальными 2 КБ ОЗУ.
Я выбрал его в основном из-за того, что он был первым в огромной линейке микроконтроллеров STM32. Тогда я думал, что на чипе с наименьшим количеством периферийных устройств мне будет проще обучаться. Я спроектировал светодиоды так, чтобы они располагались по кругу и двум дугам. Внутренний круг представляет минуты, средняя дуга - часы, а внешняя - дни:
Задним умом я думаю, что мог бы добавить чип драйвера светодиода (например, IS31FL3746A ) и избавить себя от кучи проблем, но на тот момент один микроконтроллер казался более простым решением. И теперь, когда я разложил печатную плату (сначала вручную на макетной плате, с более чем 300 паяными соединениями, но это совсем другая история) и запрограммировал работающий драйвер, то понял, что отдельный чип для этой цели не нужен. В общем, я рад, что для первого раза взял именно такое решение.
Управление светодиодами
При таком раскладе светодиод включается переключением бита в очень специфической ячейке памяти. Это бит, который соответствует одному из контактов ввода-вывода общего назначения GPIO микроконтроллера. На рисунке ниже отмечены те два контакта, к которым подключен мой первый светодиод. В моём случае это были контакты 7 и 14 , в таблице данных они обозначены как контакты PA1 и PB0 соответственно; что в свою очередь означает, что они находятся в группе контактов GPIOA и GPIOB соответственно. Всё это позволит нам найти их адрес.
В справочном руководстве, которое поставляется вместе с микроконтроллером (страница 40 из 784), есть карта памяти, показывающая, что порты ввода/вывода контроллера (IOPORTS) находятся где-то между 0x50000000 и 0x50001FFF .
А на странице ниже мы можем заметить, что GPIOA начинается с 0x5000 0000, а GPIOB - с 0x5000 0400:
Чтобы подключить светодиод с анодом (+), подключенным к PB0 (GPIOB / контакт 14 ), и катодом (-), подключенным к PA1 (GPIOA / контакт 7 ), мы должны убедиться, что PB0 посылает напряжение (в моем случае 3,3 В), а PA1 выступает в качестве земли. Сначала мы настраиваем режим этих контактов на «режим вывода общего назначения»(1) , а затем переключаем бит, соответствующий контакту 14, в регистре установки/сброса битов (BSRR) GPIOB (2). Позже я объясню, как это работает, но, если коротко, эти три шага таковы:
1. 0x50000000 ← 0xEBFFFCF7
0x50000400 ← 0xFFFFFFFD
2. 0x50000418 ← 1
Первый вопрос, который у вас может возникнуть: Откуда взялись 0xEBFFFCF7 (E:1110 B:1011 F:1111 F:1111 F:1111 C:1100 F:1111 7:0111), 0xFFFFFFFD (F:1111 F:1111 F:1111 F:1111 F:1111 F:1111 D:1101) и 1. Чтобы ответить на этот вопрос, ниже приведен еще один фрагмент из справочного руководства. В нем показаны биты, которые необходимо установить для настройки режима GPIOA и GPIOB:
Давайте сначала разберемся с 0xFFFFFFFD (значение GPIOB_MODER). F в шестнадцатеричном формате, конечно, будет 1111 , а D — это 1101, поэтому 0xFFFFFFFD (F:1111 F:1111 F:1111 F:1111 F:1111 F:1111 F:1111 D:1101) — это:
Видите ли, все контакты, за исключением 0-го, установлены в положение «11 : Аналоговый режим (состояние сброса)», а контакт 0 GPIOB — в положение « 01 : Режим вывода общего назначения».
0xEBFFFCF7 (значение GPIOA_MODER), с другой стороны, использует ту же идею, только вместо полностью аналогового режима GPIOA запускается в другом состоянии сброса. Посмотрите на рисунок 8.4.1 выше, обратите внимание, что под заголовком, выделенным жирным шрифтом, написано: «Значение сброса: 0xEBFF FCFF для порта A», потому что некоторые контакты по умолчанию установлены в аналоговый режим (14 и 13) и режим ввода (4), чтобы включить программирование и отладку микроконтроллера на определенных контактах. И мы устанавливаем контакт 1 в режим «выход общего назначения». Итак, 0xEBFFFCF7 (E:1110 B:1011 F:1111 F:1111 F:1111 C:1100 F:1111 7:0111):
Возможность одновременной настройки режимов шестнадцати выводов, на мой взгляд, довольно хороша, но поначалу ее нелегко понять.
Наконец, 1 (in 0x50000418 ← 1) устанавливается в регистр установки и сброса битов (BSRR) для отправки напряжения на 0-й вывод GPIOB:
Обратите внимание на «Adress offset: 0x18» сверху. Зная, что GPIOB находится по адресу 0x50000 400 + 0x 18 , мы получаем, что BSRR для GPIOB равен 0x50000418. Таким образом, чтобы «установить» 0-й бит в 1, мы должны записать 1 в память по этому адресу.
Вот вся последовательность сборки :
ldr r0, =0x50000000 // load the GPIOA address into register r0
ldr r1, =0xEBFFFCF7 // load the mode for GPIOA into register r1
str r1, [r0, #0x00] // write value of r1 into address at r0
ldr r0, =0x50000400 // same as above but for GPIOB
ldr r1, =0xFFFFFFFD
str r1, [r0, #0x00]
ldr r1, =1 // load 1, which is pin 0 in PB0, into r1
str r1, [r0, #0x18] // write that 1 into GPIOB with BSRR offset of 18
И в Си :
*(volatile uint32_t *)(0x50000000) = 0xEBFFFCF7;
*(volatile uint32_t *)(0x50000400) = 0xFFFFFFFD;
*(volatile uint32_t *)(0x50000418) = 1;
Но, скорее всего, вы захотите, использовать поверх CMSIS (Cortex Microcontroller Software Interface Standard), что сделает код намного более читабельным:
#include "stm32l010xb.h"
void turnOnLED() {
GPIOA->MODER = 0xEBFFFCF7;
GPIOB->MODER = 0xFFFFFFFD;
GPIOB->BSRR = 1;
}
И чтобы облегчить нам жизнь (и избежать вычисления этого шестнадцатеричного значения самостоятельно) , STM поддерживает библиотеку под названием HAL (Hardware Abstraction Layer) . Вот как это выглядит:
#include "stm32l0xx_hal.h"
void turnOnLED() {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
}
Для полноты картины добавлю, что вам придется проделать еще немного работы, прежде чем вышеуказанный код заработает, а именно настроить и включить тактовые генераторы, управляющие GPIOA/B; но эта часть выполняется либо генератором кода/инструментом начальной загрузки, который поддерживает STM и называется STM32CubeMX (я предпочитаю использовать его вместе с VSCode) , либо их IDE STM32CubeIDE .
Вот и все: мы устанавливаем три значения в трех очень конкретных разделах памяти, и это посылает 3,3 В на один контакт, а другой действует как земля. Получилось как-то так:
Спасибо за внимание!
Автор: Cloud4Y