第23章 无刷电机电压温度电流采集
前面我们已经学会了无刷电机的基本驱动,本章我们将利用无刷驱动板的电路功能,来检测电机的三相电流以及驱动板的电源电压和温度。
本章分为如下几个小节:
23.1 驱动板硬件电路原理分析
23.2 硬件设计
23.3 程序设计
23.4 下载验证
23.1驱动板硬件电路原理分析
下面我们主要分为三部分来讲解驱动板的硬件电路,包括:三相电流采集电路分析、电源电压采集电路分析、温度采集电路分析共三部分。
23.1.1 三相电流采集电路分析

图23.1.1.1 U相电流采集电路
图23.1.1.1为U相电流采集电路(U、V、W三相同理,这里只以U相为例),其中R17为采样电阻(20mR),当有电流 I 流过采样电阻时,采样电阻上就会产生一个电压,电压大小为: I*0.02Ω(20mR)。我们使用 I-V 表示电流 I 经过采样电阻后形成的电压,所以U相的电压,我们使用 I-V_U表示。
那么现在只需要测出电压I-V_U的值,就可以换算出U相电流,但是这里我们发现一个问题,假设U相电流为10A,此时电压I-V_U = 10 * 0.02 V = 0.2V,同理当电流为20A时,求得的电压值I-V_U = 0.4V,可以看到当电流比较大的情况下,求得的电压值是非常小的,不利于我们的ADC采集,考虑到ADC的精度问题,所以我还需将电压经过差分放大电路放大后在来采集,如下图:

图23.1.1.2 差分放大电路
可以从图23.1.1.2中得出该差分放大电路的放大倍数为:Diff = 12K/(1K+1K)= 6倍,并有一抬升电压1.25V,所以电压放大后的输出为: AMP_IU = 6 * ( 0.02*I ) + 1.25V。由于AMP_IU是接到电机开发板的ADC引脚的,所以该值是已知的,此时就可以计算出U相电流I的值。
23.1.2 电源电压采集电路分析

图23.1.2.1 电源电压采集电路
图23.1.2.1 为驱动板板载的电源电压检测电路。驱动板的供电电压POWER经过电阻分压后进入 LMV358 的 A 路运放,该运放为电压跟随电路,所以1脚输出电压VBUS 和电阻分压的值相同,即 VBUS = POWER/(12K+12K+1K)*1K,当ADC采集到VBUS电压后就可以计算出 POWER 电压。
23.1.3 温度采集电路分析

图23.1.3.1 温度采集电路
图23.1.3.1为温度检测电路,可以看到,NTC电阻和固定的4.7K电阻分压,然后接入B路运放,B路运放也是电压跟随电路,所以此时VTMEP = 3.3V/(Rt+4.7K)*4.7K,通过ADC采集VTMEP的电压后就可以算出Rt的值,其中 Rt 就表示 NTC 电阻在当前温度下的电阻值。从NTC电阻的数据手册可以找到温度计算公式:T1=1 / ( In (Rt/R0) / B + 1/T2 ),这里的T1和T2指的是K度即开尔文温度,K度 = 273.15(绝对温度) + 摄氏度;其中:
l T2 :273.15 + 25(25摄氏度下的K度值)
l T1 :实际温度
l Rt :热敏电阻在T1温度下的阻值;
l R0 :热敏电阻在T2常温下的标称阻值;
l B值 :热敏电阻的重要恒定参数;
其中T1是我们需要求解的温度值,Rt的大小由前面的温度采集电路可以算出,其他的参数都是可以在数据手册中查找得出,代入温度求解公式即可算出T1温度值,注意该值为K度。
注意:我们所使用的温度换算公式以及相关参数均来自《NCP18XH103F03RB 热敏电阻.pdf》这个数据手册,路径:A盘→6,硬件资料→2,芯片资料文件夹下可以找到。
23.2 硬件设计
1、例程功能
l 本实验使用无刷电机接口一连接无刷电机驱动板
l 当按下KEY0一次加速旋转,按一次KEY1减速旋转,按下KEY2则停止旋转。
l LED0作为程序运行状态指示灯,闪烁周期200ms。
l 测量并在LCD和串口显示无刷电机电压、电流和温度
2、硬件资源
1)LED灯:LED0 – PE0
2)独立按键
KEY0 – PE12
KEY1 – PE13
KEY2 – PE14
3)定时器:
TIM1_CH1:PA8
TIM1_CH2:PA9
TIM1_CH3:PA10
4)IO:
下桥臂IO:PB13\14\15
霍尔IO: PA0\1\2
使能IO:SHDN: PC13
5)无刷电机
6)无刷电机驱动板
7)12-60V的DC电源(建议使用电机额定电压24V)
8)ADC:
ADC12_CH6(温度) :PC0
ADC1_CH4(电源电压) :PA3
ADC12_CH7(U相电流) :PC1
ADC12_CH8(V相电流) :PC2
ADC12_CH9(W相电流) :PC3
3、原理图

图23.2.1 无刷电机接口1原理图
相较于上一章,本实验多了5个模拟采集引脚,分别是:电源电压(ADC1_CN4/PA3)、 温度(ADC1_CN6/PC0)、三相电流UVW (ADC1_CN7/PC1、ADC1_CN8/PC2、ADC1_CN9/PC3) 。接口所涉及的IO如下:

表23.2.1 无刷相关IO口说明
本章节的硬件接线部分和基础驱动一模一样,注意:驱动板的JP3跳线帽短接H&Z与HALL,实物连接如下:

2.2 开发板&驱动板连接图
23.3 程序设计
23.3.1 无刷电机参数采集的配置步骤
1)实现电机基本驱动
实现电机的基础驱动函数,启停、6步换向组合等。
2)初始化ADC&DMA
初始化ADC通道的IO,设置ADC工作方式、DMA等。
3)转速控制
设置电机旋转方向及PWM占空比。
4)编写中断服务函数
PWM中断用于换向控制、采集电机未开始转时的基准电压,在将旋转后得到的电压进行相减得到实际电压等。
23.3.2 程序流程图

图23.3.2.1 无刷电机基本控制流程图
23.3.3 程序解析
该程序是在上一章的工程基础上添加的,大部分相同,这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。定时器驱动相关源码与上一章节一致,这里不再赘述。我们先看ADC相关的程序,ADC驱动源码包括两个文件:adc.c和adc.h。
首先看adc.h头文件的内容:
/* ADC及引脚 定义 */
#define ADC_VBUS_CHX_GPIO_PORT GPIOA /* 电源电压采集引脚 */
#define ADC_VBUS_CHX_GPIO_PIN GPIO_PIN_3
#define ADC_VBUS_CHX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define ADC_VTEMP_CHX_GPIO_PORT GPIOC /* 温度采集引脚 */
#define ADC_VTEMP_CHX1_GPIO_PIN GPIO_PIN_0
#define ADC_VTEMP_CHX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define ADC_AMPU_CHX_GPIO_PORT GPIOC /* U相采集引脚 */
#define ADC_AMPU_CHX_GPIO_PIN GPIO_PIN_1
#define ADC_AMPU_CHX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define ADC_AMPV_CHX_GPIO_PORT GPIOC /* V相采集引脚 */
#define ADC_AMPV_CHX_GPIO_PIN GPIO_PIN_2
#define ADC_AMPV_CHX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define ADC_AMPW_CHX_GPIO_PORT GPIOC /* W相采集引脚 */
#define ADC_AMPW_CHX_GPIO_PIN GPIO_PIN_3
#define ADC_AMPW_CHX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define ADC_ADCX ADC1
#define ADC_VBUS_CHX ADC_CHANNEL_4 /* 电源电压采集ADC通道 */
#define ADC_VTEMP_CHX ADC_CHANNEL_6 /* 驱动板温度采集ADC通道 */
#define ADC_AMPU_CHX ADC_CHANNEL_7 /* U相电流采集ADC通道 */
#define ADC_AMPV_CHX ADC_CHANNEL_8 /* V相电流采集ADC通道 */
#define ADC_AMPW_CHX ADC_CHANNEL_9 /* W相电流采集ADC通道 */
#define ADC_ADCX_CHY_CLK_ENABLE() do{ __HAL_RCC_ADC12_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */
#define ADC_DMA_BUF_SIZE 50 /* 每个ADC通道的 DMA采集次数 */
#define ADC_CH_NUM 5 /* ADC 通道数量 */
#define ADC_SUM ADC_CH_NUM * ADC_DMA_BUF_SIZE /* 总采集次数 */
/* ADC单通道/多通道 DMA采集 DMA通道相关 定义
* 注意: 这里我们的通道还是使用上面的定义.
*/
#define ADC_ADCX_DMASx DMA1_Channel1
#define ADC_ADCX_DMASx_REQUEST DMA_REQUEST_ADC1
#define ADC_ADCX_DMASx_IRQn DMA1_Channel1_IRQn
#define ADC_ADCX_DMASx_IRQHandler DMA1_Channel1_IRQHandler
这里主要分为三部分,第一部分ADC通道所对应的IO以及时钟使能的宏定义;第二部分为本例程所使用的ADC通道宏定义,第三部分为ADC相关的DMA宏定义以及ADC的转换次数的宏定义。接着看下ADC的初始化程序,如下:
/**
* @brief ADC初始化函数
* @note 本函数支持ADC1/ADC2任意通道, 但是不支持ADC3
* 我们使用12位精度, ADC采样时钟=170/4 = 42.5M,
转换时间为:采样周期+ 12.5个ADC周期
* 设置最大采样周期: 640.5, 则转换时间 = 653 个ADC周期 = 0.15us
* @param 无
* @retval 无
*/
void adc_init(void)
{
g_adc_handle.Instance = ADC_ADCX;
/* 4分频,ADCCLK = SYSCLK/4 = 170/4 = 42.5Mhz */
g_adc_handle.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
g_adc_handle.Init.Resolution = ADC_RESOLUTION_12B; /* 12位模式 */
g_adc_handle.Init.GainCompensation = 0; /* 不需要增益补偿 */
g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 右对齐 */
g_adc_handle.Init.ScanConvMode = ADC_SCAN_ENABLE; /* 扫描模式 */
g_adc_handle.Init.EOCSelection = ADC_EOC_SEQ_CONV; /* 规则序列转换结束标志 */
g_adc_handle.Init.LowPowerAutoWait = DISABLE; /* 禁用低功耗延迟模式 */
g_adc_handle.Init.ContinuousConvMode = ENABLE; /* 使能连续转换 */
/* ADC_CH_NUM个转换在规则序列中 */
g_adc_handle.Init.NbrOfConversion = ADC_CH_NUM;
/* 禁止不连续采样模式 */
g_adc_handle.Init.DiscontinuousConvMode = DISABLE;
g_adc_handle.Init.NbrOfDiscConversion = 0; /* 不连续采样通道数为0 */
g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */
/* 使用软件触发 */
g_adc_handle.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
g_adc_handle.Init.DMAContinuousRequests = ENABLE; /* 开启DMA请求 */
/* 数据溢出时覆盖ADC数据 */
g_adc_handle.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
g_adc_handle.Init.OversamplingMode = DISABLE; /* 禁用过采样功能 */
HAL_ADC_Init(&g_adc_handle); /* 初始化 */
/* 以单端模式运行ADC校准 */
HAL_ADCEx_Calibration_Start(&g_adc_handle, ADC_SINGLE_ENDED);
}
该函数将ADC设置为软件触发连续转换,并配置了ADC采集的通道数等等信息,关于IO的初始化在回调函数HAL_ADC_MspInit中执行,感兴趣可以打开源码查看,接着我们看下DMA的初始化函数,内容如下:
/**
* @brief ADC DMA读取 初始化函数
* @note 本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置
* @param par : 外设地址
* @param mar : 存储器地址
* @retval 无
*/
void adc_dma_init(void)
{
__HAL_RCC_DMAMUX1_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA配置 */
g_dma_adc_handle.Instance = ADC_ADCX_DMASx; /* 设置DMA通道*/
g_dma_adc_handle.Init.Request= ADC_ADCX_DMASx_REQUEST; /* 设置DMA请求*/
/* DIR = 1 , 外设到存储器模式 */
g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE; /*外设非增量模式*/
g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE; /*存储器增量模式*/
/* 外设数据长度:16位 */
g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
/* 存储器数据长度:16位 */
g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
g_dma_adc_handle.Init.Mode = DMA_CIRCULAR; /* 外设流控模式 */
g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
HAL_DMA_Init(&g_dma_adc_handle); /* 初始化DMA */
adc_init(); /* 初始化ADC */
/* 配置对应ADC通道 */
adc_channel_set(&g_adc_handle, ADC_VBUS_CHX, ADC_REGULAR_RANK_1,
ADC_SAMPLETIME_640CYCLES_5);
adc_channel_set(&g_adc_handle, ADC_VTEMP_CHX, ADC_REGULAR_RANK_2,
ADC_SAMPLETIME_640CYCLES_5);
adc_channel_set(&g_adc_handle, ADC_AMPU_CHX, ADC_REGULAR_RANK_3,
ADC_SAMPLETIME_640CYCLES_5);
adc_channel_set(&g_adc_handle, ADC_AMPV_CHX, ADC_REGULAR_RANK_4,
ADC_SAMPLETIME_640CYCLES_5);
adc_channel_set(&g_adc_handle, ADC_AMPW_CHX, ADC_REGULAR_RANK_5,
ADC_SAMPLETIME_640CYCLES_5);
__HAL_LINKDMA(&g_adc_handle, DMA_Handle,g_dma_adc_handle);
/* 设置DMA中断优先级为3,子优先级为3 */
HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 3, 3);
HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn); /* 使能DMA中断 */
HAL_ADC_Start_DMA(&g_adc_handle, (uint32_t*)&g_adc_dma_buf,
ADC_DMA_BUF_SIZE); /* 开始DMA数据传输 */
}
首先初始化DMA的配置,并调用ADC初始化函数。DMA存储的地址就是数组g_adc_dma_buf,传输的数据量大小为:ADC_DMA_BUF_SIZE ,如果对于ADC与DMA的配置部分还不熟悉,大家可以回顾下4.4小节。然后我们还配置了ADC的转换顺序,1~5分别对应:电源电压、温度、U相电压、V相电压、W相电压,并且采样周期我们均配置最长的ADC_SAMPLETIME_640CYCLES_5,这样会使采样的ADC值更为准确。
接着来到ADC转换完成中断回调函数用于计算ADC平均值,程序如下:
/**
* @brief ADC转换完成的回调函数
* @param 无
* @retval 无
*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if (hadc->Instance == ADC1)
{
HAL_ADC_Stop_DMA(&g_adc_handle); /* 关闭DMA转换 */
/* ADC数值转换 */
calc_adc_val(g_adc_dma_buf, g_adc_val, ADC_CH_NUM, ADC_DMA_BUF_SIZE);
HAL_ADC_Start_DMA(&g_adc_handle, (uint32_t *)&g_adc_dma_buf,
(uint32_t)(ADC_CH_NUM * ADC_DMA_BUF_SIZE)); /* 再启动DMA转换 */
}
}
在ADC中断回调函数中,先关闭DMA转换,然后调用calc_adc_val函数计算ADC平均值(滤波),calc_adc_val函数我们后面再介绍。滤波后的ADC值存储到g_adc_val这个数组当中,我们将通过滤波后的ADC值来进行电压、温度以及电流的计算。计算完ADC平均值后,再次启动DMA转换。
接下来看电压、电流以及温度计算相关的程序,包括两个文件:bldc.c和bldc.h,我们这里只介绍本实验新增的内容。
首先看bldc.h头文件的几个宏定义:
/* ADC采集值 * 3.3/4.096 (mv) = 6 * ( 0.02*I ) */ #define ADC2CURT (float)(3.3f / 4.096f / 0.12f) /* ADC采集值 * 3.3/4096 (V)= POWER/(12K+12K+1K)*1K */ #define ADC2VBUS (float)(3.3f * 25 / 4096)
这里定义的是电流、电压计算相关的宏,其中涉及的公式是根据我们前面介绍的电流、电压采集电路原理得出的,当我们需要计算电流和电压的时候,直接调用相应的宏定义即可,具体的公式推导过程大家可以直接看代码的注释。
接着我们来看下ADC平均值计算函数。平均值计算函数calc_adc_val在adc.c原文件中实现,内容如下:
/**
* @brief 计算ADC的平均值(滤波)
* @param * p :代入ADC原始值
* @param * buf :存放滤波后的ADC值
* @param ch_num :采集的ADC通道数量
* @param len :每个通道的采集次数
* @note 此函数对电压、温度、电流对应的ADC值进行滤波
* @retval 无
*/
void calc_adc_val(uint16_t *p, uint16_t *buf , uint16_t ch_num, uint16_t len)
{
uint32_t temp[ADC_CH_NUM] = {0}; /* 定义一个缓存数组 */
int i , j;
for (i = 0 ; i < len ; i++) /* 根据ADC通道数循环获取,并累加 */
{
for (j = 0 ; j < ch_num ; j++) /* 将采集到的ADC值,通道进行累加*/
{
temp[j] += p[j + i * ch_num];
}
}
for (j = 0 ; j < ch_num ; j++)
{
temp[j] /= len; /* 获取平均值 */
buf[j] = temp[j]; /* 将滤波后的值存到buf里 */
}
}
该函数将5个通道采集的数据,进行软件滤波取平均并将滤波后的数据保留在入口参数指针buf的地址中。
接下来看温度值计算函数,其定义如下:
/*
Rt = Rp *exp(B*(1/T1-1/T2))
Rt 是热敏电阻在T1温度下的阻值;
Rp是热敏电阻在T2常温下的标称阻值;
exp是e的n次方,e是自然常数,就是自然对数的底数,近似等于 2.7182818;
B值是热敏电阻的重要参数,教程中用到的热敏电阻B值为3380;
这里T1和T2指的是开尔文温度,T2是常温25℃,即(273.15+25)K
T1就是所求的温度
*/
const float Rp = 10000.0f; /* 10K */
const float T2 = (273.15f + 25.0f); /* T2 */
const float Bx = 3380.0f; /* B */
const float Ka = 273.15f;
/**
* @brief 计算温度值
* @param para: 温度采集对应ADC通道的值(已滤波)
* @note 计算温度分为两步:
1.根据ADC采集到的值计算当前对应的Rt
2.根据Rt计算对应的温度值
* @retval 温度值
*/
float get_temp(uint16_t para)
{
float Rt;
float temp;
/*
第一步:
Rt = 3.3 * 4700 / VTEMP - 4700 ,其中VTEMP就是温度检测通道采集回来的电压值,VTEMP = ADC值* 3.3/4096
由此我们可以计算出当前Rt的值:Rt = 3.3f * 4700.0f / (para * 3.3f / 4096.0f ) - 4700.0f;
*/
Rt = 3.3f * 4700.0f / (para * 3.3f / 4096.0f ) - 4700.0f;
/*
第二步:
根据当前Rt的值来计算对应温度值:Rt = Rp *exp(B*(1/T1-1/T2))
*/
temp = Rt / Rp; /* 解出exp(B*(1/T1-1/T2)) temp=exp(B*(1/T1-1/T2))*/
temp = log(temp); /* 解出B*(1/T1-1/T2) ,即temp = B*(1/T1-1/T2) */
temp /= Bx; /* 解出1/T1-1/T2 ,即temp = 1/T1-1/T2 */
temp += (1.0f / T2); /* 解出1/T1 ,即temp = 1/T1 */
temp = 1.0f / (temp); /* 解出T1 ,即temp = T1 */
temp -= Ka; /* 计算T1对应的摄氏度 */
return temp; /* 返回温度值 */
}
代码中首先定义了一些关于热敏电阻的参数,各参数的含义我们在代码注释中已经详细介绍,它们的值是根据实际的热敏电阻型号来确定的,详见《NCP18XH103F03RB 热敏电阻.pdf》这个数据手册,路径:A盘→6,硬件资料 →2,芯片资料 文件夹下可以找到。
温度计算函数的实现分为两个步骤:
第一步:根据采集到的ADC值计算出当前热敏电阻阻值Rt,公式如下:
Rt = 3.3 * 4700 / VTEMP - 4700
公式中VTEMP是温度检测通道对应的电压值,结合ADC换算公式VTEMP = ADC值* 3.3/4096可得Rt= 3.3 * 4700/ (ADC值 * 3.3 / 4096 ) - 4700。
第二步:根据当前Rt的值来计算对应温度值,公式如下:
Rt = Rp * exp( B * ( 1/T1 - 1/T2 ) )
公式中的T1就是所求的温度,它的默认单位是华氏度,我们最后还需要将其转换成摄氏度。温度计算函数的实现本质上就是这两个公式的代码体现,大家一定要跟着公式去理解代码。
接着是定时器的中断回调函数,如下:
/**
* @brief 定时器中断回调
* @param htim:定时器句柄
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
uint8_t i;
static uint8_t times_count = 0; /* 定时器时间记录 */
if(htim->Instance == ATIM_TIMX_PWM) /* 55us */
{
#ifdef H_PWM_L_ON
if (g_bldc_motor1.run_flag == RUN)
{
if (g_bldc_motor1.dir == CW) /* 正转 */
{
/* 顺序6,2,3,1,5,4 */
g_bldc_motor1.step_sta = hallsensor_get_state(MOTOR_1);
}
else /* 反转 */
{
g_bldc_motor1.step_sta = 7 - hallsensor_get_state(MOTOR_1);
}
/* 判断霍尔组合值是否正常 */
if((g_bldc_motor1.step_sta <= 6)&&(g_bldc_motor1.step_sta >= 1))
{
/* 通过数组成员查找对应的函数指针 */
pfunclist_m1[g_bldc_motor1.step_sta-1]();
}
Else /* 霍尔传感器错误、接触不良、断开等情况 */
{
stop_motor1();
g_bldc_motor1.run_flag = STOP;
}
/********************* 三相电流采集 *********************/
for(i = 0; i < 3; i++)
{
/* 运动状态ADC值 - 停机状态ADC值 = 实际作用ADC值 */
adc_val_m1[i] = g_adc_val[i + 2];
adc_amp[i] = adc_val_m1[i]-
adc_amp_offset[i][ADC_AMP_OFFSET_TIMES];
if(adc_amp[i] >= 0) /* 去除反电动势引起的负电流数据 */
{
adc_amp_un[i] = adc_amp[i];
}
}
/* 运算母线电流(母线电流为任意两个有开关动作的相电流之和) */
if(g_bldc_motor1.step_sta == 0x05)
{ /* UV */
adc_amp_bus= (adc_amp_un[0] + adc_amp_un[1]) * ADC2CURT;
}
else if(g_bldc_motor1.step_sta == 0x01)
{ /* UW */
adc_amp_bus= (adc_amp_un[0] + adc_amp_un[2]) * ADC2CURT;
}
else if(g_bldc_motor1.step_sta == 0x03)
{ /* VW */
adc_amp_bus= (adc_amp_un[1] + adc_amp_un[2]) * ADC2CURT;
}
else if(g_bldc_motor1.step_sta == 0x02)
{ /* UV */
adc_amp_bus= (adc_amp_un[0] + adc_amp_un[1]) * ADC2CURT;
}
else if(g_bldc_motor1.step_sta == 0x06)
{ /* WU */
adc_amp_bus= (adc_amp_un[0] + adc_amp_un[2]) * ADC2CURT;
}
else if(g_bldc_motor1.step_sta == 0x04)
{ /* WV */
adc_amp_bus= (adc_amp_un[2] + adc_amp_un[1]) * ADC2CURT;
}
}
#endif
}
else if(htim->Instance == BTIM_TIMX_INT)
{
/***************** 采集电机停机状态下的偏置电压 ******************/
times_count++;
if (g_bldc_motor1.run_flag == STOP)
{
uint32_t avg[3] = {0,0,0};
/* 获取电机停机状态下的三相电流 U */
adc_amp_offset[0][adc_amp_offset_p] = g_adc_val[2];
adc_amp_offset[1][adc_amp_offset_p] = g_adc_val[3]; /* V */
adc_amp_offset[2][adc_amp_offset_p] = g_adc_val[4]; /* W */
adc_amp_offset_p++;
/* 如果溢出,从头开始计数 */
NUM_CLEAR(adc_amp_offset_p, ADC_AMP_OFFSET_TIMES);
for (i = 0; i < ADC_AMP_OFFSET_TIMES; i++)
{
avg[0] += adc_amp_offset[0][i]; /* 各相数值累加 */
avg[1] += adc_amp_offset[1][i];
avg[2] += adc_amp_offset[2][i];
}
for (i = 0; i < 3; i++)
{
avg[i] /= ADC_AMP_OFFSET_TIMES; /* 取平均 */
adc_amp_offset[i][ADC_AMP_OFFSET_TIMES] = avg[i]; /* 赋值 */
}
}
}
}
关于BLDC的基础驱动部分与上一章一致,不在赘述;这里主要讲下母线电流的采集。
注意:在电流采集电路的原理介绍中,我们有提到抬升电压的值为1.25V。在实际的应用中,为了使电流采集的值更加精确,我们不是直接减掉该抬升电压,因为驱动板一上电,本身是带有基准电压的,所以会造成电压不准确,所以需在电机还未开始旋转时检测当前的电压值,即在定时器6更新中断里所操作的内容:首先判断电机是否停止,是的话将此时的三相电流所对应的ADC通道采集的ADC值进行累加取平均,即软件滤波,此时滤波后的值才是电机未启动时的基准电压;
接着当电机旋转时采集此时的三相电压,实际的相电压 = 旋转时电压 – 基准电压;通过该算法求得的即为相电压。母线电压为任意导通的两相电压之和,在乘以前面原理介绍的系数即可换算出电流值。
最后是main函数:
int main(void)
{
uint8_t key, t;
char buf[32];
int16_t pwm_duty_temp = 0;
float current[3] = {0.0f};
float current_lpf[4] = {0.0f};
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(85, 2, 2, 4, 8); /* 设置时钟,170Mhz */
delay_init(170); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
key_init(); /* 初始化按键 */
lcd_init(); /* 初始化LCD */
/* 中断频率18K,约55us, 频率不能过慢或过快,否则都会导致电机堵转 */
bldc_init((170000 / 18) - 1, 0);
btim_timx_int_init(1000 - 1, 170 - 1); /* 基本定时器初始化,1ms中断一次*/
bldc_ctrl(MOTOR_1, CW, 0); /* 初始无刷电机接口1速度 */
adc_dma_init(); /* ADC初始化 */
/* 显示提示信息 */
lcd_show_string(10, 10, 239, 24, 24, "BLDC Motor Test", RED);
lcd_show_string(10, 40, 239, 16, 16, "KEY0:Start forward", RED);
lcd_show_string(10, 60, 239, 16, 16, "KEY1:Start backward", RED);
lcd_show_string(10, 80, 239, 16, 16, "KEY2:Stop", RED);
printf("按下KEY0 开始正转加速\r\n");
printf("按下KEY1 开始反转加速\r\n");
printf("按下KEY2 停止电机\r\n");
while (1)
{
t++;
if (t % 20 == 0)
{
sprintf(buf,"Power:%.3fV ", g_adc_val[0]*ADC2VBUS);
lcd_show_string(10, 100, 200, 16, 16, buf, BLUE);
sprintf(buf,"Temp:%.1fC ", get_temp(g_adc_val[1]));
lcd_show_string(10, 120, 200, 16, 16, buf, BLUE);
LED0_TOGGLE(); /* LED0(红灯) 翻转 */
current[0] = adc_amp_un[0]* ADC2CURT; /* 计算出三相电流值,U */
current[1] = adc_amp_un[1]* ADC2CURT; /* 计算出三相电流值,V */
current[2] = adc_amp_un[2]* ADC2CURT; /* 计算出三相电流值,W */
/*一阶数字滤波 滤波系数0.1 用于显示*/
FirstOrderRC_LPF(current_lpf[0], current[0], 0.1f); /* U相电流 */
FirstOrderRC_LPF(current_lpf[1], current[1], 0.1f); /* V相电流 */
FirstOrderRC_LPF(current_lpf[2], current[2], 0.1f); /* W相电流 */
FirstOrderRC_LPF(current_lpf[3], adc_amp_bus, 0.1f);/* 母线电流 */
if(g_bldc_motor1.run_flag == STOP) /* 停机的电流显示 */
{
current_lpf[0] = 0;
current_lpf[1] = 0;
current_lpf[2] = 0;
current_lpf[3] = 0;
}
/* LCD显示提示信息 */
sprintf(buf,"Amp U:%.3fmA ", (float)current_lpf[0]);
lcd_show_string(10, 140, 200, 16, 16, buf, BLUE);
sprintf(buf,"Amp V:%.3fmA ", (float)current_lpf[1]);
lcd_show_string(10, 160, 200, 16, 16, buf, BLUE);
sprintf(buf,"Amp W:%.3fmA ", (float)current_lpf[2]);
lcd_show_string(10, 180, 200, 16, 16, buf, BLUE);
sprintf(buf,"Amp Bus:%.3fmA ", (float)current_lpf[3]);
lcd_show_string(10, 200, 200, 16, 16, buf, BLUE);
/* 串口打印信息 */
printf("Valtage:%.1fV \r\n", g_adc_val[0] * ADC2VBUS);
printf("Temp:%.1fC \r\n", get_temp(g_adc_val[1]));
printf("U相电流为:%.3fmA\r\n", (current_lpf[0]));
printf("V相电流为:%.3fmA\r\n", (current_lpf[1]));
printf("W相电流为:%.3fmA\r\n", (current_lpf[2]));
printf("母线电流为:%.3fmA\r\n", (current_lpf[3]));
printf("\r\n");
}
key = key_scan(0);
if (key == KEY0_PRES) /* 按下KEY0设置比较值+500*/
{
pwm_duty_temp += 500;
if (pwm_duty_temp >= MAX_PWM_DUTY / 2) /* 限速 */
pwm_duty_temp = MAX_PWM_DUTY / 2;
if (pwm_duty_temp > 0) /* 通过判断正负设置旋转方向*/
{
g_bldc_motor1.pwm_duty = pwm_duty_temp;
g_bldc_motor1.dir = CW;
}
else
{
g_bldc_motor1.pwm_duty = -pwm_duty_temp;
g_bldc_motor1.dir = CCW;
}
g_bldc_motor1.run_flag = RUN; /* 开启运行 */
start_motor1(); /* 开启运行 */
}
else if (key == KEY1_PRES) /* 按下KEY1设置比较值-500*/
{
pwm_duty_temp -= 500;
if (pwm_duty_temp <= -MAX_PWM_DUTY / 2)
pwm_duty_temp = -MAX_PWM_DUTY / 2;
if (pwm_duty_temp < 0) /*通过判断正负号设置旋转方向*/
{
g_bldc_motor1.pwm_duty = -pwm_duty_temp;
g_bldc_motor1.dir = CCW;
}
else
{
g_bldc_motor1.pwm_duty = pwm_duty_temp;
g_bldc_motor1.dir = CW;
}
g_bldc_motor1.run_flag = RUN; /* 开启运行 */
start_motor1(); /* 运行电机 */
}
else if (key == KEY2_PRES) /* 按下KEY2关闭电机 */
{
stop_motor1(); /* 停机 */
g_bldc_motor1.run_flag = STOP; /* 标记停机 */
pwm_duty_temp = 0; /* 数据清0 */
g_bldc_motor1.pwm_duty = 0;
}
delay_ms(10);
}
}
主函数主要添加了adc初始化函数adc_dma_init,对ADC以及DMA进行初始化。在while循环里面,除了有感方波驱动的内容,我们还利用LDC以及串口1来打印电压、温度以及电流的值,计算过程以及在原理部分讲的很详细了,大家可以原理分析部分。
23.4 下载验证
硬件连接好之后下载程序到开发板,液晶屏和串口都会显示程序计算的电压、电流等信息,点击按键KEY0一次可以看到定时器比较值递增500(加速),电机开始旋转,点击一次按键KEY1可以看到定时器比较值递减500(减速),点击按键KEY2可以随时停止无刷电机旋转,并在LCD以及串口显示ADC所采集的电源电压、驱动板温度、以及三相电流。大家可以自行下载程序并点击按键测试效果。

图23.4.1 模拟信号数据
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3117053/ ,如需转载,请注明出处,否则将追究法律责任。