Добрый день, уважаемые хабровчане!
После длительного перерыва, связанного с защитой дипломного проекта в Бауманке, я снова вернулся к написанию статей. Так как с недавнего времени я занялся 32-битными микроконтроллерами серии STM32F на ядре ARM Cortex-M3, об этом и пойдет мой рассказ. Мне статья поможет систематизировать знания об этих замечательных микроконтроллерах, а вам, я надеюсь, послужит одной из ступеней на пути к их использованию и развеет страхи и сомнения, которые всегда возникают после уютных 8-битных AVRок при упоминании страшных 32-битных монстров.
Итак, почему Cortex, чем же плохи АVR?
Вообще говоря, ничем. Микроконтроллеры из семейства AVR очень удобные, мало потребляющие и легкие в освоении. Но в этом и таится некоторая особенность – начав свой путь с AVR, человеку трудно заставить себя перейти на более сложную архитектуру. Казалось бы – все и так работает, а не работает – так возьму AVRку побольше. Еще больше. Все равно не лезет? Возьму две AVRки, буду строить этажерки из Arduino, пока все что я задумал не влезет в микросхемы. Это неправильный подход. Чаще всего это проявляется у тех, кто занимается электроникой «для себя». Не могу сказать что это прямо таки отвратительно и ардуины надо разбивать молотками, но если вы хотите выйти за рамки поделок на коленке или вывести ваше хобби на новый уровень, следует объективно оценивать области применения микроконтроллеров. Кстати, производители микроконтроллеров на ARM Cortex-M3 призывают именно к активным действиям, за видео уничтожения вами вашей старой отладочной платы на 8-битном контроллере они готовы выслать новенькую отладочную на ARM, однако, мы постараемся обойтись без вандализма.
Итак, переходя к конкретным примерам. Младшие модели семейства STM32F10x можно приобрести по цене от 30р за штуку. Как видите, цена сопоставима с AVRками. За эту цену вы получите 32-разрядный микроконтроллер, рассчитанный на частоту 24 МГц с объемом Flash и RAM примерно аналогичным ATMega88. Старшие же модели могут быть тактированы с частотой до 72 МГц, объемы флеша/RAM доходят до 1М/128К. Когда же следует использовать его, а не мегу? Тогда, когда упираетесь в «потолок» AVR. Таким «потолком» обычно являются вычислительные задачи (обработка сигнала, в частности). Да, я знаю, при желании можно попытаться и в мегу упихать DSP-алгоритмы, в целях обучения это даже полезно. Но в реальном устройстве куда целесообразнее применить подходящий процессор. Что мы получим, выбрав STM?
Теперь перейдем к недостаткам. Я могу отметить два:
Какой можно сделать вывод? Очень простой: старайтесь всегда думать головой, и объективно оценивать ситуацию, не давая затмить рассудок привычкам и привязанностям к конкретным архитектурам. Если вы видите, что неплохо бы использовать 16-битные (и выше) данные, считать фильтры, пересылать большие объемы информации – не хватайтесь за этажерки из ардуин. В то же время, если вам нужно просто раз в минуту проверять значение датчика освещенности и врубать настольную лампу – не стоит сразу бежать за кортексом.
Теперь перейдем к более насущным вопросам. Что же нам понадобится для того чтобы начать работать с STM32F?
Вопреки мнениям некоторых товарищей, вам будет достаточно 300р. На покупку вот такой вот отладочной платы: STM32VLDISCOVERY
Она сразу содержит и программатор, и программируемую часть в виде контроллера STM32F100RB на борту.
Также, очень не помешает вот такая вот макетка: WBU-206
Позволяет не отвлекаться на пайку, а сразу сосредоточиться на поставленных целях. Правда, сложные схемы на ней выглядят отвратительно – тугие переплетения проводов могут вселить ужас в неокрепшие умы)
Вот, в принципе и все из железа что понадобится для начала. Так как основы работы изложил в своих статьях уважаемый DiHalt, я не стану повторяться и буду считать что создать проект в Keil, скомпилять его и прошить сможет любой читатель статьи. Если нет – просвещаемся на easyelectronics.ru.
Для того чтобы было не так скучно работать, начнем не с надоевших всем светодиодов, а с сервоприводов. Сервопривод представляет собой электродвигатель, который включен в следящую систему. Обратная связь реализуется получением информации о фактическом положении вала – допустим, размещением потенциометра на одном из валов редуктора.
Управляющая система получает от нас информацию о том, на какой угол мы хотим повернуть вал сервы, от потенциометра – на какой он сейчас повернут, и формирует управляющее напряжение на обмотках двигателя. Все это китайские товарищи упихивают в очень малогабаритные корпуса, например, такие: Tower Pro MG90S
Стоят они меньше 200р, доставка бесплатная (правда, довольно долгая), редуктор – металлический, а не пластиковый. Традиционно, сервы управляются ШИМ-сигналом с частотой 20-60 Гц (обратите внимание, герц, а не килогерц), скважностью сигнала задается угол поворота. Конкретные значения зависят от самого серво-мотора, так что будем эксперементировать, благо можно отлаживать прошивку на ходу. К сожалению, инфы конкретно на эти моторы я не нашел, поэтому навскидку выбрал значения от японских сервоприводов Futaba — к счастью, китайские разработчики не сильно отошли от стандартов, и серва его вполне поняла. Целью статьи будет завести таймер на STM32F1xx, сформировать им ШИМ-сигнал и с помощью это ШИМа покрутить китайскую серву.
Обратимся к железу. В данном случае никаких особых схем от нас не потребуется – все уже есть на отладочной плате. Нам остается только подключить к ней серву. Стандартный сервомотор имеет 3 провода – земля, питание и управление (обычно раскрашенных в черный, красный и оранжевый цвета соответственно). Для упомянутых мною MG90S приводов питание может варьироваться от 4.8 до 6В, а потребление на холостом ходу, согласно моим измерениям, не превышает 40 мА, так что запитать их можно прямо от пина 5В отладочной платы. ШИМ будем подавать с одного из таймеров на борту контроллера. Каждый таймер имеет по 4 независимых ШИМ-канала, так что можно не напрягаясь крутить до 4х сервоприводов одним таймером. Таймеры с номерами 1 и 8 несколько более продвинутые, чем остальные – они предназначены для управления драйверами полевых транзисторов, включенных в полумостовую схему, поэтому пока их касаться не будем (несмотря на то, что они также могут выдавать простые шим-сигналы).
Вместо этого воспользуемся таймером под номером 2, который попадает в категорию General Purpose Timers. Для управления сервами мне было удобно воспользоваться его каналами 3 и 4. Согласно документации, 3 и 4 каналы выведены на пины PA2 и PA3.
Итак, в итоге наш плацдарм для экспериментов представляет собой отладочную плату STM32VLDISCOVERY, подключенную к USB компьютера и два сервопривода Tower Pro MG90S, земли которых (черный провод) соединены с пином GND отладочной платы, питание подключено к ее пину 5В а управляющие провода первого и второго сервы подключены к пинам PA2 и PA3 соответственно. Если у вас есть осциллограф, можно подключить его каналы к PA2, PA3 чтобы сразу наблюдать формируемый сигнал.
Теперь перейдем к коду.
В коде я буду делать упор на работу с регистрами вместо использования Standard Peripherals Library – т.к. ИМХО следует изучить, как это все работает изнутри, прежде чем пытаться абстрагироваться от железа. Будем считать, что создать проект для отладочной платы в кейле и подключить CMSIS для вас не составит труда, это подробно описано в статье на easyelectronics ARM. Учебный Курс. Keil + CMSIS. Создание проекта
Итак, стартовой позицией для нас будет проект в Keil uVision с подключенной CMSIS:
Это все должно у вас компиляться, прошиваться в память контроллера и дебаггиться. Если этого не происходит – читаем упомянутую статью на easyelectronics.
Стартовый код из ассемблерного файла (у меня он зовется startup_stm32f10x_md_vl.s) содержит объявление хенделеров перываний и эксепшенов, которые могут быть безболезненно переопределены в нашей программе. Также он вызывает функцию SystemInit, определенную в файле core_cm3.h библиотеки CMSIS. В ней настраиваются некоторые регистры, в частности, отвечающие за тактирование – это все можно сделать и вручную, заменив вызов SystemInit в стартовом файле на вызов своей функции, но сейчас не будем останавливаться на этом вопросе. Отмечу только, что частота выставляется в соответствии с настройками в файле system_stm32f10x.c, где на строке 76 дефайнится SYSCLK_FREQ_24MHz, равное 24000000.
Управляющий код очень простой, однако, есть несколько моментов, на которые следует обратить внимание, чтобы не пришлось долго искать проблему в коде.
Первый момент упомянут DiHalt’ом – в STMках по умолчанию отключено тактирование практически всех периферийных модулей в целях сбережения энергии. Так что первым делом подаем такты на нашу периферию:
В этих двух строках мы устанавливаем биты TIM2EN (для подачи клока на таймер 2) и IOPAEN (для подачи клока на порт А) в регистрах APB1ENR и APB2ENR соответственно (APB1 и APB2 Peripheral Clock Enable Register).
А вот дальше идет еще одна важная вещь:
Мы настраиваем оба пина, PA2, PA3, на выход, но дело в том, что в STMках существует разделение в режимах работы GPIO. Если мы хотим программно устанавливать состояние пина в 1 или 0, то нам следует выбрать режим работы Output push-pull либо Output open drain. Если же предполагается, что его состоянием будет управлять аппаратно какая-то периферия типа таймера, которая прописана в даташите на конкретную модель контроллера в разделе распиновки в графе Alternate Functions, то режим работы следует выбирать Alternate function push-pull или Alternate function open drain. Если выставить режим неправильно, то таймер не сможет выдать ШИМ-сигнал.
Коротко о регистрах – каждый порт содержит два управляющих регистра CRL и CRH, по сути, абсолютно одинаковых. В CRL хранятся настройки пинов 0-7, в CRH – 8-15. На каждый пин приходится по 4 бита, два из которых, MODE, отвечают за направление ввода-вывода и ограничение на максимальную частоту переключения пина в режиме вывода, другие два, CNF, хранят тот самый режим работы.
Всего это дает нам восемь возможных состояний пина:
Для наших целей нам нужны пины, сконфигурированные на выход, управляемый периферией в режиме push-pull. Таким образом, необходимо в регистре CRL установить значения битов MODE2[1:0] и MODE3[1:0] в какое-нибудь отличное от 00 значение, допустим, в 11, а битов CNF2[1:0] и CNF3[1:0] – в значение 10
Остальные регистры, относящиеся к периферии, подробно описаны в документе RM0008, STM32F1xx Reference Manual. Если коротко – регистры IDR и ODR содержат input и output значения на пинах порта, BSRR отвечает за установку/сброс, а BRR только за сброс битов в ODR, причем делает он это атомарно, то бишь эта операция происходит за один цикл шины и в нее не может вклиниться прерывание. Последний регистр порта, LCKR позволяет «запереть» значение пина, не давая изменить его до самого резета.
Далее настроим таймер. Согласно найденным на просторах интернета данным, период ШИМ следует выбирать от 20 до 60 Гц, при этом импульсы шириной около 1 мс означают 0 градусов, 1.5 мс – 90 градусов, а 2 мс – 180.
Что касается разрядности ШИМа – в принципе, можно выбрать любую, но точность позиционирования сервы все равно конечна, поэтому я выбрал 12 разрядов. Также следует помнить, что 12 разрядов описывают изменение ширины импульса от 0 мс до величины, равной его периоду, в то время как серва работает на диапазоне от 1 до 2 мс, поэтому в нашем распоряжении будут далеко не все 12 разрядов. Забегая вперед, скажу что в результате экспериментов я пришел к следующим параметрам:
Выбрав частоту ШИМ 35 Гц, а разрядность – 12 бит, получаем 4096 значений на весь период, 0х50 будет соответствовать ширине в 0.56 мс, а 0х150 – ширине в 2.34 мс.
Таким образом, получаем 256 значений на примерно 180 градусов, что означает дискретность примерно в 40 угловых минут на один отсчет. При желании, можно продолжить эксперименты и определить точные граничные значения и точность, с которой привод может осуществлять позиционирование.
Итак, следующим фрагментом кода настраиваем таймер 2:
Тут все весьма тривиально: регистр PSC (Prescaler) отвечает за предделитель, регистр ARR (Auto-reload register) – за максимальное значение, до которого будет тикать таймер. Выставив в ARR число 0хFFF получаем 12-разрядный таймер, период его счета будет равен тактовой частоте, деленной на 0xFFF, то есть 24000000/4096 = 5859. Разделив это значение еще на 0xA7, получим 5859/167 = 35 Гц.
Значение регистров CCR3 и CCR4 (Capture Compare register) сравнивается со значением счетчика и задает собственно скважность ШИМа. Дальше настраиваем режим работы таймера:
Ситуация та же, что и с регистрами настройки порта – два регистра настройки Capture and Compare Mode register, CCMR1, CCMR2 отвечают за режимы работы каналов 1,2 и 3,4 соответственно. Настроек там много, и лучше на этот счет читать Reference Manual. Если обзорно – регистры отвечают за режимы связанные с захватом шим-сигнала по таймеру и с его выводом. Биты CCxS, где x – номер канала, настраивают данный канал на ввод или на вывод. По умолчанию включен режим вывода, поэтому их трогать не будем. Устанавливаемые биты OCxM[2:0] отвечают за то, как канал будет реагировать на сравнение значения счетчика (которое хранится, кстати, в регистре
TIM2->CNT) и регистров CCRx. Всего возможны 8 вариантов:
Устанавливаем режим 110, обычный ШИМ. Не забываем в регистре CCER (Capture/compare enable register) установить бит CCxE – включить выход соответствующего канала.
После этого включаем весь таймер установкой бита CEN в регистре CR1 (Control Register 1).
Все, ШИМ пошел, на экране осциллографа должна появиться красивая картинка, а сервы занять противоположные положения. При желании можно поставить бряк после всех инициализаций и менять значения при помощи встроенного в Кейл средства для просмотра и редактирования регистров процессора и периферии, сразу наблюдая изменение скважности сигнала на экране осциллографа и положения вала сервопривода.
Давайте теперь в целях самообразования сделаем программу чуть более интересной – будем изменять значения угла от максимума до минимума. Для этого вспомним, что в состав ядра ARM Cortex-M3 входит так называемый SysTick Timer – это 24-битный таймер, предназначенный для глобальной синхронизации и генерации прерываний, допустим, при реализации операционной системы. Инициализируется таймер при помощи функции SysTick_Config (uint32_t Ticks). Эта функция является частью CMSIS, которая более «фундаментальна» чем Standard Peripherals Library, поэтому воспользуемся ею.
Если интересны внутренности этой функции, можно перейти к ее описанию в недрах CMSIS (файл core_cm3.h, строка 1137). В целом, функция не делает ничего особо сложного, только проверяет значение передаваемого в нее параметра Ticks на предмет превышения 24 бит, после чего устанавливает регистры:
• SysTick->LOAD в значение Ticks – это вершина счетчика таймера, с такой периодичностью будут вызываться прерывания.
• SysTick->VAL в 0 – это текущее значение счетчика
• Битов CLKSOURCE, TICKINT, ENABLE регистра SysTick->CTRL в 1.
Бит CLKSOURCE отвечает за источник тактового сигнала для таймера. Вообще говоря, спецификация на ядро не описывает каким должен быть второй сигнал (первый всегда подключен к системному клоку), поэтому от производителя к производителю эффект от этого бита может меняться. В STM32F второй клоковый сигнал подключен к SystemCoreClock/8. Установив бит в 1 выбираем источником системный клок.
Бит TICKINT включает генерацию прерывания (а точнее, эксепшена) по переполнению таймера. Устанавливаем в 1, ради него все и затевалось)
Бит ENABLE включает таймер.
Кроме описанных действий функция SysTick_Config еще и выставляет приоритет прерыванию в значение 240, весьма низкий, учитывая что приоритеты начинаюся с 0 (вообще-то с -3, но приоритеты меньше 0 неконфигурируемы).
Так как текущие настройки ШИМа означают 256 шагов от 0 до 180 градусов, настроим таймер на частоту
SystemCoreClock/256, чтобы этот путь серва проходила за 1 секунду:
Константа SystemCoreClock определена в system_stm32f10x.c, строка 114, и равна числу тактовых сигналов ядра за 1 с, то есть 24000000.
Осталось описать обработчик прерывания по таймеру и работа будет завершена!
Так как каналов у нас два, напишем обработчик с расчетом на оба канала сразу. Так как пространство памяти у нас едино, мы можем свободно обращаться к регистрам как к обычным ячейкам памяти, поэтому сразу объявляем указатель на два регистра:
Дальше все очень просто – организуем цикл по двум каналам, добавляем или вычитаем 1 из текущего значения скважности (в зависимости от направления) и по достижении сервой крайнего положения меняем направления.
Все, компиляем, и если все сделано правильно, то наслаждаемся зрелищем типа того, что представлено мной на видео ниже.
Полный код проекта выглядит так:
На этом у меня пока все. Если вас эта тема заинтересовала, то в следующих статьях постараюсь рассмотреть что-нибудь поинтереснее, допустим, работу с дисплеем от мобильного телефона.
UPD:
Чтобы далеко не ходить, полезные ссылки:
После длительного перерыва, связанного с защитой дипломного проекта в Бауманке, я снова вернулся к написанию статей. Так как с недавнего времени я занялся 32-битными микроконтроллерами серии STM32F на ядре ARM Cortex-M3, об этом и пойдет мой рассказ. Мне статья поможет систематизировать знания об этих замечательных микроконтроллерах, а вам, я надеюсь, послужит одной из ступеней на пути к их использованию и развеет страхи и сомнения, которые всегда возникают после уютных 8-битных AVRок при упоминании страшных 32-битных монстров.
Итак, почему Cortex, чем же плохи АVR?
Потолок
Вообще говоря, ничем. Микроконтроллеры из семейства AVR очень удобные, мало потребляющие и легкие в освоении. Но в этом и таится некоторая особенность – начав свой путь с AVR, человеку трудно заставить себя перейти на более сложную архитектуру. Казалось бы – все и так работает, а не работает – так возьму AVRку побольше. Еще больше. Все равно не лезет? Возьму две AVRки, буду строить этажерки из Arduino, пока все что я задумал не влезет в микросхемы. Это неправильный подход. Чаще всего это проявляется у тех, кто занимается электроникой «для себя». Не могу сказать что это прямо таки отвратительно и ардуины надо разбивать молотками, но если вы хотите выйти за рамки поделок на коленке или вывести ваше хобби на новый уровень, следует объективно оценивать области применения микроконтроллеров. Кстати, производители микроконтроллеров на ARM Cortex-M3 призывают именно к активным действиям, за видео уничтожения вами вашей старой отладочной платы на 8-битном контроллере они готовы выслать новенькую отладочную на ARM, однако, мы постараемся обойтись без вандализма.
Итак, переходя к конкретным примерам. Младшие модели семейства STM32F10x можно приобрести по цене от 30р за штуку. Как видите, цена сопоставима с AVRками. За эту цену вы получите 32-разрядный микроконтроллер, рассчитанный на частоту 24 МГц с объемом Flash и RAM примерно аналогичным ATMega88. Старшие же модели могут быть тактированы с частотой до 72 МГц, объемы флеша/RAM доходят до 1М/128К. Когда же следует использовать его, а не мегу? Тогда, когда упираетесь в «потолок» AVR. Таким «потолком» обычно являются вычислительные задачи (обработка сигнала, в частности). Да, я знаю, при желании можно попытаться и в мегу упихать DSP-алгоритмы, в целях обучения это даже полезно. Но в реальном устройстве куда целесообразнее применить подходящий процессор. Что мы получим, выбрав STM?
- Полноценные 32-битные вычисления. Не придется тратить драгоценные такты на работу данными, разрядностью больше 8 бит, АЛУ сделает это за вас.
- DMA. Контроллер DMA для пользователей AVR это роскошь, доступная только в старших, монструозных моделях. В STM32F1xx DMA есть даже в самых младших кристаллах. Используя его можно легко и непринужденно пересылать блоки данных между периферией и памятью без использования процессора. Очень полезно при работе с картами памяти, пересылке больших объемов данных по всяким UARTам и иже с ними, организации захвата звука с АЦП и вывода данных на ЦАП.
- Кстати о ЦАП – в большинстве моделей, в том числе и младших, в наличии 12-битный ЦАП. Для меломанов не подойдет, а всякую отладочную аналоговую инфу выводить удобно. Да и всякие игрушки со звуком делать удобнее, чем с ШИМ.
- NVIC, то бишь Nested Vector Interrupt Controller, программируемый контроллер прерываний, позволяющий назначить им приоритеты и гарантирующий постоянное время входа в прерывание – необходимая вещь для систем с ограничениями реального времени.
- Приятная мелочь типа контроллера SD-карточки – т.к. SPI-контроллер на STMках содержит аппаратный вычислитель CRC, его можно использовать для полноценного общения с SD-карточками.
- Наличие некоторых DSP команд. По правде говоря, Cortex-M3 не совсем DSP-ядро, но часто требуется именно такой процессор – не жрущие монстры типа DSP от TI, но и не слабые AVRки. В обработке сигнала на M3 поможет наличие аппаратного умножителя 32х32 и умножения с накоплением.
- У более продвинутых моделей есть USB-контроллер, а объемы флеша доходят до мегабайта, при том что корпуса вы по-прежнему можете выбирать сами – STM совместимы по пинам и даже старшие модели можно купить в 64-ногом корпусе. Плату переразводить не придется.
Теперь перейдем к недостаткам. Я могу отметить два:
- Корпуса у STM32F1xx намного менее паябельные в домашних условиях чем у тех же AVR. Плату изготовить удастся, но это потребует некоторой сноровки.
- Архитектура этих контроллеров весьма сложная, на один таймер приходится регистров 12 (они еще и 32-битные!), поэтому за вечерок осилить не удастся, придется целенаправленно ими заниматься.
Какой можно сделать вывод? Очень простой: старайтесь всегда думать головой, и объективно оценивать ситуацию, не давая затмить рассудок привычкам и привязанностям к конкретным архитектурам. Если вы видите, что неплохо бы использовать 16-битные (и выше) данные, считать фильтры, пересылать большие объемы информации – не хватайтесь за этажерки из ардуин. В то же время, если вам нужно просто раз в минуту проверять значение датчика освещенности и врубать настольную лампу – не стоит сразу бежать за кортексом.
Теперь перейдем к более насущным вопросам. Что же нам понадобится для того чтобы начать работать с STM32F?
Начинаем работать с STM32F1xx
Вопреки мнениям некоторых товарищей, вам будет достаточно 300р. На покупку вот такой вот отладочной платы: STM32VLDISCOVERY
Она сразу содержит и программатор, и программируемую часть в виде контроллера STM32F100RB на борту.
Также, очень не помешает вот такая вот макетка: WBU-206
Позволяет не отвлекаться на пайку, а сразу сосредоточиться на поставленных целях. Правда, сложные схемы на ней выглядят отвратительно – тугие переплетения проводов могут вселить ужас в неокрепшие умы)
Вот, в принципе и все из железа что понадобится для начала. Так как основы работы изложил в своих статьях уважаемый DiHalt, я не стану повторяться и буду считать что создать проект в Keil, скомпилять его и прошить сможет любой читатель статьи. Если нет – просвещаемся на easyelectronics.ru.
Для того чтобы было не так скучно работать, начнем не с надоевших всем светодиодов, а с сервоприводов. Сервопривод представляет собой электродвигатель, который включен в следящую систему. Обратная связь реализуется получением информации о фактическом положении вала – допустим, размещением потенциометра на одном из валов редуктора.
Управляющая система получает от нас информацию о том, на какой угол мы хотим повернуть вал сервы, от потенциометра – на какой он сейчас повернут, и формирует управляющее напряжение на обмотках двигателя. Все это китайские товарищи упихивают в очень малогабаритные корпуса, например, такие: Tower Pro MG90S
Стоят они меньше 200р, доставка бесплатная (правда, довольно долгая), редуктор – металлический, а не пластиковый. Традиционно, сервы управляются ШИМ-сигналом с частотой 20-60 Гц (обратите внимание, герц, а не килогерц), скважностью сигнала задается угол поворота. Конкретные значения зависят от самого серво-мотора, так что будем эксперементировать, благо можно отлаживать прошивку на ходу. К сожалению, инфы конкретно на эти моторы я не нашел, поэтому навскидку выбрал значения от японских сервоприводов Futaba — к счастью, китайские разработчики не сильно отошли от стандартов, и серва его вполне поняла. Целью статьи будет завести таймер на STM32F1xx, сформировать им ШИМ-сигнал и с помощью это ШИМа покрутить китайскую серву.
Обратимся к железу. В данном случае никаких особых схем от нас не потребуется – все уже есть на отладочной плате. Нам остается только подключить к ней серву. Стандартный сервомотор имеет 3 провода – земля, питание и управление (обычно раскрашенных в черный, красный и оранжевый цвета соответственно). Для упомянутых мною MG90S приводов питание может варьироваться от 4.8 до 6В, а потребление на холостом ходу, согласно моим измерениям, не превышает 40 мА, так что запитать их можно прямо от пина 5В отладочной платы. ШИМ будем подавать с одного из таймеров на борту контроллера. Каждый таймер имеет по 4 независимых ШИМ-канала, так что можно не напрягаясь крутить до 4х сервоприводов одним таймером. Таймеры с номерами 1 и 8 несколько более продвинутые, чем остальные – они предназначены для управления драйверами полевых транзисторов, включенных в полумостовую схему, поэтому пока их касаться не будем (несмотря на то, что они также могут выдавать простые шим-сигналы).
Вместо этого воспользуемся таймером под номером 2, который попадает в категорию General Purpose Timers. Для управления сервами мне было удобно воспользоваться его каналами 3 и 4. Согласно документации, 3 и 4 каналы выведены на пины PA2 и PA3.
Итак, в итоге наш плацдарм для экспериментов представляет собой отладочную плату STM32VLDISCOVERY, подключенную к USB компьютера и два сервопривода Tower Pro MG90S, земли которых (черный провод) соединены с пином GND отладочной платы, питание подключено к ее пину 5В а управляющие провода первого и второго сервы подключены к пинам PA2 и PA3 соответственно. Если у вас есть осциллограф, можно подключить его каналы к PA2, PA3 чтобы сразу наблюдать формируемый сигнал.
Теперь перейдем к коду.
Программная часть
В коде я буду делать упор на работу с регистрами вместо использования Standard Peripherals Library – т.к. ИМХО следует изучить, как это все работает изнутри, прежде чем пытаться абстрагироваться от железа. Будем считать, что создать проект для отладочной платы в кейле и подключить CMSIS для вас не составит труда, это подробно описано в статье на easyelectronics ARM. Учебный Курс. Keil + CMSIS. Создание проекта
Итак, стартовой позицией для нас будет проект в Keil uVision с подключенной CMSIS:
#include "stm32f10x.h"
int main()
{
while(1);
}
Это все должно у вас компиляться, прошиваться в память контроллера и дебаггиться. Если этого не происходит – читаем упомянутую статью на easyelectronics.
Стартовый код из ассемблерного файла (у меня он зовется startup_stm32f10x_md_vl.s) содержит объявление хенделеров перываний и эксепшенов, которые могут быть безболезненно переопределены в нашей программе. Также он вызывает функцию SystemInit, определенную в файле core_cm3.h библиотеки CMSIS. В ней настраиваются некоторые регистры, в частности, отвечающие за тактирование – это все можно сделать и вручную, заменив вызов SystemInit в стартовом файле на вызов своей функции, но сейчас не будем останавливаться на этом вопросе. Отмечу только, что частота выставляется в соответствии с настройками в файле system_stm32f10x.c, где на строке 76 дефайнится SYSCLK_FREQ_24MHz, равное 24000000.
Управляющий код очень простой, однако, есть несколько моментов, на которые следует обратить внимание, чтобы не пришлось долго искать проблему в коде.
Первый момент упомянут DiHalt’ом – в STMках по умолчанию отключено тактирование практически всех периферийных модулей в целях сбережения энергии. Так что первым делом подаем такты на нашу периферию:
RCC->APB1ENR|= RCC_APB1ENR_TIM2EN;
RCC->APB2ENR|= RCC_APB2ENR_IOPAEN;
В этих двух строках мы устанавливаем биты TIM2EN (для подачи клока на таймер 2) и IOPAEN (для подачи клока на порт А) в регистрах APB1ENR и APB2ENR соответственно (APB1 и APB2 Peripheral Clock Enable Register).
А вот дальше идет еще одна важная вещь:
GPIOA->CRL |=GPIO_CRL_MODE2;
GPIOA->CRL &=~GPIO_CRL_CNF2_0;
GPIOA->CRL |=GPIO_CRL_CNF2_1;
GPIOA->CRL |=GPIO_CRL_MODE3;
GPIOA->CRL &=~GPIO_CRL_CNF3_0;
GPIOA->CRL |=GPIO_CRL_CNF3_1;
Мы настраиваем оба пина, PA2, PA3, на выход, но дело в том, что в STMках существует разделение в режимах работы GPIO. Если мы хотим программно устанавливать состояние пина в 1 или 0, то нам следует выбрать режим работы Output push-pull либо Output open drain. Если же предполагается, что его состоянием будет управлять аппаратно какая-то периферия типа таймера, которая прописана в даташите на конкретную модель контроллера в разделе распиновки в графе Alternate Functions, то режим работы следует выбирать Alternate function push-pull или Alternate function open drain. Если выставить режим неправильно, то таймер не сможет выдать ШИМ-сигнал.
Коротко о регистрах – каждый порт содержит два управляющих регистра CRL и CRH, по сути, абсолютно одинаковых. В CRL хранятся настройки пинов 0-7, в CRH – 8-15. На каждый пин приходится по 4 бита, два из которых, MODE, отвечают за направление ввода-вывода и ограничение на максимальную частоту переключения пина в режиме вывода, другие два, CNF, хранят тот самый режим работы.
Всего это дает нам восемь возможных состояний пина:
- Вход, не подтянутый (MODE[1:0]=00, CNF[1:0] = 01)
- Вход, подтянутый к питанию (MODE[1:0]=00, CNF[1:0] = 10, на выход порта подан 1 )
- Вход, потянутый к земле (MODE[1:0]=00, CNF[1:0] = 10, на выход порта подан 0)
- Аналоговый вход (MODE[1:0]=00, CNF[1:0] = 00) – при этом отключается входной триггер Шмита и резисторы подтяжки, пин переходит в Z-состояние.
- Выход, открытый сток GPIO (MODE[1:0]!=00, CNF[1:0] = 01)
- Выход, push-pull GPIO (MODE[1:0]!=00, CNF[1:0] = 00)
- Выход, открытый сток под управлением периферии (MODE[1:0]!=00, CNF[1:0] = 11)
- Выход, push-pull под управлением периферии (MODE[1:0]!=00, CNF[1:0] = 10)
Для наших целей нам нужны пины, сконфигурированные на выход, управляемый периферией в режиме push-pull. Таким образом, необходимо в регистре CRL установить значения битов MODE2[1:0] и MODE3[1:0] в какое-нибудь отличное от 00 значение, допустим, в 11, а битов CNF2[1:0] и CNF3[1:0] – в значение 10
Остальные регистры, относящиеся к периферии, подробно описаны в документе RM0008, STM32F1xx Reference Manual. Если коротко – регистры IDR и ODR содержат input и output значения на пинах порта, BSRR отвечает за установку/сброс, а BRR только за сброс битов в ODR, причем делает он это атомарно, то бишь эта операция происходит за один цикл шины и в нее не может вклиниться прерывание. Последний регистр порта, LCKR позволяет «запереть» значение пина, не давая изменить его до самого резета.
Далее настроим таймер. Согласно найденным на просторах интернета данным, период ШИМ следует выбирать от 20 до 60 Гц, при этом импульсы шириной около 1 мс означают 0 градусов, 1.5 мс – 90 градусов, а 2 мс – 180.
Что касается разрядности ШИМа – в принципе, можно выбрать любую, но точность позиционирования сервы все равно конечна, поэтому я выбрал 12 разрядов. Также следует помнить, что 12 разрядов описывают изменение ширины импульса от 0 мс до величины, равной его периоду, в то время как серва работает на диапазоне от 1 до 2 мс, поэтому в нашем распоряжении будут далеко не все 12 разрядов. Забегая вперед, скажу что в результате экспериментов я пришел к следующим параметрам:
- Частота ШИМ-сигнала: 35 Гц. Выбрал просто как среднее значение, серва стабильно работает и при 30 и при 50 Гц.
- Ширина импульса, соответствующя 0 градусам: около 0.5 мс, однако, если выбрать ее 0.5 или меньше, серва начинает трещать, т.к. находится на границе своих физических возможностей по повороту. Поэтому для более стабильной работы я использую импульсы шириной около 0.56 мс
- Ширина импульса, соответсвующая 180 градусам: около 2.3 мс, та же ситуация что и с нулевым поворотом – если задать чуть больше граничной, начнет трещать.
Выбрав частоту ШИМ 35 Гц, а разрядность – 12 бит, получаем 4096 значений на весь период, 0х50 будет соответствовать ширине в 0.56 мс, а 0х150 – ширине в 2.34 мс.
Таким образом, получаем 256 значений на примерно 180 градусов, что означает дискретность примерно в 40 угловых минут на один отсчет. При желании, можно продолжить эксперименты и определить точные граничные значения и точность, с которой привод может осуществлять позиционирование.
Итак, следующим фрагментом кода настраиваем таймер 2:
TIM2->PSC = 0x00A7;
TIM2->ARR = 0x0FFF;
TIM2->CCR3 = 0x050;
TIM2->CCR4 = 0x0150;
Тут все весьма тривиально: регистр PSC (Prescaler) отвечает за предделитель, регистр ARR (Auto-reload register) – за максимальное значение, до которого будет тикать таймер. Выставив в ARR число 0хFFF получаем 12-разрядный таймер, период его счета будет равен тактовой частоте, деленной на 0xFFF, то есть 24000000/4096 = 5859. Разделив это значение еще на 0xA7, получим 5859/167 = 35 Гц.
Значение регистров CCR3 и CCR4 (Capture Compare register) сравнивается со значением счетчика и задает собственно скважность ШИМа. Дальше настраиваем режим работы таймера:
TIM2->CCMR2 |= TIM_CCMR2_OC3M_1|TIM_CCMR2_OC3M_2;
TIM2->CCER |= TIM_CCER_CC3E;
TIM2->CCMR2 |= TIM_CCMR2_OC4M_1|TIM_CCMR2_OC4M_2;
TIM2->CCER |= TIM_CCER_CC4E;
TIM2->CR1 |= TIM_CR1_CEN;
Ситуация та же, что и с регистрами настройки порта – два регистра настройки Capture and Compare Mode register, CCMR1, CCMR2 отвечают за режимы работы каналов 1,2 и 3,4 соответственно. Настроек там много, и лучше на этот счет читать Reference Manual. Если обзорно – регистры отвечают за режимы связанные с захватом шим-сигнала по таймеру и с его выводом. Биты CCxS, где x – номер канала, настраивают данный канал на ввод или на вывод. По умолчанию включен режим вывода, поэтому их трогать не будем. Устанавливаемые биты OCxM[2:0] отвечают за то, как канал будет реагировать на сравнение значения счетчика (которое хранится, кстати, в регистре
TIM2->CNT) и регистров CCRx. Всего возможны 8 вариантов:
- 000 – сравнение никак не влияет на выход
- 001 – при совпадении значений на выходе канала устанавливается 1
- 010 – при совпадении значений на выходе канала устанавливается 0
- 011 – при совпадении значений выходной сигнал переключается в противоположное состояние
- 100 – выход всегда установлен в 0
- 101 – выход всегда установлен в 1
- 110 – ШИМ, режим 1, обычный (пока значение в CCRx<CNT, на выходе 1, иначе 0)
- 111 – ШИМ, режим 2, инверсный (пока значение в CCRx<CNT, на выходе 0, иначе 1)
Устанавливаем режим 110, обычный ШИМ. Не забываем в регистре CCER (Capture/compare enable register) установить бит CCxE – включить выход соответствующего канала.
После этого включаем весь таймер установкой бита CEN в регистре CR1 (Control Register 1).
Все, ШИМ пошел, на экране осциллографа должна появиться красивая картинка, а сервы занять противоположные положения. При желании можно поставить бряк после всех инициализаций и менять значения при помощи встроенного в Кейл средства для просмотра и редактирования регистров процессора и периферии, сразу наблюдая изменение скважности сигнала на экране осциллографа и положения вала сервопривода.
Давайте теперь в целях самообразования сделаем программу чуть более интересной – будем изменять значения угла от максимума до минимума. Для этого вспомним, что в состав ядра ARM Cortex-M3 входит так называемый SysTick Timer – это 24-битный таймер, предназначенный для глобальной синхронизации и генерации прерываний, допустим, при реализации операционной системы. Инициализируется таймер при помощи функции SysTick_Config (uint32_t Ticks). Эта функция является частью CMSIS, которая более «фундаментальна» чем Standard Peripherals Library, поэтому воспользуемся ею.
Если интересны внутренности этой функции, можно перейти к ее описанию в недрах CMSIS (файл core_cm3.h, строка 1137). В целом, функция не делает ничего особо сложного, только проверяет значение передаваемого в нее параметра Ticks на предмет превышения 24 бит, после чего устанавливает регистры:
• SysTick->LOAD в значение Ticks – это вершина счетчика таймера, с такой периодичностью будут вызываться прерывания.
• SysTick->VAL в 0 – это текущее значение счетчика
• Битов CLKSOURCE, TICKINT, ENABLE регистра SysTick->CTRL в 1.
Бит CLKSOURCE отвечает за источник тактового сигнала для таймера. Вообще говоря, спецификация на ядро не описывает каким должен быть второй сигнал (первый всегда подключен к системному клоку), поэтому от производителя к производителю эффект от этого бита может меняться. В STM32F второй клоковый сигнал подключен к SystemCoreClock/8. Установив бит в 1 выбираем источником системный клок.
Бит TICKINT включает генерацию прерывания (а точнее, эксепшена) по переполнению таймера. Устанавливаем в 1, ради него все и затевалось)
Бит ENABLE включает таймер.
Кроме описанных действий функция SysTick_Config еще и выставляет приоритет прерыванию в значение 240, весьма низкий, учитывая что приоритеты начинаюся с 0 (вообще-то с -3, но приоритеты меньше 0 неконфигурируемы).
Так как текущие настройки ШИМа означают 256 шагов от 0 до 180 градусов, настроим таймер на частоту
SystemCoreClock/256, чтобы этот путь серва проходила за 1 секунду:
SysTick_Config(SystemCoreClock/256);
Константа SystemCoreClock определена в system_stm32f10x.c, строка 114, и равна числу тактовых сигналов ядра за 1 с, то есть 24000000.
Осталось описать обработчик прерывания по таймеру и работа будет завершена!
int8_t ChannelDir[2]={1,-1};
volatile uint16_t *DutyCycle[2]={TIM2->CCR3,&TIM2->CCR4};
void SysTick_Handler()
{
uint8_t i;
for(i=0;i<2;i++)
{
*DutyCycle[i]+=ChannelDir[i];
if(*DutyCycle[i]<0x50)
ChannelDir[i]=1;
if(*DutyCycle[i]>0x150)
ChannelDir[i]=-1;
}
}
Так как каналов у нас два, напишем обработчик с расчетом на оба канала сразу. Так как пространство памяти у нас едино, мы можем свободно обращаться к регистрам как к обычным ячейкам памяти, поэтому сразу объявляем указатель на два регистра:
volatile uint16_t *DutyCycle[2]={&TIM2->CCR3,&TIM2->CCR4};
Дальше все очень просто – организуем цикл по двум каналам, добавляем или вычитаем 1 из текущего значения скважности (в зависимости от направления) и по достижении сервой крайнего положения меняем направления.
Все, компиляем, и если все сделано правильно, то наслаждаемся зрелищем типа того, что представлено мной на видео ниже.
Полный код проекта выглядит так:
#include "stm32f10x.h"
int8_t ChannelDir[2]={1,-1};
volatile uint16_t *DutyCycle[2]={&TIM2->CCR3,&TIM2->CCR4};
void SysTick_Handler()
{
uint8_t i;
for(i=0;i<2;i++)
{
*DutyCycle[i]+=ChannelDir[i];
if(*DutyCycle[i]<0x50)
ChannelDir[i]=1;
if(*DutyCycle[i]>0x150)
ChannelDir[i]=-1;
}
}
int main()
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
GPIOA->CRL |=GPIO_CRL_MODE2;
GPIOA->CRL &=~GPIO_CRL_CNF2_0;
GPIOA->CRL |=GPIO_CRL_CNF2_1;
GPIOA->CRL |=GPIO_CRL_MODE3;
GPIOA->CRL &=~GPIO_CRL_CNF3_0;
GPIOA->CRL |=GPIO_CRL_CNF3_1;
TIM2->PSC = 0x00A7;
TIM2->ARR = 0x0FFF;
TIM2->CCR3 = 0x050;
TIM2->CCR4 = 0x0150;
TIM2->CCMR2 |= TIM_CCMR2_OC3M_1|TIM_CCMR2_OC3M_2;
TIM2->CCER |= TIM_CCER_CC3E;
TIM2->CCMR2 |= TIM_CCMR2_OC4M_1|TIM_CCMR2_OC4M_2;
TIM2->CCER |= TIM_CCER_CC4E;
TIM2->CR1 |= TIM_CR1_CEN;
SysTick_Config(SystemCoreClock/256);
while(1);
}
На этом у меня пока все. Если вас эта тема заинтересовала, то в следующих статьях постараюсь рассмотреть что-нибудь поинтереснее, допустим, работу с дисплеем от мобильного телефона.
UPD:
Чтобы далеко не ходить, полезные ссылки: