Хакер № 09/08 (117)

Ликбез по периферии контроллеров AVR
Артемий «Di Halt» Исламов (di_halt@mail.ru)
Судя по числу писем, тема контроллеров актуальна и интересна – каждый второй спрашивал у меня, как реализовать на AVR какую-либо функцию. Поэтому я решил не распылять усилия, а выложить подробный мануал по использованию того барахла, что разработчики из Atmel насовали в свой микроконтроллер. В качестве примера опишу все на контроллере ATMega8. Почему не ATMega8535? А, для разнообразия. Чтобы, не замыкаясь на одном контроллере, наглядно показать, насколько они похожи.
Великие и ужасные FUSE Bits
В прошлых статьях я советовал тебе не лезть к этим битам. И на то были свои основания. Неправильно выставив биты, ты можешь наглухо заблокировать контроллер для перепрошивки (или, вообще, какого-либо использования). Но без знания этой особенности контроллера далеко не уедешь. Так что, распишу все по порядку. У разных версий контроллеров число фьюзов разное, какие-то могут отсутствовать, но основные есть всегда. Вот по ним и пройдемся.
Конфигурационные биты находятся в особой области памяти и могут быть изменены только с помощью программатора при записи контроллера. Итак, главное! В Atmel AVR принята нотация – сброшенный в ноль fuse bit считается активным. С одной стороны, это нелогично и криво, с другой – их контроллер, что хотят, то и делают. Поэтому просто запомни правило.
Однако есть такой популярный программатор – Pony Prog. Они там решили, что всех умнее и сделали все наоборот: в Pony Prog уже установленный бит считается активным. Возникает жуткая путаница! Поэтому надо быть внимательным, как никогда (иначе последствия могут быть печальными).
По умолчанию все контроллеры AVR сконфигурированы так, чтобы работать от внутреннего источника тактов. За это отвечают биты CKSEL.
Выставив их правильным образом, можно выбрать частоту работы контроллера, а также источник тактового сигнала.
CKSEL3…0 = 0000
Внешний источник сигнала – на вход XTAL1 подаются прямоугольные импульсы. Такое делают в синхронных системах, когда несколько контроллеров работают от одного генератора.
Чтобы запустить контроллер от внутреннего источника тактов, необходимо выставить CKSEL следующим образом:
CKSEL3…0 = 0001 - 1MHz
CKSEL3…0 = 0010 – 2 MHz
CKSEL3…0 = 0011 – 4 MHz
CKSEL3…0 = 0100 – 8 MHz
(обычно по умолчанию стоят такие)
Иногда нужен внешний тактовый генератор, например, чтобы его подстраивать без вмешательства в прошивку. Для этого можно подключить RC цепочку, как показано на схеме и подсчитать частоту по формуле f = 1/3RC, где f будет частотой в герцах, а R и С, соответственно, сопротивлением резистора и емкостью конденсатора, в Омах и Фарадах.
CKSEL3…0 = 0101 – для частот ниже 0.9 MHz
CKSEL3…0 = 0110 – от 0.9 до 3 MHz
CKSEL3…0 = 0111 – от 3 до 8 MHz
CKSEL3…0 = 1000 – от 8 до 12 MHz
Проблема внутреннего генератора и внешних RC-цепочек обычно в нестабильности частоты, и если сделать на ней часы, то они будут врать – не сильно, но будут. Поэтому бывает полезным запустить контроллер на кварце. Кроме того, только на кварце можно выдать максимум частоты, а значит, и производительности проца.
CKSEL3…0 = 1001 – низкочастотный «часовой» кварц. На несколько десятков килогерц.
Для обычных кварцев ситуация несколько отличается. Тут максимальная частота кварца зависит также и от бита CKOPT. Когда CKOPT = 1(по дефолту), то:
CKSEL3…0 = 1010 или 1011 - от 0,4 до 0.9 MHz
CKSEL3…0 = 1100 или 1101 - от 0,9 до 3 MHz
CKSEL3…0 = 1110 или 1111 – от 3 до 8 MHz
А если CKOPT равен 0? В этом случае, при тех же значения CКSEL, имеет смысл поставить кварц от 1 до 16MHz. Разумеется, кварц на 16MHz можно поставить только на Мегу без индекса «L».
Стоит упомянуть бит SCKDIV8, которого нет в Atmega8, но который часто встречается в других контроллерах AVR. Это – делитель частоты. Когда он установлен (т.е. в нуле), то частота, выставленная в битах CКSEL0…3, делится на 8 – на чем в свое время застрял dlinyj, долго пытаясь понять, почему у него западло не работает. Прелесть в том, что этот делитель можно отключить программно, записав в регистр CLKPR нужный коэффициент деления, например, 1.
Бит RSTDISBL способен превратить линию Reset в одну из ножек порта, что порой очень нужно, когда на какой-нибудь крошечной Tiny не хватает ножек на все задачи. Но надо помнить, что, если отрубить Reset, то автоматически отваливается возможность прошивать контроллер по пяти проводкам. И для перешивки потребуется высоковольтный параллельный программатор, который стоит несколько тысяч. На коленке сделать его проблематично, хотя и возможно. Я выложу на наш диск простенький HV-программатор для восьминогих контроллеров Tiny11-Tiny15; надо будет – спаяешь.
Второй подлый бит – это SPIEN. Если его поставить в 1, то возможность прошивать «по-простому» тоже мгновенно отваливается. Опять потребуется параллельный программатор!
WDTON отвечает за Собачий таймер, он же – Watch Dog. Этот таймер перезагружает процессор, если его периодически не сбрасывать (профилактика зависаний). Поставишь WDTON в 0, и собаку вообще нельзя будет выключить.
Остальные биты особо тебе пока не нужны. Можешь их не трогать или почитать datasheet на предмет того, что они значат.
Порты – это просто
Почти все выводы микроконтроллера являются выводами портов. Собственно, назначение контроллера: дрыгать этими портами по своему усмотрению, управляя всяким барахлом, что на них заботливо повесил разработчик девайса. Режим работы вывода порта определяется регистрами DDRx, PORTх и PINx.
Каждый бит регистра DDR указывает, в каком направлении работает соответствующая ножка порта: если в разряде «0», то эта ножка работает на вход, а если «1» – на выход. Выставляется обычно все один раз – в момент инициализации порта в начале программы.
В зависимости от регистра DDR, регистр PORT может либо определять тип входа, либо, если нога настроена в DDR на выход, выдавать в ножку «1» или «0».
Регистр PIN содержит значение состояния ножек порта, когда тот работает на вход. Кратко распишу режимы работы портов.
DDR = 0, PORT = 0 – вывод порта работает в режиме Hi-Z, о котором я уже писал в статье «Электронные исходники». Напомню, вывод прикидывается обрывом и никак не влияет на подключенную к нему линию. Однако он может чувствовать логический уровень на шине. Зачем? Например, тебе надо засниферить сигнал в проводе витой пары. Напрямую сделать это нельзя – наличие какого-либо активного девайса на линии вызовет невозможность передачи данных. Когда же ты вешаешь туда ногу порта в режиме Hi-Z, то – никакой разницы, данные по-прежнему текут рекой, и ты их можешь спокойно считывать.
DDR = 0 PORT = 1 – режим с подтягивающими резисторами (pullup). К выводу изнутри, через резистор, подается питание. В результате – на выводе слабая логическая единица. «Слабая» в том смысле, что ток через внутренний резистор течет крохотный, а значит, его можно легко придавить к нулевому уровню. Используется в тех случаях, когда надо получить сигнал от пассивного устройства, например, от кнопки. Вывод устанавливается в режим pullup и через кнопку замыкается на землю. Когда кнопка не нажата, подтяжка выставляет там единичку (это можно наблюдать в регистре PIN). А вот когда кнопку нажимают, то нога контроллера жестко замыкается на нулевой уровень, на землю, и в регистре PIN будет ноль.
DDR = 1 PORT = 0 – на выходе будет нулевой уровень (земля). Тут все просто. Можно задавить, например, вывод с типом pullup у другого контроллера, четко дав понять ему, что мы на эту линию выставляем ноль. Или дать питание светодиоду. Для этого светодиод, через резистор, плюсом подключают к плюсу питания, а минусом – на вывод контроллера. Когда выставим DDR = 1 и PORT = 0, светодиод зажжется.
DDR = 1 PORT = 1 – ситуация прямо обратная: на выводе будет напряжение питания (или близкое к нему). Пригодится, чтобы явно указать, что мы хотим на этой линии видеть единицу и ничто иное. Им тоже можно зажечь светодиод, только подключить его надо будет плюсом к ноге, а минусом к земле.
В последних двух режимах надо помнить, что если таким способом попытаться дать питание какой-либо мощной нагрузке (скажем, лампочке), то велик риск спалить порт контроллера к чертовой матери. А если одну и ту же линию связи два контроллера потянут в разные стороны – один жестко вверх, другой жестко вниз – то почти наверняка один из них сгорит. Максимальный ток, который может потянуть на себя ножка порта – не более 30 миллиампер.
Время пошло!
Как засечь время, чтобы ровно через секунду, после того, как знакомый прошел под датчиком, активировать магнит, который скинет на него ведро воды? Конечно, нужно использовать таймер. В контроллерах AVR таймеров существует целая пачка. Помимо отсчета времени таймер может служить счетчиком, подсчитывая перепады напряжения на ножке T0 или T1. Или даже генерировать ШИМ-сигнал. Проходя через конденсаторный фильтр, тот преобразуется в постоянное напряжение, зависящее от формы ШИМ-сигнала. Таймер представляет из себя встроенный в контроллер девайс, который, будучи однажды запущенным, начинает отсчет. А когда досчитает до максимума, то, во-первых, выставит битовый флажок в специальном регистре, а во-вторых, может перехватить управление на себя, вызвав прерывание. Управляется таймер внешними регистрами. Их я сейчас и распишу, на примере восьмиразрядного таймера номер ноль, контроллера Atmega8.
Самый главный регистр – это TCNT0 (Timer CouNT 0); он выступает счетным регистром, в котором происходит отсчет. Если нам нужно сделать задержку в 100 микросекунд, а таймер тикает со скоростью «1 тик в микросекунду», то в регистр TCNT нужно занести число 155. Так как максимальное число в восьмиразрядном регистре – 255, то от 155 до 255 пройдет ровно сто тиков таймера, а на 101 тике таймер выставит флаг переполнения и вызовет прерывание. Если, конечно, ему это разрешили.
Регистр TCCR0 (Timer/Counter Control Register) задает режим работы таймера.
В самом простом случае (Atmega 8, Timer0) активными будут три последних разряда CS02, CS01, CS00. Режим работы они задают следующим образом:
- CS02..CS00 = 000 – счетчик остановлен (выключен и не работает)
- CS02..CS00 = 001 – счетчик работает, отсчитывая каждый такт процессора
- CS02..CS00 = 010 – работает, отсчитывая каждый восьмой такт процессора
- CS02..CS00 = 011 – работает, отсчитывая каждый 64 такт процессора
- CS02..CS00 = 100 – то же самое, но частота делится уже на 256
- CS02..CS00 = 101 – а тут делится уже на 1024 (самый медленно отсчитывающий режим)
Если принять за данное, что длительность одного такта на 8MHz = 1,25*10^-7 секунды, то длительность одного такта в этом режиме будет порядка 1,2*10^-4 секунды, а полный цикл пересчета от загрузки нуля до переполнения при числе 255 и прерывания – 0,032 секунды. Мало? А ты помножь это дело счетчиком на регистрах.
Смотри, как можно сбацать секундную задержку. Для начала добавим прерывание на переполнение таймера номер ноль.
Впиши эту бодягу в самое начало кода сразу после .include "m8def.inc". Потом создай обработчик прерывания и где-нибудь в конце программы допиши:
Timer_over:
SET ; Установили флаг Т
LDI R16,0 ; Выключили таймер
OUT TCCR0,R16 ; записав в контрольный регистр 0
RETI ; Возврат из прерывания.
Когда таймер переполнится, сюда «прибежит» наша программа и установит флаг Т. Это флаг общего пользования – он ни на что не влияет, поэтому его можно юзать для своих целей.
Пораскинем мозгами. Нам надо 1 секунду и у нас частота 8МГц. Длительность одного тика с максимальным делителем – 1.2*10^-4 секунды. Чтобы получить 0.01 секунду, надо сделать 83 тика. Поэтому в регистр TCNT0 надо загрузить число 255-83 = 172. Тогда до переполнения будет ровно 83 тика.
Получился код:
Second:
LDI R17, 100 ; запомнили что нам надо 100 интервалов по 0.01с
Loop:
LDI R16, 172 ; загрузили число 172 в общий регистр, т.к. в регистры
OUT TCNT0, R16 ; таймера нельзя ничего пихать напрямую, только так
CLT ; сбрасываем флаг Т – это флаг общего пользования
LDI R16, 5 ; подготавливаем число 0000 0101 – делитель 1024
OUT TCCR0, R16 ; загнали это в контрольный регистр. Таймер запущен
dwait:
BRTC dwait ; ждем пока флаг Т не встанет. По сути дела мы
; крутимся в бесконечном цикле.
; Команда BRTC – это переход, если флаг Т не установлен. Флаг Т установится, когда
; произойдет переполнение счетчика и вызовется прерывание, в котором мы написали
; код, устанавливающий флаг Т. Но прерывание каждые 0.01 секунду,
; а нам надо 1 секунду. Поэтому надо сосчитать 100 прерываний
DEC R17 ; Уменьшаем счетчик интервалов на 1
BRNE Loop ; Если не ноль – переход на метку Loop
Со счетчиками работать одно удовольствие, но восьмиразрядный слабоват для больших задержек. Поэтому тут лучше заюзать Timer 1. Он шестнадцатиразрядный, а значит, не потребуется дополнительный пересчет. Хватит и одного таймера.
Запомни это!
Иногда нужно сохранить данные так, чтобы они восстановились после перезагрузки контроллера. В этом тебе поможет EEPROM (почти все контроллеры серии AVR имеют на борту некоторое количество этой памяти). Физически и логически эта память находится в отдельном адресном пространстве, а чтение из EEPROM и запись туда осуществляется через специальные порты.
Чтобы записать что-либо в EEPROM, нужно в регистры адреса EEARH и EEARL (EEPROM Adress Register) положить адрес ячейки, в которую мы хотим записать байт. Затем нужно дождаться готовности памяти к записи – EEPROM довольно медленная штука. О готовности к записи нам доложит флаг EEWE (EEPROM Write Enable). Когда он будет равен 0, – память готова к следующей записи. Сам байт, который нужно записать, помещается в регистр EEDR (EEPROM Data Register). После чего взводится предохранительный бит EEMWE (EEPROM Master Write Enable). Теперь, в течение четырех тактов, нужно установить бит EEWE, и байт будет записан. Если не успеешь выставить бит EEWE, то предохранительный бит EEMWE сбросится и его придется выставлять снова. Это сделано для защиты от случайной записи в EEPROM-память.
Чтение происходит примерно аналогичным образом. Вначале ждем готовности памяти, потом заносим в регистры нужный адрес, а затем выставляем бит чтения EERE (EEPROM Read Enable) и следующей командой забираем из регистра данных EEDR наше число, сохраняя его в любом регистре общего назначения. Для наглядности я тебе набросал две процедуры – на чтение и на запись. Чтобы записать байт, нужно в регистры R16 и R17 занести младший и старший байт адреса нужной ячейки, а в регистр R21 – байт, который мы хотим записать. И вызвать процедуру записи. Аналогично с чтением: в регистра R16 и R17 – адрес, а в регистре R21 должно быть считанное значение.
…
LDI R16,0 ; загружаем адрес нулевой ячейки
LDI R17,0 ; EEPROM
LDI R21,45 ; и хотим записать в нее число 45
RCALL EEWrite ; вызываем процедуру записи
…
LDI R16,0 ; загружаем адрес нулевой ячейки
LDI R17,0 ; EEPROM, из которой хотим прочитать байт
RCALL EERead ; вызываем процедуру чтения. После которой
; в R21 будет считанный байт
EEWrite:
SBIC EECR,EEWE ; ждем готовности памяти к записи. Крутимся в цикле
RJMP EEWrite ; до тех пор, пока не очистится флаг EEWE
CLI ; затем запрещаем прерывания
OUT EEARL,R16 ; загружаем адрес нужной ячейки
OUT EEARH,R17 ; старший и младший байт адреса
OUT EEDR,R21 ; и сами данные, которые нам нужно загрузить
SBI EECR,EEMWE ; взводим предохранитель
SBI EECR,EEWE ; записываем байт
SEI ; разрешаем прерывания
RET ; возврат из процедуры
EERead:
SBIC EECR,EEWE ; ждем, пока будет завершена прошлая запись
RJMP EERead ; также крутимся в цикле
OUT EEARL, R16 ; загружаем адрес нужной ячейки
OUT EEARH, R17 ; его старшие и младшие байты
SBI EECR,EERE ; выставляем бит чтения
IN R21, EEDR ; забираем из регистра данных результат
RET
Память EEPROM маленькая (какие-то считанные байты), а иногда нужно сохранить кучу данных, например, послание инопланетянам или таблицу синусов, чтобы не тратить время на ее расчет. Да мало ли что нужно заранее сныкать в памяти! Поэтому данные можно забивать в память программ, в те самые килобайты флеша, что имеет контроллер на борту.
Записать-то запишем, а как достать? Сначала туда надо что-либо положить – для этого добавляй в конце программы метку, например, «data» и после нее, используя оператор «.db», вписывай свои данные. Оператор DB означает, что мы на каждую константу используем по байту. Есть еще операторы «.dw», задающие двухбайтные константы.
data: .db 12,34,45,23
Теперь метка data указывает на адрес первого байта массива. Остальные байты находятся смещением (просто добавляй к адресу единичку). Кстати, есть одна тонкость: адрес изначально указан в двухбайтных словах. Из-за этого он меньше в два раза, что надо учитывать. Для загрузки данных из памяти программ используется команда
LPM Rn,Z
Она заносит в регистр Rn число из ячейки, на которую указывает регистровая пара Z. Напомню, что Z – это два регистра: R30 (ZL) и R31 (ZH). В R30 заносится младший байт адреса, а в R31 – старший.
В коде выглядит это так:
LDI ZL,low(data*2) ; заносим младший байт адреса, в регистровую пару Z
LDI ZH,high(data*2) ; заносим старший байт адреса, в регистровую пару Z
; умножение на два тут из-за того, что адрес указан в
; двухбайтных словах, а нам надо в байтах. Поэтому и умножаем на два
; после загрузки адреса можно загружать число из памяти
LPM R16, Z ; в регистре R16 после этой команды будет число 12,
; взятое из памяти программ
Пошли всех подальше
Одной из самых любимых моих штуковин, запрятанных в дебрях контроллера, является UART. Он же – последовательный асинхронный порт, работающий по протоколу RS232 – тому же самому, что и известный тебе COM-порт.
Чтобы послать байт, понадобится отправить командой OUT в регистр UDR (UART Data Register) число, а чтобы принять байт, соответственно, нужно оттуда число командой IN считать.
Но предварительно передатчик надо сконфигурировать, так как UART умеет работать почти на всех мыслимых и немыслимых вариациях протокола RS232. Конфигурируется UART посредством трех контрольных регистров – UCSRA, UCSRB, UCSRC и регистровой пары UBRRH:UBRRL, в которую записывается делитель скорости передачи.
В UCSRA нас, в первую очередь, интересуют биты RXC и TXC – флаги, сигнализирующие завершение приема и передачи. Отслеживая их, можно прикидывать, что делать дальше – забирать свежеполученный байт или готовить новую передачу. Бит UDRE (UDR Empty) сигнализирует о том, что прошлый байт передан, и в UDR можно загружать следующий байт. От ТXC его отличает то, что он выставляется при передачи каждого байта, а TXC только при окончании передачи вообще.
Ну и не следует забывать про бит U2X, осуществляющий удвоение скорости передачи.
Регистр UCSRВ интересен своими битами RXCIE, TXCIE и UDRIE, разрешающими прерывания по окончании приема, передачи и опустошении буфера приема. Это позволяет не тупо ждать завершения, а перевесить весь геморрой с приемо-передачей на обработчики прерываний. Также тут находятся биты RXEN и TXEN, которые разрешают прием и передачу (соответственно).
Регистр UCSRC содержит информацию о формате передачи, количество стоп битов, сколько бит в посылке, какой контроль четности, синхронный или асинхронный режим передачи. Причем, тут есть один косяк, из-за которого я долго тупил в свое время – бит селектора URSEL. Дело в том, что по неизвестной причине создатели решили сэкономить байт адреса и разместили регистры UCSRC и UBRRH в одной адресном пространстве. Как же определить, куда записать? А по старшему биту! Поясню на примере. Если мы записываем число, у которого седьмой бит равен 1, то оно попадет в UCSRC, а если 0, то UBRRH. Смотри код:
LDI R16,0x10000010b ; старший бит равен 1
OUT UBRRH,R16 ; оба-на! заводим вроде бы в UBRRH,
; а реально попадает в UCSRC
или вот:
LDI R16,0x00000010b ; старший бит равен 0
OUT UCSRC,R16 ; заводим вроде бы в UCSRC,
; а реально попадает в UBRRH
Вот такая вот загогулина! А во всем виноват переключающий бит, так как, с точки зрения компилятора, метки UBRRH и UCSRC ведут в одно место. Если заглянуть в m8def.inc, то увидим там следующие строки:
.equ UBRRH= 0x20
.equ UCSRC= 0x20 ; ЧТД!
Ну что, теория кончилась, сейчас выдам тебе голую практику. А конкретно – инициализирующую процедуру, настраивающую UART на работу со стандартным виндовым терминалом. Восемь бит, один старт/стоп!
uart_init:
; для начала удобные макросы:
.equ XTAL = 8000000 ; частота твоего контроллера в герцах
.equ baudrate = 9600 ; сколько надо бод
.equ bauddivider = XTAL/(16*baudrate)-1 ; подсчет делителя
LDI R16, low(bauddivider)
OUT UBRRL,R16
LDI R16, high(bauddivider)
OUT UBRRH,R16
LDI R16,0
OUT UCSRA, R16
LDI R16, (1<<RXEN)|(1<<TXEN)|(0<<RXCIE)|(0<<TXCIE)
; запрещаем прерывания от любых действий UART, разрешаем прием и передачу
; не пугайся такой жуткой записи, это логическая формула, которая на основании
; номеров битов расставит все сама по местам. Это лучше, чем вводить 00011000 –
; меньше наделаешь ошибок
OUT UCSRB, R16 ; и загружаем сформированное в R16 число в регистр
LDI R16, (1<<URSEL)|(1<<UCSZ0)|(1<<UCSZ1)
OUT UCSRC, R16
; Благодаря выставленному биту-селектору URSEL, данные уйдут в нужный регистр
; выставив две единички в UCSZ1/0, я задал посылку 8 бит – стандартная для COM
; порта
RET ; возврат к основной процедуре
Теперь, чтобы послать число во внешний мир, нужно всего лишь дождаться готовности и забросить его в регистр UDR. Отправим наше послание человечеству через UART. У кого как, а у меня послание обычно начинается с буквы «F» :).
RCALL uart_init ; вызываем нашу процедуру инициализации
LDI R16,’F’ ; загоняем в регистр код буквы «F»
uart_snt:
SBIS UCSRA,UDRE
RJMP uart_snt ; торчим на этом цикле, пока бит UDRE не станет «1»
OUT UDR, R16 ; засылаем нашу букву в порт и ловим ее на другом
; конце провода :)
RETI
Вот так, камрад. Для начала хватит. Своего первого робота или другую полезную безделушку на этом сделать можно запросто. Если интересует больше, то вперед, в инет. На сайте http://easyelectronics.ru есть неплохой, на мой взгляд, учебный курс по AVR, где на примере постройки робота последовательно и подробно изучается использование контроллера ATMEGA8.
Содержание
ВИДЕО К ЭТОМУ НОМЕРУАвтомобильный взлом В этом ролике ты увидишь, как был взломан один из зарубежных ресурсов. Сначала хакер находит бажный perl-скрипт, затем проверяет свои права на сервере и находит каталог с chmod’ом 777. Далее, взломщик заливает txt-версию веб-шелла на оди...
Зона терминального доступа В состав Win2k8 входит полнофункциональная и высокопроизводительная версия служб терминалов, обеспечивающая поддержку 32 битного изображения, передачу звука, перенаправление локальных принтеров и COM-портов, сопоставление дисков, повышен...
Процессы VS Отладчик Если ты хочешь узнать, как при помощи отладчика внедрить в PE-файл код, создающий новую пустую область памяти с соответствующими атрибутами, узнать, как завершить процесс пользователя, зашедшего в систему при помощи учетной записи, даже...
На страже безопасности Для организации совместного доступа в Сеть и защиты внутренних ресурсов администраторы со стажем предпочитают использовать специализированные мини-дистрибутивы, построенные на базе урезанных версий Linux или BSD. C их помощью можно легко...
Игра без бана В ролике рассмотрен способ защиты от бана в Warcraft 3 с помощью утилиты WC3Banlist. При помощи проги W3XNameSpoofer, смены IP-адреса и формата записи ника открывается возможность постоянного и беспрепятственного обхода банов. Любителям...
|
 |
|