tm1638에는 버튼이 8개, 빨간색 LED가 8개, tm1637에 해당하는 4칸 디스플레이 2개가 붙어 있다. 작업에 유용할 것 같아서 구입했는데, tm1638 라이브러리에는 몇 개가 있다. 보편적인 tm1638 라이브러리 2개를 설치해봤는데, tm1638.h 헤더파일을 포함했다. 하나는 구체적인 사용법이 나와 있지 않은, 본격 개발자 위주의 라이브러리였고, 다른 하나는 자세한 사용방법을 인터넷에서 찾아볼 수 있었지만, 내가 원하는 사용방식, 그러니까 왼쪽 4칸 디스플레이와 오른쪽 4칸을 독립적으로 사용하는 방식을 해당 라이브러리에서 지원하지 않는 듯했다.
8칸 디스플레이를 일괄적으로 접근하는 tm1638 라이브러리 사용법은 아래의 링크를 참고하기 바란다.
https://www.instructables.com/Arduino-and-TM1638-LED-Display-Modules/
https://bbangpan.tistory.com/72
그러다 찾은 것이, tm1638lite라는 라이브러리다. 이 라이브러리는 디스플레이 한칸 한칸을 0부터 시작하는 인덱스 방식으로 접근할 수 있다. 라이브러리 설치는 아두이노 보드 매니저에서 TM1638lite를 찾아서 설치하면 된다.
Examples -> TM1638lite -> demo 예제를 찾아보면 다음과 같다.
#include <TM1638lite.h>
// I/O pins on the Arduino connected to strobe, clock, data
// (power should go to 3.3v and GND)
TM1638lite tm(4, 7, 8);
void setup() {
tm.reset();
tm.displayText("Eh");
tm.setLED(0, 1);
delay(2000);
tm.displayASCII(6, 'u');
tm.displayASCII(7, 'p');
tm.setLED(7, 1);
delay(2000);
tm.displayHex(0, 8);
tm.displayHex(1, 9);
tm.displayHex(2, 10);
tm.displayHex(3, 11);
tm.displayHex(4, 12);
tm.displayHex(5, 13);
tm.displayHex(6, 14);
tm.displayHex(7, 15);
delay(2000);
tm.displayText("buttons");
}
void loop() {
uint8_t buttons = tm.readButtons();
doLEDs(buttons);
}
// scans the individual bits of value
void doLEDs(uint8_t value) {
for (uint8_t position = 0; position < 8; position++) {
tm.setLED(position, value & 1);
value = value >> 1;
}
}
vcc를 아두이노 일반 5v에 꽂아도 상관없다. 3.3v와 5v의 차이는 디스플레이 밝기뿐이다.
위의 지시대로, strobe(STB), clock(CLK), data(DIO)를 각각 아두이노 4,7,8번 핀에 꽂고, vcc와 gnd를 적당히 꽂고 실행시키면 상기 그림처럼 나온다. 문자와 숫자가 나오고, 버튼을 누르면 해당 LED가 켜지는 것 정도다.
내가 사용하기를 바라는 방식은, 왼쪽 디스플레이 4칸에는 서보 모터 회전 각도(위치, 0~180)가 나오고, 오른쪽 4칸에는 해당 서보 모터에 흐르는 전류를 측정(5V, 0~2000mA)하여 보여주는 것이다.
이 라이브러리의 readButtons 함수가 재미있는 점은, 동시에 여러 버튼을 누를 경우, 이 정보를 1바이트 데이터에 담아 보내준다는 것이다. 이 1바이트가 10100011 이라면 오른쪽부터 1번, 2번, 6번, 8번 버튼이 ON 상태라는 뜻이다. 따라서 이 정보를 뽑아내려면 비트 연산을 해주면 된다. 위의 예제에서 doLEDs 함수에서 이 작업을 하고 있다.
이제 이 4칸 디스플레이를 개별적으로 접근하여 숫자를 보여주는 코드를 작성해보자.
#include <TM1638lite.h>
TM1638lite tm(4, 7, 8); // strobe, clock, data
void setup() {
Serial.begin(9600);
tm.reset();
}
void loop() {
for (int i = 0; i<=180; i++) {
displayDigits(i, 0);
delay(10);
}
displayDigits(0, 0);
for (int i = 0; i<= 2000; i++) {
displayDigits(i, 1);
delay(1);
}
}
int int_pow(int base, int exp_) { // (10, 3) = 1000
int result = 1; // 1
for (int i = exp_; i > 0; i--) {
result *= base; // 1 * 10(i=3); 10 * 10(i=2); 100* 10(i=1)
}
return result;
}
// which : 0 (0-3 segments), 1 (4-7 segments), 앞에 0을 붙여서 4자리 숫자로 출력
void displayDigits(int digit, int which) {
int q = 0, r = digit; // quotient, reminder
for (int i = 3; i >= 0; i--) {
q = r / int_pow(10, i);
tm.displayHex(3 - i + (4*which), q);
r = r % int_pow(10, i);
}
}
실행하면, 아래와 같다. 왼쪽에 116이 보이고, 오른쪽에 1100이 보인다. 이건 캡쳐 순간이 겹친 듯하고, 실제로는 왼쪽와 오른쪽 숫자가 번갈아 오름차순으로 올라간다.
코드 설명을 잠깐 한다.
먼저 TM1638lite 객체를 만들어야 한다. 그리고 setup 과정에서 reset한다.
loop() 함수를 보면,
0에서 180까지 숫자를 올리면서, displayDigits 함수를 호출하면서, 그 숫자 i(0~180)와 0을 인자로 주고 있다. 이 함수는 필자가 짠 것으로, 숫자 i는 출력할 숫자, 뒤의 0은 디스플레이 번호다. 즉 0번은 왼쪽 4칸, 1은 오른쪽 4칸을 이른다.
이 작업이 끝나면,
displayDigits(0, 0)으로 왼쪽 디스플레이에 0000을 출력해준 다음, 오른쪽 디스플레이에서도 비슷한 작업을 해준다.
displayDigits() 함수는 인자로 들어온 숫자를 해당 기판(which)에 출력하는 역할을 하는 중요한 함수다.
일례로 displayDigits(123, 0)을 호출했다고 해보자. 123이라는 숫자를 왼쪽 4칸에 0123 하는 식으로 출력하는 것이다. 여기서는 빈 자리수를 0으로 채운다. 4칸 디스플레이에서 왼쪽부터 0, 1, 2, 3이라는 위치 인덱스를 부여하도록 하자.
int q = 0, r = digit;
for (int i = 3; i >= 0; i--) {
q = r / int_pow(10, i);
tm.displayHex(3 - i + (4*which), q);
r = r % int_pow(10, i);
q 는 몫이고, r은 나머지다.
123 / 1000 = 0 (몫) 출력
123 % 1000 = 123 (나머지, 이어 작업할 대상값이 됨)
123 / 100 = 1 (몫) 출력
123 % 100 = 23 (나머지, 이어 작업할 digit이 됨)
23 / 10 = 2 (몫) 출력
23 % 10 = 3 (나머지)
3 / 1 = 3 (몫) 출력
3 % 1 = 0 ( 이 마지막 부분은 사용하지 않음)
결과적으로 몫에 해당하는 0, 1, 2, 3이 왼쪽 4칸에서 왼쪽부터 차례대로 출력됨.
displayHex가 해당 라이브러리에서 제공하는, 개별 세그먼트 1개에 접근하여 숫자를 출력토록 하는 함수이고, 그 안에서는 출력할 인덱스를 계산하고 있다.
여기서 중요한 점이 한 가지 있는데, pow 함수 관련해서, 아두이노 pow 함수는 double 형 데이터 타입을 취급하는데, floating 오차가 발생해서 정수 데이터를 정확하게 다루지 못한다는 점이다. 이게 아두이노 C++의 문제인지 아니면 데스크탑 C++에서도 나타나는 문제인지는 확인해보지 않았다. 그래서 할 수 없이 정수만을 다루는 int_pow 함수를 만들어서 사용했다.
여기까지가 기본적인 사용례이고, 여기서부터 응용을 해나가면 될 것이다.
이제 수치가 기준값을 넘어가면 LED등을 켜고 끄는 것을 시도해보자. 서보 모터의 전류 부하가 위험 수준으로 높아지면 경고등을 켜는 기능을 위한 준비 운동이다.
// 수치가 기준값을 넘어가면 경고 LED를 켠다. 낮아지면 끈다.
// 버튼을 누르면 상응하는 일을 한다.
#include <TM1638lite.h>
TM1638lite tm(4, 7, 8); // strobe, clock, data
//TM1638lite tm(15, 2, 4); // strobe, clock, data for esp32
//TM1638lite tm(4, 16, 17); // strobe, clock, data for esp32
void setup() {
Serial.begin(9600);
tm.reset();
}
int pos = 0; // 각도
int current = 0; // mA
bool warn_pos = false;
bool warn_current = false;
uint8_t buttons = 0;
void loop() {
displayDigits(pos, 0);
pos++;
if (pos > 180) {
pos = 0;
}
if (pos > 150) {
warn_pos = true;
} else if (pos <= 150) {
warn_pos = false;
}
if (warn_pos) {
tm.setLED(0, 1);
} else {
tm.setLED(0, 0);
}
displayDigits(current, 1);
current++;
if (current > 1100) {
current = 0;
}
if (current > 700) { // 과부하 기준점
warn_current = true;
} else {
warn_current = false;
}
if (warn_current) {
tm.setLED(1, 1);
} else {
tm.setLED(1, 0);
}
buttons = tm.readButtons();
if (buttons & 1 == 1) { // 1번 버튼을 눌렀다면, LED 모두 끄고, 수치 초기화
for (int i = 0; i < 8; i++) {
tm.setLED(i, 0);
}
displayDigits(0, 0);
displayDigits(0, 1);
delay(1000);
} else if ((buttons >> 1) & 1 == 1) { // 2번 버튼을 눌렀다면, 메시지 출력
tm.displayText("Helowrld");
delay(1000);
} else if ((buttons >> 2) & 1 == 1) { // 3번 버튼을 눌렀다면, 메시지 출력
tm.displayText("Love you");
delay(1000);
} else if ((buttons >> 7) & 1 == 1) { // 8번 버튼을 눌렀다면, 디스플레이 리셋
tm.reset();
}
delay(1);
}
int int_pow(int base, int exp_) { // (10, 3) = 1000
int result = 1; // 1
for (int i = exp_; i > 0; i--) {
result *= base; // 1 * 10(i=3); 10 * 10(i=2); 100* 10(i=1)
}
return result;
}
// which : 0 (0-3 segments), 1 (4-7 segments), 앞에 0을 붙여서 4자리 숫자로 출력
void displayDigits(int digit, int which) {
int q = 0, r = digit; // quotient, reminder
for (int i = 3; i >= 0; i--) {
q = r / int_pow(10, i);
tm.displayHex(3 - i + (4*which), q);
r = r % int_pow(10, i);
}
}
* 참고로 이 코드는 esp32 보드에서도 실행된다. 컴파일 와중에 이 라이브러리가 아두이노 계열을 위한 것이니 조심하라는 경고 메시지는 떠지만 제대로 esp32에서도 실행은 된다.
서보 모터를 로봇암에 넣어 작업 중인데, 서보 모터가 특정 각도를 넘어가면 과부하가 발생하는 지점이 있다. 이 경고를 warn_pos 변수에 담았고, warn_current는 전류량이 과부하로 들어섰을 때 켜진다. 여기서는 pos가 150보다 크면 1번 LED 등을 켜고, current가 700을 넘으면 2번 LED를 켜도록 했다. 버튼을 눌렀을 때 작동하는 기능은 심심해서 넣어본 것이니 특별한 의미는 없다.
실행 결과는 다음과 같다.
제법 유용하게 사용될 녀석인 것 같다.