책 <미술관에 스파이가 있다> 이후로 현대 미술에 대한 관심을 계속 이어오고 있다.

2026.04.19 - [독서/문학] - <미술관에 스파이가 있다> 후기

 

<미술관에 스파이가 있다> 후기

전시회를 종종 다니기는 했었으나 예술계가 어떻게 흐르고 있는지, 어떤 맥락을 가지고 있는지에 대해서는 큰 관심이 없었다.그러나 예술계의 밑낯을 보여준다는 책의 홍보 문구에 나도 모르게

sea-sameoil.tistory.com

작가 한 사람, 혹은 작품 한 개에 대한 설명을 담은 책보다는 전반적인 흐름을 설명하는 책을 찾다 <현대미술은 처음인데요>를 발견했다.

A~Z까지의 챕터로 구성되어 있고 각 챕터가 길지는 않아 빠르게 진도를 뺄 수 있었다.


 

참여형 전시에 대해서는 꽤 부정적인 의견을 가진 사람이었다.

특히 사진용 전시를 참여형 전시로 포장한 전시장을 방문할 때에는 표값과 시간이 아까워졌다.

 

그러나 이 책을 읽으며 참여형 전시, 퍼포먼스 전시가 현대미술의 흐름 속에 자연스레 생겨난 점을 알게 되었다. 또한 내가 편협한 시각으로 이 예술들을 바라보고 있었음도 깨달았다.

참여형 전시의 경우 사진용 전시가 전부가 아니라 자신이 표현하고자 하는 내용을 관객들의 참여를 통해 완성하는 예술임을 배웠다.

그 대표적인 예시 작품으로는 쿠사마 야요이의 <소멸의 방>, 펠릭스 곤잘레스의 무제(내 사랑 로스)이 있다.

퍼포먼스 전시 역시 인터넷상에서는 이를 조롱하거나 비하하는 사람들이 많았지만, 그들은 자신의 몸을 매개로 주제를 나타내는 사람들이라는 인식으로 변경되었다.

 

다른 현대 미술에 대해서도 마찬가지지만, 참여형 전시와 퍼포먼스 전시에 대해 편협한 시각을 줄여보려 한다.

책에서는 아래와 같이 말한다.

지금 만난 작품이 어떤 작품인지 알아내기란 그리 간단한 일이 아니다. 왜냐하면, 현대미술의 종류는 너무 많고 모두 새로운 것이며 대부분의 경우 좋은 작품과 나쁜 작품을 구분할 만큼 충분한 시간을 거치지 못했기 때문이다. 자신이 본 작품에 대해 누구도 확신할 수 없고, 상당히 많은 작품이 쓰레기가 될지도 모른다는 사실은 우리를 아주 흥분시키기도 하지만 무섭게 만들기도 한다. 그러니 너무 빨리 결론 내리는 실수는 하지 말자!

 

이처럼 내가 받아들이기 어려운 전시라고 하찮게 여기거나 무시하는 것이 아닌, 왜 이런 매체를 선택했고 어떤 점을 이야기하려 하는지 더욱 열린 마음으로 받아들여야겠다.


 

책을 읽으며 <미술관에 스파이가 있다>에서 읽은 내용들이 꽤 연상되어 즐겁게 읽었다.

몇 가지만 예로 들자면,

  • 어떤 작가의 작품을 사들였다가 고점일 때 금세 판매하는 것을 Flipping이라고 하는데, 이런 과정이 여러 번 반복되면 작가에게 치명적이라는 것
  • 화이트 큐브가 요즘 전시장의 주된 테마라는 점
  • 아트페어에 참여하기 위해 많은 돈과 인력과 시간이 든다는 점
  • 갤러리 중 네임드 갤러리(ex. 가고시안) 혹은 미술관의 영향력이 크다는 점

 

책의 발행 연도를 확인하니 2017년이었다. 2026년 현재의 화두는 AI인데 아마 책이 조금 더 늦게 나왔더라면 AI와 관련된 내용도 있지 않았을까 짐작해 본다.


 

현대미술에 대한 배경지식이 없더라도 부담스럽지 않게 흐름을 알게 되는 책이라 현대미술에 관심이 있는 사람이라면 추천한다.

구체적인 내용으로 넘어가기 전 좋은 발판이 되어주리라 생각한다.

민음북클럽 가입 때 고른 선택 도서 중 하나로, 젊은 작가 도서 중 하나로 <고독사 워크숍>을 선택했다.

최근 <미술관에 스파이가 있다>를 읽고 난 후로 요즘 작가들이 현대를 어떻게 바라보는지에 대한 호기심이 커져 북클럽 도서 배송 후 가장 먼저 손이 갔다.

2026.04.19 - [독서/문학] - <미술관에 스파이가 있다> 후기

 

<미술관에 스파이가 있다> 후기

전시회를 종종 다니기는 했었으나 예술계가 어떻게 흐르고 있는지, 어떤 맥락을 가지고 있는지에 대해서는 큰 관심이 없었다.그러나 예술계의 밑낯을 보여준다는 책의 홍보 문구에 나도 모르게

sea-sameoil.tistory.com


 

 

"고독사"하면 가장 먼저 떠오르는 이미지가 '혼자, 쓸쓸히 맞이한 죽음'이다. 

타인과의 연결 없이 맞이한 죽음이지만 오히려 삶을 마무리한 후에야 타인과 연결되는 죽음이라 생각했다.

 

그러나 책에서는 오히려 쓸쓸히 죽기 위해 노력하는 사람들을 그려냈다.

고독사를 준비함으로써 오히려 고독해지지 않고 남에게 폐 끼치지 않고 죽기 위해 노력하는 사람들.

고독사를 위해 매일매일 시시한 일들을 하고, 할 일을 매일 부여하는 사람들을 보며 오히려 하루하루를 쓸모 있게 보내는 게 아닌가 하는 생각이 들었다.

 

인터넷에 가끔 돌아다니는 말 중에, "내일 죽을 것처럼 오늘 하루를 살아라"라는 말이 있다.

그만큼 하루하루를 열심히 보내라는 의미라고 생각한다.

그러한 지점에서 이 책의 사람들은 매일매일을 충실히 살아내고 있고, 나도 순간순간을 채우는 연습을 해야겠다는 마음이 생긴다.

거창하지 않아도 좋다. 어느 날은 시를 써본다거나, 턱걸이를 해본다거나 등등.. 새로운 시도들을 통해 새로운 시각을 얻을 수도 있으니.


 

책을 읽으면서 '내 문해력이 많이 떨어졌구나...'라는 생각을 자주 했다.

책에서는 '농담'을 자주 언급하고, 무거운 주제를 무겁게만 받아들이지 않도록 하는 느낌으로 농담이라는 단어를 사용하는 듯하다.

책 초반에는 이러한 '농담'의 의미를 잘 이해하지 못했기에 더욱 그렇게 생각했고 어느 정도 감을 잡은 후반부에서도 '이게 왜...'라는 생각이 계속 들기는 했다.

아무래도 시간이 좀 더 지난 후에 다시 읽어야 더 와닿지 않을까싶다.

 

책은 에피소드 형식으로 각 챕터마다 다른 주인공들이 나와 이야기를 써내려 간다.

각 에피소드가 아예 관련 없는 사람들인 것처럼 보이다가도 어느 순간에 '앗 이 사람 앞에서 본 거 같은데'라는 생각이 들며 모든 이야기들이 연결되어 있음을 느낄 수 있다.

이때의 즐거움도 책을 읽게 하는 매력 중 하나라 생각한다.

 

현시대 글임을 많이 느꼈던 포인트가 '다이소', '알파 문구'와 같은 명이 나올 때이다.

이런 포인트가 나올 때마다 진짜 현대 작가 책을 읽는 느낌이 들어 혼자 킥킥대기도 했다.


 

제목이 신선했던 만큼 내용도 흥미롭게 다가온 책이다.

다양한 에피소드로 여러 이야기의 맛을 느낄 수 있으니 읽어보면 좋은 책이라 생각한다.

이전 글들에서 PCI 관련 기본 개념들을 정리했다.

2026.03.21 - [코딩/임베디드] - VxWorks 6.9 PCI 명령어

 

VxWorks 6.9 PCI 명령어

이번에는 VxWorks 환경에서 실제로 PCI 장치를 어떻게 탐색하고 정보를 읽어오는지 정리해보려 한다.vxBus란?VxWorks는 vxBus라는 드라이버 프레임워크를 통해 하드웨어 버스와 장치를 관리한다.PCI도 vx

sea-sameoil.tistory.com

 

이번 글에서는 PCIe와 관련해서 조금 더 구체적인 내용들을 정리해보려 한다.


1. Upstream & Downstream

업스트림과 다운스트림은 데이터가 흐르는 방향과 장치 간의 계층 구조를 정의하는 개념이다.

Upstream(업스트림)

데이터가 컴퓨터의 두뇌(CPU/Root Complex)를 향해 올라가는 방향을 의미한다.

  • 방향: 말단 장치(end point) → 스위치/허브 → CPU/메모리 방향
  • 역할: RC(CPU)를 향하는 방향
            요청에 대한 응답(Completion)이나 장치 → 호스트 데이터 전송 시 사용
  • Upstream port: 스위치나 브리지(brideg) 장치에서 CPU 쪽(상위 계층)을 바라보고 있는 포트

Downstream(다운스트림)

데이터가 CPU에서 시작하여 말단 장치를 향해 내려가는 방향을 의미한다.

  • 방향: CPU/Root Complex → 스위치/허브 → 말단 장치(Endpoint)
  • 역할: RC(CPU)에서 장치를 향하는 방향
            호스트의 요청(Request)이 내려가는 방향
  • Downstream port: 스위치나 Root Complex에서 하위 장치(그래픽 카드, 사운드 카드 등)가 연결되는 포트

2. Root Complex

앞서 방향을 결정하는 기준 중 하나가 root complex임을 보았다. Root Complex의 정확한 정의는 다음과 같다.

PCIe 계층 구조의 최상위 노드(root)로, CPU/프로세서와 PCIe 패브릭(fabric) 전체를 연결하는 핵심 컴포넌트
CPU / Memory (System)
       │
  ┌────┴────┐
  │  Root   │  ← Root Complex
  │ Complex │
  └────┬────┘
       │  PCIe Links
  ┌────┴────────────────┐
  │    Switch / Bridge  │
  └────┬────────────────┘
       │
  ┌────┴────┐
  │Endpoint │  (GPU, NVMe, NIC 등)
  └─────────┘
더보기

💡 패브릭(fabric) 이란?

 

PCIe 토폴로지 내의 모든 연결 구조 전체를 의미한다.

즉, Root Complex부터 Switch, Bridge, Endpoint까지 PCIe 링크로 연결된 네트워크 전체를 하나의 천(fabric)에 비유한 표현이다.

┌─────────────────────────────────────────────┐
│              PCIe Fabric                    │
│                                             │
│   [Root Complex]                            │
│        │                                    │
│   [Switch] ── [Endpoint: GPU]               │
│        │                                    │
│   [Bridge] ── [Endpoint: NVMe]              │
│                                             │
│   (이 점선 안의 모든 것 = Fabric)               │
└─────────────────────────────────────────────┘

 

* Bridge: 서로 다른 PCIe 도메인 또는 PCI ↔ PCIe 간을 연결하는 컴포넌트로, Switch와 마찬가지로 Upstream/Downstream 포트를 가짐

Root complex를 결정하는 기준은 다음과 같다.

  • 시스템 메모리 및 CPU와의 직접 연결
    • 반드시 호스트 CPU와 시스템 메모리(DRAM)에 직접 접근 할 수 있어야 함
    • 다른 컴포넌트(Switch, Endpoint)는 PCIe 계층의 루트로서 트랜잭션을 개시할 수 없음.
      단, Endpoint는 DMA를 통해 시스템 메모리에 접근하는 것은 가능
  • 트랜잭션을 시작하는 주체
    • Requester(요청자)로서 트랜잭션 먼저 시작
    • endpoint는 요청을 받거나 DMA로 메모리에 쓸 수는 있지만, PCIe 계층의 루트로 동작하지는 않음
  • Bus 0 소유
    • 항상 Bus Number 0 소유
    • 하위 장치들에게 순차적으로 버스 번호 부여
  • Configuration Space 접근 권한
    • Type 0 / Type 1 Configuration 트랜잭션을 생성하여 패브릭 전체 장치의 설정 공간에 접근 가능

Configuration Space 관련해서 아래 글에서 확인 가능하다.

다만, Type0/Type1 Configuration 관련 글에 대해서는 다음 글에서 작성 예정이다.

2026.03.17 - [코딩/임베디드] - CPU 보드가 IO 디바이스를 인식하는 방법


3.  DFS (Depth-First Search)

PCIe 하다 갑자기 알고리즘이 나와 쌩뚱맞게 느껴질 수 있다.

그러나 PCIe 열거(Enumeration) 과정이 DFS 알고리즘으로 동작하기에 이를 언급하고 넘어가려 한다.

*enumeration: 버스 장치들에 번호를 매기는 과정 정도로 이해하고 여기서는 넘어가자

 

DFS는 깊이 우선 탐색으로, 가장 끝까지 탐색하는 방법이다.

간단히 이해하자면 미로 찾기와 유사하다.

  1. 갈림길이 나오면 일단 한 방향을 선택
  2. 해당 방향의 막다른 길까지 직진
  3. 막다른 길이 나오면, 방금 지나온 직전 갈림길까지 되돌아감
  4. 이 과정을 반복하며 모든 곳을 방문

BFS(Breadth-First Search, 너비 우선 탐색)과도 비교가 많이 되는데, 이 글에서는 생략하고 넘어간다.

그렇다면 왜 DFS를 사용해서 장치에 번호를 매길까?

  • 트리 구조 최적화: PCIe는 부모-자식 관계가 명확한 트리 구조이기 때문
  • 종속성 해결: 자식 노드들이 몇 개인지(버스 번호를 어디까지 쓸지) 알아야 부모 노드의 설정을 마무리할 수 있기 때문

이렇게 하면 계통별로 번호가 묶이기 때문에 나중에 데이터를 전달(Routing)할 때 "1번부터 2번까지는 A쪽으로 보내면 된다"는 식으로 관리가 매우 편해진다.


 

PCI에 대해 공부할 때 처음 마주하는 개념들을 간단히 정리해보았다.

아직 정리하지 못한 개념이 많고 동작 관련 내용도 알아갈 요소가 많다.

다음 글에서는 PCIe 관련 개념 정리 2탄 느낌으로, 다른 개념들도 같이 정리해보려 한다.

'코딩 > 임베디드' 카테고리의 다른 글

Memory Barrier를 알아보자  (1) 2026.04.15
Exception Hook을 알아보자  (0) 2026.04.14
watchdog을 알아보자  (0) 2026.04.13
세마포어 알아보기  (0) 2026.04.09
Serial RX를 알아보자  (0) 2026.04.08

BIT란?

BIT는 Built-In Test의 약자로, 장비의 상태를 보고하기 위한 자가진단 기능이다.
단순한 상태값이라기보다는, 장비가 스스로 자신의 상태를 점검하고 그 결과를 외부에 알려주는 메커니즘이라 볼 수 있다.

각 항목은 장비마다, 운용 로직마다 다르게 구성할 수 있으며 상태 관리를 위해 필요한 항목을 BIT에 넣어 관리한다.
일반적으로 어떤 항목이 구성되어 있는지는 ICD(Interface Control Document, 인터페이스 통제문서) 혹은 별도로 정의한 문서에서 확인할 수 있다.
 
BIT는 크게 3가지로 구성되어 있다.

  • PBIT(Power-Up BIT): 처음 보드가 부팅될 때 수행되는 BIT
  • IBIT(Initiated IT): 요청이 있을 때 수행되는 초기 진단 시험
  • CBIT(Continuoous BIT): 장비의 상태를 지속적으로 확인하는 연속 진단 BIT

각 BIT는 구성 항목이 같을 수도, 다를 수도 있다.
상태를 확인하는 시점에 따라 필요한 정보가 달라질 수 있기 때문에, BIT 항목을 목적에 맞게 다르게 구성하는 것이 일반적이다.


BIT 데이터 가져오기

PBIT은 부팅 시점에 한 번만 실행된다. 검사가 완료되면 결과를 레지스터에 반영하며, 이후 값은 변경되지 않는다.

부팅이 완료된 후 해당 레지스터를 읽으면 결과를 확인할 수 있다.

CBIT도 PBIT과 마찬가지로 장비가 검사를 완료한 뒤 결과를 레지스터에 반영한다.
다만 운용 중에 지속적으로 갱신되기 때문에, 읽는 시점에 따라 결괏값이 달라질 수 있다.

IBIT은 요청-응답 흐름이 필요하다. 장비에 IBIT 실행 명령을 보내면 장비는 이를 수신하고 IBIT을 수행한다.
완료되면 완료 Flag 신호를 보내며, 이 Flag를 확인한 후에 결과 데이터를 읽어야 한다.
Flag 확인 없이 결과를 읽으면 적절하지 않은 값을 가져올 수 있으므로 주의가 필요하다.


 

아래 예제코드는 드라이버가 제공하는 레지스터 주소를 구조체 포인터로 매핑하여 데이터를 복사해 가져오는 방식으로 작성되었다. PBIT은 CBIT과 구조가 동일하고 부팅 후 1회만 읽으면 되므로 생략한다.

아래 표는 BIT 항목을 정의해 둔 표이다.

항목 PBIT IBIT CBIT
CPU 자가진단 O O  
RAM 점검 O O  
ROM/Flash 무결성 O O  
통신 인터페이스 상태 O O O
전원 전압 감시 O   O
온도 감시     O
통신 링크 상태     O
액추에이터 응답 확인   O  

 

#include <stdint.h>

/* ---------------------------------------------------------------
 * BIT 결과 구조체 (항목 공통, 미측정 항목은 0으로 유지)
 * --------------------------------------------------------------- */
typedef struct {
    uint32_t cpuDiag;        /* CPU 자가진단          (PBIT, IBIT)       */
    uint32_t ramCheck;       /* RAM 점검              (PBIT, IBIT)       */
    uint32_t romIntegrity;   /* ROM/Flash 무결성      (PBIT, IBIT)       */
    uint32_t commIfStatus;   /* 통신 인터페이스 상태  (PBIT, IBIT, CBIT) */
    uint32_t voltageMonitor; /* 전원 전압 감시        (PBIT, CBIT)       */
    uint32_t tempMonitor;    /* 온도 감시             (CBIT)             */
    uint32_t linkStatus;     /* 통신 링크 상태        (CBIT)             */
    uint32_t actuatorResult; /* 액추에이터 응답 확인  (IBIT)             */
} BitResult;

/* ---------------------------------------------------------------
 * 드라이버 레지스터 맵 (ICD 기준, 오프셋은 예시)
 * --------------------------------------------------------------- */
typedef struct {
    uint32_t cpuDiag;        /* 0x00 */
    uint32_t ramCheck;       /* 0x04 */
    uint32_t romIntegrity;   /* 0x08 */
    uint32_t commIfStatus;   /* 0x0C */
    uint32_t voltageMonitor; /* 0x10 */
    uint32_t tempMonitor;    /* 0x14 */
    uint32_t linkStatus;     /* 0x18 */
    uint32_t actuatorResult; /* 0x1C */
} BitRegMap;


CBIT

주기적으로 항목을 체크하며 상태를 확인한다.

/* ---------------------------------------------------------------
 * getCbitResult
 * - 운용 중 주기적으로 호출
 * - 측정 항목: 통신 인터페이스 상태, 전원 전압, 온도, 통신 링크
 * --------------------------------------------------------------- */
void getCbitResult(uintptr_t pBaseAddr, BitResult *pOut)
{
    const volatile BitRegMap *pReg = (const volatile BitRegMap *)pBaseAddr;

    pOut->commIfStatus   = pReg->commIfStatus;
    pOut->voltageMonitor = pReg->voltageMonitor;
    pOut->tempMonitor    = pReg->tempMonitor;
    pOut->linkStatus     = pReg->linkStatus;
    /* CPU, RAM, ROM, 액추에이터는 CBIT 항목 아님 → 읽지 않음 */
}

 

일정 주기미다 해당 함수를 호출하는 로직을 구성하고 싶다면 아래 링크를 참고하여 코드를 작성해 보자.

2026.04.13 - [코딩/임베디드] - watchdog을 알아보자

 
IBIT

명령 전송 → 완료 Flag 폴링 → 결과 읽기 순서로 진행된다.

#define IBIT_CMD_START    (0x00000001U)
#define IBIT_DONE_VAL     (0x00000001U)
#define IBIT_DONE_TIMEOUT (10000U)

/* IBIT 명령/Flag 레지스터는 별도 오프셋으로 분리 */
typedef struct {
    uint32_t ibitCmd;      /* 0x00: 실행 명령 (1 = 시작) */
    uint32_t ibitDoneFlag; /* 0x04: 완료 Flag (1 = 완료) */
} IbitCtrlRegMap;

/* ---------------------------------------------------------------
 * requestIbit
 * - 명령 전송 후 완료 Flag 폴링, 이후 결과 복사
 * - 측정 항목: CPU 자가진단, RAM 점검, ROM 무결성, 통신 인터페이스 상태, 액추에이터
 * - return: 0 = 성공, -1 = 타임아웃
 * --------------------------------------------------------------- */
int requestIbit(uintptr_t pCtrlAddr, uintptr_t pDataAddr, BitResult *pOut)
{
    volatile IbitCtrlRegMap *pCtrl = (volatile IbitCtrlRegMap *)pCtrlAddr;
    const volatile BitRegMap *pReg = (const volatile BitRegMap *)pDataAddr;
    uint32_t timeout = IBIT_DONE_TIMEOUT;

    /* 1. IBIT 실행 명령 전송 */
    pCtrl->ibitCmd = IBIT_CMD_START;

    /* 2. 완료 Flag 폴링 */
    while (pCtrl->ibitDoneFlag != IBIT_DONE_VAL) {
        if (--timeout == 0U) {
            return -1; /* 타임아웃 */
        }
    }

    /* 3. 완료 확인 후 결과 복사 */
    pOut->cpuDiag        = pReg->cpuDiag;
    pOut->ramCheck       = pReg->ramCheck;
    pOut->romIntegrity   = pReg->romIntegrity;
    pOut->commIfStatus   = pReg->commIfStatus;
    pOut->actuatorResult = pReg->actuatorResult;
    /* 전압, 온도, 링크는 IBIT 항목 아님 → 읽지 않음 */

    return 0;
}

 

BIT의 개념과 이를 사용하는 방식은 꽤 직관적이고 이해하기에 어렵지 않다.

다만, 어떤 정보를 BIT 항목에 추가할지, 어떻게 관리할지에 대한 고민이 같이 이루어져야 한다.

'코딩 > 기타' 카테고리의 다른 글

배치(.bat) 파일 응용버전 알아보기  (0) 2026.04.20
배치 파일(.bat) 알아보기  (1) 2026.04.16
좋은 코드란 무엇일까  (0) 2026.04.01

2026.04.16 - [코딩/기타] - 배치 파일(.bat) 알아보기

 

배치 파일(.bat) 알아보기

배치 파일이란 CMD에 하나씩 입력하던 명령어들을 메모장에 쭉 적어두고, 한 번에 실행시키는 파일이다.확장자는 .bat이고, 더블클릭 하거나 터미널에서 해당 파일을 실행시킴으로써 동작한다.

sea-sameoil.tistory.com

 

앞선 글에서 배치 파일의 간단한 사용방법을 알아보았다.


더 많은 문법이 있지만, 기본 내용을 토대로 어떤 방식으로 응용 가능한지 알아보자.


1. 필요 사이트/폴더 자동 열기

아침마다 우리가 하는 루틴은 꽤 정해져 있다. 특히 회사를 다닌다면 매일 아침 여는 폴더, 사이트는 어느 정도 일정하다. 이때, 매번 클릭해서 세팅하는 것이 아닌 배치파일을 이용한 자동화가 가능하다.

@echo off
:: 현재 시각의 시(hour)만 추출
for /f "tokens=1 delims=:" %%h in ("%time%") do set HOUR=%%h

:: 오전(9시 이전)이면 업무 시작 모드, 이후면 복귀 모드
if %HOUR% LSS 9 (
    echo start working...
    start "" "https://slack.com"
    start "" "https://notion.so"
    start "" "C:\works\myproject"
) else (
    echo Welcome back. Opening the working directory.
    start "" "C:\works\myproject"
)
  • 실행 시각에 따라 출근 모드와 복귀 모드 설정
  • URL과 폴더는 각자 상황에 맞게 변경 가능

 

2. git 연동 - 태그 기반 히스토리 작상

깃 태그를 이용한 히스토리 관리를 배치파일을 이용해 자동화 가능하다.

@echo off
setlocal enabledelayedexpansion
chcp 65001 > nul

set OUTPUT_DIR=releaseNote
set OUTPUT_FILE=%OUTPUT_DIR%\release_note.md
set TEMP_FILE=%OUTPUT_DIR%\_temp.md

if not exist %OUTPUT_DIR% mkdir %OUTPUT_DIR%

:: 태그 존재 확인
for /f "tokens=*" %%t in ('git tag') do set HAS_TAG=1
if not defined HAS_TAG (
    echo [INFO] No tags found.
    goto :EOF
)

:: 최신 태그 감지
for /f "tokens=*" %%t in ('git describe --tags --abbrev=0') do set LATEST_TAG=%%t
echo [INFO] Latest tag: %LATEST_TAG%

:: 모드 판별
set MODE=view
if not exist %OUTPUT_FILE% set MODE=generate
if "%MODE%"=="view" findstr /c:"%LATEST_TAG%" %OUTPUT_FILE% > nul 2>&1 || set MODE=generate
echo [INFO] Mode: %MODE%
echo.

:: 태그 목록 출력 (공통)
echo ================================
echo  TAG LIST
echo ================================
for /f "tokens=*" %%t in ('git tag --sort=-v:refname') do (
    for /f "tokens=*" %%h in ('git rev-list -n 1 %%t') do (
        set HASH=%%h
        echo  [%%t] !HASH:~0,7!
    )
)
echo ================================
echo.

if "%MODE%"=="view" (
    echo [INFO] Release note is up to date.
    goto :EOF
)

:: 릴리즈 노트 생성
echo [INFO] Generating release note...
(
    echo # Release Note
    echo.
    echo ## History
    echo.
    echo ^| Version ^| Date ^| Type ^| Key Change ^| Author ^| Commit ^|
    echo ^|---^|---^|---^|---^|---^|---^|
) > %TEMP_FILE%

for /f "tokens=*" %%t in ('git tag --sort=-v:refname') do (
    set TAG=%%t
    set TAG_DATE=  & set TAG_KEY=  & set TAG_AUTHOR=  & set TAG_TYPE=  & set TAG_HASH=

    for /f "tokens=*" %%h in ('git rev-list -n 1 %%t') do (
        set FULL=%%h
        set TAG_HASH=!FULL:~0,7!
    )
    for /f "tokens=1* delims=:" %%a in ('git tag -l -n99 %%t') do (
        if /i "%%a"=="Date"       set TAG_DATE=%%b
        if /i "%%a"=="Key Change" set TAG_KEY=%%b
        if /i "%%a"=="Author"     set TAG_AUTHOR=%%b
        if /i "%%a"=="Type"       set TAG_TYPE=%%b
    )

    echo ^|[!TAG!]^(#!TAG!^)^|!TAG_DATE!^|!TAG_TYPE!^|!TAG_KEY!^|!TAG_AUTHOR!^|!TAG_HASH!^|
) >> %TEMP_FILE%

for /f "tokens=*" %%t in ('git tag --sort=-v:refname') do (
    (
        echo.
        echo ---
        echo.
        echo ## %%t
        echo.
    ) >> %TEMP_FILE%
    git tag -l -n99 %%t >> %TEMP_FILE%
)

move /y %TEMP_FILE% %OUTPUT_FILE% > nul
echo [INFO] Done: %OUTPUT_FILE%
pause
  • Git annotated tag의 메시지 본문을 파싱해 릴리즈 노트를 자동화한 배치 스크립트
  • rev-list로 커밋을 역추적하고 문자열 슬라이싱으로 해시 단축

 

3. git 연동 - git diff 이용한 release note 작성

git과 연동한다면 각 배포 버전별로 차이점을 확인하여 release note 작성에 자동화 도입도 가능하다.

@echo off
setlocal enabledelayedexpansion

:: 현재/이전 태그 추출
for /f "tokens=*" %%i in ('git describe --tags --abbrev=0') do set CUR_TAG=%%i
for /f "skip=1 tokens=*" %%i in ('git tag --sort=-v:refname') do (
    if not defined PREV_TAG set PREV_TAG=%%i
)

:: 범위 설정 (이전 태그 없으면 전체)
if defined PREV_TAG (
    set RANGE=%PREV_TAG%..%CUR_TAG%
) else (
    set RANGE=%CUR_TAG%
)

:: 커밋을 타입별로 분류해 마크다운 작성
(
    echo # Release Note - %CUR_TAG%
    echo.
    echo ## Features
    git log --oneline --no-merges %RANGE% --grep="^feat"
    echo.
    echo ## Bug Fixes
    git log --oneline --no-merges %RANGE% --grep="^fix"
    echo.
    echo ## Others
    git log --oneline --no-merges %RANGE% --grep="^chore\|^refactor\|^docs"
) > release_note.md

pandoc release_note.md -o release_note_%CUR_TAG%.docx --reference-doc=template.docx 2>nul || pandoc release_note.md -o release_note_%CUR_TAG%.docx

echo [INFO] Generated: release_note_%CUR_TAG%.docx
  • Conventional Commits 컨벤션을 전제로, --grep으로 커밋을 타입별 섹션에 자동 분류
  • 태그 범위를 PREV..CUR로 좁혀 버전 간 변경분만 추출

 



배치 파일은 가벼우면서도 자동으로 원하는 동작을 수행하는데 용이하다. 처음에는 해당 파일을 작성하고 이용하는 게 낯설 수 있지만, 작은 단계부터 하나씩 이용하다 보면 부담스럽지 않게 사용 가능하다.

'코딩 > 기타' 카테고리의 다른 글

BIT(Built-In Test)를 알아보자  (1) 2026.04.24
배치 파일(.bat) 알아보기  (1) 2026.04.16
좋은 코드란 무엇일까  (0) 2026.04.01

전시회를 종종 다니기는 했었으나 예술계가 어떻게 흐르고 있는지, 어떤 맥락을 가지고 있는지에 대해서는 큰 관심이 없었다.

그러나 예술계의 밑낯을 보여준다는 책의 홍보 문구에 나도 모르게 끌려 이 책을 읽게 되었다.


 

전시회, 아트 페어를 다니면서도 현대 예술에 대해서는 꽤 회의적인 시각을 가지고 있었다.

현대 예술이 지닌 맥락과 의미를 잘 이해하지 못했기에 미술관에 가서도 주로 중세 회화만을 감상하곤 했다.

 

책을 통해 예술 생태계의 단면을 조금이나마 보게 되었다. 젊은 신인 작가부터 예술계에서 미술관이 가지는 위치까지 조금이나마 맛봤다.
뉴욕의 젊은 예술가들은 생각보다 고단하게 삶을 이어가고 있었고, 예술계가 누구에게나 열려있지만 모두에게 닫혀있음을 알게 되었다.

작가의 예술계 진입기를 함께 하면서 어떤 게 예술인지 같이 고민했고 미술관에 작품이 들어가는 것만이 좋은 작품의 증표인지에 대해 생각해 보았다.

 

이러한 과정에서 고전 회화만이 진정한 '예술'이라 생각해 왔던 스스로를 반성했다. 내가 고전 작품에 끌렸던 이유는 단지 책에서 여러 번 마주한 '익숙함' 때문이었을지도 모른다. 또한 흔히 사람은 추상보다 형상이 명확한 구상회화를 선호하는데, 중세 미술이 현대 미술보다 구상적인 측면이 강하다 보니 현대 미술에서 눈을 돌렸던 것일지도 모른다.

 

하지만 현실을 살짝 비틀어 새로운 세계를 보여주는 것은 시대를 관통하는 예술의 한 줄기라 생각한다.

과거와 현대 미술이 결국 한 줄기로 이어져 있다는 사실을 깨닫자, 이제는 현대 작가들이 각자의 시각으로 세상을 어떻게 해석하고 표현하는지 궁금해졌다.

이제 다시 전시를 다니면서 다양한 예술을 접해보려 한다. 안목을 기르기 위한 목적도 있지만 다양한 예술을 하는 사람들을 응원하고 넓은 예술을 계속 만들어주기를 바라는 측면에서 전시를 다녀보려 한다.


 

아래는 간단한 느낀 점들이다.

  • 돈 많은 컬렉터들만 작품을 사는 것이 아니다.
    • 작품은 다양한 가격대로 형성되어 있고, 작품을 사는 것을 너무 부담스럽게 생각하지 않아도 괜찮다.
    • 작품을 산다는 것이 누군가에게는 응원으로, 작가의 삶을 이어가게 해주는 원동력이 된다는 것을 알게 되었다.
    • 나도 '내가 집에 두고 싶은 작품'을 만나면 구매해보고 싶다.
  • 미술관에 있는 작품들은 누군가에 의해 필터링된 작품들이다.
    • 미술관에 있다고 해서 '인증된' 작품이 아닐 수도 있다.
    • 큐레이팅도 사람이 하는 것이라 누군가의 취향과 입김이 들어가 있을 수 있다.
    • 아직 안목이 없는 사람으로서 미술관 선정 작품 위주로 즐기되 미술관이 제공한 시선만이 '좋은' 예술이라고 받아들이지는 않아야겠다.
  • 예술을 접하는 방식에는 여러 가지가 있다.
    • 예술의 맥락(어떤 큐레이터가 이 작가 게시글에 좋아요를 눌렀는지, 어떤 대학을 나왔고, 어떤 컬렉터가 해당 예술을 좋아하는지 등...)을 기반으로 작품을 이해하는 방식이 있다.
    • 예술 그 자체를 즐기는 방식이 있다. 책에서는 한 작품을 최소 5분을 보면서 어떤 점들이 나의 눈에 들어오는지 5가지를 꼽으면서 즐기면 한 작품을 보더라도 다양하게 즐길 수 있다고 한다.
    • 개인적으로는 후자의 방식이 더 마음에 든다. 천천히 작품을 즐기는 시간을 가지며 미술관을 다녀보려 한다.

 

책을 읽으면서 예술에 관한 흥미가 조금씩 더 자라남을 느낄 수 있었다.

색채, 미술사 등에 대해서도 조금씩 알아가면 예술을 더욱 즐길 수 있지 않을까 싶다.

평소 예술에 관심이 있던 사람이라면 더욱 재밌게 이 책을 읽을 수 있을듯하다.

배치 파일이란 CMD에 하나씩 입력하던 명령어들을 메모장에 쭉 적어두고, 한 번에 실행시키는 파일이다.

확장자는 .bat이고, 더블클릭 하거나 터미널에서 해당 파일을 실행시킴으로써 동작한다.

 

배치 파일을 사용하는 가장 큰 이유는 업무 자동화 때문이다.

매번 반복되는 시스템 설정이나 파일 관리 작업을 일일이 수동으로 처리하다 보면 실수가 발생하기 쉽지만, 배치 파일을 이용하면 정해진 명령어를 실행해 오류를 방지한다.

 

이 글에서는 자주 사용할 수 있는 기본 문법을 알아보자.


변수 활용과 사용자 입력받기

가장 먼저 익혀야 할 것은 set 명령어를 통해 데이터를 저장하고 불러오는 방법이다.

@echo off
set /p USER_NAME=what is your name? : 
set GREETING=hello, %USER_NAME%!

echo ---------------------------------
echo %GREETING%
echo today's date is %date%, current time is %time%.
echo ---------------------------------
pause

 

  • set /p: 사용자로부터 직접 입력을 받는다
  • %변수명%: 저장된 변수 값을 불러올 때 사용한다
  • 시스템 변수: %date%, %time% 처럼 윈도우가 기본으로 제공하는 정보
더보기

💡echo off와 pause

 

@echo off (화면 정리) : 실행되는 명령어 과정을 숨기고 결과만 보여줌 (default로 사용하면 좋음)

pause (창 닫기 방지): 작업이 끝난 뒤 창이 바로 꺼지지 않게 일시 정지 ("계속하려면 아무 키나 누르십시오"라는 문구 띄움)

| (파이프)와 find

파이프는 앞 명령어의 결과물(출력)을 뒤 명령어의 재료(입력)로 바로 넘긴다. 주로 특정 단어를 검색할 때 많이 쓴다.

@echo off
echo find all files including 'config'.

dir | find "config"

:: 네트워크 연결 상태 중 'ESTABLISHED'된 연결만 확인
echo.
echo current network list:
netstat -an | find "ESTABLISHED"

pause

& (순차 실행)와 && (성공 시 실행)

명령어 여러 개를 한 줄에 쓸 때 사용한다.

@echo off

:: 폴더를 만들고(mkdir), 성공하면(&&) 그 안으로 이동(cd)해서 파일을 생성
mkdir test_folder && cd test_folder && echo 성공! > result.txt

echo current folder location: %cd%
pause

응용

@echo off

set REPORT_NAME=Weekly_Report
set /p USER_ID=Enter User ID: 

:: (기존 양식 내용 + 사용자 정보)를 합쳐서 하나의 파일로 저장
(
  echo ==============================
  echo report title: %REPORT_NAME%
  echo author: %USER_ID%
  echo date: %date%
  echo ==============================
  echo.
  echo [directory information]
  dir /b
) > Final_Report.txt

echo Final_Report.txt created.
pause
  • >는 기존 내용을 지우고 새로 쓰며, >>는 기존 내용 끝에 덧붙인다.
  • ()를 사용하면 한 번에 여러 내용 작성 가능하다.
  • dir /b는 현재 디렉토리를 알파벳 순으로 정렬해서 보여주고, /r을 사용하면 역순으로 보여준다.

터미널 실행
결과

더보기

✅ 윈도우엔 .bat, 리눅스엔 .sh

 

배치 파일은 윈도우 전용이다. 리눅스나 macOS에서 사용하는 자동화 파일은 셸 스크립트(.sh)다.

문법은 다르지만 하는 일은 같다


이 글에서 다 설명하지는 못했지만 윈도우 명령어를 안다면 더 다양하게 조합 가능하다.

위에 정리한 내용을 바탕으로 배치 파일을 어떻게 응용할 수 있는지는 다음 글에 정리할 예정이다.

'코딩 > 기타' 카테고리의 다른 글

BIT(Built-In Test)를 알아보자  (1) 2026.04.24
배치(.bat) 파일 응용버전 알아보기  (0) 2026.04.20
좋은 코드란 무엇일까  (0) 2026.04.01

현대 CPU는 성능을 극대화하기 위해 명령어 실행 순서를 임의로 바꾸는 비순차적 실행(Out-of-Order Execution)을 수행한다.
특히 읽기(Load)와 쓰기(Store) 연산이 서로 독립적이라고 판단되면, 하드웨어는 효율을 위해 그 순서를 마음대로 재정렬한다.
단일 코어에서는 문제가 없으나, 공유 메모리를 사용하는 멀티 코어 환경이나 하드웨어 레지스터를 제어할 때는 데이터가 꼬이는 치명적인 결과를 초래한다.
 
vxWorks는 이러한 메모리 순서 문제를 해결하기 위해 아키텍처별 전용 어셈블리를 추상화한 메모리 배리어 매크로를 제공한다.


Read Memory Barrier: VX_MEM_BARRIER_R()

이 매크로는 배리어 이전의 읽기(Load) 연산들이 배리어 이후의 읽기 연산들보다 반드시 먼저 완료되도록 보장한다.
다만, 쓰기(Store) 연산의 순서에는 관여하지 않는다.
주로 공유 변수나 플래그를 먼저 확인한 후, 그 상태에 의존적인 다른 데이터를 읽어야 할 때 사용한다.

/* 다른 프로세서나 하드웨어가 데이터를 먼저 쓰고 flag를 1로 바꿈 */
void processData(void) {
    int currentStatus;
    int sensorValue;

    // 1. 상태 플래그 확인
    currentStatus = shared_status_flag;

    if (currentStatus == READY) {
        /* 하드웨어가 효율을 위해 플래그 확인(1)과 데이터 읽기(3) 순서를 바꿀 수 있음 
           이를 방지하기 위해 Read Barrier 사용 */
        VX_MEM_BARRIER_R(); 

        // 3. 최신 데이터 읽기
        sensorValue = shared_data_value;
        printf("Sensor: %d\n", sensorValue);
    }
}

 


Write Memory Barrier: VX_MEM_BARRIER_W()

이 매크로는 배리어 이전의 쓰기(Store) 연산들이 배리어 이후의 쓰기 연산들보다 먼저 메모리에 반영(가시화)되도록 보장한다.
주로 데이터 값을 메모리에 완전히 쓴 후, 상태 신호를 보내야 하는 하드웨어 제어 시나리오에 필수적이다.

void updateControlRegister(uint32_t data) {
    *(volatile uint32_t *)REG_DATA = data; // 1. 설정값 입력
    
    VX_MEM_BARRIER_W();                     // 2. 쓰기 순서 고정
    
    *(volatile uint32_t *)REG_CTRL = 0x1;  // 3. 시작 신호 전송
}

Full Memory Barrier: VX_MEM_BARRIER_RW()

이 매크로는 읽기와 쓰기 모두에 대해 장벽을 세운다.
배리어 이전의 모든 메모리 접근이 배리어 이후의 모든 메모리 접근보다 먼저 완료됨을 엄격히 보장한다.
주로 읽기와 쓰기가 뒤섞인 복합적인 연산에서 데이터 순서가 절대적으로 중요할 때 사용한다.
가장 안전하지만 성능 비용이 가장 크다.

/* 양방향 통신이나 메시지 큐 인터페이스 구현 시 */
void exchangeMessage(uint32_t outMsg) {
    // 1. 메시지 송신 (Store)
    *(volatile uint32_t *)TX_BUF = outMsg;

    /* 모든 읽기/쓰기가 완료될 때까지 대기. 
       메시지를 다 쓰기 전에 응답을 읽으려 하거나, 
       순서가 섞이는 것을 원천 차단함 */
    VX_MEM_BARRIER_RW();

    // 3. 상대방의 응답 수신 (Load)
    uint32_t response = *(volatile uint32_t *)RX_BUF;
}

개발 중인 시스템의 요구사항에 맞춰 적절한 배리어를 선택하여 개발한다면 더욱 안정적인  시스템 개발이 가능하다.

'코딩 > 임베디드' 카테고리의 다른 글

PCIe 개념 정리 (1)  (0) 2026.04.29
Exception Hook을 알아보자  (0) 2026.04.14
watchdog을 알아보자  (0) 2026.04.13
세마포어 알아보기  (0) 2026.04.09
Serial RX를 알아보자  (0) 2026.04.08

어플리케이션 동작 중 에러가 발생하면, CPU는 해당 예외(Exception)를 감지하고 자동으로 VxWorks의 Exception Handler로 제어를 넘긴다.

VxWorks는 이 과정에서 _func_excBaseHook이라는 함수 포인터를 먼저 호출하는 구조를 제공하는데, 이를 이용하면 Exception Handler로 넘어가기 전에 원하는 동작을 끼워 넣을 수 있다.

예를 들어, 에러 로그를 저장하거나, 발생한 예외의 종류와 레지스터 상태를 출력하거나, 특정 복구 루틴을 먼저 수행하는 것이 가능하다.


Exception Hook이란?

VxWorks에서 예외가 발생하면 내부적으로 다음 순서로 처리된다.

  • 예외 발생 → _func_excBaseHook 호출 (등록된 경우) → VxWorks 기본 Exception Handler

_func_excBaseHook은 전역 함수 포인터로, 기본값은 VxWorks가 제공하는 핸들러를 가리킨다.

이 포인터를 우리가 작성한 함수로 교체하면, 예외 발생 시 우리 코드가 먼저 실행된다.


Exception Hook 등록하기

과정은 크게 어렵지 않다.

  1. 기존 _func_excBaseHook 포인터를 별도 변수에 저장 (기존 핸들러 보존)
  2. _func_excBaseHook을 우리가 작성한 Hook 함수로 교체
  3. Hook 함수 마지막에 저장해 둔 기존 핸들러를 호출하여 VxWorks 기본 처리 흐름 유지
#include <vxWorks.h>
#include <arch/ppc/esfPpc.h>
#include <arch/ppc/excPpcLib.h>

/* 기존 핸들러 포인터를 보존하기 위한 전역 변수 */
BOOL (*old_func_excBaseHook)(int32_t, ESFPPC *, REG_SET *, EXC_INFO *);

void setExceptionHook(void);
BOOL testHook(int32_t vecOff, ESFPPC *pEsf, REG_SET *pRegSet, EXC_INFO *pExcInfo);

/*
 * testHook - 예외 발생 시 먼저 실행될 커스텀 Hook 함수
 *
 * vecOff   : 예외 벡터 오프셋 (어떤 종류의 예외인지 식별)
 * pEsf     : Exception Stack Frame - 예외 발생 시점의 스택 정보
 * pRegSet  : 예외 발생 시점의 CPU 레지스터 상태
 * pExcInfo : 예외의 종류와 원인 등 상세 정보
 */
BOOL testHook(int32_t vecOff, ESFPPC *pEsf, REG_SET *pRegSet, EXC_INFO *pExcInfo) {

    /* 여기에 원하는 동작 삽입 (로그 저장, 레지스터 덤프 출력 등) */
    printf("@@@ Exception Hook Called | vecOff: 0x%X @@@\n", vecOff);

    /* 기존 VxWorks 핸들러에 제어를 넘겨 정상 흐름 유지 */
    return old_func_excBaseHook(vecOff, pEsf, pRegSet, pExcInfo);
}

void setExceptionHook(void) {
    IMPORT BOOL (*_func_excBaseHook)(int32_t, ESFPPC *, REG_SET *, EXC_INFO *);

    /* 1. 기존 핸들러 보존 */
    old_func_excBaseHook = _func_excBaseHook;

    /* 2. 커스텀 Hook으로 교체 */
    _func_excBaseHook = &testHook;
}

 

다만, Hook 함수는 Exception 컨텍스트에서 실행되므로, 작성 시 다음 사항에 유의해야 한다.

  • 시간이 오래 걸리는 작업 지양
    : 예외 처리 도중 시스템이 오랫동안 블로킹되면 다른 태스크나 타이머에 영향을 줄 수 있기 때문
  • 블로킹 함수 사용 피하기
    : semTake, taskDelay 등 대기가 발생하는 함수는 이 컨텍스트에서 사용 X
  • 기존 핸들러 반드시 호출
    : Hook 함수 마지막에 old_func_excBaseHook을 호출하지 않으면 VxWorks의 기본 예외 처리(태스크 종료, 로그 기록 등)가 수행되지 않아 시스템이 불안정해질 수 있다.

Exception Hook 이라는 단어가 주는 어감에 비해 사용이 간편하니 에러 관리 차원에서 사용하면 좋은 기능이다.

'코딩 > 임베디드' 카테고리의 다른 글

PCIe 개념 정리 (1)  (0) 2026.04.29
Memory Barrier를 알아보자  (1) 2026.04.15
watchdog을 알아보자  (0) 2026.04.13
세마포어 알아보기  (0) 2026.04.09
Serial RX를 알아보자  (0) 2026.04.08

와치독(Watchdog)이란 시스템의 오동작을 감지하고 자동으로 복구하기 위한 타이머 기반의 감시 메커니즘이다.
정상적인 동작 중에는 소프트웨어가 주기적으로 와치독 타이머를 갱신(refresh)하여 시스템이 정상 동작 중임을 알린다.
만약 소프트웨어가 오류, 무한 루프, 데드락 등으로 인해 타이머를 갱신하지 못하면, 와치독은 시스템을 리셋하거나 미리 등록된 복구 루틴을 실행한다.

임베디드 시스템, RTOS 환경, 하드웨어 장치 등 신뢰성이 중요한 시스템에서 광범위하게 사용된다.


소프트웨어 와치독 vs 하드웨어 와치독

와치독은 어디서 구현되느냐에 따라 소프트웨어 와치독과 하드웨어 와치독으로 나뉜다.
두 가지 모두 "일정 주기마다 뭔가를 한다"는 타이머 역할은 동일하다.

 하드웨어 와치독소프트웨어 와치독
구현 위치MCU/FPGA 내부 전용 하드웨어 회로OS 또는 소프트웨어 레이어
OS hang 시 동작 여부동작함같이 죽음
신뢰성높음상대적으로 낮음
예시FPGA WDT 레지스터, MCU 내장 WDTVxWorks `wdCreate/wdStart`


이 글에서는 각각의 대표적인 활용 예시로 아래 두 가지를 다룬다.

  • 소프트웨어 와치독 (VxWorks API) → 주기적 태스크 실행에 활용
  • 하드웨어 와치독 (FPGA 레지스터) → 시스템 생존 감시에 활용

소프트웨어 와치독 - vxWorks API를 활용한 주기적 태스크 제어

VxWorks에서 제공하는 소프트웨어 와치독 API를 사용하면, taskDelay보다 더 정교하게 일정 주기마다 태스크를 제어할 수 있다.

#define WATCHDOG_PERIOD   (30)

typedef struct {
    SEM_ID  syncSem;
    WDOG_ID periodicWd;
} WdParam;

/* 와치독 핸들러: 인터럽트 레벨에서 호출됨 */
static void watchdogHandler(WdParam *arg) {
    /* 다음 주기를 위해 와치독을 재시작 (핸들러는 일회성이므로 반드시 재등록 필요) */
    wdStart(arg->periodicWd, WATCHDOG_PERIOD, (FUNCPTR)watchdogHandler, (long)arg);
    semGive(arg->syncSem);
}

/* 주기적으로 실행될 태스크 */
void taskManager(SEM_ID syncSem) {
    while (1) {
        semTake(syncSem, WAIT_FOREVER);
        /* 이 아래에 주기마다 실행할 작업을 작성 */
    }
}

void initTask(void) {
    SEM_ID  syncSem;
    WDOG_ID periodicWd;
    WdParam wdParam;
    TASK_ID taskId;

    /* 1. 바이너리 세마포어 및 와치독 생성 */
    syncSem    = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
    periodicWd = wdCreate();

    wdParam.syncSem    = syncSem;
    wdParam.periodicWd = periodicWd;

    /* 2. 태스크 생성 후 활성화
     *    taskSpawn  : 생성과 동시에 실행
     *    taskCreate : 생성만 수행 → taskActivate로 별도 실행 필요
     */
    taskId = taskCreate("testTask", 100, VX_USR_TASK_OPTIONS_BASE,
                        0x400000, (FUNCPTR)taskManager,
                        (long)syncSem, 0, 0, 0, 0, 0, 0, 0, 0, 0);
    taskActivate(taskId);

    /* 3. 와치독 시작 */
    wdStart(wdParam.periodicWd, sysClkRateGet() * WATCHDOG_PERIOD,
            (FUNCPTR)watchdogHandler, (long)&wdParam);

    while (1) {
        taskDelay(sysClkRateGet());
    }
}

 
전체 로직 흐름

  1. 바이너리 세마포어와 와치독 타이머 생성
  2. 태스크 생성 후 활성화 (taskCreate + taskActivate)
  3. 와치독 시작

바이너리 세마포어 관련 설명은 아래 글을 참고하면 된다.
2026.04.09 - [코딩/임베디드] - 세마포어 알아보기

세마포어 알아보기

이 글은 vxWorks6.9 기준으로 작성되었다.vxWorks6.9에서는 크게 4가지의 세마포어 기능을 제공한다.binary► 가장 빠르고, 범용 목적의 세마포어► 동기화 혹은상호 배제(mutual exclusion)에 최적화 mutual ex

sea-sameoil.tistory.com

 
와치독 동작 원리

  1. wdCreate()로 와치독 생성
  2. wdStart()로 와치독 시작 - 지정한 틱(tick) 이후 등록된 핸들러 함수를 인터럽트 레벨에서 일회성으로 호출
  3. 핸들러 내에서 다음 주기를 위해 wdStart()를 재호출 (재등록하지 않으면 한 번만 실행되고 종료됨)
  4. 핸들러가 semGive()로 세마포어를 제공 → 태스크가 깨어나 작업 수행

이 방식을 사용하면 `taskDelay` 기반의 단순 지연보다 정교한 주기 제어가 가능하다.
taskDelay는 태스크 자체의 실행 시간이 지연 주기에 포함되지만, 와치독은 독립적인 타이머로 동작하기 때문에 태스크 실행 시간에 영향을 받지 않는다.

더보기

관련 API

WDOG_ID wdCreate(void)
  • 와치독 타이머를 생성하고 WDOG 구조체를 메모리에 할당한다.
  • 반환값: 생성된 와치독의 ID, 메모리 부족 시 NULL
STATUS wdStart(
    WDOG_ID       wdId,       /* 와치독 ID */
    _Vx_Ticks_t   delay,      /* 지연 틱 수 */
    FUNCPTR       pRoutine,   /* 타임아웃 시 호출할 함수 */
    _Vx_usr_arg_t parameter   /* 함수에 전달할 인자 */
)
  • 와치독 타이머를 시스템 틱 큐에 등록한다. 지정된 틱이 경과하면 등록된 루틴이 인터럽트 레벨에서 호출된다.
  • 반환값: OK 또는 ERROR
💡 주의
핸들러는 인터럽트 컨텍스트에서 실행되므로, 블로킹 함수(semTake, taskDelay 등)는 사용할 수 없다.

하드웨어 와치독 - FPGA 레지스터를 활용한 시스템 생존 감시

하드웨어 와치독은 소프트웨어가 아닌 하드웨어 레벨(FPGA, MCU 내부 레지스터 등)에서 동작하는 타이머다.
소프트웨어가 정해진 시간 안에 와치독을 refresh 하지 않으면, 하드웨어가 직접 시스템을 리셋한다.

소프트웨어 와치독과 달리, OS가 hang되거나 커널 자체에 문제가 생긴 경우에도 동작할 수 있어 더 강력한 안전망 역할을 한다.
 
동작 흐름

  1. 와치독 등록: 동작 주기, 타임아웃 시 실행할 콜백 함수 등록 (선택)
  2. 와치독 활성화: enable 레지스터 설정
  3. 주기적 refresh: 카운터가 설정값에 도달하기 전에 주기적으로 0으로 리셋
  4. refresh가 누락되면 카운터가 설정한 주기값에 도달 → 시스템 리셋 발생
  5. 와치독 비활성화: 더 이상 필요하지 않을 때 disable 설정
구체적인 레지스터 구성은 하드웨어 구현(FPGA IP, SoC 등)에 따라 다르므로, 전체적인 흐름을 이해하는 것이 중요하다.

 
아래는 FPGA에 구현된 와치독 레지스터를 VxWorks에서 메모리 맵 I/O(Memory-Mapped I/O)로 접근하는 예시다.

#include <vxWorks.h>
#include <sysLib.h>
#include <taskLib.h>

/* FPGA 와치독 레지스터 베이스 주소 (실제 주소로 대체 필요) */
#define WDT_BASE_ADDR       (0xA0000000)

/* 레지스터 오프셋 */
#define WDT_REG_ENABLE      (WDT_BASE_ADDR + 0x00)  /* 와치독 활성화 레지스터 */
#define WDT_REG_PERIOD      (WDT_BASE_ADDR + 0x04)  /* 타임아웃 주기 레지스터 */
#define WDT_REG_REFRESH     (WDT_BASE_ADDR + 0x08)  /* 카운터 리셋(refresh) 레지스터 */

/* 레지스터 값 정의 */
#define WDT_ENABLE_VAL      (0x00000001)  /* 와치독 활성화 */
#define WDT_DISABLE_VAL     (0x00000000)  /* 와치독 비활성화 */
#define WDT_REFRESH_VAL     (0x00000001)  /* 카운터 리셋 트리거 값 */
#define WDT_PERIOD_VAL      (0x000003E8)  /* 타임아웃 주기 설정값 (하드웨어 사양에 따라 조정) */

/* 레지스터 읽기/쓰기 매크로 */
#define WDT_REG_WRITE(addr, val)  (*(volatile UINT32 *)(addr) = (val))
#define WDT_REG_READ(addr)        (*(volatile UINT32 *)(addr))

/* refresh 태스크 주기 (틱 단위) — 타임아웃 주기의 절반 이하로 설정 권장 */
#define WDT_REFRESH_PERIOD_TICKS  (sysClkRateGet() / 2)

/*
* hwWdtInit - 하드웨어 와치독 초기화 및 활성화
*/
STATUS hwWdtInit(void) {
    /* 1. 타임아웃 주기 설정 */
    WDT_REG_WRITE(WDT_REG_PERIOD, WDT_PERIOD_VAL);

    /* 2. 와치독 활성화 */
    WDT_REG_WRITE(WDT_REG_ENABLE, WDT_ENABLE_VAL);

    printf("[WDT] Hardware watchdog enabled. Period reg = 0x%08X\n",
           WDT_REG_READ(WDT_REG_PERIOD));

    return OK;
}

/*
* hwWdtRefresh - 와치독 카운터 리셋 (주기적으로 호출 필요)
*/
void hwWdtRefresh(void) {
    WDT_REG_WRITE(WDT_REG_REFRESH, WDT_REFRESH_VAL);
}

/*
* hwWdtDisable - 하드웨어 와치독 비활성화
*/
void hwWdtDisable(void) {
    WDT_REG_WRITE(WDT_REG_ENABLE, WDT_DISABLE_VAL);
    printf("[WDT] Hardware watchdog disabled.\n");
}

/*
* hwWdtRefreshTask - 주기적으로 와치독을 refresh하는 태스크
* 시스템이 정상 동작 중일 때만 refresh가 수행되도록 조건을 추가할 수 있다.
*/
void hwWdtRefreshTask(void) {
    while (1) {
        hwWdtRefresh();
        taskDelay(WDT_REFRESH_PERIOD_TICKS);
    }
}

/*
* hwWdtStart - 와치독 초기화 및 refresh 태스크 시작
*/
STATUS hwWdtStart(void) {
    TASK_ID tid;

    if (hwWdtInit() != OK) {
        printf("[WDT] Failed to initialize hardware watchdog.\n");
        return ERROR;
    }

    /* refresh 태스크 생성 및 실행 */
    tid = taskSpawn("tHwWdt", 50, VX_USR_TASK_OPTIONS_BASE,
                    0x10000, (FUNCPTR)hwWdtRefreshTask,
                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
    if (tid == TASK_ID_ERROR) {
        printf("[WDT] Failed to spawn watchdog refresh task.\n");
        return ERROR;
    }

    printf("[WDT] Watchdog refresh task started (tid = %d).\n", tid);
    return OK;
}
  • refresh 태스크의 우선순위는 시스템의 다른 태스크보다 충분히 높게 설정해야 한다.
  • 태스크가 스케줄되지 못해 refresh가 누락되면 불필요한 리셋이 발생한다.
  • 실제 레지스터 주소 및 값은 사용하는 FPGA IP의 데이터시트를 반드시 참고해야 한다.
  • 위 예시는 구조 이해를 위한 범용 예시다.

와치독은 단순한 타이머처럼 보이지만, 신뢰성이 중요한 임베디드 시스템에서 중요하게 사용되는 메커니즘이다.
두 가지를 동시에 사용하는 경우도 많으며 하드웨어 와치독의 경우 구체적인 레지스터 구성이나 API는 자신이 개발하는 환경을 확인한 후에 적용해야 한다. 

'코딩 > 임베디드' 카테고리의 다른 글

Memory Barrier를 알아보자  (1) 2026.04.15
Exception Hook을 알아보자  (0) 2026.04.14
세마포어 알아보기  (0) 2026.04.09
Serial RX를 알아보자  (0) 2026.04.08
Serial TX를 알아보자  (1) 2026.04.03

+ Recent posts