ARM – это просто (часть 3)

Hello World по микроконтроллеровски

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

Как уже говорилось в первой части, в качестве инструмента для написания программ на языке Си мы будем использовать среду разработки «IAR Embedded Workbench for ARM» (далее просто IAR), надеюсь, Вы уже скачали и установили её на свой компьютер. Также нам потребуется библиотека для работы с периферией микроконтроллеров серии STM32 «STM32F10x standard peripheral library» и драйвер внутрисхемного JTAG/SWD отладчика «ST-LINK USB». Если у Вас ещё не установлен драйвер для отладчика ST-LINK, то в таком случае необходимо произвести его установку.

Структура файлов библиотеки

При создании проекта, нам понадобится библиотека от ST для работы с периферией и ядром микроконтроллера. Структура содержащихся папок в скачанном архиве с библиотекой «STM32F10x standard peripheral library» представлена на рисунке 1.

Рис. 1. Структура папок архива STM32F10X STDPERIPH LIB

Рассмотрим подробнее, что содержится в данных папках.

В корневой папке архива находится файл «stm32f10x_stdperiph_lib_um.chm», представляющий из себя руководство пользователя библиотекой «STM32F10x Standard Peripherals Firmware Library». В этом файле описана структура всех содержащихся файлов в архиве, а также в нем можно прочесть описание всех используемых в библиотеке функций и макроопределений.

В папке «LibrariesSTM32F10x_StdPeriph_Driverinc» содержатся заголовочные файлы библиотек для работы с периферийными модулями микроконтроллера. Рассмотрим подробнее назначение каждого содержащегося файла.

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

Имя файла

Описание

misc.h

Данный файл содержит заголовки функций и описание структур для настройки контроллера вложенных векторизованных прерываний и конфигурации источника тактирования системного таймера

stm32f10x_adc.h

Данный файл содержит заголовки функций и описание структур для настройки аналогово-цифрового преобразователя

stm32f10x_bkp.h

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

stm32f10x_can.h

CAN шина передачи данных

stm32f10x_cec.h

Интерфейс для  реализации протокола управления бытовой мультимедийной аппаратурой по интерфейсу HDMI

stm32f10x_crc.h

Модуль подсчета контрольной циклической суммы

stm32f10x_dac.h

Цифро-аналоговый преобразователь

stm32f10x_dbgmcu.h

Настройка поведения периферии при работе с отладчиком (jtag и swd)

stm32f10x_dma.h

Контроллер модуля прямого доступа к памяти

stm32f10x_exti.h

Настройка внешних прерываний

stm32f10x_flash.h

Запись, чтение, стирание и прочие действия с флеш памятью

stm32f10x_fsmc.h

Шина для подключения внешних RAM, ROM, LCD и т.п. устройств с параллельной шиной передачи данных

stm32f10x_gpio.h

Настройка портов ввода/вывода

stm32f10x_i2c.h

Шина I2C

stm32f10x_iwdg.h

Независимый сторожевой таймер

stm32f10x_pwr.h

Управление питанием микроконтроллера

stm32f10x_rcc.h

Управление тактированием

stm32f10x_rtc.h

Часы реального времени

stm32f10x_sdio.h

Интерфейс для высокоскоростного подключения карт памяти SD/SDHC

stm32f10x_spi.h

Последовательный интерфейс передачи данных SPI

stm32f10x_tim.h

Настройка таймеров

stm32f10x_usart.h

Последовательный интерфейс передачи данных USART

stm32f10x_wwdg.h

Оконный сторожевой таймер

В папке «LibrariesSTM32F10x_StdPeriph_Driversrc» содержатся файлы с исходными кодами настройки и работы с соответствующими периферийными модулями.

В папке «LibrariesCMSISCM3CoreSupport» содержится файл исходного кода и соответствующий ему заголовочный файл «core_cm3», данные файлы представляют собой файлы поддержки ядром периферийных модулей.

Файлы с параметрами конкретного микроконтроллера содержатся в папке «LibrariesCMSISCM3DeviceSupportSTSTM32F10x». Имеющиеся файлы «system_stm32f10x» описывают функции начальной инициализации периферии, и в них задается тактовая частота работы микроконтроллера.

Файл «stm32f10x.h» содержит:

  • настройки выбора серии микроконтроллера с описанием обозначения серии. Наш микроконтроллер относится к 100 серии и имеет 128кБ флеш памяти, значит, в соответствии с этой классификацией он будет иметь обозначение – STM32F10X_MD_VL (Medium-density value line devices are STM32F100xx microcontrollers where the Flash memory density ranges between 64 and 128 Kbytes);
  • в макросе HSE_VALUE указывается частота работы кварцевого резонатора, для правильного расчета системы тактирования;
  • выбор источника тактирования – внешнего кварцевого резонатора или встроенного RC генератора;
  • переопределение объявления переменных на более короткую форму записи;
  • определение регистров и битовых флагов регистров периферии с кратким описанием их назначения.

В папке «LibrariesCMSISCM3DeviceSupportSTSTM32F10xstartup» содержится ассемблерный код начальной инициализации микроконтроллера для различных компиляторов. В папке с именем нашего компилятора имеется несколько файлов для разных серий микроконтроллеров, для нашего микроконтроллера подходит файл «startup_stm32f10x_md_vl.s».

Помимо файлов с библиотеками, в скачанном архиве также имеются папки «Utilities» с примерами исходных кодов для платы STM32_EVAL и папка «Project». В папке «Project» находятся папки «STM32F10x_StdPeriph_Examples» с примерами использования библиотеки и «STM32F10x_StdPeriph_Template» с шаблоном проекта для различных компиляторов.

Создание проекта

Итак, приступим к созданию своего первого проекта в среде программирования IAR на микроконтроллере STM32. Процесс создания и настройки шаблона проекта показан на видео.

После того как мы создали свой проект, необходимо произвести настройку файлов библиотеки. Рассмотрим ещё раз, более детальнее, содержимое файлов библиотеки и их назначение. Для этого открываем для редактирования файл «stm32f10x.h», предварительно сняв атрибут «лишь чтение». Находим в файле место, где определяется линейка используемого нами микроконтроллера и раскомментируем строку с используемой нами линейкой микроконтроллеров. Подсказка, как определить какая линейка соответствует какому микроконтроллеру, расположена сразу после выбора линейки микроконтроллера. Поскольку мы иcпользуем микроконтроллер серии STM32F100 с 128кБайтами флеш памяти, то нам подходит линейка «Medium-density value line», таким образом необходимо расскоментировать макроопределение STM32F10X_MD_VL.

/* Uncomment the line below according to the target STM32 device used in your
  application
 */

#if !defined (STM32F10X_LD) && !defined (STM32F10X_LD_VL) && !defined (STM32F10X_MD) && !defined (STM32F10X_MD_VL) && !defined (STM32F10X_HD) && !defined (STM32F10X_HD_VL) && !defined (STM32F10X_XL) && !defined (STM32F10X_CL)

 /* #define STM32F10X_LD */   /*!< STM32F10X_LD: STM32 Low density devices */
 /* #define STM32F10X_LD_VL */ /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */ 
 /* #define STM32F10X_MD */   /*!< STM32F10X_MD: STM32 Medium density devices */

#define STM32F10X_MD_VL /*!< STM32F10X_MD_VL: STM32 Medium density Value Line devices */ 

 /* #define STM32F10X_HD */   /*!< STM32F10X_HD: STM32 High density devices */
 /* #define STM32F10X_HD_VL */ /*!< STM32F10X_HD_VL: STM32 High density value line devices */
 /* #define STM32F10X_XL */   /*!< STM32F10X_XL: STM32 XL-density devices */
 /* #define STM32F10X_CL */   /*!< STM32F10X_CL: STM32 Connectivity line devices */

#endif

/* Tip: To avoid modifying this file each time you need to switch between these
   devices, you can define the device in your toolchain compiler preprocessor.
 — Low-density devices are STM32F101xx, STM32F102xx and STM32F103xx microcontrollers
  where the Flash memory density ranges between 16 and 32 Kbytes.
 — Low-density value line devices are STM32F100xx microcontrollers where the Flash
  memory density ranges between 16 and 32 Kbytes.
 — Medium-density devices are STM32F101xx, STM32F102xx and STM32F103xx microcontrollers
  where the Flash memory density ranges between 64 and 128 Kbytes.
 — Medium-density value line devices are STM32F100xx microcontrollers where the
  Flash memory density ranges between 64 and 128 Kbytes.  
 — High-density devices are STM32F101xx and STM32F103xx microcontrollers where
  the Flash memory density ranges between 256 and 512 Kbytes.
 — High-density value line devices are STM32F100xx microcontrollers where the
  Flash memory density ranges between 256 and 512 Kbytes.  
 — XL-density devices are STM32F101xx and STM32F103xx microcontrollers where
  the Flash memory density ranges between 512 and 1024 Kbytes.
 — Connectivity line devices are STM32F105xx and STM32F107xx microcontrollers.
 */

После выбора линейки используемого микроконтроллера, идет указание частоты источника тактирования. По умолчанию для всех микроконтроллеров, за исключением линейки STM32F10X_CL, установлена частота работы кварцевого резонатора 8МГц, поскольку именно на такую частоту у нас и установлен кварц, то нам не требуется изменять это значение. Помимо частоты работы установленного кварцевого резонатора, возможно также указание частоты работы встроенного RC генератора, это делается изменением макроса HSI_VALUE. Более никаких изменений вносить в данный файл не требуется.

/**
 * @brief In the following line adjust the value of External High Speed oscillator (HSE)
  used in your application
  Tip: To avoid modifying this file each time you need to use different HSE, you
   can define the HSE value in your toolchain compiler preprocessor.
 */

#if !defined HSE_VALUE

 #ifdef STM32F10X_CL

 #define HSE_VALUE   ((uint32_t)25000000) /*!< Value of the External oscillator in Hz */

 #else

 #define HSE_VALUE   ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */

 #endif /* STM32F10X_CL */

#endif /* HSE_VALUE */

/**
 * @brief In the following line adjust the External High Speed oscillator (HSE) Startup
  Timeout value
  */

#define HSE_STARTUP_TIMEOUT  ((uint16_t)0x0500) /*!< Time out for HSE start up */
#define HSI_VALUE   ((uint32_t)8000000) /*!< Value of the Internal oscillator in Hz*/

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

В файле «system_stm32f10x.c» также содержатся некоторые настройки, необходимость изменения которых также может возникнуть. В частности там задается рабочая тактовая частота системной шины:

/*!< Uncomment the line corresponding to the desired System clock (SYSCLK)
  frequency (after reset the HSI is used as SYSCLK source)
  IMPORTANT NOTE:
  ==============
  1. After each device reset the HSI is used as System clock source.
  2. Please make sure that the selected System clock doesn’t exceed your device’s maximum frequency.
  3. If none of the define below is enabled, the HSI is used as System clock source.
  4. The System clock configuration functions provided within this file assume that:
   — For Low, Medium and High density Value line devices an external 8MHz
   crystal is used to drive the System clock.
   — For Low, Medium and High density devices an external 8MHz crystal is
   used to drive the System clock.
   — For Connectivity line devices an external 25MHz crystal is used to drive the System clock.
   If you are using different crystal you have to adapt those functions accordingly.
   */

#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)

/* #define SYSCLK_FREQ_HSE   HSE_VALUE */

 #define SYSCLK_FREQ_24MHz 24000000

#else

/* #define SYSCLK_FREQ_HSE   HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */
/* #define SYSCLK_FREQ_36MHz 36000000 */
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */

#define SYSCLK_FREQ_72MHz 72000000

#endif

При включении, микроконтроллер начинает свою работу с тактированием от встроенного RC генератора, во время выполнения кода инициализации, программа производит переключение системы тактирования, на тактирование от внешнего кварцевого резонатора, если кварцевый резонатор окажется неисправным или не подключенным, то микроконтроллер аппаратно изменит выбор источника тактирования на встроенный RC генератор. Если кварцевый резонатор является исправным, то после его запуска производится умножение его частоты модулем «PLL», полученная частота является частотой системного тактирования (тактирования ядра микроконтроллера), и она не должна превышать максимальной частоты для данного микроконтроллера, в нашем случае 24МГц. Для тактирования периферийных модулей, эта частота может быть поделена модулем «AHB Prescaler».

Для линеек микроконтроллеров STM32F10X_LD_VL, STM32F10X_MD_VL и STM32F10X_HD_VL доступны лишь три возможных варианта тактирования, это работа на тактовой частоте 24МГц (при установленном кварцевом резонаторе на 8МГц), работа на частоте кварцевого резонатора (для этого необходимо закомментировать SYSCLK_FREQ_24MHz и раскомментировать SYSCLK_FREQ_HSE) либо от встроенного RC генератора (для этого необходимо закомментировать все строки).

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

Написание программы

Закончив с созданием и настройкой проекта, можно приступить к написанию реальной программы. Как повелось у всех программистов, первой программой написанной для работы на компьютере, является программа, выводящая на экран надпись «HelloWorld», так и у всех микроконтроллерщиков первая программа для микроконтроллера производит мигание светодиода. Мы не будем исключением из этой традиции и напишем программу, которая будет управлять светодиодом LD3 на плате STM32VL Discovery.

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

int main()
{
 return 0;
}

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

void main()
{
 while(1)
 {

 }
}

Теперь наша программа будет всегда «крутиться» в цикле while.

Для того, чтобы мы могли управлять светодиодом, нам необходимо разрешить тактирование порта к которому он подключен и настроить соответствующий вывод порта микроконтроллера на выход. Как мы уже рассматривали ранее в первой части, за разрешение тактирования порта С отвечает битIOPCEN регистра RCC_APB2ENR. Согласно документу «RM0041 Reference manual.pdf» для разрешения тактирования шины порта С необходимо в регистре RCC_APB2ENR установить бит IOPCEN в единицу. Чтобы при установке данного бита, мы не сбросили другие, установленные в данном регистре, нам необходимо к текущему состоянию регистра применить операцию логического сложения (логического «ИЛИ») и после этого записать полученное значение в содержимое регистра. В соответствии со структурой библиотеки ST, обращение к значению регистра для его чтения и записи производится через указатель на структуру RCC->APB2ENR. Таким образом, вспоминая материал со второй части, можно записать следующий код, выполняющий установку бита IOPCEN в регистре RCC_APB2ENR:

RCC->APB2ENR = RCC->APB2ENR | RCC_APB2ENR_IOPCEN;

//Или этот же код в более короткой форме записи

RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

Как можно убедиться, из файла «stm32f10x.h», значение бита IOPCEN определено как 0x00000010, что соответствует четвертому биту (IOPCEN) регистра APB2ENR и совпадает со значением указанным в даташите.

Теперь аналогичным образом настроим вывод 9 порта С. Для этого нам необходимо настроить данный вывод порта на выход в режиме push-pull. За настройку режима порта на вход/выход отвечает регистр GPIOC_CRH, мы его уже рассматривали в первой статье, его описание также находится в разделе «7.2.2 Port configuration register high» даташита. Для настройки вывода в режим выхода с максимальным быстродействием 2МГц, необходимо в регистре GPIOC_CRH установить MODE9[1] в единицу и сбросить бит MODE9[0] в нуль. За настройку режима работы вывода в качестве основной функции с выходом push-pull отвечают биты CNF9[1] и CNF9[0], для настройки требуемого нам режима работы, оба эти бита должны быть сброшены в ноль.

//очистить разряды MODE9 (сбросить биты MODE9_1 и MODE9_0 в нуль)

GPIOC->CRH &= ~GPIO_CRH_MODE9;

//Выставим бит MODE9_1, для настройки вывода на выход с быстродействием 2MHz

GPIOC->CRH |= GPIO_CRH_MODE9_1;

//очистить разряды CNF и настроить как выход общего назначения, симметричный (push-pull)

GPIOC->CRH &= ~GPIO_CRH_CNF9;

Теперь вывод порта, к которому подключен светодиод, настроен на выход, для управления светодиодом нам необходимо изменить состояние вывода порта, установив на выходе логическую единицу. Для изменения состояния вывода порта существует два способа, первый это запись непосредственно в регистр состояния порта измененного содержимого регистра порта, так же как мы производили настройку порта. Данный способ не рекомендуется использовать в виду возможности возникновения ситуации, при которой в регистр порта может записаться не верное значение. Данная ситуация может возникнуть если во время изменения состояния регистра, с момента времени когда уже было произведено чтение состояния регистра и до момента когда произведется запись измененного состояния в регистр, какое либо периферийное устройство или прерывание произведет изменение состояния данного порта. По завершению операции по изменению состояния регистра произойдет запись значения в регистр без учета произошедших изменений. Хотя вероятность возникновения данной ситуации является очень низкой, все же стоит пользоваться другим способом, при котором описанная ситуация исключена. Для этого в микроконтроллере существует два регистра GPIOx_BSRR и GPIOx_BRR. При записи логической единицы в требуемый бит регистра GPIOx_BRR произойдет сброс соответствующего вывода порта в логический ноль. Регистр GPIOx_BSRR может производить как установку, так и сброс состояния выводов порта, для установки вывода порта в логическую единицу необходимо произвести установку битов BSn, соответствующих номеру необходимого бита, данные биты располагаются в младших регистрах байта. Для сброса состояния вывода порта в логический нуль, необходимо произвести запись битов BRn соответствующих выводов, эти биты располагаются в старших разрядах регистра порта.

Светодиод LD3 подключен к выводу 9 порта С. Для включения данного светодиода, нам необходимо подать на соответствующем выводе порта логическую единицу, чтобы «зажечь» светодиод.

//Установка вывода 9 порта С в логическую единицу

GPIOC->BSRR = GPIO_BSRR_BS9;

//Сброс состояния вывода 9 порта С в логический ноль

GPIOC->BSRR = GPIO_BSRR_BR9;

//Ещё один вариант сброса состояния вывода 9 порта С в логический ноль

GPIOC->BRR  = GPIO_BRR_BR9;

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

//Не забываем подключить заголовочный файл с описанием регистров микроконтроллера

#include «stm32f10x.h»

//объявляем функцию программной задержки

void Delay (void);

//сама функция программной задержки

void Delay (void)
{
 unsigned long i;
 for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void)
{

 //Разрешаем тактирование шины порта С
 RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

 //очистим разряды MODE9 (сбросить биты MODE9_1 и MODE9_0 в нуль)
 GPIOC->CRH &= ~GPIO_CRH_MODE9;

 //Выставим бит MODE9_1, для настройки вывода на выход с быстродействием 2MHz
 GPIOC->CRH |= GPIO_CRH_MODE9_1;

 //очистим разряды CNF (настроить как выход общего назначения, симметричный (push-pull))
 GPIOC->CRH &= ~GPIO_CRH_CNF9;

 //Наш основной бесконечный цикл

  while(1)
  {

  //Установка вывода 9 порта С в логическую единицу («зажгли» светодиод)
  GPIOC->BSRR = GPIO_BSRR_BS9;

  //Добавляем программную задержку, чтобы светодиод светился некоторое время
  Delay();

  //Сброс состояния вывода 9 порта С в логический ноль
  GPIOC->BSRR = GPIO_BSRR_BR9;

  //Добавляем снова программную задержку
  Delay();

  }
}

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

Наша первая работоспособная программа написана, при её написании, для работы и настройки периферии, мы пользовались данными из официального даташита «RM0041 Reference manual.pdf», данный источник информации о регистрах микроконтроллера является самым точным, но для того чтобы им пользоваться приходится перечитывать много информации, что усложняет написание программ. Для облегчения процесса настройки периферии микроконтроллера, существуют различные генераторы кода, официальной утилитой от компании ST представлена программа Microxplorer, но она пока ещё малофункциональна и по этой причине сторонними разработчиками была создана альтернативная программа «STM32 Генератор программного кода». Данная программа позволяет легко получить код настройки периферии, используя удобный, наглядный графический интерфейс (см. рис. 2).


Рис. 2 Скриншот программы STM32 генератор кода

Как видно из рисунка 2, сгенерированный программой код настройки вывода светодиода совпадает с кодом написанным нами ранее.

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

Видео режима отладки программы мигания светодиодом

Видео работы программы мигания светодиодом на плате STM32VL Discovery

Библиотечные функции работы с периферией

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

Теперь напишем нашу программу с использованием библиотеки ST. В программе требуется произвести настройку портов ввода/вывода, для использования библиотечных функций настройки портов, необходимо произвести подключение заголовочного файла «stm32f10x_gpio.h» (см. табл. 1). Подключение данного файла можно произвести расскоментированием соответствующей строки в подключенном заголовочном конфигурационном файле «stm32f10x_conf.h». В конце файла «stm32f10x_gpio.h» имеется список объявлений функций для работы с портами. Подробное описание всех имеющихся функций можно прочитать в файле «stm32f10x_stdperiph_lib_um.chm», краткое описание наиболее часто применяемых приведено в таблице 2.

Таблица 2.Описание основных функций настройки портов

Функция

Описание функции, передаваемых и возвращаемых параметров

GPIO_DeInit (
GPIO_TypeDef* GPIOx)

Производит установку значений регистров настройки порта GPIOx на значения по умолчанию

GPIO_Init (
GPIO_TypeDef* GPIOx,
GPIO_InitTypeDef* GPIO_InitStruct)

Производит установку регистров настройки порта GPIOx в соответствии с указанными параметрами в структуре GPIO_InitStruct

GPIO_StructInit (
GPIO_InitTypeDef* GPIO_InitStruct)

Заполняет все поля структуры GPIO_InitStruct, значениями по умолчания

uint8_t GPIO_ReadInputDataBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin);

Чтение входного значения вывода GPIO_Pin порта GPIOx

uint16_t GPIO_ReadInputData (
GPIO_TypeDef* GPIOx)

Чтение входных значений всех выводов порта GPIOx

GPIO_SetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Установка выходного значения вывода GPIO_Pin порта GPIOx в логическую единицу

GPIO_ResetBits(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin)

Сброс выходного значения вывода GPIO_Pin порта GPIOx в логический ноль

GPIO_WriteBit(
GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin,
BitAction BitVal)

Запись значения BitVal в вывод GPIO_Pin порта GPIOx

GPIO_Write(
GPIO_TypeDef* GPIOx,
uint16_t PortVal)

Запись значения PortVal в порт GPIOx

Как видно из описания функций, в качестве параметров настроек порта и т.п., в функцию передают не множество различных отдельных параметров, а одну структуру. Структуры — это объединенные данные, у которых есть некоторая логическая взаимосвязь. В отличие от массивов, структуры могут содержать данные разных типов. Другими словами, структура представляет набор различных переменных с различными типами, объединенными в одну своеобразную переменную. Переменные, находящиеся в данной структуре называются полями структуры, а обращение к ним производится следующим образом, сперва пишется имя структуры, далее пишется точка и имя поля структуры (имя переменной в этой структуре).

Список переменных, включенных в структуры для функций работы с портами, описаны в том же файле несколько выше описания функций. Так, например, структура «GPIO_InitTypeDef» имеет следующую структуру:

typedef struct
{

 uint16_t GPIO_Pin;   /*!< Specifies the GPIO pins to be configured.
   This parameter can be any value of @ref GPIO_pins_define */

 GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins.
   This parameter can be a value of @ref GPIOSpeed_TypeDef */

 GPIOMode_TypeDef GPIO_Mode;   /*!< Specifies the operating mode for the selected pins.
   This parameter can be a value of @ref GPIOMode_TypeDef */

}GPIO_InitTypeDef;

Первое поле данной структуры содержит переменную «GPIO_Pin» типа unsigned short, в данную переменную необходимо записывать флаги номеров соответствующих выводов, для которых предполагается произвести необходимую настройку. Можно произвести настройку сразу несколько выводов, задав в качестве параметра несколько констант через оператор побитовое ИЛИ (см. часть 2). Побитовое ИЛИ «соберёт» все единички из перечисленных констант, а сами константы являются маской, как раз предназначенной для такого использования. Макроопределения констант указаны в этом же файле ниже.

Второе поле структуры «GPIO_InitTypeDef» задает максимально возможную скорость работы выхода порта. Список возможных значений данного поля перечислен выше:

typedef enum
{
 GPIO_Speed_10MHz = 1,
 GPIO_Speed_2MHz,
 GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;

Последнее, третье поле, «GPIOMode_TypeDef» отвечает за режим работы выводов порта. Данное поле представляет из себя перечисляемый тип enum и выглядит следующим образом:

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

Описание возможных значений:

  • GPIO_Mode_AIN — аналоговый вход (англ. Analog INput);
  • GPIO_Mode_IN_FLOATING — вход без подтяжки, болтающийся (англ. Input float) в воздухе
  • GPIO_Mode_IPD — вход с подтяжкой к земле (англ. Input Pull-down)
  • GPIO_Mode_IPU — вход с подтяжкой к питанию (англ. Input Pull-up)
  • GPIO_Mode_Out_OD — выход с открытым стоком (англ. Output Open Drain)
  • GPIO_Mode_Out_PP — выход двумя состояниями (англ. Output Push-Pull — туда-сюда)
  • GPIO_Mode_AF_OD — выход с открытым стоком для альтернативных функций (англ. Alternate Function). Используется в случаях, когда выводом должна управлять периферия, прикрепленная к данному выводу порта (например, вывод Tx USART1 и т.п.)
  • GPIO_Mode_AF_PP — то же самое, но с двумя состояниями

Аналогичным образом можно посмотреть структуру переменных других структур, необходимых для работы с библиотечными функциями.

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

GPIO_InitTypeDef GPIO_Init_struct;  //Объявляем структуру

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

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

GPIO_StructInit(&GPIO_Init_struct);

/* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем настраивать далее */

GPIO_Init_struct.GPIO_Pin=GPIO_Pin_9;

/* Подобным образом заполним поле GPIO_Speed */

GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

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

GPIO_Init(GPIOC, &GPIO_Init_struct);

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

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

//Не забываем подключить заголовочный файл с описание регистров микроконтроллера

#include «stm32f10x.h»
#include «stm32f10x_conf.h»

//объявляем функцию программной задержки

void Delay (void);

//сама функция программной задержки

void Delay (void)
{
 unsigned long i;
 for (i=0; i<2000000; i++);
}

//Наша главная функция

void main(void)
{

 //Разрешаем тактирование шины порта С
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

 //Объявляем структуру для настройки порта
 GPIO_InitTypeDef GPIO_Init_struct;

 //Заполняем структуру начальными значениями
 GPIO_StructInit(&GPIO_Init_struct);

 /* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем   настраивать далее */
 GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

 // Подобным образом заполним поля GPIO_Speed и GPIO_Mode
 GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
 GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

 //Передаем заполненную структуру, для выполнения действий по настройке регистров
 GPIO_Init(GPIOC, &GPIO_Init_struct);

 //Наш основной бесконечный цикл
 while(1)
 {
  //Установка вывода 9 порта С в логическую единицу («зажгли» светодиод)
  GPIO_SetBits(GPIOC, GPIO_Pin_9);

  //Добавляем программную задержку, чтобы светодиод светился некоторое время
  Delay();

  //Сброс состояния вывода 9 порта С в логический ноль
  GPIO_ResetBits(GPIOC, GPIO_Pin_9);

  //Добавляем снова программную задержку
  Delay();
 }
}

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

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

Данное обстоятельство, а также тот факт, что компания ST позаботилась о высокой степени совместимости, как в аппаратном, так и в программном плане, различных своих микроконтроллеров, способствует более простому их изучению, в виду того, что не требуется углубляться на особенности строения различных контроллеров серии STM32 и позволяет в качестве микроконтроллера для изучения выбрать любой из имеющихся в линейке STM32 микроконтроллер.

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

Микроконтроллеры имеют одну замечательную способность – останавливать выполнение основной программы по какому-то определенному событию, и переходить к выполнению специальной подпрограммы – обработчику прерывания. В качестве источников прерывания могут выступать как внешние события – прерывания по приему/передаче данных через какой либо интерфейс передачи данных, или изменение состояния вывода, так и внутренние – переполнение таймера и т.п.. Список возможных источников прерывания для микроконтроллеров серии STM32 приведен в даташите «RM0041 Reference manual» в разделе «8 Interrupts and events».

Поскольку обработчик прерывания также является функцией, то и записываться она будет как обычная функция, но чтобы компилятор знал, что данная функция является обработчиком определенного прерывания, в качестве имени функции следует выбрать заранее определенные имена, на которые указаны перенаправления векторов прерывания. Список имен этих функций с кратким описанием находится в ассемблерном файле «startup_stm32f10x_md_vl.s». На один обработчик прерывания может приходиться несколько источников вызывающих прерывания, например функция обработчик прерывания «USART1_IRQHandler» может быть вызвана в случае окончания приема и окончания передачи байта и т.д..

Для начала работы с прерываниями следует настроить и проинициализировать контроллер прерываний NVIC. В архитектуре Cortex M3 каждому прерыванию можно выставить свою группу приоритета для случаев, когда возникает несколько прерываний одновременно. Затем следует произвести настройку источника прерывания.

//Для начала выбираем приоритетную группу для прерываний

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

/* Далее, для каждого прерывания, нам надо произвести настройку и инициализацию с помощью структуры — точно так же, как мы поступали ранее при настройке портов*/

//Объявляем структуру

NVIC_InitTypeDef NVIC_InitStruct;

//Производим заполнение полей структуры

NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;

// Структура настроена, передаем её в функцию, тем самым инициализируем прерывания от USART:

NVIC_Init(&NVIC_InitStruct);

В поле NVIC_IRQChannel указывается, какое именно прерывание мы хотим настроить. Константа USART1_IRQn обозначает канал, отвечающий за прерывания, связанные с USART1. Она определена в файле «stm32f10x.h», там же определены другие подобные константы.

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

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

Теперь в настройках модуля необходимо установить параметры, по которым данный модуль будет генерировать прерывание. Для начала следует произвести включение прерывания, это делается вызовом функции name_ITConfig(), которая находится заголовочном файле периферийного устройства.

//Разрешаем прерывания по окончанию передачи байта по USART1
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);

//Разрешаем прерывания по окончанию приема байта по USART1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

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

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

void USART1_IRQHandler(void)
{
//Обработка события приема байта
if (USART_GetITStatus(USART1, USART_IT_RXNE) )
   {
   USART_ClearITPendingBit(USART1, USART_IT_RXNE); //Сбрасываем флаг
    // … Код обработчика прерывания…
   };

// Обработка события завершения передачи байта
if ( USART_GetITStatus(USART1, USART_IT_TXE) )
   {
   USART_ClearITPendingBit(USART1, USART_IT_TXE);
    // … Код обработчика прерывания …
   };
}

Для выполнения различных, небольших, повторяющихся с точным периодом действий, в микроконтроллерах с ядром Cortex-M3 имеется специально предназначенный для этого системный таймер. В функции данного таймера входит лишь вызов прерывания через строго заданные интервалы времени. Как правило, в вызываемом этим таймером прерывании, размещают код для измерения продолжительности различных процессов. Объявление функции настройки таймера размещено в файле «core_cm3.h». В качестве передаваемого функции аргумента указывается число тактов системной шины между интервалами вызова обработчика прерывания системного таймера.

SysTick_Config(clk);

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

Пример файла «main.с» программы мигания светодиода с использованием прерывания:

//Подключаем заголовочный файл с описанием регистров микроконтроллера

#include «stm32f10x.h»
#include «stm32f10x_conf.h»
#include «main.h»

//Объявим переменную, которая будет считать количество прерываний системного таймера

unsigned int LED_timer;

//Функция, вызываемая из функции-обработчика прерываний системного таймера

void SysTick_Timer_main(void)
{
//Если переменная LED_timer ещё не дошла до 0,
   if (LED_timer)
   {
   //Проверяем ее значение, если оно больше 1500 включим светодиод
   if (LED_timer>1500)  GPIOC->BSRR= GPIO_BSRR_BS9;

   //иначе если меньше или равно 1500 то выключим
   else GPIOC->BSRR= GPIO_BSRR_BR9;

   //Произведем декремент переменной LED_timer
   LED_timer—;
   }

   //Ели же значение переменной дошло до нуля, зададим новое значение 2000
   else LED_timer=2000;
}

//Наша главная функция

void main(void)
{

   //Разрешаем тактирование шины порта С
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

   //Объявляем структуру для настройки порта
   GPIO_InitTypeDef GPIO_Init_struct;

   //Заполняем структуру начальными значениями
   GPIO_StructInit(&GPIO_Init_struct);

   /* Запишем в поле GPIO_Pin структуры GPIO_Init_struct номер вывода порта, который мы будем    настраивать далее */
   GPIO_Init_struct.GPIO_Pin = GPIO_Pin_9;

   // Подобным образом заполним поля GPIO_Speed и GPIO_Mode
   GPIO_Init_struct.GPIO_Speed= GPIO_Speed_2MHz;
   GPIO_Init_struct.GPIO_Mode = GPIO_Mode_Out_PP;

   //Передаем заполненную структуру, для выполнения действий по настройке регистров
   GPIO_Init(GPIOC, &GPIO_Init_struct);

   //выбираем приоритетную группу для прерываний
   NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

   //Настраиваем работу системного таймера с интервалом 1мс
   SysTick_Config(24000000/1000);

   //Наш основной бесконечный цикл
   while(1)
   {
   //В этот раз тут пусто, все управление светодиодом происходит в прерываниях
    }
}

Часть исходного кода в файле «stm32f10x_it.c»:


#include «main.h»

/**
 * @brief This function handles SysTick Handler.
 * @param None
 * @retval None
 */

void SysTick_Handler(void)
{
    SysTick_Timer_main();
}

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

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

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

Часть 1. ARM – это просто. Введение.

Добавить комментарий

Ваш адрес email не будет опубликован.