Создадим программу обработки сигнала кнопки в фоновом режиме. Научимся связывать между собой переменные, размещенные в разных исходных файлах программы. Попутно затронем несколько важных тем, связанных с выполнением кода в обработчике прерывания. Научимся компилировать программу в конфигурациях Debug и Release.
Предыдущий урок Список уроков Следующий урок
Давайте, совместим теорию и практику. Повторим проект Lesson14_2, только обработку сигнала кнопки выполним параллельным процессом. Пока все это будем делать, узнаем много нового.
Создаем в в конфигураторе STM32CubeMX новый проект Lesson17_1. В нем все настройки, кроме таймера, задаем, как в предыдущем уроке:
- тактирование – на максимальную частоту 72 мГц;
- PC13 – активный выход;
- PB13 – активный выход;
- PB12 – вход с подтягивающим резистором;
- таймер TIM1 - циклические прерывания с периодом 1 мс.
По сравнению с проектом предыдущего урока внесем одно изменение. Параметр Counter Period установим равным 100.
Открываем проект в Atollic TrueStudio.
Мы собираемся использовать библиотеку Debounce, значит, проект надо преобразовать в C++. Необходимо выполнить 3 пункта (урок 11).
Мы будем использовать библиотеку. Необходимо ее подключить (урок 14). Для этого:
- Копируем в проект папку Libraries с нашими библиотеками (пока там только одна).
- Указываем в IDE путь к исходным файлам библиотеки (урок 14).
Уже в предыдущем уроке мы начали работать со вторым файлом исходного кода. Это был файл stm32f1xx_it.cpp. Он предназначен для размещения функций обработки прерываний.
Но в предыдущем проекте мы в обработчике прерывания вызывали стандартную HAL-функцию. На это раз нам надо использовать класс Debounce и сразу в двух файлах main.cpp и stm32f1xx_it.cpp. Этого мы еще делали.
Подключаем библиотеку в main.cpp
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include"../Libraries/Debounce/Debounce.h"
Но мы собираемся использовать ее и в файле stm32f1xx_it.cpp. Надо подключить библиотеку и там.
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include"../Libraries/Debounce/Debounce.h"
Если этого не сделаем, то компилятор напомнит об ошибке при трансляции.
Создаем объект для кнопки в файле main.cpp.
/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim1;
Debounce button(GPIOB, 1 << 12, 10); // экземпляр класса Debounce
Но объект button не описан в stm32f1xx_it.cpp.
Если мы повторим предыдущее определение объекта, то это будет уже другой объект. А нам надо работать с одним и тем же.
Определение и объявление.
Это не всегда одно и то же.
- Определение переменной, функции, объекта означает предоставление компилятору полной информации, необходимой для его создания. Определение функции – это код с ее телом, определение класса означает предоставление всех методов и свойств и т.п. Как только что-то определено, оно также считается объявленным. Поэтому мы раньше не разделяли эти понятия.
- При объявлении мы только сообщаем компилятору, что есть что-то определенного типа с определенным именем. Это позволяет разрабатывать код, понятный компилятору, опуская не нужные детали.
Часто объявления в чистом виде используются для работы с одними и теми же переменными в разных файлах. Еще одним примером могут служить прототипы функций. Мы размещаем полный код функции в конце исходного текста. Компилятор должен выдать ошибку, т.к. встретил в исходном тексте вызов функции, о которой он ничего не знает. Но в начале программы мы объявляем функцию. Размещаем ее прототип без тела, и компилятору этого достаточно.
Определение должно быть всегда одно, а объявлений может быть несколько.
Область видимости переменных. Ключевые слова static и extern.
Об этом мы говорили в уроках Ардуино. Давайте немного расширим знание этого вопроса.
- Переменные, объявленные внутри блока, считаются локальными. Имеют локальную область видимости, т.е. в пределах блока. Они создаются в месте определения и уничтожаются при выходе из блока.
- Переменные, объявленные вне блока, обычно в верхней части кода, называются глобальными. Они создаются при компиляции программы и имеют статическую продолжительность. Т.е. всегда существуют, никогда не уничтожаются.
В уроках Ардуино мы говорили, что глобальные переменные можно использовать в любом месте программы, что их область видимости неограниченна. Но программы Ардуино, как правило, состоят из одного файла с исходным текстом. Директива #include не в счет. Она просто позволяет разделить файл с кодом на части.
Глобальные переменные имеют файловую область видимости. Их можно использовать в любом месте файла, в котором они объявлены. Но не в других файлах.
Кроме области видимости и продолжительности, у переменных есть еще одно свойство – связь. Оно определяет, относятся ли несколько упоминаний одного идентификатора (имени) к одной и той же перемененной или нет.
- Локальная переменная – переменная без связей. Например, две переменные с одинаковыми именами, объявленные в разных функциях, не имеют никакой связи. Это разные переменные. Просто их назвали одинаковыми именами.
- Внутренняя или статическая переменная может использоваться в любом месте файла, в котором она определена. Но переменная с тем же именем, определенная в другом файле – это уже другая переменная.
- Внешняя переменная доступна, как в файле, в котором она определена, так и в других файлах проекта.
Т.е. понятие того, что мы раньше называли глобальной переменной, разделилось на внешнюю и внутреннюю (статическую) переменные.
- Для определения внутренней (статической) переменной используется ключевое слово static.
- Для определения внешней переменной используется ключевое слово extern.
- По умолчанию:
- неконстантные переменные, объявленные вне блока, считаются внешними.
- константные переменные считаются внутренними.
- Перед использованием внешней глобальной переменной, определенной в другом файле, она должна быть объявлена через ключевое слово extern.
Добавлю для полноты картины, что функции имеют такие же свойства связи. По умолчанию они внешние, но с помощью ключевого слова static можно изменить их на внутренние. Отличие заключается в том, что объявления функций не нуждаются в слове extern. Компилятор определяет, что это функция или ее прототип по наличию тела.
Вернемся к нашей программе.
В файле main.cpp мы сделали определение объекта. Предоставили компилятору все данные о нем, и компилятор создал объект, выделил для него необходимую память.
Debounce button(GPIOB, 1 << 12, 10); // экземпляр класса Debounce
Объект получился внешним, с глобальной областью видимости.
В файле stm32f1xx_it.cpp мы делаем объявление (не определение) того же объекта button. С помощью директивы extern сообщаем компилятору, что объект был создан где-то в другом месте.
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
extern Debounce button;
В прерывании по таймеру вызываем метод обработки сигнала кнопки.
void TIM1_UP_IRQHandler(void) {
/* USER CODE BEGIN TIM1_UP_IRQn 0 */
button.scanAverage(); // обработка сигнала кнопки
/* USER CODE END TIM1_UP_IRQn 0 */
HAL_TIM_IRQHandler(&htim1);
/* USER CODE BEGIN TIM1_UP_IRQn 1 */
/* USER CODE END TIM1_UP_IRQn 1 */
}
Остается в главном цикле проверять флаг нажатия кнопки и инвертировать по нему состояние светодиода.
while (1) {
/* USER CODE END WHILE */
if(button.readFlagFalling() != 0)
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
/* USER CODE BEGIN 3 */
}
Еще надо не забыть разрешить работу таймера.
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1); // запуск таймера
Обработка сигнала кнопки происходит в фоновом режиме. В главном цикле мы работаем с признаками состояния сигнала, очищенными от помех, дребезга и т.п.
Загрузить проект можно по ссылке:
Библиотеку Debounce мы подключали в двух файлах. У нас есть заголовочный файл main.h, который включается во все файлы исходного кода. Давайте подключим библиотеку один раз в нем.
Я убрал подключение библиотеки в файлах main.cpp и stm32f1xx_it.cpp и добавил строчку в main.h.
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include"../Libraries/Debounce/Debounce.h"
Вот проект с такими изменениями:
Библиотеки, которые используются только в одном файле исходного текста лучше в нем и подключать. Общие для нескольких файлов библиотеки удобно подключать в заголовочном файле main.h.
Режимы Debug или Release.
В нашей программе обработка сигнала кнопки и реакция на результат происходят параллельными процессами. Первая задача выполняется в обработчике прерывания таймера, вторая в основном цикле.
Компилятор может захотеть оптимизировать код программы. И что ему придет на ум не известно.
Признаки состояния кнопки изменяются в моменты прерывания таймера. Это аппаратные события. Компилятор не способен их предсказать. Поэтому он вполне может решить, что признаки остаются все время постоянными. Тогда зачем их проверять. В результате компилятор может выбросить код проверки признаков кнопки из основного цикла.
Оптимизация происходит по-разному в зависимости от установок компилятора. В большей мере на это должен повлиять режим компиляции: Debug или Release.
Главное отличие режимов – это назначение.
- Режим Debug предназначен для использования на этапе разработки и отладки программ.
- Release – применяется для окончательной сборки программы и последующего ее использования.
В режиме Release из кода удаляется отладочная информация. Но при этом на этапе компиляции исключаются дополнительные проверки.
В результате в режиме Release:
- исполняемый код программы короче;
- программа может работать быстрее;
- происходит оптимизация кода.
Но:
- Могут возникнуть новые ошибки, если код недостаточно корректно написан.
- Исключены некоторые проверки кода.
- Оптимизация может привести к дополнительным ошибкам, связанным с параллельными процессами.
Давайте соберем наш последний проект в режиме Release и проверим, как будет работать программа. Заодно научимся получать исполняемый код в такой конфигурации компилятора.
Я копировал проект с именем Lesson17_3.
Запустил компиляцию.
В результате в папке Debug появился файл Lesson17_3.hex. Это была компиляция в режиме Debug.
Настроим проект на режим Release.
Открываем: Project -> Build Settings -> Tool Settings.
Сверху строчка с выпадающим меню, которая говорит, что активен режим Debug.
Нажимаем кнопку Manage Configurations.
Выбираем Release. Затем Set Active и Ok.
В строке Configuration выбираем Release.
Теперь необходимо для конфигурации Release копировать установки из закладок C Compilier в C++ Compilier. Так мы делали в уроке 11, когда конвертировали проект в C++.
- C Compilier -> Symbols копировать в C++ Compilier -> Symbols;
- C Compilier -> Directories копировать в C++ Compilier ->Directories.
Остается указать компилятору путь к библиотеке Libraries.
- Правой кнопкой мыши по Libraries.
- Properties -> C/C++ General -> Paths and Symbols -> Source Location -> Add Folder
- Libraries -> OK
Теперь компилируем проект. Выходной код должен оказаться в папке Release. Смотрим.
Если вы не увидите в этом списке файла с расширением hex, то необходимо установить:
- Project -> Build Settings -> Tool Settings -> Output format
- Птичка Convert build output.
- Выбрать Format Intel Hex.
Загружаем в плату, проверяем. Напомню, что исполняемый код в папке Release, а не в Debug.
Ссылка для загрузки проекта:
Мы хотели проверить, как ведет себя компилятор с переменными, которые изменяют сое состояние в обработчике прерывания. У меня все работает. Т.е. оптимизация компилятора в режиме Release не испортила нашу программу.
Но, если изменить настройки оптимизации, то могут быть проблемы.
Поэтому я подправил библиотеку Debounce. Решил отметить признаки и функции чтения признаков квалификатором volatile.
class Debounce {
public:
uint8_t readFlagLow(void) volatile; // чтение признака СИГНАЛ В НИЗКОМ УРОВНЕ
uint8_t readFlagRising(void) volatile; // чтение признака БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
uint8_t readFlagFalling(void) volatile; // чтение признака БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ
volatile uint8_t flagLow; // признак СИГНАЛ В НИЗКОМ УРОВНЕ
volatile uint8_t flagRising; // признак БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
volatile uint8_t flagFalling; // признак БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ
По задумке теперь компилятор никогда не будет оптимизировать отмеченные переменные.
Вот откорректированная библиотека Debounce:
Если загружали библиотеку раньше времени написания этого урока, то лучше сделайте это заново.
Еще я проверил, размеры исполняемого кода для режимов Release и Debug. Они оказались абсолютно одинаковыми.
Очевидно у нас такая простая программа, что оптимизировать там нечего.
Но в любом случае на этапе разработки лучше работать с программой в конфигурации Debug, и только окончательный вариант компилировать в режиме Release.
В следующем уроке будем задавать конфигурацию таймера и реализовывать обработку кнопки с помощью библиотек HAL и LL.
Предыдущий урок Список уроков Следующий урок
Эдуард. Жду не дождусь новых уроков.
«По сравнению с проектом предыдущего урока внесем одно изменение. Параметр Counter Period установим равным 100.»
в предыдущем уроке тоже ставили 50 000, потом в комментах оказалось 49 999. тут тоже, наверное, 99, а не 100.
«Для определения внешней переменной используется ключевое слово extern.»
Исправьте, ведь extern используется только для объявления (внешних) переменных.
Да, конечно, вы правы.
Здравствуйте, Эдуард.
В примере 17.3 захотелось управлять тремя светодиодами от одной кнопки, но от разных портов. Думалось, будет работать так:
while (1)
{
/* USER CODE END WHILE */
if(buttonS.readFlagFalling() != 0)
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_3);//Светодиод2
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1);//Светодиод1
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);//Светодиод0
/* USER CODE BEGIN 3 */
}
Однако, работает только тот светодиод, чья строчка прописана первой. В чём может быть дело?
Здравствуйте!
У вас оператор if действует только на первую после него строку. Возьмите блок с управлением светодиодами в фигурные скобки.
Спасибо!!!
Здравствуйте.Изучаю Ваши уроки,спасибо Вам!Почему при компилировании не создается hex файл?Папка Debug>Drivers>STM32F1xx_HAL_Driver>Src>stm32f1xx_hal_rcc_ex.su,а hex-ка нет.В Tool settings выставляю формат hex,а все равно только .su.