4.3 计数器和PWM输出¶
PWM(脉冲宽度调制)信号是一种可实现连续信号控制效果的数字信号,由于其实现电路单元全部由数字电路组成,易于集成且成本低,现在的绝大多数MCU都支持可编程PWM信号输出。 相较于DA转换输出的模拟信号,PWM信号具有极强的抗干扰特性,这使得PWM的应用场景非常多,譬如开关电源、电池充电、显示器亮度控制、伺服控制、通信等。
我们首先使用BlueFi的白色LED指示灯来测试PWM信号的使用效果。用USB数据线将BlueFi连接到电脑,使用Python脚本编程和Python解释器可以快速修改、测试程序, 将下面的示例代码保存到“/CIRCUITPY/code.py”文件,覆盖之前的“code.py”文件。
1 2 3 4 5 6 7 8 9 10 11 12 | import time
import board
from pulseio import PWMOut
led = PWMOut(board.WHITELED, frequency=1000, duty_cycle=0)
while True:
for i in range(100):
if i < 50:
led.duty_cycle = int(i * 2 * 65535 / 100) # Up
else:
led.duty_cycle = 65535 - int((i - 50) * 2 * 65535 / 100) # Down
time.sleep(0.01)
|
当BlueFi执行这个示例脚本程序时,将会观察到白色LED指示灯的亮度“渐灭-渐亮-渐灭-..”循环变化。然后将第4行的“frequency”选项的值分别修改为50、 500、5000、10000等并观察每次修改-保存后的亮度变化。修改“frequency”选项的值实际就是改变PWM信号的频率,使用不同频率时你发现了什么不同吗? 无论PWM信号的频率如何修改,肉眼观察看不出白色LED指示灯亮度变化规律有何不同。
通过简单的测试我们了解到,1)使用PWM信号可以调节LED指示灯的亮度;2)PWM信号的频率变化不影响LED亮度控制效果;3)改变PWM输出类的实体对象“led”的 “duty_cycle”属性值可以改变LED的亮度。
PWM信号到底是什么样的呢?如果使用示波器观察BlueFi主MCU(nRF52840)的P1.14引脚的信号,我们将会看到如图4.10所示的PWM信号。
图4.10 PWM信号的形状
示例程序的第4行在实例化PWMOut类时,指定PWM信号输出引脚为P1.14(即控制白色LED的引脚),PWM信号频率(frequency)为1KHz,初始占空比(duty_cycle)为0, 实例化对象的名称为“led”。实质上,这行脚本程序是在配置PWM信号发生器参数和信号输出通道。在主循环程序块内,第9行语句是根据循环变量i计算“led”的“duty_cycle”属性值, 从表达式可以看出这个属性值与i呈正比关系,随着i的增加输出的PWM信号的占空比也随之增加,我们观察到的效果:白色LED的亮度渐大;第11行也是根据i计算“duty_cycle”属性值, 但是他与i呈反比关系,随着i的增加输出的占空比也随之减小,我们观察到的效果:白色LED的亮度渐小。
当我们反复修改这个PWM信号的frequency属性时,只要保持不低于40Hz,可以断定输出控制LED亮度的PWM信号频率肯定发生变化,但是我们的肉眼并不能观察到不同频率引起的特殊变化。 如果你知道普通的交流电灯也是以50Hz在变化,肉眼并不能观察到电灯的明暗变化,由此可知上述的测试过程中观察到的现象是为什么。如果你把frequency属性改为10甚至更低小时, 再观察白色LED的亮度变化,你将会发现明显不同。
并不是所有PWM信号的频率都是可以任意修改的,实际的频率选择应根据被控对象(如LED)的开关频率特性(这是电子元件的一种重要电气特性)来选择, 譬如伺服系统电机的响应速度较低仅适合100Hz以下的PWM信号频率。PWM信号是如何产生的呢?
PWM信号发生器由时钟预分频器(Prescaler)、波计数器(Wave Cpunter)或通用计数器(General Counter)、数值比较器(Comparator)等组成,如图4.11所示。
图4.11 PWM信号发生器的结构组成
设置时钟预分频器寄存器的值可调整PWM信号的频率,向占空比寄存器写入不同值可调整PWM信号的占空比,计数器的模式包括递增、递减、先递增-再递减等三种, 计数器的模式选择可以改变PWM信号的对齐方式(前沿对齐、后沿对齐、中心对齐),如图4.12所示。
图4.12 PWM信号对齐模式和计数器模式
考虑PWM信号不同的应用目的,大多数PWM信号发生器的输出极性都支持可编程的反转特性。从上面两图可以看出,计数器是PWM信号发生器单元的核心部件。 定时/计数器(Timer/Counter)是现代MCU片上必备的基础功能单元,编程控制定时/计数器不仅能产生单次的(One-short)定时中断请求、周期性中断请求, 还能捕获外部输入的脉冲信号进行计数,借助于数值比较器也能产生PWM信号。很多MCU的片上并没有专用的PWM信号发生器单元,通过对定时/计数器编程控制产生PWM信号。 当然,专用的PWM信号信号发生器仍具有定时器的功能。譬如,根据图4.11的结构,很容易让这个PWM信号发生器产生周期性中断请求。 由于定时/计数器的结构和原理相对简单,相关的概念大多数都属于数字电路的范畴,本书中不对其深入讨论。
本节开始的时候,我们已经使用Python解释器和“pulseio”模块中的“PWMOut”类编写脚本程序控制nRF52840的P1.14引脚输出PWM信号, 在Arduino开源平台如何编程控制I/O引脚输出PWM信号呢?Arduino的内部函数“analogWrite(pin,value)”是一个特殊的接口,将自动根据输出参数“pin”的I/O属性确定具体的执行效果。 我们已经知道,编译和下载Arduino程序之前必须使用“开发板管理器”指定开源板名称、编译和下载相关的参数,每一种开源板的MCU的每一个引脚的用法都是确定的, 这在前一章中已经遇到过。如果传递给函数“analogWrite(pin, value)”的参数“pin”对应的I/O引脚是支持DAC型模拟输出的,则将参数“value”写入DAC寄存器即执行完毕; 如果该引脚不支持DAC型模拟输出但支持PWM输出,则将参数“value”写入PWM的占空比寄存器即执行完毕;如果“pin”既不支持DAC型模拟输出也不支持PWM输出, 则该语句被忽略,不执行任何动作 [1]_ 。
对于MCU的PWM输出引脚来说,Arduino的函数“analogWrite(pin, value)”仅改变PWM的占空比,占空比的分辨率决定参数“value”的范围。如果对照Python脚本程序, 我们如何确定PWM占空比的分辨率?如何改变PWM信号的频率呢?
Arduino的函数“analogWriteResolution(bits)”用于指定“analogWrite(pin, value)”的参数“value”的分辨率,参数“bits”是二进制位宽度,默认值是8。 那么默认的参数“value”的有效范围是多少呢?根据前面所掌握的PWM信号发生器的结构,PWM信号占空比的取值范围必须与计数器的范围保持一致。
Arduino没有改变PWM信号频率的接口函数!如何知道某个开源板的PWM信号频率是多少呢?在Arduino官网的页面 [1]_ 已经列出官方开源板默认的PWM信号的频率, Arduino平台的软件架构上已经将每一种开源板的PWM信号频率进行预设。当我们了解PWM信号发生器的基本结构,开源板所用的MCU片上PWM资源及其用法,以及Arduino的PWM接口, 我们可以通过修改Arduino的PWM接口初始化参数配置PWM信号频率。事实上,使用“analogWriteResolution(bits)”设置占空比(或计数器)范围也可以改变PWM信号频率。 PWM信号的频率受PWM模块的时钟频率、分频器和计数器的范围等三个参数约束。譬如,nRF52840的PWM模块时钟频率为16MHz,分频器可选择1/2/4/8/16/32/64/128-分频, 计数器的范围3~32767(即可设置最大的二进制位宽度是15)。如果选择1分频,即16MHz时钟为计数器工作时钟(即时钟周期为62.5ns),使用8位的计数器分辨率时 的PWM信号周期为16微秒(=256*62.5ns),12位时的PWM信号周期为256微秒,15位时的PWM信号周期为2.048ms。这些参数可在nRF52840数据页的PWM相关的寄存器说明部分查询到。 按第3.5节所搭建的兼容Arduino开源平台的软件开发环境中,PWM信号发生器相关的接口在“../Arduino15/packages/adafruit/hardware/nrf52/0.20.5/cores/nRF5”文件夹中, 设计“wiring_analog.h”、“wiring_analog.cpp”、“HardwarePWM.h”和“HardwarePWM.cpp”四个文件,PWM初始化部分在“HardwarePWM.cpp”的“begin()”中。
下面我们来修改第4.1节所创建的LED类的实现代码,增加LED亮度控制接口,使用PWM信号发生器控制LED亮度,从而了解Arduino开源平台上的PWM编程控制。 BlueFi的LED类的实现代码在“../Documents/Arduino/libraries/BueFi/src/utility/”文件夹的“BlueFi_LEDs.h”和“BlueFi_LEDs.cpp”两个源文件中, 现在只需要为LED类添加一个名叫“bright(bv)”的单输入参数的成员函数,具体的代码实现极其简单,修改后的两个源文件的代码如下:
(BlueFi_LEDs.h文件,第14行代码是新增的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #ifndef ___BLUEFI_LEDS_H_
#define ___BLUEFI_LEDS_H_
#include <Arduino.h>
class LED {
public:
LED(uint8_t pin);
uint8_t getAttachPin(void);
void on(void);
void off(void);
void toggle(void);
bool state(void);
void bright(uint16_t bv); // set LED brightness
private:
bool __isInited;
bool __state;
uint8_t __pin;
};
#endif // ___BLUEFI_LEDS_H_
|
(BlueFi_LEDs.cpp文件,第34~36行代码是新增的)
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 | #include "BlueFi_LEDs.h"
LED::LED(uint8_t pin) {
__isInited = 1;
__state = 0;
__pin = pin;
pinMode(__pin, OUTPUT);
digitalWrite(__pin, __state);
}
uint8_t LED::getAttachPin(void) {
return __pin;
}
void LED::on(void) {
__state = 1;
digitalWrite(__pin, __state);
}
void LED::off(void) {
__state = 0;
digitalWrite(__pin, __state);
}
void LED::toggle(void) {
__state = (__state)?0:1;
digitalWrite(__pin, __state);
}
bool LED::state(void) {
return __state;
}
void LED::bright(uint16_t bv) {
analogWrite(__pin, bv);
}
|
仅为演示的目的,我们仍使用默认的PWM信号参数,即8位分辨率的PWM占空比、62.5KHz的频率,如果需要改变分辨率和频率则可以使用“analogWriteResolution(bits)”接口。 考虑到分辨率可配置为最大宽度是15位,因此亮度控制接口函数“bright(bv)”的参数“bv”采用16位宽的无符号整型。然后,编写这个接口的用法示例程序,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <BlueFi.h>
void setup() {
bluefi.begin();
bluefi.whiteLED.off();
}
void loop() {
static uint8_t bv=0, dir=1;
if (dir) { // fade up
bv += 5; // step length
if (bv > 250) dir=0;
} else { // fade down
if (bv >= 5) bv -= 5;
else dir=1;
}
bluefi.redLED.bright(bv);
delay(10);
}
|
如果使用示波器观察BlueFi红色LED的阳极引脚处的波形,将会清晰地看到一个周期/频率固定的PWM波形,而且高电平的宽度会“渐大-渐小”地周期性变化, 大多数示波器还能测量这个PWM波的频率,可以验证是否与理论的62.5KHz保持一致。
然后,我们也可以尝试改变这个PWM波的频率,按照前面所掌握的PWM信号发生器原理,改变占空比(即计数器)的范围也可以改变PWM信号频率。 这需要在初始化BlueFi时(“setup()”函数内)使用“analogWriteResolution(14)”接口设置分辨率为14位宽,再修改“loop()”函数内的亮度最大值和亮度增量步长。 注意,14位宽的无符号整型数范围是0~16383。修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <BlueFi.h>
void setup() {
bluefi.begin();
analogWriteResolution(14); // 14-bit resolution: 0~16,383
bluefi.whiteLED.off();
}
void loop() {
static uint16_t bv=0, dir=1;
if (dir) { // fade up
bv += 328; // step length
if (bv > 16383) dir=0;
} else { // fade down
if (bv >= 328) bv -= 328;
else dir=1;
}
bluefi.redLED.bright(bv);
delay(10);
}
|
修改的代码包括,新增第4行(改变占空比分辨率),修改第11、12、14行中的亮度变量值。将修改后的示例程序编译并下载到BlueFi开源板上后, 执行程序期间再用示波器观察和测量红色LED指示灯阳极引脚处的波形频率,验证是否与理论的1KH z频率一致。
为了便于测试,请先删除“../Documents/Arduino/libraries/BlueFi”文件夹中的全部分局,然后下载下面的压缩文件包, 并解压到“../Documents/Arduino/libraries/BlueFi”文件夹中,
本节所修改的LED类的实现代码和示例程序都已添加到该文件夹。将示例程序编译并下载到BlueFi开源板,执行这个示例程序时将会看到红色LED指示灯呈“呼吸”效果。
PWM信号发生器由可编程的分频器、计数器和数值比较器等组成,PWM信号的占空比(高电平与信号周期的比值)和频率都是可编程的,而且PWM信号的边沿对齐方式、 占空比范围(即计数器的分辨率)等也是可编程的。灵活的PWM信号发生器不仅具有结构简单、易实现等特点,输出的数字信号能实现连续信号的控制效果,具有极强的抗干扰特性。
参考文献:
.. [1] https://www.arduino.cc/reference/en/language/functions/analog-io/analogwrite/