J12 Foot Controller Reverse Engineering

Published 14th Apr 2013

The J12 foot-controller is a very heavily built device, and IMHO one of the best guitar foot-controllers out there. Unfortunately it uses the proprietary Digitech protocol, even though it has the standard MIDI connector. Information of the Digitech protocol is literally non existent, so I decided to have a closer look.


Starting with taking it apart I hooked up some wires and began measuring the serial bus pulse width. Unfortunately I only had a multimeter with very limited scope capabilities at that time but measuring pulses was still surprisingly accurate. What I found out was:

low ........ 0.4V
high ....... 4.5V
bps ........ 10416
bits ....... 8
parity ..... N
stopbits ... 1

Next on the list was the software protocol. The problem here is the rather odd baudrate of 10416 bps. Again I was limited by insuficient tools. This time a cheap RS232->USB converter. The closest baudrate I could set was 9600 bps ,so half of the received bytes were plain wrong.


I immediatly ordered a FTDI FT232RL converter to build a suitable sniffer. I'm confident to have a complete protocol spec by end of April. We'll see...

Edit: 2013/04/27

The FT232RL worked quite well. Nevertheless setting the PORT to 10416 is a bit of a hack. The following script will set a custom divisor for 38400 baud to get a real baudrate of 10416:

#!/bin/bash

PORT="/dev/ttyUSB1"

stty -F $PORT 38400
setserial $PORT spd_cust
setserial $PORT divisor 2304

The divisor is calculated like:

baudbase/baudrate = 24000000/10416 = 2304.14

hexdump can be used as a very basic sniffer:

hexdump -v -e '/1 "%ad: ""\t""    "' -e '/1    "0x%02X"' \
-e '/1 " = %03u "' -e '/1 " = _%_u\\n"' /dev/ttyUSB1


Just for the fun of it I decided to write a dedicated protocol sniffer. Because of the non standard baudrate it uses the termios2 struct to set the port speed. The C source is here, the full sourcecode is available on github.

#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>

#include <stropts.h>
#include <asm/termios.h>

#define SERIAL_OK           0 ///< no error
#define SERIAL_ERR         -1 ///< unknown error
#define SERIAL_ERR_OPEN    -2 ///< error while opening the serial port
#define SERIAL_ERR_READ    -3 ///< reading from port failed
#define SERIAL_ERR_WRITE   -4 ///< could not write to port
#define SERIAL_ERR_INIT    -5 ///< parameter mismatch error
#define SERIAL_ERR_TIMEOUT -6 ///< read did not complete in time

static int fd = -1;

static struct termios2 settings;

static unsigned char exp[2][2];
static unsigned char btn[12];

int tcdrain(int fd);
int tcflush(int fd, int queue_selector);

int
SerialOpen(const char *device) {
    // open port
    if ((fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY)) < 0) {
        return (SERIAL_ERR_OPEN);
    }

    // read current termios
    if (ioctl(fd, TCGETS2, &settings)) {
        return (SERIAL_ERR_OPEN);
    }

    return (SERIAL_OK);
}

void
SerialClose(void) {
    close(fd);
}

int
SerialFlush(void) {
    if (fd < 0) return (SERIAL_ERR);

    tcflush(fd, TCIOFLUSH);

    return (SERIAL_OK);
}

int
SerialDrain(void) {
    if (fd < 0) return (SERIAL_ERR);

    tcdrain(fd);

    return (SERIAL_OK);
}

int
SerialSetTimeout(int ms) {
    if (ms < 0) {
        settings.c_cc[VMIN]  = 0;
        settings.c_cc[VTIME] = 0;
    } else {
        settings.c_cc[VMIN]  = (ms) ? 0 : 1;
        settings.c_cc[VTIME] = (ms + 99) / 100; // recalculate from ms to ds
    }

    if (ioctl(fd, TCSETS2, &settings)) {
        return (SERIAL_ERR_INIT);
    }

    return (SERIAL_OK);
}

int
SerialInit(int baud, const char *format, int rtscts) {
    tcflag_t cflags = settings.c_cflag;
    speed_t speed = 0;

    if (fd < 0) return (SERIAL_ERR_INIT);

    if ((!format) || (strlen(format) != 3)) {
        return (SERIAL_ERR_INIT);
    }

    // we need those anyway
    cflags |= CLOCAL | CREAD;

    // datasize
    switch (format[0]) {
        case '5': cflags |= CS5; break;
        case '6': cflags |= CS6; break;
        case '7': cflags |= CS7; break;
        case '8': cflags |= CS8; break;
        default: return (SERIAL_ERR_INIT);
    }

    // parity
    switch (format[1]) {
        case 'N': break;                         // no parity
        case 'O': cflags |= PARODD; // no break! // odd parity
        case 'E': cflags |= PARENB; break;       // even parity
        default: return (SERIAL_ERR_INIT);
    }

    // stopbit
    switch (format[2]) {
        case '1': break;                   // 1 stopbit
        case '2': cflags |= CSTOPB; break; // 2 stopbits
        default: return (SERIAL_ERR_INIT);
    }

    // baudrate
    switch (baud) {
        case 115200: cflags |= B115200; break;
        case  57600: cflags |= B57600;  break;
        case  38400: cflags |= B38400;  break;
        case  19200: cflags |= B19200;  break;
        case   9600: cflags |= B9600;   break;
        case   4800: cflags |= B4800;   break;
        case   2400: cflags |= B2400;   break;
        case    300: cflags |= B300;    break;
        default:
            cflags &= ~CBAUD;
            cflags |= BOTHER;
            speed   = baud;
        break;
    }

    // handshake
    if (rtscts) {
        cflags |= CRTSCTS; // hardware handshake
    }

    // set port parameters
    settings.c_cflag = cflags;
    settings.c_iflag = IGNPAR;
    settings.c_ispeed = speed;
    settings.c_ospeed = speed;
    settings.c_cc[VMIN]  = 0;
    settings.c_cc[VTIME] = 0;

    // this finally initializes the driver
    if (ioctl(fd, TCSETS2, &settings)) {
        return (SERIAL_ERR_INIT);
    }

    // read back settings, so we know what baudrate really was set
    if (ioctl(fd, TCGETS2, &settings)) {
        return (SERIAL_ERR_INIT);
    }

    SerialFlush();

    return (SERIAL_OK);
}

int
SerialSendBuffer(const void *buf, unsigned int len) {
    int written;

    if (fd < 0) return (SERIAL_ERR_WRITE);

    while (len > 0) {
        written = write(fd, buf, len);
        if (written < 0) {
            if (errno == EINTR) continue;
            return (SERIAL_ERR_WRITE);
        }
        len -= written;
        buf = (char *)buf + written;
    }

    return (SERIAL_OK);
}

int
SerialSendByte(unsigned char b) {
    return (SerialSendBuffer(&b, 1));
}

int
SerialReceiveBuffer(void *buf, unsigned int *len, int timeout) {
    unsigned int length = 0;
    int received;

    if (fd < 0) return (SERIAL_ERR_READ);

    SerialSetTimeout(timeout);

    while (*len > 0) {
        received = read(fd, buf, *len);

        if (received > 0) length += received;

        if (received < 0) {
            if (errno == EINTR) continue;

            *len = length;

            return (SERIAL_ERR_READ);
        } else if (received == 0) {
            *len = length;

            if (timeout < 0) return (SERIAL_OK);

            return (SERIAL_ERR_TIMEOUT);
        }

        *len -= received;
        buf = (char *)buf + received;
    }

    *len = length;

    return (SERIAL_OK);
}

int
SerialReceiveByte(unsigned char *c, int timeout) {
    unsigned int len = 1;

    return (SerialReceiveBuffer(c, &len, timeout));
}

void
ReadPedal(int pedal) {
    unsigned int len = 2;

    int err = SerialReceiveBuffer(exp[pedal], &len, 0);

    if (err) {
        printf("\rError %i while reading pedal %i", err, pedal);
        fflush(stdout); sleep(1);
    }
}

void
ReadButton(int button) {
    if (button == 0x0F) {
        memset(btn, 0, sizeof (btn));
    } else {
        btn[button] = 1;
    }
}

int
main(int argc, char **argv) {
    const char *port = "/dev/ttyUSB1";
    unsigned char cmd;
    int i, err;

    if (argc >= 2) port = argv[1];

    if (SerialOpen(port)) {
        printf("Can't open serial device\n");

        return (-1);
    }

    if (SerialInit(10416, "8N1", 0)) {
        printf("Can't initialize serial device\n");

        return (-1);
    }

    printf("initialized %s with %i bps\n\n", port, settings.c_ospeed);

    memset(btn, 0, sizeof (btn));
    memset(exp, 0, sizeof (exp));

    while (1) {
        // timeout: 0 .. infinite, -1 .. non blocking
        err = SerialReceiveByte(&cmd, 1);

        if (err) {
            printf("\rError %i while reading command    ", err);
            fflush(stdout); sleep(1);
        }

        switch (cmd & 0xF0) {
            case 0xE0:  ReadPedal(cmd & 0x01); break;
            case 0xF0: ReadButton(cmd & 0x0F); break;
        }

        printf("\r");
        printf("CMD:%02X ", cmd);
        printf("Exp1:%02X%02X ", exp[0][0], exp[0][1]);
        printf("Exp2:%02X%02X ", exp[1][0], exp[1][1]);
        printf("Button:");

        for (i=0; i<12; i++) {
            btn[i] ? printf("*") : printf(" ");
        }

        fflush(stdout); usleep(10000);
    }

    SerialClose();  

    return (0);
}

Having a dedicated sniffer program made the whole process a lot easier. Not much later I had reversed the complete protocol.

Johnson J12 (Digitech) Foot Controller
Protocol Specification 1.0 (reverse engineered)
clemens@1541.org

=============================================

Hardware Spec:

Serial: 10416/8N1

0.4V .. low
4.5V .. high

=============================================

Software Spec:

Startup:

Amp -> J12: 0x80, 0xC0, 0XC6, 0x80, 0xC0, 0xC6
J12 <- Amp: 0x00, 0x00, 0xFE

Amp -> J12
------------------

Text:

0x80 + 20 ASCII Characters

Leds:

0xA#: Led ON
0xB#: Led OFF

0xC0: all leds off

=============================================

J12 -> Amp
------------------

Expression Pedal:

Left:  0xE0 0x?? 0x?? (typ: 1E14-3EFF)
Right: 0xE1 0x?? 0x?? (typ: 1938-3C20)

Buttons:

0xFF ... Release
0xF# ... Press

=============================================

Button and LED numbers:

#       Label       Key
------------------------------------
0x0B    Bank Down   (left)
0x0A    Bank Up     (right)

0x05    (Patch 1)   1
0x06    (Patch 2)   2
0x07    (Patch 3)   3
0x08    (Patch 4)   4
0x09    (Patch 5)   5

0x00    Channel     6
0x01    Effect 1    7
0x02    Effect 2    8
0x03    Effect 3    9
0x04    Effect 4    0


So, all by itself, reversing the protocol is of not much use. But it is the foundation for more advanced alterations. For example we could build a J12 to MIDI converter or remote control our DAW for home recording. Let's see what time will bring.

comments powered by Disqus