Урок 17. Разработка программ, состоящих из нескольких исходных файлов. Определение и объявление переменных, область видимости, директива extern. Режимы компиляции Debug и Release.

Уроки STM32

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

Предыдущий урок     Список уроков     Следующий урок

Давайте, совместим теорию и практику. Повторим проект Lesson14_2, только обработку сигнала кнопки выполним параллельным процессом. Пока все это будем делать, узнаем много нового.

 

Создаем в в конфигураторе STM32CubeMX новый проект Lesson17_1. В нем все настройки, кроме таймера,  задаем, как в предыдущем уроке:

  • тактирование – на максимальную частоту 72 мГц;
  • PC13 – активный выход;
  • PB13 – активный выход;
  • PB12 – вход с подтягивающим резистором;
  • таймер TIM1 - циклические прерывания с периодом 1 мс.

По сравнению с проектом предыдущего урока внесем одно изменение. Параметр Counter Period установим равным 100.

STM32CubeMX

Открываем проект в 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); // запуск таймера

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

Загрузить проект можно по ссылке:

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Библиотеку Debounce мы подключали в двух файлах. У нас есть заголовочный файл main.h, который включается во все файлы исходного кода. Давайте подключим библиотеку один раз в нем.

Я убрал подключение библиотеки в файлах main.cpp и stm32f1xx_it.cpp и добавил строчку в main.h.

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

#include"../Libraries/Debounce/Debounce.h"

Вот проект с такими изменениями:

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Библиотеки, которые используются только в одном файле исходного текста лучше в нем и подключать. Общие для нескольких файлов библиотеки удобно подключать в заголовочном файле 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.

Atollic TrueStudio

Нажимаем кнопку Manage Configurations.

Выбираем Release. Затем Set Active и Ok.

Установка Release

В строке Configuration выбираем Release.

Atollic TrueStudio

Теперь необходимо для конфигурации  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.

Ссылка для загрузки проекта:

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Мы хотели проверить, как ведет себя компилятор с переменными, которые изменяют сое состояние в обработчике прерывания. У меня все работает. Т.е. оптимизация компилятора в режиме 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:

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

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

Еще я проверил, размеры исполняемого кода для режимов Release и Debug. Они оказались абсолютно одинаковыми.

Размер исполняемого кода

Очевидно у нас такая простая программа, что оптимизировать там нечего.

Но в любом случае на этапе разработки лучше работать с программой в конфигурации Debug, и только окончательный вариант компилировать в режиме Release.

 

В следующем уроке будем задавать конфигурацию таймера и реализовывать обработку кнопки с помощью библиотек HAL и LL.

 

Предыдущий урок     Список уроков     Следующий урок

 

0

Автор публикации

не в сети 2 дня

Эдуард

280
Комментарии: 1936Публикации: 197Регистрация: 13-12-2015

8 комментариев на «Урок 17. Разработка программ, состоящих из нескольких исходных файлов. Определение и объявление переменных, область видимости, директива extern. Режимы компиляции Debug и Release.»

  1. «По сравнению с проектом предыдущего урока внесем одно изменение. Параметр Counter Period установим равным 100.»

    в предыдущем уроке тоже ставили 50 000, потом в комментах оказалось 49 999. тут тоже, наверное, 99, а не 100.

    0
  2. «Для определения внешней переменной используется ключевое слово extern.»
    Исправьте, ведь extern используется только для объявления (внешних) переменных.

    0
  3. Здравствуйте, Эдуард.
    В примере 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 */
    }
    Однако, работает только тот светодиод, чья строчка прописана первой. В чём может быть дело?

    0
  4. Здравствуйте.Изучаю Ваши уроки,спасибо Вам!Почему при компилировании не создается hex файл?Папка Debug>Drivers>STM32F1xx_HAL_Driver>Src>stm32f1xx_hal_rcc_ex.su,а hex-ка нет.В Tool settings выставляю формат hex,а все равно только .su.

    0

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

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

Нажимая кнопку "Отправить" Вы даёте свое согласие на обработку введенной персональной информации в соответствии с Федеральным Законом №152-ФЗ от 27.07.2006 "О персональных данных".