오래전에 전자공작에 대한 책이나 시판된 키트에 LED로 만들어진 주사위 놀이가 있었습니다. 7개의 LED로 주사위 눈을 표시하던 것이었죠. 문득 생각이 나서 한 번 만들어 보기로 했습니다. 물론 그때에는 콘덴서에 충전을 해서 콘덴서가 모두 방전이 되면 발진을 멈추는 발진기와 카운터 IC를 사용한 것이었지만, 저는 이것을 MCU에 CircuitPython을 이용해서 구현해 보기로 했습니다. CircuitPython에 대해서는 이전에 다룬 적이 있습니다. 컴퓨터가 발달하니 무엇인가 만들고자 할 때 하드웨어가 간단해지죠.
LED로 주사위의 각 숫자를 나타낼 때에는 아래 그림처럼 디코딩이 됩니다. 프로그램을 사용해서 이렇게 표시하도록 하면 되겠죠.
회로를 만들고, 표시하고자 하는 주사위 눈에 따라서 위의 그림처럼 LED를 켜게 만들면 되죠. 프로그램을 작성하는 것에 따라서 더 재미있게 만들어 볼 수도 있을 것입니다. 시작해 볼까요?
https://www.youtube.com/watch?v=rBiiQ7TvJ64
하드웨어의 준비
xG24 Explorer Kit
CircuitPython Board로 xG24 Explorer Kit을 사용할 것입니다. xG24 Explorer Kit에서 CircuitPython을 사용하는 방법은 예전 포스트를 참조하세요.
xG25 Explorer Kit의 User Guide를 보면 보드에 있는 소켓에 대한 설명이 있습니다.
자세한 설명은 생략하고, 이 소켓을 꾸미게 될 회로와 연결하는 데에 사용하게 됩니다. 소켓의 각 핀에 실크로 이름이 쓰여 있는데, 꼭 이 용도로만 사용되는 것이 아닙니다. MicroBUS 규격에는 이렇게 되어 있지만, 실제로는 MCU의 IO로 그냥 연결되어 있어서 원하는 대로 GPIO로 사용하면 됩니다.
표로 소켓의 각 핀이 MCU의 어느 GPIO에 연결되어 있는지 보여 주고 있습니다.
첫 번째 컬럼에 보드에 실크로 쓰여 있는 신호 이름이 나와 있습니다. 세 번째 컬럼은 이 신호들이 MCU의 어떤 GPIO와 연결이 되어 있는지 보여 줍니다. 그냥 GPIO로만 사용할 것이기 때문에 이것만 중요합니다.
회로도 작성
만드는 회로와 연결될 수 있도록 회로를 그려 보았습니다. 손으로 그리면 지저분해서 KiCad로 그렸습니다.
J1과 J2는 MicroBUS 소켓과 연결하기 위한 헤더들입니다. MicroBUS 소켓과 같이 2.54mm 피치의 헤더를 사용할 것입니다. 핀에 연결된 네트이름은 GPIO의 이름을 그대로 사용했습니다.
LED 7개 D1~D7을 각각 저항에 직렬로 연결하여 GND로 연결하였고, LED의 애노드(anode)에 GPIO 하나씩을 연결하였습니다. LED와 연결된 저항 R2~R8은 LED에 흐르는 전류를 제한하기 위한 것으로 값을 대략적으로 선택하는 방법은 저의 예전 포스트에도 나와 있습니다.
풀업 저항(Pull-Up Resistor)/풀다운 저항(Pull-Down Resistor)
SW1은 입력용으로 사용할 텐데, 역시 GPIO로 연결을 합니다. 스위치가 닫히면 GND로 연결되어 GPIO에 0V가 가해지게 됩니다. 스위치가 열리면 HIGH가 되어야 할 텐데, 이때 중요한 역할을 하는 것이 R1입니다. R1이 없다면 스위치가 열렸을 때, 전압이 결정이 되지 않습니다. 이때 결정되지 않는 전압을 결정하기 위하여 저항 통해서 전원 쪽으로 연결하여, GPIO 입력이 전원 레벨, 여기에서는 3.3V가 되도록 합니다. 스위치가 열리면 GPIO 입력은 아무것도 연결되어 있지 않기 때문에 전류가 흐르지 않게 됩니다. 저항에 전류가 흐르지 않는다면, 저항 양쪽의 전압은 0V가 되어 양쪽 끝의 전압이 같아지게 됩니다. 그래서 GPIO의 입력이 3.3V가 되는 것입니다. 전원 쪽으로 연결한 저항이라고 하여 R1을 풀업 저항(Pull-up Resistor)라고 부릅니다. 스위치가 열리면 풀업 저항에 흐르는 전류가 없으니, 저항이 소모하는 전력은 이상적으로 0W 입니다. 하지만, 스위치가 닫히면 GND와 연결이 되기 때문에 옴의 법칙에 의하여 전원으로부터 GND로 전류가 흐르게 됩니다. 이 때는 그냥 아무 일도 안 하고 에너지만 쓰는 저항이죠. 그래서 흐르는 전류를 최소화하기 위하여 큰 저항이 선호됩니다. 그렇지만, 무작정 크게 하면 안 되는 것이, 다시 스위치가 열리면 HIGH로 돌아가는 시간을 고려해야 합니다. 회로에 보이지 않는 커패시턴스(capacitance) 성분이 있기 때문이지요. 이 시간이 많이 길어도 상관없다면, 스위치가 닫혔을 때 흐르는 전류가 무시될 수 있을 만큼의 큰 저항값을 사용하게 되고, 다시 HIGH로 돌아가는 시간이 정해져 있다면 이 시간을 만족시킬 수 있는 저항값을 사용하게 됩니다. 이때 고려하는 것이 저항과 커패시턴스의 시정수(time constant)가 되는 것이죠.
여기에서는 전원 쪽으로 연결하였는데, 반대로 스위치가 닫혔을 때 3.3V가 되고, 열렸을 때 0V가 되도록 하고자 한다면, 저항을 GND 쪽으로 연결을 합니다. 풀업 저항과 반대의 원리로 생각할 수 있으며, 이 경우에는 아래쪽으로 당겼으므로, 풀다운 저항(Pull-Down Resistor)라고 부릅니다.
여기에서는 스위치의 경우를 예를 들었지만, 일반적으로 말한다면 High Impedance 상태의 Net의 상태를 정하기 위한 역할을 하는 것이 풀업(pull-up) 또는 풀다운(pull-down) 저항입니다.
부품의 배치와 배선의 구상
회로가 간단하기 때문에 만능기판에 조립을 할 수 있습니다.
사실은 다 조립을 하고 그리긴 했지만, 미리 어떻게 부품을 배치하고 배선할지를 그려 보는 것이 만능기판에 공작할 때 도움이 많이 됩니다.
미리 그려 보고 했다면, 더 깔끔하게 배선이 가능하도록 GPIO도 서로 바꾸어서 선이 서로 꼬이지 않도록 배선을 하도록 생각했겠지만, 이미 만들고 그리다 보니 선이 좀 꼬여 있는 것이 보입니다. 만능기판에 공작할 때는 주의해야 하는 것이 선이 지나가다가 닿으면 안 되는 곳이 단락(short)이 일어나는 것을 주의해야 합니다. 다음에 조립된 사진을 보면 일부 배선이 피복이 있는 상태로 되어 있는 것이 보일 텐데, 반대편의 배선과 단락 되지 않도록 하기 위한 것입니다.
조립
만능기판에 부품을 배치하고, 브레드보드 배선에 사용하던 0.5mm 단선을 피복을 벗겨 배선에 사용했습니다.
배선은 가급적 짧고 간결하게 처리하여야 합니다. 경험이 많지 않은 경우, 만능기판에 공작하다 보면 피복 전선이 길게 막 날아다니는 경우가 있는데, 많이 하다 보면 배선이 서로 겹치지 않게 기판에 붙어 배선이 가능해질 거예요.
CircuitPython Board와 연결
CircuitPython Board와 연결이 서로 연결을 하여야 합니다. 이로써 하드웨어 준비는 끝이 납니다.
소프트웨어
CircuitPython으로 하는 것이니 당연히 Python이겠죠. ^^
필요 모듈의 import
import board
import digitalio
import random
import time
가장 중요한 것은 board 모듈과 digitalio 모듈입니다. CircuitPython Board의 IO를 제어하기 위한 모듈이죠. random 모듈은 난수 발생을 위해서, time 모듈은 지연을 위한 sleep 매써드 때문에 사용했습니다.
LED와 SWITCH
LED 하나하나를 위한 LED class입니다. digitalio port를 사용해서 초기화하고, on과 off 매써드를 만들었습니다.
class LED:
'''
Class LED
Abtracts LED control over digital output port
'''
def __init__(self, port):
self.port = digitalio.DigitalInOut(port)
self.port.direction = digitalio.Direction.OUTPUT
def on(self):
self.port.value = True
def off(self):
self.port.value = False
SWITCH 동작을 위한 class입니다. LED와 똑같이 digitalio port로 초기화하였고, GPIO의 값을 있는 그대로 return 하는 value 매써드를 구현했습니다.
class SWITCH:
'''
Class SWITCH
Abstracts switch input over digital input port
'''
def __init__(self, port):
self.port = digitalio.DigitalInOut(port)
self.port.direction = digitalio.Direction.INPUT
def value(self):
return self.port.value
주사위를 추상화한 DICE
DICE는 여러 개의 LED를 가진 class입니다. 어떤 숫자를 표현하고자 하는지를 처리하는 매써드가 필요하죠.
class DICE:
'''
Class DICE
Represents each dice number with 7 LEDs
'''
def __init__(self, leds: list):
'''
Requires list comprised with 7 LEDs
[D1, D2, D3, D4, D5, D6, D6]
'''
self.led_list = leds
def decode(self, num):
'''
Coverts to the LED representation from a number
'''
decode = [ [False, False, False, True, False, False, False], \
[True, False, False, False, False, False, True], \
[True, False, False, True, False, False, True], \
[True, False, True, False, True, False, True], \
[True, False, True, True, True, False, True], \
[True, True, True, False, True, True, True] ]
led_idx = 0
for led in self.led_list:
# Look up the state of each LED for a given number
if decode[num-1][led_idx] == True:
led.on()
else:
led.off()
led_idx += 1
def all_off(self):
'''
Method to turn off all LEDs
'''
for led in self.led_list:
led.off()
주사위 놀이 클래스
주사위가 굴러가는 것을 추상화한 클래스입니다. 난수로 주사위 숫자를 표현하고 멈추었을 때 나타난 눈이 최종값이 되는 것을 구현했습니다.
class Dice_Game:
'''
Class Dice_Game
Abstracts the rolling dice
'''
def __init__(self, dice:DICE):
'''
Requires a DICE class
'''
self.dice = dice
def roll(self):
# Random number determining how many time the dice will be rolling
iter = random.randint(30, 50)
previous_number = 0 # Need to remember previously generated random number
for i in range(iter):
random_number = random.randint(1,6) # Generate a random number between 1 and 6
while random_number == previous_number or random_number + previous_number == 7:
# Cannot be same as previous generated number
# A real dice roll shouldn't show the numbers on opposite sides in sequence.
random_number = random.randint(1,6)
self.dice.decode(random_number)
# The delay between two consecutive numbers will increase exponentially.
delay = 0.1 * ( 0.5 / 0.1 ) ** ( i / iter )
# Added a random jitter
delay = delay + random.uniform(0, 0.05)
time.sleep(delay)
previous_number = random_number # Store the number for reference in the next iteration.
time.sleep(0.5)
# Show the final number 3 times.
for i in range(3):
self.dice.all_off()
time.sleep(0.5)
self.dice.decode(random_number)
time.sleep(0.5)
self.dice.decode(random_number)
게임의 main 함수
main 함수를 따로 만든 것은 다른 파일에서 import 해서 호출하기 용이하게 하기 위하여 그렇게 했습니다.
def dice_game_main():
# List of LEDs
leds = [ LED(board.PB4), LED(board.PC1), LED(board.PC2), LED(board.PD5), LED(board.PB5), LED(board.PD4), LED(board.PC3) ]
# Switch instance
switch = SWITCH(board.PC0)
# Create a DICE instance with the list of LEDs
dice = DICE(leds)
# Create a Dice_Game instance with dice
dice_game = Dice_Game(dice)
while True:
# Infinite Loop
while switch.value():
# Wait until the switch is pressed
pass
demo_number = 1
while not switch.value():
# Wait until the switch is released
# While waiting, repeatedly show the dice numbers in turn.
dice.decode(demo_number)
demo_number += 1
if demo_number > 6:
demo_number = 1
time.sleep(0.5)
dice_game.roll()
code.py
CircuitPython Board에서 가장 먼저 자동으로 실행되는 파일이 code.py인데, code.py에서 dice_game_main() 함수를 호출하도록 했습니다.
import dice_game
dice_game.dice_game_main()
Repository
위에 설명된 모든 하드웨어 설계와 코드는 아래의 Repository에 있습니다.
https://github.com/sangyoungn/dice_game
맺으면서
옛날에 만들었던 공작이 생각이 나서 한 번 만들어 보았습니다. 과거에는 하드웨어에 전적으로 의존한 공작이나 개발이었지만, 오늘날은 하드웨어와 소프트웨어의 결합으로 더 유연한 개발이 가능해졌습니다. 취미공작에서도 하드웨어만으로 만들거나 소프트웨어로만 만드는 것이 아니게 되었네요. 좀 더 재미있게 되었다고 할까요. 요즘 코딩이라고 하면 대부분 PC나 라즈베리파이 같은 고사양 컴퓨터만을 생각하는데요, 코딩을 고급 컴퓨터에서 할 수도 있지만, 이처럼 MCU로 하드웨어를 만들면서 하면 더 재미있습니다.
'전자공학을 즐겁게 > 누구나 취미전자공학' 카테고리의 다른 글
CircuitPython으로 만드는 LED 룰렛 (6) | 2024.10.24 |
---|---|
CircuitPython 시작하기 (feat. xG24 Explorer Kit) (0) | 2024.07.05 |
원격으로 라즈베리 파이 코드 편집하기 - Visual Studio Code 활용 (2) | 2024.06.02 |
Python을 이용한 UART 활용 - pyserial과 xmodem (0) | 2024.05.09 |
라즈베리 파이의 Device Tree Overlay와 UART 하드웨어 흐름제어 사용하기 (0) | 2024.04.21 |