Zephyr v3.7.0 · Shell / command registry / backend

명령은 한 트리, 접속 경로는 여러 개

Zephyr Shell에서 UART·RTT는 명령 자체가 아니라 입출력 백엔드다. 등록된 명령 트리를 셸 인스턴스가 해석하고, 선택된 핸들러가 실제 작업을 한다. 이 경계를 나누면 “명령이 안 보인다”와 “입력이 오지 않는다”를 다르게 진단할 수 있다.

본문 기준: Zephyr 3.7.0 LTS3 · 이후 유지보수 패치는 별도입니다.

기준은 Zephyr v3.7.0이다. 이 글은 Shell 자체의 명령 등록·실행·출력 경로를 다룬다. 특정 네트워크 관리 명령, 장치 에뮬레이터 모델, 각 주변장치의 제어 프로토콜은 범위 밖이다. 예시의 lab 명령은 설명용이며, 보드에서 직접 빌드·실행했다는 주장은 하지 않는다. 그림과 표는 이 글을 위해 새로 그렸다.

1. 백엔드는 바이트를 옮기고 명령 등록은 트리를 만든다

Shell은 여러 인스턴스와 UART·RTT·DUMMY 등 여러 전송 백엔드를 지원한다. 정적 명령은 컴파일할 때 전용 섹션에 등록되며, v3.7.0 문서는 생성된 명령을 모든 셸 인스턴스에서 쓸 수 있다고 명시한다. 따라서 백엔드를 두 개 켠다고 해서 명령 집합이 두 개 정의되는 것은 아니다. Shell 인스턴스가 입력을 편집·파싱해 명령 트리에서 핸들러를 찾고, 출력은 그 인스턴스의 전송 경로로 간다.

그림 1. 전송과 명령 실행은 다른 경계

↔ 좁은 화면에서는 입력 경로에서 작업과 출력까지 오른쪽으로 이동하세요.

① 전송 백엔드UART · RTT · DUMMY 등입력 바이트와 출력 바이트를 주고받는다.
② Shell 인스턴스프롬프트 · 라인 편집 · 파싱현재 접속의 상태와 출력 대상을 관리한다.
③ 명령 트리SHELL_CMD_REGISTER컴파일된 루트와 하위 명령에서 실행할 핸들러를 찾는다.
④ 핸들러int fn(sh, argc, argv)인자를 검증하고 실제 작업·응답을 수행한다.
표 1. Shell 구성 요소와 생성 시점
요소준비 시점책임
백엔드빌드 설정·초기화전송과 해당 Shell 인스턴스의 입출력
정적 루트 명령컴파일·링크고유한 루트 이름으로 공통 명령 트리에 들어간다.
동적 하위 명령런타임 조회현재 데이터로 syntax/handler/help를 채운다.
핸들러명령 선택 후인자 검증·작업 수행·반환 상태 결정

근거: 3.7.0 Shell 개요·인스턴스, 명령 등록과 모든 인스턴스에서의 가용성.

2. 가장 깊이 맞는 핸들러가 실행된다

루트와 하위 명령은 트리로 묶인다. v3.7.0 문서는 입력 경로에서 찾은 가장 깊은 핸들러를 실행하고, 더 이상 핸들러가 없는 뒤쪽 토큰을 인자로 넘기는 규칙을 설명한다. 부모에 핸들러가 없고 자식도 못 찾으면 실행할 함수가 없다. 정적 하위 명령의 개수와 이름은 빌드 때 정해진다. 동적 하위 명령은 조회 함수가 현재 항목을 제공하지만, 그것도 해당 루트 아래의 경로를 구성한다.

그림 2. lab ping과 잘못된 경로가 갈라지는 지점

↔ 예시 트리의 각 가지를 따라가며 어느 핸들러가 선택되는지 보세요.

루트: lab하위 집합 sub_lab 등록
루트 자체 핸들러 없음
lab pingcmd_lab_ping 발견
argv[0] = "ping"
출력shell_print(sh, "pong")
선택된 Shell 인스턴스로 응답
lab unknown등록된 하위 명령 없음
실행할 핸들러 없음 → 오류 표시
표 2. 명령 등록 방식과 인자 규칙
등록 방식만드는 것주의할 점
SHELL_CMD_REGISTER고유 이름의 루트 명령루트 이름 충돌을 피하고 하위 집합 포인터를 맞춘다.
SHELL_STATIC_SUBCMD_SET_CREATE + SHELL_CMD빌드할 때 고정된 하위 명령 집합끝에 SHELL_SUBCMD_SET_END를 둔다.
SHELL_CMD_ARG_REGISTER인자 개수를 정의한 루트 명령mandatory에는 명령 이름도 포함된다. 틀리면 핸들러 전에 오류가 난다.
SHELL_DYNAMIC_CMD_CREATE콜백으로 열거하는 동적 하위 집합항목 끝을 syntax = NULL로 알려 준다.

근거: 3.7.0 가장 깊은 핸들러 규칙, v3.7.0 매크로 인자 계약.

3. 작고 읽을 수 있는 정적 명령부터 만든다

아래는 공식 3.7.0 문서의 정적 명령 구성과 동일한 공개 API를 써서 새로 작성한 최소 예시다. lab는 핸들러가 없는 루트, ping이 실제 응답을 낸다. 이 소스만으로 전송 장치가 준비되지는 않으며 프로젝트에서 CONFIG_SHELL과 적절한 백엔드·보드 입출력을 선택해야 한다.

#include <zephyr/shell/shell.h>
#include <zephyr/sys/util.h>

static int cmd_lab_ping(const struct shell *sh, size_t argc, char **argv)
{
    ARG_UNUSED(argc);
    ARG_UNUSED(argv);
    shell_print(sh, "pong");
    return 0;
}

SHELL_STATIC_SUBCMD_SET_CREATE(sub_lab,
    SHELL_CMD(ping, NULL, "Reply with pong", cmd_lab_ping),
    SHELL_SUBCMD_SET_END
);
SHELL_CMD_REGISTER(lab, &sub_lab, "Lab diagnostics", NULL);

핸들러는 const struct shell *sh를 통해 호출한 인스턴스에 출력한다. argc/argv는 선택된 명령 기준으로 해석해야 하므로 인자를 추가할 때 범위를 검사한다. 예시는 읽기·출력만 수행한다. 명령을 통해 하드웨어 상태를 바꾸려면 입력의 범위, 동시 호출, 권한을 별도로 설계한다.

근거: 3.7.0 공식 demo ping 예시, v3.7.0 사용자 명령 샘플 원본.

4. 동적 명령은 런타임 데이터의 수명도 관리한다

v3.7.0의 동적 하위 명령 예시는 조회 콜백이 idx번째 항목의 syntax, 핸들러, help 등을 채우고, 끝에서는 syntax = NULL을 설정한다. Shell은 이를 탭 완성과 명령 탐색에도 사용한다. 따라서 콜백이 가리키는 문자열의 수명과, 목록이 바뀔 때의 정렬·동기화는 애플리케이션이 책임진다. 데이터가 고정이라면 정적 명령이 더 단순하다.

표 3. 정적·동적 명령의 데이터 수명
형태이름의 원천적합한 상황확인할 위험
정적 하위 명령컴파일된 배열항상 같은 동작·도움말빌드 설정에 따라 빠진 명령을 런타임 오류로 오해
동적 하위 명령조회 콜백의 현재 데이터변하는 항목 이름·목록문자열 수명, 목록 변경 중 접근, 끝 표식
사전 명령문자열과 값 쌍정해진 선택지 → 값 매핑인자 텍스트와 내부 값을 혼동

근거: 3.7.0 동적 명령 예시와 syntax=NULL 종료 규칙, 사전 명령.

5. 프롬프트가 없을 때는 명령보다 백엔드부터 본다

UART는 보드의 실제 콘솔·핀 설정, RTT는 디버거 연결과 채널, DUMMY는 물리 전송 없는 시험 경로라는 서로 다른 전제가 있다. 3.7.0 문서는 USB CDC ACM과 Bluetooth LE NUS를 위한 스니펫도 제시하지만, 이 글에서 다루는 것은 전송 선택이 Shell 인스턴스를 어디에 붙이는지다. CONFIG_SHELL_MINIMAL은 RAM·flash 사용량을 줄이는 기본 선택이므로 필요한 편집·완성 기능을 선택적으로 켜야 한다.

표 4. 백엔드별 전제와 프롬프트 진단
백엔드전제프롬프트가 없을 때
UART / serial활성 serial 백엔드와 보드의 콘솔 경로포트·baud·핀·devicetree chosen 및 초기화 순서를 확인
SEGGER RTTRTT 빌드 설정과 디버거·채널연결된 세션과 shell/log 채널 충돌을 확인
DUMMYCONFIG_SHELL_BACKEND_DUMMY물리 프롬프트 대신 테스트·shell_execute_cmd 경로로 사용
USB CDC ACM해당 장치·스니펫·호스트 열거USB 인터페이스가 먼저 뜨는지 확인

명령 트리와 노출 경로를 함께 검토한다. 명령 등록은 특정 UART에만 묶이지 않는다. 배포 빌드에서 어떤 백엔드를 켜는지와 어떤 진단·변경 명령을 포함하는지가 접근 표면을 결정한다. 네트워크 백엔드를 쓴다면 별도의 접근 제어와 운영 환경도 검토해야 한다. shell_execute_cmd()는 문서상 디버깅·진단용이며 Shell 명령 핸들러 안에서 다시 호출하면 안 된다.

근거: 3.7.0 Shell 백엔드 목록·USB 스니펫·RTT, 최소 구성 안내.

6. 핸들러 출력과 로그 출력은 수명이 다르다

shell_print() 등은 핸들러나 스레드에서 사용할 수 있지만 v3.7.0 문서는 인터럽트 컨텍스트에서 쓰지 말고 Logging을 사용하라고 안내한다. CONFIG_SHELL_LOG_BACKEND를 켜면 Shell 인스턴스가 로거 백엔드 역할을 하며 로그가 큐를 거쳐 셸 스레드에서 처리된다. 큐가 차면 설정된 시간 동안 로거 스레드가 막히거나 오래된 메시지가 제거될 수 있다. shell stats show는 이 경로의 드롭을 조사하는 관측점이다.

그림 3. 프롬프트를 지키는 로그 큐와 초기에 비는 출력 창

↔ 왼쪽의 로그 발생부터 큐·셸 스레드·전송 및 시작 전 경계를 따라가세요.

① 로그 발생Logging 코어가 메시지를 만든다. 핸들러의 shell_print()와 출처가 다르다.
② Shell 로그 큐Shell 로그 백엔드가 켜져 있으면 큐에 넣어 프롬프트 출력을 섞어 낸다.
③ Shell 스레드·전송스레드가 실행되고 백엔드가 살아 있어야 터미널에 나타난다.
④ 실패 경계Shell 스레드 시작 전 크래시는 이 로그 백엔드만으로 출력할 수 없다. 큐 포화·느린 전송도 지연·드롭을 만든다.
표 5. 직접 출력과 로그 출력의 경계
출력 경로쓸 수 있는 문맥관측할 실패
shell_print/warn/error명령 핸들러·일반 스레드백엔드 연결·전송 정체; ISR에서는 사용하지 않음
Shell 로그 백엔드Logging 코어 → Shell 큐·스레드큐 포화·대기 시간·드롭 통계
초기 크래시 로그Shell 스레드 시작 전Shell 로그 백엔드로는 출력할 수 없으므로 초기용 단순 UART/RTT 로그 백엔드 등을 검토

근거: 3.7.0 출력 함수의 ISR 제한, Shell 로그 큐·드롭·시작 전 동작.

7. 같은 “안 된다”도 네 경계로 나눈다

표 6. Shell 실패 증상별 확인 순서
관측먼저 볼 경계확인할 것
프롬프트가 없다백엔드·초기화Kconfig, 보드 콘솔, RTT 채널, USB 열거
프롬프트는 있지만 명령이 없다등록·빌드루트 이름, 조건부 등록, 관련 기능 Kconfig, 링크된 모듈
명령을 쳤는데 핸들러가 다르다트리 탐색가장 깊은 핸들러, 부모의 핸들러 유무, 뒤쪽 토큰
인자 개수 오류등록 계약·파서mandatory에 이름을 포함했는지, help와 실제 인자 규칙
명령 출력은 보이지만 로그가 빠진다로그 백엔드·큐CONFIG_SHELL_LOG_BACKEND, 시작 시점, shell stats show

핸들러가 오래 막히는 작업을 직접 수행하면 해당 인스턴스의 입력 처리도 지연될 수 있다. 작업을 별도 스레드나 작업 큐로 넘길지는 실행 시간과 동시성 요구를 보고 정한다. Shell을 디버그용 콘솔로 쓸지 제품의 조작 인터페이스로 쓸지도 배포 빌드에서 결정해야 한다.

근거: 명령 선택, 로그 큐.

8. 소스와 확인 범위

Zephyr 릴리스 v3.7.0의 공식 Shell 문서와 같은 태그의 헤더·샘플을 기준으로 썼다. 이 글의 트리·파이프라인·큐 그림은 문서를 복제한 것이 아니라 역할과 실패 경계를 새로 배열한 설명용 도식이다. 특정 보드의 UART/RTT/USB 설정이나 명령의 접근 권한을 실제 장비에서 검증한 것은 아니다.

표 7. Zephyr 3.7.0 Shell 근거
고정 근거확인할 부분
Shell 공식 문서백엔드, 명령 트리, 동적 명령, 출력·로깅 제약
v3.7.0 공개 헤더등록 매크로, 인자 개수, 핸들러 서명
Custom Shell 원본 샘플정적 명령과 핸들러 구성