藍橋杯微控制器基礎之PWM(Pulse width modulation)

2022-01-10 13:00:08

概念

對於PWM的概念這裡就不過多贅述,大家可以直接檢視一些文章對於pwm的介紹,下面給一段我認為比較好的說法:

脈衝寬度調變(PWM)是一種對模擬訊號電平進行數位編碼的方法。通過高解析度計數器的使用,方波的佔空比被調變用來對一個具體模擬訊號的電平進行編碼。PWM訊號仍然是數位的,因為在給定的任何時刻,滿幅值的直流供電要麼完全有(ON),要麼完全無(OFF)。電壓或電流源是以一種通(ON)或斷(OFF)的重複脈衝序列被加到模擬負載上去的。通的時候即是直流供電被加到負載上的時候,斷的時候即是供電被斷開的時候。只要頻寬足夠,任何模擬值都可以使用PWM進行編碼。–參考百度百科

這是一個什麼意思呢?

為了後續理解的方便,我先給出PWM的兩個重要指標:

  1. 週期(頻率)
  2. 佔空比.

先來看一幅圖:
下圖是一個週期訊號,他的週期是3.70ms,其高電平所佔時間為1.85ms
我們就說這個訊號的週期為3.70ms,佔空比( 高電平時間/總週期時間)為50%

在這裡插入圖片描述
在這裡插入圖片描述
如果高電平是5V,低電平為0V,那麼這個訊號,在一個週期有一半時間是5V,一半時間是0V,它的電壓可以這樣計算
v = ( 5 + 0 ) ∗ 50 % = 2.5 伏 v = (5 + 0)*50\% = 2.5伏 v=(5+0)50%=2.5

那麼如果我們要產生4V的電壓如何算呢?
4 = ( 5 + 0 ) ∗ m 4 = (5+0)*m 4=(5+0)m
可以解得 m = 80 % m= 80\% m=80%,也就是說高電平需要佔整個週期得4/5。這樣說來我們可以通過佔空比來調節輸出電壓的大小,但是週期的大小又對pwm來說意味著什麼呢?

我們先看如下實驗,週期為20us,佔空比為10%

在這裡插入圖片描述

然後週期為20us,佔空比為90%

在這裡插入圖片描述

然後是週期為4ms,佔空比為10%
請新增圖片描述

可以看到週期太長,會導致閃爍,這個很好理解,如何你把週期設定為1s,那麼不久是在1s開燈,1s關燈嘛,所以說週期必須設定要夠小才能有效果,根據我的測試,週期為1ms以下效果就還是不錯的

程式碼思路

瞭解原理後,我們如何實現pwm呢?因為pwm涉及到定時,因此我們可以利用定時器來實現!
如果我們設定一個1us的定時器中斷,那麼每過1us,就會進入中斷,我把這個定時器1us叫做最低步進
設pwm引數為一個結構體,其定義如下
其中cnt為計數值,max表示計數的最大值,highCnt表示高電平所佔計數值

typedef struct pwm
{
	uint cnt;
	uint max;
	uint highCnt;
}pwm_t;
pwm_t pwm1 = {0, 20, 2};

在定時器中斷這樣寫:

void Timer0Handle() interrupt 1
{
	//如果cnt為0表示週期開始,那麼週期開始的時候是亮燈(也即低電平亮)
	if(pwm1.cnt == 0)
	{
		SL(LED, 0);
	}
	//如果cnt和highCnt相等,表示高電平的時間已經到了,因此這裡要滅燈(也即高電平滅)
	else if(pwm1.cnt == pwm1.highCnt)
	{
		SL(LED, 0xff);
	}
	//讓cnt變數在0-max之間不斷執行
	if(pwm1.cnt++ < pwm1.max);
	else
	{
		pwm1.cnt = 0;
	}
}

在這個程式碼中,我只需要修改highCnt的值就是修改佔空比了,因此要想實現呼吸燈的效果,我只需要在一段時間修改佔空比的值就行了,比如
在這裡插入圖片描述
效果如下

請新增圖片描述

pwm的思考

話說其實每次考試的時候,它都會說讓你pwm設定幾個等級,其實這個也就是說佔空比在整個週期中設定為幾個值,比如你是4個等級,如果週期為100份,那麼這四個等級的佔空比就是[1,25,50,100].
因此呀,pwm佔空比的值其實是整個週期的每一份都是可以用的。所以在保證週期足夠小的基礎上面,我們儘量選擇更細一點,讓我們有更多的週期份數可以用。
比如上面說的週期有100份,如果1份的時間是1s顯然是不行的!為1us的話,100份就是100us,而這個一份的時間就是我們定時器來提供的。

比如我們可以直接實現以下效果:
從左到右亮度逐漸增加
在這裡插入圖片描述
其中核心程式碼如下

uint pwm_cnt = 0;
uint pwm_max = 105;
uint pwm_value[] = {5, 10, 15, 20, 30, 40, 50, 105};
uchar ledValue = 0x00;

void Timer0Handle() interrupt 1
{
	//如果cnt為0表示週期開始,那麼週期開始的時候是亮燈(也即低電平亮)
	//因為都是佔空比的不同,但是起始的時候都是高電平,不同在於低電平來到的時間
	uchar i = 0;
	if(pwm_cnt == 0)
	{
		SL(LED, 0);
	}
	//讓cnt變數在0-max之間不斷執行
	if(pwm_cnt++ < pwm_max);
	else
	{
		//結束週期,ledValue全部亮起
		ledValue = 0x00;
		pwm_cnt = 0;
	}
	//遍歷8個燈的情況
	for(i=0; i<8; i++)
	{
		if(pwm_cnt == pwm_value[i])
		{
			//如果cnt等於對應燈的佔空比了,那麼這個燈就應該被關閉了
			ledValue |= (1 << i);
			SL(LED, ledValue);
		}
	}
}

同時上述程式碼也可以改為流水燈,效果如下
(實現亮度不同的狀態下,實現流水燈)
請新增圖片描述
核心程式碼:
就是要在每次pwm初始亮燈的時候考慮到led狀態的變數,而這個變數決定了到底led應該如何亮!
在這裡插入圖片描述

uchar maskValue = 0xff;

void Timer0Handle() interrupt 1
{
	//如果cnt為0表示週期開始,那麼週期開始的時候是亮燈(也即低電平亮)
	//因為都是佔空比的不同,但是起始的時候都是高電平,不同在於低電平來到的時間
	uchar i = 0;
	if(pwm_cnt == 0)
	{
		SL(LED, ledValue);
	}
	//讓cnt變數在0-max之間不斷執行
	if(pwm_cnt++ < pwm_max);
	else
	{
		//結束週期,注意:(如果要外部控制每個LED,那麼這裡就不是全部亮起了!)
		ledValue = maskValue;
		pwm_cnt = 0;
	}
	//遍歷8個燈的情況
	for(i=0; i<8; i++)
	{
		if(pwm_cnt == pwm_value[i])
		{
			//如果cnt等於對應燈的佔空比了,那麼這個燈就應該被關閉了
			ledValue |= (1 << i);
			SL(LED, ledValue);
		}
	}
	TimeRun(&delay1s);
}

void liushui()
{
	if(delay1s.ok)
	{
		static uchar i;
		delay1s.ok = 0;
		if(i++ < 8);
		else
		{
			i = 0;
		}
		maskValue = ~(1 << i);
	}
}

總結

相比經過上述的介紹,對pwm的使用,各位都應該有了一個比較清晰的認識了,我想說的是pwm是一個非常常規的知識,但是想要用好它,還是需要一定的思考的,對於藍橋杯歷年的題來說,pwm也是一個比較難的考點,大家平時還是要按照題目要求,多總結一下,多練練!

測試程式碼

週期為20us,佔空比為10%

#include "STC15F2K60S2.h"
#define uchar unsigned char
#define uint unsigned int
#define SELE(x) P2 = P2 & 0x1f | x << 5; P2 = P2 & 0x1f 	

code enum{LED = 4, EXT, SEL, CODE};

typedef struct pwm
{
	uint cnt;
	uint max;
	uint highCnt;
	uchar ok;
}pwm_t;

pwm_t pwm1 = {0, 20, 2, 0};

void SL(uchar _dev, uchar _data)
{
	P0 = _data;
	SELE(_dev);
}

void Timer0Handle() interrupt 1
{
	if(pwm1.cnt == 0)
	{
		SL(LED, 0);
	}
	else if(pwm1.cnt == pwm1.highCnt)
	{
		SL(LED, 0xff);
	}
	if(pwm1.cnt++ < pwm1.max);
	else
	{
		pwm1.cnt = 0;
	}
}


void Timer0Init(void)		//1微秒@12.000MHz
{
	AUXR |= 0x80;		//定時器時鐘1T模式
	TMOD &= 0xF0;		//設定定時器模式
	TL0 = 0xF4;		//設定定時初值
	TH0 = 0xFF;		//設定定時初值
	TF0 = 0;		//清除TF0標誌
	TR0 = 1;		//定時器0開始計時
	ET0 = 1;
}


void main()
{
	SL(LED, 0x0);
	Timer0Init();
	EA = 1;
	while(1)
	{
		
	}
}                                                 

呼吸燈

#include "STC15F2K60S2.h"
#define uchar unsigned char
#define uint unsigned int
#define SELE(x) P2 = P2 & 0x1f | x << 5; P2 = P2 & 0x1f 	

code enum{LED = 4, EXT, SEL, CODE};

typedef struct pwm
{
	uint cnt;
	uint max;
	uint highCnt;
}pwm_t;

typedef struct Delay
{
	uint max;
	uint cnt;
	uchar ok;
}t_delay;

pwm_t pwm1 = {0, 20, 2};
t_delay delay5000 = {5000, 0, 0};
void SL(uchar _dev, uchar _data)
{
	P0 = _data;
	SELE(_dev);
}

void TimeRun(t_delay* _time)
{
		if(_time->cnt++ < _time->max);
		else
		{
			_time->cnt = 0;
			_time->ok = 1;
		}
}

void Timer0Handle() interrupt 1
{
	//如果cnt為0表示週期開始,那麼週期開始的時候是亮燈(也即低電平亮)
	if(pwm1.cnt == 0)
	{
		SL(LED, 0);
	}
	//如果cnt和highCnt相等,表示高電平的時間已經到了,因此這裡要滅燈(也即高電平滅)
	else if(pwm1.cnt == pwm1.highCnt)
	{
		SL(LED, 0xff);
	}
	//讓cnt變數在0-max之間不斷執行
	if(pwm1.cnt++ < pwm1.max);
	else
	{
		pwm1.cnt = 0;
	}
	TimeRun(&delay5000);
}

void Timer0Init(void)		//1微秒@12.000MHz
{
	AUXR |= 0x80;		//定時器時鐘1T模式
	TMOD &= 0xF0;		//設定定時器模式
	TL0 = 0xF4;		//設定定時初值
	TH0 = 0xFF;		//設定定時初值
	TF0 = 0;		//清除TF0標誌
	TR0 = 1;		//定時器0開始計時
	ET0 = 1;
}

void huxi()
{
	//每5ms讓佔空比加
	if(delay5000.ok)
	{
		delay5000.ok = 0;
		pwm1.highCnt = (pwm1.highCnt + 1)%pwm1.max;
	}
}

void main()
{
	SL(LED, 0x0);
	Timer0Init();
	EA = 1;
	while(1)
	{
			huxi();
	}
}                                                 

8個亮度等級

#include "STC15F2K60S2.h"
#define uchar unsigned char
#define uint unsigned int
#define SELE(x) P2 = P2 & 0x1f | x << 5; P2 = P2 & 0x1f 	

code enum{LED = 4, EXT, SEL, CODE};


uint pwm_cnt = 0;
uint pwm_max = 105;
uint pwm_value[] = {5, 10, 15, 20, 30, 40, 50, 105};
uchar ledValue = 0x00;

void SL(uchar _dev, uchar _data)
{
	P0 = _data;
	SELE(_dev);
}


void Timer0Handle() interrupt 1
{
	//如果cnt為0表示週期開始,那麼週期開始的時候是亮燈(也即低電平亮)
	//因為都是佔空比的不同,但是起始的時候都是高電平,不同在於低電平來到的時間
	uchar i = 0;
	if(pwm_cnt == 0)
	{
		SL(LED, 0);
	}
	//讓cnt變數在0-max之間不斷執行
	if(pwm_cnt++ < pwm_max);
	else
	{
		//結束週期,ledValue全部亮起
		ledValue = 0x00;
		pwm_cnt = 0;
	}
	//遍歷8個燈的情況
	for(i=0; i<8; i++)
	{
		if(pwm_cnt == pwm_value[i])
		{
			//如果cnt等於對應燈的佔空比了,那麼這個燈就應該被關閉了
			ledValue |= (1 << i);
			SL(LED, ledValue);
		}
	}
	
}

void Timer0Init(void)		//1微秒@12.000MHz
{
	AUXR |= 0x80;		//定時器時鐘1T模式
	TMOD &= 0xF0;		//設定定時器模式
	TL0 = 0xF4;		//設定定時初值
	TH0 = 0xFF;		//設定定時初值
	TF0 = 0;		//清除TF0標誌
	TR0 = 1;		//定時器0開始計時
	ET0 = 1;
}



void main()
{
	SL(LED, 0x0);
	Timer0Init();
	EA = 1;
	while(1)
	{

	}
}                                                 

8個亮度等級流水燈

#include "STC15F2K60S2.h"
#define uchar unsigned char
#define uint unsigned int
#define SELE(x) P2 = P2 & 0x1f | x << 5; P2 = P2 & 0x1f 	

code enum{LED = 4, EXT, SEL, CODE};


uint pwm_cnt = 0;
uint pwm_max = 105;
uint pwm_value[] = {3, 20, 30, 50, 60, 90, 100, 105};
uchar ledValue = 0x00;
uchar maskValue = 0xff;

typedef struct Delay
{
	unsigned long max;
	unsigned long cnt;
	uchar ok;
}t_delay;

t_delay delay1s = {10000, 0, 0};

void SL(uchar _dev, uchar _data)
{
	P0 = _data;
	SELE(_dev);
}

void TimeRun(t_delay* _time)
{
		if(_time->cnt++ < _time->max);
		else
		{
			_time->cnt = 0;
			_time->ok = 1;
		}
}

void Timer0Handle() interrupt 1
{
	//如果cnt為0表示週期開始,那麼週期開始的時候是亮燈(也即低電平亮)
	//因為都是佔空比的不同,但是起始的時候都是高電平,不同在於低電平來到的時間
	uchar i = 0;
	if(pwm_cnt == 0)
	{
		SL(LED, ledValue);
	}
	//讓cnt變數在0-max之間不斷執行
	if(pwm_cnt++ < pwm_max);
	else
	{
		//結束週期,注意:(如果要外部控制每個LED,那麼這裡就不是全部亮起了!)
		ledValue = maskValue;
		pwm_cnt = 0;
	}
	//遍歷8個燈的情況
	for(i=0; i<8; i++)
	{
		if(pwm_cnt == pwm_value[i])
		{
			//如果cnt等於對應燈的佔空比了,那麼這個燈就應該被關閉了
			ledValue |= (1 << i);
			SL(LED, ledValue);
		}
	}
	TimeRun(&delay1s);
}

void liushui()
{
	if(delay1s.ok)
	{
		static uchar i;
		delay1s.ok = 0;
		if(i++ < 8);
		else
		{
			i = 0;
		}
		maskValue = ~(1 << i);
	}
}


void Timer0Init(void)		//1微秒@12.000MHz
{
	AUXR |= 0x80;		//定時器時鐘1T模式
	TMOD &= 0xF0;		//設定定時器模式
	TL0 = 0xF4;		//設定定時初值
	TH0 = 0xFF;		//設定定時初值
	TF0 = 0;		//清除TF0標誌
	TR0 = 1;		//定時器0開始計時
	ET0 = 1;
}



void main()
{
	SL(LED, 0x0);
	Timer0Init();
	EA = 1;
	while(1)
	{
			liushui();
	}
}                                                 

加群

在這裡插入圖片描述