본문 바로가기
임베디드 개발/아두이노

Noctua 4-pin PWM fan 제어하기 with 아두이노

by eteo 2024. 7. 22.

 

 

 

Datasheet

 

Noctua_PWM_specifications_white_paper.pdf
0.67MB

 

 

 


 

 

FAN Model & Specification

 

NOCTUA NF-P14s redux-1200 PWM

 

 

 

 


 

 

Wiring

 

 

 

Blue 라인을 통해 PWM으로 속도제어를 할 수 있고, Green 라인을 통해 타코미터 신호를 받아 팬 속도를 알 수 있다.

 

 

 

 

 


 

 

 

PWM Speed Control

 

일단 PWM 신호 핀 없이 +12V랑 GND를 연결하면 팬이 최고속도로 도는데 아마도 PWM signal 입력을 받는쪽이 내부적으로 풀업이 되있나보다.

 

 

PWM 주파수는 25kHz이고 듀티를 0~100% 사이로 제어하면 된다.

처음엔 digitalWrite랑 delayMicroseconds를 써서 PWM을 만들까 했는데 25kHz는 40us정도니까 delayMicroseconds로 듀티를 세밀하게 제어하기 힘들 것 같아서 타이머와 PWM 출력핀을 쓰기로 했다.

 

 

 

 

 

 

 

팬의 속도는 PWM 신호의 듀티 사이클에 따라 거의 선형적으로 증가한다. 단 듀티가 20% 미만일 때는 출력 속도가 어떻게 된다고 딱 정의되지 않는다.

내가 구입한 팬은 최고 속도가 1200 RPM이고 미니멈이 350 RPM이니 듀티 30% 이상부터는 속도가 선형적으로 제어가될거다.

 

 

 

아두이노 GPIO 입출력 레벨은 5V라서 바로 연결하면 되겠다.

 

 

아두이노에 쓰이는 ATmega328P에는 8비트 타이머 2개와 16비트 타이머 1개가 있는데 그 중 타이머0은 delay() 등 시스템 함수에 사용되고 여기서는 PWM 신호 생성을 위해 16비트 타이머인 타이머0을 쓰기로한다.

 

타이머들은 16MHz 시스템클럭을 베이스로 사용하기 때문에 분주하지 않고 640까지 카운트하도록 설정하면 25kHz의 주기를 얻어낼 수 있다.

 

아래는 타이머1을 Fast PWM 모드로 설정하고 주파수를 25kHz로 설정한 뒤 비교 출력핀인 9번핀을 활성하하고 듀티는 0으로 초기화하는 코드이다.

 

참고로 타이머마다 비교 출력 핀이 2개씩 있고 16비트 타이머니까 ICRx와 OCRxA는 16비트 레지스터이다. 그리고 ATmega328P에도 쉐도우 레지스터가 있는지 OCR 값이 변경될 때 바로 PWM 출력에 반영되지 않고 다음 주기부터 변경된 값이 반영된다고 한다.

  // 타이머1 설정
  TCCR1A = 0; // 초기화
  TCCR1B = 0; // 초기화
  TCCR1A = (1 << WGM11);  // Fast PWM 모드
  TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10); // 프리스케일러 1
  ICR1 = 640; // 16MHz / 640 = 25kHz
  OCR1A = 320;  // 초기 듀티 50%
  TCCR1A |= (1 << COM1A1);  // OC1A핀 (9번핀) 활성화

 

 

이후 Output Compare Register 값을 변경하여 듀티 사이클을 제어할 수 있다.

OCR1A = map(adcValue, 0, 1023, 0, 640);

 

 

 

 

 

 

 

 


 

 

 

Tachometer Signal Mornitoring

 

타코미터 시그널은 1회전당 두 번의 펄스가 출력되고 오픈 컬렉터 타입 출력이기 때문에 아두이노 5V 쪽에 2.7k옴 이상으로 풀업해주면 된다.

 

 

아두이노 외부 인터럽트는 2번핀 또는 3번핀이 사용 가능한데 여기선 2번핀을 사용했고 Rising edge에 인터럽트가 트리거되도록 설정한다.

  // 외부 인터럽트 설정
  pinMode(extiPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(extiPin), extiISR, RISING);

 

void extiISR() {
  static uint32_t lastTime = 0;
  uint32_t currentTime = micros();
  if(currentTime - lastTime >= 20000) {
    pulseCount++;
    lastTime = currentTime;
  }
}

 

프로그램이 시작한 이후 경과한 시간을 마이크로초로 반환하는 micros() 함수를 사용해 Rising edge 간의 간격을 측정하면 펄스의 주파수를 알 수 있고, 펄스가 1회전당 두번 튀니까 1초에 몇 회전하는지 알 수 있다. 다만 어차피 그렇게 세밀하게 보려는 건 아니어서 외부인터럽트 콜백함수 내에서는 pulseCount만 증가 시키고 loop문 내에서 1초에 한번 pulseCount 횟수를 확인해 속도를 계산한 뒤 LCD에 표시하는 방법을 사용했다.

 

그리고 고주파로 갈수록 생각보다 노이즈가 많이타는거 같아서 노이즈를 무시하기 위한 코드를 추가했다. 팬이 가장 빠르게 돌 때 펄스 간격이 25ms 정도여서 20ms 이내에 트리거된 경우 잡음으로 보고 무시하기로 한다. 

 

 

 


 

 

Code

#include <LiquidCrystal_I2C.h> // I2C 인터페이스 1602 LCD를 제어하기 위해 라이브러리 포함

LiquidCrystal_I2C lcd(0x27, 16, 2); // I2C slave address는 0x3F 또는 0x27

const int pwmPin = 9; // 타이머1의 PWM 9번핀 사용
const int extiPin = 2;  // interrupt 0이랑 연결된 2번핀 사용
volatile uint32_t pulseCount = 0;

void setup() {
  // UART 설정
  Serial.begin(115200);  

  // LCD 초기화
  lcd.clear();
  lcd.init();
  lcd.backlight();

  // PWM 설정 설정
  pinMode(pwmPin, OUTPUT);
  TCCR1A = 0; // 타이머1 레지스터 초기화
  TCCR1B = 0; // 타이머1 레지스터 초기화
  TCCR1A = (1 << WGM11);  // Fast PWM 모드
  TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS10); // 프리스케일러 1
  ICR1 = 640; // 16MHz / 640 = 25kHz
  OCR1A = 0;  // 초기 듀티 0%
  TCCR1A |= (1 << COM1A1);  // OC1A핀 (9번핀) 활성화

  // 외부 인터럽트 설정
  pinMode(extiPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(extiPin), extiISR, RISING);
}

void loop() {
  static uint32_t lastTick = 0;
  char buf[17];

  uint32_t currentTick = millis();
  if(currentTick - lastTick >= 1000) {
    Serial.println(pulseCount);
    sprintf(buf, "SPEED : %4d RPM", round(pulseCount * 60.0 / 2));
    pulseCount = 0;
    lcd.setCursor(0, 1);
    lcd.print(buf);
    lastTick = currentTick;
  }

  // 가변저항 읽기
  int adcValue = analogRead(A3);
  
  // LCD 출력
  sprintf(buf, "PWM   : %4d %%", round((float)adcValue / 1024.0 * 100.0));
  lcd.setCursor(0, 0);
  lcd.print(buf);

  // PWM 듀티 사이클 제어
  OCR1A = map(adcValue, 0, 1023, 0, 640);
}

// 외부 인터럽트 콜백함수
void extiISR() {
  static uint32_t lastTime = 0;
  uint32_t currentTime = micros();
  if(currentTime - lastTime >= 20000) {
    pulseCount++;
    lastTime = currentTime;
  }
}