• 51单片机的软件和硬件PCA/PWM输出


    软件方式输出PWM

    PWM用于输出强度的控制, 例如灯的亮度, 轮子速度等, STC89/90系列没有硬件PWM, 需要使用代码模拟

    使用纯循环的方式实现PWM

    非中断的实现(SDCC环境编译)

    #include <8052.h>
    
    #define Led10 P0_7
    typedef unsigned int u16;
    
    int atime = 64;
    
    // 仅作为延时, pms取值区间为 0 - 64
    void delay(u16 pms) {
      u16 x, y;
      for (x=pms; x>0; x--) {
        for (y=11; y>0; y--);
      }
    }
    
    // 这里控制占空比, i取值区间为 0 - 64, 
    // i越大脉冲宽度越低, 因为输出是低位点亮, 所以i越大LED越亮
    void ledfade(u16 i) {
      Led10 = 0;
      delay(i);
      Led10 = 1;
      delay(atime-i);
    }
    
    int main(void) {
      u16 a, b;
      // 每个循环, 小灯
      while(1) {
        // a增大, 脉冲宽度降, 亮度增
        for (a=0; a<atime; a++) {
          for (b=0; b < (atime - a)/4; b++) {
            ledfade(a);
          }
        }
        // a减小, 脉冲宽度增, 亮度降
        for (a=atime; a>0; a--) {
          for (b=0; b < (atime - a)/4; b++) {
            ledfade(a);
          }
        }
      }
    }
    

    使用中断的方式

    因为需要PWM输出的场景, 一般都不会仅仅有PWM输出, 所以通常会做到定时器中断中, 由中断来实现
    将1和0的时间宽度设置为定时器, 直接做到定时器中断里面

    这个代码中

    1. pwm_flag代表了输出的0和1, 每次定时器中断时进行切换, 并设置下一次中断的时间宽度
    2. 缺点: 用TR0做开关, 但是这种停止方式, 停止后输出可能还是1
    /* Global variables and definition */
    #define PWMPIN P1_0
    
    unsigned char pwm_width;
    bit pwm_flag = 0;
    
    void pwm_setup()
    {
      TMOD = 0; // Timer mode 0, 13bit
      pwm_width = 160;
      EA = 1;
      ET0 = 1;
      TR0 = 1;
    }
    
    /* Timer 0 Interrupt service routine */
    void timer0() interrupt 1
    {
      if (!pwm_flag) {  /* Start of High level */
        pwm_flag = 1; /* Set flag */
        PWMPIN = 1;   /* Set PWM o/p pin */
        TH0 = pwm_width;  /* Load timer */
        TF0 = 0;    /* Clear interrupt flag */
      } else {      /* Start of Low level */
        pwm_flag = 0; /* Clear flag */
        PWMPIN = 0;   /* Clear PWM o/p pin */
        TH0 = 255 - pwm_width;  /* Load timer */
        TF0 = 0;    /* Clear Interrupt flag */
      }
    }
    
    void pwm_stop()
    {
      TR0 = 0;      /* Disable timer to disable PWM */
    }
    

    使用定时器模式2和中断实现的PWM输出

    • 使用定时器工作模式2
    • 定时器通过对变量tt做计数, 与scale做比较, 确定是否翻转电压
    • 这里scale分10个等级, scale=1时占比1/10个PWM周期(250us * 10 = 2.5ms), 在主循环里改变scale
    • 因为是低电平点亮LED, 所以tt<=scale的时间LED是暗的, scale增大时亮度变小, 这个可以根据自己电路的情况调整
    • 这样存在的问题是修改scale的值时, 可能正好在tt计数范围的中间, 导致输出出现毛刺, 可以通过增加一个中间变量来解决, 在tt计数时比较的是这个中间变量, 在周期结束时再用新值更新这个中间变量
    #include<reg51.h>
    sbit P10 = P1^0;
    sbit P11 = P1^1;
    unsigned int scale;   //占空比控制变量
    void main(void) {
      unsigned int n;     //延时循环变量
      TMOD = 0x02;        //定时器0,工作模式2, 8位定时, TL0溢出时自动重载TH0中的值
      TH0 = 0x06;         //定时, 250us一个中断 (12M晶振, 12分频后1MHz, 单次1us)
      TL0 = 0x06;         //初始值
      TR0 = 1;            //启动定时器0
      ET0 = 1;            //启动定时器0中断
      EA  = 1;            //开启总中断
      while(1) {
        for(n = 0; n < 50000; n++); //延时50ms
        scale++;                    //占空比控制, 自增
        if(scale == 10) scale = 0;  //使占空比从0-10循环变化
      }
    }
    
    timer0() interrupt 1 {
      static unsigned int tt;      //tt用来保存当前时间在一个时钟周期的位置
      tt++;                        //每中断一次,即每经过250us,tt的值自加1
      if (tt == 10) {              //中断10次定时2.5ms
        tt = 0;                    //使tt=0,开始新的周期,达到循环的效果
        P10 = 0;                   //点亮LED
      }
      if (tt <= scale) {           //如果占空比与中断次数相同时,此时输出高电平
        P10 = 1;                   //熄灭LED灯
      }
    }
    

    使用定时器模式2和中断实现的多路PWM输出

    实现多路PWM输出的思路

    1. 使用一个基础定时器, 定时器时间不能太大, 例如设置为100us, 可以用定时器模式2, 这样初始值能自动重置
    2. 设定一个PWM周期, 这个周期就是定时器间隔的整数倍, 例如10倍定时器周期, 就是1000us = 1ms
    3. 对于每个PWM通道
      • 设置一个计数, 计数在达到PWM周期时置零, 这是实现PWM周期的基础
      • 设置一个初始输出, 高电平或低电平
      • 设置一个输出宽度, 计数达到这个宽度值时翻转. 这个宽度决定了输出翻转的时间, 用于控制占空比
    4. 因为每个指令的执行时间需要1-2个CPU周期, 所以当通道数增加后, 误差会增大

    代码例子: 这里用8个位指定4个轮子的PWM输出, 每个轮子两位是为了控制轮子的正反向

    #include <reg52.h>
    
    typedef unsigned int u16;
    typedef unsigned char u8;
    
    // Wheel 0
    sbit  P1_0 = P1^0;
    sbit  P1_1 = P1^1;
    // Wheel 1
    sbit  P1_2 = P1^2;
    sbit  P1_3 = P1^3;
    // Wheel 2
    sbit  P1_4 = P1^4;
    sbit  P1_5 = P1^5;
    // Wheel 3
    sbit  P1_6 = P1^6;
    sbit  P1_7 = P1^7;
    
    /*
    Duty Cycle =  Toogle_P1_x / PWM_Period;
    */
    u8 PWM_Period = 128; // PWM Period = N * Timer delay(100us), between 10 - 254
    u8 Toggle_W0 = 0; // Toggle of Wheel 0
    u8 Dir_W0 = 0;    // Direction, 0:P1_0=0,P1_1=PWM, 1:P1_1=0,P1_0=PWM
    u8 Toggle_W1 = 0; // Toggle of Wheel 1
    u8 Dir_W1 = 0;    // Direction, 0:P1_2=0,P1_3=PWM, 1:P1_3=0,P1_2=PWM
    
    u8 Count_W0, Count_W1;
    
    void Time0_Init(void)
    {
      TMOD = 0x02; // Mode 2, 8-bit and auto-reload
      TH0 = 0x9C;  // 0x9c = 156, timer of 100us (12MHz OSC)
      TL0 = 0x9C;
      ET0 = 1;
      EA  = 1;
      TR0 = 1;
    
      EX0 = 1; EX1 = 1; // Enable external interrupt 0 and 1
      IT0 = 1; IT1 = 1; // Toggle = jump
    }
    
    void main()
    {
      Time0_Init();
      while(1);
    }
    
    void Timer0_IT() interrupt 1
    {
      // W0
      if(Count_W0 == Toggle_W0) {
        if (Dir_W0 == 0) { // P1_1=PWM
          P1_1 = 0;
        } else { // P1_0=PWM
          P1_0 = 0;
        }
      }
      if(Count_W0 == PWM_Period - 1) {
        Count_W0 = 0;
        if (Dir_W0 == 0) {
          P1_0 = 0;
          P1_1 = 1;
        } else {
          P1_0 = 1;
          P1_1 = 0;
        }
      } else {
        Count_W0++;
      }
     
      // W1
      if(Count_W1 == Toggle_W1) {
        if (Dir_W1 == 0) { // P1_3=PWM
          P1_3 = 0;
        } else { // P1_2=PWM
          P1_2 = 0;
        }
      }
      if(Count_W1 == PWM_Period - 1) {
        Count_W1 = 0;
        if (Dir_W1 == 0) {
          P1_2 = 0;
          P1_3 = 1;
        } else {
          P1_2 = 1;
          P1_3 = 0;
        }
      } else {
        Count_W1++;
      }
    
    }
    
    
    // W0 dir0->max
    void W0_dir0(void)
    {
      if (Dir_W0 == 0) {
        Toggle_W0++;
        if(Toggle_W0 > PWM_Period) {
          Toggle_W0 = PWM_Period;
        }
      } else {
        Toggle_W0--;
        if(Toggle_W0 == 0) {
          Dir_W0 = 0;
        }
      }
    }
    
    // W0 dir1->max
    void W0_dir1(void)
    {
      if (Dir_W0 == 0) {
        Toggle_W0--;
        if(Toggle_W0 == 0) {
          Dir_W0 = 1;
        }
      } else {
        Toggle_W0++;
        if(Toggle_W0 > PWM_Period) {
          Toggle_W0 = PWM_Period;
        }
      }
    }
    
    // W1 dir0->max
    void W1_dir0(void)
    {
      if (Dir_W1 == 0) {
        Toggle_W1++;
        if(Toggle_W1 > PWM_Period) {
          Toggle_W1 = PWM_Period;
        }
      } else {
        Toggle_W1--;
        if(Toggle_W1 == 0) {
          Dir_W1 = 0;
        }
      }
    }
    // W1 dir1->max
    void W1_dir1(void)
    {
      if (Dir_W1 == 0) {
        Toggle_W1--;
        if(Toggle_W1 == 0) {
          Dir_W1 = 1;
        }
      } else {
        Toggle_W1++;
        if(Toggle_W1 > PWM_Period) {
          Toggle_W1 = PWM_Period;
        }
      }
    }
    
    
    void IT0_INT() interrupt 0
    {
      W1_dir0();
    }
    
    void IT1_INT() interrupt 2
    {
      W1_dir1();
    }
    

    硬件PWM

    51系列单片机的增强型版本, 有些带PCA(Programmable Counter Array 可编程计数序列)模块, 可以通过PCA实现PWM的输出.

    PCA介绍

    PCA其实就是一个增强型的计数器, 这个计数器中的一些元素是可以在代码中设置的, 例如

    • 可以设置的计数脉冲源, 可以来自于系统时钟, 系统时钟可以是不分频, 2分频, 4分频, 6分频, 8分频等; 来自计数器; 来自外部输入的时钟
    • 可以设置计数的触发条件, 上升沿还是下降沿, 或者都计数. 最后这个计数方式, 可以用来计算脉宽
    • 可以设置16位的比较值
    • 不占用CPU资源, 这点很重要, 可以使输出更加精确和稳定
    • 因为上一点, 有些型号可以做到在CPU处于IDLE状态时继续计数(输出)

    可以用PCA实现PWM输出功能

    STC12C5A60S2系列PCA实现的PWM

    参考STC12C5A60S2的手册

    • 有两路输出, 默认PWM0:P1.3, PWM1:P1.4, 可以换到P4口: PWM0:P4.2, PWM1:P4.3
      • 这个在AUXR1里面控制
    • 两路共用PCA定时器, 定时器的频率由CMOD控制
      • 因为PWM输出是8位的, 所以定时器的频率/256就是PWM频率
    • 两路输出的占空比是独立变化的, 与当前的[EPCnL, CCAPnL]的值有关
      • 前者的值在 PCA_PWM0 PCA_PWM1 里控制
      • 后者的值在 CCAP0L,CCAP0H 和 CCAP1L,CCAP1H 里控制
      • 先输出低, 当CL的值大于等于[EPCnL, CCAPnL]时, 输出为高
      • 当CL由FF变为00时, 输出变低, 同时自动将[EPCnH, CCAPnH]的值装载到[EPCnL, CCAPnL], 实现无干扰更新PWM占空比

    下面的代码中, CCAP1H 控制的就是装载值, CCAP1L 控制的是比较值, PCA_PWM1 控制的是EPCnH 和 EPCnL

    • 如果 EPCnL = 0, 那么正常输出
    • 如果 EPCnL = 1, 那么会一直输出低电平
    #include <STC12C5A60S2.H>
    
    void main() {
      CCON = 0;            // Initial PCA control register
                           // PCA timer stop running
                           // Clear CF flag
                           // Clear all module interrupt flag
      CL = 0;              // Reset PCA base timer
      CH = 0;
      CMOD = 0x02;         // Set PCA timer clock source as Fosc/2
                           // Disable PCA timer overflow interrupt
      CCAP0H = CCAP0L = 0x80; // PWM0 port output 50% duty cycle suquare wave
      CCAPM0 = 0x42;       // PCA module-0 as 8-bit PWM, no PAC interrupt
      
      CCAP1H = CCAP1L = 0xFF; // PWM1port output 0% duty cycle square wave
      PCA_PWM1 = 0x03;     // PWM will keep low level
      CCAPM1 = 0x42;       // PCA module-0 as 8-bit PWM, no PAC interrupt
      
      CR = 1;              // PCA timer start run
      
      while(1);
      
    }
    

    PCA_PWM1的说明

    ;PCA_PWMn:    7       6     5   4   3   2     1       0
    ;           EBSn_1  EBSn_0  -   -   -   -  EPCnH  EPCnL
    
    ;B5-B2:		保留
    ;B1(EPCnH):	在PWM模式下,与CCAPnH组成9位数。
    ;B0(EPCnL):	在PWM模式下,与CCAPnL组成9位数。
    
    #define		PWM0_NORMAL()	PCA_PWM0 &= ~3             //PWM0正常输出(默认)
    #define		PWM0_OUT_0()	PCA_PWM0 |=  3             //PWM0一直输出0
    #define		PWM0_OUT_1()	PCA_PWM0 &= ~3, CCAP0H = 0 //PWM0一直输出1
    
    #define		PWM1_NORMAL()	PCA_PWM1 &= ~3             //PWM0正常输出(默认)
    #define		PWM1_OUT_0()	PCA_PWM1 |=  3             //PWM0一直输出0
    #define		PWM1_OUT_1()	PCA_PWM1 &= ~3, CCAP1H = 0 //PWM1一直输出1
    

    另一个例子

    void pwm() {  
      CMOD = 0x04;   //用定时器0溢出做PCA脉冲
      CL = 0x00;     //PCA定时器低8位 地址:E9H
      CH = 0x00;     //PCA高8位 地址 F9H
      CCON=0x00;
      CCAP0L = 0x60; //PWM模式时他俩用来控制占空比
      CCAP0H = 0x60; //0xff-0xc0=0x3f  64/256=25% 占空比(溢出)
      CCAPM0 = 0x42; //0100,0010 Setup PCA module 0 in PWM mode
                     // ECOM0=1使能比较 PWM0=1 使能CEX0脚用作脉宽调节输出
    /*********************
    PCA 模块工作模式设置 (CCAPMn 寄存器 n= 0-3四种)
     7     6        5        4       3       2     1      0
     -   ECOMn   CAPPn     CAPNn   MATn     TOGn   PWMn   ECCFn
    选项: 0x00 无此操作
          0x20 16位捕捉模式,由 CEXn上升沿触发
          0x10 16位捕捉模式,由CEXn下降沿触发
          0x30 16位捕捉模式,由CEXn的跳变触发
          0x48 16位软件定时器
          0x4c 16位高速输出
          0x42  8位PWM输出
    每个PCA模块另外还对应两个寄存器:CCAPnH 和 CCAPnL , 捕获或者比较时,它们用来
    保存16位计数值,当工作于PWM模式时,用来控制占空比
    *******************************/
      TMOD=0x02;
      TH0=0x06;
      TL0=0x06; 
      CR=1; //Start PCA Timer.
      TR0=1;
    }
    

    参考

  • 相关阅读:
    iOS(iPho“.NET研究”ne/iPad)开发新手必读 狼人:
    如何解决““.NET研究”呈现控件时出错”的问题 狼人:
    VS2010 测试功能之旅:编码的UI测试(4)通“.NET研究”过编写测试代码的方式建立UI测试(上) 狼人:
    ASP.NET MVC中对数据进行排序的方“.NET研究”法 狼人:
    Android用户界面设计:“.NET研究”创建列表视图程序 狼人:
    Silverlight 2.5D RPG游戏技巧与特效处理:“.NET研究”(四)天气系统 狼人:
    对抽“.NET研究”象编程:接口和抽象类 狼人:
    Silverlight 2.5D RPG游戏技巧与特效处理:(五“.NET研究”)圣赞之HLSL渲染动画 狼人:
    VS2010测试功能之旅:编码的“.NET研究”UI测试(2)操作动作的录制原理(上) 狼人:
    更改“.NET研究”SharePoint 的web.config设置的两种方式 狼人:
  • 原文地址:https://www.cnblogs.com/milton/p/14994422.html
Copyright © 2020-2023  润新知