Порты микроконтроллеров STM32. STM32. Регистры портов GPIO

Во втором уроке цикла, посвященного работе с микроконтроллерами STM32, речь пойдет о портах ввода/вывода.
Порты микроконтроллера позволяют взаимодействовать с внешними устройствами, начиная от светодиода и кнопки и заканчивая более сложными устройствами: дисплеями, GPS и GSM модемами и так далее. Также порты позволяют организовать связь с другими устройствами, например с компьютером.

General Purpose Input/Output (GPIO). GPIO основной и часто применяемый способ связи с внешней средой. Порты могут работать в двух режимах: вход (прием сигнала) и выход (передача сигнала). Работают они только с логическими уровнями 0 (низкий уровень) или 1 (высокий уровень).
Например, если подключить к порту в режиме выхода светодиод, то при подаче сигнала высокого уровня светодиод будет светиться, а при подаче низкого – потухнет.
Если включить вывод в режим входа и подключить к нему кнопку, то с помощью микроконтроллера можно отслеживать ее состояние: нажатое или отпущенное.
По сути GPIO самый простой и примитивный способ организации работы с внешними устройствами, но использование обработки прерываний и таймеров значительно расширяет возможности. Речь о них пойдет немного позже.

Решим первую практическую задачу: управление светодиодами и считывание состояние кнопки.
Следует отметить очень важный момент – порты микроконтроллера могут выдать ток не более 20 мА. Хотя выдать он их может, но один раз и ненадолго, до хлопка и сизого дыма;). Для подключения более мощных нагрузок следует использовать силовые ключи.

Итак, начнем. Для работы возьмем плату STM32F4 Discovery. На ней изначально установлена пользовательская кнопка, подключенная к порту PA0 и 4 светодиода, подключенные к портам PD12-PD15.

Схема подключение кнопки и светодиодов показаны на рисунке.

Резистор R1 номиналом 10кОм – «подтяжка к земле», позволяет избежать ситуации, когда порт не подключен ни к «0», ни к «1» - этого необходимо избегать, а резистор решает эту проблему. Такую подтяжку можно включить и программно, но лучше обезопасить себя так.

Резисторы R2-R5 330Ом ограничивают ток, протекающий через светодиоды. Их можно выбрать в диапазоне от 200Ом до 1кОм, все зависит от необходимой яркости.

Теперь перейдем к написанию программы. В качестве среды разработки я использую . Среда бесплатная и, на мой взгляд, удобная. Как начинать в ней работать рассказывать не буду – в интернете по ней достаточно информации, для прошивки использую STM32 ST-LINK Utility.
Для начала включаем тактирование порта A, к которому подключена кнопка:

RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);

Теперь нужно правильно сконфигурировать порт:

//Структура содержащая настройки порта GPIO_InitTypeDef GPIO_InitStructure; //задаем номер вывода, если кнопка подключена, например к 6 порту, то пишем GPIO_Pin_6 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //порт будет работать как цифровой вход GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;

Существует несколько вариантов режима работы порта:
GPIO_Mode_IN – цифровой вход;
GPIO_Mode_OUT – цифровой выход;
GPIO_Mode_AF – альтернативная функция (UART и т.д.);
GPIO_Mode_AN – аналоговый режим.

//включаем подтяжку к «земле» GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;

Возможны следующие режимы «подтяжки»:
GPIO_PuPd_NOPULL – без подтяжки, вывод «болтается в воздухе»
GPIO_PuPd_UP – подтяжка к 3,3В
GPIO_PuPd_DOWN – подтяжка к «земле»

//вызов функции инициализации GPIO_Init(GPIOA, &GPIO_InitStructure);

Теперь сконфигурируем выводы, к которым подключены светодиоды:

//Включаем тактирование порта D RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE); //Выбираем нужные выводы GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12| GPIO_Pin_13| GPIO_Pin_14| GPIO_Pin_15; //Включаем режим выхода GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; //вызов функции инициализации GPIO_Init(GPIOD, &GPIO_InitStructure);

Вот и все, порты сконфигурированы. Теперь напишем обработку в основном цикле программы:

While(1) { //Если кнопка нажата, то… if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1) { GPIO_SetBits(GPIOD, GPIO_Pin_12); //Подаем «1» на PD12 delay(); //Функция задержки GPIO_SetBits(GPIOD, GPIO_Pin_13); //Подаем «1» на PD13 delay(); GPIO_SetBits(GPIOD, GPIO_Pin_14); //Подаем «1» на PD14 delay(); GPIO_SetBits(GPIOD, GPIO_Pin_15); //Подаем «1» на PD15 delay(); GPIO_ResetBits(GPIOD, GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15); //Сбрасываем все пины в «0» delay(); } }

Вот и все, программа готова. Полная версия в архиве с проектом. Работа платы показана на видео.

Понимаю, что статья уже получилась довольно длинной и всем хочется поскорее написать какую-нибудь программулину, делающую хоть что-то полезное, и залить её в контроллер, но так уж вышло, что контроллеры STM32 несколько сложнее простейших восьмибитных железяк, поэтому сейчас речь снова пойдёт, как пишут некоторые нетерпеливые читатели, «не о том».

В этой части мы поговорим о том, с чем нам чаще всего придётся оперировать при работе с контроллером — о рабочих регистрах ядра Cortex-M3, о режимах его работы и о том, как контроллер включается.

Итак, в ядре Cortex-M3 имеется 13 регистров общего назначенияR0..R12 , регистр, используемый для хранения указателя стека, — R13 , регистр связи — R14 , счётчик команд — R15 и 5 регистров специального назначения.

Регистры общего назначения разделяются на младшие регистры — R0..R7 и старшие регистры — R8..R12 . Разница между ними в том, что некоторые 16-тибитные команды набора thumb-2 умеют работать только с младшими регистрами, а со старшими — не умеют.

Регистров R13 вообще-то говоря два, а не один. Первый называется MSP — указатель основного стека , а второй PSP — указатель стека процесса . Однако в каждый момент доступен только один из этих регистров. Какой именно — определяется в одном из регистров специального назначения. Зачем такое надо? Это сделано для возможности организации защиты операционной системы (ага, на этот контроллер можно поставить ОС, если хочется) от кривых прикладных программ. MSP используется обработчиками исключительных ситуаций и всеми программами, использующими привилегированный уровень выполнения (например ядро ОС), а PSP — используется программами, не требующими привилегированного уровня выполнения (например, прикладными программами от которых мы хотим защитить ядро ОС). Указатели стека всегда должны быть выровнены на границу 32-хбитного слова, т.е. два их младших бита всегда должны быть сброшены в ноль.

Регистр R14 называется LR (link register) — регистр связи и используется для запоминания адреса возврата при вызове подпрограмм.

Регистр R15 называется PC (program counter) — счётчик команд и используется для хранения адреса текущей выполняемой команды.

Теперь о специальных регистрах.

Регистр xPSR содержит флаги результатов выполнения арифметических и логических операций, состояние выполнения программы и номер обрабатываемого в данный момент прерывания. Иногда об этом регистре пишут во множественном числе. Это сделано потому, что к трём его частям можно обращаться как к отдельным регистрам. Эти «подрегистры» называются: APSR — регистр состояния приложения (тут как раз хранятся флаги), IPSR — регистр состояния прерывания (содержит номер обрабатываемого прерывания) и EPSR — регистр состояния выполнения . Полностью структура регистра xPSR приведена на рисунке ниже.

Флаги в регистре APSR стандартные:

  1. N (negative flag) — отрицательный результат операции
  2. Z (zero flag) — нулевой результат операции
  3. C (carry flag) — флаг переноса/займа
  4. V (overflow flag) — флаг переполнения
  5. Q (saturation flag) — флаг насыщения

В регистре PRIORITY MASK используется только нулевой бит (PRIMASK), который будучи установлен в единицу запрещает все прерывания с конфигурируемым приоритетом. После включения бит PRIMASK сброшен в ноль — все прерывания разрешены.

В регистре FAULT MASK также использует только нулевой бит (FAULTMASK), который будучи установлен в единицу запрещает все прерывания и исключения, кроме немаскируемого прерывания (NMI). После включения бит FAULTMASK сброшен в ноль — все прерывания разрешены.

Регистр BASEPRI используется для запрещения всех прерываний, значение приоритета которых больше или равно, чем записано в этом регистре. Тут надо сказать, что чем меньше значение — тем выше уровень приоритета. В регистре BASEPRI используются только младшие 8 бит.

Регистр CONTROL используется для управления одним из режимов процессора — режимом потока. Нулевой бит этого регистра (nPRIV) определяет уровень выполнения (привилегированный — Privilegied, или непривилегированный — Unprivilegied), а первый бит (SPSEL) — используемый указатель стека (MSP или PSP). Разница между привилегированным и непривилегированным уровнями выполнения состоит в том, что для привилегированного уровня доступны все области памяти и все команды процессора, а для непривилегированного уровня некоторые области памяти закрыты (например, регистры специального назначения, кроме APSR, системная область) и, соответственно, команды для доступа в эти обасти — запрещены. Попытка выполнения запрещённых команд, пытающихся обратиться в закрытые области памяти вызывает генерацию исключения отказа.

Теперь о режимах работы процессора.

Процессор Cortex-M3 имеет два режима работы: режим потока (Thread) и режим обработчика (Handle). Режим Handle используется для обработки исключительных ситуаций, а режим Thread — для выполнения всего остального кода. Переключение из одного режима в другой происходит автоматически. Как мы уже говорили, когда разбирались с регистром CONTROL, в режиме Thread процессор может использовать как привилегированный уровень выполнения, так и непривилегированный, в режиме Handle — только привилегированный. Аналогично, в режиме Thread может использоваться как основной стек (MSP), так и стек процесса (PSP), а в режиме Handle — только основной стек.

Важно понимать, что, например, переключившись в режиме Thread с привилегированного уровня в непривилегированный, мы потеряем доступ в регистр CONTROL и обратно сможем переключиться только в режиме Handle. В режиме Handle бит nPRIV регистра CONTROL доступен для чтения/записи, но не влияет на текущий режим выполнения. Это позволяет изменить уровень выполнения, который будет у программы, когда процессор выйдет из режима обработчика в режим потока. Бит SPSEL в режиме Handle для записи недоступен и всегда читается как ноль, а при выходе из режима обработчика в режим потока восстанавливается автоматически. Все варианты переходов между различными режимами и уровнями выполнения иллюстрирует ориентированный граф, представленный на рисунке ниже:

Стартует контроллер всегда на внутреннем генераторе, на частоте 8 Мгц. Откуда брать тактовый сигнал в дальнейшем, на сколько его умножать или делить — настраивается в программе. Если в программе этого не сделать, то хоть десять внешних кварцев повесьте, контроллер так и будет работать от внутреннего генератора 8 МГц.

При старте контроллер анализирует сочетание уовней на двух своих ногах — BOOT0, BOOT1, и, в зависимости от этого сочетания, начинает загрузку либо из flash-памяти, либо из ОЗУ, либо из системной области памяти. Это делается с помощью уже описанного нами ранее механизма псевдонимизации. По идее загрузка всегда начинается с нулевого адреса, просто в зависимости от
сочетания на ногах BOOT0, BOOT1 начальные адреса памяти назначаются псевдонимами одной из трёх областей: flash, встроенного ОЗУ или системной области. Справа приведена табличка, в которой указано, какая именно область проецируется в начальные адреса памяти в зависимости от сочетания ног BOOT0, BOOT1.

При этом в системной области производителем зашита специальная программа (bootloader), которая позволяет запрограммировать flash-память. Но об этом позже.

Первым делом контроллер считывает 32-х битное слово по адресу 0x00000000 и помещает его в регистр R13 (указатель стека). Далее он считывает 32-х битное слово по адресу 0x00000004 и помещает его в регистр R15 (счётчик команд). Последнее действие вызывает переход на начало программного кода и дальше начинается выполнение программы.

Слово по адресу 0x00000004 (адрес начала основной программы) называется вектор сброса. Вообще в памяти контроллера после указателя стека по адресу 0x00000000, начиная с адреса 0x00000004 должна лежать таблица векторов исключений и прерываний, первый вектор в которой — это вектор сброса, а остальные вектора — адреса процедур обработчиков различных исключений и прерываний. В простейших программах, если вы не собираетесь обрабатывать исключения и прерывания, все остальные вектора, кроме вектора сброса, могут отсутствовать. Хотелось бы обратить внимание, что в таблице векторов указываются именно адреса начала обработчиков, а не команды перехода на эти обработчики, как, например, в 8-ми битных пиках или атмелах.

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

Поскольку микроконтроллеры 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 настраивается по стандартной процедуре.

  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:

Выглядит сложнее, чем в 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

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

Регистр атомарного сброса битов выходных данных 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» оставляет линию неподключенной (т.е. в третьем состоянии).

  • Входной триггер Шмитта включен
  • Отключаются резисторы подтяжек

Режим альтернативной функции (не-GPIO-периферия)

  • Выходной драйвер - в режиме Push-Pull (к примеру, так работает ножка TX модуля USART) или Open drain, в зависимости от требований контроллера
  • Выходной драйвер управляется сигналами периферии, а не регистром ODR
  • Входной триггер Шмитта включен
  • Резисторы подтяжки отключены
  • Выходной сигнал семплируется каждый такт шины APB2 и записывается в регистр IDR, и чтение этого регистра сообщает состояние ножки в режиме Open drain.
  • Чтение регистра ODR сообщает последнее записанное состояние в режиме Push-Pull.

Аналоговый режим

  • Выходной драйвер выключен
  • Триггер Шмитта полностью отключается, чтобы не влиять на напряжение на входе
  • Резисторы подтяжки отключены
  • В регистре IDR - постоянно 0.

Вся внутренняя аналоговая периферия имеет высокий входной импеданс, поэтому и сама ножка по отношению к остальной схеме будет иметь высокий входной импеданс.

Наконец-то включаем светодиод

Теперь мы знаем всё, чтобы включить этот светодиод! Пойдём с самого начала.

Нужно включить тактирование GPIO порта. Поскольку мы используем светодиод на плате Discovery, выберем зелёный - он подключен к порту PC9. То есть, необходимо включить тактирование GPIOC.

Теперь говорим про Push-pull выход. Это соответствует 00 в регистре CNF.

Ну вот, честно говоря и всё. Напоследок - листинг мигающего светодиода

#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++BRR |= GPIO_BRR_BR9; i=0; while(i++

Библиотека itacone

И всё-таки ещё не всё. Ради упрощения всяческих настроек я делаю библиотеку itacone. На текущий момент в ней реализована работа с GPIO-пинами и пара функций общего применения - но работа продолжается.

В этой статье перейдем к разбору действий для управления портами ввода/вывода. В технической документации к плате Discovery с чипом STM32f407VG указано, что светодиоды подключены к порту GPIOD и ножкам 12, 13, 14, 15. Смотрим в RM0090 Reference manual, и видим, что регистры управления порта D (GPIOD) занимают пространство в памяти, начиная с адреса 0x4002 0C00. Дальше в документации идет описания регистров управления GPIOx (где "х" имя порта:A,B,C...). Первый регистр - GPIOx_MODER , адрес смещения - ноль, то есть он первый в блоке и находится по адресу 0x40020C00. Он отвечает за направление тока: на вход, выход, или режим альтернативной функции (то есть к ножкам можно подключать разные внутренние блоки/интерфейсы передачи\приема данных, а не только использовать для вкл/выкл +3V). Так как нам нужно зажечь светодиод, устанавливаем режим вывода ножек 12,13,14,15 . Записываем "01" в каждую пару битов нужных ножек, и записываем получившееся число в указатель на регистр GPIOD moder:

*((uint32_t*)0x40020C00)=0х55000000;

Смотрим дальше, следующий регистр GPIOxOTYPER , можно оставить по умолчанию. GPIOxSPEEDR , скорость считывания состояния ножки, можем оставить по умолчанию. GPIOxPUPDR -регистр позволяет "прижать" ножку к положительному или отрицательному значению тока.
GPIOx_IDR - Регистр только для чтения, указывает на логическое значение на соответствующей ножке. Можно его использовать, например, если нужно узнать нажата ли кнопка, или пришел ли какой нибудь сигнал на ножку.
GPIOx_ODR - регистр для чтения и записи. Запись "1" в нужный бит подаст логическую единицу на соответствующую ножку. Запись "0" - сбросит бит, и установит на ножке логический ноль.
GPIOx_BSRR - Регистр устанавливает логический ноль или единицу на ножках. в отличии от регистра GPIOx_ODR, значение которого нужно переписывать целиком ради установки отдельного бита, GPIOx_BSRR - можно точечно устанавливать соответствующие биты регистра GPIOx_ODR, не перезаписывая его значения целиком. Установка "1" в биты 0-15 , установит биты 0-15 в GPIOx_ODR. Установка "1" в биты 16-31 установит "0" в биты 0-15 в GPIOx_ODR. запись "0" в регистр GPIOx_BSRR не дает никакого эффекта.
GPIOx_LCKR - регистр устанавливающий блокировку на изменение настроек регистров портов.
GPIOx_AFRL - Регистр альтернативных функций ножек 0-7.
GPIOx_AFRH - Регистр альтернативных функций ножек 7-15.

Адреса регистров GPIO

#define RCC_GPIO *((uint32_t*)0x40023830) //.................
#define GPIOA_MODER *((uint32_t*)0x40020000)
#define GPIOA_OTYPER *((uint32_t*)0x40020004)
#define GPIOA_OSPEEDR *((uint32_t*)0x40020008)
#define GPIOA_IDR *((uint32_t*)0x40020010)
#define GPIOA_PUPDR *((uint32_t*)0x4002000C)
#define GPIOA_ODR *((uint32_t*)0x0x40020014)
#define GPIOA_BSRR *((uint32_t*)0x40020018)
#define GPIOA_AFRL *((uint32_t*)0x40020020)
#define GPIOA_AFRH *((uint32_t*)0x40020024)
//.................
#define GPIOB_MODER *((uint32_t*)0x40020400)
#define GPIOB_OTYPER *((uint32_t*)0x40020404)
#define GPIOB_OSPEEDR *((uint32_t*)0x40020408)
#define GPIOB_IDR *((uint32_t*)0x40020410)
#define GPIOB_PUPDR *((uint32_t*)0x4002040C)
#define GPIOB_ODR *((uint32_t*)0x40020414)
#define GPIOB_BSRR *((uint32_t*)0x40020418)
#define GPIOB_AFRL *((uint32_t*)0x40020420)
#define GPIOB_AFRH *((uint32_t*)0x40020424)
//................
#define GPIOC_MODER *((uint32_t*)0x40020800)
#define GPIOC_OTYPER *((uint32_t*)0x40020804)
#define GPIOC_OSPEEDR *((uint32_t*)0x40020808)
#define GPIOC_IDR *((uint32_t*)0x40020810)
#define GPIOC_PUPDR *((uint32_t*)0x4002080C)
#define GPIOC_ODR *((uint32_t*)0x40020814)
#define GPIOC_BSRR *((uint32_t*)0x40020818)
#define GPIOC_AFRL *((uint32_t*)0x40020820)
#define GPIOC_AFRH *((uint32_t*)0x40020824)
//.................
#define GPIOD_MODER *((uint32_t*)0x40020C00)
#define GPIOD_OTYPER *((uint32_t*)0x40020C04)
#define GPIOD_OSPEEDR *((uint32_t*)0x40020C08)
#define GPIOD_IDR *((uint32_t*)0x40020C10)
#define GPIOD_PUPDR *((uint32_t*)0x40020C0C)
#define GPIOD_ODR *((uint32_t*)0x40020C14)
#define GPIOD_BSRR *((uint32_t*)0x40020C18)
#define GPIOD_AFRL *((uint32_t*)0x40020C20)
#define GPIOD_AFRH *((uint32_t*)0x40020C24)
//................
#define GPIOE_MODER *((uint32_t*)0x40021000)
#define GPIOE_OTYPER *((uint32_t*)0x40021004)
#define GPIOE_OSPEEDR *((uint32_t*)0x40021008)
#define GPIOE_IDR *((uint32_t*)0x40021010)
#define GPIOE_PUPDR *((uint32_t*)0x4002100C)
#define GPIOE_ODR *((uint32_t*)0x40021014)
#define GPIOE_BSRR *((uint32_t*)0x40021018)
#define GPIOE_AFRL *((uint32_t*)0x40021020)
#define GPIOE_AFRH *((uint32_t*)0x40021024)

Каждый порт STM32 состоит из 16 выводов, а каждый вывод может быть сконфигурирован одним из 8 способов.

Ниже изображена структура порта ввода-вывода.


Для того чтобы порт заработал его надо подключить к шине APB2 , установив соответствующий бит IOPxEN , в регистре разрешения тактирования периферийных блоков RCC_APB2ENR .
RCC->APB2ENR |= RCC_APB2ENR_IOPxEN; // Разрешить тактирование PORTx.
После включения все выводы находятся в состоянии плавающего входа , он же высокоимпедансный вход , он же Hi-Z , он же третье состояние .
  • Выходной драйвер выключен
  • Триггер Шмитта отключён
  • Подтягивающие резисторы отключены
  • В регистре IDR всегда “0”

В режиме входа

  • Выходной драйвер выключен
  • В зависимости от настройки, включаются резисторы подтяжки
  • Каждый такт шины APB2 данные с входа поступают в регистр IDR, считав этот регистр можно узнать состояние ножки

В режиме выхода

  • В режиме Open Drain при записи “0” открывается нижний транзистор, при записи “1” линия остаётся не подключённой
  • В режиме Push Pull при записи “1” открывается верхний транзистор, при записи “0” - нижний
  • Входной Триггер Шмитта включён
  • Резисторы подтяжки отключены

В режиме альтернативной функции

  • Драйвер включается в режиме Push Pull или Open Drain, в зависимости от конфигурации
  • Выходной драйвер управляется сигналами периферии, а не регистром ODR
  • Входной триггер Шмитта включён
  • Резисторы подтяжки отключены
  • По каждому такту шины APB2 данные c выхода передаются в регистр IDR, оттуда же их можно считать в режиме Open Drain
  • Чтение регистра ODR возвращает последнее записанное значение в режиме Push Pull

Из таблицы видно, что возможны два варианта конфигурации, в режиме альтернативной функции: Push Pull и Open Drain . Например, мы хотим, настроить в режим альтернативной функции ножку, отвечающую за приём данных по USART. Для этого в Reference Manual RM0008, начиная с 161 страницы, идут таблицы, в которых можно посмотреть как cконфигурировать вывод, для разной периферии.


Нам подойдет Input floating или Input pull-up .

Конфигурация выводов задаётся в регистрах GPIOx_CRL , GPIOx_CRH , в этих регистрах для конфигурации каждого вывода отведено 4 бита, MODE и CNF . В GPIOx_CRL конфигурируются выводы с 0 по 7, а в GPIOx_CRH с 8 по 15.



Если MODE = 00 , то вывод настроен на вход, конфигурация входа в таком случае задаётся в регистрах CNF . Если MODE не равен 00, в таком случае вывод настроен как выход, а значение MODE задаёт максимальную частоту, с которой может он переключаться.
//Полагаем что выводы после сброса в режиме плавающего входа //разрешаем тактирование порта A RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //вход с подтяжкой к + GPIOA->CRL &= ~GPIO_CRL_CNF0; GPIOA->CRL |= GPIO_CRL_CNF0_1; GPIOA->ODR |= GPIO_ODR_ODR0; //вход с подтяжкой к - GPIOA->CRL &= ~GPIO_CRL_CNF1; GPIOA->CRL |= GPIO_CRL_CNF1_1; GPIOA->ODR &= ~GPIO_ODR_ODR1; //аналоговый режим GPIOA->CRL &= ~GPIO_CRL_CNF2; //выход с открытым стоком 2MHz GPIOA->CRL &= ~GPIO_CRL_CNF3; GPIOA->CRL |= GPIO_CRL_CNF3_0; GPIOA->CRL |= GPIO_CRL_MODE3_1; //двухтактный выход 10MHz GPIOA->CRL &= ~GPIO_CRL_CNF4; GPIOA->CRL |= GPIO_CRL_MODE4_0; //альтернативная ф-ция, двухтактный выход, 50 MHz GPIOA->CRL &= ~GPIO_CRL_CNF5; GPIOA->CRL |= GPIO_CRL_CNF5_1; GPIOA->CRL |= GPIO_CRL_MODE5; //альтернативная ф-ция, выход с открытым стоком, 50 MHz GPIOA->CRL |= GPIO_CRL_CNF6; GPIOA->CRL |= GPIO_CRL_MODE6;
Считать состояние входа можно с помощью Port input data register или коротко GPIOx_IDR , где x – название порта, может быть от A до G. Считать состояние любого вывода можно из 16 младших бит, старшие 16 бит не используются.


//проверяем значение нулевого вывода порта А if (GPIOА->IDR & GPIO_IDR_IDR0)
Если порт настроен на выход, управлять его состоянием можно с помощью регистра Port output data register или GPIOx_ODR . Значение, которое мы запишем в этот регистр, появится на соответствующих выводах порта. Для установки состояния порта, выделены 16 младших бит, старшие 16 бит не используются.


//если вывод в режиме входа то активируется подтяжка к питанию GPIOA->ODR |= GPIO_ODR_ODR0; //или к земле GPIOA->ODR &= ~GPIO_ODR_ODR0; //если в режиме выхода, то на нём установится соответствующий лог.уровень //например так можно установить все выходы порта в 1 GPIOA->ODR = 0xFFFF;
В STM32 возможно атомарно управлять отдельными битами порта с помощью регистров GPIOx_BSRR (Port Bit Set/Reset Register) и GPIOx_BRR (Port Bit Reset Register).
Для установки отдельного бита порта вручную, надо считать значение порта, изменить нужный бит с помощью маски и результат вернуть обратно в GPIOx_ODR . Так как действий целых три, то возникшее между ними прерывание, может подпортить данные. С помощью описанных выше регистров, это делается в одно действие.
Для сброса бита надо в нулевой бит GPIOx_BRR записать единичку, при этом в нулевой бит GPIOx_ODR запишется 0, для этой операции выделены младшие 16 бит, старшие 16 бит не используются.


//сбросить нулевой бит порта А GPIOA->BRR = GPIO_BRR_BR0;
С GPIOx_BSRR всё чуть интереснее, младшие 16 бит отвечают за установку 1, старшие 16 бит за сброс в 0. Чтобы установить 1 в нулевой бит, надо в нулевой бит GPIOx_BSRR записать 1. Чтобы установить 0 в нулевой бит, надо в 16 бит установить 1.


//сбросить нулевой бит GPIOA->BSRR = GPIO_BSRR_BR0; //установить нулевой бит GPIOA->BSRR = GPIO_BSRR_BS0;
У STM32 есть возможность защитить конфигурацию порта от изменения, для этого выделен регистр GPIOx_LCKR . Младшие 16 бит используются для выбора вывода, который хотим заблокировать (выбор бита осуществляется установкой единицы), затем специальной последовательностью записей в 16 бит(LCKK ) осуществляется блокировка.


Последовательность следующая: записать в LCKK 1 , записать 0 ,записать 1, затем из регистра LCKR считать 0, считать 1. Последняя считанная единица говорит о том, что вывод заблокирован. Разблокировка вывода произойдёт только после перезагрузки контроллера.
#include "stm32f10x.h" uint32_t temp; int main(void) { //разрешаем тактирование порта RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; //настраиваем как двухтактный выход GPIOA->CRL &= ~GPIO_CRL_CNF0; //с максимальной частотой 50MHz GPIOA->CRL |= GPIO_CRL_MODE0; //выбираем вывод который хотим залочить GPIOA->LCKR |= GPIO_LCKR_LCK0; //записываем 1 GPIOA->LCKR |= GPIO_LCKR_LCKK; //записываем 0 GPIOA->LCKR &= ~GPIO_LCKR_LCKK; //записываем 1 GPIOA->LCKR |= GPIO_LCKR_LCKK; //считываем 2 раза temp = GPIOA->LCKR; temp = GPIOA->LCKR; }
Для получения более подробной информации можно обратиться Reference Manual RM0008 , к разделу General-purpose and alternate-function I/Os (GPIOs and AFIOs) .