一、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 <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <plat/regs-timer.h>
#include <mach/regs-irq.h>
#include <asm/mach/time.h>
#include <linux/clk.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/miscdevice.h>
#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!\n");
return ret;
}
printk (DEVICE_NAME " initialized!\n");
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 <stdio.h>
#include <termios.h>//POSIX终端控制定义
#include <unistd.h>//Unix 标准函数定义
#include <stdlib.h>
#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\n");
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( "\nBUZZER TEST ( PWMControl )\n" );
printf( "Press +/- to increase/reduce the frequency of theBUZZER\n" ) ;
printf( "Press 'ESC' key to Exit this program\n\n" );
while( 1 )
{
int key;
set_buzzer_freq(freq);//设置蜂鸣器频率
printf( "\tFreq =%d\n", 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);
编译后进行测试:
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
改变频率,蜂鸣器发声会随着改变