1. Introduction
1.1. Purpose of the DMA
DMA (Direct Memory Access) enables data transfers between memory and peripherals without continuous CPU intervention. By offloading data movement tasks, DMA significantly reduces CPU load, improves real-time performance, and lowers power consumption—key benefits for embedded systems.
On legacy STM32 microcontroller families (such as STM32F0, F1, F4, L4), the DMA controller is relatively simple. Each DMA channel (or stream) is typically configured to perform a single linear transfer with fixed source, destination, and transfer parameters. Once the transfer is complete, the CPU must reconfigure or restart the DMA if a new operation is required.
This architecture works well for basic use cases but becomes limiting when handling:
- Repetitive or multi-step transfers
- Complex peripheral workflows
- Conditional or chained data movements
As application complexity increases, these limitations lead to greater CPU involvement, increased interrupt overhead, and reduced system efficiency.
To address these constraints, newer STM32 devices introduce the GPDMA (General-Purpose DMA). GPDMA extends the classical DMA concept by allowing transfers to be organized as linked sequences of operations, called nodes, which can be executed autonomously without CPU interaction. This node-based approach enables far more flexible and efficient data transfer scenarios, paving the way for advanced use cases such as continuous peripheral servicing, memory-to-memory processing chains, and low-power background transfers.
GPDMA offers two main ways to control your data transfers: by software or hardware trigger
- What is it?
- You (the software) tell the DMA to start a transfer, and it does it once.
- When do you use it?
- For simple, one-shot transfers (copying a buffer, toggling a pin once, etc.)
- For testing or debugging your DMA setup.
2. GPDMA: A More Flexible DMA Architecture
To overcome these constraints, STM32 devices equipped with GPDMA (General-Purpose DMA) introduce a more modern, autonomous, and modular approach to data transfers based on linked lists of nodes.
| Classic DMA | GPDMA |
|---|---|
| Few channels | More channels, more flexibility |
| One transfer per channel | Chain multiple transfers (Linked List) |
| No 2D support | 2D transfer support |
| Hard to automate |
Table 1: Difference between Classic DMA and GPDMA
2.1. Introduction of LinkedList and Nodes
Traditional DMA controllers in earlier STM32 families are effective for simple, single-shot transfers, but they show clear limitations when applications require more sophisticated data movement. Classic DMA typically suffers from:
- Limited channels, restricting the number of concurrent operations
- No automatic chaining, requiring CPU intervention between transfers
- Difficulty implementing complex or protocol-specific sequences
- Lack of native support for periodic, circular, or 2D data patterns
These limitations lead to increased CPU load and reduced efficiency in advanced embedded designs.
- What is a Node?
- A node is a self-contained transfer descriptor that defines:
- Source and destination addresses
- Transfer size and format
- Address incrementing rules
- Trigger configuration
- Each node describes a complete DMA operation that GPDMA can execute without further CPU involvement.
- What are LinkedLists of Nodes
- Automatic execution of multi-step transfer sequences
- Conditional or triggered progression through nodes
- Linear (execute once) or circular (repeat indefinitely) transfer patterns
- Efficient servicing of complex peripherals
Once the CPU starts the linked list, the GPDMA autonomously advances from node to node, enabling highly flexible, low-power, and deterministic data flow.
- Why Linked Lists Matter
- Full automation of complex transfer workflows
- Lower CPU and interrupt load
- Precise control over timing and sequencing
- Built-in support for 2D, periodic, or circular patterns
- Scalability across diverse application needs
| Mode | What happens | Typical use case |
|---|---|---|
| Linear | DMA runs the list of nodes once, then stops | One-time sequences |
| Circular | DMA loops the list forever | Repeating patterns, PWM, etc. |
This wiki usage guide will focus on the use of the GPDMA Linked List mode.
3. Requirements
The following table is intended to help ensure that all necessary information is gathered to allow proper progress on this article.
Note: this article is based on STM32H503 but can be applicable to any STM32 with GPDMA
3.1. Beginner Level Requirements
| Step | Question | Yes | No | Done? |
|---|---|---|---|---|
| 0.1 | Do you have the official documentation for STM32H503RB? (Reference Manual, Datasheet, AN5346, etc.) |
Go to Step 0.2 | Download and read: Reference Manual (RM0492) User Manual Datasheet Application Note AN4711 Application Note AN5346 |
☐ |
| 0.2 | Do you have experience with STM32CubeIDE, STM32CubeMX, STM32CubeProgrammer (optional)? | Go to Step 0.3 | Install STM32CubeIDE, STM32CubeMX, STM32CubeProgrammer refering to 1-2-3 step | ☐ |
| 0.3 | Do you know your product reference and revision? | Go to Step 0.4 | Check chip marking on MCU. e.g. STM32H503RBT6 |
☐ |
| 0.4 | Configure your MCU clock tree with a low frequency value around 64MHz to see easier the GPIO activities? | We have used a low frequency of 64MHz HSI to evaluate the maximum performance. Go to Step 0.5 |
Set the clock in CubeIDE and verify with the reference manual. | ☐ |
| 0.5 | Do you have GPIO PA7, PA8, PB10, PC7, Timer 3 channel 2, Timer 2 channel 2 activated? | Go to Step 1.1 | Refer to Getting started with STM32 system peripherals | ☐ |
3.2. Intermediate Level Requirements
| Step | Question | Yes | No |
|---|---|---|---|
| 1.1 | Do you understand GPDMA descriptors and linked-list nodes? | Go to Step 1.2 | Read AN5346 (sections 3 and 4) and RM0492 (GPDMA chapter) |
| 1.2 | Do you know how to configure and use circular mode? | Go to Step 1.3 | See AN5346 (section 4.2) and CubeMX documentation |
| 1.3 | Do you understand peripheral-triggered events (e.g. timer, ADC)? | Go to Step 1.4 | Check RM0492 (GPDMA and timer/ADC chapters) and CubeMX |
| 1.4 | Do you know the workflow for multi-node DMA operations? | Ready for intermediate guide | See AN5346 (section 4.3) and CubeH5 examples |
3.3. Expert Level Requirements
| Step | Question | Yes | No |
|---|---|---|---|
| 2.1 | Do you understand 2D addressing (block size, line size, stride)? | Go to Step 2.2 | Read AN5346 (section 5) and RM0492 (GPDMA chapter) |
| 2.2 | Do you know optimization concepts (burst size, FIFO, alignment)? | Go to Step 2.3 | See RM0492 (GPDMA chapter) |
| 2.3 | Are you comfortable debugging complex DMA flows? | Go to Step 2.4 | Practice with CubeIDE debugger and review AN5346 (section 6) |
| 2.4 | Do you understand multi-node synchronization and 2D mode interactions? | Ready for expert guide | See AN5346 (section 5.3) and CubeH5 examples |
4. GPDMA Beginner Usage
Before starting with practical examples, you should know the following:
IMPORTANT:
- If the data is created as 32 bits, which equals 4 bytes (8 bits are equal to 1 byte), the data is considered a WORD. Therefore, the data size is 1 * 4U.
- Do not confuse the node queue structure, which can be 1D (named differently in CubeMX and HAL) or 2D, with the CIRCULAR or LINEAR mode of the queue execution itself.
| GPDMA node mode | Description RM | CubeMX | Cube HAL |
|---|---|---|---|
| 1D or linear | Single or multiple linear addressing (1D array operations) | Queue DMA channel linked list configuration "GPDMA1" warning 1D is not mentioned | DMA_GPDMA_LINEAR_NODE (warning: linear indicates usage for 1D array "linear addressing node type") |
| 2D | Single or multiple 2D addressing (2D array operations) | Queue DMA channel linked list configuration "GPDMA1 2D capability" | DMA_GPDMA_2D_NODE (warning: linear indicates usage for 2D array "2D dimension addressing node type") |
This section will guide you step-by-step through the configuration and usage of the GPDMA Linked List mode on STM32H5xxx MCUs. You will first find a decision table to help you select the right GPDMA use case for your needs, followed by a series of progressive, ready-to-use programming examples organized by skill level:
- Beginner:
- Start with simple, single-node transfers (e.g., set a pin, generate a pulse)
- Learn how to use triggers, circular mode, and basic interrupts
- Intermediate:
- Move to multi-node transfers (e.g., sequences, alternating pulses)
- Combine circular mode, hardware triggers, and interrupts for more advanced behaviors
- Expert:
- Explore advanced features like 2D addressing for image/matrix transfers and complex data patterns
Each example is designed to be practical and copy-paste ready, and includes:
- The objective and expected behavior
- All required configuration steps (hardware and software)
- Code snippets and, where relevant, CubeMX screenshots
- Tips for debugging and validation
5. GPDMA legacy approach
In this application, we chose the NUCLEO‑H503RB board.
Historically, the legacy approach for using GPDMA on STM32 consisted of the Standard Request (Software Request) mode, which allowed a single DMA transfer from memory to a peripheral register (such as GPIO BSRR) without CPU intervention. This method was useful for simple, immediate operations like toggling a pin once, but offered limited flexibility and automation. In this example, the DMA writes a single value to the GPIOA BSRR register, producing a single pulse on GPIOA (for example, PA8) when the DMA is started. All subsequent cases in this guide will focus on the recommended Linked List mode, which provides much greater flexibility and is now the preferred method for advanced GPDMA usage.
- After configuring PA8 and PB10 in GPIO_Output mode, activate channel 0 in Standard Request Mode of GPDMA1 from the System Core.
- In Configuration parameters, choose CH0, set DataWidth to "Word".
1. In USER CODE PV, create a variable like src_buffer; its value will be set in the BSRR register of GPIOA
/* USER CODE BEGIN PV */
uint32_t src_buffer = 0x00000100; // or 0x100 SET bit 8 BSRR register
/* USER CODE END PV */
2. Then in USER CODE 4 section, create a new function like this with the same fields. We will update progressively this function.
/* USER CODE BEGIN 4 */
void GPDMA_SW_Request()
{
/*
Signal observation:
-------------------
DMA_start: ________|‾‾|_______________________________________ (PB10)
Signal1: ______________________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ (PA8)
*/
handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0;
handle_GPDMA1_Channel0.Init.Request = DMA_REQUEST_SW;
handle_GPDMA1_Channel0.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
handle_GPDMA1_Channel0.Init.Direction = DMA_MEMORY_TO_MEMORY;
handle_GPDMA1_Channel0.Init.SrcInc = DMA_SINC_FIXED;
handle_GPDMA1_Channel0.Init.DestInc = DMA_DINC_FIXED;
handle_GPDMA1_Channel0.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;
handle_GPDMA1_Channel0.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;
handle_GPDMA1_Channel0.Init.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel0.Init.SrcBurstLength = 1;
handle_GPDMA1_Channel0.Init.DestBurstLength = 1;
handle_GPDMA1_Channel0.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
handle_GPDMA1_Channel0.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
handle_GPDMA1_Channel0.Init.Mode = DMA_NORMAL;
if (HAL_DMA_Init(&handle_GPDMA1_Channel0) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
// 1- GPIOPB10 init. START DMA
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
// 2- START DMA
HAL_DMA_Start(&handle_GPDMA1_Channel0, (uint32_t)&src_buffer, (uint32_t)&GPIOA->BSRR, 1*4U);
// the data is created as 32bits, which equals 4 bytes (1 octet = 1 byte = 8bits, so 32bits = 8 bits*4 = 1 byte*4)
// that is why the data is considered a word, and then datasize is 1*4U
}
/* USER CODE END 4 */
3. Call the function GPDMA_SW_Request() before the while loop and observes the generated signals.
/* USER CODE BEGIN 2 */
void GPDMA_SW_Request(); // function created
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
6. GPDMA linked-list approach
6.1. Beginner Level Guide: Simple DMA Transfers
Objective: The beginner level is designed to provide a hands-on and progressive introduction to GPDMA, starting from simple and observable use cases. The goal at this stage is not performance or optimization, but understanding the fundamentals through direct manipulation and visible effects on GPIOs.
At this level, the user learns:
- How a single GPDMA transfer works
- How to control a GPIO directly through memory-to-memory DMA writes
- How to use Linked List mode with a single node
- How DMA can operate without CPU intervention, even for very simple actions
The approach deliberately follows a “crash and do” philosophy: the user configures, runs, observes the signal on the GPIO, and immediately understands the effect of each configuration parameter.
By the end of the beginner level, the user will be able to:
- Create simple DMA templates
- Generate basic GPIO actions (set, reset, pulse)
- Understand the role of nodes, queues, and execution modes
This level forms the foundation required to safely move toward more automated and repetitive DMA patterns.
| Step | Use Case name | What does it do? | How to do it? | Function name | Done? |
|---|---|---|---|---|---|
| 3.1 | Single Step | Sets a GPIO high with a single DMA transfer. | 1 node, 1 write, Linked List mode | Single Step Transfer | ☐ |
| 3.2 | Simple Pulse | Generate a pulse on GPIO using two consecutive DMA writes. | 1 node, 2 writes, Linked List mode | Simple Pulse Generation | ☐ |
| 3.3 | Pulse Stream (Circular Mode) | GPIO pulse repetition using two consecutive DMA writes. | 1 node, 2 writes, Linked List mode, circular | Pulse Stream (Circular Mode) | ☐ |
| 3.4 | Pulse Stream Trigger | Repeated GPIO pulses synchronized with a trigger event. | 1 node, 2 writes, Linked List mode, circular, trigger enabled | Frame Stream with Trigger | ☐ |
6.1.1. Single Step Transfer
1. Configure GPIO pins PA8 and PB10 in the STM32H503RBTx.ioc file.
- Select GPIO_Output for both pins.
- Also configure the clock.
2.Enable a peripheral GPDMA like GPDMA1
- For Channel 0, select Linked-List Mode.
- Leave other channels disabled.
3. Linked List/Node Configuration for Beginners
- For your first steps, you do not need to manually configure the nodes or the linked list in CubeMX.
- Just enable the channel as above.
- You will rely on the example code provided in
main.cto set up the node and queue in software. - This approach lets you get started quickly, and you can learn about node configuration in detail later as you gain experience.
Tip: The code you will use in
main.cwill handle all the node and queue setup for you. As a beginner, focus on CubeMX for GPIO and DMA channel, and trust the code for the rest.
4.Generate the Code:
- Click Project > Generate Code.
- Open the project in STM32CubeIDE.
5.main.c
- variables
/* Private variables ---------------------------------------------------------*/
DMA_HandleTypeDef handle_GPDMA1_Channel0;
/* USER CODE BEGIN PV */
DMA_NodeTypeDef Node1;
DMA_QListTypeDef Queue;
extern DMA_QListTypeDef Queue;
volatile uint32_t src_buffer_node1[64];
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
/*
*
*
*/
/* USER CODE BEGIN PFP */
void single_step_transfer();
- main function
single_step_transfer();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
- function to create:
void single_step_transfer()
{
/*Signal Observation
DMA_start: ________|‾‾|_______________________________________ (PB10)
Signal1: ______________________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ (PA8)
*/
/******* Variables: Prepare the value to transfer *********/
// src_buffer_node1[0] holds the value to write to the GPIOA BSRR register to SET PA8
src_buffer_node1[0]=0x00000100; //SET PA8
/******* DMA Configuration *********/
// Configure GPDMA1 Channel 0 for Linked List mode (sequence mode)
handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0;
handle_GPDMA1_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
handle_GPDMA1_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
handle_GPDMA1_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
handle_GPDMA1_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_NORMAL;
if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel0) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
/******* Node Configuration *********/
// Here, we create a "node" that describes a single memory-to-memory transfer (SET PA8)
HAL_StatusTypeDef ret = HAL_OK;
DMA_NodeConfTypeDef pNodeConfig;
pNodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;// Simple (1D) node
pNodeConfig.Init.Request = DMA_REQUEST_SW;// Software-triggered
pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.Init.SrcInc = DMA_SINC_FIXED;//Source is not incremented (single value)
pNodeConfig.Init.DestInc = DMA_DINC_FIXED;// Destination is not incremented (single register)
pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;
pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;
pNodeConfig.Init.SrcBurstLength = 1;
pNodeConfig.Init.DestBurstLength = 1;
pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
pNodeConfig.Init.Mode = DMA_NORMAL;
// No repeat or offsets for this simple transfer
pNodeConfig.RepeatBlockConfig.RepeatCount = 1;
pNodeConfig.RepeatBlockConfig.SrcAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.DestAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.BlkSrcAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.BlkDestAddrOffset = 0;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node1;// Address of the value to transfer
pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR; // Target register (SET PA8)
pNodeConfig.DataSize = 1*4U; // Transfer size (1 word = 4 bytes)
// Build and insert the node into the queue
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node1);
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node1);
//1-Create the Queue
if(HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel0, &Queue)!=HAL_OK)
{
Error_Handler();
}
//2-Start the DMA transfer
/****Toggle PB10 as a marker for oscilloscope/debug****/
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
// Start the DMA: the node will be executed (SET PA8)
if (HAL_DMAEx_List_Start(&handle_GPDMA1_Channel0)!=0)
{
Error_Handler();
}
}
/* USER CODE END 4 */
- Signal observation should look like this
/* USER CODE BEGIN WHILE */
while (1)
{
}
6.1.2. Simple Pulse Generation
Generate a pulse consisted of to generate two steps like the previous example but we should have 2 writes: SET and RESET without forget modifying the datasize to 2 WORDS
1.Generate a pulse requires resetting the BSRR register.
- therefore the buffer should contain two values:
src_buffer_node1[2] = {0x00000100, 0x01000000}; /* USER CODE BEGIN 4 */ src_buffer_node1[0]=0x00000100;//SET PA8 src_buffer_node1[1]=0x01000000;//RESET PA8
2.DMA Source Increment:
pNodeConfig.Init.SrcInc = DMA_SINC_INCREMENTED;- Here, the source is set to "incremented" because the DMA needs to read multiple consecutive values from a buffer in memory.
Ensuring the DMA reads and sends each value in your buffer in order, creating the correct pulse pattern on the output.
pNodeConfig.Init.SrcInc = DMA_SINC_INCREMENTED;
pNodeConfig.Init.DestInc = DMA_DINC_FIXED;
3.Data Size:
pNodeConfig.DataSize = 2*4U;- Set 2 words because the DMA needs to transfer both the SET and RESET values in sequence; this creates a complete pulse (high then low) on the output pin.
pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR; //--->SET PA8, RESET PA8
pNodeConfig.DataSize = 2*4U; //2 WORDS here
4. Function to modify or create:
- For this case, after these modifications, we advice to modify the function name
/* USER CODE BEGIN 4 */ /* IMPORTANT: Remove or exclude linkedlist.c/h (CubeMX generated) * to avoid conflicts with this GPDMA configuration. */ void simple_pulse_generation() { /*Signal Observation DMA_start: ________|‾‾|_______________________________________ (PB10) Signal1: ___________________|‾|_____________________________ (PA8) */ /******* 1- Variables *********/ // The buffer now contains two values: // - First value: SET PA8 (0x00000100) // - Second value: RESET PA8 (0x01000000) src_buffer_node1[0]=0x00000100;//SET PA8 src_buffer_node1[1]=0x01000000;//RESET PA8 /******* DMA Configuration *********/ __HAL_RCC_GPDMA1_CLK_ENABLE(); // Configure GPDMA1 Channel 0 for Linked List mode (sequence mode) handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0; handle_GPDMA1_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT; handle_GPDMA1_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION; handle_GPDMA1_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0; handle_GPDMA1_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER; handle_GPDMA1_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_NORMAL; if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel0) != HAL_OK) { Error_Handler(); } if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK) { Error_Handler(); } /******* Node Configuration *********/ HAL_StatusTypeDef ret = HAL_OK; DMA_NodeConfTypeDef pNodeConfig; pNodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;// Simple (1D) node pNodeConfig.Init.Request = DMA_REQUEST_SW;// Software-triggered pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST; pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY; /**2- Source incremented: DMA will read both SET and RESET values in order**/ pNodeConfig.Init.SrcInc = DMA_SINC_INCREMENTED;// Source is incremented (two values) pNodeConfig.Init.DestInc = DMA_DINC_FIXED;// Destination is not incremented (single register) pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD; pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD; pNodeConfig.Init.SrcBurstLength = 1; pNodeConfig.Init.DestBurstLength = 1; pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0; pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER; pNodeConfig.Init.Mode = DMA_NORMAL; pNodeConfig.RepeatBlockConfig.RepeatCount = 1;// No repeat or offsets for this simple transfer pNodeConfig.RepeatBlockConfig.SrcAddrOffset = 0; pNodeConfig.RepeatBlockConfig.DestAddrOffset = 0; pNodeConfig.RepeatBlockConfig.BlkSrcAddrOffset = 0; pNodeConfig.RepeatBlockConfig.BlkDestAddrOffset = 0; pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED; pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE; pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED; pNodeConfig.SrcAddress = (uint32_t)src_buffer_node1;// Address of the value to transfer pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR; //Will SET then RESET PA8 /**3- 2 words: SET and RESET**/ pNodeConfig.DataSize = 2*4U; // Build and insert the node into the queue ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node1); ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node1); /*******Link the queue to the DMA channel *********/ // Link the queue (list of nodes) to the DMA channel if(HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel0, &Queue)!=HAL_OK) { Error_Handler(); } /******* Start the DMA transfer *********/ // Toggle PB10 as a marker for oscilloscope/debug HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET); // Start the DMA: will generate a pulse (SET then RESET) on PA8 if (HAL_DMAEx_List_Start(&handle_GPDMA1_Channel0)!=0) { Error_Handler(); } }
5.Result:
- The DMA will first set PA8, then immediately reset it, generating a short pulse.
- Signal observation:
simple_pulse_generation();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
}
/* USER CODE END WHILE */
Note: Theses following steps are required to avoid conflicts between the CubeMX-generated Linked List configuration and the one implemented in this example, which may lead to incorrect DMA behavior.
Therefore, the generated linked list files must be either deleted or excluded from the build (Right click on file → Resource Configuration → Exclude from Build).
6.2. Pulse Stream (Circular Mode)
This use case is similar to simple pulse Generation, but here it needs to change the Linked List Mode from LINKEDLIST_NORMAL to LINKEDLIST_CIRCULAR and add an instruction to loop the linked list at the end of the node configuration: ret |= HAL_DMAEx_List_SetCircularMode(&Queue);
1.Configuration CubeMX:
- The Circular Mode can be directly selected in the Queue configuration
- In this example, we can use the function
pulse_stream_circular_mode
2.The selected mode used is: DMA_LINKEDLIST_CIRCULAR
3.The function would be:
/* IMPORTANT: Remove or exclude linkedlist.c/h (CubeMX generated)
* to avoid conflicts with this GPDMA configuration.
*/
void pulse_stream_circular_mode()
{
/**
* This function generates a continuous stream of pulses on PA8 using DMA in circular mode.
*
* Compared to the previous examples:
* - The DMA is configured in **circular (loop) mode** so the pulse sequence repeats automatically.
* - The source buffer still contains two values: SET and RESET for PA8.
* - The node and queue setup is similar, but the queue is set to circular, so the DMA restarts from the beginning after each cycle.
*
* This allows the DMA to generate an endless train of pulses on PA8 without CPU intervention.
*/
/******* Variables: Prepare the pulse sequence *********/
// The buffer contains two values:
// - First value: SET PA8 (0x00000100)
// - Second value: RESET PA8 (0x01000000)
// The DMA will alternate between these to create a pulse.
src_buffer_node1[0]=0x00000100;//SET PA8
src_buffer_node1[1]=0x01000000;//RESET PA8
/******* DMA Configuration *********/
// Configure GPDMA1 Channel 0 for Linked List mode, but now in CIRCULAR mode
handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0;
handle_GPDMA1_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
handle_GPDMA1_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
handle_GPDMA1_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
// 1-*** Key difference: set LinkedListMode to CIRCULAR for continuous looping ***
handle_GPDMA1_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_CIRCULAR;
if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel0) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
/******* Node Configuration *********/
HAL_StatusTypeDef ret = HAL_OK;
DMA_NodeConfTypeDef pNodeConfig;
pNodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;// Simple (1D) node
pNodeConfig.Init.Request = DMA_REQUEST_SW;// Software-triggered
pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.Init.SrcInc = DMA_SINC_INCREMENTED; //Source is incremented
pNodeConfig.Init.DestInc = DMA_DINC_FIXED; //Destination is not incremented
pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;
pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;
pNodeConfig.Init.SrcBurstLength = 1;
pNodeConfig.Init.DestBurstLength = 1;
pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
pNodeConfig.Init.Mode = DMA_NORMAL;
pNodeConfig.RepeatBlockConfig.RepeatCount = 1;// No repeat or offsets for this simple transfer
pNodeConfig.RepeatBlockConfig.SrcAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.DestAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.BlkSrcAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.BlkDestAddrOffset = 0;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node1;// Address of the value to transfer
pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR; //--->SET PA8, RESET PA8
pNodeConfig.DataSize = 2*4U;//2 words: SET and RESET
// Build and insert the node into the queue
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node1);
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node1);
// 2-*** Key difference: set the queue to circular mode so it loops forever ***
ret |= HAL_DMAEx_List_SetCircularMode(&Queue); // added instruction to loop the linked list
/*******Link the queue to the DMA channel *********/
// Link the queue (list of nodes) to the DMA channel
if(HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel0, &Queue)!=HAL_OK)
{
Error_Handler();
}
/******* Start the DMA transfer *********/
// Toggle PB10 as a marker for oscilloscope/debug
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
// Start the DMA: will generate a pulse (SET then RESET) on PA8
if (HAL_DMAEx_List_Start(&handle_GPDMA1_Channel0)!=0)
{
Error_Handler();
}
}
4. The prototype is:
/* USER CODE BEGIN 2 */
//single_step_transfer();
//simple_pulse_generation();
pulse_stream_circular_mode();
5. The signal observation look like this:
while (1)
{
/*Signal Observation
Time (µs): 0 1 2 3 4 5 6 7 8 9 10
|---|---|---|---|---|---|---|---|---|---|---|
Pulse: ________|‾‾|_______________________________________
Step : _______________________|||||||||||||||||||||||||||||||||
*/
6.2.1. Frame Stream with Trigger
This use case is similar to Pulse Stream, but here we choose a timer (TIM2) what can be triggered on each update event of the raising edge. When the raising edge triggers, the state of the BSRR register changes from one value to another. To do this, we will add, in addition to the timer, 3 instructions for the trigger on the rising edge.
1.Configuration CubeMX:
- Timer TIM2: the signal here ius generated to value of 1KHz and duty cycle of 50%
- Prescaler:
63 - AutoReload Register:
999 - Trigger Event Selection TRGO:
Update Event - Pulse:
500 - Output compare preload:
Enable - auto-reload preload:
Enable
- Prescaler:
- Linked List
- Request Configuration:
GPDMA1_REQUEST_TIM2_UP - Trigger:
- Request Configuration:
2. main function
- Privates variables:
TIM_HandleTypeDef htim2;
volatile uint32_t src_buffer_node1[64];
- Private function prototype:
void frame_stream_with_trigger();
- Prototype:
//simple_pulse_generation();
//pulse_stream_circular_mode();
frame_stream_with_trigger();
- Function to create:
/* IMPORTANT: Remove or exclude linkedlist.c/h (CubeMX generated)
* to avoid conflicts with this GPDMA configuration.
*/
void frame_stream_with_trigger()
{
/**
* This function generates a continuous stream of pulses on PA8,
* but now the DMA transfer is **synchronized to a hardware trigger** (TIM2 PWM).
*
* Compared to previous examples:
* - The DMA is still in circular mode, so the pulse sequence repeats.
* - The node is configured to wait for a **hardware trigger** (TIM2_TRGO, rising edge) before each transfer.
* - This means each pulse (SET then RESET) is generated in sync with the timer, not as fast as possible.
* - Useful for generating precise, timer-controlled pulse trains.
*/
/******* Variables: Prepare the pulse sequence *********/
// The buffer contains two values:
// - First value: SET PA8 (0x00000100)
// - Second value: RESET PA8 (0x01000000)
src_buffer_node1[0]=0x00000100;//SET PA8
src_buffer_node1[1]=0x01000000;//RESET PA8
/******* DMA Configuration *********/
__HAL_RCC_GPDMA1_CLK_ENABLE();
handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0;
handle_GPDMA1_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
handle_GPDMA1_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
handle_GPDMA1_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
// *** Circular mode: the sequence will repeat automatically ***
handle_GPDMA1_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_CIRCULAR;//normal mode instead of circular mode
if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel0) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
/******* Node Configuration:Describe the triggered pulse sequence *********/
HAL_StatusTypeDef ret = HAL_OK;
DMA_NodeConfTypeDef pNodeConfig;
pNodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;
pNodeConfig.Init.Request = DMA_REQUEST_SW;
pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.Init.SrcInc = DMA_SINC_INCREMENTED;
pNodeConfig.Init.DestInc = DMA_DINC_FIXED;
pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;
pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;
pNodeConfig.Init.SrcBurstLength = 1;
pNodeConfig.Init.DestBurstLength = 1;
pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
pNodeConfig.Init.Mode = DMA_NORMAL;
/* pNodeConfig.RepeatBlockConfig.RepeatCount = 1;
pNodeConfig.RepeatBlockConfig.SrcAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.DestAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.BlkSrcAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.BlkDestAddrOffset = 0;*/
/****** 1- Trigger Configuration: synchronize with timer ******/
pNodeConfig.TriggerConfig.TriggerMode = DMA_TRIGM_SINGLE_BURST_TRANSFER ;// Wait for trigger for each burst
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_RISING;// On rising edge
pNodeConfig.TriggerConfig.TriggerSelection = GPDMA1_TRIGGER_TIM2_TRGO;// Use TIM2 TRGO as trigger
pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node1;
pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR; // Will SET then RESET PA8
pNodeConfig.DataSize = 2*4U; //// 2 words: SET and RESET
// Build and insert the node into the queue
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node1);
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node1);
// Set the queue to circular mode so it loops forever
ret |= HAL_DMAEx_List_SetCircularMode(&Queue);
/******* Link the queue to the DMA channel *********/
if(HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel0, &Queue)!=HAL_OK)
{
Error_Handler();
}
/******* 2- Start the timer (PWM) to generate the trigger events *********/
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
/******* Start the DMA transfer *********/
// Toggle PB10 as a marker for oscilloscope/debug
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
// Start the DMA: will generate a pulse (SET then RESET) on PA8
// synchronized with each rising edge of TIM2_TRGO
if (HAL_DMAEx_List_Start(&handle_GPDMA1_Channel0)!=0)//--->SET PA8
{
Error_Handler();
}
}
- Signal observation:
/* USER CODE BEGIN WHILE */
while (1)
{
/*Signal Observation
Timer: __|‾‾|__|‾‾|__|‾‾|__|‾‾|__|‾‾|__ (update event)
Step : ___|____________________________________
GPIO A8: ______|‾‾‾‾‾|_____|‾‾‾‾‾|_____|‾‾‾‾|___ (SET/RESET at each update)
6.3. Intermediate Level Guide: Multiple DMA Transfers
Objective: The intermediate level builds on the fundamentals learned previously and introduces structured and automated DMA behaviors. At this stage, the focus shifts from single actions to sequences, patterns, and repetition, using multiple nodes and circular execution.
The objective is to move from “one shot” operations to controlled data flows, where the DMA:
- Executes multiple nodes in sequence
- Repeats operations automatically using circular linked lists
- Synchronizes transfers with external hardware events such as timers
At this level, the user will:
- Design multi-node DMA templates
- Create repeated GPIO patterns without CPU load
- Introduce hardware triggering to control when DMA operations occur
- Begin building frequency-based systems driven by timers
This stage represents a key transition: the DMA is no longer a simple accelerator, but becomes a deterministic engine capable of driving real-time GPIO behavior.By the end of the intermediate level, the user will be able to design reusable DMA patterns and control execution timing precisely.
| Step | Use Case name | What does it do? | How to do it? | Function name | Done? |
|---|---|---|---|---|---|
| 4.1 | Two-Node Pulse Sequence | Two pulses in sequence (e.g., two LEDs blink in order). | 2 nodes, 1 write each, Linked List mode | Two-Node Pulse Sequence | ☐ |
| 4.2 | Two-Node Pulse Stream (Circular Mode) | Two pulses alternate forever (circular). | 2 nodes, 1 write each, Linked List mode, circular | Two-Node Pulse Stream (Circular Mode) | ☐ |
| 4.3 | Triggered Pulse Stream (Four Nodes) | Sequence starts on a hardware event (timer, etc.). | 4 nodes, 1 write each, Linked List mode, circular, hardware trigger | Triggered Pulse Stream (Four Nodes) | ☐ |
| 4.4 | GPIO Sampling with DMA and Interrupts | Samples a GPIO at high speed, analyzes buffer in DMA IRQ, triggers software action (e.g., LED) | 1 node, Linked List mode, circular, hardware trigger, interrupt enabled, buffer analysis in IRQ | GPIO Sampling with DMA and Interrupts | ☐ |
6.3.1. Two-Node Pulse Sequence
1.Configure GPIO pins PA8, PB10 and PC7 in the STM32H503RBTx.ioc file.
- Select GPIO_Output for both pins.
- Also configure the clock.
2.Enable a peripheral GPDMA like GPDMA1
- For Channel 0, select Linked-List Mode.
- Leave other channels disabled.
3.Configure the Linked List
- Go to the Linked Lists tab (left panel).
- Create a Queue and 2 Nodes
- In the Queue configuration: LinkedList setting to Linear
- In the Node configuration:
- Request as a parameter:
GPDMA1_REQUEST_SW - Direction:
Memory To Memory - Data Width:
Word
- Request as a parameter:
4.Generate the Code:
- Click Project > Generate Code.
- Open the project in STM32CubeIDE.
5.main.c file:
- Variables
/* USER CODE BEGIN Includes */
DMA_NodeTypeDef Node1;
DMA_QListTypeDef Queue;
DMA_NodeTypeDef Node2;
/* Private variables ---------------------------------------------------------*/
DMA_HandleTypeDef handle_GPDMA1_Channel0;
/* USER CODE BEGIN PV */
volatile uint32_t src_buffer_node1[64];
volatile uint32_t src_buffer_node2[64];
/* USER CODE END PV */
/* USER CODE BEGIN PFP */
void two_node_pulse_sequence();
- main function:
two_node_pulse_sequence();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
- function to create:
/* IMPORTANT: Remove or exclude linkedlist.c/h (CubeMX generated)
* to avoid conflicts with this GPDMA configuration.
*/
void two_node_pulse_sequence()
{
/**
* This function demonstrates an intermediate use of GPDMA:
* It creates a sequence of two DMA nodes, each targeting a different GPIO pin.
* - Node 1: Sets PA8 (bit 8 of GPIOA)
* - Node 2: Sets PC7 (bit 7 of GPIOC)
*
* The DMA executes Node 1, then immediately Node 2, allowing you to generate a sequence
* (for example, two pulses on two different pins) with a single DMA start, no CPU intervention between steps.
*/
/******* 1. Variables: Prepare the values for each node *********/
// Node 1: SET PA8 (bit 8 of GPIOA)
src_buffer_node1[0] = 0x00000100; // SET PA8
// Node 2: SET PC7 (bit 7 of GPIOC)
src_buffer_node2[0] = 0x00000080; // SET PC7
/******* 2. DMA Channel Configuration *********/
__HAL_RCC_GPDMA1_CLK_ENABLE();
handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0;
handle_GPDMA1_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
handle_GPDMA1_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
handle_GPDMA1_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
handle_GPDMA1_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_NORMAL;// Not circular: runs once
if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel0) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
/******* 3. Node Configuration: Create two nodes for the sequence *********/
HAL_StatusTypeDef ret = HAL_OK;
/* DMA node configuration declaration */
DMA_NodeConfTypeDef pNodeConfig;
// --- Node 1: SET PA8 ---
pNodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;
pNodeConfig.Init.Request = DMA_REQUEST_SW;
pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.Init.SrcInc = DMA_SINC_FIXED;// Only one value per node
pNodeConfig.Init.DestInc = DMA_DINC_FIXED;
pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;
pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;
pNodeConfig.Init.SrcBurstLength = 1;
pNodeConfig.Init.DestBurstLength = 1;
pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
pNodeConfig.Init.Mode = DMA_NORMAL;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node1;// Value for PA8
pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR;// Target: GPIOA BSRR
pNodeConfig.DataSize = 1*4U; // 1 word
/* Build Node1 Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node1);
/* Insert Node1 to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node1);
// --- Node 2: SET PC7 ---
// Only change the source and destination addresses for Node 2
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node2; // Value for PC7
pNodeConfig.DstAddress = (uint32_t)&GPIOC->BSRR; // Target: GPIOC BSRR
pNodeConfig.DataSize = 1*4U; // 1 word
/* Build Node2 Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node2);
/* Insert Node2 to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node2);
/******* 4. Link the queue to the DMA channel *********/
if(HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel0, &Queue)!=HAL_OK)
{
Error_Handler();
}
/******* 5. Start the DMA transfer *********/
// Toggle PB10 as a marker for oscilloscope/debug
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
// Start the DMA: will execute Node 1 (SET PA8), then Node 2 (SET PC7)
if (HAL_DMAEx_List_Start(&handle_GPDMA1_Channel0)!=0)//--->SET PA8
{
Error_Handler();
}
}
- Signal observation should look like this:
while (1)
{
/* USER CODE END WHILE */
//Temps (µs): -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10
//|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|
//PB10: ____|‾‾‾|_______________________________________________________________
//PA8: ____________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
//PC7: _____________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
6.3.2. Two-Node Pulse Stream (Circular Mode)
Generate a pulse consisting of two steps, like the previous example, but we need to perform two writes: SET and RESET. Don’t forget to modify the data size to 16 words, and we will store the values in variables. In this use case, similar to the previous example, we will have two nodes to SET and RESET bits 7 and 8.
1.Configuration CubeMX:
- The Circular Mode can be directly selected in the Queue configuration
- In this example, we can use the function
two_node_pulse_stream
2.The prototype would be : void two_node_pulse_stream()
3.The selected mode used is: DMA_LINKEDLIST_CIRCULAR
4.The function would be:
/* IMPORTANT: Remove or exclude linkedlist.c/h (CubeMX generated)
* to avoid conflicts with this GPDMA configuration.
*/
void two_node_pulse_stream()
{
/**
* This function demonstrates an advanced use of GPDMA with two nodes and circular mode.
*
* How it differs from the previous "two_node_pulse_sequence":
* - Each node now transfers a **sequence of 16 values** (not just one) to generate a complex pulse train on each pin.
* - The source buffers for each node contain a pattern of SET and RESET values, creating multiple pulses per node.
* - The DMA is configured in **circular mode**, so the entire sequence (Node1 then Node2) repeats endlessly.
* - This allows you to generate continuous, complex, and synchronized pulse streams on two different GPIOs with zero CPU load.
*/
/******* 1. Variables: Prepare the pulse patterns for each node *********/
// Node 1: Sequence for PA8 (bit 8 of GPIOA)
// - 4x SET, 4x RESET, 4x SET, 4x RESET (last value)
// This creates a custom pulse pattern on PA8
uint32_t src_buffer_node1_size_in_bytes = 16*4U; // 16 words (4 bytes each)
src_buffer_node1[0]=0x00000100; //SET bit 8
src_buffer_node1[1]=0x00000100; //SET bit 8
src_buffer_node1[2]=0x00000100; //SET bit 8
src_buffer_node1[3]=0x00000100; //SET bit 8
src_buffer_node1[4]=0x01000000; //RESET bit 8
src_buffer_node1[5]=0x01000000; //RESET bit 8
src_buffer_node1[6]=0x01000000; //RESET bit 8
src_buffer_node1[7]=0x01000000; //RESET bit 8
src_buffer_node1[8]=0x00000100; //SET bit 8
src_buffer_node1[9]=0x00000100; //SET bit 8
src_buffer_node1[10]=0x00000100;//SET bit 8
src_buffer_node1[11]=0x00000100;//SET bit 8
src_buffer_node1[12]=0x01000000;//RESET bit 8
src_buffer_node1[13]=0x01000000;//RESET bit 8
src_buffer_node1[14]=0x01000000;//RESET bit 8
src_buffer_node1[15]=0x00800000;//RESET bit 8
// Node 2: Sequence for PC7 (bit 7 of GPIOC)
// - 4x SET, 4x RESET, 4x SET, 4x RESET
// This creates a custom pulse pattern on PC7
uint32_t src_buffer_node2_size_in_bytes = 16*4U; //number of byte in node 2
src_buffer_node2[0]=0x00000080; //SET bit 7
src_buffer_node2[1]=0x00000080; //SET bit 7
src_buffer_node2[2]=0x00000080; //SET bit 7
src_buffer_node2[3]=0x00000080; //SET bit 7
src_buffer_node2[4]=0x00800000; //RESET bit 7
src_buffer_node2[5]=0x00800000; //RESET bit 7
src_buffer_node2[6]=0x00800000; //RESET bit 7
src_buffer_node2[7]=0x00800000; //RESET bit 7
src_buffer_node2[8]=0x00000080; //SET bit 7
src_buffer_node2[9]=0x00000080; //SET bit 7
src_buffer_node2[10]=0x00000080;//SET bit 7
src_buffer_node2[11]=0x00000080;//SET bit 7
src_buffer_node2[12]=0x00800000;//RESET bit 7
src_buffer_node2[13]=0x00800000;//RESET bit 7
src_buffer_node2[14]=0x00800000;//RESET bit 7
src_buffer_node2[15]=0x00800000;//RESET bit 7
/******* DMA Channel Configuration *********/
__HAL_RCC_GPDMA1_CLK_ENABLE();
handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0;
handle_GPDMA1_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
handle_GPDMA1_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
handle_GPDMA1_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
// *** Key difference: LinkedListMode is set to CIRCULAR for continuous looping ***
handle_GPDMA1_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_CIRCULAR;
if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel0) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
/*******Node Configuration: Create two nodes with pulse patterns *********/
HAL_StatusTypeDef ret = HAL_OK;
/* DMA node configuration declaration */
DMA_NodeConfTypeDef pNodeConfig;
// --- Node 1: Pulse pattern for PA8 ---
pNodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;
pNodeConfig.Init.Request = DMA_REQUEST_SW;
pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.Init.SrcInc = DMA_SINC_INCREMENTED;// DMA will read the whole buffer sequentially
pNodeConfig.Init.DestInc = DMA_DINC_FIXED;// Always write to the same register
pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;
pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;
pNodeConfig.Init.SrcBurstLength = 1;
pNodeConfig.Init.DestBurstLength = 1;
pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
pNodeConfig.Init.Mode = DMA_NORMAL;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node1; // Buffer for PA8
pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR; // Target: GPIOA BSRR
pNodeConfig.DataSize = src_buffer_node1_size_in_bytes;// 16 words (64 bytes)
/* Build Node1 Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node1);
/* Insert Node1 to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node1);
// --- Node 2: Pulse pattern for PC7 ---
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node2; // Buffer for PC7
pNodeConfig.DstAddress = (uint32_t)&GPIOC->BSRR; // Target: GPIOC BSRR
pNodeConfig.DataSize = src_buffer_node2_size_in_bytes;// 16 words (64 bytes)
/* Build Node2 Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node2);
/* Insert Node2 to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node2);
// Set the queue to circular mode so the sequence repeats forever
ret |= HAL_DMAEx_List_SetCircularMode(&Queue);
/******* 4. Link the queue to the DMA channel *********/
if(HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel0, &Queue)!=HAL_OK)
{
Error_Handler();
}
/******* Start the DMA transfer *********/
// Toggle PB10 as a marker for oscilloscope/debug
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
// Start the DMA: will execute Node 1 (pulse pattern on PA8),
//then Node 2 (pulse pattern on PC7), and repeat
if (HAL_DMAEx_List_Start(&handle_GPDMA1_Channel0)!=0)//--->SET PA8
{
Error_Handler();
}
}
5.Signal observation should look like this:
while (1)
{
/* USER CODE END WHILE */
//Temps (µs): -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10
//|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|
//PB10: _|‾‾‾|__________________________________________________________
//PA8: _______|‾||‾|______|‾||‾|_______|‾||‾|______|‾||‾|______|‾||‾|_
//PC7: _____________|‾||‾|______|‾||‾|_______|‾||‾|_____|‾||‾|_______
6.3.3. Triggered Pulse Stream (Four Nodes)
With your configuration, TIM2_CH2 outputs a PWM signal (square wave). On each falling edge of this PWM (as set by your DMA trigger), PA8 is set high (node 1), then immediately after, PC7 outputs two short pulses (node 2: SET, RESET, SET, RESET). On the next falling edge of the PWM, PA8 is reset low (node 3), then again PC7 outputs two short pulses (node 4: SET, RESET, SET, RESET). PB10 is only toggled once at the very start as a marker and remains unchanged during the cycle. This sequence repeats in a circular fashion: PA8 toggles state on every falling edge of TIM2_CH2, and after each toggle, PC7 emits two pulses, all perfectly synchronized to the PWM timing, while PB10 stays static after its initial toggle. To do this, we will add, in addition to the timer, three instructions for the trigger on the rising edge.
1. CubeMX Configuration:
- Timer TIM2: the signal here ius generated to value of 1KHz and duty cycle of 50%
- Prescaler:
63 - AutoReload Register:
999 - Trigger Event Selection TRGO:
Update Event - Pulse:
500 - Output compare preload:
Enable - auto-reload preload:
Enable
- Prescaler:
- The Circular Mode can be directly selected in the Queue configuration.
- In this example, use the function
triggered_Pulse_Stream. - Also, add three other nodes:
node2,node3, andnode4 identical to node 1.
2. The selected mode used is: DMA_LINKEDLIST_CIRCULAR
3. Private variables:
/* USER CODE BEGIN PV */
volatile uint32_t src_buffer_node1[64]; // number of bytes in node 1
volatile uint32_t src_buffer_node2[64]; // number of bytes in node 2
volatile uint32_t src_buffer_node3[64]; // number of bytes in node 3
volatile uint32_t src_buffer_node4[64]; // number of bytes in node 4
4. The prototype is:
/* USER CODE BEGIN 2 */
//two_node_pulse_sequence();
//two_node_pulse_stream();
triggered_Pulse_Stream();
/* USER CODE END 2 */
5. The function would be:
/* IMPORTANT: Remove or exclude linkedlist.c/h (CubeMX generated)
* to avoid conflicts with this GPDMA configuration.
*/
void triggered_Pulse_Stream()
{
/**
* This function demonstrates an advanced use of GPDMA with four nodes,
* hardware triggering, and circular mode.
*
* How it differs from the previous "two_node_pulse_stream":
* - The sequence now consists of **four nodes** instead of two.
* - **Nodes 1 and 3** (for PA8) are triggered by a hardware event (falling edge of TIM2 PWM).
* - **Nodes 2 and 4** (for PC7) are executed immediately after the previous node (no trigger).
* - The pattern is:
* 1. SET PA8 (on timer trigger)
* 2. Pulse sequence on PC7 (immediate)
* 3. RESET PA8 (on next timer trigger)
* 4. Pulse sequence on PC7 (immediate)
* - The entire sequence is in **circular mode**, so it repeats endlessly,
* and the pulses on PA8 are precisely synchronized to the timer.
*/
//variables
uint32_t src_buffer_node1_size_in_bytes = 1*4U;
src_buffer_node1[0]=0x00000100; //SET bit 8
uint32_t src_buffer_node2_size_in_bytes = 16*4U;
src_buffer_node2[0]=0x00000080; //SET bit 7
src_buffer_node2[1]=0x00000080; //SET bit 7
src_buffer_node2[2]=0x00000080; //SET bit 7
src_buffer_node2[3]=0x00000080; //SET bit 7
src_buffer_node2[4]=0x00800000; //RESET bit 7
src_buffer_node2[5]=0x00800000; //RESET bit 7
src_buffer_node2[6]=0x00800000; //RESET bit 7
src_buffer_node2[7]=0x00800000; //RESET bit 7
src_buffer_node2[8]=0x00000080; //SET bit 7
src_buffer_node2[9]=0x00000080; //SET bit 7
src_buffer_node2[10]=0x00000080;//SET bit 7
src_buffer_node2[11]=0x00000080;//SET bit 7
src_buffer_node2[12]=0x00800000;//RESET bit 7
src_buffer_node2[13]=0x00800000;//RESET bit 7
src_buffer_node2[14]=0x00800000;//RESET bit 7
src_buffer_node2[15]=0x00800000;//RESET bit 7
uint32_t src_buffer_node3_size_in_bytes = 1*4U;
src_buffer_node3[0]=0x01000000; //RESET bit 8
//Node 4 equal Node 2
uint32_t src_buffer_node4_size_in_bytes = 16*4U;
//config channel
__HAL_RCC_GPDMA1_CLK_ENABLE();
handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0;
handle_GPDMA1_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
handle_GPDMA1_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
handle_GPDMA1_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
handle_GPDMA1_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_CIRCULAR;
if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel0) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
//config Linked List
HAL_StatusTypeDef ret = HAL_OK;
/* DMA node configuration declaration */
DMA_NodeConfTypeDef pNodeConfig;
/* Set node configuration ################################################*/
pNodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;
pNodeConfig.Init.Request = DMA_REQUEST_SW;
pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.Init.SrcInc = DMA_SINC_INCREMENTED;
pNodeConfig.Init.DestInc = DMA_DINC_FIXED;
pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;
pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;
pNodeConfig.Init.SrcBurstLength = 1;
pNodeConfig.Init.DestBurstLength = 1;
pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
pNodeConfig.Init.Mode = DMA_NORMAL;
/* pNodeConfig.RepeatBlockConfig.RepeatCount = 1;
pNodeConfig.RepeatBlockConfig.SrcAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.DestAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.BlkSrcAddrOffset = 0;
pNodeConfig.RepeatBlockConfig.BlkDestAddrOffset = 0;*/
pNodeConfig.TriggerConfig.TriggerMode = DMA_TRIGM_SINGLE_BURST_TRANSFER;//DMA_TRIGM_SINGLE_BURST_TRANSFER ;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_FALLING;//change rising to falling edge
pNodeConfig.TriggerConfig.TriggerSelection = GPDMA1_TRIGGER_TIM2_TRGO;
pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node1;
pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR;
pNodeConfig.DataSize = src_buffer_node1_size_in_bytes;
/* Build Node1 Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node1);
/* Insert Node1 to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node1);
/* Set node configuration ################################################*/
pNodeConfig.Init.Request = DMA_REQUEST_SW;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.TriggerConfig.TriggerMode = DMA_TRIGM_LLI_LINK_TRANSFER;//DMA_TRIGM_SINGLE_BURST_TRANSFER ;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node2;
pNodeConfig.DstAddress = (uint32_t)&GPIOC->BSRR;
pNodeConfig.DataSize = src_buffer_node2_size_in_bytes;
/* Build Node2 Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node2);
/* Insert Node2 to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node2);
pNodeConfig.Init.Request = DMA_REQUEST_SW;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.TriggerConfig.TriggerMode = DMA_TRIGM_SINGLE_BURST_TRANSFER;//DMA_TRIGM_SINGLE_BURST_TRANSFER ;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_FALLING;//change rising to falling edge
pNodeConfig.TriggerConfig.TriggerSelection = GPDMA1_TRIGGER_TIM2_TRGO;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node3;
pNodeConfig.DstAddress = (uint32_t)&GPIOA->BSRR;
pNodeConfig.DataSize = src_buffer_node3_size_in_bytes;
/* Build Node3 Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node3);
/* Insert Node3 to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node3);
pNodeConfig.Init.Request = DMA_REQUEST_SW;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
pNodeConfig.TriggerConfig.TriggerMode = DMA_TRIGM_LLI_LINK_TRANSFER;//DMA_TRIGM_SINGLE_BURST_TRANSFER ;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
pNodeConfig.SrcAddress = (uint32_t)src_buffer_node2;
pNodeConfig.DstAddress = (uint32_t)&GPIOC->BSRR;
pNodeConfig.DataSize = src_buffer_node4_size_in_bytes;
/* Build Node4 Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node4);
/* Insert Node4 to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node4);
ret |= HAL_DMAEx_List_SetCircularMode(&Queue);
//1-Create the Queue
if(HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel0, &Queue)!=HAL_OK)
{
Error_Handler();
}
//2-Signal PWM
HAL_TIM_Base_Start(&htim2);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
//3- Enable DMA interrupts (TC)
//__HAL_DMA_ENABLE_IT(&handle_GPDMA1_Channel0, DMA_IT_TC);
//4-Start GPDMA
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
if (HAL_DMAEx_List_Start(&handle_GPDMA1_Channel0)!=0)//--->SET PA8
{
Error_Handler();
}
}
6. The signal observation looks like this:
while (1)
{
/*
Time (ms): 0 2 4 6 8 10 12 14 16 18 20
|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
PB10: ___|‾‾‾|_________________________________________________________
TIM2: ‾‾‾‾‾‾‾‾|______|‾‾‾‾‾‾‾‾|______|‾‾‾‾‾‾‾‾|______|‾‾‾‾‾‾‾‾|______
PA8: _________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|_______________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|__________
PC7: __________|‾|_|‾|________|‾|_|‾|_________|‾|_|‾|___________|‾||‾|_
*/
6.3.4. GPIO Sampling with GPDMA and Interrupts
Use a function generator (GBF) to apply a square wave signal to PC7. Sample PC7 at a precise frequency using GPDMA triggered by a timer (TIM2) . Store the samples in a RAM buffer in circular mode. Use DMA interrupts (Half Transfer and Transfer Complete) to analyze the buffer in real time (e.g., count the number of "1"s). Light up a LED (PA8) if the number of "1"s exceeds a threshold.
1. STM32CubeMX Configuration
- GPIO:
- PC7 as input (to be sampled)
- PA8 and PB10 as output (for LED/status)
- GPDMA1:
- Enable Channel 0, Linked List mode
- Enable NVIC/Interrupt for GPDMA1 Channel 0
- Timer (TIM2)
- PWM mode, Channel 2
- Set frequency and duty cycle for sampling rate
- Set Trigger Output (TRGO) to OC2REF
- Linked List
- Create a queue: Queue_IT
- GPDMA1 for DMA Channel and select linear mode
- Create one node: Node_IT
- Generate code and open in STM32CubeIDE
2. Prepare Buffers and Variables
// Define the size of the data buffer for DMA sampling.
// Here, 16*4U means 16 words (each word is 4 bytes), so the buffer can hold 16 samples.
#define size_data_buffer_IT 16*4U
// Buffer where the DMA will store the sampled values from GPIOC->IDR (PC7).
uint32_t dest_buffer_IT[size_data_buffer_IT];
// Debug buffer: used to store intermediate results or for validation during debugging.
uint32_t debug_buffer_IT[size_data_buffer_IT];
// Flags to detect DMA events.
// TransferCompleteDetected: increments when the DMA completes a full buffer transfer.
// HalfTransferDetected: increments when the DMA completes half of the buffer.
__IO uint32_t TransferCompleteDetected = 0U, HalfTransferDetected = 0U;
// Variable to accumulate the result of the analysis (e.g., number of "1"s detected in the buffer).
__IO uint32_t myresult = 0U;
No function is created by default for the Callback of GPDMA, then we are creating here two personalized functions MyGPDMA_HalfCallback and MyGPDMA_TCompleteCallback
3.Create a function Callback Halftransfer:
// Callback function called automatically by the DMA driver when half of the buffer has been filled.
// This allows real-time processing of the first half of the sampled data, without waiting for the full buffer.
void MyGPDMA_HalfCallback()
{
// Optionally, you could set PA8 high here to indicate the half-transfer event on a scope or LED.
// HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, SET);
uint32_t i = 0;
// Loop through the first half of the buffer (here, 8 samples).
for(i = 0; i < 8; i++)
{
// Extract the value of bit 7 (corresponding to PC7) from each sampled word.
// The value is either 0 or 1, depending on the state of PC7 at the time of sampling.
// Add the result to the running total (myresult).
myresult = ( (dest_buffer_IT[i] >> 7) & 1 ) + myresult;
// Store the cumulative result in the debug buffer for analysis or debugging.
debug_buffer_IT[i] = myresult;
}
// Increment the half-transfer event counter.
HalfTransferDetected++;
}
4.Create a function Callback Transfercomplete:
// Callback function called automatically by the DMA driver when the buffer transfer is complete.
// This allows real-time processing of the second half of the sampled data.
void MyGPDMA_TCompleteCallback()
{
uint32_t i = 0;
// Optionally, you could reset PA8 here to indicate the transfer complete event on a scope or LED.
// HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, RESET);
// Loop through the second half of the buffer (samples 8 to 15).
for(i = 8; i < 16; i++)
{
// Extract the value of bit 7 (corresponding to PC7) from each sampled word.
// Add the result to the running total (myresult).
myresult = ( (dest_buffer_IT[i] >> 7) & 1 ) + myresult;
// Store the cumulative result in the debug buffer for analysis or debugging.
debug_buffer_IT[i] = myresult;
}
// If the number of detected "1"s (high level on PC7) exceeds the threshold (here, 5),
// turn on the LED connected to PA8. Otherwise, turn it off.
if(myresult > 5)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, SET);
}
else
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, RESET);
}
// Reset the result accumulator for the next DMA cycle.
myresult = 0;
// Increment the transfer complete event counter.
TransferCompleteDetected++;
}
3.DMA Node and Queue Configuration
/* IMPORTANT: Remove or exclude linkedlist.c/h (CubeMX generated)
* to avoid conflicts with this GPDMA configuration.
*/
// Function to configure and start a GPDMA transfer with interrupt handling for GPIO sampling.
// This function sets up the DMA, the node, the queue, the timer trigger, and the callbacks.
void transfer_DMA_IT()
{
// 1. Enable the GPDMA1 peripheral clock (required for DMA operation).
__HAL_RCC_GPDMA1_CLK_ENABLE();
// 2. Configure the interrupt for GPDMA1 Channel 0.
// Set the highest priority (0,0) and enable the IRQ in the NVIC.
HAL_NVIC_SetPriority(GPDMA1_Channel0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(GPDMA1_Channel0_IRQn);
// --- DMA Channel Configuration ---
handle_GPDMA1_Channel0.Instance = GPDMA1_Channel0;
handle_GPDMA1_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
handle_GPDMA1_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
handle_GPDMA1_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
// Set the linked list mode to circular for continuous sampling.
handle_GPDMA1_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_CIRCULAR;
// Initialize the DMA channel and configure its attributes.
if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel0) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
HAL_StatusTypeDef ret = HAL_OK;
DMA_NodeConfTypeDef pNodeConfig;
// --- DMA Node Configuration ---
// Set up a linear node for memory-to-memory transfer (from GPIOC->IDR to RAM buffer).
pNodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;
pNodeConfig.Init.Request = DMA_REQUEST_SW;
pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;
// Source is fixed (always read from GPIOC->IDR).
pNodeConfig.Init.SrcInc = DMA_SINC_FIXED;
// Destination is incremented (fill the buffer sequentially).
pNodeConfig.Init.DestInc = DMA_DINC_INCREMENTED;
pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;
pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;
pNodeConfig.Init.SrcBurstLength = 1;
pNodeConfig.Init.DestBurstLength = 1;
pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
pNodeConfig.Init.Mode = DMA_NORMAL;
// --- Trigger Configuration ---
// The DMA transfer is triggered by the falling edge of TIM2_TRGO (timer output compare).
pNodeConfig.TriggerConfig.TriggerMode = DMA_TRIGM_SINGLE_BURST_TRANSFER;
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_FALLING;
pNodeConfig.TriggerConfig.TriggerSelection = GPDMA1_TRIGGER_TIM2_TRGO;
// --- Data Handling Configuration ---
pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
// --- Source and Destination Addresses ---
// Source: GPIOC->IDR (input data register, reads the state of PC7).
// Destination: RAM buffer for storing samples.
pNodeConfig.SrcAddress = (uint32_t)&GPIOC->IDR;
pNodeConfig.DstAddress = (uint32_t)dest_buffer_IT;
pNodeConfig.DataSize = size_data_buffer_IT;
// Build the node and insert it into the queue.
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node_IT);
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue_IT, &Node_IT);
// Set the queue to circular mode for continuous operation.
ret |= HAL_DMAEx_List_SetCircularMode(&Queue_IT);
// Link the queue to the DMA channel.
if(HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel0, &Queue_IT)!=HAL_OK)
{
Error_Handler();
}
// 3. Start the PWM signal on TIM2 Channel 2.
// This timer will generate the trigger events for the DMA sampling.
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
// 4. Register the callback functions for DMA interrupts.
// These functions will be called on half-transfer and transfer-complete events.
HAL_DMA_RegisterCallback(&handle_GPDMA1_Channel0, HAL_DMA_XFER_HALFCPLT_CB_ID, MyGPDMA_HalfCallback);
HAL_DMA_RegisterCallback(&handle_GPDMA1_Channel0, HAL_DMA_XFER_CPLT_CB_ID, MyGPDMA_TCompleteCallback);
// 5. Optionally, toggle PB10 as a debug marker (e.g., for oscilloscope).
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
// 6. Start the DMA transfer in interrupt mode.
// The DMA will now sample PC7 at each timer event, fill the buffer, and call the callbacks.
if (HAL_DMAEx_List_Start_IT(&handle_GPDMA1_Channel0)!=0)
{
Error_Handler();
}
}
6.Arguments of Callbacks in transfer_DMA_IT()
Use the instruction HAL_DMA_RegisterCallback to select Callbacks functions called.
The arguments HAL_DMA_XFER_HALFCPLT_CB_ID andHAL_DMA_XFER_HALFCPLT_CB_ID come from stm32h5xx.hal_dma.h file:
7.Place the prototype:
/* USER CODE BEGIN PFP */
static void MyGPDMA_HalfCallback();
static void MyGPDMA_TCompleteCallback();
void transfer_DMA_IT();
8.main function:
/* USER CODE BEGIN 2 */
transfer_DMA_IT();
/* USER CODE END 2 */
9.Hardware setup
- Use a function generator (GBF) to apply a square wave signal to PC7.
- Sample PC7 at a precise frequency using GPDMA triggered by a timer (TIM2).
- Store the samples in a RAM buffer in circular mode.
- Use DMA interrupts (Half Transfer and Transfer Complete) to analyze the buffer in real time (e.g., count the number of "1"s).
- Light up a LED (PA8) if the number of "1"s exceeds a threshold.
6.4. Expert Level Guide: 2D Addressing
Objective: The expert level targets users who want to fully exploit the advanced capabilities of GPDMA and design systems where DMA becomes a core functional component, not just a data mover.
At this level, the focus is on:
- Advanced node configurations
- Complex synchronization between nodes, triggers, and execution flow
- 2D addressing, enabling matrix, image, or frame-based transfers
- Performance awareness and scalability for real applications
The user will learn how to:
- Build complex DMA templates suitable for reuse across projects
- Handle structured data such as matrices or frames
- Combine linked lists, triggers, and addressing modes into coherent DMA architectures
- Offload not only data transfer, but also control logic, from the CPU to GPDMA
This level is intended for:
- High‑performance embedded systems
- Display, camera, signal processing, or real‑time control applications
- Engineers who want to master GPDMA as a system-level design tool
By the end of this level, the user will be capable of designing robust, autonomous DMA-driven systems, with minimal CPU involvement and maximum determinism.
| Step | Use Case name | What does it do? | How to do it? | Function name | Done? |
|---|---|---|---|---|---|
| 5.1 | 2D transfer (matrix/image) | Transfers a block of data in two dimensions (e.g., for LCD, camera, or DSP). | Linked List mode, 2D addressing | transfer matrix | ☐ |
6.4.1. 2D transfer (matrix/image)
Objective: Efficiently transfer a matrix (or an image) from RAM to RAM (or to a peripheral) using the GPDMA 2D addressing mode, without CPU intervention, for applications such as LCD display, camera capture, or DSP processing.
1.Configuration CubeMX:
- Only 2 Channels can be used for 2D addressing, here, we are choosing: Channel7 of GPDMA1:
- Then, Linked list configuration will like that:
- Warning: Don't forget to activate 2D addressing configuration:
2.Prepare the buffers:
#define MATRIX_ROWS 4
#define MATRIX_COLS 8
volatile uint32_t src_matrix[MATRIX_ROWS][MATRIX_COLS] = {
{1,2,3,4,5,6,7,8},
{9,10,11,12,13,14,15,16},
{17,18,19,20,21,22,23,24},
{25,26,27,28,29,30,31,32}
};
volatile uint32_t dst_matrix[MATRIX_ROWS][MATRIX_COLS];
3.Configure the DMA node in 2D mode:
- NodeType:
DMA_GPDMA_2D_NODE - Direction:
DMA_MEMORY_TO_MEMORY(orDMA_MEMORY_TO_PERIPH) - DataWidth:
DMA_SRC_DATAWIDTH_WORD/DMA_DEST_DATAWIDTH_WORD - Inc:
DMA_SINC_INCREMENTED/DMA_DINC_INCREMENTED - RepeatCount: Number of rows
- DataSize: Number of bytes per line
4.Explanation of principal fields for 2D transfer:
RepeatCount = matrix_rows- Definition: Number of times the block transfer is repeated, i.e., the number of rows to transfer.
- Example: For a 4x8 matrix,
RepeatCount = 4(for 4 rows).
SrcAddrOffset = 0- Definition: Offset (in bytes) applied to the source address after each element transferred within a row.
- Why 0? If you want to transfer the entire row as a contiguous block, there should be no gap between elements.
DestAddrOffset = 0- Definition: Offset (in bytes) applied to the destination address after each element transferred within a row.
- Why 0? Same as above, no gap between elements in the destination row.
BlkSrcAddrOffset = 0- Definition: Offset (in bytes) applied to the source address after each row (after each block of
DataSize). - Why 0? If your matrix is contiguous in memory, there should be no gap between rows (DMA continues right after the previous row).
- Definition: Offset (in bytes) applied to the source address after each row (after each block of
BlkDestAddrOffset = 0- Definition: Offset (in bytes) applied to the destination address after each row.
- Why 0? Same as above, no gap between rows in the destination matrix.
TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED- Definition: DMA does not wait for a hardware trigger; it starts as soon as the software command is issued.
- Usage: For a software-triggered transfer, the trigger is masked.
DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE- Definition: No data exchange or permutation during the transfer.
- Usage: Direct transfer, no data modification.
DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED- Definition: Data is right-aligned and zero-padded if needed.
- Usage: Standard for word (
uint32_t) transfers.
SrcAddressandDstAddress- Definition: Start addresses of the source and destination matrices.
- Usage: Pointers to the beginning of your 2D buffers.
DataSize = matrix_cols * sizeof(uint32_t)- Definition: Number of bytes to transfer per row (the size of one row).
- Example: For 8 columns of 4 bytes each,
DataSize = 32.
5.The function to create would look like this:
/* IMPORTANT: Remove or exclude linkedlist.c/h (CubeMX generated)
* to avoid conflicts with this GPDMA configuration.
*/
void Transfer_2D()
{
/**
* This function demonstrates a true 2D (matrix/image) transfer using GPDMA.
*
* How it is fundamentally different from all previous examples:
* --------------------------------------------------------------------------
* - Previous examples used 1D (linear) nodes to transfer single values or pulse patterns to GPIO registers.
* - Here, we use a **2D node (DMA_GPDMA_2D_NODE)** to transfer an entire matrix (multiple rows and columns) from one memory region to another.
* - The DMA is configured to treat the source and destination as 2D arrays, automatically handling row/column traversal.
* - This is ideal for copying images, matrices, or blocks of data (e.g., for LCD, camera, or DSP), not just toggling pins.
* - There is **no hardware trigger, no GPIO, and no pulse pattern**: this is a pure memory-to-memory block transfer, optimized for 2D data.
* --------------------------------------------------------------------------
*/
/******* 1. Enable the GPDMA1 clock (required for DMA operation) *********/
__HAL_RCC_GPDMA1_CLK_ENABLE();
/* USER CODE BEGIN GPDMA1_Init 1 */
/* USER CODE END GPDMA1_Init 1 */
/******* 2. DMA Channel 7 Configuration (for 2D transfer) *********/
// This configures the DMA channel for linked list mode, but we only use a single node for the 2D transfer.
handle_GPDMA1_Channel7.Instance = GPDMA1_Channel7;
handle_GPDMA1_Channel7.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
handle_GPDMA1_Channel7.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
handle_GPDMA1_Channel7.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
handle_GPDMA1_Channel7.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
handle_GPDMA1_Channel7.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_NORMAL;// Not circular, just a single 2D transfer
if (HAL_DMAEx_List_Init(&handle_GPDMA1_Channel7) != HAL_OK)
{
Error_Handler();
}
if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA1_Channel7, DMA_CHANNEL_NPRIV) != HAL_OK)
{
Error_Handler();
}
/******* 3. Node Configuration: 2D Transfer Node *********/
HAL_StatusTypeDef ret = HAL_OK;
/* DMA node configuration declaration */
DMA_NodeConfTypeDef pNodeConfig;
// --- Key difference: NodeType is 2D, not linear ---
pNodeConfig.NodeType = DMA_GPDMA_2D_NODE;// This enables 2D addressing (matrix/image transfer)
pNodeConfig.Init.Request = DMA_REQUEST_SW; // Software-triggered
pNodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
pNodeConfig.Init.Direction = DMA_MEMORY_TO_MEMORY;// Memory-to-memory transfer
pNodeConfig.Init.SrcInc = DMA_SINC_INCREMENTED;// Source increments through the matrix
pNodeConfig.Init.DestInc = DMA_DINC_INCREMENTED;// Destination increments through the matrix
pNodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_WORD;// 32-bit data (uint32_t)
pNodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_WORD;// 32-bit data (uint32_t)
pNodeConfig.Init.SrcBurstLength = 1;
pNodeConfig.Init.DestBurstLength = 1;
pNodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0|DMA_DEST_ALLOCATED_PORT0;
pNodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
pNodeConfig.Init.Mode = DMA_NORMAL;
// --- 2D addressing parameters ---
// RepeatCount: number of rows (how many times to repeat the line transfer)
pNodeConfig.RepeatBlockConfig.RepeatCount = MATRIX_ROWS; // e.g., 4 for a 4x8 matrix
// SrcAddrOffset/DestAddrOffset: step between elements in a row (0 for contiguous)
pNodeConfig.RepeatBlockConfig.SrcAddrOffset = 0;// No gap between elements in a row
pNodeConfig.RepeatBlockConfig.DestAddrOffset =0;//
// BlkSrcAddrOffset/BlkDestAddrOffset: step between rows (0 for contiguous matrix)
// If you had a stride (e.g., copying a submatrix from a larger image), you would set these to skip bytes.
pNodeConfig.RepeatBlockConfig.BlkSrcAddrOffset = 0;// Step between lines
pNodeConfig.RepeatBlockConfig.BlkDestAddrOffset = 0;//
// No hardware trigger: transfer starts immediately
pNodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
pNodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
pNodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
// Source and destination addresses: start of the source and destination matrices
pNodeConfig.SrcAddress = (uint32_t)src_matrix;
pNodeConfig.DstAddress = (uint32_t)dst_matrix;
// DataSize: number of bytes to transfer per row (width of the matrix in bytes)
pNodeConfig.DataSize = MATRIX_COLS * sizeof(uint32_t);
/* Build Node Node */
ret |= HAL_DMAEx_List_BuildNode(&pNodeConfig, &Node);
/* Insert Node to Queue */
ret |= HAL_DMAEx_List_InsertNode_Tail(&Queue, &Node);
/******* 4. Link the queue (with a single 2D node) to the DMA channel *********/
HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel7, &Queue);
/******* 5. Start the DMA transfer *********/
// Toggle PB10 as a marker for oscilloscope/debug (optional)
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_10, RESET);
// Start the DMA: it will transfer the entire matrix from src_matrix to dst_matrix
HAL_DMAEx_List_Start(&handle_GPDMA1_Channel7);
}
/* USER CODE END 4 */
6.Place the prototype:
/* USER CODE BEGIN PFP */
void Transfer_2D();
7.main function:
Transfer_2D();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
8.Debugging: Then:
After:















