Repository navigation
Chapter 17
CPU가 직접 for문으로 데이터를 복사하지 않고, DMA2 Stream0 hardware가 source array를 destination array로 복사하도록 구성한다.
sensor_data_arr
{892, 731, 1234, 90, 23}
│
│ DMA2 Stream0
▼
temp_data_arr
{892, 731, 1234, 90, 23}전송 완료 후:
DMA Transfer Complete
↓
TCIF0 = 1
↓
DMA2_Stream0_IRQHandler()
↓
g_transfer_cmplt = 1
↓
UART 결과 출력이번 실습:
DMA : DMA2
Stream : Stream0
Mode : Memory-to-Memory
Data Size : 16-bit
Source : sensor_data_arr[]
Dest : temp_data_arr[]
Length : 5
Interrupt : Transfer Complete / Transfer ErrorCPU가 직접 배열을 복사한다면:
for (int i = 0; i < BUFFER_SIZE; i++)
{
temp_data_arr[i] = sensor_data_arr[i];
}동작은:
CPU
↓
Read Source
↓
Write Destination
↓
Read Source
↓
Write Destination
↓
...즉 데이터 이동을 CPU가 직접 수행한다.
DMA를 사용하면:
CPU
↓
DMA 설정
↓
Start
↓
DMA Hardware
├── Read Source
├── Write Destination
├── Read Source
├── Write Destination
└── ...
CPU
→ 다른 작업 가능
이 된다.
STM32F439ZI에는 DMA controller가 존재하고 각 controller는 여러 Stream을 가진다.
이번 실습:
DMA2
└── Stream0를 사용한다.
DMA transfer의 기본 요소:
Source Address
Destination Address
Data Count
Data Width
Transfer Direction
Address Increment
Interrupt이 설정되면 hardware가 데이터를 직접 이동한다.
이번 코드에서 핵심:
DMA2_Stream0->CR
DMA2_Stream0->NDTR
DMA2_Stream0->PAR
DMA2_Stream0->M0AR
DMA2_Stream0->FCR
DMA2->LISR
DMA2->LIFCR
역할:
| Register | 역할 |
|---|---|
| CR | Direction, size, increment, interrupt, enable |
| NDTR | 전송할 data 개수 |
| PAR | Source address |
| M0AR | Destination address |
| FCR | FIFO configuration |
| LISR | Stream0 interrupt status |
| LIFCR | Stream0 interrupt flag clear |
Project:
Inc/
├── dma.h
└── uart.h
Src/
├── dma.c
├── uart.c
└── main.c
#ifndef DMA_H__
#define DMA_H__
#include <stdint.h>
#include "stm32f4xx.h"
/*
- Configure DMA2 Stream0 for:
-
-
Memory → Memory
-
- transfer.
-
- Data width:
-
-
16-bit
-
- Source / destination address:
-
-
automatically incremented.
*/
void dma2_mem2mem_config(void);
/*
- Start one DMA transfer.
-
- src_buff:
-
source address
-
- dest_buff:
-
destination address
-
- len:
-
number of 16-bit data items
*/
void dma_transfer_start(
uint32_t src_buff,
uint32_t dest_buff,
uint32_t len
);
#endif
#include "dma.h"
/*
- ============================================================
- DMA Register Definitions
- ============================================================
*/
/*
- RCC AHB1ENR:
-
- Enable DMA2 peripheral clock.
*/
#define DMA2EN RCC_AHB1ENR_DMA2EN
/*
- DMA Stream Control Register EN bit.
-
- EN = 1
- → Stream active
-
- EN = 0
- → Stream disabled
*/
#define DMA_SCR_EN DMA_SxCR_EN
/*
- Memory Increment Mode.
-
- After each transfer:
-
- destination address += data size
*/
#define DMA_SCR_MINC DMA_SxCR_MINC
/*
- Peripheral Increment Mode.
-
- In memory-to-memory mode,
- PAR is used as the source address.
-
- Therefore PINC causes the source address
- to increase after every transfer.
*/
#define DMA_SCR_PINC DMA_SxCR_PINC
/*
- Transfer Complete Interrupt Enable.
*/
#define DMA_SCR_TCIE DMA_SxCR_TCIE
/*
- Transfer Error Interrupt Enable.
*/
#define DMA_SCR_TEIE DMA_SxCR_TEIE
/*
- Disable Direct Mode.
-
- DMDIS = 1
-
- → FIFO mode enabled.
*/
#define DMA_SFCR_DMDIS DMA_SxFCR_DMDIS
/*
- Stream0 Transfer Complete flag.
*/
#define LISR_TCIF0 DMA_LISR_TCIF0
/*
- Stream0 Transfer Error flag.
*/
#define LISR_TEIF0 DMA_LISR_TEIF0
/*
- Clear Stream0 Transfer Complete flag.
*/
#define LIFCR_CTCIF0 DMA_LIFCR_CTCIF0
/*
- Clear Stream0 Transfer Error flag.
*/
#define LIFCR_CTEIF0 DMA_LIFCR_CTEIF0
/*
- ============================================================
- DMA2 Stream0 Configuration
- ============================================================
*/
void dma2_mem2mem_config(void)
{
/*
* --------------------------------------------------------
* Step 1. Enable DMA2 Clock
* --------------------------------------------------------
*
* DMA2 registers cannot be used until
* the AHB1 peripheral clock is enabled.
*/
RCC->AHB1ENR |= DMA2EN;
/*
* --------------------------------------------------------
* Step 2. Disable Stream Before Configuration
* --------------------------------------------------------
*
* DMA configuration fields must not be modified
* while the stream is enabled.
*
* Writing CR = 0 requests Stream0 disable.
*/
DMA2_Stream0->CR = 0U;
/*
* IMPORTANT:
*
* Hardware may need a short time to actually
* complete the disable operation.
*
* Therefore wait until EN really becomes 0.
*/
while ((DMA2_Stream0->CR & DMA_SCR_EN) != 0U)
{
}
/*
* --------------------------------------------------------
* Step 3. Memory Data Size = 16-bit
* --------------------------------------------------------
*
* MSIZE<span>[1:0]</span>:
*
* 00 → 8-bit
* 01 → 16-bit
* 10 → 32-bit
*
* Set:
*
* bit13 = 1
* bit14 = 0
*
* → 16-bit
*/
DMA2_Stream0->CR |= (1U << 13);
DMA2_Stream0->CR &= ~(1U << 14);
/*
* --------------------------------------------------------
* Step 4. Source Data Size = 16-bit
* --------------------------------------------------------
*
* PSIZE<span>[1:0]</span>:
*
* In memory-to-memory mode PAR is used
* for the source address.
*
* 01 → 16-bit
*/
DMA2_Stream0->CR |= (1U << 11);
DMA2_Stream0->CR &= ~(1U << 12);
/*
* --------------------------------------------------------
* Step 5. Destination Address Increment
* --------------------------------------------------------
*
* MINC = 1
*
* Destination:
*
* temp_data_arr<span>[0]</span>
* ↓
* temp_data_arr<span>[1]</span>
* ↓
* temp_data_arr<span>[2]</span>
* ↓
* ...
*/
DMA2_Stream0->CR |= DMA_SCR_MINC;
/*
* --------------------------------------------------------
* Step 6. Source Address Increment
* --------------------------------------------------------
*
* PINC = 1
*
* Source:
*
* sensor_data_arr<span>[0]</span>
* ↓
* sensor_data_arr<span>[1]</span>
* ↓
* sensor_data_arr<span>[2]</span>
* ↓
* ...
*/
DMA2_Stream0->CR |= DMA_SCR_PINC;
/*
* --------------------------------------------------------
* Step 7. Select Memory-to-Memory Direction
* --------------------------------------------------------
*
* DIR<span>[1:0]</span>:
*
* 00 → Peripheral-to-Memory
* 01 → Memory-to-Peripheral
* 10 → Memory-to-Memory
*
* Therefore:
*
* bit6 = 0
* bit7 = 1
*/
DMA2_Stream0->CR &= ~(1U << 6);
DMA2_Stream0->CR |= (1U << 7);
/*
* --------------------------------------------------------
* Step 8. Enable Transfer Complete Interrupt
* --------------------------------------------------------
*
* When NDTR reaches zero after successful
* transfer:
*
* TCIF0 = 1
*
* and an interrupt is generated.
*/
DMA2_Stream0->CR |= DMA_SCR_TCIE;
/*
* --------------------------------------------------------
* Step 9. Enable Transfer Error Interrupt
* --------------------------------------------------------
*
* If a transfer error occurs:
*
* TEIF0 = 1
*/
DMA2_Stream0->CR |= DMA_SCR_TEIE;
/*
* --------------------------------------------------------
* Step 10. Enable FIFO Mode
* --------------------------------------------------------
*
* DMDIS = 1
*
* Direct Mode Disabled
* → FIFO Mode Enabled
*/
DMA2_Stream0->FCR |= DMA_SFCR_DMDIS;
/*
* --------------------------------------------------------
* Step 11. FIFO Threshold = Full
* --------------------------------------------------------
*
* FTH<span>[1:0]</span> = 11
*
* FIFO threshold:
*
* Full
*/
DMA2_Stream0->FCR |= (1U << 0);
DMA2_Stream0->FCR |= (1U << 1);
/*
* --------------------------------------------------------
* Step 12. Enable Stream0 IRQ in NVIC
* --------------------------------------------------------
*
* DMA transfer completion/error can now
* generate a Cortex-M interrupt.
*/
NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
void dma_transfer_start(
uint32_t src_buff,
uint32_t dest_buff,
uint32_t len
)
{
/*
* --------------------------------------------------------
* Step 1. Set Source Address
* --------------------------------------------------------
*
* In this Mem-to-Mem configuration:
*
* PAR
*
* is used as the SOURCE address.
*
* Example:
*
* &sensor_data_arr[0]
*/
DMA2_Stream0->PAR = src_buff;
/*
* --------------------------------------------------------
* Step 2. Set Destination Address
* --------------------------------------------------------
*
* M0AR:
*
* Memory 0 Address Register
*
* points to the destination buffer.
*
* Example:
*
* &temp_data_arr<span>[0]</span>
*/
DMA2_Stream0->M0AR = dest_buff;
/*
* --------------------------------------------------------
* Step 3. Set Number of Data Items
* --------------------------------------------------------
*
* NDTR:
*
* Number of Data To Transfer
*
* BUFFER_SIZE = 5
*
* Because data width is 16-bit:
*
* this means:
*
* 5 × 16-bit values
*/
DMA2_Stream0->NDTR = len;
/*
* --------------------------------------------------------
* Step 4. Enable DMA Stream
* --------------------------------------------------------
*
* EN = 1
*
* As soon as the stream is enabled,
* DMA hardware begins the transfer.
*/
DMA2_Stream0->CR |= DMA_SCR_EN;
}
#define BUFFER_SIZE 5U
/*
- Source buffer.
-
- Stored in initialized data memory.
*/
uint16_t sensor_data_arr[BUFFER_SIZE] =
{
892,
731,
1234,
90,
23
};
/*
- Destination buffer.
-
- DMA will fill this array.
-
- CPU does NOT execute:
-
-
temp_data_arr<span>[i]</span> = sensor_data_arr<span>[i]</span>;
*/
uint16_t temp_data_arr[BUFFER_SIZE];
/*
- Set by DMA ISR.
-
- main() waits for this flag.
-
- volatile is mandatory because:
-
-
main() reads it
-
ISR writes it
*/
volatile uint8_t g_transfer_cmplt = 0U;
#include <stdint.h>
#include "dma.h"
#include "uart.h"
#define BUFFER_SIZE 5U
/*
- ============================================================
- Source Buffer
- ============================================================
*/
uint16_t sensor_data_arr[BUFFER_SIZE] =
{
892,
731,
1234,
90,
23
};
/*
- ============================================================
- Destination Buffer
- ============================================================
-
- Before DMA:
-
-
not yet copied
-
- After DMA:
-
-
{892, 731, 1234, 90, 23}
*/
uint16_t temp_data_arr[BUFFER_SIZE];
/*
- ============================================================
- DMA Completion Flag
- ============================================================
-
- ISR:
-
writes this variable.
-
- main:
-
reads this variable.
*/
volatile uint8_t g_transfer_cmplt = 0U;
/*
- Existing practice used uart_print_uint16()
- for decimal output.
-
- If this helper is already present in uart.c,
- use the project version directly.
*/
void uart_print_uint16(uint16_t value);
int main(void)
{
/*
* --------------------------------------------------------
* Step 1. Initialize Completion Flag
* --------------------------------------------------------
*/
g_transfer_cmplt = 0U;
/*
* --------------------------------------------------------
* Step 2. Initialize USART3
* --------------------------------------------------------
*
* DMA copy result will be printed through UART.
*/
uart_init();
/*
* --------------------------------------------------------
* Step 3. Configure DMA2 Stream0
* --------------------------------------------------------
*
* Configuration:
*
* Memory-to-Memory
* Source increment
* Destination increment
* 16-bit → 16-bit
* TC interrupt
* TE interrupt
*/
dma2_mem2mem_config();
/*
* --------------------------------------------------------
* Step 4. Start DMA Transfer
* --------------------------------------------------------
*
* Source:
*
* sensor_data_arr
*
* Destination:
*
* temp_data_arr
*
* Length:
*
* 5
*/
dma_transfer_start(
(uint32_t)sensor_data_arr,
(uint32_t)temp_data_arr,
BUFFER_SIZE
);
/*
* --------------------------------------------------------
* Step 5. Wait for DMA Completion
* --------------------------------------------------------
*
* DMA hardware is transferring the data.
*
* When transfer completes:
*
* DMA2_Stream0_IRQHandler()
*
* sets:
*
* g_transfer_cmplt = 1
*
* NOTE:
*
* This busy-wait is used only to make this
* educational test simple.
*
* In a real application the CPU could perform
* other work while DMA is running.
*/
while (g_transfer_cmplt == 0U)
{
}
/*
* --------------------------------------------------------
* Step 6. Print Destination Buffer
* --------------------------------------------------------
*
* If DMA worked correctly:
*
* temp_data_arr[]
*
* must exactly match:
*
* sensor_data_arr[]
*/
for (uint32_t i = 0U;
i < BUFFER_SIZE;
i++)
{
uart_write_string("Temp buffer<span>[");
uart_print_uint16((uint16_t)i);
uart_write_string("]</span>: ");
uart_print_uint16(temp_data_arr<span>[i]</span>);
uart_write_string("\r\n");
}
/*
* Clear software completion flag after verification.
*/
g_transfer_cmplt = 0U;
/*
* Test complete.
*/
while (1)
{
}
}
void DMA2_Stream0_IRQHandler(void)
{
/*
* ========================================================
* Transfer Complete
* ========================================================
/
if ((DMA2->LISR & LISR_TCIF0) != 0U)
{
/
* Tell main():
*
* DMA transfer completed successfully.
*/
g_transfer_cmplt = 1U;
/*
* Clear Transfer Complete Flag.
*
* DMA interrupt clear registers use:
*
* Write 1 to Clear
*
* Direct assignment is preferable for W1C
* style registers when clearing a specific flag.
*/
DMA2->LIFCR = LIFCR_CTCIF0;
}
/*
* ========================================================
* Transfer Error
* ========================================================
*/
if ((DMA2->LISR & LISR_TEIF0) != 0U)
{
/*
* Transfer Error occurred.
*
* This basic exercise only clears the flag.
*
* A real system could additionally:
*
* set error flag
* retry transfer
* log the failure
*/
DMA2->LIFCR = LIFCR_CTEIF0;
}
}
원본 실습 기록은:
DMA2->LIFCR |= LIFCR_CTCIF0;
DMA2->LIFCR |= LIFCR_CTEIF0;형태였다.
하지만 interrupt clear register는 특정 bit에 1을 써서 flag를 clear하는 구조다.
그래서 Wiki 최종본에서는:
DMA2->LIFCR = LIFCR_CTCIF0;처럼 clear하려는 bit만 직접 write하는 형태로 정리했다.
Ch14의:
EXTI->PR = LINE13;과 같은 W1C register handling 원칙이다.
초기 상태:
sensor_data_arr
[0] 892
[1] 731
[2] 1234
[3] 90
[4] 23
DMA registers:
PAR
↓
sensor_data_arr[0]
M0AR
↓
temp_data_arr[0]
NDTR
↓
5
Transfer 시작:
DMA Stream EN = 1
↓첫 번째 data:
Read 892
from Source
↓
Write 892
to Destination
↓
PINC
Source Address + 2 bytes
MINC
Destination Address + 2 bytes
NDTR
5 → 4
다음:
731
↓
1234
↓
90
↓
23마지막:
NDTR
1 → 0
↓
Transfer Complete
↓
TCIF0 = 1만약 increment를 사용하지 않는다면:
Source Address
always same
Destination Address
always same
가 된다.
그러면 여러 array element를 순서대로 복사할 수 없다.
이번 설정:
DMA2_Stream0->CR |= DMA_SCR_PINC;
DMA2_Stream0->CR |= DMA_SCR_MINC;때문에:
16-bit transfer
2 bytes
씩 address가 이동한다.
Source:
&arr[0]
↓ +2
&arr[1]
↓ +2
&arr[2]
Destination도 동일하다.
배열 type:
uint16_t이다.
즉 element 하나:
16 bits
2 bytes
따라서 DMA:
PSIZE = 16-bit
MSIZE = 16-bit로 맞춘다.
Source Element
16-bit
→ DMA
Destination Element
16-bit
data width가 source variable type과 맞아야 한다.
중요:
DMA2_Stream0->NDTR = 5;는:
5 bytes라는 뜻이 아니다.
현재 transfer width가:
16-bit이므로:
5 data items
×
2 bytes
10 bytes
를 전송한다.
즉:
NDTR
Number of Transfer Items
이다.
DMA stream이 active 상태일 때는 여러 configuration field를 수정할 수 없다.
그래서:
DMA2_Stream0->CR = 0U;
while (DMA2_Stream0->CR & DMA_SCR_EN)
{
}
을 수행한다.
흐름:
Request Disable
↓
Hardware finishing current operation
↓
EN still 1
↓
Wait
↓
EN = 0
↓
Now configuration can changeCh15 RTC의 ready flag, Ch16 IWDG의 PVU/RVU와 같은 pattern이다.
Write Hardware Control
↓
Wait Until Hardware Confirms
↓
Continuevolatile uint8_t g_transfer_cmplt;main:
while (!g_transfer_cmplt)
{
}ISR:
g_transfer_cmplt = 1U;즉:
main context
↕
shared variable
↕
interrupt context구조다.
volatile이 없다면 compiler는 main만 보고:
이 loop 안에서
g_transfer_cmplt를 바꾸는 코드가 없는데?라고 판단해 memory를 반복해서 읽지 않는 최적화를 할 가능성이 있다.
volatile은:
이 변수는 CPU 실행 흐름 밖의 원인에 의해
언제든 변경될 수 있다고 compiler에게 알려준다.
현재:
while (!g_transfer_cmplt)
{
}로 기다린다.
그러면:
DMA를 써놓고 CPU가 기다리는데 무슨 의미가 있지?
라는 의문이 생길 수 있다.
맞다.
이 코드는 DMA 동작 검증용 단순 실습이다.
실제 활용:
dma_transfer_start(...);
while (1)
{
/*
* 다른 sensor 처리
* communication
* control algorithm
* state machine
*/
if (g_transfer_cmplt)
{
/*
* DMA result processing
*/
}
}
형태가 더 적절하다.
핵심은 DMA가:
CPU instruction 없이
data transfer 자체를 수행할 수 있다는 것이다.
DMA Transfer
↓
NDTR reaches 0
↓
TCIF0 = 1
↓
DMA2_Stream0_IRQn
↓
NVIC
↓
DMA2_Stream0_IRQHandler()
↓
g_transfer_cmplt = 1
↓
TCIF0 Clear
↓
ReturnISR에는 if가 두 개 있다.
if (TCIF0)
{
}
if (TEIF0)
{
}
Transfer Complete정상 종료.
Transfer Error전송 중 error 발생.
둘은 의미가 다르기 때문에 독립적으로 검사한다.
Source:
sensor_data_arr
[0] = 892
[1] = 731
[2] = 1234
[3] = 90
[4] = 23
실제 UART 출력:
Temp buffer[0]: 892
Temp buffer[1]: 731
Temp buffer[2]: 1234
Temp buffer[3]: 90
Temp buffer[4]: 23즉:
Source
Destination
를 실제 board에서 확인했다.
따라서:
DMA Memory-to-Memory Transfer
PASS이다.
Debugger를 사용한다면 DMA 전:
sensor_data_arr
892
731
1234
90
23
temp_data_arr
0
0
0
0
0
를 확인한다.
DMA 후:
temp_data_arr
892
731
1234
90
23
으로 바뀌는 것을 확인할 수 있다.
이 경우 CPU code에는:
temp_data_arr[i] = sensor_data_arr[i];가 한 줄도 없다는 것이 핵심이다.
이번 chapter에서는 구현하지 않았지만 ADC와 DMA를 연결하면:
Analog Input
↓
ADC
↓
ADC->DR
↓
DMA
↓
Buffer[]구조가 된다.
CPU가 매 conversion마다:
adc_read();하지 않아도 된다.
보통 continuous ADC acquisition에서는:
Peripheral-to-Memory
+
Circular Mode를 많이 사용한다.
개념:
ADC Sample
↓
Buffer[0]
ADC Sample
↓
Buffer[1]
...
Buffer End
↓
Back to Buffer[0]
UART TX:
Memory Buffer
↓
DMA
↓
USART->DR
↓
TX방향:
Memory-to-PeripheralUART RX:
USART->DR
↓
DMA
↓
Memory Buffer방향:
Peripheral-to-MemoryUART peripheral 자체에서도 DMA request를 enable해야 한다.
예:
USART CR3
DMAT / DMAR이번 chapter에서는 개념만 다루고 실제 구현은 하지 않았다.
실제 실습:
Build : PASS
Download : PASS
Run : PASS
DMA Copy : PASSProgram size:
text = 1640 bytes
data = 12 bytes
bss = 2572 bytes이번에는 초기값이 존재하는 global array:
uint16_t sensor_data_arr[5] =
{
892,
731,
1234,
90,
23
};가 있다.
크기:
5
×
2 bytes
10 bytes
추가 초기화 data 및 alignment의 영향까지 포함되어 실습 build에서는:
.data = 12 bytes가 기록됐다.
반면:
uint16_t temp_data_arr[5];처럼 초기값이 없는 global variable은 .bss 영역에 배치된다.
make clean
makeExpected:
text = 1640
data = 12
bss = 2572Debugger:
DMA2_Stream0->CR
DMA2_Stream0->FCR확인:
DIR = Memory-to-Memory
PINC = 1
MINC = 1
PSIZE = 16-bit
MSIZE = 16-bit
TCIE = 1
TEIE = 1
확인:
DMA2_Stream0->PAR→ sensor_data_arr 주소와 일치.
DMA2_Stream0->M0AR→ temp_data_arr 주소와 일치.
전송 전:
NDTR = 5전송 중:
5
↓
4
↓
3
↓
2
↓
1
↓
0전송 완료:
NDTR = 0Breakpoint:
void DMA2_Stream0_IRQHandler(void)전송 완료 시 ISR에 진입하는지 확인한다.
Watch:
DMA2->LISR
g_transfer_cmpltExpected:
TCIF0
0 → 1
g_transfer_cmplt
0 → 1
Expected:
temp_data_arr[0] = 892
temp_data_arr[1] = 731
temp_data_arr[2] = 1234
temp_data_arr[3] = 90
temp_data_arr[4] = 23images/ch17_build.png
images/ch17_dma_registers.png
images/ch17_addresses.png
images/ch17_ndtr.png
images/ch17_dma_isr.png
images/ch17_memory_before.png
images/ch17_memory_after.png
images/ch17_uart_result.png가장 좋은 evidence:
Source Array
+
PAR / M0AR
+
NDTR = 5 → 0
+
TC Interrupt
+
Destination Array
+
UART Result이다.
CPU
↓
DMA Configuration
↓
Source Address
Destination Address
Length
Direction
Size
↓
DMA Enable
↓
Hardware Data Transfer
↓
TC Interrupt
↓
CPU NotificationDMA는 CPU 대신 data movement를 수행하는 hardware다.
이번 실습은
DMA2 Stream0의 Memory-to-Memory transfer를 사용한다.source와 destination은 모두
uint16_t이므로 PSIZE/MSIZE를 16-bit로 설정한다.PINC와MINC를 enable하여 양쪽 address를 자동 증가시킨다.PAR는 이번 Mem-to-Mem 설정에서 source address로 사용한다.M0AR은 destination address다.NDTR=5는 5byte가 아니라 5개의 16-bit transfer item을 의미한다.Stream 설정 전
EN=0을 반드시 확인한다.Transfer Complete와 Transfer Error interrupt를 각각 enable한다.
g_transfer_cmplt는 ISR과 main이 공유하므로volatile이다.실제 board에서
{892, 731, 1234, 90, 23}이 정확히 복사되는 것을 확인했다.ADC/UART DMA는 이번 chapter에서는 개념만 다루고 실제 구현하지 않았다.