Микроконтроллеры семейств AVR, MSP430, STM32 и мои субъективные впечатления
Здравствуйте, обитатели Хабра. В этой статье хочу поделится своими впечатлениями об опыте программирования микроконтроллеров семейств AVR, MSP430, STM32.
Введение
В бытность мою студентом занимался я прикладным программированием на Delphi и горя не знал, но и счастья не ведал. Пока как-то раз не посетил меня на четвертом курсе предмет «Микропроцессорные контроллеры». Ну и пошло-поехало.
Семейство микроконтроллеров AVR
Предмет «Микропроцессорные контроллеры» как раз и был посвящен программированию микроконтроллеров на примере семейства AVR Atmega фирмы Atmel. Лабораторные работы по данному предмету заключались в программировании отладочных плат с Atmega16 на ассемблере данного семейства в программной среде AVR Studio 4.18.
Программа отлаживалась при помощи симулятора и зашивалась в микроконтроллер посредством встроенного в отладочную плату LPT-программатора на логике через программу ponyprog2000. На этих лабораторных работах я и ознакомился с волшебным миром микроконтроллеров, что включало в себя «помигать светодиодом», обработать нажатие на кнопку, настроить аппаратный таймер на работу и обрабатывать генерируемые им прерывания, настроить UART и передать данные по нему и т.д.
Прекрасный новый мир открылся мне. Но потом все это немного заглохло до следующего курса, на котором программирование тех же самых плат происходило, но уже не на ассемблере, а на языке Pascal в среде E-LAB. Об этой среде мало кто знает, а зря. Ведь задолго до всяких там arduino, данная среда включала в себя много библиотек для внешних устройств простых в использовании. Не верите?
Посмотрите сами тут. Тут вам для E-LAB и JTAG-отладчики есть.
Но во времена написания лабораторных работ JTAG-отладка доступна не была. Поэтому мы пользовались встроенным в E-LAB симулятором. Как и тогда, библиотеки E-LAB позволяют создавать проекты с ОСРВ, работающей по принципу Round-robin.
Последние версии E-LAB поддерживают и кооперативную многозадачность.
В принципе с этих двух циклов лабораторных работ я и начал свое знакомство с микроконтроллерами и, в частности, семейством AVR. Что можно сказать теперь?
AVR — самое популярное семейство микроконтроллеров в мире, я думаю.
Arduino-мания только укрепила это. Эти простые в освоении микроконтроллеры и сейчас остаются лучшим решением для первого знакомства. Позволяют получить опыт создания простых приложений с использование интерфейсов SPI, I2C, UART, позволяют понять работу портов ввода/вывода, подсистемы прерываний. По сути, на данном семействе можно научится основам и делать малые и средние проекты. В последних версиях AVR Studio можно делать проекты на С.
А если взять в руки паяльник, то можно себя обеспечить и программатором, и JTAG-отладчиком.
Есть желание начать? Тут тоже много всего.
Фирменные отладочные платы и программаторы от Atmel крайне дороги.
Главным недостатком AVR является слабое вычислительное ядро без вспомогательных математических блоков, причем восьмиразрядность усугубляет ситуацию. Т.е. на сложные математические вычисления может уйти много времени. Микроконтроллер может не успевать обрабатывать собранную или принятую информацию. Последний проект для Atmega16 я делал на С в среде разработки IAR Embedded Workbench for Atmel AVR.
Семейство микроконтроллеров MSP430
После семейства AVR микроконтроллерый мир уже открыл мне часть своих тайн.
А тут подоспел и новый предмет, посвященный также программированию микроконтроллеров, но это было уже семейство MSP430 фирмы Texas Instruments, а именно микроконтроллер msp430f169 на отладочной плате с ziff-панелью и минимальной обвязкой.
Разработка программы для него и отладка проходили в среде IAR Embedded Workbench for MSP430 при помощи JTAG-отладчика MSP-FET430UIF.
В первую очередь в этом семействе понравились примеры программ работы с внутренней периферией от производителя. Ну и с него берет моя подсадка на JTAG и IAR. Однажды попробовав JTAG-отладку не захочется возвращаться к разработке с просто программированием. Ведь под JTAG-отладкой можно по шагам видеть, что происходит в регистрах в памяти и где сейчас идет выполнение кода, ставить точки останова. С этого времени я и на IAR подсел. Ведь это кроссплатформенный компилятор выпущенный под множество микроконтроллерных семейств. Стоит один раз запомнить интерфейс и не надо каждый раз, при переходе на новое микроконтроллерное семейство, переучиваться. Это ли не чудо? Но затягивает.
Минус только в стоимости полной версии. Вообщем на этом семействе я и начал свою работу, как программист микроконтроллеров. И связка язык C (по сути кроссплатформенный ассемблер), кроссплатформенная среда разработки IAR и JTAG-отладка всегда были вместе со мной.
Семейство MSP430 в отличие от AVR шестнадцатиразрядное и более производительное за счет применения встроенного аппаратного умножителя.
Возможность использования режимов пониженного энергопотребления обеспечивает увеличение срока службы элементов питания при применении в мобильных портативных устройствах. А микроконтроллеры MSP430F5419 и MSP430F5438, с которыми я работал, на частоте 25 МГц в плотную подтягиваются к ARM. Так что они такие мощные середнячки. Если иметь фирменный JTAG-отладчик, IAR for MSP-430, нормальную отладочную плату, то работать с ними одно удовольствие.
Семейство микроконтроллеров STM32
Последним я познакомился с семейством STM32 фирмы STMicroelectronics.
Архитектура ARM сама по себе является дверью ко многим семействам.
Т.к. для множества этих микроконтроллеров разных фирм потребуется только один JTAG-отлдачик J-Link или его клон. А также если есть в наличии среда разработки IAR Embedded Workbench for ARM, то двери открыты.
Плюсом в сторону семейства STM32 является наличие библиотеки встроенной периферии, которая позволяет быстро писать свои пользовательские библиотеки с минимальными трудозатратами, а также 32-разрядность ядра в отличии от AVR и MSP430. Линейка микроконтроллеров STM32 включает много вариантов их внутреннего наполнения встроенной периферией от чего варьируется и стоимость. Например, микроконтроллер STM32L152VBT6 на ядре Cortex-M3, как микроконтроллеры семейства MSP430, нацелен на низкое энергопотребление и работает на 32 МГц.
Другой микроконтроллер STM32F107VCT6 также на ядре Cortex-M3 подходит для большинства задач, возлагаемый на данный класс устройств, и имеет частоту 72 МГц. Тут сразу видно, что для «тяжелой» математики и обработки микроконтроллеры на ядре Cortex-M3 куда больше подходят, чем MSP430 и AVR. Я работал и с «тяжеловесом» данного семейства STM32F407VGT6 на ядре Cortex-M4, частота которого доходит до 168 МГц. «Большой брат» идеально подошел для решения сложных математических задач. Кроме того, он имеет аппаратный FPU для математики с плавающей точкой. Для семейства STM32 разработана линейка плат DISCOVERY, которая позволяет получить плату со встроенным JTAG-отладчиком ST-Link, причем его можно использовать, чтобы программировать платы собственной разработки.
Результат их маркетинговой политики позволяет влиться в разработку микроконтроллеров с минимальными затратами, при этом имея фирменные платы и JTAG-отладчики от производителя.
Заключение
В заключении хочу сказать. Что у всех рассмотренных семейств есть свои плюсы. Со всеми связаны теплые воспоминания.
- msp430
- AVR
- STM32
- микроконтроллеры
- программирование микроконтроллеров
Начинаем изучать STM32 или Управляем светом по-умному
Однажды, заехав в очередную съемную квартиру, я столкнулся с определенным неудобством, которое достаточно сильно напрягало: выключатель света в основной комнате оказался за шкафом-стенкой, который был прикручен к стене, и его перестановка была невозможна т.к. на это требовалось значительно много времени и сил. Решить данную проблему хотелось очень сильно и в голову пришла одна мысль: сделать дистанционный пульт для управления освещением!
Именно с идеи создания собственного пультика для управления светом в комнате и началось моё увлечение электроникой, микроконтроллерами и различными радиоустройствами.
- Начинаем изучать STM32 или Управляем светом по-умному
- Начинаем изучать STM32: битовые операции
- Начинаем изучать STM32: Что такое регистры? Как с ними работать?
После этого я начал изучать данную тему, знакомиться с основами электроники, примерами устройств, узнавать, как люди реализуют подобного рода устройства. Поискав информацию на тему того, с чего можно было бы начать изучение микроконтроллеров я узнал о том, что такое Arduino, с чем их едят, о том, как с ними работать. Легкое решение выглядело весьма привлекательно, ведь насколько я понял на тот момент, код собирается на раз-два. Но сделав вывод, что я не узнаю, что творится внутри микроконтроллера за рамками Arduino-скетчей я решил поискать более интересный вариант, который подразумевал глубокое изучение и погружение в дебри микроконтроллерной техники.
В компании, в которой я работаю, имеется отдел разработки, и я решил обратиться к инженерам чтобы они направили меня на путь истинный и показали с чего можно было бы начать решение своей задачи. Меня решительно отговорили от изучения Arduino и у меня в руках оказалась неведомая и непонятная зеленая платка на которой виднелись надписи, буковки, разные электронные компоненты.

Всё это для меня на тот момент показалось непостижимо сложным, и я даже пришел в некоторое смятение, но от реализации поставленной задачи отказываться не собирался. Так я познакомился с семейством микроконтроллеров STM32 и платой STM32F0-Discovery, после изучения которых мне хотелось бы сваять свой девайс под нужные мне цели.
К моему большому удивлению, такого большого комьюнити, статей, примеров, различных материалов по STM не было в таком же изобилии как для Arduino. Конечно, если поискать найдется множество статей «для начинающих» где описано, как и с чего начать. Но на тот момент мне показалось, что все это очень сложно, не рассказывались многие детали, интересные для пытливого ума новичка, вещи. Многие статьи хоть и характеризовались как «обучение для самых маленьких», но не всегда с их помощью получалось достичь требуемого результата, даже с готовыми примерами кода. Именно поэтому я решил написать небольшой цикл статей по программированию на STM32 в свете реализации конкретной задумки: пульт управления освещением в комнате.
Почему не AVR/Arduino?
Предвосхищая высказывания о том, что неопытному новичку бросаться сразу же в изучение такого сложного МК как STM32 было бы рановато — я расскажу, почему я решил пойти именно этим путём, не вникая и не знакомясь с семейством процессоров от Atmel и даже не рассматривая Arduino как вариант.
Во-первых, решающую роль сыграло отношение цена-функционал, разницу видно даже между одним из самых дешевых и простых МК от ST и достаточно «жирной» ATMega:

После того, что я увидел значительные различия между ценой и возможностями AVR и STM32 – мною было принято решение, что AVR использовать в своей разработке я не буду =)
Во-вторых, я предварительно для себя старался определить набор умений и навыков, которые бы я получил к моменту, когда я достигну требуемого результата. В случае если бы я решил использовать Arduino – мне было бы достаточно скопировать готовые библиотеки, накидать скетч и вуаля. Но понимание того, как работают цифровые шины, как работает радиопередатчик, как это всё конфигурируется и используется – при таком раскладе мне бы не пришло бы никогда. Для себя я выбрал самый сложный и тернистый путь, чтобы на пути достижения результата – я бы получил максимум опыта и знаний.
В-третьих, любой STM32 можно заменить другим STM32, но с лучшими характеристиками. Причем без изменения схемы включения.
В-четвертых, люди, занимающиеся профессиональной разработкой больше склонны к использованию 32-разрядных МК, и чаще всего это модели от NXP, Texas Instruments и ST Microelectronics. Да и мне можно было в любой момент подойти к своим инженерам из отдела разработки и разузнать о том, как решить ту или иную задачу и получить консультацию по интересующим меня вопросам.
Почему стоит начинать изучение микроконтроллеров STM32 с использования платы Discovery?
Как вы уже поняли, знакомство и изучение микроконтроллера STM32 мы начнем с Вами, уважаемые читатели, с использования платы Discovery. Почему именно Discovery, а не своя плата?
- На любой плате Discovery имеется встроенный программатор/отладчик ST-LINK который подключается к компьютеру через USB и его можно использовать как для программирования микроконтроллера на плате, так и внешних устройств путем снятия/установки соответствующих перемычек. То есть плюсом ко всему — мы еще и экономим деньги, получая решение два в одном: микроконтроллер и программатор.
- Платы Discovery имеют полную разводку всех пинов прямо с микроконтроллера на пины платы. Я для удобства использования воткнул Discovery так же в две макетные платы.

Что нам понадобится для разработки помимо платы Discovery?
В своей работе с платой Discovery нам понадобится еще ряд незаменимых вещей, без которых мы не сможем обойтись:
-
Схему платы чтобы видеть куда, где и что подключено. Взять схему можно на страничке производителя Вашей платы в разделе Schematic Pack. Скачать схемы можно пролистав страницу немного ниже в блоке, указанном на картинке:




Приступим к первоначальной настройке и подготовке IDE к работе!
После того, как скачается установочный файл нашей IDE можно приступать к установке. Следуя указаниям инсталлятора проведите процесс установки. После того, как скопируются все файлы, необходимые для работы появится окно установщика софтовых пакетов для разработки Pack Installer. В данном установщике содержатся низкоуровневые библиотеки, Middleware, примеры программ, которые регулярно пополняются и обновляются.

Для начала работы с нашей платой нам необходимо установить ряд пакетов необходимых для работы и необходимо найти микроконтроллер, с которым мы будем работать. Так же можно воспользоваться поиском вверху окна. После того, как мы нашли наш МК кликаем на него и во второй половине окна и нам необходимо установить следующий перечень библиотек:
- Keil::STM32F0xx_DFP – полноценный пакет программного обеспечения для конкретного семейства микроконтроллеров, включающий в себя мануалы, даташиты, SVD-файлы, библиотеки от производителя.
- ARM::CMSIS – пакет Cortex Microcontroller Software Interface Standard, включающий в себя полный набор библиотек от ARM для поддержки ядра Cortex.
- Keil::ARM_Compiler – последняя версия компилятора для ARM.

Для этого необходимо перейти в меню Project -> New uVision Project и выбрать папку, в которую сохраним наш проект.
После Keil спросит нас какой МК будет использоваться в проекте. Выбираем нужный нам МК и нажимаем ОК.

И вновь появится, уже знакомое нам, окно в котором мы можем подключить интересующие нас модули к проекту. Для нашего проекта понадобится два модуля:
- Ядро библиотеки CMSIS, в котором объявлены настройки, адреса регистров и многое другое из того что необходимо для работы нашего МК.
- Startup-файл, который отвечает за первоначальную инициализацию МК при старте, объявление векторов и обработчиков прерываний и многое другое.

После того как мы нажмем клавишу ОК мы можем приступать к созданию нашего проекта.
Для того, чтобы сконфигурировать параметры проекта и настроить наш программатор нужно правым кликом по Target 1 открыть соответствующее меню.

В главном меню проекта настраиваем параметр Xtal в значение 8.0 MHz. Данный параметр отвечает за частоту работы кварцевого осциллятора нашего МК:

Далее переходим к настройке нашего программатора/дебагер. Кликаем в этом же окне на вкладку Debug и выбираем в поле Use параметр ST-Link Debugger и переходим в настройки:

В настройках мы должны увидеть модель нашего ST-Link установленного на плате, его серийный номер, версию HW и IDCODE МК который будем прошивать:

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

После этого нужно настроить еще одну опцию, которая позволит нам писать русскоязычные комментарии к коду наших проектов. Нажимаем кнопку Configuration и в открывшемся меню в поле Encoding выбираем Russian Windows-1251.

Всё. Наша IDE и программатор готовы к работе!
В Keil имеется удобный навигатор по проекту, в котором мы можем видеть структуру проекта, необходимые для работы справочные материалы, в т. ч. те, которые мы уже скачали к себе на компьютер до этого (схема Discovery, datasheet, reference manual), список функций, использованных в проекте и шаблоны для быстрой вставки разных языковых конструкций языка программирования.

Переименуем папку в структуре проекта с Source Group 1 на App/User, таким образом обозначив то, что в данной папке у нас будут располагаться файлы пользовательской программы:

Добавим основной файл программы через навигатор проекта, выполнив команду Add New Item To Group “App/User”.

Необходимо выбрать из предложенного списка C File (.c) и назначить ему имя main.c:

Созданный файл автоматически добавится в структуру проекта и откроется в главном окне программы.
Что ж, теперь мы можем приступить к созданию нашей программы.
Первым делом, необходимо подключить к нашему исполняемому файлу заголовочный документ нашего семейства микроконтроллеров. Добавим в файл 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 // Искусственная задержка /* Зажигаем светодиод PC9, гасим PC8 */ GPIOC->ODR = 0x200; for (int i=0; i // Искусственная задержка > >
После того, как мы написали нашу программу, настала пора скомпилировать код и загрузить прошивку в наш МК. Чтобы скомпилировать код и загрузить можно воспользоваться данным меню:

Команда Build (или горячая клавиша F7) скомпилирует код, и если не было никаких ошибок программе выведет в логе компиляции следующее сообщение о том, что ошибок и предупреждений нет:

Команда Load (или горячая клавиша F8) загрузит компилированный код в наш МК и автоматически отправит его на исполнение:

После загрузки кода мы увидим, как светодиоды начали мигать с равными временными промежутками.

Ура! Первый шаг в освоении микроконтроллеров STM32 мы сделали! В следующем уроке мы разберем что такое битовые и логические операции, как ими пользоваться и узнаем об одной очень полезной утилитке для работы с МК, ну а пока можем наслаждаться тем, как весело перемигиваются светодиоды на нашей плате Discovery. )
- Начинаем изучать STM32 или Управляем светом по-умному
- Начинаем изучать STM32: битовые операции
- Начинаем изучать STM32: Что такое регистры? Как с ними работать?
- Умный дом
- Электроника для начинающих
Программируемый микроконтроллер STM32 — с места в карьер
Ниже описан мой первый опыт общения с программируемым микроконтроллером в лице STM32VLDiscovery, результатом которого явилась машинка из LEGO, управляемая с телефона, и ещё кое-что. Я постарался изложить свой путь в виде пошагового руководства к действию, но, предупреждаю сразу, не руководства «как делать правильно». Первые два раздела являются предисловием и не относятся непосредственно к данному микроконтроллеру.

1. Первый контакт с микроэлектроникой
Неважно почему, но возникла мысль: сделать устройство, воспринимающее команды с телефона. Как это сделать? Можно через Bluetooth, WiFi, USB и т.д. Но проще и универсальней мне показалось распознавание звуков, которые можно брать с выхода на гарнитуру мобильного телефона (здесь и далее для решения задач будут выбираться самые простые в реализации, результат-ориентированные методы).
Звуки можно синтезировать специальной программой для смартфона, но есть вариант интересней — DTMF-сигналы. Они стандартны и используются в любом мобильном и в большинстве стационарных телефонов (при наборе номера или в голосовом меню), без проблем передаются в голосовом канале.
Задача 1: Распознавание DTMF-сигналов
- Дешифратор MT 8870DE — из входного аналогового сигнала получает двоичный код (4 бита) последней распознанной DTMF команды и 1 бит — наличие/отсутствие DTMF команды сию секунду (назовём его «пятый вывод»).
- Кварцевый резонатор, конденсаторы и резисторы, необходимые для работы дешифратора.
- 7-сегментный индикатор — для отображения нажатой на телефоне цифры.
- КР 514ИД-1 дешифратор для 7-сегментного индикатора — из двоичного кода получает 7 выходов для каждого из сегментов.
- Макетная плата для монтажа без пайки.
- Проводки/перемычки для макетной платы.
Собираем, проверяем, исправляем ошибки… работает. Шок.
Теперь всё это нужно заставить управлять машинкой. Причём, чтобы действия машинки логично соответствовали нажатой цифре: 2 — вперёд, 4 — влево, 6 — вправо, 8 — назад.
Задача 2: 4 определённые комбинации сигналов с DTMF-декодера должны активировать одну из 4-х функций
Простейший вариант — десятичный дешифратор. Был куплен К155ИД10 с открытым коллектором — 4 входа для двоичного кода, 10 выходов символизируют числа от 0 до 9, на нужном выходе будет «0», на остальных — «1». Данный дешифратор применяется с нагрузками, рабочий ток в которых может достигать 80 мА (лампочки накаливания, реле), что удобно. Хотя транзисторы всё равно пришлось использовать.
Задача 3: Смена полярности для моторов, Н-мост
Всё детство я собирал машинки из LEGO, которые ездили вперёд-назад ручным переключением полярности батареек (наборов LEGO с моторами и пультами у меня никогда не было и нет). Теперь пришло время играть в игрушки по-взрослому.
Нормальные люди делают Н-мост на транзисторах. Продвинутые — покупают готовое решение на этих самых транзисторах. Я сделал на переключающихся электромагнитных реле. Позже я купил мощные (на коллекторный ток до 3А) NPN и PNP транзисторы TIP31 C ST и TIP32 C ST, собрал из них этот самый Н-мост, но он почему-то работал только со встречно включёнными светодиодами, а мотор крутить был не в силах. Не знаю, почему.
Переключающихся реле требуется по два на каждый мотор (транзисторов или замыкающихся реле требуется по 4). По умолчанию, оба контакта моторчика (обычного коллекторного, как в игрушках) соединены с минусом. Если на одно из реле подать управляющий сигнал, контакт моторчика переключится на плюс, мотор заработает. Огромное преимущество такого подключения — ни при каких условиях невозможно даже кратковременное короткое замыкание по вине реле. Недостаток — при снятии управляющего напряжения с реле мотор закорачивается и становится тормозом. Решение — ещё одно реле, замыкающее. Теперь для вращения мотора нужно замыкать и его тоже, для свободного хода — размыкать, для «торможения» — снова замыкать. Это реле будет добавлено позже.
Задача 4: Собрать машинку
Моторов в машинке два: один через LEGOвский дифференциал крутит задние колёса, второй — поворачивает передние. Пластмассовые шестерни в редукторах не добавляют системе КПД, но всё это работает.
Ставим на плату 4 реле. Мне говорили, что макетная плата не рассчитана на большие токи (каждый мотор потребляет по 1-1,6 А), но если я не получу результат в короткие сроки, проект рискует остаться очередным воздушным замком. Кстати, включение любого реле вызывало броски напряжения, что приводило к зацикливанию и сбоям в работе. Наверняка это решается специальными схемами, но я просто запитал моторы от отдельного блока с батарейками.
Электромагнитные реле, два источника питания — не многовато ли хардкора? Дальше больше!
Осталось подключить всё это к десятичному дешифратору. Нужные выводы к эмиттерам 4-х NPN-транзиторов BC547, к базе которых сигнал с DTMF-декодера о том, что в данный момент нажата клавиша («пятый вывод»), коллекторы — к каждому из реле. Криво, странно, но работает.


2. Строительство машинки на непрограммируемых логических элементах
Предположим, машинка должна уметь одновременно ехать и поворачивать. Логично для этого использовать цифры 1, 3, 7, 9, при нажатии на которые будут срабатывать по два реле. Изначально я планировал реализовать это подключением диодов от эмиттеров транзисторов к десятичному дешифратору, но это всё не работало. Сейчас до меня доходит, что если десятичный дешифратор выдаёт «0», то нужен PNP транзистор, который будет этим «нулём» открываться. Но у меня были только NPN.
Задача 5: Машинка должна ехать и поворачивать при нажатии 1, 3, 7 или 9
В целях развлечения/освоения/использования куплены логические элементы 4-И, 4-ИЛИ, НЕ, И-НЕ, ИЛИ-НЕ, И по одной штуке. Первая же мысль — использовать выводы с десятичного дешифратора и «пятый вывод» с DTMF-декодера в простой логической схеме, которая в итоге будет открывать нужные транзисторы (а через них — реле). Но! Десятичный дешифратор К155ИД10 в качестве «единицы» выдаёт 2 с чем-то Вольта. Ни один из имевшихся в распоряжении логических элементов такой сигнал за «единицу» не считал. Кроме 4-И (КР1533). Если использовать «голые» 4 бита двоичного кода с DTMF-а, то собрать требующуюся логику из имеющихся элементов не представлялось возможным. Нормальный же десятичный дешифратор облегчил бы задачу во много раз. А так получилось такое маленькое издевательство — сделать тяжело, но ведь можно же! (Идея лечь спать и утром съездить за деталями была отвергнута).
Помня, что десятичный дешифратор выдаёт свою «недоединицу» на все выводы, кроме вывода с дешифрованным числом, делаем следующее.
На первый… м… квартет входов элемента 4-И подаём числа 1, 2, 3 и «единицу»; на второй — 7, 8, 9 и «единицу». С выхода первого получим «ноль», если нажата была 1, 2 или 3 (машинка едет вперёд), с выхода второго — если 7, 8, 9 (назад). Иначе оба выхода элемента 4-И — «единицы».
Теперь повороты. Используем 5 оставшихся логических элементов и собираем хитрую схему. От цифр 4 и 6 было решено отказаться (зачем на месте поворачивать колёса?!), но даже составление схемы для поворотов одними только 1 и 7 (налево), 3 и 9 (направо) заставило изрядно напрячься.
В итоге, на плате (пришлось купить ещё одну, побольше) DTMF-декодер, индикатор, дешифратор для него, десятичный дешифратор, 4-И, НЕ, 4-ИЛИ, ИЛИ-НЕ, И-НЕ, 4 транзистора, 4 реле.
Машинка исполняет 6 разных команд, но возвращать передние колёса на середину не умеет. КПД LEGOвских деталей и малая мощность моторчика не позволяют сделать возвратную пружину или резинку.
Реле иногда цокают по несколько раз, причины не ясны (помехи?).
Логика работы с логикой освоена.

3. Начало работы с STM32
Среда разработки
Пришло время перейти от программирования проводками к программированию кодом. О программируемых микроконтроллерах/чипах я имел только смутные представления, что Raspberry Pi — слишком мощно, Arduino — дорого и не комильфо. Тут-то продавец Михаил и выписал мне STM32VLDiscovery за 579 рублей. О нём здесь и не только здесь уже писали, что очень помогало мне в его изучении.
Контроллер можно воткнуть в макетную плату, но не в любую. 6 ножек при этом останутся висеть — без них можно обойтись, благо остаётся ещё несколько десятков. Проверить работоспособность залитой в память программы можно даже не отключая плату от mini-USB шнура, через который программа попала в устройство. И питаться можно от него же.
Оптимальным выбором для начала работы показалась среда CoIDE от CooCox, потребовавшая всего одного танца с бубнами — ручного скачивания определённых файлов в определённую папку. Правда, заливать из неё прошивку в устройство у меня так и не получилось — приходится пользоваться ST-LINK Utility от производителя платы. Говорят, можно компилировать прямо из удобного Sublime Text 2, но длина инструкции поселила в меня слишком много скепсиса, чтобы попробовать.
Учитывая, что на Си я никогда не писал, даже простейшие мигания светодиодами вызывали у меня существенные затруднения. Вдобавок, непонятные регистры, 16-ричная система, разные режимы работы выходов и входов для человека, плохо понимающего назначение подтягивающего резистора… Но метод копипаста чьих-то готовых примеров делал своё дело. В свою очередь, цена устройства позволяет не слишком заморачиваться с правильным подключением аппаратной части. Например, я так и не понял, надо ли подключать каждую ножку GND к земле и не вредно ли питать устройство от 4-х пальчиковых батареек — пока всё живо и работает.
Ближе к коду. Приводить и описывать здесь чужие примеры не буду, но предлагаю на ваш суд несколько своих наработок. Они появлялись в тот момент, когда в Сети не находилось приемлемое для меня решение определённой проблемы.
Задача 6: Задержка
Например, нам хочется включить светодиод, а через некоторое время его погасить. Зачастую задержка реализована пустым циклом, выполняющимся много раз. Способ безусловно прост, но применим только там, где кроме мигания светодиода от платы ничего не требуется. Мы можем прервать цикл только, простите, прерыванием (о них чуть позже) или ставить дополнительные условия выхода из цикла. Иначе плата не делает ничего и ни на что не реагирует, пока длится этот цикл.
Что предлагаю я: используем наш главный бесконечный цикл сразу для всех необходимых в программе задержек, каждый проход увеличивая значение определённой переменной (назовём её «x»). Как только переменная достигает определённых величин, в теле цикла срабатывают определённые условия (их может быть сколько угодно). Недостаток — мы заранее не знаем, сколько точно будет длиться такая задержка. Если это и неважно — подбираем эмпирически нужные значения и пользуемся.
int main(void) < unsigned int x=0; char something; while(1) < if (x>5000) < // код >; if (x==10000) < // Другой код. Сработает только один раз, пока не обнулим x. >; if (something==1)// Некоторое событие < x=0; // На следующей итерации цикла всё начнётся сначала >if (x<15000) < x++; >; // Чтобы x не прошёл рано или поздно через 0, прекращаем инкрементацию x после 15000. >; >;
Задача 7: Прерывания с заданным интервалом
Предположим, мы не меняли кварцевый резонатор, и частота работы процессора осталась 24 МГц. Создав прерывание при достижении 2400-го отсчёта процессора через специальные инструкции, мы получим выполнение функции TIM6_DAC_IRQHandler (это обработчик прерывания) с частотой 100 Гц — каждые 10 миллисекунд. Приведённый ниже код изначально где-то взят.
unsigned int ti=0; int main(void) < //Включаем порт С и таймер 6 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); TIM6->PSC = 24000 - 1; // Таймер тикает 1000 раз в секунду TIM6->ARR = 10 - 1; // Прерывание срабатывает на каждой 10-й миллисекунде TIM6->DIER |= TIM_DIER_UIE; // Разрешаем прерывание от таймера TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт! NVIC_EnableIRQ(TIM6_DAC_IRQn); // Разрешение TIM6_DAC_IRQn прерывания >; // Обработчик прерывания TIM6_DAC void TIM6_DAC_IRQHandler(void) < ti++; TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг после обработки прерывания >
- использование атомарных операций (они не прерываются) — присвоение типа int таковой не является, а использовать нужно именно «длинный» тип;
- использование дополнительных переменных или регистров для выяснения, срабатывало ли прерывание за время выполнения конкретных инструкций — немного громоздко и не слишком понятно (для меня);
- другие варианты.
Я решил эту проблему так. Присваиваем локальной переменной sti изменяющуюся переменную ti до тех пор, пока она не «присвоилась». Можно добавить небольшой «люфт», если точность непринципиальна.
do < sti=ti; >while (ti>1+sti);
Сама по себе локальная переменная sti используется только лишь для того, чтобы не вставлять подобные циклы везде, где нам нужно значение переменной ti.
Далее в программе можно безбоязненно обращаться к переменной sti и даже обнулять её. Я использовал всё это для фиксации времени, в течение которого машинке подаётся та или иная команда, чтобы потом машинка могла повторить маршрут. Естественно, обнуление должно не пропасть даром, поэтому в конце основного цикла программы добавлен нехитрый код:
if (sti==0) < do < ti=0; >while (ti>1); >
Если в момент присвоения или в момент проверки условия выхода переменная ti изменится и будет «представлена» в некорректном виде, цикл повторится. Чем выше частота прерываний, тем чаще условие выхода из цикла будет срабатывать со второго раза. Естественно, если частота прерываний слишком высока, зацикливания не заставят себя ждать. Для контроля количества неудавшихся попыток присвоения я использовал светодиоды.
Задача 8: Светодиоды в качестве дисплея
Иногда для корректировки программы хочется получать значения определённых переменных в процессе работы устройства, а дисплея или индикатора под рукой нет или их не хочется подключать. Тогда можно просто заставить светодиоды помигать нужное количество раз, сделать паузу, помигать ещё раз и т. д. Для этих целей я использую в теле главного цикла следующий код:
// Счёто-мигалка if ((todisp>0) && (sti%10==0)) < if ((sti%(todisp*20+100)BSRR=GPIO_BSRR_BS8; > else < GPIOC->BRR=GPIO_BRR_BR8; >; >;
В любом месте программы мы присваиваем переменной todisp, например, 3, и светодиод №8 на плате будет мигать по три раза, делая секундную паузу между сериями. Переменная sti инкрементируется каждые 10 миллисекунд (через переменную ti), вместо неё можно использовать переменную x, инкрементирующуюся с каждым проходом главного цикла. В этом случае коэффициенты надо будет немного увеличить.
Пример использования
while (1) < do < sti=ti; todisp++; >while (ti>1+sti); todisp--; if ((todisp>0) && (sti%10==0)) < if ((sti%(todisp*20+100)BSRR=GPIO_BSRR_BS8; > else < GPIOC->BRR=GPIO_BRR_BR8; >; >; >
В данном примере каждый раз, когда из-за прерывания операция присвоения sti=ti прошла некорректно и, следовательно, операция сравнения ti>1+sti возвращает true (что может также произойти, если прерывание случилось в момент сравнения), светодиод будет мигать на один раз больше. В принципе, можно даже замерить, сколько раз мигает светодиод через n минут работы платы и посчитать среднее время работы цикла (исходя из частоты прерываний), а также убедиться, что все эти меры предосторожности при использовании прерываний отнюдь не лишние.
Задача 9: Подключение микроконтроллера к машинке
Проведя достаточное количество экспериментов на светодиодах, можно постепенно менять их на колёса. Берём от старой схемы на базовой логике только самое нужное: DTMF-декодер, индикатор с дешифратором для красоты, реле с транзисторами. О десятичном дешифраторе можно наконец-то забыть!
Выходы с DTMF-декодера соединяем с входами на контроллере, которые мы выбрали для этих целей. Выходы с контроллера (опять-таки, выбранные и инициализированные в программе) через резистор (например, 3 кОм) на базу обычного NPN транзистора (я использовал BC547). Эмиттеры — к «плюсу», коллекторы — на катушку соответствующего реле. Добавляем пятое реле, предотвращающее закорачивание (режим «тормоза») основного мотора, поворотники/габариты по вкусу. «Плюс» для моторов подводится к реле от отдельного блока батареек. Для питания как схемы, так и моторов, я использую пластиковые корпуса (2 штуки) для 4-х пальчиковых батареек/аккумуляторов каждый. Они стоят всего по 40 рублей, а я в детстве вечно мучался со скотчем и фольгой…
У меня получилось так, что провода от DTMF-декодера проходят аккурат над реле, отвечающими за движение вперёд/назад. Возможно, это добавляет помех к уже имеющимся скачкам напряжения при включении реле. В результате, во время презентации перед однокурсниками реле начинали переодически трещать, а назад машинка вовсе отказывалась ехать — реле безостановочно переключалось, издавая равномерный треск. Диоды подключенные параллельно катушкам реле в сторону «плюса», никак не влияли на эту ситуацию. Дорабатываем программу — через каждые 10 миллисекунд проверяем значения входов и реагируем на изменения, только если 6 раз подряд комбинация пяти входов одинакова. Задержка на глаз почти незаметна, проблема с помехами решена. Вот они, главные преимущества программируемых микроконтроллеров!
4. Определяем угол поворота передних колёс — обратная связь через АЦП (аналогово-цифровой преобразователь)
Во второй части я упомянул, что механически выравнять передние колёса на середину не получается. Пришло время решить проблему, т. к. машинка должна уметь ездить по прямой!
Задача 10: Подключение реостата
Решение напрашивается само собой — если не пружина, то пусть сам мотор крутится в обратную сторону, пока не выровняет колёса. Цифровых энкодеров под рукой нет, но есть замечательный реостат на 10 кОм, в плате есть 12-разрядный АЦП (даже 2), в Интернетах есть схема подключения и код. Концы реостата к «плюсу» и «минусу», ползунок — через резистор 1 кОм на вход. Кстати, для целей АЦП можно использовать не любой вход. Я использовал тот, который был указан в примере. Пример кода можно найти прямо в CoIDE для каждой из библиотек, в том числе ADC (не забываем в CoIDE подключить библиотеку ADC). Код копипастом оказывается в моей программе и, что главное, работает.
Теперь устанавливаем реостат в машинку, вал реостата соединяем соосно с выходным валом «редуктора» поворотного механизма — он третий от мотора. Процесс сопряжения неLEGOвских деталей с LEGOвскими всегда доставляет, но мотор-таки умудряется и реостат вращать, и колёса поворачивать. С трудом, надо сказать. От реостата к плате тянется три провода, ещё четыре тянутся от моторов.
Задача 11: Использование данных с реостата
Эмпирическим методом подбираются численные значения от 0 до 4095, соответствующие приемлемой левой и правой границам угла поворота передних колёс при их установке прямо. Вроде, этот диапазон называется мёртвой зоной — в пределах него колёса считаются выровненными на середину. Также я подобрал значения для отключения мотора при достижении максимального угла поворота.
js= 2500; // Примерное значение центра j — значение входного напряжения от 0 до 4095 (АЦП) gist=150; // Диапазон мёртвой зоны full=500; // Граница достижения максимального угла поворота while (1) < //. if ((x>=max*7) && (inpok)) // inpok равен 0, если ни одна кнопка в данный момент не нажата. Нестрогое равенство приводит к проверке следующих условий на каждой последующей итерации главного цикла. if ((num==1) || (num==4) || (num==7)) //Влево < GPIOC->BRR=GPIO_BRR_BR7; if (j>0+full) < GPIOC->BSRR=GPIO_BSRR_BS6; > else < if (j<0+full-gist) < GPIOC->BRR=GPIO_BRR_BR6; > > > else < GPIOC->BRR=GPIO_BRR_BR1; > if ((num==3) || (num==6) || (num==9)) //Вправо < GPIOC->BRR=GPIO_BRR_BR6; if (j<4250-full) < GPIOC->BSRR=GPIO_BSRR_BS7; // Включаем реле «мотор вправо» > else < if (j>4250-full+gist) < GPIOC->BRR=GPIO_BRR_BR7; // Выключаем реле «мотор вправо» > > > else < GPIOC->BRR=GPIO_BRR_BR3; > // Выравниваем колёса на середину if ((num==2) || (num==5) || (num==8)) < if (jBRR=GPIO_BRR_BR6; // Выключаем реле «мотор влево» (если вдруг реле ещё включено) GPIOC->BSRR=GPIO_BSRR_BS7; // Включаем реле «мотор вправо» > else < if (j>js-gist*2) < GPIOC->BRR=GPIO_BRR_BR7; Выключаем реле «мотор вправо» > >; if (j>js+gist*3) < GPIOC->BRR=GPIO_BRR_BR7; GPIOC->BSRR=GPIO_BSRR_BS6; > else < if (jBRR=GPIO_BRR_BR6; > >; >; >; //.. >
Если машинка эксплуатируется в экстремальных режимах, и под воздействием чудовищных перегрузок колёса вдруг изменят угол поворота без соответствующей команды, этот же код вернёт их в требуемое положение. Если же мы нажимаем, например, 4, а колёса уже находятся в крайнем левом положении, реле включаться не будет.
Напомню, некоторый код брался из чьих-то примеров.
Итоговый код программы управления машинкой
/** ***************************************************************************** * @title ADC_simple.c * @author Claude * @date 2010 Dec 29 * @brief ADC Example, Blink a LED according to ADC value ******************************************************************************* */ #include #include #include #include "stm32f10x.h" #include "stm32f10x_conf.h" GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; unsigned int i,j,js,gist,full,ti=0; /* Blink a LED, blink speed is set by ADC value */ int main(void) < unsigned int x,max; unsigned int move[10]; unsigned int oldsti=0,sti=0,time[10],tit=0; unsigned short recheck=5; unsigned char todisp=0,todisp2=0; x=0; unsigned char ji,ju,inp0,inp1,inp2,inp3,inpok,ink,in0,in1,in2,in3,inok,num,oldnum,back; max=250; inp0=0; inp1=0; inp2=0; inp3=0; inpok=0; ink=0; back=0; num=0; void backreset(void) < back=0; for (ju=0;ju<10;ju++) < time[ju]=0; move[ju]=0; >GPIOC->BRR=GPIO_BRR_BR9; > GPIO_InitTypeDef PORT,GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); PORT.GPIO_Pin = (GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12| GPIO_Pin_8 | GPIO_Pin_9); PORT.GPIO_Mode = GPIO_Mode_Out_PP; PORT.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init( GPIOC , &PORT); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //включаем тактирование порта A GPIO_InitStructure.GPIO_Pin = (GPIO_Pin_9 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7); //задаем номер вывода GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOB, &GPIO_InitStructure); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE); PORT.GPIO_Pin = (GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_5); PORT.GPIO_Mode = GPIO_Mode_Out_PP; PORT.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init( GPIOA , &PORT); // input of ADC (it doesn't seem to be needed, as default GPIO state is floating input) GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 ; // that's ADC1 (PA1 on STM32) GPIO_Init(GPIOA, &GPIO_InitStructure); //clock for ADC (max 14MHz --> 72/6=12MHz) RCC_ADCCLKConfig (RCC_PCLK2_Div6); // enable ADC system clock RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // define ADC config ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // we work in continuous sampling mode ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_RegularChannelConfig(ADC1,ADC_Channel_1, 1,ADC_SampleTime_28Cycles5); // define regular conversion config ADC_Init ( ADC1, &ADC_InitStructure); //set config of ADC1 // enable ADC ADC_Cmd (ADC1,ENABLE); //enable ADC1 // ADC calibration (optional, but recommended at power on) ADC_ResetCalibration(ADC1); // Reset previous calibration while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); // Start new calibration (ADC must be off at that time) while(ADC_GetCalibrationStatus(ADC1)); // start conversion ADC_Cmd (ADC1,ENABLE); //enable ADC1 ADC_SoftwareStartConvCmd(ADC1, ENABLE); // start conversion (will be endless as we are in continuous mode) // debug information RCC_ClocksTypeDef forTestOnly; RCC_GetClocksFreq(&forTestOnly); //this could be used with debug to check to real speed of ADC clock //Включаем порт С и таймер 6 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); TIM6->PSC = 24000 - 1; // Настраиваем делитель что таймер тикал 1000 раз в секунду // TIM6->ARR = 1000 ; // Чтоб прерывание случалось раз в секунду TIM6->ARR =4; TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт! NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания j= 2500; js=j; gist=150; full=500; if (back==0) < for (ju=0;ju<10;ju++) < time[ju]=0; move[ju]=0; >> while (1) < // adc is in free run, and we get the value asynchronously, this is not a really nice way of doing, but it work! j = ADC_GetConversionValue(ADC1) ; // value from 0 to 4095 /* possible change : * ADC_ContinuousConvMode = DISABLE * then on the infinite loop, something like : * * ADC_SoftwareStartConvCmd(ADC1, ENABLE); // start ONE conversion * while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // wait end of conversion * j = ADC_GetConversionValue(ADC1) * 500; // get value * */ do < sti=ti; >while (ti>1+sti); //if ((back!=2) || GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9)) if (oldsti!=sti) < oldsti=sti; if (inkif (in1==recheck) < in1=1; >if (in2==recheck) < in2=1; >if (in3==recheck) < in3=1; >if (inok==recheck) < inok=1; >if ((back==2) && (inok==1)) < backreset(); >if ( (back!=2) && (back!=4) && ((inp0!=in0) || (inp1!=in1) || (inp2!=in2) || (inp3!=in3) || (inpok!=inok)) ) < x=0; inp0=in0; inp1=in1; inp2=in2; inp3=in3; inpok=inok; oldnum=num; if (inp0) // Двоично-десятичный дешифратор < if (inp1) < if (inp2) < if (inp3) < num=15; >else < num=7; >; > else < if (inp3) < num=11; >else < num=3; >; >; > else < if (inp2) < if (inp3) < num=13; >else < num=5; >; > else < if (inp3) < num=9; >else < num=1; >; >; >; > else < if (inp1) < if (inp2) < if (inp3) < num=14; >else < num=6; >; > else < if (inp3) < num=10; >else < num=2; >; >; > else < if (inp2) < if (inp3) < num=12; >else < num=4; >; > else < if (inp3) < num=8; >else < num=0; >; >; >; >; >; > in0=0; in1=0; in2=0; in3=0; inok=0; > > >; // Счётчики if ((todisp>0) && (sti%10==0)) < if ((sti%(todisp*20+100)BSRR=GPIO_BSRR_BS8; > else < GPIOC->BRR=GPIO_BRR_BR8; >; >; if ((todisp2>0) && (sti%10==2)) < if ((sti%(todisp2*20+100)BSRR=GPIO_BSRR_BS9; > else < GPIOC->BRR=GPIO_BRR_BR9; >; >; // Повторение маршрута if ((num==12)) < switch (back) < case 0: if (inpok) < back=3; >; break; case 3: if (!inpok) < back=4;ji=0;inpok=0;x=0; sti=0; >; break; default: ; > >; if (back==4) < if (inpok==0) < while ((move[ji]<1) && (ji<10)) < ji++; >; if ((ji<10)) < if ((sti>100)) < x=0; sti=0; num=move[ji]; tit=time[ji]; inpok=1; GPIOC->BRR=GPIO_BRR_BR8; GPIOC->BSRR=GPIO_BSRR_BS9; > > else < backreset(); >> else < if (sti>tit) < inpok=0; sti=0; x=0; ji++; GPIOC->BSRR=GPIO_BSRR_BS8; > > >; // Возвращение назад if ((num==10)) < switch (back) < case 0: if (inpok) < back=1; >; break; case 1: if (!inpok) < back=2;ji=9;inpok=0;x=0; sti=0; >; break; default: ; > >; if (back==2) < if (inpok==0) < if ((ji>0) && (move[ji]>0)) < if ((sti>100)) < x=0; sti=0; num=move[ji]; if (num<4) < num+=6; >else < if (num>6) < num-=6; >> tit=time[ji]; inpok=1; GPIOC->BRR=GPIO_BRR_BR8; GPIOC->BSRR=GPIO_BSRR_BS9; > > else < backreset(); >> else < if (sti>tit) < inpok=0; x=0; sti=0; ji--; GPIOC->BSRR=GPIO_BSRR_BS8; > > >; // Стираем историю движений при нажатии "*" if (num==11) < backreset(); >if ((x==3*max)) < GPIOC->BRR=GPIO_BRR_BR12; if ((!inpok) || (num<1) || (num>9)) < GPIOC->BRR=GPIO_BRR_BR6; GPIOC->BRR=GPIO_BRR_BR7; GPIOC->BRR=GPIO_BRR_BR10; GPIOC->BRR=GPIO_BRR_BR11; > if ((back!=2)&&(back!=4)) < in0=inp0; in1=inp1; in2=inp2; in3=inp3; inok=inpok; >if ((move[9]>0)&&(move[9] <10)&&(!inpok)&&(back!=2)&&(back!=4)) < for (ju=0;ju<9;ju++) < time[ju]=time[ju+1]; >time[9]=sti; >; >; if ((x==5*max) && (inpok)) < if ((1<=num) && (num<=9)&&(back!=2)&&(back!=4)) < for (ju=0;ju<9;ju++) //Сдвиг записей предыдущих движений < move[ju]=move[ju+1]; >move[9]=num; sti=0; //Засекаем время, в течение которого будет нажата кнопка >; if ((num==1) || (num==2) || (num==3)) < GPIOC->BRR=GPIO_BRR_BR10; GPIOC->BSRR=GPIO_BSRR_BS11; GPIOC->BSRR=GPIO_BSRR_BS2; > else < GPIOC->BRR=GPIO_BRR_BR11; >; if ((num==7) || (num==8) || (num==9)) //Едем назад < GPIOC->BRR=GPIO_BRR_BR11; GPIOC->BSRR=GPIO_BSRR_BS10; GPIOC->BSRR=GPIO_BSRR_BS2; > else < GPIOC->BRR=GPIO_BRR_BR10; >; >; if ((x>=max*7) && (inpok)) // inpok равен 0, если ни одна кнопка в данный момент не нажата. Нестрогое равенство приводит к проверке следующих условий на каждой последующей итерации главного цикла. < if ((num==1) || (num==4) || (num==7)) //Влево < GPIOC->BRR=GPIO_BRR_BR7; if (j>0+full) < GPIOC->BSRR=GPIO_BSRR_BS6; > else < if (j<0+full-gist) < GPIOC->BRR=GPIO_BRR_BR6; > > > else < GPIOC->BRR=GPIO_BRR_BR1; > if ((num==3) || (num==6) || (num==9)) //Вправо < GPIOC->BRR=GPIO_BRR_BR6; if (j<4250-full) < GPIOC->BSRR=GPIO_BSRR_BS7; > else < if (j>4250-full+gist) < GPIOC->BRR=GPIO_BRR_BR7; > > > else < GPIOC->BRR=GPIO_BRR_BR3; > // Выравниваем колёса на середину if ((num==2) || (num==5) || (num==8)) < if (jBRR=GPIO_BRR_BR6; // Выключаем реле «мотор влево» (если вдруг реле ещё включено) GPIOC->BSRR=GPIO_BSRR_BS7; // Включаем реле «мотор вправо» > else < if (j>js-gist*2) < GPIOC->BRR=GPIO_BRR_BR7; Выключаем реле «мотор вправо» > >; if (j>js+gist*3) < GPIOC->BRR=GPIO_BRR_BR7; GPIOC->BSRR=GPIO_BSRR_BS6; > else < if (jBRR=GPIO_BRR_BR6; > >; >; >; if ((x==max*15)) < if (((num==1) || (num==2) || (num==3) || (num==5) || (num==7) || (num==8) || (num==9))&&(inpok)) //Замыкаем реле основного мотора < GPIOC->BSRR=GPIO_BSRR_BS12; > else < GPIOC->BRR=GPIO_BRR_BR12; > x++; > else < x++; if (x>max*20+300000) < if (!inpok) < GPIOC->BRR=GPIO_BRR_BR2;// Гасим габариты > x=max*20+1; > > // Мигание поворотников if (x%80000<40000) < if ((num%3==1) && (inpok) && (num<10)) < GPIOC->BSRR=GPIO_BSRR_BS1; > else < GPIOC->BRR=GPIO_BRR_BR1; > if ((num%3==0) && (inpok) && (num<10)) < GPIOC->BSRR=GPIO_BSRR_BS3; > else < GPIOC->BRR=GPIO_BRR_BR3; > > else < GPIOC->BRR=GPIO_BRR_BR1; GPIOC->BRR=GPIO_BRR_BR3; > if (sti==0) < do < ti=0; >while (ti>1); > >; return 0; >; // Обработчик прерывания TIM6_DAC void TIM6_DAC_IRQHandler(void) < ti++; TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF >


Работает это примерно так:
5. Использование ЦАП (цифро-аналогового преобразователя) для воспроизведения звуков
Раз уж был освоен АЦП, то и ЦАП не должен остаться без внимания. Наиболее очевидным способом его использования в учебно-развлекательных целях мне представляется генерация звуков. Порядок освоения классический: находим в Сети готовый пример, запускаем, «вау!», модифицируем код.
Динамик я подключил к плюсу и коллектору NPN транзистора, на базу транзистора через резистор приходит сигнал с ноги, на которую выведен ЦАП. По идее, динамик питается электрическим сигналом звуковой частоты с изменяющейся полярностью, я же подаю ток на динамик только в одном направлении. Пробовал разные схемы о двух транзисторах, но услышать существенной разницы в звучании не получилось. Возможно, просто не нашёл подходящей схемы.
Первые звуки были получены с помощью этого примера.
Код примера
#include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_gpio.h" /* Массив, элементы которого нужно быстро запихивать в DAC чтоб получить синус */ const uint16_t sin[32] = < 2047, 2447, 2831, 3185, 3498, 3750, 3939, 4056, 4095, 4056, 3939, 3750, 3495, 3185, 2831, 2447, 2047, 1647, 1263, 909, 599, 344, 155, 38, 0, 38, 155, 344, 599, 909, 1263, 1647>; unsigned char i=0; int main(void) < /* Включаем порт А */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* Включаем ЦАП */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); /* Включаем таймер 6 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); /* Настраиваем ногу ЦАПа */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); /* Настраиваем таймер так чтоб он тикал почаще */ TIM6->PSC = 0; TIM6->ARR = 500; TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт! NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания /* Включить DAC1 */ DAC->CR |= DAC_CR_EN1; /* Бесконечный цикл */ while (1) < >> /*Обработчик прерывания от таймера 6 */ void TIM6_DAC_IRQHandler(void) < TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF DAC->DHR12R1=sin[i++]; //Запихиваем в ЦАП очередной элемент массива if (i==32) i=0; //Если вывели в ЦАП все 32 значения то начинаем заново >
Неплохо бы посчитать, какой частоты должно получиться пищание, исходя из кода программы, и сравнить результат с показаниями тюнера. Частота процессора: 24 000 000 Гц. Таймер TIM6->PSC сбрасывается при достижении нуля, т.е. каждый такт. Таймер TIM6->ARR считает до 500, т.е. прерывание срабатывает на каждый 501-й сброс TIM6->PSC — в нашем случае это (24 000 000 Гц / 1) / 501 ≈ 47 904 Гц. Это частота дискретизации будущего звукого сигнала. Напомню, что в подавляющем большинстве лицензионных компакт-дисков и пиратских mp3 файлов используется частота дискретизации 44 100 Гц. Но, признаться, разрядность в них повыше — 16 бит вместо наших 12. Уверен, вы это знали.
Звуковой сигнал — синусоида, один период которой описан 32 значениями в массиве sin. Делим частоту дискретизации на количество элементов этого массива и получаем частоту звукового сигнала: 47 904 Гц / 32 ≈ 1 497 Гц. Скачиваем тюнер на смартфон (ещё лет 5 назад я бы очень удивился такой фразе), запускаем программу на контроллере и… очередное «вау!». Тюнер показывает близость к F#6, что означает Фа диез третьей октавы, а маленькие циферки уточняют частоту: 1 497,1 Гц.

Задача 12: Обучим контроллер нотной грамоте

Т.к. я несколько лет учился в музыкальной школе, просто пищание на какой-то частоте меня мало устраивает. Во-первых, выберем круглую частоту дискретизации. Учитывая, что частоты более 5 кГц я использовать не планирую, достаточно частоты дискретизации в 10 кГц. Вносим настройки в таймеры:
TIM6->PSC = 23; // Это 1 MГц
TIM6->ARR = 99; // При PSC 23 и ARR 99 прерывание каждые 100 мкс (10 кГц)
Теперь берём частоту ноты Ля, например, Субконтроктавы — 27,5 Гц. Ля выбрана потому, что частота этой ноты является целым числом и считается эталоном (440 Гц Первой октавы, обычно камертоны вибрируют с этой частотой), от неё по формуле считаются остальные 11 нот (с учётом диезов). Частоты нот соседних октав отличаются друг от друга ровно в два раза (подставляем в формулу 12 вместо n и убеждаемся в этом).
- Несколько октав, в каждой по 12 нот (вместе с диезами).
- Для каждой ноты расчитывается частота по формуле исходя из базовой (Ля — 27,5 Гц) и количества ступеней до неё (Ля# — это +1 ступень, Си — +2 ступени, Ля следующей октавы — +12 ступеней).
- Получаем длины периодов в секундах путём деления единицы на частоту, результат делим на 10 000 Гц, округляем и получаем количество элементов в массиве для данной ноты при частоте 10 000 Гц.
- Высчитываем массив значений синуса конкретной ноты с учётом периода этой ноты.
- Нормализуем в диапазон от 0 до 4095, округляем.
Первым элементом массива я сделал количество элементов в нём же. Результат:
//Нота До const uint16_t noteC[]=< 76,2216,2383,2547,2709,2865,3017,3162,3299,3428,3547,3657,3755,3842,3917, 3979,4028,4064,4086,4095,4090,4070,4038,3991,3932,3860,3775,3679,3572, 3455,3328,3193,3050,2900,2744,2584,2419,2253,2085,1917,1749,1584,1422, 1264,1111,965,826,695,574,462,361,271,194,129,76,38,12,1,3,19,49,92,149, 218,300,393,498,613,738,871,1013,1161,1316,1475,1639,1805,1973 >;
Остальные ноты
//Нота До диез const uint16_t noteCd[]=< 72,2226,2402,2576,2746,2911,3069,3220,3361,3493,3614,3722,3818,3901,3970, 4024,4063,4087,4095,4088,4065,4027,3974,3907,3825,3730,3622,3503,3372, 3231,3081,2924,2759,2590,2416,2239,2061,1883,1706,1532,1362,1196,1038, 887,744,612,490,381,284,200,130,75,35,10,0,6,27,64,116,182,263,357,464, 583,713,853,1002,1159,1323,1492,1666,1842,2020 >; const uint16_t noteD[]=< 68,2236,2423,2607,2786,2959,3124,3280,3425,3559,3680,3787,3879,3955,4016, 4059,4086,4095,4087,4061,4018,3959,3883,3792,3686,3566,3432,3288,3132, 2968,2795,2617,2433,2246,2057,1869,1682,1498,1318,1145,980,823,677,543, 421,314,221,143,82,38,10,0,7,32,74,133,207,298,403,523,655,799,954,1118, 1291,1469,1652,1839,2028 >; const uint16_t noteDd[]=< 63,2247,2445,2639,2828,3009,3181,3342,3490,3625,3745,3849,3935,4003, 4053,4084,4095,4087,4059,4012,3946,3862,3761,3643,3510,3363,3204, 3034,2854,2666,2473,2275,2076,1876,1678,1483,1293,1111,938,775, 625,488,366,260,171,100,48,14,0,6,31,76,139,220,319,434,565,710, 868,1037,1215,1402,1594,1792,1991 >; //Нота Ми const uint16_t noteE[]=< 60,2259,2469,2673,2872,3061,3239,3405,3556,3691,3809,3907,3986,4044,4080, 4095,4087,4058,4008,3936,3844,3733,3604,3458,3297,3122,2937,2741,2538, 2330,2119,1907,1697,1490,1289,1097,914,744,588,447,324,219,133,69,25, 3,3,24,68,133,218,323,446,586,742,912,1095,1287,1488,1694,1905 >; const uint16_t noteF[]=< 57,2272,2493,2709,2917,3115,3300,3470,3623,3756,3870,3961,4029,4074, 4094,4089,4060,4007,3930,3831,3710,3569,3410,3234,3044,2842,2631, 2413,2190,1965,1742,1522,1309,1104,911,731,567,421,295,189,106,47, 11,0,14,51,113,199,306,434,582,748,929,1123,1329,1543,1763,1987 >; const uint16_t noteFd[]=< 54,2285,2519,2747,2966,3172,3362,3536,3689,3820,3927,4008, 4063,4091,4092,4065,4010,3929,3823,3693,3540,3367,3177, 2971,2753,2526,2291,2054,1817,1582,1354,1135,929,738,564, 410,279,171,89,33,4,3,29,83,163,269,399,550,723,912,1118,1336,1563,1797,2034 >; const uint16_t noteG[]=< 51,2299,2547,2787,3016,3230,3426,3602,3754,3880,3978,4047,4086, 4094,4071,4017,3934,3822,3683,3519,3333,3127,2905,2670,2426,2176, 1924,1674,1430,1194,972,766,580,416,276,163,79,25,1,8,47,115,213, 338,490,665,861,1075,1303,1543,1791,2042 >; const uint16_t noteGd[]=< 48,2314,2576,2829,3068,3290,3492,3668,3817,3936,4023,4076, 4095,4079,4028,3944,3828,3681,3506,3307,3086,2848,2596, 2335,2069,1802,1540,1286,1045,821,618,440,288,167,78,22, 0,14,61,143,257,401,574,771,991,1228,1479,1739,2005 >; const uint16_t noteA[]=< 45,2330,2606,2872,3123,3353,3558,3734,3878,3987,4059,4092, 4087,4043,3961,3842,3689,3504,3292,3056,2801,2532,2253, 1970,1689,1415,1153,907,684,486,319,184,85,23,0,16,71, 163,292,454,646,865,1107,1366,1639,1919 >; const uint16_t noteAd[]=< 42,2346,2639,2918,3179,3416,3624,3798,3934,4030,4083,4093, 4059,3982,3864,3707,3514,3290,3039,2767,2480,2183,1883, 1587,1301,1031,782,561,371,218,103,30,1,15,72,172,313, 490,700,940,1203,1484,1777 >; const uint16_t noteH[]=< 40,2364,2673,2967,3238,3481,3690,3859,3985,4064,4095,4076, 4009,3894,3736,3536,3301,3036,2747,2441,2126,1809,1498,1199, 922,671,453,274,137,46,3,10,65,168,316,506,732,991,1274,1577,1890 >; const uint16_t noteP[]=< // Музыкальная пауза 1,2048 >;
Теперь функция, возращающая очередное значение передаваемой ей ноты с поправкой на октаву (указывать надо 1, 2, 4, 8 и т.д.):
uint16_t mnote(const uint16_t *pa,char octave) // Возвращаем значение амплитуды ноты для данного момента времени < int ii=0; do while (ii!=i); ii=(octave*ii)%pa[0]; ii=*(pa+ii); return ii; >
Переменная i увеличивается каждые 100 микросекунд с помощью прерывания:
/*Обработчик прерывания от таймера 6 */ void TIM6_DAC_IRQHandler(void) < i++; TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF >
В общем, самое время написать мелодию. Почти как в старые добрые времена, в синтезаторе мелодий на Nokia:
uint16_t *melody[]=< noteE,noteP,noteG,noteP, noteA,noteA,noteP,noteE, noteP,noteG,noteP,noteAd, noteA,noteA,noteP,noteP, noteE,noteP,noteG,noteP, noteA,noteA,noteP,noteG, noteP,noteE,noteP,noteP, noteP,noteP,noteP,noteP >;
Интересно, как быстро в комментариях появится название культовой песни.
Чтобы образовать трезвучие (аккорд), добавим ещё две мелодии, с другими нотами:
uint16_t *melody2[]=< noteH,noteP,noteD,noteP, noteE,noteE,noteP,noteH, noteP,noteD,noteP,noteF, noteE,noteE,noteP,noteP, noteH,noteP,noteD,noteP, noteE,noteE,noteP,noteD, noteP,noteH,noteP,noteP, noteP,noteP,noteP,noteP >; uint16_t *melody3[]=< noteGd,noteP,noteH,noteP, noteCd,noteCd,noteP,noteGd, noteP,noteH,noteP,noteD, noteCd,noteCd,noteP,noteP, noteGd,noteP,noteH,noteP, noteCd,noteCd,noteP,noteH, noteP,noteGd,noteP,noteP, noteP,noteP,noteP,noteP >;
Для одновременного воспроизведения сразу нескольких нот просто складываем значения их текущих амлитуд (из массивов). Но! ЦАП принимает значения от 0 до 4095, и элементы массивов нормализованы в этот же диапазон. Если нам не нужен эффект овердрайва, просто делим получившуюся сумму на количество звучащих нот, результат скармливаем ЦАПу. И так каждые 100 микросекунд…
Пора всё это воспроизвести.
Весь получившийся код
#include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "stm32f10x_gpio.h" int i=0; uint16_t current=0; int loccurrent=0; uint16_t mnote(const uint16_t *pa,char octave) // Возвращаем значение амплитуды ноты для данного момента времени < int ii=0; do while (ii!=i); ii=(octave*ii)%pa[0]; ii=*(pa+ii); return ii; > //Значения синуса для нот через каждые 100 микросекунд, от 0 до 4095 //Нота До const uint16_t noteC[]=< 76,2216,2383,2547,2709,2865,3017,3162,3299,3428,3547,3657,3755,3842,3917,3979,4028,4064,4086,4095,4090,4070,4038,3991,3932,3860,3775,3679,3572,3455,3328,3193,3050,2900,2744,2584,2419,2253,2085,1917,1749,1584,1422,1264,1111,965,826,695,574,462,361,271,194,129,76,38,12,1,3,19,49,92,149,218,300,393,498,613,738,871,1013,1161,1316,1475,1639,1805,1973 >; //Нота До диез const uint16_t noteCd[]=< 72,2226,2402,2576,2746,2911,3069,3220,3361,3493,3614,3722,3818,3901,3970,4024,4063,4087,4095,4088,4065,4027,3974,3907,3825,3730,3622,3503,3372,3231,3081,2924,2759,2590,2416,2239,2061,1883,1706,1532,1362,1196,1038,887,744,612,490,381,284,200,130,75,35,10,0,6,27,64,116,182,263,357,464,583,713,853,1002,1159,1323,1492,1666,1842,2020 >; const uint16_t noteD[]=< 68,2236,2423,2607,2786,2959,3124,3280,3425,3559,3680,3787,3879,3955,4016,4059,4086,4095,4087,4061,4018,3959,3883,3792,3686,3566,3432,3288,3132,2968,2795,2617,2433,2246,2057,1869,1682,1498,1318,1145,980,823,677,543,421,314,221,143,82,38,10,0,7,32,74,133,207,298,403,523,655,799,954,1118,1291,1469,1652,1839,2028 >; const uint16_t noteDd[]=< 63,2247,2445,2639,2828,3009,3181,3342,3490,3625,3745,3849,3935,4003,4053,4084,4095,4087,4059,4012,3946,3862,3761,3643,3510,3363,3204,3034,2854,2666,2473,2275,2076,1876,1678,1483,1293,1111,938,775,625,488,366,260,171,100,48,14,0,6,31,76,139,220,319,434,565,710,868,1037,1215,1402,1594,1792,1991 >; //Нота Ми const uint16_t noteE[]=< 60,2259,2469,2673,2872,3061,3239,3405,3556,3691,3809,3907,3986,4044,4080,4095,4087,4058,4008,3936,3844,3733,3604,3458,3297,3122,2937,2741,2538,2330,2119,1907,1697,1490,1289,1097,914,744,588,447,324,219,133,69,25,3,3,24,68,133,218,323,446,586,742,912,1095,1287,1488,1694,1905 >; const uint16_t noteF[]=< 57,2272,2493,2709,2917,3115,3300,3470,3623,3756,3870,3961,4029,4074,4094,4089,4060,4007,3930,3831,3710,3569,3410,3234,3044,2842,2631,2413,2190,1965,1742,1522,1309,1104,911,731,567,421,295,189,106,47,11,0,14,51,113,199,306,434,582,748,929,1123,1329,1543,1763,1987 >; const uint16_t noteFd[]=< 54,2285,2519,2747,2966,3172,3362,3536,3689,3820,3927,4008,4063,4091,4092,4065,4010,3929,3823,3693,3540,3367,3177,2971,2753,2526,2291,2054,1817,1582,1354,1135,929,738,564,410,279,171,89,33,4,3,29,83,163,269,399,550,723,912,1118,1336,1563,1797,2034 >; const uint16_t noteG[]=< 51,2299,2547,2787,3016,3230,3426,3602,3754,3880,3978,4047,4086,4094,4071,4017,3934,3822,3683,3519,3333,3127,2905,2670,2426,2176,1924,1674,1430,1194,972,766,580,416,276,163,79,25,1,8,47,115,213,338,490,665,861,1075,1303,1543,1791,2042 >; const uint16_t noteGd[]=< 48,2314,2576,2829,3068,3290,3492,3668,3817,3936,4023,4076,4095,4079,4028,3944,3828,3681,3506,3307,3086,2848,2596,2335,2069,1802,1540,1286,1045,821,618,440,288,167,78,22,0,14,61,143,257,401,574,771,991,1228,1479,1739,2005 >; const uint16_t noteA[]=< 45,2330,2606,2872,3123,3353,3558,3734,3878,3987,4059,4092,4087,4043,3961,3842,3689,3504,3292,3056,2801,2532,2253,1970,1689,1415,1153,907,684,486,319,184,85,23,0,16,71,163,292,454,646,865,1107,1366,1639,1919 >; const uint16_t noteAd[]=< 42,2346,2639,2918,3179,3416,3624,3798,3934,4030,4083,4093,4059,3982,3864,3707,3514,3290,3039,2767,2480,2183,1883,1587,1301,1031,782,561,371,218,103,30,1,15,72,172,313,490,700,940,1203,1484,1777 >; const uint16_t noteH[]=< 40,2364,2673,2967,3238,3481,3690,3859,3985,4064,4095,4076,4009,3894,3736,3536,3301,3036,2747,2441,2126,1809,1498,1199,922,671,453,274,137,46,3,10,65,168,316,506,732,991,1274,1577,1890 >; const uint16_t noteP[]=< 1,2048 >; //Мелодия, основной тон uint16_t *melody[]=< noteE,noteP,noteG,noteP, noteA,noteA,noteP,noteE, noteP,noteG,noteP,noteAd, noteA,noteA,noteP,noteP, noteE,noteP,noteG,noteP, noteA,noteA,noteP,noteG, noteP,noteE,noteP,noteP, noteP,noteP,noteP,noteP >; //Чистая квинта uint16_t *melody2[]=< noteH,noteP,noteD,noteP, noteE,noteE,noteP,noteH, noteP,noteD,noteP,noteF, noteE,noteE,noteP,noteP, noteH,noteP,noteD,noteP, noteE,noteE,noteP,noteD, noteP,noteH,noteP,noteP, noteP,noteP,noteP,noteP >; //Большая терция uint16_t *melody3[]=< noteGd,noteP,noteH,noteP, noteCd,noteCd,noteP,noteGd, noteP,noteH,noteP,noteD, noteCd,noteCd,noteP,noteP, noteGd,noteP,noteH,noteP, noteCd,noteCd,noteP,noteH, noteP,noteGd,noteP,noteP, noteP,noteP,noteP,noteP >; int main(void) < GPIO_InitTypeDef PORT; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , 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); /* Включаем порт А */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* Включаем ЦАП */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); /* Включаем таймер 6 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE); /* Настраиваем ногу ЦАПа */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM6->PSC = 23; //При 24000-1 будет 1000Гц TIM6->ARR = 99; //При PSC 23 и ARR 99 прерывание каждые 100 мкс TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт! NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания /* Включить DAC1 */ DAC->CR |= DAC_CR_EN1; /* Бесконечный цикл */ while (1) < GPIOC->BRR=GPIO_BRR_BR8; GPIOC->BSRR=GPIO_BSRR_BS9; do < loccurrent=i; >while (i!=loccurrent); //Безопасное получение переменной, используемой в прерываниях GPIOC->BRR=GPIO_BRR_BR9; GPIOC->BSRR=GPIO_BSRR_BS8; current=(loccurrent/(2000))%32; //Текущая нота из мелодии, от 0 до 31. DAC->DHR12R1=(mnote(melody[current],1)+mnote(melody2[current],1)+mnote(melody[current],2)+mnote(melody2[current],2)+mnote(melody[current],4)+mnote(melody3[current],2))/6; > > /*Обработчик прерывания от таймера 6 */ void TIM6_DAC_IRQHandler(void) < i++; TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF >
Возникает логичный вопрос, можно ли вместо одного периода синусоиды записать в массив какой-нибудь замечательный звук? Да.
Задача 13: Воспроизведение звуков из файла
Сразу говорю, есть другие, более разумные варианты озвучки STM32VLDiscovery, но мне не захотелось их осваивать. Мой алгоритм:
1. Ищем на Youtube звук запуска двигателя Aston Martin.
2. Записываем (или скачиваем) этот звук.
3. Открываем файл в аудиоредакторе (я использовал бесплатный Audacity).
4. Выбираем минимально приемлемую частоту дискретизации. Я выбрал 24 кГц. Учитывая, что каждый сэмпл занимает в памяти контроллера 2 байта, мы можем записать всего 2-3 секунды моно-звука. В самый раз.
5. Сохраняем коротенький звуковой WAV-файл.
6. Преобразуем с помощью специальной программы (вроде WAV 2 TEXT) — да-да, звук в текст. Вернее, в таблицу значений амлитуд.
7. Нормализуем в значения от 0 до 4095 с помощью Excel. Все 60 000 значений копируем в Notepad++ или Sublime Text, массово меняем табуляции на запятые. Также в Excel я вставлял каждые 30 строк букву, чтобы потом все их заменить на перенос строки — CoIDE не любит длинные строки…
8. Сохраняем значения в массив.
Пожалуй, over 9000 чисел здесь приводить не буду. Предлагаю видео:
В том случае, если вы просто перемотали топик до конца и не видели, как, собственно, машинка ездит, повторяю видео:
Микроконтроллер stm32 где используется


| Теги статьи: | Добавить тег |

STM32F10x 32bit Cortex-M3 – ну очень быстрый старт или 10 простых примеров Автор: Aheir
Опубликовано 14.02.2012
Создано при помощи КотоРед.
В последние пару лет микроконтроллеры на базе ядер ARM-семейства активно начали появляться не только в промышленных, но и во вполне себе любительских изделиях. Далеко не последнее место среди них занимают микроконтроллеры производства STMicroelectronics, о которых и пойдет речь ниже. Причин для этого немало, судите сами: неплохая обеспеченность документацией, доступность средств разработки и отладки, вполне себе паябельные корпуса, более чем демократичная цена. А уж после того, как некоторое время назад была организована фактически бесплатная раздача отладочных комплектов STM32VLDiscovery, популярность этих микроконтроллеров стала весьма и весьма значительной. Не вдаваясь глубоко в подробности (желающие без труда найдут дополнительную информацию), скажу лишь, что семейство микроконтроллеров STM32F10х построено на базе унифицированного ядра Cortex-M3 (это значит, что микроконтроллеры других производителей в части, касающейся ядра, совместимы между собой), обладает 32-разрядной архитектурой и может тактироваться с частотой до 72МГц. Различные микроконтроллеры семейства существенно отличаются по количеству набортной памяти и периферийных устройств, что позволяет подобрать МК, наиболее полно соответствующий поставленной задаче, а тот факт, что чипы в однотипных корпусах разных линеек совместимы по выводам, в случае необходимости позволяет повысить вычислительную производительность устройства без переработки печатной платы. Короче говоря, мне тоже стало интересно, что это за звери такие, так что вот… Для начала работы я приобрел уже упомянутую STM32VLDiscovery (цена в розницу порядка 400 руб). На борту – микроконтроллер STM32F100RBT6B в 64-контактном корпусе cо 128кБ флеш-памяти и стандартным набором коммуникационных интерфейсов. Все выводы доступны на контактной гребенке, есть пользовательская кнопка и пара светодиодов. Кроме того, и это очень выручает, на плату интегрирован USB программатор-отладчик ST-LINK для STM32, который позволяет работать не только с установленным на плату контроллером, но и с любым другим: сигналы для программирования также доступны через разъем. Для начала работы вполне достаточно. С точки зрения программного обеспечения я остановился на следующем наборе:
- Keil uVision посвежее для ARM
- утилита STM32 ST-LINK Utility для программирования микроконтроллера
Вообще, среди IDE для ARM вообще и STM32 в частности выбор имеется, так что вполне возможно для кого-то он будет иным.
Кроме того, для комфортной работы неплохо бы обзавестись набором из Cortex Microcontroller Software Interface Standard (CMSIS) и STM32F10x_StdPeriph_Driver (набор драйверов периферии STM32). CMSIS – библиотека ядра Cortex, как следует из унифицированности ядра – единая для всех МК на его основе. С набором драйверов все и так понятно. И то, и другое, доступно для скачивания с сайта производителя и содержится в архиве с обозначением AN3268: STM32VLDISCOVERY firmware package. Кроме того, полезным будет STM32_Init – набор для быстрого конфигурирования периферии (ту версию, которую я использую, можно скачать в конце статьи)
Вобщем, Keil и прошивальщик установили, исходников накачали, теперь надо со всем этим попытаться взлететь)
Сначала создаем где-нибудь папку под наши эксперименты, в ней подпапку под конкретный проект, формируем такую структуру (названия папок произвольны, но так хотя бы понятно, что где лежит, папку output создавать необязательно, сама сгенерится):
Теперь разархивируем STM32VLDISCOVERY firmware package и раскладываем файлы по папкам.
В CMSIS кладем «core_cm3.h» и «core_cm3.c» из «/stm32vldiscovery_package/Libraries/CMSIS/CM3/CoreSupport/»
и «system_stm32f10x.h» и «system_stm32f10x.c» из
В config — «stm32f10x.h» из «stm32vldiscovery_package/Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/»
и «stm32f10x_conf.h» из любого проекта в «stm32vldiscovery_package/Project/Examples/».
В startup – все файлы из «stm32vldiscovery_package/Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/arm/».
В std_periph_lib – все файлы из
В STM32_Init – содержимое скачанного архива.
В корне папки «source» создаем файл «main.c».
В корневой папке проекта создаем файл «flash.bat» следующего содержания:
«…/ST-LINK_CLI.exe» -c SWD -ME -P «output/keil_project.hex» 0x08000000 -Rst –Run
где … путь к директории установки STM32 ST-LINK Utility (говоря по простому, это батник для запуска прошивальщика)
Уже готовый проект можно взять в конце статьи, так что потребуется только исправить путь к прошивальщику в батнике.
Теперь нужно настроить Keil. После запуска программы, выбираем «Проект-Создать новый», сохраняем файл проекта с приятным слуху именем в корневую папку проекта.
Далее Keil предложит выбрать целевой микроконтроллер, укажем то, что установлено на нашей плате:
Справа можно почитать кратенькую характеристику микроконтроллера.
Говорим ОК, Keil предлагает скопировать свой startup для выбранного микроконтроллера в папку проекта, отказываемся.
Создается структура проекта, пока что пустая.
Путем прямых и контекстных кликов на членах структуры проекта, выбирая пункты подменю «добавить группу» и «Добавить файлы в группу» добиваемся того, чтобы структура приобрела следующий вид:
Т.е. мы добавили в проект все файлы, распределенные по папкам ранее. Вообще, можно было не копировать вручную файлы по разным папкам, а использовать уже сформированную структуру каталогов из stm32vldiscovery_package. Однако лично мне так понятнее, что с чем взаимодействует и я точно знаю, что где лежит. Не претендую на истину, просто мне так удобнее.
Теперь идем в настройки проекта: через меню Проект-Опции для Project или просто Alt+F7. Device и Target просматриваем, но не трогаем. На вкладке Output жмакаем «Select Folder for Objects», в качестве целевой выбираем папку output в корне проекта (если еще не создали – создаем). Имя исполняемого файла – keil_project или любое другое. Ставим галку Create HEX File – пусть будет.
Если хотим получить еще и bin-файл (а это моет быть полезно при использовании сторонних программ-прошивальщиков, бутлоадеров или хотя бы просто для наглядного представления размера прошивки), на вкладке User заполняем строку:
Это запустит встроенную утилиту преобразования. Имя bin может быть любым (мы же его создаем), а вот axf должен назваться так же, как исполняемый файл на вкладке Output.
На вкладке С/С++ жмакаем на Include Paths и заполняем в появившемся окне пути, по которым компилятор будет искать все требуемые файлы.
Должно получиться что-то такое:
В итоге строка на вкладке заполнится этими же путями, перечисленными через точку с запятой:
На вкладке Debug оставляем все как есть, или выбираем в правой части окна ST-Link в качестве отладчика.
На последней вкладке делаем следующие настройки:
Тем самым при запросе на прошивку микроконтроллера мы будем вызывать STM32 ST-LINK Utility через батник, созданный ранее. Поясню, зачем все это надо. Если использовать встроенный в Keil прошивальщик, то в силу неких внутренних глюков особенностей, прошивка происходит только при запуске отладки через Debug-Start Session; при Flash-Download прошивки не происходит. Это весьма неудобно. Компромиссное решение и предложено выше.
Если теперь открыть в редакторе main.c и добавить туда что-то вроде «int main(void) <>», то проект вполне себе скомпилируется по команде «Project-Rebuild all target files».
На этом первичная настройка закончена, эту «рыбу» можно использовать далее при разработке приложений.
В процессе изучения STM32 я столкнулся с тем, что ответы на казалось бы совершенно простые вопросы, даже в условиях имеющихся вроде как примеров и т.д., приходилось искать самостоятельно. Это и хорошо, и неудобно одновременно. А говорю я это к тому, что далее следуют 10 проектов-примеров, реализующих какие-либо простые операции на микроконтроллере, «заточенные» и опробованные на STM32VLDiscovery, сделанные мной (как правило, с использованием каких-то примеров) и отвечающие на те вопросы, которые возникали у меня. Возможно, и даже скорее всего, у вас будут другие вопросы и эти проекты покажутся вам кривыми, неоптимальными, прививающими плохие привычки в программировании и т.д. Возможно, это и так, но мне они в чем-то помогли, чего и вам желаю.
Проект 1 – помигаем светодиодом.
Скачиваем архив проекта. Далее либо открываем имеющийся проект, либо копируем в main.c нашего шаблона все содержимое того же файла из скачанного архива. std_periph_lib мы пока пользоваться не будем, поэтому соответствующую группу можно просто удалить. Открываем файл stm32f10x.h из группы config, убеждаемся, что раскомментирован дефайн «STM32F10X_MD_VL»:
Этот дефайн задает линейку контроллера (читай – объемы памяти), с которым мы собираемся работать. Чуть ниже в этом же файле можно почитать, что это за «MD_VL» такой, какие еще бывают и чем отличаются. В данном проекте для нас не особо важна принадлежность МК к какой либо линейке, но лучше сразу привыкнуть отслеживать этот момент.
Смотрим в исходник.
Всего имеется 3 функции.
InitAll – инициализация порта PORTC.8 как выхода (к этой ножке на STM32VLDiscovery прицеплен синий светодиод)
Delay – задержка, основанная на пустых циклах
main – проводит инициализацию и затем в цикле меняет состояние ножки со светодиодом.
В ходе инициализации стоит обратить внимание на строку RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; Это – включение тактирования порта. Вообще, в STM работа почти с любой периферией начинается с включения тактирования. Логика понятна: если периферия не требуется, зачем тратить энергию на ее работу? Поэтому большинство узлов по умолчанию отключено от источников тактового сигнала. Далее идет запись в управляющий регистр порта ввода/вывода. Смысл операций рекомендую исследовать самостоятельно, описание определений можно подсмотреть в том же stm32f10x.h, а описание регистров в объемном документе «RM0041 STM32F100xx Reference Manual» — более 600 страниц описания микроконтроллера – доступен на сайте STMicroelectronics. Вобщем, даже со стороны документации ни разу не AVR… Там-то даташита вполне достаточно, а здесь в даташите можно посмотреть разве что распиновку да электрические параметры. Большая часть остальной необходимой информации в уже упомянутом документе, а так же куче других, не менее объемных.
Теперь можем нажать «Project-Rebuild all target files» и, если все сделано верно, в нижней части экрана увидим лог работы компилятора:
Ошибок не обнаружилось, можно прошить контроллер. Считаем, что Discovery к компу уже подключена (каких-либо настроек или драйверов она не требует), поэтому просто жмем «Flash-Download». Процесс прошивки много времени не займет:
К этому времени плата уже во всю будет мигать синим светодиодом…
Пример 2 – помигаем светодиодом с помощью StdPeriph_Lib
Как уже отмечалось, StdPeriph_Lib – набор драйверов для работы с периферией STM32. Удобно тем, что все управление осуществляется вызовом функций, более того, различная периферия имеет схожие по принципам построения и структуре названий функции, так что все это достаточно несложно запоминается. Минусы стандартные для такого подхода: дополнительные потери на вызовы функций, отсутствие прозрачности процесса настройки (чего оно там куда пишет, в какие регистры – неизвестно) и т.д. Тем не менее – весьма эффективно.
Открываем проект, смотрим на структуру:
std_periph_lib уже не пустая. Какие файлы за что там отвечают понятно по названиям, кроме того, стоит ознакомиться с содержимым h-файлов, многое станет понятнее. Для совсем глубокого понимания изучаем еще и с-файлы. Чтобы начать их использовать, в файле stm32f10x_conf.h нужно раскомментировать соответствующие строки:
Вообще, неиспользуемые файлы из этого набора лучше удалять из структуры проекта, в противном случае компилятор при сборке все равно ворошит их, что заметно замедляет процесс вцелом.
В самом проекте изменилась функция инициализации (теперь конфигурирование порта заключается в присвоении значений соответствующим полям структуры, описывающей единицу периферии – такой подход в рамках StdPeriph_Lib применяется для работы с любой периферией), в основном цикле сброс/установка порта так же происходит с помощью функции, описанной в stm32f10x_gpio.h
Компилируем, прошиваем, убеждаемся, что теперь на плате перемигиваются два светодиода, зеленый и синий.
Пример 3 – чем нам поможет STM32_Init
Как уже отмечалось, STM32_Init – набор для быстрого конфигурирования микроконтроллера.
Открываем проект, видим в структуре группу STM32_Init. Если открыть STM32_Init.c, в левом нижнем углу поля текстового редактора можно увидеть следующую картину:
Если ткнуть на Configuration Wizard, увидим следующее:
Думаю, комментарии излишни?
В нашем примере, настраиваем на выход порты С 8 и 9. Постановка галочек изменяет содержимое файла STM32_Init.c, в котором через директивы условной компиляции и прочие ухищрения происходит настройка периферии процессора. Для использования всего этого в проекте, нужно вызвать функцию STM32_Init (), что и делается внутри уже нам знакомой по предыдущим примерам InitAll(). В остальном ничего не изменилось, компилируем, прошиваем, убеждаемся, что таки да, визуально все работает совершенно так же…
Я этот механизм использовал в том числе и так: что-нибудь настроил, проверил, что работает, потом снял галочки и настроил все то же самое вручную. Работает? Прекрасно, значит, разобрался еще с кусочком.
Пример 4 – системный таймер
Системный таймер – часть стандартизованного ядра Cortex, т.е. есть у всех Cortex-процессоров. Особо гибкими настройками не обладает, но тем не менее может использоваться для отсчета временных интервалов для RTOS или иных задач.
Здесь мы помигаем светодиодом с использованием системного таймера для генерации задержек.
Открываем проект, в Визарде STM32_Init видим такие настройки:
Поскольку период счета настроен равным 1мс, весьма удобно сделать отдельную функцию задержки типа delay_ms(unsigned int).
Именно это и реализовано в проекте.
Функция-обработчик прерывания должна называться SysTick_Handler (). Вообще, все эти названия можно подсмотреть в startup_stm32f10x_md_vl.s Компилируем, прошиваем, видим, что частота перемигивания существенно возросла.
Пример 5 – таймер общего назначения.
В STM32 довольно много таймеров общего назначения. Исходно они 16-ти битные, однако можно включать их последовательно для образования 32-разрядного счетчика, имеется возможность генерации аппаратного ШИМ, входы захвата и т.д., и т.п. Настроек много, режимов работы тоже – нужно изучать мануал и сопутствующую документацию.
Мы пока ограничимся прерыванием и реализацией программного ШИМа для управления яркостью светодиода.
Открываем проект, в STM32_Init сделаны следующие настройки:
Период счета таймера (используем таймер 2) 40 мкс, разрешено прерывание по обновлению (т.е. каждые 40 мкс). Функция-обработчик — TIM2_IRQHandler ().
В самом начале обработчика прерывания происходит проверка причины, по которой мы там, собственно, оказались, и сброс соответствующего флага. Здесь это сделано с помощью непосредственной записи в регистры, но можно использовать и функции StdPeriph_Lib. Стоит взять за правило сразу после входа в прерывание обрабатывать его флаги. В противном случае, в ряде ситуаций, несброшенный вовремя флаг микроконтроллер, даже находясь внутри прерывания, может расценить как возникновение еще одной исключительной ситуации, в результате чего после выхода из прерывания мы сразу же влетим туда еще раз. Так что, во избежание…
В основном цикле с помощью флага, выставляемого в прерывании, каждые 200 мс изменяется состояние зеленого светодиода, кроме того, каждые 5 мс происходит изменение значения яркости синего светодиода, что в сочетании с 8-битным счетчиком дает 256*5 = 1280 мс период его плавного угасания.
Компилируем, прошиваем, наслаждаемся…
Пример 6 – USART
Здесь освоим простую отправку единичных байт информации через USART микроконтроллера. Чем мне понравились STM, так это наличием как минимум двух USART даже на младших контроллерах. Весьма часто лично мне такое требуется, весьма…
Открываем проект, USART настроен так:
Поскольку собрались использовать новую периферию, не забываем подтаскивать нужные файлы в std_periph_lib и раскомментировать соответствующие строки в stm32f10x_conf.h
В основном цикле программы добавлена строка, которая синхронно с изменением состояния зеленого светодиода производит отправку абстрактных данных в UART1.
Подцепив это к COM-порту компьютера (с использование соответствующего согласования на MAX232 или USB-RS232 преобразователя, что гораздо удобнее) в терминале получим следующую картину:
Пример 7 – прерывание по приему USART
Скачиваем проект. В настройках разрешены прерывания для USART1 по приему символа и по ошибке приема.
В случае успешного приема сохраняем принятые данные и мигаем зеленым светодиодом, в случае ошибки (ситуацию можно сэмулировать, например, установив неверную скорость порта на компе) мигаем светодиодом несколько раз.
Кроме того, подцеплены функции форматированного вывода. Для этого к проекту добавлен файл retarget.c (есть в составе Keil’a), который изменен так, чтобы отправка единичного символа осуществлялась через функцию sendchar.
В зависимости от принятых данных система обеспечивает различный отклик:
Пример 8 – декодер RC5
В принципе, среди аппнотов STM32 есть реализация RC5 декодера, но лично у меня с наскока она не заработала (да и не только у меня), так что вернусь к ней позднее, а пока просто адаптируем исходник из статьи «Некоторые протоколы ИК-пультов. Часть 2» . Там можно ознакомиться с принципами построения и алгоритмом работы этого декодера. Писано было под AVR, ну дык на Си же, так что какие проблемы?
Используем более тонкую настройку таймера:
Предделитель – 768, что при тактировании от частоты 24МГц (а это именно так, поскольку умолчальные настройки системы тактирования (внешний кварц на 8МГц * 4) мы в наших проектах не трогали) дает частоту счета таймера 24МГц / 768 = 31250Гц – как и в исходнике.
Ограничив таймер значением счета в 256 получаем практически 8-битный счетчик – аналог таймера AVR.
Далее все сводится к включению внешнего прерывания:
Весьма радует, кстати, что можно назначить прерывание на любую ногу микроконтроллера. Их всего (или целых?) 16, и номер пина должен быть уникальным (т.е. одновременно использовать С.9 и В.9 как входы прерывания, насколько я понимаю, не получится), но при должном подходе к проектированию это не проблема. Нога В.9 толерантна к 5В уровню, поэтому выход TSOP1736 подключаем к ней напрямую.
Ну а дальше все сводится к использованию функций для работы с таймером, типа старт/стоп счета, считывание/установка значения.
Результат выводится опять же через USART:
Timer Overflow – ложные срабатывания фотоприемника (внешние помехи).
Пример 9 – эмуляция ЕЕПРОМ
При всем богатстве периферии STM32F100RBT6B, ЕЕПРОМ в нем, как впрочем и во всех STM32F10х, отсутствует. Производитель мотивирует это тем, что в противном случае не удалось бы поддерживать настолько низкую цену. Впрочем, сознавая полезность энергонезависимой памяти, STMicroelectronics предлагает воспользоваться документом AN2594 — EEPROM Emulation. В нем описывается способ записи во флеш-память микроконтроллера произвольных данных пользователя непосредственно из программы. Flash имеет свои плюсы и минусы при таком использовании, однако если писать нужно не сильно часто, а вопрос цены изделия приоритетен, это вполне оправданное решение. Производитель предлагает тестовый проект с исходниками, однако пример в нем не очень интересен. Я взял из него файлы eeprom.c и eeprom.h (собственно это и есть драйвер для записи во flash) и сделал свою демку на их основе.
Как всегда скачиваем, компилируем, прошиваем проект.
При каждом включении проверяется содержимое ячейки памяти по указанному адресу. Если оно не совпадает с определенным значением, это значение туда записывается и программа останавливается. Если совпадает – проверяется вторая ячейка, если и она совпадает – третья. Так в цикле по трем ячейкам; если все 3 заполнены нужными значениями, они стираются и начинается новый цикл. Вся информация выводится в терминал:
Один момент. Для работы с flash должен быть включен внутренний тактовый генератор. Этот момент описан в PM0075 STM32F10xxx Flash memory microcontrollers.
Микроконтроллер, который мы на данный момент мучаем, имеет на борту два аппаратных I2C интерфейса. Вообще, к I2C на STM32 масса претензий, вплоть до полной неработоспособности у некоторых разработчиков и отказа от аппаратного модуля в пользу программной эмуляции интерфейса. Конечно, многое зависит от конкретного разработчика, но дыма без огня, как известно, почти не бывает, а нареканий в И-нете встречается ну уж больно много… Как бы там ни было, у меня после небольшого допиливания вполне пристойно заработал пример от уважаемого коллеги GYUR22 отсюда . Результаты приведены в примере в папке «010.1 Discovery Hard I2C». Я подключал EEPROM AT24C256-10PI, сделал простейший счетчик стартов микроконтроллера. При старте микроконтроллер читает значение байта по нулевому адресу, увеличивает на 1, записывает полученное значение по нулевому адресу и по адресу, равному полученному значению. Т.е. при первом старте содержимое первых 256 байт EEPROM выглядит так:
После нескольких включений:
После 11 включений происходит сброс и перезапись EEPROM значениями по умолчанию.
0x77 в последнем байте – просто контрольное значение.
Позволю себе заметить, что упомянутая в статье по ссылке выше необходимость задержки после акта записи в ЕЕПРОМ вполне укладывается в указанные в даташите тайминги: время цикла записи при питании от 2.7 до 5 В до 10 мс.
В папке «010.2 Discovery Soft I2C» скачанного архива содержится проект программной эмуляции I2C интерфейса. Это адаптированные под STM32 исходники, которые я довольно долго и успешно использовал на AVR. В данном случае я просто подменил все «аппаратные» процедуры из первого примера программными аналогами, поэтому с точки зрения main.c и логики работы никаких изменений нет.
Я в результате всех этих изысканий получил базу простых заведомо рабочих примеров, которые могут служить отправной точкой для дальнейшего изучения этих все же довольно сложных микроконтроллеров. Очень много осталось за кадром: система тактирования, контроллер прерываний, масса аппаратных интерфейсов и модулей микроконтроллера, особенности программирования под него… Тем не менее надеюсь, что у читателя уже появилась возможность самостоятельно сделать какие-то простейшие программы, хотя бы на уровне мигания светодиодом. Как говорится, лиха беда – начало. Так что не все так страшно в ARM-процессорах, все можно освоить, чего и вам желаю, и чем сам планирую заниматься.