# Laser Frequency / Pulse Duration 단위 계통 분석 - 작성 : 2026-09-07 - 계기 : 현장 Arc 출사 이상 → 조치 후 정상. 실제 원인은 **Frequency 값 오입력**으로 확인됨. 그 값이 어떤 경로로 스캐너에 들어가는지 역추적한 결과. - 범위 : **정적 분석 전용. 소스 미수정. 현장 검증 미실시.** - 결론 요약 : 화면값 → RTC 전달 경로에 **단위/캐스팅 문제 3건 + 적용 범위 문제 1건 + 크래시 경로 1건**이 있습니다. --- ## 1. 값이 흘러가는 경로 ``` Scanner Parameter 화면 Laser Frequency(kHz) double, 범위 0.2 ~ 5 ScannerParam.cs:142 Laser Pulse Duration(ms) double ScannerParam.cs:164 │ ▼ SeqUploadWeldingList.InitializeDeviceStatus() :206-212 IDrawObject firstObject = _InitParam.SelectedDrawObjectList[0] as IDrawObject; int firstObjectScannerNo = firstObject.ScannerParamID - 1; _ScannerParam = ScannerCanvasUserCtrl.ScannerParams[firstObjectScannerNo]; _Scanner.SetLaserTiming( (uint)_ScannerParam.LaserFrequency, _ScannerParam.LaserPulseDuration, _ScannerParam.LaserPulseDuration ); │ ▼ Scanner.cs:163 SetLaserTiming(uint periodHz, double laser1PulseWidthUs, double laser2PulseWidthUs) │ ▼ ScannerRTC6.cs:772-784 (ScannerRTC5.cs:770-782 동일) //uint halfPeriodBit = (32 / periodkHz) * 1000; ← 구버전(주석 처리됨) uint halfPeriodBit = (uint)((32.0 / (double)periodHz) * 1000000.0); uint laser1PulseWidthBit = (uint)(laser1PulseWidthUs * 64); uint laser2PulseWidthBit = (uint)(laser2PulseWidthUs * 64); RTC6Wrap.n_set_laser_timing(dev, halfPeriodBit, laser1PulseWidthBit, laser2PulseWidthBit, 1); ``` 기준 단위 : RTC의 half period·pulse width는 **1/64 µs (15.625 ns)** 단위. 펄스폭 변환이 `× 64` 인 것으로 이 기준이 확인됩니다. --- ## 2. 발견 사항 ### ① `(uint)` 캐스팅이 소수점을 버립니다 — `SeqUploadWeldingList.cs:211` `LaserFrequency` 는 **double** 이고 화면 설명상 범위가 **0.2 ~ 5 kHz** 입니다. 그런데 전달 시 `(uint)` 로 캐스팅합니다. | 화면 입력 | `(uint)` 후 | 비고 | |---|---|---| | 0.2 | **0** | 0 이 되어 아래 ②에서 0 나눗셈 | | 0.5 | **0** | 〃 | | 2.5 | 2 | 0.5 손실 | | 4.9 | 4 | 0.9 손실 | | 5 | 5 | 정상 | **화면에서는 소수 입력이 가능한데 전달 과정에서 정수로 잘립니다.** 설명상 범위(0.2~5)의 상당 부분이 0 또는 절삭값이 됩니다. ### ② 0 이 들어가면 0 으로 나눕니다 — `ScannerRTC6.cs:775` ```csharp uint halfPeriodBit = (uint)((32.0 / (double)periodHz) * 1000000.0); ``` `periodHz == 0` 이면 `32.0 / 0` = `+Infinity` → `(uint)+Infinity` 는 **C# 명세상 정의되지 않은 동작**입니다. (x64 실측 동작은 0) 예외는 발생하지 않고 **조용히 엉뚱한 값**이 RTC로 넘어갑니다. ### ③ 환산식이 1000배 큽니다 — `ScannerRTC6.cs:775` / `ScannerRTC5.cs:773` 정확한 환산 (f 단위 kHz, 결과 단위 1/64 µs) : ``` 반주기[µs] = 1 / (2 · f[kHz] · 1000) · 1e6 = 500 / f halfPeriodBit = 반주기[µs] × 64 = 32000 / f ``` | 구분 | 식 | f=5 일 때 | |---|---|---| | 정확값 | `32000 / f` | 6,400 | | **현재 코드** | `(32.0 / f) × 1,000,000` | **6,400,000** | | 바로 위 주석(구버전) | `(32 / f) × 1000` | 6,000 ※정수나눗셈 절삭은 있으나 자릿수는 맞음 | **현재 코드는 정확값의 1000배**입니다. 주석 처리된 구버전이 자릿수상 맞는 식이었습니다. 결과적으로 **화면에 kHz 로 넣은 값이 실제로는 Hz 수준으로 동작**합니다. | 화면 입력 | 의도 | 실제 (현재 코드) | |---|---|---| | 1 kHz | 1,000 Hz | 1 Hz | | 2.5 kHz | 2,500 Hz | 2 Hz (①의 절삭까지 겹침) | | 5 kHz | 5,000 Hz | 5 Hz | ### ④ Pulse Duration 이 ms → µs 변환 없이 전달됩니다 — `SeqUploadWeldingList.cs:211` - 화면 항목 : `Laser Pulse Duration(ms)` — **ms** - 받는 파라미터 : `laser1PulseWidthUs` — **µs** - 활성 코드 : `_ScannerParam.LaserPulseDuration` **그대로 전달** (변환 없음) - 같은 파일에 주석 처리된 코드(`:628`) : `(uint)(param.LaserPulseDuration * 1000)` ← **ms→µs 변환이 있었음** | 화면 입력 | 의도 (ms→µs) | 실제 | |---|---|---| | 0.1 ms | 100 µs → 6,400 bit | 0.1 → **6 bit ≈ 0.094 µs** | | 1.0 ms | 1,000 µs → 64,000 bit | 1.0 → **64 bit = 1 µs** | **1000배 작게** 전달됩니다. ### ⑤ 첫 번째 객체의 값만 적용됩니다 — `SeqUploadWeldingList.cs:206-212` `SetLaserTiming` 은 **시퀀스당 1회**, `SelectedDrawObjectList[0]` 의 Parameter ID 값으로만 호출됩니다. 리스트가 아니라 COMMAND 방식이라 리스트 실행 전에 한 번 세팅되고 끝입니다. → **Parameter ID 2, 3 … 의 `Laser Frequency` · `Laser Pulse Duration` 은 무시됩니다.**   (`Laser Power`, `Mark Speed`, `Delay` 등은 객체마다 리스트에 들어가므로 정상 적용됩니다) 현장에서 "ID 마다 Frequency 를 다르게 넣었는데 차이가 없다" 면 이것이 원인입니다. ### ⑥ 크래시 경로 — `SeqUploadWeldingList.cs:207-209` ```csharp int firstObjectScannerNo = firstObject.ScannerParamID - 1; _ScannerParam = ScannerCanvasUserCtrl.ScannerParams[firstObjectScannerNo]; ``` `ScannerParamID` 가 0 이면 인덱스 **-1** → `IndexOutOfRangeException`. `SelectedDrawObjectList` 가 비어 있으면 `[0]` 에서 예외. 둘 다 방어 코드가 없습니다. (2026-09-07 추가한 예외처리는 `FuncLoadDrawObjectScannerParamSelected` 쪽이라 이 경로는 여전히 무방비) --- ## 3. 정황 근거 `SeqUploadWeldingList.cs:624-626` 주석 : > TrumpLaser의 경우 SetLaserTiming 함수로 Gate 신호를 살려줘야 함 > 우선 무조건 셋팅하는 것으로 쓰고 … > **LaserFrequency = 10, LaserPulseDuration = 0.1로 셋팅하면 출사 되는 것 확인_241025** 물리적으로 계산해서 정한 값이 아니라 **"이 값을 넣으니 나오더라"로 찾은 경험값**이라는 기록입니다. 위 ①~④의 단위 오류가 있는 상태에서 경험적으로 맞춘 것이므로 서로 정합합니다. 이 값으로 현재 코드가 만드는 실제 신호 : - halfPeriodBit = 32,000,000 / 10 = 3,200,000 bit = 50,000 µs → **10 Hz** - pulseWidthBit = 0.1 × 64 = 6 bit → **약 0.094 µs** 즉 **10 Hz 주기에 0.1 µs 폭**의 신호입니다. 듀티로 보면 거의 0 에 가깝습니다. --- ## 4. 그래서 실제로 문제인가 — 판단 보류 **단위 오류 자체는 코드상 명확합니다.** 다만 실제 용접 품질에 영향을 주는지는 별개입니다. 현재 본 레이저는 **아날로그 파워(DA) + 게이트(DO Laser_REMOTE)** 로 제어됩니다 (`SeqAutoRunPTOP.cs:1305`, `SetScannerProcessCondition` 의 `WriteDA`). 이 구조에서 RTC 의 LASER1/LASER2 펄스 신호는 **"살아만 있으면 되는" 역할**일 수 있습니다. 위 주석도 "Gate 신호를 살려줘야 함" 이라고 적고 있습니다. → **발진기가 이 펄스 신호를 실제로 어떻게 쓰는지 확인해야 결론이 납니다.**   발진기 매뉴얼의 변조/게이트 입력 사양 확인 필요. --- ## 5. 확인·조치 후보 (미실행) | # | 항목 | 내용 | |---|---|---| | 1 | 발진기 사양 확인 | RTC LASER1/LASER2 펄스를 실제 변조에 쓰는지, 게이트로만 쓰는지 | | 2 | 현재 현장값 확인 | 각 레시피 Parameter ID 의 `Laser Frequency` · `Laser Pulse Duration` 실측 | | 3 | ①⑤ 우선 | 소수점 절삭과 "첫 객체만 적용"은 사양 확인 없이도 **현장이 오해하기 쉬운 지점** | | 4 | ③④ 수정 시 | 환산식을 고치면 **현재 경험값이 전부 어긋납니다.** 반드시 재티칭 전제로 진행 | | 5 | ⑥ 방어 | `ScannerParamID <= 0` / 빈 리스트 방어 추가 | **주의 — ③④ 를 고치면 현재 잘 나오는 조건이 깨집니다.** 경험값이 오류를 상쇄하고 있을 수 있으므로, 발진기 사양 확인 → 시험 조건 확보 → 재티칭 계획이 선 뒤에 손대야 합니다. --- ## 참조 위치 | 위치 | 내용 | |---|---| | `ScannerParam.cs:142` | `LaserFrequency` double, kHz, 0.2~5 | | `ScannerParam.cs:164` | `LaserPulseDuration` double, ms | | `SeqUploadWeldingList.cs:206-212` | 첫 객체 param 으로 `SetLaserTiming` 1회 호출 + `(uint)` 캐스팅 | | `SeqUploadWeldingList.cs:624-629` | 경험값 주석 + ms→µs 변환이 있던 주석 코드 | | `Scanner.cs:163-169` | 래퍼 (`periodHz`, `laser1PulseWidthUs`) | | `ScannerRTC6.cs:772-784` | 환산식 — 1000배 문제 지점 | | `ScannerRTC5.cs:770-782` | RTC5 동일 | | `ScannerObj.cs:264` | 추상 선언 (`halfPeriod`, `laser1PulseWidth`) — 이름이 계층마다 다름 |