01signal.com

timing closure策略

此页面属于关于 timing的一系列页面。前几页解释了 timing 计算背后的理论,讨论了 clock period constraint,并介绍了 timing closure。但是,如果您已尝试将其正确处理,但出现 timing 问题怎么办?此页面试图回答该问题。

介绍

上一页中,我试图说服您,没有单一的方法可以解决 timing closure 问题。有时专注于 critical path是正确的,有时则不然。有时只需更改工具设置即可轻松解决问题,有时则要困难得多。没有什么可以替代利用您拥有的所有经验和智慧来找到问题的根本原因。没有办法用清单来总结 timing closure 。

然而,列出可能的策略通常会有所帮助。因此,在此页面上,我收集了一些在遇到 timing问题时值得思考的主题。如果您阅读本文是因为您有一个特定的 timing 问题需要解决,那么这些想法之一可能会引导您找到解决方案。但不要指望您的解决方案会在此处详细说明。

另请记住,这一系列的页面并没有在这里结束。为了激发动力,我选择在许多其他主题之前讨论 timing closure 。但是,后面几页中的信息也是相关的。

同样的原因,关于 I/O timing constraints 的讨论推迟到后面。目前,我专注于在 FPGA内部开始和结束的 paths 。

因此,这里有一些与 timing closure相关的想法需要考虑。

想法#1: 修复 logic design

这始终是最没有吸引力的解决方案。如果已知 design 可以正常工作,则尤其如此。你不想改变有用的东西。然而,问题的根本原因通常是 Verilog 代码编写得不够好,无法满足所需的性能。 logic design 的改变一劳永逸地解决了这个问题,而不是面临持续的困难。

上一页中有一些编写快速 logic 的建议。值得重复这一点: 在开发过程中始终牢记 timing 。修复 timing 问题比从头开始正确编写 Verilog 代码要难得多。

想法#2: 缩小 fan-out

当 net 具有较高的 fan-out时, propagation delay 增加的原因主要有两个:

如果在 design里面用了一个 synchronous reset ,那么这个 signal 很可能就拥有了一个高 fan-out。该主题将在单独的页面中讨论。

然而,每个达到很多 logic elements 的 signal 都可能由于高 fan-out而导致 timing 问题。有时这个高 fan-out 是显而易见的(例如 clock enable signals),有时则不太容易期望高 fan-out。 FPGA 工具通常可以通过列出具有最高 fan-outs的 nets 来提供帮助。

有两种方法可以使 fan-out 保持低电平:

显然,这两种方法都达到了相同的结果: 高 fan-out 的 register 复制成几台 registers。因此,如果这可以通过工具自动完成(使用第一种方法),为什么还要手动执行此操作(如第二种方法)?

第二种方法需要更多的努力,但有一个显着的优势: 可以以合理的方式复制 register 。请记住,目标不仅仅是减少 fan-out。同样重要的是,每个 register 的 output 被分配到放置在 logic fabric上的一个小区域中的 logic elements 。否则,由于物理距离,结果是大 routing delays 。因此,如果在编写 Verilog 代码时考虑到 fan-out ,则可以确保 logic elements之间的短连接。这在不同页面上针对 synchronous reset signal 进行了演示。

相比之下,如果 FPGA 工具负责复制 registers,结果可能效率不高。 routing delay 的改进取决于决定如何使用每个复制的 register 的算法。结果的质量取决于使用的 FPGA 工具。

请注意,默认情况下,当 synthesizer 检测到两台行为完全相同的 registers 时,这些 registers 会自动合并为一台 register。因此,如果 Verilog 代码中复制了 register ,则 synthesizer 将用一个 register替换所有副本。即使这些等效的 registers 是在不同的 modules中定义的,情况通常也是如此。为了避免这种合并,有必要显式禁用此功能。实现此目的的常见方法是使用 synthesis attributes,例如“dont_touch”、“dont_merge”或“keep”。

想法#3: 检查 floorplanning

默认情况下, logic elements 在 logic fabric 上的放置由 FPGA 工具(更具体地说是 placer)自动确定。但是,可以要求将某些 logic elements 放置在 FPGA的特定区域。也可以请求将特定的 logic element 放置在特定的位置。此类请求称为floorplanning 。这些请求是通过 placement constraints发出的,这些请求通常是具有类似于 timing constraints语法的 Tcl 命令。

在大多数情况下, placement constraints 使实现 timing constraints变得更加困难。第一个原因很明显: 当 placer的选择有限时,结果只能比没有限制的差。但是,还有更具体的原因:

在大多数 designs中,最好避免使用 floorplanning ,从而让 placer 可以自由优化 logic elements的放置。然而,在某些情况下 placement constraints 是常用的,例如:

对于 timing closure,重要的是要意识到 placement constraints 是问题的潜在原因。特别是如果 routing delays 大于预期,根本原因可能是 placer无法优化 logic elements的位置。请记住, floorplanning 可能会对与放置受限的 logic elements 无关的 paths 产生负面影响。

想法#4: 检查 timing constraints

timing constraints 对于 FPGA的可靠运行至关重要。因此,应在项目的 implementation 之前对其进行验证。但是,有时 timing constraints 无论如何都会出错。在 timing closure 过程中,错误会变得很明显。这不应该发生,但是当它发生时,解决问题当然更好。

有一整页关于检查 timing constraints。在这里我只讨论两个可能导致 timing closure出现问题的常见错误:

首先,关于 unrelated clocks: clock domain crossings 的话题前面已经讨论过了: timing constraints 反映哪些 clocks 是 related clocks 哪些不是 related clocks 很重要的原因有很多。最重要的原因是为了保证 logic的正常运行,但是 timing closure 也受到了影响: 如果一对 clocks 被工具不必要地视为 related clocks ,这将导致在这两个 clocks之间的 paths 上不必要地执行 timing 要求。结果,这些工具以真正需要这些努力的 paths 为代价,浪费了这些 paths 的努力。

修复此问题的 timing constraint 将在本系列页面的后面进行说明。

关于 asynchronous resets: 在大多数情况下,有必要在以 asynchronous reset结尾的 path 上强制执行 timing constraints 。但有时,没有必要这样做。例如,如果保证当 reset 变为不活动时, clock 不会处于活动状态。另一种可能性是接收 asynchronous reset 的 flip-flop 具有针对 timing violations的保护机制,就像 clock domain crossing一样。在这些情况下,工具为满足 timing 要求所做的努力毫无意义。

很难意识到这类问题使得实现 timing closure变得困难: 有时 critical path 与不必要地被视为 related clocks的两个 clocks 无关。如果 asynchronous reset 转移了工具的注意力,那就更难识别了。在这种情况下,试图专注于 critical path 以改进其 timing 可能是徒劳的。

timing constraints 当然在其他方面也可能出错。这里描述的情况只是一种可能性。因此, timing 出现问题可能是总体审查 timing constraints 的好机会。

想法#5: 再试一次

回想一下, place and route 过程开始于在 logic fabric上相当任意地散布 logic elements 。这些工具然后通过反复尝试尝试改进 timing 。因此,这个过程的成功依赖于一定的运气。也有可能 place and route 算法稍微不同的行为会获得更好的结果,即使没有逻辑解释原因。

因此,如果 timing constraints 失败,并且负 slack 相对较小(大约是总 delay的 10-20% ),重试可能就足够了。但只是重新运行 implementation 可能不会有任何好处: 大多数 FPGA 软件旨在在使用相同输入运行时准确地重复其结果。所以有必要在重新运行之前改变一些东西。此更改与 critical path无关。关键只是为了避免准确重复之前的 implementation。

例如,在 Vivado 中,每个 run 都有一个名为“strategy”的属性。顾名思义,此属性控制工具在 implementation期间应用的策略。更改此属性可确保下一个 implementation 不会与前一个相同。对于特定的 logic design,不同的策略也可能更有意义。

所有 FPGA 工具都提供类似的可能性来修改 implementation 过程的参数。通常可以要求更高级别的努力来实现 design的目标。有时确实需要更高的努力,但通常要求更高水平的努力只是因为工具可以做其他事情。

避免重复的另一种方法是更改 Verilog 代码。再次说明,更改不一定与 critical path相关。有时更改 register 的名称就足以获得与之前的 implementation 足够不同的 implementation 。

这个想法可以发挥到极致: 可以在多台计算机上并行运行 implementation ,因此每台 implementation 的参数略有不同。当 FPGA 的价格很重要时,这是有道理的。在这种情况下,值得让计算机努力工作,以便在更便宜的 FPGA上实现 timing constraints 。

总结一下,这个方法主要靠运气。相应的期望应该是: 仅当工具偶尔无法实现 timing constraints时,再试一次才有帮助。但如果可能的话,以其他方式改进 timing 总是更好。

想法#6: FPGA 满了吗?

当 FPGA的液位达到大约 70%时, timing constraints 出现问题是很常见的。出现这种情况的主要原因有以下三个:

在上面给出的三个原因中,只有第三个有某种解决方案: 例如,它可能有助于手动决定哪个 logic 使用 FPGA的 block RAMs 和其他类似资源。除此之外,完整的 FPGA 的唯一解决方案是选择更大的。然而,这并不总是一种选择。因此,随着更多 logic 的加入,预计 timing closure 会变得更硬,这一点很重要。

想法#7: 也许您需要不同的 FPGA?

有时,别无选择,只能承认 FPGA 无法胜任这项工作。如果相同的 FPGA 可用于更高的 speed grade,解决方案可以是决定升级到更快的 FPGA。这个决定当然会增加购买成本,但它也会带来另一个意想不到的后果: speed grade更高的 FPGAs 可能会短缺。即使这些更快的 FPGAs 在特定时间充裕,但当供不应求时,更快的 FPGAs 通常第一个从市场上消失。

这是很自然的: 较快的 FPGAs 是那些通过测试较好的,它们总是可以用来代替较慢的 FPGAs。有时制造商无法生产速度更快的 FPGAs,有时会有大量消费者购买它可以使用的所有产品。

因此,如果您长期从事用于生产的产品,请始终选择 design 可以使用的最低 speed grade 。即使钱不是问题也是如此。

一种完全不同的升级类型是从更新的 FPGA 系列中选择 FPGA 。或者选择其他供应商的 FPGA 。这种变化更为剧烈,但如果项目处于早期阶段,则应予以考虑。我们都倾向于爱上我们熟悉的工具和组件。但是,当一切都感觉太熟悉时,就是四处寻找替代方案的好时机。

也就是说,经验丰富的 FPGA 工程师知道选择最新、全新的 FPGA 及其工具是一场危险的赌博。但通常有一个相当成熟的替代方案,它比当前选择的 FPGA好得多。在这种情况下,最好离开舒适区并尝试新事物。

想法#8: 降低温度范围

我几乎把这种可能性放在最后是有原因的: 这是所有解决方案中最丑陋的解决方案。但有时别无选择。

默认情况下,工具对 timing constraints 的强制执行保证 FPGA 在 datasheet中定义的整个温度范围内可靠地工作。一些 FPGA 工具允许为项目选择另一个温度范围(例如, Quartus 有一个名为“MAX_CORE_JUNCTION_TEMP”的属性用于此目的)。这可用于通知工具不需要支持整个温度范围。

一般来说, FPGAs 中 logic elements 的 delays 随温度升高而升高。如果最大温度降低,则 delays 的值在 timing 计算中会更小。这使得工具更容易实现 tsetup 要求。有时,这是使工具实现 timing constraints的唯一方法。

了解使用此方法的风险很重要。特别要注意的是,我们谈论的是 junction temperature。换句话说,这是 FPGA的 silicon 上的温度。这不是环境温度。

因此,当最高温度为 85°C时,并不意味着 FPGA 可以在该温度的烤箱中工作。该温度也可以在室温 (25°C) 下达到,特别是在 FPGA上没有 heatsink 的情况下。 junction 的温度与环境温度之间始终存在差异。这种差异的大小取决于 FPGA 的功耗和冷却解决方案。

如果您使用商业产品,请注意 FPGA 的环境温度可能比室温高得多。特别是,如果 FPGA 放在通风不好的箱子里,箱内的温度会比箱外的温度高很多。更糟糕的是,大多数电子产品的工作温度预计在 0°C 和 40°C 之间(大约,这些数字因产品而异)。那么当最终产品在最高温度下进行测试时, FPGA 在产品外壳内, junction的温度是多少?这就是要问的问题。 timing 计算必须基于该温度(或更高)。

换句话说,如果为了实现 timing constraints 而降低最高温度,并且在实验室中一切正常,那没有任何意义。回想一下,关于 timing constraints, FPGA design 在实验室中工作始终毫无意义。但降低最高温度在这方面更糟。不加考虑地缩小温度范围可能会招来真正的麻烦: 生产前的产品最终测试,在最高温度下进行可能会失败,如果温度范围被修正,则不可能达到 timing constraints 。解决此问题的唯一方法是从头重写 FPGA design 。

因此,在为 timing closure更改温度范围之前,请确保这样做是安全的: 在所有可能的工作条件下,对 junction 的温度范围进行严格评估。

请注意,无法通过更改 implementation的参数将温度范围扩展到默认值之外。该工具的默认温度范围始终与 datasheet中规定的相同。因此,无法保证 FPGA 在超出此默认温度范围的情况下可靠运行。

想法#9: 未对齐的 related clocks

这是一个比较深奥的情况,也有点难以理解。所以这就是为什么我把这个话题放在最后。

假设两个未对齐的 related clocks 之间有一个 clock domain crossing 。也就是说,这两个 clocks 是从同一个 reference clock派生出来的,但是这两个 clocks之间并没有控制 clock skew 的机制。

结果,工具变得更难满足这两个 clocks之间的 paths 的 timing 要求。有两种可能的困难: (回想一下之前解释过 tsetup 和 thold

需要注意的是,当工具需要比平时更努力地工作来克服这些困难时,可能会以优化其他 paths为代价。

但是未对齐的 related clocks 并不是错误,有时这是不可避免的。这种情况只意味着工具需要更加努力地工作。如果实现了 timing constraints ,那么 design就没有问题了。但是,应尽可能通过合理的努力避免这种情况。

请注意,上面“想法 #5”中提到的错误是不同的,尽管这两个错误都会对工具造成不必要的困难,并且都与 clock domain crossing相关。

解决 related clocks 未对齐情况的最佳解决方案是对齐这些 clocks。这通常是通过在现有 PLL上添加 PLL 或添加 clock output 来完成的。目标是所讨论的两个 clocks 都是同一个 PLL的 outputs 。

另一种可能的解决方案是将 clocks 视为 unrelated clocks。这需要对 logic design 本身以及 timing constraints进行更改。如果这不是太困难,那么这可能是值得的。稍后将详细介绍此主题

我们现在将查看 related clocks 之间未对齐的 clock domain crossing 示例。

wire       pll_clk;

   reg [24:0] result;
   reg [11:0] x, y, x1, y1;

   clk_wiz_0 pll_i
   (.clk_in1(clk),
    .clk_out1(pll_clk));

   always @(posedge clk)
     begin
	x1 <= x;
	y1 <= y;
     end

   always @(posedge pll_clk)
     result <= x1 * y1;

请注意,有一个 PLL (clk_wiz_0)。这个 PLL 使用 @clk 作为它的 reference clock,其频率为 250 MHz。它与上一页顶部示例中显示的 PLL 相同。 @pll_clk 的频率是 125 MHz。

此示例的重要部分是两个 related clocks (@clk 和 @pll_clk)之间的 clock domain crossing 。因为 PLL只生成了 @pll_clk ,所以这两个 clocks 没有对齐。所以在 paths 到 @result (从 @x1 和 @y1)中有一个 clock skew 。尽管是 clock skew, clocks 还是 related clocks,工具会尽量满足 timing 的要求。

如果使用 @clk 的唯一原因是需要 250 MHz 频率的 clock ,则正确的解决方案是使用 PLL生成另一个 clock 。生产一个和 reference clock同频的 clock 不算浪费资源。相反,这样做可以为工具省去很多力气。如示例所示,直接使用 @clk 的理由只有一个: 当使用 @clk 的 logic 必须在 PLL 生成可用的 clocks之前工作时。

Vivado 生成的 timing report 如下。在这个特定案例中,只有 tsetup 要求存在问题。

Slack (VIOLATED) :        -1.456ns  (required time - arrival time)
  Source:                 x1_reg[7]/C
                            (rising edge-triggered cell FDRE clocked by clk  {rise@0.000ns fall@2.000ns period=4.000ns})
  Destination:            result_reg/DSP_OUTPUT_INST/ALU_OUT[10]
                            (rising edge-triggered cell DSP_OUTPUT clocked by clk_out1_clk_wiz_0  {rise@0.000ns fall@4.000ns period=8.000ns})
  Path Group:             clk_out1_clk_wiz_0
  Path Type:              Setup (Max at Slow Process Corner)
  Requirement:            4.000ns  (clk_out1_clk_wiz_0 rise@8.000ns - clk rise@4.000ns)
  Data Path Delay:        3.012ns  (logic 2.677ns (88.878%)  route 0.335ns (11.122%))
  Logic Levels:           5  (DSP_A_B_DATA=1 DSP_ALU=1 DSP_M_DATA=1 DSP_MULTIPLIER=1 DSP_PREADD_DATA=1)
  Clock Path Skew:        -2.192ns (DCD - SCD + CPR)
    Destination Clock Delay (DCD):    0.998ns = ( 8.998 - 8.000 )
    Source Clock Delay      (SCD):    3.202ns = ( 7.202 - 4.000 )
    Clock Pessimism Removal (CPR):    0.012ns
  Clock Uncertainty:      0.148ns  ((TSJ^2 + DJ^2)^1/2) / 2 + PE
    Total System Jitter     (TSJ):    0.071ns
    Discrete Jitter          (DJ):    0.103ns
    Phase Error              (PE):    0.086ns
  Clock Net Delay (Source):      1.414ns (routing 0.002ns, distribution 1.412ns)
  Clock Net Delay (Destination): 1.184ns (routing 0.002ns, distribution 1.182ns)
  Clock Domain Crossing:  Inter clock paths are considered valid unless explicitly excluded by timing constraints such as set_clock_groups or set_false_path.

    Location             Delay type                Incr(ns)  Path(ns)    Netlist Resource(s)
  -------------------------------------------------------------------    -------------------
                         (clock clk rise edge)        4.000     4.000 r
    AG12                                              0.000     4.000 r  clk (IN)
                         net (fo=0)                   0.000     4.000    clk_IBUF_inst/I
    AG12                 INBUF (Prop_INBUF_HRIO_PAD_O)
                                                      0.738     4.738 r  clk_IBUF_inst/INBUF_INST/O
                         net (fo=1, routed)           0.105     4.843    clk_IBUF_inst/OUT
    AG12                 IBUFCTRL (Prop_IBUFCTRL_HRIO_I_O)
                                                      0.049     4.892 r  clk_IBUF_inst/IBUFCTRL_INST/O
                         net (fo=1, routed)           0.795     5.687    clk_IBUF
    BUFGCE_X1Y2          BUFGCE (Prop_BUFCE_BUFGCE_I_O)
                                                      0.101     5.788 r  clk_IBUF_BUFG_inst/O
    X2Y0 (CLOCK_ROOT)    net (fo=62, routed)          1.414     7.202    clk_IBUF_BUFGCE
    SLICE_X52Y45         FDRE                                         r  x1_reg[7]/C
  -------------------------------------------------------------------    -------------------
    SLICE_X52Y45         FDRE (Prop_HFF_SLICEM_C_Q)
                                                      0.138     7.340 f  x1_reg[7]/Q
                         net (fo=1, routed)           0.335     7.675    result_reg/A[7]
    DSP48E2_X8Y18        DSP_A_B_DATA (Prop_DSP_A_B_DATA_DSP48E2_A[7]_A2_DATA[7])
                                                      0.396     8.071 r  result_reg/DSP_A_B_DATA_INST/A2_DATA[7]
                         net (fo=1, routed)           0.000     8.071    result_reg/DSP_A_B_DATA.A2_DATA<7>
    DSP48E2_X8Y18        DSP_PREADD_DATA (Prop_DSP_PREADD_DATA_DSP48E2_A2_DATA[7]_A2A1[7])
                                                      0.182     8.253 r  result_reg/DSP_PREADD_DATA_INST/A2A1[7]
                         net (fo=1, routed)           0.000     8.253    result_reg/DSP_PREADD_DATA.A2A1<7>
    DSP48E2_X8Y18        DSP_MULTIPLIER (Prop_DSP_MULTIPLIER_DSP48E2_A2A1[7]_U[10])
                                                      0.994     9.247 f  result_reg/DSP_MULTIPLIER_INST/U[10]
                         net (fo=1, routed)           0.000     9.247    result_reg/DSP_MULTIPLIER.U<10>
    DSP48E2_X8Y18        DSP_M_DATA (Prop_DSP_M_DATA_DSP48E2_U[10]_U_DATA[10])
                                                      0.164     9.411 r  result_reg/DSP_M_DATA_INST/U_DATA[10]
                         net (fo=1, routed)           0.000     9.411    result_reg/DSP_M_DATA.U_DATA<10>
    DSP48E2_X8Y18        DSP_ALU (Prop_DSP_ALU_DSP48E2_U_DATA[10]_ALU_OUT[10])
                                                      0.803    10.214 r  result_reg/DSP_ALU_INST/ALU_OUT[10]
                         net (fo=1, routed)           0.000    10.214    result_reg/DSP_ALU.ALU_OUT<10>
    DSP48E2_X8Y18        DSP_OUTPUT                                   r  result_reg/DSP_OUTPUT_INST/ALU_OUT[10]
  -------------------------------------------------------------------    -------------------

                         (clock clk_out1_clk_wiz_0 rise edge)
                                                      8.000     8.000 r
    BUFGCE_X1Y2          BUFGCE                       0.000     8.000 r  clk_IBUF_BUFG_inst/O
                         net (fo=62, routed)          1.078     9.078    pll_i/inst/clk_in1
    MMCME3_ADV_X1Y0      MMCME3_ADV (Prop_MMCME3_ADV_CLKIN1_CLKOUT0)
                                                     -1.777     7.301 r  pll_i/inst/mmcme3_adv_inst/CLKOUT0
                         net (fo=1, routed)           0.422     7.723    pll_i/inst/clk_out1_clk_wiz_0
    BUFGCE_X1Y0          BUFGCE (Prop_BUFCE_BUFGCE_I_O)
                                                      0.091     7.814 r  pll_i/inst/clkout1_buf/O
    X2Y0 (CLOCK_ROOT)    net (fo=6, routed)           1.184     8.998    result_reg/CLK
    DSP48E2_X8Y18        DSP_OUTPUT                                   r  result_reg/DSP_OUTPUT_INST/CLK
                         clock pessimism              0.012     9.010
                         clock uncertainty           -0.148     8.862
    DSP48E2_X8Y18        DSP_OUTPUT (Setup_DSP_OUTPUT_DSP48E2_CLK_ALU_OUT[10])
                                                     -0.104     8.758    result_reg/DSP_OUTPUT_INST
  -------------------------------------------------------------------
                         required time                          8.758
                         arrival time                         -10.214
  -------------------------------------------------------------------
                         slack                                 -1.456

此报告显示该工具未能实现 timing constraints。所示的 path 从 @clk的 rising edge 在 4 ns开始,到 @pll_clk的 rising edge 在 8 ns结束。问题是 input pin 的 clock 到达第一个 flip-flop的 clock input pin所花费的时间: 3.2 ns。因此,此 clock edge 的到达时间为 7.2 ns。

但是 @pll_clk 是由 PLL生成的,所以这个 clock 和 @clk的 input pin是对齐的。因此 delay 只是 1.0 ns。 @pll_clk到达第二个 flip-flop 的时间因此是 9.0 ns。所以留给 data path 的时间是 9.0 – 7.2 = 1.8 ns (大约,因为 clock uncertainty 等)。这对于算术乘法是不够的,即使使用了指定的算术单元。因此无法满足 timing 要求。

因此,在此示例中,由于 clock skew, clock 晚于第一个 flip-flop 到达。这导致无法满足 tsetup 要求。

请注意,这可以通过操纵 @pll_clk的对齐方式来解决。例如, PLL的 reference clock 可以是 global clock buffer 的 output 分配 @clk。也可以定义 PLL 的 phase shift 以实现更好的对齐。然而,这些解决方案只能作为最后的手段使用。

概括

再一次,这些只是可能有助于解决 timing closure 问题的一些想法。不幸的是,解决这类问题可能需要的远不止这些。事实上, FPGAs 领域中没有一个主题与 timing closure不相关。

如前所述,最好的策略是从一开始就小心编写 logic design 。解决 timing closure 的最佳方法是避免它。


到此为止,这一系列的页面都讨论了 timing,但没有说太多关于 timing constraints的内容。这即将改变下一页开始,讨论变得更加技术化。

此页面由英文自动翻译。 如果有不清楚的地方,请参考原始页面
Copyright © 2021-2024. All rights reserved. (6f913017)