on 2026-05-06 3:00 AM
This article describes, in a practical and end-to-end manner, how to create and run a minimal AES-GCM example using DMA on an STM32C5 device configured using STM32CubeMX2. The goal is to provide the source code and document the full workflow from project setup and peripheral configuration through to flashing the device, running the demo, and validating the output.
It is helpful if the reader already understands the basics of:
Even without deep cryptography knowledge, the procedure remains understandable, since the demo uses fixed test vectors and does not require deriving cryptographic values manually.
Begin by creating a new STM32C5 project or opening the existing one that will host the example. In this example, NUCLEO-C562RE is used as starting board.
After the project is created, review the clock tree carefully. The clock configuration must be valid for the device and compatible with the intended runtime environment. Although this demo is not particularly sensitive to high throughput, an invalid or inconsistent clock configuration can still prevent the system from starting properly. Additionally, it can disable peripherals indirectly if bus clocks are not configured as expected.
In this example, the default clock configuration is kept.
The example uses console messages to indicate whether each phase of the demo has passed. For that reason, it is useful to configure a standard output path early in the process. Depending on the project environment, this may be UART-based, a virtual COM port, semihosting, or another redirected standard I/O method.
The purpose of this step is not merely convenience. Visible logs dramatically reduce debug time during bring-up, especially when checking whether the code reached the encryption stage, got stuck waiting for DMA completion, or failed during result comparison.
In this example, a virtual COM port is used. By inspecting the board schematics, USART2 is connected to the debug virtual COM port, so it is essential to enable that UART instance.
The default USART configuration is kept.
To simplify the COM port initialization process, the Basic STDIO utility is used. By specifying the USART instance and generating the code, calling mx_basic_stdio_init() inside the main() function takes care of overloading printf() and redirecting its output to UART so it can be displayed on the serial interface.
This example uses the GCM test vector provided by NIST Test Patterns gcmEncryptExtIV256.rsp.
First, enable the AES peripheral in the peripheral tree. Then configure the AES-GCM parameters as follows:
0a682fbc6192e1b47a5e0868787ffdafe5a50cead3575849990cdd2ea9b3597749403efb4a56684f0c6bde352d4aeec5
611ce6f9a6880750de7da6cb00000002463b412911767d57a0b33969e674ffe7845d313b88c6fe312f3d724be68e1fcaAfter AES is configured, set up the DMA channels used for the AES data path.
For both DMA channels, keep [Interrupt handler generation] enabled so that the required DMA support code is generated automatically.
Before generating the project code, enable the definition required to register custom AES callbacks. To do this:
This step is required because the example registers custom AES callbacks for DMA completion and error handling.
After the peripheral and interrupt configuration is complete, save the CubeMX2 project and generate the source code.
Once the generation is complete:
This intermediate build serves an important purpose. It confirms that the generated startup code, peripheral initialization code, includes, and linker setup are all coherent. If an issue exists here, solving it now avoids confusion later when application code is also present.
Include the application, HAL, and standard C headers required by the example.
#include "main.h"
#include "mx_basic_stdio_app.h"
#include <string.h>
Add a timeout used when retrieving the GCM authentication tag.
#define AES_TIMEOUT_MS 100
Add the global variables used to configure and access the AES peripheral.
hal_aes_gcm_config_t p_gcm_config;
hal_aes_handle_t *pAES;
Use a known NIST AES-GCM test vector so the example can validate both encryption and decryption.
extern const uint32_t AESKey[8];
extern uint32_t InitVector[4];
extern uint32_t HeaderMessage[12];
uint32_t plainText[13] =
{
0xe7d1dcf6, 0x68e28768, 0x61940e01, 0x2fe52a98,
0xdacbd78a, 0xb63c0884, 0x2cc9801e, 0xa581682a,
0xd54af0c3, 0x4d0d7f6f, 0x59e8ee0b, 0xf4900e0f,
0xd8504200
};
const uint32_t expectedCipherText[13] =
{
0x8886e196, 0x010cb384, 0x9d9c1a18, 0x2abe1eea,
0xb0a5f3ca, 0x423c3669, 0xa4a8703c, 0x0f146e8e,
0x956fb122, 0xe0d721b8, 0x69d2b6fc, 0xd4216d7d,
0x4d375800
};
const uint32_t expectedTag[4] =
{
0x2469cecd, 0x70fd98fe, 0xc9264f71, 0xdf1aee9a
};
Create buffers to store the generated ciphertext, decrypted plaintext, and authentication tag.
uint32_t computedCiphertext[13] = {0};
uint32_t computedPlaintext[13] = {0};
uint32_t Tag[4] = {0};
Declare flags that will be updated by the AES callbacks.
volatile uint32_t OutTransferCpltCb = 0;
volatile uint32_t ErrorCb = 0;
Declare the helper functions used later in the file.
static void OutTransfertCpltCallback(hal_aes_handle_t *pAES);
static int WaitForAesDmaCompletion(void);
static void ErrorCallback(hal_aes_handle_t *pAES);
At the start of main(), initialize the system and the console output.
if (mx_system_init() != SYSTEM_OK)
{
return (-1);
}
else
{
mx_basic_stdio_init();
Retrieve the AES handle from the generated code.
pAES = mx_aes_init();
if (pAES == NULL)
{
return (-1);
}
printf("[INFO] AES, DMA initialization and configuration to work with GCM DMA mode COMPLETED.\n");
Register one callback for successful transfer completion and one for error handling.
if (HAL_AES_RegisterOutTransferCpltCallback(pAES, OutTransfertCpltCallback) != HAL_OK)
{
return (-1);
}
if (HAL_AES_RegisterErrorCallback(pAES, ErrorCallback) != HAL_OK)
{
return (-1);
}
printf("[INFO] Callbacks registration COMPLETED.\n");
Launch the encryption operation.
if (HAL_AES_Encrypt_DMA(pAES, plainText, 51, computedCiphertext) != HAL_OK)
{
return (-1);
}
Wait until the encryption transfer completes or an error occurs.
if (WaitForAesDmaCompletion() != 0)
{
return (-1);
}
Mask the padded byte in the last word and compare the output with the expected ciphertext.
computedCiphertext[12] &= 0xFFFFFF00U;
if (memcmp(computedCiphertext, expectedCipherText, 51) != 0)
{
return HAL_ERROR;
}
Request the GCM authentication tag and compare it with the reference value.
if (HAL_AES_GCM_GenerateAuthTAG(pAES, Tag, AES_TIMEOUT_MS) != HAL_OK)
{
return (-1);
}
if (memcmp(Tag, expectedTag, 16) != 0)
{
return (-1);
}
printf("[INFO] AES-GCM encryption DMA test passed.\n");
Before starting the decryption phase, explicitly configure the AES peripheral again.
p_gcm_config.p_init_vect = (uint32_t *) InitVector;
p_gcm_config.p_header = HeaderMessage;
p_gcm_config.header_size_byte = 48;
HAL_AES_GCM_GMAC_SetConfig(pAES, &p_gcm_config);
HAL_AES_SetNormalKey(pAES, HAL_AES_KEY_SIZE_256BIT, AESKey);
HAL_AES_SetDataSwapping(pAES, HAL_AES_DATA_SWAPPING_NO);
Launch the decryption operation using the previously generated ciphertext.
if (HAL_AES_Decrypt_DMA(pAES, computedCiphertext, 51, computedPlaintext) != HAL_OK)
{
return (-1);
}
Wait for the DMA-based decryption flow to finish.
if (WaitForAesDmaCompletion() != 0)
{
return (-1);
}
Mask the padded byte and compare the result with the original plaintext.
computedPlaintext[12] &= 0xFFFFFF00U;
if (memcmp(computedPlaintext, plainText, 51) != 0)
{
return (-1);
}
Generate the tag after decryption and compare it against the expected reference.
if (HAL_AES_GCM_GenerateAuthTAG(pAES, Tag, AES_TIMEOUT_MS) != HAL_OK)
{
return -1;
}
if (memcmp(Tag, expectedTag, 16) != 0)
{
return -1;
}
printf("[INFO] AES-GCM decryption DMA test passed.\n");
Erase the temporary cryptographic buffers before returning from the application.
memset(computedCiphertext, 0, sizeof(computedCiphertext));
memset(computedPlaintext, 0, sizeof(computedPlaintext));
memset(Tag, 0, sizeof(Tag));
printf("[INFO] Example completed successfully.\n");
return (0);
Implement the helper function used to wait for completion or error.
static int WaitForAesDmaCompletion(void)
{
OutTransferCpltCb = 0;
ErrorCb = 0;
while ((OutTransferCpltCb == 0U) && (ErrorCb == 0U))
{
__WFI();
}
return (ErrorCb != 0U) ? -1 : 0;
}
Implement the callback triggered when the AES DMA transfer completes.
static void OutTransfertCpltCallback(hal_aes_handle_t *pAES)
{
(void)pAES;
OutTransferCpltCb = 1U;
}
Implement the callback triggered if the AES peripheral reports an error.
static void ErrorCallback(hal_aes_handle_t *pAES)
{
(void)pAES;
ErrorCb = 1U;
}
After completing the source code integration, build the project and program the target board.
When the example runs successfully, the console should print messages similar to the following:
[INFO] AES, DMA initialization and configuration to work with GCM DMA mode COMPLETED.
[INFO] Callbacks registration COMPLETED.
[INFO] AES-GCM encryption DMA test passed.
[INFO] AES-GCM decryption DMA test passed.
[INFO] Example completed successfully.
This article demonstrated how to configure and run a minimal AES-GCM DMA example on STM32C5 using STM32CubeMX2, from project setup to runtime validation. By following the described steps, you can verify both encryption and decryption against known reference data and use this project as a starting point for more advanced secure data-processing applications.