Можете закидать меня объедками, но не могу молчать! Хочу поделиться опытом, который, с моей точки зрения, может быть полезен всем, но по странным необъяснимым причинам как-то не известен широко.
Речь пойдет о GCC - бесплатном компиляторе для разных платформ. Для "больших" систем, наверное, мои советы будут лишними, а вот для AVR, мне кажется, очень хороши. Так же менее интересен этот опыт тем, кто любит и активно использует С++, я же, как истинный ретроград, применяю Си, и хочу поделиться тем, как можно (и нужно!) сделать работы с проектами на этом языке более комфортными.
1. Атрибуты
В GCC есть возможность задавать атрибуты всем элементам программы - функциям, переменным и т.п. Например, всем известен аттрибут, спрятанный в макросе PROGMEM, который заставляет компилятор помещать константы в память программ. Но есть и другие атрибуты, которые могут заметно помочь в некоторых случаях!
задается атрибут при помощи специального макроса __attribute__((А)), где А - это сам атрибут, а остальное - неотъемлемая его часть (обратите внимание на двойные скобки!).
Об атрибутах вообще вы можете прочесть в документации на GCC, а далее я остановлюсь на некоторых конкретных случаях их применения.
2. Секции памяти.
В GCC есть несколько типов секций памяти, задаваемых при помощи атрибута section("название_секции"). Особенно полезными, с моей точки зрения, следующие (указываю конкретное название_секции):
.noinit - все переменные, помещенные в эту секцию, не будут инициализированы "по умолчанию". Как известно, все статические (и глобальные) переменные в Си автоматически инициализируются нулями, есл не указано конкретное значение. Но порой бывает необходимо, чтобы после сброса (например, по WDT) переменная сохранила свое прежнее, досбросовое значение, и эта секция позволяет получить требуемое. Если поместить в этой секции массив, то посчитав CRC этого массива после сброса, можно получить весьма неплохое СЛУЧАЙНОЕ число, по-настоящему случайное, а не псевдослучайное. Чем больше массив, тем лучше случайность. Надеюсь, вы сами понимаете, почему.
.initX, где Х - номер секции от 0 до 9. Это секция для кода. Чтобы было понятно: сразу после сброса выполняется код, помещенный в секцию .init0, потом постепенно номер секции увеличивается, и функция main выполняется в секции 10. То есть пометив атрибутом с указанием определенной секции свою функцию, вы заставите ее выполниться ДО НАЧАЛА main. Только будьте осторожны: параметры стека задаются в секции 2, а до этого стек еще не определен. Я рекомендую использовать секции поближе к main, т.е. 6...9.
Зачем это надо? О! Собственно, ради этого я и затеял эту писанину! Чуть позже расскажу.
.finiX - это "зеркально" обратные секции к только что описанным: в порядке уменьшения своего номера они выполняются ПОСЛЕ ВЫХОДА ИЗ main, то есть сначала код из секции .fini9, потом меньше и меньше, и в 0-й реализуется запрет прерываний и m1: rjmp m1. Почему-то принято считать, что в МК жизни за пределами main нет, и тем более ее нет ПОСЛЕ main, но это, как видите, не так! И, хотя применений этим секциям можно найти меньше, чем инициализирующим, все-таки можно извлечь пользу и из них.
3. Потоковый ввод-вывод.
Об этом я уже писал статью, если интересуетесь - почитайте: WinAVR: консольный ввод-вывод Если кратко, то смысл в том, чтобы заставить работать функции printf и scanf так, как это и было задумано в Си - для вывода и ввода. Как-то прижился подход, когда вместо этих функций используют sprintf и sscanf, т.е. работают со строками, а вот получение этих строк приходится "длать ручками". Если же реализовать потоковый ввод-вывод, а это совсем не сложно! - то можно избавиться и от этого. Поверьте, когда вы работаете с консольным выводом (например, через USART), это просто кардинально упрощает жизнь! Кстати, совсем не лишней будет эта возможность при работе с файлами на SD-карте, что тоже многие применяют в своих проектах.
Ну, а теперь о том, какая польза может быть извлечена из вышеописанных секций.
Буду вести рассказ про себя, а мой опыт каждый может попробовать примерить на себя, чтобы решить, подходит ли, полезен ли он, или нет.
Я часто делаю проекты из кучи модулей. И многие из них работают с периферией. И периферию эту надо настраивать.
Как принято делать? в каждом таком модуле создают функцию init_xxx(), которую затем вызывают из main.
Как делаю я? эту самую функцию init_xxx() я помещаю в секцию .init7 и она вызывается сама! А в main о периферии я вообще не забочусь - все уже подготовлено само!
Какие плюсы я получаю?
Во-первых, код main становится лаконичным и красивым, в нем именно то, что и должно быть: ГЛАВНАЯ работа. Надо лампочками мигать - они мигают, надо файлы читать - они читаются... а вякая ерунда вроде портов и регистров скрыта в модулях.
Как я это делаю?
Кто скачивал мои исходники, наверняка видел там файл avr_helper.h - в нем я определил ряд макросов, при помощи которых все это и делается. Вот самые интересные из них:
Код: Выделить всё
// вспомогательный макрос конкатенации макросов
#define _CONCAT_(x,y) x ## y
/// макрос конкатенации основной
#define CONCAT(y,x) _CONCAT_(y,x)
#define DDR(x) CONCAT(DDR,x)
#define PORT(x) CONCAT(PORT,x)
#define PIN(x) CONCAT(PIN,x)
/// макрос для определения функции-инициализатора (вызывается автоматически в секции .inix)
#define INIT(x) static void __attribute__((naked, used, section(".init" #x))) CONCAT(_init_, __COUNTER__) (void)
/// то же самое для завершающих функций
#define DONE(x) static void __attribute__((naked, used, section(".fini" #x))) CONCAT(_fini_, __COUNTER__) (void)
/// это если лень писать - самая ранняя инициализация
#define AUTOINIT() INIT(2)
/// макрос для определения компактной версии функции main()
#define MAIN() int __attribute__((OS_main)) main(void)
/// неинициализируемые переменные
#define NOINIT __attribute__((section(".noinit")))
Теперь в модуле, который работает с портами ввода вывода, я просто пишу нечто подобное:
Код: Выделить всё
INIT(7){
DDR(PORT_IO) |= _BV(PIN_IO);
}Код: Выделить всё
#define PORT_IO D
#define PIN_IO 2Кстати, в INIT-секциях удобно считывать их EEPROM значения конфигурационных параметров устройства. А вот в секциях DONE иной раз удобно их сохранять. Правда, для этого надо обеспечить 2 условия:
1. обязательное завершение main, т.е. выход из основного цикла
2. тот самый выход должен быть связан с нажатием кнопки "выключения". Т.е. как минимум в проекте должна быть такая кнопка.
Эти условия ограничивают применимость завершающих секций, но если вы делаете проект, управление питанием в котором реализуется через кнопку, заведенную на МК, это так же поможет вам сделать красивый код сохранения настроек, сведя при этом количество записей EEPROM к минимуму.
Знатоки С++ наверняка узнали в секциях .init-.fini аналоги конструкторов и деструкторов...
Ну еще пара слов о моих подходах.
Главное: при инициализации портов и регистров нужно всегда использовать битовые операции ИЛИ и И для установки битов в регистрах периферии, применение прямой записи крайне нежелательно по одной причине: вы не знаете заранее, какой инициализирующий код из нескольких будет выполнен первым, а какой следующим. Но вы знаете, что после сброса все регистры обнулены (точнее - имеют дефолтное значение). Поэтому в инициализации каждого модуля вы обязаны изменять только те биты в регистрах, которые требуются этому модулю, сохраняя остальные неизменными.
Это, конечно, слегка раздувает код "лишними" операциями над всякими DDRx и т.п., но это небольшое зло, как мне кажется, с лихвой компенсируется удобством программирования.
И еще одно. Задавая разные номера для макроса INIT, вы можете управлять порядком вызова инициализации. Разумеется, что код в INIT(7) будет выполнен раньше, чем INIT(8). Просто помнить номера инициализаций в нескольких модулях сложновато. Я завел себе правило: если мне надо просто проинициализировать модуль, я использую секцию 7. Если надо гарантировать превентивность инициализации - 6. Если надо гарантировать "запоздалость" - 9. Иных вариантов стараюсь избегать.
Вот и все.
Буду рад, если кому-то открыл глаза или помог стать лучшим программистом, чем ранее.
Если хотите, у могу еще рассказать о том, как можно делать трассировку кода удобно, но накладно по ресурсам... Это полезно, если МК большой, а аппаратного отладчика нет...


