学习内容
本文首先介绍了ZYNQ的定时器的相关内容,并学习使用ZYNQ芯片中的定时器进行操作测试。
开发环境
vivado 18.3&SDK,PYNQ-Z2开发板。
定时器简介
介绍
ZYNQ有两个Cortex-A9处理器,每个Cortex-A9处理器都有自己的专用32位计时器和32位看门狗计时器。 两个都处理器共享一个全局64位计时器。 这些计时器始终以1/2的CPU频率计时(CPU_3x2x)。
在系统层级,有一个24位看门狗定时器和两个16位三重定时器/计数器。系统看门狗定时器的时钟频率为CPU频率(CPU_1x)的1/4或1/6,时钟也可以由来自MIO引脚或PL的外部信号。两个三重计时器/计数器时钟频率为在CPU频率(CPU_1x)的1/4或1/6,可以用于计算来自MIO引脚或来自PL的脉冲宽度。
系统框图
下图为ZYNQ内部的定时器的系统框图。
在图中显示,定时器部分包括系统看门狗、CPU内部的看门狗定时器,CPU私有的定时器,全局定时器,三重定时器。其中所有的定时器都可以触发中断控制器进行中断控制的操作,看门狗定时器(无论是系统 的还是CPU内部的)都可以对系统进行一个复位操作。
CPU私有定时器和看门狗定时器
关于CPU私有定时器和看门狗定时器均具有以下功能:
- 32位计数器,在达到零时会产生中断
- 八位预分频器,可以更好地控制中断周期
- 可配置的单次或自动重装模式
- 计数器的可配置起始值
- 定时器和看门狗复位信号发送到PS复位子系统
- 所有私有计时器和看门狗计时器始终以CPU频率(CPU_3x2x)的1/2计时。
下图为CPU私有定时器和看门狗定时器寄存器表:
全局定时器(GT)
全局定时器是具有自动递增功能的64位递增计数器。 全局计时器在与专用计时器相同的地址空间中映射到内存中。 全局计时器仅在复位时以安全状态访问。 所有Cortex-A9处理器均可访问全局计时器。 每个Cortex-A9处理器都有一个64位比较器,用于在全局计时器达到比较器值时声明一个私有中断。全局定时器始终以CPU频率(CPU_3x2x)的1/2计时。
全局定时器(GT)寄存器功能表如下图:
系统看门狗定时器(SWDT)
除了两个CPU私有看门狗定时器之外,还有一个系统看门狗定时器(SWDT),可以在系统发生故障时,如PS PLL锁相失败,此时看门狗可以产生一个复位信号让程序重启,从而保证系统正常运行。 与CPU私有看门狗定时器不同的是,SWDT的时钟可以来自于外部设备或者PL端,用于提供一个复位信号输出到PL端口或者外部设备。
特征
SWDT的主要功能如下:
- 内部24位计数器。
- 可选时钟输入,时钟信号可以来自:1. 内部PS总线时钟(CPU_1x);2. 内部时钟(来自PL);3. 外部时钟(来自MIO)。
- 计时超时时,可以进行系统中断(PS)和系统重置(PS,PL,MIO)。
- 可编程超时时间:1. 超时范围32,760至68,719,476,736个时钟周期(在100 MHz时可配置范围为330 µs至687.2s)。
- 超时时,可编程的输出信号持续时间:系统中断脉冲(4、8、16或32个时钟周期(CPU_1x时钟))。
系统看门狗定时器寄存器功能表如图:
编程指南
系统看门狗定时器使能顺序如下:
1、使用slcr.WDT_CLK_SEL [SEL]位选择时钟输入源 :确保禁用SWDT(swdt.MODE [WDEN] = 0),并且将时钟输入源设置为选定的正在运行,然后继续此步骤。 更改时钟输入源时,启用SWDT会导致无法预测的行为。 时钟输入源更改为非运行时钟导致APB访问挂起。
2、设置超时时间(计数器控制寄存器) :swdt.CONTROL [CKEY]字段必须为0x248,才能写入此寄存器。
3、启用计数器; 使能输出脉冲 ; 设置输出脉冲长度(零模式寄存器):swdt.MODE [ZKEY]字段必须为0xABC,才能写入此寄存器。 确保IRQLN符合指定的最小值。
4、要使用其他设置运行SWDT,请首先禁用定时器(swdt.MODE [ZKEY]位)。 然后重复上述步骤。
三重计时器(TTC)
TTC包含三个独立的计时器/计数器。 PS端中有两个TTC模块,总共六个计时器/计数器。 可以使用nic301_addr_region_ctrl_registers.security_apb [ttc1_apb]寄存器位将TTC 1控制器配置为安全或非安全模式。 TTC控制器中的三个计时器具有相同的安全状态。
特征
每个三重计时器/计数器都具有以下特征:
- 三个独立的16位预分频器和16位向上/向下计数器。
- 可选时钟输入,来自:1. 内部PS总线时钟(CPU_1x);2. 内部时钟(来自PL);3. 外部时钟(来自MIO)。
•每个计数器有一个中断。
•可以产生溢出中断,定时中断或计数中断,可编程初始值。
•可以生成通过MIO到PL的波形输出(例如PWM)。
计数器编程启用顺序
- 选择时钟输入源,设置预分频值。(slcr.MIO_MUX_SEL寄存器,TTC时钟控制)在继续执行此操作之前,请确保已禁用TTC(ttc.Counter_Control_x [DIS] = 1)。
- 设置间隔值(间隔寄存器)。 此步仅适用于间隔模式。(可选不配置)
- 设置匹配值(匹配寄存器)。 如果要启用匹配,可以配置此操作。(可选不配置)
- **使能中断(中断使能寄存器)。**如果要启用中断,可以配置此操作。(可选不配置)
- 启用/禁用波形输出,启用/禁用匹配,设置计数方向,设置模式,使能计数器(TTC计数器控制寄存器)。 此步骤完成后将启动计数器。
计数器编程停止顺序
- 读回计数器控制寄存器的值。
- 将DIS位设置为1,同时保留其他位。
- 写回计数器控制寄存器。
计数器编程重启顺序
- 读回计数器控制寄存器的值。
- 将RST位设置为1,同时保留其他位。
- 写回计数器控制寄存器。
事件计时器启用序列
- 选择外部脉冲源(slcr.MIO_MUX_SEL寄存器)。选定外部待测脉冲。
- 设置溢出处理,选择外部脉冲电平,启用事件计时器(事件控制计时器寄存器)。 此步骤开始测量外部所选电平(高或低)的宽度脉冲。
- 使能中断(中断使能寄存器)。 如果要启用中断,可以配置此操作。(可选不配置)
- 读取测量的宽度(事件寄存器)。
中断清除和应答序列
- 读取中断寄存器:读取时清除中断寄存器中的所有位。
系统框图
本次工程系统框图如下图所示。
调用UART、TIMER、GPIO资源进行开发设计,UART串口引脚直接连接到MIO,GPIO引脚连接到PL端的EMIO引脚,由ARM核进行配置定时器中断操作,实现定时中断进行流水灯操作。
硬件平台搭建
新建工程,创建 block design。添加ZYNQ7 ip,根据本次工程需要对IP进行配置。勾选本次工程使用的资源。
硬件系统构建完成如下:
然后我们进行generate output product 然后生成HDL封装。这里用到了UART和GPIO,所以需要进行管脚分配。这里使用的是PYNQZ2开发板,该板卡可直接引用以下约束文件:
#LEDs set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports { EMIO_LED_tri_io[0] }]; #IO_L6N_T0_VREF_34 Sch=led[0] set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports { EMIO_LED_tri_io[1] }]; #IO_L6P_T0_34 Sch=led[1] set_property -dict { PACKAGE_PIN N16 IOSTANDARD LVCMOS33 } [get_ports { EMIO_LED_tri_io[2] }]; #IO_L21N_T3_DQS_AD14N_35 Sch=led[2] set_property -dict { PACKAGE_PIN M14 IOSTANDARD LVCMOS33 } [get_ports { EMIO_LED_tri_io[3] }]; #IO_L23P_T3_35 Sch=led[3]
完成约束后,进行综合和布局布线,生成bit流,然后点击导出硬件资源(包含bit流文件),接着launch SDK。
SDK软件部分
打开SDK后,新建application project。
在system.mss中可以打开相关参考文档辅助设计。
可以选择timer中断的例程进行参考设计,导入uart_intr_example例程模板,
在main.c中输入以下代码:
#include <stdio.h> #include "xparameters.h" #include "xscutimer.h" #include "xscugic.h" #include "xgpiops.h" #include "xil_exception.h" #include "xil_printf.h" //声明定义 #define GPIO_ID XPAR_PS7_GPIO_0_DEVICE_ID #define TIMER_ID XPAR_PS7_SCUTIMER_0_DEVICE_ID #define INTR_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID #define TIMER_IRPT_INTR XPAR_SCUTIMER_INTR #define LOAD_VALUE 0X9AF8D9F//0.5s流水 系统时钟650M 定时器时钟325M 周期3ns #define LED0 54 #define LED1 55 #define LED2 56 #define LED3 57 //声明示例结构体 XGpioPs GpioPs; XScuTimer Timer; XScuGic ScuGic; //函数定义 void Emio_init(); void Timer_init(); void Timer_intr_init(XScuGic *intr, XScuTimer *time); void Timer_IntrHandler(void *CallBackRef); int main(){ //EMIO初始化 Emio_init(); //初始化定时器 Timer_init(); //初始化中断 Timer_intr_init(&ScuGic,&Timer); //启动定时器 XScuTimer_Start(&Timer); while(1); return 0; } void Emio_init(){ XGpioPs_Config *XGpioPs_Cfg; XGpioPs_Cfg = XGpioPs_LookupConfig(GPIO_ID); XGpioPs_CfgInitialize(&GpioPs,XGpioPs_Cfg,XGpioPs_Cfg->BaseAddr); XGpioPs_SetDirectionPin(&GpioPs,LED0,0x01); XGpioPs_SetOutputEnablePin(&GpioPs,LED0,0x01); XGpioPs_SetDirectionPin(&GpioPs,LED1,0x01); XGpioPs_SetOutputEnablePin(&GpioPs,LED1,0x01); XGpioPs_SetDirectionPin(&GpioPs,LED2,0x01); XGpioPs_SetOutputEnablePin(&GpioPs,LED2,0x01); XGpioPs_SetDirectionPin(&GpioPs,LED3,0x01); XGpioPs_SetOutputEnablePin(&GpioPs,LED3,0x01); } void Timer_init(){ int Status; XScuTimer_Config *ConfigPtr; //初始化定时器 ConfigPtr = XScuTimer_LookupConfig(TIMER_ID); XScuTimer_CfgInitialize(&Timer, ConfigPtr,ConfigPtr->BaseAddr); //定时器自检测 Status = XScuTimer_SelfTest(&Timer); if (Status != XST_SUCCESS) { printf("timer selftest failed!\n"); } printf("timer selftest success!\n"); //装载初值 XScuTimer_LoadTimer(&Timer, LOAD_VALUE); //使能自动装载模式 XScuTimer_EnableAutoReload(&Timer); } void Timer_intr_init(XScuGic *intr, XScuTimer *time){ XScuGic_Config *IntcConfig; //初始化定时器中断 IntcConfig = XScuGic_LookupConfig(INTR_DEVICE_ID); XScuGic_CfgInitialize(intr,IntcConfig,IntcConfig->CpuBaseAddress); //初始化中断异常函数 Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, intr); Xil_ExceptionEnable(); //设置中断服务函数 XScuGic_Connect(intr, TIMER_IRPT_INTR, (Xil_ExceptionHandler)Timer_IntrHandler, (void *)time); //使能中断控制器 XScuGic_Enable(intr, TIMER_IRPT_INTR); //使能定时器 XScuTimer_EnableInterrupt(time); } //中断处理函数 void Timer_IntrHandler(void *CallBackRef){ static char led_status=0x01; XScuTimer *TimerInstancePtr = (XScuTimer *) CallBackRef; if(XScuTimer_IsExpired(TimerInstancePtr)){ led_status = led_status<<1; if(led_status == 0X10) led_status =0x01; XGpioPs_WritePin(&GpioPs,LED0,led_status); XGpioPs_WritePin(&GpioPs,LED1,led_status>>1); XGpioPs_WritePin(&GpioPs,LED2,led_status>>2); XGpioPs_WritePin(&GpioPs,LED3,led_status>>3); XScuTimer_ClearInterruptStatus(TimerInstancePtr); } }
部分代码讲解
在主函数中引用了Emio_init();
、Timer_init();
、Timer_intr_init(&ScuGic,&Timer);
分别完成初始化GPIO操作,初始化定时器,初始化中断定时器等操作。最后用XScuTimer_Start(&Timer);
启动定时器。
对于定时器初始化设置主要要对计数模式进行设置,对初始值进行装载配置
也就是调用下面的函数:
//装载初值 XScuTimer_LoadTimer(&Timer, LOAD_VALUE); //使能自动装载模式 XScuTimer_EnableAutoReload(&Timer);
这里LOAD_VALUE表示需要计时或者计数的次数,因为这里的代码实现的是led的0.5s定时中断,系统CPU时钟为650M,所以定时器的时钟频率为325M,也就是周期为3.07ns,计算0.5s下的计数次数得到这里需要装载的初值。(也即为0X9AF8D9F)
Reference
- UG585
- 正点原子ZYNQ开发视频