/* * Pinephone keyboard I2C flashing tool. * * Copyright (C) 2021 OndÅ™ej Jirman * * 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 . */ #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 ] [--rom-out ] [--verbose]\n" " [--help] [...]\n" "\n" "Options:\n" " -i, --rom-in Specify path to binary file you want to flash.\n" " -o, --rom-out Specify path where you want to store the contents\n" " of code ROM read from the device.\n" " -s, --size Specify how many bytes of code rom to flash\n" " starting from offset 0x4000 in the rom file.\n" " -e, --entry \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 , 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; }