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를 활용해 모바일에서도 보기 편한 대형 그래프를 구현했습니다.
주요 기능:
- 순서 고정: 센서 ID 스캔 후 코드에 고정하여, 센서 위치가 바뀌어도 데이터가 뒤섞이지 않게 설정.
- 모바일 최적화: 한 줄에 4개 카드가 나열되는 반응형 디자인.
- 자동 스케일: 0.1도의 미세한 변화도 잘 보이도록 Y축 범위를 실시간 최적화.
4. 자주 묻는 질문 (Troubleshooting)
- Q: 센서 4개가 같은 곳인데 왜 값이 다르죠?
- A: 모든 센서는 제조 공정상 미세한 오차가 있습니다. 표준 온도계와 비교하여 소프트웨어적으로 오프셋(Offset) 값을 가감해 보정하는 과정이 필요합니다.
- Q: 차트가 안 그려져요!
- A: 센서 ID 중복이나 스캔 오류일 확률이 높습니다.
ds.scan()을 통해 정확한 16진수 주소를 먼저 확인하세요.
- A: 센서 ID 중복이나 스캔 오류일 확률이 높습니다.
5. 마치며
DS18B20은 부화기 온도 조절, 안방/거실 환경 모니터링 등 활용도가 무궁무진합니다. 이번에 구축한 실시간 웹 대시보드 코드를 활용해 여러분만의 스마트 홈 시스템을 완성해 보세요!
📊 DS18B20 핵심 기술 사양 (Specification)
| 항목 | 상세 스펙 | 비고 |
|---|---|---|
| 측정 범위 | -55°C ~ +125°C | 광범위한 환경 대응 가능 |
| 정밀도 (Accuracy) | ±0.5°C | -10°C ~ +85°C 구간 기준 |
| 분해능 (Resolution) | 9비트 ~ 12비트 (선택 가능) | 기본 12비트 설정 권장 |
| 최소 측정 단위 | 0.0625°C | 12비트 설정 시 해상도 |
| 변환 시간 | 최대 750ms | 12비트 기준 (데이터 처리 속도) |
| 동작 전압 | 3.0V ~ 5.5V | ESP32(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. 안정성을 높이는 배선 꿀팁 (중요!)
네트워크를 구성할 때 모양이 매우 중요합니다.
- 데이지 체인 (Daisy Chain) 추천: 메인 선 하나를 길게 빼고, 거기서 센서를 하나씩 징검다리처럼 연결하는 방식이 가장 안정적입니다.
- 스타(Star) 구조 지양: 한 지점에서 사방으로 선을 뻗는 방식은 신호 반사(Reflection) 현상 때문에 에러가 자주 발생합니다. 가급적 피하세요.
- 랜선(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()



micro2iot에 답글 남기기 응답 취소