-
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/ 에서 그릴 수 있습니다.
- 여러 개의 32×8 이미지를 준비
- 행(row) 단위로 긴 merged 배열로 병합
- 현재 스크롤 위치 t에 따라 표시할 구간 계산
- 현재 칩 + 다음 칩 데이터 가져오기
- 비트 시프트 + 이어붙이기 (shiftAppend)
- 4개의 MAX7219에 출력
- 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 / 8merged는 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) % totalChipsi: 실제 물리적인 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
반응형'공부 > Swift(프로그래밍 언어)' 카테고리의 다른 글