- 위치 제어 시스템의 하드웨어 구성과 신호 흐름 설계
본 시스템은 STM32F103C8T6 마이크로컨트롤러를 기반으로 한 고정밀 위치 제어 시스템으로, 실시간 반응성과 높은 신뢰성을 갖춘 단일 입력 단일 출력(SISO) 피드백 제어 구조를 채택하였다. 목표 위치(펄스 수 기준)와 실제 인코더 측정값 간의 차이를 계산하여 위치식 PID 알고리즘을 통해 제어량을 산출하고, 이를 방향 제어 신호와 사전 정의된 사각영역 보정을 거쳐 TIM1_CH1에 대한 PWM 출력으로 변환한다. 이 과정은 전부 정수 연산으로 수행되며, 부동소수점 연산 없이도 밀리초 단위 응답 속도를 달성할 수 있다.
하드웨어 구성은 명확한 기능 분리 원칙을 따르며, 각 외부 장치는 고유한 역할을 맡아 신호 간 간섭을 최소화한다.
| 모듈 | 핀 할당 | 기능 설명 | 목적 |
|---|---|---|---|
| GPIOA | PA0, PA1 | 방향 제어 출력 (DIR_A, DIR_B) | H브릿지의 전도 순서 조절로 회전 방향 결정; 본 설계에서는 정방향/역방향만 사용 |
| GPIOA | PA5 | 실행 상태 표시용 LED | 주루프 내 100ms마다 전환되며, 카드 시스템 오류 발생 시 깜빡임 중단으로 진단 가능 |
| USART1 | PA9(TX), PA10(RX) | 상위기기 통신 포트 | 목표 회전 수 전달 및 현재 상태(현재 펄스, PID 출력, RPM 등) 실시간 전송; 익명 프로토콜 기반 |
| TIM1 | PA8(CH1) | 20kHz PWM 출력 | MOSFET 게이트 제어; 인간 청취 범위 초과로 소음 감소 및 전류 리플 최소화 |
| TIM2 | PA0(CH1), PA1(CH2) | 인코더 인터페이스 (4배주파수 모드) | 하드웨어 수준에서 A/B상 정교하게 해석하며, 소프트웨어 디저밍 필요 없음 |
시스템 클록 설정은 제어 주기 일관성을 보장하기 위해 세심하게 구성된다. 시스템 클럭(SYSCLK)은 72MHz이며, TIM1이 연결된 APB2는 동일한 72MHz, TIM2와 USART1이 연결된 APB1은 36MHz이다. TIM1의 타이머 기본 주기 설정은 20kHz를 유지하면서 충분한 해상도를 확보해야 하므로, PSC=0, ARR=3599로 설정하면 주기 약 50μs(20kHz)가 된다. TIM2는 36MHz 클럭을 내부 4분주 후 사용하여 정밀한 엣지 감지가 가능하다.
- 인코더 신호 처리: 4배주파수 기술과 정밀 측정
인코더는 위치 정보의 근원이다. 본 시스템은 오므론 E6B2-CWZ6C와 같은 증분형 정교 인코더를 사용하며, A/B 두 신호 사이의 90도 위상차를 이용해 회전 방향을 판단한다. 4배주파수 기법은 단순히 각 신호의 상승 에지만 세는 것이 아니라, 전체 네 가지 유효 에지(A↑, B↑, A↓, B↓)를 식별하여 각 기계 주기당 펄스 수를 원래 값의 4배로 늘린다.
STM32에서는 이 작업을 TIM2의 인코더 모드로 하드웨어적으로 처리한다:
- PA0, PA1을 복용 출력 모드로 설정하고, 내부 풀업 저항 활성화
- TIM2를 모드 3(모든 에지 감지)로 설정 → CNT 레지스터 자동 증감
- A가 앞서면 (정방향): 모든 에지에서 CNT 증가 (각 주기 +4)
- B가 앞서면 (역방향): 모든 에지에서 CNT 감소 (각 주기 -4)
이 방식은 소프트웨어 폴링에 의존하지 않아 CPU 점유율을 낮추고, 에지 손실을 방지한다. 그러나 중요한 점은 CNT 레지스터가 16비트 정수(-32768 ~ +32767)라는 점이다. 장시간 운전 시 오버플로우 발생 가능성이 있으므로, 매번 읽은 후 즉시 초기화해야 한다.
int16_t Encoder_Read(void)
{
int16_t val = (int16_t)TIM2->CNT;
TIM2->CNT = 0;
return val;
}
여기서 (int16_t) 강제 형변환은 매우 중요하다. 만약 int32_t 변수에 직접 대입하면, 음수 값(예: -1 → 0xFFFF)이 양수로 잘못 해석되어 심각한 오류 발생. 이는 많은 오픈소스 예제에서도 간과되는 주요 결함 요인이다.
- 위치식 PID 제어기: 이론부터 실현까지
위치식 PID의 이산화된 수학적 표현은 다음과 같다:
$$ u(k) = K_p \cdot e(k) + K_i \cdot T_s \sum_{i=0}^{k} e(i) + K_d \cdot \frac{e(k)-e(k-1)}{T_s} $$
여기서 $ e(k) = r(k) - y(k) $는 펄스 기준의 오차, $ T_s $는 샘플링 주기(10ms).
3.1 오차 계산 및 적분 제한 전략
오차는 목표 펄스 수와 현재 측정값의 차이로 계산되며, 16비트 인코더 값과 32비트 목표값을 맞추기 위해 32비트 연산을 사용한다.
int32_t error = (int32_t)target_pulse - (int32_t)current_pulse;
적분 항은 안정성 보장을 위해 "적분 분리"와 "경계 제한" 전략을 병행 적용한다:
- 오차 절댓값이 500 이상일 경우 적분 미사용
- 적분 누적값은 ±5000으로 경계 설정
if (abs(error) < 500) {
integral_sum += error;
if (integral_sum > 5000) integral_sum = 5000;
if (integral_sum < -5000) integral_sum = -5000;
}
이 값은 약 1.25권(2000ppr 기준)에 해당하며, 그 이상은 비례 및 미분 항이 주도하도록 설계되었다.
3.2 미분 항의 노이즈 제거 기법
직접 오차의 차분을 계산하면, 인코더의 1펄스 노이즈가 100배로 증폭되어 출력이 불안정해진다. 이를 해결하기 위해 미분 선행(Derivative on Measurement) 구조를 채택: 실제 위치 변화량을 계산하여 미분 항을 생성한다.
int32_t derivative = current_pulse - last_pulse;
last_pulse = current_pulse;
// Ts=10ms 고정이므로, Kd에 스케일링 포함
이는 목표값의 급변에 따른 미분 충격을 피하고, 더 안정적인 신호를 제공한다.
3.3 출력 제한 및 방향 분리
PID 결과값은 0~3599 사이의 크기로 변환되어야 하지만, 직접 매핑 시 오버플로우 및 방향 혼란 문제가 발생한다. 따라서 다음 조치를 취한다:
- 출력 제한 범위: [-3500, +3500] → 3599보다 작게 설정해 여유 공간 확보
- 방향 제어는 독립적인 GPIO로 처리: 출력값이 양수면 정방향, 음수면 역방향, 0이면 제동
int16_t pwm_val = abs(output);
if (pwm_val > 3500) pwm_val = 3500;
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pwm_val);
if (output > 0) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
} else if (output < 0) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
}
- 사각 영역 보정: 저속 성능 개선
저속 구간에서는 정적 마찰력이 움직임 마찰보다 훨씬 크기 때문에, 작은 펄스 출력에도 반응하지 않고, 돌파 후에는 관성으로 과도하게 지나가는 현상이 발생한다. 이를 해결하기 위해 사각 영역 전압(Dead Zone Voltage) 개념을 도입한다.
이 값은 기본 출력에 추가되는 최소 유도 전압으로, 예를 들어 50~200의 펄스 수를 의미하며, 이는 약 1.4%~5.6%의 차지비에 해당한다.
pwm_val = abs(output) + DEAD_ZONE;
값의 선택은 균형이 중요하다:
- 너무 작으면 저속에서 "기어링" 현상 발생
- 너무 크면 최소 가속 속도 상승, 정밀 조절 불가능
실험적으로 공급 전압과 모터 특성에 따라 실험적으로 최적 값을 찾는 것이 바람직하다.
- 타이머 인터럽트 서비스 루틴: 제어 주기의 핵심
모든 제어 로직은 10ms 주기로 동작하는 TIM2의 업데이트 인터럽트에서 실행된다. 이는 인코더 카운터와 동기화되며, 시간 지연을 방지한다.
인터럽트 함수는 최소한의 연산만 포함되어야 하며, 블로킹 함수는 절대 사용하지 않는다.
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim2);
if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET) {
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
current_pulse = Encoder_Read();
int32_t error = (int32_t)target_pulse - (int32_t)current_pulse;
// PID 계산 (비율, 적분, 미분)
// ...
int16_t pwm_val = abs(output) + DEAD_ZONE;
if (pwm_val > 3500) pwm_val = 3500;
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pwm_val);
// 방향 제어
if (output > 0) { /* ... */ }
else if (output < 0) { /* ... */ }
else { /* ... */ }
}
}
이 인터럽트의 실행 시간은 50μs 이내로 유지되어야 하며, 모든 지연 작업(예: 시리얼 전송, LED 전환)은 메인 루프로 이동한다.
- 상위기기 통신: 익명 프로토콜 활용
익명 튜닝 툴의 프로토콜은 간단하고 확장성이 있어, 다양한 임베디드 시스템에 활용 가능하다. 고정 16바이트 프레임 구조로, 데이터 타입과 인덱스를 포함한다.
주로 사용하는 타입은 0x02 (4바이트 정수)이며, 다음 데이터를 전송:
- 채널 0: 현재 회전 수 (현재 펄스 / PPR × 10)
- 채널 1: PID 출력값
- 채널 2: 목표 회전 수 (목표 펄스 / PPR × 10)
void Ano_Send_FourInt32(int32_t d1, int32_t d2, int32_t d3, int32_t d4)
{
uint8_t buf[16];
buf[0] = 0xAA; buf[1] = 0xAF; buf[2] = 0x02; buf[3] = 0x00;
memcpy(&buf[4], &d1, 4);
memcpy(&buf[8], &d2, 4);
memcpy(&buf[12], &d3, 4);
HAL_UART_Transmit(&huart1, buf, 16, 100);
}
상위기기는 이를 수신하여 실시간 그래프로 표시한다.
- PID 파라미터 튜닝: 임계비례도법 적용
임계비례도법은 시스템 모델 없이도 효과적인 초기값을 도출할 수 있다.
- $ K_i = K_d = 0 $로 설정, $ K_p $를 점차 증가시켜 등폭 진동 발생 시점 확인
- 그때의 $ K_p $ (Ku)와 주기 (Tu) 기록
- 공식 적용:
- $ K_p = 0.6 \times Ku $
- $ K_i = 2 \times K_p / Tu $ (Tu는 초 단위)
- $ K_d = K_p \times Tu / 8 $
예: $ K_u = 15 $, $ T_u = 200ms $ → $ K_p = 9 $, $ K_i = 90 $, $ K_d = 0.2 $
후속 조정은 다음과 같이 진행:
- 초과 조정: $ K_p $ 또는 $ K_d $ 감소
- 반응 느림: $ K_p $ 또는 $ K_i $ 증가 (적분 제한 강화 필수)
- 안정 진동: $ K_i $ 감소 또는 사각 영역 증가
또한, 고주파 노이즈는 물리적 문제일 수 있음을 기억해야 한다. 예를 들어, 펄스 신호에 공진이 발생할 경우, 케이블을 쌍선 코어로 교체하고, 전원선에 100nF 커패시터 추가로 해결 가능하다.
- 시스템 안정성 검증 및 이상 대응
- 손으로 외부 힘 가했을 때 회귀 성능: 1~2초 내 목표 위치로 안정적으로 복귀해야 함. 지연이나 지속 진동은 적분 포화의 징후
- 전원 변동 시험: 전압을 ±15%로 변경했을 때 위치 편차가 증가하지 않는지 확인. 변화에 따라 전방 피드포워드 제어 고려
- 장시간 운전 테스트: 24시간 이상 운전 시, 인코더 카운터의 누적 오차를 점검. 오차가 지속되면 타이머 초기화 누락 또는 간섭 확인
특히, 인코더와 PWM 신호가 평행하게 배치될 경우 고주파 전자기 유도로 오차 발생 가능. 이를 방지하기 위해 신호선을 교차 배치하고, 필터 회로 추가 필요.
- 코드 구조 및 모듈화 설계
코드는 다음과 같은 계층 구조로 설계됨:
Core/
├── main.c // 초기화, 메인 루프
├── pid_control.c/h // PID 알고리즘, 파라미터 정의
├── encoder.c/h // 인코더 읽기, 속도 계산
├── ano_protocol.c/h // 프로토콜 패킹, 수신/송신
└── hardware_init.c/h // HAL 초기화
핵심 원칙:
- 하드웨어 추상화:
encoder.c는TIM2->CNT직접 참조 금지 - 파라미터 통합 관리: 모든 설정은
pid_control.h에 모아두기 - 비차단 통신:
HAL_UART_Transmit_IT()사용
메인 루프는 매우 간결하게 유지되며, 모든 작업은 비차단 방식으로 처리된다.
while (1) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(100);
if (HAL_UART_Receive(&huart1, &rx_byte, 1, 1) == HAL_OK) {
Parse_Uart_Byte(rx_byte);
}
Ano_Send_FourInt32(
current_pulse / PPR * 10,
output,
target_pulse / PPR * 10,
0
);
}
이 구조는 메인 루프 주기를 100ms로 안정화시키며, 통신 속도에 영향받지 않는다.
- 일반적인 문제 해결 가이드
| 현상 | 가능한 원인 | 점검 방법 |
|---|---|---|
| 모터 동작 없음 | 방향 신호 오류, PWM 비활성화, 인코더 접선 반전 | 전압 측정, 파형 확인, 인코더 신호 교환 |
| 한 방향만 회전 | 출력 모드 미설정, 풀업 저항 누락 | MX_GPIO_Init() 설정 확인 |
| 상위기기 데이터 없음 | 보레이트 불일치, 프로토콜 미선택 | 시리얼 앤솔라이저로 프레임 확인 |
| 과도 조정 | $ K_p $ 과다, $ K_d $ 부족 | $ K_p $ 감소, $ K_d $ 증가 |
| 목표 지점에서 진동 | 사각 영역 부족, 노이즈 발생 | $ DEAD_ZONE $ 증가, 필터 추가 |
마지막으로, 인코더 설치 정렬도는 매우 중요하다. 축의 중심이 맞지 않을 경우 고속에서 주기적 펄스 손실이 발생하여 특정 각도에서 위치 오차가 나타날 수 있다. 반드시 레이저 정렬 장치를 사용하여 정확한 정렬을 수행해야 한다.