И сразу к делу!
Протокол Maple BUS симметричный, то есть имея одну хорошую реализацию например HOST'а эту же реализацию можно использовать и как DEVICE. Проще, - можно читать джойстик, а можно им прикинуться.
Описание протокола (аппаратная часть)
Интерфейс Maple BUS двух-проводный. SDCKA/SDCKB, каждая из линий на определенных этапах выполняет роль как "передающая данные" и так и "защелкивающая данные".
Общение по шине Maple BUS осуществляется пакетами. Каждый пакет данных состоит из заголовочного паттерна, данных, контрольной суммы и завершающего паттерна. Максимальная длина пакета данных 1024 байта.
![](https://habrastorage.org/getpro/habr/upload_files/091/76a/69c/09176a69c7e84da452998af7d09a4b0d.png)
Всего паттернов 5 видов:
START - указывает на начало передачи данных (4-ре клока SDCKB в то время пока SDCKA в низком уровне).
![](https://habrastorage.org/getpro/habr/upload_files/a8f/cca/5a2/a8fcca5a2e22746493d9eb8b0b81c03c.png)
Пакет всегда должен заканчиваться паттерном END (2-ва клока SDCKA пока SDCKB в низком уровне):
![](https://habrastorage.org/getpro/habr/upload_files/a29/733/4d2/a297334d2704204d58361b10ac5c7604.png)
Occupancy паттерн - указывает на старт режима прослушивания шины (8-мь клоков SDCKB пока SDCKA в низком уровне). Переход линии HI->LO SDCKA после получения этого паттерна указывает на начало режима, LO->HI указывает на завершение. Этот режим используется для взаимодействия со световым пистолетом (Light GUN - Func. FT7):
![](https://habrastorage.org/getpro/habr/upload_files/d92/73b/f47/d9273bf472c096bcfa22e4f7e9380044.png)
RESET - аппаратный перезапуск устройства (14-ть клоков SDCKB пока SDCKA в низком уровне, только для DEVICE).
![](https://habrastorage.org/getpro/habr/upload_files/edb/b65/970/edbb659703f2329993a6429e4a8e97ff.png)
Теперь рассмотрим как по шине передаются данные.
Биты данных передаются фазами. В четной фазе линия данных - SDCKB, а клок - SDCKA, в нечетной наоборот (этот фрагмент тоже назовем паттерном :) ).
![](https://habrastorage.org/getpro/habr/upload_files/815/7d2/567/8157d256750929fb337ba1a7d3c6f30d.png)
Величина таймаута на ответ от устройства после запроса хоста 1мс:
![](https://habrastorage.org/getpro/habr/upload_files/804/519/332/804519332eecedf550677bb879d0ebcc.png)
Помним, что например к джойстику можно подключать VMU, вибропак, микрофон...
Устройства подключаемые непосредственно к Maple BUS называются Device, а устройства подключаемые к Device называются Expansion Device, общение между Device и Expansion Device осуществляется средствами протокола LM-Bus. Expansion устройств можно подключить до 5-ти, хотя я не видел ни одного устройства в котором это было реализовано, а в чипах (например 315-6211-AB) "выведено наружу" только под 2-ва (хотя в программной части протокола под идентификацию EXP-DEV выделено пять бит, но тут честно говоря нужно уточнить, VMU например содержит память и LCD дисплей, это уже два Exp. устройства).
![](https://habrastorage.org/getpro/habr/upload_files/168/a59/c74/168a59c748781b2018a8dbd71992e8b7.png)
LM-BUS это что то типа суррогата Maple BUS, то есть шина на которую DEVICE напрямую переключает шину Maple BUS согласно тому какой Exp. DEVICE выбран HOST'ом.
LM-BUS тема отдельного разговора, отвлекаться не буду, перейдем к программной реализации протокола.
Программная часть протокола
Как я уже писал выше данные передаются пакетами, рассмотрим пакет поближе:
![](https://habrastorage.org/getpro/habr/upload_files/ac2/83e/c7c/ac283ec7c7bbec33a7f687938c79c9dc.png)
COMMAND - команда, может принимать значения от 0x01 до 0xFE (см. возможные значения в коде ниже "maplebus.h").
maplebus commands
//HOST
#define DeviceRequest 0x01
#define AllStatusRequest 0x02
#define DeviceReset 0x03
#define DeviceKill 0x04
#define GetCondition 0x09
#define GetMediaInfo 0x0A
#define BlockRead 0x0B
#define BlockWrite 0x0C
#define GetLastError 0x0D
#define SetCondition 0x0E
#define FT4Control 0x0F
#define ARControl 0x10
#define TransmitAgain 0xFC
//Device
#define DeviceStatus 0x05
#define DeviceAllStatus 0x06
#define DeviceReply 0x07
#define DataTransfer 0x08
#define ARError 0xF9
#define LCDError 0xFA
#define FileError 0xFB
#define TransmitAgain 0xFC
#define CommandUnknown 0xFD
#define FunctionTypeUnknown 0xFE
DEST. AP - адрес назначения пакета (для какого устройства пакет).
ORIG. AP - от кого пакет.
Для AP справедлива следующая таблица:
![](https://habrastorage.org/getpro/habr/upload_files/6b1/f2f/30d/6b1f2f30d5d390732a1fe9f7ad3e80ae.png)
PO[1:0] - Номер порта (A - 00, B - 01, C - 10, D - 11).
D/E - (1 - Device, 0 - Expansion Device или PORT).
LM[4:0] - (1 - Exp. DEVICE подключено, 0 - Слот Exp. пуст).
DATA SIZE - размер данных в пакете в 32-х битных чанках.
DATA - Состав пакета.
CRC - побайтный XOR всех данных включая COMMAND, AP, DATA SIZE, DATA.
"Общение" между HOST и DEVICE начинается с запроса DeviceRequest, в нем хост указывает какой порт он опрашивает, устройство, первый раз после включения или сброса "увидев" номер порта присваивает его себе (A/B/C/D).
Отвечать на данный запрос любое устройство обязано статусом (DeviceStatus answer):
![](https://habrastorage.org/getpro/habr/upload_files/766/864/84c/76686484cea00af216475ee54b05df47.png)
Device ID - содержит функциональные возможности периферии (Device ID содержит блок FT, состав включенных битов в этом блоке определяет функции которые поддерживает устройство и FD - параметры поддерживаемых функций).
![](https://habrastorage.org/getpro/habr/upload_files/817/b16/e0f/817b16e0f32c6d0e5a22e3c5f405bdca.png)
Device Functions
/*Device functions*/
#define CONTROLLER MAKE_DWORD(0x00000001) //FT0 : Controller Function
#define STORAGE MAKE_DWORD(0x00000002) //FT1 : Storage Function
#define LCD MAKE_DWORD(0x00000004) //FT2 : B/W LCD Function
#define TIMER MAKE_DWORD(0x00000008) //FT3 : Timer Function
#define AUDIO_INPUT MAKE_DWORD(0x00000010) //FT4 : Audio input device Function
#define AR_GUN MAKE_DWORD(0x00000020) //FT5 : AR-Gun Function
#define KEYBOARD MAKE_DWORD(0x00000040) //FT6 : Keyboard
#define GUN MAKE_DWORD((unsigned int)0x00000080) //FT7 : Light-Gun Function
#define VIBRATION MAKE_DWORD((unsigned int)0x00000100) //FT8 : Vibration Function
#define MOUSE MAKE_DWORD((unsigned int)0x00000200) //FT9 : Pointing Function
#define EXMEDIA MAKE_DWORD((unsigned int)0x00000400) //FT10 : Exchange Media Function
#define CAMERA MAKE_DWORD((unsigned int)0x00000800) //FT11 : Camera Device Functio
Destination code - указывает на целевой регион использования устройства.
Product name - название устройства (например {'D','r','e','a','m','c','a','s','t',' ','C','o','n','t','r','o','l','l','e','r', ' ',' ',' ',' ',' ',' ',' ',' ',' ',' '} - 30 байт).
License - кому принадлежит лицензия ( {'P','r','o','d','u','c','e','d',' ','B','y',' ','o','r',' ','U','n','d','e','r',' ','L','i','c','e','n','s','e',' ','F','r','o','m',' ','S','E','G','A',' ','E','N','T','E','R','P','R','I','S','E','S',',','L','T','D','.',' ',' ',' ',' ',' ',} -60 байт ).
Min./Max. current - соотв. минимальное и максимальное потребление устройства (1мА = 10 единиц, 43мА => 0x1AE).
Далее в пакете может идти "свободный статус устройства" (на изображении не указано, так как этот кусочек не обязателен), для джойстика он выглядит так: 40 байт "Version 1.000,1998/05/11,315-6125-AB Analog Module: The 4th Edition. 05/08".
То, какие команды применимы к устройству нам показывает блок Device ID.
К FT0, CONTROLLER, применима команда GetCondition - получить состояние кнопок/триггеров и аналоговых стиков геймпада. То в каких битах расположены какие значения указано всё в том же блоке Device ID. В частном случае, для геймпада Device ID будет выглядеть так:
![](https://habrastorage.org/getpro/habr/upload_files/6f3/6c4/30d/6f36c430d4e678910d47aa75e54a42c1.png)
В ответ на запрос GetCondition, джойстик обязан отправить отчет о состоянии кнопок, выглядит он следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/fcd/62e/3ba/fcd62e3bada92df443003cc5c3c15f71.png)
Ra/La/Da/Ua - Право/Лево/Вниз/Вверх (цифровой "крестик").
Start/A/B/X/Y - соотв кнопки.
A1, A2 - аналоговые курки
A3 и A4 - положение "стика".
Вот собственно и всё что нужно знать для реализации контроллера.
Реализация (аппаратная часть)
В общем то можно взять линии SDCKA и SDCKB и прикрутить на прерывания микроконтроллера и чисто программно реализовать, однако если МК медленный, то успевать не будет, и это не самое главное, во первых "плотность данных" для разных устройств разная и если например джойстик работать будет на одной программной реализации, то не факт что будет работать вибропак или карта памяти, во вторых в программной реализации определение ошибок внутри фрейма как и скажем команда "аппаратный сброс" не реализуемы, поэтому правильнее будет сделать аппаратный кусочек приемника, а отправлять и микроконтроллером можно.
Возьмем CPLD попроще (EPM3032) и реализуем xMAPLE:
![](https://habrastorage.org/getpro/habr/upload_files/63f/3b0/000/63f3b0000aeb3be2d3957f465f087415.png)
SDCKA/SDCKB - вход линий Maple BUS.
GCLK - внешний CLK 16-48MHz.
INHTxD - сигнал блокировки работы приемника, 1 - игнорировать события на шине, 0 - нормальное функционирование.
RxD - идет прием пакета.
nSTRCV- начат прием пакета (Rising Edge).
nDLatch - Негативный импульс для "защелки данных" (сигнализирует о том что на линии данных Q[7..0] присутствует следующий полученный байт).
Q[7..0] - шина данных.
EOP - получен паттерн END (конец приема пакета).
![](https://habrastorage.org/getpro/habr/upload_files/2df/e84/9c1/2dfe849c1f47ca127ae60321c51d8e01.png)
FERR - обнаружена ошибка при приеме пакета.
![](https://habrastorage.org/getpro/habr/upload_files/7c0/bdb/a70/7c0bdba70fefbaa94e0f708498652309.png)
nRST - подключается напрямую к микроконтроллеру - если получен RESET паттерн, - 0.
![](https://habrastorage.org/getpro/habr/upload_files/a48/183/018/a481830185d5d79aa0a291c1f32571d8.png)
И общий вид:
![](https://habrastorage.org/getpro/habr/upload_files/dcc/97f/300/dcc97f3005f99457f4123be2634bdfd5.png)
Пишем это на верилог'е (3 файлика, надеюсь догадаетесь как это соединять):
SMAPLE.v
module SMAPLE(
input GCLK, //MCU Generated 16MHz clock input
input INHTxD, //Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA)
input SDCKAi, //Data/Clock A Line
input SDCKBi, //Data/Clock B Line
output RxD, //Receive on progress (While receive is 1)
output [7:0]Q // Output data bus (MCU can read valid data on this
//port in time 200uS after data latch Negative Pulse received)
, output nSTRCV, //Receive start, negative pulse - Output
output OCPYi, //Occupancy packet received - Output
output nRST, //Reset packet received - Output
output FERR, //Frame error - Output
output EOPi, //End Of Packed received - Output
output nDLatch //New Data latched on BUS (Negative Pulse)
);
/*Control Register*/
reg rRxD = 0;
assign RxD = rRxD;
reg rFERR = 0;
assign FERR = rFERR;
wire nWE;
assign nDLatch = (EOPi & nWE);
wire iFERR;
/* Align Data Packet */
reg rENA = 1'b0;
reg rENB = 1'b0;
always @(posedge GCLK or negedge nRST) begin
if(!nRST) begin
rENA <= 1'b0;
rENB <= 1'b0;
end else begin
rENA <= SDCKAi;
rENB <= SDCKBi;
end
end
always @(posedge GCLK or negedge nRST) begin
if(!nRST) begin
rFERR <= 0;
rRxD <= 0;
end else begin
if(!EOPi)// && !INHTxD)
rRxD <=0 ;
else begin
if(!iFERR) rFERR <= 1;
if(!nSTRCV) begin
rFERR <= 0;
rRxD <= ~INHTxD;
end
end
end
end
line_monitor line_monitor
(
.GCLK(GCLK), //Global Clock - Input
.SDCKA(SDCKAi|INHTxD), //CLOCK/DATA Line A disabled by data transmit - Input
.SDCKB(SDCKBi|INHTxD), //CLOCK/DATA Line B disabled by data transmit - Input
.RxDr(RxD), //Data Receive in progress - Input
.RxD(nSTRCV), //Receive start, negative pulse - Output
.OCPY(OCPYi), //Occupancy packet received - Output
.RESET(nRST), //Reset packet received - Output
.FERR(iFERR), //Frame error - Output
.EOP(EOPi), //End Of Packed received - Output
.ENA(rENA), //CLOCK For Line B
.ENB(rENB) //CLOCK For Line A
);
/*Receive Maple Frame*/
maple_receive maple_receive
(
.SDCKA(SDCKAi), //CLOCK/DATA Line A disabled by data transmit - Input
.SDCKB(SDCKBi), //CLOCK/DATA Line B disabled by data transmit - Input
.ENA(rENA), //CLOCK For Line B
.ENB(rENB), //CLOCK For Line A
.RCV(RxD), //Receive in progress, 1 - receive - Input
.Dout(Q[7:0]), //Received data byte - Output
.nWE(nWE), //Write Latch - Output
.RxDi(nSTRCV), //Receive start, negative pulse - Input
.INHTxD(INHTxD) //Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA)
);
endmodule
line_monitor.v
module line_monitor
(
input GCLK,
input SDCKA,
input SDCKB,
input RxDr, //Data Receive in progress - Input
output RxD,
output OCPY,
output RESET,
output FERR,
output EOP,
input ENA,
input ENB
);
reg [3:0] countA = 0;
reg [2:0] countB = 0;
reg [3:0] pcount = 0;
reg rEOP = 1'b1;
assign EOP = rEOP;
assign RxD = (pcount == 4'h4) ? 1'b0 : 1'b1;
assign OCPY = (pcount == 4'h8) ? 1'b0 : 1'b1;
assign RESET = (pcount == 4'hE)? 1'b0 : 1'b1; //Output reset signal does not need to check for FERR
assign FERR = (!((RxD & OCPY & RESET) && pcount[3:1])) | (!RxDr & !rEOP);
//assign EOP = (eopcount == 3'h2) ? 1'b0 : 1'b1;
always @(posedge SDCKA) pcount <= countA;
always @(posedge SDCKB) rEOP <= !(countB == 3'h2);
//Patterns
//PATTERN Counter Managing
always @(posedge ENA or negedge ENB) begin
if (ENA) begin
countA <= 0;
end
else begin
countA <= countA + 4'h1;
end
end
//EOP Counter Managing
always @(posedge ENB or negedge ENA) begin
if (ENB) begin
countB <= 0;
end
else begin
countB <= countB + 3'h1;
end
end
//synopsys translate_off
//synopsys translate_on
endmodule
maple_receive.v
module maple_receive
(
input SDCKA, //CLOCK/DATA Line A
input SDCKB, //CLOCK/DATA Line B
input ENA, //CLOCK
input ENB, //CLOCK
input RCV, //Receive in progress, 1 - valid
output [7:0]Dout, //received data output
output nWE,
input RxDi,
input INHTxD //Inhibit Input Data (User Can disable XMAPLE Detect Signals While MCU Transmit DATA)
);
reg [3:0] dataA = 4'h0;
reg [3:0] dataB = 4'h0;
reg [1:0]countB = 2'b00;
reg rLastBitCounted = 1'b1;
//B LINE
//Dout[1] = SDCKA Means Major version 1.
//Dout[0] = SDCKB Means Minor version .0
//And version result = 1.0
assign Dout[1] = !INHTxD ? dataB[0] : SDCKA;
assign Dout[3] = dataB[1];
assign Dout[5] = dataB[2];
assign Dout[7] = dataB[3];
//A LINE
assign Dout[0] = !INHTxD ? dataA[0] : SDCKB;
assign Dout[2] = dataA[1];
assign Dout[4] = dataA[2];
assign Dout[6] = dataA[3];
assign nWE = (dtaLock);
always @(negedge ENA)begin
dataB[3:1] <= dataB[2:0];
dataB[0] <= SDCKB;
if(RCV) begin
countB <= countB + 2'b1;
end else begin
countB <= 2'b11;
end
end
always @(negedge ENB)begin
dataA[3:1] <= dataA[2:0];
dataA[0] <= SDCKA;
rLastBitCounted <= !countB[0] | !countB[1];
end
wire dtaLock = rLastBitCounted;
endmodule
Чтобы не "развлекаться с проводочками" накидал Eval Board.
Общий вид по блокам:
![](https://habrastorage.org/getpro/habr/upload_files/958/284/4df/9582844df041fce44ceaa415b7bcd591.png)
Внешний вид:
![](https://habrastorage.org/getpro/habr/upload_files/278/675/08d/27867508dcb88e3be5829a88ea2c06b0.jpg)
И посадочное место под Eval...
![](https://habrastorage.org/getpro/habr/upload_files/135/25e/609/13525e60973628ac979e36a6d97927aa.png)
И соединяем всё это вместе:
![](https://habrastorage.org/getpro/habr/upload_files/492/12d/7ac/49212d7ac3c90ba8008489fa384afada.jpg)
Реализация устройства
Железки есть, схемы есть, переходим к реализации.
Для начала заделаем небольшой код чтобы чтобы геймпад Dreamcast прикидывался геймпадом XBOX360 (поскольку я заботливо "выкусил хэндшейк" с XBOX360, данная реализация на приставке работать не будет только на ПК).
И, опять таки чтобы не на проводках, делаем плату коннектора для джойстика из двух половинок:
Верхняя часть (GERBER), нижняя часть (GERBER).
![](https://habrastorage.org/getpro/habr/upload_files/5ed/b23/f58/5edb23f58fd1ced5025fe03da0acca6b.jpg)
Разумеется чтобы получить хороший контакт с разъемом геймпада, нужно припаять "усы".
Для этой цели можно к примеру разобрать разъем SD вот как-то так:
![](https://habrastorage.org/getpro/habr/upload_files/812/3da/9b6/8123da9b68c9b06d36be09b404cca232.jpg)
![](https://habrastorage.org/getpro/habr/upload_files/6ed/a9d/432/6eda9d432e72e89b75ef5c9aea013a4b.jpg)
Прикинем как должен работать алгоритм... не буду останавливаться на том как работает USB HID, опишу общую схему опроса устройств на шине MAPLE.
![](https://habrastorage.org/getpro/habr/upload_files/704/014/bd2/704014bd28d0a201ffd8e7667acb3e2a.png)
И собственно архив с исходниками.
Компилируем определив константы:
USE_STDPERIPH_DRIVER - использовать стандартную библиотеку периферии от ST.
STM32F10X_MD - чип Medium Density.
MAPLE_HOST - библиотека MAPLE работает в режиме HOST.
USB_HID - собрать целевое HID устройство.
Прошиваем, подключаемся:
![](https://habrastorage.org/getpro/habr/upload_files/d89/2ea/b5d/d892eab5de60f305b7fd547668093daa.jpg)
… и видим вот такую картину (не забываем что необходимо поставить Microsoft Xbox 360 Accessories, а ещё помним что геймпад у нас работает в режиме XInput... кому лень разбираться, можно скачать уже откомпилированную прошивку):
![](https://habrastorage.org/getpro/habr/upload_files/c20/0ac/1d7/c200ac1d7c937f03fa28a3d0439582cf.jpg)
А теперь развернем xMAPLE в обратную сторону и...
Подключим мышь от ПК к DREAMCAST.
Мышь, - FT9 : Pointing Function.
Что нам нужно, DeviceID и состав команды GetCondition, чтоб собирать пакет с данными.
Mouse DeviceID:
![](https://habrastorage.org/getpro/habr/upload_files/f55/978/f30/f55978f30caf009b721a220f2e6131e3.png)
Стандартная мышь Dreamcast содержит 3 кнопки: A,B,W, дельты смещения по осям X/Y: AC1,AC2 (ball) и смещение "колеса": AC3 (wheel).
AC1,AC2,AC3 - десяти-битные величины плюс флаг переполнения.
Вот так выглядит пакет данных:
![](https://habrastorage.org/getpro/habr/upload_files/046/b20/ee9/046b20ee96ec67a8d710ef2bc92b194d.png)
AOV2, AOV1, AOV0 - флаги переполнения для AC3, AC2, AC1 соответственно.
Для удобства накидаем схемку адаптера PS/2 для нашей борды:
![](https://habrastorage.org/getpro/habr/upload_files/8d2/3b6/841/8d23b68416c3ab76c0de131d6c54b943.jpg)
...разводим, получаем gerber'ы...
И с завода нам приезжает вот это:
![](https://habrastorage.org/getpro/habr/upload_files/322/b4f/ca3/322b4fca3c14de5fadd2bb94ba5f2952.jpg)
Ну и чтобы совсем удобно, накидаем вот такую схему, если брать провод от оригинального пада, то просто подключаемся к разъему и УРА.
![](https://habrastorage.org/getpro/habr/upload_files/7d1/473/13b/7d147313bbede2a80f35aaeff820ada7.jpg)
"Рисуем" gerber'ы и получаем вот такой переходник:
![](https://habrastorage.org/getpro/habr/upload_files/492/31c/d88/49231cd886a13e18f3d54d0705c5695a.jpg)
Собираем весь этот "огород" вместе:
![](https://habrastorage.org/getpro/habr/upload_files/801/a08/c99/801a08c9963adb5a28cb8a397d79390d.jpg)
Компилируем прошивку (ниже архив с исходниками) не забывая объявить константы препроцессора:
USE_STDPERIPH_DRIVER - использовать стандартную библиотеку периферии от ST.
STM32F10X_MD - чип Medium Density.
MAPLE_DEVICE - библиотека MAPLE работает в режиме DEVICE.
EN_MOUSE - собрать целевое - MOUSE.
MOUSE_CALLBACK - обработать функцию чтения мыши в процессе ожидания запроса от HOST.
EXTI9_5_CALLBACK - передавать в код пользователя системные прерывания EXTI5-EXTI9 библиотеки MAPLE_BUS.
(исходники, скомпилированный HEX).
К слову, если вместо EN_MOUSE в данных исходниках определить константу EN_CONTROLLER, то мы получим довольно забавную штуку, переходник превращающий PS/2 мышь в контроллер DREAMCAST, собственно специально сделал, потому как мышью в меню DREAMCAST управлять нельзя. Поэтому чтобы наглядно увидеть работоспособность исходников и оборудования в целом не запуская скажем "HALF LIFE для проверки" можно прошить откомпилированный код с константой EN_CONTROLLER и управлять внутри меню мышкой PS/2.
Прошиваем, подключаемся к DREAMCAST и оно работает!!!
![](https://habrastorage.org/getpro/habr/upload_files/8ef/5f0/105/8ef5f01059593594f285ebb62675890c.jpg)
Вот собственно и всё что хотел поведать. Однако я не рассказал о (надеюсь ещё расскажу :) ):
Как работать с VibroPAK.
Как реализовать Memory Unit (хотя на борде расширения PS/2 SPI EEPROM память можно установить и работать с ней).
И у меня остались комплекты печатных плат и трём желающим "попробовать свои силы" могу отправить комплекты печатных плат за стоимость почты.
Удачного дня! Отличного настроения и взаимопонимания!!!