本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《米联ZYNQ嵌入式开发实战教程》是一套针对Xilinx ZYNQ系列SoC的裸机开发教程,全面讲解了基于ARM Cortex-A9双核处理器的可编程系统芯片开发技术。教程涵盖ZYNQ架构、硬件接口设计、启动流程、裸机编程、设备驱动、JTAG调试、时钟与电源管理、硬件加速、SDK开发环境配置与实验等内容,通过逐步进阶的实验帮助学习者掌握嵌入式开发的核心技能。适用于学生、工程师及嵌入式系统开发者快速入门并深入理解ZYNQ平台开发流程。
米联-ZYNQ裸机篇2019版

1. ZYNQ架构组成与SoC系统解析

ZYNQ系列是Xilinx推出的一款高度集成的SoC(System on Chip)平台,融合了高性能ARM处理器(PS端)与灵活的FPGA可编程逻辑(PL端),实现了软硬件协同的嵌入式系统架构。

其核心由双核ARM Cortex-A9 MPCore构成,支持运行复杂操作系统如Linux,同时PL端提供丰富的可编程资源,实现定制化硬件加速功能。两者通过高速AXI总线互连,实现高效数据通信与资源共享。

这种异构计算架构不仅提升了系统性能,还增强了设计灵活性,广泛应用于工业控制、图像处理、通信系统等领域。

2. PS与PL模块功能详解

ZYNQ SoC的核心架构由两个主要部分组成:Processing System(PS)和Programmable Logic(PL)。PS模块基于ARM Cortex-A9双核处理器,具备完整的嵌入式系统功能,包括内存控制器、缓存、DMA、通用外设等;而PL模块则提供高度灵活的可编程逻辑资源,支持用户自定义逻辑功能、高速接口、算法加速等。两者通过高速互连总线(如AXI总线)实现紧密协作,构成了一个兼具高性能处理和高度可定制性的异构系统平台。本章将深入分析PS与PL各自的模块结构与功能特性,并探讨它们之间的协同工作机制。

2.1 Processing System(PS)模块架构

PS模块是ZYNQ SoC的核心控制单元,负责运行操作系统、执行应用程序、管理外设资源等。其核心组件包括ARM Cortex-A9 MPCore处理器、系统控制器、内存控制器、通用外设接口以及与PL模块的通信接口。

2.1.1 ARM Cortex-A9 MPCore核心架构

ARM Cortex-A9 MPCore是ZYNQ PS模块的核心处理器,采用双核架构设计,支持对称多处理(SMP)和非对称多处理(AMP)模式。每个核心支持NEON SIMD指令集和VFPv3浮点运算单元,具备较高的计算性能和能效比。

  • 架构特点
  • 双核ARM Cortex-A9处理器,主频可达667MHz
  • 支持TrustZone安全扩展
  • L1指令缓存(32KB)和数据缓存(32KB)每核独立
  • 共享L2缓存(512KB)
  • 支持MMU和Cache一致性管理

  • 典型应用场景

  • 实时控制与数据处理
  • 嵌入式Linux系统运行
  • 与PL模块协同实现算法加速
核心寄存器组与执行流程

ARM Cortex-A9采用ARMv7-A架构,具有16个通用寄存器(R0-R15),其中R15为程序计数器(PC)。此外,还包括状态寄存器(CPSR)、中断屏蔽寄存器(IMASK)等关键控制寄存器。

// 示例:使用C语言访问ARM寄存器
#include <stdint.h>

uint32_t get_processor_id(void) {
    uint32_t mpidr;
    __asm volatile("mrc p15, 0, %0, c0, c0, 5" : "=r"(mpidr));
    return mpidr;
}
  • 代码分析
  • 使用内联汇编读取MPIDR寄存器(Multiprocessor Affinity Register),获取当前运行核心的ID。
  • mrc p15, 0, %0, c0, c0, 5 :ARM协处理器指令,读取MPIDR。
  • %0 表示输出变量 mpidr
  • 此类底层操作常用于多核系统调试和任务调度。

2.1.2 通用外设与系统控制器

ZYNQ PS模块集成了多种通用外设,包括:

外设 功能
UART 串口通信
SPI 高速同步通信
I2C 低速同步通信
USB USB2.0主/从设备支持
SDIO SD卡/EMMC接口
CAN 控制器局域网通信
GPIO 通用输入输出接口

系统控制器负责管理这些外设的中断、DMA请求、时钟控制等。例如,中断控制器(GIC)负责将来自PS和PL的中断信号统一处理并分发给CPU核心。

外设初始化示例:UART配置
#include "xuartps.h"

XUartPs_Config *Config;
XUartPs UartInstance;

int main() {
    // 查找配置
    Config = XUartPs_LookupConfig(XPAR_XUARTPS_0_DEVICE_ID);
    // 初始化UART
    XUartPs_CfgInitialize(&UartInstance, Config, Config->BaseAddress);
    // 设置波特率
    XUartPs_SetBaudRate(&UartInstance, 115200);
    // 发送字符串
    char *msg = "Hello ZYNQ UART!\n\r";
    XUartPs_Send(&UartInstance, (u8*)msg, strlen(msg));
    while(1);
}
  • 代码分析
  • 使用Xilinx提供的XUartPs驱动库初始化UART。
  • XUartPs_CfgInitialize :根据配置初始化UART实例。
  • XUartPs_SetBaudRate :设置波特率为115200。
  • XUartPs_Send :发送字符串数据。
  • 此代码可在SDK中编译运行,用于验证UART通信功能。

2.1.3 PS端与PL端的接口机制

PS与PL之间通过AXI总线进行高效通信。ZYNQ支持以下主要AXI接口:

接口类型 描述
AXI GP0/1 通用主接口,用于PL访问PS资源
AXI HP0~3 高性能主接口,用于PL访问DDR内存
AXI ACP 缓存一致接口,用于PL访问L2缓存

这些接口通过寄存器映射实现数据交换。例如,PL模块可以通过AXI HP接口直接访问DDR内存,实现高速数据传输。

PS与PL通信示例:内存读写
#include "xil_io.h"

#define PL_REGISTER_ADDR 0x40000000  // PL寄存器映射地址

int main() {
    u32 data = 0x12345678;
    // 写入PL寄存器
    Xil_Out32(PL_REGISTER_ADDR, data);
    // 读取PL寄存器
    u32 read_data = Xil_In32(PL_REGISTER_ADDR);
    xil_printf("Read data: 0x%x\n\r", read_data);
    while(1);
}
  • 代码分析
  • 使用 Xil_Out32 Xil_In32 函数实现对PL寄存器的读写操作。
  • PL_REGISTER_ADDR 为PL模块在PS地址空间的映射地址。
  • 该方式常用于PS控制PL逻辑或读取PL状态。

2.2 Programmable Logic(PL)模块功能

PL模块是ZYNQ SoC的可编程逻辑部分,由FPGA架构组成,具备高度灵活性和并行处理能力。它可用于实现高速接口、自定义外设、数字信号处理、图像处理等复杂逻辑功能。

2.2.1 可编程逻辑资源与逻辑单元

ZYNQ中的PL模块基于Xilinx 7系列FPGA架构,主要资源包括:

  • 可配置逻辑块(CLB) :实现组合逻辑和时序逻辑。
  • 分布式RAM(LUTRAM) :用于小型存储结构。
  • 块RAM(BRAM) :用于大容量数据缓存。
  • DSP Slice :实现高速乘法、加法等算术运算。
  • IOB :实现引脚配置与电气特性控制。
逻辑单元结构示意图
graph TD
    A[Input Signals] --> B(LUT)
    B --> C[Flip-Flop]
    C --> D[Output]
    E[Carry Chain] --> C
  • 流程图说明
  • 输入信号进入LUT(查找表),执行组合逻辑。
  • 通过Flip-Flop实现时序控制。
  • Carry Chain用于快速进位链,提升算术运算性能。

2.2.2 PL端的时钟管理与I/O配置

ZYNQ PL模块支持多个时钟管理单元(MMCM/PLL),可用于生成多种频率的时钟信号,实现跨时钟域同步与频率合成。

时钟管理配置流程
  1. 使用Clocking Wizard IP核生成所需时钟。
  2. 将时钟信号连接至PL逻辑模块。
  3. 在Vivado中进行时序约束与优化。
// Verilog示例:时钟分频模块
module clock_divider(
    input      clk_in,
    input      rst_n,
    output reg clk_out
);

parameter DIV = 50_000_000;  // 分频系数

reg [31:0] counter;

always @(posedge clk_in or negedge rst_n) begin
    if (!rst_n) begin
        counter <= 0;
        clk_out <= 0;
    end else if (counter == DIV - 1) begin
        counter <= 0;
        clk_out <= ~clk_out;
    end else begin
        counter <= counter + 1;
    end
end

endmodule
  • 代码分析
  • 输入时钟 clk_in ,输出分频后的时钟 clk_out
  • 使用计数器实现分频功能。
  • 该模块可用于生成低频控制信号或驱动外部设备。

2.2.3 高速接口与逻辑功能实现

PL模块支持多种高速接口协议,如PCIe、Ethernet、DDR、LVDS、SPI4.2等。用户可基于这些接口实现高速数据传输、图像采集、网络通信等功能。

高速DDR接口实现示例
// DDR控制器IP核配置参数示例
parameter MEM_ADDR_WIDTH = 13;
parameter MEM_DATA_WIDTH = 32;

module ddr_interface(
    input         clk,
    input         rst_n,
    input         en,
    input [2:0]   cmd,
    input [MEM_ADDR_WIDTH-1:0] addr,
    input [MEM_DATA_WIDTH-1:0] din,
    output [MEM_DATA_WIDTH-1:0] dout
);

// 实例化DDR控制器IP核
ddr3_ctrl u_ddr3_ctrl (
    .clk(clk),
    .rst_n(rst_n),
    .en(en),
    .cmd(cmd),
    .addr(addr),
    .din(din),
    .dout(dout)
);

endmodule
  • 代码分析
  • 模块封装DDR3控制器接口。
  • cmd 控制读写操作, addr 为地址, din/dout 为数据输入输出。
  • 该模块可用于高速数据缓存、图像处理、数据采集等场景。

2.3 PS与PL的协同工作机制

PS与PL的协同是ZYNQ系统设计的关键,通过AXI总线实现数据交换与功能协同,从而构建高性能嵌入式系统。

2.3.1 AXI总线协议与通信机制

AXI(Advanced eXtensible Interface)是ARM公司提出的一种高性能、高带宽、低延迟的片上总线协议,广泛应用于ZYNQ系统中。

AXI协议特性
特性 描述
地址/数据分离 支持独立地址通道和数据通道
支持突发传输 提高数据传输效率
支持非对齐传输 灵活的内存访问方式
支持多种响应类型 保证数据完整性
AXI通信流程图
sequenceDiagram
    participant PS
    participant AXI
    participant PL

    PS->>AXI: 发送读写请求
    AXI->>PL: 传递请求
    PL->>AXI: 响应数据或确认
    AXI->>PS: 返回结果
  • 流程说明
  • PS发起对PL的访问请求。
  • AXI总线负责地址和数据的传输。
  • PL响应请求,返回数据或状态。
  • PS根据响应进行后续处理。

2.3.2 数据交互与资源访问

在ZYNQ系统中,PS与PL之间的数据交互可以通过以下方式实现:

  • 寄存器映射 :通过AXI GP接口访问PL寄存器,实现控制与状态读取。
  • DMA传输 :使用DMA引擎在PL与DDR之间高速传输数据。
  • 共享内存 :通过AXI HP接口实现PL与PS共享内存区域。
使用DMA进行PL到DDR的数据传输
#include "xaxidma.h"

XAxiDma AxiDma;

int setup_dma() {
    XAxiDma_Config *Config = XAxiDma_LookupConfig(XPAR_AXIDMA_0_DEVICE_ID);
    int Status = XAxiDma_CfgInitialize(&AxiDma, Config);
    if (Status != XST_SUCCESS) return XST_FAILURE;

    // 设置DMA传输地址与长度
    u32 *src_buffer = (u32 *)0x10000000;  // PL输出数据地址
    u32 *dst_buffer = (u32 *)0x30000000;  // DDR内存地址
    XAxiDma_SimpleTransfer(&AxiDma, (u32)src_buffer, 1024, XAXIDMA_DEVICE_TO_DMA);
    XAxiDma_SimpleTransfer(&AxiDma, (u32)dst_buffer, 1024, XAXIDMA_DMA_TO_DEVICE);

    return XST_SUCCESS;
}
  • 代码分析
  • 使用XAxiDma驱动库初始化DMA通道。
  • XAxiDma_SimpleTransfer 用于启动DMA传输。
  • 该方式适用于PL与DDR之间的高速数据搬运,如图像采集、传感器数据传输等。

2.3.3 实现高效系统级集成的策略

为了充分发挥ZYNQ的性能优势,PS与PL的协同应遵循以下策略:

  • 功能划分合理 :将实时性强、并行度高的任务交给PL,控制逻辑和系统管理交给PS。
  • 通信接口优化 :合理使用AXI GP、AXI HP、AXI ACP接口,提升数据传输效率。
  • 资源隔离与同步 :避免多个模块访问共享资源时的数据冲突,使用锁机制或DMA乒乓缓存。
  • 系统调试与监控 :利用Xilinx提供的System Debugger、Performance Monitor Unit(PMU)等工具进行性能分析与优化。
PS与PL协同设计流程表
步骤 内容
1 功能划分与模块定义
2 Vivado中设计PL逻辑并生成BIT流
3 SDK中编写PS控制程序
4 配置AXI接口与地址映射
5 联合仿真与硬件验证
6 性能分析与优化

通过上述策略与流程,开发者可以构建出高性能、高可靠性的ZYNQ系统,广泛应用于工业控制、智能图像处理、通信系统等领域。

3. GPIO、SPI、I2C、UART等外设接口编程

在嵌入式系统开发中,外设接口的编程能力是衡量开发者技能的重要标准之一。ZYNQ平台集成了丰富的外设资源,包括GPIO、SPI、I2C和UART等常见接口。本章将围绕这些接口展开深入讲解,从寄存器操作层面到实际应用案例,逐步构建开发者对ZYNQ外设编程的全面理解。

3.1 通用输入输出(GPIO)编程

GPIO是嵌入式系统中最基础也是最常用的外设之一,能够实现对引脚状态的控制与读取。ZYNQ平台的GPIO模块支持多个Bank,每个Bank可独立配置为输入或输出模式,并支持中断触发。

3.1.1 GPIO寄存器配置与操作流程

ZYNQ中的GPIO模块通过内存映射寄存器进行访问,主要涉及以下关键寄存器:

寄存器名称 地址偏移 功能描述
DATA_RO 0x000 只读寄存器,读取当前引脚状态
DATA 0x004 写入输出数据
DIRECTION 0x008 设置引脚方向(输入/输出)
INTERRUPT_MASK 0x00C 中断屏蔽寄存器
INTERRUPT_STATUS 0x010 中断状态寄存器

操作流程如下:

  1. 映射GPIO物理地址到虚拟内存空间
  2. 设置引脚方向: 通过 DIRECTION 寄存器设置为输入或输出
  3. 读写引脚状态: 使用 DATA_RO DATA 寄存器进行读写
  4. 中断配置(可选): 配置中断屏蔽与触发方式,注册中断服务程序

3.1.2 输入/输出模式设置与中断处理

#include "xil_io.h"

#define GPIO_BASE_ADDR 0xE000A000  // 假设GPIO模块基地址为0xE000A000

void gpio_init_output() {
    Xil_Out32(GPIO_BASE_ADDR + 0x008, 0xFFFFFFFF); // 设置为输出模式
}

void gpio_set_high(int pin) {
    u32 data = Xil_In32(GPIO_BASE_ADDR + 0x004);
    data |= (1 << pin);  // 设置指定引脚为高电平
    Xil_Out32(GPIO_BASE_ADDR + 0x004, data);
}

void gpio_set_low(int pin) {
    u32 data = Xil_In32(GPIO_BASE_ADDR + 0x004);
    data &= ~(1 << pin);  // 设置指定引脚为低电平
    Xil_Out32(GPIO_BASE_ADDR + 0x004, data);
}

代码逻辑分析:

  • Xil_Out32() :用于向指定地址写入32位数据,常用于寄存器操作。
  • Xil_In32() :用于从指定地址读取32位数据。
  • DIRECTION 寄存器设置为 0xFFFFFFFF ,表示所有引脚设为输出。
  • DATA 寄存器用于设置输出电平,通过位操作控制单个引脚状态。

3.1.3 简单LED控制实验

以LED控制为例,使用GPIO控制LED的亮灭。实验步骤如下:

  1. 初始化GPIO为输出模式
  2. 控制LED引脚高低电平变化
  3. 使用延时函数实现闪烁效果
void delay_ms(int ms) {
    // 实现毫秒级延时
    for(int i=0; i<ms*1000; i++);
}

int main() {
    gpio_init_output();
    while(1) {
        gpio_set_high(0);  // LED亮
        delay_ms(500);
        gpio_set_low(0);   // LED灭
        delay_ms(500);
    }
}

该程序通过不断切换LED引脚电平,实现LED每500ms闪烁一次。

3.2 同步串行接口(SPI)通信

SPI(Serial Peripheral Interface)是一种高速同步串行通信协议,常用于连接ADC、Flash、传感器等外设。

3.2.1 SPI协议原理与接口结构

SPI协议使用四根信号线:
- SCLK(Serial Clock) :时钟信号,由主设备发出
- MOSI(Master Out Slave In) :主设备发送,从设备接收
- MISO(Master In Slave Out) :从设备发送,主设备接收
- SS(Slave Select) :片选信号,选择从设备

3.2.2 主模式与从模式配置

ZYNQ的SPI控制器支持主从两种模式。主模式下,ZYNQ作为主设备发起通信;从模式下则响应外部主设备请求。

#include "xil_io.h"

#define SPI_BASE_ADDR 0xE0006000

void spi_init_master() {
    Xil_Out32(SPI_BASE_ADDR + 0x00, 0x00000000); // 控制寄存器清零
    Xil_Out32(SPI_BASE_ADDR + 0x04, 0x0000000F); // 设置主模式,时钟分频
    Xil_Out32(SPI_BASE_ADDR + 0x08, 0x00000000); // 数据长度为8位
    Xil_Out32(SPI_BASE_ADDR + 0x00, 0x00000002); // 启用SPI
}

代码分析:

  • 0x00 :控制寄存器,用于启用SPI、设置主/从模式。
  • 0x04 :波特率控制寄存器,决定SPI时钟频率。
  • 0x08 :数据长度寄存器,通常设为8位(标准SPI)。

3.2.3 基于SPI的ADC数据读取实例

以ADS7846 ADC芯片为例,读取模拟输入值:

u16 spi_read_adc(int channel) {
    u8 tx_data[2], rx_data[2];
    tx_data[0] = (0x80 | (channel << 4)); // 启动位 + 通道选择
    tx_data[1] = 0x00;

    // 启动片选
    Xil_Out32(SPI_BASE_ADDR + 0x10, 0x00); // SS低电平
    Xil_Out32(SPI_BASE_ADDR + 0x0C, tx_data[0]);
    while(!(Xil_In32(SPI_BASE_ADDR + 0x28) & 0x01)); // 等待发送完成
    Xil_Out32(SPI_BASE_ADDR + 0x0C, tx_data[1]);
    while(!(Xil_In32(SPI_BASE_ADDR + 0x28) & 0x01));

    rx_data[0] = Xil_In32(SPI_BASE_ADDR + 0x0C);
    rx_data[1] = Xil_In32(SPI_BASE_ADDR + 0x0C);

    // 结束片选
    Xil_Out32(SPI_BASE_ADDR + 0x10, 0x01); // SS高电平

    return (rx_data[0] << 8) | rx_data[1];
}

逻辑分析:

  • 通过 SS 信号控制从设备通信开始与结束
  • 发送控制字节选择通道
  • 两次发送数据后,读取两个字节的ADC结果
  • 最终返回16位数据,表示模拟电压值

3.3 I2C总线接口编程

I2C(Inter-Integrated Circuit)是一种广泛使用的双线同步串行通信协议,常用于连接EEPROM、传感器等低速设备。

3.3.1 I2C协议规范与信号时序

I2C使用两条信号线:

  • SDA(Serial Data Line) :数据线
  • SCL(Serial Clock Line) :时钟线

通信过程包括起始信号、地址发送、数据传输、应答位(ACK/NACK)和停止信号。

3.3.2 I2C控制器初始化与数据传输

ZYNQ的I2C控制器通过寄存器编程实现数据传输。以下为初始化代码:

#define I2C_BASE_ADDR 0xE0004000

void i2c_init() {
    Xil_Out32(I2C_BASE_ADDR + 0x00, 0x00000006); // 设置为标准模式(100kHz)
    Xil_Out32(I2C_BASE_ADDR + 0x20, 0x00000001); // 清除中断标志
    Xil_Out32(I2C_BASE_ADDR + 0x04, 0x00000001); // 启用I2C
}

3.3.3 读写EEPROM芯片实战

以AT24C02 EEPROM芯片为例,实现读写操作:

void i2c_write_byte(u8 dev_addr, u8 reg_addr, u8 data) {
    Xil_Out32(I2C_BASE_ADDR + 0x10, dev_addr << 1); // 发送地址+写标志
    while(!(Xil_In32(I2C_BASE_ADDR + 0x20) & 0x80)); // 等待ACK
    Xil_Out32(I2C_BASE_ADDR + 0x10, reg_addr);      // 写寄存器地址
    Xil_Out32(I2C_BASE_ADDR + 0x10, data);           // 写数据
    Xil_Out32(I2C_BASE_ADDR + 0x14, 0x00000001);     // 发送停止信号
}

u8 i2c_read_byte(u8 dev_addr, u8 reg_addr) {
    u8 data;
    Xil_Out32(I2C_BASE_ADDR + 0x10, dev_addr << 1); // 写地址
    Xil_Out32(I2C_BASE_ADDR + 0x10, reg_addr);      // 写寄存器地址
    Xil_Out32(I2C_BASE_ADDR + 0x10, (dev_addr << 1)|1); // 切换到读模式
    data = Xil_In32(I2C_BASE_ADDR + 0x10);           // 读取数据
    Xil_Out32(I2C_BASE_ADDR + 0x14, 0x00000001);     // 发送停止信号
    return data;
}

流程分析:

  • 写入设备地址和寄存器地址,发送数据或读取数据
  • 使用寄存器 0x14 控制发送停止信号
  • 读取数据后需发送NACK表示结束

3.4 异步串行通信接口(UART)

UART(Universal Asynchronous Receiver/Transmitter)是一种异步串行通信接口,广泛用于串口通信和调试。

3.4.1 UART通信参数设置与波特率计算

UART通信参数包括:
- 波特率(Baud Rate)
- 数据位(Data Bits)
- 停止位(Stop Bits)
- 校验位(Parity)

以ZYNQ UART为例,配置波特率为115200:

#define UART_BASE_ADDR 0xE0001000

void uart_init() {
    Xil_Out32(UART_BASE_ADDR + 0x00, 0x00000083); // 使能除数锁存
    Xil_Out32(UART_BASE_ADDR + 0x04, 0x00000001); // 除数低位
    Xil_Out32(UART_BASE_ADDR + 0x08, 0x00000000); // 除数高位
    Xil_Out32(UART_BASE_ADDR + 0x00, 0x00000003); // 设置8位数据,1位停止,无校验
    Xil_Out32(UART_BASE_ADDR + 0x0C, 0x00000001); // 启用FIFO
}

波特率计算公式:

Baud Rate = (Reference Clock) / (16 * Divisor)

假设系统时钟为50MHz,则:

Divisor = 50000000 / (16 * 115200) ≈ 27

3.4.2 数据发送与接收中断处理

void uart_putc(char c) {
    while((Xil_In32(UART_BASE_ADDR + 0x14) & 0x20) == 0); // 等待发送缓冲空
    Xil_Out32(UART_BASE_ADDR + 0x00, c);
}

char uart_getc() {
    while((Xil_In32(UART_BASE_ADDR + 0x14) & 0x01) == 0); // 等待接收缓冲满
    return Xil_In32(UART_BASE_ADDR + 0x00);
}

中断处理流程:

  1. 配置中断控制器(GIC)
  2. 注册中断服务程序(ISR)
  3. 在ISR中读取UART中断状态寄存器
  4. 处理接收或发送完成中断

3.4.3 串口调试助手开发示例

开发一个简单的串口回显程序,接收字符并回显:

void uart_echo() {
    char c;
    while(1) {
        c = uart_getc();
        uart_putc(c);
    }
}

该程序持续等待字符输入,并将其原样返回,适用于调试和交互式应用。

总结

本章详细介绍了ZYNQ平台上GPIO、SPI、I2C和UART四种常见外设的编程方法。从寄存器级别的操作到实际应用案例,展示了如何在裸机环境下进行外设控制与通信。下一章将深入讲解如何利用VHDL/Verilog语言在FPGA中实现自定义接口,进一步拓展ZYNQ的可编程能力。

4. 基于VHDL/Verilog的自定义接口设计

在ZYNQ平台上,FPGA部分(PL)的强大可编程能力使其能够灵活实现各种定制化外设接口。通过使用硬件描述语言如VHDL或Verilog,开发者可以构建与PS端无缝对接的自定义接口,从而扩展系统的功能边界。本章将围绕VHDL与Verilog语言基础、自定义接口开发流程,以及PS与PL协同设计实例展开详细讲解,帮助开发者掌握从逻辑设计到系统集成的完整路径。

4.1 FPGA硬件描述语言基础

FPGA的开发离不开硬件描述语言(HDL),其中VHDL和Verilog是最为常见的两种语言。它们在语法结构和设计风格上各有特色,适用于不同类型的开发需求。

4.1.1 VHDL与Verilog语法结构对比

VHDL(VHSIC Hardware Description Language)源自Ada语言,语法严谨、类型强,适用于大型系统设计和工业级应用。Verilog则源自C语言风格,语法简洁,学习门槛较低,广泛应用于学术研究和原型设计。

特性 VHDL Verilog
类型系统 强类型 弱类型
语法风格 类似Ada,结构清晰 类似C,灵活简洁
代码可读性 高,适合大型项目 中等,适合快速原型开发
工具支持 支持广泛,尤其在军工和航天领域 广泛支持,尤其在学术和消费电子领域
示例代码:VHDL与Verilog实现D触发器对比

VHDL实现:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;

entity d_flipflop is
port (
    clk : in std_logic;
    d   : in std_logic;
    q   : out std_logic
);
end entity;

architecture Behavioral of d_flipflop is
begin
    process(clk)
    begin
        if rising_edge(clk) then
            q <= d;
        end if;
    end process;
end architecture;

Verilog实现:

module d_flipflop (
    input      clk,
    input      d,
    output reg q
);

always @(posedge clk) begin
    q <= d;
end

endmodule
代码逻辑分析:
  • VHDL代码 :定义了一个D触发器实体,使用 process 块在 clk 上升沿时将输入 d 赋值给输出 q
  • Verilog代码 :同样实现D触发器功能,使用 always 块监听 clk 的上升沿,并在触发时更新 q
  • 参数说明
  • clk :时钟信号,用于触发数据更新。
  • d :数据输入。
  • q :数据输出。

4.1.2 模块化设计与行为建模

在FPGA开发中,模块化设计是构建复杂系统的关键。通过将功能模块封装为独立单元,可以提高代码的可读性和可复用性。

模块化设计流程图(Mermaid):
graph TD
    A[顶层设计] --> B[模块1]
    A --> C[模块2]
    A --> D[模块3]
    B --> E[子模块1]
    C --> F[子模块2]
    D --> G[子模块3]
示例:模块化设计实现计数器组合

顶层模块:

module top_counter (
    input      clk,
    input      rst,
    output [7:0] count
);

wire [3:0] count1, count2;

counter_4bit u1 (.clk(clk), .rst(rst), .count(count1));
counter_4bit u2 (.clk(clk), .rst(rst), .count(count2));

assign count = {count1, count2};

endmodule

子模块:4位计数器

module counter_4bit (
    input      clk,
    input      rst,
    output reg [3:0] count
);

always @(posedge clk or posedge rst) begin
    if (rst)
        count <= 4'b0;
    else
        count <= count + 1;
end

endmodule
代码分析:
  • top_counter 是顶层模块,组合两个4位计数器形成8位计数器。
  • 子模块 counter_4bit 实现了4位同步计数器逻辑,支持复位功能。
  • 使用 assign 语句将两个4位输出拼接为8位输出。

4.1.3 仿真与综合流程

FPGA开发流程通常包括设计、仿真、综合、实现和下载等步骤。仿真用于验证逻辑功能,综合则将HDL代码转换为门级网表。

仿真与综合流程图(Mermaid):
graph LR
    A[设计输入] --> B[功能仿真]
    B --> C[综合]
    C --> D[布局布线]
    D --> E[生成BIT文件]
    E --> F[下载到FPGA]
示例:使用ModelSim进行功能仿真

Testbench代码(Verilog):

module tb_d_flipflop;

reg clk, d;
wire q;

d_flipflop uut (
    .clk(clk),
    .d(d),
    .q(q)
);

initial begin
    clk = 0;
    forever #5 clk = ~clk; // 10ns周期时钟
end

initial begin
    d = 0;
    #10 d = 1;
    #20 d = 0;
    #30 $finish;
end

endmodule
代码逻辑分析:
  • clk 每10ns翻转一次,模拟时钟信号。
  • 在时钟上升沿改变 d 的值,观察 q 的响应。
  • 使用 $finish 结束仿真。

4.2 自定义外设接口开发

在ZYNQ系统中,PL端可通过AXI接口与PS端通信。通过设计自定义外设接口,开发者可以实现如寄存器访问、数据采集等功能。

4.2.1 接口协议设计与状态机实现

状态机是实现复杂协议控制的有效手段。以自定义寄存器读写接口为例,状态机可以管理地址解码、数据传输和控制信号生成。

状态机设计流程图(Mermaid):
stateDiagram
    [*] --> IDLE
    IDLE --> ADDR_DECODE : 地址匹配
    ADDR_DECODE --> READ_DATA : 读请求
    ADDR_DECODE --> WRITE_DATA : 写请求
    READ_DATA --> IDLE : 传输完成
    WRITE_DATA --> IDLE : 传输完成
示例代码:Verilog实现状态机控制寄存器读写
module reg_interface (
    input      clk,
    input      rst,
    input      wr_en,
    input      rd_en,
    input [7:0] addr,
    input [31:0] wdata,
    output reg [31:0] rdata,
    output reg       ack
);

typedef enum {IDLE, ADDR_DECODE, READ, WRITE} state_t;
state_t current_state, next_state;

reg [31:0] reg0, reg1;

always @(posedge clk or posedge rst) begin
    if (rst) begin
        current_state <= IDLE;
        reg0 <= 32'h0;
        reg1 <= 32'h0;
        rdata <= 32'h0;
        ack <= 1'b0;
    end else begin
        current_state <= next_state;
        case(current_state)
            IDLE: ;
            ADDR_DECODE: begin
                if (wr_en && addr == 8'h00)
                    reg0 <= wdata;
                else if (wr_en && addr == 8'h04)
                    reg1 <= wdata;
                if (rd_en && addr == 8'h00)
                    rdata <= reg0;
                else if (rd_en && addr == 8'h04)
                    rdata <= reg1;
                ack <= 1'b1;
            end
            default: ;
        endcase
    end
end

always @(*) begin
    case(current_state)
        IDLE: next_state = (wr_en || rd_en) ? ADDR_DECODE : IDLE;
        ADDR_DECODE: next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

endmodule
代码分析:
  • 定义了四个状态:IDLE、ADDR_DECODE、READ、WRITE(简化为仅ADDR_DECODE)。
  • 根据地址选择读写 reg0 reg1
  • ack 信号用于指示操作完成。
  • 支持8位地址空间, reg0 地址为0x00, reg1 地址为0x04。

4.2.2 地址映射与AXI接口集成

在ZYNQ系统中,PL端的外设通常通过AXI-Lite接口与PS端连接。开发者需在Vivado中配置AXI接口,并定义地址映射。

AXI接口集成流程图(Mermaid):
graph LR
    A[自定义模块] --> B[AXI接口包装]
    B --> C[添加到ZYNQ系统]
    C --> D[分配地址空间]
    D --> E[导出SDK使用]
地址映射示例(Vivado Block Design):
外设名称 基地址 地址范围
REG_IF 0x43C00000 0x43C0FFFF
ADC_IF 0x43D00000 0x43D0FFFF

4.2.3 自定义寄存器组的实现

通过自定义寄存器组,开发者可以实现控制寄存器、状态寄存器、数据寄存器等多种功能模块。

示例:实现一个带状态寄存器的接口
module reg_iface_with_status (
    input      clk,
    input      rst,
    input      wr_en,
    input      rd_en,
    input [7:0] addr,
    input [31:0] wdata,
    output reg [31:0] rdata,
    output reg       ack
);

reg [31:0] ctrl_reg, data_reg, status_reg;

always @(posedge clk or posedge rst) begin
    if (rst) begin
        ctrl_reg <= 32'h0;
        data_reg <= 32'h0;
        status_reg <= 32'h1; // 状态初始化为1
        rdata <= 32'h0;
        ack <= 1'b0;
    end else begin
        case(addr)
            8'h00: begin
                if (wr_en)
                    ctrl_reg <= wdata;
                if (rd_en)
                    rdata <= ctrl_reg;
            end
            8'h04: begin
                if (wr_en)
                    data_reg <= wdata;
                if (rd_en)
                    rdata <= data_reg;
            end
            8'h08: begin
                if (rd_en)
                    rdata <= status_reg;
            end
        endcase
        ack <= 1'b1;
    end
end

endmodule
代码逻辑分析:
  • 定义三个寄存器: ctrl_reg (控制寄存器)、 data_reg (数据寄存器)、 status_reg (状态寄存器)。
  • 通过地址 0x00 0x04 0x08 分别访问对应寄存器。
  • 支持读写操作,并通过 ack 信号确认操作完成。

4.3 综合应用:PS与PL协同接口设计

在ZYNQ系统中,PS与PL的协同设计是实现高性能嵌入式系统的关键。本节将介绍如何从PS端访问PL端的自定义寄存器,以及如何利用FPGA实现高速数据采集和实时控制逻辑。

4.3.1 从PS端访问PL自定义寄存器

在SDK中,可以通过内存映射的方式访问PL端寄存器。开发者需使用 Xil_Out32 Xil_In32 函数进行寄存器读写。

示例代码(C语言):
#include "xil_io.h"

#define REG_BASE_ADDR 0x43C00000

void write_register(u32 offset, u32 value) {
    Xil_Out32(REG_BASE_ADDR + offset, value);
}

u32 read_register(u32 offset) {
    return Xil_In32(REG_BASE_ADDR + offset);
}

int main() {
    write_register(0x00, 0x12345678); // 写入控制寄存器
    u32 val = read_register(0x00);     // 读取控制寄存器
    xil_printf("Read value: 0x%x\r\n", val);
    return 0;
}
参数说明:
  • REG_BASE_ADDR :自定义外设在地址空间中的起始地址。
  • offset :相对于基地址的偏移量。
  • value :要写入的数据。

4.3.2 利用FPGA实现高速数据采集接口

FPGA适合实现高速并行处理任务。以下是一个基于PL端的ADC数据采集接口设计。

系统结构图(Mermaid):
graph LR
    A[ADC输入] --> B[FPGA采集模块]
    B --> C[ZYNQ PS端处理]
示例代码:ADC采样控制逻辑(Verilog)
module adc_sampler (
    input      clk,
    input      rst,
    input [11:0] adc_data,
    output reg [31:0] sample_data,
    output reg       valid
);

reg [15:0] counter;

always @(posedge clk or posedge rst) begin
    if (rst) begin
        counter <= 0;
        valid <= 0;
    end else begin
        if (counter == 16'd10000) begin
            sample_data <= {20'd0, adc_data}; // 12位ADC数据填充到32位
            valid <= 1;
            counter <= 0;
        end else begin
            valid <= 0;
            counter <= counter + 1;
        end
    end
end

endmodule
代码分析:
  • 使用计数器控制采样频率,每10000个时钟周期采集一次。
  • 将12位ADC数据填充到32位 sample_data 中。
  • valid 信号用于指示数据有效。

4.3.3 实时数据处理与反馈控制逻辑设计

在一些实时控制场景中,FPGA可直接执行控制逻辑,而无需PS介入,从而实现低延迟响应。

实时控制逻辑流程图(Mermaid):
graph LR
    A[传感器输入] --> B[比较器]
    B --> C[PID控制器]
    C --> D[输出控制信号]
示例:基于比较器的简单控制逻辑(Verilog)
module simple_controller (
    input      clk,
    input [15:0] sensor_in,
    output reg [15:0] control_out
);

parameter THRESHOLD = 16'h8000;

always @(posedge clk) begin
    if (sensor_in > THRESHOLD)
        control_out <= 16'h0001;
    else
        control_out <= 16'h0000;
end

endmodule
代码分析:
  • 当传感器输入超过阈值时,输出控制信号 control_out 置为1。
  • 否则输出为0。
  • 可扩展为更复杂的PID控制逻辑。

本章从硬件描述语言的基础入手,逐步深入到接口设计与系统集成,展示了如何利用FPGA实现灵活的自定义外设接口,并与ZYNQ的PS端协同工作。下一章将介绍ZYNQ的启动流程与Bootloader配置,进一步拓展系统级开发能力。

5. ZYNQ启动流程与Bootloader配置

ZYNQ系列SoC平台的启动流程是嵌入式系统开发中的关键环节。该流程决定了处理器如何初始化硬件、加载固件以及最终启动操作系统或裸机程序。本章将深入解析ZYNQ的启动机制,包括启动源选择、BootROM阶段、FSBL(First Stage Bootloader)与SSBL(Second Stage Bootloader)的分工与实现,以及Linux操作系统启动的基础流程。同时,还将详细说明如何配置和定制FSBL,使其支持PL端的BIT流加载,并探讨U-Boot作为SSBL的使用方式,以及裸机环境下自定义Bootloader的开发实践。

5.1 ZYNQ启动模式与启动顺序

ZYNQ-7000系列SoC的启动过程是高度可配置的,支持多种启动源,如QSPI Flash、NAND Flash、SD卡、JTAG调试接口等。不同的启动模式适用于不同的应用场景和开发阶段。

5.1.1 启动源选择(QSPI、SD卡、JTAG等)

ZYNQ的启动模式由硬件引脚MIO[2:0](Boot Mode Select)决定。下表列出了不同启动模式对应的MIO引脚配置:

启动模式 MIO[2] MIO[1] MIO[0] 描述
JTAG 0 0 0 通过JTAG接口进行调试加载
QSPI 0 1 1 从外部QSPI Flash启动
NAND 1 0 0 从NAND Flash启动
SD卡 0 1 0 从SD卡启动
Reserved 1 0 1 保留模式
USB 1 1 0 从USB接口启动(部分型号支持)

例如,在嵌入式产品中,QSPI Flash是一种常见选择,因其具有非易失性、快速读取速度和成本较低的优点。

5.1.2 BootROM阶段与FSBL加载机制

ZYNQ的启动流程分为几个阶段:

  1. BootROM阶段 :这是芯片出厂时固化的一段代码,位于On-Chip Memory中。其主要任务是:
    - 检测启动模式
    - 初始化基本的时钟和DDR控制器
    - 从指定启动设备中加载FSBL(通常为 fsbl.elf

  2. FSBL(First Stage Bootloader)阶段
    - FSBL是用户可定制的启动阶段,通常由Xilinx SDK工具生成。
    - 其主要职责包括:

    • 初始化DDR内存
    • 加载PL端的BIT流(如需)
    • 将SSBL(如U-Boot)或Linux内核加载到内存中
    • 跳转执行SSBL或内核
  3. SSBL(Second Stage Bootloader)阶段
    - SSBL可以是U-Boot、裸机应用程序或其他引导程序。
    - 如果是U-Boot,则负责进一步加载Linux内核并启动它。

5.1.3 SSBL与用户程序启动流程

在ZYNQ系统中,SSBL(Second Stage Bootloader)通常扮演引导操作系统或用户程序的角色。常见的SSBL包括U-Boot和裸机程序。U-Boot是一个开源的Bootloader,广泛用于嵌入式Linux系统中。

SSBL的启动流程如下:

  1. FSBL将U-Boot镜像加载到DDR内存中。
  2. FSBL跳转执行U-Boot。
  3. U-Boot初始化外设(如以太网、USB、LCD等)。
  4. U-Boot读取内核镜像(如 uImage )和设备树( devicetree.dtb )。
  5. U-Boot将内核加载到内存并跳转执行,从而启动Linux系统。

如果是裸机应用,SSBL可以是用户自己编写的Bootloader程序,用于直接跳转到主程序入口。

5.2 FSBL(First Stage Bootloader)配置

FSBL是ZYNQ启动流程中的核心组件之一。它不仅负责初始化系统硬件,还承担着PL端BIT流加载的任务。本节将详细介绍FSBL的生成、功能分析以及如何修改FSBL以实现PL端配置。

5.2.1 FSBL生成与功能分析

在Xilinx SDK中,可以通过以下步骤生成FSBL:

  1. 创建一个新的“Application Project”。
  2. 在模板选择中选择“Zynq FSBL”。
  3. SDK将自动生成FSBL工程,包含启动代码、DDR初始化、PL加载逻辑等。

FSBL的主要功能包括:

  • 初始化ARM Cortex-A9处理器核心
  • 初始化系统时钟与DDR控制器
  • 检查启动设备(如QSPI Flash或SD卡)
  • 从启动设备中读取BIT流文件并加载到PL端
  • 加载SSBL或内核镜像到内存
  • 设置启动参数并跳转执行SSBL

5.2.2 修改FSBL实现PL端配置

默认的FSBL会尝试从启动设备中加载PL的BIT流文件。如果需要在启动时加载特定的BIT文件,可以修改FSBL的代码。

例如,在 fsbl.c 中,可以找到以下关键代码段:

if (PlCfgData != NULL) {
    Status = LoadPlBitStream(PlCfgData, PlCfgDataLen);
    if (Status != XST_SUCCESS) {
        FsblPrint(DEBUG_INFO, "PL Bitstream Load Failed\n\r");
        FsblFallbackError();
    }
}

这段代码用于加载PL端的BIT流文件。如果要指定加载路径或BIT文件名,可以在SDK中配置 pl.bit 的路径,或者在FSBL中添加自定义逻辑。

5.2.3 加载自定义BIT流文件

加载BIT流的过程通常包括以下几个步骤:

  1. BIT流文件格式 :ZYNQ支持通过FSBL加载标准的BIT文件或BIN文件。通常使用 bootgen 工具将BIT文件转换为BIN格式,并打包进启动镜像中。

  2. 文件打包 :使用 bootgen 工具将FSBL、BIT文件和SSBL打包为一个启动镜像( .bin 文件),例如:

bootgen -arch zynq -image boot.bif -o -bin BOOT.bin

其中, boot.bif 文件内容如下:

the_ROM_image:
{
    [bootloader]fsbl.elf
    pl.bit
    u-boot.elf
}
  1. 加载BIT流 :FSBL在运行时会解析该镜像,并将BIT流加载到PL端的配置寄存器中,完成FPGA功能的初始化。

5.3 多阶段引导与操作系统启动

在ZYNQ平台上,多阶段引导机制确保了从硬件初始化到操作系统启动的完整流程。本节将重点介绍U-Boot作为SSBL的配置与使用、Linux内核的启动流程,以及裸机环境下自定义Bootloader的开发方法。

5.3.1 U-Boot作为SSBL的配置与使用

U-Boot(Das U-Boot)是一个广泛使用的嵌入式Bootloader,支持多种处理器架构和硬件平台。在ZYNQ系统中,U-Boot常用于加载Linux内核并提供调试接口。

配置U-Boot的步骤如下:

  1. 获取U-Boot源码
git clone https://github.com/Xilinx/u-boot-xlnx.git
  1. 配置目标平台
make zynq_zed_config
  1. 编译U-Boot镜像
make
  1. 生成U-Boot ELF文件

编译完成后,生成的 u-boot 文件是一个ELF格式的可执行文件,可以直接由FSBL加载执行。

  1. 配置启动参数

U-Boot支持命令行参数配置,如内核地址、设备树地址、根文件系统路径等。可以在U-Boot中设置环境变量,例如:

setenv kernel_addr_r 0x8000
setenv fdt_addr_r 0x2000000
setenv ramdisk_addr_r 0x3000000

5.3.2 启动Linux内核的基本流程

Linux内核的启动流程如下:

  1. U-Boot加载内核镜像

U-Boot从启动设备(如SD卡或QSPI Flash)中读取Linux内核镜像(通常是 uImage )和设备树( .dtb 文件)。

load mmc 0:1 ${kernel_addr_r} zImage
load mmc 0:1 ${fdt_addr_r} system.dtb
  1. 启动内核
bootz ${kernel_addr_r} - ${fdt_addr_r}

该命令将跳转执行Linux内核,并传递设备树地址。

  1. 内核初始化

Linux内核开始初始化系统设备、内存管理、进程调度等核心模块,并挂载根文件系统,最终进入用户空间。

5.3.3 裸机环境下自定义Bootloader开发

在某些嵌入式应用中,开发者可能希望跳过Linux系统,直接运行裸机程序。此时可以开发一个自定义的Bootloader,替代U-Boot。

开发自定义Bootloader的关键点包括:

  • 初始化系统时钟、GPIO、DDR等基本外设
  • 加载主程序到内存
  • 设置跳转地址并执行主程序入口函数

例如,以下是一个简单的裸机Bootloader示例:

#include "xil_types.h"
#include "xparameters.h"
#include "xil_io.h"

int main() {
    // 初始化串口
    Xil_Out32(XPAR_UARTLITE_0_BASEADDR + 0x08, 0x03); // 设置波特率
    Xil_Out32(XPAR_UARTLITE_0_BASEADDR + 0x10, 0x00); // 禁用中断

    // 输出启动信息
    const char *msg = "Custom Bootloader Running...\r\n";
    while (*msg) {
        Xil_Out32(XPAR_UARTLITE_0_BASEADDR + 0x04, *msg++);
    }

    // 跳转到应用程序入口(假设位于0x80000000)
    void (*app_entry)(void) = (void (*)(void))0x80000000;
    app_entry();

    return 0;
}

代码解释:

  • XPAR_UARTLITE_0_BASEADDR :UART的基地址,由硬件设计决定。
  • Xil_Out32(addr, val) :向指定内存地址写入32位值。
  • app_entry() :跳转到应用程序入口地址,启动用户程序。

此Bootloader通过UART输出提示信息后,跳转到固定地址运行用户程序。在实际项目中,可以根据需要添加更多的初始化逻辑和错误处理机制。

本章通过深入解析ZYNQ的启动流程,展示了从硬件初始化到操作系统或裸机程序启动的全过程,并详细介绍了FSBL的配置、BIT流加载机制、U-Boot的使用以及自定义Bootloader的开发方法,为后续系统级开发打下坚实基础。

6. 裸机编程基础:寄存器操作与中断处理

在嵌入式系统开发中,尤其是在ZYNQ架构的裸机(Bare-metal)环境下,理解并掌握寄存器操作和中断处理机制是构建底层驱动和控制逻辑的核心能力。本章将深入解析ZYNQ平台中寄存器访问的基本原理、内存映射机制、C语言实现方式,并进一步探讨中断控制器(GIC)的架构、中断服务程序的注册流程以及中断处理的执行逻辑。最后,通过一个基于中断的按键检测系统案例,帮助读者掌握从硬件配置到软件响应的完整开发流程。

6.1 寄存器操作原理与方法

6.1.1 内存映射与地址访问机制

ZYNQ-7000系列SoC采用统一的内存映射机制,将所有外设寄存器映射到特定的地址空间中。这种方式使得CPU可以直接通过内存读写指令访问外设寄存器,而无需专门的I/O指令。

在ZYNQ架构中,PS端的外设(如GPIO、UART、SPI等)通常位于0xE0000000到0xE0100000的地址范围内。例如,UART0的寄存器组起始地址为0xE0001000。通过访问这些地址,可以实现对外设的初始化和控制。

下表列出了ZYNQ PS端部分常用外设的寄存器起始地址:

外设名称 基地址(Base Address)
UART0 0xE0001000
UART1 0xE0000000
SPI0 0xE000D000
I2C0 0xE0004000
GPIO 0xE000A000

6.1.2 直接访问寄存器的C语言实现

在裸机环境下,使用C语言直接访问寄存器通常通过指针的方式实现。以下是一个典型的寄存器访问代码示例:

#include <stdint.h>

#define UART0_BASE_ADDR 0xE0001000
#define UART_LSR        (*(volatile uint32_t *)(UART0_BASE_ADDR + 0x28))
#define UART_THR        (*(volatile uint32_t *)(UART0_BASE_ADDR + 0x00))

void uart_putc(char c) {
    // 等待发送缓冲区为空
    while ((UART_LSR & 0x40) == 0);  // 0x40表示THR空标志
    UART_THR = c;  // 写入发送寄存器
}
代码逐行分析:
  1. #define UART0_BASE_ADDR 0xE0001000 :定义UART0寄存器块的基地址。
  2. #define UART_LSR ... :定义UART线路状态寄存器(Line Status Register),偏移地址为0x28。
  3. #define UART_THR ... :定义UART发送保持寄存器(Transmit Holding Register),偏移地址为0x00。
  4. while ((UART_LSR & 0x40) == 0); :等待直到发送缓冲区为空(THR Empty标志)。
  5. UART_THR = c; :将字符写入发送寄存器,触发UART发送操作。
参数说明:
  • volatile 关键字:防止编译器优化对寄存器访问的重排序。
  • uint32_t :确保访问的宽度为32位,符合ZYNQ寄存器的设计规范。

6.1.3 关键寄存器配置与系统控制

除了基本的数据发送外,还需要配置控制寄存器以启用外设功能。例如,UART的控制寄存器(UART_CR)位于0xE0001000 + 0x0C地址,其典型配置如下:

#define UART_CR         (*(volatile uint32_t *)(UART0_BASE_ADDR + 0x0C))

void uart_init() {
    UART_CR = 0x0000014B; // 启用UART、发送器、接收器、清除FIFO等
}

该配置值 0x0000014B 表示:

位段 含义
15:13 FIFO触发级别(0x1 表示1/8 FIFO)
7 接收使能(RE = 1)
6 发送使能(TE = 1)
0 UART使能(UARTEN = 1)

6.2 中断系统与异常处理机制

6.2.1 中断控制器(GIC)架构

ZYNQ的中断系统基于ARM Generic Interrupt Controller(GIC)架构,支持多个中断源的管理。GIC分为两个主要部分:

  • GIC Distributor :负责中断的配置与分发,包括中断使能、优先级设置、目标CPU选择等。
  • GIC CPU Interface :每个CPU核心对应一个CPU接口,用于响应和处理中断。

ZYNQ支持的中断类型包括:

类型 来源
SPI(Shared Peripheral Interrupt) 多个CPU共享的外设中断
PPI(Private Peripheral Interrupt) 每个CPU私有的外设中断
SGI(Software Generated Interrupt) 软件生成的中断

6.2.2 中断向量表与中断服务程序注册

在裸机环境下,中断向量表通常位于0x00000000处。每个中断类型对应一个入口地址。为了响应中断,需要设置中断向量表,并注册对应的中断服务程序(ISR)。

以下是一个中断初始化的示例代码:

void enable_irq() {
    asm volatile("cpsie i");  // 使能IRQ中断
}

void gic_init() {
    // 初始化GIC Distributor和CPU Interface
    // 此处省略具体寄存器操作,详见Xilinx官方手册
}

void register_irq_handler(int irq_num, void (*handler)(void)) {
    // 注册中断处理函数
    // irq_num:中断号
    // handler:函数指针
}

6.2.3 外设中断与系统异常处理流程

当中断发生时,ARM Cortex-A9处理器会自动跳转到中断向量表指定的地址,并执行相应的中断服务程序。以下是典型的中断处理流程:

graph TD
    A[外设触发中断] --> B[GIC接收中断请求]
    B --> C[CPU进入IRQ模式]
    C --> D[跳转到中断向量表入口]
    D --> E[执行中断服务程序ISR]
    E --> F[清除中断标志]
    F --> G[恢复现场并返回]

以下是一个中断服务程序的实现示例:

void gpio_irq_handler(void) {
    // 读取GPIO中断状态寄存器
    uint32_t int_status = *(volatile uint32_t *)(GPIO_BASE_ADDR + 0x40);

    if (int_status & (1 << 11)) {  // 检查第11位是否被置位
        // 处理按键中断
        clear_gpio_irq_flag(11);
        toggle_led();
    }
}
代码说明:
  • int_status :读取GPIO中断状态寄存器,判断哪个引脚触发了中断。
  • clear_gpio_irq_flag(11) :清除第11位的中断标志,防止重复触发。
  • toggle_led() :中断处理逻辑,例如切换LED状态。

6.3 实战:基于中断的按键检测系统

6.3.1 按键中断初始化与配置

在ZYNQ系统中,GPIO引脚可以配置为输入并启用中断。以下为GPIO中断初始化的步骤:

  1. 配置GPIO方向为输入
  2. 设置中断触发类型(上升沿、下降沿或双沿)
  3. 使能GPIO中断
  4. 注册中断服务程序
  5. 全局使能中断
void gpio_interrupt_init() {
    // 设置GPIO方向为输入
    *(volatile uint32_t *)(GPIO_BASE_ADDR + 0x400) = 0x00000000;

    // 设置中断触发类型:下降沿
    *(volatile uint32_t *)(GPIO_BASE_ADDR + 0x1C0) = 0x00000000; // 0x1C0: Direction mode register
    *(volatile uint32_t *)(GPIO_BASE_ADDR + 0x148) = 0x00000001; // 0x148: Rising edge detection
    *(volatile uint32_t *)(GPIO_BASE_ADDR + 0x14C) = 0x00000001; // 0x14C: Falling edge detection

    // 使能GPIO中断
    *(volatile uint32_t *)(GPIO_BASE_ADDR + 0x110) = 0x00000001; // 0x110: Interrupt mask register

    // 注册中断服务程序
    register_irq_handler(GPIO_IRQ_NUM, gpio_irq_handler);

    // 使能全局中断
    enable_irq();
}

6.3.2 中断服务程序编写与测试

中断服务程序需完成中断源判断、清除中断标志、执行用户逻辑等任务。测试时可通过示波器或LED状态变化验证中断是否正常响应。

6.3.3 多任务中断调度与优先级管理

在复杂系统中,可能会有多个中断源同时存在。GIC支持中断优先级管理,可以通过设置优先级寄存器来控制中断的响应顺序。以下为设置中断优先级的示例代码:

void set_irq_priority(int irq_num, uint8_t priority) {
    uint32_t *gic_ipriorityr = (uint32_t *)0xF8F41400; // GIC IPRIORITYR0
    gic_ipriorityr[irq_num / 4] |= (priority << ((irq_num % 4) * 8));
}
参数说明:
  • irq_num :中断号(0~1019)。
  • priority :优先级值(0~255,数值越小优先级越高)。

通过设置优先级,可以实现中断嵌套与任务调度,提升系统响应能力与稳定性。

本章从寄存器访问机制讲起,深入解析了ZYNQ中的内存映射结构和C语言实现方式,进一步介绍了中断控制器GIC的架构与中断服务程序的注册流程,并通过一个完整的GPIO按键中断案例展示了裸机环境下中断编程的完整实现过程。下一章将进入实战阶段,通过多个具体案例帮助读者掌握ZYNQ平台的综合开发能力。

7. 多个实践案例:从“Hello World”到外设通信实验

7.1 第一个裸机应用:“Hello World”输出

在ZYNQ平台上开发裸机程序是理解底层硬件操作的基础。通过实现一个简单的“Hello World”程序,可以快速熟悉SDK开发环境、工程结构、串口输出机制等核心概念。

7.1.1 工程创建与编译环境配置

  1. 打开 Xilinx SDK,连接好硬件平台(使用导出的硬件设计文件 .hdf)。
  2. 创建新的 Application Project:
    - 类型选择 Empty Application
    - 硬件平台选择已加载的ZYNQ系统
    - 选择处理器(通常是 ps7_cortexa9_0
  3. 在工程目录中,添加一个 .c 文件,例如 helloworld.c

此时,工程结构如下:

helloworld/
├── src/
│   └── helloworld.c
├── lib/
│   └── (系统库文件)
└── Debug/
    └── (编译生成的可执行文件)

7.1.2 UART初始化与字符串输出

以下是一个完整的“Hello World”裸机程序,使用UART0输出字符串:

#include "xparameters.h"
#include "xuartps.h"
#include <stdio.h>

XUartPs UartInstance;  // UART驱动实例

int main() {
    int Status;
    XUartPs_Config *Config;

    // 查找设备配置
    Config = XUartPs_LookupConfig(XPAR_PS7_UART_0_DEVICE_ID);
    if (NULL == Config) {
        return XST_FAILURE;
    }

    // 初始化UART实例
    Status = XUartPs_CfgInitialize(&UartInstance, Config, Config->BaseAddress);
    if (Status != XST_SUCCESS) {
        return XST_FAILURE;
    }

    // 设置通信参数:115200 baud, 8 data bits, no parity, 1 stop bit
    XUartPs_SetBaudRate(&UartInstance, XPAR_XUARTPS_CLOCK_HZ, 115200);

    // 输出字符串
    char *msg = "Hello World from ZYNQ baremetal!\r\n";
    XUartPs_Send(&UartInstance, (u8 *)msg, strlen(msg));

    while (1) {
        // 主循环,可扩展其他功能
    }

    return 0;
}

代码说明:

  • XUartPs_LookupConfig :根据设备ID查找UART配置。
  • XUartPs_CfgInitialize :初始化UART驱动实例。
  • XUartPs_SetBaudRate :设置波特率为115200。
  • XUartPs_Send :发送数据到UART。

7.1.3 SDK调试与运行结果验证

  1. 编译并下载程序到ZYNQ开发板。
  2. 使用串口调试工具(如 Tera Term、Xilinx SDK终端)连接开发板的UART接口。
  3. 设置波特率为115200,8N1。
  4. 运行程序后,串口应输出如下内容:
Hello World from ZYNQ baremetal!

7.2 多外设协同通信实验

在掌握了基础的裸机编程后,下一步是实现多个外设的协同工作,例如通过UART接收指令,SPI读取传感器数据,再通过I2C传输至EEPROM,形成一个完整的数据流处理流程。

7.2.1 UART与SPI协同的数据转发系统

本实验演示如何通过UART接收命令,控制SPI接口的ADC芯片(如ADS1115)读取模拟电压值,并将结果通过UART返回。

硬件连接示意图:

graph TD
    A[ZYNQ PS] --> B(UART)
    A --> C(SPI)
    C --> D[ADC芯片]
    B --> E[PC串口助手]

核心代码片段:

#include "xspi.h"

XSpi SpiInstance;

void SpiInit() {
    XSpi_Config *Config = XSpi_LookupConfig(XPAR_SPI_0_DEVICE_ID);
    XSpi_CfgInitialize(&SpiInstance, Config, Config->BaseAddress);
    XSpi_SetOptions(&SpiInstance, XSP_MASTER_OPTION);
    XSpi_Start(&SpiInstance);
}

u16 ReadADCValue() {
    u8 SendBuffer[3] = {0x01, 0x80, 0x00};  // 控制字
    u8 RecvBuffer[3];
    XSpi_Transfer(&SpiInstance, SendBuffer, RecvBuffer, 3);
    return ((RecvBuffer[1] << 8) | RecvBuffer[2]);
}

7.2.2 I2C与GPIO联动的传感器采集系统

使用I2C接口读取温湿度传感器(如SHT30),并通过GPIO控制LED指示灯状态(如高湿度点亮LED)。

I2C初始化代码:

#include "xiicps.h"

XIicPs IicInstance;

void IicInit() {
    XIicPs_Config *Config = XIicPs_LookupConfig(XPAR_PS7_I2C_0_DEVICE_ID);
    XIicPs_CfgInitialize(&IicInstance, Config, Config->BaseAddress);
    XIicPs_SetSClk(&IicInstance, 100000);
}

I2C读取函数(以SHT30为例):

void ReadTemperature(float *temp, float *hum) {
    u8 cmd[] = {0x2C, 0x06};  // 高精度测量命令
    u8 data[6];

    XIicPs_MasterSendPolled(&IicInstance, cmd, 2, 0x44);
    usleep(10000);  // 等待转换完成
    XIicPs_MasterRecvPolled(&IicInstance, data, 6, 0x44);

    *temp = -45 + (175 * (float)((data[0] << 8) | data[1]) / 65535.0);
    *hum = 100 * (float)((data[3] << 8) | data[4]) / 65535.0;
}

7.2.3 多任务环境下的资源调度与同步

在裸机环境下,可以使用简单的状态机或轮询机制实现多任务调度。例如:

typedef enum {
    TASK_UART,
    TASK_SENSOR,
    TASK_LED
} TaskState;

void Scheduler() {
    TaskState current = TASK_UART;
    while (1) {
        switch (current) {
            case TASK_UART:
                HandleUart();
                current = TASK_SENSOR;
                break;
            case TASK_SENSOR:
                ReadSensorData();
                current = TASK_LED;
                break;
            case TASK_LED:
                UpdateLedStatus();
                current = TASK_UART;
                break;
        }
    }
}

7.3 系统整合与完整工程实现

7.3.1 PS与PL协同的图像采集系统

图像采集系统由PS端控制摄像头(如OV7670),FPGA端进行图像处理(如灰度化、边缘检测),再由PS端通过AXI接口读取处理后的图像数据。

AXI接口读取图像数据示例:

#define IMG_BASE_ADDR 0x43C00000  // PL端寄存器地址
u32 *imgBuffer = (u32*)IMG_BASE_ADDR;

void ReadImageFrame(u32 *buffer, int width, int height) {
    for(int y = 0; y < height; y++) {
        for(int x = 0; x < width; x++) {
            buffer[y * width + x] = imgBuffer[y * width + x];
        }
    }
}

7.3.2 实时控制与数据可视化展示

通过将图像数据通过以太网或USB接口传输到上位机,使用Python Matplotlib进行实时图像显示,或使用Qt构建图形界面。

7.3.3 从裸机开发到嵌入式系统构建的过渡路径

当裸机项目规模增大后,可考虑过渡到轻量级操作系统(如FreeRTOS)或完整Linux系统。例如:

开发阶段 系统类型 特点
裸机开发 无操作系统 快速启动,低资源占用
FreeRTOS 实时操作系统 多任务、定时器、通信
Linux 完整系统 驱动支持丰富,GUI、网络功能完善

过渡路径建议:

  1. 在裸机基础上移植FreeRTOS,学习任务调度、队列通信。
  2. 构建Linux系统,使用设备树配置外设,编写驱动程序。
  3. 利用ZYNQ的双核架构,实现PS与PL的高效协同。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《米联ZYNQ嵌入式开发实战教程》是一套针对Xilinx ZYNQ系列SoC的裸机开发教程,全面讲解了基于ARM Cortex-A9双核处理器的可编程系统芯片开发技术。教程涵盖ZYNQ架构、硬件接口设计、启动流程、裸机编程、设备驱动、JTAG调试、时钟与电源管理、硬件加速、SDK开发环境配置与实验等内容,通过逐步进阶的实验帮助学习者掌握嵌入式开发的核心技能。适用于学生、工程师及嵌入式系统开发者快速入门并深入理解ZYNQ平台开发流程。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐