<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>매일을 여행처럼</title>
    <link>https://onebrotravel.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 2 Jun 2026 06:09:10 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>onebrotravel</managingEditor>
    <item>
      <title>임베디드에는 가상 메모리가 없나?</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C%EC%97%90%EB%8A%94-%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC%EA%B0%80-%EC%97%86%EB%82%98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난번에 &lt;a href=&quot;https://onebrotravel.tistory.com/tag/OJTube%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C%EC%9E%85%EB%AC%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;STM32F103C8T6 칩으로 고추건조기를 만든 임베디드 프로젝트&lt;/a&gt;를 진행했다. STM32F103C8T6 칩은 Cortex-M3 코어에 Flash 64KB, SRAM 20KB짜리 칩이다. 그때 프로젝트를 진행하면서 가졌던 의문이 하나 있었다. &quot;얘는 가상 메모리가 없는 것 같은데?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데스크톱에서 돌아가는 프로그램은 가상 주소 공간 위에서 동작한다. 프로세스마다 같은 가상 주소가 서로 다른 물리 주소로 매핑되고, 그 변환을 하드웨어가 처리한다. 그런데 이 칩의 펌웨어에서는 그런 변환 계층이 보이지 않았다. 데이터시트의 메모리 맵에 적힌 주소가 코드에서 쓰는 주소와 그대로 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 이 칩에 가상 메모리가 없는 것은 맞았다. 다만 &quot;임베디드에는 가상 메모리가 없다&quot;로 일반화하면 틀린다는 것도 알게 됐다. 같은 ARM 계열 안에서도 가상 메모리를 완전히 갖춘 칩이 있고, 의도적으로 빼버린 칩이 있다. 이 글은 그 경계가 어디서 갈리는지, 그리고 STM32F103이 그 안에서 어디에 위치하는지를 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MMU와 MPU는 다른 장치다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;M1_mmu_vs_mpu.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXd4vf/dJMcaiQ1B7x/5utBYDQ3zXaqjcFnzrQ0g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXd4vf/dJMcaiQ1B7x/5utBYDQ3zXaqjcFnzrQ0g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXd4vf/dJMcaiQ1B7x/5utBYDQ3zXaqjcFnzrQ0g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXd4vf%2FdJMcaiQ1B7x%2F5utBYDQ3zXaqjcFnzrQ0g1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;720&quot; data-filename=&quot;M1_mmu_vs_mpu.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 메모리를 이야기할 때 먼저 구분해야 할 두 하드웨어 블록이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MMU(Memory Management Unit)는 가상 주소를 물리 주소로 변환하는 장치다. 페이지 테이블을 참조해 가상 주소 공간을 물리 메모리에 매핑하고, 그 과정에서 페이지 단위 접근 권한도 함께 검사한다. 가상 메모리 시리즈 &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A1-%E2%80%94-TLB%EC%99%80-%EB%A9%80%ED%8B%B0%EB%A0%88%EB%B2%A8-%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%85%8C%EC%9D%B4%EB%B8%94?category=1228741&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2편&lt;/a&gt;과 &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A2-%E2%80%94-%EC%8A%A4%EC%99%80%ED%95%91%EA%B3%BC-%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%8F%B4%ED%8A%B8?category=1228741&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;3편&lt;/a&gt;에서 다룬 페이지 테이블, TLB, 페이지 폴트가 모두 이 MMU를 전제로 한 메커니즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MPU(Memory Protection Unit)는 이름이 비슷하지만 하는 일이 다르다. MPU는 주소 변환을 하지 않는다. 미리 정의된 메모리 영역(region)별로 접근 권한과 속성만 설정한다. 예를 들어 특정 영역을 읽기 전용으로 지정하거나, 지정한 영역을 벗어난 접근에 폴트를 내도록 설정하는 식이다. 주소는 변환되지 않고 그대로 쓰이며, 위반이 발생하면 폴트를 일으킨다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-important&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;한 문장 요약&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MMU는 &quot;주소를 바꾸고 보호한다&quot;, MPU는 &quot;주소는 그대로 두고 보호만 한다&quot;. 가상 메모리는 주소 변환에서 출발하므로 MPU만으로는 가상 메모리가 성립하지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같은 ARM이라도 갈린다 &amp;mdash; Cortex 프로파일&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;M2_cortex_profiles.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw2GAa/dJMcagr9HyW/kgOwkTqM5Bdj87GOEV1mk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw2GAa/dJMcagr9HyW/kgOwkTqM5Bdj87GOEV1mk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw2GAa/dJMcagr9HyW/kgOwkTqM5Bdj87GOEV1mk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw2GAa%2FdJMcagr9HyW%2FkgOwkTqM5Bdj87GOEV1mk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;760&quot; data-filename=&quot;M2_cortex_profiles.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARM Cortex는 단일 제품군이 아니라 세 개의 아키텍처 프로파일로 나뉜다. 마케팅 분류가 아니라 ARM Architecture Reference Manual에 규정된 서로 다른 아키텍처이며, 명령어 집합&amp;middot;예외 모델&amp;middot;메모리 모델이 각각 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Cortex-A&lt;/b&gt; (Application): MMU를 갖추고 완전한 가상 메모리를 지원한다. 리눅스, 안드로이드, Windows on Arm 같은 풀 기능 OS를 돌리기 위한 프로파일이다. 스마트폰의 메인 CPU, 라즈베리파이, 차량 인포테인먼트가 여기에 속한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cortex-R&lt;/b&gt; (Real-Time): 실시간 처리용 프로파일. MMU 대신 MPU를 쓴다. 자동차&amp;middot;산업 제어처럼 마감 시한을 놓치면 실패로 간주되는 영역을 겨냥한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cortex-M&lt;/b&gt; (Microcontroller): 저전력 마이크로컨트롤러용. MMU가 없다. 일부 모델(M7, M33, M55 등)에 MPU 옵션이 있을 뿐이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32F103은 이 중 Cortex-M 코어, 그중에서도 M3를 쓴다.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;임베디드&quot;라는 단어 안의 두 부류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &quot;임베디드에는 가상 메모리가 없다&quot;는 일반화가 왜 틀리는지가 드러난다. 임베디드라는 단어는 성격이 다른 두 부류를 한데 묶는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한쪽은 MCU(Microcontroller Unit)다. STM32, ATmega, ESP32, RP2040이 여기 속한다. MMU가 없고, OS 없이 또는 RTOS(Real-Time Operating System) 위에서 펌웨어가 직접 돈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 한쪽은 AP/SoC(Application Processor / System on Chip)다. 라즈베리파이의 Cortex-A 코어, 안드로이드 기기, 차량 인포테인먼트 시스템이 여기 속한다. MMU를 완전히 갖추고 리눅스나 안드로이드 같은 OS 위에서 여러 프로세스를 돌린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 &quot;임베디드&quot;로 불리지만 가상 메모리 유무는 정반대다. 그래서 기준은 칩의 크기나 용도가 아니라 하나의 질문으로 좁혀진다 &amp;mdash; &lt;b&gt;OS 위에서 여러 프로세스를 격리해 돌릴 것인가.&lt;/b&gt; ARM의 칩 선택 가이드도 같은 기준을 제시한다. 리눅스&amp;middot;안드로이드처럼 MMU와 가상 메모리가 필요한 OS를 요구하면 Cortex-A를 쓰라는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MCU가 가상 메모리를 안 쓰는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCU에 가상 메모리가 없는 것을 &quot;메모리가 작아서&quot;로만 설명하는 경우가 많지만, 메모리 크기는 결과적 이유에 가깝다. 더 근본적인 이유가 셋 더 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하드웨어 비용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MMU는 추가 게이트와 전력을 요구하는 블록이다. 페이지 테이블 워크 로직, TLB, 권한 검사 회로가 모두 실리콘 면적과 소비 전력으로 이어진다. 수십 센트 단가에 마이크로암페어급 소비 전력을 다투는 MCU 시장에서 이 비용은 그대로 부담이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결정론(determinism)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 실시간 시스템에서 가장 중요한 이유다. 가상 메모리는 본질적으로 지연을 예측 불가능하게 만든다. 페이지 폴트가 발생하면 폴트가 난 프로세스는 디스크에서 페이지를 읽어 올 때까지 블록되고, 그동안 다른 프로세스가 CPU를 쓴다. 문제는 그 비용의 크기다. 메모리와 디스크의 속도 차는 대략 10⁴~10⁵배에 이르고, 데스크톱에서는 이런 지연이 가끔 생겨도 전체 성능에 묻혀 티가 나지 않는다. 하지만 응답 시간이 마이크로초 단위로 정해진 실시간 제어에서는 폴트 한 번이 곧 데드라인 위반으로 이어진다. 페이지 폴트의 블록 동작과 메모리&amp;middot;디스크 속도 차는 &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A2-%E2%80%94-%EC%8A%A4%EC%99%80%ED%95%91%EA%B3%BC-%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%8F%B4%ED%8A%B8?category=1228741&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;가상 메모리 시리즈 3편&lt;/a&gt;에서 다뤘다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;결정론과 가상 메모리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 시스템에서 &quot;평균이 빠른 것&quot;보다 &quot;최악이 예측 가능한 것&quot;이 중요하다. 가상 메모리는 평균을 개선하는 대신 최악 지연의 예측 가능성을 떨어뜨린다. 그래서 실시간 영역에서는 의도적으로 배제된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 모델의 단순성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCU는 단일 물리 주소 공간을 그대로 쓴다. Flash, SRAM, 그리고 주변장치 레지스터가 모두 하나의 주소 공간에 배치된다. 주변장치를 메모리처럼 주소로 접근하는 이 방식을 MMIO(Memory-Mapped I/O)라 한다. 주소 변환 계층이 없으니 포인터 주소가 곧 물리 주소이고, 데이터시트의 메모리 맵이 그대로 코드의 주소가 된다. 펌웨어 입장에서는 이 단순함이 오히려 장점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 짚어둘 점이 있다. MMU를 갖춘 코어에서 단순히 MMU를 끄는 것은 MCU의 단순함을 얻는 방법이 못 된다. MMU를 갖춘 코어에서 단순히 MMU를 끈다고 MCU처럼 단순해지는 것은 아니다. ARM 아키텍처에서 캐시 가능성(cacheability) 같은 메모리 속성은 페이지 테이블 안에 기록되기 때문에, MMU를 끄면 그 속성도 함께 사라진다. &lt;a href=&quot;https://developer.arm.com/documentation/102376/latest/&quot;&gt;Armv8-A에서는 MMU가 꺼지면 모든 접근이 Device 메모리로 취급되어&lt;/a&gt; 캐시가 동작하지 않는다. MMU가 없는 단순함과 MMU를 끈 상태는 같지 않다 &amp;mdash; MCU는 애초에 MMU 없이 설계되어 이 문제 자체가 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 크기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;M3_stm32_memory_map.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y85ri/dJMcahq6nF0/40lACMJxrBBrCS5SDhkK2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y85ri/dJMcahq6nF0/40lACMJxrBBrCS5SDhkK2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y85ri/dJMcahq6nF0/40lACMJxrBBrCS5SDhkK2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy85ri%2FdJMcahq6nF0%2F40lACMJxrBBrCS5SDhkK2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;840&quot; data-filename=&quot;M3_stm32_memory_map.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 세 가지의 결과로, MCU는 애초에 페이지 단위로 관리할 만큼 큰 메모리를 갖지 않는다. 수십 KB에서 수 MB 규모의 SRAM에 가상 메모리의 페이지 관리 오버헤드를 얹는 것은 이득이 없다. 그래서 메모리가 작은 것은 가상 메모리가 없는 원인이 아니다. 순서가 반대다 &amp;mdash; 가상 메모리가 필요 없게 설계했기 때문에 메모리도 그만큼 작은 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;중간 지대 &amp;mdash; MMU 없이 보호만 하는 영역&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 보호 방식은 MMU 있음과 없음으로 딱 갈리는 것처럼 보이지만, 실제로는 그 사이에 보호 메커니즘만 두는 중간 지대가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cortex-R&lt;/b&gt;은 자동차&amp;middot;산업 실시간 영역을 위한 프로파일로, MMU 없이 MPU로 영역 단위 보호를 제공한다. 주소 변환의 불확정성은 피하면서 메모리 보호는 확보하는 절충이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cortex-M + MPU + RTOS&lt;/b&gt; 조합에서는 MPU로 RTOS 태스크 간 메모리를 격리할 수 있다. FreeRTOS나 Zephyr 같은 RTOS는 이 MPU를 활용해 사용자 스레드와 커널 스레드를 분리한다. 가상 메모리는 없지만 &quot;태스크가 남의 메모리를 건드리면 폴트&quot;라는 보호는 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TrustZone&lt;/b&gt;은 또 다른 격리 방식이다. 여기서 주의할 점이 있는데, TrustZone은 페이지 테이블 기반이 아니다. Cortex-M의 &lt;a href=&quot;https://www.arm.com/technologies/trustzone-for-cortex-m&quot;&gt;TrustZone-M&lt;/a&gt;은 SAU(Security Attribution Unit) 설정으로 메모리를 secure/non-secure 영역으로 나누고, 그 상태를 버스 인터커넥트까지 전파해 하드웨어 수준에서 격리한다. 가상 메모리의 주소 변환과는 완전히 다른 메커니즘으로, 보안 격리를 별도 하드웨어로 구현한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 처음의 STM32F103C8T6은 이 구분에서 어디에 속하나. MPU는 Cortex-M에서도 선택적 컴포넌트라 같은 M3 코어여도 부품마다 탑재 여부가 갈린다. 그런데 이 칩의 데이터시트(STM32F103x8/xB) Features 목록에는 MPU가 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;D1_f103_datasheet_features.png&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;1755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ozhGd/dJMcacXAGX5/5smREFAkTN9KMNQJfjscB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ozhGd/dJMcacXAGX5/5smREFAkTN9KMNQJfjscB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ozhGd/dJMcacXAGX5/5smREFAkTN9KMNQJfjscB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FozhGd%2FdJMcacXAGX5%2F5smREFAkTN9KMNQJfjscB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1241&quot; height=&quot;1755&quot; data-filename=&quot;D1_f103_datasheet_features.png&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;1755&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 그 프로젝트의 칩은 MMU도 MPU도 없는, 보호 계층이 전혀 없는 단일 주소 공간이었다. 포인터 주소가 곧 물리 주소로 보였던 것은 변환 계층이 없어서였고, 잘못된 주소에 써도 막아줄 영역 보호조차 없었다는 뜻이다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-success&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;임베디드 = 가상 메모리 없음&quot;은 틀린 일반화다. 정확한 기준은 메모리 크기도 칩 용도도 아니라 &quot;OS 위에서 여러 프로세스를 격리해 돌릴 것인가&quot;이다. 그 답이 예라면 MMU를 갖춘 Cortex-A로 가고, 아니라면 MMU 없는 Cortex-M으로 충분하다. STM32F103에 가상 메모리가 없던 것은 결함이 아니라, 실시간성과 단순성과 비용을 위해 의도적으로 선택된 설계였다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Embedded</category>
      <category>ARM</category>
      <category>STM32</category>
      <category>VirtualMemory</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/109</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C%EC%97%90%EB%8A%94-%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC%EA%B0%80-%EC%97%86%EB%82%98#entry109comment</comments>
      <pubDate>Sat, 30 May 2026 15:47:23 +0900</pubDate>
    </item>
    <item>
      <title>가상 메모리 ④ &amp;mdash; 페이지 교체 정책</title>
      <link>https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A3-%E2%80%94-%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%90%EC%B2%B4-%EC%A0%95%EC%B1%85</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편에서 메커니즘이 완성됐다. 스왑 공간이 있고, present bit으로 페이지 부재를 표시하고, 페이지 폴트가 발생하면 OS 핸들러가 디스크에서 가져온다. 메모리가 가득 차면 기존 페이지 하나를 내보내고 새 페이지를 들인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 질문은 하나다 &amp;mdash; 어떤 페이지를 내보내야 하는가. 이 결정이 &lt;b&gt;페이지 교체 정책(page replacement policy)&lt;/b&gt; 이고, 이번 편의 주제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 선택의 무게는 가상 메모리에서 가장 크다. 페이지가 메모리에 있으면 접근에 약 100ns, 디스크로 내려가면 약 10ms &amp;mdash; 약 &lt;b&gt;10⁵배 차이&lt;/b&gt;다. 잘못 내보내서 다시 폴트가 나면 그 한 번에 명령어 수십만 개를 실행할 시간을 잃는다. 정책이 좋으면 시스템은 메모리 속도로 돌고, 나쁘면 디스크 속도로 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편에서 짚었듯, &lt;b&gt;가상 메모리는 캐싱이다&lt;/b&gt;. 메모리는 디스크의 캐시고, 정책은 &quot;캐시에 무엇을 둘 것인가&quot;를 결정한다. 4편 본문은 이 캐싱 관점에서 정책을 정량적으로 본다. 그러기 위해 먼저 얼마나 좋은 정책인지 측정할 도구가 필요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐싱의 정량 &amp;mdash; AMAT&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 메모리 접근 시간 &amp;mdash; &lt;b&gt;AMAT(Average Memory Access Time)&lt;/b&gt; &amp;mdash; 은 캐싱이 얼마나 잘 동작하는지를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$AMAT = T_M + (P_{Miss} \cdot T_D)$$&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;$T_M$: 메모리 접근 시간 (&amp;asymp; 100ns)&lt;/li&gt;
&lt;li&gt;$T_D$: 디스크 접근 시간 (&amp;asymp; 10ms)&lt;/li&gt;
&lt;li&gt;$P_{Miss}$: 미스율 (페이지가 메모리에 없을 확률)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;r1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgspHb/dJMcacXAmOq/uWPtEFvjVysDtrxFmC4ke1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgspHb/dJMcacXAmOq/uWPtEFvjVysDtrxFmC4ke1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgspHb/dJMcacXAmOq/uWPtEFvjVysDtrxFmC4ke1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgspHb%2FdJMcacXAmOq%2FuWPtEFvjVysDtrxFmC4ke1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;936&quot; data-filename=&quot;r1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리에서 페이지를 찾으면 $T_M$만 든다. 디스크로 내려가야 하면 $T_D$가 추가로 든다 &amp;mdash; 그것도 매번이 아니라 미스율 확률만큼. 정책의 임무는 미스율을 최소화하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수치 감을 잡아 보면 &amp;mdash; $T_M = 100ns$, $T_D = 10ms$일 때:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;미스율&lt;/th&gt;
&lt;th&gt;AMAT&lt;/th&gt;
&lt;th&gt;메모리 속도 대비&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;약 1ms&lt;/td&gt;
&lt;td&gt;10,000배 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;약 100&amp;mu;s&lt;/td&gt;
&lt;td&gt;1,000배 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;약 10.1&amp;mu;s&lt;/td&gt;
&lt;td&gt;100배 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.01%&lt;/td&gt;
&lt;td&gt;약 1.1&amp;mu;s&lt;/td&gt;
&lt;td&gt;10배 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;100ns&lt;/td&gt;
&lt;td&gt;동일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미스율을 한 자릿수 줄이는 것이 성능을 한 자릿수 끌어올린다. 1%가 좋아 보여도 메모리 속도 대비 1,000배 느림이다. 캐시 미스가 얼마나 비싼지가 이 표의 핵심이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이상적 기준 &amp;mdash; 최적 정책&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책의 얼마나 좋은지를 평가하려면 상한선이 필요하다. 1966년 Belady가 제시한 &lt;b&gt;최적 정책(OPT, Optimal)&lt;/b&gt; 이 그 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙은 단순하다 &amp;mdash; 미래에 가장 멀리 다시 쓰일 페이지를 축출한다. 직관도 단순하다. 캐시에 남은 페이지들이 제거된 페이지보다 더 빨리 필요하다는 보장이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학적으로 OPT는 미스율을 최소화하는 게 증명된 알고리즘이다. 그런데 실용성은 0이다(미래를 알아야 하기 때문). 일반적인 OS는 미래의 접근 패턴을 모른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 왜 다루는가? &lt;b&gt;다른 정책의 채점 기준&lt;/b&gt;이다. 80% 히트율이라는 수치는 그 자체로는 의미가 옅다 &amp;mdash; OPT가 82%인 워크로드에서 80%면 거의 최적이고, OPT가 95%인 워크로드에서 80%면 멀었다는 뜻이다. 얼마나 떨어졌는지를 측정하기 위해 OPT를 시뮬레이션으로 돌려 비교한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단순 정책 &amp;mdash; FIFO, Random&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미래를 모르니 과거나 우연에 의존한다. 가장 단순한 두 정책이 FIFO와 Random이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FIFO(First-In, First-Out)&lt;/b&gt; &amp;mdash; 큐를 두고 가장 먼저 들어온 페이지를 가장 먼저 내보낸다. 구현이 거의 무료 &amp;mdash; 큐의 head&amp;middot;tail 포인터 두 개만 있으면 된다. 캐시 히트가 나도 큐 순서를 건드리지 않는다. 페이지가 언제 들어왔는지만 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Random&lt;/b&gt; &amp;mdash; 그냥 무작위로 고른다. 의사난수 생성기 하나면 된다. 메타데이터 0, 결정 비용 0. 운에 맡기는 정책이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;r2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TfbwZ/dJMcacwxG34/3lBwxEhQKAf3NOGlIDnqw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TfbwZ/dJMcacwxG34/3lBwxEhQKAf3NOGlIDnqw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TfbwZ/dJMcacwxG34/3lBwxEhQKAf3NOGlIDnqw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTfbwZ%2FdJMcacwxG34%2F3lBwxEhQKAf3NOGlIDnqw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;763&quot; data-filename=&quot;r2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3페이지 캐시에 접근 시퀀스 &lt;code&gt;0, 1, 2, 0, 1, 3, 0, 3, 1, 2, 1&lt;/code&gt;을 흘려 보면 &amp;mdash; OPT는 6 히트, FIFO는 4 히트, LRU(Least Recently Used, 다음 절에서 다룬다)도 6 히트, Random은 평균 5 히트 수준이다. 11개 접근의 작은 예제지만 정책의 결의 차이가 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FIFO의 문제는 명확하다 &amp;mdash; 최근에 사용된 페이지도 진입 순서만으로 축출된다. 위 예제에서 페이지 0이 직전에 사용됐는데 FIFO는 &quot;가장 먼저 들어왔다&quot;는 이유로 0을 내보낸다. 직후에 다시 0이 필요해지므로 미스. 시간적 지역성을 전혀 활용하지 않는 정책의 대가다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;Belady의 이상 현상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FIFO에는 또 하나의 결함이 있다. 1969년 Belady가 발견한 사실 &amp;mdash; &lt;b&gt;캐시 크기를 늘렸는데 미스율이 오히려 증가하는&lt;/b&gt; 워크로드가 존재한다. 더 큰 캐시는 더 많은 페이지를 담으니 미스가 줄어야 한다는 직관에 정면으로 반한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이상 현상은 정책이 &lt;b&gt;스택 속성&lt;/b&gt;을 만족하지 않을 때 생긴다. 스택 속성이란 &quot;크기 N+1 캐시의 내용이 항상 크기 N 캐시의 내용을 포함한다&quot;는 성질이다. LRU와 OPT는 이 속성을 만족하고, FIFO와 Random은 위반한다. 즉 FIFO에서는 캐시가 커져도 *완전히 다른 페이지 집합*이 들어 있을 수 있고, 그래서 캐시 크기와 성능이 반드시 비례하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실용적 함의 &amp;mdash; 메모리를 늘렸는데 성능이 떨어진다는 의외의 보고는 정책의 스택 속성 위반에서 올 수 있다. 그래서 현대 시스템의 페이지 교체 정책은 거의 스택 속성을 만족하는 LRU 계열을 쓴다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Random은 의외로 평균적으로 FIFO보다 나쁘지 않다 &amp;mdash; 운이 좋으면 OPT와 같고, 운이 나빠도 FIFO 같은 결정적으로 잘못된 패턴에 빠지지는 않는다. 10,000번 시뮬레이션을 돌리면 약 40%의 실행에서 OPT와 동일한 성능에 도달한다는 결과도 있다. 다만 예측 불가능하다는 게 운영 시스템에서는 단점이 된다 &amp;mdash; 같은 입력에 같은 성능을 보장하지 못한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;히스토리로 미래를 추측한다 &amp;mdash; LRU&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;r3 (1).png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;835&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vzcMf/dJMcaccdxn3/scyfnwkjaLQiL66CJ0hhYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vzcMf/dJMcaccdxn3/scyfnwkjaLQiL66CJ0hhYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vzcMf/dJMcaccdxn3/scyfnwkjaLQiL66CJ0hhYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvzcMf%2FdJMcaccdxn3%2FscyfnwkjaLQiL66CJ0hhYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;835&quot; data-filename=&quot;r3 (1).png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;835&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미래를 모르면 과거가 다음으로 좋은 정보다. 그리고 과거에서 미래를 추측할 수 있는 근거가 &amp;mdash; &lt;b&gt;지역성(locality)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지역성은 1970년 Denning이 정리한 관찰로, 프로그램이 최근에 접근한 페이지나 그 주변 페이지를 곧 다시 접근하는 경향을 가리킨다. 루프, 자주 호출되는 함수, 핫한 데이터 구조 &amp;mdash; 모두 같은 페이지에 반복 접근하는 패턴이다. 지역성은 보장되는 법칙이 아니지만 경향으로는 강력해, 거의 모든 정책의 토대다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지역성을 이용하는 가장 직관적인 정책이 &lt;b&gt;LRU&lt;/b&gt;다 &amp;mdash; 가장 오래 안 쓴 페이지를 축출한다. 최근에 안 쓴 페이지는 앞으로도 안 쓸 가능성이 높다는 가정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 페이지마다 마지막으로 접근한 시각을 기록해 두고, 축출이 필요하면 그 값이 가장 오래된 페이지를 고른다. 캐시 히트가 나도 &amp;mdash; FIFO와 달리 &amp;mdash; 그 페이지의 시각을 갱신한다. 즉 자주 쓰이는 페이지는 계속 최신화되어 축출 대상에서 멀어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 11개 접근 예제에서 LRU는 6 히트로 OPT와 동일하다. 우연이 아니다. 그 시퀀스에는 지역성이 충분히 있어서, 과거 정보만으로도 최적의 결정이 가능했다. 대부분의 실제 워크로드도 비슷한 결을 띤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LRU 계열에는 &lt;b&gt;LFU(Least Frequently Used)&lt;/b&gt; &amp;mdash; 가장 적게 쓴 페이지 축출 &amp;mdash; 도 있다. 빈도를 본다는 점에서 LRU와 결이 다른데, 둘 다 지역성을 양의 방향으로 활용한다. 반대로 MRU&amp;middot;MFU 같은 정책도 정의는 가능하지만 &amp;mdash; 가장 최근에 쓴 페이지나 가장 자주 쓴 페이지를 축출 &amp;mdash; 지역성에 반하므로 일반 상황에선 비효율적이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;워크로드가 정책을 결정한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LRU가 항상 OPT에 근접하느냐 하면 그렇지는 않다. 워크로드에 따라 정책의 성능이 극단적으로 갈린다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;r3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;835&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xh2NR/dJMcai4vJXU/4YkJiDojd32SQuaHkkRoHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xh2NR/dJMcai4vJXU/4YkJiDojd32SQuaHkkRoHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xh2NR/dJMcai4vJXU/4YkJiDojd32SQuaHkkRoHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxh2NR%2FdJMcai4vJXU%2F4YkJiDojd32SQuaHkkRoHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;835&quot; data-filename=&quot;r3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;835&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 워크로드를 보면 그림이 분명해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워크로드 1 &amp;mdash; 지역성이 없을 때&lt;/b&gt;: 100개의 페이지를 완전히 무작위로 10,000번 접근하는 패턴. 결과는 단순하다 &amp;mdash; 모든 정책이 같은 성능. 지역성이 없으니 과거 정보가 의미가 없고, 정책이 무엇이든 캐시 크기에만 비례해 히트율이 올라간다. 캐시가 100페이지가 되면 모든 정책이 100% 히트율. 지역성이 없으면 정책을 정교화해도 소득이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워크로드 2 &amp;mdash; 80-20 패턴&lt;/b&gt;: 페이지의 20%(핫 페이지)에 접근의 80%가 몰리고, 나머지 80%(콜드 페이지)에 20%가 흩어지는 워크로드. 파레토 법칙의 컴퓨터 버전이고 실제 애플리케이션에서 매우 흔하다. 여기서 LRU가 빛난다 &amp;mdash; 핫 페이지가 자주 접근되니 LRU의 최근 사용 추적이 그것을 자동으로 잡아낸다. FIFO&amp;middot;Random은 진입 순서나 운에 따라 핫 페이지도 그대로 내보내, 의미 있게 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;워크로드 3 &amp;mdash; 루핑 순차 접근&lt;/b&gt;: 페이지 0, 1, ..., 49를 순서대로 접근한 뒤 다시 0부터 반복하는 패턴. 데이터베이스의 테이블 스캔이나 큰 배열의 반복 처리 같은 데서 흔하다. 이 워크로드에서 &amp;mdash; 캐시 크기가 49페이지라고 가정하면 &amp;mdash; &lt;b&gt;LRU와 FIFO의 히트율이 0%다&lt;/b&gt;. 캐시에 페이지 0~48이 들어 있는 상태에서 페이지 49가 필요해지면, LRU는 &quot;가장 오래 안 쓴&quot; 페이지 0을 축출한다. 그러나 다음 루프에서 페이지 0이 가장 먼저 필요하다. 매번 정확히 다음에 필요한 페이지를 내보내는 &amp;mdash; 최악의 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 워크로드에서는 Random이 상대적으로 낫다. 운이 좋으면 다음에 필요한 페이지를 보존할 수도 있어 0%는 면한다. 일반적으로 단순한 정책에 밀리는 Random이 이런 코너 케이스에서는 이긴다 &amp;mdash; 편향이 없다는 게 그래서 강점이 되기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 셋이 시사하는 바는 분명하다 &amp;mdash; &lt;b&gt;만능 정책은 없다&lt;/b&gt;. 지역성이 없으면 캐시 크기만 의미 있고, 지역성이 있으면 LRU 계열이 우월하며, 특정 코너 케이스에서는 단순 정책이 외려 낫다. 그래서 현대 시스템은 LRU를 기본으로 두되 루핑 시퀀스 같은 패턴에 대한 방어를 함께 갖는다 &amp;mdash; 뒤에서 다룰 ARC 등이 그런 방향의 진화다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LRU의 구현 비용과 Clock 근사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LRU는 원리가 깔끔한데 구현이 깔끔하지 않다. 모든 메모리 접근마다 &amp;mdash; 즉 명령어 인출, 데이터 읽기, 데이터 쓰기 각각에 대해 &amp;mdash; 해당 페이지의 최근 접근 시각을 갱신해야 한다. 4GB 메모리에 4KB 페이지면 페이지가 약 100만 개고, 그 중 어느 페이지가 가장 오래됐는지를 빠르게 찾는 자료구조도 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어 지원을 받아도 부담은 크다. 각 페이지에 타임스탬프 필드를 두고 접근마다 갱신하는 방식이 가능하지만, 축출 결정 시 100만 개의 타임스탬프를 다 살피는 일은 현실적이지 않다. 완벽한 LRU는 비용 대비 가치가 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제 시스템은 LRU를 근사한다. 가장 널리 쓰는 근사가 &lt;b&gt;Clock 알고리즘&lt;/b&gt;이다. 1969년 Corbato가 제안했고, 거의 LRU만큼의 성능을 훨씬 적은 비용에 낸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;r4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;835&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cF9XAL/dJMcagZW4nT/MCCisD1EqcTGyfxRSlLwPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cF9XAL/dJMcagZW4nT/MCCisD1EqcTGyfxRSlLwPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cF9XAL/dJMcagZW4nT/MCCisD1EqcTGyfxRSlLwPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF9XAL%2FdJMcagZW4nT%2FMCCisD1EqcTGyfxRSlLwPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;835&quot; data-filename=&quot;r4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;835&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 부품은 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;reference bit(또는 use bit)&lt;/b&gt;: 페이지마다 한 비트. 페이지에 접근하면 하드웨어가 자동으로 1로 설정한다. OS는 이 비트를 0으로 지우는 일만 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;clock hand&lt;/b&gt;: 페이지들을 원형 리스트로 두고, 그 위를 도는 포인터. 축출 결정 시 현재 위치에서 시작해 한 칸씩 전진하며 reference bit를 검사한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘은 단순하다. 축출이 필요하면 clock hand가 가리키는 페이지의 reference bit를 본다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1이면&lt;/b&gt; &amp;mdash; &quot;최근에 쓴 페이지다&quot; &amp;mdash; 축출하지 않고 0으로 지운 뒤 다음 페이지로 hand를 이동.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;0이면&lt;/b&gt; &amp;mdash; &quot;최근에 안 쓴 페이지다&quot; &amp;mdash; 그 페이지를 축출.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지나가면서 0으로 지운 페이지가 다시 hand에 도달했을 때 여전히 0이면 (그동안 안 쓰였다는 뜻) 축출 대상이 된다. 결과적으로 최근에 안 쓰인 페이지가 우선 축출되는 &amp;mdash; LRU와 거의 같은 결정이다. 다만 정확히 가장 오래된 페이지가 아니라 그 근처의 페이지를 고른다는 점이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용은 비교가 안 된다. 모든 접근에 대해 시각 갱신이 아니라 비트 한 개 설정만 &amp;mdash; 그것도 하드웨어가 자동으로. OS는 축출 시점에 hand를 몇 칸 전진하면 그만이다. 거의 무료 수준이고, 성능은 완벽 LRU에 매우 근접한다. 대부분의 현대 OS가 Clock 또는 그 변형을 쓴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 절약하기 &amp;mdash; Dirty 페이지 고려&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clock의 또 한 차원 개선은 축출 비용 자체의 차이에서 나온다. 페이지를 축출할 때 두 경우가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Clean 페이지&lt;/b&gt; &amp;mdash; 메모리에 적재된 뒤 수정되지 않은 페이지. 디스크의 원본과 동일하므로 그냥 메모리에서 지우면 끝. 추가 I/O 0.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dirty 페이지&lt;/b&gt; &amp;mdash; 메모리에서 수정된 페이지. 디스크에 다시 써야(write-back) 데이터를 잃지 않는다. 추가 디스크 I/O 발생.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 최근에 안 쓴 페이지 두 개 중에서 &amp;mdash; 하나는 clean이고 하나는 dirty라면 &amp;mdash; clean을 축출하는 게 훨씬 싸다. 한 번의 디스크 쓰기를 절약한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 지원하려면 또 한 비트가 필요하다 &amp;mdash; &lt;b&gt;dirty bit(또는 modified bit)&lt;/b&gt;. 페이지에 쓰기가 일어나면 하드웨어가 자동으로 1로 설정한다. 읽기만 일어났으면 0 그대로.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clock 알고리즘의 스캔 순서가 두 단계로 확장된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;1순위 스캔&lt;/b&gt; &amp;mdash; reference bit = 0 그리고 dirty bit = 0인 페이지. 최근에 안 쓴 clean 페이지. 가장 싼 축출.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2순위 스캔&lt;/b&gt; &amp;mdash; reference bit = 0 그리고 dirty bit = 1인 페이지. 최근에 안 쓴 dirty 페이지. 1순위가 없을 때만 선택.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 결정을 내리는 데 비트 한 개를 더 추가해서 디스크 I/O를 한 번 절약한 셈이다. 가상 메모리 시스템 전체가 이런 작은 비트와 큰 절약의 교환들로 채워져 있다는 점이 이번 시리즈가 반복해서 보여주는 패턴이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시야 확장 &amp;mdash; VM의 다른 정책들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 교체가 가장 중요한 정책이지만 유일한 건 아니다. VM 서브시스템에는 다른 결정들이 함께 작동한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지 선택 정책 &amp;mdash; 언제 가져올 것인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 다룬 흐름 &amp;mdash; 접근이 일어났을 때 비로소 페이지를 디스크에서 가져오는 &amp;mdash; 이 &lt;b&gt;demand paging&lt;/b&gt;이다. 1970년 Denning이 이름 붙였고 대부분의 OS가 채택한 기본 방식이다. 실제 필요한 페이지만 메모리에 두니 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 OS가 미래의 접근을 합리적으로 추측할 수 있는 경우엔 미리 가져올 수도 있다. 이게 &lt;b&gt;prefetching&lt;/b&gt;이다. 가장 흔한 사례 &amp;mdash; 코드 페이지 P가 적재되면 P+1도 함께 가져온다. 순차적 코드 실행이라는 공간적 지역성에 베팅한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;prefetching은 맞으면 절약이고 틀리면 낭비다. 예측이 맞으면 폴트 한 번을 면하지만, 틀리면 안 쓸 페이지를 위해 메모리와 I/O를 썼다. 그래서 성공 가능성이 합리적일 때만 한다. 무작정 예측해 가져오는 prefetching은 시스템 전체를 느리게 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지 쓰기 정책 &amp;mdash; 어떻게 내보낼 것인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축출되는 dirty 페이지를 한 번에 하나씩 디스크에 쓰면 비효율적이다. 디스크는 작은 쓰기 여러 번보다 큰 쓰기 한 번이 훨씬 빠르니까 &amp;mdash; 탐색&amp;middot;회전 오버헤드가 한 번에 분산된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 OS는 dirty 페이지들을 모았다가 한 번에 쓴다 &amp;mdash; &lt;b&gt;clustering&lt;/b&gt; 또는 &lt;b&gt;grouping&lt;/b&gt;. 3편의 페이지 데몬 섹션에서 이미 본 그 최적화다. 4편 관점에서 보면 이게 쓰기 정책의 한 형태 &amp;mdash; 축출 정책이 무엇을 내보낼지를 정한다면, 쓰기 정책은 어떻게 모아 내보낼지를 정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스래싱과 그 대응&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크로드의 총 작업 집합(working set) &amp;mdash; 활발히 쓰이는 페이지들의 합 &amp;mdash; 이 물리 메모리 크기를 초과하면 시스템은 곤란해진다. 폴트가 끊임없이 발생하고, 막 들여온 페이지가 다음 순간 또 축출된다. 새 페이지를 들이느라 옛 페이지를 내보내고, 그 옛 페이지가 다시 필요해지고 &amp;mdash; 정책이 무엇이든 무한 사이클이다. 이 상태를 &lt;b&gt;스래싱(thrashing)&lt;/b&gt; 이라 한다. CPU의 대부분이 유용한 작업이 아니라 페이지 교체에 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적 대응은 &lt;b&gt;admission control&lt;/b&gt; &amp;mdash; 프로세스의 일부를 의도적으로 중단해서 활성 working set을 메모리에 맞게 줄인다. &quot;전부를 잘못하느니 일부만 잘하자&quot;는 철학. 짧게 줄어들고 짧게 회복되는 자연스러운 사이클이 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스의 현대적 대응은 더 거칠다 &amp;mdash; &lt;b&gt;OOM(Out-of-Memory) killer&lt;/b&gt;. 메모리 압박이 임계점을 넘으면 가장 메모리를 많이 쓰는 프로세스를 골라 그냥 죽인다. 단순하고 빠르지만 부작용이 명확하다. 중요한 프로세스가 종료될 위험이 있다. X 서버가 OOM 대상이 되어 모든 GUI 애플리케이션이 함께 사라지는 사고가 종종 있다. 정교한 admission control 대신 급진적 회복을 택한 트레이드오프다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1부&lt;/b&gt;는 페이징을 도착점으로 두고 세그멘테이션의 외부 단편화 문제를 해결했다. 동시에 두 문제를 남겼다. 변환의 &lt;b&gt;속도&lt;/b&gt;가 두 배로 느려졌고, 페이지 테이블 자체의 &lt;b&gt;크기&lt;/b&gt;가 4MB 단위로 커졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2부&lt;/b&gt;는 그 둘을 해결했다. &lt;b&gt;TLB&lt;/b&gt;가 캐싱으로 변환 속도를 회복했고, &lt;b&gt;멀티레벨 페이지 테이블&lt;/b&gt;이 트리 구조로 페이지 테이블 자체의 크기를 압축했다. 캐싱과 시간-공간 교환 &amp;mdash; 1편에서 본 자료구조의 두 trade-off가 양쪽 다 회수됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3부&lt;/b&gt;는 1편부터 깔려 있던 가정 하나를 풀었다 &amp;mdash; &quot;모든 페이지가 메모리에 있다.&quot; &lt;b&gt;스왑 공간&lt;/b&gt;과 &lt;b&gt;present bit&lt;/b&gt;과 &lt;b&gt;페이지 폴트&lt;/b&gt; 메커니즘으로, 메모리는 디스크의 캐시가 됐다. 가상 메모리가 비로소 &quot;메모리보다 큰 메모리&quot;라는 이름값을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4부&lt;/b&gt;는 그 캐시 위에서 작동하는 &lt;b&gt;정책&lt;/b&gt;을 다뤘다. 미래를 모르는 채로 과거 정보로 어디까지 근사할 수 있는지 &amp;mdash; OPT부터 LRU, Clock까지의 진화는 지역성이라는 단 하나의 가정 위에서 이뤄졌다. 가상 메모리는 지역성을 끝까지 짜내는 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료구조의 관점에서 보면 시리즈 전체가 일관된 결을 띤다. 1편의 페이지 테이블 = 자료구조 선택의 자유, 2부의 TLB = 캐싱과 멀티레벨 = 트리, 3부의 present bit + PFN 필드 재활용, 4부의 use bit + dirty bit. 모두 작은 부품의 정교한 조합으로 큰 문제를 푼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 메모리는 모든 현대 OS가 당연한 듯이 제공하는 기능이지만, 그 안에는 50년 가까운 진화의 결과가 들어 있다. 페이징&amp;middot;세그멘테이션의 초기 아이디어부터 TLB&amp;middot;멀티레벨&amp;middot;페이지 폴트&amp;middot;Clock 근사까지, 작동하는 추상화가 어떻게 다듬어졌는지를 한 흐름으로 본 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VirtualMemory Paging PageReplacement LRU Clock OSTEP CSAPP 정글 입문&lt;/p&gt;</description>
      <category>OS</category>
      <category>Clock</category>
      <category>CSAPP</category>
      <category>LRU</category>
      <category>OSTEP</category>
      <category>PageReplacement</category>
      <category>paging</category>
      <category>VirtualMemory</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/108</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A3-%E2%80%94-%ED%8E%98%EC%9D%B4%EC%A7%80-%EA%B5%90%EC%B2%B4-%EC%A0%95%EC%B1%85#entry108comment</comments>
      <pubDate>Fri, 29 May 2026 22:24:50 +0900</pubDate>
    </item>
    <item>
      <title>가상 메모리 ③ &amp;mdash; 스와핑과 페이지 폴트</title>
      <link>https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A2-%E2%80%94-%EC%8A%A4%EC%99%80%ED%95%91%EA%B3%BC-%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%8F%B4%ED%8A%B8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 페이징을 도착점으로 두고 두 문제를 남겼고, 2편에서 TLB(Translation Lookaside Buffer)와 멀티레벨 페이지 테이블로 그 둘을 해결했다. 변환은 빨라졌고 페이지 테이블은 작아졌다. 그러나 1편 시작부터 지금까지 깔려 있던 가정 하나가 아직 남아 있다. 바로 &lt;b&gt;모든 페이지가 물리 메모리에 들어 있다&lt;/b&gt;는 가정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가정은 현실에서 자주 깨진다. 4GB 메모리에서 8GB 데이터를 다루는 프로그램, 수십 개의 프로세스가 각자 수백 MB의 주소 공간을 가진 시스템, 메모리에 다 못 담는 큰 행렬을 다루는 과학 계산 &amp;mdash; 모두 물리 메모리보다 큰 주소 공간을 요구한다. 가상 메모리 추상화는 이런 경우에도 &quot;이 프로세스에는 충분한 메모리가 있다&quot;는 환상을 유지해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 관점을 한 단계 끌어올리면 &quot; &lt;b&gt;가상 메모리는 캐싱이다&quot;&lt;/b&gt;라는 가상 메모리의 다른 얼굴이 드러난다. 물리 메모리는 디스크의 캐시이고, 자주 쓰는 페이지만 메모리에 두고 나머지는 디스크에 둔다. TLB가 페이지 테이블의 캐시였듯, 메모리는 디스크의 캐시다. 가상 메모리 시스템 전체가 캐시 계층 위에서 돌아간다 &amp;mdash; 빠르고 작은 TLB, 그 아래 페이지 테이블이 있는 메모리, 그 아래 거대하고 느린 디스크.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편는 이 캐싱이 어떻게 동작하는지의 메커니즘을 다룬다. 어떤 페이지를 내보낼지의 정책은 4편의 영역이고, 3편는 그 정책이 작동할 수 있는 토대 &amp;mdash; 스왑 공간, present bit, 페이지 폴트 처리 &amp;mdash; 를 본다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스왑 공간&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;s1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btjFfu/dJMcaarXVNI/7l1kSrRjtnHPTK0IKmCK4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btjFfu/dJMcaarXVNI/7l1kSrRjtnHPTK0IKmCK4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btjFfu/dJMcaarXVNI/7l1kSrRjtnHPTK0IKmCK4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtjFfu%2FdJMcaarXVNI%2F7l1kSrRjtnHPTK0IKmCK4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;936&quot; data-filename=&quot;s1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;가장 먼저 필요한 것은 물리 메모리에 못 담은 페이지를 둘 곳이다. 디스크의 일정 영역을 페이지 단위로 예약해 두고 OS가 이를 메모리 확장용으로 쓴다. 이 영역을 &lt;b&gt;스왑 공간(swap space)&lt;/b&gt; 이라 한다. 페이지를 메모리에서 디스크로 내보내는 것을 &lt;b&gt;스왑 아웃(swap out)&lt;/b&gt;, 디스크에서 메모리로 가져오는 것을 &lt;b&gt;스왑 인(swap in)&lt;/b&gt; 이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스왑 공간은 단순히 디스크의 일부지만, OS 입장에서 이 공간을 다루는 데에는 두 가지 추가 정보가 필요하다. 첫째, 각 페이지가 스왑 공간 어디에 있는지를 알아야 한다 &amp;mdash; 디스크 주소를 페이지마다 추적해야 한다. 둘째, 스왑 공간의 크기가 시스템의 최대 가상 메모리 용량을 결정한다는 점이다. 물리 메모리 4GB에 스왑 공간 16GB면 시스템 전체가 가질 수 있는 활성 메모리 페이지의 상한이 20GB가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스왑 공간만 페이지의 디스크 소스가 되는 것은 아니다. 프로그램의 바이너리 자체도 디스크에 있는 페이지의 원본이다. &lt;code&gt;ls&lt;/code&gt;나 직접 컴파일한 실행 파일의 코드 페이지는 처음부터 디스크의 그 바이너리 파일에 존재한다. OS는 실행 시점에 필요한 코드 페이지만 메모리로 읽어 들이고, 메모리가 부족하면 그 코드 페이지의 메모리 자리를 그냥 회수한다. 디스크의 바이너리는 그대로 있으니, 다음에 그 코드 페이지가 필요해지면 바이너리에서 다시 읽으면 된다. 읽기 전용 데이터라 디스크에 다시 쓸 필요도 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;present bit&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;s2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;835&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dofry3/dJMcadvlPC7/w1OKpPwjdE5KvVrlgketqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dofry3/dJMcadvlPC7/w1OKpPwjdE5KvVrlgketqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dofry3/dJMcadvlPC7/w1OKpPwjdE5KvVrlgketqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdofry3%2FdJMcadvlPC7%2Fw1OKpPwjdE5KvVrlgketqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;835&quot; data-filename=&quot;s2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;835&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;스왑 공간이 있어도 OS가 어느 페이지가 지금 메모리에 있고 어느 페이지가 디스크에 있는지를 알지 못하면 의미가 없다. &lt;b&gt;PTE(Page Table Entry)&lt;/b&gt; 에 이 정보를 적어 두기 위해 &lt;b&gt;present bit&lt;/b&gt; 하나를 추가한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;present = 1&lt;/code&gt;: 페이지가 물리 메모리에 현재 있음. PFN(Page Frame Number) 필드가 유효한 물리 프레임 번호를 담음.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;present = 0&lt;/code&gt;: 페이지가 물리 메모리에 없음. 디스크에 있으며, PFN 필드는 디스크 주소로 재활용됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 비트 영역을 두 의미로 쓰는 건 빈 공간을 만들지 않으려는 절약이다. 페이지가 메모리에 있으면 PFN, 없으면 디스크 주소 &amp;mdash; 어차피 둘 중 하나만 의미가 있고, present bit가 어느 쪽인지를 알려 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 비트가 변환 흐름에 미치는 영향은 명확하다. 변환 도중 MMU(Memory Management Unit, 또는 OS의 TLB miss 핸들러)가 PTE를 읽었는데 &lt;code&gt;present = 0&lt;/code&gt;이면, 변환을 계속할 수 없다. PFN 필드에는 PFN 대신 디스크 주소가 들어 있으니, 그걸로 물리 주소를 만들 수 없다. 이 상황을 &lt;b&gt;페이지 폴트(page fault)&lt;/b&gt; 라 한다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;&quot;페이지 폴트&quot;라는 이름의 부정확함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;폴트&quot;는 보통 잘못된 일을 가리키는 말이지만, 페이지 폴트는 잘못된 접근이 아니다. 프로세스의 가상 주소 공간에 정당하게 매핑된 페이지에 정당한 방식으로 접근했는데 단지 그 페이지가 지금 메모리에 없는 상황일 뿐이다. 더 정확한 이름은 &quot;페이지 미스(page miss)&quot;이고, TLB miss&amp;middot;캐시 miss와 같은 결의 용어다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 용어가 굳어진 데는 하드웨어 사정이 있다. 페이지가 없는 상황도, 권한 위반 같은 진짜 오류도, 모두 같은 예외 처리 경로 &amp;mdash; &quot;하드웨어가 처리 못 함 &amp;rarr; OS로 제어권 이전&quot; &amp;mdash; 를 거친다. 메커니즘이 같다 보니 둘 다 &quot;폴트&quot;로 묶어 부르게 됐고, 이제 와서 바꾸기엔 너무 굳어 버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 문맥에 따라 의미가 갈린다. &lt;b&gt;넓게 쓰면&lt;/b&gt; 페이지 폴트는 PTE 관련 모든 예외 &amp;mdash; 권한 위반, 유효하지 않은 주소, 페이지 부재 &amp;mdash; 를 포함하고, &lt;b&gt;좁게 쓰면&lt;/b&gt; 본문에서 다루는 페이지 부재만 가리킨다. 이 글에서는 좁은 뜻으로 쓴다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;페이지 폴트는 누가 처리하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 폴트가 발생하면 &amp;mdash; 즉 MMU가 &lt;code&gt;present = 0&lt;/code&gt;인 PTE를 만나면 &amp;mdash; 하드웨어는 즉시 예외를 발생시켜 OS로 제어권을 넘긴다. OS의 &lt;b&gt;페이지 폴트 핸들러&lt;/b&gt;가 실행되어 폴트를 서비스한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 짚을 점은 &amp;mdash; 2편에서 본 하드웨어 관리 TLB냐 소프트웨어 관리 TLB냐와 무관하게, &lt;b&gt;페이지 폴트는 항상 OS가 처리한다&lt;/b&gt;. 하드웨어 관리 TLB가 보통의 TLB miss를 하드웨어로 직접 처리해도, 페이지 폴트만은 OS에 넘긴다. 이유는 두 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 &lt;b&gt;성능 균형의 문제&lt;/b&gt;다. 페이지 폴트의 비용은 본질적으로 디스크 I/O가 결정한다(수 ms 단위). OS 핸들러가 수 &amp;mu;s 더 걸리든 말든 전체 비용에 비하면 무시할 만한 수준이다. 하드웨어로 일부 시간을 줄여 봐야 의미가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 하나는 &lt;b&gt;복잡성의 회피&lt;/b&gt;다. 페이지 폴트를 처리하려면 스왑 공간 관리, 디스크 I/O 발행, 페이지 교체 정책 &amp;mdash; 모두 복잡하고 자주 바뀌는 정책들을 알아야 한다. 이런 걸 하드웨어에 박는 건 비효율적이다. 소프트웨어로 두면 OS마다 자유롭게 정책을 바꿀 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 폴트 핸들러가 하는 일은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;디스크 주소 확인&lt;/b&gt; &amp;mdash; 폴트가 발생한 페이지의 PTE를 읽는다. &lt;code&gt;present = 0&lt;/code&gt;이므로 PFN 필드에는 디스크 주소가 들어 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디스크 I/O 발행&lt;/b&gt; &amp;mdash; 그 디스크 주소에서 페이지를 읽어 물리 메모리로 가져오는 I/O 요청을 낸다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;블록(block)&lt;/b&gt; &amp;mdash; I/O는 ms 단위로 느리므로 OS는 폴트가 난 프로세스를 블록 상태로 두고, 다른 준비된 프로세스를 CPU에 올린다. I/O가 진행되는 동안 CPU가 노는 일이 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;I/O 완료&lt;/b&gt; &amp;mdash; 디스크에서 페이지가 메모리에 적재되면, OS가 PTE를 갱신한다. &lt;code&gt;present&lt;/code&gt;를 1로, PFN 필드에는 페이지가 들어간 새 물리 프레임 번호를 적는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재시도&lt;/b&gt; &amp;mdash; 블록되어 있던 프로세스를 깨운다. 원래 폴트가 났던 명령어를 다시 실행한다. 이번에는 PTE의 &lt;code&gt;present = 1&lt;/code&gt;이고 PFN이 유효해, 정상적으로 변환이 끝난다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 멀티 프로그래밍(한 프로세스의 I/O 동안 다른 프로세스가 CPU를 쓰는)은 OS의 핵심 효율 기법이다. I/O 비용이 클수록 이 중첩의 가치가 커진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리가 가득 차면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 설명은 &quot;디스크에서 페이지를 읽어 들일 자리(빈 프레임)가 있다&quot;는 또 하나의 가정 위에 서 있었다. 현실에서는 메모리가 거의 늘 차 있다. 새 페이지를 읽어 오려면 먼저 기존 페이지 하나를 내보내 자리를 만들어야 한다 &amp;mdash; &lt;b&gt;페이지 교체(page replacement)&lt;/b&gt; 다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 페이지를 내보낼지는 &lt;b&gt;페이지 교체 정책&lt;/b&gt;이 결정한다. 이 선택이 좋으면 시스템은 메모리 속도에 가깝게 돈다. 나쁘면 &amp;mdash; 즉 곧 다시 필요한 페이지를 자꾸 내보내면 &amp;mdash; 프로그램이 디스크 속도로 떨어진다. 메모리와 디스크의 속도 차이가 약 10⁴~10⁵배 수준이라, 잘못된 정책의 비용은 다른 어떤 최적화 이슈보다 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편은 정책 자체에는 들어가지 않는다(4편의 주제). 여기서 짚을 점은 정책이 작동할 자리를 메커니즘이 마련한다는 사실이다. 폴트 핸들러의 흐름에 단계 하나가 끼어든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;s3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1037&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvx60y/dJMcaa6zOKW/u9ZDvqCB9KrPTJZ4Fy9lQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvx60y/dJMcaa6zOKW/u9ZDvqCB9KrPTJZ4Fy9lQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvx60y/dJMcaa6zOKW/u9ZDvqCB9KrPTJZ4Fy9lQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbvx60y%2FdJMcaa6zOKW%2Fu9ZDvqCB9KrPTJZ4Fy9lQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1037&quot; data-filename=&quot;s3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1037&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정된 흐름은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;PTE 검사&lt;/b&gt; &amp;mdash; 1부의 정상 흐름. 페이지가 valid&amp;middot;present면 그대로 변환.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이지 폴트 분기&lt;/b&gt; &amp;mdash; &lt;code&gt;valid = 1&lt;/code&gt;이지만 &lt;code&gt;present = 0&lt;/code&gt;이면 폴트 핸들러로.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유효하지 않은 접근 분기&lt;/b&gt; &amp;mdash; &lt;code&gt;valid = 0&lt;/code&gt;이면 OS 트랩 핸들러로 (보통 SIGSEGV(segmentation violation signal)로 프로세스 종료).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폴트 처리 (OS)&lt;/b&gt; &amp;mdash; 빈 프레임이 있으면 바로 디스크 I/O. 없으면 교체 정책을 돌려 내보낼 페이지를 고른다 &amp;rarr; 그 페이지를 디스크에 쓴다(필요하면) &amp;rarr; 그 자리에 새 페이지를 읽어 들인다 &amp;rarr; PTE 갱신 &amp;rarr; 명령어 재시도.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 분기가 같은 하드웨어 예외 메커니즘을 공유한다. 하드웨어는 그냥 &quot;처리 못 함, OS가 결정하라&quot;고 넘기고, OS가 PTE 비트들을 보고 어떤 종류의 폴트인지 가린다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 시스템 &amp;mdash; 메모리가 차기 전에 움직인다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 흐름은 정확하지만 너무 늦다. 메모리가 완전히 찰 때까지 기다렸다가 폴트가 났을 때 교체를 시작하면, 폴트 처리 시간이 두 배가 된다 &amp;mdash; 새 페이지를 읽기 전에 기존 페이지를 디스크에 쓰는 시간이 추가되니까. 그래서 실제 시스템은 능동적으로 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;s4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nxB7A/dJMcaciWqlF/s3pdQA8qQDmRbxIKSYyi01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nxB7A/dJMcaciWqlF/s3pdQA8qQDmRbxIKSYyi01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nxB7A/dJMcaciWqlF/s3pdQA8qQDmRbxIKSYyi01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnxB7A%2FdJMcaciWqlF%2Fs3pdQA8qQDmRbxIKSYyi01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;806&quot; data-filename=&quot;s4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS는 두 개의 임계점을 둔다 &amp;mdash; &lt;b&gt;낮은 워터마크(LW)&lt;/b&gt; 와 &lt;b&gt;높은 워터마크(HW)&lt;/b&gt;. 여유 페이지 수가 LW 아래로 떨어지면 백그라운드의 페이지 데몬(또는 스왑 데몬)이 깨어나 페이지를 축출하기 시작하고, HW까지 여유 페이지가 회복되면 다시 잠든다. 사용자 프로세스는 페이지 폴트 시 이미 마련된 빈 프레임을 즉시 받을 수 있어 폴트 비용이 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 능동적 관리에는 부가 이점이 더 있다. 페이지 데몬은 여러 페이지를 클러스터링해 한 번에 디스크에 쓸 수 있다. 디스크는 작은 쓰기 여러 번보다 큰 쓰기 한 번이 훨씬 빠르다 &amp;mdash; 탐색&amp;middot;회전 오버헤드가 분산되니까. 그래서 같은 양의 데이터를 내보내도 데몬이 일괄로 내보내는 게 폴트마다 한 페이지씩 내보내는 것보다 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정된 폴트 흐름은 단순해진다. 폴트가 나면 OS는 직접 교체를 수행하지 않는다. 여유 페이지가 있으면 그걸 쓰고, 없으면 데몬에게 신호를 보내 깨운 뒤 자기는 잠시 잠들고, 데몬이 여유를 만들어 깨워 주면 재개한다. 교체의 시점과 폴트 처리의 시점이 분리된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편은 가상 메모리가 물리 메모리보다 큰 주소 공간을 다루기 위한 메커니즘을 깔았다. 핵심 부품은 네 개다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스왑 공간&lt;/b&gt; &amp;mdash; 메모리에 못 담은 페이지를 보관할 디스크 영역.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;present bit&lt;/b&gt; &amp;mdash; PTE 한 비트로 &quot;이 페이지가 지금 메모리에 있는지&quot;를 표시. 없으면 PFN 필드는 디스크 주소로 재활용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이지 폴트 처리&lt;/b&gt; &amp;mdash; &lt;code&gt;present = 0&lt;/code&gt;을 만난 변환은 OS의 핸들러로 넘어가 디스크에서 페이지를 가져온다. 그동안 프로세스는 블록, 다른 프로세스가 CPU를 쓴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백그라운드 페이지 데몬&lt;/b&gt; &amp;mdash; 폴트가 날 때가 아니라 나기 전에 미리 빈 프레임을 마련해 둔다. 워터마크와 클러스터링으로 효율을 끌어올린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메커니즘 덕분에 메모리 = 디스크의 캐시라는 캐시 계층이 비로소 작동한다. 자주 쓰는 페이지는 메모리에, 나머지는 디스크에. 페이지 폴트는 캐시 miss와 같은 성격의 사건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 메커니즘이 작동하려면 한 가지 결정이 남아 있다. 어떤 페이지를 메모리에 두고 어떤 페이지를 내보낼 것인가. 잘못된 선택의 비용이 10⁴~10⁵배라는 사실이 이 결정의 무게를 보여 준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음으로&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4편은 이 &lt;b&gt;선택&lt;/b&gt;을 다룬다. 미래를 안다면 최적은 무엇인지(Belady의 OPT), 미래를 모르는 채로 어디까지 근사할 수 있는지(FIFO, Random, LRU), 그리고 실제 시스템이 LRU를 어떻게 흉내 내는지(Clock 알고리즘). 같은 지역성이라는 가정 위에서, 이번엔 자료구조가 아니라 시간을 보는 휴리스틱이다.&lt;/p&gt;</description>
      <category>OS</category>
      <category>CSAPP</category>
      <category>OSTEP</category>
      <category>PageFault</category>
      <category>paging</category>
      <category>SwapIn</category>
      <category>SwapOut</category>
      <category>VirtualMemory</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/107</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A2-%E2%80%94-%EC%8A%A4%EC%99%80%ED%95%91%EA%B3%BC-%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%8F%B4%ED%8A%B8#entry107comment</comments>
      <pubDate>Fri, 29 May 2026 20:53:28 +0900</pubDate>
    </item>
    <item>
      <title>가상 메모리 ② &amp;mdash; TLB와 멀티레벨 페이지 테이블</title>
      <link>https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A1-%E2%80%94-TLB%EC%99%80-%EB%A9%80%ED%8B%B0%EB%A0%88%EB%B2%A8-%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%85%8C%EC%9D%B4%EB%B8%94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1편은 페이징을 도착점으로 두고 끝맺었지만, 그 끝에서 두 문제가 남았다. 하나는 &lt;b&gt;속도&lt;/b&gt;다. 모든 메모리 접근마다 페이지 테이블을 한 번 더 읽어야 하니, 단순히 페이징을 켜는 것만으로 프로그램이 두 배 가까이 느려진다. 다른 하나는 &lt;b&gt;크기&lt;/b&gt;다. 32비트 주소 공간에 4KB 페이지면 페이지 테이블 항목이 약 100만 개, 프로세스 하나당 4MB가 든다. 대부분이 빈 항목인데도 선형 배열이라 다 만들어 둬야 한다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;VPN&amp;middot;오프셋 비트 빠르게 구하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프셋 비트 = log₂(페이지 크기) &amp;mdash; 페이지 안의 모든 바이트를 가리킬 수 있어야 하므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN 비트 = 전체 주소 비트 &amp;minus; 오프셋 비트 &amp;mdash; 남는 비트가 페이지 번호&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: 32비트 주소 공간 + 4KB 페이지 &amp;rarr; 오프셋 log₂(4KB)=12비트, VPN = 32&amp;minus;12 = 20비트. 페이지 테이블 항목 수 = 2^(VPN 비트) = 2&amp;sup2;⁰ &amp;asymp; 100만 개.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2부는 이 두 결함을 해결한다. 속도는 &lt;b&gt;TLB(Translation Lookaside Buffer)&lt;/b&gt;라는 변환 캐시로, 크기는 &lt;b&gt;멀티레벨 페이지 테이블(multi-level page table)&lt;/b&gt;이라는 자료구조 교체로. 두 해법 모두 가상 메모리만의 발명이 아니라 컴퓨터 시스템 전체에서 반복되는 원리 &amp;mdash; &quot;자주 쓰는 것은 캐싱하라&quot;, &quot;쓰지 않는 것은 만들지 말라&quot;의 적용이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TLB &amp;mdash; 속도 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편의 페이징은 변환 한 번에 메모리 접근 두 번을 요구했다. 가상 주소가 들어오면 먼저 페이지 테이블에서 PTE(Page Table Entry)를 읽고, 그다음 실제 데이터를 읽는다. 메모리 접근이 두 배가 되니 페이징을 켜는 순간 프로그램이 두 배 느려진다. 가상 메모리 추상화가 아무리 유용해도 이대로면 실용성이 없다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;페이지 테이블은 어디에 있나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 테이블은 프로세스 자신의 주소 공간(코드&amp;middot;힙&amp;middot;스택)이 아니라 &lt;b&gt;커널이 관리하는 물리 메모리&lt;/b&gt;에 있다. 프로세스 생성 시 OS의 메모리 관리자가 할당하고, 그 시작 위치(물리 주소)를 PTBR에 담아 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 가상 주소 공간 안이 아닌가 &amp;mdash; 페이지 테이블이 프로세스의 가상 주소 공간 안에 있다면, 그것을 읽기 위해 또 다른 변환이 필요한 닭과 달걀 문제에 빠진다. 그래서 PTBR은 가상 주소가 아니라 물리 주소를 담고, MMU는 PTBR을 따라 변환 없이 페이지 테이블에 직접 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 1편 끝의 크기 결함이 정확히 무엇을 부담하는지 드러난다 &amp;mdash; 프로세스 100개의 페이지 테이블 400MB는 사용자 프로세스 메모리가 아니라 &lt;b&gt;커널 메모리&lt;/b&gt;를 잠식한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결은 변환 결과를 캐싱하는 것이다. CPU 안에 &lt;b&gt;TLB(Translation Lookaside Buffer)&lt;/b&gt;라는 작은 캐시를 두고, 한 번 풀어낸 VPN(Virtual Page Number)&amp;rarr;PFN(Physical Frame Number) 매핑을 보관한다. 같은 페이지에 다시 접근하면 페이지 테이블을 거치지 않고 TLB만 보고 즉시 물리 주소를 만든다. 이름은 buffer지만 동작은 캐시다. 즉 TLB는 MMU(Memory Management Unit) 안에 자리 잡은, 주소 변환 전용 캐시이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변환 흐름 &amp;mdash; hit과 miss&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;t1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;893&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PLFfl/dJMcaayJoVp/qXnrZmiKFch10O3ua3eu3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PLFfl/dJMcaayJoVp/qXnrZmiKFch10O3ua3eu3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PLFfl/dJMcaayJoVp/qXnrZmiKFch10O3ua3eu3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPLFfl%2FdJMcaayJoVp%2FqXnrZmiKFch10O3ua3eu3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;893&quot; data-filename=&quot;t1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;893&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 주소가 도착하면 하드웨어는 먼저 상위 비트에서 VPN을 뽑아 TLB를 조회한다. 매치되는 항목이 있으면 &lt;b&gt;TLB hit&lt;/b&gt;이다. 그 항목에서 PFN을 꺼내 오프셋과 결합해 물리 주소를 만들고 곧장 메모리에 접근한다. 페이지 테이블 접근은 일어나지 않는다. 접근당 메모리 1회, 페이징이 아예 없을 때와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매치가 없으면 &lt;b&gt;TLB miss&lt;/b&gt;다. 하드웨어는 1부에서 본 절차대로 페이지 테이블을 한 번 읽어 PTE를 가져오고, 유효성과 권한을 검사한 뒤 그 변환을 TLB에 채워 넣는다. 그리고 같은 명령어를 다시 실행한다. 두 번째 실행 때는 방금 채워진 TLB가 hit를 내므로 일반 속도로 진행된다. miss 한 번의 비용은 페이지 테이블 접근 한 번이지만, 그 미스가 채운 항목 덕분에 이후 같은 페이지에 대한 접근은 모두 빠르게 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 TLB의 효과는 명백히 &lt;b&gt;확률 게임&lt;/b&gt;이다. miss는 비싸지만, miss가 hit를 만든다. 결국 성능은 hit율이 결정한다 &amp;mdash; hit가 충분히 많으면 페이징의 두 배 비용은 거의 사라지고, hit율이 낮으면 TLB가 있어도 두 배 가까이 느려진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지역성이 TLB를 살린다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hit율이 높을 수 있는 이유는 프로그램의 &lt;b&gt;지역성(locality)&lt;/b&gt; 때문이다. 짧은 시간에 접근하는 메모리는 좁은 영역에 몰리는 경향이 있다(공간적 지역성, spatial locality). 그리고 한 번 접근한 곳은 곧 다시 접근될 가능성이 크다(시간적 지역성, temporal locality). TLB는 작지만 이 두 지역성이 작용하는 한 hit율은 자연스럽게 올라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열 순회가 가장 단순한 예다. 10개짜리 정수 배열을 처음부터 끝까지 더한다고 하자. 페이지 크기 16바이트, 배열 시작 주소가 가상 100번지라면, 배열은 VPN 6&amp;middot;7&amp;middot;8 세 페이지에 나뉘어 들어간다(&lt;code&gt;a[0]&lt;/code&gt;은 VPN 6의 오프셋 4, &lt;code&gt;a[1]&lt;/code&gt;은 오프셋 8, ..., &lt;code&gt;a[3]&lt;/code&gt;은 VPN 7의 오프셋 0...). 루프가 돌면서 TLB는 다음 패턴을 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;t2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lOgQ0/dJMcahxQq5F/hbXAr06GSVuK9K5Vu0IHs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lOgQ0/dJMcahxQq5F/hbXAr06GSVuK9K5Vu0IHs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lOgQ0/dJMcahxQq5F/hbXAr06GSVuK9K5Vu0IHs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlOgQ0%2FdJMcahxQq5F%2FhbXAr06GSVuK9K5Vu0IHs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;734&quot; data-filename=&quot;t2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;a[0]&lt;/code&gt;은 VPN 6을 처음 만나니 miss. 그 miss가 TLB에 VPN 6 매핑을 채운다. 이어지는 &lt;code&gt;a[1]&lt;/code&gt;, &lt;code&gt;a[2]&lt;/code&gt;는 같은 VPN 6이라 hit, hit. &lt;code&gt;a[3]&lt;/code&gt;은 VPN 7로 넘어가니 또 miss, 이후 &lt;code&gt;a[4]&lt;/code&gt;~&lt;code&gt;a[6]&lt;/code&gt;은 hit 셋. &lt;code&gt;a[7]&lt;/code&gt;에서 VPN 8 miss, &lt;code&gt;a[8]&lt;/code&gt;&amp;middot;&lt;code&gt;a[9]&lt;/code&gt; hit. 10번 접근에 miss 3번, hit 7번 &amp;mdash; hit율 70%다. 처음 보는 배열인데도 공간적 지역성만으로 단번에 이 정도가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 루프를 한 번 더 돌면 어떻게 될까. 이미 세 페이지의 매핑이 TLB에 있으니 10번 접근 모두 hit이다. 이번엔 시간적 지역성(방금 접근한 페이지를 곧 다시 접근)이 작동한 것이다. 실제 시스템의 페이지 크기는 4KB라 한 페이지에 정수 1024개가 들어가므로, 같은 배열의 hit율은 더 올라간다. 거의 모든 접근이 한 번의 miss 뒤에는 hit로 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 TLB의 본질이 드러난다. TLB는 모든 접근을 빠르게 만들지 않는다. 지역성 있는 접근을 빠르게 만든다. 그리고 대다수 프로그램은 (의도하지 않아도) 충분한 지역성을 갖는다. 그래서 작은 TLB(보통 64~128개 항목)만으로도 페이징의 두 배 비용이 거의 사라진다. TLB의 성공은 하드웨어 캐시 자체보다 프로그램이 본래 지역성을 갖는다는 사실에 기댄다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;미스는 누가 처리하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLB miss가 나면 페이지 테이블을 읽어 변환을 가져오고 TLB에 채워야 한다. 이 작업의 주체로 두 갈래가 갈린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하드웨어 관리(hardware-managed) TLB&lt;/b&gt;는 하드웨어가 직접 페이지 테이블을 읽는다. CPU 안에 페이지 테이블 베이스 레지스터가 있고(예: x86의 CR3), miss가 나면 하드웨어가 그 레지스터를 따라 페이지 테이블을 &quot;워킹&quot;해 PTE를 가져온 뒤 TLB를 채우고 명령어를 재시도한다. OS는 페이지 테이블의 형식과 위치만 약속해 두면 되고, miss 처리에는 개입하지 않는다. x86 계열이 이 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소프트웨어 관리(software-managed) TLB&lt;/b&gt;는 miss를 OS의 일로 떠넘긴다. TLB miss가 발생하면 하드웨어는 예외(trap)를 발생시켜 커널 모드로 전환하고, OS의 TLB miss 핸들러를 호출한다. 핸들러는 페이지 테이블에서 변환을 찾아 특별한 권한 명령어로 TLB에 써넣은 뒤 원래 명령어로 복귀한다. MIPS, SPARC v9 같은 RISC 계열이 이 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 트랩의 복귀는 일반 시스템 콜과 다르다. 시스템 콜은 호출 다음 명령어로 돌아가지만, TLB miss 트랩은 트랩을 발생시킨 그 명령어로 돌아가 다시 실행한다. 이번에는 TLB에 변환이 채워져 있으니 hit이 난다. 즉 명령어 입장에서는 한 번 중단되었다가 같은 자리에서 다시 시작되는 것처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 방식의 trade-off는 분명하다. 하드웨어 관리는 빠르지만 페이지 테이블 형식이 하드웨어에 박제되어 OS가 자유롭게 바꾸지 못한다. 소프트웨어 관리는 트랩 비용을 감수하는 대신 OS가 페이지 테이블 자료구조를 자유롭게 설계할 수 있다 &amp;mdash; 해시 테이블이든 트리든. 이는 1 끝에서 본 &quot;선형 배열 vs 다른 구조&quot; 선택의 자유와 정확히 같은 자유다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;왜 x86은 하드웨어 관리, ARM&amp;middot;MIPS는 소프트웨어 관리인가 &amp;mdash; CISC와 RISC&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 갈래는 1980년대의 명령어 집합 설계 철학 차이에서 비롯한다. CISC(Complex Instruction Set Computing)는 &quot;복잡한 일은 하드웨어가 처리한다&quot;는 노선이고, RISC(Reduced Instruction Set Computing)는 &quot;하드웨어는 단순하게, 복잡한 일은 소프트웨어로&quot;라는 노선이다. TLB 미스 처리도 이 노선을 그대로 따라간다. CISC 계열인 x86은 하드웨어가 페이지 테이블을 직접 워킹하고, RISC 계열인 MIPS&amp;middot;SPARC v9&amp;middot;ARM(일부 세대)은 OS가 트랩 핸들러로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 두 진영은 사실상 수렴했다. x86 내부는 CISC 명령어를 RISC식 마이크로 명령어로 분해해 실행하고, ARM도 세대를 거치며 하드웨어 페이지 테이블 워킹을 도입했다. 그럼에도 시장 분포는 남았다 &amp;mdash; 데스크톱&amp;middot;서버는 x86(CISC 계보), 모바일&amp;middot;임베디드는 ARM(RISC 계보). 임베디드에서 ARM이 우세한 이유 중 하나도 RISC의 단순한 하드웨어가 전력&amp;middot;면적 면에서 유리하기 때문이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TLB 항목 안에는 무엇이 들어가는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLB의 한 항목은 단순히 VPN과 PFN의 쌍이 아니다. 변환을 안전하고 정확히 수행하기 위한 메타데이터가 함께 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본은 검색 키인 &lt;b&gt;VPN&lt;/b&gt;과 결과인 &lt;b&gt;PFN&lt;/b&gt;이다. 여기에 &lt;b&gt;유효 비트(valid bit)&lt;/b&gt;가 붙어 그 항목이 실제 변환을 담고 있는지를 표시한다. 부팅 직후나 TLB를 비울 때 모든 항목은 유효 비트 0이 된다. &lt;b&gt;보호 비트(protection bits)&lt;/b&gt;는 페이지에 대한 읽기&amp;middot;쓰기&amp;middot;실행 권한을 담아, 변환과 동시에 권한 검사가 이뤄지게 한다. 권한 위반이면 TLB가 hit이어도 예외가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 컨텍스트 스위치를 다루기 위한 &lt;b&gt;ASID(Address Space Identifier)&lt;/b&gt;가 추가되곤 한다. 이게 왜 필요한지가 다음 섹션의 주제다. TLB는 완전 연관(fully associative) 캐시로 구현되는 게 보통이라, 검색 시 모든 항목과 동시에 병렬 비교가 이뤄진다. 항목 수가 32~128개로 제한된 이유(병렬 비교 회로의 비용과 전력 때문)도 여기서 나온다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨텍스트 스위치 &amp;mdash; 누구의 변환인가&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;t3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nT9Ur/dJMcajoMVtx/y5jq3hBK5zmLzqD3TWqFwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nT9Ur/dJMcajoMVtx/y5jq3hBK5zmLzqD3TWqFwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nT9Ur/dJMcajoMVtx/y5jq3hBK5zmLzqD3TWqFwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnT9Ur%2FdJMcajoMVtx%2Fy5jq3hBK5zmLzqD3TWqFwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;706&quot; data-filename=&quot;t3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;706&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLB의 한 항목은 &quot;VPN 10번은 PFN 100번&quot;이라고 적혀 있다. 그런데 이 매핑은 어떤 프로세스의 VPN 10번인가? 각 프로세스는 독립된 가상 주소 공간을 가지므로, 프로세스 P1의 VPN 10번과 P2의 VPN 10번은 완전히 다른 물리 페이지로 가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P1이 실행 중 TLB에 (VPN 10 &amp;rarr; PFN 100)을 채웠다고 하자. 컨텍스트 스위치가 일어나 P2가 실행되면서 VPN 10번을 접근한다. 이때 TLB는 hit를 내며 P1의 매핑(PFN 100)을 돌려준다 &amp;mdash; P2 입장에서는 완전히 잘못된 물리 페이지다. 메모리 보호가 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 가장 단순한 해법은 컨텍스트 스위치마다 TLB를 &lt;b&gt;flush(비우기)&lt;/b&gt; 하는 것이다. 모든 항목의 유효 비트를 0으로 설정해 무효화한다. 안전하지만 비용이 크다. 새 프로세스는 TLB가 텅 빈 상태로 시작하니 한동안 miss가 폭주한다(콜드 스타트).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나은 방법은 &lt;b&gt;ASID&lt;/b&gt;를 도입하는 것이다. TLB의 각 항목에 그 변환이 속한 프로세스의 ASID를 함께 적어 둔다. PID와 비슷한 식별자지만 TLB 내 구분만 필요하므로 보통 8비트(256개)면 충분하다. CPU에는 &quot;현재 실행 중 프로세스의 ASID&quot;를 담는 레지스터가 있고, 컨텍스트 스위치 때 OS가 이 값을 새 프로세스의 ASID로 갱신한다. TLB는 그 자체로 비우지 않는다. 조회 시 VPN과 ASID가 둘 다 일치해야 hit이 되므로, P1의 항목이 남아 있어도 P2가 잘못 가져갈 일이 없다. 나중에 P1이 다시 스케줄되면 그때 남아 있던 항목들이 그대로 hit를 만들어 콜드 스타트 비용도 줄어든다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TLB가 해결한 문제, 남은 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLB는 1편의 속도 문제를 해결한다. hit율이 높으면 페이징의 두 배 비용이 사라지고, 프로그램은 가상 메모리가 없는 듯이 동작한다. 그래서 OSTEP의 표현대로, TLB는 가상 메모리를 실용적으로 만든 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지만 TLB가 모든 걸 해결하진 않는다. 프로그램이 짧은 시간에 접근하는 페이지 수가 TLB가 담을 수 있는 항목 수를 초과하면 &amp;mdash; 이를 &lt;b&gt;TLB 커버리지 초과&lt;/b&gt;라 한다 &amp;mdash; miss율이 급증한다. 64개 항목 &amp;times; 4KB = 256KB. 워킹 셋이 이를 넘는 워크로드(대규모 데이터베이스, 포인터 체이싱이 많은 자료구조 순회 등)는 여전히 TLB miss로 느려진다. 이 한계는 큰 페이지 지원으로 일부 완화할 수 있지만, 본질적인 해결은 알고리즘과 자료구조를 TLB 친화적으로 설계하는 쪽에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1편의 또 다른 문제, 크기 문제는 여전히 그대로다. 프로세스당 4MB의 선형 페이지 테이블이라는 부담은 TLB가 줄여주지 않는다 &amp;mdash; TLB는 변환을 빠르게 할 뿐, 페이지 테이블 자체를 작게 만들지는 못한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티레벨 페이지 테이블 &amp;mdash; 크기 문제&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;t4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekplT2/dJMcagyT09K/GEKfdgdBnrY4KGrvTkcuKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekplT2/dJMcagyT09K/GEKfdgdBnrY4KGrvTkcuKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekplT2/dJMcagyT09K/GEKfdgdBnrY4KGrvTkcuKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekplT2%2FdJMcagyT09K%2FGEKfdgdBnrY4KGrvTkcuKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;922&quot; data-filename=&quot;t4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크기 결함을 다시 짚자. 32비트 주소 공간에 4KB 페이지면 VPN 20비트, 항목 약 100만 개, 프로세스당 4MB. 프로세스 100개가 돌면 커널 메모리에서 400MB가 페이지 테이블만으로 사라진다. 그런데 이 항목 대부분은 무효다 &amp;mdash; 코드&amp;middot;힙&amp;middot;스택 사이의 거대한 빈 공간이 모두 항목을 할당받는다. 실제로 쓰는 페이지 수는 수십에서 수백 개에 불과한데, 100만 개를 다 가지고 있는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제는 변환 자체가 아니라 자료구조 선택이다. 1편 끝에서 선형 배열을 골랐기 때문에, 안 쓰는 자리도 다 가지고 있어야 한다는 제약이 생긴 거다. 다른 자료구조를 택하면 빈 자리는 만들지 않을 수 있다. 단, 그 선택에는 대가가 따른다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단순한 시도 &amp;mdash; 페이지를 더 크게&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 발상은 페이지 크기 자체를 키우는 것이다. 페이지가 커지면 같은 주소 공간에서 페이지 개수가 줄고, 항목 수도 같이 줄어든다. 32비트 주소 공간에 페이지를 4KB에서 16KB로 키우면 오프셋이 12비트에서 14비트로 늘고, VPN은 20비트에서 18비트로 줄어든다. 항목 수는 100만에서 26만으로, 페이지 테이블 크기는 4MB에서 1MB로 &amp;mdash; 정확히 4분의 1이다. 페이지 크기를 2ⁿ배 키우면 페이지 테이블도 2ⁿ분의 1이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 단순한 해법은 내부 단편화(internal fragmentation)를 발생시킨다. 1편에서 본 외부 단편화가 &quot;할당 단위 사이&quot;의 낭비라면, 내부 단편화는 &quot;할당 단위 안의 낭비다. 페이지 단위로만 메모리를 할당할 수 있으므로, 어떤 영역이 5KB만 필요해도 16KB 페이지 통째로 할당해야 한다. 이 경우 11KB가 낭비된다. 큰 페이지를 많이 쓸수록 이런 낭비가 누적되어 시스템 전체 메모리 효율이 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 대부분의 시스템은 페이지를 작게 유지한다 &amp;mdash; x86은 4KB, SPARC v9는 8KB. 큰 페이지는 일부 시스템에서 선택적으로 지원되지만(2MB, 1GB 등), 이는 페이지 테이블 절약보다는 TLB 커버리지 확대(앞 섹션의 TLB 한계 회피)가 주목적이다. 페이지 크기 키우기만으로 크기 결함을 푸는 건 부족하다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;큰 페이지의 진짜 용도 &amp;mdash; TLB 커버리지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLB 한 항목이 한 페이지를 덮으므로, &quot;TLB가 동시에 덮을 수 있는 총 메모리&quot;는 항목 수 &amp;times; 페이지 크기로 정해진다. 이걸 &lt;b&gt;TLB 커버리지(TLB coverage)&lt;/b&gt;라 한다. 64항목 &amp;times; 4KB = 256KB, 64항목 &amp;times; 2MB = 128MB.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워킹 셋이 TLB 커버리지를 넘으면 miss가 급증한다(앞 절의 &quot;TLB 커버리지 초과&quot;). 데이터베이스&amp;middot;가상화처럼 워킹 셋이 큰 워크로드에서 huge page(2MB)나 giant page(1GB)를 켜는 이유가 이거다 &amp;mdash; 같은 64항목으로 더 넓은 범위를 덮어 miss를 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 큰 페이지의 주목적은 페이지 테이블 절약이 아니라 TLB miss 회피다. 페이지 테이블이 작아지는 건 부수 효과이고, 큰 페이지를 시스템 전체에 적용하면 내부 단편화가 따라오므로 기본 페이지 크기는 여전히 4~8KB로 둔다. 크기 결함의 본 해법은 다른 데에 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하이브리드 &amp;mdash; 페이징과 세그멘테이션 결합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 시도는 1편에서 본 세그멘테이션을 페이징과 결합하는 것이다. 페이지 테이블이 큰 이유가 &quot;주소 공간 전체를 하나의 선형 배열로 다루기 때문&quot;이라면, 주소 공간을 코드&amp;middot;힙&amp;middot;스택 세그먼트로 나누고 각 세그먼트마다 별도의 페이지 테이블을 가지면 어떨까. 세그먼트 사이의 거대한 빈 공간은 어느 페이지 테이블에도 속하지 않으니, 그 공간만큼의 항목을 만들 필요가 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 주소를 &lt;code&gt;[세그먼트 번호 | VPN | 오프셋]&lt;/code&gt;으로 쪼갠다. 상위 2비트로 코드(01)&amp;middot;힙(10)&amp;middot;스택(11) 중 어디인지를 가린다. MMU는 세그먼트 번호로 해당 세그먼트의 페이지 테이블 베이스 레지스터를 고른 뒤, VPN을 인덱스로 그 페이지 테이블에서 PTE를 가져온다. 세그멘테이션의 베이스+바운드 메커니즘이 그대로 살아 있어 바운드 검사로 잘못된 접근을 막을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 메모리를 어느 정도 절약한다. 힙과 스택 사이의 빈 영역은 더 이상 페이지 테이블 항목을 만들지 않으니까. 그러나 두 한계가 따라온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;세그멘테이션의 유연성 한계가 그대로 상속된다&lt;/b&gt;. 코드&amp;middot;힙&amp;middot;스택이라는 미리 정해진 영역 구분을 전제로 하므로, 힙 안에서의 희소 사용 &amp;mdash; 예를 들어 큰 희소 자료구조 &amp;mdash; 에는 여전히 무력하다. 힙 세그먼트의 페이지 테이블은 그 세그먼트의 가장 높은 사용 페이지까지 모든 항목을 가져야 하니까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;b&gt;더 큰 문제는 외부 단편화의 재발이다&lt;/b&gt;. 세그먼트별 페이지 테이블은 그 세그먼트가 실제 쓰는 페이지 수에 따라 크기가 제각각이다. 작은 코드 세그먼트의 페이지 테이블, 큰 힙 세그먼트의 페이지 테이블, 중간 크기의 스택 세그먼트의 페이지 테이블 &amp;mdash; 모두 다른 크기로 커널 메모리에 들어앉는다. 1부에서 외부 단편화를 없애려 페이지라는 균일한 단위를 도입했는데, 페이지 테이블 자체를 임의 크기로 두면 그 단편화가 다시 살아난다. OS가 페이지 테이블용 연속 공간을 찾기가 다시 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세그멘테이션과 페이징의 결합은 직관적이지만, 두 방식의 단점도 같이 가져온다. 더 근본적인 해법이 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지 테이블 자체를 페이지로 쪼개기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근본적인 발상의 전환은 이거다 &amp;mdash; &lt;b&gt;페이지 테이블 자체&lt;/b&gt;도 페이지로 쪼개 놓고, 모든 항목이 무효인 페이지는 &lt;b&gt;아예 만들지 않는다&lt;/b&gt;. 코드&amp;middot;힙&amp;middot;스택 같은 미리 정해진 세그먼트 구분에 의존하지 않고, 빈 영역이 어디에 있든 그곳의 페이지 테이블 페이지는 생략할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 그러면 페이지 테이블 페이지들이 띄엄띄엄 흩어져 존재하게 된다는 점이다. VPN으로 곧장 인덱싱하던 선형 구조가 깨진다. 어느 VPN이 어느 페이지 테이블 페이지에 있는지를 알려줄 추가 자료구조가 필요하다. 그게 &lt;b&gt;페이지 디렉토리(page directory)&lt;/b&gt; 다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 디렉토리는 페이지 테이블 페이지들의 인덱스다. 각 항목 &amp;mdash; PDE(Page Directory Entry) &amp;mdash; 는 유효 비트 하나와 PFN 하나를 담는다. 유효 비트는 그 PDE가 가리키는 페이지 테이블 페이지 안에 유효한 PTE가 하나라도 있는지를 표시한다. 무효 PDE라면 그 페이지 테이블 페이지는 존재하지 않는다. 항목만 비어 있는 게 아니라 메모리에 만들지조차 않은 것이다. PFN은 그 페이지 테이블 페이지가 어느 물리 프레임에 있는지를 가리킨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;t5.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg33jn/dJMb997C29g/8CZsD4kmPVVytPVU9agK11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg33jn/dJMb997C29g/8CZsD4kmPVVytPVU9agK11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg33jn/dJMb997C29g/8CZsD4kmPVVytPVU9agK11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg33jn%2FdJMb997C29g%2F8CZsD4kmPVVytPVU9agK11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;950&quot; data-filename=&quot;t5.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;950&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN은 이제 두 부분으로 쪼개진다. 상위 비트는 페이지 디렉토리의 인덱스, 하위 비트는 그 PDE가 가리키는 페이지 테이블 페이지 안에서의 PTE 인덱스가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환은 두 단계가 된다. 가상 주소가 들어오면 먼저 VPN의 상위 비트로 페이지 디렉토리에서 PDE를 꺼낸다. PDE가 유효하면 그 PFN으로 페이지 테이블 페이지의 위치를 알아내고, VPN의 하위 비트로 그 안에서 PTE를 꺼낸다. PTE에서 데이터 페이지의 PFN을 얻어 오프셋과 결합하면 최종 물리 주소다. 무효 PDE를 만나면 그 영역은 아예 매핑되지 않은 거니까 예외가 발생한다 &amp;mdash; 페이지 테이블 페이지를 만들 필요조차 없었던 영역이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;t6.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;893&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/75PiJ/dJMcag6KxJv/Wz5sc4Jkab6cbH415v0Uv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/75PiJ/dJMcag6KxJv/Wz5sc4Jkab6cbH415v0Uv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/75PiJ/dJMcag6KxJv/Wz5sc4Jkab6cbH415v0Uv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F75PiJ%2FdJMcag6KxJv%2FWz5sc4Jkab6cbH415v0Uv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;893&quot; data-filename=&quot;t6.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;893&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선형 페이지 테이블과 비교하면 효과가 분명하다. 16KB 주소 공간에 64바이트 페이지면 256개 PTE가 필요한데, 이를 페이지당 16개 PTE로 쪼개면 16개의 페이지 테이블 페이지가 된다. 코드 한 페이지, 힙 한 페이지, 스택 두 페이지만 실제로 쓴다면, 유효한 PTE를 담은 페이지 테이블 페이지는 단 두 개뿐이다. 페이지 디렉토리 하나(16개 PDE) + 페이지 테이블 페이지 둘 = 총 세 페이지. 선형으로 16페이지 다 만드는 것과 비교해 거의 5분의 1이다. 실제 32비트 시스템에서는 이 비율이 훨씬 극적이다. 4MB의 선형 페이지 테이블이 희소 주소 공간에서는 보통 수십 KB 수준으로 줄어든다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대가 &amp;mdash; 시간과 공간의 교환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티레벨 페이지 테이블의 절약은 공짜가 아니다. 선형 페이지 테이블에서 TLB miss 한 번이 메모리 접근 한 번을 의미했다면(PTE 한 번), 멀티레벨에서는 두 번이 된다 &amp;mdash; PDE 한 번, PTE 한 번. TLB hit에는 영향이 없지만, miss 비용은 정확히 두 배가 된다. 자료구조 깊이가 한 단계 더 들어간 만큼 변환 경로도 한 단계 더 길어진 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 trade-off는 &lt;b&gt;시간-공간 트레이드오프(time-space tradeoff)&lt;/b&gt; 의 전형이다. 1편 끝에 깔아둔 자료구조 관점의 두 번째 trade-off가 여기서 실물로 드러난다. 공간을 절약하려면 시간을 더 쓰고, 시간을 절약하려면 공간을 더 쓴다. 선형 배열은 시간($O(1)$ 조회)에 최적화한 선택이었고, 멀티레벨은 공간을 위해 시간을 한 단계 양보한 선택이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 양보가 받아들여지는 이유는 다시 지역성에 있다. TLB hit율이 충분히 높으면 miss 자체가 드물고, miss 비용이 두 배여도 평균 변환 비용은 거의 그대로다. TLB가 깔려 있다는 전제 위에서만 멀티레벨이 실용적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;더 깊은 계층&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시는 두 단계 &amp;mdash; 페이지 디렉토리 + 페이지 테이블 &amp;mdash; 였지만, 주소 공간이 커지면 페이지 디렉토리 자체도 한 페이지에 안 들어갈 수 있다. 그러면 페이지 디렉토리도 페이지로 쪼개고, 그 위에 또 한 단계의 인덱스를 둔다. 3단, 4단으로 계층이 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;64비트 주소 공간에서는 이 깊이가 4단 이상이 거의 필수다. 64비트 주소 공간이 VPN을 50비트 이상 요구하는데, 그걸 선형 배열로 둔다는 건 &amp;mdash; 항목 수가 2⁵⁰ &amp;asymp; 천조 &amp;mdash; 비현실적이다. 멀티레벨이 없으면 64비트 시스템 자체가 성립하지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 시스템 &amp;mdash; Intel Core i7의 4단 페이지 테이블&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;t7.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0mLNy/dJMcaa6zJVf/0cvLbgKHoB7Jkqy0RampkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0mLNy/dJMcaa6zJVf/0cvLbgKHoB7Jkqy0RampkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0mLNy/dJMcaa6zJVf/0cvLbgKHoB7Jkqy0RampkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0mLNy%2FdJMcaa6zJVf%2F0cvLbgKHoB7Jkqy0RampkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;806&quot; data-filename=&quot;t7.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Intel Core i7은 64비트 CPU지만 가상 주소를 48비트만 쓴다. 64비트(2⁶⁴ &amp;asymp; 16 엑사바이트) 주소 공간 전체를 지원하는 건 현재 워크로드에 과하고, 페이지 테이블 비용도 폭증하기 때문이다. 48비트로 충분히 256TB를 가리키니 당분간 부족할 일은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 48비트 가상 주소를 4단으로 쪼갠다. 오프셋이 12비트(4KB 페이지), 나머지 36비트를 9비트씩 네 조각으로 잘라 각각의 페이지 테이블 인덱스로 쓴다. 위에서부터 차례로 &lt;b&gt;PML4(Page Map Level 4)&lt;/b&gt; &amp;middot; &lt;b&gt;PDPT(Page Directory Pointer Table)&lt;/b&gt; &amp;middot; &lt;b&gt;PD(Page Directory)&lt;/b&gt; &amp;middot; &lt;b&gt;PT(Page Table)&lt;/b&gt; 이다. 한 페이지 안에 항목이 2⁹ = 512개 들어가고(64비트 항목 = 8바이트 &amp;times; 512 = 4KB), 그 512항목 중 어디로 갈지를 9비트로 인덱싱한다. 정확히 한 페이지에 떨어지게 설계된 거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환 시작점은 &lt;b&gt;CR3 레지스터&lt;/b&gt;다. CR3는 그 프로세스의 PML4가 어느 물리 프레임에 있는지를 담는다(1편의 PTBR이 이걸 일반화한 이름이다). 컨텍스트 스위치 때 OS가 CR3를 새 프로세스의 PML4 주소로 바꾼다. 변환은 CR3에서 출발해 PML4 &amp;rarr; PDPT &amp;rarr; PD &amp;rarr; PT를 차례로 타고 내려가, 마지막 PT에서 데이터 페이지의 PFN을 얻는다. 그 PFN과 12비트 오프셋을 결합하면 최종 물리 주소다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 함의는 두 가지다. 하나는 공간 절약이다. 주소 공간 중 안 쓰는 영역은 그 영역에 해당하는 PML4/PDPT/PD 항목이 무효이고, 그 아래 페이지 테이블 페이지들은 만들지조차 않는다. 64비트 공간 256TB가 워킹 셋 수MB뿐인 프로세스에서도, 페이지 테이블 자체는 수십 KB 수준으로 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 하나는 변환 비용이다. TLB miss가 나면 4번의 메모리 접근이 필요하다 &amp;mdash; PML4 항목, PDPT 항목, PD 항목, PT 항목. 1편의 선형 페이지 테이블이 한 번, 앞 섹션의 2단 멀티레벨이 두 번이었던 것과 비교하면 두 배 더 늘었다. 그래서 64비트 시스템에서 TLB는 더 중요해진다 &amp;mdash; hit율이 떨어지면 변환 비용이 5배(=1+4)로 뛰니까. 실제로 i7은 TLB를 데이터&amp;middot;명령어용 L1, 통합 L2로 계층화해 hit율을 끌어올린다. TLB가 덜 미스 나게 만드는 데 더 많은 하드웨어가 들어간다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TLB&lt;/b&gt;는 변환 결과를 작은 캐시에 보관해 페이징의 두 배 비용을 거의 사라지게 했고, &lt;b&gt;멀티레벨 페이지 테이블&lt;/b&gt;은 안 쓰는 영역의 항목을 만들지 않는 방식으로 페이지 테이블 자체를 수십 KB 수준으로 줄였다. 이 둘이 결합되어야 페이징이 비로소 실용적이다 &amp;mdash; TLB만 있으면 페이지 테이블이 여전히 4MB씩이라 메모리가 모자라고, 멀티레벨만 있으면 4단 변환 비용 때문에 TLB miss가 너무 비싸진다. 그래서 모든 현대 시스템은 둘을 함께 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자료구조 관점에서 2편은 1편 끝에서 깔아둔 트레이드오프를 양쪽 다 회수했다. 첫째, &lt;b&gt;캐싱&lt;/b&gt;. TLB는 작고 빠른 저장공간으로 큰 자료구조 접근을 가속하는 전형이고, 지역성이라는 프로그램의 본성이 그 캐싱을 살린다. 둘째, &lt;b&gt;시간-공간 트레이드오프&lt;/b&gt;. 선형 페이지 테이블이 시간($O(1)$)을 위해 공간을 쓴 선택이었다면, 멀티레벨은 공간을 위해 시간($O(d)$, $d$=깊이)을 양보한 선택이다. 둘 다 같은 trade-off의 양극단에서 골라낸 것이지, 어느 한쪽이 절대적으로 우월한 게 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 페이지 테이블이 데이터 구조라는 관점도 살아 있다. 1편에서 본 &quot;선형 배열 vs 트리 vs 해시&quot;라는 자료구조 선택의 자유는 멀티레벨에서 트리 구조를, 그리고 (본문에선 깊이 다루지 않았지만) 역 페이지 테이블에서 해시 구조를 선택할 수 있게 해주는 토대였다. OS는 하드웨어가 허용하는 한 그 자유를 쓴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 가정은 &quot;프로세스의 모든 페이지가 물리 메모리에 들어 있다&quot;였다. 그래서 페이지 테이블 항목이 valid이면 곧장 PFN으로 변환된다고 봤다. 그러나 현실에서는 물리 메모리보다 큰 주소 공간을 다뤄야 한다 &amp;mdash; 4GB 시스템에서 8GB 프로그램을 돌리거나, 100개 프로세스가 각자 수십 MB씩 쓰는 상황. 모든 페이지를 메모리에 둘 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3부의 주제는 이 가정의 해결한다. 일부 페이지는 디스크에 두고(스왑), 필요할 때만 메모리로 불러온다. 페이지 테이블 항목에 present bit을 추가해 &quot;이 페이지가 지금 메모리에 있는지&quot;를 표시하고, 없으면 &lt;b&gt;페이지 폴트(page fault)&lt;/b&gt; 가 발생해 OS가 디스크에서 가져온다. 그 과정에서 어느 페이지를 메모리에서 내보낼지를 정하는 &lt;b&gt;페이지 교체 정책&lt;/b&gt; &amp;mdash; LRU, clock 알고리즘, 워킹 셋 &amp;mdash; 이 등장한다. 가상 메모리가 &quot;메모리보다 큰 메모리&quot;라는 이름값을 비로소 하는 단계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VirtualMemory Paging TLB MultiLevel CacheLocality ASID OSTEP CSAPP&lt;/p&gt;</description>
      <category>OS</category>
      <category>ASID</category>
      <category>CSAPP</category>
      <category>locality</category>
      <category>OSTEP</category>
      <category>PageTable</category>
      <category>paging</category>
      <category>TLB</category>
      <category>VirtualMemory</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/106</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A1-%E2%80%94-TLB%EC%99%80-%EB%A9%80%ED%8B%B0%EB%A0%88%EB%B2%A8-%ED%8E%98%EC%9D%B4%EC%A7%80-%ED%85%8C%EC%9D%B4%EB%B8%94#entry106comment</comments>
      <pubDate>Fri, 29 May 2026 19:06:51 +0900</pubDate>
    </item>
    <item>
      <title>가상 메모리 ① &amp;mdash; 주소 공간에서 페이징까지</title>
      <link>https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A0-%E2%80%94-%EC%A3%BC%EC%86%8C-%EA%B3%B5%EA%B0%84%EC%97%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%95%EA%B9%8C%EC%A7%80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스는 자신이 0번지부터 시작하는 연속된 메모리를 독차지한다고 믿는다. 실제로는 물리 메모리의 어딘가에, 다른 프로세스들과 섞여, 흩어진 채로 올라가 있다. 이 간극을 메우는 운영체제의 기법이 &lt;b&gt;메모리 가상화(memory virtualization)&lt;/b&gt;이고, 그 결과 프로세스에게 주어지는 추상화가 &lt;b&gt;가상 메모리(virtual memory)&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 글들에서 프로세스는 자신만의 &lt;b&gt;주소 공간(address space)&lt;/b&gt;을 가진다고 했고, CPU 스케줄링에서는 그 프로세스를 언제 실행할지를 다뤘다. 이 글은 그 주소 공간이 실제 물리 메모리 위에 어떻게 올라가는가를 다룬다. 가상 메모리는 한 편에 담기에는 큰 주제라, 이 글(①)에서는 주소 공간이라는 추상화에서 출발해 베이스/바운드, 세그멘테이션을 거쳐 페이징에 도달하기까지를 따라간다. TLB&amp;middot;멀티레벨 페이지 테이블&amp;middot;스와핑은 다음 글(②)로 넘긴다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 메모리를 가상화하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 컴퓨터는 메모리에 관한 추상화가 거의 없었다. 물리 메모리에 운영체제 루틴 한 덩어리와 실행 중인 프로그램 하나가 올라가는 것이 전부였다. 프로그램은 물리 주소를 직접 보고 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기계가 비싸지면서 한 대를 여러 프로그램이 나눠 쓰는 &lt;b&gt;멀티프로그래밍(multiprogramming)&lt;/b&gt;과 &lt;b&gt;시분할(time-sharing)&lt;/b&gt;이 등장했다. 여러 프로세스를 메모리에 동시에 올려 두고 전환하며 실행하니 CPU 활용도가 크게 올랐다. 그런데 여러 프로그램이 한 메모리에 공존하게 되자 새로운 문제가 생겼다. 한 프로세스가 다른 프로세스의 메모리를 읽거나 덮어쓸 수 있다는 것, 즉 &lt;b&gt;보호(protection)&lt;/b&gt;의 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 풀기 위해 운영체제는 물리 메모리를 그대로 노출하는 대신 다루기 쉬운 추상화를 제공한다. 그것이 주소 공간이다. 주소 공간은 실행 중인 프로그램이 보는 메모리 전체이며, 세 부분으로 이뤄진다. 명령어가 담기는 &lt;b&gt;코드&lt;/b&gt;, 함수 호출과 지역 변수가 쌓이는 &lt;b&gt;스택&lt;/b&gt;, 그리고 동적 할당에 쓰이는 &lt;b&gt;힙&lt;/b&gt;이다. 관례적으로 코드는 맨 아래에 고정되고, 힙과 스택은 주소 공간의 양 끝에서 서로 마주 보며 자란다. 힙은 위로, 스택은 아래로 자라며 가운데 빈 공간을 사이에 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램은 이 주소 공간이 0번지부터 시작한다고 가정하고 작성된다. 하지만 실제로 물리 메모리의 0번지에 올라가는 것은 아니다. 운영체제는 프로세스를 물리 메모리의 임의 위치에 배치하고, 프로세스가 그 사실을 모르게 한다. 프로세스가 사용하는 주소를 &lt;b&gt;가상 주소(virtual address)&lt;/b&gt;, 그것이 가리키는 실제 위치를 &lt;b&gt;물리 주소(physical address)&lt;/b&gt;라 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;v1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUJmgd/dJMcaa6yMBe/tJsdbOcEWwtfU8UPtoioI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUJmgd/dJMcaa6yMBe/tJsdbOcEWwtfU8UPtoioI0/img.png&quot; data-alt=&quot;Virtual address / Physical address&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUJmgd/dJMcaa6yMBe/tJsdbOcEWwtfU8UPtoioI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUJmgd%2FdJMcaa6yMBe%2FtJsdbOcEWwtfU8UPtoioI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;634&quot; data-filename=&quot;v1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;634&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Virtual address / Physical address&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 프로세스가 내는 가상 주소를 실제 물리 주소로 바꾸는 일이 매번 필요하다. 이 변환은 CPU 안의 전용 하드웨어인 &lt;b&gt;MMU(Memory Management Unit)&lt;/b&gt;가 메모리 접근이 일어날 때마다 실시간으로 수행한다. CPU가 가상 주소를 내면 MMU가 이를 물리 주소로 바꿔 메모리에 전달한다. 프로그램은 이 과정을 전혀 의식하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;v2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH0Jwn/dJMcacJ5rBV/vpHHsn47f1CUx6QbxWVfSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH0Jwn/dJMcacJ5rBV/vpHHsn47f1CUx6QbxWVfSK/img.png&quot; data-alt=&quot;MMU&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH0Jwn/dJMcacJ5rBV/vpHHsn47f1CUx6QbxWVfSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH0Jwn%2FdJMcacJ5rBV%2FvpHHsn47f1CUx6QbxWVfSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;461&quot; data-filename=&quot;v2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MMU&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 메모리가 갖춰야 할 목표는 셋이다. 첫째는 &lt;b&gt;투명성(transparency)&lt;/b&gt;으로, 프로세스가 메모리가 가상화되었다는 사실을 알아차리지 못해야 한다. 둘째는 &lt;b&gt;효율성(efficiency)&lt;/b&gt;으로, 변환이 프로그램을 크게 느리게 하거나 너무 많은 메모리를 잡아먹지 않아야 한다. 셋째는 &lt;b&gt;보호&lt;/b&gt;로, 한 프로세스가 다른 프로세스나 운영체제의 메모리에 영향을 줄 수 없어야 한다. 아래에서 보는 기법들은 이 세 목표를 어떻게 달성하는지, 그리고 어디서 부족한지를 차례로 드러낸다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;베이스와 바운드 &amp;mdash; 첫 번째 기법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 변환 방법은 주소 공간을 물리 메모리의 한 자리에 통째로 올리고, 시작 위치만큼 더해 주는 것이다. 1950년대 후반 시분할 시스템에서 도입된 &lt;b&gt;베이스와 바운드(base and bounds)&lt;/b&gt;, 또는 &lt;b&gt;동적 재배치(dynamic relocation)&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU에 레지스터 두 개를 둔다. 베이스 레지스터는 주소 공간이 올라간 물리 메모리의 시작 위치를 담고, 바운드 레지스터는 주소 공간의 크기를 담는다. 변환은 덧셈 하나로 끝난다.&lt;/p&gt;
&lt;pre class=&quot;fix&quot;&gt;&lt;code&gt;물리 주소 = 가상 주소 + 베이스&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램은 0번지부터 시작한다고 가정하고 작성&amp;middot;컴파일된다. 운영체제가 프로세스를 물리 메모리의 어디에 올릴지 정해 베이스 레지스터를 설정하면, 이후 모든 메모리 참조에서 하드웨어가 자동으로 베이스를 더한다. 4KB 주소 공간을 가진 프로세스를 물리 메모리 16KB 위치에 올렸다면, 가상 주소 0은 물리 주소 16KB로, 가상 주소 3000은 물리 주소 19384로 변환된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;v3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LnL7s/dJMcafGNs5W/ievcpDvcZyha2sDSoi8evK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LnL7s/dJMcafGNs5W/ievcpDvcZyha2sDSoi8evK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LnL7s/dJMcafGNs5W/ievcpDvcZyha2sDSoi8evK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLnL7s%2FdJMcafGNs5W%2FievcpDvcZyha2sDSoi8evK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;547&quot; data-filename=&quot;v3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보호는 바운드 레지스터가 맡는다. 가상 주소가 주소 공간 크기를 벗어나거나 음수이면 하드웨어가 예외를 일으켜 운영체제로 넘기고, 운영체제는 해당 프로세스를 종료한다. 위의 예에서 가상 주소 4400은 4KB 범위를 넘으므로 오류가 된다. 베이스와 바운드 레지스터를 담은 이 하드웨어가 앞서 말한 MMU의 가장 단순한 형태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베이스와 바운드는 적은 하드웨어로 투명성&amp;middot;효율성&amp;middot;보호를 모두 얻는다. 하지만 한계가 있다. 주소 공간을 통째로 한 자리에 올리기 때문에, 힙과 스택 사이의 쓰지 않는 빈 공간까지 물리 메모리에 자리를 차지한다. 이렇게 할당된 영역 안쪽에서 낭비되는 공간을 &lt;b&gt;내부 단편화(internal fragmentation)&lt;/b&gt;라 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세그멘테이션 &amp;mdash; 일반화된 베이스/바운드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 단편화는 주소 공간을 하나의 덩어리로 다룬 데서 비롯된다. 주소 공간을 코드&amp;middot;힙&amp;middot;스택이라는 논리적 단위로 나누고, 각 단위마다 베이스와 바운드 쌍을 따로 두는 기법이 &lt;b&gt;세그멘테이션(segmentation)&lt;/b&gt;이다. 베이스/바운드를 한 쌍에서 여러 쌍으로 일반화한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 세그먼트를 물리 메모리의 서로 다른 위치에 독립적으로 배치하고, 실제로 쓰는 세그먼트만 메모리에 올린다. 힙과 스택 사이의 빈 공간은 더 이상 물리 메모리를 차지하지 않는다. 빈 곳이 많은 큰 주소 공간, 즉 희소 주소 공간(sparse address space)을 효율적으로 담을 수 있게 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;v4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaV3Cr/dJMcajvCPab/WieuFS0soGKXA6eBfmkAK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaV3Cr/dJMcajvCPab/WieuFS0soGKXA6eBfmkAK0/img.png&quot; data-alt=&quot;Segmentation&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaV3Cr/dJMcajvCPab/WieuFS0soGKXA6eBfmkAK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaV3Cr%2FdJMcajvCPab%2FWieuFS0soGKXA6eBfmkAK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;662&quot; data-filename=&quot;v4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Segmentation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환에는 &quot;이 가상 주소가 어느 세그먼트에 속하는가&quot;를 먼저 가려내는 단계가 필요하다. 한 가지 방법은 가상 주소의 상위 비트를 세그먼트 번호로 쓰는 것이다. 세그먼트가 셋이면 상위 2비트로 구분한다. 나머지 하위 비트는 세그먼트 안에서의 오프셋이 된다. 하드웨어는 상위 비트로 세그먼트를 정하고, 오프셋이 그 세그먼트의 바운드를 넘지 않는지 검사한 뒤, 해당 세그먼트의 베이스를 더해 물리 주소를 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택은 다른 세그먼트와 성장 방향이 반대라 변환에 한 가지 손질이 더 필요하다. 코드와 힙은 낮은 주소에서 높은 주소로 자라지만, 스택은 높은 주소에서 낮은 주소로 자란다. 그래서 세그먼트가 어느 방향으로 자라는지를 나타내는 비트를 하드웨어에 추가하고, 스택의 오프셋은 음수로 계산한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;v5 (1).png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;677&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVFOrg/dJMb99Ng5xG/Ar4OwuzGBz2T1YSbK4any0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVFOrg/dJMb99Ng5xG/Ar4OwuzGBz2T1YSbK4any0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVFOrg/dJMb99Ng5xG/Ar4OwuzGBz2T1YSbK4any0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVFOrg%2FdJMb99Ng5xG%2FAr4OwuzGBz2T1YSbK4any0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;677&quot; data-filename=&quot;v5 (1).png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;677&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 최대 세그먼트 크기가 4KB이고 스택 베이스가 물리 28KB일 때, 가상 주소 15KB를 변환해 보자. 상위 2비트로 스택 세그먼트임을 가려내면 하위 비트가 가리키는 오프셋은 3KB다. 스택은 역방향이므로 이 오프셋을 그대로 쓰지 않고, 최대 크기(주소로 표현 가능한 4KB)에서 뺀 값을 음수 오프셋으로 삼는다. 즉 3KB &amp;minus; 4KB = &amp;minus;1KB다. 여기에 베이스를 더하면 28KB + (&amp;minus;1KB) = 27KB가 최종 물리 주소가 된다. 바운드 검사도 절댓값으로 한다. 이 접근의 음수 오프셋 크기 1KB가 스택에 실제 할당된 크기 2KB 안에 들어오므로 정상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세그멘테이션은 코드 공유라는 이점도 준다. 코드 세그먼트는 보통 읽기 전용이라 여러 프로세스가 같은 물리 코드 세그먼트를 공유해도 안전하다. 이를 위해 각 세그먼트에 읽기&amp;middot;쓰기&amp;middot;실행 권한을 나타내는 &lt;b&gt;보호 비트(protection bit)&lt;/b&gt;를 두고, 하드웨어가 변환 때 권한까지 함께 검사한다. 같은 프로그램을 여러 번 실행해도 코드 사본을 하나만 메모리에 두면 되니 메모리가 절약된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세그멘테이션은 내부 단편화를 크게 줄였지만 새로운 결함을 안고 있다. 세그먼트마다 크기가 다르기 때문에, 프로세스가 생기고 사라지기를 반복하면 물리 메모리 곳곳에 크고 작은 빈틈이 흩어진다. 전체 빈 공간을 합치면 충분한데도 연속된 자리가 없어 새 세그먼트를 못 올리는 상황이 생긴다. 이렇게 할당된 영역 바깥에 쓸 수 없는 빈틈이 흩어지는 것을 &lt;b&gt;외부 단편화(external fragmentation)&lt;/b&gt;라 한다. 가변 크기로 메모리를 나누는 한 외부 단편화는 따라붙는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;페이징 &amp;mdash; 고정 크기로 문제를 피하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 단편화는 세그먼트의 크기가 제각각이라는 데서 나온다. 그렇다면 메모리를 가변 크기로 나누지 않으면 된다. &lt;b&gt;페이징(paging)&lt;/b&gt;은 주소 공간을 모두 같은 크기의 조각으로 자른다. 가상 주소 공간을 자른 고정 크기 조각을 &lt;b&gt;페이지(page)&lt;/b&gt;, 물리 메모리를 같은 크기로 자른 조각을 &lt;b&gt;프레임(frame)&lt;/b&gt;이라 한다. 모든 조각의 크기가 같으니 어떤 페이지든 어떤 빈 프레임에나 들어갈 수 있고, 외부 단편화는 원천적으로 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 예로 64바이트 가상 주소 공간을 16바이트 페이지 4개로, 128바이트 물리 메모리를 16바이트 프레임 8개로 나눈 경우를 보자. 가상 페이지들은 물리 메모리의 빈 프레임 아무 데나 흩어져 올라간다. 어느 가상 페이지가 어느 물리 프레임에 들어갔는지는 프로세스마다 따로 관리하는 자료구조인 &lt;b&gt;페이지 테이블(page table)&lt;/b&gt;에 기록한다. 페이지 테이블은 &lt;b&gt;가상 페이지 번호(VPN, Virtual Page Number)&lt;/b&gt;를 &lt;b&gt;물리 프레임 번호(PFN, Physical Frame Number)&lt;/b&gt;로 매핑한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 주소는 두 부분으로 나뉜다. 윗부분은 어느 페이지인지를 가리키는 VPN, 아랫부분은 그 페이지 안에서의 위치를 가리키는 오프셋이다. 변환은 VPN을 페이지 테이블에서 찾아 PFN으로 바꾸고, 오프셋은 그대로 붙이는 식으로 이뤄진다. 페이지 안에서의 상대 위치는 가상이든 물리든 같으므로 오프셋은 변환되지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;v6 (1).png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bECCvR/dJMcaak8gTN/SuPqbjZd4I85KrLeWYBsJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bECCvR/dJMcaak8gTN/SuPqbjZd4I85KrLeWYBsJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bECCvR/dJMcaak8gTN/SuPqbjZd4I85KrLeWYBsJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbECCvR%2FdJMcaak8gTN%2FSuPqbjZd4I85KrLeWYBsJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;698&quot; data-filename=&quot;v6 (1).png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예에서 &lt;code&gt;movl 21, %eax&lt;/code&gt;가 가상 주소 21에 접근한다고 하자. 21을 6비트 이진수로 쓰면 010101이다. 64바이트 공간에 16바이트 페이지이므로 상위 2비트가 VPN, 하위 4비트가 오프셋이다. VPN은 01 즉 1, 오프셋은 0101 즉 5다. 페이지 테이블에서 가상 페이지 1이 물리 프레임 7에 매핑돼 있다면, PFN 7(111)에 오프셋 5(0101)를 붙여 물리 주소 1110101, 즉 117을 얻는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;페이지 테이블의 각 항목(PTE, Page Table Entry)&lt;/b&gt;에는 PFN 외에도 여러 비트가 들어간다. &lt;b&gt;유효 비트(valid bit)&lt;/b&gt;는 그 매핑이 유효한지를 나타낸다. 코드&amp;middot;힙&amp;middot;스택 사이의 쓰지 않는 페이지들을 무효로 표시해 두면 물리 프레임을 할당할 필요가 없어, 희소 주소 공간을 메모리 낭비 없이 지원할 수 있다. 보호 비트는 읽기&amp;middot;쓰기&amp;middot;실행 권한을, &lt;b&gt;현재 비트(present bit)&lt;/b&gt;는 페이지가 물리 메모리에 있는지 아니면 디스크로 밀려났는지를 나타낸다. &lt;b&gt;더티 비트(dirty bit)&lt;/b&gt;는 페이지가 수정됐는지를, &lt;b&gt;참조 비트(reference bit)&lt;/b&gt;는 최근 접근됐는지를 기록한다. 뒤의 두 비트는 다음 글에서 다룰 페이지 교체에서 쓰인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이징은 외부 단편화를 없애고 희소 주소 공간을 깔끔히 지원하지만, 그 대가로 두 가지 새로운 문제를 안고 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 &lt;b&gt;크기&lt;/b&gt;다. 페이지 테이블은 가상 페이지마다 항목을 하나씩 가진다. 32비트 주소 공간에 4KB 페이지라면 VPN이 20비트이므로 항목이 약 100만 개, 한 항목이 4바이트라면 프로세스 하나당 페이지 테이블이 4MB에 이른다. 프로세스 100개가 돌면 오직 주소 변환을 위해서만 400MB가 든다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;VPN&amp;middot;오프셋 비트 빠르게 구하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프셋 비트 = log₂(페이지 크기) &amp;mdash; 페이지 안의 모든 바이트를 가리킬 수 있어야 하므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPN 비트 = 전체 주소 비트 &amp;minus; 오프셋 비트 &amp;mdash; 남는 비트가 페이지 번호&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: 32비트 주소 공간 + 4KB 페이지 &amp;rarr; 오프셋 log₂(4KB)=12비트, VPN = 32&amp;minus;12 = 20비트. 페이지 테이블 항목 수 = 2^(VPN 비트) = 2&amp;sup2;⁰ &amp;asymp; 100만 개.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 하나는 &lt;b&gt;속도&lt;/b&gt;다. 페이지 테이블이 메모리에 있으므로, 모든 메모리 접근마다 페이지 테이블을 한 번 읽어 변환 정보를 가져온 뒤 실제 데이터를 읽어야 한다. 메모리 접근이 두 배가 되어, 페이징은 프로그램을 두 배가량 느리게 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 문제 &amp;mdash; 페이지 테이블이 너무 크고, 변환이 너무 느린 것 &amp;mdash; 를 푸는 것이 다음 글의 주제다. 속도는 변환 결과를 캐시하는 TLB로, 크기는 페이지 테이블 자체를 계층화하는 멀티레벨 페이지 테이블로 다룬다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;v6.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;677&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VTr5M/dJMcabYHmC0/gUdvHj7p9iYD7AxSPk9zOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VTr5M/dJMcabYHmC0/gUdvHj7p9iYD7AxSPk9zOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VTr5M/dJMcabYHmC0/gUdvHj7p9iYD7AxSPk9zOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVTr5M%2FdJMcabYHmC0%2FgUdvHj7p9iYD7AxSPk9zOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;677&quot; data-filename=&quot;v6.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;677&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리 가상화의 발전은 한 기법의 결함이 다음 기법의 출발점이 되는 사슬이었다. 베이스와 바운드는 적은 하드웨어로 재배치와 보호를 얻었지만 주소 공간을 통째로 올려 내부 단편화를 남겼다. 세그멘테이션은 주소 공간을 논리 단위로 쪼개 내부 단편화를 줄였지만 가변 크기 탓에 외부 단편화를 낳았다. 페이징은 고정 크기 조각으로 외부 단편화를 없앴지만 페이지 테이블의 크기와 변환 속도라는 새 문제를 들여왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 두 문제를 푸는 TLB와 멀티레벨 페이지 테이블, 그리고 물리 메모리보다 큰 주소 공간을 가능케 하는 스와핑까지 다룬다. 페이징을 작동하게 만드는 데서 나아가 잘 작동하게 만드는 단계다.&lt;/p&gt;</description>
      <category>OS</category>
      <category>AddressSpace</category>
      <category>CSAPP</category>
      <category>OSTEP</category>
      <category>paging</category>
      <category>segmentation</category>
      <category>VirtualMemory</category>
      <category>vm</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/105</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EA%B0%80%EC%83%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%E2%91%A0-%E2%80%94-%EC%A3%BC%EC%86%8C-%EA%B3%B5%EA%B0%84%EC%97%90%EC%84%9C-%ED%8E%98%EC%9D%B4%EC%A7%95%EA%B9%8C%EC%A7%80#entry105comment</comments>
      <pubDate>Fri, 29 May 2026 02:16:40 +0900</pubDate>
    </item>
    <item>
      <title>CPU 스케줄링</title>
      <link>https://onebrotravel.tistory.com/entry/CPU-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 스케줄링은 준비 큐(ready queue)에 있는 여러 프로세스 가운데 누구에게 CPU를 줄지, 얼마 동안 줄지를 결정하는 운영체제의 정책(policy)이다. 실행을 기다리는 프로세스는 여럿이고 CPU는 한정돼 있으므로, 그 선택을 맡은 구성 요소가 필요하다. 이것이 CPU 스케줄러다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 컨텍스트 스위치(context switch)를 다뤘다. 실행 중인 프로세스를 멈추고 다른 프로세스로 갈아타는 메커니즘, 즉 &lt;b&gt;방법&lt;/b&gt;은 거기서 정리됐다. 스케줄링은 그 메커니즘을 언제, 누구에게 쓸지를 정하는 위층의 문제다. 프로세스의 상태 전환을 누가 어떤 기준으로 결정하는가 &amp;mdash; 그 기준을 정의한 것이 스케줄링 정책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책을 비교하려면 잣대가 필요하다. 이 글에서 쓰는 지표는 둘이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/osSj7/dJMcadhTOHl/jEq8j6NC3kt7NsyEpGKbdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/osSj7/dJMcadhTOHl/jEq8j6NC3kt7NsyEpGKbdk/img.png&quot; data-alt=&quot;응답시간 vs 반환시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/osSj7/dJMcadhTOHl/jEq8j6NC3kt7NsyEpGKbdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FosSj7%2FdJMcadhTOHl%2FjEq8j6NC3kt7NsyEpGKbdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;518&quot; data-filename=&quot;d1.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;응답시간 vs 반환시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;반환시간(turnaround time)&lt;/b&gt;: 작업이 도착한 순간부터 끝나는 순간까지의 시간이다. &lt;code&gt;T_반환 = T_완료 &amp;minus; T_도착&lt;/code&gt;. 작업을 얼마나 빨리 완료시키는가를 본다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답시간(response time)&lt;/b&gt;: 작업이 도착한 순간부터 처음 실행되는 순간까지의 시간이다. &lt;code&gt;T_응답 = T_첫실행 &amp;minus; T_도착&lt;/code&gt;. 반응이 얼마나 빠른가를 본다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 지표는 종종 충돌한다. 한 정책이 반환시간을 줄이면 응답시간을 잃고, 그 반대도 마찬가지인 경우가 많다. 아래에서 다루는 정책들은 앞선 정책의 약점을 보완하며 발전해 왔다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FIFO &amp;mdash; 가장 단순한 출발점&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccpYRw/dJMcagyS6YK/aVig3aqmnbDAaN7hjrZqPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccpYRw/dJMcagyS6YK/aVig3aqmnbDAaN7hjrZqPK/img.png&quot; data-alt=&quot;FIFO&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccpYRw/dJMcagyS6YK/aVig3aqmnbDAaN7hjrZqPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccpYRw%2FdJMcagyS6YK%2FaVig3aqmnbDAaN7hjrZqPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;490&quot; data-filename=&quot;d2.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FIFO&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 정책은 도착한 순서대로 처리하는 것이다. 먼저 온 작업이 먼저 실행되고, 끝날 때까지 CPU를 점유한다. &lt;b&gt;선입선출(First In, First Out)이라 FIFO&lt;/b&gt;, 먼저 온 순서대로 서비스한다는 뜻에서 &lt;b&gt;FCFS(First Come, First Served)&lt;/b&gt;라고도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FIFO는 단순하고 구현이 쉽다. 작업들의 길이가 비슷할 때는 성능도 나쁘지 않다. 세 작업 A&amp;middot;B&amp;middot;C가 거의 동시에 도착해 각각 10초씩 걸린다면, A는 10초, B는 20초, C는 30초에 끝나 평균 반환시간은 (10+20+30)/3 = 20초가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 작업 길이가 제각각일 때 나타난다. A가 100초, B와 C가 각각 10초이고 이 순서로 도착하면, A가 100초 동안 CPU를 점유한 뒤에야 B와 C가 실행된다. 평균 반환시간은 (100+110+120)/3 = 110초로 크게 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 작업들이 긴 작업 하나 뒤에 줄지어 막히는 이 현상을 &lt;b&gt;호위 효과(convoy effect)&lt;/b&gt;라 부른다. 자원을 잠깐만 쓰면 되는 작업들이 자원을 오래 쥔 작업 뒤에 묶여 한꺼번에 대기하게 되는 상황이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SJF &amp;mdash; 짧은 작업 먼저&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rVIvt/dJMcaipXkAw/cdjSJ1TYxb0uHIOPh0vKKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rVIvt/dJMcaipXkAw/cdjSJ1TYxb0uHIOPh0vKKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rVIvt/dJMcaipXkAw/cdjSJ1TYxb0uHIOPh0vKKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrVIvt%2FdJMcaipXkAw%2FcdjSJ1TYxb0uHIOPh0vKKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;504&quot; data-filename=&quot;d3.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호위 효과는 긴 작업이 짧은 작업 앞에 놓이는 데서 생긴다. 짧은 작업을 먼저 실행하는 정책이 &lt;b&gt;최단 작업 우선(Shortest Job First), SJF&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 예시 &amp;mdash; A=100초, B&amp;middot;C=10초, 거의 동시 도착 &amp;mdash; 에 SJF를 적용하면 B, C, A 순으로 실행된다. B는 10초, C는 20초, A는 120초에 끝나 평균 반환시간은 (10+20+120)/3 = 50초다. 110초에서 50초로 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 작업이 동시에 도착한다는 가정 아래에서 SJF는 평균 반환시간을 최소화하는 최적 정책임이 증명돼 있다. 짧은 작업을 앞으로 당길 때 줄어드는 대기 시간이, 뒤로 밀린 긴 작업이 더 기다리는 시간보다 항상 크기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SJF에는 &lt;b&gt;두 가지&lt;/b&gt; 한계가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째는 &lt;b&gt;작업 길이를 미리 알아야 한다는 점&lt;/b&gt;이다. SJF는 가장 짧은 작업을 골라야 하는데, 실제 운영체제는 작업이 얼마나 걸릴지 대개 알지 못한다. 이 가정은 스케줄러가 미래를 내다본다고 전제하는 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째는 &lt;b&gt;늦게 도착한 짧은 작업이 여전히 막힌다&lt;/b&gt;는 점이다. SJF는 한번 시작한 작업을 끝까지 실행하는 비선점(non-preemptive) 정책이다. A가 0초에 도착해 100초를 실행하기 시작한 뒤 10초 시점에 B와 C가 도착하면, 짧은 B&amp;middot;C가 도착했더라도 실행 중인 A를 멈출 수단이 없어 둘은 A가 끝나기를 기다린다. 이 경우 평균 반환시간은 다시 103.33초로 나빠진다. 호위 효과가 재현되는 셈이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;STCF &amp;mdash; 선점을 더하다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brG2FB/dJMcabj6OW2/4psKIuhEdZeF157KmonKvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brG2FB/dJMcabj6OW2/4psKIuhEdZeF157KmonKvK/img.png&quot; data-alt=&quot;STCF&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brG2FB/dJMcabj6OW2/4psKIuhEdZeF157KmonKvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrG2FB%2FdJMcabj6OW2%2F4psKIuhEdZeF157KmonKvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;504&quot; data-filename=&quot;d4.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;STCF&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SJF의 둘째 한계는 실행 중인 작업을 멈출 수 없다는 데서 비롯된다. 앞 글에서 본 타이머 인터럽트와 컨텍스트 스위치가 그 멈춤을 가능하게 하는 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SJF에 &lt;b&gt;선점(preemption)&lt;/b&gt;을 더한 정책이 &lt;b&gt;최단 잔여시간 우선(Shortest Time-to-Completion First), STCF&lt;/b&gt;다. 선점형 SJF라고도 한다. 새 작업이 도착할 때마다 스케줄러는 남아 있는 작업들과 새 작업의 잔여 시간을 비교해, 가장 적게 남은 작업을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 늦은 도착 시나리오에 STCF를 적용하면, A가 100초를 실행하던 중 10초 시점에 B&amp;middot;C가 도착했을 때 A를 멈추고 더 짧은 B&amp;middot;C를 먼저 끝낸 뒤 A의 나머지를 마저 실행한다. 평균 반환시간은 다시 50초가 된다. 도착 시점이 제각각이어도 STCF는 반환시간 면에서 최적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 길이를 알고, 작업이 CPU만 사용하며, 잣대가 반환시간뿐이라면 STCF는 충분히 좋은 정책이다. 실제로 초기 배치(batch) 컴퓨팅 환경에서는 이런 정책이 잘 맞았다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새 지표 &amp;mdash; 응답시간&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전제를 바꾼 것이 &lt;b&gt;시분할(time-sharing)&lt;/b&gt; 시스템과 &lt;b&gt;대화형(interactive)&lt;/b&gt; 사용자의 등장이다. 사용자가 터미널 앞에 앉아 입력하고 즉각적인 반응을 기대하면서, &quot;언제 끝나는가&quot;만으로는 정책을 평가하기에 부족해졌다. &quot;언제 처음 반응하는가&quot;가 중요해졌고, 그 잣대가 앞서 정의한 응답시간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STCF 계열은 응답시간에 약하다. 작업 셋이 동시에 도착하면 셋째 작업은 앞의 둘이 통째로 끝난 뒤에야 처음 실행된다. 터미널에 입력해 두고 다른 작업이 끝날 때까지 첫 반응을 기다려야 한다면 대화형 환경에는 적합하지 않다. 반환시간에 강한 정책이 응답시간에는 약하다는 점이 여기서 드러난다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Round Robin &amp;mdash; 번갈아 돌리기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d5.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/psQTk/dJMcacDjpv1/A87UGdmLJAw88KHoPWgmj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/psQTk/dJMcacDjpv1/A87UGdmLJAw88KHoPWgmj0/img.png&quot; data-alt=&quot;Round Robin&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/psQTk/dJMcacDjpv1/A87UGdmLJAw88KHoPWgmj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpsQTk%2FdJMcacDjpv1%2FA87UGdmLJAw88KHoPWgmj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;518&quot; data-filename=&quot;d5.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Round Robin&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;라운드 로빈(Round Robin, RR)&lt;/b&gt;은 각 작업을 끝까지 실행하는 대신, &lt;b&gt;타임 슬라이스(time slice, 스케줄링 퀀텀이라고도 한다)&lt;/b&gt;만큼만 실행하고 다음 작업으로 넘어간다. 준비 큐를 한 바퀴 돌고 또 도는 방식이라 타임 슬라이싱이라고도 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 작업 A&amp;middot;B&amp;middot;C가 동시에 도착해 각각 5초씩 걸리고 타임 슬라이스가 1초라고 하자. RR은 A&amp;middot;B&amp;middot;C&amp;middot;A&amp;middot;B&amp;middot;C&amp;hellip; 순으로 잘게 번갈아 실행한다. A의 응답시간은 0, B는 1, C는 2로 평균 1초다. 같은 작업을 SJF로 실행하면 응답시간은 0, 5, 10으로 평균 5초였다. 응답시간이 크게 개선된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임 슬라이스 길이가 성능을 좌우한다. 짧을수록 응답시간에는 유리하지만, 너무 짧으면 컨텍스트 스위치 비용이 전체 성능을 잠식한다. 슬라이스가 10ms이고 스위치 비용이 1ms라면 시간의 약 10%가 스위칭에 쓰인다. 슬라이스를 100ms로 늘리면 그 비율은 1% 아래로 떨어진다. 고정 비용을 더 드물게 치러 총비용을 낮추는 &lt;b&gt;분할상환(amortization)&lt;/b&gt;이다. 슬라이스는 스위치 비용을 상쇄할 만큼 길되, 시스템이 둔해질 만큼 길지는 않은 선에서 정해진다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;컨텍스트 스위치 비용은 레지스터를 저장하고 복원하는 데서만 오는 것이 아니다. 프로그램이 실행되면서 CPU 캐시&amp;middot;TLB&amp;middot;분기 예측기에 쌓아 둔 상태가 작업을 바꾸면 씻겨 나가고, 새 작업의 상태로 다시 채워야 한다. 이 보이지 않는 비용이 적지 않다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 RR은 반환시간에 약하다. 앞의 예시에서 A는 13초, B는 14초, C는 15초에 끝나 평균 반환시간은 14초로, FIFO보다도 나쁠 때가 많다. RR이 모든 작업을 잘게 늘여 실행하기 때문이다. 작업이 끝나는 시점만 보는 반환시간 입장에서는 불리할 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 우연이 아니라 본질적인 trade-off다. CPU를 작은 단위로 공평하게 나누는 정책은 응답시간을 얻는 대신 반환시간을 잃고, 공평함을 포기하면 반환시간을 얻는 대신 응답시간을 잃는다. 두 지표를 동시에 최적화할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 두 갈래다. &lt;b&gt;SJF&amp;middot;STCF는 반환시간에 강하고 응답시간에 약하다&lt;/b&gt;. &lt;b&gt;RR은 응답시간에 강하고 반환시간에 약하다&lt;/b&gt;. 그리고 아직 풀지 않은 가정이 둘 남아 있다. 작업이 I/O를 한다는 것, 그리고 작업 길이를 모른다는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;I/O를 끼워넣기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 &lt;b&gt;작업이 CPU만 사용한다고 가정&lt;/b&gt;했다. 현실의 프로그램은 입력을 읽고 출력을 쓴다. 작업이 I/O를 요청하면 그동안 CPU를 사용하지 못하고 &lt;b&gt;블록(blocked)&lt;/b&gt;된다. 디스크 I/O라면 수 밀리초 이상 멈출 수도 있다. 이때 CPU를 놀리는 것은 낭비이므로, 스케줄러는 다른 작업을 올려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A와 B 두 작업이 각각 50ms의 CPU 시간을 필요로 한다고 하자. A는 10ms를 실행하고 I/O를 거는 패턴을 다섯 번 반복하고(I/O는 각 10ms), B는 50ms를 통째로 CPU만 사용한다. A를 다 실행한 뒤 B를 실행하면 A가 I/O로 블록된 동안 CPU가 놀게 된다. 자원을 비효율적으로 쓰는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 A의 10ms짜리 CPU 구간 하나하나를 독립된 작은 작업처럼 취급하면, A가 I/O로 블록된 사이에 B를 CPU에 올려 겹쳐(overlap) 실행할 수 있다. CPU와 디스크가 동시에 동작하므로 시스템 활용도가 올라간다. 각 CPU 버스트를 작업 단위로 보는 이 방식 덕분에, I/O를 자주 하는 대화형 작업은 자연스럽게 자주 실행된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MLFQ &amp;mdash; 길이를 모른 채 SJF에 다가가기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;d6.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BJZ6l/dJMcacccz9o/GiQGxuKM7sGTP6kMgZPMc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BJZ6l/dJMcacccz9o/GiQGxuKM7sGTP6kMgZPMc0/img.png&quot; data-alt=&quot;MLFQ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BJZ6l/dJMcacccz9o/GiQGxuKM7sGTP6kMgZPMc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBJZ6l%2FdJMcacccz9o%2FGiQGxuKM7sGTP6kMgZPMc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;590&quot; data-filename=&quot;d6.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;MLFQ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 가정은 &lt;b&gt;스케줄러가 작업 길이를 안다는 것&lt;/b&gt;이었다. 가장 비현실적인 가정이다. &lt;b&gt;다단계 피드백 큐(Multi-Level Feedback Queue, MLFQ)&lt;/b&gt;는 작업 길이를 모르고도 SJF처럼 짧은 작업을 먼저 실행하고, 동시에 RR처럼 응답시간을 확보하는 정책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MLFQ의 핵심은 과거 행동으로 미래를 예측하는 데 있다. 작업이 그동안 어떻게 행동했는지 관찰해, 짧은 대화형 작업인지 긴 CPU 위주 작업인지 추정한다. 이를 위해 MLFQ는 우선순위가 다른 여러 개의 큐를 두고, 높은 큐에 있는 작업을 먼저 실행한다. 규칙을 차례로 쌓아 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 우선순위로 누구를 고를지 정하는 두 규칙이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 1&lt;/b&gt;: Priority(A) &amp;gt; Priority(B)이면 A를 실행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 2&lt;/b&gt;: Priority(A) = Priority(B)이면 A와 B를 RR로 번갈아 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위를 고정해 두면 의미가 없다. MLFQ의 핵심은 우선순위를 작업의 행동에 따라 바꾸는 데 있고, 다음 세 규칙이 그 변화를 정의한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 3&lt;/b&gt;: 작업이 시스템에 들어오면 가장 높은 우선순위(최상위 큐)에 놓는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 4a&lt;/b&gt;: 주어진 타임 슬라이스를 다 쓰면 우선순위를 한 단계 낮춘다(큐를 한 칸 내린다).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 4b&lt;/b&gt;: 슬라이스를 다 쓰기 전에 CPU를 양보하면 같은 우선순위를 유지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 규칙들은 새 작업을 일단 짧은 작업으로 가정해 최상위에 둔다. 정말 짧은 작업이면 금세 끝나고, 긴 작업이면 슬라이스를 거듭 다 쓰며 차츰 아래로 내려가 스스로 긴 작업임을 드러낸다. 이런 방식으로 MLFQ는 작업 길이를 모른 채 SJF를 근사한다. 규칙 4b 덕분에, I/O를 자주 걸어 슬라이스를 다 쓰기 전에 양보하는 대화형 작업은 높은 우선순위에 머물러 응답성도 확보된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기본형에는 두 가지 결함이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 &lt;b&gt;기아(starvation)&lt;/b&gt;다. 대화형 작업이 너무 많으면 이들이 CPU를 모두 차지해, 아래로 내려간 긴 작업이 영영 실행되지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 하나는 &lt;b&gt;스케줄러 게이밍(gaming)&lt;/b&gt;이다. 사용자가 규칙 4b를 악용할 수 있다. 슬라이스가 끝나기 직전에 불필요한 I/O를 한 번 걸어 CPU를 양보하면, 같은 큐에 머물면서 높은 우선순위를 유지한다. 슬라이스의 99%를 사용하고 양보하기를 반복하면 CPU를 거의 독점할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기아를 막기 위한 규칙이 추가된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 5&lt;/b&gt;: 일정 시간 S가 지날 때마다 시스템의 모든 작업을 최상위 큐로 옮긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주기적인 &lt;b&gt;우선순위 부스트(priority boost)&lt;/b&gt;다. 아래에 깔려 있던 긴 작업도 주기적으로 최상위로 올라와 실행 기회를 얻으므로 굶지 않는다. CPU 위주로 동작하다가 대화형으로 성격이 바뀐 작업도 부스트를 통해 다시 높은 우선순위를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이밍은 누적 사용량을 기준으로 삼아 막는다. 원인이 된 규칙 4a&amp;middot;4b는 슬라이스를 다 쓰기 전에 양보하면 우선순위를 유지하게 했다. 양보 횟수와 무관하게 한 큐에서 쓴 시간의 총량을 기준으로 강등하면 이 허점이 사라진다. 규칙 4a&amp;middot;4b를 하나로 합친다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 4&lt;/b&gt;: 한 큐에서 주어진 시간 할당량을 (몇 번에 나눠 썼든) 다 쓰면 우선순위를 낮춘다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 한 번에 길게 쓰든 잘게 나눠 쓰든 누적 사용량이 차면 강등되므로, I/O를 끼워 넣는 방식으로는 우선순위를 유지할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다듬어진 MLFQ의 최종 규칙은 다섯이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;규칙 1&lt;/b&gt;: Priority(A) &amp;gt; Priority(B)이면 A를 실행한다(B는 실행하지 않는다).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 2&lt;/b&gt;: Priority(A) = Priority(B)이면 A와 B를 RR로 실행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 3&lt;/b&gt;: 작업이 시스템에 들어오면 가장 높은 우선순위(최상위 큐)에 놓는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 4&lt;/b&gt;: 한 큐에서 시간 할당량을 다 쓰면(몇 번에 나눠 양보했든) 우선순위를 낮춘다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 5&lt;/b&gt;: 일정 시간 S가 지나면 모든 작업을 최상위 큐로 옮긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MLFQ는 작업에 대한 사전 지식을 요구하는 대신 실행을 관찰해 우선순위를 매긴다. 그 결과 짧은 대화형 작업에는 SJF&amp;middot;STCF에 가까운 성능을, 긴 CPU 위주 작업에는 공평한 진척을 함께 제공한다. BSD 계열 UNIX, Solaris, Windows NT 이후의 Windows 등 많은 시스템이 MLFQ 형태를 기본 스케줄러로 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리 &amp;mdash; 무엇을 언제 쓰나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다섯 정책이 같은 문제를 어떻게 다르게 푸는지 정리하면 다음과 같다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;정책&lt;/th&gt;
&lt;th&gt;선점&lt;/th&gt;
&lt;th&gt;길이 사전지식&lt;/th&gt;
&lt;th&gt;반환시간&lt;/th&gt;
&lt;th&gt;응답시간&lt;/th&gt;
&lt;th&gt;한 줄&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FIFO&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;불필요&lt;/td&gt;
&lt;td&gt;나쁨 (호위 효과)&lt;/td&gt;
&lt;td&gt;나쁨&lt;/td&gt;
&lt;td&gt;도착 순서대로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SJF&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;필요&lt;/td&gt;
&lt;td&gt;최적 (동시 도착)&lt;/td&gt;
&lt;td&gt;나쁨&lt;/td&gt;
&lt;td&gt;짧은 작업 먼저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STCF&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;필요&lt;/td&gt;
&lt;td&gt;최적&lt;/td&gt;
&lt;td&gt;나쁨&lt;/td&gt;
&lt;td&gt;선점형 SJF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round Robin&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;불필요&lt;/td&gt;
&lt;td&gt;나쁨&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;잘게 번갈아&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MLFQ&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;불필요&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;좋음&lt;/td&gt;
&lt;td&gt;관찰로 SJF 근사&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들을 관통하는 한 가지는 trade-off다. 반환시간을 최적화하는 일과 응답시간을 최적화하는 일은 본질적으로 긴장 관계에 있다. SJF&amp;middot;STCF는 전자를, RR은 후자를 택했고, MLFQ는 작업 길이라는 알 수 없는 정보를 관찰로 메워 둘 사이에서 균형을 잡는다.&lt;/p&gt;</description>
      <category>OS</category>
      <category>FIFO</category>
      <category>MLFQ</category>
      <category>OSTEP</category>
      <category>Roundrobin</category>
      <category>Scheduling</category>
      <category>sjf</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/104</guid>
      <comments>https://onebrotravel.tistory.com/entry/CPU-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81#entry104comment</comments>
      <pubDate>Thu, 28 May 2026 23:35:23 +0900</pubDate>
    </item>
    <item>
      <title>프로세스 vs 스레드</title>
      <link>https://onebrotravel.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-vs-%EC%8A%A4%EB%A0%88%EB%93%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스 &amp;mdash; 실행 중인 프로그램이라는 추상화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스는 운영체제가 실행 중인 프로그램에 대해 제공하는 추상화다. 디스크에 저장된 실행 파일은 코드와 데이터의 정적인 모음일 뿐이고, 그것이 메모리에 올라가 CPU가 명령어를 실행하기 시작하면 비로소 프로세스가 된다. 같은 프로그램을 두 번 실행하면 두 개의 프로세스가 생긴다. 프로그램은 명사, 프로세스는 그 명사가 살아 움직이는 인스턴스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스를 구성하는 핵심은 &lt;b&gt;머신 상태(machine state)&lt;/b&gt; &amp;mdash; 프로그램이 실행되는 동안 읽거나 바꿀 수 있는 모든 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주소 공간(address space)&lt;/b&gt;: 프로세스가 접근할 수 있는 메모리 전체. 명령어와 데이터가 여기 들어간다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레지스터&lt;/b&gt;: 프로그램 카운터(PC, 다음에 실행할 명령어 주소), 스택 포인터, 프레임 포인터 등 실행에 필수적인 CPU 레지스터.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;I/O 정보&lt;/b&gt;: 현재 열어둔 파일 목록 등.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 레이아웃&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;process_memory_pcb.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/braLbt/dJMb99T4FQV/DVYYxDM39raX0pPp6mCOBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/braLbt/dJMb99T4FQV/DVYYxDM39raX0pPp6mCOBK/img.png&quot; data-alt=&quot;프로세스 메모리 레이아웃&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/braLbt/dJMb99T4FQV/DVYYxDM39raX0pPp6mCOBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbraLbt%2FdJMb99T4FQV%2FDVYYxDM39raX0pPp6mCOBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;780&quot; data-filename=&quot;process_memory_pcb.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로세스 메모리 레이아웃&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스의 주소 공간은 정해진 구조를 가진다. 낮은 주소부터 코드, 데이터, 힙이 자리하고, 힙은 위로 자라며, 스택은 주소 공간 최상단에서 아래로 자란다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;코드&lt;/b&gt;: 프로그램 명령어. 읽기 전용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터&lt;/b&gt;: 초기화된/초기화되지 않은 전역 변수.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;힙&lt;/b&gt;: &lt;code&gt;malloc&lt;/code&gt; 등으로 동적 할당되는 영역. 위로 성장.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스택&lt;/b&gt;: 함수 호출, 지역 변수, 매개변수, 반환 주소. 아래로 성장.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙과 스택이 서로 마주 보며 자라는 구조라는 점을 기억해 두면, 뒤에서 스레드를 다룰 때 &quot;스택만 따로 떼어낸다&quot;는 말이 자연스럽게 이해된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로세스 상태&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;process_states.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mH27x/dJMcabqQT8R/gCKxT6b9jrJbyxcZwRS5ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mH27x/dJMcabqQT8R/gCKxT6b9jrJbyxcZwRS5ck/img.png&quot; data-alt=&quot;프로세스 상태 전환 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mH27x/dJMcabqQT8R/gCKxT6b9jrJbyxcZwRS5ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmH27x%2FdJMcabqQT8R%2FgCKxT6b9jrJbyxcZwRS5ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;720&quot; data-filename=&quot;process_states.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로세스 상태 전환 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스가 생성되어 종료될 때까지 늘 CPU에서 돌고 있는 것은 아니다. 프로세스는 세 가지 기본 상태를 오간다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Running(실행)&lt;/b&gt;: 프로세서에서 실제로 명령어를 실행 중.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ready(준비)&lt;/b&gt;: 실행할 준비는 됐지만 OS가 아직 CPU를 주지 않은 상태. 스케줄링 대기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Blocked(차단)&lt;/b&gt;: 어떤 이벤트(주로 I/O 완료)가 일어나기 전까지는 실행할 수 없는 상태. 이때 CPU는 다른 프로세스가 쓴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 사이의 전환에는 이름이 붙어 있다. Ready에서 Running으로 가는 것은 &lt;b&gt;스케줄됨(scheduled)&lt;/b&gt;, 그 반대는 &lt;b&gt;디스케줄됨(descheduled)&lt;/b&gt;이다. Running 상태의 프로세스가 디스크 읽기 같은 I/O를 시작하면 Blocked로 내려가고, I/O가 끝나면 다시 Ready로 올라온다. 이 전환을 누가 언제 결정하느냐가 바로 스케줄러 일이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PCB &amp;mdash; OS가 프로세스를 기억하는 법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS는 시스템에서 돌아가는 모든 프로세스를 추적해야 한다. 이를 위해 프로세스마다 정보를 담은 자료구조를 두는데, 이것을 &lt;b&gt;PCB(Process Control Block)&lt;/b&gt; 또는 프로세스 기술자라 부른다. PCB에는 프로세스의 상태, PID, 부모 프로세스 포인터, 레지스터 컨텍스트(중단된 프로세스를 재개하기 위해 저장해 둔 레지스터 값들), 메모리 정보, 열린 파일 목록 등이 담긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스가 잠시 멈추고 다른 프로세스에 CPU를 넘길 때, OS는 현재 레지스터 값들을 PCB에 저장하고 다음 프로세스의 PCB에서 레지스터를 복원한다. 이 과정이 &lt;b&gt;컨텍스트 스위치(context switch)&lt;/b&gt;다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;PCB의 다른 이름들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PCB는 자료에 따라 프로세스 리스트(process list), 작업 리스트(task list), 프로세스 기술자(process descriptor)로도 불린다. 여러 프로그램을 동시에 실행하는 모든 OS는 형태는 달라도 이런 구조를 반드시 가진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스 생성 &amp;mdash; fork / exec / wait&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스는 어떻게 만들어질까? UNIX 계열은 새 프로세스를 만드는 일을 세 개의 시스템 콜로 쪼개 놓았다. 각각은 단순하지만, 조합하면 셸 같은 강력한 도구를 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fork &amp;mdash; 자기 자신을 복제&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fork_branch.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuF1JC/dJMcajoL1Ru/5gHwGuBfihVwNMGXa8iHbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuF1JC/dJMcajoL1Ru/5gHwGuBfihVwNMGXa8iHbK/img.png&quot; data-alt=&quot;fork 분기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuF1JC/dJMcajoL1Ru/5gHwGuBfihVwNMGXa8iHbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuF1JC%2FdJMcajoL1Ru%2F5gHwGuBfihVwNMGXa8iHbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;720&quot; data-filename=&quot;fork_branch.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fork 분기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;fork()&lt;/code&gt;는 호출한 프로세스의 (거의) 정확한 복사본을 만든다. 호출하는 쪽을 &lt;b&gt;부모(parent)&lt;/b&gt;, 새로 생긴 쪽을 &lt;b&gt;자식(child)&lt;/b&gt;이라 한다. 자식은 부모의 주소 공간, 레지스터, 열린 파일 디스크립터를 복사해서 받는다. 단, 둘은 별개의 주소 공간을 가지므로 복사 이후의 변경은 서로에게 영향을 주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;fork()&lt;/code&gt;의 가장 특이한 점은 &lt;b&gt;&quot;한 번 호출하면 두 번 반환한다&quot;&lt;/b&gt;는 것이다. 한 번의 호출이 부모와 자식 양쪽에서 각각 반환되는데, 반환값이 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모에게는 &lt;b&gt;자식의 PID&lt;/b&gt;(양수)가 반환된다.&lt;/li&gt;
&lt;li&gt;자식에게는 &lt;b&gt;0&lt;/b&gt;이 반환된다.&lt;/li&gt;
&lt;li&gt;실패하면 &lt;b&gt;-1&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 반환값 차이가 부모와 자식을 구분하는 유일한 수단이다. 같은 코드를 실행하지만 &lt;code&gt;if (pid == 0)&lt;/code&gt; 분기로 서로 다른 길을 가게 만든다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int main() {
    pid_t pid = fork();
    if (pid &amp;lt; 0) {
        // fork 실패
    } else if (pid == 0) {
        // 자식 프로세스
        printf(&quot;child\n&quot;);
    } else {
        // 부모 프로세스
        printf(&quot;parent of %d\n&quot;, pid);
    }
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fork 직후 부모와 자식은 둘 다 실행 가능 상태가 되고, 누가 먼저 실행될지는 스케줄러가 정한다. 즉 출력 순서는 &lt;b&gt;비결정적(nondeterministic)&lt;/b&gt;이다. 실행할 때마다 &quot;parent&quot;가 먼저 나올 수도, &quot;child&quot;가 먼저 나올 수도 있다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;실행 순서를 가정하지 말 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모가 항상 먼저 실행된다거나 자식이 먼저 끝난다는 가정은 위험하다. fork 이후의 순서는 스케줄러에 달려 있고 시스템&amp;middot;상황마다 다르다. 올바른 프로그램은 어떤 순서로 실행돼도 동작해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;wait &amp;mdash; 자식이 끝나기를 기다림&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모가 자식의 작업 완료를 기다려야 할 때가 있다. &lt;code&gt;wait()&lt;/code&gt;(또는 더 세밀한 &lt;code&gt;waitpid()&lt;/code&gt;)는 자식이 종료될 때까지 부모를 멈춰 세운다. 자식이 끝나면 부모에게 제어가 돌아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wait는 두 가지 일을 한다. 하나는 &lt;b&gt;동기화(synchronization)&lt;/b&gt; &amp;mdash; 자식이 끝날 때까지 부모를 블록해서 실행 순서를 결정적으로 만든다. fork만 썼을 때 비결정적이던 출력이, 부모가 wait를 호출하면 &quot;자식이 먼저 끝나고 부모가 이어받는&quot; 순서로 고정된다. 다른 하나는 &lt;b&gt;자식 수거(reaping)&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종료된 자식 프로세스는 즉시 사라지지 않는다. 부모가 종료 상태를 확인할 수 있도록, 커널은 자식을 &lt;b&gt;좀비(zombie)&lt;/b&gt; 상태로 남겨 둔다. 좀비는 실행되지는 않지만 프로세스 테이블 엔트리를 차지한다. 부모가 wait를 호출해 종료 상태를 거둬 가면 그제야 커널이 자식을 완전히 정리한다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;좀비를 방치하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸이나 서버처럼 오래 도는 프로그램이 자식을 수거하지 않으면 좀비가 쌓여 프로세스 테이블을 채운다. 장기 실행 프로그램은 반드시 자식을 명시적으로 수거해야 한다. 참고로 부모가 먼저 죽으면 고아가 된 자식은 PID 1번 init 프로세스가 입양해 대신 거둬 간다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;exec &amp;mdash; 다른 프로그램으로 변신&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fork만으로는 같은 프로그램의 복사본밖에 만들 수 없다. 자식이 부모와 &lt;b&gt;다른&lt;/b&gt; 프로그램을 실행하려면 &lt;code&gt;exec()&lt;/code&gt; 계열 시스템 콜이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;exec는 새 프로세스를 만들지 않는다. 현재 프로세스의 코드&amp;middot;데이터&amp;middot;힙&amp;middot;스택을 지정한 실행 파일의 것으로 통째로 덮어쓴다. PID는 그대로 유지되지만 내용물은 완전히 다른 프로그램이 된다. 그래서 exec는 &lt;b&gt;&quot;한 번 호출하면 절대 반환하지 않는다&quot;&lt;/b&gt; &amp;mdash; 성공하면 돌아올 원래 코드가 더 이상 메모리에 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fork가 &quot;한 번 호출, 두 번 반환&quot;이라면 exec는 &quot;한 번 호출, 반환 없음&quot;이라는 대비가 깔끔하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fork + exec + wait = 셸&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fork_exec_wait.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NX1Ll/dJMcaiQ0cb7/mAP24VXS5woF0awN16FIY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NX1Ll/dJMcaiQ0cb7/mAP24VXS5woF0awN16FIY0/img.png&quot; data-alt=&quot;fork+exec+wait 타임라인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NX1Ll/dJMcaiQ0cb7/mAP24VXS5woF0awN16FIY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNX1Ll%2FdJMcaiQ0cb7%2FmAP24VXS5woF0awN16FIY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;840&quot; data-filename=&quot;fork_exec_wait.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;fork+exec+wait 타임라인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 조각이 합쳐지면 셸의 동작이 된다. 셸이 명령어를 실행하는 과정은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;셸이 &lt;code&gt;fork()&lt;/code&gt;로 자식을 만든다.&lt;/li&gt;
&lt;li&gt;자식이 &lt;code&gt;exec()&lt;/code&gt;로 사용자가 입력한 프로그램으로 변신한다.&lt;/li&gt;
&lt;li&gt;부모(셸)는 &lt;code&gt;wait()&lt;/code&gt;로 자식이 끝나기를 기다린다(포어그라운드 실행). 끝나면 다음 프롬프트를 띄운다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fork와 exec를 분리한 덕분에, 그 사이(자식이 fork된 직후, exec하기 직전)에 환경을 조작할 수 있다. 출력 리다이렉션(&lt;code&gt;&amp;gt;&lt;/code&gt;)이나 파이프(&lt;code&gt;|&lt;/code&gt;)가 바로 이 틈에서 파일 디스크립터를 바꿔 끼워 구현된다. 단순한 두 시스템 콜의 조합이 셸의 고급 기능을 떠받치는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스레드 &amp;mdash; 같은 주소 공간 안의 여러 실행 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 프로세스는 실행 흐름이 하나였다. 즉 PC가 하나뿐이라 한 번에 한 군데의 명령어만 실행했다. &lt;b&gt;스레드&lt;/b&gt;는 이 가정을 깬다. 하나의 프로세스 안에 여러 개의 실행 흐름, 즉 여러 개의 PC를 두는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 스레드는 자신만의 PC, 레지스터 집합, 그리고 &lt;b&gt;스택&lt;/b&gt;을 가진다. 하지만 그 외의 것 &amp;mdash; 코드, 데이터, 힙, 열린 파일 &amp;mdash; 은 같은 프로세스 안의 모든 스레드가 &lt;b&gt;공유&lt;/b&gt;한다. 스레드는 프로세스와 거의 같지만, 주소 공간을 공유한다는 결정적 차이가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무엇을 공유하고 무엇을 나누는가&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;thread_address_space.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VbtIU/dJMcaayIvma/Sg9Y3tTZoJakibQ7KQrOI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VbtIU/dJMcaayIvma/Sg9Y3tTZoJakibQ7KQrOI0/img.png&quot; data-alt=&quot;싱글 vs 멀티스레드 주소 공간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VbtIU/dJMcaayIvma/Sg9Y3tTZoJakibQ7KQrOI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVbtIU%2FdJMcaayIvma%2FSg9Y3tTZoJakibQ7KQrOI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;840&quot; data-filename=&quot;thread_address_space.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;싱글 vs 멀티스레드 주소 공간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 스레드 프로세스의 주소 공간에는 스택이 하나뿐이다. 멀티 스레드 프로세스에서는 스레드마다 스택이 하나씩 생긴다. 코드&amp;middot;데이터&amp;middot;힙은 그대로 공유하면서, 스택만 스레드 수만큼 주소 공간 안에 흩어져 자리 잡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택이 스레드별로 나뉘는 이유는 분명하다. 각 스레드가 독립적으로 함수를 호출하고 지역 변수를 쌓아야 하므로, 호출 스택을 공유할 수는 없다. 반면 힙과 전역 데이터는 공유하기 때문에, 스레드끼리 데이터를 주고받기가 매우 쉽다 &amp;mdash; 그냥 같은 변수를 읽고 쓰면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨텍스트 스위치 관점에서도 차이가 있다. 프로세스 간 전환은 주소 공간(페이지 테이블)까지 바꿔야 하지만, 같은 프로세스의 스레드 간 전환은 주소 공간이 그대로라 레지스터만 갈아 끼우면 된다. 그만큼 가볍다. 프로세스의 상태를 PCB에 저장했듯, 스레드의 상태는 &lt;b&gt;TCB(Thread Control Block)&lt;/b&gt;에 저장된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 스레드가 필요한가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;병렬성(parallelism)&lt;/b&gt;: 멀티코어 시스템에서 작업을 여러 스레드로 쪼개면 여러 코어에서 동시에 실행돼 속도가 빨라진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답성(responsiveness)&lt;/b&gt;: 한 흐름이 느린 I/O를 기다리는 동안 다른 흐름이 계속 일할 수 있다. 디스크나 네트워크를 기다리며 프로그램 전체가 멈추는 것을 막는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;낮은 통신 비용&lt;/b&gt;: 주소 공간을 공유하므로, 프로세스처럼 IPC를 거치지 않고 메모리로 직접 데이터를 나눈다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드는 부모-자식 관계가 아니라 &lt;b&gt;peer(동등)&lt;/b&gt; 관계라는 점도 프로세스와 다르다. 모든 프로세스는 메인 스레드로 시작하지만, 메인 스레드는 그저 &quot;첫 번째&quot; 스레드일 뿐 특별한 권한이 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스 vs 스레드 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네 가지 축으로 정리하면 차이가 분명해진다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;프로세스&lt;/th&gt;
&lt;th&gt;스레드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;자원 공유&lt;/td&gt;
&lt;td&gt;주소 공간 완전 분리&lt;/td&gt;
&lt;td&gt;코드&amp;middot;데이터&amp;middot;힙 공유, 스택&amp;middot;레지스터&amp;middot;PC만 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성&amp;middot;전환 비용&lt;/td&gt;
&lt;td&gt;무겁다 (주소 공간 복사)&lt;/td&gt;
&lt;td&gt;가볍다 (스택&amp;middot;컨텍스트만)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;통신 방식&lt;/td&gt;
&lt;td&gt;IPC 필요 (파이프&amp;middot;소켓 등)&lt;/td&gt;
&lt;td&gt;공유 메모리 직접 접근&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;격리&amp;middot;안정성&lt;/td&gt;
&lt;td&gt;높다 (한쪽이 죽어도 무관)&lt;/td&gt;
&lt;td&gt;낮다 (한 스레드 오류가 전체에 영향)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 가지는 부연이 필요하다. 프로세스 생성이 무거운 이유는 주소 공간 전체를 복사하기 때문인데, 실제로는 Copy-on-Write로 최적화되지만 그래도 스레드보다는 무겁다. 전환 비용도 마찬가지다 &amp;mdash; 프로세스 간 전환은 주소 공간(페이지 테이블)까지 바꿔야 하지만, 같은 프로세스의 스레드 간 전환은 페이지 테이블을 그대로 두고 레지스터만 갈아 끼우면 되므로 더 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면, 스레드는 격리와 안정성을 일부 포기하는 대신 가벼움과 쉬운 공유를 얻는 절충이다. 어느 쪽이 맞는지는 만들려는 프로그램의 성격에 달려 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;pthread 기초&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POSIX 스레드(pthread)는 리눅스를 비롯한 모든 UNIX 계열에서 쓰는 표준 스레드 API다. 약 60개 함수가 있지만, 시작에 필요한 것은 생성&amp;middot;종료&amp;middot;동기화 세 가지다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성과 종료 &amp;mdash; create / join&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine)(void *),
                   void *arg);

int pthread_join(pthread_t thread, void **value_ptr);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pthread_create&lt;/code&gt;는 스레드를 만들어 &lt;code&gt;start_routine&lt;/code&gt; 함수부터 실행시킨다. 인자는 네 개다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;thread&lt;/code&gt;: 생성된 스레드를 가리킬 식별자가 여기 채워진다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attr&lt;/code&gt;: 스택 크기 같은 속성. 보통 기본값이면 되므로 &lt;code&gt;NULL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;start_routine&lt;/code&gt;: 스레드가 실행할 함수. &lt;code&gt;void *&lt;/code&gt;를 받아 &lt;code&gt;void *&lt;/code&gt;를 반환하는 형태로 고정돼 있다. void 포인터라 어떤 타입이든 넘기고 받을 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arg&lt;/code&gt;: 그 함수에 전달할 인자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pthread_join&lt;/code&gt;은 지정한 스레드가 끝나기를 기다리고, 그 반환값을 거둔다. 프로세스의 wait에 대응하는 역할이다. 단, wait가 &quot;아무 자식이나&quot;를 기다릴 수 있는 것과 달리 join은 &lt;b&gt;특정 스레드만&lt;/b&gt; 지정해 기다린다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void *mythread(void *arg) {
    printf(&quot;%s\n&quot;, (char *) arg);
    return NULL;
}

int main() {
    pthread_t p1, p2;
    pthread_create(&amp;amp;p1, NULL, mythread, &quot;A&quot;);
    pthread_create(&amp;amp;p2, NULL, mythread, &quot;B&quot;);
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로그램에서 &quot;A&quot;와 &quot;B&quot; 중 무엇이 먼저 출력될지는 정해져 있지 않다. fork와 마찬가지로 스레드의 실행 순서도 스케줄러에 달려 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공유 변수와 mutex&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드가 주소 공간을 공유한다는 것은 양날의 검이다. 전역 변수나 정적 변수, 힙의 데이터는 모든 스레드가 같은 인스턴스를 본다(지역 자동 변수는 각 스레드의 스택에 따로 존재하므로 보통 공유되지 않는다). 공유는 편하지만, 여러 스레드가 같은 변수를 동시에 건드리면 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 예가 공유 카운터다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;static int cnt = 0;

void *thread(void *arg) {
    for (int i = 0; i &amp;lt; 1000000; i++)
        cnt++;       // 위험
    return NULL;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cnt++&lt;/code&gt;는 한 줄처럼 보이지만 실제로는 세 단계로 컴파일된다. 컴파일러가 만들어내는 (어셈블리)코드는 대략 이렇다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;mov 0x8049a1c, %eax    # ① cnt 값을 레지스터로 읽어옴
add $0x1, %eax         # ② 레지스터 값을 1 증가
mov %eax, 0x8049a1c    # ③ 레지스터 값을 cnt에 다시 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 한 스레드가 이 세 명령어를 실행하는 도중에 다른 스레드로 전환될 수 있다는 점이다. 예를 들어 cnt가 50일 때, 스레드 1이 ①②까지 실행해 레지스터에 51을 들고 있는 상태에서 전환이 일어나고, 스레드 2가 ①부터 ③까지 다 실행해 cnt를 51로 만든 뒤, 다시 스레드 1이 돌아와 ③을 실행하면 자기가 들고 있던 51을 저장한다. 두 스레드가 각각 1씩 더했는데 cnt는 52가 아니라 51이다. 증가 한 번이 통째로 사라진 것이다. 두 스레드가 각각 백만 번씩 더했는데 결과가 이백만이 아닌, 매번 다른 엉뚱한 값이 나오는 이유가 이것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결책은 그 코드 조각을 한 번에 한 스레드만 실행하도록 묶는 것이다. 이를 위한 가장 기본적인 도구가 &lt;b&gt;mutex&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&amp;amp;lock);
cnt++;                        // 한 번에 한 스레드만 들어옴
pthread_mutex_unlock(&amp;amp;lock);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pthread_mutex_lock&lt;/code&gt;을 호출한 스레드가 잠금을 쥐면, 다른 스레드는 그 스레드가 &lt;code&gt;pthread_mutex_unlock&lt;/code&gt;으로 풀어 줄 때까지 잠금 획득 지점에서 기다린다. 이렇게 보호된 코드 영역을 임계 구역(critical section)이라 하고, 한 번에 하나의 스레드만 들어가도록 보장하는 성질을 상호 배제(mutual exclusion)라 한다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;여기서는 여기까지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 변수가 왜 깨지는지(race condition의 정확한 분석), 잠금만으로 풀리지 않는 순서 제어, 세마포어&amp;middot;조건 변수, 그리고 잘못 쓰면 빠지는 데드락 &amp;mdash; 이들은 동시성을 본격적으로 다루는 별도의 글에서 이어 간다. 이 글에서는 &quot;공유 변수는 깨질 수 있고, 임계 구역을 잠금으로 묶어 막는다&quot;까지만 짚는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컴파일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pthread를 쓰는 코드는 헤더 &lt;code&gt;pthread.h&lt;/code&gt;를 include하고, 링크할 때 &lt;code&gt;-pthread&lt;/code&gt; 플래그를 붙여야 한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;gcc -o main main.c -Wall -pthread&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스와 스레드는 결국 &quot;실행에 필요한 자원을 어떻게 묶을 것인가&quot;에 대한 두 가지 답이다. 프로세스는 주소 공간까지 통째로 분리해 안전을 얻고, 스레드는 주소 공간을 공유한 채 실행 흐름만 나눠 가벼움과 쉬운 공유를 얻는다. fork/exec/wait가 프로세스를 다루는 기본 도구이고, pthread의 create/join/mutex가 스레드를 다루는 기본 도구다.&lt;/p&gt;</description>
      <category>OS</category>
      <category>CSAPP</category>
      <category>OSTEP</category>
      <category>process</category>
      <category>thread</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/103</guid>
      <comments>https://onebrotravel.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-vs-%EC%8A%A4%EB%A0%88%EB%93%9C#entry103comment</comments>
      <pubDate>Thu, 28 May 2026 21:51:56 +0900</pubDate>
    </item>
    <item>
      <title>Git Merge vs Rebase</title>
      <link>https://onebrotravel.tistory.com/entry/Git-Merge-vs-Rebase</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 가지 통합 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 브랜치의 작업을 통합하는 방법은 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;git merge&lt;/code&gt;&lt;/b&gt; &amp;mdash; 양쪽 브랜치의 끝점을 묶어 새 머지 커밋을 만든다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;git rebase&lt;/code&gt;&lt;/b&gt; &amp;mdash; 한 브랜치의 커밋들을 다른 브랜치 위에 패치로 다시 적용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 최종 결과(스냅샷)를 만들지만, &lt;b&gt;이력의 모양&lt;/b&gt;이 달라진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fast-forward merge &amp;mdash; 분기가 없는 경우&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;merge_fast_forward.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buOIUe/dJMcabRQkvC/oNKDSDdRzqftdzWUKpiUaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buOIUe/dJMcabRQkvC/oNKDSDdRzqftdzWUKpiUaK/img.png&quot; data-alt=&quot;Merge - fast forward&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buOIUe/dJMcabRQkvC/oNKDSDdRzqftdzWUKpiUaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuOIUe%2FdJMcabRQkvC%2FoNKDSDdRzqftdzWUKpiUaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;840&quot; data-filename=&quot;merge_fast_forward.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Merge - fast forward&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;master&lt;/code&gt;에서 &lt;code&gt;hotfix&lt;/code&gt; 브랜치를 만들어 작업한 후 다시 &lt;code&gt;master&lt;/code&gt;로 돌아와 머지하는 시나리오. &lt;code&gt;master&lt;/code&gt; 포인터가 &lt;code&gt;hotfix&lt;/code&gt;의 직계 조상이므로, Git은 단순히 &lt;b&gt;master 포인터를 hotfix까지 전진&lt;/b&gt;시키는 것으로 머지를 완료한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ git switch master
$ git merge hotfix
Updating f30ab..87ab2
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력에 &lt;code&gt;Fast-forward&lt;/code&gt;가 보이면 새 머지 커밋이 만들어지지 않았다는 신호다. 두 브랜치 사이에 분기가 없었으므로 합칠 것이 없고, 포인터만 옮기면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-way merge &amp;mdash; 분기된 경우&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;merge_3way_mechanism.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ8bRW/dJMcaiXIMvO/rl8PAMd73AKhiXUItkwjoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ8bRW/dJMcaiXIMvO/rl8PAMd73AKhiXUItkwjoK/img.png&quot; data-alt=&quot;3-way merge mechanism&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ8bRW/dJMcaiXIMvO/rl8PAMd73AKhiXUItkwjoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ8bRW%2FdJMcaiXIMvO%2Frl8PAMd73AKhiXUItkwjoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;720&quot; data-filename=&quot;merge_3way_mechanism.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3-way merge mechanism&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 브랜치가 공통 조상에서 분기한 후 양쪽 모두 작업이 진행되어, &lt;b&gt;한쪽이 다른 쪽의 직계 조상이 아닌 경우&lt;/b&gt;다. 예를 들어 &lt;code&gt;master&lt;/code&gt;에서 &lt;code&gt;hotfix&lt;/code&gt;를 머지한 후 &lt;code&gt;iss53&lt;/code&gt; 브랜치를 다시 머지할 때.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;$ git switch master
$ git merge iss53
Merge made by the 'recursive' strategy.
 index.html |    1 +
 1 file changed, 1 insertion(+)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Git은 &lt;b&gt;3-way merge&lt;/b&gt;를 수행한다. 두 브랜치의 끝점(&lt;code&gt;C4&lt;/code&gt;, &lt;code&gt;C5&lt;/code&gt;)과 두 브랜치의 공통 조상(&lt;code&gt;C2&lt;/code&gt;)을 비교해 변경 사항을 합치고, &lt;b&gt;두 개의 부모를 가진 새 머지 커밋&lt;/b&gt;(&lt;code&gt;C6&lt;/code&gt;)을 만든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;merge_3way_result.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZjiFv/dJMcafGMxUj/FepakoocyVfPP8Koa7IJg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZjiFv/dJMcafGMxUj/FepakoocyVfPP8Koa7IJg1/img.png&quot; data-alt=&quot;3-way merge&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZjiFv/dJMcafGMxUj/FepakoocyVfPP8Koa7IJg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZjiFv%2FdJMcafGMxUj%2FFepakoocyVfPP8Koa7IJg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;720&quot; data-filename=&quot;merge_3way_result.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3-way merge&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머지 커밋은 일반 커밋과 달리 부모가 둘이다. 이것이 이력에서 &lt;b&gt;분기가 일어났음을 영구히 기록&lt;/b&gt;한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;머지 충돌 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽 브랜치가 &lt;b&gt;같은 파일의 같은 부분&lt;/b&gt;을 서로 다르게 수정했다면 Git은 자동으로 합칠 수 없다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머지 커밋이 만들어지지 않고, 충돌이 해결될 때까지 머지가 일시 중단된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;충돌 확인 &amp;mdash; &lt;code&gt;git status&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run &quot;git commit&quot;)

Unmerged paths:
  (use &quot;git add &amp;lt;file&amp;gt;...&quot; to mark resolution)
    both modified:   index.html&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;both modified&lt;/code&gt; 표시가 있는 파일이 충돌 파일이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;충돌 마커&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 파일을 열면 Git이 충돌 영역에 마커를 추가해두었다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD:index.html
&amp;lt;div id=&quot;footer&quot;&amp;gt;contact : email.support@github.com&amp;lt;/div&amp;gt;
=======
&amp;lt;div id=&quot;footer&quot;&amp;gt;
  please contact us at support@github.com
&amp;lt;/div&amp;gt;
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; iss53:index.html&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD&lt;/code&gt; ~ &lt;code&gt;=======&lt;/code&gt; &amp;mdash; 현재 브랜치(&lt;code&gt;master&lt;/code&gt;)의 내용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=======&lt;/code&gt; ~ &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; iss53&lt;/code&gt; &amp;mdash; 머지할 브랜치(&lt;code&gt;iss53&lt;/code&gt;)의 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 단계&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;파일을 열어 한쪽을 선택하거나 두 내용을 적절히 결합한 형태로 작성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt;, &lt;code&gt;=======&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; 마커를 모두 제거&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt;로 해결됨을 표시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git commit&lt;/code&gt;으로 머지 커밋 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;GUI 도구 &amp;mdash; &lt;code&gt;git mergetool&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌을 시각적으로 해결하고 싶다면 &lt;code&gt;git mergetool&lt;/code&gt;이 시스템에 설치된 머지 도구(vimdiff, meld, opendiff 등)를 실행해준다. 도구 종료 후 Git이 자동으로 해결된 파일을 staging해주므로 &lt;code&gt;git commit&lt;/code&gt;만 실행하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rebase &amp;mdash; 다른 통합 방식&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;rebase_mechanism.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D8hz0/dJMb99T3UwA/sShvwpNzAwiApJ3s5JqG61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D8hz0/dJMb99T3UwA/sShvwpNzAwiApJ3s5JqG61/img.png&quot; data-alt=&quot;rebase mechanism&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D8hz0/dJMb99T3UwA/sShvwpNzAwiApJ3s5JqG61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD8hz0%2FdJMb99T3UwA%2FsShvwpNzAwiApJ3s5JqG61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;960&quot; data-filename=&quot;rebase_mechanism.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;rebase mechanism&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 통합 결과를 만드는 &lt;b&gt;다른 방법&lt;/b&gt;이 있다. 한 브랜치의 커밋들을 가져와 다른 브랜치 위에 &lt;b&gt;패치로 다시 적용&lt;/b&gt;하는 방식 &amp;mdash; &lt;code&gt;git rebase&lt;/code&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분기된 두 브랜치 &lt;code&gt;master&lt;/code&gt;(끝 &lt;code&gt;C3&lt;/code&gt;)와 &lt;code&gt;experiment&lt;/code&gt;(끝 &lt;code&gt;C4&lt;/code&gt;)가 있다고 하자.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;$ git switch experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령의 동작:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;두 브랜치의 공통 조상을 찾는다&lt;/li&gt;
&lt;li&gt;현재 브랜치(&lt;code&gt;experiment&lt;/code&gt;)의 각 커밋이 도입한 변경(diff)을 추출해 임시 파일에 저장&lt;/li&gt;
&lt;li&gt;현재 브랜치를 &lt;code&gt;master&lt;/code&gt;의 끝(&lt;code&gt;C3&lt;/code&gt;)으로 리셋&lt;/li&gt;
&lt;li&gt;저장해둔 변경들을 순차적으로 다시 적용 &amp;mdash; 새 커밋 &lt;code&gt;C4'&lt;/code&gt;이 만들어짐&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;code&gt;master&lt;/code&gt;에서 fast-forward 머지로 마무리.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;$ git switch master
$ git merge experiment&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에 &lt;code&gt;C4'&lt;/code&gt;이 가리키는 스냅샷은 머지 방식으로 만들었을 때의 &lt;code&gt;C5&lt;/code&gt; 스냅샷과 &lt;b&gt;정확히 동일&lt;/b&gt;하다. 차이는 이력의 모양뿐이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Merge vs Rebase &amp;mdash; 결과 비교&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;merge_vs_rebase.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crO6uF/dJMcaiQZm6Z/Djib2BRjbjAalirohM3OV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crO6uF/dJMcaiQZm6Z/Djib2BRjbjAalirohM3OV1/img.png&quot; data-alt=&quot;Merge vs Rebase 결과 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crO6uF/dJMcaiQZm6Z/Djib2BRjbjAalirohM3OV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrO6uF%2FdJMcaiQZm6Z%2FDjib2BRjbjAalirohM3OV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;960&quot; data-filename=&quot;merge_vs_rebase.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Merge vs Rebase 결과 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Merge&lt;/b&gt; &amp;mdash; 분기가 보존되고 두 부모를 가진 머지 커밋이 남는다. 이력이 &quot;실제로 일어난 일&quot;을 그대로 보여준다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rebase&lt;/b&gt; &amp;mdash; 분기 흔적이 사라지고 직선형 이력이 된다. &quot;처음부터 차례로 작업된 것처럼&quot; 보인다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅샷의 최종 상태는 같지만, 이력을 어떻게 표현하느냐가 다르다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rebase의 위험성&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;rebase_danger.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d9YERY/dJMcacQL3Bg/K8bzhxWKLMk6KFx9KLfgK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d9YERY/dJMcacQL3Bg/K8bzhxWKLMk6KFx9KLfgK1/img.png&quot; data-alt=&quot;rebase danger&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d9YERY/dJMcacQL3Bg/K8bzhxWKLMk6KFx9KLfgK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9YERY%2FdJMcacQL3Bg%2FK8bzhxWKLMk6KFx9KLfgK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;860&quot; data-filename=&quot;rebase_danger.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;rebase danger&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rebase는 &lt;b&gt;기존 커밋을 버리고 새 커밋을 만드는&lt;/b&gt; 작업이다. SHA-1 해시가 바뀐다. 이 사실이 협업 상황에서 큰 문제를 일으킬 수 있다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;Rebase 황금률&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;외부에 push된, 다른 사람이 기반으로 작업할 수 있는 커밋은 절대 rebase하지 말 것.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 원칙만 지키면 문제없다. 어기면 협업자 전체가 혼란에 빠진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 시나리오: 누군가 push된 커밋을 rebase로 정리한 후 &lt;code&gt;git push --force&lt;/code&gt;로 덮어쓰면, 그 커밋을 기반으로 작업하던 협업자의 로컬 저장소에 &lt;b&gt;이름은 다르지만 내용은 거의 같은 커밋들&lt;/b&gt;이 남는다. 이후 협업자가 &lt;code&gt;git pull&lt;/code&gt;을 하면 같은 작업을 두 번 머지하는 꼴이 되고, 이력이 엉킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협업자가 이 상황을 만났을 때 &lt;code&gt;git pull --rebase&lt;/code&gt;(또는 &lt;code&gt;git fetch&lt;/code&gt; 후 &lt;code&gt;git rebase&lt;/code&gt;)를 사용하면 Git이 patch-id로 중복을 인식해 자동 정리해주는 경우가 있다. 다만 완벽하지 않으므로 &lt;b&gt;애초에 push된 커밋을 rebase하지 않는 것&lt;/b&gt;이 최선이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# pull이 항상 rebase 방식으로 동작하게 설정
$ git config --global pull.rebase true&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어느 쪽을 선택할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이력을 보는 두 관점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;이력 = 실제 일어난 일의 기록&lt;/b&gt; &amp;mdash; 분기&amp;middot;머지의 흐름을 그대로 보존해야 한다. 이 관점에서는 &lt;b&gt;Merge&lt;/b&gt;가 정답&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이력 = 프로젝트가 만들어진 이야기&lt;/b&gt; &amp;mdash; 후속 독자가 이해하기 좋은 흐름으로 재구성한다. 이 관점에서는 &lt;b&gt;Rebase&lt;/b&gt;가 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 가장 흔히 권장되는 패턴은 둘의 조합이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로컬 작업 중&lt;/b&gt;(아직 push 전): 필요하면 rebase로 이력 정리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미 push한 커밋&lt;/b&gt;: 절대 rebase하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 깔끔한 이력의 이점을 누리면서 협업 안정성도 지킬 수 있다.&lt;i&gt;&lt;/i&gt;&lt;/p&gt;</description>
      <category>DevTools</category>
      <category>Conflict</category>
      <category>GIT</category>
      <category>merge</category>
      <category>progit</category>
      <category>rebase</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/102</guid>
      <comments>https://onebrotravel.tistory.com/entry/Git-Merge-vs-Rebase#entry102comment</comments>
      <pubDate>Wed, 27 May 2026 22:56:05 +0900</pubDate>
    </item>
    <item>
      <title>Git 브랜치 &amp;mdash; 포인터로 이해하기</title>
      <link>https://onebrotravel.tistory.com/entry/Git-%EB%B8%8C%EB%9E%9C%EC%B9%98-%E2%80%94-%ED%8F%AC%EC%9D%B8%ED%84%B0%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 모델 &amp;mdash; 커밋과 부모 포인터&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;commit_object_structure.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beY2pX/dJMcaayHt1k/hMVNMz7B6TIWLOV2takYnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beY2pX/dJMcaayHt1k/hMVNMz7B6TIWLOV2takYnk/img.png&quot; data-alt=&quot;커밋 객체 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beY2pX/dJMcaayHt1k/hMVNMz7B6TIWLOV2takYnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeY2pX%2FdJMcaayHt1k%2FhMVNMz7B6TIWLOV2takYnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;800&quot; data-filename=&quot;commit_object_structure.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;커밋 객체 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git의 브랜치 동작을 이해하려면 먼저 &lt;b&gt;커밋이 어떻게 저장되는지&lt;/b&gt;를 정확히 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/Git-Basics-%E2%80%94-%EB%B3%80%EA%B2%BD-%EC%B7%A8%EC%86%8C%C2%B7%EC%9B%90%EA%B2%A9-%EC%A0%80%EC%9E%A5%EC%86%8C%C2%B7%ED%83%9C%EA%B7%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Git Basics &amp;mdash; 변경 취소&amp;middot;원격 저장소&amp;middot;태그&lt;/a&gt;에서 다룬 대로, Git은 데이터를 변경 차이가 아니라 &lt;b&gt;스냅샷&lt;/b&gt;으로 저장한다. 커밋을 만들면 Git은 다음 세 종류의 객체를 만든다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;객체&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;blob&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파일 하나의 내용을 담는 객체 (이름&amp;middot;경로 정보 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;tree&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;디렉터리 구조 &amp;mdash; 어떤 파일 이름이 어떤 blob을 가리키는지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;commit&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;루트 tree를 가리키는 포인터 + 메타데이터(작성자, 메시지, 부모 커밋 포인터)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 파일 3개를 처음 커밋하면 저장소에 객체 5개가 생성된다 &amp;mdash; blob 3개 + 루트 tree 1개 + commit 1개.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 커밋을 만들면 또 다른 commit 객체가 생기는데, 이 객체는 &lt;b&gt;이전 커밋을 가리키는 부모 포인터&lt;/b&gt;를 가진다. 일반적인 커밋은 부모가 1개, 첫 커밋은 부모가 0개, 머지 커밋은 부모가 2개 이상이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;commit_parent_chain.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/espBmX/dJMcahR4iue/nUKulGYHbqn8qx88R3RykK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/espBmX/dJMcahR4iue/nUKulGYHbqn8qx88R3RykK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/espBmX/dJMcahR4iue/nUKulGYHbqn8qx88R3RykK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FespBmX%2FdJMcahR4iue%2FnUKulGYHbqn8qx88R3RykK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;760&quot; data-filename=&quot;commit_parent_chain.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 커밋 이력은 부모 포인터를 따라 거슬러 올라가는 &lt;b&gt;단방향 체인&lt;/b&gt;이 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브랜치 = 포인터&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;branch_as_pointer.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qOAJI/dJMcadvjZIm/11VoBPnp9QJBLx3iwLlOE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qOAJI/dJMcadvjZIm/11VoBPnp9QJBLx3iwLlOE0/img.png&quot; data-alt=&quot;브랜치 = 포인터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qOAJI/dJMcadvjZIm/11VoBPnp9QJBLx3iwLlOE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqOAJI%2FdJMcadvjZIm%2F11VoBPnp9QJBLx3iwLlOE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;760&quot; data-filename=&quot;branch_as_pointer.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;브랜치 = 포인터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git에서 브랜치는 &lt;b&gt;커밋 하나를 가리키는 가볍고 이동 가능한 포인터&lt;/b&gt;일 뿐이다. 별다른 자료구조가 아니라 단순히 커밋 SHA-1을 담은 41바이트 파일(40자 해시 + 줄바꿈).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git init&lt;/code&gt; 시 자동으로 만들어지는 &lt;code&gt;master&lt;/code&gt;(또는 &lt;code&gt;main&lt;/code&gt;)도 특별한 브랜치가 아니다. 다른 브랜치와 완전히 동일한 포인터이며, 단지 기본값으로 생성될 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 커밋을 만들면 현재 브랜치 포인터가 &lt;b&gt;자동으로&lt;/b&gt; 새 커밋을 가리키도록 이동한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HEAD &amp;mdash; 현재 위치 포인터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git이 &quot;지금 어느 브랜치에서 작업 중인지&quot; 알아내는 방법은 &lt;b&gt;HEAD&lt;/b&gt;라는 특별한 포인터다. HEAD는 현재 체크아웃된 브랜치를 가리킨다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ git log --oneline --decorate
f30ab (HEAD -&amp;gt; master, testing) Add feature #32
34ac2 Fix bug #1328
98ca9 Initial commit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 출력에서 &lt;code&gt;master&lt;/code&gt;와 &lt;code&gt;testing&lt;/code&gt; 두 브랜치는 같은 커밋(&lt;code&gt;f30ab&lt;/code&gt;)을 가리키고 있고, HEAD는 &lt;code&gt;master&lt;/code&gt;에 있다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;SVN/CVS의 HEAD와 다르다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Subversion이나 CVS의 HEAD는 &quot;원격의 최신 리비전&quot;을 의미했지만, Git에서 HEAD는 &lt;b&gt;로컬에서 현재 작업 중인 브랜치를 가리키는 포인터&lt;/b&gt;다. 개념이 다르므로 혼동하지 말 것.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브랜치 만들기와 전환&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;branch_switch_sequence.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FGFZa/dJMcageAUY6/BY3fCUBukp5sY17zxdU6B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FGFZa/dJMcageAUY6/BY3fCUBukp5sY17zxdU6B1/img.png&quot; data-alt=&quot;브랜치 생성&amp;amp;middot;전환 전후 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FGFZa/dJMcageAUY6/BY3fCUBukp5sY17zxdU6B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFGFZa%2FdJMcageAUY6%2FBY3fCUBukp5sY17zxdU6B1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1520&quot; data-filename=&quot;branch_switch_sequence.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1520&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;브랜치 생성&amp;middot;전환 전후 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새 브랜치 만들기 &amp;mdash; &lt;code&gt;git branch&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;$ git branch testing&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 현재 위치한 커밋을 가리키는 새 포인터를 만든다. &lt;b&gt;전환은 하지 않는다.&lt;/b&gt; HEAD는 여전히 이전 브랜치(예: &lt;code&gt;master&lt;/code&gt;)에 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 브랜치로 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 2.23 이전에는 &lt;code&gt;git checkout&lt;/code&gt;이 표준이었고, 2.23부터는 &lt;code&gt;git switch&lt;/code&gt;가 권장된다.&lt;/p&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 전통 방식
$ git checkout testing

# Git 2.23+ 권장
$ git switch testing&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 명령 모두 HEAD를 해당 브랜치로 옮긴다. 동시에 &lt;b&gt;작업 디렉터리의 파일 상태도 그 브랜치의 마지막 커밋 시점으로 되돌아간다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성 + 전환 한 번에&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# 전통 방식
$ git checkout -b iss53

# Git 2.23+ 권장
$ git switch -c iss53&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-c&lt;/code&gt;는 &lt;code&gt;--create&lt;/code&gt;의 약자다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;브랜치 전환은 작업 디렉터리를 바꾼다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치를 전환하면 작업 디렉터리의 파일들이 새 브랜치의 마지막 커밋 시점 상태로 되돌아간다. 만약 작업 디렉터리에 커밋되지 않은 변경이 있고 그게 새 브랜치와 충돌한다면, Git은 전환 자체를 거부한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전환 전에 변경을 커밋하거나, 임시로 치워두려면 &lt;code&gt;git stash&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분기와 합류 &amp;mdash; 기본 워크플로우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치를 만들어 작업하다가 다른 브랜치로 잠시 전환했다가 다시 합치는 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# 1. master에서 새 기능 작업 시작
$ git switch -c iss53
$ vim index.html
$ git commit -a -m 'Create new footer [issue 53]'

# 2. 긴급 hotfix 요청 &amp;mdash; master로 돌아가서 hotfix 브랜치 만들기
$ git switch master
$ git switch -c hotfix
$ vim index.html
$ git commit -a -m 'Fix broken email address'

# 3. hotfix를 master에 머지
$ git switch master
$ git merge hotfix

# 4. iss53 브랜치로 돌아가 작업 계속
$ git switch iss53&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름의 핵심은 &lt;b&gt;각 브랜치의 작업이 서로 독립적&lt;/b&gt;이라는 점이다. &lt;code&gt;iss53&lt;/code&gt;에서 작업한 내용은 &lt;code&gt;master&lt;/code&gt;에 영향을 주지 않고, 그 반대도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 머지 메커니즘(fast-forward vs 3-way merge, rebase, 충돌 해결)은 다음 글에서 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이력을 그래프로 확인하려면:&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;$ git log --oneline --decorate --graph --all
* c2b9e (HEAD -&amp;gt; master) Make other changes
| * 87ab2 (iss53) Make a change
|/
* f30ab Add feature #32
* 34ac2 Fix bug #1328
* 98ca9 Initial commit&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;브랜치 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목록 보기&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;$ git branch
  iss53
* master
  testing&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;*&lt;/code&gt; 표시가 현재 체크아웃된 브랜치(HEAD가 가리키는 브랜치)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 브랜치의 마지막 커밋 정보까지 보려면 &lt;code&gt;-v&lt;/code&gt; 옵션:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;$ git branch -v
  iss53    93b412c Fix javascript issue
* master   7a98805 Merge branch 'iss53'
  testing  782fd34 Add scott to the author list&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;삭제&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;$ git branch -d iss53&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-d&lt;/code&gt;는 &lt;b&gt;안전 삭제&lt;/b&gt;. 해당 브랜치가 현재 브랜치에 머지되지 않았다면 Git이 삭제를 거부한다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머지하지 않은 작업을 정말 버리고 싶다면 &lt;code&gt;-D&lt;/code&gt;로 강제 삭제.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;$ git branch -D testing&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이름 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서:&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;$ git branch --move bad-name corrected-name
# 또는 짧게
$ git branch -m bad-name corrected-name&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격까지 반영하려면 새 이름으로 push하고 옛 이름을 원격에서 삭제한다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;$ git push --set-upstream origin corrected-name
$ git push origin --delete bad-name&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;공유 브랜치 이름은 함부로 바꾸지 말 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 협업자들이 사용 중인 브랜치 &amp;mdash; 특히 &lt;code&gt;master&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;mainline&lt;/code&gt; 같은 메인 브랜치 &amp;mdash; 의 이름을 바꾸면 모든 협업자의 로컬 추적 브랜치가 망가진다. 이런 경우 사전에 팀과 합의하고, CI/CD&amp;middot;이슈 트래커&amp;middot;다른 통합 도구에서 참조하는 브랜치명도 모두 함께 업데이트해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevTools</category>
      <category>Branch</category>
      <category>checkout</category>
      <category>GIT</category>
      <category>Head</category>
      <category>progit</category>
      <category>Switch</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/101</guid>
      <comments>https://onebrotravel.tistory.com/entry/Git-%EB%B8%8C%EB%9E%9C%EC%B9%98-%E2%80%94-%ED%8F%AC%EC%9D%B8%ED%84%B0%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#entry101comment</comments>
      <pubDate>Wed, 27 May 2026 17:24:12 +0900</pubDate>
    </item>
    <item>
      <title>Git Basics &amp;mdash; 변경 취소&amp;middot;원격 저장소&amp;middot;태그</title>
      <link>https://onebrotravel.tistory.com/entry/Git-Basics-%E2%80%94-%EB%B3%80%EA%B2%BD-%EC%B7%A8%EC%86%8C%C2%B7%EC%9B%90%EA%B2%A9-%EC%A0%80%EC%9E%A5%EC%86%8C%C2%B7%ED%83%9C%EA%B7%B8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;변경 취소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업하다 보면 어느 단계에서든 변경을 되돌리고 싶을 수 있다. Git이 제공하는 되돌리기 도구는 영역(Working/Staging/Repository)에 따라 다른 명령을 쓴다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;되돌릴 수 없는 작업도 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git의 거의 모든 작업은 데이터를 추가만 하므로 복구 가능하지만, 일부 되돌리기 명령은 데이터를 실제로 잃게 만든다. 특히 커밋되지 않은 변경(Modified/Staged 상태)을 폐기하면 거의 복구되지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;git commit --amend&lt;/code&gt; &amp;mdash; 마지막 커밋 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 일찍 커밋했을 때 &amp;mdash; 파일을 빠뜨렸거나 메시지를 잘못 입력한 경우 &amp;mdash; &lt;code&gt;--amend&lt;/code&gt;로 마지막 커밋을 수정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ git commit -m &quot;Initial commit&quot;
$ git add forgotten_file
$ git commit --amend&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 현재 staged 상태인 내용을 마지막 커밋으로 만든다. 변경 사항이 없으면 메시지만 수정된다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--amend&lt;/code&gt;는 &quot;수정&quot;이 아니라 &quot;대체&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--amend&lt;/code&gt;는 기존 커밋을 새 커밋으로 완전히 대체한다. 원래 커밋은 이력에서 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;아직 push되지 않은 커밋에만&lt;/b&gt; 사용해야 한다. 이미 push된 커밋을 amend하고 다시 push하면 협업자에게 문제가 생긴다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스테이지 취소 &amp;mdash; Unstaging&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git add&lt;/code&gt;로 스테이지한 파일을 다시 unstaging area에서 빼는 작업.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Git 2.23 이전&lt;/b&gt;: &lt;code&gt;git reset HEAD &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;$ git reset HEAD CONTRIBUTING.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Git 2.23 이후 권장&lt;/b&gt;: &lt;code&gt;git restore --staged &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;$ git restore --staged CONTRIBUTING.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 명령 모두 같은 효과 &amp;mdash; 파일을 Staged에서 Modified로 되돌린다. 작업 디렉터리의 실제 파일 내용은 건드리지 않으므로 비교적 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git status&lt;/code&gt;가 직접 어느 명령을 쓰면 되는지 알려주기 때문에 외울 필요는 없다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;Changes to be committed:
  (use &quot;git restore --staged &amp;lt;file&amp;gt;...&quot; to unstage)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수정 내용 폐기 &amp;mdash; Unmodifying&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 디렉터리에서 수정한 내용을 마지막 커밋(또는 마지막 staged) 상태로 되돌리는 작업.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Git 2.23 이전&lt;/b&gt;: &lt;code&gt;git checkout -- &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;$ git checkout -- CONTRIBUTING.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Git 2.23 이후 권장&lt;/b&gt;: &lt;code&gt;git restore &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;dos&quot;&gt;&lt;code&gt;$ git restore CONTRIBUTING.md&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;이 명령들은 실제로 데이터를 잃게 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git restore &amp;lt;file&amp;gt;&lt;/code&gt;과 &lt;code&gt;git checkout -- &amp;lt;file&amp;gt;&lt;/code&gt;은 작업 디렉터리의 변경을 강제로 폐기한다. Git이 추적하지 않은 변경이므로 복구되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠시 치워두고 싶을 뿐이라면 폐기하지 말고 &lt;code&gt;git stash&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;git_restore_modes.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DxPRX/dJMcabRPUDP/DNIg24hEJLTpEY02srWndk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DxPRX/dJMcabRPUDP/DNIg24hEJLTpEY02srWndk/img.png&quot; data-alt=&quot;영역별 변경 취소 흐름 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DxPRX/dJMcabRPUDP/DNIg24hEJLTpEY02srWndk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDxPRX%2FdJMcabRPUDP%2FDNIg24hEJLTpEY02srWndk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;720&quot; data-filename=&quot;git_restore_modes.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;영역별 변경 취소 흐름 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원격 저장소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;협업하려면 원격 저장소(remote repository)를 관리해야 한다. 원격은 인터넷이나 네트워크에 호스팅된 프로젝트의 복사본이다. Git은 여러 원격을 동시에 다룰 수 있고, 각각 읽기 전용 또는 읽기/쓰기 권한을 가진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원격 목록 보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정된 원격 목록을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ git remote
origin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;origin&lt;/code&gt;은 clone 시 Git이 자동으로 부여하는 기본 이름이다. &lt;code&gt;-v&lt;/code&gt; 옵션으로 fetch/push에 사용되는 URL까지 본다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;$ git remote -v
origin  https://github.com/schacon/ticgit (fetch)
origin  https://github.com/schacon/ticgit (push)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원격 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 원격을 short name(별칭)으로 등록한다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;$ git remote add pb https://github.com/paulboone/ticgit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 URL 전체 대신 &lt;code&gt;pb&lt;/code&gt;로 참조할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ git fetch pb&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fetch vs Pull&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;remote_sync_flow.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CJtZS/dJMcaaSZkuV/jOi4p2Zc7azk7eB2Ykk9Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CJtZS/dJMcaaSZkuV/jOi4p2Zc7azk7eB2Ykk9Tk/img.png&quot; data-alt=&quot;로컬&amp;amp;middot;원격 동기화 흐름 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CJtZS/dJMcaaSZkuV/jOi4p2Zc7azk7eB2Ykk9Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCJtZS%2FdJMcaaSZkuV%2FjOi4p2Zc7azk7eB2Ykk9Tk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;800&quot; data-filename=&quot;remote_sync_flow.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로컬&amp;middot;원격 동기화 흐름 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 명령 모두 원격에서 데이터를 가져오지만 동작이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;git fetch &amp;lt;remote&amp;gt;&lt;/code&gt;&lt;/b&gt; &amp;mdash; 원격의 변경을 로컬 저장소에 &lt;b&gt;다운로드만 한다&lt;/b&gt;. 현재 작업 중인 브랜치에는 영향 없음. 가져온 후 수동으로 병합해야 한다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ git fetch origin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;git pull&lt;/code&gt;&lt;/b&gt; &amp;mdash; fetch + merge를 한 번에 수행한다. 현재 브랜치가 원격 브랜치를 추적 중이라면, 가져온 변경을 현재 브랜치에 자동 병합한다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ git pull&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;Git 2.27 이후 &lt;code&gt;pull.rebase&lt;/code&gt; 경고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 2.27부터 &lt;code&gt;git pull&lt;/code&gt; 실행 시 &lt;code&gt;pull.rebase&lt;/code&gt; 설정이 없으면 경고가 나온다. 다음 중 한쪽으로 설정해두는 게 좋다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 병합 동작 (fast-forward 우선, 안 되면 merge commit): &lt;code&gt;git config --global pull.rebase &quot;false&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;pull 시 rebase: &lt;code&gt;git config --global pull.rebase &quot;true&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Push&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 변경을 원격으로 올린다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;$ git push &amp;lt;remote&amp;gt; &amp;lt;branch&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;$ git push origin master&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 두 조건이 모두 충족돼야 작동한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기 권한이 있는 원격에서 clone한 경우&lt;/li&gt;
&lt;li&gt;그 사이 다른 사람이 먼저 push하지 않은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람이 먼저 push했다면 push가 거부된다. 그 경우 먼저 fetch 후 병합한 뒤 다시 push해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원격 상세 확인&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;$ git remote show origin
* remote origin
  Fetch URL: https://github.com/schacon/ticgit
  Push  URL: https://github.com/schacon/ticgit
  HEAD branch: master
  Remote branches:
    master      tracked
    dev-branch  tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL, 추적 중인 브랜치, pull/push 시 어느 브랜치가 어디로 매핑되는지를 한눈에 보여준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원격 이름 변경&amp;middot;삭제&lt;/h3&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;# 이름 변경
$ git remote rename pb paul

# 삭제
$ git remote remove paul&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격을 삭제하면 연결된 원격 추적 브랜치와 설정 정보도 함께 사라진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;태그&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소 이력에서 특정 시점을 표시하는 기능. 일반적으로 릴리스 시점(&lt;code&gt;v1.0&lt;/code&gt;, &lt;code&gt;v2.0&lt;/code&gt;)에 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;태그 종류&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Lightweight&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;특정 커밋을 가리키는 포인터. 다른 정보 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Annotated&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Git 데이터베이스에 객체로 저장. 태그 작성자&amp;middot;날짜&amp;middot;메시지 포함. GPG 서명 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴리스 등 공식 시점 표시는 &lt;b&gt;annotated&lt;/b&gt;를 권장한다. 단순 임시 태그라면 lightweight도 무방.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;태그 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Annotated&lt;/b&gt; &amp;mdash; &lt;code&gt;-a&lt;/code&gt; 옵션 + &lt;code&gt;-m&lt;/code&gt;으로 메시지&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;$ git tag -a v1.4 -m &quot;my version 1.4&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git show&lt;/code&gt;로 태그 정보를 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ git show v1.4
tag v1.4
Tagger: Ben Straub &amp;lt;ben@straub.cc&amp;gt;
Date:   Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lightweight&lt;/b&gt; &amp;mdash; 옵션 없이 이름만&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;$ git tag v1.4-lw&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git show v1.4-lw&lt;/code&gt;는 태그 자체 정보 없이 가리키는 커밋만 보여준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;태그 목록 보기&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;$ git tag
v1.0
v2.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴으로 필터링하려면 &lt;code&gt;-l&lt;/code&gt; 또는 &lt;code&gt;--list&lt;/code&gt; 옵션이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;$ git tag -l &quot;v1.8.5*&quot;
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
...&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;과거 커밋에 태그 달기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그를 깜빡 잊었을 때, 커밋 해시를 지정해서 사후에 달 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;$ git tag -a v1.2 9fceb02&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;태그 공유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;code&gt;git push&lt;/code&gt;는 &lt;b&gt;태그를 전송하지 않는다&lt;/b&gt;. 명시적으로 push해야 한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# 특정 태그
$ git push origin v1.5

# 아직 원격에 없는 모든 태그
$ git push origin --tags&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;Annotated 태그만 전송&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--tags&lt;/code&gt;는 lightweight와 annotated를 모두 전송한다. annotated만 전송하려면 &lt;code&gt;--follow-tags&lt;/code&gt;를 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;태그 삭제&lt;/h3&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# 로컬에서 삭제
$ git tag -d v1.4-lw

# 원격에서 삭제
$ git push origin --delete v1.4-lw&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격 삭제는 다음 형태도 가능하지만 위 명령이 더 직관적이다.&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;$ git push origin :refs/tags/v1.4-lw&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;태그 체크아웃&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;태그 시점의 파일 상태를 보려면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ git checkout v2.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태는 &lt;b&gt;detached HEAD&lt;/b&gt;다. 어느 브랜치에도 속하지 않은 상태에서 커밋을 만들면 그 커밋은 정확한 해시를 모르면 다시 찾기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 시점에서 작업을 이어가려면 새 브랜치로 분기하는 게 안전하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ git checkout -b version2 v2.0.0&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DevTools</category>
      <category>GIT</category>
      <category>progit</category>
      <category>Remote</category>
      <category>Restore</category>
      <category>TAG</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/100</guid>
      <comments>https://onebrotravel.tistory.com/entry/Git-Basics-%E2%80%94-%EB%B3%80%EA%B2%BD-%EC%B7%A8%EC%86%8C%C2%B7%EC%9B%90%EA%B2%A9-%EC%A0%80%EC%9E%A5%EC%86%8C%C2%B7%ED%83%9C%EA%B7%B8#entry100comment</comments>
      <pubDate>Wed, 27 May 2026 13:59:45 +0900</pubDate>
    </item>
    <item>
      <title>Git Basics &amp;mdash; 저장소 만들기와 변경 기록</title>
      <link>https://onebrotravel.tistory.com/entry/Git-Basics-%E2%80%94-%EC%A0%80%EC%9E%A5%EC%86%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0%EC%99%80-%EB%B3%80%EA%B2%BD-%EA%B8%B0%EB%A1%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;저장소 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git을 사용하려면 먼저 Git 저장소가 있어야 한다. 저장소를 시작하는 방법은 두 가지: &lt;b&gt;기존 디렉터리를 Git 저장소로 만들기&lt;/b&gt; 또는 &lt;b&gt;이미 존재하는 저장소를 복제하기&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 디렉터리 초기화&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;cd /path/to/your/project
git init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 현재 디렉터리에 &lt;code&gt;.git&lt;/code&gt; 하위 디렉터리를 생성한다. &lt;code&gt;.git&lt;/code&gt; 안에는 저장소의 모든 메타데이터와 객체 데이터베이스가 들어간다. 이 시점에서는 아직 어떤 파일도 추적되고 있지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 파일들을 추적하려면 add + commit으로 첫 커밋을 만든다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;git add .
git commit -m &quot;Initial commit&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 저장소 복제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 존재하는 Git 저장소의 전체 사본을 받아오려면 &lt;code&gt;git clone&lt;/code&gt;을 쓴다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git clone https://github.com/libgit2/libgit2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 &lt;code&gt;libgit2&lt;/code&gt;라는 디렉터리를 만들고, 그 안에 &lt;code&gt;.git&lt;/code&gt; 디렉터리를 초기화한 뒤 원격 저장소의 &lt;b&gt;모든 데이터&lt;/b&gt;와 &lt;b&gt;모든 이력&lt;/b&gt;을 가져와 최신 버전을 체크아웃한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 이름으로 받고 싶다면 두 번째 인자를 지정한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;git clone https://github.com/libgit2/libgit2 mylibgit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git은 다양한 전송 프로토콜을 지원한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;https://&lt;/code&gt; &amp;mdash; HTTPS 프로토콜&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git@github.com:...&lt;/code&gt; &amp;mdash; SSH 프로토콜&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git://&lt;/code&gt; &amp;mdash; Git 자체 프로토콜&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 상태 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git에서 작업 디렉터리의 모든 파일은 다음 두 그룹 중 하나에 속한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Tracked&lt;/b&gt; (추적되는 파일): 이전 스냅샷에 있던 파일. Modified / Staged / Unmodified 중 하나의 상태&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Untracked&lt;/b&gt; (추적되지 않는 파일): 작업 디렉터리에 있지만 이전 스냅샷에도 없고 staging area에도 없는 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소를 처음 클론하면 모든 파일이 Tracked + Unmodified 상태다. 새로 만든 파일은 처음에 Untracked다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pi5L1/dJMcacJ3ER9/AwHvdVvVw85ElQngRjw0k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pi5L1/dJMcacJ3ER9/AwHvdVvVw85ElQngRjw0k1/img.png&quot; data-alt=&quot;파일 상태 생명주기 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pi5L1/dJMcacJ3ER9/AwHvdVvVw85ElQngRjw0k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpi5L1%2FdJMcacJ3ER9%2FAwHvdVvVw85ElQngRjw0k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1004&quot; height=&quot;428&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파일 상태 생명주기 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;git status&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 어떤 파일이 어떤 상태인지 확인하는 핵심 명령.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use &quot;git add &amp;lt;file&amp;gt;...&quot; to include in what will be committed)
    README

nothing added to commit but untracked files present (use &quot;git add&quot; to track)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력에서 알 수 있는 것:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 브랜치 (&lt;code&gt;master&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;원격 브랜치와의 차이 (없음)&lt;/li&gt;
&lt;li&gt;Untracked / Modified / Staged 파일 목록&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Short status &amp;mdash; &lt;code&gt;git status -s&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 출력이 부담스러울 때 압축형을 쓴다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 줄 앞의 두 글자가 상태를 나타낸다. &lt;b&gt;좌측은 staging area&lt;/b&gt;, &lt;b&gt;우측은 working directory&lt;/b&gt;.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표기&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;??&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Untracked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Staging area에 새로 추가됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;nbsp;M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Modified (스테이지 안 됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Modified 후 staged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Staged 했고, 그 이후 또 modified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변경 사항 보기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;git_diff_comparison.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPsofb/dJMcaak6HN4/K2gaK1J5Cfhhi0XfSUEo4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPsofb/dJMcaak6HN4/K2gaK1J5Cfhhi0XfSUEo4k/img.png&quot; data-alt=&quot;git diff 비교 대상 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPsofb/dJMcaak6HN4/K2gaK1J5Cfhhi0XfSUEo4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPsofb%2FdJMcaak6HN4%2FK2gaK1J5Cfhhi0XfSUEo4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;680&quot; data-filename=&quot;git_diff_comparison.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git diff 비교 대상 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git status&lt;/code&gt;는 어떤 파일이 변경됐는지 알려주지만, 정확히 &lt;b&gt;무엇이&lt;/b&gt; 변경됐는지 보려면 &lt;code&gt;git diff&lt;/code&gt;를 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git diff&lt;/code&gt;는 어느 영역과 어느 영역을 비교하느냐에 따라 두 가지 모드가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스테이지 안 한 변경 &amp;mdash; &lt;code&gt;git diff&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵션 없이 실행하면 &lt;b&gt;Working Directory vs Staging Area&lt;/b&gt; 비교. 즉 &quot;아직 스테이지하지 않은 변경&quot;을 보여준다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;git diff&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다음 커밋에 들어갈 변경 &amp;mdash; &lt;code&gt;git diff --staged&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--staged&lt;/code&gt; (또는 동의어 &lt;code&gt;--cached&lt;/code&gt;) 옵션을 붙이면 &lt;b&gt;Staging Area vs 마지막 커밋(HEAD)&lt;/b&gt; 비교. 즉 &quot;다음 커밋에 들어갈 변경&quot;을 보여준다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;git diff --staged&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변경 추적과 커밋&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 추가&amp;middot;스테이징 &amp;mdash; &lt;code&gt;git add&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;git add&lt;/code&gt;는 두 가지 일을 모두 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Untracked 파일을 &lt;b&gt;추적 대상&lt;/b&gt;으로 만든다&lt;/li&gt;
&lt;li&gt;Modified 파일을 &lt;b&gt;다음 커밋에 포함될 변경&lt;/b&gt;으로 스테이지한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;git add README
git add CONTRIBUTING.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디렉터리를 지정하면 그 아래 모든 파일을 재귀적으로 처리한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;git add .&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커밋 만들기 &amp;mdash; &lt;code&gt;git commit&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스테이지된 변경 사항을 영구 저장하는 명령.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;git commit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령은 에디터를 열어 커밋 메시지 작성을 요구한다. 메시지를 저장하고 에디터를 닫으면 커밋이 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄 메시지면 &lt;code&gt;-m&lt;/code&gt; 옵션이 빠르다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;git commit -m &quot;Story 182: Fix benchmarks for speed&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Staging 생략 &amp;mdash; &lt;code&gt;git commit -a&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 tracked인 파일들에 한해, staging area를 거치지 않고 바로 커밋할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;git commit -a -m &quot;Quick fix&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션은 모든 Modified 파일을 자동으로 staging하고 커밋한다. &lt;b&gt;단, Untracked 파일은 포함되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-a&lt;/code&gt; 옵션의 트레이드오프&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Staging area를 거치지 않는다는 건 &quot;커밋에 들어갈 변경을 선별하는&quot; 단계를 건너뛴다는 뜻이다. 작은 수정에는 편하지만, 여러 변경을 작업 단위로 나누어 커밋하는 워크플로우와는 맞지 않는다. 모든 커밋이 &quot;이번 작업에서 바꾼 거 전부&quot;가 되어버린다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추적 제외 &amp;mdash; &lt;code&gt;.gitignore&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 산출물, 로그 파일, 의존성 디렉터리(&lt;code&gt;node_modules&lt;/code&gt; 등)처럼 추적하고 싶지 않은 파일들이 있다. 이런 파일들은 &lt;code&gt;.gitignore&lt;/code&gt;에 패턴으로 등록한다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# 모든 .a 파일 무시
*.a

# 단 lib.a는 추적 (앞의 규칙 무효화)
!lib.a

# 현재 디렉터리의 TODO 파일만 무시 (subdir/TODO는 추적)
/TODO

# build 라는 이름의 모든 디렉터리 무시
build/

# doc/notes.txt 무시, doc/server/arch.txt는 추적
doc/*.txt

# doc/ 아래 모든 .pdf 파일 (재귀)
doc/**/*.pdf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴 문법 요약:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt; &amp;mdash; 임의의 문자열&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!&lt;/code&gt; &amp;mdash; 앞의 무시 규칙을 무효화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; 접두사 &amp;mdash; 루트 디렉터리에서만 매치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; 접미사 &amp;mdash; 디렉터리 매치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;**&lt;/code&gt; &amp;mdash; 재귀적 매치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub가 언어&amp;middot;프레임워크별 표준 &lt;code&gt;.gitignore&lt;/code&gt; 템플릿을 제공한다 &amp;rarr; &lt;a href=&quot;https://github.com/github/gitignore&quot;&gt;github.com/github/gitignore&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 제거와 이동&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 제거 &amp;mdash; &lt;code&gt;git rm&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tracked 파일을 작업 디렉터리에서 제거하고 staging area에서도 빼는 명령.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;git rm README.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &lt;code&gt;rm&lt;/code&gt;만 사용해도 작동은 한다 (Modified + Deleted 상태로 잡힘). 다만 &lt;code&gt;git rm&lt;/code&gt;이 한 번에 처리해주므로 더 깔끔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디스크 파일은 남기고, Git 추적에서만 제외&lt;/b&gt;하고 싶다면 &lt;code&gt;--cached&lt;/code&gt; 옵션을 쓴다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;git rm --cached config.local&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;에 등록을 깜빡한 채 commit해버린 파일을 사후에 제외할 때 자주 쓴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 이동&amp;middot;이름 변경 &amp;mdash; &lt;code&gt;git mv&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;git mv README.md README&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git은 파일의 rename을 자동으로 추적한다. 사실 &lt;code&gt;git mv&lt;/code&gt;는 다음 세 명령의 단축형이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;mv README.md README
git rm README.md
git add README&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이력 보기 &amp;mdash; &lt;code&gt;git log&lt;/code&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 히스토리를 시간 역순으로 출력한다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;git log&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 출력은 각 커밋의 SHA, 저자, 날짜, 커밋 메시지를 모두 보여준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 옵션&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;효과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--oneline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커밋당 한 줄 (SHA 짧게 + 메시지)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--graph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브랜치&amp;middot;머지를 ASCII 그래프로 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 브랜치의 커밋 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-p&lt;/code&gt; 또는 &lt;code&gt;--patch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;각 커밋의 diff까지 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;각 커밋의 변경 파일&amp;middot;라인 수 통계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&amp;lt;n&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최근 n개만 (예: &lt;code&gt;git log -5&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--pretty=format:&quot;...&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커스텀 포맷으로 출력&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 쓰이는 조합은 &lt;code&gt;git log --oneline --graph --all&lt;/code&gt; &amp;mdash; 모든 브랜치의 흐름을 한 화면에 압축.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출력 범위 제한&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;효과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--since=&amp;lt;date&amp;gt;&lt;/code&gt;, &lt;code&gt;--until=&amp;lt;date&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;시간 범위 (예: &lt;code&gt;--since=2.weeks&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--author=&amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 저자의 커밋만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--grep=&amp;lt;pattern&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커밋 메시지에 패턴이 들어간 것만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-- &amp;lt;path&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;특정 파일&amp;middot;경로의 변경이 포함된 커밋만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 옵션을 조합할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;git log --since=2.weeks --author=&quot;Alice&quot; --oneline&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 쓰는 조합을 alias로 등록해두면 편하다.&lt;i&gt;&lt;/i&gt;&lt;/p&gt;</description>
      <category>DevTools</category>
      <category>COMMIT</category>
      <category>GIT</category>
      <category>gitignore</category>
      <category>log</category>
      <category>progit</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/99</guid>
      <comments>https://onebrotravel.tistory.com/entry/Git-Basics-%E2%80%94-%EC%A0%80%EC%9E%A5%EC%86%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0%EC%99%80-%EB%B3%80%EA%B2%BD-%EA%B8%B0%EB%A1%9D#entry99comment</comments>
      <pubDate>Tue, 26 May 2026 23:57:01 +0900</pubDate>
    </item>
    <item>
      <title>Git 설치와 초기 설정</title>
      <link>https://onebrotravel.tistory.com/entry/Git-%EC%84%A4%EC%B9%98%EC%99%80-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Git 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git을 사용하려면 컴퓨터에 Git이 설치되어 있어야 한다. 설치 방법은 크게 세 가지: &lt;b&gt;패키지 관리자&lt;/b&gt;, &lt;b&gt;공식 인스톨러&lt;/b&gt;, &lt;b&gt;소스 빌드&lt;/b&gt;. 일반적인 사용 환경에서는 앞의 두 방법이면 충분하고, 최신 버전이 꼭 필요한 경우에만 소스 빌드를 선택한다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;Git 버전 호환성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pro Git은 Git 2.x 기준으로 작성되었으며, Git은 하위 호환성이 매우 뛰어나다. 최근 버전이라면 책의 명령어 대부분이 그대로 작동한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Linux&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포판의 패키지 관리자를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Fedora &amp;middot; RHEL &amp;middot; CentOS&lt;/b&gt; (RPM 계열):&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;sudo dnf install git-all&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ubuntu &amp;middot; Debian&lt;/b&gt; 계열:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;sudo apt install git-all&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기타 Unix 계열 배포판은 &lt;a href=&quot;https://git-scm.com/downloads/linux&quot;&gt;Git 공식 다운로드 페이지&lt;/a&gt; 참조.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;macOS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법은 Xcode Command Line Tools 설치다. macOS Mavericks(10.9) 이상에서는 터미널에 처음 &lt;code&gt;git&lt;/code&gt; 명령을 입력하면 자동으로 설치 프롬프트가 뜬다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;git --version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git이 없으면 위 명령에 설치 안내가 따라온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 버전이 필요하면 &lt;a href=&quot;https://git-scm.com/downloads/mac&quot;&gt;공식 macOS 인스톨러&lt;/a&gt;를 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Windows&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 설치 파일은 &lt;a href=&quot;https://git-scm.com/downloads/win&quot;&gt;git-scm.com/downloads/win&lt;/a&gt;에서 받는다. 페이지 접속 시 자동으로 다운로드가 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설치 파일은 &quot;Git for Windows&quot;라는 별도 프로젝트가 유지하며, Git 본 프로젝트와는 구분된다. 자세한 정보는 &lt;a href=&quot;https://gitforwindows.org/&quot;&gt;gitforwindows.org&lt;/a&gt; 참조.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화된 설치가 필요한 경우 &lt;a href=&quot;https://community.chocolatey.org/packages/git&quot;&gt;Chocolatey 패키지&lt;/a&gt;로 설치할 수도 있다. 단, Chocolatey 패키지는 커뮤니티 유지보수다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소스 빌드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리 인스톨러는 최신 버전보다 다소 뒤처져 있을 수 있다. 최신 버전이 꼭 필요할 때 소스 빌드를 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존성 설치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# Fedora 계열
sudo dnf install dh-autoreconf curl-devel expat-devel gettext-devel \
  openssl-devel perl-devel zlib-devel

# Debian 계열
sudo apt-get install dh-autoreconf libcurl4-gnutls-dev libexpat1-dev \
  gettext libz-dev libssl-dev&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서(doc/HTML/info) 생성을 원한다면 추가 의존성이 필요하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# Fedora 계열
sudo dnf install asciidoc xmlto docbook2X

# Debian 계열
sudo apt-get install asciidoc xmlto docbook2x&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;RHEL &amp;middot; CentOS &amp;middot; Scientific Linux 추가 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;docbook2X&lt;/code&gt; 패키지 설치를 위해 &lt;a href=&quot;https://docs.fedoraproject.org/en-US/epel/#how_can_i_use_these_extra_packages&quot;&gt;EPEL 저장소&lt;/a&gt;를 활성화해야 한다. 또한 &lt;code&gt;db2x_docbook2texi&lt;/code&gt;의 이름이 다르므로 심볼릭 링크가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sudo ln -s /usr/bin/db2x_docbook2texi /usr/bin/docbook2x-texi&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소스 다운로드 + 빌드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스는 &lt;a href=&quot;https://www.kernel.org/pub/software/scm/git/&quot;&gt;kernel.org 공식 페이지&lt;/a&gt; 또는 &lt;a href=&quot;https://github.com/git/git/tags&quot;&gt;GitHub 미러&lt;/a&gt;에서 받는다. kernel.org는 릴리스 서명도 함께 제공하므로 다운로드 신뢰성 검증이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;tar -zxf git-2.8.0.tar.gz
cd git-2.8.0
make configure
./configure --prefix=/usr
make all doc info
sudo make install install-doc install-html install-info&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 후에는 Git 자체로 Git 저장소를 클론받아 업데이트할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;git clone https://git.kernel.org/pub/scm/git/git.git&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;초기 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git이 설치되면 환경을 개인에게 맞게 초기 설정한다. 이 설정은 한 컴퓨터에서 한 번만 하면 되며, 업그레이드 이후에도 그대로 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git의 설정 도구는 &lt;code&gt;git config&lt;/code&gt;다. 설정은 다음 세 위치 중 하나에 저장된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정이 저장되는 세 위치&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위치&lt;/th&gt;
&lt;th&gt;옵션&lt;/th&gt;
&lt;th&gt;적용 범위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[path]/etc/gitconfig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;시스템의 모든 사용자&amp;middot;저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.gitconfig&lt;/code&gt; 또는 &lt;code&gt;~/.config/git/config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--global&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.git/config&lt;/code&gt; (저장소 내부)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--local&lt;/code&gt; (기본값)&lt;/td&gt;
&lt;td&gt;현재 저장소만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우선순위&lt;/b&gt;: &lt;code&gt;local &amp;gt; global &amp;gt; system&lt;/code&gt;. 좁은 범위의 설정이 넓은 범위를 덮어쓴다. 예를 들어 &lt;code&gt;--global&lt;/code&gt;로 &lt;code&gt;user.email&lt;/code&gt;을 설정해두고, 특정 저장소에서만 &lt;code&gt;--local&lt;/code&gt;로 다른 이메일을 설정하면 해당 저장소에서는 local 값이 적용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;git_config_scopes.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NyZ2f/dJMcaiDsF4b/T0BncBiDlxywuZK6SxdR1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NyZ2f/dJMcaiDsF4b/T0BncBiDlxywuZK6SxdR1K/img.png&quot; data-alt=&quot;git config 스코프 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NyZ2f/dJMcaiDsF4b/T0BncBiDlxywuZK6SxdR1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNyZ2f%2FdJMcaiDsF4b%2FT0BncBiDlxywuZK6SxdR1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;800&quot; data-filename=&quot;git_config_scopes.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git config 스코프 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--system&lt;/code&gt;은 시스템 구성 파일을 건드리므로 관리자 권한이 필요하다. 개인 작업 환경에서는 &lt;code&gt;--global&lt;/code&gt;이 가장 자주 쓰인다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 정보 (필수)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git을 설치한 후 가장 먼저 해야 할 설정. Git은 커밋을 만들 때마다 이 정보를 메타데이터로 영구히 기록한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global user.name &quot;John Doe&quot;
git config --global user.email &quot;johndoe@example.com&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--global&lt;/code&gt;을 사용하면 해당 컴퓨터의 모든 프로젝트에서 이 값이 기본값으로 쓰인다. 특정 프로젝트에서 다른 이름&amp;middot;이메일을 사용하고 싶다면 그 저장소 안에서 &lt;code&gt;--global&lt;/code&gt; 없이 같은 명령을 실행하면 로컬 설정이 덮어쓴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 편집기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git이 커밋 메시지나 기타 메시지를 작성할 때 실행하는 편집기. 설정하지 않으면 시스템 기본 편집기를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global core.editor vim&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vim은 Unix 계열 시스템에 대부분 기본 탑재되어 있어 추가 설치가 필요 없는 경우가 많지만, 환경에 따라 직접 설치해야 할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Linux&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;# Fedora 계열
sudo dnf install vim

# Debian 계열
sudo apt install vim&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;macOS&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 탑재된 Vim을 그대로 사용하거나, Homebrew로 최신 버전을 설치할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;brew install vim&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Windows&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git for Windows 설치 시 Vim도 함께 들어가므로 별도 설치가 보통은 필요 없다. 그렇지 않은 경우 &lt;a href=&quot;https://www.vim.org/download.php&quot;&gt;vim.org 공식 페이지&lt;/a&gt;에서 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 확인:&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;vim --version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 편집기를 쓰고 싶다면 &lt;code&gt;core.editor&lt;/code&gt; 값을 바꾸면 된다. Emacs, VS Code 등도 같은 방식으로 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global core.editor emacs
git config --global core.editor &quot;code --wait&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows에서는 편집기의 전체 경로를 정확히 지정해야 한다. 예를 들어 Notepad++:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global core.editor &quot;'C:/Program Files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;편집기 미설정 시 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;편집기를 적절히 설정하지 않으면 Git이 편집기를 실행할 때 문제가 발생할 수 있다. 특히 Windows에서는 Git 명령이 비정상 종료되거나 커밋 메시지가 작성되지 않은 상태로 종료되는 혼란스러운 상태에 빠질 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 브랜치명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 2.28 이전 버전은 &lt;code&gt;git init&lt;/code&gt; 시 자동으로 &lt;code&gt;master&lt;/code&gt; 브랜치를 생성했다. 2.28부터는 기본 브랜치 이름을 사용자가 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global init.defaultBranch main&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 적용된 모든 설정을 보려면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --list&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;user.name=John Doe
user.email=johndoe@example.com
color.status=auto
color.branch=auto
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 키가 여러 번 출력될 수 있다. Git이 여러 설정 파일(&lt;code&gt;~/.gitconfig&lt;/code&gt;, &lt;code&gt;[path]/etc/gitconfig&lt;/code&gt; 등)에서 같은 키를 읽기 때문이다. 이 경우 마지막으로 읽은 값이 최종값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 키의 값만 보려면:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config user.name&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정의 &lt;b&gt;출처&lt;/b&gt;까지 확인하려면 &lt;code&gt;--show-origin&lt;/code&gt; 옵션을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --list --show-origin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상치 못한 값이 적용되어 있을 때 어느 설정 파일이 우선 적용되었는지 추적할 수 있다.&lt;/p&gt;</description>
      <category>DevTools</category>
      <category>GIT</category>
      <category>progit</category>
      <category>설정</category>
      <category>설치</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/98</guid>
      <comments>https://onebrotravel.tistory.com/entry/Git-%EC%84%A4%EC%B9%98%EC%99%80-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95#entry98comment</comments>
      <pubDate>Tue, 26 May 2026 22:56:06 +0900</pubDate>
    </item>
    <item>
      <title>Git을 이해하기 위한 세 가지 &amp;mdash; VCS, 데이터 모델, Three States</title>
      <link>https://onebrotravel.tistory.com/entry/Git%EC%9D%84-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%84%B8-%EA%B0%80%EC%A7%80-%E2%80%94-VCS-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8-Three-States</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;버전 관리란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;버전 관리(version control)&lt;/b&gt; 는 파일의 변경 이력을 시간순으로 기록해두는 작업이다. 변경할 때마다 누가, 언제, 무엇을 바꿨는지 기록을 남기면 다음과 같은 일이 가능해진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 파일이나 전체 프로젝트를 과거의 어느 시점 상태로 복원&lt;/li&gt;
&lt;li&gt;두 시점 사이에 어떤 변경이 있었는지 비교&lt;/li&gt;
&lt;li&gt;문제를 일으킨 변경의 시점&amp;middot;작성자&amp;middot;내용 추적&lt;/li&gt;
&lt;li&gt;실수로 손상&amp;middot;삭제된 파일 복구&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드만의 이야기가 아니다. 이미지, 문서, 디자인 레이아웃 등 시간에 따라 바뀌는 거의 모든 파일에 적용할 수 있는 개념이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 필요한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 버전 관리는 파일을 복사해서 다른 이름으로 저장하는 것이다. &lt;code&gt;report.docx&lt;/code&gt;, &lt;code&gt;report_v2.docx&lt;/code&gt;, &lt;code&gt;report_final.docx&lt;/code&gt;, &lt;code&gt;report_final_진짜.docx&lt;/code&gt; 같은 형태.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;b&gt;혼자 작업할 때조차&lt;/b&gt; 다음 문제를 일으킨다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어느 파일이 최신인지 잊기 쉬움&lt;/li&gt;
&lt;li&gt;잘못된 파일을 덮어쓸 위험&lt;/li&gt;
&lt;li&gt;두 시점의 변경 내역을 비교하기 어려움&lt;/li&gt;
&lt;li&gt;파일 수가 늘어날수록 디렉터리가 폭발&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여러 명이 협업하면&lt;/b&gt; 문제가 한 차원 더 커진다. A가 수정한 부분과 B가 수정한 부분이 충돌할 때 어떻게 합칠지, 누가 어떤 시점에 무엇을 바꿨는지 파악할 방법이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제들을 체계적으로 해결하기 위해 등장한 것이 &lt;b&gt;버전 관리 시스템(VCS, Version Control System)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VCS의 진화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VCS는 크게 세 세대(Local / Centralized / Distributed)로 진화해왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 VCS&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;835&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6Bbdv/dJMcad3aFMW/hYkFPHUkY35kFsDZ4ko6X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6Bbdv/dJMcad3aFMW/hYkFPHUkY35kFsDZ4ko6X0/img.png&quot; data-alt=&quot;로컬 버전 관리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6Bbdv/dJMcad3aFMW/hYkFPHUkY35kFsDZ4ko6X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6Bbdv%2FdJMcad3aFMW%2FhYkFPHUkY35kFsDZ4ko6X0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;617&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;835&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;로컬 버전 관리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 복사 방식을 체계화한 첫 세대. 대표적으로 RCS는 파일 간 변경을 패치(patch) 형태로 저장하고, 이를 누적 적용해 특정 시점의 파일을 복원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한계: &lt;b&gt;협업 불가능&lt;/b&gt;. 다른 사람과 같은 프로젝트를 공유할 방법이 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중앙집중식 VCS (CVCS)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;693&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buRPcp/dJMcahkiO6N/1Uhz4w5byApaDYPkRGD70k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buRPcp/dJMcahkiO6N/1Uhz4w5byApaDYPkRGD70k/img.png&quot; data-alt=&quot;중앙집중식 버전 관리(CVCS)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buRPcp/dJMcahkiO6N/1Uhz4w5byApaDYPkRGD70k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuRPcp%2FdJMcahkiO6N%2F1Uhz4w5byApaDYPkRGD70k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;990&quot; height=&quot;693&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;중앙집중식 버전 관리(CVCS)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CVS, Subversion, Perforce 등이 속한다. 모든 버전 관리 파일을 하나의 중앙 서버에 저장하고, 클라이언트들은 서버에서 파일을 체크아웃해 작업한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;협업 참여자가 서로의 작업 상태를 파악할 수 있음&lt;/li&gt;
&lt;li&gt;관리자가 권한을 세밀하게 제어 가능&lt;/li&gt;
&lt;li&gt;개별 로컬 데이터베이스보다 중앙 서버 한 곳 관리가 단순&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;치명적 단점: &lt;b&gt;단일 실패 지점(Single Point of Failure)&lt;/b&gt;. 중앙 서버가 멈추면 협업도 멈춘다. 하드디스크가 손상되고 백업이 없으면 전체 이력이 사라진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분산 VCS (DVCS)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;1161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbkPAw/dJMcafzZkHw/kzMgLiaJ5FOw03MH1LsD41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbkPAw/dJMcafzZkHw/kzMgLiaJ5FOw03MH1LsD41/img.png&quot; data-alt=&quot;분산 버전 관리 시스템(DVCS)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbkPAw/dJMcafzZkHw/kzMgLiaJ5FOw03MH1LsD41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbkPAw%2FdJMcafzZkHw%2FkzMgLiaJ5FOw03MH1LsD41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;862&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;970&quot; data-origin-height=&quot;1161&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분산 버전 관리 시스템(DVCS)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git, Mercurial, Darcs 등이 속한다. 클라이언트가 최신 스냅샷만 받아오는 것이 아니라 &lt;b&gt;저장소 전체를 통째로 복제&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 결과:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 클론이 전체 데이터의 완전한 백업&lt;/li&gt;
&lt;li&gt;서버 손실 시 클라이언트 중 하나로 복구 가능&lt;/li&gt;
&lt;li&gt;여러 원격 저장소와 동시에 작업 가능 &amp;rarr; 계층적 협업 모델 등 다양한 워크플로우 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Git의 데이터 모델 &amp;mdash; Snapshots, Not Differences&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git과 다른 VCS의 가장 근본적인 차이는 &lt;b&gt;데이터를 저장하는 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;델타 기반 (CVS&amp;middot;Subversion 등)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;storage_delta_based.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYRcpb/dJMcadPC4qx/gCgcZ8W0EpflSXYmeHkj5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYRcpb/dJMcadPC4qx/gCgcZ8W0EpflSXYmeHkj5k/img.png&quot; data-alt=&quot;델타 기반 저장 방식 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYRcpb/dJMcadPC4qx/gCgcZ8W0EpflSXYmeHkj5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYRcpb%2FdJMcadPC4qx%2FgCgcZ8W0EpflSXYmeHkj5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;680&quot; data-filename=&quot;storage_delta_based.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;델타 기반 저장 방식 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 세트 하나를 기준점으로 잡고, 시간이 흐르며 어떤 파일이 어떻게 변경됐는지 그 차이(delta)를 누적 기록한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스냅샷 기반 (Git)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;storage_snapshot_based.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7dfSB/dJMcacDhCRO/phkIb6HiiYK1BCNqvKOCRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7dfSB/dJMcacDhCRO/phkIb6HiiYK1BCNqvKOCRk/img.png&quot; data-alt=&quot;스냅샷 기반 저장 방식 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7dfSB/dJMcacDhCRO/phkIb6HiiYK1BCNqvKOCRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7dfSB%2FdJMcacDhCRO%2FphkIb6HiiYK1BCNqvKOCRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;680&quot; data-filename=&quot;storage_snapshot_based.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스냅샷 기반 저장 방식 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋할 때마다 그 시점의 전체 파일 상태를 통째로 저장한다. 효율을 위해 변경되지 않은 파일은 다시 저장하지 않고 이전 버전 링크만 남기지만, &lt;b&gt;개념상 매 커밋은 독립된 스냅샷&lt;/b&gt;이다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-quote&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;Pro Git, 1장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git은 단순한 버전 관리 시스템이 아니라, 작은 파일 시스템 위에 강력한 도구들이 얹혀진 구조다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅샷 모델이 만들어내는 결과:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;거의 모든 작업이 로컬에서 즉시 처리됨&lt;/b&gt; &amp;mdash; 히스토리 조회, diff, 과거 버전 비교 모두 네트워크 불필요. 오프라인에서도 커밋 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무결성 보장&lt;/b&gt; &amp;mdash; 모든 데이터는 SHA-1 해시로 체크섬된다. Git이 모르게 파일이 변경되는 일은 불가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 손실에 강함&lt;/b&gt; &amp;mdash; Git의 대부분 작업은 데이터를 추가(add)할 뿐이다. 커밋된 스냅샷은 매우 제거하기 어렵다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Three States&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;three_states_flow.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8zRA2/dJMcacDhCSm/2J1WwZdIUkdJZHyHeClfTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8zRA2/dJMcacDhCSm/2J1WwZdIUkdJZHyHeClfTk/img.png&quot; data-alt=&quot;Three States 흐름 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8zRA2/dJMcacDhCSm/2J1WwZdIUkdJZHyHeClfTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8zRA2%2FdJMcacDhCSm%2F2J1WwZdIUkdJZHyHeClfTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;640&quot; data-filename=&quot;three_states_flow.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Three States 흐름 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git에서 파일은 세 가지 상태 중 하나에 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Modified&lt;/b&gt; (수정됨)&lt;/td&gt;
&lt;td&gt;파일 내용을 바꿨지만 아직 Git 데이터베이스에 커밋하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Staged&lt;/b&gt; (스테이징됨)&lt;/td&gt;
&lt;td&gt;변경된 파일을 다음 커밋에 포함시키도록 표시함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Committed&lt;/b&gt; (커밋됨)&lt;/td&gt;
&lt;td&gt;데이터가 로컬 Git 데이터베이스에 저장됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 상태는 Git 프로젝트의 세 가지 영역과 짝을 이룬다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Working Directory&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;실제 파일들이 디스크에 풀려있는 곳. 사용자가 편집하는 공간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Staging Area&lt;/b&gt; (= &quot;index&quot;)&lt;/td&gt;
&lt;td&gt;다음 커밋에 들어갈 변경사항 목록. &lt;code&gt;.git&lt;/code&gt; 내부 파일로 존재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Git Directory&lt;/b&gt; (&lt;code&gt;.git&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;메타데이터와 객체 데이터베이스. clone 시 함께 복사되는 부분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 작업 흐름&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Working Directory에서 파일을 수정한다 &amp;rarr; &lt;b&gt;Modified&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;다음 커밋에 포함할 변경만 골라 Staging Area에 올린다 (&lt;code&gt;git add&lt;/code&gt;) &amp;rarr; &lt;b&gt;Staged&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;커밋하면 Staging Area의 내용이 &lt;code&gt;.git&lt;/code&gt;에 영구 저장된다 (&lt;code&gt;git commit&lt;/code&gt;) &amp;rarr; &lt;b&gt;Committed&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Staging Area의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git이 &lt;code&gt;add&lt;/code&gt;와 &lt;code&gt;commit&lt;/code&gt;을 분리한 이유는 &lt;b&gt;커밋 단위를 사용자가 직접 큐레이션할 수 있게 하기 위함&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업한 모든 변경을 통째로 커밋해야 한다면 &amp;mdash; SVN처럼 &amp;mdash; 커밋 히스토리는 &quot;여러 작업이 섞인 덩어리&quot;가 된다. Staging Area가 있으면 같은 작업 디렉터리 안에서도 A 변경은 첫 커밋, B 변경은 두 번째 커밋으로 분리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 Git을 단순 &quot;버전 관리 도구&quot;가 아니라 &quot;히스토리 디자인 도구&quot;로 만드는 차별점이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버전 관리는 파일의 변경 이력을 기록해 복원&amp;middot;비교&amp;middot;추적&amp;middot;복구를 가능하게 하는 작업이며, 협업이 더해질 때 필수가 된다&lt;/li&gt;
&lt;li&gt;VCS는 &lt;b&gt;로컬 &amp;rarr; 중앙집중 &amp;rarr; 분산&lt;/b&gt;으로 진화해왔다 &amp;mdash; 협업&amp;middot;복구&amp;middot;신뢰성 문제를 단계적으로 해결&lt;/li&gt;
&lt;li&gt;Git은 변경 차이(delta)가 아니라 &lt;b&gt;스냅샷&lt;/b&gt;을 저장한다 &amp;mdash; 로컬 즉시 처리&amp;middot;무결성&amp;middot;데이터 손실 강건성의 기반&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Three States(Modified/Staged/Committed)&lt;/b&gt; + &lt;b&gt;세 영역(Working/Staging/.git)&lt;/b&gt; &amp;mdash; Git 명령어 대부분이 이 모델 안에서 영역 간 데이터 이동으로 해석된다&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DevTools</category>
      <category>GIT</category>
      <category>progit</category>
      <category>VCS</category>
      <category>버전관리</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/97</guid>
      <comments>https://onebrotravel.tistory.com/entry/Git%EC%9D%84-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%84%B8-%EA%B0%80%EC%A7%80-%E2%80%94-VCS-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8-Three-States#entry97comment</comments>
      <pubDate>Tue, 26 May 2026 22:31:12 +0900</pubDate>
    </item>
    <item>
      <title>Custom Git Alias 목록</title>
      <link>https://onebrotravel.tistory.com/entry/Custom-Git-Alias-%EB%AA%A9%EB%A1%9D</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Git을 자주 쓰다 보면 &lt;code&gt;checkout&lt;/code&gt;, &lt;code&gt;branch&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt; 같은 명령어를 하루에도 수십 번씩 타이핑한다. 명령어가 길어서가 아니라, 반복 횟수 자체가 많으니 누적되는 피로가 꽤 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git에는 자주 쓰는 명령어를 짧은 별명으로 등록하는 &lt;b&gt;alias&lt;/b&gt; 기능이 내장돼 있다. &lt;code&gt;git config&lt;/code&gt;로 한 번 등록해두면 그 컴퓨터의 모든 저장소에서 단축 명령으로 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 내가 실제로 사용 중인 alias 7개를 그대로 공유한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정 목록&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/.gitconfig&lt;/code&gt;에 다음 7줄을 한 번씩 실행하면 모든 alias가 등록된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.st status
git config --global alias.ci commit
git config --global alias.last 'log -1 HEAD --stat'
git config --global alias.lg &quot;log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)&amp;lt;%an&amp;gt;%Creset' --abbrev-commit&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--global&lt;/code&gt; 플래그&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--global&lt;/code&gt;을 붙이면 사용자 홈 디렉토리의 &lt;code&gt;~/.gitconfig&lt;/code&gt;에 기록되어 모든 저장소에서 작동한다. 특정 저장소에만 적용하고 싶다면 &lt;code&gt;--global&lt;/code&gt;을 빼고 그 저장소 안에서 실행하면 된다. 그 경우 &lt;code&gt;.git/config&lt;/code&gt;에 저장된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;매일 쓰는 4개&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.st status
git config --global alias.ci commit&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;alias&lt;/th&gt;
&lt;th&gt;원본&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git co&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git checkout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브랜치 전환, 파일 복원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git br&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git branch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브랜치 목록&amp;middot;생성&amp;middot;삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git st&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;작업 디렉토리 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git ci&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git commit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커밋 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &lt;code&gt;git status&lt;/code&gt; 9타가 &lt;code&gt;git st&lt;/code&gt; 5타로 줄어드는 것 같지만 의외로 체감이 많이 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마지막 커밋 확인하기&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;git config --global alias.last 'log -1 HEAD --stat'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 최근 커밋 1개의 상세 정보(변경 파일 목록 + 라인 수)를 한 줄 명령으로 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIULuN/dJMcad3awNi/MCdJsOuPbstGWBInIAlZ8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIULuN/dJMcad3awNi/MCdJsOuPbstGWBInIAlZ8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIULuN/dJMcad3awNi/MCdJsOuPbstGWBInIAlZ8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIULuN%2FdJMcad3awNi%2FMCdJsOuPbstGWBInIAlZ8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1568&quot; height=&quot;346&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방금 무엇을 커밋했는지 빠르게 되돌아볼 때 유용하다. &lt;code&gt;git log&lt;/code&gt; 전체를 띄우지 않아도 직전 작업 단위만 확인 가능.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;풍성한 그래프 뷰&lt;/h2&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;git config --global alias.lg &quot;log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)&amp;lt;%an&amp;gt;%Creset' --abbrev-commit&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;graph&lt;/code&gt;보다 정보량을 늘린 버전. &lt;code&gt;--pretty=format:&lt;/code&gt; 안에서 색깔별로 의미를 구분한다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;색&lt;/th&gt;
&lt;th&gt;토큰&lt;/th&gt;
&lt;th&gt;표시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;빨강&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;짧은 커밋 해시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노랑&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브랜치/태그 ref&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(기본)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커밋 메시지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초록&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%cr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;상대 시간 (예: &quot;3시간 전&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파랑&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%an&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;저자명&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1569&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RkmlE/dJMcadhRRyO/fhpqfS93pcENW7jzK7Yzvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RkmlE/dJMcadhRRyO/fhpqfS93pcENW7jzK7Yzvk/img.png&quot; data-alt=&quot;git lg 실행 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RkmlE/dJMcadhRRyO/fhpqfS93pcENW7jzK7Yzvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRkmlE%2FdJMcadhRRyO%2FfhpqfS93pcENW7jzK7Yzvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1569&quot; height=&quot;348&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1569&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git lg 실행 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`git log --graph`를 내가 원하는 정보를 포함해서 더 자주 볼 수 있게 개량한 것. 처음 보면 &quot;이거 좀 과한데?&quot; 싶지만 익숙해지면 한눈에 더 많은 정보가 들어온다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 정리한 7개는 출발점일 뿐, alias는 본인이 자주 치는 명령일수록 강력해진다. 며칠 작업하면서 손이 가장 많이 가는 명령을 관찰해보고, 거기서부터 alias로 옮기면 자연스럽게 본인만의 단축 세트가 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;~/.gitconfig&lt;/code&gt;를 직접 열어 &lt;code&gt;[alias]&lt;/code&gt; 섹션을 편집해도 같은 효과다.&lt;/p&gt;</description>
      <category>DevTools</category>
      <category>Alias</category>
      <category>GIT</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/96</guid>
      <comments>https://onebrotravel.tistory.com/entry/Custom-Git-Alias-%EB%AA%A9%EB%A1%9D#entry96comment</comments>
      <pubDate>Tue, 26 May 2026 17:45:57 +0900</pubDate>
    </item>
    <item>
      <title>맵(Map)과 해시 테이블(Hash Table)</title>
      <link>https://onebrotravel.tistory.com/entry/%EB%A7%B5Map%EA%B3%BC-%ED%95%B4%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94Hash-Table</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;맵(Map)은 키-값 쌍을 저장하는 추상 자료형(ADT)으로, 같은 키를 가진 쌍은 최대 하나만 존재합니다. 전화번호와 이름, 단어와 빈도수, 사용자 ID와 프로필처럼 &quot;키로 값을 찾는&quot; 모든 곳에 쓰입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블은 Map을 구현한 자료구조 중 가장 흔히 쓰이는 형태로, &lt;b&gt;해시 함수&lt;/b&gt;를 통해 키를 배열의 인덱스로 변환해 평균 $O(1)$ 시간에 데이터에 접근합니다. Python의 &lt;code&gt;dict&lt;/code&gt;, Java의 &lt;code&gt;HashMap&lt;/code&gt;, C++의 &lt;code&gt;unordered_map&lt;/code&gt;이 모두 해시 테이블 기반입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hash_table_structure.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7fOBs/dJMcahR2Q6y/YvHMES5jbvh8DMaYzJG7b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7fOBs/dJMcahR2Q6y/YvHMES5jbvh8DMaYzJG7b0/img.png&quot; data-alt=&quot;Hash table&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7fOBs/dJMcahR2Q6y/YvHMES5jbvh8DMaYzJG7b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7fOBs%2FdJMcahR2Q6y%2FYvHMES5jbvh8DMaYzJG7b0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;680&quot; data-filename=&quot;hash_table_structure.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Hash table&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 개념&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;해시 함수(Hash Function)&lt;/b&gt;: 키를 배열 인덱스로 변환하는 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버킷(Bucket)&lt;/b&gt;: 해시 테이블의 각 슬롯&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해시 충돌(Hash Collision)&lt;/b&gt;: 서로 다른 키가 같은 인덱스로 해시되는 현상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로드 팩터(Load Factor)&lt;/b&gt;: 저장된 원소 수 / 테이블 크기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해시 함수&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 해시 함수의 조건&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;균등 분포&lt;/b&gt;: 키들이 테이블 전체에 고르게 분산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빠른 계산&lt;/b&gt;: $O(1)$ 시간에 계산 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결정적&lt;/b&gt;: 같은 키는 항상 같은 값 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반적인 해시 함수들&lt;/h3&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def division_hash(key, table_size):
    &quot;&quot;&quot;나눗셈 해시&quot;&quot;&quot;
    return key % table_size

def multiplication_hash(key, table_size):
    &quot;&quot;&quot;곱셈 해시&quot;&quot;&quot;
    A = 0.6180339887  # (&amp;radic;5 - 1) / 2
    return int(table_size * ((key * A) % 1))

def string_hash(s, table_size):
    &quot;&quot;&quot;문자열 해시 (djb2 알고리즘)&quot;&quot;&quot;
    hash_value = 5381
    for char in s:
        hash_value = ((hash_value &amp;lt;&amp;lt; 5) + hash_value) + ord(char)
    return hash_value % table_size

def polynomial_hash(s, table_size):
    &quot;&quot;&quot;다항식 해시&quot;&quot;&quot;
    p = 31  # 소수
    hash_value = 0
    p_pow = 1
    for char in s:
        hash_value = (hash_value + ord(char) * p_pow) % table_size
        p_pow = (p_pow * p) % table_size
    return hash_value&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 영상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블의 동작 &amp;mdash; 키-밸류 저장, 해시 충돌, 체이닝&amp;middot;선형 탐사, 리사이징 &amp;mdash; 은 그림으로 보면 한 번에 잡힙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ZBu_slSH5Sk&quot;&gt;맵(Map)과 해시 테이블(Hash Table) 핵심만 모아보기 (YouTube)&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=ZBu_slSH5Sk&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/tRkme/dJMb9hC7rds/QKNetLoWeGH47bA88mQyUk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;BJ.23 맵(map)과 해시 테이블(hash table) 혹은 해시 맵(hash map)의 핵심만 모아보기! 20분간 아주아주아&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/ZBu_slSH5Sk&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;충돌 해결 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 개별 체이닝 (Separate Chaining)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hash_table_chaining.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGuLcw/dJMcajvz4xS/C3rD53JC3NTcsAKP5I9oNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGuLcw/dJMcajvz4xS/C3rD53JC3NTcsAKP5I9oNK/img.png&quot; data-alt=&quot;Hash table chaining&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGuLcw/dJMcajvz4xS/C3rD53JC3NTcsAKP5I9oNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGuLcw%2FdJMcajvz4xS%2FC3rD53JC3NTcsAKP5I9oNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;720&quot; data-filename=&quot;hash_table_chaining.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Hash table chaining&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 인덱스로 해시되는 키들을 연결 리스트로 묶어 한 버킷에 함께 저장합니다. 충돌이 발생해도 테이블 외부에 데이터를 매달 수 있어 로드 팩터가 1을 넘어도 동작하지만, 한 버킷의 리스트가 길어지면 검색 시간이 그만큼 늘어나는 것이 단점입니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class Node:
    &quot;&quot;&quot;체이닝용 연결 리스트 노드&quot;&quot;&quot;
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.next = None
        
class HashTableChaining:
    def __init__(self, size=10):
        self.size = size
        self.table = [None] * size  # 각 슬롯이 연결 리스트의 head
        self.count = 0
        
    def _hash(self, key):
        &quot;&quot;&quot;해시 함수&quot;&quot;&quot;
        if isinstance(key, str):
            return sum(ord(c) for c in key) % self.size
        return key % self.size
        
    def put(self, key, value):
        &quot;&quot;&quot;키-값 쌍 삽입&quot;&quot;&quot;
        index = self._hash(key)
        node = self.table[index]
        
        # 기존 키가 있는지 확인
        while node is not None:
            if node.key == key:
                node.value = value  # 값 업데이트
                return
            node = node.next
            
        # 새 노드를 리스트 맨 앞에 삽입 (head insertion)
        new_node = Node(key, value)
        new_node.next = self.table[index]
        self.table[index] = new_node
        self.count += 1
        
    def get(self, key):
        &quot;&quot;&quot;값 조회&quot;&quot;&quot;
        index = self._hash(key)
        node = self.table[index]
        while node is not None:
            if node.key == key:
                return node.value
            node = node.next
        raise KeyError(f&quot;Key '{key}' not found&quot;)
        
    def delete(self, key):
        &quot;&quot;&quot;키-값 쌍 삭제&quot;&quot;&quot;
        index = self._hash(key)
        node = self.table[index]
        prev = None
        
        while node is not None:
            if node.key == key:
                if prev is None:
                    self.table[index] = node.next  # 첫 번째 노드 삭제
                else:
                    prev.next = node.next
                self.count -= 1
                return node.value
            prev = node
            node = node.next
            
        raise KeyError(f&quot;Key '{key}' not found&quot;)
        
    def load_factor(self):
        &quot;&quot;&quot;로드 팩터 계산&quot;&quot;&quot;
        return self.count / self.size
        
    def display(self):
        &quot;&quot;&quot;테이블 출력&quot;&quot;&quot;
        for i in range(self.size):
            node = self.table[i]
            if node is not None:
                items = []
                while node is not None:
                    items.append(f&quot;({node.key}, {node.value})&quot;)
                    node = node.next
                print(f&quot;Bucket {i}: {' -&amp;gt; '.join(items)}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 개방 주소법 (Open Addressing)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌이 발생하면 같은 테이블 안에서 다른 빈 슬롯을 찾아 거기에 저장합니다. 외부 자료구조 없이 배열만으로 동작하지만, 로드 팩터가 1을 넘을 수 없고 데이터가 특정 영역에 뭉치는 클러스터링 문제가 생깁니다. 빈 슬롯을 찾아가는 방식(탐사 함수)에 따라 선형 탐사&amp;middot;이차 탐사&amp;middot;더블 해싱으로 나뉩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선형 탐사 (Linear Probing)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hash_table_linear_probing.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bztGMF/dJMcagZTHF1/rjX4eojmXmV1k7vEtGZXtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bztGMF/dJMcagZTHF1/rjX4eojmXmV1k7vEtGZXtK/img.png&quot; data-alt=&quot;Hash table linear probing&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bztGMF/dJMcagZTHF1/rjX4eojmXmV1k7vEtGZXtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbztGMF%2FdJMcagZTHF1%2FrjX4eojmXmV1k7vEtGZXtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;640&quot; data-filename=&quot;hash_table_linear_probing.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Hash table linear probing&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌이 일어난 인덱스의 바로 다음 칸(&lt;code&gt;+1&lt;/code&gt;)부터 차례로 빈 슬롯을 찾습니다. 구현이 가장 간단하지만, 충돌이 모인 위치 주변에 데이터가 길게 늘어붙는 1차 클러스터링이 발생해 점점 검색이 느려집니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class HashTableLinearProbing:
    def __init__(self, size=10):
        self.size = size
        self.keys = [None] * size
        self.values = [None] * size
        self.count = 0

    def _hash(self, key):
        if isinstance(key, str):
            return sum(ord(c) for c in key) % self.size
        return key % self.size

    def _probe(self, key):
        &quot;&quot;&quot;선형 탐사로 빈 슬롯 또는 키 위치 찾기&quot;&quot;&quot;
        index = self._hash(key)
        original_index = index
        while self.keys[index] is not None:
            if self.keys[index] == key:
                return index
            index = (index + 1) % self.size

            # 테이블이 가득 찬 경우
            if index == original_index:
                raise Exception(&quot;Hash table is full&quot;)
        return index

    def put(self, key, value):
        if self.count &amp;gt;= self.size:
            raise Exception(&quot;Hash table is full&quot;)
        index = self._probe(key)

        # 새로운 키인 경우
        if self.keys[index] is None:
            self.count += 1
        self.keys[index] = key
        self.values[index] = value

    def get(self, key):
        index = self._hash(key)
        original_index = index
        while self.keys[index] is not None:
            if self.keys[index] == key:
                return self.values[index]
            index = (index + 1) % self.size
            if index == original_index:
                break
        raise KeyError(f&quot;Key '{key}' not found&quot;)

    def delete(self, key):
        index = self._hash(key)
        original_index = index
        while self.keys[index] is not None:
            if self.keys[index] == key:
                value = self.values[index]
                self.keys[index] = None
                self.values[index] = None
                self.count -= 1

                # 삭제 후 재해싱 (클러스터링 방지)
                self._rehash_after_delete(index)
                return value
            index = (index + 1) % self.size
            if index == original_index:
                break
        raise KeyError(f&quot;Key '{key}' not found&quot;)

    def _rehash_after_delete(self, deleted_index):
        &quot;&quot;&quot;삭제 후 클러스터링 해결을 위한 재해싱&quot;&quot;&quot;
        index = (deleted_index + 1) % self.size
        while self.keys[index] is not None:
            key_to_rehash = self.keys[index]
            value_to_rehash = self.values[index]
            self.keys[index] = None
            self.values[index] = None
            self.count -= 1
            self.put(key_to_rehash, value_to_rehash)
            index = (index + 1) % self.size&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이차 탐사 (Quadratic Probing)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌 시 &lt;code&gt;+1, +4, +9, +16...&lt;/code&gt; 처럼 제곱수만큼 떨어진 위치를 탐사합니다. 선형 탐사의 1차 클러스터링은 완화되지만, 같은 인덱스로 해시된 키들은 같은 경로를 따라가는 2차 클러스터링이 남습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class HashTableQuadraticProbing:
    def __init__(self, size=10):
        self.size = size
        self.keys = [None] * size
        self.values = [None] * size
        self.count = 0

    def _hash(self, key):
        if isinstance(key, str):
            return sum(ord(c) for c in key) % self.size
        return key % self.size

    def _probe(self, key):
        &quot;&quot;&quot;이차 탐사로 빈 슬롯 또는 키 위치 찾기&quot;&quot;&quot;
        index = self._hash(key)
        original_index = index
        i = 0
        while self.keys[index] is not None:
            if self.keys[index] == key:
                return index
            i += 1
            index = (original_index + i * i) % self.size

            # 무한 루프 방지
            if i &amp;gt;= self.size:
                raise Exception(&quot;Hash table is full or cannot find slot&quot;)
        return index

    def put(self, key, value):
        if self.count &amp;gt;= self.size:
            raise Exception(&quot;Hash table is full&quot;)
        index = self._probe(key)
        if self.keys[index] is None:
            self.count += 1
        self.keys[index] = key
        self.values[index] = value&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;더블 해싱 (Double Hashing)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hash_table_double_hashing.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rEpzJ/dJMcahdt43P/CVFVO08PqMZl8nlKs2l7uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rEpzJ/dJMcahdt43P/CVFVO08PqMZl8nlKs2l7uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rEpzJ/dJMcahdt43P/CVFVO08PqMZl8nlKs2l7uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrEpzJ%2FdJMcahdt43P%2FCVFVO08PqMZl8nlKs2l7uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;640&quot; data-filename=&quot;hash_table_double_hashing.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 해시 함수의 결과만큼 점프하면서 탐사합니다. 키마다 점프 폭이 달라지기 때문에 1차&amp;middot;2차 클러스터링이 모두 줄어드는 대신, 해시 함수를 두 번 계산해야 하는 비용이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class HashTableDoubleHashing:
    def __init__(self, size=10):
        self.size = size
        self.keys = [None] * size
        self.values = [None] * size
        self.count = 0

    def _hash1(self, key):
        &quot;&quot;&quot;첫 번째 해시 함수&quot;&quot;&quot;
        if isinstance(key, str):
            return sum(ord(c) for c in key) % self.size
        return key % self.size

    def _hash2(self, key):
        &quot;&quot;&quot;두 번째 해시 함수&quot;&quot;&quot;
        if isinstance(key, str):
            hash_val = sum(ord(c) for c in key)
        else:
            hash_val = key
        return 7 - (hash_val % 7)  # 7은 테이블 크기보다 작은 소수

    def _probe(self, key):
        &quot;&quot;&quot;더블 해싱으로 빈 슬롯 또는 키 위치 찾기&quot;&quot;&quot;
        index = self._hash1(key)
        step = self._hash2(key)
        original_index = index
        while self.keys[index] is not None:
            if self.keys[index] == key:
                return index
            index = (index + step) % self.size
            if index == original_index:
                raise Exception(&quot;Hash table is full&quot;)
        return index

    def put(self, key, value):
        if self.count &amp;gt;= self.size:
            raise Exception(&quot;Hash table is full&quot;)

        index = self._probe(key)
        if self.keys[index] is None:
            self.count += 1
        self.keys[index] = key
        self.values[index] = value&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동적 리사이징&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hash_table_resize.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5CDHG/dJMcabqOpsE/OKOn22Bxk3oetVSEEDXuPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5CDHG/dJMcabqOpsE/OKOn22Bxk3oetVSEEDXuPK/img.png&quot; data-alt=&quot;Hash table resize&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5CDHG/dJMcabqOpsE/OKOn22Bxk3oetVSEEDXuPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5CDHG%2FdJMcabqOpsE%2FOKOn22Bxk3oetVSEEDXuPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;760&quot; data-filename=&quot;hash_table_resize.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Hash table resize&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 일정 비율 이상 차면(보통 로드 팩터 0.75 전후) 테이블 크기를 두 배로 늘리고 모든 원소를 새 크기에 맞춰 다시 해시합니다. 테이블이 좁아져서 충돌이 잦아지는 문제를 근본적으로 해결하는 방법이며, 충돌 해결 방식과 무관하게 함께 쓰입니다. 줄이는 방향도 가능해서 데이터가 빠진 뒤 일정 수준 이하가 되면 크기를 줄이기도 합니다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class ResizableHashTable:
    def __init__(self, initial_size=8):
        self.size = initial_size
        self.table = [[] for _ in range(self.size)]
        self.count = 0

    def _hash(self, key):
        if isinstance(key, str):
            return sum(ord(c) for c in key) % self.size
        return key % self.size

    def _resize(self, new_size):
        &quot;&quot;&quot;테이블 크기 조정&quot;&quot;&quot;
        old_table = self.table
        self.size = new_size
        self.table = [[] for _ in range(self.size)]
        old_count = self.count
        self.count = 0

        # 모든 원소를 새 테이블에 재삽입
        for bucket in old_table:
            for key, value in bucket:
                self.put(key, value)

    def put(self, key, value):
        # 로드 팩터가 0.75를 초과하면 크기 두 배로 증가
        if self.count / self.size &amp;gt; 0.75:
            self._resize(self.size * 2)
        index = self._hash(key)
        bucket = self.table[index]

        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)
                return
        bucket.append((key, value))
        self.count += 1

    def delete(self, key):
        index = self._hash(key)
        bucket = self.table[index]

        for i, (k, v) in enumerate(bucket):
            if k == key:
                del bucket[i]
                self.count -= 1
                # 로드 팩터가 0.25 미만이면 크기 절반으로 감소
                if self.count / self.size &amp;lt; 0.25 and self.size &amp;gt; 8:
                    self._resize(self.size // 2)
                return v
        raise KeyError(f&quot;Key '{key}' not found&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시간복잡도&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;연산&lt;/th&gt;
&lt;th&gt;평균&lt;/th&gt;
&lt;th&gt;최악&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;삽입&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삭제&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검색&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 단어 빈도 계산&lt;/h3&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def word_frequency(text):
    word_count = {}
    words = text.lower().split()
    for word in words:
        word_count[word] = word_count.get(word, 0) + 1
    return word_count

text = &quot;hello world hello python world&quot;
print(word_frequency(text))  # {'hello': 2, 'world': 2, 'python': 1}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 두 수의 합 찾기&lt;/h3&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def two_sum(nums, target):
    &quot;&quot;&quot;두 수의 합이 target인 인덱스 찾기&quot;&quot;&quot;
    num_to_index = {}
    for i, num in enumerate(nums):
        complement = target - num
        if complement in num_to_index:
            return [num_to_index[complement], i]
        num_to_index[num] = i
    return []

nums = [2, 7, 11, 15]
target = 9
print(two_sum(nums, target))  # [0, 1]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 그룹 애너그램&lt;/h3&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def group_anagrams(strs):
    &quot;&quot;&quot;애너그램끼리 그룹화&quot;&quot;&quot;
    anagram_groups = {}

    for s in strs:
        # 정렬된 문자열을 키로 사용
        key = ''.join(sorted(s))
        if key not in anagram_groups:
            anagram_groups[key] = []
        anagram_groups[key].append(s)

    return list(anagram_groups.values())

strs = [&quot;eat&quot;, &quot;tea&quot;, &quot;tan&quot;, &quot;ate&quot;, &quot;nat&quot;, &quot;bat&quot;]
print(group_anagrams(strs))  # [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. LRU 캐시 (해시 테이블 + 연결 리스트)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hash_table_lru_cache.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m9BQs/dJMcaf0ZLAO/hxCJma8iWDvTKBimp9L2V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m9BQs/dJMcaf0ZLAO/hxCJma8iWDvTKBimp9L2V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m9BQs/dJMcaf0ZLAO/hxCJma8iWDvTKBimp9L2V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm9BQs%2FdJMcaf0ZLAO%2FhxCJma8iWDvTKBimp9L2V0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;800&quot; data-filename=&quot;hash_table_lru_cache.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;class Node:
    &quot;&quot;&quot;이중 연결 리스트 노드&quot;&quot;&quot;
    def __init__(self, key=None, value=None):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None
        
class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = {}  # 해시 테이블: key -&amp;gt; Node
        
        # dummy head/tail로 경계 조건 단순화
        self.head = Node()
        self.tail = Node()
        self.head.next = self.tail
        self.tail.prev = self.head
        
    def _add_to_front(self, node):
        &quot;&quot;&quot;노드를 head 직후에 삽입 (가장 최근)&quot;&quot;&quot;
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node
        
    def _remove(self, node):
        &quot;&quot;&quot;노드를 리스트에서 제거&quot;&quot;&quot;
        node.prev.next = node.next
        node.next.prev = node.prev
        
    def get(self, key):
        if key not in self.cache:
            return -1
        node = self.cache[key]
        # 최근 접근이므로 맨 앞으로 이동
        self._remove(node)
        self._add_to_front(node)
        return node.value
        
    def put(self, key, value):
        if key in self.cache:
            # 기존 키: 값 업데이트 + 맨 앞으로
            node = self.cache[key]
            node.value = value
            self._remove(node)
            self._add_to_front(node)
        else:
            # 새 키
            if len(self.cache) &amp;gt;= self.capacity:
                # 용량 초과: tail 직전 노드 (가장 오래된 것) 제거
                lru = self.tail.prev
                self._remove(lru)
                del self.cache[lru.key]
            new_node = Node(key, value)
            self.cache[key] = new_node
            self._add_to_front(new_node)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해시 테이블 vs 다른 자료구조&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;자료구조&lt;/th&gt;
&lt;th&gt;검색 시간&lt;/th&gt;
&lt;th&gt;삽입 시간&lt;/th&gt;
&lt;th&gt;삭제 시간&lt;/th&gt;
&lt;th&gt;메모리 사용량&lt;/th&gt;
&lt;th&gt;순서 유지&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;해시 테이블&lt;/td&gt;
&lt;td&gt;$O(1)$ avg&lt;/td&gt;
&lt;td&gt;$O(1)$ avg&lt;/td&gt;
&lt;td&gt;$O(1)$ avg&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배열&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;연결 리스트&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이진 탐색 트리&lt;/td&gt;
&lt;td&gt;$O(\log n)$&lt;/td&gt;
&lt;td&gt;$O(\log n)$&lt;/td&gt;
&lt;td&gt;$O(\log n)$&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장단점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 연산: 평균 $O(1)$ 시간복잡도&lt;/li&gt;
&lt;li&gt;유연한 키: 다양한 데이터 타입을 키로 사용 가능&lt;/li&gt;
&lt;li&gt;동적 크기: 필요에 따라 크기 조정 가능&lt;/li&gt;
&lt;li&gt;메모리 효율적: 필요한 만큼만 공간 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순서 없음: 삽입 순서나 정렬 순서 보장 안됨&lt;/li&gt;
&lt;li&gt;해시 충돌: 성능 저하 원인&lt;/li&gt;
&lt;li&gt;메모리 오버헤드: 추가 공간 필요&lt;/li&gt;
&lt;li&gt;최악의 경우: $O(n)$ 시간복잡도 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 활용 분야&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스: 인덱싱, 조인 연산&lt;/li&gt;
&lt;li&gt;캐싱: 웹 캐시, CPU 캐시&lt;/li&gt;
&lt;li&gt;컴파일러: 심볼 테이블, 변수 관리&lt;/li&gt;
&lt;li&gt;네트워크: 라우팅 테이블, DNS&lt;/li&gt;
&lt;li&gt;보안: 패스워드 저장, 체크섬&lt;/li&gt;
&lt;li&gt;빅데이터: 분산 해싱, 샤딩&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;Map과 해시 테이블의 관계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map은 ADT(Abstract Data Type)다. 키-값 매핑이라는 동작 인터페이스만 정의할 뿐, 어떻게 구현하는지는 명시하지 않는다. 해시 테이블은 그 Map을 구현한 데이터 구조이며, 트리 기반(Red-Black Tree 같은) 방식으로도 Map을 구현할 수 있다. 해시 테이블이 사실상 표준처럼 쓰이는 이유는 평균 O(1) 접근 성능이다. 다만 충돌 해결 방식은 언어마다 다르다 &amp;mdash; Python의 &lt;code&gt;dict&lt;/code&gt;는 개방 주소법, Java의 &lt;code&gt;HashMap&lt;/code&gt;은 체이닝을 쓴다 (JDK 8부터는 한 버킷의 노드가 일정 수를 넘으면 Red-Black Tree로 전환한다).&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>DSA</category>
      <category>datastructure</category>
      <category>Hashtable</category>
      <category>map</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/95</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EB%A7%B5Map%EA%B3%BC-%ED%95%B4%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94Hash-Table#entry95comment</comments>
      <pubDate>Mon, 25 May 2026 19:03:58 +0900</pubDate>
    </item>
    <item>
      <title>우선순위 큐(Priority Queue)와 힙(Heap)</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90Priority-Queue%EC%99%80-%ED%9E%99Heap</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위 큐는 각 원소가 &lt;b&gt;우선순위&lt;/b&gt;를 가지며, 높은 우선순위를 가진 원소가 먼저 처리되는 추상 자료형입니다. FIFO로 동작하는 일반 큐와 달리, 들어온 순서가 아니라 원소의 우선순위에 따라 꺼내는 점이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 힙(Heap)이라는 자료구조로 구현하며, 운영체제의 프로세스 스케줄링, 다익스트라 같은 그래프 알고리즘, top-K 문제 등에서 핵심 역할을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;heap_structure.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqzwVc/dJMcaa6v3eG/roju67zhFc6V5LW3qwnGK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqzwVc/dJMcaa6v3eG/roju67zhFc6V5LW3qwnGK1/img.png&quot; data-alt=&quot;힙은 트리지만 배열로 저장된다 &amp;amp;mdash; 인덱스 공식으로 부모&amp;amp;middot;자식 위치 계산&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqzwVc/dJMcaa6v3eG/roju67zhFc6V5LW3qwnGK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqzwVc%2FdJMcaa6v3eG%2Froju67zhFc6V5LW3qwnGK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;840&quot; data-filename=&quot;heap_structure.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;힙은 트리지만 배열로 저장된다 &amp;mdash; 인덱스 공식으로 부모&amp;middot;자식 위치 계산&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 개념&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;priority_queue_vs_fifo.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mEaO5/dJMcaf0ZMoR/mxA6u2zgSg6lkaPqP4Wgyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mEaO5/dJMcaf0ZMoR/mxA6u2zgSg6lkaPqP4Wgyk/img.png&quot; data-alt=&quot;FIFO 큐와 우선순위 큐의 출력 순서 차이 &amp;amp;mdash; 같은 입력에 다른 출력&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mEaO5/dJMcaf0ZMoR/mxA6u2zgSg6lkaPqP4Wgyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmEaO5%2FdJMcaf0ZMoR%2FmxA6u2zgSg6lkaPqP4Wgyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;640&quot; data-filename=&quot;priority_queue_vs_fifo.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FIFO 큐와 우선순위 큐의 출력 순서 차이 &amp;mdash; 같은 입력에 다른 출력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;우선순위 기반&lt;/b&gt;: 삽입 순서가 아닌 우선순위에 따라 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동적 정렬&lt;/b&gt;: 삽입과 삭제 시 우선순위 순서 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;힙 구조&lt;/b&gt;: 일반적으로 힙(Heap)으로 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;완전 이진 트리&lt;/b&gt;: 힙의 특성을 이용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 연산&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;insert(item, priority)&lt;/b&gt;: 우선순위와 함께 원소 삽입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;extract_max/min()&lt;/b&gt;: 최고/최저 우선순위 원소 제거 및 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;peek()&lt;/b&gt;: 최고 우선순위 원소 조회 (제거하지 않음)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;is_empty()&lt;/b&gt;: 큐가 비어있는지 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;size()&lt;/b&gt;: 큐의 크기 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 영상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=P-FTb1faxlo&quot;&gt;우선순위 큐와 힙의 개념과 차이, 사용 사례 (YouTube)&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=P-FTb1faxlo&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/c38aiR/dJMb9fZByun/iQ0Nb4WqNOIWA3TD1Eikm1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/62NJu/dJMb9kmiS1t/IVNIgLu1iIUgYzQa0PTgs0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/eixWa8/dJMb9jOs8xG/ekiw2E0opUcm0Iez8S0coK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;BJ.12 우선순위 큐와 힙의 개념과 차이, 사용 사례를 설명합니다! 힙이 어떻게 동작하는지도 예를 &quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/P-FTb1faxlo&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 파이썬 heapq 모듈 사용&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;import heapq

class PriorityQueue:
    def __init__(self):
        self.heap = []

    def insert(self, item, priority):
        # 파이썬 heapq는 최소 힙이므로 음수로 변환 (최대 힙 구현 시)
        heapq.heappush(self.heap, (priority, item))

    def extract_min(self):
        if self.is_empty():
            raise IndexError(&quot;Priority queue is empty&quot;)
        return heapq.heappop(self.heap)[1]

    def peek(self):
        if self.is_empty():
            raise IndexError(&quot;Priority queue is empty&quot;)
        return self.heap[0][1]

    def is_empty(self):
        return len(self.heap) == 0

    def size(self):
        return len(self.heap)

# 사용 예시
pq = PriorityQueue()
pq.insert(&quot;task1&quot;, 3)
pq.insert(&quot;task2&quot;, 1)
pq.insert(&quot;task3&quot;, 2)

print(pq.extract_min())  # &quot;task2&quot; (우선순위 1)
print(pq.extract_min())  # &quot;task3&quot; (우선순위 2)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 최대 힙 구현&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;import heapq

class MaxPriorityQueue:
    def __init__(self):
        self.heap = []

    def insert(self, item, priority):
        # 우선순위를 음수로 만들어 최대 힙 구현
        heapq.heappush(self.heap, (-priority, item))

    def extract_max(self):
        if self.is_empty():
            raise IndexError(&quot;Priority queue is empty&quot;)
        return heapq.heappop(self.heap)[1]

    def peek(self):
        if self.is_empty():
            raise IndexError(&quot;Priority queue is empty&quot;)
        return self.heap[0][1]

    def is_empty(self):
        return len(self.heap) == 0&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 클래스 기반 우선순위 큐&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;import heapq

class Task:
    def __init__(self, name, priority):
        self.name = name
        self.priority = priority

    def __lt__(self, other):
        return self.priority &amp;lt; other.priority

    def __repr__(self):
        return f&quot;Task({self.name}, {self.priority})&quot;

class PriorityQueue:
    def __init__(self):
        self.heap = []

    def insert(self, task):
        heapq.heappush(self.heap, task)

    def extract_min(self):
        if self.is_empty():
            raise IndexError(&quot;Priority queue is empty&quot;)
        return heapq.heappop(self.heap)

    def is_empty(self):
        return len(self.heap) == 0

# 사용 예시
pq = PriorityQueue()
pq.insert(Task(&quot;urgent&quot;, 1))
pq.insert(Task(&quot;normal&quot;, 5))
pq.insert(Task(&quot;high&quot;, 2))

print(pq.extract_min())  # Task(urgent, 1)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 직접 힙 구현&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QEzmX/dJMcaftbelZ/vmr580wOaDfAJgV8pGMHwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QEzmX/dJMcaftbelZ/vmr580wOaDfAJgV8pGMHwk/img.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot; data-is-animation=&quot;false&quot; data-filename=&quot;heap_heappush.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QEzmX/dJMcaftbelZ/vmr580wOaDfAJgV8pGMHwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQEzmX%2FdJMcaftbelZ%2Fvmr580wOaDfAJgV8pGMHwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dB3FLL/dJMcabYEBG1/7it9Qq4AloDvd9sLC3Py6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dB3FLL/dJMcabYEBG1/7it9Qq4AloDvd9sLC3Py6k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot; data-filename=&quot;heap_heappop.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dB3FLL/dJMcabYEBG1/7it9Qq4AloDvd9sLC3Py6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdB3FLL%2FdJMcabYEBG1%2F7it9Qq4AloDvd9sLC3Py6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;heappush &amp;mdash; 새 원소를 끝에 추가한 뒤 sift-up으로 적절한 위치까지 올라간다, heappop &amp;mdash; root를 추출하고 마지막 원소를 root로 옮긴 뒤 sift-down으로 내려간다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;class MaxHeap:
    def __init__(self):
        self.heap = []

    def parent(self, i):
        return (i - 1) // 2

    def left_child(self, i):
        return 2 * i + 1

    def right_child(self, i):
        return 2 * i + 2

    def insert(self, key):
        self.heap.append(key)
        self._heapify_up(len(self.heap) - 1)

    def extract_max(self):
        if not self.heap:
            raise IndexError(&quot;Heap is empty&quot;)
        max_val = self.heap[0]
        self.heap[0] = self.heap[-1]
        self.heap.pop()
        if self.heap:
            self._heapify_down(0)
        return max_val

    def _heapify_up(self, i):
        if i &amp;gt; 0 and self.heap[i] &amp;gt; self.heap[self.parent(i)]:
            self.heap[i], self.heap[self.parent(i)] = self.heap[self.parent(i)], self.heap[i]
            self._heapify_up(self.parent(i))

    def _heapify_down(self, i):
        largest = i
        left = self.left_child(i)
        right = self.right_child(i)
        if left &amp;lt; len(self.heap) and self.heap[left] &amp;gt; self.heap[largest]:
            largest = left
        if right &amp;lt; len(self.heap) and self.heap[right] &amp;gt; self.heap[largest]:
            largest = right
        if largest != i:
            self.heap[i], self.heap[largest] = self.heap[largest], self.heap[i]
            self._heapify_down(largest)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시간복잡도&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삽입 (insert): $O(\log n)$&lt;/li&gt;
&lt;li&gt;삭제 (extract): $O(\log n)$&lt;/li&gt;
&lt;li&gt;조회 (peek): $O(1)$&lt;/li&gt;
&lt;li&gt;힙 구성: $O(n \log n)$ 또는 $O(n)$ (heapify 사용 시)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;활용 예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 다익스트라 알고리즘&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;import heapq

def dijkstra(graph, start):
    distances = {node: float('inf') for node in graph}
    distances[start] = 0
    pq = [(0, start)]

    while pq:
        current_distance, current_node = heapq.heappop(pq)

        if current_distance &amp;gt; distances[current_node]:
            continue

        for neighbor, weight in graph[current_node].items():
            distance = current_distance + weight

            if distance &amp;lt; distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor))

    return distances&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 작업 스케줄링&lt;/h3&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;import heapq
from datetime import datetime, timedelta

class Job:
    def __init__(self, name, priority, deadline):
        self.name = name
        self.priority = priority
        self.deadline = deadline

    def __lt__(self, other):
        # 우선순위가 높을수록 먼저 (숫자가 작을수록 높은 우선순위)
        if self.priority != other.priority:
            return self.priority &amp;lt; other.priority
        # 우선순위가 같으면 마감일이 빠른 것부터
        return self.deadline &amp;lt; other.deadline

    def __repr__(self):
        return f&quot;Job({self.name}, priority={self.priority})&quot;

class JobScheduler:
    def __init__(self):
        self.jobs = []

    def add_job(self, job):
        heapq.heappush(self.jobs, job)

    def get_next_job(self):
        if self.jobs:
            return heapq.heappop(self.jobs)
        return None

# 사용 예시
scheduler = JobScheduler()
scheduler.add_job(Job(&quot;backup&quot;, 3, datetime.now() + timedelta(hours=2)))
scheduler.add_job(Job(&quot;urgent_fix&quot;, 1, datetime.now() + timedelta(minutes=30)))
scheduler.add_job(Job(&quot;maintenance&quot;, 2, datetime.now() + timedelta(hours=1)))
print(scheduler.get_next_job())  # Job(urgent_fix, priority=1)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 병합 정렬된 배열들&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;import heapq

def merge_k_sorted_arrays(arrays):
    heap = []
    result = []

    # 각 배열의 첫 번째 원소를 힙에 삽입
    for i, arr in enumerate(arrays):
        if arr:
            heapq.heappush(heap, (arr[0], i, 0))

    while heap:
        value, array_idx, element_idx = heapq.heappop(heap)
        result.append(value)

        # 다음 원소가 있으면 힙에 추가
        if element_idx + 1 &amp;lt; len(arrays[array_idx]):
            next_value = arrays[array_idx][element_idx + 1]
            heapq.heappush(heap, (next_value, array_idx, element_idx + 1))

    return result

# 사용 예시
arrays = [
    [1, 4, 7],
    [2, 5, 8],
    [3, 6, 9]
]

print(merge_k_sorted_arrays(arrays))  # [1, 2, 3, 4, 5, 6, 7, 8, 9]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 상위 K개 원소 찾기&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import heapq

def find_k_largest(nums, k):
    # 최소 힙을 사용하여 크기 k 유지
    heap = []

    for num in nums:
        if len(heap) &amp;lt; k:
            heapq.heappush(heap, num)
        elif num &amp;gt; heap[0]:
            heapq.heapreplace(heap, num)

    return sorted(heap, reverse=True)

def find_k_smallest(nums, k):
    return heapq.nsmallest(k, nums)

# 사용 예시
nums = [3, 2, 1, 5, 6, 4]
print(find_k_largest(nums, 2))   # [6, 5]
print(find_k_smallest(nums, 2))  # [1, 2]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현 방식 비교&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구현 방식&lt;/th&gt;
&lt;th&gt;삽입&lt;/th&gt;
&lt;th&gt;삭제&lt;/th&gt;
&lt;th&gt;조회&lt;/th&gt;
&lt;th&gt;공간복잡도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;정렬된 배열&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정렬되지 않은 배열&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;연결 리스트&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이진 힙&lt;/td&gt;
&lt;td&gt;$O(log n)$&lt;/td&gt;
&lt;td&gt;$O(log n)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이진 탐색 트리&lt;/td&gt;
&lt;td&gt;$O(log n)$&lt;/td&gt;
&lt;td&gt;$O(log n)$&lt;/td&gt;
&lt;td&gt;$O(log n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;힙의 종류&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이진 힙 (Binary Heap): 가장 일반적, 완전 이진 트리&lt;/li&gt;
&lt;li&gt;피보나치 힙 (Fibonacci Heap): 상수 시간 삽입, 병합&lt;/li&gt;
&lt;li&gt;이항 힙 (Binomial Heap): 병합 연산에 최적화&lt;/li&gt;
&lt;li&gt;d-ary 힙: 각 노드가 d개의 자식을 가짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 활용 분야&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운영체제: 프로세스 스케줄링&lt;/li&gt;
&lt;li&gt;네트워크: 패킷 라우팅, QoS&lt;/li&gt;
&lt;li&gt;게임: A* 알고리즘, 이벤트 시뮬레이션&lt;/li&gt;
&lt;li&gt;그래프 알고리즘: 다익스트라, 프림 알고리즘&lt;/li&gt;
&lt;li&gt;데이터 압축: 허프만 코딩&lt;/li&gt;
&lt;li&gt;실시간 시스템: 태스크 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장단점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우선순위 기반 효율적 처리&lt;/li&gt;
&lt;li&gt;동적 정렬 유지&lt;/li&gt;
&lt;li&gt;다양한 알고리즘의 핵심 구조&lt;/li&gt;
&lt;li&gt;실시간 시스템에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반 큐보다 복잡한 구현&lt;/li&gt;
&lt;li&gt;추가 메모리 오버헤드&lt;/li&gt;
&lt;li&gt;우선순위 변경이 어려움&lt;/li&gt;
&lt;li&gt;순차 접근 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;우선순위 큐와 힙의 관계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위 큐는 ADT(Abstract Data Type)다. 즉 &quot;어떤 동작들이 있는지&quot;만 정의할 뿐 구현은 명시하지 않는다. 반면 힙은 자료 구조다 &amp;mdash; 구현까지 정해져 있다. 우선순위 큐를 구현하는 방법은 힙 외에도 정렬된 배열, 연결 리스트, 이진 탐색 트리 등 여러 가지가 있지만, 힙이 성능 균형이 가장 좋아 사실상 표준처럼 쓰인다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>DSA</category>
      <category>datastructure</category>
      <category>heap</category>
      <category>PriorityQueue</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/94</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90Priority-Queue%EC%99%80-%ED%9E%99Heap#entry94comment</comments>
      <pubDate>Sun, 24 May 2026 22:36:38 +0900</pubDate>
    </item>
    <item>
      <title>정렬(Sorting) 알고리즘 6가지 &amp;mdash; 원리&amp;middot;복잡도&amp;middot;선택 가이드</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%A0%95%EB%A0%ACSorting-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-6%EA%B0%80%EC%A7%80-%E2%80%94-%EC%9B%90%EB%A6%AC%C2%B7%EB%B3%B5%EC%9E%A1%EB%8F%84%C2%B7%EC%84%A0%ED%83%9D-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;정렬은 컴퓨터 과학에서 가장 기본적이면서도 가장 자주 다루는 문제다. 탐색&amp;middot;중복 제거&amp;middot;통계 처리 등 거의 모든 데이터 처리의 전처리 단계로 들어가기 때문에, 어떤 정렬을 언제 쓰는지 감을 잡아두는 것이 실전에서 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 정렬을 분류하는 3가지 기준을 짚고, 대표적인 6가지 정렬 알고리즘을 원리&amp;middot;복잡도&amp;middot;구현까지 살펴본다. 마지막에 상황별 선택 가이드까지 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정렬(Sorting)이란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 &lt;b&gt;특정 순서&lt;/b&gt;(오름차순, 내림차순)로 재배열하는 과정&lt;/li&gt;
&lt;li&gt;컴퓨터 과학에서 &lt;b&gt;가장 기본적이고 중요한&lt;/b&gt; 알고리즘 중 하나&lt;/li&gt;
&lt;li&gt;탐색, 데이터 처리의 &lt;b&gt;전처리 단계&lt;/b&gt;로 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정렬의 분류&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 안정성(Stability)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_stability.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GKD0N/dJMcaipUVXa/Fty2KUuLl9N3hTpSO5cHn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GKD0N/dJMcaipUVXa/Fty2KUuLl9N3hTpSO5cHn0/img.png&quot; data-alt=&quot;정렬 안정성은 같은 키를 가진 원소의 상대 순서가 유지되는지로 구분&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GKD0N/dJMcaipUVXa/Fty2KUuLl9N3hTpSO5cHn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGKD0N%2FdJMcaipUVXa%2FFty2KUuLl9N3hTpSO5cHn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;640&quot; data-filename=&quot;sort_stability.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정렬 안정성은 같은 키를 가진 원소의 상대 순서가 유지되는지로 구분&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;안정(Stable)&lt;/b&gt;: 같은 값의 원소들의 &lt;b&gt;상대적 순서 유지&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불안정(Unstable)&lt;/b&gt;: 같은 값의 원소들의 순서가 바뀔 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 메모리 사용&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;제자리 정렬(In-place)&lt;/b&gt;: 추가 메모리 $O(1)$ 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 정렬(Out-of-place)&lt;/b&gt;: 추가 메모리 $O(n)$ 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 적응성(Adaptive)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;적응적&lt;/b&gt;: 이미 정렬된 데이터에 대해 성능 향상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비적응적&lt;/b&gt;: 입력 상태와 무관하게 일정한 성능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주요 정렬 알고리즘&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 버블 정렬(Bubble Sort)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_bubble.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GYNVM/dJMcadhQ8PM/ThPT2RnmEHpRPwg7e5mUeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GYNVM/dJMcadhQ8PM/ThPT2RnmEHpRPwg7e5mUeK/img.png&quot; data-alt=&quot;1 pass에서 가장 큰 값이 인접 swap을 통해 끝까지 이동&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GYNVM/dJMcadhQ8PM/ThPT2RnmEHpRPwg7e5mUeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGYNVM%2FdJMcadhQ8PM%2FThPT2RnmEHpRPwg7e5mUeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;720&quot; data-filename=&quot;sort_bubble.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;1 pass에서 가장 큰 값이 인접 swap을 통해 끝까지 이동&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인접한 두 원소를 &lt;b&gt;비교하고 교환&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;큰 값이 &lt;b&gt;거품처럼 뒤로 이동&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;: $O(n&amp;sup2;)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;: $O(1)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;: 안정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적응성&lt;/b&gt;: 적응적 (최선 $O(n)$)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 구현 간단, 안정 정렬&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 성능 매우 느림&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 버블 정렬 알고리즘 구현하기

from typing import MutableSequence

def bubble_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;버블 정렬&quot;&quot;&quot;
    n = len(a)
    for i in range(n - 1):
        for j in range(n - 1, i, -1):
            if a[j - 1] &amp;gt; a[j]:
                a[j - 1], a[j] = a[j], a[j - 1]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 버블 정렬 알고리즘 구현하기 (알고리즘의 개선 1)

from typing import MutableSequence

def bubble_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;버블 정렬(교환 횟수에 따른 중단)&quot;&quot;&quot;
    n = len(a)
    for i in range(n - 1):
        exchng = 0
        for j in range(n - 1, i, -1):
            if a[j - 1] &amp;gt; a[j]:
                a[j - 1], a[j] = a[j], a[j - 1]
                exchng += 1
        if exchng == 0:
            break&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 버블 정렬 알고리즘 구현하기 (알고리즘의 개선 2)

from typing import MutableSequence

def bubble_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;버블 정렬 (스캔 범위를 제한)&quot;&quot;&quot;
    n = len(a)
    k = 0
    while k &amp;lt; n - 1:
        last = n - 1
        for j in range(n - 1, k, -1):
            if a[j - 1] &amp;gt; a[j]:
                a[j - 1], a[j] = a[j], a[j - 1]
                last = j
        k = last&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 셰이커 정렬 알고리즘 구현하기

from typing import MutableSequence

def shaker_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;셰이커 정렬&quot;&quot;&quot;
    left = 0
    right = len(a) - 1
    last = right
    while left &amp;lt; right:
        for j in range(right, left, -1):
            if a[j - 1] &amp;gt; a[j]:
                a[j - 1], a[j] = a[j], a[j - 1]
                last = j
        left = last

        for j in range(left, right):
            if a[j] &amp;gt; a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]
                last = j
        right = last&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 선택 정렬(Selection Sort)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_selection.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsZZAn/dJMcabdc7Hd/SglLkhQTL8Czs2ZblU2kZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsZZAn/dJMcabdc7Hd/SglLkhQTL8Czs2ZblU2kZk/img.png&quot; data-alt=&quot;매 단계마다 미정렬 부분의 최솟값을 정렬 부분 끝으로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsZZAn/dJMcabdc7Hd/SglLkhQTL8Czs2ZblU2kZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsZZAn%2FdJMcabdc7Hd%2FSglLkhQTL8Czs2ZblU2kZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;840&quot; data-filename=&quot;sort_selection.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;매 단계마다 미정렬 부분의 최솟값을 정렬 부분 끝으로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매번 &lt;b&gt;최솟값을 선택&lt;/b&gt;해서 앞쪽에 배치&lt;/li&gt;
&lt;li&gt;정렬된 부분과 미정렬된 부분을 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;: $O(n&amp;sup2;)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;: $O(1)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;: 불안정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적응성&lt;/b&gt;: 비적응적&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 메모리 사용 최소, 교환 횟수 적음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 성능 느림, 불안정&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 단순 선택 정렬 알고리즘 구현하기

from typing import MutableSequence

def selection_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;단순 선택 정렬&quot;&quot;&quot;
    n = len(a)
    for i in range(n - 1):
        min = i
        for j in range(i + 1, n):
            if a[j] &amp;lt; a[min]:
                min = j
        a[i], a[min] = a[min], a[i]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 삽입 정렬(Insertion Sort)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_insertion.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6gAld/dJMcadB2P3N/mVaOUwJSCrNwum2xKKO1j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6gAld/dJMcadB2P3N/mVaOUwJSCrNwum2xKKO1j0/img.png&quot; data-alt=&quot;새 원소가 정렬된 부분의 적절한 자리에 끼워넣어진다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6gAld/dJMcadB2P3N/mVaOUwJSCrNwum2xKKO1j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6gAld%2FdJMcadB2P3N%2FmVaOUwJSCrNwum2xKKO1j0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;840&quot; data-filename=&quot;sort_insertion.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;새 원소가 정렬된 부분의 적절한 자리에 끼워넣어진다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 원소를 &lt;b&gt;정렬된 부분의 적절한 위치&lt;/b&gt;에 삽입&lt;/li&gt;
&lt;li&gt;카드 정렬과 유사한 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;: $O(n&amp;sup2;)$ / 최선 $O(n)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;: $O(1)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;: 안정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적응성&lt;/b&gt;: 적응적&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 거의 정렬된 배열에 효율적, 안정 정렬&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 큰 데이터에 비효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 단순 삽입 정렬 알고리즘 구현하기

from typing import MutableSequence

def insertion_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;단순 삽입 정렬&quot;&quot;&quot;
    n = len(a)
    for i in range(1, n):
        j = i
        tmp = a[i]
        while j &amp;gt; 0 and a[j - 1] &amp;gt; tmp:
            a[j] = a[j - 1]
            j -= 1
        a[j] = tmp&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 이진 삽입 정렬 알고리즘 구현하기

from typing import MutableSequence

def binary_insertion_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;이진 삽입 정렬&quot;&quot;&quot;
    n = len(a)
    for i in range(1, n):
        key = a[i]
        pl = 0
        pr = i - 1

        while True:
            pc = (pl + pr) // 2
            if a[pc] == key:
                break
            elif a[pc] &amp;lt; key:
                pl = pc + 1
            else:
                pr = pc - 1
            if pl &amp;gt; pr:
                break

        pd = pc + 1 if pl &amp;lt;= pr else pr + 1

        for j in range(i, pd, -1):
            a[j] = a[j - 1]
        a[pd] = key&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 이진 삽입 정렬 알고리즘 구현 (bisect.insort 사용)

from typing import MutableSequence
import bisect

def binary_insertion_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;이진 삽입 정렬 (bisect.insort 사용)&quot;&quot;&quot;
    for i in range(1, len(a)):
        bisect.insort(a, a.pop(i), 0, i)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 셸 정렬 알고리즘 구현하기

from typing import MutableSequence

def shell_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;셸 정렬&quot;&quot;&quot;
    n = len(a)
    h = n // 2
    while h &amp;gt; 0:
        for i in range(h, n):
            j = i - h
            tmp = a[i]
            while j &amp;gt;= 0 and a[j] &amp;gt; tmp:
                a[j + h] = a[j]
                j -= h
            a[j + h] = tmp
        h //= 2&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 셸 정렬 알고리즘 구현하기 (h * 3 + 1의 수열 사용)

from typing import MutableSequence

def shell_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;셸 정렬 (h * 3 + 1의 수열 사용)&quot;&quot;&quot;
    n = len(a)
    h = 1

    while h &amp;lt; n // 9:
        h = h * 3 + 1

    while h &amp;gt; 0:
        for i in range(h, n):
            j = i - h
            tmp = a[i]
            while j &amp;gt;= 0 and a[j] &amp;gt; tmp:
                a[j + h] = a[j]
                j -= h
            a[j + h] = tmp
        h //= 3&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;참고 영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=g06hNBhoS1k&quot;&gt;Shell sort vs Insertion sort&lt;/a&gt; &amp;mdash; 삽입 정렬에서 셸 정렬로 자연스럽게 확장되는 흐름을 시각화한 영상. gap 크기가 정렬 진행에 어떻게 작용하는지 한눈에 들어온다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 병합 정렬(Merge Sort)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_merge.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFLTPN/dJMcadWpTde/sV4dY41ANysAeDLqa2iTok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFLTPN/dJMcadWpTde/sV4dY41ANysAeDLqa2iTok/img.png&quot; data-alt=&quot;정렬 안정성은 같은 키를 가진 원소의 상대 순서가 유지되는지로 구분&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFLTPN/dJMcadWpTde/sV4dY41ANysAeDLqa2iTok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFLTPN%2FdJMcadWpTde%2FsV4dY41ANysAeDLqa2iTok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;880&quot; data-filename=&quot;sort_merge.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;정렬 안정성은 같은 키를 가진 원소의 상대 순서가 유지되는지로 구분&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분할 정복&lt;/b&gt; 기법 사용&lt;/li&gt;
&lt;li&gt;배열을 &lt;b&gt;반으로 나누고&lt;/b&gt; 각각 정렬 후 &lt;b&gt;병합&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;: $O(n \log n)$ &amp;mdash; 모든 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;: $O(n)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;: 안정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적응성&lt;/b&gt;: 비적응적&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 안정적인 $O(n \log n)$ 성능, 안정 정렬&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 추가 메모리 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 병합 정렬 알고리즘 구현하기

from typing import MutableSequence

def merge_sorted(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;병합 정렬&quot;&quot;&quot;


    def _merge_sort(a: MutableSequence, left: int, right: int) -&amp;gt; None:
        &quot;&quot;&quot;a[left] ~ a[right]를 재귀적으로 병합 정렬&quot;&quot;&quot;
        if left &amp;lt; right:
            center = (left + right) // 2

            _merge_sort(a, left, center)
            _merge_sort(a, center + 1, right)

            p = j = 0
            i = k = left

            while i &amp;lt;= center:
                buff[p] = a[i]
                p += 1
                i += 1

            while i &amp;lt;= right and j &amp;lt; p:
                if buff[j] &amp;lt;= a[i]:
                    a[k] = buff[j]
                    j += 1
                else:
                    a[k] = a[i]
                    i += 1
                k += 1

            while j &amp;lt; p:
                a[k] = buff[j]
                k += 1
                j += 1

    n = len(a)
    buff = [None] * n
    _merge_sort(a, 0, n - 1)
    del buff&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;참고 영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=es2T6KY45cA&quot;&gt;Merge Sort vs Quick Sort&lt;/a&gt; &amp;mdash; 두 알고리즘을 같은 입력으로 나란히 시각 비교. 분할 정복의 직관과 두 알고리즘의 성능 차이를 한 번에 잡을 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 퀵 정렬(Quick Sort)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_quick.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf5vY1/dJMcaciSVXM/vQGfjDjTXScFkQmArPLKTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf5vY1/dJMcaciSVXM/vQGfjDjTXScFkQmArPLKTK/img.png&quot; data-alt=&quot;피벗 기준 분할 후 좌&amp;amp;middot;우 부분에 재귀 호출&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf5vY1/dJMcaciSVXM/vQGfjDjTXScFkQmArPLKTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf5vY1%2FdJMcaciSVXM%2FvQGfjDjTXScFkQmArPLKTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;680&quot; data-filename=&quot;sort_quick.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;피벗 기준 분할 후 좌&amp;middot;우 부분에 재귀 호출&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;피벗(Pivot)&lt;/b&gt; 선택&lt;/li&gt;
&lt;li&gt;피벗보다 작은 값은 왼쪽, 큰 값은 오른쪽으로 분할&lt;/li&gt;
&lt;li&gt;재귀적으로 정렬&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;: 평균 $O(n \log n)$ / 최악 $O(n&amp;sup2;)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;: $O(\log n)$ &amp;mdash; 재귀 스택&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;: 불안정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적응성&lt;/b&gt;: 비적응적&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 평균적으로 가장 빠름, 제자리 정렬&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 최악의 경우 $O(n&amp;sup2;)$, 불안정&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;# 퀵 정렬 알고리즘 구현하기

from typing import MutableSequence

def qsort(a: MutableSequence, left: int, right: int) -&amp;gt; None:
    &quot;&quot;&quot;a[left] ~ a[right]를 퀵 정렬&quot;&quot;&quot;
    pl = left
    pr = right
    x = a[(left + right) // 2]

    while pl &amp;lt;= pr:
        while a[pl] &amp;lt; x: pl += 1
        while a[pr] &amp;gt; x: pr -= 1
        if pl &amp;lt;= pr:
            a[pl], a[pr] = a[pr], a[pl]
            pl += 1
            pr -= 1

    if left &amp;lt; pr: qsort(a, left, pr)
    if pl &amp;lt; right: qsort(a, pl, right)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 퀵 정렬 알고리즘 구현하기 (피벗 선택 개선: 3-중앙값)

from typing import MutableSequence

def sort3(a: MutableSequence, idx1: int, idx2: int, idx3: int):
    &quot;&quot;&quot;a[idx1], a[idx2], a[idx3]을 오름차순으로 정렬하고 중앙값의 인덱스를 반환&quot;&quot;&quot;
    if a[idx2] &amp;lt; a[idx1]: a[idx2], a[idx1] = a[idx1], a[idx2]
    if a[idx3] &amp;lt; a[idx2]: a[idx3], a[idx2] = a[idx2], a[idx3]
    if a[idx2] &amp;lt; a[idx1]: a[idx2], a[idx1] = a[idx1], a[idx2]
    return idx2

def qsort(a: MutableSequence, left: int, right: int) -&amp;gt; None:
    &quot;&quot;&quot;a[left] ~ a[right]를 퀵 정렬&quot;&quot;&quot;
    pl = left
    pr = right
    m = sort3(a, pl, (pl + pr) // 2, pr)
    x = a[m]

    a[m], a[pr - 1] = a[pr - 1], a[m]
    pl += 1
    pr -= 2
    while pl &amp;lt;= pr:
        while a[pl] &amp;lt; x: pl += 1
        while a[pr] &amp;gt; x: pr -= 1
        if pl &amp;lt;= pr:
            a[pl], a[pr] = a[pr], a[pl]
            pl += 1
            pr -= 1

    if left &amp;lt; pr: qsort(a, left, pr)
    if pl &amp;lt; right: qsort(a, pl, right)

def quick_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;퀵 정렬&quot;&quot;&quot;
    qsort(a, 0, len(a) - 1)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;참고 영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=aXXWXz5rF64&quot;&gt;Visualization of Quick sort (HD)&lt;/a&gt; &amp;mdash; 피벗 선정과 좌우 분할 과정을 단순한 시각화로. 재귀가 어떻게 작은 부분 문제로 쪼개지는지 명확히 보인다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 힙 정렬(Heap Sort)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_heap.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cduMw8/dJMcacDgK7W/HpJafaj00sdurakduzcRg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cduMw8/dJMcacDgK7W/HpJafaj00sdurakduzcRg1/img.png&quot; data-alt=&quot;힙의 root를 정렬 부분으로 옮기며 힙 크기를 줄여 나간다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cduMw8/dJMcacDgK7W/HpJafaj00sdurakduzcRg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcduMw8%2FdJMcacDgK7W%2FHpJafaj00sdurakduzcRg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;840&quot; data-filename=&quot;sort_heap.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;힙의 root를 정렬 부분으로 옮기며 힙 크기를 줄여 나간다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원리&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배열을 &lt;b&gt;최대 힙&lt;/b&gt;으로 구성&lt;/li&gt;
&lt;li&gt;루트(최댓값)를 맨 뒤로 이동&lt;/li&gt;
&lt;li&gt;힙 크기를 줄이고 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;: $O(n \log n)$ &amp;mdash; 모든 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;: $O(1)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;: 불안정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적응성&lt;/b&gt;: 비적응적&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 안정적인 $O(n \log n)$ 성능, 제자리 정렬&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 실제로는 퀵 정렬보다 느림, 불안정&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;# 힙 정렬 알고리즘 구현하기

from typing import MutableSequence

def heap_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;힙 정렬&quot;&quot;&quot;

    def down_heap(a: MutableSequence, left: int, right: int) -&amp;gt; None:
        &quot;&quot;&quot;a[left] ~ a[right]를 힙으로 만들기&quot;&quot;&quot;
        temp = a[left]

        parent = left
        while parent &amp;lt; (right + 1) // 2:
            cl = parent * 2 + 1
            cr = cl + 1
            child = cr if cr &amp;lt;= right and a[cr] &amp;gt; a[cl] else cl
            if temp &amp;gt;= a[child]:
                break
            a[parent] = a[child]
            parent = child
        a[parent] = temp

    n = len(a)

    for i in range((n - 1) // 2, -1, -1):
        down_heap(a, i, n - 1)

    for i in range(n - 1, 0, -1):
        a[0], a[i] = a[i], a[0]
        down_heap(a, 0, i - 1)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;참고 영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=H5kAcmGOn4Q&quot;&gt;Heaps and Heap sort&lt;/a&gt; &amp;mdash; 힙 자료구조 자체부터 시작해 sift down, heapify, 힙 정렬까지 차근차근. 힙이 왜 정렬에 쓰이는지 단번에 이해된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정렬 알고리즘 비교표&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_complexity_chart.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG1rgC/dJMcaaem2qG/AzyFcrhpMuTPH9lqUwBB70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG1rgC/dJMcaaem2qG/AzyFcrhpMuTPH9lqUwBB70/img.png&quot; data-alt=&quot;n이 커질수록 $O(n&amp;amp;sup2;)$는 폭주, $O(n log n)$는 완만, $O(n)$는 거의 직선&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG1rgC/dJMcaaem2qG/AzyFcrhpMuTPH9lqUwBB70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG1rgC%2FdJMcaaem2qG%2FAzyFcrhpMuTPH9lqUwBB70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;760&quot; data-filename=&quot;sort_complexity_chart.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;n이 커질수록 $O(n&amp;sup2;)$는 폭주, $O(n log n)$는 완만, $O(n)$는 거의 직선&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;알고리즘&lt;/th&gt;
&lt;th&gt;최선&lt;/th&gt;
&lt;th&gt;평균&lt;/th&gt;
&lt;th&gt;최악&lt;/th&gt;
&lt;th&gt;공간&lt;/th&gt;
&lt;th&gt;안정성&lt;/th&gt;
&lt;th&gt;적응성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;버블 정렬&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n&amp;sup2;)$&lt;/td&gt;
&lt;td&gt;$O(n&amp;sup2;)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;안정&lt;/td&gt;
&lt;td&gt;적응적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선택 정렬&lt;/td&gt;
&lt;td&gt;$O(n&amp;sup2;)$&lt;/td&gt;
&lt;td&gt;$O(n&amp;sup2;)$&lt;/td&gt;
&lt;td&gt;$O(n&amp;sup2;)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;불안정&lt;/td&gt;
&lt;td&gt;비적응적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;삽입 정렬&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n&amp;sup2;)$&lt;/td&gt;
&lt;td&gt;$O(n&amp;sup2;)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;안정&lt;/td&gt;
&lt;td&gt;적응적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;병합 정렬&lt;/td&gt;
&lt;td&gt;$O(n \log n)$&lt;/td&gt;
&lt;td&gt;$O(n \log n)$&lt;/td&gt;
&lt;td&gt;$O(n \log n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;안정&lt;/td&gt;
&lt;td&gt;비적응적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;퀵 정렬&lt;/td&gt;
&lt;td&gt;$O(n \log n)$&lt;/td&gt;
&lt;td&gt;$O(n \log n)$&lt;/td&gt;
&lt;td&gt;$O(n&amp;sup2;)$&lt;/td&gt;
&lt;td&gt;$O(\log n)$&lt;/td&gt;
&lt;td&gt;불안정&lt;/td&gt;
&lt;td&gt;비적응적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;힙 정렬&lt;/td&gt;
&lt;td&gt;$O(n \log n)$&lt;/td&gt;
&lt;td&gt;$O(n \log n)$&lt;/td&gt;
&lt;td&gt;$O(n \log n)$&lt;/td&gt;
&lt;td&gt;$O(1)$&lt;/td&gt;
&lt;td&gt;불안정&lt;/td&gt;
&lt;td&gt;비적응적&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;참고 영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=TZRWRjq2CAg&quot;&gt;Insertion Sort vs Bubble Sort + Some analysis&lt;/a&gt; &amp;mdash; 버블&amp;middot;삽입&amp;middot;퀵의 비교 횟수를 차트로 분석. $N^2/2$ vs $N^2/4$ vs $N \log N$의 차이가 시각으로 와닿는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고급 정렬 알고리즘&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 계수 정렬(Counting Sort)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비교 기반이 아닌&lt;/b&gt; 정렬&lt;/li&gt;
&lt;li&gt;값의 &lt;b&gt;범위가 제한적&lt;/b&gt;일 때 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;: $O(n + k)$ ($k$는 값의 범위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;: $O(k)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 도수 정렬 알고리즘 구현하기

from typing import MutableSequence

def fsort(a: MutableSequence, max: int) -&amp;gt; None:
    &quot;&quot;&quot;도수 정렬(배열 원솟값은 0 이상 max 이하)&quot;&quot;&quot;
    n = len(a)
    f = [0] * (max + 1)
    b = [0] * n

    for i in range(n):              f[a[i]] += 1
    for i in range(1, max + 1):     f[i] += f[i - 1]
    for i in range(n - 1, -1, -1):  f[a[i]] -= 1; b[f[a[i]]] = a[i]
    for i in range(n):              a[i] = b[i]

def counting_sort(a: MutableSequence) -&amp;gt; None:
    &quot;&quot;&quot;도수 정렬&quot;&quot;&quot;
    fsort(a, max(a))&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;참고 영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=mVRHvZF8xtg&quot;&gt;Visualization of Radix sort&lt;/a&gt; &amp;mdash; 계수 정렬(pigeon hole), LSD&amp;middot;MSD 기수 정렬, 그리고 안정 정렬(stable) 개념까지 한 영상에. 자릿수별 정렬이 어떻게 누적되어 최종 정렬이 되는지 직관적으로 보여준다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 기수 정렬(Radix Sort)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자릿수별로&lt;/b&gt; 정렬&lt;/li&gt;
&lt;li&gt;계수 정렬을 &lt;b&gt;여러 번 적용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;: $O(d \times (n + k))$ ($d$는 자릿수)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 팀 정렬(Tim Sort)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;병합 정렬과 삽입 정렬의 하이브리드&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실제 데이터의 &lt;b&gt;부분적 정렬 활용&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Python, Java의 기본 정렬 알고리즘&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정렬 알고리즘 선택 가이드&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sort_selection_guide.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ztMjX/dJMcacb941o/ZZiBtBwkP1kxs6Lh9XGHo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ztMjX/dJMcacb941o/ZZiBtBwkP1kxs6Lh9XGHo1/img.png&quot; data-alt=&quot;상황별로 4가지 결정 기준 &amp;amp;mdash; n 크기 &amp;amp;rarr; 안정성 &amp;amp;rarr; 메모리 &amp;amp;rarr; 최악 보장&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ztMjX/dJMcacb941o/ZZiBtBwkP1kxs6Lh9XGHo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FztMjX%2FdJMcacb941o%2FZZiBtBwkP1kxs6Lh9XGHo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;920&quot; data-filename=&quot;sort_selection_guide.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상황별로 4가지 결정 기준 &amp;mdash; n 크기 &amp;rarr; 안정성 &amp;rarr; 메모리 &amp;rarr; 최악 보장&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 데이터 크기가 작을 때 ($n &amp;lt; 50$)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;삽입 정렬&lt;/b&gt; 추천&lt;/li&gt;
&lt;li&gt;구현 간단, 작은 데이터에 효율적&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 안정성이 중요할 때&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;병합 정렬&lt;/b&gt; 또는 &lt;b&gt;팀 정렬&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;동일한 값의 순서 유지 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 메모리가 제한적일 때&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;힙 정렬&lt;/b&gt; 또는 &lt;b&gt;퀵 정렬&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;제자리 정렬로 메모리 사용 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 평균 성능이 중요할 때&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;퀵 정렬&lt;/b&gt; 또는 &lt;b&gt;팀 정렬&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실제 환경에서 가장 빠른 성능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 최악의 경우 성능 보장이 필요할 때&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;병합 정렬&lt;/b&gt; 또는 &lt;b&gt;힙 정렬&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;항상 $O(n \log n)$ 성능 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 구현에서의 고려사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 하이브리드 정렬&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큰 데이터: 퀵 정렬&lt;/li&gt;
&lt;li&gt;작은 데이터: 삽입 정렬&lt;/li&gt;
&lt;li&gt;최악의 경우: 힙 정렬로 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 피벗 선택 전략&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;중간값의 중간값&lt;/b&gt; (Median-of-3)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;랜덤 피벗&lt;/b&gt; 선택&lt;/li&gt;
&lt;li&gt;&lt;b&gt;듀얼 피벗&lt;/b&gt; 퀵 정렬&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 캐시 효율성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;지역성 원리&lt;/b&gt; 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 접근 패턴&lt;/b&gt; 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비교 기반 정렬의 이론적 하한&lt;/b&gt;: $O(n \log n)$&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실제 성능&lt;/b&gt;: 상수 팩터, 캐시 효율성도 중요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상황에 맞는 알고리즘 선택&lt;/b&gt;이 핵심&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하이브리드 접근법&lt;/b&gt;이 실제 구현에서 효과적&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 인용한 영상들은 모두 같은 시리즈로, &quot;short-sighted robot&quot;이 정렬을 수행하는 시각화다. 정렬 알고리즘 전반을 시각으로 정리하고 싶다면 시리즈 6편을 묶어서 보는 것을 추천한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=TZRWRjq2CAg&quot;&gt;Insertion Sort vs Bubble Sort + Some analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=g06hNBhoS1k&quot;&gt;Shell sort vs Insertion sort&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=es2T6KY45cA&quot;&gt;Merge Sort vs Quick Sort&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=aXXWXz5rF64&quot;&gt;Visualization of Quick sort (HD)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=H5kAcmGOn4Q&quot;&gt;Heaps and Heap sort&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=mVRHvZF8xtg&quot;&gt;Visualization of Radix sort&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DSA</category>
      <category>algorithm</category>
      <category>datastructure</category>
      <category>DoItAlgorithm</category>
      <category>sort</category>
      <category>정리</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/93</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%A0%95%EB%A0%ACSorting-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-6%EA%B0%80%EC%A7%80-%E2%80%94-%EC%9B%90%EB%A6%AC%C2%B7%EB%B3%B5%EC%9E%A1%EB%8F%84%C2%B7%EC%84%A0%ED%83%9D-%EA%B0%80%EC%9D%B4%EB%93%9C#entry93comment</comments>
      <pubDate>Sun, 24 May 2026 20:10:56 +0900</pubDate>
    </item>
    <item>
      <title>라즈베리파이에 셀프호스팅한 n8n으로 매일 아침 뉴스 요약 받기</title>
      <link>https://onebrotravel.tistory.com/entry/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4%EC%97%90-%EC%85%80%ED%94%84%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%9C-n8n%EC%9C%BC%EB%A1%9C-%EB%A7%A4%EC%9D%BC-%EC%95%84%EC%B9%A8-%EB%89%B4%EC%8A%A4-%EC%9A%94%EC%95%BD-%EB%B0%9B%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4%EC%97%90-n8n-%EC%85%80%ED%94%84-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 라즈베리파이에 n8n을 셀프 호스팅했다. 이번 글에서는 n8n으로 간단한 워크플로우를 구성하는 과정을 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 아침 네이버 경제 뉴스를 확인하는데, 매번 네이버에 접속해서 기사를 하나하나 클릭하는 과정이 번거로웠다. AI가 기사를 요약해서 Slack으로 보내준다면 이 과정을 줄일 수 있을 거라고 생각했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cN3I1X/dJMcabqLHkv/AGnwcUKT6oSxkA14Cde6kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cN3I1X/dJMcabqLHkv/AGnwcUKT6oSxkA14Cde6kk/img.png&quot; data-alt=&quot;n8n 워크플로우 전체 노드 구성도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cN3I1X/dJMcabqLHkv/AGnwcUKT6oSxkA14Cde6kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcN3I1X%2FdJMcabqLHkv%2FAGnwcUKT6oSxkA14Cde6kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2556&quot; height=&quot;1265&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1265&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;n8n 워크플로우 전체 노드 구성도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우의 전체 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;Schedule Trigger (매일 09:00)
  -&amp;gt; HTTP Request (네이버 경제 뉴스 페이지 크롤링)
    -&amp;gt; HTML (헤드라인 + 링크 추출)
      -&amp;gt; Code (상위 10개 기사 선택)
        -&amp;gt; Loop Over Items (기사별 반복)
          -&amp;gt; HTTP Request (개별 기사 본문 크롤링)
            -&amp;gt; Wait (1초 대기)
              -&amp;gt; HTML (본문 + 제목 + URL 추출)
        -&amp;gt; Aggregate (10개 기사 데이터 합치기)
          -&amp;gt; OpenAI (GPT로 요약)
            -&amp;gt; Slack (채널에 메시지 전송)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 11개 노드로 구성되어 있고, 크게 네 단계로 나눌 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;사용 노드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. 뉴스 목록 수집&lt;/td&gt;
&lt;td&gt;네이버 경제 섹션에서 헤드라인과 링크 추출&lt;/td&gt;
&lt;td&gt;Schedule Trigger, HTTP Request, HTML, Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. 개별 기사 크롤링&lt;/td&gt;
&lt;td&gt;10개 기사의 본문을 하나씩 가져오기&lt;/td&gt;
&lt;td&gt;Loop Over Items, HTTP Request, Wait, HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. LLM 요약&lt;/td&gt;
&lt;td&gt;수집한 본문을 GPT에 넘겨 구조화된 요약 생성&lt;/td&gt;
&lt;td&gt;Aggregate, OpenAI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. 슬랙 전송&lt;/td&gt;
&lt;td&gt;요약 결과를 슬랙 채널에 전송&lt;/td&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 뉴스 목록 수집&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Schedule Trigger&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;1270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1JL6d/dJMcacQHPsv/uQWDlxzAoRqyDuT4zkfC51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1JL6d/dJMcacQHPsv/uQWDlxzAoRqyDuT4zkfC51/img.png&quot; data-alt=&quot;Schedule Trigger 노드 설정 화면 &amp;amp;mdash; 매일 09:00 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1JL6d/dJMcacQHPsv/uQWDlxzAoRqyDuT4zkfC51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1JL6d%2FdJMcacQHPsv%2FuQWDlxzAoRqyDuT4zkfC51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2554&quot; height=&quot;1270&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;1270&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Schedule Trigger 노드 설정 화면 &amp;mdash; 매일 09:00 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우의 시작점이다. 매일 아침 9시에 자동으로 실행되도록 설정했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP Request - 뉴스 페이지 가져오기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/npOa3/dJMcac4eH0X/WWmyzMhnLoXy3e8WC3JE60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/npOa3/dJMcac4eH0X/WWmyzMhnLoXy3e8WC3JE60/img.png&quot; data-alt=&quot;HTTP Request 노드 &amp;amp;mdash; 네이버 뉴스 경제 섹션 GET 요청 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/npOa3/dJMcac4eH0X/WWmyzMhnLoXy3e8WC3JE60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnpOa3%2FdJMcac4eH0X%2FWWmyzMhnLoXy3e8WC3JE60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2559&quot; height=&quot;1269&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1269&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HTTP Request 노드 &amp;mdash; 네이버 뉴스 경제 섹션 GET 요청 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 뉴스 경제 섹션의 HTML을 가져온다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Method&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://news.naver.com/section/101&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML - 헤드라인과 링크 추출&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Aq3NY/dJMcagMov6F/emYAuAtakOmqZYrbYDPjf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Aq3NY/dJMcagMov6F/emYAuAtakOmqZYrbYDPjf0/img.png&quot; data-alt=&quot;HTML 노드 &amp;amp;mdash; 헤드라인/링크 CSS 셀렉터 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Aq3NY/dJMcagMov6F/emYAuAtakOmqZYrbYDPjf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAq3NY%2FdJMcagMov6F%2FemYAuAtakOmqZYrbYDPjf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2556&quot; height=&quot;1265&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1265&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HTML 노드 &amp;mdash; 헤드라인/링크 CSS 셀렉터 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가져온 HTML에서 CSS 셀렉터로 기사 제목과 링크를 배열로 추출한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;추출 대상&lt;/th&gt;
&lt;th&gt;CSS Selector&lt;/th&gt;
&lt;th&gt;반환 형태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;헤드라인&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.sa_text_title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;배열 (텍스트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;링크&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.sa_text_title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;배열 (href 속성)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Code - 상위 10개 기사 선택&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2558&quot; data-origin-height=&quot;1268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ya3TV/dJMcad261yf/KXwhPACSeKgxVcYKiuWvqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ya3TV/dJMcad261yf/KXwhPACSeKgxVcYKiuWvqK/img.png&quot; data-alt=&quot;Code 노드 &amp;amp;mdash; 상위 10개 기사 추출 JavaScript&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ya3TV/dJMcad261yf/KXwhPACSeKgxVcYKiuWvqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fya3TV%2FdJMcad261yf%2FKXwhPACSeKgxVcYKiuWvqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2558&quot; height=&quot;1268&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2558&quot; data-origin-height=&quot;1268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Code 노드 &amp;mdash; 상위 10개 기사 추출 JavaScript&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 노드에서 추출한 배열을 받아 상위 10개만 잘라내고, 각각을 개별 아이템으로 변환한다. n8n에서 반복 처리를 하려면 데이터가 개별 아이템 형태여야 한다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;const headlines = $input.first().json.headlines;
const links = $input.first().json.links;

const articles = [];
const limit = Math.min(10, headlines.length);

for (let i = 0; i &amp;lt; limit; i++) {
  articles.push({
    json: {
      index: i + 1,
      title: headlines[i],
      link: links[i]
    }
  });
}

return articles;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서 &lt;code&gt;{ index, title, link }&lt;/code&gt; 형태의 아이템 10개가 만들어진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 개별 기사 크롤링&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Loop Over Items&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2551&quot; data-origin-height=&quot;1269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEn6Vl/dJMcacpF4mb/RduXabBatWsuC11VJGgtVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEn6Vl/dJMcacpF4mb/RduXabBatWsuC11VJGgtVk/img.png&quot; data-alt=&quot;Loop 노드 설정 화면 &amp;amp;mdash; Done/Loop 출력&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEn6Vl/dJMcacpF4mb/RduXabBatWsuC11VJGgtVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEn6Vl%2FdJMcacpF4mb%2FRduXabBatWsuC11VJGgtVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2551&quot; height=&quot;1269&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2551&quot; data-origin-height=&quot;1269&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Loop 노드 설정 화면 &amp;mdash; Done/Loop 출력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n의 Split In Batches 노드로 10개 기사를 하나씩 순회한다. 이 노드는 두 개의 출력을 가진다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;출력&lt;/th&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;다음 노드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;td&gt;모든 아이템 처리 완료&lt;/td&gt;
&lt;td&gt;Aggregate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop&lt;/td&gt;
&lt;td&gt;아직 처리할 아이템 있음&lt;/td&gt;
&lt;td&gt;HTTP Request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP Request - 기사 본문 가져오기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2558&quot; data-origin-height=&quot;1267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b95WLZ/dJMcadoukri/bkv5kK4loFLKbyGK1IReNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b95WLZ/dJMcadoukri/bkv5kK4loFLKbyGK1IReNk/img.png&quot; data-alt=&quot;HTTP Request 노드 &amp;amp;mdash; User-Agent/Accept-Language 헤더 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b95WLZ/dJMcadoukri/bkv5kK4loFLKbyGK1IReNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb95WLZ%2FdJMcadoukri%2Fbkv5kK4loFLKbyGK1IReNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2558&quot; height=&quot;1267&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2558&quot; data-origin-height=&quot;1267&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HTTP Request 노드 &amp;mdash; User-Agent/Accept-Language 헤더 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기사 URL로 접근해서 본문 HTML을 가져온다. User-Agent와 Accept-Language 헤더를 설정해야 정상적인 응답을 받을 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;헤더&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User-Agent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept-Language&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ko-KR,ko;q=0.9&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;헤더 누락 시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더 없이 요청하면 네이버가 봇으로 인식해서 정상적인 HTML 대신 에러 페이지를 반환할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Wait - 1초 대기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;1270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RNmJP/dJMcabxuZ6J/7HmeIBxrzLV18T9WT7GlGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RNmJP/dJMcabxuZ6J/7HmeIBxrzLV18T9WT7GlGk/img.png&quot; data-alt=&quot;Wait 노드 &amp;amp;mdash; 1초 대기 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RNmJP/dJMcabxuZ6J/7HmeIBxrzLV18T9WT7GlGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRNmJP%2FdJMcabxuZ6J%2F7HmeIBxrzLV18T9WT7GlGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2552&quot; height=&quot;1270&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;1270&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Wait 노드 &amp;mdash; 1초 대기 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;차단 위험&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기사를 연속으로 요청하면 서버에서 차단당할 수 있다. 1초 간격을 두어 네이버 서버에 부담을 주지 않도록 했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTML - 본문 데이터 추출&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2555&quot; data-origin-height=&quot;1264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q7FMe/dJMcahR0HDH/REZHQFFQXKhqFpxJ4Ts0j0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q7FMe/dJMcahR0HDH/REZHQFFQXKhqFpxJ4Ts0j0/img.png&quot; data-alt=&quot;HTML 노드 &amp;amp;mdash; 본문/제목/URL CSS 셀렉터 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q7FMe/dJMcahR0HDH/REZHQFFQXKhqFpxJ4Ts0j0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq7FMe%2FdJMcahR0HDH%2FREZHQFFQXKhqFpxJ4Ts0j0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2555&quot; height=&quot;1264&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2555&quot; data-origin-height=&quot;1264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HTML 노드 &amp;mdash; 본문/제목/URL CSS 셀렉터 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 기사 페이지에서 필요한 정보를 CSS 셀렉터로 추출한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;추출 대상&lt;/th&gt;
&lt;th&gt;CSS Selector&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;본문&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#newsct_article&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기사 전문 텍스트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제목&lt;/td&gt;
&lt;td&gt;&lt;code&gt;h2#title_area span&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기사 제목&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta[property=&quot;og:url&quot;]&lt;/code&gt; (content 속성)&lt;/td&gt;
&lt;td&gt;기사 원문 링크&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추출이 끝나면 다시 Loop 노드로 돌아가서 다음 기사를 처리한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. LLM 요약&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Aggregate&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2553&quot; data-origin-height=&quot;1271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHDtEJ/dJMcaiXE2eA/ZCS7TQIPaST1xGv5psEQZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHDtEJ/dJMcaiXE2eA/ZCS7TQIPaST1xGv5psEQZ0/img.png&quot; data-alt=&quot;Aggregate 노드 설정 화면 &amp;amp;mdash; 10개 아이템을 배열로 합치기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHDtEJ/dJMcaiXE2eA/ZCS7TQIPaST1xGv5psEQZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHDtEJ%2FdJMcaiXE2eA%2FZCS7TQIPaST1xGv5psEQZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2553&quot; height=&quot;1271&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2553&quot; data-origin-height=&quot;1271&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Aggregate 노드 설정 화면 &amp;mdash; 10개 아이템을 배열로 합치기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10개 기사 크롤링이 모두 끝나면 Loop의 Done 출력으로 넘어온다. Aggregate 노드는 개별 아이템 10개를 하나의 배열로 합쳐준다. GPT에 한 번에 넘기기 위한 전처리다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OpenAI - GPT로 요약&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCzlME/dJMcaicnI34/yW9MUSq3gs4maFIlGMCrW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCzlME/dJMcaicnI34/yW9MUSq3gs4maFIlGMCrW1/img.png&quot; data-alt=&quot;OpenAI 노드 설정 화면 &amp;amp;mdash; 프롬프트 및 모델 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCzlME/dJMcaicnI34/yW9MUSq3gs4maFIlGMCrW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCzlME%2FdJMcaicnI34%2FyW9MUSq3gs4maFIlGMCrW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2556&quot; height=&quot;1268&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OpenAI 노드 설정 화면 &amp;mdash; 프롬프트 및 모델 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합쳐진 10개 기사 데이터를 GPT에 전달한다. 프롬프트는 다음과 같이 구성했다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;당신은 전문 경제 뉴스 분석가입니다.
아래는 오늘의 네이버 경제 헤드라인 뉴스 10개의 전문(본문)입니다.

각 기사를 자세히 읽고 다음 형식으로 심층 요약해주세요:

##   [기사 번호] 기사 제목
-   핵심 내용: 기사의 핵심 사실 2-3줄로 요약
-   시사점: 이 뉴스가 경제/시장에 미치는 영향
-   관련 키워드: 핵심 키워드 2-3개
-   원문 링크: 기사 원문 URL

---

마지막에   오늘의 경제 트렌드 종합 섹션을 추가하여
10개 기사를 종합한 오늘의 경제 동향을 3-4줄로 분석해주세요.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기사 데이터는 n8n의 Expression 문법으로 동적 주입한다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{{ $json.data.map((item, i) =&amp;gt; 
  `[기사 ${i+1}]\n${item.body}\n  원문: ${item.link}`
).join('\n\n---\n\n') }}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfGgyt/dJMcafzVWtk/m6Ocsj8E2kOz1J9dozb6J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfGgyt/dJMcafzVWtk/m6Ocsj8E2kOz1J9dozb6J1/img.png&quot; data-alt=&quot;gpt-5-nano 모델 선택&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfGgyt/dJMcafzVWtk/m6Ocsj8E2kOz1J9dozb6J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfGgyt%2FdJMcafzVWtk%2Fm6Ocsj8E2kOz1J9dozb6J1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2556&quot; height=&quot;1268&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;gpt-5-nano 모델 선택&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;모델 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용한 모델은 &lt;code&gt;gpt-5-nano&lt;/code&gt;다. 높은 성능이 필요하지 않은 간단한 요약 작업이면서 매일 실행되는 워크플로우인 만큼 비용을 고려해서 가벼운 모델을 선택했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 슬랙 전송&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3M08m/dJMcagZRfp2/X1ojmxrimMhdIJMnhlRUfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3M08m/dJMcagZRfp2/X1ojmxrimMhdIJMnhlRUfk/img.png&quot; data-alt=&quot;Slack 노드 설정 화면 &amp;amp;mdash; 채널 및 메시지 내용 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3M08m/dJMcagZRfp2/X1ojmxrimMhdIJMnhlRUfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3M08m%2FdJMcagZRfp2%2FX1ojmxrimMhdIJMnhlRUfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2556&quot; height=&quot;1265&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2556&quot; data-origin-height=&quot;1265&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Slack 노드 설정 화면 &amp;mdash; 채널 및 메시지 내용 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT의 응답을 슬랙 채널로 전송한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;채널&lt;/td&gt;
&lt;td&gt;&lt;code&gt;news&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메시지 내용&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{{ $json.output[0].content[0].text }}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Pasted image 20260301211157.png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wEv5J/dJMcaaZFtUB/tqiq6iFrJKceCkbTNFo9f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wEv5J/dJMcaaZFtUB/tqiq6iFrJKceCkbTNFo9f1/img.png&quot; data-alt=&quot;슬랙 채널에 도착한 뉴스 요약 메시지 캡처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wEv5J/dJMcaaZFtUB/tqiq6iFrJKceCkbTNFo9f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwEv5J%2FdJMcaaZFtUB%2Ftqiq6iFrJKceCkbTNFo9f1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2559&quot; height=&quot;1392&quot; data-filename=&quot;Pasted image 20260301211157.png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;슬랙 채널에 도착한 뉴스 요약 메시지 캡처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 매일 아침 9시에 슬랙을 열면 경제 뉴스 10개의 요약이 정리되어 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;API 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OpenAI API 키 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT를 이용하기 위해서는 API 키를 설정해 주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;a href=&quot;https://platform.openai.com/api-keys&quot;&gt;여기&lt;/a&gt;에서 API key를 설정한 후 n8n에서 아래 사진과 같이 credential을 설정해주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wHvgf/dJMcad27fSB/PsqlN0jfBO6nL7TNqVH14k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wHvgf/dJMcad27fSB/PsqlN0jfBO6nL7TNqVH14k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1269&quot; data-filename=&quot;blob&quot; style=&quot;width: 49.5446%; margin-right: 10px;&quot; data-widthpercent=&quot;50.13&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wHvgf/dJMcad27fSB/PsqlN0jfBO6nL7TNqVH14k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwHvgf%2FdJMcad27fSB%2FPsqlN0jfBO6nL7TNqVH14k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2559&quot; height=&quot;1269&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1SXSt/dJMcaak3tqL/kwtMnkmjZccQrI1as8RaZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1SXSt/dJMcaak3tqL/kwtMnkmjZccQrI1as8RaZK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2550&quot; data-origin-height=&quot;1271&quot; data-filename=&quot;blob&quot; style=&quot;width: 49.2926%;&quot; data-widthpercent=&quot;49.87&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1SXSt/dJMcaak3tqL/kwtMnkmjZccQrI1as8RaZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1SXSt%2FdJMcaak3tqL%2FkwtMnkmjZccQrI1as8RaZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2550&quot; height=&quot;1271&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;n8n OpenAI credential 등록 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Slack API 설정&lt;/h3&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-note&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;참고 영상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slack API 연결은 생각 외로 복잡하고 시간이 꽤 걸리는 작업이었다. 여기에서 설명하기보다는 내가 참고했던 &lt;a href=&quot;https://www.youtube.com/watch?v=qk5JH6ImK0I&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;영상 링크&lt;/a&gt;를 남긴다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개선 아이디어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 기본적인 뉴스 요약만 하고 있지만, 확장할 수 있는 방향이 몇 가지 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;경제(101) 외에 정치(100), IT/과학(105) 등 다른 섹션 추가&lt;/li&gt;
&lt;li&gt;특정 키워드가 포함된 기사만 별도 알림&lt;/li&gt;
&lt;li&gt;요약 결과를 Notion이나 Google Sheets에 저장해서 트렌드 추적&lt;/li&gt;
&lt;li&gt;긍정/부정 감성 분석 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n으로 첫 번째 워크플로우를 만들었다. 정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HTTP Request + HTML 노드로 네이버 뉴스 크롤링&lt;/li&gt;
&lt;li&gt;Split In Batches + Wait으로 개별 기사 안정적으로 수집&lt;/li&gt;
&lt;li&gt;OpenAI 노드로 GPT 요약 생성&lt;/li&gt;
&lt;li&gt;Slack 노드로 매일 아침 자동 전송&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Infra</category>
      <category>Automation</category>
      <category>n8n</category>
      <category>RaspberryPi</category>
      <category>selfhosted</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/83</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4%EC%97%90-%EC%85%80%ED%94%84%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%9C-n8n%EC%9C%BC%EB%A1%9C-%EB%A7%A4%EC%9D%BC-%EC%95%84%EC%B9%A8-%EB%89%B4%EC%8A%A4-%EC%9A%94%EC%95%BD-%EB%B0%9B%EA%B8%B0#entry83comment</comments>
      <pubDate>Thu, 21 May 2026 14:32:13 +0900</pubDate>
    </item>
    <item>
      <title>라즈베리파이에 n8n 셀프 호스팅하기</title>
      <link>https://onebrotravel.tistory.com/entry/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4%EC%97%90-n8n-%EC%85%80%ED%94%84-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4-5-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C&quot;&gt;이전 글&lt;/a&gt;에서 라즈베리파이 5 초기 설정을 마쳤다. 이번 글에서는 Docker를 설치하고 n8n을 셀프 호스팅하는 과정을 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n은 노코드 워크플로우 자동화 도구다. Zapier나 Make와 비슷하지만, 셀프 호스팅이 가능해서 비용 부담 없이 사용할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Docker 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 시스템 업데이트&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Docker 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 설치 스크립트를 사용하면 간단하게 설치할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;curl -fsSL https://get.docker.com | sh&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 현재 사용자를 docker 그룹에 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 docker 명령어는 root 권한이 필요하다. 매번 sudo를 입력하지 않으려면 현재 사용자를 docker 그룹에 추가한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;sudo usermod -aG docker $USER&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 재로그인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그룹 변경 사항을 적용하려면 SSH 연결을 끊고 다시 접속해야 한다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;exit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 SSH로 접속한 후 현재 사용자가 docker 그룹에 추가되었는지, Docker가 정상 작동하는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;groups

docker --version&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;39&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lfY9o/dJMcaiJ7xPh/zCaXB2laSgTawcwzkco4yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lfY9o/dJMcaiJ7xPh/zCaXB2laSgTawcwzkco4yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfY9o/dJMcaiJ7xPh/zCaXB2laSgTawcwzkco4yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlfY9o%2FdJMcaiJ7xPh%2FzCaXB2laSgTawcwzkco4yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;39&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;39&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;43&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nVQWX/dJMcahqY6xv/t7fCzMoSJ6QzzBYLRWOwZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nVQWX/dJMcahqY6xv/t7fCzMoSJ6QzzBYLRWOwZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nVQWX/dJMcahqY6xv/t7fCzMoSJ6QzzBYLRWOwZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnVQWX%2FdJMcahqY6xv%2Ft7fCzMoSJ6QzzBYLRWOwZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1013&quot; height=&quot;43&quot; data-filename=&quot;image (1).png&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;43&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 정보가 출력되면 설치 완료다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;n8n 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로젝트 폴더 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker-compose.yml 파일을 저장할 폴더를 만든다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;mkdir -p ~/_projects/n8n
cd ~/_projects/n8n&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. docker-compose.yml 작성&lt;/h3&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;services:
  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    container_name: n8n
    restart: always
    ports:
      - &quot;127.0.0.1:5678:5678&quot;
    environment:
      - GENERIC_TIMEZONE=Asia/Seoul
      - TZ=Asia/Seoul
    volumes:
      - n8n_data:/home/node/.n8n

volumes:
  n8n_data:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 설정:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;127.0.0.1:5678:5678&lt;/code&gt;: localhost에서만 접속 가능 (외부 직접 접속 차단)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GENERIC_TIMEZONE&lt;/code&gt;, &lt;code&gt;TZ&lt;/code&gt;: 타임존을 서울로 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n8n_data&lt;/code&gt;: 워크플로우 데이터를 Docker 볼륨에 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. n8n 실행&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;-d&lt;/code&gt; 옵션은 백그라운드 실행이다. 컨테이너 상태를 확인하려면:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdYTYI/dJMcadILDjd/mayLGPtK5F4qXEnqzsTID1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdYTYI/dJMcadILDjd/mayLGPtK5F4qXEnqzsTID1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdYTYI/dJMcadILDjd/mayLGPtK5F4qXEnqzsTID1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdYTYI%2FdJMcadILDjd%2FmayLGPtK5F4qXEnqzsTID1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1016&quot; height=&quot;100&quot; data-filename=&quot;image (2).png&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n 컨테이너가 실행 중이면 성공이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;n8n 접속&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제: Secure Cookie 에러&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 &lt;code&gt;http://라즈베리파이IP:5678&lt;/code&gt;로 접속하니 다음 에러가 나왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (3).png&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dER497/dJMcadotLSZ/ZFrBipiFZ5HEont29LEY3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dER497/dJMcadotLSZ/ZFrBipiFZ5HEont29LEY3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dER497/dJMcadotLSZ/ZFrBipiFZ5HEont29LEY3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdER497%2FdJMcadotLSZ%2FZFrBipiFZ5HEont29LEY3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;348&quot; data-filename=&quot;image (3).png&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Your n8n server is configured to use a secure cookie, however you are either visiting this via an insecure URL, or using Safari.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n은 기본적으로 HTTPS 접속을 요구한다. HTTP로 접속하면 보안 쿠키 문제로 로그인이 안 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방법들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secure Cookie 에러를 해결하는 방법은 크게 세 가지로 나눌 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. HTTPS 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 HTTPS를 적용하는 방법이다. 외부에서 n8n에 접속하거나, GitHub/Stripe 같은 외부 서비스의 웹훅을 받아야 한다면 이 방법을 사용해야 한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;난이도&lt;/th&gt;
&lt;th&gt;요구사항&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Tunnel&lt;/td&gt;
&lt;td&gt;⭐ 쉬움&lt;/td&gt;
&lt;td&gt;도메인 필요 (무료 도메인 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailscale&lt;/td&gt;
&lt;td&gt;⭐ 쉬움&lt;/td&gt;
&lt;td&gt;계정만 있으면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx + Let's Encrypt&lt;/td&gt;
&lt;td&gt;⭐⭐ 보통&lt;/td&gt;
&lt;td&gt;도메인 + 포트포워딩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traefik&lt;/td&gt;
&lt;td&gt;⭐⭐⭐ 복잡&lt;/td&gt;
&lt;td&gt;도메인 + 포트포워딩&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 보안 기능 비활성화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n의 Secure Cookie 기능 자체를 끄는 방법이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;난이도&lt;/th&gt;
&lt;th&gt;요구사항&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;N8N_SECURE_COOKIE=false&lt;/td&gt;
&lt;td&gt;⭐ 쉬움&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker-compose.yml의 environment에 추가하면 된다. 간단하지만 보안 기능을 끄는 것이므로 권장하지 않는다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. SSH 터널링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localhost로 접속해서 Secure Cookie 검사를 우회하는 방법이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;난이도&lt;/th&gt;
&lt;th&gt;요구사항&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSH 터널링&lt;/td&gt;
&lt;td&gt;⭐ 쉬움&lt;/td&gt;
&lt;td&gt;SSH 접속 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안 기능을 유지하면서 추가 설정 없이 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 집 안 로컬 네트워크에서만 n8n에 접속한다. 외부에서 접속할 일이 없으니 HTTPS 설정까지는 필요 없었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 SSH 터널링인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;N8N_SECURE_COOKIE=false&lt;/code&gt;로 설정하면 가장 간단하지만, 보안 기능을 끄는 것이라 찝찝했다. SSH 터널링은 보안 기능을 유지하면서도 추가 설정 없이 바로 사용할 수 있어서 선택했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH 터널링으로 접속하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데스크탑에서 다음 명령어를 실행한다. (Git Bash 또는 터미널)&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ssh -L 5678:localhost:5678 사용자이름@라즈베리파이IP&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (4).png&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;565&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beSvN3/dJMcaglgriy/DOtiW59VV0wC7C1NME8r11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beSvN3/dJMcaglgriy/DOtiW59VV0wC7C1NME8r11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beSvN3/dJMcaglgriy/DOtiW59VV0wC7C1NME8r11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeSvN3%2FdJMcaglgriy%2FDOtiW59VV0wC7C1NME8r11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;925&quot; height=&quot;565&quot; data-filename=&quot;image (4).png&quot; data-origin-width=&quot;925&quot; data-origin-height=&quot;565&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데스크탑의 localhost:5678을&lt;/li&gt;
&lt;li&gt;라즈베리파이의 localhost:5678로 연결한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 연결이 유지된 상태에서 브라우저로 접속한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:5678&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (5).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUazUf/dJMcaaZEWBB/G4WBeOXD1ZCTZPotkIhgE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUazUf/dJMcaaZEWBB/G4WBeOXD1ZCTZPotkIhgE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUazUf/dJMcaaZEWBB/G4WBeOXD1ZCTZPotkIhgE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUazUf%2FdJMcaaZEWBB%2FG4WBeOXD1ZCTZPotkIhgE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2559&quot; height=&quot;1393&quot; data-filename=&quot;image (5).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;localhost로 접속하면 n8n이 HTTPS로 인식해서 Secure Cookie 에러가 발생하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 127.0.0.1로 바인딩했나?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker-compose.yml에서 포트를 &lt;code&gt;127.0.0.1:5678:5678&lt;/code&gt;로 설정한 이유는 보안 때문이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;접속 범위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;5678:5678&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;같은 네트워크의 모든 기기에서 접속 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;127.0.0.1:5678:5678&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;라즈베리파이 자체에서만 접속 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 터널링을 통해서만 접속할 수 있게 하면, 같은 네트워크에 있는 다른 기기에서 임의로 접속하는 것을 막을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라즈베리파이에 n8n 셀프 호스팅을 완료했다. 정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Docker 공식 스크립트로 설치&lt;/li&gt;
&lt;li&gt;docker-compose.yml로 n8n 컨테이너 실행&lt;/li&gt;
&lt;li&gt;SSH 터널링으로 Secure Cookie 문제 해결&lt;/li&gt;
&lt;li&gt;127.0.0.1 바인딩으로 외부 직접 접속 차단&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;셀프 호스팅의 장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n은 클라우드 버전도 있지만, 셀프 호스팅하면 몇 가지 장점이 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;클라우드&lt;/th&gt;
&lt;th&gt;셀프 호스팅&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;월 $24~ (Starter 기준)&lt;/td&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 횟수&lt;/td&gt;
&lt;td&gt;플랜별 제한&lt;/td&gt;
&lt;td&gt;무제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 저장&lt;/td&gt;
&lt;td&gt;n8n 서버&lt;/td&gt;
&lt;td&gt;내 라즈베리파이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;커스터마이징&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;자유로움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라즈베리파이는 저전력으로 24시간 구동이 가능하니, 개인용 자동화 서버로 딱 맞는 조합이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이렇게 구축한 n8n으로 실제 자동화 워크플로우를 만들어본다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra</category>
      <category>docker</category>
      <category>n8n</category>
      <category>RaspberryPi</category>
      <category>selfhosted</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/82</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4%EC%97%90-n8n-%EC%85%80%ED%94%84-%ED%98%B8%EC%8A%A4%ED%8C%85%ED%95%98%EA%B8%B0#entry82comment</comments>
      <pubDate>Wed, 20 May 2026 20:18:39 +0900</pubDate>
    </item>
    <item>
      <title>라즈베리파이 5 초기 설정 가이드</title>
      <link>https://onebrotravel.tistory.com/entry/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4-5-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년 전 C언어 공부를 하면서 native Linux 환경을 경험해보고 싶어서 라즈베리파이를 구매했다. 당시에는 잠깐 사용하다가 서랍 속에 넣어뒀는데, 최근 n8n으로 자동화 워크플로우를 구성하면서 다시 꺼내게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n8n 워크플로우는 24시간 돌아가야 하는데, 데스크탑을 하루 종일 켜놓을 수는 없었다. 클라우드 서버를 사용하면 비용이 발생하기 때문에, 전기세 빼면 추가 비용 없이 사용할 수 있는 라즈베리파이가 생각났다. 저전력으로 24시간 구동이 가능하니 딱 맞는 용도였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 라즈베리파이 5 초기 설정 과정을 정리한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;준비물&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;사용 제품&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;본체&lt;/td&gt;
&lt;td&gt;Raspberry Pi 5 8GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;저장장치&lt;/td&gt;
&lt;td&gt;microSD 카드 256GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;모니터, 키보드, 유선 랜 케이블 (초기 설정용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OS 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Raspberry Pi Imager 다운로드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 사이트에서 &lt;a href=&quot;https://www.raspberrypi.com/software/&quot;&gt;Raspberry Pi Imager&lt;/a&gt;를 다운로드한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. OS 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raspberry Pi Imager를 실행하고 다음을 선택한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Device&lt;/b&gt;: Raspberry Pi 5&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OS&lt;/b&gt;: Raspberry Pi OS (64-bit) - Debian Trixie with Desktop&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Storage&lt;/b&gt;: microSD 카드&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ONHX8/dJMcaja8w6w/ep6BypkQlJlTzL0WanYnXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ONHX8/dJMcaja8w6w/ep6BypkQlJlTzL0WanYnXk/img.png&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;505&quot; data-is-animation=&quot;false&quot; data-filename=&quot;image.png&quot; style=&quot;width: 32.5605%; margin-right: 10px;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ONHX8/dJMcaja8w6w/ep6BypkQlJlTzL0WanYnXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FONHX8%2FdJMcaja8w6w%2Fep6BypkQlJlTzL0WanYnXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/njZgd/dJMcahxJRlX/evKxf6pIjHDDhUtjUdNZQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/njZgd/dJMcahxJRlX/evKxf6pIjHDDhUtjUdNZQ1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;503&quot; data-filename=&quot;image (1).png&quot; data-widthpercent=&quot;33.47&quot; style=&quot;width: 32.69%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/njZgd/dJMcahxJRlX/evKxf6pIjHDDhUtjUdNZQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnjZgd%2FdJMcahxJRlX%2FevKxf6pIjHDDhUtjUdNZQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QD8ir/dJMcag6Ec2h/jicQeRnCaHeCdCtKae4N0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QD8ir/dJMcag6Ec2h/jicQeRnCaHeCdCtKae4N0K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;510&quot; data-filename=&quot;image (2).png&quot; style=&quot;width: 32.424%;&quot; data-widthpercent=&quot;33.19&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QD8ir/dJMcag6Ec2h/jicQeRnCaHeCdCtKae4N0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQD8ir%2FdJMcag6Ec2h%2FjicQeRnCaHeCdCtKae4N0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Device 선택 화면 - Raspberry Pi 5 선택/ OS 선택 화면 - Raspberry Pi OS (64-bit) 선택/ Storage 선택 화면 - SD 카드 선택&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 설정 커스터마이징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS 설치 전 설정을 미리 지정할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PfNoV/dJMcagyNjq3/kImHOgAkKiLKEn4uZKZBv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PfNoV/dJMcagyNjq3/kImHOgAkKiLKEn4uZKZBv0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;507&quot; data-filename=&quot;image (3).png&quot; style=&quot;width: 49.3617%; margin-right: 10px;&quot; data-widthpercent=&quot;49.94&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PfNoV/dJMcagyNjq3/kImHOgAkKiLKEn4uZKZBv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPfNoV%2FdJMcagyNjq3%2FkImHOgAkKiLKEn4uZKZBv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;714&quot; height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQK1yT/dJMcabEgR23/seTxt0FqZQHkEJKpQ0a7qK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQK1yT/dJMcabEgR23/seTxt0FqZQHkEJKpQ0a7qK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;503&quot; data-filename=&quot;image (4).png&quot; style=&quot;width: 49.4755%;&quot; data-widthpercent=&quot;50.06&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQK1yT/dJMcabEgR23/seTxt0FqZQHkEJKpQ0a7qK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQK1yT%2FdJMcabEgR23%2FseTxt0FqZQHkEJKpQ0a7qK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V0tOG/dJMcajvwMjI/FM6Rm410EDp3u4tkleEli1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V0tOG/dJMcajvwMjI/FM6Rm410EDp3u4tkleEli1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;502&quot; data-filename=&quot;image (5).png&quot; style=&quot;width: 49.8793%; margin-right: 10px; margin-top: 10px;&quot; data-widthpercent=&quot;50.47&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V0tOG/dJMcajvwMjI/FM6Rm410EDp3u4tkleEli1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV0tOG%2FdJMcajvwMjI%2FFM6Rm410EDp3u4tkleEli1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dD94uf/dJMcabEgR6v/QUAedoPsAfwp8635IiA3Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dD94uf/dJMcabEgR6v/QUAedoPsAfwp8635IiA3Ek/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;510&quot; data-filename=&quot;image (7).png&quot; style=&quot;width: 48.958%; margin-top: 10px;&quot; data-widthpercent=&quot;49.53&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dD94uf/dJMcabEgR6v/QUAedoPsAfwp8635IiA3Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdD94uf%2FdJMcabEgR6v%2FQUAedoPsAfwp8635IiA3Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;설정 커스터마이징 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트 이름&lt;/li&gt;
&lt;li&gt;사용자 이름 / 비밀번호&lt;/li&gt;
&lt;li&gt;로케일 (Asia/Seoul)&lt;/li&gt;
&lt;li&gt;SSH 활성화 옵션(선택)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 쓰기&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FQS2H/dJMcaii5CfF/H0px2FnY8PjIshKTlc8Wmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FQS2H/dJMcaii5CfF/H0px2FnY8PjIshKTlc8Wmk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;507&quot; data-filename=&quot;image (9).png&quot; style=&quot;width: 49.3978%; margin-right: 10px;&quot; data-widthpercent=&quot;49.98&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FQS2H/dJMcaii5CfF/H0px2FnY8PjIshKTlc8Wmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFQS2H%2FdJMcaii5CfF%2FH0px2FnY8PjIshKTlc8Wmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZKg29/dJMcac4ehha/PPx0jBibkg4YkGb3IBlmd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZKg29/dJMcac4ehha/PPx0jBibkg4YkGb3IBlmd0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;508&quot; data-filename=&quot;image (13).png&quot; style=&quot;width: 49.4394%;&quot; data-widthpercent=&quot;50.02&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZKg29/dJMcac4ehha/PPx0jBibkg4YkGb3IBlmd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZKg29%2FdJMcac4ehha%2FPPx0jBibkg4YkGb3IBlmd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;712&quot; height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Write 완료 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Write&quot; 버튼을 누르면 OS가 SD 카드에 설치된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;초기 부팅 및 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 첫 부팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SD 카드를 라즈베리파이에 삽입하고 전원을 연결한다. 모니터와 키보드를 연결한 상태로 부팅한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (14).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc5CF0/dJMcabj0Yj8/B8v1xaEGCtTAr9AUFBkfvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc5CF0/dJMcabj0Yj8/B8v1xaEGCtTAr9AUFBkfvk/img.png&quot; data-alt=&quot;라즈베리파이 데스크탑 첫 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc5CF0/dJMcabj0Yj8/B8v1xaEGCtTAr9AUFBkfvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc5CF0%2FdJMcabj0Yj8%2FB8v1xaEGCtTAr9AUFBkfvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;image (14).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;라즈베리파이 데스크탑 첫 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 네트워크 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 와이파이로 연결하려 했는데 잘 잡히지 않았다. 유선 랜 케이블을 연결하니 바로 인터넷에 연결됐다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-tip&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;팁&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와이파이 연결이 불안정하다면 초기 설정은 유선 랜으로 진행하는 걸 추천한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. IP 주소 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 IP 주소를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;hostname -I&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (15).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PAuR3/dJMcaayCDaq/2PJ9RykqoKu35MMgE4Dsl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PAuR3/dJMcaayCDaq/2PJ9RykqoKu35MMgE4Dsl1/img.png&quot; data-alt=&quot;터미널에서 hostname -I 실행 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PAuR3/dJMcaayCDaq/2PJ9RykqoKu35MMgE4Dsl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPAuR3%2FdJMcaayCDaq%2F2PJ9RykqoKu35MMgE4Dsl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-filename=&quot;image (15).png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;터미널에서 hostname -I 실행 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 IP 주소로 SSH 접속할 예정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSH 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SSH 활성화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라즈베리파이에서 SSH를 활성화한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;sudo raspi-config&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Interface Options &amp;rarr; SSH &amp;rarr; Enable&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. RSA 키 생성 (데스크탑에서)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데스크탑에서 SSH 키를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ssh-keygen -t rsa -b 4096&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 공개 키 복사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 공개 키를 라즈베리파이에 복사한다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;ssh-copy-id 사용자이름@라즈베리파이IP&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 접속 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호 없이 SSH 접속이 되는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ssh 사용자이름@라즈베리파이IP&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (16).png&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AJPnL/dJMcadPy6dD/daVi9716Cft5rKCpQJlZz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AJPnL/dJMcadPy6dD/daVi9716Cft5rKCpQJlZz0/img.png&quot; data-alt=&quot;SSH 접속 성공 화면 - 비밀번호 입력 없이 접속됨&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AJPnL/dJMcadPy6dD/daVi9716Cft5rKCpQJlZz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAJPnL%2FdJMcadPy6dD%2FdaVi9716Cft5rKCpQJlZz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;927&quot; height=&quot;564&quot; data-filename=&quot;image (16).png&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SSH 접속 성공 화면 - 비밀번호 입력 없이 접속됨&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-warning&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;주의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 주소와 게이트웨이는 자신의 네트워크 환경에 맞게 수정한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VS Code로 원격 개발 환경 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 연결 후에는 VS Code의 Remote - SSH 확장을 사용하면 편하게 작업할 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;VS Code에서 Remote - SSH 확장 설치&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (17).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xr1P9/dJMcaarQ2Bf/HGLuWMykGJ9HpTu7mBY4b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xr1P9/dJMcaarQ2Bf/HGLuWMykGJ9HpTu7mBY4b0/img.png&quot; data-alt=&quot;VS Code Extensions에서 Remote - SSH 검색/설치 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xr1P9/dJMcaarQ2Bf/HGLuWMykGJ9HpTu7mBY4b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxr1P9%2FdJMcaarQ2Bf%2FHGLuWMykGJ9HpTu7mBY4b0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2559&quot; height=&quot;1392&quot; data-filename=&quot;image (17).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;VS Code Extensions에서 Remote - SSH 검색/설치 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;F1&lt;/code&gt; &amp;rarr; &quot;Remote-SSH: Connect to Host&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (18).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rX6xI/dJMcaarQ2B0/uOkb8O1QJmEreWOVKHSsLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rX6xI/dJMcaarQ2B0/uOkb8O1QJmEreWOVKHSsLK/img.png&quot; data-alt=&quot;Command Palette에서 Remote-SSH: Connect to Host 선택&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rX6xI/dJMcaarQ2B0/uOkb8O1QJmEreWOVKHSsLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrX6xI%2FdJMcaarQ2B0%2FuOkb8O1QJmEreWOVKHSsLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2559&quot; height=&quot;1392&quot; data-filename=&quot;image (18).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Command Palette에서 Remote-SSH: Connect to Host 선택&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;사용자이름@라즈베리파이IP&lt;/code&gt; 입력&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (19).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfgyrm/dJMcac4ehvY/4MiaOaNhFIkXCKZE7k2f2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfgyrm/dJMcac4ehvY/4MiaOaNhFIkXCKZE7k2f2K/img.png&quot; data-alt=&quot;호스트 입력 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfgyrm/dJMcac4ehvY/4MiaOaNhFIkXCKZE7k2f2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfgyrm%2FdJMcac4ehvY%2F4MiaOaNhFIkXCKZE7k2f2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2559&quot; height=&quot;1392&quot; data-filename=&quot;image (19).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;호스트 입력 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;연결 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image (20).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvM04w/dJMcaak2SLq/ya0arFUrGgKHMP8s724Tt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvM04w/dJMcaak2SLq/ya0arFUrGgKHMP8s724Tt0/img.png&quot; data-alt=&quot;VS Code 좌측 하단에 SSH: 라즈베리파이 연결됨 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvM04w/dJMcaak2SLq/ya0arFUrGgKHMP8s724Tt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvM04w%2FdJMcaak2SLq%2Fya0arFUrGgKHMP8s724Tt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2559&quot; height=&quot;1392&quot; data-filename=&quot;image (20).png&quot; data-origin-width=&quot;2559&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;VS Code 좌측 하단에 SSH: 라즈베리파이 연결됨 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 데스크탑에서 라즈베리파이 파일을 직접 편집할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라즈베리파이 5 초기 설정을 마쳤다. 정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Raspberry Pi Imager로 OS 설치&lt;/li&gt;
&lt;li&gt;유선 랜으로 네트워크 연결&lt;/li&gt;
&lt;li&gt;RSA 키로 SSH 설정&lt;/li&gt;
&lt;li&gt;고정 IP 설정&lt;/li&gt;
&lt;li&gt;VS Code Remote로 원격 개발 환경 구성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이렇게 세팅한 라즈베리파이에 Docker를 설치하고 n8n을 셀프 호스팅하는 과정을 다룬다.&lt;/p&gt;</description>
      <category>Infra</category>
      <category>linux</category>
      <category>n8n</category>
      <category>RaspberryPi</category>
      <category>selfhosted</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/81</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%ED%8C%8C%EC%9D%B4-5-%EC%B4%88%EA%B8%B0-%EC%84%A4%EC%A0%95-%EA%B0%80%EC%9D%B4%EB%93%9C#entry81comment</comments>
      <pubDate>Wed, 20 May 2026 17:36:26 +0900</pubDate>
    </item>
    <item>
      <title>[크래프톤 정글 10기] 나만무 프로젝트 회고 (4)</title>
      <link>https://onebrotravel.tistory.com/entry/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-%EB%82%98%EB%A7%8C%EB%AC%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편에서는 워크플로우 빌더의 포트 시스템이 어떻게 만들어졌다가 사라졌는지, 그리고 재설계를 분석한 경험을 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4편에서는 5주간의 프로젝트를 돌아보며 시리즈를 마무리하려 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5주간의 회고&lt;/li&gt;
&lt;li&gt;팀 협업 이야기&lt;/li&gt;
&lt;li&gt;기술적 회고&lt;/li&gt;
&lt;li&gt;이후 계획&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5주간의 회고&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;잘한 점: 공통의 목표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주간 우리 팀이 잘했던 건 &quot;출시할 수 있는 프로덕트 완성&quot;이라는 목표를 끝까지 놓지 않은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나만무 프로젝트 특성상 개인의 성장이나 취업을 위한 기술 스택에 집중하기 쉽다. 물론 중요한 부분이지만, 그것에만 매몰되면 작은 기능에 복잡한 기술을 적용하는 오버엔지니어링으로 이어질 수 있다. 잘못하면 결과물 없이 프로젝트가 끝나는 상황도 생길 수 있다. 실제로 이전 기수에는 일정 내에 프로덕트가 완성되지 않아 발표를 하지 못하고 부트캠프를 마무리 했던 팀도 있었다고 들었다. 우리 팀은 방향이 흔들릴 때마다 &quot;일단 출시할 수 있는 상태로 만들자&quot;를 상기시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 나만무 프로젝트를 완주할 수 있었던 이유였다고 생각한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아쉬운 점: 구현하지 못한 기능들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 더 있었다면 추가하고 싶었던 기능들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;워크플로우를 코드로 변환해서 배포&lt;/b&gt;: 사용자가 만든 워크플로우를 Python 코드로 export하는 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;셀프 호스트 배포&lt;/b&gt;: n8n처럼 Docker image로 배포해서 사용자가 직접 호스팅할 수 있는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 기획 단계에서 논의됐지만, 5주라는 시간 안에 핵심 기능을 완성하는 것만으로도 빠듯했다. 다음에 비슷한 프로젝트를 한다면 초기 기획 단계에서 스코프를 더 명확하게 잡아야겠다는 생각이 들었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팀 협업 이야기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5주차의 갈등&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 있었던 갈등에 대해서 이야기 해 보자면, 5주차 최종 발표 전, 마지막 리허설 이틀 전에 한 팀원이 기능 추가를 제안했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 나는 현재 구현된 내용만으로도 충분히 최종 발표가 가능하다고 판단했다. 게다가 발표 PPT 제작, 포스터 세션용 포스터 제작 등 구현 외적인 일도 남아있는 상태라서 이 부분을 마무리하는 게 더 중요하다고 생각했다. 혹시라도 일정을 맞추지 못하는 상황이 생길까 봐 걱정됐기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 팀원은 프로덕트의 퀄리티와 발표의 퍼포먼스를 위해 추가 기능 구현이 필요하다고 주장했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 팀장이 두 일을 병렬로 처리하자고 결정했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능 구현을 주장한 팀원은 추가 기능 개발을 진행&lt;/li&gt;
&lt;li&gt;나머지는 그 기능이 없다고 가정하고 발표 시나리오, PPT, 포스터를 준비&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 구현이 완료되지 않아도 발표를 마무리할 수 있는 구조를 만든 것이다. 결과적으로 기능도 완성됐고 발표 준비도 잘 마무리됐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌아보면 둘 다 맞는 말이었다. 나는 리스크 관리를 중요하게 생각했고, 팀원은 결과물의 퀄리티를 중요하게 생각했다. 팀장의 &quot;병렬 처리&quot; 결정이 두 관점을 모두 살릴 수 있는 방법이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀 프로젝트에서 의견 충돌이 생겼을 때, 꼭 하나를 선택해야 하는 건 아니라는 걸 배웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술적 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 만든다면 다르게 할 것들이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;진행 중 상태를 처음부터 고려하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편에서 다뤘듯이 처음에 만든 포트 시스템은 완성된 워크플로우를 기준으로 검증을 짰다. 그런데 워크플로우 빌더는 사용자가 점진적으로 만들어가는 도구다. 필수 입력이 잠시 비어 있을 수도 있고, ports 정의가 없는 노드가 있을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 디자인 단계에서 완성된 상태만이 아니라 만들어가는 중인 상태도 머릿속에 두고 검증의 시점을 결정하려 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MVP 노드만 보고 디자인하지 않기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트 시스템을 만들 때 머릿속에 있던 노드는 MVP의 4개 노드뿐이었다. 그래서 고정된 ports를 가진 노드만 가정한 시스템이 나왔고, IF/ELSE 같이 동적 ports를 가진 노드를 만나자마자 깨졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에는 디자인 단계에서 가장 복잡한 노드도 미리 고려하려고 한다. 일단 단순한 케이스로 만들고 나중에 확장하면 된다고 생각했지만, 시스템의 구조 자체가 단순한 케이스에 묶이면 확장이 어려워진다는 걸 배웠다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이후 계획&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 취업 준비를 진행하면서, SnapAgent를 만들며 알게 된 것들을 활용해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 Dify, n8n, ComfyUI 같은 AI 워크플로우 비주얼 빌더가 있다는 걸 알게 됐다. 직접 써보니 꽤 유용했다. 앞으로는 내가 매일 하는 일들을 이런 서비스로 자동화하는 과정을 공부하고, 그 과정을 블로그로 기록할 예정이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4편에 걸쳐 SnapAgent 프로젝트를 회고했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1편&lt;/b&gt;: 기획 배경, 매주 피드백을 받으며 방향을 찾아간 과정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2편&lt;/b&gt;: 팀 협업을 위한 base 컴포넌트 설계와 노드 생성 가이드 문서화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3편&lt;/b&gt;: 워크플로우 빌더의 포트 시스템을 만들고 사라진 과정과 재설계 분석&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4편&lt;/b&gt;: 5주간의 회고와 이후 계획&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주라는 짧은 시간 동안 기획부터 개발, 배포까지 경험할 수 있었다. 부족한 점도 많았지만, &quot;출시할 수 있는 프로덕트&quot;를 완성했다는 게 가장 큰 성과였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경험을 바탕으로 다음 프로젝트에서는 더 나은 구조와 협업 방식을 적용해보고 싶다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 발표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jungle.krafton.com/news/83&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[크래프톤 정글 뉴스 카드 &amp;mdash; Snap Agent 소개]&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ipPAZrdBSJM&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[최종 발표회 YouTube 영상]&lt;/a&gt;&lt;/p&gt;</description>
      <category>Projects</category>
      <category>SnapAgent</category>
      <category>나만무</category>
      <category>크래프톤정글</category>
      <category>회고</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/80</guid>
      <comments>https://onebrotravel.tistory.com/entry/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-%EB%82%98%EB%A7%8C%EB%AC%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-4#entry80comment</comments>
      <pubDate>Tue, 19 May 2026 22:47:05 +0900</pubDate>
    </item>
    <item>
      <title>[크래프톤 정글 10기] 나만무 프로젝트 회고 (3)</title>
      <link>https://onebrotravel.tistory.com/entry/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-10%EA%B8%B0-%EB%82%98%EB%A7%8C%EB%AC%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-3</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 SnapAgent의 기획 배경을, 2편에서 팀 협업을 위한 base 컴포넌트 설계와 노드 생성 가이드 문서화 과정을 다뤘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편에서는 내가 담당한 워크플로우 빌더 프론트엔드의 핵심 구현 이야기를 풀어보려 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;워크플로우 빌더 구현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주 프로젝트에서 워크플로우 빌더 프론트엔드를 맡았다. ReactFlow 기반의 노드-엣지 에디터를 만드는 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactFlow를 사용해본 적도, 노드-엣지 구조를 만들어본 적도 없었다. 1편에서 다뤘듯이 코치님이 n8n, Dify, Botpress를 레퍼런스로 추천해줬고, 분석 끝에 Dify를 메인 레퍼런스로 잡았다. 오픈소스라 GitHub 코드를 참고하기 편했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차에 들어가기 전에 ReactFlow docs와 Dify 코드를 보면서 베이스를 만들었다. 노드 타입 정의나 컴포넌트 구조 같은 패턴은 Dify를 참고했다. 2주차 초에 Start &amp;rarr; Knowledge Retrieval &amp;rarr; LLM &amp;rarr; End로 이어지는 RAG 샘플이 처음 화면에 떴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;main.png&quot; data-origin-width=&quot;2875&quot; data-origin-height=&quot;1622&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wAnhX/dJMcahkcEd1/jPDRA6M57YgK8VKGNtjJl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wAnhX/dJMcahkcEd1/jPDRA6M57YgK8VKGNtjJl0/img.png&quot; data-alt=&quot;초기 RAG 샘플 워크플로우 &amp;amp;mdash; 노드를 클릭하면 오른쪽에 설정 패널이 뜬다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wAnhX/dJMcahkcEd1/jPDRA6M57YgK8VKGNtjJl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwAnhX%2FdJMcahkcEd1%2FjPDRA6M57YgK8VKGNtjJl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2875&quot; height=&quot;1622&quot; data-filename=&quot;main.png&quot; data-origin-width=&quot;2875&quot; data-origin-height=&quot;1622&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;초기 RAG 샘플 워크플로우 &amp;mdash; 노드를 클릭하면 오른쪽에 설정 패널이 뜬다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;포트 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3주차에 포트 시스템을 만들었다. 노드에 포트를 시각적으로 표시하고, 사용자가 두 노드를 연결할 때 포트 타입이 호환되는지 검증하는 기능이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드는 이미 PortType 8가지(STRING, NUMBER, BOOLEAN, ARRAY, OBJECT, FILE, ANY 등)를 정의해두고 있었고, Dify도 같은 개념을 쓰고 있었다. 프론트엔드도 거기에 맞춰서 백엔드와 일치시키려고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초에는 연결되는 노드끼리의 타입이 맞지 않는다는 것을 시각적으로 사용자에게 표현하기 위해 노드를 연결할 때 검증하도록 만들었다. 노드마다 input/output type이 정해져 있으니 type이 다른 노드끼리는 연결할 수 없어야 한다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 만든 코드는 대략 이런 구조였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;PortIndicator&lt;/code&gt;, &lt;code&gt;PortHandle&lt;/code&gt;, &lt;code&gt;PortTooltip&lt;/code&gt;, &lt;code&gt;NodePort&lt;/code&gt; &amp;mdash; 노드에 포트를 시각적으로 표시하는 컴포넌트&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usePortConnection&lt;/code&gt; &amp;mdash; onConnect 시점에 포트끼리 연결을 검증하는 훅&lt;/li&gt;
&lt;li&gt;&lt;code&gt;portValidation&lt;/code&gt; &amp;mdash; 검증 로직 모음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;typeCompatibility&lt;/code&gt; &amp;mdash; 포트 타입 호환성 매트릭스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증은 세 단계로 짰다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;타입 호환성&lt;/b&gt; &amp;mdash; 연결하려는 두 포트의 타입이 호환되는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필수 입력 누락&lt;/b&gt; &amp;mdash; 노드에 필수 입력 포트가 비어 있는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다중 연결&lt;/b&gt; &amp;mdash; 같은 입력 포트에 이미 연결된 게 있는지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 셋을 다 통과해야 실제 엣지가 만들어졌다. 그렇지 않으면 연결 자체가 막히도록 설계했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사라진 포트 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;며칠 뒤, 새벽에 팀원의 commit이 들어왔다.&lt;/p&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;fix: 버그 해결

뭘 해결했는지 차차 알아 갈 예정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 commit에서 시각화 컴포넌트 4개(&lt;code&gt;PortIndicator&lt;/code&gt;, &lt;code&gt;PortHandle&lt;/code&gt;, &lt;code&gt;PortTooltip&lt;/code&gt;, &lt;code&gt;NodePort&lt;/code&gt;)와 연결 검증 훅(&lt;code&gt;usePortConnection&lt;/code&gt;)이 삭제됐다. 자리에 &lt;code&gt;InputMappingSection&lt;/code&gt;이라는 다른 컴포넌트가 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;portValidation&lt;/code&gt;과 &lt;code&gt;typeCompatibility&lt;/code&gt;는 파일로는 남았지만 어디서도 호출되지 않는 상태가 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 사라졌나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 commit이 들어오고 팀원에게 물어봤다. 내 코드가 삭제되었는데, 이 commit 메시지만으로는 왜 사라졌는지 알 수가 없었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원 말로는, 자기 파트를 구현하다 보니 자꾸 에러가 나는데 &lt;b&gt;어떤 코드인지 이해하는 건 나중에 하고, 일단 돌아가게 하려고 &lt;/b&gt;에러 나는 부분을 들어냈다는 거였다. &quot;뭘 해결했는지 차차 알아 갈 예정&quot;이라는 commit 메시지가 그래서 나온 거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정된 코드는 내 코드였으니 내가 가장 잘 알고 있었고, 내가 분석해서 설명해주기로 했다. 사실 처음에는 내 코드가 맞다는 걸 증명하고 싶다는 마음으로 분석했는데, 분석결과 결국 내 검증 시스템이 너무 빡빡해서 생기는 문제였다는 걸 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 포트 시스템을 만들 때 머릿속에 있던 노드는 처음에 만든 MVP용 RAG 샘플의 노드들 &amp;mdash; Start, Knowledge Retrieval, LLM, End &amp;mdash; 였다. 이 노드들은 input/output port가 처음부터 정해져 있고 모두 단순한 구조였다. 노드별 ports 메타데이터를 미리 정의해두고, 그걸로 검증하는 방식이 자연스러워 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 3주차 후반에 추가될 노드들이었다. IF/ELSE 노드는 사용자가 조건을 추가하면 output port가 늘어났다. 처음엔 두 개였다가, 조건을 추가하면 세 개, 네 개로 늘어났다. 노드별 ports를 미리 정의해두는 내 방식으로는 표현할 수 없는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ports 정의 자체가 없는 노드들도 있었다. 새로 추가된 노드 타입이거나, 다른 사람이 ports 메타데이터까지 신경 쓰지 않고 작업한 노드들. 내 시스템에서는 이런 노드는 연결 자체가 막혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필수 입력 검증도 문제였다. 사용자가 워크플로우를 만드는 도중에는 필수 입력 포트가 비어 있는 게 정상이다. 노드를 먼저 놓고 나중에 연결할 수도 있고, 임시로 연결을 끊었다가 다시 붙일 수도 있다. 그런데 내 시스템은 그런 진행 중 상태를 허용하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 검증 시스템을 만든 시점에는 이 문제들이 잘 안 보였다. 그때 캔버스에 띄워서 테스트한 건 MVP 단계의 RAG 샘플뿐이었다. RAG 샘플은 ports가 미리 정의된 정적인 노드들이고, 한 번 연결하면 그 상태로 끝이라 검증이 막을 일이 없었다. 사용자가 만들어가는 중인 워크플로우가 어떤 모양일지는 그때 잘 떠오르지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러다 보니 팀원이 새로 추가하는 노드를 바탕으로 워크플로우를 만들고 검증할 수 없어서 통째로 들어낸 거였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;재설계: Variable Mapping&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어내진 시스템 대신 들어온 코드는 두 가지였다. 노드 베이스 컴포넌트(&lt;code&gt;node.tsx&lt;/code&gt;)에서 input/output handle을 표시하는 로직이 단순해졌고, 새로 추가된 &lt;code&gt;InputMappingSection&lt;/code&gt;이라는 컴포넌트가 노드 패널에 들어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 단순해진 부분. 내 시스템에서는 노드별 ports 메타데이터를 미리 정의해두고, 그걸 보고 input handle을 표시할지 결정했다. 새 시스템에서는 이렇게 바뀌었다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const hasInputHandle = nodeType !== BlockEnum.Start;
const hasOutputHandle = nodeType !== BlockEnum.End;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Start 노드는 input이 없고, End 노드는 output이 없고, 나머지는 다 있다. 끝. 동적 output port 같은 건 핸들 자체에서는 신경 쓰지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 IF/ELSE 노드처럼 output이 여러 개 필요한 경우는 어떻게 풀까? 여기서 &lt;code&gt;InputMappingSection&lt;/code&gt;이 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;InputMappingSection&lt;/code&gt;은 노드 패널 안에 들어가는 컴포넌트다. 사용자가 노드를 클릭해서 설정 패널을 열면, 그 안에 &lt;b&gt;이 노드의 입력으로 어떤 변수를 쓸지&lt;/b&gt; 선택하는 picker가 뜬다. picker에서 변수를 선택하면 그 정보가 노드의 &lt;code&gt;variable_mappings&lt;/code&gt;라는 속성에 저장된다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;variable_mappings: {
  &quot;prompt&quot;: {
    target_port: &quot;prompt&quot;,
    source: [&quot;start_node_id&quot;, &quot;user_query&quot;]  // 어디서 가져올지
  },
  &quot;context&quot;: {
    target_port: &quot;context&quot;,
    source: [&quot;knowledge_node_id&quot;, &quot;results&quot;]
  },
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조의 핵심은 데이터 흐름이 엣지가 아니라 노드의 속성으로 옮겨졌다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 시스템에서는 &lt;b&gt;엣지 자체가 데이터 흐름&lt;/b&gt;이었다. 노드 A의 output port에서 노드 B의 input port로 엣지가 연결되면, 그 엣지를 통해 데이터가 흐르는 모델. 그래서 연결 시점에 두 포트가 호환되는지를 엄격하게 검증해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 시스템에서는 엣지는 &lt;b&gt;시각적 표시&lt;/b&gt;다. 실제 데이터 흐름은 각 노드가 자기 &lt;code&gt;variable_mappings&lt;/code&gt;에 어떤 변수에서 어떤 값을 가져올지 적어두는 방식. 엣지를 그리는 건 두 노드가 연결돼 있다는 걸 보여주는 정도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;모델비교_도식.png&quot; data-origin-width=&quot;1960&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctg1fe/dJMcaiDmtkE/9HuyG5iIxFSI69NFaBd80k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctg1fe/dJMcaiDmtkE/9HuyG5iIxFSI69NFaBd80k/img.png&quot; data-alt=&quot;Port 모델과 Variable Mapping 모델 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctg1fe/dJMcaiDmtkE/9HuyG5iIxFSI69NFaBd80k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctg1fe%2FdJMcaiDmtkE%2F9HuyG5iIxFSI69NFaBd80k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1960&quot; height=&quot;720&quot; data-filename=&quot;모델비교_도식.png&quot; data-origin-width=&quot;1960&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Port 모델과 Variable Mapping 모델 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석해보니 이 차이가 내 시스템의 문제들을 풀어주고 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IF/ELSE의 동적 output: &lt;code&gt;variable_mappings&lt;/code&gt;는 그냥 dict라서 ports 메타데이터에 의존하지 않는다. output이 두 개든 네 개든 상관없다.&lt;/li&gt;
&lt;li&gt;ports 정의 없는 노드: picker는 &lt;b&gt;현재 그래프에서 가져올 수 있는 변수들&lt;/b&gt;을 보여주는 방식이라, 노드별 ports 메타데이터가 없어도 작동한다.&lt;/li&gt;
&lt;li&gt;진행 중 상태: &lt;code&gt;variable_mappings&lt;/code&gt;가 비어 있어도 노드는 살아있을 수 있다. 사용자가 천천히 채우면 된다.&lt;/li&gt;
&lt;li&gt;검증의 시점: 변수를 선택할 때 picker가 type compatibility를 확인한다. 연결 시점에는 아무것도 검증하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 팀원이 처음부터 이런 모델을 의도해서 들어낸 건 아니었다. 어떤 코드인지 이해는 나중에 하고 일단 돌아가게 하려고 빡빡한 검증 부분을 들어낸 게 이렇게 된 거였다. 하지만 분석해보니 들어내고 남은 부분이 더 적합한 모델이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 워크플로우 빌더의 포트 시스템을 만들고, 그게 며칠 뒤에 들어내지고, 재설계를 분석한 경험을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 시스템의 차이는 검증을 &lt;b&gt;어디에서 할 것인가&lt;/b&gt;에 있었다. 내 시스템은 연결 시점에서 그래프 단위로 검증했고, 재설계된 시스템은 변수 선택 시점에서 컴포넌트 단위로 검증한다. 워크플로우를 만들어가는 중인 상태를 허용하려면 후자가 더 적합했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 돌아보면 내가 처음에 그렸던 &lt;b&gt;타입이 다르면 아예 연결 자체를 막는 방식&lt;/b&gt;을 채택하는 서비스도 있다. 노드-엣지 구조로 이미지를 생성하는 ComfyUI 같은 도구가 그렇다. 써본 기억으로는 거기서도 타입이 안 맞으면 엣지가 연결되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDrvdc/dJMcaf7GxXc/DPVtoYKBiWH68QjcK5eVsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDrvdc/dJMcaf7GxXc/DPVtoYKBiWH68QjcK5eVsk/img.png&quot; data-origin-width=&quot;1429&quot; data-origin-height=&quot;509&quot; data-is-animation=&quot;false&quot; style=&quot;width: 55.9863%; margin-right: 10px;&quot; data-widthpercent=&quot;56.65&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDrvdc/dJMcaf7GxXc/DPVtoYKBiWH68QjcK5eVsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDrvdc%2FdJMcaf7GxXc%2FDPVtoYKBiWH68QjcK5eVsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1429&quot; height=&quot;509&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9lHhD/dJMcahLeQH4/4gnA3gwHyPpmrz4w10d9z0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9lHhD/dJMcahLeQH4/4gnA3gwHyPpmrz4w10d9z0/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;410&quot; data-filename=&quot;interface.d36f3ce2.jpg&quot; style=&quot;width: 42.8509%;&quot; data-widthpercent=&quot;43.35&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9lHhD/dJMcahLeQH4/4gnA3gwHyPpmrz4w10d9z0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9lHhD%2FdJMcahLeQH4%2F4gnA3gwHyPpmrz4w10d9z0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;881&quot; height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;왼 - Dify의 노드 / 우 - ComfyUI의 노드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 만드는 서비스는 ComfyUI보다 Dify 쪽에 가까웠다. 이미지 생성처럼 완성된 그래프를 실행하는 도구가 아니라, 사용자가 AI 에이전트의 워크플로우를 점진적으로 빌드하는 도구. IF/ELSE 노드의 동적 output처럼 그래프가 만들어지는 도중에 모양이 바뀌는 상황이 자연스럽게 일어났다. 우리 서비스에 맞는 방향은 ComfyUI 쪽이 아니라 Dify 쪽이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원이 내 코드를 들어낸 commit은 결국 내 시스템의 문제를 가장 빠르게 알려준 피드백이었다. 분석을 마친 뒤에는 내가 만든 시스템에 너무 집착하지 않고 들어낸 것을 받아들였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 5주차 발표 직전과 발표 이후의 회고를 다룰 예정이다.&lt;/p&gt;</description>
      <category>Projects</category>
      <category>SnapAgent</category>
      <category>나만무</category>
      <category>크래프톤정글</category>
      <category>회고</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/79</guid>
      <comments>https://onebrotravel.tistory.com/entry/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-10%EA%B8%B0-%EB%82%98%EB%A7%8C%EB%AC%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-3#entry79comment</comments>
      <pubDate>Tue, 19 May 2026 22:46:34 +0900</pubDate>
    </item>
    <item>
      <title>[크래프톤 정글 10기] 나만무 프로젝트 회고 (2)</title>
      <link>https://onebrotravel.tistory.com/entry/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-10%EA%B8%B0-%EB%82%98%EB%A7%8C%EB%AC%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-2</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서는 SnapAgent의 기획 배경과 기술 스택 선정 과정을 다뤘다. 비개발자에서 개발자로 타겟을 전환한 이유, 5주간의 타임라인, 그리고 팀의 기술적 배경을 고려한 스택 선정 과정을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편에서는 팀 협업 이야기를 해보려 한다. 5명이서 워크플로우 에디터를 개발하면서, 어떻게 하면 서로의 작업이 충돌하지 않고 효율적으로 진행될 수 있을지 고민한 과정을 다룬다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SnapAgent가 어떻게 동작하는가&lt;/li&gt;
&lt;li&gt;팀 협업을 위한 base 컴포넌트 설계&lt;/li&gt;
&lt;li&gt;노드 생성 가이드 문서화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SnapAgent가 어떻게 동작하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적인 구현 이야기에 앞서, SnapAgent가 어떻게 동작하는지 간단히 소개한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;시작 - 지식 검색 - LLM - 종료&quot; 노드로 구성된 워크플로우 기준으로, 사용자가 채팅창에 메시지를 입력하면 백엔드에서 워크플로우가 실행된다. 이때 &lt;b&gt;SSE(Server-Sent Events)&lt;/b&gt; 를 통해 각 노드의 실행 상태와 LLM 응답이 실시간으로 프론트엔드에 전달된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;5420&quot; data-origin-height=&quot;8105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbXGlw/dJMcabj0iZC/kd4OcGvMXy0C42Md7WQ4uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbXGlw/dJMcabj0iZC/kd4OcGvMXy0C42Md7WQ4uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbXGlw/dJMcabj0iZC/kd4OcGvMXy0C42Md7WQ4uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbXGlw%2FdJMcabj0iZC%2Fkd4OcGvMXy0C42Md7WQ4uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;5420&quot; height=&quot;8105&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;5420&quot; data-origin-height=&quot;8105&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;노드 실행 상태 시각화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우가 실행되면 각 노드의 테두리 색상이 변한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;성공&lt;/b&gt;: 초록색 테두리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실패&lt;/b&gt;: 빨간색 테두리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.gif&quot; data-origin-width=&quot;2874&quot; data-origin-height=&quot;1624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxASyl/dJMcaak2eJN/sMAnIq4vL3II2fKrQuje11/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxASyl/dJMcaak2eJN/sMAnIq4vL3II2fKrQuje11/img.gif&quot; data-alt=&quot;노드_실행상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxASyl/dJMcaak2eJN/sMAnIq4vL3II2fKrQuje11/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cxASyl/dJMcaak2eJN/sMAnIq4vL3II2fKrQuje11/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2874&quot; height=&quot;1624&quot; data-filename=&quot;image.gif&quot; data-origin-width=&quot;2874&quot; data-origin-height=&quot;1624&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;노드_실행상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 현재 어떤 노드가 실행 중인지, 어디서 에러가 발생했는지 한눈에 파악할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM 응답 스트리밍&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 노드가 실행되면 응답이 한 번에 표시되지 않고, 타이핑하듯 한 글자씩 나타난다. 백엔드에서 토큰 단위로 스트리밍되는 응답을 받아서 프론트엔드에서 렌더링하는 방식이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 워크플로우 에디터를 어떻게 구현했는지 이야기해보자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팀 협업을 위한 base 컴포넌트 설계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하드코딩의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 언급했듯이, 초반에는 &quot;시작 &amp;rarr; 지식 검색 &amp;rarr; LLM &amp;rarr; 종료&quot; 4개 노드가 하드코딩된 상태였다. MVP를 빠르게 만들어서 발표에서 서비스에 대한 피드백을 받는 게 우선이었기 때문에, 확장성보다는 속도를 택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 타겟을 개발자로 전환하면서 상황이 달라졌다. 빈 워크플로우에서 시작해 사용자가 직접 노드를 추가하는 방식으로 바뀌니, IF-ELSE, Question Classifier 등 다양한 노드가 필요해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 하드코딩 구조로는 노드 하나 추가할 때마다 중복 코드가 쌓일 게 뻔했다. 게다가 분업을 위해서는 5명이서 각자 다른 노드를 개발해야 하는데, 두 가지 문제가 예상됐다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;UX 일관성&lt;/b&gt;: 노드마다 스타일이 달라지면 사용자 경험이 깨진다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발 병목&lt;/b&gt;: 매번 &quot;이거 어떻게 구현해요?&quot; 질문이 오면 내가 병목이 된다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 기존 하드코딩 구조를 걷어내고, 공통 컴포넌트 기반으로 리팩토링하기로 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결책: base 공통 컴포넌트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 노드가 공유하는 공통 요소를 &lt;code&gt;_base&lt;/code&gt; 폴더에 분리했다. 이때 팀에서 이미 합의된 Feature 기반 디렉토리 구조에 따라, &lt;code&gt;_base&lt;/code&gt; 폴더는 &lt;code&gt;features/workflow/components/nodes/_base&lt;/code&gt; 위치에 자리잡았다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BaseNode &amp;mdash; 노드 래퍼 컴포넌트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 노드의 공통 래퍼로서 다음을 자동 처리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;240px 고정 폭으로 일관된 크기&lt;/li&gt;
&lt;li&gt;실행 상태에 따른 테두리 색상 변화 (성공: 초록, 실패: 빨강)&lt;/li&gt;
&lt;li&gt;Validation 에러 시 빨간 &quot;오류&quot; 뱃지 표시&lt;/li&gt;
&lt;li&gt;입출력 핸들(Handle) 자동 배치&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// _base/node.tsx (핵심 부분)
type BaseNodeProps = {
  id: string;
  data: NodeProps['data'];
  children: ReactElement;
  selected?: boolean;
};

const BaseNode = ({ id, data, children, selected }: BaseNodeProps) =&amp;gt; {
  const hasValidationError = useWorkflowStore((state) =&amp;gt;
    state.validationErrorNodeIds.includes(id)
  );

  const { showRunningBorder, showSuccessBorder, showFailedBorder } =
    useMemo(() =&amp;gt; ({
      showRunningBorder: data._runningStatus === NodeRunningStatus.Running,
      showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded,
      showFailedBorder: data._runningStatus === NodeRunningStatus.Failed,
    }), [data._runningStatus]);

  return (
    &amp;lt;div
      className={clsx(
        'w-[240px] rounded-[15px] border-2',
        selected &amp;amp;&amp;amp; 'border-blue-500',
        hasValidationError &amp;amp;&amp;amp; 'border-red-400',
        showSuccessBorder &amp;amp;&amp;amp; 'border-green-500',
        showFailedBorder &amp;amp;&amp;amp; 'border-red-500',
      )}
    &amp;gt;
      {hasValidationError &amp;amp;&amp;amp; (
        &amp;lt;div className=&quot;absolute -top-2 right-3 rounded-full bg-red-500 px-2 text-xs text-white&quot;&amp;gt;
          오류
        &amp;lt;/div&amp;gt;
      )}
      &amp;lt;NodeTargetHandle /&amp;gt;
      &amp;lt;NodeSourceHandle /&amp;gt;
      {children}
    &amp;lt;/div&amp;gt;
  );
};

export default memo(BaseNode);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원은 BaseNode 안에 들어갈 내부 컨텐츠만 구현하면 된다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;const MyNewNode = ({ data }: NodeProps) =&amp;gt; (
  &amp;lt;div className=&quot;px-3 py-2&quot;&amp;gt;
    {/* BaseNode가 래핑해주므로 내부만 신경쓰면 됨 */}
  &amp;lt;/div&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BasePanel &amp;mdash; 설정 패널 컴포넌트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드를 클릭하면 오른쪽에 설정 패널이 나타난다. 이 패널의 공통 UI도 분리했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더 (노드 아이콘 + 제목 + 닫기 버튼)&lt;/li&gt;
&lt;li&gt;설명 입력 필드&lt;/li&gt;
&lt;li&gt;탭 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(TODO_BasePanel_스크린샷)&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;// _base/panel.tsx (핵심 부분)
interface BasePanelProps {
  children: React.ReactNode;
}

export const BasePanel = ({ children }: BasePanelProps) =&amp;gt; {
  const { selectedNodeId, nodes, updateNode, selectNode } = useWorkflowStore();
  const node = nodes.find((n) =&amp;gt; n.id === selectedNodeId);

  if (!node) return null;

  return (
    &amp;lt;div className=&quot;flex flex-col h-full&quot;&amp;gt;
      {/* 헤더: 아이콘 + 제목 + 닫기 버튼 */}
      &amp;lt;div className=&quot;flex items-center justify-between px-4 py-3 border-b&quot;&amp;gt;
        &amp;lt;div className=&quot;flex items-center gap-2&quot;&amp;gt;
          &amp;lt;BlockIcon type={node.data.type} size=&quot;sm&quot; /&amp;gt;
          &amp;lt;h3 className=&quot;font-semibold&quot;&amp;gt;{node.data.title}&amp;lt;/h3&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; selectNode(null)}&amp;gt;
          &amp;lt;X className=&quot;w-4 h-4&quot; /&amp;gt;
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;

      {/* 설명 입력 필드 */}
      &amp;lt;div className=&quot;px-4 py-2 border-b&quot;&amp;gt;
        &amp;lt;Input
          value={node.data.desc || ''}
          onChange={(e) =&amp;gt; updateNode(selectedNodeId!, { desc: e.target.value })}
          placeholder=&quot;설명 추가...&quot;
        /&amp;gt;
      &amp;lt;/div&amp;gt;

      {/* 탭 UI */}
      &amp;lt;div className=&quot;border-b&quot;&amp;gt;
        &amp;lt;button className=&quot;px-4 py-2 text-sm font-medium border-b-2 border-blue-600&quot;&amp;gt;
          설정
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;

      {/* 노드별 설정 내용 */}
      &amp;lt;div className=&quot;flex-1 overflow-y-auto p-4&quot;&amp;gt;
        {children}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;레이아웃 컴포넌트 &amp;mdash; Box, Group, Field&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패널 내부 UI를 일관되게 구성하기 위한 레이아웃 컴포넌트도 만들었다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;Box&amp;gt;
  &amp;lt;Group title=&quot;모델 설정&quot; description=&quot;사용할 AI 모델을 선택하세요&quot;&amp;gt;
    &amp;lt;Field label=&quot;Provider&quot; required&amp;gt;
      &amp;lt;Select /&amp;gt;
    &amp;lt;/Field&amp;gt;
    &amp;lt;Field label=&quot;Model&quot; required&amp;gt;
      &amp;lt;Select /&amp;gt;
    &amp;lt;/Field&amp;gt;
  &amp;lt;/Group&amp;gt;
&amp;lt;/Box&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Box &amp;gt; Group &amp;gt; Field 계층으로 조합하면, 어떤 노드의 패널이든 일관된 레이아웃이 나온다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보이지 않는 작업의 교훈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;base 컴포넌트로 리팩토링한 주의 발표 피드백은 뼈아팠다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-quote&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;발표 피드백&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 주 발표랑 차이가 없다. 여전히 4개 노드밖에 없는데 1주일 동안 뭐했냐?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 그 주에는 base 컴포넌트 리팩토링만 한 게 아니었다. 타겟을 비개발자에서 개발자로 전환하고, IF-ELSE / Question Classifier / Variable Assigner / Answer 등 새 노드들도 같이 작업하고 있었다. 다만 그 작업들이 내부 구조에 묶여 있어서 발표 시점에 눈에 보이는 결과물로는 드러나지 않았다. 리팩토링의 가치는 당장 눈에 보이지 않는다는 걸 체감한 순간이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과: 다음 주의 폭발적 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 base 컴포넌트의 진가는 그 다음 주에 드러났다. 공통 구조가 잡혀 있으니 위에서 말한 새 노드들에 더해 Tavily, 마켓플레이스 템플릿 노드 등 다양한 노드를 빠르게 추가할 수 있었다. 팀원들도 각자 노드를 개발할 때 공통 스타일과 기능이 자동 적용되니, 노드별 고유 로직에만 집중할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4주차 발표에서야 &quot;발표 시나리오는 다듬어야겠지만 서비스는 잘 만든 것 같다&quot;는 피드백을 받을 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;노드 생성 가이드 문서화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계기: 폴더 위치가 달랐다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;base 컴포넌트를 만들고 나서, 팀원이 Variable Assigner 노드를 구현했다. 그런데 코드를 보니 다른 노드들이 있는 폴더가 아니라 엉뚱한 위치에 파일이 있었다. 엉뚱한 위치에 있는 노드의 위치를 옮기면서 생각했다. 내가 처음부터 안내를 잘 했으면 일을 두 번 할 필요가 없었을 텐데.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공통 컴포넌트가 있어도, 새 노드를 추가하려면 여전히 여러 파일을 수정해야 했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;BlockEnum에 타입 추가&lt;/li&gt;
&lt;li&gt;노드 타입 정의 생성&lt;/li&gt;
&lt;li&gt;노드 컴포넌트 작성&lt;/li&gt;
&lt;li&gt;패널 컴포넌트 작성&lt;/li&gt;
&lt;li&gt;컴포넌트 맵에 등록&lt;/li&gt;
&lt;li&gt;컨텍스트 메뉴에 추가&lt;/li&gt;
&lt;li&gt;아이콘 추가&lt;/li&gt;
&lt;li&gt;기본값 정의 (선택)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 모르면 &quot;다음에 뭘 해야 하지?&quot;에서 막히거나, 폴더 위치처럼 사소하지만 중요한 부분을 놓치게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;나 없이도 돌아가는 구조&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드를 대폭 추가해야 하는 상황에서, 매번 질문에 답하다 보면 내가 병목이 될 게 뻔했다. 그래서 노드 생성 가이드 문서를 작성하기로 했다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-important&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;가이드 문서의 목표&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 없을 때도 팀원이 막힘없이 노드를 추가할 수 있어야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표는 단순했다. &lt;b&gt;내가 없을 때도 팀원이 막힘없이 노드를 추가할 수 있어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문서 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서는 다음과 같이 구성했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;8단계 체크리스트&lt;/b&gt; &amp;mdash; 순서대로 따라하면 완성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;각 단계별 상세 설명&lt;/b&gt; &amp;mdash; 파일 경로, 코드 예시 포함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의사항&lt;/b&gt; &amp;mdash; TypeScript strict 모드, 상태 관리 규칙 등&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증 체크리스트&lt;/b&gt; &amp;mdash; 노드 추가 후 확인할 항목&lt;/li&gt;
&lt;li&gt;&lt;b&gt;참고 자료&lt;/b&gt; &amp;mdash; 난이도별 기존 노드 예시 (간단/설정있음/복잡)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 난이도별 예시를 넣은 이유는, 새 노드를 만들 때 비슷한 복잡도의 기존 노드를 참고하면 훨씬 빠르기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서를 공유한 후 변화가 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀원이 새 노드 추가 시 질문 없이 진행 가능해짐&lt;/li&gt;
&lt;li&gt;코드 리뷰 시 &quot;가이드 몇 단계 빠뜨렸네요&quot;로 피드백 간소화&lt;/li&gt;
&lt;li&gt;내가 다른 작업에 집중할 시간 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 문서를 작성하는 데 1~2시간 정도 걸렸지만, 이후 절약된 시간을 생각하면 충분히 가치 있는 투자였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편에서는 워크플로우 빌더의 포트 시스템을 만들고 사라진 과정, 그리고 재설계를 분석한 경험을 다룰 예정이다.&lt;/p&gt;</description>
      <category>Projects</category>
      <category>SnapAgent</category>
      <category>크래프톤정글</category>
      <category>회고</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/78</guid>
      <comments>https://onebrotravel.tistory.com/entry/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-10%EA%B8%B0-%EB%82%98%EB%A7%8C%EB%AC%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-2#entry78comment</comments>
      <pubDate>Tue, 19 May 2026 22:46:23 +0900</pubDate>
    </item>
    <item>
      <title>[크래프톤 정글 10기] 나만무 프로젝트 회고 (1)</title>
      <link>https://onebrotravel.tistory.com/entry/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-10%EA%B8%B0-%EB%82%98%EB%A7%8C%EB%AC%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-1</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5개월간의 크래프톤 정글 여정의 마지막 관문, &quot;나만의 무기 만들기(나만무)&quot; 프로젝트. 5주라는 시간 동안 팀원들과 함께 실제 서비스를 기획하고 개발해야 했다. 우리 팀이 선택한 주제는 &lt;b&gt;AI 에이전트 워크플로우 빌더&lt;/b&gt;, 프로젝트명은 &lt;b&gt;SnapAgent&lt;/b&gt;였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GitHub (Frontend)&lt;/b&gt;: &lt;a href=&quot;https://github.com/Krafton-Jungle10-Team4/Frontend&quot;&gt;https://github.com/Krafton-Jungle10-Team4/Frontend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub (Backend)&lt;/b&gt;: &lt;a href=&quot;https://github.com/Krafton-Jungle10-Team4/Backend&quot;&gt;https://github.com/Krafton-Jungle10-Team4/Backend&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCQXm1/dJMcaglb7ev/lKOFGqoKMqaH2K7HZv4wr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCQXm1/dJMcaglb7ev/lKOFGqoKMqaH2K7HZv4wr0/img.png&quot; data-origin-width=&quot;2558&quot; data-origin-height=&quot;1270&quot; data-is-animation=&quot;false&quot; data-filename=&quot;landing_page.png&quot; style=&quot;width: 49.4865%; margin-right: 10px;&quot; data-widthpercent=&quot;50.07&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCQXm1/dJMcaglb7ev/lKOFGqoKMqaH2K7HZv4wr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCQXm1%2FdJMcaglb7ev%2FlKOFGqoKMqaH2K7HZv4wr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2558&quot; height=&quot;1270&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGSZsw/dJMcaa6n6ba/TD9UJfBQPxHEMnktsVEt6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGSZsw/dJMcaa6n6ba/TD9UJfBQPxHEMnktsVEt6k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2555&quot; data-origin-height=&quot;1272&quot; data-filename=&quot;final.png&quot; style=&quot;width: 49.3507%;&quot; data-widthpercent=&quot;49.93&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGSZsw/dJMcaa6n6ba/TD9UJfBQPxHEMnktsVEt6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGSZsw%2FdJMcaa6n6ba%2FTD9UJfBQPxHEMnktsVEt6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2555&quot; height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;SnapAgent 인트로/메인 이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기획 배경: 아이디어가 나오기까지&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째 아이디어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 우리 팀이 가져간 아이디어는 &lt;b&gt;GitHub 레포지토리 시각화 도구&lt;/b&gt;였다. 사용자가 GitHub URL을 입력하면 해당 레포지토리의 아키텍처를 시각적으로 재구성해주는 서비스를 구상했다. 이 아이디어는 &lt;a href=&quot;https://deepwiki.org/&quot;&gt;deepwiki&lt;/a&gt;를 레퍼런스로 삼았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코치님의 피드백은 냉정했다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-quote&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;코치님의 피드백&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 기준으로 시각적으로 정렬할 것인지 모르겠지만, 단순히 폴더 구조를 시각적으로 재구성하는 것은 이미 옛날부터 존재하는 구식 아이디어예요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 모듈 기준으로 분석할 건지, 클래스 기준으로 분석할 건지, 함수 기준으로 분석할 건지에 대한 아이디어가 구체적이지 않다는 피드백이었다. 만약 함수를 기준으로 호출 플로우를 분석한다면 비동기 함수를 포함하는 복잡한 플로우는 분석하기 어려울 것이고, 폴더만을 기준으로 시각화한다면 그건 너무 단순한 프로젝트가 될 것이라는 지적도 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로운 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코치님이 대안으로 제시한 방향은 이랬다.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-callout markdown-callout-quote&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p class=&quot;callout-title&quot; data-ke-size=&quot;size16&quot;&gt;코치님의 제안&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 langchain으로 AI agent를 구축하려는 시도가 굉장히 늘고 있는데, 그걸 시각적으로 표현하는 툴이 너무 약해요. 이 주제가 더 트렌디하다고 생각하는데 어때요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 힌트를 바탕으로 우리는 &lt;a href=&quot;https://dify.ai/&quot;&gt;&lt;b&gt;Dify&lt;/b&gt;&lt;/a&gt;, &lt;a href=&quot;https://n8n.io/?ps_partner_key=NTI5MzI0MWI1N2Y4&amp;amp;ps_xid=HI7TxFCCIWL4CH&amp;amp;gsxid=HI7TxFCCIWL4CH&amp;amp;gspk=NTI5MzI0MWI1N2Y4&amp;amp;gad_source=1&amp;amp;gad_campaignid=23397401030&amp;amp;gbraid=0AAAABCODLjuDHs7IMWZDn9vWZD8irecCo&amp;amp;gclid=CjwKCAjw5ZXQBhBdEiwAI5XVWXSuwOfOjxn3elfarnRQyjkfF9OxjMh-DVlIFK035v9EADhMktxJ7xoClhIQAvD_BwE&quot;&gt;&lt;b&gt;n8n&lt;/b&gt;&lt;/a&gt; 같은 시각적 AI 워크플로우 빌더를 조사하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀원들은 모두 AI 관련 프레임워크를 처음 접하는 사람들뿐이었다. 이때부터 코치님께 RAG 파이프라인의 플로우에 대해 질문하거나 관련 도서를 추천받아서, 팀원들끼리 LLM과 AI 워크플로우 구축 방법을 따로 학습하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 과정을 거치면서 최종적으로 우리의 방향이 정해졌다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;노코드/로우코드로 AI 워크플로우를 시각적으로 구축하고, 원클릭으로 배포할 수 있는 서비스를 만들자.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방향을 찾기까지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 최종 방향이 바로 정해진 건 아니었다. 매주 발표와 피드백을 거치며 서비스의 형태가 계속 바뀌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;b&gt;비개발자&lt;/b&gt;를 타겟으로 잡았다. 그런데 노드-엣지 구조는 비개발자가 다루기엔 어려울 것이라는 의견이 팀 내에서 지배적이었다. 그래서 1주차에는 노드-엣지 구조 대신 폼 형식으로 데이터를 입력받아 RAG 파이프라인을 자동 생성하고, 프롬프트 테스트로 빠르게 프로토타이핑할 수 있는 서비스를 기획했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 피드백은 &quot;이 서비스를 누가 사용할지에 대한 고민이 없는 것 같다.&quot;, &quot;타겟 유저가 애매하다&quot;였다. 추가로 &quot;도메인만 AI로 바뀌었지, 기획 단계에서 얘기했던 &lt;b&gt;시각화&lt;/b&gt; 아이디어는 사라졌다&quot;는 피드백도 받았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ct93M7/dJMcab5gNXA/GiPs73ZHeZiGuzy3inLbLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ct93M7/dJMcab5gNXA/GiPs73ZHeZiGuzy3inLbLk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;1556&quot; data-filename=&quot;before1.png&quot; style=&quot;width: 48.9928%; margin-right: 10px;&quot; data-widthpercent=&quot;49.57&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ct93M7/dJMcab5gNXA/GiPs73ZHeZiGuzy3inLbLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fct93M7%2FdJMcab5gNXA%2FGiPs73ZHeZiGuzy3inLbLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2844&quot; height=&quot;1556&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k4A1J/dJMcaipNB6p/Qe9CtDlbyJY4nFHMOAjn0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k4A1J/dJMcaipNB6p/Qe9CtDlbyJY4nFHMOAjn0K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1443&quot; data-origin-height=&quot;776&quot; data-filename=&quot;before2.png&quot; style=&quot;width: 49.8444%;&quot; data-widthpercent=&quot;50.43&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k4A1J/dJMcaipNB6p/Qe9CtDlbyJY4nFHMOAjn0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk4A1J%2FdJMcaipNB6p%2FQe9CtDlbyJY4nFHMOAjn0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1443&quot; height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;1주차 폼 형식 RAG 서비스 프로토타입&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차에는 시각화 피드백을 수용해 Dify를 참고한 노드-엣지 구조를 도입했다. 하지만 비개발자 타겟은 쉽게 포기할 수 없었다. 결국 폼으로 입력받으면 &quot;시작 &amp;rarr; Knowledge &amp;rarr; LLM &amp;rarr; 종료&quot; 워크플로우가 자동 생성되는 절충안을 선택했다. MVP를 빠르게 검증하기 위해 이 4개 노드는 하드코딩된 상태였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZrOiZ/dJMcaayx8Y7/Wah6L03baRM2mfw7fvFndK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZrOiZ/dJMcaayx8Y7/Wah6L03baRM2mfw7fvFndK/img.png&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1200&quot; data-is-animation=&quot;false&quot; data-filename=&quot;form.png&quot; style=&quot;width: 46.8908%; margin-right: 10px;&quot; data-widthpercent=&quot;47.44&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZrOiZ/dJMcaayx8Y7/Wah6L03baRM2mfw7fvFndK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZrOiZ%2FdJMcaayx8Y7%2FWah6L03baRM2mfw7fvFndK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bf5nj/dJMcafs3Gsl/vcarT0mg356C4PeNGG32q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bf5nj/dJMcafs3Gsl/vcarT0mg356C4PeNGG32q1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2875&quot; data-origin-height=&quot;1622&quot; data-filename=&quot;main.png&quot; style=&quot;width: 51.9464%;&quot; data-widthpercent=&quot;52.56&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bf5nj/dJMcafs3Gsl/vcarT0mg356C4PeNGG32q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBf5nj%2FdJMcafs3Gsl%2FvcarT0mg356C4PeNGG32q1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2875&quot; height=&quot;1622&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;2주차 노드-엣지 도입&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 이전과 같은 피드백을 받았다. &quot;여전히 이 서비스의 타겟층을 잘 모르겠다&quot;는 것이었다. 또한 비개발자를 위해 선택한 &quot;폼을 입력하면 자동으로 생성되는 워크플로우&quot;에 대해서는 &quot;노드를 사용자가 직접 만들어야 하는데 왜 자동 생성되냐&quot;, &quot;서비스의 사용 플로우가 어색하다&quot;는 피드백을 받았다. 결국 우리팀은 &lt;b&gt;시각화를 위한 노드-엣지 구조와 비개발자 타겟은 양립하기 어렵다&lt;/b&gt;는 결론을 내렸다. 3주차에 &lt;b&gt;타겟을 개발자로 완전히 전환&lt;/b&gt;하고, 빈 워크플로우에서 시작해 사용자가 직접 노드를 추가하는 방식으로 확정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때부터 프로젝트명도 &lt;b&gt;SnapAgent&lt;/b&gt;로 정해졌다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;최종 발표&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jungle.krafton.com/news/83&quot;&gt;[크래프톤 정글 뉴스 카드 &amp;mdash; Snap Agent 소개]&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ipPAZrdBSJM&quot;&gt;[최종 발표회 YouTube 영상]&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=ipPAZrdBSJM&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/bS5vjd/dJMb84qfznz/9zq9aSm7z3dxwbMkStkx80/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/gxTaq/dJMb84qfzny/1zks5nqSlbucY89PSArlA0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/qX253/dJMb85W0jBp/TQdQawNyKTbLaJmemSblK0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;SnapAgent: 쉽고 빠른 RAG 파이프라인 및 AI Agent 구축/배포 서비스 | 크래프톤 정글 10기&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/ipPAZrdBSJM&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;팀 협업 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5명이 5주간 협업하면서 &lt;b&gt;Notion&lt;/b&gt;을 적극 활용했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;칸반 보드&lt;/b&gt; &amp;mdash; 태스크 관리 및 진행 상황 공유&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공유 문서&lt;/b&gt; &amp;mdash; 기술 조사 내용, 회의록, API 명세 등 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd7uqJ/dJMcad22gUh/a1yBYfSCkGSDKdz8RDiCcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd7uqJ/dJMcad22gUh/a1yBYfSCkGSDKdz8RDiCcK/img.png&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;1269&quot; data-is-animation=&quot;false&quot; data-filename=&quot;notion.png&quot; style=&quot;width: 49.4187%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd7uqJ/dJMcad22gUh/a1yBYfSCkGSDKdz8RDiCcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd7uqJ%2FdJMcad22gUh%2Fa1yBYfSCkGSDKdz8RDiCcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1277&quot; height=&quot;1269&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nowiv/dJMcacpANcJ/P3cwXOKxY7MKgGHtbSSF8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nowiv/dJMcacpANcJ/P3cwXOKxY7MKgGHtbSSF8k/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;1270&quot; data-filename=&quot;notion2.png&quot; style=&quot;width: 49.4185%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nowiv/dJMcacpANcJ/P3cwXOKxY7MKgGHtbSSF8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnowiv%2FdJMcacpANcJ%2FP3cwXOKxY7MKgGHtbSSF8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1278&quot; height=&quot;1270&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;Notion 공유 문서 &amp;mdash; API 명세/회의록, Notion 칸반 보드 &amp;mdash; 태스크 관리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 스택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이번 프로젝트에서 React Flow 기반 워크플로우 에디터 프론트엔드를 담당했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 전반적으로 비전공자로 구성되었고, 특히 프론트엔드 경험자가 없었다. 그래서 문서화가 잘 되어 있고 초기 러닝커브가 적은 스택을 우선적으로 선택했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Frontend&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;React&lt;/b&gt; + &lt;b&gt;TypeScript&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나만무를 시작하기 전에 JavaScript가 TypeScript보다 러닝커브가 낮다는 말을 많이 들었다. 그런데 나만무 직전까지 C 언어를 사용하다 보니 타입이 명시되는 TypeScript가 오히려 더 익숙해서 함께 채택했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;React Flow&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드 기반 워크플로우 에디터의 핵심. 레퍼런스 프로젝트인 Dify도 이 기술 스택을 차용하고 있어서 채택했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Zustand&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가벼운 전역 상태 관리. 이것도 Redux 대비 학습 난이도가 낮아서 빠르게 우리 프로젝트에 도입하기 위해 채택했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Backend&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Python&lt;/b&gt; + &lt;b&gt;FastAPI&lt;/b&gt; &amp;mdash; 비동기 처리와 자동 API 문서화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQLAlchemy&lt;/b&gt; + &lt;b&gt;PostgreSQL&lt;/b&gt; &amp;mdash; ORM과 관계형 데이터베이스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pgvector&lt;/b&gt; &amp;mdash; PostgreSQL 확장으로 벡터 검색 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis&lt;/b&gt; &amp;mdash; 응답 캐싱으로 비용&amp;middot;속도 최적화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Alembic&lt;/b&gt; &amp;mdash; DB 마이그레이션 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OpenAI API&lt;/b&gt; &amp;mdash; GPT 모델 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS Bedrock&lt;/b&gt; &amp;mdash; Titan Embeddings로 문서 임베딩&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Infrastructure&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Vercel&lt;/b&gt; &amp;mdash; 프론트엔드 배포&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS ECS&lt;/b&gt; &amp;mdash; 백엔드 배포&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Docker Compose&lt;/b&gt; &amp;mdash; 로컬 개발 환경 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 타임라인&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주차&lt;/th&gt;
&lt;th&gt;계획&lt;/th&gt;
&lt;th&gt;실제 진행&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1주차&lt;/td&gt;
&lt;td&gt;주제 선정 &amp;amp; 기획&lt;/td&gt;
&lt;td&gt;주제 선정 &amp;amp; 기획 (폼 형식 RAG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2주차&lt;/td&gt;
&lt;td&gt;MVP 개발&lt;/td&gt;
&lt;td&gt;노드-엣지 도입, 하드코딩된 워크플로우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3주차&lt;/td&gt;
&lt;td&gt;MVP 추가 개발&lt;/td&gt;
&lt;td&gt;타겟 전환, &lt;code&gt;_base&lt;/code&gt; 컴포넌트 리팩토링, &lt;b&gt;노드 4종(IF-ELSE / Question Classifier / Variable Assigner / Answer) 신규 구현&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4주차&lt;/td&gt;
&lt;td&gt;개발&lt;/td&gt;
&lt;td&gt;&lt;b&gt;마켓플레이스 / 템플릿 시스템(ImportedWorkflowNode) 구축, Dify 스타일 NodeSelector&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5주차&lt;/td&gt;
&lt;td&gt;폴리싱&lt;/td&gt;
&lt;td&gt;UI 통일, 발표 준비&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말했듯이 2~3주차는 기획 방향이 계속 바뀌면서 개발 속도가 나지 않았다. 특히 3주차에는 타겟을 개발자로 전환하면서 기존 하드코딩된 노드를 &lt;code&gt;_base&lt;/code&gt; 컴포넌트 기반으로 리팩토링하는 작업을 진행했는데, 발표 피드백은 &quot;겉으로 보기엔 저번 주와 차이가 없다, 1주일 동안 뭐 했냐&quot;였다. 내부 구조는 완전히 바뀌었지만 눈에 보이는 결과물은 여전히 4개 노드뿐이었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 &lt;b&gt;3주차 후반에 IF-ELSE, Question Classifier, Variable Assigner, Answer 등 4개 노드 타입을 한꺼번에 만들고, 4주차에는 마켓플레이스와 템플릿 시스템(ImportedWorkflowNode)까지 구축&lt;/b&gt;하며 MVP의 대부분을 완성했다. 4주차 발표에서야 &quot;서비스는 잘 만든 것 같다&quot;는 피드백을 받을 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 나만무 프로젝트의 기획 배경과 매주 피드백을 받으며 방향을 찾아가는 과정을 이야기했다. 비개발자에서 개발자로 타겟을 전환하고, 폼 형식에서 노드-엣지 구조로 바꾸기까지 2주가 걸렸다. 그 과정에서 개발 일정이 밀렸고, 3주차 후반부터 4주차에 걸쳐 MVP 대부분을 몰아서 구현하는 상황이 벌어지기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5주차에 팀 내부적으로 작은 트러블이 있기도 했지만, 결과적으로 5주간의 프로젝트는 무사히 마무리되었다. 이 이야기는 4편 회고에서 다룰 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 팀 협업을 위한 &lt;b&gt;컴포넌트 설계&lt;/b&gt;와 &lt;b&gt;노드 생성 가이드 문서화 과정&lt;/b&gt;을 다룰 예정이다&lt;/p&gt;</description>
      <category>Projects</category>
      <category>SnapAgent</category>
      <category>나만무</category>
      <category>크래프톤정글</category>
      <category>회고</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/77</guid>
      <comments>https://onebrotravel.tistory.com/entry/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-10%EA%B8%B0-%EB%82%98%EB%A7%8C%EB%AC%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-1#entry77comment</comments>
      <pubDate>Thu, 14 May 2026 17:28:18 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 외전. 최신 DS18B20 라이브러리로 고추건조기 리팩토링하기</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-%EC%99%B8%EC%A0%84-%EC%B5%9C%EC%8B%A0-DS18B20-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-%EA%B3%A0%EC%B6%94%EA%B1%B4%EC%A1%B0%EA%B8%B0-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;동기 &amp;mdash; 왜 리팩토링을 시도하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 강의 시리즈에서 가장 고생했던 문제 중 하나가 &lt;b&gt;FND와 온도 센서의 통신 충돌&lt;/b&gt;이었다. &quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-30-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%9D%89%EB%82%B4%EB%82%B4%EA%B8%B0-%EB%AA%A8%EB%93%A0-%EC%9E%A5%EC%B9%98-%ED%86%B5%ED%95%A9%EC%8B%9C%ED%82%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;쓰레드 흉내내기&lt;/a&gt;&quot;부터 &quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-32-%EB%93%9C%EB%94%94%EC%96%B4-%ED%95%B4%EA%B2%B0%EB%90%9C-%ED%81%AC%EB%A6%AC%ED%8B%B0%EC%BB%AC-%EB%AC%B8%EC%A0%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;드디어 해결된 크리티컬 문제&lt;/a&gt;&quot;까지 세 글에 걸쳐 다뤘던 이 문제를 요약하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TIM3 인터럽트(100&amp;mu;s 주기)에서 FND를 갱신하고, main 루프에서 DS18B20 온도 센서를 읽는 구조&lt;/li&gt;
&lt;li&gt;main에서 1-Wire 통신 중에 TIM3 인터럽트가 발생하면, FND의 SPI GPIO가 끼어들어 &lt;b&gt;1-Wire 타이밍이 깨짐&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;FND에 온도가 0으로만 표시되거나, 깜빡이는 현상 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 &lt;b&gt;이전 버전 라이브러리의 소스를 직접 수정&lt;/b&gt;하여 해결했다. &lt;code&gt;is_busy&lt;/code&gt; 플래그를 수동으로 추가하고, &lt;code&gt;write_bit&lt;/code&gt; 단위까지 내려가서 실제 통신 구간에만 락을 걸었다. 동작은 했지만, 라이브러리 내부를 뜯어야 하는 부담이 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_35_fnd_collision.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsCqRq/dJMcabYpXWE/j35UYvr5PuWfI8lKfdr620/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsCqRq/dJMcabYpXWE/j35UYvr5PuWfI8lKfdr620/img.png&quot; data-alt=&quot;diagram_01_fnd_collision&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsCqRq/dJMcabYpXWE/j35UYvr5PuWfI8lKfdr620/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsCqRq%2FdJMcabYpXWE%2Fj35UYvr5PuWfI8lKfdr620%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;700&quot; data-filename=&quot;diagram_35_fnd_collision.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_01_fnd_collision&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-32-%EB%93%9C%EB%94%94%EC%96%B4-%ED%95%B4%EA%B2%B0%EB%90%9C-%ED%81%AC%EB%A6%AC%ED%8B%B0%EC%BB%AC-%EB%AC%B8%EC%A0%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;드디어 해결된 크리티컬 문제&quot;)&lt;/a&gt;의 마지막 섹션에서 현재 버전 DS18B20 라이브러리가 Non-Blocking으로 설계되어 있고, &lt;code&gt;ds18b20_is_busy()&lt;/code&gt;, &lt;code&gt;ds18b20_is_cnv_done()&lt;/code&gt; 등의 API가 내장되어 있다는 것을 확인했다. 그때 &lt;b&gt;&quot;강의에서 마주한 FND-온도센서 충돌 문제를, 현재 버전 라이브러리를 사용하면 라이브러리 수정 없이 더 간단하게 해결할 수 있지 않을까?&quot;&lt;/b&gt;라는 생각을 했다. 이번 글에서 실제로 최신 버전의 라이브러리를 포팅하여 리팩토링을 시도한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 버전 라이브러리 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이전 버전 (블로킹)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 버전의 &lt;code&gt;Ds18b20_ManualConvert&lt;/code&gt;는 함수 내부에서 &lt;b&gt;변환 시작 &amp;rarr; 완료 대기 &amp;rarr; 데이터 읽기&lt;/b&gt;를 한 번에 수행했다. 함수가 리턴될 때까지 수백 ms 동안 CPU를 점유하므로, 그 사이에 TIM3가 끼어들면 충돌이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;text-align: center; caret-color: transparent; letter-spacing: 0px;&quot; src=&quot;https://blog.kakaocdn.net/dna/dgNPBO/dJMcacpt4Um/AAAAAAAAAAAAAAAAAAAAAKucefYmTwaWtpl14K43R6JfdAakBpZD_Wa1YqeMU93M/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1780239599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=aqIoxCSgbA4Pdz0ZSTgruSBIzPU%3D&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;1325&quot; data-filename=&quot;temper sensor datasheet.png&quot; /&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;image&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; data-ke-style=&quot;widthContent&quot;&gt;
&lt;figcaption&gt;DS18B20 데이터시트 Figure 13 &amp;mdash; Initialization Timing / Figure 14 &amp;mdash; Read/Write Time Slot&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜 수백 ms나 걸릴까? 1-Wire 통신 자체가 &amp;mu;s 단위의 정밀한 타이밍으로 동작하기 때문이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC&quot;&gt;이전 글(&quot;어디서도 안 알려주는 프로토콜의 원리&quot;)&lt;/a&gt;에서 다뤘듯이, 통신 시작만 해도 Master가 480&amp;mu;s 이상 Low를 유지(리셋 펄스) &amp;rarr; Slave가 Presence Pulse로 응답하는 과정이 필요하고, 이후 ROM 커맨드 + Function 커맨드 + 데이터 송수신이 비트 단위로 이어진다. 여기에 12비트 해상도 기준&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;온도 변환 자체에 최대 750ms&lt;/b&gt;가 소요된다. 이 전체 과정을 하나의 함수 안에서 블로킹으로 처리하면, CPU가 그동안 다른 일을 할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_36_ds18b20_process.png&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H3Mtc/dJMcacJN5VU/1ac6mZ3FfceJiYQJ9S7g21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H3Mtc/dJMcacJN5VU/1ac6mZ3FfceJiYQJ9S7g21/img.png&quot; data-alt=&quot;diagram_02_ds18b20_process&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H3Mtc/dJMcacJN5VU/1ac6mZ3FfceJiYQJ9S7g21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH3Mtc%2FdJMcacJN5VU%2F1ac6mZ3FfceJiYQJ9S7g21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1700&quot; height=&quot;600&quot; data-filename=&quot;diagram_36_ds18b20_process.png&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_02_ds18b20_process&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 버전 (Non-Blocking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 버전(&lt;a href=&quot;https://github.com/nimaltd/ds18b20&quot;&gt;nimaltd/ds18b20&lt;/a&gt;)은 &lt;b&gt;Non-Blocking 방식&lt;/b&gt;으로 설계되어 있다. 각 동작을 호출하면 즉시 리턴하고, 완료 여부는 별도 함수로 체크한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_cnv()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;온도 변환 시작(0xCC + 0x44 전송)&lt;/td&gt;
&lt;td&gt;호출 후 &lt;b&gt;즉시 리턴&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_is_cnv_done()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;변환 완료 여부 확인&lt;/td&gt;
&lt;td&gt;HAL_GetTick 기반, 블로킹 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_req_read()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;온도 데이터 읽기 요청(0xBE 전송)&lt;/td&gt;
&lt;td&gt;호출 후 &lt;b&gt;즉시 리턴&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_is_busy()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1-Wire 버스가 통신 중인지 확인&lt;/td&gt;
&lt;td&gt;ow 내부 상태 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_read_c()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;수신된 데이터에서 온도 계산&lt;/td&gt;
&lt;td&gt;버퍼에서 읽기만, 통신 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-Wire의 실제 비트 단위 통신은 &lt;b&gt;TIM2 타이머 콜백(&lt;code&gt;ow_callback&lt;/code&gt;)&lt;/b&gt;에서 처리된다. main에서는 명령만 내리고, 타이밍 처리는 타이머가 알아서 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라이브러리 교체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프로젝트에서 이전 버전 라이브러리 파일을 삭제하고, 현재 버전 파일로 교체한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;삭제:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ds18b20.c&lt;/code&gt; / &lt;code&gt;ds18b20.h&lt;/code&gt; (이전 버전)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onewire.c&lt;/code&gt; / &lt;code&gt;onewire.h&lt;/code&gt; (이전 버전)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;추가:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ds18b20.c&lt;/code&gt; / &lt;code&gt;ds18b20.h&lt;/code&gt; (현재 버전)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ow.c&lt;/code&gt; / &lt;code&gt;ow.h&lt;/code&gt; / &lt;code&gt;ow_config.h&lt;/code&gt; &amp;mdash; 현재 버전에서는 1-Wire 통신 부분이 &lt;b&gt;별도의 독립 라이브러리(&lt;a href=&quot;https://github.com/nimaltd/ow&quot;&gt;nimaltd/ow&lt;/a&gt;)&lt;/b&gt;로 분리되었다. 이전 버전에서 &lt;code&gt;onewire.c/h&lt;/code&gt;로 ds18b20 라이브러리에 포함되어 있던 것과 달리, 이제는 ds18b20과 ow를 각각 가져와야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;library change.png&quot; data-origin-width=&quot;1575&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rek1O/dJMcabqAILa/1nvZ3jyrMQnlRaBkbKf900/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rek1O/dJMcabqAILa/1nvZ3jyrMQnlRaBkbKf900/img.png&quot; data-alt=&quot;프로젝트 탐색기 &amp;amp;mdash; 교체 전/후 파일 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rek1O/dJMcabqAILa/1nvZ3jyrMQnlRaBkbKf900/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frek1O%2FdJMcabqAILa%2F1nvZ3jyrMQnlRaBkbKf900%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1575&quot; height=&quot;1192&quot; data-filename=&quot;library change.png&quot; data-origin-width=&quot;1575&quot; data-origin-height=&quot;1192&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로젝트 탐색기 &amp;mdash; 교체 전/후 파일 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ow_config.h&lt;/code&gt;에서 &lt;code&gt;OW_MAX_DEVICE&lt;/code&gt;를 1로 설정한다 (온도 센서가 1개이므로).&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kxP13/dJMcaaZt6wD/Ngb2FmKCxOsupKp90IQbOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kxP13/dJMcaaZt6wD/Ngb2FmKCxOsupKp90IQbOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kxP13/dJMcaaZt6wD/Ngb2FmKCxOsupKp90IQbOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkxP13%2FdJMcaaZt6wD%2FNgb2FmKCxOsupKp90IQbOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;360&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도 센서 데이터 핀(PA3)은 &lt;code&gt;.ioc&lt;/code&gt;에서 &lt;b&gt;Output Open Drain + Pull-up&lt;/b&gt;으로 설정해야 한다. 1-Wire 통신은 풀업 저항으로 평상시 High를 유지하고, Master와 Slave가 &lt;b&gt;Low로 당기는 것&lt;/b&gt;으로 신호를 만든다(Figure 14 참고). Push-Pull로 설정하면 Master가 직접 High를 구동하게 되어, Slave가 Low로 당기려 할 때 충돌이 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1529&quot; data-origin-height=&quot;918&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clObef/dJMcagSXhSt/VM1VKd9TSa91zGKbTeN9qK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clObef/dJMcagSXhSt/VM1VKd9TSa91zGKbTeN9qK/img.png&quot; data-alt=&quot;PA3 Output Open Drain + Pull-up 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clObef/dJMcagSXhSt/VM1VKd9TSa91zGKbTeN9qK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclObef%2FdJMcagSXhSt%2FVM1VKd9TSa91zGKbTeN9qK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1529&quot; height=&quot;918&quot; data-origin-width=&quot;1529&quot; data-origin-height=&quot;918&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PA3 Output Open Drain + Pull-up 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교체 후 빌드하면 API 이름이 전부 바뀌었으므로 대량의 에러가 발생한다. 이전 버전의 호출 코드를 모두 제거하고, 현재 버전 API로 새로 작성해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;513&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EBlo3/dJMcadolXJd/utMmkavtH2ytBZw5sKcK01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EBlo3/dJMcadolXJd/utMmkavtH2ytBZw5sKcK01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EBlo3/dJMcadolXJd/utMmkavtH2ytBZw5sKcK01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEBlo3%2FdJMcadolXJd%2FutMmkavtH2ytBZw5sKcK01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1532&quot; height=&quot;513&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;513&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩토링 시작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 버전 라이브러리의 README에서 제공하는 example code를 보자.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 라이브러리 README의 example code
int16_t temp_c[2];

while(1) {
    ds18b20_cnv(&amp;amp;ds18);
    while(ds18b20_is_busy(&amp;amp;ds18));
    while(!ds18b20_is_cnv_done(&amp;amp;ds18));

    ds18b20_req_read(&amp;amp;ds18, 0);
    while(ds18b20_is_busy(&amp;amp;ds18));
    temp_c[0] = ds18b20_read_c(&amp;amp;ds18);

    ds18b20_req_read(&amp;amp;ds18, 1);
    while(ds18b20_is_busy(&amp;amp;ds18));
    temp_c[1] = ds18b20_read_c(&amp;amp;ds18);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 자체는 Non-Blocking으로 설계되어 있지만, 사용법이 &lt;b&gt;Busy Waiting(바쁜 대기)&lt;/b&gt;이다. &lt;code&gt;while(ds18b20_is_busy(&amp;amp;ds18));&lt;/code&gt;로 완료될 때까지 빈 루프를 돌면서 CPU를 점유한다. 특히 &lt;code&gt;while(!ds18b20_is_cnv_done(&amp;amp;ds18));&lt;/code&gt;는 12비트 해상도에서 &lt;b&gt;약 800ms 동안 CPU가 아무것도 하지 못하고 대기&lt;/b&gt;한다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이대로 사용하면 이전 버전의 블로킹 방식과 본질적으로 같은 문제가 발생한다. 800ms 동안 FND 갱신이 멈추고, 다른 작업도 할 수 없다. Non-Blocking API를 제공하는데 busy waiting으로 사용하면 &lt;b&gt;API의 장점을 전혀 살리지 못하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 busy waiting 대신 &lt;b&gt;상태머신&lt;/b&gt;을 도입한다. 각 단계에서 조건만 체크하고 즉시 빠져나와서, 대기 시간 동안 main 루프가 다른 작업(버튼 체크, FND 갱신 등)을 수행할 수 있게 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비동기 상태머신 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 example code에서 busy waiting을 제거하고, &lt;b&gt;각 단계를 상태로 분리&lt;/b&gt;한다. 핵심 원칙은 &lt;b&gt;모든 상태에서 &quot;호출하고 즉시 빠져나옴&quot;&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태 정의&lt;/h3&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;// defines.h
typedef enum {
    TEMPER_SENSOR_STATE_IDLE,
    TEMPER_SENSOR_STATE_WAIT_CONVERT,
    TEMPER_SENSOR_STATE_WAIT_READ,
} TEMPER_SENSOR_STATE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_37_state_machine.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wIVXd/dJMcajhNjNj/ceuQ5KBiv8hcxNqNwh9Xm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wIVXd/dJMcajhNjNj/ceuQ5KBiv8hcxNqNwh9Xm1/img.png&quot; data-alt=&quot;diagram_03_state_machine&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wIVXd/dJMcajhNjNj/ceuQ5KBiv8hcxNqNwh9Xm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwIVXd%2FdJMcajhNjNj%2FceuQ5KBiv8hcxNqNwh9Xm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1500&quot; height=&quot;640&quot; data-filename=&quot;diagram_37_state_machine.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_03_state_machine&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태 전이&lt;/h3&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;// main.c
volatile TEMPER_SENSOR_STATE temper_sensor_state;
volatile int16_t g_temper;

// while 루프 내
switch (temper_sensor_state) {
case TEMPER_SENSOR_STATE_IDLE:
    if (ds18b20_cnv(&amp;amp;ds18) == OW_ERR_NONE) {
        temper_sensor_state = TEMPER_SENSOR_STATE_WAIT_CONVERT;
    }
    break;

case TEMPER_SENSOR_STATE_WAIT_CONVERT:
    if (ds18b20_is_cnv_done(&amp;amp;ds18)) {
        if (ds18b20_req_read(&amp;amp;ds18) == OW_ERR_NONE) {
            temper_sensor_state = TEMPER_SENSOR_STATE_WAIT_READ;
        }
    }
    break;

case TEMPER_SENSOR_STATE_WAIT_READ:
    if (!ds18b20_is_busy(&amp;amp;ds18)) {
        g_temper = ds18b20_read_c(&amp;amp;ds18);
        temper_sensor_state = TEMPER_SENSOR_STATE_IDLE;
    }
    break;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 상태에서 하는 일:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IDLE&lt;/b&gt;: &lt;code&gt;ds18b20_cnv()&lt;/code&gt; 호출(변환 시작 명령 전송) &amp;rarr; 즉시 WAIT_CONVERT로 전이&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAIT_CONVERT&lt;/b&gt;: &lt;code&gt;ds18b20_is_cnv_done()&lt;/code&gt; 체크만 하고 리턴. 12비트 해상도에서 약 800ms 소요. 완료되면 &lt;code&gt;ds18b20_req_read()&lt;/code&gt; 호출 &amp;rarr; WAIT_READ로 전이&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAIT_READ&lt;/b&gt;: &lt;code&gt;ds18b20_is_busy()&lt;/code&gt; 체크만 하고 리턴. 읽기 완료되면 &lt;code&gt;ds18b20_read_c()&lt;/code&gt;로 온도 계산 &amp;rarr; IDLE로 복귀&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어떤 상태에서도 while로 대기하지 않는다.&lt;/b&gt; main 루프가 블로킹 없이 계속 돌면서, 버튼 체크나 다른 작업을 동시에 수행할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;g_temper&lt;/code&gt;와 &lt;code&gt;temper_sensor_state&lt;/code&gt;는 main과 TIM3 인터럽트 양쪽에서 접근하므로 &lt;b&gt;volatile&lt;/b&gt;을 붙여야 한다. 컴파일러 최적화로 인터럽트에서 변경된 값을 main이 인식하지 못하는 문제를 방지한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FND와의 공존&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TIM3에서 ds18b20_is_busy 체크&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// stm32f1xx_it.c &amp;mdash; TIM3_IRQHandler 내
if (!ds18b20_is_busy(&amp;amp;ds18)) {
    digit_temper((int)(g_temper / 10));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ds18b20_is_busy()&lt;/code&gt;가 true인 구간은 &lt;b&gt;1-Wire 라이브러리가 실제로 GPIO를 제어하며 비트를 보내고 받는 구간&lt;/b&gt;이다. 이때만 FND 갱신을 스킵하고, 나머지 시간(변환 대기 800ms 등)에는 자유롭게 FND를 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 &lt;code&gt;write_bit&lt;/code&gt; 단위까지 내려가서 수동으로 구현했던 것을, &lt;b&gt;라이브러리 API 한 줄로 대체&lt;/b&gt;한 것이다. 라이브러리 소스를 수정할 필요가 전혀 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260412_123345-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vruvb/dJMcagrRWvP/zZP2xBDkwindpq1XdEMKB0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vruvb/dJMcagrRWvP/zZP2xBDkwindpq1XdEMKB0/img.gif&quot; data-alt=&quot;FND 깜빡임 없는 온도 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vruvb/dJMcagrRWvP/zZP2xBDkwindpq1XdEMKB0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/vruvb/dJMcagrRWvP/zZP2xBDkwindpq1XdEMKB0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260412_123345-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND 깜빡임 없는 온도 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩토링 과정에서 만난 문제들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. HardFault &amp;mdash; done_cb 미초기화&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;925&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n4cQw/dJMcaicbEaW/uKkjeIz3bGepprB6VKfdBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n4cQw/dJMcaicbEaW/uKkjeIz3bGepprB6VKfdBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n4cQw/dJMcaicbEaW/uKkjeIz3bGepprB6VKfdBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4cQw%2FdJMcaicbEaW%2FuKkjeIz3bGepprB6VKfdBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1530&quot; height=&quot;925&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;925&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// 수정 전 &amp;mdash; 쓰레기 값
ow_init_t ow_init_struct;
ow_init_struct.tim_handle = &amp;amp;htim2;
// done_cb을 설정하지 않음 &amp;rarr; 쓰레기 주소

// 수정 후 &amp;mdash; 0으로 초기화
ow_init_t ow_init_struct = {0};
ow_init_struct.tim_handle = &amp;amp;htim2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택 변수를 초기화하지 않으면 &lt;code&gt;done_cb&lt;/code&gt;에 쓰레기 주소가 들어간다. 1-Wire 전송 완료 시 라이브러리가 이 콜백을 호출하면서 &lt;b&gt;존재하지 않는 메모리로 점프&lt;/b&gt; &amp;rarr; HardFault가 발생했다. &lt;code&gt;= {0}&lt;/code&gt;으로 초기화하면 &lt;code&gt;done_cb&lt;/code&gt;이 NULL이 되어 호출을 건너뛴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 디버그 모드에서 Memory 뷰(Window &amp;rarr; Show View &amp;rarr; Memory)를 열어 &lt;code&gt;ow_init_struct&lt;/code&gt;의 메모리를 확인하면, 초기화하지 않았을 때 &lt;code&gt;done_cb&lt;/code&gt; 위치에 쓰레기 값이 들어있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;memory view.png&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/39V5T/dJMcabjPRWJ/F22JGLTEqXAFYEsfuDczCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/39V5T/dJMcabjPRWJ/F22JGLTEqXAFYEsfuDczCk/img.png&quot; data-alt=&quot;Memory 뷰 &amp;amp;mdash; done_cb 쓰레기 값&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/39V5T/dJMcabjPRWJ/F22JGLTEqXAFYEsfuDczCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F39V5T%2FdJMcabjPRWJ%2FF22JGLTEqXAFYEsfuDczCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1822&quot; height=&quot;245&quot; data-filename=&quot;memory view.png&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Memory 뷰 &amp;mdash; done_cb 쓰레기 값&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C 언어에서 구조체를 스택에 선언할 때는 &lt;b&gt;항상 &lt;code&gt;= {0}&lt;/code&gt;으로 초기화하는 습관&lt;/b&gt;을 들이자. 특히 함수 포인터가 포함된 구조체는 쓰레기 값이 치명적이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. ds18b20_read_c()의 리턴값 단위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ds18b20_read_c()&lt;/code&gt;는 &lt;b&gt;온도 &amp;times; 100&lt;/b&gt; 값을 리턴한다 (예: 25.06&amp;deg;C &amp;rarr; 2506). 릴레이 제어에서 &lt;code&gt;m_fixed_temper&lt;/code&gt;(정수, 예: 25)와 비교할 때 단위를 맞춰야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// heater_controller.c
void heaterControl(int16_t temper) {
    // temper는 x100 단위
    if (m_state) {
        if ((int)temper &amp;gt;= (m_fixed_temper - PRE_OFF_GAP) * 100) {
            heaterOnOff(OFF_t);
        }
    } else {
        if ((int)temper &amp;lt; (m_fixed_temper - RE_ON_GAP) * 100) {
            heaterOnOff(ON_t);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리팩토링된 고추건조기 완성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태머신에서 온도 읽기가 완료되는 시점(WAIT_READ &amp;rarr; IDLE 전이)에 &lt;b&gt;히터 제어 로직&lt;/b&gt;을 연동했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;case TEMPER_SENSOR_STATE_WAIT_READ:
    if (!ds18b20_is_busy(&amp;amp;ds18)) {
        g_temper = ds18b20_read_c(&amp;amp;ds18);
        if (getSwState() == ON_t) {
            heaterControl(g_temper);
        } else {
            if (getHeaterState() == ON_t) {
                heaterOnOff(OFF_t);
            }
        }
        temper_sensor_state = TEMPER_SENSOR_STATE_IDLE;
    }
    break;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전원 스위치 ON 상태에서만 온도 기반 릴레이 제어가 동작하고, 스위치 OFF 시에는 히터가 강제 정지된다. 이전 코드의 로직 그대로이지만, &lt;b&gt;블로킹 없는 상태머신 안에 자연스럽게 통합&lt;/b&gt;되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[VIDEO: 리팩토링된 고추건조기 전체 동작]&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오프닝&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;opening.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AegUS/dJMcabqAIXY/afjbkVf502BtK5hPJfxhiK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AegUS/dJMcabqAIXY/afjbkVf502BtK5hPJfxhiK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AegUS/dJMcabqAIXY/afjbkVf502BtK5hPJfxhiK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/AegUS/dJMcabqAIXY/afjbkVf502BtK5hPJfxhiK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;opening.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;updownfix-스위치-조작&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;UP/DOWN/FIX 스위치 조작&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;updownfix.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co0CA5/dJMcaiiUBk7/vuodGpzjrMn8sR4DobzLa1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co0CA5/dJMcaiiUBk7/vuodGpzjrMn8sR4DobzLa1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co0CA5/dJMcaiiUBk7/vuodGpzjrMn8sR4DobzLa1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/co0CA5/dJMcaiiUBk7/vuodGpzjrMn8sR4DobzLa1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;updownfix.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;start-스위치-on&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Start 스위치 ON&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;start.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl6YUy/dJMcaayrFfL/Wk3DV5PrKFAQ6jWLnzagx1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl6YUy/dJMcaayrFfL/Wk3DV5PrKFAQ6jWLnzagx1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl6YUy/dJMcaayrFfL/Wk3DV5PrKFAQ6jWLnzagx1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bl6YUy/dJMcaayrFfL/Wk3DV5PrKFAQ6jWLnzagx1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;start.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;off-전환&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;OFF 전환&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;off.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wNKCQ/dJMcadPorek/JBaRmh1qsNbe2ST97w1Sl0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wNKCQ/dJMcadPorek/JBaRmh1qsNbe2ST97w1Sl0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wNKCQ/dJMcadPorek/JBaRmh1qsNbe2ST97w1Sl0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/wNKCQ/dJMcadPorek/JBaRmh1qsNbe2ST97w1Sl0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;off.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;다시-on-전환&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;다시 ON 전환&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;on.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MmB8L/dJMcabcZ6VF/OhF7QKCt8jLLjzL8Z2rWJk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MmB8L/dJMcabcZ6VF/OhF7QKCt8jLLjzL8Z2rWJk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MmB8L/dJMcabcZ6VF/OhF7QKCt8jLLjzL8Z2rWJk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/MmB8L/dJMcabcZ6VF/OhF7QKCt8jLLjzL8Z2rWJk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;on.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id=&quot;on-상태에서-start-스위치-down&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;ON 상태에서 Start 스위치 DOWN&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;on-down.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvjCIM/dJMcaarFZnm/iNnA56eGR2QfOq0FNmfZ6K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvjCIM/dJMcaarFZnm/iNnA56eGR2QfOq0FNmfZ6K/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvjCIM/dJMcaarFZnm/iNnA56eGR2QfOq0FNmfZ6K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bvjCIM/dJMcaarFZnm/iNnA56eGR2QfOq0FNmfZ6K/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;on-down.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과 및 비교&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;이전 버전 (강의)&lt;/th&gt;
&lt;th&gt;현재 버전 (리팩토링)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;라이브러리 수정&lt;/td&gt;
&lt;td&gt;필요 (ds18b20.c, onewire.c 직접 수정)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;불필요&lt;/b&gt; (API 호출만)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is_busy 구현&lt;/td&gt;
&lt;td&gt;수동 플래그 추가, write_bit 단위 락&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ds18b20_is_busy()&lt;/code&gt; 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;크리티컬 섹션&lt;/td&gt;
&lt;td&gt;수동 관리 (락 범위 시행착오)&lt;/td&gt;
&lt;td&gt;라이브러리가 내부 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;main 루프&lt;/td&gt;
&lt;td&gt;블로킹 대기 (&lt;code&gt;while (!AllDone)&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Non-Blocking 상태머신&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FND 충돌&lt;/td&gt;
&lt;td&gt;락 범위에 따라 깜빡임 발생 가능&lt;/td&gt;
&lt;td&gt;&lt;code&gt;is_busy&lt;/code&gt; 한 줄로 해결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;코드 구조&lt;/td&gt;
&lt;td&gt;ManualConvert를 3~6개 함수로 수동 분해&lt;/td&gt;
&lt;td&gt;switch 상태머신으로 깔끔하게 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;느낀 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 이전 버전 라이브러리를 직접 뜯어서 &lt;code&gt;write_bit&lt;/code&gt; 단위까지 내려가본 경험이 있었기 때문에, 현재 버전의 &lt;code&gt;ds18b20_is_busy()&lt;/code&gt;가 &lt;b&gt;내부적으로 무엇을 보호하는지&lt;/b&gt; 이해할 수 있었다. 단순히 API를 호출하는 것과, 그 API가 왜 그렇게 설계되었는지 이해하는 것은 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-Blocking 설계는 임베디드에서 매우 중요하다. OS 없는 베어메탈 환경에서 여러 장치를 동시에 제어하려면, &lt;b&gt;&quot;호출하고 즉시 빠져나와서 다른 일을 하고, 나중에 완료를 확인한다&quot;&lt;/b&gt;는 패턴이 필수적이다. 이번 리팩토링이 그 패턴을 직접 경험하는 좋은 기회였다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>OJTube임베디드입문</category>
      <category>OneWire</category>
      <category>refactoring</category>
      <category>statemachine</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/76</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-%EC%99%B8%EC%A0%84-%EC%B5%9C%EC%8B%A0-DS18B20-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A1%9C-%EA%B3%A0%EC%B6%94%EA%B1%B4%EC%A1%B0%EA%B8%B0-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0#entry76comment</comments>
      <pubDate>Wed, 6 May 2026 22:02:04 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 40. 클라스가 다른 ADC 강의</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-40-%ED%81%B4%EB%9D%BC%EC%8A%A4%EA%B0%80-%EB%8B%A4%EB%A5%B8-ADC-%EA%B0%95%EC%9D%98</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;가변 저항 회로 연결&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eTODhn/dJMcagel59k/C6hcRWmKkSlzr8NGUKxgkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eTODhn/dJMcagel59k/C6hcRWmKkSlzr8NGUKxgkk/img.png&quot; data-alt=&quot;가변저항 데이터시트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eTODhn/dJMcagel59k/C6hcRWmKkSlzr8NGUKxgkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeTODhn%2FdJMcagel59k%2FC6hcRWmKkSlzr8NGUKxgkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;866&quot; height=&quot;411&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가변저항 데이터시트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-39-%EB%84%88%EB%AC%B4%EB%82%98%EB%8F%84-%EC%A4%91%EC%9A%94%ED%95%9C-ADC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;너무나도 중요한 ADC&quot;)&lt;/a&gt;에서 ADC의 이론을 다뤘다. 이번에는 &lt;b&gt;가변 저항을 이용하여 실제로 ADC를 사용&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가변 저항은 돌리면 저항값이 변하고, 이에 따라 출력 전압이 변하는 부품이다. 3개의 핀이 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;핀&lt;/th&gt;
&lt;th&gt;연결&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1번&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;td&gt;입력 전압&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2번&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PA2&lt;/b&gt; (ADC 입력)&lt;/td&gt;
&lt;td&gt;가변 저항에 따라 0~3.3V 변화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3번&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;접지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스터기로 확인하면, 1번-3번 사이에 약 3.29V가 측정되고, 2번 핀의 전압은 가변 저항을 조절함에 따라 &lt;b&gt;0V ~ 3.29V까지 변화&lt;/b&gt;한다. 이 가변 저항 대신 온도 센서, 습도 센서 등 다른 아날로그 센서를 연결하면 다양한 측정이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260408_170120.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcTxbN/dJMcac33swf/bJAV1MX7NYxEbtBndmILSk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcTxbN/dJMcac33swf/bJAV1MX7NYxEbtBndmILSk/img.jpg&quot; data-alt=&quot;가변 저항 회로 빵판 배선&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcTxbN/dJMcac33swf/bJAV1MX7NYxEbtBndmILSk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcTxbN%2FdJMcac33swf%2FbJAV1MX7NYxEbtBndmILSk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260408_170120.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;가변 저항 회로 빵판 배선&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ADC 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.ioc 설정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1475&quot; data-origin-height=&quot;1155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l51Np/dJMcacbWo5C/WfBlMFgxRE1UzXFXtHNDK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l51Np/dJMcacbWo5C/WfBlMFgxRE1UzXFXtHNDK1/img.png&quot; data-alt=&quot;ADC 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l51Np/dJMcacbWo5C/WfBlMFgxRE1UzXFXtHNDK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl51Np%2FdJMcacbWo5C%2FWfBlMFgxRE1UzXFXtHNDK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1475&quot; height=&quot;1155&quot; data-origin-width=&quot;1475&quot; data-origin-height=&quot;1155&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ADC 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에서 PA2를 &lt;b&gt;ADC1 채널 2&lt;/b&gt;로 설정하고, 다음과 같이 옵션을 구성한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정 항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mode&lt;/td&gt;
&lt;td&gt;Independent&lt;/td&gt;
&lt;td&gt;ADC1, ADC2 독립 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Alignment&lt;/td&gt;
&lt;td&gt;Right&lt;/td&gt;
&lt;td&gt;12비트 값을 오른쪽 정렬로 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous Conversion&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Enable&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;변환 완료 후 자동으로 다음 변환 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External Trigger&lt;/td&gt;
&lt;td&gt;Software&lt;/td&gt;
&lt;td&gt;소프트웨어로 변환 시작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel&lt;/td&gt;
&lt;td&gt;2 (PA2)&lt;/td&gt;
&lt;td&gt;입력 채널&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ADC 클럭 제한&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터시트에 따르면 &lt;b&gt;ADC 클럭은 최대 14MHz&lt;/b&gt;로 제한된다. 72MHz 시스템 클럭에서 적절히 분주하여 &lt;b&gt;12MHz&lt;/b&gt;로 설정해야 한다. 이 값을 초과하면 .ioc에서 경고가 발생한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 코드 구현 (폴링 방식)&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// main.c
uint32_t adc_value = 0;
float voltage = 0.0f;

HAL_ADC_Start(&amp;amp;hadc1);

while (1) {
    if (HAL_ADC_PollForConversion(&amp;amp;hadc1, 100) == HAL_OK) {
        adc_value = HAL_ADC_GetValue(&amp;amp;hadc1);
        voltage = (float)adc_value / 4096.0f * 3.3f;
    }
    printf(&quot;ADC: %lu, Voltage: %.2fV\r\n&quot;, adc_value, voltage);
    HAL_Delay(500);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;HAL_ADC_Start&lt;/code&gt; &amp;mdash; ADC 변환 시작&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAL_ADC_PollForConversion&lt;/code&gt; &amp;mdash; 변환 완료 대기 (폴링)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAL_ADC_GetValue&lt;/code&gt; &amp;mdash; 변환된 디지털 값 획득 (0~4095)&lt;/li&gt;
&lt;li&gt;전압 계산: &lt;b&gt;값 &amp;divide; 4096 &amp;times; 3.3V&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가변 저항을 돌리면 ADC 값과 전압이 실시간으로 변하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;1058&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZzvKu/dJMcaciImYq/rymMgF0SqkqSnjXVWvym00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZzvKu/dJMcaciImYq/rymMgF0SqkqSnjXVWvym00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZzvKu/dJMcaciImYq/rymMgF0SqkqSnjXVWvym00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZzvKu%2FdJMcaciImYq%2FrymMgF0SqkqSnjXVWvym00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;1058&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;1058&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터시트 분석&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;1140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfzh2H/dJMcagk5T2N/xruObw8iSvIUaJzKw8hv6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfzh2H/dJMcagk5T2N/xruObw8iSvIUaJzKw8hv6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfzh2H/dJMcagk5T2N/xruObw8iSvIUaJzKw8hv6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcfzh2H%2FdJMcagk5T2N%2FxruObw8iSvIUaJzKw8hv6K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;944&quot; height=&quot;1140&quot; data-origin-width=&quot;944&quot; data-origin-height=&quot;1140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12비트 = 4096단계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-39-%EB%84%88%EB%AC%B4%EB%82%98%EB%8F%84-%EC%A4%91%EC%9A%94%ED%95%9C-ADC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;너무나도 중요한 ADC&quot;)&lt;/a&gt;에서 다뤘던 &lt;b&gt;분해능(Resolution)&lt;/b&gt; 개념이 여기서 적용된다. 12비트 ADC는 0~3.3V를 2&amp;sup1;&amp;sup2; = 4096단계로 나눈다. 1단계(1 LSB)는 3.3V &amp;divide; 4096 ≒ &lt;b&gt;0.8mV&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VREF &amp;mdash; 기준 전압&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;vref.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;1067&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dRw6fz/dJMcaarFYQ0/KCk6PFOmq9ekuYpzM05fXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dRw6fz/dJMcaarFYQ0/KCk6PFOmq9ekuYpzM05fXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dRw6fz/dJMcaarFYQ0/KCk6PFOmq9ekuYpzM05fXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdRw6fz%2FdJMcaarFYQ0%2FKCk6PFOmq9ekuYpzM05fXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1098&quot; height=&quot;1067&quot; data-filename=&quot;vref.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;1067&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;핀&lt;/th&gt;
&lt;th&gt;연결&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VREF+&lt;/td&gt;
&lt;td&gt;VDDA (3.3V)&lt;/td&gt;
&lt;td&gt;ADC가 측정할 수 있는 &lt;b&gt;최대 전압&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VREF-&lt;/td&gt;
&lt;td&gt;VSSA (GND)&lt;/td&gt;
&lt;td&gt;측정 &lt;b&gt;시작점&lt;/b&gt; (0V)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터시트에 따르면 VREF의 허용 범위는 &lt;b&gt;2.4V ~ 3.6V&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VDD / VCC / VSS / VSSA 용어 정리&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;용어&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;유래&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VDD&lt;/td&gt;
&lt;td&gt;+ 전원 (디지털)&lt;/td&gt;
&lt;td&gt;MOSFET의 Drain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VCC&lt;/td&gt;
&lt;td&gt;+ 전원&lt;/td&gt;
&lt;td&gt;BJT의 Collector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VSS&lt;/td&gt;
&lt;td&gt;GND (디지털)&lt;/td&gt;
&lt;td&gt;MOSFET의 Source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VSSA&lt;/td&gt;
&lt;td&gt;GND (아날로그)&lt;/td&gt;
&lt;td&gt;아날로그 회로용 GND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VDDA&lt;/td&gt;
&lt;td&gt;+ 전원 (아날로그)&lt;/td&gt;
&lt;td&gt;아날로그 회로용 전원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 회로에서는 VDD/VCC, VSS/GND를 거의 같은 의미로 사용한다. 핵심은 &lt;b&gt;VDD/VCC = 플러스 전압, VSS/VSSA = 그라운드&lt;/b&gt;라는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ADC 동작 모드 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스캔 / 연속 / 불연속&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모드&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Scan Conversion&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;여러 채널을 순차적으로 자동 변환&lt;/td&gt;
&lt;td&gt;다채널 센서 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Continuous Conversion&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;변환 완료 후 자동으로 다음 변환 시작&lt;/td&gt;
&lt;td&gt;실시간 연속 측정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Discontinuous Conversion&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;수동 제어로 필요할 때만 변환&lt;/td&gt;
&lt;td&gt;전력 절약&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Regular vs Injected&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모드&lt;/th&gt;
&lt;th&gt;레지스터&lt;/th&gt;
&lt;th&gt;최대 채널&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Regular&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1개 공유&lt;/td&gt;
&lt;td&gt;16개&lt;/td&gt;
&lt;td&gt;폴링이 아니면 데이터 혼동 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Injected&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;채널별 개별&lt;/td&gt;
&lt;td&gt;4개&lt;/td&gt;
&lt;td&gt;정확하지만 채널 수 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Regular 모드는 하나의 데이터 레지스터를 여러 채널이 공유하므로, 폴링으로 즉시 읽지 않으면 다음 채널의 값으로 덮어써질 수 있다. Injected 모드는 각 채널마다 별도 레지스터가 있어 안전하지만 최대 4개까지만 지원한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기타 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;External Trigger&lt;/b&gt;: 소프트웨어 또는 타이머로 변환 시작 시점 제어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Watchdog&lt;/b&gt;: 특정 전압 범위를 벗어나면 인터럽트 발생. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-27-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;온도센서 검증하기&quot;)&lt;/a&gt;에서 다뤘던 Threshold 개념과 유사&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DMA를 이용한 고속 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DMA란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DMA(Direct Memory Access)&lt;/b&gt;는 CPU의 개입 없이 ADC 변환 결과를 &lt;b&gt;자동으로 메모리에 기록&lt;/b&gt;하는 방식이다. 폴링처럼 CPU가 매번 값을 읽어올 필요 없이, 하드웨어가 알아서 메모리에 써준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DMA 설정&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정 항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mode&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Circular&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;지속적으로 기록 (Normal은 한 번만)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Width&lt;/td&gt;
&lt;td&gt;Half Word (16비트)&lt;/td&gt;
&lt;td&gt;12비트 ADC 값 저장에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory Address Increment&lt;/td&gt;
&lt;td&gt;다채널 시 Enable&lt;/td&gt;
&lt;td&gt;채널별로 다른 메모리 주소에 기록&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DMA 코드&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// main.c
uint16_t adc_dma_value = 0;

HAL_ADC_Start_DMA(&amp;amp;hadc1, (uint32_t*)&amp;amp;adc_dma_value, 1);

while (1) {
    float voltage = (float)adc_dma_value / 4096.0f * 3.3f;
    printf(&quot;ADC: %u, Voltage: %.2fV\r\n&quot;, adc_dma_value, voltage);
    HAL_Delay(500);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HAL_ADC_Start_DMA&lt;/code&gt;를 한 번 호출하면, 이후 &lt;code&gt;adc_dma_value&lt;/code&gt;에 자동으로 최신 값이 업데이트된다. while 루프에서는 변수를 읽기만 하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DMA 오류 및 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DMA를 적용한 후 &lt;b&gt;ADC 값이 정상적으로 읽히지 않는 문제&lt;/b&gt;가 발생했다. 코드를 변경한 적이 없는데도 오류가 발생했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 &amp;mdash; 인터럽트 충돌&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 기존 프로젝트에서 사용 중이던 &lt;b&gt;다른 인터럽트들(타이머, EXTI 등)과의 충돌&lt;/b&gt;이었다. PA2 설정이나 샘플링 사이클 등 ADC 자체의 문제는 아니었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 타이머 인터럽트 등을 모두 비활성화하고, &lt;b&gt;ADC + DMA만 동작하도록&lt;/b&gt; 설정을 정리하자 정상 동작했다. 가변 저항을 조절하면 3.29V까지 값이 변하며, 실측값과도 일치한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ADC를 학습할 때는 기존 프로젝트(여러 인터럽트가 활성화된 상태)보다 &lt;b&gt;새 프로젝트를 생성하여 ADC만 단독으로 테스트&lt;/b&gt;하는 것이 좋다. 인터럽트 간 충돌은 원인 추적이 어렵고, 학습 목표에서 벗어나기 쉽다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3PtNc/dJMcafT3gko/oQnX8sd7041KxhkvvUtL0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3PtNc/dJMcafT3gko/oQnX8sd7041KxhkvvUtL0K/img.png&quot; data-alt=&quot;DMA 정상 동작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3PtNc/dJMcafT3gko/oQnX8sd7041KxhkvvUtL0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3PtNc%2FdJMcafT3gko%2FoQnX8sd7041KxhkvvUtL0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1836&quot; height=&quot;890&quot; data-origin-width=&quot;1836&quot; data-origin-height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DMA 정상 동작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 강의 시리즈 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글로 &lt;b&gt;임베디드 입문 강의 시리즈 전체가 완료&lt;/b&gt;되었다. 약 40시간에 걸쳐 다음을 학습했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전기 기본 상식부터 GPIO, 비트 연산, 데이터시트 읽기&lt;/li&gt;
&lt;li&gt;SPI, 1-Wire, I2C, UART 4가지 통신 방식&lt;/li&gt;
&lt;li&gt;온도 센서, FND, OLED, 릴레이, 스위치, LED 제어&lt;/li&gt;
&lt;li&gt;타이머 인터럽트, 크리티컬 섹션, 외부 인터럽트&lt;/li&gt;
&lt;li&gt;최종 고추 건조기 프로젝트 완성&lt;/li&gt;
&lt;li&gt;ADC 이론과 실습&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Embedded</category>
      <category>ADC</category>
      <category>DMA</category>
      <category>hal</category>
      <category>OJTube임베디드입문</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/75</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-40-%ED%81%B4%EB%9D%BC%EC%8A%A4%EA%B0%80-%EB%8B%A4%EB%A5%B8-ADC-%EA%B0%95%EC%9D%98#entry75comment</comments>
      <pubDate>Wed, 6 May 2026 21:39:16 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 39. 너무나도 중요한 ADC</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-39-%EB%84%88%EB%AC%B4%EB%82%98%EB%8F%84-%EC%A4%91%EC%9A%94%ED%95%9C-ADC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;아날로그와 디지털&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ADC를 이해하려면 먼저 아날로그와 디지털의 차이를 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아날로그(Analog)&lt;/b&gt;는 물질이나 시스템의 상태를 &lt;b&gt;연속적으로 변하는 물리량&lt;/b&gt;으로 나타낸 것이다. 예를 들어 온도를 그래프로 표현하면, 아무리 확대해도 그 사이에 모든 값이 연속적으로 존재하며 끊어지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디지털(Digital)&lt;/b&gt;은 연속적인 값을 &lt;b&gt;0과 1로 표현하고 처리하는 방법&lt;/b&gt;이다. 아날로그처럼 모든 값을 저장하는 것은 불가능하므로, 중간 값들을 생략하여 저장한다. 이미지를 확대했을 때 픽셀이 깨지는 것이 대표적인 예다. 사람 눈에는 연결된 것처럼 보이지만, 사실은 &lt;b&gt;빈 공간을 생략하고 촘촘하게 연결하여 아날로그를 흉내 내는 것&lt;/b&gt;이다. MP3, AVI, JPG 같은 파일들이 모두 아날로그 신호를 디지털로 변환한 결과물이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_33_analog_vs_digital.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxuJJZ/dJMcahdfwMI/3karwvoLkjnpUQg8tN1Oh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxuJJZ/dJMcahdfwMI/3karwvoLkjnpUQg8tN1Oh1/img.png&quot; data-alt=&quot;아날로그 vs 디지털 비교 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxuJJZ/dJMcahdfwMI/3karwvoLkjnpUQg8tN1Oh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxuJJZ%2FdJMcahdfwMI%2F3karwvoLkjnpUQg8tN1Oh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;760&quot; data-filename=&quot;diagram_33_analog_vs_digital.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아날로그 vs 디지털 비교 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ADC란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ADC(Analog to Digital Converter)는 &lt;b&gt;아날로그 신호를 디지털 신호로 변환&lt;/b&gt;하여 컴퓨터가 처리할 수 있게 만드는 기술이다. 컴퓨터(MCU 포함)는 0과 1의 디지털만 처리할 수 있으므로, 현실 세계의 연속적인 물리량(온도, 압력, 빛, 소리 등)을 다루려면 ADC가 반드시 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-12-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%8B%9C%ED%8A%B8-%EC%9D%BD%EC%96%B4%EC%A3%BC%EB%8A%94-%EB%82%A8%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;데이터시트 읽어주는 남자&quot;)&lt;/a&gt;에서 STM32의 Features 중 &quot;2x 12-bit ADC&quot;를 다뤘는데, 이것이 바로 칩에 내장된 ADC 기능이다. 또한 &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-23-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C%EB%A5%BC-%EB%B6%99%EC%97%AC%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;온도센서를 붙여보자&quot;)&lt;/a&gt;에서 &quot;순수 센서 vs 모듈&quot;을 비교할 때, 순수 센서는 &lt;b&gt;ADC로 전압 변화를 직접 측정해야&lt;/b&gt; 하는 고난도 방식이라고 언급했다. 이번 강의에서 그 ADC 개념을 본격적으로 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ADC 활용 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디지털 온도계&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;특정 쇠막대기(프로브)가 &lt;b&gt;온도 변화에 따라 저항값이 변하는 특성&lt;/b&gt;을 발견&lt;/li&gt;
&lt;li&gt;저항값 변화는 회로를 통해 &lt;b&gt;전압값 변화&lt;/b&gt;로 이어짐&lt;/li&gt;
&lt;li&gt;MCU에 내장된 ADC로 이 &lt;b&gt;전압값을 디지털 숫자로 변환&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;변환된 숫자를 온도 변환 테이블과 대조하여 &lt;b&gt;현재 온도를 계산&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;화면에 온도 표시&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글(&quot;온도센서를 붙여보자&quot;)에서 우리가 사용한 DS18B20 모듈은 이 과정을 모듈 내부에서 처리해줬지만, 순수 센서를 사용한다면 이 전체 과정을 직접 구현해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스트레인 게이지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트레인 게이지는 &lt;b&gt;누르는 힘에 따라 저항값이 변하는&lt;/b&gt; 소자다. 활용 예시:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파이프 모니터링&lt;/b&gt;: 파이프에 부착하여 변형을 감지. ADC로 전압 변화를 측정하여 파손 위험을 경고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로드셀(체중계)&lt;/b&gt;: 무게에 따른 저항 변화를 ADC로 측정하여 몸무게 계산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IoT 센서&lt;/b&gt;: 습도 센서, 조도 센서 등 대부분의 아날로그 센서가 같은 원리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 &lt;b&gt;저항 변화 &amp;rarr; 전압 변화 &amp;rarr; ADC &amp;rarr; 디지털 값&lt;/b&gt;이라는 원리는 모두 동일하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ADC 변환 3단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아날로그 신호를 디지털로 변환하는 과정은 크게 세 단계로 나뉜다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 필터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신호에서 &lt;b&gt;노이즈를 제거&lt;/b&gt;하는 전처리 작업이다. 실제 신호에는 원하지 않는 잡음이 섞여 있으므로, 변환 전에 걸러낸다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: 샘플링 (X축 &amp;mdash; 시간)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아날로그 신호를 &lt;b&gt;얼마나 자주 측정할 것인지&lt;/b&gt; 결정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;샘플링 간격이 &lt;b&gt;좁을수록&lt;/b&gt;: 원본 신호에 가까움 &amp;rarr; 음질/화질 좋음 &amp;rarr; &lt;b&gt;용량 큼&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;샘플링 간격이 &lt;b&gt;넓을수록&lt;/b&gt;: 원본과 차이 발생 &amp;rarr; 음질/화질 낮음 &amp;rarr; &lt;b&gt;용량 작음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음악으로 비유하면, CD 품질(44.1kHz)은 1초에 44,100번 샘플링하고, 전화 품질(8kHz)은 1초에 8,000번만 샘플링한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: 양자화 (Y축 &amp;mdash; 값의 단위)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;측정된 값을 &lt;b&gt;얼마나 세밀하게 나눌 것인지&lt;/b&gt; 결정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나누는 단위가 &lt;b&gt;촘촘할수록&lt;/b&gt;: 원본에 가까움 &amp;rarr; &lt;b&gt;용량 큼&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;나누는 단위가 &lt;b&gt;거칠수록&lt;/b&gt;: 원본과 차이 발생 &amp;rarr; &lt;b&gt;용량 작음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양자화 이후에는 &lt;b&gt;부호화(인코딩)&lt;/b&gt;를 통해 측정된 좌표 값을 0과 1의 이진수로 변환하여 저장하거나 전송한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_34_sampling_quantization.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VZwdW/dJMcagSXhdS/VVdlaQ47PDXnnues9YjLiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VZwdW/dJMcagSXhdS/VVdlaQ47PDXnnues9YjLiK/img.png&quot; data-alt=&quot;샘플링(X축) vs 양자화(Y축) 비교 다이어그램&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VZwdW/dJMcagSXhdS/VVdlaQ47PDXnnues9YjLiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVZwdW%2FdJMcagSXhdS%2FVVdlaQ47PDXnnues9YjLiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;960&quot; data-filename=&quot;diagram_34_sampling_quantization.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;샘플링(X축) vs 양자화(Y축) 비교 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분해능 (Resolution)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분해능은 &lt;b&gt;ADC가 얼마나 세밀하게 값을 표현할 수 있는지&lt;/b&gt;를 나타내며, 비트 수로 표현한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;비트 수&lt;/th&gt;
&lt;th&gt;경우의 수&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8비트&lt;/td&gt;
&lt;td&gt;2⁸ = 256&lt;/td&gt;
&lt;td&gt;256단계로 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10비트&lt;/td&gt;
&lt;td&gt;2&amp;sup1;⁰ = 1,024&lt;/td&gt;
&lt;td&gt;1,024단계로 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;12비트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2&amp;sup1;&amp;sup2; = &lt;b&gt;4,096&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4,096단계로 구분 (STM32)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16비트&lt;/td&gt;
&lt;td&gt;2&amp;sup1;⁶ = 65,536&lt;/td&gt;
&lt;td&gt;65,536단계로 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32의 ADC는 &lt;b&gt;12비트&lt;/b&gt;이므로, 0~3.3V 전압을 4,096단계로 나눌 수 있다. 1단계(1 LSB)의 전압 분해능은 3.3V &amp;divide; 4096 ≒ &lt;b&gt;0.0008V(약 0.8mV)&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비트 수가 높을수록 원본에 가까운 디지털 값을 얻을 수 있지만, 데이터 용량과 처리 시간이 늘어나는 &lt;b&gt;트레이드오프&lt;/b&gt;가 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;SPI통신 제대로 배워보자&quot;)&lt;/a&gt;에서 다뤘던 &quot;타임베이스 vs 클럭&quot;처럼, 샘플링은 X축(시간)을, 양자화는 Y축(값)을 얼마나 촘촘하게 나누느냐의 문제다. 결국 &lt;b&gt;얼마나 촘촘하게 측정하느냐에 따라 용량과 정확도가 달라진다&lt;/b&gt;는 원리는 통신이든 ADC든 동일하다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Embedded</category>
      <category>ADC</category>
      <category>Analog</category>
      <category>OJTube임베디드입문</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/74</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-39-%EB%84%88%EB%AC%B4%EB%82%98%EB%8F%84-%EC%A4%91%EC%9A%94%ED%95%9C-ADC#entry74comment</comments>
      <pubDate>Wed, 6 May 2026 21:21:03 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 38. 고추건조기 보드완성</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-38-%EA%B3%A0%EC%B6%94%EA%B1%B4%EC%A1%B0%EA%B8%B0-%EB%B3%B4%EB%93%9C%EC%99%84%EC%84%B1</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;버튼 더블클릭 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-37-%EB%B3%B4%EB%93%9C-%EC%99%84%EC%84%B1%EA%B9%8C%EC%A7%80-%EB%91%90%EB%B2%88-%EB%82%A8%EC%95%98%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;보드 완성까지 두번 남았다&quot;)&lt;/a&gt;에서 버튼으로 온도를 설정하고 릴레이를 제어하는 기본 골격을 완성했다. 이번에는 &lt;b&gt;디테일을 다듬어 실제 사용 가능한 수준&lt;/b&gt;으로 마무리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 문제는 &lt;b&gt;버튼을 한 번 눌렀는데 여러 번 입력되는 현상&lt;/b&gt;(더블클릭/채터링)이다. 스위치 접점이 물리적으로 튀면서 인터럽트가 여러 번 발생하기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소프트웨어적 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HAL_GetTick()&lt;/code&gt;으로 이전 버튼 입력 시간을 저장하고, 현재 시간과의 차이가 &lt;b&gt;200ms 미만이면 무시&lt;/b&gt;하도록 구현한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// stm32f1xx_it.c
static uint32_t m_button_before_time = 0;

void EXTI0_IRQHandler(void) {
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);

    uint32_t now = HAL_GetTick();
    if (now - m_button_before_time &amp;lt; 200) return;  // 200ms 이내면 무시
    m_button_before_time = now;

    g_f_sw_up = 1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 버튼(Up/Down/Fix)에 동일하게 적용한다. 200이라는 값은 &lt;code&gt;#define BUTTON_GAP 200&lt;/code&gt;으로 정의하여 개발자가 쉽게 조절할 수 있게 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어적으로는 스위치 출력에 캐패시터를 달아 LPF(Low Pass Filter) 필터링을 하면 해결되지만, 부품이 없으므로 소프트웨어로 처리했다. 실제 제품 설계 시에는 HW/SW 양쪽을 모두 고려하는 것이 좋다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전원 스위치 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PB12에 연결된 Start 스위치는 &lt;b&gt;장비 전체의 on/off를 제어&lt;/b&gt;한다. 이 스위치는 토글 방식(왔다 갔다)이라 인터럽트보다 &lt;b&gt;폴링으로 현재 상태를 읽는 것이 적합&lt;/b&gt;하다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// buttonController.c
uint8_t getSwState(void) {
    if (HAL_GPIO_ReadPin(PB12_START_SW_GPIO_Port, PB12_START_SW_Pin) == GPIO_PIN_RESET) {
        return 1;  // LOW = 켜짐
    }
    return 0;  // HIGH = 꺼짐
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수를 &lt;b&gt;TIM3 인터럽트에서 주기적으로 호출&lt;/b&gt;하여 스위치 상태에 따라 LED1을 제어한다. 스위치를 ON으로 밀면 LED가 켜지고, OFF로 밀면 꺼진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;switch.png&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q179z/dJMcacC11cP/EaKbuIsjhrMn4JAQjgwqB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q179z/dJMcacC11cP/EaKbuIsjhrMn4JAQjgwqB0/img.png&quot; data-alt=&quot;전원 스위치 ON/OFF에 따른 LED 동작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q179z/dJMcacC11cP/EaKbuIsjhrMn4JAQjgwqB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq179z%2FdJMcacC11cP%2FEaKbuIsjhrMn4JAQjgwqB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;697&quot; height=&quot;272&quot; data-filename=&quot;switch.png&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전원 스위치 ON/OFF에 따른 LED 동작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화면 깜빡임 피드백&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fix 버튼을 누르면 &lt;b&gt;OLED 화면이 4번 깜빡이도록&lt;/b&gt; 하여, 사용자에게 &quot;온도가 저장되었다&quot;는 피드백을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// oledController.c
static uint8_t m_toggle = 0;
static uint8_t m_toggle_count = 0;
static uint32_t m_toggle_timer = 0;

void startToggle(void) {
    m_toggle_count = 8;  // 켜짐/꺼짐 각 1번 = 2, 4번 깜빡임 = 8
}

void toggleScreen(void) {
    if (m_toggle_count == 0) return;

    uint32_t now = HAL_GetTick();
    if (now - m_toggle_timer &amp;lt; TOGGLE_TIME) return;
    m_toggle_timer = now;

    m_toggle = !m_toggle;
    if (m_toggle) {
        SSD1306_Fill(SSD1306_COLOR_BLACK);
    } else {
        printBackground();
    }
    SSD1306_UpdateScreen();

    if (m_toggle_count &amp;gt; 0) m_toggle_count--;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버그 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 과정에서 두 가지 버그가 발생했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;m_toggle_count overflow&lt;/b&gt;: 0보다 클 때만 감소시키도록 조건 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;깜빡이는 도중 온도 업데이트&lt;/b&gt;: 화면이 꺼진 상태에서 온도 표시 함수가 호출되어 잘못된 화면이 나타남 &amp;rarr; m_toggle_count == 0일 때만 온도 업데이트가 실행되도록 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OLED 상태 표시 개선&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;히터 상태 출력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히터의 ON/OFF 상태를 OLED에 표시하고, LED2도 연동한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// oledController.c
void printHeaterState(uint8_t state) {
    SSD1306_GotoXY(0, 40);
    if (state) {
        SSD1306_Puts(&quot;Heater: ON &quot;, &amp;amp;Font_7x10, 1);
        HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);  // LED ON
    } else {
        SSD1306_Puts(&quot;Heater: OFF&quot;, &amp;amp;Font_7x10, 1);
        HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET);    // LED OFF
    }
    SSD1306_UpdateScreen();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디폴트 온도 표시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 화면에 0도가 아닌 &lt;b&gt;디폴트 설정 온도(25도)&lt;/b&gt;가 표시되도록 &lt;code&gt;printBackground&lt;/code&gt; 함수에서 &lt;code&gt;getFixedTemper()&lt;/code&gt;를 호출하여 온도를 출력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;1047&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WkAGL/dJMcaaZt5sC/PBkfFdkKkkkPp9U7hbncO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WkAGL/dJMcaaZt5sC/PBkfFdkKkkkPp9U7hbncO1/img.png&quot; data-alt=&quot;OLED 종합 화면 &amp;amp;mdash; 설정/현재 온도/히터 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WkAGL/dJMcaaZt5sC/PBkfFdkKkkkPp9U7hbncO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWkAGL%2FdJMcaaZt5sC%2FPBkfFdkKkkkPp9U7hbncO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1530&quot; height=&quot;1047&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;1047&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OLED 종합 화면 &amp;mdash; 설정/현재 온도/히터 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전원 스위치-히터 연동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전원 스위치가 OFF일 때는 히터가 무조건 꺼지도록&lt;/b&gt; 안전 로직을 추가한다. 설정 온도에 도달하지 않았더라도 사용자가 스위치를 끄면 히터가 즉시 정지해야 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// heaterController.c
void heaterOnOff(void) {
    if (!getSwState()) {
        // 스위치 OFF &amp;rarr; 히터 강제 OFF
        if (heater_state == 1) {
            HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET);
            heater_state = 0;
            printHeaterState(0);
        }
        return;
    }

    // 스위치 ON일 때만 온도 기반 제어
    float current_temp = get_current_temp();
    int fixed_temp = getFixedTemper();
    // ... 온도 비교 로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;미리 끄기 (Pre-off)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-29-%EB%82%9C%EB%B0%A9%EC%8B%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;난방실 만들기&quot;)&lt;/a&gt;에서 언급했던 &lt;b&gt;오버슈트 문제&lt;/b&gt;를 해결한다. 설정 온도에 정확히 도달한 시점에 히터를 끄면, 잔열로 온도가 더 올라간다. 따라서 &lt;b&gt;설정 온도에 도달하기 전에 미리 끈다&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;#define PRE_OFF_GAP  5   // 설정 온도와의 차이가 5도 이하면 OFF
#define RE_ON_GAP    3   // 설정 온도와의 차이가 3도 이상이면 다시 ON

void heaterOnOff(void) {
    // ... 스위치 체크 로직 ...

    float current_temp = get_current_temp();
    int fixed_temp = getFixedTemper();

    if (heater_state == 1 &amp;amp;&amp;amp; (fixed_temp - current_temp) &amp;lt;= PRE_OFF_GAP) {
        // 히터 ON 상태에서 목표와의 차이가 5도 이하 &amp;rarr; OFF
        HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET);
        heater_state = 0;
        printHeaterState(0);
    }
    else if (heater_state == 0 &amp;amp;&amp;amp; (fixed_temp - current_temp) &amp;gt;= RE_ON_GAP) {
        // 히터 OFF 상태에서 목표와의 차이가 3도 이상 &amp;rarr; ON
        HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET);
        heater_state = 1;
        printHeaterState(1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;PRE_OFF_GAP&lt;/code&gt;과 &lt;code&gt;RE_ON_GAP&lt;/code&gt; 값은 &lt;code&gt;#define&lt;/code&gt;으로 정의하여 쉽게 조절할 수 있다. 이 값을 환경에 맞게 &lt;b&gt;반복적으로 조정하는 작업이 실제 개발에서 가장 많은 시간이 소요&lt;/b&gt;되는 부분이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (9).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FAWdC/dJMcadPopNR/XZRgSKUZ0o7MRWFIPhICy0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FAWdC/dJMcadPopNR/XZRgSKUZ0o7MRWFIPhICy0/img.gif&quot; data-alt=&quot;미리 끄기 동작 &amp;amp;mdash; 도달 전 OFF, 차이 벌어지면 ON&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FAWdC/dJMcadPopNR/XZRgSKUZ0o7MRWFIPhICy0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/FAWdC/dJMcadPopNR/XZRgSKUZ0o7MRWFIPhICy0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (9).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;미리 끄기 동작 &amp;mdash; 도달 전 OFF, 차이 벌어지면 ON&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시리즈 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;37강에 걸친 고추 건조기 프로젝트가 완성되었다. 전체 시리즈에서 다룬 내용을 정리하면:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;영역&lt;/th&gt;
&lt;th&gt;학습 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;기초&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;전기 상식, GPIO, 비트 연산, Startup Code, 데이터시트, 회로도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;디버깅&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;멀티미터, 오실로스코프, UART printf, Live Expression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;통신&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SPI(FND), 1-Wire(온도센서), I2C(OLED), UART(디버깅)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;주변장치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;FND, 온도센서(DS18B20), 릴레이, LED, 스위치, OLED(SSD1306)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;소프트웨어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;타이머 인터럽트, 크리티컬 섹션, 외부 인터럽트, 파일 구조 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;프로젝트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;온도 측정 &amp;rarr; FND/OLED 표시 &amp;rarr; 버튼 설정 &amp;rarr; 릴레이 제어 &amp;rarr; 완전체 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도 수준의 회로도를 혼자서 구성하고, 코드를 작성하고, 문제를 해결할 수 있다면 &lt;b&gt;신입 임베디드 개발자로서 충분히 경쟁력 있다&lt;/b&gt;고 강사도 강조한다. 바닥부터 직접 만들어보는 경험이 가장 중요하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시운전 영상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 고추 건조기 보드의 전체 동작을 단계별로 확인한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 오프닝 &amp;mdash; 전원 인가 및 OLED 로고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전원을 인가하면 OLED에 커스텀 로고가 표시된 뒤, 디폴트 설정 온도(25&amp;deg;C)가 표시되는 기본 화면으로 전환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[VIDEO: 전원 인가 &amp;rarr; OLED 로고 &amp;rarr; 기본 화면 전환]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260408_132014_1-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KR7mA/dJMcadBPNl4/4mRXAzYE3x1n5njbIn7c00/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KR7mA/dJMcadBPNl4/4mRXAzYE3x1n5njbIn7c00/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KR7mA/dJMcadBPNl4/4mRXAzYE3x1n5njbIn7c00/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/KR7mA/dJMcadBPNl4/4mRXAzYE3x1n5njbIn7c00/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260408_132014_1-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Start 스위치 ON&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Start 스위치를 ON으로 밀면 LED1이 켜지며 장비가 동작 대기 상태에 들어간다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (8).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W4T2y/dJMcacpt3Xs/T7cqaj5WCrgXG8rKpbGPE1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W4T2y/dJMcacpt3Xs/T7cqaj5WCrgXG8rKpbGPE1/img.gif&quot; data-alt=&quot;Start 스위치 ON &amp;amp;rarr; LED1 점등&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W4T2y/dJMcacpt3Xs/T7cqaj5WCrgXG8rKpbGPE1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/W4T2y/dJMcacpt3Xs/T7cqaj5WCrgXG8rKpbGPE1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (8).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Start 스위치 ON &amp;rarr; LED1 점등&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Up/Down/Fix 스위치로 온도 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Up/Down 버튼으로 목표 온도를 조절하고, Fix 버튼을 누르면 OLED가 4번 깜빡이며 온도가 확정된다. 확정된 온도가 OLED에 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (5).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgGRgc/dJMcagel5Kw/gJJugObgG9zbKY8s0p80Qk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgGRgc/dJMcagel5Kw/gJJugObgG9zbKY8s0p80Qk/img.gif&quot; data-alt=&quot;Up/Down 온도 조절 &amp;amp;rarr; Fix &amp;amp;rarr; 화면 깜빡임 &amp;amp;rarr; 온도 확정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgGRgc/dJMcagel5Kw/gJJugObgG9zbKY8s0p80Qk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bgGRgc/dJMcagel5Kw/gJJugObgG9zbKY8s0p80Qk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (5).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Up/Down 온도 조절 &amp;rarr; Fix &amp;rarr; 화면 깜빡임 &amp;rarr; 온도 확정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 드라이기 작동 및 온도 상승&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 온도보다 현재 온도가 낮으므로 릴레이가 동작하여 드라이기가 켜진다. OLED에 &quot;Heater: ON&quot;이 표시되고, LED2가 켜진다. FND에 실시간 온도가 올라가는 모습이 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (6).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXGwD8/dJMcaad8O64/uBsk6BLjPv5tZDD085jXKk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXGwD8/dJMcaad8O64/uBsk6BLjPv5tZDD085jXKk/img.gif&quot; data-alt=&quot;릴레이 ON &amp;amp;rarr; 드라이기 작동 &amp;amp;rarr; OLED ON + LED2 점등 &amp;amp;rarr; FND 온도 상승&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXGwD8/dJMcaad8O64/uBsk6BLjPv5tZDD085jXKk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cXGwD8/dJMcaad8O64/uBsk6BLjPv5tZDD085jXKk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (6).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;릴레이 ON &amp;rarr; 드라이기 작동 &amp;rarr; OLED ON + LED2 점등 &amp;rarr; FND 온도 상승&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 설정 온도 근접 &amp;mdash; 드라이기 자동 정지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 온도가 &lt;code&gt;m_fixed_temper - PRE_OFF_GAP&lt;/code&gt;에 도달하면, 오버슈트를 방지하기 위해 드라이기가 자동으로 꺼진다. OLED에 &quot;Heater: OFF&quot;로 전환되고, LED2가 꺼진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (7).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v2XCk/dJMcaja1TLv/FJL3tVxC5An3hEjfzrRVG1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v2XCk/dJMcaja1TLv/FJL3tVxC5An3hEjfzrRVG1/img.gif&quot; data-alt=&quot;설정 온도 근접 &amp;amp;rarr; 릴레이 OFF &amp;amp;rarr; OLED OFF + LED2 소등&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v2XCk/dJMcaja1TLv/FJL3tVxC5An3hEjfzrRVG1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/v2XCk/dJMcaja1TLv/FJL3tVxC5An3hEjfzrRVG1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (7).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;설정 온도 근접 &amp;rarr; 릴레이 OFF &amp;rarr; OLED OFF + LED2 소등&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 온도 하강&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이기가 꺼진 후 자연 냉각으로 온도가 서서히 내려간다. FND에 온도가 내려가는 모습이 보인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (10).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2Zaqe/dJMcabxjtD6/fKNigN9y1gcewAl1uJoI8k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2Zaqe/dJMcabxjtD6/fKNigN9y1gcewAl1uJoI8k/img.gif&quot; data-alt=&quot;FND 온도 자연 하강&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2Zaqe/dJMcabxjtD6/fKNigN9y1gcewAl1uJoI8k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/2Zaqe/dJMcabxjtD6/fKNigN9y1gcewAl1uJoI8k/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (10).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND 온도 자연 하강&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 온도 차이 벌어짐 &amp;mdash; 드라이기 자동 재작동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 온도가 &lt;code&gt;m_fixed_temper - RE_ON_GAP&lt;/code&gt; 이상으로 떨어지면, 드라이기가 다시 켜진다. OLED에 &quot;Heater: ON&quot;으로 전환되고, LED2가 다시 켜진다. 이 on/off 반복으로 목표 온도 부근이 유지된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (9).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9ISFU/dJMcacXiB9i/8wA4oqjkRaWfqGOUIWc6mK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9ISFU/dJMcacXiB9i/8wA4oqjkRaWfqGOUIWc6mK/img.gif&quot; data-alt=&quot;충분히 하강 &amp;amp;rarr; 릴레이 ON &amp;amp;rarr; OLED O N + LED2 점등 &amp;amp;rarr; 재상승&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9ISFU/dJMcacXiB9i/8wA4oqjkRaWfqGOUIWc6mK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/c9ISFU/dJMcacXiB9i/8wA4oqjkRaWfqGOUIWc6mK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (9).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;충분히 하강 &amp;rarr; 릴레이 ON &amp;rarr; OLED O N + LED2 점등 &amp;rarr; 재상승&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. Start 스위치 OFF &amp;mdash; 강제 정지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이기가 작동 중인 상태에서 Start 스위치를 OFF로 밀면, 릴레이가 즉시 해제되어 드라이기가 멈춘다. LED1과 LED2가 모두 꺼지고, OLED에 &quot;Heater: OFF&quot;가 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (11).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CP9wZ/dJMcabD6GbM/G678xFlEK1kP95BqJLJpwK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CP9wZ/dJMcabD6GbM/G678xFlEK1kP95BqJLJpwK/img.gif&quot; data-alt=&quot;동작 중 &amp;amp;rarr; 스위치 OFF &amp;amp;rarr; 릴레이 해제 &amp;amp;rarr; LED 소등 &amp;amp;rarr; 드라이기 정지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CP9wZ/dJMcabD6GbM/G678xFlEK1kP95BqJLJpwK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/CP9wZ/dJMcabD6GbM/G678xFlEK1kP95BqJLJpwK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (11).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;동작 중 &amp;rarr; 스위치 OFF &amp;rarr; 릴레이 해제 &amp;rarr; LED 소등 &amp;rarr; 드라이기 정지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>debouncing</category>
      <category>GPIO</category>
      <category>INTERRUPT</category>
      <category>OJTube임베디드입문</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/73</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-38-%EA%B3%A0%EC%B6%94%EA%B1%B4%EC%A1%B0%EA%B8%B0-%EB%B3%B4%EB%93%9C%EC%99%84%EC%84%B1#entry73comment</comments>
      <pubDate>Wed, 6 May 2026 21:08:13 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 37. 보드 완성까지 두번 남았다</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-37-%EB%B3%B4%EB%93%9C-%EC%99%84%EC%84%B1%EA%B9%8C%EC%A7%80-%EB%91%90%EB%B2%88-%EB%82%A8%EC%95%98%EB%8B%A4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPIO SPI &amp;rarr; HW SPI 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-36-%EB%93%9C%EB%94%94%EC%96%B4-%EC%99%84%EC%84%B1%EB%90%98%EB%8A%94-%EC%99%84%EC%A0%84%EC%B2%B4-%EB%B3%B4%EB%93%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;드디어 완성되는 완전체 보드&quot;)&lt;/a&gt;에서 모든 모듈의 하드웨어 점검을 마쳤다. 이번에는 본격적인 코딩에 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 해결할 문제는 &lt;b&gt;FND가 올바로 켜지지 않는 현상&lt;/b&gt;이다. 장치가 늘어나면서 MCU의 작업량이 증가했고, GPIO로 직접 SPI 신호를 만드는 방식으로는 FND 갱신에 필요한 타이밍을 안정적으로 유지하기 어려워졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-22-STM32%EC%97%90%EC%84%9C%EB%8A%94-SPI-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%9C%EB%8B%A4%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;STM32에서는 SPI 기능을 제공한다구&quot;)&lt;/a&gt;에서 다뤘듯이, GPIO SPI를 &lt;b&gt;칩에서 제공하는 HW SPI(HAL_SPI_Transmit)&lt;/b&gt;로 전환하면 MCU의 부담이 줄어든다. FND 제어 코드의 &lt;code&gt;send&lt;/code&gt; 함수를 HAL_SPI_Transmit 한 줄로 교체하고, &lt;code&gt;init_fnd&lt;/code&gt;에 SPI 핸들러를 인자로 넘기도록 수정한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// fnd_controller.c
void init_fnd(SPI_HandleTypeDef *hspi) {
    _hspi = hspi;
    // 매핑 테이블 초기화
}

void send(uint8_t X) {
    HAL_SPI_Transmit(_hspi, &amp;amp;X, 1, 100);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핀 배치도 HW SPI에 맞게 변경한다. 이 과정은 &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-36-%EB%93%9C%EB%94%94%EC%96%B4-%EC%99%84%EC%84%B1%EB%90%98%EB%8A%94-%EC%99%84%EC%A0%84%EC%B2%B4-%EB%B3%B4%EB%93%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글&lt;/a&gt;에서 상세히 다뤘으므로 참고한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 구조 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.c에 모든 코드가 몰려 있으면 관리가 어렵다. &lt;b&gt;기능별로 파일을 분리&lt;/b&gt;하여 구조를 정리한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;oledController.c/h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OLED 초기화, 로고 출력, 온도 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;buttonController.c/h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;버튼 인터럽트 처리, 온도 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heaterController.c/h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;온도 센서 읽기, 릴레이 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fnd_controller.c/h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FND 제어 (기존)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g_var.h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;전역 변수 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ex_var.h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;extern 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH5BSo/dJMcagSXgzS/xSaoW4uDGqDubN2zfLXMqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH5BSo/dJMcagSXgzS/xSaoW4uDGqDubN2zfLXMqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH5BSo/dJMcagSXgzS/xSaoW4uDGqDubN2zfLXMqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH5BSo%2FdJMcagSXgzS%2FxSaoW4uDGqDubN2zfLXMqk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;525&quot; height=&quot;820&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.c에서는 각 컨트롤러의 초기화 함수를 호출하고, while 루프에서 버튼 체크와 온도 제어만 수행하는 깔끔한 구조가 된다.&lt;/p&gt;
&lt;!-- 이미지: 파일 구조 — Lib 폴더 내 컨트롤러 파일들 (본인 캡처) --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OLED에 온도 표시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Print_Temp 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OLED에 온도를 표시하는 함수를 &lt;code&gt;oledController.c&lt;/code&gt;에 구현한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// oledController.c
#include &amp;lt;stdio.h&amp;gt;

void printTemper(int temper) {
    char buf[16];
    sprintf(buf, &quot;Temp: %02d&quot;, temper);
    SSD1306_GotoXY(0, 0);
    SSD1306_Puts(buf, &amp;amp;Font_11x18, 1);
    SSD1306_UpdateScreen();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sprintf&lt;/code&gt;로 숫자를 문자열로 변환하고, &lt;code&gt;SSD1306_Puts&lt;/code&gt;로 버퍼에 쓴 뒤, &lt;code&gt;SSD1306_UpdateScreen&lt;/code&gt;으로 화면을 갱신한다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-35-%EB%82%B4-%EB%A1%9C%EA%B3%A0%EB%8A%94-%EB%82%B4%EA%B0%80-%EB%A7%8C%EB%93%A0%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;내 로고는 내가 만든다&quot;)&lt;/a&gt;에서 다뤘던 &lt;b&gt;&quot;버퍼에 그리고 &amp;rarr; UpdateScreen으로 전송&quot;&lt;/b&gt; 패턴 그대로다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Opening 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작 시 로고를 표시한 후 기본 화면으로 전환하는 흐름을 함수로 묶는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;void oled_opening(void) {
    SSD1306_Init();
    SSD1306_InvertDisplay(1);
    // 로고 애니메이션 출력
    SSD1306_DrawBitmap(0, 0, my_logo_1, 128, 64, 1);
    SSD1306_UpdateScreen();
    HAL_Delay(1000);
    // 기본 화면으로 전환
    printDefault();
}&lt;/code&gt;&lt;/pre&gt;
&lt;!-- 이미지: OLED 기본 화면 — 온도 표시 (본인 촬영) --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버튼으로 온도 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 가지 온도 변수&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;변수&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_desired_temper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Up/Down 버튼으로 &lt;b&gt;조절 중인&lt;/b&gt; 온도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;m_fixed_temper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fix 버튼으로 &lt;b&gt;확정한&lt;/b&gt; 목표 온도&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디폴트 온도는 헤더 파일에 &lt;code&gt;DEFAULT_TEMP = 25&lt;/code&gt;로 정의한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;buttonController&lt;/h3&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;// buttonController.c
static int m_desired_temper = DEFAULT_TEMP;
static int m_fixed_temper = DEFAULT_TEMP;

void checkButton(void) {
    if (g_f_sw_up) {
        g_f_sw_up = 0;
        if (m_desired_temper &amp;lt; 99) m_desired_temper++;
        printTemper(m_desired_temper);
    }
    if (g_f_sw_down) {
        g_f_sw_down = 0;
        if (m_desired_temper &amp;gt; 0) m_desired_temper--;
        printTemper(m_desired_temper);
    }
    if (g_f_sw_fix) {
        g_f_sw_fix = 0;
        m_fixed_temper = m_desired_temper;
    }
}

int getFixedTemper(void) {
    return m_fixed_temper;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Up 버튼을 누르면 온도가 1도 올라가고, Down 버튼으로 1도 내려간다. 0~99도 범위를 벗어나지 않도록 제한한다. Fix 버튼을 누르면 현재 조절 중인 온도가 &lt;b&gt;목표 온도로 확정&lt;/b&gt;된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260327_193312-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F6tmh/dJMcabxjtdv/kYhhuGLISaCTvffvXNVt71/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F6tmh/dJMcabxjtdv/kYhhuGLISaCTvffvXNVt71/img.gif&quot; data-alt=&quot;OLED Up/Down 온도 조절&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F6tmh/dJMcabxjtdv/kYhhuGLISaCTvffvXNVt71/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/F6tmh/dJMcabxjtdv/kYhhuGLISaCTvffvXNVt71/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260327_193312-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OLED Up/Down 온도 조절&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;!-- 이미지: OLED에서 Up/Down으로 온도 조절하는 모습 (본인 촬영) --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설정 온도 기반 릴레이 제어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제어 로직&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-29-%EB%82%9C%EB%B0%A9%EC%8B%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;난방실 만들기&quot;)&lt;/a&gt;에서는 하드코딩된 45~50도 범위로 제어했다. 이번에는 &lt;b&gt;버튼으로 설정한 목표 온도와 현재 온도를 비교&lt;/b&gt;하여 릴레이를 제어한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// heaterController.c
void heaterOnOff(void) {
    float current_temp = get_current_temp();
    int fixed_temp = getFixedTemper();

    if (current_temp &amp;lt; (float)fixed_temp) {
        // 현재 온도 &amp;lt; 설정 온도 &amp;rarr; 드라이기 ON
        HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET);
    } else {
        // 현재 온도 &amp;ge; 설정 온도 &amp;rarr; 드라이기 OFF
        HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;main 루프&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c
oled_opening();
Ds18b20_Init_Simple();
init_fnd(&amp;amp;hspi2);
HAL_TIM_Base_Start_IT(&amp;amp;htim3);

int m_count = 0;

while (1) {
    checkButton();

    m_count++;
    if (m_count &amp;gt;= 10) {  // 약 1초마다
        m_count = 0;
        heaterOnOff();
    }

    HAL_Delay(100);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도를 1초마다 확인하여 릴레이를 제어한다. 100ms마다 버튼을 체크하고, 10번째 루프(≒1초)마다 온도를 읽어 릴레이를 on/off한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (3).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kUecY/dJMcaciIl7H/duLmSWv6kWPudBuqyYgymk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kUecY/dJMcaciIl7H/duLmSWv6kWPudBuqyYgymk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kUecY/dJMcaciIl7H/duLmSWv6kWPudBuqyYgymk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/kUecY/dJMcaciIl7H/duLmSWv6kWPudBuqyYgymk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (3).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;!-- 이미지: 설정 온도보다 낮을 때 릴레이 ON → 온도 상승 → 설정 온도 도달 시 OFF (본인 촬영) --&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;임베디드 개발의 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의로 고추 건조기의 &lt;b&gt;핵심 골격이 완성&lt;/b&gt;되었다. 강사는 임베디드 개발에서 &quot;길 뚫기&quot; &amp;mdash; 하드웨어 장치를 코드로 제어할 수 있는 환경을 만드는 과정 &amp;mdash; 가 &lt;b&gt;전체 시간의 80% 이상&lt;/b&gt;을 차지한다고 강조한다. 지금까지 35강에 걸쳐 해온 것이 바로 이 &quot;길 뚫기&quot;였고, 응용 소프트웨어(버튼으로 온도 설정, 릴레이 제어 로직 등)는 상대적으로 빠르게 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 기본 골격을 &lt;b&gt;더 다듬어&lt;/b&gt; 실제 사용 가능한 수준으로 마무리한다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>hal</category>
      <category>INTERRUPT</category>
      <category>OJTube임베디드입문</category>
      <category>SPI</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/72</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-37-%EB%B3%B4%EB%93%9C-%EC%99%84%EC%84%B1%EA%B9%8C%EC%A7%80-%EB%91%90%EB%B2%88-%EB%82%A8%EC%95%98%EB%8B%A4#entry72comment</comments>
      <pubDate>Wed, 6 May 2026 20:51:56 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 36. 드디어 완성되는 완전체 보드</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-36-%EB%93%9C%EB%94%94%EC%96%B4-%EC%99%84%EC%84%B1%EB%90%98%EB%8A%94-%EC%99%84%EC%A0%84%EC%B2%B4-%EB%B3%B4%EB%93%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;완전체 보드의 목표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 각 모듈을 개별적으로 다뤄왔다. 이번 강의에서는 &lt;b&gt;모든 모듈을 하나의 보드에 통합하고, 각각이 정상 동작하는지 점검&lt;/b&gt;한다. 회로도 v4 기준으로 진행하며, 이전 버전과 핀 배치가 일부 변경되었으므로 주의가 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 핀 배치 요약&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모듈&lt;/th&gt;
&lt;th&gt;핀&lt;/th&gt;
&lt;th&gt;이전 글&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;스위치 (Up/Fix/Down)&lt;/td&gt;
&lt;td&gt;PB0 / PB1 / PB2&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-17-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-LED%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;내 힘으로 LED 회로 만들어서 제어하기&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스위치 (Start)&lt;/td&gt;
&lt;td&gt;PB12&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UART TX&lt;/td&gt;
&lt;td&gt;PA9&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-16-printf%EB%8F%84-%EC%89%BD%EC%A7%80-%EC%95%8A%EB%8B%A4%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;printf도 쉽지 않다구&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LED &amp;times; 2&lt;/td&gt;
&lt;td&gt;PB6 / PB7&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-17-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-LED%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;내 힘으로 LED 회로 만들어서 제어하기&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OLED (I2C)&lt;/td&gt;
&lt;td&gt;PB10(SCL) / PB11(SDA)&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-34-%EB%B0%B0%EC%9A%B4-I2C%EB%A1%9C-OLED%EB%A5%BC-%EC%A0%9C%EC%96%B4%ED%95%B4%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;배운 I2C로 OLED를 제어해보자&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;온도 센서 (1-Wire)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PA2&lt;/b&gt; (v4에서 변경)&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-24-1-Wire-%ED%86%B5%EC%8B%A0-%EB%82%98%EB%A6%84-%EC%9C%A0%EB%AA%85%ED%96%88%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;1-Wire 통신&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FND (SPI)&lt;/td&gt;
&lt;td&gt;PB15(SCLK) / PB13(RCLK) / PB14(DIO)&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-22-STM32%EC%97%90%EC%84%9C%EB%8A%94-SPI-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%9C%EB%8B%A4%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;STM32에서는 SPI기능을 제공한다구&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;릴레이&lt;/td&gt;
&lt;td&gt;PB5&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-28-%EB%93%9C%EB%9D%BC%EC%9D%B4%EA%B8%B0%EB%A5%BC-%EB%82%B4-%EB%A7%98%EB%8C%80%EB%A1%9C-%EA%BB%90%EB%8B%A4-%EC%BC%B0%EB%8B%A4-%ED%95%B4%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;드라이기를 내 맘대로 껐다, 켰다 해보자&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260324_202930.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dUBvwn/dJMcafT3e3B/8tW0IXmQH9PPCjNedPsR70/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dUBvwn/dJMcafT3e3B/8tW0IXmQH9PPCjNedPsR70/img.jpg&quot; data-alt=&quot;완성된 회로도 v4 전체&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dUBvwn/dJMcafT3e3B/8tW0IXmQH9PPCjNedPsR70/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdUBvwn%2FdJMcafT3e3B%2F8tW0IXmQH9PPCjNedPsR70%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260324_202930.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;완성된 회로도 v4 전체&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스위치 점검 및 인터럽트 전환&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스위치 배선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회로도에 따라 PB0(Up), PB1(Fix), PB2(Down) 세 개의 스위치를 빵판에 구성한다. 이 스위치들은 최종적으로 &lt;b&gt;목표 온도를 올리고 내리며, 설정을 저장하는 기능&lt;/b&gt;을 수행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260324_202940.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csaAH2/dJMcacJN4wy/KHQu4JqBEYLLxidDOaA5N0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csaAH2/dJMcacJN4wy/KHQu4JqBEYLLxidDOaA5N0/img.jpg&quot; data-alt=&quot;스위치 3개 빵판 구성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csaAH2/dJMcacJN4wy/KHQu4JqBEYLLxidDOaA5N0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsaAH2%2FdJMcacJN4wy%2FKHQu4JqBEYLLxidDOaA5N0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260324_202940.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스위치 3개 빵판 구성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;폴링 &amp;rarr; 인터럽트 전환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;HAL_GPIO_ReadPin&lt;/code&gt;으로 스위치 상태를 확인(폴링)했지만, 더 효율적인 동작을 위해 &lt;b&gt;인터럽트 방식&lt;/b&gt;으로 전환한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;867&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnr4za/dJMcaja1ST4/Bon8qzk3Em0iJmBvDoQNd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnr4za/dJMcaja1ST4/Bon8qzk3Em0iJmBvDoQNd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnr4za/dJMcaja1ST4/Bon8qzk3Em0iJmBvDoQNd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcnr4za%2FdJMcaja1ST4%2FBon8qzk3Em0iJmBvDoQNd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1314&quot; height=&quot;867&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;867&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정 항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPIO Mode&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GPIO_EXTI&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;외부 인터럽트 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPIO Trigger&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Falling Edge&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;평상시 High &amp;rarr; 버튼 누르면 Low. 하강 순간에 감지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIC 우선순위&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;다른 인터럽트와 우선순위 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;글로벌 플래그 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터럽트 핸들러 안에서 printf 같은 긴 작업을 하면 문제가 발생할 수 있다. 대신 &lt;b&gt;전역 변수(플래그)를 설정하고, main 루프에서 플래그를 확인&lt;/b&gt;하는 패턴을 사용한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// g_var.h &amp;mdash; 전역 변수 선언
char g_f_sw_up = 0;
char g_f_sw_down = 0;
char g_f_sw_fix = 0;

// ex_var.h &amp;mdash; extern 선언
extern char g_f_sw_up;
extern char g_f_sw_down;
extern char g_f_sw_fix;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// stm32f1xx_it.c &amp;mdash; 인터럽트 핸들러
void EXTI0_IRQHandler(void) {
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
    g_f_sw_up = 1;  // 플래그만 설정
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;// main.c &amp;mdash; 메인 루프에서 처리
if (g_f_sw_up) {
    g_f_sw_up = 0;
    printf(&quot;UP 버튼 눌림\r\n&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PB1(Fix), PB2(Down)도 동일한 방식으로 설정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PB12 (Start 스위치) 트러블슈팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PB12에 연결한 Start 스위치가 처음에 동작하지 않았다. 원인은 &lt;b&gt;택타일 스위치의 핀 배열을 잘못 연결&lt;/b&gt;한 것이었다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-18-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-%EC%8A%A4%EC%9C%84%EC%B9%98%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;내 힘으로 스위치 회로 만들기&quot;)&lt;/a&gt;에서 다뤘듯이 택타일 스위치는 핀 1/3, 2/4가 내부적으로 연결된 쌍이다. 2번 핀이 GND에 연결되어야 하는데 1번 핀을 연결했던 것이 문제였다. 수정 후 정상 동작을 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UART 통신 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PA9(USART TX)을 FTDI 모듈의 RX에 연결하고, GND를 공유한다. 시리얼 모니터에서 &lt;b&gt;&quot;Hello World&quot;가 정상 출력&lt;/b&gt;되어 UART 통신에 문제가 없음을 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LED 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PB6과 PB7에 LED를 연결한다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-17-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-LED%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;내 힘으로 LED 회로 만들어서 제어하기&quot;)&lt;/a&gt;에서 다뤘듯이 이 회로에서 LED는 &lt;b&gt;GPIO가 Low일 때 켜지는 구조&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_6);
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
HAL_Delay(100);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.1초 간격으로 LED가 빠르게 점멸하는 것을 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OLED 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PB10(SCL)과 PB11(SDA)에 OLED 모듈을 연결한다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-35-%EB%82%B4-%EB%A1%9C%EA%B3%A0%EB%8A%94-%EB%82%B4%EA%B0%80-%EB%A7%8C%EB%93%A0%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;내 로고는 내가 만든다&quot;)&lt;/a&gt;에서 제작한 로고 출력 코드를 활성화하면, &lt;b&gt;OLED에 로고가 정상 출력&lt;/b&gt;되는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[IMG: OLED 로고 출력]&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온도 센서 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회로도 v4에서 온도 센서의 DAT 핀이 &lt;b&gt;PA3 &amp;rarr; PA2로 변경&lt;/b&gt;되었다. 모듈의 DAT을 PA2에 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 테스트에서 온도가 출력되지 않는 문제가 발생했다. 원인은 .ioc에서 &lt;b&gt;PA2의 GPIO 설정이 변경되지 않았기 때문&lt;/b&gt;이었다. PA2를 GPIO Output으로 설정하고, &lt;code&gt;ds18b20Config.h&lt;/code&gt;의 GPIO/PIN 정의도 PA2로 수정하면 해결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수정 후 &lt;b&gt;현재 온도(24.6&amp;deg;C)가 정상 출력&lt;/b&gt;되고, 프로브를 손으로 잡으면 온도가 올라가는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[IMG: 온도 센서 정상 동작]&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FND 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PB15(SCLK), PB13(RCLK), PB14(DIO)에 FND 모듈을 연결한다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-30-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%9D%89%EB%82%B4%EB%82%B4%EA%B8%B0-%EB%AA%A8%EB%93%A0-%EC%9E%A5%EC%B9%98-%ED%86%B5%ED%95%A9%EC%8B%9C%ED%82%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;쓰레드 흉내내기&quot;)&lt;/a&gt;에서 구현한 TIM3 타이머 인터럽트 기반 FND 코드를 활성화한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c
init_fnd(&amp;amp;hspi2);
HAL_TIM_Base_Start_IT(&amp;amp;htim3);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FND에 현재 온도가 실시간으로 표시&lt;/b&gt;되는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[IMG: FND 온도 표시]&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;릴레이 점검&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PB5에 릴레이를 연결한다. Start 스위치(PB12) 인터럽트와 연동하여, 스위치를 누르면 릴레이가 토글되도록 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// main.c
if (g_f_sw_on) {
    g_f_sw_on = 0;
    HAL_GPIO_TogglePin(PB5_RELAY_ON_OFF_CTRL_GPIO_Port,
                        PB5_RELAY_ON_OFF_CTRL_Pin);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 릴레이가 반응하지 않았으나, &lt;b&gt;전원을 뺐다 다시 꽂으니&lt;/b&gt; 딸깍 소리와 함께 정상 동작했다. 드라이기를 릴레이에 연결하여 &lt;b&gt;스위치 조작으로 드라이기가 켜지고 꺼지는&lt;/b&gt; 최종 테스트를 성공적으로 마쳤다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260325_214138.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Zazvd/dJMcaiwq0J7/cBKqzlQcKmK9I2LrFdJJK1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Zazvd/dJMcaiwq0J7/cBKqzlQcKmK9I2LrFdJJK1/img.jpg&quot; data-alt=&quot;릴레이 + 드라이기 최종 동작&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zazvd/dJMcaiwq0J7/cBKqzlQcKmK9I2LrFdJJK1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZazvd%2FdJMcaiwq0J7%2FcBKqzlQcKmK9I2LrFdJJK1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260325_214138.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;릴레이 + 드라이기 최종 동작&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의로 고추 건조기에 필요한 &lt;b&gt;모든 하드웨어 구성과 검증이 완료&lt;/b&gt;되었다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모듈&lt;/th&gt;
&lt;th&gt;상태&lt;/th&gt;
&lt;th&gt;핵심 학습 내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;스위치 &amp;times; 4&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;GPIO Input &amp;rarr; EXTI 인터럽트, Falling Edge, 글로벌 플래그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UART&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;printf 디버깅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LED &amp;times; 2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;GPIO Output, Toggle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OLED&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;I2C 통신, SSD1306 라이브러리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;온도 센서&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;1-Wire 통신, DS18B20 라이브러리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FND&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;SPI 통신, 타이머 인터럽트로 잔상 효과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;릴레이&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;트랜지스터 구동, Low Level Trigger&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도 수준의 회로도를 &lt;b&gt;혼자서 구성하고 이해할 수 있다면 신입으로서 충분히 경쟁력 있다&lt;/b&gt;고 강사도 강조한다. 다음 글에서는 이 하드웨어를 바탕으로 &lt;b&gt;본격적인 시스템 코딩&lt;/b&gt;에 들어간다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>GPIO</category>
      <category>INTERRUPT</category>
      <category>OJTube임베디드입문</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/71</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-36-%EB%93%9C%EB%94%94%EC%96%B4-%EC%99%84%EC%84%B1%EB%90%98%EB%8A%94-%EC%99%84%EC%A0%84%EC%B2%B4-%EB%B3%B4%EB%93%9C#entry71comment</comments>
      <pubDate>Wed, 6 May 2026 20:39:56 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 35. 내 로고는 내가 만든다</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-35-%EB%82%B4-%EB%A1%9C%EA%B3%A0%EB%8A%94-%EB%82%B4%EA%B0%80-%EB%A7%8C%EB%93%A0%EB%8B%A4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;OLED 로고 출력의 목표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-34-%EB%B0%B0%EC%9A%B4-I2C%EB%A1%9C-OLED%EB%A5%BC-%EC%A0%9C%EC%96%B4%ED%95%B4%EB%B3%B4%EC%9E%90?category=1228736&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;배운 I2C로 OLED를 제어해보자&quot;)&lt;/a&gt;에서 SSD1306 라이브러리를 포팅하고, Fast Mode(400kHz)로 변경하여 OLED를 성공적으로 제어했다. 샘플 코드에 포함된 말 애니메이션과 Hello World가 출력되는 것을 확인했는데, 이번에는 &lt;b&gt;샘플 이미지를 자신만의 로고로 교체&lt;/b&gt;한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로고 이미지 제작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OLED의 해상도가 128&amp;times;64이므로, 로고 이미지도 &lt;b&gt;128&amp;times;64 픽셀&lt;/b&gt;로 만들어야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;그림판&lt;/b&gt; 등 이미지 편집 도구를 열고, 캔버스 크기를 128&amp;times;64로 설정&lt;/li&gt;
&lt;li&gt;텍스트, 도형 등을 추가하여 원하는 로고를 디자인&lt;/li&gt;
&lt;li&gt;흑백(모노크롬)으로 저장 &amp;mdash; OLED가 흑백이므로 색상은 의미 없음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션 효과를 원한다면 &lt;b&gt;여러 버전의 로고 이미지&lt;/b&gt;를 만들어둔다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비트맵 변환 &amp;mdash; image2cpp&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만든 이미지를 코드에서 사용하려면 &lt;b&gt;C 배열 형태로 변환&lt;/b&gt;해야 한다. &lt;a href=&quot;http://javl.github.io/image2cpp/&quot;&gt;image2cpp&lt;/a&gt; 사이트를 사용한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사이트에 이미지 업로드&lt;/li&gt;
&lt;li&gt;Canvas size를 128&amp;times;64로 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Generate code&lt;/b&gt; 클릭 &amp;rarr; C 배열 코드가 생성됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 코드는 이전 글에서 다뤘던 OLED 메모리 구조(128바이트 &amp;times; 8 page = 1024바이트)에 대응하는 배열이다. 각 바이트가 세로 8픽셀을 ON/OFF하는 구조 그대로다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;1161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/udAb8/dJMcahxzcf2/LDV6jk175ksmvpMUm2Kz30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/udAb8/dJMcahxzcf2/LDV6jk175ksmvpMUm2Kz30/img.png&quot; data-alt=&quot;image2cpp &amp;amp;mdash; 이미지 업로드 및 코드 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/udAb8/dJMcahxzcf2/LDV6jk175ksmvpMUm2Kz30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FudAb8%2FdJMcahxzcf2%2FLDV6jk175ksmvpMUm2Kz30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2208&quot; height=&quot;1161&quot; data-origin-width=&quot;2208&quot; data-origin-height=&quot;1161&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;image2cpp &amp;mdash; 이미지 업로드 및 코드 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 적용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;헤더 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 샘플의 &lt;code&gt;horse_anim.h&lt;/code&gt;를 복사하여 새 헤더 파일(예: &lt;code&gt;my_logo.h&lt;/code&gt;)을 만든다. 내부의 배열 데이터를 image2cpp에서 생성한 코드로 교체한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;// my_logo.h
#ifndef MY_LOGO_H
#define MY_LOGO_H

const unsigned char my_logo_1[] = {
    // image2cpp에서 생성된 1024바이트 데이터
    0x00, 0x00, 0x00, ...
};

const unsigned char my_logo_2[] = {
    // 애니메이션용 두 번째 프레임
    0x00, 0x00, 0x00, ...
};

#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 버전의 로고가 있다면 배열을 &lt;code&gt;my_logo_1&lt;/code&gt;, &lt;code&gt;my_logo_2&lt;/code&gt; 등으로 나눈다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;main에서 출력&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// main.c
#include &quot;ssd1306.h&quot;
#include &quot;my_logo.h&quot;

// main 함수 내
SSD1306_Init();
SSD1306_InvertDisplay(1);  // 색 반전: 검은 바탕에 흰색 그림

while (1) {
    SSD1306_DrawBitmap(0, 0, my_logo_1, 128, 64, 1);
    SSD1306_UpdateScreen();
    HAL_Delay(500);

    SSD1306_DrawBitmap(0, 0, my_logo_2, 128, 64, 1);
    SSD1306_UpdateScreen();
    HAL_Delay(500);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SSD1306_InvertDisplay(1)&lt;/code&gt;을 사용하면 &lt;b&gt;검은 바탕에 흰색 그림&lt;/b&gt;으로 표시되어 더 선명하다. &lt;code&gt;HAL_Delay&lt;/code&gt;로 프레임 간 딜레이를 주면 간단한 애니메이션이 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260406_211554-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wXnev/dJMcagk5SrW/5dN0xD9Opz7gGurZS6eHvk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wXnev/dJMcagk5SrW/5dN0xD9Opz7gGurZS6eHvk/img.gif&quot; data-alt=&quot;OLED에 커스텀 로고 출력&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wXnev/dJMcagk5SrW/5dN0xD9Opz7gGurZS6eHvk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/wXnev/dJMcagk5SrW/5dN0xD9Opz7gGurZS6eHvk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260406_211554-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OLED에 커스텀 로고 출력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OLED 내부 동작 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 OLED의 메모리 구조(page, 1024바이트)를 다뤘다. 이번에는 &lt;b&gt;코드가 내부적으로 어떤 흐름으로 동작하는지&lt;/b&gt; 분석한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 흐름&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_32_oled_flow.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvR6RJ/dJMcabKUDUn/2g7goirLk1Bcr9y9OGakN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvR6RJ/dJMcabKUDUn/2g7goirLk1Bcr9y9OGakN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvR6RJ/dJMcabKUDUn/2g7goirLk1Bcr9y9OGakN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvR6RJ%2FdJMcabKUDUn%2F2g7goirLk1Bcr9y9OGakN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;800&quot; data-filename=&quot;diagram_32_oled_flow.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;STM32 내부 버퍼에 먼저 그리고, UpdateScreen으로 한 번에 전송&lt;/b&gt;하는 구조다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;SSD1306_DrawBitmap&lt;/code&gt; / &lt;code&gt;SSD1306_Puts&lt;/code&gt; 등 &amp;rarr; 내부 버퍼(&lt;code&gt;SSD1306_Buffer&lt;/code&gt;)에 데이터를 쓰기만 함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SSD1306_UpdateScreen&lt;/code&gt; &amp;rarr; 버퍼의 1024바이트를 &lt;b&gt;I2C로 한꺼번에 OLED 칩에 전송&lt;/b&gt; &amp;rarr; 화면 갱신&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UpdateScreen 이전에 호출하는 함수들은 모두 &lt;b&gt;화면에 쓸 데이터를 버퍼에 준비하는 과정&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DrawBitmap &amp;rarr; DrawPixel &amp;rarr; SSD1306_Buffer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SSD1306_DrawBitmap&lt;/code&gt; 함수는 비트맵 배열(image2cpp로 생성한 데이터)을 읽으면서, 각 픽셀에 대해 &lt;code&gt;SSD1306_DrawPixel&lt;/code&gt;을 호출한다. &lt;code&gt;DrawPixel&lt;/code&gt;은 x, y 좌표를 받아 &lt;code&gt;SSD1306_Buffer&lt;/code&gt;의 해당 위치에 비트를 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 &lt;code&gt;SSD1306_Buffer&lt;/code&gt;에 완성된 이미지가 담기고, &lt;code&gt;UpdateScreen&lt;/code&gt; 내부의 &lt;code&gt;WriteMulti&lt;/code&gt; 함수가 이 버퍼를 I2C로 전송한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PutChar &amp;mdash; 텍스트 출력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SSD1306_Puts&lt;/code&gt;는 문자열을 한 글자씩 &lt;code&gt;PutChar&lt;/code&gt;로 처리한다. &lt;code&gt;PutChar&lt;/code&gt;는 아스키 코드 32번(스페이스)을 기준으로 폰트 데이터에서 해당 글자의 픽셀 정보를 가져와 &lt;code&gt;DrawPixel&lt;/code&gt;로 버퍼에 점을 찍는다. 폰트 크기(Font_7x10, Font_11x18 등)에 따라 사용하는 픽셀 수가 달라진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스크롤링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크롤링은 &lt;code&gt;SSD1306&lt;/code&gt; 칩 자체에서 지원하는 하드웨어 명령어(0x26, 0x27 등)로 구현된다. 버퍼의 픽셀을 하나씩 옮기는 소프트웨어 방식보다 훨씬 효율적이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글까지 해서 OLED 시리즈가 완결된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;이제는 쉬워진 I2C 통신&lt;/b&gt; &amp;mdash; 클럭 기반 동기 방식, 7비트 주소, SCL/SDA 두 선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배운 I2C로 OLED를 제어해보자&lt;/b&gt; &amp;mdash; SSD1306 라이브러리 포팅, Fast Mode 필수, 128&amp;times;64 = 1024바이트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이번 글&lt;/b&gt; &amp;mdash; image2cpp로 커스텀 로고 제작, 내부 동작 분석(버퍼 &amp;rarr; UpdateScreen &amp;rarr; I2C 전송)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 지금까지 다룬 모든 모듈(온도 센서, FND, OLED, 스위치, LED, 릴레이)을 &lt;b&gt;하나의 보드에 통합&lt;/b&gt;한다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>I2C</category>
      <category>OJTube임베디드입문</category>
      <category>OLED</category>
      <category>SSD1306</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/70</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-35-%EB%82%B4-%EB%A1%9C%EA%B3%A0%EB%8A%94-%EB%82%B4%EA%B0%80-%EB%A7%8C%EB%93%A0%EB%8B%A4#entry70comment</comments>
      <pubDate>Wed, 6 May 2026 20:26:15 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 34. 배운 I2C로 OLED를 제어해보자!</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-34-%EB%B0%B0%EC%9A%B4-I2C%EB%A1%9C-OLED%EB%A5%BC-%EC%A0%9C%EC%96%B4%ED%95%B4%EB%B3%B4%EC%9E%90</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;I2C 옵션 보충&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-33-%EC%9D%B4%EC%A0%9C%EB%8A%94-%EC%89%AC%EC%9B%8C%EC%A7%84-I2C-%ED%86%B5%EC%8B%A0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;이제는 쉬워진 I2C 통신&quot;)&lt;/a&gt;에서 I2C의 기본 개념을 다뤘다. 이번에는 실제 설정에서 만나는 옵션들을 보충한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fast Mode Duty Cycle&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_30_duty_cycle.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGUDJh/dJMcag6tZfl/pCN7ulMqNaDD6yDI8XRIzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGUDJh/dJMcag6tZfl/pCN7ulMqNaDD6yDI8XRIzK/img.png&quot; data-alt=&quot;diagram_01_duty_cycle&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGUDJh/dJMcag6tZfl/pCN7ulMqNaDD6yDI8XRIzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGUDJh%2FdJMcag6tZfl%2FpCN7ulMqNaDD6yDI8XRIzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;840&quot; data-filename=&quot;diagram_30_duty_cycle.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_01_duty_cycle&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 I2C는 Standard Mode(100kHz)와 Fast Mode(400kHz)가 있다고 다뤘다. Fast Mode에서는 &lt;b&gt;Duty Cycle이 1:1이 아닌 1:2 또는 1:1.77&lt;/b&gt;로 설정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;400kHz는 1사이클이 2.5&amp;mu;s다. 이를 상승(High)과 하강(Low)으로 나누면 각각 1.25&amp;mu;s가 되어야 한다. 그런데 I2C를 정의한 NXP(구 필립스)의 사양에 따르면 &lt;b&gt;Low 구간의 최소 유지 시간이 1.3&amp;mu;s 이상&lt;/b&gt;이어야 한다. 1.25&amp;mu;s로는 이 스펙을 충족하지 못하므로, Low 구간을 더 길게 잡아 1:2(High 0.83&amp;mu;s, Low 1.67&amp;mu;s) 또는 1:1.77 비율이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Clock No Stretch Mode&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_31_clock_stretch.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/onveQ/dJMcahRPKZD/wEGA7lkeou3aAk7uKkFgPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/onveQ/dJMcahRPKZD/wEGA7lkeou3aAk7uKkFgPk/img.png&quot; data-alt=&quot;diagram_02_clock_stretch&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/onveQ/dJMcahRPKZD/wEGA7lkeou3aAk7uKkFgPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FonveQ%2FdJMcahRPKZD%2FwEGA7lkeou3aAk7uKkFgPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;880&quot; data-filename=&quot;diagram_31_clock_stretch.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;880&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_02_clock_stretch&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clock Stretch(클럭 스트레칭)는 Slave나 Master가 &lt;b&gt;바쁠 때 클럭을 Low로 유지하여 통신을 일시 정지&lt;/b&gt;시키는 기능이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Slave가 스트레칭&lt;/b&gt;: 온도 센서처럼 데이터를 준비하는 데 시간이 필요한 경우, Slave가 클럭을 Low로 잡아두어 &quot;잠깐 기다려&quot;라고 알림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Master가 스트레칭&lt;/b&gt;: Master의 버퍼에 아직 처리하지 못한 데이터가 있는데 Slave가 새 데이터를 보내려 할 때, Master가 클럭을 멈춰 버퍼 덮어쓰기를 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32의 &quot;Clock No Stretch Mode: Disabled&quot;는 &lt;b&gt;Stretch Mode를 사용하겠다&lt;/b&gt;(= 필요 시 클럭을 멈출 수 있다)는 의미다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OLED 모듈 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서 사용하는 OLED 모듈의 사양:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;크기&lt;/td&gt;
&lt;td&gt;0.96인치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;해상도&lt;/td&gt;
&lt;td&gt;128 &amp;times; 64 픽셀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;드라이버 칩&lt;/td&gt;
&lt;td&gt;SSD1306&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;통신&lt;/td&gt;
&lt;td&gt;I2C (SCL, SDA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동작 전압&lt;/td&gt;
&lt;td&gt;3.3V ~ 5V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동작 전류&lt;/td&gt;
&lt;td&gt;최대 20mA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시야각&lt;/td&gt;
&lt;td&gt;160도&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OLED는 FND와 달리 &lt;b&gt;자체 메모리&lt;/b&gt;가 있어, 한 번 데이터를 보내면 화면이 계속 유지된다. FND처럼 잔상 효과를 위해 타이머 인터럽트로 계속 갱신할 필요가 없으므로, 이전 글들(&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-30-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%9D%89%EB%82%B4%EB%82%B4%EA%B8%B0-%EB%AA%A8%EB%93%A0-%EC%9E%A5%EC%B9%98-%ED%86%B5%ED%95%A9%EC%8B%9C%ED%82%A4%EA%B8%B0?category=1228736&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;쓰레드 흉내내기&lt;/a&gt;&quot;, &quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-31-%ED%9D%89%EB%82%B4%EB%82%B8-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%81%AC%EB%A6%AC%ED%8B%B0%EC%BB%AC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0?category=1228736&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;흉내낸 쓰레드 크리티컬 섹션 문제 해결&lt;/a&gt;&quot;)에서 겪었던 &lt;b&gt;통신 충돌 문제가 발생하지 않는다&lt;/b&gt;. 온도 표시용으로는 OLED가 훨씬 적합하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라이브러리 포팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 제공하는 SSD1306 라이브러리 파일을 프로젝트에 추가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lib/Inc 폴더:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ssd1306.h&lt;/code&gt; &amp;mdash; OLED 제어 함수 선언&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fonts.h&lt;/code&gt; &amp;mdash; 폰트 데이터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test.h&lt;/code&gt; &amp;mdash; 테스트 함수 선언&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bitmap.h&lt;/code&gt; &amp;mdash; 비트맵 이미지 데이터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;horse_anim.h&lt;/code&gt; &amp;mdash; 말 애니메이션 프레임 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lib/Src 폴더:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ssd1306.c&lt;/code&gt; &amp;mdash; OLED 제어 함수 구현&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fonts.c&lt;/code&gt; &amp;mdash; 폰트 데이터&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test.c&lt;/code&gt; &amp;mdash; 테스트 함수 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;1106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mxBwj/dJMcagMcJwW/t2gX8M3x8IKGRi33tNkBg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mxBwj/dJMcagMcJwW/t2gX8M3x8IKGRi33tNkBg1/img.png&quot; data-alt=&quot;Lib/Inc, Src 폴더 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mxBwj/dJMcagMcJwW/t2gX8M3x8IKGRi33tNkBg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmxBwj%2FdJMcagMcJwW%2Ft2gX8M3x8IKGRi33tNkBg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;554&quot; height=&quot;1106&quot; data-origin-width=&quot;554&quot; data-origin-height=&quot;1106&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Lib/Inc, Src 폴더 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;I2C 핸들러 매크로 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샘플 코드에서는 &lt;code&gt;hi2c1&lt;/code&gt;을 직접 사용하지만, 우리 프로젝트에서는 I2C2를 사용할 수 있다. 유연하게 대응하기 위해 &lt;code&gt;ssd1306.h&lt;/code&gt;에 매크로를 정의한다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;// ssd1306.h
#include &quot;main.h&quot;

extern I2C_HandleTypeDef hi2c2;
#define H_I2C hi2c2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ssd1306.c&lt;/code&gt; 내의 모든 &lt;code&gt;hi2c1&lt;/code&gt;을 &lt;code&gt;H_I2C&lt;/code&gt;로 변경한다. 이렇게 하면 나중에 I2C 포트를 바꿀 때 매크로 한 줄만 수정하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V0i7R/dJMcagZGvKP/k4bY4oBRDKhv97ut3Mb9yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V0i7R/dJMcagZGvKP/k4bY4oBRDKhv97ut3Mb9yk/img.png&quot; data-alt=&quot;ssd1306.h &amp;amp;mdash; H_I2C 매크로 정의&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V0i7R/dJMcagZGvKP/k4bY4oBRDKhv97ut3Mb9yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV0i7R%2FdJMcagZGvKP%2Fk4bY4oBRDKhv97ut3Mb9yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;616&quot; height=&quot;68&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ssd1306.h &amp;mdash; H_I2C 매크로 정의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OLED 메모리 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OLED의 128&amp;times;64 해상도는 내부적으로 &lt;b&gt;8개의 가로 띠(page)&lt;/b&gt;로 나뉜다. 각 page는 가로 128픽셀 &amp;times; 세로 8픽셀의 영역이다. 화면 전체를 위에서 아래로 8개의 가로 띠로 잘라놓은 구조라고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 page 안에서 한 열(column)의 세로 8픽셀을 &lt;b&gt;1바이트&lt;/b&gt;로 제어한다. 예를 들어 &lt;code&gt;0b10110001&lt;/code&gt;을 보내면, 해당 열의 세로 8픽셀이 위에서부터 켜짐-꺼짐-켜짐-켜짐-꺼짐-꺼짐-꺼짐-켜짐으로 표시된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1 page = 가로 128열 &amp;times; 세로 8픽셀 = &lt;b&gt;128바이트&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;화면 전체 = 세로 64픽셀 &amp;divide; 8 = &lt;b&gt;8 page&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;총 데이터 = 128바이트 &amp;times; 8 page = &lt;b&gt;1024바이트&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SSD1306_UpdateScreen&lt;/code&gt;을 호출하면 이 1024바이트가 I2C로 한꺼번에 전송된다. Standard Mode(100kHz)에서 이 데이터 양이 문제를 일으킨 원인이기도 하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하드웨어 연결&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OLED 핀&lt;/th&gt;
&lt;th&gt;연결&lt;/th&gt;
&lt;th&gt;보드 핀&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;빵판 - 레일&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VCC&lt;/td&gt;
&lt;td&gt;빵판 + 레일&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCL&lt;/td&gt;
&lt;td&gt;점퍼선&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PB10&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDA&lt;/td&gt;
&lt;td&gt;점퍼선&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PB11&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1149&quot; data-origin-height=&quot;1148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjLE1o/dJMcaa6hoxs/BBPhJBqjqT34jHCQt4ynO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjLE1o/dJMcaa6hoxs/BBPhJBqjqT34jHCQt4ynO0/img.png&quot; data-alt=&quot;OLED 모듈 빵판&amp;amp;middot;보드 배선&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjLE1o/dJMcaa6hoxs/BBPhJBqjqT34jHCQt4ynO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjLE1o%2FdJMcaa6hoxs%2FBBPhJBqjqT34jHCQt4ynO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1149&quot; height=&quot;1148&quot; data-origin-width=&quot;1149&quot; data-origin-height=&quot;1148&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OLED 모듈 빵판&amp;middot;보드 배선&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실패와 해결 &amp;mdash; Standard &amp;rarr; Fast Mode&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 발생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 포팅과 하드웨어 연결을 마치고, main에서 테스트 코드를 실행했다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// main.c
#include &quot;ssd1306.h&quot;
#include &quot;test.h&quot;

// main 함수 내
SSD1306_Init();
SSD1306_GotoXY(0, 0);
SSD1306_Puts(&quot;Hello World&quot;, &amp;amp;Font_11x18, 1);
SSD1306_UpdateScreen();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과: &lt;b&gt;OLED 화면에 불은 들어왔지만 글자가 출력되지 않았다&lt;/b&gt;. 화면이 켜졌다는 것은 I2C 통신 자체는 이루어지고 있다는 의미다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1147&quot; data-origin-height=&quot;1149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d44TAQ/dJMcafzKEcM/GbZEqizblqqppVaxmpVqR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d44TAQ/dJMcafzKEcM/GbZEqizblqqppVaxmpVqR1/img.png&quot; data-alt=&quot;OLED 불 들어옴 + 글자 미출력&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d44TAQ/dJMcafzKEcM/GbZEqizblqqppVaxmpVqR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd44TAQ%2FdJMcafzKEcM%2FGbZEqizblqqppVaxmpVqR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1147&quot; height=&quot;1149&quot; data-origin-width=&quot;1147&quot; data-origin-height=&quot;1149&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OLED 불 들어옴 + 글자 미출력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 및 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 &lt;b&gt;I2C 전송 속도&lt;/b&gt;였다. OLED는 화면 전체를 갱신할 때 1024바이트를 전송해야 하는데, Standard Mode(100kHz)에서는 데이터 양이 많아 제대로 처리되지 않았다. .ioc에서 I2C Speed Mode를 &lt;b&gt;Fast Mode(400kHz)&lt;/b&gt;로 변경하자 모든 기능이 정상 동작했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;1076&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5DWAo/dJMcadIDUqD/09jjITJGq0OXkddrDKUzw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5DWAo/dJMcadIDUqD/09jjITJGq0OXkddrDKUzw0/img.png&quot; data-alt=&quot;I2C 설정 &amp;amp;mdash; Fast Mode 400kHz&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5DWAo/dJMcadIDUqD/09jjITJGq0OXkddrDKUzw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5DWAo%2FdJMcadIDUqD%2F09jjITJGq0OXkddrDKUzw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1139&quot; height=&quot;1076&quot; data-origin-width=&quot;1139&quot; data-origin-height=&quot;1076&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;I2C 설정 &amp;mdash; Fast Mode 400kHz&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fast Mode로 변경 후 테스트 코드를 실행하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;Hello World&quot;&lt;/b&gt; 문구가 OLED에 출력&lt;/li&gt;
&lt;li&gt;&lt;b&gt;말이 뛰어다니는 애니메이션&lt;/b&gt; 재생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지구본&lt;/b&gt; 등 다양한 비트맵 이미지 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260406_204039.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c3tvGg/dJMcaja1Scm/WoQCdjb784Dolzc9f3BEy0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c3tvGg/dJMcaja1Scm/WoQCdjb784Dolzc9f3BEy0/img.jpg&quot; data-alt=&quot;OLED 동작 &amp;amp;mdash; Hello World&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c3tvGg/dJMcaja1Scm/WoQCdjb784Dolzc9f3BEy0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc3tvGg%2FdJMcaja1Scm%2FWoQCdjb784Dolzc9f3BEy0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260406_204039.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OLED 동작 &amp;mdash; Hello World&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OLED는 한 번 데이터를 보내면 화면이 유지되므로, FND와 달리 while 루프를 점유하지 않는다. 온도 센서에서 값을 읽어 OLED에 표시하는 것도 &lt;code&gt;SSD1306_Puts&lt;/code&gt;와 &lt;code&gt;SSD1306_UpdateScreen&lt;/code&gt;만 호출하면 된다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>I2C</category>
      <category>OJTube임베디드입문</category>
      <category>OLED</category>
      <category>SSD1306</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/69</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-34-%EB%B0%B0%EC%9A%B4-I2C%EB%A1%9C-OLED%EB%A5%BC-%EC%A0%9C%EC%96%B4%ED%95%B4%EB%B3%B4%EC%9E%90#entry69comment</comments>
      <pubDate>Wed, 6 May 2026 20:19:26 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 33. 이제는 쉬워진 I2C 통신</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-33-%EC%9D%B4%EC%A0%9C%EB%8A%94-%EC%89%AC%EC%9B%8C%EC%A7%84-I2C-%ED%86%B5%EC%8B%A0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;통신 방식 분류 복습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 강의 시리즈에서 지금까지 다양한 통신 방식을 다뤘다. 크게 두 가지로 분류할 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;th&gt;이전 글&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시간 기반 (비동기)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;보드 레이트로 시간 단위를 맞춤&lt;/td&gt;
&lt;td&gt;UART, 1-Wire&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-16-printf%EB%8F%84-%EC%89%BD%EC%A7%80-%EC%95%8A%EB%8B%A4%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;printf도 쉽지 않다구&lt;/a&gt;&quot;, &quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;어디서도 안 알려주는 프로토콜의 원리&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;클럭 기반 (동기)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클럭 신호로 데이터를 동기화&lt;/td&gt;
&lt;td&gt;SPI, &lt;b&gt;I2C&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;SPI통신 제대로 배워보자&lt;/a&gt;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I2C는 SPI와 마찬가지로 &lt;b&gt;클럭 기반의 동기 방식&lt;/b&gt;이다. 클럭 신호(SCL)와 데이터 신호(SDA) 두 선을 사용하여 데이터를 주고받는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;I2C 통신의 특징&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;데이터 단위&lt;/td&gt;
&lt;td&gt;&lt;b&gt;8비트&lt;/b&gt;(1바이트)씩 주고받음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주소 공간&lt;/td&gt;
&lt;td&gt;&lt;b&gt;7비트&lt;/b&gt; &amp;rarr; 최대 128개(0~127) 장치 연결 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 속도&lt;/td&gt;
&lt;td&gt;Fast 모드에서 &lt;b&gt;400kHz&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;통신 방향&lt;/td&gt;
&lt;td&gt;&lt;b&gt;반이중&lt;/b&gt; &amp;mdash; 송수신 동시에 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동기화&lt;/td&gt;
&lt;td&gt;&lt;b&gt;클럭 기반&lt;/b&gt; (동기 방식)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;필요 선 수&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2가닥&lt;/b&gt; &amp;mdash; SCL(클럭), SDA(데이터)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기본 상태&lt;/td&gt;
&lt;td&gt;SCL, SDA 모두 &lt;b&gt;High&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기타&lt;/td&gt;
&lt;td&gt;Sleep 모드 장치를 주소 인식으로 깨울 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPI와 I2C 비교 &amp;mdash; 장치 선택 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 장치가 하나의 버스에 연결되어 있을 때, &lt;b&gt;특정 장치를 어떻게 선택하느냐&lt;/b&gt;가 통신 프로토콜의 핵심 차이 중 하나다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;SPI&lt;/th&gt;
&lt;th&gt;I2C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;장치 선택&lt;/td&gt;
&lt;td&gt;&lt;b&gt;CS 선&lt;/b&gt; (하드웨어적)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;주소 호명&lt;/b&gt; (소프트웨어적)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;추가 선&lt;/td&gt;
&lt;td&gt;장치마다 CS 선 1개씩 필요&lt;/td&gt;
&lt;td&gt;추가 선 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 장치&lt;/td&gt;
&lt;td&gt;CS 선 수에 따라 제한&lt;/td&gt;
&lt;td&gt;128개 (7비트 주소)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;통신 방향&lt;/td&gt;
&lt;td&gt;전이중 (MOSI + MISO)&lt;/td&gt;
&lt;td&gt;반이중 (SDA 1개)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;SPI통신 제대로 배워보자&quot;)&lt;/a&gt;에서 SPI는 CS 선으로 물리적으로 장치를 선택한다고 다뤘다. I2C는 &lt;b&gt;별도의 CS 선 없이 주소를 호명하여 특정 장치를 선택&lt;/b&gt;한다. 이 방식은 &lt;b&gt;Master-Slave 관계를 필수적으로 요구&lt;/b&gt;한다. Slave가 먼저 통신을 시작하면 신호 충돌이 발생하므로, 항상 Master가 먼저 주소를 호명하고 Slave가 응답하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;어디서도 안 알려주는 프로토콜의 원리&quot;)&lt;/a&gt;에서 1-Wire도 ROM 코드(주소)로 장치를 선택한다고 다뤘는데, I2C의 주소 방식과 같은 개념이다. 다만 1-Wire는 64비트 주소인 반면, I2C는 7비트로 훨씬 짧다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 송신 시퀀스&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eDavwU/dJMcaiXoAvw/CKQKOpv6iPPkmokcJ3aIN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eDavwU/dJMcaiXoAvw/CKQKOpv6iPPkmokcJ3aIN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eDavwU/dJMcaiXoAvw/CKQKOpv6iPPkmokcJ3aIN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeDavwU%2FdJMcaiXoAvw%2FCKQKOpv6iPPkmokcJ3aIN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;932&quot; height=&quot;445&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;445&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I2C의 데이터 송수신은 SCL(클럭)과 SDA(데이터)의 조합으로 이루어진다. 클럭을 발생하는 주체는 항상 &lt;b&gt;Master&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;START 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCL이 High인 상태에서 &lt;b&gt;SDA가 High &amp;rarr; Low로 떨어지면&lt;/b&gt; 통신 시작을 의미한다. 이것이 START 조건이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 전송 (8비트 반복)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 세팅&lt;/b&gt;: Master가 SCL을 Low로 내린다. 이 구간에서 SDA에 보낼 데이터(0 또는 1)를 세팅한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 읽기&lt;/b&gt;: Master가 SCL을 High로 올린다. 이 구간에서 Slave가 SDA의 값을 읽는다&lt;/li&gt;
&lt;li&gt;이 과정을 &lt;b&gt;8번 반복&lt;/b&gt;하여 1바이트를 전송한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;SPI통신 제대로 배워보자&quot;)&lt;/a&gt;에서 SPI도 클럭의 Rising/Falling Edge에서 데이터를 읽는다고 다뤘는데, I2C도 &lt;b&gt;SCL이 High인 구간에서 SDA를 읽는다&lt;/b&gt;는 점에서 유사하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STOP 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 전송이 끝나면, SDA를 Low로 내린 상태에서 &lt;b&gt;SCL을 High로 유지&lt;/b&gt;하고, 이후 &lt;b&gt;SDA도 High로 올리면&lt;/b&gt; 통신 종료를 의미한다. 둘 다 High로 돌아와 기본 상태로 복귀한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;쓰기 / 읽기 전체 흐름&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;쓰기 (Master &amp;rarr; Slave)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LMyo9/dJMcajaWwjB/AhNIgWQq3QK4ezqVWDnYI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LMyo9/dJMcajaWwjB/AhNIgWQq3QK4ezqVWDnYI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LMyo9/dJMcajaWwjB/AhNIgWQq3QK4ezqVWDnYI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLMyo9%2FdJMcajaWwjB%2FAhNIgWQq3QK4ezqVWDnYI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1495&quot; height=&quot;475&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;START&lt;/b&gt; &amp;mdash; SDA&amp;darr; (SCL High 상태에서)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주소 전송&lt;/b&gt; &amp;mdash; Slave의 7비트 주소를 클럭에 맞춰 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;R/W 비트&lt;/b&gt; &amp;mdash; 1비트를 &lt;b&gt;Low(= Write)&lt;/b&gt;로 보냄. 주소 7비트 + R/W 1비트 = 총 8비트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACK&lt;/b&gt; &amp;mdash; Slave가 SDA를 &lt;b&gt;Low로&lt;/b&gt; 내려 &quot;잘 받았다&quot;고 응답&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 전송&lt;/b&gt; &amp;mdash; 8비트 단위로 데이터를 보내고, 매 8비트 후 Slave가 ACK 응답&lt;/li&gt;
&lt;li&gt;&lt;b&gt;STOP&lt;/b&gt; &amp;mdash; 통신 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;읽기 (Slave &amp;rarr; Master)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;465&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NSqya/dJMcaiXoAxs/J1iXQTD0q8UwXHeKgBaEn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NSqya/dJMcaiXoAxs/J1iXQTD0q8UwXHeKgBaEn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NSqya/dJMcaiXoAxs/J1iXQTD0q8UwXHeKgBaEn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNSqya%2FdJMcaiXoAxs%2FJ1iXQTD0q8UwXHeKgBaEn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1515&quot; height=&quot;465&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;465&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;START&lt;/b&gt; &amp;rarr; 주소 전송 &amp;rarr; R/W 비트를 &lt;b&gt;High(= Read)&lt;/b&gt;로 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACK&lt;/b&gt; &amp;mdash; Slave 응답&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 수신&lt;/b&gt; &amp;mdash; Slave가 클럭에 맞춰 8비트 데이터를 보냄&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACK/NACK&lt;/b&gt; &amp;mdash; Master가 더 읽을 데이터가 있으면 ACK(Low), 마지막이면 NACK(High)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;STOP&lt;/b&gt; &amp;mdash; 통신 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;STM32에서 I2C 설정&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgKlsV/dJMcaaLTaQQ/bHVzL0KyC1iLYVcYYd3Uh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgKlsV/dJMcaaLTaQQ/bHVzL0KyC1iLYVcYYd3Uh1/img.png&quot; data-alt=&quot;I2C1 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgKlsV/dJMcaaLTaQQ/bHVzL0KyC1iLYVcYYd3Uh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgKlsV%2FdJMcaaLTaQQ%2FbHVzL0KyC1iLYVcYYd3Uh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1524&quot; height=&quot;756&quot; data-origin-width=&quot;1524&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;I2C1 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I2C를 사용하는 방법은 두 가지다: GPIO를 직접 제어하여 신호를 만들거나, &lt;b&gt;칩에서 제공하는 I2C 기능을 사용&lt;/b&gt;하는 것. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-22-STM32%EC%97%90%EC%84%9C%EB%8A%94-SPI-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%9C%EB%8B%A4%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;STM32에서는 SPI기능을 제공한다구&quot;)&lt;/a&gt;에서 SPI도 GPIO &amp;rarr; HW SPI로 전환했듯이, I2C도 하드웨어 기능을 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.ioc 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에서 I2C1을 활성화하면 자동으로 &lt;b&gt;PB8(SCL), PB9(SDA)&lt;/b&gt; 핀이 설정된다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정 항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;I2C Speed Mode&lt;/td&gt;
&lt;td&gt;Standard Mode&lt;/td&gt;
&lt;td&gt;&lt;b&gt;100kHz&lt;/b&gt; (Fast Mode는 400kHz)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock Speed&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;td&gt;통신 속도&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주소 충돌 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 I2C 버스에 &lt;b&gt;동일한 주소를 가진 장치가 2개 이상&lt;/b&gt; 연결되면 통신이 불가능하다. 이 문제를 방지하기 위해 대부분의 I2C 장치는 &lt;b&gt;핀 설정으로 주소를 변경할 수 있는 기능&lt;/b&gt;을 제공한다. 특정 핀을 High 또는 Low로 묶어 주소 비트를 바꾸는 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;General Call Address&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;General Call Address(0x00)는 &lt;b&gt;모든 연결된 장치에 동시에 명령을 보낼 때&lt;/b&gt; 사용한다. 예를 들어 전체 장치 리셋 등에 활용된다. 이전 글(&quot;어디서도 안 알려주는 프로토콜의 원리&quot;)에서 다뤘던 1-Wire의 Skip ROM(0xCC)과 같은 개념이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HAL 라이브러리 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되면 HAL 라이브러리 함수로 간단하게 통신할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 데이터 쓰기
HAL_I2C_Master_Transmit(&amp;amp;hi2c1, slave_addr &amp;lt;&amp;lt; 1, data, size, timeout);

// 데이터 읽기
HAL_I2C_Master_Receive(&amp;amp;hi2c1, slave_addr &amp;lt;&amp;lt; 1, data, size, timeout);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;slave_addr &amp;lt;&amp;lt; 1&lt;/code&gt;로 주소를 1비트 왼쪽 시프트하는 이유는, HAL 함수가 내부적으로 &lt;b&gt;7비트 주소 + R/W 비트&lt;/b&gt;를 합쳐서 8비트로 전송하기 때문이다. I2C 통신 개념을 이해하면 이러한 파라미터의 의미가 명확해진다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>Communication</category>
      <category>I2C</category>
      <category>OJTube임베디드입문</category>
      <category>SPI</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/68</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-33-%EC%9D%B4%EC%A0%9C%EB%8A%94-%EC%89%AC%EC%9B%8C%EC%A7%84-I2C-%ED%86%B5%EC%8B%A0#entry68comment</comments>
      <pubDate>Wed, 29 Apr 2026 00:23:38 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 32. 드디어 해결된 크리티컬 문제</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-32-%EB%93%9C%EB%94%94%EC%96%B4-%ED%95%B4%EA%B2%B0%EB%90%9C-%ED%81%AC%EB%A6%AC%ED%8B%B0%EC%BB%AC-%EB%AC%B8%EC%A0%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이전 글에서의 상태&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-31-%ED%9D%89%EB%82%B4%EB%82%B8-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%81%AC%EB%A6%AC%ED%8B%B0%EC%BB%AC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;흉내낸 쓰레드 크리티컬 섹션 문제 해결&quot;)&lt;/a&gt;에서 ManualConvert를 &lt;code&gt;start_converting&lt;/code&gt; / &lt;code&gt;check_converting&lt;/code&gt; / &lt;code&gt;get_temper&lt;/code&gt; 세 함수로 분해하고, &lt;code&gt;is_busy&lt;/code&gt; 플래그로 통신 중 FND 제어를 막는 구조를 만들었다. 온도 정보는 정상적으로 가져올 수 있었지만, 강의에서는 &lt;b&gt;FND에 숫자가 제대로 표현되지 않는 문제&lt;/b&gt;가 남아 있었다. 통신 구간이 여전히 길어서 is_busy 상태가 오래 유지되고, 그동안 FND가 꺼져 있었기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제의 원인 &amp;mdash; 락 범위가 너무 넓다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 &lt;code&gt;start_converting&lt;/code&gt;, &lt;code&gt;get_temper&lt;/code&gt; 등 &lt;b&gt;함수 단위&lt;/b&gt;로 is_busy를 설정했다. 하지만 이 함수들 내부에는 실제로 GPIO 신호를 주고받는 &lt;b&gt;통신 구간&lt;/b&gt;과, 단순히 시간을 기다리는 &lt;b&gt;딜레이 구간&lt;/b&gt;이 섞여 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;통신 구간&lt;/b&gt;: GPIO를 Output/Input으로 전환하며 비트를 보내고 받는 구간. 이 구간에서 TIM3 인터럽트가 끼어들면 타이밍이 깨짐 &amp;rarr; &lt;b&gt;반드시 락 필요&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;딜레이 구간&lt;/b&gt;: &lt;code&gt;delayMicroseconds&lt;/code&gt; 등으로 단순 대기하는 구간. 이 구간에서는 GPIO를 건드리지 않으므로 &amp;rarr; &lt;b&gt;락이 불필요&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 전체에 락을 걸면 딜레이 구간에서도 FND가 멈추므로, FND가 꺼져 있는 시간이 길어지는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 &amp;mdash; write_bit 단위까지 내려가서 락 걸기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-Wire 라이브러리의 &lt;b&gt;가장 하위 단위&lt;/b&gt;인 &lt;code&gt;OneWire_WriteBit&lt;/code&gt; / &lt;code&gt;OneWire_ReadBit&lt;/code&gt;까지 내려간다. 이 함수들이 실제로 GPIO 신호를 생성하는 최소 단위다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;락 범위 세분화&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// onewire.c &amp;mdash; WriteBit 내부 (개념)
void OneWire_WriteBit(OneWire_t* OneWireStruct, uint8_t bit) {
    m_is_busy = 1;          // 락 시작
    // GPIO Output &amp;rarr; Low &amp;rarr; 딜레이 &amp;rarr; High (실제 통신)
    m_is_busy = 0;          // 락 해제
    delayMicroseconds(60);  // 슬롯 완료 대기 (여기서는 FND 제어 가능)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 통신하는 짧은 구간에서만 is_busy = 1&lt;/b&gt;, 딜레이 구간에서는 is_busy = 0으로 풀어준다. 이렇게 하면 딜레이 구간에 TIM3 인터럽트가 발생해도 FND 제어가 가능하고, 실제 GPIO 신호 구간에서는 안전하게 보호된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;write_read_bit.png&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;615&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DkU3D/dJMb99MVXi7/ydO2hbuzFPSkfoq2ROOo0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DkU3D/dJMb99MVXi7/ydO2hbuzFPSkfoq2ROOo0K/img.png&quot; data-alt=&quot;WriteBit/ReadBit에 is_busy 적용한 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DkU3D/dJMb99MVXi7/ydO2hbuzFPSkfoq2ROOo0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDkU3D%2FdJMb99MVXi7%2FydO2hbuzFPSkfoq2ROOo0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1215&quot; height=&quot;615&quot; data-filename=&quot;write_read_bit.png&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;615&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;WriteBit/ReadBit에 is_busy 적용한 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용 후 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 적용하면 &lt;b&gt;FND에 온도가 실시간으로 정확하게 표시&lt;/b&gt;되고, 깜빡임 없이 안정적으로 동작한다. 프로브를 손으로 잡으면 온도가 올라가고, 놓으면 내려가는 것이 FND에 즉시 반영된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (2).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ueweE/dJMcajaWv4t/dxNS8DtFF2hqzjxJz6bNMk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ueweE/dJMcajaWv4t/dxNS8DtFF2hqzjxJz6bNMk/img.gif&quot; data-alt=&quot;깜빡임 없이 FND에 온도 실시간 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ueweE/dJMcajaWv4t/dxNS8DtFF2hqzjxJz6bNMk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ueweE/dJMcajaWv4t/dxNS8DtFF2hqzjxJz6bNMk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (2).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;깜빡임 없이 FND에 온도 실시간 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;data valid 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락 범위를 세분화한 후, &lt;b&gt;data valid 값이 True/False를 왔다갔다하는 문제&lt;/b&gt;가 발생했다. 온도값 자체는 정상이지만 data valid가 불안정하면 신뢰성이 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 &lt;b&gt;아주 짧은 딜레이 구간&lt;/b&gt;에서도 락을 풀었기 때문이다. 1~3&amp;mu;s 정도의 매우 짧은 딜레이 구간에서 TIM3가 끼어들면, 다음 통신 동작의 시작 타이밍이 미세하게 밀려 CRC 검사에서 간헐적으로 실패하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결 &amp;mdash; 짧은 딜레이는 통신 구간과 묶기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 짧은 딜레이(수 &amp;mu;s 이하)는 굳이 락을 풀지 않고 &lt;b&gt;통신 구간과 함께 묶어서 is_busy를 유지&lt;/b&gt;한다. 충분히 긴 딜레이(수십 &amp;mu;s 이상)에서만 락을 풀어 FND 제어를 허용한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 개념 예시
m_is_busy = 1;
GPIO_Low();           // 실제 통신
delayMicroseconds(3); // 짧은 딜레이 &amp;rarr; 묶어서 락 유지
GPIO_Input();         // 실제 통신
m_is_busy = 0;        // 여기서 락 해제
delayMicroseconds(53); // 긴 딜레이 &amp;rarr; FND 제어 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 수정을 적용하면 &lt;b&gt;data valid가 항상 True&lt;/b&gt;로 유지되고, 전체적인 데이터 신뢰성이 높아진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 해결에서 얻는 교훈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제 해결 과정에서 가장 중요한 것은 &lt;b&gt;사용하는 재료(하드웨어와 라이브러리)에 대한 깊이 있는 이해&lt;/b&gt;다. 1-Wire 라이브러리의 가장 하위 함수(write_bit)까지 내려가서 &quot;어디가 실제 통신이고 어디가 딜레이인지&quot;를 파악했기 때문에, 락 범위를 정확하게 설정할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베디드 개발에서는 이처럼 &lt;b&gt;답이 없는 상황에서 스스로 해결책을 찾아내는 능력&lt;/b&gt;이 핵심이다. 재료에 대한 온전한 이해가 있을 때 비로소 복잡한 문제도 풀어낼 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크리티컬 섹션 문제의 해결 과정을 정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;쓰레드 흉내내기&lt;/b&gt; &amp;mdash; FND를 타이머 인터럽트로 갱신하는 구조 구현, 통신 충돌 발견&lt;/li&gt;
&lt;li&gt;&lt;b&gt;흉내낸 쓰레드 크리티컬 섹션 문제 해결&lt;/b&gt; &amp;mdash; is_busy 플래그 도입, ManualConvert를 3개 함수로 분해 (함수 단위 락)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이번 글&lt;/b&gt; &amp;mdash; write_bit 단위까지 내려가서 실제 통신 구간에만 락, 짧은 딜레이는 묶어서 data valid 안정화&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 원리는 &lt;b&gt;&quot;실제 GPIO 통신 구간에서만 최소한으로 락을 걸고, 딜레이 구간에서는 풀어서 다른 작업이 가능하게 한다&quot;&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 버전 라이브러리라면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 이전 버전 라이브러리의 소스(ds18b20.c, onewire.c)를 직접 수정하여 &lt;code&gt;is_busy&lt;/code&gt;를 수동으로 추가하고, &lt;code&gt;write_bit&lt;/code&gt;까지 내려가 락 범위를 조절했다. 하지만 &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-24-1-Wire-%ED%86%B5%EC%8B%A0-%EB%82%98%EB%A6%84-%EC%9C%A0%EB%AA%85%ED%96%88%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;1-Wire 통신&quot;)&lt;/a&gt;에서 다뤘던 &lt;b&gt;현재 버전&lt;/b&gt;(&lt;a href=&quot;https://github.com/nimaltd/ds18b20&quot;&gt;nimaltd/ds18b20&lt;/a&gt;)은 이 문제를 &lt;b&gt;라이브러리 차원에서 이미 해결&lt;/b&gt;하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 버전은 Non-Blocking 방식으로 설계되어, 타이머 콜백으로 1-Wire 통신을 처리한다. 다음 함수들이 내장되어 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_is_busy()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 통신 중인지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_is_cnv_done()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;온도 변환이 완료되었는지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_cnv()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;온도 변환 시작 (Non-Blocking)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ds18b20_req_read()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;온도 데이터 읽기 요청 (Non-Blocking)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 TIM3 인터럽트에서 &lt;code&gt;ds18b20_is_busy()&lt;/code&gt;만 체크하면 &lt;b&gt;라이브러리 소스를 수정하지 않고도&lt;/b&gt; 크리티컬 섹션 문제를 해결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// stm32f1xx_it.c &amp;mdash; 현재 버전 사용 시
void TIM3_IRQHandler(void) {
    HAL_TIM_IRQHandler(&amp;amp;htim3);
    if (!ds18b20_is_busy(&amp;amp;ds18)) {
        digit4_temper(m_display_temp);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의의 이전 버전으로는 &quot;라이브러리를 직접 뜯어서 해결하는 과정&quot;을 배울 수 있고, 현재 버전으로는 &quot;잘 설계된 라이브러리가 이런 문제를 어떻게 구조적으로 방지하는지&quot;를 이해할 수 있다. 두 가지 모두 경험해보면 크리티컬 섹션에 대한 이해가 깊어진다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>INTERRUPT</category>
      <category>OJTube임베디드입문</category>
      <category>OneWire</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/67</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-32-%EB%93%9C%EB%94%94%EC%96%B4-%ED%95%B4%EA%B2%B0%EB%90%9C-%ED%81%AC%EB%A6%AC%ED%8B%B0%EC%BB%AC-%EB%AC%B8%EC%A0%9C#entry67comment</comments>
      <pubDate>Wed, 29 Apr 2026 00:12:51 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 31. 흉내낸 쓰레드 크리티컬 문제 해결</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-31-%ED%9D%89%EB%82%B4%EB%82%B8-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%81%AC%EB%A6%AC%ED%8B%B0%EC%BB%AC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 복습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-30-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%9D%89%EB%82%B4%EB%82%B4%EA%B8%B0-%EB%AA%A8%EB%93%A0-%EC%9E%A5%EC%B9%98-%ED%86%B5%ED%95%A9%EC%8B%9C%ED%82%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;쓰레드 흉내내기&quot;)&lt;/a&gt;에서 TIM3 타이머 인터럽트로 FND를 갱신하고, main 루프에서 온도 센서를 읽는 구조를 만들었다. 강의에서는 이 구조에서 &lt;b&gt;FND에 온도가 0으로만 표시되는 문제&lt;/b&gt;가 발생했다. 원인은 main에서 1-Wire 통신(PA3) 중에 TIM3 인터럽트가 발생하여 SPI 통신(PB13/14/15)으로 FND를 제어하면서 &lt;b&gt;1-Wire의 &amp;mu;s 단위 타이밍이 깨진&lt;/b&gt; 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인 환경에서는 이 문제가 재현되지 않았지만, 강의에서 다루는 해결 과정은 임베디드에서 매우 중요한 개념(크리티컬 섹션)이므로 함께 정리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 하드웨어로 해결할 수 없는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&quot;소프트웨어적으로 복잡하게 하지 말고 하드웨어적으로 해결할 수 없을까?&quot;라는 의문이 들 수 있다. 결론부터 말하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;현재 모듈 구조에서는 불가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;7세그먼트 내부 구조&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;fnd detail.png&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZCq7w/dJMcacXdbC8/KvhYxRPy8JfPJFmbvLFsKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZCq7w/dJMcacXdbC8/KvhYxRPy8JfPJFmbvLFsKK/img.png&quot; data-alt=&quot;7세그먼트 내부구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZCq7w/dJMcacXdbC8/KvhYxRPy8JfPJFmbvLFsKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZCq7w%2FdJMcacXdbC8%2FKvhYxRPy8JfPJFmbvLFsKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;921&quot; height=&quot;296&quot; data-filename=&quot;fnd detail.png&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;296&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;7세그먼트 내부구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7세그먼트의 회로도를 보면, &lt;span&gt;A&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;~&lt;/span&gt;&lt;span&gt;G 세그먼트가 &lt;/span&gt;&lt;b&gt;&lt;span&gt;4자리 모두에 공통으로 연결&lt;/span&gt;&lt;/b&gt;&lt;span&gt;되어 있다. A 세그먼트에 전압을 가하면 4개의 자릿수 모두에서 A가 켜진다. DIG1&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;~&lt;/span&gt;&lt;span&gt;DIG4&lt;/span&gt; 신호로 몇 번째 자릿수를 켤지 선택할 수 있지만, &lt;b&gt;한 번에 하나의 숫자 패턴만 보낼 수 있는 구조&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &quot;8888&quot;처럼 같은 숫자는 동시에 표시 가능하지만, &quot;1234&quot;처럼 다른 숫자를 동시에 표시하는 것은 하드웨어적으로 불가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시프트 레지스터&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;shift register.png&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;899&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buLUdO/dJMcagSRULW/8CZWu7qWB7ajVqjLKRfQp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buLUdO/dJMcagSRULW/8CZWu7qWB7ajVqjLKRfQp1/img.png&quot; data-alt=&quot;시프트 레지스터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buLUdO/dJMcagSRULW/8CZWu7qWB7ajVqjLKRfQp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuLUdO%2FdJMcagSRULW%2F8CZWu7qWB7ajVqjLKRfQp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;796&quot; height=&quot;899&quot; data-filename=&quot;shift register.png&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;899&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시프트 레지스터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7세그먼트 모듈에 포함된 시프트 레지스터는 &lt;b&gt;프로그래밍이 가능한 MCU 칩이 아니다&lt;/b&gt;. 직렬 신호를 병렬 신호로 바꿔주는 단순한 회로일 뿐이다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-21-FND-%EC%A0%9C%EC%96%B4-%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;FND 제어 소스 분석&quot;)&lt;/a&gt;에서 다뤘던 &lt;code&gt;send&lt;/code&gt; 함수가 8비트를 직렬로 보내면, 시프트 레지스터가 이를 병렬로 변환하여 7세그먼트에 출력하고, &lt;code&gt;send_port&lt;/code&gt;에서 래치 클럭(RCLK)을 올리면 숫자가 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 &lt;b&gt;소프트웨어적으로 빠르게 전환하여 잔상 효과를 만드는 것이 유일한 방법&lt;/b&gt;이고, 이를 위해 타이머 인터럽트를 사용하되 통신 충돌을 해결해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 전략은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;온도 정보를 가져오는 통신 구간을 &lt;b&gt;최우선 순위&lt;/b&gt;로 설정&lt;/li&gt;
&lt;li&gt;온도 통신 중에는 &lt;b&gt;FND 제어를 하지 않는다&lt;/b&gt; (&lt;code&gt;is_busy&lt;/code&gt; 플래그)&lt;/li&gt;
&lt;li&gt;온도 통신 구간을 &lt;b&gt;최대한 분해하여 짧게&lt;/b&gt; 만든다&lt;/li&gt;
&lt;li&gt;분해된 구간 사이사이에 &lt;b&gt;FND 제어가 동작할 수 있도록&lt;/b&gt; 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;code&gt;is_busy&lt;/code&gt; 플래그다. 온도 센서와 통신 중일 때 &lt;code&gt;is_busy = 1&lt;/code&gt;로 설정하고, TIM3 인터럽트에서는 &lt;code&gt;is_busy&lt;/code&gt;를 확인하여 바쁘면 FND 제어를 건너뛴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Init 간소화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ROM 주소 하드코딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;Ds18b20_Init&lt;/code&gt;은 &lt;code&gt;OneWire_First&lt;/code&gt; &amp;rarr; &lt;code&gt;OneWire_Search(0xF0)&lt;/code&gt; &amp;rarr; &lt;code&gt;OneWire_Next&lt;/code&gt;를 호출하여 연결된 장치의 ROM 주소를 탐색했다. 하지만 우리는 온도 센서가 하나뿐이고 항상 같은 주소를 가지므로, &lt;b&gt;탐색 과정을 생략하고 ROM 주소를 직접 입력&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버그 모드에서 &lt;code&gt;Ds18b20_Init&lt;/code&gt; 호출 후 &lt;code&gt;ds18b20[0].Address&lt;/code&gt;의 값을 확인하여 그대로 하드코딩한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// heaterController.c
static uint8_t m_init = 0;

void init_simple(void) {
    OneWire_Init(&amp;amp;OneWire, _DS18B20_GPIO, _DS18B20_PIN);

    // ROM 주소 직접 입력 (디버그에서 확인한 값)
    OneWire.ROM_NO[0] = 0x28;
    OneWire.ROM_NO[1] = 0xFF;
    // ... (실제 센서의 주소값으로 채움)
    OneWire.ROM_NO[7] = 0x00;

    ds18b20[0].Address[0] = OneWire.ROM_NO[0];
    // ... 나머지도 동일하게 복사

    DS18B20_SetResolution(&amp;amp;OneWire, ds18b20[0].Address, DS18B20_Resolution_12bits);
    DS18B20_DisableAlarmTemperature(&amp;amp;OneWire, ds18b20[0].Address);

    m_init = 1;
}

uint8_t is_init(void) {
    return m_init;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1341&quot; data-origin-height=&quot;677&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QcOVp/dJMcacQr6Wb/iLp4kbQWPbgynZ1oktszsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QcOVp/dJMcacQr6Wb/iLp4kbQWPbgynZ1oktszsk/img.png&quot; data-alt=&quot;디버그 워치 &amp;amp;mdash; Address 초기값&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QcOVp/dJMcacQr6Wb/iLp4kbQWPbgynZ1oktszsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQcOVp%2FdJMcacQr6Wb%2FiLp4kbQWPbgynZ1oktszsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1341&quot; height=&quot;677&quot; data-origin-width=&quot;1341&quot; data-origin-height=&quot;677&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디버그 워치 &amp;mdash; Address 초기값&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Init 과정에서 ROM Search 통신이 사라지므로 초기화 시간이 크게 단축된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ManualConvert 분해&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 구조의 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;Ds18b20_ManualConvert&lt;/code&gt;는 &lt;b&gt;온도 변환 시작 &amp;rarr; 완료 대기 &amp;rarr; 데이터 읽기&lt;/b&gt;를 한 번에 수행한다. 이 전체 과정이 수백 ms 이상 걸리므로, 그동안 TIM3가 끼어들면 충돌이 발생한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계로 분해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 ManualConvert를 세 개의 함수로 나눈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① start_converting &amp;mdash; 온도 변환 시작&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;static uint8_t m_is_busy = 0;

void start_converting(void) {
    m_is_busy = 1;  // 바쁨 상태 설정
    DS18B20_StartAll(&amp;amp;OneWire);  // 0xCC + 0x44 전송
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② check_converting &amp;mdash; 변환 완료 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;uint8_t check_converting(void) {
    if (DS18B20_AllDone(&amp;amp;OneWire)) {
        m_is_busy = 0;  // 바쁨 해제
        return 1;  // 완료
    }
    return 0;  // 아직 진행 중
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ get_temper &amp;mdash; 온도 데이터 읽기&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;float get_temper(void) {
    m_is_busy = 1;
    float temp = 0.0f;
    DS18B20_Read(&amp;amp;OneWire, ds18b20[0].Address, &amp;amp;temp);
    m_is_busy = 0;
    return temp;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;is_busy 확인 함수:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;uint8_t is_busy(void) {
    return m_is_busy;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;809&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E9Y0k/dJMcadogBN8/9PQGDdn37ToKZrWiwaU5N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E9Y0k/dJMcadogBN8/9PQGDdn37ToKZrWiwaU5N0/img.png&quot; data-alt=&quot;분해된 함수들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E9Y0k/dJMcadogBN8/9PQGDdn37ToKZrWiwaU5N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE9Y0k%2FdJMcadogBN8%2F9PQGDdn37ToKZrWiwaU5N0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1143&quot; height=&quot;809&quot; data-origin-width=&quot;1143&quot; data-origin-height=&quot;809&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;분해된 함수들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TIM3에서 is_busy 체크&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// stm32f1xx_it.c
void TIM3_IRQHandler(void) {
    HAL_TIM_IRQHandler(&amp;amp;htim3);
    if (!is_busy()) {
        digit4_temper(m_display_temp);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;is_busy()&lt;/code&gt;가 1이면 FND 제어를 건너뛴다. 온도 센서 통신이 끝나면 &lt;code&gt;is_busy&lt;/code&gt;가 0이 되어 다시 FND가 갱신된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;main 루프&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c
init_simple();

float temper = 0.0f;

while (1) {
    start_converting();

    while (!check_converting()) {
        HAL_Delay(10);
    }

    temper = get_temper();
    m_display_temp = (int)(temper * 10);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 상태 및 다음 글 예고&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UHzZ7/dJMcafNdHpR/htuiFcx7k7cJHKHLAcPw5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UHzZ7/dJMcafNdHpR/htuiFcx7k7cJHKHLAcPw5k/img.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;444&quot; data-is-animation=&quot;false&quot; style=&quot;width: 74.2111%; margin-right: 10px;&quot; data-widthpercent=&quot;75.08&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UHzZ7/dJMcafNdHpR/htuiFcx7k7cJHKHLAcPw5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUHzZ7%2FdJMcafNdHpR%2FhtuiFcx7k7cJHKHLAcPw5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1338&quot; height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btxmPf/dJMcacXdbPn/Lz9Z8rv2IWF1rRcmmyQTN0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btxmPf/dJMcacXdbPn/Lz9Z8rv2IWF1rRcmmyQTN0/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (1).gif&quot; data-widthpercent=&quot;24.92&quot; style=&quot;width: 24.6261%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btxmPf/dJMcacXdbPn/Lz9Z8rv2IWF1rRcmmyQTN0/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtxmPf%2FdJMcacXdbPn%2FLz9Z8rv2IWF1rRcmmyQTN0%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식으로 온도 정보는 정상적으로 가져올 수 있게 되었다. 그러나 강의에서는 &lt;b&gt;FND 표시가 여전히 불완전한 문제&lt;/b&gt;가 남아 있었다. 온도 통신 구간이 아직 충분히 짧게 분해되지 않아, is_busy로 FND가 멈추는 시간이 길었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 1-Wire 통신의 &lt;b&gt;가장 하위 단위인 write_bit까지 내려가서&lt;/b&gt;, 실제 통신하는 짧은 구간에서만 is_busy를 걸고 딜레이 구간에서는 풀어주는 방식으로 최종 해결한다. 또한 data valid 값이 불안정한 문제도 함께 다룬다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>FND</category>
      <category>INTERRUPT</category>
      <category>OJTube임베디드입문</category>
      <category>OneWire</category>
      <category>STM32</category>
      <category>timer</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/66</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-31-%ED%9D%89%EB%82%B4%EB%82%B8-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%81%AC%EB%A6%AC%ED%8B%B0%EC%BB%AC-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0#entry66comment</comments>
      <pubDate>Tue, 28 Apr 2026 23:55:47 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 30. 쓰레드 흉내내기 (모든 장치 통합시키기)</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-30-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%9D%89%EB%82%B4%EB%82%B4%EA%B8%B0-%EB%AA%A8%EB%93%A0-%EC%9E%A5%EC%B9%98-%ED%86%B5%ED%95%A9%EC%8B%9C%ED%82%A4%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 인식 &amp;mdash; FND는 동시에 표시하지 않는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글들에서 FND에 숫자를 표시할 때 &lt;code&gt;digit4_replay&lt;/code&gt; 함수를 사용했다. 이 함수는 4자리 숫자를 &quot;동시에&quot; 보여주는 것처럼 보이지만, 실제로는 &lt;b&gt;한 자릿수씩 순서대로 매우 빠르게 전환하면서 잔상 효과&lt;/b&gt;로 동시에 켜져 있는 것처럼 보이는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;딜레이 테스트로 증명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 증명하기 위해 각 자릿수 출력 사이에 &lt;b&gt;1초의 딜레이&lt;/b&gt;를 넣어본다. 결과적으로 숫자가 하나씩 순차적으로 깜빡이다가 마지막 자릿수만 남는 현상이 발생한다. 4개의 숫자가 동시에 표시되는 것이 아님을 눈으로 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260406_000717-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d58UnD/dJMcac3X5OR/m8MvWbe7x0hPVBfgDxPl6k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d58UnD/dJMcac3X5OR/m8MvWbe7x0hPVBfgDxPl6k/img.gif&quot; data-alt=&quot;딜레이 테스트 &amp;amp;mdash; 자릿수 순차 깜빡임&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d58UnD/dJMcac3X5OR/m8MvWbe7x0hPVBfgDxPl6k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/d58UnD/dJMcac3X5OR/m8MvWbe7x0hPVBfgDxPl6k/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260406_000717-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;딜레이 테스트 &amp;mdash; 자릿수 순차 깜빡임&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이것이 왜 문제인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FND를 잔상 효과로 표시하려면 &lt;b&gt;while 루프에서 FND를 계속 갱신&lt;/b&gt;해야 한다. 그런데 while 루프에서는 온도 센서 읽기, 릴레이 제어 등 다른 작업도 수행해야 한다. FND 갱신이 while 루프를 점유하면 다른 작업과 충돌하거나, 다른 작업 수행 중 FND가 깜빡이는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제가 있다면 쓰레드로 FND 갱신을 별도로 돌리면 되지만, 우리는 &lt;b&gt;OS 없는 베어메탈 환경&lt;/b&gt;이다. 이 문제를 타이머 인터럽트로 해결한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법 &amp;mdash; 타이머 인터럽트로 쓰레드 흉내내기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현재 인터럽트 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 시스템에는 이미 두 개의 인터럽트가 동작 중이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SysTick&lt;/b&gt;: HAL_Delay 등 시스템 타이밍 담당&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TIM2&lt;/b&gt;: 1-Wire 통신의 1&amp;mu;s 타이밍 담당 (&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-24-1-Wire-%ED%86%B5%EC%8B%A0-%EB%82%98%EB%A6%84-%EC%9C%A0%EB%AA%85%ED%96%88%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;1-Wire 통신&quot;)&lt;/a&gt;에서 설정)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 &lt;b&gt;TIM3&lt;/b&gt;를 새로 추가하여, FND 갱신을 인터럽트에서 처리한다. while 루프와 독립적으로 FND가 계속 갱신되므로, 마치 별도의 쓰레드처럼 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Timer3 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에서 TIM3를 활성화하고 다음과 같이 설정한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정 항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clock Source&lt;/td&gt;
&lt;td&gt;Internal Clock&lt;/td&gt;
&lt;td&gt;내부 클럭 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prescaler&lt;/td&gt;
&lt;td&gt;(72 - 1)&lt;/td&gt;
&lt;td&gt;72MHz &amp;rarr; 1MHz (1틱 = 1&amp;mu;s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counter Period&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;100&amp;mu;s마다 인터럽트 발생&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NVIC 우선순위&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;TIM2(1-Wire용)보다 낮은 우선순위&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;tim config.png&quot; data-origin-width=&quot;2077&quot; data-origin-height=&quot;949&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IPbVn/dJMcajoq3Nt/1AXFCjjBoS3PbjSOlSMDRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IPbVn/dJMcajoq3Nt/1AXFCjjBoS3PbjSOlSMDRk/img.png&quot; data-alt=&quot;TIM3 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IPbVn/dJMcajoq3Nt/1AXFCjjBoS3PbjSOlSMDRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIPbVn%2FdJMcajoq3Nt%2F1AXFCjjBoS3PbjSOlSMDRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2077&quot; height=&quot;949&quot; data-filename=&quot;tim config.png&quot; data-origin-width=&quot;2077&quot; data-origin-height=&quot;949&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TIM3 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인터럽트 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main 함수에서 Timer3를 시작한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main 함수 내, while 루프 전
HAL_TIM_Base_Start_IT(&amp;amp;htim3);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 100&amp;mu;s마다 &lt;code&gt;TIM3_IRQHandler&lt;/code&gt;가 호출된다. 이 핸들러 안에서 FND를 갱신하면 while 루프와 독립적으로 FND가 표시된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;digit4_temper 함수 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인터럽트에서 한 자릿수씩 표시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 &lt;code&gt;digit4_show&lt;/code&gt;는 한 번에 4자리를 모두 보냈지만, 인터럽트 방식에서는 &lt;b&gt;호출될 때마다 한 자릿수만 표시&lt;/b&gt;하고, 다음 호출 때 다음 자릿수를 표시한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;// fnd_controller.c
static uint8_t m_temp_count = 0;

void digit4_temper(int temper) {
    int n1 = temper % 10;
    int n2 = (temper / 10) % 10;
    int n3 = (temper / 100) % 10;
    int n4 = (temper / 1000) % 10;

    switch (m_temp_count) {
        case 0:
            send_port(_LED_0F[n1], 0b0001);
            break;
        case 1:
            send_port(_LED_0F[n2] &amp;amp; 0x7F, 0b0010);  // 소수점 포함
            break;
        case 2:
            send_port(_LED_0F[n3], 0b0100);
            break;
        case 3:
            send_port(_LED_0F[n4], 0b1000);
            break;
        default:
            m_temp_count = 0;
            return;
    }
    m_temp_count++;
    if (m_temp_count &amp;gt; 3) m_temp_count = 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100&amp;mu;s마다 호출되므로 4자리를 한 바퀴 도는 데 400&amp;mu;s(0.4ms)가 걸린다. 사람 눈에는 깜빡임 없이 동시에 표시되는 것처럼 보인다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인터럽트 핸들러에서 호출&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// stm32f1xx_it.c
#include &quot;fnd_controller.h&quot;

void TIM3_IRQHandler(void) {
    HAL_TIM_IRQHandler(&amp;amp;htim3);
    digit4_temper(500);  // 테스트: 50.0으로 표시
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;밝기 불균일 문제&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7QOlh/dJMcadogBDN/dlk0UfGVEUaWaIUkxkjAH0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7QOlh/dJMcadogBDN/dlk0UfGVEUaWaIUkxkjAH0/img.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot; data-is-animation=&quot;false&quot; data-filename=&quot;20260406_002017.jpg&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7QOlh/dJMcadogBDN/dlk0UfGVEUaWaIUkxkjAH0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7QOlh%2FdJMcadogBDN%2Fdlk0UfGVEUaWaIUkxkjAH0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rKrom/dJMcabRxB2E/Ik6vkbVUntP6gRZHrLjDo0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rKrom/dJMcabRxB2E/Ik6vkbVUntP6gRZHrLjDo0/img.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot; data-is-animation=&quot;false&quot; data-filename=&quot;20260406_002631.jpg&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rKrom/dJMcabRxB2E/Ik6vkbVUntP6gRZHrLjDo0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrKrom%2FdJMcabRxB2E%2FIk6vkbVUntP6gRZHrLjDo0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;해결 전 / 해결 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 시 &lt;b&gt;마지막 자릿수(1의 자리)가 유독 밝게&lt;/b&gt; 보이는 문제가 발생할 수 있다. 원인은 case 3 이후 default에서 아무 것도 표시하지 않고 돌아가면서, 1의 자리가 상대적으로 오래 켜져 있기 때문이다. m_temp_count가 3을 넘으면 즉시 0으로 리셋하여 &lt;b&gt;모든 자릿수가 균일한 시간 동안 켜지도록&lt;/b&gt; 수정하면 해결된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온도 표시 시도&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 온도 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 값(50.0) 대신 실제 온도를 표시하도록 수정한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c
while (1) {
    float temp = get_current_temp();
    int temp_int = (int)(temp * 10);
    digit4_temper(temp_int);  // 전역 변수를 통해 인터럽트에 전달
    HAL_Delay(1000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행하면 &lt;b&gt;FND에 현재 온도가 정상적으로 표시&lt;/b&gt;된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260406_010956.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk3nZr/dJMcaa6bWaN/h6PFuwk37yuKFHpvTuEsC0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk3nZr/dJMcaa6bWaN/h6PFuwk37yuKFHpvTuEsC0/img.jpg&quot; data-alt=&quot;FND에 현재 온도 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk3nZr/dJMcaa6bWaN/h6PFuwk37yuKFHpvTuEsC0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk3nZr%2FdJMcaa6bWaN%2Fh6PFuwk37yuKFHpvTuEsC0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260406_010956.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND에 현재 온도 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;강의에서 발생한 통신 충돌 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서는 이 단계에서 &lt;b&gt;FND에 온도가 0으로만 표시&lt;/b&gt;되는 문제가 발생했다. 원인은 TIM3 인터럽트와 main의 온도 센서 통신이 동시에 GPIO를 사용하면서 &lt;b&gt;1-Wire 통신의 타이밍이 깨진&lt;/b&gt; 것이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;main 루프&lt;/b&gt;: &lt;code&gt;Ds18b20_ManualConvert&lt;/code&gt; &amp;rarr; 1-Wire 통신으로 GPIO(PA3)를 통해 온도 데이터 수신&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TIM3 인터럽트&lt;/b&gt;: &lt;code&gt;digit4_temper&lt;/code&gt; &amp;rarr; SPI 통신으로 GPIO(PB13/14/15)를 통해 FND 데이터 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-Wire는 &amp;mu;s 단위의 정밀한 타이밍이 필요한데, main에서 1-Wire 통신 중에 TIM3 인터럽트가 발생하면 인터럽트 핸들러에서 SPI GPIO 신호를 보내면서 타이밍이 깨질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인 환경에서는 이 문제가 재현되지 않았지만, 환경에 따라 충분히 발생할 수 있는 문제다. 다음 글에서는 이 문제의 근본적인 해결 방법인 &lt;b&gt;크리티컬 섹션(Critical Section)&lt;/b&gt; &amp;mdash; 온도 센서 통신 중에는 TIM3 인터럽트를 잠시 멈추고, 완료 후 다시 활성화하는 방식 &amp;mdash; 을 다룬다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>FND</category>
      <category>INTERRUPT</category>
      <category>OJTube임베디드입문</category>
      <category>STM32</category>
      <category>timer</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/65</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-30-%EC%93%B0%EB%A0%88%EB%93%9C-%ED%9D%89%EB%82%B4%EB%82%B4%EA%B8%B0-%EB%AA%A8%EB%93%A0-%EC%9E%A5%EC%B9%98-%ED%86%B5%ED%95%A9%EC%8B%9C%ED%82%A4%EA%B8%B0#entry65comment</comments>
      <pubDate>Tue, 28 Apr 2026 23:42:13 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 29. 난방실 만들기</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-29-%EB%82%9C%EB%B0%A9%EC%8B%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;전기 작업 &amp;mdash; 소켓과 릴레이 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-28-%EB%93%9C%EB%9D%BC%EC%9D%B4%EA%B8%B0%EB%A5%BC-%EB%82%B4-%EB%A7%98%EB%8C%80%EB%A1%9C-%EA%BB%90%EB%8B%A4-%EC%BC%B0%EB%8B%A4-%ED%95%B4%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;드라이기를 내 맘대로 껐다, 켰다 해보자&quot;)&lt;/a&gt;에서 트랜지스터를 이용한 릴레이 구동 회로를 구성하고, 수동으로 릴레이 동작을 확인했다. 이번에는 &lt;b&gt;실제로 220V 드라이기를 릴레이에 연결&lt;/b&gt;한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소켓 제작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돼지코(플러그 소켓), 전선, 전기 테이프를 준비한다. 드라이기 사용 시 흐르는 전류량을 고려하여 &lt;b&gt;두껍고 용량이 큰 전선&lt;/b&gt;을 사용해야 한다. 전선 피복을 벗기고 소켓 내부에 나사로 고정한 뒤, 전기 테이프로 노출 부분을 감싼다. 전기 테이프는 &lt;b&gt;당겨서 탄력을 받게끔&lt;/b&gt; 감는 것이 요령이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 연결 단계마다 &lt;b&gt;멀티미터 쇼트 테스트&lt;/b&gt;로 합선 여부를 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260405_190103.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZfssw/dJMcajopj5G/nWdtkaDQSCTWydtd4c4rp0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZfssw/dJMcajopj5G/nWdtkaDQSCTWydtd4c4rp0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZfssw/dJMcajopj5G/nWdtkaDQSCTWydtd4c4rp0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZfssw%2FdJMcajopj5G%2FnWdtkaDQSCTWydtd4c4rp0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260405_190103.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;릴레이에 드라이기 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이기 전원선의 한 가닥을 끊어서, 한쪽을 릴레이의 &lt;b&gt;COM&lt;/b&gt;, 다른 쪽을 &lt;b&gt;NO&lt;/b&gt; 단자에 연결한다. 이전 글에서 다뤘듯이 평상시 COM-NC(OFF) &amp;rarr; 릴레이 동작 시 COM-NO(ON)로 드라이기 전원이 연결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전선이 두꺼워 단자에 잘 들어가지 않으면, 피복을 벗긴 부분을 넓게 펴서 넣은 후 나사를 단단히 조인다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260405_190317.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvIg6a/dJMcaaykr9D/tcBFCu1X815VlzhVZ9gWhK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvIg6a/dJMcaaykr9D/tcBFCu1X815VlzhVZ9gWhK/img.jpg&quot; data-alt=&quot;릴레이 COM/NO에 드라이기 전선 연결&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvIg6a/dJMcaaykr9D/tcBFCu1X815VlzhVZ9gWhK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvIg6a%2FdJMcaaykr9D%2FtcBFCu1X815VlzhVZ9gWhK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260405_190317.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;릴레이 COM/NO에 드라이기 전선 연결&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AC 220V를 다루므로 &lt;b&gt;반드시 전원을 분리한 상태에서 작업&lt;/b&gt;해야 한다. 양쪽 전선을 동시에 잡으면 감전 위험이 있다. 모든 연결이 완료되면 쇼트 테스트로 합선 여부를 최종 확인한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPIO로 릴레이 제어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PB5 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에서 PB5를 &lt;b&gt;GPIO_Output&lt;/b&gt;으로 설정한다. 초기 출력 레벨은 Low(릴레이 미동작)로 설정하고, 유저 라벨은 &lt;code&gt;PB5_RELAY_ON_OFF_CTRL&lt;/code&gt;로 지정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1521&quot; data-origin-height=&quot;849&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bweEUN/dJMcaciBfn9/YAplnZf7VYp83PE4wJvFO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bweEUN/dJMcaciBfn9/YAplnZf7VYp83PE4wJvFO1/img.png&quot; data-alt=&quot;PB5 GPIO Output 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bweEUN/dJMcaciBfn9/YAplnZf7VYp83PE4wJvFO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbweEUN%2FdJMcaciBfn9%2FYAplnZf7VYp83PE4wJvFO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1521&quot; height=&quot;849&quot; data-origin-width=&quot;1521&quot; data-origin-height=&quot;849&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PB5 GPIO Output 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;릴레이 동작 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 간단한 토글 코드로 릴레이가 정상 동작하는지 확인한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;while (1) {
    HAL_GPIO_TogglePin(PB5_RELAY_ON_OFF_CTRL_GPIO_Port, PB5_RELAY_ON_OFF_CTRL_Pin);
    HAL_Delay(2000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행하면 2초마다 '딸깍딸깍' 소리가 나며 릴레이가 on/off된다. AC 전원을 연결하면 &lt;b&gt;드라이기가 2초 간격으로 켜졌다 꺼졌다&lt;/b&gt; 하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260405_195007-ezgif.com-video-to-gif-converter (1).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dclBLW/dJMcah5h4UO/xSwu4zbZM0EryJ70bIrKz1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dclBLW/dJMcah5h4UO/xSwu4zbZM0EryJ70bIrKz1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dclBLW/dJMcah5h4UO/xSwu4zbZM0EryJ70bIrKz1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dclBLW/dJMcah5h4UO/xSwu4zbZM0EryJ70bIrKz1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260405_195007-ezgif.com-video-to-gif-converter (1).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;간이 난방실 제작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도에 따라 드라이기를 제어하려면 &lt;b&gt;밀폐된 공간&lt;/b&gt;이 필요하다. 택배 상자를 활용하여 간이 난방실을 만든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;윗면에 &lt;b&gt;온도 센서 프로브&lt;/b&gt;가 들어갈 구멍&lt;/li&gt;
&lt;li&gt;옆면에 &lt;b&gt;드라이기&lt;/b&gt;가 들어갈 구멍&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구멍을 일부러 작게 뚫어 센서와 드라이기가 강제로 들어가게 하면 열이 새는 것을 줄일 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260405_195543.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDyC5m/dJMcahc8huA/f9efBr7zQwN4nxcK6IIukk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDyC5m/dJMcahc8huA/f9efBr7zQwN4nxcK6IIukk/img.jpg&quot; data-alt=&quot;택배 상자 간이 난방실 &amp;amp;mdash; 드라이기와 온도 센서 배치&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDyC5m/dJMcahc8huA/f9efBr7zQwN4nxcK6IIukk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDyC5m%2FdJMcahc8huA%2Ff9efBr7zQwN4nxcK6IIukk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260405_195543.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;택배 상자 간이 난방실 &amp;mdash; 드라이기와 온도 센서 배치&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;온도 센서 재연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글들에서 사용했던 온도 센서를 회로도에 따라 다시 연결한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모듈 핀&lt;/th&gt;
&lt;th&gt;연결&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VCC&lt;/td&gt;
&lt;td&gt;빵판 + 레일 (3.3V)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;빵판 - 레일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAT&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PA3&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM 보드의 3.3V와 GND도 빵판의 +/- 레일에 연결하여, 모든 부품이 동일한 기준 전압에서 동작하도록 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온도 기반 드라이기 제어 코드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도 제어 로직을 별도 파일로 분리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;heaterController.h:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#ifndef SRC_HEATERCONTROLLER_H_
#define SRC_HEATERCONTROLLER_H_

#include &quot;main.h&quot;
#include &quot;ds18b20.h&quot;

float get_current_temp(void);
void heater_control(void);

#endif&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;heaterController.c:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;#include &quot;heaterController.h&quot;

uint8_t heater_state = 0;  // 0: OFF, 1: ON

float get_current_temp(void) {
    Ds18b20_ManualConvert();
    return ds18b20[0].Temperature;
}

void heater_control(void) {
    float current_temp = get_current_temp();

    if (current_temp &amp;gt; 50.0f &amp;amp;&amp;amp; heater_state == 1) {
        // 50도 초과 + 히터 켜져 있으면 &amp;rarr; OFF
        HAL_GPIO_WritePin(PB5_RELAY_ON_OFF_CTRL_GPIO_Port,
                          PB5_RELAY_ON_OFF_CTRL_Pin, GPIO_PIN_RESET);
        heater_state = 0;
    }
    else if (current_temp &amp;lt; 45.0f &amp;amp;&amp;amp; heater_state == 0) {
        // 45도 미만 + 히터 꺼져 있으면 &amp;rarr; ON
        HAL_GPIO_WritePin(PB5_RELAY_ON_OFF_CTRL_GPIO_Port,
                          PB5_RELAY_ON_OFF_CTRL_Pin, GPIO_PIN_SET);
        heater_state = 1;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;main.c&lt;/h3&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;#include &quot;heaterController.h&quot;

// main 함수 내
Ds18b20_Init();

while (1) {
    heater_control();
    HAL_Delay(1000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제어 로직 정리&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;현재 온도 &amp;gt; 50&amp;deg;C &lt;b&gt;그리고&lt;/b&gt; 히터 ON&lt;/td&gt;
&lt;td&gt;히터 &lt;b&gt;OFF&lt;/b&gt; (GPIO Low)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;현재 온도 &amp;lt; 45&amp;deg;C &lt;b&gt;그리고&lt;/b&gt; 히터 OFF&lt;/td&gt;
&lt;td&gt;히터 &lt;b&gt;ON&lt;/b&gt; (GPIO High)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그 외&lt;/td&gt;
&lt;td&gt;현재 상태 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;45&amp;deg;C 미만이면 드라이기가 켜져서 상자 내부에 열을 가하고, 50&amp;deg;C를 넘으면 꺼진다. 이 on/off를 반복하여 &lt;b&gt;45~50&amp;deg;C 사이를 유지&lt;/b&gt;한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 확인 및 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행하면 드라이기가 켜져서 온도가 올라가고, 50&amp;deg;C를 넘으면 자동으로 꺼진다. 온도가 45&amp;deg;C 아래로 떨어지면 다시 켜지는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;heater run.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lVzJL/dJMcacpmTgZ/3kZHnnyQ7pWSzcKunuj0B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lVzJL/dJMcacpmTgZ/3kZHnnyQ7pWSzcKunuj0B0/img.png&quot; data-alt=&quot;동작 확인 &amp;amp;mdash; FND 온도 표시 + 드라이기 자동 제어&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lVzJL/dJMcacpmTgZ/3kZHnnyQ7pWSzcKunuj0B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlVzJL%2FdJMcacpmTgZ%2F3kZHnnyQ7pWSzcKunuj0B0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1672&quot; height=&quot;218&quot; data-filename=&quot;heater run.png&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;동작 확인 &amp;mdash; FND 온도 표시 + 드라이기 자동 제어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작에는 성공했지만, &lt;b&gt;정교한 온도 제어는 어려운 과제&lt;/b&gt;다. 예를 들어 50&amp;deg;C에서 드라이기를 끄더라도 잔열로 온도가 더 올라갈 수 있다(오버슈트). 목표 온도에 정확히 맞추려면 미리 꺼야 하는 등 세밀한 조정이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의로 고추 건조기의 &lt;b&gt;핵심 기능인 온도 제어 드라이기 구동&lt;/b&gt;이 완성되었다. 다음 글에서는 여러 기능(온도 센서, FND, 릴레이)이 동시에 동작하도록 &lt;b&gt;타이머 인터럽트를 활용한 쓰레드 흉내내기&lt;/b&gt;를 다룬다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>GPIO</category>
      <category>OJTube임베디드입문</category>
      <category>Relay</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/64</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-29-%EB%82%9C%EB%B0%A9%EC%8B%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry64comment</comments>
      <pubDate>Sun, 26 Apr 2026 20:58:55 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 28. 드라이기를 내 맘대로 껐다, 켰다 해보자.</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-28-%EB%93%9C%EB%9D%BC%EC%9D%B4%EA%B8%B0%EB%A5%BC-%EB%82%B4-%EB%A7%98%EB%8C%80%EB%A1%9C-%EA%BB%90%EB%8B%A4-%EC%BC%B0%EB%8B%A4-%ED%95%B4%EB%B3%B4%EC%9E%90</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;고추 건조기의 큰 그림&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고추 건조기의 최종 목표는 &lt;b&gt;특정 온도를 유지하면서 건조&lt;/b&gt;하는 것이다. 이를 위해 세 가지 요소가 필요하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;FND&lt;/b&gt; &amp;mdash; 현재 온도를 표시 (이전 글들에서 구현 완료)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;온도 센서&lt;/b&gt; &amp;mdash; 현재 온도를 측정 (이전 글들에서 구현 완료)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;릴레이&lt;/b&gt; &amp;mdash; 드라이기를 껐다 켰다 제어 (이번 글에서 구현)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 원리는 간단하다. 목표 온도가 60&amp;deg;C라면, 온도가 60&amp;deg;C에 도달할 때까지 드라이기를 켜고, 도달하면 끈다. 온도가 55&amp;deg;C로 떨어지면 다시 켜는 방식으로 &lt;b&gt;on/off를 반복하여 목표 온도 부근을 유지&lt;/b&gt;한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;릴레이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴레이(Relay)는 &lt;b&gt;GPIO 신호에 따라 전류의 흐름을 제어하는 스위치&lt;/b&gt;다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-10-%ED%9A%8C%EB%A1%9C%EB%8F%84-%EB%94%B1-%ED%95%84%EC%9A%94%ED%95%9C-%EB%A7%8C%ED%81%BC%EB%A7%8C-%EB%B0%B0%EC%9A%B0%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;회로도 딱 필요한 만큼만 배우자&quot;)&lt;/a&gt;에서 릴레이의 회로도 기호와 &quot;GPIO로 AC 전원을 차단하는 개념&quot;을 다뤘는데, 이번에 실물로 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전압을 가하면 내부 접점이 붙고(딸깍), 전압을 제거하면 떨어진다. 트랜지스터와 유사한 역할이지만, 릴레이는 물리적으로 접점이 움직이므로 작동 시 '딸깍' 소리가 난다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260405_170757.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R7PS6/dJMcacpmSSj/J5j0TXAApzksMTJdryk4kK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R7PS6/dJMcacpmSSj/J5j0TXAApzksMTJdryk4kK/img.jpg&quot; data-alt=&quot;릴레이 모듈 실물&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R7PS6/dJMcacpmSSj/J5j0TXAApzksMTJdryk4kK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR7PS6%2FdJMcacpmSSj%2FJ5j0TXAApzksMTJdryk4kK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260405_170757.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;릴레이 모듈 실물&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Low Level Trigger 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 릴레이 모듈의 딥 스위치를 &lt;b&gt;'L'(Low Level Trigger)&lt;/b&gt;로 설정한다. 이는 IN 단자가 평상시 High(5V) 상태에 있다가, &lt;b&gt;Low로 떨어지면 릴레이가 동작&lt;/b&gt;하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴레이 모듈의 단자 구성:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단자&lt;/th&gt;
&lt;th&gt;연결&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DC+&lt;/td&gt;
&lt;td&gt;5V&lt;/td&gt;
&lt;td&gt;릴레이 동작 전원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DC-&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;접지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IN1&lt;/td&gt;
&lt;td&gt;제어 신호&lt;/td&gt;
&lt;td&gt;평상시 High, Low가 되면 릴레이 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.3V 보드에서 5V 릴레이 구동&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이 릴레이가 &lt;b&gt;5V에서 동작&lt;/b&gt;하는데, 우리 보드는 &lt;b&gt;3.3V&lt;/b&gt;로 동작한다는 점이다. 원래 3.3V용 릴레이를 사용하려 했으나 수급 문제로 5V용을 사용하게 되었다. 따라서 &lt;b&gt;트랜지스터를 이용한 추가 회로&lt;/b&gt;가 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 원리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kNPbV/dJMcaiXmPQn/2d9XQVCu3z6n2rYglkoXKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kNPbV/dJMcaiXmPQn/2d9XQVCu3z6n2rYglkoXKK/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;422&quot; data-filename=&quot;OFF.png&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kNPbV/dJMcaiXmPQn/2d9XQVCu3z6n2rYglkoXKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkNPbV%2FdJMcaiXmPQn%2F2d9XQVCu3z6n2rYglkoXKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cczlVj/dJMb990vsZB/RBdmViwt7N0H2YVe3sl3d1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cczlVj/dJMb990vsZB/RBdmViwt7N0H2YVe3sl3d1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;422&quot; data-filename=&quot;ON.png&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cczlVj/dJMb990vsZB/RBdmViwt7N0H2YVe3sl3d1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcczlVj%2FdJMb990vsZB%2FRBdmViwt7N0H2YVe3sl3d1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; 회로도 &amp;mdash; PB5 Low vs High 비교 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GPIO(PB5)가 Low일 때:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜지스터가 OFF 상태이므로, 릴레이의 IN1은 4.7k&amp;Omega; 풀업 저항을 통해 5V(High)를 유지한다. Low Level Trigger 조건이 아니므로 &lt;b&gt;릴레이 미동작&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GPIO(PB5)가 High일 때:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜지스터의 Base에 3.3V가 인가되어 트랜지스터가 ON. Collector-Emitter가 연결되면서 IN1이 &lt;b&gt;GND로 끌려 내려가 Low&lt;/b&gt;가 된다. Low Level Trigger 조건이 충족되어 &lt;b&gt;릴레이 동작(딸깍)&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저항의 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1k&amp;Omega; 저항&lt;/b&gt; (GPIO &amp;rarr; 트랜지스터 Base): 트랜지스터에 과도한 전류가 흐르지 않도록 제한&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4.7k&amp;Omega; 저항&lt;/b&gt; (5V &amp;rarr; IN1 풀업): IN1을 평상시 High로 유지. 트랜지스터 ON 시 IN1이 Low로 떨어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저항 없이 5V와 GND가 직접 연결되면 &lt;b&gt;쇼트(단락)&lt;/b&gt;가 발생하여 회로가 손상된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;COM / NC / NO &amp;mdash; 드라이기 제어 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴레이의 출력 단자는 세 개다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단자&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;COM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Common&lt;/td&gt;
&lt;td&gt;기준점 (드라이기 전선의 한쪽)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;NC&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Normally Closed&lt;/td&gt;
&lt;td&gt;평상시 COM과 연결됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;NO&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Normally Open&lt;/td&gt;
&lt;td&gt;평상시 열려 있음, 릴레이 동작 시 COM과 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이기의 전원선 한 가닥을 끊어서, 한쪽을 COM에, 다른 쪽을 NO에 연결한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;릴레이 미동작 (평상시)&lt;/b&gt;: COM-NC 연결 &amp;rarr; 드라이기 전원 경로 끊김 &amp;rarr; &lt;b&gt;드라이기 OFF&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;릴레이 동작 (GPIO High)&lt;/b&gt;: COM-NO 연결 &amp;rarr; 드라이기 전원 경로 연결 &amp;rarr; &lt;b&gt;드라이기 ON&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N1hf6/dJMcah5h4IA/Hse4ksE53A9n9Z7X4aWxf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N1hf6/dJMcah5h4IA/Hse4ksE53A9n9Z7X4aWxf1/img.png&quot; data-alt=&quot;COM/NC/NO와 드라이기 연결 회로도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N1hf6/dJMcah5h4IA/Hse4ksE53A9n9Z7X4aWxf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN1hf6%2FdJMcah5h4IA%2FHse4ksE53A9n9Z7X4aWxf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1006&quot; height=&quot;640&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;COM/NC/NO와 드라이기 연결 회로도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;납땜 작업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보드의 5V 출력 핀에 핀 헤더가 없으므로 &lt;b&gt;납땜으로 핀을 붙여야&lt;/b&gt; 한다. 보드 상단의 6구멍이 위에서부터 5V, 3.3V, GND 순서로 배치되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;납땜은 인두기로 납을 녹여 도체들을 연결하는 작업이다. 다이소에서 5천원에 구입할 수 있는 저렴한 인두기로도 충분히 작업 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;납땜 시 주의사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;과도한 납 사용&lt;/b&gt;: 인접한 패턴끼리 연결되어 쇼트 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;냉땜&lt;/b&gt;: 납이 제대로 녹지 않아 접촉 불량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;패턴 손상&lt;/b&gt;: 인두기를 너무 오래 대면 기판의 구리 패턴이 벗겨짐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P2bB4/dJMb99MUd3i/LlzNeYPS3GwKf3veWAKvQ1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P2bB4/dJMb99MUd3i/LlzNeYPS3GwKf3veWAKvQ1/img.gif&quot; data-is-animation=&quot;true&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-filename=&quot;20260321_192052_1-ezgif.com-video-to-gif-converter.gif&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P2bB4/dJMb99MUd3i/LlzNeYPS3GwKf3veWAKvQ1/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP2bB4%2FdJMb99MUd3i%2FLlzNeYPS3GwKf3veWAKvQ1%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5vLmb/dJMcadV4cll/v4Vw9NeVrWIxxUnKyvJM4k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5vLmb/dJMcadV4cll/v4Vw9NeVrWIxxUnKyvJM4k/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot; data-filename=&quot;20260405_171606.jpg&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5vLmb/dJMcadV4cll/v4Vw9NeVrWIxxUnKyvJM4k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5vLmb%2FdJMcadV4cll%2Fv4Vw9NeVrWIxxUnKyvJM4k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; 납땜 작업 &amp;mdash; 5V 핀에 핀 헤더 납땜 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빵판 배선&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회로도에 따라 브레드보드에 부품을 조립한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;부품 목록&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜지스터 (NPN)&lt;/li&gt;
&lt;li&gt;1k&amp;Omega; 저항 (갈-검-빨)&lt;/li&gt;
&lt;li&gt;4.7k&amp;Omega; 저항 (노-보-빨)&lt;/li&gt;
&lt;li&gt;릴레이 모듈 (딥 스위치 L로 설정)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배선 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;노란 암-수 점퍼선으로 &lt;b&gt;STM 보드의 PB5&lt;/b&gt; &amp;rarr; 빵판 j5에 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1k&amp;Omega; 저항&lt;/b&gt;을 i5와 i10에 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜지스터&lt;/b&gt;의 B(Base, 가운데)가 h15에 오도록 꽂기 (평평한 면이 -쪽)&lt;/li&gt;
&lt;li&gt;주황 수-수 점퍼선으로 j10과 j15 연결 (저항 끝 &amp;rarr; 트랜지스터 Base)&lt;/li&gt;
&lt;li&gt;검정 수-수 점퍼선으로 g16(트랜지스터 E) &amp;rarr; - 레일(GND) 연결&lt;/li&gt;
&lt;li&gt;초록 수-수 점퍼선으로 g14(트랜지스터 C) &amp;rarr; g20 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4.7k&amp;Omega; 저항&lt;/b&gt;을 i20과 i25에 연결&lt;/li&gt;
&lt;li&gt;빨간 수-수 점퍼선으로 h25 &amp;rarr; + 레일(5V) 연결&lt;/li&gt;
&lt;li&gt;릴레이 &lt;b&gt;DC+&lt;/b&gt; &amp;rarr; 빵판 j25 (5V 라인)&lt;/li&gt;
&lt;li&gt;릴레이 &lt;b&gt;DC-&lt;/b&gt; &amp;rarr; 빵판 - 레일(GND)&lt;/li&gt;
&lt;li&gt;릴레이 &lt;b&gt;IN1&lt;/b&gt; &amp;rarr; 빵판 j20 (트랜지스터 C와 풀업 저항의 연결점)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260405_174337.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjs6br/dJMb997e5Zj/uvy7TOCpECwQNGUL2h7jq1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjs6br/dJMb997e5Zj/uvy7TOCpECwQNGUL2h7jq1/img.jpg&quot; data-alt=&quot;빵판 배선 완성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjs6br/dJMb997e5Zj/uvy7TOCpECwQNGUL2h7jq1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjs6br%2FdJMb997e5Zj%2Fuvy7TOCpECwQNGUL2h7jq1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260405_174337.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빵판 배선 완성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전원 연결&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검정 암-수 점퍼선으로 빵판 - 레일 &amp;rarr; STM 보드 GND&lt;/li&gt;
&lt;li&gt;빨간 암-수 점퍼선으로 빵판 + 레일 &amp;rarr; STM 보드 5V (납땜한 핀)&lt;/li&gt;
&lt;li&gt;5V 전원 공급을 위해 &lt;b&gt;USB 어댑터로 외부 전원 연결&lt;/b&gt; 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 없이 릴레이 동작 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 GPIO 코드를 작성하기 전에, &lt;b&gt;수동으로 릴레이 동작을 검증&lt;/b&gt;한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;PB5에 연결된 노란 점퍼선을 빼서, 납땜한 &lt;b&gt;3.3V 핀에 꽂는다&lt;/b&gt; &amp;rarr; GPIO High를 흉내냄&lt;/li&gt;
&lt;li&gt;트랜지스터 ON &amp;rarr; IN1 Low &amp;rarr; Low Level Trigger &amp;rarr; &lt;b&gt;릴레이 '딸깍' 소리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;멀티미터 쇼트 테스트로 &lt;b&gt;COM-NO가 연결&lt;/b&gt;되었는지 확인&lt;/li&gt;
&lt;li&gt;점퍼선을 빼면 &amp;rarr; IN1 High 복귀 &amp;rarr; 릴레이 해제 &amp;rarr; &lt;b&gt;COM-NC로 복귀&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260405_174531.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMet4K/dJMcacXborJ/J6vxURaSuW7iS2naS3HnCK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMet4K/dJMcacXborJ/J6vxURaSuW7iS2naS3HnCK/img.jpg&quot; data-alt=&quot;수동 테스트 &amp;amp;mdash; 3.3V로 릴레이 동작 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMet4K/dJMcacXborJ/J6vxURaSuW7iS2naS3HnCK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMet4K%2FdJMcacXborJ%2FJ6vxURaSuW7iS2naS3HnCK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260405_174531.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수동 테스트 &amp;mdash; 3.3V로 릴레이 동작 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것으로 드라이기 제어의 핵심 회로가 검증되었다. 다음 글에서는 &lt;b&gt;GPIO 코드로 릴레이를 제어하고, 온도 센서와 연동하여 목표 온도를 유지하는 난방실을 구현&lt;/b&gt;한다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>GPIO</category>
      <category>OJTube임베디드입문</category>
      <category>Relay</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/63</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-28-%EB%93%9C%EB%9D%BC%EC%9D%B4%EA%B8%B0%EB%A5%BC-%EB%82%B4-%EB%A7%98%EB%8C%80%EB%A1%9C-%EA%BB%90%EB%8B%A4-%EC%BC%B0%EB%8B%A4-%ED%95%B4%EB%B3%B4%EC%9E%90#entry63comment</comments>
      <pubDate>Sun, 26 Apr 2026 20:46:08 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 27. 온도센서 검증하기</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-27-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Init 함수 복습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-26-%EB%8B%A4%EB%A5%B8-%EC%82%AC%EB%9E%8C-%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;다른 사람 소스 분석하는 방법&quot;)&lt;/a&gt;에서 &lt;code&gt;Ds18b20_Init&lt;/code&gt; 함수의 전체 흐름을 분석했다. 간단히 복습하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;OneWire_Init&lt;/code&gt; &amp;mdash; GPIO PA3 초기화, 타이머 시작&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OneWire_First&lt;/code&gt; &amp;rarr; &lt;code&gt;OneWire_Search(0xF0)&lt;/code&gt; &amp;mdash; 장치 검색, ROM_NO[8]에 주소 저장&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OneWire_Next&lt;/code&gt; &amp;mdash; 다음 장치 검색 (우리는 센서 1개이므로 1번만)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DS18B20_SetResolution&lt;/code&gt; &amp;mdash; 12비트 정밀도 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DS18B20_DisableAlarmTemperature&lt;/code&gt; &amp;mdash; 알람 비활성화&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도 센서와 통신하는 과정은 항상 &lt;b&gt;리셋 &amp;rarr; ROM 주소 선택 &amp;rarr; 명령어 전송 &amp;rarr; 결과 읽기&lt;/b&gt;의 정해진 시퀀스를 따른다. 이 시퀀스는 ManualConvert에서도 동일하게 적용된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ManualConvert 분석 &amp;mdash; 온도 변환 시작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Ds18b20_ManualConvert&lt;/code&gt;는 while문에서 반복 호출되며, &lt;b&gt;실제로 온도를 읽어오는 핵심 함수&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;bool	Ds18b20_ManualConvert(void)
{
	Ds18b20Timeout=_DS18B20_CONVERT_TIMEOUT_MS/10;
	DS18B20_StartAll(&amp;amp;OneWire);
	Ds18b20Delay(100);
	while (!DS18B20_AllDone(&amp;amp;OneWire))
	{
		Ds18b20Delay(10);  
		Ds18b20Timeout-=1;
		if(Ds18b20Timeout==0)
			break;
	}	
	if(Ds18b20Timeout&amp;gt;0)
	{
		for (uint8_t i = 0; i &amp;lt; TempSensorCount; i++)
		{
			Ds18b20Delay(100);
			ds18b20[i].DataIsValid = DS18B20_Read(&amp;amp;OneWire, ds18b20[i].Address, &amp;amp;ds18b20[i].Temperature);
		}
	}
	else
	{
		for (uint8_t i = 0; i &amp;lt; TempSensorCount; i++)
			ds18b20[i].DataIsValid = false;
	}
	if(Ds18b20Timeout==0)
		return false;
	else
		return true;
	#endif
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;StartAll &amp;mdash; 모든 장치에 온도 변환 명령&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;DS18B20_StartAll(&amp;amp;OneWire);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 두 가지 명령어를 순서대로 전송한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;OneWire_Reset&lt;/code&gt; &amp;mdash; 리셋 펄스 전송&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OneWire_WriteByte(0xCC)&lt;/code&gt; &amp;mdash; &lt;b&gt;Skip ROM&lt;/b&gt; (ROM 커맨드). &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;어디서도 안 알려주는 프로토콜의 원리&quot;)&lt;/a&gt;에서 다뤘듯이, 모든 Slave 장치에 동시에 명령을 보낼 때 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OneWire_WriteByte(0x44)&lt;/code&gt; &amp;mdash; &lt;b&gt;Convert T&lt;/b&gt; (Function 커맨드). 센서에게 &quot;온도를 측정하라&quot;고 명령&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 ROM 커맨드(모든 장치 공통)와 Function 커맨드(칩마다 다름)를 구분했는데, 여기서 실제로 적용되는 것이다. 0xCC로 모든 장치를 선택한 뒤 0x44로 온도 변환을 시작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변환 완료 대기 &amp;mdash; AllDone&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도 변환이 시작되면, 센서가 완료할 때까지 기다려야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;while (!DS18B20_AllDone(&amp;amp;OneWire)) {
    Ds18b20Delay(10);
    if (Ds18b20Timeout-- &amp;lt;= 0) break;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DS18B20_AllDone&lt;/code&gt;은 내부적으로 1비트를 읽어 변환 상태를 확인한다. 1-Wire는 pull-up 상태이므로, &lt;b&gt;한 장치라도 변환 중이면 신호선을 Low로 당긴다&lt;/b&gt;. 즉 0이 반환되면 아직 진행 중이고, 1이 반환되면 완료된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임아웃은 &lt;code&gt;_DS18B20_CONVERT_TIMEOUT_MS&lt;/code&gt;(= 5000ms)로 설정되어 있다. 12비트 해상도의 변환 시간이 최대 750ms이므로 5초면 충분한 여유가 있다. 타임아웃이 발생하면 실패로 처리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ManualConvert 분석 &amp;mdash; 온도 데이터 읽기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환이 완료되면 &lt;code&gt;DS18B20_Read&lt;/code&gt; 함수로 온도 데이터를 읽어온다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;bool DS18B20_Read(OneWire_t* OneWire, uint8_t *ROM, float *destination) 
{
	uint16_t temperature;
	uint8_t resolution;
	int8_t digit, minus = 0;
	float decimal;
	uint8_t i = 0;
	uint8_t data[9];
	uint8_t crc;
	
	/* Check if device is DS18B20 */
	if (!DS18B20_Is(ROM)) {
		return false;
	}
	
	/* Check if line is released, if it is, then conversion is complete */
	if (!OneWire_ReadBit(OneWire)) 
	{
		/* Conversion is not finished yet */
		return false; 
	}

	/* Reset line */
	OneWire_Reset(OneWire);
	/* Select ROM number */
	OneWire_SelectWithPointer(OneWire, ROM);
	/* Read scratchpad command by onewire protocol */
	OneWire_WriteByte(OneWire, ONEWIRE_CMD_RSCRATCHPAD);
	
	/* Get data */
	for (i = 0; i &amp;lt; 9; i++) 
	{
		/* Read byte by byte */
		data[i] = OneWire_ReadByte(OneWire);
	}
	
	/* Calculate CRC */
	crc = OneWire_CRC8(data, 8);
	
	/* Check if CRC is ok */
	if (crc != data[8])
		/* CRC invalid */
		return 0;

	
	/* First two bytes of scratchpad are temperature values */
	temperature = data[0] | (data[1] &amp;lt;&amp;lt; 8);

	/* Reset line */
	OneWire_Reset(OneWire);
	
	/* Check if temperature is negative */
	if (temperature &amp;amp; 0x8000)
	{
		/* Two's complement, temperature is negative */
		temperature = ~temperature + 1;
		minus = 1;
	}

	
	/* Get sensor resolution */
	resolution = ((data[4] &amp;amp; 0x60) &amp;gt;&amp;gt; 5) + 9;

	
	/* Store temperature integer digits and decimal digits */
	digit = temperature &amp;gt;&amp;gt; 4;
	digit |= ((temperature &amp;gt;&amp;gt; 8) &amp;amp; 0x7) &amp;lt;&amp;lt; 4;
	
	/* Store decimal digits */
	switch (resolution) 
	{
		case 9:
			decimal = (temperature &amp;gt;&amp;gt; 3) &amp;amp; 0x01;
			decimal *= (float)DS18B20_DECIMAL_STEPS_9BIT;
		break;
		case 10:
			decimal = (temperature &amp;gt;&amp;gt; 2) &amp;amp; 0x03;
			decimal *= (float)DS18B20_DECIMAL_STEPS_10BIT;
		 break;
		case 11: 
			decimal = (temperature &amp;gt;&amp;gt; 1) &amp;amp; 0x07;
			decimal *= (float)DS18B20_DECIMAL_STEPS_11BIT;
		break;
		case 12: 
			decimal = temperature &amp;amp; 0x0F;
			decimal *= (float)DS18B20_DECIMAL_STEPS_12BIT;
		 break;
		default: 
			decimal = 0xFF;
			digit = 0;
	}
	
	/* Check for negative part */
	decimal = digit + decimal;
	if (minus) 
		decimal = 0 - decimal;
	
	
	/* Set to pointer */
	*destination = decimal;
	
	/* Return 1, temperature valid */
	return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Family Code 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 발견된 장치의 주소 첫 바이트가 &lt;b&gt;0x28(DS18B20의 family code)&lt;/b&gt;인지 확인한다. 0x28이 아니면 올바른 센서가 아니므로 false를 반환한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Read Scratchpad&lt;/h3&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;OneWire_Reset(OneWire);
OneWire_SelectWithPointer(OneWire, ROM);       // 특정 주소의 센서 선택
OneWire_WriteByte(OneWire, 0xBE);              // Read Scratchpad 명령&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;어디서도 안 알려주는 프로토콜의 원리&quot;)&lt;/a&gt;에서 다룬 &lt;b&gt;Read Scratchpad(0xBE)&lt;/b&gt; 명령이 여기서 사용된다. 이 명령을 보내면 센서의 Scratchpad에서 9바이트의 데이터를 읽어올 수 있다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;for (i = 0; i &amp;lt; 9; i++) {
    data[i] = OneWire_ReadByte(OneWire);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9바이트를 모두 읽은 후 &lt;b&gt;CRC 검사&lt;/b&gt;를 수행하여 데이터 오류 여부를 확인한다. 오류가 있으면 false를 반환한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ManualConvert 분석 &amp;mdash; 온도 계산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어온 9바이트 중 &lt;b&gt;0번지와 1번지&lt;/b&gt;에 온도 정보가 들어있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;16비트 temperature 값 조합&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;temperature = (uint16_t)(data[1] &amp;lt;&amp;lt; 8 | data[0]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번지를 상위 8비트로, 0번지를 하위 8비트로 합쳐 16비트 temperature 값을 만든다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;음수 검사&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if (temperature &amp;amp; 0x8000) {
    minus = 1;
    temperature = (temperature ^ 0xFFFF) + 1;  // 2의 보수
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSB(최상위 비트)가 1이면 음수다. &lt;code&gt;0x8000&lt;/code&gt;(= &lt;code&gt;1000000000000000&lt;/code&gt;)과 AND 연산하여 MSB만 검사한다. 음수인 경우 &lt;b&gt;2의 보수 연산&lt;/b&gt;으로 양수로 변환하고 minus 플래그를 설정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정수부와 소수부 분리&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;digit = temperature &amp;gt;&amp;gt; 4;                    // 상위 12비트 &amp;rarr; 정수부
digit |= ((temperature &amp;gt;&amp;gt; 8) &amp;amp; 0x7) &amp;lt;&amp;lt; 4;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;16비트 temperature 값에서 &lt;b&gt;상위 12비트가 정수부, 하위 4비트가 소수부&lt;/b&gt;에 해당한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Resolution에 따른 소수점 계산&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;resolution = ((data[4] &amp;amp; 0x60) &amp;gt;&amp;gt; 5) + 9;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scratchpad의 4번지에서 resolution 정보를 읽어온다. 12비트 해상도의 경우:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;case 12:
    decimal = (temperature &amp;amp; 0x0F) * 0.0625;
    break;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 4비트를 추출하여 &lt;b&gt;0.0625를 곱하면&lt;/b&gt; 소수점 값이 된다. 0.0625는 1/16로, 4비트(16단계)로 0~1 사이를 나눈 최소 단위다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 온도값&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;float decimal = digit + decimal_part;
if (minus) decimal = 0 - decimal;
*destination = decimal;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정수부와 소수부를 더하고, 음수였다면 마이너스를 적용하여 &lt;b&gt;최종 온도값(float)&lt;/b&gt;을 destination 포인터에 저장한다. 이 값이 &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-24-1-Wire-%ED%86%B5%EC%8B%A0-%EB%82%98%EB%A6%84-%EC%9C%A0%EB%AA%85%ED%96%88%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;1-Wire 통신&quot;)&lt;/a&gt;에서 &lt;code&gt;ds18b20[0].Temperature&lt;/code&gt;로 읽었던 값이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ManualConvert 전체 흐름 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;DS18B20_StartAll&lt;/code&gt; &amp;mdash; 0xCC(Skip ROM) + 0x44(Convert T)로 온도 변환 시작&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DS18B20_AllDone&lt;/code&gt; &amp;mdash; 1비트 읽기로 변환 완료 대기 (타임아웃 5000ms)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DS18B20_Read&lt;/code&gt; &amp;mdash; family code 0x28 확인&lt;/li&gt;
&lt;li&gt;Read Scratchpad(0xBE) &amp;mdash; 9바이트 읽기 + CRC 검사&lt;/li&gt;
&lt;li&gt;0번지/1번지 &amp;rarr; 16비트 temperature 조합&lt;/li&gt;
&lt;li&gt;0x8000으로 음수 검사, 2의 보수 변환&lt;/li&gt;
&lt;li&gt;상위 12비트(정수) + 하위 4비트 &amp;times; 0.0625(소수) &amp;rarr; float 온도값&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온도 센서 검증&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도 센서의 정확도를 확인하는 방법은 여러 가지가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비교 테스트&lt;/b&gt;: 신뢰할 수 있는 다른 온도 센서와 함께 측정하여 오차 확인. 가장 간단한 방법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전문 기관 검증&lt;/b&gt;: 상용화를 목표로 한다면 온도 센서 검증 기관에서 고정밀 장치로 테스트. 고온/저온을 발생시켜 오차를 측정하고 증서를 발행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기전력 표 이용&lt;/b&gt;: K 타입 센서 등 특정 센서는 기전력 테이블이 있어, 센서를 제거하고 임의의 전류값을 넣어 해당 온도에 맞는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 검증 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 &lt;b&gt;차가운 물과 뜨거운 물에 센서를 넣어 시중 센서와 비교&lt;/b&gt;하는 방식으로 검증했다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;DS18B20&lt;/th&gt;
&lt;th&gt;비교 센서&lt;/th&gt;
&lt;th&gt;오차&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;차가운 물&lt;/td&gt;
&lt;td&gt;8.5&amp;deg;C&lt;/td&gt;
&lt;td&gt;7.6&amp;deg;C&lt;/td&gt;
&lt;td&gt;약 0.9&amp;deg;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;뜨거운 물 (~80&amp;deg;C)&lt;/td&gt;
&lt;td&gt;36.6&amp;deg;C&lt;/td&gt;
&lt;td&gt;35.5&amp;deg;C&lt;/td&gt;
&lt;td&gt;약 1.1&amp;deg;C&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 1&amp;deg;C 내외의 오차는 &lt;b&gt;일반적인 실험용으로 충분히 허용 가능한 범위&lt;/b&gt;다. 고추 건조기 프로젝트에서 설정 온도대로 건조기를 제어하는 용도로는 문제없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도 센서 시리즈를 정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;온도센서를 붙여보자&lt;/b&gt; &amp;mdash; 센서 소개, 1-Wire 통신 특징, read_bit/read 함수 분석, 하드웨어 배선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1-Wire 통신&lt;/b&gt; &amp;mdash; 라이브러리 포팅, 컴파일 에러 해결, 타이머/클럭 설정, 온도 측정 및 FND 표시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;어디서도 안 알려주는 프로토콜의 원리&lt;/b&gt; &amp;mdash; 1-Wire 통신의 장단점 예측, ROM/Function 커맨드, 데이터 송수신 타이밍&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다른 사람 소스 분석하는 방법&lt;/b&gt; &amp;mdash; Ds18b20_Init 흐름, ROM Search 알고리즘&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이번 글&lt;/b&gt; &amp;mdash; Ds18b20_ManualConvert 흐름, 온도 계산 비트 연산, 센서 검증&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 릴레이를 사용하여 &lt;b&gt;드라이기를 제어하는 방법&lt;/b&gt;을 다룬다. 릴레이는 GPIO Output 제어와 회로 구성이 주를 이루어 비교적 간단하다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>OJTube임베디드입문</category>
      <category>OneWire</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/62</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-27-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0#entry62comment</comments>
      <pubDate>Sun, 26 Apr 2026 20:26:52 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 26. 다른 사람 소스 분석하는 방법</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-26-%EB%8B%A4%EB%A5%B8-%EC%82%AC%EB%9E%8C-%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;남의 소스를 분석하는 깊이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 사람의 소스 코드를 분석할 때, C언어 문법이 어려운 게 아니라 &lt;b&gt;코드가 표현하려는 &quot;논리&quot;를 파악하는 것&lt;/b&gt;이 더 어렵다. 라이브러리처럼 블랙박스로 처리된 코드는 내부를 몰라도 사용할 수 있지만, 코드에 대한 깊은 이해는 개발자로서의 성장에 필수적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드를 어디까지 파고들지는 &lt;b&gt;필요에 따라 선택&lt;/b&gt;하면 된다. 특정 센서를 계속 사용해야 한다면 더 깊이 분석해야 하지만, 단순히 온도 값을 불러오는 것이 목적이라면 &lt;code&gt;Ds18b20_Init&lt;/code&gt;과 &lt;code&gt;Ds18b20_ManualConvert&lt;/code&gt; 두 함수만 이해하면 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 이전 글들에서 포팅하여 사용한 DS18B20 라이브러리의 내부를 분석한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ROM Search &amp;mdash; 단일 선에서 여러 장치 주소 찾기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 문제인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;어디서도 안 알려주는 프로토콜의 원리&quot;)&lt;/a&gt;에서 1-Wire는 &lt;b&gt;주소 체계(ROM 코드)&lt;/b&gt;를 사용한다고 다뤘다. 그런데 여러 장치가 하나의 신호선에 연결되어 있을 때, 각 장치가 동시에 자기 주소를 보내면 &lt;b&gt;전기적 신호가 중첩&lt;/b&gt;되어 Master는 올바른 값을 읽을 수 없다. 이 문제를 어떻게 해결할까?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 원리: 0이 1을 덮는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 다뤘듯이 1-Wire에서 0을 보낼 때는 60~120&amp;mu;s 동안 Low를 유지하고, 1을 보낼 때는 1&amp;mu;s만 Low로 내린다. 여러 장치가 동시에 0과 1을 보내면 &lt;b&gt;0의 긴 Low 신호가 1의 짧은 Low 신호를 덮어써서&lt;/b&gt; Master는 0으로 읽게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ROM Search 4단계&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Master가 &lt;b&gt;Search ROM (0xF0)&lt;/b&gt; 커맨드를 전송&lt;/li&gt;
&lt;li&gt;Master가 Slave들로부터 &lt;b&gt;1비트를 읽는다&lt;/b&gt; (&lt;code&gt;id_bit&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Master가 Slave들로부터 &lt;b&gt;그 비트의 보수를 읽는다&lt;/b&gt; (&lt;code&gt;cmp_id_bit&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;두 값의 &lt;b&gt;경우의 수를 판단&lt;/b&gt;하여 0 또는 1을 Write&lt;/li&gt;
&lt;/ol&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id_bit&lt;/th&gt;
&lt;th&gt;cmp_id_bit&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;연결된 장치들의 해당 비트가 &lt;b&gt;0과 1이 섞여 있음&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;연결된 장치들의 해당 비트가 &lt;b&gt;모두 0&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;연결된 장치들의 해당 비트가 &lt;b&gt;모두 1&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;b&gt;연결된 장치가 없음&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시로 따라가기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4개의 장치가 연결되어 있다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;ROM1: 00110101...
ROM2: 10101010...
ROM3: 11110101...
ROM4: 00010001...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1번째 비트 읽기:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROM1~4가 각각 첫 번째 비트 0, 1, 1, 0을 동시에 보낸다. 0이 1을 덮으므로 Master는 &lt;code&gt;id_bit = 0&lt;/code&gt;으로 읽는다. 보수를 취해 1, 0, 0, 1을 보내면 마찬가지로 &lt;code&gt;cmp_id_bit = 0&lt;/code&gt;으로 읽힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 (0, 0) &amp;mdash; 비트가 섞여 있다. Master가 0을 Write하면, 첫 번째 비트가 1인 &lt;b&gt;ROM2, ROM3는 탈락&lt;/b&gt;하고 ROM1, ROM4만 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2번째 비트 읽기:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROM1, ROM4의 두 번째 비트가 모두 0이므로 &lt;code&gt;id_bit = 0&lt;/code&gt;, 보수는 모두 1이므로 &lt;code&gt;cmp_id_bit = 1&lt;/code&gt;. 결과는 (0, 1) &amp;mdash; 모두 0이다. Master는 두 장치의 두 번째 비트가 0임을 알아낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3번째 비트 읽기:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROM1의 세 번째 비트는 1, ROM4는 0. 다시 (0, 0)으로 섞여 있다. Master가 0을 Write하면 ROM1이 탈락하고 &lt;b&gt;ROM4만 남는다&lt;/b&gt;. 이후 64비트까지 계속 읽어서 ROM4의 전체 주소를 확정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 장치 검색:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(0, 0)이었던 분기점으로 돌아가서 이번에는 &lt;b&gt;반대 값(1)을 Write&lt;/b&gt;하여 다른 장치를 찾아간다. 이 과정을 반복하면 모든 장치의 64비트 주소를 순차적으로 확인할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DS18B20 소스 분석: Ds18b20_Init&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드는 크게 두 부분으로 나뉜다: &lt;b&gt;ds18b20.c&lt;/b&gt;(칩 자체 기능)와 &lt;b&gt;onewire.c&lt;/b&gt;(1-Wire 통신 방식). &lt;code&gt;Ds18b20_Init&lt;/code&gt; 함수가 모든 동작의 시작점이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;926&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7UCFw/dJMcahqGByQ/eE70Q9sqkqEZZAKjEYzJ61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7UCFw/dJMcahqGByQ/eE70Q9sqkqEZZAKjEYzJ61/img.png&quot; data-alt=&quot;Ds18b20_Init 함수 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7UCFw/dJMcahqGByQ/eE70Q9sqkqEZZAKjEYzJ61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7UCFw%2FdJMcahqGByQ%2FeE70Q9sqkqEZZAKjEYzJ61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1526&quot; height=&quot;926&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;926&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Ds18b20_Init 함수 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: OneWire_Init &amp;mdash; GPIO 초기화&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;OneWire_Init(&amp;amp;OneWire, _DS18B20_GPIO, _DS18B20_PIN);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-24-1-Wire-%ED%86%B5%EC%8B%A0-%EB%82%98%EB%A6%84-%EC%9C%A0%EB%AA%85%ED%96%88%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;1-Wire 통신&quot;)&lt;/a&gt;에서 &lt;code&gt;ds18b20Config.h&lt;/code&gt;에 설정한 GPIO 포트(PA3)와 핀으로 OneWire 구조체를 초기화한다. 내부적으로 GPIO 핀을 설정하고 High-Low-High 신호를 한 번 보내는 초기 동작을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OneWire 구조체(&lt;code&gt;OneWire_t&lt;/code&gt;)에는 중요한 멤버들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ROM_NO[8]&lt;/b&gt;: 발견된 장치의 64비트 주소 저장 (uint8_t &amp;times; 8 = 64비트)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LastDiscrepancy&lt;/b&gt;: ROM Search에서 마지막 분기점 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ISVy8/dJMcageeUyW/cCHTYtwdwvpDQZb7EIvjtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ISVy8/dJMcageeUyW/cCHTYtwdwvpDQZb7EIvjtK/img.png&quot; data-alt=&quot;OneWire_t 구조체 정의&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ISVy8/dJMcageeUyW/cCHTYtwdwvpDQZb7EIvjtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FISVy8%2FdJMcageeUyW%2FcCHTYtwdwvpDQZb7EIvjtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1311&quot; height=&quot;238&quot; data-origin-width=&quot;1311&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;OneWire_t 구조체 정의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: OneWire_First &amp;mdash; 첫 번째 장치 검색&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;OneWireDevices = OneWire_First(&amp;amp;OneWire);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 분기점 관련 플래그들을 0으로 초기화한 후, &lt;code&gt;OneWire_Search&lt;/code&gt;를 호출한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계: OneWire_Search &amp;mdash; ROM Search 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;OneWire_Search&lt;/code&gt;가 위에서 설명한 ROM Search 4단계를 실제로 구현한 함수다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;OneWire_Reset&lt;/code&gt;으로 리셋 펄스 전송 (480&amp;mu;s Low)&lt;/li&gt;
&lt;li&gt;Slave의 Presence Pulse 확인 (응답 없으면 0 반환)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OneWire_WriteByte(0xF0)&lt;/code&gt; &amp;mdash; Search ROM 커맨드 전송&lt;/li&gt;
&lt;li&gt;64비트 반복: &lt;code&gt;id_bit&lt;/code&gt;와 &lt;code&gt;cmp_id_bit&lt;/code&gt;를 읽고 경우의 수 판단&lt;/li&gt;
&lt;li&gt;찾은 장치의 주소를 &lt;code&gt;ROM_NO[8]&lt;/code&gt;에 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #fbfcfd; color: #212529; text-align: start;&quot;&gt;&lt;code&gt;uint8_t OneWire_Search(OneWire_t* OneWireStruct, uint8_t command) {
	uint8_t id_bit_number;
	uint8_t last_zero, rom_byte_number, search_result;
	uint8_t id_bit, cmp_id_bit;
	uint8_t rom_byte_mask, search_direction;

	/* Initialize for search */
	id_bit_number = 1;
	last_zero = 0;
	rom_byte_number = 0;
	rom_byte_mask = 1;
	search_result = 0;

	// if the last call was not the last one
	if (!OneWireStruct-&amp;gt;LastDeviceFlag)
	{
		// 1-Wire reset
		if (OneWire_Reset(OneWireStruct)) 
		{
			/* Reset the search */
			OneWireStruct-&amp;gt;LastDiscrepancy = 0;
			OneWireStruct-&amp;gt;LastDeviceFlag = 0;
			OneWireStruct-&amp;gt;LastFamilyDiscrepancy = 0;
			return 0;
		}

		// issue the search command 
		OneWire_WriteByte(OneWireStruct, command);  

		// loop to do the search
		do {
			// read a bit and its complement
			id_bit = OneWire_ReadBit(OneWireStruct);
			cmp_id_bit = OneWire_ReadBit(OneWireStruct);

			// check for no devices on 1-wire
			if ((id_bit == 1) &amp;amp;&amp;amp; (cmp_id_bit == 1)) {
				break;
			} else {
				// all devices coupled have 0 or 1
				if (id_bit != cmp_id_bit) {
					search_direction = id_bit;  // bit write value for search
				} else {
					// if this discrepancy if before the Last Discrepancy
					// on a previous next then pick the same as last time
					if (id_bit_number &amp;lt; OneWireStruct-&amp;gt;LastDiscrepancy) {
						search_direction = ((OneWireStruct-&amp;gt;ROM_NO[rom_byte_number] &amp;amp; rom_byte_mask) &amp;gt; 0);
					} else {
						// if equal to last pick 1, if not then pick 0
						search_direction = (id_bit_number == OneWireStruct-&amp;gt;LastDiscrepancy);
					}
					
					// if 0 was picked then record its position in LastZero
					if (search_direction == 0) {
						last_zero = id_bit_number;

						// check for Last discrepancy in family
						if (last_zero &amp;lt; 9) {
							OneWireStruct-&amp;gt;LastFamilyDiscrepancy = last_zero;
						}
					}
				}

				// set or clear the bit in the ROM byte rom_byte_number
				// with mask rom_byte_mask
				if (search_direction == 1) {
					OneWireStruct-&amp;gt;ROM_NO[rom_byte_number] |= rom_byte_mask;
				} else {
					OneWireStruct-&amp;gt;ROM_NO[rom_byte_number] &amp;amp;= ~rom_byte_mask;
				}
				
				// serial number search direction write bit
				OneWire_WriteBit(OneWireStruct, search_direction);

				// increment the byte counter id_bit_number
				// and shift the mask rom_byte_mask
				id_bit_number++;
				rom_byte_mask &amp;lt;&amp;lt;= 1;

				// if the mask is 0 then go to new SerialNum byte rom_byte_number and reset mask
				if (rom_byte_mask == 0) {
					//docrc8(ROM_NO[rom_byte_number]);  // accumulate the CRC
					rom_byte_number++;
					rom_byte_mask = 1;
				}
			}
		} while (rom_byte_number &amp;lt; 8);  // loop until through all ROM bytes 0-7

		// if the search was successful then
		if (!(id_bit_number &amp;lt; 65)) {
			// search successful so set LastDiscrepancy,LastDeviceFlag,search_result
			OneWireStruct-&amp;gt;LastDiscrepancy = last_zero;

			// check for last device
			if (OneWireStruct-&amp;gt;LastDiscrepancy == 0) {
				OneWireStruct-&amp;gt;LastDeviceFlag = 1;
			}

			search_result = 1;
		}
	}

	// if no device found then reset counters so next 'search' will be like a first
	if (!search_result || !OneWireStruct-&amp;gt;ROM_NO[0]) {
		OneWireStruct-&amp;gt;LastDiscrepancy = 0;
		OneWireStruct-&amp;gt;LastDeviceFlag = 0;
		OneWireStruct-&amp;gt;LastFamilyDiscrepancy = 0;
		search_result = 0;
	}

	return search_result;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;OneWire_Search&lt;/code&gt;가 1을 반환하면 장치를 찾은 것이고, 0을 반환하면 더 이상 장치가 없는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계: 여러 장치 검색 및 주소 저장&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;while (OneWireDevices) {
    TempSensorCount++;
    OneWire_GetFullROM(&amp;amp;OneWire, ds18b20[TempSensorCount - 1].Address);
    OneWireDevices = OneWire_Next(&amp;amp;OneWire);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장치를 찾을 때마다 &lt;code&gt;TempSensorCount&lt;/code&gt;를 증가시키고, &lt;code&gt;OneWire_GetFullROM&lt;/code&gt;으로 주소를 &lt;code&gt;ds18b20&lt;/code&gt; 배열에 저장한다. &lt;code&gt;OneWire_Next&lt;/code&gt;로 다음 장치를 계속 검색하며, 더 이상 장치가 없으면 while문을 빠져나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장치를 하나도 못 찾으면 최대 5번(&lt;code&gt;Ds18b20TryToFind = 5&lt;/code&gt;) 재시도한 후 false를 반환한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계: 정밀도 설정&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DS18B20_SetResolution(&amp;amp;OneWire, ds18b20[i].Address, DS18B20_Resolution_12bits);
DS18B20_DisableAlarmTemperature(&amp;amp;OneWire, ds18b20[i].Address);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾은 장치에 대해 &lt;b&gt;온도 정밀도를 12비트로 설정&lt;/b&gt;하고, 알람 Threshold를 비활성화한다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;어디서도 안 알려주는 프로토콜의 원리&quot;)&lt;/a&gt;에서 다뤘듯이 9비트(93.75ms)~12비트(750ms) 중 선택할 수 있으며, 12비트가 가장 정밀하지만 변환 시간이 가장 길다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Ds18b20_Init 전체 흐름 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;OneWire_Init&lt;/code&gt; &amp;mdash; GPIO PA3 초기화, 초기 신호 전송&lt;/li&gt;
&lt;li&gt;3초 미만이면 100ms 대기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OneWire_First&lt;/code&gt; &amp;rarr; &lt;code&gt;OneWire_Search(0xF0)&lt;/code&gt; &amp;mdash; 첫 번째 장치 검색, ROM_NO[8]에 주소 저장&lt;/li&gt;
&lt;li&gt;while문에서 &lt;code&gt;OneWire_GetFullROM&lt;/code&gt;으로 주소를 ds18b20 배열에 복사&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OneWire_Next&lt;/code&gt; &amp;mdash; 다음 장치 검색, 없으면 while 종료&lt;/li&gt;
&lt;li&gt;장치를 못 찾으면 최대 5번 재시도&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DS18B20_SetResolution&lt;/code&gt; &amp;mdash; 12비트 정밀도 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DS18B20_DisableAlarmTemperature&lt;/code&gt; &amp;mdash; 알람 비활성화&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 이해하면 &lt;code&gt;Ds18b20_Init()&lt;/code&gt; 한 줄이 내부적으로 어떤 일을 하는지 파악한 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;분석의 깊이에 대한 조언&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베디드 프로그래밍에서 가장 중요한 자세는 &lt;b&gt;&quot;알고 보면 쉽다&quot;는 마음가짐&lt;/b&gt;이다. 어려운 문제도 기초로 돌아가 문서와 코드를 분석하고 추리하면 결국 해결할 수 있다. 이번 글에서 ROM Search 알고리즘이 복잡해 보이지만, &quot;0이 1을 덮는다&quot;는 하나의 물리적 원리에서 전체가 설명된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩이 안 되는 이유는 크게 두 가지다: &lt;b&gt;언어 문제&lt;/b&gt;(C언어 문법)와 &lt;b&gt;논리 이해 문제&lt;/b&gt;(프로토콜 원리, 알고리즘). 지금까지의 강의처럼 데이터시트를 읽고 프로토콜 원리를 이해하는 과정은 논리 이해에 해당한다. 부족한 선행 지식이 무엇인지 파악하고 보충하는 것이 학습의 핵심이다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>OJTube임베디드입문</category>
      <category>OneWire</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/61</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-26-%EB%8B%A4%EB%A5%B8-%EC%82%AC%EB%9E%8C-%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95#entry61comment</comments>
      <pubDate>Sun, 26 Apr 2026 20:21:12 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 25. 어디서도 안 알려주는 프로토콜의 원리</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;유선 프로토콜의 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의는 코드 작성 없이 &lt;b&gt;프로토콜의 원리를 이해하는 이론 강의&lt;/b&gt;다. 강사가 다양한 통신 장비 개발 경험을 통해 내린 결론은, &lt;b&gt;세상에 존재하는 모든 유선 프로토콜은 특정 틀에서 벗어나지 않는다&lt;/b&gt;는 것이다. 이 원리를 이해하면 새로운 프로토콜을 접할 때 훨씬 수월하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로토콜의 분류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토콜은 크게 세 가지로 분류할 수 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;물리적 통신 방식만 규정&lt;/td&gt;
&lt;td&gt;SPI, I2C&lt;/td&gt;
&lt;td&gt;전기 신호 수준의 규칙만 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;물리적 + SW 규칙까지 규정&lt;/td&gt;
&lt;td&gt;&lt;b&gt;1-Wire&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;물리적 방식과 커맨드 체계까지 정의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SW 규칙만 정의&lt;/td&gt;
&lt;td&gt;HTTP, Modbus, TCP&lt;/td&gt;
&lt;td&gt;물리적 방식은 하위 레이어에 의존&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SW 규칙만 정의하는 프로토콜은 대부분 &lt;b&gt;레이어 구조&lt;/b&gt;를 가진다. 예를 들어 HTTP는 TCP 위에 올라가므로, TCP의 장단점을 그대로 물려받는다. 마치 클래스 상속처럼, 하위 레이어의 특성이 상위 레이어에 영향을 주는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1-Wire 통신의 장점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-Wire 통신의 최대 장점은 &lt;b&gt;단 두 선만으로 양방향 통신이 가능&lt;/b&gt;하다는 것이다. 더 나아가 두 선으로 &lt;b&gt;전원 공급까지&lt;/b&gt; 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선 가닥수는 비용과 직결된다. 수백 개의 장치가 연결되는 환경에서는 선 하나 줄이는 것이 하드웨어 비용, 선 자체 가격, 인건비 모두에 영향을 미친다. 이는 UTP 선으로 전원과 통신을 동시에 제공하는 PoE(Power over Ethernet) 시스템이 현장에서 크게 선호되는 것과 같은 이유다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 가지 모드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Normal Power Mode (우리가 사용하는 모드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GND(접지) + DQ(신호선) + VDD(전원선) 3선을 사용한다. 5V 전원과 4.7k&amp;Omega; 풀업 저항을 거쳐 신호선이 MCU의 GPIO에 연결된다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-23-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C%EB%A5%BC-%EB%B6%99%EC%97%AC%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;온도센서를 붙여보자&quot;)&lt;/a&gt;에서 구성한 배선이 이 모드다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parasite Power Mode&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GND와 VDD를 묶어버리고, &lt;b&gt;데이터 선 하나로 전원 공급과 신호 송수신을 모두&lt;/b&gt; 수행한다. &quot;기생(Parasite)&quot;이라는 이름처럼 데이터 선에서 전원을 빼내는 구조다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;mode.png&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c91QXL/dJMcabjIzN1/TK2v2iGUq8KQIdwSBNYZ8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c91QXL/dJMcabjIzN1/TK2v2iGUq8KQIdwSBNYZ8K/img.png&quot; data-alt=&quot;Normal Power Mode vs Parasite Power Mode 회로도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c91QXL/dJMcabjIzN1/TK2v2iGUq8KQIdwSBNYZ8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc91QXL%2FdJMcabjIzN1%2FTK2v2iGUq8KQIdwSBNYZ8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;717&quot; height=&quot;488&quot; data-filename=&quot;mode.png&quot; data-origin-width=&quot;717&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Normal Power Mode vs Parasite Power Mode 회로도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구조에서 예측하는 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-Wire의 물리적 구조를 보면, 이전에 배운 통신 방식들과 비교하여 몇 가지 특징을 예측할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장거리 통신은 힘들다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신호선 하나로 전압의 High/Low를 이용하여 통신하는 방식은 &lt;b&gt;노이즈에 취약&lt;/b&gt;하다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-16-printf%EB%8F%84-%EC%89%BD%EC%A7%80-%EC%95%8A%EB%8B%A4%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;printf도 쉽지 않다구&quot;)&lt;/a&gt;에서 다룬 RS232도 같은 이유로 장거리에 불리하다. 선 길이가 길어질수록 전압 강하도 발생한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주소 체계를 사용할 것이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 장치가 하나의 신호선에 연결되어 있으면, 특정 장치와 통신하려면 &lt;b&gt;주소로 구분&lt;/b&gt;해야 한다. 그렇지 않으면 모든 장치가 동시에 응답하여 전기적 충돌이 발생한다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;SPI통신 제대로 배워보자&quot;)&lt;/a&gt;에서 SPI는 CS 선(하드웨어적)으로 해결했고, I2C는 주소(소프트웨어적)로 해결한다고 배웠다. 1-Wire도 &lt;b&gt;ROM 코드라는 주소 체계&lt;/b&gt;를 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빠른 속도는 불가능하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클럭 선이 없으므로 SPI처럼 클럭 기반 동기화가 불가능하다. 시간을 미리 쪼개서 데이터를 보내는 &lt;b&gt;타임베이스 방식&lt;/b&gt;을 사용할 수밖에 없다. 실제로 온도 변환에만 93.75ms~750ms가 소요되어 고속 통신과는 거리가 멀다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하드웨어 구성이 간단하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선이 적으므로 회로 구성이 단순하다. 이것이 1-Wire가 온도 센서 같은 단순한 장치에 많이 사용되는 이유다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;통신 시작 시퀀스&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjp05l/dJMcajaUJrO/ro3gXo4bHWELtkX8YGBxfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjp05l/dJMcajaUJrO/ro3gXo4bHWELtkX8YGBxfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjp05l/dJMcajaUJrO/ro3gXo4bHWELtkX8YGBxfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjp05l%2FdJMcajaUJrO%2Fro3gXo4bHWELtkX8YGBxfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1656&quot; height=&quot;678&quot; data-origin-width=&quot;1656&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-Wire 통신은 &lt;b&gt;Master가 리셋 펄스를 보내는 것&lt;/b&gt;으로 시작된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Master&lt;/b&gt;: 신호선을 &lt;b&gt;480&amp;mu;s 이상 Low&lt;/b&gt; 상태로 유지 (리셋 펄스)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Master&lt;/b&gt;: 신호선을 놓아 High로 복귀&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Slave&lt;/b&gt;: 15~60&amp;mu;s 대기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Slave&lt;/b&gt;: &lt;b&gt;60~240&amp;mu;s 동안 Low&lt;/b&gt; 상태 유지 (Presence Pulse &amp;mdash; &quot;나 살아있어&quot;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Master&lt;/b&gt;: Presence Pulse를 읽고 통신 시작을 인지&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시퀀스는 시간을 기반으로 신호를 구분하는 방식이다. 480&amp;mu;s 미만으로 Low를 보내면 시작 신호로 인식되지 않는다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-24-1-Wire-%ED%86%B5%EC%8B%A0-%EB%82%98%EB%A6%84-%EC%9C%A0%EB%AA%85%ED%96%88%EB%8B%A4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;1-Wire 통신&quot;)&lt;/a&gt;에서 설정한 &lt;b&gt;1&amp;mu;s 단위 타이머&lt;/b&gt;가 이 타이밍 제어에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 라이브러리의 &lt;code&gt;OneWire_Reset&lt;/code&gt; 함수에서 480&amp;mu;s 딜레이 후 인풋을 읽는 코드가 존재하여, &lt;b&gt;데이터시트와 코드가 일치&lt;/b&gt;함을 확인할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ROM 커맨드 &amp;mdash; 장치 선택&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신이 시작되면 Master는 &lt;b&gt;ROM 커맨드&lt;/b&gt;를 보내 통신 대상을 선택한다. 모든 1-Wire 장치는 ROM 코드(주소)를 가지며, 이 커맨드는 &lt;b&gt;장치 종류에 관계없이 모든 1-Wire 장치가 공통으로 지키는 약속&lt;/b&gt;이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;커맨드&lt;/th&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Search ROM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0xF0&lt;/td&gt;
&lt;td&gt;버스에 연결된 모든 Slave의 ROM 코드(주소) 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Match ROM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0x55&lt;/td&gt;
&lt;td&gt;특정 64비트 ROM 코드에 해당하는 Slave만 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Skip ROM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0xCC&lt;/td&gt;
&lt;td&gt;모든 Slave에 동시에 커맨드 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Alarm Search&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0xEC&lt;/td&gt;
&lt;td&gt;알람 플래그가 설정된 Slave 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Search ROM은 연결된 장치가 뭐가 있는지, 주소가 무엇인지 알아내는 명령이다. Match ROM은 특정 장치를 선택한 뒤 명령을 내리는 것이고, 선택되지 않은 장치들은 리셋 펄스가 올 때까지 대기한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;SPI통신 제대로 배워보자&quot;)&lt;/a&gt;에서 SPI의 CS 선이 &quot;누구에게 말하는가?&quot;를 해결했듯이, 1-Wire에서는 ROM 커맨드가 같은 역할을 &lt;b&gt;소프트웨어적으로&lt;/b&gt; 수행하는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Function 커맨드 &amp;mdash; 장치별 동작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ROM 커맨드와 달리 Function 커맨드는 &lt;b&gt;제조사와 칩의 종류마다 다르다&lt;/b&gt;. 온도 센서는 온도 변환 명령이 있고, 도어락은 문 열기 명령이 있을 것이다. DS18B20의 주요 Function 커맨드:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;커맨드&lt;/th&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Convert T&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0x44&lt;/td&gt;
&lt;td&gt;온도 변환 시작. 변환 시간 93.75ms(9비트)~750ms(12비트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Write Scratchpad&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0x4E&lt;/td&gt;
&lt;td&gt;3바이트 데이터를 Scratchpad(임시 저장 공간)에 쓰기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Read Scratchpad&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0xBE&lt;/td&gt;
&lt;td&gt;Scratchpad 데이터 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Copy Scratchpad&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0x48&lt;/td&gt;
&lt;td&gt;Scratchpad 내용을 EEPROM에 영구 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Recall E&amp;sup2;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0xB8&lt;/td&gt;
&lt;td&gt;EEPROM에서 알람 Threshold 값과 설정 데이터를 Scratchpad로 불러오기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scratchpad는 전원이 꺼지면 사라지는 임시 저장 공간이다. 영구 보존이 필요하면 Copy Scratchpad로 EEPROM에 저장해야 한다. &lt;b&gt;Threshold(임계값)&lt;/b&gt;는 특정 동작을 트리거하는 기준값으로, 이 값을 넘으면 알람이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Parasite Power Mode에서는 Convert T 커맨드를 보낸 후 &lt;b&gt;신호선을 High 상태로 유지하여 온도 변환 동안 전원을 공급&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 송수신 &amp;mdash; 시간으로 0과 1을 구분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-Wire의 데이터 송수신은 &lt;b&gt;1 슬롯에 1비트&lt;/b&gt;를 보내는 방식이다. 0과 1의 구분 방법이 독특하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Master가 데이터를 보낼 때:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;0을 보낼 때&lt;/b&gt;: GPIO를 &lt;b&gt;60~120&amp;mu;s 동안 Low&lt;/b&gt; 유지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1을 보낼 때&lt;/b&gt;: GPIO를 &lt;b&gt;1&amp;mu;s 동안만 Low&lt;/b&gt;로 내렸다가 바로 High&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;Low 상태의 시간 길이로 0과 1을 구분&lt;/b&gt;한다. Slave는 15~60&amp;mu;s 사이에 샘플링하여 값을 읽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Slave가 데이터를 보낼 때:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Master가 1&amp;mu;s 이상 신호선을 Low로 내리면 Slave가 전송 가능&lt;/li&gt;
&lt;li&gt;Slave가 0을 표현할 때 15&amp;mu;s 동안 Low 유지&lt;/li&gt;
&lt;li&gt;Master는 &lt;b&gt;15&amp;mu;s 안에&lt;/b&gt; 값을 읽어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-23-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C%EB%A5%BC-%EB%B6%99%EC%97%AC%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;온도센서를 붙여보자&quot;)&lt;/a&gt;에서 분석한 &lt;code&gt;read_bit&lt;/code&gt; 함수가 바로 이 동작을 구현한 것이다. Output으로 Low를 보낸 뒤 Input으로 전환하여 읽는 과정이 데이터시트의 타이밍과 정확히 일치한다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>OJTube임베디드입문</category>
      <category>OneWire</category>
      <category>protocol</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/60</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-25-%EC%96%B4%EB%94%94%EC%84%9C%EB%8F%84-%EC%95%88-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C%EC%9D%98-%EC%9B%90%EB%A6%AC#entry60comment</comments>
      <pubDate>Sun, 26 Apr 2026 20:02:09 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 24. 1-Wire 통신! 나름 유명했다.</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-24-1-Wire-%ED%86%B5%EC%8B%A0-%EB%82%98%EB%A6%84-%EC%9C%A0%EB%AA%85%ED%96%88%EB%8B%A4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;라이브러리 포팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-23-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C%EB%A5%BC-%EB%B6%99%EC%97%AC%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;온도센서를 붙여보자&quot;)&lt;/a&gt;에서 온도 센서의 원리, 1-Wire 통신의 특징, 샘플 코드 분석, 하드웨어 배선까지 완료했다. 이번 글에서는 &lt;b&gt;DS18B20 라이브러리를 STM32 프로젝트에 포팅하여 실제로 온도를 읽고 FND에 표시&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 사용하는 DS18B20 라이브러리(&lt;a href=&quot;https://github.com/nimaltd/ds18b20&quot;&gt;nimaltd/ds18b20&lt;/a&gt;)는 현재 최신 버전과 크게 달라졌다. 이 섹션에서는 &lt;b&gt;강의에서 사용하는 이전 버전&lt;/b&gt;(&lt;a href=&quot;https://github.com/nimaltd/ds18b20/tree/2c3ebbb5aa8f6c8a9f7e780d95af87c487d3e321&quot;&gt;GitHub 커밋 링크&lt;/a&gt;)을 기준으로 진행한다. 현재 버전 적용은 글 후반부에서 별도로 다룬다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;폴더 구조 및 파일 복사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 Core 폴더 안에 Lib 폴더를 생성하고, 그 안에 Inc(헤더)와 Src(소스) 폴더를 만든다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Src 폴더&lt;/b&gt;: &lt;code&gt;ds18b20.c&lt;/code&gt;, &lt;code&gt;onewire.c&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Inc 폴더&lt;/b&gt;: &lt;code&gt;ds18b20.h&lt;/code&gt;, &lt;code&gt;ds18b20Config.h&lt;/code&gt;, &lt;code&gt;onewire.h&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw3LeK/dJMcahRIKMk/9E6tOfXqZUstnRot0yjvK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw3LeK/dJMcahRIKMk/9E6tOfXqZUstnRot0yjvK0/img.png&quot; data-alt=&quot;Core/Lib/Inc, Src 폴더 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw3LeK/dJMcahRIKMk/9E6tOfXqZUstnRot0yjvK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw3LeK%2FdJMcahRIKMk%2F9E6tOfXqZUstnRot0yjvK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;947&quot; height=&quot;756&quot; data-origin-width=&quot;947&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Core/Lib/Inc, Src 폴더 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴파일 에러 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 복사한 후 컴파일하면 여러 에러가 순서대로 발생한다. 하나씩 해결한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) onewire.h, ds18b20.h &amp;mdash; Include 경로 미지정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일러가 헤더 파일을 찾지 못한다. &lt;code&gt;Project &amp;rarr; Properties &amp;rarr; C/C++ Build &amp;rarr; Settings &amp;rarr; Include paths&lt;/code&gt;에 &lt;b&gt;Lib/Inc 경로를 추가&lt;/b&gt;한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;1015&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjlgv5/dJMcadV4a7v/0kukR7Ki8ydM27MdKYOM50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjlgv5/dJMcadV4a7v/0kukR7Ki8ydM27MdKYOM50/img.png&quot; data-alt=&quot;Include paths 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjlgv5/dJMcadV4a7v/0kukR7Ki8ydM27MdKYOM50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjlgv5%2FdJMcadV4a7v%2F0kukR7Ki8ydM27MdKYOM50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1530&quot; height=&quot;1015&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;1015&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Include paths 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) gpio.h &amp;mdash; 존재하지 않는 파일&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpFWIW/dJMcageeTWq/NB8Me8RvkwwAdlynzju5oK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpFWIW/dJMcageeTWq/NB8Me8RvkwwAdlynzju5oK/img.png&quot; data-origin-width=&quot;1441&quot; data-origin-height=&quot;471&quot; data-is-animation=&quot;false&quot; style=&quot;width: 61.5836%; margin-right: 10px;&quot; data-widthpercent=&quot;62.31&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpFWIW/dJMcageeTWq/NB8Me8RvkwwAdlynzju5oK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpFWIW%2FdJMcageeTWq%2FNB8Me8RvkwwAdlynzju5oK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1441&quot; height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6J1Xa/dJMcageeTWG/kuJFNTs3hkow4x6i4KkBK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6J1Xa/dJMcageeTWG/kuJFNTs3hkow4x6i4KkBK1/img.png&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;603&quot; data-is-animation=&quot;false&quot; style=&quot;width: 37.2536%;&quot; data-widthpercent=&quot;37.69&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6J1Xa/dJMcageeTWG/kuJFNTs3hkow4x6i4KkBK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6J1Xa%2FdJMcageeTWG%2FkuJFNTs3hkow4x6i4KkBK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1116&quot; height=&quot;603&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gpio.h&lt;/code&gt;를 include하고 있지만 우리 프로젝트에는 이 파일이 없다. &lt;code&gt;gpio.h&lt;/code&gt;를 주석 처리하면 &lt;code&gt;GPIO_TypeDef&lt;/code&gt; 타입을 찾을 수 없다는 에러가 발생한다. &lt;code&gt;GPIO_TypeDef&lt;/code&gt;는 &lt;code&gt;stm32f103xb.h&lt;/code&gt;에 정의되어 있고, &lt;code&gt;main.h&lt;/code&gt;가 이를 include하고 있으므로 &lt;b&gt;&lt;code&gt;onewire.h&lt;/code&gt;에서 &lt;code&gt;main.h&lt;/code&gt;를 include&lt;/b&gt;하면 해결된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) cmsis_os.h &amp;mdash; FreeRTOS 미사용&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCztTC/dJMcagyvocA/GHkASKAVODBEkzzRg4j2sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCztTC/dJMcagyvocA/GHkASKAVODBEkzzRg4j2sk/img.png&quot; data-origin-width=&quot;1272&quot; data-origin-height=&quot;531&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.2279%; margin-right: 10px;&quot; data-widthpercent=&quot;48.8&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCztTC/dJMcagyvocA/GHkASKAVODBEkzzRg4j2sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCztTC%2FdJMcagyvocA%2FGHkASKAVODBEkzzRg4j2sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1272&quot; height=&quot;531&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lyVYn/dJMcagZzDWx/2lTFFRtES1pHrdK5kbmfU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lyVYn/dJMcagZzDWx/2lTFFRtES1pHrdK5kbmfU0/img.png&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;545&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.6093%;&quot; data-widthpercent=&quot;51.2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lyVYn/dJMcagZzDWx/2lTFFRtES1pHrdK5kbmfU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlyVYn%2FdJMcagZzDWx%2F2lTFFRtES1pHrdK5kbmfU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1370&quot; height=&quot;545&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ds18b20Config.h&lt;/code&gt;에서 &lt;code&gt;_DS18B20_USE_FREERTOS&lt;/code&gt;가 1로 설정되어 있으면 FreeRTOS 헤더를 include하려 한다. 우리는 FreeRTOS를 사용하지 않으므로 &lt;b&gt;이 값을 0으로 변경&lt;/b&gt;한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) tim.h &amp;mdash; 타이머 미생성&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIDXzH/dJMcacXbmXT/zxq1wf880dXfRU88edbVek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIDXzH/dJMcacXbmXT/zxq1wf880dXfRU88edbVek/img.png&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;443&quot; data-is-animation=&quot;false&quot; style=&quot;width: 63.9655%; margin-right: 10px;&quot; data-widthpercent=&quot;64.72&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIDXzH/dJMcacXbmXT/zxq1wf880dXfRU88edbVek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIDXzH%2FdJMcacXbmXT%2Fzxq1wf880dXfRU88edbVek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1442&quot; height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfo6bx/dJMcajvelLz/5yRSiZEmyZ1DTqvowN41V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfo6bx/dJMcajvelLz/5yRSiZEmyZ1DTqvowN41V1/img.png&quot; data-origin-width=&quot;1283&quot; data-origin-height=&quot;723&quot; data-is-animation=&quot;false&quot; style=&quot;width: 34.8717%;&quot; data-widthpercent=&quot;35.28&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfo6bx/dJMcajvelLz/5yRSiZEmyZ1DTqvowN41V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdfo6bx%2FdJMcajvelLz%2F5yRSiZEmyZ1DTqvowN41V1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1283&quot; height=&quot;723&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;tim.h&lt;/code&gt;를 주석 처리하면 타이머 핸들러가 정의되지 않았다는 에러가 발생한다. 이를 해결하려면 STM32CubeIDE에서 &lt;b&gt;TIM2를 활성화&lt;/b&gt;하여 타이머를 생성해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(5) GPIO, PIN 설정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEakI5/dJMcaiC5043/OkZ4grxDwPam4PKcBMkPM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEakI5/dJMcaiC5043/OkZ4grxDwPam4PKcBMkPM1/img.png&quot; style=&quot;width: 63.3278%; margin-right: 10px;&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;467&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;64.07&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEakI5/dJMcaiC5043/OkZ4grxDwPam4PKcBMkPM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEakI5%2FdJMcaiC5043%2FOkZ4grxDwPam4PKcBMkPM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1444&quot; height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvdABX/dJMcaaE6J0V/kb9GYXybNkVYMOl3xtW0U0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvdABX/dJMcaaE6J0V/kb9GYXybNkVYMOl3xtW0U0/img.png&quot; style=&quot;width: 35.5094%;&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;849&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;35.93&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvdABX/dJMcaaE6J0V/kb9GYXybNkVYMOl3xtW0U0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvdABX%2FdJMcaaE6J0V%2Fkb9GYXybNkVYMOl3xtW0U0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1472&quot; height=&quot;849&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cni8Y6/dJMcagSP9E6/fgnrpfeYw0TpZMrKsfXZJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cni8Y6/dJMcagSP9E6/fgnrpfeYw0TpZMrKsfXZJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cni8Y6/dJMcagSP9E6/fgnrpfeYw0TpZMrKsfXZJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcni8Y6%2FdJMcagSP9E6%2FfgnrpfeYw0TpZMrKsfXZJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1533&quot; height=&quot;642&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ds18b20Config.h&lt;/code&gt;에서 GPIO 포트와 핀을 설정해야 한다. 이전 글에서 온도 센서 DAT 핀을 PA3에 연결했으므로, &lt;code&gt;main.h&lt;/code&gt;에 정의된 &lt;code&gt;TEMP_DATA_GPIO_Port&lt;/code&gt;와 &lt;code&gt;TEMP_DATA_Pin&lt;/code&gt;을 사용한다. &lt;code&gt;ds18b20Config.h&lt;/code&gt;에 &lt;b&gt;&lt;code&gt;main.h&lt;/code&gt;를 include&lt;/b&gt;하고 GPIO/PIN 값을 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[IMG: ds18b20Config.h 설정]&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(6) 타이머 핸들러 설정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1521&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DeBS4/dJMcacJGTSy/P7wkIMpH31IjZP8PMScN10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DeBS4/dJMcacJGTSy/P7wkIMpH31IjZP8PMScN10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DeBS4/dJMcacJGTSy/P7wkIMpH31IjZP8PMScN10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDeBS4%2FdJMcacJGTSy%2FP7wkIMpH31IjZP8PMScN10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1521&quot; height=&quot;648&quot; data-origin-width=&quot;1521&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;821&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPrMQd/dJMcabxcsy4/nxghfNC9fwMqgyKeAyOX61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPrMQd/dJMcabxcsy4/nxghfNC9fwMqgyKeAyOX61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPrMQd/dJMcabxcsy4/nxghfNC9fwMqgyKeAyOX61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPrMQd%2FdJMcabxcsy4%2FnxghfNC9fwMqgyKeAyOX61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1452&quot; height=&quot;821&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;821&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ds18b20Config.h&lt;/code&gt;에서 타이머를 &lt;code&gt;htim2&lt;/code&gt;로 설정한다. &lt;code&gt;htim2&lt;/code&gt;는 &lt;code&gt;main.c&lt;/code&gt;에 전역 변수로 선언되어 있으므로, Config 파일에서 &lt;b&gt;&lt;code&gt;extern TIM_HandleTypeDef htim2;&lt;/code&gt;&lt;/b&gt;를 작성하여 참조한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 모두 거치면 에러 없이 컴파일이 완료된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;타이머와 클럭 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Prescaler와 Counter Period&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;1074&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buCHPH/dJMcaf0Fav3/K8TEEkIAKm7svHqdoYtjH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buCHPH/dJMcaf0Fav3/K8TEEkIAKm7svHqdoYtjH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buCHPH/dJMcaf0Fav3/K8TEEkIAKm7svHqdoYtjH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuCHPH%2FdJMcaf0Fav3%2FK8TEEkIAKm7svHqdoYtjH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1512&quot; height=&quot;1074&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;1074&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리에서 &quot;1틱이 1&amp;mu;s가 되도록 타이머를 설정하라&quot;고 요구한다. 타이머의 시간 조절은 &lt;b&gt;Prescaler(분주비)&lt;/b&gt;와 &lt;b&gt;Counter Period(카운터 주기)&lt;/b&gt; 두 가지로 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 클럭이 8MHz(= 8,000,000Hz)일 때:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Prescaler = (10000 - 1)&lt;/b&gt;: 8MHz를 10000으로 나눠 800Hz로 만든다. 즉 카운터가 1 증가하는 데 10000 클럭이 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Counter Period = (800 - 1)&lt;/b&gt;: 카운터가 0~799까지 올라가고 다시 0으로 돌아오면 인터럽트 발생. 800Hz로 카운터가 증가하므로 800번 세면 &lt;b&gt;1초&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 라이브러리가 요구하는 것은 1초가 아니라 &lt;b&gt;1&amp;mu;s&lt;/b&gt;이므로, 클럭을 72MHz로 올려야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RCC로 72MHz 변경&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t7lcA/dJMb990vr9h/adTLOUWkAfLIlT2T625Lkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t7lcA/dJMb990vr9h/adTLOUWkAfLIlT2T625Lkk/img.png&quot; data-alt=&quot;RCC Clock Configuration &amp;amp;mdash; 72MHz&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t7lcA/dJMb990vr9h/adTLOUWkAfLIlT2T625Lkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft7lcA%2FdJMb990vr9h%2FadTLOUWkAfLIlT2T625Lkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1528&quot; height=&quot;754&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RCC Clock Configuration &amp;mdash; 72MHz&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE의 .ioc에서 &lt;code&gt;RCC &amp;rarr; High Speed Clock &amp;rarr; Crystal/Ceramic Resonator&lt;/code&gt;를 선택하여 외부 크리스탈을 활성화한다. Clock Configuration에서 &lt;b&gt;HCLK를 72MHz로 설정&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;72MHz에서 1&amp;mu;s 틱을 만들려면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Prescaler = (72 - 1)&lt;/b&gt;: 72MHz를 72로 나눠 1MHz로 만든다. 1MHz는 1초에 1,000,000번이므로 1틱 = 1&amp;mu;s&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Counter Period = 0xFFFF&lt;/b&gt;: 최대값으로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Counter Period를 작게 설정하면 onewire 내부의 while문에서 카운터 값이 기대보다 작아져 &lt;b&gt;무한 루프가 발생&lt;/b&gt;한다. 반드시 0xFFFF로 설정해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JOXjf/dJMcadPhjUp/9aZ9vybbfcxiEUEkBi1C8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JOXjf/dJMcadPhjUp/9aZ9vybbfcxiEUEkBi1C8k/img.png&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;849&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.4883%; margin-right: 10px;&quot; data-widthpercent=&quot;53.11&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JOXjf/dJMcadPhjUp/9aZ9vybbfcxiEUEkBi1C8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJOXjf%2FdJMcadPhjUp%2F9aZ9vybbfcxiEUEkBi1C8k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1078&quot; height=&quot;849&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/evls7v/dJMcabqtz87/J6JikVjzS9pDzBMuGKLTjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/evls7v/dJMcabqtz87/J6JikVjzS9pDzBMuGKLTjk/img.png&quot; data-origin-width=&quot;1332&quot; data-origin-height=&quot;1188&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;46.89&quot; style=&quot;width: 46.3489%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/evls7v/dJMcabqtz87/J6JikVjzS9pDzBMuGKLTjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fevls7v%2FdJMcabqtz87%2FJ6JikVjzS9pDzBMuGKLTjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1332&quot; height=&quot;1188&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; TIM2 설정 &amp;mdash; Prescaler 71, Counter Period 65535 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클럭 변경에 따른 딜레이 조정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클럭이 8MHz에서 72MHz로 9배 빨라졌으므로, 기존 코드의 &lt;code&gt;HAL_Delay(50)&lt;/code&gt;을 &lt;b&gt;&lt;code&gt;HAL_Delay(450)&lt;/code&gt;&lt;/b&gt;으로 변경해야 이전과 비슷한 속도로 동작한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온도 측정 및 FND 표시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;초기화 및 온도 읽기&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c
#include &quot;ds18b20.h&quot;

// main 함수 내
Ds18b20_Init();

while (1) {
    Ds18b20_ManualConvert();
    // ds18b20[0].Temperature에 온도값이 저장됨
    int temp = (int)(ds18b20[0].Temperature * 10);  // 소수점 1자리까지 정수로 변환
    DG_Temper(temp);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Ds18b20_Init()&lt;/code&gt;을 호출하면 센서와 1-Wire 통신이 초기화되고, &lt;code&gt;Ds18b20_ManualConvert()&lt;/code&gt;를 호출하면 온도 변환이 수행된다. 변환 결과는 &lt;code&gt;ds18b20[0].Temperature&lt;/code&gt;에 float으로 저장된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;1047&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oqpvs/dJMcaaLRkqh/Z1adYxuXHlUoWNzFQzieF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oqpvs/dJMcaaLRkqh/Z1adYxuXHlUoWNzFQzieF1/img.png&quot; data-alt=&quot;디버그 워치 &amp;amp;mdash; Temperature 값 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oqpvs/dJMcaaLRkqh/Z1adYxuXHlUoWNzFQzieF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOqpvs%2FdJMcaaLRkqh%2FZ1adYxuXHlUoWNzFQzieF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;890&quot; height=&quot;1047&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;1047&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디버그 워치 &amp;mdash; Temperature 값 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FND에 온도 표시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-21-FND-%EC%A0%9C%EC%96%B4-%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;FND 제어 소스 분석&quot;)&lt;/a&gt;에서 구현한 &lt;code&gt;DG_Temper&lt;/code&gt; 함수를 활용한다. 온도값에 10을 곱해 정수로 변환한 뒤 &lt;code&gt;DG_Temper&lt;/code&gt;에 전달하면, 소수점이 포함된 온도가 FND에 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로브를 손으로 잡으면 온도가 올라가고, 놓으면 내려가는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260404_211529-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwpFIp/dJMcagkYLqu/VAKR4vGvef1nLc6SPpHrrk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwpFIp/dJMcagkYLqu/VAKR4vGvef1nLc6SPpHrrk/img.gif&quot; data-alt=&quot;FND에 온도 표시 &amp;amp;mdash; 프로브 잡았을 때/놓았을 때&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwpFIp/dJMcagkYLqu/VAKR4vGvef1nLc6SPpHrrk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cwpFIp/dJMcagkYLqu/VAKR4vGvef1nLc6SPpHrrk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260404_211529-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND에 온도 표시 &amp;mdash; 프로브 잡았을 때/놓았을 때&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트러블슈팅: GND 선 빠짐&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 중 온도가 변하지 않고 고정값만 출력되는 문제가 발생했다. 원인은 허무하게도 &lt;b&gt;GND 선이 빠져 있었던 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GND가 빠지면 송수신 신호의 기준점이 사라져 &lt;b&gt;간헐적인 통신 오류&lt;/b&gt;가 발생한다. 때로는 값이 읽히는 것처럼 보이지만 실제로는 정확하지 않은 데이터다. 이전 글들(&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-04-%EC%A0%84%EA%B8%B0-%EA%B8%B0%EB%B3%B8-%EC%83%81%EC%8B%9D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;전기 기본상식&lt;/a&gt;&quot;, &quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-16-printf%EB%8F%84-%EC%89%BD%EC%A7%80-%EC%95%8A%EB%8B%A4%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;printf도 쉽지 않다구&lt;/a&gt;&quot;)에서 GND가 신호의 기준점이라고 강조했는데, 이번에 실제로 체감한 사례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베디드에서 동작이 이상할 때는 코드를 의심하기 전에 &lt;b&gt;물리적 연결부터 확인&lt;/b&gt;하는 습관이 중요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현재 버전 라이브러리 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 사용한 이전 버전과 달리, DS18B20 라이브러리의 &lt;b&gt;현재 버전&lt;/b&gt;(&lt;a href=&quot;https://github.com/nimaltd/ds18b20&quot;&gt;GitHub 링크&lt;/a&gt;)은 구조가 크게 바뀌었다. 본인은 두 버전을 모두 적용해보았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이전 버전과의 차이&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;이전 버전 (강의)&lt;/th&gt;
&lt;th&gt;현재 버전&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;방식&lt;/td&gt;
&lt;td&gt;블로킹&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Non-Blocking&lt;/b&gt; (타이머 콜백)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 구성&lt;/td&gt;
&lt;td&gt;ds18b20.c/h + onewire.c/h 단일&lt;/td&gt;
&lt;td&gt;ds18b20.c/h + &lt;b&gt;별도 ow 라이브러리&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-Wire 의존&lt;/td&gt;
&lt;td&gt;내장&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/nimaltd/ow&quot;&gt;nimaltd/ow&lt;/a&gt; 별도 추가 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초기화&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ds18b20_Init()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ds18b20_init()&lt;/code&gt; + ow_init 구조체 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;온도 변환&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ds18b20_ManualConvert()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ds18b20_cnv()&lt;/code&gt; + &lt;code&gt;ds18b20_req_read()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;온도 읽기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ds18b20[0].Temperature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ds18b20_read_c()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포팅 과정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oGSQX/dJMb99MUc1J/qJIZKzxFEjOCl8QKCVk9A1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oGSQX/dJMb99MUc1J/qJIZKzxFEjOCl8QKCVk9A1/img.png&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;844&quot; data-is-animation=&quot;false&quot; style=&quot;width: 30.8629%; margin-right: 10px;&quot; data-widthpercent=&quot;31.23&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oGSQX/dJMb99MUc1J/qJIZKzxFEjOCl8QKCVk9A1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoGSQX%2FdJMb99MUc1J%2FqJIZKzxFEjOCl8QKCVk9A1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1266&quot; height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6rk0j/dJMcaffms80/MHUcrKYGNwXToaWq4buZY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6rk0j/dJMcaffms80/MHUcrKYGNwXToaWq4buZY0/img.png&quot; data-origin-width=&quot;1523&quot; data-origin-height=&quot;461&quot; data-is-animation=&quot;false&quot; style=&quot;width: 67.9743%;&quot; data-widthpercent=&quot;68.77&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6rk0j/dJMcaffms80/MHUcrKYGNwXToaWq4buZY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6rk0j%2FdJMcaffms80%2FMHUcrKYGNwXToaWq4buZY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1523&quot; height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; 현재 버전 포팅 &amp;mdash; 프로젝트 폴더 구조 / 타이머 콜백 함수 설정 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nimaltd/ow&quot;&gt;nimaltd/ow&lt;/a&gt; 라이브러리를 프로젝트에 추가&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nimaltd/ds18b20&quot;&gt;nimaltd/ds18b20&lt;/a&gt; 최신 버전 파일 추가&lt;/li&gt;
&lt;li&gt;타이머 콜백 함수 설정 &amp;mdash; ow 라이브러리가 타이머 인터럽트를 통해 Non-Blocking으로 동작&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;온도 측정 코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c
#include &quot;ds18b20.h&quot;

ds18b20_t ds18;

// 타이머 콜백
void ds18_tim_cb(TIM_HandleTypeDef *htim) {
    ow_callback(&amp;amp;ds18.ow);
}

// main 함수 내
ow_init_t ow_init_struct;
ow_init_struct.tim_handle = &amp;amp;htim2;
ow_init_struct.gpio = TEMP_DATA_GPIO_Port;
ow_init_struct.pin = TEMP_DATA_Pin;
ow_init_struct.tim_cb = ds18_tim_cb;
ow_init_struct.done_cb = NULL;
ow_init_struct.rom_id_filter = DS18B20_ID;

ds18b20_init(&amp;amp;ds18, &amp;amp;ow_init_struct);
ds18b20_update_rom_id(&amp;amp;ds18);
while (ds18b20_is_busy(&amp;amp;ds18));

while (1) {
    ds18b20_cnv(&amp;amp;ds18);
    while (ds18b20_is_busy(&amp;amp;ds18));
    while (!ds18b20_is_cnv_done(&amp;amp;ds18));

    ds18b20_req_read(&amp;amp;ds18, 0);
    while (ds18b20_is_busy(&amp;amp;ds18));

    int temp = (int)(ds18b20_read_c(&amp;amp;ds18) * 10);
    DG_Temper(temp);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 버전의 &lt;code&gt;Ds18b20_Init()&lt;/code&gt; + &lt;code&gt;Ds18b20_ManualConvert()&lt;/code&gt; 두 줄로 끝나던 것이, 현재 버전에서는 초기화 구조체 설정, 타이머 콜백, busy 체크 등 &lt;b&gt;초기 설정이 더 복잡&lt;/b&gt;하다. 하지만 Non-Blocking 방식이라 CPU가 온도 변환을 기다리는 동안 다른 작업을 수행할 수 있어 &lt;b&gt;실제 제품에서는 유리&lt;/b&gt;하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsqZVS/dJMcahEbcF4/L9wT6cBPDjtNTDkzYCjeC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsqZVS/dJMcahEbcF4/L9wT6cBPDjtNTDkzYCjeC0/img.png&quot; data-origin-width=&quot;1186&quot; data-origin-height=&quot;694&quot; data-is-animation=&quot;false&quot; style=&quot;width: 62.3516%; margin-right: 10px;&quot; data-widthpercent=&quot;63.09&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsqZVS/dJMcahEbcF4/L9wT6cBPDjtNTDkzYCjeC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsqZVS%2FdJMcahEbcF4%2FL9wT6cBPDjtNTDkzYCjeC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1186&quot; height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDH6sG/dJMcahRIK73/KOxakZ2UXHM4k3Vs2MYqa1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDH6sG/dJMcahRIK73/KOxakZ2UXHM4k3Vs2MYqa1/img.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot; data-is-animation=&quot;true&quot; data-filename=&quot;ezgif.com-video-to-gif-converter.gif&quot; style=&quot;width: 36.4857%;&quot; data-widthpercent=&quot;36.91&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDH6sG/dJMcahRIK73/KOxakZ2UXHM4k3Vs2MYqa1/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDH6sG%2FdJMcahRIK73%2FKOxakZ2UXHM4k3Vs2MYqa1%2Fimg.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; 현재 버전 코드 &amp;mdash; 온도 읽기 동작 확인 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>OJTube임베디드입문</category>
      <category>OneWire</category>
      <category>STM32</category>
      <category>timer</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/59</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-24-1-Wire-%ED%86%B5%EC%8B%A0-%EB%82%98%EB%A6%84-%EC%9C%A0%EB%AA%85%ED%96%88%EB%8B%A4#entry59comment</comments>
      <pubDate>Sun, 26 Apr 2026 19:54:48 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 23. 온도센서를 붙여보자</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-23-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C%EB%A5%BC-%EB%B6%99%EC%97%AC%EB%B3%B4%EC%9E%90</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;온도 센서 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고추 건조기의 핵심 기능은 &lt;b&gt;온도를 측정하고 제어&lt;/b&gt;하는 것이다. 이번 강의부터 온도 센서를 다루며, 난이도가 높아 총 3강에 걸쳐 진행된다. 이번 글에서는 센서 소개, 샘플 코드 분석, 하드웨어 배선까지 다루고, 실제 동작은 다음 글에서 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도 센서는 크게 세 부분으로 구성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로브&lt;/b&gt;: 온도를 감지하는 쇠 막대 부분. 열이 가해지면 내부 저항값이 변하고, 이에 따라 전압값이 변한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모듈&lt;/b&gt;: 프로브에서 받은 전압 변화를 내장 칩이 계산하여 디지털 데이터 신호로 변환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3개 핀&lt;/b&gt;: VCC(전원), GND(접지), DAT(데이터)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1147&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTatf5/dJMcabxcsj8/lytTUBneSpBy4nPA4mdgi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTatf5/dJMcabxcsj8/lytTUBneSpBy4nPA4mdgi0/img.png&quot; data-alt=&quot;온도센서 실물 &amp;amp;mdash; 프로브&amp;amp;middot;전선&amp;amp;middot;모듈&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTatf5/dJMcabxcsj8/lytTUBneSpBy4nPA4mdgi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTatf5%2FdJMcabxcsj8%2FlytTUBneSpBy4nPA4mdgi0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1147&quot; height=&quot;1144&quot; data-origin-width=&quot;1147&quot; data-origin-height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;온도센서 실물 &amp;mdash; 프로브&amp;middot;전선&amp;middot;모듈&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;순수 센서 vs 모듈&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순수 센서 (ADC 방식)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로브만 있는 순수 센서를 사용하려면, 회로를 직접 구성하여 &lt;b&gt;ADC(아날로그&amp;rarr;디지털 변환)&lt;/b&gt;로 전압 변화를 측정해야 한다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-12-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%8B%9C%ED%8A%B8-%EC%9D%BD%EC%96%B4%EC%A3%BC%EB%8A%94-%EB%82%A8%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;데이터시트 읽어주는 남자&quot;)&lt;/a&gt;에서 다룬 것처럼 STM32에는 12비트 ADC가 내장되어 있어 0~3.6V를 4096단계로 나눌 수 있다. 측정된 전압값을 온도 변환 테이블과 대조하여 온도를 구하는 방식인데, &lt;b&gt;회로 구성과 캘리브레이션의 난이도가 매우 높다&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모듈 방식 (이번 강의)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 사용하는 모듈형 센서는 &lt;b&gt;내장 칩이 온도 계산을 대신 해준다&lt;/b&gt;. 모듈이 프로브의 전압 변화를 받아 내부적으로 온도를 계산하고, DAT 핀을 통해 디지털 데이터로 STM32에 전달한다. 우리는 이 데이터를 받아서 FND에 표시하면 된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1-Wire 통신의 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 온도 센서 모듈은 &lt;b&gt;1-Wire(원 와이어) 통신&lt;/b&gt;을 사용한다. 이전에 다룬 SPI(MOSI/MISO/SCLK/CS)나 UART(TX/RX)와 달리, &lt;b&gt;DAT 핀 하나로 읽기와 쓰기를 모두 수행&lt;/b&gt;하는 독특한 방식이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;통신 방식&lt;/th&gt;
&lt;th&gt;데이터 선 수&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SPI&lt;/td&gt;
&lt;td&gt;2개 (MOSI, MISO) + 클럭&lt;/td&gt;
&lt;td&gt;전이중, 클럭 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UART&lt;/td&gt;
&lt;td&gt;2개 (TX, RX)&lt;/td&gt;
&lt;td&gt;전이중, 타임베이스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1-Wire&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;1개 (DAT)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;반이중, 같은 핀으로 Read/Write&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-Wire에서는 같은 DAT 핀의 &lt;b&gt;GPIO 모드를 Output&amp;harr;Input으로 전환&lt;/b&gt;하면서 데이터를 주고받는다. Output 모드에서 신호를 보내고, Input 모드로 전환하여 응답을 읽는 방식이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;샘플 코드 분석: read_bit (1비트 읽기)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의에서 제공된 Arduino용 OneWire 라이브러리의 &lt;code&gt;read_bit&lt;/code&gt; 함수를 분석한다. 데이터시트를 처음부터 구현하는 것보다 &lt;b&gt;샘플 코드를 분석하는 것이 훨씬 효율적&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;read_bit&lt;/code&gt; 함수의 동작 순서:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;인터럽트 비활성화&lt;/b&gt; &amp;mdash; 타이밍이 중요하므로 다른 인터럽트가 끼어들지 않게&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DAT 핀을 Output 모드로 전환&lt;/b&gt; &amp;rarr; &lt;b&gt;LOW 출력&lt;/b&gt; &amp;mdash; &quot;데이터를 보내달라&quot;는 신호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3&amp;mu;s 대기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DAT 핀을 Input 모드로 전환&lt;/b&gt; &amp;mdash; 같은 핀을 이제 읽기용으로 변경&lt;/li&gt;
&lt;li&gt;&lt;b&gt;10&amp;mu;s 대기&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DAT 핀에서 1비트 읽기&lt;/b&gt; &amp;mdash; High면 1, Low면 0&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인터럽트 재활성화&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;53&amp;mu;s 대기&lt;/b&gt; &amp;mdash; 다음 비트 읽기 전 슬롯 완료 대기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;읽은 값(r) 반환&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;890&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KJw2e/dJMcagFkgOE/FZUpVT5fYeIk7b3S10BkL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KJw2e/dJMcagFkgOE/FZUpVT5fYeIk7b3S10BkL0/img.png&quot; data-alt=&quot;read_bit 함수 코드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KJw2e/dJMcagFkgOE/FZUpVT5fYeIk7b3S10BkL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKJw2e%2FdJMcagFkgOE%2FFZUpVT5fYeIk7b3S10BkL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1533&quot; height=&quot;890&quot; data-origin-width=&quot;1533&quot; data-origin-height=&quot;890&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;read_bit 함수 코드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;같은 DAT 선을 Output&amp;rarr;Input으로 모드 전환&lt;/b&gt;하면서 사용한다는 점이다. Output으로 LOW 신호를 보내 센서에게 &quot;데이터를 보내라&quot;고 요청하고, Input으로 전환하여 센서가 보낸 응답을 읽는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;샘플 코드 분석: read (1바이트 읽기)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;read&lt;/code&gt; 함수는 &lt;code&gt;read_bit&lt;/code&gt;을 8번 호출하여 1바이트(8비트)를 조합한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2B9IX/dJMcab43fia/AQ1okVYVRbkKahVYuERFN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2B9IX/dJMcab43fia/AQ1okVYVRbkKahVYuERFN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2B9IX/dJMcab43fia/AQ1okVYVRbkKahVYuERFN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2B9IX%2FdJMcab43fia%2FAQ1okVYVRbkKahVYuERFN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;583&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 과정:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;bitMask&lt;/code&gt;가 &lt;code&gt;0x01&lt;/code&gt;(= &lt;code&gt;00000001&lt;/code&gt;)에서 시작&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read_bit()&lt;/code&gt;이 1을 반환하면, &lt;code&gt;r |= bitMask&lt;/code&gt;로 해당 비트 위치를 1로 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bitMask &amp;lt;&amp;lt;= 1&lt;/code&gt;로 마스크를 왼쪽으로 시프트 (&lt;code&gt;00000010&lt;/code&gt;, &lt;code&gt;00000100&lt;/code&gt;, ...)&lt;/li&gt;
&lt;li&gt;8번 반복하면 &lt;code&gt;bitMask&lt;/code&gt;가 &lt;code&gt;00000000&lt;/code&gt;이 되어 for문 종료&lt;/li&gt;
&lt;li&gt;조합된 바이트 &lt;code&gt;r&lt;/code&gt;을 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;b&gt;LSB(최하위 비트)부터 읽는다&lt;/b&gt;. 이전 글(&quot;FND 제어&quot;)에서 다룬 send 함수가 MSB부터 보냈던 것과 반대 방향이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하드웨어 배선&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로브 &amp;harr; 모듈 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온도 센서 프로브의 3가닥 전선을 모듈의 해당 핀에 연결한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;프로브 전선 색상&lt;/th&gt;
&lt;th&gt;모듈 핀&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;빨간색&lt;/td&gt;
&lt;td&gt;VCC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;노란색&lt;/td&gt;
&lt;td&gt;DAT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검정색&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모듈 &amp;harr; 보드 연결&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모듈 핀&lt;/th&gt;
&lt;th&gt;연결&lt;/th&gt;
&lt;th&gt;보드 핀&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VCC&lt;/td&gt;
&lt;td&gt;빨간색 점퍼선 &amp;rarr; 빵판 + 레일&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;검정색 점퍼선 &amp;rarr; 빵판 - 레일&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAT&lt;/td&gt;
&lt;td&gt;노란색 점퍼선&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PA3&lt;/b&gt; (11번 핀)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260404_172227.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dxinn/dJMcaiXmOqn/xog9Dgne013fwAiTTXeHu0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dxinn/dJMcaiXmOqn/xog9Dgne013fwAiTTXeHu0/img.jpg&quot; data-alt=&quot;프로브&amp;amp;middot;모듈&amp;amp;middot;빵판&amp;amp;middot;보드 최종 연결&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dxinn/dJMcaiXmOqn/xog9Dgne013fwAiTTXeHu0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDxinn%2FdJMcaiXmOqn%2Fxog9Dgne013fwAiTTXeHu0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260404_172227.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로브&amp;middot;모듈&amp;middot;빵판&amp;middot;보드 최종 연결&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 글 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어 배선이 완료되었다. 다음 글에서는 오늘 분석한 OneWire 라이브러리를 STM32 환경에 포팅하여, &lt;b&gt;실제로 온도를 읽고 FND에 표시&lt;/b&gt;하는 과정을 다룬다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>DS18B20</category>
      <category>OJTube임베디드입문</category>
      <category>OneWire</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/58</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-23-%EC%98%A8%EB%8F%84%EC%84%BC%EC%84%9C%EB%A5%BC-%EB%B6%99%EC%97%AC%EB%B3%B4%EC%9E%90#entry58comment</comments>
      <pubDate>Sun, 26 Apr 2026 19:37:11 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 22. STM32에서는 SPI 기능을 제공한다구</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-22-STM32%EC%97%90%EC%84%9C%EB%8A%94-SPI-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%9C%EB%8B%A4%EA%B5%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트 복사 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 기능을 추가할 때 기존 프로젝트를 복사하여 별도로 작업하고 싶은 경우가 있다. 단순히 폴더를 복사하면 &lt;b&gt;메타 정보가 함께 복사되어, 복사본을 수정해도 실제로는 원본 프로젝트가 수정되는 문제&lt;/b&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 깔끔한 방법은 &lt;b&gt;Export/Import를 활용&lt;/b&gt;하는 것이다. 강의에서는 폴더를 직접 복사한 뒤 IDE에서 삭제/재임포트하는 방식을 사용하지만, 아래 방법이 더 간단하고 메타 정보 충돌 위험도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Export (기존 프로젝트 내보내기):&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Project Explorer에서 프로젝트 &lt;b&gt;우클릭 &amp;rarr; Export&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;General &amp;rarr; Archive File&lt;/code&gt; 선택&lt;/li&gt;
&lt;li&gt;저장 경로 설정 &amp;rarr; &lt;b&gt;Finish&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Import (새 프로젝트로 가져오기):&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;새 workspace 생성&lt;/li&gt;
&lt;li&gt;&lt;code&gt;File &amp;rarr; Import &amp;rarr; General &amp;rarr; Existing Projects into Workspace&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Archive File&lt;/b&gt; 옵션에서 내보낸 .zip 파일 선택 &amp;rarr; &lt;b&gt;Finish&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2m6to/dJMcadhu5Mq/GcwjbRCDKPBX3L3kjZJgZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2m6to/dJMcadhu5Mq/GcwjbRCDKPBX3L3kjZJgZ0/img.png&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;881&quot; data-is-animation=&quot;false&quot; style=&quot;width: 56.9175%; margin-right: 10px;&quot; data-widthpercent=&quot;57.59&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2m6to/dJMcadhu5Mq/GcwjbRCDKPBX3L3kjZJgZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2m6to%2FdJMcadhu5Mq%2FGcwjbRCDKPBX3L3kjZJgZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;873&quot; height=&quot;881&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqIMbl/dJMcaiiNBq7/JKhyUmWJLI5zAupbw51iRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqIMbl/dJMcaiiNBq7/JKhyUmWJLI5zAupbw51iRK/img.png&quot; data-origin-width=&quot;732&quot; data-origin-height=&quot;1003&quot; data-is-animation=&quot;false&quot; style=&quot;width: 41.9197%;&quot; data-widthpercent=&quot;42.41&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqIMbl/dJMcaiiNBq7/JKhyUmWJLI5zAupbw51iRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqIMbl%2FdJMcaiiNBq7%2FJKhyUmWJLI5zAupbw51iRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;732&quot; height=&quot;1003&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; Export/Import 설정 화면 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법을 사용하면 메타 정보 충돌 없이 완전히 독립된 프로젝트가 생성된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPIO SPI에서 하드웨어 SPI로 전환하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글들(&quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-19-FND-%EC%A0%9C%EC%96%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FND 제어&lt;/a&gt;&quot;, &quot;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-21-FND-%EC%A0%9C%EC%96%B4-%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;FND 제어 소스 분석&lt;/a&gt;&quot;)에서는 GPIO를 직접 HIGH/LOW로 전환하여 SPI 통신을 소프트웨어적으로 구현했다. 이 방식은 동작하지만 두 가지 한계가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클럭 간격이 불균일&lt;/b&gt; &amp;mdash; &lt;code&gt;HAL_GPIO_WritePin&lt;/code&gt; 호출 사이에 다른 코드가 실행되면서 미세한 시간 차이가 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드가 복잡&lt;/b&gt; &amp;mdash; for문으로 비트를 하나씩 쪼개고, 클럭을 수동으로 생성해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32는 SPI 통신을 &lt;b&gt;하드웨어적으로 지원&lt;/b&gt;한다. 칩 내부의 SPI 모듈이 클럭 생성과 데이터 전송을 자동으로 처리하므로, 코드는 &lt;code&gt;HAL_SPI_Transmit&lt;/code&gt; 한 줄로 줄어들고 클럭도 균일해진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핀 재배치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하드웨어 SPI를 사용하려면 &lt;b&gt;STM32가 정해놓은 특정 핀&lt;/b&gt;으로 클럭과 데이터를 내보내야 한다. 기존 GPIO 핀 배치를 다음과 같이 변경한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;기존 (GPIO)&lt;/th&gt;
&lt;th&gt;변경 (HW SPI)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FND_SCLK (클럭)&lt;/td&gt;
&lt;td&gt;PB15&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PB13&lt;/b&gt; (SPI2_SCK)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FND_DIO (데이터)&lt;/td&gt;
&lt;td&gt;PB14&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PB15&lt;/b&gt; (SPI2_MOSI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FND_RCLK (래치)&lt;/td&gt;
&lt;td&gt;PB13&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PB14&lt;/b&gt; (GPIO Output 유지)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SCLK과 DIO는 하드웨어 SPI가 담당하고, RCLK은 래치 신호이므로 여전히 GPIO Output으로 직접 제어한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;1019&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS141D/dJMcaiXmOaW/3cONG3PsUkl950PYK1jSPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS141D/dJMcaiXmOaW/3cONG3PsUkl950PYK1jSPK/img.png&quot; data-alt=&quot;.ioc 핀 재배치 &amp;amp;mdash; PB13 SPI2_SCK, PB15 SPI2_MOSI&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS141D/dJMcaiXmOaW/3cONG3PsUkl950PYK1jSPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS141D%2FdJMcaiXmOaW%2F3cONG3PsUkl950PYK1jSPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1830&quot; height=&quot;1019&quot; data-origin-width=&quot;1830&quot; data-origin-height=&quot;1019&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.ioc 핀 재배치 &amp;mdash; PB13 SPI2_SCK, PB15 SPI2_MOSI&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPI 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에서 SPI2를 활성화하고 다음과 같이 설정한다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;SPI통신 제대로 배워보자&quot;)&lt;/a&gt;에서 다룬 각 옵션의 의미가 여기서 실제로 적용된다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정 항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mode&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Transmit Only Master&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;FND에 데이터를 보내기만 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NSS Signal&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Disable&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CS 핀 불필요 (FND 1개)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Size&lt;/td&gt;
&lt;td&gt;&lt;b&gt;8 bits&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기존 send 함수가 8비트 단위 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First Bit&lt;/td&gt;
&lt;td&gt;&lt;b&gt;MSB First&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기존 코드가 MSB부터 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prescaler&lt;/td&gt;
&lt;td&gt;&lt;b&gt;500 kbps&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;적절한 클럭 속도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock Polarity (CPOL)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;High&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기존 코드의 클럭 기본 상태가 High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock Phase (CPHA)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;1 Edge&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기존 코드가 Rising Edge에서 데이터 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드 변경&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;init_fnd 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPI 핸들러를 매개변수로 받도록 변경한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// fnd_controller.h
void init_fnd(SPI_HandleTypeDef *hspi);

// fnd_controller.c
SPI_HandleTypeDef *_hspi;

void init_fnd(SPI_HandleTypeDef *hspi) {
    _hspi = hspi;
    // 기존 매핑 테이블 초기화 코드 동일
    _LED_0F[0] = 0xC0;  // 0
    _LED_0F[1] = 0xF9;  // 1
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;send 함수 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 for문 + GPIO 제어 코드 전체가 &lt;b&gt;HAL_SPI_Transmit 한 줄&lt;/b&gt;로 교체된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 기존 (GPIO 방식)
void send(uint8_t X) {
    for (int i = 8; i &amp;gt;= 1; i--) {
        if (X &amp;amp; 0x80)
            HAL_GPIO_WritePin(FND_DIO_GPIO_Port, FND_DIO_Pin, HIGH);
        else
            HAL_GPIO_WritePin(FND_DIO_GPIO_Port, FND_DIO_Pin, LOW);
        X &amp;lt;&amp;lt;= 1;
        HAL_GPIO_WritePin(FND_SCLK_GPIO_Port, FND_SCLK_Pin, LOW);
        HAL_GPIO_WritePin(FND_SCLK_GPIO_Port, FND_SCLK_Pin, HIGH);
    }
}

// 변경 (HW SPI 방식)
void send(uint8_t X) {
    HAL_SPI_Transmit(_hspi, &amp;amp;X, 1, 100);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;send_port 함수의 RCLK 래치 부분은 GPIO로 유지하므로 변경하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;main 함수&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;// main.c
#include &quot;fnd_controller.h&quot;

// main 함수 내
init_fnd(&amp;amp;hspi2);  // SPI2 핸들러 전달

int count = 0;
while (1) {
    digit4_replay(count, 10);
    count++;
    if (count &amp;gt; 99) count = 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의: 코드를 &lt;b&gt;반드시 &lt;code&gt;USER CODE BEGIN&lt;/code&gt;과 &lt;code&gt;USER CODE END&lt;/code&gt; 주석 사이에 작성&lt;/b&gt;해야 한다. .ioc 설정 변경 후 코드를 재생성하면 이 주석 밖의 코드는 사라진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오실로스코프로 파형 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오실로스코프로 GPIO SPI와 하드웨어 SPI의 클럭 파형을 비교하면 차이가 명확히 보인다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GPIO SPI&lt;/b&gt;: 클럭 간격이 불균일. 소프트웨어 실행 시간에 따라 미세하게 달라짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HW SPI&lt;/b&gt;: 클럭 간격이 &lt;b&gt;매우 균일&lt;/b&gt;. 칩이 하드웨어적으로 정확한 타이밍을 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;gpio_vs_spi.png&quot; data-origin-width=&quot;2039&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q8dyy/dJMcafzDwVQ/XEirdQVLkFtAX3cxP4v9x0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q8dyy/dJMcafzDwVQ/XEirdQVLkFtAX3cxP4v9x0/img.png&quot; data-alt=&quot;오실로스코프 &amp;amp;mdash; GPIO SPI vs HW SPI 클럭 파형 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q8dyy/dJMcafzDwVQ/XEirdQVLkFtAX3cxP4v9x0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq8dyy%2FdJMcafzDwVQ%2FXEirdQVLkFtAX3cxP4v9x0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2039&quot; height=&quot;903&quot; data-filename=&quot;gpio_vs_spi.png&quot; data-origin-width=&quot;2039&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;오실로스코프 &amp;mdash; GPIO SPI vs HW SPI 클럭 파형 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 결과는 동일하다. FND에 이전과 똑같이 숫자가 올라가는 것을 확인할 수 있다. 하지만 내부적으로는 코드가 훨씬 간결해지고 클럭이 안정적이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FND 제어 시리즈를 정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;FND 제어&lt;/b&gt; &amp;mdash; 샘플 코드를 확보하여 GPIO로 SPI를 수동 구현, 일단 동작시킴&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FND 제어 소스 분석&lt;/b&gt; &amp;mdash; 오실로스코프로 파형 검증, 래치/반전 동작/소수점 비트 연산 분석&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이번 글&lt;/b&gt; &amp;mdash; GPIO SPI를 HAL_SPI_Transmit 한 줄로 교체, 클럭 균일성 비교&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 앞으로 다른 SPI 장치를 제어할 때도 동일하게 적용된다. GPIO로 먼저 원리를 이해한 뒤, 하드웨어 SPI로 전환하면 코드가 간결해지고 성능이 개선된다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>FND</category>
      <category>hal</category>
      <category>OJTube임베디드입문</category>
      <category>SPI</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/57</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-22-STM32%EC%97%90%EC%84%9C%EB%8A%94-SPI-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%9C%EB%8B%A4%EA%B5%AC#entry57comment</comments>
      <pubDate>Sun, 26 Apr 2026 19:28:10 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 21. FND 제어 소스 분석</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-21-FND-%EC%A0%9C%EC%96%B4-%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;send 함수 파형 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-19-FND-%EC%A0%9C%EC%96%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;FND 제어&quot;)&lt;/a&gt;에서 send 함수의 비트 연산을 코드 레벨에서 분석했다. 이번에는 &lt;b&gt;오실로스코프로 실제 파형을 확인&lt;/b&gt;하여, 코드가 만들어내는 전기적 신호를 눈으로 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;send 함수로 &lt;code&gt;0xF8&lt;/code&gt;(이진수 &lt;code&gt;11111000&lt;/code&gt;)을 보냈을 때, 오실로스코프로 SCLK(클럭)과 DIO(데이터) 핀의 파형을 관찰하면 클럭에 맞춰 1,1,1,1,1,0,0,0 순서로 데이터가 전송되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260403_211149.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/30I3O/dJMcah5h2pC/X3O2234wgk98hknSruH5tK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/30I3O/dJMcah5h2pC/X3O2234wgk98hknSruH5tK/img.jpg&quot; data-alt=&quot;오실로스코프 &amp;amp;mdash; 0xF8 전송 시 SCLK과 DIO 파형&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/30I3O/dJMcah5h2pC/X3O2234wgk98hknSruH5tK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F30I3O%2FdJMcah5h2pC%2FX3O2234wgk98hknSruH5tK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260403_211149.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;오실로스코프 &amp;mdash; 0xF8 전송 시 SCLK과 DIO 파형&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 눈에 띄는 점은 &lt;b&gt;클럭 간격이 완벽히 일정하지 않다&lt;/b&gt;는 것이다. 이는 GPIO를 소프트웨어적으로 HIGH/LOW 전환하여 클럭을 만들기 때문이다. &lt;code&gt;HAL_GPIO_WritePin&lt;/code&gt; 함수 호출 사이에 다른 코드가 실행되면서 미세한 시간 차이가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPI 통신에서는 클럭 간격이 기계적으로 완벽히 일정하지 않아도 동작한다. 클럭의 엣지 시점에 데이터를 읽기 때문에, 간격이 다소 불균일해도 데이터 전송에는 문제가 없다. 다만 STM32가 제공하는 &lt;b&gt;하드웨어 SPI 통신&lt;/b&gt;을 사용하면 훨씬 일정한 클럭 파형을 얻을 수 있다. 이 내용은 다음 글에서 다룰 예정이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;send_port &amp;mdash; FND를 켜는 최소 조건&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;send만으로는 FND가 켜지지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main 함수에서 &lt;code&gt;send(0xF8);&lt;/code&gt;만 호출하면 FND에 아무것도 표시되지 않는다. send 함수는 SCLK과 DIO를 통해 &lt;b&gt;데이터를 시프트 레지스터에 밀어넣기만&lt;/b&gt; 할 뿐, FND 출력에 반영하라는 신호를 주지 않기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RCLK 래치 시퀀스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FND를 실제로 켜려면 &lt;code&gt;send_port&lt;/code&gt; 함수를 사용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;void send_port(uint8_t X, uint8_t port) {
    send(X);      // 세그먼트 데이터 전송
    send(port);   // 자릿수 선택 데이터 전송
    HAL_GPIO_WritePin(FND_RCLK_GPIO_Port, FND_RCLK_Pin, HIGH);
    HAL_GPIO_WritePin(FND_RCLK_GPIO_Port, FND_RCLK_Pin, LOW);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;send로 8비트 데이터를 두 번 보낸 후(세그먼트 데이터 + 자릿수 선택), &lt;b&gt;RCLK을 HIGH &amp;rarr; LOW로 전환&lt;/b&gt;한다. 이 RCLK의 하강 엣지가 &quot;시프트 레지스터에 들어있는 데이터를 출력 레지스터로 옮겨라&quot;라는 래치 신호가 된다. 이 시퀀스가 FND를 동작시키는 최소 조건이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;port 비트로 자릿수 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 send에서 보내는 port 값은 FND의 4자리 중 어느 자릿수에 표시할지를 결정한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;port 값&lt;/th&gt;
&lt;th&gt;켜지는 자릿수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0b0001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1번째 (1의 자리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0b0010&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2번째 (10의 자리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0b0100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3번째 (100의 자리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0b1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4번째 (1000의 자리)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FND 반전 동작 &amp;mdash; Low일 때 켜진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FND의 각 세그먼트(a, b, c, d, e, f, g, dp)는 해당 비트가 &lt;b&gt;0(Low)일 때 켜지고, 1(High)일 때 꺼지는&lt;/b&gt; 반전 방식으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;0xF8&lt;/code&gt;은 이진수로 &lt;code&gt;11111000&lt;/code&gt;이다. 하위 3비트(a, b, c)가 0이므로 a, b, c 세그먼트가 켜진다. 이 세 세그먼트의 조합이 숫자 &lt;b&gt;'7'&lt;/b&gt;을 표시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 다뤘던 매핑 테이블도 이 원리로 만들어진 것이다. 예를 들어 &lt;code&gt;0xC0&lt;/code&gt;(= &lt;code&gt;11000000&lt;/code&gt;)은 하위 6비트가 0이므로 a~f 세그먼트가 켜져 숫자 '0'이 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;11111000.png&quot; data-origin-width=&quot;126&quot; data-origin-height=&quot;191&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gp71E/dJMcacQqm5Z/vCqep5kw7Ytgj8Yux9Vb9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gp71E/dJMcacQqm5Z/vCqep5kw7Ytgj8Yux9Vb9K/img.png&quot; data-alt=&quot;7세그먼트 비트 대응 &amp;amp;mdash; 0xF8이 '7'로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gp71E/dJMcacQqm5Z/vCqep5kw7Ytgj8Yux9Vb9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGp71E%2FdJMcacQqm5Z%2FvCqep5kw7Ytgj8Yux9Vb9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;126&quot; height=&quot;191&quot; data-filename=&quot;11111000.png&quot; data-origin-width=&quot;126&quot; data-origin-height=&quot;191&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;7세그먼트 비트 대응 &amp;mdash; 0xF8이 '7'로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소수점 표현 &amp;mdash; 0x7F AND 연산&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_29_dp_bitmask.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmN3ex/dJMcaiC50j9/xm2kGqsKkl8VWHXretDqPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmN3ex/dJMcaiC50j9/xm2kGqsKkl8VWHXretDqPk/img.png&quot; data-alt=&quot;diagram_01_dp_bitmask&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmN3ex/dJMcaiC50j9/xm2kGqsKkl8VWHXretDqPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmN3ex%2FdJMcaiC50j9%2Fxm2kGqsKkl8VWHXretDqPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;640&quot; data-filename=&quot;diagram_29_dp_bitmask.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_01_dp_bitmask&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;매핑 테이블의 데이터에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MSB(최상위 비트)가 dp(소수점)에 대응&lt;/b&gt;한다. 모든 숫자 데이터의 MSB가 1이므로, 기본 상태에서는 소수점이 항상 꺼져 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;소수점을 켜려면 MSB를 0으로 만들면 된다. 다른 세그먼트에는 영향을 주지 않으면서 MSB만 0으로 바꾸려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;0x7F(=&lt;span&gt;&amp;nbsp;&lt;/span&gt;01111111)와 AND 연산&lt;/b&gt;을 하면 된다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;// 예: 숫자 '7' 데이터에 소수점 추가
0xF8 &amp;amp; 0x7F
= 11111000 &amp;amp; 01111111
= 01111000   // MSB가 0이 되어 dp가 켜짐, 나머지는 그대로&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 숫자 데이터든&lt;span&gt;&amp;nbsp;&lt;/span&gt;0x7F와 AND 연산하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;해당 숫자는 그대로 표시되면서 소수점만 추가&lt;/b&gt;된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온도 표시 함수 (DG_Temper)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FND를 온도 표시에 활용하기 위해 &lt;code&gt;DG_Temper&lt;/code&gt; 함수를 구현한다. 이 함수는 정수 입력을 받아 소수점이 포함된 온도로 표시한다. 예를 들어 300을 넣으면 '30.0', 1234를 넣으면 '123.4'로 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;소수점 위치를 2번째 자릿수에 고정&lt;/b&gt;하는 것이다. 2번째 자릿수의 데이터에만 &lt;code&gt;0x7F&lt;/code&gt; AND 연산을 적용하면, 다른 자릿수에는 영향 없이 해당 위치에만 소수점이 찍힌다.&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;void DG_Temper(int n) {
    int n1, n2, n3, n4;
    n1 = n % 10;
    n2 = (n / 10) % 10;
    n3 = (n / 100) % 10;
    n4 = (n / 1000) % 10;

    send_port(_LED_0F[n1], 0b0001);
    send_port(_LED_0F[n2] &amp;amp; 0x7F, 0b0010);  // 2번째 자리에 소수점 추가
    send_port(_LED_0F[n3], 0b0100);
    send_port(_LED_0F[n4], 0b1000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260403_212918-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rT90B/dJMcadIwH1x/6IiKTEK8ajZOJcwarKtIk0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rT90B/dJMcadIwH1x/6IiKTEK8ajZOJcwarKtIk0/img.gif&quot; data-alt=&quot;FND 소수점 포함 온도 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rT90B/dJMcadIwH1x/6IiKTEK8ajZOJcwarKtIk0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/rT90B/dJMcadIwH1x/6IiKTEK8ajZOJcwarKtIk0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260403_212918-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND 소수점 포함 온도 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-19-FND-%EC%A0%9C%EC%96%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;FND 제어&quot;)&lt;/a&gt;에서는 샘플 코드를 확보하여 일단 동작시킨 후 코드 레벨에서 분석했다. 이번 글에서는 한 단계 더 깊이 들어가서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;오실로스코프로 실제 파형을 확인&lt;/b&gt;하여 코드가 만드는 전기 신호를 검증했고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;send만으로는 안 되고 RCLK 래치가 필요한 이유&lt;/b&gt;를 이해했고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FND의 반전 동작 원리&lt;/b&gt;(Low = 켜짐)를 파악했고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;소수점 표현을 위한 비트 연산&lt;/b&gt;(&lt;code&gt;0x7F&lt;/code&gt; AND)을 구현했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 GPIO로 직접 만든 SPI 통신을 STM32의 &lt;b&gt;하드웨어 SPI 기능&lt;/b&gt;으로 대체하여, 클럭의 균일성과 코드 간결함이 어떻게 달라지는지 비교할 예정이다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>FND</category>
      <category>GPIO</category>
      <category>OJTube임베디드입문</category>
      <category>SPI</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/56</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-21-FND-%EC%A0%9C%EC%96%B4-%EC%86%8C%EC%8A%A4-%EB%B6%84%EC%84%9D#entry56comment</comments>
      <pubDate>Sun, 26 Apr 2026 19:21:29 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 20. SPI통신 제대로 배워보자</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPI 통신이란&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_28_timebase_vs_clock.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdklAW/dJMcabjIyNR/JXF47k8IhJRNeAJxOCM9Q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdklAW/dJMcabjIyNR/JXF47k8IhJRNeAJxOCM9Q0/img.png&quot; data-alt=&quot;diagram_01_timebase_vs_clock&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdklAW/dJMcabjIyNR/JXF47k8IhJRNeAJxOCM9Q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdklAW%2FdJMcabjIyNR%2FJXF47k8IhJRNeAJxOCM9Q0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1500&quot; height=&quot;760&quot; data-filename=&quot;diagram_28_timebase_vs_clock.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_01_timebase_vs_clock&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPI(Serial Peripheral Interface)는 칩과 칩, 모듈과 모듈 간의 &lt;b&gt;근거리 통신&lt;/b&gt;에 사용되는 시리얼 통신 방식이다. I2C와 함께 임베디드 개발에서 가장 많이 사용되며, 플래시 메모리 데이터 기록 등 다양한 분야에서 활용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-16-printf%EB%8F%84-%EC%89%BD%EC%A7%80-%EC%95%8A%EB%8B%A4%EA%B5%AC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;printf도 쉽지 않다구&quot;)&lt;/a&gt;에서 다룬 UART는 시간을 쪼개는 &lt;b&gt;타임베이스 방식&lt;/b&gt;(비동기)이었다. 예를 들어 9600 bps면 1초를 9600개로 나눠서 각 구간의 전압이 High인지 Low인지로 0과 1을 구분했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPI는 타임베이스가 아니라 &lt;b&gt;클럭 선을 이용하는 방식&lt;/b&gt;(동기)이다. 클럭 주기에 맞춰 데이터 선의 High/Low를 읽어 0과 1을 구분한다. 클럭에 제약이 없으므로 &lt;b&gt;이론상 속도 제한이 없다&lt;/b&gt;는 것이 SPI의 큰 장점이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 통신의 기본&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신은 두 개 이상의 장치(A와 B) 사이에서 이루어진다. 데이터를 주고받으려면 &lt;b&gt;전압 차이를 이용한 데이터 선&lt;/b&gt;이 필요하며, High = 1, Low = 0으로 데이터를 표현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPI 통신에는 최소한 &lt;b&gt;데이터 선과 클럭 선&lt;/b&gt;이 필요하다. 클럭이 한 주기 동작할 때(LOW &amp;rarr; HIGH &amp;rarr; LOW) 데이터 선의 전압을 읽어서 해당 비트가 0인지 1인지 판단한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1:1 통신 &amp;mdash; 반이중 vs 전이중&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A와 B 사이에 &lt;b&gt;데이터 선이 1개&lt;/b&gt;면 반이중(Half Duplex) 방식이다. A가 데이터를 보내는 동안 B는 아무것도 할 수 없고, 번갈아가며 통신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 선이 2개&lt;/b&gt;면 전이중(Full Duplex) 방식이다. A가 말하는 전용선과 B가 말하는 전용선이 각각 있어서 &lt;b&gt;동시에 양방향 통신&lt;/b&gt;이 가능하다. SPI는 기본적으로 이 전이중 방식을 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1:다 통신의 두 가지 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A가 여러 장치(B, C, D)와 통신할 때 두 가지 문제가 생긴다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 1: 누가 말하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A가 데이터를 보내면 B, C, D 모두가 듣게 된다. 반대로 B, C, D가 동시에 데이터를 보내면 누가 보낸 건지 구분할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결: Master / Slave 관계 정의&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPI는 Master와 Slave라는 명확한 역할을 정한다. &lt;b&gt;Master는 통신을 시작하고 명령을 내리는 주체&lt;/b&gt;이고, &lt;b&gt;Slave는 말하는 권한이 없고 Master의 명령에 응답만&lt;/b&gt; 한다. Master는 반드시 1개다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 2: 누구에게 말하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Master가 말하는 건 알겠는데, B, C, D 중 &lt;b&gt;누구에게&lt;/b&gt; 말하는 건지 Slave들이 알 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결: CS(Chip Select) 선 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Slave에 독립적으로 CS 선을 연결한다. 평상시에는 CS가 High 상태이고, 특정 Slave와 통신하고 싶으면 &lt;b&gt;해당 CS 선만 Low로 떨어뜨려서&lt;/b&gt; 그 칩을 활성화한다. CS가 Low인 Slave만 Master의 데이터를 수신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1:1 통신일 때는 상대가 하나뿐이므로 CS를 아예 Low로 묶어버려서 Slave가 항상 듣는 상태로 만들 수 있다. 이전 글(&quot;FND 제어&quot;)에서 FND 모듈에 CS 핀이 없었던 이유가 이것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_27_spi_master_slave.png&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbGayG/dJMcacJGSRE/ONTnFJVKTkqT6TkNtKGXAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbGayG/dJMcacJGSRE/ONTnFJVKTkqT6TkNtKGXAk/img.png&quot; data-alt=&quot;diagram_02_spi_master_slave&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbGayG/dJMcacJGSRE/ONTnFJVKTkqT6TkNtKGXAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbGayG%2FdJMcacJGSRE%2FONTnFJVKTkqT6TkNtKGXAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1360&quot; height=&quot;640&quot; data-filename=&quot;diagram_27_spi_master_slave.png&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_02_spi_master_slave&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPI의 기본 뼈대 &amp;mdash; 4개의 선&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;선 이름&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MOSI&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Master Out Slave In&lt;/td&gt;
&lt;td&gt;Master가 말하는 전용선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MISO&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Master In Slave Out&lt;/td&gt;
&lt;td&gt;Slave가 말하는 전용선 (Master가 듣는 선)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SCLK&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Serial Clock&lt;/td&gt;
&lt;td&gt;클럭 신호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SS (CS)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Slave Select (Chip Select)&lt;/td&gt;
&lt;td&gt;통신 대상 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Slave 입장에서는 MOSI를 SI(Slave In), MISO를 SO(Slave Out)라고 표기하기도 한다. 이전 글들에서 접했던 SPI 관련 핀 이름들이 이 구조에서 나온 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-11-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%ED%8A%B8-%EB%B3%B4%EB%8A%94-%EA%BC%BC%EC%88%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;데이터시트 보는 꼼수&quot;)&lt;/a&gt;에서 STB/CLK/DIO로 설명했던 것, &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-13-%EB%A9%80%ED%8B%B0%EB%AF%B8%ED%84%B0%EA%B8%B0-%EC%82%AC%EC%9A%A9%EB%B0%A9%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;멀티미터기 사용방법&quot;)&lt;/a&gt;에서 클럭+데이터 파형을 오실로스코프로 관찰했던 것, &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-19-FND-%EC%A0%9C%EC%96%B4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;FND 제어&quot;)&lt;/a&gt;에서 SCLK/RCLK/DIO로 GPIO를 직접 제어했던 것이 모두 이 SPI 통신의 구현이었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;STM32 SPI 설정 옵션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32는 SPI 통신을 하드웨어적으로 지원한다. STM32CubeIDE에서 SPI를 활성화하면 다양한 옵션을 설정할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1831&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NY25b/dJMcabDZvek/W9AFosBD9QYK4gI1WMxiqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NY25b/dJMcabDZvek/W9AFosBD9QYK4gI1WMxiqK/img.png&quot; data-alt=&quot;STM32CubeIDE SPI 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NY25b/dJMcabDZvek/W9AFosBD9QYK4gI1WMxiqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNY25b%2FdJMcabDZvek%2FW9AFosBD9QYK4gI1WMxiqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1831&quot; height=&quot;1022&quot; data-origin-width=&quot;1831&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;STM32CubeIDE SPI 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mode&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Full Duplex&lt;/b&gt;: 전이중. 데이터 선 2개 + 클럭 = 3선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Half Duplex&lt;/b&gt;: 반이중. 데이터 선 1개 + 클럭 = 2선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Receive Only&lt;/b&gt;: 수신 전용. 데이터 선 1개 + 클럭 = 2선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Transmit Only&lt;/b&gt;: 송신 전용. 데이터 선 1개 + 클럭 = 2선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;칩의 용도에 따라 선택한다. 예를 들어 온도 센서처럼 데이터를 보내기만 하는 장치는 Transmit Only로 충분하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Master / Slave&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;칩의 역할에 따라 선택한다. 메인 MCU는 보통 Master, 주변 모듈은 Slave로 설정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HW NSS Signal&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CS를 하드웨어적으로 자동 제어할지 여부다. HW로 설정하면 SPI 통신 시 자동으로 CS를 Low로 떨어뜨리지만, &lt;b&gt;통신 대상이 1개일 때만 사용 가능&lt;/b&gt;하다. 여러 Slave를 제어해야 하면 GPIO로 직접 CS를 제어해야 하므로 보통 &lt;b&gt;Disable(SW 방식)&lt;/b&gt;로 설정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Data Size&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 전송하는 데이터 단위. &lt;b&gt;8비트 또는 16비트&lt;/b&gt;를 선택할 수 있다. 데이터시트에 따라 결정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;First Bit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 보낼 때 &lt;b&gt;MSB(최상위 비트)부터&lt;/b&gt; 보낼지 &lt;b&gt;LSB(최하위 비트)부터&lt;/b&gt; 보낼지 결정한다. 예를 들어 52(= 0011 0100)를 보낼 때, MSB First면 왼쪽부터, LSB First면 오른쪽부터 보낸다. 데이터시트에 따라 결정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Prescaler (for Baud Rate)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCU의 기본 클럭을 얼마나 나눌지 결정하여 SPI 통신 속도를 설정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Clock Polarity (CPOL)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클럭이 &lt;b&gt;평상시에 Low인지 High인지&lt;/b&gt; 결정한다. 데이터시트를 보고 결정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Clock Phase (CPHA)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클럭의 &lt;b&gt;1st Edge에서 데이터를 읽을지, 2nd Edge에서 읽을지&lt;/b&gt; 결정한다. 한 클럭 내에서 데이터가 0인지 1인지 판단하는 시점을 정하는 설정이다. 데이터시트를 보고 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션들은 모두 &lt;b&gt;데이터시트를 보고 결정&lt;/b&gt;해야 한다. SPI 통신이 지원된다고 해서 모든 칩이 호환되는 것이 아니며, 이 옵션들이 정확히 맞아야 통신이 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPI vs I2C&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;비교 항목&lt;/th&gt;
&lt;th&gt;SPI&lt;/th&gt;
&lt;th&gt;I2C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;선 개수&lt;/td&gt;
&lt;td&gt;4개 (MOSI, MISO, SCLK, CS)&lt;/td&gt;
&lt;td&gt;2개 (SDA, SCL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;통신 방식&lt;/td&gt;
&lt;td&gt;전이중 가능&lt;/td&gt;
&lt;td&gt;반이중만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;칩 선택 방식&lt;/td&gt;
&lt;td&gt;&lt;b&gt;CS 선&lt;/b&gt; (HW적, 각 Slave마다 별도 선)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;주소&lt;/b&gt; (SW적, 데이터에 주소 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;이론상 제한 없음&lt;/td&gt;
&lt;td&gt;프로토콜에 의한 속도 제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선 수 증가&lt;/td&gt;
&lt;td&gt;Slave 수만큼 CS 선 추가&lt;/td&gt;
&lt;td&gt;Slave 수 늘어도 선 2개 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPI는 빠르고 전이중이 가능하지만 선이 많고, I2C는 선이 적지만 느리고 반이중이다. 용도에 따라 선택한다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>I2C</category>
      <category>OJTube임베디드입문</category>
      <category>SPI</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/55</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-20-SPI%ED%86%B5%EC%8B%A0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90#entry55comment</comments>
      <pubDate>Sun, 26 Apr 2026 19:15:44 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 19. FND 제어</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-19-FND-%EC%A0%9C%EC%96%B4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;정보 없는 모듈을 만났을 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 데이터시트나 정보가 부족한 상태에서 모듈을 제어해야 하는 상황이 자주 발생한다. 이번 강의의 핵심은 &lt;b&gt;FND 모듈에 대한 정보가 거의 없을 때, 어떻게 접근하고 문제를 해결하는지&lt;/b&gt;에 대한 실무 노하우다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 접근 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;하드웨어 담당자에게 자료 요청&lt;/b&gt; &amp;mdash; 가장 편한 방법이지만, 자료를 얻기 어려운 경우도 많다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모듈 뒷면의 메인 칩셋 이름 확인&lt;/b&gt; &amp;mdash; 모든 분석의 시작점. 육안으로 안 보이면 핸드폰 카메라로 촬영하여 확대&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메인 칩의 데이터시트 검색&lt;/b&gt; &amp;mdash; 칩 이름(예: TM74HC595)으로 데이터시트를 찾는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;샘플 코드를 구해서 일단 동작시키고, 코드를 분석하며 추리&lt;/b&gt; &amp;mdash; 이것이 실무에서 가장 빠르고 효율적인 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-11-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%ED%8A%B8-%EB%B3%B4%EB%8A%94-%EA%BC%BC%EC%88%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;데이터시트 보는 꼼수&quot;)&lt;/a&gt;에서 다룬 &quot;데이터시트만이 답이 아니다 &amp;mdash; 샘플 코드를 구해서 역추적하는 방법이 가장 빠르다&quot;는 원칙이 여기서 그대로 적용된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FND 모듈 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FND란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FND는 7세그먼트의 다른 이름이다. 7개의 LED와 소수점(dp)까지 총 8개의 LED로 구성되어 있으며, 원하는 LED에 전압을 가하면 해당 부분에 불이 들어와 숫자를 표현한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1TTcG/dJMcag6mWqR/SOUngAonFhwVeID0Z8RMjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1TTcG/dJMcag6mWqR/SOUngAonFhwVeID0Z8RMjK/img.png&quot; data-alt=&quot;7세그먼트 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1TTcG/dJMcag6mWqR/SOUngAonFhwVeID0Z8RMjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1TTcG%2FdJMcag6mWqR%2FSOUngAonFhwVeID0Z8RMjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;552&quot; height=&quot;206&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;7세그먼트 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모듈 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FND 모듈은 &lt;b&gt;메인 칩(드라이버 IC) + 7세그먼트&lt;/b&gt;를 합친 형태다. 모듈 뒷면을 보면 드라이버 칩이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_201425.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eTx6Sl/dJMcaa593gt/jmUvvZfG7yBtsaHjE1gKYK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eTx6Sl/dJMcaa593gt/jmUvvZfG7yBtsaHjE1gKYK/img.jpg&quot; data-alt=&quot;FND 모듈 뒷면 &amp;amp;mdash; 드라이버 칩&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eTx6Sl/dJMcaa593gt/jmUvvZfG7yBtsaHjE1gKYK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeTx6Sl%2FdJMcaa593gt%2FjmUvvZfG7yBtsaHjE1gKYK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_201425.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND 모듈 뒷면 &amp;mdash; 드라이버 칩&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;드라이버 IC가 필요한 이유&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;diagram_26_fnd_driver.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbQ1Kg/dJMcajaUHHW/8McA8uLkMM8YfwLrBX9k51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbQ1Kg/dJMcajaUHHW/8McA8uLkMM8YfwLrBX9k51/img.png&quot; data-alt=&quot;diagram_01_fnd_driver&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbQ1Kg/dJMcajaUHHW/8McA8uLkMM8YfwLrBX9k51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbQ1Kg%2FdJMcajaUHHW%2F8McA8uLkMM8YfwLrBX9k51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;800&quot; data-filename=&quot;diagram_26_fnd_driver.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;diagram_01_fnd_driver&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-11-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%ED%8A%B8-%EB%B3%B4%EB%8A%94-%EA%BC%BC%EC%88%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;데이터시트 보는 꼼수&quot;)&lt;/a&gt;에서 다뤘듯이, 드라이버 IC 없이 7세그먼트를 직접 제어하려면 LED 하나당 GPIO 핀 하나가 필요하여 핀 수가 너무 많아진다. 드라이버 IC를 사용하면 &lt;b&gt;STM32와 드라이버 IC 사이에 3개의 통신선만으로&lt;/b&gt; FND 전체를 제어할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 정리하면: &lt;b&gt;STM32 &amp;harr; (SPI 통신) &amp;harr; 드라이버 IC(TM74HC595) &amp;harr; FND LED들&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPI 통신과 핀 연결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-wire SPI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터시트를 보면 이 모듈은 SPI 통신을 사용한다. 일반적인 SPI는 CLK, DIO, CS 세 가지 선이 필요하지만, 이 모듈은 &lt;b&gt;CS 핀이 없다&lt;/b&gt;. CS는 여러 장치 중 하나를 선택하는 역할인데, 장치가 하나뿐이면 생략할 수 있다. 대신 RCLK이라는 래치 핀이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FND 모듈의 핀 구성:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FND 모듈 핀&lt;/th&gt;
&lt;th&gt;연결&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VCC&lt;/td&gt;
&lt;td&gt;3.3V&lt;/td&gt;
&lt;td&gt;전원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;접지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCLK&lt;/td&gt;
&lt;td&gt;PB15&lt;/td&gt;
&lt;td&gt;클럭 신호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RCLK&lt;/td&gt;
&lt;td&gt;PB13&lt;/td&gt;
&lt;td&gt;래치(데이터 확정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DIO&lt;/td&gt;
&lt;td&gt;PB14&lt;/td&gt;
&lt;td&gt;데이터 입출력&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_202514.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brnV6C/dJMcadPhiSD/n33AA1K60RNqKwbDZZz8F0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brnV6C/dJMcadPhiSD/n33AA1K60RNqKwbDZZz8F0/img.jpg&quot; data-alt=&quot;FND 모듈과 STM 보드 연결&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brnV6C/dJMcadPhiSD/n33AA1K60RNqKwbDZZz8F0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrnV6C%2FdJMcadPhiSD%2Fn33AA1K60RNqKwbDZZz8F0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_202514.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND 모듈과 STM 보드 연결&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핀 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암-암 점퍼선으로 FND 모듈의 핀을 STM 보드의 해당 GPIO 핀에 직접 연결한다. VCC와 GND는 빵판의 전원 레일을 통해 연결한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPIO 설정 및 코드 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPIO 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32는 SPI 통신을 하드웨어적으로 지원하지만, 이번에는 &lt;b&gt;GPIO를 직접 제어하여 클럭과 데이터 신호를 소프트웨어적으로 만들어내는 방식&lt;/b&gt;을 사용한다. PB13, PB14, PB15를 모두 GPIO Output으로 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1023&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NdCes/dJMcacJGSyk/ll5WkUNLYHrc7JhWiN0mEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NdCes/dJMcacJGSyk/ll5WkUNLYHrc7JhWiN0mEK/img.png&quot; data-alt=&quot;PB13/14/15 GPIO Output 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NdCes/dJMcacJGSyk/ll5WkUNLYHrc7JhWiN0mEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNdCes%2FdJMcacJGSyk%2Fll5WkUNLYHrc7JhWiN0mEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1826&quot; height=&quot;1023&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;1023&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PB13/14/15 GPIO Output 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;send 함수 &amp;mdash; 8비트 데이터 전송&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 함수인 &lt;code&gt;send&lt;/code&gt;는 8비트 데이터를 받아 비트 단위로 쪼개어 DIO 핀으로 보낸다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;void send(uint8_t X) {
    for (int i = 8; i &amp;gt;= 1; i--) {
        if (X &amp;amp; 0x80) {
            HAL_GPIO_WritePin(PB14_FND_DIO_GPIO_Port, PB14_FND_DIO_Pin, HIGH);
        } else {
            HAL_GPIO_WritePin(PB14_FND_DIO_GPIO_Port, PB14_FND_DIO_Pin, LOW);
        }
        X &amp;lt;&amp;lt;= 1;

        HAL_GPIO_WritePin(PB15_FND_SCLK_GPIO_Port, PB15_FND_SCLK_Pin, LOW);
        HAL_GPIO_WritePin(PB15_FND_SCLK_GPIO_Port, PB15_FND_SCLK_Pin, HIGH);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 과정을 풀어보면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;X &amp;amp; 0x80&lt;/code&gt; &amp;mdash; 데이터의 최상위 비트(MSB)가 1인지 확인. &lt;code&gt;0x80&lt;/code&gt;은 이진수로 &lt;code&gt;10000000&lt;/code&gt;이므로 MSB만 검사한다&lt;/li&gt;
&lt;li&gt;MSB가 1이면 DIO를 HIGH로, 0이면 LOW로 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X &amp;lt;&amp;lt;= 1&lt;/code&gt; &amp;mdash; 데이터를 왼쪽으로 1비트 시프트하여 다음 비트를 MSB 위치로 올린다&lt;/li&gt;
&lt;li&gt;SCLK을 LOW &amp;rarr; HIGH로 전환하여 &lt;b&gt;클럭 한 주기를 생성&lt;/b&gt;. 이 상승 엣지에서 DIO의 값이 전송된다&lt;/li&gt;
&lt;li&gt;이 과정을 8번 반복하면 8비트 데이터 전송 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;0b10100011&lt;/code&gt;을 보내면: H, L, H, L, L, L, H, H 순서로 DIO가 설정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;for문이 끝나면 SCLK이 HIGH 상태로 남아 있으므로, &lt;b&gt;데이터를 안 보낼 때는 기본적으로 HIGH 상태&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;send_port 함수 &amp;mdash; 데이터 + 자릿수 전송&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;void send_port(uint8_t X, uint8_t port) {
    send(X);      // 세그먼트 데이터 전송
    send(port);   // 어느 자릿수에 표시할지 전송
    HAL_GPIO_WritePin(PB13_FND_RCLK_GPIO_Port, PB13_FND_RCLK_Pin, HIGH);
    HAL_GPIO_WritePin(PB13_FND_RCLK_GPIO_Port, PB13_FND_RCLK_Pin, LOW);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;send&lt;/code&gt;로 세그먼트 데이터와 자릿수(port)를 연속으로 보낸 뒤, RCLK을 HIGH &amp;rarr; LOW로 전환하여 데이터를 확정(래치)한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;숫자-세그먼트 매핑 테이블&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 숫자/문자를 7세그먼트로 표현하기 위한 데이터가 배열로 정의되어 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;uint8_t _LED_0F[29];

void init_fnd() {
    _LED_0F[0] = 0xC0;  // 0
    _LED_0F[1] = 0xF9;  // 1
    _LED_0F[2] = 0xA4;  // 2
    // ...
    _LED_0F[9] = 0x90;  // 9
    _LED_0F[10] = 0x88; // A
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;0xC0&lt;/code&gt;은 이진수로 &lt;code&gt;11000000&lt;/code&gt;이고, 각 비트가 7세그먼트의 &lt;span&gt;a&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;~&lt;/span&gt;&lt;span&gt;g, dp에 대응한다. 0인 비트의 LED가 켜지므로 하위 6비트가 0인 &lt;/span&gt;&lt;span&gt;`0xC0`&lt;/span&gt;&lt;span&gt;은 a&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;~&lt;/span&gt;&lt;span&gt;f 세그먼트가 켜져 숫자 '0'이&lt;/span&gt; 표시된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;digit4_show 함수 &amp;mdash; 4자리 숫자 표시&lt;/h3&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;void digit4_show(int n, int replay, uint8_t showZero) {
    int n1 = (int)  n % 10;
    int n2 = (int) ((n % 100) - n1) / 10;
    int n3 = (int) ((n % 1000) - n2 - n1) / 100;
    int n4 = (int) ((n % 10000) - n3 - n2 - n1) / 1000;

    for (int i = 0; i &amp;lt;= replay; i++) {
        send_port(_LED_0F[n1], 0b0001);  // 1의 자리
        if (showZero | n &amp;gt; 9)   send_port(_LED_0F[n2], 0b0010);  // 10의 자리
        if (showZero | n &amp;gt; 99)  send_port(_LED_0F[n3], 0b0100);  // 100의 자리
        if (showZero | n &amp;gt; 999) send_port(_LED_0F[n4], 0b1000);  // 1000의 자리
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자 n을 각 자릿수로 분리하고, &lt;code&gt;port&lt;/code&gt; 비트(&lt;code&gt;0b0001&lt;/code&gt;, &lt;code&gt;0b0010&lt;/code&gt;, &lt;code&gt;0b0100&lt;/code&gt;, &lt;code&gt;0b1000&lt;/code&gt;)로 어느 자릿수에 표시할지 선택한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;main 함수에서 실행&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c
#include &quot;fnd_controller.h&quot;

// main 함수 내
init_fnd();

while (1) {
    for (int i = 0; i &amp;lt; 9999; i++){
        digit4_replay(i, 50);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행하면 FND에 &lt;b&gt;1부터 9999까지 숫자가 빠르게 증가&lt;/b&gt;하며 표시된다. GPIO를 직접 제어하는 방식임에도 성공적으로 FND를 구동한 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (4).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u9MWm/dJMcajaUHOS/MDWfc3Mfp6KiIjM5g7Pay0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u9MWm/dJMcajaUHOS/MDWfc3Mfp6KiIjM5g7Pay0/img.gif&quot; data-alt=&quot;FND에 숫자가 표시되는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u9MWm/dJMcajaUHOS/MDWfc3Mfp6KiIjM5g7Pay0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/u9MWm/dJMcajaUHOS/MDWfc3Mfp6KiIjM5g7Pay0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (4).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND에 숫자가 표시되는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;digit4_replay(count, 50)&lt;/code&gt;의 &lt;code&gt;count&lt;/code&gt;를 9999까지 올리면 4자리 숫자가 모두 표시된다. &lt;code&gt;send_port(0x88, 0b0001)&lt;/code&gt;을 보내면 'A'가 표시되는 것도 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_211831.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS3YGN/dJMcagkYKfH/ylkNGbXXD7prFTsEnLrbK1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS3YGN/dJMcagkYKfH/ylkNGbXXD7prFTsEnLrbK1/img.jpg&quot; data-alt=&quot;FND에 'A' 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS3YGN/dJMcagkYKfH/ylkNGbXXD7prFTsEnLrbK1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS3YGN%2FdJMcagkYKfH%2FylkNGbXXD7prFTsEnLrbK1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_211831.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FND에 'A' 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의의 핵심은 FND 제어 자체보다 &lt;b&gt;실무에서 정보가 부족할 때의 접근법&lt;/b&gt;이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모듈 뒷면에서 &lt;b&gt;칩 이름 확인&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;데이터시트에서 &lt;b&gt;통신 방식 파악&lt;/b&gt; (이 경우 SPI)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;샘플 코드를 확보하여 일단 동작&lt;/b&gt;시키고&lt;/li&gt;
&lt;li&gt;동작하는 코드를 &lt;b&gt;분석하며 원리를 추리&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 시간에는 이 코드가 어떻게 동작하는지 심층 분석하고, GPIO 방식 대신 STM32의 &lt;b&gt;하드웨어 SPI 통신 기능&lt;/b&gt;을 활용하는 방법도 다룰 예정이다.&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>FND</category>
      <category>GPIO</category>
      <category>OJTube임베디드입문</category>
      <category>SPI</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/54</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-19-FND-%EC%A0%9C%EC%96%B4#entry54comment</comments>
      <pubDate>Sun, 26 Apr 2026 19:00:34 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 18. 내 힘으로 스위치회로 만들기</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-18-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-%EC%8A%A4%EC%9C%84%EC%B9%98%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;스위치 회로 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-17-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-LED%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;내 힘으로 LED 회로 만들어서 제어하기&quot;)&lt;/a&gt;에서는 GPIO 없이 스위치를 물리적으로 눌러 LED를 켜고 껐다. 이번에는 한 단계 더 나아가서, &lt;b&gt;GPIO Input으로 스위치 상태를 코드에서 읽고, 코드로 LED를 제어&lt;/b&gt;하는 회로를 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회로도는 다음과 같다: 3.3V &amp;rarr; 4.7k&amp;Omega; 풀업 저항 &amp;rarr; PB0(GPIO Input) &amp;rarr; 스위치 &amp;rarr; GND&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스위치 회로도.png&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8JyXc/dJMcacXblXI/HGQQzLgBFN8pjykJwXVE6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8JyXc/dJMcacXblXI/HGQQzLgBFN8pjykJwXVE6k/img.png&quot; data-alt=&quot;스위치 회로도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8JyXc/dJMcacXblXI/HGQQzLgBFN8pjykJwXVE6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8JyXc%2FdJMcacXblXI%2FHGQQzLgBFN8pjykJwXVE6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;409&quot; height=&quot;302&quot; data-filename=&quot;스위치 회로도.png&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스위치 회로도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀업 저항의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PB0 핀과 3.3V 사이에 4.7k&amp;Omega; 저항이 연결되어 있다. 이 저항은 &lt;b&gt;스위치가 눌리지 않았을 때 PB0 핀을 확실히 High(3.3V) 상태로 유지&lt;/b&gt;하는 역할을 한다. 풀업 저항이 없으면 스위치가 열린 상태에서 PB0 핀이 아무 곳에도 연결되지 않아 전압이 불안정해진다(플로팅 상태).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빵판 배선&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빨간 수-수 점퍼선으로 &lt;b&gt;+ 레일 &amp;rarr; 빵판 특정 행&lt;/b&gt;(예: a50)에 전원 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4.7k&amp;Omega; 저항&lt;/b&gt;(노랑-보라-빨강)을 같은 행(예: b50)과 다른 행(예: b45)에 연결&lt;/li&gt;
&lt;li&gt;흰색 수-암 점퍼선으로 &lt;b&gt;STM 보드의 PB0&lt;/b&gt; &amp;rarr; 저항 끝과 같은 행(예: a45)에 연결&lt;/li&gt;
&lt;li&gt;노란 수-수 점퍼선으로 같은 행(예: c45) &amp;rarr; &lt;b&gt;스위치의 한쪽 핀&lt;/b&gt;(예: d32)에 연결&lt;/li&gt;
&lt;li&gt;파란 수-수 점퍼선으로 &lt;b&gt;스위치의 다른 쪽 핀&lt;/b&gt;(예: d30) &amp;rarr; &lt;b&gt;- 레일(GND)&lt;/b&gt;에 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 다룬 것처럼, 스위치의 1-3/2-4 핀이 내부적으로 연결되어 있으므로, 빵판의 가운데 홈을 사이에 두고 꽂아야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_192813.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJlWB/dJMcabYiCYd/jfLnWiRagAlKBFHYo9v4dK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJlWB/dJMcabYiCYd/jfLnWiRagAlKBFHYo9v4dK/img.jpg&quot; data-alt=&quot;스위치 회로 완성된 빵판&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJlWB/dJMcabYiCYd/jfLnWiRagAlKBFHYo9v4dK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJlWB%2FdJMcabYiCYd%2FjfLnWiRagAlKBFHYo9v4dK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_192813.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스위치 회로 완성된 빵판&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스위치 동작 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스위치를 누르지 않았을 때&lt;/b&gt;: 스위치가 열려 있으므로 GND로 가는 경로가 끊겨 있다. PB0 핀은 풀업 저항을 통해 3.3V에 연결되어 있으므로 &lt;b&gt;High(1)&lt;/b&gt;로 읽힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스위치를 눌렀을 때&lt;/b&gt;: 스위치가 닫혀 PB0 핀이 GND에 직접 연결된다. 전류가 3.3V &amp;rarr; 저항 &amp;rarr; 스위치 &amp;rarr; GND로 흐르면서 PB0 핀의 전압이 GND 쪽으로 끌려 내려간다. PB0은 &lt;b&gt;Low(0)&lt;/b&gt;로 읽힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;스위치 상태&lt;/th&gt;
&lt;th&gt;PB0 전압&lt;/th&gt;
&lt;th&gt;GPIO ReadPin 값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;누르지 않음&lt;/td&gt;
&lt;td&gt;3.3V (High)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;누름&lt;/td&gt;
&lt;td&gt;0V (Low)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPIO Input 설정 및 코드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IDE 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에서 PB0 핀을 &lt;b&gt;GPIO_Input&lt;/b&gt;으로 설정한다. 풀업/풀다운 설정은 Pull-up으로 한다. 유저 라벨에는 회로도에 표시된 핀 이름을 입력한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1831&quot; data-origin-height=&quot;1019&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceg2Uw/dJMcadIwHja/ixf2kURsC6dmGXigYE40JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceg2Uw/dJMcadIwHja/ixf2kURsC6dmGXigYE40JK/img.png&quot; data-alt=&quot;PB0 GPIO Input 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceg2Uw/dJMcadIwHja/ixf2kURsC6dmGXigYE40JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fceg2Uw%2FdJMcadIwHja%2Fixf2kURsC6dmGXigYE40JK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1831&quot; height=&quot;1019&quot; data-origin-width=&quot;1831&quot; data-origin-height=&quot;1019&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PB0 GPIO Input 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 작성&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 전역 변수 선언
uint8_t button_state = 0;

// while(1) 루프 안
while (1) {
    button_state = HAL_GPIO_ReadPin(PB0_SW_START_GPIO_Port, PB0_SW_START_Pin);
    HAL_Delay(100);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디버깅으로 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HAL_Delay&lt;/code&gt;에 브레이크포인트를 걸고 디버깅한다. 스위치를 &lt;b&gt;누르지 않은 상태&lt;/b&gt;에서 다음 줄로 넘기면 &lt;code&gt;button_state = 1&lt;/code&gt;(High), 스위치를 &lt;b&gt;누른 상태&lt;/b&gt;에서 넘기면 &lt;code&gt;button_state = 0&lt;/code&gt;(Low)이 되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;738&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCNNBN/dJMcaib4msF/9j2YPwKGbstXPFKHIANuJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCNNBN/dJMcaib4msF/9j2YPwKGbstXPFKHIANuJ0/img.png&quot; data-alt=&quot;디버깅 화면 &amp;amp;mdash; button_state 값 변화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCNNBN/dJMcaib4msF/9j2YPwKGbstXPFKHIANuJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCNNBN%2FdJMcaib4msF%2F9j2YPwKGbstXPFKHIANuJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1835&quot; height=&quot;738&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;738&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;디버깅 화면 &amp;mdash; button_state 값 변화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스위치 + LED 통합 제어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 스위치 입력에 따라 LED를 제어한다. 이전 글에서 구성한 &lt;b&gt;LED 회로(3.3V &amp;rarr; LED &amp;rarr; 저항 &amp;rarr; PB6)&lt;/b&gt;를 그대로 두고, 스위치 회로를 추가한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LED 회로 추가 배선&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빨간 수-수 점퍼선으로 &lt;b&gt;+ 레일 &amp;rarr; 빵판 특정 행&lt;/b&gt;(예: f60)에 전원 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LED의 + 다리(긴 쪽)&lt;/b&gt;를 같은 행(예: g60)에, &lt;b&gt;- 다리(짧은 쪽)&lt;/b&gt;를 다른 행(예: g57)에 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;220&amp;Omega; 저항&lt;/b&gt;을 LED - 다리와 같은 행(예: h57)과 다른 행(예: h53)에 연결&lt;/li&gt;
&lt;li&gt;주황 수-암 점퍼선으로 저항 끝(예: i53) &amp;rarr; &lt;b&gt;STM 보드의 PB6&lt;/b&gt;에 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_194732.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgYzQM/dJMcaiC5ZrU/89CBgOKwAiRtVGQKiQoDQK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgYzQM/dJMcaiC5ZrU/89CBgOKwAiRtVGQKiQoDQK/img.jpg&quot; data-alt=&quot;스위치 + LED 통합 회로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgYzQM/dJMcaiC5ZrU/89CBgOKwAiRtVGQKiQoDQK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgYzQM%2FdJMcaiC5ZrU%2F89CBgOKwAiRtVGQKiQoDQK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_194732.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스위치 + LED 통합 회로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 작성&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;while (1) {
    button_state = HAL_GPIO_ReadPin(PB0_SW_START_GPIO_Port, PB0_SW_START_Pin);

    if (!button_state) {  // 버튼 눌림 (Low = 0)
        HAL_GPIO_WritePin(PB6_LED1_GPIO_Port, PB6_LED1_Pin, 0);  // Low &amp;rarr; LED 켜짐
    } else {              // 버튼 안 눌림 (High = 1)
        HAL_GPIO_WritePin(PB6_LED1_GPIO_Port, PB6_LED1_Pin, 1);  // High &amp;rarr; LED 꺼짐
    }

    HAL_Delay(100);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 배운 것처럼, GPIO가 Low일 때 3.3V와 전압차가 생겨 LED가 켜지고, High일 때 전압차가 없어 LED가 꺼진다. &lt;code&gt;!button_state&lt;/code&gt;로 조건을 쓰는 이유는 버튼이 눌리면 ReadPin이 0을 반환하기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행하면 &lt;b&gt;버튼을 누르면 LED가 켜지고, 떼면 꺼진다&lt;/b&gt;. 이전 글에서는 물리적 스위치로 직접 전류를 차단/연결했지만, 이번에는 GPIO Input으로 스위치 상태를 읽고 GPIO Output으로 LED를 제어하는 &lt;b&gt;소프트웨어 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (3).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpOIqf/dJMcaaryHUg/fEFjI13Euhklnn7KNjy1fk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpOIqf/dJMcaaryHUg/fEFjI13Euhklnn7KNjy1fk/img.gif&quot; data-alt=&quot;버튼 눌러 LED 켜진 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpOIqf/dJMcaaryHUg/fEFjI13Euhklnn7KNjy1fk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bpOIqf/dJMcaaryHUg/fEFjI13Euhklnn7KNjy1fk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (3).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;버튼 눌러 LED 켜진 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>GPIO</category>
      <category>OJTube임베디드입문</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/53</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-18-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-%EC%8A%A4%EC%9C%84%EC%B9%98%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry53comment</comments>
      <pubDate>Sun, 26 Apr 2026 18:49:34 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 17. 내 힘으로 LED회로 만들어서 제어하기</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-17-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-LED%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;LED 회로 기초&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 강의에서는 &lt;b&gt;직접 회로를 구성하여 LED를 제어&lt;/b&gt;한다. 남이 만든 회로가 아니라, 원리를 이해하고 직접 설계하여 구현하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 LED 회로는 3.3V 전원 &amp;rarr; LED &amp;rarr; 저항 &amp;rarr; GND로 구성된다. 전류는 항상 높은 전압에서 낮은 전압으로 흐르므로, 3.3V에서 GND 방향으로 전류가 흘러 LED가 켜진다. 저항은 &lt;b&gt;LED에 흐르는 전류량을 조절하여 과전류를 방지&lt;/b&gt;하는 역할을 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LED 극성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LED는 다이오드의 일종으로 전류가 한 방향으로만 흐른다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-10-%ED%9A%8C%EB%A1%9C%EB%8F%84-%EB%94%B1-%ED%95%84%EC%9A%94%ED%95%9C-%EB%A7%8C%ED%81%BC%EB%A7%8C-%EB%B0%B0%EC%9A%B0%EC%9E%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;회로도 딱 필요한 만큼만 배우자&quot;)&lt;/a&gt;에서 다이오드의 기호와 특성을 다뤘는데, 실물에서는 &lt;b&gt;다리가 긴 쪽이 +(애노드), 짧은 쪽이 -(캐소드)&lt;/b&gt;다. 극성을 반대로 연결하면 전류가 흐르지 않아 LED가 켜지지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_172054.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beFeWx/dJMcaaryHGZ/dEtZilcakjm7hoNMZdFgPK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beFeWx/dJMcaaryHGZ/dEtZilcakjm7hoNMZdFgPK/img.jpg&quot; data-alt=&quot;LED 실물 &amp;amp;mdash; 다리 길이로 극성 구분&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beFeWx/dJMcaaryHGZ/dEtZilcakjm7hoNMZdFgPK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeFeWx%2FdJMcaaryHGZ%2FdEtZilcakjm7hoNMZdFgPK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_172054.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LED 실물 &amp;mdash; 다리 길이로 극성 구분&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저항 읽는 법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저항의 띠 색깔로 저항값을 읽을 수 있다. 일반적으로 4개의 띠가 있으며, 첫 번째와 두 번째 띠는 숫자, 세 번째 띠는 승수, 네 번째 띠는 오차율이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: &lt;b&gt;빨강-빨강-갈색-금색&lt;/b&gt; &amp;rarr; 2, 2, &amp;times;10, &amp;plusmn;5% &amp;rarr; &lt;b&gt;220&amp;Omega; &amp;plusmn;5%&lt;/b&gt; (209~231&amp;Omega;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 띠 색깔을 외우는 것보다 &lt;b&gt;멀티미터로 직접 저항값을 측정&lt;/b&gt;하는 것이 정확하고 편리하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_172350.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Sdo5d/dJMcahc8eHB/orJrI66bNzXYFdmjVVJFOk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Sdo5d/dJMcahc8eHB/orJrI66bNzXYFdmjVVJFOk/img.jpg&quot; data-alt=&quot;저항 띠 색깔과 멀티미터 측정 비교&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Sdo5d/dJMcahc8eHB/orJrI66bNzXYFdmjVVJFOk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSdo5d%2FdJMcahc8eHB%2ForJrI66bNzXYFdmjVVJFOk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_172350.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;저항 띠 색깔과 멀티미터 측정 비교&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 1: LED 직접 켜기 (GPIO 없이)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPIO 제어 없이, 3.3V 전원과 GND만으로 LED를 켜본다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빵판 배선&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;STM 보드의 &lt;b&gt;3.3V&lt;/b&gt;에 빨간 점퍼선, &lt;b&gt;GND&lt;/b&gt;에 검정 점퍼선을 연결하여 빵판의 +/- 레일에 공급&lt;/li&gt;
&lt;li&gt;수-수 점퍼선으로 &lt;b&gt;+ 레일 &amp;rarr; 빵판 특정 행&lt;/b&gt;(예: a40)에 전원 연결&lt;/li&gt;
&lt;li&gt;LED의 &lt;b&gt;+ 다리(긴 쪽)&lt;/b&gt;를 같은 행(예: e40)에, &lt;b&gt;- 다리(짧은 쪽)&lt;/b&gt;를 다른 행(예: e37)에 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저항&lt;/b&gt;(220&amp;Omega;)의 한쪽을 LED - 다리와 같은 행(예: c37)에, 다른 쪽을 다른 행(예: c33)에 연결&lt;/li&gt;
&lt;li&gt;수-수 점퍼선으로 저항 끝(예: a33) &amp;rarr; &lt;b&gt;- 레일(GND)&lt;/b&gt;에 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전원을 공급하면 &lt;b&gt;LED에 불이 들어온다&lt;/b&gt;. 3.3V &amp;rarr; LED &amp;rarr; 저항 &amp;rarr; GND로 전류가 흐르는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IdNFA/dJMcabRvM5h/5dax8wjN4UNisHkdX8SIzK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IdNFA/dJMcabRvM5h/5dax8wjN4UNisHkdX8SIzK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot; data-filename=&quot;20260402_173040.jpg&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IdNFA/dJMcabRvM5h/5dax8wjN4UNisHkdX8SIzK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIdNFA%2FdJMcabRvM5h%2F5dax8wjN4UNisHkdX8SIzK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6mbup/dJMcahxr5j1/6LEKliz7wk86oRhkSFf9kK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6mbup/dJMcahxr5j1/6LEKliz7wk86oRhkSFf9kK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot; data-filename=&quot;20260402_173242.jpg&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6mbup/dJMcahxr5j1/6LEKliz7wk86oRhkSFf9kK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6mbup%2FdJMcahxr5j1%2F6LEKliz7wk86oRhkSFf9kK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; LED가 켜진 모습 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 2: GPIO로 LED 제어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원리 &amp;mdash; GPIO High/Low와 LED 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 GND에 연결했던 부분을 &lt;b&gt;GPIO 핀으로 대체&lt;/b&gt;한다. 회로 구성은 3.3V &amp;rarr; LED &amp;rarr; 저항 &amp;rarr; GPIO가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관과 반대로 동작하는 부분이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GPIO Low (0V)&lt;/b&gt;: 3.3V와 0V 사이에 전압차가 있으므로 전류가 흐른다 &amp;rarr; &lt;b&gt;LED 켜짐&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPIO High (3.3V)&lt;/b&gt;: 양쪽 모두 3.3V로 전압차가 없으므로 전류가 흐르지 않는다 &amp;rarr; &lt;b&gt;LED 꺼짐&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핀 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 보드 회로도에서 PB6_LED1이 보드의 7번 핀에 연결되어 있음을 확인한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빵판 배선 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 1에서 GND로 갔던 점퍼선을 제거하고, 대신 &lt;b&gt;STM 보드의 PB6 핀&lt;/b&gt;에서 빵판의 같은 행(예: a33)으로 점퍼선을 연결한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_173919.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbJvOX/dJMcaciBcZn/xNcYXtl0iiCKsFrWPtDIS1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbJvOX/dJMcaciBcZn/xNcYXtl0iiCKsFrWPtDIS1/img.jpg&quot; data-alt=&quot;GPIO로 변경된 최종 회로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbJvOX/dJMcaciBcZn/xNcYXtl0iiCKsFrWPtDIS1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbJvOX%2FdJMcaciBcZn%2FxNcYXtl0iiCKsFrWPtDIS1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_173919.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GPIO로 변경된 최종 회로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPIO 설정 및 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에서 PB6을 GPIO_Output으로 설정하고, 초기 출력 레벨을 High로 설정하여 시작 시 LED가 꺼지도록 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;while (1) {
    HAL_GPIO_WritePin(PB6_LED1_GPIO_Port, PB6_LED1_Pin, 0);  // Low &amp;rarr; LED 켜짐
    HAL_Delay(1000);
    HAL_GPIO_WritePin(PB6_LED1_GPIO_Port, PB6_LED1_Pin, 1);  // High &amp;rarr; LED 꺼짐
    HAL_Delay(1000);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 실행하면 &lt;b&gt;LED가 1초 간격으로 깜빡인다&lt;/b&gt;. 이전 글들에서 코드로만 GPIO를 제어했던 것이, 직접 만든 회로에서 실제로 동작하는 것을 확인하는 순간이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_174549-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/98IQd/dJMcaffmrLJ/LtQtBFN0zxupk6K9KjSSB0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/98IQd/dJMcaffmrLJ/LtQtBFN0zxupk6K9KjSSB0/img.gif&quot; data-alt=&quot;LED가 깜빡이는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/98IQd/dJMcaffmrLJ/LtQtBFN0zxupk6K9KjSSB0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/98IQd/dJMcaffmrLJ/LtQtBFN0zxupk6K9KjSSB0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;20260402_174549-ezgif.com-video-to-gif-converter.gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LED가 깜빡이는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스위치 이해&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내부 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치는 4개의 핀이 있으며, 내부적으로 &lt;b&gt;1-3번 핀끼리, 2-4번 핀끼리 항상 연결&lt;/b&gt;되어 있다. 1-3 쌍과 2-4 쌍 사이는 평소에 끊어져 있다가, &lt;b&gt;버튼을 누르면 연결&lt;/b&gt;된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스위치.png&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vCNaw/dJMcab43d0i/ea51nrkSX8CeGrtLEkzjZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vCNaw/dJMcab43d0i/ea51nrkSX8CeGrtLEkzjZ0/img.png&quot; data-alt=&quot;스위치 내부 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vCNaw/dJMcab43d0i/ea51nrkSX8CeGrtLEkzjZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvCNaw%2FdJMcab43d0i%2Fea51nrkSX8CeGrtLEkzjZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;358&quot; height=&quot;319&quot; data-filename=&quot;스위치.png&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스위치 내부 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멀티미터로 핀 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치의 어느 핀이 1-3이고 어느 핀이 2-4인지 &lt;b&gt;멀티미터 쇼트 테스트로 직접 확인&lt;/b&gt;한다. 두 핀에 프로브를 대고 삐- 소리가 나면 같은 쌍(1-3 또는 2-4)이다. 버튼을 누른 상태에서 다른 쌍과도 삐- 소리가 나는지 확인하면 스위치 동작을 검증할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치를 빵판에 꽂을 때는, 1-3 쌍과 2-4 쌍이 &lt;b&gt;가운데 홈을 사이에 두고 양쪽에 위치&lt;/b&gt;하도록 꽂는다. 같은 쪽(a-e)에 꽂으면 이미 연결된 핀끼리 같은 행에 들어가므로 의미가 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습 3: 스위치로 LED 제어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회로 구성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.3V &amp;rarr; LED &amp;rarr; 저항 &amp;rarr; 스위치 &amp;rarr; GND 순서로 연결한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스위치의 한쪽 쌍(예: 1-3)을 저항과 같은 행에 연결&lt;/li&gt;
&lt;li&gt;스위치의 다른 쪽 쌍(예: 2-4)을 GND에 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20260402_175127.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xcezk/dJMcahqGz3y/zgLktKIuC1e9EOhA9lf6n0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xcezk/dJMcahqGz3y/zgLktKIuC1e9EOhA9lf6n0/img.jpg&quot; data-alt=&quot;스위치가 추가된 최종 회로&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xcezk/dJMcahqGz3y/zgLktKIuC1e9EOhA9lf6n0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXcezk%2FdJMcahqGz3y%2FzgLktKIuC1e9EOhA9lf6n0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot; data-filename=&quot;20260402_175127.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스위치가 추가된 최종 회로&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치를 &lt;b&gt;누르면 LED가 켜지고, 떼면 꺼진다&lt;/b&gt;. 버튼을 누르면 스위치 내부에서 1-3과 2-4 사이가 연결되어 3.3V &amp;rarr; LED &amp;rarr; 저항 &amp;rarr; 스위치 &amp;rarr; GND 경로로 전류가 흐르고, 버튼을 떼면 경로가 끊어져 전류가 차단된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (2).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cE9gvb/dJMcajophfX/nHtXFzbIserHOKI86s1HS1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cE9gvb/dJMcajophfX/nHtXFzbIserHOKI86s1HS1/img.gif&quot; data-alt=&quot;스위치 눌러 LED 켜진 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cE9gvb/dJMcajophfX/nHtXFzbIserHOKI86s1HS1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cE9gvb/dJMcajophfX/nHtXFzbIserHOKI86s1HS1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;ezgif.com-video-to-gif-converter (2).gif&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스위치 눌러 LED 켜진 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>GPIO</category>
      <category>led</category>
      <category>OJTube임베디드입문</category>
      <category>STM32</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/52</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-17-%EB%82%B4-%ED%9E%98%EC%9C%BC%EB%A1%9C-LED%ED%9A%8C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%84%9C-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0#entry52comment</comments>
      <pubDate>Sun, 26 Apr 2026 18:44:33 +0900</pubDate>
    </item>
    <item>
      <title>[오제이 튜브 임베디드 강의] 16. printf도 쉽지 않다구</title>
      <link>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-16-printf%EB%8F%84-%EC%89%BD%EC%A7%80-%EC%95%8A%EB%8B%A4%EA%B5%AC</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;printf()를 임베디드에서 쓰려면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;printf()는 표준 출력(stdout)으로 데이터를 보내는 함수다. PC에서는 콘솔 창이 표준 출력이지만, 임베디드 환경에서는 콘솔 창이 없다. 대신 &lt;b&gt;UART 통신을 통해 PC로 데이터를 보내고, PC의 터미널 프로그램에서 출력을 확인&lt;/b&gt;하는 방식으로 printf()를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표준 출력은 사용자가 원하는 대로 변경할 수 있다. printf() 내부에서 호출되는 &lt;code&gt;_write&lt;/code&gt; 함수를 재정의하면, printf()의 출력이 UART를 통해 전송되도록 만들 수 있다. 이번 글에서는 이 과정을 처음부터 끝까지 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하드웨어 준비&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FTDI 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FTDI 모듈은 &lt;b&gt;UART 신호를 USB 형태로 변환&lt;/b&gt;하는 장치다. 이 모듈의 핵심은 FTDI 칩으로, UART 데이터를 USB 프로토콜로 변환하여 PC가 인식할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;USB 케이블로 PC에 연결하면 빨간 불이 들어오며, 장치 관리자에서 &lt;code&gt;USB 직렬 포트 (COMx)&lt;/code&gt; 형태로 인식된다. COM 포트 번호는 PC마다 다를 수 있으므로 확인이 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CN5uA/dJMcajaUG5k/KwPvbEJgFIY1Js1CoKfae0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CN5uA/dJMcajaUG5k/KwPvbEJgFIY1Js1CoKfae0/img.png&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;556&quot; data-is-animation=&quot;false&quot; style=&quot;width: 57.1742%; margin-right: 10px;&quot; data-widthpercent=&quot;57.85&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CN5uA/dJMcajaUG5k/KwPvbEJgFIY1Js1CoKfae0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCN5uA%2FdJMcajaUG5k%2FKwPvbEJgFIY1Js1CoKfae0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;763&quot; height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TB5z0/dJMcaaE6Iuw/9wKsMYUkXjlNAiLknIHLak/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TB5z0/dJMcaaE6Iuw/9wKsMYUkXjlNAiLknIHLak/img.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot; data-is-animation=&quot;false&quot; data-filename=&quot;20260401_203137.jpg&quot; style=&quot;width: 41.663%;&quot; data-widthpercent=&quot;42.15&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TB5z0/dJMcaaE6Iuw/9wKsMYUkXjlNAiLknIHLak/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTB5z0%2FdJMcaaE6Iuw%2F9wKsMYUkXjlNAiLknIHLak%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; FTDI 모듈 실물 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FTDI 모듈에서 중요한 핀은 세 개다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FTDI 모듈 핀&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;GND&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;송수신 신호의 기준점. 반드시 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;RX&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모듈이 데이터를 받는 선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TX&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모듈이 데이터를 보내는 선&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VCC는 USB를 통해 전원이 공급되므로 반드시 연결할 필요는 없다. 모듈의 스위치를 통해 5V/3.3V를 선택할 수 있는데, 우리 보드는 3.3V로 동작하므로 &lt;b&gt;스위치를 3.3V로 설정&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TX/RX 교차 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UART 통신에서 핵심 규칙: &lt;b&gt;보드의 TX는 모듈의 RX로, 보드의 RX는 모듈의 TX로&lt;/b&gt; 교차 연결해야 한다. 보드가 보내는 데이터(TX)를 모듈이 받아야(RX) 하고, 모듈이 보내는 데이터(TX)를 보드가 받아야(RX) 하기 때문이다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FTDI 모듈&lt;/th&gt;
&lt;th&gt;방향&lt;/th&gt;
&lt;th&gt;STM 보드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RX (수신)&lt;/td&gt;
&lt;td&gt;&amp;larr;&lt;/td&gt;
&lt;td&gt;PA9 (TX, 송신)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TX (송신)&lt;/td&gt;
&lt;td&gt;&amp;rarr;&lt;/td&gt;
&lt;td&gt;PA10 (RX, 수신)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;GND&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;펌웨어 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에서 &lt;b&gt;USART1을 비동기(Asynchronous) 방식으로 선택&lt;/b&gt;하면, PA9(TX)와 PA10(RX) 핀이 자동으로 할당된다. 데이터시트를 일일이 찾을 필요 없이 툴이 핀 할당을 해주는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1829&quot; data-origin-height=&quot;977&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bceLsK/dJMcacQqlXt/G7NSS3LrI19FBwZKYr9jKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bceLsK/dJMcacQqlXt/G7NSS3LrI19FBwZKYr9jKK/img.png&quot; data-alt=&quot;STM32CubeIDE USART1 Async 설정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bceLsK/dJMcacQqlXt/G7NSS3LrI19FBwZKYr9jKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbceLsK%2FdJMcacQqlXt%2FG7NSS3LrI19FBwZKYr9jKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1829&quot; height=&quot;977&quot; data-origin-width=&quot;1829&quot; data-origin-height=&quot;977&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;STM32CubeIDE USART1 Async 설정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UART 설정값은 다음과 같다. 이 값들은 송신 측과 수신 측이 &lt;b&gt;동일하게 맞춰야&lt;/b&gt; 통신이 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Baud Rate&lt;/b&gt;: 115,200 bps&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Word Length&lt;/b&gt;: 8 bit&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Parity&lt;/b&gt;: None&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Stop Bits&lt;/b&gt;: 1&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Data Direction&lt;/b&gt;: Transmit and Receive&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hardware Flow Control&lt;/b&gt;: None&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정의 의미는 아래 &quot;UART 통신 원리&quot; 섹션에서 자세히 설명한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;빵판 배선&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보드와 FTDI 모듈을 빵판을 이용해 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STM 보드 &amp;rarr; 빵판:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GND &amp;rarr; 빵판의 - 라인 (검정색 선)&lt;/li&gt;
&lt;li&gt;PA9(TX) &amp;rarr; 빵판 특정 행 (주황색 선)&lt;/li&gt;
&lt;li&gt;PA10(RX) &amp;rarr; 빵판 다른 행 (흰색 선)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FTDI 모듈 &amp;rarr; 빵판 (같은 행에 연결):&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GND &amp;rarr; 빵판의 - 라인, 보드 GND와 같은 라인 (검정색 선)&lt;/li&gt;
&lt;li&gt;RX &amp;rarr; PA9(TX)와 같은 행 (주황색 선)&lt;/li&gt;
&lt;li&gt;TX &amp;rarr; PA10(RX)과 같은 행 (흰색 선)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빵판의 같은 행은 내부적으로 연결되어 있으므로, 같은 행에 꽂으면 전기적으로 연결된다. &lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-09-%EC%A7%80%EA%B8%88%EA%B9%8C%EC%A7%80-%EB%B0%B0%EC%9A%B4-%EA%B2%83%EC%9D%84-%ED%81%B0-%EA%B7%B8%EB%A6%BC%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;큰 그림에 저장하기&quot;)&lt;/a&gt;에서 빵판의 내부 연결 구조를 다뤘다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZlxeQ/dJMcacQqlYm/qi71sK9NS91TZHIZ3VEjP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZlxeQ/dJMcacQqlYm/qi71sK9NS91TZHIZ3VEjP1/img.png&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;387&quot; data-is-animation=&quot;false&quot; style=&quot;width: 56.1475%; margin-right: 10px;&quot; data-widthpercent=&quot;56.81&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZlxeQ/dJMcacQqlYm/qi71sK9NS91TZHIZ3VEjP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZlxeQ%2FdJMcacQqlYm%2Fqi71sK9NS91TZHIZ3VEjP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;509&quot; height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KoZQL/dJMcacQqlYj/Rmp7RlfKjWBw7a37RNoyuK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KoZQL/dJMcacQqlYj/Rmp7RlfKjWBw7a37RNoyuK/img.jpg&quot; data-origin-width=&quot;2992&quot; data-origin-height=&quot;2992&quot; data-is-animation=&quot;false&quot; data-filename=&quot;20260401_205919.jpg&quot; data-widthpercent=&quot;43.19&quot; style=&quot;width: 42.6897%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KoZQL/dJMcacQqlYj/Rmp7RlfKjWBw7a37RNoyuK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKoZQL%2FdJMcacQqlYj%2FRmp7RlfKjWBw7a37RNoyuK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2992&quot; height=&quot;2992&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; STM 보드와 FTDI 모듈 빵판 연결 최종 모습 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UART 통신 원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 vs 동기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-11-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%ED%8A%B8-%EB%B3%B4%EB%8A%94-%EA%BC%BC%EC%88%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 글(&quot;데이터시트 보는 꼼수&quot;)&lt;/a&gt;에서 다룬 SPI 통신은 &lt;b&gt;동기(Synchronous) 방식&lt;/b&gt;이다. 클럭 선이 있어서 클럭 신호를 기준으로 데이터를 해석한다. 클럭의 패턴이 약간 바뀌어도 기준점이 있으므로 통신이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 UART는 &lt;b&gt;비동기(Asynchronous) 방식&lt;/b&gt;이다. 클럭 선이 없고, 대신 송수신 측이 &lt;b&gt;시간을 동일하게 쪼개는 약속&lt;/b&gt;을 해서 데이터를 주고받는다. 이 약속이 바로 Baud Rate와 Word Length 등의 설정이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 C언어에서 배우는 동기/비동기와 혼동하기 쉽다. 소프트웨어에서의 동기/비동기는 &quot;함수 호출 후 결과를 기다리느냐(동기) 기다리지 않느냐(비동기)&quot;, 즉 &lt;b&gt;실행 흐름을 맞추느냐&lt;/b&gt;의 문제다. 통신에서의 동기/비동기는 &quot;송신 측과 수신 측이 공유하는 클럭 선이 있느냐(동기) 없느냐(비동기)&quot;, 즉 &lt;b&gt;클럭(시간 기준)을 맞추느냐&lt;/b&gt;의 문제다. 둘 다 &quot;synchronize(맞추다)&quot;라는 어원에서 나왔지만, 맞추는 대상이 다르다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Baud Rate (보드 레이트)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1초를 얼마나 잘게 쪼개서 비트를 전송할 것인지 결정하는 속도 단위다. &lt;b&gt;115,200 bps는 1초를 115,200개로 쪼개어 비트를 전송&lt;/b&gt;한다는 뜻이다. 9,600 bps는 디버깅에 사용하기엔 너무 느려 병목이 발생할 수 있으므로, 최소 115,200 bps를 사용하는 것이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Word Length (워드 랭스)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 몇 비트 단위로 데이터를 보낼지에 대한 약속이다. &lt;b&gt;8비트로 설정하면 Start 비트 이후 8비트를 전송하고 Stop 비트가 오는 형식&lt;/b&gt;이다. 받는 쪽에서 다른 비트 수로 설정하면 데이터가 깨진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parity (패리티)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리얼 통신은 노이즈의 영향을 받기 쉬우므로, 데이터 오류를 검출하기 위한 비트다. 현업에서는 None으로 설정하는 경우가 많다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Stop Bits (스톱 비트)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 전송이 끝났음을 알리는 비트다. 1비트로 설정하면 &quot;끝났다&quot;를 1비트로 보낸다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 UART 송신 테스트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Xshell 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리얼 통신 프로그램(Xshell 등)을 설치하고, 새 세션을 만든다. 프로토콜을 SERIAL로 변경하고, 장치 관리자에서 확인한 &lt;b&gt;COM 포트 번호와 펌웨어에서 설정한 Baud Rate(115200), 데이터 비트(8), 패리티(None), 스톱 비트(1)를 동일하게&lt;/b&gt; 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DVaYU/dJMcab43dDV/xNXYR3B3fG1BQI7o0K1NXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DVaYU/dJMcab43dDV/xNXYR3B3fG1BQI7o0K1NXk/img.png&quot; data-alt=&quot;Xshell 시리얼 설정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DVaYU/dJMcab43dDV/xNXYR3B3fG1BQI7o0K1NXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDVaYU%2FdJMcab43dDV%2FxNXYR3B3fG1BQI7o0K1NXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;990&quot; height=&quot;858&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;858&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Xshell 시리얼 설정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HAL_UART_Transmit으로 Hello World&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c &amp;mdash; while(1) 루프 안
uint8_t msg[] = &quot;Hello World\r\n&quot;;
HAL_UART_Transmit(&amp;amp;huart1, msg, sizeof(msg) - 1, HAL_MAX_DELAY);
HAL_Delay(1000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 빌드하고 보드에 업로드하면, Xshell 터미널에 &lt;b&gt;Hello World가 1초마다 출력&lt;/b&gt;되는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1127&quot; data-origin-height=&quot;1084&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cauTzw/dJMcafTVZk8/1jeQTszvvXmCzCWzWnOAKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cauTzw/dJMcafTVZk8/1jeQTszvvXmCzCWzWnOAKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cauTzw/dJMcafTVZk8/1jeQTszvvXmCzCWzWnOAKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcauTzw%2FdJMcafTVZk8%2F1jeQTszvvXmCzCWzWnOAKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1127&quot; height=&quot;1084&quot; data-origin-width=&quot;1127&quot; data-origin-height=&quot;1084&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 데이터 흐름 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 구성을 정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;STM 보드의 PA9(TX)에서 &quot;Hello World&quot; 데이터를 UART로 전송&lt;/li&gt;
&lt;li&gt;FTDI 모듈이 UART 신호를 받아 USB 형태로 변환&lt;/li&gt;
&lt;li&gt;PC는 USB 드라이버를 통해 이 신호를 COM 포트 데이터로 해석&lt;/li&gt;
&lt;li&gt;Xshell이 COM 포트를 열어 수신된 데이터를 화면에 출력&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FTDI 모듈의 핵심 역할은 UART &amp;harr; USB 변환이고, PC 측의 드라이버가 USB 신호를 해석할 수 있게 해준다. 리눅스 환경에서 FTDI 드라이버를 포팅하여 사용하는 경우도 많으므로 이 키워드를 기억해두면 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;printf() 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;_write 함수 재정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;printf()를 호출하면 내부적으로 &lt;code&gt;_write&lt;/code&gt; 함수가 최종적으로 호출된다. 이 함수를 &lt;code&gt;HAL_UART_Transmit&lt;/code&gt;을 호출하도록 재정의하면, printf()의 출력이 UART를 통해 전송된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// main.c 또는 별도 파일
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&amp;amp;huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    return len;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;printf(&quot;Hello %d\r\n&quot;, 42);&lt;/code&gt; 같은 코드를 쓰면 Xshell에 &quot;Hello 42&quot;가 출력된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;1082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mlhs0/dJMcai37BkG/EhrUZih8YlgKJfILLDf4FK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mlhs0/dJMcai37BkG/EhrUZih8YlgKJfILLDf4FK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mlhs0/dJMcai37BkG/EhrUZih8YlgKJfILLDf4FK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmlhs0%2FdJMcai37BkG%2FEhrUZih8YlgKJfILLDf4FK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1124&quot; height=&quot;1082&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;1082&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실수 출력 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;printf()로 실수(&lt;code&gt;%f&lt;/code&gt;)를 출력하려면 컴파일러 옵션을 추가해야 한다. STM32CubeIDE에서 &lt;code&gt;C/C++ Build &amp;rarr; Settings &amp;rarr; MCU GCC Linker &amp;rarr; Miscellaneous&lt;/code&gt;로 이동하여 linker flags에 다음을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;--specs=nano.specs -u _printf_float&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정을 적용하면 &lt;code&gt;printf(&quot;temp: %.2f\r\n&quot;, 36.5);&lt;/code&gt; 같은 실수 출력이 가능해진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQWDnZ/dJMcagyvl44/FqBOvRMe4LKFV3Allx4HRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQWDnZ/dJMcagyvl44/FqBOvRMe4LKFV3Allx4HRK/img.png&quot; data-origin-width=&quot;1147&quot; data-origin-height=&quot;1186&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.9666%; margin-right: 10px;&quot; data-widthpercent=&quot;48.53&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQWDnZ/dJMcagyvl44/FqBOvRMe4LKFV3Allx4HRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQWDnZ%2FdJMcagyvl44%2FFqBOvRMe4LKFV3Allx4HRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1147&quot; height=&quot;1186&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ej2gOO/dJMcaf0E8HC/JJ1O1tGDDuEEvm91ng3gXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ej2gOO/dJMcaf0E8HC/JJ1O1tGDDuEEvm91ng3gXk/img.png&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;935&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.8706%;&quot; data-widthpercent=&quot;51.47&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ej2gOO/dJMcaf0E8HC/JJ1O1tGDDuEEvm91ng3gXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fej2gOO%2FdJMcaf0E8HC%2FJJ1O1tGDDuEEvm91ng3gXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;959&quot; height=&quot;935&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt; STM32CubeIDE linker flags 설정 &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Live Expression &amp;mdash; 실시간 변수 모니터링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STM32CubeIDE에는 &lt;b&gt;Live Expression&lt;/b&gt;이라는 실시간 변수 모니터링 기능이 있다. 디버깅 중에 변수 값이 실시간으로 변하는 것을 IDE 화면에서 바로 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, Live Expression으로 모니터링하려면 &lt;b&gt;해당 변수를 전역 변수로 선언&lt;/b&gt;해야 한다. 지역 변수는 스택에 할당되어 함수가 끝나면 사라지므로 실시간 모니터링에 제약이 있다. 전역 변수는 컴파일 시점에 메모리 주소가 확정되므로 안정적으로 모니터링할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmXjcM/dJMcadaFyzq/hEN9JdiNAwk4ltrLlkgR7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmXjcM/dJMcadaFyzq/hEN9JdiNAwk4ltrLlkgR7k/img.png&quot; data-alt=&quot;Live Expression 실시간 모니터링&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmXjcM/dJMcadaFyzq/hEN9JdiNAwk4ltrLlkgR7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmXjcM%2FdJMcadaFyzq%2FhEN9JdiNAwk4ltrLlkgR7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1835&quot; height=&quot;707&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;707&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Live Expression 실시간 모니터링&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Embedded</category>
      <category>hal</category>
      <category>OJTube임베디드입문</category>
      <category>printf</category>
      <category>STM32</category>
      <category>UART</category>
      <author>onebrotravel</author>
      <guid isPermaLink="true">https://onebrotravel.tistory.com/51</guid>
      <comments>https://onebrotravel.tistory.com/entry/%EC%98%A4%EC%A0%9C%EC%9D%B4-%ED%8A%9C%EB%B8%8C-%EC%9E%84%EB%B2%A0%EB%94%94%EB%93%9C-%EA%B0%95%EC%9D%98-16-printf%EB%8F%84-%EC%89%BD%EC%A7%80-%EC%95%8A%EB%8B%A4%EA%B5%AC#entry51comment</comments>
      <pubDate>Sun, 26 Apr 2026 18:30:20 +0900</pubDate>
    </item>
  </channel>
</rss>