基于S3C2440芯片linux系统下的pwm波驱动蜂鸣器
2014-06-19 19:34
429 查看
本驱动用的开发板是:
飞凌公司的OK2440开发板;
Linux内核版本是:linux2.6.35;
编译器:arm-linux-gcc-4.3.2
这里蜂鸣器一端连的是GPB0/TOUT0这个引脚,说明这里用到了定时器0,我们要用定时器0一产生这个PWM波,用以驱动这个无源蜂鸣器发声(有源蜂鸣是有振荡源,通直流电就能发声的);先说一下定时器,S3C2440芯片共有5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM)。定时器的工作原理可以用以下的定时器硬件原理图来说明。
1)每个定时器都有一个比较缓存寄存器(TCMPB)和一个计数缓存寄存器(TCNTB);
2)定时器0、1共享一个8位的预分频器(预定标器),定时器2、3、4共享另一个8位的预分频器(预定标器),其值范围是0~255;
3)定时器0、1共享一个时钟分频器,定时器2、3、4共享另一个时钟分频器,这两个时钟分频器都能产生5种不同的分频信号值(即:1/2、1/4、1/8、1/16和TCLK);
4)两个8位的预分频器是可编程的且根据装载的值来对PCLK进行分频,预分频器和钟分频器的值分别存储在定时器配置寄存器TCFG0和TCFG1中;
5)有一个TCON控制寄存器控制着所有定时器的属性和状态,TCON的第0~7位控制着定时器0、第8~11位控制着定时器1、第12~15位控制着定时器2、第16~19位控制着定时器3、第20~22位控制着定时器4。
还是根据S3C2440手册的描述和上图的结构,要开始一个PWM定时器功能的步骤如下(假设使用的是第一个定时器):
1)分别设置定时器0的预分频器值和时钟分频值,以供定时器0的比较缓存寄存器和计数缓存寄存器用;
2)设置比较缓存寄存器TCMPB0和计数缓存寄存器TCNTB0的初始值(即定时器0的输出时钟频率);
3)关闭定时器0的死区生成器(设置TCON的第4位);
4)开启定时器0的自动重载(设置TCON的第3位);
5)关闭定时器0的反相器(设置TCON的第2位);
6)开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位);
7)启动定时器0(设置TCON的第0位);
8)清除定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位)。
由此可以看到,PWM的输出频率跟比较缓存寄存器和计数缓存寄存器的取值有关,而比较缓存寄存器和计数缓存寄存器的值又跟预分频器和时钟分频器的值有关;要使用PWM功能其实也就是对定时器的相关寄存器进行操作。手册上也有一个公式:定时器输出频率 = PCLK / {预分频器值 + 1} / 时钟分频值。总的来说,就是设置几个与定时器相关的寄存器的值,即可以让定时器输出一定频率的PWM波。
(2).本驱动采用混杂设备的形式注册设备,内核中用到了3个操作函数对设备进行操作,分别是ok2440_pwm_open();
ok2440_pwm_close();ok2440_pwm_ioctl(),简而言之,这里的open作用是获得设备锁,close是释放设备锁,ioctl就是对定时器0各相关寄存器的操作并包括最终启动定时器0,让其输出一pwm波。这里设备锁的引入是为了避免资源并发性产生的恶态竞争。
在应用程序中调用了ioctl()函数,通过此函数向内核传递期望频率的数值。
三、驱动程序和应用程序源码
(1)驱动程序
(2)应用程序
四、再进一步,要想使用定时1,定时2,定时3也产生PWM波形的话,就得先来了解一下上面所说的跟定时器相关的几个常用寄存器;这几个定时器分别是1.TCFG0寄存器(用于预分频);2.TCFG1寄存器(用于二次分频);3.TCON寄存器(定时器总控制寄存器);4.TCNTB0/TCMPB0/TOUT0寄存器(分别用来保存定时数值和翻转时标识数和观察TCONT0的数)。
PWM定时器常用寄存器,以定时器0为例
(1).TCFG0寄存器(TIMER CONFIGURATION),用于控制预分频器,其地址和各位含义如图8所示
图8 TCFG0寄存器地址和各位含义
定时器的输入频率计算公式为:定时器工作频率=PCLK/(prescaler value+1)/(divider value),其中prescaler value的值(预分频器)通过TCFG0寄存器设置,TCFG0[0:7]设置定时器0和定时器1的prescaler
value值,TCFG0[8:15]设置定时器2、3、4的prescaler value的值,该值的范围为0~255,本例程选择定时器0的该值为99,因此该寄存器的值设置为99。divider value的值(第二级分频器)为2、4、8、16,由TCFG1寄存器设置
(2).TCFG1寄存器(TIMER CONFIGURATION),经预分频器得到的时钟被输入到第二级分频器,可以再次被2分频、4分频、8分频和16分频,由图6可知,定时器0、1还可以工作在外接时钟TCLK0下,定时器2、3、4还可以工作在外接时钟TCLK1下,使用TCFG1寄存器来设置这5个定时器的第二级分频器的分频数,其地址和各位含义如图9所示
图9 TCFG1寄存器地址和各位含义
TCFG1[20:23]用于设置5个定时器的DMA模式,TCFG1[0:3]设置定时器0的分频数。本例程中不使用定时器的DMA模式,定时器0的第二级分频选择1/16,所以该寄存器的值为0x3。
(3).TCON寄存器(TIMER CONTROL),其地址和各位含义如图10所示
图10 TCON地址和各位含义
TCON[0]设置定时器0的开启和停止;TCON[1]设置定时器0“手动更新”,将TCMPB0/TCNTB0的值装入内部寄存器TCMP0/TCNT0中;TCON[2]设置TOUT0是否反转;TCON[3]设置自动加载。对于本例程,使用定时器0,开启定时器、手动更新、反转、自动加载。
(4).TCNTB0/TCMPB0/TOUT0寄存器,TCNTB0用于保存定时器初始计数值,TCMPB0用于保存比较值,TOUT0用来观察TCNT0的数值;其地址和各位含义如图11所示
图11 TCNTB0/TCMPB0/TOUT0寄存器地址和各位含义
五.了解过上面的几个寄存器后,下面来看看从定时器1产生PWM波的驱动程序和应用程序源代码
下面是驱动程序:
下面是应用程序:
总结一下,这与定时器0产生的pwm波主要有两点区别:
第一:设置定时器的输出时钟频率,它是以PCLK为基准,再除以用寄存器TCFG0配置的prescaler参数,和用寄存器TCFG1配置的divider参数。
tcfg0 |= (50 - 1); // 预分频为50,
tcfg1 &= ~S3C2410_TCFG1_MUX1_MASK; //S3C2410_TCFG1_MUX0_MASK定时器1分割值的掩码:TCFG1[0~3]
tcfg1 |= S3C2410_TCFG1_MUX1_DIV16; //定时器1进行16分割
__raw_writel(tcfg1, S3C2410_TCFG1); //把tcfg1的值写到分割寄存器S3C2410_TCFG1中
__raw_writel(tcfg0, S3C2410_TCFG0); //把tcfg0的值写到分割寄存器S3C2410_TCFG0中
##修改的部分为将计时器0的复用器改成计时器1,即把S3C2410_TCFG1_MUX0_MASK和S3C2410_TCFG1_MUX0_DIV16中的MUX0改成MUX1;
第二:对 PWM 的控制,它是通过寄存器 TCON 来实现的,一般来说每个定时器主要有 4 个位要配置(定时器 0 多一个死区位):
启动/终止位,用于启动和终止定时器;
手动更新位,用于手动更新 TCNTBn 和 TCMPBn,这里要注意的是在开始定时时,一定要把这位清零,否则是不能开启定时器的;
输出反转位,用于改变输出的电平方向,使原先是高电平输出的变为低电平,而低电平的变为高电平;
自动重载位,用于 TCNTn 减为零后重载TCNTBn 里的值,当不想计数了,可以使自动重载无效,这样在 TCNTn 减为零后,不会有新的数加载给它,那么 TOUTn 输出会始终保持一个电平(输出反转位为 0 时,是高电平输出;输出反转位为 1 时,是低电平输出),这样就没有 PWM 功能了,因此这一位可以用于停止PWM。
阅读2440的datasheet,发现timer0对应的S3C2410_TCON寄存器的位是0到3位,timer1对应的S3C2410_TCON寄存器的位是8到11位。
tcon &= ~0x0f00;
tcon |= 0x0b00; // auto-reload, inv-off, update TCNTB1&TCMPB1, start timer 1
__raw_writel(tcon, S3C2410_TCON);
tcon &= ~0x0200; //clear manual update bit
__raw_writel(tcon, S3C2410_TCON);
##修改的地方就是把原来寄存器的设置值向右移8位。
感谢网上的各位大牛,本博文主要参考:
http://blog.chinaunix.net/uid-11829250-id-337321.html
http://trollybupt.blog.163.com/blog/static/20916720320126973432561/
http://blog.chinaunix.net/uid-28680352-id-3977816.html
飞凌公司的OK2440开发板;
Linux内核版本是:linux2.6.35;
编译器:arm-linux-gcc-4.3.2
一、驱动分析
(1).首先来看看OK2440开发板上蜂鸣器的电路
这里蜂鸣器一端连的是GPB0/TOUT0这个引脚,说明这里用到了定时器0,我们要用定时器0一产生这个PWM波,用以驱动这个无源蜂鸣器发声(有源蜂鸣是有振荡源,通直流电就能发声的);先说一下定时器,S3C2440芯片共有5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM)。定时器的工作原理可以用以下的定时器硬件原理图来说明。
1)每个定时器都有一个比较缓存寄存器(TCMPB)和一个计数缓存寄存器(TCNTB);
2)定时器0、1共享一个8位的预分频器(预定标器),定时器2、3、4共享另一个8位的预分频器(预定标器),其值范围是0~255;
3)定时器0、1共享一个时钟分频器,定时器2、3、4共享另一个时钟分频器,这两个时钟分频器都能产生5种不同的分频信号值(即:1/2、1/4、1/8、1/16和TCLK);
4)两个8位的预分频器是可编程的且根据装载的值来对PCLK进行分频,预分频器和钟分频器的值分别存储在定时器配置寄存器TCFG0和TCFG1中;
5)有一个TCON控制寄存器控制着所有定时器的属性和状态,TCON的第0~7位控制着定时器0、第8~11位控制着定时器1、第12~15位控制着定时器2、第16~19位控制着定时器3、第20~22位控制着定时器4。
还是根据S3C2440手册的描述和上图的结构,要开始一个PWM定时器功能的步骤如下(假设使用的是第一个定时器):
1)分别设置定时器0的预分频器值和时钟分频值,以供定时器0的比较缓存寄存器和计数缓存寄存器用;
2)设置比较缓存寄存器TCMPB0和计数缓存寄存器TCNTB0的初始值(即定时器0的输出时钟频率);
3)关闭定时器0的死区生成器(设置TCON的第4位);
4)开启定时器0的自动重载(设置TCON的第3位);
5)关闭定时器0的反相器(设置TCON的第2位);
6)开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位);
7)启动定时器0(设置TCON的第0位);
8)清除定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位)。
由此可以看到,PWM的输出频率跟比较缓存寄存器和计数缓存寄存器的取值有关,而比较缓存寄存器和计数缓存寄存器的值又跟预分频器和时钟分频器的值有关;要使用PWM功能其实也就是对定时器的相关寄存器进行操作。手册上也有一个公式:定时器输出频率 = PCLK / {预分频器值 + 1} / 时钟分频值。总的来说,就是设置几个与定时器相关的寄存器的值,即可以让定时器输出一定频率的PWM波。
(2).本驱动采用混杂设备的形式注册设备,内核中用到了3个操作函数对设备进行操作,分别是ok2440_pwm_open();
ok2440_pwm_close();ok2440_pwm_ioctl(),简而言之,这里的open作用是获得设备锁,close是释放设备锁,ioctl就是对定时器0各相关寄存器的操作并包括最终启动定时器0,让其输出一pwm波。这里设备锁的引入是为了避免资源并发性产生的恶态竞争。
二、应用程序分析
在应用程序中调用了ioctl()函数,通过此函数向内核传递期望频率的数值。三、驱动程序和应用程序源码
(1)驱动程序
<span style="font-size:14px;">#include <linux/module.h> //所有模块都需要这个头文件 #include <linux/kernel.h> // 声明了printk()这个内核态用的函数 #include <linux/fs.h> // 文件系统有关的,结构体file_operations在头文件 linux/fs.h中定义 #include <linux/init.h> // init和exit相关宏 #include <linux/delay.h> #include <linux/poll.h> #include <asm/irq.h> #include <asm/io.h> #include <linux/interrupt.h> #include <asm/uaccess.h> /*Linux 中的用户和内核态内存交互函数(这些函数在 include/asm/uaccess.h 中被声 明): unsigned long copy_from_user(void *to, const void *from, unsigned long n); unsigned long copy_to_user (void * to, void * from, unsigned long len)*/ #include <mach/regs-gpio.h> // 寄存器设置 #include <mach/hardware.h> // IO空间的物理地址定义 #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> //在这个头文件中主要是misc(混合)设备注册和注销,linux中用struct miscdevice来描述一个混杂设备 #include <linux/gpio.h> #define DEVICE_NAME "PWM0" static struct semaphore lock; /*semaphore是内核中比较重要和常用的同步方式之一,他主要的特点是实现了Sleep机制下的同步。也就是当获取一个semaphore但是又不能立刻获取的时候,他使当前的执行进程进入到Sleep状态中等待,当semaphore可以获取的时候,从新开始运行,而不像splin lock在获取锁的时候是BusyWait。 首先看其定义: struct semaphore { atomic_t count; // 原子变量,是后续的实际代码中,我们能看到其即为我们在初始化时所设置的信号量。 int sleepers; // 有几个等待者。 wait_queue_head_t wait; // 等待队列 }; 初始化函数: static inline void sema_init (struct semaphore *sem, int val) { atomic_set(&sem->count, val); // 原子操作,把信号量的值设为原子操作的值。 sem->sleepers = 0; // 设为等待者为0。 init_wait 4000 queue_head(&sem->wait); // 初始化等待队列。 } */ static int ok2440_pwm_open(struct inode *inode, struct file *file) { if (!down_trylock(&lock)) return 0; else return -EBUSY; } /*P函数:int down_trylock(struct semaphore *sem);带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。 因此可知上述代码是判断信号量是否可以获得。可获得则return 0,不可获得则返回return -EBUSY*/ static int ok2440_pwm_close(struct inode *inode, struct file *file) { up(&lock); return 0; } static int ok2440_pwm_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { // 定义一些局部变量 unsigned long tcfg0; //定时器配置寄存器0,设置定时器0-4的预分频值 unsigned long tcfg1; //定时器配置寄存器1,设置各个pwm定时器的除值各DMA应答通道 unsigned long tcntb; //定时器缓冲区寄存器,通过TCNTB0\TCMPB0共同现实频率改变,发出不同的声音 unsigned long tcmpb; unsigned long tcon; //定时器控制寄存器,设置timer0-4的状态 if(cmd <= 0) // 如果输入的参数小于或等于0的话,就让蜂鸣器停止工作 { s3c2410_gpio_cfgpin(S3C2410_GPB(0), S3C2410_GPIO_OUTPUT); // 设置GPIO引脚的功能:本驱动中PWM所涉及的GPIO引脚设为输出功能 s3c2410_gpio_setpin(S3C2410_GPB(0), 0); //由原理图可知直接给低电平可让蜂鸣器停止工作 } else //如果输入的参数大于0,就让蜂鸣器开始工作,不同的参数,蜂鸣器的频率也不一样 { struct clk *clk_p; unsigned long pclk; s3c2410_gpio_cfgpin(S3C2410_GPB(0), S3C2410_GPB0_TOUT0); tcon = __raw_readl(S3C2410_TCON); // 读取寄存器TCON到tcon, mach/uncompress.h中定义了__raw_readl的定义 tcfg1 = __raw_readl(S3C2410_TCFG1); // 读取寄存器TCFG1到tcfg1 tcfg0 = __raw_readl(S3C2410_TCFG0); // 读取寄存器TCFG0到tcfg0 tcfg0 &= ~S3C2410_TCFG_PRESCALER0_MASK; /*#define S3C2410_TCFG_PRESCALER0_MASK (255<<0),S3C2410_TCFG_PRESCALER0_MASK定时器0和1的预分频值的掩码,TCFG[0~8]*/ tcfg0 |= (50 - 1); // 预分频为50,设置tcfg0的值为49 tcfg1 &= ~S3C2410_TCFG1_MUX0_MASK; //#define S3C2410_TCFG1_MUX0_MASK (15<<0) tcfg1 |= S3C2410_TCFG1_MUX0_DIV16; //#define S3C2410_TCFG1_MUX0_DIV16 (3<<0) 定时器0进行16分割 __raw_writel(tcfg1, S3C2410_TCFG1); //把tcfg1的值写到时钟分频寄存器S3C2410_TCFG1中 __raw_writel(tcfg0, S3C2410_TCFG0); //把tcfg0的值写到预分频寄存器S3C2410_TCFG0中 clk_p = clk_get(NULL, "pclk");/* clk_get获取一个名为id的时针 * 输入参数dev: 可以为NULL* 输入参数id: 时针名称,如fclk、hclk、pclk等 * 返回值: 返回该时钟的clk结构体 */ pclk = clk_get_rate(clk_p); //从系统平台时钟队列中获取pclk的时钟频率,在include/linux/clk.h中定义 tcntb = (pclk/50/16)/arg; // 计算定时器0的输出时钟频率(pclk/{prescaler0 + 1}/divider value) tcmpb = tcntb>>1; __raw_writel(tcntb, S3C2410_TCNTB(0)); //PWM脉宽调制的频率等于定时器的输出时钟频率 __raw_writel(tcmpb, S3C2410_TCMPB(0)); //占空比是tcntb/2为占空比50% tcon &= ~0x1f; //此时tcon=11100000 tcon |= 0xb; // 此时tcon=11101011 /* 关闭定时器0的死区生成器(设置TCON的第4位); 开启定时器0的自动重载(设置TCON的第3位); 关闭定时器0的反相器(设置TCON的第2位); 开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位); 启动定时器0(设置TCON的第0位);*/ __raw_writel(tcon, S3C2410_TCON); tcon &= ~2; //清除定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位)。 __raw_writel(tcon, S3C2410_TCON); } return 0; } static struct file_operations dev_fops = { .owner = THIS_MODULE, //这是一个宏,推向编译模块时自动创建的__this_module变量 .open = ok2440_pwm_open, .release = ok2440_pwm_close, .ioctl = ok2440_pwm_ioctl, }; static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops, }; /*misc(混合)设备注册和注销: struct miscdevice { int minor; const char *name; const struct file_operations *fops; struct list_head list; struct device *parent; struct device *this_device; };*/ static int __init dev_init(void) { int ret; init_MUTEX(&lock); /*带有“_LOCKED”的是将信号量初始化为0,即锁定,允许任何线程访问时必须先解锁。没带的为1,这种信号量在任何给定时刻只能由单个进程或线程拥有*/ ret = misc_register(&misc); printk (DEVICE_NAME" initialized\n"); return ret; } static void __exit dev_exit(void) { misc_deregister(&misc); // printk("<1> display a little,hei hei \n\n "); } module_init(dev_init); module_exit(dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("luo"); MODULE_DESCRIPTION("PWM Drivers for ok2440 Board"); </span>
(2)应用程序
<span style="font-size:14px;">#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, char **argv) { int fd; unsigned long temp =0; int i; fd = open("/dev/PWM0", 0); if (fd < 0) { perror("open device EmbedSky-BEEP"); exit(1); } printf("Please enter the times number (0 is stop) :\n"); for(i=0;;i++) { scanf("%d",&temp); printf("times = %d \n",temp); if(temp == 0) { ioctl(fd, 0); printf("Stop Control Beep!\n"); // break; continue; } else { ioctl(fd, 1, temp); } } close(fd); return 0; } </span>
四、再进一步,要想使用定时1,定时2,定时3也产生PWM波形的话,就得先来了解一下上面所说的跟定时器相关的几个常用寄存器;这几个定时器分别是1.TCFG0寄存器(用于预分频);2.TCFG1寄存器(用于二次分频);3.TCON寄存器(定时器总控制寄存器);4.TCNTB0/TCMPB0/TOUT0寄存器(分别用来保存定时数值和翻转时标识数和观察TCONT0的数)。
PWM定时器常用寄存器,以定时器0为例
(1).TCFG0寄存器(TIMER CONFIGURATION),用于控制预分频器,其地址和各位含义如图8所示
图8 TCFG0寄存器地址和各位含义
定时器的输入频率计算公式为:定时器工作频率=PCLK/(prescaler value+1)/(divider value),其中prescaler value的值(预分频器)通过TCFG0寄存器设置,TCFG0[0:7]设置定时器0和定时器1的prescaler
value值,TCFG0[8:15]设置定时器2、3、4的prescaler value的值,该值的范围为0~255,本例程选择定时器0的该值为99,因此该寄存器的值设置为99。divider value的值(第二级分频器)为2、4、8、16,由TCFG1寄存器设置
(2).TCFG1寄存器(TIMER CONFIGURATION),经预分频器得到的时钟被输入到第二级分频器,可以再次被2分频、4分频、8分频和16分频,由图6可知,定时器0、1还可以工作在外接时钟TCLK0下,定时器2、3、4还可以工作在外接时钟TCLK1下,使用TCFG1寄存器来设置这5个定时器的第二级分频器的分频数,其地址和各位含义如图9所示
图9 TCFG1寄存器地址和各位含义
TCFG1[20:23]用于设置5个定时器的DMA模式,TCFG1[0:3]设置定时器0的分频数。本例程中不使用定时器的DMA模式,定时器0的第二级分频选择1/16,所以该寄存器的值为0x3。
(3).TCON寄存器(TIMER CONTROL),其地址和各位含义如图10所示
图10 TCON地址和各位含义
TCON[0]设置定时器0的开启和停止;TCON[1]设置定时器0“手动更新”,将TCMPB0/TCNTB0的值装入内部寄存器TCMP0/TCNT0中;TCON[2]设置TOUT0是否反转;TCON[3]设置自动加载。对于本例程,使用定时器0,开启定时器、手动更新、反转、自动加载。
(4).TCNTB0/TCMPB0/TOUT0寄存器,TCNTB0用于保存定时器初始计数值,TCMPB0用于保存比较值,TOUT0用来观察TCNT0的数值;其地址和各位含义如图11所示
图11 TCNTB0/TCMPB0/TOUT0寄存器地址和各位含义
五.了解过上面的几个寄存器后,下面来看看从定时器1产生PWM波的驱动程序和应用程序源代码
下面是驱动程序:
<span style="font-size:14px;">#include <linux/module.h> //所有模块都需要这个头文件 #include <linux/kernel.h> // 声明了printk()这个内核态用的函数 #include <linux/fs.h> // 文件系统有关的,结构体file_operations在头文件 linux/fs.h中定义 #include <linux/init.h> // init和exit相关宏 #include <linux/delay.h> #include <linux/poll.h> #include <asm/irq.h> #include <asm/io.h> #include <linux/interrupt.h> #include <asm/uaccess.h> /*Linux 中的用户和内核态内存交互函数(这些函数在 include/asm/uaccess.h 中被声 明): unsigned long copy_from_user(void *to, const void *from, unsigned long n); unsigned long copy_to_user (void * to, void * from, unsigned long len)*/ #include <mach/regs-gpio.h> // 寄存器设置 #include <mach/hardware.h> // IO空间的物理地址定义 #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> //在这个头文件中主要是misc(混合)设备注册和注销,linux中用struct miscdevice来描述一个混杂设备 #include <linux/gpio.h> //s3c2410_gpio_cfgpin(); s3c2410_gpio_getpin(); #define DEVICE_NAME "PWM1" static struct semaphore lock; /*semaphore是内核中比较重要和常用的同步方式之一,他主要的特点是实现了Sleep机制下的同步。也就是当获取一个semaphore但是又不能立刻获取的时候,他使当前的执行进程进入到Sleep状态中等待,当semaphore可以获取的时候,从新开始运行,而不像splin lock在获取锁的时候是BusyWait。 首先看其定义: struct semaphore { atomic_t count; // 原子变量,是后续的实际代码中,我们能看到其即为我们在初始化时所设置的信号量。 int sleepers; // 有几个等待者。 wait_queue_head_t wait; // 等待队列 }; 初始化函数: static inline void sema_init (struct semaphore *sem, int val) { atomic_set(&sem->count, val); // 原子操作,把信号量的值设为原子操作的值。 sem->sleepers = 0; // 设为等待者为0。 init_waitqueue_head(&sem->wait); // 初始化等待队列。 } */ static int ok2440_pwm_open(struct inode *inode, struct file *file) { if (!down_trylock(&lock)) return 0; else return -EBUSY; } /*P函数:int down_trylock(struct semaphore *sem);带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。 因此可知上述代码是判断信号量是否可以获得。可获得则return 0,不可获得则返回return -EBUSY*/ static int ok2440_pwm_close(struct inode *inode, struct file *file) { up(&lock); return 0; } static int ok2440_pwm_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { // 定义一些局部变量 unsigned long tcfg0; //定时器配置寄存器0,设置定时器0-4的预分频值 unsigned long tcfg1; //定时器配置寄存器1,设置各个pwm定时器的除值各DMA应答通道 unsigned long tcntb; //定时器缓冲区寄存器,通过TCNTB0\TCMPB0共同现实频率改变,发出不同的声音 unsigned long tcmpb; unsigned long tcon; //定时器控制寄存器,设置timer0-4的状态 if(cmd <= 0) // 如果输入的参数小于或等于0的话,就让蜂鸣器停止工作 { s3c2410_gpio_cfgpin(S3C2410_GPB(1), S3C2410_GPIO_OUTPUT); // 设置GPIO引脚的功能:本驱动中PWM所涉及的GPIO引脚设为输出功能 s3c2410_gpio_setpin(S3C2410_GPB(1), 0); //由原理图可知直接给低电平可让蜂鸣器停止工作 } else //如果输入的参数大于0,就让蜂鸣器开始工作,不同的参数,蜂鸣器的频率也不一样 { struct clk *clk_p; unsigned long pclk; s3c2410_gpio_cfgpin(S3C2410_GPB(1), S3C2410_GPB1_TOUT1); tcon = __raw_readl(S3C2410_TCON); // 读取寄存器TCON到tcon, mach/uncompress.h中定义了__raw_readl的定义 tcfg1 = __raw_readl(S3C2410_TCFG1); // 读取寄存器TCFG1到tcfg1 tcfg0 = __raw_readl(S3C2410_TCFG0); // 读取寄存器TCFG0到tcfg0 tcfg0 &= ~S3C2410_TCFG_PRESCALER0_MASK; /*#define S3C2410_TCFG_PRESCALER0_MASK (255<<0),S3C2410_TCFG_PRESCALER0_MASK定时器0和1的预分频值的掩码,TCFG[0~8]*/ tcfg0 |= (50 bfac - 1); // 预分频为50,设置tcfg0的值为49 tcfg1 &= ~S3C2410_TCFG1_MUX1_MASK; //#define S3C2410_TCFG1_MUX1_MASK (15<<0) tcfg1 |= S3C2410_TCFG1_MUX1_DIV16; //#define S3C2410_TCFG1_MUX1_DIV16 (3<<0) 定时器0进行16分割 __raw_writel(tcfg1, S3C2410_TCFG1); //把tcfg1的值写到时钟分频寄存器S3C2410_TCFG1中 __raw_writel(tcfg0, S3C2410_TCFG0); //把tcfg0的值写到预分频寄存器S3C2410_TCFG0中 clk_p = clk_get(NULL, "pclk");/* clk_get获取一个名为id的时针 * 输入参数dev: 可以为NULL* 输入参数id: 时针名称,如fclk、hclk、pclk等 * 返回值: 返回该时钟的clk结构体 */ pclk = clk_get_rate(clk_p); //从系统平台时钟队列中获取pclk的时钟频率,在include/linux/clk.h中定义 tcntb = (pclk/50/16)/arg; // 计算定时器0的输出时钟频率(pclk/{prescaler0 + 1}/divider value) tcmpb = tcntb>>1; __raw_writel(tcntb, S3C2410_TCNTB(1)); //PWM脉宽调制的频率等于定时器的输出时钟频率 __raw_writel(tcmpb, S3C2410_TCMPB(1)); //占空比是tcntb/2为占空比50% tcon &= ~0x0f00; //此时tcon=1111000011111111 tcon |= 0x0b00; // 此时tcon=1111101111111111 /* 关闭定时器0的死区生成器(设置TCON的第4位); 开启定时器0的自动重载(设置TCON的第3位); 关闭定时器0的反相器(设置TCON的第2位); 开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位); 启动定时器0(设置TCON的第0位);*/ __raw_writel(tcon, S3C2410_TCON); tcon &= ~0x0200; //清除定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位)。 __raw_writel(tcon, S3C2410_TCON); } return 0; } static struct file_operations dev_fops = { .owner = THIS_MODULE, //这是一个宏,推向编译模块时自动创建的__this_module变量 .open = ok2440_pwm_open, .release = ok2440_pwm_close, .ioctl = ok2440_pwm_ioctl, }; static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops, }; /*misc(混合)设备注册和注销: struct miscdevice { int minor; const char *name; const struct file_operations *fops; struct list_head list; struct device *parent; struct device *this_device; };*/ static int __init dev_init(void) { int ret; init_MUTEX(&lock); /*带有“_LOCKED”的是将信号量初始化为0,即锁定,允许任何线程访问时必须先解锁。没带的为1,这种信号量在任何给定时刻只能由单个进程或线程拥有*/ ret = misc_register(&misc); printk (DEVICE_NAME" initialized\n"); return ret; } static void __exit dev_exit(void) { misc_deregister(&misc); // printk("<1> display a little,hei hei \n\n "); } module_init(dev_init); module_exit(dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("luo"); MODULE_DESCRIPTION("PWM Drivers for ok2440 Board"); </span>
下面是应用程序:
<span style="font-size:14px;">#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, char **argv) { int fd; unsigned long temp =0; int i; fd = open("/dev/PWM1", 0); if (fd < 0) { perror("open device EmbedSky-BEEP"); exit(1); } printf("Please enter the times number (0 is stop) :\n"); for(i=0;;i++) { scanf("%d",&temp); printf("times = %d \n",temp); if(temp == 0) { ioctl(fd, 0); printf("Stop Control Beep!\n"); printf("Please enter the times number (0 is stop) :\n"); // break; continue; } else { ioctl(fd, 1, temp); } } close(fd); return 0; } </span>
总结一下,这与定时器0产生的pwm波主要有两点区别:
第一:设置定时器的输出时钟频率,它是以PCLK为基准,再除以用寄存器TCFG0配置的prescaler参数,和用寄存器TCFG1配置的divider参数。
tcfg0 |= (50 - 1); // 预分频为50,
tcfg1 &= ~S3C2410_TCFG1_MUX1_MASK; //S3C2410_TCFG1_MUX0_MASK定时器1分割值的掩码:TCFG1[0~3]
tcfg1 |= S3C2410_TCFG1_MUX1_DIV16; //定时器1进行16分割
__raw_writel(tcfg1, S3C2410_TCFG1); //把tcfg1的值写到分割寄存器S3C2410_TCFG1中
__raw_writel(tcfg0, S3C2410_TCFG0); //把tcfg0的值写到分割寄存器S3C2410_TCFG0中
##修改的部分为将计时器0的复用器改成计时器1,即把S3C2410_TCFG1_MUX0_MASK和S3C2410_TCFG1_MUX0_DIV16中的MUX0改成MUX1;
第二:对 PWM 的控制,它是通过寄存器 TCON 来实现的,一般来说每个定时器主要有 4 个位要配置(定时器 0 多一个死区位):
启动/终止位,用于启动和终止定时器;
手动更新位,用于手动更新 TCNTBn 和 TCMPBn,这里要注意的是在开始定时时,一定要把这位清零,否则是不能开启定时器的;
输出反转位,用于改变输出的电平方向,使原先是高电平输出的变为低电平,而低电平的变为高电平;
自动重载位,用于 TCNTn 减为零后重载TCNTBn 里的值,当不想计数了,可以使自动重载无效,这样在 TCNTn 减为零后,不会有新的数加载给它,那么 TOUTn 输出会始终保持一个电平(输出反转位为 0 时,是高电平输出;输出反转位为 1 时,是低电平输出),这样就没有 PWM 功能了,因此这一位可以用于停止PWM。
阅读2440的datasheet,发现timer0对应的S3C2410_TCON寄存器的位是0到3位,timer1对应的S3C2410_TCON寄存器的位是8到11位。
tcon &= ~0x0f00;
tcon |= 0x0b00; // auto-reload, inv-off, update TCNTB1&TCMPB1, start timer 1
__raw_writel(tcon, S3C2410_TCON);
tcon &= ~0x0200; //clear manual update bit
__raw_writel(tcon, S3C2410_TCON);
##修改的地方就是把原来寄存器的设置值向右移8位。
http://blog.chinaunix.net/uid-11829250-id-337321.html
http://trollybupt.blog.163.com/blog/static/20916720320126973432561/
http://blog.chinaunix.net/uid-28680352-id-3977816.html
相关文章推荐
- 基于S3C2440芯片linux系统下的ds18b20设备驱动
- 基于S3C2440的Linux-3.6.6移植 PWM蜂鸣器驱动
- 基于S3C2440芯片的蜂鸣器驱动开发
- 基于S3C2440的Linux-3.6.6移植——PWM蜂鸣器驱动
- Linux 2.6.32系统中基于dm6467平台ASOC架构的音频驱动
- 基于S3C2440的Linux内核移植和yaffs2文件系统制作
- 基于S3C2440的嵌入式Linux驱动——Framebuffer子系统解读
- s3c2440基于linux的button和led字符设备驱动
- 基于S3C2440的嵌入式Linux驱动——SPI子系统解读(三)
- 基于S3C2440的嵌入式Linux驱动——SPI子系统解读(二)
- 基于S3C2440的Linux内核移植和yaffs2文件系统制作
- 基于S3C2440的嵌入式Linux驱动——DS18B20温度传感器(添加使用platform总线机制)
- s3c2440基于linux的gpio led字符设备驱动实践
- 基于S3C2440的嵌入式Linux驱动——SPI子系统解读(四)
- 基于S3C2440的Linux内核移植和yaffs2文件系统制作--内核移植
- 基于S3C2440的嵌入式Linux驱动——SPI子系统解读(一)
- 基于ARM处理器S3C2440和Linux系统的I2C触摸屏设计
- s3c2440基于linux的gpio led字符设备驱动实践 [转]
- s3c2440基于linux的按键和外部中断驱动实践及驱动机制分析含代码
- 基于S3C2440的Linux内核移植和yaffs2文件系统制作