STC MCU的軟件和硬件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;
}

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM