<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>wangki.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 19 May 2026 14:52:40 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>wangki.log</title>
            <url>https://velog.velcdn.com/images/wang_ki/profile/565fcb49-2b74-4f92-9f3f-71c73b496b38/image.webp</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. wangki.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/wang_ki" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[windows driver npu hardware 온도 읽어오기 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-npu-hardware-%EC%98%A8%EB%8F%84-%EC%9D%BD%EC%96%B4%EC%98%A4%EA%B8%B0</link>
            <guid>https://velog.io/@wang_ki/windows-driver-npu-hardware-%EC%98%A8%EB%8F%84-%EC%9D%BD%EC%96%B4%EC%98%A4%EA%B8%B0</guid>
            <pubDate>Tue, 19 May 2026 14:52:40 GMT</pubDate>
            <description><![CDATA[<p>google coral tpu의 온도 값을 읽어오는 기능을 구현했다. 어떤 흐름으로 구현했는지에 대해서 포스팅할 예정이다.</p>
<hr>
<h3 id="구현-흐름">구현 흐름</h3>
<p>npu_driver에 ioctl handler, hardware register offset, init 을 해준다. 
npu runtime dll에서는 온도 값을 읽는 IOCTL을 래핑 한다. 
python gui 에서 npu runtime dll을 로드하여 호출해 준다. </p>
<hr>
<h3 id="linux-driver-소스-참고">linux driver 소스 참고</h3>
<p><code>gasket-driver</code> 소스를 참고하였다. </p>
<h4 id="변환-공식">변환 공식</h4>
<pre><code class="language-c">// linux
  static int adc_to_millic(int adc) {
      return (662 - adc) * 250 + 550;
  }
  static int millic_to_adc(int millic) {
      return (550 - millic) / 250 + 662;
  }</code></pre>
<p>하드웨어로부터 읽은 값을 온도 값으로 변환해 주는 공식인 것 같다. </p>
<p>우리 쪽 소스에도 동일하게 추가했다.</p>
<pre><code class="language-c">// windows
static inline INT32 apex_adc_to_millic(UINT32 adc) {
    return (662 - (INT32)adc) * 250 + 550;
}
static inline UINT32 apex_millic_to_adc(INT32 millic) {
    return (UINT32)((550 - millic) / 250 + 662);
}</code></pre>
<h4 id="ioctl-핸들러">IOCTL 핸들러</h4>
<pre><code class="language-c">// linux
  case ATTR_TEMP:
      value = gasket_dev_read_32(gasket_dev, APEX_BAR_INDEX,
                                 APEX_BAR2_REG_OMC0_DC);
      value = (value &gt;&gt; 16) &amp; ((1 &lt;&lt; 10) - 1);    // ★ bits [25:16]
      ret = scnprintf(buf, PAGE_SIZE, &quot;%i\n&quot;, adc_to_millic(value));
      break;</code></pre>
<p>gasket driver의 IOCTL 핸들러 부분이다. 32비트 레지스터 값을 읽은 뒤 온도 부분 10비트만 파싱 하는 내용이다. </p>
<pre><code class="language-c">// windows
#define APEX_OMC_DC_ADC_SHIFT          16
#define APEX_OMC_DC_ADC_MASK           0x3FF  // 10-bit 
#define APEX_OMC_DC_ADC_FROM_REG(r)    (((UINT32)(r) &gt;&gt; APEX_OMC_DC_ADC_SHIFT) &amp; APEX_OMC_DC_ADC_MASK)</code></pre>
<p>값을 16비트 우측으로 shift 시킨 뒤, 10비트를 가져오는 방법을 매크로로 표현했다. </p>
<h4 id="초기화">초기화</h4>
<p>gasket에서 사용한 초기화 방식을 그대로 모방하였다. </p>
<pre><code class="language-c">// linux
  static void enable_thermal_sensing(struct gasket_dev *gasket_dev) {
      // Enable thermal sensor clocks
      gasket_read_modify_write_32(gasket_dev, APEX_BAR_INDEX,
                                  APEX_BAR2_REG_OMC0_D0, 0x1, 1, 7);
      // Enable thermal sensor (ENAD ENVR ENBG)
      gasket_read_modify_write_32(gasket_dev, APEX_BAR_INDEX,
                                  APEX_BAR2_REG_OMC0_D8, 0x7, 3, 0);
      // 100us settle
      schedule_timeout(usecs_to_jiffies(100));
      // Enable OMC thermal sensor controller
      gasket_read_modify_write_32(gasket_dev, APEX_BAR_INDEX,
                                  APEX_BAR2_REG_OMC0_DC, 0x1, 1, 0);
  }</code></pre>
<p>npu driver의 preparehardware 쪽에 추가하였다. </p>
<pre><code class="language-c">// windows 
    {
        PVOID bar2 = deviceContext-&gt;Bar2BaseAddress;
        apex_rmw_register_32(bar2, APEX_REG_OMC0_D0, 1, APEX_OMC_D0_CLK_EN_WIDTH, APEX_OMC_D0_CLK_EN_SHIFT);
        apex_rmw_register_32(bar2, APEX_REG_OMC0_D8, APEX_OMC_D8_SENSOR_EN_VAL, APEX_OMC_D8_SENSOR_EN_WIDTH, APEX_OMC_D8_SENSOR_EN_SHIFT);
        KeStallExecutionProcessor(APEX_OMC_SENSOR_SETTLE_US);
        apex_rmw_register_32(bar2, APEX_REG_OMC0_DC, 1, APEX_OMC_DC_CTRL_EN_WIDTH, APEX_OMC_DC_CTRL_EN_SHIFT);
        DbgPrint(&quot;[thermal] sensor enabled\n&quot;);
    }</code></pre>
<hr>
<h3 id="최종-로직">최종 로직</h3>
<pre><code class="language-c">// windows npu driver 
case IOCTL_GET_TEMPERATURE:
{
    PDEVICE_CONTEXT pDC = DeviceGetContext(device);
    PVOID bar2 = pDC-&gt;Bar2BaseAddress;

    if (bar2 == NULL) {
        status = STATUS_DEVICE_NOT_READY;
        break;
    }

    WDFMEMORY outMem;
    IOCTL_GET_TEMPERATURE_OUT* pOut = NULL;
    status = WdfRequestRetrieveOutputMemory(Request, &amp;outMem);
    if (!NT_SUCCESS(status)) break;
    pOut = (IOCTL_GET_TEMPERATURE_OUT*)WdfMemoryGetBuffer(outMem, NULL);
    if (pOut == NULL) { status = STATUS_INVALID_PARAMETER; break; }

    UINT32 reg = apex_read_register_32(bar2, APEX_REG_OMC0_DC);
    UINT32 adc = APEX_OMC_DC_ADC_FROM_REG(reg);
    pOut-&gt;raw_adc = adc;
    pOut-&gt;millic = apex_adc_to_millic(adc);
    DbgPrint(&quot;[thermal] reg=0x%x adc=%u millic=%d\n&quot;, reg, adc, pOut-&gt;millic);  // ← 임시

    bytesReturned = sizeof(IOCTL_GET_TEMPERATURE_OUT);
    status = STATUS_SUCCESS;

    break;
}</code></pre>
<p>32bit 레지스터 값을 읽는다. 읽은 값을 비트 연산하여 온도 값을 읽어준 뒤, 공식에 넣어서 user buffer space에 넘겨주는 코드이다. </p>
<hr>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/555a0ec5-c312-42f3-8e39-6e7031ff6c1b/image.png" alt=""></p>
<p>하단에 온도가 표시되도록 하였다. </p>
<p>추가로 데모 영상은 아래에서 확인할 수 있다. </p>
<iframe width="315" height="560" 
src="https://www.youtube.com/embed/FChT0XPICRc" 
title="YouTube video player" frameborder="0" 
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
allowfullscreen></iframe>

<hr>
<h3 id="결론">결론</h3>
<p>user space에서 hardware를 컨트롤하기 위해서 runtime을 통해 요청하고 driver에서는 실제 hardware의 register를 읽어서 다시 userspace로 보내주는 이 과정이 조금은 이해가 되는 것 같다. </p>
<p><a href="https://github.com/wangki-kyu/npu_driver">https://github.com/wangki-kyu/npu_driver</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver npu runtime dll 만들기 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-npu-runtime-dll-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@wang_ki/windows-driver-npu-runtime-dll-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 19 May 2026 09:09:52 GMT</pubDate>
            <description><![CDATA[<p>기존에는 console application에 mvp로 개발을 하였다. dll로 만들어서 여러 언어에서 사용할 수 있도록 개발할 예정이다. dll 개발 경험이 있어서 수월하게 만들 수 있었다. </p>
<hr>
<h3 id="기능">기능</h3>
<p>runtime dll의 가장 큰 역할은 <code>npu_driver</code>와의 통신을 래핑 하는 것이다. 즉 IOCTL 명령을 추상화하여 사용하도록 한다. 크게 3가지 기능이 필요할 것 같다.</p>
<ol>
<li>init(memory allocate)</li>
<li>infer (descriptor submit)</li>
<li>free (memory free) </li>
</ol>
<p>일단 동작을 하는지 확인하기 위해 만드는 것이므로 범용적이라기보다는 <code>SSD model</code>을 포커싱하여 만들었다. </p>
<hr>
<h3 id="기능-구현">기능 구현</h3>
<p>기존 코드를 거의 복사해서 붙여 넣었다. 헤더의 경우 아래와 같이 선언해 주었다. </p>
<pre><code class="language-c">// Initialize a runtime instance for the face SSD model.
//   model_path_utf8   : UTF-8 path to ssd_mobilenet_v2_face_*.tflite
//   anchors_bin_path_utf8 : UTF-8 path to anchors.bin (float32 [num_anchors][4])
//   out_handle        : on success, receives a non-NULL handle
NPU_API npu_status_t npu_runtime_init_face_ssd(
    const char*   model_path_utf8,
    const char*   anchors_bin_path_utf8,
    npu_handle_t* out_handle);

// Query model input dimensions (w, h, c). Bytes = w * h * c.
NPU_API npu_status_t npu_runtime_get_input_size(
    npu_handle_t h,
    uint32_t* out_w,
    uint32_t* out_h,
    uint32_t* out_c);

// Run one inference.
//   image_rgb       : RGB uint8 packed, must be exactly w*h*c bytes
//   image_byte_len  : length of image_rgb (sanity check)
//   out_dets        : caller-allocated array of `max` entries (may be NULL if max==0)
//   max             : capacity of out_dets
//   out_count       : actually written (always &lt;= max)
NPU_API npu_status_t npu_runtime_infer_image(
    npu_handle_t     h,
    const uint8_t*   image_rgb,
    uint32_t         image_byte_len,
    npu_detection_t* out_dets,
    uint32_t         max,
    uint32_t*        out_count);

// Release all resources held by the handle (chip buffers, device handle, COM).
// Safe to call with NULL.
NPU_API void npu_runtime_free(npu_handle_t h);</code></pre>
<p>정의 부분은 복잡하기 때문에 아래 github을 확인하면 된다.</p>
<hr>
<h3 id="실제-사용">실제 사용</h3>
<p>카메라를 사용하여 실시간으로 추론을 테스트하기 위해서 python을 선택했다. opencv를 손쉽게 사용할 수 있고 <code>ctypes</code>를 활용해 손쉽게 <code>dll</code>을 사용할 수 있기 때문이다. 또한 agent의 도움을 받아 빠르게 결과를 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/9dcd7c91-b9a1-4aa8-8dca-e7800d0eacf2/image.png" alt=""></p>
<p>정확히 얼굴을 찾는 것을 확인할 수 있다. 그러나 여기서 문제점이 발생했다. 특정 시점이 되면 추론에 실패하고 driver에서 에러를 뱉었다. </p>
<hr>
<h3 id="문제-해결">문제 해결</h3>
<pre><code>[DIE] infer failed at frame=128 infer_count=127
[DIE] uptime since loop start: 0.98s (last interval)
[DIE] error: infer_image failed (status=6): IOCTL_INFER_NEW failed, GetLastError=483
Traceback (most recent call last):
  File &quot;C:\work\real\npu_runtime_camera.py&quot;, line 198, in &lt;module&gt;
    sys.exit(main())
  File &quot;C:\work\real\npu_runtime_camera.py&quot;, line 140, in main
    dets = npu.infer(rgb, max_dets=50)
  File &quot;C:\work\real\npu_runtime_demo.py&quot;, line 115, in infer
    raise RuntimeError(f&quot;infer_image failed (status={st}): {self.last_error()}&quot;)</code></pre><p>위와 같은 에러 로그가 발생했다. 확인 결과 128프레임에 죽는 현상이 계속해서 발생했다. </p>
<p>한 프레임마다 infer를 하는데 tail이 증가하도록 되어있다. tail에 써주는 csr이 최대 0 ~ 255 까지만 받을 수 있도록 설계되어 있는 것을 확인했다. 코드에 </p>
<pre><code class="language-c">apex_write_register(bar2, APEX_REG_INSTR_QUEUE_TAIL, pDc-&gt;DescRingTail);</code></pre>
<p>한 번 제출 시, param cache와 infer descriptor를 2개씩 제출하기 때문에 2씩 증가한다. 따라서 <code>pDc-&gt;DescRingTail</code>을 <code>% 256</code> 연산을 해야 한다. </p>
<pre><code class="language-c">apex_write_register(bar2, APEX_REG_INSTR_QUEUE_TAIL, pDc-&gt;DescRingTail % 256);</code></pre>
<p>위처럼 코드를 변경하니 문제가 발생하지 않았다!!!</p>
<hr>
<h3 id="결론">결론</h3>
<p>npu driver가 잘 동작한다!! 너무 기쁘다. </p>
<p>수정 및 개선해야 할 부분이 많다. <code>anchor</code> 및 <code>quant params</code> 값들을 하드 코딩 형태로 사용하고 있는 점도 수정해야 한다. 또한 <code>byte-identical</code>을 위해서 넣어두었던 로깅 코드들도 제거해야 하고 <code>param cache descriptor</code>도 infer 마다 하기 때문에 최초에 한번 하도록 수정해야 한다. </p>
<p><a href="https://github.com/wangki-kyu/npu_driver/tree/main/npu_runtime">https://github.com/wangki-kyu/npu_driver/tree/main/npu_runtime</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver npu convert_scores 버그가 아니었다]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-npu-convertscores-%EB%B2%84%EA%B7%B8%EA%B0%80-%EC%95%84%EB%8B%88%EC%97%88%EB%8B%A4</link>
            <guid>https://velog.io/@wang_ki/windows-driver-npu-convertscores-%EB%B2%84%EA%B7%B8%EA%B0%80-%EC%95%84%EB%8B%88%EC%97%88%EB%8B%A4</guid>
            <pubDate>Mon, 18 May 2026 15:53:32 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅에서 추론 버그라고 했던 convert_scores의 충격적인 사실을 발견했다. 
아무리 <code>Byte-identical</code>로 바이트 단위로 정말 많은 <code>CSR</code>을 비교했다. input data의 CRC32를 비교했더니 틀렸다. 동작하는 libedgetpu의 input data를 binary file로 뽑아내서 npu driver에서 로드하는 방식을 사용하기도 했다. </p>
<p>그렇다면 어떻게 convert_scores 값이 쓰레기 값이 아닌지 알 수 있었던 내용을 작성해 보겠다. </p>
<hr>
<h3 id="libedgetpu의-output-buffer">libedgetpu의 output buffer</h3>
<p>당연히 추론 이후의 output buffer의 <code>Byte-identical</code>을 진행해서 다른 내용을 확인했기에 개발 중인 npu driver의 output buffer에 쓰레기 값이 들어왔을 것이라고 판단한 것이다.</p>
<p>테스트를 하기 위해 가설을 세웠다. </p>
<blockquote>
<p><em>혹시 exe0의 param data의 값이 libedgetpu에서 wrong 값이 들어간다면?</em></p>
</blockquote>
<p>테스트를 위해서 exe0의 param 값에 <code>0xcc</code>값을 채워서 빌드 후 테스트했다. 결과가 너무 충격적이라서 소리를 질렀다. </p>
<p>우리의 convert_scores와 비슷한 패턴으로 나오는 것이었다. 뭐지 싶었다. 
<em>그러면 우리가 넣어주는 exe0의 param 값이 정상적으로 안 들어가는 건가?</em>라는 생각을 했다. </p>
<p>좀 이상해서 <code>Squeeze1</code> 값도 비교해 보기 위해서 dump를 찍어서 확인했더니 동일한 것이다!! 그러면 우리가 비교했던 값은 무엇인가?라는 생각이 들었다. </p>
<hr>
<h3 id="convert-scores는-잘못이-없었다">convert scores는 잘못이 없었다.</h3>
<p>libedgetpu로 로그만 추가한 상태로 추론을 해보았다. 정상적으로 동작이 되었고 로그를 확인하니 여전히 우리와 같은 값이 나오는 것이었다. </p>
<p><em>그러면 npu driver가 정상적으로 추론 값을 받고 있는 건가?</em> 라는 생각이 들었다. 
동시에 <em>Squeeze1과 convert_scores 값을 해석하는 방식이 잘못된 건가?</em> 라는 생각을 했다. </p>
<blockquote>
<p><em>그러면 convert_scores 값은 유의미한 값이고 full dump를 찍어보자</em></p>
</blockquote>
<pre><code>  [uniform] convert_scores: 2025 / 2034 anchors match &#39;ff 00 80 80&#39;
  [diff-anchor] idx=143  : ff 01 80 80
  [diff-anchor] idx=146  : ff 01 80 80
  [diff-anchor] idx=200  : fb 05 80 80
  [diff-anchor] idx=203  : 01 ff 80 80   ← ★
  [diff-anchor] idx=206  : 02 fe 80 80   ← ★
  [diff-anchor] idx=263  : 80 80 80 80
  [diff-anchor] idx=266  : 80 80 80 80
  [diff-anchor] idx=1278 : f6 0a 80 80
  [diff-anchor] idx=1284 : f6 0a 80 80</code></pre><p>full dump를 찍었더니 4byte 씩 패턴을 보였고 다른 것들이 발견되었다. 
0xff / 256 = 0.996은 pycoral의 detection score 0.9961과 byte-identical 하다는 것을 발견했다. 즉 head 256 byte만 보고 <em>다 균일이다 =  망가졌다</em>라고 잘못 읽었던 것이다. 2034 anchor 중 9 개만 의미 있는 값을 갖고 있었고, 그 9개가 head 256 byte 안에 없었던 것뿐이다. </p>
<p>chip은 처음부터 정확한 답을 보내고 있었다.</p>
<hr>
<h3 id="그러면-box-좌표는-어디서-오는가">그러면 box 좌표는 어디서 오는가</h3>
<p>score는 byte 자체에서 풀렸다. 그런데 detection 결과의 box 같은 좌표는 chip output 어디에도 명시되어 있지 않다. <code>Squeeze1</code>의 byte를 dequant 해도 그냥 4개의 float일뿐 픽셀이 안 나온다. </p>
<p>anchor 0 의 Squeeze1: 94 88 73 4b
dequant: dy=+0.43, dx=-0.87, dh=-3.14, dw=-7.47 이게 픽셀이 아니면 어떤 보정값 같은데, 출발점이 되는 좌표가 chip output에 없다. </p>
<blockquote>
<p><em>그러면 box 좌표 만들기에 chip output 외에 더 필요한 정보가 있는 거 아닌가?</em></p>
</blockquote>
<hr>
<h3 id="tflite-graph-까보기">tflite graph 까보기</h3>
<p>chip 다음에 어떤 op가 실행되는지 알아야 했다. tflite schema.fbs 를 받아서 flatc로 모델을 JSON으로 변환한 후, op chain만 추출하는 <code>extract_op_chain.py</code> 를 작성해서 돌렸다.</p>
<pre><code>  op[0] &#39;edgetpu-custom-op&#39;
     OUT: 2(Squeeze1), 4(convert_scores)
  op[1] &#39;DEQUANTIZE&#39;
     IN : 2(Squeeze1)         OUT: 3(Squeeze2)
  op[2] &#39;DEQUANTIZE&#39;
     IN : 4(convert_scores)   OUT: 5(convert_scores1)
  op[3] &#39;TFLite_Detection_PostProcess&#39;
     IN : 3(Squeeze2), 5(convert_scores1), 1(anchors)   ← ★
     OUT: 6, 7, 8, 9</code></pre><p>tflite 모델 파일(.tflite)은 일종의 <code>연산 설계도(Graph)</code>인데, 이걸 JSON으로 변환해서 열어보면 모델 내부에서 데이터가 어떤 순서로 흐르고, 어떤 <code>Op</code>들이 체인처럼 엮여서 실행되는지 그 지도를 100% 명확하게 까볼 수 있다. </p>
<ul>
<li>0단계: PCIe 가속기 칩이 담당한 연산</li>
<li>1단계 &amp; 2단계: 칩이 내뱉는 무식한 바이트 데이터들을 cpu가 알아들을 수 있게 부동소수점으로 변환하는 과정</li>
<li>3단계: 대망의 최종 후처리 단계 </li>
</ul>
<p>사실 tflite model에 대해서 깊이 있게 아는 것이 아니라 정확히는 모르겠다. 하지만 찾아본 결과 2,034개의 <code>anchor</code> 박스마다 각각 4개(x,yu,w,h)의 기본 규격 좌표를 미리 기억하고 있는 고정된 데이터 테이블이 있다고 한다. <code>buf=1</code>의 의미는 모델 안에 const data가 들어있다는 의미라고 보면 된다. </p>
<p>즉, 최종 얼굴 사각형 박스는 칩이 주는 게 아니라, 모델 파일에 적혀 있던 2,034개의 고정 앵커 좌표에다가 칩이 실시간으로 뱉어낸 미세 조정값(Squeeze1)을 수학적으로 합성해서 cpu가 만들어내고 있었던 것이다!!!!</p>
<hr>
<h3 id="anchors와-quant-params-추출">anchors와 quant params 추출</h3>
<p>standalone으로 우리가 만든 c++ 모듈에서 동작시키기 위해서는 3개의 const 값이 필요하다. </p>
<ol>
<li>anchors의 실제 byte (2034 * 4 float32 = 32544 byte) </li>
<li>Squeeze1의 quant params (scale, zero_point)</li>
<li>convert_scores의 quant params </li>
</ol>
<p><code>extract_postproc_deps.py</code>를 ai 도움을 받아 작성하여 한 번에 추출했다. </p>
<pre><code># 결과 
 anchors.bin               32544 bytes float32
 Squeeze1       scale=0.10822763  zp=144
 convert_scores scale=0.00390625  zp=0
 anchor[203]    = [yc=0.175, xc=0.375, h=0.2828, w=0.1414]</code></pre><p>anchor[203] 좌표를 픽셀로 풀면 (yc=56, xc=120, h=90, w=45) 의 세로 직사각형 — karina 얼굴 근처에 미리 정의된 prior box.</p>
<hr>
<h3 id="anchor--squeeze1의-합성">anchor + Squeeze1의 합성</h3>
<p>표준 SSD MobileNet 의 box decode 공식이 있다. 이걸 그대로 사용했다. </p>
<pre><code>  ycenter = anchor.yc + (dy / 10.0f) * anchor.h;
  xcenter = anchor.xc + (dx / 10.0f) * anchor.w;
  h       = exp(dh / 5.0f) * anchor.h;
  w       = exp(dw / 5.0f) * anchor.w;

  out[0] = ycenter - h / 2;   // ymin
  out[1] = xcenter - w / 2;   // xmin
  out[2] = ycenter + h / 2;   // ymax
  out[3] = xcenter + w / 2;   // xmax</code></pre><p>이 공식의 수학적 의미를 깊게 알지는 못한다. 그냥 <em>anchor의 좌표에서 chip이 보낸 보정값만큼 조정해서 진짜 box를 만든다</em> 정도로 이해하고 사용했다. 아직 공부할게 너무 많다..</p>
<p>계산 결과로는 다음과 같다.</p>
<pre><code>  ycenter = 0.175 + 0.022 * 0.2828 = 0.181
  xcenter = 0.375 + 0.130 * 0.1414 = 0.393
  h       = exp(-0.108) * 0.2828   = 0.254
  w       = exp(0.433)  * 0.1414   = 0.218

  box (normalized) : (0.054, 0.284) - (0.308, 0.502)
  box (320x320 px) : (90, 17) - (160, 98)</code></pre><hr>
<h3 id="opencv로-시각화">opencv로 시각화</h3>
<p>driver가 만든 box는 320*320 model space의 normalized 좌표이다. 원본 이미지에 그리려면 추론 시 letterbox 전처리를 역으로 풀어야 한다. </p>
<blockquote>
<p>scale = min(320 / srcW, 320 / srcH)
  newW = int(srcW * scale)
  newH = int(srcH * scale)
  x0 = int(round(max(0, min(d.xmin * 320, newW)) / scale))
  y0 = int(round(max(0, min(d.ymin * 320, newH)) / scale))
  ...
  cv2.rectangle(img, (x0, y0), (x1, y1), (0, 0, 255), 2)</p>
</blockquote>
<hr>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/fc6a90d6-3355-41eb-a4b4-fb5ecb7f939e/image.png" alt=""></p>
<p>눈물이 났다.</p>
<hr>
<h3 id="결론">결론</h3>
<p>4월 말부터 이 프로젝트를 했으니 약 3주가 넘게 흘렀다. 처음 해보는 영역이라서 너무 어려웠고 방향성을 몰랐지만 그래도 첫 번째 목표인 추론에 성공하였다. </p>
<p>지금은 특정 model에 대해서 의존성이 강하게 만들었는데 범용으로 사용할 수 있도록 runtime을 만들 예정이다. 또한 ai agent의 도움을 많이 받았는데 모르는 부분을 공부로 채워나가야겠다. </p>
<p><a href="https://github.com/wangki-kyu/npu_driver">https://github.com/wangki-kyu/npu_driver</a>
<a href="https://github.com/wangki-kyu/libedgetpu">https://github.com/wangki-kyu/libedgetpu</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows npu driver descriptor ring / status block 을 dma-coherent 메모리로 교체]]></title>
            <link>https://velog.io/@wang_ki/windows-npu-driver-descriptor-ring-status-block-%EC%9D%84-dma-coherent-%EB%A9%94%EB%AA%A8%EB%A6%AC%EB%A1%9C-%EA%B5%90%EC%B2%B4</link>
            <guid>https://velog.io/@wang_ki/windows-npu-driver-descriptor-ring-status-block-%EC%9D%84-dma-coherent-%EB%A9%94%EB%AA%A8%EB%A6%AC%EB%A1%9C-%EA%B5%90%EC%B2%B4</guid>
            <pubDate>Sun, 17 May 2026 15:46:22 GMT</pubDate>
            <description><![CDATA[<p>convert scores가 계속 쓰레기로 나오는 문제, 이번에는 input도 아니고 weight도 아닌 descriptor ring / status block의 cache coherency 를 의심해봤다. <code>libedgetpu</code> (정상 동작)와 우리 <code>driver</code> (비정상)의 메모리 할당 방식이 다른 걸 발견했고, 우리쪽을 <code>libedgetpu</code> 쪽에 맞춰 봤다. </p>
<hr>
<h3 id="libedgetpu-가-queue--status-block-메모리-잡는-방식">libedgetpu 가 queue / status block 메모리 잡는 방식</h3>
<p>먼저 정상 동작하는 libedgetpu가 instruction queue ring 과 status block 을 어떻게 잡는지 확인했다.소스 경로는 <code>driver/kernel/kernel_coherent_allocator.cc</code> 와 windows 전용 <code>driver/kernel/windows/kernel_coherent_allocator_windows.cc</code> 을 참고하면 된다. </p>
<pre><code class="language-c"> // kernel_coherent_allocator.cc:42~
  ioctl_buffer.enable = 1;
  ioctl_buffer.size   = size_bytes;   // 16KB (queue 4KB + status 4KB + padding)
  ioctl(fd, GASKET_IOCTL_CONFIG_COHERENT_ALLOCATOR, &amp;ioctl_buffer);
  dma_address_ = ioctl_buffer.dma_address;   // ← coral.sys 가 dma_alloc_coherent 로 받은 PA

  // windows 전용
  apex_memmap_ioctl.dev_dma_addr = dma_address;
  ioctl(fd, GASKET_IOCTL_MAP_UMDMA_VIEW, &amp;apex_memmap_ioctl);
  return (char*)apex_memmap_ioctl.virtaddr;   // user-mode UC view</code></pre>
<p>요약하면 이렇다</p>
<ul>
<li>libedgetpu user-mode는 coral.sys 한테 IOCTL 로 위임</li>
<li>coral.sys (kernel) 가 DMA-coherent (uncached) 메모리를 잡음 (linux의 dma_alloc_coherent 등가)</li>
<li>잡은 메모리를 user-mode 에 UC view로 mmap 해서 돌려줌 </li>
</ul>
<p>chip이 ring descriptor를 fetch 하거나 status block에 DMA write 할 때, CPU cache 와 sync 가 hardware 차원에서 보장 된다.</p>
<hr>
<h3 id="우리-driver의-문제">우리 driver의 문제</h3>
<p>반면 우리 npu driver의 <code>Device.c</code>를 보면 두 군데가 그냥 cacheable pool 로 잡혀 있었다. </p>
<pre><code class="language-c">  // Device.c:593 (변경 전)
  deviceContext-&gt;DescRingBase = ExAllocatePoolWithTag(NonPagedPoolNx, PAGE_SIZE, &#39;DRNG&#39;);

  // Device.c:620 (변경 전)
  deviceContext-&gt;StatusBlockBase = ExAllocatePoolWithTag(NonPagedPoolNx, PAGE_SIZE, &#39;SBLK&#39;);</code></pre>
<p><code>NonPagedPoolNx</code>는 nonpaged (page-out 안됨) 이긴 한데 cache attribute 가 cacheable 이다. PCIe device가 직접 DMA write 한 데이터를 CPU가 cache 에서 stale 값 읽을 가능성이 있다. </p>
<p>가설은 두 가지이다. </p>
<ol>
<li>status block stale read - chip 이 inference 끝나고 completed_head_pointer를 status block 에 write 해도, 우리 driver가 cache 에서 stale 0을 읽어서 completion 감지 못함</li>
<li>descriptor ring stale fetch - 우리가 cache 에만 descriptor 16b 를 write 하고 PCIe로 chip이 PA fetch 했을 때 stale garbage 를 가져감 -&gt; 잘못된 DMA </li>
</ol>
<p>증상이랑 부합한다. </p>
<hr>
<h3 id="수정---alloc--free-교체">수정 - alloc / free 교체</h3>
<pre><code class="language-c"> 1. DescRing 할당

  // Device.c:593 (변경 후)
  {
      PHYSICAL_ADDRESS lo, hi, none;
      lo.QuadPart   = 0;
      hi.QuadPart   = 0xFFFFFFFFLL;   // &lt; 4GB. chip MMU 32-bit PA 제약
      none.QuadPart = 0;
      deviceContext-&gt;DescRingBase = MmAllocateContiguousMemorySpecifyCache(
          PAGE_SIZE, lo, hi, none, MmNonCached);
  }
  if (deviceContext-&gt;DescRingBase == NULL) {
      DbgPrint(&quot;[%s] Failed to allocate descriptor ring\n&quot;, __FUNCTION__);
      return STATUS_INSUFFICIENT_RESOURCES;
  }
  RtlZeroMemory(deviceContext-&gt;DescRingBase, PAGE_SIZE);

  hi.QuadPart = 0xFFFFFFFFLL 가 중요하다. chip MMU 가 32-bit PA 만 받기 때문에 4GB 미만 영역에서 잡아야 한다.

  2. StatusBlock 할당

  // Device.c:620 (변경 후)
  {
      PHYSICAL_ADDRESS lo, hi, none;
      lo.QuadPart   = 0;
      hi.QuadPart   = 0xFFFFFFFFLL;
      none.QuadPart = 0;
      deviceContext-&gt;StatusBlockBase = MmAllocateContiguousMemorySpecifyCache(
          PAGE_SIZE, lo, hi, none, MmNonCached);
  }
  if (deviceContext-&gt;StatusBlockBase == NULL) {
      DbgPrint(&quot;[%s] Failed to allocate status block\n&quot;, __FUNCTION__);
      MmFreeContiguousMemory(deviceContext-&gt;DescRingBase);   // ← 이것도 교체
      deviceContext-&gt;DescRingBase = NULL;
      return STATUS_INSUFFICIENT_RESOURCES;
  }

  3. Cleanup path

  ExFreePoolWithTag 였던 free 도 전부 MmFreeContiguousMemory 로 교체. tag 인자가 없어서 더 깔끔하다.

  // Device.c:1149 (변경 후)
  if (deviceContext-&gt;DescRingBase != NULL) {
      MmFreeContiguousMemory(deviceContext-&gt;DescRingBase);
      deviceContext-&gt;DescRingBase = NULL;
  }
  if (deviceContext-&gt;StatusBlockBase != NULL) {
      MmFreeContiguousMemory(deviceContext-&gt;StatusBlockBase);
      deviceContext-&gt;StatusBlockBase = NULL;
  }</code></pre>
<hr>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/7244fed3-ce99-44d5-96ae-950b39bc1d0d/image.png" alt=""></p>
<p>여전히 쓰레기 값이 나온다..
또 다음 가설을 설정 후, 테스트해야겠다..
화이팅</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver npu input data crc 검증 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-npu-input-data-crc-%EA%B2%80%EC%A6%9D</link>
            <guid>https://velog.io/@wang_ki/windows-driver-npu-input-data-crc-%EA%B2%80%EC%A6%9D</guid>
            <pubDate>Fri, 15 May 2026 16:02:28 GMT</pubDate>
            <description><![CDATA[<p>추론 output 데이터 중 <code>convert_scores</code>가 계속 잘못된 값으로 나와서 상당히 쉽지 않다. 이번 포스팅에서는 정상으로 추론되는 <code>libedgetpu</code>에서 실제 input buffer를 <code>bin</code>파일로 만들어서 만들어진 파일을 개발 중인 npu driver에서 사용하도록 할 예정이다. 우리 코드가 만드는 input data와 <code>libedgetpu</code>가 만드는 input data의 crc 달라서 추론에 문제가 있는지 검증하겠다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/b51ce3df-451e-4d2a-a342-751d445412c6/image.png" alt=""></p>
<hr>
<h3 id="libedgetpu-코드-수정-및-빌드">libedgetpu 코드 수정 및 빌드</h3>
<p><a href="https://github.com/wangki-kyu?tab=repositories">https://github.com/wangki-kyu?tab=repositories</a> 해당 github에 fork로 windows build가 성공되도록 수정해 놓았다. 원하는 로그 및 코드를 수정해서 테스트할 수 있는 환경이다.</p>
<p><code>libedgetpu</code>에 코드를 추가한 뒤, docker를 통해 빌드를 했다. 명령어는 아래와 같다. </p>
<blockquote>
<p>docker run --rm -v &quot;E:/work/project/libedgetpu:C:/workspace&quot; -w C:/workspace libedgetpu-windows:latest cmd /c build.bat</p>
</blockquote>
<p>약 3~5분 정도 기다리면 빌드가 완료된다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/74f0308b-9c68-4e41-b07f-cdcdecf31699/image.png" alt=""></p>
<p>빌드된 결과물을 사용 환경으로 옮겨주는 작업을 해주면 된다. </p>
<hr>
<h3 id="input-bin-생성하기">input bin 생성하기</h3>
<p>간단하게 coral.sys를 등록하고 추론을 돌려주면 된다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/d1396ede-e9ab-44c0-a8ea-925c320abfc0/image.png" alt=""></p>
<p>현재 개발 중인 npu driver가 설치되어 있는데 이 부분을 <code>google</code>에서 제공해 주는 <code>coral.sys</code>로 업데이트 해주면 된다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/00db5d2e-f7c5-4cfd-bfe0-828f09bdb6ca/image.png" alt=""></p>
<p>제거 후 업데이트하여 정상적으로 설치된 것을 확인할 수 있다. </p>
<p>python으로 미리 만들어놓은 코드를 사용해 얼굴 인식 추론을 하면 된다. 카리나 사진으로 테스트를 해보았다. 테스트 결과는 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/1d8b809e-130d-47f6-8af6-6fbdb1d5c88c/image.png" alt=""></p>
<p>정상적으로 얼굴이 인식이 되었고 opencv를 활용해 인식된 Box 좌표로 얼굴 부분을 강조하도록 했다. </p>
<p>추가로 <code>C:\temp\</code>하위에 <code>input bin</code>파일이 생성된 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/71d4aab6-160c-4d1c-a947-7403d8f759f7/image.png" alt=""></p>
<hr>
<h3 id="input-bin-파일-읽어서-사용하기">input bin 파일 읽어서 사용하기</h3>
<p>기존 코드에 아래와 같이 define을 통해 분기 처리했다.</p>
<pre><code class="language-c">    #define USE_LIBEDGETPU_INPUT 1
    {
        UINT imgSide = static_cast&lt;UINT&gt;(std::sqrt(static_cast&lt;double&gt;(INPUT_SIZE) / 3.0));
        if (imgSide * imgSide * 3 != INPUT_SIZE) {
            std::cout &lt;&lt; &quot;[main] WARNING: input size &quot; &lt;&lt; INPUT_SIZE
                      &lt;&lt; &quot; is not square*3 — falling back to imgSide=&quot; &lt;&lt; imgSide &lt;&lt; std::endl;
        }

#if USE_LIBEDGETPU_INPUT
        {
            const char* libe_input_path = &quot;C:\\temp\\libe_input_raw.bin&quot;;
            FILE* f = nullptr;
            fopen_s(&amp;f, libe_input_path, &quot;rb&quot;);
            if (f) {
                size_t r = fread(pInputBuf, 1, INPUT_SIZE, f);
                fclose(f);
                std::cout &lt;&lt; &quot;[main] USE_LIBEDGETPU_INPUT=1 — loaded &quot; &lt;&lt; r &lt;&lt; &quot;/&quot;
                          &lt;&lt; INPUT_SIZE &lt;&lt; &quot; bytes from &quot; &lt;&lt; libe_input_path &lt;&lt; std::endl;
                if (r != INPUT_SIZE) {
                    std::cout &lt;&lt; &quot;[main] FAIL: libe input file size mismatch — expected &quot;
                              &lt;&lt; INPUT_SIZE &lt;&lt; &quot; B&quot; &lt;&lt; std::endl;
                    goto cleanup;
                }
            } else {
                std::cout &lt;&lt; &quot;[main] FAIL: &quot; &lt;&lt; libe_input_path
                          &lt;&lt; &quot; not found — run libedgetpu first to generate it&quot; &lt;&lt; std::endl;
                goto cleanup;
            }
        }
#else
        std::cout &lt;&lt; &quot;[main] loading image (target &quot; &lt;&lt; imgSide &lt;&lt; &quot;x&quot; &lt;&lt; imgSide &lt;&lt; &quot;)&quot; &lt;&lt; std::endl;
        if (!LoadJpegToRGB(L&quot;.\\assets\\karina.jpg&quot;, pInputBuf, imgSide, imgSide)) {
            std::cout &lt;&lt; &quot;[main] FAIL: image load&quot; &lt;&lt; std::endl;
            goto cleanup;
        }
#endif
    }</code></pre>
<p>빌드 후, 테스트를 진행하였다. </p>
<p>CRC 값은 <code>0xf993d860</code>로 동일하게 나왔는데 여전히 <code>convert_scores</code>값이 쓰레기 값이 나왔다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>실제 추론이 성공하는 input data를 사용해서도 추론이 안된 것을 보면 npu driver에서 정상적으로 <code>Weight(가중치)</code> 값을 사용하지 못하거나, 추론이 정상적으로 발생되지 않는 것으로 볼 수 있다. </p>
<p>아쉽지만 좋은 시도였다..</p>
<p>다른 방법도 시도해 봐야겠다....</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows npu driver infer input problem]]></title>
            <link>https://velog.io/@wang_ki/windows-npu-driver-infer-input-problem</link>
            <guid>https://velog.io/@wang_ki/windows-npu-driver-infer-input-problem</guid>
            <pubDate>Thu, 14 May 2026 15:31:17 GMT</pubDate>
            <description><![CDATA[<p><code>libedgetpu</code>의 face detection은 정상이다. 직접 만든 npu driver로는 모든 anchor score가 균일하게 나오는 버그가 있다. 이번에는 input dump를 통해서 비교해 볼 예정이다. </p>
<hr>
<h3 id="분석하기">분석하기</h3>
<p>분석하기 전에 아래와 같은 내용을 알아야 하는 것 같다. </p>
<table>
<thead>
<tr>
<th align="left">구성 요소</th>
<th align="left">역할</th>
<th align="left">상세 내용</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>CRC32</strong></td>
<td align="left"><strong>무결성 검증</strong></td>
<td align="left">데이터 전송 중 에러 발생 여부를 체크하는 32비트 체크섬 값</td>
</tr>
<tr>
<td align="left"><strong>HEAD</strong></td>
<td align="left"><strong>메타데이터</strong></td>
<td align="left">텐서의 Shape, Dtype, 레이아웃 등 데이터 해석 정보</td>
</tr>
<tr>
<td align="left"><strong>MID</strong></td>
<td align="left"><strong>실제 데이터</strong></td>
<td align="left">연산에 직접 사용되는 본문(Payload) 데이터</td>
</tr>
<tr>
<td align="left"><strong>TAIL</strong></td>
<td align="left"><strong>마무리/패딩</strong></td>
<td align="left">메모리 정렬(Alignment)을 위한 여백 및 종료 지점 정보</td>
</tr>
</tbody></table>
<p>정상 동작하는 코드와 npu driver 코드에 input dump 출력 코드를 작성하고 돌려서 비교했다. </p>
<blockquote>
<p>libedgetpu  CRC32=0xf993d860  TAIL: 00 00 00 00 00 00 00 ...
npu_driver  CRC32=0x68ea0706  TAIL: 34 3c 35 34 3c 35 34 ...</p>
</blockquote>
<p>HEAD / MID 는 일치하는데 TAIL만 달랐다. </p>
<p><code>libedgetpu</code>의 경우는 letterbox 방식이고 npu_driver에서는 stretch 방식의 resize를 하는 것 같다. SSD backbone은 receptive field가 전체 이미지라서 입력이 조금만 달라도 score head 전체가 흔들린다고 한다. 따라서 정상적으로 추론이 안될 수 있다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>코드를 수정하여 동일한 포맷으로 만들었지만, 여전히 정확한 추론이 되지 않는다.
하나씩 다른 점을 찾아보면서 가설을 제거해 나가야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver npu infer garbage output ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-npu-infer-garbage-output</link>
            <guid>https://velog.io/@wang_ki/windows-driver-npu-infer-garbage-output</guid>
            <pubDate>Thu, 14 May 2026 13:40:45 GMT</pubDate>
            <description><![CDATA[<p>개발을 진행하며 문제가 발생했다. param cache와 infer descriptor를 정상적으로 submit 한다고 생각했는데 기존 <code>libedgetpu</code>처럼 정상적으로 추론이 되지 않고 garbage 값이 나오는 현상이 있다. </p>
<hr>
<h3 id="추론-값">추론 값</h3>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/d3383411-dbcc-4084-bff3-72c247fa92c7/image.png" alt=""></p>
<p>위 사진은 추론 값이다. 이전에 발생했던 infeed fault 나 outfeed fault같은 에러가 발생하지 않았고 정상적으로 chip이 dma 방식으로 output buffer에 값을 적어준 것 을 확인할 수 있다. </p>
<p>일단 <code>Squeeze1</code>과 <code>convert_scores</code>는 <code>SSD(SingleShot Multibox Dector</code>계열의 객체 인식 모델에서 포스트 프로세싱 단계 직전의 raw data 상태일 때의 이름이다. </p>
<ol>
<li>Sqeeuze1 (Bounding Box Locations)
모델이 예측한 각 Anchor Box들이 실제 객체의 위치에 맞게 수정된 좌표값이다. </li>
<li>convert_scores (Classification Scores)
이 텐서는 각 후보 박스가 특정 클래스(사물 종류)일 확률을 담고 있다. </li>
</ol>
<hr>
<h3 id="가설-검증">가설 검증</h3>
<p>param data를 chip이 읽지 않는지 검증하기 위해서 pte에 mapping된 param buffer를 0으로 채운 뒤, 추론을 돌려보았다. </p>
<pre><code class="language-c">memset(pParamData, 0x00, model.parameters.size());          // weight 를 전부 0 으로
memset(pExe0ParamData, 0x00, model.exe0_parameters.size()); // exe0 보조도 0</code></pre>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/67a19283-5eca-4c10-85ff-6f9ba522ed80/image.png" alt=""></p>
<p>output buffer의 값이 변경된 것을 확인할 수 있다. 결론적으로 chip이 cache된 parameters 값을 참조하고 있다고 생각할 수 있다.</p>
<hr>
<h3 id="sqeeuze1-정상인지-검사">Sqeeuze1 정상인지 검사</h3>
<p>python으로 해당 raw 데이터를 box 좌표로 변환해서 실제로 동일한 이미지 사진에 정상적으로 그려지는지 검증을 해보려고 한다. 하나씩 변수를 제거해나가는 부분이 이 작업의 매력인 것 같다. <code>claude code</code>를 적극적으로 사용하고 있다. </p>
<p>테스트 결과 아래와 같이 나왔다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/e4cb0dd9-5732-42ea-8dec-6b0b815cd592/image.png" alt=""></p>
<p>현재 <code>Squeeze1</code>값도 비정상이라는 것을 확인할 수 있다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>chip이 비정상적인 추론 결과를 output buffer에 넣어준 거라고 봐야한다. 무엇이 잘못되었는지 좀 더 확인해 봐야겠다...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver npu 진행상황]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-npu-%EC%A7%84%ED%96%89%EC%82%AC%ED%99%A9-1</link>
            <guid>https://velog.io/@wang_ki/windows-driver-npu-%EC%A7%84%ED%96%89%EC%82%AC%ED%99%A9-1</guid>
            <pubDate>Wed, 13 May 2026 03:05:55 GMT</pubDate>
            <description><![CDATA[<p>windows driver를 계속해서 개발을 하고 있는데, 유의미한 추론 값이 나오고 나서 블로그를 정리해야지 하고 있었다. 너무나 어렵고 생각보다 프로젝트가 길어지고 있다. 그래서 지금부터는 만나는 문제에 대해서 조금씩 포스팅을 해보려고 한다.</p>
<hr>
<h3 id="진행-상황">진행 상황</h3>
<p>전체적인 플로우는 완성이 된 것 같다. 대략적인 작업 단계는 다음과 같다. </p>
<ul>
<li>contiguous allocate memory </li>
<li>input, output 등 필요한 메모리 할당 및 memcpy</li>
<li>tflite model bitstream patch</li>
<li>coral tpu pte mapping </li>
<li>필요한 csr 초기화 및 세팅 </li>
<li>param cache descriptor 제출 </li>
<li>infer bitstream descriptor 제출 </li>
<li>descriptor 제출에 대한 interrupt 처리, isr, dpc </li>
<li>추론 데이터 확인하기 output buffer</li>
</ul>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/dde0ca21-fcf1-4a1b-a176-6fbfbe879692/image.png" alt=""></p>
<p>중간중간에 더 많은 로직들이 필요하다. 오픈소스인 <code>libedgetpu</code>와 linux driver인 <code>gasket</code>을 참고를 많이 했다. 그리고 실제 windows driver인 <code>coral.sys</code>를 디컴파일링하여 ai agent에게 분석을 요청해 유의미한 값을 찾아내기도 하였다. </p>
<p><a href="https://github.com/google-coral/libedgetpu">https://github.com/google-coral/libedgetpu</a>
<a href="https://github.com/google/gasket-driver">https://github.com/google/gasket-driver</a></p>
<p>특히 libedgetpu의 소스를 수정하여 필요한 로그를 찍어서 확인해 보고 싶은 상황이 있었다. 그러나 해당 소스는 관리를 하지 않는 상태였다.
<img src="https://velog.velcdn.com/images/wang_ki/post/fec06a37-9e3e-4c62-b046-14486d39404e/image.png" alt="">
따라서 <code>README</code>에 나온 방식으로 빌드가 되지 않았다. 따라서 fork를 떠서 필요한 내용을 수정해서 빌드를 진행했고, 그 과정에서 docker windows container, docker build, bazel 등 여러 도구를 활용하여 빌드에 성공했고 로그를 남길 수 있었다. 해당 내용도 포스팅을 할 예정이다. <a href="">libedgetpu docker + bazel build</a></p>
<hr>
<h3 id="이슈">이슈</h3>
<h4 id="bitstream-align-6bit">bitstream align 6bit</h4>
<p>가장 크게 막혔던 부분은 bitstream align 문제였다. 이것을 해결하기 위해 일주일은 넘는 시간이 소요되었다. <code>libedgetpu</code>에서 bitstream을 patch한 모습과 개발 중인 <code>npu_driver</code>에서 bitstream을 patch 한 모습을 비교하기 위해 full dump를 로깅하였고 유의미한 차이점을 발견하였다. <code>libedgetpu</code>에서는 6bit를 남겨두고 오프셋 위치에 정확히 patch를 하는데 우리 소스에서는 6bit 부분을 오버라이트하여 패치하는 문제가 있었던 것이다. 이 부분을 해결하니 output va로 값이 쓰여지지 않는 문제만 빼고는 해결이 되었다. <a href="">bitstream align 문제 해결</a></p>
<h4 id="descriptor-struct-size-issue">Descriptor struct size issue</h4>
<p>가속기(하드웨어)에게 일을 시키기 위해서는 descriptor를 제출해야 한다. </p>
<pre><code class="language-c">typedef struct {
                UINT64 address;
                UINT64 size_in_bytes;
                UINT32 reserved;
            } HOST_QUEUE_DESC;</code></pre>
<p>위처럼 사용하고 있었는데 <code>libedgetpu</code> 소스에는 <code>size_in_bytes</code>의 타입이 <code>UINT64</code>가 아닌 <code>UINT32</code>여서 변경해 주었다. </p>
<p>그 외에도 많은 이슈가 있었다. 관련 내용들은 포스팅을 할 예정이다. </p>
<hr>
<h3 id="현재-상황">현재 상황</h3>
<p>간단하게 로그를 보면 정확한 추론 값을 얻지 못하는 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/8630599d-06f1-47d6-8839-29c41bf530bc/image.png" alt=""></p>
<p>얼굴 인식 모델이므로 박스 좌표 값을 얻어와야 한다. 위 내용을 해결하기 위해서 디버깅 및 시간을 투자해야 할 것 같다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>완벽하지는 않지만 그래도 많은 진척이 있는 것 같다. 완벽히 이해를 했다기보다는 <em>이제 좀 driver에 친숙해졌다?</em> 라는 느낌이다. 빨리 정확한 추론 값을 얻어서 어플리케이션단에서 실시간으로 얼굴을 추론해 블러 처리하는 기능을 구현하고 싶다. </p>
<p>화이팅!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver npu tflite flatbuffer ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-npu-tflite-flatbuffer</link>
            <guid>https://velog.io/@wang_ki/windows-driver-npu-tflite-flatbuffer</guid>
            <pubDate>Tue, 05 May 2026 07:47:58 GMT</pubDate>
            <description><![CDATA[<p>tflite 모델은 내부가 FlatBuffers로 되어있다고 한다. google coral tpu에서 모델을 사용하기 위해서는 model 내부 구조의 bitstream을 뽑아서 맵핑을 시켜줘야 한다. google에서 제공해주는 libedgetpu를 사용하면 자동으로 해준다. 그러나 kmdf로 만들고 있기 때문에 직접 FlatBuffers로 파싱 하여 bitstream의 오프셋을 구해 적절한 va를 맵핑시켜주어야 한다. 어떻게 하는지 정리를 해보겠다. </p>
<hr>
<h3 id="edgetpu-compile-된-tflite">edgetpu compile 된 tflite</h3>
<p>기본적인 tflite 파일의 경우 edgetpu compiler로 한 번 더 가공해 주어야 한다. 일반적인 CPU나 GPU와는 완전히 다른 Edge TPU만의 독특한 하드웨어 가속 구조에 맞춰 연산 과정을 최적화하고 배치하기 위해서이다. 특징을 표로 정리하면 아래와 같다. </p>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">일반 .tflite</th>
<th align="left">Edge TPU 컴파일 후 .tflite</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>대상</strong></td>
<td align="left">범용 CPU / 모바일 장치</td>
<td align="left">Google Coral (Edge TPU) 전용</td>
</tr>
<tr>
<td align="left"><strong>연산 방식</strong></td>
<td align="left">소프트웨어 인터프리팅</td>
<td align="left">하드웨어 직접 실행 (Direct Execution)</td>
</tr>
<tr>
<td align="left"><strong>속도</strong></td>
<td align="left">보통</td>
<td align="left"><strong>매우 빠름 (실시간 추론 가능)</strong></td>
</tr>
<tr>
<td align="left">참고로 컴파일을 하려면 반드시 모델이 <code>전체 정수 양자화</code> 되어 있어야 한다.</td>
<td align="left"></td>
<td align="left"></td>
</tr>
</tbody></table>
<hr>
<h3 id="fbs-파일-컴파일하여-header-파일-생성하기">fbs 파일 컴파일하여 header 파일 생성하기</h3>
<p><code>libedgetpu</code> 저장소 내의 executable.fbs는 일반적인 <code>.tflite</code> 모델을 위한 것이 아니라, 오직 Edge TPU 컴파일러를 거친 <code>컴파일된 모델</code> 전용이다. fbs 파일로 header 파일을 뽑아내야 하는데 그러기 위해서는 <code>flatc.exe</code> 컴파일러가 필요하다. <a href="https://github.com/google/flatbuffers/releases">Release</a>에서 다운로드하여 설치할 수 있다. </p>
<blockquote>
<p>flatc --cpp --gen-object-api --no-includes your_file.fbs</p>
</blockquote>
<p>위 명령어를 통해서 <code>.h</code>파일을 뽑아낼 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/d26b43c2-86a5-48b8-9e6c-de471bb65bb5/image.png" alt=""></p>
<p><code>executable_generated.h</code> 파일이 생성되는데 내부에서 flatbuffers dependency가 필요해서 FlatBuffers 소스의 include도 필요하다. </p>
<hr>
<h3 id="libedgetpu-fbs-구조">libedgetpu fbs 구조</h3>
<p>우리는 tflite 모델의 bitstream을 구해야한다. 아래 스키마를 확인해보겠다. </p>
<pre><code class="language-fbs">// Holds information for an instruction bitstream chunk.
table InstructionBitstream {
  // Encoded bitstream for a real hardware.
  bitstream:[ubyte];

  // Offset (in bits) of various fields in the instruction bit stream. These
  // fields are filled in by the driver before sending the instruction stream
  // to the hardware.
  field_offsets:[FieldOffset];
}</code></pre>
<p><code>field_offsets</code>는 <code>FieldOffset</code>의 배열로 보인다. 그러면 <code>FieldOffset</code>을 확인해보면 된다. </p>
<pre><code>// Holds offset information of a field in an instruction bit stream chunk.
table FieldOffset {
  // Linker metadata.
  meta:Meta;

  // Bit offset.
  offset_bit:int;
}</code></pre><p><code>meta</code>필드를 확인할 수 있다. 그러면 <code>Meta</code>가 무엇인지 확인해보자. </p>
<pre><code class="language-fbs">// Linker metadata. Enums for various special fields in the encoded instruction
// stream that will be populated by the driver at run time.
table Meta {
  // Indicates which base address this metadata is targeting.
  desc:Description;

  // For input/output/scratch, provides batch information.
  // Parameter will not contain batch.
  batch:int;

  // Name of the input/output layer for input/output activations. Parameter and
  // scratch should not have this field.
  name:string;

  // Tells which bit position to update.
  position:Position;
}

enum Description : short {
  // Bundle::Alu::MOVI instruction to load output activation base address.
  BASE_ADDRESS_OUTPUT_ACTIVATION = 0,

  // Bundle::Alu::MOVI instruction to load input activation base address.
  BASE_ADDRESS_INPUT_ACTIVATION = 1,

  // Bundle::Alu::MOVI instruction to load parameter base address.
  BASE_ADDRESS_PARAMETER = 2,

  // Bundle::Alu::MOVI instruction to load scratch buffer base address.
  BASE_ADDRESS_SCRATCH = 3,
}

enum Position : short {
  // Lower 32-bit of 64-bit address.
  LOWER_32BIT = 0,

  // Upper 32-bit of 64-bit address.
  UPPER_32BIT = 1,
}</code></pre>
<p><code>Meta</code>는 또 <code>Descriptor</code> 필드를 가지는데 이는 output, input 등 여러 정보에 대한 base address에 대한 정보인 것 같다. 즉 Descriptor를 보고서 해당 오프셋 위치에는 어떤 base address를 넣어야 하는지 결정할 수 있는 것이다. </p>
<p>정리를 하면, 각 모델마다 bitstream에 input, output, parameter, scratch에 대한 base address를 넣어주고 bitstream을 device에게 넘긴다. device(hardware)는 bitstream에서 필요한 주소를 얻어 host ram에 접근하여 cpu의 개입 없이 데이터를 가져오거나 쓸 수 있다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>kmdf로 google coral tpu의 windows driver를 개발하고 있는데 bitstream을 넘기는 것부터가 고비였다. 하지만 조금씩 이해를 하고 있는 것 같다. </p>
<p>마치 protobuffer처럼 edgetpu는 FlatBuffers를 사용하는 것 같다. <code>IDL</code>에 대해 익숙하여 빨리 이해할 수 있었던 것 같다. </p>
<p>천천히 이해하면서 개발을 해야겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver NPU Project settings]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-NPU-Project-settings</link>
            <guid>https://velog.io/@wang_ki/windows-driver-NPU-Project-settings</guid>
            <pubDate>Wed, 22 Apr 2026 13:01:45 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@wang_ki/series/windows-driver">이전 Windows Driver</a> 시리즈에 이어서 실제 NPU를 컨트롤하는 windows driver를 만들예정이다. mcu와 하드웨어 가속기의 구조적 차이로 인해 실제 가속기 처럼 동작시키지는 못했다. 또한 usb를 활용했기때문에 PCIe 방식으로 구현해볼 예정이다. </p>
<hr>
<h3 id="google-coral-m2-accelerator">Google Coral M.2 Accelerator</h3>
<p>[정품] 구글 코랄 Coral 미니 PCIe M.2 가속기 B/M G650-04686-01
위 제품을 구매하였다. USB 모델도 있었지만 PCIe Driver를 만들어보기 위해서 해당 제품을 구매했다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/da0f9666-9d97-435c-b358-c649bdea94c3/image.png" alt=""></p>
<p>코랄은 4 TOPS의 연산 능력(Int8 특화)을 가진다. 일반적인 CPU나 GPU가 부동소수점 연산을 주로 하는 것과 달리, Edge TPU는 모델 양자화를 통해 크기를 줄인 모델을 극도로 빠르게 처리한다. MobileNet V2 기준 400FPS로 실시간 영상 분석 시 지연 시간이 거의 느껴지지 않는 수준이다. </p>
<p>이전 사용한 Pico와 달리 Coral은 진짜 딥러닝 모델등을 PC급 환경에서 실시간으로 돌리기 위한 도구이다.</p>
<hr>
<h3 id="coral-m2-슬롯에-장착하기">Coral M.2 슬롯에 장착하기</h3>
<p>사용 중인 메인보드를 확인하기 위해서 <code>win+r</code>을 누르고 아래와 같이 입력해주면 알 수 있습니다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/ddf6cefd-daf2-4c89-85ec-d9b2c89411bf/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/9b751a92-ec1b-498e-873c-d46eed9cc12f/image.png" alt=""></p>
<p>사용 중인 메인보드 스펙을 인터넷에서 찾아 몇 개의 M.2 슬롯을 제공해 주는지 확인해 볼 수 있다. 더 자세히 확인해 보고 싶다면 HWiINFO64를 설치하면 된다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/bd7435d4-99cd-48e6-ab12-4312eb99fdca/image.png" alt=""></p>
<p>3개의 M.2 슬롯을 사용하고 있고 모두 SSD로 활용중이다. 남은 슬롯은 그래픽카드가 가리고 있어 분리 후 장착해 주었다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/5f3d5418-8e44-4342-9214-54420f6bb268/image.png" alt=""></p>
<p>장치 관리자에서 정상적으로 인식된 것을 확인했다. </p>
<h4 id="문제-발생">문제 발생</h4>
<p>USB 처럼 당연히 virtual box로 Passthrough가 될줄 알았는데 PCIe의 경우 현재 버전에서 지원을 하지 않는다고 한다. virtual box 6.x.x 버전에서 삭제되었는데 지원하더라도 linux만 지원을 한다고 했다. </p>
<p>vmware도 알아봤는데 역시나 일반적으로는 Passthrough가 어렵다고 하였다. 결국 방법은 메인 PC에 Coral TPU를 장착하여 사용하고 노트북에서 개발하여 원격으로 디버깅하거나 Coral을 장착한 새로운 PC가 필요했다. </p>
<p>다행히 집에 남는 Desktop이 있어서 코랄을 옮겨서 설치해준 뒤, 개발 환경을 구축해주었다. </p>
<hr>
<h3 id="원격-데스크톱-설정">원격 데스크톱 설정</h3>
<p>virtual box가 아닌 실제 pc에 원격으로 접속해야 하므로 원격 데스크톱을 설정해 주었다. <code>tailscale</code>을 설치해 줘서 외부에서 원격으로 접속할 수 있도록 설정해 주었다. 노트북으로 카페나 외부에서 안전하게 접속할 수 있다. virtual box를 사용할 때는 노트북으로 외부에서 접속하기 까다로워서 불편했지만 이제는 손쉽게 이용할 수 있을 것 같다. 집에서는 내부망이므로 더욱 손쉽게 접근할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/bf5e39da-dc2c-49b3-90da-75a6f10e23eb/image.png" alt=""></p>
<p><a href="https://www.coral.ai/docs/m2/get-started/#2-install-the-pcie-driver-and-edge-tpu-runtime">https://www.coral.ai/docs/m2/get-started/#2-install-the-pcie-driver-and-edge-tpu-runtime</a>
위 문서를 확인하면 ProductID가 <code>089a</code>인 것을 확인할 수 있다. 실제로 확인해 보면 다음과 같다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/ca1e56a1-53df-46be-a245-041ed1948997/image.png" alt=""></p>
<hr>
<h3 id="결론">결론</h3>
<p>역시 초기 세팅 및 환경설정이 개발의 절반이라는 게 맞는 말이다. Virtual Box에서 Passthrough가 되지 않아서 당황했지만 다행히 남는 pc가 있어서 무사히 세팅한 것 같다. Linux Driver는 OpenSource라서 참고하면서 KMDF로 DMA, ISR, DPC 등 여러 커널 드라이버 개발 방식을 배우며 만들어야겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver interrupt continuous read 사용하기 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-interrupt-continuous-read-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@wang_ki/windows-driver-interrupt-continuous-read-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 14 Apr 2026 14:27:11 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@wang_ki/windows-driver-interrupt-endpoint-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0">전편</a>에서 interrupt endpoint를 추가하였다. 온도 센서값을 읽어서 kmdf driver와 통신은 성공하였다. application level에서 실시간으로 값을 읽는 것을 구현하였다. </p>
<hr>
<h3 id="전체-흐름도">전체 흐름도</h3>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/2b14f23e-ad5a-4eda-b346-79862af0cd96/image.png" alt=""></p>
<hr>
<h3 id="kmdf-pico-driver-구조">KMDF pico driver 구조</h3>
<p>중요한 포인트는 어플리케이션에서 Interrupt read 관련 IOCTL 요청을 할 때 받은 <code>reuqest</code>를 바로 완료 처리하지 않고 PendingQueue에 담아준다. 그리고 pico mcu로부터 온도 센서 값을 수신할 때 호출되는 콜백에서 Pending Queue 내부의 request를 꺼내서 완료 처리해 주면 된다. </p>
<pre><code class="language-c">status = WdfIoQueueRetrieveNextRequest(pDeviceContext-&gt;PendingQueue, &amp;request);
if (!NT_SUCCESS(status)) {
    DbgPrint(&quot;[picodriverEvtInterruptReadComplete] No pending request, discarding data\n&quot;);
    return;
}

// App의 output buffer에 interrupt 데이터 복사
status = WdfRequestRetrieveOutputBuffer(request, NumBytesTransferred, &amp;outBuffer, NULL);
if (!NT_SUCCESS(status)) {
    DbgPrint(&quot;[picodriverEvtInterruptReadComplete] WdfRequestRetrieveOutputBuffer failed 0x%x\n&quot;, status);
    WdfRequestComplete(request, status);
    return;
}

RtlCopyMemory(outBuffer, WdfMemoryGetBuffer(Buffer, NULL), NumBytesTransferred);
WdfRequestCompleteWithInformation(request, STATUS_SUCCESS, NumBytesTransferred);</code></pre>
<p>위 코드를 참고하면 된다. </p>
<p>결과적으로 어플리케이션 레벨에서 icotl로 요청한 request가 있어야 실시간으로 읽어갈 수 있는 점에서 bulk 통신으로 폴링 방식으로 읽는 방식과 크게 다르지는 않다.</p>
<p>*Application 레벨에서는 요청-응답 구조로 Polling과 유사해 보이지만, USB 버스 레벨에서는 디바이스가 준비됐을 때만 전송이 발생하므로 불필요한 버스 점유가 없다는 점에서 본질적으로 다르다. *</p>
<hr>
<h3 id="어플리케이션-구조">어플리케이션 구조</h3>
<p>어플리케이션 레벨의 동작을 간략히 설명하겠다. 
OnInitDialoge에서 interrupt read init을 통해서 DeviceIoCotrol로 비동기 요청을 하고 콜백을 등록을 해준다. 콜백이 호출되면 postmessage를 통해서 gui thread로 메시지를 넘겨준다. 해당 메시지에 연결된 핸들러가 호출이 되면서 ui control을 업데이트해준다. </p>
<hr>
<h3 id="테스트">테스트</h3>
<p>pico를 복합장치로 설정했기에 cdc 인터페이스로 시리얼 통신으로 실제 로그를 받아 볼 수 있다. pico에서 보내는 로그와 어플리케이션인 mfc에서 실시간으로 온도 데이터를 수신 테스트를 하였다. 중간에 이미지를 bulk endpoint로 보내서 얼굴 인식하도록 하였고 받은 추론 값으로 블러처리도 진행하였다. interrupt 방식은 문제 없는 것을 확인할 수 있다. </p>
<iframe width="315" height="560" 
src="https://www.youtube.com/embed/TQSuxVxm_3Q" 
title="YouTube video player" frameborder="0" 
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
allowfullscreen></iframe>

<hr>
<h3 id="결론">결론</h3>
<p>pico mcu를 활용한 프로젝트를 마무리할 예정이다. </p>
<ol>
<li>mcu 전용으로 만든 model embed</li>
<li>kmdf driver usb 개발 </li>
<li>tinyusb를 활용하여 vendor specific, cdc interface로 복합 장치 구현 </li>
<li>bulk, interrupt endpoint 통신 </li>
</ol>
<p>이외에도 정말 많은 시간이 들어갔다. led 디버깅을 하면서 불편해 cdc 인터페이스를 추가한 것, driver 개발하며 bosd 발생으로 windbg로 덤프 분석 및 실시간 커널 디버깅 등등 개발에 필요한 지식이 정말 많다는 것을 깨달았다. 프로젝트를 진행하며 <a href="https://www.youtube.com/@%EC%9D%B4%EB%B4%89%EC%84%9D-d6u">https://www.youtube.com/@%EC%9D%B4%EB%B4%89%EC%84%9D-d6u</a> 해당 유튜브 채널이 정말로 도움이 많이 되었다. windows kernel과 driver 개발이 접근하기 쉽지는 않다. </p>
<p>다음 프로젝트로는 실제 가속기를 활용할 예정이다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/7442fdf8-c54a-4da3-a40e-b7ccca38238b/image.png" alt="">
위 가속기를 가지고 PCIe driver를 kmdf로 만들 것이다. usb보다 훨씬 난이도가 높다고 한다. 화이팅! </p>
<p><a href="https://github.com/wangki-kyu/pico_usb_vendor">https://github.com/wangki-kyu/pico_usb_vendor</a>
<a href="https://github.com/wangki-kyu/pico_driver">https://github.com/wangki-kyu/pico_driver</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver interrupt endpoint 추가하기 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-interrupt-endpoint-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@wang_ki/windows-driver-interrupt-endpoint-%EC%B6%94%EA%B0%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 14 Apr 2026 09:22:50 GMT</pubDate>
            <description><![CDATA[<p>pico의 온도 값을 application level에서 반복적으로 요청해도 되지만 interrupt 방식으로 처리해보고 싶어 endpoint를 추가하였다. </p>
<hr>
<h3 id="interrupt-endpoint-추가">interrupt endpoint 추가</h3>
<p>vendor specific interface에 interrupt endpoint를 추가한 뒤, 다시 장치 인식을 시켜주었다. </p>
<pre><code class="language-c">const uint8_t desc_configuration[] = {
    // Config number, interface count, string index, total length, attribute, power in mA
    TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),

    // CDC first (IAD is automatically included by TUD_CDC_DESCRIPTOR)
    TUD_CDC_DESCRIPTOR(ITF_NUM_CDC_CMD, 0, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64),

    // 2. Vendor Interface (수동 정의)
    // Interface Descriptor
    9, TUSB_DESC_INTERFACE, ITF_NUM_VENDOR, 0, 3, // bNumEndpoints를 3으로 설정 (Bulk 2 + Int 1)
    TUSB_CLASS_VENDOR_SPECIFIC, 0x00, 0x00, 0,    // Vendor Class, Subclass, Protocol, String Index

    // Endpoint: Bulk IN
    7, TUSB_DESC_ENDPOINT, EPNUM_VENDOR_IN, TUSB_XFER_BULK, 64, 0, 0,
    // Endpoint: Bulk OUT
    7, TUSB_DESC_ENDPOINT, EPNUM_VENDOR_OUT, TUSB_XFER_BULK, 64, 0, 0,
    // Endpoint: Interrupt IN (온도 데이터용)
    7, TUSB_DESC_ENDPOINT, EPNUM_VENDOR_INT_IN, TUSB_XFER_INTERRUPT, 8, 0, 10
};</code></pre>
<p>장치 관리자에서 정상으로 인식이 되어, <code>usbview</code>로 확인해보았다.</p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/99571b14-d042-4fef-8313-11640ff24488/image.png" alt=""></p>
<p>정상적으로 Endpoint가 추가된 것을 확인할 수 있다. </p>
<hr>
<h3 id="온도-센서-보내기">온도 센서 보내기</h3>
<p>문제가 발생하였다. </p>
<pre><code class="language-c">void send_temperature_data_interrupt(float temperature)
    {
        static uint8_t data[3];
        data[0] = 0xBB;                                                  // Different marker for interrupt data
        data[1] = (uint8_t)temperature;                                  // Integer part
        data[2] = (uint8_t)((temperature - (uint8_t)temperature) * 100); // Fractional part

        // Use low-level endpoint transfer API for interrupt endpoint
        if (!usbd_edpt_busy(0, EPNUM_VENDOR_INT_IN))
        {
            usbd_edpt_xfer(0, EPNUM_VENDOR_INT_IN, data, sizeof(data));
            printf(&quot;[TEMP] Interrupt sent: %.2f°C\n&quot;, temperature);
            fflush(stdout);
        } else {
            printf(&quot;EP 0x84 is busy!\n&quot;);
            fflush(stdout);
        }
    }</code></pre>
<p>위처럼 코딩을 한 뒤, main loop에서 1초마다 호출하도록 설정했는데 장치가 인식을 하지 못하였다. <code>usbd_edpt_xfer</code> 함수를 주석하니 아래와 같이 cdc로 로그를 확인할 수 있었다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/6301245f-24f2-457f-b942-26c070ddb4f1/image.png" alt=""></p>
<p>온도 센서로부터 정상으로 데이터를 읽어 보내주는 것을 확인하였다. interrupt가 bulk랑 같이 동시에 잘 동작하는지 확인하기 위해서 얼굴 인식 추론을 하였다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/5b60bad5-89c0-4fe8-a8e3-cc334e7dd656/image.png" alt=""></p>
<p>이미지의 얼굴 부분을 얼추 인식하여 블러처리한 결과이다. 그런데 로그를 확인해보니 이상한 점을 발견하였다.</p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/9280b0fb-ad97-4745-893c-68b1e3250c30/image.png" alt=""></p>
<p>분명 bulk endpoint를 통해서 추론 결과 값을 전달해주는데 interrupt endpoint가 busy한 상태가 되었다. </p>
<p>곰곰히 생각을 해보니 endpoint를 추가해주면서 kmdf driver 단에서 read pipe에 문제가 생겼을 수 있다는 생각을 하였다. pico_driver 코드를 확인해보니 역시나 read pipe가 interrupt endpoint로 설정되고 있었다. </p>
<pre><code class="language-cpp">// Get pipes
for (pipeIndex = 0; pipeIndex &lt; numEndpoints; pipeIndex++) {
    WDF_USB_PIPE_INFORMATION pipeInfo;
    WDFUSBPIPE pipe;

    WDF_USB_PIPE_INFORMATION_INIT(&amp;pipeInfo);
    pipe = WdfUsbInterfaceGetConfiguredPipe(usbInterface, pipeIndex, &amp;pipeInfo);

    if (pipe != NULL) {
        DbgPrint(&quot;Pipe %d: EndpointAddress=0x%02x, Direction=%s\n&quot;,
            pipeIndex,
            pipeInfo.EndpointAddress,
            USB_ENDPOINT_DIRECTION_IN(pipeInfo.EndpointAddress) ? &quot;IN&quot; : &quot;OUT&quot;);

        // Store bulk IN and OUT pipes
        if (USB_ENDPOINT_DIRECTION_IN(pipeInfo.EndpointAddress)) {
            pDeviceContext-&gt;ReadPipe = pipe;
            DbgPrint(&quot;Stored READ pipe (IN endpoint 0x%02x)\n&quot;, pipeInfo.EndpointAddress);
        }
        else {
            pDeviceContext-&gt;WritePipe = pipe;
            DbgPrint(&quot;Stored WRITE pipe (OUT endpoint 0x%02x)\n&quot;, pipeInfo.EndpointAddress);
        }
    }
}</code></pre>
<p>windbg로 로그를 확인해 보니 아래와 같다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/45360101-82b4-421a-be97-e8a46f8c9ad8/image.png" alt=""></p>
<p>마지막에 read pipe로 0x84를 저장하는 것을 확인했다. interrupt read pipe를 context에 저장하도록 로직을 변경해 주면 될 것 같다. </p>
<hr>
<h3 id="kmdf-소스-수정">kmdf 소스 수정</h3>
<pre><code class="language-c">if (pipe != NULL) {
    DbgPrint(&quot;Pipe %d: EndpointAddress=0x%02x, Direction=%s\n&quot;,
        pipeIndex,
        pipeInfo.EndpointAddress,
        USB_ENDPOINT_DIRECTION_IN(pipeInfo.EndpointAddress) ? &quot;IN&quot; : &quot;OUT&quot;);

    // Store bulk IN and OUT pipes
    if (USB_ENDPOINT_DIRECTION_IN(pipeInfo.EndpointAddress)) {
        if (pipeInfo.EndpointAddress == 0x83) {
            pDeviceContext-&gt;ReadPipe = pipe;
            DbgPrint(&quot;Stored READ pipe (IN endpoint 0x%02x)\n&quot;, pipeInfo.EndpointAddress);
        }
        else if (pipeInfo.EndpointAddress == 0x84) {
            pDeviceContext-&gt;InterruptReadPipe = pipe;
            DbgPrint(&quot;Stored Interrupt READ pipe (IN endpoint 0x%02x)\n&quot;, pipeInfo.EndpointAddress);
        }
    }
    else {
        pDeviceContext-&gt;WritePipe = pipe;
        DbgPrint(&quot;Stored WRITE pipe (OUT endpoint 0x%02x)\n&quot;, pipeInfo.EndpointAddress);
    }
}
</code></pre>
<pre><code class="language-c">typedef struct _DEVICE_CONTEXT
{
    WDFUSBDEVICE UsbDevice;
    WDFUSBINTERFACE UsbInterface;
    WDFUSBPIPE WritePipe;
    WDFUSBPIPE ReadPipe;
    WDFUSBPIPE InterruptReadPipe;
    ULONG PrivateDeviceData;  // just a placeholder

} DEVICE_CONTEXT, *PDEVICE_CONTEXT;</code></pre>
<p><code>InterruptReadPipe</code>를 추가해 주었다. windbg로 로그를 확인해 보면 정상적으로 잘 저장된 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/695bf522-a83d-4242-9c1d-5757b0b74dd4/image.png" alt=""></p>
<p>interrupt endpoint와 bulk endpoint가 각각 동작하는지 테스트해 보았다. </p>
<p>사소한 이슈가 하나 더 발생하였다. 시작부터 busy가 발생하였다. 문제는 pico 쪽에서 사용하는 <code>tud_vendor_n_write</code> 함수에 있었다. vendor_specific의 경우 기본적으로 out endoint 1, in endpoint 1로 구성하는 게 기본이다. 현재 2개의 in endpoint가 설정되어 있기에 늦게 설정된 0x84 interrupt endpoint로 내보내고 있던 것이었다. </p>
<p>따라서 api가 아닌 더 저수준의 함수를 호출하도록 변경했다. </p>
<pre><code class="language-c">if (!usbd_edpt_busy(0, 0x83)) {
    usbd_edpt_claim(0, 0x83);
    usbd_edpt_xfer(0, 0x83, response, 64);
}</code></pre>
<p>다시 테스트를 진행하엿다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/8c28afd9-456d-4a69-83e3-4ab1592c172e/image.png" alt=""></p>
<p>bulk endoint와 interrupt endpoint가 각각의 transfer 통로로 동작하는 것을 확인하였다. </p>
<h3 id="문제점2">문제점2</h3>
<p>여전히 busy가 발생하며 통신이 되지 않았다. 찾아보니 usb 인터럽트는 장치가 호스트를 방해해서 먼저 말을 거는 게 아니라, 호스트가 아주 짧은 간격으로 계속 물어봐 주는 정기점검 방식이라고 한다. 완전 착각하고 있었다. 
즉, kmdf pico driver에서 interrupt read pipe로 읽고 있지 않아서 발생한 문제라고 판단했다. </p>
<pre><code class="language-c">UNREFERENCED_PARAMETER(Device);
NTSTATUS status;
WDF_USB_CONTINUOUS_READER_CONFIG readerConfig;
ULONG bufferSize = 64;  // Typical interrupt endpoint size

DbgPrint(&quot;[picodriverStartInterruptRead] Starting interrupt continuous reader\n&quot;);

// Configure continuous reader
WDF_USB_CONTINUOUS_READER_CONFIG_INIT(&amp;readerConfig,
                                     picodriverEvtInterruptReadComplete,
                                     DeviceContext,
                                     bufferSize);

// Set number of reader buffers (WDF will manage multiple buffers)
readerConfig.NumPendingReads = 2;

// Start the continuous reader (configure it first)
status = WdfUsbTargetPipeConfigContinuousReader(
    DeviceContext-&gt;InterruptReadPipe,
    &amp;readerConfig
);

if (!NT_SUCCESS(status)) {
    DbgPrint(&quot;[picodriverStartInterruptRead] WdfUsbTargetPipeConfigContinuousReader failed 0x%x\n&quot;, status);
    return status;
}

DbgPrint(&quot;[picodriverStartInterruptRead] Continuous reader configured\n&quot;);

// CRITICAL: Start the I/O target to actually begin polling the interrupt endpoint
// Without this, the host will NOT send IN tokens to the device
status = WdfIoTargetStart(WdfUsbTargetPipeGetIoTarget(DeviceContext-&gt;InterruptReadPipe));

if (!NT_SUCCESS(status)) {
    DbgPrint(&quot;[picodriverStartInterruptRead] WdfIoTargetStart failed 0x%x\n&quot;, status);
    return status;
}

DbgPrint(&quot;[picodriverStartInterruptRead] I/O Target started - polling interrupt endpoint (0x84)\n&quot;);
return STATUS_SUCCESS;</code></pre>
<p><code>WdfUsbTargetPipeConfigContinuousReader</code>를 등록해 주고 <code>WdfIoTargetStart</code>을 호출해 주면 pico 쪽에서 설정한 주기로 읽어온다고 한다. </p>
<p>테스트를 해보았다. </p>
<h4 id="windbg-driver-log">windbg driver log</h4>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/568dd57d-4f1d-4911-8634-a17393c904a8/image.png" alt=""></p>
<h4 id="pico-0x84-interrupt-endpoint-send">pico 0x84 interrupt endpoint send</h4>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/3dd6797d-41ac-4c85-89d3-0c73ead0d5df/image.png" alt=""></p>
<p>정상으로 보내는 것을 확인했다. </p>
<p>변경된 소스는 깃헙에서 확인할 수 있다. </p>
<p><a href="https://github.com/wangki-kyu/pico_usb_vendor">https://github.com/wangki-kyu/pico_usb_vendor</a>
<a href="https://github.com/wangki-kyu/pico_driver">https://github.com/wangki-kyu/pico_driver</a></p>
<hr>
<h3 id="결론">결론</h3>
<p>usb 인터럽트는 일반적으로 하드웨어 인터럽트와는 다른 개념인 것 같다. 문제를 해결해 나가는 게 정말 재밌는 것 같다. 이제 어플리케이션 레벨에서 실시간 온도를 읽어서 gui로 보여주는 것을 개발하고 마무리하려고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver pico USB Composite Device로 사용하기 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-pico-USB-Composite-Device%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@wang_ki/windows-driver-pico-USB-Composite-Device%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 13 Apr 2026 04:14:56 GMT</pubDate>
            <description><![CDATA[<p>pico를 미니 NPU로 사용하는 프로젝트를 진행하며 어려웠던 내용과 해결한 방법에 대해 작성하겠다. </p>
<h3 id="문제점">문제점</h3>
<p>현재 pico에 <code>tinyusb</code>를 사용하여 vendor specific interface로 driver와 통신하고 있다. 개발을 진행하며 너무나 불편한 점이 많았다. pico에 올린 펌웨어 내용을 디버깅할 수 없었다. 익숙한 개발 환경이 아니라서 잘 못하는 걸 수도 있는데 일단 한번 올리고 나면 디버깅을 하기 힘들었다. LED 색상을 통해 해봤는데 컴퓨터를 부실뻔했다. </p>
<hr>
<h3 id="usb-복합-장치-구성하기">USB 복합 장치 구성하기</h3>
<p>그래서 찾아보니 USB 장치의 경우 물리적 하드웨어 내에 여러 개의 인터페이스가 존재한다. 호스트(PC)에 연결했을 때, 장치 관리자에서 CDC 용 가상 COM 포트와 Vendor-Specific 장치가 각각 독립적으로 나타날 수 있다고 한다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/460382c3-975a-436d-9f6b-5c3e68a2d7e8/image.png" alt=""></p>
<p>위 사진처럼 USB Composite Device 하위에 여러 장치가 나타나는 것을 말한다. 
pico의 경우 printf를 호출하면 내부적으로 표준 출력(stdout)이 USB CDC Serial로 리다이렉션되어 PC의 터미널에서 내용을 볼 수 있다고 한다. </p>
<p>소스레벨에서는 <code>tusb_config.h</code>와 <code>usb_descriptor.c</code>를 수정해주면 된다. 자세한 내용은 깃헙을 참조하면 된다. 
<a href="https://github.com/wangki-kyu/pico_usb_vendor">https://github.com/wangki-kyu/pico_usb_vendor</a></p>
<h3 id="serial-연결-하기">Serial 연결 하기</h3>
<p>pico에 소스를 올리게 되면 다음과 같은 동작을 예상할 수 있다. vendor specific interface로 인식된 장치의 경우 직접 만든 driver가 올라갈 것이고 CDC 인터페이스는 usbser.sys 드라이버가 자동을 로드되어 가상 COM 포트를 생성할 것이다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/0c22f663-58e7-435d-bc1d-60f087a80344/image.png" alt=""></p>
<p>장치 관리자를 확인해보니 정확히 로드가 잘 된 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/ea505c71-6f68-4deb-b7e1-556372c924ae/image.png" alt=""></p>
<p>속성에서 하드웨어 ID를 확인해 보면 기존과 다르게 <code>MI_02</code>가 붙은 것을 확인할 수 있다. <code>Multiple Interface</code>의 약자로 해당 장치가 복합 장치일 때, 몇 번째 인터페이스인지를 나타내는 식별자라고 한다. </p>
<p><code>USB Device Viewer</code>를 확인해 보면 정확히 볼 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/9d7064a2-46f8-480a-b22e-7f69c12dfaca/image.png" alt=""></p>
<p>InterfaceNuber가 0x02이고 InterfaceClass가 0xFF로 vendor specific인 것을 확인할 수 있다. 동일하게 가상 COM 장치도 확인해보겠다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/cbc0f263-dd37-4872-8301-6f29ef9205d5/image.png" alt="">
역시 <code>MI_00</code>이 붙은 것을 확인할 수 있다. <code>USB Device Viewer</code>로 확인해보면 아래와 같다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/b206bfe8-26d2-4e06-826e-45813fd922b4/image.png" alt=""></p>
<hr>
<h3 id="python으로-serial-읽기">python으로 serial 읽기</h3>
<p>pico에서의 설정은 깃헙을 참조하면 된다. 
<code>printf</code>를 호출했을 때 Serial로 받을 수 있으므로 테스트를 해봐야 한다. </p>
<pre><code class="language-python">import serial
import time

# 포트 설정 (COM27)
PORT = &#39;COM27&#39;
BAUD = 115200

try:
    # timeout을 설정해야 장치가 응답 없을 때 무한 대기에 빠지지 않습니다.
    ser = serial.Serial(PORT, BAUD, timeout=1)
    print(f&quot;Connected to {PORT}&quot;)

    while True:
        if ser.in_waiting &gt; 0:  # 읽을 데이터가 있을 때만 처리
            try:
                # decode(&#39;utf-8&#39;, &#39;ignore&#39;)를 써야 이상한 바이트가 섞여도 안 죽습니다.
                line = ser.readline().decode(&#39;utf-8&#39;, &#39;ignore&#39;).strip()
                if line:
                    print(f&quot;[LOG] {line}&quot;)
            except Exception as e:
                print(f&quot;Read error: {e}&quot;)

        time.sleep(0.01)  # CPU 점유율 과다 방지

except serial.SerialException as e:
    print(f&quot;Could not open port {PORT}: {e}&quot;)
except KeyboardInterrupt:
    print(&quot;\nStop logging.&quot;)
finally:
    if &#39;ser&#39; in locals() and ser.is_open:
        ser.close()</code></pre>
<p>PORT는 현재 가상 장치의 COM을 확인하고 입력해 주면 된다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/fdde6b78-e919-46f8-8944-46b9b7c451c3/image.png" alt="">
실행해 주면 정상적으로 COM27에 연결이 된 것을 확인할 수 있다. </p>
<p>test 영상입니다. </p>
<iframe width="315" height="560" 
src="https://www.youtube.com/embed/VYiRp1z8OiQ" 
title="YouTube video player" frameborder="0" 
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
allowfullscreen></iframe>

<p><a href="https://github.com/wangki-kyu/pico_usb_vendor">https://github.com/wangki-kyu/pico_usb_vendor</a>
<a href="https://github.com/wangki-kyu/pico_driver">https://github.com/wangki-kyu/pico_driver</a></p>
<hr>
<h3 id="결론">결론</h3>
<p>led 디버깅으로 답답한 부분을 해결할 수 있었다. 기존에 이미지 추론이 되지 않아서 너무 답답했는데 이유를 알 수가 없었다. 로깅으로 확인하니 이미지가 정상적으로 수신되고 있지 않았다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/1d2ee26a-0dd7-42da-a591-729668ba9bd3/image.png" alt=""></p>
<p>cmd + image data를 붙여서 보냈는데 모든 수신에서 cmd를 체크하니 정상적인 로직을 태우지 못했던 것이다. 수정을 하여 정상으로 수신하도록 하였다. </p>
<p>cdc를 통한 디버깅을 할 수 있음으로 인해 개발 난이도가 확 낮아진 느낌이다. 나이스 </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver kernel dump 분석하기]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-kernel-dump-%EB%B6%84%EC%84%9D-1</link>
            <guid>https://velog.io/@wang_ki/windows-driver-kernel-dump-%EB%B6%84%EC%84%9D-1</guid>
            <pubDate>Sat, 11 Apr 2026 11:36:38 GMT</pubDate>
            <description><![CDATA[<p>driver개발 중 bsod가 발생하여 분석하는 내용에 대해서 작성해본다. </p>
<h3 id="dump-설정-방법">Dump 설정 방법</h3>
<p>VM에서 bsod가 발생했다. 별다른 설정한적 없었지만 <code>C:\windows</code>를 확인하니 <code>MiniDump</code> 폴더에 미니 덤프가 생성된 것을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/5afab7ce-8966-42e8-946e-80a1280df244/image.png" alt=""></p>
<p>레지스트리에서 <code>HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\CrashControl</code> 경로에서 덤프 저장 경로 및 덤프 파일 종류를 설정할 수 있다. <img src="https://velog.velcdn.com/images/wang_ki/post/388ad322-253b-4f16-b47c-1e75239f746b/image.png" alt=""></p>
<p><code>CrashDumpEnabled</code>가 3으로 설정된 것은 MiniDump를 생성한다는 의미이다. 
<a href="https://learn.microsoft.com/ko-kr/troubleshoot/windows-server/performance/memory-dump-file-options">https://learn.microsoft.com/ko-kr/troubleshoot/windows-server/performance/memory-dump-file-options</a> 공식 사이트에서 자세한 내용을 확인할 수 있다. <img src="https://velog.velcdn.com/images/wang_ki/post/a0924718-6980-42d9-8637-57fdb07ab143/image.png" alt=""></p>
<hr>
<h3 id="minidump-분석하기">MiniDump 분석하기</h3>
<h4 id="analyze--v">!analyze -v</h4>
<p>첫 번째로 해야하는 것은 <code>!analyze -v</code> 명령어를 통해서 분석을 하는 것이다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/f65013bf-999d-42a7-acaf-eeb3ecb8185f/image.png" alt=""></p>
<p>공부를 하면서 안것인데 빨간 박스인 <code>WDF_VIOLATION (10d)</code>가 오류가 난 원인인 것이다. Bug Check Code라고도 하는데 <code>MSDN</code>을 통해 보면 각 코드에 대해서 Argument가 정의 되어있다. 
<a href="https://learn.microsoft.com/ko-kr/windows-hardware/drivers/debugger/bug-check-0x10d---wdf-violation">https://learn.microsoft.com/ko-kr/windows-hardware/drivers/debugger/bug-check-0x10d---wdf-violation</a>
<img src="https://velog.velcdn.com/images/wang_ki/post/ad1bf79a-cefc-4317-8cbd-d30b4aa1ca65/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/38057a2b-13f6-4be1-83e6-651ad6e92754/image.png" alt=""></p>
<p>Arg1이 0x3이므로 Arg2가 WDFREQUEST HANDLE인 것을 확인할 수 있다. 즉 이번 fault가 발생한 원인은 미해결 참조 때문이고 Arg3를 보면 남아있는 참조 수가 1이라고 나와있는 것을 볼 수 있다. </p>
<p>이 힌트를 보고 대략적으로 왜 문제가 발생했는지 1차적으로 알 수 있다. </p>
<h4 id="k호출스택-확인">k(호출스택) 확인</h4>
<p><code>k</code> 명령어를 입력하면 호출스택을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/f545f7e2-c3b9-486e-8884-5073425906fc/image.png" alt=""></p>
<p>pdb를 symbol로 등록해놓은 상태라서 정확히 개발 중인 <code>pico_driver</code>의 어떤 함수에서 발생한지 확인할 수 있다. <code>PicoInferenceReadComplete</code>함수에서 발생한 것을 한눈에 확인할 수 있다. </p>
<h4 id="소스-분석">소스 분석</h4>
<p>IOCTL을 처리하는 소스에 브레이크를 잡은 뒤, 다시 상황을 재현하기 위해서 재시작하여 windbg로 분석한다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/d8e533b9-9791-437f-93b7-2760ca338645/image.png" alt=""></p>
<p><code>dv</code> 명령어로 현재 로컬 변수들을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/1f44541a-f47a-4d4a-b471-3aea1a306276/image.png" alt="">
mfc에서 image 64*64 bytes + cmd 구조로 총 4097 byte를 보냈는데 정확한 것을 확인할 수 있다. </p>
<pre><code class="language-c">PINFERENCE_CONTEXT infContext = (PINFERENCE_CONTEXT)ExAllocatePoolWithTag(NonPagedPool, sizeof(INFERENCE_CONTEXT), &#39;ifnc&#39;);
if (!infContext) {
    DbgPrint(&quot;[pico_driver] ERROR: Failed to allocate inference context\n&quot;);
    status = STATUS_INSUFFICIENT_RESOURCES;
    WdfRequestComplete(Request, status);
    return;
}</code></pre>
<p>Context를 만들어서 힙 할당을 통해서 Request를 새로 만들어주는 로직을 사용했다. 그러나 디버깅을 통해 확인할 결과 Request는 Complete Rountine에 등록한 콜백함수의 매개변수로 동일한 Request가 들어온다는 것을 확인하여 재사용하도록 했다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/377b3786-e8ff-4eee-b7f8-5f067dd12c84/image.png" alt=""></p>
<p>위 다이어그램의 흐름처럼 정상적으로 동작하는 것을 확인하였다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>커널 crash가 발생하여 bsod가 발생하든 appication을 개발 중 process가 crash가 발생하든 dump를 분석하여 어느 곳에서 문제가 발생했는지 분석할 수 있다는 자신감이 있는 것 같다. 물론 모든 명령어를 알지는 못하지만 ai를 활용하여 검색하면서 디버깅한다면 해결할 수 있을 것이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Windows Driver USB Endpoint Bulk 통신 오류 해결 ]]></title>
            <link>https://velog.io/@wang_ki/Windows-Driver-USB-Endpoint-Bulk-%ED%86%B5%EC%8B%A0-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@wang_ki/Windows-Driver-USB-Endpoint-Bulk-%ED%86%B5%EC%8B%A0-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Thu, 09 Apr 2026 04:35:07 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@wang_ki/Windows-Driver-pico-rp2040-Vendor-Interface-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0">pico 펌웨어 올리기</a> 여기서 pico에 펌웨어를 올렸기 때문에 KMDF로 USB Vendor Interface가 호환되는 드라이버를 만들어서 올렸다. 드라이버 올리는 것은 문제가 없었지만 endpoint로 write 하는 과정에서 문제가 발생하고 해결한 내용에 대해 포스팅하겠다. </p>
<hr>
<h3 id="pico-driver-개발-흐름">pico driver 개발 흐름</h3>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/a99bc8a1-cbab-4e1f-860a-2a06d65c6063/image.png" alt=""></p>
<h4 id="1단계-device-생성">1단계: Device 생성</h4>
<p>PnP 매니저가 새 하드웨어를 감지하면 디바이스 생성을 요청한다. 드라이버는 WDFDEVICE 객체를 만들고 EvtDevicePrepareHardware 콜백을 등록한다. </p>
<h4 id="2단계-리소스-할당-및-pipe-저장">2단계: 리소스 할당 및 Pipe 저장</h4>
<p>이 단계가 중요하다. 시스템 디바이스에 I/O 리소스를 할당한 후 호출된다. 드라이버는 이 콜백에서 USB 인터페이스와 엔드포인트 정보를 얻을 수 있다. 여기서 device conetxt에 앞으로 통신에 활용할 Pipe를 저장한다. 이렇게 해야 이후에 IOCTL 요청이 들어왔을 때, Context에서 꺼내 Bulk 통신을 진행할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/83bef002-95b8-4913-9cf1-e93d0cec9fb4/image.png" alt=""></p>
<hr>
<h3 id="user-mode-application">user mode application</h3>
<p>user mode에서 IOCTL 요청으로 led를 제어할 수 있도록 간단하게 코드를 작성했다. </p>
<pre><code class="language-c">int main()
{
    std::cout &lt;&lt; &quot;Pico Driver Test Application&quot; &lt;&lt; std::endl;
    std::cout &lt;&lt; &quot;=============================&quot; &lt;&lt; std::endl;

    // Find and open the pico driver device
    HANDLE hDevice = FindPicoDriverDevice();

    if (hDevice != INVALID_HANDLE_VALUE) {
        std::cout &lt;&lt; &quot;\nDriver handle obtained successfully!&quot; &lt;&lt; std::endl;

        // LED Control Examples
        DWORD bytesReturned = 0;
        UCHAR command = 0;
        UCHAR status = 0;

        // Turn LED ON
        std::cout &lt;&lt; &quot;\nTurning LED ON...&quot; &lt;&lt; std::endl;
        command = LED_ON;
        if (DeviceIoControl(hDevice, IOCTL_PICO_TEST_WRITE, &amp;command, sizeof(command), NULL, 0, &amp;bytesReturned, NULL)) {
            std::cout &lt;&lt; &quot;LED ON - Success!&quot; &lt;&lt; std::endl;
        } else {
            std::cerr &lt;&lt; &quot;LED ON - Failed: &quot; &lt;&lt; GetLastError() &lt;&lt; std::endl;
        }

        Sleep(1500);

        // Turn LED OFF
        std::cout &lt;&lt; &quot;\nTurning LED OFF...&quot; &lt;&lt; std::endl;
        command = LED_OFF;
        if (DeviceIoControl(hDevice, IOCTL_PICO_TEST_WRITE, &amp;command, sizeof(command), NULL, 0, &amp;bytesReturned, NULL)) {
            std::cout &lt;&lt; &quot;LED OFF - Success!&quot; &lt;&lt; std::endl;
        } else {
            std::cerr &lt;&lt; &quot;LED OFF - Failed: &quot; &lt;&lt; GetLastError() &lt;&lt; std::endl;
        }

        CloseHandle(hDevice);
    } else {
        std::cerr &lt;&lt; &quot;\nFailed to find or open the pico driver device.&quot; &lt;&lt; std::endl;
        return 1;
    }

    MessageBox(NULL, L&quot;test&quot;, L&quot;test&quot;, 0);

    return 0;
}
</code></pre>
<p><code>FindPicoDriverDevice()</code> 이 함수는 root_path/include/utils.hpp 에 작성해두었다. <code>GUID</code>를 활용하여 Device Handle을 얻어오는 함수이다. 
빌드를 한 후 vm으로 파일을 옮겨서 테스트 해보았다.</p>
<p><a href="https://github.com/wangki-kyu/pico_usb_vendor">driver github</a></p>
<hr>
<h3 id="문제-발생">문제 발생</h3>
<p>LED를 켜기 위해서는 <code>0x01</code> 1바이트를 던져주면 되는데 pico에서 반응이 없었다. 그리고 device handle을 얻은 뒤 최초 ioctl 요청은 성공했는데 두 번째 요청에서 timeout을 걸어도 아무런 반응이 없었다. windbg로 디버깅을 해도 잡을 수 없었다.</p>
<h4 id="하드웨어-스펙-차이-명시">하드웨어 스펙 차이 명시</h4>
<p>일단 첫 번째 문제는 하드웨어 타겟에 맞지 않는 PIN 번호 설정이었다. 실제로 내가 산 pico 보드는 <code>Waveshare RP2040 Zero</code> 모델인데 기본 Pico 보드의 내장 led pin을 사용하고 있어서 25 -&gt; 16으로 변경해주었다. </p>
<h4 id="led-구동-방식의-차이ws2812b">LED 구동 방식의 차이(WS2812B)</h4>
<p>일반적인 LED는 단순히 High/Low 신호만 주면 켜지지만, WS2812B ARGB는 데이터라인 하나로 색상 정보를 직렬 전송해야 하는 프로토콜 기반 LED이다. 단순히 핀을 켜는 것이 아니라, Pico SDK에서 제공하는 PIO나 전용 라이브러리를 사용하여 타이밍에 맞춰 데이터를 밀어 넣어줘야 한다. </p>
<h4 id="초기화-시-동작-확인">초기화 시 동작 확인</h4>
<p>보드가 정상적으로 동작하고 로드가 되었는지 확인하기 위해서 LED 점멸 3회를 하도록 코드에 넣어줬다. </p>
<pre><code class="language-c">for (int i = 0; i &lt; 3; i++) {
        // Turn LED on
        led_on();
        sleep_ms(200);  // Wait 200ms with LED on

        // Turn LED off
        led_off();
        sleep_ms(200);  // Wait 200ms with LED off
    }</code></pre>
<h4 id="buffer-초기화">buffer 초기화</h4>
<blockquote>
<p>첫 번째 LED ON 명령: 성공 
두 번째 LED OFF 명령: 무한 대기 (타임아웃)</p>
</blockquote>
<p>TinyUSB의 OUT endpont 수신 콜백에서 하드웨어 버퍼를 명시적으로 비우지 않아 발생한 문제이다. </p>
<pre><code class="language-c">// 기존
void tud_vendor_rx_cb(uint8_t itf, uint8_t const* buffer, uint16_t bufsize) {
    // buffer 파라미터로만 데이터 읽음
    uint8_t command = buffer[0];

    // ❌ 하드웨어의 실제 버퍼는 여전히 &quot;데이터 있음&quot; 상태
    // ❌ OUT endpoint는 &quot;아직 처리 중&quot; 상태로 유지됨
}</code></pre>
<pre><code class="language-c">// 수정 후 
void tud_vendor_rx_cb(uint8_t itf, uint8_t const* buffer, uint16_t bufsize) {
    // Explicitly read from buffer to clear hardware buffer and arm OUT endpoint
    uint8_t dummy[64];
    tud_vendor_n_read(itf, dummy, bufsize);

    // Extract command byte from read data
    uint8_t command = dummy[0];

    // Queue command for main loop to process
    // This avoids blocking operations in USB interrupt handler
    if (command &lt;= 0x02) {  // Valid commands: 0x00, 0x01, 0x02
        pending_command = command;
    }
}</code></pre>
<p><code>tud_vendor_n_read</code> 을 명시적으로 호출해 주어서 매개변수로 받아온 버퍼의 데이터를 임시 버퍼로 옮겨준다. 물론 성능을 위해서는 최소한의 copy를 해야 하지만 지금은 테스트성으로 만드는 것이므로 추후에 성능 최적화는 할 예정이다.</p>
<hr>
<h3 id="문제-해결">문제 해결</h3>
<p>하드웨어 버퍼를 명시적으로 비우니 정상적으로 동작이 되었다. 간단하게 MFC로 만들어서 테스트도 해보았다. </p>
<h4 id="테스트-영상">테스트 영상</h4>
<iframe width="315" height="560" 
src="https://www.youtube.com/embed/gdbmGLJYa24" 
title="YouTube video player" frameborder="0" 
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
allowfullscreen></iframe>

<p><a href="https://github.com/wangki-kyu/pico_usb_vendor">https://github.com/wangki-kyu/pico_usb_vendor</a>
<a href="https://github.com/wangki-kyu/pico_driver">https://github.com/wangki-kyu/pico_driver</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Windows Driver pico rp2040 Vendor Interface 등록하기]]></title>
            <link>https://velog.io/@wang_ki/Windows-Driver-pico-rp2040-Vendor-Interface-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@wang_ki/Windows-Driver-pico-rp2040-Vendor-Interface-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 08 Apr 2026 11:22:45 GMT</pubDate>
            <description><![CDATA[<p>pico를 구매하여 tinyusb로 vendor interface를 설정할 예정이다. kmdf로 만든 드라이버로 endpoint로 bulk 통신을 할 것이다. 이전에 구매한 아두이노 우노 r3의 경우에는 기본적으로 cdc 인터페이스를 사용하여 kmdf로 만든 커스텀 드라이버로는 디스크립터를 읽어 엔드포인트를 얻어와 파이프 핸들까지는 얻어오는데 성공했지만 통신하는 것은 다소 무리가 있었다. 이번에는 엔드포인트로 led 제어하는 것을 해볼 예정이다. </p>
<hr>
<h3 id="pico-펌웨어-올리기">pico 펌웨어 올리기</h3>
<p>귀여운 pico를 구매하였다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/3494983c-460d-4486-9184-719d3f8617bf/image.png" alt=""></p>
<p>아두이노는 Arduino IDE에서 직접 파일을 올렸었는데 pico는 UF2라는 부트로더 방식으로 usb 메모리에 파일을 옮겨서 펌웨어를 올린다고 한다. 기본적으로 올려야 하는 뼈대를 만들어서 올리기 위해서 <code>vscode</code>에서 <code>Raspberry Pi Pico</code> Extension을 설치하였다. 설치를 하게 되면 아래처럼 나온다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/eb2ae706-54c9-40a6-ad0d-d16705bd17d5/image.png" alt=""></p>
<p><code>New C/C++ Project</code>를 선택하여 project를 만들어주었다.</p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/6b740c03-ea90-481d-b167-8700c649d897/image.png" alt="">
필요한 부분만 체크하여 만들었다. <em>처음 해보는거라서 익숙치 않다...</em> </p>
<p>또한 빌드에 필요한 도구들을 설치해야한다. </p>
<blockquote>
<ol>
<li>CMAKE</li>
<li>MAKE</li>
<li>GNU Arm Embeded Toolchain</li>
</ol>
</blockquote>
<p>3개를 모두 설치하면된다. </p>
<p><a href="https://github.com/wangki-kyu/pico_usb_vendor">https://github.com/wangki-kyu/pico_usb_vendor</a> 테스트 소스는 여기서 확인할 수 있다. 필수 도구들을 설치한 뒤 cmake를 활용해 빌드 구성을 만들고 make로 빌드를 한다.</p>
<blockquote>
<p>cmake --preset default
cd build
make </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/cec13dad-823d-443d-9b45-f57dc3c906a3/image.png" alt=""></p>
<p><code>pico_test.utf2</code> 파일이 생성되었다. pico의 boot 버튼을 누른 상태에서 usb를 연결 후, 잡힌 F 디스크에 파일을 넣어주었더니 자동으로 부트가 되었다. </p>
<hr>
<h3 id="pico를-vm으로-usb-리다이렉트-하기">pico를 VM으로 usb 리다이렉트 하기</h3>
<p>최종 목표는 kmdf로 드라이버를 만들어야하는 것이므로 호스트 pc에서 테스트를 할 수 없다. 따라서 pico를 vm에서 인식을 시켜야한다. 아두이노에서 해봤기 때문에 쉽게 할 수 있을 줄 알았다. 그런데 문제가 발생했다. <a href="https://forums.virtualbox.org/viewtopic.php?t=84040">이 사이트 </a>에 나온 이슈처럼 busy하다고 나오면서 이미 호스트 pc의 장치관리자에서 점유를 하고 있어 vm에서 장치 인식이 안되는 이슈가 있었다. 약 3시간 정도 삽질을 하다가 펌웨어 문제인가? 라고 생각을 하고 다시 소스를 확인했다. </p>
<p>USB 호스트는 기본 enumeration 과정에서 device의 string descriptor(제조사명, 제품명, 시리얼번호 등)를 요청한다. 이때 descriptor 데이터의 포맷이 USB 스페을 준수하지 않으면, 호스트의 USB 스택이 올바른 응답을 받지 못해 계속해서 재시도하게 되고, 이로 인해 device가 &quot;busy&quot; 상태에 머물러 있게 될 수 있다고 한다. </p>
<p>간단하게 테스트용으로 만드느라 아래와 같이 설정했었다.</p>
<pre><code class="language-c">// before
const char string_manufacturer[] = &quot;TestMfg&quot;;
const char string_product[] = &quot;Pico USB Device&quot;;
const char string_serial[] = &quot;123456&quot;;

const uint8_t *desc_strings[4] = {
    NULL
};</code></pre>
<h4 id="descriptor-포맷-변경">descriptor 포맷 변경</h4>
<pre><code class="language-c">// after
// Language descriptor (Index 0)
static const uint16_t _desc_str_langid[] = {
    (uint16_t) ((TUSB_DESC_STRING &lt;&lt; 8) | (2 + 2)),
    0x0409  // English US
};

// Manufacturer string descriptor (Index 1)
static const uint16_t _desc_str_manufacturer[] = {
    (uint16_t) ((TUSB_DESC_STRING &lt;&lt; 8) | (2 + 2*7)),
    &#39;T&#39;, &#39;e&#39;, &#39;s&#39;, &#39;t&#39;, &#39;M&#39;, &#39;f&#39;, &#39;g&#39;, 0
};

// Product string descriptor (Index 2)
static const uint16_t _desc_str_product[] = {
    (uint16_t) ((TUSB_DESC_STRING &lt;&lt; 8) | (2 + 2*15)),
    &#39;P&#39;, &#39;i&#39;, &#39;c&#39;, &#39;o&#39;, &#39; &#39;, &#39;U&#39;, &#39;S&#39;, &#39;B&#39;, &#39; &#39;, &#39;D&#39;, &#39;e&#39;, &#39;v&#39;, &#39;i&#39;, &#39;c&#39;, &#39;e&#39;, 0
};

// Serial number string descriptor (Index 3)
static const uint16_t _desc_str_serial[] = {
    (uint16_t) ((TUSB_DESC_STRING &lt;&lt; 8) | (2 + 2*6)),
    &#39;1&#39;, &#39;2&#39;, &#39;3&#39;, &#39;4&#39;, &#39;5&#39;, &#39;6&#39;, 0
};</code></pre>
<h4 id="descriptor-callback-함수-개선">descriptor callback 함수 개선</h4>
<pre><code class="language-c">// before
const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
    (void) langid;
    return NULL;
}</code></pre>
<pre><code class="language-c">// after
const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
    (void) langid;

    // Index 0 always returns language descriptor
    if (index == 0) {
        return _desc_str_langid;
    }

    // Return string descriptor for valid indices
    if (index &lt; sizeof(_desc_string_table) / sizeof(_desc_string_table[0])) {
        return _desc_string_table[index];
    }

    return NULL;
}</code></pre>
<p>USB 호스트가 descriptor를 요청할 때, 이전 코드에서 항상 NULL을 반환했기 때문에, 호스트는 string descriptor를 받지 못하고 이를 enumeration 실패의 신호로 받아들여 계속 재시도했다. </p>
<hr>
<h3 id="vm에서의-장치관리자-확인">vm에서의 장치관리자 확인</h3>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/7bda8325-4cbc-4c01-9c49-7b30b66d5dc3/image.png" alt="">
하드웨어 ID를 보니 VID/PID를 확인할 수 있다. <code>USB Device Viewer</code>로 확인하니 </p>
<pre><code>[Port2] 


Is Port User Connectable:         yes
Is Port Debug Capable:            no
Companion Port Number:            10
Companion Hub Symbolic Link Name: USB#ROOT_HUB30#4&amp;24054718&amp;0&amp;0#{f18a0e88-c30c-11d0-8815-00a0c906bed8}
Protocols Supported:
 USB 1.1:                         yes
 USB 2.0:                         yes
 USB 3.0:                         no


       ---===&gt;Device Information&lt;===---
String Descriptor for index 2 not available while device is in low power state.

ConnectionStatus:                  
Current Config Value:              0x00  -&gt; Device Bus Speed: Full (is not SuperSpeed or higher capable)
Device Address:                    0x02
Open Pipes:                           0
*!*ERROR:  No open pipes!

          ===&gt;Device Descriptor&lt;===
bLength:                           0x12
bDescriptorType:                   0x01
bcdUSB:                          0x0200
bDeviceClass:                      0x00  -&gt; This is an Interface Class Defined Device
bDeviceSubClass:                   0x00
bDeviceProtocol:                   0x00
bMaxPacketSize0:                   0x40 = (64) Bytes
idVendor:                        0xCAFE = Vendor ID not listed with USB.org
idProduct:                       0x4005
bcdDevice:                       0x0100
iManufacturer:                     0x01
String Descriptor for index 1 not available while device is in low power state.
iProduct:                          0x02
String Descriptor for index 2 not available while device is in low power state.
iSerialNumber:                     0x03
String Descriptor for index 3 not available while device is in low power state.
bNumConfigurations:                0x01

       ---===&gt;Full Configuration Descriptor&lt;===---

          ===&gt;Configuration Descriptor&lt;===
bLength:                           0x09
bDescriptorType:                   0x02
wTotalLength:                    0x0020  -&gt; Validated
bNumInterfaces:                    0x01
bConfigurationValue:               0x01
iConfiguration:                    0x00
bmAttributes:                      0x80  -&gt; Bus Powered
MaxPower:                          0x32 = 100 mA

          ===&gt;Interface Descriptor&lt;===
bLength:                           0x09
bDescriptorType:                   0x04
bInterfaceNumber:                  0x00
bAlternateSetting:                 0x00
bNumEndpoints:                     0x02
bInterfaceClass:                   0xFF  -&gt; Interface Class Unknown to USBView
bInterfaceSubClass:                0x00
bInterfaceProtocol:                0x00
iInterface:                        0x00

          ===&gt;Endpoint Descriptor&lt;===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x81  -&gt; Direction: IN - EndpointID: 1
bmAttributes:                      0x02  -&gt; Bulk Transfer Type
wMaxPacketSize:                  0x0040 = 0x40 bytes
bInterval:                         0x00

          ===&gt;Endpoint Descriptor&lt;===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x01  -&gt; Direction: OUT - EndpointID: 1
bmAttributes:                      0x02  -&gt; Bulk Transfer Type
wMaxPacketSize:                  0x0040 = 0x40 bytes
bInterval:                         0x00
</code></pre><p>하위 계층 구조와 상세 명세를 시각적으로 확인하니 드라이버를 아직 올리지않아 발생한 문제들이 있는 것 같다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>환경 세팅이 제일 고통스럽다. 세팅이 완료되었으니 kmdf로 pico를 제어하는 드라이버를 만들어봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver pnp 매니저]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-pnp-%EB%A7%A4%EB%8B%88%EC%A0%80</link>
            <guid>https://velog.io/@wang_ki/windows-driver-pnp-%EB%A7%A4%EB%8B%88%EC%A0%80</guid>
            <pubDate>Sun, 29 Mar 2026 13:44:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/wang_ki/post/fdf82a4c-1eeb-42f0-80eb-b964fbd4d83f/image.png" alt=""></p>
<p>Windows PnP 드라이버 아키텍처의 흐름을 이해하기 위해서 작성한다. </p>
<hr>
<h3 id="버스-드라이버가-하드웨어-장치-발견">버스 드라이버가 하드웨어 장치 발견</h3>
<hr>
<p><code>pci.sys</code>같은 드라이버가 하드웨어 장치를 발견하면 PDO를 생성해주고 Hardware ID를 할당해준다.</p>
<h3 id="inf-파일과-하드웨어-id-매칭">INF 파일과 하드웨어 ID 매칭</h3>
<hr>
<p>INF 파일에서 Hardware ID를 매칭해 드라이버를 선택하여 실행해 준다. </p>
<h3 id="driverentry-실행">DriverEntry 실행</h3>
<hr>
<p>DriverEntry에서 AddDevice 콜백 등록, 필요한 Dispatch Routine 등록을 해준다. 특히 Unload와 AddDevice는 리소스 해제를 위해 꼭 추가해 주어야 한다. </p>
<h3 id="deviceobject-생성-및-attach">DeviceObject 생성 및 Attach</h3>
<hr>
<p>AddDevice 콜백 내부에서 Device Object 생성해 준다. 이때 Device Extension도 할당해 주어야 한다. 유저 모드 어플리케이션에서 Device에 접근하기 위해 심볼릭 링크도 생성해 준다. 매개변수로 받는 PDO 위에 Attach 해주면 된다. </p>
<h3 id="irp_mn_start_device-후처리에서-하드웨어-리소스-획득">IRP_MN_START_DEVICE 후처리에서 하드웨어 리소스 획득</h3>
<hr>
<p>AddDevice가 완료된 후, PnP Manager가 I/O Manger를 통해서 IRP를 드라이버로 보내주게 되는데 <code>IRP_MJ_PNP</code> 하위의 <code>IRP_MN_START_DEVICE</code>를 받게 된다. 후처리를 통해서 필요한 하드웨어 리소스를 획득하여 Device Extension에 저장하면 나중에 사용할 수 있다. 얻어온 하드웨어의 베이스 물리 주소는 MmMapIoSpace을 통해 가상 주소로 변경하여 사용해야 한다. </p>
<h3 id="결론">결론</h3>
<hr>
<p>Windows Driver를 공부하고 있는데 완벽하게 이해가 가지는 않지만 흐름을 잡기 위해서 위 내용을 작성했다. PCIe 장치가 PnP 메커니즘으로 os에 하드웨어로 인식되는 방법에 대해서 간단히 알아보았다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver AddDevice]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-AddDevice</link>
            <guid>https://velog.io/@wang_ki/windows-driver-AddDevice</guid>
            <pubDate>Fri, 27 Mar 2026 13:27:17 GMT</pubDate>
            <description><![CDATA[<p>장치를 추가할 때 DeviceStack에 참여하는 콜백함수를 등록하고 windbg로 디버깅을 했다. </p>
<hr>
<h3 id="driverentry">DriverEntry</h3>
<hr>
<pre><code class="language-c">NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObj, PUNICODE_STRING pRegPath)
{
    pRegPath = pRegPath;

    pDrvObj-&gt;MajorFunction[IRP_MJ_PNP] 
        = SampleDriverPnPDispatch;  

    pDrvObj-&gt;DriverExtension-&gt;AddDevice = SampleDriverAddDevice; 
    pDrvObj-&gt;DriverUnload = SampleDriverUnload;

    return STATUS_SUCCESS;
}</code></pre>
<h3 id="adddevice-callback-함수">AddDevice Callback 함수</h3>
<hr>
<pre><code class="language-c">NTSTATUS SampleDriverAddDevice(PDRIVER_OBJECT pDrvObj, PDEVICE_OBJECT pPhysicalDeviceObject)
{
    PDEVICE_OBJECT pDeviceObject = NULL;
    PDEVICE_EXTENSION pDeviceExtension = NULL;
    NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;

    // DeviceObject 생성
    ntStatus = IoCreateDevice(
        pDrvObj,
        sizeof(DEVICE_EXTENSION),
        NULL,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &amp;pDeviceObject
    );
    if (!NT_SUCCESS(ntStatus))
    {
        goto exit;
    }

    pDeviceExtension = (PDEVICE_EXTENSION)pDeviceObject-&gt;DeviceExtension;

    pDeviceExtension-&gt;pNextLayerDeviceObject = 
        IoAttachDeviceToDeviceStack(pDeviceObject, pPhysicalDeviceObject);

    pDeviceObject-&gt;Flags &amp;= ~DO_DEVICE_INITIALIZING;
    ntStatus = STATUS_SUCCESS;

exit:
    return ntStatus;
}</code></pre>
<h3 id="windbg-디버깅">windbg 디버깅</h3>
<hr>
<p>장치 관리자의 시스템 장치에 등록한 <code>SampleDevice</code>를 사용하기로 누르면 windbg에 설정한 break point를 히트하게 된다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/859c6f3e-3ac5-4a9c-b05f-b7b55929761a/image.png" alt=""></p>
<p>DrvierEntry 함수에 브레이크 포인트를 걸었다.
<code>bp pnpsample!DriverEntry</code></p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/054ced08-62cd-47a0-9837-d77e465be9e2/image.png" alt=""></p>
<p>SampleDriverAddDevice함수를 탔다. 로컬 변수인 <code>pDeviceObject</code>가 무엇을 가르키는지 windbg에서 확인하려면 <code>dv</code> 명령어를 입력하면 된다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/6f784117-58ec-41c6-a5c8-945b9e396d3d/image.png" alt="">
현재는 NULL을가르키는 것을 확인할 수 있다.  </p>
<pre><code class="language-c"> ntStatus = IoCreateDevice(
        pDrvObj,
        sizeof(DEVICE_EXTENSION),
        NULL,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &amp;pDeviceObject
    );
    if (!NT_SUCCESS(ntStatus))
    {
        goto exit;
    }
</code></pre>
<p><code>IoCreateDevice</code>를 호출하여 <code>pDeviceObject</code>에 값을 넣어준 뒤, 다시 <code>dv</code> 명령어로 확인하면 주소값이 들어간 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/6053f1fd-e23c-4e00-938d-fe113f6e303b/image.png" alt=""></p>
<p>다음은 <code>DEVICE_EXTENSION</code>의 주소값을 가져와서 <code>PDEVICE_OBJECT pNextLayerDeviceObject</code> 의 값을 채워준다.</p>
<pre><code class="language-c">    pDeviceExtension-&gt;pNextLayerDeviceObject = 
        IoAttachDeviceToDeviceStack(pDeviceObject, pPhysicalDeviceObject);</code></pre>
<p><code>IoAttachDeviceToDeviceStack</code>의 반환 값은 현재 추가한 디바이스 오브젝트 이전 최상위 디바이스 오브젝트를 반환해준다. </p>
<p>그전에 <code>PDO</code>의 구조체를 확인하기 위해서 <code>dt _DEVICE_OBJECT 0xffffb78f20289860</code> 명령어를 입력해주면 <code>PDO</code>에 대한 필드들이 나온다. </p>
<p><code>PDO</code>가 root stack으로 쌓이기 때문에 만약에 <code>PDO</code>의 <code>AttachedDevice</code> 필드가 NULL이라면 현재 추가하려는 <code>DeviceObject</code>가 <code>PDO</code>위에 올라가게 될 것이다.
<img src="https://velog.velcdn.com/images/wang_ki/post/3c080442-2f52-4d2b-afe6-988b82a9db5f/image.png" alt=""></p>
<p>windbg로 확인결과 null인 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/7f2c4e1c-48f5-494e-b409-0f66aa64382a/image.png" alt=""></p>
<p>IoAttachDeviceToDeviceStack 호출 후 필드 확인 결과 생성한 <code>pDeviceObject</code>인 것을 확인할 수 있다. 또한 반환된 <code>pDeviceExtension-&gt;pNextLayerDeviceObject</code> 이 값을 확인하면 <code>PDO</code>의 주소와 같은 것을 확인할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/0ae8067f-e9c1-4137-a655-a7f65a0310c4/image.png" alt=""></p>
<pre><code class="language-c">    pDeviceObject-&gt;Flags &amp;= ~DO_DEVICE_INITIALIZING;</code></pre>
<p>플래그 값을 위와 같이 설정해주는 이유는 생성한 디바이스위에도 올라갈 수 있도록 하는 것이라고 한다. FLAGS 필드를 확인하면 아래와 같이 0으로 변하는것을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/471cf88c-b278-4564-b5d5-aa0af014b596/image.png" alt=""></p>
<h3 id="결론">결론</h3>
<hr>
<p>간단한 <code>pnp</code> 드라이버를 만들고 하드웨어 스택이 어떻게 생성되는지 windbg를 통해 실제 동작을 확인하였다. 좀 어렵고 생소하지만 계속해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows Driver WinObj 활용]]></title>
            <link>https://velog.io/@wang_ki/windows-Driver-WinObj-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@wang_ki/windows-Driver-WinObj-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Wed, 25 Mar 2026 05:22:12 GMT</pubDate>
            <description><![CDATA[<p>windows driver의 Device Object를 생성하고 어플리케이션 레벨에서 연결하기 위해서는 <code>CreateFile</code>을 호출하여 Handle을 얻어야 한다. <code>Symbol Name</code>을 어떻게 생성하고 생성된 Symbol을 WinObj로 확인하는 과정을 실습해 보았다. </p>
<hr>
<h3 id="1-device-object-생성">1. Device Object 생성</h3>
<hr>
<pre><code class="language-c">NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObj, PUNICODE_STRING pRegistryPath) {
    PDEVICE_OBJECT DeviceObject = NULL;
    NTSTATUS ntStatus;
    UNICODE_STRING DeviceName;
    UNICODE_STRING SymbolicLinkName;

    pRegistryPath = pRegistryPath;

    pDrvObj-&gt;DriverUnload = SampleDriverUnload;

    // DeviceObject 생성
    RtlInitUnicodeString(&amp;DeviceName, L&quot;\\Device\\SAMPLE&quot;);    // 대소문자 상관없음 
    ntStatus = IoCreateDevice(
        pDrvObj,
        0,
        &amp;DeviceName,
        FILE_DEVICE_UNKNOWN,
        0,
        FALSE,
        &amp;DeviceObject
    );

    RtlInitUnicodeString(&amp;SymbolicLinkName, L&quot;\\DosDevices\\MYSAMPLE&quot;);    
    IoCreateSymbolicLink(&amp;SymbolicLinkName, &amp;DeviceName);    // 유저 모드에서 접근할 수 있도록 설정해주는 것이다. 

    return STATUS_SUCCESS;
}</code></pre>
<h3 id="2-device-object-리소스-해제">2. Device Object 리소스 해제</h3>
<hr>
<pre><code class="language-c">VOID SampleDriverUnload(PDRIVER_OBJECT pDriverObj) {
    UNICODE_STRING SymbolicLinkName;

    KdPrint((&quot;SampleDriveUnload&quot;));
    pDriverObj = pDriverObj;

    // device symbol link 삭제 

    RtlInitUnicodeString(&amp;SymbolicLinkName, L&quot;\\DosDevices\\MYSAMPLE&quot;);
    IoDeleteSymbolicLink(&amp;SymbolicLinkName);
    // device object 삭제 
    IoDeleteDevice(pDriverObj-&gt;DeviceObject);
}</code></pre>
<h3 id="3-vm에서-driver-로드">3. vm에서 driver 로드</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/b46ab315-725d-4501-9333-60d5a6e729c7/image.png" alt=""></p>
<h3 id="4-winobj-실행하여-symbol-확인">4. WinObj 실행하여 Symbol 확인</h3>
<hr>
<blockquote>
<p>WinObj는 <a href="https://learn.microsoft.com/en-us/sysinternals/downloads/winobj">링크</a> 에서 다운로드 할 수 있음.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/1dd9a319-2f57-40ec-8fcd-a4fee8ed965f/image.png" alt=""></p>
<p>등록한 <code>MYSAMPLE</code>을 확인할 수 있다. </p>
<h3 id="5-driver-해제-후-symbol-확인">5. driver 해제 후 symbol 확인</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/04b27521-a0fa-4d0a-a5e6-5bc359eab7ff/image.png" alt=""></p>
<p>WinObj에서 MYSAMPLE을 찾을 수 없다.</p>
<h3 id="6-결론">6. 결론</h3>
<p>어플리케이션을 개발할 때 <code>CreateFile</code>에 symbol을 넣어 핸들을 얻어와 사용한 적이 있다. 실제로 어떻게 내부적으로 심볼을 생성하는지 알 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windbg kernel debugging 환경 세팅]]></title>
            <link>https://velog.io/@wang_ki/windbg-kernel-debugging-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@wang_ki/windbg-kernel-debugging-%ED%99%98%EA%B2%BD-%EC%84%B8%ED%8C%85</guid>
            <pubDate>Tue, 24 Mar 2026 11:51:55 GMT</pubDate>
            <description><![CDATA[<p>환경 세팅을 하며 실습한 내용에 대해서 간략히 작성한다.</p>
<p>반드시 <strong>관리자 권한</strong> 터미널에서 실행한다. </p>
<h3 id="1-target-pc-setting">1. target pc setting</h3>
<hr>
<ol>
<li><p>테스트 서명 활성화</p>
<pre><code class="language-jsx"> # 관리자 권한으로 실행

 bcdedit /set testsigning on</code></pre>
<p> windows driver의 경우 인증을 받지 못한 경우, 드라이버 등록이 되지 않는다. 따라서 테스트를 위해서는 <code>testsigning</code>을 활성화 시켜줘야한다.  우회하기 위한 필수 설정이다. </p>
</li>
<li><p>디버깅 모드 활성화</p>
<pre><code class="language-jsx"> bcdedit /debug on</code></pre>
<p> 이 설정을 켜두면 시스템 보안이 매우 취약해진다. 누군가 포트만 알면 내 커널 메모리를 다 들여다볼 수 있기 때문이다. 그래서 실제 사용하는 PC에서는 절대 켜지 말고, 반드시 테스트용 VM 에서만 켜야한다. </p>
</li>
<li><p>네트워크 디버깅 설정(KDNET)</p>
<pre><code class="language-jsx"> # 키값 생성하기
 // target pc에서 host pc ip를 넣어줘야함
 bcdedit /dbgsettings net hostip:192.168.1.104 port:50000 </code></pre>
<p> host pc에서 windbg로 커널에 붙기위한 키값을 생성하는 명령어이다. </p>
</li>
<li><p>설정 확인 및 재부팅</p>
<pre><code class="language-jsx"> bcdedit /dbgsettings # 설정된 Key값과 Ip 확인 </code></pre>
</li>
</ol>
<p>windbg의 경우에 예전에는 pipe통신으로 가상 Serial Port를 만들어서 연결했지만 요즘은 network로 연결하는게 속도가 더 빠르다고 한다. </p>
<p><a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/setting-up-a-network-debugging-connection">Set Up KDNET Network Kernel Debugging Manually - Windows drivers</a></p>
<h3 id="2-host-pc-분석용-pc-설정">2. Host PC (분석용 PC) 설정</h3>
<hr>
<ol>
<li><p>Windbg symbol 설정하기 </p>
<pre><code class="language-jsx">  settings -&gt; Debugging Settings -&gt; Default symbol path

  srv*c:\symbols\*http://msdl.microsoft.com/download/symbols
  // path는 개별 설정</code></pre>
</li>
<li><p>Windbg Attach to Kernel</p>
<p> 생성한 키를 key에넣어준다.</p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/8f7c0c4d-08f1-4c1c-bbab-67bd5e40e39d/image.png" alt=""></p>
</li>
</ol>
<h3 id="3-간단한-wdf-프로젝트-생성">3. 간단한 WDF 프로젝트 생성</h3>
<hr>
<ol>
<li><p>wdf 프로젝트 생성한다.</p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/ca1c283f-b560-4860-9e77-a772751a51d7/image.png" alt=""></p>
</li>
</ol>
<ol start="2">
<li><p>inf 파일 삭제 </p>
<p> <img src="https://velog.velcdn.com/images/wang_ki/post/88391ef3-e1d0-460a-8171-29a638797602/image.png" alt=""></p>
</li>
</ol>
<ol start="3">
<li><p>테스트 코드 작성</p>
<pre><code class="language-c"> #include &lt;ntddk.h&gt;

 NTSTATUS DriverEntry(PDRIVER_OBJECT pDrvObj, PUNICODE_STRING pRegistryPath) {
     pDrvObj = pDrvObj;
     pRegistryPath = pRegistryPath;

     return STATUS_UNSUCCESSFUL;
 }</code></pre>
</li>
<li><p>빌드 </p>
</li>
</ol>
<h3 id="4-테스트">4. 테스트</h3>
<hr>
<p>vm으로 <code>.sys</code> 파일을 옮기고 함께 생성된 <code>pdb</code> 파일을 symbol path에 넣어준다. </p>
<ol>
<li><p>테스트 vm 에서 <code>sc</code> 명령어로 드라이버를 등록해준다. </p>
<pre><code class="language-c"> sc.exe create sample binpath= z:\sample.sys type= kernel start= demand</code></pre>
</li>
<li><p>host pc에서 windbg로 breakpoint를 설정한다. </p>
<pre><code class="language-c"> bp sample!DriverEntry</code></pre>
</li>
</ol>
<p>  <img src="https://velog.velcdn.com/images/wang_ki/post/f07dadfa-bf46-4db4-9c5f-b0ed3cf5e01d/image.png" alt=""></p>
<pre><code>아직 드라이버를 메모리에 로드하기 전이라서 찾을 수 없다고 나온다. </code></pre><ol start="3">
<li><p>test vm에서 드라이브를 실행한다. </p>
<pre><code class="language-c"> sc.exe start sample</code></pre>
</li>
<li><p>windbg break 확인 </p>
<p> <img src="https://velog.velcdn.com/images/wang_ki/post/49c0ae9c-aab0-4af2-b521-49aa0d69cf9f/image.png" alt=""></p>
</li>
</ol>
<pre><code>디버깅을 성공적으로 걸었다. </code></pre><h3 id="5-결론">5. 결론</h3>
<hr>
<p>윈도우 커널 디버깅은 환경 세팅이 절반이라는 소리가 있다. host pc와 target pc를 나누어서 한다는 점이 이전에 원격으로 디버깅을 한 경험이 있어서 익숙했다.</p>
]]></description>
        </item>
    </channel>
</rss>