Firmware & RTOS/Troubleshooting

[임베디드 C] UART/SPI 노이즈 데이터 왜곡을 방지하는 C언어 CRC-16-CCITT 알고리즘 검증

임베디드 친구 2026. 6. 24. 21:22
반응형

핵심 요약

현상: UART 또는 SPI 데이터 수신 시 간헐적으로 패킷이 왜곡되거나 데이터 불일치 현상 발생.

원인: 외부 전자기 간섭 및 기생 커패시턴스로 인한 비트 반전 오류.

해결: 송 / 수신단에 CRC-16-CCIT 알고리즘을 적용하여 수신 패킷 무결성 검증.

UART/SPI 시리얼 통신 환경의 전송 데이터 왜곡(Data Corruption) 발생 증상

임베디드 하드웨어 개발 및 상용화 단계에서 전자기 환경에 장비가 노출되면 시리얼 인터페이스 라인에 심각한 물리 무선 서지 및 유도성 노이즈가 유입됩니다. 이때 UART 데이터 스트림 내 특정 바이트가 누락되거나 변형되면서 시스템은 하드웨어 먹통, 유효하지 않은 메모리 접근 또는 오동작이 발생할 수 있습니다.
해외 포럼이나 Stack Overflow 등에서 전 세계 엔지니어들이 주로 검색하는 주요 키워드와 대표 증상은 다음과 같습니다.

  • Serial Data Corruption / Bit Flipping Error
  • Framing Error Flags (UART_FLAG_FE)
  • Garbage Characters in SPI RX Buffer
  • Invalid Packet Length Exception

단순히 체크섬(Checksum)이나 패리티 비트(Parity Bit)만으로 방어 코드를 설계한 펌웨어 구조라면 2비트 이상의 다중 비트 반전(Multi-bit Error Flip)이나 군집 에러가 발생할 때 논리 검증 필터를 그대로 우회 통과하여 제어 레지스터 메모리를 오염시키는 치명적인 결함이 발생 가능합니다.

하드웨어 전자기 간섭(EMI)과 데이터 무결성 상실의 근본적인 원인 분석

하드웨어 버스 상에서 데이터 무결성(Data Integrity)이 상실되는 원인은 전기적 잡음과 부실한 소프트웨어 필터 설계의 결합입니다. 높은 주파수로 스위칭하는 인버터나 릴레이 소자 근처에 UART/SPI 신호선이 지나가면 정전 커패시턴스 결합(Capacitive Coupling)에 의해 임계 전압 기준선($V_{IH}$ 및 $V_{IL}$)이 순간적으로 붕괴됩니다.단순 덧셈 방식의 체크섬(Checksum) 구조는 바이트 배열 내에서 앞 바이트가 +1만큼 오염되고 뒤 바이트가 -1만큼 동시에 오염될 때 최종 누적합이 일치하는 논리적 수학 오류가 존재합니다.이와 달리 순환 중복 검사(CRC, Cyclic Redundancy Check)는 데이터를 이진 다항식 코드로 변환하여 수학적 나눗셈 연산을 수행합니다. 송신단이 전체 데이터 스트림을 지정된 기약 다항식(Generator Polynomial)으로 나눈 나머지인 16비트 프레임 체크 시퀀스를 데이터 꼬리단에 결합해 송신하면 수신단은 전체 스트림을 동일 다항식으로 재연산합니다. 비트 에러가 단 1비트라도 유입되면 나머지(Remainder)가 절대로 0이 될 수 없는 강력한 대칭 성질을 이용해 하드웨어 레벨의 데이터 왜곡을 검출해 냅니다.

데이터 왜곡 유입을 방지하지 못하는 잘못된 단순 체크섬 C 코드 예시 (Bad Case)

아래의 소스코드는 상용 임베디드 프로젝트에서 다중 비트 에러 플립이 유입될 때 에러를 전혀 검출해 내지 못하고 변형된 패킷을 유효한 패킷으로 파싱하는 구조입니다.

#include <stdint.h>
#include <stdbool.h>

/* Bad Case: Weak 8-bit simple summation checksum */
bool Is_Packet_Valid_Bad(const uint8_t *p_data, uint16_t length) {
    uint8_t calculated_sum = 0;

    /* Calculate simple sum for data except the last checksum byte */
    for (uint16_t i = 0; i < (length - 1); i++) {
        calculated_sum += p_data[i];
    }

    /* CRITICAL BUG: Multiple bit flips can cancel each other out, passing this verification */
    if (calculated_sum == p_data[length - 1]) {
        return true; 
    }

    return false;
}

고속 데이터 처리를 보장하는 룩업 테이블 기반 방어적 CRC-16 C 코드 구현법 (Good Case)

통신 데이터 무결성을 확실하게 보장하기 위해 산업용 글로벌 표준 다항식인 CRC-16-CCITT ($0x1021$: $X^{16} + X^{12} + X^5 + 1$) 프로토콜을 적용하고, MCU 연산 풋프린트를 최소화하도록 256바이트 룩업 테이블(Lookup Table)을 결합한 방어적 프로그래밍 예시입니다.

#include <stdint.h>
#include <stdbool.h>

/* Pre-computed CRC-16-CCITT Lookup Table using Polynomial 0x1021 */
static const uint16_t crc16_ccitt_table[256] = {
    0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
    0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
    0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
    0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
    0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
    0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD5AD,
    0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
    0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
    0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
    0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
    0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
    0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
    0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
    0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
    0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
    0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
    0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
    0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
    0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
    0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
    0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
    0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
    0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
    0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
    0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
    0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
    0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
    0x4BF5, 0x5BD4, 0x6B37, 0x7B16, 0x0B71, 0x1B50, 0x2B53, 0x3B72,
    0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
    0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
    0xEFAF, 0xDF8E, 0xCFED, 0xDFFC, 0xAF2B, 0xBF0A, 0x8F69, 0x9F48,
    0x6E8E, 0x7EA5, 0x4EDC, 0x5EFD, 0x2E0A, 0x3E2B, 0x0E48, 0x1E69
};

/**
 * @brief Computes CRC-16-CCITT fast using pre-calculated table
 * @param p_data Pointer to the raw input byte stream
 * @param length Total length of the data to compute
 * @return 16-bit unsigned integer CRC result
 */
uint16_t Calculate_CRC16_Fast(const uint8_t *p_data, uint16_t length) {
    uint16_t crc = 0xFFFF; /* Initial value for CCITT standard */

    for (uint16_t i = 0; i < length; i++) {
        uint8_t table_index = ((crc >> 8) ^ p_data[i]) & 0xFF;
        crc = (crc << 8) ^ crc16_ccitt_table[table_index];
    }

    return crc;
}

/**
 * @brief Validates data packet using CRC-16-CCITT trailing 2 bytes
 * @param p_packet Pointer to full buffer containing [Data...][CRC_High][CRC_Low]
 * @param total_length Packet length including 2-byte CRC field
 * @return True if data matches CRC perfectly, otherwise false
 */
bool Verify_Packet_Integrity(const uint8_t *p_packet, uint16_t total_length) {
    if (total_length < 3) return false;

    /* Hardware fallback isolation: compute across entire block including trailing CRC */
    uint16_t validation_result = Calculate_CRC16_Fast(p_packet, total_length);

    /* Mathematical property: standard remainder must equal 0x0000 */
    return (validation_result == 0x0000);
}

핵심 수정 포인트 설명

  • 정적 룩업 테이블(Static Lookup Table) 도입: 매 바이트마다 비트 시프트를 8회 연산하는 무거운 반복 루프를 제거하고, 배열 주소 다이렉트 매핑 구조로 치환하여 단 몇 사이클 이내에 계산을 종결합니다. ISR(Interrupt Service Routine) 핸들러 내부에서도 지연을 유발하지 않는 강력한 최적화 기법입니다.
  • 수학적 제로 검증(Zero Remainder Validation): 데이터 페이로드만 연산하여 수신된 바이트와 개별 비교하는 대신, 수신된 CRC 데이터 2바이트까지 포함한 패킷 전체를 통째로 CRC 함수에 넣었습니다. 데이터 손상이 전혀 없다면 다향식의 대칭 특성으로 인해 결과값은 완벽하게 0x0000으로 고정 출력되므로 소프트웨어 분기 처리가 극도로 단순화됩니다.

통신 무결성(Data Integrity) 디버깅 및 트러블슈팅 가이드

실무 환경에서 데이터 왜곡 에러를 마주했을 때 빠르게 추적하는 필드 가이드라인입니다.

  • 오실로스코프 지터 및 전압 강하 계측 (Signal Integrity Verification): 소프트웨어 에러 카운터율이 비정상적으로 치솟는다면, 즉시 Oscilloscope 프로브를 인터페이스 라인에 연결하고 신호 하강 에지에서의 오버슈트/언더슈트 및 유도성 노이즈의 진폭을 실측하십시오. 특히 SPI 클록 라인(SCK)에 발생하는 반사파 글리치 노이즈는 복수의 가짜 데이터를 복제하는 주원인입니다.
  • 하드웨어 내장 CRC 가속기 교차 매핑 (Hardware CRC Peripheral Interlock): STM32 등 최신 32비트 MCU 아키텍처 내부에는 독립형 하드웨어 CRC Peripheral 연산 장치가 탑재되어 있습니다. 룩업 테이블 메모리($512\text{ Byte}$) 공간조차 아쉬운 극도의 소형 저용량 시스템에서는 레지스터 주소 공간에 버퍼 포인터를 직접 포워딩하여 연산 전반을 하드웨어 코어 단에 위임함으로써 CPU 스케줄러 점유율을 $0%$로 최적화하십시오.
  • 에러 인젝션 기법을 통한 시뮬레이션 검증 (Error Injection Testing): 디버깅 환경에서 임의로 UART 수신 버퍼 내 특정 비트를 비트 연산자(p_packet[index] ^= 0x04;)로 강제 반전시키는 에러 인젝션 테스트를 수행하여, 시스템이 왜곡된 유입 패킷을 비정상 상태(SYS_STATE_ERROR)로 안전하게 격리 및 폐기(Drop) 시키는지 소프트웨어 방어벽 구동 유무를 상시 유닛 테스트로 검증하십시오
반응형