Zynq-仅复位PS不复位PL

在使用Zynq进行PS+PL互联开发时,PL端上运行的是时序电路,如果PS端的C代码由于各种异常原因挂死,可以通过复位系统重新执行以恢复功能。但是如果想在复位的过程中保持PL时序电路继续运行,则处理起来比较复杂,阅读本文,您可以了解到以下内容

  • Zynq的watchdog工作原理是什么,如何控制watchdog实现监测软件运行异常触发复位
  • 如何让watchdog的复位仅复位PS,而不复位PL
  • 复位后如何知道启动原因
  • 复位后C代码是从FSBL执行的,如何跳过重新初始化PL加载比特流的处理

为什么只复位PS?

在一些业务场景中,PL通过某种接口形式与外部系统连接,某些接口或系统间可能在建立连接的阶段存在比较复杂的交互,例如训练、协商、握手等机制,而连接一旦异常断开,再想要重新建立恢复通信是一件非常麻烦的事情,例如协议或时序的设计时就没有考虑到断开后的快速恢复,异常断开后双方状态不一致,导致无法重新连接,更严重的会直接导致对端出现错误。如果是一对多或者多对多的场景下就更为复杂。因此如果只复位PS端而不复位PL端,在保持链接的情况下,重新运行PS端的初始化,能够大大缩减系统复位后的恢复时间

Zynq Watchdog

Zynq平台由于整个系统设计较为复杂,因此设计了多种复位方式和复位源

  • Power-on Reset (PS_POR_B) 系统上电复位引脚
  • External System Reset (PS_SRST_B) 不会复位调试环境
  • System Software Reset 可以通过写软件寄存器产生和PS_SRST_B一样的效果
  • Watchdog Timer Resets 当定时时间到达时触发复位

本文所使用的方式就是看门狗(Watchdog)复位方式。Zynq PS端有3个看门狗

  • 1个系统级看门狗 SWDT(Sytem Watchdog Timer)
  • 2个ARM核私有的应用级看门狗 AWDT0核AWDT1 (Application Watchdog Timer)

SWDT会复位整个系统,而AWDT可以选择复位整个系统或者只复位单个ARM核,本文中所使用的方式就是AWDT仅复位单个ARM核

AWDT

AWDT并不是Xilinx的IP核,而是直接使用了ARM提供的第三方IP核,本文对AWDT的配置参考”ARM Cortex-A9 MPCore Technical Reference Manual”,Zynq的手册中并没有对AWDT配置的介绍

看门狗本质上是一个不重新加载的定时器,当定时器时间到期时会输出一个复位信号,Zynq利用这个复位信号来实现复位功能。程序正常运行时需要通过一个任务或线程,按照一定的周期来喂狗,也就是将定时器的时间重置,这个喂狗的周期必须小于定时器设定的时长,在程序正常运转时,能够保证定时器总是在到期之前被重置,而软件出现异常时,程序无法正常喂狗,定时器到期就触发复位

AWDT控制器的寄存器绝对基地址为0xF8F00620,它既可以当作看门狗来使用,也可以当作一个通用定时器使用,本文仅介绍看门狗的使用方法。与AWDT有关的寄存器如下

  • 0x20, Watchdog Load Register
  • 0x24, Watchdog Counter Register
  • 0x28, Watchdog Control Register
  • 0x2C, Watchdog Interrupt Status Register
  • 0x30, Watchdog Reset Status Register
  • 0x34, Watchdog Disable Register

在C语言中可以定义一个用于操作看门狗寄存器的结构体,并创建一个该结构体的指针强制指向看门狗的基地址,此后代码中就可以通过指针成员的方式直接操作寄存器了

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct 
{
volatile uint32_t load;
volatile uint32_t counter;
volatile uint32_t control;
volatile uint32_t isr;
volatile uint32_t rsr;
volatile uint32_t dr;
} watchdog_regs;

#define WDT_BASE_ADDR 0xF8F00620

static watchdog_regs *wdt_regs = (watchdog_regs *)WDT_BASE_ADDR;

Disable和Interrupt Status两个寄存器仅用于将看门狗当作普通定时器的场景,这里不需要操作。Load加载寄存器用于存放定时器的定时值,每次写Load寄存器时会自动触发将Load的值拷贝到Counter寄存器中,当通过Control寄存器使能看门狗工作后,看门狗就开始自动将Counter的计数值自减,减到0时就触发复位。软件上只需要操作Load和Control两个寄存器,在初始化时通过配置Control来设置定时数值的精度以及设置看门狗模式,并设置一个初始的Load数值,然后在喂狗任务中启动定时器,再定时喂狗即可,伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#define WDT_CTRL_WM         BIT(3)
#define WDT_CTRL_INT BIT(2)
#define WDT_CTRL_RELOAD BIT(1)
#define WDT_CTRL_EN BIT(0)

void watchdog_disable()
{
wdt_regs->contrl &= ~WDT_CTRL_EN;
}

void watchdog_enable()
{
wdt_regs->contrl |= WDT_CTRL_EN;
}

void watchdog_set_mode()
{
wdt_regs->contrl |= WDT_CTRL_WM;
}

void watchdog_feed_task(void *args)
{
watchdog_enable();

while(1) {
msleep(feed_period);
watchdog_feed(timeout);
}
}

watchdog_init()
{
watchdog_disable();
watchdog_set_mode(WDT_MODE);
watchdog_set_prescaler(prescaler);
watchdog_feed(timeout);
create_task(wdt_feed_task);
}

以上伪代码在初始化时先禁用了看门狗,设置为看门狗模式,通过watchdog_set_prescaler()函数设置了定时器精度,通过watchdog_feed()为看门狗设定了超时时间,最后创建一个喂狗的任务。在任务中先使能看门狗,然后按照一个喂狗的周期进行喂狗

定时器精度

前面的介绍中提到了定时器精度,那么这个精度应该怎么设置呢,我给看门狗设置了一个整数的超时数值,怎么知道这个数值对应多长的时间呢?在Cortex-A9 MPCore手册中,Watchdog Control Register的bit[15:8]位域Prescaler,就是用来设置定时器精度的,最终的定时时长与精度prescaler、Load寄存器中的定时器初值和外设时钟PERIPHCLK存在以下计算关系

prescaler共8位,数值范围就是0~255,load数值范围是0~0xFFFFFFFF,这两个数值都是自己设置的,很好确认,但是PERIPHCLK是怎么确定的呢?

在Cortex-A9 MPCore中有关于时钟PERIPHCLK的介绍,Cortex-A9处理器有一个主要的时钟CLK,系统中所有部分的时钟都源自该时钟,中断、全局时钟、看门狗的时钟是使用PERIPHCLK,PERIPHCLK与CLK是同步的,多倍数的关系,但是没有明确倍数是多少、CLK数值是多少。在Zynq手册(ug585)中的第25章时钟介绍部分,这里有一张关于CPU时钟分频的介绍

有两种分频方案,6:2:1和4:2:1,由于我的工程中在vivado中选择PS的CPU频率为800MHz,因此使用的是6:2:1的分频方案,看门狗使用的是APU timer这一栏的三倍CLK速率为400MHz,也就是PERIPHCLK是400MHz,因此定时器一个基本单位是2.5纳秒乘以(Prescaler+1)

我在工程中对Prescaler的设置为159,这样Prescaler+1就是160,也就是400纳秒,那么我设置Load为1,超时时间就是400ns,Load为1000000,超时时间就是400毫秒。当然在设计接口时,不希望由外部来进行计算,最好是将时间的换算关系封装在函数内部,因此对外提供一个毫秒级的超时设置接口,如果我想要1毫秒的超时,需要的Load值应该是多少呢?当然是用1ms除以400ns,得到2500,因此设置超时的函数可以写为

1
2
3
4
void watchdog_feed(uint32_t timeout_ms)
{
wdt_regs->load = timeout_ms * 2500;
}

主动触发复位

当完成以上工作后,如果一切正常,那么程序启动后由于喂狗任务的存在,每当定时器还未到期之前就已经被重新喂狗,因此不会触发复位,如果想要主动触发复位,可以通过外部的一个指令来修改定时器的超时时间小于喂狗时间,那么下一次定时器会在喂狗周期之前定时到期,触发复位。程序中也可以通过触发断言、死循环来让喂狗程序得不到执行的方式来触发复位,进行复位功能测试

仅复位PS

对看门狗的设置,除了以上操作之外,在Zynq的系统级控制寄存器组slcr(System Level Control Registers)中,还有一个与控制AWDT复位相关的寄存器RS_AWDT_CTRL,其绝对地址为0xF800024C,该寄存器中只有CTRL0和CTRL1两个bit位,分别用于控制CPU0和CPU1上的AWDT复位方式,为0代表复位整个系统,为1表示仅复位当前CPU,其默认值为0,而我们想要仅复位CPU,所以需要在看门狗初始化时将其改为1。slcr寄存器组比较特殊,由于涉及到非常重要的系统级控制,因此整个寄存器组的操作受到写保护,程序中不能随意进行写入,而是需要进行解锁→写→上锁的处理,slcr中提供了SLCR Write Protection Lock和SLCR Write Protection Unlock两个寄存器来进行处理,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define SLCR_BASE_ADDR      0xF8000000
#define SLCR_LOCK_REG (SLCR_BASE_ADDR + 4)
#define SLCR_LOCK_KEY 0x767B767B
#define SLCR_UNLOCK_REG (SLCR_BASE_ADDR + 8)
#define SLCR_UNLOCK_KEY 0xDF0DDF0D
#define SLCR_AWDT_CTRL (SLCR_BASE_ADDR + 0x24C)
#define slcr_unlock() Xil_Out32(SLCR_UNLOCK_REG, SLCR_UNLOCK_KEY)
#define slcr_lock() Xil_Out32(SLCR_LOCK_REG, SLCR_LOCK_KEY)

void watchdog_set_ps_only_reset()
{
slcr_unlock();
Xil_Out32(SLCR_AWDT_CTRL, 0x1);
slcr_lock();
}

获取复位状态

当PS复位重启后,程序中必定需要知道本次启动是否是由于看门狗复位引起的,那么从哪里获取这个信息呢?Watchdog Reset Status Register就是做这个事情的,读取该寄存器,如果数值为1,表明是由于看门狗复位的,如果数值为0,表明未发生看门狗复位,写1会清除该标志位

1
2
3
4
5
6
7
8
9
int is_watchdog_reset()
{
return wdt_regs->rsr == 0x1;
}

void watchdog_clear_status()
{
wdt_regs->rsr = 0x1;
}

FSBL的处理

当做完以上处理后,触发看门狗复位后,会发现PL端仍然被复位了,原因是虽然看门狗设置的复位方式是仅复位PS,但是PS复位后是从FSBL开始执行的,而FSBL在读取镜像时,由于镜像分区中有PL的比特流,因此会执行复位FPGA和重新加载比特流的处理,这是由于FSBL的固定处理流程导致的。因此如果想彻底杜绝对PL端的复位,就要手动修改FSBL的代码,当判断本次启动如果是由于看门狗复位的话,跳过操作PL的处理

首先在fsbl.h中添加以下内容

1
2
3
4
5
/*
* If reboot status is watchdog, don't reconfig PL
*/
#define FSBL_WDT_SKIP_PL
#define FSBL_WDT_RSR 0xF8F00630

宏定义FSBL_WDT_SKIP_PL用于将我们在FSBL中修改过的部分使用一个宏定义保护起来,确保该功能能够随时启用和关闭,关闭后不影响原本的FSBL处理逻辑。FSBL_WDT_RSR用于获取复位原因

在image_mover.c文件的LoadBootImage()函数中,会循环遍历BOOT.bin的每个分区进行处理,我们需要做的就是在判断是PL比特流分区的时候,跳过本次循环。代码如下,宏定义FSBL_WDT_SKIP_PL中是我添加的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (PartitionAttr & ATTRIBUTE_PL_IMAGE_MASK) {
fsbl_printf(DEBUG_INFO, "Bitstream\r\n");
PLPartitionFlag = 1;
PSPartitionFlag = 0;
BitstreamFlag = 1;
if (ApplicationFlag == 1) {
#ifdef STDOUT_BASEADDRESS
xil_printf("\r\nFSBL Warning !!!"
"Bitstream not loaded into PL\r\n");
xil_printf("Partition order invalid\r\n");
#endif
break;
}

#ifdef FSBL_WDT_SKIP_PL
u32 wdt_rsr = Xil_In32(FSBL_WDT_RSR);
fsbl_printf(DEBUG_INFO, "Watchdog status 0x%x\r\n", wdt_rsr);
if (wdt_rsr & 0x1) {
fsbl_printf(DEBUG_INFO, "Watchdog reboot, skip PL\r\n");
PartitionNum++;
continue;
}
#endif
}

另外,由于FSBL本身的设计是考虑到整个系统已经复位,因此在main()函数初始化阶段,注册的异常回调中,中断IRQ的处理是直接认为发生错误,而如果我们的系统中不复位PL,而PL会有中断上报到PS端的话,这里就会触发FSBL的IRQ异常回调,所以看门狗复位后,需要将异常回调关闭

1
2
3
4
5
6
7
8
9
10
11
#ifdef FSBL_WDT_SKIP_PL
u32 wdt_rsr = Xil_In32(FSBL_WDT_RSR);
fsbl_printf(DEBUG_INFO, "Watchdog reset status 0x%x\r\n", wdt_rsr);
if (wdt_rsr & 0x1) {
fsbl_printf(DEBUG_INFO, "Watchdog reboot, skip handlers\r\n");
} else {
RegisterHandlers();
}
#else
RegisterHandlers();
#endif

至此,我们已经完全实现了Zynq平台仅复位PS不复位PL的功能,然而要实现完全的功能,还需要在应用工程中自己判断启动原因,然后跳过对PL的配置处理