第一次接触ZYNQ的开发者可能会被它独特的架构惊艳到。这种将ARM处理器(PS)和FPGA(PL)集成在同一芯片上的设计,让嵌入式开发变得前所未有的灵活。就拿定时器来说,PS端自带的私有定时器数量有限,而通过AXI总线连接PL端的AXI TIMER IP核,我们可以轻松扩展出多个高精度定时器通道。
AXI TIMER本质上是一个可编程逻辑实现的定时器模块,它通过AXI4-Lite接口与处理器通信。每个AXI TIMER IP核包含两个完全独立的定时器通道,每个通道都能配置为以下几种工作模式:
我在最近的一个电机控制项目中就遇到了定时器资源不足的问题。PS端的两个CPU核各自只有一个私有定时器,根本无法满足多路电机控制的需求。这时候AXI TIMER就派上了大用场——通过PL端实现了四路独立的PWM输出(用了两个AXI TIMER IP核),而且还能空出定时器做其他用途。
在Vivado中配置AXI TIMER其实非常简单,但有几个关键参数需要注意。打开Block Design后,添加AXI Timer IP核,双击进入配置界面:
基本参数配置:
配置完成后,记得在Block Design里添加一个AXI Interconnect连接PS和AXI TIMER。这里有个小技巧:如果系统中有多个AXI外设,可以共享同一个AXI Interconnect来节省资源。
引脚分配示例:
tcl复制set_property PACKAGE_PIN Y11 [get_ports pwm_out]
set_property IOSTANDARD LVCMOS33 [get_ports pwm_out]
生成Bitstream之前,建议先Validate Design检查连接是否正确。我遇到过因为AXI总线没连好导致SDK中找不到设备的情况,白白浪费了半天时间排查。
Xilinx提供了完善的驱动程序,但直接使用官方例程可能会遇到一些坑。下面分享我优化过的初始化代码:
定时器初始化关键步骤:
c复制// 更健壮的初始化函数
int Timer_Init(XTmrCtr *InstancePtr, u16 DeviceId, u32 PeriodUs)
{
int Status;
XTmrCtr_Config *Config;
// 查找硬件配置
Config = XTmrCtr_LookupConfig(DeviceId);
if (!Config) {
xil_printf("Timer config not found\n");
return XST_FAILURE;
}
// 初始化驱动实例
Status = XTmrCtr_CfgInitialize(InstancePtr, Config, Config->BaseAddress);
if (Status != XST_SUCCESS) {
xil_printf("Timer init failed\n");
return Status;
}
// 硬件自检(测试两个通道)
for (int i = 0; i < 2; i++) {
Status = XTmrCtr_SelfTest(InstancePtr, i);
if (Status != XST_SUCCESS) {
xil_printf("Timer %d self test failed\n", i);
return Status;
}
}
// 设置定时周期(自动计算寄存器值)
u32 RegVal = XTmrCtr_UsToRegValue(InstancePtr, PeriodUs);
XTmrCtr_SetResetValue(InstancePtr, 0, RegVal);
XTmrCtr_SetResetValue(InstancePtr, 1, RegVal);
// 启用自动重载和中断
XTmrCtr_SetOptions(InstancePtr, 0, XTC_INT_MODE_OPTION | XTC_AUTO_RELOAD_OPTION);
XTmrCtr_SetOptions(InstancePtr, 1, XTC_INT_MODE_OPTION | XTC_AUTO_RELOAD_OPTION);
return XST_SUCCESS;
}
这段代码增加了完善的错误检查,并且将定时周期计算封装成了独立函数。实际项目中,建议把定时器操作都封装成这样的模块化函数,方便复用。
AXI TIMER的两个通道可以完全独立工作,这为复杂应用提供了很大便利。下面通过两个实际案例说明:
案例一:电机控制
c复制// 电机控制初始化
void Motor_Init(void)
{
// 通道0配置为PWM模式
XTmrCtr_PwmConfigure(&Timer, 0,
XTmrCtr_UsToNs(100), // 10kHz周期
XTmrCtr_UsToNs(30)); // 初始占空比30%
// 通道1配置为定时中断
XTmrCtr_SetResetValue(&Timer, 1, XTmrCtr_UsToRegValue(&Timer, 100000));
XTmrCtr_Start(&Timer, 1);
}
案例二:LED调光
c复制// LED渐变效果
void LED_Effect(void)
{
static u8 brightness = 0;
static bool increasing = true;
// 每2秒调整一次亮度
if (timer1_flag) {
timer1_flag = 0;
if (increasing) {
brightness += 10;
if (brightness >= 100) increasing = false;
} else {
brightness -= 10;
if (brightness <= 0) increasing = true;
}
XTmrCtr_PwmSetDutyCycle(&Timer, 0, brightness);
}
}
在实际调试时,建议先用逻辑分析仪或示波器观察波形。我曾经遇到过PWM频率设置过高导致电机驱动器无法响应的问题,最终发现是没仔细看驱动器规格书的最大PWM频率限制。
除了基本的PWM功能,AXI TIMER还有一些高级用法值得掌握:
动态调整占空比
c复制// 平滑改变占空比(用于电机软启动)
void Pwm_SoftStart(XTmrCtr *InstancePtr, u8 Channel, u32 TargetDuty)
{
u32 current = XTmrCtr_PwmGetDutyCycle(InstancePtr, Channel);
while (current != TargetDuty) {
if (current < TargetDuty) current++;
else current--;
XTmrCtr_PwmSetDutyCycle(InstancePtr, Channel, current);
usleep(10000); // 10ms间隔
}
}
多通道同步
当需要多个PWM信号同步时,可以这样操作:
c复制// 同步启动两个PWM通道
void Pwm_SyncStart(XTmrCtr *InstancePtr)
{
// 先停止两个通道
XTmrCtr_Stop(InstancePtr, 0);
XTmrCtr_Stop(InstancePtr, 1);
// 设置参数(省略)
// 同步启动
XTmrCtr_Start(InstancePtr, 0);
XTmrCtr_Start(InstancePtr, 1);
}
死区时间控制
在H桥电路等应用中,需要在上、下管切换时插入死区时间:
c复制void Pwm_SetDeadTime(u32 ns)
{
// 配置第一个PWM的正常占空比
XTmrCtr_PwmConfigure(&Timer, 0, period_ns, high_ns);
// 配置第二个PWM为互补输出,并加入死区时间
XTmrCtr_PwmConfigure(&Timer, 1, period_ns, high_ns - deadtime_ns);
}
使用AXI TIMER时可能会遇到一些性能问题,这里分享几个优化经验:
中断延迟优化
c复制// 优化的中断处理
void Timer_Handler(void *CallBackRef, u8 TmrCtrNumber)
{
// 仅设置标志位,主循环中处理实际任务
timer_flags |= (1 << TmrCtrNumber);
// 清除中断标志
XTmrCtr_ClearStats(XPAR_AXI_TIMER_0_BASEADDR);
}
定时精度测试
可以通过以下方法验证定时精度:
我做过一个72小时连续测试,发现AXI TIMER的误差小于0.01%,完全满足工业控制需求。
常见问题排查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 无法产生中断 | 中断未连接或未使能 | 检查GIC配置和中断控制器初始化 |
| PWM无输出 | 引脚未分配或约束错误 | 检查约束文件和原理图 |
| 定时不准 | 时钟频率配置错误 | 核对Vivado和SDK中的时钟设置 |
| 双通道不同步 | 启动时间不一致 | 使用同步启动方式 |
AXI TIMER的真正威力在于可以和PL端的其他逻辑配合使用。比如:
硬件PWM参数自动调整
多轴运动控制
c复制// 三轴联动示例
void MoveToPosition(int x, int y, int z)
{
// 计算各轴步进频率(简化版)
int freq_x = CalculateFreq(x);
int freq_y = CalculateFreq(y);
int freq_z = CalculateFreq(z);
// 设置三个AXI TIMER的频率
SetPwmFrequency(TIMER_X, freq_x);
SetPwmFrequency(TIMER_Y, freq_y);
SetPwmFrequency(TIMER_Z, freq_z);
// 等待运动完成
while (!MotionDone());
}
在最近的一个机器人项目中,我们就是用这种方法实现了六轴机械臂的平滑控制。PS端负责上层轨迹规划和网络通信,PL端处理实时控制,AXI TIMER则提供了精确的定时基准。