Последние записи
- Преобразовать массив байт в вещественное число (single)
- TChromium (CEF3), сохранение изображений
- Как в Delphi XE обнулить таймер?
- Изменить цвет шрифта TextBox на форме
- Ресайз PNG без потери прозрачности
- Вывод на печать графического файла
- Взаимодействие через командную строку
- Перенести программу из Delphi в Lazarus
- Определить текущую ОС
- Автоматическая смена языка (раскладки клавиатуры)
Интенсив по Python: Работа с API и фреймворками 24-26 ИЮНЯ 2022. Знаете Python, но хотите расширить свои навыки?
Slurm подготовили для вас особенный продукт! Оставить заявку по ссылке - https://slurm.club/3MeqNEk
Online-курс Java с оплатой после трудоустройства. Каждый выпускник получает предложение о работе
И зарплату на 30% выше ожидаемой, подробнее на сайте академии, ссылка - ttps://clck.ru/fCrQw
19th
Авг
MP3 изну-три
В этой статье я расскажу, как устроен MP3 файл, и покажу, как можно работать с ним в ваших программах. Мы попробуем извлечь информацию о файле, такую как длину трека, его битрейт и частоту дискретизации. Воспроизведение звука мы рассматривать не будем, это отдельная, и я думаю намного более сложная тема…
Александр Терлецкий
by mutabor altair.79@mail.ru
Немного про формат
MP3 расшифровывается как MPEG 1 Layer 3, т.е. MPEG версии 1, третья редакция, или как-то в этом роде. Нам важно понять, что бывает еще MPEG 2, а Layer не обязательно может быть третьим. Что такое MPEG, почему Layer 3, чем отличается MPEG1 от MPEG2, и прочие подобные вопросы я рассматривать не буду, т.к. это само по себе тянет на отдельную статью. MP3 это сжатый формат, сжатие достигается за счет убирания из исходного звука частот заведомо не слышимых человеком, ну и еще за счет алгоритма сжатия какого-нибудь (это я тоже не буду здесь рассматривать). Именно поэтому сжимать архиваторами MP3 файлы не получается (вернее получается, но результат не впечатляет), они уже сжатые. Внутри файл состоит из фреймов. Заголовка у MP3 файла нет, зато у каждого фрейма есть свой заголовок, с ним то мы и будем в основном работать. Фрейм можно рассматривать, как некий дискретный кусок звукового потока.
Теги
Помимо фреймов, в файле могут быть один или несколько ID3 тегов. ID3 – это теги специально разработанные для формата MP3, т.к. он сам не содержит никакой описательной части. Теги бывают разных версий, чаще всего это или ID3v1.x или ID3v2.x. Теги первой версии находятся в последних 128 байтах файла, начинаются с символов TAG (такой тег может занимать и более чем 128 байт, но это редко, это усовершенствованная первая версия ID3). Теги второй версии могут находиться в любой части файла, но чаще они располагаются в начале файла, и начинаются с символов ID3. Теги второй версии намного более расширенные, чем теги первой версии, в них нет ограничений на длину полей с описанием трека, количество доступных полей намного больше, можно использовать Юникод, тег может содержать в себе изображение. Длина тега второй версии не фиксированная, и определяется по заголовку тега (в отличие от MP3 файла, у ID3v2 тега есть заголовок). Наличие тегов в файле не является обязательным, и их может не быть совсем, а могут быть оба, и тег первой версии в конце файла, и второй в начале, это делается в целях совместимости с большим количеством плееров. Часто возникает проблема с русскими символами в тегах в плеерах мобильных телефонах, я думаю это связано с Java машиной в телефоне, дело в том, что она поддерживает строки в формате UTF-8, а русские теги часто имеют кодировку Win-1251. Чтобы избежать «козябриков» на этих устройствах, нужно сохранять теги в Юникоде.
Теперь немного о том, как читать теги программно. Я не буду подробно освещать эту тему здесь, скажу лишь, что существуют библиотеки, компоненты для их чтения, также в сети доступна спецификация на эти теги, так что можно и самому написать их обработку, если есть желание. Звуковые движки, например тот же BASS, тоже умеют читать теги. Они кстати умеют и все остальное, о чем я буду писать ниже, и если ваша задача – получить информацию о файле, и вам не интересно как он устроен, можете в принципе дальше не читать, так как через интерфейс движка это сделать намного легче. Если вам звук нужно еще и воспроизводить, то этот способ даже лучше, зачем ковыряться в спецификациях, если есть удобный инструмент. Но бывают случаи, когда звук воспроизводить не надо, а нужна только информация о файле, и тогда лучше подключить легкий модуль, чем таскать движок за программой.
Битрейт
В этой статье я затрону только те характеристики, которые мы будем читать из файла, остальную общую информацию об MP3 можно без проблем найти в Интернете, ее достаточно, в отличие от более специализированной информации о формате.
Как вы все, наверное, знаете, MP3 файл может иметь различный битрейт, это кол-во бит выделенных на кодирование звука в единицу времени. Понятно, что чем он выше, тем качество звука лучше, и размер файла соответственно тоже больше. Значение битрейта в MP3 может находиться в пределах от 8 до 320 кбит/с. Полный список смотрите в Рис 2. Битрейт может быть постоянным (constant) и переменным (variable), это обозначается аббревиатурами CBR и VBR соответственно. Переменный битрейт позволяет снизить размер файла, не снижая качества. Это достигается за счет того, что на участках, где это не требуется, например тишина в начале трека, используется меньшее количество бит для кодирования. На уровне структуры файла это выражается в том, что один фрейм может иметь битрейт, например 128, а следующий может иметь уже 192, и т.д. В каждом отдельном фрейме битрейт имеет значение кратное степени двойки, соответствующее спецификации (см. рисунок 2). В целом по файлу, битрейт, в случае если он постоянный, не будет отличаться от значения битрейта первого фрейма, таким образом, нам достаточно взять информацию из первого фрейма, и мы имеем информацию о файле. В случае если битрейт переменный, то все усложняется, нам недостаточно одного фрейма, чтобы делать выводы об общем битрейте файла.
Частота дискретизации
Вторая характеристика, которую мы рассмотрим, это частота дискретизации или Sample Rate. Это частота, с которой при кодировании звука снимаются замеры с источника звука. Например, если частота у нас 44100 Гц, то это значит, что столько раз в секунду снимаются и сохраняются значения оригинального, аналогового звука. По сути, оцифровка. Можно конечно и уже оцифрованный звук перегнать с другим сэмпл-рейтом, но качество можно только понизить, повысить уже вряд ли удастся. Как и в случае с битрейтом, от частоты дискретизации напрямую зависит качество звука. Частота дискретизации в MP3 не может варьироваться, и всегда постоянна для всего файла.
Длина
У любой музыкальной композиции или любой другой звуковой записи есть такая характеристика, как длина. Упоминаю я ее отдельно для того, чтобы обрадовать вас, что так как у MP3 файла общего заголовка нет, то и готовые сведения о длине нам взять в принципе неоткуда (ID3 тег не в счет, в нем может быть длина трека, а может и не быть, а может и самого тега не быть). Поэтому длину нам придется высчитывать самостоятельно. На этом с характеристиками закончим и перейдем к разбору фреймов.
Рис. 1.
Фреймы
Один фрейм состоит из двух блоков (последовательностей бит) – заголовка и блока данных (см. рисунок 1). Заголовок представляет собой последовательность из 32 бит (4 байта), в которых описываются все необходимые параметры звука, а также параметры самого фрейма (например, его длина). В блоке данных находиться непосредственно звуковые данные. Рассмотрим заголовок подробнее (см. рисунок 2). Вначале идут 12 бит сигнатуры (Sync), по этим битам нужно искать фреймы, все они установлены в единицу. Затем идут еще три бита: версия, слой и защита от ошибок. Я сделал вывод, что если принять по умолчанию что мы работаем с MP3 файлом, а не с какой-нибудь другой версией MPEG, томожно брать первые два байта как сигнатуру, в этом случае они могут иметь всего две комбинации: FF FA или FF FB. Мне показалось так удобнее искать фреймы, и именно так я реализовал это в коде. Через третий байт мы пока перескочим, и я вкратце расскажу о четвертом. В нем есть такая интересная штука как режим стерео. Дело в том, что MP3 имеет еще оди н способ уменьшить вес файла, не ухудшив качество. Это режим Joint Stereo (объединенное стерео). Что же это такое? Пройдемся по порядку. Режим моно, это, как вы знаете, когда всего один звуковой канал. Стерео — это независимые левый и правый каналы. Для хранения стерео данных, необходимо ровно в два раза больше места, чем для моно. Joint Stereo же позволяет хранить два независимых канала, при этом, занимая меньше места, это достигается за счет «умной» паковки во время сжатия. Если выбран этот режим, и в данном сэмпле звука левый и правый каналы не отличаются, то кодер сохраняет только один из них, когда же каналы отличаются, то они сохраняются оба. Это в своем роде стерео по требованию, оно используется там, где это реально нужно, а где не нужно, место экономится. В четвертом байте хранится еще ряд параметров, не буду на них останавливаться, кому интересно, смотрите на рисунке, там все подписано, обычно они интереса не представляют.
Самый значимый для нас это 3-й байт заголовка. В нем содержится информация о битрейте, частоте дискретизации, установлен или нет Pad бит (определяет наличие добавочного байта в фрейме), все это вместе взятое позволяет нам высчитать размер этого фрейма. Размер фрейма высчитывается по формуле 1:
144 * BitRate / SampleRate + Pad; (1)
Рис. 2.
Если Pad бит равен единице то и в формулу подставляем единицу, если нулю – подставляем нуль. Битрейт и частоту подставляем в их полном виде, без округлений, битрейт в битах, а частоту в герцах. В файлах к этой статье вы найдете пример извлечения нужной информации из заголовка фрейма и реализацию этой формулы на языке Delphi. Чтобы что-то извлечь из заголовка, его нужно сначала найти, это тоже там есть. На самом деле достаточно найти первый фрейм, позицию каждого следующего мы уже будем знать, прибавляя к позиции текущего фрейма его длину.
Теперь поговорим о том, как найти переменный битрейт и длину трека. С постоянным битрейтом все ясно, он одинаковый для всего файла, и его можно взять из первого фрейма. С переменным не так. Во-первых, нигде не написано что он переменный, и чтобы это определить, нужно прочесть больше чем один фрейм. Я пошел самым простым путем, и читаю два первых фрейма. Если битрейт у них одинаковый я считаю что битрейт постоянный, если разный то переменный. Это скорее всего не точно, ведь переменным он может стать и после второго фрейма. Однозначных рекомендаций по этому вопросу я не встречал, возможно, есть соглашение, что если битрейт переменный, обязательно кодировать первые два фрейма с различным битрейтом, я бы так и сделал на месте разработчиков, но это только мои предположения. Если у вас есть более точная информация на этот счет, оставляйте комментарии к статье на сайте журнала, интересно будет почитать. Понятно, что в случае переменного битрейта необходимо как-то высчитать его среднее значение по всем фреймам. Оставлю это вам, у меня в коде в этом месте стоит заглушка, т.е. просто в случае переменного битрейта, в качестве его значения присваивается ноль.
Заключение
Теперь про длину трека. Ее можно высчитать, поделив размер файла (за вычетом тегов и прочего мусора, нужны только фреймы, не совсем ясно только, включать заголовки или нет) на битрейт, если он постоянный. Таким образом, мы получим длину трека в секундах. Если битрейт переменный, этот способ не подходит (хотя можно на усредненный битрейт поделить), тогда мы можем задействовать сэмпл-рейт, он у нас постоянный для всего файла, и представляет собой кол-во сэмплов в секунду. Если предположить что сэмпл и фрейм это одно и то же, то можно узнать количество секунд в треке, посчитав все фреймы. Я не пробовал ни тот, ни другой способ, не буду портить вам удовольствие и позволю самим попробовать высчитать длину трека. Если что интересное получится, пишите в комментариях. Опять же, можно переменный битрейт считать от обратного, найти по сэмпл- рейту длину, и потом уже одним действием найти средний битрейт.
Все исходные коды, упомянутые в статье, приложены непосредственно к журналу «ПРОграммист. Пятый выпуск».
На этом закончим, спасибо за внимание, читайте наш журнал.
Статья из пятого выпуска журнала «ПРОграммист».
Скачать этот номер можно по ссылке.
Ознакомиться со всеми номерами журнала.
14th
Июл
Энкодер датчика PDF на ПЛИС. Часть 1
Данная статья рассчитана в помощь программистам и инженерам-разработчикам в области промышленной автоматизации и АСУТП. Поскольку материал объемный, то было решено разделить статью на части, в первой рассмотрим схемотехнику и конструктив модулей передачи и приема сигналов с датчиков приращений, алгоритм работы энкодера, а в остальных – его реализацию на ПЛИС, методику программирования и непосредственно практическое создание тестовой утилиты визуализации состояний датчика приращений…
Сергей Бадло
by raxp http://raxp.radioliga.com
Каждый компонент системы не должен быть наилучшим, а ровно таким, чтобы обеспечить требуемый уровень функционирования всей системы… / принцип системотехники
Для передачи импульсных сигналов с датчика приращений на относительно большие расстояния в условиях промышленных помех можно использовать как радиоканал, так и проводной вариант*, к примеру, дифференциальные интерфейсы на основе RS- 485 и LVDS. Реализация радиоканала и RS-485 видится избыточной, так как необходимо наличие «упаковщика» как минимум трех сигналов с шифратора приращений (A, B и строба), т.е. наличие контроллера. Как обойти эту проблему?
Список сокращений, использованных в статье:
PDF – датчик углового положения,
EEPROM – перепрограммируемая ПЗУ,
ПЛИС – перепрограммируемые логические матрицы с сохранением памяти CPLD и без сохранения FPGA (работа схемы ограничивается наличием питающего напряжения),
ТИ – тактовые импульсы,
LVDS (Low-Voltage Differential Signaling) – интерфейс передачи информации дифференциальными сигналами малых напряжений,
Шифратор приращений – преобразователь, на выходе которого в цифровой форме представляются воспринимаемые ими перемещения. Различают* поворотные и абсолютные шифраторы.
Краткий экскурс…
Вспомним про формат LVDS. LVDS используется в таких компьютерных шинах как FireWire, USB 3.0, PCI Express, DVI, Serial ATA. Но среди прочего, данный интерфейс получил распространение и для передачи сигналов на больших скоростях на расстояния до сотни метров. Да, именно так, уже существуют и такие LVDS драйверы. Кроме того, сигналы с шифратора приращений необходимо декодировать и привести в удобоваримый вид. А значит, возникает необходимость создания аппаратного энкодера для определения таких параметров как: положение ротора, направление вращения, обрыв в канале и т.п.
Рис. 1
Для реализации алгоритма энкодера видится два пути:
- Использование микроконтроллера
- Использование ПЛИС
Хороши оба варианта. Но для меня как разработчика более удобен вариант представления в виде схемы. Достоинства программируемых логических интегральных схем (ПЛИС) хорошо известны, это и наличие множества готовых библиотек от простейших логических элементов до микропроцессоров и возможность многократного перепрограммирования для изменения схемы, без внесения изменений в печать, и наличие си- подобного языка VHDL и возможность просто нарисовать поведение схемы. Хотя последние вряд-ли можно назвать преимуществами, так эти свойства присущи и микроконтроллерам…
Таким образом, задачу разработки энкодера для шифраторов приращений можно разбить на следующие этапы:
- Разработка модуля связи (передачи и приема) для передачи импульсных сигналов на большие расстояния в условиях промышленных помех
- Аппаратная реализация алгоритма энкодера на существующих ПЛИС
- Создание тестовой программы визуализации состояния датчиков приращений
Практика. Реализация модуля передачи и приема сигналов с шифратора приращений
Дифференциальный метод передачи используется в LVDS, поскольку обладает меньшей чувствительностью к общим помехам, чем простая однопроводная схема. Применение источников сигнала с дифференциальным токовым выходом и приемников с низкоомным дифференциальным входом (см. рисунок 2) обеспечивает минимальные индуктивные наводки, поскольку информация передается в форме тока, а емкостная наводка мала, так как при хорошей симметрии линии передачи она является синфазной и подавляется входным дифференциальным приемником. Дополнительной защитой линии является ее экранирование.
Рис. 2. Канал передачи сигнала с помощью тока нечувствителен к индуктивным наводкам
Поскольку дифференциальные технологии, в том числе и LVDS, менее чувствительны к шумам, то в них возможно использование меньших перепадов напряжения, до 350 мВ. что позволяет по сравнению с другими способами передачи сигналов значительно снизить потребляемую мощность. Например, статическая мощность, рассеиваемая на нагрузочном резисторе LVDS, составляет всего 1.2 мВт, по сравнению с 90 мВт, рассеиваемыми на нагрузочном резисторе интерфейса RS-422. На рисунке 3 представлена схема организации канала связи между шифратором приращений и контроллером энкодера с использованием дифференциальных линий передачи:
* Комментарий автора.
Поворотные шифраторы – генерируют выходные импульсы, которые подсчитываются реверсивным счетчиком, поэтому их показания соответствуют тому, как далеко диск продвинулся с начала отсчета. Здесь в основном применяются два чувствительных элемента, расположенных в преобразователях таким образом, что их выходы сдвинуты относительно друг друга на 90° по фазе.
Абсолютные шифраторы – реализуют кодированный выход, который индицирует абсолютное положение контролируемого объекта, причем кодирование производится в двоичном коде, а его длина соответствует длине кода измерительной системы. Как правило, снабжены интерфейсами: SSI (Synchronous Serial Interface), СAN, PROFIBUS, RS-485.
** Комментарий редакции.
Следует упомянуть про параллельно-последовательные преобразователи со встроенным антидребезгом и нормализацией сигналов цифровых входов и преобразования их в единый поток данных, передаваемый по SPI интерфейсу. К примеру, ИМС SN65HVS88x от Texas Instruments. Однако это потребует минимум еще трех корпусов со стороны передатчика и приемника, что приведет к удорожанию модуля связи. Да и сам интерфейс SPI не предназначен для таких расстояний. Кроме того, существуют кодеры на основе сдвигающих регистров (НТ12Е/НТ12D или MC145026/28). Но их применение оправдано в случае использования радиоканала.
Рис. 3. Схема организации канала передачи сигналов энкодера
Заданным условиям удовлетворяют нижеприведенные схемы (см. рисунок 4 и 5) на основе дифференциальных приемо-передатчиков сигнала LVDS формата SN65LVDS31 и SN65LVDS32 фирмы Texas Instruments [1].
Модуль связи энкодера конструктивно состоит из двух модулей: передачи и приема, разнесенных в пространстве. Модуль передачи размещается рядом с энкодером, а модуль приема непосредственно около контроллера энкодера.
Плата модуля передачи имеет в своем составе:
- узлы согласования входного уровня для типов датчиков приращений с открытым коллектором или потенциальным выходом
- узел формирования питающего напряжения 3.3В для ИМС дифференциального передатчика – U1
- дифференциальный передатчик с согласующими сопротивлениями – U2
- клеммник для подключения входных сигналов с датчика приращений – X1
- клеммник выходных сигналов – X2
Плата модуля приема включает:
- дифференциальный приемник с согласующими сопротивлениями – U1
- узел формирования питающего напряжения 3.3В для ИМС дифференциального приемника с гальванической развязкой – U2
- узлы согласования выходного уровня по типу СК или открытый коллектор
- клеммник для подключения входных сигналов с линии связи – X1
- клеммник выходных сигналов для энкодера – X2
Конструктив модуля связи
Платы модулей [3], габаритами 100х85 мм, выполнены из стеклотекстолита и разведены в пакете OrCad (см. рисунки 6 и 7). На плате передатчика перемычки X1…X4 (PLS-2) предназначены для конфигурации платы под датчики с открытым коллектором и потенциальным выходом. Суппрессоры D7…D10 служат для ограничения входных уровней и дополнительной защиты входных цепей микросхемы дифференциального передатчика U9. R21…R24 обеспечивают согласование длинной линии на основе витых пар***.
Рис. 4. Схема электрическая принципиальная модуля передачи сигналов датчика приращений
На плате приемника предусмотрен вариант, как потенциальных выходов, так и с открытым коллектором. Для питания цепей приемника использован DC-DC преобразователь PUS-0505 (U5). Допустимо использование любого 5-вольтового в корпусе SIP мощностью не менее полуватта. Гальваническую развязку сигналов обеспечивают оптроны TLP621 (U2, U3, U4, U6). Предусмотрено питание схемы как от 5В, так и 24В источника. Элементы R1…R4 обеспечивают согласование длинной линии на основе витых пар. При необходимости инверсии выходных сигналов предусмотрена схема НЕ на ИМС SN74F04 (U10).
Рис. 5. Схема электрическая принципиальная модуля приема сигналов датчика приращений
* Комментарий автора.
В качестве кабеля связи рекомендуется использовать следующие марки кабеля:
— FTP 4x2xAWG 24/1,
— S- FTP 4x2xAWG 24/1,
— S- STP 4x2xAWG 24/1.
Рис. 6. Печатная плата “вид снизу” модуля передачи и приема (сверху-вниз)
Все резисторы, для упрощения разводки платы, корпусные. Конденсаторы типоразмера 0805. Используемые входные и выходные клеммники – WAGO 236-401 c защелками под отвертку. Платы установлены в корпуса из поликарбоната PHOENIX CONTACT с креплением на DIN рейку. Непосредственно габариты и вся конструкция в сборе представлена на рисунке 8.
Аппаратная часть. Краткое описание контроллера энкодера
Аппаратной основой энкодера датчика приращений служит модуль DIC110 (UNIOxx-5) фирмы Fastwell [2, 3], выполненный в формате MicroPC и установленный в 8-ми слотовое промышленное шасси на базе 486 процессора под управлением DOS. Шасси позволяет установку как ISA, так и PCI плат периферийного ввода-вывода.
Модули UNIOxx-5 имеют 5 разделяемых линий прерываний, канал прямого доступа к памяти (DMA) и светодиод обращения к плате. Внутренняя структура представлена на рисунке 9. Платы UNIO, в зависимости от загруженной прошивки, могут выполнять цифровой/частотный ввод-вывод, аналоговый ввод-вывод (через модули Grayhill), измерение частоты и многие другое. Прошивка изменяется программно, благодаря чему разработчики получают уникальную возможность решать с помощью одной платы множество задач. Если взглянуть на структуру, то сразу видно, что подобные возможности реализуются в основном благодаря наличию ПЛИС. Каждая матрица обслуживает 24 канала ввода-вывода. Загрузка схем матриц производится при включении питания или аппаратном сбросе (RESET) из электрически перепрограммируемого постоянного запоминающего устройства (EEPROM). Изменение варианта загружаемой схемы и, следовательно, способа обработки сигналов осуществляется перепрограммирова- нием EEPROM непосредственно в системе.
Рис. 7. Расположение элементов “вид сверху и снизу” модуля передачи и приема (сверху-вниз)
Рис. 8. Конструктив модуля передачи и приема импульсных сигналов
Рис. 9. Структура модуля UNIOxx
Разработка ПО. Алгоритм энкодера
Итак, приступим к основной задаче. Для работы нам понадобится следующее:
- Turbo C++ IDE ver.3.0 от Borland
- IDE среда Xilinx Foundation Series 3.1i / 6.2i для ПЛИС [4]
- Модуль передачи-приема импульсных сигналов датчика приращений
- Промышленная плата UNIOxx-5 от Fastwell
- JTAG.XILINX программатор из материала [5] или утилита внутреннего загрузчика ISP от FastWell [6]
Энкодер предназначен для декодирования сигналов угловых датчиков положения и передачи информации об угловом положении, скорости, направления вращения, количества оборо- тов, а также допол- ните- льных сигна- лов (в виде приз- наков отказа кана- лов, на- личия строба) в виде 8-ми разрядной цифровой последовательности для дальнейшей расшифровки программным путем.
Входные сигналы контроллера
Сигналы с каждого датчика шифратора приращений представляют последовательности импульсов, смещенных на 90 градусов по периоду следования. Частота следования импульсов определяет скорость вращения. Максимальная частота – 200 кГц. Максимальное количество импульсов за 1- оборот – 6000. Каждый оборот идентифицируется стробирующим импульсом. На рисунке 10 представлена последовательность импульсов от датчика, поступающих на вход контроллера:
A – прямой и инверсный канал A,
B – прямой и инверсный канал B,
O – прямой и инверсный строб O (длительность равна четверти периодаимпульса по каналу А или В ±10%)
Рис. 10. Входные сигналы энкодера
Управляющие сигналы:
- сигнал чтения READ по шине ISA платы UNIOxx-5
- адресация (А0…А2) для организации чтения
- (коммутации) 6-ти внутренних регистров тактовые импульсы (меандр) частотой 50 МГц от внешнего генератора платы UNIOxx-5
Выходные сигналы контроллера
Выходные сигналы для каждой микросхемы FPGA передаются по шине ISA в последовательности, описанной в таблице. Дальнейшая работа с шиной ISA определяется принципиальной схемой и протоколом обмена с микросхемами FPGA. Cформируем основные требования к энкодеру:
- работоспособность при входных частотах канальных импульсов от 0.0 до 200 кГц
- выставление бита направления вращения
- запись данных в буферные регистры по поступлению: сигнала READ и опросе нулевого адреса из шины ISA, и хранение данных до поступления следующего сигнала READ
- измерение количества импульсов, соответствующих угловому положению датчика и реверсом счетчика количества импульсов по направлению вращения
- антидребезговый прием дискретных сигналов каналов (прямого и инверсного А, прямого и инверсного В)
- динамическое изменение ширины защитного интервала для каждого из каналов в пределах длительности канального импульса;
- хранение результата по предыдущему сигналу READ и выдачу текущего значения относительного углового положения, признаков канальных отказов, наличия строба, количества оборотов, относительной скорости
- выдача должна осуществляться путем последовательного выбора (адресации по шине адреса А0…А3) внутренних регистров
- хранения и записи в виде битовой последовательности DO0…DO7 по портам платы модуля UNIOxx-5
Теперь перейдем к программной части…
Скажем пару слов о среде САПР Xilinx****. Программные средства Xilinx Foundation представляют собой систему сквозного проектирования, которая реализует полный цикл разработки цифровых устройств на базе ПЛИС, включая программирование кристалла. Оболочка Project Navigator предоставляет пользователю интерфейс для работы с проектом и управления всеми процессами проектирования и программирования ПЛИС. Исходные описания проектируемых устройств могут быть представлены в текстовой форме с использованием языков HDL (Hardware Description Language), в виде диаграмм состояний или принципиальных схем. В состав пакета включен схемотехнический редактор и комплект библиотек.
Согласно спецификации на модуль UNIOxx-5 [2, 3], используются ПЛИС FPGA – XC5204. Теперь, запустив IDE Xilinx, создадим новый проект и выберем соответствующую матрицу (см. рисунок 11):
Рис. 11. Создание проекта и выбор параметров в среде Xilinx
Заключение
Следует обратить ваше внимание, что при необходимости передачи импульсных сигналов с датчика приращений на расстояния более чем несколько сот метров (но менее километра), нужно использовать дифференциальные приемо- передатчики с драйверами предкоррекции сигналов и компенсацией потерь. В качестве таких приемо-передатчиков можно рекомендовать микросхемы LVDS CLC001 и CLC012 фирмы National Semiconductor [8, 9].
* Комментарий автора.
Если вы впервые столкнулись с данной средой, то сперва рекомендуется ознакомиться с кратким руководством [7] по работе с пакетом.
Ресурсы
- Спецификация на ИМС SN65LVDS31 (SO-16) http://www.alldatasheet.com/datasheet-pdf/pdf/28186/TI/SN65LVDS31D.html и спецификация на ИМС SN65LVDS32 http://www.alldatasheet.com/datasheet-pdf/pdf/28217/TI/SN65LVDS32A.html
- Fastwel Micro PC compatible UNIOxx-5. Программируемые модули ввода-вывода. Руководство пользователя. – Doc. UNIOxx-5 ver.02.02
- Спецификация на модуль ввода-вывода UNIOXX-5 http://www.fastwel.ru/content/ecmsfiles/239170.pdf
- Сайт производителя Xilinx http://www.xilinx.com
- С. Бадло. JTAG.XILINX программатор. – Радиолюбитель, Минск, 2008, №7, с.38 http://raxp.radioliga.com/cnt/s.php?p=jtag.pdf
- Печатные платы модуля связи GERBER RS-274X http://programmersclub.ru/pro/pro4.zip
- Руководство по работе с Xilinx Foundation Series 3.1 http://raxp.radioliga.com/cnt/s.php?p=x3.zip
- Спецификация на драйвер CLC001 http://national.com/pf/CL/CLC001.html
- Спецификация на драйвер CLC012 http://national.com/pf/CL/CLC012.html
12th
Июл
Работа с MySQL в C++
Небольшая статья по взаимодействию с СУБД MySQL из программы на C++
автор статьи: psycho-coder
Тема на форуме
Немного теории
Код:
MYSQL mysql; // Дескриптор соединения. Структура, содержащая HANDLE для одного подключения к серверу.
MYSQL_RES *res; // Дескриптор результирующей таблицы
MYSQL_ROW row; // Массив полей текущей строки
MYSQL_FIELD *field; // Структура, которая содержит всю информацию, касающуюся отдельного поля таблицы
Функиции которые нам понабодятся:
Функция инициализации
MYSQL *mysql_init(MYSQL *mysql);
Где соответственно host — компьютер, на котором запущена СУБД MySQL, user — имя юзера для подключения, passwd — пароль, db — название предполагаемой для использования базы данных, port — порт, unix_socket — сокет или pipe-канал, который необходимо использовать.
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned int client_flag)
client_ flag может принимать несколько значений:
CLIENT_COMPRESS — используется сжатие.
CLIENT_FOUND_ROWS — возвращать число найденных строк.
CLIENT_IGNORE_SPACE — делает все имена функций зарезервированными словами.
CLIENT_INTERACTIVE — разрешает interactive_timeout секунд бездействовать (вместо wait_timeout) перед закрытием подключения.
CLIENT_NO_SCHEMA — запрещает синтаксис вида “db_name.tbl_name.col_name” (имя_базы_данных.имя_таблицы.имя_ко лонки). Используется для ODBC.
CLIENT_ODBC — устанавливает то, что это клиент ODBC.
CLIENT_SSL — используется защищенный протокол SSL.
Мы флагами пользоваться не будем.
Функция выполняющая запрос
int mysql_query(MYSQL *mysql, const char *query);
Функция возвращяющая строку с описанием ошибки
Код:
char *mysql_error(MYSQL *mysql);
функция, которая получает все строки результата запроса и хранит их в буфере-клиенте
MYSQL_RES * mysql_store_result(MYSQL *mysql);
Получает количество строк в результате запроса
my_ulonglong mysql_num_rows(MYSQL_RES *res);
Получает количество полей (столбцов) в результате запроса
unsigned int mysql_num_fields(MYSQL_RES *res);
Заполняет массив полей для текущей строки
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);
Заполняет структуру для текущего поля (fieldnr)
MYSQL_FIELD *mysql_fetch_field_direct(MYSQL_RES *res, unsigned int fieldnr);
Начальная “настройка”
Для работы в Builder необходимо конвертировать libmysql.lib.
Для этого, нужно открыть консоль и набрать там это
C:\>”C:\Program Files\Borland\CBuilder6\Bin\coff2omf.exe”
-lib:st “C:\Program Files\Borland\CBuilder6\Lib\libmysql.lib”
“C:\Program Files\Borland\CBuilder6\Lib\libmysql_.lib”
Здесь “C:\Program Files\Borland\CBuilder6\Lib\libmysq l.lib” оригинальная библиотека,
а “C:\Program Files\Borland\CBuilder6\Lib\libmysq l_.lib” конвертированная
У каждого пути будут свои.
Также в папке с программой (или в “C:\Program Files\Borland\CBuilder6\Lib\”) должны быть libmysql_.lib, а для VS libmysql.dll.
Заголовочные файлы можно бросить в папку с программой или в “C:\Program Files\Borland\CBuilder6\Include\”.
Для VS “C:\Program Files\Microsoft Visual Studio Х.0\VC\include”. Где Х – версия VS.
В среде MS VC++ можно использовать библиотеку без конвертации, т.е. libmysql.lib.
Все заголовочные файлы могут быть в папке с программой, но тогда нужно подключать их локально.
Есть замечания для VC++ WinForms.
Так как типы String^ и char[] несовместимы, то для конвертирования из String^ в char[] можно использовать следующие функции (взято из MySQL++):
private: String^ ToUCS2(const char* utf8)
{
try
{
return gcnew String(utf8, 0, strlen(utf8), System::Text::Encoding::Default);
}
catch(…)
{
return “”;
}
}
private: Void ToUTF8(char* pcOut, int nOutLen, String^ sIn)
{
try
{
array^ bytes = System::Text::Encoding::Default->GetBytes(sIn);
nOutLen = Math::Min(nOutLen – 1, bytes->Length);
System::Runtime::InteropServices::Marshal::Copy(bytes, 0, IntPtr(pcOut), nOutLen);
pcOut[nOutLen] = ”;
}
catch (…)
{
pcOut[nOutLen] = ”;
}
}
Пример использования
const int buf = 512;
char host[buf];
ToUTF8(host, buf, hostText->Text); // Перевод из String^ в char[]
String ^tmp = ToUCS2(mysql_error(&mysql)); // Перевод из char* в String^
Вот все необходимое для работы:
libmysql_lib.rar 2.5 кб
libmysql.lib.rar 5.1 кб
LibMySQL.dll.rar 447 кб
include.rar 73.7 кб
Вывод в консоль
Вывод таблиц в консоли. Интерфейс правда не супер, но для практики думаю хватит.
В коде есть комментарии. Если будут вопросы задавайте здесь
#define __LCC__ // Объявляем директиву без которой программа не может работать. Можно конечно поключить windows.h, но это будет не красиво
#pragma comment(lib, “libmysql_.lib”) // подключаем библиотеку
#include // Заголовочный файл с описание функций
#include
#include
void mysql(const char query[])
{
MYSQL mysql; // Дескриптор соединения
MYSQL_ROW row; // Массив полей текущей строки
MYSQL_RES *res; // Дескриптор результирующей таблицы
char host[] = “localhost”; // хост
char user[] = “admin”; // пользователь
char passwd[] = “admin”; // пароль
char db[] = “library”; // название базы данных
int port = 0; // порт. Если порт у сервера MySQL не по умолчанию (3306), то нужно указывать конкретный номер порта
mysql_init(&mysql); // Инициализация
mysql_real_connect(&mysql, host, user, passwd, db, port, NULL, 0); // соединение
if (mysql_query(&mysql, query) > 0) // запорс. Если ошибок нет, то продолжаем работу
{
// Если была ошибка, …
printf(”%s”, mysql_error(&mysql)); // … вывдем ее
return; // и завершим работу
}
res = mysql_store_result(&mysql); // Берем результат,
int num_fields = mysql_num_fields(res); // количество полей
int num_rows = mysql_num_rows(res); // и количество строк.
for (int i = 0; i < num_fields; i++) // Выводим названия полей
{
field = mysql_fetch_field_direct(res, i); // Получение названия текущего поля
printf(”| %s |”, field->name);
}
printf(”\n”);
for (int i = 0; i < num_rows; i++) // Вывод таблицы
{
row = mysql_fetch_row(res); // получаем строку
for (int l = 0; l < num_fields; l++)
printf("| %s |", row[l]); // Выводим поля
printf(”\n”);
}
printf(”Count records = %d”, num_rows); // Вывод информации о количестве записей
mysql_free_result(res); // Очищаем результаты
mysql_close(&mysql); // Закрываем соединение
}
int main()
{
mysql(”SELECT * FROM t_mid_author”); // Запрос
getch(); // Ожидаем нажатие клавиши
return 0;
}
Графический интерфейс
Очередная статья по взаимодействию с СУБД MySQL из программы на С++
Мутим простейший интерфейс
Кидаем на форму:
TLabel (5 шт.). В свойство Caption пишем хост, порт и т.д.
TEdit (5 шт.). Названия TEdit’ов: hostText, userText, passText, dbText и portText.
TButton (2 шт.). В свойства Caption пишем “Пошел!” и “Закрыть”.
TMemo (1 шт.)
TStringGrid (1 шт.)
Подключаем заголовочные файлы, библиотеку и объявим одну константу
#define __LCC__
#include
#pragma comment(lib, “libmysql_.lib”) // Для Builder 6. Подробней см. в первой статье
#pragma comment(lib, “libmysql.lib”) // Для MS VC++
// Для других сред программирования не пробовал
const int buf = 512;
Обработчик кнопки “Закрыть” думаю понятен
А в обработчик копки “Пошел!” пишем следующее
/* Проверим что все данные были введены? в.ч. и сам запос (Memo1) */
if (hostText->Text.IsEmpty() || userText->Text.IsEmpty() ||
passText->Text.IsEmpty() || dbText->Text.IsEmpty() ||
portText->Text.IsEmpty() || Memo1->Text.IsEmpty())
{
MessageBox(this->Handle, “Не все поля заполнены!”, “Ошибка!”,
MB_OK | MB_ICONERROR);
return;
}
// Тут Вам все должно быть знакомо
MYSQL mysql;
MYSQL_ROW row;
MYSQL_RES *res;
MYSQL_FIELD *field;
/* Объявляем массивы для работы */
char host[buf];
char user[buf];
char passwd[buf];
char db[buf];
char query[buf];
int port = portText->Text.ToInt();
int num_fields = 0;
int num_rows = 0;
/* Инициализируем имя хоста, пользователя, пароль и БД */
strcpy(host, hostText->Text.c_str());
strcpy(user, userText->Text.c_str());
strcpy(db, dbText->Text.c_str());
strcpy(passwd, passText->Text.c_str());
strcpy(query, Memo1->Text.c_str()); //*/
mysql_init(&mysql);
if (!mysql_real_connect(&mysql, host, user, passwd, db, port, NULL, 0))
{ /* Пробуем подключиться, если кдето ошибка то сообщим об этом */
MessageBox(this->Handle, mysql_error(&mysql), “Error!”,
MB_OK | MB_ICONERROR);
return;
}
if (mysql_query(&mysql, query) > 0)
{ /* Пробуе выполнить запрос, если запрос не верен то сообщаем об ошибке,
Выведем ее и выходим
*/
MessageBox(this->Handle, mysql_error(&mysql), “Error!”,
MB_OK | MB_ICONERROR);
return;
}
// Получаем результат
res = mysql_store_result(&mysql);
/* Устанавливаем кол-во строк в таблице и сохраняем кол-во строк */
StringGrid1->RowCount = num_rows = mysql_num_rows(res);
/* Устанавливаем кол-во полей и сохраняем это кол-во столбцов */
StringGrid1->ColCount = num_fields = mysql_num_fields(res);
StringGrid1->FixedRows = 1; // Фиксируем первую строку.
for (int i = 0; i < num_fields; i++) // Выводим названия полей
{
field = mysql_fetch_field_direct(res, i);
StringGrid1->Cells[0] = field->name; // В первую строку, которую мы зафиксировали
}
for (int i = 1; i < num_rows; i++) // Вывод результата запроса
{
row = mysql_fetch_row(res); // Получаем строку
for (int l = 0; l Cells[l] = row[l]; // Выводим строку по ячейкам
}
mysql_free_result(res); // Освобождаем память
mysql_close(&mysql); // Закрываем соединение
Вот и все. пишем запрос и “Пошел!”.
Графический интерфейс. Специальный проект
Теперь напишем “специальный” клиент для базы данных Библиотека
Для начала создадим базу банных
# create.sql
# Создаем базу данных
CREATE database lib;
# Переключаемся на нее
use lib;
# Добавляем пользователя admin с паролем admin и связываем его с базой library
GRANT ALL ON lib.* TO ‘admin’@’%’ IDENTIFIED BY ‘admin’;
# Создаем таблицу книги
CREATE TABLE IF NOT EXISTS t_books
(
ID INT(6) UNSIGNED NOT NULL AUTO_INCREMENT,
Title CHAR(150) NOT NULL,
FIO CHAR(150) NOT NULL,
PRIMARY KEY(ID),
KEY(Title)
);
Интерфейс формы у меня получился такой.
Компоненты:
TLabel и TEdit . по 5 штук, как из предыдущей статьи
ListBox – 1 шт список книг
GroupBox – 1 шт. В нем 2 TLabel и 2 TEdit, название книги и автор
TButton – 5 шт. Их желательно обозвать как у меня (Удалить, Изменить, Добавить, Подключиться, Закрыть).
BitBtn – 1 шт кнопка обновление данных.
TTimer – 1 шт проверка содинения.
Все подробности на скрине.
Объявляем необходимые переменные и подключаем все что нужно…
#define __LCC__
#pragma comment(lib, “libmysql_.lib”)
#include
const int buf = 512; // Буфер
bool connected = false; // Есть соединение или нет
int *arrIDs = NULL; // Массив идентификаторов
int ID = 0; // Идентификатор текущей книги
MYSQL mysql; // Дескриптор соединения
MYSQL_RES *res; // Структура результатов
MYSQL_ROW row; // Массив строк результата
/* Объявляем массивы для работы */
char host[buf];
char user[buf];
char passwd[buf];
char db[buf];
int port = 0;
Настройки таймера:
Enabled := false; // Выключен
Interval := 5000; // 5 секунд
Код обработчика таймера:
if (mysql_ping(&mysql) > 0) // Если соединение разорвано…
{
connected = false; // ставим флаг дисконнекта
if (arrIDs) delete []arrIDs; // Очищаем массив
Timer1->Enabled = false; // … выключаем таймер
MessageBox(this->Handle, “Соединение с сервером потеряно”, “Ошибка!”,
MB_OK | MB_ICONERROR); // Выведем сообщение лоб этом
Button5->Click(); // Запустим повторное соединение
}
Далее обработчики кнопок
Код кнопки “Подключиться”
// Подключение
if (connected) return; // Если уже соединены, то выходим.
/* Проверим, что все данные были введены */
if (hostText->Text.IsEmpty() || userText->Text.IsEmpty() ||
passText->Text.IsEmpty() || dbText->Text.IsEmpty() ||
portText->Text.IsEmpty())
{
MessageBox(this->Handle, “Не все поля заполнены!”, “Ошибка!”,
MB_OK | MB_ICONERROR);
return;
}
/* Инициализируем имя хоста, пользователя, пароль, порт и БД */
strcpy(host, hostText->Text.c_str());
strcpy(user, userText->Text.c_str());
strcpy(db, dbText->Text.c_str());
strcpy(passwd, passText->Text.c_str());
port = portText->Text.ToInt();
mysql_init(&mysql); // Инициализация дескриптора
if (!mysql_real_connect(&mysql, host, user, passwd, db, port, NULL, 0))
{ /* Пробуем подключиться, если кде-то ошибка, то сообщим об этом */
MessageBox(this->Handle, mysql_error(&mysql), “Error!”,
MB_OK | MB_ICONERROR);
return;
}
connected = true; // Соединены
Timer1->Enabled = true; // Порверка соединения каждые 5 сек.
BitBtn1->Click(); // Обновить список
Код кнопки “Обновить” (BitBtn1)
// Обновление списка книг
if (!connected) return; // Если соединения нет, то выходим
/* Так как, мы знаем что нам нужно, то и запрос будет статическим.
Получим названия всех книг и идентификаторов, потом заполним ими ListBox
Сортируем в порядке возрастания по названию книги */
if (mysql_query(&mysql, “SELECT ID, Title FROM t_books ORDER BY Title”) > 0)
{ // Проверка на ошибки
MessageBox(this->Handle, mysql_error(&mysql), “Ошибка!”,
MB_OK | MB_ICONERROR);
return;
}
ListBox1->Clear(); // Очистка списка.
if (arrIDs) delete []arrIDs; // Очистка массива
// Заполняем структуру
res = mysql_store_result(&mysql);
// Получаем количество записей
int count = mysql_num_rows(res); // Получаем количество строк
arrIDs = new int[count]; // Инициализируем массив
for (int i = 0; i < count; i++)
{
// Полчаем строку
row = mysql_fetch_row(res);
// Заполняем массив
arrIDs = StrToInt(row[0]);
// Добавляем в ListBox название книги
ListBox1->Items->Add(row[1]);
}
mysql_free_result(res); // Освобождаем ресурсы
Графический интерфейс. Специальный проект. Продолжение
Код кнопки “Добавить”
// Добавление книг
// Если соединения нет или поля ввода пустые, то выходим
if (!connected && (bookText->Text.IsEmpty() || authorText->Text.IsEmpty()))
return;
// Формируем запрос на добавление книги
AnsiString tmp = “INSERT INTO t_books (ID, Title, FIO) VALUES (NULL,\
‘” + bookText->Text + “‘, ‘” + authorText->Text + “‘)”;
char query[buf]; // Переменная дла запроса
strcpy(query, tmp.c_str()); // Ковертируем в нужный формат.
// Запрос. Если ошибки есть, то выводим их и выходим из функции
if (mysql_query(&mysql, query) > 0)
{
MessageBox(this->Handle, mysql_error(&mysql), “Ошибка!”,
MB_OK | MB_ICONERROR);
return;
}
// Вывод сообщения о том, что все хорошо.
MessageBox(this->Handle, “Книга добавлена!”, “”, MB_OK | MB_ICONINFORMATION);
bookText->Clear();
authorText->Clear();
BitBtn1->Click(); // Обновим данные
Код кнопки “Изменить”
// Изменение книг
// Если соединения нет и книга не выбрана, то выходим
if (!connected && ID < 1) return;
AnsiString tmp = “UPDATE t_books SET Title = ‘” + bookText->Text +”‘,\
FIO = ‘” + authorText->Text + “‘ WHERE ID = ” + IntToStr(ID);
char query[buf];
strcpy(query, tmp.c_str());
if (mysql_query(&mysql, query) > 0)
{
MessageBox(this->Handle, mysql_error(&mysql), “Ошибка!”,
MB_OK | MB_ICONERROR);
return;
}
MessageBox(this->Handle, “Информация о книге обновлена”, “”,
MB_OK | MB_ICONINFORMATION);
Код кнопки “Удалить”
// Удаление книг
// Если соединения нет и нет выделенной книги для удаления, то выходим
if (!connected && ID < 1) return;
// Подтверждение удаления
if (MessageBox(this->Handle, “Удалить?”, “Удаление”,
MB_YESNO | MB_ICONQUESTION) != 6) return;
// Формируем запрос на удаление
AnsiString tmp = “DELETE FROM t_books WHERE ID = ” + IntToStr(ID);
char query[buf];
// Приводим его
strcpy(query, tmp.c_str());
// Выполняем.
if (mysql_query(&mysql, query) > 0)
{
MessageBox(this->Handle, mysql_error(&mysql), “Ошибка!”,
MB_OK | MB_ICONERROR);
return;
}
MessageBox(this->Handle, “Книга удалена”, “”, MB_OK | MB_ICONINFORMATION);
BitBtn1->Click();
Выбор книги из ListBox будет производится по двойному щелчку
// Выбор книги
// Если соединения нет, то выходим
if (!connected) return;
// Тут надеюсь все понятно. Если нет, смотрите “Обновление списка”
AnsiString tmp = “SELECT FIO FROM t_books WHERE ID = ”
+ IntToStr(arrIDs[ListBox1->ItemIndex]);
char query[buf];
strcpy(query, tmp.c_str());
if (mysql_query(&mysql, query) > 0)
{
MessageBox(this->Handle, mysql_error(&mysql), “Ошибка!”,
MB_OK | MB_ICONERROR);
return;
}
// Получаем результаты
res = mysql_store_result(&mysql);
row = mysql_fetch_row(res);
// Получаем идентификатор книги
ID = arrIDs[ListBox1->ItemIndex];
// Выводим название книги
bookText->Text = ListBox1->Items->Strings[ListBox1->ItemIndex];
// Выводим автора
authorText->Text = row[0];
mysql_free_result(res);
И, по закрытию программы написать
// Закрытие программы
// Если соединены, то разрываем соединение.
if (connected)
{
mysql_close(&mysql);
if (arrIDs) delete []arrIDs;
}
все исходники статьи и скрипт sql
Вся статья (сделана под форум). В аттаче.
mysql.txt
10th
Июл
Как работать с графикой на канве в среде Дельфи. Урок 7-8
Вот мы и подошли к заключительной серии наших уроков по графике в среде DELPHI. Сегодня рассмотрим практическое применение модуля <LoadObjectToBufferMod>, из нашего прошлого материала, на примере работы с движущимися графическими объектами…
Продолжение. Начало цикла смотрите в первом выпуске журнала…
Как работать с графикой на канве в среде Дельфи. Урок 7—8
Владимир Дегтярь
DeKot degvv@mail.ru
Проект с использованием дополнительного универсального модуля. Урок 7
Создайте и сохраните аналогично предыдущим урокам новый проект <Lesson 4>. В разделе uses введем модуль и в разделе var переменные для фона и звездолета ‘ship1 . Сам модуль должен находится в папке проекта (см. листинг 1):
Interface ЛИСТИНГ-1
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,LoadObjectToBufferMod, ExtCtrls;
var
Form1: TForm1;
xf,yf: integer; // координаты вывода общего буфера на форму
dyf: integer; // приращение изменения координаты yf по вертикали
xS1,yS1: integer; // координаты звездолета 'ship1'
dxS1,dyS1: integer; // приращение координат 'ship1' по гориз. и вертик.
xR,yR: integer; // координаты звездолетов 'ship4' ...'ship7'
dyR: integer; // приращение координат 'ship2 - 5'
N_kadrS1: byte; // номер изображения спрайта 'ship1'
implementation
В процедуре OnCreate() формы проведем инициализацию буферов и введем начальные данные для переменных (см. листинг 2):
procedure TForm1.FormCreate(Sender: TObject); ЛИСТИНГ-2
begin
// инициализация и загрузка битовых образов буфера фона и общего буфера
InitFon(1,2,'data/star1.bmp');
LoadFon(0,0,'data/star1.bmp');
LoadFon(0,540,'data/star2.bmp');
InitBuff;
// начальные значения переменных
xS1:= 250; yS1:= 1006;
dxS1:= 5; dyS1:= 2;
xf:= 0; yf:= -540; dyf:= 2;
end;
Процедура InitFon() вызывается из модуля ‘1‘ – количество используемых файлов фона по ширине, ‘2‘ – по высоте, ‘data/star1.bmp’ – по этому файлу определяем размеры основного буфера фона BufFon. Далее в процедурах loadFon() загружаем все файлы фона в BufFon. InitBuff() – инициализация буфера Buffer и загрузка в него фона. Движение графических объектов организовано в обработчике таймера (см. листинг 3):
procedure TForm1.Timer1Timer(Sender: TObject); ЛИСТИНГ-3
begin
// вывод движения 'ship1'
N_kadrS1:= InitSprite('data/ship1.bmp',2,1,1,N_kadrS1);
LoadBuff(xS1-dxS1,yS1+dyS1,xS1,yS1,0);
Form1.Canvas.Draw(xf,yf,Buffer);
yf:=yf+dyf; // приращения координат для движ. фона
if yf >= 0 then // если половина фона "прошла" через окно формы
begin
yf:= -540; yS1:= 1006; // возврат к начальным координатам
FreeBuff; // "перезагрузка" фона
end;
yS1:= yS1 - dyS1; // приращение для 'ship1',обратное приращению фона
end;
Функция InitSprite() возвращает номер следующего кадра (следующий рисунок спрайта ‘ship1’), который при следующем такте таймера опять передается в эту же функцию. В процедуре LoadBuff() предыдущее изображение спрайта перекрывается областью TRect фона и затем выводится новое изображение спрайта.
Когда весь фон проходит через окно формы в процедуре FreeBuff() фон снова перерисовывается в Buffer и возвращаются начальные координаты вывода Buffer на форму. Движение звездолета по горизонтали организовано в обработчике нажатия клавиш (см. листинг 4):
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; ЛИСТИНГ-4
Shift: TShiftState);
begin
case Key of
VK_Left: begin // клавиша "стрелка" - влево
if xS1 <= 0 then EXIT; { если звездолет у левого края формы
xS1 не изменяется}
dxS1:= -5; // движение звездолета влево
xS1:= xS1 + dxS1;
end;
VK_Right: begin // клавиша "стрелка" - вправо
if xS1 >= 630 then EXIT; { если звездолет у правого края формы
xS1 не изменяется}
dxS1:= 5; // движение звездолета вправо
xS1:= xS1 + dxS1;
end;
end;
end;
Запустите данный проект. Получим приложение аналогичное проекту <Lesson 3>. Сравнив коды этих проектов, заметно преимущество применения модуля. Сам проект приведен в папке <Lesson 4_1> (см. ресурсы к статье). Мы же продолжим создание проекта <Lesson 4> далее, введя еще один движущийся
графический объект (звездолет из файлов ‘ship4’ … ‘ship7’). Эти файлы имеют разный размер и разное количество рисунков).
В разделе var введем переменные для нового графического объекта:
var
N_kadrR: byte; // номер изображения спрайта 'ship4' ... 'ship7'
ns: byte = 5; // номер файла спрайтов
Nspr: byte = 2; // кол-во изображений в строке файла спрайтов
Добавим начальные значения новых этих переменных:
xs1 := 250; ys1 := 1006; // начальные значения переменных-
dxs1:= 4; dys1:= 2;
xf := 0; yf := -540; dyf:= 2;
xr := 50; yr:= 450;
dyr:= 3;
Randomize;
В обработчике таймера для нового объекта повторно вызовем методы вывода изображений спрайтов (см. листинг 6):
// вывод движения ‘ship4-ship7’ ЛИСТИНГ-6
N_kadrR:= InitSprite(‘data/ship’ + inttostr(ns) + ‘.bmp’, Nspr, 1, 1, N, N_kadR);
LoadBuf(xR, yR – dyR, xR, yR, 0);
Form1.canvas.draw(xf, yf, Buffer);
yR:= yR + dyR; // приращение для ‘ship4-ship7’
if yR >= yS1 + 100 then // проход ‘ship4-ship7’ через окно формы
begin
xR:= random(600);
yR:= yS1 – 600;
ns:= random(4) + 4;
if ns = 7 then Nspr:= 4
else Nspr:= 2
end
end
Здесь, в функции InitSprite() переменная Nspr означает количество изображений в файле спрайтов и определяется по значению ns – номера файла нового объекта, который в свою очередь определяется функцией random() после каждого прохождения новым графическим объектом окна формы. Причем, движение ‘ship1’ аналогично предыдущим примерам в процедуре FormKeyDown(). Полностью проект приведен в папке <Lesson 4> (см. ресурсы к статье).
Развитие проекта Lesson 4. Урок 8
Продолжим наш проект <Lesson 4>, превратив его в небольшой «шутер» или попросту в «стрелялку». Добавим также возможность стрельбы ракетами для ‘ship1’ по встречным звездолетам и эффект взрыва при попадании. Соответствующие графические файлы ракеты (bullet) и взрыва (explo) находятся в папке <data>. Сделаем это новым проектом с использованием проекта <Lesson 4>.
Итак, приступим… Создайте новую папку и присвойте ей имя <Lesson 5>. Скопируйте в нее все файлы
проекта <Lesson 4> и затем откройте проект в Delphi. Переименуем проект – File => Save Project as … и в диалоговом окне “Введите имя файла” – введите Lesson5 и сохраните проект под новым именем. Теперь снова откройте проект <Lesson 5> и добавьте переменные для новых объектов:
xb, yb: integer; // координаты ракеты
dyb: integer; // приращение координат ракеты
n_kadrb: byte; // номер изображения ракеты
n_kadre: byte; // номер изображения взрыва
vistrel,flag_s: boolean;
Флаги vistrel и flag_S типа Boolean необходимы для управления выводом изображения при выстреле ракетой. Случайный выбор звездолетов ‘ship4’ … ‘ship7’ выделим в отдельную процедуру, так как выбор в программе осуществляется несколько раз после прохождения звездолетом нижней границы окна формы, после попадания ракеты, а значит и уничтожения звездолета (см. листинг 7):
procedure randomship; ЛИСТИНГ-7
begin
xr:= random(600);
yr:= ys1 – 600;
ns:= random(4) + 4;
if ns = 7 then nspr:= 4
else nspr:= 2
end;
В процедуре FormKeyDown() добавим обработку клавиши ”пробел” – пуск ракеты (см. листинг 8):
vk_space: // пробел – выстрел ЛИСТИНГ-8
if not vistrel then begin
xb:= xs1 + 22;
yb:= ys1 – 30;
dyb:= 20;
vistrel:= true
end;
В обработчике таймера выводим движение ракеты при произведенном пуске – флаг (см. листинг 9):
if yr >= ys1 + 100 then ЛИСТИНГ-9
randomShip; // проход ship4-7 через окно формы
if (vistrel) and (not flag_s) then begin // если произведен выстрел, то
// вывод изображения ракеты
n_kadrb:= initSprite(‘data/bullet.bmp’, 1, 1, 1, n_kadrb);
loadBuff(xb, yb + dyb, yb, 0);
yb:= yb – dyb;
if yb <= ys1 – 510 then begin // ракета вышла за пределы окна формы
vistrel:= false;
freeBuff // ПЕРЕЗАГРУЗКА фона
end
end;
и также вводим “эффект взрыва” при попадании ракеты в цель (см. листинг 10):
if (yb <= yr) and ЛИСТИНГ-10
(xb >= xr) and
(xb <= xr+100) and
(vistrel) then begin
flag_s:= true;
dyb:= 0;
n_kadre:= initSprite(‘data/explo.bmp’, 8, 1, 1, n_kadre);
loadBuff(xb-54, yb + 2, xb-54, 0);
form1.canvas.draw(0, yf, buffer);
if n_kadre = 0 then begin // конец эффекта взрыва
vistrel:= false;
flag_s:= false;
freeBuff;
randomShip
end
end;
И собственно то, чего добивались. Скомпилируем и запустим наш проект (см. рисунок):
Заключение
На этом мы закончим рассмотрение простейших приемов работы с движущимися графическими объектами. Предложу еще самостоятельно разобрать метод вывода на канву текстовой графики. Добавьте в код последнего приложения счетчики количества пусков ракет и попаданий в цель (например: count1, count2) и используя свойства Font(Size,Style,Color) и метод TextOut(X,Y, IntToStr(count..) введите статистику в приложении. Будем считать это домашним заданием…
Рассматриваемые в данной статье проекты полностью приведены в ресурсах к статье на http://www.programmersforum.ru в разделе «Журнал клуба программистов. Четвертый выпуск» (папка Lesson5*).
* Комментарий автора
Перед запуском в среде Дельфи скопируйте в папку с проектом папку data с графическими файлами.
Статья из четвертого выпуска журнала “ПРОграммист”.
Скачать этот номер можно по ссылке.
Ознакомиться со всеми номерами журнала.
Обсудить на форуме — Как работать с графикой на канве в среде Дельфи. Урок 7-8
1st
Июл
Введение в Sсheme. Часть 1
Краткое описание функционального языка программирования Sсheme. Рассчитано на программистов, имеющих навыки программирования в императивном стиле.
by Utkin
В 1970 году Гай Стил (Guy L. Steele) и Джеральд Сассман (Gerald Jay Sussman) из Массачусетского технологического института начали работу на новым языком программирования. Тогда Джеральд Сассман уже имел ученую степень, а его помощник Гай Стил являлся студентом данного института. В 1975 году работы были закончены и получили свое дальнейшее воплощение в принципах работы СБИС (в 1978 году) и CAD-системах. Идеи, положенные в эти работы, позже использовались для конструирования элементов космических аппаратов и проведения экспериментов в космосе (с использованием элементов искусственного интеллекта). В 1984 году большое количество версий в разных институтах заставило разработчиков выработать единый стандарт (при участии Массачусетского технологического института и университета Индианы). Такие серьезные сферы дали хороший старт языку, однако его распространение сдерживалось низкой скоростью интерпретации. В 90-х годах прошлого века были разработаны новые технологии компиляции, что позволило повысить эффективность Scheme путем компиляции в байт-код и машинные коды (хотя экспериментальные компиляторы со Scheme существовали и ранее). На данный момент производительность Scheme в целом выше, чем у Java (учитывая, что языки разных стилей, то такое сравнение условно).
Scheme («скиим»)
это функциональный язык программирования, диалект Лиспа. Особенности: разумный минимализм, интерактивность, однородность синтаксиса (лисповые списки), высокий уровень абстрагирования, возможность писать программы оторванные от конкретной платформы, «строгий» язык (требует придерживаться определенного стиля), позволяет использовать разные парадигмы программирования и много чего еще особенного.
В дальнейшем будем рассматривать язык на примере PLT Scheme. Данная среда выбрана не случайно (имеются десятки других). Она бесплатна, имеет широкое распространение, большой набор библиотек и позволяет компилировать программы в exe-файлы. Взять можно отсюда: http://www.plt-scheme.org/ IDE в PLT Scheme называется DrScheme, имеет минималистический вид, однако содержит практически все, что необходимо для работы программиста. Редактор имеет два окна – первое для записи программы, второе информационное и окно интерактивного режима – команды языка, введенные в него, исполняются немедленно.
Также в комплекте имеется MrEd – система для создания переносимого графического интерфейса пользователя. Далее будет следовать краткое описание языка, настолько, насколько это возможно в рамках статьи. Все примеры, приведенные ниже можно сразу опробовать в окне интерактивного режима.
Синтаксические элементы и что еще обязательно нужно знать
Прежде чем, начать описание синтаксических элементов следует обратить внимание на некоторые вещи. Все вычисления, в процедурах, аргументах всегда осуществляются слева направо. Язык имеет поддержку типов, однако, типизация скрытая (динамический контроль типов). Объекты (включая процедуры – сама программа также может являться объектом данных) имеют неразрушаемую природу, однако переполнение памяти не возникает за счет того, что объекты могут быть перезаписаны, в случае, если больше не будут участвовать в вычислительном процессе. На данный момент все реализации Scheme используют хвостовую рекурсию, это позволяет выделять на выполнение рекурсионных вызовов ограниченный размер памяти. Такие вызовы позволяют не использовать специализированные операции цикла (могут быть получены за счет автоматического преобразования хвостовой рекурсии в итерации). Все функции являются самостоятельными объектами (могут создаваться динамически, сохранены в структурах данных, возвращены как результаты функций и т.д.). Примером таких языков являются Common Lisp и ML. Параметры процедур всегда передаются по значению (в противовес Haskell, который позволяет откладывать вычисление параметра, до тех пор, пока он не потребуется явно).
Модель арифметических вычислений создана таким образом, чтобы максимально не зависеть от конкретных аппаратных платформ. В Scheme каждое целое число является рациональным, каждое рациональное число является реальным, а каждое реальное к тому же является еще и комплексным. Поэтому разница между различными типами чисел, возведенная в ранг абсолютизма в императивных языках, здесь не проявляется.
Программа является списком, элементы списка отделяются друг от друга пробелом, сам список заключен в скобки. Операции представляется списком, в котором символ операции (который, в сущности, является именем функции) всегда занимает первое место в списке (префиксная форма или польская нотация):
Пример.
(+ 2 2) Результатом будет 4.
(+ 2 (* 2 2)) Результатом будет 6.
Особенностью такой записи является тот факт, что операция может быть применена к неограниченному количеству входящих параметров, можно написать и (+ 2 2 2 2 2 2 2 2 2 2 2) (сравните 2 + 2). Это приводит к тому, что программы, написанные на Scheme, могут являться данными для других программ написанных на Scheme. Для функциональных языков программирования является традицией писать интерпретаторы/компиляторы с них на этих же языках программирования.
PLT Scheme не делает разницы между идентификаторами, набранными в разных регистрах. В тоже время правила образования идентификаторов может различаться для разных версий и реализаций языка, однако есть общее требование – идентификатор не должен начинаться с цифры. Примеры идентификаторов:
lambda
q
list->vector
soup
+
V17a
<=?
a34kTMNs
the-word-recursion-has-many-meanings
Для идентификаторов разрешено использовать символы. Можно использовать, например, такой идентификатор ——>, однако по соглашению фрагмент -> делит идентификатор на две части и говорит о преобразовании типа, например list->vector для функции может трактоваться как преобразование списка в вектор (элементы которого являются элементами списка). Помимо пробелов и переходов на новую строку допускается также использовать символы табуляции, все эти элементы обычно используются для улучшения восприятия текста программы и в качестве разделителей лексем языка.
Одним из важных понятий языка является понятие представления объектов. Если в программе, это всего лишь поток символов, то внутренне объект может представляться совершенно иным способом. Далее, представление объектов не обязательно уникально, так число 28 может быть представлено как #e28.000 или #x1c. Список (1 2) можно представить как (01 02) или (1. (2. ())) Здесь () является пустым списком. Однако, список (+ 1 2) не является эквивалентом числа 3, это список, состоящий из трех элементов (результат вычисления которого является числом 3). С одной стороны, это может путать на начальном этапе знакомства с языком, но с другой это источник его мощи, данные могут являться программой, программа может являться данными.
Переменные перед использованием должны быть объявлены: (define x 28) В данном случае мы определяем переменную х со значением 28. В Дальнейшем допускается неоднократное переопределение (например, (define x 38)).
Точка с запятой (;) служит для обозначения комментария, который продолжается до конца строки и является неисполняемым оператором языка. Поэтому комментарии посреди лексемы языка не допускаются.
Небольшой пример:
;;; процедура FACT вычисляет факториал
;;; из неотрицательного целого числа.
(define fact
(lambda (n)
(if (= n 0)
1 ;Base case: return 1
(* n (fact (- n 1))))))
Вызов факториала (fact 100) даст результат:
933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565182862536979208272
23758251185210916864000000000000000000000000
Это говорит о том, что строгий диапазон для чисел не установлен и ограничен только объемом памяти и Вашим временем (кстати, считает за вполне приемлемое время), на операции над ними (справедливо для целых чисел).
Существует соглашение, по которому предикаты (элементы, возвращающие значения Истина/Ложь) должны именоваться с вопросительным знаком на конце.
Ну и еще что нужно знать об основных синтаксических элементах: *. + — могут использоваться в идентификаторах (например, предыдущем примере мы могли бы обозначить идентификатор функции как fact+ или fact+1).
Одинарная кавычка (‘) используется для абсолютного значения, которое вычислять не требуется (допускается второе обозначение данной функции — quote). Например, имеется элемент списка (х) и имеется функция с аналогичным именем, чтобы в списке х не вычислялся, используется данная функция.
Чтобы было понятней, объявим такую функцию:
(define (prostoX x) ( quote x))
Вызовем ее
(prostoX 100) Результат будет х, а не 100
(quote a) Результат a
(quote #(a b c)) Результат #(a b c)
(quote (+ 1 2)) Результат (+ 1 2), а не 3
'(quote a) Результат (quote a)
Можно заставить Scheme вычислять частично, для этого используется запятая:
`(list ,(+ 1 2) 4) Результат (list 3 4), то есть выражение перед запятой (+1 2) было вычислено.
Более сложный пример
(let ((name 'a)) `(list ,name ',name)) Результат (list a (quote a))
Числовые, символьные, строковые и булевые константы «квотировать» не допускается.
Символ двойной кавычки (”) используется для ограничения строковых констант.
Символ обратного слеша (\) используется в строковых константах.
[ ] { } | Левые и правые квадратные скобки, изогнутые скобки и вертикальный штрих зарезервированы для возможных будущих расширений языка.
Символ шарп (#) используется для множества целей в зависимости от символа, который немедленно следует за этим:
- #t #f — обозначение булевых констант;
- # \ — вводит символьную константу;
- #( — вводит векторную константу;
- #e #i #b #o #d #x — используются для обозначения чисел.
Объявление процедур:
(lambda <formals> <body>)
<Formals> — список формальных параметров
<Body> — последовательность из одного или более выражений.
Лямбда-выражение считается процедурой, не имеющей имени. Результат(ы) последнего выражения в теле будет возвращен как результат(ы) вызова процедуры.
(lambda (x) (+ x x)) Есть описание процедуры без имени, имеющий один входящий параметр и возвращающий удвоенное значение переменной х.
((lambda (x) (+ x x)) 4) Результат 8 (последнее выражение, передано в процедуру)
Определим такую функцию
(define reverse-subtract
(lambda (x y) (- y x)))
Ее вызов с параметрами (reverse-subtract 7 10) Результат результат 3 (для последнего выражения происходит обмен параметров в списке). Таким образом, сначала определяется безымянная процедура с двумя входящими параметрами х и y. Ее задача вычитать из y x. Далее, ей просто присваивается имя reverse-subtract, которую мы и вызвали с параметрами 7 и 10. х получает значение 7, у соответственно 10, затем мы вычитаем из y x тело (- у х), то есть (- 10 7). Результатом будет 3.
Определим следующую функцию:
(define add4
(let ((x 4))
(lambda (y) (+ x y))))
Ее вызов с параметрами (add4 6) Результат 10. Обратите внимание – внутри функции определяется локальная переменная х, имеющая значение 4. Данная переменная существует только в рамках add4.
Lambda удобно использовать для образования хвостовой рекурсии – один из важнейших элементов функционального программирования. Для императивного программирования хвостовую рекурсию можно выразить через цикл. Также lambda в связке с define можно рассматривать как аналог механизма интерфейсов ООП.
Допускается и короткая форма функций, без использования процедур lambda: (define (имя параметры) (тело_функции)). Определим функцию возведения в квадрат: (define (square x) (* x x)). Вызовем ее (square 10), в результате получим 100. Вызовем ее еще раз (Square 10.5), в результате получим 110.25. Код может следовать сразу друг за другом без всяких приведений типов и никаких фокусов с параметризацией здесь не требуется совершенно. Хотелось бы на этом заострить внимание: то, что в последнее время привносится как новая веха ООП – параметризация, существует в функциональном программировании чуть ли не с момента его возникновения. Однако в функциональном программировании оно представлено на совершенно ином уровне.
Еще один момент, касающийся идентификаторов – в PLT Scheme все идентификаторы могут содержать дополнительные символы. Это значит, что вместо square можно смело определить функцию КВАДРАТ, и вообще можно переписать практически все ключевые элементы языка, используя национальный алфавит.
Условные выражения представляются в следующих формах:
(if <тест> <следствие> <альтернативный_вариант>)
Здесь все просто и аналогично другим языкам программирования:
(if (> 3 2) 'да 'нет) Результат да (обратите внимание на одинарные кавычки)
(if (> 2 3) 'да 'нет) Результат нет
(if (> 3 2)
(-3 2)
(+ 3 2)) Результат 1 (3 больше 2? Если да, то выполним (- 3 2), то есть получим в итоге 1).
Можно использовать также cond:
(cond ((> 3 2) 'больше)
((<3 2) 'меньше)) Результат больше
(cond ((> 3 3) 'больше)
((<3 3) 'меньше)
(else 'равно)) Результат равно
Общая форма: (cond выражение1 выражение2 … выражениеN), где выражение состоит из ((условие) (действие)). Ветвь else будет выполнена в случае, если ни одно из предыдущих условий не было выполнено. Cond аналогична множественному использованию if (изначально конструкции if не было, и все условия обсчитывали через cond). В качестве задания: попробуйте выразить if через использование cond.
Давайте рассмотрим такой фрагмент программы:
(define (PAIR (a b)
(lambda (msg)
(case msg ; конструкция case для каждого значения msg
; выполняет соответствующее действие
((first) a) ; если сообщение – символ first, возвратить a
((second) b))))
В зависимости от msg будет возвращено соответствующее значение, то есть фактически полностью соответствует case из императивных языков (таких как С++ или Дельфи). Case аналогично cond поддерживает else – ветвь которая будет обязательно выполнена в случае, если ни одна из предыдущих выполнена не была.
Можно составлять сложные условия за счет грамотного использования and и or:
(and (= 2 2) (> 2 1)) Результат #t (истина). 2=2 и 2>1? Истина
(and (= 2 2) (< 2 1)) Результат #f (ложь). 2=2 и 2<1? Ложь
(and 1 2 'c '(f g)) Результат (f g) (список не вычисляется из-за одинарных кавычек)
(and) Результат #t
(and (< 2 2) (= 2 2)) Результат #f
Выражения оцениваются слева направо до первого ложного утверждения. Если все утверждения верны, то возвращается последний входящий параметр функции (у нас это пример со списком (f g)). Если and не имеет входящих утверждений, будет возращено #t (истина).
(or (= 2 2) (> 2 1)) Результат #t
(or (= 2 2) (< 2 1)) Результат #t
(or #f #f #f) Результат #f
Выражения оцениваются слева направо до первого истинного утверждения (оставшиеся не рассматриваются). Если все выражения ложны, будет возвращено последнее из них. Теперь немного о присваивании переменных. Вообще-то, в чистом функциональном программировании переменных вообще существовать не должно, но подавляющее большинство функциональных языков программирования признают пользу существования переменных и вводят различные механизмы для их использования. Присваивание в PLT Scheme осуществляется таким образом:
(define x 2)
(set! x 4)
(+ x 1)
Результат 5. Ничто нам не мешает переопределить переменную x через define, но все же необходимо помнить, что define не эквивалентно set! (хотя в нашем случае и приводит к одному результату). Разница заключается в том, что define связывает идентификатор переменной с ее новым значением (предыдущее значение переменной будет уничтожено в результате сборки мусора), а set! присваивает значение уже существующей. Вообще, следует учитывать, что имя переменной и ее значения связаны не напрямую между собой. В частности, это проявляется в том, что переменная может существовать по имени, без значения (проявляется, например, при операции quote). При этом нет точных рамок между именем переменной и именем функции. И через define имя переменной можно переопределить как имя функции.
Теперь рассмотрим более подробно конструкцию let. Собственно у нее ее два родственника let* и letrec с тем же синтаксисом (let ((определение_переменной1) (определение_переменнойN)) (тело)). Разница в них только в уровнях видимости связанных ими переменных. Это позволяет строить блочные структуры на манер принятый в структурном программировании. Короткая форма такова: (let (определение переменной) выражение), где определение переменной выглядит все в том же списочном стиле (переменная значение).
(lambda <formals> <body>)
<Formals> — список формальных параметров
<Body> — последовательность из одного или более выражений.
Лямбда-выражение считается процедурой, не имеющей имени. Результат(ы) последнего выражения в теле будет возвращен как результат(ы) вызова процедуры.
(lambda (x) (+ x x)) Есть описание процедуры без имени, имеющий один входящий параметр и возвращающий удвоенное значение переменной х.
((lambda (x) (+ x x)) 4) Результат 8 (последнее выражение, передано в процедуру)
Определим такую функцию
(define reverse-subtract
(lambda (x y) (- y x)))
Ее вызов с параметрами (reverse-subtract 7 10) Результат результат 3 (для последнего выражения происходит обмен параметров в списке). Таким образом, сначала определяется безымянная процедура с двумя входящими параметрами х и y. Ее задача вычитать из y x. Далее, ей просто присваивается имя reverse-subtract, которую мы и вызвали с параметрами 7 и 10. х получает значение 7, у соответственно 10, затем мы вычитаем из y x тело (- у х), то есть (- 10 7). Результатом будет 3.
Определим следующую функцию:
(define add4
(let ((x 4))
(lambda (y) (+ x y))))
Ее вызов с параметрами (add4 6) Результат 10. Обратите внимание – внутри функции определяется локальная переменная х, имеющая значение 4. Данная переменная существует только в рамках add4.
Lambda удобно использовать для образования хвостовой рекурсии – один из важнейших элементов функционального программирования. Для императивного программирования хвостовую рекурсию можно выразить через цикл. Также lambda в связке с define можно рассматривать как аналог механизма интерфейсов ООП.
Допускается и короткая форма функций, без использования процедур lambda: (define (имя параметры) (тело_функции)). Определим функцию возведения в квадрат: (define (square x) (* x x)). Вызовем ее (square 10), в результате получим 100. Вызовем ее еще раз (Square 10.5), в результате получим 110.25. Код может следовать сразу друг за другом без всяких приведений типов и никаких фокусов с параметризацией здесь не требуется совершенно. Хотелось бы на этом заострить внимание: то, что в последнее время привносится как новая веха ООП – параметризация, существует в функциональном программировании чуть ли не с момента его возникновения. Однако в функциональном программировании оно представлено на совершенно ином уровне.
Еще один момент, касающийся идентификаторов – в PLT Scheme все идентификаторы могут содержать дополнительные символы. Это значит, что вместо square можно смело определить функцию КВАДРАТ, и вообще можно переписать практически все ключевые элементы языка, используя национальный алфавит.
Условные выражения представляются в следующих формах:
(if <тест> <следствие> <альтернативный_вариант>)
Здесь все просто и аналогично другим языкам программирования:
(if (> 3 2) 'да 'нет) Результат да (обратите внимание на одинарные кавычки)
(if (> 2 3) 'да 'нет) Результат нет
(if (> 3 2)
(-3 2)
(+ 3 2)) Результат 1 (3 больше 2? Если да, то выполним (- 3 2), то есть получим в итоге 1).
Можно использовать также cond:
(cond ((> 3 2) 'больше)
((<3 2) 'меньше)) Результат больше
(cond ((> 3 3) 'больше)
((<3 3) 'меньше)
(else 'равно)) Результат равно
Общая форма: (cond выражение1 выражение2 … выражениеN), где выражение состоит из ((условие) (действие)). Ветвь else будет выполнена в случае, если ни одно из предыдущих условий не было выполнено. Cond аналогична множественному использованию if (изначально конструкции if не было, и все условия обсчитывали через cond). В качестве задания: попробуйте выразить if через использование cond.
Давайте рассмотрим такой фрагмент программы:
(define (PAIR (a b)
(lambda (msg)
(case msg ; конструкция case для каждого значения msg
; выполняет соответствующее действие
((first) a) ; если сообщение – символ first, возвратить a
((second) b))))
В зависимости от msg будет возвращено соответствующее значение, то есть фактически полностью соответствует case из императивных языков (таких как С++ или Дельфи). Case аналогично cond поддерживает else – ветвь которая будет обязательно выполнена в случае, если ни одна из предыдущих выполнена не была.
Можно составлять сложные условия за счет грамотного использования and и or:
(and (= 2 2) (> 2 1)) Результат #t (истина). 2=2 и 2>1? Истина
(and (= 2 2) (< 2 1)) Результат #f (ложь). 2=2 и 2<1? Ложь
(and 1 2 'c '(f g)) Результат (f g) (список не вычисляется из-за одинарных кавычек)
(and) Результат #t
(and (< 2 2) (= 2 2)) Результат #f
Выражения оцениваются слева направо до первого ложного утверждения. Если все утверждения верны, то возвращается последний входящий параметр функции (у нас это пример со списком (f g)). Если and не имеет входящих утверждений, будет возращено #t (истина).
(or (= 2 2) (> 2 1)) Результат #t
(or (= 2 2) (< 2 1)) Результат #t
(or #f #f #f) Результат #f
Выражения оцениваются слева направо до первого истинного утверждения (оставшиеся не рассматриваются). Если все выражения ложны, будет возвращено последнее из них. Теперь немного о присваивании переменных. Вообще-то, в чистом функциональном программировании переменных вообще существовать не должно, но подавляющее большинство функциональных языков программирования признают пользу существования переменных и вводят различные механизмы для их использования. Присваивание в PLT Scheme осуществляется таким образом:
(define x 2)
(set! x 4)
(+ x 1)[/c
(let ((x 2)) (* x 2)) Результат 4. Создаем переменную х=2, умножаем на 2 и передаем значение. Сам х при выходе из данного выражения будет разрушен. Let позволяет создавать сколько угодно переменных, действие которых будет распространяться исключительно на тело определения (при этом в рамках одного определения не допускается использовать несколько переменных с одинаковыми идентификаторами).
(let ((x 2) (y 3))
(* x y)) Результат 6
(let ((x 2) (y 3))
(let ((x 7)
(z (+ x y)))
(* z x)))
Результат 35. В первом блоке определим х=2, у=3, переходим к исполнению тела let. Создадим второй х со значением 7, затем определяем еще одну переменную второго блока z, которая будет равна x+y (число 5) первого блока (поскольку х второго блока имеет силу только в теле второго блока, но не на этапе определения переменных этого же блока). Приступаем к исполнению тела второго блока – умножаем 5 (в данном случае, переменная z) на 7 (переменная х второго блока). Результат 35.
Как видно из примера, допускается неограниченная глубина вложенности let, внутри каждого определения допускается свободное повторение идентификаторов по отношению к другим let. В Scheme let часто используется как аналог программных скобок ({-}, begin-end) императивных языков программирования. Как правило, let используется чаще родственных ей let* и letrec.
Рассмотрим пример с let*
(let ((x 2) (y 3))
(let* ((x 7)
(z (+ x y)))
(* z x)))
Результат 70. В первом блоке определим х=2 и у=3. Далее конструкция let* определяет переменную х=7, действие которой распространяется на все определения переменных справа и на тело let*, таким образом, z=7 (новый х)+3 (у из первого блока)=10. Приступаем к выполнению тела let* - z (число 10) умножаем на х блока let* (число 7). Результат 70.
Что касается letrec, ее особенность в том, что переменная получает свое значение не мгновенно, а в момент, когда она будет необходима (соответственно в теле переменная может получать разные значения в зависимости от того выражения, каким она должна инициализироваться). Это удобно в случае многочисленных рекурсионных вызовов.
(letrec ((even?
(lambda (n)
(if (zero? n)
#t
(odd? (- n 1)))))
(odd?
(lambda (n)
(if (zero? n)
#f
(even? (- n 1))))))
(even? 88))
Данный пример определяет четность числа (#t, если четное). Как видно из примера, even? может вычисляться в зависимости от входящего числа через рекурсию. Еще один момент – если Вы заметили, то even? это должна быть переменная или функция, имеющая булевый тип, а 88 есть число (в рамках Scheme – не важно какое, считайте, что все числа есть комплексные величины). Здесь видна работа letrec, для тела вычисляется even? определенная не как переменная, а как функция (через lambda). Letrec часто используют для получения значения переменной через рекурсионные вызовы. Эта конструкция имеет один подводный камень – поскольку все параметры передаются по значению, должны существовать однозначные условия для получения результата. Никаких двусмысленностей в letrec быть не должно (могут возникнуть, в частности, при комбинациях родственников letrec, let и let*).
Одной из важных особенностью функциональных языков программирования является концепция функций – иными словами процедуры в обычном понимании в функциональном программировании быть не должно. Выражения и функции получают параметры и передают результат. Однако на практике часто возникают ситуации, когда результат либо не требуется, либо не определен, либо просто не имеет смысла. Самым простым примером является вывод информации на экран. Какой результат может в данном случае получить функция вывода сообщения на экран? В тоже время отсутствие результата (либо возникновение ситуации, когда результат работы функции определить невозможно) может привести программу к краху – следующая функция не сможет получить входящий параметр (либо не сможет определить что это за параметр). Явление, при котором функция проявляет себя каким-либо образом помимо передачи результата, называется побочными эффектами (явлениями). В чистом функциональном программировании побочных явлений быть не должно в принципе (например, побочным эффектом является присваивание переменной какого-либо значения, создание объектов без их непосредственной передачи (так себя ведет define), ввод и вывод данных и т.д.). Специально для решения подобных проблем существует конструкция (begin (выражение1) … (выражениеN)), все вычисления в которой осуществляются последовательно и строго слева направо. Особенностью данной конструкции является результат – он всегда будет возвращен для последнего выражения.
Пример:
(define x 0)
(begin (set! x 5)
(+ x 1)) Результат 6 (5+1)
(begin (display "4 plus 1 equals ")
(display (+ 4 1))) Результат 5 (4+1), проигнорировав результаты вывода на экран посредством display (при этом сама строка будет напечатана). Begin также удобно использовать при создании законченных блоков, содержащих несколько выражений (например в If или cond) как замена let, с той лишь разницей, что для данного блока новых переменных не создается (соответственно и не разрушаются).
Рассмотрим итерации. Хотя хвостовая рекурсия может быть преобразована в цикл (при правильном ее составлении), циклы в Scheme существуют также и в общей форме. Один из них:
(do ((<переменная1> <инициализатор_переменной1> <шаг_приращения1>)
...)
(<тест> <выражение> ...)
<операторы_языка> ...)
Сразу стоит обратить внимание, на то, что приращивать можно не одну переменную и условие для выхода из цикла тоже может быть не одно.
(do ((vec (make-vector 5))
(i 0 (+ i 1)))
((= i 5) vec)
(vector-set! vec i i)) Результат вектор #(0 1 2 3 4), здесь создается вектор и переменная i
(let ((x '(1 3 5 7 9)))
(do ((x x (cdr x))
(sum 0 (+ sum (car x))))
((null? x) sum)))
Результат 25, проводиться подсчет списка. Здесь создается две переменные х (обратите внимание, переменная цикла инициализируется переменной, объявленной через let) и sum.
Также если рассмотреть конструкцию let более внимательно, то можно обнаружить, что с помощью нее также можно формировать итеративные процессы. Для этого определяемую переменную можно представить как функцию (через ее инициализатор) и вызывать ее для каждого упоминания в теле let (через условные операторы cond или if).
Несмотря на то, что все параметры в функции передаются по значению, есть возможность отложить вычисление параметров до момента их действительного использования. Плюсов от этого как минимум два: первое – это увеличение скорости вычислений (при грамотном использовании) и второе – решение задач, которые нельзя вычислить обычным способом. Увеличение скорости возможно, в случае, если в функцию передается несколько параметров и часть из них напрямую влияет на логику работы функции. Допустим, первый параметр функции однозначно говорит о том, что результат ее работы известен (например, частный случай), и в дальнейшем ничего вычислять в рамках данной функции не требуется. Если передавать все параметры по значению, то в момент входа в функцию, должны быть известны все параметры, значит, все выражения должны быть вычислены. Даже если как в нашем абстрактном примере эти параметры для получения результата не понадобятся. Можно конечно и посчитать, но не следует забывать, что в функциональном программировании рекурсия обычное дело, а это либо чистая рекурсия, либо итеративный процесс, и то и другое медленно. Поэтому прирост скорости может достигать больших величин (а до недавнего времени в функциональном программировании это был очень актуальный вопрос). Что касается дополнительных возможностей, продолжим рассмотрение все того же примера. Что если выполнение одного или нескольких параметров невозможно в конкретный момент вычислительного процесса? Допустим, в выражении происходит деление на нуль. Решить такую задачу обычным способом невозможно. Однако, есть математические выкладки, говорящие о том, что функция имеет решение (потому что ее результат в данном случае зависит от другого параметра, например общий коэффициент, который в данный момент равен нулю, тогда результат функции также будет равен нулю независимо от остальных параметров). Если отложить на некоторое время вычисление остальных параметров и работать с тем, что влияет на логику вычислений, то функция способна вернуть корректный результат, даже если вычисление остальных параметров может привести к фатальной ошибке.
В Scheme отложить вычисление можно через (delay (выражение)). Выражение вычислено не будет до конструкции force (синтаксис аналогичен). Force требует немедленного вычисления выражения (обычно используется в связке с delay).
Как видите, между delay и force может находиться произвольное число выражений, лишь бы p по-прежнему оставалось актуальным (то есть в нашем случае force должно наступить в рамках let, иначе после p перестанет существовать). На самом деле пользоваться delay и force легко, главное чтобы каждое обращение к не вычисленному параметру сопровождалось force.
Подобно многим языкам программирования Scheme поддерживает макроопределения (макросы), позволяющие образовывать новые типы выражений. Можно переопределить большинство стандартных конструкций, с помощью уже известной нам (define новое_имя наименование_конструкции). При этом конструкция под старым именем тоже сохраняется. Возьмем наш факториал:
Теперь fact можно вызывать как под старым именем, так и под именем mmm. Scheme обладает мощными средствами для образования новых возможностей языка на основе макроопределения ключевых слов (не просто синтаксическая замена), но в связи со сложностью и большим объемом материала мы далее макросы рассматривать не будем. Если есть желание, то описание работы с макросами можно найти в большой и подробной справке по PLT Scheme (справочная система называется Help Desk).
И традиционная строчка кода:
Список сокращенных обозначений и терминов, использованных в статье
СБИС – сверхбольшие интегральные схемы.
CAD - Computer Aided Design - компьютерная поддержка конструирования, предполагает объемное и плоское геометрическое моделирование, инженерный анализ на расчётных моделях высокого уровня, оценку проектных решений, получение чертежей.
Java – язык программирования.
Цикл – разновидность управляющей конструкции в высокоуровневых языках программирования, предназначенная для организации многократного исполнения набора инструкций. Также циклом может называться любая многократно исполняемая последовательность инструкций, организованная любым способом (например, с помощью условного перехода).
Продолжение следует ...
Обсудить на форуме – Введение в Sсheme. Часть 1
Статья из четвёртого выпуска журнала "ПРОграммист".
26th
Июн
Как работать с графикой на канве в среде Дельфи. Урок 3-4
Одним из основных методов получения эффекта движущегося объекта является принцип уничтожения объекта на текущем месте и вывод его на новом с заданным приращением координат dX и dY. Однако прямолинейное применение такого метода на практике приводит к неприятному эффекту “мерцания” объекта во время движения. Причина этого в том, что при проведении этих операций непосредственно на канве объекта, где происходит движение, мы получаем двойную перерисовку участка канвы. Такая двойная смена цвета пикселей и является причиной. Сказанное наглядно видно на рисунке (см. рис.1), во время каждого такта движения происходит два процесса рисования: сначала старое изображение «затирается» фоном, а затем рисуется новое изображение движущегося объекта на новом месте…
Владимир Дегтярь
Урок 3
Создание движущихся объектов (еще немного теории)
Рис. 1. Демонстрация эффекта «мерцания»
Избежать данной проблемы можно, объединив обе операции «стирания» изображения и вывода нового в одну. Суть этого состоит в том, чтобы на канве объекта, невидимого на экране, вывести стирающий участок фона, затем вывести изображение движущегося объекта на новом месте и уже после этих операций вывести полученную комбинацию изображения на видимую часть канвы экрана. Для этого создаем дополнительный объект со свойством канвы (лучше всего подходит дополнительный буфер типа TBitMap), на котором и проводим предварительные операции. Размер такого дополнительного буфера определяется в зависимости от величины смещения движущегося объекта.
Возможны два варианта: когда старое и новое положение объекта имеют общую область (перекрывают друг друга) и не перекрывают (см. рис.2):
Рис. 2. Перекрытие объектов
Размер буфера в первом случае, как видно из рисунка 2 по каждой из координат равен размеру объекта плюс двойное приращение координат по соответствующим координатам (берется абсолютная величина). Теперь достаточно вывести в буфер область фона, соответствующего текущему положению объекта (старое положение) и наложить поверху изображение объекта, разместив его в буфере со смещением dX и dY (опять же по абсолютной величине) относительно нулевых координат буфера. После вывода изображения всего буфера на экран изображение объекта исчезнет и появится в новом положении.
Во втором случае, когда старое и новое изображения не имеют общей области также можно применить указанный метод. Но при этом, судя по рисунку 3, значительно увеличивается размер дополнительного буфера:
Рис. 3. Случай отсутствия перекрытия изображений
Можно уменьшить размер дополнительного буфера, применив немного другой подход (см. рис.4):
Рис. 4. Принцип уменьшения буфера
Здесь размер дополнительного буфера значительно меньше, чем на рисунке 3, но вывод изображения
объекта происходит не в центр буфера, а в один из четырех секторов. Перед заполнением буфера необходимо определить величину и знак следующего приращения dx и dy. Затем заполнить буфер фоном текущего положения объекта и вывести изображение объекта с учетом следующего приращения координат:
Если
dx > 0 и dy <= 0, то bx = dx и by = 0;
dx <= 0 и dy > 0 , то bx = 0 и by = dy;
dx <= 0 и dy <= 0, то bx = 0 и by = 0;
Значения bx и by присваиваются , естественно по абсолютным величинам dx и dy. Вот код программы для первого случая (см. листинг 1):
ЛИСТИНГ-1
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
procedure FormActivate(Sender: TObject);
procedure FormPaint(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
end;
var
Form1: TForm1;
{ битовые образы рисунка, фона и дополнит. буфера, соответственно }
Pic, Fon, Buf : TBitMap;
// области вывода фона
RectFon, RectBuf : TRect;
// текущие координаты
x,y: integer;
// приращения координат
dx, dy: integer;
// размеры дополнит.рисунка и буфера
W,H,WB,HB: integer;
implementation
{$R *.dfm}
procedure TForm1.FormActivate(Sender: TObject);
begin // инициализация TBitMap и TRect
// загружаем рисунок фона
Fon:= TBitMap.Create;
Fon.LoadFromFile(‘fon.bmp’);
{ загружаем рисунок объекта, который будет двигаться}
Pic:= TBitMap.Create;
Pic.LoadFromFile(‘picture.bmp’);
// задаем прозрачность объекту
Pic.Transparent:= true;
Pic.TransparentColor:= Pic.Canvas.Pixels[1,1];
{ определяем и устанавливаем размеры дополнит. буфера }
W:= Pic.Width;
H:= Pic.Height;
Buf:= TBitMap.Create;
Buf.Width:= W + 2*abs(dx);
Buf.Height:= H + 2*abs(dy);
WB:= Buf.Width;
HB:= Buf.Height;
// назначаем соответствие палитр цветов
Buf.Palette:= Fon.Palette;
Buf.Canvas.CopyMode:= cmSrcCopy;
{ определяем область в дополнит. Буфере для загрузки участка фона }
RectBuf:= Bounds(0,0,WB,HB);
// инициализация координат
x:=300; y:=150;
end;
procedure TForm1.FormPaint(Sender: TObject);
begin // выводим на экран фон и объект
Form1.Canvas.Draw(0,0,Fon);
Form1.Canvas.Draw(x,y,Pic);
end;
{ перемещение объекта на один шаг происходит после каждого нажатия
соответствующей клавиши}
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
case Key of
37: begin dx:= -3; dy:= 0; end;
38: begin dx:= 0; dy:= -3; end;
39: begin dx:= 3; dy:= 0; end;
40: begin dx:= 0; dy:= 3; end;
end;
{ определяем область фона, которую надо запомнить }
RectFon:= Bounds(x+dx,y+dy,WB,HB);
// запомним фон в буфере
Buf.Canvas.CopyRect(RectBuf,Fon.Canvas,RectFon);
// наложим в буфере на фон рисунок с прозрачным фоном
Buf.Canvas.Draw(abs(dx),abs(dy),Pic);
// выводим на форму в новой позиции
x:=x+dx; y:=y+dy;
Form1.Canvas.Draw(x,y,Buf);
end;
end.
А это для второго случая (см. листинг 2):
ЛИСТИНГ-2
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs;
type
TForm1 = class(TForm)
procedure FormActivate(Sender: TObject);
procedure FormPaint(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
end;
var
Form1: TForm1;
{ битовые образы рисунка, фона и дополнит.буфера, соответственно }
Pic, Fon, Buf : TBitMap;
// области вывода фона
RectFon ,RectBuf : TRect;
// текущие координаты
x,y: integer;
// приращения координат
dx, dy: integer;
{ смещение изображения объекта
в дополнительном буфере }
bx, by: integer;
// размеры дополнит.рисунка и буфера
W,H,WB,HB: integer;
implementation
{$R *.dfm}
procedure TForm1.FormActivate(Sender: TObject);
begin // инициализация TBitMap и TRect
// загружаем рисунок фона
Fon:= TBitMap.Create;
Fon.LoadFromFile(‘fon.bmp’);
{ загружаем рисунок объекта, который будет двигаться}
Pic:= TBitMap.Create;
Pic.LoadFromFile(‘picture.bmp’);
// задаем прозрачность объекту
Pic.Transparent:= true;
Pic.TransparentColor:= Pic.Canvas.Pixels[1,1];
{ определяем и устанавливаем размеры дополнит. буфера }
W:= Pic.Width;
H:= Pic.Height;
Buf:= TBitMap.Create;
Buf.Width:= W + abs(dx);
Buf.Height:= H + abs(dy);
WB:= Buf.Width;
HB:= Buf.Height;
// назначаем соответствие палитр цветов
Buf.Palette:= Fon.Palette;
Buf.Canvas.CopyMode:= cmSrcCopy;
{ определяем область в дополнит. Буфере для загрузки участка фона }
RectBuf:= Bounds(0,0,WB,HB);
// инициализация координат
x:=300; y:=150;
end;
procedure TForm1.FormPaint(Sender: TObject);
begin // выводим на экран фон и объект
Form1.Canvas.Draw(0,0,Fon);
Form1.Canvas.Draw(x,y,Pic);
end;
{ перемещение объекта на один шаг происходит после каждого нажатия
соответствующей клавиши}
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
case Key of
37: begin dx:= -3; dy:= 0; end;
38: begin dx:= 0; dy:= -3; end;
39: begin dx:= 3; dy:= 0; end;
40: begin dx:= 0; dy:= 3; end;
end;
if (dx>0) and (dy>0) then begin bx:=abs(dx); by:=abs(dy); end;
if (dx>0) and (dy<=0) then begin bx:=abs(dx); by:=0; end;
if (dx<=0) and (dy>0) then begin bx:=0; by:=abs(dy); end;
if (dx<=0) and (dy<=0) then begin bx:=0; by:=0; end;
{ определяем область фона, которую
надо запомнить }
RectFon:= Bounds(x+dx,y+dy,WB,HB);
// запомним фон в буфере
Buf.Canvas.CopyRect(RectBuf,Fon.Canvas,RectFon);
// наложим в буфере на фон рисунок с прозрачным фоном
Buf.Canvas.Draw(bx,by,Pic);
// выводим на форму в новой позиции
x:=x+dx; y:=y+dy;
Form1.Canvas.Draw(x,y,Buf);
end;
end.
В принципе обе эти программы можно объединить, введя процедуру проверки на совпадение старой и новой областей изображения. Но это уже другая тема.
В данных примерах подразумевалось, что движущийся объект состоит всего из одного изображения ‘picture.bmp’. Но на практике, обычно таких изображений несколько. Наиболее часто изображения хранятся в файле в виде набора спрайтов. Вот здесь есть один небольшой подводный камень. Обычно выбор необходимого спрайта производится процедурой CopyRect() из буфера типа TBitMap, куда предварительно загружены изображения всех спрайтов. Но, при таком методе вывода изображения в дополнительный буфер с участком фона нельзя присвоить изображению свойство прозрачности.
Для обхода этого препятствия следует ввести еще один дополнительный буфер типа TBitMap, куда методом CopyRect() загрузить изображение нужного спрайта, а затем уже, присвоив свойство прозрачности, методом Canvas.Draw вывести изображение на канву дополнительного буфера с участком фона.
Следует отметить, что для создания «эффекта» движения можно оставить изображение графического объекта неподвижным, а двигать сам фон. При этом размер фона должен быть больше размера окна формы. Также можно применять одновременно или поочередно сдвиг объекта и сдвиг фона.
Урок 4
Проект с движением звездолета.
Аналогично Уроку 2 создадим новый проект и сохраним его под именем Lesson2. Параметры формы Form1 также установим аналогично Уроку 2, изменив только заголовок Form1 на «Урок по графике № 2».
Для получения движения объекта применим метод, когда сам объект (звездолет) неподвижен на форме, а “двигается” сам фон. Фон создадим в два раза больше по высоте размера окна формы. Для этого используем файлы ‘star1.bmp’ и ‘star2.bmp’. Нам также понадобится еще один дополнительный буфер BufFonDop. Кроме этого введем еще переменные – координаты вывода на форму фона и звездолета, а также приращения к координатам (см. листинг 3 ):
ЛИСТИНГ-3
Form1: TForm1;
BufFon,BufFonDop,BufSpr,BufPic,Buffer: TBitMap;
xS1,yS1: integer; // координаты звездолета ‘ship1’
xf,yf: integer; // координаты вывода общего буфера на форму
dyf: integer = 2; // приращение изменения координаты yf по вертикали
dxS1,dyS1: integer; // приращение координат звездолета по гориз. и вертик.
ns: byte; // номер спрайта
{Здесь-же вводим начальные данные для переменных}
xs1:= 250; ys1:= 1006; // начальные значения переменных-
dxs1:= 4; dys1:= 2;
xf:= 0; yf:= -540; dyf:= 2;
{ Инициализацию буферов проведем в процедуре OnCreate() формы }
procedure TForm1.FormCreate(Sender: TObject);
begin
BufFon:= TBitMap.Create;
BufFonDop:= TBitMap.Create;
BufSpr:= TBitMap.Create;
BufPic:= TBitMap.Create;
Buffer:= TBitMap.Create;
BufFonDop.LoadFromFile(‘data/star1.bmp’); // загрузка 1-го рисунка фона из файла
BufSpr.LoadFromFile(‘data/ship1.bmp’); // загрузка спрайтов из файла
BufFon.Width:= BufFonDop.Width; // размеры общего буфера фона
BufFon.Height:= (BufFonDop.Height) * 2; { по высоте = двум Height одного
рисунка фона}
BufFon.Canvas.Draw(0,0,BufFonDop); // заносим 1-й рисунок фона
BufFonDop.LoadFromFile(‘data/star2.bmp’); // загрузка 2-го рисунка фона из файла
BufFon.Canvas.Draw(0,540,BufFonDop); // заносим 2-й рисунок фона
Buffer.Width:= BufFon.Width; // размеры общего буфера = размерам буфера фона
Buffer.Height:= BufFon.Height;
BufPic.Width:= round((BufSpr.Width) / 2); // размеры буфера рисунка одного спрайта
BufPic.Height:= BufSpr.Height;
В процедуре DrawShip1(i) добавлено условие прозрачности фона рисунка звездолета (см. листинг 4):
ЛИСТИНГ-4
begin
BufPic.Canvas.CopyRect(bounds(0, 0, BufPic.Width, BufPic.Height),
BufSpr.Canvas,bounds( i * 66, 0, BufPic.Width,
BufPic.Height));
BufPic.transparent:= true;
BufPic.transparentcolor:= BufPic.canvas.pixels[1, 1]
end;
«Эффект» движения организован в обработчике таймера. В каждом такте таймера 50 мсек:
- смещаем координату вывода общего буфера yf на dyf вниз (+ 2)
- координату yS1 вывода звездолета смещаем на dyS1 вверх (- 2). В этом случае изображение звездолета будет неизменным в координатах формы, а двигаться будет только фон
- в общий буфер (Buffer) выводим фон (при этом «уничтожается» изображение звездолета на старом месте)
- выводим в общий буфер на фон новое изображение звездолета со смещением dyS1
- выводим общий буфер на форму
В процедуре DrawShip1(ns) через переменную ns, принимающую попеременно значения 0 или 1, выводятся поочередно спрайты изображения звездолета (см. листинг 5):
ЛИСТИНГ-5
begin
ns:= ns + 1; if ns > 1 then ns:= 0; // смена спрайтов
DrawShip1(ns);
yf:= yf + dyf; if yf > 0 then begin yf:= -540; yS1:= 1006; end; // приращение yf
yS1:= yS1 — dyS1; // приращениие звездолета обратно приращению фона
Buffer.Canvas.Draw(0,0,BufFon); // выводим фон в общий буфер
Buffer.Canvas.Draw(xS1,yS1,BufPic); // выводим рисунок спрайта поверх фона
Form1.Canvas.Draw(xf,yf,Buffer); // выводим все на форму
end;
//Движение звездолета по горизонтали в пределах окна формы организовано
//в обработчике нажатия клавиши OnKeyDown
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
case Key of
37: begin // клавиша "стрелка" — влево
if xS1 <= 0 then EXIT; { если звездолет у левого края формы
xS1 не изменяется}
xS1:= xS1 — dxS1; // движение звездолета влево
end;
39: begin // клавиша "стрелка" — вправо
if xS1 >= 630 then EXIT; { если звездолет у правого края формы
xS1 не изменяется}
xS1:= xS1 + dxS1; // движение звездолета вправо
end;
end;
end;
Запустим проект на компиляцию и взглянем на результаты нашего труда (см. рис.5):
Рис. 5. Движение звездолета по-горизонтали
Заключение:
В следующих уроках добавим еще несколько движущихся объектов в проект.
Рассматриваемые в данной статье проекты полностью приведены в ресурсах к статье на http://www.programmersforum.ru в разделе «Журнал клуба программистов. Второй выпуск» (папка Lesson2*).
Обсудить на форуме – Как работать с графикой на канве в среде Дельфи. Урок 3-4
Статья из второго выпуска журнала «ПРОграммист».
23rd
Июн
Персональный язык программирования
Статья имеет целью познакомить читателя с одним представителем огромной армии неизвестных языков программирования. Этот язык я придумал сам для себя, он ни на какой язык не похож и этим может быть интересен.
Виктор Кон
by http://kohnvict.narod.ru
Про языки программирования написано очень много статей и философских размышлений. Их много и есть необходимость в их классификации. Главный аспект – это то, что языки могут быть многоуровневыми. Есть ассемблер, есть базовые языки более высокого уровня: Basic, C++, Java и много других. С их помощью можно написать компиляторы и интерпретаторы других языков еще более высокого уровня и так далее. Второй аспект – разделение языков на компилируемые и интерпретируемые. Первые используются для создания независимых и конечных программ, например программы решения квадратного уравнения. Вторые являются текстовыми инструкциями для одной единственной программы – интерпретатора. Бывают и пересечения, когда программа частично компилируется в промежуточный код, который затем интерпретируется. Ну и, наконец, третий аспект – структура самой программы, тут три уровня. Вся программа одним куском, программа с процедурами, программа с объектами. Какой язык и какая программа удобнее для конкретного пользователя зависит от свойств самого пользователя.
Вместо введения
Я хочу рассказать о себе. Я пользователь в том смысле, что пишу программы для своей научной работы. Почти все программы моделируют те или другие физические процессы и часто используются только один раз. Условно конечно, много раз в процессе отладки до тех пор пока не получен достоверный результат. После того, как результат получен – программа уже не нужна. Опять неточно, нужен ее код, чтобы развивать его дальше и учесть более сложные процессы, но на каждом этапе программа пишется на результат, который получается конечным числом ее запусков. Как писать такие программы? Можно конечно одним куском и на ассемблере. Но это будет огромный труд с очень низким коэффициентом полезного действия. Можно писать процедуры и компилировать их в новые сборки программ. Но и это не оптимально. В каждой программе будет очень много одного и того же кода, который будет повторяться, засоряя носители информации. Это не так важно сейчас, но было важно раньше, когда компьютеры были маломощные. Сама практика подсказала, что наиболее оптимальным путем является программа-интерпретатор, выполняющая команды одну за другой, а наиболее удобным и простым для выполнения конкретных работ является командный язык программирования с возможностью группировать код в процедуры.
Так получилось, что я пришел к этому выводу в далеком 1991-м году, когда программ еще было мало, все работали в операционной системе ДОС, которая мало что умела и о программах типа Mathematica, IGOR-pro и им аналогичных даже и не мечтали. Моя идея состояла в том, что нужно сделать интерпретатор языка программирования высокого уровня, который имел бы команды всех уровней, начиная от прерываний ассемблера и кончая запуском редактора текстов или программы построения графика одной командой. В то же время структура языка должна быть такой, чтобы четкие и конечные операции можно было сделать написав несколько строк текста. Я потратил два года работы и сделал себе такую программу. Работая в ДОС, она имела графические возможности, необычный редактор текстов с уникальными возможностями, хорошие возможности по интерфейсу и очень мне помогала в работе. Самый главный плюс такой программы состоял в том, что я мог очень быстро получить ответ на многие сложные вопросы. А быстрота получения ответа иногда имеет решающее значение. Часто просто нет времени на написание и отладку огромного кода, ответ нужен, как говорят, еще вчера. И я мог это делать.
Минусы у программы тоже были – отсутствие шрифтов. Конечно, я создал свой масштабируемый фонт, который можно было использовать, но это было не очень быстро и не очень красиво. Когда появилась Виндовс, а компьютеры стали более мощными, необходимость в интерпретаторе отпала. Самый главный минус был в том, что программа могла работать только в ДОС. Виндовс не разрешал многие операции, которые она могла делать, да и просто ее не запускал по соображениям безопасности. Я снова рассыпал программу на отдельные куски, но и они постепенно становились все менее привлекательными.
Прошло время – появились КПК (карманные персональные компютеры), это чудо на ладони с большими возможностями по части поиграть музыку или показать кино. Но они не умели делать вычисления, не было программ. А уж о том, чтобы программировать прямо на КПК не было и речи. Я снова вспомнил про свой язык программирования и свой интерпретатор. Код интерпретатора был написан на фортране, но для КПК нужно было писать на чем-то другом. Так получилось, что я в то время (это уже был 2004 год) начал работать на Java, поэтому выбор был сделан. Я просто перетранслировал вручную весь код с фортрана на Java, даже порой уже не понимая, что этот код конкретно делает. И все получилось – код заработал. Конечно, пришлось отказаться от прерываний, немного по-другому написать редактор текстов, по-другому сделать интерфейс. Но сам язык программирования и его интерпретация почти не изменились. Эта программа до сих пор работает у меня на КПК, который я как раз и купил в начале 2004 года. Он до сих пор работает с той же самой батареей. Это отдельная тема и я не буду ее развивать здесь.
Важно, что мне снова понравилось работать на своем языке, и я сделал клон этой программы также и для ПК, то есть настольного компьютера и ноутбука. Эта программа имеет больше возможностей и настолько удобна, по крайней мере, для меня, что я уже не мыслю себе другой жизни. Вот вчера мне понадобилось выделить из pdf файла книги в 150 страниц всего 4 страницы и записать в другой pdf файл. На моем яыке это делается в несколько строк, но и их писать не надо – они уже написаны. А вычислить, скажем, таблицу значений функции Бесселя и построить график можно с помощью программы в одну строчку.
Но это только примеры. Реально программа умеет очень много всего, практически все, что лично мне нужно и самое главное – если чего-то нехватает, то просто дописываем интерпретатор и создаем новую команду. Сложность только в том, что, как и у любого интерпретируемого языка, команд очень много. Но они хорошо структурированы и есть описание с хорошей навигацией. Так что перечитав описание можно все легко вспомнить. Наизусть я свой язык не помню.
Вместо рекламы
Цель этой статьи – просто познакомить уважаемую публику с тем, что у меня есть. С самого начала я не стремился делать коммерческий продукт, но в какой-то момент работы над программой я подготовил вариант, который содержит только команды общего назначения, могущие представлять интерес для всех. Этот вариант я выложил в интернет на отдельный сайт http://vkacl.narod.ru, а также разместил в каталоги программ. Неожиданно программа стала популярной на сайте FreeSoft, где ее скачали уже более 40 тыс. раз. Были и письма, из которых я понял, что кое-кто действительно научился пользоваться. У программы есть несколько вариантов. Первый, основной, содержит интерпретатор языка, который я назвал ACL (advanced command language), среду разработки, включающую встроенное описание языка, и примеры готовых программ. Эта программа называется vkACL.jar. Как любая Java программа, она сама запускается через виртуальную машину Java или JRE, которую предварительно надо установить на компьютер. Дистрибутив JRE можно бесплатно скачать хоть с родного сайта, хоть с разных зеркальных сайтов, например [1]. Скриншот программы можно посмотреть вот по этому адресу [2].
Есть также вариант проигрывателя с окном, то есть это программа, которую можно настроить таким образом, что она будет показывать меню и выполнять (интерпретировать) одну ACL программу. Такой вариант не содержит помощи и выглядит как конечная программа на одну функцию. При этом jar-файл может иметь любое имя. Программу в таком варианте я использую для разработки конкретных программ, с которыми могли бы работать мои коллеги, не умеющие программировать. Эти программы выглядят как обычные Java программы. Есть еще и второй вариант проигрывателя, который совсем не имеет головного окна. Такая программа либо выполняет свою работу, не общаясь с пользователем совсем (так работали старые программы в эпоху до ПК), либо средства общения с пользователем написаны в самой ACL программе. Иногда такой вариант бывает оптимальным.
Наконец, есть вариант программы в виде Java-апплета, который сразу работает в интернете и запускается вот по этому адресу [3]. Этот вариант сделан совсем недавно и я его сейчас развиваю. Если у кого есть быстрый интернет, то можно сразу пользоваться программой с любого компьютера. К сожалению Java-апплет не может работать с файлами пользователя, но я организовал ввод и вывод данных через редактор текстов. Любую информацию, в том числе и картинки, можно ввести в программу и вывести из программы копированием текста через буфер обмена (клавиши Ctrl-A, Ctrl-C, Ctrl-V), а готовые картинки можно скопировать с экрана, используя любую программу с функцией snapshot, то есть фотографирования части экрана в файл. Таких программ много, подробности можно прочитать в моей статье http://kohnvict.narod.ru/Data/advice.htm. Java — апплет содержит несколько готовых программ общего назначения, в частности таблицу Менделеева в виде книги, а также мои научные программы для коллег. Также каждый может скопировать и выполнить свою программу.
Что такое ACL?
Невозможно в двух словах объяснить, как работает космический корабль, слишком там много деталей. Такая же проблема с языком программирования, который рассчитан на то, чтобы делать все на свете. Когда я начинал, то не имел никакого опыта, да тогда вообще все было в очень ограниченном ассортименте. Поэтому я ничего не читал, а все выдумал сам. Первый момент – никаких строк и меток. Символ конца строки ничего не значит. В эпоху ДОС это было революционно. Программа пишется таким образом, что как раз комментарии ничем не выделяются, они пишутся свободно, а вот сами команды языка программирования начинаются с символа (#). Этот символ для интерпретатора служит знаком, что пора начинать работу. После этого знака пишется имя команды. Принцип написания всех имен следующий: можно писать сколько угодно букв, но слитно (без пробелов). Реально команды различаются максимум по трех первым буквам, а часто даже просто по одной букве. Я не люблю много писать, поэтому мне не нравится даже Java, хотя я считаю ее лучшим языком программирования. Команды из одной или двух букв абстрактны, но это дело привычки. Любой язык хорош, который знаешь. Вот у меня фамилия Кон. И это намного удобнее, чем Константинополев. Итак, читается слово до пробела и выбирается три первые буквы или меньше, если их меньше.
Что дальше?
Каждая команда – это отдельный блок программы, который знает, умеет и выполняет свою работу. Но часто ему нужны данные, конкретизирующие задачу. Входные данные раделены на два типа. Первый тип – это целочисленные параметры, которые имеют имена. Сделано это исключительно для более удобного понимания программы. Параметры универсальные и используются разными командами, причем один раз определенный параметр сохраняет свое значение до следующего определения. Определять параметры можно после каждой команды в поле, заключенном в квадратные скобки. Есть дополнительно к целочисленным несколько текстовых параметров. А всю остальную информацию надо записывать как аргументы сразу после квадратных скобок. Аргументы задаются как во многих командах многих интерпретаторов, хоть в ДОС (батч- файлы), хоть в компиляторах и прочих программах с командной строкой. Потому язык и называется командным.
Самое интересное, что больше ничего и нет. Точнее нет грамматики языка. Одни команды. И вся грамматика тоже реализована через команды. Одна команда запускает огромную готовую программу, другая реализует грамматику, но структурно они неразличимы. Возникает законный вопрос – а как делать вычисления? А где циклы, условия, логика? Ведь языки программирования, как и разговорные языки – как ни пиши, а предмет то один и тот же. Все верно. Но у меня «быстрый» язык, он кстати в первой редакции назывался quick. Для него главное – запускать готовые блоки, а все остальное вспомогательное. Поэтому все остальное реализовано по минимуму. Переменные все реальные и их имена короткие – одной буквой от (a) до (z), затем от (A) до (Z) и еще три символа ($), (%). Можно писать одну букву и одну цифру, например (z1=x3;). И все – больше переменных нет, а зачем много? Массивы – есть один целый массив i(), один реальный массив r() и один массив символов (уникодов) t(). Индексы в круглых скобках. Каждый из массивов имеет фиксированный (большой) размер, а все команды работают с частями этих массивов, причем есть специальные параметры (b [begin]) и (le [length]), которые как раз и выделяют индексы массивов, используемые в программе. То есть фактически вместо имени массива используется его начальный индекс в большом массиве. А зачем имена? Проблема выбора для некоторых людей – непреодолимое препятствие. А так все массивы плотно и динамически упаковываются в одном. Нет строк. Массив символов и строка – одно и то же. Более того, символы и числа – тоже одно и то же. Элементы символьного массива могут участвовать в вычислениях.
Как делаются вычисления? А очень просто. Открывается команда вычислений #cal или #c или просто #. Это единственная команда, которая может иметь нулевое имя. И все формулы вычислений являются ее аргументами. Она вычисляет и переопределяет переменные и массивы, это ее работа. В вычислениях можно использовать имена функций, кроме стандартных есть еще функции Бессела и интегралы Френеля. Пример: # z=sin(3.14*20/180); X1=5/exp(-2.7)+r(25); #% А что с циклами? Есть простой цикл, релизованный командой #rep N (repeat) с одним аргументом (числом повторений). Все команды после этой выполняются до команды #end , которая возвращает сканирование программы в самую первую команду после #rep и так ровно N раз. Все индексы и переменные должны переопределяться внутри цикла вручную. А условные циклы? Вот тут интереснее. Все сделано с помощью всего одной команды – и условия, и условные циклы. Зачем много? Опять проклятая проблема выбора. Просто есть еще одна команда цикла, она называется #case N.
Эта команда так же точно, выполняет все команды до команды #end. Но при одном условии, что ее аргумент совпадает со значением переменной (&). Если не совпадает, то вообще ничего не делается. В данном случае команда #end снова проверяет это же самое условие. Если не изменить переменную (&), то цикл будет делаться бесконечно. Поэтому внутри тела цикла ее надо изменить как раз тогда, когда надо. Таким способом можно сделать выбор: # &=2; #case 1 . . . #end | #case 2 . . . #end | #case 3 . . . #end | Вот в этом коде выполнится только многоточие после #case 2. Знак вертикальной черты (|) после #end через пробел означает автоматическую смену переменной (&) на значение 12345. Таким образом, тело цикла выполнится один раз. Перебор вариантов мы делаем. Но нам нужна логика. Программа должна уметь мыслить.
На это я могу авторитетно заявить. Не умеет компьютер мыслить. Все что он умеет – это сравнивать два числа на больше и меньше, а еще точнее – определять знак числа. И одной этой операции ему хватает, чтобы обыгрывать человека в шахматы. И не только в шахматы, а вообще делать разумную работу. Я не стал напрягаться и делать как у всех. Я просто ввел две новые операции < и > в математические вычисления. Результатом операции a<b является минимум из двух значений a и b. Аналогично a>b вычисляет максимум. И все, больше ничего не надо. Это позволяет писать полноценные программы, реализующие любую логику при переборе вариантов.
Заключение
Есть еще много интересных нюансов в языке и много такого, что позволяет ему иметь неограниченные возможности по созданию очень большой программы, включающей разные куски кода, написанные в разных файлах где угодно, в том числе и на серверах в интернете. Есть готовая научная графика, возможность работы с картинками, zip- архивами, делать любые операции с файлами и текстом. Но об этом можно очень долго писать. Моя задача состояла в том, чтобы заинтересовать читателя обратиться на указанные выше сайты за более подробной информацией. Я надеюсь, что эту задачу я выполнил. Напоследок скажу чего нет. Нет работы с базами данных стандартного вида, хотя аппарат работы с небольшими базами данных есть. Я не работаю с базами данных и мне это не нужно. До сих пор я не освоил аппарат работы с xml файлами, каюсь. Но и это мне не нужно. Персональный язык программирования не обязан быть универсальным.
Ресурсы
- Дистрибутив JRE http://java.sun.com или http://www.java.com/ru/download/index.jsp
- Скриншот программы http://img-fotki.yandex.ru/get/3904/kohnvict.12/0_24d12_9b5a6cf9_orig
- Вариант программы в виде Java-апплета http://vkacl.narod.ru/applets/00/vkACLa.htm
Обсудить на форуме – Персональный язык программирования
Статья из первого выпуска журнала «ПРОграммист».
23rd
GAMEDEV на Delphi. Делаем змейку
Вэтой статье я подробно рассмотрю создание игры «Змейка» с использованием ООП (объектно-ориентированного программирования). Делать игру мы будем средствами GDI на Delphi 7 (хотя должна подойти и любая другая версия). Для полного осознания того, о чем говорится в статье желательно иметь хоть какой-нибудь опыт программирования, знать что такое Сanvas и TBitmap, что такое классы и в чем заключаются их особенности.
Вадим Буренков
Вообще, при создании любой игры можно выделить три момента:
- Создание идеи, примерный план реализации. При желании разработчиком пишется «диздок» (дизайн-документ — это документ, который описывает то, что вы хотите создать).
- Алгоритмизация. То есть мы представляем план строения игры, какие типы данных будут использоваться. Обдумываются алгоритмы работы и взаимодействия элементов игры.
- Все выше мы записываем в виде кода, то есть программируем.
В принципе, игра не использует наследования, поэтому можно было обойтись и без классов, но мне захотелось сделать так.
Краткий экскурс…
При создании любой игры нужно придерживаться определенного строения кода.
Каждая игра имеет следующие секции:
- инициализация (тут происходит настройка окна для рендера (вывода изображения), загрузка всех игровых ресурсов и уровня)
- обработка (всех игровых объектов, их взаимодействие друг с другом. Эта секция в отличие от инициализации и деинициализации выполняется постоянно, причем скорость выполнения измеряется в миллисекундах и обычно** равна 10-100 мс)
- деинициализация (выполняется при завершении работы приложения и служит для очищения занятой приложением памяти)
Обратите внимание! Компонент TTimer имеет низкую точность. Ставить на нем низкие значения (менее 30 мс) не имеет смысла.
Сделаем основу для игры
Откройте Delphi, создайте новое приложение. Сразу хочу заметить, что именам всех переменных, окон, файлов, и.т.п. надо давать осмысленные имена. Конечно, кто-то скажет что и так запомнит что такое Form1 и Label2, но когда пишется проект не на 10 а на 10000 строк и используется не одна, а пятнадцать форм… Также не надо мелочиться на комментарии, поскольку смысл определенного кода через месяц простоя не поймет и тот, кто его написал, не говоря уже о том – что вам может понадобиться сторонняя помощь.
Назовем форму MainForm, модуль GameMain, а проект SnakeProject. Поскольку эта игра простая и небольшая, то я буду использовать один модуль, но в остальных случаях рекомендуется использовать свой модуль для отдельной игровой части. Создадим два события формы: OnCreate и OnDestroy которые и будут инициализатором и деинициализатором игры. За обработку игры будет отвечать компонент-таймер TTimer (поместите его на форму из вкладки System, назовем его MainTimer), а именно его событие – OnTimer.
Я не буду делать игру очень гибкой в плане игрового поля (его размеры нельзя будет менять. Поле будет 16х12 клеток, но о них будет написано ниже, основные параметры будут константами. Игровое же поле 640х480 по центру экрана без возможности перемещения (без синей рамки). Объявим константы:
SCR_WIDTH=640;
SCR_HEIGHT=480;
Можно не выставлять параметры окна в среде разработки, а написать их в коде (событие OnCreate):
MainForm.Width := SCR_WIDTH;
MainForm.Height:= SCR_HEIGHT;
// параметры формы
MainForm.BorderStyle:= bsNone; // без рамки
MainForm.Position := poDesktopCenter; // по центру экрана
Теперь заставим работать таймер (изначально параметр таймера должен быть enable = false):
MainTimer.Enabled:= true;
Создадим и добавим в событие OnTimer две процедуры (пока пустые) – Main_Update() и Main_Draw(). Для чего это? Мы ведь можем писать весь код напрямую в таймер. Поясню. Например, если мы захотим сделать в игре меню, то должны быть отдельные процедуры обработки/рисования для меню и игры и выполняться они будут в зависимости от того, где игрок находится:
if Scene = sMenu then Menu_Draw;
if Scene = sGame then Main_Draw;
Отрисовка
В этой статье я не буду делать меню, но все же. Итак, мы получили некую заготовку игры. Теперь надо правильно настроить рендер. Canvas отличается простотой, но его недостаток – медленная скорость. Если просто выводить спрайты (изображения) на форму, то будет видно – что они выводятся не сразу, а один за другим и появляется эффект мерцания изображения. Чтобы избежать этого – нужно выводить спрайты не на экран, а в буфер памяти. После того как финальное изображение будет построено, можно вывести его на форму. После вывода буфер можно очистить и он будет готов для построения следующего кадра.
Теперь реализуем данную систему. Объявим переменную – буфер scr_Buffer как Tbitmap. Перед использованием, буфер нужно проинициализировать и установить размеры:
scr_Buffer:= TBitmap.Create;
scr_Buffer.Width := SCR_WIDTH;
scr_buffer.Height:= SCR_HEIGHT;
И в событии Main_Draw() напишем код отрисовки буфера (все рисование должно происходить до этих строчек):
scr_Buffer.Canvas.Rectangle(0, 0, SCR_WIDTH, SCR_HEIGHT); // очищаем буфер ***
Тут же можем проверить, как наша заготовка справляется с рисованием. Загрузим фон для игры в переменную BackImage:
BackImage:= Tbitmap.Create;
BackImage.LoadFromFile(‘fon.bmp’);
// отрисовка
scr_Buffer.Canvas.Draw(0 ,0, BackImage);
// очищение
BackImage.Free;
Нажимаем клавишу <F9> и любуемся на результат (см. рис.1):
Рис. 1. Вывод фоновой картинки игры
Концепция игры. Долгосрочная стратегия
Итак, мы сделали «скелет» игры, на который будем насаживать игровой код. Обдумаем, что же будет представлять из себя игра? Есть змейка, она ползет по уровню, маневрирует между препятствиями и ест все съедобное на ее пути. Но змея не должна есть стены и себя, иначе она погибнет. Из выше написанного подобия «диздока» можно выделить два класса, которые мы будем использовать в игре: змея (назовем ее TSnake) и остальные объекты (TGameObject). Объекты будут иметь переменную, которая определяет их тип: либо это еда, либо препятствие. Каждый объект имеет координату. Поле будет поделено на клетки, это позволит легко определять столкновения (если координаты двух объектов совпадают, то они столкнулись). Размер каждой клетки будет определяться константой CAGE_SIZE = 40. Зная координату клетки можно получить координату на экране, умножив ее на размер клетки. Это нам понадобится при рисовании****. Эти вспомогательные функции делают следующие:
begin
result:= x*CAGE_SIZE
end;
Function G_Y(Y:integer):integer;
begin
result:= Y*CAGE_SIZE
end;
Программируем поведение змейки
Обдумаем, как же будет работать змейка. Каждая деталь змейки – это отдельный элемент со своими координатами. Когда змейка движется – первый элемент (голова) занимает новую позицию, а второй элемент занимает позицию предыдущего до перемещения (там, где раньше была голова), третий позицию второго и.т.д. Это самый логичный вариант, который приходит в голову. Но тогда надо сохранять координаты, где было тело змейки. Для упрощения кода есть более легкий способ – двигать змейку с хвоста. Перейдем к созданию класса змейки:
TSnake=class
Sprite:TBitmap; // спрайт составной части змейки
len:integer; // длина змейки (из какого кол-ва частей состоит змейка)
BodyPos: array of vect2D; // динамический (!) массив с координатами частей змейки
Speed:vect2D; // скорость змейки (сколько клеток она проходит за ход)
MoveTo:(left,right,up,down); // показатель направления движения
Constructor Create(spr_name:string;pos:vect2D;size:integer); // создание змейки со спрайтом,
// позицией и длиной
Procedure SetSize(size:integer);// изменение размера
Procedure Drive; // управление змейкой
Procedure Update; // один ход змейки
Procedure Draw; // отрисовка змейки
Destructor Free; // удаление змейки
end;
Как и в любой игре, в этой нельзя обойтись без математики. Тип «Vect2D» облегчает нам задачу хранения координат и взаимодействия с ним:
vect2D = record
X,Y: integer;
end;
Function v2(x,y:integer):vect2D; // создание
begin
result.X:= X;
result.Y:= Y
end;
Function v2ident(v1,v2:vect2D):boolean; // проверка одинаковых координат
begin
result:= false;
if (v1.x=v2.x) and (v1.y=v2.y) then result:=true
end;
Function v2add(v1,v2:vect2D):vect2D; // сложение координат
begin
result.X:= v1.x+v2.x;
result.Y:= v1.y+v2.y
end;
И вот код всех процедур класса:
Constructor TSnake.Create(spr_name:string;pos:vect2D;size:integer);
Begin
Sprite:= TBitmap.create; // загружаем спрайт
Sprite.LoadFromFile(spr_name);
Sprite.Transparent:= true;
SetSize(size); // установка длины
BodyPos[0]:= pos; // установка положения
MoveTo:= left; // изначальное направление
end;
// установка длины массива частей змейки:
Procedure TSnake.SetSize(size:integer);
begin
len:=size;
SetLength(BodyPos, len)
end;
// вот тут самое сложенное и интересное. Относительно MoveTo устанавливается скорость змейки
// и происходит сдвиг змейки в сторону движения
procedure TSnake.Update;
var i: integer;
begin
if MoveTo=Up then Speed:=v2( 0,-1);
if MoveTo=Down then Speed:=v2( 0, 1);
if MoveTo=Left then Speed:=v2(-1, 0);
if MoveTo=right then Speed:=v2( 1, 0);
// двигаем хвост (downto означает, что мы идет от большего к меньшему)
for i:= len-1 downto 1 do
BodyPos[i]:= BodyPos[i-1];
// двигаем первый элемент (голову) прибавляя скорость
BodyPos[0]:= v2add(BodyPos[0], Speed);
// проверка столкновения головы змейки с хвостом
for i:=1 to len-1 do if v2Ident(BodyPos[0],BodyPos[i]) then TryAgain;
end;
TryAgain() – процедура вызываемая при проигрыше. К ней вернемся позже. В следующей процедуре мы управляем змейкой, задавая ее направление движения через MoveTo(). Это отдельная от Tsnake.Update процедура, поскольку Update() будет вызываться реже таймера, чтобы контролировать скорость движения змейки. При этом, ловить нажатые клавиши управления надо постоянно. Определение нажатых клавиш происходит через функцию Key_Press():
var
keys: TKeyboardState;
begin
result:=false;
GetKeyboardState(keys);
If (keys[key] = 128) or (keys[key] = 129) then result:= true
end;
Коды клавиш можно определить специальной программой [2]. Те, что нам понадобятся, я занес в константы:
KEY_UP= 38;
KEY_DOWN= 40;
KEY_LEFT= 37;
KEY_RIGHT= 39;
KEY_ESC= 27;
Поскольку змейка может только поворачивать влево и вправо, а не может изменять направления движения мгновенно на 180 градусов, введены проверки типа v2Ident(v2(BodyPos[0].x,BodyPos[0].y-1),BodyPos[1])=false, которые не позволяют ей этого сделать:
begin
if Key_Press(KEY_UP) then if v2Ident(v2(BodyPos[0].x,BodyPos[0].y-1),BodyPos[1])=false then MoveTo:=Up;
if Key_Press(KEY_DOWN) then if v2Ident(v2(BodyPos[0].x,BodyPos[0].y+1),BodyPos[1])=false then MoveTo:=Down;
if Key_Press(KEY_LEFT) then if v2Ident(v2(BodyPos[0].x-1,BodyPos[0].y),BodyPos[1])=false then MoveTo:=Left;
if Key_Press(KEY_RIGHT) then if v2Ident(v2(BodyPos[0].x+1,BodyPos[0].y),BodyPos[1])=false then MoveTo:=Right;
end;
И две последние процедуры. Отрисовка отобразит все части змейки по координатам (голова отрисовывается два раза для наглядности):
var i: integer;
begin
for i:=0 to len-1 do if not v2Ident(BodyPos[i],v2(0,0)) then begin
// не отрисовываются части с нулевыми координатами,
// так как их имеют новые части змейки до движения
if i=0 then scr_Buffer.Canvas.draw(G_X(BodyPos[i].x)+Speed.x*5,G_Y(BodyPos[i].y)+Speed.y*5,sprite);
scr_Buffer.Canvas.draw(G_X(BodyPos[i].x),G_Y(BodyPos[i].y),sprite);
end
end;
// при удалении змейки очищается спрайт
destructor TSnake.Free;
begin
Sprite.Free;
end;
Рис. 2. Отображение нашей змейки
Объекты в игре. Не дадим змейке «помереть с голоду»
Теперь необходимо добавить объекты. Они будут двух типов:
TObjType=(oWall,oFood); // стены и еда
type
TgameObject = class
ObjType:TObjType; // тип объекта
Sprite:TBitmap; // спрайт
Pos:vect2D; // положение
Constructor Create(spr_name:string;oType:TObjType;p:vect2D); // создание
procedure Update(Snake:TSnake); // обновление (проверка столкновения со змеей)
procedure Draw; // отрисовка
destructor Free; // удаление
end;
Тут ничего сложного нет. Многие процедуры аналогичны змейке:
begin
Sprite:=TBitmap.create;
Sprite.LoadFromFile(spr_name);
Sprite.Transparent:=true;
pos:= p;
ObjType:= oType
end;
Procedure TGameObject.Update(Snake:TSnake);
begin
if v2Ident(Snake.BodyPos[0],pos) then begin // если змея столкнулась с объектом
if ObjType = oWall then TryAgain;
if ObjType = oFood then begin Snake.SetSize(Snake.len+1);
pos:= RandomFieldPos
end;
// увеличиваем размер змеи, перемещаем еду в другое место
// что такое RandomFieldPos см. ниже
end;
end;
Procedure TGameObject.Draw;
begin
scr_Buffer.Canvas.Draw(G_X(pos.x),G_Y(pos.y),sprite)
end;
Destructor TGameObject.Free;
begin
Sprite.Free
end;
Используем классы в игре
Итак, компоненты игры написаны. Теперь их надо создать, добавить обработку и отрисовку в Main_Update() и Main_Draw(). Вот полный код инициализации змейки и стен. Объявим следующие переменные:
Wall: array of TGameObject; // все стены игры
WallNum:integer; // их кол-во
Food:TGameObject; // еда для змейки
И добавим в инициализацию следующий код:
MySnake:= TSnake.Create(‘snake.bmp’,v2(7,7),2);
// теперь создам еду. А на ужин у нас аппетитненький пингвинчик
Food:=TGameObject.Create(‘food.bmp’,oFood,RandomFieldPos);
Чтобы не вводить координату каждой стенки уровня, так как загрузчика уровней нет, я просчитал создание всех стен в операторе for:
SetLength(Wall,WallNum);
for i:=0 to 15 do Wall[i] := TGameObject.Create(‘wall.bmp’, oWall, v2(i,0)); // верхняя
for i:=16 to 31 do Wall[i]:= TGameObject.Create(‘wall.bmp’, oWall, v2(i-16,11)); // нижняя
for i:=32 to 42 do Wall[i]:= TGameObject.Create(‘wall.bmp’, oWall, v2(0,i-32)); // левая
for i:=43 to 53 do Wall[i]:= TGameObject.Create(‘wall.bmp’, oWall, v2(15,i-43)); // правая
Выше я писал о функции RandomFieldPos(), которая возвращает случайную координату на поле, где нет стен. В OnCreate() надо поставить randomize() для инициализации генератора случайных чисел:
begin
result:=v2(random(13)+1,random(9)+1); // я просчитал допустимые значения
// по X от 1 до 14, по Y от 1 до 10
// тут ничего сложного нет.
end;
Собираем запчасти
Теперь надо добавить нашу змейку, стены и пингвинчика в обработку и отрисовку. Поскольку скорость движения змейки надо ограничить, мы заводим переменную-счетчик WaitTime. Она считает до 5 и выполняет процедуру движения и сбрасывает себя на 0. В итоге, MySnake.Update() срабатывает в 5 раз реже таймера. Ошибкой многих начинающих разработчиков является использование большого количества таймеров, что сильно усложняет код. Чтобы из игры можно было нормально выйти, сделаем условие нажатия на клавишу <ESCAPE>:
var i: integer;
begin
MySnake.Drive;
for i:=0 to WallNum-1 do wall[i].Update(MySnake);
Food.Update(MySnake);
if WaitTime>5 then begin MySnake.Update; WaitTime:= 0; end else inc(WaitTime);
if Key_Press(KEY_ESC) then Application.Terminate
end;
procedure Main_Draw;
var i: integer;
begin
scr_Buffer.Canvas.Draw(0, 0, BackImage);
MySnake.Draw;
for i:=0 to WallNum-1 do Wall[i].Draw;
Food.Draw;
MainForm.Canvas.Draw(0, 0, scr_Buffer);
scr_Buffer.Canvas.Rectangle(0, 0, SCR_WIDTH, SCR_HEIGHT);
end;
Напишем «правильное очищение» всех ресурсов игры:
var i: integer;
begin
scr_Buffer.Free;
BackImage.Free;
MySnake.Free;
for i:=0 to wallnum-1 do wall[i].Free
end;
После чего остается процедура «проигрыша» TryAgain(). В ней сбрасывается длина змейки, а сама она перемещается на стартовую позицию:
begin
MySnake.SetSize(0);
MySnake.SetSize(2);
MySnake.BodyPos[0]:= v2(7,7);
MySnake.MoveTo:= left;
Food.Pos:=RandomFieldPos
end;
Размер устанавливается два раза: первый для того, чтобы сбросить координаты частей змейки на нулевые значения, второй для установки начальной длины. И вот результат (см. рис.3):
Рис. 3. Результат наших трудов. Любуемся игрой
Заключение
Теперь можно запустить игру и наслаждаться результатом. Посмотрев на финальный код можно увидеть, что разработка простой игры не требует каких-либо особых усилий и знаний. Мной эта игра для статьи была написана за 2-3 часа. Весь исходный код проекта, а также текстуры, используемые для создания игры можно найти на http://www.programmersforum.ru в разделе «Журнал клуба программистов. Второй выпуск».
Обсудить на форуме – GAMEDEV на Delphi. Делаем змейку
Статья из второго выпуска журнала «ПРОграммист».
23rd
Создание спектрограммы в Дельфи
Многие начинающие программисты пробуют свои силы в создании аудиоплеера. И вот когда у них более-менее получается создать функциональную основную часть и музыка уже играет, хочется добавить еще чего-нибудь крутого. Часто это спектроанализатор или спектрограмма, которую многие ошибочно называют эквалайзером (эквалайзер – это средство настройки, а не анализа звука) [1].
Александр Терлецкий
В этом уроке мы попробуем создать спектрограмму на Delphi, а прикручивать ее мы будем к движку BASS. Скажу сразу, что существует Delphi оболочка для BASS от нашего корейского коллеги под названием TBassPlayer, в которой есть спектрограмма, но наша задача – научиться самим, а не использовать готовые решения. Однако вам никто не запрещает просмотреть исходный код этого компонента (он свободный), это никогда не повредит. Изучив этот урок, вы научитесь создавать свою собственную, уникальную своим внешним видом спектрограмму. Желательно, чтобы вы имели основные понятия об ООП, так как я не буду подробно останавливаться на этом и больше внимания уделю вопросам вывода графики.
Урок разбит на две части. В первой – пишем класс спектрограммы и отлаживаем его, во второй – прикручиваем его к звуковому движку BASS (вы можете использовать и другой движок, FMOD например, но вам придется разобраться самостоятельно как получить уровни частот). Вариант с самостоятельным разбиением спектра с помощью преобразования Фурье, без использования каких либо звуковых библиотек, я не рассматриваю вообще, если кому очень интересно попробовать, ищите информацию в Интернете, он большой и там все есть*.
Часть 1. Создание класса спектрограммы
Листинг программы смотрите в приложении к журналу [12]. Создадим новый модуль для нашего класса, назовем его Spectrum. В нем описываем главный класс спектрограммы TAnalizer, и вспомагательные TChannel и TChannels для каналов частот. Класс TAnalizer мы наследуем от TPaintBox и добавляем к нему несколько нужных нам полей, это:
- Timer (нужен для анимации ниспадающих пиков, в принципе можно было бы обойтись и без собственного таймера, но так наш анализатор получает большую независимость от деталей реализации использующего его приложения)
- Buffer: TBitmap (буфер нужен чтобы не было мерцания при обновлении изображения)
- Channels (массив каналов, кол-во их будет настраиваемое)
Сами каналы у нас будут иметь градиентную заливку, которая задается полями LowColor: TColor; и HighColor: TColor.
Количество каналов и их координаты на экране задается в методе TChannels.SetCount. Для обратной связи с хозяином в классе TChannels предусмотрено поле Owner, которое заполняется сразу после его создания в конструкторе класса TSpectrum. Возможно, здесь я отошел от канонов. Обычно поле Owner в VCL используется немного по-другому, но в данном случае мне было удобнее сделать так и вообще я не претендую на академичность.
Подробно остановлюсь на методе Draw. В нем происходит обновление изображения в такой последовательности. Очищается буфер и заливается черным фоном, затем с помощью Win API функции GradientFill() мы заливаем градиентом все прямоугольники каналов. Функция GradientFill() довольно сложная и имеет много параметров, так что перед ее вызовом много строк кода занимает заполнение нужных структур для передачи в нее. Затем мы проверяем, есть ли пики, которые стали ниже уровня соответствующей им частоты с момента последней отрисовки, если таковые есть, обновляем их положение. Те пики, которые наоборот стали выше текущего уровня, мы пока не трогаем, они будут обработаны в таймере, чтобы они плавно падали вниз (но рисуются все они в этой процедуре, в таймере только смена позиций и вызов отрисовки). После этого мы закрасим черным (цветом фона) верхушки тех каналов, уровни которых ниже максимального. БылаТут я использованал функцияю из WinAPI – OffsetRect() для смещения прямоугольника канала вверх, иа затем, после закрашивания, повторный вызов OffsetRect(), только теперь вниз, для восстановления прежних координат прямоугольника канала. И в завершении рисуем пики (короткие горизонтальные линии) для всех каналов. После этого у нас имеется обновленное изображение спектрограммы в буфере, и мы выводим его на экран, а точнее на канву нашего объекта.
Чтобы изображение не пропадало с нашего компонента при перерисовке окна, мы обрабатываем унаследованный метод Paint и в ней вызываем нашу процедуру обновления Draw(). Хотя с тех пор, как я добавил таймер, это уже лишнее, можете убрать обработку Paint() если хотите, но если нет таймера, она необходима.
Класс написан, пора его протестировать. Для этого создадим простое приложение. Для чего, поместим на форму TGroupBox, чтобы удобнее было размещать наш анализатор, и две кнопки, по нажатию одной – будет задаваться случайный уровень для всех каналов (см. рисунок 1):
Рис. 1. Тестовая форма. Спектрограмма случайных величин
По нажатию другой – пройдет волна (см. рисунок 2):
Рис. 2. Тестовая форма. Спектрограмма волновой функции
Для волны нам понадобится таймер и подключение в Uses модуля Math, так как мы будем использовать синус. По нажатию третьей – все каналы по очереди заполнятся пиковым значением. Визуально это будет, похоже, как будто кто-то пальцем провел по клавишам пианино (см. рисунок 3):
Рис. 3. Тестовая форма. Спектрограмма в виде пилы
Смотрите листинг, компилируйте ,и разбирайтесь. Если все понятно, то переходим к части 2, игде будем подключать анализатор к плееру.
Часть 2. Подключение к BASS
Как это осуществить? Здесь я отдельно рассмотрю два случая, подключение к компоненту-оболочке TBassPlayer и непосредственно к BASS. Ну что-ж, приступим…
TBassPlayer
Хочу сразу обратить ваше внимание на нюансы с различными версиями как Delphi, так и TBassPlayer. Как вы знаете, начиная с D2009, для строк используется Unicode. Если у вас Delphi более ранних версий, можете дальше не читать и переходить к коду. Если же у вас Unicode версия, то придется внести некоторые изменения в исходники TBassPlayer, в его главный модуль, иначе работать не будет (у меня в D2009 возникали ошибки во время выполнения, связанные с некорректным форматом строк). Хотя в последней его версии и указана совместимость с D2009, да и по времени выхода (май 2009) уже по идее должна быть поддержка юникода, но мне пришлось лезть в исходники даже последней версии. Исправленный модуль вы найдете в файлах к статье [1].
Добавлю еще, что помимо функций оболочки над <bass.dll>, TBassPlayer добавляет поддержку Winamp — плагинов, визуализации и другие функции. Подробнее читайте в справке к компоненту.
Итак, вернемся к нашим баранам. Скопируйте в папку с проектом файл <Spectrum.pas> с нашим анализатором и подключите его в Uses. Я не стал устанавливать в среду TBassPlayer, не люблю я все подряд устанавливать, а просто включил его в Uses. Пути к компоненту можете указать в свойствах среды, а в директорию с программой нужно скопировать файл <bass.dll> подходящей версии (разные версии TBassPlayer работают с разной версией BASS, последняя версия компонента 2.1 может работать с последним BASS 2.4.5). В обработчике события OnCreate() главной формы создаем экземпляр TBassPlayer и назначаем ему на событие OnNewFFTData() процедуру DisplayFFTBand. Затем создаем объект анализатора и назначаем ему количество каналов соответствующее количеству каналов в TBassPlayer (константа NumFFTBands).
Код открытия файла и запуска я скопировал из «демки» к TBassPlayer, заодно там и отображение некоторых свойств файла я оставил, будем выводить их в метки TLabel. Своего я добавил только кнопку Play/Pause, для определения режима работы кнопки используется ее поле Tag (меняем с «0» на «1» попеременно), при нажатии кнопки читаем ее Tag и производим необходимые действия: меняем название, запускаем или останавливаем воспроизведение и меняем Tag.
Кроме этого, мы создаем процедуру ClearChannels. В ней происходит обнуление каналов, это будет происходить при паузе и остановке. Ну и самое интересное для нас – это метод DisplayFFTBand, который привязан к событию обновления данных спектра, в него приходит состояние каналов в виде объекта Bands: TBandOut. Мы просто перегоняем значения в наш Analizer.Channels и запускаем обновление Analizer.Draw. Вот и все. Как видите, подключение элементарное, инкапсуляция проявляет себя во всей красе, все детали скрыты в модулях с классами, и нужно сделать всего несколько телодвижений, для того чтобы приложение заработало.
BASS
Пример кода подключения к <bass.dll> и получения данных спектра любезно предоставил наш форумчанин Зарипов Равиль (ZuBy). Кстати он разрабатывает плеер на базе BASS, можете подробнее ознакомиться на его сайте www.zubymplayer.com.
Библиотеку BASS и API для разных языков вы можете скачать с официального сайта проекта www.un4seen.com.
В поставке с BASS идет оболочка для Delphi <bass.pas>. Скопируйте ее в папку с проектом и подключите ее в Uses. Тоже самое проделайте с <Spectrum.pas>. По сути, подключение анализатора мало отличается от случая с TBassPlayer. Также создаем объект, назначаем ему родителя, координаты и количество каналов.
Небольшие отличия есть в механизме получения данных о спектре. Если кто не знает, FFT – означает Fast Fourier Transorm, по-русски – Быстрое Преобразование Фурье или БПФ. Таким образом, FFT Data переводится как БПФ данные. В таймере мы получаем от BASS эти самые БПФ данные в массив FData и запускаем процедуру SpectrumRender. В ней проверяется, остановлено ли воспроизведение, если остановлено, то обнуляется массив FData. В связи с этим хочу заметить, что в этом случае наш таймер в классе TAnalizer является «пятым колесом», так как отрисовка спектра будет вызвана все равно, даже если воспроизведение остановлено. Он не мешает (нагрузку большую он не дает), но и не помогает, если хотите, можете встроить в класс команду по его отключению или вообще убрать таймер, тогда немного переделать функцию Draw нужно будет. Однако, в случае с TBassPlayer собственный таймер нам пригодился.
Но вернемся к процедуре – SpectrumRender. После проверки на остановку, мы заполняем значениями каналы анализатора и вызываем его отрисовку. В отличие от случая с TBassPlayer, где мы просто переносили значения без изменений, здесь мы имеем сырые БПФ данные, и расшифровку их мы должны делать самостоятельно. В данном случае мы берем, из массива с БПФ, данные, начиная с 5-го элемента, в количестве равном количеству каналов в спектрограмме. Правильно это или нет, я не знаю, и похоже мало кто вообще знает как правильно**, и в основном все возможно больше ориентируются на красивый вид спектрограммы, чем на точные значения частот. В любом случае, мы каналы не подписываем точным значением частоты, так что с нас взятки гладки.
Автор TBassPlayer применяет другой подход. Можете ознакомиться с ним в исходниках к компоненту [22]. А так как цель статьи – показать, как можно вывести на экран спектрограмму, а не разбор БПФ данных, то на этом скажем Фурье до-свидания и продолжим рассмотрение процедуры.
Итак, мы определились, какие элементы из массива будем брать. Затем мы умножаем значение на коэффициент, найденный пробным путем, он зависит от высоты спектрограммы. Судя по тому, что к полученному значению применена функция модуля Abs, значения могут иногда быть и отрицательными, оставляем это без изменений (напоминаю, что эта часть кода не моя, я только прикрутил сюда заполнение анализатора). Откидываем дробную часть с помощью функции Trunc(), и можем заполнять этими значениями каналы нашей спектрограммы. После заполнения вызываем отрисовку методом Analizer.Draw.
Таким образом, при каждом срабатывании таймера, анализатор будет заполняться новыми значениями и будет вызываться его отрисовка. В случае, если трек будет остановлен или поставлен на паузу, так же будет заполняться анализатор, только в этом случае нулями, и так же будет вызываться отрисовка. Это позволит нам увидеть, как пики плавно падают вниз. На этом закончим рассмотрение подключения к BASS.
Заключение
Итак, мы создали свой класс спектрограммы, который умеет вывести себя на экран, как по событию перерисовки, так и по таймеру, к тому же он еще и не мерцает при отрисовке и имеет удобный интерфейс. Также мы имеем образец повторно используемого кода, мы ведь его уже использовали как минимум три раза в разных проектах, первый раз в тесте, второй – при подключении к TBassPlayer, и третий раз при подключении напрямую к BASS.
Исходники тестовых модулей получения спектрограмм прилагаются в виде ресурсов в теме «Журнал клуба программистов. Третий выпуск» или непосредственно в архиве с журналом.
Ресурсы
- Типичные примеры обсуждения на форуме по получению спектрограммы через библиотеку BASS
http://www.programmersforum.ru/showthread.php?t=71069&highlight=%FD%EA%E2%E0%EB%E0%E9%E7%E5%F0
http://www.programmersforum.ru/showthread.php?t=89708&highlight=%FD%EA%E2%E0%EB%E0%E9%E7%E5%F0
http://www.programmersforum.ru/showthread.php?t=94224&highlight=%FD%EA%E2%E0%EB%E0%E9%E7%E5%F0
Модули и проекты, использованные в статье http://programmersclub.ru/pro/pro3.zip. Модули и проекты, использованные в статье http://programmersclub.ru/pro/pro3.zip
Статья из третьего выпуска журнала «ПРОграммист».
13th
Июн
Кое-что о ремонте…
Почти несерьезная статья о серьезной работе: аппаратном ремонте компьютера. Коротко рассказано об основных неисправностях компьютера, и методах их устранения. Приведены основные направления поиска неисправностей, в основном статья предназначена для начинающих ремонтников, а также тех, кто хочет отремонтировать свой компьютер, но не знает, с чего начать…
Дмитрий Дмитренко
ddn.research@gmail.com
Скажу сразу: полностью «спалить» компьютер можно только в печке, если что-то в нем выходит со строя, то обычно какая-то отдельная часть, блок, который вполне возможно восстановить собственными силами, в крайнем случае – купить. Так что, если паяльник Вы держите в руках без заметного волнения, то «лечение» Вашего доброго помощника, советчика, а для некоторых, и «сожителя», Вам очень даже по плечу! Дело за малым: определить, какая именно часть «тела» Вашего «друга» отказала.
Краткий экскурс…
Я не занимаюсь специализированным ремонтом компьютеров, сфера моей деятельности немного не та [1, если кому интересно], но у меня есть много знакомых, друзей и родственников, у которых есть компьютеры, имеющие дурную привычку ломаться. Поэтому я решил описать здесь большинство поломок, имеющих наглость встречаться лично мне. Конечно, можно было бы привести кучу скопированного текста с разных сайтов и умных книжек, но я ограничусь своим собственным мнением на эту тему.
Лично я разделяю проявления неисправностей компьютеров на следующие группы:
- Компьютер не включается (не запускается) – при этом не подаются никакие признаки жизни, максимум, что можно от него «добиться», это включения и последующего отключения. Как ни странно, но для меня такого рода поломки – одни из самых легких, хотя, частенько бывают самые материалозатратные…
- Компьютер включается, но останавливается на стадии BIOS, то есть зависает при появлении информации о системе, или до начала загрузки операционной системы на экране появляются различные сообщения с ключевыми словами «failure» или «failed», или, наконец, раздаются различные кодированные сигналы внутренним динамиком. Такие неисправности также удачно «лечатся», если знать особенности Вашей материнской платы (я имею виду расшифровки этих самых звуковых сигналов).
- Не запускается операционная система. Это, конечно, тяжелый случай, но исправимый. Если операционка не грузится, а в общем компьютер проявляет все радости жизни, то, вероятнее всего, виноваты Вы, когда беспардонно лазя по Интернету или всовывая в порты флешки и диски непроверенные антивирусом, поймали вирус, также, как и, когда удаляли какую-нибудь архисложную программу, удалили вместе с ней очень важный системный файл… Да много еще чего может быть! Я в таких случаях восстанавливаю систему, в крайнем случае – переустанавливаю. Но эту неисправность паяльником не вылечить, поэтому здесь рассматривать не будем.
- Сбои во время работы. Самая сложная неисправность, причин может быть много, но, зная принципы возникновения этих причин, а также физические свойства полупроводников, проводников и диэлектриков, можно с легкостью одолеть и их.
Что ж, начнем…
С радостью прибежав с работы, наспех разувшись и раздевшись, не поевши и не поздоровавшись с родными, с мыслями о вчерашней «недобитой» игре, недописанному сообщении друзьям, недоперенабранной давеча статье, подходите к компьютеру, включаете его, нажимаете заветные кнопки, и… Нет повести печальнее на свете, чем время, проведенное не в нете! Что делать, спросите Вы? Конечно, можно и повеситься, но не сейчас!
Для начала давайте проверим, а поступает ли вообще напряжение на наш компьютер. При наличии тестера это сделать проще простого, вплоть до того, что замерять напряжение на штекере питания компа. Если напруга есть – идем дальше*. Снимаем крышку корпуса и смотрим умоляющими глазами на это разобранное безобразие. Скорее всего, сейчас Вам понадобиться пылесос. Не побрезгуйте, сделайте милость Вашему помощнику – почистите его. Кстати, очень рекомендую как можно чаще чистить внутренности компьютеру, развитие передовых технологий в наше время привело к тому, что даже пыль может влиять на стабильность работы любого электронного прибора…
Ну вот, и почистили его, и поумоляли, и с бубном потанцевали, а он молчит, гад! Сейчас сразу определимся: Ваш комп вообще не включается, так как если он включается, даже на короткое время, а потом отключается, то это уже может быть другая причина.
Значится так, придется опять брать в руки тестер, если Вы его уже брали до этого… Будем измерять выходные напряжения питания.
Выходные – в смысле, не потому, что отдыхают, а потому что на выходе.
Находим разъем питания на материнской плате, его можно издалека заметить, он немаленький, и практически все идущие от него провода далеко не худенькие. Сейчас появилось множество стандартов разводки разъемов блока питания, значения напряжений на нем можно посмотреть в инструкции к материнке, поэтому я здесь конкретно останавливаться на этом не буду. Вот, например, на рисунке 1 приведены чертежи разводки некоторых особенно часто встречающихся в последнее время разъемов. Ну как, измерили? Здесь возможны три с половиной варианта: все напряжения отсутствуют, одно-два (но не все) из напряжений не соответствует номинальному значению (номинальное значение на должно отклоняться более чем на 5%), или вообще отсутствует, все напряжения в норме. Сразу скажу, что последний вариант не рассматриваю, потому что это – практически из области фантастики, очень редкий случай! По поводу первых двух вариантов немного Вас огорчу – придется разбирать блок питания. Естественно, возможны и повышенные значения напряжений, но это довольно редкий и опасный случай, который встречается чаще всего у некачественных блоков питания в основном из-за выхода из строя цепей обратной связи (оптопара и ее обвязка) или микросхемы управления. В таком случае вся надежда на «здоровье» материнки и периферии: смогут ли они выдержать повышенное напряжение, или нет, есть ли у них защита от повышенного напряжения питания, или нет…
Рисунок 1. Разъем источника питания
Стоп! Забыл… Если отсутствуют все напряжения, кроме 5 и (или) 3,3 Вольта, например, цепи 5VSB, то еще проверьте сигнал включения источника питания PS_ON (чаще всего он зеленого цвета), поступающий с процессора. Если его нет – проверьте цепь, в крайнем случае – откиньте этот провод от материнки, и подайте на него «общий» питания, или «массу». Если блок питания (не компьютер!!!) запустился, об этом судят по запуску его вентилятора, то, вероятно, у Вас тяжелый случай – что-то с процессором или материнкой… Если же этот номер оказался «дохлым», с чем Вас пока и поздравляю, то переходим к следующему этапу – разборке источника питания.
Следует обратить ваше внимание на опасность статического электричества для компьютерных комплектующих. И хоть времена КМОП без защитных диодов на входе отошли, но лучше перестраховаться. Прежде чем прикасаться к чему-либо внутри компьютера, избавьтесь от заряда статического электричества, прикоснувшись к неокрашенной металлической поверхности, например к металлической части на задней панели.
В процессе работы периодически дотрагивайтесь до неокрашенных металлических поверхностей на корпусе компьютера для снятия статического напряжения, которое может повредить внутренние компоненты. Отсоедините компьютер и все подключенные к нему устройства от электросети. Также отключите от компьютера все телефонные и телекоммуникационные линии. Это уменьшает риск несчастного случая или удара электрическим током.
Кроме того, придерживайтесь следующих правил техники безопасности… При отключении кабеля от сети беритесь за вилку или за специальную петлю на вилке. Не тяните за кабель. Некоторые кабели имеют фиксаторы на разъемах. Чтобы отсоединить такие кабели, нужно предварительно нажать на эти фиксаторы. Разъединяя разъемы, держите их прямо, чтобы не погнуть контакты. Аналогично, перед подключением кабеля убедитесь в правильной ориентации и соответствии частей разъемов. При работе с компонентами и платами соблюдайте осторожность. Не касайтесь компонентов и контактов плат. Держите плату за края или за металлические кронштейны. Держите компоненты, например микропроцессор, за края, не дотрагиваясь до контактов.
Проверьте наличие коротких замыканий в самой схеме компа. Отключите разъем, подайте «массу» на вывод включения источника питания PS_ON, и измерьте напряжения (или именно то напряжение, которое отсутствовало). Если все чудесным образом пришло в норму, или оказалось несколько завышено (такое бывает при работе блока питания на «холостом ходу»), то Вам следует искать причину в самом компьютере, а блок питания пока не трогать. Но об этом позже. А сейчас…
Разобрали? Опять пыль? Пылесос в студию! Первым делом проверьте предохранитель. Целый? Тогда нам опять не повезло…
Практически все источники питания собраны по идентичным схемам, впрочем, как и все остальные импульсные блоки питания. Поначалу кажущийся таким сложным, он состоит из высоковольтной и низковольтной частей, и трансформатора. Не будем вникать в технические и технологические подробности, начнем ремонтировать. Только помните – ремонт производят при отключенном от сети устройстве!
Я не буду останавливаться на нюансах ремонта блоков питания, так же, как и на подробных принципах их работы, это тема отдельного и очень длинного разговора, я дам лишь краткие сведения, без описания физических данных блоков, коих сейчас выпущено так много, что всех их и не рассмотришь и не опишешь.
Сначала производим внешний осмотр. Если не выявлено явных повреждений в виде «горелых», оплавленных, треснутых от кропотливой работы деталей, переходим к следующей стадии ремонта, если выявлены, то определяем вышедшую из строя запчасть, и… переходим к следующей стадии ремонта.
Проверяем низковольтную часть на наличие пробитых выпрямительных диодов. Это – самая частая неисправность. В качестве таких диодов используют мощные высокочастотные диоды Шоттки**. На рисунке 2 изображен стандартный разобранный блок питания, все основные элементы подписаны, диоды тоже. Измерять обычным тестером, как обычные диоды, они проводят ток в одном направлении, в другом – не проводят.
Затем проверяем высоковольтную часть: входные диоды, задающий транзистор. Я не буду расска- зывать, как их проверять, скажу только, что лучше их проверять выпаянными из платы. Затем на очереди – конденса- торы фильтра питания.
Также можно проверить микросхему, и оптопару, если она присутствует, и их обвязку. Для некоторых, конечно, это высший пилотаж, значения напряжений на выводах микросхемы можно посмотреть только в инструкции к блоку питания, или в datasheet на эту микросхему. В общем – Google рулит! Также проверяем трансформатор на наличие обрывов и, если есть чем измерять, короткозамкнутых витков. Впрочем, если «вылетел» трансформатор, в большинстве случаев придется покупать новый блок питания, хотя, если у Вас есть друзья на «разборке», или Вы специалист по перематыванию импульсных трансформаторов с кучей свободного времени, то Вам невероятно повезло… И, напоследок, проверка емкостей фильтра на выходах. Они бывают как обрывной, потерявшие емкость, так и короткозамкнутые. Конденсаторы, которые не соответствуют стандартным, «строгим», размерам (например, раздутые до неприличия, или с подтеками какой-то желтой жидкости), должны быть проверены тщательнейшим образом!
Ну вот, в принципе, и все, что я хотел сказать про блок питания…
Теперь поговорим о случае, когда при- сутствует замыка- ние по шине питания в самом компьютере, а блок питания вполне исправен.
Сразу оговоримся, что если КЗ «распо- ложено» на материн- ской плате, то у Вас ничего не получится! Но попытаемся…
Рисунок 2. Ремонтируем жесткий диск
Рецепт прост: отклю- чаем сеть, отключаем по одному устройству и включаем компью- тер. Очередность приблизительно такая: винчестер (очень часто встречается, во многих винчестерах установлен защитный стабилитрон на входе питания, который при, даже маленьком, превышении значения напряжения питания сразу «коротит» шину питания на землю), приводы CDDVD- флоппи (причина та же, что и у «винтов»), периферия, расположенная на самой материнке (видеокарта, TV-тюнер и прочее).
Если после отключения какого-либо устройства компьютер заработал или хотя бы включился вентилятор процессора, то виновник видимо определен. Кстати, совсем забыл о USB, COM и прочих портах. Отключите их в самую первую очередь. Никаких флешек, принтеров, джойстиков!
Сейчас немного отвлекусь от темы и расскажу, что можно сделать, если все-таки вышел из строя защитный стабилитрон. На рисунке 3 изображен жесткий диск производства Samsung (не новый, правда). Измеряем сопротивление стабилитрона, если он «коротит», то есть проводит ток во всех направлениях, притом его сопротивление приближается к нулю, то это он, 100%! Что можно сделать? Да выпаять его, и все тут! Работать будет, но… мало ли, от чего произошел перепад напряжения, из-за чего этот стабилитрон закоротило… Лучше поставить другой, необязательно такой же, просто смотрим, какое напряжение выло закорочено, и, в зависимости от этого ставим новый. Если закорочена пятивольтовая шина – стабилитрон на 6.3В, можно и 5,6В, если двенадцативольтовая – 12.6 или 13.2В. Если после замены стабилитрона и движений бубном устройство не заработало, то имеет смысл его покрасить в синий цвет, и выбросить.
Рисунок 3. Блок питания ПК
Если виновник торжества – видеокарта, или прочая периферия, то тут делать нечего, скорее всего придется покупать новую, а старую похоронить с почестями и салютом. Все, по первому пункту я уже все, что знал – рассказал. Переходим ко второму…
Начнем с теории
Как известно, прежде операционной системы в компьютере запускается встроенная в чип материнской платы программа BIOS (Base Input/Output System, основная система ввода- вывода). Назначение этого небольшого (256 Кб) программного кода — свести к «общему знаменателю» аппаратные различия компьютерного оборудования. Параметры настройки BIOS хранятся в энергозависимой CMOS RAM, которая питается от батарейки на материнской плате. Отсюда вывод: если у вас часто «слетают» установки компьютера или «вредничают» часы — скорее всего, пора менять батарейку. После включения питания напряжение подается на центральный процессор и другие микросхемы материнской платы. «Проснувшись», CPU запускает из микросхемы программу BIOS — и начинается процедура POST (Power On Self Test, инициализация при первом включении). Ее задача — просканировать и настроить все «железо». Прежде всего формируется логическая архитектура компьютера. Подается питание на все чипсеты, в их регистрах устанавливаются нужные значения. Затем определяется объем ОЗУ (этот процесс можно наблюдать на экране), включается клавиатура, распознаются LPT- и COM- порты. На следующем этапе определяются блочные устройства — жесткие диски IDE и SCSI, флоп-дисководы. Для устройств SCSI процедура несколько усложняется наличием собственной BIOS, которая берет на себя работу с соответствующим оборудованием и имеет собственную программу настройки. На заключительной стадии происходит отображение итоговой информации.
После окончания работы POST, BIOS ищет загрузочную запись. Эта запись, в зависимости от настройки, находится на первом или втором жестком диске, флоп-диске, ZIP или CDROM. После того как загрузочная записи найдена, она загружается в память и управление передается ей [2].
Если во время работы POST будут обнаружены ошибки, устройство попытается пользователя об этом предупредить. При этом вполне вероятна полная остановка работы компьютера. Задача пользователя – расшифровать сигналы ошибок, передаваемые POST, перевести их на человеческий язык. Для этого существуют специальные POST-карты. Диагностика материнской платы с помощью индикатора POST кодов, превращается в простое и увлекательное дело. Действительно, что может быть проще: вставил POST карту в слот (ISA, часто встречающаяся в «немолодых» компьютерах, или PCI) запустил плату и смотри на результат. В зависимости от модели POST-card коды ошибок выводятся в десятичном или шестнадцатеричном виде на цифровом индикаторе или простыми светодиодами. На некоторых современных материнских платах уже встроены функции заменяющие POST-карту и коды отображаются на находящемся на материнской плате индикаторе.
Но имейте в виду, что POST-карта не может являтся спасением от всех бед, так как она отображает только результат выполнения тестовой процедуры которая находится внутри микросхемы BIOS и при каждом включении и перезагрузке компьютера запускает функции самотестирования основных компонентов и подсистем ПК (таких как процессор, память, чипсет, видеокарту, клавиатуру, жесткие и гибкие диски и т.д.). Если в ходе выполнения обнаружена ошибка, система выдает звуковой сигнал и выводит сообщение на экран (конечно только в случае, если ошибка не была обнаружена раньше, чем например, видеоадаптер). Соответственно, POST карта не покажет полезной информации при неисправности самой микросхемы BIOS, ее обвязке, системах запуска и питания материнской платы. В других, не особенно тяжких, случаях с материнской платой диагностическая POST карта может существенно упро- стить поиск неисправности компонента платы или других деталей компь- ютера. Таблицы POST кодов можно легко скачать с сайтов производителей микросхем BIOS. Также можно их посмотреть в источниках [2, 3]. Единс- твенная проблема – наличие собственно этой POST-карты. Если вы не занимаетесь специализированным ремонтов компьютеров, то она Вам нужна, как собаке пятая нога, тем более в денежном выражении она «нормально стоит». Поэтому остановимся на другой индикации ошибок POST – звуковой. Если во время самотестирования BIOS не может вывести информацию на монитор, используется звуковой сигнал или голос, воспроизводимый при помощи встроенного динамика.
Для различных материнских плат применяется различная звуковая «кодировка». Остановимся только на самых распространенных [4], и тех, что мне встречались чаще всего (см. табл.1-3**).
Звуковые коды в данном случае представлены в количестве звуковых сигналов. Например, 1-2-2 означает 1 звуковой сигнал, пауза, 2 звуковых сигнала, снова пауза, и опять 2 звуковых сигнала.
таблица 1
таблица 2
Все, пожалуй, хватит на сегодня…
Как пользоваться данными таблицами? Ну вот, например, при включении Вы услышали один повторяющийся длинный «пик» динамика, и Вы знаете точно, что у вас Award BIOS. Разбираем компьютер и смотрим ОЗУ, это такая планка, «память» иногда называется (как оказалось в нашем случае – не вечная). Можно попытаться ее вынуть из разъема и почистить контакты, например, обычным ластиком, и снова поставить на место, иногда помогает. А теперь переходим к самому «страшному» пункту моей классификации неисправнос- тей – четвер- тому. Сбой во время работы – это и испор- ченные нервы, и, нередко, разбитый мо- нитор! Поэто- му с этим нужно что-то делать.
Определить причину та- кого сбоя непрофессио- налу можно, в основном, то- лько методом «научного ты- ка». К приме- ру, у меня был случай, когда во время работы ком- пьютер просто «виснул» без всяких при- чин. Причина выяснялась довольно до- лго, пока, во время работы компьютера, я случайно не задел рукой шлейф жесткого диска. Оказалось, окислился контакт, заменил шлейф и «зависания» прошли. Еще когда-то принесли ноутбук, у которого при включении было видно, что загрузка идет, а экран как был темным, так и оставался, только подсветка немного светила. Такое, как оказалось позже, часто встречается при перегреве чипсета видеокарты. В том случае проблема решилась просто: я снял радиатор с этого чипсета, включил компьютер и немного подождал, пока не сработала защита от перегревания, такая защита есть практически у всех компьютеров и ноутбуков.
таблица 3
И, о чудо, при последующем включении все чудесным образом заработало! Конечно, ненадолго, но причина была установлена быстро и дешево – видеоплата. Ведь до этого в сервисном центре предлагали поменять не только видеокарту, но и ОЗУ. Непонятно только – зачем?
Так что, экономия – на лице… заказчика. Вообще, практически все спецэффекты, иьтаситьтаскоторые могут наблюдаться на экране монитора, будь то квадратики-ромбики или цветовые «галлюцинации» происходят по вине видеокарты, а уж затем среди виноватых ищем оперативку или даже процессор.
Заключение
Кстати, перегревания не такая уж редкость в наше пыльное время. И чтобы этого не произошло, и Вам действительно ценно здоровье Вашего компьютера, производите хотя бы раз в полгода- год плановые «медосмотры» с присутствием острого глаза и пылесоса. Подправьте все разъемы, уложите провода, затяните гайки и болты. Не поленитесь также, я уже об этом говорил, очистить от пыли все внутренности, и будет Вам счастье в виде незабываемых минут с развлечениями или работой без нервов и «зависаний»!
Очень полезны в плане ремонта и обслуживания сайты [5-8, 10], есть что почитать, а если нужное не нашли, то можно и лично вопрос задать. В данной статье я, в основном, описал методику поиска простейших, наиболее часто встречающихся неисправностей, которые можно выявить с помощью отвертки и обычного мультиметра и устранить с помощью обычных рук и обычного паяльника. Для более сложных поломок я и рекомендую эти сайты, иногда полезнее воспользоваться чьим-то опытом, чем нарабатывать свой. Это может слишком дорого стоить…
А вообще – опыт приходит с годами, главное – не бояться и соблюдать меры предосторожности, особенно при работе с металлическими предметами (отвертками, пинцетами) под напряжением. И, я уверен, у Вас все получится.
Диод Шоттки – полупроводниковый диод с малым падением напряжения при прямом включении. Назван в честь немецкого физика Вальтера Шоттки. Диоды Шоттки используют переход металл- полупроводник в качестве барьера Шоттки (вместо p-n перехода, как у обычных диодов). Допустимое обратное напряжение промышленно выпускаемых диодов Шоттки ограничено 250 В (MBR40250 и аналоги), на практике большинство диодов Шоттки применяется в низковольтных цепях при обратном напряжении порядка единиц и нескольких десятков вольт.
Оптопара (оптрон) – электронный прибор, состоящий из излучателя света (обычно — светодиод, в ранних изделиях — миниатюрная лампа накаливания) и фотоприемника (биполярных и полевых фототранзисторов, фотодиодов, фототиристоров, фоторезисторов), связанных оптическим каналом и как правило объединенных в общем корпусе. Принцип работы оптрона заключается в преобразовании электрического сигнала в свет, его передаче по оптическому каналу и последующем преобразовании обратно в электрический сигнал. В оптроне входная и выходная цепи гальванически развязаны между собой… Нижняя рабочая частота оптрона не ограничена – оптроны могут работать в цепях постоянного тока. Верхняя рабочая частота оптронов, оптимизированных под высокочастотную передачу цифровых сигналов, достигает сотен МГц / Википедия.
Источники и ссылки
- Сайт автора статьи http://ddn.at.ua
- Программирование под Windows http://www.ru-coding.com
- Ремонт материнских плат http://materinki.narod.ru
- Звуковые коды BIOS http://www.umopit.ru/CompLab/BIOSbeeps.htm
- Форум, посвященный ремонту ноутбуков http://notebook1.ru/forma1
- Конференция по ремонту аппаратуры http://monitor.net.ru
- Конференция IXBT http://forum.ixbt.com
- Ремонт и настройка компьютеров http://comp-remont.info
Статья из четвёртого выпуска журнала «ПРОграммист».
Облако меток
css реестр ассемблер timer SaveToFile ShellExecute программы массив советы word MySQL SQL ListView pos random компоненты дата LoadFromFile form база данных сеть html php RichEdit indy строки Win Api tstringlist Image мысли макросы Edit ListBox office C/C++ memo графика StringGrid canvas поиск файл Pascal форма Файлы интернет Microsoft Office Excel excel winapi журнал ПРОграммист DelphiКупить рекламу на сайте за 1000 руб
пишите сюда - alarforum@yandex.ru
Да и по любым другим вопросам пишите на почту
пеллетные котлы
Пеллетный котел Emtas
Наши форумы по программированию:
- Форум Web программирование (веб)
- Delphi форумы
- Форумы C (Си)
- Форум .NET Frameworks (точка нет фреймворки)
- Форум Java (джава)
- Форум низкоуровневое программирование
- Форум VBA (вба)
- Форум OpenGL
- Форум DirectX
- Форум CAD проектирование
- Форум по операционным системам
- Форум Software (Софт)
- Форум Hardware (Компьютерное железо)