You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
512 lines
13 KiB
512 lines
13 KiB
/* |
|
* Pinephone keyboard I2C flashing tool. |
|
* |
|
* Copyright (C) 2021 Ondřej Jirman <megi@xff.cz> |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU General Public License as published by |
|
* the Free Software Foundation, either version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
*/ |
|
|
|
#include "common.c" |
|
#include "firmware/registers.h" |
|
|
|
static int iic_fd = -1; |
|
|
|
static void dump_log(void) |
|
{ |
|
int ret; |
|
uint8_t addr = 0xff; |
|
uint8_t buf[64]; |
|
struct i2c_msg msgs[] = { |
|
{ KB_ADDR, 0, 1, &addr }, // set 0xff address |
|
{ KB_ADDR, I2C_M_RD, sizeof(buf), buf }, |
|
}; |
|
|
|
struct i2c_rdwr_ioctl_data msg = { |
|
.msgs = msgs, |
|
.nmsgs = sizeof(msgs) / sizeof(msgs[0]) |
|
}; |
|
|
|
ret = ioctl(iic_fd, I2C_RDWR, &msg); |
|
syscall_error(ret < 0, "I2C_RDWR failed"); |
|
|
|
int i; |
|
for (i = 0; i < sizeof(buf) && buf[i]; i++); |
|
|
|
if (i > 0) |
|
xwrite(1, buf, i); |
|
} |
|
|
|
static void wr_buf(uint8_t addr, uint8_t* buf, size_t size) |
|
{ |
|
int ret; |
|
uint8_t mbuf[size + 1]; |
|
|
|
mbuf[0] = addr; |
|
memcpy(mbuf + 1, buf, size); |
|
|
|
struct i2c_msg msgs[] = { |
|
{ KB_ADDR, 0, size + 1, mbuf }, |
|
}; |
|
|
|
struct i2c_rdwr_ioctl_data msg = { |
|
.msgs = msgs, |
|
.nmsgs = sizeof(msgs) / sizeof(msgs[0]) |
|
}; |
|
|
|
debug("WR[%02hhx]:", addr); |
|
for (int i = 0; i < size; i++) |
|
debug(" %02hhx", buf[i]); |
|
debug("\n"); |
|
|
|
ret = ioctl(iic_fd, I2C_RDWR, &msg); |
|
syscall_error(ret < 0, "I2C_RDWR failed"); |
|
} |
|
|
|
static void rd_buf(uint8_t addr, uint8_t* buf, size_t size) |
|
{ |
|
int ret; |
|
struct i2c_msg msgs[] = { |
|
{ KB_ADDR, 0, 1, &addr }, |
|
{ KB_ADDR, I2C_M_RD, size, buf }, |
|
}; |
|
|
|
struct i2c_rdwr_ioctl_data msg = { |
|
.msgs = msgs, |
|
.nmsgs = sizeof(msgs) / sizeof(msgs[0]) |
|
}; |
|
|
|
ret = ioctl(iic_fd, I2C_RDWR, &msg); |
|
syscall_error(ret < 0, "I2C_RDWR failed"); |
|
|
|
debug("RD[%02hhx]:", addr); |
|
for (int i = 0; i < size; i++) { |
|
if (i % 8 == 0 && i > 0) |
|
debug("\n "); |
|
debug(" %02hhx", buf[i]); |
|
} |
|
debug("\n"); |
|
} |
|
|
|
static int rd_buf_nofail(uint8_t addr, uint8_t* buf, size_t size) |
|
{ |
|
int ret; |
|
struct i2c_msg msgs[] = { |
|
{ KB_ADDR, 0, 1, &addr }, |
|
{ KB_ADDR, I2C_M_RD, size, buf }, |
|
}; |
|
|
|
struct i2c_rdwr_ioctl_data msg = { |
|
.msgs = msgs, |
|
.nmsgs = sizeof(msgs) / sizeof(msgs[0]) |
|
}; |
|
|
|
ret = ioctl(iic_fd, I2C_RDWR, &msg); |
|
|
|
if (ret == 2) { |
|
debug("RD[%02hhx]:", addr); |
|
for (int i = 0; i < size; i++) { |
|
if (i % 8 == 0 && i > 0) |
|
debug("\n "); |
|
debug(" %02hhx", buf[i]); |
|
} |
|
debug("\n"); |
|
} |
|
|
|
return ret == 2 ? 0 : -1; |
|
} |
|
|
|
static uint8_t rd_reg(uint8_t addr) |
|
{ |
|
uint8_t reg; |
|
rd_buf(addr, ®, 1); |
|
return reg; |
|
} |
|
|
|
static int wait_flash_cmd_done(uint8_t cmd) |
|
{ |
|
int ret; |
|
|
|
for (int i = 0; i < 10; i++) { |
|
uint8_t status; |
|
|
|
ret = rd_buf_nofail(REG_FLASH_CMD, &status, 1); |
|
if (ret == 0) { |
|
if (status == 0xffu) |
|
error("Flashing command 0x%02hhx failed", cmd); |
|
else if (status == 0x00) |
|
return 0; |
|
} |
|
|
|
usleep(5000); |
|
} |
|
|
|
error("Flashing command 0x%02hhx timed out", cmd); |
|
} |
|
|
|
static void read_rom_block(uint8_t* out, uint16_t addr) |
|
{ |
|
uint8_t read_rom_start[] = { |
|
addr & 0xff, // Addr L |
|
(addr >> 8) & 0xff, // Addr H |
|
0x00, // CRC-8 |
|
REG_FLASH_UNLOCK_MAGIC, // Unlock |
|
REG_FLASH_CMD_READ_ROM, // Read ROM |
|
}; |
|
|
|
wr_buf(REG_FLASH_ADDR_L, read_rom_start, sizeof read_rom_start); |
|
|
|
wait_flash_cmd_done(REG_FLASH_CMD_READ_ROM); |
|
|
|
rd_buf(REG_FLASH_DATA_START, out, 128); |
|
|
|
if (rd_reg(REG_FLASH_CRC8) != crc8(out, 128)) |
|
error("CRC8 failure on ROM read"); |
|
} |
|
|
|
static void write_rom_block(uint16_t addr, uint8_t* data) |
|
{ |
|
uint8_t write_rom_start[5] = { |
|
addr & 0xff, // Addr L |
|
(addr >> 8) & 0xff, // Addr H |
|
crc8(data, 128), // CRC-8 |
|
REG_FLASH_UNLOCK_MAGIC, // Unlock |
|
REG_FLASH_CMD_WRITE_ROM, // Write ROM |
|
}; |
|
|
|
wr_buf(REG_FLASH_DATA_START, data, 128); |
|
wr_buf(REG_FLASH_ADDR_L, write_rom_start, sizeof write_rom_start); |
|
|
|
usleep(5000); |
|
wait_flash_cmd_done(REG_FLASH_CMD_WRITE_ROM); |
|
} |
|
|
|
static void run_flash_cmd(uint8_t cmd) |
|
{ |
|
uint8_t cmd_data[] = { |
|
REG_FLASH_UNLOCK_MAGIC, // Unlock |
|
cmd, // Command |
|
}; |
|
|
|
wr_buf(REG_FLASH_UNLOCK, cmd_data, sizeof cmd_data); |
|
|
|
usleep(10000); |
|
wait_flash_cmd_done(cmd); |
|
} |
|
|
|
static bool is_block_empty(uint8_t* buf) |
|
{ |
|
for (unsigned i = 0; i < 128; i++) |
|
if (buf[i] != 0xff) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
static int is_kb_stock_connected(void) |
|
{ |
|
uint8_t devid[5]; |
|
int ret; |
|
|
|
ret = rd_buf_nofail(REG_DEVID_K, devid, sizeof devid); |
|
if (ret) |
|
return 0; |
|
|
|
if (devid[REG_DEVID_K] != 'K' || devid[REG_DEVID_B] != 'B') // keyboard firmware magic |
|
return 0; |
|
if (!(devid[REG_FW_FEATURES] & REG_FW_FEATURES_STOCK_FW)) // stock firmware flag |
|
return 0; |
|
|
|
if (devid[REG_FW_REVISION] != 0x10) |
|
error("Unsupported stock pinephone keyboard firmware version %02hhx, expecting 0x10\n", devid[2]); |
|
if (!(devid[REG_FW_FEATURES] & REG_FW_FEATURES_FLASHING_MODE)) |
|
error("Your stock pinephone keyboard firmware doesn't have flashing support\n"); |
|
|
|
return 1; |
|
} |
|
|
|
static void usage(void) |
|
{ |
|
printf( |
|
"Usage: ppkb-i2c-flasher [--rom-in <path>] [--rom-out <path>] [--verbose]\n" |
|
" [--help] [<read|write|erase|info|reset>...]\n" |
|
"\n" |
|
"Options:\n" |
|
" -i, --rom-in <path> Specify path to binary file you want to flash.\n" |
|
" -o, --rom-out <path> Specify path where you want to store the contents\n" |
|
" of code ROM read from the device.\n" |
|
" -s, --size <size> Specify how many bytes of code rom to flash\n" |
|
" starting from offset 0x4000 in the rom file.\n" |
|
" -e, --entry <manual|i2c|none>\n" |
|
" Specify how to enter the stock firmware:\n" |
|
" - manual: Ask the user to power-cycle the keyboard\n" |
|
" - i2c: Send I2C command to make supporting user\n" |
|
" - none: Assume stock firmware is already running\n" |
|
" -v, --verbose Show details of what's going on.\n" |
|
" -h, --help This help.\n" |
|
"\n" |
|
"Commands:\n" |
|
" info Display information about the current firmware.\n" |
|
" read Read ROM from the device to --rom-out file.\n" |
|
" write Flash ROM file to the device from --rom-in.\n" |
|
" erase Erase the user firmware.\n" |
|
" reset Perform software reset of the MCU.\n" |
|
" usbiap Restart to USB IAP mode.\n" |
|
"\n" |
|
"Format of the ROM files is a flat binary. Only the part of it starting\n" |
|
"from 0x4000 will be flashed. Use -s to specify how many bytes to write.\n" |
|
"The stock firmware between 0x2000 and 0x4000 will be preserved.\n" |
|
"\n" |
|
"Pinephone keyboard I2C flashing tool " VERSION "\n" |
|
"Written by Ondrej Jirman <megi@xff.cz>, 2021\n" |
|
"Licensed under GPLv3, see https://xff.cz/git/pinephone-keyboard/ for\n" |
|
"more information.\n" |
|
); |
|
|
|
exit(2); |
|
} |
|
|
|
int main(int ac, char* av[]) |
|
{ |
|
char* rom_in = NULL; |
|
char* rom_out = NULL; |
|
char* entry_type = "i2c"; |
|
int size = 0x1200; |
|
int ret; |
|
|
|
while (1) { |
|
int option_index = 0; |
|
struct option long_options[] = { |
|
{ "rom-in", required_argument, 0, 'i' }, |
|
{ "rom-out", required_argument, 0, 'o' }, |
|
{ "size", required_argument, 0, 's' }, |
|
{ "entry", required_argument, 0, 'e' }, |
|
{ "verbose", no_argument, 0, 'v' }, |
|
{ "help", no_argument, 0, 'h' }, |
|
{ 0, 0, 0, 0 } |
|
}; |
|
|
|
int c = getopt_long(ac, av, "i:o:s:e:vh", long_options, &option_index); |
|
if (c == -1) |
|
break; |
|
|
|
switch (c) { |
|
case 'o': |
|
rom_out = strdup(optarg); |
|
break; |
|
case 'i': |
|
rom_in = strdup(optarg); |
|
break; |
|
case 'e': |
|
entry_type = strdup(optarg); |
|
break; |
|
case 's': |
|
if (strstr(optarg, "0x") == optarg) { |
|
errno = 0; |
|
char* next = NULL; |
|
size = strtol(optarg + 2, &next, 16); |
|
if (errno || next == optarg + 2) { |
|
printf("ERROR: Can't parse --size %s\n\n", optarg); |
|
usage(); |
|
} |
|
} else { |
|
errno = 0; |
|
char* next = NULL; |
|
size = strtol(optarg, &next, 10); |
|
if (errno || next == optarg) { |
|
printf("ERROR: Can't parse --size %s\n\n", optarg); |
|
usage(); |
|
} |
|
} |
|
break; |
|
case 'v': |
|
verbose = 1; |
|
break; |
|
case 'h': |
|
case '?': |
|
default: |
|
usage(); |
|
break; |
|
} |
|
} |
|
|
|
if (optind == ac) |
|
usage(); |
|
|
|
if (size < 128) { |
|
printf("ERROR: --size 0x%04x too small\n\n", size); |
|
usage(); |
|
} |
|
|
|
if (size > 0x4000) { |
|
printf("ERROR: --size 0x%04x too large\n\n", size); |
|
usage(); |
|
} |
|
|
|
if (size % 128 != 0) { |
|
printf("ERROR: --size 0x%04x is not multiple of 128\n\n", size); |
|
usage(); |
|
} |
|
|
|
for (int i = optind; i < ac; i++) { |
|
if (!strcmp(av[i], "read")) { |
|
if (!rom_out) { |
|
printf("ERROR: You must specify target file to write rom contents to via --rom-out\n\n"); |
|
usage(); |
|
} |
|
} else if (!strcmp(av[i], "write")) { |
|
if (!rom_in) { |
|
printf("ERROR: You must source file for flashing via --rom-in\n\n"); |
|
usage(); |
|
} |
|
} else if (!strcmp(av[i], "info")) { |
|
; |
|
} else if (!strcmp(av[i], "reset")) { |
|
; |
|
} else if (!strcmp(av[i], "erase")) { |
|
; |
|
} else if (!strcmp(av[i], "usbiap")) { |
|
; |
|
} else { |
|
printf("ERROR: Unknown command: %s\n\n", av[i]); |
|
usage(); |
|
} |
|
} |
|
|
|
printf("Opening keyboard I2C device\n"); |
|
iic_fd = pogo_i2c_open(); |
|
|
|
if (!is_kb_stock_connected()) { |
|
if (!strcmp(entry_type, "i2c")) { |
|
// send MCU reset command |
|
uint8_t cmd[] = {REG_SYS_COMMAND_MCU_RESET}; |
|
wr_buf(REG_SYS_COMMAND, cmd, sizeof cmd); |
|
|
|
// stock firmware should report itself quickly |
|
// |
|
// tell firmware to block enrty to user app (we have 1s |
|
// window to do this after reset) |
|
int i; |
|
for (i = 0; i < 10; i++) { |
|
if (is_kb_stock_connected()) { |
|
uint8_t cmd[] = {REG_SYS_USER_APP_BLOCK_MAGIC}; |
|
wr_buf(REG_SYS_USER_APP_BLOCK, cmd, sizeof cmd); |
|
break; |
|
} |
|
|
|
usleep(250000); |
|
} |
|
|
|
if (i == 10) |
|
error("Reset command issued over I2C failed, stock firmware failed to report itself within 2.5s"); |
|
} else if (!strcmp(entry_type, "manual")) { |
|
printf("Please power cycle the keyboard by removing and reinserting the pinephone.\n"); |
|
|
|
while (true) { |
|
if (is_kb_stock_connected()) { |
|
uint8_t cmd[] = {REG_SYS_USER_APP_BLOCK_MAGIC}; |
|
wr_buf(REG_SYS_USER_APP_BLOCK, cmd, sizeof cmd); |
|
break; |
|
} |
|
|
|
usleep(250000); |
|
} |
|
} else if (!strcmp(entry_type, "none")) { |
|
error("Stock pinephone keyboard firmware not detected running on the keyboard\n"); |
|
} else { |
|
error("Unknown entry method %s", entry_type); |
|
} |
|
|
|
// if after 1s the stock firmware is still running, |
|
// everything is ok |
|
usleep(1000000); |
|
if (!is_kb_stock_connected()) |
|
error("Failed to block the user app from running"); |
|
} |
|
|
|
for (int i = optind; i < ac; i++) { |
|
if (!strcmp(av[i], "read")) { |
|
printf("Reading code ROM\n"); |
|
uint8_t rom[0x8000]; |
|
for (unsigned i = 0; i < sizeof(rom); i += 128) |
|
read_rom_block(rom + i, i); |
|
|
|
int fd = open(rom_out, O_WRONLY | O_CREAT | O_TRUNC, 0666); |
|
if (fd >= 0) { |
|
ssize_t wr = write(fd, rom, sizeof rom); |
|
syscall_error(wr != sizeof(rom), "write failed"); |
|
close(fd); |
|
} |
|
} else if (!strcmp(av[i], "write")) { |
|
int fd; |
|
|
|
uint8_t rom[0x8000]; |
|
memset(rom, 0xff, sizeof rom); |
|
|
|
fd = open(rom_in, O_RDONLY); |
|
syscall_error(fd < 0, "open(%s) failed", rom_in); |
|
ssize_t len = read(fd, rom, 0x8000); |
|
syscall_error(len < 0, "read failed"); |
|
close(fd); |
|
if (len != 0x8000) |
|
error("Invalid ROM file (%s) size (%d), must be 32768 bytes", rom_in, (int)len); |
|
|
|
printf("Flashing code ROM\n"); |
|
for (unsigned i = 0x4000; i < 0x4000 + size; i += 128) |
|
write_rom_block(i, rom + i); |
|
|
|
uint8_t rd_rom[0x8000]; |
|
for (unsigned i = 0x4000; i < 0x4000 + size; i += 128) { |
|
read_rom_block(rd_rom + i, i); |
|
|
|
if (memcmp(rd_rom + i, rom + i, 128)) { |
|
printf("WARNING: Block 0x%04x write failed, retrying...\n", i); |
|
error("Retries disabled"); |
|
} |
|
} |
|
|
|
printf("Finishing flashing\n"); |
|
run_flash_cmd(REG_FLASH_CMD_COMMIT); |
|
} else if (!strcmp(av[i], "info")) { |
|
uint8_t devid[5]; |
|
|
|
rd_buf(0x00, devid, sizeof devid); |
|
|
|
printf("DEVID Register dump:\n"); |
|
for (int i = 0; i < sizeof(devid); i++) |
|
printf("0x%02x: 0x%02hhx\n", i, devid[i]); |
|
} else if (!strcmp(av[i], "reset")) { |
|
printf("Restarting the MCU\n"); |
|
|
|
uint8_t cmd1[] = {0}; |
|
wr_buf(REG_SYS_USER_APP_BLOCK, cmd1, sizeof cmd1); |
|
|
|
// send MCU reset command |
|
uint8_t cmd[] = {REG_SYS_COMMAND_MCU_RESET}; |
|
wr_buf(REG_SYS_COMMAND, cmd, sizeof cmd); |
|
} else if (!strcmp(av[i], "usbiap")) { |
|
printf("Restarting to USB IAP mode, if you don't have USB interface soldered on, you'll have to power-cycle the keyboard to get out of this flashing mode.\n"); |
|
|
|
// send MCU reset command |
|
uint8_t cmd[] = {REG_SYS_COMMAND_USB_IAP}; |
|
wr_buf(REG_SYS_COMMAND, cmd, sizeof cmd); |
|
} else if (!strcmp(av[i], "erase")) { |
|
run_flash_cmd(REG_FLASH_CMD_ERASE_ROM); |
|
} else { |
|
printf("ERROR: Unknown command: %s\n\n", av[i]); |
|
usage(); |
|
} |
|
} |
|
|
|
return 0; |
|
}
|
|
|