Статья
Версия для печати
Обсудить на форуме (2)
«Hello World!» в embedded-исполнении. Часть 2

© Dale, 23.10.2011 — 01.11.2011.


Начало: Часть 1.

Оглавление


Подготовка инфраструктуры

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

Корневая директория проекта

Несмотря на то, что наш проект невелик как по объему, так и по функциональности, мы постараемся приступить к нему со всей серьезностью, как будто работаем над оборонным заказом. Это позволит выработать ряд навыков, которые окажутся очень полезными впоследствии, когда проекты станут большими и плохо управляемыми без соблюдения строгой дисциплины.
При разработке языка программирования Ruby был провозглашен «принцип наименьшего удивления»: тот, кто изучает язык или программирует на нем, не должен испытывать недоумения по поводу его концепций, синтаксиса, идиом и т.д. Каждый раз, встречая ранее незнакомую конструкцию, у человека должно быть ощущение, что сам он сделал бы точно так же. Не стану дискутировать на тему, насколько этот благой принцип был соблюден в самом Ruby, но идея как таковая представляется весьма здравой. Постараемся и мы работать таким образом, чтобы любой, кто случайно или намеренно забрел в директорию нашего проекта, не был вынужден ломать голову над тем, куда он, собственно, попал и что здесь делают все эти непонятные файлы.
Следуя принципу наименьшего удивления, создадим в корневой директории проекта файл README.txt, в котором кратко опишем назначение и цель проекта, а также имена и назначение файлов и поддиректорий. Усилий и времени это отнимет минимум, а вот пользу таких файлов трудно переоценить (при условии, конечно, что они отвечают на наиболее вероятные вопросы потенциального читателя, а не напускают лишнего тумана). Само собой, содержимое файла следует время от времени пересматривать, чтобы оно не потеряло актуальность в ходе развития проекта.
Завершив литературную часть, начнем создавать собственно структуру директорий. Поскольку наш проект включает в себя как аппаратную, так и программную части, зарезервируем для них две поддиректории с очевидными (не забываем про принцип наименьшего удивления!) именами: Hardware и Software.

Поддиректория Hardware

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

Поддиректория Software

Содержимое этой поддиректории в дальнейшем будет представлять для нас особый интерес, поскольку, хотя в данном проекте мы намерены довести разработку до функционирующего прототипа, тем не менее в центре нашего внимания находится именно его (проекта) программная составляющая. Поэтому необходимо с самого начала разработать строгий порядок его наполнения и в дальнейшем строго его придерживаться, чтобы не запутаться. В нашем учебном проекте, к счастью, запутаться будет сложно, но при переходе к более реальным задачам без строгой дисциплины не обойтись.
Итак, начинаем вырабатывать соглашения. Поскольку наш проект (будем для краткости именовать проектом его программную составляющую) будет разбиваться на модули, вполне естественно выделить для каждого модуля свою отдельную поддиректорию внутри Software. Это не только обеспечит поддержание порядка и облегчит навигацию по дереву файлов проекта, но и позволит при необходимости повторно использовать успешно реализованные модули в других проектах.
Как и прежде, выделим отдельную директорию под названием vendor для хранения продуктов сторонних разработчиков, задействованных в проекте. Хотя включать их копии в каждый проект может показаться накладным, на самом деле плюсов в таком решении гораздо больше, чем минусов. Чужого кода на самом деле в нашем проекте будет не столь уж много, а емкость современных жестких дисков не столь мала, чтобы такое дублирование было действительно накладно. Зато любой разработчик, который пожелает присоединиться к проекту (или же сам автор, который вынужден периодически работать с разных компьютеров), лишен необходимости отыскивать эти продукты и загружать их на свой рабочий диск — достаточно лишь целиком загрузить проект из репозитория. Заодно решается проблема размещения этих продуктов, ведь у каждого разработчика обычно собственная точка зрения на то, где находится единственно правильное место для хранения библиотек. Особенно актуально это для среды MS Windows, где такого стандарта де-факто вообще не существует. Еще один плюс — гарантия совместимости, ведь далеко не факт, что спустя некоторое время мы все еще сможем скачать с сайта разработчика нужную нам версию продукта, а новая версия будет на 100% совместима со старой (да и сам сайт может к тому времени почить в бозе, что также нередко случается).
Не забываем о файле README.txt. Конечно, особо злоупотреблять им не станем, запишем лишь самое необходимое — очень кратко основные бизнес-требования к проекту, состав и назначение его модулей, а также инструкцию по сборке проекта. Не забываем, что код пишется в первую очередь для человека, а не для машины; поэтому весьма гуманно дать читателю путеводную нить, не вынуждая его лично карабкаться по всем ветвям нашей файловой структуры в попытке понять, как это все устроено. Небольшая затрата времени на написание этого файла впоследствии окупится сторицей.
Конечно же, мы будем тщательно документировать проект. Поэтому нам понадобится поддиректория для хранения документации. Назовем ее doc. Содержимое уточним в ходе проекта, когда появятся артефакты для хранения в этой поддиректории.
Последний штрих — это файл Makefile, который будет управлять сборкой нашего проекта.

Поддиректория модуля

Поддиректории всех наших модулей будут иметь единую структуру. Прежде всего, в каждой из таких поддиректорий мы обнаружим уже привычные нам файлы README.txt, содержащий краткое описание модуля и инструкцию по его сборке, и Makefile, управляющий сборкой модуля.
Главное содержимое подиректории — исходные тексты модуля. Будем хранить их в поддиректории src. Если исходный текст не зависит от платформы (IBM PC или Atmel AVR Mega), он будет размещен в самой этой поддиректории. Если же для разных платформ имеются различные реализации, они будут размещаться в поддиректориях src/PC и src/AVR соответственно.
Заголовочные файлы, содержащие интерфейс модуля, разместятся в поддиректории include.
Исходные файлы тестов модуля будем размещать в поддиректории test, туда же будут отправлены исполнимые файлы тестов после компиляции и компоновки.
Если для тестирования модуля понадобятся подставные объекты, они будут храниться в поддиректории mocks.
Объектные файлы для двух наших архитектур разместятся в obj/PC и obj/AVR.

Общий вид файловой структуры проекта

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

  • README.txt (информация о проекте в целом).
  • /Hardware (поддиректория для документации по аппаратной части проекта).
    • README.txt (информация об аппаратной части проекта).
    • Принципиальная схема устройства.
    • Фотошаблоны печатных плат.
    • Сборочные чертежи.
    • ...
  • /Software (поддиректория для документации по программной части проекта).
    • README.txt (информация о программной части проекта).
    • Makefile (сценарий сборки проекта).
    • /vendor (поддиректория для задействованных в проекте продуктов сторонних поставщиков).
      • /Стороннний продукт 1.
      • /Стороннний продукт 2.
      • ...
    • /Module1
      • README.txt (информация о Module1).
      • Makefile (сценарий сборки Module1).
      • /include (поддиректория с интерфейсами Module1).
        • Module1.h
      • /src (поддиректория с исходными текстами Module1).
        • Module1_1.c
        • Module1_2.c
        • /PC (поддиректория с исходными текстами, специфическими для IBM PC).
          • Module1_3.c (вариант для IBM PC).
        • /AVR (поддиректория с исходными текстами, специфическими для Atmel AVR).
          • Module1_3.c (вариант для Atmel AVR).
        • ...
      • /test (поддиректория с тестами Module1).
      • /mocks (поддиректория с подставными объектами для тестирования Module1).
      • /obj (поддиректория с объектными модулями).
        • /PC (поддиректория с объектными модулями для IBM PC).
        • /AVR (поддиректория с объектными модулями для Atmel AVR).

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

Стратегии разработки с использованием кросс-средств

Разработка ПО для микроконтроллеров имеет одну особенность, с которой не сталкиваются разработчики ПО для настольных или серверных платформ.
Разрабатывая ПО для персонального компьютера, разработчик, как правило, использует в качестве инструментальной платформу, совпадающую с целевой. Например, программа, предназначенная для выполнения в среде MS Windows на совместимом с IBM PC персональном компьютере, почти наверняка будет разрабатываться на таком же компьютере и в той же операционной системе с использованием подходящей среды разработки (как правило, MS Visual Studio либо одного из продуктов Borland или его преемников). Теоретически можно выполнить эту же разработку, скажем, в среде Linux или на платформе Macintosh, но для этого нужна чрезвычайно веская причина, которую мне трудно себе представить. Конечно, остаются проблемы совместимости между версиями ОС, под которыми должна работать разработанная программа, но они, как правило, незначительны по сравнению с той большой проблемой, которая поджидает разработчика ПО для микроконтроллеров.
Те, кто проектировал встроенные системы на микропроцессорах фон-неймановской архитектуры (например, Intel 8080/8085 или Zilog Z80, также имели возможность построить инструментальную систему на этих же микропроцессорах. Так, для разработки различных устройств на базе Z80 в начале 1990-х я вполне успешно использовал компьютер Sinclair ZX Spectrum, оснащенный соответствующей периферией и переработанной для моих нужд прошивкой.
Специфика архитектуры однокристальных микроконтроллеров, как правило, не позволяет построить на их базе инструментальную систему. Во-первых, многие микроконтроллеры имеют гарвардскую архитектуру со всеми вытекающими последствиями. Во-вторых, их память программ выполнена по технологии FLASH и допускает ограниченное число циклов перезаписи (например, для микроконтроллеров семейства Atmel AVR гарантируется 10.000 циклов), что более чем достаточно для использования в целевом изделии, но крайне мало для использования в инструментальной системе (особенно с учетом того, что мы намерены непрерывно запускать регрессионные тесты при малейшем изменении кода).
В данной ситуации единственный выход — разработка ПО для микроконтроллеров с использованием кросс-средств. Впрочем, и в этом случае возможны варианты, отличающиеся соотношением частей работы, выполненных на инструментальной и целевой системах. Поскольку вся работа, связанная с получением результирующего кода, в любом случае выполняется на инструментальной системе, различия между этими вариантами относятся в основном к способам тестирования и отладки этого кода.

Тестирование и отладка на целевой системе

Самый примитивный и одновременно самый трудоемкий вариант. Исполняемый код, полученный на инструментальной системе, загружается в целевую систему при помощи программатора и подвергается тестированию. Этот подход наиболее популярен среди конструкторов-самоучек, не видящих разницы между тестированием и отладкой.
Разумеется, работоспособность изделия — это конечная цель проекта, и приемочное тестирование просто обязано осуществляться на целевой системе. Однако приемочное тестирование — довольно дорогое и длительное мероприятие, в ходе которого обычно задействуется сложное стендовое оборудование и применяются ручные операции, не поддающиеся автоматизации. Регулярное использование такого тестирования в ходе проекта нацелесообразно.
Что касается модульного тестирования, его проведение на целевой системе еще более проблематично, чем для приемочного тестирования. В основном это связано с тем, что в процессе модульного тестирования на консоль выдается существенный объем диагностической информации. Поскольку зачастую встроенная система не имеет никаких средств вывода текстовой информации, выполнение модульных тестов на ней попросту теряет смысл. Конечно, можно дооснастить целевую систему консолью, но это повлечет дополнительные расходы, поскольку в штатном режиме работы разрабатываемого устройства консоль не требуется.
Прошивка кода в целевую систему требует некоторого времени. Следовательно, некладные расходы на тестирование растут. Особенно это существенно в случае, когда объем памяти микроконтроллера мал, и тесты приходится разбивать на части и выполнять цепочкой. А мы уже знаем из предыдущих статей, что модульные тесты должны выполняться практически мгновенно, иначе разработчики будут стараться избегать их, и идея TDD теряет смысл.
Выше упоминалось, что программная память микроконтроллеров допускает ограниченное число циклов перезаписи. Если мы будем запускать модульные тесты достаточно часто, мы рискуем исчерпать это число. Конечно, микроконтроллер стоит не слишком дорого, но этот фактор следует иметь в виду.
Есть и еще один, также немаловажный фактор. При таком подходе разработчики программного обеспечения вынуждены ждать, когда разработчики аппаратной части выдадут им работоспособный образец устройства. Это значит, что они неизбежно приступят к работе с некоторой задержкой. Такое вынужденное безделье затягивает срок разработки и делает его весьма чувствительным от срыва графика коллегами, ответственными за аппаратную часть. Конечно, использование древней водопадной модели позволило бы занять разработчиков на ранних этапах детальным проектированием системы; однако мы придерживаемся гибких методик, а они рекомендуют воздерживаться от больших объемов проектирования, предпочитая короткие итерации.
Отдельный вопрос — отладка на целевой системе. Хотя она в принципе возможна благодаря тому, что современные микроконтроллеры позволяют подключать устройства для аппаратной отладки на основе интерфейса JTAG, все же я бы рекомендовал пользоваться этой возможностью лишь в самых крайних случаях, когда решить проблему другими средствами не удается.

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

На первый взгляд этот вариант может показаться нереальным. Откомпилировать на инструментальной системе код для целевой платформы — еще куда бы ни шло, но тестирование, да еще и отладка на процессоре с совершенно иной архитектурой?.. Впрочем, не будем торопиться с выводами. Как мы видели ранее, писать тесты перед кодом тоже на первый взгляд казалось абсурдным, однако на деле оказалось весьма полезно и практично.
Прежде всего, воспользуемся тем, что для микроконтроллера имеются компиляторы с языка C. Поскольку для инструментальной платформы также имеются такие компиляторы, а язык C известен своей кроссплатформенностью, мы можем полагать, что программа, корректно работающая на инструментальной системе, имеет шансы столь же корректно работать и на целевой системе. Для пущей совместимости мы будем использовать для обеих платформ один и тот же компилятор GCC.
А как быть с периферийными устройствами микроконтроллера, не имеющими аналогов на инструментальной платформе? При правильном подходе к проектированию программы это тоже не проблема. Достаточно лишь сконцентрировать весь код, работающий напрямую с аппаратурой, в прослойке HAL. При этом основная часть кода не будет зависеть от конкретного оборудования и, следовательно, ее можно будет выполнять, тестировать и отлаживать на инструментальной платформе.
Еще лучше абстрагироваться от оборудования помогает использование паттерна проектирования MCH.
Остается последний вопрос: каким образом в процессе тестирования имитировать внешние по отношению к нашей системе события? Те, кто внимательно прочитал первую часть статьи, уже знают ответ на этот вопрос: будут задействованы тестовые двойники.
В нашем проекте мы будем максимально использовать тестирование и отладку на инструментальной системе.

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

Есть еще один, промежуточный вариант: Тестирование и отладка на модели.
Схемотехническое моделирование электронных устройств применяется достаточно давно, не один десяток лет. Сегодня доступны несколько инструментов моделирования (большинство из известных мне представляют собой клоны довольно древнего пакета Spice) и множество моделей различных электронных устройств, от резистора до микропроцессора. Эти инструменты (и использованные в них модели) варьируются от простейших и довольно грубых, на которых можно проверить лишь общие принципы, до довольно изощренных, учитывающих тонкие детали поведения моделируемых объектов.
Модель имеет важное преимущество перед реальным изделием: она гораздо дешевле как в изготовлении, так и в модификации. Можно оперативно вносить изменения в модель и оценивать их результативность, а в случае неудачи просто аннулировать эти изменения и вернуться к исходному варианту (это особенно просто, если модели хранятся в репозитории системы управления версиями). Изменения на макете вносятся дольше и требуют больше усилий, а неудачи чреваты выходом из строя ценных деталей.
Однако не следует идеализировать модели. Они имеют также и существенные недостатки. Модели некоторых элементов могут быть упрощены в целях ускорения их моделирования, либо могут содержать ошибки. И в том, и в другом случае поведение модели будет отличаться от поведения реальной схемы.
Возможны также сложности при моделировании поведения систем реального времени. Поскольку моделирование сложной системы — серьезная вычислительтная нагрузка, оно может производиться довольно медленно. В одних случаях это легко учесть и скорректировать, в других результат моделирования может оказаться полностью неадекватным.
Для моделирования схем с использованием микроконтроллеров семейства AVR существует несколько инструментов. Весьма популярны среди них VMLAB и Proteus. VMLAB имеет довольно ограниченные возможности моделирования (особенно это касается аналоговой части схемы), зато он бесплатен, очень прост в освоении и использовании и относительно мало нагружает процессор. Proteus дает больше возможностей, однако при этом он достаточно дорог (полнофункциональная версия обойдется в 3495 английских фунтов стерлингов), и его освоение потребует больше усилий; впрочем, Proteus также не всемогущ и не идеален.
Поскольку в целом поведение модели достаточно точно соответствует поведению реального устройства, модель можно использовать для тестирования и отладки вместо целевого устройства. Впрочем, этот процесс наследует недостатки отладки на целевом устройстве, поэтому мы не будем им злоупотреблять.
Впрочем, пренебрегать им мы тоже не будем и попробуем провести испытания готового устройства на модели перед тем, как изготовить реальный прототип.

Проверка инструментария

Перед тем, как приступить непосредственно к написанию кода, убедимся, что наш инструментарий готов к работе. Для разминки попробуем откомпилировать классический вариант «Hello World!».
Сначала, как было оговорено ранее, создадим рабочую поддиректорию для нашего проекта. Назовем ее Blinker. В ней создадим поддиректорию Software, а в той — поддиректорию Blinker. Совпадение имен с корневой поддиректорией проекта не случайно — в этой поддиректории (будем называть ее поддиректорией модуля Blinker или там, где это не приведет к путанице, — просто поддиректорией модуля) будет находиться главный модуль нашего одноименного проекта, содержащий функцию Main.
Для начала создадим в поддиректории модуля поддиректорию для исходных текстов программы src и в ней наконец-то разместим наш первый долгожданный код:

Код: (C) Blinker.c
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("Hello world!\n");
    return 0;
}

В данном проекте мы не будем пользоваться различными новомодными IDE. С учетом специфики проекта они отнимут больше времени, чем сэкономят. Для компиляции будем использовать старую добрую командную строку, а чтобы не упражняться в скоропечатании, применим утилиту GNU Make, благо она идет «прицепом» к выбранному нами GCC. Итак, в поддиректории модуля создаем файл Makefile для сборки нашего вырожденного мини-проекта:

Код: (Text) "Makefile"
# Подставьте здесь команду для вызова вашего компилятора C
CC=mingw32-gcc

.DEFAULT : Blinker.exe
Blinker.exe : obj\PC\Blinker.o
        $(LINK.c) -o Blinker.exe obj\PC\Blinker.o

obj\PC\Blinker.o : src\Blinker.c
        $(COMPILE.c) -o obj\PC\Blinker.o src\Blinker.c

.PHONY : clean
clean :
        del /q obj\PC\*.*
        del /q Blinker.exe

После того, как мы создадим поддиректорию obj\PC в поддиректории проекта, все готово к работе. запускаем сборку проекта командой make:

mingw32-gcc    -c -o obj\PC\Blinker.o src\Blinker.c
mingw32-gcc     -o Blinker.exe obj\PC\Blinker.o

Запускаем только что созданную программу Blinker.exe:

Hello world!

Состояние проекта на данный момент вы можете найти в файле Software1.zip, приложенном к статье.
Теперь у нас есть все необходимое для дальнейшей работы.

Анатомия встроенной программы

При написании кода мы должны учитывать, что встроенные программы имеют свою специфику по сравнению с обычными настольными приложениями.
Обычно программа, разработанная для персонального компьютера, запускается по инициативе пользователя в тот момент, когда в ней возникает необходимость. В данный момент нас не интересует, как именно производится ее запуск: из командной строки, комбинацией «горячих» клавиш, щелчком мыши на панели графического интерфейса или как-то еще. Главное, что в результате запуска происходит определенная последовательность действий:
  • Инициализация программы (подготовительные действия, например, настройка параметров, открытие файлов, связь с базой данных и т.п.).
  • Основная часть программы.
  • Завершение программы (освобождение ресурсов, удаление временных файлов и т.п.).
После завершения программы ресурсы компьютера освобождаются для выполнения других программ.
На псевдокоде структуру программы можно представить таким образом:

Код: (C)
int main()
{
    init();
    run();
    finalize();
}

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

Код: (C)
int main()
{
    init();
    for(;;)
        run();
}

После инициализации программа входит в бесконечный цикл, повторяя одни и те же действия (составляющие функцию run()). В принципе зацикливание можно было бы сделать внутри самой функции. Я не стал этого делать по двум причинам.
Во-первых, даже беглый взгляд на тело функции main() позволяет увидеть, что она никогда не завершится; для этого не нужно искать исходный код функции run(). Выразительность кода весьма важна, поскольку код пишется в первую очередь для человека, компьютер в нем не нуждается.
Во-вторых, мы можем трактовать функцию run() как одну итерацию некоторого процесса, который выполняется бесконечно. Если эта итерация выполняется достаточно быстро (понятие «достаточно быстро», конечно же, весьма расплывчато; то, что приемлемо в одних случаях, может оказаться совершенно приемлемым в других, поэтому нельзя рассматривать его в отрыве от контекста), мы могли бы поместить в цикл несколько подобных функций (например, run1(), run2() и т.д.), создавая иллюзию их одновременного выполнения. Мы еще вернемся к этому вопросу, когда попытаемся заставить наш контроллер выполнять несколько разных заданий одновременно.
Кстати, те, кому доводилось явно писать циклы обработки сообщений в графических приложениях MS Windows, могут обнаружить в данной конструкции некоторую аналогию.
Изменим нашу программу таким же образом:

Код: (C) Blinker.c
void init(void) { }

void run(void) { }

int main()
{
    init();
       
    for (;;)
        run();
}

До сих пор мы злостно нарушали основной принцип TDD: не писать ни строчки кода до тех пор, пока у нас нет проваленных тестов. Впрочем, на то есть уважительная причина, о которой я уже говорил: с использованием Unity затруднительно протестировать функцию main(), поскольку модульные тесты сами по себе исполняемые программы. Впрочем, до сих пор мы написали так мало кода, что в такой программе способен сделать ошибку лишь, как это принято сейчас говорить, «альтернативно одаренный». Однако на этом пора останавливаться и остальную часть программы разрабатывать цивилизованными методами.
Начнем мы с небольшого рефакторинга — вынесем функции init() и run() в отдельный модуль. Этим простым действием мы решим сразу две задачи.
Во-первых, мы сразу же получаем жизнеспособный паттерн встроенной программы, пригодный практически для любых программ. Подключая к нашему каркасу различные проблемно-ориентированные модули, мы можем решать самые разные задачи, не меняя архитектуры проекта. Фактически мы получаем некую разновидность полиморфизма, управляемого на стадии компоновки программы с нужным модулем.
Во-вторых, что не менее важно, этот внешний модуль будет самым обычным модулем без каких-либо особенностей. А это значит, что мы сможем подвергнуть его тщательному тестированию без каких-либо ограничений.
Примечание. Из вышесказанного никоим образом не следует, что программа в целом никак не тестируется. Мы всего лишь не будем писать модульных тестов для нашей функции main(). Однако это не является помехой к тому, чтобы подвергнуть программу другим видам тестирования, например, функциональному или приемочному. Об этих видах тестирования мы поговорим позже.



Продолжение: часть 3.

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

С уважением, автор.
Версия для печати
Обсудить на форуме (2)