Делаем из Raspberry клавиатуру при помощи PS/2 интерфейса

Здравствуйте, уважаемые хабражители!

В этой публикации я расскажу об эмуляции PS/2 клавиатуры при помощи Raspberry Pi.

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

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

Долго не думая, я написал программу, которая последовательно берет данные с Excel листа и посылает в активное окно. Убедившись, что все работает, пошёл спать.

На следующий день меня ждал сюрприз, о котором я, почему-то, сразу и не подумал. На машине с итальянской программой стояли жёсткие групповые политики, запрещающие запуск сторонних программ по хеш-листу. Админы (тоже итальянцы, кстати) не хотели брать на себя ответственность и разрешать запуск сторонних программ. На обход системы защиты одобрения от местного руководства я не получил. Тут-то и пришла идея решения данной проблемы с аппаратной стороны (ведь никто не запрещает подключать стороннюю клавиатуру). Вспомнил я и про Raspberry Pi, который пылился у меня дома.

Начались поиски документации с описанием PS/2 интерфейса. К счастью, на первом же попавшемся ресурсе (), нашёл всю необходимую информацию.

Распиновка кабеля PS/2 клавиатуры


Как оказалось, необходимыми для всей работы провода было 4.
Черный — земля;
Красный — 5V;
Желтый –пин синхронизации времени (CLOCK);
Белый – пин с данными (DATA).

Цвета могут слегка отличаться. Даже из этих 4-х проводов мне понадобились только два — желтый и белый (в земле и 5v мой аппарат, в отличим от клавиатуры, не нуждался).

Описание работы протокола PS/2 (клавиатура -> хост-контролер)


Определимся с терминологией: если на пине присутствует напряжение — будем считать это состояние как 1, иначе 0.

По умолчанию, когда компьютер в состоянии принимать данные, на обоих пинах (CLOCK и DATA) установлено состояние 1. Его устанавливает хост-контроллер на материнской плате компьютера. Принимаем управление на себя, подавая свое напряжение на оба пина (контроллер м.п. снимет свое напряжение). Для инициализации передачи данных мы должны послать 0-й бит компьютеру (не путать с состоянием 0). Для этого в пине DATA устанавливаем состояние 0 и сразу после этого состояние пина CLOCK тоже переводим на 0 (именно в этом порядке). Мы дали хост-контроллеру понять, что хотим передать первый бит. Теперь, если вернуть состояние пина CLOCK на состояние 1, хост-контролер считает первый бит. Таким образом, будем передавать и все остальные биты.

Первый бит всегда 0, это старт-бит (даем знать, что передаем данные).
Далее передаем 8 бит скан-кода клавиши, которую хотим нажать.
Десятым битом подаем бит четности (если количество единиц четное, то 1, иначе 0).
Последний, 11 бит, это стоп-бит, всегда 1.

Таким образом, один пакет данных формируется из 11 бит.

К примеру, если мы хотим нажать клавишу «0» (скан-код 45h= 1000101 в бинарном виде) на хост-контролер посылается следующий массив бит: 01010001001.

Приняв эти данные, компьютер обработает команду нажатия данной клавиши, но ее еще необходимо отжать. Для этого необходимо сперва послать команду F0h, а после — повторно скан-код клавиши, которую необходимо отжать. Так же, между сменами состояний необходимо удерживать паузу. Опытным путем я установил наиболее подходящее: 0.00020 сек если работать на Python и 1 наносек, если кодить на Java.

Схема подключения к хост-контролеру


image

Несколько слов о том, почему я подключил аппарат параллельно клавиатуре. При включении компьютера BIOS проводит проверку состояния PS/2 разъемов. Компьютер и клавиатура обмениваются данными о готовности работать. Клавиатура должна провести внутреннюю диагностику и доложить компьютеру о своей готовности. Только после этого биос разрешает работать с PS/2 интерфейсом. Реализовывать чтение команд от компьютера я поленился.

Теперь при включении компьютера клавиатура доложит ему о своей готовности. После этого я включаю Raspberry Pi и как только он примет управление над интерфейсом на себя работа с клавиатуры будет невозможна. При работе никаких конфликтов я не выявил. Все работало как надо, за исключением того, что изредка компьютер некорректно обрабатывал посланные ему данные, а так как мой Raspberry не настроен на получение данных (в данном случае команды повторно послать код клавиши), то ошибка просто игнорировалась. Эта проблема решилась уменьшением частоты передачи данных.

Программная часть


Сперва серверную часть написал на Java, используя библиотеку pi4j, но как показал логический анализатор, Java машина плохо работала с задержками (они получались слишком большие и компьютер очень часто некорректно принимал данные). Python показал себя намного лучше, код выполнялся быстро, а система грузилась в разы меньше.

Вот и сам код на Phyton:

import socket, select
import RPi.GPIO as GPIO
import time
#Функция посылает один бит данных к контролеру
def sendBit(pinState):
    if pinState==0:
                GPIO.output(pinData, 0)
    else:
                GPIO.output(pinData, 1)         
    GPIO.output(pinClock, 0)
    time.sleep(sleepInterval)
    GPIO.output(pinClock,1)
    time.sleep(sleepInterval)

#Функция посылает массив данных на компьютер
def sendArray(args):
    GPIO.setmode(GPIO.BCM)
    #инициализация пинов,не изменяя состояние (должно оставатся 1)
            GPIO.setup(pinData,GPIO.OUT, initial=GPIO.HIGH)
    GPIO.setup(pinClock,GPIO.OUT, initial=GPIO.HIGH)
    #посылаю 0 бит
    GPIO.output(pinData, 0)         
    GPIO.output(pinClock, 0)
    time.sleep(sleepInterval)
    GPIO.output(pinClock,1)
    time.sleep(sleepInterval)
    #Посылаю полученный массив данных
    for v in args:
                sendBit(v)
    #Посылаю стоп-бит
    GPIO.output(pinData, 1)
    GPIO.output(pinClock, 0)
    time.sleep(sleepInterval*2)
    GPIO.output(pinClock,1)
    time.sleep(sleepInterval*200)
    GPIO.cleanup()

pinClock=4
pinData=15
sleepInterval=0.00020
CONNECTION_LIST = []
RECV_BUFFER = 4096
PORT = 8928     
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# this has no effect, why ?
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(("0.0.0.0", PORT))
server_socket.listen(10)
CONNECTION_LIST.append(server_socket)
print "Готов принимать данные на порту " + str(PORT)
while 1:
    read_sockets,write_sockets,error_sockets = select.select(CONNECTION_LIST,[],[])
    for sock in read_sockets:
                if sock == server_socket:
                           sockfd, addr = server_socket.accept()
                           CONNECTION_LIST.append(sockfd)
                else:                            
                           try:
                                       data = sock.recv(RECV_BUFFER)                                        
                                       i=0
                                       scanCode=[]
                                       print "Принял данные:"+data
                                       for bukva in data:
                                                   if bukva=="1":
                                                               scanCode.append(int(1))                                                                   
                                                   else:
                                                               scanCode.append(int(0))        
                                                   i=i+1
                                       sendArray(scanCode)
                                       sendArray([0,0,0,0,1,1,1,1,1])
                                       sendArray(scanCode)
                                        sock.close()
                                       CONNECTION_LIST.remove(sock)  
                           except:
                                       sock.close()
                                       CONNECTION_LIST.remove(sock)
                                               continue

server_socket.close()


Серверная часть на Java:
import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalOutput;
import com.pi4j.io.gpio.PinState;
import com.pi4j.io.gpio.RaspiPin;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;
import com.pi4j.io.gpio.GpioPinDigitalInput;
import com.pi4j.io.gpio.PinPullResistance;

import java.net.ServerSocket;
import java.net.Socket;

import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;

class controller {
	private static int[] sc1 = {0,0,0,0,1,1,1,1,1};  //F0h
	private static int port = 8928;
	public static GpioController gpio=GpioFactory.getInstance();
	public static GpioPinDigitalOutput pinClock = gpio.provisionDigitalOutputPin(RaspiPin.GPIO_07,"Com1", PinState.HIGH);
	public static GpioPinDigitalOutput pinData = gpio.provisionDigitalOutputPin(RaspiPin.GPIO_16,"Com2", PinState.HIGH);
	public static void main(String[] args) throws IOException {
		//Инициализация сокет сервера и клиента
		ServerSocket server=null;
		try {
			server = new ServerSocket(port);
		}  catch (IOException e) {
			System.err.println("Could not listen on port:"+port);
			System.exit(1);
		}
		Socket client = null;
		System.out.println("Готов принимать данные!");     
		while (true) 
			{
				try {          
					client = server.accept();                                              
					BufferedReader in = new BufferedReader (new InputStreamReader (client.getInputStream()));                  
					String line;
					while ((line = in.readLine())!=null) 
						{
							if (line.indexOf("exit")>-1) {
								return;
							}
							else 
							{
								//Записываю в массив полученные данные
								int[] buf=new int[9];
								for (int i=0; i<9;i++) 
								{
									buf[i]=Integer.parseInt(line.substring(i,i+1));
								}
								System.out.println("Получил пакет:  "+line);//Посылаю под клавиши
								signal(buf);
								//Посылаю код отжатия клавиши F0h
								signal(sc1);
								//Посылаю код клавиши
								signal(buf);
								PrintWriter  out   = new PrintWriter (client.getOutputStream(), true);
								out.print("finished\r\n");
								out.flush();
							}
						}
					}
			}
			catch (IOException e) 
			{
				client.close();
				System.out.println("Shutdown gpio controller");
			}
		}          
	}

	//Функция устанавливает состояние на указанном пине
	static private void setPin(GpioPinDigitalOutput pinObj,int signalByte) {
		if (signalByte==0) {
			pinObj.low();
		} else {
			pinObj.high();
		}
	}

	//Функция посылаем биты с массива
	static private void signal(int[] bits) {
		int sleepInterval=1;
		//Посылаю стоп-бит
		setPin(pinData,0);
		setPin(pinClock,0);  
		sleeper(sleepInterval);
		setPin(pinClock,1);
		sleeper(sleepInterval);           
		// Посылаю массив данных
		for (int i=0; i<9; i++) {
			setPin(pinData,bits[i]);
			setPin(pinClock,0);
			sleeper(sleepInterval);
			setPin(pinClock,1);
			sleeper(sleepInterval);
		}
		//Посылаю стоп-бит
		setPin(pinData,1);
		setPin(pinClock,0);
		sleeper(sleepInterval*2);
		setPin(pinClock,1);
		sleeperM(1);
	}
	
	//Функция устанавливает задержку в макросекундах
	static private void sleeper(int i) {
		try {
			Thread.sleep(0,i);
		} catch(InterruptedException e) {
			System.out.println("Sleepin errore");
		}
	}
	
	//Функция устанавливает задержку в милли секундах.
	static private void sleeperM(int i) {
	try {
			Thread.sleep(i);
		} catch(InterruptedException e) {
			System.out.println("Sleepin errore");
		}
	}
}


Клиентская часть на Java:
import java.net.ServerSocket;
import java.net.Socket;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Scanner;

class tcpClient {
	private static int port = 8928;
	public static void main (String[] args) throws IOException {
		String host="localhost";
		Socket server;
		PrintWriter out=null;
		Scanner sc= new Scanner (System.in); 
		System.out.println("Input host adress:");
		host=sc.nextLine();
		System.out.println("Connecting to host "+host+"...");
		try {
			server = new Socket(host,port);
			out = new PrintWriter (server.getOutputStream(), true);
		} catch (IOException e) {
			System.err.println(e);
			System.exit(1);
		}
		System.out.println("Connected!");      
		BufferedReader stdIn = new BufferedReader (new InputStreamReader(System.in));
		String msg;
		while ((msg = stdIn.readLine()) != null) {
			out.println(msg);
		}
	}
}


Пример оцифрованного сигнала, отправленного с моего аппарата на компьютер. Нижний канал CLOCK, верхний DATA. Сигналы посылаю с Python'а


image

Заключение


Конечно, использовать Raspberry для такой мелкой задачи чистой воды расточительство. Можно было использовать Arduino или собрать схему на дешевеньком arm процессоре. Но это была просто импровизация, которая первой пришла на ум, да и ждать, пока прибудут все необходимые запчасти из Китая, не особо хотелось.

В общем, надеюсь, кому нибудь пригодится данный опыт. Лично я получил массу удовольствия от всего этого процесса.
Поделиться публикацией

Комментарии 47

    +1
    За день количество проданного товара могло превышать полторы тысячи позиций. Вводить штрих код каждого из них в итальянскую систему стало проблематично.

    *facepalm.jpg*, а купить обычный сканер штирхкодов в PS/2 то не судьба?
      0
      Вообщем сканер штрих кода присутствует. В том то и проблема, вбивать каждую позицию даже со сканером дело очень проблематичное. Намного легче выгрузить в Excel лист нажать кнопку «Старт».
        0
        А вбивать в 1С то же самое стало быть не проблематично?
          0
          Почему, проданный товар пробивался одновременно и в 1С и в итальянской программе, а только к концу рабочего дня, я не в курсе. Думаю на это были свои причины.
            +1
            Так ведь причины очевидны — итальянская программа только для отчетов итальянцам, а наши ведь тоже требуют налоговые отчеты…
              0
              Спс, теперь я это знаю=)
          0
          Вообщем сканер штрих кода присутствует. В том то и проблема, вбивать каждую позицию даже со сканером дело очень проблематичное.

          ШТО? Слушайте вот ваши слова:
          За день количество проданного товара могло превышать полторы тысячи позиций. Вводить штрих код каждого из них в итальянскую систему стало проблематично.

          Это делается при продаже сканером-штрих кодов. Это стандартная фича. Я решительно не понимаю зачем этот код сначала писать в файл, а затем при помощи RasberryPi фигачить это в систему. Вы какой товар то грузите? Если приход, то как правило в таких системах есть импорт данных через файл или API о чем можно узнать у поставщика.
            0
            Я не особо углублялся в работу программы. Как я понял, менеджеры в течении рабочего дня работают в 1С. К концу рабочего дня старший менеджер выгружал в один отчет все данные по проданным товарам и заносил их итальянскую программу. В подробности я не углублялся. Думаю на это были свои причины л которых мы не знаем.
              0
              Ну как обычно. Они умеют только в 1С. По идее схема должна быть обратная. Они работают в итальянской программе, а затем уже менеджер делает выгрузку из нее в 1С. И я более чем уверен что так оно и должно работать.
                +1
                Думаю это проблемы руководства магазина, а не программистов.
        0
        Можно пароли от BIOS перебирать.
          0
          Ну не малинкой же.
            0
            Можно=) Только главное во-время остановить его)
            0
            Работало это всё у вас не иначе как святым духом… как это передавать сигналы без подключения ОБЩЕГО провода? Только если запитать платку от того же системника чью клавиатуру взломали…
              0
              Нет, работало все верно. Нельзя делать выводы о том, вы просто не совсем поняли всю схему работы.
                0
                Так а чего не понять если это не озвучено? Общий провод для сигналов CLOCK и DATA на малинке где?
                  0
                  Прошу прошения, может у Вас картинка со схемой подключения пинов не открылась?
                    0
                    В схеме я указывал, что к пинам GPIO4 и GPIO15 на GPIO интерфейсе Raspberry подключены те самые провода.
                      0
                      Это я вижу. Но у вас получается что СИГНАЛЫ не имеют точки отсчета.
                      Что будет с сигналами, если потенциал общего провода «малинки» будет отличаться от потенциала системника к которому подключена клавиатура на 100 вольт?
                      Все сигналы отсчитываются от общего провода! У клавиатуры один общий провод, относительно которого отсчитываются сигналы CLOCK и DATA а у малинки свой, если их не объединить то возникнет ситуация когда уровень напряжения смещен на сотни вольт и более.
                      Например, если малинку включить сетевым адаптером в розетку, то относительно клавиатуры сигналы CLOCK и DATA будут прыгать с размахом 100...200 вольт. Но само собой из-за защитных диодов напряжение на входах не поднимется выше напряжения питания. А оно там у клавиатуры порядка 5В.
                        0
                        Хост-контроллер отсюда отсекают свое напряжение, если на нем есть чужое не менее 50 миллисекунд. Можно было даже обойтись без подачи напряжения. Достаточно было заземлять пины, что было бы равносильно перевода пина в логический 0, но в этом случаи клавиатура могла без проблем подавать свои сигнал, чего я не хотел. Надеюсь я правильно понял ваш вопрос и ваши опасения?
                          0
                          Куда заземлять, если нет общего провода? на заземление что-ли?
                            0
                            А разве есть разница, куда заземлять? Если на проводе (Clock или Data) напряжение подянуто к земле это состояние считается 0 иначе 1, а куда заземлять нет разницы. А вот и ссылка на видео
                              0
                              эээ… как это нет разницы? А если потенциал «земли» к которой подключена малинка больше 1000 вольт относительно потенциала «земли» клавиатуры?

                              От какого потенциала вы отсчитываете напряжение на проводе «CLOCK»?
                                0
                                Потенциал на малинке оказался достаточным, чтобы детектировать состояние 0. Выяснено опытным путем=)
                                  +1
                                  т.е. оно у вас работает вопреки…

                                  Это называется вам повезло. Иначе, можно хоть прям сейчас идти за нобелевкой за передачу данных по одному проводу.

                                  Для того чтобы передавать данные, нужны два провода. Как правило за второй провод для всех сигналов принимают один общий провод и отсчитывают все напряжения относительно его. Вам просто повезло что между клавиатурой и малинкой этот провод по всей видимости есть и поэтому все делает вид что работает.
                                    0
                                    =) Признаюсь, такое даже в голову не приходило. Если бы Вы не сделали замечание, я бы про это и не знал, т.к. все заработало без проблем. В следующий раз учту Ваши замечания. А как вы думаете, оптопара PC-817 подошла бы для этих целей? Просто есть подозрения, что она может не так быстро реагировать на сигналы.
                                      0
                                      Частота задается нами, передавай данные хоть на 300 бод. Поэтому всегда можно подобрать такую скорость когда оптопара срабатывает.

                                      Насколько я знаю, клавиатура работает на 38Кбод, оптрон работать на такой скорости будет но… я такие использовал только на 19200
                                      Судя по характеристикам, оптрон может сдутся уже на 50Кбод(наихудшее значение задержки 18мкс) но может обеспечить и 100кбод — все зависит от конкретных экземпляров.

                                      Но оптопара тут не нужна, достаточно было запитать малинку от USB-порта или прямо с разъема PS/2 клавиатуры и тогда общий провод обеспечен автоматически.
                                        0
                                        Можно было и от USB (HID), просто кое где прочитал, что с Raspberri на PS/2 подать данные не получится, а схема показалась вроде очень простой. Если слишком уменьшить скорость передачи сигналов, котроллер начинал прерывать прием данных (так что, не совсем уж мы и управляем скоростью и по этой причине мне пришлось кодить на Python). Спасибо за ответ, скоро малинка прилетит вторая, буду экспериментировать.
                                          0
                                          только ПИТАНИЕ от USB взять… малинка вроде это позволяет, запитаться от USB-порта.
                                            0
                                            USB надо либо через GPIO (очень геморно, т.к. нет пина синхронизации времени и я не уверен, что частота позволит). Или через USBtoUSB, но тут из -за структуры USB порта простым спаиванием двух USB не обойтись, нужен паять плату или заказать готовую. Хотя я могу что-то упустить.
                                              0
                                              Да, ты почему-то упорно упускаешь суть. зачем реализовывать протокол, с USB надо взять только питание! Всего два провода.
                                                0
                                                Понял, понял, наконец то добрались до сути. Малина то питалась как раз от компьютера, вот и общий провод=) А я и не знал про этот нюанс, получается, если бы я ее запитал допустим через зарядку от телефона то могло не сработать?
                                                  0
                                                  Более того мог бы спалить или клавиатуру или малинку…
                                                    0
                                                    К этому я был готов когда начинал экспериментировать.
                                      0
                                      А везения тут, вроде никакого нет. Слышали про схему открытого коллектора? Видать на меня жара сильно действует (ох уж этот август=)
                                        0
                                        Если эмиттер такого транзистора подцепить к чужой земле не имеющей ничего общего с общим проводом клавиатуры… то ток в цепи не появится и потенциал вывода не изменится.
                                          0
                                          Все больше удивляюсь, почему у меня состояния изменяются. Надо будет повнимательней со-схемой контроллера разобраться.
                                            0
                                            лучше для начала посмотреть осциллографом напряжение между "-" малинки и "-" клавиатуры, можно для начала конечно прозвонить омметром, может они как-то уже соединены.
                                    0
                                    А я, кстати, изначально хотел использовать общий провод напряжения и земли от котролера, с использованием оптопары PC-817, но потом подключил напрямую и все заработало.
                                  0
                                  Было бы интересно подискутировать на эту тему. Может напишите на почту gebekovas@mail.ru?
                        0
                        Вот ссылка на видео, как это работает (индикатор присутствия «святого духа» молчал=). Если есть непонятные моменты, спрашивайте, постараюсь внятно объяснить.
                          +1
                          Где видео, кстати?
                      0
                      Реализовывать чтение команд от компьютера я поленился.

                      Можно считать или найти ответы и их эмулировать.
                        0
                        Задача считывания оказалась намного проблематичной, чем передачи. Первая проблема котороую мне необходимо было решить, это скорость считывания. Raspberry не успевал считывать данные передаваемые контроллером и возникали потери. Я не стал углубляться в эту проблему, предоставив все общение с контролером самой клавиатуре.
                        0
                        Почему то не вставляются ссылки через тег
                        www.youtube.com/watch?v=Ua_5Uekg4tQ — вот ссылка на видео того как все это работает
                        • НЛО прилетело и опубликовало эту надпись здесь

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое