Порты микроконтроллера

Порты ввода/вывода пожалуй важнейшая часть микроконтроллера, без неё всё остальное просто бессмысленно. Сколько бы не было у контроллера памяти, периферии, какой бы высокой не была тактовая частота - это всё не имеет значения если он не может взаимодействовать с внешним миром. А взаимодействие это осуществляется через эти самые порты ввода/вывода. Далее для краткости будем называть их просто портами. Порт это некоторый именованный набор из 16-ти (как правило) ног контроллера, каждая из которых может быть индивидуально настроена и использована. Количество портов может различаться, например в контроллере установленном в отладочной плате STM32vl Discovery имеются три порта A,B,C. Существует два основных режима работы ног контроллера: вход и выход. Когда нога контроллера настроена на выход - к ней можно прицепить любой потребитель: светодиод, пищалку, да и вообще что угодно. Нужно понимать что ноги у контроллера не потянут большую нагрузку. Максимальный ток который может пропустить через себя одна нога составляет ~20 мА. Если планируется подключать что-то с более высоким энергопотреблением то нужно делать это через транзисторный ключ. В противном случае нога порта (а то и весь порт, чем черт не шутит) сгорит и перестанет выполнять свои функции. Чтобы обезопасить ногу порта можно прицепить к ней резистор номиналом примерно 220 ом. Таким образом при напряжении питания 3.3 вольта даже при коротком замыкании ноги на землю ток не превысит критического значения.  Второй режим работы ноги контроллера - это вход. Благодаря этому режиму мы можем считывать например состояние кнопок, проверяя есть ли на ноге напряжение или нет. Это вкратце, а сейчас рассмотрим подробнее как работать с портами. Рассматривать будем конечно же на практике, благо что аппаратная часть (светодиоды и кнопка) для наших экспериментов уже реализована на плате STM32vl Discovery. Если же платы нет, то можно подключить к контроллеру светодиоды и кнопку следующим образом: 

schematic

 

Для начала попробуем зажечь светодиоды, для этого мы должны произвести аж целых три действия: 

  1. Включить тактирование порта
  2. Настроить две ножки как выходы
  3. Установить логическую единицу на 2-х выводах порта

Для начала создадим проект в CooCox'e точно так же как мы [делали ранее]. Запишем в файл main.c следующий код и будем разбираться: 

#include <stm32f10x_rcc.h>
 
int main(void) {
    RCC->APB2ENR |= RCC_APB2Periph_GPIOC;
    GPIOC->CRH |=0x33;
    GPIOC->CRH &= ~0xCC;
    GPIOC->ODR |= (GPIO_ODR_ODR9 | GPIO_ODR_ODR8);
} 

 

Всего-то четыре строчки кода, но сколько смысла :) Для начала разберемся что значит "Включить тактирование порта". В контроллере полно периферии: Таймеры, АЦП, USART и т.д. Порт ввода/вывода является такой же периферией. Когда периферия включена (подаются тактовые импульсы) - она потребляет ток. Нет тактирования - нет потребления. По умолчанию вообще весь этот зоопарк периферии вырублен. Итак нас интересует порт C, ведь именно на нем висят наши светодиоды. Для включения/выключения периферии есть два регистра RCC_APB1ENR и RCC_APB2ENR. Нам нужен последний, потому что через него мы можем управлять тактированием порта C. Устроен этот регистр так: 

rcc_apb2enr

Как видно на картинке в нем есть бит IOPCEN. Установив его в единицу мы включим наш порт. Именно это и делает строчка кода 

RCC->APB2ENR |= RCC_APB2Periph_GPIOC;

После включения тактирования мы должны настроить некоторые (а именно 8-ю и 9-ю) ноги порта на выход. За конфигурирование вообще любого порта отвечают два регистра GPIOx_CRL и GPIOx_CRH где икс это буква порта (от A до G), в нашем случае это буква С. Оба регистра выполняют одну и ту же функцию, просто GPIOx_CRL отвечает за конфигурирование младшей половины порта (ножки с 0 по 7), а GPIOx_CRH старшей (ножки с 8 по 15). Наши светодиоды висят на ногах PC8 и PC9 а это значит что для настройки этих ног в режим выхода нам потребуется регистр GPIOC_CRH. Вот так он устроен: 

gpiox_crhКак видно из этой красивой и цветной картинки, на каждую ногу отводится по четыре бита. Причем биты объединены в две группы по два бита в каждой. первая группа - MODE. Собственно эти биты решают входм или выходом будет конкретная ножка порта, допустимы следующие комбинации: 

MODE1   MODE0   Значение
0 0 Вход (Состояние по умолчанию)
0 1 Выход, максимальная частота 10 МГц
1 0 Выход, максимальная частота 2 МГц
1 1 Выход, максимальная частота 50 МГц

Максимальная частота в моём понимании это насколько быстро нога может менять свое состояние, скорее всего частота влияет на энергопотребление. Теперь для рассмотрим следующую группу бит CNF. Если мы настроили ногу на выход (биты MODE отличны от нуля) то биты группы CNF могут принимать следующие значения: 

CNF1   CNF0   Значение
0 0  Обычный режим. Push-pull
0 1  Обычный режим. Открытый коллектор
1 0  Альтернативный режим. Push-pull
1 1  Альтернативный режим. Открытый коллектор

Тут всё немного сложнее, во-первых разберёмся что подразумевается под обычным и альтернативный режимами. В обычном режиме вы можете распоряжаться ногой как вам угодно, например установить единицу или ноль при помощи своего кода. В альтернативном режиме вы передаёте эту ножку контроллера в распоряжение какой-либо периферии контроллера например UART'у, SPI, I2c и всему прочему что нуждается в ножках. Теперь разберемся чем отличается push-pull от открытого коллектора. В режиме push-pull нога всегда находится в одном из двух состояний: На ней всегда либо земля либо полное напряжение питания. В режиме открытого коллектора: Земля или ничего, нога просто как-бы зависает в воздухе ни к чему не подключенная внутри контроллера. Теперь рассмотрим что означают те же самый два бита если наш порт настроен на вход (биты MODE обнулены):

CNF1   CNF0   Значение
0 0  Аналоговый режим
0 1  Вход без подтяжки
1 0  Вход с подтяжкой
1 1  Зарезервировано

Аналоговый режим предназначен для работы АЦП, если мы хотим чтоб АЦП мог производить измерения используя эту ногу мы должны выбрать этот режим. Вход без подтяжки делает ногу входом с Hi-z состоянием, это означает что сопротивление входа велико и любая электрическая наводка (помеха) может вызвать появление на таком входе единицу или ноль, причем сделать это не предсказуемо. Во избежание этого нужно использовать подтяжку, она позволяет установить на входе какое либо устойчивое состояние которое не будет зависеть от помех. Подтяжка представляет собой резистор большого сопротивления подключенный одним концом к земле или к плюсу питания, а другим концом ко входу. Например если включена подтяжка к плюсу питания, то когда нога контроллера ни куда не припаяна на ней всегда логическая единица. Если мы припаяем кнопку между этой ножкой и землёй, то всякий раз при нажатии кнопки на ноге будет появляться логический ноль. Если бы подтяжка была выключена, то в момент нажатия кнопки на ноге так же появлялся бы ноль, но при отпущенной кнопке нога могла бы легко поймать любую наводку и вызвать появление логической единицы на ноге. В результате, микроконтроллер бы думал что кто-то хаотично жмет на кнопку. Мы рассмотрим все это на практике чуть позже, а сейчас вернемся к нашему регистру GPIOC_CRH. Итак мы планируем установить биты этого регистра (для двух ножек PC8 и PC9) следующим образом: 

gpioc_crh_config

Исходя из вышесказанного, такая комбинация бит настроит обе ножки на выход с максимальной частотой 50 МГц в обычном режиме push-pull, что нам вполне подходит. Эта строчка устанавливает в единицы биты MODE:

    GPIOC->CRH |=0x33;

А вот эта, обнуляет биты CNF:

    GPIOC->CRH &= ~0xCC;

После выполнения этих двух строк, младшие 8 бит этого регистра будут такими как на рисунке выше: 00110011, при этом все остальные биты останутся в том состоянии в котором они и были, наш код их не затронет. В принципе, в  данном случае не будет ничего страшного если вместо этих двух строк мы просто напишем: 

    GPIOC->CRH = 0x33; // 0x33 это и есть 00110011

Но нужно понимать что во все остальные биты (кроме первых восьми) запишутся нули, и это повлияет на конфигурацию остальных пинов (они все станут аналоговыми входами). Теперь когда обе ножки сконфигурированы можно попробовать зажечь светодиоды. За вывод данных в порт C отвечает регистр GPIOC_ODR, записывая в определённый бит единицу, мы получаем логическую единицу на соответствующей ножке порта. Поскольку в порте С 16 ножек, а регистр 32-х битный, то используются только первые 16 бит. Светодиоды подключены к пинам PC8 и PC9, поэтому мы должны установить восьмой и девятый биты. Для этого служит строчка:

    GPIOC->ODR |= (GPIO_ODR_ODR9 | GPIO_ODR_ODR8);
 

Очень надеюсь, что читатели знакомы с битовой арифметикой в Си :) ибо без неё может быть сложновато. Ну собственно все, после компиляции и загрузки программы в контроллер - на платке загорятся два светодиода: синий и зелёный. Использование регистра GPIOC_ODR - это не единственный способ изменить состояние порта С. Существует еще один регистр позволяющий сделать это - GPIOC_BSRR. Этот регистр позволят атомарно устанавливает состояние какой-либо ножки. Ведь в примере выше мы делали следующим образом:

1) Считывали текущее состояние регистра GPIOC_ODR в некоторую временную переменную

2) устанавливали в ней нужные биты (8-й и 9-й)

3) записывали то что получилось обратно в регистр GPIOC_ODR.

Чтение->модификация->запись это довольно долгая процедура, иногда надо делать это очень быстро. Вот тут то и выходит на сцену регистр GPIOC_BSRR. Посмотрим как он устроен: 

gpioc_bsrr

На каждую ножку порта выделяется по два бита: BRXX и BSXX. Далее всё просто: записывая единицу в бит BSXX мы устанавливаем на соответствующей ножке логическую единицу. Записывая единицу в бит BRXX мы сбрасываем в ноль соответствующую ножку. Запись нулей в любой из битов не приводит ни к чему. Если мы заменим последнюю строчку программы на : 

GPIOC->BSRR=(GPIO_BSRR_BS8|GPIO_BSRR_BS9);

то получим тот же результат, но работает оно быстрей :) Ну а для того чтоб сбросить в ноль определённые биты порта С необходим так же записать две единицы но уже в биты BR8 и BR9:

GPIOC->BSRR=(GPIO_BSRR_BR8|GPIO_BSRR_BR9);

Но и это еще не всё :) Так же изменить состояние порта можно при помощи регистра GPIOC_BRR, установив в единицу какой либо из первых 16-бит, мы сбросим в ноль соответствующие ножки порта. Зачем он нужен - не понятно, ведь есть же регистр GPIOC_BSRR который может делать тоже самое да и еще плюс устанавливать логическую единицу на ноге (а не только сбрасывать в ноль как GPIOC_BRR).  С выводом данных в порт теперь точно всё. Настало время что-то из порта прочитать. Ну а читать мы будет состояние кнопки, которая у нас подключена к ноге PA0. Когда кнопка не нажата - на ножке PA0 присутствует логический ноль за счёт резистора номиналом 10 кОм который подтягивает этот вывод к земле.  После замыкания контактов кнопки, слабенькая подтяжка к земле будет подавлена напряжением питания и на входе появится логическая единица. Сейчас мы попробуем написать программу которая читает состояние ножки PA0 и в зависимости от наличия логической единицы, зажигает или гасит светодиоды. Управлять состоянием светодиодов мы уже научились из предыдущего примера, осталось только разобраться как читать что-то из порта. Для чтения из порта А используется регистр GPIOA_IDR. В его внутреннем устройстве нет ничего особо сложного и поэтому я не буду рисовать тут картинку, а объясню всё парой слов: Первые 16 бит регистра соответствуют 16 ногам порта. Что приходит в порт - то и попадает в этот регистр. Если на всех ногах порта А будут присутствовать логические единицы, то и из регистра GPIOA_IDR мы прочитаем 0xFFFF. Естественно, не надо забывать настраивать порт как вход, хотя по умолчанию он настроен именно так как нам надо, просто сделаем это для понимания сути дела. После этого в бесконечном цикле мы считываем регистр GPIOA_IDR, зануляем в все биты кроме нулевого (кнопка ведь висит на PA0) и сравниваем результат с единицей. Если результат равен единице значит кто-то удерживает нажатой кнопку (и надо зажечь светодиоды), в противном случае (если 0) кнопка отпущена и светодиоды надо погасить. Может возникнуть здравый вопрос: Зачем занулять все остальные биты кроме нулевого? А дело тут вот в чем, все остальные ноги порта (как и PA0) так же настроены на вход без подтяжки. Это означает что в любой момент времени там может быть вообще всё что угодно, всё зависит от количества вокруг контроллера наводок и помех. Следовательно при нажатой кнопке из регистра GPIOA_IDR может прочитаться не только 0000 0000 0000 0001 но и например 0000 0000 0101 0001 а следовательно сравнивать такое число с единицей нельзя, однако после зануления остальных битов вполне можно. Посмотрим на код реализующий всё сказанное выше: 

#include <stm32f10x_rcc.h>
int main(void) {
  //Включим тактирование порта С (со светодиодами) и порта А (с кнопкой)
  RCC->APB2ENR |= (RCC_APB2Periph_GPIOC|RCC_APB2Periph_GPIOA );
  //Настроим ножки со светодиодами как выходы
  GPIOC->CRH |=0x33;
  GPIOC->CRH &= ~0xCC;
  //Настроим ногу PA0 как вход без подтяжки (подтягивающий резистор уже есть на плате)
  GPIOA->CRL |= 0x04;
  GPIOA->CRL &= ~0x11;
  while(1) { //Бесконечный цикл
    if ((GPIOA->IDR & 0x01)==0x01) { //Кнопка нажата?
      GPIOC->BSRR=(GPIO_BSRR_BS8|GPIO_BSRR_BS9); //Зажигаем светодиоды
    } else {
      GPIOC->BSRR=(GPIO_BSRR_BR8|GPIO_BSRR_BR9); //Гасим светодиоды
    }
  }
}

Код не особо сложный, но если вдруг появились вопросы, то они принимаются в комментариях. Напоследок хотелось бы в двух словах рассказать о еще одном регистре с непонятной областью практического применения - GPIOx_LCKR. Он служит для блокировки настроек порта. Это означает что настроив какую либо ножку порта на выход и установив соответствующий бит блокировки в этом регистре, мы не сможем сделать её входом (только после сброса контроллера).

gpiox_lckr

Как видно из рисунка, кроме битов блокировки для каждой ноги порта, тут есть еще бит LCKK. Он используется когда мы хотим установить какой-либо бит блокировки. Алгоритм работы с этим регистром следующий:

  1. Устанавливаем нужные биты блокировки
  2. Записываем в LCKK единицу
  3. Записываем в LCKK ноль
  4. Записываем в LCKK единицу
  5. Читаем из LCKK ноль
  6. Читаем из LCKK единицу (опционально, только для того, чтоб убедиться что блокировка сработала)

Всё, после этого переназначить настройки ног порта (соответствующие установленным битам блокировки) уже не получится. Если вы смогли найти практическое применение этой фичи - сообщить мне об этом пожалуйста :)