<?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>Fri, 26 Jun 2026 07:16:46 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 MSI-X 고찰]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-MSI-X-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@wang_ki/windows-driver-MSI-X-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Fri, 26 Jun 2026 07:16:46 GMT</pubDate>
            <description><![CDATA[<p>windows에서는 별다른 설정을 해주지 않으면 기본으로 레거시 인터럽트를 사용한다. 따라서 MSI-X 인터럽트를 사용하기 위해서는 <code>INF</code>파일에 추가로 설정해 줘야 한다. </p>
<hr>
<h3 id="msi-x-설정하기-inf">MSI-X 설정하기 INF</h3>
<pre><code class="language-inf">[npu_driver_HW_Reg]
HKR, &quot;Interrupt Management&quot;, 0x00000010
HKR, &quot;Interrupt Management\MessageSignaledInterruptProperties&quot;, 0x00000010
HKR, &quot;Interrupt Management\MessageSignaledInterruptProperties&quot;, &quot;MSISupported&quot;, 0x00010001, 1
HKR, &quot;Interrupt Management\MessageSignaledInterruptProperties&quot;, &quot;MessageNumberLimit&quot;, 0x00010001, 4</code></pre>
<p><code>INF</code> 파일에 <code>[npu_driver_HW_Reg]</code>섹션에 위처럼 설정하였다. 레지스트리의 <code>컴퓨터\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum</code> Path에 저장이 된다. </p>
<p><code>Enum</code>은 경로는 윈도우가 인식한 모든 하드웨어 장치의 정보가 열거되어 있는 핵심 레지스트리 위치이다. </p>
<p>하위 키 구조로는 <code>버스 유형(PCI 등)</code> -&gt; <code>기기 식별자(ID)</code> -&gt; <code>인스턴스 ID</code> 순으로 내려간다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/ef161b0a-9140-4ac9-848e-695057f2ad05/image.png" alt=""></p>
<p>위 사진을 보면 실제로 <code>google coral tpu</code> 장치가 PCI로 인식이 되어서 하드웨어 하위 키로 인스턴스 ID를 생성하는 것을 확인할 수 있다. </p>
<p><code>HKR, &quot;Interrupt Management&quot;, 0x00000010</code>을 해석해 보면, &quot;<code>0x00000010</code>이라는 플래그 명령에 따라 <code>Device Parameters\Interrupt Management</code> 라는 레지스트리 경로(키)를 만들어라&quot; 이다. </p>
<p><code>HKR, &quot;Interrupt Management\MessageSignaledInterruptProperties&quot;, 0x00000010</code> 이것의 의미도 동일하게 <code>Device Parameters\Interrupt Management\MessageSignaledInterruptProperties</code>을 만들라는 것이다. </p>
<p><code>HKR, &quot;Interrupt Management\MessageSignaledInterruptProperties&quot;, &quot;MSISupported&quot;, 0x00010001, 1</code> 이것의 의미는, &quot;<code>Device Parameters\Interrupt Management\MessageSignaledInterruptProperties</code>으로 접근해서 <code>MSISupported</code>의 값을 만들고 <code>DWORD</code>타입으로 실제 값을 <code>1</code>로 주어라&quot;라는 의미로 해석하면 된다. </p>
<p><code>HKR, &quot;Interrupt Management\MessageSignaledInterruptProperties&quot;, &quot;MessageNumberLimit&quot;, 0x00010001, 4</code> 이것의 의미도 마찬가지로 <code>MessageNumberLimit</code> 값에 실제로 <code>4</code>라는 값을 설정해라는 것이다. </p>
<p>최종적으로 생성된 값들을 확인하면 아래와 같다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/9de20d26-0ba8-49ee-9b27-404d4ed939c2/image.png" alt=""></p>
<p>그러면 이후에 <code>PnP 매니저</code>가 리소스 결정 및 할당할 때, <code>MSISupproted</code>값이 1로 되어있는 것을 확인하게 되면 레거시 인터럽트 방식 대신에 <code>MSI</code>리소스를 할당해 준다. </p>
<hr>
<h3 id="evtdeviceadd에서-interrupt-object-생성">EvtDeviceAdd에서 Interrupt Object 생성</h3>
<p><a href="https://learn.microsoft.com/ko-kr/windows-hardware/drivers/ddi/wdfinterrupt/nf-wdfinterrupt-wdfinterruptcreate">WdfInterruptCreate MSDN</a>
위 <code>WdfInterruptCreate</code>함수의 MSDN을 확인해보니 아래와 같이 쓰여져 있다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/81601d36-aa5a-4346-8a81-1beeb16f80c8/image.png" alt=""></p>
<p>일반적으로는 <code>EvtDeviceAdd</code>에서 인터럽트 객체를 만들지만, <code>EvtDevicePrepareHardware</code>에서도 호출할 수 있다는 것 같다. 이 부분은 좀 더 학습해야 할 부분인 것 같다. 지금은 <code>EvtDeviceAdd</code>에서 생성하는 것을 다루도록 하겠다. </p>
<p><code>INF</code>에 설정한 인터럽트 개수만큼 <code>Object</code>를 생성해 주었다.</p>
<pre><code class="language-c">{
    deviceContext-&gt;IsrCallCount = 0;
    for (ULONG i = 0; i &lt; 4; ++i) {
        WDF_INTERRUPT_CONFIG interruptConfig;
        WDF_INTERRUPT_CONFIG_INIT(&amp;interruptConfig,
            npudriverEvtInterruptIsr,
            npudriverEvtInterruptDpcNew);
        interruptConfig.EvtInterruptEnable  = npudriverEvtInterruptEnable;
        interruptConfig.EvtInterruptDisable = npudriverEvtInterruptDisable;
        status = WdfInterruptCreate(device, &amp;interruptConfig,
            WDF_NO_OBJECT_ATTRIBUTES, &amp;deviceContext-&gt;Interrupts[i]);
        if (!NT_SUCCESS(status)) {
            DbgPrint(&quot;[%s] WdfInterruptCreate[%lu] failed: 0x%x\n&quot;,
                __FUNCTION__, i, status);
            return status;
        }
        DbgPrint(&quot;[%s] WdfInterruptCreate[%lu] OK (Interrupts[%lu]=%p)\n&quot;,
            __FUNCTION__, i, i, deviceContext-&gt;Interrupts[i]);
    }
}</code></pre>
<p>이렇게 해두면 <code>WDF</code>가 내부적으로 인터럽트 오브젝트와 인터럽트 벡터를 연결해서 npu가 인터럽트를 발생시켰을 때, 등록한 <code>ISR</code>이 호출된다고 한다. </p>
<p>등록되는 과정은 추후에 더 자세히 다루도록 하겠다.</p>
<hr>
<h3 id="결론">결론</h3>
<p>npu driver를 개발하며 MSI-X에 대한 개념을 익혔지만 내부적으로 어떻게 동작이 되고 사용이 되는지 좀 더 자세히 알아보았다. </p>
<p>추가로 <code>ISR</code>, <code>DPC</code> 내부에서 로직을 어떻게 처리하는지, <code>WDF</code> 내부에서 어떻게 미리 생성한 Interrupt Object와 연결해 주는지에 대해서 자세히 알아봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver EvtDeviceAdd 탐구 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-EvtDeviceAdd-%ED%83%90%EA%B5%AC</link>
            <guid>https://velog.io/@wang_ki/windows-driver-EvtDeviceAdd-%ED%83%90%EA%B5%AC</guid>
            <pubDate>Wed, 24 Jun 2026 08:15:44 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@wang_ki/windows-driver-inf-%ED%8C%8C%EC%9D%BC-%ED%83%90%EA%B5%AC">inf 등록 과정</a> 포스팅에서 다루었던 과정 이후, I/O 매니저가 실제로 driver를 메모리에 로드하게 된다. <code>DriverEntry</code>가 호출이 되면서 <code>WDF Driver</code> 객체를 만들어준다. 그때 <code>Config</code>로 <code>EvtDeviceAdd</code>를 등록해 준다. 언제 그리고 누가 <code>EvtDeviceAdd</code>를 호출하는지 알아보겠다. 추가로 <code>EvtDeviceAdd</code> 내부에서는 어떤 작업을 해주는지 알아보겠다. </p>
<hr>
<h3 id="evtdeviceadd는-언제-호출이-될까">EvtDeviceAdd는 언제 호출이 될까?</h3>
<p><code>DriverEntry</code>가 끝나면 드라이버는 메모리에 로드되어있지만, 아직 이 드라이버가 제어할 <code>실제 NPU 하드웨어 장치</code>와는 연결되지 않은 상태이다. </p>
<ol>
<li>PDO의 존재 
드라이버가 로드되기 전, PCIe 버스 드라이버는 이미 NPU(디바이스)를 발견하고 커널 내부에 PDO(물리 장치 객체)라는 하드웨어 뼈대를 만들어둔다. </li>
<li>PnP 매니저의 매칭 
PnP 매니저는 <code>DriverEntry</code>를 마친 NPU 드라이버 객체(Driver Object)와 이미 만들어진 PDO를 서로 매칭시켜준다.</li>
<li>EvtDeviceAdd 호출 
PnP 매니저는 <code>DriverEntry</code>가 <code>STATUS_SUCCESS</code>를 반환하면 <code>EvtDeviceAdd</code>를 호출해준다. </li>
</ol>
<hr>
<h3 id="evtdeviceadd의-매개변수">EvtDeviceAdd의 매개변수</h3>
<p>실제 <code>npu_driver</code> 소스를 보겠다. </p>
<pre><code class="language-c">NTSTATUS
npudriverEvtDeviceAdd(
    _In_ WDFDRIVER Driver,
    _Inout_ PWDFDEVICE_INIT DeviceInit
)
{
    NTSTATUS status;

    UNREFERENCED_PARAMETER(Driver);
    PAGED_CODE();    

    DbgPrint(&quot;[%s] Entry\n&quot;, __FUNCTION__);

    status = npudriverCreateDevice(DeviceInit);

    DbgPrint(&quot;[%s] Exit\n&quot;, __FUNCTION__);

    return status;
}</code></pre>
<p>매개변수로 <code>WDFDRIVER</code> 와 <code>PWDFDEVICE_INIT</code>를 받는다. 
특히 <code>PWDFDEVICE_INIT</code>은 생명 주기가 특이한데 <code>WdfDeviceCreate</code>을 호출하기 전에 사용된다. 아래와 같이 사용한다. </p>
<pre><code class="language-c">WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&amp;pnpPowerCallbacks);
pnpPowerCallbacks.EvtDevicePrepareHardware = npudriverEvtDevicePrepareHardware;
pnpPowerCallbacks.EvtDeviceReleaseHardware= npudriverEvtDeviceReleaseHardware;

WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &amp;pnpPowerCallbacks);

// Set up file object config with cleanup callback
WDF_FILEOBJECT_CONFIG_INIT(&amp;fileObjectConfig, NULL, NULL, npudriverEvtFileCleanup);
WdfDeviceInitSetFileObjectConfig(DeviceInit, &amp;fileObjectConfig, WDF_NO_OBJECT_ATTRIBUTES);

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&amp;deviceAttributes, DEVICE_CONTEXT);

status = WdfDeviceCreate(&amp;DeviceInit, &amp;deviceAttributes, &amp;device);</code></pre>
<p>디바이스 생성 전에만 쓰이는 임시 설정 바구니로 알고 있으면 된다. </p>
<p><code>WDFDRIVER</code>는 <code>DriverEntry</code>에서 생성한 객체인데 <code>EvtDeviceAdd</code>에서는 크게 사용할 일이 없어서 <code>UNREFERENCED_PARAMETER</code> 처리해두었다.</p>
<hr>
<h3 id="evtdeviceadd-내부에서-해야하는-일">EvtDeviceAdd 내부에서 해야하는 일</h3>
<p><code>EvtDeviceAdd</code>가 호출되었을 때, 해야 하는 일은 크게 아래와 같다. </p>
<ol>
<li><code>WdfDevice</code> 생성</li>
<li>디바이스 컨텍스트 초기화 </li>
<li>Interrupt 초기화 </li>
<li>I/O Queue 생성 </li>
<li>인터페이스 생성(유저 모드에서 사용)</li>
</ol>
<p>Interrupt 초기화를 하는 이유에 대해서는 <a href="https://velog.io/@wang_ki/windows-driver-MSI-X-%EA%B3%A0%EC%B0%B0">인터럽트 MSI-X에 대한 고찰</a>에서 다룬다. </p>
<p>유저 모드 어플리케이션에서 <code>Device</code>의 핸들을 얻어서 <code>IRP</code>를 발생시키는데, <code>WDF</code>가 <code>IRP</code>를 <code>WDFREQUEST</code>로 래핑을 해준다. <code>Device</code>에 연결된 <code>I/O Queue</code>에 들어가게 되는데, 따라서 Device 생성 시점에, <code>Queue</code>를 만들어준다. </p>
<pre><code class="language-c">// 인터페이스 핸들 생성 
status = WdfDeviceCreateDeviceInterface(device, &amp;GUID_DEVINTERFACE_npudriver, NULL);    

if (NT_SUCCESS(status))
{
    // Initialize the I/O Queue
    WDF_IO_QUEUE_CONFIG queueConfig;
    WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&amp;queueConfig, WdfIoQueueDispatchSequential);
    queueConfig.EvtIoDeviceControl = npudriverEvtIoDeviceControl;

    status = WdfIoQueueCreate(device, &amp;queueConfig, WDF_NO_OBJECT_ATTRIBUTES, &amp;deviceContext-&gt;IoQueue);
    if (!NT_SUCCESS(status)) {
        DbgPrint(&quot;[%s] WdfIoQueueCreate failed: 0x%x\n&quot;, __FUNCTION__, status);
        return status;
    }
}</code></pre>
<p><a href="https://github.com/wangki-kyu/npu_driver/blob/main/npu_driver/Device.c">Device.c 소스</a></p>
<hr>
<h3 id="결론">결론</h3>
<p><code>EvtDeviceAdd</code>가 어느 시점에 호출되고 내부에서 어떤 기능을 수행하는지 알아보았다. 일단 기능이 동작하도록 했었는데 내부적으로 Driver가 어떻게 동작하는지 자세히 알아볼 예정이다. 화이팅!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver inf 파일 탐구 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-inf-%ED%8C%8C%EC%9D%BC-%ED%83%90%EA%B5%AC</link>
            <guid>https://velog.io/@wang_ki/windows-driver-inf-%ED%8C%8C%EC%9D%BC-%ED%83%90%EA%B5%AC</guid>
            <pubDate>Mon, 22 Jun 2026 04:25:08 GMT</pubDate>
            <description><![CDATA[<p>windows에서 드라이버를 설치하기 위해서는 필수 파일인 inf 파일에 대해서 좀 더 자세히 알아보려고 한다. </p>
<hr>
<h3 id="inf-파일은-언제-systemrootinf로-복사가-될까">inf 파일은 언제 %SystemRoot%\Inf로 복사가 될까?</h3>
<p><code>%SystemRoot%</code> -&gt; <code>C:\Windows</code>를 의미</p>
<p>사용자가 장치 관리자에서 <code>드라이버 업데이트</code>를 누르거나, CLI에서 <code>pnputil /add-driver npu_driver.inf</code> 명령어를 실행하는 순간 <code>드라이버 스테이징</code> 또는 <code>사전 설치</code>가 발생한다. 이 과정에서 복사가 일어난다. </p>
<p>장치가 실제로 컴퓨터에 장착되었을 때, OS가 드라이버를 1초 만에 찾아서 로드할 수 있도록 정렬해 두는 것이라고 한다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/f68a3d05-7adb-4199-9801-236a9380382d/image.png" alt=""></p>
<hr>
<h3 id="언제-레지스트리에-등록이-될까">언제 레지스트리에 등록이 될까??</h3>
<p>경로: <code>HKLM\SYSTEM\CurrentControlSet\Services</code></p>
<p>INF 파일이 <code>%SystemRoot%\Inf</code>에 복사되었다고 해서 바로 레지스트리에 서비스가 등록되는 것은 아니다. 레지스트리 등록은 장치락 실제로 시스템에 인식되어 드라이버와 짝지어지는 시점에 일어난다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/e6fd0935-0410-4890-9d6d-2fd650ad5df0/image.png" alt=""></p>
<p>장착된 device의 하드웨어 ID를 추출하여 OS가 <code>%SystemRoot\Inf</code> 폴더에서 찾아서, 해당 하드웨어 ID와 일치하는 <code>[Manufacturer]</code> 섹션을 가진 INF 파일을 찾아낸다. 
일치하는 INF를 찾으면, 그 내부의 명세서대로 레지스트리에 서비스를 생성하고 장치 정보를 등록한다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/3756d6a4-d906-4bdf-93d3-3b30e29939db/image.png" alt="">
실제로 npu를 장착하고 장치 관리자에서 하드웨어 ID를 확인하면 위 사진과 같다. 
그렇다면 INF 파일의 <code>[Manufacturer]</code> 섹션을 확인해 보겠다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/5f5cccb5-1d05-4a74-9cd0-66ca888a9bad/image.png" alt=""></p>
<p><code>Manufacturer</code> 섹션을 확인해 보면 모델 이름이 나오고 해당 모델 이름을 가진 섹션을 찾아가 보면 <code>[Standard.NTamd64.10.0...16299]</code>을 확인할 수 있다. 
내용을 보면 <code>PCI\VEN_1AC1&amp;DEV_089A&amp;SUBSYS_089A1AC1&amp;REV_00</code>으로 나와있는 것을 확인할 수 있는데 이 부분은 개발자가 하드웨어 ID를 입력해서 넣어준 부분이다. 따라서 <code>npu device</code>에 올릴 드라이버를 지정해 준 것으로 볼 수 있다. </p>
<hr>
<h3 id="레지스트리에-등록되는-내용은-뭘까">레지스트리에 등록되는 내용은 뭘까?</h3>
<p><code>HKLM\SYSTEM\CurrentControlSet\Services</code>를 살펴보면 npu_driver가 레지스트리에 등록된 것을 확인할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/9f0da93e-a3b1-48e5-92d8-3bc22ce5f13c/image.png" alt=""></p>
<h4 id="enum">Enum</h4>
<p><code>Enum</code>키는 드라이버가 현재 어떤 실제 하드웨어 장치들을 지배하고 있는지 역으로 추적할 수 있게 해주는 연결고리라고 보면 된다.</p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/c59844ec-2de6-48db-a1be-48fe31920365/image.png" alt=""></p>
<p><code>0</code>의 경우 장치 인스턴스 ID를 저장한다. 현재는 npu 카드 하나만 꽂았기 때문에 <code>0</code>만 존재하지만 여러 카드를 꽂게 되면 여러 개로 등록된다고 한다. </p>
<h4 id="parameters">Parameters</h4>
<p><code>parameters</code>키는 온전히 <code>NPU 드라이버 개발자</code>를 위한 키라고 보면 된다.
드라이버가 로드될 때 참조해야 하는 각종 커스텀 설정이나 환경 변수 값들을 저장하는 곳이다. </p>
<hr>
<h3 id="driver-install-sections">driver install sections</h3>
<pre><code>; -------------- npu_driver driver install sections
[npu_driver_Service_Inst]
DisplayName    = %npu_driver.SVCDESC%
ServiceType    = 1               ; SERVICE_KERNEL_DRIVER
StartType      = 3               ; SERVICE_DEMAND_START
ErrorControl   = 1               ; SERVICE_ERROR_NORMAL
ServiceBinary  = %13%\npu_driver.sys</code></pre><p>위 내용은 <code>Pnp</code>관리자가 설치 단계에서 레지스트리에 서비스 명세서를 적을 때 사용이 된다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/b85ccc3a-842d-48c5-bbb5-14a1a73a9159/image.png" alt=""></p>
<p>위에 레지스트리 값들에 맵핑 시켜준다고 보면 된다. 그 후 실제로 실행 타이밍에(드라이버가 로드될 때) 레지스트리에 적힌 이 값들을 보면서 설정한 데로 동작한다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>inf 파일이 driver 개발함에 있어서 필요하다는 것은 알고 있었는데 어떻게 동작하는지는 몰랐다. 이번 포스팅을 하면서 알게 된 것 같다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver PCIe에 대한 고찰 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-PCIe%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@wang_ki/windows-driver-PCIe%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Fri, 05 Jun 2026 09:14:35 GMT</pubDate>
            <description><![CDATA[<p>PCIe에 대해서 조금 더 자세히 공부해 보고자 한다. </p>
<h3 id="pcie-장치-주소-체계">PCIe 장치 주소 체계</h3>
<p><code>BDF</code>에 대해서 간략히 설명하겠다. 
<code>Bus</code> -&gt; 도시
<code>Device</code> -&gt; 건물
<code>Function</code> -&gt; 층
으로 간단하게 이해하면 된다. </p>
<p><code>Bus</code>는 총 256개, 버스당 최대 <code>Device</code> 32개, 디바이스당 최대 8개의 <code>Function</code>을 가질 수 있다.</p>
<p><code>Function</code>마다 <code>Configuration Space</code>가 존재한다. 따라서 <code>BDF</code>를 통해서 찾아갈 수 있다. </p>
<p>예시 사진은 아래와 같다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/d9274d44-959f-4e25-9191-7a571c48b1b3/image.png" alt=""></p>
<p><code>Bus</code> -&gt; <code>Device</code> -&gt; <code>Function</code> 순으로 나열되어 있는 것을 확인할 수 있다. </p>
<hr>
<h3 id="rweverything">RWEVERYTHING</h3>
<p><a href="https://rweverything.com/">https://rweverything.com/</a></p>
<p>PCIe 하드웨어의 Config Space를 확인하기 위해서 설치했다. 위 홈페이지에서 Download하면 된다. </p>
<hr>
<h3 id="configuration-space">Configuration Space</h3>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/eff8c5f8-4d73-44a5-af23-97f4e6a7187c/image.png" alt=""></p>
<p>위 사진처럼 구성이 된다고 한다. <code>RWEverything</code>을 통해서 확인해보면 좋을 것 같다. </p>
<hr>
<h3 id="실제-configuration-space">실제 Configuration Space</h3>
<p>아래 사진은 <code>RWEverything</code>으로 읽은 실제 <code>Google Coral TPU PCIE</code>의 <code>Configuration Space</code>이다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/ad4ae41b-99c7-4c05-b0fc-ee05a425eb4b/image.png" alt=""></p>
<p>개발한 <code>npu driver</code>에서는 <code>BAR2</code>를 Device Context에 저장해놓고 사용한다. 실제 PA 값을 비교해 보기 위해서 <code>WinDbg</code>를 활용해 보겠다. </p>
<pre><code class="language-c">case CmResourceTypeMemory:
    // save BAR2
    if (descriptor-&gt;u.Memory.Length == 0x100000) {
        DbgPrint(&quot; (Memory) Start=0x%llx Length=0x%lx\n&quot;,
          descriptor-&gt;u.Memory.Start.QuadPart,
          descriptor-&gt;u.Memory.Length);

        deviceContext-&gt;Bar2Length = descriptor-&gt;u.Memory.Length;

        deviceContext-&gt;Bar2BaseAddress = MmMapIoSpace(
            descriptor-&gt;u.Memory.Start,
            descriptor-&gt;u.Memory.Length,
            MmNonCached
        );
    }
</code></pre>
<p><code>Length</code>를 통해서 BAR2를 얻어온다. 이때, 찍히는 로그를 확인해 보겠다.
<img src="https://velog.velcdn.com/images/wang_ki/post/0172a728-3a86-483c-904e-ee1db588653f/image.png" alt=""></p>
<p>위처럼 <code>0xa0000000</code>으로 확인이 되었다. <code>RWEverything</code>으로 확인을 할 수 있다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/1795a87b-ccf8-47db-bfc9-10b3780343ea/image.png" alt=""></p>
<p>BAR 레지스터의 bit4~31이 Base Address 필드이며, <code>0xA00000C</code>에서 하위 4비트를 마스킹 하면 실제 물리 주소 <code>0xA0000000</code>을 얻을 수 있다. B0 = 0 이므로 Memory Space, 즉 MMIO로 접근하는 영역임을 알 수 있다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>추가로 PCIe의 Configuration Space에서 Vendor ID, Device ID 등 여러 정보가 있고 필요하면 직접 확인할 수 있을 것 같다. </p>
<p>정확히 모르는 부분을 계속 공부해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[windows driver npu bitstream va 에 대한 고찰 ]]></title>
            <link>https://velog.io/@wang_ki/windows-driver-npu-bitstream-va-%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</link>
            <guid>https://velog.io/@wang_ki/windows-driver-npu-bitstream-va-%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0</guid>
            <pubDate>Fri, 29 May 2026 12:34:54 GMT</pubDate>
            <description><![CDATA[<p>추론 성공 후, 결과 영상을 기록하고 NPU 내부 구조를 깊게 파고드느라 바쁜 시간을 보냈습니다. 현재 NPU 드라이버 프로젝트는 유저 모드 런타임에서 ioctl을 통해 명령을 내리면, 커널 드라이버가 NPU 레지스터를 조작해 추론을 시작하는 구조입니다.</p>
<p>하지만 매 추론마다 발생하는 <strong>유저-커널 모드 스위치(Mode Switch)</strong> 는 고성능 처리에 있어 불필요한 비용입니다. 비록 스레드 컨텍스트 스위치보다 오버헤드가 적다 해도, 실시간 추론이 반복되는 환경에서는 무시할 수 없는 병목이 됩니다. 따라서 향후 런타임이 직접 하드웨어를 제어하는 방식으로 개선할 계획입니다.</p>
<p>그 전초 단계로, 이번 포스팅에서는 비트스트림 분석을 통해 NPU가 어떻게 DMA와 Zero-copy로 대용량 데이터를 효율적으로 처리하는지 그 내부 메커니즘을 자세히 살펴보겠습니다.</p>
<hr>
<h3 id="descriptor-ring">descriptor ring</h3>
<pre><code class="language-c">// Device.c
{
    PHYSICAL_ADDRESS maxAddr;
    maxAddr.QuadPart = 0xFFFFFFFFLL;
    deviceContext-&gt;DescRingBase =
        deviceContext-&gt;DmaAdapter-&gt;DmaOperations-&gt;AllocateCommonBufferEx(
            deviceContext-&gt;DmaAdapter,
            &amp;maxAddr,
            PAGE_SIZE,
            &amp;deviceContext-&gt;DescRingLogicalAddress,
            FALSE,                /* CacheEnabled = FALSE → uncached */
            MM_ANY_NODE_OK);
}</code></pre>
<p><code>AllocateCommonBufferEx</code>을 호출하여 <code>DescRingBase</code>의 <code>kva</code>를 얻고 있다.</p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/a23fb217-207d-4b5d-82c2-7fb90588c64f/image.png" alt="">
<code>CacheEnabled</code>를 FALSE로 설정하지 않으면 드라이버(CPU)가 명령(Descriptor)을 작성했는데, 아직 CPU 캐시에만 머물고 실제 RAM에 안 올라간 상태인 경우 NPU가 정상적으로 동작하지 않을 수 있다. </p>
<pre><code class="language-c">    {
        PHYSICAL_ADDRESS descPhys = MmGetPhysicalAddress(deviceContext-&gt;DescRingBase);
        deviceContext-&gt;DescRingDeviceVA = (UINT64)4096 * PAGE_SIZE;  // 0x1000000
        deviceContext-&gt;DescRingTail = 0;
        apex_write_register(deviceContext-&gt;Bar2BaseAddress,
            APEX_REG_PAGE_TABLE + (4096 * 8), descPhys.QuadPart | 1);
        {
            UINT64 rb = apex_read_register(deviceContext-&gt;Bar2BaseAddress,
                APEX_REG_PAGE_TABLE + (4096 * 8));
            DbgPrint(&quot;[%s] DescRing: VA=%p PA=0x%llx DeviceVA=0x%llx PTE[4096] write=0x%llx readback=0x%llx %s\n&quot;,
                __FUNCTION__, deviceContext-&gt;DescRingBase, descPhys.QuadPart,
                deviceContext-&gt;DescRingDeviceVA, descPhys.QuadPart | 1, rb,
                (rb == (UINT64)(descPhys.QuadPart | 1)) ? &quot;OK&quot; : &quot;MISMATCH&quot;);
        }
    }</code></pre>
<p><code>MmGetPhysicalAddress</code> api를 통해서 <code>PA</code>를 얻어왔는데 <code>MSDN</code>을 확인하니 아래와 같다. </p>
<p><img src="https://velog.velcdn.com/images/wang_ki/post/b631126b-5e8a-4987-9e47-e8f11c663491/image.png" alt=""></p>
<p>찾아본 결과 원격 pc에 <code>VT-d(Intel Virtualization Technology for Directed I/O)가 꺼져있어서 동작한 거라고 한다.</code> <em>추후 테스트 예정</em></p>
<p>중요한 것은 아래 코드이다. </p>
<pre><code class="language-c">apex_write_register(deviceContext-&gt;Bar2BaseAddress,
    APEX_REG_PAGE_TABLE + (4096 * 8), descPhys.QuadPart | 1);</code></pre>
<p><code>PTE</code>의 시작 offset 값에 4096 인덱스 지점에 <code>PA</code>를 맵핑해주었다. <code>4096 * 8</code>로 표현한 이유는 PTE가 8byte이기 때문이다. </p>
<p>windbg에 출력되는 로그를 확인해보았다. 
<img src="https://velog.velcdn.com/images/wang_ki/post/90c2565b-8930-4827-b1d2-85cf89c8c979/image.png" alt=""></p>
<p><strong><em>의문점 왜 bit0를 |1 연산을 하는지?</em></strong></p>
<p>칩의 <code>MMU</code>에게 해당 엔트리는 사용할 수 있다 라고 표현하는 것이라고 한다. 
물리 주소는 항상 4KB(페이지) 정렬이다. 페이지 시작 주소의 하위 12비트는 무조건 0이라고 볼 수 있다. 따라서 bit0가 1일 때, 유효한 매핑으로 상위 비트의 PA로 접근하는 것이다.  </p>
<hr>
<h3 id="bitstream-device-va">bitstream device VA</h3>
<p>그러면 bitstream의 deviceVA는 어떻게 넘기는 것일까? 라는 궁금증이 발생한다. </p>
<pre><code class="language-c">UINT32 slot0 = pDc-&gt;DescRingTail % 256;

ring[slot0].address = pIn-&gt;BitstreamDeviceVA;
ring[slot0].size_in_bytes = (UINT32)pIn-&gt;BitstreamSize;
ring[slot0].reserved = 0;
pDc-&gt;DescRingTail++;
KeMemoryBarrier();    // ring write 가 chip 보다 먼저 보이도록

apex_write_register(bar2, APEX_REG_INSTR_QUEUE_TAIL, pDc-&gt;DescRingTail % 256);</code></pre>
<p>위 코드처럼 링버퍼의 address 필드에 BitstreamVA를 넣어서 해당하는 <code>tail</code>을 레지스터에 써주면 된다.</p>
<p>그러면 또다시 bitstream의 device VA를 chip은 어떻게 알 수 있는가?라는 의문이 든다. 그것은 바로 PTE 맵핑이다. </p>
<p>가속기(chip)는 pa를 직접 가지고 있지 않고 MMU를 통해서 PTE를 참조하여 PA를 얻는다. 정확히 google coral tpu가 어떻게 동작하는지는 알 수 없지만 접근하고자 하는 위치의 va를 알고 있다면 pte를 통해 pa를 얻어서 접근한다고 이해했다.</p>
<pre><code class="language-c">// Queue.c IOCTL_ALLOC_IO_BUFFERS 
WdfSpinLockAcquire(pDC-&gt;PageTableLock);
for (j = 0; j &lt; pageCount; j++) {
    UINT64 pagePa = basePa + ((UINT64)j &lt;&lt; PAGE_SHIFT);
    apex_write_register(
        pDC-&gt;Bar2BaseAddress,
        APEX_REG_PAGE_TABLE + ((startPte + j) * 8),
        pagePa | 0x1ULL
    );
}
WdfSpinLockRelease(pDC-&gt;PageTableLock);</code></pre>
<p><code>pageCount</code>만큼 PTE에 맵핑해주는 코드이다. </p>
<hr>
<h3 id="결론">결론</h3>
<p>npu driver를 개발하면서 가장 낯설게 느껴졌던 부분은 위처럼 hardware가 mmu를 통해 pte를 참조해 실제 접근할 pa를 얻는 것이다. hardware가 직접 pa를 들고 있으면  되는 거 아닌가?라는 생각이 들 수 있다. 3가지 이유로 pte 맵핑을 사용한다. </p>
<ol>
<li><p>메모리 파편화 
흩어진 물리 메모리를 하나처럼 보이기 위해서이다. 윈도우에서는 10MB의 메모리를 할당하면, 실제 RAM에서는 4kb 단위로 여기저기 흩어져서 할당될 확률이 높다. 따라서 PTE가 있다면 가상 주소 하나만 던져주게 되면 조각난 PA들을 연결해서 읽을 수 있다.</p>
</li>
<li><p>하드웨어 보호 격리 
NPU는 엄처난 속도로 메로리를 읽고 쓴다. 만약 코드에 버그가 있어서 하드웨어가 잘못된 주소에 접근한다면 시스템 전체에 문제가 발생할 것이다. 따라서 PTE에 등록해 준 영역 안에서만 놀 수 있도록 한다. </p>
</li>
<li><p>유연한 메모리 관리 (Zero-copy)
데이터를 옮기지 않고, PTE의 물리 주소 값만 바꿔주면 하드웨어 입장에서는 데이터가 이동한 것처럼 보인다. </p>
</li>
</ol>
<p>알아야 할 것이 많지만 재밌는 것 같다. </p>
]]></description>
        </item>
        <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>
    </channel>
</rss>