가상 PWM과 입력 제한
150분 안팎
학습 목표
PWM 요청을 -1~1로 제한하고 실제 장치 입출력 대신 상태 구조체에 저장합니다.
개념
제어 요청과 장치 저장값을 구분합니다
제어기가 계산한 요청이 2라고 해서 장치가 200% 출력을 낼 수 있는 것은 아닙니다. 입력 범위를 한곳에서 정리하지 않으면 왼쪽 모터와 오른쪽 모터가 서로 다른 정책을 적용할 수 있습니다. 이번 레슨은 set_wheels에서 양쪽 요청을 검사하고 -1부터 1 사이로 제한해 Hal에 저장합니다. 실습의 성공 기준은 그림 속 로봇이 움직이는 모습이 아니라 저장된 두 출력이 계약과 일치하는지입니다.
가상 PWM의 의미를 정합니다
left_pwm와 right_pwm는 부호가 있는 정규화 명령입니다. 절댓값은 요청 크기, 부호는 방향을 뜻하도록 모델에서 약속합니다. 물리적인 PWM 듀티는 켜진 시간 비율이므로 음의 듀티를 갖지 않습니다. 실제 모터 구동에서는 방향 핀이나 드라이버 모드와 크기 신호로 바꾸는 별도 어댑터가 필요합니다. 여기에서는 그 변환 앞의 논리 요청을 편의상 가상 PWM이라고 부릅니다.
저장값 0.5를 0.5m/s 또는 모터 회전수로 해석하지 않습니다. 부하, 전압, 마찰, 드라이버 특성에 따라 실제 속도는 달라집니다. 이번 HAL은 속도 측정과 동역학을 제공하지 않으며 이후 운동학 모듈이 다른 모델을 붙일 경계를 만듭니다. 실수 필드에 값을 대입하는 일이 실제 타이머 설정이나 전압 인가를 대신했다고 주장하지 않습니다.
포화 제한의 세 구간
clamp 함수는 -1보다 작은 값을 -1로, 1보다 큰 값을 1로 반환하며 그 사이의 값은 그대로 반환합니다. 따라서 -2와 2는 각각 -1과 1이 됩니다. 입력이 정확히 -1이나 1이면 같은 값으로 남고 0.25와 -0.5는 소수 부분과 부호를 유지합니다. 제한은 값의 절댓값만 저장하는 동작이 아니며 음의 요청 방향을 지우지 않습니다.
한쪽만 제한하는 결함은 보통 양쪽에 같은 작은 요청을 넣으면 드러나지 않습니다. starter는 오른쪽만 clamp를 호출하므로 (-2,2)를 넣으면 왼쪽은 -2로 남습니다. 서로 다른 부호의 양쪽 범위 초과를 한 번에 넣어 대칭 정책을 확인합니다. 정상 내부 값과 정확한 경계값도 추가해 모든 입력을 1이나 0으로 바꾸는 잘못된 수정이 통과하지 못하게 합니다.
유한값 검사부터 실행합니다
NaN은 숫자 연산이 정의된 값이 아니라 비교가 일반 수와 다르게 동작하는 값입니다. NaN에 대해 value < -1과 value > 1은 모두 거짓이므로 단순 조건문만으로는 제한을 통과할 수 있습니다. math.h의 isfinite로 두 요청이 모두 유한한지 먼저 확인합니다. Infinity는 상한 비교로 1이 될 수도 있지만 이번 계약은 유효 요청이 아니므로 HAL_INVALID로 처리합니다.
비유한 입력을 0이나 최대 출력으로 조용히 치환하면 제어기 계산 오류를 놓치게 됩니다. 예를 들어 오른쪽이 Infinity인데 왼쪽 0.4만 저장하면 한쪽만 움직이는 새로운 상황을 만듭니다. set_wheels는 두 요청의 유한값 검사를 끝내기 전에 어느 출력도 갱신하지 않습니다. 유효하지 않은 쌍이면 두 출력을 0으로 만들고 오류 상태를 남깁니다. 제한 가능한 유한 범위 초과와 계산 실패를 다른 정책으로 처리합니다.
쌍으로 갱신하는 호출 경계
왼쪽과 오른쪽은 하나의 이동 요청입니다. 두 값이 유효하다면 각각 clamp를 계산해 저장하고 HAL_OK를 반환합니다. 호출이 반환된 시점에 두 필드는 같은 요청에서 나온 값이어야 합니다. 여기서 쌍으로 갱신한다는 것은 단일 스레드 함수 경계의 일관성입니다. 두 개의 double 쓰기가 하드웨어 원자 연산이거나 여러 스레드에 자동으로 안전하다는 의미는 아닙니다.
일반 코드에서 left_pwm를 직접 대입하면 set_wheels의 검사와 결함 정책을 우회합니다. 요청 경로를 공개 함수로 모아 테스트가 정책의 한 지점을 확인하도록 합니다. 상태 구조체를 공개한 것은 학습용 관찰과 결함 주입을 단순하게 하기 위한 선택입니다. 실제 라이브러리로 발전시키면 외부에서 상태를 수정할 수 없도록 숨기는 설계도 검토할 수 있습니다.
오류 상태의 요청은 재시작하지 않습니다
이미 HAL_INVALID, HAL_SENSOR, HAL_END 상태이면 set_wheels는 그 상태를 반환하고 새 요청을 반영하지 않습니다. 오류로 정지한 뒤 정상 숫자 (1,1)를 보냈다는 이유만으로 다시 출력이 생겨서는 안 됩니다. 결함 상태를 없애는 정책과 새 모터 요청을 처리하는 정책은 별개입니다. 이 실습은 새로운 실험을 시작할 때 hal_init을 호출하는 방식만 사용합니다.
Hal 포인터가 NULL이면 저장 대상이 없어 HAL_INVALID만 반환합니다. 장치가 없는 경우 출력 상태를 그 장치에 기록할 수는 없습니다. NULL 검사 뒤 기존 상태 검사, 유한값 검사, 제한, 저장 순서를 확인합니다. 포인터가 NULL이 아니더라도 호출자가 유효한 Hal을 제공했다는 전제는 필요합니다. 입력 숫자 검사와 메모리 수명 검사는 서로 대체할 수 없습니다.
표시와 실제 저장을 함께 봅니다
demo의 첫 줄은 (-2,2)를 제한한 left=-1.000 right=1.000이며 둘째 줄은 내부 값 left=0.250 right=-0.500입니다. 표시할 때만 clamp를 적용하면 로그는 맞아도 구조체에는 여전히 범위 초과가 남을 수 있습니다. test_hal.c는 저장 필드를 직접 비교하므로 이런 수정은 실패합니다. 로그는 구현 관찰 도구이며 테스트는 계약을 확인하는 도구로 구분합니다.
0.0과 -0.0은 비교에서 같지만 로그 문자열이 다르게 보일 수 있습니다. 이 구현은 clamp의 영값을 0.0으로 정리하여 관찰 출력이 일관되게 보이도록 합니다. 이것은 정지 판단을 소수점 표시로 하는 규칙이 아닙니다. 부동소수 계산이 포함되는 다른 테스트에서는 허용 오차가 필요하지만 여기서 쓰는 0.25, 0.5, 1은 이진 표현과 직접 비교가 간단한 fixture입니다.
실패를 출력 방향과 연결합니다
FAIL both limits가 나오면 함수의 반환 상태와 두 필드를 확인합니다. HAL_OK인데 left_pwm가 -2이면 포인터 전달이나 빌드 오류가 아니라 왼쪽 제한이 빠진 것입니다. NaN 검사에서만 실패하면 비교 순서와 isfinite를 확인합니다. 로그를 소수점 없이 출력해 결함을 감추거나 테스트의 기대 -1을 -2로 바꾸지 않습니다. 요청을 저장하는 함수의 책임을 수정합니다.
PASS motor checks=9에는 초기 정지, 양쪽 초과, 정확한 경계, 내부 값, 영값, NaN, Infinity, 결함 유지, NULL 요청 검사가 포함됩니다. 이 숫자는 실제 모터에 적용 가능한 모든 조건을 시험했다는 의미가 아닙니다. 실제 구동기의 주파수, 데드타임, 방향 전환, 전류 제한은 별도 장치 검증 대상입니다. 타이머와 듀티 계산은 더 읽기의 PWM 장에서 확장합니다.
다음 정지 실험을 준비합니다
이 레슨에서 저장 범위가 지켜져도 센서가 끊긴 뒤 이전 출력이 남는 결함은 아직 별도 검증이 필요합니다. 다음 레슨은 정상 운전 중 센서 실패를 넣어 정지 시점과 고장 상태 유지 여부를 확인합니다. 출력 제한은 정상 요청을 장치 계약에 맞추는 기능이고, 결함 정지는 운전 근거가 사라졌을 때 요청을 중단하는 기능입니다. 두 기능을 같은 테스트 하나로 대체하지 않습니다.
호출 경계를 코드로 확인합니다
다음은 초기화한 h에 요청을 전달하는 호출 조각입니다. 주석의 값은 저장 계약을 설명하며 실제 출력은 따라하기의 demo.sh로 확인합니다.
HalStatus status = set_wheels(&h, -2.0, 2.0);
if (status == HAL_OK) {
/* 저장값은 h.left_pwm=-1, h.right_pwm=1입니다. */
}따라하기
시작 코드의 실패를 확인합니다
starter ZIP을 푼 폴더에서 실행합니다. stdout 요약과 별도로 stderr의 FAIL 항목이 어느 계약을 가리키는지 읽습니다. 종료 상태 1은 이번 TODO에서 기대한 실패입니다.
bash check.sh실행 결과
FAIL motor checks=9 failures=1
계약에 맞는 대입을 작성합니다
hal_sim.c의 해당 TODO를 다음 코드로 고칩니다. 아래는 편집할 C 조각이며 셸에서 직접 실행하는 명령이 아닙니다. 헤더와 테스트는 유지합니다.
/* set_wheels의 유한값 검사 뒤 양쪽을 저장합니다. */
hal->left_pwm = clamp(left);
hal->right_pwm = clamp(right);같은 검사를 다시 실행합니다
수정한 starter 폴더에서 다시 검사합니다. 성공 요약과 종료 상태 0을 확인합니다. 이 출력은 solution 구현에서 실행해 확인한 값입니다.
bash check.sh실행 결과
PASS motor checks=9
저장된 결과를 관찰합니다
수정 후 관찰 실행 파일로 값과 상태를 확인합니다. 아래 명령은 임시 디렉터리에서 빌드하고 정리합니다.
bash demo.sh실행 결과
left=-1.000 right=1.000 left=0.250 right=-0.500
확인 문제
실습
set_wheels에서 양쪽 요청을 각각 제한하여 저장합니다. 유한 범위 초과는 포화, 비유한 요청은 두 출력 정지와 오류 잠금입니다. 경계·내부·NaN·Infinity·NULL 등 검사 9개를 통과합니다.
실행 명령
bash check.sh
기대 결과
PASS motor checks=9
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 정규화 모터 요청과 실제 PWM 듀티·속도의 차이를 어떻게 설명합니까?