Как не допускать ошибок в программировании

Научитесь выявлять их. Выработайте привычки избегать их.

Цель этой статьи не загнобить новичков за типичные ошибки, а научить выявлять их и избегать. Порядок перечисления – случайный.

От переводчика

Иногда бывает трудно объяснить простыми словами казалось бы банальные вещи: зачем использовать гит, в чем фишка инкапсуляции, зачем писать тесты, как планировать свой код, рефакторить чужой и т.д. Мне показалось, что в этой статье компактно собраны важные «гуманитарные» аспекты программирования. Что-то вроде морального кодекса, ориентира и мотиватора в принятии решений, связанных с написанием кода.

Как бы это смешно не звучало, я работал над этим текстом с середины марта, стараясь подобрать подходящие формулировки и упростить для восприятия. Ещё пару дней воевал с хабра-редактором. Поэтому, если вы найдёте недочёты, прошу не винить меня в нерадении, а оповестить, я их сразу же исправлю. Я думал украсить статью картинками, но решил, что это только раздует её до совсем неприличных размеров. Приятного чтения.

1) Программирование без планирования
2) Чрезмерное планирование
3) Недооценивание важности качества кода
4) Хвататься за первое решение
5) Не отступать
6) Не гуглить
7) Не использовать инкапсуляцию
8) Планирование неизвестного
9) Использование неподходящих структур данных
10) Ухудшать код
11) Комментирование очевидных вещей
12) Не писать тесты
13) Думать, если что-то работает, то это правильно сделано
14) Не подвергать сомнению существующий код
15) Одержимость лучшими практиками
16) Одержимость производительностью
17) Не ориентироваться на конечного пользователя
18) Не подбирать правильные инструменты
19) Непонимание, что проблемы с кодом вызывают проблемы с данными
20) Изобретение колеса
21) Неправильное отношение к инспекции кода (code review)
22) Не использование систем контроля версий
23) Злоупотребление общим состоянием (shared state)
24) Неправильное отношение к ошибкам
25) Не отдыхать

1) Программирование без планирования

Качественный контент не создаётся “на коленке”, а требует основательной работы. Программный код – не исключение.

Хороший код должен проходить через следующие стадии:

Замысел. Исследование. Планирование. Написание. Проверка. Изменение.

Каждому из этих пунктов надо уделить достаточно усилий.

Новички склонны кодить без предварительных раздумий. Это может сработать на маленьких автономных приложениях. Но на крупных может обернуться трагедией.

Прежде чем сказать что-то, вы обдумываете слова, чтобы потом не было стыдно. Точно так же избегайте непродуманного кода, за который однажды будет стыдно. И слова и код – это отображение ваших мыслей.

“Если ты зол, сосчитай до 10, прежде чем говорить. Если очень зол — то до 100”. (Томас Джефферсон)

Для нашего случая это можно перефразировать так:

“Когда проверяешь код, сосчитай до 10 прежде чем переписать 1 строчку. Если для этого кода нет тестов — то до 100”.

Программирование – это на 90% изучение существующего кода и его изменение посредством маленьких, легко тестируемых порций, вписывающихся в общую систему. Само написание кода это всего лишь 10% работы программиста.

Программирование – это не просто написание строк кода, а творчество, основанное на логике, которое надо развивать в себе.

2) Чрезмерное планирование

Планировать прежде чем нырять в написание кода – хорошая вещь. Но даже хорошие вещи могут навредить вам, если переборщить с ними. Даже водой можно отравиться, если слишком много её выпить.

Не ищите идеальный план. Такого не существует в мире программирования. Ищите достаточно хороший план для старта. Любой план изменится, зато он заставит вас придерживаться структуры в коде, которая облегчит вашу дальнейшую работу.

Линейное планирование всей программы “от А до Я” (водопадным методом) – не годится для большинства программных продуктов. Разработка подразумевает обратную связь и вы постоянно будете удалять и добавлять функционал, что никак нельзя учесть в «водопадном планировании». Планировать следует несколько следующих элементов. И каждый новый надо включать в план лишь после гибкой адаптации к реальности (Agile).

К планированию надо подойти очень ответственно, потому что и его недостаток и избыток могут навредить качеству кода. А качеством кода ни в коем случае нельзя рисковать.

3) Недооценивание важности качества кода

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

Смотрите на программу как на составные части, общающиеся посредством кода. Плохой код — это плохая коммуникация.

“Пишите свой код так, будто его будет сопровождать агрессивный психопат, знающий, где вы живете”. (Джон Вудс)

Важны даже “мелочи”. Если вы бессистемно используете заглавные буквы и отступы, то у вас нужно отобрать лицензию программиста.

tHIS is
  WAY MORE important
than
         you think

Не используйте длинные строки. Строку длиннее 80 символов очень трудно читать. Используйте специальные инструменты для приведения кода в порядок (ESLint, Prettier для js).

Следите за количеством строк в функциях и файлах. Разделяйте код на мелкие части, понятные и тестируемые. Функция больше 10 строк – слишком длинная.

Не используйте двойное отрицание. Не не не делайте так. Это очень не не плохо.

Давайте переменным осмысленные, информативные, однозначные названия. Не используйте короткие, общие, или опирающиеся на тип, названия.

“В компьютерных науках есть только две по настоящему сложные вещи: инвалидация кэша и именование переменных”. (Фил Карлтон)

Используйте константы с содержательным названием для хранения примитивов. Если вам где-то нужно использовать число 12, сделайте сначала так:

const monthsInYear = 12; 

Не используйте костыли в коде ради экономии времени. Не бегите от проблем, смотрите им в лицо и побеждайте их бескомпромиссно правильным кодом.

Краткий код в большинстве случаев лучше длинного. Но стремление к краткости не должно вредить читабельности. Не используйте замудреные тернарные операторы, чтобы уложиться в одну строчку. Но и не удлиняйте код без особой надобности. Удаление ненужного кода – это лучшее, что вы можете сделать для улучшения любой программы.

“Измерять программирование строками кода, это то же самое, что измерять авиастроительство тоннажем произведенных самолетов”. (Билл Гейтс)

Не чрезмерствуйте с условной логикой. Очень часто то, что хочется написать через условие, можно написать иначе и это улучшит читаемость кода. Не переживайте за оптимизацию, пока это не станет явным. Избегайте в условиях присвоений и стиля Йоды.

4) Хвататься за первое решение

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

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

“Есть два пути написания программы: 1) сделать её настолько простой, что в ней, очевидно, не будет недостатков; 2) сделать её настолько сложной, что в ней никакие недостатки не будут очевидными”. (Тони Хоар)

5) Не отступать

Другая частая ошибка новичков – не отступать. Даже когда они поняли, что выбранное решение не самое лучшее. Подход “не сдаваться” хорош во многих сферах, но не в программировании. Программистам полезно признавать ошибки раньше и чаще. Как только вы засомневались в решении – отбросьте его и переосмыслите проблему. Не важно, сколько вы уже вложили в этот путь. Системы контроля версий типа Git позволяют создавать ветки и экспериментировать с разными решениями, активно пользуйтесь этим.

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

6) Не гуглить

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

Поиск может показать вам проблему с неожиданной стороны. И то, что вы считали решением и хотели реализовать, может оказаться усугублением проблемы. Даже когда кажется, что вы всё знаете для решения, гугл наверняка удивит вас.

Обратная сторона медали – новички любят копировать из гугла всё подряд без особых раздумий. Но даже если код решает вашу проблему, используйте его только когда ясно понимаете каждую его строчку.

Не будьте креативным в понятиях Брета Виктора, который сказал:

“Думать, что ты знаешь, что делаешь – самая опасная мысль креативного человека”.

7) Не использовать инкапсуляцию

Инкапсуляция полезна в общем, не только как часть парадигмы ООП. Не использование инкапсуляции приводит к возникновению трудно поддерживаемых систем.

Каждый элемент приложения должен описываться в одном месте, обычно это один объект. И этот объект предоставляет другим объектам ровно столько из своих данных, сколько нужно. Не из соображений секретности, а чтобы снизить уровень зависимости отдельных частей друг от друга. Это позволяет вам редактировать каждую отдельную часть, не ломая целое.

Логические единицы программы должны быть разделены на классы. Под классами подразумевается любое разделение, будь то класс, функция, модуль или пакет.

У новичков всегда бывает проблема с выделением логических единиц в классы. Если вы видите класс со смутным названием и выполняющий разношерстные операции – перед вами код новичка. Если вы сделали небольшое изменение в коде, и это вызвало по цепочке много других изменений – это ещё один признак новичка.

Если вам нужно создать новый метод, или расширить старый, хорошенько подумайте и прислушайтесь к интуиции. Не делайте это на авось, с мыслью “потом переделаю”. Делайте это прямо сейчас.

Стремитесь к тому, чтобы ваш код имел высокое зацепление и низкую связанность (High Cohesion and Low Coupling). Этот загадочный термин означает, что внутри класса должно быть максимально связей, а между классами – минимально зависимостей.

8) Планирование неизвестного

Когда вы пишете новую строчку кода, порой в голову лезут мысли: “а что если…” И вы начинаете фантазировать о разных новых фичах, которыми можно украсить программу. В большинстве случаев такие мысли вредны и не стоит на них ориентироваться.

Пишите только тот код, который вам нужен сегодня. Не пишите код, который возможно пригодится когда-то потом.

Всегда пишите минимум кода, необходимого сегодня и для конкретной текущей задачи. Конечно, предугадывайте и обрабатывайте экстремальные случаи, но не создавайте экстремальный функционал.

“Рост ради роста — это идеология раковой клетки”. (Эдвард Эбби)

9) Использование неподходящих структур данных

Начинающие программисты обычно уделяют много внимания алгоритмам. Умение подобрать нужный алгоритм и уместно использовать его – это очень хорошо. Но простое их заучивание не много прибавляет к вашему гению программирования.

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

Неуместно использованная структура данных это кричащее предупреждение: “код новичка!”. Вот несколько примеров.

Обычный массив или ассоциативный?

Самая распространённая ошибка – это использование обычных массивов вместо ассоциативных для хранения списка записей.

Обычный массив:

[{id: 1, title: "entry1"}, {id: 2, title:"entry2"}, .... ]

Ассоциативный массив:

{ 1: {id: 1, title: "entry1"}, 2: {id: 2, title:"entry2"}, ....}

Да, вы не ослышались, для хранения списка записей нужно использовать ассоциативные массивы. Под списком записей я подразумеваю такие записи, где присутствует идентификатор. Массивы оправданы для хранения скалярных величин и если планируется активное использование методов, вроде push, pop, shift, unshift, которые обращаются к записям не через ключ, а через порядок в списке.

Дело в том, что поиск внутри ассоциативного массива намного быстрее, чем в обычном. Конечно, это особо актуально для больших коллекций, но лучше держать это в уме и при работе с маленькими.

Стек или рекурсия?

Оптимизировать рекурсии довольно сложная задача, особенно в однопоточной среде. Это усложняется, если рекурсивная функция вызывает себя 2 и более раз.

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

10) Ухудшать код

Представьте, что вам досталась такая бардачная комната. И теперь перед вами стоит задача поставить туда какую-то свою вещь. Многие склонны сразу же сделать это. Задача будет решена за секунды. Но так вы станете соучастниками этого бардака. Правильный подход — это упорядочить ту часть комнаты, куда вы добавляете что-то. Например, если это одежда, то приберитесь в шкафу для одежды, сложите туда всю одежду из комнаты, а потом уже положите свою вещь на отведенное ей место. Всегда улучшайте код хоть немного, но никогда не ухудшайте.

Вот несколько распространенных ошибок, приводящих к беспорядку в коде:

Дублирование

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

Не использование файла конфигурации

В файл конфигурации обязательно надо помещать те величины, которые:
могут меняться в зависимости от окружения
встречаются в разных местах кода
Каждый раз, когда вы вводите в код новую величину, спрашивайте себя: “может быть её поместить в файл конфигурации?” И ответ будет почти всегда “да”.

Лишние условные операторы и временные переменные

Любой if разветвляет логику вашей программы, поэтому их наличие должно быть сведено к минимуму, насколько это возможно без вреда для читабельности. Самое сложное – определить правильный уровень для изменений: будете вы расширять текущий код или вынесете его во внешнюю функцию и вызовете её?

Вот яркий пример ненужного if:

function isOdd(number) {
 if (number % 2 === 1) {
   return true;
 } else {
   return false;
 }
}

Его можно переписать без единого if:

function isOdd(number) {
 return (number % 2 === 1);
};

11) Комментирование очевидных вещей

Большинство комментариев можно заменить названиями переменных и функций. Прежде чем написать комментарий, вспомните об этом.

Например, такой код:

// This function sums only odd numbers in an array
const sum = (val) => {
  return val.reduce((a, b) => {
    if (b % 2 === 1) { // If the current number is odd
      a+=b;            // Add current number to accumulator
    }
    return a;          // The accumulator
  }, 0);
};

Можно заменить таким:

const sumOddValues = (array) => {
  return array.reduce((accumulator, currentNumber) => {
    if (isOdd(currentNumber)) { 
      return accumulator + currentNumber;
    }
    return accumulator;
  }, 0);
};

Но иногда прояснить код можно только комментарием. В таком случае сосредоточьтесь на вопросе: “ЗАЧЕМ нужен этот код”, а не “ЧТО делает этот код”. Вот пример кода, где комментарии только засоряют код:

// create a variable and initialize it to 0
let sum = 0;
// Loop over array
array.forEach(
  // For each number in the array
  (number) => {
    // Add the current number to the sum variable
    sum += number;
  }
);

Не делайте так, если вы программист. А если вы работодатель таких программистов — увольте их прямо сейчас.

12) Не писать тесты

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

Если вы не пишете тесты, вы всё равно так или иначе тестируете вашу программу вручную. Если это веб-приложение, то вы обновляете страницу и совершаете какие-то действия после каждых нескольких строк кода. Все так делают, в этом нет ничего зазорного. Так делают хотя бы, чтобы понять, как писать автоматический тест. Каждый раз, когда вы проделали что-то вручную, вернитесь в редактор кода и напишите тест, повторяющий эти же действия и ожидающий эти же ответы.

Вы человек и рано или поздно, по мере роста проекта, вы забудете проделать какой-то из прошлых успешных тестов. Пусть компьютер проделает их за вас.

Если можете, создавайте проверки даже прежде самого кода. Разработка через тестирование (test-driven development, TDD) создана не для прикола и хайпа. Она благотворно влияет на то, как вы продумываете, проектируете и реализуете программные элементы.

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

13) Думать, если что-то работает, то это правильно сделано

Взгляните на эту функцию, которая суммирует нечетные числа. Всё ли там правильно?

const sumOddValues = (array) => {
  return array.reduce((accumulator, currentNumber) => {
    if (currentNumber % 2 === 1) { 
      return accumulator + currentNumber;
    }
    return accumulator;
  });
};
 
 
console.assert(
  sumOddValues([1, 2, 3, 4, 5]) === 9
);

Тест проходит. Жизнь прекрасна. Правда?
Проблема этого кода в том, что он не полный. Он корректно работает только для нескольких случаев и наш удачный тест проверяет как раз один из них.

Проблема 1

Нет проверки на пустой ввод. Что случится, если функцию вызвать без аргументов? Выведется ошибка.

TypeError: Cannot read property 'reduce' of undefined.

И это признак плохого кода по двум главным причинам:
Пользователи вашей функции не должны сталкиваться с деталями её реализации.
Сообщение об ошибке не информативно.

Для пользователя функция просто не работает и он не поймет, что делать для исправления ситуации. Было бы намного лучше, если бы ошибка выдавалась в таком виде:

TypeError: Cannot execute function for empty list.

А может вам следует изменить функцию, чтобы она игнорировала пустой ввод и выдавала ответ 0? В любом случае, надо что-то сделать, нельзя оставлять так как было.

Проблема 2

Нет валидации. Что если в функцию передадут вместо массива строку, число или объект? Вот что произойдет:

sumOddValues(42);
TypeError: array.reduce is not a function

Подвох в этой ситуации в том, что array.reduce — это как раз функция. Но так как вы назвали аргумент функции array (массив), то что бы вы не передали ей (в данном примере это 42), будет названо массивом внутри функции. На самом деле ошибка говорит о том, что 42.reduce — это не функция. Не лучше ли сделать вывод ошибки в виде:

ОшибкаТипа: 42 - это не массив, чувак. 

Проблемы 1 и 2 описывают стандартные исключения, которые легко предусмотреть. Но бывают и менее очевидные исключения, с которыми надо быть внимательнее. Например, что произойдёт, если в массиве будут отрицательные числа?

sumOddValues([1, 2, 3, 4, 5, -13]) // => still 9

Следует ли программе воспринимать -13 как нечётное число? Или игнорировать? Или выдать ошибку? Может следует переименовать функцию в “сумма положительных нечётных чисел”? Вы легко выберете нужный вам вариант. Но самое интересное здесь то, что если вы не пишите тесты, документирующие работу вашей функции, то те, кто будут их сопровождать, даже не смогут понять, баг ли это или преднамеренное допущение.

“Это не баг. Это задуманный функционал” — удобная отмазка тех, кто не пишет тестов.

Проблема 3

Не все валидные случаи правильно работают. Забудьте разные каверзные исключения. Эта функция неправильно работает и с вполне обыденным набором переменных.

sumOddValues([2, 1, 3, 4, 5]) // => 11

В данном примере 2 попадёт в сумму, хотя не должна. Это произойдёт потому, что в функцию reduce не передано initialValue, и поэтому в качестве исходного значения будет взят первый элемент массива. Поэтому важно написать тест и для такого случая. Если такого нет, то это ещё один признак кода новичка.

14) Не подвергать сомнению существующий код

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

Самое страшное в этой ситуации, что новичок может подумать, что это хороший код, и будет повторять существующие в нём неправильные подходы.

Иногда код может выглядеть плохо, потому что разработчик был вынужден написать его таким в силу объективных причин. В таком случае уместно оставить комментарии с описанием этих причин.

Новичкам можно посоветовать такое правило: любой недокументированный код, который вы не понимаете — возможно, плохой. Изучайте его. Спрашивайте о нём. Пользуйтесь командой git blame, выдающей автора каждой строки кода.

Если автор далеко или не может вспомнить свой код, изучайте его до тех пор, пока полностью не поймете. Только так вы сможете ответить на вопрос, плохой код перед вами или хороший. Никогда не решайте этого наугад без твёрдых знаний.

15) Одержимость лучшими практиками

Термин “лучшие практики” вредный, он ограничивает вас в исследовании, «ведь уже есть лучшая практика».

“Лучших практик” не бывает. Бывают хорошие практики на сегодняшний день и для этого языка программирования.

Довольно часто то, что вчера считалось “лучшей практикой” сегодня считается плохой практикой. Вы всегда можете найти более лучшую практику, если потратите достаточно времени для этого. Поэтому не парьтесь о “лучших практиках”, а сосредоточьтесь на том, что вы можете сделать хорошо.

Не делайте что-то, потому что где-то прочитали цитату об этом, или увидели как кто-то делает это, или кто-то сказал про это “лучшая практика”. Ставьте всё под сомнение, бросайте вызов всем теориям, знайте все возможные варианты, и принимайте только обоснованные решения.

16) Одержимость производительностью

“Преждевременная оптимизация – это корень всех зол в программировании (или почти всех)”. Дональд Кнут, 1974

Хотя программирование существенно изменилось со времен Дональда Кнута, его совет актуален и в наши дни.

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

Разумеется, есть очевидные правила оптимизации, которые всегда надо держать в уме при написании кода. Например, в Node.js нельзя заполнять цикл событий и блокировать стек вызовов.

В погоне за вымышленной производительностью вы можете наделать реальные баги в самых неожиданных местах.

17) Не ориентироваться на конечного пользователя

Когда вы добавляете новый функционал в приложение, вы думаете в первую очередь о себе или конечном пользователе? Допустим, надо добавить новый ввод. Проще его прикрепить к уже существующей форме? Или нужно добавить ссылку. Проще всего прикрепить её к уже существующему меню ссылок?

Не будьте таким разработчиком. Будьте профессионалом, который смотрит на приложение глазами пользователя, ощущает его нужды и предугадывает его поведение. Новички думают, как бы проще засунуть новый функционал в уже существующие элементы. А профессионал думает о том, как сделать его удобнее для нахождения и использования.

18) Не подбирать правильные инструменты

У каждого есть набор любимых инструментов. Каждый из них прекрасен для какой-то своей задачи, в то время как для другой он ужасен. Молоток хорош для забивания гвоздя и плох для закручивания самореза. Не используйте молоток потому, что вы любите его или потому, что он самый популярный на Амазоне с рейтингом голосов 5.0.

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

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

Есть кодеры, которые чувствуют себя комфортно с известным для них набором инструментов и не хотят изучать новые. Это неправильно.

Вы можете построить дом примитивными инструментами из старой кладовки и потратить на это много драгоценного времени. А можете вложиться в новые инструменты и построить дом намного быстрее, качественнее и красивее. Так как инструменты постоянно развиваются и появляются новые, вам надо привыкнуть непрерывно изучать их и использовать.

19) Непонимание, что проблемы с кодом вызывают проблемы с данными

Один из важнейших аспектов программирования — это управление данными. Программа – это интерфейс для добавления, редактирования и удаления записей.

Даже небольшой баг в коде может оказать огромное влияние на данные. Особенно, если валидация делается только на стороне программы с багом. Новички могут сразу не заметить проблему, если ошибка в данных влияет на второстепенный функционал. И эта ошибка будет программно множиться долгое время, прежде чем будет обнаружена.

Что ещё хуже, так это то, что исправление кода без исправления уже созданных кодом данных, может увеличить ошибки в данных вплоть до степени невосстановления.

Как защитить себя от такого? Можно использовать несколько уровней валидации: на фронтенде, бекенде, при передаче и в базе данных (БД). Как минимум, используйте встроенные ограничения в БД.

Хорошо знайте все типы ограничений в БД и используйте их все при создании новых столбцов и таблиц.

NOT NULL накладывает ограничение на столбец, чтобы он не принимал пустые значения. Это обязательно делать, если ваше приложение предполагает существование этой величины.

UNIQUE отслеживает уникальность величины столбца в масштабах всей таблицы. Идеально подходит для хранения логина или почты пользователя.

CHECK проверяет произвольное выражение, например, для процентов нужно проверять вхождение в интервал от 0 до 100.

PRIMARY KEY подразумевает одновременно уникальность и не пустое значение. Каждая таблица базы данных должна иметь такое поле для идентификации записей.

FOREIGN KEY говорит о том, что величины этого столбца содержатся в другой таблице.

Другая проблема новичков – они не умеют мыслить категориями транзакций. Если несколько операций изменяют один и тот же источник данных, от которого все зависят, то они должны быть обёрнуты в транзакцию, которая будет отменена в случае, если одна из операций выдала ошибку.

20) Изобретение колеса

В программировании зачастую полезно переизобретать колёса. Это довольно гибкая и стремительно меняющаяся сфера знаний. Никакая команда не может угнаться за всеми обновлениями и новыми требованиями.

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

Иногда бывает сложно выбрать нужное колесо из-за многообразия. Проводите исследование. Пробуйте перед покупкой. Большинство “программных колёс” бесплатны и с открытым кодом. По возможности используйте заготовки с открытым исходным кодом (open source), их легко отлаживать, улучшать, заменять и поддерживать.

В то же время, если вам нужно только колесо, не надо покупать целую машину и прикручивать эту машину к другой машине на место колеса. Не подключайте целую библиотеку ради одной-двух функций. Если вам нужна функция shuffle из библиотеки lodash, импортируйте только её, не надо подключать весь lodash.

21) Неправильное отношение к инспекции кода (code review)

Один из признаков новичков, это восприятие инспекций кода как критицизма. Они не любят их, не ценят и даже боятся.

Это в корне неправильное отношение надо как можно быстрее изменить. Смотрите на каждую инспекцию кода как на ценную возможность обучения. Любите и цените их. Учитесь посредством них. И благодарите делающих замечания.

Надо принять тот факт, что программист – это вечный ученик, а инспекция кода – это одна из форм обучения.

Иногда сам инспектор ошибается и тогда приходит ваш черёд научить его чему-нибудь. Его неправильное замечание могло возникнуть по причине неочевидности вашего кода, тогда, возможно, вам следует доработать его. В любом случае, взаимный обмен знаниями крайне ценен для программистов и окупается многократно.

22) Не использование систем контроля версий (Git)

Новички склонны недооценивать пользу хорошей системы контроля версий/кода, вроде Git.

Обычно её используют, чтобы опубликовать свои изменения кода для других. Но её главное предназначение — это ясность истории. Код будут изучать и история его развития ответит на многие вопросы. Маленькие коммиты с осмысленными заголовками помогут сопровождающим код людям понять, как шаг за шагом образовывалась программа, пока не достигла текущего состояния.

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

Некоторые пишут в коммите, какие файлы они поменяли. Это лишний шум, потому что информация об измененных файлах и так содержится в самом объекте коммита и её легко извлечь при необходимости. Некоторые команды создают коммиты для изменений каждого отдельного файла и скорее всего это еще один признак слишком большого коммита.

Ещё одно предназначение системы контроля версий – понятность предназначения той или иной вещи. Допустим, вы столкнулись с функцией и вам надо понять её назначение и устройство. Вы можете найти коммит, в котором она появилась, и перед вами возникнет контекст её создания, что прольёт свет на всё остальное, связанное с ней.

Система контроля версий может даже помочь обнаружить баг, а именно, ту строку кода, с добавления которой программа дала сбой. В Гите есть бинарный поиск bisect, обнаруживающий коммит, внёсший баг.

Систему контроля можно использовать в разных целях, даже до того, как изменения кода превратились в официальные коммиты:

  • отслеживание изменений (staging changes)
  • выборочный патч (patching selectively)
  • сброс (resetting)
  • прятание (stashing)
  • перезапись истории (amending)
  • применение (applying)
  • просмотр изменений (diffing)
  • отмена коммитов (reversing)

Изучите все эти возможности, поймите, используйте и цените их. Чем меньше возможностей Git вы знаете, тем больше вы новичок.

23) Злоупотребление общим состоянием (shared state)

И снова это не про сравнение парадигмы функционального программирования с остальными.

Общее состояние является источником многих проблем. По возможности следует избегать его использования или свести к минимуму. Новички зачастую не понимают, что когда они определили переменную, она превращается в общее состояние. Это состояние содержит данные, которые могут быть изменены любыми элементами программы, находящимися в этой же области видимости. И чем больше область видимости, тем шире общее состояние и тем хуже. Старайтесь сохранять состояния в маленьких областях видимости и не давать им вылезать наружу.

Большая проблема общего состояния начинается тогда, когда несколько ресурсов меняют его в рамках одной итерации цикла событий (в событийно ориентированных средах). Возникает состояние гонки (Race conditions). И новички склонны решать эту проблему посредством таймера, особенно при блокировке данных. Это большой красный флаг. Избегайте этого. Ни в коем случае нельзя писать такой код или принимать его.

24) Неправильное отношение к ошибкам

Ошибки – это хорошо. Они говорят вам, что вы на правильном пути и подсказывают как добиться ещё большего прогресса. Опытные программисты любят ошибки, новички – ненавидят.

Если вас раздражают эти маленькие прекрасные сообщения об ошибках – вам надо изменить отношение к ним. Смотрите на них как на помощников, сотрудничайте с ними, опирайтесь на них, поднимаясь к новым высотам.

Некоторые ошибки следует превратить в исключения – запланированные за пользователем ошибки. Некоторые следует оставить как есть и позволить им уронить приложение и закрыть его.

25) Не отдыхать

Ваше тело и мозг нуждаются в отдыхе. Новички склонны увлекаться работой и забывать о перерывах. Обязательно включите в ваш рабочий процесс что-то, напоминающее об отдыхе. Вставайте, прогуливайтесь, думайте о том, какая работа на очереди и как лучше её сделать. Возвращайтесь за компьютер со свежими глазами.

Избегая ошибки в коде, вы повышаете эффективность работы, успеваете делать больше за тот же промежуток времени. Мы расскажем некоторые советы, позволяющие избегать ошибок в коде.

Полностью избежать ошибок нереально, их не делает только тот, кто бездвижно лежит на диване. Все программисты допускают ошибки, без этого никуда. Работая над крупными проектами, особенно в команде, ошибок не избежать. Тем не менее их количество реально сократить, чтобы заниматься доработкой гораздо реже. О способах избегания ошибок в программном коде сегодня и расскажем.

Планирование

Вначале любого проекта нужно всё распланировать, этому процессу следует уделить достаточно времени. Кораблю, бесцельно плывущему по морю, никакой ветер не окажется попутным. Очень важно составлять подробный план с пошаговыми действиями и критериями. Ещё полезно подводить промежуточные и конечные итоги. 

Не нужно игнорировать и составление блочных схем или UML-диаграмм. Если выявить ошибку во время проектирования, её решение обойдётся во много раз дешевле, чем та проблема, что выплыла во время разработки или даже внедрения, тестирования. Намного проще переработать схему, чем переписывать код.

Тестирование

Применяйте TDD – разработку через тестирование. После этапа создания плана сперва следует заняться разработкой модульных тестов для дальнейшего кода. Такой подходит исключит вероятность забыть их сделать. Это, конечно же, не главное. Ключевое преимущество подхода – удастся заранее продумать основные моменты интерфейса для классов. Скорее всего реализация пройдёт значительно проще. Такой метод помогает заострить внимание на общих моментах, не беря во внимание отдельные случаи.

Помимо этого, у людей использующий среду Visual Studio Enterprise есть весьма полезная возможность — Live Testing. Она помогает непосредственно в процессе разработки кода (ещё до компиляции) видеть насколько успешно выполнены тесты. Дополнительно есть возможность изучать построчное покрытие тестами и насколько правильно ведётся работа. Главное – всё это в реальном времени. Невероятно полезная возможность, рекомендую использовать всем, кто работает на C#.

Костыли

Не стоит заниматься разработкой собственных костылей. Подавляющее большинство частых задач имеют удачные решения от других разработчиков. Лучше найти готовое решение, которое уже протестировано и доработано коллегами. В таком коде значительно меньший риск обнаружения ошибок. 

Практически наверняка на разработку костыля с нуля уйдёт много времени, а также появится немало ошибок. Не стоит стесняться, применяйте чужие куски кода и дополнительные библиотеки. Что ещё можно говорить, если повторное применение того же самого кода – одна из 6 парадигм ООП.

Чистый код

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

Идеальное решение – лаконичная запись, правильное название переменных, информативные комментарии. Ещё рекомендую применять принципы SOLID, KISS, YAGNI, DRY. В таком коде одного беглого взгляда достаточно для определения, что в нём будет работать неправильно и где нужны коррективы.

Документация 

Нельзя забывать о документации и GIT (или другой системе управления версиями). Когда есть доступ к запискам и истории разработки, значительно проще понять, как и какой кусок кода должен обрабатываться. Это полезно в случаях, если к разработке могут присоединиться новые программисты. 

Сегодня даже не придётся всё делать вручную, есть возможность автоматизировать процесс. Есть возможность применять особые комментарии к объектам и создавать взаимосвязь между задачами. Для этого можно использовать TFS. Чем лучше у программиста представление о работе, тем меньше ошибок появится.

Улучшайте свои знания

Разработчику нужно постоянно развиваться и расширять знания о тонкостях работы с целевым языком. Следует понимать основные ошибки синтаксиса, в этом помогут современные анализаторы и IDE. Все равно в каждом языке существуют неочевидные особенности, что способны негативно отразиться на скорости работы приложения, а иногда и вовсе ломают логику. Вы обязаны искусно обладать инструментом, которым пользуетесь. Конечно же, это позитивно отразится на качестве кода. 

Проверка кода

Следует взять за правило Code Review. Во время продолжительного выполнения какой-то задачи часто появляется замыливание глаз. Эффект приводит к тому, что уже перестаёшь видеть легко заметные ошибки. Это действительно частое состояние. Чтобы избежать серьёзных проблем, следует давать свой код на вычитку коллегам. Чтение чужого кода поможет получить полезный опыт и позволит не допустить тех же ошибок. Обмен опытом всегда полезен.

Простой лайфхак

Код – это творение, именно такое отношение к нему и должно быть. Это продукт работы, и он должен быть качественным. Могу дать один простой лайфхак, который применялся на одном из прошлых проектов – «состязание без багов». 

Все члены команды закидывали в общую копилку какую-то сумму и начинали работать. Всю сумму забирал тот программист, у которого в коде меньше всего ошибок. Здоровая конкуренция возродила дух соперничества, и дала стимул писать код лучше.


Надеюсь, перечисленные правила оказались полезными и интересными. В любом случае, только практика поможет допускать меньше ошибок. На начальном этапе работы ошибки – это нормально. Возьмите себе за правило не гнаться за количеством, но акцентировать внимание на качестве.

Виктор Тимофеев, osa@pic24.ru (ноябрь, 2009)

Скачать в PDF-формате

Как писать программы без ошибок

Введение

Про ошибки


По профессии я занимаюсь поиском и исправлением ошибок в чужих программах. За время работы я набрал некоторую коллекцию всевозможных багов и попытался свести их в одну таблицу и классифицировать с тем, чтобы оформить некую брошюру по быстрому поиску и исправлению наиболее часто встречающихся ошибок. Однако произвести такую классификацию не удалось. Дело в том, что, выделив 5-6 основных ошибок, таких как: неправильное приведение типов, путаница со знаковыми и беззнаковыми выражениями, пренебрежение выполнением проверок аргументов функций и пр., — я увидел, что остальные ошибки слишком индивидуальны, чтобы их как-то обобщать. О приемах поиска и говорить нечего, т.к. их намного больше, чем самих ошибок.

Тем не менее, каждый раз, исправляя какую-либо ошибку, я для себя отмечал, что ее можно было и не совершить, если бы то-то, то-то. Со временем я уяснил, что большая часть ошибок совершается из-за неправильного подхода к процессу программирования. Поэтому идея написания брошюры «Локализация и исправление ошибок» перешла в идею написания брошюры «Как не совершать ошибки». И я считаю это правильным, потому что лучше учиться строить, а не восстанавливать плохо построенное. Большая часть советов, описываемых мной, довольно банальны и просты. Вроде как, все их знают, но почему-то многие не придерживаются.

О каких ошибках идет речь. Конечно же, не о тех, которые обнаруживает компилятор при трансляции программы. Пока мы не исправим эти ошибки, нам не удастся получить код, который мы сможем прошить в контроллер. Эти ошибки в большинстве своем означают, что текст не соответствует правилам языка программирования, и, по сути, являются помарками. Также в этом материале не хотелось бы касаться так называемых «запланированных» ошибок (обработка неправильных данных, пришедших извне, ошибочные действия пользователя и пр.). Мы будем говорить об ошибках реализации, т. е. о тех, которые будут проявляться в ходе выполнения программы. Эти ошибки являются следствием неправильно запрограммированного (или составленного) алгоритма, допуска условностей, невнимательности и неаккуратности.

Для кого это пособие


В этом пособии я изложил свой подход к программированию микроконтроллеров, привел свои правила, которыми руководствуюсь при написании программ, и обозначил те моменты, которым при написании программ уделяю особое внимание. Уверен, что моя точка зрения не единственная, а также, что где-то мне самому еще есть чему поучиться. Так что я буду рад, не только если пособие окажется кому-то полезным, но также если кто-то решит меня в чем-то поправить или дополнить.

Статья рассчитана на широкий круг программистов, поэтому я старался не затрагивать концепцию доказательного программирования, и вообще старался привести больше практических советов, чем теории (с которой сам на «Вы») и формалистики. Так же в этом пособии нет описания приемов повышения надежности программ (самодиагностика, троирование, перепроверка действий и пр.).

Не следует считать, что после прочтения данного пособия ваши программы автоматически станут безошибочными, и уж тем более, что в будущем ошибок не будет вовсе. Ошибки все равно будут, но процент их сильно сократится. Более того, на написание программ по правилам, изложенным в этом пособии, будет затрачено время в два, в три, в пять раз больше, чем без правил. Писать программы без ошибок — это труд, причем кропотливый. Просто в отличие от кропотливого труда по поиску и исправлению ошибок, этот вид труда поддается планированию и формализации, т.е. является предсказуемым. А применение описанных здесь правил в разы сократит время непредсказуемого процесса отладки, который, как показывает практика, зачастую отнимает намного больше времени, чем само кодирование.

Предполагается, что читатель знает, что такое контроллер, как он устроен, как включается, наконец, что такое электрический ток, потому что без этих знаний заниматься микроконтроллерами бесполезно. Так что, как говорилось в старом анекдоте: «Учите матчасть!»

Наверх

«Учите матчасть!»

Это самая очевидная рекомендация. Про нее можно было и не говорить, но для полноты картины вскользь коснемся этой темы. Программист, пишущий для контроллеров должен знать схемотехнику, устройство самого контроллера, язык программирования, на котором пишет программу, и особенности используемого компилятора. Тот программист, который пренебрегает необходимостью иметь эти знания, теряет право жаловаться на то, что его программа не работает.

Схемотехника

Как минимум нужно знать:

  • основы электроники (и цифровой и аналоговой)

  • законы Ома и Кирхгофа

  • типовые схематические решения (RC-цепочки, транзисторные ключи)

Кроме того, нужно знать, что необходимо устанавливать диоды параллельно катушкам реле, что светодиоды включаются через токоограничивающий резистор, что у механических контактов есть такое явление как дребезг, и пр.

Контроллер

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

Не нужно стесняться заглядывать в фирменные даташиты, там есть ответы на большинство вопросов. А то у кого-то не работает PORTA (в то время как не отключены аналоговые цепи), у кого-то не появляется «1» на выходе RA4, у кого-то прерывание по RB0 срабатывает только в половине случаев и т.д.

Язык

Конечно же, нужно знать сам применяемый язык программирования.

Для ассемблера это:

  • формат команд;

  • типы и количество операндов

Для языков высокого уровня это:

  • семантика операторов;

  • квалификаторы данных и функций;

  • типы данных;

  • преобразование типов;

  • приоритеты операций;

  • указатели (или указатели на указатели);

Компилятор

Разработчики компиляторов для микроконтроллеров, стремясь адаптировать компилятор под конкретную платформу, позволяют себе отходить от стандартов языка, одновременно не нарушая их. Каждый компилятор имеет свои особенности, незнание которых может привести к трудностям при портировании или при использовании чужих библиотек:

  • набор директив;

  • типы данных (размерность, знаковость);

  • квалификаторы (near, far, const, rom);

  • организация прерываний.

Наверх

Этапы программирования

Когда ТЗ согласовано и задача формализована (переведена с проблемно ориентированных требований в технические: входные/выходные данные, режимы работы и пр.), начинается сам процесс программирования.

  1. Планирование (включает в себя проектирование, составление плана действий, выявление требуемых ресурсов).

  2. Кодирование (запись самой программы в машинном языке)

  3. Отладка (локализация и устранение ошибок)

  4. Тестирование (проверка работоспособности и соответствие спецификации)

  5. Оформление проектной документации

  6. Эксплуатация и сопровождение

Беда в том, что многие пренебрегают планированием, а также считают, что отладка появляется только в том случае, если была допущена какая-то ошибка при кодировании, а так как надеются с первого раза написать без ошибок, то и процесс отладки не учитывается. Однако же, в реальности отладка – это обязательный процесс, на который, вдобавок, приходится большая часть времени, сил и нервов. Закодировать даже самый сложный и запутанный алгоритм – это нетрудно, трудно заставить его работать, или, если они был написан правильно, убедиться в его работоспособности. Поэтому процессы отладки и тестирования самые длительные и самые трудоемкие. Однако, время отладки можно сократить, сделав процесс более предсказуемым и управляемым, а именно – спланировав программу.

Наверх

Планирование программы

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

     #if defined(__PICC__) || defined(__PICC18__)
     typedef signed   char    S08;
     typedef signed   int     S16;
     typedef signed   long    S32;
 
     typedef unsigned char    U08;
     typedef unsigned int     U16;
     typedef unsigned long    U32;
 
     typedef bit              BOOL;
 
     typedef signed   char    S_ALU_INT;
     typedef unsigned char    U_ALU_INT;
     #endif

Обратим внимание на два типа: S_ALU_INT и U_ALU_INT – это знаковое и беззнаковое целые, имеющие размерность машинного слова для конкретного контроллера. Т.к. операции над операндами, имеющими размерность шины данных, производятся наиболее оптимально, иногда есть смысл пользоваться этими типами данных.

Примечание: далее в тексте для наглядности будут использоваться стандартные типы: char (8 бит), int (16 бит), long (32 бита).

Наверх

Написание программы

При написании самого текста программы нужно руководствоваться двумя сводами правил:

Сами правила могут у каждого быть свои, но они должны по возможности исключать разночтения.

Наверх

Кодирование

Наверх

Соблюдать модульность


Мы уже говорили о разбиении программы на модули, еще раз приведу преимущества модульности:

  • Мобильность (легкий перенос модуля в другую программу)

  • Наглядность (легкий поиск определений конкретных функций)

  • Заменяемость (замена одного модуля другим при изменении условий работы, например, при смене внешнего оборудования)

Раз мы решили разбить программу на модули, то нужно следовать этому решению до конца и не совершать каких-либо действий, нарушающих модульность. Т.е. наши модули должны обладать следующими характеристиками:

  • Самобытность
    Функции и переменные, относящиеся к одному модулю, определять именно внутри этого модуля. Понятно, что иначе перенос модуля в другую программу обернется тем, что в каждой новой программе придется переопределять недовнесенные переменные.

  • Самодостаточность
    Не использовать в модуле внешние переменные из верхних модулей. Опять же, причина в том, что при переносе модуля в другую программу придется в новой программе не только доопределять какие-то переменные, но и восстанавливать механизмы их работы с тем, чтобы модуль вел себя правильно.

  • Гибкость в настройке
    Например, если это модуль для работы по шине i2c, то при переносе в другой проект должна быть возможность выбирать (или задавать константами) адрес устройства, разрядность адреса данных, выводы, к которым подключено устройство i2c.

Наверх

Избегать условностей


В программе следует избегать любых конструкций, которые при прочтении могут быть истолкованы двояко, или которые смогут при определенных условиях повести себя неправильно. Самые частые допуски, которые позволяют себе программисты, — это несоблюдение границ использованных типов, путаница со знаковыми и беззнаковыми переменными, пренебрежение приведением типов. Эти допуски рассмотрим в первую очередь.

Типы данных

Нужно определять переменные тем типом, который соответствует их назначению. Например, не следует переменную, которая будет использована как числовая, определять типом char. Число может быть либо signed char либо unsigned char. Сам же char определяет символьную переменную. Понятно, что для контроллера все эти переменные – одно и то же, но для компилятора, а также для человека, читающего текст программы, — это разные вещи.

Например, типичная ошибка:

Неправильно:

char Counter;
Counter = 10;
if (Counter >= 0) ...;

Это выражение будет правильно обрабатываться в MPLAB C18, в то время как в HT-PICC18 оно всегда будет возвращать true. Все из-за того, что в стандарте языка не оговаривается знаковость типа char, и каждый разработчик компиляторов вправе толковать его по-своему. В приведенном примере переменная должна быть определена так:

Правильно:

signed char  Counter;
Counter = 10;
if (Counter >= 0) ...;

Приведение типов

Не стоит надеяться, что компилятор всегда сам сделает приведение типов в случае, когда в выражении участвуют разнотипные переменные и константы. Всегда следует выполнять приведение типов вручную, исключая разночтения выражения программистом и компилятором.

Неправильно:

int   i;
void *p;
p = &i;

Правильно:

p = (void*)&i;

В конкретном примере мы, скорее всего, получим правильный результат и без приведения типов (хотя, возможны нюансы с однобайтовыми near-указателями для PIC18). Но бывает так, что программист, рассчитывая на автоматическое приведение типов, проморгает его отсутствие в более сложном выражении, например: выражение содержит подвыражения в скобках, в которых типы будут приведены только после выполнения подвыражения, т.е. тогда, когда, например, значимость будет уже потеряна (из-за переполнения).

То же самое относится к передаче параметров в функцию.

Побайтовое обращение к многобайтовой переменной

Пример неправильного обращения:

unsigned char lo, hi;
unsigned int  ui;
...
lo = *((unsigned char*)&ui + 0);
hi = *((unsigned char*)&ui + 1);

Дело в том, что стандартом языка не предусматривается порядок чередования байтов в многобайтовых объектах.

Правильное обращение:

lo = ui & 0xFF;
hi = ui >> 8;

Определение функций

При определении функции следует указывать полностью входные и выходные типы. Если функция определена просто:

myfunc ()

, то компилятор по умолчанию будет считать, что она возвращает int, и не принимает параметров. Однако не следует оставлять такой неопределенности.

    myfunc ()       // неправильно
    myfunc (void)   // неправильно
int myfunc ()       // неправильно
int myfunc (void)   // Правильно

Пустые операторы

Не следует в теле цикла while, а также в операторах if…else использовать пустой оператор:

  while (!TRMT);          // Ожидаем освобождение буфера
  TXREG = Data;

Нечаянно пропущенная ‘;’ обернется неправильным поведением программы. Лучше вместо пустого оператора ставить continue либо {}:

  while (!TRMT) continue; // Ожидаем освобождение буфера
  TXREG = Data;

Про оператор switch

В операторе switch нужно:

  • Определять ветку default

  • В каждом case ставить break

  • неиспользуемые break закрывать комментариями

switch (...)
{
    case 0x00:
         ...
         break;
    case 0x01:
         ...
         // break; // Поставив закомментированный break, мы даем себе понять,
                   // что после обработки условия 0x01 мы хотим перейти к коду
                   // обработки условия 0x02, а не пропустили break случайно
    case 0x02:
         ...
         break;
 
    default:       // Обязательная ветка, даже если есть уверенность, что
                   // выражение в switch всегда принимает одно из описанных
                   // значений
         break;    // Ветка default также должна заканчиваться break
}

Неинициализированные переменные

Нельзя пользоваться неинициализированными переменными в расчете на то, что компилятор сам сгенерит код их инициализации (например, обнулит после сброса).

Скобки в сложных выражениях

В некоторых случаях в сложных выражениях есть смысл расставлять скобки даже тогда, когда есть уверенность в правильности приоритетов операций. Такие выражения легче анализировать, т.к. ошибка может быть не только в приоритетности операций, но и в самом выражении. Также это снижает вероятность внесения ошибки при модификации выражения.

«Такая ситуация никогда не случится!»

На эту тему можно вообще долго говорить. Вот интересный фрагмент определения типа из одной из присланных мне программ:

typedef struct
{
    unsigned int   seconds : 6;
    unsigned int   minutes : 6;
    unsigned int   hours   : 4;    // Неправильно задана размерность
} T_CLOCK;

Обратите внимание на размерность полей данной структуры. Под минуты и под секунды выделено по 6 бит, чтобы охватить весь интервал 0..59. А под часы вместо 5 бит, выделено 4. Программист, написавший это, предположил, что т.к. программа будет работать только с 8 утра, то проще выделить 4 бита (покрывая интервал в оставшиеся 16 часов) с тем, чтобы вся структура влезла в два байта, а в самой программе к значению hours всегда прибавлять 8. Надо ли говорить, что сбой не заставил себя должго ждать?

Так что не стоит забывать о первом законе Мерфи: «Если неприятность может случиться, — она случается».

Мертвые циклы

Часто в программах встречаются участки кода, которые потенциально могут привести к зависанию. Самый распространенный пример – ожидание готовности или подтверждения при работе с внешней периферией:

Неправильно:

void lcd_wait_ready (void)
{
    while (!PIN_LCD_READY) continue;
}

Если произошла непредвиденная ситуация (обрыв линии, короткое замыкание, конденсат и пр.), то из этого цикла мы никогда не выйдем. (Разве что только WDT сработает). Поэтому нужно всегда предусматривать аварийный выход из таких циклов. Можно это делать с помощью таймера.

Правильно:

char  lcd_wait_ready (void)
{
    TMR0   = -100;              // Готовим таймер для фиксации таймаута
    TMR0IF =    0;
 
    while (!PIN_LCD_READY)
    {
        if (TMR0IF) return 0;   // Выходим с кодом ошибки
    }
 
    return 1;                   // Выходим OK
}

Правда, целый таймер выделять для этого иногда накладно, и есть смысл работать с глобальной переменной, которая будет уменьшаться в прерывании по таймеру:

    // Фрагмент обработчика прерывания
 
    if (TMR0IF && TMR0IE)
    {
        TMR0IF = 0;
        TMR0 -= TMR0_CONST;     // Обновляем таймер
 
        if (!--g_WaitTimer)     // Проверяем переполнение
            g_Timeout = 1;
        ...
    }
 
...
 
char  lcd_wait_ready (void)
{
    g_WaitTimer = 10;           // Готовим таймер для фиксации таймаута
    g_Timeout = 0;
 
    while (!PIN_LCD_READY)
    {
        if (g_Timeout) return 0;// Выходим с кодом ошибки
    }
 
    return 1;                   // Выходим OK
}

Кстати, заметьте, что в обработчике прерывания проверяется не только флаг конкретного прерывания (TMR0IF), но и бит разрешения (TMR0IE). Дело в том, что в PIC-контроллерах младшего и среднего семейства несколько прерываний могут обрабатываются одним обработчиком. И если у нас прерывание по TMR0 отключено (TMR0IE = 0), а в обработчик мы попали от другого источника (например RCIF), то без проверки битов xxxIE мы обработаем все отключенные прерывания, у которых на момент входа в обработчик оказался установлен флаг xxxIF.

Наверх

Не делать длинных и сложных выражений


Такие выражения не только трудно анализировать, но их также трудно тестировать и отлаживать.

Неправильно:

t = sqrt(p*r/(a+b*b+v))+sin(sqrt(b*b-4*a*c)/(1<<SHIFT_CONST));

Представьте, что программа делает вычисления неправильно и есть подозрения, что проблема в этом выражении. А это выражение даже в симуляторе не прогнать. Такие выражения желательно разбивать на несколько подвыражений:

Правильно:

A = p*r;
B = a + b*b + v;
C = b*b – 4*a*c;
D = (1 << SHIFT_CONST);
 
if (B == 0) ...;      // Ошибка: «деление на 0»
if (C < 0)  ...;      // Ошибка: «корень из отрицательного числа»
 
E = A/B;
If (E < 0)   ...;     // Ошибка: «корень из отрицательного числа»
 
t = sqrt(E) + sin(sqrt(C)/D);

Теперь наше выражение не только легко читается, но и легко тестируется и отлаживается, а кроме того, – еще и имеет механизм защиты от неправильных входных данных (этот механизм можно было бы оставить и с длинным выражением, но тогда некоторые части выражения пришлось бы пересчитывать дважды).

Наверх

Операторные скобки


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

Рассмотрим пример одной часто встречающейся ошибки:

Неправильно:

if (A == 1) if (B == 2) C = 3;
else C = 4;

Замысел программиста был таков: если A равняется 1, то при B, равном 2, присвоить C значение 3, иначе присвоить C значение 4. Т.е. программист считал, что в C будет занесено значение 4, если A не равно 1. Однако на самом деле компилятор видит это по-другому: else применяется к ближайшему if, а не к тому, который выровнен с ним в тексте. В нашем случае else относится к условию if (b == 2).

Правильно это условие нужно было записать так:

if (A == 1)
{
    if (B == 2) C = 3;
}
else C = 4;

Наверх

Операторы break и continue во вложенных циклах


Часто встречающейся ошибкой является использование break или continue во вложенных циклах или операторе switch в расчете на то, что эти операторы сработают в рамках и внешнего цикла. Вот пример из реальной программы (он немного порезан для наглядности, и ошибка сразу бросается в глаза), который подсчитывал количество положительных и отрицательных единиц в массиве, причем нулевой элемент был признаком конца массива.

Неправильно:

Positive = Negative = 0;
for (i = 0; i < MAX_ARRAY_SIZE; i++)
{
    switch (array[i])
    {
        case  0:
              break;       // Выйти из цикла (ошибка, т.к. выйдем
                           // только из switch)
        case  1:
              Positive++;
              break;
        case -1:
              Negative++;
              break;
    }
}

Программист решил использовать для проверки конструкцию switch, в которой, среди прочего, при нахождении элемента со значением 0 счет должен был прерваться. Однако, очевидно, что break в ветке case 0 выведет программу из switch‘а, а не из цикла for.

Есть несколько способов решить эту проблему. Один из вариантов в данном случае – использовать дополнительный if:

Правильно:

Positive = Negative = 0;
for (i = 0; i < MAX_ARRAY_SIZE; i++)
{
    if (array[i] == 0) break;       // Выйти из цикла
 
    switch (array[i])
    {
        case  1:
              Positive++;
              break;
        case -1:
              Negative++;
              break;
    }
}

Однако он не очень эффективный, т.к. приводит к повторному вычислению адреса элемента массива при косвенной адресации. Есть и другие варианты решения проблемы. Главное – помнить, что break/continue прерывает/продолжает только текущий цикл (или switch).

Наверх

Точность вещественных чисел


Когда появляется необходимость работы с вещественными числами с плавающей запятой, нужно внимательнее изучить теорию об их структуре (мантисса, порядок, знаки). У некоторых программистов, которые с ней не знакомы, при виде диапазона +/- 1.7e38 появляется иллюзия всемогущества и универсальности этого типа данных. При этом из вида упускаются такие важные детали, как потеря значимости, нормирование, переполнение, относительная погрешность.

В одной программе я видел такой фрагмент:

long l;
float f;
f = 0.0;
 
for (l = 0; l < 100000L; l++)
{
    ...;
    f = f + 1.0;
    ...;
}

(Примечание: в программе был использован вещественный тип размерностью 24-бита.)

На месте троеточий делались кое-какие вычисления, одним из параметров в которых была переменная f. В этом цикле первые две трети результатов оказывались правильными, а дальше появлялись отклонения, причем, чем дальше, тем сильнее.

А происходило следующее: когда переменная f досчитывала до значения 65536 (т.е. выходила за пределы 16-разрядной мантиссы), ее экспонента (порядок) увеличивалась на единицу. При этом вес младшего разряда мантиссы оказывался равным 2. При выполнении сложения двух вещественных чисел f и 1.0 сперва производится приведение их к общей экспоненте, в результате которого 1.0 превращается в 0, т.к. при сложении в обоих числах порядок выравнивается в большую сторону, т.е. вес младшего разряда мантиссы в обоих операндах будет равен 2, при этом происходит потеря значимости, и результат сложения 65536.0 + 1.0 будет равен 65536.0.

Также стоит отметить, что в общем случае для вещественных чисел нельзя проверять деление умножением.

Наверх

Целочисленное деление

Округление

Часто встречается такой допуск при делении целых чисел, будто бы компилятор самостоятельно должен производить округление. Так вот: он этого не делает, это должен делать сам программист. Типичная ошибка, например, при расчете скорости USART’а:

Неправильно:

#define FOSC       200000000L
#define BAUDRATE      115200L
...
SPBRG = FOSC / (BAUDRATE*16)1;

При расчете на бумаге все выглядит красиво:

  20000000/(115200*16) – 1 = 9.85 

и округляется в большую сторону до 10. Ошибка скорости при этом составляет:

  E = |10 - 9.85| / 9.85 * 100% = 1.5%

, что в допустимых пределах.

Однако, когда мы поручаем компилятору вычислить формулу в том виде, в котором мы ее привели, он округление произведет не по правилам математики (<0.5 – в меньшую сторону, >=0.5 – в большую), а просто откинет дробную часть. В результате ошибка будет уже:

  E = |9 - 9.85| / 9.85 * 100% = 8.6%

, т.е. в 4 раза превышает допустимую.

Та же проблема всплывает при любых расчетах с целочисленными переменными и константами, где присутствует операция деления.

Для того чтобы округление производилось правильно, нужно к числителю прибавлять половину знаменателя:

Правильно:

    SPBRG = (FOSC + BAUDRATE*8) / (BAUDRATE*16)1;

В общем случае формула “A = B / C” при использовании целых чисел в программе должна быть записана так:

   A = (B + C / 2) / C;   

Операцию деления на 2 (в выражении C/2) целесообразно заменять сдвигом вправо на один разряд. Обратите внимание, что деление на 2 – это то же самое целочисленное деление, но выражение записывается C/2 , а не (C+1)/2, т.е. к числителю не прибавляется половина знаменателя. Почему? Не вдаваясь в подробности насчет различия погрешностей при делении на разные значения (в нашем примере деление на 2 и деление на C), приведу тривиальный пример: «разделить 1 на 1». Результат деления должен быть равен 1:

  (1 + 1 / 2) / 1 = (1 + 0) / 1 = 1

Если мы при делении на 2 в дроби 1/2 прибавим к числителю половину знаменателя, то получится:

  (1 + (1 + 2/2) / 2) / 1 = (1 + 2 / 2) / 1 = (1+1) / 1 = 2

Последовательность делений и умножений

Дополню еще одной рекомендацией: если в выражении присутствуют операции как умножения так и деления, то формулу лучше переписать так, чтобы сперва выполнялись операции умножения, а затем деления:

Неправильно:

A = B / C * D;

Правильно:

A = B * D / C;

Причины очевидны: так как приоритет операций умножения и деления одинаков, то выражение будет вычисляться слева направо, причем округление будет выполняться после каждой операции. Таким образом, в первой формуле на D будет умножено не только отношение B/C, но еще и ошибка округления.

Рассмотрим пример: «2 / 3 * 3». Результат выражения должен быть равен 2. Сначала перепишем это выражение по правилам округления, как было описано выше (иначе 2/3 дадут результат 0, и результат всего выражения тоже будет нулевой):

  (2 / 3) * 3 -> ((2 + 3/2) / 3) * 3 = ((2 + 1) / 3) * 3 = (3/3) * 3 = 1 * 3 = 3

Как видим, мы получили неправильный ответ. Почему? Потому что на 3 была умножена еще и ошибка округления отношения 2/3. Ошибка была равна 1/3, и, помножив ее на 3, мы получили лишнюю 1 (проблема произойдет также и при округлении вниз). Правильно было сначала произвести умножение, а потом деление, т.е. «2*3/3»:

  (2 * 3) / 3 -> (2 * 3 + 3/2) / 3 = (6 + 1) / 3 = 7 / 3 = 2

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

Наверх

Правила для констант

Не использовать числовые константы

Речь идет о неиспользовании числовых констант в оперативной части кода. Например, в программе предполагается использовать массив размерностью 25 элементов. Не следует по всему тексту программы явно указывать константу 25.

Пример неправильного подхода:

char String[25];
...
for (i = 0; i <= 24; i++) String[i] = ‘ ‘;

Правильно в одном месте программы определить константу, а потом в тексте оперировать только ее именем:

#define STRING_SIZE      25
...
char String[STRING_SIZE];
...
for (i = 0; i <= STRING_SIZE - 1; i++) String[i] = ‘ ‘;

Дело в том, что если по каким-то причинам придется изменить размерность массива, то придется и по всему тексту программы выискивать все относящиеся к размерности фрагменты и исправлять их. Пропустив всего один такой фрагмент, можно получить редко проявляющийся но довольно разрушительный баг. (Обратите внимание на то, что применен оператор «<= 24», а не «< 25»; я специально сделал для примера такой вариант, чтобы было понятно, что при изменении размерности массива простой поиск с заменой может не дать полноценного результата.)

Поэтому всем константам, не являющимся неоспоримыми (например, в минуте 60 секунд, в килобайте 1024 байта, при переводе в десятичную систему всегда число делим на 10 и т.д.), следует давать имена и в программе работать только с именованными константами.

Указывать тип константы

Неправильно:

#define FOSC               4000000
#define MAX_INTERATIONS      10000
#define MIDDLE_TEMPERATURE      25

Если с определением FOSC сомнений не возникает (компилятор однозначно поймет, что это 32-битная константа), то с MAX_ITERATIONS будут проблемы. Если где-нибудь в коде встретится выражение:

(MAX_ITERATIONS * 10)

, то результатом его будет не 100000, а… -31072. Почему? Потому что по умолчанию константа будет рассматриваться как знаковое 16-разрядное целое. Результат 100000 выходит за границы 16 разрядов, поэтому он будет обрезан до 0x86A0, что соответствует -31072.

То же самое касается констант, которые должны быть вещественными. Если в программе встретится выражение:

(MIDDLE_TEMPERATURE / 26)

, то результатом будет 0, а не предполагаемые 25/26 = 0,96.

Правильно:

#define FOSC               4000000L
#define MAX_INTERATIONS      10000L
#define MIDDLE_TEMPERATURE      25.0

Задавать константам осмысленные значения

Вот пример из реальной программы:

#define BAUDRATE     24     // 9600

В функции инициализации периферии была такая строчка:

SPBRG = BAUDRATE;

Программа должна была работать с тактовой частотой 4 МГц. А для такой тактовой частоты константа BAUDRATE должна иметь значение 25, а не 24. Таким образом, ошибка скорости составила 4%, что, в общем-то, почти на грани допустимого. В результате иногда принимались не те данные, которые ожидались. В разговоре с программистом выяснилось, что ранее программа работала по USART на скорости 19200 (константа BAUDRATE=12), но потом из-за смены внешнего оборудования пришлось изменить скорость на 9600, что программист и сделал, умножив константу на 2.

Но беда еще и в том, что даже с комментарием «9600» такое определение константы может заставлять задумываться. Ведь если при отладке выясняется, что что-то не так с приемом/передачей по USART, то эту константу придется проверять по формуле. Не говоря уже о том, что при выборе другой скорости или при смене тактовой частоты константу придется пересчитывать. И не надо забывать, что в программу может заглядывать не только автор, а для чужого человека заглядывание в подобный код равносильно написанию собственного.

Понятное дело, что в нашем примере программист просто ошибся с формулой (т.к. должен был сперва прибавить 1, потом умножить на 2, а затем отнять 1), но ошибки можно было бы избежать, если бы константа задавалась осмысленной:

#define FOSC     40000000L
#define BAUDRATE     9600L

А уже при присваивании регистру SPBRG переводить эту константу в нужный вид с учетом тактовой частоты, также заданной в виде константы.

SPBRG = ((FOSC + BAUDRATE*8) / (BAUDRATE*16)1;

Итак, константы лучше задавать в том виде, в котором мы привыкли их видеть, т.е.:

  • время задавать в секундах, а не в тиках 65536 мкс;

  • давление – в паскалях (или в барах), а не в единицах АЦП;

  • Напряжение – в вольтах;

  • температуру – в градусах;

  • частоту – в герцах.

Это, во-первых, сделает программу более читабельной, а во-вторых, поможет избежать ошибок, которые будут являться следствием пересчетов из одной системы в другую. Правильнее один раз настроить формулу преобразования, чем каждый раз по ней же пересчитывать константы.

Два слова о проверке правильности задания констант

Задавая константу в понятном для нас виде, хотелось бы одновременно быть уверенным в том, что ее значения не выходят за рамки возможностей контроллера. Например, BAUDRATE из нашего примера должна обеспечивать точность скорости +/- 2%. Или давление, которое мы задаем в барах, должно быть таким, чтобы при переводе его в единицы АЦП получилось значение в пределах 1023. Поэтому в программе нужно блокировать неправильно заданные константы условными директивами и сообщениями об ошибке. Например:

// Задание параметров
 
#define FOSC         4000000L
#define BAUDRATE        9600L
 
// Вычисление ошибки (в процентах)
 
#define SPBRG_CONST    ((FOSC + BAUDRATE*8) / (BAUDRATE*16) - 1)
#define REAL_BAUDRATE  ((FOSC + (SPBRG_CONST+1)*8)/((SPBRG_CONST + 1)*16))
#define BAUDRATE_ERROR ((100L * (BAUDRATE - REAL_BAUDRATE) + BAUDRATE/2) / (BAUDRATE))
 
// Проверка ошибки на диапазон -2%..+2%
 
#if BAUDRATE_ERROR < -2 || BAUDRATE_ERROR > 2
    #error "Неправильно задана константа BAUDRATE"
#endif

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

Примечание. В компиляторах HTPICC и HTPICC18 проверка константы BAUDRATE_ERROR директивой #if работать не будет (из-за ошибки компилятора). Проверку нужно делать либо в runtime’е, либо использовать другую формулу.

Соблюдать систему счисления

При задании константы нужно соблюдать ту систему счисления, которая подходит константе по сути. Бывает, делают так:

Неправильно:

TRISA = 21;    // RA0,RA2,RA4 – вход, RA1, RA3 - выход
TRISB = 65;    // 65 = 0x41 = 0b01000001

В данном случае нужно задавать константу именно в бинарном виде (хотя, надо помнить, что бинарная запись числа не предусмотрена стандартом Си; тем не менее, почти все компиляторы для PIC’ов такую запись понимают). Каково будет программисту поменять один бит в такой записи?

Правильно:

#define TRISA_CONST     0b00010101      // RA0,RA2,RA4 – входы
#define TRISB_CONST     0b01000001      // RB0, RB6 - входы
...
TRISA = TRISA_CONST;
TRISB = TRISB_CONST;

В этом случае четко видно, где единицы – там входы, нули выходы.

Также часто встречается довольно нелепая запись:

Неправильно:

TMR1H = 0xF0;
TMR1L = 0xD8;     // Отсчет 10000 тактов

Хорошо еще, если ее снабдят комментариями, хотя, как мы убедились на примере с BAURDATE, комментарий не гарантирует правильности константы. Опять же большие трудности возникают при пересчете констант. В данном случае нужно пользоваться именно макросом (тут есть тонкость для контроллеров PIC18: TMR1H – это буферный регистр, и нам важна последовательность присваивания: сначала старший, затем – младший; стандартом языка последовательность не предусмотрена).

Правильно:

#define TMR1_WRITE(timer)    { TMR1H = timer >> 8; TMR1L = timer & 0xFF; }
...
TMR1_WRITE(-10000);

Наверх

Заключать константы и операнды макросов в круглые скобки


Типичная ошибка при определении числовых констант в виде выражения – не использование скобок.

Вот пример неправильного определения:

#define PIN_MASK_1    1 << 2
#define PIN_MASK_2    1 << 5
...
PORTB = PIN_MASK_1 + PIN_MASK_2;

Данное выражение развернется компилятором в такое:

  PORTB = 1 << 2 + 1 << 5;

Вспомним, что приоритет операции «+» выше, чем приоритет сдвига, и получим выражение:

  PORTB = 1 << (2 + 1) << 5 = 1 << 8

Т.е. совсем не то, что мы ожидали. Ошибки можно было бы избежать, взяв выражения при определении констант в скобки, т.е.

правильное определение:

#define PIN_MASK_1    (1 << 2)
#define PIN_MASK_2    (1 << 5)
...
PORTB = PIN_MASK_1 + PIN_MASK_2;

Транслируется в:

PORTB = (1 << 2) + (1 << 5);

Из этой же области часто совершаемая ошибка – не заключение в скобки операндов макросов. Например, имеем макрос:

Неправильно:

#define MUL_BY_3(a)    a * 3

Выглядит просто и красиво, однако, попробуем вызвать макрос так:

i = MUL_BY_3(4 + 1);

И вместо предполагаемого результата 15 получим результат 7. Дело в том, что макрос развернется в следующее выражение:

  i = 4 + 1 * 3

Ошибки можно было бы избежать, взяв аргумент макроса в скобки:

Правильно:

#define MUL_BY_3(a)   (a) * 3

Тогда выражение примет вид:

  i = (4 + 1) * 3;

Наверх

Заключать тела макросов в фигурные скобки


Тематика схожа с предыдущей. Рассмотрим пример:

Неправильно:

#define I2C_CLOCK()    NOP(); SCL = 1; NOP(); SCL = 0;

Т.е. выдерживаем паузу в один такт, затем формируем импульс длительностью два такта. Но такое определение может сыграть с нами злую шутку, если макрос будет использоваться внутри условного оператора:

if (...) I2C_CLOCK();

Компилятор развернет макрос следующим образом:

if (...) NOP(); SCL = 1; NOP(); SCL = 0;

Как видно, условием отрезается только один NOP(), а сам импульс все-таки формируется. Ошибки можно было бы избежать, взяв тело макроса в фигурные скобки:

Правильно:

#define I2C_CLOCK()    do { NOP(); SCL = 1; NOP(); SCL = 0; } while (0)

Такие ошибки крайне трудно отслеживать, т.к. на вид все выглядит правильно, и ошибку будем искать перед условием, после условия, в выражении условия, но не в I2C_CLOCK().

Обратим внимание, что использована конструкция do {…} while (0). Если бы мы ее не использовали, то
постановка else в нашем условии:

if (...) I2C_CLOCK(); else return;

привела бы к сообщению компилятора об ошибке «inappropriate else». Все дело в том, что мы перед else и после ‘}’ ставим ‘;’, которая воспринимается компилятором как конец оператора if. Поэтому и использованы скобки в виде do {…} while.

Наверх

Правила для функций

Объявлять прототипы для всех функций

Для любой функции, объявленной в модуле, нужно объявлять прототип в начале файла с тем, чтобы не задумываться при вызовах, где функция определена: выше вызова или ниже. Кроме того, при исследовании исходного текста программы это сразу дает общую картину: какие функции есть в модуле, какие параметры они принимают и какие значения возвращают. Вдобавок это позволяет сгруппировать наиболее значимые функции в начале файла, а все вспомогательные определить где-нибудь внизу, чтобы не мешались.

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

Проверять входные аргументы функций на правильность

Перед выполнением каких-либо операций с данными нужно убеждаться в их правильности:

  • Инициализированные указатели;

  • Попадают в область допустимых значений (например, номер элемента в массиве должен быть меньше размерности массива, чтобы не произошла запись в стороннюю ячейку); строка, содержащая имя, не может состоять из цифр.

  • Соответствуют реальности (например, счетчик битов в байте не может быть больше 8; температура в комнате не может быть -128 градусов Ц);

Часто область допустимых значений и соответствие реальности – это одно и то же. Однако, есть случаи, когда требуются дополнительные проверки. Например, если параметром является структура, то можно сначала определить, что значение каждого поля в отдельности попадает в область допустимых значений, а потом по совокупности проверять соответствие данных друг другу.

Возвращать функцией код ошибки

Желательно предусмотреть возможность любой функцией возвращать код ошибки. Например, при неправильных входных данных, при ошибке в вычислениях (деление на ноль, переполнение), при ошибке работы с внешними устройствами. Причем, учитывая, что сама ошибка может произойти на самом нижнем уровне вложенных функций, нужно предусмотреть возможность передачи значения ошибки наверх.

Для возврата кода ошибки иногда удобно пользоваться неиспользуемой результатом области значений. Но есть функции, возвращающие результат, который может принять любое значение из множества значений используемого типа. Например, функция перемножения двух чисел, или функция чтения байта из EEPROM. Также может вызвать трудности ситуация, когда вложенные функции возвращают результаты разных типов, что может помешать сквозной передаче кода ошибки. В таких случаях есть смысл пользоваться глобальной переменной. Однако, такой вариант затруднителен при использовании сторонних библиотек.

Все коды ошибок должны быть предопределены константами. Нежелательно пользоваться для их определения конструкцией enum с неинициализированными значениями, т.к. после первого релиза все будут снабжены спецификацией, где ошибки будут расписаны по номерам, а в следующей версии будет добавлен еще один код ошибки (втиснут куда-то в середину списка), из-за чего половина констант изменят свое состояние. Поэтому их нужно определять либо через #define, либо через инициализированный список перечислений:

enum ENUM_ERROR_CODES
{
    // Математические ошибки
    ERROR_DIV_BY_ZERO = 0x100
    ERROR_OVERFLOW,
    ERROR_UNDERFLOW,
    ...
    // Ошибки работы с EEPROM
    ERROR_EEPROM_READ = 0x110
    ERROR_EEPROM_WRITE,
    ERROR_EEPROM_INIT,
    ...
}

Следует отметить, что коды ошибок, генерируемые функциями, — это не те ошибки, которые можно показывать пользователю. Например «ДЕЛЕНИЕ НА НОЛЬ» на экране стиральной машины приведет хозяйку в панику, если внутри ее любимая блузка. Я уж не берусь предугадать действия суеверного человека, если ему вывести шестнадцатеричное представление числа 57005.

Не делать очень больших функций

Эта рекомендация схожа с рекомендацией «Не делать длинных выражений». Если функция получается длинной, то есть смысл разбить ее на несколько функций. Это даст нам следующие преимущества:

  • упростит внутреннюю логику функции;

  • сделает ее более читабельной;

  • упростит тестирование и отладку.

Наверх

Использовать сторожевой таймер


Сторожевой таймер в контроллерах имеет три применения:

  • пробуждение контроллера из режима SLEEP с заданным инетервалом;

  • произведение аппаратного сброса при зависании программы.

  • Для PIC-контроллеров младшего семейства – выполнение программного сброса

Рассмотрим для начала второе применение, т.е. использование его в качестве дополнительного механизма защиты от зависания. Понятно, что лучше создавать правильные независающие алгоритмы и описывать их правильными независающими программами, но, к сожалению, часто даже в отлаженной и переотлаженной программе могут скрываться ошибки, которые при определенном стечении обстоятельств приведут к зависанию. Вдобавок можно сказать, что сложные программы с большим количеством состояний и параллельных процессов бывает сложно отладить и протестировать полностью. Полное тестирование таких программ может затянуться на время, превышающее экономически целесообразные сроки выпуска устройства. В таких случаях приходится идти на некий компромисс между полнотой теста и сроками выпуска, т.е. программа может выйти, грубо говоря, недоотлаженной. Это не значит, что она будет сбоить на каждом шагу и выдавать какие-то результаты, не соответствующие спецификации. Это значит, что при определенных условиях (чаще при совокупности внештатных ситуаций) возможно неадекватное поведение программы. И здесь нас может выручить сторожевой таймер, который не даст программе зависнуть наглухо. Конечно, он не является панацеей от неправильно составленного или неправильно запрограммированного алгоритма. Он всего лишь перезапустит программу, но не исправит допущенной ошибки. Тем не менее, с ним, по крайней мере, есть возможность восстановить работоспособность устройства.

Когда нужно обрабатывать WDT

Во многих программах вижу «интересный» прием программистов: в битах конфигурации включали WDT, а в самой программе сбрасывали его везде, где попало, лишь бы не произошло переполнение: и в главном цикле программы, и в прерывании, и в функции задержки. Сделают так – и радуются: «Ай, какую я надежную программу написал! С вочдогом!» Этот подход эквивалентен подходу без использования WDT с той разницей, что у программиста появляется еще одна иллюзия по поводу отказоустойчивости его программы.

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

Для сложных программ требуются разработки индивидуальных алгоритмов, которые бы отслеживали не только факт выполнения каких-то функций, но также следили за тем, чтобы функции выполнялись в правильной последовательности, и чтобы время выполнения каждой функции было в пределах допустимого.

Здесь хотел бы пару слов сказать про третье применение WDT, т.е. программный сброс в контроллерах младших семейств, т.к. они не имеют отдельной инструкции, вроде RESET в PIC18. В системах с повышенными требованиями по надежности программа должна иметь механизмы контроля правильности работы. Т.е. следить за стеком, за порядком действий, временами выполнения отдельных узлов, настройками периферийных модулей и т.д. При обнаружении отклонений, которые не могут быть исправлены на лету (например, замечено, что в функцию попали не через вызов CALL, а каким-то другим путем), единственный выход – это сброс. Но если в контроллерах среднего и старшего семейств есть инструкции, выполняющие сброс, то в контроллерах младшего семейства единственный способ выполнить сброс программно – это дать WDT досчитать до конца.

Что делать, если произошел сброс по WDT

Порядок действий при сбросе контроллера (не только по WDT) — это отдельная тема (довольно большая). Здесь опишу только в двух словах.

Сделать нужно две вещи:

  • Восстановить работу (по возможности сделать это незаметным для пользователей)

  • Выяснить причину сброса.

С первым все понятно: все переменные, отвечающие за текущий режим работы, за состояние процессов, какие-то текущие рабочие данные и пр. должны быть определены в неинициализируемой части RAM, т.е. в той, в которую код стартапа ничего не пишет при сбросе. Для HT-PICC такие переменные должны быть определены с квалификатором persistent:

persistent unsigned char Mode;
persistent unsigned int  LastData[10];

Для MPLAB C18 они должны быть определены в секции udata, т.е. не инициализироваться при определении.

В самом начале функции main нужно проверять причины сброса (биты POR, BOR, TO, PO, RI и пр.), и в зависимости от их состояния принимать решение о том, восстанавливать ли прежний режим работы, начинать ли все с нуля, переходить ли в аварийный режим и т.д.

Теперь о выяснении причин сброса. Здесь многое зависит от стадии разработки устройства и его доступности. В зависимости от того, отлаживаем ли мы устройство, тестируем ли в реальных условиях или же устройство в эксплуатации, в зависимости от доступности самого устройства разработчику, от способности самого устройства известить о несанкционированном сбросе, от средств его связи с внешним миром и пр. факторов – средства выяснения причин могут быть различными. Мы не будем говорить о технике получения данных, которые нам смогут помочь установить причину сброса, из контроллера. Это может быть что угодно: сброс данных в отдельную страницу внешней EEPROM, передача их через какой-нибудь интерфейс для протоколирования, отображение на мониторе (или ЖКИ) и т.д. Я только укажу, какие данные могут быть полезными в общем случае:

  • Содержимое стека

  • Значения регистров указателей (FSR)

По этим данным зачастую можно сделать вывод о том, в каком месте произошел сбой. При использовании компиляторов от microchip для получения этих данных придется писать свой код startup, т.к. фирменная функция заранее предустанавливает регистры FSR (WREG14, WREG15 для MCC30). В каждом частном случае могут понадобиться дополнительные данные: текущий режим, какие-то индикаторы обработки критических участков кода и пр. Чем больше будет данных, тем проще будет анализировать причину сброса. Но нужно также искать некий компромисс, чтобы не загромождать дамп всякой ерундой, а еще, чтобы не дать пользователю запаниковать, если устройство уже в эксплуатации.

Наверх

Два слова об операторе GOTO


Многие авторитетные источники строго не рекомендуют использовать оператор goto при написании программ на языках высокого уровня. Основными недостатками его применения называется сильное ухудшение читабельности текста программы и нарушение ее структурности. Да и вообще неизвестно, через что мы перепрыгиваем, выполняя goto (может быть через объявление переменной). Кроме того, часто упоминается то, что формально доказано, что любая программа, использующая goto, может быть переписана без его использования с полным сохранением функциональности. Доводы весьма убедительны, а небезызвестный Дейкстра свою статью «Доводы против оператора goto» вообще начинает с тезиса «… квалификация программистов – функция, обратно зависящая от частоты появления операторов goto в их программах».

Сам я не люблю использовать этот оператор и испытываю большие трудности с анализом чужого кода, где он применяется. Однако, применительно к микроконтроллерам с ограниченными ресурсами я бы оправдал использование goto в некоторых случаях.

Выход из вложенных циклов

Этот пример приводится чаще всех остальных. Действительно, использование оператора goto выглядит довольно простым (и наглядным) решением для выхода из циклов вида (удобны, например, при поиске вариантов решений в многомерных массивах):

for (i = 0; i < 10; i++)
{
    for (j = 0; j < 15; j++)
    {
        if (...) goto BREAK_LOOP;
    }
}
BREAK_LOOP:

В принципе, при чтении такого кода не вызывает трудностей найти метку, т.к. по смыслу операции понятно, что она внизу, после закрывающей скобки верхнего цикла. Ярые противники goto приводят два варианта альтернативного кода, позволяющего избавиться от этого оператора.

Вариант 1 – переписать цикл в виде функции.

void loop_func (void)
{
    int i, j;
    for (i = 0; i < 10; i++)
    {
        for (j = 0; j < 15; j++)
        {
            if (...) return ;
        }
    }
}

Вариант 2 – использовать переменную-флаг.

bool StopLoop = false;
for (i = 0; i < 10 && !StopLoop; i++)
{
    for (j = 0; j < 15 && !StopLoop; j++)
    {
        if (...)
        {
            StopLoop = true;
            break;
        };
    }
}

(В принципе есть еще альтернативы, но они контекстно-зависимые и в общем случае применимы не во всех случаях)

Оба варианта полностью работоспособны, однако имеют свои небольшие недостатки, когда речь идет о работе с микроконтроллерами, имеющими дефицит ресурсов. Первый вариант не очень удачен, поскольку требует дополнительный свободный уровень стека (помним, что в PIC16 их всего 8, а в PIC18 — 32), что иногда может оказаться критичным. Второй вариант требует использования дополнительной переменной, что также существенно для контроллеров с малым ОЗУ. Т.е., применяя goto, мы имеем возможность сэкономить ресурсы контроллера. (Об экономии скорости я здесь не говорю, поскольку на фоне цикла из 10×15=150 итераций лишний вызов/возврат или две лишних проверки флага StopLoop будут несущественны).

«Стандартные» метки

Лично я позволяю себе использовать метки, которые могут считаться универсальными почти для любого случая. Таких меток немного. Типичный пример – выход из функции при обнаружении ошибки в ходе ее выполнении. Ошибку функция может обнаружить на любом этапе своего выполнения (как при проверке аргументов на правильность, так, например, и при работе с внешней периферией, и при проверке контрольных сумм и т.д.), а при выходе ей требуется освободить занимаемые ресурсы и установить флаг возникновения ошибки, так что обычный return нас может не устроить просто потому, что перед каждым return придется выполнять одну и ту же последовательность действий.

Главное для меня – местоположение таких меток всегда однозначно (например, понятно, что метка, куда переходит функция при обнаружении ошибки, находится в конце функции), их использование безопасно и с точки зрения экономии ресурсов целесообразно (не любой алгоритм удобно программировать приемами структурного программирования, есть случаи, когда такие приемы делают код менее читабельным и более ресурсоемким).

Оптимизация

Некоторые операции, такие как расчеты или работа с быстрыми сигналами, требуют оптимизации кода по скорости. В таких случаях я предпочитаю использовать операторы goto вместо if…else, break, continue и пр. из соображений экономии времени выполнения, даже в ущерб наглядности. Такие участки кода должны быть тщательно проверены и перепроверены и детально откомментированы.

Наверх

Атомарный доступ


Часто причиной ошибки в программах может быть непредусмотренный механизм атомарного доступа к регистрам или переменным. Самый простой пример: на 8-битном контроллере имеем 16-битную переменную, которая обрабатывается как в основной программе, так и в обработчике прерывания. Конфликты при работе с ней могут возникнуть, если во время обращения к ней из основного тела программы возникает прерывание, обработчик которого также захочет с ней поработать. Это нестрашно, когда и там и там к переменной обращаются для чтения. А вот если в одном из кусков кода (или в обоих) производится запись, то могут возникнуть проблемы.

unsigned int ADCValue;    // Результат чтения АЦП (производится в
                          // прервании
...
// Фрагмент обработчика прерывания
If (ADIF && ADIE)
{
    ADIF     = 0;
    ADCValue = (ADRESH << 8) | ADRESL;  // Читаем последнее измерение
    ADGO     = 1;                       // Запускаем следующее
}
...
// Фрагмент основного тела программы
Data[i] = ADCValue;      // Примечание: Data[] – массив uint'ов

Я думаю, не нужно пояснять, что произойдет, если в момент чтения ADCValue в основной программе (а чтение произойдет в два этапа: сначала младший байт, затем старший), произойдет прерывание по завершению измерения ADC? В двух словах: на момент начала чтения переменная содержала значение 257 (0x101). Первым считается младший байт и скопируется в младший байт элемента массива Data[i]; далее произойдет прерывание, и в ADCValue запишется вновь измеренное значение напряжения, которое с момента последнего изменение стало, например, меньше на 3 единицы младшего разряда, т.е. 254 (0x0FE). Возвращаемся из прерывания и продолжаем копировать ADCValue в Data[i]; нам осталось скопировать старший байт, а он стал равным 0. Вот и получается, что в Data[i] окажется значение 0x001.

Часто встречал у людей недопонимание методов борьбы с этим явлением. Многие программисты считают, что их спасет квалификатор volatile, т.е. достаточно определить переменную, к которой есть доступ и в основном теле программ и в прерывании так:

volatile unsigned int ADCValue;

— и проблема будет решена автоматически на уровне компилятора. Однако, это не так. Квалификатор volatile всего лишь сообщает компилятору, что не нужно производить оптимизацию кода с участием этой переменной (подробнее о volatile читайте [9]). А это в свою очередь даст возможность программисту блокировать прерывания на время обращения к ней, но делать это надо вручную:

di();
Data[i] = ADCValue;
ei();

Более детально про атомарный доступ рекомендую почитать [6].

Наверх

Оформление

Наверх

Удобный инструментарий


Очень важная составляющая успешного программирования – инструментарий. Применительно к написанию текста программы – это в первую очередь текстовый редактор. Если он удобный, функциональный и имеет интуитивно понятный интерфейс, то форматирование будет производиться легко и непринужденно. Помимо самого ввода текста, редактор может обеспечить нам:

  • автоматические отступы;

  • замену символа табуляции пробелами;

  • подсветку синтаксиса;

  • контекстную подстановку;

  • перекрестные ссылки внутри исходного текста или целого проекта;

  • вызов компилятора для сборки проекта из редактора

(Рекомендую SlickEdit).

Наверх

Именование идентификаторов


Задавая имена переменным, функциям, константам, типам, перечислениям, макросам, файлам и пр., есть смысл следовать некоторым правилам:

  • Имя должно быть осмысленным (не i, а Counter)

  • Имя должно быть содержательным (не Counter, а BitsCounter)

И ниже приведем кое-какие отдельные правила именования различных объектов программы.

Именование функций

Имя функции внутри модуля, которую предполагается вызывать из других модулей, должно начинаться с префикса, обозначающего имя модуля. Зачем это нужно? Например, в одной программе вы используете память 24LC64, для которой определяете функцию write_byte(). Потом вы написали другую программу, которая работает с внутренней EEPROM контроллера, и для нее тоже определили функцию write_byte(). А потом понадобилось сделать проект, в котором будет использоваться и внешняя EEPROM, и внутренняя. Тут-то вы и столкнетесь с последствиями неправильного именования, потому что из-за одинаковых имен модули нельзя будет объединить в одной программе. Поэтому в самом начале нужно было предусмотреть префиксы имен функций (а запись байта – это операция универсальная, она есть в работе и с дисплеем, и с модемом и еще с чем угодно).
i2c_write_byte – для работы с памятью 24LC64;

eeprom_write_byte – для работы с внутренней EEPROM;

lcd_write_byte – для работы с LCD и т.д.

Кроме того, имя функции должно быть кратким и, по возможности, должно соответствовать системе именования: <модуль>_<действие>_<объект>[_<суффикс>] (могут быть в другом порядке, могут разделяться символом “_”), где:

<модуль>_<действие>_<объект>[_<суффикс>]

  • модуль – имя (или сокращенное имя) модуля, в котором определена функция;

  • действие – глагол, определяющий назначение функции (write, read, check, count и т.д.)

  • объект – существительное, определяющее параметрическую составляющую функции (byte, checksum, string, data и пр.)

  • суффикс – необязательное поле, отражающее какую-либо дополнительную характеристику функции (rom, timeout).

Конечно, все имена функций под одну гребенку подвести довольно трудно, и обязательно в любой системе именования будут исключения. Но не нужно называть функции так:

Неправильные имена функций:

CopyOneByteFromROMToRAM();

Check();

CompareAndGetMax();

В заключение надо сказать, что для некоторых функций можно оставить зарезервированные имена: atof(), init(), printf() и т.д.

Именование констант

  • Заглавными буквами

  • Слова разделяются символом ‘_’

  • Префикс в виде имени модуля или функционального назначения

// Определения параметров шины i2c
#define I2C_DEV_ADDR         0xA0
#define I2C_ADDR_WIDTH         16
// Определения цветов RGB
#define CL_RED           0xFF0000
#define CL_YELLOW        0xFFFF00
#define CL_GREEN         0x00FF00

Именование типов

  • Заглавными буквами

  • Слова разделяются символом ‘_’

  • Префикс в виде «T_», «TYPE_» и т.п.

typedef struct
{
    unsigned long   seconds : 6;
    unsigned long   minutes : 6;
    unsigned long   hours   : 5;
} T_CLOCK;

Именование переменных

  • Слова начинаются с заглавной буквы

  • Пишутся без разделителя

unsigned char    BytesCounter;
signed   long    XSize, YSize;

О «венгерской нотации»

Некоторые используют систему именования, в которой каждой переменной приписывается префикс, показывающий ее тип. Это часто может оказаться полезным при анализе чужого (а часто и собственного) кода. Например:

for (Counter = 0; Counter < 40000; Counter++) ...;

В данном примере мы не можем точно сказать, правильно ли у нас использована переменная в данном цикле или нет. Переменная Counter может быть, во-первых, 8-битной; во-вторых, она может быть знаковой (т.е. всегда будет меньше 40000). И для того, чтобы определить правильность использования переменной, нам нужно найти определение переменной в файле. Но если мы изначально предусмотрели в имени переменной ее тип, то это избавит нас от проделывания лишней работы:

for (wCounter = 0; wCounter < 40000; wCounter++) ...;

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

Также системой именований можно предусмотреть область видимости переменной.

Префиксы:

  • Префикс области видимости

    • Без префикса – локальная или параметр функции

    • s_ — статическая

    • m_ — локальная для модуля

    • g_ — глобальная

    • i_ — Обрабатывается в прерывании

  • Префикс типа

    • uc – unsigned char

    • sc – signed char

    • ui – unsigned int (n)

    • si – signed int (w)

И т.д.

Имя нашей переменной может выглядеть так:

static unsigned char s_ucRecCounter;

Встречая ее в любом месте функции, мы сразу понимаем, что:

  • Эта переменная предназначена для подсчета принятых байтов;

  • Эта переменная имеет тип unsigned char, т.е. может принимать значения 0..255;

  • Эта переменная статическая, т.е. сохраняет свое значение после выхода из функции.

Примечание. Можно для себя иметь некоторый набор зарезервированных имен для переменных общего назначения, которые встречаются наиболее часто. Например: i, j – signed int; a, b – char; f — float; и т.д. Но эти имена желательно согласовывать, во-первых, с применяемыми именами в литературе, а во-вторых, внутри собственной команды разработчиков, чтобы не было так, что у одного i – signed int, а у другого i – unsigned int.

Преимущества венгерской нотации:

  • Предотвращает ошибки использования типов

  • В большом коде помогает не запутаться в переменных

  • Упрощает чтение чужого кода

Недостатки венгерской нотации:

  • Ухудшают читабельность кода

    • Выражение из 3-4 переменных уже трудно читается

    • Префикс может получиться очень длинным

  • Изменение типа переменной влечет изменение ее имени во всех файлах

  • Префикс не дает гарантию правильности задания типа, из-за чего может получиться ложная уверенность в корректности применения переменной:

static signed char s_ucBytesCounter;

Наверх

Форматирование текста


Очевидно, что форматирование нужно для того, чтобы программа была более наглядной. Рекомендую в качестве примера ознакомиться с документом an_2000_rus.pdf, где описаны правила форматирования текста программ, сформулированные для сотрудников компании micrium. Не обязательно точно следовать приведенным в нем правилам, но следует посмотреть, на чем сосредоточили внимание авторы документа, и принять это на вооружение:

  • Следовать одному стилю

  • При работе в команде следовать всей командой одним и тем же правилам

  • Не оправдывать отсутствие форматирования нехваткой времени

Я не буду пересказывать этот документ, а только вкратце приведу основные моменты:

Текст файла должен быть разбит на секции

Когда мы берем в руки книгу, мы знаем, что у нее помимо самой содержательной части есть титульные данные, оглавление, выпускные данные, а часто еще алфавитный указатель, список литературы. Кроме того, мы знаем, что каждый элементы каждой из перечисленных групп находится в одном месте, а не разбросаны по всем страницам. Все эти данные упрощают восприятие книги: на титуле в двух словах сказано, о чем (иногда – для кого) эта книга, по оглавлению можно быстро найти интересующую главу, по содержанию – понять содержание и т.д.

Для того чтобы текст программы легче читался, он также должен быть разбит на части (или секции), каждая из которых имеет свое назначение:

  • Заголовок файла (с информацией о назначении файла, авторе текста, используемом компиляторе, дате создания и пр.);

  • Хронологию изменений

  • Секция включаемых файлов

  • Секция определения констант

  • Секция определения макросов

  • Секция определения типов

  • Секция определения переменных (глобальные, затем локальные)

  • Секция прототипов функций (глобальные, затем локальные)

  • Секция описания функций.

Для секций есть свои правила:

  • Каждая секция должна содержать только те описания, которые ей соответствуют

  • Секции должны быть едины, а не разбросаны по всему файлу.

  • Каждой секции должен предшествовать хорошо заметный блок комментария с названием секции.

Горизонтальные отступы

Горизонтальные отступы служат для визуального подчеркивания структуры программы. Я каждый блок операторов делаю с отступом на 4 пробела вправо от верхнего блока.

Вертикальное выравнивание

При определении переменных: типы – под типами, квалификаторы – под квалификаторами, имена переменных – под именами, атрибуты – под атрибутами. В самой программе: при присваивании блока переменных желательно выравнивать знаки «=».

    static unsigned char    BytesCounter;
    static          int     Timer;
                    double  Price;
                    char   *p, c;
    ...
    {
        BytesCounter = 0;
        Timer        = 0;
        Price        = 1.12;
    }

Хочу отдельно внимание обратить на то, что ‘*’, показывающая, что переменная является указателем, ставится рядом с переменной, а не с типом. Сравните две записи:

char*    p, c;
и
char    *p, c;

Первая запись может быть ошибочно прочитана так: переменные p и c имеют тип char*. На самом же деле указателем будет только p. Во второй записи это наглядно отражено.

Не делать в строке больше символов, чем помещается на одном экране

Например, если функция содержит много параметров, то имеет смысл каждый параметр писать с новой строки. Длинные выражения можно также разбивать на строки.

int sendto ( SOCKET                  s,
             const char             *buf,
             int                     len,
             int                     flags,
             const struct sockaddr  *to,
             int                     tolen
           );

Обращу ваше внимание, что при определении функции в нескольких строках правилом вертикального выравнивания стоит пользоваться не в полной мере. Попробуйте переписать эту функцию, ставя квалификаторы под квалификаторами и т.д. Он станет выглядеть довольно безобразно и совершенно нечитабельно, так что в данном случае лучше все определения начинать с одного столбца.

Одна строка – одно действие

Разделять функциональные узлы или конструкции (for, if, …) пустыми строками

Пробелы между операндами и операциями

Ниже пример форматирования по трем последним правилам:

Неправильно:

for(i=0;i<10;i++)a[i]=0;
if(j<k){a[0]=1;a[1]=2;}else{a[0]=3;a[1]=4;}

Правильно:

for (i = 0; i < 10; i++) a[i] = 0;
 
if (j < k)
{
    a[0] = 1;
    a[1] = 2;
}
else
{
    a[0] = 3;
    a[1] = 4;
}

Наверх

Комментирование

Почему не пишут комментарии

  • «Время поджимает, писать некогда»

  • «Это временный код, его не нужно комментировать»

  • «Я и так все запомню»

  • «Моя программа понятна и без комментариев»

  • «В код, кроме меня, никто не заглядывает»

  • «Комментарии делают текст пестрым и затрудняют чтение самой программы»

  • «Я потом откомментирую»

Для кого пишутся комментарии

Комментарий программистом пишется в первую очередь для самого программиста. За отсутствие комментариев приходится платить временем. Чаще — своим, реже – чужим.

Содержание комментариев

Частенько в присланных мне программах вижу, что местами комментарий написан только для того, чтобы он там был. Такое ощущение, что человек знает, что комментарий написать надо, но не знает зачем.

Что должно быть в комментариях:
  • Спецификация функций:

    • что делает;

    • входные и выходные параметры (типы и краткое пояснение);

  • Назначение объявляемой переменной, определяемой константы, типа, макроса;

  • Краткое, емкое, безызбыточное описание действия или пояснение к нему;

  • Пометки об изменениях:

    • версия (или дата) и номер пометки.

    • Причины и описание изменения желательно держать в отдельном месте, т.к., во-первых, текст описания причин изменений и производимых действий может получиться громоздким и будет засорять основной текст программы, а во-вторых, одна причина может повлечь за собой изменения нескольких участков программы.

  • Указание отладочных узлов и временных конструкций

Пример спецификации функции:

/*************************************************************
 *
 *  Function:       rs_buf_delete
 *
 *------------------------------------------------------------
 *
 *  description:    Удаляем N байт из буфера
 *
 *  parameters:     uchar N – количество удаляемых байтов
 *
 *  on return:      void
 *
 *************************************************************/
 
void rs_buf_delete (uchar N)
{
    ...

Пример пометок об изменениях:

/****************************************************
 * Список изменений
 * ...
 * Версия 1.6 (22.10.2009):
 *     1. ...
 *     2. ...
 *     ...
 *     8. Иванов И.И., 17.09.2009: В функции
 *        rs_buf_delete добавлена проверка
 *        входного аргумента на 0
 * ...
 ***************************************************/
...
void rs_buf_delete (signed char N)
{
    // *1.6-8* if (N < 0) return;
    if (N <= 0) return;          // *1.6-8*
    ...

Пример указания отладочного узла:

void rs_buf_delete (signed char N)
{
    if (N <= 0) return;
 
    /*D*/  DebugCounter++;
    /*D*/  PIN_DEBUG = 1;
    /*D*/  delay_ms(5);
    /*D*/  PIN_DEBUG = 0;
    ...
Чего в комментариях быть не должно:
  • Эмоций

    RB0 = 1;         // Без этой хрени не работает.
  • Описания устаревших действий

    if (BufSize > 0) // Буфер не пуст, флаг разрешения
                     // вывода установлен
  • Дублирования действия

    BufSize = 0;     // Обнуляем переменную, показывающую
                     // размер буфера
  • Бесполезной информации

    if (N < 15)      // Лучший вариант сравнения
  • Непонятных сокращений и жаргона:

    A = X / Y + Z;   // В (*1*), т.н.у., обход
  • Ложной или вводящей в заблуждение информации:

    if (timer < 100 && V < 10) // Если за 100мс напряжение
                               // стало выше 10 вольт

Расположение комментариев

Не следует мешать код и текст комментариев в одну кучу. Например, так:

Неправильный подход:

/* Инициализация портов*/
PIN_DATA = 0;
PIN_CLC = 0;
 
/* Очистка буфера */
for (i = 0; i < BUF_SIZE; i++) Buf[i] = 0;
 
/*Ожидание данных */
while (ReadData())
{
    ...

Комментарии нужно писать так, чтобы они не сливались с кодом. Один из вариантов – выносить их на поля, выравнивая по вертикали.

Правильный подход:

PIN_DATA = 0;                               /* Инициализация портов    */
PIN_CLC = 0;
 
for (i = 0; i < BUF_SIZE; i++) Buf[i] = 0;  /* Очистка буфера          */
 
while (ReadData())                          /* Ожидание данных         */
{
    ...

Многострочные комментарии

При написании многострочного комментария (например, описывающего функцию), нужно, чтобы каждая строка начиналась с символа, обозначающего, что это комментарий.

Неправильный подход:

/* Эта функция считывает начальные установки из EEPROM,
   проверяя контрольную сумму. Если контрольная сумма не
   сошлась, то будут приняты установки по умолчанию. */
   for (i = 0; i < BUF_SIZE; i++) ...;

Правильные подходы:

(не очень удобный вариант: из-за наличия правого ограничителя такой комментарий утомительно редактировать)

/* Эта функция считывает начальные установки из EEPROM,    */
/* проверяя контрольную сумму. Если контрольная сумма не   */
/* сошлась, то будут приняты установки по умолчанию.       */
   for (i = 0; i < BUF_SIZE; i++) ...;

(альтернативные варианты)

/*
 * Эта функция считывает начальные установки из EEPROM,
 * проверяя контрольную сумму. Если контрольная сумма не
 * сошлась, то будут приняты установки по умолчанию.
 */
   for (i = 0; i < BUF_SIZE; i++) ...;
// Эта функция считывает начальные установки из EEPROM,
// проверяя контрольную сумму. Если контрольная сумма не
// сошлась, то будут приняты установки по умолчанию.
   for (i = 0; i < BUF_SIZE; i++) ...;

Часто удобно пользоваться горизонтальными разделителями для визуального отделения логически разных блоков в тексте программы:

//--------------------------------------------------

или

/**************************************************/

Содержательная часть комментария

Вспомним, что мы говорили про именование идентификаторов и использование именованных констант. Рассмотрим пример:

switch (result)           // Проверяем состояние модема
{
    case 0:               // Модем выключенbreak;
    case 1:               // Модем готов к работеbreak;
    case 2:               // Модем производит дозвонbreak;
    …
}

Если бы использовали осмысленные имена (и переменной и констант), то сам фрагмент программы оказался бы понятным и без комментариев.

switch (ModemState)
{
    case MODEM_OFF:break;
    case MODEM_READY:break;
    case MODEM_CALLING:break;
    …
}

Тем самым у нас появляется возможность комментарием пояснить какой-нибудь нюанс какой-то ситуации или какого-то действия.

Формулировка

Совет: формулируя комментарий, всегда представляйте, как будто комментарий читает другой человек. Это поможет сделать формулировку более четкой.

Наверх

Отладка и тестирование

Отладка, как и тестирование, – неотъемлемая часть разработки. Я отвел для этих понятий один общий раздел, т.к. обычно, эти этапы производятся параллельно. Надо сказать, что эти два этапа дополняют друг друга.

Рассмотрим следующие моменты:

Наверх

Инструменты

В арсенале программиста может быть довольно мощный инструментарий: симулятор, внутрисхемный отладчик, лог. анализатор, осциллограф и т.д. В целом, неизвестно, сколько ошибок нужно будет обнаружить и исправить, поэтому процесс отладки и тестирования может сильно затянуться. И здесь наличие хорошего инструментария и умение с ним работать поможет сэкономить массу времени и сделает сам процесс более легким. Поэтому не нужно скупиться на средства отладки.

Наверх

Резерв по ресурсам

Желательно еще на этапе планирования предусмотреть возможность взять для отладки контроллер с запасом по ресурсам. Для чего они могут понадобиться? В процессе тестирования или отладки программы нам может понадобится контроль хода выполнения или каких-то внутренних переменных. Для этого нам понадобится запас:

  • периферийных возможностей

  • памяти для размещения отладочного кода

  • резерв по скорости

Запас по периферии

Внутренняя периферия контроллера

Здесь масса вариантов. Как минимум – иметь свободные выводы контроллера для возможности устанавливать на них логические уровни, соответствующие состоянию программы. Если в устройстве не планируется использовать модуль USART, то было бы удобно оставить свободными выводы контроллера, на которые этот модуль работает. Тогда отладочную информацию можно будет сливать, используя аппаратные возможности контроллера. Если использование USART все же планируется, а отладочную информацию все равно хочется слать через RS-232 в компьютер, то можно пожертвовать системным временем и сделать функцию вывода программно на любом свободном выводе контроллера (функция ввода нужна не так часто, а функция вывода реализуется довольно просто). Зачастую есть возможность скидывать отладочные данные в общем потоке данных (по UART, по радиоканалу).

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

Внешняя периферия

Например, если используется LDC, то можно предусмотреть какой-то сегмент для вывода отладочной информации. В EEPROM можно выделить отдельную страницу, куда сохранять состояние контроллера после сброса, если при запуске выясняется, что сброс был аварийным (например, туда можно записать содержимое стека, пускай сам указатель и не сохранился).

Также можно на плате предусмотреть дополнительные кнопки, дополнительные светодиоды.

Память для размещения отладочного кода

Здесь все понятно: отладочный код занимает место в ROM и часто требует каких-то ячеек RAM-памяти. И будет очень обидно, если из-за отладочных функций и макросов не будет умещаться основная программа.

Резерв скорости

Естественно, подпрограммы вывода отладочной информации не выполняются мгновенно. Может получиться так, что из-за ее вывода программа будет просто не успевать отрабатывать свой алгоритм. Это нужно учитывать как при выборе тактовой частоты контроллера, так и при написании отладочных подпрограмм.

Наверх

Заглушки и тестеры

Функции-заглушки

В процессе тестирования могут пригодиться так называемые заглушки – пустотелые функции, соответствующие спецификации, но подменяющие вычисления (или результаты какой-то другой операции) заведомо правильным результатом. Когда это может оказаться полезным:

  • Когда при отладке нужны результаты работы еще ненаписанных подпрограмм.

  • При тестировании в симуляторе, когда нет возможности работать с какими-то внешними устройствами (например, GPS);

  • При тестировании кода, содержащего долго выполняющиеся функции, не участвующие в тесте подпрограммы (типичный пример – задержки)

Функции-тестеры

Когда мы пишем новую функцию, мы хотим убедиться в ее работоспособности. Есть смысл писать небольшие функции, которые будут проводить тестирование новой, многократно запуская ее, передавая ей тестовые параметры. Дальнейший результат можно, в зависимости от назначения функции, наблюдать визуально или сравнивать с шаблоном. Такие функции могут формировать для нашей программы последовательность входных параметров, которая может возникнуть в реальной обстановке. Это также может оказаться особенно полезным, когда программа отлаживается или тестируется в симуляторе. Важно, чтобы при тестировании функция запускалась именно на рабочем контроллере или его аналоге (а не на Builder C++, Visual C++ и пр., на которых можно отлаживать только макет функции), т.к. каждый компилятор имеет свои особенности (типы данных, автоматическое преобразование типов, работа со стеком и пр.); каждый процессор – свои: архитектура (Гарвадрская и Неймоновская), объем доступной памяти и т.д.

Самое сложное место в этой схеме – шаблон. Ведь мы писали новую функцию именно для того, чтобы она нам этот результат вычисляла. Где брать шаблон для сравнения? Есть три пути:

  1. В некоторых случаях есть возможность результат для сравнения формировать сторонней функцией. Например, известно, что функция printf из-за своей универсальности довольно громоздкая, и далеко не в каждом случае программист может позволить себе ее использовать, отняв у контроллера чуть ли не половину памяти и несколько тысяч тактов. Поэтому частое явление, когда программист пишет свою mini-printf, удовлетворяющую требованиям спецификации конкретной программы. Для проверки работоспособности своей функции он на этапе тестирования может пользоваться результатами работы встроенной функции printf. Причем эта функция может тестироваться в автоматическом режиме. Такой же способ подойдет для проверки математических функций.

  2. Входные параметры и результат вбивать вручную. Довольно трудоемкий процесс, поэтому в первую очередь желательно тестировать граничные значения.

  3. Третий вариант – брать данные извне (например, в автоматическом режиме с PC)

Наверх

Предупреждения при компиляции

Серьезная ошибка, которую допускают программисты – это игнорирование предупреждений компилятора. Более того, к ним относятся как к назойливым мухам: «Ну, наконец-то собрал программу. Только своими дурацкими предупреждениями забил всю статистику, что ничего не прочтешь!» А между тем, компилятор ругается не просто так. Тут он нам не враг, а помощник. Он сообщает, что в программе есть двояко толкуемые конструкции, которые он транслировал на свое усмотрение. Дело в том, что ошибку можно совершить, даже написав все правильно с точки зрения языка. Например, в программе могут встретиться выражения вида:

if (a == b) ...;
if (a = b)  ...; // Warning: Assignment inside relational expression

В зависимости от того, что мы хотим сделать, мы можем применить первое или второе выражение в скобках оператора if. В первом выражении мы проверяем переменные a и b на равенство, в то время как во втором мы проверяем переменную a на 0 после присваивания ей значения из переменной b. В общем случае второй записью пользоваться не рекомендуется, т.к. гораздо нагляднее:

a = b;
if (a) ...;

Потому-то компилятор и выдает нам предупреждение, что в выражении отношения имеется оператор присваивания, давая тем самым понять, что, возможно, мы имели в виду оператор отношения “==”. (Иногда, все же, есть смысл использовать вторую запись, когда, например, код критичен ко времени выполнения.)

Также частым следствием предупреждения компилятора является неаккуратное отношение к приведению типов. Например, в том же операторе сравнения:

signed   int  a;
unsigned int  b;
…
if (a > b) …;     // Warning: signed and unsigned comparison

мы получим предупреждение, если одна переменная знаковая, а вторая – нет. Это предупреждение говорит не о том, что «осторожно! Может случиться коллизия», а о том, что программистом неправильно выбраны типы. Если же программист уверен в правильности, то он должен сделать приведение типов вручную:

signed   int  a;
unsigned int  b;
...
if ((signed long) a > (signed long) b) ...;

Тем самым давая компилятору понять, что программист в курсе неправильно выбранных типов. Обратим внимание на то, что приведение типов производится к знаковому типу большей разрядности.

(Вообще же, повторюсь, с выражениями, где участвуют переменные различных типов, нужно быть осторожнее. Например, MPLAB C18 не выдаст предупреждения на наш пример, что в свою очередь приведет к неправильному поведению программы, если a будет иметь отрицательное значение.)

Что делать, если компилятор выдал предупреждение?

В первую очередь нужно определиться, почему он выдал предупреждение. Если его текст не понятен, то нужно обратиться к документации на компилятор и прочитать больше информации (часто в описании предупреждений приводятся наиболее частые причины их возникновения). Во вторую – привести код к однозначно интерпретируемому виду.

Наверх

Вывод отладочной информации

При формировании отладочной информации нужно помнить, что информация об ошибках, выводимая для пользователя, и информация о поведении программы, выводимая для программиста, — это абсолютно разные вещи. Причем как по содержанию, так и по способу информирования.

Не нужно пользователя снабжать шестнадцатеричными кодами на дисплее, а по телефону потом разъяснять, что это «переменная режима работы приняла странное значение, сейчас подумаю, почему такое произошло». Также не нужно в конечном варианте программы оставлять всяческие вспыхивания светодиодов и взвизгивания пьезодинамика, которые при отладке говорили программисту о каком-то поведении программы или о ходе вычислений (а потом по телефону объяснять, что если лампочка мигнула 3 раза по 50 мс, то это не сошлась контрольная сумма). Т.е. вся отладочная информация, которая будет выводиться для программиста, должна быть скрыта от пользователя (запись в EEPROM, передача кода ошибки на центральный пульт, если такой есть).

Наверх

Блокировка вывода отладочной информации

Нужно помнить, что возможность вывода отладочной информации, скорее всего, придется оставить в контроллере на всю жизнь. Иногда и отлаженные и переотлаженные программы могут содержать ошибки. Тем не менее, нужно иметь возможность отключать:

  • вывод отладочной информации (как всей скопом, так и отдельных узлов);

  • заглушки

  • тестеры (их полезно оставлять в тексте программы на случай проявлений ошибок в период эксплуатации)

Блокировать можно двумя способами:

  • на этапе компиляции, блокируя все отладочные узлы условными директивами компиляции:

    #define DEBUG_ENABLE
    ...
    /*D*/ #ifdef DEBUG_ENABLE
    /*D*/ PIN_DEBUG = 1;
    /*D*/ #endif

(тогда, закомментировав определение DEBUG_ENABLE, мы удаляем из кода всю отладочную составляющую). Такой способ экономит ресурсы контроллера.

  • на программно-аппаратном уровне (например, контролируя состояние порта):

    if (PIN_DEBUG_ENABLE) PIN_DEBUG = 1;

(тогда мы можем включать/отключать работу отладочных подпрограмм в ходе выполнения.) Такой способ требует ресурсы, но позволяет отлаживать устройство в реальных условиях.

То же самое касается блокировки функций-заглушек и функций-проверок. Например:

#define DUMMY_GPS_IN
...
char * GetGPSData (void)
{
#ifdef DUMMY_GPS_IN
    char test_string[] = “$GPRMC,A,123456,…”;
    return test_string;
#else
    /* Здесь код для работы с реальными данными от GPS */
#endif
}

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

Наверх

Резервное копирование

По ходу сопровождения (а иногда и разработки) программы нужно сохранять резервные копии, отмечая даты, номера версий, сделанные изменения и версию компилятора. Причем версию следует сохранять целиком, вместе с проектными файлами и HEX’ом. Бывает так, что незначительное изменение приводит к потере работоспособности кода, и, не имея резервных копий, зачастую трудно установить, какая именно из последних модификаций привела к таким последствиям.

Однако, сохранять целиком всю версию при каждой небольшой модификации довольно накладно (и по времени, и по трудоемкости). Поэтому для решения подобных задач удобно пользоваться специальными инструментами: системами контроля версий (VCS — version control systems).

Системы контроля версий обеспечивают два основных процесса:

  • взаимодействие группы разработчиков, работающих над одним проектом

  • сохранение текущих «срезов» проекта в базе данных.

Существуют различные реализации VCS: коммерческие и бесплатные, использующие центральный сервер и с распределенным хранением. Одна из самых распространенных — бесплатная система управления версий Subversion с открытым исходным кодом. Именно ее можно порекомендовать для начального ознакомления с VCS.

Subversion (или SVN) является клиент-серверной системой. База данных проекта или репозиторий хранится на сервере, а разработчик работает с локальной копией проекта, которую «отдает» ему приложение-клиент. Поработав с локальной копией, разработчик сохраняет на сервере изменения проекта — таким образом обеспечивается безопасная работа в группе и полный контроль над ходом работы. В любой момент можно извлечь один из «срезов» (в терминологии SVN — ревизий) проекта и посмотреть всю историю изменений, а также произвести сравнение различных версий.
Наиболее популярная программа клиент Subversion для Windows — TortoiseSVN.

Наверх

Список литературы

  1. Э. Йодан «Структурное проектирование и конструирование программ», М. Мир, 1979

  2. Б. Керниган, Р. Пайк «Практика программирования»

  3. А. Голуб «Веревка достаточной длины, чтобы… выстрелить себе в ногу»

  4. С. Макконнелл «Совершенный код», Русская редакция, 2005

  5. Ван Тассел Д. «Стиль, разработка, эффективность, отладка и испытание программ», М. «Мир», 1981

Наверх

Виктор Тимофеев, ноябрь 2009

osa@pic24.ru

Кто-то из великих мыслителей когда-то сказал: «Покажите мне человека, который не ошибся ни разу в жизни, и я покажу вам человека, который ничего не достиг». Сами по себе ошибки — это не зло, они помогают нам расти и развиваться. Совершить их может специалист любого уровня. Однако учиться лучше на чужих неудачах, нежели на своих. «Библиотека программиста» детально разобралась в этой теме и составила список распространенных ошибок кодеров, которых вы можете избежать, если дочитаете статью до конца. Поехали!

1. Приступать к программированию, не до конца проработав схему проекта

Реализацию любого проекта можно разделить на три простых этапа:

  1. Анализ требований проектируемого продукта
  2. Создание схемы или прототипа самого проекта
  3. Реализация или непосредственное написание кода

Применяя такой подход, вы быстро поймете, что даже небольшой, но правильно написанный фрагмент кода, послужит прочным фундаментом для дальнейшего построения сложной архитектуры.

2. Отсутствие единообразия и формата написания кода

⚠️ 10 самых распространенных ошибок, ежедневно допускаемых каждым программистом

Это ошибка, которую чаще всего совершают неопытные разработчики и новички.

Разрозненный неоднообразный код затруднит чтение и расстроит не только членов вашей команды, но и всех, кому доведется читать ваши угловатые конструкции. Старайтесь оставлять после себя чистоту и не заставляйте потомков извергать проклятья, глядя на ваш свободный стиль написания кода. В современном программировании большинство ИТ-специалистов используют определенные методологии написания кода, которые варьируются в зависимости от области разработки и языка. Также многие из них используют «плагины для форматирования кода», помогающие мгновенно избавиться от этой ошибки.

3. Написание глупых или заумных комментариев

⚠️ 10 самых распространенных ошибок, ежедневно допускаемых каждым программистом

Почти все языки программирования предоставляют возможность написания комментариев, цель которых — облегчить понимание текущего сценария. Однако большинство разработчиков или пренебрегают ими, или спокойно пишут очевидные, а порой глупые комментарии. Всегда делайте свой код понятным для всех, если, конечно, это не секретные разработки спецслужб.

Напомним, для начинающих разработчиков, про два типа комментариев, существующих на сегодняшний день: поясняющие и документационные.

  • Документационные — нужны для тех людей, кто в будущем будет использовать ваш код, но вряд ли сможет понять или прочитать его правильно.
  • Поясняющие — это отражение вашего кода и предназначены они для всех (включая вас в будущем), кто будет его поддерживать, рефакторить и расширять.

Также нужно отметить, что комментарии более эффективны, если они сообщают другим «почему этот конкретный код пишется в данной ситуации», а не «что делает этот код».

Однако при комментировании нужно знать меру. Не стоит писать философское эссе размером в страницу, все должно быть четко, лаконично и по существу.

4. Не тестировать написанное

Эту ошибку часто допускают опытные программисты, обладающие излишней самоуверенностью. Неважно, напишите ли вы одну строку кода, небольшую функцию или целое приложение — все это будет просто набор символов без подтверждения их работоспособности. Отладка и тестирование даст вам уверенность в том, что ваш код надежен и удовлетворяет всем возможным сценариям.

5. Написание универсальных супер-функций

⚠️ 10 самых распространенных ошибок, ежедневно допускаемых каждым программистом

Постарайтесь проектировать методы так, чтобы они выполняли только одно действие. Вместо того чтобы назначать несколько задач одной большой функции, разделите ее обязанности среди нескольких мелких. Такой подход увеличит читаемость кодовой конструкции и поможет избежать полного отказа приложения, ведь при некорректно написанной одной функции — остальные не будут работать.

6. Плохие сообщения коммитов

Работая с коммитами системы контроля версий, нужно помнить, что комментарий к ним, возможно, самая важная его часть, являющаяся единственным местом, где написано не только что изменилось, но и зачем.

Чаще всего, сообщения к коммитам читают в логе изменений, где их порой бывает очень много, поэтому они должны быть достаточно короткими с четким описанием того, что произошло с кодом. Сравнить их можно с заголовками рекламных статей.

Фиксации с сообщениями типа: «Исправлена ​​ошибка» или «Обновлена ​​функция» — так себе названия. Хороший коммит содержит информацию о проблеме и ее решении, предшествующий уникальному идентификатору токена (если он доступен).

7. Чрезмерная инженерия или усложнение простых вещей

Реализация кода с помощью шаблона проектирования не всегда является разумным шагом, даже если огромное число разработчиков сделали так до вас. Поэтому, учитывая, что на протяжении разработки очередного проекта вам будут встречаться сотни различных инструментов и бесконечный океан кодовых инструкций — вы, прежде чем внедрять какое-либо решение, должны уметь ответить на три базовых вопроса:

  1. Что использовать?
  2. Где и когда использовать?
  3. Как использовать?

Не стоит городить огород из ненужных плагинов и библиотек — старайтесь делать все проще и не усложняйте себе и без того нелегкий труд.

8. Не анализировать готовые решения в сети

Это ошибка, наиболее распространена в индустрии проектирования и разработки программного обеспечения. Если программист не знает, какие функции уже предоставлены языком, фреймворком или другими разработчиками, то он может потратить много времени чтобы создать то, что уже давно создано и отлично работает. Не стоит вступать в клуб изобретателей велосипедов, даже не попробовав поискать решение в сети. Технологии меняются буквально каждый день и довольно часто, то, что вы ищете уже кто-то сделал. К тому же время, потраченное на поиск готового решения, будет в разы меньше, времени, израсходованного на разработку очередного псевдопаттерна.

9. Консерватизм и проповедование только одного стека

⚠️ 10 самых распространенных ошибок, ежедневно допускаемых каждым программистом

Поговорка, про кулика, расхваливающего свое болото актуальна и для мира ИТ. Наверняка, вам также доводилось слышать, как некоторые специалисты превозносят одну конкретную технологию, говоря, что все остальные ужасны. И заголовки кликбейтных статей, увещевающих о войнах фреймворков и языков («Vue vs React» или «Python vs Java») рождаются именно по их вине. Нельзя говорить, что один популярный инструмент хуже другого только лишь потому, что вы к нему привыкли. В разных ситуациях каждый будет хорош по-своему. Здесь главное уметь с ним работать. Грешно ругаться на молоток, утверждая, что он плохо забивает гвозди. А поскольку технологии совершенствуются довольно быстро, крайне важно быть открытым для новых идей и способов реализации. Консерватизм в ИТ — это ошибочный путь.

Последняя, наиболее важная проблема и главная ошибка начинающих разработчиков — это пренебрежение собственным здоровьем. Бесспорно, к работе нужно относиться ответственно и стараться делать поставленные задачи в срок, но не в ущерб себе. Факторов, влияющих на самый ценный ваш ресурс может быть много, начиная с вредного начальника и ежедневных мозговых штурмов и заканчивая решением срочных задач с горящими дедлайнами и работой допоздна. Однако несмотря ни на что, одно должно оставаться неизменным — ваша забота о собственном самочувствии. Помните, жизнь — это дар и ее качество гораздо важнее, чем сверхурочная работа, за которую в большинстве случаев вам даже «спасибо» никто не скажет.

***

Учитесь на чужих ошибках! Такой подход поможет вам сэкономить время, энергию и деньги. Удачи!

Материалы по теме

  • 🐛 Типовые ошибки в разработке UI: найти и обезвредить
  • 8 самых распространенных ошибок веб-разработчика
  • ⚠️ Как не нужно учить TypeScript: 5 распространенных ошибок
  • 👶 10 ошибок начинающего разработчика
  • ТОП-13 ошибок начинающего программиста

Готовая программа не всегда работает как надо. Бывает, возникают баги, предупреждения, исключения. В итоге программа зависает, дает сбой или вылетает. Но это не конец света. Любую ошибку в коде можно исправить, если знать, почему она возникла.

Программная ошибка: что это и почему возникает

Программная ошибка — это дефект в коде. Из-за него программа сбоит или выдает неверные результаты. Некоторые ошибки серьезные — например, блокируют логин и пароль, из-за чего пользователь не может попасть в личный кабинет. А другие незаметны. Некоторое время программа работает как будто бы исправно — и только потом начинает глючить.

Ошибка в программировании — это зачастую ошибки разработчиков, которые находят тестировщики. Запускают разные тесты и отладку, чтобы определить источники проблемы.

Научитесь находить ошибки в приложениях и на сайтах до того, как ими начнут пользоваться клиенты. Для этого освойте профессию «Инженер по тестированию». Изучать язык программирования необязательно. Тестировщик работает с готовыми сайтами, приложениями, сервисами, а не с кодом. В программе от Skypro: четыре проекта для портфолио, практика с обратной связью, все основные инструменты тестировщика.

Ошибки часто называют багами, но подразумевают под ними разное, например:

Ворнинги, или предупреждения. Возникают, когда программа начинает вести себя не так, как задумывалось. Не являются критичными ошибками. Программа с ворнингами работает, но с аномалиями.

Исключения. Это не ошибки, а особые ситуации, которые нужно обработать.

Синтаксические ошибки. Это ошибка в программе, связанная с написанием кода. Пример: программист забыл поставить точку или неверно написал название оператора. Если не исправить, код программы не запустится, а останется просто текстом.

Классификация багов

У багов есть два атрибута — серьезности (Severity) и приоритета (Priority). Серьезность касается технической стороны, а приоритет — организационной.

🚨 По серьезности. Атрибут показывает, как сильно ошибка влияет на общую функциональность программы. Чем выше значение атрибута, тем хуже.

По серьезности баги классифицируют так:

  • Blocker — блокирующий баг. Программа запускается, но спустя время баг останавливает ее выполнение. Чтобы снова пользоваться программой, блокирующую ошибку в коде устраняют.
  • Critical — критический баг. Нарушает функциональность программы. Появляется в разных частях кода, из-за этого основные функции не выполняются.
  • Major — существенный баг. Не нарушает, но затрудняет работу основного функционала программы либо не дает функциям выполняться так, как задумано.
  • Minor — незначительный баг. Слабо влияет на функционал программы, но может нарушать работу некоторых дополнительных функций.
  • Trivial — тривиальный баг. На работу программы не влияет, но ухудшает общее впечатление. Например, на экране появляются посторонние символы или всё рябит.

🚦 По приоритету. Атрибут показывает, как быстро баг необходимо исправить, пока он не нанес программе приличный ущерб. Бывает таким:

  • Top — наивысший. Такой баг — суперсерьезный, потому что может обвалить всю программу. Его устраняют в первую очередь.
  • High — высокий. Может затруднить работу программы или ее функций, устраняют как можно скорее.
  • Normal — обычный. Баг программу не ломает, просто где-то что-то будет работать не совсем верно. Устраняют в штатном порядке.
  • Low — низкий. Баг не влияет на программу. Исправляют, только если у команды есть на это время.

Типы ошибок в программе

🧨 Логические. Приводят к тому, что программа зависает, работает не так, как надо, или выдает неожиданные результаты — например, не записывает файл, а стирает.
Логические ошибки коварны: их трудно обнаружить. Программа выглядит так, будто в ней всё правильно, но при этом работает некорректно. Чтобы победить логические ошибки, специалист должен хорошо ориентироваться в коде программы.

🧨 Синтаксические. Это опечатки в названиях операторов, пропущенные запятые или кавычки. Безобидные ошибки: их обнаруживают и подсвечивают в коде компиляторы, а программисту остается исправить.

🧨 Взаимодействия. Это ошибка в участке кода, который отвечает за взаимодействие с аппаратным или программным окружением. Такая ошибка возникает, например, если неправильно использовать веб-протоколы. Исправляется элементарно: разработчик переписывает нужный кусок кода.

🧨 Компиляционные. Любая программа — это текст. Чтобы он заработал как программа, используют компилятор. Он преобразует программный код в машинный, но одновременно может вызывать ошибки.

Компиляционные баги появляются, если что-то не так с компилятором или в коде есть синтаксические ошибки. Компилятор будто ругается: «Не понимаю, что тут написано. Не знаю, как обработать».

🧨 Ошибки среды выполнения. Возникают, когда программа скомпилирована и уже выглядит как файл — жми и работай. Юзер запускает файл, а программа тормозит и виснет. Причина — нехватка ресурсов, например памяти или буфера.

Такой баг — ошибка разработчика. Он не предвидел реальные условия развертывания программы. Теперь ему надо вернуться в исходный код и поправить фрагмент.

🧨 Арифметические. Бывает, в коде есть числовые переменные и математические формулы. Если где-то проблема — не указаны константы или округление сработало не так, возникает баг. Надо лезть в код и проверять математику.

Инженер-тестировщик: новая работа через 9 месяцев

Получится, даже если у вас нет опыта в IT

Узнать больше

Что такое исключения в программах

Это механизм, который помогает программе обрабатывать нестандартную ситуацию и при этом не вылетать. Идеально, если программист предусмотрел все возможные ситуации. Но так бывает редко, поэтому лучше использовать специальный обработчик. Он обработает исключения так, что программа продолжит работать.

Как это происходит:

  1. Когда программист кодит, то продумывает, в какой части программы может вылезти ошибка.
  2. В этой части пишет специальный фрагмент, который предупредит компьютер, что ошибка — вполне ожидаемое явление и резко обрывать программу не нужно.
  3. Когда юзер запустит программу и появится ошибка, компьютер увидит заранее подготовленное предупреждение программиста. Продолжит выполнять алгоритм так, словно никакого бага и не было.

Исключения бывают программными и аппаратными:

  • Аппаратные создает процессор. К ним относят деление на ноль, выход за границы массива, обращение к невыделенной памяти.
  • Программные создает операционка и приложения. Возникают, когда программа их инициирует: аномальная ситуация возникла — программа создала исключение.

Как контролировать баги в программе

🔧 Следите за компилятором. Когда компилятор преобразует текст программы в машинный код, то подсвечивает в нём сомнительные участки, которые способны вызывать баги. Некоторые предупреждения не обозначают баг как таковой, а только говорят: «Тут что-то подозрительное». Всё подозрительное надо изучать и прорабатывать, чтобы не было проблемы в будущем.

🔧 Используйте отладчик. Это программа, которая без участия айтишника проверяет, исправно ли работает алгоритм. В случае чего сообщает об ошибках. Например, отладчик используют для построчного выполнения программы. Вместе с тем проверяют значения переменных: фактические сравнивают с ожидаемыми. Если что-то не сходится, ищут баги и исправляют.

🔧 Проводите юнит-тесты. Это когда разработчик или тестировщик описывает ситуации для каждого компонента и указывает, к какому результату должна привести программа. Потом запускает проверку. Если результат не совпадает с ожидаемым, появляется предупреждение. Дальше программисты находят и устраняют проблему.

Ключевое: что такое ошибки в программировании

  • Ошибка в программировании — это дефект кода, баг, который может вызывать в программе сбои и неожиданное поведение.
  • По серьезности баги делятся на блокирующие, критические, существенные, незначительные, тривиальные. По приоритету — на наивысший, высокий, обычный, низкий.
  • Ошибки в коде могут быть разными, например связанные с логикой программы. Или с математическими вычислениями — логические. Еще бывают синтаксические, ошибки взаимодействия, компиляционные и ошибки среды выполнения.
  • Некоторые ошибки помогают ловить обработчики исключений.
  • Чтобы находить ошибки в коде, тестировщики используют компиляторы, отладчики и пишут юнит-тесты.

Понравилась статья? Поделить с друзьями:
  • Как не допускать ошибок в орфографии
  • Как не допускать ошибок в математике
  • Как не допускать ошибок в макияже
  • Как не делать ошибок в орфографии
  • Как не делать ошибок в еве