AXI Timer IP核实战:从PWM生成到中断处理的全流程配置指南
在嵌入式系统与FPGA设计中,精确的定时控制是驱动电机、生成通信协议、管理任务调度乃至实现复杂人机交互的基石。当你面对一个需要精准脉冲、周期性事件或实时响应的项目时,一个功能强大且易于集成的硬件定时器IP核往往能成为破局的关键。Xilinx(现AMD)提供的AXI Timer IP核,正是这样一位“沉默的助手”。它远不止是一个简单的计数器,而是一个集成了生成、捕获、PWM及中断功能的完整定时子系统,通过标准的AXI4-Lite接口无缝融入你的SoC设计。
然而,官方文档往往侧重于寄存器位域的罗列和功能模式的枚举,对于初次接触的开发者而言,如何将其转化为可运行、可调试的实际工程代码,中间仍有一道需要跨越的鸿沟。你是否曾对着寄存器列表感到困惑,不确定配置的先后顺序?是否在调试PWM波形时,发现占空比与预期不符?又或者在处理中断时,遭遇了无法进入中断服务例程的窘境?
本文将从一名实战工程师的视角出发,彻底拆解AXI Timer IP核的应用流程。我们不满足于理论的复述,而是聚焦于“如何做”和“为什么这么做”。你将看到完整的配置代码片段、真实的调试逻辑分析仪截图(文字描述替代)、以及那些手册中未曾明说,却在实际项目中至关重要的“坑点”与技巧。无论你是正在评估该IP核的选型,还是已经将其纳入设计却卡在了某个环节,这篇文章都将为你提供一条清晰的、从零到一的实现路径。
1. 理解核心:AXI Timer的架构与工作模式精要
在动手写代码之前,我们必须先在心里建立起AXI Timer的清晰模型。这个IP核内部包含两个独立的32位定时器(Timer 0和Timer 1),它们既可以独立工作,也可以协同运作,甚至合并为一个64位定时器。每个定时器的核心都由三个关键寄存器驱动:控制状态寄存器(TCSR)、加载寄存器(TLR)和计数寄存器(TCR)。
TCSR是大脑,它决定了定时器是递增还是递减计数、选择哪种工作模式、是否使能中断或自动重载。TLR是初始值仓库,计数器从这里获取起点。TCR则是实时战场,它的值随着时钟节拍不断变化,反映了当前的计数状态。理解这三者的关系,是灵活驾驭这个IP核的前提。
AXI Timer主要支持四种工作模式,每种模式都对应着不同的应用场景:
- 生成模式:这是最基础的定时器模式。计数器从TLR值开始,按照设定的方向(增/减)计数,到达边界(全1后归零,或全零后跳变)时,可以产生一个周期的高电平脉冲(Generate Out信号),并可选择是否触发中断。它常用于产生精确的时间延迟、周期性触发事件或作为看门狗。
- 捕获模式:此模式下,定时器化身为一个“事件记录仪”。当外部捕获触发信号(Capture Trig)有效时,计数器当前的瞬时值会被“冻结”并锁存到TLR寄存器中,同时可产生中断通知处理器。这非常适合测量外部脉冲的宽度、频率或记录事件发生的精确时刻。
- PWM模式:这是将两个定时器捆绑使用的经典场景。Timer 0负责设定PWM波的整个周期,Timer 1则设定高电平(或低电平,取决于配置)的持续时间,两者共同协作,产生占空比可调的脉冲波形。这是驱动LED调光、电机调速、舵机控制的直接工具。
- 级联模式:当32位计数范围(约43秒@100MHz)不够用时,可以将Timer 0和Timer 1级联,形成一个64位定时器,从而获得极其漫长的定时周期(约5849年!)。此时,Timer 1作为高32位,Timer 0作为低32位,由TCSR0统一控制。
为了更直观地区分这些模式的核心配置差异,可以参考下表:
| 工作模式 | 核心用途 | 关键配置位 (以Timer 0为例) | 主要输出信号 |
|---|---|---|---|
| 生成模式 | 定时、延时、事件触发 | MDT0=0, GENT0=1, ENT0=1 | Generate Out0, Interrupt |
| 捕获模式 | 测量脉冲宽度、频率 | MDT0=1, CAPT0=1, ENT0=1 | Interrupt (TLR捕获值) |
| PWM模式 | 产生脉宽调制波形 | MDT0=0, MDT1=0, PWMA0=1, GENT0=1, GENT1=1 | PWM0 |
| 级联模式 | 超长周期定时 | CASC=1, MDT0=0 (生成) 或 MDT0=1 (捕获) | Generate Out0 / Interrupt |
提示:上表中的配置位仅为该模式下的必要设置,实际应用中还需根据需求配置计数方向(
UDT)、中断使能(ENIT)、自动重载(ARHT)等。ENT0=1表示使能计数器,通常在完成所有配置后最后置位。
2. 实战起点:Vivado中的IP核配置与集成
理论清晰后,我们进入第一个实战环节——在Vivado设计套件中实例化并配置AXI Timer IP核。这个过程虽然图形化,但几个关键选项的理解直接影响后续软件驱动的编写。
- 创建或打开工程:在Vivado中新建一个Block Design项目,或打开已有的设计。
- 添加IP核:在Diagram窗口中,点击“+”号,搜索“AXI Timer”,双击添加。
- 关键参数配置:双击添加的AXI Timer实例,打开配置窗口。这里有几个需要仔细斟酌的选项:
- Timer Width:这是定时器的位宽,可选8、16、32。对于绝大多数应用,32位是标准选择,它能提供足够的计数范围。除非资源极其紧张或有特殊对齐要求,否则保持默认32。
- Number of Timers:选择“2”。即使你暂时只用一个,选择两个也为未来功能扩展(如PWM模式)留有余地,且资源开销增加很小。
- Enable Capture Triggers:如果你计划使用捕获模式来测量外部信号,务必勾选此项。它会将
capturetrig0/1端口引出到IP核外部。 - Active state of ...:这两项决定了信号的极性。
Capture Trigger:选择高有效(Active High)意味着当外部输入信号为高电平时触发捕获事件。根据你的外部传感器或信号源特性选择。Generate Out Signal:选择高有效意味着当定时器计数完成时,输出一个高电平脉冲。这通常更符合直觉。
- 连接与自动化:使用“Run Connection Automation”和“Run Block Automation”功能,让Vivado自动完成AXI互联、时钟和复位网络的连接。检查生成的连接图,确保
S_AXI接口连接到处理系统的AXI总线,时钟和复位正确连接。 - 生成输出产品:在Sources窗口中右键点击Block Design,选择“Generate Output Products”。这一步会生成IP核的网表、约束文件以及最重要的——供软件开发使用的硬件定义文件(
xparameters.h等)。
完成以上步骤后,你的硬件平台就拥有了一个可用的AXI Timer外设。接下来,重心转移到软件层面,如何通过代码与它对话。
3. 软件驱动开发:寄存器操作与基础模式实现
有了硬件平台,我们需要编写软件来配置和控制它。通常我们会基于Xilinx提供的Vitis/XSDK或Vitis统一软件平台进行开发。首先,在Vitis中创建应用工程,并导入刚才Vivado导出的硬件平台(.xsa文件)。
驱动开发的核心是理解并操作内存映射的寄存器。Vitis工具会为我们生成一个包含基地址和寄存器偏移量的头文件(如xparameter.h)。假设我们的Timer实例名为axi_timer_0,那么我们可以这样定义访问宏:
#include "xparameters.h"
#include "xil_io.h"
// 假设从 xparameters.h 中获取的基地址
#define TIMER_BASE_ADDR XPAR_AXI_TIMER_0_BASEADDR
// 寄存器偏移量定义 (偏移量相对于基地址)
#define TCSR0_OFFSET 0x00 // 控制状态寄存器0
#define TLR0_OFFSET 0x04 // 加载寄存器0
#define TCR0_OFFSET 0x08 // 计数寄存器0
#define TCSR1_OFFSET 0x10 // 控制状态寄存器1
#define TLR1_OFFSET 0x14 // 加载寄存器1
#define TCR1_OFFSET 0x18 // 计数寄存器1
// 方便的寄存器读写宏
#define TIMER_REG_WRITE(offset, data) \
Xil_Out32((TIMER_BASE_ADDR) + (offset), (data))
#define TIMER_REG_READ(offset) \
Xil_In32((TIMER_BASE_ADDR) + (offset))
现在,让我们实现一个最基本的生成模式定时器,让它每隔1秒产生一次中断。假设系统AXI时钟频率为100MHz。
/**
* 配置Timer 0为自动重载的递减计数生成模式,并设置1秒周期
* @param base_addr Timer IP核基地址
*/
void timer_init_gen_mode(u32 base_addr) {
u32 tcsr0_val;
u32 load_val;
// 1. 停止定时器,并清除可能存在的旧中断标志
// 先读取当前TCSR0值,然后清除ENT0位以停止,并写1清除TINT0中断标志
tcsr0_val = TIMER_REG_READ(TCSR0_OFFSET);
tcsr0_val &= ~(1 << 7); // 清除ENT0 (bit7)
tcsr0_val |= (1 << 8); // 写1清除TINT0标志 (bit8)
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
// 2. 计算并设置加载值TLR0
// 公式: 定时周期 = (TLR0 + 2) * 时钟周期
// 对于递减模式,要定时1秒,TLR0 = (定时周期 / 时钟周期) - 2
// 时钟周期 = 1 / 100e6 = 10 ns
// TLR0 = (1.0 / 10e-9) - 2 = 100000000 - 2 = 99999998
load_val = 100000000 - 2; // 1秒 @100MHz
TIMER_REG_WRITE(TLR0_OFFSET, load_val);
// 3. 配置TCSR0寄存器
tcsr0_val = 0; // 从零开始构建配置值
tcsr0_val |= (0 << 0); // MDT0=0: 生成模式
tcsr0_val |= (0 << 1); // UDT0=0: 递减计数 (注意:0是递减,1是递增,此处容易混淆,需查手册确认)
// 根据手册,UDT=0为递减,=1为递增。我们使用递减。
tcsr0_val |= (1 << 2); // GENT0=1: 使能生成输出
tcsr0_val |= (0 << 3); // CAPT0=0: 不使能捕获
tcsr0_val |= (1 << 4); // ARHT0=1: 自动重载
tcsr0_val |= (1 << 5); // LOAD0=1: 加载TLR0值到计数器 (此位写1后会自动清零)
tcsr0_val |= (1 << 6); // ENIT0=1: 使能中断
// ENT0位(bit7)先不使能,等加载完成后再使能
// TINT0位(bit8)是状态位,只读,写1清除,我们稍后清除
// PWMA0, ENALL, CASC等位保持为0
// 4. 先写入配置,并执行加载操作
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
// 5. 短暂延时,确保加载完成(通常一个时钟周期即可,此处用循环等待)
// 然后,再次读取TCSR0,确保LOAD0位已自动清零,再使能定时器
while (TIMER_REG_READ(TCSR0_OFFSET) & (1 << 5)); // 等待LOAD0位清零
tcsr0_val = TIMER_REG_READ(TCSR0_OFFSET);
tcsr0_val |= (1 << 7); // 置位ENT0,启动定时器
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
// 6. 清除可能因加载操作产生的虚假中断标志(可选但推荐)
tcsr0_val = TIMER_REG_READ(TCSR0_OFFSET);
tcsr0_val |= (1 << 8); // 写1清除TINT0
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
}
注意:上述代码中关于
UDT位的注释指出了手册中一个常见的混淆点。不同版本或系列的IP核,递减/递增的定义可能不同。务必以你所用IP核版本的数据手册(DS764)为准。最保险的方法是编写一个简单的测试程序,通过读取TCR寄存器的变化来验证计数方向。
这段代码展示了配置一个定时器的完整流程:停止->设置参数->加载->启动。其中,等待LOAD位清零的步骤是很多新手容易忽略的,如果不等待,在LOAD位置1的同时就使能ENT,可能导致计数器从错误的值开始计数。
4. 核心应用:生成精确PWM波形的全流程解析
PWM模式是AXI Timer最广泛的应用之一。它利用两个定时器协同工作,一个定周期,一个定占空比。这里我们实现一个生成1kHz频率、30%占空比PWM波形的例子。
首先,明确PWM模式下的硬件连接:我们只需要使用pwm0这个输出引脚。generateout0和generateout1在内部被用于控制PWM逻辑,通常无需外部连接。
软件配置的关键在于理解Timer 0和Timer 1的角色分工,以及计数方向对计算公式的影响。我们选择递减计数模式,因为它对应的公式更直观:周期 = (TLR0 + 2) * Tclk, 高电平时间 = (TLR1 + 2) * Tclk。
/**
* 配置AXI Timer为PWM模式,产生指定频率和占空比的波形
* @param base_addr Timer IP核基地址
* @param freq_hz 期望的PWM频率 (Hz)
* @param duty_cycle 期望的占空比 (0.0 到 1.0)
*/
void timer_init_pwm_mode(u32 base_addr, u32 freq_hz, float duty_cycle) {
u32 tcsr0_val, tcsr1_val;
u32 load0_val, load1_val;
float period_sec, high_time_sec;
u32 clock_freq_hz = 100000000; // AXI时钟频率,假设为100MHz
float clock_period_sec = 1.0 / clock_freq_hz;
// 1. 停止两个定时器
tcsr0_val = TIMER_REG_READ(TCSR0_OFFSET);
tcsr1_val = TIMER_REG_READ(TCSR1_OFFSET);
tcsr0_val &= ~(1 << 7); // 清除ENT0
tcsr1_val &= ~(1 << 7); // 清除ENT1
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
TIMER_REG_WRITE(TCSR1_OFFSET, tcsr1_val);
// 2. 计算TLR0和TLR1的值 (递减计数模式)
period_sec = 1.0 / freq_hz;
high_time_sec = period_sec * duty_cycle;
// 公式: TLR = (时间 / 时钟周期) - 2
load0_val = (u32)(period_sec / clock_period_sec) - 2;
load1_val = (u32)(high_time_sec / clock_period_sec) - 2;
// 边界检查:确保值在32位范围内且大于0
if (load0_val > 0xFFFFFFFE || load1_val > 0xFFFFFFFE || load0_val < load1_val) {
xil_printf("Error: Invalid frequency or duty cycle for PWM calculation.\r\n");
return;
}
// 3. 设置加载寄存器
TIMER_REG_WRITE(TLR0_OFFSET, load0_val);
TIMER_REG_WRITE(TLR1_OFFSET, load1_val);
// 4. 配置TCSR0寄存器 (控制Timer 0,并开启PWM模式)
tcsr0_val = 0;
tcsr0_val |= (0 << 0); // MDT0=0: 生成模式
tcsr0_val |= (0 << 1); // UDT0=0: 递减计数
tcsr0_val |= (1 << 2); // GENT0=1: 使能生成输出 (PWM必需)
tcsr0_val |= (0 << 3); // CAPT0=0
tcsr0_val |= (1 << 4); // ARHT0=1: 自动重载,保证连续波形
tcsr0_val |= (1 << 5); // LOAD0=1: 加载TLR0
tcsr0_val |= (0 << 6); // ENIT0=0: PWM通常不需要中断,除非要动态调整
tcsr0_val |= (1 << 9); // PWMA0=1: **关键!使能PWM模式**
// ENT0稍后使能
// 5. 配置TCSR1寄存器 (控制Timer 1)
tcsr1_val = 0;
tcsr1_val |= (0 << 0); // MDT1=0: 生成模式
tcsr1_val |= (0 << 1); // UDT1=0: 递减计数 (需与Timer0一致)
tcsr1_val |= (1 << 2); // GENT1=1: 使能生成输出 (PWM必需)
tcsr1_val |= (0 << 3); // CAPT1=0
tcsr1_val |= (1 << 4); // ARHT1=1: 自动重载
tcsr1_val |= (1 << 5); // LOAD1=1: 加载TLR1
tcsr1_val |= (0 << 6); // ENIT1=0
// ENT1稍后使能
// 6. 写入配置并加载
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
TIMER_REG_WRITE(TCSR1_OFFSET, tcsr1_val);
// 7. 等待加载完成
while (TIMER_REG_READ(TCSR0_OFFSET) & (1 << 5)); // 等待LOAD0清零
while (TIMER_REG_READ(TCSR1_OFFSET) & (1 << 5)); // 等待LOAD1清零
// 8. 同时启动两个定时器
tcsr0_val = TIMER_REG_READ(TCSR0_OFFSET);
tcsr1_val = TIMER_REG_READ(TCSR1_OFFSET);
tcsr0_val |= (1 << 7); // 置位ENT0
tcsr1_val |= (1 << 7); // 置位ENT1
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
TIMER_REG_WRITE(TCSR1_OFFSET, tcsr1_val);
xil_printf("PWM Initialized: Freq=%d Hz, Duty=%.1f%%, TLR0=0x%X, TLR1=0x%X\r\n",
freq_hz, duty_cycle*100, load0_val, load1_val);
}
调用此函数后,pwm0引脚就会输出稳定的PWM波形。你可以使用示波器或逻辑分析仪进行验证。一个常见的调试技巧是,如果发现没有输出,首先检查PWMA0位是否已设置为1,这是进入PWM模式的“开关”。其次,检查两个定时器的GENT位是否使能。最后,确认两个定时器都已成功启动(ENT位为1)。
5. 中断处理:构建高效可靠的事件响应机制
定时器中断是实现实时响应的关键。AXI Timer的中断逻辑相对直接:当定时器计数完成(或捕获事件发生)且中断使能位(ENIT)为1时,TINT状态位会被硬件置1,同时interrupt输出引脚会产生一个高电平脉冲。处理器通过查询TINT位或配置好的中断控制器(如GIC)来响应。
处理中断的流程通常遵循“初始化-使能-响应-清除”的模式。下面我们以生成模式下的周期性中断为例,展示一个完整的中断服务例程(ISR)框架。这里假设你已配置好系统的中断控制器,并将AXI Timer的中断号连接到了处理器的某个中断输入。
#include "xscugic.h" // 假设使用Zynq的GIC
// 全局变量
static XScuGic InterruptController;
volatile int timer_interrupt_flag = 0;
/**
* AXI Timer 0 的中断服务例程
*/
void timer_isr(void *InstancePtr) {
u32 tcsr0_val;
// 1. 读取TCSR0寄存器,检查中断源(如果有多个中断源的话)
tcsr0_val = TIMER_REG_READ(TCSR0_OFFSET);
// 2. 检查TINT0位是否为1(表示Timer 0中断)
if (tcsr0_val & (1 << 8)) {
// 3. 清除中断标志:向TINT0位写1
tcsr0_val |= (1 << 8);
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
// 4. 执行用户的中断处理任务
timer_interrupt_flag = 1; // 设置标志位,主循环中处理
// 例如:翻转一个LED,更新系统心跳,处理超时任务等
// xil_printf("Timer Interrupt occurred!\r\n");
}
// 如果有Timer 1中断,也可以在这里判断TCSR1的TINT1位
}
/**
* 设置AXI Timer中断
* @param intc_ptr 中断控制器实例指针
* @param timer_int_id AXI Timer的中断ID(在xparameters.h中定义)
*/
int setup_timer_interrupt(XScuGic *intc_ptr, u16 timer_int_id) {
int status;
// 初始化中断控制器(此处省略,假设已完成)
// ...
// 注册中断服务例程
status = XScuGic_Connect(intc_ptr, timer_int_id,
(Xil_ExceptionHandler)timer_isr,
(void *)TIMER_BASE_ADDR);
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
// 在中断控制器中使能该中断
XScuGic_Enable(intc_ptr, timer_int_id);
// 使能AXI Timer 0自身的中断(ENIT0位)
u32 tcsr0_val = TIMER_REG_READ(TCSR0_OFFSET);
tcsr0_val |= (1 << 6); // 置位ENIT0
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
// 清除可能存在的旧中断标志
tcsr0_val |= (1 << 8);
TIMER_REG_WRITE(TCSR0_OFFSET, tcsr0_val);
return XST_SUCCESS;
}
在主循环中,你可以通过检查timer_interrupt_flag来处理定时任务。这种“ISR内标记,主循环处理”的方式,有助于保持ISR的简短,避免在中断中执行耗时操作。
注意:中断标志的清除操作是写1清零,这与很多“读后自动清除”或“写0清除”的中断系统不同,务必注意。忘记清除中断标志会导致中断持续触发,系统可能卡死在ISR中。
6. 调试技巧与常见问题排查
即使按照指南操作,在实际硬件上仍可能遇到问题。以下是一些基于经验的调试心得和常见“坑点”的排查清单。
问题一:PWM没有输出,或者波形频率/占空比不对。
- 检查时钟:首先确认AXI Timer的
s_axi_aclk端口是否正确接到了稳定的时钟信号,并且频率与你软件计算时假设的一致。可以在Vivado中设置调试探针,或者用逻辑分析仪抓取该时钟线。 - 验证配置寄存器:在启动定时器后,通过
Xil_printf回读TCSR0、TCSR1、TLR0、TLR1、TCR0、TCR1的值。确保:PWMA0位为1。ENT0和ENT1位为1。GENT0和GENT1位为1。LOAD0和LOAD1位为0(加载完成后应自动清零)。TLR0和TLR1的值符合计算预期。TCR0和TCR1的值在变化(对于递减计数,应该从TLR值开始减小)。
- 检查引脚约束:确认
pwm0这个网络在Vivado中已被分配到正确的FPGA引脚,并且.xdc约束文件中的I/O标准(如LVCMOS33)设置正确。 - 公式与方向确认:再次核对你是按照递增还是递减计数来计算的
TLR值。最稳妥的方法是写一个简单的测试:设置一个很小的TLR值(比如10),用示波器测量实际周期,反推验证公式和计数方向。
问题二:中断无法进入。
- 中断控制器配置:90%的中断问题出在中断控制器的配置上。确保:
- 处理器全局中断已使能。
- AXI Timer的中断信号线
interrupt已正确连接到中断控制器的某个输入。 - 在中断控制器中,该中断ID已被正确连接(Connect)和使能(Enable)。
- 中断服务例程(ISR)的入口地址已正确注册。
- Timer自身中断使能:确认
TCSR寄存器中的ENIT位已被置1。 - 中断标志清除:在ISR中,是否正确地写1清除了
TINT位?如果没有清除,第一次中断后,中断状态会一直保持,可能影响后续中断的识别(取决于中断控制器是电平敏感还是边沿敏感)。 - 中断优先级与屏蔽:检查系统中是否有更高优先级的中断一直占用CPU,或者当前中断是否被意外屏蔽。
问题三:级联模式不工作。
- 级联位顺序:务必在使能定时器(
ENT0置1)之前,将TCSR0的CASC位置1。如果先使能定时器再级联,行为是未定义的。 - 寄存器使用:级联模式下,仅使用
TCSR0、TLR0(低32位)、TLR1(高32位)。TCSR1和TCR1不再独立使用。generateout1和capturetrig1无效。 - 计算公式:级联模式下的周期计算公式中的常数项是
+4,而不是单定时器模式的+2,注意不要用错公式。
一个实用的调试函数:将以下函数加入你的代码,可以快速打印所有关键寄存器的状态。
void print_timer_status(u32 base_addr) {
xil_printf("--- AXI Timer Status ---\r\n");
xil_printf("TCSR0: 0x%08X\r\n", TIMER_REG_READ(TCSR0_OFFSET));
xil_printf("TLR0: 0x%08X (%u)\r\n", TIMER_REG_READ(TLR0_OFFSET), TIMER_REG_READ(TLR0_OFFSET));
xil_printf("TCR0: 0x%08X\r\n", TIMER_REG_READ(TCR0_OFFSET));
xil_printf("TCSR1: 0x%08X\r\n", TIMER_REG_READ(TCSR1_OFFSET));
xil_printf("TLR1: 0x%08X (%u)\r\n", TIMER_REG_READ(TLR1_OFFSET), TIMER_REG_READ(TLR1_OFFSET));
xil_printf("TCR1: 0x%08X\r\n", TIMER_REG_READ(TCR1_OFFSET));
}
在我最近的一个电机控制项目中,就曾因为忽略了LOAD位清零的等待,导致PWM周期随机出错。通过添加状态打印函数,很快发现LOAD位在使能后很久才变0,原因是主时钟域和AXI总线时钟域之间的同步延迟。在配置序列中插入一个等待循环后,问题迎刃而解。硬件编程就是这样,数据手册是地图,但路上的小沟小坎,还得靠实际走一遍才能发现。
62



被折叠的 条评论
为什么被折叠?



