Тарифы Услуги Сим-карты

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

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

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

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

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

#include 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. Устроен этот регистр так:

Как видно на картинке в нем есть бит 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. Вот так он устроен:

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

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

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

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


Исходя из вышесказанного, такая комбинация бит настроит обе ножки на выход с максимальной частотой 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. Посмотрим как он устроен:

На каждую ножку порта выделяется по два бита: 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, зануляем в все биты кроме нулевого (кнопка ведь висит на PA 0 ) и сравниваем результат с единицей. Если результат равен единице значит кто-то удерживает нажатой кнопку (и надо зажечь светодиоды), в противном случае (если 0) кнопка отпущена и светодиоды надо погасить. Может возникнуть здравый вопрос: Зачем занулять все остальные биты кроме нулевого? А дело тут вот в чем, все остальные ноги порта (как и PA0) так же настроены на вход без подтяжки. Это означает что в любой момент времени там может быть вообще всё что угодно, всё зависит от количества вокруг контроллера наводок и помех. Следовательно при нажатой кнопке из регистра GPIOA_IDR может прочитаться не только 0000 0000 0000 0001 но и например 0000 0000 01 01 0001 а следовательно сравнивать такое число с единицей нельзя, однако после зануления остальных битов вполне можно. Посмотрим на код реализующий всё сказанное выше:

#include 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. Он служит для блокировки настроек порта. Это означает что настроив какую либо ножку порта на выход и установив соответствующий бит блокировки в этом регистре, мы не сможем сделать её входом (только после сброса контроллера).


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

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

Каждый порт 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) .

В STM32 есть множество очень удобных и гибких в настройке таймеров. Даже у самого младшего микроконтроллера (STM32F030F4P6) есть 4 таких таймера.

Чтобы использовать таймер, нам потребуется подключить файл библиотеки периферии stm32f10x_tim.c. Точно так же, правой кнопкой щёлкаем в Workspace (окно слева) по группе StdPeriphLib, Add –> Add files, файл LibrariesSTM32F10x_StdPeriph_Driversrcstm32f10x_tim.c.

Ещё нужно включить использование заголовка к этому файлу. Открываем stm32f10x_conf.h (правой кнопкой по названию этого файла в коде, «Open stm32f10x_conf.h». Раскомментируем строчку #include «stm32f10x_tim.h».

9. Добавим таймер

Задержка пустым циклом - это кощунство, тем более на таком мощном кристалле как STM32, с кучей таймеров. Поэтому сделаем эту задержку с помощью таймера.

В STM32 есть разные таймеры, отличающиеся набором свойств. Самые простые - Basic timers, посложнее - General purpose timers, и самые сложные - Advanced timers. Простые таймеры ограничиваются просто отсчётом тактов. В более сложных таймерах появляется ШИМ. Самые сложные таймеры, к примеру, могут сгенерировать 3–фазный ШИМ с прямыми и инверсными выходами и дедтаймом. Нам хватит и простого таймера, под номером 6.

Немного теории

Всё, что нам требуется от таймера - досчитывать до определённого значения и генерировать прерывание (да, мы ещё и научимся использовать прерывания). Таймер TIM6 тактируется от системной шины, но не напрямую а через прескалер - простой программируемый счётчик–делитель (подумать только, в СССР выпускались специальные микросхемы–счётчики, причём программируемые были особым дефицитом - а теперь я говорю о таком счётчике просто между делом). Прескалер можно настраивать на любое значение от 1 (т.е. на счётчик попадёт полная частота шины, 24МГц) до 65536 (т.е. 366 Гц).

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

На самом деле процесс немного сложнее: есть два регистра ARR - внешний и внутренний. Во время счёта текущее значение сравнивается именно со внутренним регистром, и лишь при переполнении внутренний обновляется из внешнего. Таким образом, можно безопасно менять ARR во время работы таймера - в любой момент.

Код

Код будет очень похож на предыдущий, т.к. инициализация всей периферии происходит однотипно - за тем лишь исключением, что таймер TIM6 висит на шине APB1. Поэтому включение таймера: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);

Теперь заводим структуру типа TIM_TimeBaseInitTypeDef, инициализируем её (TIM_TimeBaseStructInit), настраиваем, передаём её в функцию инициализации таймера (TIM_TimeBaseInit) и наконец включаем таймер (TIM_Cmd).

Что за магические числа? Как мы помним, на шине присутствует тактовая частота 24МГц (при наших настройках проекта). Настроив предделитель таймера на 24000, мы поделим эту частоту на 24 тысячи, и получим 1кГц. Именно такая частота попадёт на вход счётчика таймера.

Значение же в счётчике - 1000. Значит, счётчик переполнится за 1000 тактов, т.е. ровно за 1 секунду.

После этого у нас действительно появляется работающий таймер. Но это ещё не всё.

10. Разберёмся с прерываниями

Окей, прерывания. Для меня когда–то (во времена PIC) они были тёмным лесом, и я старался вообще их не использовать - да и не умел, на самом деле. Однако, в них заключена сила, игнорировать которую вообще недостойно. Правда, прерывания в STM32 - ещё более сложная штука, особенно механизм их вытеснения; но об этом позже.

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

  1. Включить вообще прерывания таймера TIM6;
  2. Включить прерывание таймера TIM6 на переполнение счётчика;
  3. Написать процедуру–обработчик прерывания;
  4. После обработки прерывания сбросить его.

Включение прерываний

Честно говоря, тут вообще ничего сложного. Первым делом включаем прерывания TIM6: NVIC_EnableIRQ(TIM6_DAC_IRQn); Почему такое название? Потому что в ядре STM32 прерывания от TIM6 и от ЦАП имеют одинаковый номер. Не знаю, почему так сделано - экономия, нехватка номеров или просто какая–то наследная штука - в любом случае, никаких проблем это не принесёт, потому что в этом проекте не используется ЦАП. Даже если в нашем проекте использовался бы ЦАП - мы могли бы при входе в прерывание узнавать, кто конкретно его вызвал. Практически все другие таймеры имеют единоличное прерывание.

Настройка события–источника прерываний: TIM_ITConfig(TIM6, TIM_DIER_UIE, ENABLE); - включаем прерывание таймера TIM6 по событию TIM_DIER_UIE, т.е. событие обновления значения ARR. Как мы помним из картинки, это происходит одновременно с переполнением счётчика - так что это именно то событие, которое нам нужно.

На текущий момент код таймерных дел таков:

Обработка прерываний

Сейчас запускать проект нельзя - первое же прерывание от таймера не найдёт свой обработчик, и контроллер повиснет (точнее, попадёт в обработчик HARD_FAULT, что по сути одно и то же). Нужно его написать.

Немного теории

Он должен иметь совершенно определённое имя, void TIM6_DAC_IRQHandler(void). Это имя, так называемый вектор прерывания, описано в файле startup (в нашем проекте это startup_stm32f10x_md_vl.s - можете сами увидеть, 126 строка). На самом деле вектор - это адрес обработчика прерывания, и при возникновении прерывания ядро ARM лезет в начальную область (в которую транслирован файл startup - т.е. его местоположение задано совершенно жёстко, в самом начале флеш–памяти), ищет там вектор и переходит в нужное место кода.

Проверка события

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

В нашей программе эта проверка будет выглядеть так: if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) - всё понятно, функция TIM_GetITStatus проверяет наличие указанного события у таймера, и возвращает 0 или 1.

Очистка флага UIF

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

Выполнение действий в прерывании

Будем просто переключать состояние светодиода, как и в первой программе. Разница в том, что теперь наша программа делает это более сложно! На самом деле, так писать гораздо правильнее.

Используем глобальную переменную int state=0;

11. Весь код проекта с таймером

Архив с проектом таймера.

Ну и к слову, таймер умеет переключать ногу и сам, без прерываний и ручной обработки. Это будет наш третий проект.

Весь цикл:

1. Порты ввода–вывода

2. Таймер и прерывания

3. Выходы таймера

4. Внешние прерывания и NVIC

5. Ставим FreeRTOS

Недавно коллега меня подсадил на идею создания умного дома, я даже успел заказать себе десятки разных датчиков. Встал вопрос о выборе Микроконтроллера (далее МК) или платы. После некоторых поисков нашёл несколько вариантов. Среди них были и Arduino (включая его клоны, один из которых себе заказал ради того, чтобы просто побаловаться) и Launchpad , но всё это избыточно и громоздко (хотя в плане программирования гораздо проще, но тему холиваров поднимать не буду, у каждого свои вкусы). В итоге решил определяться не с готовой платой, а взять только МК и делать всё с нуля. В итоге выбирал между Atmel ATtiny (2313), Atmel ATmega (решил отказаться т.к. не смог найти за адекватные деньги), STM32 (Cortex на ядре ARM ). С тинькой я уже успел побаловаться, так что взял себе STM32VL-Discovery . Это можно назвать вступлением к циклу статей по STM32 . Оговорюсь сразу, автором большинства этих статей буду являться не я, т.к. сам только познаю, здесь я публикую их в первую очередь для себя, чтоб удобнее было искать если что-то забуду. И так поехали!

Общие сведения

Микроконтроллеры семейства STM32 содержат в своём составе до семи 16-разрядных портов ввода-вывода c именами от PORTA до PORTG. В конкретной модели микроконтроллера без исключений доступны все выводы портов, общее количество которых зависит от типа корпуса и оговорено в DataSheet на соответствующее подсемейство.

Для включения в работу порта x необходимо предварительно подключить его к шине APB2 установкой соответствующего бита IOPxEN в регистре разрешения тактирования периферийных блоков RCC_APB2ENR :

Управление портами STM32 осуществляется при помощи наборов из семи 32-разрядных регистров:

  • GPIOx_CRL, GPIOx_CRH – задают режимы работы каждого из битов порта в качестве входа или выхода, определяют конфигурацию входных и выходных каскадов.
  • GPIOx_IDR – входной регистр данных для чтения физического состояния выводов порта x.
  • GPIOx_ODR – выходной регистр осуществляет запись данных непосредственно в порт.
  • GPIOx_BSRR – регистр атомарного сброса и установки битов порта.
  • GPIOx_BSR – регистр сброса битов порта.
  • GPIOx_LCKR – регистр блокировки конфигурации выводов.

Режимы работы выводов GPIO

Режимы работы отдельных выводов определяются комбинацией битов MODEy и CNFy регистров GPIOx_CRL и GPIOx_CRH (здесь и далее: x-имя порта, y- номер бита порта).

GPIOx_CRL - регистр конфигурации выводов 0…7 порта x :

Структура регистра GPIOx_CRH аналогична структуре GPIOx_CRL и предназначена для управления режимами работы старших выводов порта (биты 8…15).

Биты MODEy указанных регистров определяют направление вывода и ограничение скорости переключения в режиме выхода:

  • MODEy = 00: Режим входа (состояние после сброса);
  • MODEy = 01: Режим выхода, максимальная скорость – 10МГц;
  • MODEy = 10: Режим выхода, максимальная скорость – 2МГц;
  • MODEy = 11: Режим выхода, максимальная скорость – 50МГц.

Биты CNF задают конфигурацию выходных каскадов соответствующих выводов:

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

  • CNFy = 00: Аналоговый вход;
  • CNFy = 01: Вход в третьем состоянии (состояние после сброса);
  • CNFy = 10: Вход с притягивающим резистором pull-up (если PxODR=1) или pull-down (если PxODR=0);
  • CNFy = 11: Зарезервировано.

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

  • CNFy = 00: Двухтактный выход общего назначения;
  • CNFy = 01: Выход с открытым стоком общего назначения;
  • CNFy = 10: Двухтактный выход с альтернативной функцией;
  • CNFy = 11: Выход с открытым стоком с альтернативной функцией.

С целью повышения помехоустойчивости все входные буферы содержат в своём составе триггеры Шмидта. Часть выводов STM32 , снабженных защитными диодами, соединёнными с общей шиной и шиной питания, помечены в datasheet как FT (5V tolerant) - совместимые с напряжением 5 вольт.

Защита битов конфигурации GPIO

Для защиты битов в регистрах конфигурации от несанкционированной записи в STM32 предусмотрен регистр блокировки настроек GPIOx_LCKR
GPIOx_LCKR - регистр блокировки настроек вывода порта:

Для защиты настроек отдельного вывода порта необходимо установить соответствующий бит LCKy. После чего осуществить последовательную запись в разряд LCKK значений "1” - "0” - "1” и две операции чтения регистра LCKR , которые в случае успешной блокировки дадут для бита LCKK значения "0” и "1” .

Защита настроечных битов сохранит своё действие до очередной перезагрузки микроконтроллера.

Файл определений для периферии микроконтроллеров STM32 определяет отдельные группы регистров, объединённые общим функциональным назначением (в том числе и GPIO ), как структуры языка Си, а сами регистры как элементы данной структуры.

STM32 - с нуля до RTOS. 2: Таймер и прерывания

Например:

GPIOC->BSRR – регистр BSRR установки/сброса порта GPIOC.
Воспользуемся определениями из файла stm32f10x.h для иллюстрации работы с регистрами ввода-вывода микроконтроллера STM32F100RB установленного в стартовом наборе STM32VLDISCOVERY :

Запись и чтение GPIO

Для записи и чтения портов предназначены входной GPIOx_IDR и выходной GPIOx_ODR регистры данных.

Запись в выходной регистр ODR порта настроенного на вывод осуществляет установку выходных уровней всех разрядов порта в соответствии с записываемым значением. Если вывод настроен как вход с подтягивающими резисторами, состояние соответствующего бита регистра ODR активирует подтяжку вывода к шине питания (pull-up, ODR=1) или общей шине микроконтроллера (pull-down, ODR=0).

Чтение регистра IDR возвращает значение состояния выводов микроконтроллера настроенных как входы:

Сброс и установка битов порта

Для атомарного сброса и установки битов GPIO в микроконтроллерах STM32 предназначен регистр GPIOx_BSRR . Традиционный для архитектуры ARM способ управления битами регистров не требующий применения операции типа "чтение-модификация-запись” позволяет устанавливать и сбрасывать биты порта простой записью единицы в биты установки BS (BitSet) и сброса BR (BitReset) регистра BSRR . При этом запись в регистр нулевых битов не оказывает влияния на состояние соответствующих выводов.

GPIOx_BSRR – регистр сброса и установки битов порта:

Альтернативные функции GPIO и их переназначение (remapping)
Практически все внешние цепи специального назначения STM32 (включая выводы для подключения кварцевых резонаторов, JTAG/SWD и так далее) могут быть разрешены на соответствующих выводах микроконтроллера, либо отключены от них для возможности их использования в качестве выводов общего назначения. Выбор альтернативной функции вывода осуществляется при помощи регистров с префиксом "AFIO ”_.
Помимо этого регистры AFIO _ позволяют выбирать несколько вариантов расположения специальных функций на выводах микроконтроллера. Это в частности относится к выводам коммуникационных интерфейсов, таймеров (регистры AFIO_MAPR ), выводам внешних прерываний (регистры AFIO_EXTICR ) и т. д.

Подробнее смотрите документы "Reference manual” на соответствующую подгруппу микроконтроллеров.

Проекты к статье:

  1. µVision 4.13a -> STM32GPIO_emcu_uV
  2. IAR ARM 6.0 -> STM32GPIO_emcu_iar
  3. IAR ARM 6.21 -> STM32GPIO_emcu_iar_V6.21

Для управления GPIO STM32 Вы можете применить макросы написанные как альтернативу далеко не оптимальным по мнению многих библиотекам от ST: gpio_emcu.h

Дополнительный материал:

  1. STM32F10xxx Reference manual. Справочное руководство разработчика
  2. STM32F100xx Reference manual. Справочное руководство разработчика
  3. STM32F105xx, STM32F107xx Datasheet
  4. STM32F100x4, STM32F100x6, STM32F100x8, STM32F100xB Data Sheet
  5. Руководство по созданию проектов для STM32DISCOVERY в IAR
  6. Руководство по созданию проектов для STM32DISCOVERY в MDK-ARM, uVision

Другие части

  1. (эта часть) Программирование STM32. Часть 1. GPIO, порты ввода-вывода STM32
  2. Программирование STM32. Часть 2. Система тактирования STM32
  3. Программирование STM32. Часть 3. Система прерываний
  4. Программирование STM32. Часть 4. Внешние прерывания EXTI

Basic таймеры в STM32

Таймеры — это такая периферия контроллера STM32 позволяющая нам очень точно отсчитывать интервалы времени. Это пожалуй одна из самых важных и наиболее используемых функций, однако есть и другие. Следует начать с того, что в контроллерах STM32 существуют таймеры разной степени крутости. Самые простые это Basic timers . Они хороши тем, что очень просто настраиваются и управляются при помощи минимума регистров. Все что они умеют это отсчитывать временные интервалы и генерировать прерывания когда таймер дотикает до заданного значения. Следующая группа (general-purpose timers ) гораздо круче первой, они умеют генерировать ШИМ, умеют считать испульсы поступающие на определённые ножки, можно подключать энкодер итд. И самый крутой таймер это advanced-control timer , думаю что его я использовать не буду еще очень долго так как мне пока без надобности управлять трехфазным электродвигателем. Начать знакомство с таймерами следует с чего попроще, я решил взяться за Basic таймеры. Задача которую я себе поставил: Заставить таймер генерить прерывания каждую секунду.

Первым делом отмечу, что Basic таймеры (TIM6 и TIM7) прицеплены к шине APB1, поэтому в случае если частота тактовых импульсов на ней будет меняться, то и таймеры начнут тикать быстрее или медленнее. Если ничего не менять в настройках тактирования и оставить их по умолчанию, то частота APB1 составляет 24МГц при условии что подключен внешний кварц на частоту 8 МГц. Вообще система тактирования у STM32 очень замысловатая и я попробую про неё нормально написать отдельный пост. А пока просто воспользуемся теми настройками тактирования которые задаются кодом автоматически сгенерированым CooCox’ом. Начать стоит с самого главного регистра — TIMx_CNT (здесь и далее x — номер basic таймера 6 или 7). Это счётный 16-ти битный регистр, занимающийся непосредственно счётом времени. Каждый раз когда с шины APB1 приходит тактовый импульс, содержимое этого регистра увеличивается на едницу. Когда регистр переполняется, все начинается с нуля. При нашей дефолтной частоте шины APB1, таймер за одну секунду тикнет 24 млн раз! Это очень дофига, и поэтому у таймера есть предделитель, управлять которым мы можем при помощи регистра TIMx_PSC . Записав в него значение 24000-1 мы заставим счётный регистр TIMx_CNT увеличивать свое значение каждую милисекунду (Частоту APB1 делим на число в регистре предделителе и получаем сколько раз в секунду увеличивается счётчик). Единицу нужно вычесть потому, что если в регистре ноль то это означает, что включен делитель на единицу. Теперь, когда счётный регистр дотикает до 1000 мы можем точно заявить, что прошла ровно одна секунда! И че теперь опрашивать счётный регистр и ждать пока там появится 1000? Это не наш метод, ведь мы можем заюзать прерывания! Но вот беда, прерывание у нас всего одно, и возникает оно когда счётчик обнулится. Для того чтоб счётчик обнулялся досрочно, а не когда дотикает до 0xFFFF, служит регистр TIMx_ARR . Записываем в него то число до которого должен досчитывать регистр TIMx_CNT перед тем как обнулиться. Если мы хотим чтоб прерывание возникало раз в секунду, то нам нужно записать туда 1000. По части непосредственно отсчёта времени это все, но таймер сам по себе тикать не начнет. Его нужно включить установив бит CEN в регистре TIMx_CR1 . Этот бит разрешает начать отсчёт, соответственно если его сбросить то отсчет остановится (ваш К.О.).

Таймеры на STM32. Настройка базового таймера

В регистре есть и другие биты но они нам не особо интересны. Зато интересен нам еще один бит, но уже в регистре TIMx_DIER . Называется он UIE, установив его мы разрешаем таймеру генерить прерывания при сбросе счётного регистра. Вот собственно и всё, даже не сложней чем в каких-нибудь AVRках. Итак небольше резюме: Чтоб заюзать basic таймер нужно:

  1. Установить предделитель чтоб таймер не тикал быстро (TIMx_PSC )
  2. Задать предел до которого таймер должен дотикать перед своим сбросом (TIMx_ARR )
  3. Включить отсчет битом CEN в регистре TIMx_CR1
  4. Включить прерывание по переполнению битом UIE в регистре TIMx_DIER

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

#include «stm32f10x.h» #include «stm32f10x_gpio.h» #include «stm32f10x_rcc.h» int main() { GPIO_InitTypeDef PORT; //Включаем порт С и таймер 6 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); // Настроим ноги со светодиодами на выход PORT.GPIO_Pin = (GPIO_Pin_9 | GPIO_Pin_8); PORT.GPIO_Mode = GPIO_Mode_Out_PP; PORT.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOC, &PORT); TIM6->PSC = 24000 — 1; // Настраиваем делитель что таймер тикал 1000 раз в секунду TIM6->ARR = 1000 ; // Чтоб прерывание случалось раз в секунду TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт! NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания while(1) { //Программа ничего не делает в пустом цикле } } // Обработчик прерывания TIM6_DAC void TIM6_DAC_IRQHandler(void) { TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF GPIOC->ODR^=(GPIO_Pin_9 | GPIO_Pin_8); //Инвертируем состояние светодиодов }

Стоит добавить небольшое примечание к обработчику прерывания. Дело в том, что он у нас используется сразу двумя блоками периферии: таймером 6 и DAC’ом. Это означает, что если вы будете писать программу которая разрешает прерывания от обоих этих периферийных устройств, то в теле обработчика необходимо проверять кто же из них вызвал прерывание. В нашем случае я не стал этого делать, так как ни каких прерываний от DAC возникнуть не может. Он не настроен, а по дефолту прерывания запрещены. В следующий раз рассмотрим general-purpose таймеры и их практическое применение.

Генерация ШИМ в STM32

В предыдущей статье про базовые таймеры, мы в очередной раз мигали светодиодами, а в этот раз пойдем гораздо дальше и попробуем вкурить как заставить контроллер STM32 генерировать ШИМ. Для этого нам придётся использовать один из таймеров общего назначения, ведь именно у них есть всё что для этого нужно. Весь остальной функционал этих таймеров конечно впечатляет, но в моей практике он пока не пригодился. Хотя возможно, что в будущем мне пригодятся такие полезные фичи как функция подсчёта внешних импульсов и возможность аппаратно обрабатывать повороты энкодера. Но пока займемся ШИМом. Есть вот такая схема из контроллера, трех резисторов и RGB светодиода которым мы будем управлять. Управление заключается в том, чтоб плавно зажечь и погасить каждый цвет. Разумеется можно взять три разных светодиода если нет RGB.

Мы подключили светодиод к этим выводам не случайно. Таймеры общего назначения могут генерировать ШИМ только на определённых ножках. Поскольку мы будем использовать таймер 2, то в нашем распоряжении есть 4 ноги (PA0-PA3). Чтоб таймер мог их использовать нужно разрешить это аж в двух местах: Настроить три ноги (PA1-PA3) как выход с альтернативной функцией и разрешить в настройках таймера дергать эти ноги для генерации ШИМа. Для этого нам потребуется регистр CCER

Если установить в единицу один из битов выделенных синим цветом, то таймеру будет позволено использовать для ШИМа соответствующую ногу. Из схемы видно, что нам потребуется установить биты CC2E , CC3E и CC4E . Теперь нам нужно настроить режим ШИМа: Прямой или инверсный (я не претендую на правильность терминологии). Разница вполне очевидна — при прямом ШИМе чем больше число в регистре сравнения — тем больше коэффициент заполнения ШИМа. В случае инверсного ШИМа все наоборот. Записали ноль в регистр сравнения — коэффициент заполнения 100%.

Работаем с простыми таймерами STM32 F4 discovery

Для выбора режима используются два регистра CCMR1 и CCMR2:

На настройку каждого канала выделяется аж по 8 бит! Но к счастью нам интересны только три бита OCxM которые я отметил синим. То что отмечено серым — это те же самые биты но с другим названием, они используются если канал таймера работает в режиме захвата. Рассматривать все комбинации битов я не буду, так как большинство из них к ШИМу отношения не имеют. Нам потребуются только две комбинации бит:

Прямой ШИМ

Инверсный ШИМ

RGB светодиод у меня с общим катодом и поэтому я использую инверсный ШИМ. Таким образом нам следует установить все три бита OCxM для трех каналов на которых висят светодиоды. И это всё! Настройка ШИМа закончена, теперь нужно только запустить таймер установив бит CEN в регистре CR1. Для управления скважностью просто пишем число от 0x0000 до 0xFFFF в регистры CCRx , где x номер канала. Собственно следующий код реализует то что было задумано в начале этой статьи: Наращивает яркость светодиода а потом снижает её до нуля и переходит к следующему.

Процесс повторяется бесконечно, смотрится красиво 🙂

#include «stm32f10x.h» #include «stm32f10x_gpio.h» #include «stm32f10x_rcc.h» void delay(void) { volatile uint32_t i; for (i=1; i != 0xF000; i++); } int main() { //Включем порт А RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE); //Включаем Таймер 2 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); GPIO_InitTypeDef PORT; // Настроим ноги со светодиодами на выход PORT.GPIO_Pin = (GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3); //Будем использовать альтернативный режим а не обычный GPIO PORT.GPIO_Mode = GPIO_Mode_AF_PP; PORT.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, &PORT); //Разрешаем таймеру использовать ноги PA1,PA2,PA3 для ШИМа TIM2->CCER |= (TIM_CCER_CC2E|TIM_CCER_CC3E|TIM_CCER_CC4E); // Для всех трех каналов задаем инверсный ШИМ. TIM2->CCMR1|=(TIM_CCMR1_OC2M_0| TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2); TIM2->CCMR2|=(TIM_CCMR2_OC3M_0 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC4M_0 | TIM_CCMR2_OC4M_1 | TIM_CCMR2_OC4M_2); //Запускаем таймер! TIM2->CR1 |= TIM_CR1_CEN; //После этого пишем данные в TIM2->CCRx — и яркость светодиодов меняется uint32_t pwm_arr={0,0,6553,13107,19660,26214,32768, 39321,45875,52428,58982,65535}; uint8_t i; while(1) { for (i=1;i<=11;i++) { TIM2->CCR3=pwm_arr[i]; delay(); } for (i=11;i>=1;i—) { TIM2->CCR3=pwm_arr[i]; delay(); } for (i=1;i<=10;i++) { TIM2->CCR2=pwm_arr[i]; delay(); } for (i=11;i>=1;i—) { TIM2->CCR2=pwm_arr[i]; delay(); } for (i=1;i<=10;i++) { TIM2->CCR4=pwm_arr[i]; delay(); } for (i=11;i>=1;i—) { TIM2->CCR4=pwm_arr[i]; delay(); } } }

Надеюсь, что этот код поможет вам запустить ШИМ на STM32 а мне он поможет не забыть то, что я раскуривал почти пол дня. Не сложные вопросы можно писать ниже.

Карманный осциллограф «Лори» на микроконтроллере STM32F103

Максим Керимов
Декабрь 2016 г.

Постановка задачи

Сделать простейший карманный осциллограф с минимальными затратами времени и средств.

Список компонентов

  • Китайский клон платы «Maple Mini» с микроконтроллером STM32F103C8T6.
  • Дисплей 1.8 TFT 128×160 SPI с драйвером ST7735.
  • Пять резисторов и два конденсатора (рис. 3).
  • Линейный регулятор с малым падением напряжения AMS1117-3.3 (по желанию).
  • Щуп-зажим «пинцет» — 2 шт.
  • Кнопка миниатюрная нормально разомкнутая без фиксации, с щелчком.

Рис. 1. Тестовый запуск осциллографа. Синусоида сгенерирована саунд бластером, от того ступенчатая.

Характеристики

7 диапазонов с ценой деления (клетки): 7 µS, 28 µS, 113 µS, 559 µS, 2 mS, 10 mS, 20 mS.
Чувствительность: 0.25 и 1.0 В/дел.
Максимальная амплитуда входного сигнала: 6 В.
Входное сопротивление: 20 kΩ.
Питание: 4 аккумулятора АА.
Потребляемый ток: 80 mA.

Сигнал какой частоты можно увидеть?

Теоретически можно увидеть 477 кГц. Отличить меандр от пилы, теоретически, можно на частотах 350 кГц и ниже. Практически же, более-менее комфортно можно наблюдать сигналы до 200 кГц. Размер клетки: 20 x 20 px.

«Частота развёртки» нашего осциллографа зависит от быстродействия АЦП. В STM32F103 разрядность АЦП фиксирована и равна 12. Это в полтора раза больше, чем нам нужно. В STM32F407, например, разрядность можно уменьшить, что сократит время измерений. Но это уже другая история с другим бюджетом.

Рис. 2. Подключение дисплея.

Рис. 3. Питание и входная цепь.

Делитель напряжения R1-R2 служит для контроля уровня заряда аккумуляторов. В правом верхнем углу экрана - пиктограмма батарейки, как на мобильном телефоне (на фото отсутствует).

Внешний регулятор напряжения нужен не всегда. На плате микроконтроллера есть свой регулятор 3.3 В 100 мА. Если питать дисплей от него, будет греться. На платах другого типа (с большим разъёмом JTAG) стоит как раз AMS1117, для них внешний не нужен. На некоторых дисплеях тоже есть AMS1117 (и перемычка). Решайте сами.

Последовательно с аккумуляторами имеет смысл поставить выключатель питания ПД9-1 или аналогичный.

Если есть желание увеличить размер своего импеданса, на вход можно добавить неинвертирующий повторитель на ОУ, что позволит достичь значения 1 MΩ и более. Питать ОУ следует непосредственно от аккумуляторов напряжением 4.8 — 5.4 В.

Принцип действия

Половина текста программы - это всевозможные инициализации. Принцип действия цифрового осциллографа прост и очевиден.

АЦП производит серию непрерывных последовательных измерений уровня сигнала. Полученные значения складываются в память средствами DMA. Каждый раз мы засекаем время и определяем продолжительность серии замеров. Так мы узнаём цену деления оси времени.

Анализируя записанные значения уровня сигнала, ищем первый экстремум, после чего рисуем сигнал на экране. Так мы пытаемся сделать подобие синхронизации. Она неплохо работает на гладких сигналах и практически бесполезна на широкополосных.

Даём пользователю насладиться картинкой в течение одной секунды, сами в это время опрашиваем кнопку. Кратковременное нажатие кнопки переключает диапазоны по кругу. Долгое нажатие меняет чувствительность.

STM32 для начинающих. Урок 3. Таймеры STM32.

Затем всё повторяется.

Для компиляции я использую среду CooCox CoIDE. Не выложил сюда Кокс-проект, поскольку он содержит абсолютные пути к файлам. Проще создать новый, чем править все пути. После создания проекта не забудьте подключить библиотеки: RCC, GPIO, DMA, SPI, TIM, ADC.

Прошивал при помощи программатора-отладчика ST-Link V2. Можно и без него, через USB-Serial адаптер.

Использованы материалы:
Проект шотландского мастера Pingumacpenguin
Adafruit Display
STM32 Сохранение данных АЦП с помощью DMA

Текст программы

  • main.c
  • lcd7735.c - Дисплей и SPI. Инициализация и функции.
  • delay.c - Счётчик: инициализация, функции пауз.
  • ADC.c - АЦП и DMA.
  • font7x15.h - Шрифт.

STM32 с нуля. Таймеры.

В микроконтроллерах STM32 есть несколько таймеров, способных работать в режиме широтно-импульсной модуляции. Такой функциональностью обладают все таймеры, кроме Basic timers (TIM6 и TIM7).

Всё, этого достаточно для того, чтобы на [В микроконтроллерах STM32 есть несколько таймеров, способных работать в режиме широтно-импульсной модуляции. Такой функциональностью обладают все таймеры, кроме Basic timers (TIM6 и TIM7).

Приведу пример использования таймера TIM3 в этом режиме.

Всё, этого достаточно для того, чтобы на](http://catethysis.ru/stm32-%e2%86%92-%d0%bf%d0%be%d1%80%d1%82%d1%8b-gpio/ “STM32 → Порты GPIO”) появился ШИМ-сигнал. Как это сделано?

Таймер получает тактовые импульсы с шины APB, чья частота в два раза меньше частоты ядра (24МГц в нашем случае) (ссылка на RCC), и они проходят через прескалер, настроенный нами на 10 - т.е. получается 1.2МГц. Таймер настроен на отсчёт 1000 тактов, после которых берёт новое значение из регистра ARR, которое мы не изменяем - т.е. те же 1000, это период ШИМ-сигнала. В начале цикла таймер выводит в выход «1», а спустя 200 тактов сбрасывает в «0» - это скважность ШИМ.

Доброго времени суток! Сегодня мы займемся изучением GPIO! И, в первую очередь, давайте посмотрим в каких режимах могут работать порты ввода-вывода в STM32F10x. А режимов этих существует море, а именно:

  • Input floating
  • Input pull-up
  • Input-pull-down
  • Analog
  • Output open-drain
  • Output push-pull
  • Alternate function push-pull
  • Alternate function open-drain

А если по-нашему, то при работе на вход:

  • Вход – Hi-Z
  • Вход – подтяжка вверх
  • Вход – подтяжка вниз
  • Вход – аналоговый

При работе порта на выход имеем следующие варианты:

  • Выход – с открытым коллектором
  • Выход – двухтактный
  • Альтернативные функции – выход типа «с открытым коллектором»
  • Альтернативные функции – двухтактный выход

Вот кстати документация на STM32F103CB –

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

Вот, например, выводы PA9, PA10:

В столбце Default видим, какие функции будут выполнять эти пины при их настройке для работы в режиме Alternative function. То есть, настроив эти пины соответствующим образом они из просто PA9 и PA10 превратятся в Rx и Tx для USART1. А для чего же тогда столбец Remap ? А это не что иное, как очень полезная функция ремаппинга портов. Благодаря ремапу, Tx USARTA ’а , например, может переместится с пина PA9 на PB6. Довольно часто эта функция оказывается чертовски полезной.

Ну с режимами вроде бы все более-менее понятно, пришло время окинуть взором регистры, которыми управляются порты ввода-вывода.

Раз уж только что обсудили в каких режимах могут существовать выводы STM32F10x, сразу же давайте прошарим как же их можно собственно перевести в нужный режим. А для этого выделены аж два регистра – CRL и CRH. В первом конфигурируются выводы от 0 до 7, во втором, соответственно от 8 до 15. Регистры, как вы помните, 32-разрядные. То есть на 8 выводов приходтся 32 бита – получается 4 бита на одну ножку. Открываем даташит и видим:

Например, надо нам настроить ножку PB5. Идем в регистр GPIOB->CRL и выставляем сответствующие биты так как нам требуется (на картинке 32-х битный регистр CRL). Для PB5 это биты:

После восьмибиток может показаться все достаточно сложным и каким то корявым, но на самом деле реализовано все довольно изящно =). Посмотрим, что тут есть еще.

Выходной регистр GPIOx_ODR – напоминает регистр PORTx в AVR. Все что попадает в этот регистр сразу же попадает во внешний мир. Регистр 32-разрядный, а ножек всего 16. Как думаете, для чего используются оставшиеся 16? Все очень просто, биты регистра с 15 по 31 не используются вовсе)

Входной регистр GPIOx_IDR – аналог PINx в AVR. Структура его похожа на упомянутую структуру ODR. Все, что появляется на входе микроконтроллера, сразу же оказывается во входном регистре IDR.

Еще два полезных регистра GPIOx_BSSR и GPIOx_BRR. Они позволяют менять значения битов в регистре ODR напрямую, без использования привычных бит-масок. То есть, хочу я, например, выставить в единицу пятый бит ODR. Записываю единичку в пятый бит GPIOx_BSSR, и все, цель достигнута. Вдруг захотелось сбросить пятый бит ODR – единицу в 5 бит GPIOx_BRR и готово.

Итак, основные регистры рассмотрели, но, на самом-то деле, мы в наших примерах будем делать все иначе, используя Standard Peripheral Library. Так что лезем ковырять библиотеку. За GPIO в SPL отвечают файлы stm32f10x_gpio.h и stm32f10x_gpio.c . Открываем их оба и видим очень много непонятных цифр-букв-значков итд.

На самом деле, все очень просто и понятно. За конфигурацию портов отвечает структура GPIO_InitTypeDef .

typedef struct { uint16_t GPIO_Pin; // Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define */ GPIOSpeed_TypeDef GPIO_Speed; // Specifies the speed for the selected pins. This parameter can be a value of @ref GPIOSpeed_TypeDef */ GPIOMode_TypeDef GPIO_Mode; // Specifies the operating mode for the selected pins. This parameter can be a value of @ref GPIOMode_TypeDef */ } GPIO_InitTypeDef;

Видим, что структура имеет три поля: GPIO_PIN, GPIO_Speed и GPIO_Mode . Нетрудно догадаться, что первая отвечает за номер ножки порта, которую мы хотим настроить, вторая – за скорость работы порта, ну и третья, собственно, за режим работы. Таким образом, для настройки вывода нам всего лишь нужно объявить переменную типа структуры и заполнить ее поля нужными значениями. Все возможные значения полей тут же – в stm32f10x_gpio.h . Например,

typedef enum { GPIO_Mode_AIN = 0x0 , GPIO_Mode_IN_FLOATING = 0x04 , GPIO_Mode_IPD = 0x28 , GPIO_Mode_IPU = 0x48 , GPIO_Mode_Out_OD = 0x14 , GPIO_Mode_Out_PP = 0x10 , GPIO_Mode_AF_OD = 0x1C , GPIO_Mode_AF_PP = 0x18 } GPIOMode_TypeDef;

Все значения уже рассчитаны создателями SPL, так что для настройки какого-нибудь вывода для работы в режиме Output push-pull надо всего лишь в соответствующей структуре задать поле: GPIO_Mode = GPIO_Mode_Out_PP.

Ну вот, структура объявлена, поля заполнены как надо, что же дальше? Ведь мы всего лишь создали переменную. Причем тут регистры, микроконтроллеры и вообще электроника? Лезем в файл stm32f10x_gpio.c и находим там тучу различных функций для работы с STM32 GPIO. Рассмотрим функцию GPIO_Init() (код приводить не буду, все в файле библиотеки). Так вот, эта функция как раз и связывает нашу созданную структуру и конкретные регистры контроллера. То есть мы передаем в эту функцию переменную, в соответствии с которой выставляются нужные биты нужных регистров микроконтроллера. Все очень просто, но от этого не менее гениально. Поковыряйте еще файлы библиотеки. Там функции на любой случай есть) Кстати очень удобно – перед функцией идет описание переменных, которые она принимает и возвращает, а также описание собственно того, что эта функция призвана делать. Так что, разобраться несложно, но надо немного дружить с английским. Хотя без этого никуда;)

Отвлечемся ненадолго от портов ввода-вывода и обсудим один довольно тонкий момент. Чтобы использовать порты, либо любую другую периферию, ОБЯЗАТЕЛЬНО надо включить тактирование. И порты, и периферия изначально отключены от тактирования, так что без этого действия ничего не заведется. Программа скомпилируется, но на деле работать ничего не будет. За тактирование в SPL отвечают файлы stm32f10x_rcc.c и stm32f10x_rcc.h . Не забывайте добавлять их в проект.

Давайте уже перейдем к программированию. Как это принято, заставим диодик помигать) Чтобы получше разобраться с Standard Peripheral Library немножко усложним обычное мигание диодом – будем опрашивать кнопку, и если она нажата – диод загорается, иначе – гаснет. Запускаем Keil, создаем проект, добавляем все нужные файлы, не забываем про CMSIS. Из SPL для этого проекта нам понадобятся 4 файла, уже упомянутые выше. Создание нового проекта описано в предыдущей статье учебного курса. Также там можно найти ссылки на библиотеки)

Итак, код:

/****************************gpio.c*********************************/ //Подключаем все нужные файлы #include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_gpio.h" //Тут будет вся инициализация всей использующейся периферии void initAll() { //Объявляем переменную port типа GPIO_InitTypeDef GPIO_InitTypeDef port; //Это функция из файла stm32f10x_rcc.c, включает тактирование на GPIOA //GPIOA сидит на шине APB2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE) ; //Про эту функцию напишу чуть ниже GPIO_StructInit(& port) ; //Заполняем поля структуры нужными значениями //Первый вывод – вход для обработки нажатия кнопки – PA1 port.GPIO_Mode = GPIO_Mode_IPD; port.GPIO_Pin = GPIO_Pin_1; port.GPIO_Speed = GPIO_Speed_2MHz; //А про эту функцию мы уже говорили //Отметим только что один из параметров – указатель(!) на //нашу структуру GPIO_Init(GPIOA, & port) ; //Настраиваем вывод, на котором будет висеть диодик – PA0 port.GPIO_Mode = GPIO_Mode_Out_PP; port.GPIO_Pin = GPIO_Pin_0; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, & port) ; } /*******************************************************************/ int main() { //Объявляем переменную для хранения состояния кнопки uint8_t buttonState = 0 ; initAll() ; while (1 ) { //С помощью функции из SPL считываем из внешнего мира //состояние кнопки buttonState = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) ; if (buttonState == 1 ) { GPIO_SetBits(GPIOA, GPIO_Pin_0) ; } else { GPIO_ResetBits(GPIOA, GPIO_Pin_0) ; } } } /****************************End of file****************************/

Кстати, возможно кто-то обратит внимание на наличие скобок { }, несмотря на всего лишь одну инструкцию в теле if и else . А это уже привычка) Очень рекомендуется так писать, особенно при разработке крупных проектов. При дописывании/исправлении программы невнимательный программист может не обратить внимания на отсутствие скобок и дописать вторую инструкцию, которая, как вы понимаете, уже окажется все блока if или else . Та же тема с циклами. Когда над проектом работает много народу, нет никаких гарантий, что кто-нибудь не окажется невнимательным, так что, чтобы не тратить минуты/часы на последующие поиски косяка, рекомендую ставить эти скобки всегда) Хотя может, кто-то и не согласится с такой логикой.

Нажимаем F7, компилируем, и вот наша первая программа для STM готова. Вроде бы код довольно подробно откомментирован, так что поясню только пару моментов.

Функция GPIO_StructInit(&port) – принимает в качестве аргумента адрес переменной port .

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

Еще две функции, которые мы использовали:

  • GPIO_SetBits(GPIOA, GPIO_Pin_0);
  • GPIO_ResetBits(GPIOA, GPIO_Pin_0);

Ну вы и так догадались для чего они 😉

Итак мы закончили рассматривать STM32 порты ввода-вывода. В следующей статье познакомимся со средствами Keil’а для отладки.

Мы рассмотрели работу с битовыми операциями и двоичными числами, тем самым заложив основу для рассмотрения новой темы. В этом уроке мы с Вами рассмотрим очередной вопрос: что такое регистры и как с ними работать ?

Память и регистры

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

В целом, регистр - это особый вид памяти внутри микроконтроллера, который используется для управления процессором и периферийными устройствами. Каждый регистр в архитектуре ARM представляет собой ячейку памяти и имеет длину в 32 бита, где каждый бит можно представить в виде крошечного выключателя с помощью которого осуществляется управление тем или иным параметром микроконтроллера.

Каждый из регистров имеет свой порядковый номер – адрес. Адрес регистра обозначается 32-битным числом представленным в шестнадцатеричной системе счисления. Путём записи по адресу регистра определённой комбинации единиц и нулей, которые обычно представлены в шестнадцатеричном виде, осуществляется настройка и управление тем или иным узлом в МК. Вспомним, что в программе для работы с битовыми операциями, мы могли представить в виде шестнадцатеричного числа произвольный набор единиц и нулей. В целом стоит отметить, что существует два вида регистров: регистры общего назначения и специальные регистры. Первые расположены внутри ядра МК, а вторые являются частью RAM-памяти.

Так же стоит отметить, что Reference Manual , который мы скачивали в первом уроке , это один большой справочник по регистрам, содержащимся в целевом микроконтроллере, а библиотека CMSIS позволяет нам оперировать символьными именами регистров вместо числовых адресов. Например, к регистру 0x40011018 мы можем обратиться просто, используя символьное имя GPIOC_BSSR . Конкретные примеры конфигурирования мы рассмотрим в ходе разбора нашей программы из .

Итак, обычно структура регистра описывается в виде небольшой таблицы с указанием:

  1. Названия регистра и описания его назначения
  2. Адреса регистра или смещением относительно базового адреса
  3. Значения по умолчанию после сброса
  4. Типа доступа к ячейкам регистра (чтение, запись, чтение/запись)
  5. Значения и описания параметров записываемых битов
Давайте рассмотрим пример работы с регистрами в конкретной ситуации, чтобы получить общее представление о принципах настройки микроконтроллера.

Разбор кода из первого занятия

Итак, давайте вспомним задачу, которую мы решили на используя готовый код примера: нам было необходимо написать программу, которая бы обеспечила попеременное включение двух светодиодов на плате Discovery (возможно и не двух, если у вас другая версия платы Discovery) с временным интервалом.

Давайте еще разок взглянем на код программы, которую мы использовали для того, чтобы заставить наш МК дрыгать двумя ногами на которых расположены наши светодиоды:

Код main.c

/* Заголовочный файл для нашего семейства микроконтроллеров*/ #include "stm32f0xx.h" /* Тело основной программы */ int main(void) { /* Включаем тактирование на порту GPIO */ RCC->AHBENR |= RCC_AHBENR_GPIOCEN; /* Настраиваем режим работы портов PC8 и PC9 в Output*/ GPIOC ->MODER = 0x50000; /* Настраиваем Output type в режим Push-Pull */ GPIOC->OTYPER = 0; /* Настраиваем скорость работы порта в Low */ GPIOC->OSPEEDR = 0; while(1) { /* Зажигаем светодиод PC8, гасим PC9 */ GPIOC->ODR = 0x100; for (int i=0; i<500000; i++){} // Искусственная задержка /* Зажигаем светодиод PC9, гасим PC8 */ GPIOC->ODR = 0x200; for (int i=0; i<500000; i++){} // Искусственная задержка } }


Первым делом, при работе с STM32, даже для такой простой задачи как включение и выключение светодиода нам необходимо предварительно ответить на ряд вопросов:
  1. Как настроить, нужные нам, пины порта GPIO для того чтобы можно было включить светодиод?
  2. Как включить и выключить светодиод?
Ответим на них по порядку.

Куда подключены наши светодиоды? К какому выводу микроконтроллера?

Для того, чтобы посмотреть где что находится на плате Discovery, а в частности, нужные нам светодиоды - нужно открыть Schematic-файл, либо тот который мы скачали с сайта ST , либо прямо из Keil:


Открыв Schematic мы увидим схему всего того, что есть на плате - схему ST-Link, обвязку всей периферии и многое другое. На текущий момент нас интересуют два светодиода, ищем их обозначение:


Как мы видим, наши светодиоды подключены к порту GPIOC на 8 и 9 пин.

Как включить тактирование на нужный порт GPIO?

В целом, любая работа с периферией в микроконтроллерах STM32 сводится к стандартной последовательности действий:
  1. Включение тактирования соответствующего периферийного модуля. Осуществляется это через регистр RCC путем подачи тактового сигнала напрямую с шины на которой находится данный модуль. По умолчанию тактирование всей периферии отключено для минимизации энергопотребления.
  2. Настройка через управляющие регистры, путем изменения параметров специфичных для конкретного периферийного устройства
  3. Непосредственный запуск и использование результатов работы модуля
То есть, для начала работы нам нужно запустить тактирование на порт GPIOC. Это делается напрямую через обращение к регистру RCC отвечающему за тактирование всего и вся и включению тактового сигнала с шины, к которой подключен наш порт GPIO.

Внимание! Вопрос касательно системы тактирования, её настройки и использования мы подробно рассмотрим в отдельной статье.

Найти к какой шине подключен наш порт GPIOC можно найти в Datasheet"е на наш МК в разделе Memory Mapping в Таблице 16. STM32F051xx peripheral register boundary addresses.


Как вы уже успели заметить, необходимая нам шина именуется как AHB2. Для того чтобы подробнее ознакомиться с регистром, в котором включается тактирование на нужный нам порт GPIO на шине AHB, надо перейти в соответствующий раздел в Reference Manual. По названию регистров мы можем определить тот, который нужен нам:


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


Смотрим на таблицу и видим нечто напоминающее опции включения тактирования на портах GPIO. Переходим к описанию и находим нужную нам опцию:


Соответственно если мы установим 19 бит в значение «1» то это обеспечит включение тактирования на порт I/O C – то есть на наш GPIOC. К тому же - нам нужно включить отдельно один бит из группы, не затрагивая остальные т.к. мы не должны мешать и изменять без надобности другие настройки.

Основываясь на материалах прошлого урока, мы знаем что для того чтобы выставить определенный бит нужно используя логическую операцию «ИЛИ» сложить текущее значение регистра с маской которая содержит те биты которые необходимо включить. Например, сложим значение регистра RCC->AHBENR по умолчанию, т.е. 0x14 и число 0x80000 тем самым включим тактирование GPIOC путем установки 19 бита:

Каким образом мы можем это сделать из программы? Всё достаточно просто. В данном случае у нас два варианта:

  1. Запись в регистр напрямую численного значения регистра напрямую через его адрес.
  2. Настройка с использованием библиотеки CMSIS
В записи значения в регистр напрямую нет особых проблем, но есть пара существенных недостатков. Во-первых, такой код становится не читабельным и во-вторых мы не можем сходу определить на какой регистр ссылается тот или иной адрес в памяти.

То есть, мы могли бы обращаться к адресам регистров напрямую по адресу и написать так:

IO uint32_t * register_address = (uint32_t *) 0x40021014U; // Адрес нашего регистра в памяти *(__IO uint32_t *)register_address |= 0x80000; // Включаем 19 бит с нашим параметром
Второй вариант мне кажется наиболее привлекательным, т.к. библиотека CMSIS организована таким способом, что регистру можно обращаться, используя только его название. Препроцессор в ходе обработки текста программы перед компиляцией подставит все цифровые значения адреса регистра автоматически. Давайте разберем этот вопрос чуть подробнее.

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

Наш код будет выглядеть следующим образом:

/* Заголовочный файл для нашего семейства микроконтроллеров*/ #include "stm32f0xx.h" /* Тело основной программы */ int main(void) { /* Включаем тактирование на порту GPIO */ RCC->AHBENR|=RCC_AHBENR_GPIOCEN; }
Давайте для ознакомления копнём вглубь библиотеки CMSIS.

Для того, чтобы быстро перейти к месту где объявлена та или иная константа или переменная в Keil реализована удобная функция. Кликаем правой кнопкой по необходимой нам константе, например, на RCC:


И мы переносимся в глубины библиотеки CMSIS, в которой увидим, что все регистры доступные для управления программным способом имеют вид TypeDef-структур, в том числе и наш RCC:


Провалившись подобным образом в RCC_TypeDef мы увидим структуру в которой описаны все поля нашего регистра:


Соответственно, мы можем спокойно обращаться к нужному нам регистру записью вида PERIPH_MODULE->REGISTER и присваивать ему определенное значение.

Помимо мнемонического обозначения регистров есть так же обозначения конкретных битов. Если мы провалимся к объявлению параметра RCC_AHBENR_GPIOCEN из нашей программы, то так же увидим объявление всех параметров:


Таким образом, используя библиотеку CMSIS у нас получается лаконичная читаемая запись нужного нам параметра в регистр, через установку которого мы запускаем тактирование на нужный нам порт:

/* Включаем тактирование на порту GPIO */ RCC->AHBENR|=RCC_AHBENR_GPIOCEN;
В качестве задания: определите используя возможности Keil, каким образом получился адрес регистра RCC->AHBENR как 0x40021014.

Как настроить нужные нам пины GPIO для того чтобы можно было включить светодиод?

Итак, мы знаем что нужные нам светодиоды подключены к порту GPIOC к пинам PC8 и PC9. Нам нужно настроить их в такой режим, чтобы загорался светодиод. Хотелось бы сразу же сделать оговорку, что порты GPIO мы рассмотрим подробнее в другой статье и тут мы сконцентрируемся именно на работе с регистрами.

Первым делом нам нужно перевести режим работы пинов PC8 и PC9 в режим Output. Остальные параметры порта можно оставить по умолчанию. Переходим в Reference Manual в раздел 9. General-purpose I/Os (GPIO) и открываем пункт отвечающий за режим работы пинов порта GPIO и видим что за этот параметр отвечает регистр MODER:


Судя по описанию, для установки пинов PC8 и PC9 в режим Output мы должны записать 01 в соответствующие поля регистра GPIOC.

Это можно сделать через прямую установку с помощью числовых значений:


Или через использование определений из библиотеки:

/* Включаем тактирование на порту GPIO */ GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER9_0;
После данной инструкции наши пины PC8 и PC9 перейдут в режим Output.

Как включить светодиод?

Если мы обратим внимание на список доступных регистров для управления портом GPIO то можем увидеть регистр ODR:


Каждый из соответствующих битов отвечает за один из пинов порта. Его структуру вы можете увидеть ниже:


Для того, чтобы обеспечить попеременную смену состояний светодиодов надо с определенным временным интервалом включать/выключать 8 и 9 биты. То есть попеременно присваивать регистру значение 0x100 и 0x200.

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

GPIOC->ODR = 0x100; // Зажигаем PC8, гасим PC9 GPIOC->ODR = 0x200; // Зажигаем PC9, гасим PC8
Можем через использование определений из библиотеки:

GPIOC->ODR = GPIO_ODR_8; // Зажигаем PC8, гасим PC9 GPIOC->ODR = GPIO_ODR_9; // Зажигаем PC9, гасим PC8
Но так как микроконтроллер работает очень быстро - мы не будем замечать смены состояний светодиодов и визуально будет казаться что они оба горят постоянно. Для того чтобы они действительно моргали попеременно мы внесем искусственную задержку в виде цикла который займет МК бесполезными вычислениями на некоторое время. Получится следующий код:

/* Зажигаем светодиод PC8, гасим PC9 */ GPIOC->ODR = GPIO_ODR_8; for (int i=0; i<500000; i++){} // Искусственная задержка /* Зажигаем светодиод PC9, гасим PC8 */ GPIOC->ODR = GPIO_ODR_9; for (int i=0; i<500000; i++){} // Искусственная задержка
На этом первоначальное знакомство с регистрами и методами работы с ними мы можем закончить.

Проверка результатов работы нашего кода

Небольшое приятное дополнение в конце статьи: в Keil имеется отличный Debug-инструмент с помощью которого мы можем пошагово выполнить нашу программу и просмотреть текущее состояние любого периферийного блока. Для этого после загрузки прошивки после компиляции мы можем нажать кнопку Start Debug Session: