要操控蜂鸣器,首先我们需要找到这个器件的原理图,从而找到芯片中控制这个器件的管脚,于是我们可以先去开发板的电路原理图中搜索该器件,找到原理图:

可以看到FS4412开发板使用了其中一路PWM输出(PWM0,对应管脚为GPD0.0)接蜂鸣器。
PWM,脉宽调制器,就是一个输出脉冲宽度可以调整的硬件器件,它不仅脉冲宽度可调,频率也可以调整,核心部件是一个硬件定时器。PWM管脚默认输出高电平。

上图可以说明PWM工作原理,在时刻1设置计数值为160,比较值设置也为109,时刻2启动定时器,PWM输出低电平,计数器开始做减法,当计数值减到和比较值一致时,输出反转,当计数达到0,完成一次计数。形成一个波形,波形周期由计数值决定,占空比由比较值决定。我们可以通过调整这两个值使蜂鸣器发出特有频率的声音。
然后我们在芯片手册中找到pwm0内部结构图:

可以看到输入时钟为PCLK,经过八位预分频和第二次的再分频的时钟最终给到PWM0所对应的计数器0,TCNTB0是计数寄存器,用于控制PWM输出波形的频率,TCMPB0是比较寄存器,用于控制PWM输出波形的占空比。通过查看这些寄存器,并对他们进行配置,我们可以完成对PWM的驱动代码的硬件部分的编写。
首先再回顾以下我们linux字符设备驱动的编写框架:
1.实现入口函数 xxx_init() 和卸载函数 xxx_exit()
2.申请设备号 alloc_chrdev_region (与内核相关)
3.注册字符设备驱动 cdev_alloc,cdev_init,cdev_add (与内核相关)
4.利用 udev/mdev 机制创建设备文件(节点) class_create,device_create(与内核相关)
5.硬件部分初始化从设备树获取硬件资源platform_get_resource()
IO 资源映射 ioremap ,内核提供 gpio库函数(与硬件相关)
注册中断(与硬件相关)
初始化等待队列(与内核相关)
初始化定时器(与内核相关)
6.构建 file_operation 结构(与内核相关)
7.实现操作硬件的方法 xxx_open,xxx_read,xxx_write…(与硬件相关)
首先我们将驱动的框架搭建好,再进行填充
头文件,查找对应的头文件我们可以使用多种方法,可以在内核源码目录建立索引使用ctags,进行搜索查找,或者使用soureinsight进行查找。
- #include
- #include
- #include
- #include
- #include
- #include
- #include
定义变量来接收对应寄存器的映射地址,以及设备号设备名等等。
- unsigned int *gpd0con;
- unsigned int *tcfg0;
- unsigned int *tcfg1;
- unsigned int *tcon;
- unsigned int *tcntb0;
- unsigned int *tcmpb0;
-
- static struct resource *rescon;//接收设备树中状态寄存器资源
- static struct resource *resdata;//接收设备树中数据寄存器资源
- static dev_t devnum;//设备号
- static char *name ="mybuzzer";//设备名
-
- unsigned int *gpx1con;
- unsigned int *gpx1data;
声明一些函数,在我们的驱动和设备匹配成功后,会调用probe方法,在移除驱动时,会调用remove方法,所以我们对应的字符设备申请,注册,初始化,硬件资源获取,地址映射等都在probe种执行,remove种执行相应的注销销毁释放,解除映射等操作。
-
-
- //实现platform_driver
-
- int my_probe(struct platform_device *pdev);
- int my_remove(struct platform_device *pdev);
- long my_ioctl(struct file *pf, unsigned int cmd, unsigned long args);//IO操作函数
-
-
- static struct cdev mycdev ;
- static struct class * myclass;//方便多个相同设备管理
- static struct device * mydevice;
-
- static struct file_operations myops={
-
- };
入口函数和卸载函数,不用再将硬件操作放在加载模块部分,这样更好体现了分离的思想,加载和卸载模块只需要进行驱动的注册和注销操作即可。
- static int mod_init(void)
- {
- return platform_driver_register(&mydriver); //平台驱动注册
- }
- static void mod_exit(void)
- {
- platform_driver_unregister(&mydriver); //平台驱动注销
- }
- module_init(mod_init);//注册
- module_exit(mod_exit);
- MODULE_LICENSE("GPL");//模块信息
定义平台驱动对象并初始化,设置匹配规则为对应的资源通过设备树进行获取,当然我们需要到相应的设备树dts文件中,进行修改,使我们的名字能够匹配上,并且寄存器的地址也要修改和我们芯片手册上地址一样才行,然后编译得到对应的dtb文件。
- //定义platform_driver对象
- struct of_device_id of_matches[]={
- {.compatible="fs,mybee"}, //修改匹配规则,从设备树
- {}, //获取buzzer相关硬件信息
- };
- static struct platform_driver mydriver ={
- .probe = my_probe,
- .remove = my_remove,
- .driver = {
- .name = "mytest",
- .of_match_table = of_matches, //通过设备树匹配
- },
-
- };
修改设备树文件内容,名字要和我们驱动代码中一样,才能进行匹配,寄存器地址要和芯片手册对应管脚位置一样。

获取硬件资源,由于这几个寄存器地址都是连续的所以我们只需要获取最开始的地址然后进行偏移即可,随后建立映射,字符设备驱动的注册等操作
- int my_probe(struct platform_device *pdev)
- {
- int ret;
- //通过设备树获取 硬件资源
- printk("match\n");
- rescon = platform_get_resource(pdev,IORESOURCE_MEM,0);
- if(rescon==NULL){
- goto failed_getcon;
- }
- printk("%#x\n",rescon->start);
- gpd0con = ioremap(rescon->start,4);//建立映射
-
- resdata = platform_get_resource(pdev,IORESOURCE_MEM,1);
- if(resdata==NULL){
- goto failed_getdata;
- }
- printk("%#x\n",resdata->start);
- tcfg0 = ioremap(resdata->start,4);
- tcfg1 = ioremap(resdata->start+4,4);
- tcon = ioremap(resdata->start+8,4);
- tcntb0 = ioremap(resdata->start+12,4);
- tcmpb0 = ioremap(resdata->start+16,4);
-
- //字符设备注册
- ret = alloc_chrdev_region(&devnum,0,1,name);//1.申请设备号
- if(ret!=0){
- goto failed_alloc;
- }
- cdev_init(&mycdev,&myops); //2.cdev初始化
-
- ret = cdev_add(&mycdev,devnum,1); //3.cdev添加到内核
- if(ret!=0){
- goto failed_add;
- }
- printk("register success %d,%d\n",MAJOR(devnum),MINOR(devnum));
-
- myclass = class_create(THIS_MODULE,"myclass");
- if(IS_ERR(myclass)){
- goto failed_class;
- }
- mydevice = device_create(myclass,NULL,devnum,NULL,"buzzer");
- if(IS_ERR(mydevice)){
- goto failed_device;
- }
- //硬件操作
- buzzer_init();
- buzzer_off();
- #endif
- return 0;
-
- //对应的操作失败函数
- failed_device:
- class_destroy(myclass);
- failed_class:
- cdev_del(&mycdev);
- failed_add:
- unregister_chrdev_region(devnum,1);
- failed_alloc:
- return -1;
- }
卸载时对应的操作,与我们在probe方法中进行的操作恰好相反,由地址映射,先接触映射,然后销毁设备,销毁类,删除字符设备,然后注销设备号。
- int my_remove(struct platform_device *pdev)
- {
- printk("driver remove\n");
- iounmap(gpx1con);//解除映射
- iounmap(gpx1data);
-
- device_destroy(myclass,devnum);//销毁设备
- class_destroy(myclass);//销毁类
- cdev_del(&mycdev);//删除字符设备
- unregister_chrdev_region(devnum,1);//注销设备号
-
- return 0;
- }
硬件操作pwm初始化,这里的操作就按照芯片手册对应寄存器的设置进行相应的位操作,这里我们将管脚GPD0.0设置为功能2(TOUT_0),即PWM0的输出不上拉,驱动强度为最低级别。随后进行预分频,二分频等操作。
- int buzzer_init(void)
- {
- u32 tmp;
- tmp = readl(gpd0con);
- tmp &= ~0xf;
- tmp |= 0x2;
- writel(tmp,gpd0con);
-
- tmp = readl(tcfg0);
- tmp |= 0xff;
- writel(tmp,tcfg0);
-
- tmp = readl(tcfg1);
- tmp &= ~0xf;
- tmp |= 0x3;
- writel(tmp,tcfg1);
-
- writel(110,tcntb0);
- writel(110/2,tcmpb0);
-
- tmp = readl(tcon);
- tmp |= 0x1<<3;
- tmp |= 0x1<<1;
- writel(tmp,tcon);
-
- tmp = readl(tcon);
- tmp &= ~(0x1<<1);
- writel(tmp,tcon);
-
- return 0;
- }
硬件操作,为应用层操作启动PWM,停止PWM提供方法。
- int buzzer_on(void)
- {
- u32 tmp;
- printk("buzzer_on\n");
- tmp = readl(tcon);
- tmp |= 0x1;
- writel(tmp,tcon);
- return 0;
- }
- int buzzer_off(void)
- {
- u32 tmp;
- printk("buzzer_off\n");
-
- tmp = readl(tcon);
- tmp &= ~0x1;
- writel(tmp,tcon);
- return 0;
- }
当应用层调用IO操作时就会调用到我们写好的底层驱动代码,实现相应的命令操作。
- #define BUZZER_ON _IO('B',1)//命令码
- #define BUZZER_OFF _IO('B',2)
-
-
- static struct file_operations myops={
- .unlocked_ioctl = my_ioctl,
- };
-
-
- long my_ioctl(struct file *pf, unsigned int cmd, unsigned long args)
- {
- switch (cmd)
- {
- case BUZZER_ON: buzzer_on(); break;
- case BUZZER_OFF: buzzer_off();break;
- default: return -1;
- }
-
- return 0;
- }