【ZYNQ】PS端CPU私有定时器产生定时中断


  • Vivado:2024.2
  • Vitis:2024.2
  • ZYNQ芯片:xc7z020clg400-2
  • 开发板:Microphase Z7-Lite 7020版

一、ZYNQ 7000 系列的 PS 私有定时器

Zynq 7000系列PS端的定时器的结构框图如下图所示。 从图中可以看到系统所包含的定时器资源有:系统看门狗定时器(Syetem Watchdog Timer)、每个CPU处理器包含一个32位看门狗和32位的私有定时器、 两个Triple Timer Counter 定时器、一个64位共享定时器。所有定时器都与中断 控制器相连,可以产生中断。

image-20241127141049404

ZYNQ 7000系列的私有定时器内部结构和STM32比较类似,甚至比STM32中的定时器还要简单。私有定时器的时钟频率为CPU时钟频率的一半,这个“一半”的比例是无法修改的,如 ARM 的工作时钟频率为 666.666Mhz(默认), 则私有定时器的时钟频率为 333.333Mhz。私有定时器有一个向下计数的 32 位计数器,计数到0时产生中断信号;8位预分频器(Prescaler),可控制定时器频率;可配置单次计时或自动重载(AutoReload)模式

二、ZYNQ PS 定时器的使用

PL端配置比较简单,直接在BlockDesign中定义一个PS的IP,仅保留DDR3和UART0功能即可。之后生成BD底层,生成HDL顶层,导出.xsa文件(没有PL逻辑不需要生成Bitstream),在Vitis中使用该.xsa文件创建平台部件和一个空的应用部件即可。具体的步骤可以参考我的文章:【Vivado/Vitis 2024.2】工程的组织和创建丨使用PS MIO读写GPIO

PS端私有定时器的相关库函数在xscutimer.h文件中,文件对库函数的相关使用方法都有较为清晰明了的说明。常用的库函数如下所示:

// 初始化定时器
XScuTimer_Config *XScuTimer_LookupConfig(UINTPTR BaseAddr);
s32 XScuTimer_CfgInitialize(XScuTimer *InstancePtr,
			    XScuTimer_Config *ConfigPtr, u32 EffectiveAddress);

// 开启和关闭定时器
void XScuTimer_Start(XScuTimer *InstancePtr);
void XScuTimer_Stop(XScuTimer *InstancePtr);

// 获取当前计数值
XScuTimer_GetCounterValue(InstancePtr);

// 自动重装载
XScuTimer_LoadTimer(InstancePtr, Value);
XScuTimer_EnableAutoReload(InstancePtr);
XScuTimer_DisableAutoReload(InstancePtr);

// 预分频器操作
void XScuTimer_SetPrescaler(XScuTimer *InstancePtr, u8 PrescalerValue);
u8 XScuTimer_GetPrescaler(XScuTimer *InstancePtr);

// 使能/失能中断
XScuTimer_EnableInterrupt(XScuTimer *InstancePtr);
XScuTimer_DisableInterrupt(XScuTimer *InstancePtr);

// 清除中断标志位
XScuTimer_ClearInterruptStatus(XScuTimer *InstancePtr);

本次实验主要实现的实验现象是:每秒产生一次中断,并且在串口输出信息。虽然在中断服务函数中直接进行串口输出有可能会造成程序堵塞,但是在学习阶段,还是可以使用这种方法方便地观察到实验现象。

可以将Timer的功能抽象出来,单独写一个.c.h文件:

  • timer.h
#ifndef __TIMER_H__
#define __TIMER_H__

#include "xparameters.h"
#include "xparameters_ps.h"

#include "xil_exception.h"
#include "xscutimer.h"
#include "xscugic.h"
#include "xstatus.h"

#define TIMER_BASE_ADDRESS  XPAR_SCUTIMER_BASEADDR
#define TIMER_LOAD_VALUE    XPAR_CPU_CORE_CLOCK_FREQ_HZ / 2     // 私有定时器的计数频率默认是CPU基频的一半

// #define INTC_DEVICE_ID      XPAR_SCUGIC_SINGLE_DEVICE_ID     // 添加即报错,2024.2均采用BaseAddr进行初始化
#define INTC_BASE_ADDRESS   XPAR_INTC_BASEADDR

#define TIMER_INTR_ID       XPAR_SCUTIMER_INTR      // 私有定时器中断,定义来自xparameters_ps.h,类型号为29U

int Timer_Init(XScuTimer * timer_inst_ptr);
int Timer_Intr_Init(XScuGic * intc_inst_ptr, XScuTimer * timer_inst_ptr);
void TimerInitHandler(void * CallBackRef);

extern int second;

#endif

关于不同版本Vivado/Vitis程序的宏定义区别
应该引入xparameters.h还是xparameters_ps.h,这是一个问题。如果你和我一样,尝试在Zynq系列芯片的PS上直接运行老版本的例程,可能很多程序现象都无法准确复现。AMD官方似乎对Zynq使用的标准库进行了更新,特别是修改了初始化、中断ID的定义逻辑。如果你使用Vivado 2020甚至更早的版本进行开发,可能会发现本例程的初始化方式(应该使用DEVICE_ID还是BASEADDRESS)、中断ID宏定义方面会有所不同,这就是在使用早先版本的例程在最新版本的环境中可能无法复现实验现象的原因。Zynq库中,中断ID是进入中断程序的入口。笔者在调试过程中,遇到的最难以察觉的问题就是中断ID的定义错误,这时就要翻看手册,或者详细研究之前的代码,因为中断ID是由Zynq芯片中的电路结构决定的。宏定义的名称可能有所变化,但是宏定义的值一定不会改变(总不可能开发环境更新后无法实现功能)。

  • timer.c
#include "timer.h"

int second = 10;

int Timer_Init(XScuTimer * timer_inst_ptr) {

    int Status;
    XScuTimer_Config * timer_cfg_ptr;
    timer_cfg_ptr = XScuTimer_LookupConfig(TIMER_BASE_ADDRESS);
    if (timer_cfg_ptr == NULL) {
        return XST_FAILURE;
    }
    Status = XScuTimer_CfgInitialize(timer_inst_ptr, timer_cfg_ptr, timer_cfg_ptr->BaseAddr);
    if (Status != XST_SUCCESS) {
        return XST_FAILURE;
    }

    XScuTimer_LoadTimer(timer_inst_ptr, TIMER_LOAD_VALUE);      // 设置自动重装载值
    XScuTimer_EnableAutoReload(timer_inst_ptr);     // 开启自动重装载
    XScuTimer_EnableInterrupt(timer_inst_ptr);      // 使能定时器中断
    
    XScuTimer_Start(timer_inst_ptr);

    return XST_SUCCESS;

}

int Timer_Intr_Init(XScuGic * intc_inst_ptr, XScuTimer * timer_inst_ptr) {

    // 初始化中断控制器
    XScuGic_Config * intc_cfg_ptr;
    intc_cfg_ptr = XScuGic_LookupConfig(INTC_BASE_ADDRESS);
    if (intc_cfg_ptr == NULL) {
        return XST_FAILURE;
    }
    XScuGic_CfgInitialize(intc_inst_ptr, intc_cfg_ptr, intc_cfg_ptr->CpuBaseAddress);
    
    // 设置并打开中断异常处理功能
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, 
        (Xil_ExceptionHandler) XScuGic_InterruptHandler, 
        intc_inst_ptr);
    Xil_ExceptionEnable();

    // 关联中断源和中断服务函数
    XScuGic_Connect(intc_inst_ptr, 
        TIMER_INTR_ID, 
        (Xil_InterruptHandler) TimerInitHandler, 
        (void *)timer_inst_ptr);
    XScuGic_Enable(intc_inst_ptr, TIMER_INTR_ID);
   
    return XST_SUCCESS;

}

// 中断服务函数
void TimerInitHandler(void * CallBackRef) {

    XScuTimer * timer_ptr = (XScuTimer * ) CallBackRef;

    second --;
    xil_printf("second count:%d\r\n", second);
    XScuTimer_ClearInterruptStatus(timer_ptr);

}

添加文件后需要手动在cmake中设置你要编译的文件名称,否则无法编译。只设置timer.c即可。timer.c两侧的引号可加可不加。

image-20241127161518736

  • main.c
#include "timer.h"
#include "xil_printf.h"

XScuTimer timer_inst;
XScuGic Intc_inst;

int main() {

    int Status;
    xil_printf("SCU Timer Interrupt Example Test. \r\n");
    Status = Timer_Init(&timer_inst);
    if (Status != XST_SUCCESS) {
        xil_printf("[Err] Timer init failed! \r\n");
        return XST_FAILURE;
    }

    Status = Timer_Intr_Init(&Intc_inst, &timer_inst);
    if (Status != XST_SUCCESS) {
        xil_printf("[Err] Timer Interrupt init failed! \r\n");
        return XST_FAILURE;
    }

    while (1) {
        // xil_printf("Running... \r\n");
        if (second == 0) {
            break;
        }
    }
    XScuTimer_DisableInterrupt(&timer_inst);        // 失能定时器中断
    xil_printf("SCU Timer Interrupt Test is Finished! \r\n");

    return XST_SUCCESS;
}

串口输出信息:

image-20241127162111533


  原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、FPGA方面的学习笔记。


<think>好的,我现在需要帮助用户将现有的C代码中的时间单位从秒转换为微秒。用户之前已经询问过如何将定时器计数转换为秒,现在的问题是如何进一步调整以得到微秒。让我先仔细看看用户提供的代码和之前的对话历史。 首先,回顾之前的回答。用户原来的代码中,使用了一个固定的除数166500000,而之前的建议是将这个除数改为实际的CPU频率666666000 Hz,并调整了代码结构。现在用户的新问题是,要将转换后的时间单位从秒改为微秒(us),也就是百万分之一秒。因此,我需要调整转换公式,将原来的秒转换为微秒。 微秒是秒的百万倍,所以原来的公式是时间(秒)= 时钟周期数 / 频率。要得到微秒,就需要乘以1,000,000。因此,新的公式应该是时间(微秒)= (时钟周期数 / 频率) * 1e6。不过,也可以简化这个计算,直接写成时间(微秒)= (时钟周期数 * 1e6) / 频率。这样可以避免先除以大数再乘以大数可能带来的精度问题,尤其是在使用浮点数的情况下。 接下来,检查用户提供的代码。代码中有一个注释提到,定时器的频率是CPU频率的一半,比如333MHz转换为166.5MHz。但用户之前的CPU频率是666.666 MHz,这可能意味着定时器实际运行在333.333 MHz,也就是CPU频率的一半。所以在计算时,用户原来的代码中将elapsed_ticks乘以2,以补偿定时器频率减半的情况。例如,如果定时器频率是CPU的一半,那么每个定时器的tick相当于两个CPU周期,因此需要乘以2来得到正确的CPU周期数。 现在,用户希望将结果转换为微秒。假设定时器的频率仍然是CPU的一半,那么正确的步骤应该是: 1. 计算经过的定时器tick数:0xFFFFFFFF - timer_count,这已经考虑了定时器是递减计数器。 2. 将这个tick数转换为CPU周期数,这里因为定时器频率是CPU的一半,所以每个tick对应2个CPU周期,所以需要乘以2。 3. 将CPU周期数转换为秒:周期数 / CPU频率(666666000 Hz)。 4. 转换为微秒:秒数 * 1e6。 或者,合并步骤3和4,得到:(周期数 * 1e6) / CPU频率。 在代码中,用户原来的代码是: clock_t time = (clock_t)(2 * elapsed_ticks / CPU_FREQ); 这里的乘以2是因为定时器频率是CPU的一半,所以elapsed_ticks是定时器的tick数,乘以2得到实际的CPU周期数。然后除以CPU频率得到秒数。现在要转换为微秒,需要乘以1e6,所以公式变为: (2 * elapsed_ticks * 1e6) / CPU_FREQ 但需要确保运算顺序正确,避免整数溢出或精度丢失。因为elapsed_ticks是u64类型,乘以2和1e6可能会导致很大的数值,但使用double类型可以处理较大的值,尽管可能会有精度损失。如果用户使用的是32位系统,可能需要特别注意,但u64通常可以处理较大的乘法。 接下来,检查用户提供的代码中的变量类型。原来的代码使用clock_t来存储秒数,但clock_t通常是long类型,可能不足以存储微秒数,尤其是当时间较长时。所以可能需要更改为更大的类型,比如uint64_t,或者在返回时使用不同的类型。不过用户当前函数的返回类型是clock_t,这可能限制了微秒的表示范围,需要注意潜在的溢出问题。但用户可能希望保持函数签名不变,或者需要调整返回类型。不过根据用户的问题,可能只是要修改计算部分,而不是改变函数结构。 另外,用户代码中的CPU_FREQ被定义为666666000.0,而原来的注释提到定时器频率是CPU的一半,所以这里可能需要确认是否正确。例如,如果CPU频率是666.666 MHz,定时器频率是333.333 MHz,那么正确的CPU_FREQ应该是666666000.0,而定时器频率是333333000.0。但是用户之前的代码中将elapsed_ticks乘以2,以转换为CPU周期数,这样计算的是实际的CPU周期数,再除以CPU频率得到秒数。所以现在转换为微秒,只需要在原来的秒数基础上乘以1e6,或者在计算时合并。 现在,具体修改代码的步骤: 原代码中的转换部分是:2 * elapsed_ticks / CPU_FREQ,结果转换为秒。 要转换为微秒,需要乘以1e6,所以变为:(2 * elapsed_ticks * 1e6) / CPU_FREQ 或者,等效于 (2 * elapsed_ticks / CPU_FREQ) * 1e6。这两种方式在数学上是等价的,但在计算时可能会有不同的精度,尤其是当数值很大时。使用浮点运算时,先乘以1e6再除以CPU_FREQ可能更优,因为避免了大数相乘,但需要具体分析。 考虑到CPU_FREQ是666666000.0,而1e6是1,000,000,所以整个分母可以简化为666666000.0 / 1e6 = 666.666。因此,公式可以简化为: (2 * elapsed_ticks) / (CPU_FREQ / 1e6) ) = (2 * elapsed_ticks) / 666.666 这可能更高效,但需要确认是否正确。因为微秒是秒的百万分之一,所以原来的秒数乘以1e6得到微秒数,即: 微秒数 = (周期数 / 频率) * 1e6 = 周期数 * 1e6 / 频率 这里周期数是2 * elapsed_ticks,所以: 微秒数 = (2 * elapsed_ticks * 1e6) / 666666000.0 将数值代入计算,666666000.0等于666.666 MHz,乘以1e6得到Hz。所以,简化分母: 666666000.0 = 666.666e6,因此: (2 * elapsed_ticks * 1e6) / 666.666e6 = (2 * elapsed_ticks) / 666.666 因为1e6 / 666.666e6 = 1 / 666.666,所以: 微秒数 = (2 * elapsed_ticks) / 666.666 这可能更高效,但需要注意浮点运算的精度。不过,原代码中使用的是double类型,所以没有问题。 在代码中,用户原来的代码是: clock_t time = (clock_t)(2 * elapsed_ticks / CPU_FREQ); 现在要改为微秒,所以应该修改为: clock_t time = (clock_t)( (2 * elapsed_ticks * 1000000.0) / CPU_FREQ ); 或者,可能更高效的方式是先进行乘法: (2 * elapsed_ticks) * 1e6 / CPU_FREQ 但需要注意运算顺序,避免溢出。由于elapsed_ticks是u64,乘以2和1e6可能会导致数值非常大,超过u64的范围。例如,假设elapsed_ticks是最大的u64值,那么乘以2*1e6会溢出,但在这里代码中,elapsed_ticks是32位定时器的值,所以0xFFFFFFFF,即大约4.2e9,乘以2得到8.4e9,再乘以1e6得到8.4e15,这仍然在u64的范围内(因为u64最大约1.8e19),所以不会有溢出问题。但是,当转换为double时,可能需要先转换为浮点数再进行乘法,以避免整数溢出。 不过,在代码中,用户已经将elapsed_ticks声明为u64,并且在计算时转换为double,所以应该不会有问题。例如: double us = (2.0 * elapsed_ticks * 1000000.0) / CPU_FREQ; 然后转换为clock_t。不过,clock_t通常是long类型,可能不足以存储较大的微秒数值,如果时间较长的话,可能会溢出。例如,如果经过的时间是1秒,即1,000,000微秒,在32位系统上,long的最大值是2,147,483,647,所以可以存储约35分钟左右的微秒数,超过的话会溢出。但用户可能需要使用更大的类型,比如uint64_t,但函数返回类型是clock_t,可能无法更改。不过用户的问题只是如何修改转换部分,所以可能暂时不考虑这个问题,或者提示用户注意溢出。 回到代码,原函数返回clock_t,这可能不适合微秒,但用户可能希望保持接口一致。因此,修改后的代码部分是将秒转换为微秒,即乘以1e6,并调整注释和相关常量。 总结修改步骤: 1. 修改注释中的单位说明,将秒改为微秒。 2. 在计算公式中乘以1,000,000,将秒转换为微秒。 3. 考虑是否需要调整变量类型以避免溢出,尤其是当elapsed_ticks较大时。 具体到代码: 原代码行: clock_t time = (clock_t)(2 * elapsed_ticks / CPU_FREQ); 修改为: clock_t time = (clock_t)( (2 * elapsed_ticks * 1000000.0) / CPU_FREQ ); 或者,更清晰的写法: clock_t time = (clock_t)( (2.0 * elapsed_ticks / CPU_FREQ) * 1000000.0 ); 这两种写法在数学上是等价的,但运算顺序可能影响精度。先进行乘法可能会更优,因为先乘以1e6再除以频率,可能减少浮点数的舍入误差,尤其是当elapsed_ticks较小时。 另外,需要确认CPU_FREQ的定义是否正确。用户提到定时器频率是CPU的一半,所以原来的代码中使用2 * elapsed_ticks来得到CPU周期数。例如,如果定时器每递减一个tick需要两个CPU周期,那么elapsed_ticks是定时器的tick数,乘以2得到对应的CPU周期数,再除以CPU频率得到秒数。现在转换为微秒,同样是基于CPU周期数,所以同样的逻辑适用:先乘以2得到CPU周期数,再转换为微秒。 因此,修改后的公式正确。 另外,用户代码中的注释提到“转换为秒”,需要修改为“转换为微秒”。 接下来,检查变量类型。elapsed_ticks是u64,乘以2后是u64,再乘以1e6(作为double)转换为浮点运算,然后除以666666000.0,结果转换为clock_t。这可能会有精度损失,但可能可以接受。或者,建议使用更高精度的类型,如double,但在代码中已经这样处理。 此外,用户可能需要注意,clock_t通常用于表示处理器时间,单位可能是时钟周期,但在这里被用来存储微秒,可能导致混淆。但根据用户的需求,可能只是需要修改计算部分,而函数接口保持不变。 最后,给出修改后的代码示例,并指出关键修改点,例如乘以1e6,修改注释,以及可能的溢出问题提示。</think>要将时间单位从秒转换为微秒(μs),需在原有计算基础上增加百万倍放大。以下是分步修改说明: ```c clock_t _times(struct tms *buf) { u32 timer_count = XScuTimer_GetCounterValue(&TimerInstance); // 关键修改点:新增微秒转换系数 const double US_PER_SEC = 1000000.0; // 1秒=1,000,000微秒 const double TIMER_MULTIPLIER = 2.0; // 定时器分频系数 const double CPU_FREQ = 666666000.0; // CPU频率(Hz) u64 elapsed_ticks = 0xFFFFFFFF - timer_count; // 修改计算公式:添加微秒转换 double time_us = (TIMER_MULTIPLIER * elapsed_ticks / CPU_FREQ) * US_PER_SEC; // 建议使用更合适的数据类型(原clock_t可能溢出) clock_t time = (clock_t)time_us; buf->tms_utime = time; buf->tms_stime = 0; return time; } ``` **核心修改解析**: 1. **单位换算原理**: $$1\text{秒} = 10^6 \mu s \Rightarrow \text{微秒} = \text{秒} \times 10^6$$ 2. **公式重构**: ```c // 原秒计算: (2 * ticks) / 666666000.0 // 新微秒计算: (2 * ticks / 666666000.0) * 1000000.0 ``` 3. **精度优化**: - 使用`double`类型保留小数运算 - 显式定义转换常量提高可读性 4. **潜在问题处理**: ```c // 针对32位系统建议改用uint64_t: uint64_t time_us = (uint64_t)((2.0 * elapsed_ticks * 1e6) / CPU_FREQ); // 添加溢出检查(假设最大允许值): if(time_us > CLOCK_MAX) { // 错误处理逻辑 } ``` 5. **完整优化版**: ```c #include <stdint.h> #define TIMER_FREQ_HZ 333333000.0 // 实际定时器频率 #define US_PER_SEC 1000000.0 // 微秒转换系数 clock_t _times(struct tms *buf) { uint32_t timer_current = XScuTimer_GetCounterValue(&TimerInstance); static uint32_t timer_prev = 0; // 处理定时器溢出 uint64_t elapsed_ticks = (timer_prev > timer_current) ? (0xFFFFFFFFULL - timer_prev + timer_current) : (timer_prev - timer_current); // 带溢出保护的微秒计算 double time_us = (double)elapsed_ticks / TIMER_FREQ_HZ * US_PER_SEC; // 更新基准值 timer_prev = timer_current; buf->tms_utime = (clock_t)time_us; return (clock_t)time_us; } ``` **验证方法**: 1. 已知定时器周期时测试: ```c // 当定时器配置为1ms间隔时: assert(fabs(time_us - 1000.0) < 1e-6); ``` 2. 使用示波器测量实际GPIO翻转时间 3. 增加调试输出: ```c printf("Raw ticks:%llu -> Calculated us:%.2f\n", elapsed_ticks, time_us); ``` **注意事项**: 1. 定时器工作模式确认(递减/递增计数器) 2. 中断服务程序中的上下文切换时间补偿 3. 32位系统下clock_t类型上限(通常为2147483647 ≈ 35.8分钟) 4. 浮点运算的原子性保护(若在中断上下文使用)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Include everything

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值