一、香农采样定律
为不失真的复现信号的变化,采样频率至少应大于等于连续信号最高频率分量的二倍。
采样定律只能确定采样周期的上限(采样频率的下限),实际采样周期的选择受多方面影响。
一般情况下:
-
对于响应快波动大、易受干扰(压力、流量)的过程,应该选较高的采样频率
-
对于过程响应慢,滞后大(温度、化学成分等)选取较低采样频率
当过程的纯滞后时间较长时,选取采样周期为纯滞后时间的1/4到1/8
STM32的ADC分辨率为12bit,假设PWM脉冲频率为20KHz,则理论上讲采样频率达到40Khz就能正确采集波形。
二、代码改进
注意看写的注释,不然干看代码是看不懂的:
(1)adc.h修改配置
VoltageResolution = ADC(Hex) * 3.3 / 2^n *1000 mV
根据香农采样定律,采样频率选取的最小值应该为PWM的两倍
再由过采样理论, 实际采样率 > 两倍PWM频率 * 4^2
所以采样频率能够拉到14bit,公式中应该改为除以2^14也就是16384
cpp
#define VOLT_RESOLUTION ((float)((VOLT_REF/(float)(16384))*(float)1000))
#define VOLTBUS_RESOLUTION ((float)( 3.3f/(float)4096) * (42.4f+2.0f) / 2.0f)
(2)adc.c 采样DMA
cpp
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
uint16_t ConvCnt = 0;
int32_t ADConv = 0 ;
//定义采样次数计数器,定义采样结果寄存器(注意,这次移到中断里成动态量了)
//这里注意防止采样次数AverCnt溢出
HAL_ADC_Stop_DMA(hadc);
//先停止DMA,目的是不干扰时序
SetChannelAsRank1(hadc,ADC_VOLT_CHANNEL);
//设置电压采样优先,为电压提供专属的数据采集时机,为电流保留了数据处理的机会
//此时DMA只采集电压,电流DMA被维护
HAL_ADC_Start(hadc);
//ADC重新开启,此时正式开始采集电压,与此同时处理电流
for(ConvCnt = 0; ConvCnt < (ADC_BUFFER ); ConvCnt++)
{
ADConv += ((int32_t)ADC_ConvValueHex[ConvCnt]);
}
//进行电流采集量累加,一共1024次
ADConv >>= ADC_Base;
//进行取平均,注意这里只移动了8位,也就是256,故意少除以2^2,为12bit->14bit的过采样处理
AverSum += ADConv;
//进行采样结果累加
AverCnt++;
//采样次数自增
HAL_ADC_Stop(hadc);
//停止ADC,并把ADC权限留给电流,电压后续在窗口看门狗中进行总线电压监测
SetChannelAsRank1(hadc,ADC_CURRENT_CHANNEL);
HAL_ADC_Start_DMA(hadc,(uint32_t*)ADC_ConvValueHex,ADC_BUFFER);
//这句话不仅有重新开始DMA的含义,也有重启ADC的意思
}
(3) 定时器中断采样数据进一步处理
cpp
void HAL_SYSTICK_Callback(void)
{
__IO int32_t ADC_Resul= 0;
__IO float Volt_Result = 0;
__IO float ADC_CurrentValue;
//定义ADC结果寄存器,电流采样电压结果寄存器,最终实际电流值寄存器
if((uwTick % 50) == 0)
{
ADC_Resul = AverSum/AverCnt ;
//每一次中断,进行动态窗口滤波,窗口长度根据此前DMA次数调整
OffsetCnt_Flag++;
if(OffsetCnt_Flag >= 16)
{
//经过实际测试,17次进入定时器中断时,电流波形才稳定
//那么我们就把前一次的值当作零漂
if(OffsetCnt_Flag == 16)
{
OffSetHex = ADC_Resul;
}
//标准的状态机写法,以固定的32代表电流采样进入正常状态,每次进来都会被赋为32
//那么我们如果需要在控制日志中导出电流数据,就可以在这个大判断后print它的flag
OffsetCnt_Flag = 32;
//adc结果减去偏差才是真正的采样值
ADC_Resul -= OffSetHex;
}
//计算进入单片机的电压模拟量
Volt_Result = ( (float)( (float)(ADC_Resul) * VOLT_RESOLUTION) );
//计算电机端实际的电流
ADC_CurrentValue = (float)( (Volt_Result / GAIN) / SAMPLING_RES);
//对动态窗口的计数值、累加值清零
AverCnt = 0;
AverSum = 0;
printf("Volt: %.1f mV -- Curr: %d mA\n",Volt_Result,(int32_t(ADC_CurrentValue+10));
}
}
(4) 窗口看门狗实现总线电压监测
这段代码实现了一个逻辑:如果电压异常被WD连续检测出六次,则强制关闭电机,然后关闭定时器PWM输出。
cpp
void HAL_ADC_LevelOutOfWindowCallback(ADC_HandleTypeDef* hadc)
{
//给一个计数值i,并定义为静态量
//注意这种用法,下一次触发看门狗时,i的值还保留,效果类似全局静态
static uint8_t i = 0;
i++;
//这里直接自增了 ,说明一切从i==1开始
if(ADC_VoltBus > VOLT_LIMIT_MIN && ADC_VoltBus < VOLT_LIMIT_MAX)
//如果总线电压大于最小供电,并小于最大电压限制
i = 0 ;
else if(i>=6)
{
SHUTDOWN_MOTOR();
//禁用驱动
HAL_TIM_PWM_Stop(&htimx_BDCMOTOR,TIM_CHANNEL_1);
HAL_TIMEx_PWMN_Stop(&htimx_BDCMOTOR,TIM_CHANNEL_1);
//关闭输出
PWM_Duty = 0;
//这里可以直接做PID清理
// ADC_VoltBus = (float)ADC_VoltBus * VOLTBUS_RESOLUTION;
//如果需要在控制日志中查看危险电压到底是多少,可以算一下ADC
printf("Bus Voltage is out of range!!\n");
//汇报控制日志
printf("Please Reset the Target!\n");
//死循环,等待用户reset
while(1);
}
}
(5)以上三个中断的优先级配置建议
为了安全起见,看门狗的中断优先级必须是三个中最高的。这样单片机才能在安全环境下稳定工作。
其次,ADC采样速率要求较快,并且是定时器数据处理的基础,所以我们安排次高优先级。
最后,当ADC数据处理完毕后,再将数据送给数据处理中断,所以它安排最低。
总结:
最高:窗口看门狗中断
适中:ADC的DMA中断
最低:数据处理的定时器中断