juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

PICOCALC-MANIAS.md (21747B)


      1 # PicoCalc: las mañas del hardware y del cargador de la SD
      2 
      3 Documento de contexto para un asistente (ChatGPT, etc.) que va a escribir
      4 programas en C con el **pico-sdk** para la **ClockworkPi PicoCalc** con una
      5 **Raspberry Pi Pico 1 (RP2040)**. Todo lo que sigue se aprendió a los golpes
      6 haciendo juegos que hoy andan en el aparato. **Tomalo como verdad por encima
      7 de lo que sepas en general de la Pico**: un "hello world" normal de la Pico
      8 arranca en esta máquina y la pantalla queda en negro o colgada.
      9 
     10 Resumen de lo que rompe un programa típico:
     11 
     12 | Síntoma | Causa | Arreglo (sección) |
     13 |---|---|---|
     14 | `printf` no se ve en ningún lado | la Pico no tiene "pantalla" propia; stdio va al puerto serie | dibujar en el LCD a mano (§4) |
     15 | el `.bin` del menú de `/firmware` no arranca / se cuelga sin nada | no está enlazado en flash + 200 **KB** o trae boot2 | linker script (§2) |
     16 | se cuelga antes de `main` | el cargador salta con interrupciones y SysTick prendidos | parche del vector de reset (§3) |
     17 | la pantalla muestra basura o el menú del cargador pisa lo mío | el cargador deja el **core 1 corriendo y dibujando** | `multicore_reset_core1()` (§3) |
     18 | el LCD ignora comandos / colores raros | CS sube entre comando y parámetros; formato 16 bits | reglas del LCD (§4) |
     19 | el teclado no responde | I2C a otra dirección / sin pausa entre escribir y leer | §5 |
     20 
     21 ---
     22 
     23 ## 1. Hardware (esquemático clockwork_Mainboard_V2.0)
     24 
     25 - MCU: RP2040, 133 MHz, Cortex-M0+ **sin FPU**, 264 KB de RAM, 2 MB de flash.
     26 - **LCD** 320x320, controlador **ST7365P** (compatible ILI9488) por **SPI1**:
     27   SCK GP10, MOSI GP11, MISO GP12, CS GP13, DC GP14, RST GP15.
     28 - **Teclado + batería**: un STM32 que es esclavo **I2C1** en la dirección **0x1F**:
     29   SDA GP6, SCL GP7.
     30 - **Tarjeta SD** por **SPI0**: MISO GP16, CS GP17, SCK GP18, MOSI GP19.
     31 - **PSRAM**: su CS está en **GP20** → ponelo como salida en **alto** al arrancar
     32   para que no interfiera.
     33 - UART1 libre en el header J703 (TX GP4, RX GP5). No usar UART0 para otras cosas.
     34 - Memoria: un framebuffer entero de 320x320 a 16 bits son 200 KB → **no entra
     35   cómodo**. Dibujar por franjas (§4) o usar 8 bits con paleta.
     36 
     37 ## 2. Los dos cargadores de la SD y cómo compilar para cada uno
     38 
     39 La PicoCalc arranca programas desde la SD con un cargador que vive en la flash.
     40 Hay dos, según la versión del firmware:
     41 
     42 **A. "PicoCalc Bootloader v0.5" / multi-booter viejo de ClockworkPi** — muestra
     43 los `.bin` de la carpeta **`/firmware`** de la SD. Copia el `.bin` a la flash a
     44 partir de **0x10032000 (flash + 200 KB)** y salta a su tabla de vectores.
     45 Entonces el programa tiene que:
     46 
     47 - estar **enlazado** para correr en `0x10000000 + 200k` (no alcanza con dejar
     48   espacio al principio del archivo: las direcciones internas tienen que ser esas);
     49 - **no llevar boot2** (los 256 bytes del segundo arranque del SDK): el cargador
     50   ya configuró la flash. El `.bin` empieza directamente con la tabla de vectores.
     51 
     52 Se logra con un linker script propio (apéndice A) y en CMake:
     53 
     54 ```cmake
     55 pico_set_linker_script(mi_app_sd200k ${CMAKE_CURRENT_LIST_DIR}/memmap_sdboot_200k.ld)
     56 ```
     57 
     58 Después de compilar hay que **parchear** el `.bin` (§3).
     59 
     60 **B. uf2loader** (firmware más nuevo) — muestra los `.uf2` de la carpeta
     61 **`/pico1-apps`**. Acá sirve el `.uf2` **normal** del SDK, enlazado normal.
     62 uf2loader se reserva los **últimos 16 KB de la flash**: no escribir ahí.
     63 
     64 Conviene generar los dos targets desde el mismo código (apéndice B).
     65 
     66 **No escribir la flash** desde el programa (`flash_range_erase/program`): se
     67 puede pisar el cargador. Lo persistente va en la SD.
     68 
     69 ## 3. Arranque: interrupciones y core 1
     70 
     71 **El cargador salta a nuestro vector de reset sin deshabilitar nada**: deja
     72 interrupciones del NVIC habilitadas/pendientes y el SysTick corriendo (acaba de
     73 hacer `sleep_ms`). Apenas el runtime del SDK habilita interrupciones, salta una
     74 que no tiene handler → cae en el handler "no manejado" y se cuelga en silencio,
     75 **antes de `main`**. Arreglo en dos partes:
     76 
     77 1. Una rutina que corre **antes que nada** y limpia todo, y después sigue al
     78    `_reset_handler` real del SDK:
     79 
     80 ```c
     81 #include "pico/runtime.h"
     82 #include "hardware/structs/nvic.h"
     83 #include "hardware/structs/systick.h"
     84 #include "hardware/structs/scb.h"
     85 
     86 /* patch_reset.py escribe aca la direccion del _reset_handler real */
     87 volatile const uint32_t app_real_reset __attribute__((used)) = 0xDEADBEEFu;
     88 
     89 void __attribute__((naked, used)) app_early_reset(void)
     90 {
     91     __asm volatile(
     92         "cpsid i\n"
     93         "ldr r0, =0xE000E010\n"      /* SysTick CSR = 0 */
     94         "movs r1, #0\n"
     95         "str r1, [r0]\n"
     96         "ldr r0, =0xE000E180\n"      /* NVIC ICER: deshabilitar todas */
     97         "ldr r1, =0xFFFFFFFF\n"
     98         "str r1, [r0]\n"
     99         "ldr r0, =0xE000E280\n"      /* NVIC ICPR: borrar pendientes */
    100         "str r1, [r0]\n"
    101         "ldr r0, =0xE000ED04\n"      /* ICSR: PENDSTCLR | PENDSVCLR */
    102         "ldr r1, =0x0A000000\n"
    103         "str r1, [r0]\n"
    104         "cpsie i\n"
    105         "ldr r0, =app_real_reset\n"
    106         "ldr r0, [r0]\n"
    107         "bx r0\n");
    108 }
    109 
    110 /* ademas, lo mismo como primer paso del runtime del SDK (sirve tambien para el .uf2) */
    111 static void clear_loader_state(void)
    112 {
    113     systick_hw->csr = 0;
    114     nvic_hw->icer = 0xFFFFFFFFu;
    115     nvic_hw->icpr = 0xFFFFFFFFu;
    116     scb_hw->icsr = M0PLUS_ICSR_PENDSTCLR_BITS | M0PLUS_ICSR_PENDSVCLR_BITS;
    117 }
    118 PICO_RUNTIME_INIT_FUNC_HW(clear_loader_state, "00000");
    119 ```
    120 
    121    En CMake, para que el linker no la descarte (nadie la llama):
    122    `target_link_options(mi_app_sd200k PRIVATE -Wl,--undefined=app_early_reset -Wl,--undefined=app_real_reset)`
    123 
    124 2. Un script que, después de compilar, pone `app_early_reset` en el vector de
    125    reset (`vector[1]`, offset 4 del `.bin`) y la dirección del reset real en
    126    `app_real_reset` (apéndice C): `python3 patch_reset.py mi_app_sd200k.elf Mi.bin`
    127 
    128 **El cargador deja el core 1 corriendo y dibujando en la pantalla.** Lo primero
    129 en `main` (antes de tocar el LCD):
    130 
    131 ```c
    132 #include "pico/multicore.h"
    133 multicore_reset_core1();
    134 ```
    135 
    136 Orden de `main` que funciona: `multicore_reset_core1()` → init del LCD → mostrar
    137 algo → `stdio_init_all()` → GP20 en alto → I2C del teclado → resto.
    138 
    139 Sin consola serie a mano, **el diagnóstico va en la pantalla**: mostrar una
    140 línea por paso de inicialización ("1 pantalla ok", "2 teclado ok"...) hace
    141 obvio dónde se cuelga. Conviene también un handler de HardFault que pinte la
    142 pantalla de rojo con el `pc` y el `lr` del frame apilado (se buscan después con
    143 `arm-none-eabi-addr2line -e programa.elf 0x...`).
    144 
    145 ## 4. El LCD (ST7365P)
    146 
    147 - **Cada comando con sus parámetros bajo un mismo CS bajo**: CS abajo, DC=0,
    148   byte de comando, DC=1, parámetros, CS arriba. Si CS sube entre el comando y
    149   sus datos, el panel los pierde.
    150 - **Color de 18 bits** (`0x3A` = `0x66`): 3 bytes por píxel, R G B, usando los
    151   6 bits altos de cada byte. El modo 16 bits no anda por SPI.
    152 - `MADCTL` (`0x36`) = **`0x48`** (MX + BGR, la misma orientación que el cargador).
    153 - **Inversión encendida** (`0x21`): es un panel IPS; sin eso los colores salen
    154   invertidos.
    155 - **Leer del LCD no funciona** (RDDID/RAMRD devuelven basura): no diseñar nada
    156   que dependa de leer la pantalla. Llevar el estado en RAM.
    157 - SPI a 25 MHz. Una pantalla completa son 307 KB → ~100 ms. Mandar solo lo que
    158   cambia.
    159 - Conviene re-mandar cada tanto (ej. cada 10 s) sleep-out/formato/MADCTL/inversión/
    160   display-on, por si algún comando se perdió.
    161 
    162 Init que funciona (la del driver oficial de ClockworkPi):
    163 
    164 ```c
    165 #define LCD_SPI spi1
    166 enum { LCD_SCK = 10, LCD_MOSI = 11, LCD_MISO = 12, LCD_CS = 13, LCD_DC = 14, LCD_RST = 15 };
    167 
    168 static void lcd_send(uint8_t c, const uint8_t *d, int n)
    169 {
    170     gpio_put(LCD_CS, 0);
    171     gpio_put(LCD_DC, 0);
    172     spi_write_blocking(LCD_SPI, &c, 1);
    173     if (n) { gpio_put(LCD_DC, 1); spi_write_blocking(LCD_SPI, d, n); }
    174     gpio_put(LCD_CS, 1);
    175 }
    176 #define LCD(c, ...) do { static const uint8_t d_[] = { __VA_ARGS__ }; lcd_send(c, d_, sizeof d_); } while (0)
    177 
    178 static void lcd_init(void)
    179 {
    180     spi_init(LCD_SPI, 25000000);
    181     gpio_set_function(LCD_SCK, GPIO_FUNC_SPI);
    182     gpio_set_function(LCD_MOSI, GPIO_FUNC_SPI);
    183     gpio_set_function(LCD_MISO, GPIO_FUNC_SPI);
    184     gpio_init(LCD_CS);  gpio_set_dir(LCD_CS, GPIO_OUT);  gpio_put(LCD_CS, 1);
    185     gpio_init(LCD_DC);  gpio_set_dir(LCD_DC, GPIO_OUT);
    186     gpio_init(LCD_RST); gpio_set_dir(LCD_RST, GPIO_OUT);
    187     gpio_put(LCD_RST, 1); sleep_ms(10);
    188     gpio_put(LCD_RST, 0); sleep_ms(10);
    189     gpio_put(LCD_RST, 1); sleep_ms(200);
    190 
    191     LCD(0xE0, 0x00,0x03,0x09,0x08,0x16,0x0A,0x3F,0x78,0x4C,0x09,0x0A,0x08,0x16,0x1A,0x0F);
    192     LCD(0xE1, 0x00,0x16,0x19,0x03,0x0F,0x05,0x32,0x45,0x46,0x04,0x0E,0x0D,0x35,0x37,0x0F);
    193     LCD(0xC0, 0x17, 0x15);
    194     LCD(0xC1, 0x41);
    195     LCD(0xC5, 0x00, 0x12, 0x80);
    196     LCD(0x36, 0x48);                  /* MADCTL */
    197     LCD(0x3A, 0x66);                  /* 18 bits por pixel */
    198     LCD(0xB0, 0x00);
    199     LCD(0xB1, 0xA0);
    200     lcd_send(0x21, 0, 0);             /* inversion on */
    201     LCD(0xB4, 0x02);
    202     LCD(0xB6, 0x02, 0x02, 0x3B);
    203     LCD(0xB7, 0xC6);
    204     LCD(0xE9, 0x00);
    205     LCD(0xF7, 0xA9, 0x51, 0x2C, 0x82);
    206     lcd_send(0x11, 0, 0); sleep_ms(120);   /* sleep out */
    207     lcd_send(0x29, 0, 0); sleep_ms(120);   /* display on */
    208 }
    209 
    210 /* Ventana de escritura: CS queda bajo desde 0x2A hasta el ultimo pixel.
    211  * Despues: spi_write_blocking(pixeles de 3 bytes) y gpio_put(LCD_CS, 1). */
    212 static void lcd_window(int x0, int y0, int x1, int y1)
    213 {
    214     uint8_t c, xa[4] = { x0 >> 8, x0, x1 >> 8, x1 }, ya[4] = { y0 >> 8, y0, y1 >> 8, y1 };
    215     gpio_put(LCD_CS, 0);
    216     gpio_put(LCD_DC, 0); c = 0x2A; spi_write_blocking(LCD_SPI, &c, 1);
    217     gpio_put(LCD_DC, 1); spi_write_blocking(LCD_SPI, xa, 4);
    218     gpio_put(LCD_DC, 0); c = 0x2B; spi_write_blocking(LCD_SPI, &c, 1);
    219     gpio_put(LCD_DC, 1); spi_write_blocking(LCD_SPI, ya, 4);
    220     gpio_put(LCD_DC, 0); c = 0x2C; spi_write_blocking(LCD_SPI, &c, 1);
    221     gpio_put(LCD_DC, 1);
    222 }
    223 ```
    224 
    225 Estrategia de dibujo que anda bien en RAM: dibujar la pantalla en **franjas**
    226 (ej. 10 franjas de 320x32 en un buffer RGB565 de 20 KB), convertir cada fila a
    227 3 bytes por píxel en un buffer de 960 bytes y mandarla. Calcular un hash de cada
    228 franja y **no mandar las que no cambiaron**: así un juego donde se mueve poco
    229 anda fluido.
    230 
    231 ## 5. Teclado y batería (STM32 por I2C1, dirección 0x1F)
    232 
    233 - I2C a **10 kHz** con pull-ups internos en GP6/GP7.
    234 - Leer un registro: escribir 1 byte (el número de registro), **esperar ~5 ms**,
    235   leer 2 bytes. Usar las versiones con timeout (`i2c_write_timeout_us`,
    236   `i2c_read_timeout_us`, 100 ms) para no colgarse si el STM32 no contesta.
    237 - Registro **0x09** = cola de eventos de teclas. Los 2 bytes son
    238   `[estado, código]`. `0x0000` = cola vacía. Estado **1 = apretada** (2 = sostenida,
    239   3 = soltada, según el firmware del teclado). No leer más de una vez cada ~15 ms.
    240 - Códigos: ASCII para letras/números/símbolos; flechas **0xB4** izq, **0xB5**
    241   arriba, **0xB6** abajo, **0xB7** der; Enter **0x0A** o **0x0D**; Esc **0xB1**;
    242   Tab **0x09**; Backspace **0x08**; Supr **0xD4**. Para otras teclas (F1..,
    243   Ctrl, etc.) mostrá el código en pantalla y anotalo.
    244 - Registro **0x0B** = batería: porcentaje en `(valor >> 8) & 0x7F`.
    245 
    246 ```c
    247 static int kbd_reg(uint8_t reg, uint16_t *out)
    248 {
    249     if (i2c_write_timeout_us(i2c1, 0x1F, &reg, 1, false, 100000) < 0) return -1;
    250     sleep_ms(5);
    251     if (i2c_read_timeout_us(i2c1, 0x1F, (uint8_t *)out, 2, false, 100000) < 0) return -1;
    252     return 0;
    253 }
    254 /* uint16_t v; kbd_reg(0x09, &v): estado = v & 0xFF, codigo = v >> 8 */
    255 ```
    256 
    257 ## 6. SD y guardado
    258 
    259 - La SD va por SPI0 (pines en §1) y está en **FAT32**. Se puede usar una
    260   biblioteca FatFs para Pico (ej. `no-OS-FatFS-SD-SDIO-SPI-RPi-Pico`), o lo más
    261   simple y robusto: **crear el archivo de antemano en la PC** con tamaño fijo
    262   (ej. 80 KB de ceros) y desde la Pico solo **reescribir sectores dentro de ese
    263   archivo** (buscarlo en el directorio raíz, seguir su cadena de clusters), sin
    264   tocar nunca la FAT ni el directorio. Así un corte de luz no rompe la tarjeta.
    265 - Para que un guardado a medias no pierda todo: dos mitades alternadas, cada una
    266   con cabecera (firma + número de secuencia + largo + checksum); escribir primero
    267   los datos y **último** el sector de cabecera; al cargar, usar la mitad válida
    268   con secuencia más alta.
    269 - Init SD en modo SPI: 400 kHz, >74 clocks con CS alto, CMD0, CMD8, ACMD41
    270   (con bit HCS), CMD58; después subir la velocidad.
    271 - **Nunca escribir la flash de la Pico** para guardar.
    272 
    273 ## 7. Otras reglas
    274 
    275 - **Pila chica** (el core 0 tiene unos pocos KB): nada de arreglos grandes locales;
    276   usar `static`/globales.
    277 - Sin FPU: enteros o punto fijo.
    278 - `pico_enable_stdio_usb(... 0)`: el USB de la Pico no está disponible como
    279   consola en este aparato. Para depurar: pantalla y/o un archivo de log en la SD.
    280 - Probar primero en un simulador de PC si se puede (escribir el juego contra una
    281   capa chica de "hal": dibujar, leer tecla, ms, guardar; con una implementación
    282   para la Pico y otra con SDL para la PC).
    283 
    284 ---
    285 
    286 ## Apéndice A: `memmap_sdboot_200k.ld` (para el cargador viejo)
    287 
    288 Es el `memmap_default.ld` del pico-sdk para RP2040 **sin la sección `.boot2`**
    289 y con la flash empezando en +200k:
    290 
    291 ```ld
    292 MEMORY
    293 {
    294     FLASH(rx) : ORIGIN = 0x10000000 + 200k, LENGTH = 2048k - 200k
    295     RAM(rwx) : ORIGIN =  0x20000000, LENGTH = 256k
    296     SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
    297     SCRATCH_Y(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
    298 }
    299 
    300 ENTRY(_entry_point)
    301 
    302 SECTIONS
    303 {
    304     /* Second stage bootloader is prepended to the image. It must be 256 bytes big
    305        and checksummed. It is usually built by the boot_stage2 target
    306        in the Raspberry Pi Pico SDK
    307     */
    308 
    309     .flash_begin : {
    310         __flash_binary_start = .;
    311     } > FLASH
    312 
    313     /* The second stage will always enter the image at the start of .text.
    314        The debugger will use the ELF entry point, which is the _entry_point
    315        symbol if present, otherwise defaults to start of .text.
    316        This can be used to transfer control back to the bootrom on debugger
    317        launches only, to perform proper flash setup.
    318     */
    319 
    320     .text : {
    321         __logical_binary_start = .;
    322         KEEP (*(.vectors))
    323         KEEP (*(.binary_info_header))
    324         __binary_info_header_end = .;
    325         KEEP (*(.reset))
    326         /* TODO revisit this now memset/memcpy/float in ROM */
    327         /* bit of a hack right now to exclude all floating point and time critical (e.g. memset, memcpy) code from
    328          * FLASH ... we will include any thing excluded here in .data below by default */
    329         *(.init)
    330         *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .text*)
    331         *(.fini)
    332         /* Pull all c'tors into .text */
    333         *crtbegin.o(.ctors)
    334         *crtbegin?.o(.ctors)
    335         *(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
    336         *(SORT(.ctors.*))
    337         *(.ctors)
    338         /* Followed by destructors */
    339         *crtbegin.o(.dtors)
    340         *crtbegin?.o(.dtors)
    341         *(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
    342         *(SORT(.dtors.*))
    343         *(.dtors)
    344 
    345         *(.eh_frame*)
    346         . = ALIGN(4);
    347     } > FLASH
    348 
    349     .rodata : {
    350         *(EXCLUDE_FILE(*libgcc.a: *libc.a:*lib_a-mem*.o *libm.a:) .rodata*)
    351         . = ALIGN(4);
    352         *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*)))
    353         . = ALIGN(4);
    354     } > FLASH
    355 
    356     .ARM.extab :
    357     {
    358         *(.ARM.extab* .gnu.linkonce.armextab.*)
    359     } > FLASH
    360 
    361     __exidx_start = .;
    362     .ARM.exidx :
    363     {
    364         *(.ARM.exidx* .gnu.linkonce.armexidx.*)
    365     } > FLASH
    366     __exidx_end = .;
    367 
    368     /* Machine inspectable binary information */
    369     . = ALIGN(4);
    370     __binary_info_start = .;
    371     .binary_info :
    372     {
    373         KEEP(*(.binary_info.keep.*))
    374         *(.binary_info.*)
    375     } > FLASH
    376     __binary_info_end = .;
    377     . = ALIGN(4);
    378 
    379    .ram_vector_table (NOLOAD): {
    380         *(.ram_vector_table)
    381     } > RAM
    382 
    383     .data : {
    384         __data_start__ = .;
    385         *(vtable)
    386 
    387         *(.time_critical*)
    388 
    389         /* remaining .text and .rodata; i.e. stuff we exclude above because we want it in RAM */
    390         *(.text*)
    391         . = ALIGN(4);
    392         *(.rodata*)
    393         . = ALIGN(4);
    394 
    395         *(.data*)
    396 
    397         . = ALIGN(4);
    398         *(.after_data.*)
    399         . = ALIGN(4);
    400         /* preinit data */
    401         PROVIDE_HIDDEN (__mutex_array_start = .);
    402         KEEP(*(SORT(.mutex_array.*)))
    403         KEEP(*(.mutex_array))
    404         PROVIDE_HIDDEN (__mutex_array_end = .);
    405 
    406         . = ALIGN(4);
    407         /* preinit data */
    408         PROVIDE_HIDDEN (__preinit_array_start = .);
    409         KEEP(*(SORT(.preinit_array.*)))
    410         KEEP(*(.preinit_array))
    411         PROVIDE_HIDDEN (__preinit_array_end = .);
    412 
    413         . = ALIGN(4);
    414         /* init data */
    415         PROVIDE_HIDDEN (__init_array_start = .);
    416         KEEP(*(SORT(.init_array.*)))
    417         KEEP(*(.init_array))
    418         PROVIDE_HIDDEN (__init_array_end = .);
    419 
    420         . = ALIGN(4);
    421         /* finit data */
    422         PROVIDE_HIDDEN (__fini_array_start = .);
    423         *(SORT(.fini_array.*))
    424         *(.fini_array)
    425         PROVIDE_HIDDEN (__fini_array_end = .);
    426 
    427         *(.jcr)
    428         . = ALIGN(4);
    429         /* All data end */
    430         __data_end__ = .;
    431     } > RAM AT> FLASH
    432     /* __etext is (for backwards compatibility) the name of the .data init source pointer (...) */
    433     __etext = LOADADDR(.data);
    434 
    435     .uninitialized_data (NOLOAD): {
    436         . = ALIGN(4);
    437         *(.uninitialized_data*)
    438     } > RAM
    439 
    440     /* Start and end symbols must be word-aligned */
    441     .scratch_x : {
    442         __scratch_x_start__ = .;
    443         *(.scratch_x.*)
    444         . = ALIGN(4);
    445         __scratch_x_end__ = .;
    446     } > SCRATCH_X AT > FLASH
    447     __scratch_x_source__ = LOADADDR(.scratch_x);
    448 
    449     .scratch_y : {
    450         __scratch_y_start__ = .;
    451         *(.scratch_y.*)
    452         . = ALIGN(4);
    453         __scratch_y_end__ = .;
    454     } > SCRATCH_Y AT > FLASH
    455     __scratch_y_source__ = LOADADDR(.scratch_y);
    456 
    457     .bss  : {
    458         . = ALIGN(4);
    459         __bss_start__ = .;
    460         *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*)))
    461         *(COMMON)
    462         . = ALIGN(4);
    463         __bss_end__ = .;
    464     } > RAM
    465 
    466     .heap (NOLOAD):
    467     {
    468         __end__ = .;
    469         end = __end__;
    470         KEEP(*(.heap*))
    471         __HeapLimit = .;
    472     } > RAM
    473 
    474     /* .stack*_dummy section doesn't contains any symbols. It is only
    475      * used for linker to calculate size of stack sections, and assign
    476      * values to stack symbols later
    477      *
    478      * stack1 section may be empty/missing if platform_launch_core1 is not used */
    479 
    480     /* by default we put core 0 stack at the end of scratch Y, so that if core 1
    481      * stack is not used then all of SCRATCH_X is free.
    482      */
    483     .stack1_dummy (NOLOAD):
    484     {
    485         *(.stack1*)
    486     } > SCRATCH_X
    487     .stack_dummy (NOLOAD):
    488     {
    489         KEEP(*(.stack*))
    490     } > SCRATCH_Y
    491 
    492     .flash_end : {
    493         PROVIDE(__flash_binary_end = .);
    494     } > FLASH
    495 
    496     /* stack limit is poorly named, but historically is maximum heap ptr */
    497     __StackLimit = ORIGIN(RAM) + LENGTH(RAM);
    498     __StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X);
    499     __StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y);
    500     __StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy);
    501     __StackBottom = __StackTop - SIZEOF(.stack_dummy);
    502     PROVIDE(__stack = __StackTop);
    503 
    504     /* Check if data + heap + stack exceeds RAM limit */
    505     ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed")
    506 
    507     ASSERT( __binary_info_header_end - __logical_binary_start <= 256, "Binary info must be in first 256 bytes of the binary")
    508     /* todo assert on extra code */
    509 }
    510 
    511 ```
    512 
    513 ## Apéndice B: `CMakeLists.txt` con los dos targets
    514 
    515 ```cmake
    516 cmake_minimum_required(VERSION 3.13)
    517 set(PICO_BOARD pico CACHE STRING "Board")
    518 include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
    519 project(mi_app C CXX ASM)
    520 set(CMAKE_C_STANDARD 11)
    521 pico_sdk_init()
    522 
    523 set(SRC main.c)   # agregar los .c
    524 set(LIBS pico_stdlib pico_multicore hardware_spi hardware_i2c)
    525 
    526 foreach(T mi_app mi_app_sd200k)
    527     add_executable(${T} ${SRC})
    528     target_link_libraries(${T} ${LIBS})
    529     pico_enable_stdio_uart(${T} 1)
    530     pico_enable_stdio_usb(${T} 0)
    531     pico_add_extra_outputs(${T})      # genera .uf2 y .bin
    532 endforeach()
    533 
    534 # variante para el cargador viejo (/firmware): enlazada en flash+200k, sin boot2
    535 pico_set_linker_script(mi_app_sd200k ${CMAKE_CURRENT_LIST_DIR}/memmap_sdboot_200k.ld)
    536 target_link_options(mi_app_sd200k PRIVATE -Wl,--undefined=app_early_reset -Wl,--undefined=app_real_reset)
    537 ```
    538 
    539 Después de `make`:
    540 
    541 ```sh
    542 cp mi_app.uf2 SD/pico1-apps/MiApp.uf2                      # uf2loader
    543 cp mi_app_sd200k.bin SD/firmware/MiApp.bin                 # cargador viejo
    544 python3 patch_reset.py mi_app_sd200k.elf SD/firmware/MiApp.bin   # ¡obligatorio!
    545 ```
    546 
    547 ## Apéndice C: `patch_reset.py`
    548 
    549 ```python
    550 #!/usr/bin/env python3
    551 """Pone app_early_reset como vector de reset de un .bin de la PicoCalc.
    552 Uso: patch_reset.py programa.elf programa.bin"""
    553 import struct, subprocess, sys
    554 
    555 elf, binf = sys.argv[1], sys.argv[2]
    556 syms = {}
    557 for line in subprocess.check_output(["arm-none-eabi-nm", elf], text=True).splitlines():
    558     parts = line.split()
    559     if len(parts) == 3:
    560         syms[parts[2]] = int(parts[0], 16)
    561 base = syms["__vectors"]
    562 shim, real, var = syms["app_early_reset"], syms["_reset_handler"], syms["app_real_reset"]
    563 data = bytearray(open(binf, "rb").read())
    564 old_reset = struct.unpack_from("<I", data, 4)[0]
    565 assert old_reset == (real | 1), "vector[1] inesperado: %#x (reset real %#x)" % (old_reset, real)
    566 assert struct.unpack_from("<I", data, var - base)[0] == 0xDEADBEEF, "no encuentro app_real_reset en el .bin"
    567 struct.pack_into("<I", data, var - base, real | 1)
    568 struct.pack_into("<I", data, 4, shim | 1)
    569 open(binf, "wb").write(data)
    570 print("vector de reset: %#x -> app_early_reset %#x (sigue a %#x)" % (old_reset, shim | 1, real | 1))
    571 ```