ADC电压电流实验原理
电机的控制需要电流电压数据来加强精度控制。
ADC有三种模式,独立模式,双重模式,三重模式。
独立模式使用ADC1\ADC2\ADC3中的一个。
双重模式使用ADC1和ADC2。三重模式就是三个 ADC 同时使用。
-
电压输入范围
ADC输入范围为:VREF- ≤ VIN ≤ VREF+。由VREF-、VREF+ 、VDDA 、VSSA、这四个外部引脚决定。
我们在设计原理图的时候一般把VSSA和VREF-接地, 把VREF+和VDDA 接3V3,得到ADC的输入电压范围为:0~3.3V。
如果我们想让输入的电压范围变宽,去到可以测试负电压或者更高的正电压,我们可以在外部加一个电压调理电路,把需要转换的电压抬升或者降压到0~3.3V,这样ADC就可以测量了。 -
输入通道
我们确定好ADC输入电压之后,那么电压怎么输入到ADC?这里我们引入通道的概念
外部的16个通道在转换的时候又分为规则通道和注入通道,其中规则通道最多有16路,注入通道最多有4路。
规则通道
规则通道:顾名思意,规则通道就是很规矩的意思,我们平时一般使用的就是这个通道,或者应该说我们用到的都是这个通道,没有什么特别要注意的可讲。
注入通道
注入,可以理解为插入,插队的意思,是一种不安分的通道。它是一种在规则通道转换的时候强行插入要转换的一种通道。如果在规则通道转换过程中,有注入通道插队,那么就要先转换完注入通道,等注入通道转换完成后,再回到规则通道的转换流程。这点跟中断程序很像,都是不安分的主。所以,注入通道只有在规则通道存在时才会出现。 -
转换顺序
规则序列
规则序列寄存器有3个,分别为SQR3、SQR2、SQR1。想要什么样的顺序可以自己设置。
注入序列
注入序列寄存器JSQR只有一个,最多支持4个通道
-
触发源
通道选好了,转换的顺序也设置好了,那接下来就该开始转换了。ADC转换可以由ADC控制寄存器2: ADC_CR2的ADON这个位来控制, 写1的时候开始转换,写0的时候停止转换。
除了这种庶民式的控制方法,ADC还支持外部事件触发转换,这个触发包括内部定时器触发和外部IO触发。
-
转换时间
ADC输入时钟ADC_CLK由PCLK2经过分频产生,最大值是36MHz,典型值为30MHz,分频因子由ADC通用控制寄存器ADC_CCR的ADCPRE[1:0]设置, 可设置的分频系数有2、4、6和8,注意这里没有1分频。对于STM32F407ZGT6我们一般设置PCLK2=HCLK/2=84MHz。 所以程序一般使用4分频或者6分频。
其中采样周期最小是3个,即如果我们要达到最快的采样,那么应该设置采样周期为3个周期,这里说的周期就是1/ADC_CLK。
ADC的总转换时间跟ADC的输入时钟和采样时间有关,公式为:
Tconv = 采样时间 + 12个周期
当ADCCLK = 30MHz,即PCLK2为60MHz,ADC时钟为2分频,采样时间设置为3个周期,那么总的转换时为:Tconv = 3 + 12 = 15个周期 =0.5us。
一般我们设置PCLK2=84MHz,经过ADC预分频器能分频到最大的时钟只能是21M,采样周期设置为3个周期,算出最短的转换时间为0.7142us,这个才是最常用的。
6. 数据寄存器
一切准备就绪后,ADC转换后的数据根据转换组的不同,规则组的数据放在ADC_DR寄存器,注入组的数据放在JDRx。 如果是使用双重或者三重模式那规矩组的数据是存放在通用规矩寄存器ADC_CDR内的。
规则通道可以有16个这么多,可规则数据寄存器只有一个,如果使用多通道转换,那转换的数据就全部都挤在了DR里面,前一个时间点转换的通道数据,就会被下一个时间点的另外一个通道转换的数据覆盖掉,所以当通道转换完成后就应该把数据取走,或者开启DMA模式,把数据传输到内存里面,不然就会造成数据的覆盖。最常用的做法就是开启DMA传输。
规则数据寄存器ADC_DR是仅适用于独立模式的,而通用规则数据寄存器ADC_CDR是适用于双重和三重模式的。独立模式就是仅仅适用三个ADC的其中一个,双重模式就是同时使用ADC1和ADC2,而三重模式就是三个ADC同时使用。在双重或者三重模式下一般需要配合DMA数据传输使用。 -
中断
中断分为四种:规则通道转换结束中断,注入转换通道转换结束中断,模拟看门狗中断和溢出中断。规则和注入通道转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据直接存储在内存里面。 -
电压转换
模拟电压经过ADC转换后,是一个12位的数字值,如果通过串口以16进制打印出来的话,可读性比较差,那么有时候我们就需要把数字电压转换成模拟电压,也可以跟实际的模拟电压(用万用表测)对比,看看转换是否准确。
我们一般在设计原理图的时候会把ADC的输入电压范围设定在:0~3.3v,因为ADC是12位的,那么12位满量程对应的就是3.3V,12位满量程对应的数字值是:2^12。数值0对应的就是0V。如果转换后的数值为 X ,X对应的模拟电压为Y,那么会有这么一个等式成立: 2^12 / 3.3 = X / Y,=> Y = (3.3 * X ) / 2^12。
ADC采样代码讲解
//电机初始化三部分,引脚初始化,与dma储存链接初始化,adc采样初始化
void ADC_Init(void)
{
ADC_GPIO_Config();
adc_dma_init();
ADC_Mode_Config();
}
/**
* @brief ADC 通道引脚初始化
* @param 无
* @retval 无
*/
static void ADC_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能 GPIO 时钟
CURR_ADC_GPIO_CLK_ENABLE();
VBUS_GPIO_CLK_ENABLE();
// 配置 IO
GPIO_InitStructure.Pin = CURR_ADC_GPIO_PIN;
//模式设置为模拟模式
GPIO_InitStructure.Mode = GPIO_MODE_ANALOG;
GPIO_InitStructure.Pull = GPIO_NOPULL ; //不上拉不下拉
HAL_GPIO_Init(CURR_ADC_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = VBUS_GPIO_PIN;
HAL_GPIO_Init(VBUS_GPIO_PORT, &GPIO_InitStructure);
}
void adc_dma_init(void)
{
// ------------------DMA Init 结构体参数 初始化--------------------------
// ADC1使用DMA2,数据流0,通道0,这个是手册固定死的
// 开启DMA时钟
CURR_ADC_DMA_CLK_ENABLE();
// 数据传输通道
DMA_Init_Handle.Instance = CURR_ADC_DMA_STREAM;
// 数据传输方向为外设到存储器
DMA_Init_Handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
// 外设寄存器只有一个,地址不用递增
DMA_Init_Handle.Init.PeriphInc = DMA_PINC_DISABLE;
// 存储器地址固定,使用了缓冲器进行数据接受
DMA_Init_Handle.Init.MemInc = DMA_MINC_ENABLE;
// 外设数据大小为半字,即两个字节,12位
DMA_Init_Handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
// 存储器数据大小也为半字,跟外设数据大小相同
DMA_Init_Handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
// 循环传输模式,连续工作
DMA_Init_Handle.Init.Mode = DMA_CIRCULAR;
// DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响
DMA_Init_Handle.Init.Priority = DMA_PRIORITY_HIGH;
// 禁止DMA FIFO ,使用直连模式
DMA_Init_Handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
// FIFO 大小,FIFO模式禁止时,这个不用配置
DMA_Init_Handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_HALFFULL;
DMA_Init_Handle.Init.MemBurst = DMA_MBURST_SINGLE;
DMA_Init_Handle.Init.PeriphBurst = DMA_PBURST_SINGLE;
// 选择 DMA 通道,通道存在于流中,固定值
DMA_Init_Handle.Init.Channel = CURR_ADC_DMA_CHANNEL;
//初始化DMA流,流相当于一个大的管道,管道里面有很多通道
HAL_DMA_Init(&DMA_Init_Handle);
//将adc句柄和dma句柄相链接
__HAL_LINKDMA(&ADC_Handle,DMA_Handle,DMA_Init_Handle);
}
/**
* @brief ADC 和 DMA 初始化
* @param 无
* @retval 无
*/
static void ADC_Mode_Config(void)
{
// 开启ADC时钟
CURR_ADC_CLK_ENABLE();
// -------------------ADC Init 结构体 参数 初始化------------------------
// ADC1
ADC_Handle.Instance = CURR_ADC;
// 时钟为fpclk 4分频,四分频后为21mhz
ADC_Handle.Init.ClockPrescaler = ADC_CLOCKPRESCALER_PCLK_DIV4;
// ADC 分辨率,12位
ADC_Handle.Init.Resolution = ADC_RESOLUTION_12B;
// 禁止扫描模式,多通道采集才需要
ADC_Handle.Init.ScanConvMode = ENABLE;
// 连续转换
ADC_Handle.Init.ContinuousConvMode = ENABLE;
// 非连续转换
ADC_Handle.Init.DiscontinuousConvMode = DISABLE;
// 非连续转换个数
ADC_Handle.Init.NbrOfDiscConversion = 0;
//禁止外部边沿触发
ADC_Handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
//使用软件触发
ADC_Handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;
//数据左对齐
ADC_Handle.Init.DataAlign = ADC_DATAALIGN_LEFT;
//转换通道 2个
ADC_Handle.Init.NbrOfConversion = 2;
//使能连续转换请求
ADC_Handle.Init.DMAContinuousRequests = ENABLE;
//转换完成标志
ADC_Handle.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
// 初始化ADC
HAL_ADC_Init(&ADC_Handle);
//-----------电流采集通道-----------------------------
ADC_ChannelConfTypeDef ADC_Config;
ADC_Config.Channel = CURR_ADC_CHANNEL;
ADC_Config.Rank = 1;
// 采样时间间隔
ADC_Config.SamplingTime = ADC_SAMPLETIME_3CYCLES;
ADC_Config.Offset = 0;
// 配置 ADC 通道转换顺序为1,第一个转换,采样时间为3个时钟周期
HAL_ADC_ConfigChannel(&ADC_Handle, &ADC_Config);
/** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
电压采集通道 */
ADC_Config.Channel = VBUS_ADC_CHANNEL;
ADC_Config.Rank = 2;
// 采样时间间隔
ADC_Config.SamplingTime = ADC_SAMPLETIME_3CYCLES;
ADC_Config.Offset = 0;
if (HAL_ADC_ConfigChannel(&ADC_Handle, &ADC_Config) != HAL_OK)
{
while(1);
}
// 外设中断优先级配置和使能中断配置
HAL_NVIC_SetPriority(ADC_DMA_IRQ, 1, 1);
HAL_NVIC_EnableIRQ(ADC_DMA_IRQ);
HAL_ADC_Start_DMA(&ADC_Handle, (uint32_t*)&adc_buff, ADC_NUM_MAX);
}
此时采集的数据电阻,我们需要将他转化成电流和电压
/**
* @brief 获取电流值
* @param 无
* @retval 转换得到的电流值
*/
int32_t get_curr_val(void)
{
static uint8_t flag = 0;
static uint32_t adc_offset = 0; // 偏置电压
int16_t curr_adc_mean = 0; // 电流 ACD 采样结果平均值
curr_adc_mean = adc_mean_sum / adc_mean_count; // 保存平均值
adc_mean_count = 0;
adc_mean_sum = 0;
if (flag < 17)
{
adc_offset = curr_adc_mean; // 多次记录偏置电压,待系统稳定偏置电压才为有效值
flag += 1;
}
if(curr_adc_mean>=adc_offset)
{
curr_adc_mean -= adc_offset; // 减去偏置电压
}else
{
curr_adc_mean=0;
}
float vdc = GET_ADC_VDC_VAL(curr_adc_mean); // 获取电压值
return GET_ADC_CURR_VAL(vdc);
}
/**
* @brief 获取电源电压值
* @param 无
* @retval 转换得到的电流值
*/
float get_vbus_val(void)
{
float vdc = GET_ADC_VDC_VAL(vbus_adc_mean); // 获取电压值
return GET_VBUS_VAL(vdc);
}
/**
* @brief 常规转换在非阻塞模式下完成回调,采样之后进行数据处理
* @param hadc: ADC 句柄.
* @retval 无
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
uint32_t adc_mean = 0;
HAL_ADC_Stop_DMA(hadc); // 停止 ADC 采样,处理完一次数据在继续采样
/* 计算电流通道采样的平均值 */
for(uint32_t count = 0; count < ADC_NUM_MAX; count+=2)
{
adc_mean += (uint32_t)adc_buff[count];
}
adc_mean_sum += adc_mean / (ADC_NUM_MAX / 2); // 累加电压
adc_mean_count++;
#if 1
adc_mean = 0;
/* 计算电压通道采样的平均值 */
for(uint32_t count = 1; count < ADC_NUM_MAX; count+=2)
{
adc_mean += (uint32_t)adc_buff[count];
}
vbus_adc_mean = adc_mean / (ADC_NUM_MAX / 2); // 保存平均值
#else
vbus_adc_mean = adc_buff[1];
#endif
HAL_ADC_Start_DMA(&ADC_Handle, (uint32_t*)&adc_buff, ADC_NUM_MAX); // 开始 ADC 采样
}
另外还要有一部分电流电压保护电路
//判断电流是否超过限定值,超过后卡住程序,此段函数位于main.c函数中的电流电压读取打印中
if (current > CURR_MAX) // 判断是不是超过限定的值
{
if (curr_max_count++ > 5) // 连续5次超过
{
LED2_ON;
set_motor_disable();
curr_max_count = 0;
printf("电流超过限制!请检查原因,复位开发板在试!\r\n");
while(1);
}
}
电压的保护设置在adc.c文件中,采用adc看门狗中断
/** Configure the analog watchdog
*/
ADC_AnalogWDGConfTypeDef AnalogWDGConfig = {0};
AnalogWDGConfig.WatchdogMode = ADC_ANALOGWATCHDOG_SINGLE_REG;
AnalogWDGConfig.HighThreshold = VBUS_HEX_MAX;
AnalogWDGConfig.LowThreshold = VBUS_HEX_MIN;
AnalogWDGConfig.Channel = VBUS_ADC_CHANNEL;
//看门狗中断开启
AnalogWDGConfig.ITMode = ENABLE;
看门狗中断以后执行以下回调函数
void HAL_ADC_LevelOutOfWindowCallback(ADC_HandleTypeDef* hadc)
{
float temp_adc;
flag_num++; // 电源电压超过阈值电压
temp_adc = get_vbus_val();
if (temp_adc > VBUS_MIN && temp_adc < VBUS_MAX)
flag_num = 0;
if (flag_num > ADC_NUM_MAX) // 电源电压超过阈值电压10次
{
set_motor_disable();
flag_num = 0;
LED1_ON;
printf("电源电压超过限制!请检查原因,复位开发板在试!\r\n");
while(1);
}
}
DMA储存
DMA(Direct Memory Access,直接存储区访问)为实现数据高速在外设寄存器与存储器之间或者存储器与存储器之间传输提供了高效的方法。 之所以称之为高效,是因为DMA传输实现高速数据移动过程无需任何CPU操作控制。
①外设通道选择
STM32F4xx系列资源丰富,具有两个DMA控制器,同时外设繁多,为实现正常传输,DMA需要通道选择控制。每个DMA控制器具有8个数据流, 每个数据流对应8个外设请求。在实现DMA传输之前,DMA控制器会通过DMA数据流x配置寄存器DMA_SxCR(x为0~7,对应8个DMA数据流) 的CHSEL[2:0]位选择对应的通道作为该数据流的目标外设。
每个外设请求都占用一个数据流通道,相同外设请求可以占用不同数据流通道。比如SPI3_RX请求,即SPI3数据发送请求, 占用DMA1的数据流0的通道0,因此当我们使用该请求时,我们需要在把DMA_S0CR寄存器的CHSEL[2:0]设置为“000”, 此时相同数据流的其他通道不被选择,处于不可用状态,比如此时不能使用数据流0的通道1即I2C1_RX请求等等。
②仲裁器
一个DMA控制器对应8个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息。如果我们需要同时使用同一个DMA控制器(DMA1或DMA2)多个外设请求时,那必然需要同时使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢?这就需要仲裁器来管理判断了。
仲裁器管理数据流方法分为两个阶段。第一阶段属于软件阶段,我们在配置数据流时可以通过寄存器设定它的优先级别,具体配置DMA_SxCR寄存器PL[1:0]位,可以设置为非常高、高、中和低四个级别。第二阶段属于硬件阶段,如果两个或以上数据流软件设置优先级一样,则他们优先级取决于数据流编号,编号越低越具有优先权,比如数据流2优先级高于数据流3。
③FIFO
每个数据流都独立拥有四级32位FIFO(先进先出存储器缓冲区)。DMA传输具有FIFO模式和直接模式。
直接模式在每个外设请求都立即启动对存储器传输。在直接模式下,如果DMA配置为存储器到外设传输那DMA会见一个数据存放在FIFO内,如果外设启动DMA传输请求就可以马上将数据传输过去。
FIFO用于在源数据传输到目标地址之前临时存放这些数据。可以通过DMA数据流xFIFO控制寄存器DMA_SxFCR的FTH[1:0]位来控制FIFO的阈值,分别为1/4、1/2、3/4和满。如果数据存储量达到阈值级别时,FIFO内容将传输到目标中。
FIFO对于要求源地址和目标地址数据宽度不同时非常有用,比如源数据是源源不断的字节数据,而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来4个8位字节的数据拼凑成一个32位字数据。此时使用FIFO功能先把数据缓存起来,分别根据需要输出数据。
FIFO另外一个作用使用于突发(burst)传输。
④存储器端口、⑤外设端口
DMA控制器实现双AHB主接口,更好利用总线矩阵和并行传输。DMA控制器通过存储器端口和外设端口与存储器和外设进行数据传输, 关系见 图21_4。DMA控制器的功能是快速转移内存数据,需要一个连接至源数据地址的端口和一个连接至目标地址的端口。
DMA2(DMA控制器2)的存储器端口和外设端口都是连接到AHB总线矩阵,可以使用AHB总线矩阵功能。DMA2存储器和外设端口可以访问相关的内存地址,包括有内部Flash、内部SRAM、AHB1外设、AHB2外设、APB2外设和外部存储器空间。
DMA1的存储区端口相比DMA2的要减少AHB2外设的访问权,同时DMA1外设端口是没有连接至总线矩阵的,只有连接到APB1外设,所以DMA1不能实现存储器到存储器传输。