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 | typedef struct |
Disable和Interrupt Status两个寄存器仅用于将看门狗当作普通定时器的场景,这里不需要操作。Load加载寄存器用于存放定时器的定时值,每次写Load寄存器时会自动触发将Load的值拷贝到Counter寄存器中,当通过Control寄存器使能看门狗工作后,看门狗就开始自动将Counter的计数值自减,减到0时就触发复位。软件上只需要操作Load和Control两个寄存器,在初始化时通过配置Control来设置定时数值的精度以及设置看门狗模式,并设置一个初始的Load数值,然后在喂狗任务中启动定时器,再定时喂狗即可,伪代码如下
1 |
|
以上伪代码在初始化时先禁用了看门狗,设置为看门狗模式,通过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 | void watchdog_feed(uint32_t timeout_ms) |
主动触发复位
当完成以上工作后,如果一切正常,那么程序启动后由于喂狗任务的存在,每当定时器还未到期之前就已经被重新喂狗,因此不会触发复位,如果想要主动触发复位,可以通过外部的一个指令来修改定时器的超时时间小于喂狗时间,那么下一次定时器会在喂狗周期之前定时到期,触发复位。程序中也可以通过触发断言、死循环来让喂狗程序得不到执行的方式来触发复位,进行复位功能测试
仅复位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 |
|
获取复位状态
当PS复位重启后,程序中必定需要知道本次启动是否是由于看门狗复位引起的,那么从哪里获取这个信息呢?Watchdog Reset Status Register就是做这个事情的,读取该寄存器,如果数值为1,表明是由于看门狗复位的,如果数值为0,表明未发生看门狗复位,写1会清除该标志位
1 | int is_watchdog_reset() |
FSBL的处理
当做完以上处理后,触发看门狗复位后,会发现PL端仍然被复位了,原因是虽然看门狗设置的复位方式是仅复位PS,但是PS复位后是从FSBL开始执行的,而FSBL在读取镜像时,由于镜像分区中有PL的比特流,因此会执行复位FPGA和重新加载比特流的处理,这是由于FSBL的固定处理流程导致的。因此如果想彻底杜绝对PL端的复位,就要手动修改FSBL的代码,当判断本次启动如果是由于看门狗复位的话,跳过操作PL的处理
首先在fsbl.h中添加以下内容
1 | /* |
宏定义FSBL_WDT_SKIP_PL
用于将我们在FSBL中修改过的部分使用一个宏定义保护起来,确保该功能能够随时启用和关闭,关闭后不影响原本的FSBL处理逻辑。FSBL_WDT_RSR
用于获取复位原因
在image_mover.c文件的LoadBootImage()
函数中,会循环遍历BOOT.bin的每个分区进行处理,我们需要做的就是在判断是PL比特流分区的时候,跳过本次循环。代码如下,宏定义FSBL_WDT_SKIP_PL中是我添加的代码
1 | if (PartitionAttr & ATTRIBUTE_PL_IMAGE_MASK) { |
另外,由于FSBL本身的设计是考虑到整个系统已经复位,因此在main()函数初始化阶段,注册的异常回调中,中断IRQ的处理是直接认为发生错误,而如果我们的系统中不复位PL,而PL会有中断上报到PS端的话,这里就会触发FSBL的IRQ异常回调,所以看门狗复位后,需要将异常回调关闭
1 |
|
至此,我们已经完全实现了Zynq平台仅复位PS不复位PL的功能,然而要实现完全的功能,还需要在应用工程中自己判断启动原因,然后跳过对PL的配置处理