- Родительская категория: Статьи
- Категория: Программирование
- Автор: ARV
- Просмотров: 20413
Нисходящее программирование на простом примере
Нисходящее программирование – это метод разработки программного обеспечения по принципу от общего к частному. Однако этого определения явно недостаточно для программиста-любителя, который не проходил специального обучения и осваивает тонкости программистских технологий самостоятельно.
С тем, что процесс "придумывания" программы вызывает у многих начинающих любителей сложности, я знаю не понаслышке: ко мне нередко обращаются с вопросами по этой теме по почте или на форумах. Поэтому я постараюсь на простом примере объяснить сущность и полезность этого принципа.
Написание программы для микроконтроллера имеет конечной целью реализацию какого-то устройства, помогающего человеку в реальной жизни. То есть можно сказать, что на программу возлагается решение каких-то жизненных проблем пользователя. Поэтому рассмотрение принципов нисходящего программирования я начну именно с жизненных ситуаций.
Допустим, вы заглянули в холодильник и обнаружили наличие отсутствия колбасы. Возникла задача: пополнить запасы колбасы. Какое решение этой задачи пришло вам на ум? Конечно, пойти в магазин и купить колбасы. Таким образом, вы сформировали общий алгоритм решения – сходить и купить, т.е. задача разбита на две подзадчи «сходить» и «купить».
Теперь перед вами неизбежно возникают «нюансы», ведь за словом «сходить» может крыться и «съездить» (если ближайший магазин закрыт), и даже «позвонить жене/мужу и попросить купить по дороге домой» - в общем, разные варианты. И даже если вы определились с личным походом, прежде чем покинуть дом, вам придется определиться с тем, что надеть, проверить наличие финансов и т.п. То есть укрупненная задача неизбежно распадается на ряд малых, а те в свою очередь могут быть разбиты на еще более мелкие и так до тех пор, пока, наконец, окончательное принятие решения уже будет у вас на интуитивном уровне, т.е. не требующем детализации.
Взрослый человек решает все эти проблемы мгновенно, не разделяя на крупные и мелкие задачи, но, если вы пытаетесь привлечь к походу в магазин ребенка, вы просто вынуждены объяснить ему все-все в деталях: от сорта колбасы до того, кеды или сапоги ему надеть – чем меньше возраст, тем детальнее объяснения. Собственно, в этом и заключается принцип нисходящего программирования: уже на этапе общей постановки задачи мы видим некие крупные шаги, позволяющие нам сделать вывод о возможности решения в принципе, не вникая в детали, а при необходимости мы только уточняем отдельные моменты.
Попробуем теперь перенести сказанное на ряд примеров из реальной практики любительского программирования. При этом будем стараться на каждом этапе не сосредотачиваться на несущественных или пока непонятно как решаемых проблемах, считая априори, что для них существует решение, а мы просто пользуемся его результатами. То есть используем возможность языка программирования оформлять определенные действия в виде функций: для пока не детализированной задачи будем определять отдельную функцию-пустышку, и будем считать, что позже мы заставим ее делать реально полезные вещи.
Пример: Морзе-маячок.
Постановка задачи: требуется разработать программу для радиомаяка, подающего сигнал в виде сообщения из 4 символов азбуки Морзе каждую минуту.
Решение.
Самое интуитивно простое решение задачи заключается в следующем:
- Выдать сообщение
- Подождать 1 минуту
- Перейти к п.1
Это и есть первый этап разработки – общее укрупненное решение. Запишем его, пользуясь возможностями языка Си в виде главной функции нашей программы:
{code}void send_message(void){
// пока мы не знаем, как, но эта функция передает сообщение
}
void wait(int sec){
// пока не знаем, как, но эта функция задерживает исполнение программы на sec секунд
}
int main(void){
// не забыть добавить в этом месте инициализацию периферии!
while(1){
send_message();
wait(60);
}
}{/code}
Я всегда начинаю писать код с того, что мне известно и понятно, добавляя в него в виде функций-пустышек то, что пока неизвестно или непонятно. Но здесь я привожу код в том виде, как он уже получился, т.е. сначала те самые неизвестно-непонятные функции, а потом уже то, что ясно-понятно. Так будет на протяжении всей статьи – начинайте просмотр врезок кода с конца .
Эта «программа» уже может быть скомпилирована, что является определенным плюсом нисходящего программирования – по мере разработки мы на любом этапе можем проверить, нет ли ошибок в нашем алгоритме путем компиляции. Очевидно, что ошибок в этой программе нет. Наступает следующий этап – детализация имеющихся функций. В этом случае нам, разумеется, поможет знание стандартных библиотек, имеющихся в нашем распоряжении. В частности, в библиотеках WinAVR имеется функция _delay_ms, позволяющая задержать исполнение программы на заданное количество миллисекунд – воспользуемся ею для реализации функции wait в нашей программе:
{code}#include
void wait(int sec){
for(; sec; sec--)
_delay_ms(1000);
}{/code}
Надеюсь, в отдельных комментариях данная функция не нуждается, как и не нуждается в дальнейшей детализации.
Теперь беремся за send_message, причем ровно с тех же позиций: теперь задача «передача сообщения» для нас является главной. Сообщение – это 4 символа. Очевидно, что сообщение передается укрупненно так (сразу записываю на Си):
{code}void send_morse_symbol(char c){
// пока не знаем как, но эта функция передает один символ азбуки Морзе
}
void send_message(void){
static char msg[] = "TEST";
for(uint8_t i=0; i<4; i++)
send_morse_symbol(msg[i]);
}{/code}
Все понятно – берем поочередно каждый символ сообщения и при помощи функции send_morse_symbol передаем его.
Прежде чем идти дальше, задумаемся: все ли сделано для того, чтобы наша программа была качественной? Мое личное мнение – нет, надо сразу сделать несколько вещей:
- Избавиться от магических чисел
- Сгруппировать константы, чтобы в будущем легко можно было их модифицировать
- Сделать код максимально универсальным, независимым от остальной части кода.
Хоть данные меры и не имеют прямого отношения к нисходящему программированию, но пренебрегать ими не стоит. Итак, в нашей функции есть магическое число 4, которое определяет количество передаваемых символов. Самое простое – определить константу и использовать ее, но будет ли это наилучшим решением? Ведь сейчас мы хотим передавать 4 символа, а завтра пожелаем 5, потом 10 или 2 – разве не логичнее использовать возможность автоматического определения количества символов в передаваемом сообщении? По-моему, это разумно. Поэтому наша функция слегка преображается, а следом преображается и ранее сделанные функции:
{code}#include
void send_message(char *s){
for(uint8_t i=0; i
send_morse_symbol(s[i]);
}
int main(void){
// не забыть добавить в этом месте инициализацию периферии!
while(1){
send_message("TEST");
wait(60);
}
}{/code}
Путем передачи строки-сообщения, как параметра функции send_message, мы решили сразу все проблемы! Правда, чтобы использовать стандартную функцию определения длины строки strlen, пришлось подключить стандартную библиотеку string. По тексту статьи я пишу директивы include близко к тексту, где они потребовались, но, разумеется, их желательно вводить в самом начале файла.
При желании можно скомпилировать то, что уже написано, и, если оптимизация отключена, даже убедиться в отладчике, что все работает именно так, как хотелось.
Наступает пора приступать к очередной итерации: детальному разбору функции передачи одного символа азбуки Морзе. Это далеко не так просто, как может показаться, т.к. азбука Морзе – это довольно неструктурированная система: каждый символ может кодироваться от 1 до 6 знаками «точка» и/или «тире». Нужно придумать, как эту азбуку «ввести» в программу.
Азбука – это массив букв. Буква – это комбинация знаков «точка» и «тире». Кстати, при передаче буквы между знаками должна делаться пауза длительностью в «точку», а между отдельными символами – длительностью в «тире». Знаков в букве может быть не более 6. Так как вариантов знаков всего 2, напрашивается «бинарное» кодирование, не так ли? Пусть бит, равный 0 означает «тире», а равный 1 – «точку». Таким образом, приходим к тому, что для кодирования любой буквы азбуки Морзе достаточно 1 байта. Но еще надо знать, сколько этих самых точек и тире в букве – то есть, приходим к необходимости описать буквы азбуки в виде структуры:
{code}typedef struct{
uint8_t len; // количество знаков
uint8_t code; // собственно точки и тире
} MORSE_CHAR;{/code}
Теперь вся азбука опишется нами в виде массива:
{code}static MORSE_CHAR abc[‘Z’-‘A’+1];{/code}
Чтобы не раздувать статью до невероятных размеров, я во-первых, ограничусь только заглавными буквами латинского алфавита, во-вторых, не стану «заполнять» массив конкретными структурами, а в-третьих, размещу массив в ОЗУ, хотя для микроконтроллера логичнее разместить его во flash. Надеюсь, эти допущения не помешают вам понять дальнейшее изложение.
В дальнейшем примем, что знаки в букве азбуке (поле code структуры MORSE_CHAR) кодируются битами в порядке «первый – младший», т.е. справа налево. То есть символ «точка – тире» будет закодирована двоичным байтом 0b00000010.
Теперь обдумаем алгоритм передачи буквы:
- Извлечь из массива abc структуру, соответствующую передаваемому символу
- Организовать цикл с количеством итераций, определяемым полем len этой структуры
- В теле цикла перебирать биты поля code, начиная с младшего, и в зависимоти от значения бита передавать «точку» или «тире»
- Между каждым кодом делать паузу, равную длительности точки
- После завершения цикла сделать паузу, равную длительности тире.
Как видите, и тут мы сталкиваемся с необходимостью определять некоторые константы – как минимум, для длительности точки и тире. Известно, что тире в три раза длиннее точки, а длительность точки определяется скоростью передачи. Для упрощения будем задавать не скорость передачи, а длительность точки в миллисекундах, а остальное будет получаться автоматически:
{code}#define PUNKT_T 100
#define TIRE_T (PUNKT_T * 3){/code}
Теперь можно написать реализацию функции send_morse_symbol:
{code}void sound(uint16_t d){
// пока не знаем как, но эта функция передает «звук» d миллисекунд
}
void silence(uint16_t d){
// пока не знаем как, но эта функция передает «тишину» d миллисекунд
}
void send_morse_symbol(char c){
MORSE_CHAR tmp;
uint8_t mask = 0x01;
tmp = abc[c – ‘A’];
for(uint8_t i=0; i
if(tmp.code & mask)
sound(PUNKT_T);
else
sound(TIRE_T);
silence(PUNKT_T);
mask <<= 1;
}
silence(TIRE_T);
}{/code}
Вроде бы все учтено, все соответствует плану, все компилируется и отлаживается. Задача почти решена – остается только решить, как именно передавать звук и тишину. Функция передачи тишины – это просто задержка с ничегонеделанием, а передача звука будет зависеть от конкретной схемы маячка. Если вы используете просто амплитудную или частотную манипуляцию несущей передатчика, то все сведется просто к установке нужного логического уровня на выбранном пине одного из портов микроконтроллера, а если вы передаете уже модулированный звуковой сигнал, потребуется управлять таймером, настроенным на режим генерации частоты… В общем, давайте оставим этот вопрос «за кадром», ведь он наверняка особых сложностей у вас не вызовет.
Подведем итог. Вот что вышло у нас:
{code}#define POINT_T 100
#define TIRE_T (PUNKT_T * 3)
typedef struct{
uint8_t len; // количество знаков
uint8_t code; // собственно точки и тире
} MORSE_CHAR;
static MORSE_CHAR abc[‘Z’-‘A’+1]; // не забыть про заполнение массива!
void sound(uint16_t d){
// включить звук
_delay_ms(d);
// выключить звук
}
void silence(uint16_t d){
_delay_ms(d);
}
void send_morse_symbol(char c){
MORSE_CHAR tmp;
uint8_t mask = 0x01;
tmp = abc[c – ‘A’];
for(uint8_t i=0; i
if(tmp.code & mask)
sound(TIRE_T);
else
sound(POINT_T);
silence(POINT_T);
mask <<= 1;
}
silence(TIRE_T);
}
void send_message(char *s){
for(uint8_t i=0; i
send_morse_symbol(s[i]);
}
void wait(int sec){
for(; sec; sec--)
_delay_ms(1000);
}
int main(void){
// не забыть добавить в этом месте инициализацию периферии!
while(1){
send_message(“TEST”);
wait(60);
}
}{/code}
Как видите, начав с общего мы, постепенно углубляясь, решили задачу, при этом на всем пути никаких принципиальных сложностей не испытывали, при этом в любой момент имели готовую к компиляции программу (удобно для отладки). Что еще осталось сделать, чтобы довести программу почти до совершенства?
Так как программа у нас вышла в виде одного модуля, разумным будет объявить все функции static. Разумеется, надо не забыть о периферии. Если при компиляции вы получите слишком большой объем прошивки, это будет связано с передачей в функцию _delay_ms параметра в виде переменной, а не константы, - в этом случае придется заменить функции sound и silence на макросы с параметрами (это несложно). Ну и, конечно, надо правильно заполнить массив Морзе-алфавита, причем желательно занести его во flash.
Пример учебный, упрощенный, но, надеюсь, вы уловили суть нисходящего принципа программирования и теперь при решении своих задач будете испытывать значительно меньше проблем. Не надо бояться «появления» большого числа функций – при включенной оптимизации вся эта кажущаяся избыточность будет устранена компилятором.
Успехов!
Комментарии
как самоучка, я часто испытываю трудности при разработке алгоритма работы некоего устройства, а потом, когда худо-бедно составлю кривоватый алгоритм то не могу его перевести в команды для AVR. Если на то будет Ваша милость, ПОЖАЛУЙСТА, напишите еще пароочку статей в таком же стиле, как написать прогу от задумки идеии до команд мк.
что касается статей - мне очень сложно придумать, о чем нужно писать, т.к. я уже довольно далеко ушел от начинающих, и не всегда могу представить себе их проблемы...
во-первых, наличие присутствия чувства юмора - это важно даже для программиста.
во-вторых, что именно вам не понятно в статье? конкретно, чтобы я учел на будущее.
RSS лента комментариев этой записи