DSP 프로그래밍 가이드#
이 페이지는 SP500 DSP API로 애플리케이션을 빌드해 REGULUS 장치에 배포하는 방법과 각 연산의 사용법을 설명합니다. API의 개념은 DSP 개요를, 모든 함수·구조체·상수는 DSP API 레퍼런스를 참고하세요. 완성된 예제 애플리케이션은 예제를 참고하세요.
시작하기 전에#
애플리케이션이 API를 호출하기 전에 장치에 다음이 준비되어 있어야 합니다.
DSP 펌웨어가 로드되어 있어야 합니다. 장치가 부팅된 직후에는 DSP에 펌웨어가 없습니다. 부팅할 때마다
dsp_firmware_load를 한 번 실행하세요. DSP 펌웨어 로드를 참고하세요.libopencv_core.so.409가 설치되어 있어야 합니다. API에 OpenCV 타입은 나오지 않지만libsp500.so가 이 라이브러리에 링크되어 있어 시작 시 로더가 이를 찾습니다.
라이브러리가 필요로 하는 항목은 장치에서 다음과 같이 확인합니다.
readelf -d libsp500.so | grep NEEDED
ldd libsp500.so
DSP 펌웨어 로드#
Vision DSP는 자체 펌웨어(FreeRTOS와 SP500 연산의 DSP 쪽 구현)를 실행합니다. 장치가 부팅된 직후에는 DSP에 펌웨어가 로드되어 있지 않으므로, DSP를 사용하는 애플리케이션을 시작하기 전에 펌웨어를 로드하세요.
dsp_firmware_load
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 이미지에 설치되어 있습니다. 다음 펌웨어 이미지를 로드합니다./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 툴체인으로 크로스 컴파일합니다. 먼저 툴체인 환경을 설정합니다.
source /opt/crosstools/mobilint/2.0.0/<VERSION>/environment-setup-cortexa53-mobilint-linux
그다음 SDK 헤더 경로를 지정하고 libsp500을 링크해 컴파일합니다.
$CXX my_app.cpp -I<sdk>/include -L<sdk>/lib -lsp500 -o my_app
링크 옵션에 다른 라이브러리를 추가할 필요는 없습니다. 헤더는 sp500/ 경로로 포함합니다.
#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이 기록됩니다. 라이브러리의 세 가지 이름을 모두 심볼릭 링크를 유지한 채 복사하세요.
cp -a lib/libsp500.so* /target/usr/lib/
libsp500.so만 복사하거나 일반 cp로 복사해 링크가 파일 복사본으로 바뀌면 시작 시 다음 오류가 발생하거나 루트 파일 시스템에 라이브러리 전체 복사본이 세 개 남습니다.
error while loading shared libraries: libsp500.so.1: cannot open shared object file
애플리케이션과 라이브러리를 따로 배포한다면 시작 시 둘의 major 버전이 일치하는지 확인하세요.
if (sp500_version() / 10000 != SP500_VERSION_MAJOR) {
/* the header and the library disagree */
}
애플리케이션 실행#
애플리케이션을 장치에 복사합니다. 마지막 부팅 이후 DSP 펌웨어를 로드하지 않았다면 펌웨어를 로드한 뒤 애플리케이션을 시작합니다.
dsp_firmware_load
./my_app
예제#
GitHub의 regulus-dsp-examples에는 이 페이지에서 설명하는 전처리·후처리 연산을 사용하는 SP500 DSP API 예제 애플리케이션이 모여 있습니다. 예제마다 빌드하고, 장치에 복사하고, DSP 펌웨어를 로드해 실행하는 방법이 함께 설명되어 있습니다.
첫 애플리케이션#
다음 프로그램은 1920x1080 카메라 프레임을 640x640 모델 입력으로 letterbox하고, anchor-free YOLO detection head의 출력을 원본 프레임 좌표의 박스로 디코딩합니다.
#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는 반드시 설정해야 하며, 나머지 필드의 기본값은 다음과 같습니다.
필드 |
기본값 |
설명 |
|---|---|---|
|
640, 640 |
대상 크기(픽셀)입니다. |
|
3 |
interleaved 채널 수입니다. |
|
|
|
|
114 |
모든 채널에 적용하는 테두리 값입니다. |
|
1 |
1이면 테두리를 균등하게 나누고, 0이면 오른쪽 아래에만 패딩합니다. |
|
1 |
1이면 작은 원본을 확대할 수 있고, 0이면 배율이 1.0을 넘지 않습니다. |
|
0 |
1이면 출력할 때 채널 0과 2를 맞바꿉니다(BGR과 RGB 상호 변환). 채널이 3개여야 합니다. |
결과에는 적용된 배율과 왼쪽 위 패딩이 담깁니다. 이 값으로 모델 입력의 좌표를 원본 프레임 좌표로 되돌립니다.
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와 같습니다.
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_fft400,hop_length160, 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가 처리할 프레임이 수천 개에 달하므로, 제출하기 전에 호출 기한을 늘리세요. 호출 기한을 참고하세요.
YOLO Decode#
YOLO 네트워크는 박스가 아니라 후보 위치마다 raw 값을 출력합니다. YOLO 함수는 좌표 디코딩, 신뢰도 임계값 미만 후보 제거, 중복 제거를 DSP 왕복 한 번으로 처리합니다.
Head 계열 선택#
YOLO는 세대마다 박스 인코딩과 중복 제거 방식이 다릅니다. 네트워크가 속한 계열을 선택하세요.
계열 |
박스 인코딩 |
중복 제거 |
태스크 |
모델 |
|---|---|---|---|---|
|
DFL, 4 x 16 logits |
클래스별 NMS |
Detect, segment, pose |
v3u, v5u, v8, v9, GELAN, 11, 12 |
|
Anchor offset + objectness |
클래스별 NMS |
Detect, segment |
v3, v5, v7 |
|
DFL, 4 x 16 logits |
Dual top-k |
Detect |
v10 |
|
직접 거리 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" 같은 모델 이름을 해당 계열로 변환합니다.
진입 함수 선택#
함수 |
입력 |
|---|---|
|
세 레벨(stride 8, 16, 32)의 박스·클래스·태스크 plane이 분리된 raw head 텐서. anchor-less, NMS-free, DFL-free 계열용입니다. |
|
레벨마다 하나로 합쳐진 텐서. anchor 계열용입니다. |
|
네트워크가 그래프 안에서 이미 디코딩한 테이블. 모든 계열용입니다. |
파라미터와 결과#
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 링크가 이미 해제된 뒤에야 버퍼가 해제되기 때문입니다.
호출 기한#
모든 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와 공유 메모리 할당기가 멈춥니다.