最近看到Xilinx 1588相关的驱动,简单的记录一下。
从MPsoc平台的demo中,设备树相关的节点是这个样子的。
timer_1588_v2_0@a0080000 {
clock-names = "systemtimer_clk", "s_axi_aclk";
clocks = <0x46 0x43>;
compatible = "xlnx,timer-1588-2.0";
interrupt-names = "interrupt";
interrupt-parent = <0x4>;
interrupts = <0x0 0x5c 0x4>;
reg = <0x0 0xa0080000 0x0 0x10000>;
xlnx,period = <0x3d0900>;
};
奇怪的是,我在linux-xlnx-xilinx-v2019.1分支中并没有找到相关的驱动。于是我在Xilinx的github上找到了对应驱动。这里就对这个驱动简单的分析一下,看看是什么样的原理。
首先还是那个套路,在驱动中,使用module_platform_driver(xlnx_ptp_timer_driver);注册了平台驱动。
xlnx-ptp-timer.c中, timer_1588_of_match[]与设备树compatible属性匹配,所以平台驱动xlnx_ptp_timer_driver的探针会被内核枚举。
r_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!r_mem) {
dev_err(&pdev->dev, "no IO resource defined\n");
return -ENXIO;
}
timer->baseaddr = devm_ioremap_resource(&pdev->dev, r_mem);
if (IS_ERR(timer->baseaddr)) {
err = PTR_ERR(timer->baseaddr);
return err;
}
在probe中,驱动获取了内存资源并做了相应的映射关系。所以这里, timer->baseaddr是0xa0080000的映射。timer->irq = 0x5c,触发类型为高电平。驱动中还固定写死了timer->period = 4000000;所以设备树中传过来的xlnx,period并没有什么卵用。
关键来了
timer->ptp_clock_info = xlnx_ptp_clock_info;
timer->ptp_clock = ptp_clock_register(&timer->ptp_clock_info,
&pdev->dev);
这就是驱动的核心部分,向内核注册了一个ptp时钟设备, 设备的操作信息在xlnx_ptp_clock_info里。里面包含了adjfreq,adjtime, gettime64,settime64等时间操作的接口。
static struct ptp_clock_info xlnx_ptp_clock_info = {
.owner = THIS_MODULE,
.name = "Xilinx Timer",
.max_adj = 999999999,
.n_ext_ts = 0,
.pps = 1,
.adjfreq = xlnx_ptp_adjfreq,
.adjtime = xlnx_ptp_adjtime,
.gettime64 = xlnx_ptp_gettime,
.settime64 = xlnx_ptp_settime,
.enable = xlnx_ptp_enable,
};
最后,就是注册中断啦,刚才从设备树里辛辛苦苦拿过来的中断号要用上,就是向内核申请这个中断。
err = xlnx_ptp_timer_request_irq(timer);
中断的回调服务函数是xlnx_ptp_timer_isr。
static irqreturn_t xlnx_ptp_timer_isr(int irq, void *priv)
{
struct xlnx_ptp_timer *timer = priv;
struct ptp_clock_event event;
event.type = PTP_CLOCK_PPS;
++timer->countpulse;
if (timer->countpulse >= PULSESIN1PPS) {
timer->countpulse = 0;
if ((timer->ptp_clock) && (timer->pps_enable)) {
ptp_clock_event(timer->ptp_clock, &event);
}
}
out:
out_be32((timer->baseaddr + XTIMER1588_INTERRUPT), (1 << XTIMER1588_INT_SHIFT) );
return IRQ_HANDLED;
}
这里就是做了一个计数,然后看看设备有没有被使能,如果被使能了,就使用ptp_clock_event发送一个PTP_CLOCK_PPS事件,这个是内核解耦一贯套路,至于这个事件做了啥,还要分析ptp_clock的核心层。这里我先不去那么深入分析核心层做了什么,毕竟是抽象出来的东西,要用到再去分析。
继续分析xlnx-ptp-timer.c。假设内核已经完成了xlnx-ptp-timer设备驱动的探针操作,就意味着这个驱动可以被使用了。
肯定有人会调用这个驱动的xlnx_ptp_clock_info下的接口。
settime64分析:这个操作看上去比较简单,我就直接从代码上注释了。
static int xlnx_ptp_settime(struct ptp_clock_info *ptp,
const struct timespec64 *ts)
{
struct xlnx_ptp_timer *timer = container_of(ptp, struct xlnx_ptp_timer,
ptp_clock_info);
struct timespec64 delta, tod;
struct timespec64 offset;
unsigned long flags;
// 进入临界状态,禁止内核抢占,关闭中断并保存中断当前状态
spin_lock_irqsave(&timer->reg_lock, flags);
// 对timer的offset进行清0操作
/* First zero the offset */
offset.tv_sec = 0;
offset.tv_nsec = 0;
xlnx_rtc_offset_write(timer, &offset);
// 获取当前时间值,保存在tod中。
/* Get the current timer value */
xlnx_tod_read(timer, &tod);
// 用这个传入的ts时间减去当前时间tod, 得到一个差值delta
/* Subtract the current reported time from our desired time */
delta = timespec64_sub(*ts, tod);
// delta还是不能出现负数。。。。
/* Don't write a negative offset */
if (delta.tv_sec <= 0) {
delta.tv_sec = 0;
if (delta.tv_nsec < 0)
delta.tv_nsec = 0;
}
// 用delta更新offset
xlnx_rtc_offset_write(timer, &delta);
// 退出临界状态,恢复内核抢占,恢复中断和中断状态
spin_unlock_irqrestore(&timer->reg_lock, flags);
return 0;
}
我们重点看看xlnx_rtc_offset_write和xlnx_tod_read做了啥见不得人的勾当。
static void xlnx_rtc_offset_write(struct xlnx_ptp_timer *timer,
const struct timespec *ts)
{
out_be32((timer->baseaddr + XTIMER1588_RTC_OFFSET_SEC_H), (u32) ((ts->tv_sec>>32)&0xFFFFFFFF) );
out_be32((timer->baseaddr + XTIMER1588_RTC_OFFSET_SEC_L), (u32) ((ts->tv_sec )&0xFFFFFFFF) );
out_be32((timer->baseaddr + XTIMER1588_RTC_OFFSET_NS), (u32) ts->tv_nsec );
// printk(KERN_INFO "xlnx_rtc_offset_write:ts.tv_sec:%lld, ts.tv_nsec:%d \n",ts->tv_sec,ts->tv_nsec);
}
static void xlnx_tod_read(struct xlnx_ptp_timer *timer, struct timespec64 *ts)
{
u32 sec, nsec;
nsec = in_be32(timer->baseaddr + XTIMER1588_CURRENT_RTC_NS);
sec = in_be32(timer->baseaddr + XTIMER1588_CURRENT_RTC_SEC_L);
#ifdef XTIMER1588_ENABLE_DEBUG_MESS
printk(KERN_INFO "ptp_xlnx_1588:xlnx_tod_read=%d.%09d\n", sec, nsec);
#endif
ts->tv_sec = 0; // Clear to be safe. Change sec read to 64 above.
ts->tv_sec = (u64) sec; // Linux now uses timespec64 as the function res
ts->tv_nsec = nsec;
}
好像也没干啥,看上去就是读写了某个寄存器而已啊,out_be32这个东西就是写,in_be32就是读,头文件中都有定义。
#define in_be32(offset) __raw_readl(offset)
#define out_be32(offset, val) __raw_writel(val, offset)
那寄存器是哪个?timer->baseaddr是什么?回头看一下,最开始分析的时候,探针操作的时候驱动就从设备树中获取了这个地址,它对应着一个映射后的虚拟地址,这个不用管,只需要知道,timer->baseaddr对应的物理地址是0xa0080000。所以写那个寄存器对应驱动中的宏就一清二楚了。
gettime64分析:这个就更简单了,就是直接读了当前时间寄存器,然后返回时间值。
static int xlnx_ptp_gettime(struct ptp_clock_info *ptp, struct timespec64 *ts)
{
unsigned long flags;
struct xlnx_ptp_timer *timer = container_of(ptp,
struct xlnx_ptp_timer, ptp_clock_info);
spin_lock_irqsave(&timer->reg_lock, flags);
xlnx_tod_read(timer, ts);
spin_unlock_irqrestore(&timer->reg_lock, flags);
#ifdef XTIMER1588_ENABLE_DEBUG_MESS
printk(KERN_INFO "ptp_xlnx_1588:gettime ts.tv_sec:%d, ts.tv_nsec:%d\n",ts->tv_sec,ts->tv_nsec);
#endif
return 0;
}
adjtime分析:核心部分如下,其他都是花哨。
1、// 将传过来的delta转换成timespec结构类型的值
then = ns_to_timespec(delta);
2、// 获取当前的offset值,并把刚才的delta转换之后的值加到当前的offset上
xlnx_rtc_offset_read(timer, &offset);
offset.tv_sec = 0;
offset = timespec_add(offset, then);
3、// 更新offset寄存器
out_be32((timer->baseaddr + XTIMER1588_RTC_OFFSET_SEC_H), (u32) ((offset.tv_sec>>32) & 0xffffffff));
out_be32((timer->baseaddr + XTIMER1588_RTC_OFFSET_SEC_L), (u32) ((offset.tv_sec ) & 0xffffffff));
out_be32((timer->baseaddr + XTIMER1588_RTC_OFFSET_NS), offset.tv_nsec );
这就是Xilinx 1588部分的驱动,寄存器部分和Xilinx的IP核有很大的联系,这些地址的偏移都是根据IP core而确定的。至于PTP部分,应该是统一的接口,有机会看看PTP部分的原理实现。