Most of my current projects are using the very, very nice stm32f103 series of chips, as these provide excellent performance per dollar cost. The embedded DMA in these chips is a massive help in getting a project up and running without any struggle for cpu power.
I find the DMA documentation to be lacking at best with getting this system up and running using the new HAL drivers from STM. While this is actually quite an easy system to get up and running, I could not find any nice notes on how the system should be setup and things to be aware of.
STM Cube MX
For all of my projects, I begin in STM32 Cube MX software, which allows the planning of all of the pin connections for the chips main circuit board. While I am in the cube software, I take the time to setup as much of the DMA as possible, In the settings for the ADC I set it to use the DMA engine for transfers, and also setup automatic scanning of the required inputs. In this example I have setup two channels to be read via the ADC with the plan of having these automatically buffered into RAM for me.
On the other end this will just pop out into an array of uint32_t
for us
to then read whenever suits in our code.
After this setup, I proceeded to export the C code from the cube software, and store this in a separate working folder to my code. As I prefer to work in C++, I create a new C++ project in System Workbench and then manually transfer over the generated code. I find this also provides a great opportunity to sanity check the code as I transfer it over.
The Code
The Cube MX software has already created most of the initialization code required for the setup of the IO. All I have to do after this is add the required code to link the target array we want the DMA to fill to the DMA itself. I have set up the DMA to increment in half word size (half of a 32 bit value), this provides the minimum memory footprint as the ADC only outputs 16 bit numbers (it’s bit resolution is < 16 bits). Note that for other peripherals you will want 32 bit (word) increments on the address.
In the global array space I define the following array to store the results:
uint16_t ADCReadings[2];
Now that I have declared the array that I want the data to be pushed into, I
use the HAL function HAL_ADC_Start_DMA
that lets me link the DMA result
array to the ADC and also start the ADC running.
HAL_ADC_Start_DMA(&hadc1, (uint32_t*) ADCReadings, 2);
This then starts the DMA engine and the ADC and links the two together for me. Note that I call this after i have setup all the other peripherals
How well does this work?
Excellently.
I have found that this method provides really nice quick updating of the memory address as you would expect.
Code
The Cube MX software generated 99% of the code for me, however below is outlined all required code, omitting the auto generated stuff for things like clock setup, which will be device dependant.
The only required lines, really, are in the main()
function.
main.c
Some sections are omitted for brevity.
uint16_t ADCReadings[2]; //ADC Readings
int main(void)
{
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
__enable_irq();
HAL_ADC_Start_DMA(&hadc1, (uint32_t*) ADCReadings, 2);//start the DMA collecting the data
for(;;)
{
}
}
/* ADC1 init function */
void MX_ADC1_Init(void) {
ADC_ChannelConfTypeDef sConfig;
/**Common config
*/
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.NbrOfDiscConversion = 0;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 2;
if (HAL_ADC_Init(&hadc1) != HAL_OK) {
Error_Handler();
}
/**Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_71CYCLES_5;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
Error_Handler();
}
/**Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
Error_Handler();
}
}
/**
* Enable DMA controller clock
*/
void MX_DMA_Init(void) {
/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE()
;
/* DMA interrupt init */
/* DMA1_Channel1_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
stm32f1xx_hal_msp.c
DMA_HandleTypeDef hdma_adc1;
void HAL_ADC_MspInit(ADC_HandleTypeDef* hadc)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(hadc->Instance==ADC1)
{
/* USER CODE BEGIN ADC1_MspInit 0 */
/* USER CODE END ADC1_MspInit 0 */
/* Peripheral clock enable */
__HAL_RCC_ADC1_CLK_ENABLE();
/**ADC1 GPIO Configuration
PA0-WKUP ------> ADC1_IN0
PA1 ------> ADC1_IN1
*/
GPIO_InitStruct.Pin = X_Pin|Y_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* Peripheral DMA init*/
hdma_adc1.Instance = DMA1_Channel1;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;
if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
{
}
__HAL_LINKDMA(hadc,DMA_Handle,hdma_adc1);
HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
/* USER CODE BEGIN ADC1_MspInit 1 */
/* USER CODE END ADC1_MspInit 1 */
}
}
void HAL_ADC_MspDeInit(ADC_HandleTypeDef* hadc)
{
if(hadc->Instance==ADC1)
{
/* USER CODE BEGIN ADC1_MspDeInit 0 */
/* USER CODE END ADC1_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_ADC1_CLK_DISABLE();
/**ADC1 GPIO Configuration
PA0-WKUP ------> ADC1_IN0
PA1 ------> ADC1_IN1
*/
HAL_GPIO_DeInit(GPIOA, X_Pin|Y_Pin);
/* Peripheral DMA DeInit*/
HAL_DMA_DeInit(hadc->DMA_Handle);
}
/* USER CODE BEGIN ADC1_MspDeInit 1 */
/* USER CODE END ADC1_MspDeInit 1 */
}