芯片型号:STM32F103RC
软件开发包:标准外设库
一、 任务分析
如图,左边是 ADC 扫描模式的执行流程,有 7 个通道,触发一次, 7 个通道依次进行 AD 转换,然后转换结果都放到 ADC_DR 数据寄存器中。在这里,DMA 的任务是在每个单独的通道转换完成后,进行数据转运,并且目的地址进行自增,这样数据就不会被覆盖了。
所以,这里 DMA 的配置是:
- 外设地址写入 ADC_DR 的地址,存储器的地址可以在 SRAM 中定义一个数组 ADValue,然后把 ADValue 的地址作为存储器的地址;
- 对于数据宽度,因为 ADC_DR 和 ADValue 存储的数据都是 uint16_t 型的,所以数据的宽度就是 16 位的半字;
- 对于地址是否自增,如图,外设地址不自增,存储器地址自增;
- 传输方向是外设站点到存储器站点;
- 传输计数器写入 7,因为这里通道有 7 个,要计数 7 次。计数器是否自动重装,这里可以看 ADC 的配置,ADC 如果是单次扫描,可以不自动重装,转换一轮就停止;如果是扫描模式,就可以使用自动重装,在 ADC 启动下一轮转换的时候,DMA 也启动下一轮转运,同步工作;
- 这里的 ADC_DR 的值在 ADC 单个通道转换完成后才会有效,DMA 转运的时机需要和 ADC 单个通道转换完成同步,所以 DMA 的触发要选择 ADC 的硬件触发。
说明: ADC 的扫描模式,在每个单独通道转换完成后并没有任何标志位,也不会触发中断,所以在程序中不太好判断某一个通道转换完成的时机。虽然单个通道转换完成后,不产生任何标志位和中断,但它会产生 DMA 请求,触发 DMA 转运。
二、 软件设计(ADC 单次扫描+ DMA 单次转运)
上述图片中展示了 7 个通道的使用,在这里以 2 个通道作为例子。
- 初始化 GPIO,为了方便,使用 pc0 和 pc1
void AD_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 设置为模拟输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; // 初始化pc0和pc1
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
- 配置 DMA,传输方向为外设到存储器,传输数据数目为 2,使用单次模式,硬件触发
uint16_t ad_value[2]; // 用于存储ADC的转换结果
void AD_DMA_Config(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能 DMA1 时钟
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 设置外设基地址为ADC1的DR寄存器的地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据宽度为半字
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 失能外设地址自增
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ad_value; // 设置存储器基地址为ad_value的首地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 存储器数据宽度为半字
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 使能存储器地址自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 设置数据传输方向为外设到存储器
DMA_InitStructure.DMA_BufferSize = 2; // 设置传输数据数目为2
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 设置为单次模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 失能存储器到存储器模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 设置优先级为中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 初始化 DMA1 的通道1
}
注意:这里没有加DMA_Cmd(DMA1_Channel1, ENABLE);
,使能 DMA 通道的任务交给了后面创建的函数 AD_GetValue
。
- 配置 ADC,配置两个规则通道,并分配序列号,使用单次转换、扫描模式,使能 ADC 的DMA触发信号
void AD_Config(void)
{
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 使能ADC1时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 配置ADC时钟为PCLK2的六分频(72M/6 = 12MHz)
// 配置ADC1的规则通道0,序列1,采样时间为55.5个周期
ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_55Cycles5);
// 配置ADC1的规则通道1,序列2,采样时间为55.5个周期
ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_55Cycles5);
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 设置ADC工作在独立模式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 禁用外部触发转换
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 使用单次转换模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 使能扫描转换模式
ADC_InitStructure.ADC_NbrOfChannel = 2; // 设置转换通道数量为2
ADC_Init(ADC1, &ADC_InitStructure);
ADC_DMACmd(ADC1, ENABLE); // 使能ADC1的DMA触发信号
ADC_Cmd(ADC1, ENABLE); // 使能ADC
ADC_ResetCalibration(ADC1); // 复位ADC1的校准寄存器
while (ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待复位校准结束
ADC_StartCalibration(ADC1); // 启动ADC1的校准
while (ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
}
注意: ADC_DMACmd(ADC1, ENABLE)
必须放在RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE)
之后。
- 使用 ADC 的扫描模式,多通道,所以就不需要手动更改通道了;转换结果通过 DMA 转运,所以也不需要手动调用函数取值了;ADC每次转换完成后会有 DMA 请求,所以也不需要调用等待 ADC 转换完成的函数。但因为还是单次模式,所以还是需要软件触发一下 ADC 开始转换。
void AD_GetValue(void)
{
DMA_Cmd(DMA1_Channel1, ENABLE); // 使能 DMA1 的通道1
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动ADC1的软件转换
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); // 等待传输完成标志位置位
DMA_ClearFlag(DMA1_FLAG_TC1); // 清除传输完成标志位
DMA_Cmd(DMA1_Channel1, DISABLE); // 失能 DMA1 的通道1
DMA_SetCurrDataCounter(DMA1_Channel1, 2); // 设置当前数据传输数目为2
}
三、 改用 ADC 连续扫描 + DMA 循环转运
上述是 ADC 单次扫描+ DMA 单次转运的模式,也可以配置成为 ADC 连续扫描 + DMA 循环转运的模式,程序如下改动:
- 在
AD_Config
中将
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 使用单次转换模式
改为
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 使用连续转换模式
- 在
AD_DMA_Config
中将
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 设置为单次模式
改为
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 设置为循环模式
- 在这里,可以直接将
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
放在AD_Config
的最后一行,同时将DMA_Cmd(DMA1_Channel1, ENABLE);
放在AD_DMA_Config
的最后一行。因为在 ADC 触发之后,ADC 连续转换,DMA 循环转运,两者一直在工作,始终把最新的转换结果刷新到 SRAM 数组里,想要数据直接到数组里取就行了 。因此,在该模式下,也就不需要函数AD_GetValue
了。
参考视频源于B站up主: 野火科技、江协科技
参考文档:《STM32库开发实战指南——基于野火MINI开发板》