Программная реализация протокола PS/2 для микроконтроллеров AVR
В данной статье приводится пример программной реализации протокола без использования внешних прерываний и прочих возможностей периферии микроконтроллеров, что позволяет подключить к довольно посредственному микроконтроллеру сразу несколько устройств.
Протокол PS/2 используется для обмена данными между компьютерной периферией: мышки, клавиатуры и т.п. Владея подобным инструментом и фантазией можно соорудить весьма интересные устройства. Например, подключить компьютерную мышь к микроконтроллеру с целью использования оптического (или валкодеров механического) сенсора в качестве датчика движения и направления.
Для подключения одного устройства по интерфейсу PS/2 необходимо задействовать два пина микроконтроллера: для линии передачи данных DATA, и линии тактирования CLK. Для удобства конфигурирования микроконтроллера, обозначим необходимые пины:
#define DATA_PORT PORTA #define DATA_DDR DDRA #define DATA_P 2 #define DATA_PIN PINA #define CLOCK_PORT PORTA #define CLOCK_DDR DDRA #define CLOCK_P 3 #define CLOCK_PIN PINA
Для работы по протоколу PS/2 необходимо уметь отправлять и принимать данные. То есть, необходимо написать функции:
void tx_ps2(U8 data); U8 rx_ps2(void);
В приведенных выше ссылках [1], фигурируют функции инициализации для мышки и клавиатуры, стоит отметить, что данные устройства готовы сразу работать без данных функций.
Разберем работу функции отправки данных. Для отправки байта нам потребуются следующие переменные:
U8 clock = 0, bit = 0, count_one = 0;
где:
- clock – счетчик импульсов линии тактирования, ведет подсчет отправленных битов;
- bit – хранит в себе текущий бит данных, используется для управления состоянием линии DATA;
- count_one – счетчик отправленных битов информации равных единице, используется для вычисления бита четности в конце передачи байта данных.
Начало передачи байта данных обозначает прижатие к земле линии CLK и удержание ее в таком состоянии в течение 100 мкс, также необходимо послать стартовый бит, установив лог. 0 на выходе линии DATA:
CLOCK_PORT &= ~(1<<CLOCK_P); CLOCK_DDR |= (1<<CLOCK_P); _delay_us(100); DATA_PORT &= ~(1<<DATA_P); DATA_DDR |= (1<<DATA_P); CLOCK_PORT |= (1<<CLOCK_P); CLOCK_DDR &= ~(1<<CLOCK_P);
По спецификации протокола PS/2 тактирование канала передачи данных всегда осуществляет подчиненное устройство, поэтому необходимо «успевать» отслеживать состояние линии CLK. Логика простая: готовим данные к отправке (определяем, что будем выставлять на линии DATA лог.0 или лог.1, подсчитываем количество выставленных лог.1 для вычисления бита четности), затем переходим в ожидание смены состояния линии CLK, и тут же согласно подготовленным данным, выставляем соответствующее состояние на линии DATA. Выглядит это так:
while ( clock < 9 ){// отсылаем 8 бит данных и 1 бит четности if ( clock < 8 ){// готовим к отправке байт данных if ( tx_data & (1<<clock) ){ bit = 1; count_one++ ; }else{ bit = 0; } }else{// биты данных закончились, готовим бит четности bit = count_one % 2; } while ( (CLOCK_PIN & (1<<CLOCK_P)) ) {}; if (bit){ // отправляем бит DATA_PORT |= (1<<DATA_P); }else{ DATA_PORT &= ~(1<<DATA_P); } clock++; while ( !(CLOCK_PIN & (1<<CLOCK_P)) ) {}; }
Передав все биты данных и бит четности, необходимо переключить линии DATA на прием, дабы получить бит подтверждения удачной передачи данных (ACK бит):
DATA_PORT |= (1<<DATA_P); DATA_DDR &= ~(1<<DATA_P); while ( !(CLOCK_PIN & (1<<CLOCK_P)) ) {}; while ( (CLOCK_PIN & (1<<CLOCK_P)) ) {}; // | A while ( (DATA_PIN &=(1<<DATA_P)) ) {}; // | C while ( !(CLOCK_PIN & (1<<CLOCK_P)) ) {}; // | K while ( !(DATA_PIN & (1<<DATA_P)) ) {}; // | bit
Таким образом, осуществляется передача одного байта данных по протоколу PS/2.
Перейдем к рассмотрению вопроса приема данных из подчиненного устройства. Для приема байта потребуются следующие переменные:
U8 rx_data = 0, clock = 0;
где:
- rx_data – байт принятых данных,
- clock – счетчик импульсов тактирования линии CLK (счетчик принятых бит).
Для работы в режиме приема необходимо перевести линии CLK и DATA в режим работы на вход:
CLOCK_PORT |= (1<<CLOCK_P); CLOCK_DDR &= ~(1<<CLOCK_P); DATA_PORT |= (1<<DATA_P); DATA_DDR &= ~(1<<DATA_P);
Затем, отслеживаем выставление подчиненным устройством START бита на линии DATA:
while ( (CLOCK_PIN & (1<<CLOCK_P)) ) {}; while ( (DATA_PIN & (1<<DATA_P)) ) {}; while ( !(CLOCK_PIN & (1<<CLOCK_P))) {};
Следующим шагом, принимаем биты данных и собираем их в байт:
while ( clock < 8 ){// принимаем байт while ( (CLOCK_PIN & (1<<CLOCK_P)) ) {}; if ( DATA_PIN & (1<<DATA_P) ){ rx_data |= (1<<clock); }; while ( !(CLOCK_PIN & (1<<CLOCK_P)) ) {}; clock++; }
За байтом данных следуют два бита, соответственно Parity bit (бит четности) и Stop бит:
while ( (CLOCK_PIN & (1<<CLOCK_P)) ) {}; // | parity while ( !(CLOCK_PIN & (1<<CLOCK_P)) ) {}; // | bit while ( (CLOCK_PIN & (1<<CLOCK_P)) ) {}; // | stop while ( !(CLOCK_PIN & (1<<CLOCK_P)) ) {}; // | bit
При желании (необходимости) можно осуществить проверку целостности принятых данных, и ввести возвращаемый код ошибки, для случая приема некорректных данных. Стоит отметить, что данная программная реализация протокола не имеет защиты от зависания при нештатных ситуациях, таких, как отключение подчиненного устройства "на горячую". В случае критичности отработки подобных ситуаций необходимо использовать аппаратные таймеры, например watchdog.