本篇文章使用开发板与上一篇文章一致:实战!手把手教你如何编写一个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);
}