ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Embedded Swift: 그림 및 텍스트가 스크롤되는 도트 매트릭스 전광판 만들기
    공부/Swift(프로그래밍 언어) 2026. 5. 18. 00:18
    반응형

    ESP32-C6 + idf.py + Embedded Swift 6.2 를 이용해 마이크로컨트롤러에서 시프트 레지스터를 사용하는 32×8 도트 매트릭스로 그림이 스크롤되는 전광판 구현하기

    Embedded Swift 시리즈 목록 (분량상 모든 내용을 다 설명할 수 없어 생략한 부분이 있으니 일부 내용은 이전 글들을 참고해주세요.)
    이전글: Embedded Swift에서 도트 매트릭스 사용 방법 기초 보기

     

    여러 개의 32×8 도트 이미지를 하나의 매우 긴 가상 도트매트릭스로 연결한 뒤, 4개의 MAX7219 모듈 위에서 비트 단위로 좌측 스크롤하는 예제입니다.

    스크롤 화면. 동작 GIF는 포스트 하단에 있습니다.

     

    코드 보기

    static func scroll() {
      dm.reset()
      let pin = GPIOPin(pinNumber: 11, mode: .inputOnly(.pullDownOnly))
      
      // https://dotmatrixtool.com/ 에서 그리고 배열의 값을 복사해서 붙여넣기
      // 32px by 8px, row major, big endian.
      let arrays: [[UInt8]] = [
        [
          0x85, 0xf4, 0x10, 0x3c, 0xc5, 0x04, 0x10, 0x42, 0xa5, 0x04, 0x10, 0x40,
          0x95, 0xe4, 0x90, 0x4e, 0x8d, 0x05, 0x50, 0x42, 0x85, 0x06, 0x30, 0x42,
          0x85, 0xf4, 0x10, 0x3c, 0x00, 0x00, 0x00, 0x00,
        ],
        [
          0x31, 0x05, 0xf0, 0x00, 0x49, 0x8d, 0x00, 0x00, 0x85, 0x55, 0x00, 0x00,
          0x85, 0x25, 0xe0, 0x00, 0xfd, 0x05, 0x00, 0x00, 0x85, 0x05, 0x00, 0x00,
          0x85, 0x05, 0xf2, 0xa0, 0x00, 0x00, 0x00, 0x00,
        ],
        [
          0x48, 0x51, 0xe3, 0x80, 0xfe, 0xf8, 0x00, 0x00, 0x48, 0x23, 0xf7, 0xc0,
          0x5c, 0x50, 0x11, 0x00, 0xc8, 0xf8, 0x11, 0x00, 0x5c, 0x20, 0x61, 0x00,
          0xea, 0xa9, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00,
        ],
        [
          0xe9, 0xf7, 0x2a, 0x22, 0x28, 0x42, 0x6a, 0xfa, 0x2d, 0xb5, 0x2e, 0x22,
          0x28, 0x44, 0x2a, 0x5b, 0x01, 0xf1, 0x8e, 0x82, 0xf8, 0xe2, 0x40, 0x32,
          0x09, 0x12, 0x44, 0x48, 0x08, 0xe1, 0x9f, 0x30,
        ],
        [
          0x7c, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
          0x14, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
          0x10, 0x00, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00,
        ],
      ]
     
      // 위의 2차원 배열을 행끼리 모아서 1차원 배열로 합침
      var merged: [UInt8] = []
    
      for row in 0..<8 {
        for array in arrays {
          let start = row * 4
          merged += Array(array[start..<(start + 4)])
        }
      }
      let totalChips = merged.count / 8
    
      // 반복 함수, t는 정해진 시간(1초)마다 1씩 증가
      loop { t in
        if pin.read() == 1 {
          dm.turnOffLED()
          return
        }
    
        for row in 0..<8 {
          // Chip Select 1 사이클: dm.cs.low -> dm.cs.high
          dm.csCycle {
            for i in 0..<4 {
              let chipIndex = (i + t / 8) % totalChips
    
              let value = merged[(row * totalChips) + chipIndex]
              let source = merged[(row * totalChips) + ((chipIndex + 1) % totalChips)]
              let shiftedValue = shiftAppend(to: value, from: source, bitCount: t)
    
              dm.sendByte(UInt8(row + 1))
              dm.sendByte(shiftedValue)
            }
          }
        }
      }
    }

     

    전체 흐름 요약

    [그림1][그림2][그림3][그림4][그림5]

    이렇게 여러 이미지를 하나의 긴 띠처럼 이어 붙인 뒤, 한 비트씩 왼쪽으로 밀어가는 방식입니다.

    그림은 https://dotmatrixtool.com/ 에서 그릴 수 있습니다.

    1. 여러 개의 32×8 이미지를 준비
    2. 행(row) 단위로 긴 merged 배열로 병합
    3. 현재 스크롤 위치 t에 따라 표시할 구간 계산
    4. 현재 칩 + 다음 칩 데이터 가져오기
    5. 비트 시프트 + 이어붙이기 (shiftAppend)
    6. 4개의 MAX7219에 출력
    7. t 증가 후 반복

    결과적으로 24칩(예시) 규모의 거대한 가상 화면을 만들고, 그중에서 실제 4칩 부분만 잘라서 실시간으로 보여주는 방식입니다.

     

    배열 준비

    let arrays: [[UInt8]] = [...]
    • 여기에 여러 개의 32×8 이미지가 들어갑니다.
    • 각 이미지는 row-major 방식으로 저장되어 있으며, 한 이미지는 8행 × 4바이트(32바이트)로 구성됩니다. (각 MAX7219 칩이 1바이트를 담당)
    •  https://dotmatrixtool.com/  에서 32px by 8px, row major, big endian. 형식으로 그린 뒤 배열을 복사합니다.
      • 그림 한 개당 내부 배열 1개를 차지합니다.
      • 그림 5개를 그리면 5개의 행 배열과 각 행 배열 당 32개의 원소가 있어야 합니다.

     

    메모리 구조

    한 이미지의 내부 구조는 다음과 같습니다.

    row0: [chip0][chip1][chip2][chip3]
    row1: [chip0][chip1][chip2][chip3]
    ...
    row7: [chip0][chip1][chip2][chip3]
    • row * 4 + chipIndex 형태로 원하는 바이트에 접근할 수 있습니다.
    • 각 그림의 행끼리 모아놓는 것이 중요합니다.

    각 행끼리 데이터를 모아야 합니다.

     

    merged 배열 만들기 

    var merged: [UInt8] = []
    for row in 0..<8 {
        for array in arrays {
            let start = row * 4
            merged += Array(array[start..<(start + 4)])
        }
    }
    • 단순히 flatMap으로 이어붙이지 않고 행(row) 단위로 옆으로 붙입니다.
    • 실제로 MAX7219에 데이터를 출력할 때 for row in 0..<8 루프로 행 단위로 보내기 때문입니다.
    • start: 1차원 배열에서 행 데이터가 새로 시작되는 위치입니다.
      • start 에서 그 옆의 4개 데이터 (UInt8 숫자 네 개로 4바이트=32비트에 해당)를 merged에서도 같은 행끼리 만나도록 추가합니다.

    각 start의 위치 = 각 행의 시작 위치

     

    병합 후 구조

    예를 들어 그림이 6개라면

    • 한 행당 4칩 × 5개 = 20칩
    • totalChips = 20

    전체 merged 배열은 아래처럼 됩니다:

    row0: [g0c0][g0c1][g0c2][g0c3][g1c0]...[g4c3]
    row1: [g0c0][g0c1]...
    ...

     

    totalChips 계산

    let totalChips = merged.count / 8

    merged는 8행 × 전체 칩 수로 구성되어 있으므로, 8로 나누면 가로로 이어진 총 칩 개수가 나옵니다.

     

    스크롤 루프 (t 값)

    loop { t in ... }

    t는 스크롤 진행 정도를 나타내는 값으로, 0, 1, 2, 3... 계속 증가합니다. 즉 몇 비트만큼 이동했는가를 의미합니다.

     

    loop 보기 (더보기 클릭)

    더보기
    func loop(
      delay ms: UInt32 = 100, maxTCount: Int = 0, handler: ((_ t: Int) -> Void)
    ) {
      var t = 0
      while true {
        if (maxTCount > 0 && t >= maxTCount) || t == .max {
          t = 0
          continue
        }
    
        handler(t)
    
        t += 1
    
        delay(ms: ms)
      }
    }

     

    현재 표시할 칩 계산

    let chipIndex = (i + t / 8) % totalChips
    • i: 실제 물리적인 MAX7219 위치 (0~3)
    • t / 8: 8비트(1바이트)가 지나면 다음 칩으로 넘어감
    • % totalChips: 끝까지 가면 다시 처음으로 돌아오는 무한 루프

     

    출력할 데이터 가져오기

    let value = merged[(row * totalChips) + chipIndex]          // 현재 칩
    let source = merged[(row * totalChips) + ((chipIndex + 1) % totalChips)] // 다음 칩
    • value: 현재 위치의 도트 바이트 값입니다.
    • source: 다음 위치의 도트 바이트 값입니다.

     

    왜 두 개의 바이트가 필요한가?

    비트 단위 스크롤을 하기 위해서입니다.

    예를 들어 현재 바이트가 10110000이고 왼쪽으로 1비트 이동하면 0110000?가 됩니다.
    ? 자리에 다음 칩의 MSB(최상위 비트)를 넣어줘야 이미지가 자연스럽게 이어집니다. (구현 방법은 shiftAppend 함수 이용, 밑에서 설명)

    value에서 가장 왼쪽 비트를 버리고, source의 왼쪽 비트를 가져옴

     

    shiftAppend 함수

    shiftAppend(to: value, from: source, bitCount: t)

    현재 바이트를 t만큼 시프트하고, 다음 바이트의 상위 비트를 적절히 붙여주는 함수입니다. 

     

    shiftAppend  코드 보기

    /// rhs 의 상위 비트를 시프트한 후 lhs에 붙임, 0b10110000, 0b11110000 이라는 숫자가 있을 때 bitCount = 3인 경우 10000111 이 됨.
    func shiftAppend(to lhs: UInt8, from rhs: UInt8, bitCount move: Int = 1)
      -> UInt8
    {
      let move = move % 8
      let upper = rhs >> (8 - move)
      return (lhs << move) | upper
    }
    
    func chunkArray32<T: Collection>(_ array: T) -> [[T.Element]]
    where T.Index == Int {
      stride(from: 0, to: array.count, by: 32).map {
        Array(array[$0..<min($0 + 32, array.count)])
      }
    }

     

    MAX7219로 전송

    for row in 0..<8 {
      // Chip Select 1 사이클: dm.cs.low -> dm.cs.high
      dm.csCycle {
        for i in 0..<4 {
          // ... //
    
          dm.sendByte(UInt8(row + 1))
          dm.sendByte(shiftedValue)
        }
      }
    }
    • MAX7219는 (레지스터 주소, 데이터) 형식으로 보내는데, row + 1이 행 주소가 됩니다. 즉, 1부터 시작합니다.
    • dm.csCycle은 칩 1개에 데이터를 전송하는 사이클이며 low에서 high로 변했을 때(펄스) 다음 칩으로 옮겨가게 됩니다.

     

     

    Embedded Swift: 핀(GPIO Pin) 제어하기 (3) - 도트 전광판 제어 (MAX7219 32x8 도트 매트릭스 LED 5핀제어)

    ESP32-C6 + idf.py + Embedded Swift 6.2 를 이용해 마이크로컨트롤러에서 시프트 레지스터를 사용하는 도트 매트릭스와 연결해 제어하는 방법에 대해 알아보겠습니다.Embedded Swift 시리즈 목록 (분량상 모

    infoarmory.tistory.com

     

    dm.csCycle 코드 보기 (더보기 클릭)

    더보기
    struct DotMatrixManager {
      let cs: GPIOPin
      let din: GPIOPin
      let clk: GPIOPin
    
      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()
      }
    
      func turnOffLED() {
        // 모든 LED 끄기
        for row in 1...8 {
          send(register: UInt8(row), data: 0x00)
        }
      }
    
      func sendBit(_ bit: Bool) {
        din.set(bit)
        clk.pulse()
      }
    
      func sendByte(_ byte: UInt8) {
        for i in (0..<8).reversed() {
          let bit = (byte >> i) & 0x01
          din.set(bit == 1)
          clk.pulse()
        }
      }
    
      func send(register: UInt8, data: UInt8) {
        cs.low()
        for _ in 0..<4 {
          sendByte(register)
          sendByte(data)
        }
        cs.high()
      }
    
      // 👇 csCycle
      func csCycle(handler: () -> Void) {
        cs.low()
        handler()
        cs.high()
      }
    }

     

    동작 화면

    하드웨어 연결 방법은 이전 글의 ESP32-C6에 핀 역할 설정 및 하드웨어에 핀 연결 섹션을 참고하세요.

    scroll 함수를 플래시(idf.py build flash monitor)하면 다음과 같이 스크롤되는 화면이 뜹니다.


    스크롤 화면 GIF
     
     
     
     
     
     

     

    응용하기 -  텍스트 입력해서 전광판에 스크롤하기

    웹사이트를 통해 그림을 일일히 그리지 않고도 텍스트를 입력하면 자동으로 흐르도록 설정할 수 있습니다. 이에 대한 구체적인 설명은 다음 포스트에 올리겠습니다.

    let textMatrix: [UInt8] = generateTextMatrix("  $\"\u{11}\u{12}\u{13}\u{14}The quick brown fox jumps over the lazy dog\"is an English-language pangram &")
    let totalChips = textMatrix.count / 8
    // 이후 과정 동일

     

    텍스트 입력해서 전광판에 스크롤하는 코드 보기 (gist)

     

    Embedded Swift: 도트매트릭스 텍스트 스크롤

    Embedded Swift: 도트매트릭스 텍스트 스크롤. GitHub Gist: instantly share code, notes, and snippets.

    gist.github.com

     

    반응형
Designed by Tistory.