Jetson/Gimbal_with_Jetson

PWM제어 with Jetson Nano 2)

찬영_00 2024. 11. 27. 18:19

코드에 대해서 심층 분석하기 위해 포스팅을 한다.

내 공부를 위해 정리하면서 하는 포스팅이니 틀린부분이 있으면 댓글로 알려주면 감사하다.

 

먼저 저번 블로그의 메인함수부터 보겠다.

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;
}

 

 

이 함수는 값을 한 범위에서 다른 범위로 선형 변환하는 함수이다. (맵핑함수)

인자 설명:

  1. x: 변환하려는 원본 값입니다. 이 값은 in_min과 in_max 사이에 있어야 함
  2. in_min: 원본 범위의 최소값입니다. x 값은 이 값 이상이어야 함
  3. in_max: 원본 범위의 최대값입니다. x 값은 이 값 이하이어야 함
  4. out_min: 변환된 값이 속할 새로운 범위의 최소값
  5. 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);

 

변환 과정:

  1. x - in_min = 50 - 0 = 50
  2. (in_max - in_min) = 100 - 0 = 100
  3. (x - in_min) / (in_max - in_min) = 50 / 100 = 0.5
  4. (out_max - out_min) = 255 - 0 = 255
  5. (x - in_min) * (out_max - out_min) = 50 * 255 / 100 = 127.5
  6. + 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초간격으로 움직이는 코드이다.

이 코드에 사용되는 라이브러리는 다음 포스팅에서 심층분석을 해보겠다.