前面在学习PID的时候挖了一个交流电力控制的坑,现在也终于是忙完了,现在来填上
交流Q_qun:659512171
目录
(1),打开STM32CubeMX,新建工程,选择好MCU后进入配置界面:
一,基础知识:
1,什么是交流电力控制:
交流电力控制是指对交流电力系统中的电流、电压、功率等参数进行控制和调节,以实现对电力系统运行状态和负荷分配的控制。它涵盖了电力系统中的各种控制行为,包括调频调压、负荷调节、电力质量控制等。其下依据是否改变频率又可以再分出一个变频电路,今天只说最基本的交流调压,调功电路。
2,实现原理:
(1),交流调压:
交流调压主要使用的是相控法,在前面做PID的时候提到过一句,相控法也就是对交流信号的正半周期和负半周期的导通角控制来实现对负载两端等效电压的控制,由于波形图看起来像是将正弦波切成一段一段的,所以也称之为斩控法。
值得一提,交流调压也可以应用于交流调功,毕竟初中就学过 P=UI,不过若是后面的负载功率很大,可能会产生高次谐波污染电网(其实正常都用不到这么大功率)。
(2),交流调功:
交流调功的原理在前面做PID的时候也简要介绍过,常用零位控制(简称零控法,也叫周波控制),其原理是将交流电分为多个周期(比如每1800°为一个周期),在一个周期内负载与电源接通几个周波再断开几个周波,通过控制接通周波与断开周波之比控制功率。应用简单且不会产生谐波干扰,但是控制精度有所降低。
(3),过零检测:
过零检测电路的种类很多,最简单的是整流桥+光耦,但也会有一个问题,那就是光耦的输出信号属于平滑的电压变化,不能算做方波,直接输入到单片机里无法被沿触发识别(ADC中断可以)。
为了解决这个问题,一般在光耦后面再加一个三极管使输出信号变为方波(顺带反相一下)。
当然,还有个更简单粗暴的方法,那就是选用带过零的可控硅光耦(成本要高一些),然后单片机就不用管了,直接输出就行。
(4),元器件:
在元件选用上,一般使用双向可控硅(也叫双向晶闸管)充当控制元件,可控硅有一个保持电流,所以在进行相位控制调压时,不能简单的用一个PWM方波叠加上去,而是要用过零信号进行同步。另外一点,可控硅的光耦是专用的,内置一个小功率的可控硅用于驱动。
(5),程序实现思路:
对于零控法,先用宏定义定义出一个周期(使用过零计数值,即多少次过零算一个周期),然后用需求功率百分数乘以周期并取整,就得出了需求导通的周期数,在GPIO中断中比较是否满足输出条件,满足就输出导通脉冲(不是特别好描述清楚,后面边写边讲吧)。
相控法就比较麻烦了,因为相控法需要根据需求的电压调整开启的延时,但是我们都知道,中断中是不可以使用延时的,所以就需要配置一个定时器中断来进行延时开启。
二,电路搭建:
这里我使用的是 BTA16可控硅,直接上电路图:
在洞洞板上搭建实验电路(或者可以单独打出来PCB,我把这个设计为了一个单独的模块),通电测试过零波形正常,可控硅工作正常,进入下一步。
三,软件设计:
1,STM32CubeMX配置:
(1),打开STM32CubeMX,新建工程,选择好MCU后进入配置界面:
(2),配置RCC和调试:
(3),配置定时器和GPIO:
这里预分频设置为720(减一不多说) ,自动重装值可以先不管,因为就是通过调整这个参数来控制定时器中断从而实现延时开启可控硅的,主频为72MHz,分频后频率也就为 100KHz,自动重装值每增加一,中断延时的时长增加 10us
开启定时器中断:
开启两个GPIO, 一个为中断,一个为常规输出,并将中断命名为 Zreo,输出命名为 CTR,中断输入设置为上升沿触发,输出设置为推挽输出,都设置为下拉模式。
开启GPIO的中断:
(4),配置时钟树:
(5),配置项目,生成初始化代码:
然后点击右上角的 GENERATE CODE生成项目,点击 Open Project用Keil打开项目
2,Keil编程:
(1),打开 main.c,添加测试代码:
在进行开发时,模块化地测试各部分功能是个好习惯,首先,把ST-Link设置一下:
另外,如果出现烧录程序出错(不一定是烧不进去,可能是PC端无报错但是运行不正常),可以试试在 Pack中取消勾选包(就是取消Enable复选项)
然后,在 main.c中添加测试程序,主要是几个中断的测试:
定义变量:
/* USER CODE BEGIN PV */
int i = 0;
int key = 0;
/* USER CODE END PV */
编写中断回调:
/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim == &htim4){
if(key){
i++;
}
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_PIN){
if(GPIO_PIN&GPIO_PIN_0){
key = !key;
}
}
/* USER CODE END 0 */
初始化定时器,添加GPIO测试程序:
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM4_Init();
/* USER CODE BEGIN 2 */
__HAL_TIM_SET_AUTORELOAD(&htim4,100);
HAL_TIM_Base_Start_IT(&htim4);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(i>=500){
HAL_GPIO_TogglePin(CTR_GPIO_Port,CTR_Pin);
i=0;
}
}
/* USER CODE END 3 */
}
这里把定时器自动重装值设置为了 1000,这样每次中断间隔为 1000us也就是 1ms
编译运行,在 PA0上制造一个电平跳变(用导线触碰一下 3.3v再拿开),观察到在接在PA2上的LED以 1Hz的频率闪烁,说明这三个主要程序模块都没有出现问题,连接好电路(PA0接ZERO,PA2接CTR,3.3和GND不用说了,再接上交流电源和负载),接下来就可以进行程序的开发了。
(2),交流调功:
先看代码:
这个定时器中断的作用是产生脉冲信号
过零中断
在这里将周期定位了50(即Cycle宏定义),对于50Hz的交流电而言,时间周期也就是0.5s(50Hz在1s内有100次过零)。通过比较过零计数值(即zero变量)与控制值(即ctr变量)来决定是否开启这一周期。控制值由功率因数(即power变量)与周期值乘积得到。当然,相信你也发现了,这样做不能将导通的周波平均分到每个周期内,这进一步导致了精度下降,所以我使用了一点点优化,用数组表示一个周期,将导通的周波尽量平均的分到一个周期内:
由于需要清空数组,为了减小时间复杂度,我用了 memcpy函数和一个临时数组清空 ctr数组,所以需要引用 string.h这个文件
如图,这里使用了 memcpy和为0的临时数组快速清空 ctr数组
这一段周波分散程序的测试结果如下
可以看到这一段程序成功地将导通的周波分散到整个周期内了
整个 main.c:
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "string.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define Cycle 50 //周期值
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
int ctr[Cycle] = {0}; //控制值
int zero = 0; //过零计数值
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
static int flag = 0;
if(htim == &htim4){
if(flag){
flag=0; //恢复拉高电平的模式
HAL_TIM_Base_Stop_IT(&htim4); //关闭定时器
__HAL_TIM_SET_COUNTER(&htim4,0); //清空计数器
HAL_GPIO_WritePin(CTR_GPIO_Port,CTR_Pin,GPIO_PIN_RESET); //用于拉低电平,产生脉冲
}else{
flag=1; //进入拉低电平的模式
__HAL_TIM_SET_AUTORELOAD(&htim4,20); //设置脉冲延时
HAL_GPIO_WritePin(CTR_GPIO_Port,CTR_Pin,GPIO_PIN_SET); //拉高电平
__HAL_TIM_SET_COUNTER(&htim4,0); //清空计数器
}
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_PIN){
if(GPIO_PIN&GPIO_PIN_0){
if(ctr[zero]==1){
__HAL_TIM_SET_COUNTER(&htim4,0); //清空计数器
__HAL_TIM_SET_AUTORELOAD(&htim4,1); //设置自动重装值
HAL_TIM_Base_Start_IT(&htim4); //开启定时器,输出脉冲
}
zero++;
if(zero>=Cycle){
zero=0;
}
}
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM4_Init();
/* USER CODE BEGIN 2 */
__HAL_TIM_SET_AUTORELOAD(&htim4,100);
HAL_TIM_Base_Start_IT(&htim4);
//这里先开启一次防止后续使用出现问题
double power = 0.5; //定义功率因数
double last;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(power!=last){
last = power;
int clear_tmp[Cycle] = {0};
memcpy(ctr,clear_tmp,sizeof(ctr));
for(double i = 0;i < Cycle;i += 1/power){
ctr[(int)i] = 1;
}
}
HAL_Delay(500);
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
(3),交流调压:
直接上代码:
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "tim.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "string.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define Mode 1 //调功(0),调压(1)的宏定义
#if Mode
/*调压:*/
#define Frequency 50 //交流电频率
#else
/*调功:*/
#define Cycle 50 //周期值
#endif
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
#if Mode
/*调压:*/
int delay = 0; //延时值
#else
/*调功:*/
int ctr[Cycle] = {0}; //控制值
int zero = 0; //过零计数值
#endif
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
static int flag = 0;
if(htim == &htim4){
if(flag){
flag=0; //恢复拉高电平的模式
HAL_TIM_Base_Stop_IT(&htim4); //关闭定时器
__HAL_TIM_SET_COUNTER(&htim4,0); //清空计数器
HAL_GPIO_WritePin(CTR_GPIO_Port,CTR_Pin,GPIO_PIN_RESET); //用于拉低电平,产生脉冲
}else{
flag=1; //进入拉低电平的模式
__HAL_TIM_SET_AUTORELOAD(&htim4,20); //设置脉冲延时
HAL_GPIO_WritePin(CTR_GPIO_Port,CTR_Pin,GPIO_PIN_SET); //拉高电平
__HAL_TIM_SET_COUNTER(&htim4,0); //清空计数器
}
}
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_PIN){
if(GPIO_PIN&GPIO_PIN_0){
#if Mode
__HAL_TIM_SET_COUNTER(&htim4,0); //清空计数器
__HAL_TIM_SET_AUTORELOAD(&htim4,delay);
HAL_TIM_Base_Start_IT(&htim4); //开启定时器,输出脉冲
#else
if(ctr[zero]==1){
__HAL_TIM_SET_COUNTER(&htim4,0); //清空计数器
__HAL_TIM_SET_AUTORELOAD(&htim4,1); //设置自动重装值
HAL_TIM_Base_Start_IT(&htim4); //开启定时器,输出脉冲
}
zero++;
if(zero>=Cycle){
zero=0;
}
#endif
}
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM4_Init();
/* USER CODE BEGIN 2 */
__HAL_TIM_SET_AUTORELOAD(&htim4,100);
HAL_TIM_Base_Start_IT(&htim4);
//这里先开启一次防止后续使用出现问题
double power = 0.5; //定义功率/电压因数
double last;
#if Mode
int Pre = 100000/Frequency/2 - 20; //计算每180°相位的时值并为脉冲预留时值
#endif
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(power!=last){
#if Mode
delay = (double)((1-power)*Pre); //计算延时值
#else
last = power;
int clear_tmp[Cycle] = {0};
memcpy(ctr,clear_tmp,sizeof(ctr));
for(double i = 0;i < Cycle;i += 1/power){
ctr[(int)i] = 1;
}
#endif
}
HAL_Delay(500);
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
这里我使用了宏定义来切换调功和调压,使用映射法计算延时值,需求更高精度的可以使用查表+双线性插值算法。
对于交流调压,有一点要注意,因为调压依靠的是延时,过零的沿触发不管是上升沿还是下降沿都会造成触发的偏早或偏晚,所以需要尽力优化硬件电路,并根据电路实际对延时进行一定补偿。
四,总结:
交流调功调压其实并不困难,在很多场合也有应用,这个模板可以很方便的移植到其它的项目上去。
程序在群里也有
感觉还不错就点点关注吧