Этот пост не относится конкретно к какой-то платформе, но примеры даны именно для stm32f4, ибо stm32 много у кого есть.
Раньше я особо не задумывался над структурой программ. Каждая программа была сделана по-разному, была весьма запутанной и со множеством очевидных и не очень связей между частями. Жуть. Между тем, использовать операционные системы не очень хотелось.
За год работы в ADL я всё же выработал для себя достаточно удобное представление кода в проекте.
читать дальшеОсновные принципы вполне очевидны и широко используются:
- Инкапсуляция. Все модули сами по себе, они интроверты. Как и что они делают - это их личное дело. Общаемся только через предоставленные ими функции. Никаких глобальных переменных а ля extern uint8_t dmx_buffer[512];
- Не засоряем пространство имён. Всё, что не нужно остальному коду, отмечаем как static. Всякие структуры, не нужные вне модуля, объявляем в отдельных файлах, подключаемых только к нему. Опять же, никаких глобальных переменных для связи между модулями;
- Многоуровневый код. Алгоритм работает независимо от периферии и ничего о ней не знает. Общаемся только через драйвера;
- Обратные связи через callback-процедуры. Если нам надо получить извещение о наступлении какого-либо события, то скажем драйверу, какую функцию вызывать, а дальше он сам всё в нужный момент сделает. Нет причин его постоянно опрашивать.
Практически любая программа может быть разделена на три уровня:
- драйвера, которые непосредственно управляют периферией (портами, таймерами, интерфейсами типа UART, SPI и т.д.);
- драйвера внешних устройств, которые работают через драйвера периферии (уже никаких #include или не допускаем) и предоставляют доступ ко всем устройствам на плате (светодиодам, кнопкам, микросхемам и т.д.), вся завязка на номера выводов и ножки именно здесь;
- пользовательская программа, реализующая конечный алгоритм через эти два уровня драйверов.
Показываю на структуре прилагаемого проекта:
./main.c - основной код, запуск инициализации в нужном порядке, главный цикл;
./drivers.c - инициализация драйверов периферии в нужном порядке (сначала базовые типа gpio и таймеров, потом более сложные типа spi и uart, которым gpio и таймеры могут быть нужны);
./bsp.c - инициализация драйверов внешних устройств, в данном случае драйвера светодиодов и кнопки;
./drivers/system.c - включение/выключение прерываний;
./drivers/systick.c - системный таймер. С заданной периодичностью может вызывать функции;
./drivers/gpio.c - драйвер портов ввода/вывода, позволяет настраивать как вход, выход, задавать альтернативную функцию (для других драйверов), задавать уровень на ножке и считывать её состояние;
./bsp/led.c - драйвер светодиодов. Работает через gpio.c и знает только, что светодиоды сидят на порту D и выводах 12-15. Его задача - предоставить доступ к светодиодам по независимым от платы номерам: от 0 до 3. Включать их и выключать по требованию основного алгоритма. При этом наличие или отсутствие отдельных светодиодов никак не повлияет на работу алгоритма в целом;
./bsp/button.c - драйвер кнопки. Периодический опрос и фиксация нажатия;
./main/test.c - тестовая программа. Моргает светодиодом с частотой 10 Гц и по нажатию на кнопку меняет текущий светодиод.
С CMSIS общаются только файлы, находящиеся в папке drivers. Остальные даже не знают, на каком контроллере они работают. И при сохранении интерфейса драйверов код можно перенести, например, на LPC23xx. Собственно, я проделал обратное. Почти все эти интерфейсы таймеров и GPIO взяты из проекта под LPC2368, а ранее из проекта под atmega1280.
В общем-то тут всё понятно. Из интересного есть только таймер. Так как множество процессов в мире микроконтроллеров являются периодическими (моргание светодиодом, опрос датчиков, перерисовка экрана, динамическая индикация) логично прийти к тому, что надо сделать механизм для периодического вызова отдельных функций.
Минимальный интерфейс имеет всего две функции:
Всё. Когда таймер отсчитает нужный интервал, он его сбросит для начала нового отсчёта и вызовет функцию Handler.
Раньше функция вызывалась прямо из прерывания, но я посчитал, что в большинстве случаев это нерационально: функции могут быть достаточно продолжительными, а прерывания надолго глушить не хочется. Потому в прерывании просто устанавливается флаг, а проверяется он, и вызываются обработчики в отдельной функции, которая должна быть в главном цикле:
Такой подход ещё и исключает конфликты, связанные с порядком доступа к памяти, которые могут возникнуть, если вызывать напрямую из прерывания.
Между разными контроллерами различия только в запуске таймера и в обработчике прерывания.
Сам таймер устроен примерно так:
Использование тоже не отличается сложностью.
В прилагаемом примере моргает с частотой 10 Гц светодиод. При нажатии кнопки моргать будет следующий. Если кнопку не отпускать, он так и будет бегать по кругу.
Весь алгоритм работы представлен всего двумя функциями:
Каждый модуль может запустить такие процессики для контроля своего состояния. Светодиоды могут моргать. Экран может перерисовываться. Датчики могут опрашиваться.
А остальной код и знать об этом не будет. Это не его дело, в конце-концов.
Точно так же надо делать и всё остальное. Например, кнопки. Не дело главной программы фильтровать дребезг и прочую ерунду. Она только должна сказать: при нажатии кнопки вызови этот обработчик. А драйвер кнопок уже отфильтрует дребезг, зафиксирует нажатие, и при долгом нажатии сможет периодически вызывать обработчик.
И даже если вдруг кнопки станут абсолютно другими: сенсорными, например, или появится микруха, которая эти кнопки обслуживать будет по I2C, то основная программа от этого не изменится! Изменится только сам файл button.c.
Ну и в любом случае приятно, когда алгоритм не загромождён всякими не относящимися к его задаче проверками.
Суть изоляции алгоритма от физической привязки к номерам выводов:
Если светодиод куда-то переедет, то будет достаточно поменять всего три символа, чтобы всё снова заработало.
Да и led_On(LED_LEFT) куда как понятнее, чем gpio_HighLevel(PORTD, 12) или GPIOD->BSRR = GPIO_BSRR_BS_12.
UPD от 14.05.2013: я в итоге решил, что передавать указатель на структуру, в которой указан номер порта и вывода логичнее и нагляднее:
Интересные файлы:
gpio.c
systick.c
led.c
button.c
test.c
Проект целиком.
<< Предыдущее Следующее >>
STM32F4. Структура программы
Этот пост не относится конкретно к какой-то платформе, но примеры даны именно для stm32f4, ибо stm32 много у кого есть.
Раньше я особо не задумывался над структурой программ. Каждая программа была сделана по-разному, была весьма запутанной и со множеством очевидных и не очень связей между частями. Жуть. Между тем, использовать операционные системы не очень хотелось.
За год работы в ADL я всё же выработал для себя достаточно удобное представление кода в проекте.
читать дальше
<< Предыдущее Следующее >>
Раньше я особо не задумывался над структурой программ. Каждая программа была сделана по-разному, была весьма запутанной и со множеством очевидных и не очень связей между частями. Жуть. Между тем, использовать операционные системы не очень хотелось.
За год работы в ADL я всё же выработал для себя достаточно удобное представление кода в проекте.
читать дальше
<< Предыдущее Следующее >>