1. ZYNQ裸机开发概述
ZYNQ系列芯片是Xilinx推出的革命性产品,它将ARM Cortex-A9双核处理器与可编程逻辑(FPGA)完美集成在单一芯片上。这种独特的架构为嵌入式系统开发带来了前所未有的灵活性,开发者可以根据应用需求,在软件处理(ARM)和硬件加速(FPGA)之间实现最优平衡。
裸机开发(Bare-metal Development)是指不依赖操作系统,直接在硬件上运行程序的开发方式。对于初学者而言,裸机开发是理解ZYNQ架构的最佳切入点。通过直接操作硬件寄存器,开发者可以深入掌握芯片的工作原理,为后续的Linux驱动开发或RTOS应用打下坚实基础。
提示:裸机开发虽然需要处理更多底层细节,但执行效率极高,中断响应速度快,特别适合对实时性要求高的应用场景。
2. 开发环境搭建
2.1 硬件准备
推荐使用米联客MZ7X系列开发板作为学习平台,该开发板基于Xilinx Zynq-7000芯片,具体配置如下:
- 核心芯片:XC7Z020-2CLG484I(Artix-7 FPGA + 双核Cortex-A9)
- 内存:1GB DDR3
- 存储:32MB QSPI Flash
- 外设接口:
- 千兆以太网
- USB 2.0 OTG
- HDMI输出
- 摄像头接口
- LCD触摸屏接口
2.2 软件安装
开发工具链主要包含以下组件:
-
Vivado Design Suite 2017.4:
- 包含Vivado(硬件设计)和SDK(软件开发)
- 建议分配至少100GB硬盘空间
- 需要16GB以上内存以获得流畅体验
-
串口调试工具:
- 推荐使用Tera Term或Putty
- 波特率设置为115200
-
辅助工具:
- Visual Studio Code(代码编辑)
- Git(版本控制)
安装步骤:
bash复制# 下载Vivado 2017.4安装包
wget https://www.xilinx.com/support/download/index.html/content/xilinx/en/downloadNav/vivado-design-tools/archive.html
# 运行安装程序(Windows环境)
xsetup.exe
3. 第一个裸机程序
3.1 创建Vivado工程
- 启动Vivado,选择"Create Project"
- 指定工程名称和路径(避免中文路径)
- 选择"RTL Project"类型
- 目标芯片选择:xc7z020clg484-2
3.2 配置ZYNQ Processing System
在Block Design中添加并配置ZYNQ7 IP核:
-
时钟配置:
- PS输入时钟:33.333MHz
- CPU时钟:666.666MHz
- FCLK_CLK0:100MHz
-
DDR配置:
- 选择MT41K256M16RE-125
- 工作频率:533.333MHz
-
外设使能:
- UART1:用于调试输出
- GPIO MIO:连接LED
- QSPI Flash:用于程序固化
3.3 生成硬件平台
- 运行"Generate Output Products"
- 创建HDL Wrapper
- 生成比特流文件(Generate Bitstream)
- 导出硬件到SDK(包含bitstream)
4. SDK软件开发
4.1 创建Hello World应用
在SDK中新建Application Project:
c复制#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
int main()
{
init_platform();
// 打印启动信息
xil_printf("ZYNQ Bare-metal Development Start!\n\r");
// 简单延时循环
while(1) {
xil_printf("System running...\n\r");
usleep(1000000); // 1秒延时
}
cleanup_platform();
return 0;
}
4.2 程序下载与调试
-
连接开发板:
- JTAG下载器连接USB端口
- 串口线连接UART1
-
在SDK中:
- 右键工程 → Run As → Launch on Hardware
- 打开串口终端查看输出
5. GPIO控制实战
5.1 MIO控制LED
开发板上的用户LED通常连接在MIO7上,以下是完整的LED闪烁程序:
c复制#include "xgpiops.h"
#include "xparameters.h"
#include "sleep.h"
#define LED_PIN 7
int main()
{
XGpioPs gpio;
XGpioPs_Config *config;
// 初始化GPIO
config = XGpioPs_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID);
XGpioPs_CfgInitialize(&gpio, config, config->BaseAddr);
// 设置LED引脚为输出
XGpioPs_SetDirectionPin(&gpio, LED_PIN, 1);
XGpioPs_SetOutputEnablePin(&gpio, LED_PIN, 1);
// LED闪烁循环
while(1) {
XGpioPs_WritePin(&gpio, LED_PIN, 1); // 点亮
usleep(500000); // 500ms
XGpioPs_WritePin(&gpio, LED_PIN, 0); // 熄灭
usleep(500000);
}
return 0;
}
5.2 EMIO扩展应用
当MIO资源不足时,可以使用EMIO将GPIO扩展到PL端:
-
在Vivado中:
- 打开ZYNQ IP配置
- 使能EMIO GPIO(通常为64位)
- 在Block Design中连接EMIO到外部端口
-
软件配置:
c复制// EMIO GPIO编号从54开始(MIO有54个)
#define EMIO_LED_PIN (54 + 0) // 第一个EMIO
XGpioPs_SetDirectionPin(&gpio, EMIO_LED_PIN, 1);
XGpioPs_WritePin(&gpio, EMIO_LED_PIN, 1);
6. 中断系统开发
6.1 中断控制器初始化
ZYNQ使用GIC(Generic Interrupt Controller)管理中断:
c复制#include "xscugic.h"
XScuGic Intc; // 中断控制器实例
int Init_Intc()
{
XScuGic_Config *cfg;
// 查找配置
cfg = XScuGic_LookupConfig(XPAR_PS7_SCUGIC_0_DEVICE_ID);
if(!cfg) return XST_FAILURE;
// 初始化
if(XScuGic_CfgInitialize(&Intc, cfg, cfg->CpuBaseAddress) != XST_SUCCESS)
return XST_FAILURE;
// 注册异常处理
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler)XScuGic_InterruptHandler,
&Intc);
Xil_ExceptionEnable();
return XST_SUCCESS;
}
6.2 按键中断示例
配置PL端按键触发中断:
c复制#define BTN_INT_ID XPAR_FABRIC_GPIO_0_VEC_ID
void Btn_Handler(void *arg)
{
static int count = 0;
xil_printf("Button pressed! Count=%d\n\r", ++count);
// 清除中断
XGpio_InterruptClear((XGpio *)arg, 0xFFFFFFFF);
}
int Setup_ButtonIntr()
{
// 连接中断处理函数
XScuGic_Connect(&Intc, BTN_INT_ID,
(Xil_ExceptionHandler)Btn_Handler,
(void *)&Gpio);
// 设置触发方式(上升沿)
XScuGic_SetPriorityTriggerType(&Intc, BTN_INT_ID, 0xA0, 0x03);
// 使能中断
XScuGic_Enable(&Intc, BTN_INT_ID);
// 配置GPIO中断
XGpio_InterruptEnable(&Gpio, 1); // 通道1使能
XGpio_InterruptGlobalEnable(&Gpio);
return XST_SUCCESS;
}
7. 定时器开发
7.1 私有定时器配置
ZYNQ每个ARM核都有独立的私有定时器:
c复制#include "xscutimer.h"
XScuTimer Timer;
int Init_Timer(u16 DeviceId, u32 LoadValue)
{
XScuTimer_Config *cfg;
cfg = XScuTimer_LookupConfig(DeviceId);
if(!cfg) return XST_FAILURE;
if(XScuTimer_CfgInitialize(&Timer, cfg, cfg->BaseAddr) != XST_SUCCESS)
return XST_FAILURE;
// 设置定时器加载值
XScuTimer_LoadTimer(&Timer, LoadValue);
// 自动重载模式
XScuTimer_EnableAutoReload(&Timer);
return XST_SUCCESS;
}
void Start_Timer()
{
XScuTimer_Start(&Timer);
}
7.2 定时器中断应用
创建1秒定时中断:
c复制#define TIMER_INT_ID XPAR_SCUTIMER_INTR
void Timer_Handler(void *arg)
{
static int seconds = 0;
xil_printf("System uptime: %d seconds\n\r", ++seconds);
// 清除中断
XScuTimer_ClearInterruptStatus((XScuTimer *)arg);
}
int Setup_TimerIntr()
{
// CPU频率666.666MHz,定时1秒
u32 timer_load = 666666666 / 2; // 私有定时器频率为CPU频率的一半
Init_Timer(XPAR_PS7_SCUTIMER_0_DEVICE_ID, timer_load);
// 连接中断处理
XScuGic_Connect(&Intc, TIMER_INT_ID,
(Xil_ExceptionHandler)Timer_Handler,
(void *)&Timer);
XScuGic_Enable(&Intc, TIMER_INT_ID);
// 使能定时器中断
XScuTimer_EnableInterrupt(&Timer);
Start_Timer();
return XST_SUCCESS;
}
8. UART通信开发
8.1 UART初始化
ZYNQ PS端包含两个UART控制器:
c复制#include "xuartps.h"
XUartPs Uart;
int Init_Uart(u16 DeviceId, u32 BaudRate)
{
XUartPs_Config *cfg;
cfg = XUartPs_LookupConfig(DeviceId);
if(!cfg) return XST_FAILURE;
if(XUartPs_CfgInitialize(&Uart, cfg, cfg->BaseAddress) != XST_SUCCESS)
return XST_FAILURE;
// 检查硬件是否就绪
if(XUartPs_SelfTest(&Uart) != XST_SUCCESS)
return XST_FAILURE;
// 设置波特率
XUartPs_SetBaudRate(&Uart, BaudRate);
return XST_SUCCESS;
}
8.2 中断模式UART通信
实现中断驱动的UART收发:
c复制#define UART_INT_ID XPAR_XUARTPS_1_INTR
#define BUF_SIZE 256
u8 RecvBuf[BUF_SIZE];
u32 RecvCount = 0;
void Uart_Handler(void *arg)
{
u32 isr = XUartPs_ReadReg(XPAR_PS7_UART_1_BASEADDR, XUARTPS_ISR_OFFSET);
// 接收中断
if(isr & XUARTPS_IXR_RXOVR) {
xil_printf("UART Overrun Error!\n\r");
}
if(isr & (XUARTPS_IXR_RXTRIG | XUARTPS_IXR_RXFULL)) {
while(XUartPs_IsReceiveData(XPAR_PS7_UART_1_BASEADDR)) {
RecvBuf[RecvCount++] = XUartPs_ReadReg(XPAR_PS7_UART_1_BASEADDR,
XUARTPS_FIFO_OFFSET);
// 回显接收到的字符
XUartPs_SendByte(XPAR_PS7_UART_1_BASEADDR, RecvBuf[RecvCount-1]);
if(RecvCount >= BUF_SIZE) RecvCount = 0;
}
}
}
int Setup_UartIntr()
{
// 初始化UART1,波特率115200
if(Init_Uart(XPAR_PS7_UART_1_DEVICE_ID, 115200) != XST_SUCCESS)
return XST_FAILURE;
// 连接中断处理
XScuGic_Connect(&Intc, UART_INT_ID,
(Xil_ExceptionHandler)Uart_Handler,
NULL);
XScuGic_Enable(&Intc, UART_INT_ID);
// 设置UART中断掩码
XUartPs_SetInterruptMask(&Uart, XUARTPS_IXR_RXOVR |
XUARTPS_IXR_RXTRIG |
XUARTPS_IXR_RXFULL);
return XST_SUCCESS;
}
9. AXI总线与自定义IP开发
9.1 AXI GPIO控制
通过AXI总线控制PL端的GPIO:
- 在Vivado中添加AXI GPIO IP核
- 配置IP核参数(数据宽度、中断等)
- 连接时钟和复位信号
- 导出外部引脚
软件控制代码:
c复制#include "xgpio.h"
XGpio AxiGpio;
int Init_AxiGpio(u16 DeviceId)
{
if(XGpio_Initialize(&AxiGpio, DeviceId) != XST_SUCCESS)
return XST_FAILURE;
// 通道1设为输出(控制LED)
XGpio_SetDataDirection(&AxiGpio, 1, 0x00);
return XST_SUCCESS;
}
void Toggle_LEDs()
{
static u32 state = 0;
state = ~state;
XGpio_DiscreteWrite(&AxiGpio, 1, state);
}
9.2 自定义PWM IP设计
- 在Vivado中创建AXI4-Lite接口的IP核
- 添加PWM寄存器:
- 控制寄存器(使能、极性)
- 周期寄存器
- 占空比寄存器
- 生成PWM输出逻辑
软件控制接口:
c复制#define PWM_BASEADDR XPAR_MY_PWM_0_S00_AXI_BASEADDR
void PWM_Set(u32 period, u32 duty_cycle)
{
// 设置周期
Xil_Out32(PWM_BASEADDR + 0x08, period);
// 设置占空比
Xil_Out32(PWM_BASEADDR + 0x0C, duty_cycle);
// 使能PWM
Xil_Out32(PWM_BASEADDR, 0x01);
}
10. DMA数据传输
10.1 AXI DMA配置
实现PS与PL之间的高速数据传输:
- 在Vivado中添加AXI DMA IP核
- 配置为简单模式(非Scatter Gather)
- 连接AXI Stream接口
10.2 DMA中断驱动
c复制#include "xaxidma.h"
XAxiDma Dma;
#define DMA_INT_ID XPAR_FABRIC_AXIDMA_0_VEC_ID
volatile int TxDone = 0;
volatile int RxDone = 0;
void DMA_Handler(void *arg)
{
u32 status = XAxiDma_IntrGetIrq(&Dma, XAXIDMA_DEVICE_TO_DMA);
XAxiDma_IntrAckIrq(&Dma, status, XAXIDMA_DEVICE_TO_DMA);
if(status & XAXIDMA_IRQ_IOC_MASK) {
RxDone = 1;
}
}
int Init_DMA(u16 DeviceId)
{
XAxiDma_Config *cfg;
cfg = XAxiDma_LookupConfig(DeviceId);
if(!cfg || XAxiDma_CfgInitialize(&Dma, cfg) != XST_SUCCESS)
return XST_FAILURE;
// 检查是否为简单模式
if(XAxiDma_HasSg(&Dma)) {
xil_printf("Device configured in SG mode!\n\r");
return XST_FAILURE;
}
// 禁用中断
XAxiDma_IntrDisable(&Dma, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_DMA);
return XST_SUCCESS;
}
int DMA_Transfer(u32 *buf, u32 len)
{
// 启动DMA接收
if(XAxiDma_SimpleTransfer(&Dma, (u32)buf, len,
XAXIDMA_DEVICE_TO_DMA) != XST_SUCCESS) {
return XST_FAILURE;
}
// 等待传输完成
while(!RxDone);
RxDone = 0;
return XST_SUCCESS;
}
11. 双核通信(AMP模式)
11.1 CPU1启动代码
配置ZYNQ在AMP模式下运行:
c复制// CPU1的启动地址(DDR中的位置)
#define CPU1_START_ADDR 0x10000000
void Start_CPU1()
{
// 设置CPU1的启动地址
Xil_Out32(0xFFFFFFF0, CPU1_START_ADDR);
// 发出SEV指令唤醒CPU1
__asm__("sev");
}
11.2 CPU1应用代码
c复制// CPU1的main函数
int main()
{
// 确认当前是CPU1
u32 cpu = Xil_In32(0xF8F00200) & 0x1;
if(cpu != 1) while(1); // 如果不是CPU1则挂起
// 初始化必要外设
Init_Intc();
Init_Uart(XPAR_PS7_UART_1_DEVICE_ID, 115200);
// 打印启动信息
xil_printf("CPU1 Boot Success!\n\r");
// 主循环
while(1) {
xil_printf("CPU1 Running...\n\r");
usleep(2000000);
}
return 0;
}
12. 程序固化与启动
12.1 生成BOOT.BIN
-
在SDK中:
- 右键工程 → Create Boot Image
- 添加:
- FSBL(First Stage Bootloader)
- 硬件比特流文件(.bit)
- 应用程序(.elf)
-
输出文件:
- BOOT.BIN(用于QSPI Flash或SD卡启动)
- image.ub(可选,用于Linux系统)
12.2 QSPI Flash编程
通过SDK编程Flash:
-
连接JTAG调试器
-
在SDK中:
- Xilinx Tools → Program Flash
- 选择BOOT.BIN文件
- 选择Flash型号(如N25Q128)
- 设置偏移地址(通常为0x0)
-
或者使用命令行工具:
bash复制program_flash -f BOOT.bin -offset 0 -flash_type qspi-x4-single -cable type xilinx_tcf url TCP:127.0.0.1:3121
13. 性能优化技巧
13.1 缓存管理
- 关键数据对齐:
c复制// 确保DMA缓冲区是缓存行对齐的(32字节)
__attribute__((aligned(32))) u8 DmaBuffer[1024];
- 手动缓存操作:
c复制// DMA传输前刷新缓存
Xil_DCacheFlushRange((u32)buf, len);
// DMA接收后使缓存无效
Xil_DCacheInvalidateRange((u32)buf, len);
13.2 代码优化
- 使用-O2或-O3优化级别
- 关键函数放在紧耦合内存(TCM)
- 避免浮点运算(使用定点数替代)
14. 常见问题排查
14.1 程序无法启动
现象:上电后无任何输出
排查步骤:
- 检查电源和复位信号
- 确认启动模式设置正确(QSPI/SD)
- 使用JTAG调试器连接,查看PC指针位置
- 检查FSBL是否正常生成
14.2 DMA传输失败
现象:DMA传输卡死或数据错误
解决方案:
- 确认缓存操作正确(Flush/Invalidate)
- 检查DMA缓冲区地址是否对齐
- 验证AXI总线连接是否正确
- 检查中断是否正常触发
14.3 中断不触发
现象:配置了中断但没有响应
排查步骤:
- 确认GIC初始化成功
- 检查中断ID是否正确
- 验证外设中断是否使能
- 确认中断触发条件设置正确
15. 进阶开发建议
- 使用FreeRTOS:在裸机基础上添加实时操作系统
- 开发自定义IP:将复杂算法实现在PL端
- 混合开发:部分核心在裸机运行,其他功能在Linux实现
- 性能分析:使用PMU(Performance Monitor Unit)进行代码剖析
通过本指南的系统学习,开发者可以全面掌握ZYNQ裸机开发的各项关键技术。建议按照实验顺序逐步实践,每个实验都深入理解其工作原理,而不仅仅是复制代码。在实际项目开发中,应根据具体需求选择合适的架构设计,充分发挥ZYNQ软硬件协同的优势。