본문 바로가기

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

여러 개의 7-Segment LED 표시하기

728x90
반응형

7-Segment LED에 숫자를 표시하는 법에 대해서 이야기했었습니다. 한 자리를 표시하는 방법, 한 자리를 표시하기 위한 디코딩에 관한 이야기였죠. 그런데, 보통 이런 7-Segment LED는 한 자리만 표시하는 것보다는 여러 자리를 표시하게 되죠. 전자계산기 같은 것 말이죠. 그게 뭐 어려운가? 그냥 여러 개 나란히 놓고 각각 한 자리씩 출력하면 되지. 맞습니다. 두 자리면 아마 다음 회로처럼 되겠죠. 

2개의 7-Segment LED

이렇게 생각하는 것이 가장 쉽습니다. A1, B1, ..., G1, DP1은 첫 번째 자리, A2, B2, ..., G2, DP2는 두 번째 자리를 표시하도록 연결하는 것입니다. 2개의 한 자리 7-Segment LED를 나란히 놓은 것입니다. 

3자리를 표시하려면? 하나 또 나란히 놓고 A3, B3, ..., G3, DP3 연결하면 됩니다. 하나의 7-Segment LED를 표시하기 위하여 각각 8개의 GPIO가 사용되는 셈이죠. 

이런 식으로라면 $N$자리를 표시하는데 $\mathbf{8{\times}N}$개의 GPIO가 사용이 됩니다. 

그래서 저는 이런 식으로 하지 않으려고요. 너무 많은 GPIO가 사용이 되니까요. 그러면 어떻게 할 수 있을까요?

 

GND로만 연결되고 있는 공통 단자를 이용할 수 있습니다. 이 단자도 사용하라고 있는 것이에요. 다음처럼 그려 볼까요?

A...G, DP는 공통으로 사용. Common 단자를 제어하는 회로.

결선은 다음 그림처럼 했습니다. 

결선도이지만 잘 안 보일 것입니다. 반드시 회로도를 참조해서 연결하세요.

단자가 두 자리에 같이 연결되면 똑같은 숫자를 표시하지 않나요? 글쎄요? 우리는 공통 단자도 제어할 수 있다는 것을 생각하세요. 이 단자는 항상 GND(애노드 공통인 경우에는 VCC)로만 열결 되어야 하는 것은 아닙니다. 공통 단자를 GND(애노드 공통인 경우 VCC)에 연결하고, A...G, DP를 HIGH를 만들어서 LED는 켜는 것 아닌가요? 아니오. 꼭 그렇게 해야 한다고 고정관녕을 갖지 마세요. GND와 HIGH 입력이 필요한 것이 아니라, LED를 켜기 위해서 그 LED를 정방향 바이어스를 만들어야 하는 것입니다. 

 

다음에 있는 그림처럼 신호를 만들어 볼까요?

CC0와 CC1을 번갈아서 HIGH/LOW를 바꾸어 줍니다.

CC0와 CC1은 위의 회로도에서 각각의 7-Segment LED에 연결되어 있는 공통 단자입니다. 여기에 두 7-Segment LED는 캐소드 공통(Common-Cathode)이기 때문에 공통 단자가 전압이 낮은 쪽, 또는 음(-)극이 되겠지요. [A..G, DP]는 LED의 애노드(anode) 쪽에 연결되어 있는 다른 신호들을 모두 표시한 것입니다, 각각이 어떤 상태인지는 여기에서는 중요하지 않아요. 캐소드 공통인 경우에는 CC0이든 CC1이든 LOW가 되면 해당 7-Segment의 개별 LED들을 정방향 바이어스로 만들 수 있습니다. 반대로 이 캐소드 공통 7-Segment LED의 공통 단자를 HIGH로 만들면 개별 LED들은 절대 정방향 바이어스 될 수 없습니다. 애노드가 LOW든 HIGH든 캐소드의 HIGH보다 높을 수가 없으니까요. 정방향 바이어스 되지 않으면 절대 켜지지 않습니다. 그래서 DIGIT1과 DIGIT3는 첫 번째 자리에, DIGIT2와 DIGIT4는 두 번째 자리에 표시하고자 하는 의도입니다.

 

실험을 한 번 해 보도록 하죠. 7-Segment LED를 표시하던 코드를 살짝 고쳐서 해 보겠습니다.

https://github.com/sangyoungn/play_with_RPi/tree/main/seven_seg_led

 

GitHub - sangyoungn/play_with_RPi: Example Codes for Raspberry Pi

Example Codes for Raspberry Pi. Contribute to sangyoungn/play_with_RPi development by creating an account on GitHub.

github.com

이 코드에서 나머지 부분은 모두 그대로 두고, main 부분만 고쳐서 실험해 보겠습니다. 

 

이 부분을

# Below is a code to show a demo of calss SEVEN_SEG_LED if this file is executed.
if __name__=='__main__':
    import time # Import time module for the delay.

    # Initialize a SEVEN_SEG_LED instance. 
    # A: GPIO 3, B: GPIO 2, C: GPIO 15, D: GPIO 23, E: GPIO 24, F: GPIO 17, G:GPIO 27, DP: GPIO 14
    a = SEVEN_SEG_LED((3, 2, 15, 23, 24, 17, 27, 14))

    # while(True):
    toggle = False
    for num in range(0, 10): # Will display 0 to 9.
        a.update(num, dot=toggle) # Update the 7-segment LED with a given number.
        time.sleep(1) # Delay for a second.
        toggle = True if toggle==False else False # Toggle the variable to toggle DP.

    a.update(None) # Turn off all segments.

 

이렇게 바꿔서 해 봅시다. 위에 제시한 파형처럼 파형을 만드는 것이죠. 

# Below is a code to show a demo of calss SEVEN_SEG_LED if this file is executed.
if __name__=='__main__':
    import time # Import time module for the delay.

    # Initialize a SEVEN_SEG_LED instance. 
    # A: GPIO 3, B: GPIO 2, C: GPIO 15, D: GPIO 23, E: GPIO 24, F: GPIO 17, G:GPIO 27, DP: GPIO 14
    a = SEVEN_SEG_LED((3, 2, 15, 23, 24, 17, 27, 14))

    GPIO.setup(9, GPIO.OUT)
    GPIO.setup(10, GPIO.OUT)

    GPIO.output(9, GPIO.HIGH)
    GPIO.output(10, GPIO.LOW)

    ON_DURATION = 0.01
    for num in range(0, 10): # Will display 0 to 9.
        start = time.time()
        while(True):
            a.update(None)
            GPIO.output(9, GPIO.LOW)
            a.update(num)
            time.sleep(ON_DURATION)
            GPIO.output(9, GPIO.HIGH)
            a.update(None)
            GPIO.output(10, GPIO.LOW)
            a.update((num+1)%10)
            time.sleep(ON_DURATION)
            GPIO.output(10, GPIO.HIGH)
            if (time.time()-start) > 1.: break           

    a.update(None) # Turn off all segments.

그냥 위에 설명한 파형을 무작정 만들어 보는 거예요. 공통 단자도 이제는 제어해야 하니까 7-Segment LED의 개수만큼 2개의 GPIO가 OUT으로 더 할당되어야 합니다.

 

그대로 했다면 이런 식으로 보일 것입니다.

빠르게 번갈아 켜면 이렇게 보입니다.

그냥 번갈아 켜는 거잖아요? 맞습니다. 번갈아 켜는 것이지요.

코드에서 이 부분을 조정해 볼까요?

    ON_DURATION = 0.01

이 값은 한 자리를 표시하고 유지하는 시간인데요, 단위는 초(second)입니다. CC0와 CC1 신호의 폭에 해당합니다. 한 자리를 0.01초 동안 유지하는 것이니까 두 자리를 표시하는 경우에는 0.02초가 소요되고, 그 이후에는 다시 처음 자리로 돌아옵니다. 이 값을 더 크게, 예를 들면 0.1초 정도로 바꿔 보면 어떻게 되는지 보세요. 아마 깜박깜박하는 것이 느껴질 것입니다. 0.5 정도로 바꾸면 아예 한 자리씩만 보일 거예요. 번갈아 가면서 켜지는 것이죠. 값이 작아질수록 두 자리가 동시에 켜 있는 것과 같이 느껴질 것입니다. 그렇다고 너무 작은 값을 사용하진 마세요. LED가 켜질 시간을 주어야죠.

분명히 번갈아 가면서 한 번에 한 자리만 켜지지만, 사람이 느끼지 못할 만큼 빠르게 그 동작을 하는 것입니다. 과거에 영사기로 영화를 보여 주던 시절에 1초에 서로 다른 사진 12장을 보여 주는 것과 같다고 할까요. 사람이 알아차리지 못할 만큼 빠르게 한다면, 그냥  LED가 살짝 어둡다고 느끼게 되겠지요? 꺼지는 것이 아니라 그냥 살짝 어둡다고 느낄 것입니다. 공통 단자를 GND로 연결하고 제어를 하는 것과 비교해서 LED는 $\frac{1}{공통 단자 개수}$만큼 켜 있는 시간이 줄어듭니다. 그만큼 어두워지는 것입니다. 그래서 전류 제한 저항을 좀 더 작은 값으로, 다시 말하면 LED에 흐르는 전류를 좀 더 높게 조정을 하는 편이 나을 것 같습니다.

이처럼 두 자리를 Segment를 통해서 표시하는 경우, 전체 16개의 Segment-- 8개씩 2자리 --가 존재하고, 2개의 공통단자가 존재합니다. 이런 구조를 통상 $2COM{\times}8seg$라고 표시를 합니다. 전체가 $16seg$라는 것이 되죠. 이렇게 $N$자리를 표시하는 데에는 $\mathbf{8+N}$개의 GPIO만 필요로 합니다. 대신 $8{\times}N$개의 GPIO를 사용하는 것보다는 살짝 어두워 보이는 트레이드오프(trade-off)를 감안해야 하는 것이지요. 사용하는 IO의 수를 줄일 수 있는 것이 큰 이점입니다.

 

내친김에 여러 자리의 7-Segment LED를 표시하는 코드를 만들어 보았습니다.

다음 예처럼 동작하도록 말이죠.

leds = MULTI_SEVEN_SEG_LED((3, 2, 15, 23, 24, 17, 27, 14), (9,10))
leds.display_start()
leds.update(12)
'''
Do whatever
'''
leds.display_stop()

MULTI_SEVEN_SEG_LED라는 클래스(class)를 만들면서 기본적으로 2개의 tuple을 전달하도록 했습니다. 앞의 tuple은 (A, B, C, D, E, F, G, DP)를 제어하는 GPIO의 번호, 뒤의 tuple은 공통 단자를 나열하는 tuple로, 다시 말해서 (CC0, CC1)이 됩니다. 추가적으로 7-Segment LED가 캐소드 공통인지 애노드 공통인지를 나타내는 옵션을 추가했습니다. 

    def __init__(self, pins: tuple, common_pins: tuple, isCC=True):     # Initializer
        '''
        Initilazer

        pins: A tuple containing GPIO numbers to control each LED in a seven segment LED
        common_pins: A tuple containing GPIO numbers connected to each common pin
        isCC: True if the configuration is common-cathode, False if common-anode.
        '''
        # Output pins are same for all digits. 
        # So a single SEVEN_SEG_LED instance is engough.
        self.isCC = isCC
        self.digits = SEVEN_SEG_LED(pins, isCC=self.isCC) 

        # Instead, we have the same number of common pins as the number of digits.
        self.common_pins = common_pins
        for pin in self.common_pins:
            GPIO.setup(pin, GPIO.OUT)
            if (isCC): # To disable the output, the default output is to be HIGH for Common Cathode.
                GPIO.output(pin, GPIO.HIGH)
            else: # Else the default output is to be LOW for Common Anode.
                GPIO.output(pin, GPIO.LOW)

        # Following instance variables are for Thread operation.
        self.display_task = None
        self.keepRunning = True

        # This is an instance varialble to store the value to be displayed.
        self.display_value = "" # For a value to be displayed
        # Below is a list indicating whether of not to turn on DPs of each digit.
        self.DPs = list([False]*len(self.common_pins))

이제 표시만 하면 되는데, 문제가 한 가지 있습니다. 다른 작업이 되면서 표시를 하여야 하는데, 현재 값을 계속 표시를 하려면 공통단자의 상태도 계속 바꾸면서 번갈아 표시를 계속해야 한다는 것이죠. 한 자리만 표시할 때처럼 GPIO 상태만 바꿔 놓고 다른 작업 하고 와서 GPIO 상태 바꾸고 하는 방법이 안 되고, 표시하는 부분은 계속 루프를 돌고 있어야 한다는 문제가 있습니다. 다른 작업이 일어나는 동안에 계속 같이 호출되도록 할 수 있겠지만, 다른 작업이 얼마나 걸릴지 모르니 일정한 간격으로 제어를 못한다는 문제가 발생하지요. 그래서 display_start()라는 메서드(method)를 준비하였습니다.

# Refer to https://docs.python.org/3/library/threading.html
from threading import Thread

'''

'''
	def display_start(self):    # Start display
        # Initialize Thread instance with the target of _display method.
        self.display_task = Thread(target=self._display)
        # Start Thread operation.
        self.display_task.start()

별 것이 없는데, Thread라는 클래스가 눈에 띕니다. 스레드(thread)라고 하는 것은 일반적으로 말해서 프로그램이 수행되는 하나의 시퀀스(sequence)인데, 보통은 경우에는 하나의 프로그램은 하나의 스레드가 되겠지만, 이것은 또 하나의 스레드가 실행되도록 한다는 것입니다. 스레드(thread)에 대한 좀 더 깊은 내용은 다른 프로그램 관련 글이나 도서를 참조하세요. 여기에서는 _display()라는 메서드를 또 다른 스레드로 실행시킨다는 의미입니다. 

 

_display() 메서드는 조건을 만족하지 않으면 계속 수행하는 루프입니다. 중지시키지 않으면 계속 수행하는 무한루프에 가깝지요.

    def _display(self):     # Task loop for display
        DELAY = 0.005
        while(self.keepRunning): # This value will become False in display_stop() method.
            value = self.display_value # Copy display_value to a temporary variable
            if(len(self.common_pins) > len(value)):
                # The value to be displayed is shorter than available digits.
                # Pad (a) space(s) in front.
                value = ' '*(len(self.common_pins)-len(value)) + value
            else:
                # Longer or same
                # We will display the last digits.
                value = value[-len(self.common_pins):]

            index = 0
            for pin in self.common_pins: # Repeat as many as available digits.
                # Update the digit at index
                self.digits.update(value[index], \
                                dot = True if self.DPs[index] else False)
                
                # Now make the forward bias. 
                # In case of the common-cathode, the common pin should be LOW,
                # to foreward-bias.
                if (self.isCC):
                    GPIO.output(pin, GPIO.LOW)
                else:
                    GPIO.output(pin, GPIO.HIGH)

                # Delay to retain the current state at the digit at index.
                time.sleep(DELAY)

                # Now turn off the foreward-bias state.
                if (self.isCC):
                    GPIO.output(pin, GPIO.HIGH)
                else:
                    GPIO.output(pin, GPIO.LOW)

                # Increment index and prepare to move forward to next digit.
                index += 1

하는 일은 앞서 실험했던 내용과 동일합니다. 공통 단자의 신호를 계속 바꾸어 가면서 다른 7-Segment LED들을 번갈아 표시하는 것이죠. 메서드 이름  앞에 밑줄(_)을 넣은 이유는 외부에서 호출할 필요가 없는 메서드라는 것은 나타내기 위해서였습니다. 꼭 이렇게 하지 않아도 돼요.

 

이 루프를 탈출하는 조건은 display_stop() 메서드를 호출하면서 만들어집니다.

    def display_stop(self):     # Stop display
        self.keepRunning = False # Make this value False to escape from displaying loop.

 이 클래스의 인스턴스가 파괴(?)될 때 호출되는 메서드가 __del__()인데요, 파괴자(Destructor)라고 하지요. 우리말로 바꾸니 이상하긴 하지만, 인스턴스가 마지막으로 정리되는 부분입니다. Thread.join()이라는 메서드가 호출이 됩니다.

    def __del__(self):      # Destructor
        if(self.display_task != None):  # In case the display task was running,
            self.display_task.join()    # Wait until the task ends.

Thread에서 join 한다라고 하는 것은 해당 thread가 종료되는 것을 기다린다는 의미입니다. _display() 메서드가 하나의 thread로서 실행이 되고 있었는데, 루프를 빠져나와서 종료되기를 기다리는 것입니다. 제가 이 부분은 충분히 시험해 보지는 않았으니 더 좋은 아이디어가 있으면 공유해 주세요. 

 

그래서 main에서는 MULTI_SEVEN_SEG_LED 클래스를 사용해서 이렇게 만들 수 있었습니다.

if __name__ == "__main__":
    print("Demo started")

    # Initialize a MULTI_SEVEN_SEG_LED instance.
    a = MULTI_SEVEN_SEG_LED((3, 2, 15, 23, 24, 17, 27, 14), (9,10))

    # Start display, which starts a thread.
    a.display_start()

    # Set a decimal point position.
    # The position doesn't necessarily need to be uniquely single
    # but could be multiple.
    a.set_DP(0)

    # Demo 1 
    # Display 1 to 99 every second.
    for i in range(1,100):
        a.update(i)
        time.sleep(1)

    # Unset the previousely set decimal point.
    a.unset_DP(0)

    # Demo 2
    # Display 1 to 255 every second in hexa-decimal form.
    for i in range(1,256):
        a.update(hex(i)[2:])
        time.sleep(1)

    # Stop display, which stops the function in thread.
    a.display_stop()

    print("Demo ended")

 

실행하면 다음 동영상처럼 동작할 것입니다.

 

 

여느 때처럼 주석을 포함한 전체 코드는 GitHub에 올려놓았습니다. 

https://github.com/sangyoungn/play_with_RPi/tree/main/multi_seven_seg_led

 

GitHub - sangyoungn/play_with_RPi: Example Codes for Raspberry Pi

Example Codes for Raspberry Pi. Contribute to sangyoungn/play_with_RPi development by creating an account on GitHub.

github.com

 

예전에 만들었던 코드들도 재활용하고 있으니 전체 repository를 clone 하세요. 

git clone https://github.com/sangyoungn/play_with_RPi.git
728x90
반응형