Помигаем светодиодом!
Поскольку микроконтроллеры STM32 — настоящие 32-битные ARM-ядра, сделать это будет непросто. Здесь всё сильно отличается от привычных методов в PIC или AVR, где было достаточно одной строкой настроить порт на выход, а второй строкой — вывести в него значение — но тем интереснее и гибче.
Архитектура STM32
Подробно архитектура микроконтроллеров расписана в статье, однако напомню основные положения, интересные нам сейчас.
Ядро тактируется кварцем, обычно через ФАПЧ. Это — тактовая частота ядра, или SYSCLK. На плате STM32VLDiscovery установлен кварц на 8 МГц, а ФАПЧ в большинстве случаев настраивается как умножитель на 3 — т.е. SYSCLK на плате STM32VLDiscovery обычно равен 24 МГц.
От ядра отходит шина AHB, имеющая свою тактовую частоту — ей можно установить некий прескалер относительно SYSCLK, однако можно оставить его равным единице. Эта шина подобна шине между процессором и северным мостом компьютера — точно так же она служит для связи ARM ядра и процессора периферии, а также на ней висит память и конечно, контроллер DMA.
К шине AHB подключены две периферийных шины — APB1 и APB2. Они равнозначны, просто обслуживают разные контроллеры интерфейсов. Частоты обоих шин APB1 и APB2 можно задавать собственными прескалерами относительно AHB, но их тоже можно оставить равными единице. По умолчанию после запуска микроконтроллера вся периферия на шинах APB1 и APB2 отключена в целях экономии энергии.
Интересующие нас контроллеры портов ввода-вывода висят на шине APB2.
Модель периферии в STM32
Вся периферия микроконтроллеров STM32 настраивается по стандартной процедуре.
- Включение тактирования соответствующего контроллера — буквально, подача на него тактового сигнала от шины APB;
- Настройки, специфичные для конкретной периферии — что-то записываем в управляющие регистры;
- Выбор источников прерываний — каждый периферийный блок может генерировать прерывания по разным поводам. Можно выбрать конкретные «поводы»;
- Назначение обработчика прерываний;
- Запуск контроллера.
Если прерывания не нужны — шаги 3 и 4 можно пропустить.
Вот, к примеру, инициализация таймера (указаны шаги из последовательности):
/* 1 */ RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
/* 2 */ TIM6->PSC = 24000;
TIM6->ARR = 1000;
/* 3 */ TIM6->DIER |= TIM_DIER_UIE;
/* 4 */ NVIC_EnableIRQ(TIM6_DAC_IRQn);
/* 5 */ TIM6->CR1 |= TIM_CR1_CEN;
Контроллер портов ввода-вывода
Наконец-то подобрались к основной теме статьи.
Так устроена одна нога ввода-вывода микроконтроллера STM32F100:
Выглядит сложнее, чем в PIC или AVR
На входе стоят защитные диоды, не дающие опустить потенциал ножки ниже земли или поднять его выше напряжения питания. Следом установлены управляемые подтягивающие резисторы — по желанию ножку можно подтянуть к земле или к питанию. Однако нужно помнить что эти подтяжки довольно слабые.
Вход
Рассмотрим «вход». Сигнал напрямую идёт в линию «Analog», и если ножка настроена как вход АЦП или компаратора — и если эти блоки есть на этой ножке — сигнал напрямую попадает в них. Для работы с цифровыми сигналами установлен триггер Шмитта (это тот, который с гистерезисом), и его выход попадает в регистр-защёлку входных данных — вот теперь состояние ножки можно считать в программе, читая этот регистр (кстати, он называется IDR — input data register). Для обеспечения работы не-GPIO-периферии, висящей на этой ножке как на входе — сделан отвод под именем «Alternate function input». В качестве этой периферии может выступать UART/USART, SPI, USB да и очень многие другие контроллеры.
Важно понимать, что все эти отводы одновременно включены и работают, просто к ним может быть ничего не подключено.
Выход
Теперь «выход». Цифровые данные, записанные в порт как в выход, лежат в регистре ODR — output data register. Он доступен как на запись, так и на чтение. Читая из ODR, вы не читаете состояние ножки как входа! Вы читаете то, что сами в него записали.
Здесь же — выход от не-GPIO-периферии, под названием «Alternate function output», и попадаем в Output driver. Режим работы выхода с точки зрения схемотехники настраивается именно здесь — можно сделать пуш-пулл выход (линия жёстко притягивается к земле или питанию), выход с открытым коллектором (притягиваем линию к питанию, а землю обеспечивает что-то внешнее, висящее на контакте) или вовсе отключить выход. После драйвера в линию входит аналоговый выход от ЦАП, компаратора или ОУ, и попадаем снова в подтягивающие резисторы и диоды.
Драйвер цифрового выхода имеет также контроль крутизны, или скорости нарастания напряжения. Можно установить максимальную крутизну, и получить возможность дёргать ногой с частотой 50 МГц — но так мы получим и сильные электромагнитные помехи из-за резких звенящих фронтов. Можно установить минимальную крутизну, с максимальной частотой «всего» 2 МГц — но и значительно уменьшить радиопомехи.
На картинке можно заметить ещё один регистр, «Bit set/reset registers». Дело в том, что можно писать напрямую в регистр ODR, а можно использовать регистры BRR/BSRR. На самом деле, это очень крутая фича, о которой я расскажу дальше.
Возможности
Сейчас всё стало похоже на хаос — неясно, как управлять всеми этими возможностями. Однако нет, контроллер порта отслеживает возможные режимы работы выхода, и исключает неверные комбинации — например, он не даст одновременно работать в одну выходную линию и драйверу цифрового выхода, и аналоговому выходу. Зато наличие такого количества настроек даёт обширные возможности.
Например, в более старших сериях можно настроить выход с открытым коллектором, и включить подтяжку к земле. Получается именно то, что нужно для шины 1-Wire. Правда, в серии STM32F1xx такой возможности нет, и нужно ставить внешний резистор подтяжки.
Атомарные операции
В старых микроконтроллерах часто возникала ситуация — если мы хотим изменить какие-то биты в порту (а на самом деле просто включить или выключить ножку) — нам приходилось читать весь регистр порта, устанавливать/сбрасывать в нём нужные биты и записывать обратно. Всё было хорошо до того момента, когда эту операцию посередине не прерывало прерывание. Если обработчик этого прерывания тоже что-то делал с этим же портом — возникала крайне трудноуловимая ошибка. С этим боролись разными средствами, например глобально запрещали прерывания на время обработки порта — но согласитесь, это какой-то костыльный вариант.
В STM32 эта проблема решена аппаратным путём — у вас есть регистры установки и сброса битов (BSRR и BRR), и здесь убиты сразу три зайца:
- не нужно читать порт для работы с ним
- для воздействия на конкретные пины нужно работать с конкретными битами, а не пытаться изменять весь порт
- эти операции атомарны — они проходят за один цикл, и их невозможно прервать посередине.
Подробнее про «конкретные биты» — каждый такт APB2 читаются регистры BSRR и BRR, и сразу же их содержимое применяется на регистр ODR, а сами эти регистры очищаются.Таким образом, если нужно установить 3 и 5 биты в порте — пишем в BSRR слово 10100, и всё успешно устанавливается.
Блокирование конфигурации
При желании, можно заблокировать конфигурацию любого пина от дальнейших изменений — любая попытка записи в регистр конфигурации окончится неуспехом. Это подойдёт для ответственных применений, где случайное переключение к примеру, выхода из режима open drain в push-pull выжжет всё подключенное к этому пину, или сам пин. Для включения блокирования предназначен регистр LCKR, только он снабжён защитой от случайной непреднамеренной записи — чтобы изменения вступили в силу, нужно подать специальную последовательность в бит LCKK.
Управляющие регистры
Всё управление контроллером GPIO сосредоточено в 32-битных регистрах GPIOx_RRR, где x — номер порта, а RRR — название регистра.
Младший конфигурационный регистр GPIOx_CRL
Настраивает первые 8 ножек, с номерами 0..7. У каждой ножки два параметра, MODE и CNF.
MODE отвечает за режим вход/выход и скорость нарастания сигнала.
00 — вход (режим по умолчанию)
01 — выход со скоростью 10 МГц
10 — выход со скоростью 2 МГц
11 — выход со скоростью 50 МГц
CNF отвечает за конфигурацию пина.
- В режиме входа (MODE=00):
00 — аналоговый режим
01 — плавающий вход (дефолт)
10 — вход с подтяжкой к земле или питанию
11 — зарезервирован
- В режиме выхода (MODE=01, 10 или 11):
00 — выход GPIO Push-pull
01 — выход GPIO Open drain
10 — выход альтернативной функции Push-pull
11 — выход альтернативной функции Open drain
Старший конфигурационный регистр GPIOx_CRH
Настраивает вторые 8 ножек, с номерами 8..15. Всё аналогично GPIOx_CRL.
Регистр входных данных GPIOx_IDR
Каждый бит IDRy содержит в себе состояние соответствующей ножки ввода-вывода. Доступен только для чтения.
Регистр входных данных GPIOx_ODR
Каждый бит ODRy содержит в себе состояние соответствующей ножки ввода-вывода. Можно записывать данные и они появятся на выходе порта, можно читать данные — читая предыдущее записанное значение.
Регистр атомарной установки/сброса битов выходных данных GPIOx_BSRR
Старшие 16 бит — для сброса соответствующих пинов в 0. 0 — ничего не делает, 1 — сбрасывает соответствующий бит. Младшие 16 бит — для установки битов в 1. Точно так же, запись «0» ничего не делает, запись «1» устанавливает соответствующий бит в 1.
Регистр только для записи — он сбрасывается в ноль на каждом такте APB2.
Регистр атомарного сброса битов выходных данных GPIOx_BRR
Младшие 16 бит — для сброса соответствующих пинов. 0 — ничего не делает, 1 — сбрасывает соответствующий бит.
Регистр только для записи — он сбрасывается в ноль на каждом такте APB2.
Регистр блокирования конфигурации GPIOx_LCKR
Каждый бит LCKy блокирует соответствующие биты MODE/CNF регистров CRL/CRH от изменения, таким образом конфигурацию пина невозможно будет изменить вплоть до перезагрузки. Для активации блокирования необходимо записать блокирующую последовательность в бит LCKK: 1, 0, 1, читаем 0, читаем 1. Чтение бита LCKK сообщает текущий статус блокировки: 0 — блокировки нет, 1 — есть.
Работа в разных режимах
Режим входа
- Отключается драйвер выхода
- Входной триггер Шмитта включен
- Резисторы подтяжек включаются по вашим настройкам, одно из трёх состояний — «вход, подтянутый к земле», «вход, подтянутый к питанию», или «плавающий вход»
- Входной сигнал семплируется каждый такт шины APB2 и записывается в регистр IDR, и чтение этого регистра сообщает состояние ножки.
Режим выхода
- Драйвер выхода включен, и действует так:
В режиме «Push-Pull» работает как полумост, включая верхний транзистор в случае «1» и нижний в случае «0»,
В режиме «Open drain» включает нижний транзистор в случае «0», а в случае «1» оставляет линию неподключенной (т.е. в третьем состоянии).
- Входной триггер Шмитта включен
- Отключаются резисторы подтяжек
- Выходной сигнал семплируется каждый такт шины APB2 и записывается в регистр IDR, и чтение этого регистра сообщает состояние ножки в режиме Open drain.
- Чтение регистра ODR сообщает последнее записанное состояние в режиме Push-Pull.
Режим альтернативной функции (не-GPIO-периферия)
- Выходной драйвер — в режиме Push-Pull (к примеру, так работает ножка TX модуля USART) или Open drain, в зависимости от требований контроллера
- Выходной драйвер управляется сигналами периферии, а не регистром ODR
- Входной триггер Шмитта включен
- Резисторы подтяжки отключены
- Выходной сигнал семплируется каждый такт шины APB2 и записывается в регистр IDR, и чтение этого регистра сообщает состояние ножки в режиме Open drain.
- Чтение регистра ODR сообщает последнее записанное состояние в режиме Push-Pull.
Аналоговый режим
- Выходной драйвер выключен
- Триггер Шмитта полностью отключается, чтобы не влиять на напряжение на входе
- Резисторы подтяжки отключены
- В регистре IDR — постоянно 0.
Вся внутренняя аналоговая периферия имеет высокий входной импеданс, поэтому и сама ножка по отношению к остальной схеме будет иметь высокий входной импеданс.
Наконец-то включаем светодиод
Теперь мы знаем всё, чтобы включить этот светодиод! Пойдём с самого начала.
Нужно включить тактирование GPIO порта. Поскольку мы используем светодиод на плате Discovery, выберем зелёный — он подключен к порту PC9. То есть, необходимо включить тактирование GPIOC.
RCC->APB2ENR | = RCC_APB2ENR_IOPCEN; |
Супер, теперь настраиваем порт на Push-pull выход со скоростью 2 МГц. Сначала выбираем режим, «Output mode, max speed 2 MHz» это 10 в регистре MODE. Достаточно просто установить второй бит.
GPIOC->CRH | = GPIO_CRH_MODE9_1; |
Теперь говорим про Push-pull выход. Это соответствует 00 в регистре CNF.
GPIOC->CRH &= !(GPIO_CRH_CNF9_0 | GPIO_CRH_CNF9_1); |
Устанавливаем долгожданный бит в регистре BSRR!
GPIOC->BSRR | = GPIO_BSRR_BS9; |
Ну вот, честно говоря и всё. Напоследок — листинг мигающего светодиода
#include "stm32f10x.h"
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
GPIOC->CRH &= !(GPIO_CRH_CNF9_0 | GPIO_CRH_CNF9_1);
GPIOC->CRH |= GPIO_CRH_MODE9_1;
uint32_t i, n=1000000;
while(1) {
GPIOC->BSRR |= GPIO_BSRR_BS9;
i=0; while(i++<n);
GPIOC->BRR |= GPIO_BRR_BR9;
i=0; while(i++<n);
}
}
Библиотека itacone
И всё-таки ещё не всё. Ради упрощения всяческих настроек я делаю библиотеку itacone. На текущий момент в ней реализована работа с GPIO-пинами и пара функций общего применения — но работа продолжается.
Интересная статья. Так глубоко ещё не добирался.
Но пришлось по причине надобности выводить высокочастотный сигнал.
Но вот у меня светодиоды зажигались и тухли только тогда, когда я заключил их цикл while. Иначе светодиод просто горел. (точнее мигал 8000000) раз в секунду. А пустой цикл компилятор отбрасывал как ненужную часть кода.
МК STM32F103C8TG Среда Cube+Workspace
Не знаю почему. Наверное это настроение компилятора.
Переделал под свои нужды. Оказалось, что порты включает но не выключает. однако, если воспользоваться аналогичными, то код работает.
timertri = 0;
while (timertri < 12) {
if (timertri BSRR |= GPIO_BSRR_BS3;
} else {
// HAL_GPIO_WritePin(GPIOB, Lamp2_Pin, GPIO_PIN_RESET);
GPIOB->BRR |= GPIO_BRR_BR3;
}
}
На других сайтах увидел иные обращения к портам такие как :
GPIOB->ODR &= ~ GPIO_ODR_ODR13; // зажечь светодиод
GPIOB->ODR |= GPIO_ODR_ODR13; // погасить светодиод
GPIOB -> BSRR = GPIO_BSRR_BR13; // сброс бита
GPIOB -> BSRR = GPIO_BSRR_BS13; // установка бита
которыми тоже что то зажигали
и запутался в конец. особенно с этими (|= &=~).