// c2probe -- RP2040 bit-bang programmer for the Silicon Labs C2 interface. // // Reads code flash on EFM8UB2 (and C8051F) parts. READ-ONLY by design: it // implements only the C2 frame primitives, register read/write, PI init, and // the FPDAT Block Read (0x06) command. There is no erase/write/lock path and // no Device Erase arming, so it cannot damage flash. // // Wiring: GP2 = C2CK (target RST/C2CK), GP3 = C2D. 3.3V, direct. // Protocol reference: Silicon Labs AN127 Rev 1.4. // // Host command interface over USB CDC (line-based, LF-terminated): // hello // speed C2CK half-period in us (low=high=), default 1 // fpdat set FPDAT register address (default 0xAD EFM8UB2) // pins report pin assignment // reset C2 device reset (C2CK low >=20us) // status Address Read -> status byte (FLBusy/EError/InBusy/OutReady) // rdreg Address Write + Data Read -> register value // wrreg Address Write + Data Write // id read DEVICEID(0x00) and REVID(0x01) // piinit full PI init (reset + FPCTL 0x02,0x04,0x01 + 20ms) // rawaw raw Address Write // rawar raw Address Read (-> status) // rawdw raw Data Write // rawdr raw Data Read // read FPDAT Block Read, len 1..256 (0=256) -> hex bytes // dump loop read over range, one line per block // // Replies: "ok ...", "data ", "err ", "stat ", etc. #include #include #include #include "pico/stdlib.h" #include "hardware/gpio.h" #include "hardware/sync.h" #include "pico/bootrom.h" // C2CK / C2D pin numbers are runtime-configurable via the `pins` command so the // host can try both wiring orientations without reflashing. Default: GP2=C2CK, // GP3=C2D. static int C2CK = 2; static int C2D = 3; // C2CK low/high time for a bit strobe, in microseconds. Must satisfy // 20ns <= tCL < 5000ns (else a reset is triggered). 1us is safe. static uint32_t half_us = 1; // FPDAT register address (device-specific). EFM8UB2 = 0xAD. Settable at runtime. static uint8_t fpdat_addr = 0xAD; // ----------------------------------------------------------------- low-level static inline void c2ck_set(int v) { gpio_put(C2CK, v); } static inline void c2d_drive(int v) { gpio_set_dir(C2D, GPIO_OUT); gpio_put(C2D, v ? 1 : 0); } static inline void c2d_release(void) { gpio_set_dir(C2D, GPIO_IN); // high-Z, no pull } static inline int c2d_get(void) { return gpio_get(C2D) ? 1 : 0; } // One C2CK strobe: high->low (tCL)->high (tCH). IRQs disabled across the low // window so the low time stays under the 5us reset threshold even if a USB // IRQ fires. Used when the master is driving C2D (write bits / start / stop). static void strobe_write(void) { uint32_t save = save_and_disable_interrupts(); gpio_put(C2CK, 0); busy_wait_us(half_us); gpio_put(C2CK, 1); restore_interrupts(save); busy_wait_us(half_us); } // One C2CK strobe that reads a slave-driven bit. C2D is released (input) and // sampled after the rising edge + tDV. IRQs disabled across low+sample. static int strobe_read(void) { c2d_release(); uint32_t save = save_and_disable_interrupts(); gpio_put(C2CK, 0); busy_wait_us(half_us); gpio_put(C2CK, 1); busy_wait_us(half_us); // tDV ~20ns; half_us gives margin int v = gpio_get(C2D) ? 1 : 0; restore_interrupts(save); return v; } static void c2_start(void) { c2d_drive(1); strobe_write(); } static void c2_stop(void) { c2d_drive(1); strobe_write(); c2d_release(); } static void c2_write_bit(int b) { c2d_drive(b ? 1 : 0); strobe_write(); } // ----------------------------------------------------------------- frames // Address Write (INS=11b): START, INS(1,1), ADDRESS 8 bits LSB-first, STOP. static void c2_addr_write(uint8_t addr) { c2_start(); c2_write_bit(1); c2_write_bit(1); // INS = 11b for (int i = 0; i < 8; i++) c2_write_bit((addr >> i) & 1); c2_stop(); } // Address Read (INS=10b): START, INS(0,1), release, read 8 bits = status, STOP. static uint8_t c2_addr_read(void) { c2_start(); c2_write_bit(0); c2_write_bit(1); // INS = 10b uint8_t v = 0; for (int i = 0; i < 8; i++) v |= (strobe_read() << i); c2_stop(); return v; } // Data Write (INS=01b): START, INS(1,0), LENGTH(00=1B), DATA 8, WAIT(0s then 1), STOP. // Returns 0 on success, -1 on WAIT timeout. static int c2_data_write(uint8_t val) { c2_start(); c2_write_bit(1); c2_write_bit(0); // INS = 01b c2_write_bit(0); c2_write_bit(0); // LENGTH = 00 (1 byte) for (int i = 0; i < 8; i++) c2_write_bit((val >> i) & 1); // WAIT: slave releases 0s then a 1; clock and read until 1 seen. int waited = 0; while (strobe_read() == 0) { if (++waited > 8192) { c2_stop(); return -1; } } c2_stop(); return 0; } // Data Read (INS=00b): START, INS(0,0), LENGTH(00), WAIT(0s then 1), DATA 8, STOP. // Returns 0 on success, -1 on WAIT timeout. static int c2_data_read(uint8_t *out) { c2_start(); c2_write_bit(0); c2_write_bit(0); // INS = 00b c2_write_bit(0); c2_write_bit(0); // LENGTH = 00 (1 byte) int waited = 0; while (strobe_read() == 0) { // WAIT if (++waited > 8192) { c2_stop(); return -1; } } uint8_t v = 0; for (int i = 0; i < 8; i++) v |= (strobe_read() << i); // DATA c2_stop(); *out = v; return 0; } // ----------------------------------------------------------------- helpers static uint8_t c2_status(void) { return c2_addr_read(); } static uint8_t c2_reg_read(uint8_t addr) { c2_addr_write(addr); uint8_t v = 0; c2_data_read(&v); return v; } static int c2_reg_write(uint8_t addr, uint8_t val) { c2_addr_write(addr); return c2_data_write(val); } static int c2_poll_inbusy(void) { for (long i = 0; i < 200000L; i++) { uint8_t s = c2_status(); if (!((s >> 1) & 1)) return 0; // InBusy cleared } return -1; } static int c2_poll_outready(void) { for (long i = 0; i < 200000L; i++) { uint8_t s = c2_status(); if (s & 1) return 0; // OutReady set } return -1; } static void c2_reset(void) { // Device reset: C2CK low >= 20us, high, wait >= 2us. Also leaves C2CK high. c2d_release(); c2ck_set(0); busy_wait_us(50); // tRD >= 20us c2ck_set(1); busy_wait_us(5); // tSD >= 2us } static void c2_pi_init(void) { c2_reset(); c2_reg_write(0x02, 0x02); // FPCTL <- 0x02 (enable) c2_reg_write(0x02, 0x04); // FPCTL <- 0x04 (halt core) c2_reg_write(0x02, 0x01); // FPCTL <- 0x01 busy_wait_us(20000); // >= 20ms } // Block Read: read `len` bytes (1..256, 0 => 256) from flash `addr`. // Returns number of bytes read, or negative error code. static int c2_block_read(uint16_t addr, uint8_t len, uint8_t *buf) { int n = len ? (int)len : 256; uint8_t st; c2_addr_write(fpdat_addr); if (c2_data_write(0x06)) return -10; // Block Read cmd if (c2_poll_inbusy()) return -1; if (c2_poll_outready()) return -2; if (c2_data_read(&st)) return -3; if (st != 0x0D) return -4; // status not OK if (c2_data_write((addr >> 8) & 0xFF)) return -11; // addr high if (c2_poll_inbusy()) return -5; if (c2_data_write(addr & 0xFF)) return -12; // addr low if (c2_poll_inbusy()) return -6; if (c2_data_write(len)) return -13; // length code (0=256) if (c2_poll_inbusy()) return -7; // Block-ack status: after addr+len the PI emits a second 0x0D before the // data stream (confirmed vs ec2drv: read_port(..., cmd[3]+1) "// +1 for // 0x0d"). Consume and discard it, else it lands as buf[0] and the whole // block reads shifted by one (off-by-one vs stock). if (c2_poll_outready()) return -14; if (c2_data_read(&st)) return -15; if (st != 0x0D) return -16; for (int k = 0; k < n; k++) { if (c2_poll_outready()) return -8 - k; uint8_t b; if (c2_data_read(&b)) return -200 - k; buf[k] = b; } return n; } // ----------------------------------------------------------------- write/erase // // SAFETY: the bootloader lives at 0x0000-0x23FF and the flash lock / reserved // area at 0xFA00-0xFFFF. These are UNRECOVERABLE if erased (no image available). // Every erase/write below is hard-limited to the application region // 0x2400-0xF9FF. There is no Device Erase / mass-erase command anywhere in // this firmware. The guards are both compile-time (constants) and runtime. #define APP_LO 0x2400 // first application address (inclusive) #define APP_HI 0xFA00 // first protected address (exclusive) #define PAGE_BYTES 512 #define APP_PAGE_LO (APP_LO / PAGE_BYTES) // 0x12 #define APP_PAGE_HI ((APP_HI / PAGE_BYTES) - 1) // 0x7C (0xF800 page is last app page) static int app_addr_ok(uint32_t a, uint32_t len) { return a >= APP_LO && (a + len) <= APP_HI && len > 0; } static int app_page_ok(uint8_t page) { return page >= APP_PAGE_LO && page <= APP_PAGE_HI; } // Poll until OutReady CLEARS (used by Page Erase step 8). static int c2_poll_outready_clear(void) { for (long i = 0; i < 400000L; i++) { // erase can take longer uint8_t s = c2_status(); if (!(s & 1)) return 0; // OutReady cleared } return -1; } // Direct Write (FPDAT 0x0A): write one SFR. Used for the pre-flash VDD-monitor // / flash-timing / clock setup. No address guard (SFRs are 0x80-0xFF by // definition; this only touches the four documented EFM8UB2 setup registers). static int c2_direct_write(uint8_t sfr, uint8_t val) { c2_addr_write(fpdat_addr); if (c2_data_write(0x0A)) return -10; // Direct Write cmd if (c2_poll_inbusy()) return -1; if (c2_poll_outready()) return -2; uint8_t st; if (c2_data_read(&st)) return -3; if (st != 0x0D) return -4; if (c2_data_write(sfr)) return -11; // SFR address if (c2_poll_inbusy()) return -5; if (c2_data_write(0x01)) return -12; // length = 1 byte if (c2_poll_inbusy()) return -6; if (c2_data_write(val)) return -13; // SFR value if (c2_poll_inbusy()) return -7; return 0; } // EFM8UB2 pre-flash setup (AN127 Table 3.6): flash timing, enable VDD monitor, // clock, supply-monitor reset source. Must run once after piinit, before any // erase/write. Returns 0 on success. static int c2_pgm_setup(void) { if (c2_direct_write(0xB6, 0x90)) return -1; // FLSCL = 0x90 (flash timing) if (c2_direct_write(0xFF, 0x80)) return -2; // VDM0CN = 0x80 (VDD mon enable) if (c2_direct_write(0xA9, 0x03)) return -3; // CLKSEL = 0x03 if (c2_direct_write(0xEF, 0x02)) return -4; // RSTSRC = 0x02 (VDD mon reset src) return 0; } // Page Erase (FPDAT 0x08). `page` = address / 512. Guarded to app region only. static int c2_page_erase(uint8_t page) { if (!app_page_ok(page)) return -100; // REFUSED: outside app region uint8_t st; c2_addr_write(fpdat_addr); if (c2_data_write(0x08)) return -10; // Page Erase cmd if (c2_poll_inbusy()) return -1; if (c2_poll_outready()) return -2; if (c2_data_read(&st)) return -3; if (st != 0x0D) return -4; if (c2_data_write(page)) return -11; // target page number if (c2_poll_inbusy()) return -5; // Page-number ack: the PI sets OutReady with a 0x0D status after consuming // the page number. AN127 step 8 says "poll until clear" but on the EFM8UB2 // the byte is genuinely pending (OutReady stuck SET), so we poll until SET // and read it -- mirroring the command-ack (steps 4-5) and completion-ack // (steps 12-13). The 0x0D check below rejects any error status safely. if (c2_poll_outready()) return -6; // OutReady -> 1 (ack ready) if (c2_data_read(&st)) return -7; if (st != 0x0D) return -8; if (c2_data_write(0x00)) return -12; // initiate erase if (c2_poll_inbusy()) return -9; if (c2_poll_outready()) return -13; // OutReady -> 1 (done) if (c2_data_read(&st)) return -14; if (st != 0x0D) return -15; return 0; } // Block Write (FPDAT 0x07): program `len` bytes (1..256, 0 => 256) at `addr`. // addr..addr+len must lie inside the app region. Flash must be erased (0xFF) // at the target first. Returns number of bytes written, or negative error. static int c2_block_write(uint16_t addr, uint8_t len, const uint8_t *buf) { int n = len ? (int)len : 256; if (!app_addr_ok(addr, n)) return -100; // REFUSED: outside app region uint8_t st; c2_addr_write(fpdat_addr); if (c2_data_write(0x07)) return -10; // Block Write cmd if (c2_poll_inbusy()) return -1; if (c2_poll_outready()) return -2; if (c2_data_read(&st)) return -3; if (st != 0x0D) return -4; if (c2_data_write((addr >> 8) & 0xFF)) return -11; // addr high if (c2_poll_inbusy()) return -5; if (c2_data_write(addr & 0xFF)) return -12; // addr low if (c2_poll_inbusy()) return -6; if (c2_data_write(len)) return -13; // length code (0=256) if (c2_poll_inbusy()) return -7; for (int k = 0; k < n; k++) { if (c2_data_write(buf[k])) return -200 - k; // data byte if (c2_poll_inbusy()) return -8 - k; } if (c2_poll_outready()) return -14; // write complete if (c2_data_read(&st)) return -15; if (st != 0x0D) return -16; return n; } // ----------------------------------------------------------------- command I/O static int get_line(char *buf, int maxlen) { int n = 0; while (n < maxlen - 1) { int c = getchar_timeout_us(1000); if (c == PICO_ERROR_TIMEOUT) continue; if (c < 0) continue; if (c == '\r') continue; if (c == '\n') break; buf[n++] = (char)c; } buf[n] = 0; return n; } static void print_hex(const uint8_t *p, int n) { for (int k = 0; k < n; k++) printf("%02x", p[k]); printf("\n"); } static void do_read(uint16_t addr, uint8_t len) { static uint8_t buf[256]; int rc = c2_block_read(addr, len, buf); if (rc < 0) { printf("err %d\n", rc); return; } printf("data %04x %d ", addr, rc); print_hex(buf, rc); } // (Re)configure the C2CK / C2D pins. Idles C2CK high and releases C2D. static void c2_setup_pins(int ck, int d) { C2CK = ck; C2D = d; gpio_init(ck); gpio_set_dir(ck, GPIO_OUT); gpio_put(ck, 1); gpio_init(d); gpio_set_dir(d, GPIO_OUT); gpio_put(d, 1); c2d_release(); } int main(void) { stdio_init_all(); c2_setup_pins(2, 3); // default: GP2=C2CK, GP3=C2D // Must hold the longest command: "bw ffff 0 " (10) + 512 hex chars = 522. char line[1024]; while (true) { int n = get_line(line, sizeof(line)); if (n == 0) { printf("ok\n"); continue; } char cmd[32]; if (sscanf(line, "%31s", cmd) != 1) { printf("err parse\n"); continue; } if (!strcmp(cmd, "hello")) { printf("ok c2probe v2\n"); } else if (!strcmp(cmd, "bootsel")) { printf("ok bootsel\n"); fflush(stdout); busy_wait_us(5000); reset_usb_boot(0, 0); // reboot into USB bootloader (BOOTSEL) } else if (!strcmp(cmd, "speed")) { unsigned us = 1; sscanf(line, "%*s %u", &us); half_us = (us > 1000) ? 1000 : us; printf("ok speed %luus\n", (unsigned long)half_us); } else if (!strcmp(cmd, "fpdat")) { unsigned a = fpdat_addr; sscanf(line, "%*s %x", &a); fpdat_addr = a & 0xFF; printf("ok fpdat %02x\n", fpdat_addr); } else if (!strcmp(cmd, "pins")) { int ck = C2CK, d = C2D; if (sscanf(line, "%*s %i %i", &ck, &d) == 2) { c2_setup_pins(ck, d); } printf("ok ck=gp%d d=gp%d\n", C2CK, C2D); } else if (!strcmp(cmd, "reset")) { c2_reset(); printf("ok reset\n"); } else if (!strcmp(cmd, "status")) { printf("stat %02x\n", c2_status()); } else if (!strcmp(cmd, "rdreg")) { unsigned a = 0; sscanf(line, "%*s %x", &a); printf("reg %02x\n", c2_reg_read(a & 0xFF)); } else if (!strcmp(cmd, "wrreg")) { unsigned a = 0, v = 0; sscanf(line, "%*s %x %x", &a, &v); int rc = c2_reg_write(a & 0xFF, v & 0xFF); printf("%s\n", rc ? "err wait" : "ok"); } else if (!strcmp(cmd, "id")) { uint8_t d = c2_reg_read(0x00), r = c2_reg_read(0x01); printf("id devid=%02x revid=%02x\n", d, r); } else if (!strcmp(cmd, "id2")) { // atomic: reset then read DEVICEID/REVID with no host round-trip c2_reset(); uint8_t d = c2_reg_read(0x00), r = c2_reg_read(0x01); printf("id2 devid=%02x revid=%02x\n", d, r); } else if (!strcmp(cmd, "dbg")) { // 3-state read of C2D: floating line follows the pull; a driven // line ignores it. Reveals whether a slave is driving C2D. gpio_set_dir(C2D, GPIO_IN); gpio_disable_pulls(C2D); busy_wait_us(20); int none = c2d_get(); gpio_pull_up(C2D); busy_wait_us(20); int up = c2d_get(); gpio_disable_pulls(C2D); gpio_pull_down(C2D); busy_wait_us(20); int dn = c2d_get(); gpio_disable_pulls(C2D); printf("dbg c2d none=%d up=%d dn=%d c2ck=%d (float if up=1,dn=0)\n", none, up, dn, gpio_get(C2CK)); } else if (!strcmp(cmd, "piinit")) { c2_pi_init(); printf("ok piinit\n"); } else if (!strcmp(cmd, "rawaw")) { unsigned a = 0; sscanf(line, "%*s %x", &a); c2_addr_write(a & 0xFF); printf("ok\n"); } else if (!strcmp(cmd, "rawar")) { printf("ar %02x\n", c2_addr_read()); } else if (!strcmp(cmd, "rawdw")) { unsigned v = 0; sscanf(line, "%*s %x", &v); printf("%s\n", c2_data_write(v & 0xFF) ? "err wait" : "ok"); } else if (!strcmp(cmd, "rawdr")) { uint8_t v = 0; int rc = c2_data_read(&v); printf("dr %02x %s\n", v, rc ? "err" : "ok"); } else if (!strcmp(cmd, "read")) { unsigned a = 0; int l = 0; sscanf(line, "%*s %x %i", &a, &l); if (l < 0) l = 0; if (l > 256) l = 256; do_read(a & 0xFFFF, (uint8_t)l); } else if (!strcmp(cmd, "dump")) { unsigned s = 0, e = 0; sscanf(line, "%*s %x %x", &s, &e); if (e > 0x10000) e = 0x10000; int blocks = 0; for (unsigned a = s; a < e; ) { int chunk = e - a; if (chunk > 256) chunk = 256; do_read(a & 0xFFFF, (uint8_t)chunk); a += chunk; blocks++; } printf("done %d\n", blocks); } else if (!strcmp(cmd, "sfrw")) { // Direct Write one SFR (0x80-0xFF). For the pre-flash setup only. unsigned a = 0, v = 0; sscanf(line, "%*s %x %x", &a, &v); int rc = c2_direct_write(a & 0xFF, v & 0xFF); printf("%s\n", rc ? "err" : "ok"); if (rc) printf("sfrw rc %d\n", rc); } else if (!strcmp(cmd, "wsetup")) { // EFM8UB2 pre-flash SFR setup (flash timing + VDD monitor + clock). int rc = c2_pgm_setup(); printf("%s\n", rc ? "err" : "ok"); if (rc) printf("wsetup rc %d\n", rc); } else if (!strcmp(cmd, "pe")) { // Page Erase. Page number = addr/512. Guarded to app region. unsigned pg = 0; sscanf(line, "%*s %x", &pg); int rc = c2_page_erase(pg & 0xFF); if (rc == -100) printf("refused page %02x outside app region\n", pg & 0xFF); else printf("%s\n", rc ? "err" : "ok"); if (rc && rc != -100) printf("pe rc %d\n", rc); } else if (!strcmp(cmd, "bw")) { // Block Write: bw . Guarded to app region. unsigned a = 0; int l = 0; char hex[600]; hex[0] = 0; // "bw ADDR LEN HEXSTR" int got = sscanf(line, "%*s %x %i %599s", &a, &l, hex); if (got < 3) { printf("err bw usage\n"); } else { if (l < 0) l = 0; if (l > 256) l = 256; int n = l ? l : 256; static uint8_t wbuf[256]; int hl = (int)strlen(hex); if (hl < n * 2) { printf("err bw short hex (%d want %d)\n", hl, n * 2); } else { int ok = 1; for (int k = 0; k < n; k++) { unsigned b; if (sscanf(hex + k * 2, "%2x", &b) != 1) { ok = 0; break; } wbuf[k] = (uint8_t)b; } if (!ok) printf("err bw hex parse\n"); else { int rc = c2_block_write(a & 0xFFFF, (uint8_t)l, wbuf); if (rc == -100) printf("refused addr %04x outside app region\n", a & 0xFFFF); else if (rc < 0) { printf("err\n"); printf("bw rc %d\n", rc); } else printf("ok bw %04x %d\n", a & 0xFFFF, rc); } } } } else { printf("err unknown\n"); } fflush(stdout); } return 0; }