一、S3C2440PWM定时器概述
PWM ( Pulse Width Modulation ) —— 脉宽调制,它是利用微
控制器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用于测量、通信、功率控制与变换等许多领域 。
S3C2440A 有5个16位定时器。其中定时器 0、1、2 和3具有脉宽调制(PWM)功能。定时器 4 是一个无输出引脚的内部定时器。定时器0还包含用于大电流驱动的死区发生器。定时器 0和 1共用一个8位预分频器,定时器2、3和 4 共用另外的8 位预分频器。每个定时器都有一个可以生成5 种不同分频信号(1/2,1/4,1/8,1/16 和TCLK)的时钟分频器。每个定时器模块从相应8 位预分频器得到时钟的时钟分频器中得到其自己的时钟信号。8 位预分频器是可编程的,并且按存储在 TCFG0 和 TCFG1寄存器中的加载值来分频PCLK。
定时计数缓冲寄存器(TCNTBn)包含了一个当使能了定时器时的被加载到递减计数器中的初始值。定时比较缓冲寄存器(TCMPBn)包含了一个被加载到比较寄存器中的与递减计数器相比较的初始值。这种 TCNTBn 和TCMPBn的双缓冲特征保证了改变频率和占空比时定时器产生稳定的输出。
每个定时器有自己的16bit down-counter(递减计数器),由定时器时钟驱动.当down-counter为0,定时器中断请求产生来通知CPU定时器操作以及完成了.当定时器的计数器为0,相关的TCNTBn的值会自动的加载到down-counter中来继续下一次操作.然而,当定时器停止,比如在定时器运行中清除TCONn的定时器使能位,TCNTBn不会重加载到计数器中。
TCMPBn的值被用于PWM.当down-counter的值与比较寄存器的值吻合时,定时器控制逻辑会改变输出电平.所以比较寄存器决定PWM输出的打开时间.
二、PWM 定时器操作
预脉冲分频器 & 时钟分频器
一个定时器(除了第5个定时器)有TCNTBn, TCNTn, TCMPBn 和 TCMPn.(TCNTn和TCMPn是内部寄存器名字,TCNTn寄存器能从TCNTOn寄存器中读取)当定时器为0时,TCNTn 和 TCMPn从TCNTBn和TCMPBn寄存器中加载值。当TCNTn 到0 时,如果中断使能了,将出现一个中断请求。
自动加载 和 双缓冲
S3C2440A PWM定时器有双缓冲功能。可以在不停止当前定时器的情况下设置下一轮的定时操作。因此,虽然新的定时器的值被设置了,当前定时器操作可以成功的完成。定时器的值可以写到TCNTBn,而当前定时器的计数值可以从TCNTOn获得。即,从TCNTBn获得的不是当前数值而是下一次计数的初始值。
当TCNTn达到0时,TCNTn自动加载TCNTBn中的值。仅仅当TCNTn的值递减到 0 且自动加载使能的时候,TCNTBn,中的值会被加载到TCNTn。如果TCNTn的值递减到 0且自动加载位是0,TCNTn不会在任何缓冲中操作。
当down-counter为0,定时器的自动重加载操作就会动作.所以user要预先定义TCNTn的初始值.在这种情况下,初始值通过手动更新位进行加载.下面的步骤描述如何启动一个定时器:
1.向TCNTBn和TCMPBn中写初始值
2.设置定时器的手动更新位.推荐配置inverter on/off bit(不管用不用)
3.设置定时器的开始位来启动定时器(同时清除手动更新位)
如果定时器被强行停止,TCNTn保持计数器的值而且不会从TCNTBm中重加载.如果要设置一个新值,要执行手动更新.
注意:不论何时TOUT inverter on/off bit被更改,在定时器运行时TOUTn的逻辑值都会改变.因此,最好在配置手动更新位的时候配置inverter on/off bit.
三、脉冲调制宽度(PWM)
通过使用TCMPBn来实现PWM功能.PWM的频率由TCNTBn来决定.减少TCMPBn的值可以有更高的PWM值.增加TCMPBn的值可以有更低的PWM值.如果输出反转使能了,增加和减少操作也要反转。双缓冲功能允许在ISR中将下一次PWM的TCMPBn的值在当前PWM的周期的任何一个时间点被写入.
四、输出电平控制
假定反转功能是关闭的,下面的步骤描述如何保证TOUT是高还是低:
1.关闭自动重加载位.TOUTn是高电平,当TCNTn为0时定时器停止.
2.通过清除定时器的开始位来停止定时器.如果TCNTn<=TCMPn,输出高;如果TCNTn>TCMPn,输出低.
3.可以通过TCON的反转开关来决定TOUT是否反转.反转器会移除额外的电流来适应输出电平.
五、PWM的控制
最后就是对PWM的控制,它是通过寄存器TCON来实现的,一般来说每个定时器主要有4个位要配置(定时器0多一个死区位):
启动/终止位,用于启动和终止定时器;
手动更新位,用于手动更新TCNTBn和TCMPBn,这里要注意的是在开始定时时,一定要把这位清零,否则是不能开启定时器的;
输出反转位,用于改变输出的电平方向,使原先是高电平输出的变为低电平,而低电平的变为高电平;
自动重载位,用于TCNTn减为零后重载TCNTBn里的值,当不想计数了,可以使自动重载无效,这样在TCNTn减为零后,不会有新的数加载给它,那么TOUTn输出会始终保持一个电平(输出反转位为0时,是高电平输出;输出反转位为1时,是低电平输出),这样就没有PWM功能了,因此这一位可以用于停止PWM。
六、mini2440开发板上PWM控制蜂鸣器的硬件原理
又电路图可知,电路使用的PWM定时器输出端口为TOUT0,即定时器0 。由S3C2440datasheet知:
定时器输入时钟频率 = PCLK / {预分频值+1} / {分频值}
{预分频值} = 0~255
{分频值} = 2, 4, 8, 16
Prescaler 1 由定时器配制寄存器 0(TCFG0) 的 [15:8] 位表示,该 8 位决定了定时器 2,3 和 4 的预分频值 ,Prescaler 0 [7:0] 该 8 位决定了定时器 0 和 1 的预分频值 。
定时器配制寄存器 1(TCFG1):主要用于DMA通道选择、PWM定时器的MUX输入(定时器DMA模式:当定时器计数减为0时,普通模式下便会向CPU发
出INT,而在DMA模式下会向CPU发出DMA请求 )
定时器控制寄存器 1(TCON):决定了各定时器的加载方式,变相器开关,手动更新和定时器启动
定时器 0计数缓冲寄存器(TCNTB0):当定时器使能的时候,被加载到递减寄存器中的初始值(重载初值寄存器)
比较缓冲寄存器(TCMPB0) :加载到比较寄存器中与递减寄存器相比较的初值,决定占空比
定时器 0计数监视寄存器(TCNTO0):能读出TCNTNn和TCNTBn的值
七、实现方式
又上面的原理及硬件连接可知,要想用PWM控制蜂鸣器我们需要如下工作:
1、设置GPB0引脚为PWM输出
2、设置Timer0输出是占空比50%的方波,Timer0的输出频率就是发声频率,改变输出频率即可改变蜂鸣器发声。需要详细设置的有:
(1)向TCNTBn和TCMPBn写入初始值。
(2)置位相应定时器的手动更新位,不管是否使用倒相功 能,推荐设置倒相位
(3)启动定时器,清除手动更新位。
定时器时钟频率初始化:定时器的输入时钟频率fTclk=PCLK/(预分频值+1)/(分频值)(定时器频率:每秒会把计数器减去该频率,即每秒减去计数器值得个数叫定时器频率)。PWM输出时钟频率=f Tclk∕TCNTBn
电平反转的原理:当TCNTn的值和TCMPn的值相同时,定时器n有一个反转,会在出书引脚输出一个电平,然后TCNTn继续减1,直至为0,再发生一次反转,在引脚输出一个电平,这样就实现了高低电平的反转,减为0时会触发中断。PWM输出信号占空比 = TCMPBn∕ TCNTBn
设置定时器计数值:看项目要求需要输出怎样的方波,譬如需要led灯每隔0.5s闪烁一次,则方波周期t=1s,假设比较值为0 ,则需要每隔0.5秒输出引脚反转一次,而定时器的频率假设为50mhz/(49+1)/16,分频后频率为62500hz,即每秒计数62500,就是说计数到62500个数后电平反转一次,而要求0.5秒反转一次,所以应该把计数初值设为/2
自动重载:自动重载在TCNTn的只为0时,将TCNTBn的值重装到TCNTn中,TCMPBn的值会重装到TCMPn中,可以从TCNTon读取TCNTn当前的值
变相器开关决定了起始输出为高电平还是低电平,也就决定了pwm输出的形式(输出波形图的不同,当定时器停止工作后输出的是最后时刻的电平状态,这样也不一样,通过在开始的时候设置变相器开关的转台就保证了一些
设备对电平的要求)
具体实现代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "pwm" //设备名
#define PWM_IOCTL_SET_FREQ 1 //定义宏变量,用于后面的 ioctl 中的控制命令
#define PWM_IOCTL_STOP 0 //定义信号量 lock
//定义信号量 lock用于互斥,因此,该驱动程序只能同时有一个进程使用
static struct semaphore lock;
/* freq: pclk/50/16/65536 ~ pclk/50/16
* if pclk = 50MHz, freq is 1Hz to 62500Hz
* human ear(人儿能辨别的) : 20Hz~ 20000Hz
*/
/*配置各个寄存器,设置PWM的频率*/
static void PWM_Set_Freq( unsigned long freq )
{
unsigned long tcon;
unsigned long tcnt;
unsigned long tcfg1;
unsigned long tcfg0;
struct clk *clk_p;
unsigned long pclk;
//设置GPB0 为TOUT0,pwm 输出
s3c2410_gpio_cfgpin(S3C2410_GPB(0), S3C2410_GPB0_TOUT0);
/*读取定时器控制寄存器的数值*/
tcon = __raw_readl(S3C2410_TCON);
/*读取定时器配置寄存器1的值*/
tcfg1 = __raw_readl(S3C2410_TCFG1);
/*读取定时器配置寄存器0的值*/
tcfg0 = __raw_readl(S3C2410_TCFG0);
/*设置prescaler = 50*/
//S3C2410_TCFG_PRESCALER0_MASK定时器0 和1 的预分频值的掩码,TCFG[0~7]
tcfg0 &= ~S3C2410_TCFG_PRESCALER0_MASK;
tcfg0 |= (50 - 1);
/*设置分频值为16*/
//S3C2410_TCFG1_MUX0_MASK 定时器0 分割值的掩码TCFG1[0~3]
tcfg1 &= ~S3C2410_TCFG1_MUX0_MASK;
//定时器0 进行16 分割
tcfg1 |= S3C2410_TCFG1_MUX0_DIV16;
/*将设置的参数值写入相应的寄存器中*/
//把tcfg1 的值写到分割寄存器S3C2410_TCFG1 中
__raw_writel(tcfg1, S3C2410_TCFG1);
//把tcfg0 的值写到预分频寄存器S3C2410_TCFG0 中
__raw_writel(tcfg0, S3C2410_TCFG0);
/*开启对应时钟源,并获取pclk*/
clk_p = clk_get(NULL, "pclk");
//获得pclk的时钟频率
pclk = clk_get_rate(clk_p);
/*得到定时器的输入时钟,进而设置PWM的调制频率和占空比*/
tcnt = (pclk/50/16)/freq;
//PWM 脉宽调制的频率等于定时器的输入时钟
__raw_writel(tcnt, S3C2410_TCNTB(0));
//占空比是50%
__raw_writel(tcnt/2, S3C2410_TCMPB(0));
/*失能死区,开启自动重载, 关闭变相, 更新TCNTB0&TCMPB0, 启动timer0*/
tcon &= ~0x1f;
tcon |= 0xb;
//把tcon 写到计数器控制寄存器S3C2410_TCON 中
__raw_writel(tcon, S3C2410_TCON);
//clear manual update bit
tcon &= ~2;
__raw_writel(tcon, S3C2410_TCON);
}
static void pwm_stop(void)
{
//设置GPB0 为输出
s3c2410_gpio_cfgpin(S3C2410_GPB(0), S3C2410_GPIO_OUTPUT);
//设置GPB0 为低电平,使蜂鸣器停止
s3c2410_gpio_setpin(S3C2410_GPB(0), 0);
}
static int pwm_open(struct inode *inode, struct file *file)
{
if (!down_trylock(&lock)) //是否获得信号量,是down_trylock(&lock)=0,否则非0
return 0;
else
return -EBUSY; //返回错误信息:请求的资源不可用
}
static int pwm_close(struct inode *inode, struct file *file)
{
pwm_stop();
//释放信号量lock
up(&lock);
return 0;
}
/*cmd 是1,表示设置频率;cmd 是2 ,表示停止pwm*/
static int pwm_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
switch (cmd)
{
case PWM_IOCTL_SET_FREQ: //if cmd=1 即进入case PWM_IOCTL_SET_FREQ
if (arg == 0) //如果设置的频率参数是0
return -EINVAL; //返回错误信息,表示向参数传递了无效的参数
PWM_Set_Freq(arg); //否则设置频率
break;
case PWM_IOCTL_STOP: // if cmd=0 即进入case PWM_IOCTL_STOP
pwm_stop(); //停止蜂鸣器
break;
}
return 0; //成功返回
}
/*初始化设备的文件操作的结构体*/
static struct file_operations pwm_fops = {
.owner = THIS_MODULE,
.open = pwm_open,
.release = pwm_close,
.ioctl = pwm_ioctl,
};
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &pwm_fops,
};
static int __init pwm_init(void)
{
int ret;
init_MUTEX(&lock); //初始化一个互斥锁
ret = misc_register(&misc); //注册一个misc 设备
if(ret < 0)
{
printk(DEVICE_NAME "register falid!
");
return ret;
}
printk (DEVICE_NAME " initialized!
");
return 0;
}
static void __exit pwm_exit(void)
{
misc_deregister(&misc);
}
module_init(pwm_init);
module_exit(pwm_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("DreamCatcher");
MODULE_DESCRIPTION("MINI2440 PWM Driver");
编写本目录下的Makefile文件
KERN_DIR = /work/armlinux/linux-2.6.32.2
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m := mini2440_pwm.o
为了测试该驱动程序,我们还需要编写一个简单的测试程序,在友善官方提供的光盘中已经提供了该测试程序的源代码
#include
#include //POSIX终端控制定义
#include //Unix 标准函数定义
#include
#define PWM_IOCTL_SET_FREQ1
#define PWM_IOCTL_STOP0
#defineESC_KEY0x1b//定义ESC_KEY为ESC按键的键值
static int getch(void)//定义函数在终端上获得输入,并把输入的量(int)返回
{
struct termios oldt,newt;//终端结构体struct termios
int ch;
if (!isatty(STDIN_FILENO)) {//判断串口是否与标准输入相连
fprintf(stderr, "thisproblem should be run at a terminal
");
exit(1);
}
// save terminal setting
if(tcgetattr(STDIN_FILENO, &oldt) < 0) {//获取终端的设置参数
perror("save the terminalsetting");
exit(1);
}
// set terminal as need
newt = oldt;
newt.c_lflag &= ~( ICANON | ECHO );//控制终端编辑功能参数ICANON表示使用标准输入模式;参数ECH0表示显示输入字符
if(tcsetattr(STDIN_FILENO,TCSANOW,&newt) < 0) {//保存新的终端参数
perror("set terminal");
exit(1);
}
ch = getchar();
// restore termial setting
if(tcsetattr(STDIN_FILENO,TCSANOW,&oldt) < 0) {//恢复保存旧的终端参数
perror("restore the termialsetting");
exit(1);
}
return ch;
}
static int fd = -1;
static void close_buzzer(void);
static void open_buzzer(void)//打开蜂鸣器
{
fd = open("/dev/pwm", 0);//打开pwm设备驱动文件
if (fd < 0) {
perror("open pwm_buzzer device");//打开错误,则终止进程。退出参数为1
exit(1);
}
// any function exit call will stop thebuzzer
atexit(close_buzzer);//退出回调close_buzzer
}
static void close_buzzer(void)//关闭蜂鸣器
{
if (fd >= 0) {
ioctl(fd, PWM_IOCTL_STOP);//停止蜂鸣器
close(fd);//关闭设备驱动文件
fd = -1;
}
}
static void set_buzzer_freq(int freq)
{
// this IOCTL command is the key to set frequency
int ret = ioctl(fd, PWM_IOCTL_SET_FREQ, freq);//设置频率
if(ret < 0) {//如果输入的频率错误
perror("set the frequency ofthe buzzer");
exit(1);//退出,返回1
}
}
static void stop_buzzer(void)//关闭蜂鸣器
{
int ret = ioctl(fd, PWM_IOCTL_STOP);
if(ret < 0) {
perror("stop the buzzer");
exit(1);
}
}
int main(int argc, char **argv)
{
int freq = 1000 ;
open_buzzer();//打开蜂鸣器
printf( "
BUZZER TEST ( PWMControl )
" );
printf( "Press +/- to increase/reduce the frequency of theBUZZER
" ) ;
printf( "Press 'ESC' key to Exit this program
" );
while( 1 )
{
int key;
set_buzzer_freq(freq);//设置蜂鸣器频率
printf( " Freq =%d
", freq );
key = getch();
switch(key) {
case '+':
if( freq < 20000 )
freq += 10;
break;
case '-':
if( freq > 11 )
freq -= 10 ;
break;
case ESC_KEY:
case EOF:
stop_buzzer();
exit(0);
default:
break;
}
}
}
编译后进行测试:
root@MINI2440:/opt# ./pwm_test
BUZZER TEST ( PWM Control )
Press +/- to increase/reduce the frequency of the BUZZER
Press 'ESC' key to Exit this program
Freq = 1000
Freq = 990
Freq = 980
Freq = 990
Freq = 1000
Freq = 1010
Freq = 1020
Freq = 1030
Freq = 1040
改变频率,蜂鸣器发声会随着改变