우선 스케줄러에서 다뤄줬으면 했던 건, 캐릭터를 추가할 수 있어야 하고 일일 콘텐츠를 체크하거나 카운트할 수 있는 게 필요했다. 그리고 주간 콘텐츠도 블록을 나눠서 체크 혹은 카운트할 수 있으면 좋겠다는 생각... 그리고 캐릭터 별로 휴식 게이지도 계산해서 표시해주면 좋을 거 같다. 왜냐하면 배럭을 돌리는 입장에서 휴식 게이지를 채우고 한 번에 하는 게 편리해서 나는 하루에 캐릭터 1개씩 숙제를 한다. (에포나 빼고) 그렇기 때문에 오늘 무슨 캐릭터를 숙제해야 하나 알아내려면, 매번 휴식 게이지를 확인해야 하는 번거로움을 덜 수 있지 않을까? 캐릭터 전환하는데 로딩 시간을 조금이라도 단축해야.. 그리고 지난번에도 말했듯 가까운 이벤트를 알림 해주면 좋을 거 같아서 그것도 추가하고 싶다. 물론 이 모든 게 듀얼 모니터를 써야 시인성이 좋을 거 같긴 하다.
왼쪽과 같은 느낌으로 디자인하길 원했고 파이썬 모듈은 PyQt5를 채택했다. 처음에는 tkinter를 채택하려 했으나, PyQt5쪽이 더 많이 쓰인다고 해서 PyQt5로 채택했다. 기본적으로 기능 구성을 하기 이전에 레이아웃을 짜는 게 우선이라고 생각한다. 물론 레이아웃을 짜다 보니 기능을 자꾸 넣고 있다..
사실 순서가 없다. 그냥 손 닿는 대로 짜는 중... 그래도 현재 어느 정도 GUI 위젯들은 배치를 해놓은 상태이다. 추후 로아 이벤트를 표기할 공간을 위해 변경될 수 있지만 지금 현재로서는 아래와 같이 구성해 보았다.
코드 작성은 https://wikidocs.net/book/2165 사이트를 참고하여 하나하나 예제 실행해보면서 응용해보았다. 매번 임베디드 같은 거 하다가 이런 거 하니까 나름 재밌는 거 같다. 물론 뒤에 기능 넣고 하다 보니 또 재밌지는 않다.. GUI 구성 관련해서 코드 설명은 사실 아는 게 많이 없기도 하고 예제 사이트에서 응용해서 가져다 쓴 거라 생략하도록 하겠다.
이 문제는 '문자'로 입력을 받은 후 그 문자의 '아스키 코드 값'을 출력하는 문제입니다. C언어 방식으로 접근하면 아주 간단합니다. char type의 변수를 선언한 후에 그 변수로 '문자'로 입력을 받은 후 출력을 할때는 정수형 출력을 하게 되면 입력 받은 문자를 그대로 출력을 하는 것이 아닌 아스키 코드의 값으로 출력을 하게 됩니다. 이번 문제도 아주 간단한 문제이기 때문에 flow chart와 key point는 생략하고 넘어가도록 하겠습니다.
#include <iostream>
using namespace std;
int main(){
char ch;
scanf("%c", &ch);
printf("%d\n", ch);
return 0;
}
배열 문제는 대부분 간단하게 풀리는 문제가 많은 것 같습니다. 아무래도 1차원 배열이다 보니 값을 활용해 문제를 내려다 보니 한계가 있는 것 같습니다. 문제로 돌아가서 5명의 시험 점수를 평균내서 출력하는 간단한 문제입니다. 하지만 여기서 조건이 하나 추가되는데 40점 미만인 학생은 보충학습을 통해 40점을 가져갈 수 있다는 것입니다. 그리고 보충학습은 선택이 아닌 필수이므로 40점 미만 학생은 전부 40점 처리해서 풀면 됩니다. 아주 간단하죠. 출력또한 정수형으로 출력하기 때문에 자료형을 신경써줄 필요가 없습니다.
이번 문제는 간단하게 풀리기 때문에 flow chart를 생략하고 코드와 출력 결과를 첨부하고 끝내도록 하겠습니다.
#include <iostream>
using namespace std;
int main(){
int answer = 0;
for(int i = 0;i < 5;i++){
int temp;
scanf("%d", &temp);
if(temp < 40)
answer += 40; // 40점 이하는 보충학습을 통해 40점을 받게됨
else
answer += temp; // 그 이외에는 본인 점수 입력
}
answer /= 5; // 인원 수 만큼 나누기(평균)
printf("%d\n", answer);
return 0;
}
이번 문제는 다른 배열 문제보다 더 간단한 것 같습니다. 문제 자체가 1씩 감소하고 있는지 1씩 증가하고 있는지를 물어보는 것이 아닌 첫 번째 판단 조건을 주었습니다. 가장 앞자리에 오는 음계가 1인지 8인지를 먼저 판단한다면 대부분의 케이스가 먼저 걸러질 것이라 예상됩니다. 그러고 나서 1과 8인 경우에서 다음 음계가 순차적으로 왔는지 판단하고 만약 순차적이지 않다면 그 부분에서 탐색을 종료하고 "mixed"라고 출력을 하면 됩니다. 처음 문제를 접근했던 방식은 8개의 배열을 모두 탐색하면서 인접한 배열의 원소의 차를 더해가지고 판별하는 방식으로 접근했었습니다.
예를 들어 첫 번째 예제인 "1 2 3 4 5 6 7 8"일 경우 배열을 탐색하면서 1과 2의 차는 -1이고, 다음 원소 2와 3의 차는 -1이므로 더 해나가다 보면 인접 배열의 차의 합은 -7이게 됩니다. 반대로 "8 7 6 5 4 3 2 1"일 경우에는 7이 나올 것입니다. 그 외의 경우는 전부 "mixed"라고 판단하도록 했습니다. 이렇게 할 경우 현재 배열의 크기가 8이기 때문에 탐색하는데 얼마 안 걸리지만 배열의 크기가 커질 경우 모두 탐색을 해야 알 수 있다는 점이 단점이라 생각했습니다. 또한, 모든 케이스에서 순차적으로 증가 또는 감소할 때만 저 값이 나온다는 확신이 없었기 때문에 접근 방법을 다르게 생각해봤습니다. 물론 이 방식이 코드도 훨씬 짧고 가독성도 높긴 하지만, 탐색 중간에 mixed임을 알면서도 끝까지 탐색해야 한다는 치명적 단점이 있기 때문에 다른 방식을 선택했습니다. 위에서 먼저 설명한 최종 접근 방식을 정리하면 아래와 같습니다.
* Key point!
1. 주어진 배열의 첫번째 원소가 1인지 8인지 판단
2. 그 다음 배열부터 순차적으로 증가 or 감소하는지 판단
위의 알고리즘 대로라면 처음 구상했던것 보다 훨씬 빠를 것이라 예상합니다. 물론 배열의 크기가 8로 매우 작아 큰 차이는 안보이겠지만 배열의 크기가 매우 커진다면 차이가 조금씩 발생할 것입니다. 그래도 첫 번째 접근했던 방식도 O(n)의 시간 복잡도를 가지기 때문에 눈에 띄게 느려지는 것은 아닙니다. 오늘의 풀이는 여기까지 하고 하단에 소스코드와 출력 결과 사진을 첨부하고 마치도록 하겠습니다.
#include <iostream>
using namespace std;
int main()
{
int note[8]; // 음계를 저장할 변수
for (int i = 0; i < 8; i++)
scanf("%d", ¬e[i]);
if(note[0] == 1){
bool ascending = true;
for(int i = 1;i < 8;i++){
if(note[i] != i + 1){
ascending = false;
break;
}
}
if(ascending)
printf("ascending\n");
else
printf("mixed\n");
} // 첫음계가 1부터 시작할때 ascending인지 판별
else if(note[0] == 8){
bool descending = true;
for(int i = 1;i < 8;i++){
if(note[i] != 8 - i){
descending = false;
break;
}
}
if(descending)
printf("descending\n");
else
printf("mixed\n");
} // 첫음계가 8부터 시작할때 descending인지 판별
else
printf("mixed\n"); // 그 이외의 모든 경우
return 0;
}
최근 GUI프로그래밍에 재미를 들려 평소 로스트아크 하면서 만들고 싶었던걸 만들어 보려 한다. 게임을 하면서 제일 불편(?)했던게, 소위 말해 배럭을 올리면서 휴식 게이지를 채워 일일 컨텐츠를 진행했었다. 여기서 문제는 '가장 마지막에 플레이했던 캐릭터가 뭐였지?'였다. 이를 확인하려면 캐릭터를 변경하여 컨텐츠에 가서 휴식게이지를 확인해서 확인했어야 했다. 이를 체크하고 보여주는 프로그램이 있으면 좋을거 같다는 생각이 들어 진행하게 되었다.
물론, 빈아크(ark.bynn.kr/home)라는 스케줄러가 이미 존재한다. 하지만 GUI 프로그래밍 공부 겸 겸사겸사 만들어보는게 목적이다.. 만들다보면 분명 '로아 스케줄러는 제발 사드세요..!'라는 말이 나오겠지만 ㅎ 프로젝트에 들어가기에 앞서 어떤어떤 기능을 넣을지 고민해보고 To Do 리스트를 작성해보려고 한다.
2. To Do
캐릭터를 추가할 수 있어야하고 캐릭터 별로 컨텐츠를 개별적으로 관리할 수 있어야 한다.
일일 컨텐츠 / 주간 컨텐츠는 초기화 시간에 맞춰 초기화 되어야 한다. (일일 컨텐츠: 매일 아침 6시 / 주간 컨텐츠: 매주 수요일 아침 6시)
체크 리스트에 따라 휴식 게이지도 쌓였으면 좋겠다.
모든 데이터를 저장할 수 있어야 하며, 껏다 켜더라도 내가 설정한 데이터가 날아가면 안된다.
개인적인 고질병이지만, 섬 등장 시간을 못볼때가 많다.. 이를 위해 가장 가까운 시간내의 로아 이벤트(모험섬, 카오스게이트)도 띄워주면 좋을거같다.
사실 이 프로젝트는 구상을 하자마자 작업을 해서 어느정도 코드구현이 되어있는 상태이다. 프로젝트 베이스 언어는 Python으로 진행될 예정이며, 유저 및 캐릭터 정보를 로컬 파일로 관리하다가 어느정도 완성이 되면 데이터베이스 연동 방향으로 생각하고 있다.Python으로 복잡한 code 구현해본 경험이 많지가 않아 꽤 난항을 겪을것 같다..
Computation, data processing, control, commnicaton, measurement 등의 분야에 쓰이는 시스템을 말합니다. Analog system보다 정확성과 신뢰성이 높은 장점이 있습니다.
* Binary Digit
Binary는 0과 1의 값을 갖는 진법으로 각 자리 숫자를 "bit"라 부릅니다. 숫자의 표기가 0과 1로 이루어져 있어서 전자기기를 표현하는데 적합합니다. 예를 들어 스위치가 켜졌을 때를 1, 꺼졌을 때를 0으로 표기할 수 있습니다.
* Switching Circuit
- Combination circuit(조합 회로): output이 오직 현재의 input에만 영향을 받습니다. 즉, output = f(input)
- Sequential circuit(순서회로, 순차 회로): Combination circuit 과는 다르게 현재의 input과 과거의 input 모두 영향을 받음. 이를 "memory"소자를 갖는다고 표현합니다.
(output, next_state) = f(intput, current_state)
1.2 Number Systems and Conversion
* 위치 표기법(Positional notation)
각 자리 숫자는 계수 곱의 합으로 구성됩니다. 만약 984.7810을표현하고자 하면,
983.7810= 9*10^2+ 8*10^1+ 3*10^0+ 7*10^-1+ 8*10^-2입니다. 2진법에서 10진법과 마찬가지로 표현이 가능합니다. 1011.112를 위치 표기법으로 표현하면,
이처럼 1보다 큰 어느 기저(radix or base)든 간에 위치 표기법으로 진법이 표현이 가능합니다. 기저 R에 대해서 power series로 전개가 가능합니다. 예를 들어 기저가 8인 8진수의 표기법은
147.38= 1*8^2+ 4*8^1+ 7*8^0+ 3*8^-1= 64 + 32 + 7 + 3/8 = 103.37510입니다. 그리고 모든 기저는 서로 변환이 가능한데 예를 들어서 14710를 위치 표기법으로 표현하면, 1*10^2+ 4*10^1+ 7*10^0인데, 이를 2진수로 바꾸려 할 때, 모든 계수와 기저를 2진화 시킵니다. (1)*(1010)2+ (100)*(1010)1+ (111)*(1010)0으로 나타내지는데, 이를 계산하면 100100112가 됩니다. 우리가 아는 2로 나눈 나머지 값을 이어붙이는 표기로도 표현이 가능합니다.
2진법의 경우 digital system을 이해하기는 편리하지만 1과 0으로만 이루어졌기 때문에 숫자가 조금만 커져도 길이가 너무 길어지는 단점이 있습니다. 그러한 이유로 16진수를 많이 사용하게 되는데 기저가 10 이상으로 넘어가게 되면 10 이상의 숫자를 표현해야 합니다. 그렇기 때문에 10부터 알파벳을 하나씩 부여하게 됩니다. 즉 10은 A이고 11은 B 이런 식으로 진행하게 됩니다. Digital system에서 16진법(Hexadecimal)을 제일 많이 씁니다. 그 이유는 16진수와 2진수는 서로 변환하기 편리하게 되어있기 때문입니다. 16진수에서 각 자리수는 2진수의 4자리로 변환됩니다. 변환된 2진수를 이어붙이면 동일한 값을 갖는 2진수 표기법이 됩니다. 예를 들어, A2F16을 이진수로 바꿔보면
반대로 2진수 또한 16진수로 간단하게 변환이 가능한데 뒤에서부터 4개씩 끊어서 변환해주면 됩니다. 예를 들어,
* 10진수 값을 기저 R로 나눗셈 방식을 통한 변환
10진수 N 값에 대하여 기저 R로 변환하고자 할 때 나눗셈 방식을 이용해서 변환할 수 있습니다. N에 대하여 R로 나누어서 나머지 값이 기저 R에 대한 계수 값이 됩니다.
이 과정을 계속 진행해나가다 보면 최종적으로 an의 값을 얻을 수 있습니다. 이 모든 값을 이어붙이면 기저 R에 대한 값으로 표기될 것입니다. 반대로 소수점의 경우에는 나누어주는 것이 아닌 곱셈을 통해서 표현할 수 있는데, 기저 R 만큼 곱해준 뒤 소수점을 제외한 부분이 계수가 됩니다.
이 과정을 반복하고 각 계수를 이어붙이면 동일하게 소수점 아래의 표현을 구할 수 있습니다.
1.3 Binary Arithmetic
Digital system에서의 산술연산들은 2진법으로 계산합니다. 그 이유는 10진수 계산보다 2진수로 계산하는 것이 논리회로 설계에서 더 용이하기 때문입니다. 2진수 산술연산은 10진수의 올림(carried out)과 동일하게 수행됩니다.
* 덧셈 (Addition)
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 0
10(다음 열에 1이 올림 된다.)
* 뺄셈 (Subtraction)
0 - 0 = 0
0 - 1 = 1(다음 열에서 빌려온 1이다)
1 - 0 = 1
1 - 1 = 0
* 곱셈 (Multipliation)
0 * 0 = 0
0 * 1 = 0
1 * 0 = 0
1 * 1 = 1
* 나눗셈 (Division)
1.4 Representation of Negative Numbers
현재까지 예제로 다뤄온 수는 모두 부호가 없는 양수들만 다뤘습니다. 하지만 컴퓨터에서는 음수와 양수를 표현하기 위해 부호가 있는 숫자를 별도로 표기해야 합니다. 부호가 있는 2진수는 가장 첫 번째 bit를 부호 bit로 지정하게 됩니다. 첫 번째 bit가 0일 경우 양수, 1일 경우 음수입니다. 부호가 있는 표기법에는 여러 가지 방법이 존재합니다.
- Sign and magnitude system(부호와 크기 표현 방법)
첫 번째 bit를 부호 비트로 두고 나머지 뒤의 bit를 크기로 두는 표현 방법입니다. 이 방법은 기저가 r이고 n 자리 bit라 했을 때,
의 범위를 갖습니다.
- True and complement system
이 방법은 1's complement system과 2'scomplement system으로 나눠집니다.
1's complement system의 경우 C는 2n- 1이고 2's complement system은 C가 2n으로 계산합니다. 두 방법은 약간의 차이가 존재하는데 우선 표현할 수 있는 범위의 차이가 있습니다. 길이가 4인 음수 2진수를 표현한다고 가정했을 때, 표로 정리하면
위의 표를 봤을 때, 2's complement 방법을 사용했을 때 표현할 수 있는 숫자의 폭이 제일 넓은 것을 볼 수 있습니다. 그러한 이유로 대부분의 digital system은 2's complement 방식으로 표현을 합니다.
음수를 1's complement 형태로 표현하는 방법은 우선 해당 음수의 절댓값의 2진수로 표현하고 모든 비트를 반전시켜주면 됩니다. 예를 들어 -7의 경우에 절댓값은 0b0111로 표현됩니다. 이 값의 모든 비트를 뒤집으면 0b1000으로 -7을 표현할 수 있습니다. 2's complement 역시 1's complement와 동일한 과정을 거친 후 마지막에 1을 더해주면 됩니다. 0b0111에서 반전시켜주어 0b1000로만들어준 뒤 1을 더하면 0b1001로 표기됩니다.
지난 시간 PWM 제어를 통해서 LED 밝기를 조절하는 것에 대해 다뤄보았습니다. 오늘은 동일한 이론을 가지고 DC 모터의 속도를 조절하는 법에 대해 이야기 해볼까 합니다.
먼저 이론에 앞서 모터 제어에 필요한 준비물을 알아봅시다. 가장 먼저 모터는 부하가 크기 때문에 이전 수업 내용들과는 다르게 외부 전원이 필요합니다. 여기서 외부 전원은 일상에서 쉽게 접할 수 있는 건전지부터 해서 보조배터리와 같은 충전지를 말합니다. 외부 전원을 준비했다면, DC 모터 제어의 핵심인 모터 드라이버가 필요합니다. 그럼 핵심인만큼 모터 드라이버에대해 먼저 알아보도록 하죠.
* 모터 드라이버란?
왼쪽의 사진을 모터 드라이버라 부릅니다. 해당 모듈은 모터를 정방향 또는 역방향으로 회전하게하거나 회전 속도를 조절할 수 있도록 도와줍니다. 사실 회로 자체의 역할은 스위칭을 통하여 회전 방향을 바꿔주는데 의의가 있습니다. 모터 드라이버에는 다양한 종류의 회로를 내장하고 있는데, 이번 시간에 다뤄볼 회로는 H-Bridge라는 회로입니다. 가장 많이 쓰이기도 하고 간단하니 너무 겁먹지 않아도 됩니다. 회로부분에 있어서는 공학적인 내용보다 흐름정도만 설명하고 넘어가도록 하겠습니다.
H-Bridge 회로는 회로의 모양이 H 모양 다리처럼 생겨서 붙은 이름인데 아래 그림을 보면 이해가 될 겁니다.
회로는 간단하게 스위치 4개와 모터 1개 그리고 외부전원으로 구성되어 있습니다. 하나씩 뜯어보면 먼저 스위치 역할을 해주는 4개의 반도체 소자가 보입니다. Q1~Q4가 반도체 소자인데, 보통 MOSFET이나 BJT라는 반도체 소자를 이용하여 스위치처럼 사용합니다. 밑에 적힌 PNP와 NPN은 반도체 극성을 의미하며 극성에 따라 특성이 달라지는데, 이건 중요한게 아니니 넘어가도록 하겠습니다. 이 4개의 스위치만 잘 조작하면, 모터의 방향을 조절할 수 있습니다. 이전 LED와 같은 극성 소자에서는 +와 -에 유의하면서 연결해줬었잖아요? 모터는 극성이 없습니다. 정확히 말해서 전류의 흐름에 따라 모터가 회전하는 방향이 달라진다는 이야기 입니다.
예를 들어, 오른쪽 사진과 같이 Q1과 Q4 소자에 신호(5V)를 주게 되면 다음과 같이 전류가 흐르게 될 것입니다. 여기서 조건은 Q2와 Q3가 Off 상태야 합니다. Q1과 Q4 스위치를 눌러주면서 전류가 흐를 수 있는 길을 터준것이고 전류는 빨간색 선과 같이 모터에 흘러들어가게 됩니다. 그렇게 되면 모터는 시계 방향 또는 시계 반대 방향으로 즉, 특정 방향으로 회전하게 됩니다. 여기서 특정 방향으로 회전한다 언급한 것은 모터마다 코일이 감겨있는 방향이나 여러가지를 고려했을 때, 어느 방향으로 회전한다를 단정지을 수 없기 때문에 '특정 방향으로 회전한다'라고 한겁니다. 여기서 반대로 Q1 스위치와 Q4 스위치를 Off하고 나서 Q2와 Q3 스위치를 On하게 된다면 어떤 동작을 보일까요?
왼쪽의 그림이 Q2, Q3 스위치를 On 시켰을 때, 전류의 흐름을 나타내보았습니다. 딱 봐도 위의 그림과 다르게 모터에 전류가 반대로 흘러 들어가는 것을 볼 수 있죠. 이런 회로에서는 그럼 모터는 어떤 동작 특성을 보일까요? 예상했던 대로, 위의 회로와는 반대로 모터가 회전하는 것을 볼 수 있습니다. 처음 도입부에서 말씀 드렸다 싶이 모터는 +단자와 -단자 구분이 없습니다. 그렇단 이야기는 전류가 들어오는 방향에 따라서 동작 특성이 결정된다는 이야기에요. 그럼 우리가 도출할 수 있는 결과는 모터에 전류를 흘려주는 방향 조절만 해준다면 회전 방향을 정할 수 있단 이야기죠. 여기서 문제가 하나 발생합니다. 우리는 보통 전압원(Vcc)에 직류전원(건전지, 충전지..)을 사용하기 때문에 전류 방향을 바꿔줄 수가 없어요! 그렇기 때문에 이러한 회로구성이 필요합니다. 단 스위치 4개만으로 전류의 흐름을 바꿔줄 수가 있죠.
하드웨어적인 개념은 여기까지 하고 이제 이 회로를 어떻게 제어할 것이냐를 고민해봅시다.
이번 강의의 포인트는 2가지입니다. 모터의 회전 방향을 제어하는것, 모터의 회전 속도를 제어하는 것 입니다. 모터의 회전 방향은 위의 하드웨어를 제어함으로써 해결되지만 여기서 모터의 속도 제어는 또 다른 문제입니다.
속도 제어를 위해서는 마이컴의 기능을 활용해야합니다. 바로 이전 LED 밝기 제어 강의때 다뤘던 PWM입니다. 동일한 이론을 바탕으로 모터에 출력의 변화를 줄 수 있습니다. 근데 여기서 추가 회로의 도움 없이 컨트롤러에 모터를 꽂을 경우 과전류로 인해 보드가 망가질 수 있으니 조심해야합니다. 마이크로 컨트롤러의 경우 각 보드가 허용하는 출력 전류가 정해져있어 그 이상 뽑아내려하면 죽어버립니다. 이러한 이유로도 H-Bridge와 같은 회로가 필요한 것이죠. 다시 본론으로 들어가서 우리는 위의 회로에서 1가지만 추가하여 모터의 속도를 제어할 것입니다. 바로 스위치를 켜는 부분에 PWM 신호를 인가해 볼 겁니다. PWM 신호에 따라 스위치가 깜빡깜박하게 된다면 모터는 스위치가 켜지는 시간만큼 빨라질겁다. 머릿속으로 한번 상상해보면서 따라와보세요! 10초의 주기를 가지고 실험을 한다고 했을때, 1초 눌렀다가 9초 떼고있을때와 5초 누르고있다가 5초 떼는것 중 어느게 더 빨리 돌까요?? 물론 초단위라 사실 속도는 비슷할지 모릅니다. 하지만 이게 ms, us단위로 껐다 켰다하게 되면 확실하게 모터가 회전하는 속도 차이가 보일겁니다. 즉, 제가 말씀드리고 싶은 말은 PWM의 Duration을 조절하면 모터의 속도를 제어할 수 있단말을 하고싶었던 겁니다. Duration이 30%일때보다 70%일때 더 빠르게 되겠죠.
이 두 이론을 합쳐서 코드를 짜보고 직접 눈으로 확인해봅시다. 우선 회로도는 아래와 같이 구성하면 됩니다. (준비물: 아두이노, DC 모터, 모터드라이버 L298N, 외부 전원)
IN1 & IN2 는 왼쪽 출력에 관여, IN3 & IN4 는 오른쪽 출력에 관여
IN3와 IN4는 모터의 출력 방향을 결정하는 Switch 역할
ENA와 ENB는 모터의 출력 속도를 결정하는 PWM 신호 핀
외부 전원은 VCC와 GND에 연결하며 GND의 경우 Arduino와 같이 물려주어야함
#define IN3 5
#define IN4 4
#define Motor2_PWM 3
int value = 0;
void setup() {
// put your setup code here, to run once:
pinMode(IN3, OUTPUT); // H-Bridge Switch 1
pinMode(IN4, OUTPUT); // H-Bridge Switch 2
}
void loop() {
// put your main code here, to run repeatedly:
digitalWrite(IN3, HIGH); // H-Bridge Switch ON
digitalWrite(IN4, LOW); // H-Bridge Switch LOW
for (int i = 0; i < 255; i += 50) {
analogWrite(Motor2_PWM, value); // PWM 신호를 value만큼 인가
delay(1000); // 1초 delay
}
for (int i = 255; i > 0; i -= 50) {
analogWrite(Motor2_PWM, value); // PWM 신호를 value만큼 인가
delay(1000); // 1초 delay
}
digitalWrite(IN3, LOW); // H-Bridge Switch LOW
digitalWrite(IN4, HIGH); // H-Bridge Switch ON
for (int i = 0; i < 255; i += 50) {
analogWrite(Motor2_PWM, value); // PWM 신호를 value만큼 인가
delay(1000); // 1초 delay
}
for (int i = 255; i > 0; i -= 50) {
analogWrite(Motor2_PWM, value); // PWM 신호를 value만큼 인가
delay(1000); // 1초 delay
}
}
스위치 핀으로 이용할 핀번호를 출력으로 설정해준다. (PWM 핀은 이전 시간에 확인했든 함수 내부에서 출력 설정을 해주고 있음.
오늘은 아두이노 출력 함수 중 마지막인 analogWrite 함수에 대해서 알아보도록 하겠습니다. analogWrite 함수는 이름 그대로 Analog 값을 출력할 수 있게 해 줍니다. 아두이노는 Digital 시스템인데 어떻게 Analog 출력을 낼 수 있는지 궁금하실 겁니다. 사실상 정확하게 말해서는 Analog 값을 선형적으로 매끄럽게 출력하는 것이 아닌 PWM 제어를 통해서 Analog 출력을 모사한다고 볼 수 있습니다. 그럼 본론으로 들어가기 전에 PWM 제어란 무엇인지 알아보도록 하겠습니다.
1. PWM 제어
PWM이란 Pulse-width modulation의 약자로 파형(Pulse)의 폭(Width)을 변조 혹은 조정(Modulation)을 한다는 의미입니다. 말 그대로 Digital로 출력되는 파형의 폭을 변조하면 0(GND)과 1(5V)이 아닌 Analog 값(0 ~ 5V)을 출력할 수 있습니다. 이렇게 말씀드리면 이해가 어려울 거 같으니 그림 보면서 더 자세하게 설명해드리도록 하겠습니다.
위 그림을 보시면 duty에 따라서 출력이 달라지는 것을 볼 수 있습니다. 여기서 duty란 일정 주기(Period) 내에서 HIGH로 올라와있는 비율, 혹은 시간을 의미합니다. 첫 번째 예시로 duty가 5%인 경우에는 1주기 내에서 5퍼센트의 시간만큼만 HIGH로 올라와 있다는 뜻입니다. 좀 더 디테일하게 보면, 1주기가 100 msec라고 가정했을 때, HIGH값을 유지하는 시간은 100 * 0.05로 5 msec만큼만 HIGH로 올라오겠죠? 이렇게 주기적으로 일정한 duty비만큼 HIGH로 인가하게 되었을 때, 5V(HIGH)의 값에 duty비만큼 곱해준 것과 동일한 출력을 볼 수 있습니다. 각 duty비 별로 설명드리면,
Case 1. 5V 시스템 / Duty 5% / Period 100msec
5V 시스템의 경우 HIGH의 값은 5V로 출력됩니다. 여기서 한 주기 100 msec동안 Duty비인 5%만큼만 HIGH로 출력을 하면, 이전에 계산했던 것처럼 5 msec동안만 HIGH를 출력하게 됩니다. 그러면 100 msec동안 평균 출력 전압을 계산해보면 (5V*5msec)/100msec = 0.25 V입니다. 즉, 이 동작이 계속 주기적으로 반복하게 되면 0.25V를 출력하는것과 동일하게 보인다는 이야기입니다.
Case 2. 5V 시스템 / Duty 30% / Period 100msec
위와 조건은 동일하고 Duty비가 30% 일 경우에 대해 생각해봅시다. 30 msec만큼 5V의 전압이 인가되었으니, 위와 동일한 방식으로 평균 출력 전압을 계산해보면 (5V*30 msec)/100 msec = 1.5V입니다. 그렇단 이야기는 Duty비를 30%로 두고 PWM을 출력하게 되면 1.5V를 모사할 수 있단 이야기겠네요.
위 두 Case를 통해 식 하나를 도출해낼 수 있습니다.
[ Microcontroller 출력 전압값 ] X [ Duty 비 ] = [ PWM 출력 전압 ]
이를 통해서 우리는 PWM의 Duty비만 조정을 한다면, Microcontroller의
PWM을 구현하기 위해서는 Timer/Counter라는 모듈을 이용해야 하는데, 우리는 아두이노 내장 함수를 활용할 것이기 때문에 이에 대한 설명은 다음으로 미루도록 하겠습니다.
다시 본론으로 돌아와서 위에서 설명한 PWM 제어를 아두이노에서 어떻게 사용하고 활용할 수 있을지 다뤄봅시다.
2. analogWrite()
우리는 아두이노 스케치를 이용하면 복잡하게 구현해야 했던 PWM을 손쉽게 사용할 수 있습니다. 먼저 analogWrite로 출력을 내보낼 수 있는 Pin이 무엇인지 알아보아야 하는데, 그 답은 보드에 적혀있습니다.
위의 아두이노 우노 사진을 보면 Digital Pin 숫자 옆에 물결 표시가 있는 것을 볼 수 있습니다. 빨간색 동그라미로 표시되어 있는 것처럼 PWM으로 사용 가능한 Pin은 ~표시를 해두었다입니다. '~'표시가 되어있는 핀들 아무거나 골라서 이용하시면 됩니다.
- analogWrite(Pin number, Value): PWM 출력을 내보내고자 하는 Pin number를 입력 (PWM 출력 가능 여부를 확인) / Value는 0 ~ 255의 값이 들어가는데, 만약 50% Duty비를 가진 PWM을 출력하고자 할 때, 255 * 0.5 = 127.5 (127 or 128)을 입력
analogWrite 함수에 대해 좀 더 설명을 추가하면, value값에는 0 ~ 255 사이의 값을 넣을 수 있는데 그 이유는 해당 PWM을 만들어내는 Timer/Counter가 8-bit기 때문이다. 물론 아두이노 메가에는 16-bit Timer/Counter도 존재하지만, 해당 Timer/Counter도 8-bit에 맞춰서 함수를 사용하도록 설계된 것 같다. 해당 내용에 대해 자세히 알고 싶을 경우 "~\Arduino\hardware\arduino\avr\cores\arduino\wiring_analog.c"를 확인하시면 됩니다.
void analogWrite(uint8_t pin, int val)
{
// We need to make sure the PWM output is enabled for those pins
// that support it, as we turn it off when digitally reading or
// writing with them. Also, make sure the pin is in output mode
// for consistenty with Wiring, which doesn't require a pinMode
// call for the analog output pins.
pinMode(pin, OUTPUT);
if (val == 0)
{
digitalWrite(pin, LOW);
}
else if (val == 255)
{
digitalWrite(pin, HIGH);
}
else
{
switch(digitalPinToTimer(pin))
{
// XXX fix needed for atmega8
#if defined(TCCR0) && defined(COM00) && !defined(__AVR_ATmega8__)
case TIMER0A:
// connect pwm to pin on timer 0
sbi(TCCR0, COM00);
OCR0 = val; // set pwm duty
break;
#endif
#if defined(TCCR0A) && defined(COM0A1)
case TIMER0A:
// connect pwm to pin on timer 0, channel A
sbi(TCCR0A, COM0A1);
OCR0A = val; // set pwm duty
break;
#endif
#if defined(TCCR0A) && defined(COM0B1)
case TIMER0B:
// connect pwm to pin on timer 0, channel B
sbi(TCCR0A, COM0B1);
OCR0B = val; // set pwm duty
break;
#endif
#if defined(TCCR1A) && defined(COM1A1)
case TIMER1A:
// connect pwm to pin on timer 1, channel A
sbi(TCCR1A, COM1A1);
OCR1A = val; // set pwm duty
break;
#endif
#if defined(TCCR1A) && defined(COM1B1)
case TIMER1B:
// connect pwm to pin on timer 1, channel B
sbi(TCCR1A, COM1B1);
OCR1B = val; // set pwm duty
break;
#endif
#if defined(TCCR1A) && defined(COM1C1)
case TIMER1C:
// connect pwm to pin on timer 1, channel B
sbi(TCCR1A, COM1C1);
OCR1C = val; // set pwm duty
break;
#endif
#if defined(TCCR2) && defined(COM21)
case TIMER2:
// connect pwm to pin on timer 2
sbi(TCCR2, COM21);
OCR2 = val; // set pwm duty
break;
#endif
#if defined(TCCR2A) && defined(COM2A1)
case TIMER2A:
// connect pwm to pin on timer 2, channel A
sbi(TCCR2A, COM2A1);
OCR2A = val; // set pwm duty
break;
#endif
#if defined(TCCR2A) && defined(COM2B1)
case TIMER2B:
// connect pwm to pin on timer 2, channel B
sbi(TCCR2A, COM2B1);
OCR2B = val; // set pwm duty
break;
#endif
#if defined(TCCR3A) && defined(COM3A1)
case TIMER3A:
// connect pwm to pin on timer 3, channel A
sbi(TCCR3A, COM3A1);
OCR3A = val; // set pwm duty
break;
#endif
#if defined(TCCR3A) && defined(COM3B1)
case TIMER3B:
// connect pwm to pin on timer 3, channel B
sbi(TCCR3A, COM3B1);
OCR3B = val; // set pwm duty
break;
#endif
#if defined(TCCR3A) && defined(COM3C1)
case TIMER3C:
// connect pwm to pin on timer 3, channel C
sbi(TCCR3A, COM3C1);
OCR3C = val; // set pwm duty
break;
#endif
#if defined(TCCR4A)
case TIMER4A:
//connect pwm to pin on timer 4, channel A
sbi(TCCR4A, COM4A1);
#if defined(COM4A0) // only used on 32U4
cbi(TCCR4A, COM4A0);
#endif
OCR4A = val; // set pwm duty
break;
#endif
#if defined(TCCR4A) && defined(COM4B1)
case TIMER4B:
// connect pwm to pin on timer 4, channel B
sbi(TCCR4A, COM4B1);
OCR4B = val; // set pwm duty
break;
#endif
#if defined(TCCR4A) && defined(COM4C1)
case TIMER4C:
// connect pwm to pin on timer 4, channel C
sbi(TCCR4A, COM4C1);
OCR4C = val; // set pwm duty
break;
#endif
#if defined(TCCR4C) && defined(COM4D1)
case TIMER4D:
// connect pwm to pin on timer 4, channel D
sbi(TCCR4C, COM4D1);
#if defined(COM4D0) // only used on 32U4
cbi(TCCR4C, COM4D0);
#endif
OCR4D = val; // set pwm duty
break;
#endif
#if defined(TCCR5A) && defined(COM5A1)
case TIMER5A:
// connect pwm to pin on timer 5, channel A
sbi(TCCR5A, COM5A1);
OCR5A = val; // set pwm duty
break;
#endif
#if defined(TCCR5A) && defined(COM5B1)
case TIMER5B:
// connect pwm to pin on timer 5, channel B
sbi(TCCR5A, COM5B1);
OCR5B = val; // set pwm duty
break;
#endif
#if defined(TCCR5A) && defined(COM5C1)
case TIMER5C:
// connect pwm to pin on timer 5, channel C
sbi(TCCR5A, COM5C1);
OCR5C = val; // set pwm duty
break;
#endif
case NOT_ON_TIMER:
default:
if (val < 128) {
digitalWrite(pin, LOW);
} else {
digitalWrite(pin, HIGH);
}
}
}
}
위의 함수에서 보이듯 입력받는 value는 정수형으로 전달이 되어야 하기 때문에 정확한 50% Duty비를 구현하지 못합니다. 그에 근사한 127이나 128을 입력하여 출력할 수 있습니다.
3. Example
PWM 제어는 대부분 모터를 제어하는데 많이 쓰입니다. 하지만 모터 제어에 대해서는 뒤에서 좀 더 자세하게 다루고 오늘은 LED를 이용해서 PWM 제어 실습을 해보도록 하겠습니다. LED에 PWM을 이용하여 Duty비를 조정했을 때, 출력 전압이 달라지기 때문에 밝기가 변하는 것을 볼 수 있습니다. 우선 코드 먼저 확인하시죠.
#define LEDPin 10
int value = 0;
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
for (value = 0; value < 255; value += 25) {
analogWrite(LEDPin, value); // value 값 만큼 PWM 출력
delay(1000); // 1초 지연
}
for (value = 255; value > 0; value -= 25) {
analogWrite(LEDPin, value); // value 값 만큼 PWM 출력
delay(1000); // 1초 지연
}
}
위의 코드의 구조는 익숙한 구조처럼 보일 것입니다. 그 이유는 이전 실습 때 Servo Motor를 제어할 때 비슷한 방식으로 구현을 했죠. 방식은 동일합니다. 모터 출력을 25 값 씩 증가시켜나갔다가 25씩 감소시키는 코드입니다. 여기서 25의 값이란 Timer/Counter에서의 25 값으로 duty비를 의미하는 것이 아닙니다. 여기서 각 값이 의미하는 duty비를 구하려면, Timer/Counter의 Max값인 255만큼 나누고 100을 곱해주면 어렵지 않게 구할 수 있습니다. 우리가 증가시켜주는 25의 값은 duty비로 환산했을 때, (25 / 255) * 100 = 9.8039 % 입니다. 즉, 25씩 증가시킨다는 것은 duty비를 약 9.8%씩 증가시키는 것으로 5V 시스템이라 가정하였을 때, 5 * 0.098039 = 0.490195 V 씩 증가합니다. 각 반복문이 돌면서 LED에 PWM으로 인가되는 전압이 달라지므로 밝기가 밝아졌다 어두워졌다 변화를 볼 수 있습니다.
회로는 위와 같이 구성해주시면 됩니다.
다음 시간에는 모터의 구동 원리를 알아보고 PWM을 이용하여 모터를 구동해보는 시간을 갖도록 하겠습니다.