7.6 로우사이드 스위칭 -> 하이사이드 스위칭 변경 (N채널 모스펫 -> P채널 모스펫)

기존의 N채널 모스펫을 사용한 방식으로는 시동이 걸리지 않아서 조사를 해보니 애초에 잘못된 모스펫을 사용했음을 알게되었다.
현재 차량에서 사용하는 인젝터는 2핀(전원선과 신호선)을 사용하는 구조로 되어있다. 따로 (-)선이 없기 때문에 하이사이드 스위칭구조를 사용해야하는데, N채널 모스펫은 로우사이드 스위칭에 사용해야 했다. 따라서 P채널 모스펫을 이용하여 다시 회로를 구성하였다.
7.10 SSR
P채널 모스펫으로도 정상 작동하는 것을 확인하였지만 발열이 너무 심해 그냥 SSR을 사용하여 스위칭하기로 하였다.
최종 하드웨어 구성
아두이노 우노 R3 + L298N 모터드라이버 + SSR + LCD


차량에 실제로 적용한 모습이다. 좌측 상단부터 SSR, 모터드라이버, 아두이노가 배치되어있다.
코드
상태머신 도입
버튼 디바운스 함수를 적용했음에도 테스트 중 한 번의 버튼 입력에 대해 테스트용 LED가 여러 번 켜지는 현상이 발생했다. 회로도를 검토했지만 하드웨어상의 문제는 발견되지 않았다. 이에 네트워크 시간에 배운 상태 머신을 도입하여 버튼 입력의 무결성을 보장하도록 하였다. 또한 상태머신을 도입함으로써 동작 중에 차량 진동 등으로 의도치 않게 다른 버튼이 눌리더라도 이를 무시하도록 하여 안전하게 변속을 수행할 수 있도록 하였다.
최종 코드
#include <LiquidCrystal_I2C.h>
#define DEBOUNCE_DELAY 30 // 디바운스 지연 시간 조정
LiquidCrystal_I2C lcd(0x27, 16, 2);
// 출력 핀
const byte fuelCutPin = 5;
const byte shiftUpPin = 6;
const byte shiftDownPin = 7;
const byte l298n2_IN2 = 3;
const byte l298n2_IN4 = 4;
const byte wingMotorPinA = 13; //일단 비활성화
const byte wingMotorPinB = 13;
const byte wingMotorEN = 13;
// 입력 핀 (버튼)
const byte gearResetButtonPin = 8;
const byte shiftUpButtonPin = 9;
const byte shiftUpIdleButtonPin = 10;
const byte shiftDownButtonPin = 11;
const byte shiftDownIdleButtonPin = 12;
// --- 상태 머신(State Machine) 정의 ---
enum ShiftState {
S_IDLE, // 대기 상태
S_UP_SHIFT_FUEL_CUT, // 쉬프트업: 퓨얼컷 및 실린더 지연 시작
S_UP_SHIFT_CYLINDER_ACTIVE, // 쉬프트업: 실린더 동작
S_DOWN_SHIFT_FUEL_CUT, // 쉬프트다운: 퓨얼컷 및 실린더 동작
S_IDLE_UP_SHIFT_ACTIVE, // 공회전 쉬프트업: 실린더 동작
S_IDLE_DOWN_SHIFT_ACTIVE // 공회전 쉬프트다운: 실린더 동작
};
ShiftState currentShiftState = S_IDLE; // 현재 변속 상태 저장 변수
// --- Button 구조체 및 상태 변수 ---
struct Button {
const byte pin;
bool lastReading;
bool debouncedState;
unsigned long lastDebounceTime;
// Constructor
Button(byte p) : pin(p), lastReading(HIGH), debouncedState(HIGH), lastDebounceTime(0) {}
};
Button gearResetButton(gearResetButtonPin);
Button shiftUpButton(shiftUpButtonPin);
Button shiftUpIdleButton(shiftUpIdleButtonPin);
Button shiftDownButton(shiftDownButtonPin);
Button shiftDownIdleButton(shiftDownIdleButtonPin);
// --- 전역 변수 ---
int gearSequence[5] = {1, 0, 2, 3, 4}; // 현재 코드에서는 사용X
int gearIdx = 1;
// 아날로그 입력 값
unsigned int fuelCutTime = 0;
unsigned int cylinderTime = 0;
unsigned int cylinderDelay = 0;
// 타이머 관련 변수
unsigned long stateChangeTime = 0; // 상태가 변경된 시간 기록
unsigned long lastLcdUpdateTime = 0;
const unsigned int updateLCDInterval = 500;
// 리어윙 관련 변수
int speedValue = 0;
int speedThreshold = 60;
unsigned long wingMoveStartTime = 0;
const unsigned long wingMoveDuration = 1000;
bool wingIsUp = false;
bool wingMoving = false;
bool wingDirectionUp = false;
// --- 함수 선언 ---
void updateShiftController();
bool wasPressed(Button &btn);
void setup() {
Serial.begin(9600);
// LCD 초기화
lcd.begin();
lcd.backlight();
// 출력 핀 설정
pinMode(fuelCutPin, OUTPUT);
digitalWrite(fuelCutPin, HIGH); // 평소에는 전류 흐름 ON
pinMode(shiftUpPin, OUTPUT);
pinMode(shiftDownPin, OUTPUT);
pinMode(l298n2_IN2, OUTPUT);
pinMode(l298n2_IN4, OUTPUT);
pinMode(wingMotorPinA, OUTPUT);
pinMode(wingMotorPinB, OUTPUT);
pinMode(wingMotorEN, OUTPUT);
digitalWrite(wingMotorEN, HIGH);
// 입력 핀 설정
pinMode(gearResetButton.pin, INPUT_PULLUP);
pinMode(shiftUpButton.pin, INPUT_PULLUP);
pinMode(shiftUpIdleButton.pin, INPUT_PULLUP);
pinMode(shiftDownButton.pin, INPUT_PULLUP);
pinMode(shiftDownIdleButton.pin, INPUT_PULLUP);
stopCylinders();
}
void loop() {
unsigned long currentTime = millis();
// 아날로그 값 읽기
fuelCutTime = analogRead(A0);
cylinderTime = analogRead(A1);
cylinderDelay = analogRead(A2);
// 시리얼 입력 처리
if (Serial.available() > 0) {
speedValue = Serial.parseInt();
controlRearWing(speedValue);
Serial.print("Input Speed: ");
Serial.println(speedValue);
}
// 기어 리셋 버튼 처리
if (wasPressed(gearResetButton)) {
gearIdx = 1;
Serial.println("Gear Reset: 1단으로 변경됨");
}
// 상태 머신 업데이트
updateShiftController();
// 리어윙 모터 업데이트
updateWingMotor();
// LCD 업데이트
if (currentTime - lastLcdUpdateTime >= updateLCDInterval) {
updateLCD();
lastLcdUpdateTime = currentTime;
}
}
// ---변속 상태 머신 ---
void updateShiftController() {
unsigned long currentTime = millis();
switch (currentShiftState) {
case S_IDLE:
// 쉬프트업 버튼 감지
if (wasPressed(shiftUpButton) && gearIdx < 4) {
Serial.println("ShiftUp: Fuel Cut 시작");
digitalWrite(fuelCutPin, LOW); // 퓨얼컷 시작
stopCylinders();
stateChangeTime = currentTime;
currentShiftState = S_UP_SHIFT_FUEL_CUT;
}
// 쉬프트다운 버튼 감지
else if (wasPressed(shiftDownButton) && gearIdx > 0) {
Serial.println("ShiftDown: Fuel Cut 및 실린더 동작 시작");
digitalWrite(fuelCutPin, LOW); // 퓨얼컷 시작
shiftCylinderDown();
stateChangeTime = currentTime;
currentShiftState = S_DOWN_SHIFT_FUEL_CUT;
}
// 공회전 쉬프트업 버튼 감지
else if (wasPressed(shiftUpIdleButton) && gearIdx < 4) {
Serial.println("Idle ShiftUp: 실린더 동작 시작");
shiftCylinderUp();
stateChangeTime = currentTime;
currentShiftState = S_IDLE_UP_SHIFT_ACTIVE;
}
// 공회전 쉬프트다운 버튼 감지
else if (wasPressed(shiftDownIdleButton) && gearIdx > 0) {
Serial.println("Idle ShiftDown: 실린더 동작 시작");
shiftCylinderDown();
stateChangeTime = currentTime;
currentShiftState = S_IDLE_DOWN_SHIFT_ACTIVE;
}
break;
case S_UP_SHIFT_FUEL_CUT:
// 실린더 딜레이 시간이 경과하면
if (currentTime - stateChangeTime >= cylinderDelay) {
Serial.println("ShiftUp: 실린더 동작 시작");
shiftCylinderUp();
stateChangeTime = currentTime;
currentShiftState = S_UP_SHIFT_CYLINDER_ACTIVE;
}
// (퓨얼컷은 다음 상태에서도 계속 유지)
break;
case S_UP_SHIFT_CYLINDER_ACTIVE:
// 실린더 동작 시간이 경과하면
if (currentTime - stateChangeTime >= cylinderTime) {
Serial.println("ShiftUp: 완료, 기어 증가");
stopCylinders();
digitalWrite(fuelCutPin, HIGH); // 퓨얼컷 종료
if (gearIdx < 4) gearIdx++;
currentShiftState = S_IDLE;
}
break;
case S_DOWN_SHIFT_FUEL_CUT:
if (currentTime - stateChangeTime >= cylinderTime) {
Serial.println("ShiftDown: 완료, 기어 감소");
stopCylinders();
digitalWrite(fuelCutPin, HIGH); // 퓨얼컷 종료
if (gearIdx > 0) gearIdx--;
currentShiftState = S_IDLE;
}
break;
case S_IDLE_UP_SHIFT_ACTIVE:
// 실린더 동작 시간이 경과하면
if (currentTime - stateChangeTime >= cylinderTime) {
Serial.println("Idle ShiftUp: 완료, 기어 증가");
stopCylinders();
if (gearIdx < 4) gearIdx++;
currentShiftState = S_IDLE;
}
break;
case S_IDLE_DOWN_SHIFT_ACTIVE:
// 실린더 동작 시간이 경과하면
if (currentTime - stateChangeTime >= cylinderTime) {
Serial.println("Idle ShiftDown: 완료, 기어 감소");
stopCylinders();
if (gearIdx > 0) gearIdx--;
currentShiftState = S_IDLE;
}
break;
}
}
// 버튼이 눌렸는지(Falling Edge) 감지하는 함수
bool wasPressed(Button &btn) {
bool oldDebouncedState = btn.debouncedState;
bool reading = digitalRead(btn.pin);
if (reading != btn.lastReading) {
btn.lastDebounceTime = millis();
}
if ((millis() - btn.lastDebounceTime) > DEBOUNCE_DELAY) {
btn.debouncedState = reading;
}
btn.lastReading = reading;
return (oldDebouncedState == HIGH && btn.debouncedState == LOW);
}
void updateLCD() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("FuelCut:");
lcd.print(fuelCutTime);
lcd.setCursor(0, 1);
lcd.print("T:");
lcd.print(cylinderTime);
lcd.setCursor(7,1);
lcd.print("D:");
lcd.print(cylinderDelay);
lcd.setCursor(13,1);
lcd.print("G:");
lcd.print(gearIdx);
}
void controlRearWing(int spd) {
if (wingMoving) return;
if (spd >= speedThreshold && !wingIsUp) {
digitalWrite(wingMotorPinA, HIGH);
digitalWrite(wingMotorPinB, LOW);
wingMoveStartTime = millis();
wingMoving = true;
wingDirectionUp = true;
}
else if (spd < speedThreshold && wingIsUp) {
digitalWrite(wingMotorPinA, LOW);
digitalWrite(wingMotorPinB, HIGH);
wingMoveStartTime = millis();
wingMoving = true;
wingDirectionUp = false;
}
}
void updateWingMotor() {
if (wingMoving && millis() - wingMoveStartTime >= wingMoveDuration) {
digitalWrite(wingMotorPinA, LOW);
digitalWrite(wingMotorPinB, LOW);
wingMoving = false;
wingIsUp = wingDirectionUp;
}
}
void shiftCylinderUp() {
digitalWrite(shiftUpPin, HIGH);
digitalWrite(l298n2_IN2,LOW);
Serial.println("실린더 UP");
}
void shiftCylinderDown() {
digitalWrite(shiftDownPin, HIGH);
digitalWrite(l298n2_IN4, LOW);
Serial.println("실린더 DOWN");
}
void stopCylinders() {
digitalWrite(shiftUpPin, LOW);
digitalWrite(shiftDownPin, LOW);
digitalWrite(l298n2_IN2, LOW);
digitalWrite(l298n2_IN4, LOW);
Serial.println("실린더 STOP");
}
하드웨어 디버깅(?)
변속 버튼을 누르면 차량의 시스템 전체가 다운되어버리는 문제가 있었다. 원인 파악을 위해 하나씩 제거해보며 테스트 해보니 원인은 아래와 같았다.
1. 모터드라이버 L298N
모터드라이버 L298N에는 두개의 모터 제어를 위한 OUTPUT이 1부터 4까지 존재한다.
OUTPUT1: ShiftUp Relay
OUTPUT2: ShiftDown Relay
OUTPUT3: FuelCut Relay
이런식으로 회로를 구성했는데 정확한 원인은 모르겠으나 아무래도 2개의 모터 제어를 위한 회로로 릴레이 3개를 제어하려해서 문제가 발생한 것 같았다.
따라서
OUTPUT1: ShiftUp Relay
OUTPUT2: Ground
OUTPUT3: ShiftDown Relay
OUTPUT4: Ground
FuelCut Relay -> 아두이노 디지털 입력으로 제어
위와 같이 변경해주니 문제가 해결되었다.
2. 역기전력과 플라이백 다이오드
모터드라이버 쪽 문제를 해결한 뒤에도 변속시에 차량의 시스템이 간헐적으로 꺼지는 문제가 발생하였다. 원인을 파악하던중 평소에도 염두해두고 있던 역기전력이 문제이지 않을까 해서 창고에 굴러다니는 FR603 다이오드를 릴레이 코일에 병렬로 연결하여 테스트해보니 문제가 해결되었다.
절연처리


여태 아두이노를 날려먹은적이 한번도 없었는데, 실제 차량에 아두이노를 올려서 테스트하면서 순식간에 7대를 날려먹었다.
원인은 대부분 아두이노의 전원이 켜진상태로 절연처리가 제대로 되지 않은 부분이 차량의 금속부분과 닿아서 쇼트가 났거나 릴레이의 역기전력과 잘못된 모터드라이버 사용으로 인한 것으로 보인다. 앞으로는 실제로 테스트할때는 꼭 전원을 차단하고 절연처리를 꼼꼼히 해야겠다는 것을 뼈저리게 느낄 수 있었다.
고사
변속을 성공적으로 마치고 고사(?)를 지냈다.
아직 수정할게 많지만 어쨌든 실제로 차량에 적용되어 동작하는 아두이노를 보니 매우 뿌듯했다.

