STM32F103 DMA + Timer 기반 GPIO 출력 실험 (Logic Analyzer 검증)

개요

개발 중에 있는 제품은 32bit Arm Cortex-M Core MCU를 활용해 제품의 전체 제어를 수행하고 있습니다.
모든 동작이 잘 작동을 하고 있으나 속도적인 측면에서 조금 더 개선을 하고 싶은 생각이 들었습니다.

MCU의 역할은 매우 다양합니다. 열전사 프린터 헤드 데이터 전송, 스텝모터 구동, 서보모터 제어, AC 모터 및 DC 모터 구동, 솔레노이드 제어, 온도 PID 제어, 각종 센서 입력 처리 등 거의 모든 제어를 하나의 MCU에서 담당하고 있습니다.

그러나 모든 기능을 단일 MCU에서 처리하다 보니 동시 작업이 원활하지 않은 상황이 발생하였고, 이는 전체 속도 저하의 원인이 되었습니다. 그중에서도 특히 CPU 자원을 가장 많이 사용하는 부분은 열전사 프린터 헤드로 데이터를 전송하는 과정이었습니다.

현재는 Bit-banging 방식, 즉 CPU가 GPIO를 직접 비트 단위로 제어하는 방식으로 데이터를 전송하고 있습니다. 이 방식은 구현이 직관적이라는 장점은 있지만, CPU 점유율이 매우 높다는 단점이 있습니다.

이 문제를 해결하기 위해 CPU 개입을 최소화할 수 있는 구조를 고민하게 되었고, 그 대안으로

STM32 DMA GPIO 출력 방식을 적용해 보기로 하였습니다. 본 글에서는 해당 구조를 STM32F103 개발 보드에서 구현하고, Logic Analyzer를 통해 실제 동작을 검증한 과정을 정리하고자 합니다.

실험 준비

아래는 실험을 위해서 필요한 사항을 열거 하였습니다.

개발 보드

STM32F103 계열 개발 보드는 시중에 매우 다양한 형태로 유통되고 있습니다. 본 실험에서는 AliExpress에서 구입한 저가형 F103 개발 보드를 사용하였습니다. 가격 대비 성능이 우수하고, 실험용으로 부담 없이 사용할 수 있다는 점이 장점입니다.

이번 실험의 목적은 고급 기능 구현이 아니라 DMA + Timer + GPIO 구조 검증이므로, 보드의 종류는 크게 중요하지 않습니다. F103 계열 MCU가 탑재되어 있다면 대부분 동일하게 적용할 수 있습니다.

STM32 Debugger

펌웨어 다운로드 및 디버깅을 위해 ST-LINK 디버거가 필요합니다.
STM32용 디버거로는 ST-LINK/V2, ST-LINK/V3, ST-LINK/V3 MINIE 등이 사용됩니다.

본 실험에서는 STMicroelectronics사의 NUCLEO-F429ZI 보드에 내장된 ST-LINK를 활용하였습니다. 내장 디버거를 SWD 방식으로 연결하여 펌웨어를 다운로드하였습니다.

별도의 ST-LINK가 없다면, NUCLEO 보드를 디버거로 활용하는 것도 좋은 방법입니다.

Lgic Analyzer

GPIO 출력 파형을 확인하기 위해 Logic Analyzer를 사용하였습니다. 오실로스코프를 이용해도 파형을 볼 수는 있지만, 디지털 신호의 타이밍 관계를 분석하기에는 로직 애널라이저가 훨씬 편리합니다.

본 실험에서는 Kingst LA1010을 사용하였습니다.
Timer 주기와 DMA에 의해 출력되는 GPIO 패턴이 의도한 대로 동작하는지를 확인하는 것이 핵심 목적입니다.

개발 Tool Software

개발 환경으로는 STM32CubeIDE를 사용하였습니다. STM32CubeIDE에는 그래픽 기반으로 핀 설정과 주변장치 구성을 할 수 있는 CubeMX 기능이 포함되어 있어, Timer 및 DMA 설정을 직관적으로 구성할 수 있습니다.

STM32CubeIDE는 STMicroelectronics 공식 웹사이트에서 무료로 다운로드할 수 있으며, 코드 작성, 빌드, 다운로드 및 디버깅을 하나의 환경에서 수행할 수 있습니다.
STM32CubeIDE 다운로드

실험 구성

아래 사진과 같이 실험을 위해 해당 장비들을 연결하였습니다.

STM32F103 개발 보드

좌측 하단의 보드는 STM32F103이 탑재된 개발 보드입니다.
본 실험에서 DMA + Timer + GPIO 구조를 구현하고, 실제 신호를 출력하는 역할을 수행합니다.

GPIO 핀 중 하나를 선택하여 출력 대상으로 사용하였으며, 해당 핀을 Logic Analyzer에 연결하였습니다.

ST-LINK 디버거 (NUCLEO 보드 활용)

상단의 보드는 NUCLEO-F429ZI 보드이며, 내장된 ST-LINK를 디버거로 활용하였습니다.
SWD 방식으로 F103 보드와 연결하여 펌웨어를 다운로드하고 디버깅을 수행합니다.

NUCLEO 보드를 디버거 전용으로 사용하는 방법은 별도의 ST-LINK 장비가 없는 경우 매우 유용합니다.

Logic Analyzer

우측의 장비는 Kingst LA1010입니다.
STM32F103의 GPIO 출력 핀과 직접 연결하여 실제 파형을 캡처합니다.

펌웨어 개발 목표 및 동작 흐름

펌웨어 개발 목표

이번 실험의 목표는 단순한 GPIO 출력이 아닙니다.
다음 두 가지를 동시에 만족하는 구조를 구현하는 것입니다.

– 열전사 프린터용 고속 GPIO 패턴 출력 (DMA + Timer 기반)
– 스텝모터 구동을 위한 1.2ms 주기의 안정적인 스텝 펄스 생성

기존 구조에서는 Bit-banging 방식으로 프린터 데이터를 출력하고 있었으며, 동시에 스텝모터 펄스를 소프트웨어 타이밍으로 생성하고 있었습니다.
이렇게 할 경우 솔레노이드제어, 온도제어, 모터제어 등 다른 제어를 동시에 할 수 없고 오로지 열전사 프린터를 위한 동작만 해야 하고 이 동작이 끝나야
다른 동작을 할 수 있게 되어 동작 시간이 지연되는 원인이 되고 있습니다.

따라서 이번 실험의 목표는 CPU는 상위 제어만 수행하고 데이터를 출력하는 것은 DMA에 맡김으로써 다른 태스크와의 동시 작업이 가능하도록 하는 것이 목표 입니다.

펄스 생성 및 데이터 전송

이번 실험에서는 두 가지 종류의 펄스를 생성합니다.

첫 번째는 스텝모터 구동을 위한 1.2ms 주기의 스텝 펄스이고,
두 번째는 프린터 데이터 전송을 위한 고속 클럭 및 데이터 신호입니다.

스텝모터 펄스는 1µs 단위의 타이머 기준을 사용하여 생성합니다.
600µs 동안 “High”, 600µs 동안 “Low”를 출력하여 총 1.2ms 주기를 구성하며, 이를 800회 반복하여 800개의 스텝 펄스를 생성합니다.

반면, 프린터 출력에 필요한 클럭 및 데이터 신호는 DMA + Timer 기반의 하드웨어 타이밍을 이용하여 생성합니다. 즉,
– 스텝 펄스 → micros() 기반 소프트웨어 타이밍
– 프린터 데이터 전송 → DMA + Timer 기반 하드웨어 타이밍
으로 역할을 구분하였습니다.

동작 흐름

이번 실험의 전체 동작 흐름은 다음과 같습니다.

가) 초기 설정
– 1MHz free-running Timer 설정 (micros() 기준)
– DMA 및 Timer 설정 (프린터 데이터 전송용)
– GPIO 출력 설정

나) 스텝 펄스 생성
- micros()를 이용하여 600µs 간격으로 STEP 핀을 토글
– 총 800개의 스텝 펄스 생성

다) 데이터 전송
– 스텝 펄스 동작 중 DMA를 시작
– Timer 트리거에 의해 GPIO 패턴 자동 출력

즉, CPU는 상위 제어와 타이밍 판단만 수행하고,
고속 데이터 출력은 DMA에 위임하는 구조입니다.

펌웨어 개발

펌웨어 구조

아래 그림은 본 프로젝트의 디렉터리 구조를 나타낸 것입니다.

CubeMX가 생성하는 CoreDrivers 영역과 별도로, MyApp 디렉터리를 생성하여 사용자 코드를 분리하였습니다.
- def.h : 공통 매크로 및 전역 정의
- app.c / app.h : 애플리케이션 진입점
- stepper_motor.c : 스텝모터 제어
- gpio_shift_dma.c : DMA 기반 GPIO 출력
- utils_time.c : micros() 기반 시간 시스템
- scheduler.c : 주기 플래그 관리

main()에서는 HAL 초기화 이후 appInit()appMain()을 호출하며, 실제 제어 로직은 appMain() 내부에서 수행됩니다. 이 구조를 통해:
CubeMX 재생성 시 사용자 코드 보존
기능별 모듈 분리
유지보수성 향상
을 확보하였습니다.

 

기본 인프라 프로시저

본 프로젝트에서는 RTOS를 사용하지 않고, Non-Blocking 방식으로 여러 작업을 처리하기 위한 간단한 스케줄러와, µs 단위 시간 기준을 제공하는 시간 관리 유틸리티를 사용합니다.

스케줄러는 1ms 주기 타이머를 기반으로 주기 플래그를 생성하며,(scheduler.c, scheduler.h)
micros() 함수는 1µs 해상도의 시간 기준을 제공합니다.(utils_time.c, utils_time.h)

이 두 요소는 DMA 기반 데이터 전송을 안정적으로 구동하기 위한 보조 인프라 역할을 합니다.

스케줄러는 TIM2를 1ms 주기로 동작시키고, 인터럽트에서 소프트웨어 플래그를 설정하는 방식으로 구성하였습니다.
– 1ms 기준 카운터 증가
– 5ms / 10ms / 50ms / 100ms / 500ms 등의 플래그 생성
– 메인 루프에서 Scheduler_GetFlag_xxx() 형태로 확인
이 구조는 RTOS 없이도 여러 작업을 분산 처리할 수 있으며, DMA 동작 중에도 메인 루프가 정지하지 않도록 합니다.

시간관리 utility는 us 단위의 시간을 관리할 수 있도록 합니다.
1MHz free-running Timer(TIM3)를 시간 기준으로 사용하여 생성합니다.
본 프로젝트에서는 이것을 이용하여 스텝모터의 1.2ms 주기 펄스를 생성합니다.

DMA 기반 GPIO 출력

본 실험의 핵심은 GPIO를 CPU가 아닌 DMA를 통해 구동하는 구조입니다.

기존 방식에서는 Bit-banging으로 GPIO를 직접 제어하였으며,
이는 구현은 단순하지만 CPU 점유율이 매우 높다는 단점이 있습니다.
특히 열전사 프린터와 같이 고속 데이터 전송이 필요한 경우,
출력 타이밍을 유지하기 위해 CPU가 거의 해당 작업에 묶이게 됩니다.

이를 개선하기 위해,
GPIO 출력 패턴을 메모리에 미리 구성하고,
Timer를 트리거로 하여 DMA가 GPIO 레지스터(BSRR)에 직접 데이터를 전송하도록 구성하였습니다.

즉,
– CPU → 데이터 패턴 생성
– Timer → 전송 타이밍 생성
– DMA → GPIO 레지스터로 자동 전송
이라는 역할 분리를 통해, 고속 GPIO 출력을 하드웨어 레벨에서 처리하도록 하였습니다.

이 구조의 핵심 목적은 다음과 같습니다.
– CPU 점유율 최소화
– 정밀한 출력 타이밍 확보
– 스텝모터 제어와 동시 동작 가능

이제부터는 이 DMA 기반 GPIO 출력이 어떻게 구성되었는지,
버퍼 구조와 타이머 트리거 방식, 그리고 실제 파형 검증 결과를 순차적으로 설명합니다.

가) BSRR 기반 GPIO 제어 구조

STM32의 GPIO는 단순히 ODR(Output Data Register)만으로 제어하는 것이 아닙니다.
DMA 기반 제어를 가능하게 해 주는 핵심 레지스터는 BSRR(Bit Set Reset Register) 입니다.

이 레지스터의 구조는 Reference Manual RM0390에 자세히 나와 있으며 아래 링크에서 다운로드 받을 수 있습니다.
Reference Manual RM0390

아래는 위 레퍼런스 매뉴얼에 나와 있는 BSRR 레지스터의 구조 입니다.

BSRR는 32비트 구조로 다음과 같이 동작합니다.
– 하위 16비트 → 해당 핀을 High(Set)
– 상위 16비트 → 해당 핀을 Low(Reset)
즉, 하나의 32비트 값을 쓰는 것만으로 특정 핀을 Set 또는 Reset 할 수 있습니다.

예를 들어,

BSRR는 Write-Only 레지스터이므로 Read-Modify-Write 과정이 필요하지 않으며,
하나의 32비트 쓰기 동작이 원자적으로 수행됩니다.

이러한 특성 덕분에 DMA가 메모리의 값을 GPIOx->BSRR로 직접 전송하는 구조가 가능해집니다.

나) DMA 데이터 흐름 구조

BSRR를 사용하면 GPIO는 단순한 32비트 쓰기 대상으로 바뀝니다.
이제 남은 문제는, 이 32비트 데이터를 어떤 타이밍으로, 어떻게 반복적으로 써 줄 것인가입니다.

이를 위해 다음과 같은 구조를 구성하였습니다.

전체 데이터 흐름
DMA 기반 GPIO 출력의 전체 흐름은 다음과 같습니다.

여기서 중요한 점은, DMA의 목적지 주소(Destination Address)를 GPIOx->BSRR 레지스터 주소로 설정한다는 것입니다.
DMA는 메모리의 32비트 값을 하나씩 읽어
GPIOx->BSRR에 그대로 기록합니다.

Timer –> DMA 트리거 구조
DMA는 자동으로 동작하지 않습니다.
전송 타이밍을 만들어 줄 트리거가 필요합니다.
본 실험에서는 Timer Update Event를 DMA 요청 트리거로 사용합니다.
구조는 다음과 같습니다.

즉, Timer가 일정 주기로 Update Event를 발생시키면 그 순간 DMA가 한 번 동작하여 다음 GPIO 패턴을 출력합니다.
이 구조를 통해:
– 출력 타이밍은 Timer가 결정
– 데이터 전송은 DMA가 수행
– CPU는 전송 시작만 지시
하는 완전한 역할 분리가 이루어집니다.

DMA설정 핵심 포인트
DMA 설정에서 중요한 부분은 다음과 같습니다.
– Peripheral Address → &GPIOx->BSRR
– Memory Address → GPIO 패턴 버퍼
– Data Width → 32bit (Word)
– Memory Increment → Enable
– Peripheral Increment → Disable
– Transfer Direction → Memory to Peripheral
Peripheral Increment를 Disable 하는 이유는,
항상 동일한 BSRR 주소로 데이터를 써야 하기 때문입니다.

CPU의 역할
CPU는 다음 단계까지만 수행합니다.
– GPIO 출력 패턴을 메모리에 구성
– DMA 전송 시작
– 완료 인터럽트 확인 (필요 시)
그 이후의 실제 고속 GPIO 토글은 Timer + DMA 조합이 전적으로 수행합니다.
이것이 Bit-banging 방식과의 가장 큰 차이점입니다.

다) GPIO 패턴 버퍼 구성 방식

우리 프로젝트는 데이터를 전송하기 위해 두 개의 포트가 필요 합니다. 하나는 데이터, 다른 하나는 클럭을 위한 것 입니다.
두 개의 포트에 데이터와 클럭을 출력하기 위해 메모리 버퍼를 구성해야 합니다.

BSRR 값으로 신호 표현하기
BSRR는 32비트 값 하나로 특정 핀을 Set 또는 Reset 할 수 있습니다.
예를 들어,
– DATA 핀 → PA0
– CLK 핀 → PA1
이라고 가정하면,

처럼 정의할 수 있습니다.
이제 한 클럭을 다음과 같이 표현할 수 있습니다.
– DATA 설정
– CLK High
– CLK Low
즉, 하나의 데이터 비트를 출력하기 위해 여러 개의 BSRR 값이 필요합니다.

비트 –> BSRR 패턴 변환
예를 들어 1비트를 출력한다고 가정하면 아래 그림과 같이 3개의 패턴이 필요하며

과정을 BSRR값으로 표현하면
데이터가 1이면

데이터가 0이면

즉, 하나의 비트를 출력하기 위해 여러 개의 BSRR 값이 필요하며, 이와 같이 각 비트를 여러 개의 BSRR 값으로 확장(expand) 하여
배열에 저장합니다.

예를들어 1Byte를 구성하면 아래 그림과 같이 구성 할 수 있습니다.

아래는 버퍼를 생성하는 실제 코드 입니다. 56 Byte를 생성합니다.

라) 중요 코드

중요 코드는 다음과 같습니다.

아래는 버퍼를 구성하는 루프 코드 입니다.
각 비트별로 Data, Clock High, Clock Low를 BSRR값으로 버퍼를 생성합니다.

우리 프로그램은 DMA로 출력을 하기 위해 Timer를 사용합니다.
즉, 보내고자 하는 속도로 Timer를 설정 한 다음 Timer가 클럭 마다 DMA로 출력을 요청하는 방식입니다.
아래는 Timer를 설정하는 코드 입니다.


다음은 DMA를 BSRR과 연결하는 코드 입니다.

위 코드들을 HAL기반으로 다시 쓰면 다음과 같습니다.

Timer의 Update 이벤트를 DMA 트리거로 사용(UDE)하고, DMA의 목적지 주소를 GPIOx->BSRR로 설정하면, Timer 주기마다 DMA가 한 워드씩 BSRR에 기록하여 GPIO 파형을 하드웨어로 재생할 수 있습니다.

출력 실험 및 Logic Analyzer 검증

실행되는 코드를 만들어서 실험을 하였습니다.
전체 코드는 마지막에 다운로드를 할 수 있도록 링크를 만들었습니다.
아래는 로직아날라이저이 LA1010으로 출력되는 것을 확인한 그림입니다.
4개의 Channel이 있으며 Channel 0는 Stepper Motor 클럭, Channel 1은 Stepper Motor Enable, Channel 2는 DMA 출력 클럭, Channel 3은 DMA출력 Data입니다.

아래 그림은 위 그림을 확대한 그림입니다.

Channel 2(DMA Clock)를 보면 일정한 주기로 High/Low가 반복되는 것을 확인할 수 있습니다.
이 주기는 Timer Update 이벤트에 의해 결정되며, CPU 실행 흐름과는 무관하게 일정하게 유지됩니다.

이는 GPIO 토글이 CPU 루프가 아니라 Timer + DMA 하드웨어 조합에 의해 수행되고 있음을 보여줍니다.

Logic Analyzer 결과는 다음을 확인해 줍니다.
– Timer 기반 DMA 트리거 정상 동작
– BSRR 전송 구조 정상 구현
– CPU 점유와 무관한 일정한 파형 유지
– Step 제어와 DMA 출력 동시 수행 가능

결론

이번 실험에서는 STM32F103 환경에서 GPIO를 CPU 기반 Bit-banging 방식이 아닌, Timer + DMA + BSRR 구조를 이용한 하드웨어 기반 출력 방식으로 전환하는 과정을 구현하고 검증하였습니다.

기존 방식에서는 GPIO를 직접 토글해야 했기 때문에 CPU 점유율이 높았고, 고속 데이터 출력 시 다른 작업과의 병행이 어려웠습니다. 특히 열전사 프린터와 같이 일정한 타이밍이 요구되는 환경에서는 출력 루프가 시스템 전체의 병목이 되는 문제가 있었습니다.

이에 대해 본 실험에서는 다음과 같은 구조를 적용하였습니다.
– GPIO 출력 파형을 메모리 버퍼로 구성
– Timer Update 이벤트를 DMA 트리거로 사용
– DMA가 GPIOx->BSRR에 직접 32비트 데이터를 기록

이 구조를 통해 출력 타이밍은 Timer가 결정하고, 실제 데이터 전송은 DMA가 수행하도록 하여 CPU의 개입을 최소화하였습니다.

Logic Analyzer 검증 결과,
– DMA Clock은 설정한 주기로 안정적으로 유지되었으며
– Data는 Clock과 정확히 정렬되어 출력되었고
– 스텝모터의 1.2ms 펄스 역시 DMA 동작과 무관하게 일정하게 유지됨을 확인하였습니다.

이는 출력 제어가 더 이상 소프트웨어 루프에 의존하지 않고, 하드웨어 계층으로 분리되었음을 의미합니다.

본 구조는 열전사 프린터뿐만 아니라,
LED 매트릭스 제어, 병렬 인터페이스 출력, 고속 신호 생성 등
다양한 응용 분야에 적용할 수 있습니다.

아래는 전체 프로젝트를 다운로드 받을 수 있는 링크입니다.
DMA+Timer+GPIO 출력 프로젝트
이 프로젝트는 저소음 Stepper Motor Driver인 TMC2208을 시험하면서 같이 실험을 한 것 입니다.

댓글 남기기