This is the second page in a series about resets in FPGAs. After explaining why asynchronous resets are not what many people think in the previous page, this page discusses the different options for resets and initialization of the FPGA.
First and foremost: What is a reset?
Like, come on, we all know what a reset is! It's like that button on the PC, that you press and everything starts from clean. It's that signal that goes into a chip that ensures that no matter what happened before, from now on everything will be fine. Some people refer to the reset as the signal that brings the system to a known state.
To FPGA designers, the reset is often just that extra input to add to every module and use in a repeated code pattern. Something we just do, not necessarily paying attention to what this reset signal actually ensures, if at all, or if it can be omitted altogether.
The most common misconception is to focus on the fact that the reset signal brings the system to a known state. That's true, of course, but the important part is what happens after the reset is deactivated. The logic must be ensured to begin working in a predictable manner. Or at least, predictable enough to ensure it works properly. It's pointless to reset a system if we're not sure about its behavior after we've deactivated the reset.
What makes this topic difficult in particular is that there's luck involved: Generally speaking, the state of the system when the reset was activated is unknown and random, and so is the timing of the reset's deactivation. An improper handling of the reset signal can therefore result in rare malfunctions that appear in random occasions, which may seem to be a completely different sort of problem. Likewise, it's possible to neglect this issue without any visible consequences, except for occasional problems that are usually treated with witchcraft.
Just like proper timing constraints, proper handling of clocks and clock domains, a proper treatment of the FPGA's wakeup and reset is a necessity to ensure that the FPGA works reliably. Common to all of these topics is that one can sort-of get away with neglecting them, and quite some engineers do, but unfortunately at the cost of an FPGA that behaves like it's haunted every now and then.
Simulation vs. hardware
This page focuses on how decisions about resets influence the design when it's loaded into the FPGA. But obviously, these decisions also have an impact on the simulation of the logic.
It's important to distinguish between these two situations. Simulators assign the X (unknown) initial value to all registers, in particular in behavioral simulation. These X values then propagate to any register that depends on an X value in its logic function. Hence it happens that even a single register with an X value swamps the entire design with X's, and the simulation becomes useless.
A common incorrect solution to this problem is putting an asynchronous reset on all registers in the design, in order to get rid of all X's. This is usually done by enabling the reset briefly at the beginning of the simulation. As a result, all registers get a known value, and everything looks perfect. Unfortunately, this incorrect solution often gives an illusion of a clean start, by hiding the problems discussed in the previous page.
Even if the resets are used correctly, it's still a lazy choice to reset all registers in order to avoid chasing the source of X values in the simulation. This doesn't only waste resources and possibly makes it harder to achieve the timing constraints: Resetting registers unnecessarily can also hide bugs, because a flood of X values may origin from an unintentional dependency from one register to another. Hence this flood of X's can be a warning that something is wrong with the design.
Comparing with a simulation, the hardware is much more forgiving about not using resets. But when resets are used incorrectly, or not used at all when they are necessary, the hardware may behave quite unexpectedly.
In conclusion, resets should be used with the hardware in mind, and not for getting rid of annoying X's during the simulation. And because the focus should be on the hardware, these few words are all I have to say about simulations.
Strategies for resetting
The decision on if and how to apply resets on registers requires considering the case of each register separately. Doing this doesn't just avoid an unnecessarily high fan-out of the reset signals, but it's also a good opportunity to think about whether the logic is guaranteed to start off correctly after the reset, no matter its previous state.
In principle, there are four options:
- Accept that the initial value of the register is unknown.
- Rely on the initial value setting of the FPGA's synchronous element. So the there is no explicit reset.
- Use an asynchronous reset. For example:
always @(posedge clk or negedge resetn) if (!resetn) counter <= 0; else counter <= counter + 1;
- Use a synchronous reset. For example:
always @(posedge clk) if (!resetn) counter <= 0; else counter <= counter + 1;
Each of these options is discussed below, so I'll first present what I consider to be the correct way, and then elaborate:
- Go through each register (and other elements that have a reset input) in the design separately, and check if the first two options make sense. In other words, if a reset can be avoided.
- If not, prefer a synchronous reset.
- Use asynchronous resets only with logic elements that explicitly require that (in particular complex FPGA primitives and IP cores, e.g. transceivers and bus controllers), and even so, try to use the synchronous reset signal if possible.
- Always reset state machines properly. Even if its behavioral definition ensures that it reaches a known state sooner or later, be sure that an explicit and safe reset brings it to a known state. This is mainly because the synthesizer may implement the state variable in an different way due to optimization (one-hot in particular). This representation of the state variable may hence never converge into a legal state unless the state machine is reset explicitly.
- Always reset IP cores, design blocks and primitives that have reset inputs if the documentation requires that (and it's a good idea to do so in either case). They might very well work without the reset, but that can be just pure luck. Even if the reset is related to functionality that you don't use, and even if the reset appears to merely zero some outputs which are unnecessary for your design, the rule is simple: When it comes to resets, follow the documentation carefully. Even if there's no apparent reason why it should be necessary.
These suggestions are pretty much in line with what FPGA vendors recommend these days.
I'll also add a very general guideline: Always reset control logic explicitly and let data paths flush themselves from their initial junk data.
And now the lengthy discussion on each option.
Option #1: Unknown initial value
Some registers don't need any reset nor initial value. This applies in particular to shift registers and other delay elements. Generally speaking, data paths are likely to fit this option.
Consider this snippet:
reg [31:0] d0, d1, d2, d3, d4; always @(posedge clk) begin d4 <= d3; d3 <= d2; d2 <= d1; d1 <= d0; d0 <= orig_data; end
This is clearly five delay registers with 32 bits each. On some FPGAs (Xilinx in particular), the synthesizer detects this as a shift register if only the last value (@d4) is used, and there's no reset on any of these registers. This can reduce the consumption of logic considerably.
Clearly, the logic that is connected to the output of these delay registers must be able to tolerate some initial random data. A typical case when this is no problem is when there's some other register or state machine, that is properly reset and makes sure that uninitialized values are ignored as they arrive. For example, if these delay lines are related to a pipeline, the pipeline's control logic will naturally ignore invalid data.
More generally speaking, the opportunity to not reset nor initialize a register is easily recognized when there's an accompanying flag or state that indicates its validity. Or when there's a clear sequence of first assigning the register with a value and then consuming this value. In short, when it's easy to tell that the register's value is ignored until it has been assigned a proper value.
Another type of unknown initial value is when it may depend on the synthesizer. For example:
reg val; always @(posedge clk) val <= 1;
The synthesizer may decide that @val is a wire that has a constant value of 1. Another possibility is to assign a register with zero as the initial value, so it changes to 1 on the first clock. What actually happens depends on the synthesizer. So even though the value of @val is always known, except for the first clock cycle, the initial value should be considered unknown.
Option #2: FPGA's initial values
The initial value of the basic synchronous elements in an FPGA (commonly flip-flops, shift registers and memories) is given in the configuration bitstream. The well-known use of this feature is to create a ROM by assigning a block RAM with initial values and never write to it.
Another well-known aspect of this feature is that an FPGA typically wakes up from configuration with apparently all registers having the value zero. This is because the synthesizer usually assigns all registers with zero as their initial value, but there can be surprises unless the initial value is set explicitly.
Some synchronous elements, in particular shift registers and dedicated RAM blocks, can be created by describing their behavior in Verilog or VHDL (by inference): The synthesizer usually creates a shift register when the code looks like a delay line. Likewise, a RAM element is created in response to an array. However, if a reset is used on these registers (synchronous reset or asynchronous reset), the synthesizer is prevented from using the logic resources this way. This is because neither shift registers nor RAMs have a reset input that sets the values of the internal memory.
So how are the initial values set? During the configuration process, all synchronous elements are given their initial values just before the FPGA is about to become active, i.e. before the synchronous elements start responding to their clocks and asynchronous reset inputs. For Xilinx devices, this is implemented by virtue of the Global Set Reset (GSR) signal, which puts all synchronous elements in their initial state. After this, Global Write Enable (GWE) is activated, which makes the synchronous elements operate normally.
Since the configuration process is carried out regardless of any of the clocks that are used by the FPGA's application logic, the FPGA's transition into its operational state is asynchronous to any of these clocks. Consequently, the synchronous elements behave exactly as if an asynchronous reset was deactivated regardless to any clock. In other words, it's possible that some synchronous elements respond to the first clock edge that arrives after the transition to operational state, and other synchronous elements miss this clock edge due to a violation of timing. This can cause nasty bugs, as discussed in the first page in this series.
It's important to note that the clocks aren't necessarily stable when the FPGA wakes up. If they are generated by the FPGA's own PLLs, they may violate the timing constraints wildly. As discussed above, this is not a problem if the clock is ignored until it's stable (e.g. by virtue of a clock enable), or if it's known to be stable when the FPGA wakes up. Also, if the design ensures that no synchronous element has a reason to change its value until the clock is stable, it's fine. Otherwise, setting an initial value doesn't guarantee much.
Despite the limitations of setting the initial value, it is good enough in many scenarios, and an explicit reset isn't required. And in some cases, there is simply no choice, because a reset signal isn't available. For example, the logic that creates the FPGA's reset signals immediately after the FPGA wakes up. An example of such logic is shown on the third page of this series.
With most synthesizers, setting a register's initial value is quite simple: Use Verilog's "initial":
reg [15:0] counter; initial counter = 1000;
It may come as a surprise that "initial" can be used in Verilog code for synthesis, but as it turns out, this usage is widely supported. So this keyword is definitely the preferred method if the synthesizer supports it (in other words, this usage of "initial" is explicitly mentioned in the documentation). Besides, the alternative method tends to be vendor-specific, and sometimes even specific to an FPGA family. So even if "initial" isn't always portable, it's probably still the most portable choice.
The alternative method for setting the initial value depends on the FPGA that is used. This method usually consists of an instantiation of the synchronous element as a primitive, and the assignment of the initial value as an instantiation parameter. For example, a Xilinx' flip-flop:
FDCE myflipflop ( .C(clk), .D(in), .Q(out), .CLR(1'b0), .CE(1'b1) ); defparam myflipflop.INIT = 1;
Using "initial" is nicer, isn't it?
Option #3: Asynchronous reset
If you haven't read the page on why asynchronous resets are often used incorrectly, I suggest doing that first. Unless you don't intend to use this kind of reset anyhow.
For some reason, a lot of people consider the asynchronous reset as the right solution for all purposes. Maybe because it often appears in code examples, maybe because of the illusion of a simple way to achieve a global reset that reaches all synchronous elements. Maybe because in the old ASIC world, the asynchronous reset was useful for the chip test in the manufacturing process, because it allows to reset the entire chip and begin applying test vectors.
So to reality: The proper and clean way to use an asynchronous reset is to do that with the clocks turned off. That's what the "asynchronous" part really means. In a practical design, this consists of the following stages:
- Deactivate all clocks (except the clock that is used by the logic that controls this procedure). This deactivation is often done by turning off the clock enable input of the global clock buffers (clock gating).
- Activate and deactivate the asynchronous reset signal. Make sure that the pulse is long enough to reset all synchronous elements.
- Wait enough time to ensure that all synchronous elements are ready for receiving clocks.
- Reactivate the clocks.
This sequence isn't difficult to implement, but it might be harder to ensure that the first edge of each clock is properly formed, so there are no glitches: A common problem with clock buffers is that there's a requirement on the timing between the activation of the clock buffer's output enable and the first clock edge to pass through the clock buffer. If this timing requirement is violated, the clock buffer may output a glitch (a short pulse that violates the FPGA's requirements on the clock). This can cause unpredictable behavior of all synchronous elements that depend on this clock.
Unfortunately, the documentation that is offered by FPGA vendors doesn't always offer an explanation on how to ensure this timing requirement. As a result, it may not be possible to be ensure that the first clock edge after the reset behaves correctly. And without a guarantee on the first clock edge, the reset is pointless.
If you use this method, be sure that no timing constraints are enforced on the paths that are related to the asynchronous reset. Such enforcement is unnecessary in this case, and it may be activated by default.
There's an alternative method for applying an asynchronous reset reliably, which is commonly suggested. This method doesn't involve clock gating, and hence doesn't rely on the clock buffers: The idea is to add a few flip-flops which let through the activation of the asynchronous reset signal directly, but these flip-flops deactivate the reset synchronously. In other words, a synchronized asynchronous reset.
For example, if the original asynchronous reset is @external_resetn, this generates a reset of this sort:
reg pre_rstn1, pre_rstn2; reg resetn; always @(posedge clk or negedge external_resetn) if (!external_resetn) begin resetn <= 0; pre_rstn2 <= 0; pre_rstn1 <= 0; end else begin resetn <= pre_rstn2; pre_rstn2 <= pre_rstn1; pre_rstn1 <= 1; end
Note that @clk is the clock that is used by the synchronous elements that are reset by @resetn.
When @external_resetn is active (i.e. low), all three registers become active (i.e. zero) asynchronously. However when @external_resetn is deactivated, only @pre_rstn1 becomes inactive on the next clock edge, and this propagates to @pre_rstn2 and @resetn on the clocks that follow.
The purpose of the two extra registers is to protect against metastability, so that @resetn deactivates in a safe manner. This is necessary if and when @external_resetn becomes inactive with bad timing relative to @clk, which may result in a metastable condition on @pre_rstn1 (this page explains metastability).
The benefit of this synchronizer is that @external_resetn can be used as an asynchronous reset: It works even if no clock is active. Nevertheless, the synchronous elements receive a reset signal that becomes inactive synchronously, so timing can be ensured.
Almost needless to say, each clock needs its own synchronized asynchronous reset.
It's important to note that generating @resetn as shown above is not enough. The timing constraints for @clk must be enforced on the paths from @resetn to the synchronous elements. The default of some FPGA tools is to ignore the timing of paths that end at a synchronous element's asynchronous reset input. It's may be necessary to make a change in the tool's settings for this purpose.
So if @resetn is used as a regular asynchronous reset, e.g.
always @(posedge clk or negedge resetn) if (!resetn) // Are you sure this path is timed? the_register <= 0; else [ ... ]
then the synchronizer that is shown above is not enough to ensure a reliable recovery from reset. It's your responsibility to verify that the paths that start at @resetn and end at the flip-flops' asynchronous reset inputs are indeed timed.
It's also important to note that this synchronizer doesn't help against glitches on @external_resetn: If the length of @external_resetn's active pulse is shorter than the specification of the FPGA's flip-flops, anything can happen. So @external_resetn must be generated by some logic or external electronics that ensures a long pulse. If this is not possible, the only solution is to synchronize the reset completely, as with this example for @sync_resetn:
reg pre_rstn1, pre_rstn2; reg sync_resetn; always @(posedge clk) if (!external_resetn) begin sync_resetn <= 0; pre_rstn2 <= 0; pre_rstn1 <= 0; end else begin sync_resetn <= pre_rstn2; pre_rstn2 <= pre_rstn1; pre_rstn1 <= 1; end
But this synchronizer ignores @external_resetn if @clk is inactive. This is problematic if the logic is supposed to treat @external_resetn like an asynchronous reset. In other words, that it should work even when the clocks aren't active.
Let's go back to the first synchronizer. What about using @resetn as a plain synchronous reset? Something like this:
always @(posedge clk) // @resetn not in sensitivity list! if (!resetn) the_register <= 0; else [ ... ]
This is more or less fine, because @resetn's deactivation is surely timed by virtue of the timing constraints. However, the asynchronous activation of @resetn is not timed, so the related synchronous elements may behave randomly just before the reset takes effect. It's better to use a fully synchronized reset for this purpose, e.g. @sync_resetn as defined above.
I'll finish this topic with a general comment: I've chosen to use active-low resets in the examples above, mainly because of a tradition that stems from the days when the reset signal was generated with a capacitor that was connected to the power supply voltage through a resistor. As the capacitor had no voltage initially, the reset input was '0'. This capacitor built up enough charge quite soon, and consequently the reset input changed to '1'. This ancient type of power-up reset is the reason many resets are active low to this day.
In conclusion, it is possible to use an asynchronous reset reliably, however achieving this is definitely not as simple as many believe. I've presented two ways to ensure for a reliable asynchronous reset: Either by turning off the clocks temporarily in order to avoid problems with timing, or by using a synchronizer in order to ensure the timing. As usual with FPGAs, timing is the name of the game.
In most designs that rely on an asynchronous reset in the real world, none of these methods is used. As a result, the reliability of the FPGA design depends on pure luck.
Option #4: Synchronous reset
The synchronous reset is most known with the following code pattern:
always @(posedge clk) if (reset) the_register <= 0; else [ ... ]
I'll suggest what I consider a better code pattern further on, but for now we'll stick with this one. Regardless, note that I chose active-high reset here, as this is the more common choice with synchronous resets. Or so is my impression.
A synchronous reset is better than the asynchronous reset in almost all aspects, except for these:
- The synchronous reset signal can reach huge fan-outs, and timing constraints are enforced on all its paths. So this kind of reset can make it difficult to achieve the timing constraints.
- The synchronous reset can't be used when the clock isn't active.
- Some FPGAs have dedicated resources for global routing, which can be used only with asynchronous resets (I'm not sure this is really the case, but since I've seen this mentioned for Intel FPGAs, I added this comment).
Since FPGAs are rarely used with no active clock (unlike ASICs, which traditionally need this for testing), and it's not clear if the issue with dedicated resources for routing even exists, I'll focus on the main topic: Fan-out. Luckily, this is easy to solve.
It's also worth mentioning that the same fan-out problem influences the asynchronous reset just the same, if the synchronizer that was suggested above is used. So the only ones that can truly say that fan-out is a disadvantage of the synchronous reset are those who use the asynchronous reset with the clocks turned off (i.e. gated).
The first thing that comes to mind for solving a fan-out problem is with a synthesizer constraint or attribute to limit the fan-out. This is however the less preferred way, because the synthesizer just duplicates the flip-flop when the limit is reached. Therefore, it often happens that the output of a duplicated flip-flops go to modules with completely different purposes, hence the destinations of this output can be scattered all over the FPGA. This results in long routing and a significant propagation delay.
A simple and efficient solution is to create a local reset for each significant part of the logic. Something like
module medium_sized_module ( input clk, input reset, input [15:0] in_data output [15:0] out_data ); (* dont_touch = "true" *) reg local_reset; reg the_register; always @(posedge clk) local_reset <= reset; always (@posedge clk) if (local_reset) the_register <= 0; else [ ... ]
The idea is that @local_reset is a local copy of @reset (delayed by one clock). Using @local_reset instead of @reset from this module and downwards holds the fan-out at a reasonable level. As the consumers of this local reset are expected to be interconnected tightly anyhow, odds are that they will be placed in a certain region of the FPGA. Hence the local reset won't need to travel long distances across the logic fabric.
It's important to prevent the synthesizer from removing the local reset registers for the sake of logic optimization. This something that the synthesizer usually does when there are registers with identical behavior, even if they belong to different modules. In the example above, Vivado's synthesis attribute is shown, i.e. "dont_touch". Each synthesizer has its own way to do this (for Quartus, the same is achieved with "dont_merge" as the synthesis attribute).
To verify that the synthesizer indeed retained all registers, it's useful to give all these registers the same name (e.g. local_reset as suggested above) and then search for registers with this name in the implemented design.
There's of course no need to create a local reset for each and every module. As an approximate figure, a fan-out of 50-100 is reasonable for a local reset, in particular when it reaches logic elements within a small physical region on the FPGA.
The topic of reducing fan-out is also discussed in the context of timing closure.
More on synchronous resets
A common myth about synchronous resets is that if the synthesizer encounters a Verilog code pattern that matches that of a synchronous reset, it will connect the reset signal to the flip-flop's synchronous reset input. This can be the case, but it will often not be.
This is different from an asynchronous reset, which must be connected to the flip-flop's asynchronous reset input. Otherwise, the reset won't work without a clock.
Synthesizers tend to give no special meaning to the coding pattern, but rather calculate the logic equation that is derived from the Verilog code. Consider this example:
always @(posedge clk) if (reset) the_register <= 0; else if (some_condition) the_register <= !the_register; else if (some_other_condition) the_register <= 0;
One way to read this code is that the always statement begins with a standard code pattern that asks for a synchronous reset, and then comes a specific definition for the register's behavior. Consequently, one could have expected that @reset would be connected to the synchronous reset input of the relevant flip-flop, and also that the output of a logic function (that is implemented as a LUT) would go to the flip-flop's data input.
In reality, synthesizers usually implement the value of @the_register on the next clock as concisely as possible. For example, the flip-flop's reset input may be connected to a logic function (i.e. a LUT) that implements the expression (reset || (some_other_condition && !some_condition) ).
But there an even more interesting possibility: The flip-flop's reset input might not be used at all. Instead, only the data input is used, and the logic function uses the @reset signal as one of its inputs. So if @reset is high, the logic function's output is zero. This way @reset indeed brings @the_register to zero, but it's not treated differently from any other signal.
So to reiterate: Even though the flip-flop has a synchronous reset input, synthesizers tend not to treat the synchronous reset's code pattern specially, and neither treat the reset signal any different from any other signal. The flip-flop's reset input is used in the best way to implement the behavior that is required by the Verilog code. That can sometimes mean to connect the reset input directly to the reset signal, sometimes to some logic function that may involve the reset signal, and sometimes not use the reset input at all. The synthesizer does whatever helps it meet its performance goals better, nothing else.
Users of Xilinx' Vivado can get better control on this issue with two synthesis attributes, which are DIRECT_RESET and EXTRACT_RESET.
I'll wrap this up with another disadvantage of asynchronous resets: In most FPGAs, a flip-flop only has one reset/set input. This input can behave synchronously or asynchronously. If the reset is synchronous, the synthesizer might find tricks to use this input for using less LUTs in the effort to fulfill the requested behavior. A shortcut of this sort is impossible when there is an asynchronous reset. So an asynchronous reset ties the hands of the synthesizer, forcing it to waste more logic resources.
Avoiding accidental freeze of registers
There's a pitfall with the commonly used code patterns for implementing resets, as shown in this code example:
always @(posedge clk or negedge resetn) if (!resetn) begin reg1 <= 0; reg2 <= 0; // Ayeee! Forgot to reset reg3 ! end else begin reg1 <= [ ... ]; reg2 <= [ ... ]; reg3 <= [ ... ]; end
As implied by the comment, @reg3 doesn't appear in the begin-end clause for an active @resetn. As a result, the Verilog code above requires that @reg3 won't change value as long as @resetn is active. It's the same as if @reg3 would be defined with
always @(posedge clk) if (resetn) reg3 <= [ ... ];
or in other words, @resetn functions as a clock enable for @reg3: The clock is effective only when @resetn is high.
Exactly the same happens with synchronous resets:
always @(posedge clk) if (reset) begin reg1 <= 0; reg2 <= 0; // Ayeee! Forgot to reset reg3 ! end else begin reg1 <= [ ... ]; reg2 <= [ ... ]; reg3 <= [ ... ]; end
This is actually easier to understand, because this is just a pair of begin-end clauses, with the second clause coming to effect only when @reset is inactive. So the definition of @reg3 for this example is clearly
always @(posedge clk) if (!reset) reg3 <= [ ... ];
So the obvious (and not necessarily clever) conclusion is not to forget any register in the begin-end clause for the reset. In fact, many FPGA designers reset all registers, whether needed or not, because they believe that's the only way to do it. Or alternatively, they adopt a coding style where each register has its own "always" statement.
But what if you intentionally want to reset some registers and not others?
With an asynchronous reset, only choice is to put those registers in a separate "always" statement. But with a synchronous reset, there's a simple way to work this out:
always @(posedge clk) begin reg1 <= [ ... ]; reg2 <= [ ... ]; reg3 <= [ ... ]; if (reset) begin reg1 <= 0; reg2 <= 0; // I don't want to reset reg3, and that's fine! end end
Instead of an "if (reset)" sentence at the beginning, and having the interesting part under the "else" statement, the "if (reset)" is put last, so the assignments for the reset override everything that came before them.
Note that this is not equivalent to any of the examples above: @reg1 and @reg2 are reset when @reset is active, however @reg3 is not influenced by the reset at all.
If you feel uncomfortable with this alternative way to apply a synchronous reset, I can understand that, and there are a few reasons for it: To begin with, it's generally good practice to stick to commonly used coding patterns in FPGA design. Otherwise, the synthesizer might expose an exotic bug, something that is much less likely to happen with well-established code patterns (see Golden Rule #4). So even though the Verilog standard explicitly requires that this method works, one could argue that it's not necessarily a good idea to rely on this feature.
This is a strong argument, but for what it's worth, I'm here to tell you that I've been using code patterns like this heavily for more than a decade, with a large range of synthesizers. In particular, this is how I define synchronous resets in my own code. I have never had a single problem with this.
Another possible reason for not liking this method is to think that the synthesizer might miss the hint that a synchronous reset is desired, because it's not the regular code pattern. However as already mentioned above, most synthesizers don't take the hint anyway, and consider the synchronous reset as just another definition of the logic's required behavior. So this reason has no grounds.
So if you want to take my word for it that it's safe, you'll save yourself some headache.
Can the same be done with an asynchronous reset? For example, what will this do?
always @(posedge clk or negedge resetn) begin reg1 <= [ ... ]; reg2 <= [ ... ]; if (!resetn) reg1 <= 0; end
This is of course a diversion from the asynchronous reset's common coding pattern. An anecdotal test on Vivado's synthesizer revealed this it got the hint, and assigned an asynchronous reset for @reg1.
But the behavior that is required for @reg2 is can't be achieved on an FPGA: As written above, it means that both @clk and @resetn are clocks, and that @reg2 samples a new value on their rising edges and falling edges, respectively. Since flip-flops with two clock inputs are not available in any FPGA that I know of, there's no possibility for a synthesis of @reg2's definition.
Vivado's synthesizer reacted to this by ignoring the "negedge resetn" part, and created a flip-flop that uses only @clk as a clock. There were no traces of this oddity in the result of the syntheses, and neither did the synthesizer issue any warning or otherwise complain. That's despite the fact that the synthesizer created logic that doesn't behave as defined by the Verilog code.
So specifically with Vivado's synthesizer, the same code pattern actually works even for an asynchronous reset, but this shouldn't be relied upon: The Verilog code should say what you want the logic to do. Otherwise, the synthesizer is perfectly entitled to misinterpret it as it prefers.