Linux嵌入式_详解从原理图到数据手册解析PWM蜂鸣器实现

Posted 17岁boy想当攻城狮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux嵌入式_详解从原理图到数据手册解析PWM蜂鸣器实现相关的知识,希望对你有一定的参考价值。

本篇文章使用开发板与上一篇文章一致:实战!手把手教你如何编写一个Linux驱动并写一个支持物联网的LED演示demo

首先第一步打开电路原理图,找到蜂鸣器部分

首先可以看到VDD 5V也就是说这个蜂鸣器电路需要至少5V的电流才能驱动,后面跟了一个CON2通过原理图可以看到上面有两个孔,是针孔可以在上面焊排针,或者排母把这个接口引出来,然后它接到BUZZER上面,这部分接口我的PCB板上没有引出来,所以不用这部分,这部分是直接接到蜂鸣器上的,文档里说这个蜂鸣器是有源的,给电平就可以跑的,但是没有直接的引脚引到上面,你可以引到CON2上面然后给它使能一个5V的电流它就会响。

第二部分,这里有一个XpwmTOUT1的符号,这个符号就是代表PWM引脚接在这上面,RF1是保险电阻,阻止为100欧姆,超出这个电流会把保险丝烧断,导致这条线路无法正常工作,其实这个电路名可能错了,应该是F1,保险电阻的符号是F,不过每个人画图的风格不同,它这里用来RF,R电阻,F保险电阻

这里又接了一个放大三极管,符号是Q,用于放大电流,这里就可以使电流导向蜂鸣器,正常我们电流可能不达5v,这里经过放大就达到5v就可以使能这个蜂鸣器

接下来我们在去看coreb原理图,看下XpwmTOUT1接在哪个引脚上

可以看到接在GPD0引脚上,这里PWM是一个模块,已经为我们写好了,我们只需要配置一下PWM模块时钟频率,占空比就可以实现PWM的控制。

核心还是去看数据手册,首先我们可以在数据手册里看一下PWM模块的结构原理

可以看到PWM是使用PCLK时钟源实现的,前面一个8位的PRESCALER(预分频器),然后紧接着就是MUX,也就是分频系数,这里可以理解为一级分频与二级分频,可以通过MUX来配置电路选择,然后MUX后面的电路是一个寄存器,control logic也就是控制逻辑,里面包含了两个寄存器,一个是TCMPB1,另外一个是TCNTB1。

下面给大家介绍这两个寄存器的作用:

TCNTB是计数器,当时钟设置好频率之后,每隔一段频率会对这个计数器进行减一,同时还有另外一个比较寄存器,用来做对比的,当TCNTB减为与比较寄存器相等时则触发中断,一般比较寄存器为0,所以我们一般根据频率来设置TCNTB计数器的值,这就是时钟的实现原理,大多数都是通过晶振实现,因为晶振频率受干扰率较小,最为稳定,但误差总会有的,这些误差一般在微妙与纳秒中,所以我们一般不用关心,除非做特别的数学运算。

上面提到的比较寄存器就是TCMPB,在PWM模块里当时钟中断时,就会发高电平,时钟不中断时就发低电平,这是PWM模块设计原理,我们也可以自己利用时钟实现自己的PWM模块,通过设置时钟的IQR断点来发高电平,其它时间空闲低电平。

占空比我们就可以通过设置TCMPB来控制我们高电平的输出,这一点到后面我会通过代码进行更仔细的讲解。

如果当你对这个结构不了解,或者说不知道为什么先设置分频这些,可以通过手册里对PWM的介绍,会告诉你应该先设置什么,还有推荐值,这里是芯片设计者们设计的,我们需要根据它们的规范来进行开发。

 PWM全名是Pulse width modulation(脉冲宽度调制)。

那么我们先的工作就是:

1.设置好分频值

2.设置好分频系数

3.配置好周期以及占空比

4.最后使能时钟

首先我们的第一步就是先创建一个Linux驱动文件

touch main.c && vim main.c

首先先看下预分频器怎么设置

芯片数据手册里现实了最小分频值以及分频后的MHZ对应的us秒数,这里是有个公式的,我后面会和大家说一下。

 其中可以看到min最小为1,max最大为255

同时前面的这一块就是分频系数,因为分频系数的不同也会导致频率发生变化,因为分频后的值会根据分频系数在做一个除法过程,就是%比的一个分频值.

官方也给出了计算公式:

 

 这里显示着,TCFG0用来设置不同的预分频器

从芯片手册架构里我们知道我们的预分频器是0 

 

MUX是1,这里虽然MUX没有编号没有标出来,我们可以从TCMPB0为编号作为索引来排就知道我们的MUX是1

那么我们先把TCFG0的基质给定义出来

#define TCFG0 0xE2500000

 那么接下来是MUX,我们去芯片手册里搜索一下MUX关键字

 是由TCFG1来控制的

通过设置这个寄存器不同的位来选择MUX。

那么把它的地址也一起定义出来

#define TCFG0 0xE2500000        //预分频器
#define TCFG1 0xE2500004        //MUX

 接下来就是TCMPB1,和TCNTB1

#define TCFG0     0xE2500000        //预分频器
#define TCFG1     0xE2500004        //MUX
#define TCNTB1    0xE2500018        //计数器
#define TCMPB1    0xE250001C        //占空比

其次上面看到了,是挂载到PCLK上的,数据手册上写的PCLK时钟源由TCON控制,TCON也需要定义出来

 

#define TCFG0           0xE2500000        //预分频器
#define TCFG1           0xE2500004        //MUX
#define TCNTB1          0xE2500018        //计数器
#define TCMPB1          0xE250001C        //占空比
#define TCON            0xE0200008        //时钟控制寄存器

最后就是XpwmTOUT1了,之前看核心板里的原理图XpwmTOUT1对应的是GPD0,所以我们也把GPD0定义出来 

这里我们不需要使用DAT寄存器,因为这里PWM会帮我们做好,所以我们只需要定义CON配置寄存器就可以了,来配置GPD引脚的功能

#define TCFG0           0xE2500000        //预分频器
#define TCFG1           0xE2500004        //MUX
#define TCNTB1          0xE2500018        //计数器
#define TCMPB1          0xE250001C        //占空比
#define TCON            0xE0200008        //时钟控制寄存器
#define GPD0CON_BASE    0xE02000A0        //功能寄存器

其次通过GPD口介绍也得知,如果要使能PWM需要将其设置为TOUT模式 

好了接下来就是写驱动文件

关于位操作以及驱动文件基本体系框架可以参考我这篇文章:实战!手把手教你如何编写一个Linux驱动并写一个支持物联网的LED演示demo

这篇文章对这些就不做过多的介绍了,重点说PWM

基本的驱动代码:

#include <linux/module.h>
#include <linux/io.h>
#include <linux/platform_device.h>
#include <linux/init.h>
#include <linux/serial_core.h>
#include <linux/serial.h>
#include <asm/irq.h>
#include <mach/hardware.h>
#include <plat/regs-serial.h>
#include <mach/regs-gpio.h>
#include <asm/uaccess.h>

//协议
MODULE_AUTHOR("Stephen Zhou");
MODULE_LICENSE("GPL");

//功能基地址
#define TCFG0           0xE2500000        //预分频器
#define TCFG1           0xE2500004        //MUX
#define TCNTB1          0xE2500018        //计数器
#define TCMPB1          0xE250001C        //占空比
#define TCON            0xE0200008        //时钟控制寄存器
#define GPD0CON_BASE    0xE02000A0        //功能寄存器

//指针指向
volatile unsigned long* addr_gpid0_con = (volatile unsigned long*)GPD0CON_BASE;
volatile unsigned long* addr_tcfg0     = (volatile unsigned long*)TCFG0;
volatile unsigned long* addr_tcfg1     = (volatile unsigned long*)TCFG1;
volatile unsigned long* addr_tcntb1    = (volatile unsigned long*)TCNTB1;
volatile unsigned long* addr_tcmpb1    = (volatile unsigned long*)TCMPB1;

//设备文件描述符
static int buzzer_kernel_fd = 0;
static struct class* buzzer_class = NULL;
static struct device* buzzer_son = NULL;

//open
static int buzzer_open(struct inode* inode,struct file* file){
    
    return 0;
}

//file module fops
static struct file_operations buzzer_fops = {
        .owner  = THIS_MODULE,
        .open = buzzer_open,
};


//init
static int __init buzzer_init(void){

        buzzer_kernel_fd = register_chrdev(0,"my_buzzer",&buzzer_fops);
        buzzer_class = class_create(THIS_MODULE,"my_buzzer_class");
        buzzer_son = device_create(buzzer_class,NULL,MKDEV(buzzer_kernel_fd,0),NULL,"my_buzzer");

        addr_gpid0_con = (volatile unsigned long*)ioremap(GPID0_CON,16);        
        addr_gpid0_dat = (volatile unsigned long*)ioremap(GPID0_DAT,16);        
        addr_tcfg0     = (volatile unsigned long*)ioremap(TCFG0,16);
        addr_tcfg1     = (volatile unsigned long*)ioremap(TCFG1,16);
        addr_tcon      = (volatile unsigned long*)ioremap(TCON,16);
        addr_tcntb1    = (volatile unsigned long*)ioremap(TCNTB1,16);
        addr_tcmpb1    = (volatile unsigned long*)ioremap(TCMPB1,16);

        printk("TQ210_BUZZER:init\\n");
        return 0;
}

//exit
static void __exit buzzer_exit(void){

        device_unregister(buzzer_son);
        class_destroy(buzzer_class);

        iounmap(addr_gpid0_con);
        iounmap(addr_gpid0_dat);
        iounmap(addr_tcfg0);
        iounmap(addr_tcfg1);
        iounmap(addr_tcon);
        iounmap(addr_tcntb1);
        iounmap(addr_tcmpb1);

        unregister_chrdev(buzzer_kernel_fd,"my_buzzer");

        printk("TQ210_BUZZER:exit\\n");

}

//init exit 
module_init(buzzer_init);
module_exit(buzzer_exit);

这段代码运行后会在/sysfs目录下注册my_buzzer_class,最后udev检测之后会在/dev目录下注册my_buzzer文件。

那么这里我们就在open里实现pwm

static int buzzer_open(struct inode* inode,struct file* file){
    
    return 0;
}

首先第一步先看一下GPD口的说明

注意,核心板上我们对应的是GPD0_1,这里0010是设置为TOUT_1 

那么我们可以用指针对它进行操作,在4-7这个bit范围

//先清空在赋值 2 = 0010
*addr_gpid0_con &= ~(0xF << 4);
*addr_gpid0_con |= 2 << 4;

接下来是设置预分频器

 我们的预分频器是0,所以是从最低位开始,这里我设置为65,大家可以随意设置,这个65是手册建议值,这里不用使用或运算,直接等于就可以了,因为从低位开始不会溢出到高位

//设置分频值
*addr_tcfg0 = (65<<0);

然后就是MUX,这里我们的MUX是1

 这里我选择的是1/2分频系数

//设置分频系数 1/2 1=0001
*addr_tcfg1     |= (1<<4);

好了,接下来就是设置TCNTB与TCMPB了,这里来和大家说一下怎么计算这两个数

这里通过给定的计算公式:

时钟频率=PCLK/((预分频值+1))/分频系数

PCLK我们得知是66,分频值我们设置的是65,分频系数是1/2也就是除于2

所以公式:66/(65+1)/2 =0.5

在将0.5转化为HZ的单位:0.5*1000=500KHZ,这里还有一个公式,就是算HZ转秒,赫兹的倒数就是它的秒这个公式:(1/500)=0.002ms,1毫秒=1000微秒,0.002毫秒等于2微秒,通过这样的算法公式就可以得知我们现在的频率是每2微秒工作一次

我们想让它每0.5毫秒工作一次,所以TCNTB的值=500毫秒/当前微秒频率2

500/2=250,所以TCNTB的值应该为250,当然你也可以这也算,2微秒一次,1毫秒=1000微秒,0.5毫秒就等于500微秒,500微秒/2微秒=250,就得出经过250次2微秒后就到达了500微秒,而500微秒就等于0.5毫秒

//设置周期 0.5ms
*addr_tcntb1    = 250;

接下来就是占空比,这里和大家说一下什么是占空比,占空比是指工作的时间站整个周期的百分比。

如你现在从做饭到吃饭到洗碗总共用了5秒(这里是假设, 方便大家更好理解),吃饭时间用了2秒,这里吃饭对应着高电平,其余的3秒都非吃饭时间,所以占空比=3/5=0.6*100,占空比为60%

这里我想让占空比为50%,计数器为250每一个0.5ms周期,那就是250*50%(0.5)=125即可,若想为70%则250*0.7=175

所以这里我们设置占空比为50%

//设置占空比 
*addr_tcmpb1    = 125;

最后就是使能时钟了

这里我们设置TIMER 1,在芯片手册里我们的架构对应的是TIMER1

可以在手册里清楚的看到是由哪个时钟控制

 这里我们根据手册先设置,更新时钟寄存器,将设置好的值更新到时钟控制寄存器中

//更新计数器以及占空比到时钟寄存器中
*addr_tcon      = (1<<9);

 这里需要注意,当更新一次时钟中断结束以后就会把内部计数器值清空,所以需要重新装载

//自动装载
*addr_tcon      = (1<<11);

 然后启动时钟

//启动时钟
*addr_tcon      = (1<<8);

 完整open代码:

static int buzzer_open(struct inode* inode,struct file* file){
    //先清空在赋值 2 = 0010
    *addr_gpid0_con &= ~(0xF << 4);
    *addr_gpid0_con |= 2 << 4;
    //设置分频值
    *addr_tcfg0 = (65<<0);
    //设置分频系数 1/2 1=0001
    *addr_tcfg1     |= (1<<4);
    //设置周期 0.5ms
    *addr_tcntb1    = 250;
    //设置占空比 
    *addr_tcmpb1    = 125;
    //更新计数器以及占空比到时钟寄存器中
    *addr_tcon      = (1<<9);
    //自动装载
    *addr_tcon      = (1<<11);
    //启动时钟
    *addr_tcon      = (1<<8);
}

完整代码:

#include <linux/module.h>
#include <linux/io.h>
#include <linux/platform_device.h>
#include <linux/init.h>
#include <linux/serial_core.h>
#include <linux/serial.h>
#include <asm/irq.h>
#include <mach/hardware.h>
#include <plat/regs-serial.h>
#include <mach/regs-gpio.h>
#include <asm/uaccess.h>

//协议
MODULE_AUTHOR("Stephen Zhou");
MODULE_LICENSE("GPL");

//功能基地址
#define TCFG0           0xE2500000        //预分频器
#define TCFG1           0xE2500004        //MUX
#define TCNTB1          0xE2500018        //计数器
#define TCMPB1          0xE250001C        //占空比
#define TCON            0xE0200008        //时钟控制寄存器
#define GPD0CON_BASE    0xE02000A0        //功能寄存器

//指针指向
volatile unsigned long* addr_gpid0_con = (volatile unsigned long*)GPD0CON_BASE;
volatile unsigned long* addr_tcfg0     = (volatile unsigned long*)TCFG0;
volatile unsigned long* addr_tcfg1     = (volatile unsigned long*)TCFG1;
volatile unsigned long* addr_tcntb1    = (volatile unsigned long*)TCNTB1;
volatile unsigned long* addr_tcmpb1    = (volatile unsigned long*)TCMPB1;

//设备文件描述符
static int buzzer_kernel_fd = 0;
static struct class* buzzer_class = NULL;
static struct device* buzzer_son = NULL;

//open
static int buzzer_open(struct inode* inode,struct file* file){
    //先清空在赋值 2 = 0010
    *addr_gpid0_con &= ~(0xF << 4);
    *addr_gpid0_con |= 2 << 4;
    //设置分频值
    *addr_tcfg0 = (65<<0);
    //设置分频系数 1/2 1=0001
    *addr_tcfg1     |= (1<<4);
    //设置周期 0.5ms
    *addr_tcntb1    = 250;
    //设置占空比 
    *addr_tcmpb1    = 125;
    //更新计数器以及占空比到时钟寄存器中
    *addr_tcon      = (1<<9);
    //自动装载
    *addr_tcon      = (1<<11);
    //启动时钟
    *addr_tcon      = (1<<8);
}

//file module fops
static struct file_operations buzzer_fops = {
        .owner  = THIS_MODULE,
        .open = buzzer_open,
};


//init
static int __init buzzer_init(void){

        buzzer_kernel_fd = register_chrdev(0,"my_buzzer",&buzzer_fops);
        buzzer_class = class_create(THIS_MODULE,"my_buzzer_class");
        buzzer_son = device_create(buzzer_class,NULL,MKDEV(buzzer_kernel_fd,0),NULL,"my_buzzer");

        addr_gpid0_con = (volatile unsigned long*)ioremap(GPID0_CON,16);        
        addr_gpid0_dat = (volatile unsigned long*)ioremap(GPID0_DAT,16);        
        addr_tcfg0     = (volatile unsigned long*)ioremap(TCFG0,16);
        addr_tcfg1     = (volatile unsigned long*)ioremap(TCFG1,16);
        addr_tcon      = (volatile unsigned long*)ioremap(TCON,16);
        addr_tcntb1    = (volatile unsigned long*)ioremap(TCNTB1,16);
        addr_tcmpb1    = (volatile unsigned long*)ioremap(TCMPB1,16);

        printk("TQ210_BUZZER:init\\n");
        return 0;
}

//exit
static void __exit buzzer_exit(void){

        device_unregister(buzzer_son);
        class_destroy(buzzer_class);

        iounmap(addr_gpid0_con);
        iounmap(addr_gpid0_dat);
        iounmap(addr_tcfg0);
        iounmap(addr_tcfg1);
        iounmap(addr_tcon);
        iounmap(addr_tcntb1);
        iounmap(addr_tcmpb1);

        unregister_chrdev(buzzer_kernel_fd,"my_buzzer");

        printk("TQ210_BUZZER:exit\\n");

}

//init exit 
module_init(buzzer_init);
module_exit(buzzer_exit);

编译成驱动文件之后在写一个用户态的代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void){

        open("/dev/my_buzzer",O_RDWR);
}

以上是关于Linux嵌入式_详解从原理图到数据手册解析PWM蜂鸣器实现的主要内容,如果未能解决你的问题,请参考以下文章

Candence基础知识1(CADENCE从原理图到PCB步骤 )

智能小车十《从原理图到PCB图》

MCAL PWM Module详解

05day02pwm

lm358寻迹电路:从原理图到焊接的总结

嵌入式PWM定时器