嵌入式Linux之我行——PWM在ARM Linux中的原理和蜂鸣器驱动实例开发

2019-07-12 14:46发布

嵌入式Linux之我行——PWM在ARM Linux中的原理和蜂鸣器驱动实例开发
    嵌入式Linux之我行,主要讲述和总结了本人在学习嵌入式linux中的每个步骤。一为总结经验,二希望能给想入门嵌入式Linux的朋友提供方便。如有错误之处,谢请指正。 一、开发环境
  • 主  机:VMWare--Fedora 9
  • 开发板:Mini2440--64MB Nand, Kernel:2.6.30.4
  • 编译器:arm-linux-gcc-4.3.2
二、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之我行——内核、驱动开发篇  TAG Linux PWM 蜂鸣器驱动 发表于: 2009-11-26,修改于: 2009-11-26 15:32,已浏览2343次,有评论1条 推荐 投诉         网友评论
  网友: 本站网友 时间:2010-07-20 23:16:27 IP地址:118.249.84.★       博主你好,我的文件系统照你的做的,加了mdev那些。。
  驱动也照你这个写的,奇怪的是它自动在dev生成了backlight节点,驱动里面也没加class_create,和device_create
  我的rcS  是这么写的。。
    #!/bin/sh  
PATH=/sbin:/bin:/usr/sbin:/usr/bin  
runlevel=S  
prevlevel=N  
umask 022  
export PATH runlevel prevlevel  
mount -a  
/bin/mount -t     proc     procfs    /proc
/bin/mount -n -t  sysfs    sysfs     /sys
/bin/mount -n -t  usbfs    usbfs     /proc/bus/usb
/bin/mount -t     ramfs    ramfs     /dev

/bin/mkdir -p /dev/pts
/bin/mkdir -p /dev/shm
/bin/mkdir -p /var/log
/bin/mount -n -t devpts none     /dev/pts -o mode=0622
/bin/mount -n -t tmpfs tmpfs     /dev/shm

 /sbin/mdev>/proc/sys/kernel/hotplug  
mdev -s  
/bin/hostname -F /etc/sysconfig/HOSTNAME   Blog作者的回复:
你是指LCD的背光驱动吗?这就并不奇怪了。要注意该驱动被注册成为misc(混杂)设备,在Linux中主设备号规定为10,再看看混杂设备的注册函数定义(drivers/char/misc.c):
int misc_register(struct miscdevice * misc)
{
    ......
    misc->this_device = device_create(misc_class, misc->parent, dev, NULL, "%s", misc->name);
    ......
}
其实是在这里面进行了设备文件的动态创建。    
  网友: 本站网友 时间:2010-07-21 19:34:53 IP地址:118.249.84.★       谢谢。。明白了。    
  网友: clever0725 时间:2010-08-12 10:13:35 IP地址:60.190.128.★       看不太明白的事
一会儿DMA传输 一会儿FIFO传输的
请问是怎么区别啊 ?    
  网友: 张磊 时间:2010-08-26 11:16:32 IP地址:218.58.60.★       您好,您的文章非常精彩,也很有帮助,我使用了您了触摸屏驱动后,解决了和AD采集的冲突,但是不知道为什么AD采集一段时间后就停住了,也没有任何出错信息,您觉得可能是什么原因造成的?谢谢    
  网友: 本站网友 时间:2010-08-30 16:30:02 IP地址:220.248.113.★       谢谢你给学习arm的人提供的帮助。    
  网友: 本站网友 时间:2010-09-11 10:12:41 IP地址:119.4.150.★       嗯,正好,我也用的这个内核和2440板子,TQ2440。多向您学习!我也想把自己做好的驱动写到这上面! 我注册了:zajiju    
  网友: clever0725 时间:2010-09-20 14:44:44 IP地址:60.190.128.★       在RTC平台设备驱动中
请问设备号怎么没看见???
在哪里申请的 ? Blog作者的回复:
你是否看到“四、回过头再来分析理解具体RTC驱动程序代码的结构”上面的一段描述呢?这里讲到了的。    
  网友: 本站网友 时间:2010-09-26 21:08:10 IP地址:221.12.174.★       非常好的博客!
谢谢资源共享    
  网友: 本站网友 时间:2010-09-26 21:08:47 IP地址:221.12.174.★       这么好的图是那里的呢?
呵呵    
  网友: 本站网友 时间:2010-09-28 15:23:44 IP地址:210.21.125.★       写的太好了,清晰明了
想请教下目前正在做一个项目,linux系统是跑在norflash上的,怎样实现把文件写在nandflash上,能否提供一下思路,谢谢 Blog作者的回复:
参考这里:
http://blog.chinaunix.net/u3/101649/showart_2119943.html
http://blog.chinaunix.net/u3/101649/showart_2134240.html
把kernel和filesystem烧写到NandFlash上,那文件就自然是在NandFlash上的filesystem中了。    
  网友: 本站网友 时间:2010-09-30 14:42:39 IP地址:218.104.96.★       看了你的分享,很感激你    
  网友: 鬼鬼 时间:2010-10-23 18:48:40 IP地址:121.22.29.★       首先多谢你的回复,呵呵!
我的疑问如下:
在rtc_device_register("my2440", &pdev->dev, &rtcops, THIS_MODULE)把rtcops传递给结构体rtc,
然后是rtc_dev_prepare(rtc);并且rtc_dev_prepare(struct rtc_device *rtc)
和cdev_init(&rtc->char_dev, &rtc_dev_fops);这个地方rtc_dev_fops是已经定义好的接口函数,
这个rtc_dev_fops和你的rtcops接口函数究竟是什么联系,我不明白这里????????????
/////////注意观察rtc->ops->read_time(rtc->dev.parent, tm);
这句,其实就是调用自己写的驱动中rtc_class_ops结构中的read_time。//////////
我的理解是ioctl函数是调用了rtc_dev_fops里的接口函数ioctl然后执行rtc_read_time(rtc, &tm);
怎会调用自己写的驱动中rtc_class_ops结构中的read_timeNE ??????????????????????????

   
  网友: 笑寒 时间:2010-10-28 22:10:46 IP地址:121.248.2.★       /*定义一个AUTOPST宏,将ADC触摸屏控制寄存器设置成自动转换模式*/
#define AUTOPST    (S3C2410_ADCTSC_YM_SEN | S3C2410_ADCTSC_YP_SEN | S3C2410_ADCTSC_XP_SEN | /
                S3C2410_ADCTSC_AUTO_PST | S3C2410_ADCTSC_XY_PST(0))

按照寄存器,自动转换模式只要设置PULL-UP为1,AUTO_PST为1,XY_PST为00,就可以了吧,4到7位不用设置的吧    
  网友: 本站网友 时间:2010-11-24 21:53:19 IP地址:222.175.103.★       受益良深,请问“嵌入式Linux之我行——S3C2440上Flash驱动实例开发讲解(二)” 什么时候出啊    
  网友: 本站网友 时间:2010-11-25 09:16:18 IP地址:121.50.21.★       你好,请问下画流程图的工具是什么啊?  多谢! Blog作者的回复:
就用office2007    
  网友: 本站网友 时间:2010-11-25 18:02:32 IP地址:125.71.92.★       你好,谢谢博主的分享,你的这些文章对我们这些初学着来说很有启发性。
关于s3c2410的LCD驱动还有一个地方不是很清楚,那就是在哪里激活控制器的,即使能控制器!在函数.fb_set_par= s3c2410fb_set_par,中设置了控制寄存器,但是这个函数是什么时候调用呢?
还有一个问题就是,如何读fbcon的代码呢,感觉自己思路很混乱    
  网友: 本站网友 时间:2010-12-24 17:24:03 IP地址:210.5.128.★       你好!
我用的也是S3C2440的板子,linux2.6.32的内核,make modules时,编译报错:
:48: error: 'adc_fops' undeclared here (not in a function)
:55: error: 'adc_open' undeclared here (not in a function)
:56: error: 'adc_read' undeclared here (not in a function)
:57: error: 'adc_release' undeclared here (not in a function)
,这跟2.6.30的内核相比,还需要添加什么头文件吗?    
  网友: 本站网友 时间:2010-12-24 17:52:33 IP地址:210.5.128.★       是我没注意,编译报错是我没有理顺代码的顺序。因为有些函数用了,但之前却没有申明。
把代码的顺利理好后就可以make modules了    
  网友: 本站网友 时间:2011-01-26 14:54:35 IP地址:58.39.20.★       请问下,如果我想用俩路AD转换同时运行,需要怎么做?有哪些地方需要注意的吗?    
  网友: muyuyuzhong 时间:2011-02-23 22:29:06 IP地址:121.8.210.★       博主:您好,您的文章非常精彩,为了便于学习,我几乎把您所有的文章都转载到了我的博客http://blog.chinaunix.net/space.php?uid=11304735,如果侵犯了您的利益,请告知我。非常感谢您对我的帮助,O(∩_∩)O谢谢!    
  网友: 本站网友 时间:2011-02-26 11:22:27 IP地址:210.42.147.★       版主你好,我在写代码中遇到头文件问题#include 

#include 这两个头文件应该是包含的一些端口定义,但是我用的内核里没有这两个文件,我就从别的中拷贝到include下,结果还是不行,请求解决
谢谢