PWM제어 with Jetson Nano 2)
코드에 대해서 심층 분석하기 위해 포스팅을 한다.
내 공부를 위해 정리하면서 하는 포스팅이니 틀린부분이 있으면 댓글로 알려주면 감사하다.
먼저 저번 블로그의 메인함수부터 보겠다.
https://github.com/PCY00/Ubicomp_Lab/blob/main/AIoT/U-Neck/MotorControl/Servo/V1/servoExample.cpp
Ubicomp_Lab/AIoT/U-Neck/MotorControl/Servo/V1/servoExample.cpp at main · PCY00/Ubicomp_Lab
started 2022.06. Contribute to PCY00/Ubicomp_Lab development by creating an account on GitHub.
github.com
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include "JHPWMPCA9685.h"
위 헤더 파일들이 왜 사용되는지 분석해보자
설명 | |
stdio.h, stdlib.h, string,h | 기본 입출력, 메모리 조작, 문자열 처리를 위한 라이브러리 |
termios.h | 터미널 설정을 제어하기 위해 사용 |
time.h | 시간 관련 함수 제공 (sleep 사용) |
JHPWMPCA9685.h | PCA9685 제어를 위한 커스텀 라이브러리 |
int servoMin = 120; // Minimum pulse length
int servoMax = 720; // Maximum pulse length
먼저 본인이 사용하는 모터의 PWM의 주기를 알아내야한다.
현재 내 모터는 180각도를 가지고 있고, 500 ~ 2500us의 주기를 갖는다.
여기서 궁금한 점이 생길 수 있다.
각도랑 펄스 주기 안에서의 high값이랑 무슨 관계인가?
https://blog.naver.com/yuyyulee/220345769854
[아두이노 강좌] 40. 서보 모터 (1) - 서보 모터 동작 방식
모터 시리즈 중 마지막인 서보 모터. 뭔가 힘든 여정이었음. 서보 모터는 정확한 각도 회전을...
blog.naver.com
우리는 위 블로그를 통해 자세히 알수 있다. (서보모터의 동작 방식에 대해 알아야한다.)
대충 요약하면 펄스 주기 안에서 high가 얼마나 유지되는지에 따라 서보 모터의 각도가 정해져있다.
따라서 내 모터는 500us = 0도, 1500us는 90도 2500us는 180도를 갖는다.
따라서 내 모터의 값을 아래처럼 계산이 가능하다.
int servoMin = 102; // 500 µs에 해당하는 PCA9685 값
int servoMax =512; // 2500 µs에 해당하는 PCA9685 값
PCA9685에 맞게 설정하려면, 해당 펄스 폭을 12비트 해상도(0~4095)에 매핑해야한다.
PCA9685에서의 변환
PCA9685의 주기 (50Hz 기준):1 사이클 주기 = 20ms (20000 µs).
해상도 = 4096 단계.
펄스 폭을 PCA9685 값으로 변환
500 µs → (500 / 20000) * 4096 ≈ 102
2500 µs → (2500 / 20000) * 4096 ≈ 512
이전 포스팅에서의 값은 테스트용으로 해본거기때문에 이 값이 맞다.
int getkey() {
int character;
struct termios orig_term_attr;
struct termios new_term_attr;
/* set the terminal to raw mode */
tcgetattr(fileno(stdin), &orig_term_attr);
memcpy(&new_term_attr, &orig_term_attr, sizeof(struct termios));
new_term_attr.c_lflag &= ~(ECHO | ICANON);
new_term_attr.c_cc[VTIME] = 0;
new_term_attr.c_cc[VMIN] = 0;
tcsetattr(fileno(stdin), TCSANOW, &new_term_attr);
/* read a character from the stdin stream without blocking */
/* returns EOF (-1) if no character is available */
character = fgetc(stdin);
/* restore the original terminal attributes */
tcsetattr(fileno(stdin), TCSANOW, &orig_term_attr);
return character;
}
위 함수는 키보드 입력을 비동기적으로 처리하는 함수이다.
사용자가 키를 누르는 동안 프로그램이 멈추지 않고 다른 작업을 계속 수행할 수 있게 한다.
터미널 입력 설정을 일시적으로 변경하여 키 입력을 처리하고, 작업이 끝난 후 원래 상태로 복원한다.
int character;
struct termios orig_term_attr;
struct termios new_term_attr;
설명 | |
character | 입력된 키를 저장하는 변수 |
orig_term_attr | 원래 터미널 속성을 저장하는 구조체 |
new_term_attr | 입력 모드를 설정하기 위해 수정된 터미널 속성을 저장하는 구조체 |
termios의 구조체는 termios.h안에 정의되어있다.
tcgetattr(fileno(stdin), &orig_term_attr);
tcgetattr는 터미널의 현재 속성을 가져오는 함수이다.
따라서 이 코드는 현재 터미널의 속성을 orig_term_attr에 저장한다. ( 복원할 때 사용 )
memcpy(&new_term_attr, &orig_term_attr, sizeof(struct termios));
new_term_attr.c_lflag &= ~(ECHO | ICANON);
new_term_attr.c_cc[VTIME] = 0;
new_term_attr.c_cc[VMIN] = 0;
memcpy는 orig_term_attr 터미널 속성을 복사하여 new_term_attr에 저장한다.
- new_term_attr.c_lflag &= ~(ECHO | ICANON);
- ECHO: 입력된 문자를 터미널에 출력하는 플래그. 끄면 사용자가 입력한 문자가 화면에 표시되지 않음
- ICANON: 표준 모드(Canonical mode) 플래그. 끄면 입력 버퍼 없이 즉시 키 입력을 처리함
- new_term_attr.c_cc[VTIME] = 0;: 타이머 값을 0으로 설정(입력 대기 시간을 0으로 설정)
- new_term_attr.c_cc[VMIN] = 0;: 최소 입력 문자를 0으로 설정(입력이 없더라도 즉시 반환)
tcsetattr(fileno(stdin), TCSANOW, &new_term_attr);
character = fgetc(stdin);
tcsetattr(fileno(stdin), TCSANOW, &orig_term_attr);
return character;
tcsetattr: 터미널 속성을 설정하는 함수
- 첫 번째 매개변수: 파일 디스크립터 (fileno(stdin))
- 두 번째 매개변수: 즉시 속성을 적용하기 위한 플래그 (TCSANOW)
- 세 번째 매개변수: 설정할 속성 구조체(new_term_attr)
fgetc(stdin): 표준 입력에서 한 문자를 읽는다.
- 비동기 처리: VMIN=0으로 설정되어 있으므로 키 입력이 없을 경우 즉시 **EOF(-1)**를 반환
- 키 입력이 있다면 해당 ASCII 값을 반환
원래의 터미널 속성(orig_term_attr)을 복원하여, 프로그램 종료 후 터미널이 원래 상태로 돌아오도록 설정.
마지막으로
- character: 입력된 키의 ASCII 값
- 입력이 없으면 **-1 (EOF)**를 반환
이렇게 된다.
따라서 요약하면 이 함수는 터미널의 입력을 "비동기적"으로 처리하기 위해 터미널 설정을 일시적으로 변경한다.
// Map an integer from one coordinate system to another
int map(int x, int in_min, int in_max, int out_min, int out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
이 함수는 값을 한 범위에서 다른 범위로 선형 변환하는 함수이다. (맵핑함수)
인자 설명:
- x: 변환하려는 원본 값입니다. 이 값은 in_min과 in_max 사이에 있어야 함
- in_min: 원본 범위의 최소값입니다. x 값은 이 값 이상이어야 함
- in_max: 원본 범위의 최대값입니다. x 값은 이 값 이하이어야 함
- out_min: 변환된 값이 속할 새로운 범위의 최소값
- out_max: 변환된 값이 속할 새로운 범위의 최대값
예를 들어 이해를 돕자
int x = 50; // 원본 값
int in_min = 0; // 원본 범위의 최소값
int in_max = 100; // 원본 범위의 최대값
int out_min = 0; // 출력 범위의 최소값
int out_max = 255; // 출력 범위의 최대값
int result = map(x, in_min, in_max, out_min, out_max);
변환 과정:
- x - in_min = 50 - 0 = 50
- (in_max - in_min) = 100 - 0 = 100
- (x - in_min) / (in_max - in_min) = 50 / 100 = 0.5
- (out_max - out_min) = 255 - 0 = 255
- (x - in_min) * (out_max - out_min) = 50 * 255 / 100 = 127.5
- + out_min = 127.5 + 0 = 127.5
result는 127(정수 값)로 변환됩니다. 이제 x = 50은 새로운 범위인 0~255에서 127에 해당하는 값으로 매핑된다.
int main() {
PCA9685 *pca9685 = new PCA9685();
int err = pca9685->openPCA9685();
if (err < 0) {
printf("Error: %d\n", pca9685->error);
} else {
printf("PCA9685 Device Address: 0x%02X\n", pca9685->kI2CAddress);
pca9685->setAllPWM(0, 0);
pca9685->reset();
pca9685->setPWMFrequency(60); // Set frequency to 60 Hz
printf("Press ESC key to exit\n");
while (pca9685->error >= 0 && getkey() != 27) {
// Move to 120 degrees
int position120 = map(120, 0, 180, servoMin, servoMax);
pca9685->setPWM(0, 0, position120);
printf("Servo moved to 120 degrees\n");
sleep(2);
// Move back to 0 degrees
int position0 = map(0, 0, 180, servoMin, servoMax);
pca9685->setPWM(0, 0, position0);
printf("Servo moved back to 0 degrees\n");
sleep(2);
}
// Stop the servo when exiting
pca9685->setPWM(0, 0, 0);
sleep(1);
}
pca9685->closePCA9685();
}
메인 함수이다. 자세히 살펴보자.
PCA9685 *pca9685 = new PCA9685();
PCA9685 객체를 동적으로 생성합니다. 이는 PWM 제어를 위한 라이브러리에서 제공하는 클래스이다
int err = pca9685->openPCA9685();
if (err < 0) {
printf("Error: %d\n", pca9685->error);
}
- openPCA9685() 메서드는 I2C 버스를 열고, PCA9685 장치를 연결합니다. 이 함수는 I2C 버스를 열고 장치 주소로 통신을 설정한다.
- 실패할 경우, 에러 코드가 출력된다
pca9685->setAllPWM(0, 0);
pca9685->reset();
pca9685->setPWMFrequency(50);
- setAllPWM(0, 0): 모든 채널의 PWM을 0으로 설정하여 초기화한다.
- reset(): PCA9685를 초기화합니다. 모듈을 초기 상태로 설정한다.
- setPWMFrequency(50): PWM 주파수를 50Hz로 설정한다. 이 값은 서보 모터의 동작에 적합한 값으로, 보통 서보 모터는 50Hz 주파수로 작동한다
printf("Press ESC key to exit\n");
while (pca9685->error >= 0 && getkey() != 27) {
// Move to 120 degrees
int position120 = map(120, 0, 180, servoMin, servoMax);
pca9685->setPWM(0, 0, position120);
printf("Servo moved to 120 degrees\n");
sleep(2);
// Move back to 0 degrees
int position0 = map(0, 0, 180, servoMin, servoMax);
pca9685->setPWM(0, 0, position0);
printf("Servo moved back to 0 degrees\n");
sleep(2);
}
- 이 부분은 사용자가 ESC 키를 눌러 종료할 때까지 서보 모터를 제어하는 루프이다
- getkey() 함수는 사용자가 키를 입력할 때까지 대기합니다. ESC 키는 27에 해당한다
- 루프 내에서:
- 120도로 서보 모터를 이동시키기 위해 map() 함수를 사용하여 120도를 PWM 범위로 변환하고, setPWM() 메서드를 통해 해당 PWM 값을 설정한다
- 0도로 서보 모터를 이동시키기 위해 다시 map()을 사용하여 0도를 PWM 범위로 변환하고, setPWM() 메서드를 통해 값을 설정한다
- 각 동작 후 sleep(2)로 2초 동안 대기하여 서보 모터가 완전히 이동할 시간을 준다
위 코드에 pca9685->setPWM(0, 0, position120); 이 부분에 대해 조금 더 자세히 설명해 보겠다.
pca9685->setPWM(channel, onValue, offValue)
로 구성되어 있다.
근데 여기서 채널은 0~15개중 0번인건 알겠는데 onValue, offValue가 무엇인가?
onValue = 0: 이 값은 PWM 신호가 HIGH 상태인 시간이 0인 상태를 의미
즉, 서보 모터의 HIGH 상태가 없다는 뜻이 아닌 이 값은 해당 채널에서 상태 전환의 시작을 나타내는 값임
offValue = position120: position120은 서보 모터가 120도에 도달하기 위해 필요한 "off" 시간을 계산한 값임
이 값은 서보가 120도 위치로 이동하기 위해 필요한 펄스 폭을 설정하는데 사용됨
이때 "off" 값이 사용되지만, 이는 실제 PWM 주기의 HIGH/LOW 비율을 맞추기 위한 설정이기 때문에 서보 모터가 이동하는 각도와 관련이 있음
중요한 포인트
onValue = 0은 HIGH 상태를 제어하는 값이 아니라, PWM 신호가 시작되는 시점을 설정하는 값
position120은 서보 모터의 120도 위치를 맞추기 위해 설정된 펄스 값, 이 값은 주어진 주기 내에서 HIGH 상태의 지속 시간을 제어하게 됨
다시 한번 정리하면,
onValue가 0이고 offValue가 서보모터 각도이면 한 주기 (20ms)가 시작되었을 때 이 부분을 0으로 설정하고, offValue의 값까지 High로 흘러가다가 offValue 값에 도달하면 Low로 떨어진다는 이야기이다.
따라서 High가 시작된 부분을 0의 시간으로 설정, 이후 offValue값까지 High유지 후 Low로 떨어진다.
즉, 주기 내에서 HIGH가 유지되는 시간은 offValue 가 결정하며, LOW 상태는 onValue 와 offValue 설정에 따라 다르게 분포한다.
이렇게 main함수를 자세히 알아보았다.
이 코드는 그저 0도와 120도를 2초간격으로 움직이는 코드이다.
이 코드에 사용되는 라이브러리는 다음 포스팅에서 심층분석을 해보겠다.