
Sonda de depuración SWD para núcleos RISC-V (Hazard3) de RP2350. Un Pico2 depura a otro mediante dos cables GPIO.
Aproximadamente el 80% del código es código vibe; el readme está casi completamente generado (excepto toda la sección de advertencia de código vibe). Pasé muchas noches con el osciloscopio y la documentación e hice un prototipo funcional que podía hacer sba/lectura/escritura de registros, comandos abstractos y progbuf; el resto se hizo con claude code. Los tests son una suite de pruebas bastante completa y uso el núcleo de la librería en mis propios proyectos, pero, como dicen, «hic sunt dracones». También leí el readme y el código y no noté nada incorrecto (y eliminé las partes incorrectas/poco claras).
Este proyecto fue mi caso de estudio sobre vibecoding en un proyecto más complicado que no entiendo al 100% y para el que no hay ningún código existente obvio que pueda «usarse». Empezó como ~1000 líneas de código que escribí y conocía muy bien, leyendo la documentación de rp2350, arm swd y riscv debug, capturando datos con osciloscopio y openocd, decodificándolos y analizando la secuencia de wakeup y luego los comandos de lectura/escritura. Cuando lo hice funcionar, se lo di a claude para que lo convirtiera en una librería que pudiera usar en otros proyectos, y luego lo fui construyendo poco a poco.
Después de unas 3-4k líneas de código perdí por completo el hilo de lo que estaba pasando, y ya no consideraría que este código lo he escrito yo, pero añadir más y más tests resultaba «agradable», o al menos reconfortante.
Hubo algo de gaslighting, sobre todo cuando malinterpretó dap_read_mem32 pensando que leía de la RAM y no el protocolo MEM-AP TAR/DRW/RDBUFF, lo que condujo a una cantidad increíble de disparates.
En general diría que fue una experiencia horrible. Aunque tardé 10 horas en escribir cerca de 10000 líneas de código, no considero este proyecto como mío, y no tengo sensación de logro ni de crecimiento.
En contraste, usar IA para leer toda la documentación (que son miles de páginas) y escribir scripts útiles para decodificar los datos del osciloscopio, crear structs de C empaquetadas a partir de la documentación, etc., fue muy agradable, y sí me sentí bien después. En el momento en que leí el primer registro y luego, cuando pude leer memoria vía SBA, me sentí increíble.
El problema principal es el gusto: cuando escribo código siento si es bueno o malo; mientras lo escribo, sé si está mal, pero usando claude code me insensibilizo muy rápido y simplemente no puedo distinguirlo. «Se lee» bien, pero no sé cómo se siente. En este caso ocurrió cuando el código creció unas 4 veces, de 1k a 4k líneas. Y lo peor de todo: mi modelo mental del código ha desaparecido por completo, y con él, mi sentido de propiedad.
Los tokens no tienen razón ni propósito, lo que hace que leer código sea ridículamente difícil, ya que cada token puede ser un completo disparate. Al leer código humano, los símbolos tienen un propósito: alguien pensó «pondré esto en una variable, luego comprobaré su estado», así que finjo ser esa persona y pienso por qué habrán escrito esto. Poco después lo entiendo, porque son humanos y yo soy humano. Pero los símbolos de la IA no tienen razón, y lo peor de todo es que todos parecen engañosamente correctos, así que tengo que pensar 10 veces más para saber si está mal. Con cualquier código humano (incluido el tuyo propio) es bastante fácil calibrar cuánto puedes confiar en él, y es bastante consistente. Con el código de la IA, una función puede ser mucho mejor de lo que tú habrías escrito, y el código 2 líneas más abajo puede ser porquería de culto cargo que se ve increíblemente bien, pero es estructuralmente incorrecta.
Al final diría que he adquirido un buen entendimiento de los cables, los tiempos y la mecánica de bajo nivel de ap/dp, sba y progbuf, pero me arrepiento de no haber escrito todo yo mismo, aunque me hubiera llevado 10 veces más tiempo.
Odio esto, joder.
Y no puedo evitar sentir asco y vergüenza. ¿Es esto lo que es programar ahora? De verdad espero que esto sea una etapa intermedia y que cambie para mejor. El problema es que no sé qué es «mejor»: para algunos parece ser no escribir el código, para otros no modelar el problema y para un tercer grupo no tener que pensar. En mi caso, no estoy seguro. Sí quiero crear cosas, y muchas veces no quiero saber algo, sino usarlo; por ejemplo, el controlador de host USB del rp2350: la forma en que tienes que rearmar las interrupciones y cómo se comparte el registro epx es súper molesta, probablemente por buenas razones, pero yo solo quiero usarlo para hacer mi driver CBI.
Supongo que la pregunta es qué es lo que quiero crear, porque puedes subir mucho en la pila: desde los registros del chip USB hasta CBI, UFI, FAT16 y el sistema operativo del ordenador retro que estoy construyendo; pero ¿por qué parar? ¿Hacer los esquemáticos, los PCB, los archivos CAD, quizás enviarlo automáticamente a la fábrica y luego simplemente enviármelo? ¿Pero por qué parar? ¿Hacer mi tienda web, empezar a vender, crear una comunidad, anuncios, marketing, generar algunos vídeos de unboxing, quizás algunos memes virales? ¿Procesar los pedidos directamente a la fábrica, bajo demanda, y si hay algún problema, que esté listo con atención al cliente?
¿Qué hago mientras tanto? ¿Sentarme en la playa? Odio la playa.
¿Dónde se detiene?
PD: como dijo el poeta: el precio de conseguir lo que quieres es conseguir lo que una vez quisiste.
Application
|
rp2350.c RISC-V Debug Module (halt/resume/step, registers, memory, trace)
|
dap.c Debug Access Port (DP/AP registers, bank caching, MEM-AP)
|
swd_protocol.c SWD wire protocol (PIO bit-banging, packet encoding, retry)
|
swd.pio PIO state machine (4-cycle SWCLK, bidirectional SWDIO)
Cada capa mantiene su propio estado y se comunica únicamente con su vecina.
swd_config_t config = swd_config_default();
config.pin_swclk = 2;
config.pin_swdio = 3;
swd_target_t *target = swd_target_create(&config);
swd_connect(target);
rp2350_init(target);
rp2350_halt(target, 0);
swd_result_t pc = rp2350_read_pc(target, 0);
rp2350_resume(target, 0);
swd_target_destroy(target);
rp2350_halt(target, 0);
rp2350_step(target, 0);
rp2350_resume(target, 0);
rp2350_reset(target, 0, true);
swd_result_t pc = rp2350_read_pc(target, 0);
rp2350_write_pc(target, 0, 0x20000000);
swd_result_t val = rp2350_read_reg(target, 0, 5);
rp2350_write_reg(target, 0, 5, 0xDEADBEEF);
uint32_t regs[32];
rp2350_read_all_regs(target, 0, regs);
swd_result_t csr = rp2350_read_csr(target, 0, 0x300);
rp2350_write_csr(target, 0, 0x300, value);
Ambos harts (0 y 1) se controlan de forma independiente.
No intrusivo mediante System Bus Access. Funciona mientras el hart está en ejecución.
swd_result_t val = rp2350_read_mem32(target, 0x20000000);
rp2350_write_mem32(target, 0x20000000, 0xDEADBEEF);
rp2350_read_mem16(target, addr);
rp2350_write_mem8(target, addr, byte);
uint32_t buf[256];
rp2350_read_mem_block(target, 0x20000000, buf, 256);
rp2350_write_mem_block(target, 0x20000000, buf, 256);
Las transferencias por bloques usan autoincremento SBA para mayor rendimiento.
const uint32_t program[] = {
0x200415b7, // lui a1, 0x20040
0xabcd0537, // lui a0, 0xabcd0
0x00a5a223, // sw a0, 4(a1)
0x0000006f, // j . (loop)
};
rp2350_execute_code(target, 0, 0x20000000, program, 4);
Sube a la SRAM de destino, verifica, establece el PC y reanuda.
bool on_instruction(const trace_record_t *rec, void *ctx) {
printf("0x%08x: 0x%08x\n", rec->pc, rec->instruction);
return true;
}
int traced = rp2350_trace(target, 0, 100, on_instruction, NULL, false);
Ejecuta paso a paso las instrucciones mediante DCSR.step. ~5ms por instrucción sin captura de registros, ~80ms con captura completa de registros.
Ejecución directa de instrucciones RISC-V en contexto de depuración:
uint32_t progbuf[] = {
0x34202473, // csrr s0, mcause
0x00100073 // ebreak
};
rp2350_execute_progbuf(target, 0, progbuf, 2);
swd_result_t mcause = rp2350_read_reg(target, 0, 8);
El acceso a CSR usa esto internamente, ya que Hazard3 no admite comandos CSR abstractos.
add_subdirectory(lib/pico2-swd-riscv)
target_link_libraries(your_app pico2_swd_riscv)
Verbosidad de depuración (en tiempo de compilación):
set(PICO2_SWD_DEBUG_LEVEL 3) # 0=none, 1=warn, 2=info, 3=debug
SWD usa paquetes de solicitud de 8 bits (start, APnDP, RnW, addr[3:2], paridad, stop, park), ACK de 3 bits (OK=1, WAIT=2, FAULT=4) y fases de datos de 33 bits con paridad. Los ciclos de turnaround gestionan los cambios de dirección de SWDIO. Las respuestas WAIT se reintentan automáticamente (por defecto: 5 reintentos, retroceso de 100us).
No estándar: [15:12]=APSEL, [11:8]=0xD, [7:4]=bank, [0]=ctrlsel. El 0xD en los bits[11:8] es obligatorio pero no está documentado.
Protocolo de enlace de tres fases a través del CSW del Banco 1: desactivar (0x00000000), activar (0x00000001), configuración completa (0x07FFFFC1). Respuesta de estado esperada: 0x04010001.
GPRs mediante comandos abstractos (regno 0x1000+n, transferencia de 32 bits). CSRs mediante el búfer de programa: guardar s0, ejecutar csrr s0, <csr> o csrw <csr>, s0, leer/restaurar s0.
SBCS configurado con sbaccess=32bit y sbreadonaddr. Escribir en SBADDRESS0 dispara la lectura del bus; los datos están disponibles inmediatamente en SBDATA0. Las transferencias por bloques habilitan sbautoincrement para lecturas/escrituras en streaming sin configuración de dirección por palabra.
Leer DCSR mediante progbuf, establecer el bit de paso (bit 2), reanudar el hart. El hart ejecuta una instrucción y vuelve a entrar en modo de depuración. Limpiar el bit de paso después.
MIT. Consulta LICENSE.