呼吸灯在 C51 和 STM32 上的实现

2021-06-14
2716 字
6 分钟阅读时长
Featured Image

前言

上学期在学习 51 单片机的时候,曾经写了一个叫做 『 LED Show 』的程序,它的效果是这样的:

很弱智哈哈哈,原理也很直接:选好音乐,给音乐中每个小节设计一个灯光效果,控制好节拍(试出最合适的延时使其与音乐节拍吻合)就行。不过我想说的是,那个时候我想过要在 51 上面实现呼吸灯的效果,但一番查阅资料,又是中断(是的当时连中断是啥都不晓得🤷‍♂️)又是啥 PWM ,无奈放弃了。最近翻回去查了查资料,发现实现呼吸灯的基本原理已经差不多理解了,不过,51 相关的知识已经忘得差不多了,正好借这个机会,好好复习一下。

我所使用的 51 开发板来自清翔电子,芯片型号为 STC89C52。

中断和定时计数器的复习

呼吸灯的实现原理其实就是控制 LED 灯的亮灭时间比例,正好中断的知识已经忘得差不多了,那不如用定时器中断来实现,所以首先简单复习一下 STC89C52 的中断系统。

STC89C52 提供了包括 4 个外部中断、3 个定时器中断以及 1 个串口中断在内的 8 个中断请求源。具体在程序中的的控制方法实在记不起来了,查阅了芯片手册,才勉强回忆起来,在此做一个总结:

  1. CPU 总中断控制位为 EA

  2. 外部中断 INT0 ~ INT3 的中断允许控制位分别是 EX0 ~ EX3,触发方式由 ITx 控制,ITx = 1 代表下降沿触发而 ITx = 0 代表低电平触发。清翔的开发板给每个外部中断的引脚接到的不同的外部硬件,如按键或红外接收器,可以查阅开发板原理图找到每个外部中断对应的可用的硬件模块;

  3. 定时器中断 Timer0 ~ Timer2 的中断允许控制位分别是 ET0 ~ ET2,其运行控制位分别为 TR0 ~ TR2,3个定时器中断中的 Timer0Timer1 共用一个工作模式寄存器 TMOD,低四位控制 Timer0 而高四位控制 Timer1Timer2 有一个单独的控制寄存器 T2CON 用来实现更丰富的功能。

  4. 寄存器TMOD

    TMOD.7 TMOD.6 TMOD.5 TMOD.4 TMOD.3 TMOD.2 TMOD.1 TMOD.0
    GATE C/T' M1 M0 GATE C/T' M1 M0

    GATE 控制定时器:GATE = 0 时用 TRx 来控制定时器的启动,GATE = 1时则用外部中断来控制。
    C/T' 控制定时器用作定时器还是计数器:C/T' = 0 表示定时器模式;C/T' = 1 表示计数器模式,定时器的外部引脚下降沿作为脉冲。
    M1M0 控制定时器的模式:分别配置为 0 0(模式 0)为 13 位定时器,THx 仅低五位参与分频;分别配置为 0 1(模式 1)为 16 位定时器,数据的低 8 位和高 8 位分别在 THxTLx 寄存器中;分别配置为 1 0(模式 2)为 8 位自动重装定时器,当 TLx 溢出时自动将 THx 数据重装入 TLx(对于模式 0 和模式 2,往往是在中断程序中手动装载计数值来实现周期定时);而当M1 M0 设置为 1 1(模式 3)时,对定时器 1 是停止计数,效果与将TR1设置为 0 相同,而对于定时器 0 而言,此时 TH0TL0 将作为两个独立的 8 位计数器,且定时器 0 的部分控制位将被 TL0 占用,因为这一方式的逻辑结构较为复杂,故不在此处阐述,读者可以查阅芯片手册的 7.1.2.4 模式 3 ( 两个 8 位计数器 ) 章节。

接下来是最重要的定时器中断的时间间隔控制方法,因为定时器在不同的模式下位数不同,因此在不同的工作模式下计算方法也略有不同。

对于模式 1(16 位定时器):

\[ THx = \left ( 2^{16}-{定时时间\ \mu s}\ {\div} \ \frac{1\ 个机器周期对应的时钟周期数}{晶振频率\ MHz}\right )/ \ 256 \\ TLx = \left ( 2^{16}-{定时时间\ \mu s}\ {\div} \ \frac{1\ 个机器周期对应的时钟周期数}{晶振频率\ MHz}\right )\% \ 256 \\ \]

对于模式 2(8 位定时器):

\[ THx = \left ( 2^{8}-{定时时间\ \mu s}\ {\div} \ \frac{1\ 个机器周期对应的时钟周期数}{晶振频率\ MHz}\right ) \\ TLx \ 当然也可以设置初值 \\ \]

对应到我所使用的开发板(晶振频率 11.0592 MHz,1 机器周期 = 12时钟周期),就是:

\[ THx \approx \left ( 65536-{定时时间\ \mu s} \ {\div} \ 1.085\mu s\right )/ \ 256 \\ TLx \approx \left ( 65536-{定时时间\ \mu s} \ {\div} \ 1.085\mu s\right )\% \ 256 \\ 以及 \\ THx \approx \left ( 256-{定时时间\ \mu s} \ {\div} \ 1.085\mu s\right ) \]

这样就可以写出定时器中断的代码了:

/**
 * 在我的开发板上,
 * 8位定时器最多实现 256 × 1.085 ≈ 277us 的中断时间间隔,
 * 13位定时器最多实现 8192 × 1.085 ≈ 8888us 的中断时间间隔,
 * 16位定时器最多实现 65536 × 1.085 ≈ 71ms 的中断时间间隔,
 * 这里用了 200us 做演示,因此选择模式 2。
 */
void Timer0_Init(void) {
    EA   = 1;    // 打开 CPU 中断总开关
    ET0  = 1;    // 打开 Timer0 的中断允许控制位
    TMOD = 0x02; // 设置 Timer0 为 TR0 控制、定时器模式、8 位定时器
    TR0  = 1;    // 打开 Timer0
    TH0  = (256 - 184);
    TL0  = (256 - 184); // 时间间隔 200us
}

void timer0() interrupt 1 { // Timer0 的中断号为 1
    /* code */
}

思路和实现

前面提到,控制 LED 亮灭的时间的比例变化就可以实现呼吸灯,我先用了最简单的办法实现: 用变量 flag 来划分 0maxi 属于 flag 之前时亮灯,i 属于 flag 之后时灭灯,i 走完 0max 后,flag 移动一位(改变亮灭时间比例)。

#include <reg52.h>

unsigned int max = 100, flag = 0, i = 0;
char step = 1;  // 不能用 unsigned

void timer0() interrupt 1 {  // Timer0 的中断号为 1
    if(i < flag) P1 = 0x00;
    if(i >= flag && i <= max) P1 = 0xff;
    if(++i == max){
        i = 0;
        if(flag == max) step = -1;
        if(flag == 0) step = 1; // 改变flag移动方向,实现由灭到亮再到亮
        flag += step;
    }
}

void Timer0_Init(void) {...} // 此处代码与上文相同,故省略

void main() {
    Timer0_Init();
    while(1);
}

优化:代码精简

这样一来,开发板上已经可以看到不错的呼吸效果了,不过我们仔细看看代码,现在每次中断都要把 LED 的亮灭状态设置一次,而我更想让它只在变化的时候设置一次。那也不难发现,i0 递增到 max,加到 max 后回到 0,而我们刚才说了,i 属于 flag 之前时亮灯,i 属于 flag 之后时灭灯,也就可以简化成:i == 0 || i == flag 时变换亮灭状态,这样就可以把前两句 if 变成一句了。不过实现过程中,仍然发现了需要注意的点:flag 的范围不能再是 0 ~ max,因为当 flagi 都等于 0 时,那一次 i0max 的循环 LED 只会变化一次,具体代码如下:

unsigned int max = 100, flag = 2, i = 0;
char step = 1;

void timer0() interrupt 1 {
    if(i == 0 || i == flag) P1 = ~P1;
    if(++i > max){
        i = 0;
        /** 
         * 下面这一句也可以改,原理近似,但注意点还是挺多的:
         * 首先 flag 的初值不能与下面的比较值一样,避免第一次就改变 step;
         * 其次 flag 的比较值不可以与 i 的初值一样,这样 i 在一次循环中才可以改变一次亮灭,P1 = ~ P1 才可以按照正常的规律进行,
         * 上一句要写 ++i > max 而不是 >= 或 ==,也是这个原因。
         * 这里的逻辑还是挺有意思的,读者可以自己尝试不同的初值、比较值,看看呼吸灯是怎样的效果。
         */
        if(flag == max || flag == 1) step = -step; 
        flag += step;
    }
}

一些简单的概念

上述的方法中我们通过改变数字信号的占空比,从而实现了近似于输出模拟信号的功能,这就叫做 PWM(脉冲宽度调制),在上文案例中,每 100 次进入定时器中断完成一次从暗到亮的变化,所以我们的 PWM 周期就是 100 × 200us = 2s,我们可以改变定时器周期和 max 变量来配合改变 PWM 周期和亮暗变化的细腻程度,比如将上面代码中的 max 改为 20 就可以得到近似于 Disco 灯球的动感;再比如将定时器的 TH0 TL0 都设置为 0xECmax 设置为 200,就可以得到变化细腻且在低亮度下也不易观察到频闪的呼吸灯效。读者可以随意尝试看看不一样的效果,而在电机控制等应用场景中,对于 PWM 周期会有更为严格的要求(比如不合适的周期脉冲可能会让电机产生异响等),如果读者是初学者,应该对这些要有一个概念。

参考

STC89C52 芯片手册:官网 | 芯片手册

Avatar
Chclt That's me!