最近玩起了Xilinx的zynq 7000系列芯片,打算用znyq的双核A9独立运行两个程序,也就是所谓的AMP模式,需求是这样的,cpu0先运行,然后cpu0从外部媒介加载cpu1的代码到特定内存区域,然后cpu0唤醒cpu1,两个cpu同时干活,完了cpu0将cpu1复位停止,等待下一次需要cpu1工作时再重新加载cpu1运行,总的来说就是cpu0代码是固定的,cpu1代码是随时可变的。
由于是新接触这个双核芯片,原先对它不太熟,一顿搜索之后大概了解了啥是AMP后就动手测试起了,不得不吐槽一下,现在的博客真是各种抄各种转载,连坑也一起抄过去了,按照网上的文章都是将启动cpu1的代码放到FSBL里面,虽然能正常运行,但是明显FSBL启动CPU1是不符合我的要求的,有的文章简要说了一下直接把启动CPU1的代码放到cpu0的main函数里就可以用cpu0启动cpu1了,但是我实际测试后发现是不行的,下面就以我自己总结的经验来说明一下如何设置能达到文章开头的那种 “另类” 双核AMP运行效果。
首先说明一下,我用的开发环境时Xilinx vivado 2019.2 和 vitis 2019.2,创建硬件环境xsa文件的步骤就不说了,硬件环境不包含PL,只有PS部分,两个LED从MIO引出,分别是MIO0和MIO9,演示时cpu0启动后MIO9上的LED固定频率闪烁,然后cpu0加载cpu1代码,启动cpu1,此时cpu1控制MIO0上的LED一秒一次闪烁,过10秒后cpu0复位cpu1,并重新加载cpu1的另外一份代码,运行后cpu1控制MIO0的LED以0.5秒一次闪烁,可以明显看到cpu1的代码被cpu0动态更改了。
要实现上面的功能,首先按网上的大堆AMP文章搭建硬件环境,并且创建1个硬件bsp平台(包含cpu0,cpu1,FSBL),3个硬件app程序(cpu0,cpu1,fsbl),如下图
自己设置一下cpu0和cpu1的启动地址,地址随意,注意不要重叠即可,cpu1的硬件bsp需要加入-DUSE_AMP=1 这个宏定义,表示cpu1启动时不需要初始化某些共用外设,然后编译一下,生成bsp硬件。
软件app的话按上图,创建3个app工程,其中cpu0和fsbl工程创建时选择运行在cpu0,cpu1的app选择运行在cpu1,不要搞错了。
下面的两个函数,一个是复位cpu1,一个是启动cpu1,网上的一大堆文章都是说将启动地址写到0xFFFFFFF0就行了,实际是不行的,就是漏了关键的一行代码,禁用OCM cache缓存!否则实际cpu1不能正常启动!
void ResetCpu1(void)
{
u32 RegVal;
Xil_SetTlbAttributes(0xFFFF0000,0x14de2);// S=b1 TEX=b100 AP=b11, Domain=b1111, C=b0, B=b0 Diasble OCM CACHE
Xil_Out32(CPU1_CATCH, CPU1STARTMEM);
Xil_Out32(XSLCR_UNLOCK_ADDR, XSLCR_UNLOCK_CODE);
RegVal = Xil_In32(A9_CPU_RST_CTRL);
RegVal |= A9_RST1_MASK;
Xil_Out32(A9_CPU_RST_CTRL, RegVal);
RegVal |= A9_CLKSTOP1_MASK;
Xil_Out32(A9_CPU_RST_CTRL, RegVal);
RegVal &= ~A9_RST1_MASK;
Xil_Out32(A9_CPU_RST_CTRL, RegVal);
RegVal &= ~A9_CLKSTOP1_MASK;
Xil_Out32(A9_CPU_RST_CTRL, RegVal);
Xil_Out32(XSLCR_LOCK_ADDR, XSLCR_LOCK_CODE);
dmb();
}
void StartCpu1(void)
{
Xil_SetTlbAttributes(0xFFFF0000,0x14de2);// S=b1 TEX=b100 AP=b11, Domain=b1111, C=b0, B=b0 Diasble OCM CACHE
Xil_Out32(CPU1STARTADR, CPU1STARTMEM);
dmb();
sev();
}
以上两个代码只是复位和重启cpu1,但是由于cpu1复位后不是在wfe状态,因此还需要预先准备一下一小段汇编代码,让cpu1复位后先跳转到这段汇编代码,然后运行这小段汇编代码后cpu就进入wfe状态等待sev指令唤醒。
void initCpu1_RstAddr(void)
{
Xil_Out32(0xFFFFFF00, 0xe3e0000f);
Xil_Out32(0xFFFFFF04, 0xe3a01000);
Xil_Out32(0xFFFFFF08, 0xe5801000);
Xil_Out32(0xFFFFFF0C, 0xe320f002);
Xil_Out32(0xFFFFFF10, 0xe5902000);
Xil_Out32(0xFFFFFF14, 0xe1520001);
Xil_Out32(0xFFFFFF18, 0x0afffffb);
Xil_Out32(0xFFFFFF1C, 0xe1a0f002);
Xil_Out32(0x00000000, 0xe3e0f0ff);
Xil_Out32(0xFFFFFFF0, 0x00000000);
}
上面的这个函数就是让cpu1进入wfe状态用的,实际上就是往cpu1复位后的默认地址里写入一小段汇编指令,具体是什么指令,其他文章也有介绍,我就不卖弄了,知道这回事即可。
有了上面的三个函数基本就能实现目标了,不过还有一点需要注意,initCpu1_RstAddr这个函数最好放在FSBL里面,这样的话cpu0启动后复位cpu1默认就能自动运行这段代码,如果放在cpu0的main函数里面的话可能还会有cache问题,导致什么奇怪现象。放置位置如下图
最后就是cpu0运行的测试代码,如下图所示,其中data和data1是一个大数组,内容是cpu1编译出的elf转为bin文件,按照代码所示即可每隔大约10秒看到MIO0的LED闪烁频率变化,证明cpu1的代码被cpu0动态修改了
int main()
{
static XGpioPs psGpioInstancePtr;
XGpioPs_Config *GpioConfigPtr;
int i=0,j=0,k=0;
unsigned char* p = CPU1STARTMEM;
int xStatus;
init_platform();
for(i=0;i<32776;i++)
p[i]= data1[i];
StartCpu1();
GpioConfigPtr =XGpioPs_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID);
if(GpioConfigPtr == NULL)
return XST_FAILURE;
xStatus =XGpioPs_CfgInitialize(&psGpioInstancePtr,GpioConfigPtr,GpioConfigPtr->BaseAddr);
if(XST_SUCCESS != xStatus)
print("PS GPIO INIT FAILED \n\r");
XGpioPs_SetDirectionPin(&psGpioInstancePtr,9,1);
XGpioPs_SetOutputEnablePin(&psGpioInstancePtr,9,1);
print("star up cpu1 ...!\n\r");
while(1)
{
for(i=0;i<1000000;i++)
XGpioPs_WritePin(&psGpioInstancePtr,9,0);
for(i=0;i<1000000;i++)
XGpioPs_WritePin(&psGpioInstancePtr,9,1);
j++;
if(j>60)
{
ResetCpu1();
k++;
if(k%2)
{
for(i=0;i<32776;i++)
p[i]= data[i];
}
else
{
for(i=0;i<32776;i++)
p[i]= data1[i];
}
StartCpu1();
j=0;
}
}
cleanup_platform();
return 0;
}
补充一下,如何将zynq vitis编译环境下生成的elf文件转为bin文件,很简单,只需要在vitis环境下设置如下图即可在编译后自动在elf目录生成同名的bin了
arm-none-eabi-objcopy -O binary cpu1.elf cpu1.bin ,将cpu1.elf更换为你自己的工程名字
下一步,研究一个核跑petalinux,并用petalinux启动cpu1