# DSP 프로그래밍 가이드

이 페이지는 SP500 DSP API로 애플리케이션을 빌드해 REGULUS 장치에 배포하는 방법과 각 연산의 사용법을 설명합니다. API의 개념은 [DSP 개요](dsp_overview.md)를, 모든 함수·구조체·상수는 DSP API 레퍼런스를 참고하세요. 완성된 예제 애플리케이션은 [예제](#dsp-examples)를 참고하세요.

## 시작하기 전에

애플리케이션이 API를 호출하기 전에 장치에 다음이 준비되어 있어야 합니다.

- **DSP 펌웨어가 로드되어 있어야 합니다.** 장치가 부팅된 직후에는 DSP에 펌웨어가 없습니다. 부팅할 때마다 `dsp_firmware_load`를 한 번 실행하세요. [DSP 펌웨어 로드](#loading-the-dsp-firmware)를 참고하세요.
- **`libopencv_core.so.409`가 설치되어 있어야 합니다.** API에 OpenCV 타입은 나오지 않지만 `libsp500.so`가 이 라이브러리에 링크되어 있어 시작 시 로더가 이를 찾습니다.

라이브러리가 필요로 하는 항목은 장치에서 다음과 같이 확인합니다.

```bash
readelf -d libsp500.so | grep NEEDED
ldd libsp500.so
```

(loading-the-dsp-firmware)=

## DSP 펌웨어 로드

Vision DSP는 자체 펌웨어(FreeRTOS와 SP500 연산의 DSP 쪽 구현)를 실행합니다. 장치가 부팅된 직후에는 DSP에 펌웨어가 로드되어 있지 않으므로, DSP를 사용하는 애플리케이션을 시작하기 전에 펌웨어를 로드하세요.

```bash
dsp_firmware_load
```

```text
SP500 reset assert
SP500 ptcm.bin loading complete
SP500 dtcm.bin loading complete
SP500 external.bin loading complete
SP500 reset de-assert
```

- 부팅 후 한 번만 실행하면 됩니다. 로드된 펌웨어는 장치가 꺼지거나 재부팅될 때까지 유지되므로, 애플리케이션을 다시 시작하거나 다른 애플리케이션을 실행할 때 다시 로드할 필요가 없습니다.
- `dsp_firmware_load`는 SP500 패키지가 포함된 REGULUS 이미지에 설치되어 있습니다. 다음 펌웨어 이미지를 로드합니다.

  ```text
  /usr/lib/firmware/mobilint/sp500/ptcm.bin
  /usr/lib/firmware/mobilint/sp500/dtcm.bin
  /usr/lib/firmware/mobilint/sp500/external.bin
  ```

- 펌웨어가 로드되어 있지 않으면 `sp500_dsp_open()`은 제한 시간(기본 10초) 동안 기다린 뒤 `SP500_ERR_TIMEOUT`을 반환합니다.

## 애플리케이션 빌드

애플리케이션은 호스트 PC에서 REGULUS 툴체인으로 크로스 컴파일합니다. 먼저 툴체인 환경을 설정합니다.

```bash
source /opt/crosstools/mobilint/2.0.0/<VERSION>/environment-setup-cortexa53-mobilint-linux
```

그다음 SDK 헤더 경로를 지정하고 `libsp500`을 링크해 컴파일합니다.

```bash
$CXX my_app.cpp -I<sdk>/include -L<sdk>/lib -lsp500 -o my_app
```

링크 옵션에 다른 라이브러리를 추가할 필요는 없습니다. 헤더는 `sp500/` 경로로 포함합니다.

```c
#include <sp500/letterbox.h>
#include <sp500/log_mel.h>
#include <sp500/yolo.h>
```

## 장치에 배포

SP500 패키지가 포함된 REGULUS 이미지에는 `libsp500.so`와 그 링크가 이미 `/usr/lib`에 설치되어 있습니다. 라이브러리를 직접 복사해야 하는 경우는 다른 버전의 라이브러리를 애플리케이션과 함께 배포할 때뿐입니다.

애플리케이션 바이너리에는 `libsp500.so`가 아니라 SONAME인 `libsp500.so.1`이 기록됩니다. 라이브러리의 세 가지 이름을 모두 심볼릭 링크를 유지한 채 복사하세요.

```bash
cp -a lib/libsp500.so* /target/usr/lib/
```

`libsp500.so`만 복사하거나 일반 `cp`로 복사해 링크가 파일 복사본으로 바뀌면 시작 시 다음 오류가 발생하거나 루트 파일 시스템에 라이브러리 전체 복사본이 세 개 남습니다.

```text
error while loading shared libraries: libsp500.so.1: cannot open shared object file
```

애플리케이션과 라이브러리를 따로 배포한다면 시작 시 둘의 major 버전이 일치하는지 확인하세요.

```c
if (sp500_version() / 10000 != SP500_VERSION_MAJOR) {
    /* the header and the library disagree */
}
```

## 애플리케이션 실행

애플리케이션을 장치에 복사합니다. 마지막 부팅 이후 DSP 펌웨어를 로드하지 않았다면 펌웨어를 로드한 뒤 애플리케이션을 시작합니다.

```bash
dsp_firmware_load
./my_app
```

(dsp-examples)=

## 예제

GitHub의 [regulus-dsp-examples](https://github.com/mobilint/regulus-dsp-examples)에는 이 페이지에서 설명하는 전처리·후처리 연산을 사용하는 SP500 DSP API 예제 애플리케이션이 모여 있습니다. 예제마다 빌드하고, 장치에 복사하고, DSP 펌웨어를 로드해 실행하는 방법이 함께 설명되어 있습니다.

## 첫 애플리케이션

다음 프로그램은 1920x1080 카메라 프레임을 640x640 모델 입력으로 letterbox하고, anchor-free YOLO detection head의 출력을 원본 프레임 좌표의 박스로 디코딩합니다.

```c
#include <sp500/letterbox.h>
#include <sp500/yolo.h>
#include <stdio.h>

static unsigned char frame[1920 * 1080 * 3];   /* captured BGR frame */

int main(void)
{
    /* Attach once, at start-up. This waits for the DSP firmware to register. */
    sp500_status rc = sp500_dsp_open(0);
    if (rc != SP500_OK) {
        fprintf(stderr, "SP500: %s\n", sp500_status_string(rc));
        return 1;
    }

    /* 1. 1920x1080 BGR frame -> 640x640 model input */
    sp500_letterbox_params lb;
    sp500_letterbox_params_init(&lb);
    lb.src_w = 1920;
    lb.src_h = 1080;

    sp500_buffer src = sp500_buffer_wrap(frame, 1920 * 1080 * 3);
    sp500_buffer dst = sp500_buffer_alloc(640 * 640 * 3, SP500_ALIGN_NPU);

    sp500_letterbox_result geom;
    rc = sp500_letterbox(&src, &lb, &dst, &geom);

    /* ... run the network on dst ... */

    /* 2. raw head tensors -> detections */
    sp500_yolo_params yp;
    sp500_yolo_params_init(&yp);
    yp.max_detections = 300;

    sp500_yolo_detection dets[300];
    sp500_yolo_output out;
    sp500_yolo_output_init(&out);
    out.records = sp500_buffer_wrap(dets, sizeof dets);

    sp500_yolo_raw raw;
    sp500_yolo_raw_init(&raw);
    /* set raw.box[i] and raw.cls[i] for i = 0, 1, 2 (strides 8, 16, 32) */

    sp500_yolo_result res;
    rc = sp500_yolo_decode(SP500_YOLO_HEAD_ANCHORLESS, SP500_YOLO_TASK_DETECT,
                           &raw, &yp, &out, &res);
    if (rc == SP500_OK) {
        for (uint32_t i = 0; i < res.count; ++i) {
            /* boxes are in the 640x640 network frame; undo the letterbox
               to get back to the 1920x1080 source */
            float x1 = (dets[i].x1 - geom.pad_x) / geom.scale;
            float y1 = (dets[i].y1 - geom.pad_y) / geom.scale;
            (void)x1; (void)y1;
        }
    }

    sp500_buffer_release(&dst);
    return 0;
}
```

다음 절부터 각 단계를 자세히 설명합니다.

## DSP 연결

다른 DSP 호출보다 먼저 `sp500_dsp_open(timeout_ms)`가 성공해야 합니다. 이 함수는 DSP 펌웨어가 등록될 때까지 최대 `timeout_ms` 밀리초 동안 대기하며, 0을 전달하면 기본값 10초를 사용합니다. 한 번 성공한 뒤에는 다시 호출해도 아무 작업 없이 바로 반환하므로 애플리케이션의 어느 모듈에서든 호출해도 됩니다.

연결에 성공하면 공유 메모리 영역도 매핑됩니다. 따라서 첫 DSP 호출 전에 실행되는 NPU 설정 코드에서도 `sp500_shmem_contains()`와 `sp500_shmem_phys_addr()`를 사용할 수 있습니다.

`sp500_dsp_open()`이 `SP500_ERR_TIMEOUT`을 반환하면 반복해서 재시도하지 말고 치명적인 시작 오류로 처리하세요. 제한 시간이 초과되면 DSP가 나타날 때까지 작업 스레드 하나가 링크 계층 안에서 계속 대기합니다.

## Letterbox

`sp500_letterbox()`는 원본 프레임을 `r = min(dst_w / src_w, dst_h / src_h)` 배율로 조정해 왜곡 없이 대상 크기에 맞추고, 남은 테두리를 상수 값으로 채웁니다.

프레임은 행 패딩이 없는 8비트 interleaved HWC 형식이며, 한 행은 정확히 `width * channels` 바이트입니다. `src_w`와 `src_h`는 반드시 설정해야 하며, 나머지 필드의 기본값은 다음과 같습니다.

| 필드 | 기본값 | 설명 |
| --- | --- | --- |
| `dst_w`, `dst_h` | 640, 640 | 대상 크기(픽셀)입니다. |
| `channels` | 3 | interleaved 채널 수입니다. |
| `interp` | `SP500_RESIZE_BILINEAR` | `SP500_RESIZE_NEAREST`, `_BILINEAR`, `_BICUBIC`, `_LANCZOS` 중 하나입니다. |
| `pad_value` | 114 | 모든 채널에 적용하는 테두리 값입니다. |
| `center` | 1 | 1이면 테두리를 균등하게 나누고, 0이면 오른쪽 아래에만 패딩합니다. |
| `scale_up` | 1 | 1이면 작은 원본을 확대할 수 있고, 0이면 배율이 1.0을 넘지 않습니다. |
| `swap_rb` | 0 | 1이면 출력할 때 채널 0과 2를 맞바꿉니다(BGR과 RGB 상호 변환). 채널이 3개여야 합니다. |

결과에는 적용된 배율과 왼쪽 위 패딩이 담깁니다. 이 값으로 모델 입력의 좌표를 원본 프레임 좌표로 되돌립니다.

```c
src_x = (dst_x - result.pad_x) / result.scale;
src_y = (dst_y - result.pad_y) / result.scale;
```

대상 버퍼를 `sp500_buffer_alloc(size, SP500_ALIGN_NPU)`로 할당하면 NPU가 모델 입력을 바로 읽을 수 있습니다.

## Log-Mel Spectrogram

`sp500_log_mel()`은 float32 PCM을 Whisper와 Qwen3-ASR 인코더가 받는 log-mel spectrogram으로 변환합니다. 계산 방식은 Hugging Face `WhisperFeatureExtractor`와 같습니다.

```c
sp500_log_mel_params p;
sp500_log_mel_params_init(&p);
p.n_samples = 16000 * 30;          /* 30 s at 16 kHz */

const int32_t frames = sp500_log_mel_num_frames(p.n_samples, p.hop_length);

sp500_buffer audio = sp500_buffer_wrap(pcm, p.n_samples * sizeof(float));
sp500_buffer feats = sp500_buffer_alloc(frames * p.n_mels * sizeof(float),
                                        SP500_ALIGN_DSP);

sp500_log_mel_result r;
if (sp500_log_mel(&audio, &p, &feats, &r) == SP500_OK) {
    /* feats.data holds r.n_frames * p.n_mels floats, frame-major */
}
sp500_buffer_release(&feats);
```

- 반드시 설정해야 하는 필드는 `n_samples`뿐입니다. 기본값은 Whisper 80-mel 전처리(16 kHz, `n_fft` 400, `hop_length` 160, 0~8000 Hz, Whisper 정규화 적용)입니다. Qwen3-ASR과 Whisper large-v3는 `n_mels = 128`을 사용합니다.
- 출력은 frame-major인 `features[t * n_mels + m]`이며 `n_frames = n_samples / hop_length`입니다. MXQ Whisper 모델이 받는 `[n_frames, n_mels]` 레이아웃이며, Hugging Face의 `input_features`는 이를 전치한 형태입니다.
- `sp500_log_mel_cpu()`는 DSP 없이 호스트 CPU에서 같은 결과를 계산합니다. bring-up 단계에서 DSP 결과를 검증하거나 대체 경로로 사용하세요.

30초 길이의 클립은 DSP가 처리할 프레임이 수천 개에 달하므로, 제출하기 전에 호출 기한을 늘리세요. [호출 기한](#call-deadline)을 참고하세요.

## YOLO Decode

YOLO 네트워크는 박스가 아니라 후보 위치마다 raw 값을 출력합니다. YOLO 함수는 좌표 디코딩, 신뢰도 임계값 미만 후보 제거, 중복 제거를 DSP 왕복 한 번으로 처리합니다.

### Head 계열 선택

YOLO는 세대마다 박스 인코딩과 중복 제거 방식이 다릅니다. 네트워크가 속한 계열을 선택하세요.

| 계열 | 박스 인코딩 | 중복 제거 | 태스크 | 모델 |
| --- | --- | --- | --- | --- |
| `SP500_YOLO_HEAD_ANCHORLESS` | DFL, 4 x 16 logits | 클래스별 NMS | Detect, segment, pose | v3u, v5u, v8, v9, GELAN, 11, 12 |
| `SP500_YOLO_HEAD_ANCHOR` | Anchor offset + objectness | 클래스별 NMS | Detect, segment | v3, v5, v7 |
| `SP500_YOLO_HEAD_NMSFREE` | DFL, 4 x 16 logits | Dual top-k | Detect | v10 |
| `SP500_YOLO_HEAD_DFLFREE` | 직접 거리 4개 | Dual top-k | Detect, segment, pose | 26 |

YOLOv3u와 YOLOv5u는 anchor-free head로 재학습된 모델이므로, 일반 YOLOv3·YOLOv5가 속한 anchor 계열이 아니라 anchor-less 계열에 속합니다. `sp500_yolo_head_for_model()`은 `"yolov8n"`, `"yolo11"` 같은 모델 이름을 해당 계열로 변환합니다.

### 진입 함수 선택

| 함수 | 입력 |
| --- | --- |
| `sp500_yolo_decode()` | 세 레벨(stride 8, 16, 32)의 박스·클래스·태스크 plane이 분리된 raw head 텐서. anchor-less, NMS-free, DFL-free 계열용입니다. |
| `sp500_yolo_decode_anchor()` | 레벨마다 하나로 합쳐진 텐서. anchor 계열용입니다. |
| `sp500_yolo_postprocess()` | 네트워크가 그래프 안에서 이미 디코딩한 테이블. 모든 계열용입니다. |

### 파라미터와 결과

`sp500_yolo_params_init()`은 640x640, 80 클래스 detection 모델에 맞는 기본값(`max_detections` 100, `conf_threshold` 0.25, `iou_threshold` 0.7)을 설정합니다. 일반 검출 모델은 따로 설정할 필드가 없습니다. Segmentation은 `proto`, `mask_coeffs`, `proto_h`, `proto_w`, `mask_threshold`를 추가로 설정해야 합니다.

`output.records`는 최대 `max_detections`개의 고정 크기 레코드를 받습니다. 크기는 `sp500_yolo_records_bytes()`로 계산하고, 태스크에 따라 `sp500_yolo_detection`, `sp500_yolo_segmentation`, `sp500_yolo_pose`로 읽습니다. Segmentation에서는 `output.mask`가 박스별 마스크를 받으며, 각 레코드의 마스크 오프셋은 `sp500_yolo_mask_offset()`으로 읽습니다.

박스 좌표는 네트워크 입력 프레임 기준의 픽셀 값입니다. 원본을 letterbox했다면 `sp500_letterbox()`가 반환한 배율과 패딩으로 원본 좌표로 되돌리세요.

## 권장 사항

### 모든 파라미터 구조체 초기화

필드를 설정하기 전에 `sp500_letterbox_params_init()`, `sp500_log_mel_params_init()`, `sp500_yolo_params_init()`을 호출하고, 필요한 필드만 덮어쓰세요. 0으로 채운 뒤 필수 필드만 설정한 구조체는 나머지 옵션이 모두 0인 채로 실행됩니다. 예를 들어 letterbox는 114 대신 검은색으로 패딩하고 영상을 가운데에 배치하지 않습니다.

### 복사 없는 버퍼 사용

`sp500_buffer_wrap()`으로 만든 버퍼는 호출할 때마다 입출력 복사가 발생하며, 640x640x3 프레임이라면 호출당 1.2 MB를 복사합니다. `sp500_buffer_alloc()`으로 만든 버퍼는 공유 메모리 영역에 있어 복사가 없습니다. `sp500_shmem_phys_addr()`은 그 물리 주소를 반환하므로 NPU가 같은 메모리를 읽고 쓸 수 있습니다.

### 작업 버퍼는 한 번만 할당

공유 메모리 영역은 compaction을 하지 않는 first-fit 할당기를 사용합니다. 해제한 메모리는 회수되고 인접 영역과 합쳐지지만, 수명이 서로 다른 버퍼를 매 프레임 할당·해제하면 단편화가 생깁니다. 단편화가 생기면 실제 데이터보다 약 4분의 1 더 많은 메모리를 차지하고, 작은 빈 공간이 남아 있어도 큰 요청은 실패할 수 있습니다. 버퍼는 시작 시 한 번 할당해 재사용하세요.

공유 메모리 버퍼를 전역 또는 static 객체에 보관하지 마세요. DSP 링크가 이미 해제된 뒤에야 버퍼가 해제되기 때문입니다.

(call-deadline)=

### 호출 기한

모든 DSP 호출에는 기한이 있으며 기본값은 3초입니다. 기한은 응답이 멈춘 DSP를 감지하기 위한 것이지 지연 시간 예산이 아니므로, 제출하는 가장 긴 작업보다 길어야 합니다. 특히 긴 log-mel 클립을 처리할 때 확인하세요.

기한이 지나도 작업은 취소되지 않습니다. DSP는 호출이 이미 해제한 버퍼로 작업을 계속하고, 다음 호출이 이전 호출의 결과를 반환할 수 있습니다. 이 상태에서 복구하려면 DSP를 다시 로드해야 합니다. 그래서 `sp500_set_dsp_timeout_ms()`로는 기한을 늘리기만 할 수 있습니다. 기본값 3초보다 작은 값을 지정하면 3초로 올려 적용되며, 실제 적용된 값은 `sp500_get_dsp_timeout_ms()`로 확인합니다.

긴 호출 전에 기한을 늘리세요. 호출이 `SP500_ERR_TIMEOUT`을 반환하면 재시도하지 말고 기한을 늘리세요.

### 시스템 전체에서 직렬화되는 호출

모든 DSP 호출은 왕복이 끝날 때까지 모든 스레드와 프로세스가 공유하는 잠금 하나를 잡습니다. 따라서 동시 호출은 안전하지만 병렬로 실행되지는 않습니다. 할당, letterbox, 읽기, 해제처럼 여러 단계를 하나의 원자적 단위로 묶으려면 `sp500_dsp_lock()`과 `sp500_dsp_unlock()`으로 감싸세요. 잠금 한 번마다 해제를 정확히 한 번 호출해야 합니다. 짝이 맞지 않으면 시스템의 모든 프로세스에서 DSP와 공유 메모리 할당기가 멈춥니다.
