본문 바로가기
MCU/STM32

[ STM32]STM32L432KC로 Modbus RTU Slave 코드 구현: DMA와 저전력 최적화

by linuxgo 2025. 8. 13.
반응형

모드버스 RTU는 산업 자동화에서 널리 사용되는 통신 프로토콜입니다. 이 글에서는 STM32L432KC 마이크로컨트롤러를 사용해 Modbus RTU 슬레이브를 구현하는 방법을 설명합니다. DMA, 링버퍼, 저전력 모드, 80MHz 클럭을 활용해 최적화된 코드를 제공하며, 초보자와 숙련자 모두를 위해 상세한 주석과 설명을 포함하였습니다.

1. 모드버스 RTU란?

1.1 개요

모드버스 RTU 메시지 구조에 대해 상세히 설명하겠습니다. 아래 내용은 이전 답변에서 제공한 모드버스 RTU의 개요를 포함하되, 메시지 구조에 초점을 맞춰 더 깊이 탐구합니다. 메시지 구조의 각 필드, 데이터 형식, 예시, 그리고 관련 고려사항을 포함하여 포괄적으로 다룹니다.


1. 모드버스 RTU 개요

모드버스 RTU(Remote Terminal Unit)는 모드버스 프로토콜의 바이너리 기반 변형으로, 주로 시리얼 통신(RS-232, RS-485, RS-422)을 통해 산업 자동화 환경에서 장치 간 데이터를 교환하는 데 사용됩니다. 모드버스 RTU는 마스터-슬레이브 구조를 따르며, 간단하고 효율적인 데이터 전송으로 인해 PLC, SCADA, HMI, 센서 등 다양한 장비에서 널리 채택됩니다. 


2. 모드버스 RTU 메시지 구조

모드버스 RTU 메시지는 컴팩트한 바이너리 형식으로, 요청(Request)과 응답(Response) 모두 동일한 기본 구조를 따릅니다. 메시지는 다음과 같은 필드로 구성됩니다:

필드 길이 설명
슬레이브 주소 1바이트 대상 슬레이브 장치의 고유 주소 (1~247). 0은 브로드캐스트 주소.
기능 코드 1바이트 수행할 작업을 정의 (예: 레지스터 읽기/쓰기).
데이터 가변 길이 요청 또는 응답에 필요한 데이터 (레지스터 주소, 데이터 값 등).
CRC 2바이트 오류 검출을 위한 CRC-16 체크섬.

메시지 타이밍:

  • 모드버스 RTU는 메시지 간 구분을 위해 최소 3.5 캐릭터 시간(3.5T)의 침묵 시간(Silent Interval)을 요구합니다.
  • 메시지 끝에는 1.5 캐릭터 시간(1.5T) 이상의 침묵 시간이 필요합니다.
  • 예: 9600bps에서 1 캐릭터(10비트, 8데이터 비트+1스타트 비트+1스톱 비트)는 약 1.04ms이므로, 3.5T는 약 3.64ms입니다.

3. 메시지 구조의 상세 분석

3.1 슬레이브 주소 (1바이트)

  • 역할: 메시지가 어떤 슬레이브 장치로 전송되는지를 지정합니다.
  • 범위: 1247 (0x010xF7). 0은 모든 슬레이브를 대상으로 하는 브로드캐스트 주소입니다.
  • 브로드캐스트:
    •    주소 0은 모든 슬레이브에게 메시지를 전송하며, 슬레이브는 응답하지 않습니다.
    •    예: 설정 변경 명령을 모든 장치에 동시에 적용할 때 사용.
  • 고려사항:
    •    네트워크 내 각 슬레이브는 고유한 주소를 설정해야 충돌을 방지할 수 있습니다.
    •    RS-485 네트워크에서는 최대 247개 슬레이브를 지원하지만, 실제로는 하드웨어 제약(예: 드라이버 용량)으로 32~128개로 제한될 수 있습니다.

3.2 기능 코드 (1바이트)

  • 역할: 마스터가 슬레이브에게 요청하는 작업의 유형을 정의합니다.
  • 주요 기능 코드:
    •    0x01: 코일 상태 읽기 (Read Coils, 디지털 출력).
    •    0x02: 디지털 입력 상태 읽기 (Read Discrete Inputs).
    •    0x03: 홀딩 레지스터 읽기 (Read Holding Registers, 16비트 데이터).
    •    0x04: 입력 레지스터 읽기 (Read Input Registers).
    •    0x05: 단일 코일 쓰기 (Write Single Coil).
    •    0x06: 단일 레지스터 쓰기 (Write Single Register).
    •    0x0F: 다중 코일 쓰기 (Write Multiple Coils).
    •    0x10: 다중 레지스터 쓰기 (Write Multiple Registers).
  • 에러 코드:
    •    슬레이브가 요청을 처리할 수 없는 경우, 기능 코드에 0x80을 더한 에러 코드를 반환합니다.
    •    예: 0x83는 기능 코드 0x03(홀딩 레지스터 읽기)에 대한 에러 응답.
    •    에러 원인은 데이터 필드의 예외 코드(Exception Code)로 제공됩니다 (예: 0x02는 잘못된 주소).

3.3 데이터 (가변 길이)

  • 역할: 요청 또는 응답에 필요한 추가 정보를 포함합니다.
  • 요청 메시지:
    •    레지스터 주소(2바이트): 읽기/쓰기 대상 레지스터의 시작 주소.
    •    데이터 개수(2바이트): 읽거나 쓸 레지스터/코일의 개수.
    •    쓰기 데이터: 쓰기 요청 시 포함되는 데이터 값.
  • 응답 메시지:
    •    바이트 수(1바이트): 응답 데이터의 길이.
    •    데이터 값: 요청된 레지스터 또는 코일의 값.
  • 고려사항:
    •    데이터는 빅엔디안(Big-Endian) 형식으로 전송됩니다 (상위 바이트가 먼저).
    •    최대 메시지 길이는 256바이트로 제한됩니다 (데이터 필드 포함).

3.4 CRC (2바이트)

  • 역할: 메시지의 무결성을 검증하기 위한 오류 검출 코드.
  • 알고리즘: CRC-16-IBM (다항식: x^16 + x^15 + x^2 + 1, 초기값: 0xFFFF).
  • 계산:
    •    슬레이브 주소, 기능 코드, 데이터 필드를 포함하여 계산.
    •    결과는 2바이트로, 하위 바이트(Low Byte)가 먼저, 상위 바이트(High Byte)가 나중에 전송됩니다.
  • 고려사항:
    •    CRC 계산은 정확해야 하며, 잘못된 CRC는 메시지 무시로 이어집니다.
    •    라이브러리(예: libmodbus, pymodbus)를 사용하면 CRC 계산이 자동화됩니다.

4. 메시지 구조 예시

4.1 요청 메시지: 홀딩 레지스터 읽기

  • 상황: 슬레이브 ID 1번 장치에서 주소 0번 홀딩 레지스터 2개를 읽기.
  • 메시지:
    •    슬레이브 주소: 0x01
    •    기능 코드: 0x03 (홀딩 레지스터 읽기)
    •    데이터: 0x0000 (시작 주소), 0x0002 (레지스터 개수)
    •    CRC: 0xC40B (계산된 값)
    •    전체 메시지: 01 03 00 00 00 02 C4 0B

4.2 응답 메시지:

  • 상황: 슬레이브가 요청에 대해 2개 레지스터 값을 반환 (값: 0x1234, 0x5678).
  • 메시지:
    •    슬레이브 주소: 0x01
    •    기능 코드: 0x03
    •    데이터: 0x04 (바이트 수, 2레지스터 × 2바이트 = 4바이트), 0x1234 0x5678 (레지스터 값)
    •    CRC: 0x9C7B (계산된 값)
    •    전체 메시지: 01 03 04 12 34 56 78 9C 7B

4.3 요청 메시지: 단일 레지스터 쓰기

  • 상황: 슬레이브 ID 1번 장치의 주소 10번 레지스터에 값 0x00FF 쓰기.
  • 메시지:
    •    슬레이브 주소: 0x01
    •    기능 코드: 0x06 (단일 레지스터 쓰기)
    •    데이터: 0x000A (레지스터 주소), 0x00FF (쓰기 값)
    •    CRC: 0x4BFA
    • 전체 메시지: 01 06 00 0A 00 FF 4B FA

4.4 에러 응답:

  • 상황: 잘못된 주소로 인해 에러 발생 (기능 코드 0x03).
  • 메시지:
    •    슬레이브 주소: 0x01
    •    기능 코드: 0x83 (에러 코드)
    •    데이터: 0x02 (예외 코드: 잘못된 주소)
    •    CRC: 0xF1C3
    •    전체 메시지: 01 83 02 F1 C3

5. 메시지 구조의 구현 고려사항

5.1 데이터 형식:

  • 바이너리 전송: 모드버스 RTU는 바이너리 데이터를 사용하므로, ASCII 문자로 변환하지 않습니다.
  • 엔디안: 레지스터 데이터는 16비트 단위로, 빅엔디안 형식(상위 바이트 먼저)으로 전송됩니다.
  • 최대 길이: 전체 메시지는 최대 256바이트로 제한되며, 데이터 필드는 기능 코드에 따라 가변적입니다.

5.2 타이밍 제어:

  • 침묵 시간: 메시지 간 3.5T 침묵 시간을 준수해야 하며, 이는 보드레이트에 따라 달라집니다.
    •    예: 19200bps에서 3.5T는 약 1.82ms.
  • 타임아웃: 슬레이브는 일정 시간(보통 1초) 내에 응답해야 하며, 그렇지 않으면 마스터는 타임아웃으로 처리.

5.3 오류 검출:

  • CRC 계산: CRC-16-IBM 알고리즘은 모든 바이트를 포함하여 계산됩니다. 잘못된 CRC는 메시지 무시로 이어집니다.
  • 예외 처리: 슬레이브는 잘못된 요청(예: 유효하지 않은 주소, 데이터 값)에 대해 예외 코드를 반환해야 합니다.

5.4 프로토콜 구현:

  • 라이브러리 사용: pymodbus(Python), libmodbus(C), Modbus.NET(C#) 등은 메시지 생성 및 CRC 계산을 자동화합니다.
  • 수동 구현: CRC 계산 및 타이밍 제어를 직접 구현할 경우, 프로토콜 사양(Modbus over Serial Line Specification)을 준수해야 합니다.

6. 모드버스 RTU 메시지 구조의 장단점

장점:

  • 컴팩트함: 바이너리 형식으로 데이터 오버헤드가 적어 효율적.
  • 간단함: 고정된 구조로 구현 및 디버깅이 용이.
  • 신뢰성: CRC-16을 통한 강력한 오류 검출.

단점:

  • 복잡한 데이터 처리: 복잡한 데이터 구조는 별도의 매핑(레지스터 매핑) 필요.
  • 제한된 데이터 크기: 최대 256바이트로 대량 데이터 전송에 부적합.
  • 보안 없음: 암호화나 인증 기능이 없어 보안이 취약.

7. 타이밍 및 CRC 추가 설명

7.1 타이밍 요구사항

Modbus RTU는 엄격한 타이밍을 요구합니다:

  • 1.5T: 문자 간 최대 간격. 9600bps에서 1 캐릭터(10비트) ≈ 1.04ms, 1.5T ≈ 1.56ms.
  • 3.5T: 프레임 간 최소 침묵 시간. 9600bps에서 3.5T ≈ 3.64ms.
  • STM32 구현: TIM6을 0.1ms 틱으로 설정, 3.5T ≈ 36틱.

7.2 CRC-16-IBM 계산

CRC-16-IBM은 다항식 \( x^{16} + x^{15} + x^2 + 1 \) (0xA001)을 사용합니다. 계산 과정:

  1. 초기 CRC = 0xFFFF.
  2. 데이터 바이트를 CRC와 XOR.
  3. 8비트에 대해 오른쪽 시프트 및 다항식 XOR 반복.
  4. 하위 바이트 먼저 전송.

최적화: 4비트 단위 룩업 테이블(16항목, 32바이트)로 계산 속도 향상.


// CRC 테이블 생성 예시 (참고용)
uint16_t generate_crc_table(uint8_t index) {
    uint16_t crc = index;
    for (uint8_t i = 0; i < 4; i++) {
        if (crc & 0x0001) {
            crc = (crc >> 1) ^ 0xA001;
        } else {
            crc >>= 1;
        }
    }
    return crc;
}

7.3 산업 활용 사례

  • PLC 통신: 공장 설비의 센서 데이터 수집.
  • BMS: HVAC, 조명 제어.
  • 에너지 관리: 스마트 미터로 전력 모니터링.
  • SCADA: 원격 데이터 수집 및 제어.

8. STM32L432KC 구현 개요

STM32L432KC는 Cortex-M4 기반 저전력 MCU로, 80MHz 클럭, 64KB 플래시, 20KB SRAM을 제공합니다. 다음 하드웨어를 활용했습니다:

  • UART: USART2 (PA2-TX, PA3-RX), 9600bps, 8-N-1.
  • DMA: RX (DMA1_Channel6, Circular), TX (DMA1_Channel7, Normal).
  • 타이머: TIM6 (0.1ms 틱, 80MHz 클럭).
  • RS-485: MAX485 트랜시버, DE 핀 (PA1).
  • 링버퍼: 512바이트, 오버플로우 감지.
  • 저전력: Sleep 모드.

9. 코드 구현

라이브러리는 다음 파일로 구성됩니다:

  • modbus_rtu.h: 프로토콜 상수와 함수 선언.
  • modbus_rtu.c: 메시지 처리, CRC 계산.
  • platform.h: STM32 하드웨어 인터페이스.
  • platform.c: UART, DMA, 링버퍼, 저전력 구현.
  • main.c: 메인 프로그램 및 초기화.

9.1 modbus_rtu.h

Modbus RTU 설정과 함수 선언을 정의합니다.


#ifndef MODBUS_RTU_H
#define MODBUS_RTU_H

#include <stdint.h>
#include "platform.h"

// Configuration
#define MODBUS_MAX_SLAVES      247        // 최대 슬레이브 수 (1~247)
#define MODBUS_MAX_FRAME_SIZE  256        // 최대 프레임 크기 (바이트)
#define MODBUS_SILENT_TICKS    36         // 3.5T at 9600bps (3.64ms, 0.1ms 틱 기준)

// Modbus function codes
#define FC_READ_COILS          0x01       // 코일 읽기
#define FC_READ_DISCRETE_INPUTS 0x02      // 디지털 입력 읽기
#define FC_READ_HOLDING_REG    0x03       // 홀딩 레지스터 읽기
#define FC_READ_INPUT_REG      0x04       // 입력 레지스터 읽기
#define FC_WRITE_SINGLE_COIL   0x05       // 단일 코일 쓰기
#define FC_WRITE_SINGLE_REG    0x06       // 단일 레지스터 쓰기
#define FC_WRITE_MULTIPLE_COILS 0x0F      // 다중 코일 쓰기
#define FC_WRITE_MULTIPLE_REG  0x10       // 다중 레지스터 쓰기

// Exception codes
#define EXC_ILLEGAL_FUNCTION   0x01       // 지원되지 않는 기능 코드
#define EXC_ILLEGAL_ADDRESS    0x02       // 잘못된 주소
#define EXC_ILLEGAL_VALUE      0x03       // 잘못된 데이터 값
#define EXC_SLAVE_FAILURE      0x04       // 슬레이브 처리 실패

// Modbus 데이터 구조체
typedef struct {
    uint8_t coils[256];                   // 코일 배열 (최대 2048 코일)
    uint8_t discrete_inputs[256];         // 디지털 입력 배열
    uint16_t holding_registers[256];      // 홀딩 레지스터 배열
    uint16_t input_registers[256];        // 입력 레지스터 배열
} ModbusData;

// Modbus RTU 구조체
typedef struct {
    uint8_t slave_id;                     // 슬레이브 ID (1~247)
    ModbusData* data;                     // 데이터 저장소 포인터
    uint8_t rx_buffer[MODBUS_MAX_FRAME_SIZE]; // 수신 버퍼
    uint8_t rx_len;                       // 수신 데이터 길이
    uint8_t tx_buffer[MODBUS_MAX_FRAME_SIZE]; // 송신 버퍼
} ModbusRTU;

// 함수 프로토타입
void modbus_init(ModbusRTU* modbus, uint8_t slave_id, ModbusData* data); // 초기화
void modbus_process(ModbusRTU* modbus); // 메시지 처리 루프
uint16_t modbus_crc16(const uint8_t* data, uint16_t len); // CRC-16-IBM 계산

#endif // MODBUS_RTU_H

9.2 modbus_rtu.c

메시지 처리와 CRC 계산을 구현합니다.


#include "modbus_rtu.h"
#include <string.h>

// CRC-16-IBM 룩업 테이블 (4비트 단위, 16항목, 32바이트)
static const uint16_t crc_table[16] = {
    0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
    0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440
};

// 모드버스 RTU 초기화
void modbus_init(ModbusRTU* modbus, uint8_t slave_id, ModbusData* data) {
    modbus->slave_id = slave_id;        // 슬레이브 ID 설정
    modbus->data = data;                // 데이터 저장소 연결
    modbus->rx_len = 0;                 // 수신 버퍼 초기화
    memset(modbus->rx_buffer, 0, MODBUS_MAX_FRAME_SIZE);
    memset(modbus->tx_buffer, 0, MODBUS_MAX_FRAME_SIZE);
    platform_uart_init();               // UART, DMA, 타이머 초기화
}

// CRC-16-IBM 계산 (4비트 룩업 테이블)
uint16_t modbus_crc16(const uint8_t* data, uint16_t len) {
    uint16_t crc = 0xFFFF;              // 초기값 0xFFFF
    for (uint16_t i = 0; i < len; i++) {
        uint8_t idx = (crc ^ data[i]) & 0x0F; // 하위 4비트 처리
        crc = (crc >> 4) ^ crc_table[idx];
        idx = ((crc ^ (data[i] >> 4)) & 0x0F); // 상위 4비트 처리
        crc = (crc >> 4) ^ crc_table[idx];
    }
    return crc;                         // 하위 바이트 먼저 전송
}

// 모드버스 메시지 처리
void modbus_process(ModbusRTU* modbus) {
    // 3.5T 침묵 시간 확인 (9600bps에서 3.64ms ≈ 36틱)
    if (!platform_timer_expired(MODBUS_SILENT_TICKS)) {
        return;
    }

    // 링버퍼에서 데이터 읽기
    while (platform_uart_available()) {
        if (modbus->rx_len < MODBUS_MAX_FRAME_SIZE) {
            modbus->rx_buffer[modbus->rx_len++] = platform_uart_read();
            platform_timer_reset();     // 새 데이터 시 타이머 리셋
        } else {
            modbus->rx_len = 0;         // 버퍼 오버플로우 처리
        }
    }

    // 최소 프레임 크기 확인
    if (modbus->rx_len < 4) return;

    // 슬레이브 ID 또는 브로드캐스트 확인
    if (modbus->rx_buffer[0] != modbus->slave_id && modbus->rx_buffer[0] != 0) {
        modbus->rx_len = 0;
        return;
    }

    // CRC 검증
    uint16_t crc_received = (modbus->rx_buffer[modbus->rx_len - 1] << 8) | modbus->rx_buffer[modbus->rx_len - 2];
    uint16_t crc_calculated = modbus_crc16(modbus->rx_buffer, modbus->rx_len - 2);
    if (crc_received != crc_calculated) {
        modbus->rx_len = 0;
        return;
    }

    // 기능 코드 처리
    uint8_t func_code = modbus->rx_buffer[1];
    uint8_t* tx = modbus->tx_buffer;
    uint8_t tx_len = 0;
    uint8_t exception = 0;

    tx[0] = modbus->rx_buffer[0]; // 슬레이브 ID
    tx[1] = func_code;            // 기능 코드
    tx_len = 2;

    // 기능 코드별 처리
    if (func_code == FC_READ_COILS) {
        uint16_t start_addr = (modbus->rx_buffer[2] << 8) | modbus->rx_buffer[3];
        uint16_t quantity = (modbus->rx_buffer[4] << 8) | modbus->rx_buffer[5];
        if (start_addr + quantity > 256 || quantity == 0 || quantity > 2000) {
            exception = EXC_ILLEGAL_ADDRESS;
        } else {
            uint8_t byte_count = (quantity + 7) / 8;
            tx[2] = byte_count;
            tx_len += 1;
            for (uint16_t i = 0; i < quantity; i++) {
                uint8_t bit = modbus->data->coils[start_addr + i];
                tx[3 + (i / 8)] |= (bit & 0x01) << (i % 8);
            }
            tx_len += byte_count;
        }
    } else if (func_code == FC_READ_DISCRETE_INPUTS) {
        uint16_t start_addr = (modbus->rx_buffer[2] << 8) | modbus->rx_buffer[3];
        uint16_t quantity = (modbus->rx_buffer[4] << 8) | modbus->rx_buffer[5];
        if (start_addr + quantity > 256 || quantity == 0 || quantity > 2000) {
            exception = EXC_ILLEGAL_ADDRESS;
        } else {
            uint8_t byte_count = (quantity + 7) / 8;
            tx[2] = byte_count;
            tx_len += 1;
            for (uint16_t i = 0; i < quantity; i++) {
                uint8_t bit = modbus->data->discrete_inputs[start_addr + i];
                tx[3 + (i / 8)] |= (bit & 0x01) << (i % 8);
            }
            tx_len += byte_count;
        }
    } else if (func_code == FC_READ_HOLDING_REG) {
        uint16_t start_addr = (modbus->rx_buffer[2] << 8) | modbus->rx_buffer[3];
        uint16_t quantity = (modbus->rx_buffer[4] << 8) | modbus->rx_buffer[5];
        if (start_addr + quantity > 256 || quantity == 0 || quantity > 125) {
            exception = EXC_ILLEGAL_ADDRESS;
        } else {
            tx[2] = quantity * 2;
            tx_len += 1;
            for (uint16_t i = 0; i < quantity; i++) {
                uint16_t value = modbus->data->holding_registers[start_addr + i];
                tx[tx_len++] = value >> 8;
                tx[tx_len++] = value & 0xFF;
            }
        }
    } else if (func_code == FC_READ_INPUT_REG) {
        uint16_t start_addr = (modbus->rx_buffer[2] << 8) | modbus->rx_buffer[3];
        uint16_t quantity = (modbus->rx_buffer[4] << 8) | modbus->rx_buffer[5];
        if (start_addr + quantity > 256 || quantity == 0 || quantity > 125) {
            exception = EXC_ILLEGAL_ADDRESS;
        } else {
            tx[2] = quantity * 2;
            tx_len += 1;
            for (uint16_t i = 0; i < quantity; i++) {
                uint16_t value = modbus->data->input_registers[start_addr + i];
                tx[tx_len++] = value >> 8;
                tx[tx_len++] = value & 0xFF;
            }
        }
    } else if (func_code == FC_WRITE_SINGLE_COIL) {
        uint16_t addr = (modbus->rx_buffer[2] << 8) | modbus->rx_buffer[3];
        uint16_t value = (modbus->rx_buffer[4] << 8) | modbus->rx_buffer[5];
        if (addr >= 256 || (value != 0x0000 && value != 0xFF00)) {
            exception = EXC_ILLEGAL_ADDRESS;
        } else {
            modbus->data->coils[addr] = (value == 0xFF00) ? 1 : 0;
            tx[2] = modbus->rx_buffer[2];
            tx[3] = modbus->rx_buffer[3];
            tx[4] = modbus->rx_buffer[4];
            tx[5] = modbus->rx_buffer[5];
            tx_len = 6;
        }
    } else if (func_code == FC_WRITE_SINGLE_REG) {
        uint16_t addr = (modbus->rx_buffer[2] << 8) | modbus->rx_buffer[3];
        uint16_t value = (modbus->rx_buffer[4] << 8) | modbus->rx_buffer[5];
        if (addr >= 256) {
            exception = EXC_ILLEGAL_ADDRESS;
        } else {
            modbus->data->holding_registers[addr] = value;
            tx[2] = modbus->rx_buffer[2];
            tx[3] = modbus->rx_buffer[3];
            tx[4] = modbus->rx_buffer[4];
            tx[5] = modbus->rx_buffer[5];
            tx_len = 6;
        }
    } else if (func_code == FC_WRITE_MULTIPLE_COILS) {
        uint16_t start_addr = (modbus->rx_buffer[2] << 8) | modbus->rx_buffer[3];
        uint16_t quantity = (modbus->rx_buffer[4] << 8) | modbus->rx_buffer[5];
        uint8_t byte_count = modbus->rx_buffer[6];
        if (start_addr + quantity > 256 || quantity == 0 || quantity > 2000 || byte_count != (quantity + 7) / 8) {
            exception = EXC_ILLEGAL_ADDRESS;
        } else {
            for (uint16_t i = 0; i < quantity; i++) {
                modbus->data->coils[start_addr + i] = (modbus->rx_buffer[7 + (i / 8)] >> (i % 8)) & 0x01;
            }
            tx[2] = modbus->rx_buffer[2];
            tx[3] = modbus->rx_buffer[3];
            tx[4] = modbus->rx_buffer[4];
            tx[5] = modbus->rx_buffer[5];
            tx_len = 6;
        }
    } else if (func_code == FC_WRITE_MULTIPLE_REG) {
        uint16_t start_addr = (modbus->rx_buffer[2] << 8) | modbus->rx_buffer[3];
        uint16_t quantity = (modbus->rx_buffer[4] << 8) | modbus->rx_buffer[5];
        uint8_t byte_count = modbus->rx_buffer[6];
        if (start_addr + quantity > 256 || quantity == 0 || quantity > 123 || byte_count != quantity * 2) {
            exception = EXC_ILLEGAL_ADDRESS;
        } else {
            for (uint16_t i = 0; i < quantity; i++) {
                modbus->data->holding_registers[start_addr + i] = 
                    (modbus->rx_buffer[7 + i * 2] << 8) | modbus->rx_buffer[8 + i * 2];
            }
            tx[2] = modbus->rx_buffer[2];
            tx[3] = modbus->rx_buffer[3];
            tx[4] = modbus->rx_buffer[4];
            tx[5] = modbus->rx_buffer[5];
            tx_len = 6;
        }
    } else {
        exception = EXC_ILLEGAL_FUNCTION;
    }

    // 예외 응답
    if (exception) {
        tx[1] = func_code + 0x80;
        tx[2] = exception;
        tx_len = 3;
    }

    // CRC 추가 및 송신
    if (tx_len > 0 && modbus->rx_buffer[0] != 0) {
        uint16_t crc = modbus_crc16(tx, tx_len);
        tx[tx_len++] = crc & 0xFF;
        tx[tx_len++] = crc >> 8;
        platform_uart_write(tx, tx_len);
    }

    // 수신 버퍼 리셋
    modbus->rx_len = 0;
}

9.3 platform.h

STM32L432KC의 하드웨어 인터페이스를 정의합니다.


#ifndef PLATFORM_H
#define PLATFORM_H

#include <stdint.h>
#include "stm32l4xx_hal.h"

// UART, DMA, 타이머 핸들
extern UART_HandleTypeDef huart2;
extern DMA_HandleTypeDef hdma_usart2_rx;
extern DMA_HandleTypeDef hdma_usart2_tx;
extern TIM_HandleTypeDef htim6;

// RS-485 DE 핀
#define RS485_DE_PORT GPIOA
#define RS485_DE_PIN  GPIO_PIN_1

// 링버퍼 설정
#define RING_BUFFER_SIZE 512 // 대량 데이터 처리용

// 링버퍼 구조체
typedef struct {
    uint8_t buffer[RING_BUFFER_SIZE]; // 데이터 저장 배열
    uint16_t head;                    // 쓰기 인덱스 (DMA)
    uint16_t tail;                    // 읽기 인덱스 (애플리케이션)
    uint8_t overflow_flag;            // 오버플로우 플래그
} RingBuffer;

// 플랫폼 함수
void platform_uart_init(void);              // UART 및 DMA 초기화
uint8_t platform_uart_available(void);      // 데이터 사용 가능 여부
uint8_t platform_uart_read(void);           // 데이터 읽기
void platform_uart_write(const uint8_t* data, uint16_t len); // 데이터 쓰기
void platform_timer_reset(void);            // 타이머 리셋
uint8_t platform_timer_expired(uint32_t ticks); // 타이머 만료 확인
void platform_enter_sleep(void);            // Sleep 모드 진입

// 링버퍼 함수
void ring_buffer_init(RingBuffer* rb);      // 링버퍼 초기화
uint8_t ring_buffer_read(RingBuffer* rb, uint8_t* data); // 링버퍼 읽기
uint8_t ring_buffer_available(RingBuffer* rb); // 데이터 사용 가능 여부
uint8_t ring_buffer_is_overflow(RingBuffer* rb); // 오버플로우 확인

#endif // PLATFORM_H

3.4 platform.c

DMA, 링버퍼, 저전력 모드를 구현합니다.


#include "platform.h"

// 링버퍼 인스턴스
static RingBuffer rx_ring_buffer;

// 타이머 카운터 (0.1ms 틱)
static volatile uint32_t timer_count = 0;

// 링버퍼 초기화
void ring_buffer_init(RingBuffer* rb) {
    rb->head = 0;                   // 쓰기 인덱스 초기화
    rb->tail = 0;                   // 읽기 인덱스 초기화
    rb->overflow_flag = 0;          // 오버플로우 플래그 초기화
    memset(rb->buffer, 0, RING_BUFFER_SIZE); // 버퍼 초기화
}

// 링버퍼에서 데이터 읽기
uint8_t ring_buffer_read(RingBuffer* rb, uint8_t* data) {
    if (rb->head == rb->tail) {     // 버퍼가 비어있으면
        return 0;
    }
    *data = rb->buffer[rb->tail];   // 데이터 읽기
    rb->tail = (rb->tail + 1) % RING_BUFFER_SIZE; // 테일 이동
    return 1;
}

// 링버퍼 데이터 사용 가능 여부
uint8_t ring_buffer_available(RingBuffer* rb) {
    return (rb->head != rb->tail);  // 데이터 존재 시 1
}

// 링버퍼 오버플로우 확인
uint8_t ring_buffer_is_overflow(RingBuffer* rb) {
    return rb->overflow_flag;       // 오버플로우 플래그 반환
}

// UART 송신 완료 콜백
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        // RS-485 송신 모드 비활성화
        HAL_GPIO_WritePin(RS485_DE_PORT, RS485_DE_PIN, GPIO_PIN_RESET);
    }
}

// UART 수신 완료/하프 콜백
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        rx_ring_buffer.overflow_flag = 1; // 링버퍼 풀
    }
}

void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        platform_timer_reset();       // 새 데이터 시 타이머 리셋
    }
}

// 타이머 오버플로우 콜백 (0.1ms 틱)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM6) {
        timer_count++;                // 타이머 카운터 증가
    }
}

// UART, DMA, 타이머 초기화
void platform_uart_init(void) {
    ring_buffer_init(&rx_ring_buffer); // 링버퍼 초기화
    HAL_GPIO_WritePin(RS485_DE_PORT, RS485_DE_PIN, GPIO_PIN_RESET); // RS-485 수신 모드
    HAL_UART_Receive_DMA(&huart2, rx_ring_buffer.buffer, RING_BUFFER_SIZE); // DMA 수신
    HAL_TIM_Base_Start_IT(&htim6); // 타이머 인터럽트 시작
}

// 데이터 사용 가능 여부 확인
uint8_t platform_uart_available(void) {
    if (ring_buffer_is_overflow(&rx_ring_buffer)) {
        // 오버플로우 시 링버퍼 초기화 및 DMA 재시작
        ring_buffer_init(&rx_ring_buffer);
        HAL_UART_Receive_DMA(&huart2, rx_ring_buffer.buffer, RING_BUFFER_SIZE);
    }
    return ring_buffer_available(&rx_ring_buffer);
}

// 데이터 읽기
uint8_t platform_uart_read(void) {
    uint8_t data = 0;
    ring_buffer_read(&rx_ring_buffer, &data);
    return data;
}

// 데이터 쓰기 (DMA)
void platform_uart_write(const uint8_t* data, uint16_t len) {
    HAL_GPIO_WritePin(RS485_DE_PORT, RS485_DE_PIN, GPIO_PIN_SET); // RS-485 송신 모드
    HAL_UART_Transmit_DMA(&huart2, (uint8_t*)data, len); // DMA 송신
}

// 타이머 리셋
void platform_timer_reset(void) {
    timer_count = 0;
}

// 3.5T 만료 확인
uint8_t platform_timer_expired(uint32_t ticks) {
    return timer_count >= ticks;
}

// Sleep 모드 진입
void platform_enter_sleep(void) {
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); // WFI로 Sleep
}

9.5 main.c

메인 프로그램과 80MHz 클럭 설정으로 구현합니다.


#include "modbus_rtu.h"
#include "platform.h"
#include "stm32l4xx_hal.h"

// UART, DMA, 타이머 핸들
UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart2_rx;
DMA_HandleTypeDef hdma_usart2_tx;
TIM_HandleTypeDef htim6;

// 모드버스 데이터 저장소
ModbusData modbus_data;

// 시스템 클럭 설정 (80MHz, HSE + PLL)
static void SystemClock_Config(void) {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    // HSE 활성화 (8MHz 외부 크리스털)
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM = 1;  // 8MHz / 1 = 8MHz
    RCC_OscInitStruct.PLL.PLLN = 20; // 8MHz * 20 = 160MHz
    RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // ADC용
    RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // 80MHz (USART)
    RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 80MHz (시스템)
    HAL_RCC_OscConfig(&RCC_OscInitStruct);

    // 시스템 클럭 설정
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4);
}

// UART 설정 (USART2, 9600bps)
static void MX_USART2_UART_Init(void) {
    huart2.Instance = USART2;
    huart2.Init.BaudRate = 9600;
    huart2.Init.WordLength = UART_WORDLENGTH_8B;
    huart2.Init.StopBits = UART_STOPBITS_1;
    huart2.Init.Parity = UART_PARITY_NONE;
    huart2.Init.Mode = UART_MODE_TX_RX;
    huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart2.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart2);
}

// DMA 설정
static void MX_DMA_Init(void) {
    __HAL_RCC_DMA1_CLK_ENABLE();

    // USART2_RX DMA
    hdma_usart2_rx.Instance = DMA1_Channel6;
    hdma_usart2_rx.Init.Request = DMA_REQUEST_2;
    hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart2_rx.Init.Mode = DMA_CIRCULAR;
    hdma_usart2_rx.Init.Priority = DMA_PRIORITY_LOW;
    HAL_DMA_Init(&hdma_usart2_rx);
    __HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx);

    // USART2_TX DMA
    hdma_usart2_tx.Instance = DMA1_Channel7;
    hdma_usart2_tx.Init.Request = DMA_REQUEST_2;
    hdma_usart2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart2_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart2_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_usart2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_usart2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_usart2_tx.Init.Mode = DMA_NORMAL;
    hdma_usart2_tx.Init.Priority = DMA_PRIORITY_LOW;
    HAL_DMA_Init(&hdma_usart2_tx);
    __HAL_LINKDMA(&huart2, hdmatx, hdma_usart2_tx);

    // DMA 인터럽트 활성화
    HAL_NVIC_SetPriority(DMA1_Channel6_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA1_Channel6_IRQn);
    HAL_NVIC_SetPriority(DMA1_Channel7_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA1_Channel7_IRQn);
}

// 타이머 설정 (TIM6, 0.1ms 틱)
static void MX_TIM6_Init(void) {
    htim6.Instance = TIM6;
    htim6.Init.Prescaler = 7999; // 80MHz / (7999+1) = 10kHz
    htim6.Init.Period = 0;       // 0.1ms 틱
    HAL_TIM_Base_Init(&htim6);
}

// GPIO 설정 (RS-485 DE 핀)
static void MX_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    __HAL_RCC_GPIOA_CLK_ENABLE();

    // RS-485 DE 핀 (PA1)
    GPIO_InitStruct.Pin = RS485_DE_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(RS485_DE_PORT, &GPIO_InitStruct);
}

int main(void) {
    // HAL 초기화
    HAL_Init();

    // 시스템 클럭 설정 (80MHz)
    SystemClock_Config();

    // DMA, GPIO, UART, 타이머 초기화
    MX_DMA_Init();
    MX_GPIO_Init();
    MX_USART2_UART_Init();
    MX_TIM6_Init();

    // 모드버스 데이터 초기화
    memset(&modbus_data, 0, sizeof(ModbusData));
    modbus_data.holding_registers[0] = 0x1234; // 테스트 데이터
    modbus_data.coils[0] = 1;

    // 모드버스 RTU 초기화
    ModbusRTU modbus;
    modbus_init(&modbus, 1, &modbus_data);

    // Sleep 모드 인터럽트 활성화
    __HAL_UART_ENABLE_IT(&huart2, UART_IT_RXNE);
    __HAL_TIM_ENABLE_IT(&htim6, TIM_IT_UPDATE);

    // 메인 루프
    while (1) {
        modbus_process(&modbus);
        platform_enter_sleep();
    }
}

// HAL MSP 초기화
void HAL_MspInit(void) {
    __HAL_RCC_SYSCFG_CLK_ENABLE();
    __HAL_RCC_PWR_CLK_ENABLE();
}

// UART 및 타이머 MSP 초기화
void HAL_UART_MspInit(UART_HandleTypeDef* huart) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (huart->Instance == USART2) {
        __HAL_RCC_USART2_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();

        // PA2 (TX), PA3 (RX)
        GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
        GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
}

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) {
    if (htim_base->Instance == TIM6) {
        __HAL_RCC_TIM6_CLK_ENABLE();
        HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 0, 0);
        HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
    }
}

// DMA 인터럽트 핸들러
void DMA1_Channel6_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_usart2_rx);
}

void DMA1_Channel7_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_usart2_tx);
}

10. 구현 세부사항

10.1 시스템 클럭 (80MHz)

80MHz 클럭은 HSE(8MHz)와 PLL로 설정됩니다:

  • PLL: PLLM=1, PLLN=20, PLLR=2 → \( 8 \times 20 / 2 = 80 \) MHz.
  • 플래시 레이턴시: 80MHz에 맞게 4로 설정.
  • 효과: 빠른 처리 속도와 안정적인 타이밍.

10.2 DMA와 링버퍼

  • RX DMA: Circular 모드로 링버퍼에 연속 수신.
  • TX DMA: Normal 모드로 단일 송신, 완료 후 DE 핀 LOW.
  • 링버퍼: 512바이트, head(DMA), tail(애플리케이션) 관리.
  • 오버플로우: overflow_flag로 감지, 버퍼 초기화.

10.3 타이밍 정밀도

TIM6은 80MHz 클럭에서 0.1ms 틱으로 설정:

  • 프리스케일러: 7999 → \( 80 \, \text{MHz} / 8000 = 10 \, \text{kHz} \).
  • 3.5T: 9600bps에서 3.64ms ≈ 36틱.

10.4 저전력 모드

Sleep 모드는 CPU를 정지시키고 인터럽트로 깨어납니다:

  • 인터럽트: UART, DMA, TIM6.
  • 전력 소모: 1mA 이하 (STM32L432KC 저전력 특성).

11. STM32CubeIDE 설정

  1. 프로젝트: STM32L432KC 프로젝트 생성, 위 파일 추가.
  2. 클럭: HSE 8MHz, PLL로 80MHz.
  3. 하드웨어:
    • USART2: 9600bps, 8-N-1, DMA (RX: Circular, TX: Normal).
    • TIM6: 10kHz (0.1ms 틱).
    • GPIO: PA1 (DE), PA2/PA3 (TX/RX, AF7).
  4. 인터럽트: DMA1_Channel6, DMA1_Channel7, TIM6_DAC 활성화.
  5. 빌드: ST-Link로 플래시.

12. 테스트 방법

  1. 연결: STM32L432KC와 MAX485 연결 (PA2-TX, PA3-RX, PA1-DE).
  2. 테스트:
    • 기능 코드 0x03: 홀딩 레지스터 0번 (0x1234).
    • 기능 코드 0x05: 코일 0번 쓰기.
    • 오버플로우: 대량 데이터 송신.
    • 저전력: Sleep 모드 전류 측정.
  3. 디버깅: STM32CubeIDE로 변수 확인, Modbus Poll로 메시지 검증.

13. 제한사항과 개선사항

13.1 제한사항

  • 메모리: 64KB 플래시, 20KB SRAM으로 제한.
  • 속도: 9600bps, 고속 통신 시 조정 필요.
  • 인터럽트: 빈번한 인터럽트로 CPU 부하 가능.

13.2 개선사항

  • 512바이트 링버퍼, 오버플로우 처리.
  • DMA 송수신으로 CPU 부하 감소.
  • 0.1ms 틱으로 정밀 타이밍.
  • Sleep 모드로 저전력.

14. 결론

이 글에서는 STM32L432KC로 Modbus RTU 슬레이브를 구현하는 방법을 다뤘습니다. 80MHz 클럭, DMA, 링버퍼, 저전력 모드로 최적화된 코드를 공유합니다.  산업 자동화에 활용 가능한 이 라이브러리를 통해 임베디드 시스템을 효율적으로 구축하세요.

반응형