本文系ZED-Board从入门到精通(三):从传统ARM开发到PS开发的转变之后增加的PS例程。由于原文较长,在原帖后面添加例程会使阅读不便,于是单独开一帖。
实际项目中几乎离不开时间的测量。定时器是硬件系统运行状态的忠实记录者,它不受CPU直接干预,自己独立运行,可以完成计时、定时、中断、实时时钟等功能。
ARM Cortex-A9内部有一个64bit全局定时器,特性包括:
64bit,增计数;
内存映射至私有内存空间;
只有复位后,在安全模式下才能访问;
可被所有Cortex-A9核访问,每个核有私有比较器;
时钟源为PERIPHCLK;
定时器的精度是由其时钟源决定的,而时钟源来自ARM系统时钟。我们先来看一下硬件系统时钟分配情况,
系统PS_CLK为板上的晶振输入,频率为33.3333MHz
PS-CLK进入芯片后,又做如下分配(摘自Zynq-7000-TRM):
可见经过了3个PLL,最终生成的系统时钟有cpu_6x4x,cpu_3x2x,cpu_2x,cpu_1x。具体的系统时钟频率值我们可以查看XPS中的时钟选项,这里不再详述,只要知道全局定时器的输入时钟为cpu_3x2x,它的频率为CPU时钟的一半(333.333MHz),定时精度为3ns,又由于其具有64bit范围,最大定时值可达3e34s。
操作定时器需要访问其对应寄存器,我们看一下TRM中的描述:
这里只给出了基地址,具体寄存器的分布需要查看ARM文档cortex_a9_mpcore_r4p1_trm:
其中前两个为定时器的计数值存放寄存器,两个32bit凑成一个64bit实现连续增计数。
第三个寄存器为控制寄存器,位定义如下:
我们需要关注的是最低位(b0),即定时器使能位,该位为0时,定时器停止,这时可以读写计数值;而该位为1时,定时器运行,不能写入计数值(只能读出)。
其它的寄存器我们暂时不用,不加解释。需要的话可以自己翻一翻手册。
相比基于操作系统的软件计时器,我们采用硬件计时器具有非常高的精度,可以精确到ns级别!对于非常窄的脉冲,我们照样可以通过计时器完成其脉宽测量。程序中有时需要精确延时(例如红外通信,DS18b20单总线读写),我们先写一个精确延时1s的函数,然后把它用在我们第一个流水灯实验中。本节例程仍基于第一个例程进行,硬件部分不需要改动,只需要改软件,打开helloworld.c,将内容改为:
/* * Copyright (c) 2009 Xilinx, Inc. All rights reserved. * * Xilinx, Inc. * XILINX IS PROVIDING THIS DESIGN, CODE, OR INFORMATION "AS IS" AS A * COURTESY TO YOU. BY PROVIDING THIS DESIGN, CODE, OR INFORMATION AS * ONE POSSIBLE IMPLEMENTATION OF THIS FEATURE, APPLICATION OR * STANDARD, XILINX IS MAKING NO REPRESENTATION THAT THIS IMPLEMENTATION * IS FREE FROM ANY CLAIMS OF INFRINGEMENT, AND YOU ARE RESPONSIBLE * FOR OBTAINING ANY RIGHTS YOU MAY REQUIRE FOR YOUR IMPLEMENTATION. * XILINX EXPRESSLY DISCLAIMS ANY WARRANTY WHATSOEVER WITH RESPECT TO * THE ADEQUACY OF THE IMPLEMENTATION, INCLUDING BUT NOT LIMITED TO * ANY WARRANTIES OR REPRESENTATIONS THAT THIS IMPLEMENTATION IS FREE * FROM CLAIMS OF INFRINGEMENT, IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE. * */ /* * helloworld.c: simple test application */ #include <stdio.h> #include "platform.h" #define MIO_BASE 0xE000A000 //MIO基地址 #define DATA1_RO 0x64 #define DATA2 0x48 #define DATA2_RO 0x68 #define DIRM_2 0x284 #define OEN_2 0x288 #define GTC_BASE 0xF8F00200 //Global Timer基地址 #define GTC_CTRL 0x08 //控制寄存器偏移量 #define GTC_DATL 0x00 //数据寄存器(低32bit) #define GTC_DATH 0x04 //数据寄存器(高32bit) #define CLK_3x2x 333333333 //定时器输入时钟频率 void print(char *str); void delay_1s(int t) //t无实际意义 { int i = CLK_3x2x,j; *((volatile int*)(GTC_BASE+GTC_CTRL)) = 0x00; //清零定时器使能位,定时器停止 *((volatile int*)(GTC_BASE+GTC_DATL)) = 0x00000000; //写入计数值(低32bit) *((volatile int*)(GTC_BASE+GTC_DATH)) = 0x00000000; //写入计数值(高32bit) *((volatile int*)(GTC_BASE+GTC_CTRL)) = 0x01; //开启定时器 do { j=*((volatile int*)(GTC_BASE+GTC_DATL)); } while(j<i); //判断是否计时够1s? } void print(char *str); int main() { int i; init_platform(); *((volatile int*)(MIO_BASE+OEN_2)) = 0xff; *((volatile int*)(MIO_BASE+DIRM_2)) = 0xff; print("Hello world!\r\nThe Leds are flowing...\r\n"); while(1) { for(i = 0;i < 8; i++) { *((volatile int*)(MIO_BASE+DATA2)) = 0x01<<i; delay_1s(1000); } } cleanup_platform(); return 0; }
上面例子中,将原来的delay_1s改成了利用64bit全局定时器实现的精确定时(虽然这样做有点浪费,呵呵)。
运行结果仍为流水灯,灯移一位的时间应该是标准的1s。
我们可以通过简单的编程,实现对程序性能的监测,例如在运行算法程序之前,先开启计时器,等算法程序结束,再停止计时,读取计时器的计数值从而计算算法运行时间,这样可以评估算法性能。这个功能有点像Matlab里面的tic,toc,为了方便程序编写,我们也如此定义函数:
#define GTC_BASE 0xF8F00200 #define GTC_CTRL 0x08 #define GTC_DATL 0x00 #define GTC_DATH 0x04 #define CLK_3x2x 333333333 void tic(void) { *((volatile int*)(GTC_BASE+GTC_CTRL)) = 0x00; *((volatile int*)(GTC_BASE+GTC_DATL)) = 0x00000000; *((volatile int*)(GTC_BASE+GTC_DATH)) = 0x00000000; //清零定时器的计数值 *((volatile int*)(GTC_BASE+GTC_CTRL)) = 0x01; } double toc(void) { *((volatile int*)(GTC_BASE+GTC_CTRL)) = 0x00; long long j=*((volatile int*)(GTC_BASE+GTC_DATH)); double elapsed_time = j<<32; j=*((volatile int*)(GTC_BASE+GTC_DATL)); //读取64bit定时器值,转换为double elapsed_time+=j; elapsed_time/=CLK_3x2x; elapsed_time*=1000; printf("Elapsed time is %f ms.\r\n",elapsed_time); return elapsed_time; }
调用时非常简单:
tic(); my_algorithm(); toc();
运行时,程序输出和matlab完全一致。这里使用硬件计时,精度可以达到ns级别,具有普通软件计时无法比拟的特性,对于非常窄的脉冲,我们照样可以用上面的方法测量其脉宽。
通过本节定时器的例子,相信童鞋们对PS开发有种驾轻就熟的感觉。没错,真正基于Zynq的PS开发流程就是如此,首先查阅文档,知道硬件寄存器定义,然后按照说明进行底层软件编写,并为上层程序提供较为简洁和直观的接口。掌握了这个技巧,后面进行PS与PL协同开发时,只要根据PL相应内存映射地址和寄存器定义,就可以完成PS端控制软件的设计,从而为后面进一步编写基于操作系统的驱动程序打下坚实的基础。
大家可以读完本文后,进一步利用官方文档,熟悉一下PS的其他外设操作。