본문 바로가기
Arduino&RasberryPi/Arduino Uno

19. Arduino - 4자리 7 세그먼트 LED(2): 최적화 코드 분석

by Rosmary 2019. 11. 24.
728x90
반응형

7자리 세그먼트에 적용할 코드를 이것저것 실험해 본 결과, Arduino의 저장 공간을 최소한으로 사용할 수 있는 가장 획기적인 방법은 이진 자료형인 byte를 변수에 사용하는 것이었다. 이 방법으로 작성된 코드를 밀어넣으니, Arduino의 총 저장 공간 중 단 3%만 차지할 정도로 크기가 획기적으로 줄어든다. 필자가 이전 포스팅에서 자체적으로 작성한 코드가 이의 두 배인 6% 공간을 차지했었으니... Arduino가 개선된 코드를 작동시키며 느끼는 부담감이 많이 사라졌을 것이다.

 

byte 자료형으로 4자리 7세그먼트를 동작시키는 코드는 인터넷에 찾아보면 무수하게 널려있다. 그러나 문제는, "그냥 이거 붙여쓰면 동작 잘해요~!"라고만 작성된 글들이 대부분이라, 실제 이 코드가 Arduino 내에서 어떻게 동작하는지 원리를 파악하고자 하는 사람들에게 별 도움이 되지 않는다는 것이다.(물론, 그냥 재미삼아 Arduino에 코드를 넣어 돌려보는 분들이라면, 굳이 이렇게까지 파고 들 필요는 없다...)

 

 

 

1. Arduino 코드에서 이진법 및 16진법 표기에 대해 알아보자

 

이진법은 0과 1, 두 개의 숫자만으로 사람이 사용하는 10진 숫자를 표현하는 방법이다. 10진 숫자 0은 0, 1은 1로 표기되며, 2는 이진 숫자 한 자리만으로 표기가 불가능하기 때문에 자릿수가 하나 증가하여 "10"으로 표기횐다. 몇몇 숫자를 이진법으로 나타내면 다음과 같다.

 

10진 숫자 이진 숫자
0 0
1 1
2 10
3 11
4 100
5 101
6 110
7 111

 

이진 숫자의 연산도 10진 숫자와 크게 다른 점은 없다. 특정 자릿수의 연산 결과가 1보다 크다면, 더 큰 자릿수에 1을 더해주면 되고, 이는 10진 연산의 덧셈 방식과도 동일하다.

 

3(011) + 4(100) = 7(111)

 

 

Arduino에서 사용하는 코드는 C언어를 기반으로 하고 있다. 하드웨어의 빠른 제어를 위해 많이 사용되는 언어인만큼, 이진 숫자 표현과 연산도 자주 사용되는데, 문제는 10진 숫자의 값이 커지면, 이를 이진 숫자로 표기하기가 매우 불편해진다. 가령 필자가 243이라는 숫자를 이진법으로 표기한다면

 

=======================

11110011

=======================

 

로 표기해야한다. 만약 숫자가 1024를 넘어간다면, 무려 10자리를 초과하는 이진 숫자로 표기해야하는 아찔한 상황이 발생한다.

 

따라서, 코드를 작성할 때, 이진법보다 16진법 표기를 많이 사용한다. 16진법은 숫자 0~9와 알파벳 A~F를 사용하여 숫자를 표현하는 방식이다. 한 자리의 16진법 숫자는 4자리의 2진 숫자를 표현할 수 있다.

 

이진 숫자 16진 숫자
0 0
1 1
10 2
11 3
... ...
1001 9
1010 A
1011 B
1100 C
1101 D
1110 E
1111 F

 

필자가 10진 숫자를 16진법으로 표시한다면 다음과 같은 과정을 거쳐 표기할 수 있을 것이다.

 

================================

 28 => 00011100   =>   0001 1100 => 0x1C

128 => 10000000   =>  1000 0000 => 0x80

255 => 11111111   =>  1111 1111 => 0xFF

===============================

 

C언어 기반의 코드에서, 16진법을 표기하는 방법은 다음과 같다

 

=====================

byte 변수명 = 0x{16진숫자}

=====================

 

따라서, 255라는 값을 byte 자료형 변수로 저장하고자 한다면 "byte variable = 0xFF;"로 선언해주면 된다.

 

 

 

2.  각 세그먼트와 연결된 핀을 이진법으로 표현하기.

 

필자가 A부터 G, 그리고 dp 세그먼트와 연결된 Arduino 핀을 6번부터 13번까지 할당했다고 가정하자. 이를 표로 나타내면 아래와 같이 될 것이다.

 

A B C D E F G dp
6 7 8 9 10 11 12 13

 

지금까지는 각 세그먼트와 연결된 핀을 하나의 변수에 저장한 뒤, 이들 변수를 사용해 pinMode()와 digitalWrite() 함수를 사용했다(int a = 6; int b = 7....). 하지만, 이들 변수가 모두 정수 자료형이다보니, byte 연산을 기본으로 진행하는 Arduino 입장에서는, 코드 내의 연산을 진행하기 전에 10진 숫자를 이진 숫자로 변경하는 작업을 거쳐야하므로, 이진 자료형을 사용하는 코드에 비해 처리 속도가 상대적으로 느릴 수 밖에 없다.(마치 사람에게 1과 0으로 구성된 이진 숫자 하나 던져주고, "이거 10진 숫자로 연산 결과 출력해주세요" 라고 말하는 것과 같다..)

 

따라서, 이전의 방식과는 달리, 각 핀에 전달할 신호를 이진 형태의 변수로 만드는 작업이 선행되어야 한다. 이진 변수를 선언하는 방법은 아래와 같다

 

=======================

byte 변수명 = 변수값;

=======================

 

이진 변수는 8자리의 이진 숫자로 구성할 예정이다. 8자리의 이진 숫자는 "00000000", "10010010"과 같은 형태로 나타내는데, 앞에서부터 순차적으로 A, B, C, D, E, F, G, dp 핀에 전달할 신호를 명시한다. 가령, 7세그먼트에 1을 출력하고 싶다면 이진 숫자는 "10011111"로 표현할 수 있다(B, C 세그먼트만 불이 들어와야하니, 둘을 제외한 나머지 핀에 전달되는 신호는 모두 HIGH(1)가 되어야 한다).

 

그럼, 변수명에 따라 이진 변수값이 결정되니, 0부터 9에 해당하는 세그먼트 이진값을 등록하기 위해서는 아래와 같이 10개의 변수가 필요하다. 

 

숫자 A B C D E F G dp
0 0 0 0 0 0 0 1 1
1 1 0 0 1 1 1 1 1
2 0 0 1 0 0 1 1 1
3 0 0 0 0 1 1 0 1
4 1 0 0 1 1 0 0 1
5 0 1 0 0 1 0 0 1
6 0 1 0 0 0 0 0 1
7 0 0 0 1 1 1 1 1
8 0 0 0 0 0 0 0 1
9 0 0 0 0 1 0 0 1

====================================================

byte zero = 00000011; byte one = 10011111;....

====================================================

 

혹은 배열을 사용하여 변수를 지정해도 좋다. 필자는 Arduino가 빠르게 변수값을 참고할 수 있도록 배열을 사용할 것을 권고한다.

 

=====================================================

byte reference = {00000011, 10011111, ...., 00001000};

=====================================================

 

8자리의 이진 숫자를 일일이 타이핑하기는 어려우니, 16진법 표기로 수정하여 표현하면 다음과 같이 변수를 선언할 수 있을 것이다. 

 

======================================================

byte reference = {0x03, 0x9F, ....., 0x08};

======================================================

 

 

 

 

3. 이진 연산을 통해, 각 숫자와 일치하는 세그먼트 신호 전달하기

 

4자리 세그먼트를 작동시키는 공식 코드를 보면, 다음과 같은 코드가 존재한다.

 

=======================================

segment_pin[8] = {6, 7, 8, 9, 10, 11, 12, 13};

....

....

void show (int pos, int number)

{

...

for(int i = 0; i < 8; i++){
     byte segment = (reference[number] & (0x01 << i)) >> i;
     if(segment == 1){
       digitalWrite(segment_pin[7 - i], HIGH);
     } else {
       digitalWrite(segment_pin[7 - i], LOW);

}

=======================================

 

이 코드에서 가장 중요한 부분은 볼드체로 색칠해 놓은 부분이다. 이 부분을 분석해보도록 하자.

 

(1)  reference[number]

 

reference[number] 값은, show() 함수의 두 번째 인자에 따라 7 세그먼트에 보내는 신호를 지정한 변수를 나타낸다. 표시하고자 하는 숫자가 0이라면, number = 0이며, reference[number] 값은 우리가 이전에 작성한 reference 배열의 첫 번째 값인 0x03이 된다. 이 값을 이진법으로 나타내면 "00000011"이며, 이는 G와 dp 세그먼트를 제외한 모든 세그먼트에 digitalWrite(#, LOW)가 전달되도록 하는 신호와 동일하다.

 

 

(2)  0x01 << i

 

0x01은 10진 숫자로 표기하면 1이다. 그런데 << i라는 연산에 의해, 1값이 변화하게 된다. i 값은 for 문에 의해 0부터 7까지 변화하며, 연산 결과는 아래와 같을 것이다.

 

0x01 << 0   :        0000 0001  << 0   =>  0000 0001

0x01 << 1   :        0000 0001  << 1   =>  0000 0010

0x01 << 2   :        0000 0001  << 2   =>  0000 0100

........

 

큰 어려움이 있는 연산은 아니다.

 

 

 

(3) reference[number] & (0x01 <<i)

 

&는 "그리고"의 의미를 가지며, 논리 연산에서 피연산자인 두 값이 모두 참(1)인 경우, 참(1)을 출력하고, 아니라면 거짓(0)을 출력한다. 따라서, 위의 연산은 다음과 같이 나타낼 수 있다.

 

===========================

 

***  i =0, number=0 일 때,

     reference[0] = 00000011

 &  (0x01 << 0) = 00000001

---------------------------------

       Result       = 00000001

 

***  i =1, number =0 일 때,

     reference[0] = 00000011

 &  (0x01 << 1) = 00000010

---------------------------------

       Result       = 00000010

 

***  i =2, number =0 일 때,

     reference[0] = 00000011

 &  (0x01 << 2) = 00000100

---------------------------------

       Result       = 00000000

......

===========================

 

 

(4) (reference[number] & (0x01 << i))>> i

 

이제 이 연산값을 >> i에 적용하면 다음과 같은 결과가 나온다.

 

===========================

*** i = 0, number = 0 일 때,

    00000001 >> 0 :  segment = 1

 

*** i = 1, number = 0 일 때,

    00000010 >> 1 :  segment = 1

 

*** i = 2, number = 0 일 때,

    00000000 >> 2 : segment = 0

......

===========================

 

지금까지의 코드를 통해, 위에서 분석한 연산이 각 핀을 제어하는 신호의 값을 반환한다는 것을 확인할 수 있다. 가령, 숫자 0이 number 변수의 값으로 지정된 경우, 숫자 0을 표시하는 reference[number] 값이 00000011이 되는데, 위에서 분석한 연산을 for 문안에 돌림으로써, reference[number] 값의 역값인 11000000을 얻을 수 있게 된다.

 

이제, 이렇게 출력된 값을 이용해 각 핀에 신호를 주는 방법만 코드로 명시하면 된다.

 

 

 

(5) 신호의 전달

 

i=0일 때, 연산의 최종 값이 1이 발생했으므로, for 문 내의 if 분기점에서 digitalWrite(segment_pin[7 - i], HIGH) 를 실행하게 된다. i = 0일때 segment=1이므로, segment_pin[7]에 해당하는 핀에 전기신호를 주라는 명령이 이루어지게 된다. segment 배열의 7번은 dp와 연결된 13번 핀이며, 이 핀에 전기 신호가 공급됨에 따라, dp 세그먼트에는 불이 들어오지 않게 된다. 

 

i=2일때도 동일한 절차로 확인할 수 있다. i=2의 segment값이 0이므로, if 분기점 내에서 digitalWrite(segment_pin[5], LOW)를 실행하게 되며, segment_pin의 5번 배열에 해당하는 11번 핀(F 세그먼트)에 전기 공급이 이루어지지 않으므로, F 세그먼트에 불이 들어오게 된다. 

 

나머지도 동일한 절차를 거쳐, 특정 핀에 전기 공급 유무를 결정하게 된다.

 

 


 

사실, 위의 최적화 코드는 byte와 이진법, 이진 연산에 대한 기초 지식이 없다면, 작성할 수 없는 코드다. 하지만, 해당 내용에 대한 지식이 갖춰져있고, 이진 연산에 익숙한 분들이라면, 코드 분석 및 작성에 큰 무리는 없을 것이다. 필자는 일이 바뻐 여유없는 와중에, 너무나도 오랜만에 이진 연산을 접해서인지 코드 분석에 시간이 조금 걸렸다...

 

다음 포스팅에서는 최적화 코드를 활용하여 한 자리 / 4자리 7 세그먼트를 동시에 동작시켜보려한다.

 

 

 

FIN.

반응형

댓글