Главная / Технологии / Программирование / Программа "Светофор" на STM32

Программа "Светофор" на STM32

Недавно я заинтересовался программированием микроконтроллеров, и для начала остановился на STM32. Сегодня я хочу познакомиться с библиотекой HAL, собрать на макетной плате небольшую схему и написать управляющую программу. Не мудрствуя лукаво, и за отсутствием у меня на данный момент каких-либо внешних датчиков, моторчиков и прочей периферии для микроконтроллеров, решил я попрактиковаться в работе не с встроенными в плату светодиодами/кнопками, а с внешними, и реализовать приближенный к реальности проект. Программа "Светофор" на STM32 - первое приложение, которое я сам создаю на плате STM32F407VG. Это работающий макет обычного дорожного светофора.

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

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

Алгоритм работы будет такой:

  1. Красный свет горит 5 сек.
  2. Вместе с красным зажигается желтый на 2 сек.
  3. Загорается зеленый на 4 секунды.
  4. Зеленый цвет мигает 3 раза с интервалом в полсекунды.
  5. Загорается желтый на 2 секунды.

После этого цикл повторяется.

Управление осуществляется платой STM32F407VG-Discovery, а сама схема выполнена на макетной плате.

Для программирования используется программа Keil_v5 с библиотекой HAL. О подключении этой библиотеки есть масса информации в сети и останавливаться на этом я не буду. Скажу только, что достаточно поставить программу STM32CubeMX, которую можно скачать на сайте производителя микроконтроллеров st.com, и нужная нам библиотека подключится автоматически.

В этой версии программы задержки будут выполняться при помощи встроенной в библиотеку HAL процедуры HAL_delay.

Использование CubeMX

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

Итак, запускаем эту программу, выбираем из списка модель микроконтроллера (напомню, у меня STM32407VG), после чего появится рисунок чипа со всеми его контактами. Для решения нашей задачи нам необходимо задействовать три контакта для подключения – красного желтого и зеленого светодиодов. Пусть это будут контакты 5, 6 и 7 порта B.

Отмечаем их как GPIO_Output. На вкладке «System View» немного отредактируем параметры контактов, в частности, подключим подтягивающие резисторы к общему проводу.

Теперь необходимо включить тактирование от встроенного кварцевого резонатора. Слева, в раскрывающемся разделе «System Core» в пункте «Crystal/Ceramic Resonator». На рисунке два вывода, «PH0-OSC_IN» и «PH1-OSC_OUT», станут зелеными, т. е. будут задействованы.

Теперь надо перейти в раздел «Clock Configuration». Переключаем тактирование на HSE, и «System Clock Mux» переключаем на «PLLCLK». Теперь надо отредактировать значения частот и делителей, чтобы светодиоды действительно мигали с необходимыми интервалами. Значения видны на скриншоте:

Программа "Светофор" на STM32

Теперь можно генерить код. Указываем нужную папку, в которой будет расположен проект, его название, и указываем, что мы пользуемся IDE MDK-ARM V5. Все, после генерации кода можем открыть проект, автоматически запустится среда программирования.

Разбираем автоматически созданный код

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

Просмотрим на главную функцию main. Сначала вызывается инициализирующая библиотеку HAL функция

HAL_Init();

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

Следующий шаг – вызов функций SystemClock_Config() и MX_GPIO_Init(), на них остановимся чуть подробнее.

Функция SystemClock_Config

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

Если присмотреться к следующим строкам кода, то становится понятным, что это тестовое представление тех шагов, которые выполнялись в программе CubeMX.

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

RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; 

Если вдруг понадобится поменять параметры работы, достаточно изменить только значения делителей и прочих элементов, указав, например, RCC_HCLK_DIV2 вместо RCC_HCLK_DIV1.

Фунция MX_GPIO_Init()

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

Что мы видим в этой функции? После объявления переменной GPIO_InitStruct, являющейся указателем на структуру GPIO_InitTypeDef, происходит включение тактирования используемых портов:

__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE(); 

GPIOH – порт, отвечающий за работу тактового генератора, GPIOB – порт, к ножкам 5, 6 и 7 которого подключены светодиоды нашего светофора. Итак, тактирование включено, теперь надо настроить порты. Прежде всего, надо выполнить начальную инициализацию, выключив ножки 5, 6 и 7:

HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_RESET);

Теперь надо заполнить структуру параметрами работы порта B:

GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); 

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

Все, на этом этапе мы уже можем включать и выключать светодиоды, подключенные к контактам PB5, PB6 и PB7.

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

Пишем программу переключения сигналов светофора

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

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

Готовая функция выглядит так:

void blink_LED(GPIO_TypeDef* Port, uint16_t GPIO_Pin, uint8_t cnt, uint32_t delay_value)
{
    HAL_GPIO_WritePin(Port, GPIO_Pin, GPIO_PIN_RESET);
    for (int i=0; i<cnt; i++)
    {
        HAL_Delay(delay_value);
        HAL_GPIO_WritePin(Port, GPIO_Pin, GPIO_PIN_SET);
        HAL_Delay(delay_value);
        HAL_GPIO_WritePin(Port, GPIO_Pin, GPIO_PIN_RESET);
    }
} 

Теперь подробнее про сам код. Функция получает 4 параметра:

  • GPIO_TypeDef* Port – ссылка на порт, к которому подключены светодиоды. В нашем случае это GPIOB.
  • uint16_t GPIO_Pin – контакт, к которому подключен светодиод, и которым надо поморгать.
  • uint8_t cnt – количество морганий. В простейшем случае передается «1», и светодиод моргнет 1 раз.
  • uint32_t delay_value – задержка между включением/выключением светодиода в миллисекундах.

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

  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
  HAL_Delay(5000);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
  HAL_Delay(2000);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7|GPIO_PIN_6, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
  HAL_Delay(4000);
  blink_LED(GPIOB, GPIO_PIN_5, 3, 500);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
  HAL_Delay(2000);
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); 

Объяснюсь. Первая строка включает красный светодиод, после чего ждем 5 секунд (5 000 мс задержка). Далее, по истечении этого времени включаем еще и желтый, как предупреждение о том, что скоро включится зеленый. Оба сигнала горят 2 секунды.

Следующая команда гасит красный и желтый сигналы и включает зеленый светодиод, который горит 4 секунды. После того, как это время прошло, запускаем процедуру мигания этим же сигналом, для чего вызываем созданную только что функцию blink_LED, указав в параметре, что количество миганий составляет 3.

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

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

Добавление режима работы «мигающий желтый»

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

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

Выбираем нужную ножку контролера (PA1), указываем режим работы «Input». Подтянем через резистор к общему проводу, и, собственно, все. Теперь можно генерить код.

После того, как открылся проект в Keil, заглянем в функцию MX_GPIO_Init(), в которой произошли изменения. Во-первых, было включено тактирование порта «A»:

  __HAL_RCC_GPIOA_CLK_ENABLE();

Также, добавился такой кусок:

  /*Configure GPIO pin : PA1 */
   GPIO_InitStruct.Pin = GPIO_PIN_1;
   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
   GPIO_InitStruct.Pull = GPIO_PULLDOWN;
   HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); 

Согласитесь, чем-то напоминает ту часть кода, где инициализировались светодиоды. Различия разве что в параметре Mode, где указано, что эта ножка работает на вход, и… все. Теперь, считывая значения с этой ножки, мы можем определить, нажата у нас кнопка или нет.

Добавим в основной цикл программы следующее условие:

  if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1)==GPIO_PIN_SET)
  {                 
      blink_LED (GPIOB, GPIO_PIN_6, 1, 500);
  }
  else {
         /* предыдущий код управления светодиодами */
  } 

Что это означает? Если на момент начала цикла управления светодиодами детектируется, что кнопка нажата, то происходит переход к части кода, где выполняется мигание желтым цветом. Теперь компилируем, заливаем в контроллер и запускаем. Если в момент начала цикла зажигания светодиодов кнопка нажата, то производится переход на мигание желтым светодиодом. При отпускании кнопки осуществляется возврат в обычный режим работы светофора.

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

Для выполнения этой задачи объявим глобальную переменную:

int workmode = 0;

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

Теперь модифицируем код главного цикла следующим образом:

if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1)==GPIO_PIN_SET)
 {
   if (workmode == 1)
   {
     workmode = 0;
   }
   else {
     workmode = 1;
   }
 }
 if(workmode ==0)
   {                 
      blink_LED (GPIOB, GPIO_PIN_6, 1, 500);
   }
   Else {
           /* предыдущий код управления светодиодами */ 
   } 

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

В нашем случае это не столь критично, и подгадать к этому моменту или продержать нажатой кнопку 10-15 секунд большой проблемы не составляет, но, если говорить про реальный светофор, у которого цикл работы может длиться несколько минут, это не очень удобно. Давайте сделаем лучше.

Использование прерываний

Сейчас не буду подробно останавливаться на том, что такое прерывание, но советую обратиться к документации на контроллер, в частности, к Reference Manual, где в разделе 12 «Interrupts and Events» контролера STM32F407, используемого мной (у другой модели контроллера нумерация разделов может быть другая), подробно расписано что за прерывания, как их использовать.

Из этого раздела нас больше интересует следующая иллюстрация:

Программа "Светофор" на STM32

Здесь отмечено, как объединяются выводы портов и на каких прерываниях они работают. Давайте вспомним, что кнопка подключена к контакту 1 порта «A», т. е. «PA1». Согласно схеме, это прерывание EXTI1.

Теперь можно вновь запустить программу CubeMX, открыть наш проект и несколько изменить конфигурацию ножки «1» порта «А», к которой подключена кнопка. Необходимо переключить режим работы с «GPIO_Input» на «GPIO_EXTI1».

Следует заглянуть во вкладку «System view». Выберем раздел «GPIO», отметим контакт «PA1», убедимся, что режим работы «GPIO Mode» – это внешнее прерывание со срабатыванием от фронта сигнала, а если необходимо, то в пункте «GPIO Pull-up/Pull-down» указываем «Pull-down», т. е. в ненажатом состоянии кнопка подключена к общему проводу.

Программа "Светофор" на STM32

Последнее, что осталось сделать – это включить линию прерываний EXTI1. Для этого надо выбрать пункт «NVIC» и отметить галочкой строку «EXTI1 line interrupt».

Теперь можно генерировать код. После запуска среды Keil давайте посмотрим, что же добавилось в программе. На первый взгляд, изменений немного. Так, в функции MX_GPIO_Init(void) изменился режим работы ножки PA1:

GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;

И появились две новые строчки:

  /* EXTI interrupt init*/
   HAL_NVIC_SetPriority(EXTI1_IRQn, 0, 0);
   HAL_NVIC_EnableIRQ(EXTI1_IRQn); 

Собственно, этим включаются прерывания, и все, что осталось сделать – это написать обработчик нашего прерывания.

 Для того создаем функцию:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
 {
    if (GPIO_Pin == GPIO_PIN_1)
    {
        if (workmode == 1)
        {
           workmode = 0; 
        }
        else {
           workmode = 1;
        }
    }
    else{
      __NOP();
    }
 } 

По сути, мы переносим сюда код обработки нажатия кнопки из цикла while(1). Что нам это дает? Давайте немного подкорректируем основной цикл программы, т. е. вместо убранного куска проверки нажатия кнопки мы теперь будем просто проверять состояние переменной workmode. Таким образом, главный цикл теперь выглядит так:

while (1) 
{ 
  if (workmode == 1) 
  { 
     blink_LED(GPIOB, GPIO_PIN_6, 1, 500); 
  } 
  else {
 /******* код управления красным, желтым и зеленым светодиодами *******/ 
}

Только предлагаю сделать еще одну, и последнюю на сегодня доработку – добавим индикацию нажатия кнопки, т. е. при нажатии на нее в том случае, если будет включен режим «1» (мигающий желтый), будет загораться контрольный белый светодиод, а при переходе в обычный режим этот светодиод будет выключен.

Как подключить еще один светодиод? Можно опять воспользоваться CubeMX, выбрать ножку порта, сконфигурить ее уже знакомым способом, сгенерить код и наслаждаться результатом. И все же я не рекомендовал бы злоупотреблять всяческими такими помощниками. Помните, ранее мы рассматривали ту часть программы, которая была создана CubeMX. В частности, ту часть, где настраивались ножки GPIO_PIN_5, GPIO_PIN_6 и GPIO_PIN_7 светодиодов светофора.

Давайте обойдемся без помощников, и сконфигурим ножку порта для контрольного светодиода самостоятельно, благо сделать это совсем нетрудно. Для начала выберем, какую именно ножку – пусть это будет «PD3». Это значит, что нам нужно настроить порт «D». Копируем те несколько строк, в которых производилась настройка порта «B» основных светодиодов, и просто меняем порт на «D», а также указываем ножку «3».

Таким образом, в функцию MX_GPIO_Init(void) добавится следующий кусок кода:

  /*Configure GPIO pins : PD3 */
 GPIO_InitStruct.Pin = GPIO_PIN_3; 
 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; 
 GPIO_InitStruct.Pull = GPIO_PULLDOWN; 
 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM; 
 HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); 

Все, ничего не забыли? Мы же использовали новый порт «D», а включить его тактирование? Забыли! Сделать это можно одной строкой, по аналогии с тем, как у нас включается тактирование портов «B» и «A»:

  __HAL_RCC_GPIOD_CLK_ENABLE();

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

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{ 
  if (GPIO_Pin == GPIO_PIN_1) 
  { 
     if (workmode == 1) 
     { 
        workmode = 0; 
        HAL_GPIO_WritePin (GPIOD, GPIO_PIN_3, GPIO_PIN_RESET); 
     } 
     else { 
        workmode = 1; 
        HAL_GPIO_WritePin (GPIOD, GPIO_PIN_3, GPIO_PIN_SET); 
     }
   }
   else {
       __NOP(); 
   }
}

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

Заключение. Программа Светофор на STM32 – что дальше?

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

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

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

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *