1.九鼎提供的在x210-buzzer.c中,但是没有默认集成到内核中,make menuconfig选中,九鼎提供的Makefile 和 Kconfig不符合,有bug,Kconfig中的是X210_BUZZER_DRIVER, Makefile中的是
CONFIG_BUZZER_DRIVER,修改Makefile为CONFIG_X210_BUZZER_DRIVER。
2.注意Kconfig和Makefile中的对应关系。
Kconfig里面定义的项,例如:X210_BUZZER_DRIVER
在Makefile中,obj-$(CONFIG_X210_BUZZER_DRIVER) += x210-buzzer.o
都是在Kconfig项的前面加CONFIG_来代表这个项。make menuconfig的配置体系能用图像化的界面来配置这个项为Y N M。而且Makefile中的
CONFIG_X210_BUZZER_DRIVER这个最终会记录到内核根目录的.config文件中。
3.ls /dev/buzzer -l,可以看到Major = 10,Minor = 61.
写一个应用程序来操作buzzer
代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define PWM_IOCTL_SET_FREQ 1
#define PWM_IOCTL_STOP 0
#define PATHNAME “/dev/buzzer”
int main(void)
{
int fd = -1;
fd = open(PATHNAME, O_RDWR);
if (fd <0)
{
perror("");
return -1;
}
ioctl(fd, PWM_IOCTL_SET_FREQ, 2000);
sleep(3);
ioctl(fd, PWM_IOCTL_SET_FREQ, 3000);
sleep(3);
ioctl(fd, PWM_IOCTL_SET_FREQ, 4000);
sleep(3);
ioctl(fd, PWM_IOCTL_SET_FREQ, 5000);
sleep(3);
ioctl(fd, PWM_IOCTL_SET_FREQ, 6000);
sleep(3);
ioctl(fd, PWM_IOCTL_SET_FREQ, 7000);
sleep(3);
ioctl(fd, PWM_IOCTL_STOP);
close(fd);
return 0;
}
misc类设备 – 杂散类设备
1.中文名:杂项设备\杂散设备
2.类名和位置:/sys/class/misc
3.典型的字符设备
4.有一套驱动框架,内核实现一部分(misc.c),驱动实现一部分(x210-buzzer.c)
5.misc是对原始的字符设备注册接口的一个类层次的封装,很多典型字符设备都可以归类到misc类中,使用misc驱动框架来管理。
misc驱动框架源码分析
1.内核开发者实现部分,关键点有2个:一个是类的创建,另一个是开放给驱动开发者的接口
2.misc源码框架本身也是一个模块,内核启动时自动加载
subsys_initcall(misc_init); misc_init是这个模块的初始化函数。
3.源码框架的主要工作:注册misc类,使用老接口注册字符设备驱动(主设备号10),开放device注册的接口misc_register给驱动工程师
4.驱动工程师需要借助misc来加载自己的驱动时,只需要调用misc_register接口注册自己的设备即可
填充这个结构体:
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const char *nodename;
mode_t mode;
};
然后调用:misc_register函数注册即可。
5.misc_list链表的作用。内核定义了一个misc_list链表用来记录所有内核中注册了的杂散类设备。当我们向内核注册一个misc类设备时,内核就会向misc_list链表中insert一个节点
static LIST_HEAD(misc_list);;定义一个全局的头节点。
驱动框架中misc_register函数中相关重点类容分析:
1.INIT_LIST_HEAD(&misc->list);
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
初始化list_head 节点。
2.static DEFINE_MUTEX(misc_mtx);定义了一个全局的互斥锁
mutex_lock互斥锁上锁。
mutex_unlock互斥锁解锁。
内核防止竞争状态的手段:原子访问、自旋锁、互斥锁、信号量。
原子访问主要用来做计数、自旋锁之后分析、互斥锁和信号量很相似(其实就是计数值为1的信号量),互斥锁的出现比信号量晚,实现上比信号量优秀,尽量使用互斥锁
3.list_for_each_entry(c, &misc_list, list)分析:
struct miscdevice *c;一个局部变量
static LIST_HEAD(misc_list);;定义一个全局的头节点,这个头节点会把所有的miscdevice 结构体用list_head 串起来。
list:list_head类型在miscdevice 结构体中的名字。
作用:遍历misc_list链表中所有的miscdevice 对象
接着看:
#define list_for_each_entry(pos, head, member)
for (pos = list_entry((head)->next, typeof(*pos), member);
prefetch(pos->member.next), &pos->member != (head);
pos = list_entry(pos->member.next, typeof(*pos), member))
#define list_entry(ptr, type, member)
container_of(ptr, type, member)
这里实现的起始就是一个for循环,去遍历以misc_list为头节点的内核链表上的miscdevice 对象。
三个参数:
参数1:存放遍历到的对象的指针
参数2:头节点
参数3:list_head在对象结构体中的命名。
4.device_create,创建class下面的device
5.list_add(&misc->list, &misc_list);把每一个杂项类设备都用list_head串起来。
6.驱动框架里面实现的open等函数,最终都会调用到我们自己写的驱动的open等函数中。
蜂鸣器驱动源码分析
先看原理图:
我们是有源蜂鸣器,输入高电平就会响,低电平就不会响。
1.static struct semaphore lock;定义一个信号量
init_MUTEX(&lock);初始化信号量
2.miscdevice 结构体定义
#define DEVICE_NAME “buzzer”
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR, 动态分配次设备号
.name = DEVICE_NAME,
.fops = &dev_fops,
};
3.misc_register注册杂项类设备
4.gpio_request(S5PV210_GPD0(2), “GPD0”);申请gpio的GPD0_2
5.s3c_gpio_setpull(S5PV210_GPD0(2), S3C_GPIO_PULL_UP);设置内部上拉
s3c_gpio_cfgpin(S5PV210_GPD0(2), S3C_GPIO_SFN(1));设置为输出模式
gpio_set_value(S5PV210_GPD0(2), 0);设置输出值为0
分析一下这个函数:
s3c_gpio_setpull
调用:
s3c_gpio_do_setpull
调用:
return (chip->config->set_pull)(chip, off, pull);
赋值在s5pv210_gpiolib_init函数中:
static struct s3c_gpio_cfg gpio_cfg = {
.set_config = s3c_gpio_setcfg_s3c64xx_4bit,
.set_pull = s3c_gpio_setpull_updown,
.get_pull = s3c_gpio_getpull_updown,
};
for (i = 0; i < nr_chips; i++, chip++) {
if (chip->config == NULL)
chip->config = &gpio_cfg;
if (chip->base == NULL)
chip->base = S5PV210_BANK_BASE(i);
}
6.杂项类的注销函数是:misc_deregister
7.file_operations 结构体
static struct file_operations dev_fops = {
.owner = THIS_MODULE,
.open = x210_pwm_open,
.release = x210_pwm_close,
.ioctl = x210_pwm_ioctl,
};
x210_pwm_open:信号量-1,这个信号量再无法被别的进程使用
x210_pwm_close:信号量+1,这个信号量又可以被别的进程使用
所以这里实现的就是只能被一个进程open这个设备节点。
重点分析:x210_pwm_ioctl
static int x210_pwm_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
switch (cmd)
{
case PWM_IOCTL_SET_FREQ:
printk(“PWM_IOCTL_SET_FREQ:\r\n”);
if (arg == 0)
return -EINVAL;
PWM_Set_Freq(arg);
break;
case PWM_IOCTL_STOP:
default:
printk("PWM_IOCTL_STOP:\r\n");
PWM_Stop();
break;
}
return 0;
}
在应用层使用ioctl就会调用到这个函数:
例如:ioctl(fd, PWM_IOCTL_SET_FREQ, 2000);就会执行:PWM_Set_Freq函数。
ioctl(fd, PWM_IOCTL_STOP);就会执行,PWM_Stop函数。
重点分析:PWM_Set_Freq
static void PWM_Set_Freq( unsigned long freq )
{
unsigned long tcon;
unsigned long tcnt;
unsigned long tcfg1;
struct clk *clk_p;
unsigned long pclk;
//unsigned tmp;
//设置GPD0_2为PWM输出
s3c_gpio_cfgpin(S5PV210_GPD0(2), S3C_GPIO_SFN(2));
tcon = __raw_readl(S3C2410_TCON); // TCON寄存器值的读取
tcfg1 = __raw_readl(S3C2410_TCFG1); //TCFG1寄存器值的读取
//mux = 1/16
tcfg1 &= ~(0xf<<8);
tcfg1 |= (0x4<<8);
__raw_writel(tcfg1, S3C2410_TCFG1);
clk_p = clk_get(NULL, "pclk");
pclk = clk_get_rate(clk_p);
tcnt = (pclk/16/16)/freq;
__raw_writel(tcnt, S3C2410_TCNTB(2));
__raw_writel(tcnt/2, S3C2410_TCMPB(2));//占空比为50%
tcon &= ~(0xf<<12);
tcon |= (0xb<<12); //disable deadzone, auto-reload,
inv-off, update TCNTB0&TCMPB0, start timer 0
__raw_writel(tcon, S3C2410_TCON);
tcon &= ~(2<<12); //clear manual update bit
__raw_writel(tcon, S3C2410_TCON);
}
1.首先设置GPD0_2为PWM输出
2.tcon = __raw_readl(S3C2410_TCON); TCON寄存器值的读取
tcfg1 = __raw_readl(S3C2410_TCFG1); //TCFG1寄存器值的读取
#define S3C2410_TCON S3C_TIMERREG(0x08)
调用:
#define S3C_TIMERREG(x) (S3C_VA_TIMER + (x))
调用:
#define S3C_VA_TIMER S3C_ADDR(0x00300000) /* timer block */
这里我突然有个疑问?我们的S3C_VA_TIMER怎么就是0xFD300000开始的值?它的物理地址开始是:0xE2500000
突然想到我们的静态映射表:/arch/arm/plat-s5p/cpu.c中有定义:
static struct map_desc s5p_iodesc[] __initdata = {
{
.virtual = (unsigned long)S5P_VA_CHIPID,
.pfn = __phys_to_pfn(S5P_PA_CHIPID),
.length = SZ_4K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)S3C_VA_SYS,
.pfn = __phys_to_pfn(S5P_PA_SYSCON),
.length = SZ_64K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)S3C_VA_UART,
.pfn = __phys_to_pfn(S3C_PA_UART),
.length = SZ_4K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)VA_VIC0,
.pfn = __phys_to_pfn(S5P_PA_VIC0),
.length = SZ_16K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)VA_VIC1,
.pfn = __phys_to_pfn(S5P_PA_VIC1),
.length = SZ_16K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)S3C_VA_TIMER,
.pfn = __phys_to_pfn(S5P_PA_TIMER),
.length = SZ_16K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)S5P_VA_GPIO,
.pfn = __phys_to_pfn(S5P_PA_GPIO),
.length = SZ_4K,
.type = MT_DEVICE,
},
};
静态映射表中做了映射:
#define S5PV210_PA_TIMER (0xE2500000)
#define S5P_PA_TIMER S5PV210_PA_TIMER
对应了:
#define S3C_VA_TIMER S3C_ADDR(0x00300000) /* timer block */
3.往TCFG1寄存器中写了:0x4,也就是往PWM2的MUX里面写了0x4,就会实现1/16的分频,如下面红色方框。
4.时钟获取
clk_p = clk_get(NULL, “pclk”);//不太清楚是在哪里去初始化的整个时钟
pclk = clk_get_rate(clk_p); //66MHZ
5.TCFG0在Uboot中设置,这里不再重复设置。主要设置预分频器为16
6.TIMER2的输出频率:pclk/(prescaler1+1)/MUX1 =
66MHZ / 16 / 16,所以意味着tcnt里面的值的减少频率是:f = 66MHZ / 16 / 16,t1 = 1 / f。
我们要设置的频率是fre,所以对应的t2 = 1 / fre。
所以tcnt = t1 / t2 = f / fre = 66MHZ / 16 /16 /fre
5.S5PV210时钟的设置是在哪里?
arch/arm/mach-s5pv210/cpu-freq.c中的clk_info数组。
我们使用到的应该是第一组,pclk = 66MHZ。
clk_get这个函数在arch/arm/plat-samsung/clock.c中定义的。
clk_get_rate这个函数根据时钟源来获取时钟的速率,例如这里获取的就是
66000000HZ
6.写TCNT2寄存器和写TCMP2寄存器,前面的是控制周期的,后面的寄存器是控制占空比的。
__raw_writel(tcnt, S3C2410_TCNTB(2));
__raw_writel(tcnt/2, S3C2410_TCMPB(2));//占空比为50%
7.写CON寄存器
tcon &= ~(0xf<<12);
tcon |= (0xb<<12); //disable deadzone, auto-reload, inv-off, update TCNTB0&TCMPB0, start timer 0
__raw_writel(tcon, S3C2410_TCON);
先往CON寄存器里面写了:1011,分别对应下图。
bit15:打开自动reload的功能,这样就可以实现循环产生波形
bit14:死区,一般都是直接关闭死区
bit13:手动更新TCNTB2、TCMPB2功能,我们要先手动刷新,第一次需要手工将TCNTB中的值刷新到TCNT中去,以后就可以auto-reload了
bit12:打开timer
tcon &= ~(2<<12); //clear manual update bit
__raw_writel(tcon, S3C2410_TCON);
~((0b10<<12) ) = 0b01 << 12,清了bit13.,关闭手动刷新,以后就是自动将TCNTB中的值刷新到TCNT中去了
8.__raw_writel这个函数使用:
第一个参数:值
第二个参数:寄存器地址
那么这个函数就弄明白了,最不清楚的,就是不知道我们linux在哪里初始化的我们的时钟部分。
9.我们看一下stop函数,直接将引脚设置为了输入模式…
void PWM_Stop( void )
{
//将GPD0_2设置为input
s3c_gpio_cfgpin(S5PV210_GPD0(2), S3C_GPIO_SFN(0));
}
10.register_chrdev
第一个参数:MAJOR
第二个参数:name
第三个参数:fops
这个函数注册的字符设备驱动,需要去mknod生成设备节点,例如
mknod /dev/xxx c 61 0,mknod /dev/xxx c 61 2,可以去创建多个设备节点。
10.自动创建设备节点,device_create创建字符设备节点的时候,需要一个设备号(主+次)