Робота з паралельним портом під Windows, Різне, Залізо, статті

Кузин Ю.Р.

Прочитав я недавно десь в інтернеті, що драйвер паралельного порту в Windows 2000 і Windows XP безпосередньо підтримує роботу з пристроями в режимах EPP і ECP, і вирішив перевірити, в чому це виражається і як це використовувати. Мене більше цікавив режим EPP, який більш практичний, тому що являє собою “винос” шини ISA за межі комп’ютера. Спроби знайти щось путнє в інтернеті привели до статті Тарасенко, де непогано викладені загальні принципи роботи з драйвером паралельного порту. Але цього було недостатньо, тому довелося лізти на MSDN і подивитися, що по цього приводу говорить Майкрософт. Далекий від досконалості online-довідник зорієнтований в основному на розробників драйверів, оскільки передбачає спеціальні знання на кожному кроці. Тому одночасно з бібліотечним розділом Operating a Parallel Device Attached to a
Parallel Port мені довелося тримати відкритими деякі файли з DDK. Можливо тому, що я сам швидше железячнік, ніж програміст, я уникаю написання власних драйверів. Адже для того, щоб моє пристрій запрацював на чужому комп’ютері, туди доведеться поставити власний драйвер кустарного виробництва, а це, по-перше, незручно, по-друге, може привести до “Непередбачуваного поведінки системи”: починаючи від дірок для вірусів (Що завжди дуже важко прорахувати) і закінчуючи тривіальним крахом системи. Як мені здається, велика частина користувачів схиляється до застосування того, що нам дісталося від Microsoft в тому убогому вигляді як це є. Саме для цих людей я і вирішив написати статтю, щоб допомогти їм уникнути труднощів, з якими я зіткнувся.


Почну з того, що поясню загальні правила роботи клієнта з небудь драйвером Windows. Взагалі кажучи, під клієнтом розуміється або інший драйвер, який працює в режимі ядра, або програми працює в режимі користувача. У MSDN, на жаль, не часто проводять цю грань відмінності в тому чи іншому документі, а різниця є: не все, що може використовувати клієнт-драйвер, може застосувати клієнт-додаток. Проходить час поки слідуючи тексту, нарешті зрозумієш, що ось саме ЦЕ придатне тільки для драйвера. Windows слід ідеології багаторівневих драйверів. Як приклад можна привести драйвер від виробника принтера, що використовує для своєї роботи драйвер паралельного порту Microsoft, той же самий яким ми хотіли б скористатися безпосередньо з програми. Забігаючи вперед скажу, що оскільки моє тестове пристрій все-таки не захотіло відразу працювати з під Windows, незважаючи на нормальну роботу в EPP режимі з під DOS, мені довелося, озброївшись цифровим осцилографом з пам’яттю і логічним аналізатором, більш уважно вивчити, що ж все-таки пропонує Майкрософт за рамками власної документації. Результати і спонукали мене до написання цієї статті.


Робота з драйвером пристрою (bus driver) з програми зводиться до чотирьох кроків:



  1. відкрити пристрій;
  2. налаштувати потрібний режим;
  3. читати з та писати в пристрій;
  4. закрити пристрій.

Всі ці операції проробляються з допомогою механізму запитів вводу-виводу драйвера (IRPs). Користувачеві доступні функції, формують такі запити. В Delphi для їх використання потрібно підключити модуль Windows.pas:


uses Windows;


1 КРОК

Функція CreateFile формує запит, що відкриває пристрій. Наприклад, такий виклик відкриває порт LPT1 для операцій асинхронного читання і запису:


hLPT := CreateFile(‘LPT1’, GENERIC_READ or GENERIC_WRITE, 0, nil,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);


Якщо всі запити повинні виконуватися синхронно, то виклик буде виглядати так:


hLPT := CreateFile(‘LPT1’, GENERIC_READ or GENERIC_WRITE, 0, nil,
OPEN_EXISTING, 0, 0);


Тут hLPT – дескриптор пристрою, який потім використовується при зверненні до його драйверу:


hLPT: THandle;


2 КРОК

Управління роботою пристрою, визначення його стану, підтримка режимів і т.д. здійснюється за допомогою запитів IRP_MJ_DEVICE_CONTROL і IRP_MJ_INTERNAL_DEVICE_CONTROL. В режимі користувача підтримується тільки запит першого типу – він формується за допомогою функції DeviceIoControl. В якості одного з параметрів цієї функції фігурує код операциі
(IOCTL), Який визначає дію, що виконується драйвером. Для деяких пристроїв (наприклад, COM-порта) в API Win32 визначені функції управління, які служать оболонкою для
DeviceIoControl при певному значенні IOCTL. Це робить програму більш наочною і читається. Для паралельного порту я такої можливості не виявив, хоча це не викликає труднощів, оскільки ніхто не заважає описати такі функції самому.


Тіло такої функції буде зводиться до виклику
DeviceIoControl з певним набором параметрів. Набагато більш неприємний момент полягає в тому, що для LPTx неможливо визначити комунікаційне подія, щоб потім обробити його в окремому потоці як це можна зробити, наприклад, для послідовного порту за допомогою виклику SetCommEvents. Тому про готовність Вашого пристрою до будь-яких операціях, воно може повідомити, тільки виставивши прапор в деякому своєму регістрі, який Ви будете регулярно опитувати (наприклад, по таймеру), що звичайно не дуже зручно і ефективно. Взагалі кажучи, в режимі EPP паралельного порту визначено зовнішнє переривання по позитивному переходу на лінії 10 (nACK), і драйвер містить його обробник. Доступ до обробника можна отримати за допомогою IRP_MJ_INTERNAL_DEVICE_CONTROL, але тільки в режимі ядра. Ще раз хочу підкреслити, що все викладене – результат моїх власних пошуків, тому не виключено, що якесь рішення від Microsoft для обробки подій паралельного порту в режимі користувача існує. Якщо хто-небудь знає, поділіться. В кінці статті я вкажу E-mail для зв’язку.


Розглянемо по порядку всі коди операцій доступні в якості аргументу у виклику DeviceIoControl при роботі з LPTx. Код являє собою 32-розрядне слово і будується за певним правилом, яке враховує тип пристрою, вид і метод доступу. Чи не вдаючись у подробиці, перелічимо значення кодів для управління паралельним портом і їх ідентифікатори, прийняті в Microsoft.




















































Ідентифікатор коду IOCTL Значення коду
IOCTL_IEEE1284_GET_MODE $160014
IOCTL_IEEE1284_NEGOTIATE $160018
IOCTL_PAR_GET_DEFAULT_MODES $160028
IOCTL_PAR_GET_DEVICE_CAPS $160024
IOCTL_PAR_IS_PORT_FREE $160054
IOCTL_PAR_QUERY_DEVICE_ID $16000C
IOCTL_PAR_QUERY_DEVICE_ID_SIZE $160010
IOCTL_PAR_QUERY_INFORMATION $160004
IOCTL_PAR_QUERY_LOCATION $160058
IOCTL_PAR_QUERY_RAW_DEVICE_ID $160030
IOCTL_PAR_SET_INFORMATION $160008
IOCTL_PAR_SET_READ_ADDRESS $160020
IOCTL_PAR_SET_WRITE_ADDRESS $16001C
IOCTL_SERIAL_GET_TIMEOUTS $1B001C
IOCTL_SERIAL_SET_TIMEOUTS $1B0020

Звертаю увагу читача, що стаття приготовлена ​​для сайту Delphi, тому всі приклади коду і т.п. наводяться на Паскалі. Коди у таблиці вказані в шістнадцятковому форматі (тобто 0х160014 і т.д.).


Код IOCTL_IEEE1284_GET_MODE призначений для формування запиту поточного режиму порту:


const
IOCTL_IEEE1284_GET_MODE = $160014;
type
PARCLASS_NEGOTIATION_MASK = record
usReadMask: word;
usWriteMask: word;
end;
PPARCLASS_NEGOTIATION_MASK = ^PARCLASS_NEGOTIATION_MASK;
var
Mode: PARCLASS_NEGOTIATION_MASK;
lpOverlapped: POverlapped;
ret: DWORD;
DeviceIoControl(hLpt, IOCTL_IEEE1284_GET_MODE, nil, 0, @Mode,
sizeof(PARCLASS_NEGOTIATION_MASK), ret, lpOverlapped);


Якщо запит синхронний, то він виглядає так:


DeviceIoControl(hLpt, IOCTL_IEEE1284_GET_MODE, nil, 0, @Mode,
sizeof(PARCLASS_NEGOTIATION_MASK), ret, nil);


@ Mode – покажчик на буфер, в якому драйвер повертає поточний режим. Наступний параметр повідомляє драйверу розмір буфера, а параметр ret повертає розмір структури, через яку передаються дані. Структура PARCLASS_NEGOTIATION_MASK має два поля: usReadMask – визначає режим роботи порту при читанні, а usWriteMask – При запису. Ось можливі значення цих змінних:


const
NONE = $0000;
// SPP modes CENTRONICS = $ 0001; / / Тільки для запису IEEE_COMPATIBILITY = $ 0002; / / Тільки для запису NIBBLE = $ 0004; / / Тільки для читання CHANNEL_NIBBLE = $ 0008; / ​​/ Тільки для читання BYTE_BIDIR = $ 0010; / / Тільки для читання
// EPP modes EPP_HW = $ 0020; / / Апаратний EPP EPP_SW = $ 0040; / / Програмний EPP
// ECP modes BOUNDED_ECP = $ 0080; / / Спрощений ECP ECP_HW_NOIRQ = $ 0100; / / Апаратний ECP без IRQ ECP_HW_IRQ = $ 0200; / / Апаратний ECP з IRQ ECP_SW = $ 0400; / / Програмний ECP


Відразу після того, як порт відкритий, він встановлюється в режим CENTRONICS по запису і NIBBLE з читання.


Код операції IOCTL_IEEE1284_NEGOTIATE призначений для узгодження режиму роботи порту й пристрої:


const
IOCTL_IEEE1284_NEGOTIATE = $160018;
var
ReqMode, LptMode: PARCLASS_NEGOTIATION_MASK;
ReqMode. usReadMask: = $ 7FF; / / Тестувати всі режими для читання ReqMode. usWriteMask: = $ 7FF; / / Тестувати всі режими для запису
DeviceIoControl(hLpt, IOCTL_IEEE1284_NEGOTIATE,
@ReqMode, sizeof(PARCLASS_NEGOTIATION_MASK),
@LptMode, sizeof(PARCLASS_NEGOTIATION_MASK),
ret, lpOverlapped);


@ ReqMode вказує на структуру PARCLASS_NEGOTIATION_MASK, що містить маску тестування режимів для роботи з пристроєм. Маска являє собою довільну суму, зазначених вище констант.


Драйвер виконує послідовність узгодження з пристроєм для кожного зазначеного в масці режиму відповідно до специфікації IEEE 1284. Якщо відповідний біт у масці виставлений в 1, то даний режим перевіряється на можливість його застосування при роботі з підключеним пристроєм, а потім обирається режиму з максимальною пропускною здатністю. Цей режим встановлюється в якості поточного, а відповідні йому значення драйвер повертає в структурі LptMode. Крім зазначених вище констант, однозначно позначають режими, Windows визначає ще дві маски для запиту:


EPP_ANY = $ 0060; / / будь-який з EPP ECP_ANY = $ 0780; / / будь-який з ECP


Щоб запросити перевірку всіх режимів, потрібно вказати $ 7FF.


Після узгодження режиму драйвер не переводить вихідні лінії порту в стан, що відповідає обраному режиму! Він залишає їх у стані ініціалізації:
























Сигнал Конт. Стан
nSelectIn(1284 Active) 17 низький
nAutoFeed 4 високий
nStrobe 1 високий
Initialize 16 високий

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


Код IOCTL_PAR_GET_DEFAULT_MODES застосовується для запиту режиму драйвера за замовчуванням: CENTRONICS для запису і NONE для читання. Може бути можливі й інші варіанти, тому наведу команду цілком.


const
IOCTL_PAR_GET_DEFAULT_MODES = $160028;
var
Mode: PARCLASS_NEGOTIATION_MASK;
DeviceIoControl(hLpt, IOCTL_PAR_GET_DEFAULT_MODES, nil, 0, @Mode,
sizeof(PARCLASS_NEGOTIATION_MASK), ret, lpOverlapped);


У структурі Mode повертається режим за замовчуванням в тій же манері, що і для запиту поточного режиму IOCTL_IEEE1284_GET_MODE.


Код IOCTL_PAR_GET_DEVICE_CAPS формує запит підтримки пристроєм різних режимів паралельного порту. Майкрософт заявляє про підтримку їх драйвером специфікації IEEE1284, хоча є чимало істотних винятків, якщо довіряти сайту експертів цього протоколу www.fapo.com/. На жаль, у мене немає на руках самого документа, тому що IEEE просить за нього близько $ 100, тому дати 100% гарантію, що Майкрософт не прав не можу. Якщо у когось є pdf оригіналу, поділіться з народом. Я говорю про IEEE1284.3 від 1994 року. А то без исходников відбувається вічна плутанина. Специфікація IEEE1284 визначає п’ять основних режимів:



Майкрософт додає кілька підрежимів:



Запит з кодом IOCTL_PAR_GET_DEVICE_CAPS схожий на IOCTL_IEEE1284_NEGOTIATE з тією різницею, що він не змінює режим драйвера, а тільки проводить цикли узгодження з пристроєм для всіх (!) режимів, щоб з’ясувати які з них підтримуються. Потім результат перевірки повертається у вже відомій структурі
PARCLASS_NEGOTIATION_MASK.


const
IOCTL_PAR_GET_DEVICE_CAPS = $160024;
var
LptMode: PARCLASS_NEGOTIATION_MASK;
DeviceIoControl(hLpt, IOCTL_PAR_GET_DEVICE_CAPS, nil, 0, @LptMode,
sizeof(PARCLASS_NEGOTIATION_MASK), ret, lpOverlapped);


Якщо видати цей запит на порожній порт (без пристрою) то буде відзначена підтримка тільки CENTRONICS і IEEE_COMPATIBILITY. Відповідно і включити ніякі інші режими буде неможливо. Виняток становить тільки NIBBLE, який згідно специфікації повинен підтримуватися всіма IEEE1284-пристроями. Щоб використовувати інший режим, наприклад EPP, необхідно підключити пристрій, що підтримує послідовність узгодження для цього режиму. В ході циклу узгодження хост виставляє на шину даних байт сумісності, який унікальний для кожного режиму:


EPP		01000000
ECP 00×10000
byte mode 00000001


Потім по негативному переходу сигналу Paper End (конт. 12) хост перевіряє рівень на лінії Select (конт. 13). Якщо пристрій підтверджує режим, то на цю лінію воно повинно виставити 1, в Інакше – 0. Байт сумісності має дуже зручну структуру для його апаратної обробки: за кожним режимом закріплений певний біт, який виставляється в одиницю тільки для цього режиму. Тому, скажімо для EPP, досить заклацнути 6 біт (рахуючи від 0) по сигналу nStrobe (1 конт.) і повернути його по лінії Select. Якщо сигнал Select просто назавжди встановити в 1 (підтягнути до +5 В), то пристрій буде підтверджувати всі режими.


Що стосується інших сигналів, то для узгодження роботи з драйвером паралельного порту достатньо на лінях nError (конт. 15) і Paper End виставити лот. 1, а для формування сигналу завершення циклу узгодження можна по лінії nACK (конт. 10) повернути в хост рівень nAutoFeed (конт. 14). Так як обірваний вхід LPT-порту сприймається як 1, то для реалізації цієї послідовності досить просто увіткнути в роз’єм перемичку на контакти 10 і 14. Таке “пристрій” драйвер розпізнає як підтримуюче наступні режими: CENTRONICS, IEEE_COMPATIBILITY, NIBBLE, CHANNEL_NIBBLE, BYTE_BIDIR, EPP_SW, – тобто всі режими, окрім апаратного EPP і всіх різновидів ECP. Справа в тому, що узгодження ECP (який, до речі, придуманий Microsoft в співпраці з HP) вимагає більш складної послідовності узгодження, і простий апаратурою тут не обійтися. В інтернеті вільно поширюється файл ecp_reg.pdf від Microsoft з докладним описом режиму.


Ситуація з апаратним EPP ще більш оригінальна: при запиті цього режиму на роз’ємі порту …….. нічого не відбувається! А драйвер повідомляє, що режим не підтримується. Відсутність навіть натяку на спробу узгодження EPP_HW дозволяє зробити висновок про те, що саме сам драйвер не підтримує цей режим, – можливо через якихось непорозумінь між Microsoft і Intel – автором EPP, може через технічних нестиковок. Нижче зіставляються сигнали в режимах апаратного та програмного EPP, а також застосування відповідних ліній в циклі узгодження.
































































№ конт. Centronics Апарат. EPP Прогр. EPP Узгодження
1 nStrobe nWrite nWrite nStrobe
10 nAck Interrupt ? nAck
11 Busy nWait nWait
12 Paper End PE
13 Select Select
14 nAutoFeed Data Strobe Data Strobe nAutoFeed
15 nError nError
16 nInitialize nReset Address Strobe
17 nSelectPrinter Address Strobe 1284 Active 1284 Active

Тут треба звернути увагу на дві речі. По-перше, в програмної реалізації EPP Майкрософт випадково або навмисно переплутав сигнали місцями: строб адреси тепер на контакті 16 роз’єму, тобто він відповідає сигналу nInitialize. На контакті 17 залишається ознака інтерфейсу 1284, як і у всіх інших режимах, включаючи послідовність узгодження. Цей сигнал можна використовувати як ChipSelect (активний високий). Сигналу скидання nReset як такого немає, але є послідовність скидання, яку виконує драйвер за відповідним запитом (див. далі). По-друге, через обмеженість набору сигналів одні й ті ж лінії використовуються як в циклах квитирования режимів, так і в циклі узгодження (наприклад, Data Strobe – AutoFeed, Write – Strobe). Тому якщо не вжити певних заходів, то під час послідовності узгодження буде зімітовано цикл записи, і в регістр пристрою буде записаний байт сумісності, що не завжди допустимо. Мало того, безпосередньо перед цим імітується читання даних з пристрою, незважаючи на наявність байта на виході LPT. Робота двох пристроїв на вихід одночасно не обіцяє нічого хорошого. В моєму випадку зовнішній пристрій “перетягує” порт, змінюючи байт сумісності. В гіршому випадку щось може вигоріти.


Послідовність узгодження передбачає можливість запиту ідентифікатора пристрою, який представляє собою нуль-термінальну рядок. Цю рядок Windows видає на екран при виявлення нового пристрою. Щоб зробити запит ID, доведеться скористатися кодами IOCTL_PAR_QUERY_DEVICE_ID,
IOCTL_PAR_QUERY_DEVICE_ID_SIZE, IOCTL_PAR_QUERY_RAW_DEVICE_ID.


За допомогою коду IOCTL_PAR_QUERY_INFORMATION у драйвера можна запросити інформацію про його стан.


const
IOCTL_PAR_QUERY_INFORMATION = $160004;
type
PAR_QUERY_INFORMATION = record
Status: byte;
end;
PPAR_QUERY_INFORMATION = ^PAR_QUERY_INFORMATION;
var
ParInfo: PAR_QUERY_INFORMATION;
DeviceIoControl(hLpt, IOCTL_PAR_QUERY_INFORMATION, nil, 0,
@ParInfo, sizeof(PAR_QUERY_INFORMATION),
ret, lpOverlapped);


В поле ParInfo.Status драйвер повертає байт, який може являти собою комбінацію наступних чисел


const
PARALLEL_INIT = $01;
PARALLEL_AUTOFEED = $02;
PARALLEL_PAPER_EMPTY = $04;
PARALLEL_OFF_LINE = $08;
PARALLEL_POWER_OFF = $10;
PARALLEL_NOT_CONNECTED = $20;
PARALLEL_BUSY = $40;
PARALLEL_SELECTED = $80;


Прямого відповідності між бітами ParInfo.Status та фізичними регістрами паралельного порту немає, але якась нетривіальна взаємозв’язок існує: певні комбінації вхідних сигналів на роз’ємі породжують різне стан поля статусу драйвера.


Код IOCTL_PAR_SET_INFORMATION застосовується для скидання драйвера.


const
IOCTL_PAR_SET_INFORMATION = $160008;
type
PAR_SET_INFORMATION = record
Init: byte;
end;
PPAR_SET_INFORMATION = ^PAR_SET_INFORMATION;
var
ParControl: PAR_SET_INFORMATION;
ParControl.Init := PARALLEL_INIT;
DeviceIoControl(hLpt, IOCTL_PAR_SET_INFORMATION, @ParControl,
sizeof(PAR_SET_INFORMATION), nil, 0, ret, lpOverlapped);


Зверніть увагу на значення, яке присвоюється змінної ParControl.Init. Інших варіантів не існує. При такому запиті драйвер скидає nSelectIn (1284 Active) в нуль, а на лінію nInitialize (Address Strobe) видає негативний строб. При цьому сигнал nStrobe (Write) зберігає високий рівень. Така послідовність чимось схожа на цикл читання адреси EPP, яка на практиці рідко використовується. Тому за умовою



можна сформувати сигнал скидання пристрою.


Запит IOCTL_SERIAL_SET_TIMEOUTS встановлює значення таймауту, яке драйвер використовує в операціях запису в режимах SPP і ECP_SW. Читати встановлене значення можна за допомогою
IOCTL_SERIAL_GET_TIMEOUTS.


3 КРОК

Щоб сформувати цикл запису адреси EPP, потрібно скористатися кодом IOCTL_PAR_SET_WRITE_ADDRESS.


const
IOCTL_PAR_SET_WRITE_ADDRESS = $16001C;
var
Address: byte;
Address := $AA;
DeviceIoControl(hLpt, IOCTL_PAR_SET_WRITE_ADDRESS, @Address, 1, nil, 0,
ret, lpOverlapped);


Новий цикл буде сформований лише за умови, що в черговому запиті драйверу буде переданий нову адресу. Драйвер намагається усувати надлишкові операції – блага мета, яка ускладнює систему і як наслідок тягне помилки. Я вважаю, що програміст Майкрософт не правильно зрозумів сенс читання адреси EPP, тому така операція відсутня, зате є команда IOCTL_PAR_SET_READ_ADDRESS, яка не формує фізичних циклів, але налаштовує драйвер таким чином, що він створює цикл запису адреси безпосередньо перед циклом читання даних. При цьому драйвер оптимізуючи цикли адреси, не завжди працює розумно. Тому, не вдаючись в подробиці, я не рекомендую взагалі використовувати IOCTL_PAR_SET_READ_ADDRESS, тим більше що він не дає нічого додатково порівняно з
IOCTL_PAR_SET_WRITE_ADDRESS.


Для запису даних у пристрій використовується функція WriteFile, а для читання ReadFile. Обидві функції обробляють потік даних, а не окремий байт. Це означає, що для режиму EPP буде послідовно сформовано стільки циклів запису (читання) даних, скільки необхідно, щоб передати весь масив байт за байтом. Природно, що всі дані при цьому підуть за однією адресою. Нижче наводиться приклад коду, в якому проводиться запис $ 55 за адресою $ AA, а потім читання за адресами $ AA і $ BB.


uses
Windows;
var
hLpt: THandle;
ret: DWORD;
Address: byte;
Data: byte;
/ / Відкрити порт для синхронного доступу
hLpt := CreateFile(‘LPT1’, GENERIC_READ or GENERIC_WRITE, 0, nil,
OPEN_EXISTING, 0, 0);
/ / Запис $ 55 за адресою $ AA
Address := $AA;
DeviceIoControl(hLpt, IOCTL_PAR_SET_WRITE_ADDRESS, @Address, 1, nil, 0,
ret, nil);
WriteFile(hLpt, [$55], 1, ret, nil);
/ / Читання за адресою $ AA
ReadFile(hLPT, Data, 1, ret, nil);
/ / Читання за адресою $ BB
Address := $BB;
DeviceIoControl(hLpt, IOCTL_PAR_SET_WRITE_ADDRESS, @Address, 1, nil,
0, ret, nil);
ReadFile(hLPT, Data, 1, ret, nil);
/ / Закрити пристрій
CloseHandle(hLpt);


4 КРОК

Закривається пристрій звичайним чином: за допомогою функції
CloseHandle.


 

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*