DS18B20으로 구축하는 정밀 다중 온도 모니터링 시스템 : 1-Wire 디지털 통신


DIY 프로젝트에서 가장 사랑받는 온도 센서인 DS18B20을 활용해, 4개의 포인트를 동시에 측정하고 실시간 그래프로 시각화하는 시스템 구축 과정을 총정리해 드립니다.

1. 왜 DS18B20인가? (핵심 특징)

DS18B20은 일반적인 아날로그 센서와 달리 1-Wire 디지털 통신을 사용합니다.

  • 고유 ID: 각 센서마다 전 세계에 단 하나뿐인 64비트 시리얼 번호가 내장되어 있어, 선 하나(병렬)에 수십 개의 센서를 연결해도 각각 구분할 수 있습니다.
  • 정밀도: ±0.5°C의 오차 범위를 가지며, 9~12비트 분해능 설정이 가능합니다.
  • 간편한 배선: VCC, GND, DATA 단 3개의 선만 있으면 됩니다.

2. 하드웨어 연결 가이드 (배선도 확인)

센서의 평평한 면을 바라볼 때 왼쪽부터 GND – DATA – VCC 순서입니다.

  • 병렬 연결: 4개의 센서 DATA 핀을 모두 ESP32의 한 개 핀(예: GPIO 0)에 묶어서 연결합니다.
  • 풀업 저항 필수: 데이터 안정성을 위해 VCC와 DATA 사이에 4.7kΩ 저항을 반드시 달아주어야 합니다. (10kΩ 사용 시 통신 불안정이 생길 수 있으니 10k 두 개를 병렬로 엮어 5k로 쓰는 것을 추천합니다.)

3. MicroPython 실시간 대시보드 구현

이번 프로젝트의 핵심은 **웹소켓(WebSocket)**을 이용한 실시간 데이터 전송입니다. Chart.js를 활용해 모바일에서도 보기 편한 대형 그래프를 구현했습니다.

주요 기능:

  1. 순서 고정: 센서 ID 스캔 후 코드에 고정하여, 센서 위치가 바뀌어도 데이터가 뒤섞이지 않게 설정.
  2. 모바일 최적화: 한 줄에 4개 카드가 나열되는 반응형 디자인.
  3. 자동 스케일: 0.1도의 미세한 변화도 잘 보이도록 Y축 범위를 실시간 최적화.

4. 자주 묻는 질문 (Troubleshooting)

  • Q: 센서 4개가 같은 곳인데 왜 값이 다르죠?
    • A: 모든 센서는 제조 공정상 미세한 오차가 있습니다. 표준 온도계와 비교하여 소프트웨어적으로 오프셋(Offset) 값을 가감해 보정하는 과정이 필요합니다.
  • Q: 차트가 안 그려져요!
    • A: 센서 ID 중복이나 스캔 오류일 확률이 높습니다. ds.scan()을 통해 정확한 16진수 주소를 먼저 확인하세요.

5. 마치며

DS18B20은 부화기 온도 조절, 안방/거실 환경 모니터링 등 활용도가 무궁무진합니다. 이번에 구축한 실시간 웹 대시보드 코드를 활용해 여러분만의 스마트 홈 시스템을 완성해 보세요!


📊 DS18B20 핵심 기술 사양 (Specification)

항목상세 스펙비고
측정 범위-55°C ~ +125°C광범위한 환경 대응 가능
정밀도 (Accuracy)±0.5°C-10°C ~ +85°C 구간 기준
분해능 (Resolution)9비트 ~ 12비트 (선택 가능)기본 12비트 설정 권장
최소 측정 단위0.0625°C12비트 설정 시 해상도
변환 시간최대 750ms12비트 기준 (데이터 처리 속도)
동작 전압3.0V ~ 5.5VESP32(3.3V)와 완벽 호환
통신 방식1-Wire (Single Bus)선 하나로 다중 연결 가능
고유 식별자64-bit ROM Code공장 출하 시 부여된 고유 ID

질문: 센서 몇개까지 거리는 얼마까지 병렬 연결 할 수 있나?

DS18B20의 가장 큰 장점이 **’줄줄이 사탕’**처럼 엮을 수 있다는 점이지만, 무한정 늘릴 수는 없습니다. 통신 방식(1-Wire)의 특성상 센서 개수거리는 서로 밀접하게 연결되어 있습니다.


1. 연결 가능한 센서 개수 (최대 몇 개?)

이론과 실제 사용 환경에는 차이가 있습니다.

  • 이론적 한계: 고유 ID가 64비트이므로 거의 무한대에 가깝게 주소를 할당할 수 있습니다.
  • 실제 권장 한계: 일반적인 마이크로컨트롤러(ESP32 등) 환경에서는 10~20개 정도가 안정적입니다.
    • 이유: 센서가 늘어날수록 데이터 선의 전기적 부하(Capacitance)가 커져서 신호가 뭉개집니다.
    • 해결: 센서가 20개 이상으로 많아지면 풀업 저항 값을 2.2kΩ 정도로 낮추어 신호를 강하게 잡아줘야 합니다.

2. 연결 가능한 거리 (얼마나 멀리?)

선이 길어질수록 저항과 노이즈가 발생하여 통신 속도가 떨어집니다.

  • 일반적인 케이블 (전화선, 점퍼선):5~10m까지는 무난합니다.
  • 고급 케이블 (CAT5/6 랜선): 배선 방식만 최적화하면 50~100m까지도 가능합니다.
  • 주의: 거리가 10m를 넘어가면 전압 강하가 일어나므로, 전원 공급 방식을 ‘기생 전원(Parasite Power)’이 아닌 **전용 3선 방식(VCC 별도 연결)**으로 사용해야 합니다.

3. 안정성을 높이는 배선 꿀팁 (중요!)

네트워크를 구성할 때 모양이 매우 중요합니다.

  1. 데이지 체인 (Daisy Chain) 추천: 메인 선 하나를 길게 빼고, 거기서 센서를 하나씩 징검다리처럼 연결하는 방식이 가장 안정적입니다.
  2. 스타(Star) 구조 지양: 한 지점에서 사방으로 선을 뻗는 방식은 신호 반사(Reflection) 현상 때문에 에러가 자주 발생합니다. 가급적 피하세요.
  3. 랜선(UTP) 활용: 거리가 멀다면 랜선의 꼬임 쌍선(Twisted Pair) 중 하나는 DATA, 하나는 GND로 묶어서 사용하면 노이즈 차단 효과가 탁월합니다.

💡 요약

  • 센서 4~5개, 거리 5m 이내: 지금처럼 4.7kΩ 저항 하나로 충분합니다.
  • 센서 20개 이상, 거리 20m 이상: 저항을 2.2kΩ으로 낮추고, 가급적 랜선을 사용해 데이지 체인 방식으로 연결하세요.

Python ds18b20_ex.py

import machine, time, _thread, socket, json, uhashlib, ubinascii, gc, onewire, ds18x20

# --- 1. 하드웨어 설정 ---
DAT_PIN = 0
ds_pin = machine.Pin(DAT_PIN)
ds_sensor = ds18x20.DS18X20(onewire.OneWire(ds_pin))

SENSOR_FIXED_ORDER = [
    "28616434d401baad",
    "286164342b97a494",
    "28616434d4039a1f",
    "28616434ca19d64f"
]

ws_clients = []

# --- 2. UI 수정 (4열 일렬 배치 및 폰트 최적화) ---
html_page = """
<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mana's IoT</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
    :root { --bg: #0d1117; --card: #161b22; --accent: #58a6ff; --font: #c9d1d9; --temp: #ff7b72; }
    body { background: var(--bg); color: var(--font); font-family: sans-serif; margin: 0; padding: 5px; overflow-x: hidden; }
    
    .header { padding: 8px 5px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #30363d; margin-bottom: 8px; }
    h2 { margin: 0; font-size: 1rem; color: #fff; }
    #status { font-size: 0.65rem; }

    /* 4개를 무조건 한 줄로 나열 */
    .container { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin-bottom: 8px; }
    
    .card { background: var(--card); border: 1px solid #30363d; border-radius: 6px; padding: 8px 4px; text-align: center; position: relative; }
    .card::before { content:''; position:absolute; left:0; top:0; width:2px; height:100%; background: var(--accent); }
    
    /* 좁은 공간을 위한 폰트 크기 조정 */
    .id-label { font-size: 0.45rem; color: #8b949e; font-family: monospace; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 2px; }
    .temp-val { font-size: 1.3rem; font-weight: 800; color: var(--temp); line-height: 1; margin: 4px 0; }
    .room-name { font-size: 0.65rem; color: #fff; font-weight: bold; }

    .chart-wrapper { background: var(--card); border: 1px solid #30363d; border-radius: 8px; padding: 5px; height: 180px; }
</style></head>
<body>
    <div class="header">
        <h2>🌡️ 4-CH Monitor</h2>
        <div id="status">연결 중...</div>
    </div>
    <div id="monitor" class="container"></div>
    <div class="chart-wrapper"><canvas id="tempChart"></canvas></div>

    <script>
        let ws, chart;
        const colors = ['#58a6ff', '#3fb950', '#d29922', '#f85149'];

        function initChart() {
            const ctx = document.getElementById('tempChart').getContext('2d');
            chart = new Chart(ctx, {
                type: 'line',
                data: { labels: [], datasets: [] },
                options: {
                    responsive: true, maintainAspectRatio: false,
                    scales: {
                        x: { display: false },
                        y: { beginAtZero: false, grid: { color: '#222' }, ticks: { color: '#8b949e', font: { size: 9 } } }
                    },
                    plugins: { legend: { display: true, labels: { color: '#c9d1d9', boxWidth: 6, font: { size: 9 } } } },
                    animation: false, elements: { point: { radius: 0 }, line: { borderWidth: 2 } }
                }
            });
        }

        function connect() {
            ws = new WebSocket('ws://' + window.location.host);
            ws.onopen = () => { document.getElementById('status').innerHTML = "<span style='color:#3fb950'>● LIVE</span>"; };
            ws.onmessage = (e) => {
                const data = JSON.parse(e.data);
                let html = "";
                data.forEach((s, i) => {
                    html += `<div class="card">
                                <span class="id-label">${s.id.slice(-6)}</span>
                                <div class="temp-val">${s.temp.toFixed(1)}°</div>
                                <div class="room-name">S${i+1}</div>
                             </div>`;
                    if (chart) {
                        if (!chart.data.datasets[i]) {
                            chart.data.datasets[i] = { label: 'S'+(i+1), data: [], borderColor: colors[i], fill: false, tension: 0.3 };
                        }
                        chart.data.datasets[i].data.push(s.temp);
                        if (chart.data.datasets[i].data.length > 50) chart.data.datasets[i].data.shift();
                    }
                });
                document.getElementById('monitor').innerHTML = html;
                if (chart) {
                    chart.data.labels.push("");
                    if (chart.data.labels.length > 50) chart.data.labels.shift();
                    chart.update();
                }
            };
            ws.onclose = () => { setTimeout(connect, 2000); };
        }
        initChart(); connect();
    </script>
</body></html>
"""

# --- (이하 통신 및 메인 루프 로직은 이전과 동일하므로 생략) ---
# ... (broadcast_ws, web_server, main_loop 함수는 이전 코드를 그대로 사용하세요)

# --- 3. 통신 및 서버 로직 ---
def broadcast_ws(msg_obj):
    try:
        msg = json.dumps(msg_obj).encode()
        length = len(msg)
        header = bytearray([0x81, length]) if length <= 125 else bytearray([0x81, 126, (length >> 8) & 0xFF, length & 0xFF])
        frame = header + msg
        for cl in ws_clients[:]:
            try: cl.send(frame)
            except: 
                if cl in ws_clients: ws_clients.remove(cl)
                cl.close()
    except: pass

def web_server():
    s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('', 80)); s.listen(5)
    while True:
        gc.collect()
        cl = None
        try:
            cl, addr = s.accept()
            req = cl.recv(1024).decode()
            if "Upgrade: websocket" in req:
                key = [l.split(': ')[1] for l in req.split('\r\n') if "Sec-WebSocket-Key:" in l][0]
                accept = ubinascii.b2a_base64(uhashlib.sha1(key.encode() + b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest()).decode().strip()
                cl.send("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + accept + "\r\n\r\n")
                ws_clients.append(cl)
            else:
                cl.send("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" + html_page)
                cl.close()
        except:
            if cl: cl.close()

# --- 4. 메인 루프 (온도 읽기 및 전송) ---
def main_loop():
    while True:
        try:
            all_roms = ds_sensor.scan()
            if all_roms:
                ds_sensor.convert_temp()
                time.sleep_ms(750)
                ordered_data = []
                for target_id in SENSOR_FIXED_ORDER:
                    found = False
                    for rom in all_roms:
                        rom_id = ''.join(['%02x' % b for b in rom])
                        if rom_id == target_id:
                            ordered_data.append({"id": rom_id, "temp": ds_sensor.read_temp(rom)})
                            found = True; break
                    if not found: ordered_data.append({"id": target_id, "temp": 0.0})
                if ws_clients: broadcast_ws(ordered_data)
        except: pass
        time.sleep(2); gc.collect()

# 실행
_thread.start_new_thread(web_server, ())
main_loop()

코멘트

“DS18B20으로 구축하는 정밀 다중 온도 모니터링 시스템 : 1-Wire 디지털 통신” 에 하나의 답글

  1. micro2iot 아바타

    [구매정보] 블로그에서 사용한 부품 구매정보입니다. 부품 상세 사양 및 기술 정보 참조용으로 활용하세요.
    아래 링크를 통해 구매 시 소정의 수수료를 제공받으며, 채널 운영에 큰 도움이 됩니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다