Помигаем светодиодом!

Поскольку микроконтроллеры STM32 — настоящие 32-битные ARM-ядра, сделать это будет непросто. Здесь всё сильно отличается от привычных методов в PIC или AVR, где было достаточно одной строкой настроить порт на выход, а второй строкой — вывести в него значение — но тем интереснее и гибче.

Архитектура STM32

Подробно архитектура микроконтроллеров расписана в статье, однако напомню основные положения, интересные нам сейчас.

Архитектура контроллера STM32F100 в области GPIO и RCC

Ядро тактируется кварцем, обычно через ФАПЧ. Это — тактовая частота ядра, или SYSCLK. На плате STM32VLDiscovery установлен кварц на 8 МГц, а ФАПЧ в большинстве случаев настраивается как умножитель на 3 — т.е. SYSCLK на плате STM32VLDiscovery обычно равен 24 МГц.

От ядра отходит шина AHB, имеющая свою тактовую частоту — ей можно установить некий прескалер относительно SYSCLK, однако можно оставить его равным единице. Эта шина подобна шине между процессором и северным мостом компьютера — точно так же она служит для связи ARM ядра и процессора периферии, а также на ней висит память и конечно, контроллер DMA.

К шине AHB подключены две периферийных шины — APB1 и APB2. Они равнозначны, просто обслуживают разные контроллеры интерфейсов. Частоты обоих шин APB1 и APB2 можно задавать собственными прескалерами относительно AHB, но их тоже можно оставить равными единице. По умолчанию после запуска микроконтроллера вся периферия на шинах APB1 и APB2 отключена в целях экономии энергии.

Интересующие нас контроллеры портов ввода-вывода висят на шине APB2.

Модель периферии в STM32

Вся периферия микроконтроллеров STM32 настраивается по стандартной процедуре.

  1. Включение тактирования соответствующего контроллера — буквально, подача на него тактового сигнала от шины APB;
  2. Настройки, специфичные для конкретной периферии — что-то записываем в управляющие регистры;
  3. Выбор источников прерываний — каждый периферийный блок может генерировать прерывания по разным поводам. Можно выбрать конкретные «поводы»;
  4. Назначение обработчика прерываний;
  5. Запуск контроллера.

Если прерывания не нужны — шаги 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:

Структура одной ножки ввода-вывода микроконтроллера 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), и здесь убиты сразу три зайца:

  1. не нужно читать порт для работы с ним
  2. для воздействия на конкретные пины нужно работать с конкретными битами, а не пытаться изменять весь порт
  3. эти операции атомарны — они проходят за один цикл, и их невозможно прервать посередине.

Подробнее про «конкретные биты» — каждый такт APB2 читаются регистры BSRR и BRR, и сразу же их содержимое применяется на регистр ODR, а сами эти регистры очищаются.Таким образом, если нужно установить 3 и 5 биты в порте — пишем в BSRR слово 10100, и всё успешно устанавливается.

Блокирование конфигурации

При желании, можно заблокировать конфигурацию любого пина от дальнейших изменений — любая попытка записи в регистр конфигурации окончится неуспехом. Это подойдёт для ответственных применений, где случайное переключение к примеру, выхода из режима open drain в push-pull выжжет всё подключенное к этому пину, или сам пин. Для включения блокирования предназначен регистр LCKR, только он снабжён защитой от случайной непреднамеренной записи — чтобы изменения вступили в силу, нужно подать специальную последовательность в бит LCKK.

Управляющие регистры

Всё управление контроллером GPIO сосредоточено в 32-битных регистрах GPIOx_RRR, где x — номер порта, а RRR — название регистра.

Младший конфигурационный регистр GPIOx_CRL

Регистр GPIO_CRL микроконтроллера STM32F100

Настраивает первые 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

Регистр GPIO_CRH микроконтроллера STM32F100

Настраивает вторые 8 ножек, с номерами 8..15. Всё аналогично GPIOx_CRL.

Регистр входных данных GPIOx_IDR

Регистр GPIO_IDR микроконтроллера STM32F100

Каждый бит IDRy содержит в себе состояние соответствующей ножки ввода-вывода. Доступен только для чтения.

Регистр входных данных GPIOx_ODR

Регистр GPIO_ODR микроконтроллера STM32F100

Каждый бит ODRy содержит в себе состояние соответствующей ножки ввода-вывода. Можно записывать данные и они появятся на выходе порта, можно читать данные — читая предыдущее записанное значение.

Регистр атомарной установки/сброса битов выходных данных GPIOx_BSRR

Регистр GPIO_BSRR микроконтроллера STM32F100

Старшие 16 бит — для сброса соответствующих пинов в 0. 0 — ничего не делает, 1 — сбрасывает соответствующий бит. Младшие 16 бит — для установки битов в 1. Точно так же, запись «0» ничего не делает, запись «1» устанавливает соответствующий бит в 1.

Регистр только для записи — он сбрасывается в ноль на каждом такте APB2.

Регистр атомарного сброса битов выходных данных GPIOx_BRR

Регистр GPIO_BRR микроконтроллера STM32F100

Младшие 16 бит — для сброса соответствующих пинов. 0 — ничего не делает, 1 — сбрасывает соответствующий бит.

Регистр только для записи — он сбрасывается в ноль на каждом такте APB2.

Регистр блокирования конфигурации GPIOx_LCKR

Регистр GPIO_LCKR микроконтроллера STM32F100

Каждый бит 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-пинами и пара функций общего применения — но работа продолжается.