Pull to refresh

Первые шаги с STM32 и компилятором mikroC для ARM архитектуры — Часть 2, начало…

Reading time8 min
Views12K

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


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


Вывод порта может работать в нескольких режимах, непосредственно как цифровой порт, кроме этого многие выводы могут использоватся как входы АЦП, выходы ШИМ модулятора, входы внешних прерываний, аппаратных последовательных SPI и I2C интрефейсов. (например PA6 это непосредственно 6 вывод порта А, 6 вход АЦП, выход 1 канала 3-го ШИМ модулятора / таймера и сигнал MISO 1-го SPI интерфейса).


В microC для расширенной конфигурации GPIO есть функция


GPIO_Set_Pin_Mode(&port_base, pin, config);

  • &port_base — указатель на порт GPIO (&GPIOE_BASE для порта Е или, например, &GPIOА_BASE для порта А);
  • pin — вывод порта с которым мы работаем (_GPIO_PIN_5 для 5 вывода или, например, _GPIO_PIN_ALL для всех выводов порта);
  • config — режим работы порта (несколько параметров записываем через оператор |);
    • _GPIO_CFG_MODE_INPUT — Сконфигурирован на вход;
      • _GPIO_CFG_PULL_UP — Включен подтягивающий резистор на +Vcc;
      • _GPIO_CFG_PULL_DOWN — Включен стягивающий резистор на GND;.
      • _GPIO_CFG_PULL_NO — Подтягивающий и стягивающий резисторы отключены;
    • _GPIO_CFG_MODE_ALT_FUNCTION — Вывод использован как вход/выход встроенного периферийного модуля, например UART или SPI
    • _GPIO_CFG_MODE_ANALOG — Вывод используется как вход АЦП (в микроконтроллерах с встроенным ЦАП — как выход ЦАП);
    • _GPIO_CFG_MODE_OUTPUT — Сконфигурирован на выход;
      • _GPIO_CFG_OTYPE_OD — Включен режим работы "Открытый сток";
      • _GPIO_CFG_OTYPE_PP — Включен режим работы "push-pull" ("двухтактный выход", его чаще всего и используют);
    • _GPIO_CFG_SPEED_MAX — Скорость работы GPIO, MAX для работы на полной скорости, которую обеспечивает APB допустимо использовать 400KHZ, 2MHZ, 10MHZ, 40MHZ, 50MHZ, 100MHZ.

В прошлой части мы конфигурировали вывод порта на вход командой


GPIO_Digital_Input (&GPIOb_BASE, _GPIO_PINMASK_8);

По другому это можно сделать:


GPIO_Set_Pin_Mode(&GPIOb_BASE, _GPIO_PINMASK_8, _GPIO_CFG_MODE_INPUT | _GPIO_CFG_PULL_NO);

То-же но включив подтяжку к питанию:


GPIO_Set_Pin_Mode(&GPIOb_BASE, _GPIO_PINMASK_8, _GPIO_CFG_MODE_INPUT | _GPIO_CFG_PULL_UP);

По умолчанию, microC инициализируя порт на вход (GPIO_Digital_Input()) не подключает подтягивающие резисторы к пину МК, поэтому если одни нужны, то пользуемся GPIO_Set_Pin_Mode() с дополнительными параметрами.


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


Button(&port_base, pin, delay, state)

Данной функции мы передаем *port_base (например &GPIOb_BASE ); pin — номер вывода порта МК; delay — время, импульсы с длительностью меньше которого считаются дребезгом (например 1мс) и state — активное состояние (выокий или низкиий уровень на входе порта контролирует функция).


Немного изменим программу из первой части, введя в нее программную защиту от дребезга кнопки:


void main()
{
  GPIO_Digital_Output(&GPIOb_BASE, _GPIO_PINMASK_1);
  GPIO_Digital_Input(&GPIOb_BASE, _GPIO_PINMASK_8); // Настраиваем вывод PB8 на вход
  GPIOb_ODR.b1 = 0;

    while(1)
      {
        if (Button(&GPIOb_IDR, 8, 1, 1)) // кнопка на порту PB(GPIOb), пин №8, задержка 1 мс, активное состояние (кнопка нажата) - высокий уровень. Функция вернет 1 если высокий уровень на входе продлится  1 мс, все импульсы короче  - это дребезг, и не обрабатываются
          {
           GPIOb_ODR.b1=~GPIOb_ODR.b1;
           Delay_ms(500); //Задержка 500 мс
          }
        else
          {
           GPIOb_ODR.b1 = 0; //Если кнопку отпустили, погасить светодиод
          }
      }
}

При нажатие на кнопку видим мигающий светодиод. Теперь сделаем чтобы светодиод при первом нажатии начинал моргать, а при следующем переставал.


unsigned short state = 0; // объявим переменную, в которой будем хранить нужные нам данные, в этой программе мы используем 2 бита (2 - используется для контроля отпускания кнопки и 1 - мигает или не мигает наш светодиод.)
void main()
{
  GPIO_Digital_Output(&GPIOb_BASE, _GPIO_PINMASK_1);
  GPIO_Digital_Input(&GPIOb_BASE, _GPIO_PINMASK_8);
  GPIOb_ODR.b1 = 0;

    while(1)
      {
        if (Button(&GPIOb_IDR, 8, 1, 1)) // кнопка нажата 
           state.b2=1; // ставим флаг нажатой кнопки
        if (state.b2 && Button(&GPIOb_IDR, 8, 1, 0)) // кнопка отжалась, но в прошлом проходе цикла была нажата, значит включим/выключим наш светодиод
           {
           state.b2 = 0; // сбросим флаг нажатой кнопки
           state.b1= ~state.b1; // инвертируем флаг включенного светодиода
           }
         if (state.b1) // если флаг светодиода установлен то мигаем им
          {
           GPIOb_ODR.b1=~GPIOb_ODR.b1;
           Delay_ms(250);
          }
           else
          {
           GPIOb_ODR.b1 = 0;
          }
          Delay_ms(1);
      }
}

Теперь по нажатию на кнопку светодиод начинает мигать, а по следующему нажатию перестает (должен во всяком случае) Но так происходит не всегда… Причина в том что Delay_ms(250) это по сути 250 мс перебора пустого цикла ядром МК. В это время он только этим и занят, и напрочь игнорирует любые внешние воздействия на него (ну кроме ресет и отключения питания конечно, шутка юмора:))
Для того чтобы не занимать драгоценные такты микроконтроллера формированием временных интервалов у него существуют специальные блоки, называемые таймерами. У STM32 их количество доходит 14. Они имеют немного разные параметры настройки и могут использоваться не только для отсчета временных отрезков, но и например для генерации ШИМ. По сути каждый таймер представляет собой регистр TIMx_CNT (у STM 32 они 16 разрядные) который инкриметируется через определенное количество тактового тактового генератора МК (Точнее шины APB1 (для таймера Timer1 — APB2)к которой он подключен). Сколько тактов APB для увеличения значения регистра таймера на 1 должно пройти, указывает регистр предделителя, TIMx_PSC , на 16-разрядное значение в котором делится частота тактовой шины.
У STM32F103 — 7 таймеров,


  • TIM1 — Advanced-control timers, таймеры позволяющие расширенную настройку параметров (например формирование импульсов необходимых для работы преобразователей частоты используемых в электроприводе). В дальнейшей мы вернемся к ней в наших статьях. TIM1 имеет 4 канала ШИМ модулатора;
  • TIM2, TIM3, TIM4 — General-purpose timers — Таймеры общего назначения. Их чаще всего используют, TIM2, TIM3, TIM4 имеют по 4 канала ШИМ модулатора;
  • WDT — Wachdog timer — Таймер, используемый для предотвращения зависания МК;
  • SysTick timer — 24 битный счетчик тактов ядра МК, используется, например, для оценки загрузки ядра .

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


  • TIMx_CNT — Собственно счетный регистр;
  • TIMx_PSC — Предделитель частоты, коэффициент предделителя равен TIMx_PSC + 1(не забывайте это, чтобы не попасть в ситуацию: "как не работает?" :)) ;
  • TIMx_ARR — Значение счетного регистра, при достижении которого таймер обнуляется и генерируется прерывание (о них ниже);
  • TIMx_CR1 — Регистр параметров таймера;
  • TIMх_DIER — Регистр настойки прерванный и DMAтаймера;
  • TIMх_SR — Регистр статуса таймера, в нем например присутствует флаг, показывающий срабатывание прерываня.

При обнулении таймера, если это разрешено программно генерируется прерывание (interrupt). Прерывание это вызов определенного участка кода программы по некому событию. Источниками прерываний могут быть встроенные блоки МК, входы имеющие функцию создания прерывания (int) а также блок DMA. Фактически при поступлении прерывания МК переходит от выполнения основного кода к выполнению определенной подпрограммы, по окончании выполнения которой он возвращается в основной код (особая история когда при обработке одного прерывания возникает другое, но на этом акцентировать внимание пока не будем). В STM32 имеется более 70 так называемых векторов прерываний — событий инициирующих выполнения подпрограммы — обработчика прерывания. ПО умолчанию все прерывания запрещены. Для разрешения прерывания от того или иного блока необходимо изменить конфигурационные биты в управляющих регистрах того или иного блока МК. В microC для включения прерывания используется команда


NVIC_IntEnable(NVIC_name) //NVIC_name - название вектора прерывания 

Вообще в microC есть очень полезная функция, называемая "Code assistant". Вызывается она по нажатию комбинации клавиш Ctrl+Spase. Она полезна когда не помнишь точное название какого-то параметра. Набрав начальные символы, компилятор сам предложит завершить команду, выбрав из возможных значений. Например набрав "NVICIntEnable(NVIC" b и вызвав "Code assistant" имеем:


image


аналогично с параметрам регистров управления. Например набрав "TIM4_CR1bits.", получаем:


image


Внимательные читатели заметили что для доступа к отдельному биту регистра я использовал выражение типа REGx.by, например


GPIOb_ODR.b1=0; \\1 бит регистра GPIOb_ODR
TIM4_CR1.CEN \\ бит CEN регистра TIM4_CR1 

есть и другая форма записи того же самого действия, причем "Code assistant" нормально работает только с ней


GPIOb_ODRbits.ODR1=1; \\1 бит регистра GPIOb_ODR
TIM4_CR1bits.CEN \\ бит CEN регистра TIM4_CR1 

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


  RCC_APB1ENR.TIM2EN = 1;  //Подаем тактирование  от шины APB1 (1/2 частоты ядра, 72/2=36 МГц) на таймер 2, по умолчанию оно отключено
  TIM2_CR1.CEN = 0; //Отключаем таймер 2, при включённом таймере не устанавливаются многие конфигурационные биты
  TIM2_PSC = 7199; //Устанавливаем частоту делителя, 7199+1 = 7200, при частоте на входе предделителя  72 МГц вход таймера тактируется с частотой 10 KГц
  TIM2_ARR = 5000; //Устанавливаем значение таймера при котором произойдет генерация прерывания и обнуление таймера
  NVIC_IntEnable(IVT_INT_TIM2); //Разрешаем вектор прерывания по таймеру
  TIM2_DIER.UIE = 1; // Разрешаем таймер создавать прерывание
  TIM2_CR1.CEN = 1; //Включаем таймер 2

*Важное замечание. Несмотря на то что TIM2, TIM3, TIM4 подключены к шине APB1, которая работает, в нашем случае, на половинной частоте (APB1 имеет максимальную частоту 36 МГц) HCLK, таймеры при установке предделителя APB1 отличного от 1 тактируются на удвоенной частоте шины, то есть при частоте APB1=HCLK/2 TIMxCLK = APB1 х 2, фактически, в нашем случае, частотой ядра.


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


void Timer2_interrupt() iv IVT_INT_TIM2
  {
  TIM2_SR.UIF = 0; // Сбарсываем флаг прерывания, это нужно делать обязательно после каждого срабатывания прерывания таймера!!!

     //далее наш код

  }

теперь все готово чтобы в очередной раз мигнуть нашим светодиодом, то теперь уже при помощи таймера


unsigned short state = 0;
void main()
{
  GPIO_Digital_Output(&GPIOb_BASE, _GPIO_PINMASK_1);
  GPIO_Digital_Input(&GPIOb_BASE, _GPIO_PINMASK_8);

  RCC_APB1ENR.TIM2EN = 1;
  TIM2_CR1.CEN = 0;
  TIM2_PSC = 7199;
  TIM2_ARR = 5000;
  NVIC_IntEnable(IVT_INT_TIM2);
  TIM2_DIER.UIE = 1;
  TIM2_CR1.CEN = 1;

    while(1)
      {
      }
}

void Timer2_interrupt() iv IVT_INT_TIM2
  {
     TIM2_SR.UIF = 0;
     GPIOb_ODR.b1=~GPIOb_ODR.b1;
  }

Давайте будем включать и отключать таймер при помощи нашей кнопки, которая теперь будет работать как нужно.


unsigned short state = 0;
void main()
{
  GPIO_Digital_Output(&GPIOb_BASE, _GPIO_PINMASK_1);
  GPIO_Digital_Input(&GPIOb_BASE, _GPIO_PINMASK_8);

  RCC_APB1ENR.TIM2EN = 1;
  TIM2_CR1.CEN = 0;
  TIM2_PSC = 7199;
  TIM2_ARR = 5000;
  NVIC_IntEnable(IVT_INT_TIM2);
  TIM2_DIER.UIE = 1;
    while(1)
      { 
      if (Button(&GPIOb_IDR, 8, 1, 1))
           state.b2=1;
        if (state.b2 && Button(&GPIOb_IDR, 8, 1, 0))
           {
           state.b2 = 0;
           state.b1= ~state.b1;
           }
         if (state.b1)
          {
           TIM2_CR1.CEN = 1; // запускаем таймер
          }
           else
          {
           TIM2_CR1.CEN = 0; // отключаем таймер
           GPIOb_ODR.b1 = 0; // гасим светодиод
          }
          Delay_ms(1);
      }
}

void Timer2_interrupt() iv IVT_INT_TIM2
  {
     TIM2_SR.UIF = 0;
     GPIOb_ODR.b1=~GPIOb_ODR.b1;
  }

*продолжение следует

Tags:
Hubs:
+7
Comments6

Articles