TQ2440 PWM驱动程序设计(内核2.6.30.4)
2016-06-27 15:47
405 查看
PWM驱动程序设计(内核2.6.30.4) 原文出自http://blog.chinaunix.net/uid-25577798-id-1761435.html
下面是以TQ2440开发板为基础的硬件电路图:
在驱动程序里面首先初始化该PWM所对应的管脚的功能,然后初始化定时器设置对PWM的定时,最后启动PWM功能。
由硬件图可知蜂鸣器是由TOUT0(GPB0)控制的,这里需要开启TIME0作为PWM输出控制蜂鸣器的工作。
设计之前先介绍一些这方面的基础知识:
1. 什么是PWM?
PWM(脉冲宽度调制)简单的讲是一种变频技术之一,是靠改变脉冲宽度来控制输出电压,通过改变周期来控制其输出频率。如果还不是很清楚,好吧,来看看我们实际生活中的例子,我们的电风扇为什么扭一下按扭,风扇的转速就会发生变化;调一下收音机的声音按钮,声音的大小就会发生变化;还有待会儿我们要讲的蜂鸣器也会根据不同的输入值而发出不同频率的叫声等等!!这些都是PWM的应用,都是通过PWM输出的频率信号进行控制的。
2. ARM Linux中的PWM
根据S3C2440的手册介绍,S3C2440A内部有5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM),定时器4是一个没有输出引脚的内部定时器,定时器0有一个用于大电流设备的死区生成器。看下图解释吧!!
由S3C2440的技术手册和上面这幅结构图,我们来总结一下2440内部定时器模块的特性吧:
1)共5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM);
2)每个定时器都有一个比较缓存寄存器(TCMPB)和一个计数缓存寄存器(TCNTB);
3)定时器0、1共享一个8位的预分频器(预定标器),定时器2、3、4共享另一个8位的预分频器(预定标器),其值范围是0~255;
4)定时器0、1共享一个时钟分频器,定时器2、3、4共享另一个时钟分频器,这两个时钟分频器都能产生5种不同的分频信号值(即:1/2、1/4、1/8、1/16和TCLK);
5)两个8位的预分频器是可编程的且根据装载的值来对PCLK进行分频,预分频器和钟分频器的值分别存储在定时器配置寄存器TCFG0和TCFG1中;
6)有一个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功能的使用。
三、蜂鸣器驱动实例
1. 蜂鸣器的种类和工作原理
蜂鸣器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。
压电式蜂鸣器主要由多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱、外壳等组成。有的压电式蜂鸣器外壳上还装有发光二极管。多谐振荡器由晶体管或集成电路构成。当接通电源后(1.5~15V直流工作电压),多谐振荡器起振,输出1.5~2.5kHZ的音频信号,阻抗匹配器推动压电蜂鸣片发声。
电磁式蜂鸣器由振荡器、电磁线圈、磁铁、振动膜片及外壳等组成。接通电源后,振荡器产生的音频信号电流通过电磁线圈,使电磁线圈产生磁场。振动膜片在电磁线圈和磁铁的相互作用下,周期性地振动发声。
有源蜂鸣器和无源蜂鸣器的区别:这个“源”字是不是指电源,而是指震荡源,即有源蜂鸣器内有振荡源而无源蜂鸣器内部没有振荡源。有振荡源的通电就可以发声,没有振荡源的需要脉冲信号驱动才能发声。
额外知识:简单蜂鸣器的制作方法
1)制备电磁铁M:在长约6厘米的铁螺栓上绕100圈导线,线端留下5厘米作引线,用透明胶布把线圈粘好,以免线圈松开,再用胶布把它粘在一个盒子上,电磁铁就做好了;
2)制备弹片P:从铁罐头盒上剪下一条宽约2厘米的长铁片,弯成直角,把电磁铁的一条引线接在弹片上,再用胶布把弹片紧贴在木板上;
3)用曲别针做触头Q,用书把曲别针垫高,用胶布粘牢,引出一条导线,如图连接好电路;
4)调节M与P之间的距离(通过移动盒子),使电磁铁能吸引弹片,调节触点与弹片之间的距离,使它们能恰好接触,通电后就可以听到蜂鸣声。
下面是驱动程序的源码,由于直接对部分源码在在程序中直接解释,所以用褐色标注源代码:
#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来描述一个混杂设备
#define DEVICE_NAME "PWM-Test"
加载模式后,执行“cat /proc/devices"命令看到的设备名称
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 tq2440_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 tq2440_pwm_close(struct inode *inode, struct file *file)
{
up(&lock);
return 0;
}
void up(struct semaphore *sem);/*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。*/
static int tq2440_pwm_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
定义一些局部变量
unsigned long tcfg0;
unsigned long tcfg1;
unsigned long tcntb;
unsigned long tcmpb;
unsigned long tcon;
if(cmd <= 0) 如果输入的参数小于或等于0的话,就让蜂鸣器停止工作
{
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_OUTP);
设置GPIO引脚的功能:本驱动中PWM所涉及的GPIO引脚设为输出功能
s3c2410_gpio_setpin(S3C2410_GPB0, 0);
由原理图可知直接给低电平可让蜂鸣器停止工作
}
else如果输入的参数大于0,就让蜂鸣器开始工作,不同的参数,蜂鸣器的频率也不 一样
{
struct clk *clk_p;
unsigned long pclk;
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_TOUT0);
tcon = __raw_readl(S3C2410_TCON); 读取寄存器TCON到tcon
tcfg1 = __raw_readl(S3C2410_TCFG1); 读取寄存器TCFG1到tcfg1
tcfg0 = __raw_readl(S3C2410_TCFG0); 读取寄存器TCFG0到tcfg0
mach/uncompress.h中定义了__raw_readl的定义,如下
static unsigned int __raw_readl(unsigned int ptr)
{
return *((volatile unsigned int *)ptr);
}
预分频为50
tcfg0 &= ~S3C2410_TCFG_PRESCALER0_MASK;
#define S3C2410_TCFG_PRESCALER0_MASK (255<<0)
S3C2410_TCFG_PRESCALER0_MASK定时器0和1的预分频值的掩码,TCFG[0~8]
tcfg0 |= (50 - 1); 设置tcfg0的值为49
tcfg1 &= ~S3C2410_TCFG1_MUX0_MASK;
#define S3C2410_TCFG1_MUX0_MASK (15<<0)
S3C2410_TCFG1_MUX0_MASK定时器0时钟分频器的掩
tcfg1 |= S3C2410_TCFG1_MUX0_DIV16;
#define S3C2410_TCFG1_MUX0_DIV16 (3<<0)
定时器0进行16分割
__raw_writel(tcfg1, S3C2410_TCFG1);
把tcfg1的值写到时钟分频寄存器S3C2410_TCFG1中
static void __raw_writel(unsigned int value, unsigned int ptr)
{
*((volatile unsigned int *)ptr) = value;
}
__raw_writel(tcfg0, S3C2410_TCFG0);
把tcfg0的值写到预分频寄存器S3C2410_TCFG0中
clk_p = clk_get(NULL, "pclk");
/**
* clk_get - lookup and obtain a reference to a clock producer.
* @dev: device for clock "consumer"
* @id: clock comsumer ID
*
* Returns a struct clk corresponding to the clock producer, or
* valid IS_ERR() condition containing errno. The implementation
* uses @dev and @id to determine the clock consumer, and thereby
* the clock producer. (IOW, @id may be identical strings, but
* clk_get may return different clock producers depending on @dev.)
*
* Drivers must assume that the clock source is not enabled.
*
* clk_get should not be called from within interrupt context.
*/
pclk = clk_get_rate(clk_p);
从系统平台时钟队列中获取pclk的时钟频率,在include/linux/clk.h中定义
/**
* clk_get_rate - obtain the current clock rate (in Hz) for a clock source.
* This is only valid once the clock source has been enabled.
* @clk: clock source
*/
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
3)关闭定时器0的死区生成器(设置TCON的第4位);
4)开启定时器0的自动重载(设置TCON的第3位);
5)关闭定时器0的反相器(设置TCON的第2位);
6)开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位);
7)启动定时器0(设置TCON的第0位);
__raw_writel(tcon, S3C2410_TCON);
tcon &= ~2;
8)清除定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位)。
__raw_writel(tcon, S3C2410_TCON);
}
return 0;
}
static struct file_operations dev_fops = {
.owner = THIS_MODULE,
这是一个宏,推向编译模块时自动创建的__this_module变量
.open = tq2440_pwm_open,
.release = tq2440_pwm_close,
.ioctl = tq2440_pwm_ioctl,
};
file_operations结构体,驱动函数的设置,分别将前面的驱动函数设置进来
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &dev_fops,
};
misc(混合)设备注册和注销:
其它类型---不能严格划分的设备类型,也叫混合类型
有:
1.
结构体:
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
};
2.
misc设备注册:
extern int misc_register(struct miscdevice * misc);
misc设备注销:
extern int misc_deregister(struct miscdevice * misc);
说明:上面的结构体是注册混合设备所需要的参数。主要有:
minor:次设备号,所有的misc设备共用一个主设备号,所以注册misc设备时只要次
设备号就可以了。利用次设备号来区分设备的。
name:misc设备名。
*fops:misc设备文件操作结构体。
其它三个参数很少用。
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>\n display a little,hei hei \n\n ");
}
module_init(dev_init);
module_exit(dev_exit);
模块创建时的入口点
模块卸载时的入口点
MODULE_LICENSE("GPL");
MODULE_AUTHOR("http://hi.baidu.com/shentuhongfeng");
MODULE_DESCRIPTION("PWM Drivers for EmbedSky TQ2440 Board");
下面是一个测试程序:
#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/PWM-Test", 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;
}
else
{
ioctl(fd, 1, temp);
}
}
close(fd);
return 0;
}
网上看到一个很经典的分析过程,个人自叹不如,先粘贴下来作为借鉴:
PWM在ARM Linux中的原理和蜂鸣器驱动实例开发
Linux内核驱动编程 2010-04-07 23:53:49
嵌入式Linux之我行,主要讲述和总结了本人在学习嵌入式linux中的每个步骤。一为总结经验,二希望能给想入门嵌入式Linux的朋友提供方便。如有错误之处,谢请指正。
一、开发环境
二、PWM怎样工作在ARM Linux中
1. 什么是PWM?
PWM(脉冲宽度调制)简单的讲是一种变频技术之一,是靠改变脉冲宽度来控制输出电压,通过改变周期来控制其输出频率。如果还不是很清楚,好吧,来看看我们实际生活中的例子,我们的电风扇为什么扭一下按扭,风扇的转速就会发生变化;调一下收音机的声音按钮,声音的大小就会发生变化;还有待会儿我们要讲的蜂鸣器也会根据不同的输入值而发出不同频率的叫声等等!!这些都是PWM的应用,都是通过PWM输出的频率信号进行控制的。
2. ARM Linux中的PWM
根据S3C2440的手册介绍,S3C2440A内部有5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM),定时器4是一个没有输出引脚的内部定时器,定时器0有一个用于大电流设备的死区生成器。看下图解释吧!!
由S3C2440的技术手册和上面这幅结构图,我们来总结一下2440内部定时器模块的特性吧:
1)共5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM);
2)每个定时器都有一个比较缓存寄存器(TCMPB)和一个计数缓存寄存器(TCNTB);
3)定时器0、1共享一个8位的预分频器(预定标器),定时器2、3、4共享另一个8位的预分频器(预定标器),其值范围是0~255;
4)定时器0、1共享一个时钟分频器,定时器2、3、4共享另一个时钟分频器,这两个时钟分频器都能产生5种不同的分频信号值(即:1/2、1/4、1/8、1/16和TCLK);
5)两个8位的预分频器是可编程的且根据装载的值来对PCLK进行分频,预分频器和钟分频器的值分别存储在定时器配置寄存器TCFG0和TCFG1中;
6)有一个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功能的使用。
三、蜂鸣器驱动实例
1. 蜂鸣器的种类和工作原理
蜂鸣器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。
压电式蜂鸣器主要由多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱、外壳等组成。有的压电式蜂鸣器外壳上还装有发光二极管。多谐振荡器由晶体管或集成电路构成。当接通电源后(1.5~15V直流工作电压),多谐振荡器起振,输出1.5~2.5kHZ的音频信号,阻抗匹配器推动压电蜂鸣片发声。
电磁式蜂鸣器由振荡器、电磁线圈、磁铁、振动膜片及外壳等组成。接通电源后,振荡器产生的音频信号电流通过电磁线圈,使电磁线圈产生磁场。振动膜片在电磁线圈和磁铁的相互作用下,周期性地振动发声。
有源蜂鸣器和无源蜂鸣器的区别:这个“源”字是不是指电源,而是指震荡源,即有源蜂鸣器内有振荡源而无源蜂鸣器内部没有振荡源。有振荡源的通电就可以发声,没有振荡源的需要脉冲信号驱动才能发声。
额外知识:简单蜂鸣器的制作方法
1)制备电磁铁M:在长约6厘米的铁螺栓上绕100圈导线,线端留下5厘米作引线,用透明胶布把线圈粘好,以免线圈松开,再用胶布把它粘在一个盒子上,电磁铁就做好了;
2)制备弹片P:从铁罐头盒上剪下一条宽约2厘米的长铁片,弯成直角,把电磁铁的一条引线接在弹片上,再用胶布把弹片紧贴在木板上;
3)用曲别针做触头Q,用书把曲别针垫高,用胶布粘牢,引出一条导线,如图连接好电路;
4)调节M与P之间的距离(通过移动盒子),使电磁铁能吸引弹片,调节触点与弹片之间的距离,使它们能恰好接触,通电后就可以听到蜂鸣声。
2. 开发板上蜂鸣器原理图分析
由原理图可以得知,蜂鸣器是通过GPB0 IO口使用PWM信号驱动工作的,而GPB0口是一个复用的IO口,要使用它得先把他设置成TOUT0
PWM输出模式。
3. 编写合适开发板的蜂鸣器驱动程序,文件名:my2440_pwm.c
/*
================================================
Name : my2440_pwm.c
Author : Huang Gang
Date : 25/11/09
Copyright : GPL
Description : my2440 pwm driver
================================================
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <asm/io.h>
#include <mach/hardware.h>
#include <mach/regs-gpio.h>
#include <plat/regs-timer.h>
#define PWM_MAJOR 0 //主设备号
#define PWM_NAME "my2440_pwm" //设备名称
static int device_major = PWM_MAJOR; //系统动态生成的主设备号
//打开设备
static int pwm_open(struct inode *inode, struct file *file)
{
//对GPB0复用口进行复用功能设置,设置为TOUT0 PWM输出
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_TOUT0);
return 0;
}
//关闭设备
static int pwm_close(struct inode *inode, struct file *file)
{
return 0;
}
//对设备进行控制
static int pwm_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
if(cmd <= 0)//如果输入的参数小于或等于0的话,就让蜂鸣器停止工作
{
//这里又恢复GPB0口为IO口输出功能,由原理图可知直接给低电平可让蜂鸣器停止工作
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_OUTP);
s3c2410_gpio_setpin(S3C2410_GPB0, 0);
}
else//如果输入的参数大于0,就让蜂鸣器开始工作,不同的参数,蜂鸣器的频率也不一样
{
//定义一些局部变量
unsigned long tcon;
unsigned long tcnt;
unsigned long tcfg1;
unsigned long tcfg0;
struct clk *clk_p;
unsigned long pclk;
//以下对各寄存器的操作结合上面讲的开始一个PWM定时器的步骤和2440手册PWM寄存器操作部分来看就比较容易理解
tcfg1 = __raw_readl(S3C2410_TCFG1); //读取定时器配置寄存器1的值
tcfg0 = __raw_readl(S3C2410_TCFG0); //读取定时器配置寄存器0的值
tcfg0 &= ~S3C2410_TCFG_PRESCALER0_MASK;
tcfg0 |= (50 - 1); //设置tcfg0的值为49
tcfg1 &= ~S3C2410_TCFG1_MUX0_MASK;
tcfg1 |= S3C2410_TCFG1_MUX0_DIV16; //设置tcfg1的值为0x0011即:1/16
__raw_writel(tcfg1, S3C2410_TCFG1); //将值tcfg1写入定时器配置寄存器1中
__raw_writel(tcfg0, S3C2410_TCFG0); //将值tcfg0写入定时器配置寄存器0中
clk_p = clk_get(NULL, "pclk");
pclk = clk_get_rate(clk_p); //从系统平台时钟队列中获取pclk的时钟频率,在include/linux/clk.h中定义
tcnt = (pclk/50/16)/cmd; //计算定时器0的输出时钟频率(pclk/{prescaler0
+ 1}/divider value)
__raw_writel(tcnt, S3C2410_TCNTB(0)); //设置定时器0计数缓存寄存器的值
__raw_writel(tcnt/2, S3C2410_TCMPB(0)); //设置定时器0比较缓存寄存器的值
tcon = __raw_readl(S3C2410_TCON); //读取定时器控制寄存器的值
tcon &= ~0x1f;
tcon |= 0xb; //关闭死区、自动重载、关反相器、更新TCNTB0&TCMPB0、启动定时器0
__raw_writel(tcon, S3C2410_TCON); //设置定时器控制寄存器的0-4位,即对定时器0进行控制
tcon &= ~2;
__raw_writel(tcon, S3C2410_TCON); //清除定时器0的手动更新位
}
return 0;
}
//设备操作结构体
static struct file_operations pwm_fops =
{
.owner = THIS_MODULE,
.open = pwm_open,
.release = pwm_close,
.ioctl = pwm_ioctl,
};
//定义一个设备类
static struct class *pwm_class;
static int __init pwm_init(void)
{
//注册为字符设备,主设备号为0让系统自动分配,设备名为my2440_pwm,注册成功返回动态生成的主设备号
device_major = register_chrdev(PWM_MAJOR, PWM_NAME, &pwm_fops);
if(device_major < 0)
{
printk(PWM_NAME " register falid!\n");
return device_major;
}
//注册一个设备类,使mdev可以在/dev/目录下自动建立设备节点
pwm_class = class_create(THIS_MODULE, PWM_NAME);
if(IS_ERR(pwm_class))
{
printk(PWM_NAME " register class falid!\n");
return -1;
}
//创建一个设备节点,设备名为PWM_NAME,即:my2440_pwm
device_create(pwm_class, NULL, MKDEV(device_major, 0), NULL, PWM_NAME);
return 0;
}
static void __exit pwm_exit(void)
{
//注销设备
unregister_chrdev(device_major, PWM_NAME);
//删除设备节点
device_destroy(pwm_class, MKDEV(device_major, 0));
//注销设备类
class_destroy(pwm_class);
}
module_init(pwm_init);
module_exit(pwm_exit);
MODULE_LICENSE("PGL");
MODULE_AUTHOR("Huang Gang");
MODULE_DESCRIPTION("my2440 pwm driver");
4. 将PWM蜂鸣器驱动代码部署到内核中。
#cp -f my2440_pwm.c /linux-2.6.30.4/drivers/char //把驱动源码复制到内核驱动的字符设备下
#gedit /linux-2.6.30.4/drivers/char/Kconfig //添加PWM蜂鸣器设备配置
config MY2440_PWM_BEEP
tristate "My2440 PWM Beep Device"
depends on ARCH_S3C2440
default y
---help---
My2440 PWM Beep
#gedit /linux-2.6.30.4/drivers/char/Makefile //添加PWM蜂鸣器设备配置
obj-$(CONFIG_MY2440_PWM_BEEP) += my2440_pwm.o
5.配置内核,选择PWM蜂鸣器设备选项
#make menuconfig
Device Drivers --->
Character devices --->
<*> My2440 PWM Beep Device (NEW)
6. 编译内核并下载到开发板上。这里要注意,现在我们不需要手动的在开发板上创建设备的节点了,因为我们现在使用了mdev进行管理了(使用方法请看:设备文件系统剖析与使用),在驱动程序中也添加了对类设备接口的支持。之前讲的一些驱动都没有,以后我们都使用这种方法。现在可以查看到/dev目录下自动创建好的my2440_pwm设备节点,就直接可以使用它了。
7. 编写PWM蜂鸣器驱动的测试程序。文件名:pwm_test.c
/*
==============================================
Name : pwm_test.c
Author : Huang Gang
Date : 25/11/2009
Copyright : GPL
Description : my2440 pwm driver test
==============================================
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
int main(int argc, char **argv)
{
int tmp;
int fd;
int i;
//打开蜂鸣器设备
fd = open("/dev/my2440_pwm", O_RDWR);
if(fd < 0)
{
printf("Open PWM Device Faild!\n");
exit(1);
}
//提示用户输入一个参数来对蜂鸣器进行调频,0表示停止工作
printf("please enter the times number(0 is stop):\n");
while(1)
{
//输入参数
scanf("%d", &tmp);
printf("times = %d\n", tmp);
//IO控制
ioctl(fd, tmp);
if(tmp <= 0)
{
break;
}
}
//关闭设备
close(fd);
return 0;
}
8. 在开发主机上交叉编译测试应用程序,并复制到文件系统的/usr/sbin目录下,然后重新编译文件系统下载到开发板上。
#arm-linux-gcc -o pwm_test pwm_test.c
9. 在开发板上运行测试程序。可以看到根据你输入参数的大小,蜂鸣器也会发生不同频率的叫声,输入0蜂鸣器停止鸣叫。
顶
下面是以TQ2440开发板为基础的硬件电路图:
在驱动程序里面首先初始化该PWM所对应的管脚的功能,然后初始化定时器设置对PWM的定时,最后启动PWM功能。
由硬件图可知蜂鸣器是由TOUT0(GPB0)控制的,这里需要开启TIME0作为PWM输出控制蜂鸣器的工作。
设计之前先介绍一些这方面的基础知识:
1. 什么是PWM?
PWM(脉冲宽度调制)简单的讲是一种变频技术之一,是靠改变脉冲宽度来控制输出电压,通过改变周期来控制其输出频率。如果还不是很清楚,好吧,来看看我们实际生活中的例子,我们的电风扇为什么扭一下按扭,风扇的转速就会发生变化;调一下收音机的声音按钮,声音的大小就会发生变化;还有待会儿我们要讲的蜂鸣器也会根据不同的输入值而发出不同频率的叫声等等!!这些都是PWM的应用,都是通过PWM输出的频率信号进行控制的。
2. ARM Linux中的PWM
根据S3C2440的手册介绍,S3C2440A内部有5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM),定时器4是一个没有输出引脚的内部定时器,定时器0有一个用于大电流设备的死区生成器。看下图解释吧!!
由S3C2440的技术手册和上面这幅结构图,我们来总结一下2440内部定时器模块的特性吧:
1)共5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM);
2)每个定时器都有一个比较缓存寄存器(TCMPB)和一个计数缓存寄存器(TCNTB);
3)定时器0、1共享一个8位的预分频器(预定标器),定时器2、3、4共享另一个8位的预分频器(预定标器),其值范围是0~255;
4)定时器0、1共享一个时钟分频器,定时器2、3、4共享另一个时钟分频器,这两个时钟分频器都能产生5种不同的分频信号值(即:1/2、1/4、1/8、1/16和TCLK);
5)两个8位的预分频器是可编程的且根据装载的值来对PCLK进行分频,预分频器和钟分频器的值分别存储在定时器配置寄存器TCFG0和TCFG1中;
6)有一个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功能的使用。
三、蜂鸣器驱动实例
1. 蜂鸣器的种类和工作原理
蜂鸣器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。
压电式蜂鸣器主要由多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱、外壳等组成。有的压电式蜂鸣器外壳上还装有发光二极管。多谐振荡器由晶体管或集成电路构成。当接通电源后(1.5~15V直流工作电压),多谐振荡器起振,输出1.5~2.5kHZ的音频信号,阻抗匹配器推动压电蜂鸣片发声。
电磁式蜂鸣器由振荡器、电磁线圈、磁铁、振动膜片及外壳等组成。接通电源后,振荡器产生的音频信号电流通过电磁线圈,使电磁线圈产生磁场。振动膜片在电磁线圈和磁铁的相互作用下,周期性地振动发声。
有源蜂鸣器和无源蜂鸣器的区别:这个“源”字是不是指电源,而是指震荡源,即有源蜂鸣器内有振荡源而无源蜂鸣器内部没有振荡源。有振荡源的通电就可以发声,没有振荡源的需要脉冲信号驱动才能发声。
额外知识:简单蜂鸣器的制作方法
1)制备电磁铁M:在长约6厘米的铁螺栓上绕100圈导线,线端留下5厘米作引线,用透明胶布把线圈粘好,以免线圈松开,再用胶布把它粘在一个盒子上,电磁铁就做好了;
2)制备弹片P:从铁罐头盒上剪下一条宽约2厘米的长铁片,弯成直角,把电磁铁的一条引线接在弹片上,再用胶布把弹片紧贴在木板上;
3)用曲别针做触头Q,用书把曲别针垫高,用胶布粘牢,引出一条导线,如图连接好电路;
4)调节M与P之间的距离(通过移动盒子),使电磁铁能吸引弹片,调节触点与弹片之间的距离,使它们能恰好接触,通电后就可以听到蜂鸣声。
下面是驱动程序的源码,由于直接对部分源码在在程序中直接解释,所以用褐色标注源代码:
#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来描述一个混杂设备
#define DEVICE_NAME "PWM-Test"
加载模式后,执行“cat /proc/devices"命令看到的设备名称
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 tq2440_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 tq2440_pwm_close(struct inode *inode, struct file *file)
{
up(&lock);
return 0;
}
void up(struct semaphore *sem);/*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。*/
static int tq2440_pwm_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
定义一些局部变量
unsigned long tcfg0;
unsigned long tcfg1;
unsigned long tcntb;
unsigned long tcmpb;
unsigned long tcon;
if(cmd <= 0) 如果输入的参数小于或等于0的话,就让蜂鸣器停止工作
{
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_OUTP);
设置GPIO引脚的功能:本驱动中PWM所涉及的GPIO引脚设为输出功能
s3c2410_gpio_setpin(S3C2410_GPB0, 0);
由原理图可知直接给低电平可让蜂鸣器停止工作
}
else如果输入的参数大于0,就让蜂鸣器开始工作,不同的参数,蜂鸣器的频率也不 一样
{
struct clk *clk_p;
unsigned long pclk;
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_TOUT0);
tcon = __raw_readl(S3C2410_TCON); 读取寄存器TCON到tcon
tcfg1 = __raw_readl(S3C2410_TCFG1); 读取寄存器TCFG1到tcfg1
tcfg0 = __raw_readl(S3C2410_TCFG0); 读取寄存器TCFG0到tcfg0
mach/uncompress.h中定义了__raw_readl的定义,如下
static unsigned int __raw_readl(unsigned int ptr)
{
return *((volatile unsigned int *)ptr);
}
预分频为50
tcfg0 &= ~S3C2410_TCFG_PRESCALER0_MASK;
#define S3C2410_TCFG_PRESCALER0_MASK (255<<0)
S3C2410_TCFG_PRESCALER0_MASK定时器0和1的预分频值的掩码,TCFG[0~8]
tcfg0 |= (50 - 1); 设置tcfg0的值为49
tcfg1 &= ~S3C2410_TCFG1_MUX0_MASK;
#define S3C2410_TCFG1_MUX0_MASK (15<<0)
S3C2410_TCFG1_MUX0_MASK定时器0时钟分频器的掩
tcfg1 |= S3C2410_TCFG1_MUX0_DIV16;
#define S3C2410_TCFG1_MUX0_DIV16 (3<<0)
定时器0进行16分割
__raw_writel(tcfg1, S3C2410_TCFG1);
把tcfg1的值写到时钟分频寄存器S3C2410_TCFG1中
static void __raw_writel(unsigned int value, unsigned int ptr)
{
*((volatile unsigned int *)ptr) = value;
}
__raw_writel(tcfg0, S3C2410_TCFG0);
把tcfg0的值写到预分频寄存器S3C2410_TCFG0中
clk_p = clk_get(NULL, "pclk");
/**
* clk_get - lookup and obtain a reference to a clock producer.
* @dev: device for clock "consumer"
* @id: clock comsumer ID
*
* Returns a struct clk corresponding to the clock producer, or
* valid IS_ERR() condition containing errno. The implementation
* uses @dev and @id to determine the clock consumer, and thereby
* the clock producer. (IOW, @id may be identical strings, but
* clk_get may return different clock producers depending on @dev.)
*
* Drivers must assume that the clock source is not enabled.
*
* clk_get should not be called from within interrupt context.
*/
/* clk_get获取一个名为id的时针 * 输入参数dev: 可以为NULL * 输入参数id: 时针名称,如fclk、hclk、pclk等 * 返回值: 返回该时钟的clk结构体 |
从系统平台时钟队列中获取pclk的时钟频率,在include/linux/clk.h中定义
/**
* clk_get_rate - obtain the current clock rate (in Hz) for a clock source.
* This is only valid once the clock source has been enabled.
* @clk: clock source
*/
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
3)关闭定时器0的死区生成器(设置TCON的第4位);
4)开启定时器0的自动重载(设置TCON的第3位);
5)关闭定时器0的反相器(设置TCON的第2位);
6)开启定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位);
7)启动定时器0(设置TCON的第0位);
__raw_writel(tcon, S3C2410_TCON);
tcon &= ~2;
8)清除定时器0的手动更新TCNTB0&TCMPB0功能(设置TCON的第1位)。
__raw_writel(tcon, S3C2410_TCON);
}
return 0;
}
static struct file_operations dev_fops = {
.owner = THIS_MODULE,
这是一个宏,推向编译模块时自动创建的__this_module变量
.open = tq2440_pwm_open,
.release = tq2440_pwm_close,
.ioctl = tq2440_pwm_ioctl,
};
file_operations结构体,驱动函数的设置,分别将前面的驱动函数设置进来
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &dev_fops,
};
misc(混合)设备注册和注销:
其它类型---不能严格划分的设备类型,也叫混合类型
有:
1.
结构体:
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
};
2.
misc设备注册:
extern int misc_register(struct miscdevice * misc);
misc设备注销:
extern int misc_deregister(struct miscdevice * misc);
说明:上面的结构体是注册混合设备所需要的参数。主要有:
minor:次设备号,所有的misc设备共用一个主设备号,所以注册misc设备时只要次
设备号就可以了。利用次设备号来区分设备的。
name:misc设备名。
*fops:misc设备文件操作结构体。
其它三个参数很少用。
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>\n display a little,hei hei \n\n ");
}
module_init(dev_init);
module_exit(dev_exit);
模块创建时的入口点
模块卸载时的入口点
MODULE_LICENSE("GPL");
MODULE_AUTHOR("http://hi.baidu.com/shentuhongfeng");
MODULE_DESCRIPTION("PWM Drivers for EmbedSky TQ2440 Board");
下面是一个测试程序:
#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/PWM-Test", 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;
}
else
{
ioctl(fd, 1, temp);
}
}
close(fd);
return 0;
}
网上看到一个很经典的分析过程,个人自叹不如,先粘贴下来作为借鉴:
PWM在ARM Linux中的原理和蜂鸣器驱动实例开发
Linux内核驱动编程 2010-04-07 23:53:49
嵌入式Linux之我行,主要讲述和总结了本人在学习嵌入式linux中的每个步骤。一为总结经验,二希望能给想入门嵌入式Linux的朋友提供方便。如有错误之处,谢请指正。
一、开发环境
二、PWM怎样工作在ARM Linux中
1. 什么是PWM?
PWM(脉冲宽度调制)简单的讲是一种变频技术之一,是靠改变脉冲宽度来控制输出电压,通过改变周期来控制其输出频率。如果还不是很清楚,好吧,来看看我们实际生活中的例子,我们的电风扇为什么扭一下按扭,风扇的转速就会发生变化;调一下收音机的声音按钮,声音的大小就会发生变化;还有待会儿我们要讲的蜂鸣器也会根据不同的输入值而发出不同频率的叫声等等!!这些都是PWM的应用,都是通过PWM输出的频率信号进行控制的。
2. ARM Linux中的PWM
根据S3C2440的手册介绍,S3C2440A内部有5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM),定时器4是一个没有输出引脚的内部定时器,定时器0有一个用于大电流设备的死区生成器。看下图解释吧!!
由S3C2440的技术手册和上面这幅结构图,我们来总结一下2440内部定时器模块的特性吧:
1)共5个16位的定时器,定时器0、1、2、3都带有脉冲宽度调制功能(PWM);
2)每个定时器都有一个比较缓存寄存器(TCMPB)和一个计数缓存寄存器(TCNTB);
3)定时器0、1共享一个8位的预分频器(预定标器),定时器2、3、4共享另一个8位的预分频器(预定标器),其值范围是0~255;
4)定时器0、1共享一个时钟分频器,定时器2、3、4共享另一个时钟分频器,这两个时钟分频器都能产生5种不同的分频信号值(即:1/2、1/4、1/8、1/16和TCLK);
5)两个8位的预分频器是可编程的且根据装载的值来对PCLK进行分频,预分频器和钟分频器的值分别存储在定时器配置寄存器TCFG0和TCFG1中;
6)有一个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功能的使用。
三、蜂鸣器驱动实例
1. 蜂鸣器的种类和工作原理
蜂鸣器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。
压电式蜂鸣器主要由多谐振荡器、压电蜂鸣片、阻抗匹配器及共鸣箱、外壳等组成。有的压电式蜂鸣器外壳上还装有发光二极管。多谐振荡器由晶体管或集成电路构成。当接通电源后(1.5~15V直流工作电压),多谐振荡器起振,输出1.5~2.5kHZ的音频信号,阻抗匹配器推动压电蜂鸣片发声。
电磁式蜂鸣器由振荡器、电磁线圈、磁铁、振动膜片及外壳等组成。接通电源后,振荡器产生的音频信号电流通过电磁线圈,使电磁线圈产生磁场。振动膜片在电磁线圈和磁铁的相互作用下,周期性地振动发声。
有源蜂鸣器和无源蜂鸣器的区别:这个“源”字是不是指电源,而是指震荡源,即有源蜂鸣器内有振荡源而无源蜂鸣器内部没有振荡源。有振荡源的通电就可以发声,没有振荡源的需要脉冲信号驱动才能发声。
额外知识:简单蜂鸣器的制作方法
1)制备电磁铁M:在长约6厘米的铁螺栓上绕100圈导线,线端留下5厘米作引线,用透明胶布把线圈粘好,以免线圈松开,再用胶布把它粘在一个盒子上,电磁铁就做好了;
2)制备弹片P:从铁罐头盒上剪下一条宽约2厘米的长铁片,弯成直角,把电磁铁的一条引线接在弹片上,再用胶布把弹片紧贴在木板上;
3)用曲别针做触头Q,用书把曲别针垫高,用胶布粘牢,引出一条导线,如图连接好电路;
4)调节M与P之间的距离(通过移动盒子),使电磁铁能吸引弹片,调节触点与弹片之间的距离,使它们能恰好接触,通电后就可以听到蜂鸣声。
2. 开发板上蜂鸣器原理图分析
由原理图可以得知,蜂鸣器是通过GPB0 IO口使用PWM信号驱动工作的,而GPB0口是一个复用的IO口,要使用它得先把他设置成TOUT0
PWM输出模式。
3. 编写合适开发板的蜂鸣器驱动程序,文件名:my2440_pwm.c
/*
================================================
Name : my2440_pwm.c
Author : Huang Gang
Date : 25/11/09
Copyright : GPL
Description : my2440 pwm driver
================================================
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/clk.h>
#include <linux/device.h>
#include <asm/io.h>
#include <mach/hardware.h>
#include <mach/regs-gpio.h>
#include <plat/regs-timer.h>
#define PWM_MAJOR 0 //主设备号
#define PWM_NAME "my2440_pwm" //设备名称
static int device_major = PWM_MAJOR; //系统动态生成的主设备号
//打开设备
static int pwm_open(struct inode *inode, struct file *file)
{
//对GPB0复用口进行复用功能设置,设置为TOUT0 PWM输出
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_TOUT0);
return 0;
}
//关闭设备
static int pwm_close(struct inode *inode, struct file *file)
{
return 0;
}
//对设备进行控制
static int pwm_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
if(cmd <= 0)//如果输入的参数小于或等于0的话,就让蜂鸣器停止工作
{
//这里又恢复GPB0口为IO口输出功能,由原理图可知直接给低电平可让蜂鸣器停止工作
s3c2410_gpio_cfgpin(S3C2410_GPB0, S3C2410_GPB0_OUTP);
s3c2410_gpio_setpin(S3C2410_GPB0, 0);
}
else//如果输入的参数大于0,就让蜂鸣器开始工作,不同的参数,蜂鸣器的频率也不一样
{
//定义一些局部变量
unsigned long tcon;
unsigned long tcnt;
unsigned long tcfg1;
unsigned long tcfg0;
struct clk *clk_p;
unsigned long pclk;
//以下对各寄存器的操作结合上面讲的开始一个PWM定时器的步骤和2440手册PWM寄存器操作部分来看就比较容易理解
tcfg1 = __raw_readl(S3C2410_TCFG1); //读取定时器配置寄存器1的值
tcfg0 = __raw_readl(S3C2410_TCFG0); //读取定时器配置寄存器0的值
tcfg0 &= ~S3C2410_TCFG_PRESCALER0_MASK;
tcfg0 |= (50 - 1); //设置tcfg0的值为49
tcfg1 &= ~S3C2410_TCFG1_MUX0_MASK;
tcfg1 |= S3C2410_TCFG1_MUX0_DIV16; //设置tcfg1的值为0x0011即:1/16
__raw_writel(tcfg1, S3C2410_TCFG1); //将值tcfg1写入定时器配置寄存器1中
__raw_writel(tcfg0, S3C2410_TCFG0); //将值tcfg0写入定时器配置寄存器0中
clk_p = clk_get(NULL, "pclk");
pclk = clk_get_rate(clk_p); //从系统平台时钟队列中获取pclk的时钟频率,在include/linux/clk.h中定义
tcnt = (pclk/50/16)/cmd; //计算定时器0的输出时钟频率(pclk/{prescaler0
+ 1}/divider value)
__raw_writel(tcnt, S3C2410_TCNTB(0)); //设置定时器0计数缓存寄存器的值
__raw_writel(tcnt/2, S3C2410_TCMPB(0)); //设置定时器0比较缓存寄存器的值
tcon = __raw_readl(S3C2410_TCON); //读取定时器控制寄存器的值
tcon &= ~0x1f;
tcon |= 0xb; //关闭死区、自动重载、关反相器、更新TCNTB0&TCMPB0、启动定时器0
__raw_writel(tcon, S3C2410_TCON); //设置定时器控制寄存器的0-4位,即对定时器0进行控制
tcon &= ~2;
__raw_writel(tcon, S3C2410_TCON); //清除定时器0的手动更新位
}
return 0;
}
//设备操作结构体
static struct file_operations pwm_fops =
{
.owner = THIS_MODULE,
.open = pwm_open,
.release = pwm_close,
.ioctl = pwm_ioctl,
};
//定义一个设备类
static struct class *pwm_class;
static int __init pwm_init(void)
{
//注册为字符设备,主设备号为0让系统自动分配,设备名为my2440_pwm,注册成功返回动态生成的主设备号
device_major = register_chrdev(PWM_MAJOR, PWM_NAME, &pwm_fops);
if(device_major < 0)
{
printk(PWM_NAME " register falid!\n");
return device_major;
}
//注册一个设备类,使mdev可以在/dev/目录下自动建立设备节点
pwm_class = class_create(THIS_MODULE, PWM_NAME);
if(IS_ERR(pwm_class))
{
printk(PWM_NAME " register class falid!\n");
return -1;
}
//创建一个设备节点,设备名为PWM_NAME,即:my2440_pwm
device_create(pwm_class, NULL, MKDEV(device_major, 0), NULL, PWM_NAME);
return 0;
}
static void __exit pwm_exit(void)
{
//注销设备
unregister_chrdev(device_major, PWM_NAME);
//删除设备节点
device_destroy(pwm_class, MKDEV(device_major, 0));
//注销设备类
class_destroy(pwm_class);
}
module_init(pwm_init);
module_exit(pwm_exit);
MODULE_LICENSE("PGL");
MODULE_AUTHOR("Huang Gang");
MODULE_DESCRIPTION("my2440 pwm driver");
4. 将PWM蜂鸣器驱动代码部署到内核中。
#cp -f my2440_pwm.c /linux-2.6.30.4/drivers/char //把驱动源码复制到内核驱动的字符设备下
#gedit /linux-2.6.30.4/drivers/char/Kconfig //添加PWM蜂鸣器设备配置
config MY2440_PWM_BEEP
tristate "My2440 PWM Beep Device"
depends on ARCH_S3C2440
default y
---help---
My2440 PWM Beep
#gedit /linux-2.6.30.4/drivers/char/Makefile //添加PWM蜂鸣器设备配置
obj-$(CONFIG_MY2440_PWM_BEEP) += my2440_pwm.o
5.配置内核,选择PWM蜂鸣器设备选项
#make menuconfig
Device Drivers --->
Character devices --->
<*> My2440 PWM Beep Device (NEW)
6. 编译内核并下载到开发板上。这里要注意,现在我们不需要手动的在开发板上创建设备的节点了,因为我们现在使用了mdev进行管理了(使用方法请看:设备文件系统剖析与使用),在驱动程序中也添加了对类设备接口的支持。之前讲的一些驱动都没有,以后我们都使用这种方法。现在可以查看到/dev目录下自动创建好的my2440_pwm设备节点,就直接可以使用它了。
7. 编写PWM蜂鸣器驱动的测试程序。文件名:pwm_test.c
/*
==============================================
Name : pwm_test.c
Author : Huang Gang
Date : 25/11/2009
Copyright : GPL
Description : my2440 pwm driver test
==============================================
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
int main(int argc, char **argv)
{
int tmp;
int fd;
int i;
//打开蜂鸣器设备
fd = open("/dev/my2440_pwm", O_RDWR);
if(fd < 0)
{
printf("Open PWM Device Faild!\n");
exit(1);
}
//提示用户输入一个参数来对蜂鸣器进行调频,0表示停止工作
printf("please enter the times number(0 is stop):\n");
while(1)
{
//输入参数
scanf("%d", &tmp);
printf("times = %d\n", tmp);
//IO控制
ioctl(fd, tmp);
if(tmp <= 0)
{
break;
}
}
//关闭设备
close(fd);
return 0;
}
8. 在开发主机上交叉编译测试应用程序,并复制到文件系统的/usr/sbin目录下,然后重新编译文件系统下载到开发板上。
#arm-linux-gcc -o pwm_test pwm_test.c
9. 在开发板上运行测试程序。可以看到根据你输入参数的大小,蜂鸣器也会发生不同频率的叫声,输入0蜂鸣器停止鸣叫。
顶
相关文章推荐
- linux用户权限管理(二)
- 常常困扰C语言初学者的几个问题
- jQuery LigerUI 插件介绍及使用之ligerTree
- C# 串口操作系列(4) -- 协议篇,文本协议数据解析
- Android APP —— 百度地图使用手记(二)
- 二叉搜索树及其C++实现
- BootStrap Progressbar 实现大文件上传的进度条的实例代码
- Redis客户端之Spring整合Jedis
- 常用的服务器操作命令
- [Leetcode]82. Remove Duplicates from Sorted List II
- Consul入门04 - Consul集群
- Qt 关于使用Qt5.4获取主屏幕分辨率的程序
- rsync+inotify实现服务器之间文件实时同步
- LeetCode Largest Divisible Subset(动态规划)
- MarkDownPad 2
- C# 串口操作系列(3) -- 协议篇,二进制协议数据解析
- hibernate注解-属性级别注解
- 《Motion Design for iOS》(二十八)
- 【BBST 之伸展树 (Splay Tree)】
- SQL Server批量数据导出导入BCP使用