본문 바로가기

전자공학을 즐겁게/누구나 취미전자공학

CircuitPython으로 만드는 LED 주사위 놀이

728x90
반응형

오래전에 전자공작에 대한 책이나 시판된 키트에 LED로 만들어진 주사위 놀이가 있었습니다. 7개의 LED로 주사위 눈을 표시하던 것이었죠. 문득 생각이 나서 한 번 만들어 보기로 했습니다. 물론 그때에는 콘덴서에 충전을 해서 콘덴서가 모두 방전이 되면 발진을 멈추는 발진기와 카운터 IC를 사용한 것이었지만, 저는 이것을 MCU에 CircuitPython을 이용해서 구현해 보기로 했습니다. CircuitPython에 대해서는 이전에 다룬 적이 있습니다. 컴퓨터가 발달하니 무엇인가 만들고자 할 때 하드웨어가 간단해지죠. 
 
LED로 주사위의 각 숫자를 나타낼 때에는 아래 그림처럼 디코딩이 됩니다. 프로그램을 사용해서 이렇게 표시하도록 하면 되겠죠.
 

LED 7개를 사용해서 어느 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 Socket

 
자세한 설명은 생략하고, 이 소켓을 꾸미게 될 회로와 연결하는 데에 사용하게 됩니다. 소켓의 각 핀에 실크로 이름이 쓰여 있는데, 꼭 이 용도로만 사용되는 것이 아닙니다. MicroBUS 규격에는 이렇게 되어 있지만, 실제로는 MCU의 IO로 그냥 연결되어 있어서 원하는 대로 GPIO로 사용하면 됩니다. 
 
표로 소켓의 각 핀이 MCU의 어느 GPIO에 연결되어 있는지 보여 주고 있습니다.
 

MicroBUS 소켓의 핀아웃

 
첫 번째 컬럼에 보드에 실크로 쓰여 있는 신호 이름이 나와 있습니다. 세 번째 컬럼은 이 신호들이 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

GitHub - sangyoungn/dice_game

Contribute to sangyoungn/dice_game development by creating an account on GitHub.

github.com

 

맺으면서

옛날에 만들었던 공작이 생각이 나서 한 번 만들어 보았습니다. 과거에는 하드웨어에 전적으로 의존한 공작이나 개발이었지만, 오늘날은 하드웨어와 소프트웨어의 결합으로 더 유연한 개발이 가능해졌습니다. 취미공작에서도 하드웨어만으로 만들거나 소프트웨어로만 만드는 것이 아니게 되었네요. 좀 더 재미있게 되었다고 할까요. 요즘 코딩이라고 하면 대부분 PC나 라즈베리파이 같은 고사양 컴퓨터만을 생각하는데요, 코딩을 고급 컴퓨터에서 할 수도 있지만, 이처럼 MCU로 하드웨어를 만들면서 하면 더 재미있습니다.

728x90
반응형