Оформление текста программ
Как известно, людям свойственно ошибаться. Особенно это верно в отношении программистов. По некоторым оценкам ядро системы Linux содержит порядка 15000 пока еще не исправленных ошибок (источник). Практически любой написанный вами текст программы будет содержать ошибки и значительная часть времени будет уходить на их поиск и исправление.
Отсюда вытекает преставление о стиле программирования. Существуют определенные правила написания программ, соблюдение которых позволяет уменьшить вероятность появления ошибок. Программы (особенно такие простые, как вам придется писать на начальном этапе) могут правильно работать и при нарушении этих правил, однако все же следует их соблюдать. Дополнительное время и усилия, которые будут на это потрачены, многократно окупятся впоследствии.
Простейшее, но при этом очень важное стилистическое правило описывает то, как надо располагать текст программы. Существует несколько вариантов соглашений о правильном расположении текста. В данном пособии мы будем следовать Object Pascal Style Guide — стандарту, выработанному разработчиками языка Delphi. Итак, следует действовать следующим образом:
1) Слово var пишется на отдельной строчке. Следующие за ним описания переменных начинаются с новой строки. При этом у всех описаний делается отступ слева в два пробела. Переменные разных типов описываются на разных строках.
2) В полиграфии существует такое понятие как типографика. Это набор правил, описывающий как должен располагаться на страницах книги текст, чтобы быть удобочитаемым. Одно из простых правил говорит, что после знаков препинания всегда ставится пробел. Не следует пренебрегать этим правилом и при написании программ. Пробел всегда следует ставить после запятых и двоеточий (как в вышеприведенном примере).
3) Слова begin и end, ограничивающие раздел операторов пишутся без отступа. Весь же текст программы между ними снова пишется с отступом в два пробела.
4) На одной строке должен располагаться только один оператор. Так, хотя допустимо писать, например:
begin
x:=1; y:=2; z:=3; writeln(x, y, z);
end.
следует все же писать:
begin
x:=1;
y:=2;
z:=3;
writeln(x, y, z);
end.
5) Условный оператор записывается следующим образом:
Пример правильного оформления:
Пример НЕправильного оформления:
Данная «неправильная» программа будет работать совершенно также, как и приведенная выше «правильная», однако использовать надо именно правильный вариант.
6) Внутри условного оператора могут располагаться любы другие операторы, в том числе и другие условные операторы. То, что находится внутри них, пишется с еще большим отступом.
Общая идея такова: отступы показывают подчиненность (вложенность) структур в вашей программе. Текст внутри раздела операторов — делаем отступ, внутри условного оператора — еще больший отступ, внутри еще одного условного оператора — еще больший отступ и т. д.
Соблюдение данных правил делает визуально очевидной логическую структуру вашей программы. Создание сколько-нибудь сложной программы без этого оказывается невозможным.
Введение
По профессии я занимаюсь поиском и исправлением ошибок в чужих программах. За время работы я набрал некоторую коллекцию всевозможных багов и попытался свести их в одну таблицу и классифицировать с тем, чтобы оформить некую брошюру по быстрому поиску и исправлению наиболее часто встречающихся ошибок. Однако произвести такую классификацию не удалось. Дело в том, что, выделив 5-6 основных ошибок, таких как: неправильное приведение типов, путаница со знаковыми и беззнаковыми выражениями, пренебрежение выполнением проверок аргументов функций и пр., — я увидел, что остальные ошибки слишком индивидуальны, чтобы их как-то обобщать. О приемах поиска и говорить нечего, т.к. их намного больше, чем самих ошибок.
Тем не менее, каждый раз, исправляя какую-либо ошибку, я для себя отмечал, что ее можно было и не совершить, если бы то-то, то-то. Со временем я уяснил, что большая часть ошибок совершается из-за неправильного подхода к процессу программирования. Поэтому идея написания брошюры «Локализация и исправление ошибок» перешла в идею написания брошюры «Как не совершать ошибки». И я считаю это правильным, потому что лучше учиться строить, а не восстанавливать плохо построенное. Большая часть советов, описываемых мной, довольно банальны и просты. Вроде как, все их знают, но почему-то многие не придерживаются.
О каких ошибках идет речь. Конечно же, не о тех, которые обнаруживает компилятор при трансляции программы. Пока мы не исправим эти ошибки, нам не удастся получить код, который мы сможем прошить в контроллер. Эти ошибки в большинстве своем означают, что текст не соответствует правилам языка программирования, и, по сути, являются помарками. Также в этом материале не хотелось бы касаться так называемых «запланированных» ошибок (обработка неправильных данных, пришедших извне, ошибочные действия пользователя и пр.). Мы будем говорить об ошибках реализации, т. е. о тех, которые будут проявляться в ходе выполнения программы. Эти ошибки являются следствием неправильно запрограммированного (или составленного) алгоритма, допуска условностей, невнимательности и неаккуратности.
Для кого это пособие
В этом пособии я изложил свой подход к программированию микроконтроллеров, привел свои правила, которыми руководствуюсь при написании программ, и обозначил те моменты, которым при написании программ уделяю особое внимание. Уверен, что моя точка зрения не единственная, а также, что где-то мне самому еще есть чему поучиться. Так что я буду рад, не только если пособие окажется кому-то полезным, но также если кто-то решит меня в чем-то поправить или дополнить.
Статья рассчитана на широкий круг программистов, поэтому я старался не затрагивать концепцию доказательного программирования, и вообще старался привести больше практических советов, чем теории (с которой сам на «Вы») и формалистики. Так же в этом пособии нет описания приемов повышения надежности программ (самодиагностика, троирование, перепроверка действий и пр.).
Не следует считать, что после прочтения данного пособия ваши программы автоматически станут безошибочными, и уж тем более, что в будущем ошибок не будет вовсе. Ошибки все равно будут, но процент их сильно сократится. Более того, на написание программ по правилам, изложенным в этом пособии, будет затрачено время в два, в три, в пять раз больше, чем без правил. Писать программы без ошибок — это труд, причем кропотливый. Просто в отличие от кропотливого труда по поиску и исправлению ошибок, этот вид труда поддается планированию и формализации, т.е. является предсказуемым. А применение описанных здесь правил в разы сократит время непредсказуемого процесса отладки, который, как показывает практика, зачастую отнимает намного больше времени, чем само кодирование.
Предполагается, что читатель знает, что такое контроллер, как он устроен, как включается, наконец, что такое электрический ток, потому что без этих знаний заниматься микроконтроллерами бесполезно. Так что, как говорилось в старом анекдоте: «Учите матчасть!»
«Учите матчасть!»
Это самая очевидная рекомендация. Про нее можно было и не говорить, но для полноты картины вскользь коснемся этой темы. Программист, пишущий для контроллеров должен знать схемотехнику, устройство самого контроллера, язык программирования, на котором пишет программу, и особенности используемого компилятора. Тот программист, который пренебрегает необходимостью иметь эти знания, теряет право жаловаться на то, что его программа не работает.
Схемотехника
Как минимум нужно знать:
- основы электроники (и цифровой и аналоговой)
- законы Ома и Кирхгофа
- типовые схематические решения (RC-цепочки, транзисторные ключи)
Кроме того, нужно знать, что необходимо устанавливать диоды параллельно катушкам реле, что светодиоды включаются через токоограничивающий резистор, что у механических контактов есть такое явление как дребезг, и пр.
Контроллер
Нужно знать архитектуру контроллера, способы адресации, регистры и их назначения, периферийные модули и режимы их работы, диапазоны рабочих температур, частот, питаний и пр., нагрузочная способность портов и т.д.
Не нужно стесняться заглядывать в фирменные даташиты, там есть ответы на большинство вопросов. А то у кого-то не работает PORTA (в то время как не отключены аналоговые цепи), у кого-то не появляется «1» на выходе RA4, у кого-то прерывание по RB0 срабатывает только в половине случаев и т.д.
Язык
Конечно же, нужно знать сам применяемый язык программирования.
Для ассемблера это:
Для языков высокого уровня это:
- квалификаторы данных и функций;
- указатели (или указатели на указатели);
Компилятор
Разработчики компиляторов для микроконтроллеров, стремясь адаптировать компилятор под конкретную платформу, позволяют себе отходить от стандартов языка, одновременно не нарушая их. Каждый компилятор имеет свои особенности, незнание которых может привести к трудностям при портировании или при использовании чужих библиотек:
- типы данных (размерность, знаковость);
- квалификаторы (near, far, const, rom);
Этапы программирования
Когда ТЗ согласовано и задача формализована (переведена с проблемно ориентированных требований в технические: входные/выходные данные, режимы работы и пр.), начинается сам процесс программирования.
- Планирование (включает в себя проектирование, составление плана действий, выявление требуемых ресурсов).
- Кодирование (запись самой программы в машинном языке)
- Отладка (локализация и устранение ошибок)
- Тестирование (проверка работоспособности и соответствие спецификации)
- Оформление проектной документации
- Эксплуатация и сопровождение
Беда в том, что многие пренебрегают планированием, а также считают, что отладка появляется только в том случае, если была допущена какая-то ошибка при кодировании, а так как надеются с первого раза написать без ошибок, то и процесс отладки не учитывается. Однако же, в реальности отладка – это обязательный процесс, на который, вдобавок, приходится большая часть времени, сил и нервов. Закодировать даже самый сложный и запутанный алгоритм – это нетрудно, трудно заставить его работать, или, если они был написан правильно, убедиться в его работоспособности. Поэтому процессы отладки и тестирования самые длительные и самые трудоемкие. Однако, время отладки можно сократить, сделав процесс более предсказуемым и управляемым, а именно – спланировав программу.
Планирование программы
Почему-то часто планированием программы вообще пренебрегают. И в лучшем случае все планирование состоит из подсчета количества требуемых выводов входа/выхода. Напомню одну старую шутку: «Нетщательно спланированная работа отнимает в 3 раза больше предполагаемого времени, тщательно спланированная – только в два». Ее можно дополнить: «Неспланированная работа отнимает все время».
Кто-то может сказать: «Я пишу маленькие программы, что там планировать?». Тем не менее, практика показывает, что лучше для маленькой программы потратить 10-15 минут на планирование (просто расписать на листе бумаги), чем тратить 3-4 дня на поиск ошибки, рытье Интернета и отбивание от обвинений в ламерстве на форумах в сети.
Ниже приведены этапы планирования программы:
Рассмотрим каждый этап более подробно.
Расписать алгоритм
Алгоритм – это ДНК программы, вернее – ее генетический код. Если в нем ошибка, то программа будет вести себя неправильно. Часто еще при составлении алгоритма на бумаге всплывают некоторые ответвления, которые могут быть проигнорированы при написании программы «в лоб». Преимущество в том, что алгоритм составляется в абстрактных терминах, еще нет ни типов данных, ни переменных, поэтому есть возможность сосредоточиться на конкретных алгоритмических моментах, не думая пока о деталях реализации. В большинстве случаев требуется составить несколько алгоритмов: один общий для всей программы, описывающий режимы работы и порядок переключения между ними, и алгоритмы работы каждого режима в отдельности. Степень детальности алгоритма, конечно, зависит от сложности программы в целом и каждого узла в отдельности.
Алгоритм может быть представлен как в виде блок-схемы, так и в виде графа переходов, в виде графика эпюр сигналов и т.д. Не стоит забывать о требованиях к проектной документации; например, если в документации требуется привести алгоритм в виде блок-схемы, то не нужно его сначала рисовать в виде графа переходов, а затем переводить в блок-схему.
Продумать модули
Нам нужно заранее продумать, из каких модулей будет собираться наша программа, вернее, на какие модули она будет разбита. Преимущества модульности я поясню ниже, а здесь приведу рекомендации, как правильно разбить программу на модули.
Во-первых, разбиение должно быть произведено по функциональному признаку. Т.е. не нужно в один модуль заталкивать USART и вывод звука на пьезодинамик, даже если при конкретной реализации это и кажется целесообразным. Дело в том, что однажды написанный модуль можно будет переносить в другие проекты, избавляя себя от необходимости писать один и тот же код по много раз. Чем модуль будет функционально изолированнее, тем проще будет его перенос. Однако и здесь не следует бросаться в крайности и делать отдельно модули для передачи данных по USART, для приема данных по USART, для подсчета и сравнения контрольной суммы данных, принятых по USART, и т.п. Функционально модуль должен быть полным, но безысбыточным.
Также стоит отметить, что при разбиении своей будущей программы на модули нужно учитывать наличие уже готовых модулей (либо своих, либо чужих).
Большинство модулей можно разделить на два типа: системные (работают на уровне сигналов и железа) и алгоритмические (работают на уровне обработки данных и режимов). Хороший пример – работа ЖКИ. Предположим, нам требуется выводить информацию на ЖКИ HD44780. Крайне неудачным решением будет такое, при котором функции вывода конкретных данных на экран, вызываемые из головной программы, и функции работы с самим HD44780, вызываемые из функции вывода данных, будут помещены в один модуль. Тут получается, что модули обоих типов – алгоритмический и системный – смешаны в один, что сильно затруднит в дальнейшем, например, использование индикатора другого типа. Если же мы четко разделим системный функционал и алгоритмический, то в дальнейшем замена типа индикатора выльется для нас всего лишь в замену системного модуля.
Продумать данные
Также на этапе планирования мы определяем для себя, какими данными будет оперировать наша программа. Причем нужно обозначить не только назначение данных и требуемый их объем, но также заранее предусмотреть их размещение (ROM или RAM, конкретный банк RAM) и область видимости (например, нам нет смысла делать видимым во всей программе буфер выходных данных i2c).
Разделить периферию контроллера между процессами
Периферийные модули контроллера помогают упростить некоторые программные узлы, а иногда просто делают их возможными (например, без АЦП мы не сможем измерить аналоговый сигнал). Но зачастую бывает, что программных узлов, требующих использование встроенной периферии, больше, чем имеется на борту у контроллера. Поэтому периферийные модули приходится разделять между несколькими задачами (типичный пример – таймеры). При проектировании программы нужно заранее распределить периферийные модули между задачами, что поможет выбрать оптимальные параметры для каждого модуля.
(Видел программу, в которой неправильно распределенные ресурсы привели к тому, что пришлось мультиплексировать управление GSM-модулем и GPS-приемником на один USART. Программа начала сбоить и, насколько я знаю, ее так и не привели в рабочее состояние.)
Учесть физические свойства обвеса
Контроллер с нашей программой будет жить не сам по себе, его будут окружать какие-то внешние схематические узлы, специализированные микросхемы и пр. Проектируя программу, нужно заранее учесть особенности всех подключенных к контроллеру устройств, цепей и микросхем.
Например, если в устройстве будут кнопки, то нужно помнить, что механические контакты дребезжат и шуршат. Это сразу должно наталкивать на использование каких-то дополнительных счетчиков и/или таймеров для подавления этих эффектов.
Также стоит помнить про «холодный» старт внешней периферии. Например, у нас есть ЖКИ, которому после подачи питания требуется 10 мс для самоинициализации, во время которой он не будет слышать команд извне. Если этого не учесть, то может получиться так, что наш контроллер начинает инициализацию ЖКИ примерно в то же время, когда ЖКИ заканчивает свою внутреннюю инициализацию. В результате ЖКИ то будет успевать самоинициализирваться и начинать прием наших данных, то не будет. И когда мы поймем, в чем дело, и увеличим задержку перед инициализацией до 20 мс, мы с удивлением обнаружим, что теперь при включении питания, например, успевает туда-сюда сработать реле, т.к. с новой задержкой неинициализированное состояние управляющего им вывода держится достаточно для того, чтобы ток в катушке реле успел вырасти и привести к срабатыванию.
Если в нашем устройстве предполагается прием данных по радио, то мы должны учесть, что в радиоканале есть помехи, и что прием такого сигнала нельзя делать «в лоб» по фронтам, а нужно, как и в случае с обработкой сигналов от механических контактов, заводить дополнительные счетчики, таймеры и т.п.
Примеров про обвес можно придумать еще много: состязания на шине, емкостные нагрузки, внешние источники прерываний и т.д. Все это нужно учитывать еще до начала написания текста программы, заранее предусматривая порядок действий и резервируя дополнительные ресурсы (память и скорость).
Предусмотреть возможность расширения
При проектировании программы следует заранее убедиться, что в случае значительного разрастания в дальнейшем функционала (а значит и кода) будет возможность заменить выбранный контроллер более мощным с минимальными затратами. Если мы, например, решили использовать в нашей разработке контроллер из линейки PIC16, а на этапе планирования мы прикинули, что подойдет только 16F877, или 16F946, или 16F887 (короче говоря – с максимальным объемом памяти), значит, мы неправильно выбрали линейку. В этом случае нужно брать PIC18, потому что велика вероятность того, что программа в выбранный контроллер просто не влезет.
Часто встречаю на форумах крики души: «помогите оптимизировать программу, а то она не лезет в PIC18F67J60!» (прим.: контроллер из линейки PIC18, имеющий максимально возможный объем ROM = 128Кб). Это результат непродуманного выбора контроллера на этапе планирования, если этот этап вообще был проведен.
Так же надо учесть, что при отладке программы нам потребуются кое-какие ресурсы (об этом речь пойдет ниже).
Предусмотреть смену платформы или компилятора
Здесь речь, конечно, не о том, что нужно напичкать программу директивами условной компиляции, позволяющими максимально эффективно использовать возможности каждого компилятора, на который, возможно, придется переносить программу. Речь как раз о том, чтобы:
- минимально использовать уникальные особенности конкретного компилятора, а те куски кода, которые все-таки так пишутся, блокировать условными директивами (бывает, что компилятор простую операцию развернет черт те во что, а – кровь из носа — хочется сделать кратко и красиво);
- не использовать недокументированные особенности компилятора; некоторые операции могут быть реализованы различным способом, например, операция сдвига влево может после своего выполнения оставить флаг переноса, а может и изменит его, либо, выполнив оптимизацию, заменит инструкцию сдвига чем-нибудь, либо в качестве результата возьмет один из промежуточных результатов, получившихся при вычислении предыдущего выражения. В общем, нет гарантии, что флаг переноса будет установлен правильно.
- переопределять типы данных. Стоит учитывать, что знаковость и размерность в стандарте языка определены для каждого типа довольно расплывчато (например, в HT-PICC18 тип char по умолчанию беззнаковый, в то время как в MPLAB C18 – знаковый; или в CCS тип int – 8-битный беззнаковый, а в остальных компиляторах – 16-битный знаковый). Поэтому в каждом проекте хорошо бы иметь h-файл с переопределениями типов
S08;
S16;
S32;
U08;
U16;
U32;
bit ;
S_ALU_INT;
U_ALU_INT;
Обратим внимание на два типа: S_ALU_INT и U_ALU_INT – это знаковое и беззнаковое целые, имеющие размерность машинного слова для конкретного контроллера. Т.к. операции над операндами, имеющими размерность шины данных, производятся наиболее оптимально, иногда есть смысл пользоваться этими типами данных.
Примечание: далее в тексте для наглядности будут использоваться стандартные типы: char (8 бит), int (16 бит), long (32 бита).
Написание программы
При написании самого текста программы нужно руководствоваться двумя сводами правил:
Сами правила могут у каждого быть свои, но они должны по возможности исключать разночтения.