一、测试环境
单片机型号:STC8G1K08-38I-TSSOP20,其他型号请自行测试;
IDE:Keil C51;
二、PWM功能配置
PWM输出需要用到PCA功能,相关的寄存器如下:

2.1 引脚选择
本文中,我们使用PCA0模块,将其映射到P1.1引脚,相关寄存器如下图,需要将P_SW1寄存器的B5/B4位,即CCP_S[1:0],设置为00;

2.2 计数脉冲源选择

如上图,PCA模式寄存器(CMOD)的CPS[2:0]位,选择计数脉冲源,因为蜂鸣器的驱动频率由可能是可变的,我们是想得到频率和占空比可调的PWM,选择系统时钟为输入时钟源的话显然不合适,因为时钟频率是固定的,PWM频率也就固定了;笔者使用的开发板,ECI脚没有外接时钟,因此也不能选择这个;选择定时器0的溢出脉冲正好可以满足需求,因为定时器的溢出频率是可以通过寄存器设置的,可变的;本文使用的时钟频率为22.1184MHz;
2.3 PWM频率计算
STC8G1K08的PWM有6/7/8/10四种模式,本文使用分辨率最高的10位PWM;
先看频率的计算公式:

可见频率就是时钟源的频率,除以2^n,其中n为PWM的位数;
可得
f P C A = 2 n f P W M f_{PCA}=2^n{ f_{PWM}} fPCA=2nfPWM
其中 f P C A f_{PCA} fPCA是定时器的溢出频率,定时器的溢出周期为其倒数,则
1 T = 2 n f P W M \frac{1}{T}=2^n{ f_{PWM}} T1=2nfPWM
定时器重载值的计算方法可参考51单片机定时器中断配置,从中可知,
65536 − R e l o a d f O S C = T \frac{65536-Reload}{f_{OSC}}=T fOSC65536−Reload=T
综上可得:
65536 − R e l o a d f O S C = 1 2 n f P W M \frac{65536-Reload}{f_{OSC}}=\frac{1}{2^n{ f_{PWM}}} fOSC65536−Reload=2nfPWM1
f P W M = f O S C 2 n ( 65536 − R e l o a d ) f_{PWM}=\frac{f_{OSC}}{{2^n(65536-Reload})} fPWM=2n(65536−Reload)fOSC
即定时器的重载值计算公式为:
R e l o a d = 65536 − f O S C 2 n f P W M Reload=65536-\frac{f_{OSC}}{2^n{ f_{PWM}}} Reload=65536−2nfPWMfOSC
注意:该重载值Reload是定时器的重载值,而不是PCA的寄存器EPCnH,XCCAPnH[1:0],CCAPnH[7:0]的值!
2.4 PWM占空比计算
PWM的占空比,与计数值寄存器CL、CH,和比较值寄存器EPCnL、XCCAPnL、CCAPnL的比值有关,当计数值小于比较值时,输出低电平,当计数值大于等于比较值时,输出高电平,也就是说,在 2 n 2^n 2n个计数值中,计数值在[0~cmp)区间时(比较值记为cmp),输出低电平,其余的( 2 n − c m p 2^n-cmp 2n−cmp)个为高电平,则占空比计算公式为:
D P W M 100 = 2 n − c m p 2 n \frac{D_{PWM}}{100}=\frac{2^n-cmp}{ 2^n} 100DPWM=2n2n−cmp
为方便计算,占空比的单位为1%,即DPWM=50,则实际占空比为50%;
可得比较值的计算公式如下:
c m p = ( 1 − D P W M 100 ) ∗ 2 n cmp=(1- \frac{D_{PWM}}{100})*2^n cmp=(1−100DPWM)∗2n
2.5 主要代码
芯片手册中关于10位PWM的描述如下图:
配置完成初始化后,只需要在PCA计数寄存器CH、CL溢出后,再次将重载值载入寄存器即可;虽然10位比较器的重载值及比较值是11位,但是从下面的结构框图可以看出,计数器的最高位实际是0,那么重载值和比较值的最高位也是0,否则计数值永远也比不过比较值,因此重载值和比较值实际还是10位的,与PWM的位数一致;

初始化程序:
c
/*******************************************************************************
* 函数名:T0_Init
* 功 能:定时器T0初始化
* 参 数:无
* 返回值:无
* 说 明:用作PWM的输入时钟源
*******************************************************************************/
void T0_Init(void)
{
AUXR |= 0x80;//T0为1T模式
TMOD &= 0xFC;//T0为16位自动重载
}
/*******************************************************************************
* 函数名:PCA_Init
* 功 能:PCA初始化
* 参 数:无
* 返回值:无
* 说 明:PCA模块0的PWM模式
*******************************************************************************/
void PCA_Init(void)
{
P_SW1 &= 0xCF;//P1.1
CCON = 0x00;//停止PCA计数
CMOD = 0x04;//计数源选择定时器0的溢出脉冲
CL = 0x00;//PCA计数器
CH = 0x00;
CCAPM0 = 0x42;//允许PCA模块0的PWM输出
PCA_PWM0 |= 0xC0;//10位PWM
}
PWM驱动程序:
c
/*******************************************************************************
* 函数名:PWM_Start
* 功 能:开始PWM输出
* 参 数:无
* 返回值:无
* 说 明:无
*******************************************************************************/
void PWM_Start(void)
{
TF0 = 0;//清除T0溢出中断标志
TR0 = 1;//定时器T0开始计时
CR = 1;//启动PCA计数
}
/*******************************************************************************
* 函数名:PWM_Stop
* 功 能:停止PWM输出
* 参 数:无
* 返回值:无
* 说 明:无
*******************************************************************************/
void PWM_Stop(void)
{
TF0 = 0;//清除T0溢出中断标志
TR0 = 0;//定时器T0停止计时
ET0 = 0;//禁止定时器中断
CR = 0;//停止PCA计数
}
/*******************************************************************************
* 函数名:PWM_SetFreq
* 功 能:设置PWM频率
* 参 数:Freq:频率,1Hz~345600Hz
* 返回值:无
* 说 明:PCA模块0,计数源选择定时器0的溢出脉冲
输出引脚P1.1
PWM频率=PCA时钟源输入频率/(2^n), n=PWM模式的位数,6,7,8,10;此处n=10;
可知PWM频率>=FOSC/(65536*2^n);当FOSC=22.1184MHz时,n=10,PWM频率>=0.33Hz,
n=10,f=0.33Hz~21600Hz
*******************************************************************************/
void PWM_SetFreq(uint32_t Freq)
{
uint16_t u16Reload = 0;//定时器重载值
u16Reload = (uint16_t)((uint32_t)65536 - (((uint32_t)FOSC >> 10)/ Freq));
TL0 = (uint8_t)((u16Reload & 0xFF) >> 0); //设置定时初始值
TH0 = (uint8_t)(u16Reload >> 8); //设置定时初始值
}
/*******************************************************************************
* 函数名:PWM_SetDuty
* 功 能:PWM设置占空比
* 参 数:Duty:占空比,整数,*100
* 返回值:无
* 说 明:D/100=(2^n-比较值)/(2^n),则比较值=(100-D)*(2^n)/100,n=10
*******************************************************************************/
void PWM_SetDuty(uint8_t Duty)
{
uint16_t u16Cmp = 0;//比较值
uint8_t temp = 0;
u16Cmp = (100 - (uint16_t)Duty) * 256 / 25;//根据公式化简
u16Cmp &= 0x03FF;//最多10位
temp = (u16Cmp >> 4) & 0x30;
PCA_PWM0 = (PCA_PWM0 & 0xCF) | temp;//先写高2位,赋值给寄存器的bit5/bit4
CCAP0H = (uint8_t)(u16Cmp & 0xFF);//低8位
}
/*******************************************************************************
* 函数名:PWM_Config
* 功 能:PWM配置
* 参 数:Freq:频率
Duty:占空比,*1%
* 返回值:无
* 说 明:无
*******************************************************************************/
void PWM_Config(uint32_t Freq, uint8_t Duty)
{
PWM_SetFreq(Freq);
PWM_SetDuty(Duty);
}
三、实际输出效果
设置频率为2000Hz、占空比为50%,通过以下代码设置:
c
PWM_Config(2000, 50);
实际输出的PWM波形如下:

可看出占空比为50%,但频率实际为2.17KHz,这是因为实际计算出的定时器重载值为65526,有取整误差,将该值代入频率计算公式,得到实际频率为2160Hz,与逻辑分析仪测得的频率基本一致;
如果设置频率为800Hz,占空比为75%,实际输出的波形如下图:

可看出频率及占空比都较为准确;
四、总结
1.STC8G1K08的PCA模块输出PWM,如果想要频率及占空比均可调,需要使用定时器0的溢出脉冲作为时钟源;
2.虽然规格书上说的重载值、比较值都比PWM的位数多一位,但实际最高位都是0,所以实际操作的位数与PWM的位数一致;
3.由于计算时的取整误差,输出的频率与理论频率可能有误差;