ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Embedded Swift: 핀(GPIO Pin) 제어하기 (3) - 도트 전광판 제어 (MAX7219 32x8 도트 매트릭스 LED 5핀제어)
    공부/Swift(프로그래밍 언어) 2026. 5. 3. 23:25
    반응형
    ESP32-C6 + idf.py + Embedded Swift 6.2 를 이용해 마이크로컨트롤러에서 시프트 레지스터를 사용하는 도트 매트릭스와 연결해 제어하는 방법에 대해 알아보겠습니다.
    Embedded Swift 시리즈 목록 (분량상 모든 내용을 다 설명할 수 없어 생략한 부분이 있으니 일부 내용은 이전 글들을 참고해주세요.)

     

    MAX7219 32x8 도트 매트릭스 모듈은 8x8 도트 매트릭스 4개가 가로로 결합된 디스플레이 모듈로, ESP-32와 같은 마이크로컨트롤러를 사용하여 텍스트 스크롤이나 간단한 그래픽을 표현하는 데 널리 사용됩니다. 
    MAX7219 32x8 도트 매트릭스 모듈

     

     

    주요 사양 및 특징

    • 구성: 4개의 MAX7219 드라이버 IC가 각각 8x8 LED 매트릭스를 제어하며, 내부적으로 직렬(Cascading) 연결되어 있습니다.
    • 작동 전압: DC 5V.
    • 인터페이스: 3핀 직렬 SPI 통신(DIN, CS, CLK)을 사용하여 마이크로컨트롤러의 핀 소모를 최소화합니다.
    • 확장성: 입출력 인터페이스를 갖추고 있어 여러 개의 모듈을 줄지어 연결(Daisy-chain)하여 더 긴 화면을 만들 수 있습니다.
    • 제어: 16단계의 밝기 조절이 가능하며, 개별 도트 단위로 제어할 수 있습니다.

     

    ESP32-C6에 연결

    VCC 5V 전원 공급 (밝기 설정에 따라 외부 전원 권장)
    GND GND 접지
    DIN 커스텀 핀에 연결 (MOSI) 데이터 입력
    CS 커스텀 핀에 연결 (SS) 칩 선택 (LATCH)
    CLK 커스텀 핀에 연결 (SCK) 클럭 신호

     

    시프트 레지스터

    이 도트 매트릭스는 MAX7219 칩 4개를 시프트 레지스터로 연결한 것입니다. 시프트 레지스터(Shift Register)는 여러 개의 플립플롭을 연결하여 클럭 신호에 맞춰 데이터를 한 방향으로 이동(Shift)하며 저장하는 순차 논리 회로입니다. 주로 데이터를 직렬(Serial)에서 병렬(Parallel)로, 또는 그 반대로 변환하거나, 데이터를 일시 저장 및 이동시키는 용도로 디지털 시스템에서 사용됩니다.

    간단하게 말하면, 원래대로라면 32*8 = 256 개의 핀이 필요했을 전광판에 특수 기술을 적용해서 3개의 핀(데이터 부분) 만으로 데이터를 전송할 수 있도록 하는 기법입니다. 시프트 레지스터와 관련해서는 추후 포스트를 작성하도록 하겠습니다.

     

    절차

    1. ESP32-C6에 핀 역할 설정 및 하드웨어에 핀 연결
    2. 데이터 전송 함수 작성
    3. 도트 매트릭스 초기화
    4. 데이터 전송 (아래 과정을 8번 반복) 
      1. CS를 Low 상태로 설정
      2. 행 번호 및 데이터 (1바이트 = 8비트)를 전송. (이것을 4번 진행)
        • DIN을 통해 비트를 전송하고 비트를 전송할때마다 CLK을 펄스(pulse) 시킴. 
      3. CS를 High로 설정하면 데이터가 전송됨

     

    ESP32-C6에 핀 역할 설정 및 하드웨어에 핀 연결

    이용 가능한 핀 목록을 파악해 CS, DIN, CLK의 역할을 부여합니다.

     

    Embedded Swift: 핀(GPIO Pin) 제어하기 (1) - LED 깜빡이기

    GPIO(General Purpose Input/Output) 핀은 마이크로컨트롤러나 라즈베리 파이 같은 임베디드 시스템에서 외부 센서, LED, 모터 등과 디지털 신호(0 또는 1, High/Low)를 주고받는 범용 입출력 핀입니다. 프로그

    infoarmory.tistory.com

    struct DotMatrixManager {
      let cs: GPIOPin
      let din: GPIOPin
      let clk: GPIOPin
    }
    struct ExampleDotMatrix {
      private static let dm = DotMatrixManager(
        cs: .init(pinNumber: 4),
        din: .init(pinNumber: 5),
        clk: .init(pinNumber: 6)
      )
    }

     

    ESP32-C6의 4, 5, 6번 핀을 각각 도트 매트릭스 모듈의 CS, DIN, CLK 핀과 연결합니다. 도트 매트릭스의 VCC, GND 핀은 (+), (-) 전원 레일에 연결합니다.

    핀 연결

     

    데이터 전송 함수 작성

    다음 코드는 시프트 레지스터 기반으로 데이터를 전송하는 핵심 로직입니다. 바이트 단위 데이터를 비트로 쪼개서 순차적으로 전송하고, 특정 레지스터에 값을 쓰는 방식으로 동작합니다. 

     

    sendByte(_:)

    이 함수는 1바이트(8비트) 데이터를 MSB(최상위 비트)부터 LSB(최하위 비트)까지 하나씩 전송합니다.

    도트 매트릭스에 그림 또는 문자 비트맵을 전송할 때 이용하는 함수입니다.

    func sendByte(_ byte: UInt8) {
      for i in (0..<8).reversed() {
        let bit = (byte >> i) & 0x01
        din.set(bit == 1)
        clk.pulse()
      }
    }

     

    (0..<8).reversed()

    MSB와 LSB

    • 7부터 0까지 역순으로 순회합니다.
    • 즉, 가장 왼쪽 비트(MSB)부터 전송합니다.

     

    (byte >> i) & 0x01

    i = 7인 경우

    • i번째 비트를 오른쪽으로 이동시킨 뒤, 마지막 1비트만 추출합니다.
    • 결과는 0 또는 1입니다.
    • 이것을 i = 7~0 순으로 반복하면 MSB → LSB (왼쪽 → 오른쪽) 순으로 비트값을 추출하는 것과 동일합니다.

     

    din.set(bit == 1)

    • 데이터 핀(DIN)에 해당 비트를 출력합니다.
    • 1이면 HIGH, 0이면 LOW로 설정됩니다.

     

    clk.pulse()

    func set(_ level: Bool) {
      gpio_set_level(gpio_num_t(Int32(pinNumber)), level ? 1 : 0)
    }
    
    func pulse() {
      set(true)
      set(false)
    }
    • 클럭 신호를 한 번 발생시켜, 현재 DIN 값이 장치 내부로 “시프트”되도록 합니다.

    즉, 이 함수는 데이터 핀에 비트를 올려놓고 클럭을 한 번 일으켜 밀어 넣는 과정을 8번 반복하여 1바이트(=8비트)를 전송합니다.

     

    send(register:data:) 함수

    func send(register: UInt8, data: UInt8) {
      cs.low()
      for _ in 0..<4 {
        sendByte(register)
        sendByte(data)
      }
      cs.high()
    }

    이 함수는 특정 레지스터에 값을 쓰는 역할을 합니다. 이 함수는 똑같은 값을 여러 칩에 보낼 때 사용하며, 초기 설정을 할 때 이용합니다.

    • cs.low()
      • Chip Select(CS)를 LOW로 내려 통신 시작을 알립니다.
      • 이 구간에서 들어오는 데이터만 유효하게 처리됩니다.
    • for _ in 0..<4
      • 같은 데이터를 4번 반복해서 전송합니다.
      • 이는 보통 여러 개의 드라이버 IC가 직렬로 연결(데이지 체인)된 상황을 의미합니다.
      • 각 IC는 앞에서 들어온 데이터를 뒤로 넘기면서 자신의 데이터만 취하게 됩니다.
    • sendByte(register) → 레지스터 주소 전송
    • sendByte(data) → 해당 레지스터에 쓸 값 전송
      • 즉, 한 쌍(레지스터 + 데이터)을 4번 보내면서 체인에 연결된 4개의 디바이스 각각에 동일한 명령을 전달합니다.
    • cs.high()
      • CS를 다시 HIGH로 올리면 지금까지 시프트된 데이터가 각 IC에 “확정(latch)”됩니다.

     

    도트 매트릭스 초기화

    이 부분은 전원 인가 직후 반드시 한 번 실행해야 하는 기본 초기화 루틴이며, 이후 원하는 패턴을 출력하기 위한 준비 단계라고 볼 수 있습니다.

    다음 코드는 LED 드라이버(예: MAX7219)를 초기 상태로 설정하고, 모든 LED를 끄는 역할을 합니다. 앞서 작성한 send(register:data:) 함수를 기반으로 각 레지스터를 설정하는 구조입니다.

     

    reset() 함수

    장치의 동작 모드를 초기화하는 역할을 합니다.

    func reset() {
      // 초기화
      send(register: 0x0C, data: 0x01)  // shutdown off
      send(register: 0x0F, data: 0x00)  // test off
      send(register: 0x0B, data: 0x07)  // scan limit
      send(register: 0x09, data: 0x00)  // decode off
      send(register: 0x0A, data: 0x03)  // intensity 낮게
    
      turnOffLED()
    }

    각 레지스터 설정을 하나씩 보면 다음과 같습니다.

    • 0x0C (Shutdown Register):
      • 디바이스를 정상 동작 상태로 전환합니다.
      • 0x01로 설정하여 셧다운 모드를 해제합니다.
    • 0x0F (Display Test Register):
      • 전원만 연결했을 시 테스트 모드로 기동됩니다. 테스트 모드에서는 모든 LED가 강제로 켜지므로, 일반 사용 시 반드시 꺼야 합니다.
      • 0x00으로 설정하여 테스트 모드를 끕니다.
    • 0x0B (Scan Limit Register)
      • 8x8 매트릭스를 전체 활성화하는 설정입니다.
      • 0x07은 0~7까지, 즉 8개의 row를 모두 사용한다는 의미입니다.
    • 0x09 (Decode Mode Register)
      • 본래 MAX7219는 7세그먼트 8숫자(LED 64개)를 제어하기 위한 칩이었는데, 그것을 일반 도트 LED(8*8=64)로 바꾼 것이 도트 매트릭스 입니다.
      • 따라서 7세그먼트 숫자 디코딩이 아니라, LED 매트릭스를 직접 제어할 때 사용하는 설정입니다.
      • 0x00으로 설정하여 디코드 모드를 사용하지 않습니다.

    원래 7 Segment용으로 제작된 MAX7219

    • 0x0A (Intensity Register)
      • 밝기를 설정하는 레지스터입니다.
      • 0x03은 비교적 낮은 밝기 값입니다 (0x00 ~ 0x0F 범위).
    • 마지막으로 turnOffLED()를 호출하여 초기 화면을 모두 꺼진 상태로 만듭니다.

     

    turnOffLED() 함수

    LED 매트릭스의 모든 행(row)에 0을 써서 전체를 끄는 역할을 합니다.

    func turnOffLED() {
      // 모든 LED 끄기
      for row in 1...8 {
        send(register: UInt8(row), data: 0x00)
      }
    }

    동작 방식은 다음과 같습니다.

    • row는 1부터 8까지 순회합니다 → MAX7219에서는 각 row(또는 digit)가 1~8 레지스터에 매핑되어 있습니다.
    • data: 0x00 → 8비트 모두 0이므로 해당 row의 모든 LED가 꺼집니다.

     

    즉, 이 루프는 “1번 행부터 8번 행까지 전부 0을 써서 화면을 클리어한다”는 의미입니다.

     

    데이터 전송

    전체 코드

     

    Embedded Swift를 이용하여 MAX7219 32 * 8 Dot Matrix 전광판 조작하는 방법

    Embedded Swift를 이용하여 MAX7219 32 * 8 Dot Matrix 전광판 조작하는 방법 - DotMatrixManager.swift

    gist.github.com

    [1] 양쪽 끝에 점찍기

    다음 코드는 4개의 MAX7219가 가로로 연결된 32×8 LED 매트릭스에서 특정 좌표 (x, y)에 점을 찍는 예제입니다.

    결과 화면

     

    초기화 및 반복 구조

    static func dot() {
      dm.reset()
    
      for i in 0...1 {
        let x = i == 0 ? 0 : 31  // 0 ~ 31
        let y = i == 0 ? 0 : 7  // 0 ~ 7
    • dm.reset()
      • 앞서 설명한 초기화 루틴을 호출하여 디바이스를 정상 상태로 만듭니다.
    • for i in 0...1
      • 예제로 두 번 반복하며, 서로 다른 좌표에 점을 찍습니다.
      • 처음 점은 가장 왼쪽 상단, 다음 점은 가장 오른쪽 하단에 찍습니다.
    • (x, y) 설정
      • 전체 매트릭스 크기가 32×8임을 전제로 합니다. 
      • 첫 번째는 (0, 0) (좌측 상단)
      • 두 번째는 (31, 7) (우측 하단)

     

    칩 인덱스와 비트 위치 계산

    let chipIndex = x / 8
    let bitIndex = x % 8
    • chipIndex = x / 8
      • 32*8 도트 매트릭스에는 4개의 MAX7219 칩이 내장되어 있습니다. 32칸을 8칸씩 나누면 총 4개의 MAX7219가 필요합니다.
      • 현재 x 좌표가 몇 번째 칩에 해당하는지 계산합니다.
      • 예:
        • x = 0 → 0번 칩
        • x = 15 → 1번 칩 (15/8 = 1)
        • x = 31 → 3번 칩 (31/8 =3)
    • bitIndex = x % 8
      • 해당 칩 내부에서 몇 번째 LED인지 계산합니다.
      • 0~7 범위의 값이 됩니다.

     

    버퍼 구성 (어느 칩에 점을 찍을지 결정)

    var buffer = Array(repeating: UInt8(0), count: 4)
    buffer[chipIndex] = 1 << bitIndex
    • buffer
      • 4개의 칩 각각에 보낼 데이터를 담는 배열입니다.
      • 초기값은 모두 0 (LED 꺼짐)
    • 1 << bitIndex
      • 특정 비트만 1로 만들어 “한 점”을 표현합니다.
      • 예: bitIndex = 2 → 00000100
    • buffer[chipIndex]에만 값 설정
      • 나머지 칩은 0, 즉 아무 것도 표시하지 않음
      • 결과적으로 “하나의 칩에만 점이 찍힘”

     

    전송 시작 (CS 제어)

    dm.cs.low()
    • CS를 LOW로 내려 전송 시작
    • 이 구간에서 들어가는 데이터가 하나의 프레임으로 처리됩니다.

     

    데이지 체인 전송 구조

    for i in stride(from: 3, through: 0, by: -1) {
      dm.sendByte(UInt8(8 - y))
      dm.sendByte(buffer[i])
    }
    • stride(from: 3, through: 0, by: -1)
      • 3 → 2 → 1 → 0 순서로 전송
      • 가장 먼 칩부터 먼저 데이터를 넣어야 최종 위치가 맞습니다.
      • 시프트 레지스터 특성: 먼저 들어간 데이터가 끝으로 밀림

     

    row(세로 위치) 처리

    dm.sendByte(UInt8(8 - y))
    • MAX7219의 row는 1~8로 표현됩니다.
    • LED가 위치한 방향에 따라 좌표를 반전시켜야 할 경우도 있습니다. 스마트폰에서 같은 가로 방향이라도 180도 회전하는 것과 같은 이치입니다. 방향과 관련된 내용은 예제 2번에서 자세히 다룹니다.
    • 이 코드에서는 “세로만 반전”된 상태를 가정하고 있습니다.
    • 8 - y를 사용하는 이유
      • y=0 (위쪽) → row=8
      • y=7 (아래쪽) → row=1
      • 즉, 화면의 상하를 뒤집어서 보정하는 역할

     

    column(가로 위치) 데이터 전송

    dm.sendByte(buffer[i])
    • 각 칩에 대해 8비트 데이터를 전송
    • 해당 비트 위치만 1이므로 LED 한 점만 켜짐
    • buffer[i]를 사용하는 이유
      • 현재 순회 중인 칩에 맞는 데이터를 보내기 위함
      • 특정 칩만 1, 나머지는 0

     

    전송 종료 (Latch)

    dm.cs.high()
    • CS를 HIGH로 올리는 순간 지금까지 밀어 넣은 데이터가 각 칩에 확정됩니다.
    • 이 시점에서 LED 상태가 실제로 바뀝니다.


    [2] 방향을 고려해서 그림 또는 문자 넣기

    핀 방향에 따른 좌표 조정

    1. 기본 방향: 핀이 오른쪽에 있음 (칩: 정방향)
    이 방향을 기준으로 하면 좌표 위치를 조절하지 않아도 됩니다.
        [1][2][3][4]=
        row: 1, 2, 3, 4 (정방향)

    2. 핀이 왼쪽에 있음 (칩: 역방향)
        =[4][3][2][1]
        row: 4, 3, 2, 1 (역방향)

    3. 핀이 아래쪽에 있음 (칩: 정방향)
        [1] col: 8, 7, 6, 5(역방향)
        [2]
        [3]
        [4]
        ||

    4. 핀이 위쪽에 있음 (칩: 역방향)
        || col: 4, 3, 2, 1 (정방향)
        [4]
        [3]
        [2]
        [1]

     

    도트 매트릭스 그리는 사이트

     

    Dot Matrix Tool - LCD Font Generator

    Left mouse button to draw. Right mouse button (or ctrl+left) to erase.

    dotmatrixtool.com

     

    코드 구현

    다음 예제는 4개의 MAX7219로 구성된 32×8 LED 매트릭스에서 표시 방향(orientation) 을 바꿔가며 같은 데이터를 서로 다른 방식으로 출력하는 예제입니다. 가로/세로, 그리고 좌우/상하 반전까지 포함하여 총 4가지 상태를 순환합니다.

    이 네 가지를 조합하면 같은 데이터라도 회전, 반전, 방향 전환을 모두 구현할 수 있습니다.

     

    전체 구조

    static func orientation1() {
      dm.reset()
      let input = GPIOPin(pinNumber: 11, mode: .inputOnly(.pullDownOnly))
    • dm.reset(): 디스플레이 초기화
    • input
      • 버튼 입력을 받아 표시 모드를 변경하기 위한 GPIO
      • pull-down이므로 눌렸을 때 1이 됩니다
     

    Embedded Swift: 핀(GPIO Pin) 제어하기 (2) - 입력 받기 (Input Mode)

    ESP32-C6 + idf.py + Embedded Swift 6.2 를 이용해 마이크로컨트롤러에서 핀을 통해 입력을 받고 그에 대한 작업을 처리하는 방법에 대해 설명하겠습니다.Embedded Swift 시리즈 목록 (분량상 모든 내용을 다

    infoarmory.tistory.com

     

    이미지 데이터 구조 (landscape vs portrait)

    let landscapeMatrix: [UInt8] = [ ... ]
    let portraitMatrix: [UInt8] = [ ... ]

    두 배열은 같은 이미지를 서로 다른 방식으로 저장한 것입니다. Dot Matrix Tool 에서 설정을 다음과 같이 한 뒤 그림을 그리면 됩니다.  (Big-Endian으로 설정)

    그림이 완성되면 사이트 아래에 C/C++ 배열이 생성되는데 내용만 복사해서 붙여넣으면 됩니다.

    • landscapeMatrix
      • 32×8 (가로형)
      • row-major (행 우선): 한 row에 4바이트 (32bit)
      • landscape는 “한 줄씩 잘라서 저장”
    • portraitMatrix
      • 8×32 (세로형)
      • column-major (열 우선): 한 column 단위로 데이터가 구성됨
      • portrait는 “세로줄 기준으로 저장”

     

    모드 전환 루프

    var index = 0
    
    while true {
      switch index {
      case 0: landscape()
      case 1: landscapeReversed()
      case 2: portrait()
      case 3: portraitReversed()
      case 4: dm.turnOffLED()
    • index에 따라 출력 방식이 바뀝니다.

    index동작

    0 가로 (정방향)
    1 가로 (좌우 + 상하 반전)
    2 세로
    3 세로 반전
    4 전체 끄기

     

    버튼 입력 처리

    let current = input.read()
    if current == 1 {
      index += 1
    }
    • 버튼을 누르면 다음 모드로 넘어갑니다.

     

    landscape() — 기본 가로 출력

    func landscape() {
      for row in 0..<8 {
        dm.csCycle {
          for i in 0..<4 {
            let value = landscapeMatrix[(row * 4) + i]
            dm.sendByte(UInt8(row + 1))
            dm.sendByte(value)
          }
        }
      }
    }

     

    • (row * 4) + i: 한 row에 4개의 칩 데이터가 있음
    • row + 1: MAX7219 row는 1~8
    • i = 0 → 3: 왼쪽 → 오른쪽 순서

    즉, 정방향 그대로 출력입니다.

     

    landscapeReversed() — 가로 + 좌우/상하 반전

    func landscapeReversed() {
      for row in 0..<8 {
        dm.csCycle {
          for i in 0..<4 {
            let value = landscapeMatrix[(row * 4) + (3 - i)]
            let reversed = reverseBits(value)
            dm.sendByte(UInt8(8 - row))
            dm.sendByte(reversed)
          }
        }
      }
    }

    여기서는 3가지 반전이 동시에 일어납니다. 결과적으로 180도 회전된 출력이 됩니다.

    1. 칩 순서 반전
      • (3 - i)좌우 반전
    2. 비트 반전
      • reverseBits(value) → 한 바이트 내부의 좌우 반전
      • 비트의 순서를 반전합니다. 예를 들어 10001000인 경우 순서를 뒤집으면 00010001
    3. row 반전
      • 8 - row → 상하 반전

     

    portrait() — 세로 방향 출력

    func portrait() {
      for row in 0..<8 {
        dm.csCycle {
          for i in 0..<4 {
            let value = portraitMatrix[row + (i * 8)]
            dm.sendByte(UInt8(8 - row))
            dm.sendByte(value)
          }
        }
      }
    }
    • row + (i * 8) → column-major 접근 방식
    • 8 - row → 세로 방향 보정

    portrait 데이터는 구조가 다르기 때문에 인덱싱 방식이 landscape와 다릅니다.

     

    portraitReversed() — 세로 + 반전

    func portraitReversed() {
      for row in 0..<8 {
        dm.csCycle {
          for i in 0..<4 {
            let value = portraitMatrix[row + ((3 - i) * 8)]
            let reversed = reverseBits(value)
            dm.sendByte(UInt8(1 + row))
            dm.sendByte(reversed)
          }
        }
      }
    }

    여기도 동일하게 3가지 변환이 적용됩니다.

    • (3 - i) → 칩 순서 반전
    • reverseBits → 비트 순서 반전
    • 1 + row → row 방향 보정 (portrait 기준)

     

    csCycle의 역할

    dm.csCycle {
      ...
    }

    이 함수는 단순 편의를 위해 작성된 것으로 내부적으로 다음을 수행합니다.

    • cs.low()
    • 데이터 전송 (클로저에 작성)
    • cs.high()

     

    결과 GIF

    반응형
Designed by Tistory.