What follows is a very basic framework for button debouncing. It works on an STM32 and is demonstrated in the following code on an STM32F446RE Nucleo board running at the default 84MHz. It uses timer #2 (TIM2), but could be modified to use another timer. The code was written primarily for the Arduino IDE and STM32duino core, however it could easily be adapted for other environments.
This demo simply uses the builtin Nucleo button, but additional buttons can be added by expanding the buttons array. The comments in the code should be adequate to explain how it works.
This Arduino IDE code requires the HAL TIM module only, so add a “build_opt.h” file with the following definition:
-DHAL_TIM_MODULE_ONLY
The basic code is included in this demonstration file:
#include <cassert>
#include "clamp.h"
#ifdef __cplusplus
extern "C"
#endif
// Number of buttons used.
constexpr size_t NUM_BUTTONS{1};
// Define our button (built in).
#define USER_BUTTON PC13
#define BLUE_BUTTON 0
// Debounce specifics (modify per system clock MHz).
#define DEBOUNCE_THESHOLD 7
#define COUNT_MIN 0
#define COUNT_MAX 15
// Specifics for each defined button.
typedef struct Button
{
GPIO_TypeDef* port = nullptr;
uint16_t pin = NULL;
bool state = false;
bool latch = false;
uint8_t count = 0;
} button_t;
// Array of buttons.
button_t buttons[NUM_BUTTONS];
// Debounce functionality called by timer interrupt.
void debounce()
{
assert(NUM_BUTTONS != 0);
// Iterate through all defined buttons.
for (int i=0; i<NUM_BUTTONS; i++)
{
// Check for incomplete button defintion.
if (buttons[i].port == nullptr)
continue;
// Read button pin, adjust count.
if (buttons[i].count != COUNT_MAX && !HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin))
buttons[i].count++;
else if (buttons[i].count)
buttons[i].count--;
// For the paranoid, force min/max value on count.
//buttons[i].count = clamp<uint8_t>(buttons[i].count, COUNT_MIN, COUNT_MAX);
// Debounce upon count > threshold value.
if (buttons[i].count > DEBOUNCE_THESHOLD)
buttons[i].state = true;
else
buttons[i].state = false;
}
}
TIM_HandleTypeDef timerInstance;
extern "C" void TIM2_IRQHandler() { HAL_TIM_IRQHandler(&timerInstance); }
extern "C" void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { debounce(); }
void initTimer()
{
// TIM2 by default has clock of 84MHz (MCU freq).
// Set value of prescaler and period, so update event is every 1ms:
// Update Event (Hz) = timer_clock/((TIM_Prescaler + 1)*(TIM_Period + 1))
// Update Event (Hz) = 84MHz / ((0 + 1) * (83999 + 1)) = 1ms.
__TIM2_CLK_ENABLE();
timerInstance.Instance = TIM2;
timerInstance.Init.Period = 83999;
timerInstance.Init.Prescaler = 0;
timerInstance.Init.CounterMode = TIM_COUNTERMODE_UP;
timerInstance.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
timerInstance.Init.RepetitionCounter = 0;
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
HAL_TIM_Base_Init(&timerInstance);
HAL_TIM_Base_Start_IT(&timerInstance);
}
void initButtons()
{
// Define button port/pin and set mode of pin.
buttons[BLUE_BUTTON].port = GPIOC;
buttons[BLUE_BUTTON].pin = GPIO_PIN_13;
pinMode(USER_BUTTON, INPUT);
}
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
initButtons();
initTimer();
}
void loop()
{
// Detect a new button press.
if (buttons[BLUE_BUTTON].state && !buttons[BLUE_BUTTON].latch)
{
buttons[BLUE_BUTTON].latch = true;
// Perform action on button press.
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
// Detect button release.
if (!buttons[BLUE_BUTTON].state && buttons[BLUE_BUTTON].latch)
buttons[BLUE_BUTTON].latch = false;
}
Contents of “clamp.h” file:
#include <algorithm>
template <class T>
inline const T& _max(const T& a, const T& b) { return (a < b) ? b : a; }
template <class T>
inline const T& _min(const T& a, const T& b) { return !(b < a) ? a : b; }
template <typename T>
inline T clamp(T& val, T lo, T hi) { return _max(lo, _min(hi, val)); }
This is a refactoring of the existing Arduino (avr) C string library. This was simply an academic exercise. This code should compile to the same size, since in most respects it is the same code. The majority of the comments were just copied from the original source code. Note, that the function names have an underscore appended to them. Beware, some of the functions have not been tested. Therefore, please check functionality before using any of this code.
This program demonstrates the RTC timestamp feature of the STM32F4 family of MCUs. The timestamp is activated by one of two external pins (PA0 or PC13 on the STM32F446). Since the PC13 pin is connected to the user button on the Nucleo board, we use it in this demonstration to activate the timestamp. The timestamp feature can be used in either polling or interrupt mode. This demonstration utilizes the interrupt and callback. The ISR in this code simply flags a timestamp overflow by turning on the LED.
The code should be self explanatory. This code is compiled inside the Arduino IDE using the official STM 2.0 Core.
// RTC timestamp demo.
// 1. timestamp occurs when user button is pressed.
// 2. include a sketch level "build_opt.h" file with "-DHAL_RTC_MODULE_ONLY"
// LSI
#define RTC_ASYNCH_PREDIV 0x7F
#define RTC_SYNCH_PREDIV 0x00F9
// LSE
//#define RTC_ASYNCH_PREDIV 0x7F
//#define RTC_SYNCH_PREDIV 0x00FF
RTC_HandleTypeDef RtcHandle;
RTC_TimeTypeDef sTimeStamp;
RTC_DateTypeDef sDateStamp;
volatile bool stamp {false};
static void RTC_TimeStampConfig(void)
{
RTC_DateTypeDef sDate;
RTC_TimeTypeDef sTime;
// RTC TimeStamp generation: TimeStamp Rising Edge on PC13 Pin
HAL_RTCEx_SetTimeStamp_IT(&RtcHandle, RTC_TIMESTAMPEDGE_RISING, RTC_TIMESTAMPPIN_PC13);
// Set a date & time.
sTime.Hours = 0x1;
sTime.Minutes = 0x2;
sTime.Seconds = 0x3;
sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
sTime.StoreOperation = RTC_STOREOPERATION_RESET;
if (HAL_RTC_SetTime(&RtcHandle, &sTime, RTC_FORMAT_BCD) != HAL_OK)
while (1);
sDate.WeekDay = RTC_WEEKDAY_MONDAY;
sDate.Month = RTC_MONTH_SEPTEMBER;
sDate.Date = 0x14;
sDate.Year = 0x0;
if (HAL_RTC_SetDate(&RtcHandle, &sDate, RTC_FORMAT_BCD) != HAL_OK)
while (1);
}
void RTC_Init(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct;
RCC_PeriphCLKInitTypeDef RCC_RTCPeriClkInit;
// 1. Turn on the LSI
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI;
RCC_OscInitStruct.LSEState = RCC_LSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
while (1);
// 2. Select LSI as RTCCLK
RCC_RTCPeriClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
RCC_RTCPeriClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
if (HAL_RCCEx_PeriphCLKConfig(&RCC_RTCPeriClkInit) != HAL_OK)
while (1);
__HAL_RCC_RTC_ENABLE();
// Configure the NVIC for RTC TimeStamp.
HAL_NVIC_SetPriority(TAMP_STAMP_IRQn, 0x0F, 0);
HAL_NVIC_EnableIRQ(TAMP_STAMP_IRQn);
// Configure RTC prescaler and RTC data registers
// - Hour Format = Format 12
// - Asynch Prediv = Value according to source clock
// - Synch Prediv = Value according to source clock
// - OutPut = Output Disable
// - OutPutPolarity = High Polarity
// - OutPutType = Open Drain
__HAL_RTC_RESET_HANDLE_STATE(&RtcHandle);
RtcHandle.Instance = RTC;
RtcHandle.Init.HourFormat = RTC_HOURFORMAT_12;
RtcHandle.Init.AsynchPrediv = RTC_ASYNCH_PREDIV;
RtcHandle.Init.SynchPrediv = RTC_SYNCH_PREDIV;
RtcHandle.Init.OutPut = RTC_OUTPUT_DISABLE;
RtcHandle.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
RtcHandle.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
__HAL_RTC_RESET_HANDLE_STATE(&RtcHandle);
if (HAL_RTC_Init(&RtcHandle) != HAL_OK)
while (1);
RTC_TimeStampConfig();
}
// EXTI line detection callback.
void buttonISR() { stamp = true; }
extern "C" void HAL_RTCEx_TimeStampEventCallback(RTC_HandleTypeDef *hrtc)
{
HAL_RTCEx_GetTimeStamp(&RtcHandle, &sTimeStamp, &sDateStamp, RTC_FORMAT_BIN);
}
// This function handles RTC tamper and timestamp interrupts through EXTI line 21.
extern "C" void TAMP_STAMP_IRQHandler(void)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
if (RTC->ISR & RTC_FLAG_TSOVF)
{
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
CLEAR_BIT(RTC->ISR, RTC_FLAG_TSOVF);
}
HAL_RTCEx_TamperTimeStampIRQHandler(&RtcHandle);
}
void setup()
{
Serial.begin(9600);
while(!Serial);
RTC_Init();
pinMode(LED_BUILTIN, OUTPUT);
attachInterrupt(digitalPinToInterrupt(PC13), buttonISR, RISING);
Serial.println("RTC Timestamp Demo");
}
void loop()
{
RTC_TimeTypeDef sTime;
RTC_DateTypeDef sDate;
char s[16] = { 0 };
HAL_RTC_GetTime(&RtcHandle, &sTime, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&RtcHandle, &sDate, RTC_FORMAT_BIN);
sprintf(s, "%02d:%02d", sTime.Minutes, sTime.Seconds);
Serial.print(s);
if (stamp)
{
sprintf(s, " [%02d:%02d]", sTimeStamp.Minutes, sTimeStamp.Seconds);
Serial.println(s);
stamp = false;
}
else
Serial.println();
HAL_Delay(1000);
}
Demonstrates UART/DMA Rx, FreeRTOS, specific sentence selection and SD Card logging.
Compiled with STMicroelectronics core using the Arduino IDE. Note, all references to “Serial” in the STM32 SD library SD.cpp file were commented out since this program is not using the builtin serial functionality. Program listing below:
// Adafruit STM32F405 Feather GPS Logger.
// UART DMA RX undetermined length data demonstration.
// build_opt.h: -DHAL_UART_MODULE_ENABLED -DHAL_UART_MODULE_ONLY
// USART: disabled, USB support: none.
//
// Adaptation of technique described here:
// https://stm32f4-discovery.net/2017/07/stm32-tutorial-efficiently-receive-uart-data-using-dma/
// https://community.st.com/s/global-search/efficiently%20use%20dma%20with%20uart%20rx%20stm32
//
// Comment out all reference to "Serial" inside STM32SD library file SD.cpp file.
//
// Use TeraTerm to connect to board at 9600 baud to monitor.
// USART1 RX is on SDA pin, PB7 (GPS).
// USART1 TX is on SCL pin, PB6 (PC).
// SD detect pin is on PB12 (D32).
#include <STM32FreeRTOS.h>
#include "stm32f4xx.h"
#include "stm32f4xx_hal.h"
#include "stm32f4xx_ll_dma.h"
#include "stm32f4xx_ll_usart.h"
#include "ctype.h"
#include <atomic>
// All reference to Serial commented out in STM32SD.h/SD.cpp files of STM32SD library.
#include <STM32SD.h>
#define BUFFER_SIZE 128
// UART and DMA handles.
UART_HandleTypeDef hUART1;
DMA_HandleTypeDef hDMA2;
// UART DMA data buffer.
uint8_t dmaBuffer[BUFFER_SIZE];
// GPS sentence buffer.
uint8_t sentenceBuffer[BUFFER_SIZE];
// Overkill? Because ARM Architecture Reference Manual says any aligned 32-bit memory access is atomic.
// https://developer.arm.com/documentation/ddi0403/eb/
std::atomic<uint32_t> count(0);
//uint32_t volatile __attribute__((aligned(4))) count{0};
// File object refers to gps text file.
File file;
void errorHandler(uint32_t d)
{
while(1)
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_1);
HAL_Delay(d);
}
}
void UART_DMA_Init(void)
{
// USART1 init.
__HAL_RCC_USART1_CLK_ENABLE();
hUART1.Instance = USART1;
hUART1.Init.BaudRate = 9600;
hUART1.Init.WordLength = UART_WORDLENGTH_8B;
hUART1.Init.StopBits = UART_STOPBITS_1;
hUART1.Init.Parity = UART_PARITY_NONE;
hUART1.Init.Mode = UART_MODE_TX_RX;
hUART1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
hUART1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&hUART1) != HAL_OK)
errorHandler(1000);
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
LL_USART_EnableIT_IDLE(USART1);
// DMA2 USART1 RX init.
__HAL_RCC_DMA2_CLK_ENABLE();
hDMA2.Instance = DMA2_Stream2;
hDMA2.Init.Channel = DMA_CHANNEL_4;
hDMA2.Init.Direction = DMA_PERIPH_TO_MEMORY;
hDMA2.Init.PeriphInc = DMA_PINC_DISABLE;
hDMA2.Init.MemInc = DMA_MINC_ENABLE;
hDMA2.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hDMA2.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hDMA2.Init.Mode = DMA_CIRCULAR;
hDMA2.Init.Priority = DMA_PRIORITY_VERY_HIGH;
hDMA2.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hDMA2) != HAL_OK)
errorHandler(1000);
__HAL_LINKDMA(&hUART1, hdmarx, hDMA2);
LL_DMA_SetPeriphAddress(DMA2, LL_DMA_STREAM_2, (uint32_t)&USART1->DR);
LL_DMA_SetMemoryAddress(DMA2, LL_DMA_STREAM_2, (uint32_t)dmaBuffer);
LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_2, (uint32_t)BUFFER_SIZE);
LL_DMA_EnableIT_HT(DMA2, LL_DMA_STREAM_2);
LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_2);
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_2);
LL_USART_EnableDMAReq_RX(USART1);
}
void GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// USART1 GPIOs: PB6->TX, PB7->RX
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// LED GPIO pin.
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// Neopixel LED GPIO pin reset.
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
}
// Print RMC sentences.
void printRMCSentence(const size_t len)
{
// Null terminate gps sentence.
sentenceBuffer[len] = 0;
if (strstr((const char *)sentenceBuffer, "$GPRMC") != NULL)
{
for (uint8_t i=0; i<len; i++)
{
LL_USART_TransmitData8(USART1, sentenceBuffer[i]);
while (!LL_USART_IsActiveFlag_TXE(USART1));
}
// cr/lf.
LL_USART_TransmitData8(USART1, '\r');
while (!LL_USART_IsActiveFlag_TXE(USART1));
LL_USART_TransmitData8(USART1, '\n');
while (!LL_USART_IsActiveFlag_TXE(USART1));
// Wait until xfer complete.
while (!LL_USART_IsActiveFlag_TC(USART1));
// Open file, write data then close file.
file = SD.open("gps.txt", FILE_WRITE);
if (file)
{
file.print((const char *)sentenceBuffer);
file.close();
}
}
}
void constructGPSSentence(const void* data, size_t len)
{
static size_t index = 0;
const uint8_t* d = (const uint8_t*)data;
while (len--)
{
if (*d == '\n')
printRMCSentence(index);
if (*d == '$' || index >= BUFFER_SIZE)
index = 0;
// Eat cr/lf (0x0d/0x0a).
if (*d == '\n' || *d == '\r')
d++;
else
sentenceBuffer[index++] = *d++;
}
}
// Extract data from circular buffer.
void checkData(void)
{
static size_t prevPos;
size_t curPos;
// Calculate current position in buffer.
curPos = BUFFER_SIZE - LL_DMA_GetDataLength(DMA2, LL_DMA_STREAM_2);
// Check for change in received data.
if (curPos != prevPos)
{
if (curPos > prevPos)
{
// Process data directly by subtracting pointers.
constructGPSSentence(&dmaBuffer[prevPos], curPos - prevPos);
}
else
{
// Buffer overflow occurred, first, process data to the end of buffer.
constructGPSSentence(&dmaBuffer[prevPos], BUFFER_SIZE - prevPos);
// Check and continue with beginning of buffer.
if (curPos)
constructGPSSentence(&dmaBuffer[0], curPos);
}
}
// Save current position.
prevPos = curPos;
// Check and manually update if at end of buffer.
if (prevPos == BUFFER_SIZE)
prevPos = 0;
}
// Rx dma stream interrupt check for HT/TC interrupts.
extern "C" void DMA2_Stream2_IRQHandler(void)
{
if (LL_DMA_IsEnabledIT_HT(DMA2, LL_DMA_STREAM_2) && LL_DMA_IsActiveFlag_HT2(DMA2))
{
// Clear half-transfer complete flag.
LL_DMA_ClearFlag_HT2(DMA2);
// Flag data needs processing.
std::atomic_fetch_add(&count, 1);
}
if (LL_DMA_IsEnabledIT_TC(DMA2, LL_DMA_STREAM_2) && LL_DMA_IsActiveFlag_TC2(DMA2))
{
// Clear half-transfer complete flag.
LL_DMA_ClearFlag_TC2(DMA2);
// Flag data needs processing.
std::atomic_fetch_add(&count, 1);
}
}
// USART1 global interrupt check IDLE line.
extern "C" void USART1_IRQHandler(void)
{
if (LL_USART_IsEnabledIT_IDLE(USART1) && LL_USART_IsActiveFlag_IDLE(USART1))
{
// Clear IDLE line flag.
LL_USART_ClearFlag_IDLE(USART1);
// Flag data needs processing.
std::atomic_fetch_add(&count, 1);
}
}
void checkTask(void *arg)
{
UNUSED(arg);
UART_DMA_Init();
__HAL_UART_FLUSH_DRREGISTER(&hUART1);
HAL_UART_Transmit(&hUART1, (uint8_t *)"USART/DMA FreeRTOS GPS Demo\n\r", 29, 100);
while (1)
{
if (std::atomic_load(&count))
{
std::atomic_fetch_sub(&count, 1);
checkData();
}
}
}
void setup()
{
GPIO_Init();
if (!SD.begin())
errorHandler(100);
xTaskCreate(checkTask, NULL, configMINIMAL_STACK_SIZE, NULL, 1, NULL);
vTaskStartScheduler();
}
void loop() { }
For applications where you don’t know the length of a data stream prior to receiving it via DMA UART. Uses Arduino IDE with STMicroelectronics 2.0 core.
Here is a picture of the Adafruit STM32F405 Feather connected to a GPS receiver and using the following code to simply repeat the GPS sentences to a terminal program running on a PC.
Terminal output:
// Adafruit STM32F405 Feather
// UART DMA RX undetermined length data demonstration.
//
// Adaptation of technique described here:
// https://stm32f4-discovery.net/2017/07/stm32-tutorial-efficiently-receive-uart-data-using-dma/
// https://community.st.com/s/global-search/efficiently%20use%20dma%20with%20uart%20rx%20stm32
//
// Use TeraTerm to connect to board at 9600 baud.
// USART1 RX is on SDA pin, PB7.
// USART1 TX is on SCL pin, PB6.
// Observe chars are repeated with case change.
//
#include "stm32f4xx.h"
#include "stm32f4xx_hal.h"
#include "stm32f4xx_ll_dma.h"
#include "stm32f4xx_ll_usart.h"
#include "ctype.h"
// UART and DMA handles.
UART_HandleTypeDef hUART1;
DMA_HandleTypeDef hDMA2;
// Buffer of unprocessed UART DMA data.
static uint8_t dmaBuffer[16];
#define BUFFER_SIZE ( sizeof(dmaBuffer) / sizeof(dmaBuffer[0]) )
void errorHandler(void) {
while(1) {
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_1);
HAL_Delay(250);
}
}
void UART_DMA_Init(void)
{
// USART1 init.
__HAL_RCC_USART1_CLK_ENABLE();
hUART1.Instance = USART1;
hUART1.Init.BaudRate = 9600;
hUART1.Init.WordLength = UART_WORDLENGTH_8B;
hUART1.Init.StopBits = UART_STOPBITS_1;
hUART1.Init.Parity = UART_PARITY_NONE;
hUART1.Init.Mode = UART_MODE_TX_RX;
hUART1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
hUART1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&hUART1) != HAL_OK)
errorHandler();
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
LL_USART_EnableIT_IDLE(USART1);
// DMA2 USART1 RX init.
__HAL_RCC_DMA2_CLK_ENABLE();
hDMA2.Instance = DMA2_Stream2;
hDMA2.Init.Channel = DMA_CHANNEL_4;
hDMA2.Init.Direction = DMA_PERIPH_TO_MEMORY;
hDMA2.Init.PeriphInc = DMA_PINC_DISABLE;
hDMA2.Init.MemInc = DMA_MINC_ENABLE;
hDMA2.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hDMA2.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hDMA2.Init.Mode = DMA_CIRCULAR;
hDMA2.Init.Priority = DMA_PRIORITY_VERY_HIGH;
hDMA2.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hDMA2) != HAL_OK)
errorHandler();
__HAL_LINKDMA(&hUART1, hdmarx, hDMA2);
LL_DMA_SetPeriphAddress(DMA2, LL_DMA_STREAM_2, (uint32_t)&USART1->DR);
LL_DMA_SetMemoryAddress(DMA2, LL_DMA_STREAM_2, (uint32_t)dmaBuffer);
LL_DMA_SetDataLength(DMA2, LL_DMA_STREAM_2, (uint32_t)BUFFER_SIZE);
LL_DMA_EnableIT_HT(DMA2, LL_DMA_STREAM_2);
LL_DMA_EnableIT_TC(DMA2, LL_DMA_STREAM_2);
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
LL_DMA_EnableStream(DMA2, LL_DMA_STREAM_2);
LL_USART_EnableDMAReq_RX(USART1);
}
void GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
// USART1 GPIOs: PB6->TX, PB7->RX
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// LED GPIO pin.
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET);
GPIO_InitStruct.Pin = GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
// Neopixel LED GPIO pin reset.
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET);
}
// Process received data.
void processData(const void* data, size_t len)
{
const uint8_t* d = (const uint8_t*)data;
while (len--)
{
LL_USART_TransmitData8(USART1, tolower(*d++));
while (!LL_USART_IsActiveFlag_TXE(USART1));
}
while (!LL_USART_IsActiveFlag_TC(USART1));
}
// Check for new data received via DMA.
void checkData(void)
{
static size_t prevPos;
size_t curPos;
// Calculate current position in buffer.
curPos = BUFFER_SIZE - LL_DMA_GetDataLength(DMA2, LL_DMA_STREAM_2);
// Check for change in received data.
if (curPos != prevPos)
{
if (curPos > prevPos)
{
// Process data directly by subtracting pointers.
processData(&dmaBuffer[prevPos], curPos - prevPos);
}
else
{
// Buffer overflow occurred, first, process data to the end of buffer.
processData(&dmaBuffer[prevPos], BUFFER_SIZE - prevPos);
// Check and continue with beginning of buffer.
if (curPos)
processData(&dmaBuffer[0], curPos);
}
}
// Save current position.
prevPos = curPos;
// Check and manually update if at end of buffer.
if (prevPos == BUFFER_SIZE)
prevPos = 0;
}
// Rx dma stream interrupt check for HT/TC interrupts.
extern "C" void DMA2_Stream2_IRQHandler(void)
{
if (LL_DMA_IsEnabledIT_HT(DMA2, LL_DMA_STREAM_2) && LL_DMA_IsActiveFlag_HT2(DMA2))
{
// Clear half-transfer complete flag.
LL_DMA_ClearFlag_HT2(DMA2);
// Check for data to process.
checkData();
}
if (LL_DMA_IsEnabledIT_TC(DMA2, LL_DMA_STREAM_2) && LL_DMA_IsActiveFlag_TC2(DMA2))
{
// Clear half-transfer complete flag.
LL_DMA_ClearFlag_TC2(DMA2);
// Check for data to process.
checkData();
}
}
// USART1 global interrupt checks IDLE line.
extern "C" void USART1_IRQHandler(void)
{
if (LL_USART_IsEnabledIT_IDLE(USART1) && LL_USART_IsActiveFlag_IDLE(USART1))
{
// Clear IDLE line flag.
LL_USART_ClearFlag_IDLE(USART1);
// Check for data to process.
checkData();
}
}
void setup()
{
GPIO_Init();
UART_DMA_Init();
__HAL_UART_FLUSH_DRREGISTER(&hUART1);
HAL_UART_Transmit(&hUART1, (uint8_t *)"USART/DMA Demo\r\n", 16, 100);
}
void loop() { }
Code updated 7.17.21 to remove error, and correct attribution.
In the final post on STM32 DMA programming in the Arduino IDE, we will examine register-level setup of the peripherals. The demo program here is a simple refactoring of a previous program that utilizes DMA M2P via the UART.
Bare-Metal Programming
Register-level coding is sometimes referred to as scratch or bare-metal-programming. It involves foregoing the use of the HAL and SPL high-level library functions and directly reading and writing to the peripheral control registers. Typically, this type of programming will require frequent consultation with both the microcontroller datasheet and reference manual. At the end of each chapter in the reference manual is a section entitled Registers, that is particularly helpful.
Once compiled and installed on the STM32F405 Feather board, this program simply sends “Hello World” through UART1 via the DMA2 M2P mode every time the button is pressed. The button must be connected to the PB13, which is the pin labelled “SCK”.
Note, their are additional file and IDE setting requirements outlined in the comment section of the code.
To fully understand the intricacies of this code you must complete the uDemy course, ARM Cortex M Microcontroller DMA Programming Demystified. All I did was adapt the example code supplied with the course for the Adafruit STM32F405 Feather board and the Arduino IDE. Please note, I am not affiliated with this course or the uDemy website in any manner. I simply found this course to be very helpful when I was learning DMA programming on the STM32 series of microcontrollers.
The Code
// Register level application code.
// Memory to Peripheral (m2p) DMA Transfer.
// SRAM1 --> USART1_TX
//
// Adafruit STM32F405 Feather.
// Modified STM32F446RE Nucleo Board code.
//
// Button (PB13, or SCK) with external interrupt.
// Button connected to ground.
//
// Add build_opt.h file with -DHAL_UART_MODULE_ENABLED
// Tools > U(S)ART Support: Disabled.
// Add hal_conf_extra.h file with #define HAL_EXTI_MODULE_DISABLED
#define isHT() DMA2->HISR & (1 << 26)
#define isFT() DMA2->HISR & (1 << 27)
#define isTE() DMA2->HISR & (1 << 25)
#define isFE() DMA2->HISR & (1 << 22)
#define isDME() DMA2->HISR & (1 << 24)
char data[] ="Hello World\r\n";
// Test uart peripheral.
void sendSomeData(void) {
USART_TypeDef *pUART1 = USART1;
char someData[] = "Hello World\r\n";
// Make sure that status register TXE is set (TDR is empty).
// If TXE is 1, put a byte in DR.
uint32_t len = sizeof(someData);
for (uint32_t i=0 ; i<len ; i++) {
// Waiting for TXE to become 1.
while( !(pUART1->SR & (1 << 7)) ) ;
pUART1->DR = someData[i];
}
}
// Clears pending interrupt bit.
void clearExtiPendingBit(void) {
EXTI_TypeDef *pEXTI;
pEXTI = EXTI;
if ((EXTI->PR & (1 << 13)))
EXTI->PR |= (1 << 13);
}
// IRQ handler for the button interrupt.
extern "C" void EXTI15_10_IRQHandler(void)
{
USART_TypeDef *pUART1 = USART1;
// Send UART1_TX DMA request to DMA2 controller.
pUART1->CR3 |= (1 << 7);
clearExtiPendingBit();
}
void dma2EnableStream7(void) {
DMA_Stream_TypeDef *pSTREAM7 = DMA2_Stream7;
// Enable stream.
pSTREAM7->CR |= (1 << 0);
}
void callbackErrorTE(void) { while(1); }
void callabckErrorFE(void) { while(1); }
void callbackErrorDME(void) { while(1); }
void callbackHTComplete(void) { /* Do nothing. */ }
void callabckFTComplete(void) {
USART_TypeDef *pUART1 = USART1;
DMA_Stream_TypeDef *pSTREAM7 = DMA2_Stream7;
// Program number of data items to send.
uint32_t len = sizeof(data);
pSTREAM7->NDTR = len;
pUART1->CR3 &= ~(1 << 7);
dma2EnableStream7();
}
// IRQ handler for DMA2 stream7 global interrupt.
extern "C" void DMA2_Stream7_IRQHandler(void) {
if (isHT()) {
// Half transfer.
DMA2->HIFCR |= (1 << 26);
callbackHTComplete();
} else if (isFT()) {
// Full transfer.
DMA2->HIFCR |= (1 << 27);
callbackFTComplete();
} else if (isTE()) {
// Transfer error.
DMA2->HIFCR |= (1 << 25);
callbackErrorTE();
} else if (isFE()) {
// FIFO error.
DMA2->HIFCR |= (1 << 22);
callbackErrorFE();
} else if (isDME()) {
// Direct mode error.
DMA2->HIFCR |= (1 << 24);
callbackErrorDME();
} else
;
}
void dma2InterruptConfig(void) {
DMA_Stream_TypeDef *pSTREAM7 = DMA2_Stream7;
// 1. Half-transfer IE (HTIE).
pSTREAM7->CR |= (1 << 3);
// 2. Transfer complete IE (TCIE).
pSTREAM7->CR |= (1 << 4);
// 3. Transfer error IE (TEIE).
pSTREAM7->CR |= (1 << 2);
// 4. FIFO overrun/underrun IE (FEIE).
pSTREAM7->FCR |= (1 << 7);
// 5. Direct mode error (DMEIE).
pSTREAM7->CR |= (1 << 1);
// 6. Enable IRQ for DMA2 stream 7 global interrupt in NVIC.
NVIC_EnableIRQ(DMA2_Stream7_IRQn);
}
void dma2Init(void) {
RCC_TypeDef *pRCC = RCC;
DMA_Stream_TypeDef *pSTREAM7 = DMA2_Stream7;
USART_TypeDef *pUART1 = USART1;
// 1. enable the peripheral clock for the dma2.
pRCC->AHB1ENR |= (1 << 22);
// 2. Identify the stream which is suitable for your peripheral.
// <channel 4 , stream 7>.
// 3. Identify the channel number on which uart1 peripheral sends DMA request.
// <channel 4>.
pSTREAM7->CR &= ~(0x7 << 25);
pSTREAM7->CR |= (0x4 << 25);
// 4. Program the source address (memory).
pSTREAM7->M0AR = (uint32_t)data;
// 5. Program the destination address (peripheral).
pSTREAM7->PAR = (uint32_t)&pUART1->DR;
// 6. Program number of data items to send.
uint32_t len = sizeof(data);
pSTREAM7->NDTR = len;
// 7. The direction of data transfer. m2p, p2m, m2m.
pSTREAM7->CR |= (0x1 << 6);
// 8. Program the source and destination data width.
pSTREAM7->CR &= ~(0x3 << 13);
pSTREAM7->CR &= ~(0x3 << 11);
// 8a. Enable memory auto increment.
pSTREAM7->CR |= (1 << 10);
// 9. Direct mode or fifo mode.
pSTREAM7->FCR |= (1 << 2);
// 10. Select the fifo threshold.
pSTREAM7->FCR &= ~(0x3 << 0); // Clearing
pSTREAM7->FCR |= (0x3 << 0); // Setting
// 11. Enable the circular mode if required.
// 12. Single transfer or burst transfer.
// 13. Configure the stream priority.
}
// USART1 configuration (PB6-->USART1_TX, PB7-->USART1_RX)
void uart1Init(void) {
RCC_TypeDef *pRCC = RCC;
GPIO_TypeDef *pGPIOB = GPIOB;
USART_TypeDef *pUART1 = USART1;
// 1. Enable the peripheral clock for the UART1 peripheral.
pRCC->APB2ENR |= (1 << 4);
// 2. Configure the GPIO pins for uart_tx and uart_rx functionality.
// Configure PB6 as TX.
// 2.1 Enable the clock for the GPIOB peripheral.
pRCC->AHB1ENR |= (1 << 1);
// 2.2 Change the mode of PB6 to alternate function.
pGPIOB->MODER &= ~(0x3 << 12);
pGPIOB->MODER |= (0x2 << 12);
pGPIOB->AFR[0] &= ~(0xF << 24);
pGPIOB->AFR[0] |= (0x7 << 24);
// 2.3 Enable or disable Pull-up resistor for PB6.
pGPIOB->PUPDR |= (0x1 << 12);
// Configure PB7 as UART2 RX.
// 2.4 Change the mode of the PB7 to alternate function.
pGPIOB->MODER &= ~(0x3 << 14);
pGPIOB->MODER |= (0x2 << 14);
pGPIOB->AFR[0] &= ~(0xF << 28);
pGPIOB->AFR[0] |= (0x7 << 28);
// 2.5 Enable or disable Pull-up resistor for PB7.
pGPIOB->PUPDR |= (0x1 << 14);
// 3. Configure baudrate.
pUART1->BRR = 0x222e; // 9600@84MHz (OVER8=0), Table 142, 546.875 (546=0x222, shift left 1=0x2220 + (0.875*16=0xE) 0x222E).
// 4. Configure data width, # of stop bits, etc.
// <no configuration required here, use default values>
// 5. Enable TX for the UART peripheral.
pUART1->CR1 |= (1 << 3);
// 6. Enable UART peripheral.
pUART1->CR1 |= (1 << 13);
}
void buttonInit(void)
{
// Button is connected to PB13.
GPIO_TypeDef *pGPIOB = GPIOB;
RCC_TypeDef *pRCC = RCC;
EXTI_TypeDef *pEXTI = EXTI;
SYSCFG_TypeDef *pSYSCFG = SYSCFG;
// 1. Enable the peripheral clock for the GPIOB peripheral.
pRCC->AHB1ENR |= (1 << 1);
// 2. Keep GPIO pin in input mode.
pGPIOB->MODER &= ~(0x3 << 26);
// 2.1 Enable pull-up resistor for PB13.
pGPIOB->PUPDR |= (0x1 << 26);
// 3. Enable the interrupt for the GPIO pin.
pEXTI->IMR |= (1 << 13);
// 4. Enable the clock for SYSCFG.
pRCC->APB2ENR |= (1 << 14);
// 5. Configuring the SYSCFG CR4 register.
pSYSCFG->EXTICR[3] &= ~(0xF << 4); // Clearing
pSYSCFG->EXTICR[3] |= (0x1 << 4); // Set
// 6. Configure edge detection for the EXTI 13 line.
pEXTI->FTSR |= (1 << 13);
// 7. Enable the IRQ related to the GPIO pin in NVIC register of processor.
NVIC_EnableIRQ(EXTI15_10_IRQn);
}
void setup() {
buttonInit();
uart1Init();
//sendSomeData();
dma2Init();
dma2InterruptConfig();
dma2EnableStream7();
}
void loop() { }
This is the fourth example of DMA usage on the STM32F405 Feather board. Programming is from the Arduino IDE. This example demonstrates the peripheral to memory (P2M) DMA Mode. We send data from the ADC peripheral to memory via DMA.
This code sends the internal temperature of the MCU to the serial monitor every second.
This is the third example of DMA usage on the STM32F405 Feather board. Programming is via the Arduino IDE. This example demonstrates the memory to peripheral (M2P) DMA Mode. We send data from memory to the UART via DMA.
For this demo to work, the Arduino serial code needs to be disabled. The instructions are included in the code comments. Also, you will note some code inside the main loop function. The purpose of this code is to reset the transmit registers so the MCU acknowledges the transmission is complete. This code would not be necessary except the Arduino STM32 code uses the USART1_IRQHandler and therefore doesn’t reset after the the UART1 transmit.
Make a serial connection to the SCL pin (PB6). Every press of the button sends a paragraph of text to the serial terminal.
The Code
// DMA USART2 transfer M2P memory to peripheral.
// Adafruit STM32F405 Feather.
//
// Add build_opt.h file with -DHAL_UART_MODULE_ENABLED
// Tools > U(S)ART Support: Disabled.
//
// SCL (PB6) TX pin.
// 10 (PB9) Button pin.
//
uint8_t data_stream[] =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \
mollit anim id est laborum.\r\n";
uint32_t data_length{sizeof(data_stream)};
uint32_t deBounce{0};
uint32_t lastBounce{0};
constexpr uint32_t DEBOUNCE_INTERVAL{50};
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
extern "C" void DMA2_Stream7_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart1_tx); }
void uartInit(void) {
huart1.Instance = USART1;
huart1.Init.BaudRate = 9600;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
// USART1 GPIO Configuration
// PB6 --> USART1_TX
// PB7 --> USART1_RX
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
hdma_usart1_tx.Instance = DMA2_Stream7;
hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
hdma_usart1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
while(1);
__HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
if (HAL_UART_Init(&huart1) != HAL_OK)
while(1);
}
void dmaInit(void) {
__HAL_RCC_DMA2_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);
}
void buttonISR() {
if (millis() > deBounce) {
deBounce = millis() + DEBOUNCE_INTERVAL;
HAL_UART_Transmit_DMA(&huart1, data_stream, (uint16_t)data_length);
}
}
void setup(void) {
dmaInit();
uartInit();
pinMode(10, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PB9), buttonISR, FALLING);
}
void loop() {
if (deBounce != lastBounce) {
lastBounce = deBounce;
// UART in mode Transmision end?
while ( !(huart1.Instance->SR & USART_SR_TC) &&
!(huart1.Instance->CR1 & USART_CR1_TCIE) ) ;
// Disable TXEIE and TCIE interrupts.
huart1.Instance->CR1 &= ~(USART_CR1_TXEIE | USART_CR1_TCIE);
// At end of Tx process, restore huart->gState to ready.
huart1.gState = HAL_UART_STATE_READY;
}
}
This is the second example of DMA usage on the STM32F405 Feather board. Programming is from the Arduino IDE. This example demonstrates the peripheral to memory (P2M) DMA Mode. We send serial data to the board via UART DMA storing the data into SRAM memory.
For this demo to work, the Arduino serial code needs to be disabled. The instructions are included in the code comments.
Make a serial connection to the SDA pin (PB7). Every 5 characters the board receives toggles the onboard LED.