
RP2350 RISC-V 코어용 SWD 디버그 프로브 라이브러리입니다. PIO 비트 뱅잉을 통해 두 개의 GPIO 와이어로 중단/재개/단계 실행, 레지스터 및 메모리 접근, 코드 실행, 명령어 추적을 제공합니다.
RP2350 RISC-V (Hazard3) 코어용 SWD 디버그 프로브. 하나의 Pico2가 두 개의 GPIO 와이어를 통해 다른 Pico2를 디버깅합니다.
코드의 약 80%는 바이브 코딩되었습니다. README는 거의 완전히 생성되었습니다(이 바이브 코드 경고 섹션 제외). 오실로스코프와 문서를 보며 많은 밤을 보냈고, sba/read/write 레지스터, 추상 명령어, progbuf를 할 수 있는 작동하는 프로토타입을 만들었습니다. 나머지는 Claude Code로 완료했습니다. 테스트는 꽤 포괄적인 테스트 스위트이며, 저는 이 라이브러리의 핵심을 제 프로젝트에서 사용하지만, 흔히 말하듯 "hic sunt dracones"(여기 용이 있다)입니다. 또한 README를 읽고 코드에서 잘못된 점을 발견하지 못했으며(잘못되거나 불명확한 부분은 제거했습니다).
이 프로젝트는 제가 100% 이해하지 못하고, 명백히 "사용할 수 있는" 기존 코드가 없는 더 복잡한 프로젝트를 바이브 코딩하는 사례 연구였습니다. 약 1000줄의 코드로 시작했으며, 제가 직접 작성하고 잘 알고 있었고, rp2350, arm swd, riscv 디버그 문서를 읽고, 오실로스코프와 openocd로 데이터를 캡처한 다음 디코딩하여 웨이크업 시퀀스와 읽기/쓰기 명령어를 분석했습니다. 작동하게 만든 후 Claude에게 주어 다른 프로젝트에서 사용할 수 있는 라이브러리로 만들게 했고, 그 후 천천히 구축해 나갔습니다.
약 3~4천 줄의 코드 이후에는 무슨 일이 일어나고 있는지 완전히 감을 잃었고, 이 코드를 제가 작성했다고 생각하지 않지만, 테스트를 계속 추가하는 것이 "좋게" 느껴졌거나 적어도 안심이 되었습니다.
약간의 가스라이팅이 있었는데, 특히 dap_read_mem32가 MEM-AP TAR/DRW/RDBUFF 프로토콜이 아닌 RAM에서 읽는 것이라고 오해하여 엄청난 양의 넌센스가 발생했습니다.
전반적으로 끔찍한 경험이었다고 말하고 싶습니다. 거의 10,000줄의 코드를 작성하는 데 10시간이 걸렸지만, 이 프로젝트를 제 프로젝트로 생각하지 않으며, 성취감이나 성장감도 없습니다.
반대로, AI를 사용하여 모든 문서(수천 페이지)를 읽고, 오실로스코프 데이터를 디코딩하는 유용한 스크립트를 작성하고, 문서에서 압축된 C 구조체를 만드는 등의 작업은 매우 좋았고, 그 후에는 기분이 좋았습니다. 첫 번째 레지스터를 읽고 SBA를 통해 메모리를 읽을 수 있었을 때는 정말 기분이 좋았습니다.
주요 문제는 '취향'입니다. 제가 코드를 작성할 때는 좋은지 나쁜지 느낌이 옵니다. 작성하는 동안 잘못된 것을 알지만, Claude Code를 사용하면 매우 빨리 무감각해져서 구분할 수 없게 됩니다. "읽기"는 괜찮지만, 느낌이 어떤지 모르겠습니다. 이 경우 코드가 약 4배(1k에서 4k 라인)로 늘어났을 때 발생했습니다. 더 나쁜 것은, 코드에 대한 제 정신 모델이 완전히 사라졌고, 그와 함께 소유권도 사라졌습니다.
토큰에는 이유나 목적이 없습니다. 그래서 코드를 읽는 것이 엄청나게 어렵습니다. 각 토큰이 완전히 넌센스일 수 있기 때문입니다. 사람이 작성한 코드를 읽을 때는 기호에 목적이 있습니다. 누군가 "이걸 변수에 넣고, 나중에 상태를 확인해야지"라고 생각한 것입니다. 그래서 나는 그 사람인 척하고, 왜 이것을 작성했을까? 하고 생각합니다. 곧 이해하게 됩니다. 그들은 인간이고 나도 인간이기 때문입니다. 하지만 AI 기호에는 이유가 없고, 더 나쁜 것은 모두 기만적으로 정확해 보여서, 잘못된 것인지 10배는 더 열심히 생각해야 합니다. 인간의 코드(자신의 코드 포함)는 얼마나 신뢰할 수 있는지 평가하기가 꽤 쉽고, 일관성이 있습니다. AI 코드에서는 한 함수가 당신이 작성했을 것보다 훨씬 나을 수 있고, 두 줄 아래의 코드는 엄청나게 좋아 보이지만 구조적으로 잘못된 카고 컬트 쓰레기일 수 있습니다.
결국 저는 와이어, 타이밍, 하위 레벨 AP/DP 메커니즘, SBA, progbuf에 대한 좋은 이해를 얻었다고 말할 수 있지만, 전체를 직접 작성하지 않은 것을 후회합니다. 10배의 시간이 걸렸더라도 말이죠.
정말 싫어요.
그리고 역겨움과 수치심을 느끼지 않을 수 없습니다. 이것이 지금의 프로그래밍인가요? 이것이 중간 단계이고 더 나은 방향으로 변하기를 진심으로 바랍니다. 문제는 "더 나은 것"이 무엇인지 모른다는 것입니다. 어떤 사람들에게는 코드를 작성하지 않는 것이고, 다른 사람들에게는 문제를 모델링하지 않는 것이며, 또 다른 사람들에게는 생각하지 않아도 되는 것입니다. 제 경우에는 잘 모르겠습니다. 저는 물건을 만들고 싶고, 많은 경우 무언가를 알고 싶지 않지만 사용하고 싶습니다. 예를 들어 rp2350 USB 호스트 컨트롤러에서 인터럽트를 재무장하는 방식과 epx 레지스터가 공유되는 방식은 아마 좋은 이유로 매우 짜증나지만, 저는 그것을 사용하여 제 CBI 드라이버를 만들고 싶을 뿐입니다.
제가 만들고 싶은 것이 무엇인지 질문해야 할 것 같습니다. USB 칩 레지스터에서 CBI, UFI, FAT16, OS까지 스택을 올라갈 수 있기 때문입니다. 하지만 왜 거기서 멈추나요? 회로도, PCB, CAD 파일을 만들고, 공장에 자동으로 보내고, 그냥 저에게 배송합니까? 하지만 왜 거기서 멈추나요? 웹샵을 만들고, 판매를 시작하고, 커뮤니티를 만들고, 광고, 마케팅, 언박싱 비디오를 생성하고, 아마도 바이럴 밈? 주문을 공장에서 직접 처리하고, 주문에 따라, 문제가 있으면 고객 지원이 준비되어 있습니다.
그동안 저는 무엇을 합니까? 해변에 앉아 있나요? 저는 해변을 싫어합니다.
어디서 멈출까요?
추신: 시인이 말했듯이: 당신이 원하는 것을 얻는 대가는 당신이 한때 원했던 것을 얻는 것입니다.
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)
각 레이어는 자체 상태를 유지하며 바로 이웃하고만 통신합니다.
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);
두 hart(0과 1)는 독립적으로 제어 가능합니다.
시스템 버스 접근(System Bus Access)을 통한 비침습적 접근입니다. Hart가 실행 중인 상태에서도 작동합니다.
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);
블록 전송은 성능을 위해 SBA 자동 증가(auto-increment)를 사용합니다.
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);
대상 SRAM에 업로드하고, 검증하고, PC를 설정하고, 실행을 재개합니다.
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);
DCSR.step을 통해 명령어를 단일 스텝(single-step)합니다. 레지스터 캡처 없이 명령어당 ~5ms, 전체 레지스터 캡처 시 ~80ms.
디버그 컨텍스트에서 직접 RISC-V 명령어 실행:
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);
CSR 접근은 Hazard3가 추상 CSR 명령어를 지원하지 않으므로 내부적으로 이를 사용합니다.
add_subdirectory(lib/pico2-swd-riscv)
target_link_libraries(your_app pico2_swd_riscv)
디버그 상세도 (컴파일 타임):
set(PICO2_SWD_DEBUG_LEVEL 3) # 0=없음, 1=경고, 2=정보, 3=디버그
SWD는 8비트 요청 패킷(start, APnDP, RnW, addr[3:2], parity, stop, park), 3비트 ACK(OK=1, WAIT=2, FAULT=4) 및 턴어라운드(turnaround) 사이클과 함께 33비트 데이터 단계를 사용합니다. 턴어라운드 사이클은 SWDIO 방향 변경을 처리합니다. WAIT 응답은 자동으로 재시도됩니다(기본값: 재시도 5회, 100us 백오프).
비표준: [15:12]=APSEL, [11:8]=0xD, [7:4]=bank, [0]=ctrlsel. bits[11:8]의 0xD는 필수지만 문서화되지 않았습니다.
Bank 1 CSW를 통한 3단계 핸드셰이크: 비활성화(0x00000000), 활성화(0x00000001), 전체 구성(0x07FFFFC1). 예상 상태 응답: 0x04010001.
GPR은 추상 명령어(regno 0x1000+n, 32비트 전송)를 통해 접근. CSR은 프로그램 버퍼를 통해: s0 저장, csrr s0, <csr> 또는 csrw <csr>, s0 실행, s0 읽기/복원.
SBCS는 sbaccess=32bit 및 sbreadonaddr로 구성됩니다. SBADDRESS0에 쓰면 버스 읽기가 트리거됩니다. 데이터는 SBDATA0에서 즉시 사용 가능합니다. 블록 전송은 스트리밍 읽기/쓰기를 위해 sbautoincrement를 활성화하여 단어별 주소 설정 없이 수행됩니다.
progbuf를 통해 DCSR을 읽고, step 비트(비트 2)를 설정하고, hart를 재개합니다. Hart는 하나의 명령어를 실행하고 디버그 모드로 다시 진입합니다. 이후 step 비트를 지웁니다.
MIT. LICENSE를 참조하십시오.