ZYNQ笔记(十三):双核 AMP 通信实验
版本:Vivado2020.2(Vitis)
ZYNQ 裸机双核 AMP 实验:
CPU0 接收串口的数据,并写入 OCM 中,然后利用软件产生中断触发 CPU1;CPU1 接收到中断后,根据从 OCM 中读出的数据控制呼吸灯的频率,并在控制结束后触发 CPU0 中断,实现了双核 CPU 通信的功能。
目录
一、介绍
(1)双核 AMP 通信
(2)ZYNQ启动流程(裸机)
二、硬件设计
三、软件设计
(1)新建 CPU0 应用工程
(2)分配 CPU0 DDR 空间
(3)新建 CPU1 应用工程
(4)分配 CPU0 DDR 空间
(5)代码部分
(6)下载
四、效果
一、介绍
Zynq-7000 SoC 集成了 双核 ARM Cortex-A9 MPCore,采用对称多处理(SMP)架构,同时支持非对称多处理(AMP)模式,适用于嵌入式高性能计算和实时控制场景。
(1)双核 AMP 通信
直接附上正点原子开发指南的内容切片:
— — — — — — — — — — — — — 分割线 — — — — — — — — — — — —
(2)ZYNQ启动流程(裸机)
上面介绍有提到了一些专有名词(如BootROM、FSBL、OCM),这里连带介绍一下 ZYQN(Xilinx Zynq-7000 SoC) 裸机开发时的启动流程,主要分为以下几个阶段:
1. Boot ROM
-
硬件自动执行:上电或复位后,芯片首先运行片内ROM中的固化代码(Boot ROM)。
-
启动模式检测:根据硬件配置(如模式引脚设置)确定启动源(如QSPI Flash、SD卡、NAND Flash、JTAG等)。
-
加载FSBL:从启动设备中读取 FSBL(First Stage Boot Loader)到芯片的 OCM(On-Chip Memory)或 DDR 中(需预先配置DDR,本次例程配置了所以程序基于 DDR 运行)。
2. FSBL(First Stage Boot Loader)
-
功能:
-
初始化必要外设(如DDR控制器、时钟、PLL)。
-
从Flash或SD卡中加载用户程序(裸机代码或第二阶段引导程序)。
-
可配置 PL(Programmable Logic)部分(可选,需加载比特流文件)。
-
-
生成方式:通过 Xilinx SDK 或 Vitis 工具链生成(基于用户硬件设计)。
-
执行位置:通常在 OCM 中运行。
3. 用户程序(裸机代码)
-
加载与执行:FSBL将用户编写的裸机程序(ELF文件)加载到DDR或OCM中,并跳转到其入口地址。执行程序。
二、硬件设计
(1)本实验是用软件产生中断(SGI)的方式来控制两个 CPU 访问共享内存的过程:
首先需要处理器向共享内存中写入一个数据,本实验选择 CPU0 往共享内存中写入数据。当 CPU0 写入成功后就发送一条软中断指令去触发 CPU1 去读取共享内存中的数据。
当 CPU1 读取到共享内存的数据后需要通过 M_AXI_GP0 接口的 AXI4-Lite 总线将数据发送到 PL 端呼吸灯的 IP 核, 呼吸灯的IP核根据PS发过来的数据对呼吸频率进行相应的配置,然后 CPU1 发送一条软中断指令触发 CPU0 继续往共享内存中写入数据。
到此便实现了两个 CPU 通过共享内存进行数据交互的功能,同时为了对IP核进行控制以及方便观察数据流的走向,用 UART 接收控制命令以及打印 Debug 信息。
(2)关于自定义 LED 呼吸灯 IP 核的设计与使用这里不再赘述,跟之前笔记的例程操作步骤一致,详细参考:ZYNQ笔记(六):自定义IP核-LED呼吸灯
(3)最后整体 bd 设计部分如图所示:设计检查、Generate Output Products、 Create HDL Wrapper、管脚约束、Gnerate Bitstream、Export Hardware(包含比特流文件)、启动Vitis
注意 LED 呼吸灯的 IP 核要手动添加 LED 管脚并与开发板的 LED 对应管脚绑定。
三、软件设计
(1)新建 CPU0 应用工程
新建工程名 “cpu0_uart” ,系统工程名“AMP_2core_system”,通过cpu0实现串口通信,主要是从uart接收控制命令。如图所示,同时处理器选择ps7_cortexa9_0(默认也是cpu0).
(2)分配 CPU0 DDR 空间
单核应用工程,默认将DDR空间全部分配给cpu0,这里用到了双核所以需要进行分配,就直接将DDR分二部分,一部分给cpu0,另一部分给cpu1(具体怎么大小分配都行),如图所示:
红框标注的地方左侧为基地址,右侧为存储空间大小(单位字节),存储空间大小将默认的完整空间大小 0x1FF00000 (十进制 535,822,336;535,822,336/1024/1024 = 511MByte),修改为为 0x0FF00000(十进制 267,386,880;267,386,880/1024/1024 = 255MByte)。修改完成后,按下“Ctrl”+ “S”保存。
另外图中的 ps7_ram_0 和 ps_ram_1 为 OCM 共享内存的基地址和存储空间大小。后面双核通信就是通过这访问这两块共享存储区域实现的,后面软件设计也会用到其的基地址以进行访问(本例程任选一个即可,后面的设计选用的是 ps_ram_0 基地址0x0)。
(3)新建 CPU1 应用工程
在创建 CPU1 应用工程之前,需要新建基于 CPU1 的板级支持包(BSP)。双击 platform.spr,打开 system_wrapper 设置界面,如下图所示:
之后依次对硬件平台工程(system_wrapper)右击选择 Clean Project 和 Build Project。
接下来创建 CPU1 的应用工程,右键系统工程 AMP_2core_system 选择“Add Application Project”,从而在系统工程下添加一个新的应用工程,工程名命名为 cpu1_led,处理器选择 ps7_cortexa9_1, 如图所示:
(4)分配 CPU0 DDR 空间
同样需要为 cpu2 分配 DDR 空间,两个核的DDR空间不能有重合,负责程序运行时会出现同时访问导致程序异常,所以这里要对起始地址和大小同时进行修改,如图所示:
起始地址 = CPU0 的 DDR 起始地址 + 其分配大小 = 0x10000 + 0x0FF00000 = 0x10000000 ;分配大小 ≤ DDR 完整大小 - CPU0 的 DDR 分配大小 = 0x1FF00000(由开发板DDR大小决定)- 0x0FF00000 = 0x10000000 。修改完成后,按下“Ctrl”+“S”保存。
(5)代码部分
新建 cpu0_uart 应用工程源文件“mian_0.c”,编写代码:
#include "xparameters.h"
#include "xscugic.h"
#include "xil_printf.h"
#include "xil_exception.h"
#include "xil_mmu.h"
#include "stdio.h"//============================宏定义===============================//
#define SHARE_BASE 0x0 //共享OCM首地址(根据lscript.ld设置)
#define CPU1_COPY_ADDR 0xfffffff0 //存放CPU1应用起始地址的地址(参考ug585手册)
#define CPU1_START_ADDR 0x10000000 //CPU1应用起始地址(给cpu1分配的DDR起始地址)#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID //GIC中断控制器ID
#define CPU1_ID XSCUGIC_SPI_CPU1_MASK //CPU1 ID
#define SOFT_INTR_ID_CPU0 0 //软件中断号 0 ,范围0~15
#define SOFT_INTR_ID_CPU1 1 //软件中断号 1 ,范围0~15//"SEV"指令唤醒CPU1并跳转至相应的程序
#define sev() __asm__("sev") //C语言内嵌汇编写法 send event指令//===========================函数声明==============================//
void start_cpu1();
void cpu0_intr_init(XScuGic *intc_ptr);
void soft_intr_handler(void *CallbackRef);//===========================全局变量==============================//
XScuGic Intc; //中断控制器驱动程序实例
int rec_freq_flag = 0; //接收到呼吸灯频率设置的标志
int speed; //频率速度档位(0~7)//=========================CPU0 main函数===========================//
int main()
{//S=b1 TEX=b100 AP=b11, Domain=b1111, C=b0, B=b0Xil_SetTlbAttributes(SHARE_BASE,0x14de2); //禁用OCM的Cache属性//S=b1 TEX=b100 AP=b11, Domain=b1111, C=b0, B=b0Xil_SetTlbAttributes(CPU1_COPY_ADDR,0x14de2);//禁用0xfffffff0的Cache属性//启动CPU1(固化程序时需使用)//start_cpu1();//CPU0中断初始化cpu0_intr_init(&Intc);while(1){if(rec_freq_flag == 0){xil_printf("CPU0: Input 0~7 to change breath LED frequency \r\n");xil_printf("\r\n");//输入0~7指令scanf("%d",&speed);if(speed >= 0 && speed <= 7){xil_printf("CPU0: Input Command %d \r\n",speed);//向共享的地址中写入输入的数据Xil_Out8(SHARE_BASE,speed);//CPU0软件触发CPU1中断XScuGic_SoftwareIntr(&Intc, SOFT_INTR_ID_CPU1, CPU1_ID);rec_freq_flag = 1;}else{xil_printf("CPU0: Command Error, out of range 0~7 \r\n");}}}return 0 ;
}//====================启动CPU1,用于固化程序=======================//
// 如果只是通过 JTAG 模式下载程序,可以不编写这个启动函数,这个函数仅在固化程序的时候起作用。
void start_cpu1()
{//向 CPU1_COPY_ADDR(0Xffffffff0)地址写入 CPU1 的访问内存基地址Xil_Out32(CPU1_COPY_ADDR, CPU1_START_ADDR);dmb(); //等待内存写入完成(同步)sev(); //通过"SEV"指令唤醒CPU1并跳转至相应的程序
}//=========================中断处理函数===========================//
void soft_intr_handler(void *CallbackRef)
{xil_printf("CPU0: Received Soft Interrupt from CPU1 (set finish) \r\n");rec_freq_flag = 0;
}//========================CPU0中断初始化==========================//
void cpu0_intr_init(XScuGic *intc_ptr)
{//初始化中断控制器XScuGic_Config *intc_cfg_ptr;intc_cfg_ptr = XScuGic_LookupConfig(INTC_DEVICE_ID);XScuGic_CfgInitialize(intc_ptr, intc_cfg_ptr,intc_cfg_ptr->CpuBaseAddress);//设置中断异常处理功能Xil_ExceptionInit();Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,(Xil_ExceptionHandler)XScuGic_InterruptHandler, intc_ptr);//使能处理器中断Xil_ExceptionEnable();//关联中断函数XScuGic_Connect(intc_ptr, SOFT_INTR_ID_CPU0,(Xil_ExceptionHandler)soft_intr_handler, (void *)intc_ptr);//使能中断XScuGic_Enable(intc_ptr, SOFT_INTR_ID_CPU0); //CPU0软件中断
}
新建 cpu1_led 应用工程源文件“mian_1.c”,编写代码:
#include "xparameters.h"
#include "xscugic.h"
#include "xil_printf.h"
#include "xil_exception.h"
#include "xil_mmu.h"
#include "stdio.h"
#include "breath_led_ip.h"//============================宏定义===============================//
#define SHARE_BASE 0x0 //共享OCM首地址(根据lscript.ld设置)#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID //GIC中断控制器ID
#define CPU0_ID XSCUGIC_SPI_CPU0_MASK //CPU0 ID
#define SOFT_INTR_ID_CPU0 0 //软件中断号 0 ,范围0~15
#define SOFT_INTR_ID_CPU1 1 //软件中断号 1 ,范围0~15#define LED_IP_BASEADDR XPAR_BREATH_LED_IP_0_S0_AXI_BASEADDR //LED IP基地址
#define LED_EN BREATH_LED_IP_S0_AXI_SLV_REG0_OFFSET //宏定义led_en 对应寄存器(slv_reg0)地址偏移量
#define SPEED_EN BREATH_LED_IP_S0_AXI_SLV_REG1_OFFSET //宏定义speed_en对应寄存器(slv_reg1)地址偏移量
#define SPEED BREATH_LED_IP_S0_AXI_SLV_REG2_OFFSET //宏定义speed 对应寄存器(slv_reg2)地址偏移量//===========================函数声明==============================//
void cpu1_intr_init(XScuGic *intc_ptr);
void soft_intr_handler(void *CallbackRef);//===========================全局变量==============================//
XScuGic Intc; //中断控制器驱动程序实例
int soft_intr_flag = 0; //软件中断的标志
int speed; //频率档位(0~7)//=========================CPU1 main函数===========================//
int main()
{//S=b1 TEX=b100 AP=b11, Domain=b1111, C=b0, B=b0Xil_SetTlbAttributes(SHARE_BASE,0x14de2); //禁用OCM的Cache属性//CPU1中断初始化cpu1_intr_init(&Intc);//打开LED呼吸灯BREATH_LED_IP_mWriteReg(LED_IP_BASEADDR, LED_EN, 0x1);//使能LED呼吸灯频率设置BREATH_LED_IP_mWriteReg(LED_IP_BASEADDR, SPEED_EN, 0x1);while(1){if(soft_intr_flag){speed = Xil_In8(SHARE_BASE); //从共享OCM中读出数据xil_printf("CUP1: Received Command is %d \r\n",speed) ;//设置LED呼吸灯频率BREATH_LED_IP_mWriteReg(LED_IP_BASEADDR, SPEED, speed);//CPU1软件触发CPU0中断XScuGic_SoftwareIntr(&Intc,SOFT_INTR_ID_CPU0,CPU0_ID);soft_intr_flag = 0;}}return 0 ;
}//=========================中断处理函数===========================//
void soft_intr_handler(void *CallbackRef)
{xil_printf("CPU1: Received Soft Interrupt from CPU0 (command ready) \r\n") ;soft_intr_flag = 1;
}//=========================CPU1中断初始化=========================//
void cpu1_intr_init(XScuGic *intc_ptr)
{//初始化中断控制器XScuGic_Config *intc_cfg_ptr;intc_cfg_ptr = XScuGic_LookupConfig(INTC_DEVICE_ID);XScuGic_CfgInitialize(intc_ptr, intc_cfg_ptr,intc_cfg_ptr->CpuBaseAddress);//设置中断异常处理功能Xil_ExceptionInit();Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,(Xil_ExceptionHandler)XScuGic_InterruptHandler, intc_ptr);//使能处理器中断Xil_ExceptionEnable();//关联中断函数XScuGic_Connect(intc_ptr, SOFT_INTR_ID_CPU1,(Xil_ExceptionHandler)soft_intr_handler, (void *)intc_ptr);//使能中断XScuGic_Enable(intc_ptr, SOFT_INTR_ID_CPU1); //CPU1软件中断
}
(6)下载
下载时需要将两个应用工程以及FPGA的比特流文件全部勾选下载。操作如图所示:
选中两个核的应用工程:
选中同时下载FPGA波特率文件(这个选项一般是默认勾选了):
四、效果
1.下载后CPU0打印输入指令的提示信息,同时LED灯已经开始工作:
2.第一次输入9(正确指令范围0~7),CPU0打印报错信息并重新提示输入指令。
3.第二次输入1,CPU0打印出接受到的正确指令,接着CPU1打印接收到来自CPU0的中断的提示(表示CPU0已经将指令写入OCM),接着CPU0打印接收到来自CPU1的中断的提示(表示CPU1已经从OCM读取指令并设置LED灯速率),此时LED呼吸灯的速度发生对应变化(比初始的0档位闪烁的快一点),最后CPU0打印输入指令的提示信息(表示可以进行下一次设置操作)
4.第三次输入7,流程同上,LED灯速率达到最大,闪烁最快。
注意:
本次例程用到scanf()(头文件 stidio.h)实现串口输入指令,这种方式可以在vitis串口终端上进行调试。但是如果要用一般的串口助手调试就不行,需要修改串口部分的软件设计,可以加上串口中断功能实现接收串口输入输出。参考笔记:ZYNQ笔记(八):UART 串口中断
本次例程没有进行程序固化,所以没有使用到CPU0启动CPU1的函数,如果需要固化需要实现该函数,同时也许在硬件设计作配置,例如固化程序到 Flash 需要配置 QSPI Flash ,相关固化可操作参考笔记:ZYNQ笔记(七):程序固化(QSPI Flash)