41. 呼吸灯与SPWM波

本章讲解的实验工程目录为:固件库例程呼吸灯与SPWM波:TIM—单色呼吸灯、全彩呼吸灯、输出SPWM波。

学习本章需要全彩LED灯章节为基础。

41.1. 呼吸灯简介

呼吸灯,就是指灯光设备的亮度随着时间由暗到亮逐渐增强,再由亮到暗逐渐衰减,很有节奏感地一起一伏,就像是在呼吸一样,因而被广泛应用于手机、 电脑等电子设备的指示灯中,冰冷的电子设备应用呼吸灯后,顿时增添了几分温暖。

41.2. 呼吸灯与PWM控制原理

呼吸的特性是一种类似图 指数曲线 中的指数曲线过程,吸气是指数上升过程,呼气是指数下降过程,成年人吸气呼气整个过程持续约3秒。

指数曲线

要控制LED灯达到呼吸灯的效果,实际上就是要控制LED灯的亮度拟合呼吸特性曲线。前面控制全彩LED灯时,通过控制脉冲的占空比来调整各个通道LED灯的亮度, 从而达到混色的效果。若控制脉冲的占空比在3秒的时间周期内按呼吸特性曲线变化,那么就可以实现呼吸灯的效果了。

这种使用脉冲占空比拟合不同波形的方式称为PWM(脉冲宽度调制)控制技术——通过对一系列脉冲的宽度进行调制,来等效地获得所需要波形(含形状和幅值)。 PWM控制的基本原理为:冲量相等而开头不同的窄脉冲加在具有惯性的环节上时,其效果基本相同。其中冲量指窄脉冲的面积;效果相同指环节输出响应波形基本相同。

例如:可以用一系列等幅不用一系列等幅不等宽的脉冲来代替一个正弦半波,见图 PWM等效正弦波

  • 把正弦半波 N 等分,可看成 N 个彼此相连的脉冲序列,宽度相等,但幅值不等;

  • 用矩形脉冲代替,各个矩形脉冲等幅,不等宽,中点重合,脉冲宽度按正弦规律变化,脉冲的总面积(冲量)与正弦半波相等。

PWM等效正弦波

这种脉冲波形被称为SPWM 波形——脉冲宽度按正弦规律变化而和正弦波等效的 PWM 波形。SPWM是一种非常典型的PWM波形,它在数字电路控制中应用非常广泛, 如果使用低通滤波器,可以由SPWM波得到其等效的连续正弦半波。

要改变等效输出正弦波幅值,按同一比例改变各脉冲宽度即可。

若把拟合的波形改成呼吸特性曲线,即可得到控制呼吸灯使用的PWM波形,要生成拟合的PWM波形,通常使用计算法和调制法:

(1) 计算法:根据拟合波形的频率、幅值和半周期脉冲数,准确计算 PWM 波各脉冲宽度和间隔, 据此控制开关器件的通断,就可得到所需PWM波形;

(2) 调制法:拟合波形作调制信号,进行调制得到期望的PWM波;该方法一般采用等腰三角波为载波, 其任一点水平宽度和高度成线性关系且左右对称。载波(等腰三角波)与平缓变化的调制信号波(即要拟合的波形)相交, 在载波与信号波的交点控制器件通断,就得宽度正比于信号波幅值的脉冲,符合PWM的要求, 见图 调制法得到PWM波。相对于计算法,其处理过程计算简单。

调制法得到PWM波

在本章的各个实验中,演示如何使用计算法得到的呼吸曲线PWM波和SPWM波,并使用STM32定时器TIM的PWM功能输出波形控制LED灯,达到呼吸灯的效果。

41.3. 硬件设计

为便于讲解,本章设计了三个实验,其工程名为:TIM—单色呼吸灯、TIM—全彩呼吸灯和 TIM—输出SPWM波, 它们的简要介绍见表 各实验工程功能介绍

各实验工程功能介绍

本实验中使用的RGB灯硬件与前面全彩LED灯章节中的完全相同,见图 LED硬件原理图 ,这是一个RGB灯, 里面由红蓝绿三个小灯构成, 使用PWM控制时可以混合成256不同的颜色。本实验与LED灯基础章节的主要差异是软件中的控制方式。

LED硬件原理图

本开发板中设计的RGB灯控制引脚是经过仔细选择的,因为本实验的软件将使用STM32的定时器控制输出PWM脉冲, 然而并不是任意GPIO都具有STM32定时器的输出通道功能,所以在设计硬件时,需要根据《STM32中文数据手册》中的说明, 选择具有定时器输出通道功能的引脚来控制RGB灯 ,见图 LED引脚说明

LED引脚说明

本实验中的RGB灯使用阴极分别连接到了PF6、PF7及PF8,它们分别是定时器TIM10、11、13的通道1。在本硬件设计方案中三个引脚使用了不同的定时器, 它会给设计程序时会带来不便,若是在硬件资源分配充足的情况下,建议这三个控制引脚使用同一个定时器的不同通道。

41.4. 单色呼吸灯实验

首先以单色呼吸灯工程为例,其核心的驱动代码分别位于bsp_breath_led.c和bsp_breath_led.h文件中,可根据应用需要移植这些文件。

41.4.1. 编程要点

1) 初始化PWM输出通道,初始化PWM工作模式;

2) 计算获取PWM数据表;

3) 编写中断服务函数,在中断服务函数根据PWM数据表切换比较寄存器的值;

41.4.2. 代码分析

41.4.2.1. LED灯硬件相关宏定义

为方便迁移代码适应其它硬件设计,实验中把硬件相关的部分使用宏定义到bsp_breath_led.h文件中, 使用不同硬件设计时,修改该文件即可,见 代码清单:SPWM-1

代码清单:SPWM-1 硬件相关宏定义(bsp_breath_led.h文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#define RED_LIGHT     1
#define GREEN_LIGHT   2
#define BLUE_LIGHT    3

/*要使用什么颜色的呼吸灯,可选RED_LIGHT、GREEN_LIGHT、BLUE_LIGHT*/
#define LIGHT_COLOR   RED_LIGHT

/********************定时器通道**************************/
#if  LIGHT_COLOR == RED_LIGHT
//R 红色灯
#define BRE_TIM                         TIM10
#define BRE_TIM_CLK                   RCC_APB2Periph_TIM10
#define BRE_TIM_APBxClock_FUN        RCC_APB2PeriphClockCmd

#define BRE_TIM_IRQn                TIM1_UP_TIM10_IRQn
#define BRE_TIM_IRQHandler        TIM1_UP_TIM10_IRQHandler

/*计算说明见c文件*/
/*部分通用定时器的时钟为HCLK/4,部分为HCLK/2*/
#define BRE_TIM_PRESCALER           (470-1)


#define BRE_LED_PIN                  GPIO_Pin_6
#define BRE_LED_GPIO_PORT            GPIOF
#define BRE_LED_GPIO_CLK             RCC_AHB1Periph_GPIOF
#define BRE_LED_PINSOURCE             GPIO_PinSource6
#define BRE_LED_AF                    GPIO_AF_TIM10

//通道比较寄存器,以TIMx->CCRx方式可访问该寄存器,设置新的比较值,控制占空比
//以宏封装后,使用这种形式:BRE_TIMx->BRE_LED_CCRx ,可访问该通道的比较寄存器
#define BRE_LED_CCRx                  CCR1
#define BRE_LED_TIM_CHANNEL         TIM_Channel_1

#define  BRE_TIM_OCxInit              TIM_OC1Init     //通道选择,1~4
#define  BRE_TIM_OCxPreloadConfig    TIM_OC1PreloadConfig


#elif LIGHT_COLOR == GREEN_LIGHT
//G 绿色灯
#define BRE_TIM                         TIM11
#define BRE_TIM_CLK                   RCC_APB2Periph_TIM11
#define BRE_TIM_APBxClock_FUN        RCC_APB2PeriphClockCmd

#define BRE_TIM_IRQn                TIM1_TRG_COM_TIM11_IRQn
#define BRE_TIM_IRQHandler        TIM1_TRG_COM_TIM11_IRQHandler

/*计算说明见c文件*/
/*部分通用定时器的时钟为HCLK/4,部分为HCLK/2*/
#define BRE_TIM_PRESCALER           (470-1)


#define BRE_LED_PIN                  GPIO_Pin_7
#define BRE_LED_GPIO_PORT            GPIOF
#define BRE_LED_GPIO_CLK             RCC_AHB1Periph_GPIOF
#define BRE_LED_PINSOURCE             GPIO_PinSource7
#define BRE_LED_AF                    GPIO_AF_TIM11

//通道比较寄存器,以TIMx->CCRx方式可访问该寄存器,设置新的比较值,控制占空比
//以宏封装后,使用这种形式:BRE_TIM->BRE_LED_CCRx ,可访问该通道的比较寄存器
#define BRE_LED_CCRx                  CCR1
#define BRE_LED_TIM_CHANNEL         TIM_Channel_1

#define  BRE_TIM_OCxInit              TIM_OC1Init      //通道选择,1~4
#define  BRE_TIM_OCxPreloadConfig    TIM_OC1PreloadConfig

#elif LIGHT_COLOR == BLUE_LIGHT
//B 蓝色灯
#define BRE_TIM                         TIM13
#define BRE_TIM_CLK                   RCC_APB1Periph_TIM13
#define BRE_TIM_APBxClock_FUN        RCC_APB1PeriphClockCmd

#define BRE_TIM_IRQn                TIM8_UP_TIM13_IRQn
#define BRE_TIM_IRQHandler        TIM8_UP_TIM13_IRQHandler

/*计算说明见c文件*/
/*部分通用定时器的时钟为HCLK/4,部分为HCLK/2*/
#define BRE_TIM_PRESCALER           (235-1)


#define BRE_LED_PIN                  GPIO_Pin_8
#define BRE_LED_GPIO_PORT            GPIOF
#define BRE_LED_GPIO_CLK             RCC_AHB1Periph_GPIOF
#define BRE_LED_PINSOURCE             GPIO_PinSource8
#define BRE_LED_AF                    GPIO_AF_TIM13

//通道比较寄存器,以TIMx->CCRx方式可访问该寄存器,设置新的比较值,控制占空比
//以宏封装后,使用这种形式:BRE_TIM->BRE_LED_CCRx ,可访问该通道的比较寄存器
#define BRE_LED_CCRx                  CCR1
#define BRE_LED_TIM_CHANNEL         TIM_Channel_1

#define  BRE_TIM_OCxInit              TIM_OC1Init    //通道选择,1~4
#define  BRE_TIM_OCxPreloadConfig    TIM_OC1PreloadConfig

#endif

为方便切换LED灯的颜色,它定义了三组宏,通过修改代码中的“#define LIGHT_COLOR RED_LIGHT”语句, 可以切换使用红、绿、蓝三种颜色的呼吸灯。

在每组宏定义中,与全彩LED灯实验中的类似,定义了定时器编号、定时器时钟使能库函数、引脚重映射操作、GPIO端口和引脚号、 通道对应的比较寄存器名以及中断通道和中断服务函数名。

与全彩LED灯实验不同,本实验中定时器的比较寄存器CCRx在控制呼吸灯的单个周期内需要切换为PWM表中不同的数值,所以需要利用定时器中断。

41.4.2.2. 初始化GPIO

首先,初始化用于定时器输出通道的GPIO,见 代码清单:SPWM-2

代码清单:SPWM-2 初始化GPIO(bsp_breath_led.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @brief  配置TIM复用输出PWM时用到的I/O
* @param  无
* @retval 无
*/
static void TIMx_GPIO_Config(void)
{
    /*定义一个GPIO_InitTypeDef类型的结构体*/
    GPIO_InitTypeDef GPIO_InitStructure;

    /*开启LED相关的GPIO外设时钟*/
    RCC_AHB1PeriphClockCmd ( BRE_LED_GPIO_CLK, ENABLE);

    GPIO_PinAFConfig(BRE_LED_GPIO_PORT,BRE_LED_PINSOURCE,BRE_LED_AF);

    /*BREATH_LED1*/
    GPIO_InitStructure.GPIO_Pin = BRE_LED_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(BRE_LED_GPIO_PORT, &GPIO_InitStructure);
}

由于本实验直接使用定时器输出通道的脉冲信号控制LED灯,此处代码把GPIO相关的引脚配置成了复用推挽输出模式,后面将使用定时器控制引脚进行PWM输出。

41.4.2.3. 定义PWM表

在本工程中的bsp_breath_led.c文件定义了一个PWM表,见 代码清单:SPWM-3

代码清单:SPWM-3 工程中定义的PWM表(bsp_breath_led.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/*******bsp_breathig.c文件*********/
/* LED亮度等级 PWM表,指数曲线 ,
此表使用工程目录下的python脚本index_wave.py生成*/
uint16_t indexWave[] = {
    1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 5,
    6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 22,
    25, 28, 32, 36, 41, 47, 53, 61, 69, 79,
    89, 102, 116, 131, 149, 170, 193, 219,
    250, 284, 323, 367, 417, 474, 539, 613,
    697, 792, 901, 1024, 1024, 901, 792, 697,
    613, 539, 474, 417, 367, 323, 284, 250,
    219, 193, 170, 149, 131, 116, 102, 89,
    79, 69, 61, 53, 47, 41, 36, 32, 28, 25,
    22, 19, 17, 15, 13, 11, 10, 9, 8, 7, 6,
    5, 5, 4, 4, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1
};

//计算PWM表有多少个元素
uint16_t POINT_NUM = sizeof(indexWave)/sizeof(indexWave[0]);

该PWM表是利用本工程目录下的python脚本index_wave.py生成的,该脚本的代码见 代码清单:SPWM-4

代码清单:SPWM-4 生成PWM表的python脚本
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#! python3
#coding=utf-8

"""
Python版本:3.x
外部库:matplotlib1.5.3、numpy1.11.2

运行方式:
在命令行中输入:python index_wave.py

运行结果:
命令行中会打印计算得的各点数据,
在当前目录下会生成py_index_wave.c文件,包含上述数据,
并且会弹出描绘曲线的对话框。
"""

import matplotlib.pyplot as plt
import numpy as np
import math

#修改本变量可以更改点数,如16、32、64等
POINT_NUM = 110

#指数曲线最大为2的MAX_POWER次方
MAX_POWER = 10

# POINT_NUM 个点
x1 = np.linspace(0,MAX_POWER,POINT_NUM/2)

#f = 2^(x)
up =[]
for i in x1:
    temp = round(2**i)
    #得到升序列
    up.append( temp )

x2 = np.linspace(MAX_POWER,2*MAX_POWER,POINT_NUM/2)

#f = 2^(2*MAX_POWER-x)
down=[]
for i in x2:
    temp = round(2**(MAX_POWER*2-i))

    #得到降序列
    down.append( temp )

line = list(x1)+list(x2)
val = list(up)+list(down)

print(line)
print("*"*80)
print(list(map(int,val)))

#写入序列到文件
with open("py_index_Wave.c",'w',encoding= 'gb2312') as f:
    print(list(map(int,val)),file= f)

#绘图
plt.plot(line,val,"-o")
plt.show()

该脚本运行后会在工程目录下生成一个包含该PWM表的py_index_Wave.c文件,复制这些数据到bsp_breath_led.c文件,即可得到本工程中的indexWave数组。 实际上使用C语言也可以编写这样的脚本制作出PWM表,只是使用python脚本比较方便绘制图形而已。

该python脚本生成PWM表数据的原理,实质是按照如下函数曲线进行采样:

若0<= x <=10:

\[y = 2^{x}\]

若10< x <=20:

\[y = 2^{(20 - x)}\]

python脚本在这样的函数曲线上取110个点,即可得到上述代码中PWM表数组indexWave,绘制其图形如图 绘制PWM表的图形

绘制PWM表的图形

可以看到,这个PWM表记录了呼吸特性曲线,在本实验中,PWM表的数据将会被赋值到定时器的CCRx比较寄存器,从而控制输出占空比呈呼吸特性曲线变化的PWM波。

至于计算PWM表时,为什么选择采样110个点以及为什么表中的最大元素为1024,需要结合下面定时器和中断服务函数中的配置来理解。

41.4.2.4. 定时器PWM配置

本实验的定时器的PWM输出模式配置,见 代码清单:SPWM-5

代码清单:SPWM-5 定时器PWM配置(bsp_breath_led.c文件)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
* @brief  通用定时器 TIMx,x[2,3,4,5]中断优先级配置
* @param  无
* @retval 无
*/
static void TIMx_NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    // 设置中断来源
    NVIC_InitStructure.NVIC_IRQChannel = BRE_TIM_IRQn;

    // 设置抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

    // 设置子优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}



/**
* @brief  配置TIM输出的PWM信号的模式,如周期、极性
* @param  无
* @retval 无
*/

static void TIM_Mode_Config(void)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;

    // 开启TIMx_CLK,x[2,3,4,5]
    BRE_TIM_APBxClock_FUN(BRE_TIM_CLK, ENABLE);

    /* 基本定时器配置 ,配合PWM表点数、中断服务函数中的period_cnt循环次数设置*/

    /* 设置使得整个呼吸过程为3秒左右即可达到很好的效果 */

    //要求:
    //TIM_Period:与PWM表中数值范围一致
    //TIM_Prescaler:越小越好,可减轻闪烁现象
    //PERIOD_CLASS:中断服务函数中控制单个点循环的次数,调整它可控制拟合曲线的周期
    //POINT_NUM:PWM表的元素,它是PWM拟合曲线的采样点数

    /*************本实验中的配置***************/
    /***********************************************
    #PWM点数
    POINT_NUM = 110

    #周期倍数
    PERIOD_CLASS = 10

    #幅值等级
    AMPLITUDE_CLASS = 1

    #定时器定时周期
    TIMER_TIM_Period = 2**10

    #APB1定时器分频
    TIMER_TIM_Prescaler1 = 235
    #APB2定时器分频
    TIMER_TIM_Prescaler2 = 470

    #STM32系统时钟频率和周期
    f_hclk = 168000000

    #定时器计时频率
    f_pclk1 = f_hclk/2
    t_pclk1 = 1/f_pclk1

    f_pclk2 = f_hclk
    t_pclk2 = 1/f_pclk2

    #APB1定时器update事件周期
    t_timer1 = t_pclk1*TIMER_TIM_Prescaler1*TIMER_TIM_Period

    #APB1每个PWM点的时间
    T_Point1 = t_timer1 * PERIOD_CLASS * AMPLITUDE_CLASS

    #APB1整个呼吸周期
    T_Up_Down_Cycle1 = T_Point1 * POINT_NUM

    print ("APB1总线定时器呼吸周期:",T_Up_Down_Cycle1)


    #APB2定时器update事件周期
    t_timer2 = t_pclk2*TIMER_TIM_Prescaler2*TIMER_TIM_Period

    #APB2每个PWM点的时间
    T_Point2 = t_timer2 * PERIOD_CLASS * AMPLITUDE_CLASS

    #APB2整个呼吸周期
    T_Up_Down_Cycle2 = T_Point2 * POINT_NUM

    print ("APB2总线定时器呼吸周期:",T_Up_Down_Cycle2)

    #运行结果:

    APB1总线定时器呼吸周期:3.151238
    APB2总线定时器呼吸周期:3.151238
    ************************************************************/
    //当定时器从0计数到 TIM_Period+1  ,为一个定时周期
    TIM_TimeBaseStructure.TIM_Period = (1024-1);

    TIM_TimeBaseStructure.TIM_Prescaler = BRE_TIM_PRESCALER;  //设置预分频
    //设置时钟分频系数:不分频(这里用不到)
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //向上计数模式

    // 初始化定时器TIMx, x[2,3,4,5]
    TIM_TimeBaseInit(BRE_TIM, &TIM_TimeBaseStructure);


    /*PWM模式配置*/
    /* PWM1 Mode configuration: Channel1 */
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;     //配置为PWM模式

    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //使能输出
    TIM_OCInitStructure.TIM_Pulse = 0;          //设置初始PWM脉冲宽度为0
    //当定时器计数值小于CCR1_Val时为低电平,LED灯亮
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;

    BRE_TIM_OCxInit(BRE_TIM, &TIM_OCInitStructure);  //使能通道

    /*使能通道重载*/
    BRE_TIM_OCxPreloadConfig(BRE_TIM, TIM_OCPreload_Enable);

    TIM_ARRPreloadConfig(BRE_TIM, ENABLE);      //使能TIM重载寄存器ARR

    // 开启计数器中断
    TIM_ITConfig(BRE_TIM,TIM_IT_Update,ENABLE);

    // 使能计数器
    TIM_Cmd(BRE_TIM, ENABLE);
}

本配置主体与全彩LED灯实验中的类似,代码中初始化了控制RGB灯用的定时器,它被配置为向上计数, PWM通道输出也被配置成当计数器CNT的值小于输出比较寄存器CCRx的值时,PWM通道输出低电平,点亮LED灯。 在函数的最后还使能了定时器中断,每当定时器的一个计数周期完成时,产生中断,配合中断服务函数, 即可切换CCRx比较寄存器的值。

代码中的TIM_Period和TIM_Prescaler是关键配置。

其中TIMPeriod被配置为(1024-1),它控制控制定时器的定时周期,定时器的计数寄存器CNT从0开始,每个时钟会对计数器加1, 计数至1023时完成一次计数,产生中断,也就是说一共1024个计数周期,与PWM表元素中的最大值相同。若定时器的输出比较寄存器CCRx被赋值为PWM表中的元素, 即可改变输出对应占空比的PWM波,控制LED灯,如:

若CCRx=1,那么在CNT<CCRx时,通道输出低电平,LED灯亮,CNT>CCRx时,输出高电平,LED灯灭, 此时\(\frac{T_{LED\_ ON}}{T_{LED\_ OFF}} = \frac{1}{1024}\)

若CCRx=474,那么在CNT<CCRx时,通道输出低电平,LED灯亮,CNT>CCRx时,输出高电平,LED灯灭, 此时\(\frac{T_{LED\_ ON}}{T_{LED\_ OFF}} = \frac{474}{1024}\)

若CCRx=1024,那么在CNT<CCRx时,通道输出低电平,LED灯亮,CNT>CCRx时,输出高电平,LED灯灭, 此时\(\frac{T_{LED\_ ON}}{T_{LED\_ OFF}} = \frac{1024}{1024}\)

根据本工程中的PWM表更新CCRx的值,即可输出占空比呈呼吸特性曲线变化的PWM波形,达到呼吸灯的效果。

最终,拟合曲线的周期由TIMPeriod、PWM表的点数、TIM_Prescaler以及下面中断服务函数的period_cnt比较值共同决定, 本工程需要调整这些参数使得拟合曲线的周期约为3秒,从而达到较平缓的呼吸效果。

41.4.2.5. 定时器中断服务函数

定时器的中断服务函数见 代码清单:SPWM-6

代码清单:SPWM-6 定时器中断服务函数(stm32f4xx_it.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//控制输出波形的频率
__IO uint16_t period_class = 10;

//计算PWM表有多少个元素
uint16_t POINT_NUM = sizeof(indexWave)/sizeof(indexWave[0]);

/**
* @brief  This function handles TIM interrupt request.
* @param  None
* @retval None
*/
extern uint16_t indexWave[];

void  BRE_TIM_IRQHandler (void)
{
    static uint16_t pwm_index = 0;      //用于PWM查表
    static uint16_t period_cnt = 0;   //用于计算周期数

if (TIM_GetITStatus(BRE_TIM, TIM_IT_Update) != RESET) { //TIM_IT_Update
        period_cnt++;
        //根据PWM表修改定时器的比较寄存器值
        BRE_TIM->BRE_LED_CCRx = indexWave[pwm_index];

        //每个PWM表中的每个元素使用period_class次
        if (period_cnt > period_class) {
            pwm_index++;      //标志PWM表指向下一个元素

            //若PWM表已到达结尾,重新指向表头
            if ( pwm_index >=  POINT_NUM) {
                pwm_index=0;
            }

            period_cnt=0;   //重置周期计数标志
        } else {
        }

    TIM_ClearITPendingBit (BRE_TIM, TIM_IT_Update); //必须要清除中断标志位
    }
}

在中断服务函数中,包含两个静态变量period_cnt和pwm_index。

其中pwm_index比较容易理解,它用于指示当前要使用PWM表中的哪个元素,从而在“BRE_TIMx->BRE_CCRx = indexWave[pwm_index];”语句中可以给CCRx赋予正确的数值,而且当PWM表中的数据都使用一遍时,pwm_index将重新指向PWM表的开头,开始下一次呼吸循环。

在本实验的单次呼吸循环中,每个PWM表元素都会使用10次,代码中利用period_cnt变量指示当前使用的次数, 当period_cnt> period_class时(即period_cnt>10时),pwm_index才会指向下一个元素。每个PWM表元素使用多次, 主要是为了在TIMPeriod、PWM表的点数、TIM_Prescaler都固定的情况下,通过调整每个元素的重复次数可以调整整个拟合波形的周期。 如把代码中的比较值period_class改为100,每个PWM表遍历一次的时间就变为原来配置的10倍,其拟合的呼吸周期也就相应地改变了。 图 PWM波形 说明了period =3和period=1时输出的PWM波形。

PWM波形

41.4.2.6. 计算拟合波形的周期

在本工程中,TIMPeriod、PWM表的点数、TIM_Prescaler以及period_cnt都会影响到拟合曲线的周期,而在实际应用中又有如下要求:

  • TIMPeriod:定时器的计数周期,它的值必须与PWM表中的极大值相等(应用中赋值需要减1),而PWM表的极大值决定了控制的分辨率。 例如极大值为10时,PWM占空比只有10个等级,精确到0.1,当极大值为1000时,PWM占空比有1000个等级,精确到0.001。

  • TIM_Prescaler:定时器时钟分频因子,它控制定时器计数器CNT计数加1所需要的时间,它的值太大会导致输出的单个PWM波周期过长, 影响控制的动态特性。如控制LED灯时,该值太大会导致LED灯开关时间变长,闪烁明显。一般来说,该值越小越好。

  • PWM表的点数:PWM表的点数即对拟合曲线的采样点数,采样点越多,能更好地还原拟合曲线,采样点太少,可能会导致失真, 见图 对呼吸特性曲线采样110个和10个点时的情况

对呼吸特性曲线采样110个和10个点时的情况
  • period_class:周期倍数,即PWM表中每个元素的循环次数,它影响拟合曲线的周期。 当period_class=1时,可以输出本配置中周期最短的拟合曲线。

  • amplitude_class:幅值分级,在后面全彩呼吸灯和SPWM实验中,我们还会增加该变量,它可以把拟合曲线的幅值分成N个等级, 控制时可以选择按某个幅值等级进行输出。本实验中没有配置该参数,所以只能输出最大的等级,即amplitude_class=1。

以上各个参数虽然侧重点不同,但若修改其中的任何一个,最终都会影响到所拟合曲线的周期,所以在实际应用中,通常先设定好TIMPeriod、 TIM_Prescaler、PWM表的点数以及幅值等级数amplitude_class,得到适合的控制精度、动态特性拟合度以及幅值等级后, 然后再调整period_class控制拟合曲线的周期,而且period_class在程序中动态修改非常方便,不需要重置定时器和PWM表。

最终,我们把本工程配置中的拟合曲线周期计算公式概括如下:

  1. STM32系统时钟默认频率和周期:

    f_hclk = 168000000

    某些定时器使用APB1:

    f_pclk1 = f_hclk/2

    t_pclk1 = 1/f_pclk1

    某些定时器使用APB2:

    f_pclk2 = f_hclk

    t_pclk2 = 1/f_pclk2

(2) 定时器update事件周期,即定时器中断周期:

APB1定时器:t_timer1 = t_pclk1*TIMER_TIM_Prescaler1*TIMER_TIM_Period

APB2定时器:t_timer2 = t_pclk2*TIMER_TIM_Prescaler2*TIMER_TIM_Period

(3) 每个PWM点的时间:

APB1定时器:T_Point1 = t_timer1 * PERIOD_CLASS * AMPLITUDE_CLASS

APB2定时器:T_Point2 = t_timer2 * PERIOD_CLASS * AMPLITUDE_CLASS

(4) 最终,遍历PWM表的周期,即拟合曲线的周期:

APB1定时器: T_Up_Down_Cycle1 = T_Point1 * POINT_NUM

APB2定时器: T_Up_Down_Cycle2 = T_Point2 * POINT_NUM

例如,本工程配置中:

#**PWM点数**

POINT_NUM = 110

#**周期倍数**

PERIOD_CLASS = 10

#**幅值等级**

AMPLITUDE_CLASS = 1

#**定时器定时周期**

TIMER_TIM_Period = 1024

#**APB1定时器分频**

TIMER_TIM_Prescaler1 = 235

#**APB2定时器分频**

TIMER_TIM_Prescaler2 = 470

#**STM32系统时钟频率和周期**

f_hclk = 168000000

代入公式计算得:

APB1总线定时器呼吸周期:T_Up_Down_Cycle1 = 3.151238

APB2总线定时器呼吸周期:T_Up_Down_Cycle2 = 3.151238

通过公式的计算可知本工程的配置可使得输出的拟合曲线周期为3.15秒,是比较平缓的呼吸周期。

41.4.2.7. 主函数

代码清单:SPWM-7 主函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{

    /*初始化串口*/
    Debug_USART_Config();

    printf("\r\n 欢迎使用野火  STM32 F407 开发板。\r\n");
    printf("\r\n 呼吸灯例程\r\n");
    printf("\r\n RGB LED 以呼吸灯的形式闪烁\r\n ");

    /* 初始化呼吸灯 */
    BreathLED_Config();

    while (1) {
    }

}

main函数中直接调用了BreathLED_Config函数,而该函数内部又直接调用了前面讲解的GPIO和PWM配置函数:TIMx_GPIO_Config和 TIMx_Mode_Config。 初始化完成后,定时器开始工作,然后它会在中断服务函数中切换PWM数据,控制LED灯显示呼吸效果。

41.4.3. 下载验证

编译并下载本程序到开发板,给开发板上电复位,可看到LED灯显示呼吸效果。

41.5. 全彩呼吸灯及输出SPWM波实验

全彩呼吸灯例程和输出SPWM波实验的工程基本一样,只是控制使用的的PWM表不同,一个为呼吸特性曲线,另一个为正弦半波曲线, 下面讲解主要以全彩呼吸灯实验为例子。这两个工程的核心驱动代码分别位于bsp_breath_led.c和bsp_breath_led.h文件中,可根据应用需要移植这些文件。

这两个工程都是在单色呼吸灯例程拓展的,主要增加了对拟合曲线幅值等级的控制功能。

41.5.1. 编程要点

1) 在单色呼吸灯的基础上,增加PWM输出通道,三个通道分别控制红绿蓝颜色;

2) 编写中断服务函数,增加对拟合波形幅值的控制;

3) 计算获取新的PWM数据表;

41.5.2. 代码分析

下面主要以全彩呼吸灯的例程分析,输出SPWM波例程只是宏的名称和PWM表的数据不一样。

41.5.2.1. LED灯硬件相关宏定义

类似地,实验中把硬件相关的部分使用宏定义到bsp_breath_led.h文件中,使用不同硬件设计时, 修改该文件即可,见 代码清单:SPWM-8

代码清单:SPWM-8 硬件相关宏定义(bsp_breath_led.h文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/*电压幅值等级数*/
#define AMPLITUDE_CLASS 256

//控制输出波形的频率
extern __IO uint16_t period_class ;

/*PWM表中的点数*/
extern uint16_t POINT_NUM;

/********************定时器通道**************************/

//R 红色灯
#define BRE_RED_TIM                         TIM10
#define BRE_RED_TIM_CLK                   RCC_APB2Periph_TIM10
#define BRE_TIM_GPIO_CLK                  (RCC_AHB1Periph_GPIOF)
#define BRE_RED_TIM_APBxClock_FUN        RCC_APB2PeriphClockCmd

#define BRE_RED_TIM_IRQn                TIM1_UP_TIM10_IRQn
#define BRE_RED_TIM_IRQHandler        TIM1_UP_TIM10_IRQHandler

/*计算说明见c文件*/
/*部分通用定时器的时钟为HCLK/4,部分为HCLK/2*/
#define BRE_RED_TIM_PRESCALER           (22-1)

#define BRE_RED_PIN                  GPIO_Pin_6
#define BRE_RED_GPIO_PORT            GPIOF
#define BRE_RED_PINSOURCE             GPIO_PinSource6
#define BRE_RED_AF                    GPIO_AF_TIM10

//通道比较寄存器,以TIMx->CCRx方式可访问该寄存器,设置新的比较值,控制占空比
//以宏封装后,使用这种形式:BRE_TIMx->BRE_LED_CCRx ,可访问该通道的比较寄存器
#define BRE_RED_CCRx                  CCR1
#define BRE_RED_TIM_CHANNEL         TIM_Channel_1

#define  BRE_RED_TIM_OCxInit       TIM_OC1Init            //通道选择,1~4
#define  BRE_RED_TIM_OCxPreloadConfig    TIM_OC1PreloadConfig

//G 绿色灯
#define BRE_GREEN_TIM                         TIM11
#define BRE_GREEN_TIM_CLK                   RCC_APB2Periph_TIM11
#define BRE_GREEN_TIM_APBxClock_FUN        RCC_APB2PeriphClockCmd

#define BRE_GREEN_TIM_IRQn                TIM1_TRG_COM_TIM11_IRQn
#define BRE_GREEN_TIM_IRQHandler        TIM1_TRG_COM_TIM11_IRQHandler

/*计算说明见c文件*/
/*部分通用定时器的时钟为HCLK/4,部分为HCLK/2*/
#define BRE_GREEN_TIM_PRESCALER           (22-1)

#define BRE_GREEN_PIN                  GPIO_Pin_7
#define BRE_GREEN_GPIO_PORT            GPIOF
#define BRE_GREEN_PINSOURCE             GPIO_PinSource7
#define BRE_GREEN_AF                    GPIO_AF_TIM11

//通道比较寄存器,以TIMx->CCRx方式可访问该寄存器,设置新的比较值,控制占空比
//以宏封装后,使用这种形式:BRE_TIM->BRE_LED_CCRx ,可访问该通道的比较寄存器
#define BRE_GREEN_CCRx                  CCR1
#define BRE_GREEN_TIM_CHANNEL         TIM_Channel_1

#define  BRE_GREEN_TIM_OCxInit        TIM_OC1Init       //通道选择,1~4
#define  BRE_GREEN_TIM_OCxPreloadConfig    TIM_OC1PreloadConfig

//B 蓝色灯
#define BRE_BLUE_TIM                        TIM13
#define BRE_BLUE_TIM_CLK                  RCC_APB1Periph_TIM13
#define BRE_BLUE_TIM_APBxClock_FUN        RCC_APB1PeriphClockCmd

#define BRE_BLUE_TIM_IRQn               TIM8_UP_TIM13_IRQn
#define BRE_BLUE_TIM_IRQHandler         TIM8_UP_TIM13_IRQHandler

/*计算说明见c文件*/
/*部分通用定时器的时钟为HCLK/4,部分为HCLK/2*/
#define BRE_BLUE_TIM_PRESCALER            (11-1)

#define BRE_BLUE_PIN                  GPIO_Pin_8
#define BRE_BLUE_GPIO_PORT            GPIOF
#define BRE_BLUE_PINSOURCE              GPIO_PinSource8
#define BRE_BLUE_AF                   GPIO_AF_TIM13

//通道比较寄存器,以TIMx->CCRx方式可访问该寄存器,设置新的比较值,控制占空比
//以宏封装后,使用这种形式:BRE_TIM->BRE_LED_CCRx ,可访问该通道的比较寄存器
#define BRE_BLUE_CCRx                 CCR1
#define BRE_BLUE_TIM_CHANNEL          TIM_Channel_1

#define  BRE_BLUE_TIM_OCxInit     TIM_OC1Init            //通道选择,1~4
#define  BRE_BLUE_TIM_OCxPreloadConfig    TIM_OC1PreloadConfig

41.5.2.2. 初始化GPIO

首先,初始化用于定时器输出通道的GPIO,见 代码清单:SPWM-9

代码清单:SPWM-9 初始化GPIO(bsp_breath_led.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @brief  配置TIM复用输出PWM时用到的I/O
* @param  无
* @retval 无
*/
static void TIMx_GPIO_Config(void)
{
    /*定义一个GPIO_InitTypeDef类型的结构体*/
    GPIO_InitTypeDef GPIO_InitStructure;

    /*开启LED相关的GPIO外设时钟*/
    RCC_AHB1PeriphClockCmd ( BRE_TIM_GPIO_CLK, ENABLE);

    GPIO_PinAFConfig(BRE_RED_GPIO_PORT,BRE_RED_PINSOURCE,BRE_RED_AF);
    GPIO_PinAFConfig(BRE_GREEN_GPIO_PORT,BRE_GREEN_PINSOURCE,BRE_GREEN_AF);
    GPIO_PinAFConfig(BRE_BLUE_GPIO_PORT,BRE_BLUE_PINSOURCE,BRE_BLUE_AF);

    /* 配置LED灯用到的引脚 */
    //红
    GPIO_InitStructure.GPIO_Pin = BRE_RED_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_Init(BRE_RED_GPIO_PORT, &GPIO_InitStructure);

    //绿
    GPIO_InitStructure.GPIO_Pin = BRE_GREEN_PIN;
    GPIO_Init(BRE_GREEN_GPIO_PORT, &GPIO_InitStructure);

    //蓝
    GPIO_InitStructure.GPIO_Pin = BRE_BLUE_PIN;
    GPIO_Init(BRE_BLUE_GPIO_PORT, &GPIO_InitStructure);

}

本实验对GPIO的初始化也相对单色呼吸灯实验作了修改,同时初始化三个通道。

41.5.2.3. 定义PWM表

在全彩呼吸灯和SPWM波工程中定义了见 代码清单:SPWM-10 中的PWM表。

代码清单:SPWM-10 工程中定义的PWM表(bsp_breath_led.c和bsp_breath_led.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/********全彩呼吸灯实验的指数曲线表**********/
/* LED亮度等级 PWM表,指数曲线 ,
此表使用工程目录下的python脚本index_wave.py生成*/
const uint16_t indexWave[] = {
    1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2,
    2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5,
    5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10,
    11, 12, 12, 13, 14, 15, 17, 18,
    19, 20, 22, 23, 25, 27, 29, 31,
    33, 36, 38, 41, 44, 47, 51, 54,
    58, 63, 67, 72, 77, 83, 89, 95,
    102, 110, 117, 126, 135, 145, 156,
    167, 179, 192, 206, 221, 237, 254,
    272, 292, 313, 336, 361, 387, 415,
    445, 477, 512, 512, 477, 445, 415,
    387, 361, 336, 313, 292, 272, 254,
    237, 221, 206, 192, 179, 167, 156,
    145, 135, 126, 117, 110, 102, 95,
    89, 83, 77, 72, 67, 63, 58, 54, 51,
    47, 44, 41, 38, 36, 33, 31, 29, 27,
    25, 23, 22, 20, 19, 18, 17, 15, 14,
    13, 12, 12, 11, 10, 9, 9, 8, 8, 7, 7,
    6, 6, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3,
    3, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1
};

/*********SPWM实验的正弦曲线表*************/
/* SPWM表,正弦曲线,此表使用工程目录下的python脚本sin_wave.py生成*/
const uint16_t indexWave[] = {
    0, 9, 18, 27, 36, 45, 54, 63, 72, 81, 89, 98,
    107, 116, 125, 133, 142, 151, 159, 168, 176,
    184, 193, 201, 209, 218, 226, 234, 242, 249,
    257, 265, 273, 280, 288, 295, 302, 310, 317,
    324, 331, 337, 344, 351, 357, 364, 370, 376,
    382, 388, 394, 399, 405, 410, 416, 421, 426,
    431, 436, 440, 445, 449, 454, 458, 462, 465,
    469, 473, 476, 479, 482, 485, 488, 491, 493,
    496, 498, 500, 502, 503, 505, 506, 508, 509,
    510, 510, 511, 512, 512, 512, 512, 512, 512,
    511, 510, 510, 509, 508, 506, 505, 503, 502,
    500, 498, 496, 493, 491, 488, 485, 482, 479,
    476, 473, 469, 465, 462, 458, 454, 449, 445,
    440, 436, 431, 426, 421, 416, 410, 405, 399,
    394, 388, 382, 376, 370, 364, 357, 351, 344,
    337, 331, 324,  317, 310, 302, 295, 288, 280,
    273, 265, 257, 249, 242, 234, 226, 218, 209,
    201, 193, 184, 176, 168, 159, 151, 142, 133,
    125, 116, 107, 98, 89, 81, 72, 63, 54, 45, 36,
    27, 18, 9, 0
};

//计算PWM表有多少个元素
uint16_t POINT_NUM = sizeof(indexWave)/sizeof(indexWave[0]);

代码中列出的PWM表内元素的最大值均为512,元素个数均为128, 把两个表绘制成成曲线如图 呼吸曲线和正弦曲线

呼吸曲线和正弦曲线

这些PWM表均由工程目录下的python脚本生成的,见 代码清单:SPWM-11代码清单:SPWM-12

代码清单:SPWM-11 生成呼吸曲线PWM表的python脚本
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#! python3
#coding=utf-8

"""
Python版本:3.x
外部库:matplotlib1.5.3、numpy1.11.2

运行方式:
在命令行中输入:python index_wave.py

运行结果:
命令行中会打印计算得的各点数据,
在当前目录下会生成py_index_wave.c文件,包含上述数据,
并且会弹出描绘曲线的对话框。
"""

import matplotlib.pyplot as plt
import numpy as np
import math

#修改本变量可以更改点数,如16、32、64等
POINT_NUM = 180

#指数曲线最大为2的MAX_POWER次方
MAX_POWER = 9

# POINT_NUM 个点
x1 = np.linspace(0,MAX_POWER,POINT_NUM/2)

#f = 2^(x)
up =[]
for i in x1:
    temp = round(2**i)
    #得到升序列
    up.append( temp )

x2 = np.linspace(MAX_POWER,2*MAX_POWER,POINT_NUM/2)

#f = 2^(2*MAX_POWER-x)
down=[]
for i in x2:
    temp = round(2**(MAX_POWER*2-i))

    #得到降序列
    down.append( temp )


line = list(x1)+list(x2)
val = list(up)+list(down)

print(line)
print("*"*80)
print(list(map(int,val)))

#写入序列到文件
with open("py_index_Wave.c",'w',encoding= 'gb2312') as f:
    print(list(map(int,val)),file= f)

#绘图
plt.plot(line,val,"-o")
plt.show()

以上脚本使用的采样函数如下:

若0<= x <=9:

\[y = 2^{x}\]

若9< x <=18:

\[y = 2^{(18 - x)}\]

该函数与单色呼吸灯实验中的类似,只是x的取值范围不同而导致PWM表的极大值不同。脚本运行后会在工程目录下生成一个包含该PWM表的py_index_Wave.c文件, 复制这些数据到bsp_breath_led.c文件,即可得到工程中呼吸灯的indexWave数组。

生成正弦曲线的SPWM表脚本见 代码清单:SPWM-12

代码清单:SPWM-12 生成正弦曲线PWM表的python脚本
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#! python3
#coding=utf-8

"""
Python版本:3.x
外部库:matplotlib1.5.3、numpy1.11.2

运行方式:
在命令行中输入:python sinWave.py

运行结果:
命令行中会打印计算得的各点数据,
在当前目录下会生成py_dac_sinWav.c文件,包含上述数据,
并且会弹出描绘曲线的对话框。
"""

import matplotlib.pyplot as plt
import numpy as np
import math

#修改本变量可以更改点数,如16、32、64等
POINT_NUM = 180
#正弦函数放大倍数
MULTIPLE = 2**9

#正弦函数X取值范围
X_VALUE = math.pi

# POINT_NUM 个点
n = np.linspace(0,X_VALUE,POINT_NUM)

#计算POINT_NUM个点的正弦值
a = map(math.sin,n)

r =[]
for i in a:
    temp = round(i*MULTIPLE)
    #得到序列
    r.append( temp )

print(list(map(int,r)))

#写入序列到文件
with open("py_pwm_sinWave.c",'w',encoding= 'gb2312') as f:
    print(list(map(int,r)),file= f)

#绘图
plt.plot(n,r,"-o")
plt.show()

以上生成SPWM的采样曲线函数如下:

0<=x<=π:

y = 512*sin(x)

脚本运行后会在工程目录下生成一个包含该PWM表的py_pwm_sinWave.c文件,复制这些数据到bsp_breath_led.c文件,即可得到工程中SPWM的indexWave数组。

使用这样的函数曲线,使得呼吸PWM表和SPWM表的极大值均为512,而脚本中又控制两个表的采样点均为180个,在实际应用中可根据需求作不同的更改, 此处仅为了演示方便,使用同样的配置。

41.5.2.4. 定时器PWM配置

本实验的定时器的PWM输出模式配置,见 代码清单:SPWM-13

代码清单:SPWM-13 定时器PWM配置(bsp_breath_led.c文件)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/**
* @brief  通用定时器 TIMx,x[2,3,4,5]中断优先级配置
* @param  无
* @retval 无
*/
static void TIMx_NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    //红
    // 设置中断来源
    NVIC_InitStructure.NVIC_IRQChannel = BRE_RED_TIM_IRQn;

    // 设置抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

    // 设置子优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    //绿
    // 设置中断来源
    NVIC_InitStructure.NVIC_IRQChannel = BRE_GREEN_TIM_IRQn;
    NVIC_Init(&NVIC_InitStructure);

    //蓝
    // 设置中断来源
    NVIC_InitStructure.NVIC_IRQChannel = BRE_BLUE_TIM_IRQn;
    NVIC_Init(&NVIC_InitStructure);

}


/**
* @brief  配置TIM输出的PWM信号的模式,如周期、极性
* @param  无
* @retval 无
*/

static void TIM_Mode_Config(void)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;

    // 开启TIMx_CLK,x[2,3,4,5]
    BRE_RED_TIM_APBxClock_FUN(BRE_RED_TIM_CLK,ENABLE);
    BRE_GREEN_TIM_APBxClock_FUN(BRE_GREEN_TIM_CLK,ENABLE);
    BRE_BLUE_TIM_APBxClock_FUN(BRE_BLUE_TIM_CLK,ENABLE);


    /* 基本定时器配置 ,配合PWM表点数、中断服务函数中的period_cnt循环次数设置*/

    /* 设置使得整个呼吸过程为3秒左右即可达到很好的效果 */

    //要求:
    //TIM_Period:与PWM表中数值范围一致
    //TIM_Prescaler:越小越好,可减轻闪烁现象
    //PERIOD_CLASS:中断服务函数中控制单个点循环的次数,调整它可控制拟合曲线的周期
    //POINT_NUM:PWM表的元素,它是PWM拟合曲线的采样点数

    /*************本实验中的配置***************/
    /***********************************************
    #PWM点数
    POINT_NUM = 180

    #周期倍数
    PERIOD_CLASS = 1

    #幅值等级
    AMPLITUDE_CLASS = 256

    #定时器定时周期
    TIMER_TIM_Period = 2**9

    #APB1定时器分频
    TIMER_TIM_Prescaler1 = 11
    #APB2定时器分频
    TIMER_TIM_Prescaler2 = 22

    #STM32系统时钟频率和周期
    f_hclk = 168000000

    #定时器计时频率
    f_pclk1 = f_hclk/2
    t_pclk1 = 1/f_pclk1

    f_pclk2 = f_hclk
    t_pclk2 = 1/f_pclk2

    #APB1定时器update事件周期
    t_timer1 = t_pclk1*TIMER_TIM_Prescaler1*TIMER_TIM_Period

    #APB1每个PWM点的时间
    T_Point1 = t_timer1 * PERIOD_CLASS * AMPLITUDE_CLASS

    #APB1整个呼吸周期
    T_Up_Down_Cycle1 = T_Point1 * POINT_NUM

    print ("APB1总线定时器呼吸周期:",T_Up_Down_Cycle1)


    #APB2定时器update事件周期
    t_timer2 = t_pclk2*TIMER_TIM_Prescaler2*TIMER_TIM_Period

    #APB2每个PWM点的时间
    T_Point2 = t_timer2 * PERIOD_CLASS * AMPLITUDE_CLASS

    #APB2整个呼吸周期
    T_Up_Down_Cycle2 = T_Point2 * POINT_NUM

    print ("APB2总线定时器呼吸周期:",T_Up_Down_Cycle2)



    #运行结果:

    APB1总线定时器呼吸周期:3.08955
    APB2总线定时器呼吸周期:3.08955
    ************************************************************/
    //当定时器从0计数到255,即为256次,为一个定时周期
    TIM_TimeBaseStructure.TIM_Period = 512-1;
    TIM_TimeBaseStructure.TIM_Prescaler = BRE_RED_TIM_PRESCALER;
    //设置时钟分频系数:不分频(这里用不到)
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //向上计数模式

    // 初始化定时器TIMx
    TIM_TimeBaseInit(BRE_RED_TIM, &TIM_TimeBaseStructure);


    /*基本定时器配置TIM_Prescaler根据效果来设置即可,中断周期小
    灯闪烁快,大则闪烁缓慢*/
    TIM_TimeBaseStructure.TIM_Prescaler = BRE_GREEN_TIM_PRESCALER;
    // 初始化定时器TIMx
    TIM_TimeBaseInit(BRE_GREEN_TIM, &TIM_TimeBaseStructure);

    /*基本定时器配置TIM_Prescaler根据效果来设置即可,中断周期小
    灯闪烁快,大则闪烁缓慢*/
    TIM_TimeBaseStructure.TIM_Prescaler = BRE_BLUE_TIM_PRESCALER;
    // 初始化定时器TIMx
    TIM_TimeBaseInit(BRE_BLUE_TIM, &TIM_TimeBaseStructure);

    /*PWM模式配置*/
    /* PWM1 Mode configuration: Channel1 */
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;     //配置为PWM模式

    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //使能输出
    TIM_OCInitStructure.TIM_Pulse = 0;        //设置初始PWM脉冲宽度为0
    //当定时器计数值小于CCR_Val时为低电平 LED灯亮
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;

    //使能通道
    BRE_RED_TIM_OCxInit(BRE_RED_TIM, &TIM_OCInitStructure);
    /*使能通道重载*/
    BRE_RED_TIM_OCxPreloadConfig(BRE_RED_TIM, TIM_OCPreload_Enable);

    //使能通道
    BRE_GREEN_TIM_OCxInit(BRE_GREEN_TIM, &TIM_OCInitStructure);
    /*使能通道重载*/
    BRE_GREEN_TIM_OCxPreloadConfig(BRE_GREEN_TIM, TIM_OCPreload_Enable);

    //使能通道
    BRE_BLUE_TIM_OCxInit(BRE_BLUE_TIM, &TIM_OCInitStructure);
    /*使能通道重载*/
    BRE_BLUE_TIM_OCxPreloadConfig(BRE_BLUE_TIM, TIM_OCPreload_Enable);

    //使能TIM重载寄存器ARR
    TIM_ARRPreloadConfig(BRE_RED_TIM, ENABLE);
    TIM_ARRPreloadConfig(BRE_GREEN_TIM, ENABLE);
    TIM_ARRPreloadConfig(BRE_BLUE_TIM, ENABLE);

    // 开启计数器中断
    TIM_ITConfig(BRE_RED_TIM,TIM_IT_Update,ENABLE);
    TIM_ITConfig(BRE_GREEN_TIM,TIM_IT_Update,ENABLE);
    TIM_ITConfig(BRE_BLUE_TIM,TIM_IT_Update,ENABLE);

    // 使能计数器
    TIM_Cmd(BRE_RED_TIM, ENABLE);
    TIM_Cmd(BRE_GREEN_TIM, ENABLE);
    TIM_Cmd(BRE_BLUE_TIM, ENABLE);
}

本配置中同时使能三个PWM通道,而定时器初始化中的TIM_Period成员被配置为(512-1),即PWM表元素的极大值,TIM_Prescaler被配置为11和22,, 相对于单色呼吸灯实验,提高了定时器的时钟频率。

41.5.2.5. 定时器中断服务函数

定时器的中断服务函数见 代码清单:SPWM-14

代码清单:SPWM-14 定时器中断服务函数(stm32f4xx_it.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*电压幅值等级数*/
#define AMPLITUDE_CLASS 256

//控制输出波形的频率
__IO uint16_t period_class = 1;

extern uint16_t indexWave[];

extern __IO uint32_t rgb_color;

void  BRE_RED_TIM_IRQHandler (void)
{
    static uint16_t pwm_index = 0;      //用于PWM查表
    static uint16_t period_cnt = 0;   //用于计算周期数
    static uint16_t amplitude_cnt = 0;  //用于计算幅值等级

    if (TIM_GetITStatus(BRE_RED_TIM, TIM_IT_Update) != RESET) { //TIM_IT_Update
        amplitude_cnt++;

        //每个PWM表中的每个元素有AMPLITUDE_CLASS个等级,
        //每增加一级多输出一次脉冲,即PWM表中的元素多使用一次
        //使用256次,根据RGB颜色分量设置通道输出
        if (amplitude_cnt > (AMPLITUDE_CLASS-1)) {
            period_cnt++;

            //每个PWM表中的每个元素使用period_class次
            if (period_cnt > period_class) {
                pwm_index++;    //标志PWM表指向下一个元素

                //若PWM表已到达结尾,重新指向表头
                if ( pwm_index >=  POINT_NUM) {
                    pwm_index=0;
                }
                period_cnt = 0;     //重置周期计数标志
            }
            amplitude_cnt=0;    //重置幅值计数标志
        } else {
            //每个PWM表中的每个元素有AMPLITUDE_CLASS个等级,
            //每增加一级多输出一次脉冲,即PWM表中的元素多使用一次

            //根据RGB颜色分量值,设置各个通道是否输出当前的PWM表元素表示的亮度
            //红
            if (((rgb_color&0xFF0000)>>16) >= amplitude_cnt)
                //根据PWM表修改定时器的比较寄存器值
                BRE_RED_TIM->BRE_RED_CCRx = indexWave[pwm_index];
            else
                //比较寄存器值为0,通道输出高电平,该通道LED灯灭
                BRE_RED_TIM->BRE_RED_CCRx = 0;
        }
TIM_ClearITPendingBit (BRE_RED_TIM, TIM_IT_Update); //必须要清除中断标志位
    }
}
/*绿灯和蓝灯的中断服务函数类似,此处省略…*/

本中断服务函数相对于单色呼吸灯例程增加了对拟合曲线电压等级的控制,它是利用计数变量amplitude_cnt、 电压分级宏AMPLITUDE_CLASS以及电压等级变量rgb_color实现的,其实现原理如下:

为便于讲解,先假设用于配置拟合波形周期长度的period_class值为1,即在控制周期长度时, 每个PWM表中的元素只使用1次(关于period_class的作用请复习前面单色呼吸灯实验中的说明)。

在这个基础上增加对电压的分级,用于控制拟合波形的输出电压,本实验中由宏AMPLITUDE_CLASS控制,其值为256,即可输出256种不同的电压等级。

该宏值在中断中会与amplitude_cnt进行比较(第19行),amplitude_cnt每次进入中断加1, 当amplitude_cnt大于AMPLITUDE_CLASS时才会进入周期配置的判断,以便使PWM表指向下一个元素,也就是说增加电压分级配置后, 遍历每个PWM表元素时,每个元素会增加256个周期。

在这256个周期内,会进入控制电压等级的处理(第38~49行),处理的过程是使用电压等级值rgb_color与amplitude_cnt进行比较, 本实验中rgb_color包含红绿蓝三个通道的电压值,各通道的取值范围是[0:255],与RGB888颜色格式一致。当通道的电压值R/G/B数据大于amplitude_cnt时, 向该通道的比较寄存器赋予PWM表中当前指向的元素值,否则赋予0值。根据定时器的配置可知,比较寄存器中的值就是该通道输出低电平的时间, 即LED灯亮的时间,所以,在256个周期时间内,各通道有R/G/B个周期会点亮LED灯当前PWM表元素表示的时间。

例如:

若红色通道值R=0,那么R值在256个周期内,均小于amplitude_cnt,通道输出高电平,红色LED灯一直灭, 此时\(\frac{T_{R\_ LED\_ ON}}{T_{\text{CLASS}}} = \frac{0}{256}\)

若绿色通道值G=128,那么G值在前128个周期内大于amplitude_cnt,通道输出当前PWM表元素表示时间的低电平,绿色LED灯亮128个周期, 此时\(\frac{T_{G\_ LED\_ ON}}{T_{\text{CLASS}}} = \frac{128}{256}\)

若蓝色通道值B=200,那么B值在前200个周期内大于amplitude_cnt,通道输出当前PWM表元素表示时间的低电平,蓝色LED灯亮200个周期, 此时\(\frac{T_{B\_ LED\_ ON}}{T_{\text{CLASS}}} = \frac{200}{256}\)

所以,三个通道控制的LED灯点亮的时间比例即为RGB888颜色值表示的量,混合后可得到该颜色。

又由于PWM表中的元素值则表示了混合颜色的亮度,把PWM表遍历一遍,即控制混合颜色亮度呈PWM表变化,即可得到该颜色的呼吸灯效果。

当周期倍数period_class=1,电压分级数量AMPLITUDE_CLASS=5,电压等级分别为3和1时, 输出的PWM波形如图 电压等级分别为3和1时的PWM波形 所示。

电压等级分别为3和1时的PWM波形

推广至当period_class不等于1时,遍历PWM表中的每个元素需要进入定时器中断AMPLITUDE_CLASS*period_class次,设定时器的中断周期为T_timer, 那么输出的拟合曲线周期为T_timer *AMPLITUDE_CLASS*period_class。

41.5.2.6. 计算拟合波形的周期

本工程相对单色呼吸灯例程增加了电压分级AMPLITUDE_CLASS,结合前面的TIMPeriod、PWM表的点数、TIM_Prescaler以及period_cnt参数,重新总结如下:

  • TIMPeriod:定时器的计数周期,它的值必须与PWM表中的极大值相等(应用中赋值需要减1),而PWM表的极大值决定了控制的分辨率。 例如极大值为10时,PWM占空比只有10个等级,精确到0.1,当极大值为1000时,PWM占空比有1000个等级,精确到0.001。

  • TIM_Prescaler:定时器时钟分频因子,它控制定时器计数器CNT计数加1所需要的时间,它的值太大会导致输出的单个PWM波周期过长, 影响控制的动态特性。如控制LED灯时,该值太大会导致LED灯开关时间变长,闪烁明显。一般来说,该值越小越好。

  • PWM表的点数:PWM表的点数即对拟合曲线的采样点数,采样点越多,能更好地还原拟合曲线,采样点太少,可能会导致失真, 见 图对呼吸特性曲线采样110个和10个点时的情况

对呼吸特性曲线采样110个和10个点时的情况
  • period_class:PWM表中每个元素的循环次数,它影响拟合曲线的周期。当period_class=1时,可以输出本配置中周期最短的拟合曲线。

  • AMPLITUDE_CLASS:电压分级数,它可以把输出拟合曲线的幅值分成N个等级,控制时可以选择按某个幅值等级进行输出, 可根据实际情况进行分级,如本实验中分级为与RGB888各通道一致的256等级,若只需要支持RGB555格式,那么AMPLITUDE_CLASS配置为32即可。

以上各个参数虽然侧重点不同,但若修改其中的任何一个,最终都会影响到所拟合曲线的周期,所以在实际应用中, 通常先设定好TIMPeriod、TIM_Prescaler、PWM表的点数以及幅值等级数AMPLITUDE_CLASS,得到适合的控制精度、动态特性拟合度以及幅值等级后, 然后再调整period_class控制拟合曲线的周期,而且period_class在程序中动态修改非常方便,不需要重置定时器和PWM表。

最终,我们把本工程配置中的拟合曲线周期计算公式概括如下:

(1) STM32系统时钟默认频率和周期:

f_hclk = 168000000

某些定时器使用APB1:

f_pclk1 = f_hclk/2

t_pclk1 = 1/f_pclk1

某些定时器使用APB2:

f_pclk2 = f_hclk

t_pclk2 = 1/f_pclk2

(2) 定时器update事件周期,即定时器中断周期:

APB1定时器:t_timer1 = t_pclk1*TIMER_TIM_Prescaler1*TIMER_TIM_Period

APB2定时器:t_timer2 = t_pclk2*TIMER_TIM_Prescaler2*TIMER_TIM_Period

(3) 每个PWM点的时间:

APB1定时器:T_Point1 = t_timer1 * PERIOD_CLASS * AMPLITUDE_CLASS

APB2定时器:T_Point2 = t_timer2 * PERIOD_CLASS * AMPLITUDE_CLASS

(4) 最终,遍历PWM表的周期,即拟合曲线的周期:

APB1定时器: T_Up_Down_Cycle1 = T_Point1 * POINT_NUM

APB2定时器: T_Up_Down_Cycle2 = T_Point2 * POINT_NUM

例如,本工程配置中:

#**PWM点数**

POINT_NUM = 180

#**周期倍数**

PERIOD_CLASS = 1

#**幅值等级**

AMPLITUDE_CLASS = 256

#**定时器定时周期**

TIMER_TIM_Period = 512

#**APB1定时器分频**

TIMER_TIM_Prescaler1 = 11

#**APB2定时器分频**

TIMER_TIM_Prescaler2 = 22

#**STM32系统时钟频率和周期**

f_hclk = 168000000

代入公式计算得:

APB1总线定时器呼吸周期:T_Up_Down_Cycle1 = 3.08955

APB2总线定时器呼吸周期:T_Up_Down_Cycle2 = 3.08955

通过公式的计算可知本工程的配置可使得输出的拟合曲线周期为3.08955秒,是比较平缓的呼吸周期。

41.5.2.7. 主函数

代码清单:SPWM-15 主函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//该变量在定时器中断服务函数中使用,用于控制各通道的输出
//修改该变量的值可直接改变呼吸灯的颜色
//变量格式:RGB888
__IO uint32_t rgb_color = 0xFF00FF;



/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{
    /* 初始化呼吸灯 */
    BreathLED_Config();

    while (1) {

//      //可动态修改颜色,使用各种颜色的呼吸灯
//    rgb_color = 0xFF00FF;
//    SOFT_DELAY();

//    rgb_color =0x8080ff;
//    SOFT_DELAY();
//
//    rgb_color =0xff8000;
//    SOFT_DELAY();
//
//    rgb_color =0xffc90e;
//    SOFT_DELAY();

    }
}

main函数中初始化呼吸灯使用的定时器和GPIO后,定时器开始工作,然后它会在中断服务函数中切换PWM数据,并且根据rgb_color的值控制拟合曲线的电压值, 达到控制LED灯可以各种颜色显示呼吸效果。在main函数中,可通过修改rgb_color的值可以改变呼吸灯的颜色。

对于输出SPWM波的工程,也直接使用了定时器输出的SPWM波控制RGB彩灯,因为正弦曲线和呼吸曲线变化方向类似,所以它控制RGB灯时, 看起来也有呼吸灯的效果,在实际应用中可以使用类似的方法控制SPWM波拟合正弦曲线的频率和电压, 通过定时器PWM模式初始化中的结构体成员TIM_OCInitStructure.TIM_OCPolarity可控制当定时器计数值小于CCRx_Val时为高电平还是低电平。

41.5.3. 下载验证

编译并下载本程序到开发板,给开发板上电复位,可看到LED灯显示呼吸效果。