-
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개의 핀(데이터 부분) 만으로 데이터를 전송할 수 있도록 하는 기법입니다. 시프트 레지스터와 관련해서는 추후 포스트를 작성하도록 하겠습니다.
절차
- ESP32-C6에 핀 역할 설정 및 하드웨어에 핀 연결
- 데이터 전송 함수 작성
- 도트 매트릭스 초기화
- 데이터 전송 (아래 과정을 8번 반복)
- CS를 Low 상태로 설정
- 행 번호 및 데이터 (1바이트 = 8비트)를 전송. (이것을 4번 진행)
- DIN을 통해 비트를 전송하고 비트를 전송할때마다 CLK을 펄스(pulse) 시킴.
- 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도 회전된 출력이 됩니다.
- 칩 순서 반전
- (3 - i) → 좌우 반전
- 비트 반전
- reverseBits(value) → 한 바이트 내부의 좌우 반전
- 비트의 순서를 반전합니다. 예를 들어 10001000인 경우 순서를 뒤집으면 00010001
- 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 반응형'공부 > Swift(프로그래밍 언어)' 카테고리의 다른 글
Embedded Swift: 그림 및 텍스트가 스크롤되는 도트 매트릭스 전광판 만들기 (0) 2026.05.18 [Swift iOS] Lite 버전 배포 - 이미 유료로 개발 및 판매중인 앱을 일반 버전, Lite 버전으로 나눠서 앱스토어에 배포하는 방법 (0) 2026.05.08 Embedded Swift: 핀(GPIO Pin) 제어하기 (2) - 입력 받기 (Input Mode) (1) 2026.04.29 [SwiftUI] SF Symbol에서 계층, 팔레트, 여러 가지 색상 사용하기 (0) 2026.04.28 Embedded Swift: 핀(GPIO Pin) 제어하기 (1) - LED 깜빡이기 (0) 2026.04.23